聊聊 Vercel AI SDK 的流式协议:前后端到底是怎么"边想边说"的

聊聊 Vercel AI SDK 的流式协议:前后端到底是怎么"边想边说"的

做 AI 应用绕不开一个体验问题:模型的回答不能等它全部生成完再"啪"地甩给用户,得像打字机一样一个字一个字往外冒。这个"边想边说"的能力,前端拿到的其实不是一坨最终文本,而是一条结构化的事件流

Vercel AI SDK 把这条流的格式标准化了下来,官方叫它 Stream Protocol(流式协议) 。这篇文章就带你把它讲清楚:它解决什么问题、有哪几种、每种长什么样,以及------最有意思的------怎么脱离这个 SDK,用你自己的后端(哪怕是 Python)去对接它的前端

参考自官方文档:ai-sdk.dev/docs/ai-sdk...


一、两种协议:文本流 vs 数据流

1. Text Stream Protocol(文本流)------ 极简版

流里只有纯文本片段,前端拿到一段拼一段,最后凑成完整回答。没有类型、没有元数据,就是最朴素的字符流。

适合:只需要输出一段纯文本、不涉及工具调用/推理过程/引用来源的简单场景。

前端要显式指定用文本协议(通过 TextStreamChatTransport):

typescript 复制代码
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { TextStreamChatTransport } from 'ai';

const { messages, sendMessage } = useChat({
  transport: new TextStreamChatTransport({ api: '/api/chat' }),
});

后端用 toTextStream + createTextStreamResponse

typescript 复制代码
// app/api/chat/route.ts
import { convertToModelMessages, createTextStreamResponse, streamText, toTextStream } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: 'xai/grok-4.5',
    messages: await convertToModelMessages(messages),
  });
  return createTextStreamResponse({
    stream: toTextStream({ stream: result.stream }),
  });
}

2. UI Message Stream Protocol(数据流)------ 完整版

它基于 SSE(Server-Sent Events),相比纯文本多了几个关键好处:

  • 标准化:走 SSE 规范,生态工具通用
  • 保活(keep-alive):靠 ping 维持长连接
  • 可重连:断线能续
  • 缓存友好

最核心的区别是:它传的不是文本,而是一连串带类型、带 ID 的结构化事件(stream part)。文本、推理过程、工具调用、引用来源、错误......每一种都有自己的事件类型。


二、数据流协议的"事件字典"

这是整个协议的精华。后端往外发的每一帧,都是一个带 type 字段的 JSON。按用途分组来看:

消息与步骤生命周期

事件类型 示例 含义
start {"type":"start","messageId":"..."} 一条新消息开始
start-step {"type":"start-step"} 一个步骤(一次 LLM 调用)开始
finish-step {"type":"finish-step"} 当前步骤结束(多次调用拼接时用)
finish {"type":"finish"} 整条消息结束
abort {"type":"abort","reason":"user cancelled"} 流被中断

文本内容(最常用)

文本不是一次发完,而是 start → delta(多次)→ end 三段式,每个文本块有唯一 id

事件类型 示例 含义
text-start {"type":"text-start","id":"msg_1"} 文本块开始
text-delta {"type":"text-delta","id":"msg_1","delta":"你好"} 增量文本
text-end {"type":"text-end","id":"msg_1"} 文本块结束

推理过程(Reasoning)

和文本同构的三段式,专门传模型的思考过程;还能带推理时生成的文件:

事件类型 示例 含义
reasoning-start {"type":"reasoning-start","id":"r_1"} 推理块开始
reasoning-delta {"type":"reasoning-delta","id":"r_1","delta":"..."} 增量推理内容
reasoning-end {"type":"reasoning-end","id":"r_1"} 推理块结束
reasoning-file {"type":"reasoning-file","url":"data:image/png;base64,...","mediaType":"image/png"} 推理过程中生成的文件

工具调用(Tool Calling)

这一组最能体现数据流协议的价值------纯文本流根本表达不了工具调用。它把「输入流式生成 → (可选)审批 → 输出」完整地建了模:

事件类型 示例 含义
tool-input-start {"type":"tool-input-start","toolCallId":"c_1","toolName":"getWeather"} 开始流式生成工具入参
tool-input-delta {"type":"tool-input-delta","toolCallId":"c_1","inputTextDelta":"北京"} 入参增量
tool-input-available {"type":"tool-input-available","toolCallId":"c_1","toolName":"getWeather","input":{...}} 入参就绪,可执行
tool-approval-request {"type":"tool-approval-request","toolCallId":"c_1","approvalId":"a_1","isAutomatic":true} 该工具调用需要审批
tool-approval-response {"type":"tool-approval-response","approvalId":"a_1","approved":false,"reason":"..."} 审批结论
tool-output-available {"type":"tool-output-available","toolCallId":"c_1","output":{...}} 工具执行结果
tool-output-denied {"type":"tool-output-denied","toolCallId":"c_1"} 审批被拒,未执行

