Agent 调用工具可能返回超大结果(比如代码搜索返回 50KB),这会带来什么问题?你会怎么处理?OpenClaw 是怎么做的?

魏远标 Lv7

参考答案

调用工具得到超大结果会带来三个直接问题:token 爆炸、挤占上下文空间、延迟飙升。

如果一条代码搜索返回 50KB 文本,按 1 token ≈ 4 字符估算,光这一条就吃掉 12000+ token。

模型的 context window 如果是 128K token,一条 tool result 就干掉了将近 10%,后面的对话历史、系统提示、用户消息全被挤压,模型理解质量直线下降。

而且,API 是按 token 计费,多塞进去的这些内容绝大部分是噪声,等于花钱买垃圾。

所以处理思路就两步:限额 + 截断

给每条 tool result 设一个字符上限,超了就砍。

而截断的关键在于砍哪里。直觉是保留开头,但实际上尾部也很重要,因为错误堆栈、诊断信息往往在末尾(就像看日志,最后几行的报错信息通常最关键)。

所以最优策略是 head+tail 截断:保留开头让模型知道内容是什么,保留尾部抓住错误信息,中间砍掉加一个省略标记。

OpenClaw 实际上有两层防护

1)单条截断:给每条 tool result 设字符上限(按 context window 的 30% 计算,硬上限 400K 字符)。截断时先检测尾部有没有错误信息,有的话走 head+tail 分割(head 拿大头约 70%,tail 拿约 30% 上限 4000 字符),没有就只保留开头。截断后附加提示语告诉模型内容不完整、可以用 offset/limit 重新获取。

附加提示语如下:

image.png

2)全局预算守卫:每次发 LLM 请求前,计算所有消息的总字符开销,如果超过全局预算(context window 的 75%),就从最早的 tool result 开始,把内容替换成一句占位提示。思路是越早的结果对当前决策影响越小,优先牺牲给新内容让路。

占位提示如下:
image.png

流程图如下:

扩展知识

为什么不能只保留前 N 个字符

最容易想到的方案是直接 result.substring(0, maxLen),但这样会丢关键信息。

比如一个 grep 搜索返回了 200 个匹配,最相关的那条可能在中间或末尾。

更典型的场景是命令执行失败,stdout 里一堆正常输出,真正有用的 error 在最后几行。只保留开头的话,模型拿到的全是无用信息,还以为执行成功了。

head+tail 策略虽然也不完美,但至少能兜住两头。

OpenClaw 的 truncateToolResultMessage() 实现里,对多 block 内容还会按比例分配字符 budget,每个 block 都能分到一份额度,避免某个 block 独占所有空间。

另外 head+tail 不是对所有内容都启用的。hasImportantTail() 会检测尾部是否包含 error/exception/failed/traceback 等关键词或 JSON 闭合结构,只有检测到才走 head+tail 分割,否则默认只保留开头。

完整关键词如下:
image.png

抢占式压缩机制

单条截断解决的是”一条结果太大”的问题,但还有一种情况:每条都不超标,但累积起来总量太大。

OpenClaw 的做法是在每次发 LLM 请求前,通过 transformContext 管线自动执行全局预算检查。先把每条 tool result 按单条上限裁一遍,然后估算所有消息的总字符开销。

如果总量超过全局预算,就从最早的 tool result 开始逐条替换为占位提示,直到总量回到预算内。

核心思路是:越早的 tool result 对当前决策的影响越小,优先牺牲它们给新内容让路

这是一种抢占式策略:不等 context overflow 报错,主动腾空间。被动等溢出再处理往往来不及做优雅降级。

字符预算的计算

核心公式就是 context window tokens × 每 token 字符数 × 比例系数

拿 128K token 的模型举例:单条上限大概是 128000 × 0.3 × 4 ≈ 150K 字符,全局预算大概是 128000 × 4 × 0.75 ≈ 384K 字符。

另外 OpenClaw 对 tool result 用了不同的 token 换算系数(2 而非 4),因为代码和结构化文本的 token 密度比自然语言高,这样估算更保守也更准确。

其他 Agent 框架怎么处理

不只 OpenClaw 一家考虑了这个问题。LangChain 的 ToolMessage 默认不截断,但社区实践里通常在 tool 的 output parser 层加限制。Anthropic 的 Claude tool use 文档建议单条 tool result 不超过 100K 字符。AutoGPT 早期版本压根没做截断,文件读取返回太大直接把 context 撑爆,后来才加了 max_length 参数。

面试官追问

提问:如果截断后模型根据不完整的信息做了错误判断,你怎么处理?

回答:最直接的办法是在截断标记里告诉模型内容被截断了,让它自己决定要不要重新获取。OpenClaw 的截断后缀会明确告诉模型 Content truncated,并建议使用 offset/limit 参数或请求特定部分来获取更多内容。更进一步可以在截断标记里附上原始内容的字符数和行数,模型就能判断丢了多少信息。如果模型觉得关键信息可能在被截断的部分,可以发起更精确的二次查询,比如缩小搜索范围或者指定行号范围。

提问:head+tail 截断的比例怎么定?head 和 tail 各占多少合适?

回答:没有通用最优比例,看 tool 的类型。搜索类工具的结果通常按相关性排序,head 更重要,可以 head 占 70%、tail 占 30%。命令执行类工具的关键信息往往在末尾,tail 要多给,head 40%、tail 60%。OpenClaw 的实际做法是 tail 拿 budget 的 30%(上限 4000 字符),head 拿剩余的大部分空间,head 最少保留 2000 字符。而且不是所有截断都走 head+tail,只有 hasImportantTail() 检测到尾部含有 error/exception/traceback 等关键词时才分割,否则默认只保留开头。

提问:除了截断,还有没有其他方式处理超大 tool result?

回答:有几种思路。一是在工具端就做好过滤,比如代码搜索只返回最相关的 top 10 结果,不吐全量。二是用摘要模型先把大结果压缩成摘要再喂给主模型,Anthropic 内部就有类似的 sub-agent 做 result summarization。三是分页,把大结果拆成多页,模型可以选择翻页获取更多内容。截断是最简单粗暴的兜底方案,理想情况下应该在工具端就控制好输出量。

目录
Agent 调用工具可能返回超大结果(比如代码搜索返回 50KB),这会带来什么问题?你会怎么处理?OpenClaw 是怎么做的?