LangGraph TypeScript 版入门与实践

你有没有遇到过这种情况:用 LangChain 写了个 Agent,一切顺利,直到你的 PM 说"能不能让它先思考,再问用户确认,失败了还能重试?"------然后你盯着那条线性 chain,陷入了沉默。别担心,LangGraph 就是来拯救你的。

为什么需要它?(Why)

LangChain LCEL 的"天花板"

LangChain 的 LCEL(LangChain Expression Language)用来搭线性 pipeline 非常爽:

复制代码
prompt → model → parser → tool → 完事

但现实中的 Agent 往往不是"一条直线"------它需要:

  • 循环:调用工具 → 分析结果 → 再调用工具 → ......直到找到答案
  • 等待:暂停执行,等人类审批,再继续
  • 回溯:某一步失败了,回滚状态重试
  • 并行:同时处理多个子任务,汇总结果

LCEL 遇到这些场景,基本就翻车了。

AgentExecutor 已在倒计时

如果你在用 AgentExecutor,官方已经宣布它将在 2026 年底废弃。现在是时候把手里的项目迁移到 LangGraph 了------越早越省心。

LangGraph 解决了什么

LangGraph 的核心思路是把 Agent 的执行过程建模成一张有向图(Directed Graph):

  • 节点(Node) = 一个处理步骤(调用 LLM、执行工具、做决策......)
  • 边(Edge) = 步骤之间的跳转关系,可以是固定的,也可以是条件分支
  • 状态(State) = 贯穿所有节点的共享数据,自动持久化

这样,循环、分支、等待、并行都变成了图结构的自然表达,而不是代码层面的硬编码逻辑。


它是什么?(What)

LangGraph 的 TypeScript 版本由 @langchain/langgraph npm 包提供(当前版本约 1.2.6)。你只需要理解四个核心概念:

1. State --- 贯穿全局的共享内存

State 是图的"血液",每个节点都可以读取和更新它。用 StateSchema + MessagesValue 定义:

typescript 复制代码
import { StateSchema, MessagesValue, ReducedValue } from "@langchain/langgraph";
import * as z from "zod";

// 定义 State:messages 字段自动追加,不会覆盖
const AgentState = new StateSchema({
  messages: MessagesValue,           // 内置消息列表 reducer,自动 append
  stepCount: new ReducedValue(       // 自定义 reducer:累加步骤数
    z.number().default(0),
    { reducer: (x, y) => x + y }
  ),
});

MessagesValue 是 TypeScript 版中 Python add_messages 的等价物,它内置了消息列表的追加逻辑------节点只需返回新消息,框架自动合并,不会覆盖历史。

2. Node --- 图中的处理单元

Node 就是一个 TypeScript 函数,接收当前 State,返回对 State 的更新(不是全量替换):

typescript 复制代码
import { GraphNode } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

// 类型注解:GraphNode<typeof YourState>
const agentNode: GraphNode<typeof AgentState> = async (state) => {
  // 读取 state
  const lastMessage = state.messages.at(-1);
  
  // 返回部分更新,MessagesValue reducer 会自动 append
  return {
    messages: [new AIMessage("我思考完毕,准备调用工具")],
    stepCount: 1,   // reducer 会把这个 +1 累加进去
  };
};

3. Edge --- 控制执行流程的箭头

边分两种:

typescript 复制代码
import { StateGraph, START, END } from "@langchain/langgraph";
import { ConditionalEdgeRouter } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

// 固定边:总是从 A 到 B
graph.addEdge("agentNode", "toolNode");

// 条件边:根据 State 决定去哪里(实现循环的关键!)
const shouldContinue: ConditionalEdgeRouter<typeof AgentState, "toolNode"> = (state) => {
  const lastMessage = state.messages.at(-1);
  
  // 如果 AI 发起了工具调用,继续循环
  if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls?.length) {
    return "toolNode";
  }
  // 否则结束
  return END;
};

graph.addConditionalEdges("agentNode", shouldContinue, ["toolNode", END]);

注意 :TypeScript 版全程用驼峰命名(addEdgeaddConditionalEdges),Python 版是下划线(add_edge)。别混了。

4. Checkpointer --- 状态的持久化引擎

Checkpointer 负责把每一步的 State 快照存储下来,这是实现"记忆"和"Human-in-the-Loop"的基础:

typescript 复制代码
import { MemorySaver } from "@langchain/langgraph";

// 开发环境用内存版,零配置
const checkpointer = new MemorySaver();

const graph = new StateGraph(AgentState)
  // ...添加节点和边...
  .compile({ checkpointer });  // 编译时传入 checkpointer