引用、文件与自定义数据

事件类型 示例 含义
source-url {"type":"source-url","sourceId":"...","url":"https://..."} 引用的外部链接
source-document {"type":"source-document","sourceId":"...","mediaType":"file","title":"标题"} 引用的文档
file {"type":"file","url":"https://.../a.png","mediaType":"image/png"} 文件引用
data-* {"type":"data-weather","data":{"location":"SF","temperature":100}} 任意结构化自定义数据
custom {"type":"custom","kind":"openai.compaction","providerMetadata":{...}} provider 专属内容
error {"type":"error","errorText":"出错了"} 错误信息

data-* 是个很实用的口子:data- 后面接什么由你定,前端能拿到对应的结构化 payload,适合往消息里塞自定义 UI 组件需要的数据。

流的结束

整条流以一个特殊标记收尾:

ini 复制代码
data: [DONE]

三、一条真实的 SSE 流长什么样

把上面的事件串起来,一次"你好 → 模型回复"的完整 SSE 响应大致是这样(每行一个 SSE data: 帧):

vbnet 复制代码
data: {"type":"start","messageId":"msg_abc"}

data: {"type":"start-step"}

data: {"type":"text-start","id":"t_1"}

data: {"type":"text-delta","id":"t_1","delta":"你"}

data: {"type":"text-delta","id":"t_1","delta":"好"}

data: {"type":"text-delta","id":"t_1","delta":",有什么可以帮你?"}

data: {"type":"text-end","id":"t_1"}

data: {"type":"finish-step"}

data: {"type":"finish"}

data: [DONE]

前端 useChat 会自动把这些帧还原成一条 message,并把 text-delta 逐字拼进去,于是你就看到了打字机效果。


四、用 SDK 实现(TypeScript)

需要说明:下面这段是基于 AI SDK 7.0 + Next.js 的写法 ,toUIMessageStreamcreateUIMessageStreamResponse 这些 API 都是这个 SDK 特有的。协议本身是语言无关的,但"帮你自动处理协议"的这套封装只有官方 TS SDK 提供;换到别的语言/框架,实现方式完全不同(见下一节)。

如果前后端都在 Next.js 里,你几乎不用碰协议细节------SDK 全帮你处理了。

前端就一行:

typescript 复制代码
// app/page.tsx
const { messages, sendMessage } = useChat(); // 默认就是数据流协议

后端用 toUIMessageStream + createUIMessageStreamResponse

typescript 复制代码
// app/api/chat/route.ts
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
} from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: 'xai/grok-4.5',
    messages: await convertToModelMessages(messages),
  });
  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
}

渲染时按 part.type 分支即可:

typescript 复制代码
{message.parts.map((part, i) => {
  switch (part.type) {
    case 'text':
      return <div key={i}>{part.text}</div>;
    // case 'tool-getWeather': ...
    // case 'data-weather': ...
  }
})}

五、真正的重头戏:用自定义后端对接

这才是协议标准化最大的意义。假设你的模型编排逻辑在一个 Python 后端 (或者任何非 JS 服务)里,前端仍然想用 useChat,你只需要做两件事:

  1. 响应头带上x-vercel-ai-ui-message-stream: v1
  2. 按 SSE 格式,把上面那些事件一帧帧写出去 ,最后补一个 data: [DONE]

下面用 Python + FastAPI 举例,但这只是其中一种语言的实现。协议不挑语言,Go、Ruby、Java 都能做------核心就是"按 SSE 格式吐符合上面事件字典的 JSON",各语言的 HTTP 流式响应写法不同而已。

一个最小化的 FastAPI 示例:

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json

app = FastAPI()

def sse(event: dict) -> str:
    return f"data: {json.dumps(event, ensure_ascii=False)}\n\n"

@app.post("/api/chat")
async def chat():
    async def gen():
        yield sse({"type": "start", "messageId": "msg_1"})
        yield sse({"type": "start-step"})
        yield sse({"type": "text-start", "id": "t_1"})
        for token in ["你", "好", ",世界"]:
            yield sse({"type": "text-delta", "id": "t_1", "delta": token})
        yield sse({"type": "text-end", "id": "t_1"})
        yield sse({"type": "finish-step"})
        yield sse({"type": "finish"})
        yield "data: [DONE]\n\n"

    return StreamingResponse(
        gen(),
        media_type="text/event-stream",
        headers={"x-vercel-ai-ui-message-stream": "v1"},
    )

