LangGraph入门——多条件分支的实现

前言

前面两篇已经把 LangChain Agent 和 RAG 的主线跑通了:

  • 最小 Agent:模型、工具、多轮消息、OpenAI 兼容模型接入
  • RAG:文档切分、向量检索、Query Rewrite、Rerank、直接 Prompt 回答

这一篇继续往下走,重点放在 LangGraph:如何把对话、状态、分支、记忆和工具调用组织成一条清晰的 Agent 执行链路。

项目地址:

https://github.com/HJunLong601/mylangchain

本文示例目录是:

langgraph_agent_workspace/

这个目录提供了一个统一的聊天入口:

POST /api/agent/chat

用户只负责发消息,后端 Agent Graph 负责完成状态流转、意图识别、条件路由、短期记忆和工具调用。


为什么需要 LangGraph

最开始写 Agent 时,直接用 LangChain 的 Agent 封装就够了。

比如:

  • 用户输入问题
  • 模型判断是否调用工具
  • 工具返回结果
  • 模型生成最终回答

这条链路很适合入门。

但当流程开始变复杂,比如加入下面这些能力时,单纯靠一段顺序代码就会越来越乱:

  • 多轮对话记忆
  • 根据意图走不同分支
  • 工具调用前后记录日志
  • RAG 检索为空时走兜底
  • 某些节点失败后重试
  • 前端展示执行链路和 State
  • 后续支持人审、暂停、恢复

这时候 LangGraph 的价值就出来了。

它不是替代 LangChain,而是把大模型应用里的“执行流程”显式表达成一张图:

State:流程共享的数据
Node:每一步做什么
Edge:下一步去哪
Conditional Edge:根据 State 决定走哪条路
Checkpointer:把执行状态保存下来

简单说:

LangChain 更像组件库,LangGraph 更像工作流编排层。


LangChain 能不能做多条件

可以。

如果只是简单分支,LangChain 里直接用普通代码就能处理:

if (intent === "rag") {
  return runRagChain(input);
}

if (intent === "tool") {
  return runToolChain(input);
}

return runChatChain(input);

这类流程适合用 LangChain:

输入 -> 判断类型 -> 执行某条 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 兼容模型封装

目录结构大致如下:

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

运行方式:

cd E:\AIProject\mylangchain\langgraph_agent_workspace
npm install
npm run dev

启动后访问:

http://localhost:5174

Agent 的调用流程

先看整体流程。

chat

save_note

search_notes

用户在前端输入消息

POST /api/agent/chat

agentGraph.invoke

prepareTurn
预处理本轮输入

classifyIntent
判断用户意图

routeByIntent
条件路由

chatAnswer
普通对话回复

saveNote
保存学习笔记

searchNotes
查询学习笔记

返回 answer + debugState

前端展示回复、State、Steps、Debug JSON

用户侧看到的是一个普通聊天入口:

用户直接和 Agent 对话
-> Agent 内部自己做 State、路由、记忆和工具调用

用户不需要知道背后有几个节点,也不需要关心哪个节点先执行。调试面板会把内部执行过程展示出来,方便开发阶段观察。


LangGraph 里的几个核心概念

1. State

State 是整张图运行过程中的共享上下文。

示例项目里用 Annotation.Root 定义 State:

const AgentState = Annotation.Root({
  question: Annotation<string>,
  normalizedQuestion: Annotation<string>,
  intent: Annotation<AgentIntent>,
  routeReason: Annotation<string>,
  toolResult: Annotation<string>,
  answer: Annotation<string>,
});

可以把它理解成:

这张图执行时允许保存哪些字段

每个节点都可以读取完整 State,但返回时只需要返回自己负责更新的字段。

比如 classifyIntent 节点只负责写入:

return {
  intent,
  routeReason,
  steps: [`classifyIntent: intent=${intent}`],
};

LangGraph 会把这个局部更新合并回完整 State。

2. Node

Node 是图里的一个处理步骤。

它本质上就是一个函数:

function classifyIntentNode(state: AgentStateType) {
  // 读取 state
  // 返回局部更新
}