怎么用?(How)

最小可运行示例:天气查询 Agent(无需 API Key)

先把环境搭好,下面这个示例用 mock 函数模拟 LLM,不需要任何 API Keynpm install && npm start 即可运行。

package.json

json 复制代码
{
  "name": "langgraph-ts-demo",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  },
  "dependencies": {
    "@langchain/langgraph": "^1.2.6",
    "@langchain/core": "^0.3.0"
  },
  "devDependencies": {
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

src/index.ts

typescript 复制代码
import {
  StateGraph,
  StateSchema,
  MessagesValue,
  GraphNode,
  ConditionalEdgeRouter,
  START,
  END,
} from "@langchain/langgraph";
import {
  HumanMessage,
  AIMessage,
  ToolMessage,
} from "@langchain/core/messages";
import * as z from "zod";

// ── 1. 定义 State ──────────────────────────────────────
const AgentState = new StateSchema({
  messages: MessagesValue,   // 自动 append,不覆盖历史
});

// ── 2. Mock 工具:天气查询(替代真实 API)──────────────
const weatherDatabase: Record<string, string> = {
  北京: "晴天,25°C,微风",
  上海: "多云,22°C,东南风",
  广州: "小雨,28°C,南风",
};

function mockWeatherTool(city: string): string {
  return weatherDatabase[city] ?? `抱歉,暂无 ${city} 的天气数据`;
}

// ── 3. Mock LLM:模拟"决策 → 调用工具 → 总结"的两轮对话 ──
let callCount = 0;
function mockLLM(messages: (HumanMessage | AIMessage | ToolMessage)[]): AIMessage {
  callCount++;

  if (callCount === 1) {
    // 第一轮:LLM 决定调用天气工具
    const userMsg = messages[0].content as string;
    const cityMatch = userMsg.match(/北京|上海|广州/);
    const city = cityMatch ? cityMatch[0] : "北京";

    return new AIMessage({
      content: "",
      tool_calls: [
        {
          id: "call_001",
          name: "getWeather",            // 工具名
          args: { city },
          type: "tool_call",
        },
      ],
    });
  } else {
    // 第二轮:LLM 收到工具结果,生成最终回复
    const toolMsg = messages.at(-1) as ToolMessage;
    return new AIMessage(`根据最新数据:${toolMsg.content}。祝您出行愉快!`);
  }
}

// ── 4. 定义 Agent 节点(调用 LLM)──────────────────────
const agentNode: GraphNode<typeof AgentState> = async (state) => {
  // 用 mock LLM 替代真实 ChatOpenAI
  const response = mockLLM(state.messages as any);
  return { messages: [response] };
};

// ── 5. 定义 Tool 节点(执行工具调用)──────────────────────
const toolNode: GraphNode<typeof AgentState> = async (state) => {
  const lastMessage = state.messages.at(-1);

  // 只处理 AIMessage 且有 tool_calls 的情况
  if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {
    return { messages: [] };
  }

  const results: ToolMessage[] = [];
  for (const toolCall of lastMessage.tool_calls) {
    if (toolCall.name === "getWeather") {
      const result = mockWeatherTool(toolCall.args.city as string);
      results.push(
        new ToolMessage({
          content: result,
          tool_call_id: toolCall.id ?? "call_001",  // 与 AIMessage 中的 id 对应
        })
      );
    }
  }

  return { messages: results };
};

// ── 6. 定义条件边:是否继续调用工具(实现循环)──────────
const shouldContinue: ConditionalEdgeRouter<typeof AgentState, "toolNode"> = (
  state
) => {
  const lastMessage = state.messages.at(-1);

  // 如果最后一条是 AIMessage 且有工具调用 → 去 toolNode
  if (AIMessage.isInstance(lastMessage) && lastMessage.tool_calls?.length) {
    return "toolNode";
  }

  // 否则结束
  return END;
};

// ── 7. 构建并编译图 ────────────────────────────────────
const graph = new StateGraph(AgentState)
  .addNode("agentNode", agentNode)          // 注册节点
  .addNode("toolNode", toolNode)
  .addEdge(START, "agentNode")              // 入口 → agentNode
  .addConditionalEdges("agentNode", shouldContinue, ["toolNode", END])
  .addEdge("toolNode", "agentNode")         // 工具执行完 → 回到 agentNode
  .compile();

// ── 8. 运行并流式输出每步结果 ──────────────────────────
async function main() {
  console.log("🚀 天气查询 Agent 启动\n");

  const inputMessages = [new HumanMessage("帮我查一下北京的天气")];

  // .stream() 可以看到每个节点的输出,调试利器
  for await (const step of await graph.stream({ messages: inputMessages })) {
    const [nodeName, nodeOutput] = Object.entries(step)[0];
    const msgs = (nodeOutput as any).messages as any[];

    console.log(`📍 节点 [${nodeName}] 输出:`);
    for (const msg of msgs) {
      if (AIMessage.isInstance(msg)) {
        if (msg.tool_calls?.length) {
          console.log(`  → AI 决定调用工具: ${JSON.stringify(msg.tool_calls[0].args)}`);
        } else {
          console.log(`  → AI 最终回复: ${msg.content}`);
        }
      } else if (msg instanceof ToolMessage) {
        console.log(`  → 工具返回: ${msg.content}`);
      }
    }
    console.log();
  }
}

main().catch(console.error);

运行效果:

css 复制代码
🚀 天气查询 Agent 启动

📍 节点 [agentNode] 输出:
  → AI 决定调用工具: {"city":"北京"}

📍 节点 [toolNode] 输出:
  → 工具返回: 晴天,25°C,微风

📍 节点 [agentNode] 输出:
  → AI 最终回复: 根据最新数据:晴天,25°C,微风。祝您出行愉快!

运行方式:

bash 复制代码
npm install && npm start

快速上手:接入真实 LLM(ChatOpenAI 版)

替换 mock LLM 为真实的 ChatOpenAI,只需改两处:

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import * as z from "zod";

// 定义真实工具
const getWeather = tool(
  ({ city }: { city: string }) => weatherDatabase[city] ?? "暂无数据",
  {
    name: "getWeather",
    description: "查询指定城市的天气",
    schema: z.object({ city: z.string().describe("城市名称") }),
  }
);

const toolsByName = { getWeather };

// 替换为真实 LLM,绑定工具
const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });
const modelWithTools = model.bindTools([getWeather]);

