普通问答只需等待模型返回文本,但当 Agent 开始调用工具、持续更新状态或等待人工确认时,单一响应已难以呈现完整过程。AG-UI 正是面向这类交互而设计的事件协议。理解它与 Vercel AI SDK、LangChain 的分工,有助于为不同前后端架构选择合适的流式渲染方案。
AG-UI(Agent-User Interaction Protocol,智能体-用户交互协议) 他是前端和智能体交互的协议。
AG-UI 跑在 HTTP 上面,但不是 HTTP 的替代品。AG-UI 是"寄生在 HTTP 之上的一套语义方言
前端用http协议调用大模型接口,此时大模型运行需要很长的时间,才能给我结果。AG-UI 管的不是"调用"这个动作,是调用之后那漫长的几分钟里发生的事。
AG-UI 默认的传输方式就是 HTTP SSE(Server-Sent Events),它还能跑在 WebSocket、Webhook 上
综上所述,AG_UI是跑在http上的协议,但它不是"大模型界的 HTTP",它是"Agent 执行过程的直播协议"——借 HTTP 的道,把 Agent 干活的全过程实时转播给前端,还允许观众中途喊停。
如果你的需求只是"前端发句话、拿回大模型回复",那 AG-UI 对你毫无价值,Vercel AI SDK 可以直接搞定。只有当你开始头疼"用户盯着转圈干等""Agent 调了什么工具我不知道""这个危险操作要不要让人确认一下"——这些过程性问题,AG-UI 才登场。
官网地址:ai-sdk.dev/docs/ai-sdk…
Vercel 出的一套 TypeScript 工具箱,让你用同一套代码调遍所有大模型。 装它只要
npm i ai。开源(Apache-2.0),是 TS 圈做 AI 应用事实上的默认选择。
说白了,他就是一个能够调用大模型,实现前后端代码的包。他里面包含以下三个部分:
1. ai 包:写 agent 逻辑
2. @ai-sdk/openai、 @ai-sdk/anthropic 等包:对接不同的大模型,就和 langchain 的 ChatModel 一样
3. @ai-sdk/react、@ai-sdk/vue 等包:对接后端接口,实现页面渲染
他里面有前端所需要的工具,也有后端所需要的的工具,你可以选择它实现前后端关于大模型的代码。

Vercel AI SDK 回答"模型和工具怎么接进我的应用",AG-UI 回答"Agent 的运行过程怎么持续传给前端"。他们两个关注点不一样。

你可能又会问 @langchain 也能在用一套代码调遍所有大模型,那它和 Vercel AI SDK 之间的区别是什么?
| **** | 关心的问题 |
|---|---|
| Vercel AI SDK | "用户看到的界面怎么丝滑?字怎么一个一个蹦出来?" |
| LangChain | "这个 Agent 跑 20 分钟崩了怎么办?中间要人审批怎么办?状态存哪?" |

