本文是「Agent 开发实践与思考」系列第 5 篇。系列目录:
- 2024
- 07-02 我手写了一个 Agent:从一问一答到”思考-行动-观察”循环
- 08-06 工具调用踩坑记:schema、描述与错误返回怎么写
- 09-10 系统提示词写到几千字之后:让 Agent 迁移 Vue 业务与资产到 React
- 10-15 给 Agent 做记忆:分层、状态与遗忘
- 11-01 迁移知识库的 embedding 召回评测方法(本篇)
在接入 RAG 前评测召回
上一篇将迁移 Agent 的长期记忆拆成可检索的经验条目。接入 RAG 时,需要为每个条目生成 embedding,并在新任务到来时按相似度检索候选经验。
可以先从公开榜单靠前的模型中选择候选,写入向量库后查看检索结果;但仅凭这些结果无法判断模型是否适合迁移记录。搜 “表格 reload 怎么处理” 时,返回的条目可能都涉及列表刷新,但部分属于其他表格组件,部分缺少 ref 调用这一前提。若模型将这些条目排在前面,Agent 可能将不适用经验用于当前任务。
embedding 在候选集合中按相似度排序条目,供后续步骤处理。模型是否适用,需要根据当前知识库、查询类型和下游预算评测。公开检索榜单可用于缩小候选模型范围,不能替代本地评测。
本文说明比较迁移知识库中多个 embedding 模型的流程,将模型差异记录为可复查的评测结论。
先明确模型需要找什么
知识库里的每一条记录都带有自然语言和结构化元数据。自然语言部分描述问题、处理动作、适用条件、验证结果与反例。元数据包括组件类型、项目、框架版本、接口名和经验状态。
查询至少包括以下几类,应分别统计:
- 问具体标识符,例如
LegacyTable、reload、selection-change。这类查询需要精确命中,不能把名字相似的组件当成同一个对象。 - 问处理模式,例如 “Vue 动态插槽迁到 React 后如何保留行渲染”。查询和经验条目可能不复用同一组词,需要评测语义匹配。
- 问限制或失败条件,例如 “列表能展示但提交后没有刷新时先查什么”。相关条目可能没有出现 “刷新” 这个词,却记录了 ref 方法、请求参数和调用链。
- 问项目内术语,例如团队习惯称为 “审批流弹窗” 的组件。通用模型未必理解这个名字,必须单独测它是否被正确召回。
将这些查询合并为一个总分会掩盖不同查询类型的失败情况。某个模型可能擅长聚合相似描述,却持续漏掉组件名;另一个模型能命中标识符,但对不同表述的同类问题不敏感。下游是否同时使用关键词检索和重排,也会改变 embedding 的职责。模型评测前,需要确定 embedding 在检索链路中承担的职责。
从真实任务整理评测集
评测集需要来自真实任务,评测结果才可用于当前迁移场景。评测问题从已完成的迁移任务中整理,不由模型生成。每个样本包含一个查询、若干相关条目和相关等级。
例如,查询 “LegacyTable 的 reload 要不要保留” 对应的直接相关条目,应同时说明调用方是否通过 ref 调用它、触发刷新时是否要保留分页和请求参数。只说 “表格需要刷新” 的条目只能算部分相关。另一个组件的 reload 经验如果接口和调用链不相同,则属于不相关,即使它的文字主题很接近。
我把人工标注分成三档:
| 等级 | 含义 | 在指标中的用法 |
|---|---|---|
| 可直接复用 | 条件和契约与当前任务一致,Agent 可以将它作为候选方案 | 主要正例 |
| 只能参考 | 处理模式相近,但需要回到当前代码确认前提 | 次要正例,单独统计 |
| 不适用 | 名称、组件或业务主题相近,但契约不一致 | 负例 |
标注不能仅以条目“看起来有帮助”判定相关。检索系统服务于 Agent 的下一步动作,因此判据应是:将记录提供给 Agent 后,它能否减少一次无效搜索,或提出一项正确的检查。若记录只能提供宽泛背景,甚至可能诱导 Agent 修改错误接口,就不应算作正例。
评测集应避免将同一条经验的改写版本同时放入库和查询。否则模型可能进行近似复述匹配,离线分数无法代表真实提问的结果。对于每条查询,我会检查措辞是否来自任务日志,是否保留了调用方实际会用到的字段和约束,并让标注者在不看候选排序的情况下先确定相关条目。
初始评测集可以较小,但应覆盖实际出现的查询类型。十几条只包含常规语义查询的样本,可能无法区分多个模型;加入缩写、接口名、反例和不同表述后,模型差异才可能出现。新任务中的漏召回和误召回也应补充到评测集,不应只记录在复盘文档中。
比较模型时固定文档、查询和索引条件
我先根据语言、部署条件、向量维度和成本筛出可试的候选,再在同一个评测集上比较。例如中文知识库可以把 m3e-base、bge-large-zh-v1.5、bge-m3 与 text-embedding-3-large 放进候选列表。是否适合仍需根据本地评测数据判断。
对比时要固定以下条件:
- 所有模型使用同一份文档切分结果和同一份元数据。模型 A 按段落写入、模型 B 按句子写入,比较到的是切分策略,不是 embedding。
- 查询和文档分别使用模型要求的输入格式。部分模型为查询和文档定义了不同前缀,漏掉前缀可能让结果失真。
- 使用同一种向量归一化和距离计算方式。若采用余弦相似度,应确认向量是否已经归一化,避免把实现差异当成模型能力。
- 固定每次返回的候选数 K。下游只会拿 top 5 做重排,就不能只报告 top 100 的召回。
- 记录索引版本、模型版本、维度、请求批次和评测时间。托管模型或模型服务更新后,旧分数不能自动沿用。
下面是一个最小的离线计算框架。它不关心向量来自哪个服务,只接收每个查询的排序结果和人工标注:
type Judgement = "direct" | "reference" | "irrelevant";
type QueryCase = {
id: string;
relevant: Map<string, Judgement>;
};
function recallAtK(results: string[], testCase: QueryCase, k: number): number {
const directIds = [...testCase.relevant]
.filter(([, judgement]) => judgement === "direct")
.map(([id]) => id);
if (directIds.length === 0) return 1;
const retrieved = new Set(results.slice(0, k));
return directIds.some((id) => retrieved.has(id)) ? 1 : 0;
}
function mrr(results: string[], testCase: QueryCase): number {
const firstRank = results.findIndex(
(id) => testCase.relevant.get(id) === "direct",
);
return firstRank === -1 ? 0 : 1 / (firstRank + 1);
}记录召回、排序和误召回情况
评测至少记录以下三个方面。
第一个是 Recall@K:直接可复用的条目是否出现在前 K 名。对后面还要重排的流程,Recall@50 更适合作为指标。该指标检查直接可复用条目是否进入候选集。如果相关条目没有被召回,reranker 再准确也无法找回它。
第二个是 MRR,也就是第一个直接相关条目的倒数排名。它会区分 “相关条目在第 1 名” 和 “相关条目在第 50 名”。对于不走重排、只注入少量片段的简单流程,MRR 和 Recall@5 更接近实际体验。
第三个是误召回检查,并非传统检索指标。将每个查询 top K 的不适用条目单独列出,检查其排名较高的原因。常见情况有三种:组件名相似、共享了 “刷新” 等宽泛词,或同一个失败现象对应不同契约。平均分接近时,bad case 列表可说明不同模型将哪些不适用条目排在前列,作为选型依据。
“只能参考”的条目应与“可直接复用”的条目分开统计。前者排在前面可能有助于调研,但不能替代直接经验。单独统计后,可区分模型返回的是主题相近的资料,还是条件也相符的经验。
还需要按查询类型拆分结果。一个模型整体 MRR 最高,但在标识符查询上持续失败时,不应单独承担检索任务。此时可由它负责语义召回,再由关键词检索补充接口名和组件名。
相似度分数不能单独作为全局采用阈值
一次评测中,某条查询的最高余弦相似度可能为 0.82,但不能据此规定高于 0.8 的条目即可采用。该阈值无法覆盖不同模型、查询和语料条件。
相似度分数的含义由模型、语料分布、查询长度和知识库共同决定。同一个数值出现在不同模型上不代表相同的相关性;同一个模型面对 “reload” 等短查询和完整问题时,分数分布也可能不同。embedding 用于排序,不能单独判断某条经验能否直接采用。
相似度分数应作为诊断信息,不应作为全局采用阈值。对于每个模型,分别查看直接相关、只能参考和不适用条目的得分区间是否重叠。三类样本大面积重叠时,单个 embedding 阈值无法区分它们。此时可先按项目、版本、经验状态等条件过滤,再用关键词和向量共同召回,最后由重排模型或当前代码验证。
评测记录还应包含建库耗时、单次查询延迟、索引体积和调用成本。模型的速度不能抵消其漏掉关键经验的影响。若模型用于第一阶段召回,应结合性能和 Recall@50 评估;若检索结果直接进入上下文,应关注 top 5 的准确性。
将评测结果转为检索配置
模型评测完成后,还应挑出每个模型表现最差的查询,并回到原始经验条目检查原因。
部分失败由 embedding 的语义匹配造成,例如同一概念使用不同表述、相关条目没有共同词时,模型未将它们排在相近位置。部分失败来自切分:组件契约和反例被拆到两个片段里,任一片段都不足以判断是否适用。查询包含明确的组件名和 ref 方法时,可加入精确字段过滤或 BM25;此类条件不应仅通过更换 embedding 模型处理。
评测结论应记录为具体检索配置。例如:某个模型作为语义召回器,返回 50 个候选;查询包含组件名、prop 名或错误码时,同时执行关键词召回;经验状态不是 verified 的条目不进入默认方案;检索结果注入 Agent 前仍需核对当前代码。这些条件可用于复现评测配置,并会直接影响检索结果。
embedding 用于排序候选集,确定哪些条目进入后续处理。它不验证接口是否仍存在,不判断当前分支是否已变更,也不保证过去的经验可以直接套用。下一篇将讨论如何处理候选集中的精确标识符、语义匹配和不适用经验,以及为何需要将关键词检索、向量检索、重排和拒绝直接采用组织在同一条链路中。
