> ## Documentation Index
> Fetch the complete documentation index at: https://qitor.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 第 2 课：PlanAct

> 在 ReAct 内核上加入显式规划，理解 QitOS 如何分离规划控制与执行控制。

这是你第一次主动改造默认循环。

你仍然在构建一个普通的 QitOS 智能体，但会引入三个新东西：

* 放入状态的规划产物（持久化输出）
* 与执行提示词分离的规划提示词
* 一个只处理规划边界的 `decide()` 覆写

关键在于：你仍然没有引入第二个运行时。

## 相比第 1 课的变化

| 设计分支    | 第 1 课             | 第 2 课                      |
| ------- | ----------------- | -------------------------- |
| 控制流     | 每步都走默认大模型路径       | 只有规划边界由 `decide()` 拦截      |
| 提示词     | 单个 ReAct 系统提示词    | 规划提示词 + 执行提示词              |
| 状态      | 临时记录 + 任务字段       | 新增 `plan_steps` 与 `cursor` |
| 解析器     | `ReActTextParser` | 执行阶段仍是 `ReActTextParser`   |
| 工具      | 精简编码工具集           | 保持不变                       |
| 记忆 / 历史 | 除状态外没有额外层         | 仍不引入记忆或上下文压缩               |

最后一行尤其重要：这节课增加的是规划能力，而不是上下文复杂度。

## 双提示词架构

这节课会显式使用两份提示词契约。

### 规划提示词

```text theme={null}
You are a planning module.
Break the task into 3-7 atomic executable steps.

Constraints:
- Each step must be actionable and verifiable.
- Prefer tool-executable operations over vague reasoning.
- No prose outside the numbered list.
```

在代码中通常命名为 `PLAN_DRAFT_PROMPT`。

### 执行提示词

```text theme={null}
You are the execution module for a Plan-Act agent.

You will receive the global task and one current plan step.
Execute only the current step. Do not jump ahead.

Output contract (strict):
Thought: <one sentence>
Action: <tool_name>(arg=value, ...)
or
Final Answer: <step result>
```

在代码中通常命名为 `PLAN_EXEC_SYSTEM_PROMPT`。

这节课想让你建立的设计意识是：

* 规划和执行可以使用不同的提示词
* 但它们仍然流经同一个 `AgentModule + Engine` 运行时

## 这节课的解析器逻辑

规划路径**不**使用 `ReActTextParser`。

它通常是这样工作的：

* `_plan()` 渲染 `PLAN_DRAFT_PROMPT`
* `NumberedPlanBuilder` 调用同一个大模型适配层
* 构建器把编号列表解析成 `list[str]`

而执行路径**仍然**使用 `ReActTextParser`。

这里体现了一个关键的 QitOS 设计点：

**同一个智能体的不同阶段可以使用不同的解析契约，只要控制边界是显式的。**

## 大模型适配层保持不变

和第 1 课一样，这节课仍然使用：

```python theme={null}
OpenAICompatibleModel(...)
```

保持同一适配层的原因是：

* 这样你能单独观察规划带来的变化
* 提示词与解析器的变化更容易解释
* 课程一次只引入一个真正的新变量

