feat(上下文): 复利机制批次2——混合检索偏好重排:三信号分列、版本化权重、有界惩罚与探索位
This commit is contained in:
parent
d41a9b5ea4
commit
f0a61286df
@ -5740,6 +5740,17 @@
|
||||
"default": 10,
|
||||
"title": "Limit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"maxLength": 100,
|
||||
"default": "",
|
||||
"title": "Domain"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -21307,6 +21318,11 @@
|
||||
"title": "Index Generation",
|
||||
"default": 0
|
||||
},
|
||||
"weight_version": {
|
||||
"type": "string",
|
||||
"title": "Weight Version",
|
||||
"default": ""
|
||||
},
|
||||
"种类": {
|
||||
"type": "string",
|
||||
"title": "种类",
|
||||
|
||||
129
src/muse/上下文/偏好重排.py
Normal file
129
src/muse/上下文/偏好重排.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""偏好重排:挑选/消费信号 + 版本化权重 → 混合检索的偏好路;纯函数,可回放可审计。
|
||||
|
||||
合同见模块设计/B09-上下文.md「偏好重排通路」:
|
||||
- 弱强信号分列计数,不混算;"种类→权重"映射由版本化权重表承担。
|
||||
- 偏好路相对关键词/向量双路(各 1.0)初始权重保守(0.25)。
|
||||
- 正偏好进第三 RRF 路;负偏好按有界惩罚从融合分中扣减,
|
||||
惩罚量级不超过偏好路末位贡献,保证弱信号不能反转强相关。
|
||||
- 保留探索位:未被探索过的高相关版本注入候选,对抗熟悉度泡沫。
|
||||
- v1 不做权重自动调参:权重表只经人工审查的版本号变更。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
权重表: dict[str, Any] = {
|
||||
"version": "preference-rerank-v1",
|
||||
"种类权重": {"选用": 0.05, "拒绝": -0.08, "消费": 0.10},
|
||||
"路权重": 0.25,
|
||||
"探索位": 2,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 偏好信号:
|
||||
"""单个方法版本的信号聚合;弱信号(挑选)与强信号(消费)分列。"""
|
||||
|
||||
选用次数: int = 0
|
||||
拒绝次数: int = 0
|
||||
消费次数: int = 0
|
||||
选中位次: tuple[int, ...] = ()
|
||||
|
||||
|
||||
def 合并信号表(挑选信号: list[dict], 消费统计: Mapping[str, int]) -> dict[str, 偏好信号]:
|
||||
"""把 B07 挑选信号行与 B04 消费统计合并为版本→信号表;纯函数。"""
|
||||
表: dict[str, 偏好信号] = {}
|
||||
for 行 in 挑选信号:
|
||||
vid = str(行["method_version_id"])
|
||||
表[vid] = 偏好信号(
|
||||
选用次数=int(行.get("选用次数", 0)),
|
||||
拒绝次数=int(行.get("拒绝次数", 0)),
|
||||
选中位次=tuple(行.get("选中位次", ())),
|
||||
)
|
||||
for vid, 次数 in 消费统计.items():
|
||||
既有 = 表.get(str(vid))
|
||||
if 既有 is None:
|
||||
表[str(vid)] = 偏好信号(消费次数=int(次数))
|
||||
else:
|
||||
表[str(vid)] = 偏好信号(
|
||||
选用次数=既有.选用次数,
|
||||
拒绝次数=既有.拒绝次数,
|
||||
消费次数=int(次数),
|
||||
选中位次=既有.选中位次,
|
||||
)
|
||||
return 表
|
||||
|
||||
|
||||
def 偏好值(信号: 偏好信号, *, 权重: Mapping[str, Any] = 权重表) -> float:
|
||||
种 = 权重["种类权重"]
|
||||
return 信号.选用次数 * 种["选用"] + 信号.拒绝次数 * 种["拒绝"] + 信号.消费次数 * 种["消费"]
|
||||
|
||||
|
||||
def 构建偏好路(信号表: Mapping[str, 偏好信号]) -> dict[str, float]:
|
||||
"""只有正偏好进 RRF 第三路;无信号或负值不占路。"""
|
||||
return {vid: 偏好值(信) for vid, 信 in 信号表.items() if 偏好值(信) > 0}
|
||||
|
||||
|
||||
def 应用拒绝惩罚(
|
||||
融合: dict[str, float], 信号表: Mapping[str, 偏好信号], *, 权重: Mapping[str, Any] = 权重表
|
||||
) -> dict[str, float]:
|
||||
"""负偏好按有界值从融合分扣减;上界为偏好路末位贡献(路权重/61)。"""
|
||||
路 = float(权重["路权重"])
|
||||
结果 = dict(融合)
|
||||
for vid, 信 in 信号表.items():
|
||||
值 = 偏好值(信, 权重=权重)
|
||||
if 值 < 0 and vid in 结果:
|
||||
结果[vid] -= 路 * (-值 / (1 + -值)) / 61.0
|
||||
return 结果
|
||||
|
||||
|
||||
def 注入探索位(
|
||||
排名: list[tuple[str, float]],
|
||||
信号表: Mapping[str, 偏好信号],
|
||||
数量: int,
|
||||
*,
|
||||
探索位数: int | None = None,
|
||||
) -> tuple[list[tuple[str, float]], frozenset[str]]:
|
||||
"""保证头部结果含至多 N 个未探索版本;返回新排名与探索位版本集。
|
||||
|
||||
未探索 = 无任何挑选或消费信号。头部已够则不动;不足时从尾部按分
|
||||
取未探索候选,替换头部末尾的已探索项;确定性、不改分数。
|
||||
"""
|
||||
位数 = 权重表["探索位"] if 探索位数 is None else 探索位数
|
||||
if 位数 <= 0 or 数量 <= 0 or not 排名:
|
||||
return 排名, frozenset()
|
||||
|
||||
def 未探索(vid: str) -> bool:
|
||||
return vid not in 信号表
|
||||
|
||||
头部, 尾部 = 排名[:数量], 排名[数量:]
|
||||
已有 = sum(1 for vid, _ in 头部 if 未探索(vid))
|
||||
if 已有 >= min(位数, len(头部)):
|
||||
return 排名, frozenset(vid for vid, _ in 头部 if 未探索(vid))
|
||||
候选 = [(vid, 分) for vid, 分 in 尾部 if 未探索(vid)][: 位数 - 已有]
|
||||
if not 候选:
|
||||
return 排名, frozenset(vid for vid, _ in 头部 if 未探索(vid))
|
||||
结果: list[tuple[str, float]] = []
|
||||
待换 = len(候选)
|
||||
for 项 in reversed(头部):
|
||||
if 待换 > 0 and not 未探索(项[0]):
|
||||
待换 -= 1
|
||||
continue
|
||||
结果.append(项)
|
||||
结果.reverse()
|
||||
结果.extend(候选)
|
||||
return 结果, frozenset(vid for vid, _ in 结果 if 未探索(vid))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"权重表",
|
||||
"偏好信号",
|
||||
"合并信号表",
|
||||
"偏好值",
|
||||
"构建偏好路",
|
||||
"应用拒绝惩罚",
|
||||
"注入探索位",
|
||||
]
|
||||
@ -10,6 +10,7 @@ from muse.上下文.任务范围 import (
|
||||
)
|
||||
from muse.上下文.依赖校验 import 依赖重验检查, 核对依赖
|
||||
from muse.上下文.偏好材料 import 偏好绑定, 核对范围偏好
|
||||
from muse.上下文.偏好重排 import 偏好信号, 合并信号表, 权重表
|
||||
from muse.上下文.冻结快照 import (
|
||||
快照身份,
|
||||
快照载荷,
|
||||
@ -71,6 +72,9 @@ __all__ = [
|
||||
"核对回放字段策略",
|
||||
"保护模型发送",
|
||||
"混合检索",
|
||||
"合并信号表",
|
||||
"权重表",
|
||||
"偏好信号",
|
||||
"重建方法索引",
|
||||
"方法索引操作",
|
||||
"登记方法索引参与者",
|
||||
|
||||
@ -6,8 +6,17 @@ work_id 只用于绑定过滤提示,不进入事实存储。
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from muse.上下文.偏好重排 import (
|
||||
偏好信号,
|
||||
偏好值,
|
||||
应用拒绝惩罚,
|
||||
权重表,
|
||||
构建偏好路,
|
||||
注入探索位,
|
||||
)
|
||||
from muse.上下文.模型 import 上下文错误
|
||||
from muse.上下文.索引维护 import 索引字节上限, 索引数量上限, 读取新鲜索引
|
||||
from muse.基础设施.检索.关键词 import 关键词得分
|
||||
@ -26,10 +35,33 @@ def _rrf(得分表列: list[dict[str, float]], 权重: list[float]) -> dict[str,
|
||||
return 融合
|
||||
|
||||
|
||||
def _偏好呈现(信号: 偏好信号 | None) -> dict[str, Any]:
|
||||
"""单候选的偏好分项;信号快照随回执留痕,支撑重排可审计。"""
|
||||
if 信号 is None:
|
||||
return {"选用次数": 0, "拒绝次数": 0, "消费次数": 0, "偏好值": 0.0}
|
||||
return {
|
||||
"选用次数": 信号.选用次数,
|
||||
"拒绝次数": 信号.拒绝次数,
|
||||
"消费次数": 信号.消费次数,
|
||||
"偏好值": round(偏好值(信号), 6),
|
||||
}
|
||||
|
||||
|
||||
def 混合检索(
|
||||
连, 查询: str, 嵌入器: 嵌入器, *, work_id: str = "", 数量: int = 10, 作者: str | None = None
|
||||
连,
|
||||
查询: str,
|
||||
嵌入器: 嵌入器,
|
||||
*,
|
||||
work_id: str = "",
|
||||
数量: int = 10,
|
||||
作者: str | None = None,
|
||||
偏好信号表: Mapping[str, 偏好信号] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""整体字节损坏拒绝;有界候选的当前失效单独呈现,不扩大为全库不可用。"""
|
||||
"""整体字节损坏拒绝;有界候选的当前失效单独呈现,不扩大为全库不可用。
|
||||
|
||||
偏好信号表为空时不启用重排,回执与双路等权融合完全一致;
|
||||
启用时正偏好进第三 RRF 路、负偏好有界惩罚,并保留探索位。
|
||||
"""
|
||||
|
||||
if not isinstance(查询, str) or not 查询.strip():
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "方法检索需要明确的非空查询")
|
||||
@ -63,10 +95,22 @@ def 混合检索(
|
||||
向量 = {str(r["method_version_id"]): r["vector"] for r in 行}
|
||||
量分 = 向量得分(嵌入器.嵌入(查询), 向量)
|
||||
融合 = _rrf([词分, 量分], [1.0, 1.0])
|
||||
重排启用 = bool(偏好信号表)
|
||||
探索集: frozenset[str] = frozenset()
|
||||
if 重排启用 and 偏好信号表 is not None:
|
||||
偏好路 = 构建偏好路(偏好信号表)
|
||||
得分表列: list[dict[str, float]] = [词分, 量分]
|
||||
路权重: list[float] = [1.0, 1.0]
|
||||
if 偏好路:
|
||||
得分表列.append(偏好路)
|
||||
路权重.append(float(权重表["路权重"]))
|
||||
融合 = 应用拒绝惩罚(_rrf(得分表列, 路权重), 偏好信号表)
|
||||
绑定集 = set()
|
||||
if work_id:
|
||||
绑定集 = {str(b["version_id"]) for b in 读取生效绑定(连, work_id)}
|
||||
排名 = sorted(融合.items(), key=lambda kv: (-kv[1], kv[0]))
|
||||
if 重排启用 and 偏好信号表 is not None:
|
||||
排名, 探索集 = 注入探索位(排名, 偏好信号表, 数量)
|
||||
索引行 = {str(条["method_version_id"]): 条 for 条 in 行}
|
||||
来源缓存: dict = {}
|
||||
剩余字节 = 索引字节上限
|
||||
@ -109,6 +153,14 @@ def 混合检索(
|
||||
"向量得分": round(量分.get(version_id, 0.0), 4),
|
||||
"已绑定本书": str(version_id) in 绑定集,
|
||||
"来源回读": 版本["来源回读"],
|
||||
**(
|
||||
{
|
||||
"探索位": str(version_id) in 探索集,
|
||||
"偏好": _偏好呈现(偏好信号表.get(str(version_id)) if 偏好信号表 else None),
|
||||
}
|
||||
if 重排启用
|
||||
else {}
|
||||
),
|
||||
}
|
||||
)
|
||||
if len(结果) == 数量:
|
||||
@ -132,6 +184,7 @@ def 混合检索(
|
||||
else ("" if 结果 else "方法检索没有命中可授权卡"),
|
||||
"empty_reason": None if 结果 else ("index_pending" if 待更新 else "no_match"),
|
||||
**状态,
|
||||
**({"重排权重版本": 权重表["version"]} if 重排启用 else {}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -159,6 +159,7 @@ class 参考挑选反馈请求:
|
||||
work_id: str = ""
|
||||
quote: str = ""
|
||||
index_generation: int = 0
|
||||
weight_version: str = ""
|
||||
种类: str = "参考挑选"
|
||||
|
||||
def __post_init__(self):
|
||||
@ -181,6 +182,7 @@ class 参考挑选反馈请求:
|
||||
type(self.index_generation) is int and self.index_generation >= 0,
|
||||
"索引代次须为非负整数",
|
||||
)
|
||||
_需要(len(self.weight_version) <= 64, "重排权重版本标识不超64字")
|
||||
_需要(len(self.quote) <= 2000, "作者原话不超2000字")
|
||||
|
||||
|
||||
|
||||
@ -182,9 +182,12 @@ def 检索方法(
|
||||
q: str = Query(min_length=1),
|
||||
work_id: str = Query(default=""),
|
||||
limit: int = Query(default=10, ge=1, le=50),
|
||||
domain: str = Query(default="", max_length=100),
|
||||
):
|
||||
装配 = request.app.state.装配
|
||||
return 装配.要求知识方法().检索方法(创作身份(作者, 装配.配置), q, work_id=work_id, 数量=limit)
|
||||
return 装配.要求知识方法().检索方法(
|
||||
创作身份(作者, 装配.配置), q, work_id=work_id, 数量=limit, 领域=domain
|
||||
)
|
||||
|
||||
|
||||
@路由.post("/methods/reindex", operation_id="rebuild_method_index")
|
||||
|
||||
@ -422,5 +422,16 @@ class 方法存储:
|
||||
(method_version_id, 数量),
|
||||
).fetchall()
|
||||
|
||||
def 统计消费(
|
||||
self, 作者: str, *, 种类: tuple[str, ...] = ("generation", "review", "planning")
|
||||
) -> dict[str, int]:
|
||||
"""按方法版本聚合真实创作消费计数;启用验证消费不算偏好信号。"""
|
||||
行 = self._查(
|
||||
"SELECT method_version_id::text AS vid, count(*) AS n FROM muse_method_usage "
|
||||
"WHERE created_by=%s AND kind = ANY(%s) GROUP BY method_version_id",
|
||||
(作者, list(种类)),
|
||||
).fetchall()
|
||||
return {r["vid"]: int(r["n"]) for r in 行}
|
||||
|
||||
|
||||
__all__ = ["方法存储"]
|
||||
|
||||
@ -35,7 +35,7 @@ from muse.知识方法.模型 import (
|
||||
绑定命令,
|
||||
证据引用,
|
||||
)
|
||||
from muse.知识方法.消费记录 import 记录消费
|
||||
from muse.知识方法.消费记录 import 记录消费, 读取消费统计
|
||||
from muse.知识方法.绑定身份 import 编码绑定身份
|
||||
from muse.知识方法.评测材料 import 核对方法评测投影, 读取方法评测材料
|
||||
|
||||
@ -261,15 +261,28 @@ class 知识方法服务:
|
||||
|
||||
# ---- 检索与索引 ----
|
||||
|
||||
def 检索方法(self, 身份, 查询: str, *, work_id: str = "", 数量: int = 10) -> dict:
|
||||
def 检索方法(
|
||||
self, 身份, 查询: str, *, work_id: str = "", 数量: int = 10, 领域: str = ""
|
||||
) -> dict:
|
||||
self._核对作者(身份)
|
||||
from muse.上下文.接口 import 混合检索
|
||||
from muse.上下文.接口 import 合并信号表, 混合检索
|
||||
from muse.作品规划.接口 import 读取作品范围
|
||||
from muse.作者经验.接口 import 读取挑选信号
|
||||
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
if work_id:
|
||||
读取作品范围(连, 身份.作者, work_id)
|
||||
return 混合检索(连, 查询, 特征哈希嵌入器(), work_id=work_id, 数量=数量, 作者=身份.作者)
|
||||
# 偏好信号经公开接口取得:弱信号来自 B07 挑选台账,强信号来自本模块消费统计。
|
||||
信号表 = 合并信号表(读取挑选信号(连, 身份.作者, 领域=领域), 读取消费统计(连, 身份.作者))
|
||||
return 混合检索(
|
||||
连,
|
||||
查询,
|
||||
特征哈希嵌入器(),
|
||||
work_id=work_id,
|
||||
数量=数量,
|
||||
作者=身份.作者,
|
||||
偏好信号表=信号表 or None,
|
||||
)
|
||||
|
||||
def 重建索引(self, 身份) -> dict:
|
||||
self._核对作者(身份)
|
||||
@ -306,6 +319,7 @@ __all__ = [
|
||||
"读取方法检索资料",
|
||||
"读取方法检索候选",
|
||||
"读取参考材料",
|
||||
"读取消费统计",
|
||||
"读取方法检索身份",
|
||||
"读取方法索引维护资料",
|
||||
"读取版本材料大小",
|
||||
|
||||
@ -110,6 +110,15 @@ def 记录消费(
|
||||
return {"usage_id": usage_id, "幂等": 写入 == 0}
|
||||
|
||||
|
||||
def 读取消费统计(连, 作者: str) -> dict[str, int]:
|
||||
"""按版本聚合的真实创作消费计数;只读,供 B09 偏好重排的强信号路。
|
||||
|
||||
只统计 generation/review/planning 三类实际创作消费;
|
||||
enablement 是启用验证消费,不代表作者偏好,不计入。
|
||||
"""
|
||||
return 方法存储(连).统计消费(作者)
|
||||
|
||||
|
||||
def 读取来源回查链(连, method_version_id: str) -> dict[str, Any]:
|
||||
"""版本→方法→提案证据→B03 来源版本 的回查链。"""
|
||||
|
||||
@ -151,4 +160,4 @@ def 读取来源回查链(连, method_version_id: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["记录消费", "读取来源回查链"]
|
||||
__all__ = ["记录消费", "读取消费统计", "读取来源回查链"]
|
||||
|
||||
105
tests/单元/test_偏好重排.py
Normal file
105
tests/单元/test_偏好重排.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""偏好重排纯函数合同:分列计数、有界惩罚、确定性探索位与权重护栏。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from muse.上下文.偏好重排 import (
|
||||
偏好信号,
|
||||
偏好值,
|
||||
合并信号表,
|
||||
应用拒绝惩罚,
|
||||
权重表,
|
||||
构建偏好路,
|
||||
注入探索位,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w32-a10015",
|
||||
environment="离线",
|
||||
given="挑选信号行与消费统计",
|
||||
when="合并信号表",
|
||||
then=["弱强信号分列计数不混算;同版本两路信号并存"],
|
||||
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
)
|
||||
def test_合并信号表分列不混算__a10015():
|
||||
表 = 合并信号表(
|
||||
[
|
||||
{"method_version_id": "v1", "选用次数": 3, "拒绝次数": 1, "选中位次": [1, 2]},
|
||||
{"method_version_id": "v2", "选用次数": 0, "拒绝次数": 2, "选中位次": []},
|
||||
],
|
||||
{"v1": 5, "v3": 2},
|
||||
)
|
||||
assert 表["v1"] == 偏好信号(3, 1, 5, (1, 2))
|
||||
assert 表["v2"] == 偏好信号(0, 2, 0, ())
|
||||
assert 表["v3"] == 偏好信号(0, 0, 2, ())
|
||||
# 分列:选用与消费不互相折算
|
||||
assert 偏好值(表["v1"]) == pytest.approx(3 * 0.05 + 1 * -0.08 + 5 * 0.10)
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w32-a10016",
|
||||
environment="离线",
|
||||
given="正负偏好信号与双路融合分",
|
||||
when="构建偏好路并应用拒绝惩罚",
|
||||
then=["只有正偏好进路;惩罚有界且不超过偏好路末位贡献"],
|
||||
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
)
|
||||
def test_正偏好进路负偏好有界惩罚__a10016():
|
||||
信号表 = {
|
||||
"v1": 偏好信号(选用次数=2),
|
||||
"v2": 偏好信号(拒绝次数=3),
|
||||
"v3": 偏好信号(),
|
||||
}
|
||||
路 = 构建偏好路(信号表)
|
||||
assert set(路) == {"v1"} and 路["v1"] > 0
|
||||
融合 = {"v1": 0.03, "v2": 0.03, "v3": 0.03}
|
||||
调整 = 应用拒绝惩罚(融合, 信号表)
|
||||
assert 调整["v1"] == 0.03 and 调整["v3"] == 0.03
|
||||
assert 调整["v2"] < 0.03
|
||||
# 惩罚上界 = 路权重/61(偏好路末位贡献量级)
|
||||
assert 0.03 - 调整["v2"] <= 权重表["路权重"] / 61.0 + 1e-12
|
||||
# 大量拒绝也不能把惩罚推出上界(有界归一)
|
||||
巨量 = {"v9": 偏好信号(拒绝次数=10000)}
|
||||
融合巨量 = {"v9": 0.03}
|
||||
assert 0.03 - 应用拒绝惩罚(融合巨量, 巨量)["v9"] < 权重表["路权重"] / 61.0
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w32-a10017",
|
||||
environment="离线",
|
||||
given="含已探索头部与未探索尾部的排名",
|
||||
when="注入探索位",
|
||||
then=["至多两个未探索版本晋升,替换头部末尾已探索项,结果确定"],
|
||||
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
)
|
||||
def test_探索位晋升确定且有界__a10017():
|
||||
信号表 = {"a": 偏好信号(选用次数=1), "b": 偏好信号(消费次数=1), "c": 偏好信号(拒绝次数=1)}
|
||||
排名 = [("a", 0.9), ("b", 0.8), ("c", 0.7), ("d", 0.6), ("e", 0.5)]
|
||||
新排名, 探索集 = 注入探索位(排名, 信号表, 3)
|
||||
头部 = [vid for vid, _ in 新排名[:3]]
|
||||
# d、e 未探索:晋升两个,替换头部末尾的已探索项(c、b)
|
||||
assert 头部[0] == "a" and set(头部[1:]) == {"d", "e"}
|
||||
assert 探索集 == {"d", "e"}
|
||||
# 确定性:同输入同输出
|
||||
assert 注入探索位(排名, 信号表, 3)[0] == 新排名
|
||||
# 头部已含足量未探索项时不动
|
||||
不动, 集 = 注入探索位([("d", 0.9), ("e", 0.8), ("a", 0.7)], 信号表, 3)
|
||||
assert [vid for vid, _ in 不动] == ["d", "e", "a"] and 集 == {"d", "e"}
|
||||
# 全已探索且无尾部候选时不动
|
||||
全探索 = [("a", 0.9), ("b", 0.8)]
|
||||
assert 注入探索位(全探索, 信号表, 2)[0] == 全探索
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w32-a10018",
|
||||
environment="离线",
|
||||
given="版本化权重表",
|
||||
when="核对权重护栏",
|
||||
then=["偏好路权重不超过0.25,权重表带版本标识"],
|
||||
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
)
|
||||
def test_权重表护栏__a10018():
|
||||
assert 权重表["version"] == "preference-rerank-v1"
|
||||
assert 0 < 权重表["路权重"] <= 0.25
|
||||
assert 权重表["种类权重"]["拒绝"] < 0 < 权重表["种类权重"]["选用"]
|
||||
assert 权重表["探索位"] in (1, 2)
|
||||
124
tests/用例清单.json
124
tests/用例清单.json
@ -27307,6 +27307,130 @@
|
||||
"数据库"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w32-a10015",
|
||||
"environment": "离线",
|
||||
"given": "挑选信号行与消费统计",
|
||||
"when": "合并信号表",
|
||||
"then": [
|
||||
"弱强信号分列计数不混算;同版本两路信号并存"
|
||||
],
|
||||
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
"file": "tests/单元/test_偏好重排.py",
|
||||
"symbol": "test_合并信号表分列不混算__a10015",
|
||||
"parameter_ids": [],
|
||||
"node_ids": [
|
||||
"tests/单元/test_偏好重排.py::test_合并信号表分列不混算__a10015"
|
||||
],
|
||||
"fixtures": [
|
||||
"request",
|
||||
"测试资源接缝",
|
||||
"源码资源",
|
||||
"离线防护"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w32-a10016",
|
||||
"environment": "离线",
|
||||
"given": "正负偏好信号与双路融合分",
|
||||
"when": "构建偏好路并应用拒绝惩罚",
|
||||
"then": [
|
||||
"只有正偏好进路;惩罚有界且不超过偏好路末位贡献"
|
||||
],
|
||||
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
"file": "tests/单元/test_偏好重排.py",
|
||||
"symbol": "test_正偏好进路负偏好有界惩罚__a10016",
|
||||
"parameter_ids": [],
|
||||
"node_ids": [
|
||||
"tests/单元/test_偏好重排.py::test_正偏好进路负偏好有界惩罚__a10016"
|
||||
],
|
||||
"fixtures": [
|
||||
"request",
|
||||
"测试资源接缝",
|
||||
"源码资源",
|
||||
"离线防护"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w32-a10017",
|
||||
"environment": "离线",
|
||||
"given": "含已探索头部与未探索尾部的排名",
|
||||
"when": "注入探索位",
|
||||
"then": [
|
||||
"至多两个未探索版本晋升,替换头部末尾已探索项,结果确定"
|
||||
],
|
||||
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
"file": "tests/单元/test_偏好重排.py",
|
||||
"symbol": "test_探索位晋升确定且有界__a10017",
|
||||
"parameter_ids": [],
|
||||
"node_ids": [
|
||||
"tests/单元/test_偏好重排.py::test_探索位晋升确定且有界__a10017"
|
||||
],
|
||||
"fixtures": [
|
||||
"request",
|
||||
"测试资源接缝",
|
||||
"源码资源",
|
||||
"离线防护"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w32-a10018",
|
||||
"environment": "离线",
|
||||
"given": "版本化权重表",
|
||||
"when": "核对权重护栏",
|
||||
"then": [
|
||||
"偏好路权重不超过0.25,权重表带版本标识"
|
||||
],
|
||||
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
"file": "tests/单元/test_偏好重排.py",
|
||||
"symbol": "test_权重表护栏__a10018",
|
||||
"parameter_ids": [],
|
||||
"node_ids": [
|
||||
"tests/单元/test_偏好重排.py::test_权重表护栏__a10018"
|
||||
],
|
||||
"fixtures": [
|
||||
"request",
|
||||
"测试资源接缝",
|
||||
"源码资源",
|
||||
"离线防护"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"case_id": "NC-w32-a10019",
|
||||
"environment": "隔离PG与真实检索索引;不证明文学效果",
|
||||
"given": "两个已确认方法与新鲜索引,其一有真实挑选反馈",
|
||||
"when": "经服务公开面检索",
|
||||
"then": [
|
||||
"无信号回执保持等权合同;有信号启用重排并携带偏好分项与探索位"
|
||||
],
|
||||
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
"file": "tests/集成/test_检索偏好重排.py",
|
||||
"symbol": "test_挑选信号改变检索回执且无信号保持等权__a10019",
|
||||
"parameter_ids": [],
|
||||
"node_ids": [
|
||||
"tests/集成/test_检索偏好重排.py::test_挑选信号改变检索回执且无信号保持等权__a10019"
|
||||
],
|
||||
"fixtures": [
|
||||
"request",
|
||||
"tmp_path",
|
||||
"tmp_path_factory",
|
||||
"内置种子方案",
|
||||
"内置结构测试库",
|
||||
"数据库底座",
|
||||
"方法环境",
|
||||
"测试资源接缝",
|
||||
"源码资源",
|
||||
"离线防护",
|
||||
"隔离数据库URL"
|
||||
],
|
||||
"markers": [
|
||||
"数据库"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-work-current-binding",
|
||||
"environment": "隔离 PostgreSQL;结构升级为明确的已完成状态夹具,不代替作者升级旅程",
|
||||
|
||||
70
tests/集成/test_检索偏好重排.py
Normal file
70
tests/集成/test_检索偏好重排.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""检索偏好重排真实链路:挑选信号改变回执与排序;无信号时保持等权合同。"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import test_上下文快照与索引写入 as 方法测试
|
||||
|
||||
from muse.上下文.接口 import 混合检索
|
||||
from muse.作者经验.接口 import 参考挑选反馈请求, 挑选候选
|
||||
from muse.共享.调用身份 import 用途
|
||||
from muse.基础设施.模型.嵌入 import 特征哈希嵌入器
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
方法环境 = 方法测试.方法环境
|
||||
|
||||
|
||||
def _确认方法(服务, 作者, 来源, 标题):
|
||||
请求 = 方法测试._提案(str(uuid4()), 标题, str(来源.source_id), owner=作者.作者)
|
||||
回执 = 服务.提出方法(作者, str(uuid4()), 请求)
|
||||
方法测试._确认(服务, 作者, 回执["target_ref"])
|
||||
版本 = 服务.读取方法详情(作者, 请求.method_id)["versions"][0]
|
||||
return 请求.method_id, str(版本["version_id"])
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w32-a10019",
|
||||
environment="隔离PG与真实检索索引;不证明文学效果",
|
||||
given="两个已确认方法与新鲜索引,其一有真实挑选反馈",
|
||||
when="经服务公开面检索",
|
||||
then=["无信号回执保持等权合同;有信号启用重排并携带偏好分项与探索位"],
|
||||
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#偏好重排通路",
|
||||
)
|
||||
def test_挑选信号改变检索回执且无信号保持等权__a10019(方法环境):
|
||||
装配, 作者, 服务, 来源, _, 库 = 方法环境
|
||||
_, 版本A = _确认方法(服务, 作者, 来源, "信息差打脸")
|
||||
_, 版本B = _确认方法(服务, 作者, 来源, "信息差反转")
|
||||
服务.重建索引(作者)
|
||||
|
||||
基线 = 服务.检索方法(作者, "信息差", work_id="method-work")
|
||||
assert "重排权重版本" not in 基线
|
||||
with 库[用途.生产].连接(只读=True) as 连:
|
||||
裸 = 混合检索(
|
||||
连, "信息差", 特征哈希嵌入器(), work_id="method-work", 数量=10, 作者=作者.作者
|
||||
)
|
||||
assert [(r["version_id"], r["得分"]) for r in 基线["结果"]] == [
|
||||
(r["version_id"], r["得分"]) for r in 裸["结果"]
|
||||
]
|
||||
assert all("偏好" not in r for r in 基线["结果"])
|
||||
|
||||
经验 = 装配.要求作者经验()
|
||||
经验.记录参考挑选反馈(
|
||||
作者,
|
||||
"rerank-sel-1",
|
||||
参考挑选反馈请求(
|
||||
query="信息差",
|
||||
domain="combat",
|
||||
candidates=(挑选候选(版本A, 1, 0.03), 挑选候选(版本B, 2, 0.02)),
|
||||
selected=(版本A,),
|
||||
index_generation=int(基线.get("index_generation", 0)),
|
||||
),
|
||||
)
|
||||
重排 = 服务.检索方法(作者, "信息差", work_id="method-work", 领域="combat")
|
||||
assert 重排["重排权重版本"] == "preference-rerank-v1"
|
||||
行A = next(r for r in 重排["结果"] if r["version_id"] == 版本A)
|
||||
行B = next(r for r in 重排["结果"] if r["version_id"] == 版本B)
|
||||
基A = next(r for r in 基线["结果"] if r["version_id"] == 版本A)
|
||||
assert 行A["偏好"]["选用次数"] == 1 and 行A["偏好"]["消费次数"] == 0
|
||||
assert 行A["得分"] > 基A["得分"] # 正偏好第三路严格加分
|
||||
# B 无任何挑选/消费信号:属未探索,标记探索位;A 已有信号不占探索位
|
||||
assert 行B["探索位"] is True and 行A["探索位"] is False
|
||||
@ -62,6 +62,7 @@ export function Ui选段改写({
|
||||
const [refDomain, setRefDomain] = useState<string>("combat");
|
||||
const [refResults, setRefResults] = useState<检索结果行[]>([]);
|
||||
const [refGeneration, setRefGeneration] = useState(0);
|
||||
const [refWeightVersion, setRefWeightVersion] = useState("");
|
||||
const [refSelected, setRefSelected] = useState<string[]>([]);
|
||||
const [refRejected, setRefRejected] = useState<string[]>([]);
|
||||
const [refSignals, setRefSignals] = useState<Record<string, number>>({});
|
||||
@ -111,12 +112,24 @@ export function Ui选段改写({
|
||||
setError("");
|
||||
try {
|
||||
const { data } = await 客户端.GET("/api/v1/methods/search", {
|
||||
params: { query: { q: refQuery.trim(), work_id: current.work_id, limit: 20 } },
|
||||
params: {
|
||||
query: {
|
||||
q: refQuery.trim(),
|
||||
work_id: current.work_id,
|
||||
limit: 20,
|
||||
domain: refDomain,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!data) throw new Error("检索未返回。");
|
||||
const 页 = data as unknown as { 结果: 检索结果行[]; index_generation?: number };
|
||||
const 页 = data as unknown as {
|
||||
结果: 检索结果行[];
|
||||
index_generation?: number;
|
||||
重排权重版本?: string;
|
||||
};
|
||||
setRefResults(页.结果 ?? []);
|
||||
setRefGeneration(Number(页.index_generation ?? 0));
|
||||
setRefWeightVersion(页.重排权重版本 ?? "");
|
||||
setRefSelected([]);
|
||||
setRefRejected([]);
|
||||
refFeedbackDone.current = false;
|
||||
@ -160,6 +173,7 @@ export function Ui选段改写({
|
||||
work_id: current.work_id,
|
||||
quote: "",
|
||||
index_generation: refGeneration,
|
||||
weight_version: refWeightVersion,
|
||||
种类: "参考挑选",
|
||||
candidates: refResults.map((行, 序) => ({
|
||||
method_version_id: 行.version_id,
|
||||
|
||||
@ -7330,6 +7330,11 @@ export interface components {
|
||||
* @default 0
|
||||
*/
|
||||
index_generation: number;
|
||||
/**
|
||||
* Weight Version
|
||||
* @default
|
||||
*/
|
||||
weight_version: string;
|
||||
/**
|
||||
* 种类
|
||||
* @default 参考挑选
|
||||
@ -11979,6 +11984,7 @@ export interface operations {
|
||||
q: string;
|
||||
work_id?: string;
|
||||
limit?: number;
|
||||
domain?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user