LangGraph Functional API 实战:用最小改动为应用注入持久化与交互能力

在开发智能应用时,我们常常面临这样的挑战:如何在不重构现有代码的前提下,为应用添加持久化、内存管理、人机交互等高级功能?传统框架往往要求我们将代码重构成复杂的流水线或有向无环图,这不仅耗时费力,还可能引入新的问题。今天我们要聊的 LangGraph Functional API,正是解决这一困境的利器 —— 它允许我们用最小的代码改动,为应用注入这些关键能力,让开发过程更加流畅高效。

一、Functional API 核心概念:轻量级的工作流革命

Functional API 的设计理念非常贴合实际开发需求:它不需要我们改变现有的代码结构,而是通过两个核心装饰器@entrypoint@task,将持久化、内存管理等功能无缝集成到现有代码中。这种设计就像给应用装上了 "增强插件",无需大动干戈就能获得强大的新能力。

1.1 两大核心组件解析

@entrypoint:工作流的 "总指挥"

@entrypoint装饰器将普通函数转换为工作流的入口点,它负责封装整个工作流的逻辑,管理执行流程,包括处理长时间运行的任务和中断请求。就像一个经验丰富的指挥官,@entrypoint掌控着工作流的全局节奏:

python

from langgraph.func import entrypoint
from langgraph.checkpoint.memory import MemorySaver

@entrypoint(checkpointer=MemorySaver())
def data_processing_workflow(input_data: dict) -> dict:
    """处理数据并请求人工审核的工作流"""
    # 调用任务处理数据
    processed_data = data_processor(input_data).result()
    # 中断工作流以请求人工审核
    is_approved = interrupt({
        "processed_data": processed_data,
        "action": "请审核数据处理结果"
    })
    return {
        "data": processed_data,
        "approval_status": is_approved
    }
@task:离散任务的 "执行者"

@task装饰器用于定义离散的工作单元,如 API 调用或数据处理步骤。这些任务可以在@entrypoint内异步执行,返回类似未来对象(future-like),允许我们等待结果或异步处理。每个任务就像团队中的专业成员,专注完成自己的特定工作:

python

from langgraph.func import task
import time

@task
def data_processor(input_data: dict) -> dict:
    """模拟长时间运行的数据处理任务"""
    time.sleep(2)  # 模拟耗时操作
    return {
        "processed": True,
        "result": f"处理后的数据: {input_data['raw']}"
    }

1.2 与 Graph API 的差异化优势

很多同学可能会问,Functional API 和 LangGraph 的 Graph API 有什么区别?这里我们用一张表格清晰对比:

对比维度Functional APIGraph API
控制流使用标准 Python 结构(if、for、函数调用)需要定义图结构和状态更新
状态管理隐式管理,作用域限于函数显式声明状态和 reducers
Checkpointing任务结果保存到现有 checkpoint每个超级步骤生成新 checkpoint
可视化不支持(运行时动态生成)易于可视化工作流图
代码量通常需要更少的代码可能需要更多结构化代码

二、深入 @entrypoint:工作流的中枢控制

2.1 定义与基本用法

@entrypoint的定义非常简洁,只需装饰一个函数并指定 checkpointer(用于持久化):

python

from langgraph.func import entrypoint
from langgraph.checkpoint.memory import MemorySaver

@entrypoint(checkpointer=MemorySaver())
def counter_workflow(number: int, *, previous: int = None) -> int:
    """累加器工作流,演示短期记忆功能"""
    previous = previous or 0
    return number + previous

这个工作流接收一个数字,与之前的结果累加。当我们多次调用时:

python

# 第一次调用
result1 = counter_workflow.invoke(1, config={"configurable": {"thread_id": "counter_thread"}})
print(result1)  # 输出: 1

# 第二次调用,previous会记住上次的结果
result2 = counter_workflow.invoke(2, config={"configurable": {"thread_id": "counter_thread"}})
print(result2)  # 输出: 3

2.2 注入参数:动态获取运行时上下文

@entrypoint支持注入多个有用的参数,让我们能在工作流中获取运行时状态:

  • previous:获取同一线程上一次检查点的状态
  • store:访问 BaseStore 实例,用于长期记忆
  • writer:访问 StreamWriter,用于流式处理
  • config:获取运行时配置

python

@entrypoint(checkpointer=MemorySaver())
def workflow_with_injections(input_data: dict, *, previous: dict = None, store: BaseStore = None) -> dict:
    """使用注入参数的工作流"""
    # 访问上一次的结果
    previous_result = previous or {"count": 0}
    # 访问长期存储
    user_data = store.get("user_preferences")
    # 处理逻辑...
    return {
        "current": input_data,
        "previous": previous_result,
        "user_data": user_data
    }

2.3 工作流的执行与恢复

同步与异步执行

python

# 同步执行
result = my_workflow.invoke(input_data, config=config)

# 异步执行
async_result = my_workflow.ainvoke(input_data, config=config)
中断后恢复

