Agent 调用工具时,返回结果可能远超预期——代码搜索返回 50KB 日志、文件读取吐出 200KB 源码、exec 命令输出 MB 级构建信息。把这些原始数据直接塞进上下文窗口,会导致对话历史被挤掉、推理质量断崖下降、延迟与成本同步飙升。OpenClaw 用三道防线处理这个问题:执行层截断、Agent 端渐进式获取、会话修剪(Pruning)。前两道在工具执行时即时生效,第三道在后续轮次中持续清理。
一、超大工具结果带来的问题
工具返回的数据量和对话上下文的容量不在一个数量级。一次 npm test 可能输出 200KB 日志,一次 grep 可能返回 50KB 匹配结果,而模型的上下文窗口即使有 128K Token,也经不起几次这样的填充。
| 问题 | 直接后果 | 严重程度 |
|---|---|---|
| 上下文被撑满 | 对话历史被挤出窗口,Agent”失忆” | 高 |
| 推理质量下降 | 无关信息占比过高,幻觉增多 | 高 |
| 延迟飙升 | Token 处理时间与数量成正比 | 中 |
| 成本飙升 | 按 Token 计费的 API 费用直线上升 | 中 |
| 缓存失效 | 提示缓存前缀被大结果破坏,无法复用 | 中 |
相关研究表明,当 Prompt 中无关信息占比超过 70% 时,LLM 的准确率可能下降 30% 以上。一条 50KB 的工具结果大约消耗 1.2 万 Token,在 128K 窗口中占比近 10%,几次这样的调用就能把可用空间吃光。
二、OpenClaw 的三道防线
2.1 第一道防线:执行层截断
每个工具调用执行完成后,Gateway 会检查返回结果的 Token 大小。如果超过配置的 maxToolResultTokens 阈值,Gateway 截断结果,保留开头部分,末尾附加截断标记和 Token 统计信息。
{
"tools": {
"maxToolResultTokens": 4096,
"truncationMessage": "[结果已被截断,原始大小: {original} tokens,保留前 {kept} tokens。请基于已有信息回答或使用更精确的查询参数重试。]"
}
}
截断不是简单的 Token 级切分。OpenClaw 做的是内容格式感知截断,在语义边界处截断,保证返回给 LLM 的片段仍有语义完整性:
| 内容格式 | 截断边界 | 原因 |
|---|---|---|
| JSON 格式结果 | 完整 JSON 对象边界 | 避免破坏结构,模型仍可解析 |
| 代码输出 | 函数或类边界 | 保留逻辑单元完整性 |
| 纯文本 | 段落边界 | 保持语义连贯 |
截断标记通过 truncationMessage 模板配置,包含 {original}(原始大小)和 {kept}(保留 Token 数)两个占位符,同时承担引导功能——提示 Agent “基于已有信息回答或使用更精确的查询参数重试”。
2.2 第二道防线:Agent 端渐进式信息获取
截断标记不只是告知”被截了”,它同时引导 Agent 自主决策。Agent 收到截断提示后,可以采取渐进式策略——使用更精细的查询参数重新调用工具:
# Agent 渐进式获取伪代码
# 第一次调用:范围太大,结果被截断
result = tool_call("search_code", query="auth", scope="entire_repo")
# 结果截断标记:[结果已被截断,原始大小: 12000 tokens,保留前 4096 tokens]
# Agent 自主缩小范围
result = tool_call("search_code", query="auth", scope="src/auth/",
limit=20, file_type="*.ts")
# 结果在阈值内,完整返回
这种策略类似”分页查询”的思维模式。Agent 根据截断标记判断结果不完整,主动添加 limit、filter、时间范围、路径约束等参数,逐步获取所需信息,而非一次性拉取全量数据。
2.3 第三道防线:会话修剪(Session Pruning)
前两道防线在工具执行时即时生效,但即使每条结果都被截断到 4096 Token,几十轮对话累积下来仍然可观。会话修剪在后续轮次中持续清理旧工具结果。
修剪以 cache-ttl 模式运行,分两步处理:
- 软修剪:当上下文总大小占比超过
softTrimRatio(默认 0.3)时,对超大工具结果保留开头和结尾各 1500 字符(合计最多 4000 字符),中间插入省略号; - 硬清除:若仍超过
hardClearRatio(默认 0.5),将旧工具结果内容替换为占位符[Old tool result content cleared]。
{
agents: {
defaults: {
contextPruning: {
mode: "cache-ttl",
ttl: "5m",
softTrimRatio: 0.3,
hardClearRatio: 0.5,
keepLastAssistants: 3
}
}
}
}
两条安全规则始终生效:最近 3 个助手轮次绝不被修剪,会话首条用户消息之前的内容(如 SOUL.md、USER.md 引导读取)绝不被修剪。修剪仅在内存中进行,磁盘上的完整历史记录始终保留。
三、ToolResult 的标准化结构
OpenClaw 在工具执行层将所有返回结果统一为标准化的 ToolResult 对象,截断信息通过元数据传递:
interface ToolResult {
toolCallId: string;
content: string; // 给模型看的文本
isError: boolean;
metadata: {
executionTimeMs: number;
sandbox: boolean;
approved?: boolean;
truncated?: boolean; // 输出是否被截断
};
}
metadata.truncated 标记告诉 Agent 这条结果不完整,Agent 可以据此决定是否重新获取。不同模型提供商的工具结果格式有差异,OpenClaw 在 ToolResult 层统一抽象,在最后一步才做格式适配:
| 提供商 | 格式 |
|---|---|
| Anthropic | { type: "tool_result", tool_use_id, content, is_error } |
| OpenAI | { role: "tool", tool_call_id, content } |
这个格式差异是多模型 Agent 框架的常见痛点。OpenClaw 在 ToolResult 层统一,避免了 Provider 特定逻辑污染整个工具链路。
四、为什么不在 Agent 层做全量分段处理
一个自然的想法是:把超大结果存到文件,让 Agent 分段读取。OpenClaw 没有采用这种方案,有三个原因:
- 状态管理复杂度高:Agent 需要记住当前分段偏移量,引入额外状态管理,违反工具调用的无状态原则;
- 前 4K Token 足够:工程实践表明,绝大多数场景下工具返回结果的前 4096 Token 已包含足够信息,无需分段读取;
- 保持原子性:每次调用原子化、可重试,不引入”读取到一半”的中间态。
# 不采用的方案:文件存储 + 分段读取(引入状态)
offset = 0
while True:
chunk = read_file("tool_output.txt", offset=offset, limit=4096)
if not chunk:
break
# Agent 需要记住 offset,违反无状态原则
offset += len(chunk)
# 采用的方案:截断 + 提示重新查询(无状态)
result = tool_call("exec", cmd="npm test")
if result.metadata.truncated:
# Agent 自主决定是否用更精确的参数重试
result = tool_call("exec", cmd="npm test --grep='auth'")
对于确实需要处理海量数据的场景,OpenClaw 推荐将工具封装为 Skill:Skill 脚本在本地处理数据,只把处理后的摘要或结论返回给 Agent。这样即使 Skill 内部处理了 50MB 数据,Agent 上下文中也只有几百 Token 的摘要。
五、实际配置建议
根据工具类型设置差异化阈值,反映各类输出的信息密度:
| 工具类型 | 建议阈值 | 理由 |
|---|---|---|
exec(命令执行) |
较低(如 4096 Token) | 构建日志大量重复,截断损失小 |
read(文件读取) |
较高 + 建议 offset/limit | 源码有结构,可分段读取 |
web(搜索结果) |
每条限制摘要长度 | 搜索引擎已做初步摘要 |
browser(截图) |
Base64 或文字描述 | 视觉信息无法文本截断 |
启用会话修剪时,对 Anthropic 提供商价值尤其大。修剪能减小提示缓存写入量,缓存 TTL 过期后重新缓存时直接降低成本。非 Anthropic 提供商默认关闭修剪,需手动启用。
常见问题(FAQ)
Q1:截断后的工具结果还能用吗?
能用。截断保留开头部分并标注截断,Agent 可据此决定是否重新查询。
Q2:会话修剪会删除磁盘上的历史吗?
不会。修剪仅在内存中处理,磁盘上的完整历史始终保留。
Q3:如何避免工具频繁返回超大结果?
在工具参数中加 limit/filter 约束,或封装为 Skill 在本地处理。