[LangGraph] Functional API

在 langgraph 中,其实有两套写流程的方式:

  1. Graph API
  2. Functional API

Functional API 把工作流从画流程图变成写业务函数,但依然保留可恢复、可中断、可追踪的能力。

  • 传统 Graph API 的思路:先设计一张图,再往图里塞节点
  • Function API 的思路:先写业务逻辑函数,LangGraph 帮我托管执行状态

看起来像普通 async 函数,但背后具备:

  • 可中断(interrupt)
  • 可恢复(resume)
  • 有 checkpoint
  • 支持时间旅行

它存在的意义就是:降低建模成本,让你先把业务跑通,再逐步演进成更复杂的图。

核心模块

要使用 Functional API,需要了解构建工作流的两个核心模块:

  • entrypoint:封装工作流逻辑并负责管理执行流程,包括处理长时间运行的任务和中断。
  • task:表示一个离散的工作单元,例如一次 API 调用或数据处理步骤,可以在 entrypoint 中异步执行。一般用于有副作用、耗时或者外部调用的部分。

1. entrypoint

在 Functional API 中,entrypoint 是整个工作流的入口

它的作用不是“执行一个函数”,而是将一个普通函数封装为一个可被 langgraph 管理的工作流,由 langgraph 负责执行过程中的状态保存、中断、恢复以及多次调用之间的衔接。

entrypoint 的基本使用规则

在定义 entrypoint 时,有几条非常重要的规则需要提前明确:

  1. entrypoint 必须基于一个函数定义

    entrypoint({
      // 配置对象
    }, async ()=>{})
    
  2. 该函数只能接收一个参数作为输入

    官方文档原话:

    An entrypoint is defined by calling the entrypoint function with configuration and a function.The function must accept a single positional argument, which serves as the workflow input. If you need to pass multiple pieces of data, use an object as the input type for the first argument.

  3. 输入和输出必须是可 JSON 序列化的数据

  4. 如果需要传多个参数,应将它们封装为一个对象

  5. 实际使用中通常需要配合 checkpointer,否则无法启用恢复与记忆能力

这些规则的本质原因是:langgraph 需要将函数的输入、输出和执行状态持久化到检查点中。

定义一个 entrypoint

使用 entrypoint 函数即可创建一个工作流入口。

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

const workflow = entrypoint(
  // 配置对象
  { checkpointer, name: "workflow" },
  async (input) => {
    // 这里编写你的工作流逻辑
    return result;
  }
);
await workflow.invoke();

通过这种方式定义后,得到的并不是一个普通函数,而是一个 workflow 实例,后续所有的执行、恢复、流式处理,都是通过这个实例完成的。

entrypoint 的执行方式

entrypoint 返回的 workflow 实例,主要有两种执行方式。

invoke:一次性执行并返回结果

await workflow.invoke(input, config);

stream:流式执行

for await (const chunk of workflow.stream(input, config)) {
  console.log(chunk);
}

这种方式会在执行过程中不断产出结果,适合:

entrypoint 与中断和恢复

当在执行过程中触发 interrupt 时:

  • 工作流会立即暂停
  • 当前状态会被写入检查点
  • 执行权返回给调用方

恢复执行时,并不是“从中断的下一行继续执行”,而是:使用相同的 thread_id,再次调用 entrypoint,langgraph 会自动恢复到上一次的状态。

换句话说:恢复执行的时候,是重新执行整个 entrypoint 所对应的函数,不是从中断的开始。

通过使用 Command ,可以恢复中断,并且从外界将数据传递到中断处。

await workflow.invoke(
  new Command({ resume: value }),
  config
);

恢复执行本质上是一次新的调用(entrypoint的那个函数),通过 checkpoint 能够拿出中断前所执行的 task 的缓存结果。

在 entrypoint 中,中断前的副作用操作同样必须具备幂等性。

2. task

在 Functional API 中,task 用来表示工作流中的一个独立步骤。

你可以把 task 理解为:被 langgraph 接管执行的一小段业务逻辑,比如一次接口请求、一次数据库查询,或者一次明确的数据处理过程。

task特点

  1. task 是一个独立的工作单元,它代表一件具体要做的事,比如:

    • 调一次 API

    • 处理一段数据

    • 执行一次计算

    每个 task 都是一个“原子步骤”,不关心整个流程,只关心自己这一步。

  2. task 是异步执行的,task 不会阻塞主流程:

    • 可以并发跑多个 task

    • 某个 Task 在等网络、等 I/O 时,不会卡住整个系统

    本质上就是:适合长耗时、I/O 型操作

  3. task 会自动做检查点,Task 执行完成后:

    • 结果会被保存到 checkpoint 检查点

    • 如果流程中断、崩溃或被暂停

    • 下次可以从这个 task 执行完的地方继续,而不是从头来

