学习笔记 · Obsidian
LangGraph Python 基础、运行时与 API 选择
/oss/python/langgraph是 overview 的规范入口别名,浏览器最终落到/overview。
核心结论
LangGraph 是面向长时间运行、有状态 agent 的低层编排框架与运行时。它不替你抽象提示词、模型或固定 agent 架构,而是解决更底层的问题:
- 在一个流程中混合确定性代码与 LLM 决策;
- 显式管理状态、分支、循环和并行;
- 在节点边界持久化并从失败或人工暂停处恢复;
- 持续流式暴露执行过程;
- 把人工审批、长期任务和可追踪状态作为一等能力。
如果只是常规“模型调用工具直到回答”的 agent,优先从 LangChain 的 create_agent 开始;当需要显式状态机、复杂恢复语义、确定性控制或定制编排时再使用 LangGraph。Deep Agents 和 LangChain agent 都构建在 LangGraph 之上。
安装与最低认知
当前 Python 栈以 Python 3.10+ 为基线:
pip install -U langgraph
文档示例通常同时使用 LangChain 的模型、消息和工具接口,因此实际项目常见安装组合是:
pip install -U langgraph langchain
模型提供商包需要单独安装。LangGraph 本身不要求必须使用 LangChain,也不绑定具体模型提供商。
在开始前至少要理解:
- 模型如何接收消息并返回 AI 消息;
- 工具 schema、工具调用与 ToolMessage 的配对;
- 节点副作用为什么需要幂等;
- thread、checkpoint 与恢复不是普通函数调用语义。
“用 LangGraph 思考”的五步法
1. 先画真实业务流程
不要先画一个“agent”盒子。先把待自动化过程拆为离散步骤,每个步骤只做一类事情,例如:
- LLM 步骤:分类、推理、生成;
- 数据步骤:搜索、读取数据库、加载客户资料;
- 动作步骤:发邮件、写数据库、创建工单;
- 人工步骤:审批、补充信息、纠正输出。
每个节点应有清晰输入、输出、失败模式和是否允许重试的定义。
2. 决定哪些路径由代码控制,哪些由模型决定
- 固定顺序、权限、金额阈值、审计要求:用确定性边或代码判断;
- 意图识别、工具选择、未知子任务拆分:可以交给模型;
- 不应让模型控制的内容仍由服务端控制,例如权限、租户、真实资源 ID 和最终写操作。
3. 设计共享 State
State 是节点间共享的原始业务数据,而不是拼好的提示词。只存:
- 后续步骤无法重建的输入;
- 多个下游节点都要使用的结果;
- 重新获取成本高的数据;
- 恢复、审计或人工复核需要的数据。
能推导出来的内容在节点内按需计算。提示词应在调用模型时格式化,不要写入 State。这样提示词调整不会破坏已有 checkpoint,调试时也能看清真实数据。
4. 按失败责任设计节点
| 错误类型 | 处理者 | 推荐策略 |
|---|---|---|
| 网络抖动、限流、暂时性 5xx | 系统 | RetryPolicy,指数退避与抖动 |
| 工具失败、模型可修正的格式错误 | 模型 | 把错误写回状态并重新规划 |
| 缺少账号、审批或澄清信息 | 用户 | interrupt 暂停并等待输入 |
| 重试耗尽但可补偿 | 工作流 | error_handler 进入补偿分支 |
| 未知程序错误 | 开发者 | 向外抛出,保留 trace 与 checkpoint |
节点越小,checkpoint 越细,失败后重复工作越少,单元测试和观测也越容易;但过度拆分会增加状态接口与图复杂度。外部服务、不同重试策略和需要单独观测的步骤通常值得拆开。
5. 再连接图
节点完成工作,边决定下一步。动态路由可以放在 conditional edge 中,也可以由节点返回 Command 同时更新状态与决定去向。人工暂停要求父图配置 checkpointer,并在调用时提供稳定的 thread_id。
Workflow 与 Agent 的边界
- Workflow:路径由代码预先规定,强调可预测、可测试和稳定顺序。
- Agent:模型动态决定工具与下一步,适合问题与解法都不可完全预知的场景。
实际生产系统通常是混合体:外层是确定性的工作流边界,局部节点内部允许 agent 自主选择工具。
常见编排模式
| 模式 | 结构 | 适用场景 | 主要风险 |
|---|---|---|---|
| Prompt chaining | 上一步输出进入下一步 | 可拆分且每步可验证的任务 | 上游错误逐步放大 |
| Parallelization | 多个独立分支并行后汇总 | 降低延迟、多视角评分 | 合并顺序与并发限额 |
| Routing | 先分类再进入专用路径 | 售前、退款、技术支持等分流 | 分类错误导致错误能力边界 |
| Orchestrator-worker | 动态拆任务、派发 worker、汇总 | 子任务数量事先未知 | 成本失控、重复任务、汇总偏差 |
| Evaluator-optimizer | 生成、评价、反馈、迭代 | 有明确质量标准但需多轮改进 | 无终止条件形成死循环 |
| ReAct agent | 模型与工具持续反馈循环 | 解法不可预先确定 | 工具权限、循环上限、提示注入 |
Orchestrator-worker 的动态 fan-out 使用 Send:每个 worker 获得自己的输入状态,结果通过 reducer 聚合到共享键。ToolNode 是常用的预构建工具执行节点,负责并行工具调用、错误处理和状态注入;工具只能看到传给 ToolNode 的状态。
Graph API 与 Functional API
两套 API 共享同一个 Pregel 运行时,可以在同一应用里互相调用,也都支持 persistence、streaming、HITL 和 memory。差异主要是表达方式,不是能力等级。
| 维度 | Graph API | Functional API |
|---|---|---|
| 控制流 | 显式 nodes、edges、shared state | Python if、for、函数调用 |
| 状态 | 显式 schema 与 reducer | 函数作用域状态,previous 保存跨调用值 |
| checkpoint | 每个 super-step 形成新 checkpoint | task 结果写入 entrypoint 对应 checkpoint |
| 可视化 | 静态结构可直接画图 | 运行时动态生成,不支持完整静态图 |
| 并行与汇合 | 分支、Send、reducer 表达清晰 | 并发启动 task future 后统一等待 |
| 改造成本 | 需要把流程重构为图 | 对既有过程式代码改动较小 |
| 团队协作 | 节点接口和图结构适合多人协作 | 适合局部、线性或快速原型 |
何时选 Graph API
- 多个决策点与复杂条件分支;
- 并行路径需要汇合;
- 多组件共享状态;
- 需要可视化、审计或团队共同维护;
- 希望节点级别配置超时、重试、缓存与错误处理。
何时选 Functional API
- 给既有过程式代码增加持久化、流式和 HITL;
- 线性流程或少量简单分支;
- 状态天然局限在函数内;
- 快速验证想法,避免先定义完整图 schema。
组合与迁移
- Graph 节点可以调用 Functional entrypoint;
- Functional entrypoint 可以调用已编译 graph;
- 当过程式流程出现多处分支、共享状态或可视化需求时,从 Functional 拆为 StateGraph;
- 当一个图只是线性包装且节点划分没有独立恢复价值时,可收敛为 Functional API。
Functional API 的执行契约
两个原语
- @entrypoint:工作流入口,只接受一个位置参数;多参数应包装为字典。通常传入 checkpointer 才能启用持久化和 HITL。
- @task:可并发、可重试、结果可 checkpoint 的离散工作单元。task 只能从 entrypoint、其他 task 或 StateGraph node 内调用,不能从应用主代码直接调用。
task 调用立即返回 future;同步代码用 result,异步代码用 await 获取结果。
哪些工作必须放进 task
- API、数据库写入、发邮件等副作用;
- 当前时间、随机数等非确定性输入;
- 长耗时且恢复时不应重复计算的操作;
- 希望并行、独立重试或单独观测的操作;
- interrupt 前后会受 replay 影响的工作。
entrypoint、task 的输入输出必须可序列化;以 JSON 兼容的字典、列表、字符串、数字和布尔值为主。
replay 不是从暂停代码行继续
恢复时 Functional API 会从 entrypoint 开头重放,但已完成的 task 和 subgraph 结果从 checkpoint 读取,不重新执行。由此得到三个硬约束:
- task 与 interrupt 的顺序不能随意调整;
- 非确定性控制流必须由输入或已 checkpoint 的 task 结果决定;
- 一个未完成的 task 可能再次执行,写操作仍必须具备幂等键或查重逻辑。
@entrypoint.final 可以把“返回给调用者的值”和“保存到 previous、供下次调用使用的值”分开。
Functional API 已覆盖的能力
- task 并行执行;
- 调用 graph 或其他 entrypoint;
- 自定义 streaming;
- retry、async-only timeout 和 task cache;
- 从错误恢复;
- interrupt、工具调用审批;
- previous 形式的短期记忆;
- Store 形式的跨 thread 长期记忆。
Pregel 运行时模型
StateGraph.compile 与 @entrypoint 最终都会生成 Pregel 实例。运行时把 actor 与 channel 组合起来,采用 Bulk Synchronous Parallel 模型。
每个 super-step 有三个阶段:
- Plan:找出本轮因输入或上轮 channel 更新而被激活的 actor;
- Execution:并行执行本轮 actor;本轮写入对同轮其他 actor 不可见;
- Update:统一把写入应用到 channel,然后规划下一轮。
当没有 actor 被激活,或达到最大 step 数时结束。这解释了:
- 同一 super-step 的节点天然并行;
- reducer 在汇合并行写入时决定如何合并;
- checkpoint、time travel 和恢复都以 super-step 边界为核心;
- recursion_limit 统计的是 super-step,不是单个节点调用次数。
Channel 类型
| Channel | 行为 | 典型用途 |
|---|---|---|
| LastValue | 新值覆盖旧值 | 普通输入、输出与步间传值 |
| Topic | 发布订阅,可累积或去重 | 多生产者、多值传播 |
| BinaryOperatorAggregate | 用二元操作持续聚合 | 计数、总和、累积结果 |
| DeltaChannel | 每步只存增量,读取时重建 | 长对话消息等持续增长字段 |
DeltaChannel 从 LangGraph 1.2 起为 beta。其 bulk reducer 必须满足结合律且为纯函数;稳定 ID 应在写入前生成,不能在重建 reducer 中生成。设置 snapshot_frequency 可以用额外完整快照换取有界读取延迟。已有 thread 写入 DeltaChannel 格式后,旧版本运行时无法直接读取,回滚前必须迁移或丢弃相关 checkpoint。
Quickstart 背后的最小闭环
官方计算器示例的 Graph API 结构是:
- messages 与调用次数构成 State;
- model node 决定回答还是发起 tool call;
- tool node 执行工具并返回 ToolMessage;
- conditional edge 在 tool node 与 END 之间选择;
- 工具结果返回 model node,形成循环。
Functional API 实现同一闭环时,把工具和模型调用包成 task,在 entrypoint 内使用普通 while 与 if 控制循环。二者行为相同,差别只在结构表达。
选型速查
- 能用 LangChain create_agent 满足吗?能则先用高层 API。
- 是否需要显式图、并行汇合、多处条件分支或节点级恢复?是则选 Graph API。
- 是否是在既有 Python 流程上补 durable execution?是则先选 Functional API。
- 是否存在外部写操作、人工暂停或恢复?无论 API,都先设计幂等与 checkpoint。
- 是否需要长对话、高频写入大字段?评估 DeltaChannel,但把 beta 格式与回滚成本写进发布方案。