[LangGraph] Command指令

有些时候,可能会出现这么一种场景:

在节点内部,根据计算结果,动态决定要跳到哪个下一节点,而不是提前把所有边写死。

  • 节点:负责做事情
  • 边:决定走哪一条路线

传统方案节点只能做:

return { ...state update... }

而流程走向由 graph 的 .addEdge() 固定决定的。也就是说,在构建图的时候,流程走向就确定下来了,无法动态跳转。

因此,Langgraph 中提供了一个Command的特性,让更新节点和跳转能够合并到一起。

基础语法

// Command是一个类,对外返回一个Command的实例对象
new Command({
  update: ...,  
  goto: ...,  
})

配置对象包含两项:

  • update:更新的状态
  • goto:要跳转的节点

例如:

import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  // 返回一个Command实例对象
  return new Command({
    update: { foo: "bar" },  // 更新状态
    goto: "myOtherNode",     // 要跳转到哪一个节点 
  });
});

在上面的myNode节点中:

  1. 更新当前状态的字段 foo
  2. 执行完后直接跳转到 myOtherNode

允许列表

当一个节点使用Command来做更新跳转时,需要指定一个允许列表(数组),这相当于是一个路径的白名单,列出了所有可能的去向。

例如:

import { Command } from "@langchain/langgraph";

// 1. 节点名称
// 2. 节点对应的回调函数:返回一个Command的实例对象
// 3. 配置对象:ends(允许列表)
graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },  // 更新状态
    goto: "B",     					 // 跳转下一个节点
  });
},{ 
  ends: ["B", "C"] 					 // 允许列表
});

快速上手

接下来我们来看一个快速上手的案例。

流程:

  1. 用户提交订单
  2. A 节点做基础校验
  3. 根据订单类型跳转到 B 或 C → D 做最终汇总
// 使用Command
import { StateGraph, START, END, Command } from "@langchain/langgraph";
import * as z from "zod";

// 1. 状态Schema
const Schema = z.object({
  orderType: z.enum(["digital", "physical"]).optional().describe("订单的类型"),

  basicInfo: z.string().optional().describe("基础校验"),
  digitalInfo: z.string().optional().describe("数码产品校验"),
  physicalInfo: z.string().optional().describe("非数码产品校验"),

  logs: z.array(z.string()).default([]),
});

// 根据Schema生成ts类型
type TState = z.infer<typeof Schema>;

// 2. 构建图
const app = new StateGraph(Schema)
  .addNode(
    "A",
    (state: TState) => {
      console.log("运行A节点的基础校验");

      // 判断下一个跳转节点
      const next = state.orderType === "digital" ? "B" : "C";

      // 关键点:对外返回一个Command
      return new Command({
        // 更新状态
        update: {
          basicInfo: "A节点基础校验已完成",
          logs: [
            ...state.logs,
            `A节点校验已完成,订单类型为${state.orderType},前往${next}`,
          ],
        },
        // 指定跳转节点
        goto: next,
      });
    },
    {
      ends: ["B", "C"],
    }
  )
  .addNode(
    "B",
    (state: TState) => {
      console.log("运行B节点的校验");
      return new Command({
        update: {
          digitalInfo: `B节点已经对数码产品进行了校验`,
          logs: [...state.logs, "数码产品订单校验完毕"],
        },
        goto: "D",
      });
    },
    {
      ends: ["D"],
    }
  )
  .addNode(
    "C",
    (state: TState) => {
      console.log("运行C节点的校验");
      return new Command({
        update: {
          digitalInfo: `C节点已经对非数码产品进行了校验`,
          logs: [...state.logs, "非数码产品订单校验完毕"],
        },
        goto: "D",
      });
    },
    { ends: ["D"] }
  )
  .addNode("D", (state: TState) => {
    console.log("运行D节点");

    const summary =
      state.orderType === "digital"
        ? `D:数码商品流程完成:${state.digitalInfo}`
        : `D:非数码商品流程完成:${state.physicalInfo}`;

    return {
      logs: [...state.logs, summary, "处理完成"],
    };
  })
  .addEdge(START, "A")
  .addEdge("D", END)
  .compile();

// 3. 测试用例
const result = await app.invoke({
  logs: [],
  orderType: "digital",
});

console.log("\n最终输出:");
console.log(JSON.stringify(result, null, 2));

条件分支

🙋:这个Command不就是和前面的条件分支一样的么?

重构快速上手示例