当工作流被中断(如等待人工审核)后,可以通过传递 resume 值恢复执行:

python

from langgraph.types import Command

# 第一次调用,工作流中断并返回等待审核
initial_result = my_workflow.invoke(input_data, config=config)

# 人工审核后,传递恢复值
resumed_result = my_workflow.invoke(Command(resume=approval_result), config=config)
错误恢复

如果工作流执行出错,解决问题后可以用 None 和相同线程 ID 恢复:

python

# 错误后恢复执行
recovered_result = my_workflow.invoke(None, config={"configurable": {"thread_id": "error_thread"}})

2.4 entrypoint.final:分离返回值与存储值

有时我们需要工作流返回一个值,同时存储另一个值到检查点,这时可以使用entrypoint.final

python

from langgraph.func import entrypoint, entrypoint_final

@entrypoint(checkpointer=MemorySaver())
def special_counter(number: int, *, previous: int = None) -> entrypoint_final[int, int]:
    """返回当前累加值,但存储双倍的当前值作为下一次的previous"""
    previous = previous or 0
    current_sum = previous + number
    # 返回current_sum给调用者,但存储2*current_sum到检查点
    return entrypoint_final(value=current_sum, save=2 * current_sum)

调用效果:

python

# 第一次调用:返回0+3=3,存储6
result1 = special_counter.invoke(3, config={"configurable": {"thread_id": "special_counter"}})
print(result1)  # 输出: 3

# 第二次调用:previous是6,返回6+1=7,存储14
result2 = special_counter.invoke(1, config={"configurable": {"thread_id": "special_counter"}})
print(result2)  # 输出: 7

三、@task:异步任务的封装与执行

3.1 任务的定义与基本特性

@task装饰器将普通函数转换为可异步执行的任务,具备两个关键特性:

  • 异步执行:不阻塞主线程,可并发处理多个任务
  • 检查点保存:任务结果会保存到检查点,支持工作流恢复

python

from langgraph.func import task
import requests

@task
def fetch_data(url: str) -> dict:
    """异步获取网络数据的任务"""
    response = requests.get(url)
    response.raise_for_status()
    return response.json()

3.2 任务的调用与结果获取

任务只能在@entrypoint或其他任务内部调用,返回一个 future 对象,可通过result()同步获取结果或await异步获取:

python

@entrypoint(checkpointer=MemorySaver())
def data_aggregator_workflow(urls: list[str]) -> list[dict]:
    """聚合多个URL数据的工作流"""
    futures = [fetch_data(url) for url in urls]
    # 同步等待所有任务完成
    results = [future.result() for future in futures]
    return results

3.3 何时使用 @task

任务适用于以下场景:

  • 需要持久化结果:避免重复计算长时间运行的任务
  • 人机交互流程:封装随机性操作(如 API 调用)确保恢复时结果一致
  • 并行执行:I/O 密集型任务并行处理,提高效率
  • 可观察性:通过 LangSmith 跟踪任务执行进度
  • 可重试操作:封装重试逻辑,处理临时故障

四、关键技术要点:序列化、确定性与幂等性

4.1 序列化要求:确保状态可持久化

Functional API 对输入输出有严格的序列化要求:

  • @entrypoint的输入和输出必须是 JSON 可序列化的
  • @task的输出必须是 JSON 可序列化的

这是因为检查点需要将状态保存到存储中,以便后续恢复。使用 Python 基本类型(字典、列表、字符串等)可以确保序列化成功:

python

# 正确做法:使用可序列化类型
@entrypoint(checkpointer=MemorySaver())
def valid_workflow(data: dict) -> dict:
    return {"processed": data}

# 错误做法:使用不可序列化的类型(如文件对象)
@entrypoint(checkpointer=MemorySaver())
def invalid_workflow(file: File) -> dict:  # 运行时会报错
    return {"file": file}

4.2 确定性:确保恢复时流程一致

在人机交互的工作流中,确定性至关重要。任何随机性操作(如生成随机数、获取当前时间)都应封装在任务中,确保恢复时执行相同的步骤:

python

# 正确:随机数生成在任务中,恢复时返回相同值
@task
def generate_random() -> int:
    return random.randint(1, 10)

@entrypoint(checkpointer=MemorySaver())
def deterministic_workflow() -> dict:
    random_num = generate_random().result()
    is_approved = interrupt(f"是否批准随机数 {random_num}?")
    return {"random": random_num, "approved": is_approved}

# 错误:随机数生成在任务外,恢复时会产生新值
@entrypoint(checkpointer=MemorySaver())
def non_deterministic_workflow() -> dict:
    random_num = random.randint(1, 10)  # 不在任务中
    is_approved = interrupt(f"是否批准随机数 {random_num}?")
    return {"random": random_num, "approved": is_approved}

4.3 幂等性:避免重复操作的副作用

设计任务时应确保幂等性,即多次执行同一操作产生相同结果,这有助于避免 API 重复调用或数据不一致:

python

