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 时不再靠猜。

版本锚定

本章基于 ai@6.0.134(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 钩子。每一页都建立在上一页的骨架之上:

顺序对应运行时的哪一层
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 陷阱

读完这四页,你应该能回答开头提到的四类问题。如果还有疑问,回来对照地图定位。

本章不做什么

  • 不是 AI SDK 入门教程——假设读者已经会用 streamTextgenerateTextToolLoopAgent 的基础 API。入门看官方文档
  • 不覆盖 UI 框架绑定@ai-sdk/reactuseChatuseCompletion)——Zapvol 用自定义 React Query hooks 消费 SSE,不走 useChat
  • 不覆盖 provider 层@ai-sdk/anthropic 等)——provider 选择是 Zapvol 层决策,参见 Agent Engine
  • 不是 SDK 贡献指南——只讨论作为应用层消费者需要知道的内部机制。

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

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

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

延伸阅读

这页有帮助吗?