首页 › ai-agents › 返回即淹没:结果裁剪五栈实测

返回即淹没:工具结果裁剪五栈实测

概述

工具使用(Tool Use)的所有讨论几乎都聚焦在"调对工具"——选型、召回、参数、幂等、编排——却几乎没人正视工具执行完之后那坨返回值。而恰恰是这坨返回值,才是真正吃窗口、烧 token、还会让智能体撒谎的元凶。本期实测发现,主流六栈对"工具返回什么、怎么进上下文"的默认姿态差异巨大:Claude Code 设 10K token 告警 / 25K token 硬截两道闸门;OpenAI Assistants 走线程级 truncation:auto 智能压缩;AutoGen 依赖 condenser 事后蒸馏;而 LangChain、CrewAI、原生 MCP server 几乎"零默认"——返回多大就灌多大,裁不裁全靠开发者自觉。最危险的是一类"静默截断":框架悄悄砍掉尾部字节,模型却把残缺结果当成完整事实自信作答。一篇实测记录了 142KB JSON 被截到 8192 byte 的真实事故,作者称之为"让智能体撒谎的无声 bug"。本期拆解五栈的裁剪姿态、五式裁剪策略的取舍,以及根治它的工程范式。

六栈工具结果裁剪姿态横评

框架 / 运行时 默认截断阈值 截断方式 截断是否可感知 默认防护强度 典型坑
Claude Code(MCP 客户端) 10K token 告警 / 25K token 硬截 头部保留,尾部丢弃 告警可见,硬截静默 强(可调 MAX_MCP_OUTPUT_TOKENS) 25K 一刀切,长返回尾部全丢
OpenAI Assistants / Responses API 线程超限时触发 truncation:auto(保留系统指令+近期消息,中段智能压缩) 自动,日志可见 中 历史工具结果被压缩成摘要,丢细节
LangChain / LangGraph 无默认阈值 不截断,原样灌入 messages 无任何提示 极弱(开发者全责) 一个大文件返回直接撑爆窗口
CrewAI 无默认阈值 工具输出进 task output,不截断 无提示 弱 长 task output 累积拖慢推理
AutoGen 无默认阈值(靠 condenser) 消息全量入历史,可选 condenser 蒸馏 condenser 触发时可见 中(需显式配置) 不配 condenser 则线性膨胀
MCP server(服务端) 由客户端决定 返回 preview + cursor(推荐)或全量 dump server 不感知客户端阈值 取决于实现 全量 dump 必撞客户端闸门

核心结论先行:没有一个栈在"返回值该多大、该不该进上下文"上给出开箱即用的安全默认值。差异只在于"撞墙时谁先拦、拦得粗不粗暴"。

深度分析

一、「静默截断」为何是最阴险的失效模式

工具调用最常见的两种失败是"调用失败"和"返回错误",二者都会抛异常、进日志、被监控捕获。但还有第三种——返回成功,只是不完整。一篇被广泛引用的实测记录了典型场景:一次工具调用返回 142KB 的 JSON,框架静默丢弃 byte 8192 之后的所有内容,把头部前缀交给模型,模型基于这个"它根本不知道是片段的片段"写出了一个自信满满的答案。三周后客户投诉,运维翻 trace,看到的却是 tool returned successfully。

dev.to 上另一篇同名主题的文章给了一个更刺眼的数字:工具返回 50KB,模型实际只看到约 700 字符,然后它"自信地总结了那些根本不存在的内容"。作者把这种现象命名为 "the silent bug that makes agents lie"——让智能体撒谎的无声 bug。

它阴险在三处:第一,HTTP 状态码是 200,trace 里是成功,没有任何 error 通道;第二,模型不知道自己看到的是残缺数据,于是把它当成完整事实去推理,错误被放大到下游决策;第三,截断点往往是按字节/token 硬切,恰好可能切在 JSON 中段、截断一个数组或一个字段,于是模型看到的甚至不是"少了尾巴的合法数据",而是"结构破损的数据",但它照样硬解析。这类 bug 不会在开发期暴露——小数据集时永远不会触发阈值——只在生产环境的真实大返回上炸。

二、Claude Code 的双闸门:10K 告警与 25K 硬截

