

📚 Agent全栈工程师 · 第3/20篇
前情回顾:第1篇 LangChain 三件套搭 RAG 管线 | 第2篇 LangGraph 状态图编排多步 Agent
本篇:把后端的流式与 Tool Loop,呈现在前端用户面前
📰 每日要闻
• Vercel AI SDK 6 正式发布:agents 转稳定、MCP 从 experimental 转正、新增 Realtime API 实验性语音支持,前端工程师手上多了把瑞士军刀(6 月 5 日)
• ChatGPT 将迎史上最大幅度升级:36氪报道,AI 等机器网络请求量首超人类,OpenAI 与美国政府商讨捐赠股权事宜
• 📈 美联储新掌门沃什「首秀」在即:特朗普再施压不应加息,CPI 本周接棒非农,市场降息幻觉被数据生吞,加息风暴预期升温(Investing.com)
• 📊 美 5 月非农就业周五公布:CNBC 称今年偏强的就业开局可能迎来现实检验,是观察利率路径的关键窗口
• 「华超神控」获亿元天使轮融资:经纬创投领投新一代 AI 超声脑机接口平台,AI×医疗赛道继续发热
上周末我把项目里那段写了大半年的 useChat 代码整个推倒重来了。原因很简单——AI SDK 6 在 6 月 5 号正式发布,message-parts 模型直接把 v4 时代靠字符串拼接的渲染逻辑废掉了一半。
这一篇是「Agent 全栈工程师:企业级知识库项目」系列的第 3 篇。前两篇我们用 LangChain 搭好了 RAG 管线,又用 LangGraph 把多步 Agent 工作流跑通了。后端这边骨架算是立住了,但用户看不见。从这一篇开始,我们把视线挪到前端——具体说,就是怎么用 Vercel AI SDK 6 配合 Next.js 15,把后端的流式输出、工具调用、HITL 审批,全都呈现在一个真正能上生产的对话界面里。
我打算先快速过一遍 v4→v5→v6 的协议演进,让你明白为什么旧代码非改不可;然后从 useChat 这个 Hook 切入,把 message-parts、Server Actions + Edge Runtime、Tool Calling、HITL 审批、多模态输入这几块 v6 真正变了的东西讲清楚;最后落到生产 checklist——错误边界、超时重连、Token 预算、可观测性这些工程细节,是我自己在内网项目里摔过坑总结出来的。
一、为什么是 Vercel AI SDK:框架无关才是它的杀手锄
老实说,我最初是拒绝 Vercel AI SDK 的。一个前端服务商做的 AI SDK,听起来就像是为了绑定 Vercel 那套 Edge 生态凑出来的胶水层。这些年唬唬咻咻到上手之后发现,我错了。它这套设计里真正值钱的不是 Edge,是「框架无关」这四个字。
什么意思?我们后端是在前两篇里用 Python + LangChain 写的,跑在公司内网一台 GPU 机器上;前端是 Next.js 15 部署在 CDN 后面。两者中间隔着一道 HTTP 边界。AI SDK 最让我意外的点在于:它不要求你后端也用 TypeScript,不要求你部署在 Vercel,甚至不要求你用 OpenAI。只要你的后端能吝出符合 SSE 格式的 chunk,前端这边的 useChat 就能接上。
💡 内网证据:KM 657176 《前端开发者构建 AI Agent 系统》有个让我印象深刻的数据——同样实现一个带工具调用的聊天 UI,裸用 OpenAI SDK + 手写 SSE 解析大约要 800 行代码;换成 Vercel AI SDK 后只剩 320 行,减少 60% 左右,还不算各种错误边界许业务代码。
系列中的定位:SDK 是「前后端契约」这一层
为了不迷路,我画了个完整架构图,把本篇要讲的东西放进整个全栈里。
用户(浏览器)
↓
Next.js 15 + AI SDK 6 UI 层
本篇 → useChat / Tool Approval / Realtime
↓ HTTP Streaming
Python 服务端(LangGraph Agent)
前2篇 → Chain / Tool / StateGraph
↓
Milvus / Embedding / 后续篇章覆盖
务必看清楚一件事:useChat 只是个 Hook,背后是一套 HTTP 协议。后端不一定要是 Vercel AI SDK 的 streamText——只要你实现了他们定义的 SSE 事件格式,用 Java 写的 Spring Boot 也能接。这件事后面讲 streamText 时会给你看。
二、AI SDK 4→5→6:三个版本的协议演进
这一节你可能会觉得枯燥,但老项目迁移者请仔细看。我们公司内网那个项目上个月从 v4 升 v6,被坑了三天,原因全部藏在「破坏性变更」这几个字里。
版本 | 消息模型 | Tool Calling | HITL |
|---|---|---|---|
v4 | 字符串 + toolInvocations 数组 | 实验性 | 全手工 |
v5 | 过渡 parts 概念(部分场景) | 稳定 + Zod schema | cookbook 示例 |
v6 | message-parts 统一,类型完备 | multi-step + Tool Loop Agent | 原生 API |
重点在第一列。v4 时代的 message 是一个带 content: string 的东西,所以你看到的聊天页面本质上是在渲染一个字符串。他们顺便给了你 toolInvocations 这个辅助数组,让你手动拼接。
v6 直接把 content 变成了 parts: MessagePart[]。每个 part 是一个独立单元,可能是文本、可能是工具调用、可能是推理过程、可能是附件。这个变化的影响在于——你原来那些『message.content.includes(...)』『ReactMarkdown source={message.content}』的写法全部废了。
// v4 旧写法(现在会崩)
{messages.map(m => (
<div key={m.id}>
<ReactMarkdown>
{m.content}
</ReactMarkdown>
{m.toolInvocations?.map(
ti => <ToolCard ti={ti}/>
)}
</div>
))}// v6 新写法
{messages.map(m => (
<div key={m.id}>
{m.parts.map(
(p, i) => {
if (p.type ===
'text')
return <Markdown
key={i}
text={p.text}/>;
if (p.type ===
'tool-call')
return <ToolCall
key={i}
part={p}/>;
return null;
}
)}
</div>
))}看出问题了吗?不是代码量变多了,是你的「消息渲染」工作单元从「一条消息」变为「一条消息里的一个 part」。这是思维转变,不是 API 换名。官方提供了 codemod 帮你迁移,但业务代码里那些「临时拼出一个 tool result 插到 markdown 里」的魔法写法,所有人都得重写。
三、useChat 实战:message-parts 下的对话 UI
说三个小时模型不如看一眼代码。下面是我项目里一个生产可用的 ChatPanel 组件(简化后),用 v6 的 useChat,背后接我们前两篇的 LangGraph Agent。
骨架:消息列表 + 输入框 + 全状态机
// app/chat/ChatPanel.tsx
'use client';import { useChat }
from 'ai/react';export function
ChatPanel() {
const {
messages,
sendMessage,
status,
stop,
error,
reload,
} = useChat({
api: '/api/chat',
// v6 多步 Agent
// 的关键开关
maxSteps: 5,
onError: (e) => {
// 上报到 LangSmith
reportToSentry(e);
},
});return (
<div className="chat">
<MessageList
items={messages}/>
<StatusBar
status={status}
error={error}
onStop={stop}
onReload={reload}/>
<Composer
onSend={sendMessage}
disabled={
status ===
'streaming'
}/>
</div>
);
}有三个细节是我迫于踩过坑才提醒你的。第一是 status,v6 把以前那个调用者质疑的 isLoading 换成了有限状态机:submitted | streaming | ready | error。用 status 继距判断 UI 状态事半功倍,别再同时看 isLoading + error + messages.length 这种查表式代码了。
第二是 maxSteps。这东西默认为 1,意思是调用一次模型就结束。但我们这是 Agent 系统,模型返回一个 tool-call、工具产出 tool-result、模型拿到结果继续生成——这是三步。设为 5 是个经验值,留点冗余,不够拍脑门再加。
第三是 stop() 和 reload()。用户发现模型跳蹬了要能中断,这事你是越早提供越好。不要依赖用户刷页了事。
MessageList:从 parts 渲染不同类型
function
MessageList({ items }) {
return items.map(
(m) => (
<div
key={m.id}
data-role={m.role}
>
{m.parts.map(
(p, i) =>
renderPart(p, i)
)}
</div>
)
);
}function
renderPart(p, i) {
switch (p.type) {
case 'text':
return <Md
key={i}
text={p.text}/>;
case 'tool-call':
return <ToolCall
key={i}
part={p}/>;
case 'reasoning':
return <Think
key={i}
text={p.text}/>;
case 'file':
return <FileCard
key={i}
part={p}/>;
default:
return null;
}
}重点在那个 'reasoning' 分支。上个月报道出来说 OpenAI/Anthropic 都推了思考类模型,v6 原生支持了这种 part。你可以在 UI 上用折叠的「思考过程」胶囊把它裹起来,不动声色地开了个难以身价的交互类别。
这个 switch 还有个隐藏好处:未来 SDK 增加新的 part 类型(比如 audio、image-generation),你的代码不会炸——default 返回 null 就是了。这是老手才会留的后门。
四、服务端:Server Actions + Edge Runtime 最小架构
前端的脸只是门面,里面的发动机在 /api/chat。我们这个项目里这一层是个薄代理——主要责任是接住请求、给 LangGraph 后端打一次 HTTP、然后把 SSE 转换为 AI SDK 能读懂的东西。变压器层。
// app/api/chat/route.ts
import { streamText, tool }
from 'ai';
import { openai }
from '@ai-sdk/openai';
import { z }
from 'zod';export const
runtime = 'edge';export async function
POST(req) {
const { messages } =
await req.json();const result =
streamText({
model: openai(
'gpt-4.1-mini'
),
messages,
maxSteps: 5,
tools: {
kbSearch: tool({
description:
'检索企业知识库',
inputSchema:
z.object({
query: z.string(),
topK: z.number()
.default(
5
),
}),
execute: async ({
query,
topK,
}) => {
return
callLangGraph(
query, topK
);
},
}),
},
});return
result
.toUIMessageStreamResponse();
}三个细节重要到我愿意单独拿出来说。
一是 runtime = 'edge'。你仿佛会问——我后端都部署在内网 GPU 机器上了,这个 Edge 是干嘛的?它不负责推理,只负责转发。Edge Runtime 最大的价值是冷启动几乎为零,而且天生为 HTTP 流优化。Node.js Runtime 也能跑,就是冷启动会让第一个 token 慢个 200ms 以上。
二是 tool({ inputSchema: z.object(...) })。Zod schema 是这些年社区达成的组合拳,TypeScript 类型 + 运行时校验一步到位。模型返回的参数不符合 schema,SDK 会自动重试,不会让你吞个初始 token 的 NaN。
三是 toUIMessageStreamResponse()。这个方法在 v6 被重命名过(v5 叫 toDataStreamResponse),作用是拼出符合 message-parts 协议的 SSE 响应。迁移者应该记在 codemod 扫描清单里。
五、Tool Calling 与 HITL:让 Agent 等一等用户
这是 v6 最让我兴奋的部分。企业知识库系统里总会遇到一些「高风险动作」——比如删除文档、导出敏感表、向外部发邮件。模型不能自作主张干,必须让用户按一下「确认执行」。以前这种需求要自己在 toolInvocations 上加 awaitingApproval 状态、手写 resume 逻辑——一身胶水。
v6 原生支持了。思路是:服务端定义工具时不给 execute 实现,只留 schema。到了前端会接到一个 tool-call part 却没 tool-result,这时取决于用户点 「同意」还是「拒绝」,前端调用 addToolResult() 把结果什进去,Agent 才接着跳下一步。
// 服务端只给 schema
tools: {
deleteDoc: tool({
description:
'删除知识库文档',
inputSchema:
z.object({
docId:
z.string(),
}),
// 注意:不给 execute
}),
}// 前端 ToolCall 组件
function
ToolCall({ part }) {
const { addToolResult } =
useChat();if (
part.toolName ===
'deleteDoc' &&
part.state ===
'input-available'
) {
return (
<ApprovalCard
title={
'要删除文档?'
}
docId={
part.input.docId
}
onApprove={
() => addToolResult(
{
toolCallId:
part.toolCallId,
result: {
ok: true,
},
}
)
}
onReject={
() => addToolResult(
{
toolCallId:
part.toolCallId,
result: {
ok: false,
reason:
'用户拒绝',
},
}
)
}/>
);
}
return
<ToolResultCard
part={part}/>;
}这个设计优雅在哪?它把「是否人工审批」从后端架构决策点变成了「前端是否提供 execute」这么个一行代码可控的开关。业务人员说这个动作不需要审批了,在服务端加回 execute 即可。
💡 内网经验:KM 660327 《Agent 对话式 UI 中的定制化交互》里对比了 MCP Apps、Frontend Tool、Inline Render 三种方案。结论是一样的——HITL 场景最合适用 Frontend Tool,因为只有这种模式能拿到 React 状态、能调用业务表单、能接入现有权限体系。
六、多模态与 Realtime:v6 还带了什么新玩具
文件/图片上传
v6 的 useChat 现在原生接受 attachments。你不需要手写 base64 转换、不需要自己拼 multipart:sendMessage 接受 files: FileList 参数,SDK 自动处理。服务端会以 file part 的形式拿到。
另外一个容易被忽略的点:openai('gpt-4.1-mini') 这种拽字符串的写法背后,是 provider 适配器在起作用。换成 anthropic、google、xai 都是一行代码的事。公司内部要是接了自己的模型网关,实现个 custom provider 类也就 50 行。
Realtime(experimental):语音到语音
新增的 Realtime API 接入了 OpenAI/Google/xAI 的 speech-to-speech 能力,浏览器录音直推模型、模型出音返身。我试了个 demo,延迟低到 600ms 以内,能做出「在说中被打断」的交互。但标 experimental 有标的道理,生产谨慎。我们这个知识库项目预计下个季度才开试点。
七、生产级 Checklist:上线前你一定要走一遍
这些都是我项目上线前两周被 QA 推进生产环境猛推出来的问题清单,各位可以照这个表什骤一骤过。
项 | 问题 | 推荐做法 |
|---|---|---|
错误边界 | model 502 干死 UI | onError 接管 + reload 可见 |
超时重连 | 市务网络中断 | SSE 末尾插 keep-alive ping |
Token 预算 | 思考模型烧钱快 | maxOutputTokens + 会话摘要 |
可观测性 | 问题定位难 | 后端接 LangSmith(下期) |
防重发 | 双点发送 | status 为 streaming 时禁 send |
最后一点话。西经东译上周那期 Cole Medin 聊 Harness Engineering,说顶级 Agent 工程师与一般工程师的差距,不在写 prompt 的手艺,而在能不能给 Agent 搭一根「控制之绳」(harness)。我越品越觉得,前端这一层就是 harness 的关键环节——哪里该马上运行、哪里该等人、哪里该中断。以前这些都要手搭,v6 把它们变成了库提供的原语。
八、下一篇预告:Milvus 向量数据库
前 3 篇是 Agent 架构的三个层:LangChain 是胶水、LangGraph 是编排、AI SDK 是后端与前端的握手。下一篇我们要去动「脑」里面的东西了——从 Milvus 安装到百万级向量检索,会讲透如何选索引类型、IVF 与 HNSW 的取舍、如何调参数让召回与延迟同时走在绿线。
你可以提前报个名:安装 Milvus 2.5、准备 100 万条 Embedding 样本、预设 IVF\_FLAT 与 HNSW 两个索引。下周一见。
📚 Agent全栈工程师:企业级知识库项目系列 · 第3/20篇
从 LangChain 到 Kubernetes,20篇系统掌握AI Agent全栈开发
✅ 第1篇:LangChain入门:Chain/Agent/Tool三件套搭起第一个RAG管线
✅ 第2篇:LangGraph实战:用状态图构建多步Agent工作流
📝 第3篇:Vercel AI SDK前端接入:流式响应与Conversation UI实现(本篇)
⏳ 第4篇:Milvus向量数据库:从安装到百万级向量检索的工程化实践
⏳ 第5篇:文档解析与Chunking策略
⏳ 第6篇:Embedding模型选型
⏳ 第7篇:ElasticSearch全文检索
⏳ 第8-20篇:混合检索、知识图谱、评估、观测、K8s部署……
—— 如果你走到了这里,代码都读了,那请你点个赞。下周一见。 ——