loading

Loading

探索与发现

首页 Agent 工程

工具调用预算和上下文压缩得一起算账

分类:Agent 工程
字数: (2684)
阅读: (5)
0
摘要:工具响应吃掉大量预算,压缩器却在另一套阈值里单机运作。本文梳理 Hermes、Claude Code、Suna 等系统的压缩与预算实现,讨论两者联动时尾部预算、摘要开销和恢复锚点该怎么留。

把 Agent 的上下文压缩和工具调用预算当成两个独立模块,是很多线上事故的起点。压缩器按自己那套阈值决定砍什么,预算模块按步数和 token 上限决定还能不能继续调工具,两边互不知情。结果是压缩刚把上一轮的 tool_result 抹成一行占位符,预算模块又放行了新一轮工具调用,而模型手里剩下的信息已经不足以构造正确的参数。

先看一组数量级。公开的统计里有一个挺有说服力的数据:工具响应占 Agent 总 token 的 67.6%,系统提示只占 3.4%。也就是说,上下文真正被撑爆的地方是工具返回值——读文件的几百行代码、网页搜索的几千字正文、一次 pytest 的完整输出。想靠优化 system prompt 来省上下文,等于在一栋失火的房子里修玄关。

压缩的触发点和预算的剩余量应该互相看得见

Hermes Agent 的做法比较有代表性。它把压缩分成两层:网关侧在上下文用到 85% 时做一次会话清理,作为安全网,防止 Telegram、Discord 这类场景隔夜堆出巨大会话直接打挂 API;agent 自己的 ContextCompressor 默认在 50% 触发,运行在工具循环内部,能拿到 API 实际报告的 token 数。

这两层阈值为什么不设成一样?资料里给的理由很具体:如果网关也用 50%,长会话每一轮都会提前触发压缩,白白多花摘要模型的调用和延迟。把安全网放高,正常机制放低,是在让主压缩器先干活。


真正该连起来看的是压缩器处在什么位置、预算剩多少。一个可用的规则是:压缩后保留的尾部 token 预算,必须大于下一轮最坏情况下的单次工具返回。项目里常见的 target_ratio: 0.20 之类的默认值来自通用经验,但如果你某个工具一次能返回 40K token 的 CSV,那么 20 万上下文模型下手握 100K × 0.20 = 20K 的尾部预算就不够,压缩刚结束就可能被一个新工具结果顶穿。

压缩过程的成本要计入调用预算

压缩不是免费的。Hermes 的摘要预算公式是 content_tokens × 0.20,下限 2000 token、上限 min(context_length × 0.05, 12000) token。Claude Code 走的是五层压缩引擎,从低成本阶段往上叠:先做预算缩减,再做剪裁去掉多余空行、过长缩进块、已被后续编辑覆盖的旧文件版本,然后才轮到微压缩等更重的步骤。排序原则就是便宜的先上。

这里有个容易踩的坑:摘要模型的上下文窗口必须不小于主 agent 模型。否则整段中间历史一次性送进去会直接返回上下文长度错误,Hermes 的实现会捕获这个错误、记一条警告、返回空摘要,然后丢弃中间轮次而不带摘要——静默丢上下文,是压缩质量下降最常见的原因。你会在几十轮之后突然发现 agent 忘了最初的约束条件,而日志里只有一个 warning。

从预算角度看,这意味着两件事:一,压榨后的连续多轮里,agent 实际拿到的总 token 数并不会线性下降,因为摘要本身也要付钱;二,若会话特别长,触发多次压缩,原文提到 Hermes 会做迭代重压缩,把上一次的摘要连同指令一起交给模型更新,条目从“进行中”移到“已完成”。这个机制能让信息在多次压缩里存活,但也让每次压缩多一次模型调用的账。

Suna 的演进方向是个有意思的反例。它早期的 context manager 是构造一段精心设计的 prompt 丢给 LLM 摘要,7 月的重构把 LLM 移除了,改用基于文本长度的本地截断,同时提供了 expand-message 工具,被截断的超长消息带 message_id 标记,模型需要时可回查完整内容。这样做出来的取舍很清楚:本地截断耗时稳定,省掉摘要那笔 token,但代价是上下文里常驻着“这里有东西被砍掉了”的痕迹;压缩不再是有损的语义提炼,变成了分页。哪种更好,取决于你的场景容不容忍模型做一次额外的工具调用去补细节。

预算:一次工具调用至少占三段开销

工具调用的成本不是一个数字。第一段是工作流本身从上一轮结束点重新读上下文时的 prefilling;第二段是模型花在 CoT 上的 reasoning token;第三阶段是天真的工具返回值。真正的陷阱往往在第一段和第三段的相加:如果你用推测解码、prefix cache 做优化,却忽略了工具响应输入经常是一个巨大的非缓存命中段落,那压缩策略就会与缓存策略打架。

这类问题的解法是让压缩与预算共用一份可观测的 token 台账。每次准备调用工具前先预测返回大小(正常情况下压到 read_file 的 max_lines / 搜索的 top_k 上去固化),如果 remaining_budget < 预计返回 + 兜底,那就先清理遗留的 snapshot,把文件读取改成离线读结果列摘要,但这一轮请求仍然放行。短期看像是把超支从下一轮挪到了这一轮,但上下文确实少了一条垃圾结果,代价是用户多看不到完整的工具响应。

关键在于是“把完整证据传给模型”还是“把证据拦在门外”。对大输出场景,正确做法通常是让工具层返回已裁剪、带分页游标和 message_id 的可替代结果,而不是靠压缩器做统一砍伐,因为压缩器看不到领域语义。

真实业务里,模型早就练出了在缺证据时发起扩查的本能,因为多数 harness 就是让它“记住 truncation 警告-下次再拉具体段落”这样驱动工具循环。所以一个报文的摘要摘要也不完美,但尾部紧丢掉可还原的线索,反而让 agent 进入无效循环。要想少踩,压缩逻辑至少该替 agent 留一个 message_id / turn_span_id / 查询窗口之类的恢复锚点,被截断的 tool_result 可以机械地通过一次工具调用恢复原文。Hermes 在 lean 尾部模式里做的是类似的“降级成一行占位 + 恢复指引入口”的设计,保留 session_search 的路径指向。

结论

把上下文压缩和工具调用预算拆成两个人管,Agent 就会在两者边界上反复出事。更稳的设计是把它们放在同一个循环里:用真实的 API token 数做 watch,用最便宜的清理步骤优先触发,为预留的尾部预算叠加最大工具返回实测值而不是经验比例,压缩器不允许输出不可恢复的“上下文段落被吞掉”警告。剩下的就是把这些逻辑都写便宜——能本地截断的不调摘要模型,因为模型的稳定输出还没那种命。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/agent-engineering/681.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图