AI SDK: 用 Tool Calling 替代 Output, 并安全解析流式输出

最近把一个多 Agent 对话项目从“直接结构化输出 (output)”迁移到了“工具调用 (tool calling)”, 过程中踩了几个很典型的坑, 尤其是前端流式消息解析。
这篇就按实战过程做一次对比:

  1. 当前 assistant 消息解析方式 (useChat + message.parts + tool-showResponse)
  2. useObject 的能力边界
  3. 之前 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()),
  }),
})

这一步的意义是:

  1. 把“最终展示载荷”从自然语言里剥离出来
  2. 让最终展示内容受 schema 约束
  3. 前端读取路径统一为 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 更擅长的部分。

一个简化判断

  1. 页面核心是“单对象” -> 优先 useObject
  2. 页面核心是“会话与编排” -> 优先 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. 解析层的工程建议 (避免再踩坑)

  1. 解析函数纯函数化
    getDisplayText(message) 只做映射, 不产生副作用, 更容易测试。

  2. 永远使用 find 而不是固定索引
    parts[0] 在流式下不可靠。

  3. 按 role 设计不同解析路径
    user 路径和 assistant 路径应该天然分开。

  4. 准备回退链路
    tool -> text -> placeholder 是最稳妥的一条链。

  5. 把业务“最终展示协议”固化为工具
    比如 showResponse, 后续做埋点、回放、审计会非常轻松。


8. 一句话总结

output 依然有价值, 但在多 Agent 流式系统里, 用 tool calling 承载最终展示协议, 再配合 role/type 分层解析, 会更稳定、更可控、也更容易长期维护。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