<Steps>
  <Step title="在状态中加入计划与游标">
    新状态只增加执行真正需要的字段：

    ```python theme={null}
    @dataclass
    class PlanActState(StateSchema):
        plan_steps: list[str] = field(default_factory=list)
        cursor: int = 0
        target_file: str = "buggy_module.py"
        test_command: str = TEST_COMMAND
        scratchpad: list[str] = field(default_factory=list)
    ```

    这是课程里第一次把原本隐藏的推理产物显式写进状态。

    为什么要这么做：

    * 追踪记录可以直接展示它
    * `prepare()` 可以显式暴露它
    * `reduce()` 可以推进它
    * 以后若需要，也可以主动重写它
  </Step>

  <Step title="使用专用的计划构建器">
    规划器通常在构造时初始化：

    ```python theme={null}
    self.plan_builder = NumberedPlanBuilder()
    ```

    调用方式类似：

    ```python theme={null}
    prompt = render_prompt(
        PLAN_DRAFT_PROMPT,
        {
            "task": (
                f"{state.task}\n"
                f"Target file: {state.target_file}\n"
                f"Last step must run: {state.test_command}"
            ),
        },
    )
    plan = self.plan_builder.build(self.llm, prompt)
    ```

    正确的 QitOS 做法是：

    **让规划变成有名字的持久化产物，并且有自己清晰的解析器，而不是混进主记录里的一段自由文本。**
  </Step>

  <Step title="把 decide 只用作规划门控">
    这节课的控制逻辑通常非常小：

    ```python theme={null}
    def decide(self, state: PlanActState, observation: dict[str, Any]):
        if not state.plan_steps or state.cursor >= len(state.plan_steps):
            if not self._plan(state):
                return Decision.final("Failed to build a valid plan.")
            return Decision.wait("plan_ready")
        return None
    ```

    其中最重要的是最后一句：

    `return None`

    一旦计划已经存在，引擎会重新回到默认的大模型路径：

    `提示词 -> ReActTextParser -> 决策 -> 工具执行`

    所以第 2 课不是替换运行时，而是在运行时上加了一道显式控制边界。
  </Step>

  <Step title="把执行提示词与解析器明确绑定">
    执行阶段仍是：

    ```python theme={null}
    super().__init__(
        tool_registry=registry,
        llm=llm,
        model_parser=ReActTextParser(),
    )
    ```

    同时：

    ```python theme={null}
    def build_system_prompt(self, state: PlanActState) -> str | None:
        return render_prompt(
            PLAN_EXEC_SYSTEM_PROMPT,
            {
                "current_step": self._current_step_text(state),
                "tool_schema": self.tool_registry.get_tool_descriptions(),
            },
        )
    ```

    因此规划阶段和执行阶段是清晰分离的：

    * 规划：编号列表构建器
    * 执行：ReAct 文本协议
  </Step>

  <Step title="让 prepare 显式展示计划">
    `prepare()` 现在不再只渲染任务，而是同时渲染当前计划进度：

    ```python theme={null}
    def prepare(self, state: PlanActState) -> str:
        lines = [
            f"Task: {state.task}",
            f"Plan cursor: {state.cursor}/{len(state.plan_steps)}",
            f"Current plan step: {self._current_step_text(state)}",
            f"Step: {state.current_step}/{state.max_steps}",
        ]
    ```

    智能体的工作记忆也因此发生了变化。模型每一步看到的不再是整个问题，而是：

    * 一个任务
    * 一份显式计划
    * 当前要执行的单个计划条目
  </Step>

  <Step title="在归约里推进计划进度">
    计划推进仍然是普通的状态逻辑：

    ```python theme={null}
    if isinstance(first, dict) and first.get("status") == "success":
        state.cursor += 1
    if isinstance(first, dict) and int(first.get("returncode", 1)) == 0:
        state.final_result = "Verification passed."
        state.cursor = len(state.plan_steps)
    ```

    关键不在于这两个条件本身，而在于它们出现的位置：

    **`reduce()`（归约）就是你定义"什么算计划完成"的地方。**
  </Step>

  <Step title="故意保持记忆与历史简单">
    第 2 课依旧不引入：

    * 记忆适配器
    * `HistoryPolicy` 调优
    * `CompactHistory`

    因为此时计划本身已经是一种压缩任务结构的方式。如果同时引入上下文压缩，你就很难判断行为变化到底来自规划还是来自上下文管理。
  </Step>

  <Step title="运行并在 qita 中检查规划边界">
    运行：

    ```bash theme={null}
    python examples/patterns/planact.py
    ```

    查看：

    ```bash theme={null}
    qita board --logdir runs
    ```

    在追踪记录中重点看：

    * 哪一步出现了 `Decision.wait("plan_ready")`
    * `plan_steps` 是何时进入状态的
    * 后续执行是否仍然沿着同一个 ReAct 解析器路径前进
  </Step>
</Steps>

## 为什么 PlanAct 仍然是同一个内核

很多人一想到规划，就会下意识引入：

* 单独的规划服务
* 框架之外的规划-执行循环
* 第二个智能体运行时

QitOS 在这节课想强调的恰恰相反：

* 规划器只是另一种受控的大模型调用
* 计划只是另一种状态产物
* 执行仍然走普通的引擎路径

这是 QitOS 一条很深的设计原则。

## 完整示例

完整可运行代码位于：

* [`examples/patterns/planact.py`](https://github.com/Qitor/qitos/blob/main/examples/patterns/planact.py)

## 第 3 课会新增什么

第 3 课依旧不换内核，但智能体会真正变成长时运行的工作流。

你将第一次认真面对：

* 预设工具集，而不是手工组装
* 面向工作流的系统提示词
* 显式历史控制
* 上下文压缩与记忆何时成为必要设计问题

<CardGroup cols={2}>
  <Card title="下一课：Claude Code 风格智能体" icon="rectangle-terminal" href="/zh/tutorials/claude-code">
    从模式设计走向长时运行的工作区智能体
  </Card>

  <Card title="相关指南：记忆与历史" icon="clock-rotate-left" href="/zh/guides/memory-and-history">
    在进入长时运行前，先回顾状态、历史、压缩与记忆的边界
  </Card>
</CardGroup>