Claude Code 是目前少数在工具返回上设了"两道闸门"的运行时。官方文档明确:当单个 MCP 工具输出超过 10,000 token 时,Claude Code 显示一条 warning;当超过 25,000 token 时,触发硬截断,超出部分直接丢弃。这个 25K 上限可以通过环境变量 MAX_MCP_OUTPUT_TOKENS 调高。

# 调高单次工具返回的硬截上限
export MAX_MCP_OUTPUT_TOKENS=100000

这套机制值得拆解:10K 的告警是"软提示",它告诉模型和开发者"这个返回偏大,你可能想分页或提取",但不阻止;25K 的硬截是"硬墙",过墙即丢。问题在于,硬截是头部保留、尾部丢弃——而工具返回的尾部往往是结构化数据里最有信息密度的部分(比如一个查询结果的最后几条记录、一个日志的最近时间戳)。一刀切头部保留,等于默认"前面的比后面的重要",这对绝大多数时序型、追加型返回是反的。

更要命的是,Claude Code 的 UI 渲染层还有一道独立的显示截断——GitHub issue #2638 报告 MCP 工具响应在界面里被截到约 700 字符。这意味着同一个返回值,可能经历"服务端返回全量 → 客户端上下文层 25K 截断 → UI 渲染层 700 字符截断"三道刀,开发者看 trace 看到的、模型实际读到的、工具实际返回的,是三个不同长度的东西。调试时若只看 UI,会得出完全错误的结论。

三、LangChain / LangGraph 与 CrewAI 的「零默认」陷阱

与 Claude Code 形成鲜明对比的是 LangChain 生态。LangChain 的 @tool 装饰器对返回值不做任何默认处理——函数 return 什么,字符串就原样进入 ToolMessage,再原样拼进下一次模型调用的上下文。CrewAI 同理:工具输出直接进 task output,没有截断、没有摘要、没有分页。

这不是 bug,是设计哲学:框架把"返回多大合适"的决定权完全交给开发者。但实测中这是最大的坑——开发期用 5 行测试数据,一切正常;上了生产,工具去读一个 2 万行的日志文件,返回一个 8000 行的 SQL 结果,一次 web 抓取带回 1.3MB 的 DOM(约 154K token,见 mcp-cache 实测),整个上下文瞬间被一个工具返回淹没,后续推理全部在噪声上滑行。

LangChain 官方推荐的解法是分页——返回首屏 N 条 + 一个 next_offset 游标,让模型自己决定要不要翻下一页:

from langchain_core.tools import tool

@tool
def search_logs(query: str, offset: int = 0, limit: int = 100) -> dict:
    """分页检索日志,返回首屏与下一页游标。"""
    total, rows = db.search(query)
    chunk = rows[offset:offset + limit]
    return {
        "rows": chunk,
        "total": total,
        "next_offset": offset + limit if offset + limit < total else None,
    }

这种模式的代价是多轮往返的延迟税——但换来的是"返回永远小而完整"。CrewAI 没有等价的一等公民抽象,开发者只能自己在工具内部切,或借助 handle_tool_error 兜底。没有默认防护 = 开发者必须自己写防护,这是这两个栈最大的隐性成本。

四、OpenAI Assistants 的 truncation_strategy:线程级智能压缩

OpenAI 走的是另一条路。Assistants API 的 Run 对象有个 truncation_strategy 字段,模式 auto 会在线程总长度逼近模型上下文上限时,自动压缩中段历史——保留系统指令和最近的若干条消息(含最近的工具返回),把中间部分蒸馏成摘要。Responses API 延续了类似的思路。

这种"线程级智能压缩"和 Claude Code 的"单返回级硬截"是两种截然不同的哲学:Claude Code 在单个工具返回层面动刀,每次都拦;OpenAI 在整个对话线程层面动刀,只在总量超限时才压缩。前者粒度细、可预测(每次返回独立判定),后者粒度粗、更省事(不用关心单个返回多大),但代价是历史工具结果会被压成摘要——三天前那次工具调用返回的详细数据,在线程变长后可能只剩一句"此前查询到 47 条记录",细节永久丢失。

azure AI 的文档把这种策略归入更大的 compaction 家族:滑动窗口(sliding window,只留最近 N 轮)、截断(truncation,硬切)、工具结果压缩(tool-result compaction,专门针对 ToolMessage 做蒸馏)、摘要(summarization)。四者本质都是在"保留多少历史工具结果"上做取舍。OpenAI 选了摘要,Claude Code 选了截断,没有银弹——摘要丢细节、截断丢尾部,两者都会让模型在某些追问上"失忆"或"误信"。

