创建日期:2026-09-20 | 最近更新:2026-09-20 本篇所有线格式与帧序列均为本机实测:
ai@7.0.107(AI SDK 7)+ Node v24.14.1,用内置 Mock 模型驱动,不需要任何 API Key。脚本与完整输出留档在source/aisdk-lab/。 承接上一篇:Agent 流式传输选型——那篇从协议层证明「大厂主流是 POST + fetch + ReadableStream + SSE 帧」,这篇直接把 Vercel AI SDK 的字节抓下来验证这个结论。
Vercel AI SDK 入门:把它的线格式抓下来看
Vercel AI SDK 常被误解成「又一个 LangChain」。它不是。LangChain 的野心是编排方法论;AI SDK 的野心是「让流式对话在你的 Web 应用里跑通」——所以它的核心资产是一套流式协议 + 前后端绑定,而不是一堆抽象。
这篇的做法很直接:不看文档说它用什么协议,直接把响应字节抓下来。结论一句话——它就是 SSE,而且它自己把上一篇讲的「缓冲陷阱」写进了响应头。
1. 一句话定位
| 它是什么 | 它不是什么 | |
|---|---|---|
| 定位 | 模型接入统一层 + 流式协议 + UI 绑定 | 不是编排框架(没有图、没有 reducer) |
| 核心资产 | UI Message Stream 协议(前后端约定) | 不是「抽象层越多越好」 |
| 强项 | 25+ provider 统一调用、React/Next 的 useChat 开箱、MCP 支持、部署无关 | 内置记忆/可观测性相对薄(见下一篇) |
版本现状(实测):装下来的就是 ai@7.0.107。v7 里 streamText 只是「单步流式原语」,多步 Agent 循环已经升格成独立的类(ToolLoopAgent),另外还有 HarnessAgent(可直接把 Claude Code / Codex / Pi 当成 harness 跑,见 §5)。
2. ★ 核心实测:它的线格式到底是什么
用一句话总结:text/event-stream(也就是 SSE),并且自带防缓冲头。 直接看抓到的响应头与字节:
content-type : text/event-stream
cache-control : no-cache
x-accel-buffering: no ← ★ 它主动让 nginx 不要缓冲
x-vercel-ai-ui-message-stream: v1 ← ★ 协议版本头
收到 13 个 chunk,用时 161 ms(分块到达 = 真流式)
前几帧的原始字节(就是上一篇里那套 data: ...\n\n 结构):
[ 4 ms] "data: {\"type\":\"start\"}\n\n"
[ 12 ms] "data: {\"type\":\"start-step\"}\n\n"
[ 12 ms] "data: {\"type\":\"text-start\",\"id\":\"t1\"}\n\n"
[ 27 ms] "data: {\"type\":\"text-delta\",\"id\":\"t1\",\"delta\":\"你好\"}\n\n"
[ 43 ms] "data: {\"type\":\"text-delta\",\"id\":\"t1\",\"delta\":\",我是\"}\n\n"
[ 59 ms] "data: {\"type\":\"text-delta\",\"id\":\"t1\",\"delta\":\"助手\"}\n\n"
[ 75 ms] "data: {\"type\":\"text-end\",\"id\":\"t1\"}\n\n"
[ 90 ms] "data: {\"type\":\"tool-input-start\",\"toolCallId\":\"call_1\",\"toolName\":\"get_weather\",\"dynamic\":false}\n\n"
[ 106 ms] "data: {\"type\":\"tool-input-delta\",\"toolCallId\":\"call_1\",\"inputTextDelta\":\"{\\\"city\\\":\"}\n\n"
[ 122 ms] "data: {\"type\":\"tool-input-delta\",\"toolCallId\":\"call_1\",\"inputTextDelta\":\"\\\"杭州\\\"}\"}\n\n"
[ 158 ms] "data: {\"type\":\"finish-step\"}\n\n"
[ 159 ms] "data: {\"type\":\"finish\"}\n\n"
[ 161 ms] "data: [DONE]\n\n"
三个可以直接下结论的点:
- 它和「SSE」不是竞品关系 —— 它就是 SSE。上一篇那句「ReadableStream + fetch 是读 SSE 的客户端 API」在这里被官方实现坐实了;
x-accel-buffering: no这个头,正是上一篇 §5.2 那个「缓冲代理把流式变成一次性返回」的官方对策。SDK 作者显然被同一个坑坑过——他们选择直接在响应里告诉 nginx「别缓冲」;- 末帧是
data: [DONE],不是合法 JSON —— 解析器必须特判(我的第一版脚本就在这上面直接抛了SyntaxError)。这是从 OpenAI 那边沿用过来的哨兵。
帧类型速查
帧 type | 含义 |
|---|---|
start / finish | 整条流的开始 / 结束 |
start-step / finish-step | 一次模型调用的开始 / 结束(多步 Agent 会出现多组,见 §5) |
text-start / text-delta / text-end | 正文增量的生命周期(带 id,可多路并发) |
tool-input-start / tool-input-delta | 工具入参的增量(模型边想边说 JSON) |
tool-input-available / tool-output-available | 工具入参就绪 / 工具结果回填 |
用上一篇的读取骨架解析它
我把上一篇那套(按 \n\n 切、取 data: 后面那段)原样复用,不改一行:
按 SSE 规则切出 13 帧(残留尾巴 0 字节)
→ type=start / start-step / text-start / text-delta ×3 / text-end
→ tool-input-start / tool-input-delta ×2 / finish-step / finish
→ [DONE] ← 非 JSON 哨兵帧,直接 JSON.parse 会抛错
同一段 20 行代码,既能读 Vercel AI SDK 的流 —— 这正是上一篇的核心论点:帧格式与读取方式是两层,协议才是可复用的那层。
3. 同一个 streamText(),换方法就换协议
这点很能说明「流」与「帧」是解耦的。同一段生成逻辑,只换响应方法:
| 方法 | content-type | 帧形态 |
|---|---|---|
.toUIMessageStreamResponse() | text/event-stream | SSE,13 帧,带 [DONE] |
.toTextStreamResponse() | text/plain; charset=utf-8 | 完全没有 SSE,3 个纯文本块 |
实测纯文本那条:
content-type : text/plain; charset=utf-8
收到 3 个 chunk,用时 49 ms
[ 19 ms] "你好" [ 34 ms] ",我是" [ 49 ms] "助手"
仍然是真流式(分块到达),但一个 SSE 帧都没有。 所以「流式」和「SSE」也是两件事——前者是传输行为,后者是帧格式。要不要带工具事件、要不要结构化帧,是「用哪套协议」的选择。
SSE 在 SDK 里是个可单独使用的原语
SDK 导出了 JsonToSseTransformStream,把「JSON → SSE 帧」做成了一等公民:
输入:两个 JS 对象({a:1} 和 {b:"中文"})
输出: "data: {\"a\":1}\n\ndata: {\"b\":\"中文\"}\n\ndata: [DONE]\n\n"
注意它自动补了 [DONE]——哨兵的来源就在这里。(顺带一个实测细节:这个 transform 输出的是字符串块,直接塞进 Response 会报 Received non-Uint8Array chunk,需要自己编码。)
4. Agent 循环在「一条流」里跑完
这是承接上一篇最该讲的一点。用 v7 的 ToolLoopAgent 跑一次「模型调工具 → 服务端执行 → 再调模型 → 出答案」,实测帧序列:
[ 3 ms] {"type":"start"}
[ 14 ms] {"type":"start-step"} ← 第 1 次模型调用
[ 14 ms] {"type":"tool-input-available","toolCallId":"c1","toolName":"get_weather","input":{"city":"杭州"}}
[服务端] 工具真的被执行了:get_weather(杭州)
[ 29 ms] {"type":"tool-output-available","toolCallId":"c1","output":"杭州: 25°C 晴"}
[ 29 ms] {"type":"finish-step"}
[ 38 ms] {"type":"start-step"} ← 第 2 次模型调用
[ 38 ms] {"type":"text-start","id":"t9"}
[ 48 ms] {"type":"text-delta","id":"t9","delta":"杭州 25°C 晴"}
[ 58 ms] {"type":"text-end","id":"t9"}
[ 69 ms] {"type":"finish-step"}
[ 69 ms] {"type":"finish","finishReason":"stop"}
[ 72 ms] data: [DONE]
汇总:SSE 帧 11 个,step 2 个,服务端工具执行 1 次
读法:一次 HTTP 响应里,装着「2 次模型调用 + 1 次服务端工具执行」,用 start-step/finish-step 成对划分。
这对架构的含义很大:
- 客户端不需要自己驱动 Agent 循环——它只读一条流(对比:手写 ReAct 循环时,循环和状态都在客户端或你自己服务端);
- 工具在服务端执行(实测那行
[服务端] 工具被真正执行了),密钥和权限不落到浏览器; step边界在协议里是一等公民,所以 UI 可以把「第 2 步」渲染成独立的折叠块;- 也因为是一条流,所以上一篇讲的取消语义问题依然存在——停止生成仍然只能靠掐断连接,除非你在应用层再设计一套 cancel 协议。
v7 的 Agent 抽象:不止 ToolLoopAgent
实测确认 v7 里有两层:ToolLoopAgent(自己搭模型+工具循环)和 HarnessAgent(把现成的 harness 当后端跑)。官方文档列的 harness 适配器是独立包:
@ai-sdk/harness-claude-code Claude Code
@ai-sdk/harness-codex Codex
@ai-sdk/harness-pi Pi ← 与本站 pi-agent / InkOS 系列同一个运行时
有意思的是这条收敛:「coding agent 的 harness」正在变成可插拔的后端。本站 pi-agent 系列 与 InkOS 里那套「宿主自己掌控循环、落盘、确认闸门」的设计,和「把 harness 当一个流式后端接进来」正是同一个趋势的两端。
5. 六个实践要点
finishReason在 v7 是对象,不是字符串。我写自定义 Mock provider 时传finishReason: 'tool-calls',结果是other——Agent 循环直接不继续了(因为「有没有工具调用」判定失败)。正确写法是{ unified: 'tool-calls' }。自己接 provider 时这是最容易静默失败的一处。tool-call与tool-input-*两种工具帧形态都存在。我最初用tool-input-start/delta/end三件套,SDK 认不出、工具不执行;换成完整的tool-call({toolCallId, toolName, input})后正常。接真实 provider 时要按它实际的帧形态给。- 流式一定要在响应头或配置里关掉中间层缓冲。SDK 已经带了
x-accel-buffering: no;你自己写端点时要照做(否则见上一篇那组「收到 1 次 vs 50 次」的实测)。 [DONE]要特判,别直接JSON.parse。- 协议带版本头
x-vercel-ai-ui-message-stream: v1。服务端与客户端 SDK 版本不匹配时,这是你唯一的排查线索——升级 SDK 时把服务端和客户端一起升。 - 工具执行在服务端 = 权限边界也在服务端。别把「模型能调什么」当成安全边界;边界是你注册了哪些工具(这条与 InkOS 的「模型手里只有工具,没有权限」 是同一个道理)。
6. 一页速查
定位 : 模型统一接入 + 流式协议 + UI 绑定(不是编排框架)
协议 : SSE(text/event-stream),带协议版本头与防缓冲头
末帧 : data: [DONE](非 JSON,需特判)
两种形态: toUIMessageStreamResponse() = SSE;toTextStreamResponse() = text/plain
Agent : v7 用 ToolLoopAgent(自建循环)/ HarnessAgent(接 Claude Code / Codex / Pi)
读法 : fetch + ReadableStream,按 \n\n 切帧、取 data: 后解析(上一篇那套直接可用)
关联
- 前置:Agent 流式传输选型——SSE / ReadableStream / WebSocket 三层拆解与本篇的协议层依据
- 下一篇:AI SDK 横向调研——OpenAI / Anthropic 官方 SDK 的真实请求与消费形状实测,以及生态位划分
- harness 的两种设计取向:pi-agent 系列、InkOS 为什么不用 LangGraph
- 对照框架(编排方法论那一侧):LangChain + LangGraph 系列
自测
- Vercel AI SDK 的 UI Message Stream 用的什么线格式?举出两个实测到的证据。
x-accel-buffering: no这个响应头,对应上一篇里哪一类生产故障?- 同一个
streamText(),toUIMessageStreamResponse()与toTextStreamResponse()的线格式差别是什么?这说明「流式」和「SSE」是什么关系? - 多步 Agent 循环在协议里怎么体现?客户端需要自己驱动循环吗?
finishReason传字符串会发生什么?后果为什么严重?- 为什么说「工具执行在服务端」意味着权限边界也在服务端?
参考
- 实测脚本(随仓库留档,含 Mock 模型驱动,无需 API Key):
source/aisdk-lab/——wire.mjs(线格式抓取)、agent.mjs(ToolLoopAgent 多步循环)、sdks.mjs(官方 SDK 对比);跑法见其中README.md - 环境:
ai@7.0.107/ Node v24.14.1 / macOS(Darwin 24.6.0) - AI SDK 官方文档:ai-sdk.dev(Stream Protocols、Agents、Harnesses)
- 前作:Agent 流式传输选型