// agentNode 改为调用真实 LLM
const agentNode: GraphNode<typeof AgentState> = async (state) => {
  const response = await modelWithTools.invoke(state.messages);
  return { messages: [response] };
};

// toolNode 改为真实执行工具
const toolNode: GraphNode<typeof AgentState> = async (state) => {
  const lastMessage = state.messages.at(-1);
  if (!AIMessage.isInstance(lastMessage) || !lastMessage.tool_calls?.length) {
    return { messages: [] };
  }

  const results: ToolMessage[] = [];
  for (const toolCall of lastMessage.tool_calls) {
    const tool = toolsByName[toolCall.name as keyof typeof toolsByName];
    const observation = await tool.invoke(toolCall);
    results.push(observation);
  }
  return { messages: results };
};

核心用法

1. 带记忆的多轮对话(MemorySaver + thread_id)

有了 Checkpointer,同一个 thread_id 下的多轮对话会共享历史消息:

typescript 复制代码
import { MemorySaver } from "@langchain/langgraph";

const checkpointer = new MemorySaver();  // 开发环境用内存版

const graphWithMemory = new StateGraph(AgentState)
  .addNode("agentNode", agentNode)
  .addNode("toolNode", toolNode)
  .addEdge(START, "agentNode")
  .addConditionalEdges("agentNode", shouldContinue, ["toolNode", END])
  .addEdge("toolNode", "agentNode")
  .compile({ checkpointer });  // 关键:传入 checkpointer

// 每次调用都带上同一个 thread_id,Agent 就能"记住"上下文
const config = { configurable: { thread_id: "user-alice-session-1" } };

// 第一轮
await graphWithMemory.invoke(
  { messages: [new HumanMessage("你好,我叫 Alice")] },
  config
);

// 第二轮:Agent 知道你叫 Alice
const result = await graphWithMemory.invoke(
  { messages: [new HumanMessage("我叫什么名字?")] },
  config
);

console.log(result.messages.at(-1)?.content);
// → "你叫 Alice。"

换一个 thread_id,就是全新的对话,互不干扰。

2. Human-in-the-Loop(interrupt + Command)

这是 LangGraph 最强大的特性之一:在 Agent 执行到关键步骤时暂停,等待人类审批:

typescript 复制代码
import { interrupt, Command, MemorySaver } from "@langchain/langgraph";

// 在节点中调用 interrupt() 暂停执行
const reviewNode: GraphNode<typeof AgentState> = async (state) => {
  const pendingAction = "发送邮件给全体员工";

  // 暂停!把 pendingAction 返回给调用方,等待人类决策
  const approved = interrupt({
    question: "是否批准以下操作?",
    action: pendingAction,
  });

  if (approved) {
    return { messages: [new AIMessage(`✅ 已执行:${pendingAction}`)] };
  } else {
    return { messages: [new AIMessage("❌ 操作已取消")] };
  }
};

