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

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

STARTEND 是 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 复用记忆

这里要特别注意两点。

第一,AnnotationStateGraphMemorySaver 都不是 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 相关笔记有哪些?

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

对应入口仍然是:

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/openaiChatOpenAI,但不代表只能调用 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 层

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

可以整理成:

text 复制代码
server/tools/
  saveLearningNoteTool.ts
  searchLearningNotesTool.ts
  readLocalFileTool.ts

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

2. 接入真正 RAG

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

text 复制代码
rewriteQuery
-> retrieve
-> rerank
-> buildPrompt
-> generateAnswer

可以把这条链路作为 search_notesknowledge_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 分支、持久化记忆和可视化调试。

相关推荐
Allenliu _Andy3 小时前
2026 最新版|免登录适配国内网络 Claude Code 终端版安装配置教程(Windows)
ide·chatgpt·openai·ai编程·claude code
Python私教3 小时前
给AI代理选大脑:别只盯着『谁最强』,这6个维度才决定上限
agent·ai编程·claude
Holman3 小时前
用 Claude Code 30 分钟建立代码心智模型
人工智能·ai编程
码哥字节3 小时前
我把 Matt Pocock 的 18 个 Skill 全用了一遍,才发现自己一直在瞎用 AI
ai编程·claude·vibecoding
人月神话Lee4 小时前
【图像处理】颜色空间——RGB之外的世界
ios·ai编程·图像识别
财经资讯数据_灵砚智能4 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月6日
人工智能·python·ai·信息可视化·自然语言处理·ai编程·灵砚智能
CodeAI4 小时前
Prompt Engineering 进阶:6 个让输出更稳定的实用技巧
openai·ai编程
老梁agent4 小时前
LangChain4j AiServices 深度解析:声明式 Agent 编程的魔法背后
物联网·ai编程