1. 这一篇的位置
Vercel AI SDK 我们讲过「为什么 AI SDK 和 LangChain 是分层互补而不是替代」,并给出了三种协作模式,C(混合模式)是 AI 伴侣项目的选择。
前面 19 篇把 AI SDK 各层讲透了,LangChain 第 2 章和 LangGraph 第 3 章把后端编排讲透了。这一篇把它们真正组合起来,给出 AI 伴侣主管线的完整代码骨架。
具体回答四个问题:
- LangGraph 的
.stream()事件,怎么翻译成 AI SDK 的 UIMessageStream 让前端useChat消费? - AI SDK 的
tool()的execute里,怎么复用 LangChain 的 Retriever / Memory? - 前端
UIMessage历史,怎么同步成 LangGraph 的 State? - 谁放哪里——哪些逻辑用 LangGraph、哪些用 AI SDK、哪些干脆用 Zod + generateObject?
读完这一篇,你就能在 重构端到端 AI Chat 里直接上手。
2. 明确每一层的职责
先把 AI 伴侣项目的每一层职责列清楚:
01┌─────────────────────────────────────────────┐02│ 前端(Next.js 16 / React 19) │03│ - useChat + UI Parts │04│ - 思考态 / tool part / data part 渲染 │05│ - UI SDK 层 │06├─────────────────────────────────────────────┤07│ HTTP 层(Hono on Cloudflare Workers) │08│ - Route: /chat, /resume, /feedback │09│ - 认证 / 限流 / abort 传递 │10├─────────────────────────────────────────────┤11│ 桥接层(本章重点) │12│ - LangGraph Event ↔ AI SDK UIMessageStream │13│ - UIMessage ↔ LangGraph State │14├─────────────────────────────────────────────┤15│ 管线层(LangGraph StateGraph) │16│ 节点: │17│ ├─ loadContext(D1 加载画像 / 历史) │18│ ├─ emotionClassifier(generateObject) │19│ ├─ memoryRetrieval(LangChain Retriever) │20│ ├─ buildPrompt(prompts 包) │21│ ├─ llm(streamText + tools) │22│ └─ persistState(D1 / Vectorize 异步写回)│23├─────────────────────────────────────────────┤24│ 能力层 │25│ - @ai-sdk/openai / workers-ai-provider │26│ - @langchain/community Retriever / Memory │27│ - Cloudflare D1 / KV / Vectorize │28└─────────────────────────────────────────────┘
桥接层是本章的主角。它把两个生态缝合起来。
3. 桥接一:LangGraph Event → UIMessageStream
LangGraph 的 .stream({ streamMode: 'messages' }) 返回一个异步迭代器,每次 yield 一个 [chunk, metadata] 二元组。chunk 是 AIMessageChunk(LangChain 自己的类型),需要翻译成 AI SDK 的 UIMessageStream 协议。
核心思路是:包装成一个 ReadableStream,每个 chunk 产出对应的 UIMessage part。
01import { createUIMessageStream, createUIMessageStreamResponse } from 'ai'02import type { CompiledGraph } from '@langchain/langgraph'03import type { AIMessageChunk } from '@langchain/core/messages'0405export function createGraphUIResponse(06graph: CompiledGraph,07initialState: CompanionState,08config: { signal?: AbortSignal },09) {10const uiStream = createUIMessageStream({11execute: async ({ writer }) => {12// 1. 跑 LangGraph 的流13for await (const [event, metadata] of graph.stream(initialState, {14streamMode: 'messages',15signal: config.signal,16})) {17// 2. 按 LangGraph 节点分发18const nodeName = metadata.langgraph_node1920if (nodeName === 'emotionClassifier' && 'emotion' in event) {21// 情绪分类节点完成后,发一个 data part 到前端22writer.write({23type: 'data-emotion',24data: { primary: event.emotion, intensity: event.intensity },25})26}2728if (nodeName === 'memoryRetrieval' && 'memories' in event) {29// 记忆检索节点完成后30writer.write({31type: 'data-memories-used',32data: { count: event.memories.length },33})3435// 每条记忆作为 source-document part36for (const m of event.memories) {37writer.write({38type: 'source-document',39sourceId: m.id,40mediaType: 'application/json',41title: m.content.slice(0, 20),42})43}44}4546if (nodeName === 'llm' && isAIMessageChunk(event)) {47// LLM 节点产 text-delta48if (event.content) {49writer.write({50type: 'text-delta',51id: event.id ?? 'txt_0',52delta: typeof event.content === 'string'53? event.content54: event.content.map((c) => (c.type === 'text' ? c.text : '')).join(''),55})56}57}58}59},60})6162return createUIMessageStreamResponse({ stream: uiStream })63}
使用:
01app.post('/chat', async (c) => {02const { messages, sessionId } = await c.req.json()03const graph = createCompanionGraph(c.env)0405const initialState: CompanionState = {06sessionId,07messages: langchainMessagesFromUI(messages), // 见桥接三08}0910return createGraphUIResponse(graph, initialState, {11signal: c.req.raw.signal,12})13})
前端 useChat 消费这个响应,就能看到完整体验:情绪 badge → 记忆 hint → source 徽章 → 文本流式吐出。
4. 桥接二:AI SDK tool 内部用 LangChain
AI SDK 的 tool() 的 execute 是一个 async 函数,里面想用什么都行。LangChain 的各种 Retriever / Memory / VectorStore 封装都挺成熟,直接 import 过来用:
01import { tool } from 'ai'02import { z } from 'zod'03import { CloudflareVectorizeStore } from '@langchain/cloudflare'04import { CloudflareWorkersAIEmbeddings } from '@langchain/cloudflare'0506export function searchMemoryTool(env: Env, userId: string) {07return tool({08description: '从长期记忆库里检索与当前对话相关的回忆',09inputSchema: z.object({10query: z.string().describe('检索的关键词或问题'),11topK: z.number().int().min(1).max(10).default(3),12}),13execute: async ({ query, topK }) => {14// 用 LangChain 的 Embeddings 和 VectorStore15const embeddings = new CloudflareWorkersAIEmbeddings({16binding: env.AI,17modelName: '@cf/baai/bge-m3',18})1920const store = new CloudflareVectorizeStore(embeddings, {21index: env.VECTORIZE,22})2324const retriever = store.asRetriever({25k: topK,26filter: { userId },27})2829const docs = await retriever.invoke(query)3031return docs.map((d) => ({32content: d.pageContent,33metadata: d.metadata,34}))35},36})37}
关键在于:从 AI SDK 的视角看,searchMemoryTool 就是一个普通 tool,LLM 根本不知道里面用了 LangChain。这就是分层的价值。
同样的模式还可以这么组合:
- 用 LangChain 的
ConversationSummaryMemory做历史摘要,包成 AI SDK 的 tool - 用 LangChain 的
MultiQueryRetriever做多路检索,包成 AI SDK 的 tool - 用 LangChain 的
WebBaseLoader+RecursiveCharacterTextSplitter处理 RAG 入库,放在定时任务里用,不走 tool
5. 桥接三:UIMessage ↔ LangChain BaseMessage
两套消息格式转换,两个方向。
5.1 UIMessage → LangChain BaseMessage
01import type { UIMessage } from 'ai'02import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages'03import type { BaseMessage } from '@langchain/core/messages'0405export function langchainMessagesFromUI(uiMessages: UIMessage[]): BaseMessage[] {06const result: BaseMessage[] = []0708for (const m of uiMessages) {09// 把 parts 里的 text 合并成一个字符串10const text = m.parts11.filter((p) => p.type === 'text')12.map((p: any) => p.text)13.join('')1415// 处理 tool part(这里只处理简单文本,复杂情况参考下面)16if (m.role === 'user') {17result.push(new HumanMessage(text))18} else if (m.role === 'assistant') {19// 检查是否有 tool-invocation20const toolParts = m.parts.filter((p) => p.type.startsWith('tool-'))2122if (toolParts.length > 0) {23// 一条 AIMessage 带 tool_calls24result.push(25new AIMessage({26content: text,27tool_calls: toolParts.map((p: any) => ({28id: p.toolCallId,29name: p.type.slice('tool-'.length),30args: p.input,31})),32}),33)34// 再加 ToolMessage 传结果35for (const p of toolParts) {36if ((p as any).state === 'output-available') {37result.push(38new ToolMessage({39tool_call_id: (p as any).toolCallId,40content: JSON.stringify((p as any).output),41}),42)43}44}45} else {46result.push(new AIMessage(text))47}48} else if (m.role === 'system') {49result.push(new SystemMessage(text))50}51}5253return result54}
5.2 LangChain BaseMessage → UIMessage
反向转换用得比较少(前端一般只用 useChat 的消息数据源,不直接读 LangChain Message)。但持久化时可能需要——比如 LangGraph checkpointer 把 State 存进 Postgres,刷新页面时要把它转回 UIMessage 喂给 useChat。
01import type { UIMessage } from 'ai'02import type { BaseMessage } from '@langchain/core/messages'03import { AIMessage, HumanMessage } from '@langchain/core/messages'0405export function uiMessagesFromLangchain(lcMessages: BaseMessage[]): UIMessage[] {06return lcMessages07.filter((m) => !(m.getType() === 'system')) // system 不给前端看08.map((m, i) => ({09id: `msg_${i}`,10role: m.getType() === 'human' ? 'user' : 'assistant',11parts: [12{13type: 'text',14text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),15},16],17}))18}
6. 完整代码骨架:AI 伴侣主管线
把上面的桥接全拼起来,看完整的主管线结构。
6.1 LangGraph StateGraph(管线层)
01import { StateGraph, Annotation } from '@langchain/langgraph'02import type { BaseMessage } from '@langchain/core/messages'03import { generateObject } from 'ai'04import { z } from 'zod'0506const CompanionState = Annotation.Root({07sessionId: Annotation<string>,08messages: Annotation<BaseMessage[]>({ reducer: (a, b) => [...a, ...b] }),09emotion: Annotation<{ primary: string; intensity: number } | undefined>,10memories: Annotation<Memory[]>({ default: () => [] }),11systemPrompt: Annotation<string>,12})1314export function createCompanionGraph(env: Env) {15const workflow = new StateGraph(CompanionState)16// 节点 1:加载上下文17.addNode('loadContext', async (state) => {18const profile = await loadUserProfile(env.DB, state.sessionId)19return { /* 更新 state */ }20})2122// 节点 2:情绪分类(用 AI SDK generateObject)23.addNode('emotionClassifier', async (state) => {24const lastUser = state.messages.at(-1)25const { object } = await generateObject({26model: models.structured,27schema: z.object({28primary: z.enum(['happy', 'sad', 'angry', 'calm', 'neutral']),29intensity: z.number().min(0).max(1),30}),31prompt: `分类这段话的情绪:${lastUser?.content}`,32})33return { emotion: object }34})3536// 节点 3:记忆检索(用 LangChain Retriever)37.addNode('memoryRetrieval', async (state) => {38const query = String(state.messages.at(-1)?.content ?? '')39const retriever = buildLangchainRetriever(env)40const docs = await retriever.invoke(query)41return { memories: docs.map(d => ({ content: d.pageContent, ...d.metadata })) }42})4344// 节点 4:构造 system prompt45.addNode('buildPrompt', async (state) => {46const prompt = buildCompanionPrompt({ /* ... */ })47return { systemPrompt: prompt }48})4950// 节点 5:LLM 生成(这个节点特别,交给 AI SDK 流式处理)51.addNode('llm', async (state) => {52// 这个节点只是占位,真正的 streamText 发生在桥接层53// 节点返回空更新54return {}55})5657.addEdge('__start__', 'loadContext')58.addEdge('loadContext', 'emotionClassifier')59.addEdge('emotionClassifier', 'memoryRetrieval')60.addEdge('memoryRetrieval', 'buildPrompt')61.addEdge('buildPrompt', 'llm')62.addEdge('llm', '__end__')6364return workflow.compile()65}
6.2 桥接层:把 LangGraph + AI SDK 合流
01export async function runCompanionPipeline(02env: Env,03sessionId: string,04uiMessages: UIMessage[],05signal?: AbortSignal,06) {07const graph = createCompanionGraph(env)0809const uiStream = createUIMessageStream({10execute: async ({ writer }) => {11let systemPrompt = ''1213// 跑到 llm 节点前的所有节点14for await (const [event, metadata] of graph.stream(15{16sessionId,17messages: langchainMessagesFromUI(uiMessages),18},19{ streamMode: 'values', signal },20)) {21const node = metadata.langgraph_node2223if (node === 'emotionClassifier' && event.emotion) {24writer.write({25type: 'data-emotion',26data: event.emotion,27})28}2930if (node === 'memoryRetrieval' && event.memories) {31writer.write({32type: 'data-memories-used',33data: { count: event.memories.length },34})35}3637if (node === 'buildPrompt' && event.systemPrompt) {38systemPrompt = event.systemPrompt39}4041if (node === 'llm') {42// 此时 graph 跑到 llm 节点,让 AI SDK 流式接管43const result = streamText({44model: buildCompanionModel(env, sessionId), // 带中间件45system: systemPrompt,46messages: convertToModelMessages(uiMessages),47tools: {48searchMemory: searchMemoryTool(env, sessionId),49updateEmotion: updateEmotionTool(env, sessionId),50},51stopWhen: stepCountIs(5),52abortSignal: signal,5354onFinish: ({ text, usage }) => {55// 异步写回 D1 / Vectorize56writeBackAsync(env, sessionId, text, usage)57},58})5960// 合并 AI SDK 的流到 UIMessageStream61writer.merge(result.toUIMessageStream())62}63}64},65})6667return createUIMessageStreamResponse({ stream: uiStream })68}
6.3 HTTP 入口(Hono)
1import { Hono } from 'hono'23app.post('/chat', async (c) => {4const { messages, sessionId } = await c.req.json()5return runCompanionPipeline(c.env, sessionId, messages, c.req.raw.signal)6})
6.4 前端(useChat)
前端代码不变。就是 聊天 UI 标准实现 的 useChat 标准用法,前端根本不需要知道后端是怎么组合 LangGraph 和 AI SDK 的。
7. 这样做值不值?
协同模式相比「纯 AI SDK」多了一些复杂度:
- 两套消息模型转换
- 桥接层代码(大约 150 行)
- 学习成本(要懂 LangGraph)
换来的收益也不小:
- 管线可视化和可调试:LangGraph Studio / LangSmith 里能看整个图的每一步
- 节点级替换:情绪节点换个实现,其他代码不动
- HITL 就位:LangGraph 的
interrupt天然支持审批断点 - 时间旅行:checkpointer 可以回到任意历史状态重跑
- 多 Agent 扩展:想加个 supervisor、加个 swarm,LangGraph 生态都是现成的
判断要不要用协同模式:
- 管线节点少于 3 个,不需要 HITL 或 checkpointer,纯 AI SDK 够用
- 管线节点超过 3 个,或者需要上面任一特性,就值得用协同模式
AI 伴侣项目管线超过 5 节点,未来要加 HITL(情绪低落时的人工干预),还需要回放 debug(用户投诉某句回复时)。这种情况下协同模式的收益明显大于复杂度。
8. 其他组合场景
除了 AI 伴侣主管线,下面几个场景也适合协同。
纯 LangChain RAG + AI SDK 前端
大型知识库问答产品,后端主要是 LangChain 的 RAG 管线(Document Loader、多路 Retriever + Re-ranker、Memory + 摘要)。前端还是 useChat + UI Parts 享受流式体验。桥接层只需要把最终的 LangChain LLM 调用换成 AI SDK streamText。
多 Agent LangGraph Swarm + AI SDK UI
Agent 协作型产品(写代码、写报告、做研究),LangGraph Swarm 负责 agent 之间的 handoff。前端用 UI Parts 渲染每个 agent 的贡献、handoff 的过程、最终 supervisor 的决策。
AI SDK 主管线 + LangChain 工具箱
最轻量的组合:主管线就是 streamText + tools,但有一两个工具(RAG、摘要)用 LangChain 实现。不需要桥接层,只是在 tool 的 execute 里 import LangChain。AI 伴侣的初期 MVP 其实也可以走这种组合,随着能力复杂再升级到完整协同模式。
9. 小结
- 协同模式 C 在 AI 伴侣项目里是最优解:LangGraph 做管线,AI SDK 做胶水和前端
- 桥接一:LangGraph event → UIMessageStream,用
createUIMessageStream+ 按节点分发 part - 桥接二:AI SDK
tool.execute内部直接 import 用 LangChain 的 Retriever / Memory / VectorStore - 桥接三:UIMessage ↔ LangChain BaseMessage,双向转换函数
- 完整骨架:LangGraph 节点到 llm 前是 pipeline,llm 节点交给 AI SDK 的
streamText流式处理,结果并回 UIMessageStream - 取舍:3 节点以下不用协同模式;超过 3 节点或要 HITL / checkpointer,协同模式值得
下一篇是本章实战——用 AI SDK 重构端到端 AI Chat。对照 实战:端到端 AI Chat 的手写版本,把本章所有内容落成一个可运行的完整 demo。