
























在写普通函数的时候,测试其实是一件很自然的事情:给输入,看输出,对不对,一跑就知道。
但在 langgraph 里,情况会复杂一些。因为这里测试的对象,已经不只是一个函数了,而是:
也就是说,在 langgraph 里,我们测试的,其实是流程在某种状态下的行为是否符合预期。
正因为如此,langgraph 官方在测试这一块,给出了几种不同层级的测试方式,用来覆盖不同的开发场景。
这是最基础、也是最常见的一种测试方式。它解决的问题只有一个:这个 graph,从头跑到尾,能不能正常执行完?
基本思路
整体流程测试的做法非常简单:
invoke,跑完整个流程这里有一个非常重要的原则:不同的测试用例之间,不能共享 graph 实例,也不能共享 checkpointer。
原因也很好理解:
所以在测试里,哪怕 graph 的结构完全一样,也要每个 test 都重新 new 一份。
import { test, expect } from "vitest";
import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import { z } from "zod/v4";
// 图的状态
const State = z.object({
my_key: z.string(),
});
// 这是一个工厂函数,调用之后返回一个 graph 的实例
const createGraph = () => {
return new StateGraph(State)
.addNode("node1", () => {
return { my_key: "node1节点信息" };
})
.addNode("node2", () => {
return { my_key: "node2节点信息" };
})
.addEdge(START, "node1")
.addEdge("node1", "node2")
.addEdge("node2", END);
};
test("基本测试,针对整张图做一个测试", async () => {
// 测试核心
// 1. 执行 2. 给出预期 3. 查看执行的结果是否符合预期
// 1. 创建图的实例
const builder = createGraph();
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
// 2. 执行整张图
const result = await graph.invoke(
{
my_key: "初始值",
},
{
configurable: {
thread_id: "1",
},
},
);
// 3. 断言
expect(result.my_key).toBe("node2节点信息");
});
有些时候,我们并不想跑完整个流程。比如:
这时候,就可以使用 单节点测试。
核心做法
在 graph 编译完成之后,可以直接:
compiledGraph.nodes 中取出某个节点invoke这样做的结果是:
一个非常关键的点
有一个很容易被忽略,但非常重要的说明:直接调用节点,不会触发 checkpointer。
也就是说:
这点在理解上一定要拎清楚,否则很容易产生误解。
import { test, expect } from "vitest";
import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import { z } from "zod/v4";
const State = z.object({
my_key: z.string(),
});
// 工厂函数
const createGraph = () => {
return new StateGraph(State)
.addNode("node1", () => {
return { my_key: "node1节点信息" };
})
.addNode("node2", () => {
return { my_key: "node2节点信息" };
})
.addNode("node3", (state) => {
return { my_key: `${state.my_key} -> 节点3处理完毕` };
})
.addNode("node4", () => {
throw new Error("节点4发生的测试错误");
})
.addEdge(START, "node1")
.addEdge("node1", "node2")
.addEdge("node2", END)
.addEdge(START, "node3")
.addEdge("node3", END)
.addEdge(START, "node4")
.addEdge("node4", END);
};
test("测试节点1", async () => {
const builder = createGraph();
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
// 拿到了节点1的执行结果
const result = await graph.nodes["node1"].invoke({
my_key: "初始值",
});
// 断言
expect(result.my_key).toBe("node1节点信息");
});
test("测试节点3", async () => {
// 每一个测试用例需要有一个独立的graph
const builder = createGraph();
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
const result = await graph.nodes["node3"].invoke({
my_key: "aaa",
});
expect(result.my_key).toBe("aaa -> 节点3处理完毕");
});
test("测试节点4", async () => {
// 每一个测试用例需要有一个独立的graph
const builder = createGraph();
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
await expect(
graph.nodes["node4"].invoke({
my_key: "bbb",
}),
).rejects.toThrow("节点4发生的测试错误");
});
前面两种测试方式,其实都比较好理解。但在真实项目中,很容易遇到这样一种情况:
为了解决这个问题,langgraph 提供了 部分执行 的能力。
核心思想
部分执行的核心思想只有一句话:通过 checkpoint,模拟流程已经走到某个节点之后,再继续往下执行。
具体来说,就是两步:
updateState
thread_id 再次 invoke
这样一来,就可以做到:
这种方式的意义
这种测试方式非常“工程化”,它解决的是:
你可以把它类比成:加载一个存档,直接从关卡中间开始测试。
import { test, expect } from "vitest";
import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import { z } from "zod/v4";
const State = z.object({
my_key: z.string(),
step_history: z.array(z.string()).default([]), // 做字段变化的记录,方便观察流程,相当于节点足迹
});
const createGraph = () => {
return (
new StateGraph(State)
.addNode("node1", (state) => ({
my_key: "node1节点信息",
step_history: [...state.step_history, "经过node1"],
}))
// 节点2
.addNode("node2", (state) => ({
my_key: "node2节点信息",
step_history: [...state.step_history, "经过node2"],
}))
// 节点3
.addNode("node3", (state) => ({
my_key: "node3节点信息",
step_history: [...state.step_history, "经过node3"],
}))
// 节点4
.addNode("node4", (state) => ({
my_key: "node4节点信息",
step_history: [...state.step_history, "经过node4"],
}))
.addEdge(START, "node1")
.addEdge("node1", "node2")
.addEdge("node2", "node3")
.addEdge("node3", "node4")
.addEdge("node4", END)
);
};
test("跳过node1,直接从node2开始执行,并在node3停下", async () => {
// 每一个测试用例需要有一个独立的graph
const builder = createGraph();
const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
const config = {
configurable: {
thread_id: "test-thread-1",
},
};
// 更新状态:因为要跳过node1,更新一个假的node1节点执行完的状态
await graph.updateState(
config,
{
my_key: "我是伪造的节点1数据",
step_history: ["伪造的node1的执行历史"],
},
// 这个参数表明当前的这份状态是node1产生的
// 系统看到这个,就会去找node1的下一个节点
"node1",
);
// 断言
const result = await graph.getState(config); // 拿到当前最新的状态
expect(result.next).toEqual(["node2"]); // 下一个节点应该是node2节点
expect(result.values.my_key).toBe("我是伪造的节点1数据");
// 接下来要跑到node3停止
// 第1个参数是null,代表不要新的状态输入,而是使用checkpoint存档里面的状态继续跑
const result2 = await graph.invoke(null, {
...config,
interruptAfter: ["node3"], // 代表了执行完node3节点后中断
});
// 断言
expect(result2.my_key).toBe("node3节点信息");
expect(result2.step_history).toEqual([
"伪造的node1的执行历史",
"经过node2",
"经过node3",
]);
// 再次查看当前的状态
const finalState = await graph.getState(config);
// 因为目前是在node3中断了,所以下一个节点应该是node4
expect(finalState.next).toEqual(["node4"]);
});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。