LangGraph入门------多条件分支的实现
- LangGraph入门------多条件分支的实现
-
- 前言
- [为什么需要 LangGraph](#为什么需要 LangGraph)
- [LangChain 能不能做多条件](#LangChain 能不能做多条件)
- 示例项目做了什么
- [Agent 的调用流程](#Agent 的调用流程)
- [LangGraph 里的几个核心概念](#LangGraph 里的几个核心概念)
-
- [1. State](#1. State)
- [2. Node](#2. Node)
- [3. Edge](#3. Edge)
- [4. Conditional Edge](#4. Conditional Edge)
- [5. Checkpointer](#5. Checkpointer)
- [LangGraph 常用 API 梳理](#LangGraph 常用 API 梳理)
- [reducer 到底是什么](#reducer 到底是什么)
- 短期记忆是怎么实现的
- [Agent 的三个分支](#Agent 的三个分支)
-
- [1. 普通对话分支](#1. 普通对话分支)
- [2. 保存学习笔记分支](#2. 保存学习笔记分支)
- [3. 查询学习笔记分支](#3. 查询学习笔记分支)
- [OpenAI 兼容模型怎么接](#OpenAI 兼容模型怎么接)
- 前端调试面板
- 实现边界
- 可以继续扩展的方向
-
- [1. 抽象 Tool 层](#1. 抽象 Tool 层)
- [2. 接入真正 RAG](#2. 接入真正 RAG)
- [3. 换成持久化记忆](#3. 换成持久化记忆)
- [4. 接入 React Flow](#4. 接入 React Flow)
- 总结
LangGraph入门------多条件分支的实现
前言
前面两篇已经把 LangChain Agent 和 RAG 的主线跑通了:
- 最小 Agent:模型、工具、多轮消息、OpenAI 兼容模型接入
- RAG:文档切分、向量检索、Query Rewrite、Rerank、直接 Prompt 回答
这一篇继续往下走,重点放在 LangGraph:如何把对话、状态、分支、记忆和工具调用组织成一条清晰的 Agent 执行链路。
项目地址:
https://github.com/HJunLong601/mylangchain
本文示例目录是:
text
langgraph_agent_workspace/
这个目录提供了一个统一的聊天入口:
text
POST /api/agent/chat
用户只负责发消息,后端 Agent Graph 负责完成状态流转、意图识别、条件路由、短期记忆和工具调用。
为什么需要 LangGraph
最开始写 Agent 时,直接用 LangChain 的 Agent 封装就够了。
比如:
- 用户输入问题
- 模型判断是否调用工具
- 工具返回结果
- 模型生成最终回答
这条链路很适合入门。
但当流程开始变复杂,比如加入下面这些能力时,单纯靠一段顺序代码就会越来越乱:
- 多轮对话记忆
- 根据意图走不同分支
- 工具调用前后记录日志
- RAG 检索为空时走兜底
- 某些节点失败后重试
- 前端展示执行链路和 State
- 后续支持人审、暂停、恢复
这时候 LangGraph 的价值就出来了。
它不是替代 LangChain,而是把大模型应用里的"执行流程"显式表达成一张图:
text
State:流程共享的数据
Node:每一步做什么
Edge:下一步去哪
Conditional Edge:根据 State 决定走哪条路
Checkpointer:把执行状态保存下来
简单说:
LangChain 更像组件库,LangGraph 更像工作流编排层。
LangChain 能不能做多条件
可以。
如果只是简单分支,LangChain 里直接用普通代码就能处理:
ts
if (intent === "rag") {
return runRagChain(input);
}
if (intent === "tool") {
return runToolChain(input);
}
return runChatChain(input);
这类流程适合用 LangChain:
text
输入 -> 判断类型 -> 执行某条 Chain -> 输出
但如果流程开始出现多轮状态、分支后继续流转、失败重试、工具结果回写、RAG 兜底、调试面板展示执行路径,继续用一堆 if/else 就会很难维护。
这就是 LangGraph 和 LangChain 的区别:
| 对比 | LangChain | LangGraph |
|---|---|---|
| 定位 | 模型、Prompt、Tool、Chain 组件 | 有状态的工作流编排 |
| 多条件 | 可以做,常见是代码判断 | 原生用条件边表达 |
| 状态管理 | 通常自己维护 | State 是核心概念 |
| 短期记忆 | 手动维护 messages 较常见 | 可用 checkpointer 按 thread 保存 |
| 复杂流程 | 能做,但容易散 | 更适合多步骤、多分支、可观察流程 |
一句话总结:
LangChain 能做多条件,LangGraph 更适合管理复杂、有状态、需要持续流转的 Agent 流程。
示例项目做了什么
示例项目实现了一个最小但完整的个人 Agent。
它包含:
- React 前端聊天界面
- Node.js / TypeScript 后端
- LangGraph 统一 Agent Graph
- 本地 JSON 学习笔记存储
- 短期记忆
- 条件路由
- 调试面板
- OpenAI 兼容模型封装
目录结构大致如下:
text
langgraph_agent_workspace/
├─ server/
│ ├─ index.ts # 后端 API 入口
│ ├─ graphs/
│ │ └─ agentGraph.ts # 统一 Agent Graph
│ ├─ lib/
│ │ ├─ assistantModel.ts # 模型调用封装,可接 GLM
│ │ └─ learningStore.ts # 本地学习笔记存储
│ ├─ data/
│ │ └─ learning-notes.json # 学习笔记 JSON 文件
│ └─ types.ts # 后端核心类型
└─ web/
├─ index.html
└─ src/
├─ App.tsx # 聊天界面 + 调试面板
└─ styles.css
运行方式:
powershell
cd E:\AIProject\mylangchain\langgraph_agent_workspace
npm install
npm run dev
启动后访问:
text
http://localhost:5174
Agent 的调用流程
先看整体流程。
#mermaid-svg-FeLALWlq8WWTaLxH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FeLALWlq8WWTaLxH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FeLALWlq8WWTaLxH .error-icon{fill:#552222;}#mermaid-svg-FeLALWlq8WWTaLxH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FeLALWlq8WWTaLxH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FeLALWlq8WWTaLxH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FeLALWlq8WWTaLxH .marker.cross{stroke:#333333;}#mermaid-svg-FeLALWlq8WWTaLxH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FeLALWlq8WWTaLxH p{margin:0;}#mermaid-svg-FeLALWlq8WWTaLxH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FeLALWlq8WWTaLxH .cluster-label text{fill:#333;}#mermaid-svg-FeLALWlq8WWTaLxH .cluster-label span{color:#333;}#mermaid-svg-FeLALWlq8WWTaLxH .cluster-label span p{background-color:transparent;}#mermaid-svg-FeLALWlq8WWTaLxH .label text,#mermaid-svg-FeLALWlq8WWTaLxH span{fill:#333;color:#333;}#mermaid-svg-FeLALWlq8WWTaLxH .node rect,#mermaid-svg-FeLALWlq8WWTaLxH .node circle,#mermaid-svg-FeLALWlq8WWTaLxH .node ellipse,#mermaid-svg-FeLALWlq8WWTaLxH .node polygon,#mermaid-svg-FeLALWlq8WWTaLxH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FeLALWlq8WWTaLxH .rough-node .label text,#mermaid-svg-FeLALWlq8WWTaLxH .node .label text,#mermaid-svg-FeLALWlq8WWTaLxH .image-shape .label,#mermaid-svg-FeLALWlq8WWTaLxH .icon-shape .label{text-anchor:middle;}#mermaid-svg-FeLALWlq8WWTaLxH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FeLALWlq8WWTaLxH .rough-node .label,#mermaid-svg-FeLALWlq8WWTaLxH .node .label,#mermaid-svg-FeLALWlq8WWTaLxH .image-shape .label,#mermaid-svg-FeLALWlq8WWTaLxH .icon-shape .label{text-align:center;}#mermaid-svg-FeLALWlq8WWTaLxH .node.clickable{cursor:pointer;}#mermaid-svg-FeLALWlq8WWTaLxH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FeLALWlq8WWTaLxH .arrowheadPath{fill:#333333;}#mermaid-svg-FeLALWlq8WWTaLxH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FeLALWlq8WWTaLxH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FeLALWlq8WWTaLxH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FeLALWlq8WWTaLxH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FeLALWlq8WWTaLxH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FeLALWlq8WWTaLxH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FeLALWlq8WWTaLxH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FeLALWlq8WWTaLxH .cluster text{fill:#333;}#mermaid-svg-FeLALWlq8WWTaLxH .cluster span{color:#333;}#mermaid-svg-FeLALWlq8WWTaLxH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FeLALWlq8WWTaLxH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FeLALWlq8WWTaLxH rect.text{fill:none;stroke-width:0;}#mermaid-svg-FeLALWlq8WWTaLxH .icon-shape,#mermaid-svg-FeLALWlq8WWTaLxH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FeLALWlq8WWTaLxH .icon-shape p,#mermaid-svg-FeLALWlq8WWTaLxH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FeLALWlq8WWTaLxH .icon-shape .label rect,#mermaid-svg-FeLALWlq8WWTaLxH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FeLALWlq8WWTaLxH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FeLALWlq8WWTaLxH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FeLALWlq8WWTaLxH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} chat
save_note
search_notes
用户在前端输入消息
POST /api/agent/chat
agentGraph.invoke
prepareTurn
预处理本轮输入
classifyIntent
判断用户意图
routeByIntent
条件路由
chatAnswer
普通对话回复
saveNote
保存学习笔记
searchNotes
查询学习笔记
返回 answer + debugState
前端展示回复、State、Steps、Debug JSON
用户侧看到的是一个普通聊天入口:
text
用户直接和 Agent 对话
-> Agent 内部自己做 State、路由、记忆和工具调用
用户不需要知道背后有几个节点,也不需要关心哪个节点先执行。调试面板会把内部执行过程展示出来,方便开发阶段观察。
LangGraph 里的几个核心概念
1. State
State 是整张图运行过程中的共享上下文。
示例项目里用 Annotation.Root 定义 State:
ts
const AgentState = Annotation.Root({
question: Annotation<string>,
normalizedQuestion: Annotation<string>,
intent: Annotation<AgentIntent>,
routeReason: Annotation<string>,
toolResult: Annotation<string>,
answer: Annotation<string>,
});
可以把它理解成:
text
这张图执行时允许保存哪些字段
每个节点都可以读取完整 State,但返回时只需要返回自己负责更新的字段。
比如 classifyIntent 节点只负责写入:
ts
return {
intent,
routeReason,
steps: [`classifyIntent: intent=${intent}`],
};
LangGraph 会把这个局部更新合并回完整 State。
2. Node
Node 是图里的一个处理步骤。
它本质上就是一个函数:
ts
function classifyIntentNode(state: AgentStateType) {
// 读取 state
// 返回局部更新
}
示例项目里有几个核心节点:
| 节点 | 作用 |
|---|---|
prepareTurn |
清理用户输入,重置本轮临时状态 |
classifyIntent |
判断用户意图 |
chatAnswer |
普通对话回复 |
saveNote |
保存学习笔记 |
searchNotes |
查询学习笔记 |
3. Edge
Edge 决定节点之间的固定执行顺序。
例如:
ts
.addEdge(START, "prepareTurn")
.addEdge("prepareTurn", "classifyIntent")
意思是:
text
START -> prepareTurn -> classifyIntent
START 和 END 是 LangGraph 内置的虚拟起点和终点,不是业务函数。
4. Conditional Edge
真实 Agent 不可能永远固定路线。
比如用户可能是:
- 普通聊天
- 想保存笔记
- 想查询之前的笔记
这时候就要用条件边:
ts
.addConditionalEdges("classifyIntent", routeByIntent, {
save_note: "saveNote",
search_notes: "searchNotes",
chat: "chatAnswer",
})
执行逻辑是:
text
classifyIntent 执行完
-> 调用 routeByIntent(state)
-> 返回 chat / save_note / search_notes
-> 进入对应节点
这就是 Agent 路由的雏形。
5. Checkpointer
Checkpointer 负责保存图的执行状态。
示例项目使用的是:
ts
const checkpointer = new MemorySaver();
然后在 compile 时传进去:
ts
.compile({
checkpointer,
});
MemorySaver 是内存版 checkpointer。它会根据 thread_id 保存每个会话的 State。
注意它是短期记忆:
text
服务不重启:记忆还在
服务一重启:记忆丢失
如果要做长期记忆,需要换成数据库型 checkpointer。
LangGraph 常用 API 梳理
下面这些 API 是 LangGraph 入门阶段最常用的。
| API | 作用 | 示例项目中的用法 |
|---|---|---|
Annotation.Root |
定义 State 结构 | 定义 AgentState |
Annotation<T> |
定义某个 State 字段类型 | question: Annotation<string> |
reducer |
定义字段新旧值如何合并 | messages 用 concat 追加 |
default |
定义字段默认值 | steps 默认空数组 |
StateGraph |
创建一张状态图 | new StateGraph(AgentState) |
addNode |
注册节点函数 | addNode("prepareTurn", prepareTurnNode) |
addEdge |
定义固定流转 | START -> prepareTurn |
addConditionalEdges |
定义条件分支 | 根据 intent 路由 |
START |
图的虚拟起点 | addEdge(START, "prepareTurn") |
END |
图的虚拟终点 | addEdge("chatAnswer", END) |
compile |
编译成可执行 graph | 传入 checkpointer |
invoke |
执行 graph | 后端接口里调用 |
MemorySaver |
内存版状态保存器 | 实现短期记忆 |
configurable.thread_id |
指定会话 ID | 同一个 ID 复用记忆 |
这里要特别注意两点。
第一,Annotation、StateGraph、MemorySaver 都不是 TypeScript 自带的,它们来自:
ts
import { Annotation, END, MemorySaver, START, StateGraph } from "@langchain/langgraph";
第二,TypeScript 负责类型提示和编译检查,真正决定图如何执行的是 LangGraph。
reducer 到底是什么
reducer 是初学者很容易卡住的点。
示例项目里有这样一段:
ts
messages: Annotation<AgentMessage[]>({
reducer: (currentMessages, newMessages) =>
currentMessages.concat(newMessages),
default: () => [],
}),
这不是 TypeScript 自带语法,而是 LangGraph 的 State 合并规则。
它的意思是:
text
旧 messages + 新 messages = 合并后的 messages
比如旧消息是:
ts
[
{ role: "user", content: "你好" }
]
某个节点返回新消息:
ts
[
{ role: "assistant", content: "你好,我是 Agent" }
]
最终会变成:
ts
[
{ role: "user", content: "你好" },
{ role: "assistant", content: "你好,我是 Agent" }
]
如果没有 reducer,后一次返回的 messages 很可能会覆盖之前的历史。
所以 reducer 解决的问题是:
text
同一个 State 字段被多次更新时,新旧值到底怎么合并
常见写法有两类。
追加历史:
ts
steps: Annotation<string[]>({
reducer: (oldValue, newValue) => oldValue.concat(newValue),
default: () => [],
})
覆盖旧值:
ts
retrievedNotes: Annotation<LearningNote[]>({
reducer: (_oldValue, newValue) => newValue,
default: () => [],
})
聊天消息和执行日志通常适合追加;检索结果通常适合覆盖,因为我们只关心本轮命中的内容。
短期记忆是怎么实现的
短期记忆不是靠前端自己保存完整历史,也不是每次手动把所有消息拼进 Prompt。
示例实现靠三件事:
text
前端 threadId
-> 后端 configurable.thread_id
-> LangGraph MemorySaver 保存同一个 thread 的 State
前端生成并保持一个 threadId:
ts
function createInitialThreadId() {
return `web-thread-${Date.now()}`;
}
后端调用 graph 时传入:
ts
const state = await agentGraph.invoke(
{
question: message,
messages: [
{
role: "user",
content: message,
},
],
},
{
configurable: {
thread_id: threadId,
},
},
);
同一个 thread_id 会恢复同一个 State。
第一次请求:
text
threadId = web-thread-1
messages = [用户:解释 State]
Agent 回复后,MemorySaver 保存:
text
messages = [用户:解释 State, Agent:回答]
第二次请求还是同一个 threadId:
text
messages = [用户:你还记得我刚才问了什么吗?]
LangGraph 会先恢复旧 State,再把新消息追加进去。这样 Agent 就能看到同一个会话里的历史消息。
Agent 的三个分支
1. 普通对话分支
如果用户没有命中保存或查询笔记的关键词,就走普通对话:
text
classifyIntent -> chatAnswer
chatAnswer 会调用:
ts
generateAssistantReply(state.question, state.messages)
如果配置了 OpenAI 兼容模型,就调用真实模型;如果没有配置 API Key,就走本地规则回复。
这样做是为了降低入门门槛。即使你暂时没有配置 GLM,也能先看懂 LangGraph 主链路。
2. 保存学习笔记分支
如果用户输入包含:
text
保存 / 记一下 / 记录 / 沉淀
就走:
text
classifyIntent -> saveNote
如果简单保存 state.question,会带来一个问题:
text
用户:将上面回答的内容保存到笔记
如果直接保存本轮输入,笔记里存下来的就是这条命令本身,而不是上面那段回答。
现在保存逻辑拆成了两层。
第一层是模型辅助提取。配置了 OpenAI 兼容模型后,会把保存指令和最近几轮历史对话交给模型,让模型判断最应该保存哪段内容,并返回结构化结果:
ts
{
title: "LangGraph State 核心概念与用法",
content: "State 是图在节点间传递的共享上下文...",
tags: ["langgraph", "state", "reducer"],
reason: "用户要求保存刚才关于 State 的回答"
}
第二层是规则兜底。如果没有配置模型,或者模型返回的 JSON 解析失败,就回退到规则版:
text
保存这句话:xxx
-> 保存冒号后的 xxx
保存上面回答 / 刚才内容 / 上一条回答
-> 保存最近一条 assistant 回复
都不满足
-> 兜底保存本轮用户输入
最终仍然调用本地存储:
ts
createLearningNote({
title: noteContent.title,
content: noteContent.content,
kind: "observation",
tags: noteContent.tags,
});
示例中先用 JSON 文件保存:
text
server/data/learning-notes.json
生产环境可以替换成 SQLite 或 PostgreSQL。
3. 查询学习笔记分支
如果用户输入包含:
text
笔记 / 学过 / 知识库 / 之前 / 总结
就走:
text
classifyIntent -> searchNotes
这里使用轻量关键词检索,并处理两类常见问题。
第一类是泛查询:
text
笔记有什么内容?
有哪些笔记?
保存了什么?
这类问题没有具体主题词,系统会直接返回最近几条笔记,而不是拿整句话去匹配。
第二类是主题查询:
text
state 的笔记有什么内容?
reducer 相关笔记有哪些?
这类问题会提取 state、reducer、langgraph、memory 等技术关键词,再去匹配标题、正文和标签。
对应入口仍然是:
ts
searchLearningNotes(state.normalizedQuestion)
这层检索可以继续升级成完整 RAG。
升级后的链路可以是:
text
用户问题
-> Query Rewrite
-> Embedding
-> 向量库召回
-> Rerank
-> 证据拼 Prompt
-> 模型回答
这样 RAG 就会成为 Agent 的知识库分支。
OpenAI 兼容模型怎么接
模型封装在:
text
server/lib/assistantModel.ts
核心代码是:
ts
const model = new ChatOpenAI({
apiKey,
model: modelName,
temperature: 0.2,
configuration: baseURL
? {
baseURL,
}
: undefined,
});
这里用的是 @langchain/openai 的 ChatOpenAI,但不代表只能调用 OpenAI。
只要模型服务商提供 OpenAI 兼容接口,就可以通过:
env
OPENAI_API_KEY=你的APIKey
OPENAI_BASE_URL=模型服务商的OpenAI兼容地址
OPENAI_MODEL=glm-5
来接入对应模型。
也就是说,应用代码可以继续使用 OpenAI 风格 SDK,底层服务可以换成智谱 GLM 或其他兼容模型。
如果没有配置 API Key,示例会走本地规则回复:
text
我现在运行在本地规则模式,还没有调用真实大模型。
这个兜底回复主要用于本地开发,避免模型配置影响主链路调试。
前端调试面板
如果只看最终回答,Agent 很容易变成黑盒。
用户问一句话,系统答一句话,中间发生了什么完全看不到。
前端右侧加了一个调试面板,展示:
intent:本轮识别出的意图routeReason:为什么走这个分支toolResult:工具执行结果steps:节点执行顺序debug JSON:更结构化的节点日志
比如保存笔记时,你能看到类似链路:
text
prepareTurn: 收到用户输入
classifyIntent: intent=save_note
saveNote: 调用学习笔记工具并保存成功
这对学习 LangGraph 很有帮助。
因为你看的不只是"回答是什么",而是:
text
Agent 为什么这样回答
它走了哪个分支
有没有调用工具
State 中间发生了什么变化
接入 RAG 后,这个调试面板还可以继续展示:
- 改写后的 query
- 命中的 chunk
- distance / score
- rerank 分数
- 最终拼进 Prompt 的证据
实现边界
这个版本已经是一个可运行的 Agent 雏形,但还不是生产级系统。
边界很明确:
- 意图识别使用关键词规则,不是模型分类
- 学习笔记查询支持泛查询和技术关键词匹配,但不是向量检索
- 保存笔记支持模型基于历史对话提取,但依赖模型配置和 JSON 解析稳定性
MemorySaver是进程内存储,服务重启后短期记忆会丢- 学习笔记用 JSON 文件保存,不适合多人并发写入
- 还没有标准 Tool 抽象
- 还没有真正接入 RAG Graph
- 还没有 React Flow 节点可视化
这些边界可以随着工程演进逐步替换。
最重要的是先把主链路搭起来:
text
用户输入
-> Agent Graph
-> State
-> 条件路由
-> 工具/模型
-> 返回结果
-> 前端可观察
只要这条链路清楚,后面替换任何一个模块都会比较自然。
可以继续扩展的方向
可以从下面几个方向继续增强这个 Agent:
1. 抽象 Tool 层
现在 saveNote、searchNotes 还是直接写在节点里。
可以整理成:
text
server/tools/
saveLearningNoteTool.ts
searchLearningNotesTool.ts
readLocalFileTool.ts
这样 Agent 节点只负责调度工具,具体能力放到工具层。
2. 接入真正 RAG
把前面 Python 版本 RAG 的经验迁移过来:
text
rewriteQuery
-> retrieve
-> rerank
-> buildPrompt
-> generateAnswer
可以把这条链路作为 search_notes 或 knowledge_answer 分支接入 Agent。
3. 换成持久化记忆
MemorySaver 适合学习。
长期使用时,需要把会话和记忆保存到数据库,比如:
- SQLite
- PostgreSQL
- Redis
4. 接入 React Flow
调试面板目前是文字和 JSON。
可以把 Agent Graph 画成节点图:
text
prepareTurn -> classifyIntent -> chatAnswer
-> saveNote
-> searchNotes
运行时高亮当前路径,这样会更直观。
总结
这篇文章最重要的不是多学几个 API,而是理解 LangGraph 如何把 Agent 的执行过程拆成清晰的状态和节点。
示例项目把几个关键能力放进了一条 Agent 执行链路里:
State保存 Agent 执行上下文Node表达每个处理步骤Edge表达固定流程Conditional Edge表达分支选择MemorySaver实现短期记忆reducer控制 State 字段如何合并- 前端调试面板让执行过程可观察
这就是 LangGraph 真正值得学的地方。
它不是为了把代码写复杂,而是让复杂流程变得可拆、可控、可观察。
接下来可以继续在这条链路上补工具抽象、RAG 分支、持久化记忆和可视化调试。