工具多不是问题,工具返回的东西重复才是。一个 Agent 在一次任务里查三次天气、两次库存、四次同一个订单,每次拿回来的 JSON 有一大半字段是同一个副本。模型看到的是四份“物流状态=已发货”,却要花四次注意力把它对齐成同一个事实。工程上这件事落到两个账单上:Prompt 的组合 token 和模型的判断噪声。
所谓语义去重,不是把两份 JSON 字符串去掉尾逗号后做 diff。你是要判断“这两个工具结果说的是不是同一件事”,如果相同就留一份,不同就在上下文里明确写清楚差异。前者省 token,后者省事故。
先搞清楚要去重的是什么
Agent 工具输出大体分三类,去重策略完全不同:
- 幂等读:天气、汇率、商品详情、订单状态。同一 query 短窗口内重复返回,内容几乎不变。这类是最该去重的。
- 列表类:搜索接口、日志查询、分页游标。两次返回可能重叠一部分条目,需要按主键合并而不是整体替换。
- 有副作用的结果:创建订单、发送消息、改配置。这类结果里的返回体可能长得一样,但每次代表一个不同的动作,去重掉会让 Agent 误以为刚才那次没执行。
最容易踩坑的是第三类。很多人写了一个统一的 dedupe 中间件,按内容哈希去重,结果把两次成功创建不同订单的响应折叠成一条,Agent 后续回查时把两单当成一单处理。副作用工具的输出应该带上动作 ID 或操作日志的 link,去重只允许去重展示字段,不允许改动标识字段。
语义相同的判定放在哪一层
代码层判定最可靠,也最便宜。给工具写结果模式(schema)的时候顺手标注一个“业务主键 + 语义版本”。主键相同、语义关键字段集合相同,就判定语义相同。比如订单查询的主键是 order_id,语义关键字段是 status、latest_shipment_time、amount。三个字段都没变,就可以把第二次的完整返回折叠成一句“status 仍为已发货,与上次一致”,保留变更时间。
纯代码判定的问题在于工具数量一多,每个工具都要维护主键字段表和关键字段表,维护成本上去后大家会偷懒,全部退化成按整个 payload 哈希。这个退化会引发另一类错误:接口每次返回带了 service_time 或 nonce 这类无意义的变化字段,哈希就永远不同,去重率趋近于零。
向量召回那一套在这里不该用来做去重决策。工具的返回体经常是数字和枚举,embedding 的区分度不如字段比对。更适合向量的是工具描述检索,把“这个工具是干嘛的”和“这个结果跟当前任务是否有关”交给模型打分,用来决定需不需要把整块结果放回上下文。两件事别混在一起做。
分层剪裁比整块删更好
一个常见的错误做法是把重复的工具结果整块删掉。模型不是数据库,它获得的是一个连续叙述。你把上一次结果从 trajectory 里删了,它就会去找上一次的引用,或者重新调用一次工具。来回几次后步数预算就被这种补账耗掉一大半。
做法上可以把结果分三层保留:
结论层也常被称为摘要,始终留在上下文里,写的是“订单 A 从已发货变为已签收”。证据层只在新结论被生成的这一轮保留,再下一轮只保留引用指针。原始层原样保存,但不放进上下文,标记成可检索。真需要它时,通过工具名加 key 的寻址能捞回来。
去重过程中不需要复述整段 JSON,只需要告诉模型“这跟之前一样”和“哪里不一样”。只要确认两边语义等价、指得回原始层,压缩就不会丢事实。
边界:什么时候语义相同也不该合并
检索类结果集永远不该按内容去重。两次相似但不同的搜索结果,条目本身不同,合并会把新出现的链接吞掉,模型以为没查到。
含时间序列的工具输出,比如监控区间、库存流水,也不能按投影后的结论合并。可以压缩成“其余间隔与上次一致”,但不能把序列改写成单值,因为下一步的推理往往要吃走势。
列表类结果合并要处理删除。第二次结果不含上一次的某个条目,不一定代表它被删除,也可能只是排序截断。只有在明确拿到完整快照,并且工具声明此次返回是全量的时候,才允许把缺席条目解释为已删除。
一条把结果折叠成更新的实现骨架
下面这个形式可以直接落在 Agent 的工具中间件里,不需要上向量库:
// 伪代码:工具返回后先判定相关性再折叠
ResultStore.put(orderSnap); // dedupeStore 里存储的是上一次的快照
DeltaHandler.inject(orderSnap, orderDelta); // DeltaHandler 负责写进上下文
if (orderDelta.kind == SPECULATIVE && !orderDelta.hasVerdictReview) {
orderDelta.markPendingReview(); // 推测出的结论先打待复核标记
}
mergeNodes(prevBaseNode, stateDeltaNodeStrids.index);最后一行是把真实基线节点和本轮的增量节点拼起来。生产里我吃过这个顺序的亏:没真实基线直接拿推测起点当真相,写了很久才发现上游订单 id 搞错了,delta 根本没法往上拼。所有 dedupe 的时间线必须收敛到一个已知真实的基线节点(注释里叫 baseNodeStrIds),初始锚点要显式写入,运行时产生的新分支由运行时侧的 worker 收口。db 字段偏移也可能让基线节点里的记录对不上,必须在重建的时候先做一次锚点核对。这个约束看起来跟去重无关,实际上是判断“哪个结论算正式结论、哪个只是内部诊断”的分界线。
计费账怎么算
我把线上一次任务的上下文 token 结构拆出来过:工具结果平均占整轮上下文的 55%,其中同一实体的重复出现在 17 个订单模型的场景里占了大概 34% 的 tool token 支出。把这部分操作列出来,任务 token 中间位数(P50)改变了 18% 的取值范围(上述压缩方案处理的是同一个变量),P95 是 26%。不同任务类型相差很大;只调用单工具的任务没什么账可记。
这部分成本节省有意义,但不能回头告诉团队“去重做完了,规模可以翻倍”。另外也要注意压缩让某些推理的路径少了,个别任务反而失败,这就要求单独保留一组未命中去重的评测集做对照。
去重中间件自身也会出错。最常见的实现故障是把上一轮的历史残留当成当前结论,要求所有结果都必须先和基线合并;合并失败的直接丢弃,丢弃后必须补充一条说明,否则模型以为哪一步被静默跳过了,就会开始编造。
转载请注明出处: 皮哈皮阿
本文的链接地址: https://blog.phpat.com/ai-dev/664.html
微信或手机浏览器扫一扫,随时随地随心阅读与分享
-
上下文工程:从 Prompt 到 Context 的工程化实践
客服 Agent 答错时,团队第一反应是换模型。把那次请求的上下文打印出来才发现,模型看到的是一份过期政策、两份互相矛盾的条款,还有一长串无关日志。上下文工程要解决的,就是这类问题。
2026/10/06
-
RAG 的下一站:Agentic Retrieval
用户问“我们去年买的套餐现在还能退吗”,经典 RAG 检索出一段通用退款政策就结束了。可这个问题至少要拆成订单时间、地区、套餐版本三件事。Agentic Retrieval 要解决的,就是这种“一次检索不够用”的场景。
2026/10/12
-
Agent 上下文压缩:四层策略与失败代价
把 Claude Code 的四段分层与 Hermes Agent 的双阈值压缩系统并排看重:200K 窗口下的 50%/85%、legacy 与 lean 尾部的实测差池、未处理到的摘要模型窗口溢出,以及工具对配对边界为什么是能不能跑的问题。
2026/10/23
-
约束解码让 JSON 不再崩,但它会改坏推理
约束解码让 JSON 解析失败率归零,工程师可以删掉一堆重试和修复代码。但它会挤占模型的推理预算:强制格式在部分任务上带来10%以上的准确率下降。文章梳理 OpenAI、Anthropic、Gemini 和其他自托管引擎的现状,以及 schema 设计、两阶段模式与编译缓存几个实操要点。
2026/10/22
-
推理模型输出被截断后怎么接回去
推理模型遇到输出截断,重试整条请求代价高;续写也有隐藏门槛。从识别边界、提示回灌到 prefill 与成本判断,聊聊工程上怎么做才不算亏。
2026/10/25
-
推理模型的并行思维链采样怎么用才不亏
并行思维链采样的成本不是白花的。结合推理模型测试时计算的几条实证结论,梳理best-of-N、束搜索、自洽性投票和自我修订在什么场景下划算、什么场景下只是在烧钱。
2026/10/26
京公网安备11011402053654 
@muffin:链接已更新,分类图像功能...
AI网址导航模版图标库更换指南
作者大大,图标库邀请链接失效了,麻烦更新...
AI网址导航模版图标库更换指南
太好看了,快点更新!
友情链接
@戴墨镜的刘备:哈喽,路过点赞。
vue.js组件之手写签名板vue-signature-simple
太好看了,快点更新!太棒了!👍真的很赞!
vue.js组件之手写签名板vue-signature-simple
你写得非常清晰明了,让我很容易理解你的观...
Emlog插件RSS订阅器发布