LangGraph 源码深潜:Reflection 与评价-修改循环机制解剖
核心问题: LangGraph 如何把 Reflection 从“让模型再想想”的提示词技巧,变成由状态、评价节点、修改节点、条件边和循环保护共同约束的可控评价-修改循环?
源码主线:
StateGraph.add_node() / add_conditional_edges() → BranchSpec → CompiledStateGraph → Pregel super-step → state update / route → revise loop / END前置文章: 第 6 篇 StateGraph 源码解剖、第 7 篇 Conditional Edge 与 Router、第 8 篇 Reducer 与并行状态合并、第 9 篇 Plan-and-Execute 与 Subgraph
依赖基线:
langgraph==1.2.7、langchain-core随langgraph依赖解析源码基线:
langchain-ai/langgraphtag1.2.7,release commit5931a5f阅读边界: 本文只解释 Reflection / Evaluator-Optimizer 在 LangGraph Graph API 中如何落到节点、状态、条件边、循环和运行时保护;不展开 Reflexion 论文、LATS 搜索树、多 Agent 辩论、模型微调或 LangSmith 自动评估平台。
0. 本篇在源码学习主线中的位置
前面几篇已经完成了 LangGraph 的三层基础:
StateGraph:节点如何读写共享 state ↓Conditional Edge:节点执行后如何根据 state 路由 ↓Reducer:多个节点写同一个 key 时如何合并 ↓Plan-and-Execute:planner / executor / subgraph 如何形成多步骤执行本篇继续进入 Reflection:
生成草案 ↓评价草案 ↓根据分数与轮次判断是否修改 ↓修改草案 ↓再次评价 ↓达到质量阈值或最大修改次数后结束这篇要解决的是:
- Reflection 如何从一句 prompt 变成图中的可控循环。
quality_score、critique、revision_count、draft_plan、final_plan这些状态字段如何驱动条件边。route_by_score()返回revise_draft或final_response时,LangGraph 运行时如何调度下一节点。- 如何通过
MAX_REVISION和recursion_limit双层机制避免无限执行。 - 高风险任务中 Reflection 为什么不能替代规则、Policy、Verifier 和人工确认。
本篇不展开:
- 复杂搜索式 Reflection,例如 Tree of Thoughts / LATS。
- 训练式 Reflexion 或轨迹反馈微调。
- 多 Agent Debate。
- LangSmith Evaluation 的离线评估工作流。
1. 本篇问题、学习目标与能力边界
1.1 核心问题
如何用 LangGraph 的状态图、条件边和 Pregel 执行模型,把 Reflection 表达为一个可终止、可观测、可回滚的评价-修改循环?
1.2 学习目标
完成本篇后,读者必须能够:
- 解释 Reflection 为什么应该拆成
generator node、evaluator node、revise node与conditional edge,而不是只在 prompt 里写“请再检查一下”。 - 设计
TravelState中与反思相关的状态字段:draft_plan、critique、quality_score、revision_count、final_plan。 - 读懂
add_conditional_edges()如何注册route_by_score(),并理解返回节点名或END如何影响 Pregel 下一步调度。 - 解释
revision_count应该由revise_draft或专门的状态更新节点递增,而不是在路由函数里偷偷修改 state。 - 说明 evaluator 为什么应该结构化输出,并能把
quality_score、issues、suggestions作为可测试字段。 - 区分业务质量优化、规则校验、Policy Gate、Verifier 和 Human-in-the-loop 的职责边界。
- 在旅行规划助手中判断哪些任务适合 Reflection,哪些任务不应该让 Reflection 自动放行。
1.3 能力边界
| 能力 | 本篇是否覆盖 | 说明 |
|---|---|---|
| Reflection 循环建模 | 是 | 重点讲 generate → evaluate → route → revise → evaluate 的状态图结构 |
| 条件边源码机制 | 是 | 复用第 7 篇的 BranchSpec / _route / _finish 机制,但聚焦循环场景 |
| 状态更新与 partial update | 是 | 解释 revision_count、critique、draft_plan 如何更新 |
| 结构化评价输出 | 是 | 讲 evaluator output schema 的工程必要性 |
| 模型选择 | 是 | 讲 evaluator 和 generator 是否应使用不同模型的工程判断 |
| 高风险策略校验 | 部分覆盖 | 只说明边界,不展开 policy engine 设计 |
| HITL | 否 | 由 Interrupt / Command Resume 专篇展开 |
| 多 Agent 反思 | 否 | 由 Multi-Agent 专篇展开 |
2. 核心概念与最小心智模型
2.1 一句话定义
Reflection 在 LangGraph 中不是一个模型“自我反省”的神秘动作,而是一个由状态字段保存草案、评价结果和修改次数,再由条件边决定继续修改或终止的循环型 Workflow。
它负责:
- 对生成结果做质量评价。
- 根据评价结果触发修改。
- 在达到阈值、最大修改次数或保护上限时终止。
它不负责:
- 替代确定性业务规则。
- 替代权限校验。
- 自动批准高风险动作。
- 保证事实正确性,除非评价节点有可靠证据输入。
2.2 最小心智模型
user_request ↓generate_draft ↓evaluate_draft ↓route_by_score ├── revise_draft → evaluate_draft └── final_response → END关键不是“多调用一次模型”,而是每次循环都有显式状态:
draft_plan:当前草案critique:评价意见quality_score:质量分数revision_count:已修改次数final_plan:最终输出2.3 核心术语
| 术语 | 源码对象 | 语义 | 不要误解为 |
|---|---|---|---|
| Reflection Loop | StateGraph + add_conditional_edges() | 有停止条件的评价-修改循环 | 单个 prompt 中的“请反思” |
| Generator Node | add_node("generate_draft", fn) | 生成初稿 | 负责最终验收的节点 |
| Evaluator Node | add_node("evaluate_draft", fn) | 输出结构化评价 | 规则引擎或安全网关 |
| Revise Node | add_node("revise_draft", fn) | 根据 critique 修改草案并递增轮次 | 无限制重写器 |
| Route Function | route_by_score(state) | 根据 score / count 选择下一节点 | 修改 state 的地方 |
| Stop Condition | route_by_score → final_response / END | 业务级终止条件 | 框架递归上限 |
| Recursion Limit | config={"recursion_limit": n} | 框架级保护上限 | 质量评价规则 |
2.4 与相邻抽象的边界
| 对象 | 负责什么 | 不负责什么 | 与本篇对象的关系 |
|---|---|---|---|
StateGraph | 构建节点和边 | 自动知道质量标准 | Reflection 的承载结构 |
add_conditional_edges() | 把路由函数接到节点后 | 修改业务状态 | 用于 route_by_score |
| Reducer | 合并并发状态写入 | 判断是否要修改草案 | 可用于追加 critique history |
recursion_limit | 防止图无限运行 | 证明结果质量足够 | Reflection 的安全保险丝 |
| Policy / Verifier | 确定性校验和合规判断 | 文案润色 | Reflection 不能替代它们 |
3. 完整执行链路
3.1 高层链路
用户旅行请求 ↓StateGraph(TravelState) ↓注册 generate_draft / evaluate_draft / revise_draft / final_response ↓注册 route_by_score 条件边 ↓compile 生成 CompiledStateGraph ↓graph.invoke(initial_state) ↓Pregel 按 super-step 执行节点、写入状态、计算路由 ↓final_plan 写入 state 并到达 END3.2 最小代码骨架
观察目标:这段代码展示 Reflection 最小图结构,而不是完整业务实现。重点看状态字段、条件边和循环边。
from typing_extensions import TypedDict, Literalfrom langgraph.graph import StateGraph, START, END
MAX_REVISION = 2
class TravelState(TypedDict): user_request: str draft_plan: str | None critique: str | None quality_score: float | None revision_count: int final_plan: str | None
def generate_draft(state: TravelState): return { "draft_plan": "东京 5 天旅行计划草案", "revision_count": 0, }
def evaluate_draft(state: TravelState): draft = state["draft_plan"] or "" if "交通" in draft and "预算" in draft: return { "quality_score": 0.9, "critique": "计划较完整,可以输出。", } return { "quality_score": 0.6, "critique": "计划缺少交通或预算信息,需要修改。", }
def route_by_score(state: TravelState) -> Literal["revise_draft", "final_response"]: if (state["quality_score"] or 0.0) >= 0.8: return "final_response" if state["revision_count"] >= MAX_REVISION: return "final_response" return "revise_draft"
def revise_draft(state: TravelState): return { "draft_plan": (state["draft_plan"] or "") + "\n补充交通与预算信息。", "revision_count": state["revision_count"] + 1, }
def final_response(state: TravelState): return { "final_plan": state["draft_plan"] }
builder = StateGraph(TravelState)builder.add_node("generate_draft", generate_draft)builder.add_node("evaluate_draft", evaluate_draft)builder.add_node("revise_draft", revise_draft)builder.add_node("final_response", final_response)
builder.add_edge(START, "generate_draft")builder.add_edge("generate_draft", "evaluate_draft")builder.add_conditional_edges("evaluate_draft", route_by_score)builder.add_edge("revise_draft", "evaluate_draft")builder.add_edge("final_response", END)
graph = builder.compile()3.3 对象流转
| 阶段 | 输入类型 | 核心函数 | 输出类型 | 状态变化 |
|---|---|---|---|---|
| 构图 | TravelState schema | StateGraph.__init__() | builder | 注册状态字段和 channel |
| 注册节点 | Python callable | add_node() | builder | 保存 node spec |
| 注册循环路由 | callable | add_conditional_edges() | builder | 保存 branch spec |
| 编译 | builder | compile() | CompiledStateGraph | 生成可执行 Pregel 图 |
| 生成草案 | TravelState | generate_draft() | partial update | 写入 draft_plan |
| 评价草案 | TravelState | evaluate_draft() | partial update | 写入 quality_score / critique |
| 路由判断 | TravelState | route_by_score() | node name | 决定修改或终止 |
| 修改草案 | TravelState | revise_draft() | partial update | 更新 draft_plan / revision_count |
| 最终输出 | TravelState | final_response() | partial update | 写入 final_plan |
3.4 时序链路
Caller │ graph.invoke(initial_state) ▼CompiledStateGraph / Pregel │ 执行 generate_draft ▼State: draft_plan, revision_count │ 执行 evaluate_draft ▼State: critique, quality_score │ 执行 route_by_score ├── revise_draft │ │ 更新 draft_plan + revision_count │ └── 回到 evaluate_draft └── final_response │ 写入 final_plan ▼END3.5 正常结束条件
一次 Reflection 正常结束必须满足以下条件之一:
quality_score >= threshold 或revision_count >= MAX_REVISION 或route_by_score 返回 END / final_response最终结果保存在:
state["final_plan"]如果没有业务级停止条件,循环可能一直运行,最终由 LangGraph 的 recursion_limit 触发 GraphRecursionError。
4. 源码地图、关键文件与阅读顺序
4.1 核心目录
langgraph/├── graph/│ ├── state.py│ ├── graph.py│ └── branch.py├── pregel/│ ├── __init__.py│ ├── loop.py│ ├── runner.py│ └── algo.py├── channels/└── errors.py4.2 关键文件
| 优先级 | 文件 | 核心对象 | 阅读目的 |
|---|---|---|---|
| 1 | langgraph/graph/state.py | StateGraph / CompiledStateGraph | 理解节点、边、条件边如何注册和编译 |
| 2 | langgraph/graph/branch.py | BranchSpec | 理解 route function 如何包装、调用和映射结果 |
| 3 | langgraph/pregel/__init__.py | Pregel | 理解编译图为什么可执行 |
| 4 | langgraph/pregel/loop.py | Pregel loop | 理解 super-step、写入和停止条件 |
| 5 | langgraph/pregel/runner.py | runner | 理解节点任务如何被调度执行 |
| 6 | langgraph/errors.py | GraphRecursionError | 理解循环保护异常 |
4.3 推荐阅读顺序
1. StateGraph.add_node()2. StateGraph.add_edge()3. StateGraph.add_conditional_edges()4. BranchSpec.from_path()5. StateGraph.compile()6. CompiledStateGraph.attach_branch()7. BranchSpec.run() / _route() / _finish()8. Pregel.invoke() / stream()9. Pregel loop 的 super-step 调度10. recursion_limit / GraphRecursionError4.4 不建议的阅读顺序
不建议直接从 pregel/loop.py 开始。Pregel 是运行时调度内核,如果没有先理解 StateGraph 如何把节点、边、branch 和 channel 编译成 Pregel 可执行对象,会把 Reflection 误解为普通 while 循环。
也不建议只读 branch.py。BranchSpec 只解释“路由函数如何返回下一步”,但 Reflection 的关键还包括状态字段如何更新、循环如何终止、运行时如何限制步数。
5. 对象模型、继承关系与协议边界
5.1 核心对象关系
StateGraph ↓ compile()CompiledStateGraph ↓ inherits / wraps Pregel-style executable graphPregel runtime ↓ executes nodes and branches by super-stepFinal state5.2 对象职责
| 对象 | 生命周期 | 输入 | 输出 | 核心职责 |
|---|---|---|---|---|
StateGraph | 构建期 | state schema、node、edge | builder | 保存图定义,不直接执行 |
BranchSpec | 构建期 + 运行时 | route function | branch runnable | 封装条件边逻辑 |
CompiledStateGraph | 运行时 | initial state | final state / stream chunks | 执行已编译图 |
Pregel | 运行时 | channels、nodes、config | state updates | 调度 super-step |
TravelState | 全生命周期 | state dict | state dict | 保存 draft、评价和轮次 |
5.3 协议边界
Node 协议 负责:读取完整 state,返回 partial state update 不负责:决定图如何跳转,除非返回 Command
Conditional Edge 协议 负责:读取 state,返回下一节点名、多个节点名或 END 不负责:修改 state
Pregel 协议 负责:执行节点、收集写入、推进 super-step 不负责:理解 quality_score 的业务含义
Reflection 业务协议 负责:定义质量指标、最大修改次数和保底策略 不负责:替代框架循环保护5.4 稳定接口与内部实现
| 类型 | 对象 | 文章中的使用原则 |
|---|---|---|
| 公共 API | StateGraph、add_node()、add_edge()、add_conditional_edges()、compile() | 可用于工程示例 |
| 公共 API | graph.invoke()、graph.stream()、config={"recursion_limit": n} | 可用于运行时控制 |
| 扩展接口 | context_schema、RunnableConfig、Command、Send | 说明契约和约束 |
| 内部实现 | BranchSpec、Pregel loop、channel writes | 只用于解释,不建议业务代码直接依赖 |
6. 源码阅读策略与证据标准
6.1 本篇阅读策略
先确认 Reflection 的公开构图方式 ↓沿 StateGraph.add_conditional_edges 追到 BranchSpec ↓沿 compile 追到 CompiledStateGraph / Pregel ↓看一次 invoke 如何执行节点并应用 state update ↓看 route_by_score 如何返回 revise 或 final ↓检查 recursion_limit 和业务 MAX_REVISION 如何共同防死循环 ↓回到 Reflection 工程边界6.2 证据等级
| 标记 | 含义 | 写作要求 |
|---|---|---|
| 源码事实 | 可以由 langgraph==1.2.7 源码证明 | 附 tag / commit 源码链接 |
| 官方契约 | 官方文档或 API Reference 明确承诺 | 附官方链接 |
| 简化伪代码 | 对真实控制流的压缩表达 | 明确标注“不是源码逐字复制” |
| 作者推断 | 根据调用关系得出的设计理解 | 明确使用“从调用关系可以推断” |
| 工程建议 | 面向旅行规划助手实践的建议 | 说明适用条件 |
6.3 本篇证据清单
| 结论 | 证据类型 | 文件或文档 | 定位 |
|---|---|---|---|
| LangGraph 适合用节点、边和 state 表达 workflow | 官方契约 | Graph API / Thinking in LangGraph | nodes、edges、shared state |
| Evaluator-Optimizer 是“生成-评价-反馈-再生成”循环 | 官方契约 | Workflows and agents | Evaluator-optimizer |
条件边可以返回一个或多个节点,返回 END 会停止 | 官方契约 | StateGraph.add_conditional_edges API | path 返回值 |
| 循环需要终止条件,否则会触发 recursion limit | 官方契约 | Graph API / GRAPH_RECURSION_LIMIT | recursion limit |
| Reflection 状态应显式保存 score、critique 和 revision_count | 工程建议 | 本文状态设计 | 旅行规划助手实践 |
7. 构建期源码解剖
本章回答:
用户写下
generate_draft → evaluate_draft → route_by_score → revise_draft / final_response后,LangGraph 在构建期如何把这些函数注册成可编译的图结构?
7.1 构建期职责
| 输入 | 归一化动作 | 构建结果 |
|---|---|---|
TravelState | 解析 state schema 和 channels | state 字段与更新规则 |
generate_draft 等 callable | 转换为 runnable node spec | 节点注册表 |
route_by_score | 转换为 branch spec | 条件边注册表 |
START / END | 特殊边界节点 | 图入口和终止点 |
compile() | 校验图结构并 attach 节点/边/branch | CompiledStateGraph |
7.2 构建期总链路
StateGraph(TravelState) ↓_add_schema 解析状态字段 ↓add_node 注册 generator / evaluator / revise / final ↓add_edge 注册固定边 ↓add_conditional_edges 注册 route_by_score ↓compile 校验图并 attach nodes / edges / branches ↓CompiledStateGraph7.3 StateGraph.__init__() 源码解剖
职责与所处阶段
StateGraph.__init__() 处于构建期,负责接收用户定义的 state schema,并初始化节点表、边表、分支表、channel 表等构图数据结构。Reflection 的 draft_plan、quality_score、revision_count 等字段能成为运行时状态,第一步就在这里发生。
真实源码签名
以下签名来自当前正式版源码的公开构造入口,细节以 langgraph==1.2.7 为准:
class StateGraph(Graph): def __init__( self, state_schema: type[StateT], context_schema: type[ContextT] | None = None, *, input_schema: type[InputT] | None = None, output_schema: type[OutputT] | None = None, **kwargs: Any, ) -> None: ...调用方与被调用方
用户代码 StateGraph(TravelState) ↓StateGraph.__init__ ↓_add_schema / _get_channels / Graph.__init__输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | type[TravelState] | TypedDict state schema |
| 输出 | StateGraph builder | 构建期对象 |
| 状态变化 | self.schemas、self.channels、self.nodes、self.branches | 初始化图定义容器 |
| 副作用 | 无外部副作用 | 只修改 builder 内部结构 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
class StateGraph(Graph): def __init__(self, state_schema, context_schema=None, input_schema=None, output_schema=None): # 1. 初始化 Graph 基类中的节点、边、分支容器 super().__init__()
# 2. 保存 schema 形态 self.schema = state_schema self.input_schema = input_schema or state_schema self.output_schema = output_schema or state_schema self.context_schema = context_schema
# 3. 初始化状态相关容器 self.schemas = {} self.channels = {} self.managed = {}
# 4. 解析 state schema self._add_schema(state_schema)
# 5. 如果 input / output schema 不同,也单独解析 if self.input_schema is not state_schema: self._add_schema(self.input_schema) if self.output_schema is not state_schema: self._add_schema(self.output_schema)逐段解释
第 1 段 super().__init__() 初始化普通图结构:节点、普通边、条件边都会先存到 builder 的内部容器中。此时还没有任何运行时调度。
第 2 段保存 schema。Reflection 场景中,TravelState 既是输入、运行时状态,也是输出 schema 的默认来源。
第 3 段初始化 channels。LangGraph 的 state 不是普通 dict,每个 key 在运行时都对应 channel;后续更新、覆盖、聚合都由 channel 处理。
第 4 段 _add_schema(state_schema) 解析 draft_plan、critique、quality_score、revision_count、final_plan 这些字段。没有 reducer 时,大多是覆盖式更新。
正常路径
TravelState ↓_add_schema ↓channels: draft_plan / critique / quality_score / revision_count / final_plan ↓StateGraph builder ready关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 未传 input/output schema | 默认使用 state schema | 简化普通 workflow |
state key 使用 Annotated[..., reducer] | 解析为聚合 channel | 支持追加式更新 |
| schema 无法解析 | 构建期报错 | 图无法 compile |
设计原因与工程影响
State schema 必须在构建期解析,因为运行时 Pregel 需要提前知道每个 state key 的更新规则。Reflection 中 revision_count 应该是覆盖式字段;critique_history 如果存在,则应该用 reducer 追加。
源码证据
libs/langgraph/langgraph/graph/state.py::StateGraph.__init__- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py
7.4 add_node() 注册 Reflection 节点源码解剖
职责与所处阶段
add_node() 处于构建期,负责把普通 Python 函数注册成 LangGraph 节点。Reflection 中的 generate_draft、evaluate_draft、revise_draft、final_response 都通过它进入图结构。
真实源码签名
def add_node( self, node: str | StateNode[NodeInputT] | None = None, action: StateNode[NodeInputT] | None = None, *, defer: bool = False, metadata: dict[str, Any] | None = None, input_schema: type[NodeInputT] | None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, **kwargs: Any,) -> Self: ...调用方与被调用方
builder.add_node("evaluate_draft", evaluate_draft) ↓StateGraph.add_node ↓coerce_to_runnable / node spec 保存输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | str + callable | 节点名与节点函数 |
| 输出 | Self | 返回 builder,支持链式调用 |
| 状态变化 | self.nodes[name] | 保存节点 spec |
| 副作用 | 无外部副作用 | 只修改 builder |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def add_node(self, node=None, action=None, **options): # 1. 解析调用形式 if isinstance(node, str): node_name = node node_action = action else: node_action = node node_name = infer_name_from_callable(node_action)
# 2. 校验节点名 if node_name in RESERVED_NAMES: raise ValueError("Node name conflicts with reserved names") if node_name in self.nodes: raise ValueError("Node already exists")
# 3. 把 callable 转成 runnable-like 节点 runnable = coerce_to_runnable( node_action, name=node_name, )
# 4. 保存节点 spec self.nodes[node_name] = StateNodeSpec( runnable=runnable, metadata=options.get("metadata"), input_schema=options.get("input_schema"), retry_policy=options.get("retry_policy"), cache_policy=options.get("cache_policy"), destinations=options.get("destinations"), defer=options.get("defer", False), )
return self逐段解释
第 1 段支持两种写法:add_node("name", fn) 和 add_node(fn)。Reflection 建议显式命名,这样 trace、可视化和条件边都更清晰。
第 2 段防止节点名冲突。START 和 END 是特殊边界,不应该作为普通节点名。
第 3 段把普通函数转成可运行对象。运行时 Pregel 不直接关心它是不是普通函数,而是按 runnable-like 节点统一调度。
第 4 段保存 retry_policy、cache_policy、defer 等运行时配置。Reflection 中 evaluator 可以设置 retry,但 revise 节点如果包含外部副作用则要谨慎。
正常路径
evaluate_draft function ↓add_node ↓StateNodeSpec ↓self.nodes["evaluate_draft"]关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 节点名重复 | 抛出构建期错误 | 防止覆盖已有节点 |
| 节点名为保留名 | 抛出错误 | 防止破坏图边界 |
| 未传显式名称 | 从 callable 推断 | 可读性可能变差 |
设计原因与工程影响
Reflection 循环至少有 4 个节点,如果不显式命名,条件边返回值和可视化图会难以维护。建议生产代码明确使用 generate_draft、evaluate_draft、revise_draft、final_response。
源码证据
libs/langgraph/langgraph/graph/state.py::StateGraph.add_node- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py
7.5 add_conditional_edges() 注册 route_by_score 源码解剖
职责与所处阶段
add_conditional_edges() 处于构建期,负责把 evaluate_draft 之后的动态路由逻辑注册为 branch。Reflection 的核心循环不是 while,而是这里注册的条件边。
真实源码签名
def add_conditional_edges( self, source: str, path: Callable[..., Hashable | Sequence[Hashable]] | Callable[..., Awaitable[Hashable | Sequence[Hashable]]] | Runnable[Any, Hashable | Sequence[Hashable]], path_map: dict[Hashable, str] | list[str] | None = None,) -> Self: ...调用方与被调用方
builder.add_conditional_edges("evaluate_draft", route_by_score) ↓StateGraph.add_conditional_edges ↓BranchSpec.from_path ↓self.branches[source][branch_name] = branch输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | source node + route callable | 条件边定义 |
| 输出 | Self | 返回 builder |
| 状态变化 | self.branches["evaluate_draft"] | 保存 branch spec |
| 副作用 | 无外部副作用 | 构建期注册 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def add_conditional_edges(self, source, path, path_map=None): # 1. 校验 source if source not in self.nodes: # 允许稍后校验,也可能立即报错,具体以源码为准 record_pending_source(source)
# 2. 把 route function 转成 runnable path_runnable = coerce_to_runnable(path, name=None)
# 3. 推断 branch 名称 branch_name = path_runnable.name or "condition"
# 4. 防止同一个 source 下 branch 名冲突 if branch_name in self.branches[source]: raise ValueError("Branch already exists")
# 5. 从 path / path_map / Literal 返回类型构造 BranchSpec branch = BranchSpec.from_path( path=path_runnable, path_map=path_map, infer_schema=True, )
# 6. 保存到 source 的 branch 表 self.branches[source][branch_name] = branch
return self逐段解释
第 2 段说明 route function 也会被视为可运行对象。它和普通 node 一样可以同步或异步执行。
第 5 段是关键:BranchSpec.from_path() 会处理 path_map,并尝试根据返回类型里的 Literal["revise_draft", "final_response"] 推断可视化目标。
第 6 段把 branch 挂到 evaluate_draft 后面。运行时只有当 evaluate_draft 完成并写入 state 后,route function 才会被调用。
正常路径
evaluate_draft 执行完成 ↓state 包含 quality_score / revision_count ↓route_by_score(state) ↓revise_draft 或 final_response关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
route 返回 revise_draft | 调度 revise 节点 | 进入修改循环 |
route 返回 final_response | 调度 final 节点 | 准备结束 |
| route 返回未知节点 | 运行时报错 | 路由目标无效 |
| route 返回多个节点 | 多目标写入 | 可能并行执行 |
route 返回 END | 终止图 | 不再调度后继节点 |
设计原因与工程影响
Reflection 的循环控制应该放在 route function 中,而不是让 evaluator 或 revise 节点直接调用下一节点。这样每个节点只负责状态更新,条件边负责控制流,图结构可视化也更清晰。
源码证据
libs/langgraph/langgraph/graph/state.py::StateGraph.add_conditional_edgeslibs/langgraph/langgraph/graph/branch.py::BranchSpec.from_path- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py
- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/branch.py
7.6 compile() 组装 Reflection 可执行图源码解剖
职责与所处阶段
compile() 是构建期到运行时的边界。它把 builder 中保存的节点、普通边和条件边转换成 CompiledStateGraph,使图具备 invoke()、stream()、ainvoke() 等运行能力。
真实源码签名
def compile( self, checkpointer: Checkpointer = None, *, cache: BaseCache | None = None, store: BaseStore | None = None, interrupt_before: All | list[str] | None = None, interrupt_after: All | list[str] | None = None, debug: bool = False, name: str | None = None,) -> CompiledStateGraph: ...调用方与被调用方
builder.compile() ↓StateGraph.compile ↓validate graph ↓CompiledStateGraph(...) ↓attach_node / attach_edge / attach_branch输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | builder 内部图定义 | nodes / edges / branches |
| 输出 | CompiledStateGraph | 可执行图 |
| 状态变化 | compiled graph 的 channels / nodes / triggers | 运行时结构被构造 |
| 副作用 | 可选 checkpointer/cache/store 注入 | 运行时能力增强 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def compile(self, checkpointer=None, **options): # 1. 校验构建期图结构 self.validate( interrupt_before=options.get("interrupt_before"), interrupt_after=options.get("interrupt_after"), )
# 2. 创建 CompiledStateGraph compiled = CompiledStateGraph( builder=self, channels=self.channels, nodes={}, input_channels=START, output_channels=self.output_channels, checkpointer=checkpointer, cache=options.get("cache"), store=options.get("store"), debug=options.get("debug", False), name=options.get("name"), )
# 3. attach START、普通节点 compiled.attach_node(START, None) for node_name, node_spec in self.nodes.items(): compiled.attach_node(node_name, node_spec)
# 4. attach 普通边 for start, end in self.edges: compiled.attach_edge(start, end)
# 5. attach 条件分支 for source, branches in self.branches.items(): for branch_name, branch in branches.items(): compiled.attach_branch(source, branch_name, branch)
# 6. 校验 compiled graph return compiled.validate()逐段解释
第 1 段校验图结构,确保节点、边和中断点合法。Reflection 中如果忘记给 final_response 连到 END,或 route 返回不存在节点,这里或运行时会暴露问题。
第 2 段创建可执行图。StateGraph 是 builder,CompiledStateGraph 才是运行时对象。
第 5 段把 route_by_score 这种 branch attach 到 compiled graph。后续 Pregel 执行到 evaluate_draft 后,会触发这个 branch。
正常路径
StateGraph builder ↓compile ↓CompiledStateGraph ↓可 invoke / stream / ainvoke关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 图结构合法 | 返回 compiled graph | 可运行 |
| 节点不存在 | 校验失败 | 构建期暴露错误 |
| interrupt 配置非法 | 校验失败 | 防止运行时悬空 |
| checkpointer 存在 | 注入持久化能力 | 支持恢复和 HITL |
设计原因与工程影响
Reflection 循环在 builder 阶段只是声明。只有 compile 后,节点、边、条件分支才被转成 Pregel 可调度结构。生产代码中不应在 compile 后继续修改 builder 并期待已编译图同步变化。
源码证据
libs/langgraph/langgraph/graph/state.py::StateGraph.compilelibs/langgraph/langgraph/graph/state.py::CompiledStateGraph- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py
7.7 构建期产物
| 产物 | 保存的信息 | 运行时用途 |
|---|---|---|
self.nodes | generate_draft / evaluate_draft / revise_draft / final_response | Pregel 调度节点 |
self.edges | START → generate_draft、generate_draft → evaluate_draft 等 | 固定拓扑 |
self.branches | evaluate_draft → route_by_score | 动态循环控制 |
self.channels | draft_plan、quality_score、revision_count 等 | 合并 partial updates |
CompiledStateGraph | attach 后的 nodes / edges / branches | 对外执行入口 |
8. 运行时主链源码解剖
本章回答:
构建完成后,一次
graph.invoke()如何执行生成、评价、修改、再评价,直到final_plan产出?
8.1 运行时入口
| 调用方式 | 公开入口 | 核心内部入口 | 返回类型 |
|---|---|---|---|
| 同步调用 | graph.invoke(input, config=...) | Pregel stream/invoke 主链 | final state |
| 异步调用 | graph.ainvoke(input, config=...) | async Pregel 主链 | final state |
| 流式调用 | graph.stream(input, ...) | Pregel stream | step chunks / values |
| 批量调用 | graph.batch([...]) | Runnable batch | list[final state] |
8.2 运行时总链路
graph.invoke(initial_state) ↓归一化 input / config ↓初始化 Pregel loop 与 state channels ↓执行 START 触发的 generate_draft ↓收集 partial update 并写入 state ↓执行 evaluate_draft ↓写入 critique / quality_score ↓执行 route_by_score branch ↓根据返回值调度 revise_draft 或 final_response ↓如果 revise_draft:更新 draft_plan / revision_count 后回到 evaluate_draft ↓如果 final_response:写入 final_plan 并到达 END8.3 输入归一化源码解剖
职责与所处阶段
输入归一化发生在运行时入口。它把用户传入的初始 state 变成 Pregel 可以写入 state channels 的初始值。
真实源码签名
公开入口遵循 Runnable 协议,简化签名如下:
def invoke( self, input: InputT, config: RunnableConfig | None = None, *, context: ContextT | None = None, stream_mode: StreamMode | None = None, **kwargs: Any,) -> OutputT: ...调用方与被调用方
graph.invoke(initial_state) ↓CompiledStateGraph / Pregel.invoke ↓stream loop ↓return latest output输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | dict | 初始 TravelState |
| 输出 | dict | 最终 state |
| 状态变化 | state channels 初始化 | 写入初始值 |
| 副作用 | 可选 checkpoint / trace | 取决于 config |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def invoke(self, input_value, config=None, context=None, **kwargs): # 1. 归一化 RunnableConfig config = ensure_config(config)
# 2. 把 context 放入 runtime,上下文不是 state runtime = create_runtime_context( config=config, context=context, )
# 3. 调用 stream 主链,但只保留最终结果 latest_output = None for chunk in self.stream( input_value, config=config, context=context, **kwargs, ): latest_output = extract_output(chunk)
# 4. 返回最终 state 或 output schema 对应对象 return latest_output逐段解释
第 1 段确保 recursion_limit、metadata、callbacks 等配置被统一处理。
第 2 段区分 context 和 state。Reflection 的 quality_score 属于 state;模型实例、用户 ID、数据库连接更适合 context。
第 3 段说明 invoke 可以理解为对 stream 的封装:运行时不断产生 step 输出,invoke 返回最终值。
正常路径
initial TravelState ↓state channels 初始化 ↓Pregel 执行 ↓final TravelState关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 初始 state 缺少必需字段 | 输入校验或节点访问时报错 | 运行失败 |
recursion_limit 太低 | 可能提前触发错误 | 需要调高或优化循环 |
| context 缺失 | 依赖 context 的节点报错 | 需要 context_schema |
设计原因与工程影响
Reflection 循环推荐初始化所有关键字段,尤其是 revision_count=0。如果依赖节点内自行处理缺省值,会使状态语义不稳定。
源码证据
libs/langgraph/langgraph/pregel/__init__.py::Pregel.invokelibs/langgraph/langgraph/pregel/__init__.py::Pregel.stream- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/__init__.py
8.4 Config、Context 与 State 传播源码解剖
数据边界
| 数据 | 来源 | 生命周期 | 下游消费者 |
|---|---|---|---|
| Config | graph.invoke(..., config=...) | 单次运行 | Pregel loop、节点、tracing、recursion limit |
| Context | graph.invoke(..., context=...) | 单次运行 | 节点函数、路由函数、工具/模型选择 |
| State | 初始输入 + 节点 update | 整个图运行 | 所有节点和 route function |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def execute_superstep(loop, config, runtime): # 1. 从 channels 读取当前 state 快照 current_state = loop.read_state()
# 2. 根据当前触发条件选择待执行 tasks tasks = loop.prepare_tasks(current_state)
# 3. 给每个 task 注入 config / runtime / state for task in tasks: task_input = build_node_input( state=current_state, config=config, runtime=runtime, ) task.schedule(task_input)
# 4. task 返回 partial update writes = collect_task_writes(tasks)
# 5. 应用 writes 到 channels loop.apply_writes(writes)逐段解释
第 1 段读取的是当前 super-step 开始时的 state 快照。同一个 super-step 中并行节点通常看到的是同一轮输入。
第 3 段说明 node 不只接收 state,还可能接收 RunnableConfig 或 Runtime。如果 evaluator 和 generator 用不同模型,可以通过 runtime context 传入模型选择。
第 5 段把节点返回的 partial update 写回 state。revision_count 的递增必须通过节点返回 update,而不是在路由函数中原地修改。
8.5 generate_draft 节点运行源码解剖
职责与所处阶段
generate_draft 是 Reflection 主链的第一个业务节点,负责生成初稿并初始化修改次数。
真实源码签名
用户节点签名遵循 State -> Partial[State]:
def generate_draft(state: TravelState) -> dict[str, Any]: ...调用方与被调用方
Pregel runner ↓generate_draft runnable ↓node function ↓state channel writes输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | TravelState | 初始请求状态 |
| 输出 | dict | partial update |
| 状态变化 | draft_plan、revision_count | 写入草案和计数 |
| 副作用 | 不建议有 | 生成节点应尽量纯 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def run_generate_draft_task(state, config, runtime): # 1. Pregel 构造节点输入 node_input = state
# 2. 调用用户函数 update = generate_draft(node_input)
# 3. 校验返回值是 mapping / Command 等合法写入 writes = normalize_node_update(update)
# 4. 将每个 key 转成 channel write for key, value in writes.items(): emit_write(channel=key, value=value)
# 5. 当前 super-step 结束时统一应用 write return writes逐段解释
第 2 段调用的是你的 Python 函数。LangGraph 不知道它是不是调用了 LLM,只关心它返回了哪些 state 更新。
第 3 段保证节点输出符合协议。节点应该返回 partial update,而不是直接修改 state 原对象。
第 4 段把 draft_plan、revision_count 变成 channel writes。后续合并逻辑由 channel 处理。
正常路径
user_request ↓generate_draft ↓{"draft_plan": ..., "revision_count": 0} ↓state 更新关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 节点返回 dict | 应用 state update | 正常 |
| 节点返回非法类型 | 报错 | 运行失败 |
| LLM 调用失败 | 异常向上传播或由 retry 处理 | 取决于 retry policy |
设计原因与工程影响
generate_draft 应该只负责生成,不应同时决定是否合格。否则 evaluator 失去独立性,Reflection 会退化为普通“自说自话”。
源码证据
libs/langgraph/langgraph/pregel/runner.pylibs/langgraph/langgraph/pregel/algo.py- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/runner.py
8.6 evaluate_draft 节点运行源码解剖
职责与所处阶段
evaluate_draft 是 Reflection 的评价节点。它读取当前草案,输出结构化评价字段:quality_score、critique,必要时也可以输出 issues、risk_level、suggested_changes。
真实源码签名
def evaluate_draft(state: TravelState) -> dict[str, Any]: ...调用方与被调用方
Pregel runner ↓evaluate_draft runnable ↓LLM / rule-based evaluator ↓state channel writes输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | TravelState | 包含 draft_plan |
| 输出 | dict | 评价 update |
| 状态变化 | quality_score、critique | 写入本轮评价 |
| 副作用 | 不建议有 | evaluator 应可重复运行 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def run_evaluate_draft_task(state, config, runtime): # 1. 读取当前草案 draft = state.get("draft_plan") if not draft: return { "quality_score": 0.0, "critique": "没有可评价的草案。", }
# 2. 构造评价请求 evaluation_input = { "draft_plan": draft, "criteria": runtime.context.criteria, }
# 3. 调用 evaluator,推荐结构化输出 evaluation = evaluator_model.with_structured_output( EvaluationResult ).invoke(evaluation_input)
# 4. 返回 partial update return { "quality_score": evaluation.quality_score, "critique": evaluation.critique, }逐段解释
第 1 段处理空草案。这不是源码强制逻辑,而是工程上必须处理的异常状态。
第 3 段建议 evaluator 使用结构化输出。不要只让模型返回“还不错”,否则 route_by_score 没有稳定字段可读。
第 4 段只返回评价字段,不修改 revision_count。计数应该由 revise 节点递增。
正常路径
draft_plan ↓evaluator ↓quality_score + critique ↓route_by_score关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 草案为空 | 返回低分或抛出错误 | 进入 revise / fail path |
| evaluator 输出结构非法 | 结构化解析失败 | retry / fallback / fail |
| evaluator 调用失败 | 抛异常 | 由 retry policy 或上层处理 |
设计原因与工程影响
Evaluator 应该尽量独立于 generator。对于质量要求高的节点,可以使用更强模型做 evaluator、便宜模型做 generator;但如果 evaluator 没有明确评价指标,使用更强模型也无法保证可靠。
源码证据
libs/langgraph/langgraph/pregel/runner.pylangchain_corestructured output 由上一篇 OutputParser / Structured Output 覆盖
8.7 route_by_score 分支运行源码解剖
职责与所处阶段
route_by_score 是运行时分支函数,在 evaluate_draft 节点完成、state 更新后执行。它读取 quality_score 和 revision_count,决定进入 revise_draft 还是 final_response。
真实源码签名
用户侧签名:
def route_by_score(state: TravelState) -> Literal["revise_draft", "final_response"]: ...内部 branch 运行可抽象为:
class BranchSpec: def run(self, writer, reader=None) -> Runnable: ...调用方与被调用方
evaluate_draft 完成 ↓BranchSpec.run ↓BranchSpec._route ↓route_by_score(state) ↓BranchSpec._finish ↓写入目标节点 channel输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | TravelState | 已包含最新 quality_score |
| 输出 | str | revise_draft 或 final_response |
| 状态变化 | 无 | route function 不应修改 state |
| 副作用 | 无 | 应保持纯函数 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def branch_route_after_evaluate(state, config, runtime): # 1. 读取 state 快照 branch_input = state
# 2. 调用用户路由函数 route_result = route_by_score(branch_input)
# 3. 归一化返回值 if isinstance(route_result, (list, tuple)): destinations = list(route_result) else: destinations = [route_result]
# 4. 处理 END normalized = [] for dest in destinations: if dest == END: normalized.append(END) else: normalized.append(resolve_path_map(dest))
# 5. 将目的地写入 Pregel channel for dest in normalized: if dest is END: emit_end_signal() else: emit_branch_write(target_node=dest)
return normalized逐段解释
第 2 段调用 route_by_score。这里不应该递增 revision_count,因为 route function 是控制流函数,不是状态更新节点。
第 3 段说明 route function 可以返回单个节点,也可以返回多个节点。Reflection 通常返回单个节点。
第 4 段处理 path_map 和 END。本例没有显式 path_map,因为 Literal 返回值已经是节点名。
第 5 段把路由结果转换为 Pregel 可调度的目标节点写入。
正常路径
quality_score >= 0.8 ↓final_response
quality_score < 0.8 and revision_count < MAX_REVISION ↓revise_draft关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
quality_score >= 0.8 | 返回 final_response | 正常结束路径 |
revision_count >= MAX_REVISION | 返回 final_response | 保底结束路径 |
| 二者都不满足 | 返回 revise_draft | 继续循环 |
| 返回未知节点 | 运行时报错 | 路由错误 |
| 返回多个节点 | 并行触发多个节点 | Reflection 一般不建议这样做 |
设计原因与工程影响
Reflection 的停止条件必须明确写在 route function 里。如果只依赖 recursion_limit,图会以异常方式结束,而不是业务可解释的“已达到最大修改次数”。
源码证据
libs/langgraph/langgraph/graph/branch.py::BranchSpec.runlibs/langgraph/langgraph/graph/branch.py::BranchSpec._routelibs/langgraph/langgraph/graph/branch.py::BranchSpec._finish- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/branch.py
8.8 revise_draft 节点运行源码解剖
职责与所处阶段
revise_draft 是修改节点。它读取当前草案和 critique,生成新版草案,并递增 revision_count。
真实源码签名
def revise_draft(state: TravelState) -> dict[str, Any]: ...调用方与被调用方
route_by_score → revise_draft ↓Pregel runner ↓revise_draft callable ↓state update ↓evaluate_draft输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | TravelState | 包含 draft、critique、revision_count |
| 输出 | dict | 新草案和新计数 |
| 状态变化 | draft_plan、revision_count | 覆盖草案,递增计数 |
| 副作用 | 不建议有 | 修改应可重放 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def run_revise_draft_task(state, config, runtime): # 1. 读取旧草案与评价意见 old_draft = state["draft_plan"] critique = state["critique"] old_count = state["revision_count"]
# 2. 调用 revise 模型或规则修改器 revised = revise_model.invoke({ "draft_plan": old_draft, "critique": critique, "user_request": state["user_request"], })
# 3. 递增 revision_count new_count = old_count + 1
# 4. 返回 partial update return { "draft_plan": revised.content, "revision_count": new_count, }逐段解释
第 1 段读取的是当前 state 中的最新草案和评价意见。它不需要知道上一次 route 是怎么判断的。
第 3 段递增轮次。revision_count 的更新应该和一次实际修改绑定,不能只因为 evaluator 认为质量不够就递增。
第 4 段覆盖 draft_plan。如果需要回滚,应该额外设计 draft_history 并使用 reducer 追加。
正常路径
critique ↓revise_draft ↓new draft_plan + revision_count + 1 ↓evaluate_draft关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| revise 成功 | 覆盖 draft_plan | 继续评价 |
| revise 生成更差结果 | 需要 evaluator 识别 | 可能继续循环或最终输出较差版本 |
| revise 失败 | 抛异常或 fallback | 取决于 retry policy |
| 达到最大轮次 | route 不再进入 revise | 保底结束 |
设计原因与工程影响
如果新版本更差,系统应能回滚到旧版本。工程上可以保存:
draft_history: Annotated[list[DraftVersion], add]best_draft: str | Nonebest_score: float | None这样 final node 可以选择最高分版本,而不是盲目选择最后一次修改结果。
源码证据
libs/langgraph/langgraph/pregel/runner.pylibs/langgraph/langgraph/channels/state channel update 机制
8.9 final_response 节点运行源码解剖
职责与所处阶段
final_response 是 Reflection 主链的出口节点。它把当前草案或最佳草案写入 final_plan。
真实源码签名
def final_response(state: TravelState) -> dict[str, Any]: ...调用方与被调用方
route_by_score → final_response ↓Pregel runner ↓final_response callable ↓state update ↓END输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | TravelState | 当前 draft / score / critique |
| 输出 | dict | final update |
| 状态变化 | final_plan | 写入最终计划 |
| 副作用 | 无 | 输出节点不应执行高风险动作 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def run_final_response_task(state, config, runtime): # 1. 优先选择 best_draft,如果没有则选择当前 draft if "best_draft" in state and state["best_draft"]: selected = state["best_draft"] else: selected = state["draft_plan"]
# 2. 附加质量说明 final_text = format_final_plan( plan=selected, score=state.get("quality_score"), critique=state.get("critique"), revision_count=state.get("revision_count"), )
# 3. 写入 final_plan return { "final_plan": final_text }逐段解释
第 1 段是工程建议:如果保存了最佳版本,则应该选择最佳版本,而不是最后版本。
第 2 段可以把质量说明保留给内部 trace,但面向用户的最终回答不一定展示 evaluator 的所有 critique。
第 3 段写入 final_plan,随后普通边进入 END。
正常路径
final_response ↓{"final_plan": ...} ↓END关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 有 best_draft | 输出最佳版本 | 更稳 |
| 无 best_draft | 输出当前草案 | 简化实现 |
| final_plan 格式化失败 | 抛异常 | 运行失败 |
设计原因与工程影响
Reflection 的最终节点应该只做输出整理,不应该执行“创建订单、退款、发邮件”等高风险动作。高风险动作需要 Policy / Verifier / HITL。
源码证据
libs/langgraph/langgraph/pregel/runner.pylibs/langgraph/langgraph/graph/state.pycompiled graph update path
8.10 运行时主链总结
initial state ↓generate_draft 写 draft_plan ↓evaluate_draft 写 quality_score / critique ↓route_by_score 读 score / count ├── revise_draft 写 draft_plan / revision_count │ ↓ │ evaluate_draft └── final_response 写 final_plan ↓ END9. 关键分支、异常与边界
本章回答:
当 Reflection 循环质量不达标、模型输出异常、轮次耗尽或图无限循环时,框架和业务代码分别如何处理?
9.1 分支矩阵
| 分支类型 | 触发条件 | 核心函数 | 结果 |
|---|---|---|---|
| 质量达标 | quality_score >= 0.8 | route_by_score() | final_response |
| 轮次耗尽 | revision_count >= MAX_REVISION | route_by_score() | final_response |
| 继续修改 | 分数低且轮次未耗尽 | route_by_score() | revise_draft |
| 框架保护 | 超过 recursion_limit | Pregel loop | GraphRecursionError |
| 评价失败 | evaluator 输出非法 | evaluator node / parser | retry / fallback / error |
| 修改失败 | revise 模型异常 | revise node | retry / fallback / error |
9.2 同步与异步分支
| 维度 | 同步路径 | 异步路径 |
|---|---|---|
| 入口 | graph.invoke() | graph.ainvoke() |
| 节点 | 普通 def | async def |
| 调度方式 | 同步 Pregel runner | 异步 Pregel runner |
| 适用 | 简单本地逻辑或同步模型调用 | 多个异步 I/O、异步模型服务 |
| 风险 | 阻塞线程 | 异步异常传播更复杂 |
9.3 Batch、Stream、Parallel 或路由分支
Reflection 支持 stream() 观察每一轮:
for chunk in graph.stream(initial_state, config={"recursion_limit": 10}): print(chunk)这对调试非常关键,因为你可以看到:
generate_draft 输出什么evaluate_draft 给了多少分route_by_score 走向哪里revise_draft 修改了什么Reflection 通常不建议让 route_by_score() 同时返回多个修改节点,因为多个修改分支会并行写 draft_plan,如果没有 reducer 或版本字段,会产生覆盖冲突或语义混乱。
9.4 异常分类
| 异常类别 | 抛出位置 | 是否可恢复 | 处理策略 | 是否反馈上层 |
|---|---|---|---|---|
| evaluator 结构化输出失败 | evaluate_draft | 是 | retry / fallback evaluator | 是 |
| revise 模型调用失败 | revise_draft | 是 | retry / 使用旧 draft | 是 |
| route 返回未知节点 | branch finish | 否 | 修复代码 | 是 |
| 无限循环 | Pregel loop | 部分可恢复 | 捕获 GraphRecursionError,返回最佳已有草案 | 是 |
| 业务高风险未校验 | final / action 前 | 不应自动恢复 | Policy / HITL | 是 |
9.5 异常路径源码解剖
职责与所处阶段
异常路径横跨运行时节点执行、branch 路由和 Pregel 循环保护。Reflection 必须同时有业务停止条件和框架停止条件。
真实源码签名
相关对象:
class GraphRecursionError(RecursionError): ...调用方与被调用方
Pregel loop ↓检查 super-step / recursion_limit ↓超过限制 ↓raise GraphRecursionError输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | 当前 step counter | Pregel 运行时元数据 |
| 输出 | 异常 | 超限时抛出 |
| 状态变化 | 无正常 final state | 可通过 checkpoint 获取中间状态 |
| 副作用 | 可选 checkpoint | 如果启用持久化可恢复 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def pregel_loop_tick(loop, config): # 1. 读取 recursion_limit limit = config.get("recursion_limit", DEFAULT_LIMIT)
# 2. 检查当前 super-step if loop.step >= limit: raise GraphRecursionError( "Graph reached recursion limit before stop condition" )
# 3. 执行本轮可运行任务 tasks = loop.prepare_next_tasks() if not tasks: return DONE
# 4. 调度任务并收集写入 writes = run_tasks(tasks)
# 5. 应用写入并进入下一轮 loop.apply_writes(writes) loop.step += 1逐段解释
第 1 段读取框架保护上限。它不是 Reflection 的业务轮次,而是图的 super-step 上限。
第 2 段说明如果业务逻辑没有走到 END,框架会保护性报错。
第 3 段如果没有任务可执行,图自然完成。
第 5 段说明每个 super-step 后 state 才推进。Reflection 中一次“修改轮”可能包含多个 super-step:revise_draft 和 evaluate_draft 至少各一步。
正常路径
业务 stop condition 先触发 ↓final_response ↓END关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 达到业务质量阈值 | route 到 final | 正常结束 |
| 达到 MAX_REVISION | route 到 final | 保底结束 |
| 未达到 END 且超过 recursion_limit | 抛 GraphRecursionError | 异常结束 |
设计原因与工程影响
Reflection 必须有 MAX_REVISION,不能只依赖 recursion_limit。前者是业务可解释的停止条件,后者是框架保险丝。
源码证据
libs/langgraph/langgraph/errors.py::GraphRecursionErrorlibs/langgraph/langgraph/pregel/loop.py- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/errors.py
- https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/loop.py
9.6 Retry、Fallback 与恢复边界
| 机制 | 适用条件 | 不适用条件 | 幂等要求 |
|---|---|---|---|
| Retry | evaluator 输出格式偶发错误 | 评价指标本身错误 | 节点应无副作用 |
| Fallback | evaluator 模型不可用 | 高风险自动放行 | fallback 也必须遵守 schema |
| Repair | critique 格式不合法 | 事实错误或权限错误 | 只修格式,不改业务判断 |
| 回滚 | revise 后质量下降 | 没有保存历史版本 | 需要 draft_history 或 best_draft |
9.7 停止条件与保护上限
正常结束:quality_score >= threshold保底结束:revision_count >= MAX_REVISION提前结束:evaluator 判定不可修复或需要人工人工中断:高风险任务进入 interrupt / HITL框架保护:recursion_limit 超限抛 GraphRecursionError异常失败:route 返回非法目标、节点输出非法、模型调用失败9.8 能力边界
| 容易误判的能力 | 实际提供者 | Reflection 的真实职责 |
|---|---|---|
| 安全放行 | Policy / Guardrail / HITL | 只能提出修改意见 |
| 合规判断 | 规则引擎 / Verifier | 不能替代确定性规则 |
| 工具执行确认 | Tool layer / human approval | 不能自动确认高风险 action |
| 事实真实性 | RAG evidence / external tools | 只能检查文本一致性,除非有证据输入 |
| 无限循环保护 | MAX_REVISION + recursion_limit | 必须显式设计停止条件 |
10. 扩展机制与框架协作
本章回答:
Reflection 循环如何与结构化输出、Reducer、Checkpoint、Subgraph、Command、HITL 和评估系统协作?
10.1 扩展点总览
| 扩展点 | 扩展方式 | 执行时机 | 可修改内容 | 约束 |
|---|---|---|---|---|
| 结构化 evaluator | Pydantic schema / with_structured_output | evaluate_draft 内部 | score、issues、suggestions | 只能保证结构,不保证事实 |
| revision history | reducer | revise_draft 写入时 | 追加版本历史 | 注意 token 和状态膨胀 |
| checkpoint | compile(checkpointer=...) | 每个 super-step | 保存中间状态 | 需要 thread_id |
| subgraph | 子图作为节点 | revise 或 evaluate 内部 | 封装复杂评价流程 | 需要 state 映射 |
Command | 节点返回控制指令 | 节点执行后 | 同时更新 state 和跳转 | 控制流更强但更复杂 |
| HITL | interrupt() | 高风险判断处 | 暂停等待人工 | 需要 checkpoint |
10.2 结构化 Evaluator 源码协作解剖
职责与所处阶段
结构化 Evaluator 是 Reflection 的关键扩展。它不属于 LangGraph 核心,但与 LangGraph state 强协作:evaluator 输出对象中的字段会被拆成 partial state update。
真实源码签名
用户侧结构示例:
from pydantic import BaseModel, Fieldfrom typing import Literal
class EvaluationResult(BaseModel): quality_score: float = Field(ge=0, le=1) critique: str should_revise: bool risk_level: Literal["low", "medium", "high"]调用方与被调用方
evaluate_draft node ↓model.with_structured_output(EvaluationResult) ↓parsed EvaluationResult ↓partial state update输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | draft_plan | 当前草案 |
| 输出 | EvaluationResult | 结构化评价 |
| 状态变化 | quality_score、critique | 写入 LangGraph state |
| 副作用 | 模型调用 | 可追踪、可 retry |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def evaluate_draft(state, runtime): # 1. 构造结构化 evaluator evaluator = runtime.context.evaluator_model.with_structured_output( EvaluationResult )
# 2. 调用 evaluator result = evaluator.invoke({ "user_request": state["user_request"], "draft_plan": state["draft_plan"], "criteria": runtime.context.criteria, })
# 3. 将结构化对象映射为 state update return { "quality_score": result.quality_score, "critique": result.critique, }逐段解释
第 1 段把 evaluator 的输出约束成结构化对象,避免 route function 解析自然语言。
第 3 段只写入 route 所需字段。如果需要更完整的诊断,可以额外写入 issues 或 critique_history。
正常路径
LLM evaluation ↓EvaluationResult ↓quality_score / critique ↓route_by_score关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| schema 校验成功 | 写入 state | 正常路由 |
| schema 校验失败 | 抛解析错误 | retry / fallback |
| evaluator 分数偏差 | 路由错误 | 需要 eval dataset 校准 |
设计原因与工程影响
Evaluator 输出应该结构化,因为 route function 是代码,不应该解析“这个计划还行,但……”这样的自然语言。
源码证据
langchain-corestructured output 机制见第 3 篇- LangGraph node 只要求返回 partial state update
10.3 Revision History 与 Reducer 协作源码解剖
职责与所处阶段
如果担心新版更差,需要保留历史版本。这个能力可以通过 reducer 实现:每次 revise_draft 追加一个版本,而不是只覆盖 draft_plan。
真实源码签名
用户 state 示例:
from typing import Annotatedfrom operator import add
class TravelState(TypedDict): draft_plan: str | None draft_history: Annotated[list[str], add] best_draft: str | None best_score: float | None调用方与被调用方
revise_draft ↓return {"draft_history": [new_draft]} ↓channel reducer add ↓state["draft_history"] 追加输入、输出与状态变化
| 项目 | 类型 | 说明 |
|---|---|---|
| 输入 | draft_history | 历史版本列表 |
| 输出 | list[str] update | 新版本 |
| 状态变化 | 追加 | reducer 合并 |
| 副作用 | 无 | 状态内记录 |
细粒度伪代码
以下为保留关键控制流的简化伪代码,不是源码逐字复制:
def revise_draft(state): new_draft = revise_model.invoke(...).content new_count = state["revision_count"] + 1
return { "draft_plan": new_draft, # 覆盖当前版本 "draft_history": [new_draft], # 追加历史版本 "revision_count": new_count, }逐段解释
draft_plan 是覆盖式字段,始终代表当前版本。
draft_history 是追加式字段,用 reducer 保存所有历史版本。
best_draft 和 best_score 可以由 evaluator 或专门节点维护,防止最终输出最后一版但不是最好一版。
正常路径
revise_draft ↓覆盖 draft_plan ↓追加 draft_history ↓evaluate_draft关键分支与异常路径
| 条件 | 行为 | 结果 |
|---|---|---|
| 使用 reducer | 历史版本保留 | 可回滚 |
| 不使用 reducer | 只保留当前版本 | 无法比较旧版本 |
| 历史无限增长 | 状态膨胀 | 需要裁剪或 checkpoint 策略 |
设计原因与工程影响
Reflection 常见问题是“越改越差”。保存历史版本可以让 final node 选择最佳版本,而不是最后版本。
源码证据
libs/langgraph/langgraph/graph/state.pyschema / channel 解析libs/langgraph/langgraph/channels/reducer channel
10.4 与相邻框架组件的协作
| 相邻组件 | 输入协议 | 输出协议 | 协作边界 |
|---|---|---|---|
| Structured Output | prompt / messages / schema | Pydantic object | 生成 evaluator 结构化结果 |
| Reducer | state key update | merged state | 保存 critique / draft history |
| Checkpoint | thread_id / state snapshot | persisted state | 支持回放和恢复 |
| Interrupt | interrupt payload | Command resume | 高风险场景人工确认 |
| Subgraph | parent state 映射 | subgraph output | 封装复杂 evaluator 或 revise 流程 |
| LangSmith | traces | eval / debugging | 观察每轮修改质量 |
10.5 公共扩展接口与内部实现
业务代码可以依赖: StateGraph add_node add_edge add_conditional_edges compile graph.invoke / graph.stream recursion_limit reducer annotation
业务代码避免依赖: BranchSpec 内部字段 PregelLoop 内部 step 结构 私有 channel 名称 未文档化的 task 元数据10.6 自定义扩展示例
示例只展示扩展契约,不重新实现框架:
class ReflectionConfig(TypedDict): max_revision: int score_threshold: float
def route_by_score(state: TravelState, config: RunnableConfig): threshold = config.get("configurable", {}).get("score_threshold", 0.8) max_revision = config.get("configurable", {}).get("max_revision", 2)
if (state["quality_score"] or 0.0) >= threshold: return "final_response" if state["revision_count"] >= max_revision: return "final_response" return "revise_draft"说明:
- 扩展点接收当前 state 和 config。
- 扩展点允许修改路由决策。
- 扩展点必须返回合法目标节点。
- 异常会导致图运行失败。
- 不应在 route function 内修改 state。
10.7 选择扩展还是重写流程
| 条件 | 选择扩展点 | 选择更底层框架 |
|---|---|---|
| 只调整分数阈值 | 是 | 否 |
| 增加 critique history | 是 | 否 |
| evaluator 变成多节点流程 | 视复杂度 | 是,考虑 subgraph |
| 引入人工审批 | 否 | 是,使用 interrupt |
| 多个专家并行评价 | 否 | 是,使用并行分支 + reducer |
| 需要复杂搜索树 | 否 | 是,单独建搜索图 |
11. 工程决策与适用场景
11.1 适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 旅行计划质量优化 | 是 | 有明确用户偏好和可迭代改进空间 |
| 文案润色 | 是 | 质量指标主观但可评价 |
| 报告生成 | 是 | 可通过结构化评价逐步改进 |
| 代码修复 | 是 | 可结合测试结果做 verifier |
| 退款审批 | 否 | 高风险动作,不能靠反思自动放行 |
| 权限判断 | 否 | 应使用确定性 policy / RBAC |
| 合规决策 | 否 | 需要规则、证据和审计 |
11.2 工程决策表
| 决策点 | 推荐选择 | 前提 | 风险 |
|---|---|---|---|
| evaluator 输出 | 结构化输出 | 需要稳定 route | schema 设计过复杂会失败 |
| 修改轮次 | MAX_REVISION 2–3 | 质量优化任务 | 过高导致成本失控 |
| 循环保护 | 同时设置 recursion_limit | 图中有循环 | 过低可能误杀复杂流程 |
| 草案保存 | 当前版本 + 可选 history | 需要回滚 | 状态膨胀 |
| 模型选择 | generator 可便宜,evaluator 可更强 | 评价重要 | 成本上升 |
| 高风险动作 | Reflection 后仍进 verifier/HITL | 有副作用 | 自动放行风险 |
11.3 性能、可靠性与安全边界
性能:每次 revise 都至少增加一次 generator 和一次 evaluator 调用。可靠性:主要失败模式是 evaluator 标准不清、循环无法结束、越改越差。安全:Reflection 不能替代 policy、权限、合规和人工确认。可观测性:必须记录 draft、critique、score、revision_count、route decision。12. 常见误区与源码纠正
12.1 误区:Reflection 就是在 prompt 里写“再检查一下”
错误原因:
很多教程把 Reflection 写成单次模型调用后的二次提示,没有状态字段,也没有可测试的停止条件。
源码事实:
LangGraph 中可控 Reflection = state 字段 + evaluator node + route branch + revise loop + stop condition。工程影响:
如果只靠 prompt,无法稳定知道模型是否真的检查过,也无法做 trace、评估和回滚。
12.2 误区:route function 可以顺便修改 revision_count
错误原因:
route function 能拿到 state,容易误以为可以直接原地修改。
源码事实:
LangGraph 的状态更新协议是 node 返回 partial update;route function 的职责是返回路径。工程影响:
在 route 中修改 state 会破坏数据流清晰性,也可能不会被框架作为正式 state update 追踪。
12.3 误区:只设置 recursion_limit 就能防止反思循环失控
错误原因:
recursion_limit 看起来能限制循环步数。
源码事实:
recursion_limit 是框架级 super-step 上限;MAX_REVISION 是业务级停止条件。工程影响:
只依赖 recursion limit 会让任务以异常结束,用户和日志都难以解释为什么停止。
12.4 误区:evaluator 输出自然语言也可以
错误原因:
人类读 critique 时自然语言很舒服。
源码事实:
route_by_score 需要稳定读取 quality_score / should_revise 等结构化字段。工程影响:
自然语言评价会导致路由逻辑脆弱,后续测试也难以量化。
12.5 误区:Reflection 可以替代 Verifier
错误原因:
evaluator 看起来也在“检查”。
源码事实:
Reflection 主要优化质量;Verifier / Policy 负责确定性规则和高风险边界。工程影响:
让 Reflection 自动批准退款、下单、发邮件等动作,会产生严重安全风险。
13. 最终心智模型与掌握检查
13.1 构建期心智模型
TravelState ↓add_node(generator / evaluator / revise / final) ↓add_conditional_edges(evaluate_draft, route_by_score) ↓add_edge(revise_draft, evaluate_draft) ↓compile ↓CompiledStateGraph13.2 运行时心智模型
initial state ↓generate_draft ↓evaluate_draft ↓route_by_score ├── revise_draft → evaluate_draft └── final_response → END13.3 分支与异常心智模型
正常路径 → quality_score 达标 → final_response保底路径 → revision_count 达上限 → final_response修改路径 → 分数低且未达上限 → revise_draft可恢复异常 → evaluator / revise 失败 → retry 或 fallback不可恢复异常 → 路由目标非法 / state 输出非法 → 抛出保护上限 → recursion_limit 超限 → GraphRecursionError13.4 一句话总结
Reflection 通过
StateGraph构建 generator、evaluator、revise 和 final 节点,运行时沿evaluate → route → revise循环推进 state,使用quality_score与revision_count作为业务停止条件,并通过recursion_limit、structured evaluator、reducer history、checkpoint 和 verifier 协作,形成可观测、可终止、可回滚的评价-修改机制。
13.5 掌握检查
- 能说清 Reflection 和普通二次 prompt 的区别。
- 能画出
generate → evaluate → route → revise → evaluate的图结构。 - 能解释
route_by_score为什么不应修改 state。 - 能说明
revision_count应该在哪里递增。 - 能说明 evaluator 为什么应该结构化输出。
- 能区分
MAX_REVISION和recursion_limit。 - 能说明新版本更差时如何回滚。
- 能说明 Reflection 为什么不能替代 Policy / Verifier。
- 能判断旅行规划助手哪些节点适合 Reflection。
14. 参考资料与下一篇衔接
14.1 官方概念文档
-
LangGraph Workflows and agents:Evaluator-optimizer
https://docs.langchain.com/oss/python/langgraph/workflows-agents -
LangGraph Graph API overview
https://docs.langchain.com/oss/python/langgraph/graph-api -
LangGraph Thinking in LangGraph
https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph -
LangGraph Recursion Limit Error
https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
14.2 官方 API Reference
-
StateGraph
https://reference.langchain.com/python/langgraph/graphs/#langgraph.graph.state.StateGraph -
StateGraph.add_conditional_edges
https://reference.langchain.com/python/langgraph/graph/state/StateGraph/add_conditional_edges -
CompiledStateGraph
https://reference.langchain.com/python/langgraph/graphs/#langgraph.graph.state.CompiledStateGraph
14.3 官方源码
-
langgraph/graph/state.py::StateGraph
https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/state.py -
langgraph/graph/branch.py::BranchSpec
https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/graph/branch.py -
langgraph/pregel/loop.py
https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/pregel/loop.py -
langgraph/errors.py::GraphRecursionError
https://github.com/langchain-ai/langgraph/blob/5931a5f/libs/langgraph/langgraph/errors.py
14.4 下一篇衔接
下一篇进入:
第 11 篇:Checkpoint / Thread / Durable Execution 源码解剖需要继续回答:
thread_id 如何定位一次长期执行?checkpoint 在每个 super-step 如何保存 state snapshot?get_state / get_state_history 如何读取历史?interrupt / resume 为什么必须依赖 checkpointer?生产级 Agent 如何用 checkpoint 支撑恢复、回放、HITL 和长期记忆?