示例项目里有几个核心节点:

节点 作用
prepareTurn 清理用户输入,重置本轮临时状态
classifyIntent 判断用户意图
chatAnswer 普通对话回复
saveNote 保存学习笔记
searchNotes 查询学习笔记

3. Edge

Edge 决定节点之间的固定执行顺序。

例如:

.addEdge(START, "prepareTurn")
.addEdge("prepareTurn", "classifyIntent")

意思是:

START -> prepareTurn -> classifyIntent

STARTEND 是 LangGraph 内置的虚拟起点和终点,不是业务函数。

4. Conditional Edge

真实 Agent 不可能永远固定路线。

比如用户可能是:

  • 普通聊天
  • 想保存笔记
  • 想查询之前的笔记

这时候就要用条件边:

.addConditionalEdges("classifyIntent", routeByIntent, {
  save_note: "saveNote",
  search_notes: "searchNotes",
  chat: "chatAnswer",
})

执行逻辑是:

classifyIntent 执行完
-> 调用 routeByIntent(state)
-> 返回 chat / save_note / search_notes
-> 进入对应节点

这就是 Agent 路由的雏形。

5. Checkpointer

Checkpointer 负责保存图的执行状态。

示例项目使用的是:

const checkpointer = new MemorySaver();

然后在 compile 时传进去:

.compile({
  checkpointer,
});

MemorySaver 是内存版 checkpointer。它会根据 thread_id 保存每个会话的 State。

注意它是短期记忆:

服务不重启:记忆还在
服务一重启:记忆丢失

如果要做长期记忆,需要换成数据库型 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 复用记忆

这里要特别注意两点。

第一,AnnotationStateGraphMemorySaver 都不是 TypeScript 自带的,它们来自:

import { Annotation, END, MemorySaver, START, StateGraph } from "@langchain/langgraph";

第二,TypeScript 负责类型提示和编译检查,真正决定图如何执行的是 LangGraph。


reducer 到底是什么

reducer 是初学者很容易卡住的点。

示例项目里有这样一段:

messages: Annotation<AgentMessage[]>({
  reducer: (currentMessages, newMessages) =>
    currentMessages.concat(newMessages),
  default: () => [],
}),

这不是 TypeScript 自带语法,而是 LangGraph 的 State 合并规则。

它的意思是:

旧 messages + 新 messages = 合并后的 messages

比如旧消息是:

[
  { role: "user", content: "你好" }
]

某个节点返回新消息:

[
  { role: "assistant", content: "你好,我是 Agent" }
]

最终会变成:

[
  { role: "user", content: "你好" },
  { role: "assistant", content: "你好,我是 Agent" }
]

如果没有 reducer,后一次返回的 messages 很可能会覆盖之前的历史。

所以 reducer 解决的问题是:

同一个 State 字段被多次更新时,新旧值到底怎么合并

常见写法有两类。

追加历史:

steps: Annotation<string[]>({
  reducer: (oldValue, newValue) => oldValue.concat(newValue),
  default: () => [],
})

覆盖旧值:

retrievedNotes: Annotation<LearningNote[]>({
  reducer: (_oldValue, newValue) => newValue,
  default: () => [],
})

聊天消息和执行日志通常适合追加;检索结果通常适合覆盖,因为我们只关心本轮命中的内容。


短期记忆是怎么实现的

短期记忆不是靠前端自己保存完整历史,也不是每次手动把所有消息拼进 Prompt。

示例实现靠三件事:

前端 threadId
-> 后端 configurable.thread_id
-> LangGraph MemorySaver 保存同一个 thread 的 State

前端生成并保持一个 threadId

function createInitialThreadId() {
  return `web-thread-${Date.now()}`;
}

后端调用 graph 时传入:

const state = await agentGraph.invoke(
  {
    question: message,
    messages: [
      {
        role: "user",
        content: message,
      },
    ],
  },
  {
    configurable: {
      thread_id: threadId,
    },
  },
);

同一个 thread_id 会恢复同一个 State。