五、AutoGen 的 condenser:事后压缩派 vs 事前裁剪派的分歧

AutoGen 的处理最能体现"事后 vs 事前"两条路线的根本分歧。默认情况下,AutoGen 把每一条消息(含工具返回)全量追加到对话历史,没有任何截断。它提供的解法是 condenser——一个专门的"压缩代理",在历史膨胀到阈值时触发,把整段历史蒸馏成摘要再继续。

这是一个"事后压缩派"的典型代表:先让返回值原样进上下文,膨胀了再压。好处是保留了完整的中间状态供压缩代理精挑细选;坏处显而易见——压缩代理本身要消耗一次模型调用,且压缩发生在"已经膨胀之后",那一次峰值 token 消耗躲不掉,账单照样付。更关键的是,condenser 不是默认开启的,必须显式配置,很多团队上线后才发现"为什么 token 用量线性增长"——因为根本没配 condenser。

与之相对的是"事前裁剪派"(Claude Code、MCP 客户端闸门):在返回值进入上下文之前就拦掉超限部分。事前派省 token、可预测,但丢的是原始数据;事后派保完整、可智能取舍,但贵且不可预测。两派没有谁对谁错,但生产环境应明确站队:成本敏感、可接受丢尾部的选事前;数据完整敏感、预算充足的选事后。最差的是 AutoGen 默认态——既不事前裁也不事后压,全量堆积,是最容易在长会话里 OOM 或超 token 上限的姿态。

六、五式抉择:盲截 / 压缩 / 分页 / 抽取 / 落盘的取舍矩阵

把所有手段摊开,工具返回的处理本质就五种范式,langcopilot 把它们归纳为 Offload / Summarize / Isolate / Cache 四大类,加上最原始的盲截,凑成五式:

  1. 盲截(Blind Truncate):头部保留 N 字节/token,尾部丢弃。实现最简,是 Claude Code 25K、各类客户端闸门的默认。代价:尾部数据全丢,时序型返回尤其致命。适用:纯文本、信息均匀分布、尾部无关键数据的返回。
  2. 压缩(Summarize / Compaction):用一个 LLM 调用把返回蒸馏成摘要再注入。OpenAI auto、AutoGen condenser 属此列。代价:多一次模型调用、延迟 + 成本 + 摘要本身可能幻觉。适用:长文本、可容忍细节损失的返回。
  3. 分页(Paginate / Cursor):返回首屏 + 游标,模型按需翻页。LangChain 推荐、MCP server 最佳实践。代价:多轮往返延迟税。适用:列表型、查询型、可天然切片的返回。
  4. 抽取(Structured Extract):在注入前用 jq / JSONPath / 正则只取需要的字段。代价:需预知 schema、需写提取规则。适用:结构化 JSON、只需其中几个字段的返回。
  5. 落盘(Offload to File / Memory + 返回引用):把大返回写到文件或向量库,只把"指针 + 元信息"注入上下文,模型需要细节时再读取。代价:需要文件系统或外部存储、读取也是一次工具调用。适用:超大返回(DOM、整库 dump、大日志)、需跨会话复用的数据。

实测经验:返回体量越大,越该往右走(分页→抽取→落盘)。盲截和压缩是"治标",分页和抽取是"治本但需 schema",落盘是"终极方案但最重"。大多数事故的根因,是把本该分页或落盘的返回,用盲截糊弄了——省了开发的脑子,赔了生产的数据。

七、MCP server 端的根治:返回 preview + cursor,而非 dump 全量

真正彻底的解法不在客户端,而在工具(尤其 MCP server)这一侧。问题的根源是:工具把自己当成了"数据源",把全量数据一股脑 dump 出去,把"该返回多大、该返回哪些"的难题甩给了客户端的截断闸门。正确的设计是把工具当成"带游标的 API"——永远只返回首屏 preview + 一个 cursor,让模型在需要时主动翻页或下钻。

MCP 官方推荐的正是这个范式:

