跳到内容

功能性 API

概述

功能性 API 允许您将 LangGraph 的关键功能——持久化记忆人在回路流式处理——添加到您的应用程序中,并且对现有代码的改动极小。

它旨在将这些功能集成到可能使用标准语言原语进行分支和控制流(例如 if 语句、for 循环和函数调用)的现有代码中。与许多需要将代码重构为显式管道或 DAG 的数据编排框架不同,功能性 API 允许您在不强制执行严格执行模型的情况下集成这些功能。

功能性 API 使用两个关键构建块

  • @entrypoint – 将函数标记为工作流的起点,封装逻辑并管理执行流,包括处理长时间运行的任务和中断。
  • @task – 表示一个离散的工作单元,例如 API 调用或数据处理步骤,可以在入口点内异步执行。任务返回一个类似 Future 的对象,可以进行 await 或同步解析。

这为构建具有状态管理和流式处理功能的工作流提供了一个最小抽象。

提示

对于更喜欢声明式方法的用户,LangGraph 的图 API 允许您使用图范式定义工作流。两个 API 共享相同的底层运行时,因此您可以在同一应用程序中一起使用它们。请参阅功能性 API 对比 图 API 部分,以了解这两种范式的比较。

示例

下面我们演示一个简单的应用程序,它可以写一篇论文并中断以请求人工审阅。

API 参考:MemorySaver | entrypoint | task | interrupt

from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import interrupt


@task
def write_essay(topic: str) -> str:
    """Write an essay about the given topic."""
    time.sleep(1) # A placeholder for a long-running task.
    return f"An essay about topic: {topic}"

@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
    """A simple workflow that writes an essay and asks for a review."""
    essay = write_essay("cat").result()
    is_approved = interrupt({
        # Any json-serializable payload provided to interrupt as argument.
        # It will be surfaced on the client side as an Interrupt when streaming data
        # from the workflow.
        "essay": essay, # The essay we want reviewed.
        # We can add any additional information that we need.
        # For example, introduce a key called "action" with some instructions.
        "action": "Please approve/reject the essay",
    })

    return {
        "essay": essay, # The essay that was generated
        "is_approved": is_approved, # Response from HIL
    }
详细解释

这个工作流将写一篇关于“猫”主题的论文,然后暂停以获取人工审阅。工作流可以无限期地中断,直到提供审阅。

当工作流恢复时,它从头开始执行,但由于 write_essay 任务的结果已保存,任务结果将从检查点加载,而不是重新计算。

import time
import uuid

from langgraph.func import entrypoint, task
from langgraph.types import interrupt
from langgraph.checkpoint.memory import MemorySaver

@task
def write_essay(topic: str) -> str:
    """Write an essay about the given topic."""
    time.sleep(1) # This is a placeholder for a long-running task.
    return f"An essay about topic: {topic}"

@entrypoint(checkpointer=MemorySaver())
def workflow(topic: str) -> dict:
    """A simple workflow that writes an essay and asks for a review."""
    essay = write_essay("cat").result()
    is_approved = interrupt({
        # Any json-serializable payload provided to interrupt as argument.
        # It will be surfaced on the client side as an Interrupt when streaming data
        # from the workflow.
        "essay": essay, # The essay we want reviewed.
        # We can add any additional information that we need.
        # For example, introduce a key called "action" with some instructions.
        "action": "Please approve/reject the essay",
    })

    return {
        "essay": essay, # The essay that was generated
        "is_approved": is_approved, # Response from HIL
    }

thread_id = str(uuid.uuid4())

config = {
    "configurable": {
        "thread_id": thread_id
    }
}

for item in workflow.stream("cat", config):
    print(item)
{'write_essay': 'An essay about topic: cat'}
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}

论文已写好,可以审阅了。一旦提供审阅,我们可以恢复工作流

from langgraph.types import Command

# Get review from a user (e.g., via a UI)
# In this case, we're using a bool, but this can be any json-serializable value.
human_review = True

for item in workflow.stream(Command(resume=human_review), config):
    print(item)
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}

工作流已完成,审阅已添加到论文中。

入口点

@entrypoint 装饰器可用于从函数创建工作流。它封装工作流逻辑并管理执行流,包括处理长时间运行的任务中断

定义

通过使用 @entrypoint 装饰器装饰函数来定义入口点

该函数必须接受一个位置参数,该参数用作工作流输入。如果您需要传递多段数据,请使用字典作为第一个参数的输入类型。

