7164 字
36 分钟
LangGraph 源码深潜:Functional API、entrypoint 与 task 机制解剖

LangGraph源码学习路线:第 18 篇 Functional API、entrypoint 与 task源码解剖#

核心问题: LangGraph Functional API 如何把普通 Python 函数包装成可 checkpoint、可恢复、可组合的运行时工作流?

源码主线: @entrypoint@taskPregel runtimecheckpointtask result / final output

前置文章: 第 6 篇 StateGraph、第 11 篇 Checkpoint、第 17 篇 Multi-Agent

依赖基线: langgraph==1.2.7langchain==1.3.11langchain-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、可恢复、可组合的运行时工作流?

学习目标#

完成本篇后,读者必须能够:

  1. 画出Functional API / task runtime的构建期对象关系。
  2. 解释一次运行时调用如何进入主链。
  3. 说明关键分支、异常和停止条件。
  4. 区分公共 API、扩展接口和内部实现。
  5. 根据旅行规划助手场景做工程选型。

能力边界#

能力本篇是否覆盖说明
构建期对象组装解释公开参数如何变成运行时对象
运行时主链解释 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/resulttask return handle延迟取得 task 结果立即同步值
checkpointcheckpointer保存 entrypoint/task 进度日志文件

与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
Runnable统一 invoke / stream / batch 协议不决定业务策略提供可组合执行底座
StateGraph编排状态、节点和边不实现所有外部系统承载复杂 Agent 工作流
RunnableConfig传递 config、metadata、callbacks不保存业务状态让观测和配置沿调用链传播

3. 完整执行链路#

本篇最重要的链路不是“装饰器把函数包了一层”,而是:@entrypoint 把普通 Python 函数注册成可由 LangGraph 运行时调度的工作流入口,@task 把入口函数内部的子步骤变成有名字、有输入、有 checkpoint 结果缓存的运行时任务。

从最小工作流观察对象变化#

from langgraph.func import entrypoint, task
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
@task
def search_city(query: str) -> list[dict]:
return [{"title": "Osaka food guide", "city": "Osaka"}]
@task
def 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
调用 taskargs/kwargs生成任务调用记录与 futurefuture-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_idcheckpoint_id、checkpointer 合并成运行上下文,后续 task 能从这个上下文判断自己属于哪一次 workflow 执行。

源码观察点是 langgraph/func/__init__.pyentrypoint 相关包装逻辑,以及它如何复用 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/

关键文件#

优先级文件核心对象阅读目的
1libs/langgraph/langgraph/func/__init__.pyentrypoint / task理解 Functional API 入口
2libs/langgraph/langgraph/pregel/main.pyPregel理解运行时复用
3libs/langgraph/langgraph/pregel/_checkpoint.pycheckpoint helpers理解恢复点
4libs/checkpoint/langgraph/checkpoint/base/__init__.pyBaseCheckpointSaver理解持久化协议

推荐阅读顺序#

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 协作#

@task
def 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. 参考资料与下一篇衔接#

官方概念文档#

  1. LangGraph Functional API
    https://docs.langchain.com/oss/python/langgraph/functional-api

  2. LangGraph Persistence
    https://docs.langchain.com/oss/python/langgraph/persistence

  3. Thinking in LangGraph
    https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph

官方 API Reference#

  1. entrypoint / task
    https://reference.langchain.com/python/langgraph/func/

  2. Pregel
    https://reference.langchain.com/python/langgraph/pregel/

官方源码#

  1. libs/langgraph/langgraph/func/__init__.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/func/__init__.py

  2. libs/langgraph/langgraph/pregel/main.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/pregel/main.py

  3. 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 源码解剖

需要继续回答:

这项能力在旅行规划助手中应该放在哪一层?
它和前一篇能力如何协作?
哪些边界需要通过源码证据确认?
LangGraph 源码深潜:Functional API、entrypoint 与 task 机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/18_langgraph_functional_api_entrypoint_task_source_deep_dive/
作者
Jupiter
发布于
2026-03-22
许可协议
CC BY-NC-SA 4.0