📋 模块3:Agent 任务规划能力实战

3.1 链式思考与任务拆解基础

链式思考(Chain-of-Thought, CoT)是让 AI 在回答问题前先"说出自己的思考过程"——就像学生做数学题要写"解"的过程。对 Agent 来说,这意味着在执行前先列出步骤。

没有规划 vs 有规划

❌ 没有规划:
用户: "帮我做一个市场调研报告"
Agent: 直接开始写 → 写到一半发现缺数据 → 乱写 → 质量差

✅ 有规划:
用户: "帮我做一个市场调研报告"
Agent 思考:
  步骤1: 明确调研范围和目标
  步骤2: 搜索行业数据和竞品信息
  步骤3: 分析数据,提取关键洞察
  步骤4: 组织报告结构(概述→市场分析→竞品→趋势→建议)
  步骤5: 逐段撰写
  步骤6: 检查完整性和一致性

3.2 规划 Agent 完整代码

创建 planning_agent.py

#!/usr/bin/env python3
"""
PlanningAgent — 具备任务规划能力的 AI Agent
=============================================
核心能力:
  ✅ 复杂任务自动拆分
  ✅ 分步骤有序执行
  ✅ 每步结果验证
  ✅ 异常步骤自动调整
  ✅ 最终结果汇总

运行:python3 planning_agent.py
"""

import json
import os
import re
from datetime import datetime
from pathlib import Path
from typing import Any

from dotenv import load_dotenv
from anthropic import Anthropic

load_dotenv()


# ================================================================
# 第1部分:任务规划器
# ================================================================

class TaskPlanner:
    """任务规划器 — 负责把复杂任务拆成可执行的步骤"""

    def __init__(self, client: Anthropic, model: str = "claude-sonnet-4-6"):
        self.client = client
        self.model = model

    def decompose(self, task: str, context: dict = None) -> list[dict]:
        """
        将复杂任务拆解为多个子步骤
        返回格式: [{"step": 1, "action": "做什么", "tool": "用哪个工具(可选)",
                   "params": {}, "expected": "预期结果"}]
        """
        context_text = f"\n当前上下文: {json.dumps(context, ensure_ascii=False)}" if context else ""

        prompt = f"""你是一个任务规划专家。请将以下用户任务拆解为可执行的子步骤。

用户任务: {task}{context_text}

请以 JSON 数组格式返回步骤列表,每个步骤包含:
- step: 步骤编号
- action: 具体要做什么(中文描述)
- tool: 需要调用的工具名(可选,如 calculator/file_reader/web_search)
- params: 工具参数(如不需要工具则为空对象)
- expected: 完成后预期的结果描述

可用工具:calculator(expression), file_reader(filepath), web_search(query),
          text_summarizer(text), time_getter()

规则:
1. 步骤要具体、可执行,不能模糊
2. 步骤之间逻辑连贯,前一步的输出是后一步的输入
3. 如果任务很简单(1步就能完成),也返回单元素数组
4. 每个步骤的结果需要能够验证是否成功

只返回 JSON 数组,不要加其他文字。"""

        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.2,
            )
            plan_text = response.content[0].text.strip()

            # 提取 JSON
            if "```" in plan_text:
                plan_text = plan_text.split("```")[1].split("```")[0]
            if plan_text.startswith("```json"):
                plan_text = plan_text[7:]

            plan = json.loads(plan_text)
            return plan if isinstance(plan, list) else [plan]
        except json.JSONDecodeError as e:
            print(f"⚠️ 规划 JSON 解析失败: {e}")
            print(f"原始返回: {plan_text[:200]}")
            # 降级:当作单步任务
            return [{"step": 1, "action": task, "tool": None, "params": {}, "expected": "完成任务"}]

    def adjust_plan(self, original_plan: list[dict], failed_step: int, error_info: str) -> list[dict]:
        """当某步骤失败时,调整后续计划"""
        prompt = f"""原计划执行到第{failed_step}步时失败。
失败信息: {error_info}

原计划:
{json.dumps(original_plan, ensure_ascii=False, indent=2)}

请调整第{failed_step}步及之后的步骤,给出修正后的完整计划。
返回格式与原计划相同的 JSON 数组。"""

        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.2,
            )
            text = response.content[0].text.strip()
            if "```" in text:
                text = text.split("```")[1].split("```")[0]
            if text.startswith("json"):
                text = text[4:]
            new_plan = json.loads(text)
            return new_plan if isinstance(new_plan, list) else original_plan
        except Exception:
            return original_plan  # 调整失败,返回原计划


# ================================================================
# 第2部分:工具集
# ================================================================