entrypoint 装饰函数会产生一个 Pregel 实例,该实例有助于管理工作流的执行(例如,处理流式处理、恢复和检查点)。

您通常会希望将检查点传递给 @entrypoint 装饰器,以启用持久化并使用诸如人在回路之类的功能。

from langgraph.func import entrypoint

@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: dict) -> int:
    # some logic that may involve long-running tasks like API calls,
    # and may be interrupted for human-in-the-loop.
    ...
    return result
from langgraph.func import entrypoint

@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: dict) -> int:
    # some logic that may involve long-running tasks like API calls,
    # and may be interrupted for human-in-the-loop
    ...
    return result 

序列化

入口点的输入输出必须是 JSON 可序列化的,以支持检查点。有关更多详细信息,请参阅序列化部分。

可注入参数

声明 entrypoint 时,您可以请求访问将在运行时自动注入的附加参数。这些参数包括

参数 描述
previous 访问给定线程与上一个 checkpoint 相关联的状态。参见状态管理
store 一个 BaseStore 实例。对于长期记忆很有用。
writer 用于流式传输自定义数据,将自定义数据写入 custom 流。对于流式传输自定义数据很有用。
config 用于访问运行时配置。有关信息,请参见 RunnableConfig

重要

使用适当的名称和类型注解声明参数。

请求可注入参数
from langchain_core.runnables import RunnableConfig
from langgraph.func import entrypoint
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore

in_memory_store = InMemoryStore(...)  # An instance of InMemoryStore for long-term memory

@entrypoint(
    checkpointer=checkpointer,  # Specify the checkpointer
    store=in_memory_store  # Specify the store
)  
def my_workflow(
    some_input: dict,  # The input (e.g., passed via `invoke`)
    *,
    previous: Any = None, # For short-term memory
    store: BaseStore,  # For long-term memory
    writer: StreamWriter,  # For streaming custom data
    config: RunnableConfig  # For accessing the configuration passed to the entrypoint
) -> ...:

执行

使用 @entrypoint 会产生一个 Pregel 对象,可以使用 invokeainvokestreamastream 方法执行。

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}
my_workflow.invoke(some_input, config)  # Wait for the result synchronously
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}
await my_workflow.ainvoke(some_input, config)  # Await result asynchronously
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

for chunk in my_workflow.stream(some_input, config):
    print(chunk)
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

async for chunk in my_workflow.astream(some_input, config):
    print(chunk)

恢复

中断后恢复执行可以通过将 resume 值传递给 Command 原语来完成。

from langgraph.types import Command

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

my_workflow.invoke(Command(resume=some_resume_value), config)
from langgraph.types import Command

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

await my_workflow.ainvoke(Command(resume=some_resume_value), config)
from langgraph.types import Command

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
    print(chunk)
from langgraph.types import Command

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
    print(chunk)

错误后恢复

要在错误后恢复,请使用 None 和相同的线程 ID (config) 运行 entrypoint

这假定底层错误已解决,并且执行可以成功进行。

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

my_workflow.invoke(None, config)
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

await my_workflow.ainvoke(None, config)
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

for chunk in my_workflow.stream(None, config):
    print(chunk)
config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

async for chunk in my_workflow.astream(None, config):
    print(chunk)

状态管理

entrypointcheckpointer 定义时,它将在同一个线程 ID 上连续调用之间的信息存储在检查点中。

这允许使用 previous 参数访问上次调用的状态。

默认情况下,previous 参数是上次调用的返回值。

@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> int:
    previous = previous or 0
    return number + previous

config = {
    "configurable": {
        "thread_id": "some_thread_id"
    }
}

my_workflow.invoke(1, config)  # 1 (previous was None)
my_workflow.invoke(2, config)  # 3 (previous was 1 from the previous invocation)

entrypoint.final

entrypoint.final 是一个特殊的原语,可以从入口点返回,并允许将保存在检查点中的值与入口点的返回值解耦

第一个值是入口点的返回值,第二个值是保存在检查点中的值。类型注解是 entrypoint.final[return_type, save_type]

@entrypoint(checkpointer=checkpointer)
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
    previous = previous or 0
    # This will return the previous value to the caller, saving
    # 2 * number to the checkpoint, which will be used in the next invocation 
    # for the `previous` parameter.
    return entrypoint.final(value=previous, save=2 * number)

config = {
    "configurable": {
        "thread_id": "1"
    }
}

my_workflow.invoke(3, config)  # 0 (previous was None)
my_workflow.invoke(1, config)  # 6 (previous was 3 * 2 from the previous invocation)

任务

