用 LangGraph 从零实现 ReCAP:一个可运行的递归任务规划框架(纯模拟版)

上文书中说道(浅谈 ReCAP(Recursive Context-Aware Reasoning and Planning)------大模型推理与规划的递归范式),ReCAP(Recursive Context-Aware Planning) 用显式递归 + 任务栈结构,让 LLM 能够处理真正的长链条任务(Long-horizon tasks)。

本文使用 LangGraph + Python 模拟ReCAP框架,通过代码的方式讲解:

  • ReCAP 的核心思想是什么?
  • 为什么智能体需要"显式递归与栈帧"?
  • 如何用 LangGraph 实现一个完整、可运行的 ReCAP 执行器?
  • 代码架构如何组织?每部分组件扮演什么角色?

我们以前文中一个简单的任务作为例子展开讲解:

root_task = "做一个牛肉汉堡"

(注:本文的所有代码均为可直接运行的纯模拟版 ,不依赖任何 LLM API。文末附完整项目代码


1. ReCAP 的核心思想:显式递归 + 上下文修正 + 结构化计划

ReCAP 的核心可以归纳为三句话:

① 任务是可以被递归拆解的(Task → Subtasks)

如果一个任务不可直接执行,它应被拆成子任务,再逐个求解。这就天然形成一棵任务树。

② 每个任务在执行过程中,可以因观察(obs)而动态修正计划(Refine)

真实世界具有不确定性,执行动作后环境会变化,因此需要:

  • 根据反馈动态调整剩余子任务
  • 保留(或更新)当前任务的思考(Think)

③ 整个执行过程必须由一个显式的"栈"来管理递归(类似函数调用栈)

ReCAP 论文强调:

长任务执行需要"持久结构化记忆",不能仅靠 token stream。

因此我们会显式维护一个 stack = [Frame, Frame, Frame...]

每一层 Frame 包含:

python 复制代码
{
    "task": 任务名称,
    "think": 当前层的思考内容,
    "subtasks": 该层拆解出的子任务列表,
    "cursor": 当前执行到第几个子任务,
}

可把它理解为一次"函数调用"的运行时结构。


2. 为什么 LangGraph 天然适合实现 ReCAP?

LangGraph 本质是一个控制流程编排引擎,相比传统 chain 或 Agent:

  • 它允许你显式控制循环、条件分支、状态流转
  • 它接受一个显式的 state,可以持续更新、持久保存
  • 允许 streaming 观察每一步状态变化
  • 几乎可以把任何复杂执行逻辑都变成一个状态机

ReCAP 恰恰需要:

  • 可控循环(直到任务完成)
  • 持续更新的状态(stack + obs)
  • 在每一步根据状态决定下一步行为

因此:LangGraph 是目前最适合实现 ReCAP 的框架之一。


3. 系统架构概览(Architecture Overview)

下面是本项目的结构图:

复制代码
┌───────────────────────────────────────────────┐
│                LangGraph State                │
│                                               │
│  root_task  ------ 初始目标                       │
│  stack      ------ ReCAP 递归执行栈               │
│  last_obs   ------ 最近的环境观察反馈             │
│  done       ------ 是否执行完成                   │
└───────────────────────────────────────────────┘

              ▼ 每一步执行 recap_step

┌───────────────────────────────────────────────┐
│ recap_step():                                  │
│                                               │
│ 1. 若栈为空 → 初始化根任务                    │
│ 2. 取出栈顶 Frame:                           │
│    - 若没有子任务 → 弹栈 + refine 父任务       │
│    - 若有子任务:                              │
│        a) primitive → 执行动作,refine 当前层   │
│        b) 非 primitive → 创建子 Frame 压栈      │
└───────────────────────────────────────────────┘

              ▼ 条件流转

route_next():
- 若 done = True → END
- 否则 → recap_step(继续循环)

用一句话概括:所有 ReCAP 的逻辑都浓缩在一个节点 recap_step 中,而 LangGraph 则负责控制其循环执行与状态管理。


4. 系统模块解析