第一次请求:

threadId = web-thread-1
messages = [用户:解释 State]

Agent 回复后,MemorySaver 保存:

messages = [用户:解释 State, Agent:回答]

第二次请求还是同一个 threadId

messages = [用户:你还记得我刚才问了什么吗?]

LangGraph 会先恢复旧 State,再把新消息追加进去。这样 Agent 就能看到同一个会话里的历史消息。


Agent 的三个分支

1. 普通对话分支

如果用户没有命中保存或查询笔记的关键词,就走普通对话:

classifyIntent -> chatAnswer

chatAnswer 会调用:

generateAssistantReply(state.question, state.messages)

如果配置了 OpenAI 兼容模型,就调用真实模型;如果没有配置 API Key,就走本地规则回复。

这样做是为了降低入门门槛。即使你暂时没有配置 GLM,也能先看懂 LangGraph 主链路。

2. 保存学习笔记分支

如果用户输入包含:

保存 / 记一下 / 记录 / 沉淀

就走:

classifyIntent -> saveNote

如果简单保存 state.question,会带来一个问题:

用户:将上面回答的内容保存到笔记

如果直接保存本轮输入,笔记里存下来的就是这条命令本身,而不是上面那段回答。

现在保存逻辑拆成了两层。

第一层是模型辅助提取。配置了 OpenAI 兼容模型后,会把保存指令和最近几轮历史对话交给模型,让模型判断最应该保存哪段内容,并返回结构化结果:

{
  title: "LangGraph State 核心概念与用法",
  content: "State 是图在节点间传递的共享上下文...",
  tags: ["langgraph", "state", "reducer"],
  reason: "用户要求保存刚才关于 State 的回答"
}

第二层是规则兜底。如果没有配置模型,或者模型返回的 JSON 解析失败,就回退到规则版:

保存这句话:xxx
-> 保存冒号后的 xxx

保存上面回答 / 刚才内容 / 上一条回答
-> 保存最近一条 assistant 回复

都不满足
-> 兜底保存本轮用户输入

最终仍然调用本地存储:

createLearningNote({
  title: noteContent.title,
  content: noteContent.content,
  kind: "observation",
  tags: noteContent.tags,
});

示例中先用 JSON 文件保存:

server/data/learning-notes.json

生产环境可以替换成 SQLite 或 PostgreSQL。

3. 查询学习笔记分支

如果用户输入包含:

笔记 / 学过 / 知识库 / 之前 / 总结

就走:

classifyIntent -> searchNotes

这里使用轻量关键词检索,并处理两类常见问题。

第一类是泛查询:

笔记有什么内容?
有哪些笔记?
保存了什么?

这类问题没有具体主题词,系统会直接返回最近几条笔记,而不是拿整句话去匹配。

第二类是主题查询:

state 的笔记有什么内容?
reducer 相关笔记有哪些?

这类问题会提取 statereducerlanggraphmemory 等技术关键词,再去匹配标题、正文和标签。

对应入口仍然是:

searchLearningNotes(state.normalizedQuestion)

这层检索可以继续升级成完整 RAG。

升级后的链路可以是:

用户问题
-> Query Rewrite
-> Embedding
-> 向量库召回
-> Rerank
-> 证据拼 Prompt
-> 模型回答

这样 RAG 就会成为 Agent 的知识库分支。


OpenAI 兼容模型怎么接

模型封装在:

server/lib/assistantModel.ts

核心代码是:

const model = new ChatOpenAI({
  apiKey,
  model: modelName,
  temperature: 0.2,
  configuration: baseURL
    ? {
        baseURL,
      }
    : undefined,
});

这里用的是 @langchain/openaiChatOpenAI,但不代表只能调用 OpenAI。

只要模型服务商提供 OpenAI 兼容接口,就可以通过:

OPENAI_API_KEY=你的APIKey
OPENAI_BASE_URL=模型服务商的OpenAI兼容地址
OPENAI_MODEL=glm-5

来接入对应模型。