说白了,Vercel AI SDK关注的更多的是前端的事情,LangChain关注的是后端的事情,他们的侧重点不一样。
他们三个之间的关系如图所示:
[前端 UI] ==== 中间这条线 ==== [后端 Agent] ---- [大模型]
↑ ↑ ↑ ↑
useChat ★问题在这★ LangChain OpenAI API
(Vercel AI SDK)
LangChain 和 Vercel AI SDK 是两个家族的工具箱,各自带一根自家规格的线。你只用一家的东西时,线是配套的;你要把两家的东西接起来、或者以后想随便换,就该买一根标准线——那就是 AG-UI。
当你做为一个架构师,你到底应该怎么选择技术方案?
| 信号 | 为什么这时需要 AG-UI |
|---|---|
| 前端不是 Vercel 生态 | 你要接 React Native、Flutter、Kotlin、Python 客户端、或者自己写的任意 UI —— useChat 用不了,需要一份中立格式 |
| 后端可能换 | 今天是 LangGraph,明年想换 CrewAI。用 AG-UI,前端一行不动(因为这些框架官方都支持 AG-UI) |
| 前后端两拨人 / 两个仓库 | 需要一份白纸黑字的契约,吵架时能拿出来对照 |
| 要 AG-UI 独有的语义 | JSON Patch 状态增量同步、跨 run 的 interrupt/resume(Agent 暂停几小时后等你审批再继续)、子 Agent 归属追踪 |
| 同一个后端要喂多个前端 | Web、CLI、IDE 插件、企业微信…… 后端只发一种格式 |
任何情况下, AGUI 都不是必须的。
因为 LangChain 的流可以转成 AI SDK 格式。 AG-UI 不是必需品,它只是一份"保鲜"。
如果你的后端可能会换,或者后端是 Python 写的、前端是另一拨人在维护 —— 那么 AG-UI 就该在第一天写上,如果后期再补就会很难受。
| 处境 | 结果 |
|---|---|
| 团队是 Python / 有数据科学栈 | LangChain 是主场;Vercel AI SDK 直接出局(它只有 TS) |
| 全栈 TS / Next.js 团队 | Vercel AI SDK 主场;LangChain.js 是次一等选择(功能少于 Python 版) |
| 混合(Python 算 + TS 编排) | 两个都用,但边界必须划清楚 |
TS 全栈、需求中等(最常见,也最推荐起步)
前端 useChat ←── AI SDK 自己的 UI message stream 协议 ──← 后端 streamText
此时,前端用Vercel AI SDK的 useChat ,后端也用 Vercel AI SDK 的 streamText,那中间走的是它自带的私有协议(text-start / text-delta / text-end 那套)。里面没有用 LangChain 和 AG-UI。
这种所述,反转成本最低,如果要添加 AG-UI 和 LangChain 的门都还开着。
一般场景:Python 后端 + Web 前端
Vercel AI SDK useChat ←── (适配器转换,或 AG-UI)──← LangChain create_agent / LangGraph
此时你会发现一个问题:LangChain 返回出来的流,格式和 Vercel 的对不上。 怎么办?
两个选择:
1.适配器转换:把 LangChain 的输出转成 AI SDK 的 UI message stream 格式(有现成适配器,或自己用 LangChain 的 streamEvents 手写一个转换层)。仍然不需要 AG-UI。
综上所述:如果后端 LangGraph 且前端只有 React → 用适配器就够;如果前端会扩展或后端会换(Python换成其他语言) → 上 AG-UI。
企业级多端 + 长任务 Agent
任意前端(Web / CLI / 移动端,各自实现 AG-UI client) ←── AG-UI 事件流 ──← 任意后端(LangGraph(持久化、HITL、LangSmith))
这种场景就是:企业网里面他既有pc端,还有小程序,app等等,其实这个时候你最该引入AG-UI。
当任意前后端的时候,就必须要用AG-UI 事件流,来连接前后端数据。
这是 AG-UI 真正的主场。

之前我们写model是这样的

现在使用createAgent是这样的

createAgent的优点,他会根据tool的description去自主选择到底要使用哪个tool,不再像model一样,先绑定tool,绑定好以后还要用for循环,看看tool_calls里面的需要用哪个tool,然后再比对使用。
明显少了很多代码。
所以以后直接使用createAgent就好了。
nest new agui-backend
cd agui-backend
pnpm install @langchain/core @langchain/openai @nestjs/config zod
pnpm install ai @ai-sdk/langchain
配置 .env
OPENAI_API_KEY=sk-xx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-plus
BOCHA_API_KEY=sk-xx
博查apikey:open.bochaai.com/api-keys

app.module.ts的配置

由于前后端属于2个不同的项目,他们的服务启动后,会有两个不同的端口号,这时候,访问接口就会存在跨域问题,我们在后端项目的main.ts里面配置如下:
enableCors表示容许跨域