@task
def create_user(user_data: dict) -> dict:
    """创建用户的幂等任务"""
    # 使用idempotency_key避免重复创建
    idempotency_key = user_data["email"]
    # 检查是否已创建
    if user_exists(idempotency_key):
        return get_user(idempotency_key)
    # 否则创建新用户
    return db_create_user(user_data)

五、常见陷阱与最佳实践

5.1 副作用处理:封装而非暴露

永远不要在@entrypoint中直接执行有副作用的操作(如写文件、发邮件),这些操作应封装在任务中,避免恢复时重复执行:

python

# 错误:副作用在entrypoint中,恢复时会重复执行
@entrypoint(checkpointer=MemorySaver())
def bad_workflow(data: dict) -> dict:
    with open("output.txt", "a") as f:
        f.write(f"Data processed: {data}\n")  # 恢复时会重复写入
    result = process_data(data).result()
    return result

# 正确:副作用封装在任务中
@task
def log_to_file(data: dict) -> None:
    with open("output.txt", "a") as f:
        f.write(f"Data processed: {data}\n")

@entrypoint(checkpointer=MemorySaver())
def good_workflow(data: dict) -> dict:
    result = process_data(data).result()
    log_to_file(result)  # 任务会确保只执行一次
    return result

5.2 非确定性控制流:任务内决策

避免在@entrypoint中使用依赖外部非确定性因素的控制流(如当前时间),应将决策逻辑封装在任务中:

python

# 错误:根据当前时间决定执行路径,恢复时可能不同
@entrypoint(checkpointer=MemorySaver())
def bad_control_flow(data: dict) -> dict:
    current_time = time.time()
    if current_time % 2 == 0:
        result = task_a(data).result()
    else:
        result = task_b(data).result()
    return result

# 正确:将决策封装在任务中,确保恢复时路径一致
@task
def decide_task(data: dict) -> str:
    current_time = time.time()
    return "task_a" if current_time % 2 == 0 else "task_b"

@entrypoint(checkpointer=MemorySaver())
def good_control_flow(data: dict) -> dict:
    task_to_run = decide_task(data).result()
    if task_to_run == "task_a":
        result = task_a(data).result()
    else:
        result = task_b(data).result()
    return result

六、实战案例:构建带人工审核的数据处理流程

下面我们通过一个完整案例,演示如何使用 Functional API 构建一个带人工审核的数据处理工作流:

python

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

# 定义数据处理任务
@task
def process_data(raw_data: str) -> dict:
    """模拟数据处理任务,耗时2秒"""
    time.sleep(2)
    return {
        "processed": True,
        "result": f"处理后的内容: {raw_data.upper()}",
        "timestamp": time.time()
    }

# 定义人工审核任务
@task
def request_approval(processed_data: dict) -> bool:
    """请求人工审核并返回结果"""
    # 中断工作流,等待人工审核
    approval = interrupt({
        "data": processed_data,
        "instruction": "请审核数据处理结果是否符合要求"
    })
    return approval["is_approved"]

# 定义主工作流
@entrypoint(checkpointer=MemorySaver())
def data_approval_workflow(raw_data: str) -> dict:
    """数据处理与审核工作流"""
    print("开始处理数据...")
    # 处理数据
    processed = process_data(raw_data).result()
    print(f"数据处理完成: {processed['result']}")
    
    # 请求人工审核
    is_approved = request_approval(processed).result()
    
    if is_approved:
        print("审核通过,保存结果")
        return {
            "status": "approved",
            "data": processed
        }
    else:
        print("审核未通过,返回原始数据")
        return {
            "status": "rejected",
            "raw_data": raw_data
        }

# 执行工作流
if __name__ == "__main__":
    config = {
        "configurable": {
            "thread_id": "data_approval_thread"
        }
    }
    
    # 第一次调用:处理数据并等待审核
    result = data_approval_workflow.invoke("示例数据", config=config)
    print(f"工作流状态: {result['status']}")
    
    # 假设人工审核通过,恢复工作流
    if result["status"] == "pending_approval":
        # 模拟人工审核结果
        approval_result = {"is_approved": True}
        resumed_result = data_approval_workflow.invoke(
            Command(resume=approval_result),
            config=config
        )
        print(f"恢复后结果: {resumed_result}")

这个案例展示了 Functional API 的完整工作流程:从数据处理到人工审核,再到恢复执行,整个过程无需复杂的图结构定义,只需使用标准 Python 流程控制,大大降低了开发门槛。

七、总结与进阶方向

通过 Functional API,我们能够以最小的代码改动为应用添加持久化、内存管理、人机交互等高级功能,这对于迭代现有项目或快速开发新功能非常有帮助。其核心优势在于:

  1. 非侵入性:无需重构现有代码,直接添加装饰器即可
  2. 易用性:使用标准 Python 语法,降低学习成本
  3. 强大功能:支持异步执行、状态持久化、人机交互等复杂场景

如果本文对你有帮助,别忘了点赞收藏,关注我,一起探索更高效的开发方式~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

佑瞻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值