@mcp.tool()
def search_code(query: str, limit: int = 20, cursor: str | None = None) -> dict:
    """返回首屏结果与下一页游标,而非全量 dump。"""
    items, next_cursor = index.search(query, limit=limit, after=cursor)
    return {
        "items": items,            # 只返回首屏
        "count": len(items),
        "next_cursor": next_cursor,  # 模型按需翻页
        "truncated": next_cursor is not None,  # 明确告知"还有更多"
    }

注意三个关键设计:第一,默认 limit 小(20 而非无界),从源头控制单次返回体量;第二,返回 next_cursor,让翻页是模型主动选择而非客户端被动截断;第三,显式 truncated 标志,告诉模型"你看的不是全部,要更多请翻页"——这一条直接破解了"静默截断让模型撒谎"的根源,模型不再误信残缺数据是完整事实。

这套范式把"返回多大"的决定权从客户端闸门(粗、按字节切、破坏结构)移到了 server(细、按业务语义切、保持结构完整)。客户端从此只需一个宽松的硬上限做兜底,日常返回永远小而完整。这才是工具结果问题的根治路径——不是把闸门调得更聪明,而是让源头不再倾泻。

工具返回的完整生命周期与截断点

flowchart TD
    A[工具执行 返回大数据] --> B{MCP server 设计}
    B -->|全量 dump| C[客户端闸门 1: Claude 10K告警]
    B -->|preview + cursor| Z[小而完整 直接注入]
    C --> D{超过 25K?}
    D -->|否| E[原样注入上下文]
    D -->|是| F[硬截 头部保留 尾部丢弃]
    F --> G[模型把残缺当完整事实]
    G --> H[自信输出错误答案]
    E --> I{线程总量超限?}
    I -->|是 OpenAI auto| J[中段压缩成摘要 丢细节]
    I -->|否| K[模型正常推理]
    Z --> K
    H -.事故三周后.-> L[客户投诉 trace 却显示成功]
    J -.后续追问.-> M[模型失忆 细节已蒸馏]

    style F fill:#f66,color:#fff
    style G fill:#f96,color:#fff
    style H fill:#f96,color:#fff
    style Z fill:#6c6,color:#fff
    style K fill:#6c6,color:#fff

图中红色路径(硬截→误信→自信撒谎→事故)正是 142KB→8192 byte 案例的真实轨迹;橙色路径(摘要→失忆)是 OpenAI auto / AutoGen condenser 的副作用;绿色路径(preview+cursor→小而完整→正常推理)是根治方案。

推荐方案

按返回体量分层选型:

按栈选型:

一句话工程准则:永远不要让一个工具返回超过模型一次能消化的量;返回多大不是工具的自由,而是工具和模型共同协商的结果。 截断闸门是最后一道兜底,不是第一道设计——把"返回多大"的决策推到闸门,就等于把"丢哪部分数据"的决策交给了一个不懂业务、只懂字节的切刀。

参考来源

  1. Silent Tool Truncation: The Default Cap Your Agent Reasons Over Without Knowing — Tian Pan — 记录 142KB JSON 被截到 8192 byte 的真实事故,剖析"静默截断让模型误信"的失效链。
  2. Tool-Result Truncation: The Silent Bug That Makes Agents Lie — dev.to — 50KB 返回只剩 700 字符,模型自信总结不存在的数据,提出"silent bug that makes agents lie"概念。
  3. Connect Claude Code to tools via MCP — Claude Code Docs — 官方文档明确 10,000 token 告警与 25,000 token 硬截阈值,及 MAX_MCP_OUTPUT_TOKENS 调参。
  4. Truncated MCP Tool Responses Limit Collaborative Functionality — GitHub Issue #2638 — 报告 MCP 工具响应在 UI 渲染层被截到约 700 字符,揭示"三道刀"问题。
  5. Solving AI's 25,000 Token Wall: Introducing mcp-cache — dev.to — 实测网页 DOM 1.3MB+(154K token)撑爆 25K 限制,提出缓存式解法。
  6. Context Engineering for AI Agents: Offload, Summarize, Isolate, and Cache — LangCopilot — 归纳工具返回治理四大策略:Offload / Summarize / Isolate / Cache。
  7. Handling Token Limits When Processing Large JSON Responses in an MCP Server — 阐明 MCP 将工具结果 verbatim 注入上下文,50KB JSON payload 的实际 token 占用。

本文由「爱卫生」(微信:gprshome201101)整理输出,聚焦 AI 智能体实战与工程化拆解。如需转载请联系原作者。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入