nest g resource ai --no-spec
module里面的代码和以前的没有变,就是提取创建model和搜索网页的tool。
import { Module } from '@nestjs/common';
import { AiService } from './ai.service.js';
import { AiController } from './ai.controller.js';
import { ConfigService } from '@nestjs/config';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import z from 'zod';
@Module({
controllers: [AiController],
providers: [
AiService,
{
provide: 'CHAT_MODEL',
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const model = new ChatOpenAI({
modelName: config.get('MODEL_NAME'),
apiKey: config.get('OPENAI_API_KEY'),
configuration: {
baseURL: config.get('OPENAI_BASE_URL'),
},
});
return model;
},
},
{
provide: 'WEB_SEARCH_TOOL',
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const websearchSchema = z.object({
query: z
.string()
.min(1)
.describe('搜索关键词,比如:公司年报,某个事件等等'),
count: z
.number()
.int()
.min(1)
.max(20)
.describe('返回结果数量,默认为10条'),
});
return tool(
async ({ query, count }) => {
const apikey = config.get('BOCHA_API_KEY');
if (!apikey) {
throw new Error('请配置 BOCHA_API_KEY 环境变量');
}
const url = 'https://api.bochaai.com/v1/web-search';
const body = {
query,
freshness: 'noLimit',
summary: true,
count: count ?? 10,
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apikey}`,
},
body: JSON.stringify(body),
});
if (!response.ok) {
const err = await response.text();
return `搜索失败:${err}`;
}
try {
let json = await response.json();
if (json.code != 200 || !json.data) {
return '搜索失败:${json.msg}';
}
const webpage = json.data.webpages?.value ?? [];
if (!webpage.length) {
return `搜索结果为空`;
}
const formatted = webpage
.map(
(page: any, idx: number) => `
引用: ${idx + 1}
标题: ${page.name}
URL: ${page.url}
摘要: ${page.summary}
网站名称: ${page.siteName}
网站图标: ${page.siteIcon}
发布时间: ${page.dateLastCrawled}
`,
)
.join('nn');
return formatted;
} catch (e) {
return `搜索失败:${e}`;
}
},
{
name: 'web_search',
description:
'使用 Bocha Web Search API 搜索互联网网页。输入为搜索关键词(可选 count 指定结果数量),返回包含标题、URL、摘要、网站名称、图标和时间等信息的结果列表。',
schema: websearchSchema,
},
);
},
},
],
})
export class AiModule {}
之前我们用new ChatOpenAI初始化出来的model直接调用stream或者invoke来执行大模型,执行后回来需要调用agent tool就需要用循环语句,一个一个地找,然后在处理。现在有了createAgent,我们只需要将model还有tool传进这个方法,之前做的脏活累活,他都会帮我我们做了。
在service里面将入参 UIMessage 用toBaseMessages转化LangChain可用的message。
大模型执行完以后,将他返回的langchain message用toUIMessageStream处理成UIMessage。
import { Inject, Injectable } from '@nestjs/common';
import { ChatOpenAI } from '@langchain/openai';
import {
AIMessage,
AIMessageChunk,
createAgent,
HumanMessage,
SystemMessage,
ToolMessage,
} from 'langchain';
import { UIMessage } from 'ai';
import { toBaseMessages, toUIMessageStream } from '@ai-sdk/langchain';
@Injectable()
export class AiService {
private readonly agent: ReturnType<typeof createAgent>;
constructor(
@Inject('CHAT_MODEL') model: ChatOpenAI,
@Inject('WEB_SEARCH_TOOL') private readonly webSearchTool: any,
) {
this.agent = createAgent({
model,
tools: [this.webSearchTool],
systemPrompt:
'你是 AI 助手,需要最新信息、事实核查或联网信息时,请使用 web_search 工具搜索后再作答。',
});
}
async stream(messages: UIMessage[]) {
const lcMessages = await toBaseMessages(messages);// 转换 UIMessage 到 LangChain 的 Message[]
const lgStream = await this.agent.stream(
{
messages: lcMessages,
},
{
streamMode: ['messages', 'values'],
recursionLimit: 12,
},
);
return toUIMessageStream(lgStream);// 转换 LangChain 的 AsyncGenerator<AIMessageChunk> 到 UIMessageStream
}
}
这里如果是流式输出,我们之前用的SSE来实现。现在有了VercelAISDK,就直接用post请求就好了。
import {
BadRequestException,
Body,
Controller,
Get,
Post,
Query,
Res,
Sse,
} from '@nestjs/common';
import type { Response } from 'express';
import { AiService } from './ai.service.js';
import { pipeUIMessageStreamToResponse, UIMessage } from 'ai';
@Controller('ai')
export class AiController {
constructor(private readonly aiService: AiService) {}
// 这里用的ai就不要再用SSE了,他里面已经帮我封装好了,直接用这个就好。
@Post('ch@t')
async postChat(
@Body() body: { messages?: UIMessage[] },
@Res({ passthrough: false }) res: Response,
): Promise<void> {
if (!body?.messages || !Array.isArray(body.messages)) {
throw new BadRequestException('Invalid JSON');
}
const stream = await this.aiService.stream(body.messages);
pipeUIMessageStreamToResponse({ response: res, stream }); // 最近修改的代码片段结束处
}
}
老的SSE请求如下:

对比两者代码,你会发现:现在的代码比之前的已经精简很多倍。
curl.exe --% -N -sS -g -X POST http://127.0.0.1:3000/ai/ch@t -H "Content-Type: application/json" -d "{"messages":[{"id":"1","role":"user","parts":[{"type":"text","text":"北京今天的天气"}]}]}"

看到红框标注,就说明接口已经开发成功。
前端就是依靠type不同的类型来处理具体的样式。
用户问大模型问题后,大模型利用vercel AI SDK给前端特定格式的数据。前端拿到数据,根据不同的type解析成具体的样式,比如,我说请给我写一段加法js代码案例,放到代码块里面。

比如:今日金价用表格展示出来。
如果前端不做处理,你的页面是这样的

使用样式处理后,你的ai agent是这样的

综上所述,怎么搜索是后端的事情,具体数据怎么展示还是前端的事情,大模型给我们的数据永远是一堆文本,AGUI和Vercel AI SDK 将文本流转化成一个json对象,前端根据这个对象里面的属性值,来判断具体怎么渲染。
现在有个现成的解析包叫streamdown,他就能将markdown格式的文本按照一定的样式显示在页面上。
Vercel AI SDK 的前端包名是ai,不管你选的是vue,还是react,都可以用@ai-sdk来连接。
react 项目,用@ai-sdk/react来对接。
vue 项目,用@ai-sdk/vue来对接。
具体安装如下:
npx create-vite agui-frontend
cd agui-frontend
pnpm install @ai-sdk/react ai
pnpm install streamdown @streamdown/code @streamdown/mermaid
npm i -D tailwindcss @tailwindcss/vite

streamdown @streamdown/code @streamdown/mermaid 是Vercel 出的一套 AI 流式 Markdown 渲染方案,一个主包 + 两个可选插件。
streamdown —— 主包(必装),把 AI 返回的 Markdown 文本渲染成漂亮 HTML 的 React 组件。直接显示就是一堆带 # 和 * 的纯文本。streamdown 把它渲染成真正的标题、列表、加粗、代码块。
@streamdown/code —— 代码高亮插件(可选)把代码显示在代码块里面。
@streamdown/mermaid —— 图表插件(可选)
Streamdown 强制依赖 Tailwind,所以需要安装tailwindcss @tailwindcss/vite
npm i -D tailwindcss @tailwindcss/vite
安装完毕以后还需要在vite.config.ts配置tailwindcss
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
在src/index.css里面,全局配置CSS样式
/* src/index.css 或 src/globals.css */
@import "tailwindcss";
@source "../node_modules/streamdown/dist/*.js";
@source "../node_modules/@streamdown/code/dist/*.js";
@source "../node_modules/@streamdown/mermaid/dist/*.js";
在使用Streamdown组件的时候,添加import "streamdown/styles.css";

用了vercel AI SDK 我们的流式输出的文本都不用在拼接了,他能直接实现流式打印。

message就是后端返回的那个json,你从message里面就能拿到很多属性值,parts就是一些特殊处理的部分。

根据part.type的类型不同,做不同的处理。

这个就是代码块处理的地方,你把mermaid, code: codePlugin放到plugin里面他会自动处理。

ToolMessagePart进入的地方

接口数据对照:

text-start 代表文本流开始
text-delta 是流式的文本数据
text-end 代表文本流结束 tool-input-start 代表开始接收到 tool 的参数
tool-input-delta 是流式的 tool call 的参数
tool-input-available 代表 tool 的参数接收完
tool-output-available 代表有了 tool 的调用结果,可以从 output 里取
启动前后端代码

创建一个nextjs项目,Vercel AI SDK 前后端都可以用js写。
# 1. 建 Next.js 项目
npx create-next-app@latest my-app --yes
cd my-app
# 2. 装 AI SDK(装完就是 AI 项目了)
npm i ai @ai-sdk/react zod
# 3. 配 key
echo "OPENAI_API_KEY=sk-xxx" > .env.local
# 4. 跑
npm run dev
--yes = TypeScript + Tailwind + ESLint + App Router + Turbopack,全程默认。
生成项目的目录结构如下:
my-app/
├── app/
│ ├── api/ch@t/route.ts ← 后端(就这一个文件)
│ ├── page.tsx ← 前端
│ └── layout.tsx
└── .env.local
app/api/ch@t/route.tsimport { streamText, convertToModelMessages, stepCountIs, tool, type UIMessage } from 'ai'
import { z } from 'zod'
export const maxDuration = 30
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json()
const result = streamText({
model: 'openai/gpt-5.2', // 换模型改这行
instructions: '你是一个有用的助手。',
messages: await convertToModelMessages(messages),
stopWhen: stepCountIs(5), // 多步工具调用上限
tools: {
getWeather: tool({
description: '查询城市天气',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, temp: 26, desc: '晴' }),
}),
},
})
return result.toUIMessageStreamResponse()
}
app/page.tsx'use client'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useState } from 'react'
export default function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/ch@t' }),
})
return (
<div className="mx-auto max-w-2xl p-4">
{messages.map(m => (
<div key={m.id} className="whitespace-pre-wrap py-2">
<b>{m.role === 'user' ? '你' : 'AI'}:</b>
{m.parts.map((p, i) => {
if (p.type === 'text') return <span key={i}>{p.text}</span>
if (p.type === 'tool-getWeather') return <span key={i}>(查天气中…)</span>
return null
})}
</div>
))}
<form onSubmit={e => {
e.preventDefault()
sendMessage({ text: input })
setInput('')
}}>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={status === 'streaming'}
placeholder="说点什么…"
/>
<button type="submit">发送</button>
</form>
</div>
)
}
前后端都用 AI SDK"的好处:前端 useChat 和后端 streamText 说同一种方言,中间零转换层。
前后端整体只用三个地方应用 Vercel AI SDK 就解决了很多脏活累活。
import { streamText, convertToModelMessages, stepCountIs, tool, type UIMessage } from 'ai'
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
Vercel AI SDK 只干一件事:把"模型 → 流 → 前端状态"这条链路上的所有脏活标准化。
你的代码里它只露脸 5 次,但省掉的是协议设计、流式解析、工具循环、状态管理这四块——这才是"前后端都用同一个 SDK"最大的价值:两端说同一种方言,中间零转换层。
@latest为最新版本
npx create-ag-ui-app my-agent-app
npx create-ag-ui-app@latest my-agent-app
运行中优先选择:


npm run dev 一把梭,最省事。适合先跑通看效果。agent-py/ + ui-react/ 两个目录,后端是独立 FastAPI,更接近生产架构,但要配 Python 环境。(后期学完Python,我们可以试试这个,到时候我会把案例补充在后面,现在就用js即可。)上述选项选择完毕以后,他就会自动拉模板代码
ai、@ag-ui/*、@copilotkit/* 等依赖
等待登录,登录后才能继续安装。