你与AI对话: 从 Messages 协议到 SSE 流式渲染

在目前的 AI 全栈开发中,前端实现一个“打字机”效果的聊天框似乎轻而易举。但当你按下“发送”键的那一刻,数据在网络底层到底经历了怎样的变形?大模型是怎么“记住”上下文的?前端接到的那些奇怪的字符 0:"字" 又是从哪来的?

今天,我们就来扒开大模型对话交互的底层外衣,深入探讨 Messages 协议、HTTP SSE 流式通信,以及 Vercel AI SDK 在背后究竟帮你做了多少脏活累活。


🧬 一、大模型没有记忆:无状态的 Messages 数组

很多人以为,和 AI 聊天就像和人发微信,服务器端会保存我们的对话记录。事实上,大模型的 HTTP 接口是完全“无状态 (Stateless)”的。它没有记忆,每一次回答,都是在根据你传入的完整历史记录重新计算生成概率。

在底层,这个历史记录绝对不是把所有的聊天文本简单拼成一个字符串,而是一个有着严格结构的 JSON 对象数组——Messages

这个数组中的每一个对象,都必须包含 role 和 content。其中 role(角色)是底层数据结构中最核心的枚举值,通常分为四种:

1. "role": "system" (系统指令/人设)

  • 底层本质:权重最高的系统级配置,定义了模型的“灵魂”和行为准则(“你是谁”、“你应该怎么做”)。

  • 实战应用:在 RAG(检索增强生成)场景中,大模型就像一个演员,而 system 就是剧本。我们在后端查到的【私有背景知识】,就是强行塞进了 system 里,让模型在回答前先“脑补”这些设定。

  • 数据样例

    JSON

    1
    { "role": "system", "content": "你是一个严厉的代码审查员,只用 Markdown 找 bug。" }

2. "role": "user" (用户提问)

  • 底层本质:代表终端人类用户的输入。

  • 数据样例

    JSON

    1
    { "role": "user", "content": "我的 useState 报错了。" }

3. "role": "assistant" (AI 的历史回答)

  • 底层本质:代表大模型自己曾经说的话。

  • 为什么必须传? 如果用户追问“那么第二个参数呢?”,你必须把之前大模型回答的关于“第一个参数”的 assistant 消息一并传过去,它才知道你们在接着聊什么。

4. "role": "tool" (工具调用结果 / Agent 核心)

  • 底层本质:专为 Agent(智能体)准备。当大模型决定调用外部工具(比如查实时天气)时,后端执行完代码后,会将得到的结果以 tool 的身份塞回数组,再传给大模型做总结。

  • 数据样例

    JSON

    1
    { "role": "tool", "tool_call_id": "call_abc123", "content": "{ \"temp\": 24, \"city\": \"北京\" }" }

📡 二、打字机效果的真相:SSE 协议与 Vercel 的封装

你可能会好奇,大模型是一字一句吐出回答的,这在普通的 HTTP 请求(发一个 Request,等一个 Response)里是怎么做到的?答案是 HTTP Server-Sent Events (SSE)

1. 真实的底层:残酷的原生 SSE 字节流

当后端请求 DeepSeek 或 OpenAI 时,HTTP 请求头里会带上 Accept: text/event-stream。这意味着服务器保持 TCP 连接不断开,一块一块地给你推数据(HTTP/1.1 Chunked Encoding)。

大模型原生吐出来的原始字节流字符串,极其冗长复杂:

1
2
3
4
5
6
7
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"deepseek-chat","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"deepseek-chat","choices":[{"index":0,"delta":{"content":"use"},"finish_reason":null}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1694268190,"model":"deepseek-chat","choices":[{"index":0,"delta":{"content":"State"},"finish_reason":null}]}

data: [DONE]

解析底层细节:

  • 每一帧数据都严格以 data: 开头,并以 \n\n 结尾(SSE 协议的硬性边界)。

  • 你真正需要的那个增量文本(比如 “use” 和 “State”),藏在极其深的层级里:choices[0].delta.content

  • 最后一帧永远是一个特殊的标志 data: [DONE],表示流结束。

2. Vercel AI SDK 做了什么?(Data Stream Protocol)

如果你在前端 page.tsx 中直接去解析上面那一大坨原生字符串,你需要自己处理残缺的 JSON 解析、字符串手动拼接、以及各种网络异常断流。这简直是灾难。

在现代 Next.js 全栈项目中,后端 streamText() 函数在 Node.js 服务器里接住了这些原生的 data: {...},然后 Vercel SDK 将其“解压、清洗、重组”,通过网络推给前端时,变成了极其精简的私有协议结构:

1
2
0:"use"
0:"State"

这套协议使用了前缀魔数 (Magic Numbers)

  • 0: 代表这是普通的文本数据 (Text chunk)。

  • 3: 代表有内部报错 (Error)。

  • 9: 代表大模型触发了 Tool Call(工具调用)。

此时,前端的 useChat 钩子本质上就是一个状态机。它监听到 0: 开头的数据,就把冒号后面的字符串剥离出来,自动拼接到当前 UI 消息气泡的状态里。这就是你屏幕上看到的如丝般顺滑的打字机效果。


📉 附加篇:大模型如何理解“语义”?向量的底层本质

既然聊到了底层数据,如果你在做 RAG(检索增强生成),一定会接触到 Embedding(向量化)。文本变向量,这玩意儿在底层没有任何神秘感,它就是一个高维浮点数组

调用 Embedding 接口后,底层返回的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
-0.006929283495992422,
-0.005336422007530928,
-0.009327292000000000
// ... (包含 1024 或 1536 个这样的浮点数)
]
}
],
"model": "text-embedding-3-small"
}
  • 本质:一段自然语言文本,在多维空间中被映射成了一个由上千个小数组成的“坐标”。

  • 向量数据库(如 Pinecone)在底层干了什么? 它就是把用户问题的这个坐标拿进去,与数据库里几万个坐标,两两计算余弦相似度 (Cosine Similarity)

similarity=cos(θ)=∥A∥∥B∥A⋅B​

底层就是疯狂地做这几千个浮点数的点乘运算,找出这个值最接近 1(即在多维空间中夹角最小,语义最相近)的几个数据块,再把它们打包塞进 Messages 的 system 角色中去。


总结

一个优雅的 AI 对话界面的背后,是严谨的 Messages 状态管理、复杂的 SSE 字节流传输,以及 Vercel 对前端开发体验的极致封装。理解了这些协议的流转过程,你不仅能在排查 AI 断流报错时游刃有余,更能在构建复杂的 Agentic RAG(智能体检索)时,从容设计你的底层数据结构。

源码已托管至🫱 GitHub,期待你的 Star🌟。


你与AI对话: 从 Messages 协议到 SSE 流式渲染
http://example.com/2026/05/02/你与AI对话-从-Messages-协议到-SSE-流式渲染/
作者
Lingkai Shi
发布于
2026年5月2日
许可协议