[LangGraph] 节点 & cache
节点的本质就是一个函数,这个函数可以是同步的,也可以是异步的,该函数会自动被框架包装成 RunnableLambda。
1. RunnableLambda
LangChain 提供的一种轻量级工具,它能把普通函数封装成符合 Runnable 接口规范的实例,从而让该函数能够无缝参与到 LCEL 的链式调用与流式处理流程中。
import { RunnableLambda } from "@langchain/core/runnables";
// 普通函数
const fn = (text) => {
return text.toUpperCase();
};
// 将 fn 转换为 Runnable 类型函数
const runnableFn = RunnableLambda.from(fn);
await runnableFn.invoke("hello");
Code:
import { END, START, StateGraph } from "@langchain/langgraph";
import { InMemoryCache } from "@langchain/langgraph-checkpoint";
import z from "zod/v4";
const Schema = z.object({
userId: z.string(),
query: z.string(),
results: z.string().optional(),
});
type TState = z.infer<typeof Schema>;
const app = new StateGraph(Schema)
.addNode(
"node1",
async (state: TState) => {
console.log("Long operation");
await new Promise((resolve) => setTimeout(resolve, 2000));
return {
results: `${state.query} result`,
};
},
{
cachePolicy: {
ttl: 10, // seconds
keyFunc: (args: any) => {
const s = Array.isArray(args) ? args[args.length - 1] : args;
return s?.userId; // If userId is the same, the cache will be used
},
},
},
)
.addEdge(START, "node1")
.addEdge("node1", END)
.compile({ cache: new InMemoryCache() });
async function test() {
console.log(
await app.invoke({
userId: "123",
query: "What is the capital of France?",
}),
);
console.log(
await app.invoke({
userId: "234",
query: "What is the capital of France?",
}),
);
console.log(
await app.invoke({
userId: "123",
query: "What is the capital of France?",
}),
);
}
test();
2. 节点参数
在一个节点函数中,会传入两个参数:
-
state:这是图当前的状态,由 StateGraph 控制
function node1(state){ // ... return { // ... } } -
config:一个 RunnableConfig 类型的配置对象
通过 graph 实例的 addNode 方法可以添加节点:
graph.addNode("节点名称", 节点函数);
graph.addNode("node1", node1);
graph.addNode("node2", async ()=>{});
例如:
graph.addNode("myNode", (state, config) => {
console.log("In node: ", config?.configurable?.user_id);
return { results: `Hello, ${state.input}!` };
})
addNode("otherNode", (state) => {
return state;
})
可以看到,节点函数在经过一些处理后,会返回新的状态。
3. 默认节点名
如果不书写节点名称,会自动取函数名作为节点名。例如:
function myNode(state) {
return { ... };
}
graph.addNode(myNode);
上面的代码等价于:
graph.addNode("myNode", myNode);
4. 开始节点和结束节点
START 是一个特殊的节点,表示图的起点。
import { START } from "@langchain/langgraph";
graph.addEdge(START, "nodeA");
该节点并不做什么事情,只是代表一个开始的标识。
addEdge(START, "nodeA") 表示将 START 和 nodeA 相连,nodeA 其实是真正要运行的第一个实际节点。
与之相对的,END 也是一个特殊节点,表示图的结束:
import { END } from "@langchain/langgraph";
graph.addEdge("nodeA", END);
上面的 addEdge("nodeA", END) 表示将 nodeA 和 END 相连,而 END 表示终止执行、停止流程。
5. 节点缓存机制
某一些节点函数计算昂贵,可以选择将其缓存起来,这样下次有相同的输入时不用再重新执行节点,而是直接返回缓存结果。
要使用缓存,需要下面两个步骤:
1. compile() 编译图的时候,启用缓存
graph.compile({ cache: new InMemoryCache() });
表示整个图都使用内存缓存。
也可以换成 Redis、file、database 等。
2. 指定缓存策略
缓存策略有两种:
- ttl:缓存有效时长(秒),决定“缓存多久过期”,适用于想让缓存自动失效时。
- keyFunc:自定义缓存 key,决定“什么输入算同一个缓存”,适用于想精细控制缓存命中逻辑时。
- LangGraph 在执行流程中有多个“检查缓存”的时机,而每个时机的 state 结构不同(有时已 reduce、有时未 reduce)
- 因此 cachePolicy 的 keyFunc 会发生在 “reduce 前” 或 “reduce 后” 的不同位置,有可能拿到:
- 单一 state
- reducer 尚未处理的 state 数组
-EOF-

浙公网安备 33010602011771号