定义 task 的方式也非常简单,只需要使用 task 函数包裹一个普通函数即可。被包装后的函数不再是“随便调用的工具函数”,而是一个只能在工作流内部运行的受管执行单元。

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

const slowComputation = task("slowComputation", async (inputValue: any) => {
  // 模拟一个长时间运行的操作
  return result;
});

task 只能在三种地方被调用:

  1. entrypoint 内部
  2. 另一个 task 内部
  3. StateGraph 的节点中

它不能直接在主应用代码中执行,这是因为 task 的执行依赖于 LangGraph 的上下文与检查点机制。

在使用时,调用 task 会返回一个 Promise,可以像调用普通异步函数一样使用 await 获取结果。

const myWorkflow = entrypoint(
  { checkpointer, name: "workflow" },
  async (someInput: number): Promise<number> => {
    return await slowComputation(someInput);
  }
);

但与普通异步函数不同的是,task 的执行过程和结果都会被 langgraph 纳入工作流管理体系中,这是它存在的核心价值。

import "dotenv/config";
import {
  MemorySaver,
  entrypoint,
  task,
  interrupt,
  Command,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import readline from "readline-sync";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0.7,
});

// 1. 定义一个任务
const writeEssay = task("writeEssay", async (topic: string) => {
  console.log(`\n正在生成关于 "${topic}" 的文章...\n`);

  // 通过模型生成文章
  const stream = await model.stream([
    new HumanMessage(
      `请写一篇50字左右的,关于以下主题的短文:${topic}。请用中文回答。`,
    ),
  ]);

  // 将文章的内容显示出来
  let content = ""; // 拼接文章内容

  // 流式输出
  for await (const chunk of stream) {
    process.stdout.write(chunk.content as string);
    content += chunk.content as string;
  }

  process.stdout.write("\n\n");

  return content; // 对外返回最终的内容
});

const checkpointer = new MemorySaver();

// 2. 定义一个工作流
const workflow = entrypoint(
  {
    name: "writeEssayWorkflow",
    checkpointer,
  },
  async (topic: string) => {
    let currentTopic = topic;

    while (true) {
      // 因为我们需要不断的让用户确认,生成的文章是否符合要求

      // 2-1. 执行任务
      const essay = await writeEssay(currentTopic); // 当这个任务执行完了之后,结果就会缓存到 checkpoint 里面

      // 2-2. 中断,代码回到外界,让用户决定是否通过审核
      const { approved, newTopic } = interrupt({
        // 传递给外界的
        essay, // 生成的文章
        action: "请审核该文章", // 给外界提供的一个提示语句
      });

      if (approved) {
        // 审核通过了,返回最终结果
        return {
          essay,
          approved,
        };
      }

      // 代码来到这儿,说明审核没通过
      // 更新主题
      if (newTopic) currentTopic = newTopic;
    }
  },
);

const config = {
  configurable: {
    thread_id: "generate_essay",
  },
};

async function main() {
  console.log("--- 启动工作流 ---");
  // 让用户输入主题
  const topic = readline.question("请输入文章主题:");

  // 启动工作流
  await workflow.invoke(topic, config);

  const finalState = await handleHumanReview();

  console.log("\n--- 工作流结束 ---");
  console.log("最终结果:", finalState.values);
}

// 该方法专门是用于处理中断相关的逻辑的
async function handleHumanReview() {
  while (true) {
    // 拿到最新的状态
    const state = await workflow.getState(config);
    // console.log("state>>>", state);

    // 从这个状态上面获取中断
    const interruptTask = state.tasks?.find((t) => t.interrupts?.length > 0);
    // console.log("interruptTask>>>>", interruptTask);
    if (!interruptTask) return state;

    // 代码来到这里,说明需要处理中断
    // console.log(
    //   "interruptTask.interrupts[0].value>>>",
    //   interruptTask.interrupts[0].value
    // );
    const { action } = interruptTask.interrupts[0].value;
    console.log("\n!!! 收到中断请求 !!!");
    console.log("操作提示:", action);

    // 再次询问用户是否审批通过这篇文章
    const answer = readline.question("\n是否批准这篇文章?(yes/no): ");
    // 根据用户的输入决定返回给中断的值
    // approved 是一个布尔值
    const approved = ["yes", "y", "ok", "批准"].includes(
      answer.trim().toLowerCase(),
    );

    let newTopic;
    if (!approved) {
      // 如果进入此分支,说明审核不通过,需要外界的用户输入新的主题
      newTopic = readline.question("审核不通过,请输入新的文章主题:");
    }

    // 代码来到这里,需要恢复中断
    console.log(
      `\n--- 恢复工作流 (用户输入: ${approved ? "批准" : "重写"}) ---`,
    );

    // 如何恢复工作流?
    await workflow.invoke(
      new Command({
        resume: {
          approved, // 审批是否通过
          newTopic, // 可能是undefined、也可能是新主题
        },
      }),
      config,
    );
  }
}