任务表示一个离散的工作单元,例如 API 调用或数据处理步骤。它有两个关键特性

  • 异步执行:任务设计为异步执行,允许多个操作并发运行而不会阻塞。
  • 检查点:任务结果保存到检查点,使工作流能够从上次保存的状态恢复。(有关更多详细信息,请参阅持久化)。

定义

任务使用 @task 装饰器定义,该装饰器包装常规 Python 函数。

API 参考:task

from langgraph.func import task

@task()
def slow_computation(input_value):
    # Simulate a long-running operation
    ...
    return result

序列化

任务的输出必须是 JSON 可序列化的,以支持检查点。

执行

任务只能从入口点、另一个任务状态图节点中调用。

任务不能直接从主应用程序代码中调用。

调用任务时,它会立即返回一个 Future 对象。Future 是稍后可用的结果的占位符。

要获取任务的结果,您可以同步等待它(使用 result())或异步等待它(使用 await)。

@entrypoint(checkpointer=checkpointer)
def my_workflow(some_input: int) -> int:
    future = slow_computation(some_input)
    return future.result()  # Wait for the result synchronously
@entrypoint(checkpointer=checkpointer)
async def my_workflow(some_input: int) -> int:
    return await slow_computation(some_input)  # Await result asynchronously

何时使用任务

任务在以下场景中很有用

  • 检查点:当您需要将长时间运行的操作结果保存到检查点时,以便在恢复工作流时无需重新计算。
  • 人在回路:如果您正在构建需要人工干预的工作流,则必须使用任务来封装任何随机性(例如 API 调用),以确保工作流可以正确恢复。有关更多详细信息,请参阅确定性部分。
  • 并行执行:对于 I/O 密集型任务,任务支持并行执行,允许多个操作并发运行而不会阻塞(例如调用多个 API)。
  • 可观察性:将操作包装在任务中提供了一种使用 LangSmith 跟踪工作流进度和监控单个操作执行的方法。
  • 可重试工作:当需要重试工作以处理故障或不一致性时,任务提供了一种封装和管理重试逻辑的方法。

序列化

LangGraph 中序列化有两个关键方面

  1. @entrypoint 输入和输出必须是 JSON 可序列化的。
  2. @task 输出必须是 JSON 可序列化的。

这些要求对于启用检查点和工作流恢复是必要的。使用 Python 原语,如字典、列表、字符串、数字和布尔值,以确保您的输入和输出可序列化。

序列化确保工作流状态,例如任务结果和中间值,可以可靠地保存和恢复。这对于启用人在回路交互、容错和并行执行至关重要。

提供不可序列化的输入或输出将在工作流配置了检查点时导致运行时错误。

确定性

为了利用诸如人在回路之类的功能,任何随机性都应该封装在任务内部。这保证了当执行暂停(例如,为了人在回路)然后恢复时,即使任务结果是非确定性的,它也将遵循相同的步骤序列

LangGraph 通过在执行时持久化任务子图结果来实现此行为。设计良好的工作流可确保恢复执行遵循相同的步骤序列,从而无需重新执行即可正确检索先前计算的结果。这对于长时间运行的任务或结果不确定性的任务特别有用,因为它避免了重复先前完成的工作,并允许从基本相同的状态恢复

虽然工作流的不同运行可以产生不同的结果,但恢复特定运行应始终遵循相同的记录步骤序列。这使得 LangGraph 可以高效地查找在图中断之前执行的任务子图结果,并避免重新计算它们。

幂等性

幂等性确保多次运行相同的操作会产生相同的结果。如果某个步骤因失败而重新运行,这有助于防止重复的 API 调用和冗余处理。始终将 API 调用放在任务函数内部以进行检查点,并将其设计为在重新执行时具有幂等性。如果任务启动但未成功完成,则可能发生重新执行。然后,如果工作流恢复,任务将再次运行。使用幂等性键或验证现有结果以避免重复。

功能性 API 对比 图 API

功能性 API图 API (StateGraph) 提供了两种不同的范式来使用 LangGraph 创建应用程序。以下是一些主要区别

  • 控制流:功能性 API 不需要考虑图结构。您可以使用标准的 Python 结构来定义工作流。这通常会减少您需要编写的代码量。
  • 状态管理图 API 需要声明一个状态 (State),并且可能需要定义归约器 (reducers) 来管理图状态的更新。@entrypoint@tasks 不需要显式状态管理,因为它们的状态作用域限定在函数内部,并且不在函数之间共享。
  • 检查点:两个 API 都生成并使用检查点。在图 API 中,每次超步 (superstep) 后都会生成一个新的检查点。在功能性 API 中,执行任务时,结果会保存到与给定入口点关联的现有检查点,而不是创建新的检查点。
  • 可视化:图 API 使将工作流可视化为图变得容易,这对于调试、理解工作流和与他人共享非常有用。功能性 API 不支持可视化,因为图是在运行时动态生成的。

