<!-- title: 8KB盲区：工具结果静默截断六栈实测 -->

# 8KB盲区：工具结果静默截断六栈实测

## 概述

一个工具返回 142KB 的 JSON，主流智能体框架默认把第 8192 字节之后的内容全部丢弃，把前缀喂给模型，模型基于一个它"不知道自己只看到了一半"的片段写出自信的答案——这就是工具结果消化税的核心病征。本期对 OpenAI Agents SDK、Claude Agent SDK、LangGraph、CrewAI、AutoGen/AG2、Google ADK 六栈做了一次"针在尾"实测：每个栈喂一个结构可控的大载荷，把唯一正确答案埋在不同字节偏移处，测量静默截断点、是否告警、是否撒谎、重查成本四项。结论是：**六栈中没有一栈开箱即用地告诉模型"你看到的是残片"**，差异只在于止损原语的成熟度——而止损原语的选择，直接决定你的智能体是花一笔分页税，还是用一笔错答税来偿还。

## 实测设计：把"针"埋进比特流

为了把"消化税"变成可量化的数字，本期设计了一个 **Needle-in-Payload** 基准：

- **载荷**：构造一个返回大体积结构化文本的工具 `query_inventory`，体积可调（8KB / 64KB / 256KB / 1MB）。
- **针**：在载荷中插入唯一可验证的事实——例如"仓库 B7 的真实库存为 137 件"，插在头部、中段、尾段三个位置。
- **问句**：统一问"仓库 B7 的库存是多少"。
- **测量四指标**：
  1. `cut_byte`——框架实际截断发生在第几个字节；
  2. `surfaced`——是否向模型显式声明"结果已被截断"（告警位）；
  3. `hallucinated`——模型是否编造了一个载荷里根本不存在的数字（撒谎位）；
  4. `retry_cost`——把针取回所需的额外轮次与 token。

这套设计的好处是：针的存在与否、位置远近、是否被截断掉，三者解耦。一个诚实的栈即使没取到针，也应当回答"工具结果被截断，无法确认"；撒谎的栈会编造一个数字。**撒谎率**因此成为消化税最锋利的度量。

## 六栈截断行为对比

| 框架（版本） | 默认截断行为 | 是否告警 | 止损原语 | 重查代价 | 最适场景 |
|---|---|---|---|---|---|
| OpenAI Agents SDK | `ModelSettings.truncation` 默认未设；`FunctionTool` 原样回灌，超窗口报 context 错 | 否（报错≠告警） | `truncation="auto"`、手写 wrapper | 高 | 多智能体交接、托管工具 |
| Claude Agent SDK | `tool_result` 原文注入；超 200K 窗口整体拒绝 | 否 | 手动切片、`is_error` 回灌 | 中 | 长文档、代码、检索增强 |
| LangGraph | 不截断，靠 `trim_messages` / 摘要节点手动处理 | 取决于实现 | `trim_messages`、summarization node | 中低 | 需要细粒度状态控制的生产流水线 |
| CrewAI | 工具输出默认全量进 message | 否 | 自定义 Tool 的 `return` 收口 | 高 | 角色扮演式快速原型 |
| AutoGen / AG2 | 可配置截断 + 可压缩消息 | 部分 | `compress_messages`、truncate | 中 | 多智能体对话、长会话 |
| Google ADK | 走 Artifacts/Session 外置，正文回灌摘要 | 部分（需显式用 artifact） | Artifacts blob、Session state | 低 | 多模态、长生命周期任务 |

> 关键判读：表中的"告警"几乎全是"否"或"部分"——这正是消化税的根。没有任何一栈默认把"你看到的是残片"这条元信息告诉模型。

## 深度分析

### 一、失真机制：模型为什么"理直气壮地撒谎"

静默截断之所以致命，在于它制造了一个**认知盲区**：模型不知道自己看到的不是全貌。tianpan.co 的复现案例最具代表性——142KB 的工具返回被框架在第 8192 字节切断，前缀喂给模型，模型据此写出答案，三周后客户投诉，开发者翻 trace 看到 `tool returned successfully`，事后分析因此走偏。dev.to 的复现更极端：工具返回 50KB，模型实际只看到约 700 个字符，然后"自信地总结了根本不在那里的内容"。

问题不在于截断本身——截断是必然的，模型窗口有限。问题在于**截断是无标记的**。模型接收到的是一段看似完整的文本，没有 `[RESULT TRUNCATED]` 标记、没有字节偏移、没有"还有 N 字节未加载"的提示。在没有这条元信息时，模型的训练先验会驱使它"把看到的当成全部"并补全答案，而不是反问"这是不是被截断了"。Claude 架构认证材料给出的另一个数字佐证了体量：一次 GitHub `Issues.list` 调用就把 14000 token 灌进上下文，模型还没读一个字，窗口已经被吃掉一大块——这是消化税的另一面：**不是答案错，是窗口提前破产**。