main();

核心区别

下面是两者之间的一些关键区别:

1. 控制流层面

Functional API 不要求你先“画图”。你就按正常的思路写函数,顺序执行、条件判断都用原生语法即可,更像在写普通业务代码,代码量也更少。

2. 短期记忆管理

  • Graph API 中,状态是“全局共享的”,需要你提前声明 State,甚至还要写 reducer 来控制状态怎么合并。
  • 而 Functional API 中,state 只存在于当前函数调用上下文,函数之间不共享,不用你手动管状态结构,理解成本更低。

Function API 根本“没有全局 state 这个概念”

在 Function API 里:

  • 没有 StateSchema
  • 没有 state 对象在 task 之间自动流转
  • 没有 reducer

写的 entrypoint,本质上就是一个普通的 async 函数:

const workflow = entrypoint(
  { checkpointer: new MemorySaver(), name: "wf" },
  async (input) => {
    // 这里的一切,都是“函数内部的局部变量”
  }
);

Function API 的 “state” ≈ 普通函数的局部变量

看一个最直观的例子:

const step1 = task("step1", async (x: number) => x + 1);
const step2 = task("step2", async (x: number) => x * 2);

const workflow = entrypoint(
  { checkpointer: new MemorySaver(), name: "wf" },
  async (input: number) => {
    const a = await step1(input); // a 是局部变量
    const b = await step2(a);     // b 也是局部变量
    
    // 两个任务的执行结果,会缓存到checkpoint里面

    return b;
  }
);

这里的 ab 就是 Function API 里的“短期记忆”,它们:

  • 只存在于这一次 entrypoint 执行过程中
  • 不会自动暴露给别的 workflow
  • 不会在 entrypoint 之间共享

一旦这次调用结束:所有这些“state”都会自然消失

那中断 / 恢复时,这些“state”去哪了?

你可能会问:a、b 这些变量在 interrupt 之后还能用吗?

答案是:不能直接用,但可以“被重算出来”。

看这个例子:

const workflow = entrypoint(
  { checkpointer: new MemorySaver(), name: "wf" },
  async (input: number) => {
    const a = await step1(input);
    const b = await step2(a);

    const ok = interrupt({ b });

    return b;
  }
);

发生 interrupt 后,JS 层面的 ab 都没了。下次恢复执行时:

  • entrypoint 从头执行
  • a = await step1(input) 再算一次
  • 但如果 step1 有 checkpoint,那就直接取缓存结果,如果没有才真正执行

所以 Function API 的“state 持久性”不是靠 state 对象,而是靠 task 的结果缓存

3. 检查点机制

两种 API 都支持检查点。

  • Graph API 的特点是:每走完一个 superstep,就会生成一个新的检查点。也就是说,在Graph API中,是有多个检查点的。
  • Functional API 则不同:task 的执行结果会写回到 entrypoint 对应的检查点中,而不是不断生成新的检查点,整体更“轻量”。

Function API 里,checkpoint 是 entrypoint 执行状态的快照,每当某个 task 成功完成或发生 interrupt,langgraph 会更新这一个 checkpoint,把「已完成的 task 结果」写进去,注意是“更新”,不是“新增”

checkpoint 里实际存的是什么?

在 Function API 的 checkpoint 中,核心存的是三类信息:

  1. entrypoint 的输入
  2. 已经完成的 task 及其结果
  3. interrupt 的 payload(如果有)

它不关心:

  • JS 执行到哪一行
  • 中间变量的值
  • 调用栈状态

所以 checkpoint 的语义是:“entrypoint 重跑时,哪些 task 可以直接复用结果”

恢复执行时,两种 API 的行为差异

Graph API 恢复时:

  • 从 checkpoint 记录的 node 位置
  • 继续向后调度节点
  • 不会重新走已经完成的节点

恢复像是:流程接着走

Function API 恢复时:

  • 整个 entrypoint 从第一行重新执行
  • 但遇到已经有 checkpoint 结果的 task,直接“命中缓存”,不再真正执行
  • 只执行还没有结果的 task

恢复像是:流程重放 + task 级缓存

4. 可视化能力

  • Graph API 天然就是“图”,可以直接可视化流程,非常适合分析复杂结构。
  • Functional API 的流程是在运行时动态展开的,本身不提供图形化展示,更偏向执行而非展示。
posted @ 2026-03-24 14:53  Zhentiw  阅读(2)  评论(0)    收藏  举报