feat(上下文): 复利机制批次2——混合检索偏好重排:三信号分列、版本化权重、有界惩罚与探索位

This commit is contained in:
zizi 2026-09-19 00:49:52 +08:00
parent d41a9b5ea4
commit f0a61286df
14 changed files with 570 additions and 10 deletions

View File

@ -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": "种类",

View 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__ = [
"权重表",
"偏好信号",
"合并信号表",
"偏好值",
"构建偏好路",
"应用拒绝惩罚",
"注入探索位",
]

View File

@ -10,6 +10,7 @@ from muse.上下文.任务范围 import (
)
from muse.上下文.依赖校验 import 依赖重验检查, 核对依赖
from muse.上下文.偏好材料 import 偏好绑定, 核对范围偏好
from muse.上下文.偏好重排 import 偏好信号, 合并信号表, 权重表
from muse.上下文.冻结快照 import (
快照身份,
快照载荷,
@ -71,6 +72,9 @@ __all__ = [
"核对回放字段策略",
"保护模型发送",
"混合检索",
"合并信号表",
"权重表",
"偏好信号",
"重建方法索引",
"方法索引操作",
"登记方法索引参与者",

View File

@ -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 {}),
}

View File

@ -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字")

View File

@ -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")

View File

@ -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__ = ["方法存储"]

View File

@ -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__ = [
"读取方法检索资料",
"读取方法检索候选",
"读取参考材料",
"读取消费统计",
"读取方法检索身份",
"读取方法索引维护资料",
"读取版本材料大小",

View File

@ -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__ = ["记录消费", "读取消费统计", "读取来源回查链"]

View 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)

View File

@ -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;结构升级为明确的已完成状态夹具,不代替作者升级旅程",

View 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

View File

@ -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,

View File

@ -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;