LangGraph源码学习路线:第 18 篇 Functional API、entrypoint 与 task源码解剖
核心问题: LangGraph Functional API 如何把普通 Python 函数包装成可 checkpoint、可恢复、可组合的运行时工作流?
源码主线:
@entrypoint→@task→Pregel runtime→checkpoint→task result / final output前置文章: 第 6 篇
StateGraph、第 11 篇Checkpoint、第 17 篇Multi-Agent依赖基线:
langgraph==1.2.7、langchain==1.3.11、langchain-core==1.4.8源码基线: https://github.com/langchain-ai/langgraph/tree/1.2.7
阅读边界: 本文覆盖 Functional API 的 entrypoint、task、future/result、checkpoint 和函数式编排;不展开远程任务队列、分布式 worker 和非 Python SDK。
0. 本篇在源码学习主线中的位置
前面的文章已经解释了底层 Runnable、Tool、StateGraph、Checkpoint 和 Streaming。本篇把这些能力放到一个更接近生产 Agent 的问题里:Functional API / task runtime。
StateGraph / Checkpoint / Multi-Agent ↓Functional API / task runtime ↓Time Travel / Debug Replay本篇只解决:
- 解释 entrypoint 如何成为工作流入口。
- 解释 task 如何获得可恢复语义。
- 解释函数式 API 与 StateGraph 的选择边界。
本篇不展开:
- 不把 Functional API 当普通装饰器语法糖。
- 不讨论外部分布式任务系统。
1. 本篇问题、学习目标与能力边界
核心问题
LangGraph Functional API 如何把普通 Python 函数包装成可 checkpoint、可恢复、可组合的运行时工作流?
学习目标
完成本篇后,读者必须能够:
- 画出Functional API / task runtime的构建期对象关系。
- 解释一次运行时调用如何进入主链。
- 说明关键分支、异常和停止条件。
- 区分公共 API、扩展接口和内部实现。
- 根据旅行规划助手场景做工程选型。
能力边界
| 能力 | 本篇是否覆盖 | 说明 |
|---|---|---|
| 构建期对象组装 | 是 | 解释公开参数如何变成运行时对象 |
| 运行时主链 | 是 | 解释 invoke/stream/tool/task 等主路径 |
| 分支与异常 | 是 | 解释失败、降级、重试、终止边界 |
| 扩展协作 | 是 | 解释与相邻框架组件如何组合 |
| 底层供应商实现 | 否 | 不展开模型、数据库或平台内部实现 |
2. 核心概念与最小心智模型
一句话定义
Functional API 是 LangGraph 用装饰器把函数式控制流接入图运行时、checkpoint 和任务恢复机制的 API 层。
最小心智模型
StateGraph / Checkpoint / Multi-Agent ↓Functional API / task runtime ↓Time Travel / Debug Replay ↓工程化 Agent 能力核心术语
| 术语 | 源码对象 | 语义 | 不要误解为 |
|---|---|---|---|
| entrypoint | @entrypoint | 工作流公开入口 | 普通 Python main 函数 |
| task | @task | 可恢复子步骤 | 线程池任务而已 |
| future/result | task return handle | 延迟取得 task 结果 | 立即同步值 |
| checkpoint | checkpointer | 保存 entrypoint/task 进度 | 日志文件 |
与相邻抽象的边界
| 对象 | 负责什么 | 不负责什么 | 与本篇对象的关系 |
|---|---|---|---|
Runnable | 统一 invoke / stream / batch 协议 | 不决定业务策略 | 提供可组合执行底座 |
StateGraph | 编排状态、节点和边 | 不实现所有外部系统 | 承载复杂 Agent 工作流 |
RunnableConfig | 传递 config、metadata、callbacks | 不保存业务状态 | 让观测和配置沿调用链传播 |
3. 完整执行链路
本篇最重要的链路不是“装饰器把函数包了一层”,而是:@entrypoint 把普通 Python 函数注册成可由 LangGraph 运行时调度的工作流入口,@task 把入口函数内部的子步骤变成有名字、有输入、有 checkpoint 结果缓存的运行时任务。
从最小工作流观察对象变化
from langgraph.func import entrypoint, taskfrom langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
@taskdef search_city(query: str) -> list[dict]: return [{"title": "Osaka food guide", "city": "Osaka"}]
@taskdef write_plan(docs: list[dict]) -> str: return f"Plan with {len(docs)} verified docs"
@entrypoint(checkpointer=checkpointer)def travel_workflow(user_request: str) -> str: docs_future = search_city(f"travel evidence: {user_request}") docs = docs_future.result() answer_future = write_plan(docs) return answer_future.result()
config = {"configurable": {"thread_id": "trip-001"}}result = travel_workflow.invoke("大阪 4 天美食行程", config=config)这段代码表面上仍是普通函数调用:search_city(...) 返回一个对象,随后 .result() 取值,再把取出的值传给 write_plan(...)。但对象形态已经变化:在 entrypoint 运行上下文中,task 调用先返回 future-like handle;真正的任务结果要么来自本轮执行,要么来自同一 thread_id 下已经保存过的 checkpoint。
输入是业务字符串和 RunnableConfig;输出是 entrypoint 的最终返回值。中间对象依次是:原始函数、entrypoint wrapper、task wrapper、task future、task result、checkpoint record。源码阅读时要盯住两个问题:task 调用发生时是否已经处在 entrypoint 上下文中;.result() 取值时是执行任务、等待任务,还是复用 checkpoint 中的既有结果。
框架这样设计,是为了保留 Python 控制流的可读性,同时把“可恢复、可重放、可观测”这些图运行时语义挂到函数调用上。Functional API 的价值不是少写几行 StateGraph,而是把函数式编排翻译成 LangGraph runtime 能理解的步骤边界。
真实对象流转
| 阶段 | 输入对象 | 关键动作 | 输出对象 | 状态变化 |
|---|---|---|---|---|
| 装饰器构建 | Python callable + checkpointer | 记录原函数、名字、checkpoint 能力 | entrypoint wrapper | 尚未执行业务逻辑 |
| 调用入口 | input + RunnableConfig | 提取 thread_id、创建运行上下文 | entrypoint/Pregel 调用上下文 | 准备读取或写入 checkpoint |
| 调用 task | args/kwargs | 生成任务调用记录与 future | future-like handle | 可能登记 pending task |
| 取得结果 | future.result() | 查 checkpoint 或执行原函数 | Python 业务值 | 写入 task 结果 checkpoint |
| 返回入口 | entrypoint return value | 写入最终输出 | 调用方可见结果 | checkpoint 持久化本轮进度 |
entrypoint 主链伪代码
以下是压缩后的控制流,不是源码逐字复制:
def invoke_entrypoint(wrapper, input_value, config): runtime_config = ensure_config(config) thread_id = runtime_config["configurable"].get("thread_id") checkpoint_id = runtime_config["configurable"].get("checkpoint_id")
previous = None if wrapper.checkpointer is not None: previous = wrapper.checkpointer.get_tuple(runtime_config)
context = FunctionalRuntimeContext( entrypoint_name=wrapper.name, thread_id=thread_id, checkpoint_id=checkpoint_id, checkpointer=wrapper.checkpointer, previous_checkpoint=previous, )
with context.as_current(): try: output = wrapper.func(input_value) except BaseException as error: context.record_error(error) raise
if wrapper.checkpointer is not None: checkpoint = context.to_checkpoint(final_output=output) wrapper.checkpointer.put(runtime_config, checkpoint, context.metadata)
return output这段伪代码的输入是 entrypoint wrapper、业务输入和 config;输出是原函数最终返回值。对象形态变化发生在调用入口:普通 input_value 没有被直接传给所有下游组件,而是先与 thread_id、checkpoint_id、checkpointer 合并成运行上下文,后续 task 能从这个上下文判断自己属于哪一次 workflow 执行。
源码观察点是 langgraph/func/__init__.py 中 entrypoint 相关包装逻辑,以及它如何复用 Pregel/checkpoint 基础设施。阅读时不要只看装饰器签名,要继续追 invoke、config 归一化、checkpoint 读写和当前运行上下文的建立。
这里的关键分支是 checkpointer 是否存在、config 是否包含可持久化所需的 thread_id、调用是否指定历史 checkpoint_id。这些分支决定本次调用是普通一次性执行、可恢复执行,还是从历史点继续/重放。
框架把这些能力放在 entrypoint 层,是为了让 workflow 边界成为持久化边界。入口函数内部仍然是 Python 控制流,但入口函数整体对外表现为一个可 invoke、可 stream、可 checkpoint 的运行时对象。
task future/result 链路伪代码
def call_task(task_wrapper, *args, **kwargs): context = FunctionalRuntimeContext.current() if context is None: return task_wrapper.func(*args, **kwargs)
task_key = context.make_task_key( name=task_wrapper.name, args=args, kwargs=kwargs, )
cached = context.lookup_task_result(task_key) if cached is not MISSING: return TaskFuture(task_key=task_key, status="cached", value=cached)
call = TaskCall(task_key, task_wrapper.func, args, kwargs) context.schedule_task(call) return TaskFuture(task_key=task_key, status="pending")
def resolve_task_result(future): context = FunctionalRuntimeContext.current() if future.status == "cached": return future.value
call = context.get_scheduled_task(future.task_key) try: value = call.func(*call.args, **call.kwargs) except BaseException as error: context.record_task_error(future.task_key, error) raise
context.record_task_result(future.task_key, value) return value这段伪代码的输入是被 @task 包装过的函数及其参数;第一次输出不是业务值,而是 TaskFuture。只有 .result() 才把 future 解析成实际业务对象,例如 list[dict] 或 str。
对象形态变化非常关键:search_city(...) 从“函数调用”变成 TaskCall,再变成 TaskFuture,最后才在 .result() 处恢复成 Python 值。checkpoint 记录的也不是“某行 Python 已经执行过”,而是 task key、输入参数、结果和错误这些运行时事实。
源码观察点是 task wrapper 如何检测当前是否处于 entrypoint 上下文。如果不在上下文中,它可以退化成普通函数调用;如果在上下文中,它必须走 future/result 和 checkpoint 语义。这解释了为什么同一个 task 函数既能单独测试,也能在 workflow 中获得恢复能力。
框架这样设计,是为了避免把 Python 控制流强行改写成显式节点/边,同时仍然给每个昂贵或有副作用的步骤一个稳定恢复点。对 Agent 工程来说,检索、模型调用、工具调用、人工审批都适合被 task 包住,因为这些步骤最需要复用结果、定位失败和避免重复副作用。
正常结束条件
一次 Functional API 调用正常结束,至少要满足四件事:entrypoint 返回了业务结果;所有被 .result() 消费的 task 已经产生结果或复用旧结果;checkpoint 写入没有丢失必要的 thread/config 信息;异常没有被错误地伪装成成功输出。
4. 源码地图、关键文件与阅读顺序
核心目录
langgraph/├── graph/├── pregel/├── types.py└── func/
langchain_core/├── runnables/├── tools/├── retrievers.py└── messages/关键文件
| 优先级 | 文件 | 核心对象 | 阅读目的 |
|---|---|---|---|
| 1 | libs/langgraph/langgraph/func/__init__.py | entrypoint / task | 理解 Functional API 入口 |
| 2 | libs/langgraph/langgraph/pregel/main.py | Pregel | 理解运行时复用 |
| 3 | libs/langgraph/langgraph/pregel/_checkpoint.py | checkpoint helpers | 理解恢复点 |
| 4 | libs/checkpoint/langgraph/checkpoint/base/__init__.py | BaseCheckpointSaver | 理解持久化协议 |
推荐阅读顺序
1. 先读官方概念文档,确认公共契约。2. 再读公开 API 或装饰器入口。3. 顺着构建期对象进入 runtime。4. 追踪一次 invoke / stream / tool call / task call。5. 单独检查异常、重试、恢复和扩展点。6. 最后回到旅行规划助手做工程判断。不建议的阅读顺序
不建议直接从最底层 private helper 开始读。源码学习的第一目标是建立调用链,而不是收集函数名。先找公开入口,再沿参数和返回值追下去,才不会把内部实现误当成稳定 API。
5. 对象模型、继承关系与协议边界
核心对象关系
Public API / Decorator / Tool Protocol ↓Runtime wrapper ↓State / Config / Context ↓Storage / Model / Tool / Graph Runtime对象职责
| 对象 | 生命周期 | 输入 | 输出 | 核心职责 |
|---|---|---|---|---|
| 公开入口 | 构建期或调用期 | 用户参数 | runtime object | 提供稳定 API |
| runtime context | 单次调用 | config/state | 下游上下文 | 传递配置、状态和观测信息 |
| 协议对象 | 单步执行 | 上游对象 | 下游可消费结果 | 维持模块边界 |
| 扩展点 | 构建期注册、运行时触发 | request/response | 修改或观察后的结果 | 插入业务控制逻辑 |
协议边界
公共 API 负责:给业务代码稳定入口。 不负责:暴露所有内部调度细节。
运行时协议 负责:让状态、配置、工具、模型和持久化协作。 不负责:替业务判断所有策略。
扩展接口 负责:允许业务插入可维护的定制逻辑。 不负责:保证错误扩展仍然安全。稳定接口与内部实现
| 类型 | 对象 | 文章中的使用原则 |
|---|---|---|
| 公共 API | 官方文档列出的函数、类和装饰器 | 可以用于工程示例 |
| 扩展接口 | middleware、store、retriever、task、Command 等协议 | 说明契约和约束 |
| 内部实现 | private helper、runner、loop 细节 | 只用于解释,不建议业务依赖 |
6. 源码阅读策略与证据标准
本篇阅读策略
先找公开入口 ↓确认输入输出类型 ↓沿调用方追到核心实现 ↓记录状态与对象变化 ↓检查分支、异常和结束条件 ↓回到设计目的证据等级
| 标记 | 含义 | 写作要求 |
|---|---|---|
| 源码事实 | 当前正式版源码可以证明 | 附源码链接 |
| 官方契约 | 官方文档或 API Reference 明确说明 | 附官方链接 |
| 简化伪代码 | 压缩真实控制流 | 标注不是源码逐字复制 |
| 作者推断 | 根据调用链得出的理解 | 明确使用“从调用关系可以推断” |
| 工程建议 | 面向项目实践的建议 | 说明适用条件 |
本篇证据清单
| 结论 | 证据类型 | 文件或文档 | 定位 |
|---|---|---|---|
| 公共入口存在稳定契约 | 官方契约 | 官方 docs / reference | 第 14 章链接 |
| 核心协议对象存在源码定义 | 源码事实 | 官方源码仓库 | 第 4 章关键文件 |
| 运行时分支需要按协议处理 | 作者推断 | 调用链和源码结构 | 第 9 章 |
| 工程选型依赖场景边界 | 工程建议 | 旅行规划助手场景 | 第 11 章 |
7. 构建期源码解剖
本章回答:@entrypoint 和 @task 在函数真正执行之前做了什么。重点不是“装饰器语法”,而是普通 callable 如何变成带名字、配置、checkpoint 和运行时上下文的协议对象。
entrypoint 包装
def build_entrypoint(func=None, *, checkpointer=None, name=None): def decorate(user_func): workflow_name = name or user_func.__name__ wrapper = EntrypointWrapper( name=workflow_name, func=user_func, checkpointer=checkpointer, ) wrapper.invoke = lambda input_value, config=None: invoke_entrypoint( wrapper, input_value, config ) wrapper.stream = lambda input_value, config=None, **kwargs: stream_entrypoint( wrapper, input_value, config, **kwargs ) return wrapper
if func is None: return decorate return decorate(func)这段伪代码的输入是用户写的 Python 函数和可选 checkpointer;输出是 entrypoint wrapper。对象形态从 callable 变成“可 invoke/stream 的运行时入口”,业务函数本身没有丢失,而是作为 wrapper 的 func 字段被保存。
源码观察点是 entrypoint 如何支持两种写法:@entrypoint 和 @entrypoint(checkpointer=...)。如果第一个参数已经是函数,立即装饰;否则先返回 decorate,等真正函数传入后再构建 wrapper。
这里最重要的设计不是 wrapper 长什么样,而是 checkpointer 被绑定在入口边界上。只有入口知道一次 workflow 调用的 thread/config,task 才能把自己的结果挂到同一条执行线里。
框架这样设计,是为了把持久化能力放在工作流生命周期边界,而不是让每个 task 自己决定写到哪里。这样可以统一处理 thread_id、checkpoint_id、metadata、debug event 和错误传播。
task 包装
def build_task(func=None, *, name=None): def decorate(user_func): task_name = name or user_func.__name__ wrapper = TaskWrapper(name=task_name, func=user_func)
def task_call(*args, **kwargs): context = FunctionalRuntimeContext.current() if context is None: return user_func(*args, **kwargs)
task_key = context.make_task_key(task_name, args, kwargs) return context.create_future( task_key=task_key, task_name=task_name, func=user_func, args=args, kwargs=kwargs, )
wrapper.__call__ = task_call return wrapper
if func is None: return decorate return decorate(func)这段伪代码的输入是用户函数;输出是 task wrapper。对象形态变化不是“函数变成线程任务”,而是“函数调用变成可被运行时识别的 task 调用记录”。在 entrypoint 外部调用时,它可以像普通函数一样返回业务值;在 entrypoint 内部调用时,它返回 future-like handle。
源码观察点是当前运行上下文的判断。task 本身并不知道 thread_id,也不应该直接读写 checkpoint;它依赖 entrypoint 建好的上下文来生成稳定 task key、登记调用、缓存结果和传播异常。
这里的设计让 task 保持可测试性。你可以直接调用 search_city("osaka") 做单元测试,也可以在 entrypoint 中调用它,让它获得 checkpoint 语义。
checkpoint 绑定
def prepare_checkpoint_context(wrapper, config): runtime_config = ensure_config(config) configurable = runtime_config.setdefault("configurable", {})
thread_id = configurable.get("thread_id") if wrapper.checkpointer is not None and not thread_id: raise ValueError("checkpointed entrypoint requires configurable.thread_id")
checkpoint_tuple = None if wrapper.checkpointer is not None: checkpoint_tuple = wrapper.checkpointer.get_tuple(runtime_config)
return FunctionalRuntimeContext( entrypoint_name=wrapper.name, config=runtime_config, checkpointer=wrapper.checkpointer, previous_checkpoint=checkpoint_tuple, )这段伪代码的输入是 entrypoint wrapper 和调用 config;输出是本次调用上下文。对象形态从普通 dict config 变成携带 checkpointer、thread_id、旧 checkpoint 的 runtime context。
源码观察点是 config 的 configurable 字段。LangGraph 的持久化不是只靠函数名定位,而是依赖 thread 维度来区分不同会话/任务;如果没有稳定 thread_id,checkpoint 就无法安全地表示“这一次工作流的历史”。
框架把 checkpoint 读取放在业务函数执行之前,是为了支持恢复与重放:task 在 .result() 时可以先查旧结果,而不是盲目重新调用外部工具。
8. 运行时主链源码解剖
本章回答:一次 travel_workflow.invoke(...) 进入运行时后,输入、future、result 和 checkpoint 如何真实流转。
entrypoint invoke 主链
def run_functional_workflow(wrapper, input_value, config): context = prepare_checkpoint_context(wrapper, config)
with context.as_current(): context.emit_event("entrypoint_start", input=input_value) output = wrapper.func(input_value) context.emit_event("entrypoint_end", output=output)
checkpoint = context.build_checkpoint( values={"__entrypoint_output__": output}, task_results=context.task_results, pending_tasks=context.pending_tasks, )
if context.checkpointer is not None: context.checkpointer.put(context.config, checkpoint, metadata=context.metadata)
return output输入是 entrypoint wrapper、用户业务输入和 config;输出是业务结果,例如字符串行程。中间对象从 input_value 变成当前上下文、task 结果集合、checkpoint,最后再变回调用方可见的 Python 值。
源码观察点是 entrypoint 函数体执行时的“当前上下文”。@task 能返回 future,不是因为 Python 函数天然支持 future,而是因为调用发生在这个上下文里,task wrapper 可以把自己登记到本次 workflow。
框架把 checkpoint 写在入口结束处,是为了把 task result 和 final output 放进同一条执行记录。这样下次同 thread 调用时,运行时能判断哪些步骤已经完成,哪些步骤需要继续执行。
.result() 触发 task 执行或复用
def task_future_result(future): context = FunctionalRuntimeContext.current() task_key = future.task_key
if context.has_checkpointed_result(task_key): value = context.get_checkpointed_result(task_key) context.emit_event("task_result_reused", task_key=task_key) return value
if context.has_in_memory_result(task_key): return context.task_results[task_key]
call = context.scheduled_tasks[task_key] context.emit_event("task_start", task_key=task_key) value = call.func(*call.args, **call.kwargs) context.task_results[task_key] = value context.emit_event("task_end", task_key=task_key, result=value) return value这段伪代码的输入是 future;输出是具体业务值。future 本身只是“怎样取得结果”的句柄,不是结果本身。.result() 是语义边界:它可能复用旧 checkpoint,也可能执行原函数,也可能重新抛出之前记录的错误。
对象形态变化是 TaskFuture -> Python value。对读源码来说,这一行最值得追,因为很多恢复语义都藏在 result 解析里,而不是 task 装饰器定义处。
框架这样设计,是为了把“声明要做一个任务”和“消费任务结果”分开。这样未来可以支持并发、缓存、恢复、stream 事件,而用户代码仍然像同步 Python 一样可读。
恢复执行链路
def resume_from_checkpoint(wrapper, input_value, config): context = prepare_checkpoint_context(wrapper, config)
for saved_task in context.previous_task_results: context.task_results[saved_task.key] = saved_task.value
with context.as_current(): output = wrapper.func(input_value)
context.write_new_checkpoint(output) return output输入仍然是同一个 entrypoint 调用,但 config 指向已有 thread 或 checkpoint。输出仍是业务结果;不同的是,部分 task 结果来自旧 checkpoint,而不是本轮重新执行。
源码观察点是“恢复不是从 Python 栈帧恢复”。Python 函数会重新进入,但当它再次走到同一个 task .result() 时,运行时可以用 task key 命中旧结果,从而跳过实际副作用。
框架这样设计,是因为 Python 进程、线程和栈帧本身不可长期持久化;可持久化的是输入、任务标识、结果、错误、next 信息和 metadata。
9. 关键分支、异常与边界
本章回答:Functional API 在哪些情况下走普通函数语义,哪些情况下进入 durable runtime 语义,以及失败如何传播。
task 在 entrypoint 外部调用
def call_task_or_plain_function(task_wrapper, *args, **kwargs): context = FunctionalRuntimeContext.current() if context is None: return task_wrapper.func(*args, **kwargs)
return context.create_future( task_name=task_wrapper.name, func=task_wrapper.func, args=args, kwargs=kwargs, )输入是 task wrapper 和普通函数参数;输出可能是业务值,也可能是 future。分支条件只有一个:当前是否存在 Functional runtime context。
这个分支解释了一个常见误区:@task 不是把函数永久变成异步任务队列。它只有在 entrypoint 调用链里才获得 checkpoint/future 语义;脱离上下文时,保持普通函数可调用性更利于测试和复用。
框架这样设计,是为了降低迁移成本。已有 Python 函数可以逐步标记成 task,而不是必须立刻改造成图节点或远程 worker。
checkpoint 缺失或 config 不完整
def validate_checkpoint_config(checkpointer, config): if checkpointer is None: return "stateless"
configurable = ensure_config(config).get("configurable", {}) thread_id = configurable.get("thread_id") if not thread_id: raise ValueError("checkpointed workflow requires thread_id")
return "durable"输入是 checkpointer 和 config;输出是运行模式。没有 checkpointer 时是一次性函数式执行;有 checkpointer 且有 thread_id 时才是可恢复工作流。
源码观察点是错误应尽早暴露。如果等到 task 写 checkpoint 时才发现 thread_id 缺失,错误位置会离用户调用点太远,也更难解释为什么恢复失败。
框架这样设计,是为了把持久化边界显式化。checkpoint 不是“自动日志”,它需要稳定的会话键和可序列化状态。
task 异常与恢复边界
def execute_task_call(context, call): try: value = call.func(*call.args, **call.kwargs) except BaseException as error: context.record_task_error(call.key, error) context.emit_event("task_failed", task_key=call.key, error=repr(error)) raise
context.record_task_result(call.key, value) return value输入是已登记的 task call;输出要么是 task value,要么是异常。异常不是被吞掉后变成空结果,而是记录后继续向上抛出。
对象形态变化是 Exception -> task error metadata / debug event -> raised exception。这能保证 trace 和 checkpoint 知道失败发生在哪里,同时不让坏状态伪装成成功状态继续进入后续步骤。
从调用关系可以推断,框架保留异常传播,是为了让用户明确选择 retry、fallback、interrupt 或人工修复,而不是由 runtime 猜测业务上应该怎么降级。
不能自动恢复的内容
def should_wrap_as_task(operation): if operation.has_external_side_effect and not operation.is_idempotent: return "wrap_and_add_business_idempotency_key" if operation.is_expensive_or_slow: return "wrap_as_task" if operation.is_pure_local_calculation: return "plain_python_is_ok"输入是工程操作的性质;输出是建模建议。这个伪代码不是框架 API,而是读源码后应该形成的工程判断。
Functional API 能复用 task result,但不能自动撤销已经发出的邮件、支付、数据库写入或第三方 API 调用。checkpoint 记录的是 LangGraph 运行时事实,不是外部系统事务日志。
因此高风险工具调用要额外设计业务幂等键、确认步骤或人工中断点。框架提供恢复点,但不会替你证明副作用安全。
10. 扩展机制与框架协作
本章回答:Functional API 如何与 StateGraph、Time Travel、Streaming 和业务工程边界协作。
函数式 workflow 组合
@entrypoint(checkpointer=checkpointer)def answer_trip(request: str) -> str: parsed = parse_request(request).result()
if parsed["needs_search"]: docs = search_docs(parsed["query"]).result() else: docs = []
draft = write_draft(parsed, docs).result() reviewed = review_plan(draft).result() return reviewed输入是一个业务请求字符串;输出是最终答案。对象形态沿着 Python 控制流变化:request -> parsed dict -> docs list -> draft str -> reviewed str,但每个被 @task 包住的步骤都额外产生 task key、future、result 和 checkpoint 记录。
源码观察点是 if/else、for 循环和普通局部变量仍然属于 Python,不需要显式画边。真正进入 LangGraph runtime 语义的是 entrypoint 边界和 task result 边界。
框架这样设计,是为了让线性或轻分支 workflow 保持代码直观。复杂路由、并行状态合并和多 actor 协作仍然更适合 StateGraph。
与 StateGraph 协作
@taskdef run_planner_graph(planner_input: dict) -> dict: return planner_graph.invoke(planner_input)
@entrypoint(checkpointer=checkpointer)def travel_agent(request: str) -> str: intent = parse_request(request).result() plan_state = run_planner_graph(intent).result() return render_answer(plan_state).result()输入从 entrypoint 进入,局部复杂规划交给已有 StateGraph,输出再回到函数式 workflow。对象形态是 request -> intent dict -> graph state dict -> final str。
源码观察点是两层 checkpoint 的边界要分清:外层 Functional API 的 task 记录 run_planner_graph 这个步骤的结果;内层 StateGraph 如果也配置 checkpointer,则会有自己的节点级状态历史。
框架这样设计,是为了允许渐进式建模:简单编排用函数,复杂子系统用图。工程上不要为了使用 Functional API 而把复杂图路由塞进一个巨大的 Python 函数。
与 Time Travel 协作
def debug_functional_run(workflow, thread_id): config = {"configurable": {"thread_id": thread_id}} history = list(workflow.get_state_history(config))
bad_snapshot = choose_snapshot(history, where="after_search") fork_config = workflow.update_state( bad_snapshot.config, {"search_result_override": [{"title": "corrected evidence"}]}, )
return workflow.invoke(None, config=fork_config)输入是 workflow 和 thread_id;输出是从修正分支继续得到的结果。对象形态从 checkpoint history 里的 StateSnapshot 变成 update 后的新 config,再进入 replay/invoke。
这段伪代码说明 Functional API 与 Time Travel 的协作点不是“修改 Python 局部变量”,而是修改 checkpoint 表示的运行时状态或输入。已经写入外部系统的副作用不会因为 fork/replay 自动撤销。
框架这样设计,是为了让调试和修复围绕持久化事实展开。你能回到某个任务结果之后继续执行,也能修正可写状态字段,但不能指望 runtime 还原任意 Python 栈内存。
扩展选择边界
| 场景 | 建议 |
|---|---|
| 线性流程、少量分支、希望用 Python 表达控制流 | Functional API + task |
| 节点多、条件边复杂、需要 reducer 合并状态 | StateGraph |
| 外部工具昂贵或有副作用 | 用 task 包住,并设计幂等键 |
| 需要排查历史执行 | checkpointer + Time Travel |
| 需要观察执行过程 | stream/debug events 或 LangSmith trace |
11. 工程决策与适用场景
适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 旅行规划助手生产化 | 是 | 需要状态、工具、检索、观测和恢复闭环 |
| 一次性脚本问答 | 否 | 直接调用模型更简单 |
| 高风险工具调用 | 是 | 可以加入 guardrail、interrupt、trace |
| 大规模知识问答 | 是 | 可以把 retrieval、rerank、grounding 拆清楚 |
| 简单静态 FAQ | 否 | 复杂运行时收益不高 |
工程决策表
| 决策点 | 推荐选择 | 前提 | 风险 |
|---|---|---|---|
| 是否抽象成独立模块 | 有稳定职责时抽象 | 边界清楚 | 过早拆分增加复杂度 |
| 是否持久化 | 需要恢复或复盘时持久化 | 有 thread_id / user_id | 隐私和清理成本 |
| 是否加入 guardrail | 涉及外部工具或敏感数据时加入 | 有明确策略 | 误杀正常请求 |
| 是否流式观测 | 用户等待时间长时加入 | 前端能消费事件 | 泄露内部状态 |
性能、可靠性与安全边界
性能:额外抽象会增加序列化、检索、trace 和存储成本。可靠性:可恢复机制要求状态 schema、幂等工具和错误分类清楚。安全:metadata、state、tool args、retrieved docs 都可能包含敏感信息。可观测性:至少记录 thread_id、node/tool、策略命中、错误和耗时。12. 常见误区与源码纠正
误区:把公共 API 当成全部源码机制
错误原因:
公开 API 很短,看起来像全部逻辑都在这一层。
源码事实:
公开 API 通常只负责归一化和装配;真正的运行语义在 runtime、协议对象和扩展点之间流转。工程影响:
只读公开 API 会误判能力边界,导致扩展时依赖错误位置。
误区:把所有中间结果都写进模型上下文
错误原因:
Agent 运行时的 state、memory、documents、trace 看起来都像“上下文”。
源码事实:
state、store、Document、metadata、trace、prompt messages 是不同协议;只有经过明确注入的内容才进入模型上下文。工程影响:
如果不区分这些协议,轻则上下文膨胀,重则泄露敏感数据或污染长期记忆。
误区:异常都应该在框架层吞掉
错误原因:
Agent 产品希望“永远给用户一个答案”。
源码事实:
可恢复异常可以转成 retry、fallback、interrupt;不可恢复异常必须上抛并进入 trace。工程影响:
吞掉异常会让错误状态继续进入后续节点,调试成本比显式失败高得多。
13. 最终心智模型与掌握检查
构建期心智模型
Python callable ↓@entrypoint / @task wrapper ↓entrypoint context + task key ↓checkpointer / stream / invoke 能力运行时心智模型
workflow.invoke(input, config) ↓读取 thread checkpoint ↓执行 entrypoint Python 控制流 ↓task(...) 返回 future ↓future.result() 执行或复用 task result ↓写入 checkpoint 并返回 final output分支与异常心智模型
task 在 entrypoint 外 → 普通函数语义task 在 entrypoint 内 → future/result + checkpoint 语义已有 task result → 复用 checkpoint,避免重复副作用task 执行失败 → 记录 task error 并继续抛出外部副作用 → 需要业务幂等键,checkpoint 不负责撤销一句话总结
Functional API / task runtime通过构建期协议装配形成可运行对象,运行时沿公开入口进入核心执行链,使用分支机制处理边界,并通过扩展点与 LangChain / LangGraph 的状态、工具、模型、存储和观测能力协作。
掌握检查
- 能说清公开入口与真实执行入口的区别。
- 能画出构建期对象关系。
- 能画出运行时对象流转。
- 能解释至少一个核心函数的伪代码。
- 能指出同步、异步、流式或批量路径的边界。
- 能说明异常在哪里抛出、在哪里处理。
- 能说明正常结束和保护性终止的区别。
- 能区分公共 API、扩展接口和内部实现。
- 能根据旅行规划助手场景判断是否应该使用该抽象。
14. 参考资料与下一篇衔接
官方概念文档
-
LangGraph Functional API
https://docs.langchain.com/oss/python/langgraph/functional-api -
LangGraph Persistence
https://docs.langchain.com/oss/python/langgraph/persistence -
Thinking in LangGraph
https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph
官方 API Reference
官方源码
-
libs/langgraph/langgraph/func/__init__.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/func/__init__.py -
libs/langgraph/langgraph/pregel/main.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/pregel/main.py -
libs/checkpoint/langgraph/checkpoint/base/__init__.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/checkpoint/base/__init__.py
下一篇衔接
下一篇进入:
第 19 篇:Time Travel、update_state 与 Debug Replay 源码解剖需要继续回答:
这项能力在旅行规划助手中应该放在哪一层?它和前一篇能力如何协作?哪些边界需要通过源码证据确认?