scope 优先于相似度
租户 / 用户 / persona / team 行级硬隔离,先过 scope 再算相似度,不允许"宽松匹配"
Hybrid 而非纯向量
向量召回 + BM25 关键词召回,RRF 融合,专有名词与 ID-like 词不漏召
阈值优先于 topK
低分条目宁可不要——噪音让 LLM 跑偏,召回质量优先于召回数量
详细设计说明
把"召回"从一个标题展开成可实现、可调参、可观测的算法契约
本文档专门讲 Retrieval:从 T2 长期 store 取出与当前对话相关的记忆条目。其余四个 Memory 动作(Precheck / Compression / Writeback / Isolation)见 Memory / Context 设计。本文重点定义五阶段召回管道、四维打分公式骨架、双闸截断原则,以及 embedding / 向量库 / BM25 引擎的 Provider 抽象边界——具体模型与产品由实现选型决定,platform 层不锁死。
设计主张
召回不是"跑一次 cosine 取 topK"。它是 scope 过滤 + 候选生成 + 多维打分 + 双闸截断 + Trace 五个阶段的有序管道。任一阶段缺位,就会出现跨用户串数据、漏召专有名词、低分噪音污染 ctx、出问题没法回放四类典型故障。
评审关注点
- scope 过滤是否在 DAO 层强制注入,跨租户绝对不串
- 纯向量是否会漏掉专有名词 / ID-like 词,hybrid 是否兜住
- 低分条目(< 阈值)是否被坚决砍掉,不靠 token 富余把噪音塞进 ctx
- 向量索引故障 / store 超时 / 0 召回的降级路径是否清楚
- memory_retrieve ToolTrace 是否能支持回放与权重调优
1. 召回管道总览
用一张图把召回的五个阶段、各阶段的输入输出、与 Memory / Context 子系统其他动作的边界讲清楚,作为后续章节的导航。
召回管道接收三类输入:当前 query(user 最新一条消息或子任务描述)、CallerIdentity(tenant_id / user_id / persona_id / team_ids)、token 预算余量(来自 Precheck 估算)。输出是一组 MemoryItem,每条带 content / scope / written_at / source_run_id / 最终 score。整个管道在 Harness 第二步 s2_context_assemble 里调用,调用一次出一个 ToolTrace。
1.1 阶段定位与边界
| 阶段 | 核心职责 | 本文档章节 |
|---|---|---|
| ① scope 过滤 | 按 CallerIdentity 在 DAO 层做行级硬隔离,把不可见数据挡在管道入口 | § 2 |
| ② 候选生成 | Hybrid 检索:向量召回 + BM25 关键词召回,RRF 融合产出 topN_candidate(典型 30–50) | § 3 |
| ③ 打分排序 | 对候选集做四维加权打分(语义 / 时间 / 频次 / 来源),输出每条最终 score | § 4 |
| ④ 截断双闸 | score 阈值 + topK 上限 + token 预算三者取交集,留下进 ctx 的最终条目 | § 5 |
| ⑤ Trace | 每次召回写一条 memory_retrieve ToolTrace,含命中、分布、耗时、降级标记 | § 7 |
| ⑥ 失败降级 | 四类故障的明确降级动作,遵循"宁可少召回,不可错召回" | § 6 |
1.2 与其他 Memory 动作的边界
| 动作 | 本文档 | memory-context-design |
|---|---|---|
| Retrieval(召回) | 本文专属 | 仅保留概要(§3 一段总览 + 跳转) |
| Precheck(预检) | 不展开,仅引用其输出(token 预算余量) | §2 主写 |
| Compression(压缩) | 不展开,仅引用其触发条件(召回 + history 超预算) | §4 主写 |
| Writeback(写回) | 不展开,但 source_quality 维度引用其写入元数据 | §5 主写 |
| Isolation(隔离) | scope 过滤即是 Isolation 在召回路径的具体落地 | §6 主写隔离总则 |
2. scope 过滤(第一道闸)
说明为什么 scope 过滤必须在相似度排序之前、必须在 DAO 行级强制注入,以及 scope 缺失时为什么直接终止 Run 而不是"宽松匹配"。
scope 过滤是召回的第一道也是最硬的一道闸。所有候选条目至少携带四个 scope 标签:tenant_id(必)、user_id(必)、persona_id(写入 persona 私有记忆时必)、team_id(写入团队共享记忆时必)。召回时按当前 RunContext 的 CallerIdentity 做行级过滤——这一步是后续相似度排序的前提,不是优化项,缺一不可。
2.1 四维 scope
| 维度 | 过滤强度 | 典型 store |
|---|---|---|
tenant_id | DAO 行级强制注入,一票否决 | 所有 store 必走,跨租户绝不串 |
user_id | 行级过滤 | user_memory · 个人 session 摘要 |
persona_id | 行级过滤 | persona_memory · 角色私有经验(运营助手 / 代码 reviewer 各自独立) |
team_id | 应用层过滤 + 双向校验 | team_memory · 当前 caller 必须在 team 成员表内才允许召回 |
2.2 行级强制注入
scope 过滤的实现要求不能依赖业务代码自觉——任何 store 的 DAO 层在构造查询时必须强制把 tenant_id / user_id 加进 WHERE 子句,业务层无法绕过。这一约束的工程意义:即使某个 Skill 写错了 SQL,跨租户数据也不会泄漏,因为 DAO 层会拒绝没有 scope 谓词的查询。
具体落地由 Provider 决定:
- 关系型 store(pgvector / 普通 RDB):DAO 拦截器在 SQL 构造阶段注入 scope 谓词,无 scope 上下文的查询直接抛错
- 向量索引(FAISS / Qdrant / Milvus):scope 字段以 metadata filter 形式参与检索,向量库原生支持的 pre-filter 而非 post-filter(pre-filter 在索引层就裁剪掉跨 scope 的向量,post-filter 是先全召回再丢弃 — 后者在大数据量下会泄露 metadata)
- 缓存层(Redis 等):cache key 必须以 scope 标签作为前缀,禁止以 query hash 作为唯一 key
2.3 失败语义
| 故障 | 动作 | 原因 |
|---|---|---|
| tenant_id 为空 | 拒绝召回,立即终止 Run,写 ErrorTrace | 没 tenant 等于"宽松匹配 + 全租户可见",比错答更危险 |
| user_id 为空,但 tenant_id 存在 | 仅召回该 tenant 的 team_memory(公开维度),不召回 user_memory | 系统级 / 平台维护场景的合理路径 |
| team_id 列表为空 | 跳过 team_memory 维度,其余正常 | 合法状态——caller 不在任何 team 内 |
| persona_id 为空 | 跳过 persona_memory 维度,其余正常 | 合法状态——通用 agent 没有 persona 绑定 |
3. 候选生成 · Hybrid Search
说明为什么纯向量召回会在中文 + 专有名词场景下漏召回,为什么必须叠加 BM25 关键词路径,以及如何用 RRF 把两路结果融合成同一个候选集。
scope 过滤通过后进入候选生成。这一阶段的目标是从可能上万条记忆里粗筛出几十条候选——既不能漏(漏召回比错召回更难发现),也不能太多(候选越多打分阶段越慢)。Hybrid Search 是工业级 RAG 系统验证过的标准做法:向量召回处理"语义近",关键词召回处理"字面命中",二者用 RRF(Reciprocal Rank Fusion)融合。
3.1 向量召回
预先用 embedding 模型把每条 memory 的 content 转成定长向量,存入向量索引。召回时把 query 转成同一空间的向量,用余弦相似度(或 inner product)找 top-N 最近邻。这条路径擅长"意思接近但用词不同"的场景,比如 query 说"继续推进",能召回写着"上线 / 落地 / 推进 / 实施"的旧记忆。
| 子项 | 规则 |
|---|---|
| 向量维度 | 由 embedding 模型决定(典型 768 / 1024 / 1536);同一 store 内必须一致,模型升级 = 全量重算 |
| 距离度量 | 余弦相似度归一到 [0, 1],作为后续打分的 semantic_sim 输入 |
| 索引结构 | HNSW / IVF-PQ 等近似最近邻索引;精确度可调,召回率优先于延迟 |
| top-N 上限 | 典型 30;过大会让候选集稀释,过小会漏掉边缘相关条目 |
| scope 注入 | 必须以 metadata pre-filter 形式参与检索,不允许 post-filter |
3.2 关键词召回(BM25)
纯向量在三类场景下会漏召回:专有名词("Q2 launch" / "模块 9" / "项目代号 X")、ID-like 词(run_id / 文件路径 / 错误码)、低频专业术语(embedding 训练语料没覆盖到的)。BM25 走全文倒排索引补一手——它对字面命中很敏感,能把"提到 Q2 launch"的旧记忆稳稳召回。
| 子项 | 规则 |
|---|---|
| 分词 | 中英混合走基础分词器(jieba / ik / ES analyzer);专有名词维护词典,避免被切碎 |
| 评分 | BM25 标准公式(k1 ≈ 1.2,b ≈ 0.75);输出原始分数,召回阶段不归一 |
| top-N 上限 | 典型 20;BM25 在长尾词上排序更稳,召回数量略少于向量路径 |
| 停用词 | 常规中英文停用词;不过滤短查询(避免"@ 协作群"被切完没了) |
| scope 注入 | 同向量路径——BM25 引擎的 filter 子句强制带 scope 谓词 |
3.3 RRF 融合
两路召回各自给出有序列表,RRF(Reciprocal Rank Fusion)按"排名倒数"相加,得到融合排序。它的好处是不依赖两路分数同尺度——向量的 cosine 和 BM25 的原始分数本来就不可比,RRF 只看 rank 不看 score,融合鲁棒。
其中 k 是平滑常数,工业界经验取 60。某条记忆只在向量路径出现、不在 BM25 出现时,BM25 项视为 0(不参与求和);反之亦然。融合后按 rrf_score 倒序,取前 topN_candidate(典型 30–50)进入下一阶段打分。
3.4 候选集大小
| topN_candidate | 权衡 | 典型场景 |
|---|---|---|
| 20 | 打分快,但可能漏掉边缘相关条目 | 实时对话、低延迟优先 |
| 30 – 50 | 默认值;打分阶段在毫秒级,覆盖足够 | 主流场景 |
| 100+ | 打分耗时显著,效益边际递减;除非做研究 / 评估 | 离线评估管道 |
候选集大小不固定写死,由 Provider 配置参数 retrieval.candidate_top_n 控制;同一部署可按 user / agent 维度做 override,灰度调参不需要发版。
4. 打分排序 · 四维加权
定义召回打分的四个维度(语义 / 时间 / 频次 / 来源)各自的物理意义、归一区间、加权公式骨架,以及为什么权重要给区间不给死值。
候选集进来后要做精排。单靠 RRF 排名不够——RRF 只看相对位置,不看绝对相关性,也不看时效。打分阶段把四个独立信号融合成最终 score,作为后续截断的依据。
四个分量都归一到 [0, 1],权重 w_* 由配置决定。下面分别说每一维。
4.1 语义相似度(f_sem)
来源是 §3.1 向量召回的余弦相似度,归一到 [0, 1]。它衡量的是"这条记忆和 query 在语义空间里有多近"——主轴信号,权重最大。
- 纯 BM25 命中、向量路径未召回的条目,
f_sem取候选集中向量路径最低分作 fallback,避免该项为 0 把整体打分压死 - query 本身是 ID-like 词("模块 9"、错误码)时,
f_sem不一定靠谱,靠 BM25 主导——这正是 hybrid 的价值
4.2 时间衰减(f_rec)
越新的记忆权重越高。时间衰减用指数函数,让旧条目优雅退场而非一刀切:
Δt 是当前时间到 written_at 的天数;τ 是半衰期(典型 30 天)。直观理解:
| Δt | τ=30 天时的 f_rec | τ=90 天时的 f_rec |
|---|---|---|
| 1 天 | 0.97 | 0.99 |
| 7 天 | 0.79 | 0.93 |
| 30 天 | 0.37 | 0.72 |
| 90 天 | 0.05 | 0.37 |
| 365 天 | ≈ 0 | 0.02 |
τ 由场景决定:
- 偏好类记忆("喜欢简洁回复" / "不要 emoji")τ 大,可以 90–180 天
- 项目状态类记忆("Q2 上线计划" / "模块 9 由 X 负责")τ 中等,30 天
- 临时上下文("刚才我让你看的那段代码")τ 小,1–3 天,session 之外几乎不再召回
不同 memory 类型在写入时通过 decay_class 字段标记,召回时查表取对应 τ。
4.3 频次加成(f_freq)
被多次写入或多次确认的记忆更可信。用对数归一避免高频条目过度压制其他维度:
| write_count | f_freq |
|---|---|
| 1 | 0.29 |
| 3 | 0.58 |
| 5 | 0.75 |
| 10+ | 1.00 |
频次的工程含义:用户反复说同一件事("再说一次,不要 emoji")= 强偏好,应该跑赢只说过一次的偏好。
4.4 来源可信度(f_src)
记忆的写入来源不同,可信度也不同。这一维主要用于压制 LLM 自动写回的噪音条目:
| 来源 | f_src | 说明 |
|---|---|---|
| 用户显式 "记一下 X" | 1.00 | 最强信号 |
| 从 SUCCEEDED Run 写出 + 用户未否认 | 1.00 | 正常路径 |
| 用户反馈纠正写出("以后不要再这样做") | 1.00 | 纠偏类强信号 |
| LLM 主动写回(无人确认) | 0.60 | 压制噪音;如果用户后续没纠正,频次会自动累计补偿 |
| 从 FAILED / REJECTED Run 写出 | 0.30 | 原则上 Writeback 已经过滤这类,0.30 是双保险 |
| 批量导入 / 迁移而来 | 0.50 | 无 Run 上下文兜底,给中性权重 |
4.5 加权公式与权重边界
把四维合起来:
权重不写死具体数值,给一个工业经验的参考区间和场景说明,由 Provider 配置:
| 权重 | 参考区间 | 调高的场景 | 调低的场景 |
|---|---|---|---|
w_sem | 0.5 – 0.7 | 对话型场景,语义匹配最重要 | 项目管理 / 状态追踪场景,时效更重要 |
w_rec | 0.15 – 0.30 | 状态类记忆为主、时效敏感 | 偏好类记忆为主、长期稳定 |
w_freq | 0.05 – 0.15 | 用户反复确认的偏好极重要 | 每条记忆都只写一次的场景,频次没区分度 |
w_src | 0.03 – 0.10 | LLM 自动写回噪音多,需要压制 | 所有写回都有人审,来源差异不大 |
默认配置建议:0.60 / 0.25 / 0.10 / 0.05,作为冷启动权重;上线后根据 ToolTrace 的命中分布与用户反馈周期性调整。不要把权重当固定参数——它是平台可调的运营参数,跟 LLM 温度、HITL 阈值同一类。
5. 截断双闸
定义打分输出到最终进 ctx 之间的两道闸:score 阈值(质量优先)+ topK 上限(数量上限),并说明 token 预算如何与 Precheck 对齐。
打分排完序之后并不直接全部塞进 ctx——要过两道独立的闸。这两道闸是平等的,任一不满足都不进 ctx。这条原则在工程上的反向表述更清楚:"即使 token 预算还有富余,分数低于阈值的也坚决不要"。
5.1 score 阈值(质量闸)
设一个 absolute 阈值 score_threshold(典型 0.4–0.5),低于该值的候选条目直接砍掉,不进入 topK 排队。这一闸的工程意义:低分条目对 LLM 是噪音而不是信息——塞进 ctx 反而让模型注意力被无关历史冲淡。
| 阈值 | 含义 | 调整方向 |
|---|---|---|
| 0.30 | 宽松,召回多但夹杂边缘条目 | 用户反馈"AI 经常提无关旧事"时调高 |
| 0.40 | 默认偏宽 | 主流场景 |
| 0.50 | 偏严,只留高置信条目 | 对幻觉敏感的合规场景 |
| 0.60+ | 非常严,可能召回为空 | 仅在准确率压倒一切的窄场景 |
5.2 topK 上限(数量闸)
哪怕分数都过阈值,最多也只取 topK 条进 ctx(典型 5–10)。这一闸保证记忆段不会无限膨胀,给 system + history + 工具 schema 留出预算空间。
| topK | 典型 token 占用 | 适用 |
|---|---|---|
| 3 – 5 | 100 – 250 tokens | 短对话 / 低预算模型 |
| 5 – 10 | 250 – 600 tokens | 默认 |
| 10 – 20 | 600 – 1500 tokens | 长上下文模型 + 高复杂度任务 |
5.3 token 预算对齐 Precheck
第三道隐式闸:召回结果总 token 不能超过 Precheck 给出的预算。Memory / Context 设计文档 §3.2 已经定义了召回部分占 target_budget × 0.4 的总配额——召回管道在打分排序后做累加截断,超预算时按 score 倒序丢弃尾部条目。
三闸的优先级:
- score 阈值——绝对的,低分一律不要
- topK 上限——数量硬上限
- token 预算——动态调整,按 Precheck 余量决定能塞多少
三者取交集:候选 ⊃ (过阈值) ∩ (前 K) ∩ (累计 token ≤ 预算)。
6. 失败降级
列举召回管道四类典型故障的明确降级动作;核心原则是宁可少召回也不要错召回。
召回链路涉及向量索引、关系型 store、BM25 引擎、缓存层多个外部依赖,故障概率不低。降级原则有两条:(1)scope 缺失永远不允许"宽松匹配"——错答比无答更难发现;(2)打分维度的某一路挂了允许降级,整路都挂了直接走"无记忆"路径。
| 故障 | 降级动作 | 状态影响 |
|---|---|---|
| 向量索引不可用 | 跳过 §3.1 向量路径,仅走 BM25;打分阶段 f_sem 取 BM25 rank 反推的近似值 | 召回率下降但不返错;写 WarnTrace |
| BM25 引擎不可用 | 跳过 §3.2 关键词路径,仅走向量;打分照常 | 专有名词召回率下降;写 WarnTrace |
| 两路检索都不可用 | 跳过 Hybrid,仅走"按 (user_id, persona_id) 直查最近 N 条"的 fallback 列表,绕过相似度排序 | 召回质量明显下降;写 ErrorTrace + 告警 |
| store 整体超时 | 退到只用 system + 当前 input,按"无记忆"路径执行 | 对话能继续;写 WarnTrace |
| scope 上下文缺失(tenant_id 为空) | 拒绝召回,立即终止 Run | RunState → FAILED → TRACING → CLOSED_FAILED;写 ErrorTrace |
| 召回结果 0 条(合法) | 正常进入装配,按"无记忆"路径执行 | 无影响;不写 Warn |
| 打分阶段超时 | 用 RRF 融合排名直接当 final_score,跳过四维加权 | 排序质量下降但不阻塞;写 WarnTrace |
所有降级路径都必须留 Trace 字段(§7),这样事后可以查"这次 Run 是不是因为某路降级导致召回质量下降,进而影响了 LLM 输出"。
7. Trace 与可观测
定义 memory_retrieve ToolTrace 的字段集合、用于权重调优的召回质量指标,让"为什么这次召回了这几条而不是那几条"成为可机器查询的事实。
每次召回写一条 ToolInvocationTrace(虚拟 tool 名 memory_retrieve)。它和真实 Tool 调用走同一套 trace schema,区别仅在于 skill_id = memory.retrieve、不进沙箱。
7.1 memory_retrieve ToolTrace 字段
| 字段族 | 关键字段 | 用途 |
|---|---|---|
| 调用上下文 | run_id · step_id · caller_identity(tenant / user / persona / team_ids)· budget | 定位是哪次 Run 的召回 |
| 查询 | query_hash · query_token_count | 不写原文(隐私),只留 hash 用于聚合统计 |
| scope 过滤 | scope_filter(生效的 SQL 谓词摘要)· filtered_count | 验证 scope 注入是否符合预期 |
| 候选生成 | vector_top_n · bm25_top_n · rrf_k · candidate_count_after_fusion | 调试 hybrid 是否正常工作 |
| 打分 | weights(w_sem / w_rec / w_freq / w_src)· score_distribution(min / median / max)· score_threshold | 调权重必看 |
| 截断 | topK · token_budget · final_count · final_token_total | 验证三闸是否按预期截断 |
| 命中条目 | memory_id 列表 + 各自 final_score(不存 content,按 id 关联) | 具体召回了什么 |
| 降级标记 | degradation: vector_unavailable / bm25_unavailable / store_timeout / scope_missing / 无 | 事后查"这次为什么质量下降" |
| 耗时 | 各阶段独立 latency(scope_ms / candidate_ms / scoring_ms / truncation_ms) | 定位性能瓶颈 |
7.2 召回质量指标
在 ToolTrace 之上做聚合,得到指标看板,作为权重调优 / 阈值调优的输入:
| 指标 | 定义 | 用途 |
|---|---|---|
| 召回命中率 | final_count > 0 的占比 | 过低 → 阈值或 topK 太严 / 候选池太小 |
| 平均最终 score | 命中条目 final_score 的均值 | 稳定下降 → 提示数据陈旧或权重失衡 |
| token 利用率 | final_token_total / token_budget | 持续低于 0.5 → topK 太小,可放开;持续 1.0 → 预算压力大 |
| 降级路径占比 | 带 degradation 标记的 trace 占比 | 非零项需要立即排查依赖健康度 |
| scope_filter 异常率 | filtered_count = 0 但理论应有候选的情况 | 排查 DAO 注入 / 索引 metadata 一致性 |
| 用户反馈 vs 召回 | 用户在对话中纠正"AI 提到了无关旧事" 与召回 score 分布的关联 | 调整阈值的最终决策依据 |
这些指标对接 可观测与 Replay 的总体看板,按 tenant / persona / agent 三维度切分。
8. 与其他模块关系
说明本召回设计被哪些上游模块调用、依赖哪些下游能力、与哪些子系统在边界上需要明确契约。
| 模块 | 关系 | 边界对齐 |
|---|---|---|
| Memory / Context 设计 | 本文档是其 §3 召回章节的展开版。memory-context §3 仅保留管道总览 + 跳转链接 | 命名 / 字段 / 阶段编号必须一致;任一处变更需双向同步 |
| Harness 内核设计 | 召回管道在 s2_context_assemble 内调用,输入 RunContext + token 预算余量,输出 MemoryItem 列表 | 本管道是 Harness 第二步的内部调用,不暴露到 Tool 控制器;不进入沙箱 |
| Skill 版本治理 | memory_retrieve 作为 platform 内置虚拟 skill,manifest 同样走 manifest_hash 锁定,权重 / 阈值升级走 minor,重大算法变更走 major | 和普通 Skill 一样有版本生命周期;权重 hot-swap 在 manifest 之外的 RolloutConfig 里 |
| 可观测与 Replay | memory_retrieve ToolTrace 入主 Trace 表;Replay 时按 trace 内的 weights / threshold / scope_filter 重建当时的召回上下文 | 不写 query 原文(隐私);命中条目按 memory_id 关联,replay 时再去 store 查 |
| 治理与 HITL | 降级路径走 ErrorTrace 时按风险矩阵处理(scope 缺失类视为 high) | 不为召回单独造一套治理通道 |
9. 实现选型边界
明确 platform 层不锁死具体 embedding 模型、向量库、BM25 引擎;只定义 Provider 接口契约,留给实现侧按场景选型。
召回管道的所有外部能力都走 Provider 抽象。Provider 之间不需要换源代码,只需要换配置 + 换 manifest_hash。这条原则对应"沙箱执行后端抽象"的同一思路——platform 不绑死具体后端。
9.1 Provider 接口契约
| Provider | 接口要求 | 切换粒度 |
|---|---|---|
| EmbeddingProvider | 输入:文本;输出:定长向量(维度由 Provider 自报)。同一 store 内必须保持模型版本一致;切换模型 = 全量 re-index | tenant 级 |
| VectorIndexProvider | 支持 metadata pre-filter + ANN 检索;返回 (memory_id, similarity) 列表 | tenant 级 |
| KeywordIndexProvider | BM25 风格全文检索;支持 filter 子句 + 中英混合分词;返回 (memory_id, raw_score) 列表 | tenant 级 |
| MemoryStore(DAO) | scope 谓词强制注入;按 memory_id 批量查 content 与 metadata;写入时同步更新 vector / keyword 索引 | 部署级 |
9.2 选型示例(仅举例,不构成绑定)
- EmbeddingProvider:商用闭源(OpenAI
text-embedding-3-small、Cohere)/ 开源(BGE / GTE / E5 系列)/ 自训练垂类模型;选型权衡 = 中文能力 × 成本 × 推理延迟 - VectorIndexProvider:嵌入式(FAISS / Annoy)/ 服务化(Qdrant / Milvus / Weaviate)/ 数据库内置(pgvector / Elasticsearch dense_vector);选型权衡 = scope 过滤能力 × 数据规模 × 运维成本
- KeywordIndexProvider:Elasticsearch / OpenSearch / Lucene / Tantivy / SQLite FTS5;选型权衡 = 中文分词质量 × 部署复杂度
9.3 不锁死的事
本文档不规定具体使用哪家模型、哪个向量库;这些属于实现选型,由部署环境决定。文档只规定:
- 必须 Hybrid(向量 + BM25),不允许只走单路
- scope 必须 pre-filter,不允许 post-filter
- 权重必须可配置 hot-swap,不允许写死在代码里
- 每次召回必须写 ToolTrace,不允许"直查不留痕"
- 失败必须显式降级路径,不允许静默返回空
这五条是 platform 层的硬契约;除此之外的选型自由。