1. Agent-Reach 解决的核心问题:为什么模型明明会答,却总答不上来
上个月我在做内部知识库问答 Agent 的评测,碰上一件特别拧巴的事:同一批文档、同一个模型、同样的 Prompt,上午跑一轮测试还有 13% 的问题完全答偏,下午我把检索参数换了换,错误率直接掉到 6%。模型没动,文档没动,唯一变的是召回策略。当时我就意识到,这类 RAG 应用里最折磨人的根本不是"模型不够聪明",而是 Agent 压根没触达它需要的那段信息。
这个现象在行业里太普遍了。很多人把 Agent 答错归因到模型推理能力不行,但实际排查下来,大量失败发生在更早的阶段:关键段落没有被检索出来、检索出来却被截断了、上下文里塞满了无关内容把答案淹没了、或者 Agent 不知道该调哪个工具。这些都不是"会不会推理"的问题,而是"够不够得到信息"的问题。我把这一类失败统称为触达失败,然后做了个小开源项目 Agent-Reach,专门用来量化、追踪和优化 Agent 的触达能力。
Agent-Reach 解决的核心问题就是这么一句话:当一个 Agent 答错时,你能分清它到底是没找到信息、没看到信息、还是没用对信息。它不是又一个 Agent 框架,也不是大模型评测工具,而是一层专门盯着"上下文与工具触达过程"的观测与评分层。
适合用它的场景大概有三类。第一,你在跑 RAG 应用,反复调 chunk、调 TopK、调 Prompt 却拿不出一组数据来证明哪个改动真正有效,Agent-Reach 能给你一张分阶段得分表,比单看最终准确率有用得多。第二,你在做 Agent 的横向对比,想区分两个 Agent 的差距到底来自工具路由、检索覆盖率还是上下文组织方式。第三,你的 Agent 已经上线,需要一套能持续监控"信息交付质量"的指标,而不是等问题爆了才去翻日志。
我得先把话说清楚:Agent-Reach 不是要替代 RAG 评测里的 Recall、EM、ROUGE 这些指标,它是一层更细的探针,专门回答"信息在到达 LLM 之前到底经历了哪些损耗"。下面我会从这套项目的设计逻辑开始,把每个计算口径、接入方式、调参心得和踩坑记录都过一遍。
1.1 一个真实案例:答案在文档里躺着,模型还是答错了
先看一个我实际测出来的例子。某产品的售后政策文档里有这么一段话,在第 3.2 节:"已拆封的电子配件,在不影响二次销售的前提下,支持 7 天无理由退货;但如果配件已安装或通电,则不再适用无理由退货,仅支持质量问题换新。"
用户问的是:"我拆封了但没通电的耳机,能退吗?"
正确答案应该是"能退",因为拆封不影响二次销售。但这个 Agent 答的是"不能退",理由写得头头是道:"已拆封产品不适用于无理由退货。"我当时差点以为模型逻辑错了,后来打开检索日志一看,问题出在文档预处理阶段。这个文档的切片器把"已拆封的电子配件,在不影响二次销售的前提下"切到了 chunk 17,"支持 7 天无理由退货"切到了 chunk 18,而"配件已安装或通电,则不再适用"又跟着后面的内容进了 chunk 19。检索时 TopK=3,召回的段落里只有 chunk 17 和 chunk 19,偏偏少了 chunk 18。模型看到了"已拆封……不再适用"这类片段,自然就回答了"不能退"。
这个案例特别典型:embedding 模型没问题,LLM 也没问题,文档内容完全够用,输在切片和召回配合上。模型根本没触达到那个"支持 7 天无理由退货"的片段,你让它怎么推理都不可能有正确结果。这就是触达失败的实例。
我用 Agent-Reach 跑这个 case 的时候,输出很清楚:检索覆盖率只有 0.33(三个黄金片段只召回一个),有效上下文载荷 0.21(真正跟答案相关的 token 占加载总量不到四分之一),触达失败阶段被定位到 retrieval+chunking。你一看就知道该去改切片或调 TopK,而不是去换模型、改 Prompt。这个归因能力就是我做这个项目最原始的动机。
1.2 触达的三个动作:找得到、装得下、递得准
既然要量化触达,我第一步是把"触达"拆成三个可以独立测量的动作。
找得到,对应的是检索触达。Agent 的信息来源不管是向量数据库、搜索引擎还是 API,第一步都是把可能相关的候选信息捞出来。这一步失败的典型表现是召回结果里根本没有黄金片段,不管后续怎么组织上下文都没用。衡量指标是硬召回率 Recall@K(hard),也就是一组预标注的"必须触达片段"中有多少个真的被召回。
装得下,对应的是上下文触达。信息被找回来了,但能不能以干净、完整、不被打断的方式进入 LLM 的上下文窗口,这是另一回事。这里常见的损耗包括:上下文截断把关键句切掉、召回了太多无关内容把有效信息稀释、系统 Prompt 太长挤占了任务上下文。我用的指标是有效上下文载荷 EffectiveLoadRatio,计算方式是"最终进入模型的上下文里,与答案直接相关的 token 数 / 该次请求加载的总上下文 token 数"。这个比值能直接反映上下文有没有被污染。
递得准,对应的是工具触达。多步任务里,信息往往要通过正确工具才能拿到,例如查库存得调库存服务、查物流得调物流服务。如果 Agent 在意图分类时搞错了工具,就算工具本身没问题、文档里也有数据,结果照样错误。这一步我用工具命中率 ToolHitRate 来衡量:每一条需要调用工具的步骤中,Agent 正确调用目标工具的次数占比。
这三个动作合起来就是 Agent-Reach 的评分骨架。每个动作对应独立的调试方向,这也是它区别于"最终答案对错"这类黑盒指标的最大价值——失败归因直接落到可改的环节上。
1.3 三种典型用户和各自的使用姿势
我自己把它分成三种用法,对应三类人的不同目标。
做 RAG 应用调优的工程师,核心动作是"定向优化"。他们已经有了一个能跑的系统,但效果不稳定,需要在改切片参数、换 embedding、调重排策略之间快速决策。这类用户建议跑离线评测模式,把优化前后的 Reach 得分并排对比,哪个阶段的分涨了、哪个阶段没涨,一目了然。
做 Agent 评测和横向对比的算法工程师,核心动作是"归因拆解"。两个 Agent 在同一批任务上,你用 Reach 这三个分项对比,能看出 A 系统检索覆盖率更高但上下文组织更差,B 系统工具路由更强但召回偏弱,这样你在写评测结论的时候就有据可依,不用拍脑袋说"A 更好"。
做线上 Agent 运维的工程同学,核心动作是"持续监测"。线上跑着几十个 Agent 任务,不可能每个都人工看。Agent-Reach 的旁路模式可以按采样比例抓取运行日志,按小时维度统计 Reach 得分分布,分数一旦跌破阈值就自动标记,方便回溯到具体 query。这个模式牺牲一点精确度,换来的是低侵入、低延迟,线上比较友好。
2. Reach 得分的计算逻辑:把"触达"拆成三个可测算的动作
聊完了理念,下面进入这个项目最核心的部分:Reach 得分到底是怎么从一堆原始日志里算出来的。这一章我会把计算口径、事件结构、加权公式都展开讲清楚。如果你只想用工具不想深入细节,可以跳过公式,直接看 3.3 的 Demo,但理解计算逻辑对后期调参会有很大帮助,我建议还是看一眼。
2.1 三层评分的具体口径与公式
Agent-Reach 的最终得分是一个加权后的综合值,范围固定在 0 到 1 之间。综合得分的公式长这样:
Reach = w1 * Recall@K(hard) + w2 * EffectiveLoadRatio + w3 * ToolHitRate其中权重 w1、w2、w3 默认是 0.4、0.4、0.2,因为从我的经验看,检索和上下文这两层决定了大多数问答任务的成败,工具层只在多步 Agent 场景里权重才需要调高。你也可以在配置里改成 0.3 / 0.3 / 0.4,看任务类型。三个分项的具体计算口径如下。
Recall@K(hard) 计算的是硬召回率:每个评测问题会预先标注一组黄金片段,编号记入 golden_chunk_ids,跑完检索后看最终进入候选集合的片段里命中了多少个。注意这里用的是"进入候选集合"而不是"进入最终上下文",因为这一层只管"找得到"。
EffectiveLoadRatio 计算有效上下文载荷,这个指标在传统 RAG 评测里几乎见不到,是我自己做项目后加上的。设想一个场景:检索召回了 10 个片段,一共 3000 token,但真正回答用户问题需要的可能只有中间 200 token。LLM 面对这 3000 token 时,那 200 token 的信息被大量噪声包围,照样很容易答错。EffectiveLoadRatio 就是"有效信息 token 数 / 上下文总 token 数"。
ToolHitRate 计算工具命中率,多步任务单独统计:Agent 在推理过程中每次调用工具都记录意图标签和实际调用工具,比对两者是否一致。这个数据在正常的 Agent 日志里通常有迹可循,但很少有人专门把它做成指标。
三个子分数会用事件日志聚合,聚合层级是"每个问题一条总分 + 每个阶段一条子分",方便同时看单条质量和大盘趋势。
2.2 事件日志流:每个步骤都会留下一条 ReachEvent
要让上面的公式落地,前提是每个阶段性动作都有结构化日志。Agent-Reach 定义了一套叫 ReachEvent 的事件流,每个事件记录一次检索装载、一次工具调用、一次上下文构建的现场信息。
一个典型的检索装载事件长这样:
{ "event_type": "retrieval_loaded", "query_id": "q_001", "agent_session_id": "s_20240618_1145", "chunk_ids": ["c_017", "c_018", "c_019", "c_032"], "golden_chunk_ids": ["c_018", "c_012"], "loaded_tokens": 1240, "golden_tokens_in_load": 480, "stage": "retrieval", "timestamp": "2024-06-18T11:45:32Z" }这条日志能直接推导出两个分数:Recall@K(hard) 是 0.5,因为两个黄金片段召回一个;EffectiveLoadRatio 是 0.387,因为 480 / 1240。注意 $golden_tokens_in_load$ 和 $golden_chunk_ids$ 的区别:前者只统计被载入上下文的那部分 token 中与答案相关的数量,后者只管召回。
工具调用事件是另一套结构,里面有意图标签和实际工具名。为了让 ToolHitRate 能自动算出来,我会在评测集里给每条需要调工具的任务打上 expected_tool 标签,然后跟日志里的 actual_tool 比对。
所有事件会先落到本地文件或者一个轻量队列里,然后由聚合器按 query_id 分组计算得分。这样设计的好处是采集和计算解耦——线上只负责记录事件,评分可以放到离线批次跑,不会阻塞 Agent 主流程。
2.3 为什么传统的 Recall、EM、ROUGE 替代不了这套逻辑
我遇到过不少人问:RAG 评测不是有现成的 Recall 和 Answer 指标吗,为什么还要搞一套新东西?原因很简单,这些老指标回答的问题和触达问题不是同一层。
传统 Recall 只看"相关文档是否被召回",但它不问"召回的内容是否真的进入了模型视野"。如果你的上下文被截断,或者召回的片段顺序不对导致关键信息被切掉一半,Recall 照样 1.0,但模型就是看不到关键句。EM 和 ROUGE 这类答案对比指标更靠后,它们只为最终文本打分,一旦推理模型很擅长从错误上下文里自洽地编一个错误答案,EM 和 ROUGE 也无法告诉你问题出在检索还是提示词。
拿 1.1 那个耳机退货的例子来说,传统指标会告诉你"模型答错了,EM 得分低",这当然对,但没用——你仍然不知道要去改 chunk 大小。用 Agent-Reach 的日志,你一眼看到 retrieval 阶段 Recall@K(hard) 只有 0.33,而上下文阶段 LoadRatio 只有 0.21,归因已经定位到"信息没找全 + 找到的信息也不够聚焦",你就能有的放矢。这就是我说 Agent-Reach 是探针而不是量表的原因:它不评价最终质量的高低,只测量信息在到达 LLM 之前损耗在哪里。
3. 从零接入:三种接入模式和一份最小可用 Demo
原理讲完,该上手了。Agent-Reach 目前提供三种接入模式,分别对应不同侵入程度和不同用途。我按从轻到重来介绍,然后给出一份能直接跑通的最小 Demo。
3.1 旁路模式:不碰主流程,只挂日志探针
旁路模式是最推荐的起步方式,尤其适合线上环境。它的思路很简单:Agent 主流程保持原样,通过一个探针层劫持或者包装检索器、上下文加载器和工具调用函数,把这些环节的出入参记录下来,生成 ReachEvent 后异步写入本地或远程队列。
接入成本很低,基本等同于给现有函数加一层包装。比如你原本有这样一个检索函数:
def retrieve(query: str) -> list[Chunk]: ...通过 Agent-Reach 的 SDK,你只需要一行代码把它包起来:
from agent_reach import ReachTracker tracker = ReachTracker( endpoint="http://localhost:8000/events", sample_rate=0.1, # 线上建议 10% 采样,避免日志量过大 mode="sidecar", # 旁路模式,不修改 Agent 返回结果 ) retrieve = tracker.wrap_retriever(retrieve, top_k=5)这个模式的好处是:随时可以空降到一个已上线的系统上,不用改业务代码,不用重新部署 Agent,只需要在启动入口加几行初始化代码。坏处是它拿不到 Agent 内部的思维链,只能看到外部函数进出数据,所以它更适合做"输入输出质量监测",不适合做深度归因。
3.2 评测模式:离线批量跑数据集,做版本对比
需要深挖触达瓶颈、对比优化效果时,换评测模式。这个模式需要你准备一个评测数据集,每条数据至少要包含 question、golden_chunk_ids、expected_tool 三个字段。Agent-Reach 会按数据集逐条跑,跑完后输出一份带阶段标记的评测报告。
评测模式的接入动作是把 Agent 的整个执行函数交给 runner 控制。跑一遍类似这样:
agent-reach run \ --agent my_agent:run_agent \ --dataset eval_dataset.jsonl \ --output report_0620.json \ --weights 0.4 0.4 0.2输出 report 里会按 query 列出三个子分和综合分,还会标注每个 query 的"最短路径失败环节",比如有些 query 是检索覆盖率不足,有些是上下文载荷过低,有些是工具调用错误。这个报告我一般直接拿去做版本对比,例如上周跑一份、这周改完检索参数再跑一份,diff 两份报告就看出改动方向对不对。
3.3 最小 Demo:五分钟跑通一个回收测试
下面是我的建议部署流程,走一遍你就能理解整体结构了。整个 Demo 用的是我自己写的一个极简 ReAct Agent,结构是完全标准的那一套:LLM 生成下一步动作 → 如果是检索就调向量库 → 如果是回答就直接输出。
首先安装:
pip install agent-reach然后初始化一个简单的检索器和一个工具函数,再用 ReachTracker 包装它们:
from agent_reach import ReachTracker from agent_reach.dataset import ReachDataset # 1. 构造 trackker tracker = ReachTracker( endpoint="file://./reach_events.jsonl", mode="eval", ) # 2. 模拟一个检索函数 def fake_retrieve(query: str): # 实际项目里这里接的是向量库 return [ {"chunk_id": "c_017", "text": "...", "tokens": 310}, {"chunk_id": "c_018", "text": "...", "tokens": 280}, ] # 3. 包装 retrieve = tracker.wrap_retriever(fake_retrieve, top_k=3) # 4. 加载评测集 ds = ReachDataset.from_jsonl("demo_data.jsonl") # demo_data.jsonl 每行形如: # {"question": "...", "golden_chunk_ids": ["c_018"], "expected_tool": null} # 5. 逐条跑 for item in ds: chunks = retrieve(item.question) result = my_agent_completion(item.question, chunks) tracker.finish_query(query_id=item.id) # 6. 输出报告 report = tracker.report() print(report.scores())注意第 5 步里的 my_agent_completion 是演示用的占位函数,你实际接自己的 Agent 生成逻辑即可。跑完这一步,你会在 report 里看到每个 query 的三项子分与综合分。
3.4 看懂报告:一张 Reach 报告怎么读
我截一份真实到不能再真实的报告样例(字段做了演示化处理):
| query_id | Recall@K(hard) | EffectiveLoadRatio | ToolHitRate | Reach | 失败定位 |
|---|---|---|---|---|---|
| q_001 | 1.0 | 0.62 | - | 0.85 | 无 |
| q_002 | 0.5 | 0.40 | - | 0.45 | 检索阶段:黄金片段 c_012 未被召回 |
| q_003 | 0.75 | 0.33 | - | 0.52 | 上下文阶段:目标片段被截断 |
| q_004 | 0.67 | 0.55 | 0.0 | 0.53 | 工具阶段:调用了错误工具 |
常见读法分三步。第一步看综合分偏低的那批 query 集中在哪个失败定位上,如果 70% 标的是检索阶段,那就去改召回;第二步单独看 EffectiveLoadRatio 这一列,如果普遍低于 0.3,说明上下文组织有系统性污染,需要动重排或过滤逻辑;第三步看 ToolHitRate 低的任务集中在哪些工具上,说明这些工具的意图边界有歧义,需要给工具描述补充正反示例。
报告里我特意保留了 raw event 路径,方便你觉得某条分数不对时,直接翻原始日志复核。因为自动标注的黄金片段存在误标可能,完全相信分数而不复核原句,是部署 Agent-Reach 最容易犯的错误之一,后面我还会展开讲。
4. 调参实战:我把一个小型问答 Agent 的 Reach 从 0.58 拉到 0.93
这一章拿一个我实际做过的小项目当案例,完整走一遍调参链路。目标是让大家看到 Reach 得分是怎么指导决策的,而不是干讲理论。案例场景是一个公司内部政策问答 Agent,文档大约 20 份 PDF,总字数 30 万左右,问题类型包含售后、请假、报销、IT 支持四类,一共准备了 60 条评测问题。
4.1 先花 20 分钟标注黄金数据集
很多项目一上来就急着调参,这是最常见的误区。Agent-Reach 的使用前提是有一批带标注的评测数据,否则 Recall@K(hard) 根本无从算起。标注方法很朴素:把文档按既有的 chunk 方式切片,对于每个评测问题,人工判断"要回答该问题,模型必须看到哪些切片",把这些切片编号写进 golden_chunk_ids。60 条问题,两条业务负责人加我一个算法,花了大概一个下午,不算多。
动作快的同学可以尝试半自动标注:先跑一次带大窗口检索的 Agent,把召回的 Top 50 片段拿给 LLM 筛选"哪些可能相关",再人工复核。LLM 预筛能省不少时间,但最后一定要人工过一遍,否则容易引入标注偏差,后面会专坑你。
4.2 建基线:先知道现在有多烂
第一次用默认配置跑出来的基线数据我记得很清楚:
- Recall@K(hard) = 0.62
- EffectiveLoadRatio = 0.34
- ToolHitRate = 0.80(只有报销场景需要调工具)
- 综合 Reach = 0.58
0.58 这个数看着不高,但说实话这个 Agent 当时已经能答对 70% 的问题了。问题在于它答错的那 30%,归因几乎都在触达阶段,而不是推理阶段。有 20% 的错误是检索没召回关键片段,10% 是召回但上下文被噪声干扰。真正模型不会答的,微乎其微。这组数据直接告诉我:优化重心应该放在检索和上下文组织上,而不是换更大的模型。
4.3 第一刀切在检索:TopK 和 chunk 大小
基线数据摆出来,第一步不用说,先动 Recall@K(hard)。我当时把 chunk 大小从 512 降到 256,TopK 从 3 升到 5,原因是 512 的切片对政策文档这种密集信息类文本太粗,黄金片段经常被切得七零八落。
改动后小测试集上 Recall@K(hard) 从 0.62 升到 0.79,效果显著。但这里出现一个典型副作用:TopK 调大后,EffectiveLoadRatio 从 0.34 降到 0.27。原因不复杂——TopK 变大意味着更多次相关但未必对当前问题有用的片段进了上下文,噪声绝对值增加了。
我停下来想了想:Recall 提升是事实,Load 下降也是事实,说明指标之间存在张力,不能只看单分。于是我给检索器加了一道轻量重排,对召回的候选做一次意图相关性打分,过滤掉明显无关的片段。重排后 Recall@K(hard) 稳定在 0.82,EffectiveLoadRatio 回升到 0.39,Reach 综合分来到 0.66。
4.4 第二刀切在上下文:给加载器加了道相关性闸门
检索端提升到一个平台期后,真正的瓶颈开始转移到 EffectiveLoadRatio 上。0.39 意味着每次请求加载的上下文里,只有不到四成 token 是对回答有效的信息,其余六成都是陪跑。
这一刀我做的是在上下文加载进 LLM 之前,对每个候选片段按 query 用轻量分类模型打一次"是否有必答信息"的二分类分,低于阈值的段落直接不进上下文,宁可少不可滥。这一步有点像机场安检:先让所有人过一遍闸机,可疑的留下,不相关的放行。
改完后 EffectiveLoadRatio 从 0.39 跳到 0.66,综合 Reach 涨到 0.84。更让我惊喜的是,最终答案的 EM 也涨了大概 9 个点——这说明上下文干净与否,直接影响模型把检索结果转化为正确答案的成功率。很多场景里你觉得是模型不行,其实是噪声把正确信息淹了。
4.5 第三刀切在路由:工具描述和意图分类
四类场景里只有报销场景需要调用工具,基线 ToolHitRate 0.80,意味着五分之四调用正确,还有改进空间。翻日志发现一个规律:Agent 经常把"报销流程"和"报销额度"两个工具搞混。用户问"报销能报多少",它去查了流程工具,查回来一段流程文本,自然答非所问。
问题根源在工具描述太抽象,两个工具都写着"报销相关",LLM 分不清边界。我改成给每个工具加上触发场景示例,写成"什么时候该用、什么时候不该用"的格式:
# 工具:报销额度查询 用途:查询各类费用的报销上限、报销比例、年度额度剩余量。 该用的场景:用户询问"能报多少""额度多少""xx费用能报吗""还有多少额度"。 不该用的场景:用户询问"报销流程是什么""需要提交什么材料",此类请使用 reimbursement_process 工具。只改了描述文本,没有任何代码逻辑变化,ToolHitRate 从 0.80 升到 0.92。这一步给我的启发是:工具触达不只是路由模型的事,工具自身的签名和描述写得清不清楚,直接决定 Agent 能不能"触达"对的那个工具。
最终这轮的回归结果:Recall@K(hard) 0.88,EffectiveLoadRatio 0.66,ToolHitRate 0.92,综合 Reach 0.93。对比基线 0.58,提升是实打实的。
4.6 这里藏了一个必须说的陷阱
为了追求 Recall 提升,我把 TopK 从 3 调到 5,代价是检索延迟增加了将近 30%。在离线评测里这个代价看不出来,线上用户可不会等你多 300 毫秒。所以在做版本上线决策时,必须把延迟预算拉进考虑,不能只看 Reach 分数。
还有一点提醒:加权综合分只是为了给你一个快速比较的数字,真正做决策要盯分项。有一次我把工具层权重调高后,综合分涨了不少,但检索层其实是退步的,掩盖在总分下面。这就是为什么我建议你在调参过程中导出分阶段明细,别只看一个总分。
5. 生产环境落地最容易踩的四个坑
把 Agent-Reach 从 demo 推到生产,我踩过的坑不少,挑四个最典型的展开讲讲,希望能帮你绕开。
5.1 LLM 辅助标注会引入标注偏差
我在 4.1 提过半自动标注省时间,但如果最后不人工复核,后面所有分数都可能失真。举个实际案例:有一次我图省事,直接用 Claude 标黄金段落,AI 把一段"提到报销这个词但并不是报销规则"的内容标成了黄金片段,结果那批 query 的 Recall@K(hard) 虚高到了接近 1.0,但实际 Agent 回答质量却没变化。我花了半天排查才发现是标注错了——项目里有一半的"假阳性"来自标注,而不是检索质量提升。
我的建议是:任何一次批量标注改动,至少人工抽查 30 条以上,确认标注结果符合业务直觉再放行。自动化为的是效率,不是免检。
5.2 在回调函数里跑评估会拖慢推理
Agent-Reach 初版我犯过一个愚蠢的错误:在检索回调里同步跑了一个 LLM 做相关性评分,用来决定片段要不要进上下文。结果每次检索都多出 500ms 以上的延迟,线上 p99 直接翻倍。这个教训很深刻:任何评估和重排逻辑如果在主链路里同步执行,都会被用户感知。
整改方式放到异步队列里:检索完成先返回原始结果,相关性评分和重排在后台异步执行,下一次请求再使用已重排的缓存片段。生产环境如果对延迟敏感,建议一律走旁路+异步处理,评测可以在离线跑,不要把它塞进线上实时链路。
5.3 任务长短分布不均导致分数虚高
Reach 是一个平均聚合出来的数,如果评测集里大量是简单短问题,分数会天然偏高,因为单跳检索、轻量上下文的任务触达门槛很低。有一段时间我的报告综合分稳定在 0.9 以上,团队一度以为系统已经接近天花板,后来按任务复杂度分层统计才发现,长任务、多跳检索场景的 Reach 只有 0.7 左右。
解决方法是分层上报:按问题类型、检索轮数、上下文长度分桶,不要只报一个平均数。我现在的习惯是每次评测至少要按"短问答 / 单跳检索 / 多跳检索 / 工具调用"四个桶分别看分,分数才有对比价值。
5.4 日志合规问题:不要盲目记录全部 Prompt
ReachEvent 里会包含检索片段、工具出入参,这些数据往往带着业务敏感信息。如果直接把全量日志落盘到普通日志系统,线上数据合规风险很高。我踩过一回坑,漏配了脱敏字段,把用户订单号直接写进了检索事件,后来花了大力气清理。
Agent-Reach 的采集器目前支持三种脱敏手段:字段级脱敏、只记录 chunk_id 不记录文本内容、以及按采样率丢弃敏感 query。我强烈建议上线前就把脱敏规则配好,宁可丢一点数据,也不能把明文业务信息写进日志。
6. 在放弃这套方案之前,先确认这三件事
Agent-Reach 不是万能的,我见过有些场景它确实帮不上忙,硬上反而浪费功夫。但如果你在考虑放弃,我建议先确认下面三件事。
第一,你的任务是否真的有动态信息获取动作。如果 Agent 只是拿一个固定 Prompt 生成答案,没有任何检索、工具调用或外部数据加载,那触达问题根本不存在,这套工具对你没有价值。第二,你是否已经有一套能反映业务质量的端到端指标。Reach 是中间过程指标,它必须跟最终任务成功率配合使用,否则容易出现过程优化了、结果没变的尴尬。第三,你是否留出了标注数据的时间预算。没有黄金标注数据,Recall@K(hard) 和 ToolHitRate 就算不出来,只能退化成纯 LoadRatio 监测,那跟看普通日志差别不大。
我自己的经验是:Agent-Reach 对我最大的价值不是那个综合分数,而是逼着我给每类失败做了一个可追溯的归因路径。项目上线这几周,我的工作习惯变成了先看触达数据、再决定动不动模型。绝大多数被当推理问题报上来的 Case,最后都发现是触达环节出了问题。如果你也遇到模型明明会答却答不上来的怪现象,建议先从这个角度查一遍,你可能会跟我一样,发现"推理不行"其实是最后一块背锅的拼图。