AI SDK: 用 Tool Calling 替代 Output, 并安全解析流式输出
文章目录
AI SDK: 用 Tool Calling 替代 Output, 并安全解析流式输出
最近把一个多 Agent 对话项目从“直接结构化输出 (output)”迁移到了“工具调用 (tool calling)”, 过程中踩了几个很典型的坑, 尤其是前端流式消息解析。
这篇就按实战过程做一次对比:
- 当前 assistant 消息解析方式 (
useChat + message.parts + tool-showResponse) useObject的能力边界- 之前
output结构化输出的优缺点
1. 先说结论
在 多 Agent + 流式 + 前端消息渲染 场景里:
output更像“单步结构化返回”useObject更像“单对象流式更新”tool calling更像“可编排、可观测、可回退的协议层”
如果你要的是“稳定展示给用户的最终回复”, 推荐把它收敛到一个明确工具里, 比如 showResponse, 然后前端按 role + part.type 分层解析。
2. 背景: 为什么 output 在流式场景会变脆
output 的核心优点是直接: 定义 schema, 拿结构化结果, 类型清晰。
但在流式过程中, 前端真实拿到的是 message.parts 的混合片段, 而不是“永远完整且单一”的对象:
- 可能有
text - 可能有
tool-* - 可能还有中间状态片段
当你做多阶段编排 (admin -> structure -> critic -> style) 时, “只读最后一段文本”会越来越不稳定。
这时如果仍把展示逻辑绑在自然语言文本上, 很容易出现 UI 显示错位或空白。
更关键的问题是:通过 output 约束的结构化结果在流式阶段通常以文本增量传输,前端在中途解析时容易遇到 JSON 不完整等问题。
3. 迁移思路: 把“给用户看的最终答复”变成工具参数
我在 agent 中定义了一个专门用于展示的工具:
showResponse: tool({
description: "Show the response to the user.",
inputSchema: z.object({
text: z.string(),
necessary: z.boolean(),
uiDescription: z.string(),
uiNeeds: z.array(z.string()),
}),
})
这一步的意义是:
- 把“最终展示载荷”从自然语言里剥离出来
- 让最终展示内容受 schema 约束
- 前端读取路径统一为
tool-showResponse.input
换句话说, 我们不再“猜模型说了什么”, 而是“消费模型明确提交的参数”。
4. 当前 assistant 消息解析方式 (安全解析)
这里最关键的是 按角色分流, 不能所有消息都按工具字段去读:
const getMessageText = (message: AdminAgentMessage) =>
message.parts
?.map((part) => (part.type === "text" ? part.text : ""))
.join("")
.trim();
const getDisplayText = (message: AdminAgentMessage) => {
if (message.role === "user") {
return getMessageText(message) || "(empty user message)";
}
const toolText = message.parts
.find((part) => part.type === "tool-showResponse")
?.input?.text;
return toolText || getMessageText(message) || "(non-text message)";
};
这段策略对应了三个现实问题:
- 用户消息没有
tool-showResponse: 所以 user 必须读text - assistant 可能同时有 text/tool: 优先业务工具, 再回退 text
- 流式分片可能暂时不完整: 最后兜底占位, 避免空白气泡
之前“只能显示 assistant, 不显示 user”的问题, 本质就是把所有消息都按
tool-showResponse读取了。
5. 和 useObject 的对比
useObject (experimental_useObject) 很好用, 但它是另一个抽象层。
它更适合“我就想要一个持续刷新的对象”, 例如:
- 表单草稿自动补全
- 一份配置 JSON 逐步成型
- 一个 dashboard 配置对象的流式编辑
而多轮对话 + 多 Agent 编排里, 你往往还需要:
- role 区分 (user/assistant/system)
- 多消息历史
- 工具调用链与中间过程可观测
- 服务端多段
writer.merge(...)流拼接
这些是 useChat + tool calling 更擅长的部分。
一个简化判断
- 页面核心是“单对象” -> 优先
useObject - 页面核心是“会话与编排” -> 优先
useChat + tool calling
6. 和旧 output 结构化输出的对比
6.1 约束强度
output: 模型直接产出对象, 结构明确tool calling: 工具 schema + 调用路径约束, 对“最终载荷”控制更细
6.2 可观测性
output: 更像结果导向, 过程细节少tool calling: 每次工具调用参数天然可观察、可审计
6.3 流式解析成本
output: 简单场景接入成本低tool calling: 前期要写parts解析器, 但长期更稳
6.4 工程可演进性
output: 适合单 agent 单步返回tool calling: 更适合多 agent 多阶段的扩展
7. 解析层的工程建议 (避免再踩坑)
-
解析函数纯函数化
getDisplayText(message)只做映射, 不产生副作用, 更容易测试。 -
永远使用
find而不是固定索引parts[0]在流式下不可靠。 -
按 role 设计不同解析路径
user 路径和 assistant 路径应该天然分开。 -
准备回退链路
tool -> text -> placeholder是最稳妥的一条链。 -
把业务“最终展示协议”固化为工具
比如showResponse, 后续做埋点、回放、审计会非常轻松。
8. 一句话总结
output 依然有价值, 但在多 Agent 流式系统里, 用 tool calling 承载最终展示协议, 再配合 role/type 分层解析, 会更稳定、更可控、也更容易长期维护。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)