首页 › ai-agents › 8KB盲区:工具结果静默截断六栈实测

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

概述

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

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

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

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

六栈截断行为对比

框架(版本) 默认截断行为 是否告警 止损原语 重查代价 最适场景
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 灌进上下文,模型还没读一个字,窗口已经被吃掉一大块——这是消化税的另一面:不是答案错,是窗口提前破产。

# 反面教材:一个会撒谎的工具
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 又不告警。要真正止损,必须自己包一层:

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 通道把截断这件事变成一种结构化信号:

# 截断 → 走 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 里永远能看到截断发生过。

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"的判断,在消化税这一维度上得到了精确印证。

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 是重查代价最低的栈——因为它根本没把全量放进需要重查的地方。

# 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 的"游标分页修静默截断"正是分页续读路线的两个落地范本。

工具结果消化流水线

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 截断,有没有那一行截断标记,决定了路径走向绿色的"按需续读"还是红色的"盲区编造"。这行标记成本近乎为零,却把撒谎率从高位压到接近零——这是整期实测投入产出比最高的一笔改动。

推荐方案

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

参考来源

  1. Silent Tool Truncation: The Default Cap Your Agent Reasons Over Without Knowing — 复现 142KB 载荷被默认截断到 8192 字节、模型基于残片自信作答的盲区案例,给出截断标记与续读入口的修复方向。
  2. Tool-Result Truncation: The Silent Bug That Makes Agents Lie — 工具返回 50KB、模型只看到约 700 字符并"自信总结不存在内容"的失真机制剖析。
  3. Handling Token Limits When Processing Large JSON Responses in an MCP Client/Server Flow — GitHub Issues.list 单次灌入 14000 token 的实例,以及把载荷压到 500 token 以内的四种止损模式。
  4. MCP Pagination Patterns: Handling Large Result Sets Without Blowing Your Context — MCP 工具返回万行结果压垮上下文的问题,及游标/偏移分页模式的工程实现。
  5. Tools — OpenAI Agents SDK 官方文档 与 Issue #1494:truncation 选项语义 — 五类工具与 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学通信知识星球海报-扫码加入