你与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 | |
解析底层细节:
每一帧数据都严格以
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 | |
这套协议使用了前缀魔数 (Magic Numbers):
0:代表这是普通的文本数据 (Text chunk)。3:代表有内部报错 (Error)。9:代表大模型触发了 Tool Call(工具调用)。
此时,前端的 useChat 钩子本质上就是一个状态机。它监听到 0: 开头的数据,就把冒号后面的字符串剥离出来,自动拼接到当前 UI 消息气泡的状态里。这就是你屏幕上看到的如丝般顺滑的打字机效果。
📉 附加篇:大模型如何理解“语义”?向量的底层本质
既然聊到了底层数据,如果你在做 RAG(检索增强生成),一定会接触到 Embedding(向量化)。文本变向量,这玩意儿在底层没有任何神秘感,它就是一个高维浮点数组。
调用 Embedding 接口后,底层返回的数据结构如下:
1 | |
本质:一段自然语言文本,在多维空间中被映射成了一个由上千个小数组成的“坐标”。
向量数据库(如 Pinecone)在底层干了什么? 它就是把用户问题的这个坐标拿进去,与数据库里几万个坐标,两两计算余弦相似度 (Cosine Similarity):
similarity=cos(θ)=∥A∥∥B∥A⋅B
底层就是疯狂地做这几千个浮点数的点乘运算,找出这个值最接近 1(即在多维空间中夹角最小,语义最相近)的几个数据块,再把它们打包塞进 Messages 的 system 角色中去。
总结
一个优雅的 AI 对话界面的背后,是严谨的 Messages 状态管理、复杂的 SSE 字节流传输,以及 Vercel 对前端开发体验的极致封装。理解了这些协议的流转过程,你不仅能在排查 AI 断流报错时游刃有余,更能在构建复杂的 Agentic RAG(智能体检索)时,从容设计你的底层数据结构。
源码已托管至🫱 GitHub,期待你的 Star🌟。