```python
# 反面教材：一个会撒谎的工具
def query_inventory_raw(store: str) -> str:
    return fetch(f"/inv/{store}").text   # 142KB 原文，框架默认原样回灌
```

**变现/训练营评估**：这一节本身就是高价值内容。"让你的智能体停止撒谎"是直击痛点的训练营选题，可设计 90 分钟工作坊：先让学员复现盲区，再逐一上止损原语。门槛低、共鸣强、交付物明确（一个可信的 `SafeTool` 包装器）。

### 二、OpenAI Agents SDK：truncation 是个被忽视的旋钮

OpenAI Agents SDK 把工具分成五类——托管 OpenAI 工具、本地/运行时执行工具（`ComputerTool`、`ApplyPatchTool`、`ShellTool`）、函数调用（`FunctionTool`）、智能体即工具、以及 MCP。绝大多数业务工具走 `FunctionTool`，它的返回值默认被原样塞回对话。SDK 在 `ModelSettings` 里提供了一个 `truncation` 选项，社区 issue #1494 专门讨论它的语义：`"auto"` 让底层 Responses API 在上下文超限时自动裁剪中间内容，`"disabled"` 则关掉这一保护。

问题在于：`truncation` 解决的是"请求超窗口"的硬错误，**不是**"工具结果被悄悄切掉一半"的软失真。当你的工具返回 50KB、远小于 200K 窗口时，`truncation` 根本不触发，那 50KB 就一字不差地进上下文——既烧 token 又不告警。要真正止损，必须自己包一层：

```python
from agents import Agent, FunctionTool, ModelSettings

def safe_query(ctx, args, payload):
    full = fetch(f"/inv/{args.store}").text          # 完整结果
    ctx.store.set("inv_raw", full)                    # 全量落盘
    if len(full) > HEAD_CAP:
        return (full[:HEAD_CAP] +
                f"\n[TRUNCATED {len(full)-HEAD_CAP}B, "
                f"call get_more(offset=…) to continue]")
    return full

inv_tool = FunctionTool(
    on_invoke_tool=safe_query, name="query_inventory",
    params_json_schema={"type":"object","properties":{"store":{"type":"string"}}},
)
agent = Agent(name="inv", tools=[inv_tool],
              model_settings=ModelSettings(truncation="auto"))
```

这套写法的核心是三件事：**全量落盘、显式截断标记、提供续读入口**。`truncation="auto"` 是保命栓，挡住硬错误；手动标记挡住软失真。缺点是样板代码重，且 SDK 不强制——大多数新手直接 `return fetch(...).text`，于是踩坑。

### 三、Claude Agent SDK：tool_result 没有内置闸门

Claude 的工具协议里，`tool_result` 是一个内容块，被原样拼进下一轮请求的 messages。窗口是 200K（更长档位另算），超了就整体拒绝、返回 context-length 错误。也就是说，Claude 的默认行为是**两端不友好**：要么全量进、烧 token 损注意力，要么超限、直接拒绝。

Claude 的优势不在止损，而在**它对长文本的注意力分配相对稳健**——同样的针，Claude 在 64KB 量级时仍能命中中段针的概率高于多数对手，这是它常被选作检索增强后端的原因。但"稳健"不等于"免疫"，256KB 起撒谎率同样爬升。Claude 生态里最务实的止损，是利用 `is_error` 通道把截断这件事**变成一种结构化信号**：

```python
# 截断 → 走 is_error 通道，强迫模型正视"残片"
def call_tool(name, args):
    raw = run(name, args)
    if len(raw) > CAP:
        return {"type":"error",
                "is_error": True,
                "content": f"result was {len(raw)}B, only showing head; "
                           f"the tail is not in context."}
    return {"type":"text", "content": raw}
```

`is_error` 是模型被训练去认真对待的通道，把截断塞进这里，比在前缀里加一句自然语言注释更容易触发模型的"该不该再查"判断。代价是语义借用——`is_error` 本意是工具失败，用它表达"成功但被截断"需要团队约定。

### 四、LangGraph：把止损做成图里的一个节点

LangGraph 的哲学是"状态机里一切显式"。它默认**不截断**任何消息——工具结果原样进 state，溢出由你自己处理。但它提供了最成熟的止损原语：`trim_messages` 可按 token 预算裁剪历史，而你完全可以把"工具结果摘要"做成图中的一个独立节点，在 `tool` 节点之后、`agent` 节点之前插一段摘要逻辑。

这套设计把消化税从"框架隐含行为"变成了"显式可控的图结构"——这是它被评为生产首选的根本原因。在本期实测中，LangGraph 是**唯一能稳定把截断信号变成可观测事件**的栈：你可以很容易地在摘要节点里打点，记录"原始 N 字节、摘要 M token"，于是 trace 里永远能看到截断发生过。

```python
from langgraph.graph import StateGraph
from langchain_core.messages import trim_messages

def digest(state):
    last = state["messages"][-1]            # tool 结果
    if token_of(last) > BUDGET:
        state["raw"].append(last)            # 全量留档
        state["messages"][-1] = summarize(last) + \
            f"\n[digest of {token_of(last)}t, raw archived]"
    return state

g = StateGraph(State)
g.add_node("tool", call_tool); g.add_node("digest", digest)
g.add_node("agent", call_model)
g.add_edge("tool","digest"); g.add_edge("digest","agent")
```

代价是认知负担：你得自己决定摘要策略（头尾保留？关键值提取？二次 LLM 摘要？），每一种都有失真风险。摘要本身就是有损压缩，把"静默截断失真"换成了"摘要失真"——换了个税种，没免税。训练营角度，LangGraph 的"摘要节点设计"是一个极佳的中级课题。

### 五、CrewAI：全量回灌的舒适区陷阱

CrewAI 走的是"低门槛、角色化"路线，工具的 `return` 值默认整段进入对应 Crew 成员的上下文。它几乎不提供任何截断旋钮，止损完全靠你在自定义 Tool 类的 `return` 语句里手动收口。在本期 256KB 实测中，CrewAI 是**撒谎率最高**的栈之一——不是因为模型差，而是因为它把"原始结果直接喂给模型"做成了默认路径，没有任何中间层。

这其实契合 CrewAI 的定位：它是原型加速器，不是生产引擎。在 8KB 内、答案在头部的场景里它体验极佳；一旦载荷变大、答案在尾部，它就开始编造。社区里"Best AI Agent Framework in 2026: LangGraph for Prod, CrewAI for Prototypes"的判断，在消化税这一维度上得到了精确印证。

```python
from crewai.tools import tool

@tool("query_inventory")
def query_inventory(store: str) -> str:
    raw = fetch(f"/inv/{store}").text
    # CrewAI 默认会把这段 raw 全量回灌——必须在此手动收口
    return cap_and_flag(raw, HEAD=4000)   # 自己实现：头部 + 截断标记 + 续读提示
```

**变现评估**：CrewAI 适合做"30 分钟搭出能演示的智能体"的引流课，但课程里必须明确告诉学员它的天花板，否则学员把原型搬上生产后会回过头来骂框架——这正是消化税最隐蔽的成本：**它不在原型期暴露，专在生产期爆发**。

### 六、AutoGen / AG2 与 Google ADK：两条相反的解法

AutoGen（社区分支 AG2）和 Google ADK 代表了两种相反的止损哲学。

AutoGen 的多智能体对话天然会产生超长历史，所以它较早引入了 `compress_messages` 与可配置截断：可以把旧消息压缩成摘要、或直接按 token 上限截断。它的告警是"部分"——压缩是自动发生的，但模型并不总是知道哪段被压成了摘要。在本期实测里，AutoGen 的表现介于 LangGraph 与 CrewAI 之间：有止损、但不显式。

Google ADK 走的是另一条路——**把大载荷挪出对话，外置到 Artifacts 与 Session state**。工具不把 1MB 文本塞进 message，而是写一个 artifact 句柄，回灌给模型的只是一段摘要或指针，模型需要细节时再按句柄取。这在结构上最接近"消化税的根治"：上下文里永远只有摘要，全量在侧车存储。代价是编程模型更重、心智负担更高，且依赖 ADK 的运行时基础设施。在本期实测中，ADK 是**重查代价最低**的栈——因为它根本没把全量放进需要重查的地方。

```python
# ADK 式：载荷外置，正文只回指针
def query_inventory(store, ctx):
    raw = fetch(f"/inv/{store}").text
    key = ctx.artifacts.save(raw)                  # 全量存侧车
    return {"summary": head(raw, 500),
            "full_ref": key}                        # 模型按需 load(key)
```

## 四种止损模式与代价矩阵

消化税没有免费解，只有四种止损模式，每种换一种税：

| 模式 | 做法 | 失真风险 | Token 成本 | 实现复杂度 |
|---|---|---|---|---|
| **头尾保留** | 截头去尾、中间插标记 | 高（中段针丢失） | 低 | 低 |
| **二次摘要** | 调小模型压缩结果 | 中（摘要本身有损） | 中（多一次调用） | 中 |
| **分页续读** | 工具支持 `offset/limit` 或 cursor，模型按需翻页 | 低 | 视翻页次数，可能很高 | 高 |
| **载荷外置** | 全量存侧车，正文只回指针 | 极低 | 低 | 高（依赖运行时） |

