<!-- title: 返回即淹没：结果裁剪五栈实测 -->

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

## 概述

工具使用（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` 调高。

```bash
# 调高单次工具返回的硬截上限
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` 游标，让模型自己决定要不要翻下一页：

```python
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 官方推荐的正是这个范式：

```python
@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（细、按业务语义切、保持结构完整）。客户端从此只需一个宽松的硬上限做兜底，日常返回永远小而完整。**这才是工具结果问题的根治路径——不是把闸门调得更聪明，而是让源头不再倾泻。**

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

```mermaid
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→小而完整→正常推理）是根治方案。

## 推荐方案

**按返回体量分层选型：**

- **小返回（< 4K token）**：盲截兜底即可，任何栈都能扛。无需特殊设计。
- **中返回（4K–25K token）**：首选**分页**。LangChain 用游标模式，CrewAI 工具内部自切。Claude Code 内可调高 `MAX_MCP_OUTPUT_TOKENS` 但不要无限调——那只是把闸门推后，没解决结构破坏。
- **大返回（25K–100K token）**：首选**抽取**（jq / JSONPath 只取需要的字段）或**落盘**（写文件返回引用）。绝不能用盲截——25K 硬切必然切在结构中段。
- **超大返回（> 100K token，如整库 dump、DOM）**：必须**落盘 + 返回指针**。模型只在需要时按引用读取片段。这是唯一能扛住 1.3MB DOM 级返回的范式。

**按栈选型：**

- **怕麻烦、要开箱安全**：选 Claude Code（10K/25K 双闸门 + 可调阈值），接受"硬截丢尾部"的代价，配合 MCP server 的 preview 模式补齐。
- **要精细控制、不怕写代码**：选 LangChain/LangGraph，自己实现分页/抽取/落盘，把"零默认"变成"完全可控"。
- **要智能、预算充足**：选 OpenAI Responses API 的 auto 压缩 + AutoGen condenser，接受摘要丢细节和额外 token 成本。
- **建工具生态**：MCP server 一律按 preview + cursor + truncated 三件套设计，从源头根治，让任何客户端都不需要粗暴截断。

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

## 参考来源

1. [Silent Tool Truncation: The Default Cap Your Agent Reasons Over Without Knowing — Tian Pan](https://tianpan.co/blog/2026-05-10-silent-tool-truncation-8kb-default-agent-reasons-blind) — 记录 142KB JSON 被截到 8192 byte 的真实事故，剖析"静默截断让模型误信"的失效链。
2. [Tool-Result Truncation: The Silent Bug That Makes Agents Lie — dev.to](https://dev.to/gabrielanhaia/tool-result-truncation-the-silent-bug-that-makes-agents-lie-3epe) — 50KB 返回只剩 700 字符，模型自信总结不存在的数据，提出"silent bug that makes agents lie"概念。
3. [Connect Claude Code to tools via MCP — Claude Code Docs](https://code.claude.com/docs/en/mcp) — 官方文档明确 10,000 token 告警与 25,000 token 硬截阈值，及 `MAX_MCP_OUTPUT_TOKENS` 调参。
4. [Truncated MCP Tool Responses Limit Collaborative Functionality — GitHub Issue #2638](https://github.com/anthropics/claude-code/issues/2638) — 报告 MCP 工具响应在 UI 渲染层被截到约 700 字符，揭示"三道刀"问题。
5. [Solving AI's 25,000 Token Wall: Introducing mcp-cache — dev.to](https://dev.to/swapnilsurdi/solving-ais-25000-token-wall-introducing-mcp-cache-1fie) — 实测网页 DOM 1.3MB+（154K token）撑爆 25K 限制，提出缓存式解法。
6. [Context Engineering for AI Agents: Offload, Summarize, Isolate, and Cache — LangCopilot](https://langcopilot.com/posts/2026-03-23-context-engineering-ai-agents-offload-summarize-isolate-cache) — 归纳工具返回治理四大策略：Offload / Summarize / Isolate / Cache。
7. [Handling Token Limits When Processing Large JSON Responses in an MCP Server](https://claudearchitectcertification.com/knowledge/mcp-token-limit-large-json) — 阐明 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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
