[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节点中:
- 更新当前状态的字段
foo; - 执行完后直接跳转到
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"] // 允许列表
});
快速上手
接下来我们来看一个快速上手的案例。
流程:
- 用户提交订单
- A 节点做基础校验
- 根据订单类型跳转到 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 自己完成了“计算 + 更新 + 跳转”三件事,图外不再需要再问一次“去哪”。
总结:
- Command:在节点里面计算,计算完成后进行跳转
- 条件分支:在节点外编排跳转逻辑
两者的具体区别如下表:
| 特性 | 条件分支 | Command |
|---|---|---|
| 定义位置 | 图结构层(Graph 设计阶段) | 节点函数内部(运行阶段) |
| 逻辑归属 | 结构控制(流程图层) | 业务逻辑(节点层) |
| 能否更新 state | 不能更新,只决定方向 | 可以更新并跳转 |
| 调用时机 | 节点执行完之后(外部控制流) | 节点执行过程中(内部决定) |
| 使用场景 | 固定的流程路由、if/else 分支 | 动态决策、代理交接、HITL 恢复等 |
应用场景
由于执行机制的不同,Command常用于以下两类场景:
- 跨子图跳转
- 人工介入、人工干预、中断
跨子图跳转
由于条件边的跳转范围仅限于当前图内,无法直接跨出子图跳到父图或兄弟子图。比如:
父图:
├── 子图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-

浙公网安备 33010602011771号