也就是说,应用代码可以继续使用 OpenAI 风格 SDK,底层服务可以换成智谱 GLM 或其他兼容模型。

如果没有配置 API Key,示例会走本地规则回复:

我现在运行在本地规则模式,还没有调用真实大模型。

这个兜底回复主要用于本地开发,避免模型配置影响主链路调试。


前端调试面板

如果只看最终回答,Agent 很容易变成黑盒。

用户问一句话,系统答一句话,中间发生了什么完全看不到。

前端右侧加了一个调试面板,展示:

  • intent:本轮识别出的意图
  • routeReason:为什么走这个分支
  • toolResult:工具执行结果
  • steps:节点执行顺序
  • debug JSON:更结构化的节点日志

比如保存笔记时,你能看到类似链路:

prepareTurn: 收到用户输入
classifyIntent: intent=save_note
saveNote: 调用学习笔记工具并保存成功

这对学习 LangGraph 很有帮助。

因为你看的不只是“回答是什么”,而是:

Agent 为什么这样回答
它走了哪个分支
有没有调用工具
State 中间发生了什么变化

接入 RAG 后,这个调试面板还可以继续展示:

  • 改写后的 query
  • 命中的 chunk
  • distance / score
  • rerank 分数
  • 最终拼进 Prompt 的证据

实现边界

这个版本已经是一个可运行的 Agent 雏形,但还不是生产级系统。

边界很明确:

  • 意图识别使用关键词规则,不是模型分类
  • 学习笔记查询支持泛查询和技术关键词匹配,但不是向量检索
  • 保存笔记支持模型基于历史对话提取,但依赖模型配置和 JSON 解析稳定性
  • MemorySaver 是进程内存储,服务重启后短期记忆会丢
  • 学习笔记用 JSON 文件保存,不适合多人并发写入
  • 还没有标准 Tool 抽象
  • 还没有真正接入 RAG Graph
  • 还没有 React Flow 节点可视化

这些边界可以随着工程演进逐步替换。

最重要的是先把主链路搭起来:

用户输入
-> Agent Graph
-> State
-> 条件路由
-> 工具/模型
-> 返回结果
-> 前端可观察

只要这条链路清楚,后面替换任何一个模块都会比较自然。


可以继续扩展的方向

可以从下面几个方向继续增强这个 Agent:

1. 抽象 Tool 层

现在 saveNotesearchNotes 还是直接写在节点里。

可以整理成:

server/tools/
  saveLearningNoteTool.ts
  searchLearningNotesTool.ts
  readLocalFileTool.ts

这样 Agent 节点只负责调度工具,具体能力放到工具层。

2. 接入真正 RAG

把前面 Python 版本 RAG 的经验迁移过来:

rewriteQuery
-> retrieve
-> rerank
-> buildPrompt
-> generateAnswer

可以把这条链路作为 search_notesknowledge_answer 分支接入 Agent。

3. 换成持久化记忆

MemorySaver 适合学习。

长期使用时,需要把会话和记忆保存到数据库,比如:

  • SQLite
  • PostgreSQL
  • Redis

4. 接入 React Flow

调试面板目前是文字和 JSON。

可以把 Agent Graph 画成节点图:

prepareTurn -> classifyIntent -> chatAnswer
                              -> saveNote
                              -> searchNotes

运行时高亮当前路径,这样会更直观。


总结

这篇文章最重要的不是多学几个 API,而是理解 LangGraph 如何把 Agent 的执行过程拆成清晰的状态和节点。

示例项目把几个关键能力放进了一条 Agent 执行链路里:

  • State 保存 Agent 执行上下文
  • Node 表达每个处理步骤
  • Edge 表达固定流程
  • Conditional Edge 表达分支选择
  • MemorySaver 实现短期记忆
  • reducer 控制 State 字段如何合并
  • 前端调试面板让执行过程可观察

这就是 LangGraph 真正值得学的地方。

它不是为了把代码写复杂,而是让复杂流程变得可拆、可控、可观察。

接下来可以继续在这条链路上补工具抽象、RAG 分支、持久化记忆和可视化调试。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