// 使用条件分支
import { StateGraph, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 1. 状态Schema
const Schema = z.object({
  orderType: z.enum(["digital", "physical"]).optional().describe("订单的类型"),

  basicInfo: z.string().optional().describe("基础校验"),
  digitalInfo: z.string().optional().describe("数码产品校验"),
  physicalInfo: z.string().optional().describe("非数码产品校验"),

  logs: z.array(z.string()).default([]),
});

// 根据Schema生成ts类型
type TState = z.infer<typeof Schema>;

// 2. 构建图
const app = new StateGraph(Schema)
  .addNode("A", (state: TState) => {
    console.log("运行A节点的基础校验");

    // 判断下一个跳转节点
    const next = state.orderType === "digital" ? "B" : "C";

    return {
      basicInfo: "A节点基础校验已完成",
      logs: [
        ...state.logs,
        `A节点校验已完成,订单类型为${state.orderType},前往${next}`,
      ],
    };
  })
  .addNode("B", (state: TState) => {
    console.log("运行B节点的校验");

    return {
      digitalInfo: `B节点已经对数码产品进行了校验`,
      logs: [...state.logs, "数码产品订单校验完毕"],
    };
  })
  .addNode("C", (state: TState) => {
    console.log("运行C节点的校验");
    return {
      digitalInfo: `C节点已经对非数码产品进行了校验`,
      logs: [...state.logs, "非数码产品订单校验完毕"],
    };
  })
  .addNode("D", (state: TState) => {
    console.log("运行D节点");

    const summary =
      state.orderType === "digital"
        ? `D:数码商品流程完成:${state.digitalInfo}`
        : `D:非数码商品流程完成:${state.physicalInfo}`;

    return {
      logs: [...state.logs, summary, "处理完成"],
    };
  })
  .addEdge(START, "A")
  .addConditionalEdges("A", (state: TState) => {
    if (state.orderType === "digital") return "B";
    else return "C";
  })
  .addEdge("B", "D")
  .addEdge("C", "D")
  .addEdge("D", END)
  .compile();

// 3. 测试用例
const result = await app.invoke({
  logs: [],
  orderType: "physical",
});

console.log("\n最终输出:");
console.log(JSON.stringify(result, null, 2));

和条件分支区别

相同点

条件分支通过路由函数决定接下来走哪条边,Command 通过 goto 字段决定下一个节点是谁。

二者都能实现类似于:

if (xxx) 去 B;
else 去 C;

的分支逻辑。

不同点

条件边写的路由逻辑是图结构层面的:

graph.addConditionalEdges("A", (state) => {
  if (state.foo === "bar") return "B";
  else return "C";
});

流程像是:

A执行完 → 图外:下一步去哪? → 路由函数返回结果 → 图外按照流程走

这里 A 的函数自己不知道下一个是谁,是“外部控制器”决定的。

而 Command 的逻辑是节点自己就决定跳哪:

graph.addNode("A", (state) => {
  if (state.foo === "bar") {
    return new Command({
      update: { foo: "baz" },
      goto: "B",
    });
  }
});

流程像是:

A 执行时 → 自己计算出结果 + 自己决定去哪 → 直接返回 Command

A 自己完成了“计算 + 更新 + 跳转”三件事,图外不再需要再问一次“去哪”。

总结:

  1. Command:在节点里面计算,计算完成后进行跳转
  2. 条件分支:在节点外编排跳转逻辑

两者的具体区别如下表:

特性 条件分支 Command
定义位置 图结构层(Graph 设计阶段) 节点函数内部(运行阶段)
逻辑归属 结构控制(流程图层) 业务逻辑(节点层)
能否更新 state 不能更新,只决定方向 可以更新并跳转
调用时机 节点执行完之后(外部控制流) 节点执行过程中(内部决定)
使用场景 固定的流程路由、if/else 分支 动态决策、代理交接、HITL 恢复等

应用场景

由于执行机制的不同,Command常用于以下两类场景:

  1. 跨子图跳转
  2. 人工介入、人工干预、中断

跨子图跳转

由于条件边的跳转范围仅限于当前图内,无法直接跨出子图跳到父图或兄弟子图。比如:

父图:
  ├── 子图A(负责分析)
  ├── 子图B(负责生成)

如果子图A分析完想直接跳到父图中的子图B,这时条件边就做不到,因为它只识别当前子图的节点。

而通过Command就能够很好的解决这个问题:

return new Command({
  update: { result: "analysis done" },
  goto: "SubgraphB",
  graph: Command.PARENT, // 指定跳转到父图
});

人工介入

在 LangGraph 的执行流程中,有些节点可能需要等待人工输入或外部事件的反馈才能继续执行,比如:

  • 等待用户确认某个决策;
  • 等待客服或审核人员填写信息;
  • 等待外部系统的异步回调。

这类场景下,普通的节点返回值或条件边无法暂停图的运行,而Command可以做到这一点。

例如一个客服系统流程:

AI回复 → 等待用户确认 → 继续执行

执行到“等待用户输入”时,流程需要暂停。这时系统会:

// 暂停:在节点里
await interrupt({ reason: "需要人工确认报价" });

用户输入后,系统会再恢复。这个恢复动作就是用Command完成的:

// 恢复:拿到外部输入后(继续同一节点的后续逻辑)
return new Command({
  resume: { approved: true, note: "人工已确认" },
  goto: "NextNode",
});

-EOF-

posted @ 2026-02-16 14:31  Zhentiw  阅读(0)  评论(0)    收藏  举报