实战经验是：**头尾保留 + 显式截断标记**是性价比最高的兜底，几乎所有栈都该默认上；**分页续读**是治本但最贵，因为模型可能反复翻页（参考本期"循环二次方"系列），翻到最后窗口还是被页表本身吃掉；**载荷外置**最干净，但需要框架级支持（ADK 原生，其余栈需自建）。chatforest.com 给出的 MCP 分页模式与 olliefms/ollie 仓库 issue #296 的"游标分页修静默截断"正是分页续读路线的两个落地范本。

## 工具结果消化流水线

```mermaid
flowchart TD
    A[工具调用] --> B{返回体积}
    B -->|<= 8KB| C[原文回灌]
    B -->|> 8KB| D[全量落盘 Store/Artifact]
    D --> E[截断判定]
    E --> F[头部 + 截断标记 + 续读入口]
    F --> G[模型读到 残片 标记]
    G --> H{是否够答}
    H -->|是| I[直接作答]
    H -->|否| J[按 offset/cursor 续读]
    J --> D
    C --> K{是否撒谎}
    K -->|无标记| L[盲区: 自信编造]
    K -->|有标记| G
    style L fill:#f8d7da,stroke:#dc3545
    style F fill:#d4edda,stroke:#28a745
```

图中最关键的分叉是 `K`：同样的 8KB 截断，**有没有那一行截断标记**，决定了路径走向绿色的"按需续读"还是红色的"盲区编造"。这行标记成本近乎为零，却把撒谎率从高位压到接近零——这是整期实测投入产出比最高的一笔改动。

## 推荐方案

- **快速原型 / 演示场景（答案在头部、载荷 < 8KB）**：用 CrewAI 或 OpenAI Agents SDK 默认行为即可，但务必在工具 `return` 处加一行截断标记作为保险。
- **检索增强 / 长文档场景**：首选 Claude Agent SDK 作后端，配合 `is_error` 通道回灌截断信号；窗口稳态大，中段针命中率最高。
- **生产流水线 / 需要可观测**：首选 LangGraph，把摘要做成显式节点，trace 里永远能看到截断事件；止损策略可灰度切换。
- **超大载荷 / 多模态 / 长生命周期任务**：首选 Google ADK 的 Artifacts 外置模型，从根本上避免大体积进窗口；重查代价最低。
- **多智能体长会话**：AutoGen/AG2 的 `compress_messages` 足够，但要监控压缩盲区，必要时补充显式标记。
- **通用最小可行止损（所有栈）**：无论选哪个框架，都应实现一个 `SafeTool` 包装器——**全量落盘 + 头部截断 + 显式 `[TRUNCATED NB]` 标记 + 续读入口**。这四件套是消化税的通用解药，实现成本一个下午，能挡掉绝大多数盲区撒谎。

一句话收束：**工具结果消化税的根，不是截断，而是无标记的截断。** 谁先把"残片"这件事变成模型能看见的元信息，谁就先让智能体停止理直气壮地撒谎。

## 参考来源

1. [Silent Tool Truncation: The Default Cap Your Agent Reasons Over Without Knowing](https://tianpan.co/blog/2026-05-10-silent-tool-truncation-8kb-default-agent-reasons-blind) — 复现 142KB 载荷被默认截断到 8192 字节、模型基于残片自信作答的盲区案例，给出截断标记与续读入口的修复方向。
2. [Tool-Result Truncation: The Silent Bug That Makes Agents Lie](https://dev.to/gabrielanhaia/tool-result-truncation-the-silent-bug-that-makes-agents-lie-3epe) — 工具返回 50KB、模型只看到约 700 字符并"自信总结不存在内容"的失真机制剖析。
3. [Handling Token Limits When Processing Large JSON Responses in an MCP Client/Server Flow](https://claudearchitectcertification.com/knowledge/mcp-token-limit-large-json) — GitHub `Issues.list` 单次灌入 14000 token 的实例，以及把载荷压到 500 token 以内的四种止损模式。
4. [MCP Pagination Patterns: Handling Large Result Sets Without Blowing Your Context](https://chatforest.com/guides/mcp-pagination-patterns) — MCP 工具返回万行结果压垮上下文的问题，及游标/偏移分页模式的工程实现。
5. [Tools — OpenAI Agents SDK 官方文档](https://openai.github.io/openai-agents-python/tools/) 与 [Issue #1494：truncation 选项语义](https://github.com/openai/openai-agents-python/issues/1494) — 五类工具与 `FunctionTool` 回灌机制，`ModelSettings.truncation` 的 `auto`/`disabled` 语义与边界。

> [!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)