常见陷阱

处理副作用

将副作用(例如写入文件、发送电子邮件)封装在任务中,以确保在恢复工作流时它们不会被多次执行。

在此示例中,副作用(写入文件)直接包含在工作流中,因此在恢复工作流时它将被第二次执行。

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    # This code will be executed a second time when resuming the workflow.
    # Which is likely not what you want.
    with open("output.txt", "w") as f:
        f.write("Side effect executed")
    value = interrupt("question")
    return value

在此示例中,副作用被封装在任务中,确保在恢复时一致执行。

from langgraph.func import task

@task
def write_to_file():
    with open("output.txt", "w") as f:
        f.write("Side effect executed")

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    # The side effect is now encapsulated in a task.
    write_to_file().result()
    value = interrupt("question")
    return value

非确定性控制流

每次可能给出不同结果的操作(如获取当前时间或随机数)应封装在任务中,以确保在恢复时返回相同的结果。

  • 在任务中:获取随机数 (5) → 中断 → 恢复 → (再次返回 5) → ...
  • 不在任务中:获取随机数 (5) → 中断 → 恢复 → 获取新的随机数 (7) → ...

在使用具有多次中断调用的人在回路工作流时,这一点尤为重要。LangGraph 会为每个任务/入口点保留一个恢复值列表。当遇到中断时,它会与相应的恢复值匹配。这种匹配严格基于索引,因此恢复值的顺序应与中断的顺序匹配。

如果在恢复时未维护执行顺序,则一个 interrupt 调用可能与错误的 resume 值匹配,从而导致结果不正确。

有关更多详细信息,请阅读确定性部分。

在此示例中,工作流使用当前时间来确定执行哪个任务。这是非确定性的,因为工作流的结果取决于执行时的时间。

from langgraph.func import entrypoint

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    t0 = inputs["t0"]
    t1 = time.time()

    delta_t = t1 - t0

    if delta_t > 1:
        result = slow_task(1).result()
        value = interrupt("question")
    else:
        result = slow_task(2).result()
        value = interrupt("question")

    return {
        "result": result,
        "value": value
    }

在此示例中,工作流使用输入 t0 来确定执行哪个任务。这是确定性的,因为工作流的结果仅取决于输入。

import time

from langgraph.func import task

@task
def get_time() -> float:
    return time.time()

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    t0 = inputs["t0"]
    t1 = get_time().result()

    delta_t = t1 - t0

    if delta_t > 1:
        result = slow_task(1).result()
        value = interrupt("question")
    else:
        result = slow_task(2).result()
        value = interrupt("question")

    return {
        "result": result,
        "value": value
    }

模式

下面是一些简单模式,展示了如何使用功能性 API 的示例。

定义 entrypoint 时,输入仅限于函数的第一个参数。要传递多个输入,可以使用字典。

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    value = inputs["value"]
    another_value = inputs["another_value"]
    ...

my_workflow.invoke({"value": 1, "another_value": 2})  

并行执行

任务可以通过并发调用并等待结果来并行执行。这对于提高 I/O 密集型任务(例如调用 LLM 的 API)的性能很有用。

@task
def add_one(number: int) -> int:
    return number + 1

@entrypoint(checkpointer=checkpointer)
def graph(numbers: list[int]) -> list[str]:
    futures = [add_one(i) for i in numbers]
    return [f.result() for f in futures]

调用子图

功能性 API图 API 可以一起在同一个应用程序中使用,因为它们共享相同的底层运行时。

API 参考:entrypoint | StateGraph

from langgraph.func import entrypoint
from langgraph.graph import StateGraph

builder = StateGraph()
...
some_graph = builder.compile()

@entrypoint()
def some_workflow(some_input: dict) -> int:
    # Call a graph defined using the graph API
    result_1 = some_graph.invoke(...)
    # Call another graph defined using the graph API
    result_2 = another_graph.invoke(...)
    return {
        "result_1": result_1,
        "result_2": result_2
    }

调用其他入口点

您可以从入口点任务内部调用其他入口点

@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
def some_other_workflow(inputs: dict) -> int:
    return inputs["value"]

