实战
要点
- 前面这一整章,其实一直在把 LangGraph 的几块能力拆开讲
- 把 AI 伴侣系统换成 LangGraph 来看,思路会不一样
- AI 伴侣系统一上来最值得做的事,往往不是马上回复,而是先把这个用户该带进来的长期资料读出来
- 伴侣系统并不是每一轮都只是陪聊
- 到了这里,前面的子图和多 Agent 几篇就开始真正有用了
内容
1. 到了这里,问题已经不是某一个节点怎么写
前面这一整章,其实一直在把 LangGraph 的几块能力拆开讲。
先讲状态、节点、边,再讲 reducer、条件路由、checkpointer、interrupt()、子图、多 Agent、Store 和容错。单看每一篇,它们都不难理解。但只要真的要做一个能长期演进的 AI 伴侣系统,问题很快就不再是「某个节点怎么写」了。
真正的问题会变成:
这套系统到底该怎么拆。
如果还用前面最早的那种做法,把提示词、工具、记忆、外部调用、人工介入全堆在一个 Agent 入口里,功能当然能继续往上加,但代码会越来越难收。记忆读写混在一起,工具调用和回复生成混在一起,异常处理也全塞在一层里。写到后面,很多问题不是不能修,而是改一个地方总会牵一大片。
所以这一篇不再单独讲某个 API,而是把前面这一章已经讲过的东西,重新放回一条完整的 AI 伴侣管线里,看看它们各自应该待在哪。
2. 先看没有重构之前,系统会卡在哪
先把问题讲具体一点。
一个最常见的 AI 伴侣入口,早期通常会长这样:
接收用户消息,拼系统提示词,把历史消息塞进去,需要时查记忆,需要时调工具,最后再生成回复。
刚开始这么写没有问题。可一旦需求往前走,下面这些东西就会开始互相缠住:
用户长期偏好要跨会话保留,当前这轮对话的状态还得继续接上,某些问题要交给不同角色处理,有些节点失败以后不能直接整条断掉,某些场景下还要暂停等人工确认。
这时候继续把所有逻辑挂在一个入口上,代码不会立刻坏掉,但结构已经开始吃力了。
3. 重构以后,这条管线更像一张图
把 AI 伴侣系统换成 LangGraph 来看,思路会不一样。
你不再把它当成「一次模型调用」,而是把它当成一条会持续流动的状态管线。用户消息进来以后,不是直接冲向一个万能 Agent,而是先经过一串明确的阶段:
- 读取长期记忆
- 判断当前意图
- 进入合适的角色或子图
- 需要时调工具或检索
- 生成回复
- 写回该留下的长期信息
- 如果中途出问题,再按失败路径处理
Canvas actions52%Exit zen mode
Drawing canvas 一旦这样拆,很多原来缠在一起的问题,就会自然落到不同层里去。
4. 先把状态边界划出来
这条管线里,最先要划清楚的不是节点,而是状态。
一个 AI 伴侣系统里,最容易混在一起的通常有三类东西:
当前线程里的即时对话状态,跨线程的长期资料,以及某一轮工作流临时产生的中间结果。
放到 LangGraph 里,我们可以先把线程里的状态收进一份 StateSchema。
// companion-state.ts
import { StateSchema, MessagesValue, ReducedValue } from '@langchain/langgraph'
import { z } from 'zod'
const appendToolNotes = (current: string[], update: string[]) => {
return [...current, ...update]
}
const State = new StateSchema({
messages: MessagesValue,
intent: z.string().default('chat'),
activeRole: z.string().default('companion'),
retrievedMemories: new ReducedValue(
z.array(z.string()).default([]),
{ reducer: appendToolNotes },
),
toolNotes: new ReducedValue(
z.array(z.string()).default([]),
{ reducer: appendToolNotes },
),
draftReply: z.string().default(''),
lastError: z.string().default(''),
status: z.string().default('idle'),
})这一层先只做一件事:把线程里会随着流程推进而变化的东西放好。
而长期资料,比如用户偏好、沟通风格、常用称呼,不直接塞进这份状态里。它们更适合放在 Store 里,按用户去取。
5. 第一段流程,通常先读长期记忆
AI 伴侣系统一上来最值得做的事,往往不是马上回复,而是先把这个用户该带进来的长期资料读出来。
// load-long-term-memory.ts
import type { GraphNode } from '@langchain/langgraph'
const loadLongTermMemory: GraphNode<typeof State> = async (state, runtime) => {
const userId = runtime.context?.userId
const namespace = ['users', userId ?? 'anonymous', 'profile']
const preferenceItem = await runtime.store?.get(namespace, 'preferences')
const tone = preferenceItem?.value?.tone ?? 'warm'
const language = preferenceItem?.value?.language ?? 'zh-CN'
return {
toolNotes: [
`用户长期偏好:language=${language}`,
`用户长期偏好:tone=${tone}`,
],
status: 'memory-loaded',
}
}这里的关键不是「多读了一次数据」,而是整条图后面都开始有了这个用户的背景信息。
这一步如果还像以前一样夹在提示词拼接里,就会越来越隐蔽。单独拆成节点以后,读长期记忆这件事就清楚了。
6. 第二段流程,判断当前这轮到底该找谁
伴侣系统并不是每一轮都只是陪聊。有时是闲聊,有时是情绪安抚,有时是日程安排,有时是写消息、改文案、查资料。
所以第二步通常不会直接回复,而是先判断当前请求属于哪类问题,再决定让谁接手。
// route-intent.ts
import type { GraphNode } from '@langchain/langgraph'
import { Command } from '@langchain/langgraph'
const routeIntent: GraphNode<typeof State> = (state) => {
const lastMessage = state.messages.at(-1)?.text ?? ''
if (lastMessage.includes('开会') || lastMessage.includes('日程')) {
return new Command({
update: {
intent: 'schedule',
activeRole: 'schedule-agent',
},
goto: 'scheduleSubgraph',
})
}
if (lastMessage.includes('邮件') || lastMessage.includes('文案')) {
return new Command({
update: {
intent: 'writing',
activeRole: 'writing-agent',
},
goto: 'writingSubgraph',
})
}
return new Command({
update: {
intent: 'chat',
activeRole: 'companion',
},
goto: 'companionReply',
})
}这一层其实就是把前面讲过的路由、Command 和多角色拆分重新放回真实业务里。
7. 第三段流程,角色内部各走各的子图
到了这里,前面的子图和多 Agent 几篇就开始真正有用了。
日程问题不需要走写作那套逻辑,写作问题也不需要进入陪聊回复。所以在重构后的系统里,更自然的做法通常是让每类能力有自己的一块子图。
// role-subgraphs.ts
const graph = new StateGraph(State)
.addNode('loadLongTermMemory', loadLongTermMemory)
.addNode('routeIntent', routeIntent)
.addNode('scheduleSubgraph', scheduleSubgraph)
.addNode('writingSubgraph', writingSubgraph)
.addNode('companionReply', companionReply)
.addNode('persistLongTermMemory', persistLongTermMemory)
.addNode('fallbackReply', fallbackReply)
.addEdge(START, 'loadLongTermMemory')
.addEdge('loadLongTermMemory', 'routeIntent')
.addEdge('scheduleSubgraph', 'persistLongTermMemory')
.addEdge('writingSubgraph', 'persistLongTermMemory')
.addEdge('companionReply', 'persistLongTermMemory')
.addEdge('persistLongTermMemory', END)
.compile({
checkpointer,
store,
contextSchema: ContextSchema,
})这里真正的变化,是「系统不再只有一个中央处理函数」了。
不同意图进入不同子图,子图里再各自挂工具、状态和控制流,这样功能加深以后,结构也不容易乱。
这里的 scheduleSubgraph 和 writingSubgraph 可以直接接前面子图那篇的写法。重点不是子图内部细节,而是这条总管线终于不需要自己把所有事情都做完了。
8. 第四段流程,最后再把该留下的东西写回去
伴侣系统和普通问答系统最大的区别之一,就是有些东西值得被长期记住。
比如用户明确表达了新的偏好,或者某个长期资料需要更新。这个时候更稳的做法,不是边回复边顺手写,而是在流程快结束时,留一个专门节点来做这件事。
// persist-long-term-memory.ts
import type { GraphNode } from '@langchain/langgraph'
const persistLongTermMemory: GraphNode<typeof State> = async (state, runtime) => {
const userId = runtime.context?.userId
const namespace = ['users', userId ?? 'anonymous', 'profile']
// 走到这里时,最后一条消息有可能已经是助手回复,
// 所以这里要回头找最近一条用户消息,再决定要不要写回长期记忆
const lastUserMessage = [...state.messages]
.reverse()
.find(message => message.getType() === 'human')
?.text ?? ''
// 这里只演示一种最小判断:用户明确提到语言偏好,就写回 Store
if (lastUserMessage.includes('以后都用中文')) {
await runtime.store?.put(namespace, 'preferences', {
language: 'zh-CN',
tone: 'warm',
})
}
return {
status: 'completed',
}
}把这一步单独留出来以后,长期记忆的读写边界就很清楚了:
开头读进来,中间拿来参与决策,结尾根据需要再写回去。
9. 容错这层,不能等出事了再补
如果只是把意图路由、子图和长期记忆接起来,这条管线已经能跑。
但真正落到业务里,最怕的还是某一层一失败,整条图跟着一起断掉。所以容错最好不是最后临时打补丁,而是从一开始就留在结构里。
// error-fallback.ts
import type { GraphNode } from '@langchain/langgraph'
const fallbackReply: GraphNode<typeof State> = (state) => {
return {
messages: [{
role: 'assistant',
content: `我这次处理时遇到了一点问题:${state.lastError || '未知错误'}。先继续陪你聊,不会影响后面的对话。`,
}],
status: 'fallback',
}
}这类节点看上去很普通,但放在完整系统里就会很重要。因为它意味着:
即使某个角色、某个工具、某个子图出错,系统也还能保住一条对外可用的回复路径。
10. 如果把整条管线串起来,大概就是这个样子
把前面几段合在一起,这条重构后的 AI 伴侣管线,大概可以概括成下面这条顺序:
用户消息先进来,系统先读取长期记忆;读完以后判断当前意图,决定该走陪聊、日程还是写作;进入对应子图以后,各自处理自己的工具和内部流程;快结束时把值得长期保存的资料写回 Store;如果中间有节点失败,再切到备用回复路径。
这时候,前面这一整章讲过的东西,基本都落到了同一条系统里:
StateSchema负责线程状态checkpointer负责把线程继续接上Store负责跨会话长期记忆Command和条件边负责控制流- 子图负责拆业务模块
- 多 Agent 负责角色分工
- 容错节点负责让系统别轻易断掉
11. 总结
走到这里,这一章也差不多收住了。
如果只把 LangGraph 看成一个图编排库,当然也没问题。但前面这一整章一路写下来,其实已经能看出更完整的样子了:它真正适合的,不是单次模型调用,而是那些状态会持续流动、角色会分工、流程会暂停恢复、而且系统还得长期演进的 AI 应用。
所以这一篇最后的重点也不只是「把 AI 伴侣重构了一遍」,而是把前面那些分散的能力重新落回一个系统视角里。写到这里,LangGraph 这一大章就不只是 API 笔记了,而是开始变成一套能落系统的做法。