跳到主要内容

流式工具结果破坏了请求-响应式智能体规划器

· 阅读需 11 分钟
Tian Pan
Software Engineer

SQL 工具在数据从网络线路传出时即发送行。智能体调用它并期待得到结果。而一年前编写的运行环境(当时所有工具都是请求-响应式的)在调用模型之前,会尽职地将整个流缓冲成一个单一字符串。40 秒后,缓冲区达到了 200 KB,上下文窗口被消耗了一半,智能体正在对一个查询的第 47,000 行进行推理,而它本可以在第 30 行就停止。没有人故意设计这种失败——这仅仅是因为将“工具已返回”视为规划器唯一响应事件的结果。

向流式工具的转变正在规划器尚未察觉的情况下发生。SQL 引擎发出渐进式结果集。文档提取器生成分页。搜索 API 在相关性评分稳定后按批次返回命中结果。MCP 的 Streamable HTTP 传输协议(2025-03-26 规范中 HTTP+SSE 的替代方案)使增量响应成为一流的传输模式,而不再是一项稀有的功能。传输层已经准备就绪,但其上的规划器还没有。

大多数智能体运行环境仍将工具调用建模为 result = tool(args) —— 一个返回类型为单一聚合值的函数。当底层工具使用流式传输时,运行环境唯一的诚实选择是等待直到 EOF(传输结束)、按硬性的字节限制截断,或者停止读取并丢弃剩余部分。这些选择都没有让模型参与决策。真正有趣的举措几乎没人尝试:让模型在流传输过程中决定何时已经看够了。

缓冲并移交的失败模式

默认的集成路径将流式工具变成了同步工具。运行环境打开流,将数据块累积到缓冲区中,等待关闭事件,然后将整个有效载荷作为下一条用户消息交给模型。从模型的角度来看,工具的行为与缓慢的请求-响应调用没有区别。从系统的角度来看,有三件事已经悄然崩溃。

第一处崩溃是上下文窗口的计算。一个返回 50,000 行中等宽度 JSON 的 SQL 查询可能会产生超过 800 KB 的工具输出。在 200K token 的上下文窗口中,加上通常已经包含系统提示词、先前的工具调用和用户历史记录的智能体状态,一次贪婪的缓冲在模型开始推理结果之前就消耗了三分之一的预算。执行轨迹中的第二次工具调用空间比第一次少。第五次调用则必须剔除一些内容——通常是智能体最需要记住的用户请求部分。最近关于智能体中上下文窗口溢出的研究指出,工具输出膨胀是排名前三的失败模式,甚至排在长指令或草稿纸推理之前。

第二处崩溃是延迟。运行环境无法询问“这些够了吗?”,因为直到 EOF 之前模型都不在链路中。如果工具是按需分页的——例如一个文档提取器,每当上一页被确认时就产生一个新页面——“缓冲并移交”模式产生的流将永远无法到达 EOF,直到触发某些外部超时。智能体停止读取的决定被委托给了挂钟截止时间,而不是智能体自身关于第 30 行是否已经回答了问题的判断。

第三处崩溃是隐形的。在运行环境层进行截断是标准的退路,而 OpenAI Codex 等项目的 issue 队列将其追踪为智能体困惑的常态化来源:模型看到的工具结果只是工具实际输出的前缀,如果团队处理得当,会带有一个类似 [truncated] 的页脚;如果没处理好,则完全没有标记。模型会对部分数据集进行推理,仿佛它是完整的。输出会以追踪日志无法察觉的方式变得极其错误,因为从追踪的角度来看,工具返回了一个值,而模型接受了它。

规划器可以推理的流式工具契约

修复方法不是“逐个 token 地流向模型”——这解决的是 UX 问题(感知到的响应延迟),而不是规划问题。修复方法是教会规划器某些工具是增量发出的,并赋予它参与流的原始语。

最小可行契约由四部分组成:

工具描述符中的 streaming 标志。 工具目录已经声明了名称、参数和描述;添加一个布尔值(或更丰富的模式枚举:request_responsechunkedpaginatedunbounded),以便规划器提前知道是否可以预期工具返回一个完整的结果。看到 streaming: true 的规划器应该构建其提示词以建模部分结果,设置中断路径,并且永远不要期待收到单一的“工具返回了 X”的消息。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates