HuanCode Docs

Harness实战:Plan模式——让Agent先想清楚再动手

Agent收到任务就立刻开干,简单任务没问题,复杂任务容易走弯路。一个技巧:第一次API调用不传tools,强制模型只输出计划文本;用户确认后再带tools执行。用一次额外调用换取执行方向的确定性。

写在前面

前几篇我们的 Agent 收到任务就立刻开干——模型边想边调工具,一步一步摸着石头过河。

对简单任务没问题。但当任务复杂时,比如"重构这个模块的错误处理",直接上手容易走弯路:改到一半发现方向不对再回头,浪费大量 token。

怎么让模型先想清楚再动手?这就是 Plan → Execute 模式。


为什么需要先计划?

对比两种执行方式。

不做计划(当前模式):

用户: 给这个项目添加单元测试
模型: [调用 bash: ls] → [调用 read_file: main.py] → [调用 write_file: test_main.py]
     → 发现遗漏了 utils.py → [调用 read_file: utils.py] → [调用 edit_file: test_main.py]
     → 运行测试失败 → [调用 edit_file: test_main.py] → ...

模型走一步看一步,中途才发现遗漏,回头修改,来来回回好几轮。

先做计划:

用户: 给这个项目添加单元测试

模型(计划阶段,无工具):
  1. 先读取项目结构了解有哪些模块
  2. 读取 main.py 和 utils.py 的代码
  3. 为每个模块设计测试用例
  4. 创建 test_main.py 和 test_utils.py
  5. 运行测试确保通过

用户: 好的,执行吧

模型(执行阶段,有工具):
  按计划有条理地完成...

边做边想 vs 先想后做

好处很明显:

  1. 减少无效调用 — 先看全貌再动手,避免"改了 A 才发现还需要 B"
  2. 用户可以介入 — 计划出来后用户可以审核、调整、拒绝
  3. 可追溯 — 执行过程有计划对照,出问题知道偏离了哪一步
  4. 省 token — 避免走弯路的反复调用

两种实现方式

方式一:两阶段调用(推荐)

最干净的做法——第一次调用不传 tools,强制模型只输出文本计划;第二次调用传 tools,让模型执行计划。

def plan_then_execute(messages: list):
    # ── 阶段 1:计划(不给工具,模型只能输出文本) ──
    plan_messages = messages + [{
        "role": "user",
        "content": "Before acting, outline your plan step by step. Do NOT use any tools yet."
    }]

    plan_response = client.messages.create(
        model=MODEL,
        system=SYSTEM,
        messages=plan_messages,
        # 关键:不传 tools 参数,模型无法调用工具
        max_tokens=2000,
    )

    plan_text = plan_response.content[0].text
    print(f"\n📋 Plan:\n{plan_text}\n")

    # 用户确认
    confirm = input("Execute this plan? (y/n) ")
    if confirm.lower() != "y":
        print("Plan cancelled.")
        return

    # ── 阶段 2:执行(正常 agent loop,带工具) ──
    messages.append({"role": "assistant", "content": plan_response.content})
    messages.append({"role": "user", "content": "Good plan. Now execute it step by step."})
    agent_loop(messages)  # 复用之前写的 agent_loop

为什么不传 tools 就能强制输出计划?因为模型没有工具可调用时,stop_reason 一定是 end_turn,只能返回文本。这比在 system prompt 里写"请先计划再执行"要可靠得多——prompt 是"请求",不传 tools 是"约束"。

方式二:System Prompt 引导

更轻量的做法——通过 system prompt 告诉模型先输出计划文本,再调用工具:

SYSTEM_WITH_PLAN = f"""You are a coding agent at {WORKDIR}.

When given a task:
1. FIRST output your plan as a numbered list (text only, no tool calls)
2. THEN execute the plan using tools

Always plan before acting."""

这种方式更简单,但有个问题:模型可能跳过计划直接调工具(尤其是简单任务时)。而且用户无法在计划和执行之间介入审核。

对比

两阶段调用System Prompt
计划可靠性100%(没有工具可调)依赖模型遵从 prompt
用户审核天然支持(两阶段之间)需要额外逻辑
实现复杂度多一次 API 调用改一行 prompt
适合场景高风险操作、复杂任务日常轻量任务

两种实现方式对比


完整实现

agentic-demo.py 基础上增加 plan 模式,用户输入 /plan 前缀触发。

工具定义和 Agent Loop 和之前完全一样,只新增一个 plan_then_execute 函数:

def plan_then_execute(messages: list, task: str):
    # 阶段 1:生成计划(不传 tools)
    plan_messages = messages + [
        {"role": "user", "content": task},
        {"role": "user", "content": "Analyze the task and outline a step-by-step plan. Do NOT execute anything yet."},
    ]

    print("\n⏳ Generating plan...\n")
    plan_response = client.messages.create(
        model=MODEL,
        system=SYSTEM,
        messages=plan_messages,
        max_tokens=2000,
        # 注意:没有 tools 参数
    )

    plan_text = plan_response.content[0].text
    print(f"📋 Plan:\n{plan_text}\n")

    # 用户确认
    confirm = input("Execute? (y/n/or type feedback) ")
    if confirm.strip().lower() == "n":
        print("Cancelled.")
        return

    feedback = ""
    if confirm.strip().lower() not in ("y", "yes"):
        feedback = f" User feedback: {confirm}"

    # 阶段 2:执行(把计划作为上下文,启动正常 agent loop)
    messages.append({"role": "user", "content": task})
    messages.append({"role": "assistant", "content": plan_response.content})
    messages.append({"role": "user", "content": f"Good plan.{feedback} Now execute it step by step."})

    print("\n⚡ Executing...\n")
    agent_loop(messages)

主程序加一个 /plan 前缀的判断:

if __name__ == "__main__":
    history = []
    while True:
        query = input("❯ ")
        if query.strip().lower() in ("q", "exit", ""):
            break

        if query.strip().startswith("/plan "):
            task = query.strip()[6:]
            plan_then_execute(history, task)
        else:
            history.append({"role": "user", "content": query})
            agent_loop(history)

就这么多。核心只是一个函数,复用已有的 agent_loop


运行效果

普通模式(直接执行)

❯ 当前目录有什么文件
> bash: agentic-demo.py  harness-agent-loop.md  ...

和之前一样,收到就干。

计划模式(/plan 前缀)

❯ /plan 给当前目录的所有 py 文件添加文件头注释

⏳ Generating plan...

📋 Plan:
1. 用 bash 列出当前目录的所有 .py 文件
2. 逐个读取每个 .py 文件的前几行,检查是否已有文件头注释
3. 对没有注释的文件,用 edit_file 在文件开头插入统一格式的注释
4. 最后用 bash 验证所有文件的头部

 Execute? (y/n/or type feedback) y

⚡ Executing...
  > bash: agentic-demo.py harness.py llm-claude.py ...
  > read_file: import json ...
  > edit_file: Edited agentic-demo.py
  ...

注意 Execute? 那一步,用户可以输入:

  • y — 直接执行
  • n — 取消
  • 任意文字 — 作为反馈传给模型,比如"跳过 harness.py,那个是空文件"

这就是 Plan 模式的核心价值:在执行前插入一个人类审核点


核心机制解析

整个 plan 模式只改了一个地方——plan_then_execute 函数。它和 agent_loop 的关系:

plan_then_execute
  ├── 第 1 次 API 调用(无 tools)→ 输出计划文本
  ├── 用户确认
  └── agent_loop(有 tools)→ 正常执行

Plan → Execute 核心流程

关键技巧是第一次调用不传 tools

# 计划阶段 — 没有 tools,模型被迫只输出文本
plan_response = client.messages.create(
    model=MODEL,
    system=SYSTEM,
    messages=plan_messages,
    max_tokens=2000,
    # 没有 tools=TOOLS
)

# 执行阶段 — 正常带 tools 的 agent loop
agent_loop(messages)

这比在 prompt 里写"请先计划"要可靠得多。Prompt 是"建议",模型可能忽略;不传 tools 是"约束",模型物理上无法调用工具。


Claude Code 的 Plan Mode

Claude Code 的 Plan Mode(Shift+Tab 切换)就是这个模式的生产级实现:

  • 进入 plan mode 后,模型只输出分析和计划,不执行任何工具
  • 用户确认后切回执行模式,模型按计划操作
  • 还可以在计划中和模型对话,反复修正策略

底层原理完全一样:plan mode 时不给工具,execute mode 时给工具。


小结

  1. 两阶段调用 — 不传 tools 强制输出计划,传 tools 正常执行
  2. 用户审核点 — 计划和执行之间插入确认,用户可以修正方向
  3. 实现极简 — 核心只是一个 plan_then_execute 函数,复用已有的 agent_loop

这个模式的本质是:用一次额外的 API 调用换取执行方向的确定性。对简单任务可能显得多余,但对复杂任务——重构、迁移、批量修改——能显著减少无效操作和 token 浪费。

On this page