class ToolExecutor:
    """工具执行器 — 执行规划步骤中指定的工具"""

    def __init__(self):
        self.execution_log: list[dict] = []

    def execute(self, tool_name: str, params: dict) -> str:
        """执行指定工具"""
        tools = {
            "calculator": self._calc,
            "file_reader": self._read_file,
            "web_search": self._search,
            "text_summarizer": self._summarize,
            "time_getter": self._get_time,
        }

        func = tools.get(tool_name)
        if not func:
            return f"❌ 未知工具: {tool_name}"

        try:
            first_arg = list(params.values())[0] if params else ""
            result = func(first_arg)
            self.execution_log.append({
                "tool": tool_name,
                "params": params,
                "result": result[:200],
                "time": datetime.now().isoformat(),
            })
            return result
        except Exception as e:
            return f"❌ 执行失败: {e}"

    def _calc(self, expr: str) -> str:
        allowed = set("0123456789+-*/().% ")
        if not all(c in allowed for c in expr):
            return f"❌ 不支持的表达式: {expr}"
        try:
            return str(eval(expr, {"__builtins__": {}}, {}))
        except Exception as e:
            return f"❌ {e}"

    def _read_file(self, path: str) -> str:
        p = Path(path)
        if not p.exists():
            return f"❌ 文件不存在: {path}"
        try:
            content = p.read_text("utf-8")
            return content[:2000]
        except Exception as e:
            return f"❌ {e}"

    def _search(self, query: str) -> str:
        return f"[搜索结果] 关于 '{query}' 的模拟搜索结果:这是搜索功能的占位实现。"

    def _summarize(self, text: str) -> str:
        return f"[摘要] 文本长度 {len(text)} 字符,主要内容概括..."

    def _get_time(self, _=None) -> str:
        return datetime.now().strftime('%Y-%m-%d %H:%M:%S')


# ================================================================
# 第3部分:规划执行 Agent
# ================================================================

class PlanningAgent:
    """具备任务规划与执行能力的 Agent"""

    def __init__(self, name: str = "PlanningAgent"):
        self.name = name
        self.client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
        self.model = "claude-sonnet-4-6"
        self.planner = TaskPlanner(self.client, self.model)
        self.executor = ToolExecutor()
        self.max_retries_per_step = 2
        self.memory: list[dict] = []

    def run(self, user_input: str) -> str:
        """主运行流程:规划 → 执行 → 汇总"""
        print(f"\n{'='*60}")
        print(f"👤 用户任务: {user_input}")
        print(f"{'='*60}")

        # 阶段1:任务规划
        print("\n📋 [阶段1] 任务规划中...")
        plan = self.planner.decompose(user_input)

        print(f"📋 将任务拆分为 {len(plan)} 个步骤:")
        for step in plan:
            tool_info = f" [工具: {step['tool']}]" if step.get('tool') else ""
            print(f"  步骤{step['step']}: {step['action']}{tool_info}")

        # 阶段2:逐步执行
        print(f"\n⚡ [阶段2] 开始执行 ({len(plan)} 步)...")
        step_results = []
        current_plan = plan.copy()

        for i, step in enumerate(current_plan):
            step_num = step["step"]
            print(f"\n  📍 执行步骤{step_num}/{len(current_plan)}: {step['action']}")

            # 执行步骤(带重试)
            result = self._execute_step(step, i)

            if result.startswith("❌"):
                print(f"  ⚠️ 步骤{step_num}失败: {result}")
                # 尝试调整计划
                print(f"  🔄 尝试调整计划...")
                current_plan = self.planner.adjust_plan(current_plan, step_num, result)
                # 重试当前步骤
                if i < len(current_plan):
                    result = self._execute_step(current_plan[i], i)
                    if result.startswith("❌"):
                        print(f"  ❌ 调整后仍然失败,跳过此步骤")
                        step_results.append(f"[步骤{step_num}失败: {result}]")
                        continue

            step_results.append(result)
            print(f"  ✅ 步骤{step_num}完成: {result[:100]}")

        # 阶段3:结果汇总
        print(f"\n📊 [阶段3] 结果汇总...")
        summary = self._summarize_results(user_input, plan, step_results)

        # 保存到记忆
        self.memory.append({
            "task": user_input,
            "plan": plan,
            "results": step_results,
            "summary": summary,
            "time": datetime.now().isoformat(),
        })

        print(f"\n{'='*60}")
        print(f"🤖 最终结果:\n{summary}")
        print(f"{'='*60}")
        return summary

    def _execute_step(self, step: dict, index: int) -> str:
        """执行单个步骤,带重试"""
        for attempt in range(self.max_retries_per_step + 1):
            if step.get("tool"):
                # 有工具,调用工具执行
                result = self.executor.execute(step["tool"], step.get("params", {}))
            else:
                # 无工具,让 Claude 直接完成这一步
                result = self._llm_step(step)

            if not result.startswith("❌"):
                return result

            if attempt < self.max_retries_per_step:
                print(f"    🔄 重试 {attempt+1}/{self.max_retries_per_step}...")

        return result

    def _llm_step(self, step: dict) -> str:
        """让 Claude 直接完成无工具的步骤"""
        try:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=512,
                messages=[{"role": "user", "content":
                    f"请完成以下步骤(只输出执行结果,不要解释):\n{step['action']}\n预期结果: {step['expected']}"}],
                temperature=0.3,
            )
            return response.content[0].text.strip()
        except Exception as e:
            return f"❌ {e}"

    def _summarize_results(self, task: str, plan: list, results: list) -> str:
        """汇总所有步骤的结果"""
        try:
            steps_text = "\n".join([
                f"步骤{s['step']}: {s['action']} → {r[:150]}"
                for s, r in zip(plan, results)
            ])
            response = self.client.messages.create(
                model=self.model,
                max_tokens=800,
                messages=[{"role": "user", "content":
                    f"请将以下任务执行结果汇总为清晰的中文报告:\n\n"
                    f"任务: {task}\n\n执行步骤与结果:\n{steps_text}\n\n"
                    f"格式要求:先概述完成情况,再逐条列出关键结果。"}],
                temperature=0.3,
            )
            return response.content[0].text.strip()
        except Exception as e:
            return "\n".join([f"{i+1}. {r}" for i, r in enumerate(results)])


