AI SDK 底层机制
为什么要把 Vercel AI SDK 的内部机制单独拉一个章节讲——三层 API、版本锚定、阅读路径
为什么单独一章
Zapvol 的 Agent 引擎建立在 Vercel AI SDK 之上。表面上我们只是调 new ToolLoopAgent(...)、agent.stream(...)、result.toUIMessageStream(...) 三个 API,但真正决定
agent 跑得对不对的细节,全部藏在这三层 API 的内部状态模型和回调顺序里。
仅凭官方文档不够看。社区里常见的四类问题都源于没吃透底层:
prepareStep里改消息为什么下一步还在?(共享引用,不是深拷贝)onStepFinish和onFinish为什么触发了两次?(其实是两份同名回调,分别在 streamText 层和 UI 流层)stopWhen没设,agent 怎么只跑一步就结束了?(ToolLoopAgent和streamText的默认值不同,兜底链有坑)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 | 职责 | 生命周期 |
|---|---|---|---|
| L1 | new ToolLoopAgent({ ... }) | 定义 agent 的静态配置:model、instructions、tools、stopWhen、prepareStep、6 个回调 | 构造一次,多次 stream() / generate() 复用 |
| L2 | agent.stream({ ... }) | 单次执行参数:messages、abortSignal、timeout,以及 L1 同名回调的覆盖版 | 每次调用一次,会把参数下发给 streamText |
| L3 | result.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 默认值 |
| 2 | UI 流编排 | L3 深挖——lifecycle 里的 L3 只覆盖了 result.toUIMessageStream() 的 transform 路径;本页补 createUIMessageStream({ execute }) 的 execute-driven 路径、writer 三方法、自定义 data-* 事件、transient 语义、错误捕获全貌 |
| 3 | 消息引用模型 | 步循环里的消息机制——四条消息链的引用关系(initialMessages / responseMessages / stepInputMessages / prepareStepResult.messages) |
| 4 | prepareStep 语义 | 最深层的步钩子——可覆盖字段、典型模式、mutate-vs-push 陷阱 |
读完这四页,你应该能回答开头提到的四类问题。如果还有疑问,回来对照地图定位。
本章不做什么
- 不是 AI SDK 入门教程——假设读者已经会用
streamText、generateText、ToolLoopAgent的基础 API。入门看官方文档。 - 不覆盖 UI 框架绑定(
@ai-sdk/react、useChat、useCompletion)——Zapvol 用自定义 React Query hooks 消费 SSE,不走useChat。 - 不覆盖 provider 层(
@ai-sdk/anthropic等)——provider 选择是 Zapvol 层决策,参见 Agent Engine。 - 不是 SDK 贡献指南——只讨论作为应用层消费者需要知道的内部机制。
不做什么的原因:避免和官方文档重复
Vercel 官方文档覆盖”怎么用”,本章覆盖”为什么这样用才对”。这是精确的分工:
| 官方文档 | 本章 |
|---|---|
| API 签名、参数列表、代码示例 | 回调触发顺序、引用语义、默认值兜底链、反直觉陷阱 |
| 教你写第一个 agent | 教你 debug 第三次出问题的 agent |
| 按主题组织(generating text / tools / streaming) | 按真实执行顺序组织(三层 × 时间线) |
延伸阅读
- Zapvol Agent Engine — Zapvol 如何组合 AI SDK 构建自己的 agent 循环
- Context Compaction — 基于
prepareStep实现的三级压缩 - Tool Search — 基于
prepareStep的activeTools动态过滤 - AI SDK 官方文档 — 外部参考