Nodejs也能写Agent - 14.LangGraph篇 - 核心概念入门
Nodejs也能写Agent - 14.LangGraph篇 - 核心概念入门
上一篇我们把 LangChain 的四件套打通了:Runnable、模型与 Prompt、Tools、RAG。零件齐了,你已经能做出「会调工具、能查知识库」的 AI 应用。
但零件是零件。一旦你想让 Agent 自己循环——思考 → 调工具 → 看结果 → 再思考——靠手写 while 去管状态,马上会发现:分支一多就糊成浆糊,想在中间停下来等人审批更是无从下手,会话一断上下文全丢。
这一篇我们正式进入 LangGraph.js:用来编排 Agent 的状态与流程。先把心智模型立住——后面的条件分支、Checkpointer、多 Agent,全是建在这套骨架上的。
老规矩,本文 API 均以官网最新文档核对过(https://docs.langchain.com/oss/javascript/langgraph)。LangGraph 迭代很快,跟敲时如果某个 API 对不上,以你当时的官网版本为准。

先装好包
LangGraph 是独立包,跟 @langchain/core 一起装即可:
npm install @langchain/langgraph @langchain/core
# 本系列继续用本地 Ollama
npm install @langchain/ollama
本系列演示仍用
ChatOllama+qwen2.5:7b。换 OpenAI / Anthropic 只是换模型包,图编排逻辑不用动。
一、为何需要 Graph:Chain 的天花板
LCEL 的 pipe 很好用,但本质是一条偏线性的管道:
输入 → Prompt → Model → Parser → 输出
单次「问完就走」的任务,Chain 完全够用。下面这些场景就会开始难受:
| 场景 | 纯 Chain 的难受点 | Graph 怎么解 |
|---|---|---|
| Tool calling 循环 | 自己写 while 管 tool_calls |
条件边在 agent ↔ tools 之间路由 |
| 条件分支 | 管道中间很难塞干净的 if/else | addConditionalEdges 按状态动态跳转 |
| 多节点协作 | 多个 Chain 之间状态传来传去易乱 | 共享 State,节点只读写各自关心的字段 |
| 人机协作(HITL) | 中间没法「暂停等人」 | interrupt + Checkpointer 暂停/恢复 |
| 跨轮持久化 | 每次 invoke 各自为政 |
thread_id + Checkpointer 记住会话 |
经验法则很简单:单次输入→输出,用 Chain;需要循环、分支、共享状态、持久化,升级到 Graph。
二、三要素:State、Node、Edge
官网把这件事说得很干脆:nodes do the work, edges tell what to do next——节点干活,边决定下一步。整个 Agent 工作流,就靠这三样拼出来。
1. State:大家共用的「黑板」
State 是图里所有节点共享的数据结构。每个节点读当前快照,返回需要更新的字段(Partial Update),由框架按规则合并回全局 State。
用官网现在主推的 StateSchema + Zod 来声明(简单字段默认是 last-value:新值直接覆盖旧值):
import { StateSchema } from "@langchain/langgraph";
import * as z from "zod";
const State = new StateSchema({
text: z.string(),
result: z.string().default(""),
});
// 完整状态类型 / 节点返回的更新类型
type FullState = typeof State.State;
type StateUpdate = typeof State.Update;
记两句话就够用,深度下一篇再掰开:
- 节点应只返回要改的字段,别把整份 state 原样扔回去。
- 没配 reducer 的字段默认覆盖;消息列表这类要「追加」的,用
MessagesValue/ReducedValue(下一章重点)。
网上很多旧教程还会写 Annotation.Root({...})——现在依然能用,但官网 Graph API 的主路径已经是 StateSchema。本系列后面统一跟官网走;你若看到旧笔记里的 Annotation,心里有个数:语义类似,写法不同。
2. Node:干活的函数
Node 就是一个函数:吃进当前 State,干完活,返回 Partial Update。里面可以是纯逻辑,也可以调模型、调工具、调整条 LCEL Chain——图负责调度,节点里随便放本事。
类型上推荐用 GraphNode(或 typeof State.Node),少踩 TS 坑:
import { GraphNode } from "@langchain/langgraph";
import { ChatOllama } from "@langchain/ollama";
import { HumanMessage } from "@langchain/core/messages";
const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const translateNode: GraphNode<typeof State> = async (state) => {
const res = await llm.invoke([
new HumanMessage(`将以下文本翻译为英文:${state.text}`),
]);
return { result: res.content as string }; // 只更新 result
};
3. Edge:下一步去哪
- 固定边
addEdge("a", "b"):A 跑完一定去 B - 条件边
addConditionalEdges(...):根据 State 动态选下一跳(本篇先知道有这东西,展开留给后文)
两个特殊虚拟节点(用常量最干净;字符串 "__start__" / "__end__" 等价):
START:图的唯一入口END:图的唯一出口
三、compile():Builder 不能直接跑
StateGraph 是构建器:你在上面 addNode / addEdge,描述「图画成什么样」。描述完之前,它还不是可执行对象。
必须 .compile(),才会得到能 invoke / stream 的编译图:
const builder = new StateGraph(State)
.addNode("translate", translateNode)
.addEdge(START, "translate")
.addEdge("translate", END);
// await builder.invoke(...) — 构建器不能这么调
const graph = builder.compile(); // 得到可运行图
await graph.invoke({ text: "你好" });
compile() 主要干几件事:
- 结构校验(比如有没有孤立节点、路径是否合理)
- 挂运行时能力——比如后面要讲的 Checkpointer、断点(breakpoint),都是在这里注入的
- 内部按 Pregel 思路做 super-step 调度(本篇不用抠算法,知道「图是按步推进」就行)
铁律:忘了
compile,百分之百报错。看见StateGraph就想invoke的,先拍自己一下。
四、LangGraph 和 LangChain:姐妹,不是父子
这里必须纠正一个很常见的说法——「LangGraph 构建在 LangChain Runnable 之上」。
官网的定位更准确:
| 层 | 干什么 |
|---|---|
| LangGraph | 编排运行时:状态、边、持久化、流式、人机协作 |
| LangChain | 能力与集成:模型、Prompt、Tool、RAG、LCEL |
LangGraph 可以单独用,不强制依赖 LangChain。只不过在真实项目里,节点内部调用 ChatOllama、tool()、RAG chain 太常见了,所以两者经常「捆着卖」。
一句话:LangGraph 管「何时跑哪个节点」,LangChain(以及你自己的代码)管「节点里具体怎么调 AI」。 本系列两个都用,是因为集成爽,不是因为二选一。
五、动手:最小可跑的 StateGraph
两节点线性流程——先翻译,再总结。把「定义 State → 写 Node → 连边 → compile → invoke」这条肌肉记忆练出来:
import {
StateGraph,
StateSchema,
GraphNode,
START,
END,
} from "@langchain/langgraph";
import { ChatOllama } from "@langchain/ollama";
import { HumanMessage } from "@langchain/core/messages";
import * as z from "zod";
// 1. 定义 State(简单字段 = last-value 覆盖)
const State = new StateSchema({
text: z.string(),
result: z.string().default(""),
});
const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
// 2. 定义 Node:只返回要更新的字段
const translateNode: GraphNode<typeof State> = async (state) => {
const res = await llm.invoke([
new HumanMessage(`将以下文本翻译为英文:${state.text}`),
]);
return { result: res.content as string };
};
const summarizeNode: GraphNode<typeof State> = async (state) => {
const res = await llm.invoke([
new HumanMessage(`用一句话总结:${state.result}`),
]);
return { result: res.content as string };
};
// 3. 构图并 compile
const graph = new StateGraph(State)
.addNode("translate", translateNode)
.addNode("summarize", summarizeNode)
.addEdge(START, "translate")
.addEdge("translate", "summarize")
.addEdge("summarize", END)
.compile();
// 4. 执行
const output = await graph.invoke({
text: "LangGraph 让 Agent 工作流可编排。",
});
console.log(output.result);
跑通之后你就已经掌握了 LangGraph 90% 入门姿势:剩下的都是「边更复杂、状态更聪明、要不要持久化」。
六、对比彩蛋:管道插不进的「校验节点」
同一任务,LCEL 管道很难在中间干净地塞一个「校验失败就重试」的逻辑;Graph 只是多插一个节点、多连一条边:
const validateNode: GraphNode<typeof State> = async (state) => {
if (!state.result?.trim()) {
throw new Error("翻译结果为空,需要重试");
}
return {}; // 只检查、不改字段,也完全合法
};
const graphWithValidation = new StateGraph(State)
.addNode("translate", translateNode)
.addNode("validate", validateNode) // Graph 的灵活:中间随时可插
.addNode("summarize", summarizeNode)
.addEdge(START, "translate")
.addEdge("translate", "validate")
.addEdge("validate", "summarize")
.addEdge("summarize", END)
.compile();
真要「校验失败就绕回路重跑」,会用到条件边——那是后文的戏。这里只要你感受到:Graph 把流程拆成可插拔的节点,比一条死管道好扩展得多。
常见坑
- 忘记
.compile():对StateGraph构建器直接invoke,必死。先 compile,再跑。 - Node 返回完整 state:应返回 Partial Update。整份扔回去,碰上带 reducer 的字段时,合并行为会让你怀疑人生。
- 分不清 Builder 和编译后的图:Builder 用来拼结构;只有
compile()的结果才能invoke/stream。 - 每个 Node 里
new ChatOllama(...):教程图省事可以;正式代码请在图外创建一次、节点内复用,少吃初始化成本。 - 漏连
END/ 孤立节点:某节点没有出边,compile时可能警告或直接报错——构图时把路径画闭合。 - 被旧教程的
Annotation.Root带偏:不是不能用,但新项目优先StateSchema;看到 Annotation 知道是旧主推写法即可。
更多推荐



所有评论(0)