[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. 节点参数

在一个节点函数中,会传入两个参数:

  1. state:这是图当前的状态,由 StateGraph 控制

    function node1(state){
      // ...
      return {
        // ...
      }
    }
    
  2. 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") 表示将 STARTnodeA 相连,nodeA 其实是真正要运行的第一个实际节点。

与之相对的,END 也是一个特殊节点,表示图的结束:

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

graph.addEdge("nodeA", END);

上面的 addEdge("nodeA", END) 表示将 nodeAEND 相连,而 END 表示终止执行、停止流程。

5. 节点缓存机制

某一些节点函数计算昂贵,可以选择将其缓存起来,这样下次有相同的输入时不用再重新执行节点,而是直接返回缓存结果。

要使用缓存,需要下面两个步骤:

1. compile() 编译图的时候,启用缓存

graph.compile({ cache: new InMemoryCache() });

表示整个图都使用内存缓存。

也可以换成 Redis、file、database 等。

2. 指定缓存策略

缓存策略有两种:

  1. ttl:缓存有效时长(秒),决定“缓存多久过期”,适用于想让缓存自动失效时。
  2. keyFunc:自定义缓存 key,决定“什么输入算同一个缓存”,适用于想精细控制缓存命中逻辑时
    • LangGraph 在执行流程中有多个“检查缓存”的时机,而每个时机的 state 结构不同(有时已 reduce、有时未 reduce)
    • 因此 cachePolicy 的 keyFunc 会发生在 “reduce 前” 或 “reduce 后” 的不同位置,有可能拿到:
      • 单一 state
      • reducer 尚未处理的 state 数组

-EOF-

posted @ 2026-02-10 14:29  Zhentiw  阅读(5)  评论(0)    收藏  举报