# ================================================================
# 第4部分:交互入口
# ================================================================

def main():
    print("=" * 60)
    print("📋 PlanningAgent — 任务规划与执行 Agent")
    print("=" * 60)
    print("支持复杂任务,如:")
    print("  • 帮我计算 (100+50)×0.8,然后把结果保存到result.txt")
    print("  • 读取test.txt的内容,总结要点,搜索相关资料")
    print("  • 先看现在几点,然后计算距离明天早上9点还有多少小时")
    print("  • quit → 退出")
    print("=" * 60)

    if not os.getenv("ANTHROPIC_API_KEY"):
        print("❌ 请先配置 ANTHROPIC_API_KEY!")
        return

    # 准备测试文件
    Path("test.txt").write_text(
        "AI Agent 是一种能够自主感知环境、制定计划、使用工具、"
        "执行任务的智能程序。它不同于传统的聊天机器人,"
        "能够独立思考和行动直到任务完成。\n",
        encoding="utf-8"
    )

    agent = PlanningAgent()

    while True:
        try:
            user_input = input("\n👤 任务: ").strip()
            if not user_input:
                continue
            if user_input.lower() in ("quit", "exit", "q"):
                break
            agent.run(user_input)
        except KeyboardInterrupt:
            break

    print(f"\n📊 本次会话完成 {len(agent.memory)} 个任务")
    print(f"👋 {agent.name} 已退出。")


if __name__ == "__main__":
    main()

3.3 规划逻辑优化

问题表现优化方案
规划过于粗略步骤太大,执行时频繁失败提示词中加入"每个步骤必须原子化(不可再分)"的约束
规划过于细节产生大量无用微步骤,效率低设置步骤数上限(如5-8步),超出则合并
步骤遗漏最终结果缺少关键环节在规划后增加"验证清单"检查,如"是否包含输入→处理→输出→验证"
依赖关系错误步骤B在步骤A之前执行,但B依赖A增加步骤间依赖声明,规划时让 AI 输出 DAG(有向无环图)结构

📝 知识点总结

概念一句话
链式思考 CoT让 AI 在执行前先说/写出思考过程,大幅提升复杂任务的准确性
任务拆解把大任务分解为多个原子化的子步骤,每步有明确输入、输出、验证标准
迭代规划执行中某步失败时,动态调整后续计划,而非全部重新开始
结果汇总将所有步骤的结果整合为完整、连贯的最终输出

✏️ 课后练习

  1. 复杂任务测试:给 PlanningAgent 一个需要5步以上的任务(如"分析test.txt→提取关键词→搜索每个关键词→汇总搜索结果→保存到report.txt"),观察规划质量和执行效率。
  2. 添加验证步骤:在规划完成后增加一个 plan_validator,检查计划是否包含完整闭环(输入→处理→输出→验证)。
  3. 并行执行优化:思考:如果步骤3和步骤4互相独立(无依赖),能否并行执行?尝试修改代码实现。
  4. Reflection 预习:在执行完成后,让 Agent 自己评价"完成得怎么样",给出自评分数和改进建议。(第四阶段会深入自反思机制)
← 工具调用 下一模块:Prompt工程 →