@entrypoint(checkpointer=checkpointer)
def my_workflow(inputs: dict) -> int:
    value = some_other_workflow.invoke({"value": 1})
    return value

流式传输自定义数据

您可以使用 StreamWriter 类型从入口点流式传输自定义数据。这允许您将自定义数据写入 custom 流。

API 参考:MemorySaver | entrypoint | task | StreamWriter

from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter

@task
def add_one(x):
    return x + 1

@task
def add_two(x):
    return x + 2

checkpointer = MemorySaver()

@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter) -> int:
    """A simple workflow that adds one and two to a number."""
    writer("hello") # Write some data to the `custom` stream
    add_one(inputs['number']).result() # Will write data to the `updates` stream
    writer("world") # Write some more data to the `custom` stream
    add_two(inputs['number']).result() # Will write data to the `updates` stream
    return 5 

config = {
    "configurable": {
        "thread_id": "1"
    }
}

for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
    print(chunk)
('updates', {'add_one': 2})
('updates', {'add_two': 3})
('custom', 'hello')
('custom', 'world')
('updates', {'main': 5})

重要

writer 参数在运行时自动注入。仅当参数名称以该精确名称出现在函数签名中时,才会注入它。

重试策略

API 参考:MemorySaver | entrypoint | task | RetryPolicy

from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import RetryPolicy

attempts = 0

# Let's configure the RetryPolicy to retry on ValueError.
# The default RetryPolicy is optimized for retrying specific network errors.
retry_policy = RetryPolicy(retry_on=ValueError)

@task(retry=retry_policy) 
def get_info():
    global attempts
    attempts += 1

    if attempts < 2:
        raise ValueError('Failure')
    return "OK"

checkpointer = MemorySaver()

@entrypoint(checkpointer=checkpointer)
def main(inputs, writer):
    return get_info().result()

config = {
    "configurable": {
        "thread_id": "1"
    }
}

main.invoke({'any_input': 'foobar'}, config=config)
'OK'

错误后恢复

API 参考:MemorySaver | entrypoint | task | StreamWriter

import time
from langgraph.checkpoint.memory import MemorySaver
from langgraph.func import entrypoint, task
from langgraph.types import StreamWriter

# This variable is just used for demonstration purposes to simulate a network failure.
# It's not something you will have in your actual code.
attempts = 0

@task()
def get_info():
    """
    Simulates a task that fails once before succeeding.
    Raises an exception on the first attempt, then returns "OK" on subsequent tries.
    """
    global attempts
    attempts += 1

    if attempts < 2:
        raise ValueError("Failure")  # Simulate a failure on the first attempt
    return "OK"

# Initialize an in-memory checkpointer for persistence
checkpointer = MemorySaver()

@task
def slow_task():
    """
    Simulates a slow-running task by introducing a 1-second delay.
    """
    time.sleep(1)
    return "Ran slow task."

@entrypoint(checkpointer=checkpointer)
def main(inputs, writer: StreamWriter):
    """
    Main workflow function that runs the slow_task and get_info tasks sequentially.

    Parameters:
    - inputs: Dictionary containing workflow input values.
    - writer: StreamWriter for streaming custom data.

    The workflow first executes `slow_task` and then attempts to execute `get_info`,
    which will fail on the first invocation.
    """
    slow_task_result = slow_task().result()  # Blocking call to slow_task
    get_info().result()  # Exception will be raised here on the first attempt
    return slow_task_result

# Workflow execution configuration with a unique thread identifier
config = {
    "configurable": {
        "thread_id": "1"  # Unique identifier to track workflow execution
    }
}

# This invocation will take ~1 second due to the slow_task execution
try:
    # First invocation will raise an exception due to the `get_info` task failing
    main.invoke({'any_input': 'foobar'}, config=config)
except ValueError:
    pass  # Handle the failure gracefully

当我们恢复执行时,无需重新运行 slow_task,因为其结果已保存在检查点中。

main.invoke(None, config=config)
'Ran slow task.'

人在回路

功能性 API 支持使用 interrupt 函数和 Command 原语的人在回路工作流。

有关更多详细信息,请参阅以下示例

短期记忆

使用 previous 参数以及可选地使用 entrypoint.final 原语进行状态管理,可以用于实现短期记忆

有关更多详细信息,请参阅以下实操指南

长期记忆

长期记忆允许在不同的线程 ID 之间存储信息。这对于在一次对话中学习给定用户的信息并在另一次对话中使用它很有用。

有关更多详细信息,请参阅以下实操指南

工作流

智能体

评论