前端完全不用改,useChat() 照常工作。

不用自己拼 JSON:现成的适配器

上面手写 SSE 是为了讲清楚原理。真到生产里,更省事的是用生态里已有的适配层------它们把「框架内部的流」直接翻译成这套协议,你连事件字典都不用背。举两个典型:

① Pydantic AI(Python)------ VercelAIAdapter

Pydantic AI 官方内置了对 Vercel AI Data Stream Protocol 的支持,一行 dispatch_request 就把「接收前端输入 → 跑 Agent → 按协议流式吐事件」全包了:

python 复制代码
from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import Response
from pydantic_ai import Agent
from pydantic_ai.ui.vercel_ai import VercelAIAdapter

agent = Agent('openai:gpt-5.2')
app = FastAPI()

@app.post('/chat')
async def chat(request: Request) -> Response:
    return await VercelAIAdapter.dispatch_request(request, agent=agent)

文档:pydantic.dev/docs/ai/int...

② LangChain / LangGraph(TS)------ @ai-sdk/langchain

如果后端在 JS 侧但用的是 LangChain,官方 adapter 提供 toBaseMessages()(把 UIMessage 转成 LangChain 消息)和 toUIMessageStream()(把 LangChain/LangGraph 的流转回协议格式),自动识别流类型:

typescript 复制代码
import { toBaseMessages, toUIMessageStream } from '@ai-sdk/langchain';
import { ChatOpenAI } from '@langchain/openai';
import { createUIMessageStreamResponse, UIMessage } from 'ai';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const model = new ChatOpenAI({ model: 'gpt-4o-mini' });

  const langchainMessages = await toBaseMessages(messages);
  const stream = await model.stream(langchainMessages);

  return createUIMessageStreamResponse({
    stream: toUIMessageStream(stream),
  });
}

文档:ai-sdk.dev/providers/a...

一句话:能找到现成 adapter 就别手写 SSE;实在没有(比如 Ruby),再照着事件字典自己吐------反正协议是公开且稳定的。

小提示:SDK 版本号和协议 wire 版本号是两回事。响应头目前固定 v1;内部 data-stream 协议里 6 起支持工具审批流,而 7 发出的线格式与 6 一致(v7 == v6 的 wire)。做对接时对齐这几个事件类型即可,不必纠结 SDK 大版本号。


六、小结

  • 协议不是 SDK:它是前后端之间传流式数据的格式约定,可脱离 SDK 单独使用。
  • 两种协议:纯文本流(简单)与 UI 数据流(默认、功能全,基于 SSE)。
  • 数据流的核心是"事件字典" :文本、推理、工具调用、引用、错误各有类型,文本/推理都是 start→delta→end 三段式。
  • 跨语言对接很简单 :带上 x-vercel-ai-ui-message-stream: v1,按 SSE 吐事件,以 data: [DONE] 收尾,你的 Python/Go/Ruby 后端就能喂饱前端的 useChat

如果你正在做 AI 应用,又不想把模型编排全塞进 Node,这套协议就是你把「任意后端」和「Vercel 那套顺手的前端 hook」缝合起来的那根线。


参考:AI SDK UI: Stream Protocols · AI SDK 官网

相关推荐
胡萝卜术1 小时前
在浏览器中跑 DeepSeek-R1:WebGPU 推理全流程深度解析
前端·javascript·面试
得物技术1 小时前
得物知识问答:复合检索 Agent 的系统设计实践
人工智能·后端·ai编程
andongni2031 小时前
SpringBoot 入门实验报告
java·spring boot·后端
windliang1 小时前
Claude Code 源码分析(十):MCP 外部工具如何进入下一轮 Agent 调用
前端·算法·面试
Yan_chen6661 小时前
CTFHub XSS反射型实战攻略
前端·网络安全·漏洞·xss·ctfhub web前置技能
李广坤2 小时前
Agent 记忆系统详解:从"金鱼脑"到"过目不忘"的进化之路
后端
web像素之境2 小时前
前端工程化梳理
前端
小林ixn2 小时前
全栈项目实战:前端独立开发,不再傻等后端接口
前端·javascript·react.js
李剑一2 小时前
Anthropic将在AI生成文本中嵌入水印!难道是用我之前写的这个技术?
前端·aigc·ai编程