首先介绍本项目代码与论文的对应部分(论文链接:https://arxiv.org/pdf/2510.23822)

  • 栈帧 Frame → 论文中每个任务的上下文 C
  • plan_llm → π ( C )
  • refine_llm → ρ ( C )
  • 栈结构 → 递归执行与回溯
  • LangGraph → 有向图驱动执行主循环

下面按功能拆解代码结构,并解释设计动机。


4.1 Frame & GraphState:ReCAP 的运行时模型

每个 Frame 就是一个任务节点的执行上下文:

python 复制代码
# ========== 1. 状态与栈帧定义 ==========
class Frame(TypedDict):
    """ReCAP 中的一层任务帧(相当于递归函数调用栈的一帧)"""
    task: str  # 当前任务描述
    think: str  # 当前任务层的思考 T(这里用字符串模拟)
    subtasks: List[str]  # 当前层的子任务列表 S
    cursor: int  # 当前执行到第几个子任务(下标,从 0 开始)

决定这层任务"下一步要干什么"。

GraphState 是 LangGraph 的整体状态:

python 复制代码
class GraphState(TypedDict):
    """
    LangGraph 的全局状态 = ReCAP 论文里的 Context C + 调用栈
    """
    root_task: str  # 根任务,例如 "做一个牛肉汉堡"
    stack: List[Frame]  # 手动维护的递归任务栈
    last_obs: Optional[str]  # 最近一次执行动作的环境反馈
    done: bool  # 整体任务是否完成

这使得 LangGraph 能在每一步迭代中携带整个 ReCAP 的上下文。


4.2 提供一个"模拟环境":primitive + action

ReCAP 的环境交互通过 primitive 动作实现:

python 复制代码
# ========== 2. 环境与 primitive 判定 ==========

VALID_ACTIONS = [
    "拿起面包片",
    "煎牛肉饼",
    "切生菜",
    "涂酱",
    "组装汉堡",
]

def is_primitive(subtask: str) -> bool:
    """
    primitive = 可以直接执行的环境动作(叶子节点)
    在这个例子里:
    - 精确等于 VALID_ACTIONS 的动作视为 primitive
    - 或者也可以约定以 "ACTION:" 开头的也算 primitive
    """
    return subtask in VALID_ACTIONS or subtask.startswith("ACTION:")

def execute_action(action: str) -> str:
    """
    执行 primitive 动作,返回观察(obs)
    这里是一个极简的「模拟环境」。
    """
    if action not in VALID_ACTIONS:
        return f"动作[{action}]无效:当前环境不支持该操作。"
    return f"已执行动作[{action}],环境状态有所进展。"

这样可以专注于逻辑,而忽略实际环境复杂性。


4.3 plan_llm:模拟 LLM 的任务拆解函数 π ( C )

python 复制代码
# ========== 3. 模拟 plan 函数 π(C) ==========

def plan_llm(task: str) -> tuple[str, List[str]]:
    """
    用硬编码规则模拟 ReCAP 的 plan:
    给定一个任务,返回 (think, subtasks)

    可以根据需要扩展这些规则。
    """
    task = task.strip()

    # 根任务
    if task in ("做一个牛肉汉堡", "做牛肉汉堡"):
        think = "先准备食材,再处理牛肉,最后组装汉堡。"
        subtasks = ["准备食材", "处理牛肉", "组装汉堡"]

    # 子任务:准备食材
    elif task == "准备食材":
        think = "需要把面包和生菜准备好。"
        subtasks = ["拿起面包片", "切生菜"]

    # 子任务:处理牛肉
    elif task == "处理牛肉":
        think = "把牛肉饼煎熟。"
        subtasks = ["煎牛肉饼"]

    # 子任务:组装汉堡
    elif task == "组装汉堡":
        think = "先涂酱,再把所有材料组装起来。"
        subtasks = ["涂酱", "组装汉堡"]

    else:
        # 未知任务:当作已经原子化的动作,或者空任务处理
        think = f"任务[{task}]没有专门的拆解规则,尝试直接执行或跳过。"
        subtasks = []

    return think, subtasks

它给出对该任务的一个思考(think)并给出子任务(subtasks)。

在真实系统中这里会替换为 LLM 调用,而本文只是用规则模拟。


4.4 refine_llm:模拟 ReCAP 论文里的观察驱动修正

python 复制代码
# ========== 4. 模拟 refine 函数 ρ(C) ==========

def refine_llm(
        frame: Frame,
        obs: Optional[str],
        is_child_done: bool = False,
        child_task: Optional[str] = None,
) -> Frame:
    """
    模拟 ReCAP 的 refine 行为:

    - 真实论文里会根据【最新观察 obs】【剩余子任务】动态调整计划。
    - 这里简化成:
        * 保留「剩余子任务」不变(不新增、不删除)
        * 但会重置 cursor,从新的剩余列表的头开始执行
        * think 字段简单拼接一下「已收到反馈」,纯显示用
    """
    old_think = frame["think"]
    cursor = frame["cursor"]
    old_subtasks = frame["subtasks"]

    # 剩余子任务 = 当前 cursor 之后的所有子任务
    if cursor < len(old_subtasks):
        remaining = old_subtasks[cursor:]
    else:
        remaining = []

    # 简单处理:把反馈记在 think 里,便于打印观察
    extra_info = ""
    if is_child_done and child_task:
        extra_info += f"[子任务 {child_task} 已完成] "
    if obs:
        extra_info += f"[最新观察: {obs}]"

    new_think = old_think
    if extra_info:
        new_think = old_think + " " + extra_info

    new_frame: Frame = {
        "task": frame["task"],
        "think": new_think,
        "subtasks": remaining,
        "cursor": 0,  # 从新的剩余列表头开始
    }
    return new_frame

Refine 做两件事:

  • 根据 obs / 子任务完成情况修正当前任务状态
  • 重建 subtasks 的剩余部分并重置 cursor

这使得 ReCAP 执行过程具有"反馈驱动"的特点。


4.5 recap_step:ReCAP 的单步执行逻辑(核心)

这是整个系统的核心:

python 复制代码
# ========== 5. ReCAP 主节点:一次执行一个"栈帧步骤" ==========

def recap_step(state: GraphState) -> GraphState:
    """
    这是 LangGraph 中的一个节点:
    - 如果栈为空:初始化根任务 Frame(相当于 ReCAP(root) 的第一次调用)
    - 如果栈非空:取栈顶 Frame,执行一个「子任务或递归一步」
    """
    # 确保 stack 至少是个 list(防御一下)
    if "stack" not in state or state["stack"] is None:
        state["stack"] = []

    stack = state["stack"]

    # 1. 如果栈为空 → 初始化根任务 Frame
    if not stack:
        root_task = state["root_task"]
        think, subtasks = plan_llm(root_task)
        root_frame: Frame = {
            "task": root_task,
            "think": think,
            "subtasks": subtasks,
            "cursor": 0,
        }
        stack.append(root_frame)
        state["stack"] = stack
        state["last_obs"] = None
        state["done"] = False
        return state

    # 2. 取出当前栈顶 Frame
    frame = stack[-1]

    # 2.1 如果当前 Frame 没有子任务了 → 认为当前任务已完成,弹栈,然后回到父任务做 refine
    if frame["cursor"] >= len(frame["subtasks"]):
        finished_task = frame["task"]
        stack.pop()  # 当前任务完成

        # 没有父任务了 → 整体完成
        if not stack:
            state["done"] = True
            state["last_obs"] = f"任务[{finished_task}]已完成,所有任务结束。"
            return state

        # 有父任务 → 回到父任务,调用 refine_llm 更新它的剩余子任务
        parent = stack[-1]
        parent_refined = refine_llm(
            frame=parent,
            obs=state.get("last_obs"),
            is_child_done=True,
            child_task=finished_task,
        )
        # 用最新的 think/subtasks 替换父 Frame
        stack[-1] = parent_refined
        state["stack"] = stack
        return state

    # 3. 当前 Frame 还有子任务要执行
    subtask = frame["subtasks"][frame["cursor"]]

    # 3.1 primitive → 执行动作(叶子节点)
    if is_primitive(subtask):
        obs = execute_action(subtask)
        state["last_obs"] = obs

        # 更新当前 Frame 的 cursor(这一项已执行)
        frame["cursor"] += 1

        # 针对当前 Frame 做一次 refine(类似 Leaf Backtracking)
        refined = refine_llm(
            frame=frame,
            obs=obs,
        )
        stack[-1] = refined
        state["stack"] = stack
        return state

    # 3.2 非 primitive → 作为新的子任务 Frame 压栈(递归向下)
    else:
        child_task = subtask
        child_think, child_subtasks = plan_llm(child_task)
        child_frame: Frame = {
            "task": child_task,
            "think": child_think,
            "subtasks": child_subtasks,
            "cursor": 0,
        }

        # 父 Frame 的 cursor 往后移一位,表示这个位置的任务由子 Frame 负责
        frame["cursor"] += 1
        stack[-1] = frame

        # 压入子 Frame
        stack.append(child_frame)
        state["stack"] = stack
        # last_obs 保留为之前的值,等子任务执行后再更新
        return state

recap_step 的行为逻辑:

  1. 栈为空 → 初始化根任务

  2. 取出栈顶 Frame

  3. 若当前任务已经无子任务 → 弹栈 + refine 父任务

  4. 否则:

    • 若子任务是 primitive → 执行动作 + refine
    • 若是 composite → 拆成新的 Frame,压栈

最终形成一个可控的"递归展开 → 执行 → 回溯 → 完成"的全过程。


4.6 LangGraph 图构建

python 复制代码
# ========== 6. 路由函数:继续还是结束 ==========
def route_next(state: GraphState) -> str:
    """根据状态决定下一步是继续 recap_step,还是结束"""
    if state.get("done"):
        return "end"
    return "continue"

# ========== 7. 构建 LangGraph 图 ==========
def build_graph():
    builder = StateGraph(GraphState)

    builder.add_node("recap_step", recap_step)

    # START → recap_step
    builder.add_edge(START, "recap_step")

    # recap_step → (recap_step 或 END)
    builder.add_conditional_edges(
        "recap_step",
        route_next,
        {
            "continue": "recap_step",
            "end": END,
        },
    )

    graph = builder.compile()
    return graph

由此建立 ReCAP 的执行主循环。


5. 整体流程演示(任务:做一个牛肉汉堡)

执行过程大致如下:

复制代码
Step 0: 初始化 root 任务
    task = 做一个牛肉汉堡
    subtasks = [准备食材, 处理牛肉, 组装汉堡]

Step 1: 进入 "准备食材"
Step 2: 执行 primitive "拿起面包片"
Step 3: 执行 primitive "切生菜"
    → 上层任务准备食材完成 → refine 父任务

Step 4: 进入 "处理牛肉"
Step 5: 执行 primitive "煎牛肉饼"

Step 6: 进入 "组装汉堡"
Step 7: 执行 "涂酱"
Step 8: 执行 "组装汉堡"

Step 9: root 任务完成 → END

实际的程序运行结果为:

复制代码
=== ReCAP + LangGraph Demo(纯模拟版): 做一个牛肉汉堡 ===

====== Step 0 ======
当前栈深度: 0
当前栈为空
last_obs: None

====== Step 1 ======
当前栈深度: 1
当前任务: 做一个牛肉汉堡
当前子任务列表: ['准备食材', '处理牛肉', '组装汉堡']
cursor: 0
当前层 think: 先准备食材,再处理牛肉,最后组装汉堡。
last_obs: None

====== Step 2 ======
当前栈深度: 2
当前任务: 准备食材
当前子任务列表: ['拿起面包片', '切生菜']
cursor: 0
当前层 think: 需要把面包和生菜准备好。
last_obs: None

====== Step 3 ======
当前栈深度: 2
当前任务: 准备食材
当前子任务列表: ['切生菜']
cursor: 0
当前层 think: 需要把面包和生菜准备好。 [最新观察: 已执行动作[拿起面包片],环境状态有所进展。]
last_obs: 已执行动作[拿起面包片],环境状态有所进展。

====== Step 4 ======
当前栈深度: 2
当前任务: 准备食材
当前子任务列表: []
cursor: 0
当前层 think: 需要把面包和生菜准备好。 [最新观察: 已执行动作[拿起面包片],环境状态有所进展。] [最新观察: 已执行动作[切生菜],环境状态有所进展。]
last_obs: 已执行动作[切生菜],环境状态有所进展。

====== Step 5 ======
当前栈深度: 1
当前任务: 做一个牛肉汉堡
当前子任务列表: ['处理牛肉', '组装汉堡']
cursor: 0
当前层 think: 先准备食材,再处理牛肉,最后组装汉堡。 [子任务 准备食材 已完成] [最新观察: 已执行动作[切生菜],环境状态有所进展。]
last_obs: 已执行动作[切生菜],环境状态有所进展。

====== Step 6 ======
当前栈深度: 2
当前任务: 处理牛肉
当前子任务列表: ['煎牛肉饼']
cursor: 0
当前层 think: 把牛肉饼煎熟。
last_obs: 已执行动作[切生菜],环境状态有所进展。

====== Step 7 ======
当前栈深度: 2
当前任务: 处理牛肉
当前子任务列表: []
cursor: 0
当前层 think: 把牛肉饼煎熟。 [最新观察: 已执行动作[煎牛肉饼],环境状态有所进展。]
last_obs: 已执行动作[煎牛肉饼],环境状态有所进展。

====== Step 8 ======
当前栈深度: 1
当前任务: 做一个牛肉汉堡
当前子任务列表: ['组装汉堡']
cursor: 0
当前层 think: 先准备食材,再处理牛肉,最后组装汉堡。 [子任务 准备食材 已完成] [最新观察: 已执行动作[切生菜],环境状态有所进展。] [子任务 处理牛肉 已完成] [最新观察: 已执行动作[煎牛肉饼],环境状态有所进展。]
last_obs: 已执行动作[煎牛肉饼],环境状态有所进展。

====== Step 9 ======
当前栈深度: 1
当前任务: 做一个牛肉汉堡
当前子任务列表: []
cursor: 0
当前层 think: 先准备食材,再处理牛肉,最后组装汉堡。 [子任务 准备食材 已完成] [最新观察: 已执行动作[切生菜],环境状态有所进展。] [子任务 处理牛肉 已完成] [最新观察: 已执行动作[煎牛肉饼],环境状态有所进展。] [最新观察: 已执行动作[组装汉堡],环境状态有所进展。]
last_obs: 已执行动作[组装汉堡],环境状态有所进展。

====== Step 10 ======
当前栈深度: 0
当前栈为空
last_obs: 任务[做一个牛肉汉堡]已完成,所有任务结束。
所有任务完成。

这个流程完全符合 ReCAP 的设计:

  • 任务被递归拆解
  • 每层任务的状态由 Frame 持续追踪
  • 环境反馈会驱动 refine 行为
  • 完成后逐层回溯

6. 可扩展的方向

这个架构具备极强的扩展能力:

① 将 plan_llm / refine_llm 替换为真实 LLM

可无缝接入 GPT / Qwen / Claude / Gemini。

② 扩展为工具调用系统

primitive 动作可映射为:

  • HTTP API Tool
  • Python Tool
  • Shell Tool
  • Browser Action

③ 扩展为多智能体系统

不同子任务可分配给不同 Agent:

  • Planner
  • Worker
  • Critic
  • Executor

④ 支持持久化与断点恢复

LangGraph CheckpointSaver 可天然支持大任务的恢复执行。

⑤ 支持实时 UI 可视化(如 AG-UI 协议)

可把每一步 recap_step 的状态推到前端渲染。


7. 总结

智能体系统正在从"黑箱的 ReAct 工程"向"可控制、可调试、可扩展的结构化系统"演进,而 ReCAP 或许会成为其中关键的一环。

本篇示例展示了:

  • 如何把 ReCAP 的递归执行模型映射到 LangGraph
  • 如何构建任务栈 Frame
  • 如何实现 plan / refine 的行为循环
  • LangGraph 是如何保证 flow 可控且可重入的

如果你正在构建:

  • 深度工具链 AI Agent
  • 多智能体协作系统
  • 企业级自动化任务执行引擎
  • LangGraph 层级任务编排系统

那么笔者希望本文的实现可以为你提供一些思路。


附完整项目代码:

python 复制代码
"""
ReCAP + LangGraph 示例(纯模拟版,无需接入 LLM)

核心点:
- 用 LangGraph 的 StateGraph 做主循环
- 手动维护一个「任务栈 stack」,模拟 ReCAP 的递归执行栈
- 每一层栈帧 Frame = {task, think, subtasks, cursor}
- plan_llm / refine_llm 都用本地模拟逻辑,不调用任何模型或网络
"""

from __future__ import annotations

from typing import List, TypedDict, Optional

from langgraph.graph import StateGraph, START, END


# ========== 1. 状态与栈帧定义 ==========

class Frame(TypedDict):
    """ReCAP 中的一层任务帧(相当于递归函数调用栈的一帧)"""
    task: str  # 当前任务描述
    think: str  # 当前任务层的思考 T(这里用字符串模拟)
    subtasks: List[str]  # 当前层的子任务列表 S
    cursor: int  # 当前执行到第几个子任务(下标,从 0 开始)


class GraphState(TypedDict):
    """
    LangGraph 的全局状态 = ReCAP 论文里的 Context C + 调用栈
    """
    root_task: str  # 根任务,例如 "做一个牛肉汉堡"
    stack: List[Frame]  # 手动维护的递归任务栈
    last_obs: Optional[str]  # 最近一次执行动作的环境反馈
    done: bool  # 整体任务是否完成


# ========== 2. 玩具环境与 primitive 判定 ==========

VALID_ACTIONS = [
    "拿起面包片",
    "煎牛肉饼",
    "切生菜",
    "涂酱",
    "组装汉堡",
]


def is_primitive(subtask: str) -> bool:
    """
    primitive = 可以直接执行的环境动作(叶子节点)
    在这个玩具例子里:
    - 精确等于 VALID_ACTIONS 的动作视为 primitive
    - 或者你也可以约定以 "ACTION:" 开头的也算 primitive
    """
    return subtask in VALID_ACTIONS or subtask.startswith("ACTION:")


def execute_action(action: str) -> str:
    """
    执行 primitive 动作,返回观察(obs)
    这里是一个极简的「假环境」。
    """
    if action not in VALID_ACTIONS:
        return f"动作[{action}]无效:当前环境不支持该操作。"
    return f"已执行动作[{action}],环境状态有所进展。"


# ========== 3. 模拟版 plan 函数 π(C) ==========

def plan_llm(task: str) -> tuple[str, List[str]]:
    """
    用硬编码规则模拟 ReCAP 的 plan:
    给定一个任务,返回 (think, subtasks)

    你可以根据需要扩展这些规则。
    """
    task = task.strip()

    # 根任务
    if task in ("做一个牛肉汉堡", "做牛肉汉堡"):
        think = "先准备食材,再处理牛肉,最后组装汉堡。"
        subtasks = ["准备食材", "处理牛肉", "组装汉堡"]

    # 子任务:准备食材
    elif task == "准备食材":
        think = "需要把面包和生菜准备好。"
        subtasks = ["拿起面包片", "切生菜"]

    # 子任务:处理牛肉
    elif task == "处理牛肉":
        think = "把牛肉饼煎熟。"
        subtasks = ["煎牛肉饼"]

    # 子任务:组装汉堡
    elif task == "组装汉堡":
        think = "先涂酱,再把所有材料组装起来。"
        subtasks = ["涂酱", "组装汉堡"]

    else:
        # 未知任务:当作已经原子化的动作,或者空任务处理
        think = f"任务[{task}]没有专门的拆解规则,尝试直接执行或跳过。"
        subtasks = []

    return think, subtasks


# ========== 4. 模拟版 refine 函数 ρ(C) ==========

def refine_llm(
        frame: Frame,
        obs: Optional[str],
        is_child_done: bool = False,
        child_task: Optional[str] = None,
) -> Frame:
    """
    模拟 ReCAP 的 refine 行为:

    - 真实论文里会根据【最新观察 obs】【剩余子任务】动态调整计划。
    - 这里简化成:
        * 保留「剩余子任务」不变(不新增、不删除)
        * 但会重置 cursor,从新的剩余列表的头开始执行
        * think 字段简单拼接一下「已收到反馈」,纯显示用
    """
    old_think = frame["think"]
    cursor = frame["cursor"]
    old_subtasks = frame["subtasks"]

    # 剩余子任务 = 当前 cursor 之后的所有子任务
    if cursor < len(old_subtasks):
        remaining = old_subtasks[cursor:]
    else:
        remaining = []

    # 简单处理:把反馈记在 think 里,便于打印观察
    extra_info = ""
    if is_child_done and child_task:
        extra_info += f"[子任务 {child_task} 已完成] "
    if obs:
        extra_info += f"[最新观察: {obs}]"

    new_think = old_think
    if extra_info:
        new_think = old_think + " " + extra_info

    new_frame: Frame = {
        "task": frame["task"],
        "think": new_think,
        "subtasks": remaining,
        "cursor": 0,  # 从新的剩余列表头开始
    }
    return new_frame


# ========== 5. ReCAP 主节点:一次执行一个"栈帧步骤" ==========

def recap_step(state: GraphState) -> GraphState:
    """
    这是 LangGraph 中的一个节点:
    - 如果栈为空:初始化根任务 Frame(相当于 ReCAP(root) 的第一次调用)
    - 如果栈非空:取栈顶 Frame,执行一个「子任务或递归一步」
    """
    # 确保 stack 至少是个 list(防御一下)
    if "stack" not in state or state["stack"] is None:
        state["stack"] = []

    stack = state["stack"]

    # 1. 如果栈为空 → 初始化根任务 Frame
    if not stack:
        root_task = state["root_task"]
        think, subtasks = plan_llm(root_task)
        root_frame: Frame = {
            "task": root_task,
            "think": think,
            "subtasks": subtasks,
            "cursor": 0,
        }
        stack.append(root_frame)
        state["stack"] = stack
        state["last_obs"] = None
        state["done"] = False
        return state

    # 2. 取出当前栈顶 Frame
    frame = stack[-1]

    # 2.1 如果当前 Frame 没有子任务了 → 认为当前任务已完成,弹栈,然后回到父任务做 refine
    if frame["cursor"] >= len(frame["subtasks"]):
        finished_task = frame["task"]
        stack.pop()  # 当前任务完成

        # 没有父任务了 → 整体完成
        if not stack:
            state["done"] = True
            state["last_obs"] = f"任务[{finished_task}]已完成,所有任务结束。"
            return state

        # 有父任务 → 回到父任务,调用 refine_llm 更新它的剩余子任务
        parent = stack[-1]
        parent_refined = refine_llm(
            frame=parent,
            obs=state.get("last_obs"),
            is_child_done=True,
            child_task=finished_task,
        )
        # 用最新的 think/subtasks 替换父 Frame
        stack[-1] = parent_refined
        state["stack"] = stack
        return state

    # 3. 当前 Frame 还有子任务要执行
    subtask = frame["subtasks"][frame["cursor"]]

    # 3.1 primitive → 执行动作(叶子节点)
    if is_primitive(subtask):
        obs = execute_action(subtask)
        state["last_obs"] = obs

        # 更新当前 Frame 的 cursor(这一项已执行)
        frame["cursor"] += 1

        # 针对当前 Frame 做一次 refine(类似 Leaf Backtracking)
        refined = refine_llm(
            frame=frame,
            obs=obs,
        )
        stack[-1] = refined
        state["stack"] = stack
        return state

    # 3.2 非 primitive → 作为新的子任务 Frame 压栈(递归向下)
    else:
        child_task = subtask
        child_think, child_subtasks = plan_llm(child_task)
        child_frame: Frame = {
            "task": child_task,
            "think": child_think,
            "subtasks": child_subtasks,
            "cursor": 0,
        }

        # 父 Frame 的 cursor 往后移一位,表示这个位置的任务由子 Frame 负责
        frame["cursor"] += 1
        stack[-1] = frame

        # 压入子 Frame
        stack.append(child_frame)
        state["stack"] = stack
        # last_obs 保留为之前的值,等子任务执行后再更新
        return state


# ========== 6. 路由函数:继续还是结束 ==========

def route_next(state: GraphState) -> str:
    """根据状态决定下一步是继续 recap_step,还是结束"""
    if state.get("done"):
        return "end"
    return "continue"


# ========== 7. 构建 LangGraph 图 ==========

def build_graph():
    builder = StateGraph(GraphState)

    builder.add_node("recap_step", recap_step)

    # START → recap_step
    builder.add_edge(START, "recap_step")

    # recap_step → (recap_step 或 END)
    builder.add_conditional_edges(
        "recap_step",
        route_next,
        {
            "continue": "recap_step",
            "end": END,
        },
    )

    graph = builder.compile()
    return graph


# ========== 8. 示例运行入口 ==========

if __name__ == "__main__":
    graph = build_graph()

    # 初始状态:只给 root_task,其余让 recap_step 自行初始化
    init_state: GraphState = {
        "root_task": "做一个牛肉汉堡",
        "stack": [],
        "last_obs": None,
        "done": False,
    }

    print("=== ReCAP + LangGraph Demo(纯模拟版): 做一个牛肉汉堡 ===")

    step = 0
    for state in graph.stream(init_state, stream_mode="values"):
        print(f"\n====== Step {step} ======")

        stack = state.get("stack", [])
        print("当前栈深度:", len(stack))

        if stack:
            top = stack[-1]
            print("当前任务:", top.get("task"))
            print("当前子任务列表:", top.get("subtasks"))
            print("cursor:", top.get("cursor"))
            print("当前层 think:", top.get("think"))
        else:
            print("当前栈为空")

        print("last_obs:", state.get("last_obs"))

        if state.get("done"):
            print("所有任务完成。")
            break

        step += 1
相关推荐
九年义务漏网鲨鱼21 小时前
【大模型微调】QLoRA微调原理及实战
深度学习·算法·大模型·智能体
MonkeyKing_sunyuhua21 小时前
SOTA 级别的模型,其中SOTA是什么意思
大模型
模型启动机21 小时前
GELab-Zero:阶跃开源的4B端侧多模态GUI Agent模型,助力本地可控的移动设备智能化
人工智能·ai·大模型·智能化
快手技术21 小时前
可灵团队提出OmniSync:无限时长、强id保持、遮挡情况下强鲁棒性,视频口型编辑新突破!
人工智能·语言模型·大模型·快手·顶会论文
EdisonZhou21 小时前
MAF快速入门(6)混合编排工作流
llm·aigc·agent·.net core
我很哇塞耶1 天前
KDD 2026|小红书搜索:生成式相关性让搜索“会思考”
人工智能·ai·大模型
AndrewHZ1 天前
【AI分析进行时】AI 时代软件开发新范式:基于斯坦福CS146S课程分析
人工智能·llm·软件开发·斯坦福·cs146s·能力升级·代码agent
七夜zippoe1 天前
告别API碎片化与高成本 - 用AI Ping打造下一代智能编程工作流
人工智能·架构·大模型·智能编程·ai ping·模型聚合
杰克逊的日记1 天前
大模型的原理是什么
人工智能·大模型·gpu·算力