AI SDK 底层机制

为什么要把 Vercel AI SDK 的内部机制单独拉一个章节讲——三层 API、版本锚定、阅读路径

为什么单独一章

Zapvol 的 Agent 引擎建立在 Vercel AI SDK 之上。表面上我们只是调 new ToolLoopAgent(...)agent.stream(...)result.toUIMessageStream(...) 三个 API,但真正决定 agent 跑得对不对的细节,全部藏在这三层 API 的内部状态模型和回调顺序里。

仅凭官方文档不够看。社区里常见的四类问题都源于没吃透底层:

  • prepareStep 里改消息为什么下一步还在?(共享引用,不是深拷贝)
  • onStepFinishonFinish 为什么触发了两次?(其实是两份同名回调,分别在 streamText 层和 UI 流层)
  • stopWhen 没设,agent 怎么只跑一步就结束了?ToolLoopAgentstreamText 的默认值不同,兜底链有坑)
  • messageMetadata 在 UI 里怎么执行得这么频繁?(每个 chunk 都会调一次)

这些都不是 bug,是 SDK 的设计语义——弄明白之前只能靠试错,弄明白之后每一个都是可利用的工程杠杆。

这一章的目标:把这些机制一次性讲透,让你写 agent 时不再靠猜。

版本锚定

本章基于 [email protected](Zapvol 当前依赖版本,位于 packages/backend/package.json)。所有源码引用、代码行号、回调签名都对应这个版本。v7有路线图变更时会单开迁移页。

ai@^6
├── streamText / generateText              ← 底层执行函数
├── ToolLoopAgent                           ← 对 streamText 的 agent 风格封装
└── createUIMessageStream / toUIMessageStream  ← 下游 UI 消费流

三层 API 地图

所有”为什么这样”的解释都会回到这张地图——把你目前读到的参数或回调定位到它属于哪一层,能回答一半的问题。

API职责生命周期
L1new ToolLoopAgent({ ... })定义 agent 的静态配置:model、instructions、tools、stopWhen、prepareStep、6 个回调构造一次,多次 stream() / generate() 复用
L2agent.stream({ ... })单次执行参数:messages、abortSignal、timeout,以及 L1 同名回调的覆盖版每次调用一次,会把参数下发给 streamText
L3result.toUIMessageStream({ ... })fullStream 转换为 UI 消息流(SSE 事件序列),拥有独立的 messageMetadata / onFinish / onError 回调每次调用一次,下游 transform,和 L1/L2 回调并发运行

关键点:L3 的 onFinish 和 L1/L2 的 onFinish 不是同一个东西——触发时机、payload、用途都不同。弄混直接导致 token 统计错位、消息持久化时点错乱。完整对照见 生命周期

本章六页怎么读

按”发送端骨架 → 钩子 → 接收端 → 端到端”的顺序——先把一次 agent.stream() 的完整时间线吃透(三层 × 12 个回调),再往下钻 L3 的两条 UI 流路径、步循环里的消息引用、最深的 prepareStep 钩子;跳到接收端,看 useChat 怎么消费;最后用”端到端协同”把五页串起来看。前四页是发送端,第五页是接收端,第六页把两侧合在一起。

顺序对应运行时的哪一层
1运行生命周期骨架——三层 API(L1 ToolLoopAgent / L2 streamText / L3 toUIMessageStream)在时间轴上的 12 个回调 + 双层同名回调陷阱 + stopWhen / timeout 默认值
2UI 流编排L3 深挖 / 发送端——lifecycle 里的 L3 只覆盖了 result.toUIMessageStream() 的 transform 路径;本页补 createUIMessageStream({ execute }) 的 execute-driven 路径、writer 三方法、自定义 data-* 事件、transient 语义、错误捕获全貌
3消息引用模型步循环里的消息机制——四条消息链的引用关系(initialMessages / responseMessages / stepInputMessages / prepareStepResult.messages)
4prepareStep 语义最深层的步钩子——可覆盖字段、典型模式、mutate-vs-push 陷阱
5客户端消费 (useChat)接收端——@ai-sdk/reactuseChat 怎么把 UI 流变成 React state;ChatStatus 四态、ChatTransport 四选一、UIMessage vs ModelMessage 桥接、onFinish 三态区分、客户端工具状态机、resume 双端协议、10 条常见陷阱
6端到端协同双端合起来看——一次 sendMessage 的完整旅程(序列图 + 每环源码位置)、UIMessageChunk 25 种类型全参考、abort / resume / error 三个双端协议的实现模板、SSE 环境层陷阱

读完这六页,你应该能回答开头提到的四类问题,外加接收端的 “status 为什么卡在 submitted” / “onError 之后 messages 为什么还在” / “tool call UI 为什么不渲染”,以及双端的”client 停了 server 怎么还在跑” / “resume 到底怎么落地” / “SSE 为什么被反向代理卡住”。如果还有疑问,回来对照地图定位。

本章不做什么

  • 不是 AI SDK 入门教程——假设读者已经会用 streamTextgenerateTextToolLoopAgent 的基础 API。入门看官方文档
  • UI 框架绑定只深挖 useChat(第 5 页 客户端消费)——@ai-sdk/react 的其他 hook(useCompletionuseObject)不展开。Zapvol 自己主对话不用 useChat(走自定义 React Query + SSE hook,Case study 见第 5 页末尾),但在一次性补全场景(AI Assistant 侧边面板)使用 useCompletion
  • 不覆盖 provider 层@ai-sdk/anthropic 等)——provider 选择是 Zapvol 层决策,参见 Agent Engine
  • 不是 SDK 贡献指南——只讨论作为应用层消费者需要知道的内部机制。

不做什么的原因:避免和官方文档重复

Vercel 官方文档覆盖”怎么用”,本章覆盖”为什么这样用才对”。这是精确的分工:

官方文档本章
API 签名、参数列表、代码示例回调触发顺序、引用语义、默认值兜底链、反直觉陷阱
教你写第一个 agent教你 debug 第三次出问题的 agent
按主题组织(generating text / tools / streaming)真实执行顺序组织(三层 × 时间线)

延伸阅读

这页有帮助吗?