// 必须有 checkpointer 才能使用 interrupt!
const graphHITL = new StateGraph(AgentState)
  .addNode("reviewNode", reviewNode)
  .addEdge(START, "reviewNode")
  .compile({ checkpointer: new MemorySaver() });

const config = { configurable: { thread_id: "hitl-demo" } };

// 第一次调用:执行到 interrupt() 时暂停
const paused = await graphHITL.invoke(
  { messages: [new HumanMessage("帮我发一封全员邮件")] },
  config
);

console.log(paused.__interrupt__);
// → [{ value: { question: "是否批准...", action: "发送邮件..." }, ... }]

// 人类审批后,用 Command({ resume: true }) 继续执行
const resumed = await graphHITL.invoke(
  new Command({ resume: true }),  // 传入审批结果
  config                           // 同一个 thread_id!
);

console.log(resumed.messages.at(-1)?.content);
// → "✅ 已执行:发送邮件给全体员工"

3. 并行子图(Send API)

需要同时处理多个独立任务时,用 Send API 实现 Map-Reduce:

typescript 复制代码
import { Send, StateSchema, ReducedValue, GraphNode, StateGraph, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 整体 State:收集所有子任务的结果
const PipelineState = new StateSchema({
  cities: z.array(z.string()),                          // 待查询的城市列表
  results: new ReducedValue(                            // 使用 reducer 收集并行结果
    z.array(z.string()).default(() => []),
    { reducer: (x, y) => x.concat(y) }
  ),
  summary: z.string().default(""),
});

// 单个城市查询节点
const queryCityWeather: GraphNode<typeof PipelineState> = async (state) => {
  // state.city 是通过 Send 注入的临时字段
  const city = (state as any).city as string;
  const weather = weatherDatabase[city] ?? "暂无数据";
  return { results: [`${city}: ${weather}`] };
};

// 汇总节点
const summarize: GraphNode<typeof PipelineState> = async (state) => {
  const summary = state.results.join("\n");
  return { summary: `今日天气汇报:\n${summary}` };
};

// 用 Send 动态分发并行任务
const fanOut = (state: typeof PipelineState.State) => {
  // 为每个城市创建一个独立的并行任务
  return state.cities.map((city) => new Send("queryCityWeather", { city }));
};

const parallelGraph = new StateGraph(PipelineState)
  .addNode("queryCityWeather", queryCityWeather)
  .addNode("summarize", summarize)
  .addEdge(START, "fanOut" as any)    // 注意:fanOut 是条件边的路由函数
  .addConditionalEdges(START, fanOut) // 从 START 直接 fan-out
  .addEdge("queryCityWeather", "summarize")
  .addEdge("summarize", END)
  .compile();

// 并行查询三个城市
for await (const step of await parallelGraph.stream({
  cities: ["北京", "上海", "广州"],
  results: [],
  summary: "",
})) {
  console.log(step);
}
// → queryCityWeather: { results: ['北京: 晴天,25°C,微风'] }
// → queryCityWeather: { results: ['上海: 多云,22°C,东南风'] }
// → queryCityWeather: { results: ['广州: 小雨,28°C,南风'] }
// → summarize: { summary: '今日天气汇报:\n北京: ...\n上海: ...\n广州: ...' }

最佳实践

1. 用 MessagesValue 管理消息,节点只返回新消息

typescript 复制代码
// ✅ 正确:返回新消息,reducer 自动 append
const node: GraphNode<typeof State> = (state) => {
  return { messages: [new AIMessage("新消息")] };
};

// ❌ 错误:手动拼接破坏 reducer 语义
const badNode: GraphNode<typeof State> = (state) => {
  return { messages: [...state.messages, new AIMessage("新消息")] };
};

2. 开发用 MemorySaver,生产上持久化后端

MemorySaver 数据存在进程内存中,重启就丢失。生产环境需要接入数据库:

typescript 复制代码
// 开发环境
import { MemorySaver } from "@langchain/langgraph";
const checkpointer = new MemorySaver();

// 生产环境(以 PostgreSQL 为例,需安装 @langchain/langgraph-checkpoint-postgres)
// import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
// const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);

3. 节点语义化命名,条件边明确声明目标节点

typescript 复制代码
// ✅ 清晰:节点名 + 条件边目标列表一目了然
graph.addConditionalEdges("callLlm", shouldContinue, ["executeTool", END]);

// ❌ 模糊:单字母节点名,维护噩梦
graph.addConditionalEdges("a", r, ["b", END]);

4. 用 .stream() 调试,而不是盲猜

.invoke() 只返回最终结果,.stream() 能看到每个节点的输出,排查问题效率高 10 倍:

typescript 复制代码
// 调试时优先用 stream,上线后按需切换 invoke
for await (const step of await graph.stream(input)) {
  const [node, output] = Object.entries(step)[0];
  console.log(`[${node}]`, JSON.stringify(output, null, 2));
}

常见误区

误区一:LangGraph ≠ LangChain 替代品

LangGraph 是 LangChain 生态的补充,不是替代。简单的单次 LLM 调用、RAG 问答,直接用 LCEL 就行,引入 LangGraph 反而增加复杂度。

判断标准 :你的流程需要循环、条件分支、持久化、人工干预中的任意一项吗?需要 → LangGraph;不需要 → LCEL 够用。

误区二:不是所有场景都需要 Agent + LangGraph

很多人一上来就用 LangGraph 包装一切,结果维护成本爆炸。记住这条原则:

"能用 if-else 解决的问题,不要用 Agent。"

客服机器人需要查订单 → 普通 LCEL 链就够了。需要"思考 → 查多个系统 → 协调多部门 → 等审批" → 才值得上 LangGraph。

误区三:interrupt() 必须搭配 Checkpointer

这是新手最常踩的坑。没有 Checkpointer,interrupt() 无法保存状态,调用会直接报错:

typescript 复制代码
// ❌ 忘记传 checkpointer,调用 interrupt() 时炸掉
const badGraph = new StateGraph(State)
  .addNode("review", reviewNode)   // reviewNode 里调用了 interrupt()
  .addEdge(START, "review")
  .compile();  // 没有 checkpointer!

// ✅ 正确:必须传入 checkpointer
const goodGraph = new StateGraph(State)
  .addNode("review", reviewNode)
  .addEdge(START, "review")
  .compile({ checkpointer: new MemorySaver() });  // ← 必须!

同时,恢复执行时必须使用相同的 thread_id,否则找不到保存的状态。

误区四:不要直接修改 State 对象

TypeScript/JavaScript 没有不可变数据的语言级保证,但 LangGraph 依赖 State 的不可变性来做差量更新和时间旅行(time travel)。直接修改 State 会导致不可预期的行为:

typescript 复制代码
// ❌ 危险:直接 push 修改了原始 state 对象
const badNode: GraphNode<typeof State> = (state) => {
  state.messages.push(new AIMessage("直接改原始对象!"));  // 🚫 不要这样
  return {};
};

// ✅ 安全:返回新对象,让框架合并
const goodNode: GraphNode<typeof State> = (state) => {
  return {
    messages: [new AIMessage("返回新消息,框架来合并")],  // ✅
  };
};

总结

一句话定位:LangGraph 是构建有状态、可循环、支持人工干预的 AI Agent 的"基础设施"------当你的 Agent 不再是一条直线,就是它登场的时候。

四步上手路径

  1. 装包npm install @langchain/langgraph @langchain/core
  2. 跑通 Mock 示例:用本文的天气查询 Demo,理解 State → Node → Edge 的数据流
  3. 接入真实 LLM :换上 ChatOpenAI + bindTools,观察 .stream() 输出
  4. 按需叠加能力 :需要记忆 → 加 MemorySaver;需要审批 → 加 interrupt;需要并行 → 用 Send

参考文档

相关推荐
土豆12502 小时前
OpenSpec:让 AI 编码助手从"乱猜"到"照单执行"
人工智能·llm
Thomas.Sir2 小时前
第二章:LlamaIndex 的基本概念
人工智能·python·ai·llama·llamaindex
m0_694845572 小时前
Dify部署教程:从AI原型到生产系统的一站式方案
服务器·人工智能·python·数据分析·开源
LS_learner2 小时前
VS Code 终端默认配置从 PowerShell 改为 CMD
人工智能
小毅&Nora3 小时前
【人工智能】【大模型】大模型“全家桶”到“精兵简政”:企业AI落地的理性进化之路
人工智能·大模型·平安科技
KaneLogger3 小时前
如何把AI方面的先发优势转化为结构优势
人工智能·程序员·架构
冬奇Lab3 小时前
一天一个开源项目(第67篇):OpenClaw-Admin - AI Agent 网关的可视化管理驾驶舱
人工智能·开源·资讯
飞哥数智坊4 小时前
【大纲】TRAE AI 编程入门第四讲——打破编程界限的智能体
人工智能·ai编程·trae
冬奇Lab4 小时前
5种来自谷歌的Agent Skill设计模式:减少Token浪费,精准触发正确行为
人工智能·agent