AI Agent 开发实战(十):Agent 设计模式(ReAct / Plan-Execute / Reflection)

0 阅读13分钟

上一篇我们让 Agent 学会了"反问",在执行前搞清楚需求。但搞清楚需求之后呢?Agent 该怎么一步步执行?遇到意外怎么办?执行完了怎么保证质量?今天聊三种经典 Agent 设计模式——ReAct、Plan-Execute、Reflection——它们分别解决"边做边想"、"先规划后执行"、"做完自我检查"三个问题,而且在真实项目中往往需要组合使用。


一、为什么需要设计模式

没有设计模式的 Agent 是什么样的?

用户:帮我分析一下这个商机的风险等级
Agent:
  → 直接调用 LLM 分析
  → 输出:风险等级为中
  → 完成

问题:
├── 没有查询数据,直接"编"了结论
├── 不知道"中"是怎么判断的
├── 没有验证结论是否合理
└── 用户追问"为什么",Agent 只能重新"编"

设计模式解决的核心问题:如何让 Agent 的推理过程更可靠、更可追溯、更可纠错。

Agent 设计模式要解决的三个问题
│
├── 1. 推理可靠性:Agent 不能"编",每步推理要有依据
│   └── ReAct:边推理边行动,每步都有真实数据支撑
│
├── 2. 执行可控性:Agent 不能"跑偏",要按计划执行
│   └── Plan-Execute:先规划步骤,再逐步执行
│
└── 3. 输出质量:Agent 不能"糊弄",要自我检查
    └── Reflection:执行后反思,迭代改进

二、ReAct 模式:推理 + 行动

2.1 核心思想

ReAct = Reasoning + Acting。Agent 在每一步都先推理(Thought),再行动(Action),然后观察结果(Observation),形成循环。

ReAct 循环
│
├── Thought:根据当前信息推理下一步该做什么
├── Action:执行一个工具调用
├── Observation:观察工具返回的结果
│
└── 重复上述步骤,直到得出最终答案

2.2 一个完整的 ReAct 示例

用户:这个商机值得跟进吗?

Thought 1:我需要先查询商机的详细信息
Action 1queryOpportunity(id="OPP-2024-001")
Observation 1:客户=XX科技,金额=50万,行业=电商,阶段=需求确认

Thought 2:有了基本信息,现在需要评估风险。先查客户历史合作记录
Action 2queryCustomerHistory(customer="XX科技")
Observation 2:历史合作3次,按时回款率100%,无违约记录

Thought 3:客户信用不错。再查行业趋势
Action 3queryIndustryTrend(industry="电商")
Observation 3:电商行业增长15%,竞争激烈,但客户是头部企业

Thought 4:综合信息:客户信用好、行业增长、金额适中,但有竞争风险
Final Answer:建议跟进,风险等级为"中低"。关键依据:客户历史回款100%,
             行业增长15%,但需关注竞品动态

对比无模式的直接回答:

对比项直接回答ReAct 模式
数据来源LLM 编造工具调用真实数据
推理可追溯不可追溯每步 Thought 可追溯
可纠错无法纠错某步出错可在后续修正
Token 消耗高(多次调用)
延迟慢(多轮交互)

2.3 Spring AI 实现 ReAct

public class ReActAgent {

    private final ChatClient chatClient;
    private final List<Tool> tools;
    private final int maxIterations;

    public ReActAgent(ChatClient chatClient, List<Tool> tools, int maxIterations) {
        this.chatClient = chatClient;
        this.tools = tools;
        this.maxIterations = maxIterations;
    }

    public String execute(String userRequest) {
        String context = "用户请求:" + userRequest + "\n\n";
        
        for (int i = 0; i < maxIterations; i++) {
            // Step 1: Thought — 推理下一步
            var thought = chatClient.prompt()
                .user(context + "请推理下一步该做什么,或给出最终答案。" +
                      "如果信息足够,直接输出 Final Answer: ...")
                .call()
                .content();

            context += "Thought: " + thought + "\n";

            // Step 2: 检查是否已有最终答案
            if (thought.contains("Final Answer:")) {
                return thought.substring(thought.indexOf("Final Answer:") + 14);
            }

            // Step 3: Action — 执行工具调用
            var action = parseAction(thought);
            if (action != null) {
                String observation = executeTool(action);
                context += "Action: " + action + "\n";
                context += "Observation: " + observation + "\n\n";
            }
        }

        return "达到最大迭代次数,未能得出最终答案";
    }
}

2.4 ReAct 的关键限制

ReAct 的局限
│
├── 1. 步骤数不可控
│   ├── 简单问题可能1步就够,复杂问题需要10+步
│   └── 设置 maxIterations 是必要的,但要平衡效率和完整性
│
├── 2. 推理可能跑偏
│   ├── LLM 可能在 Thought 中做出错误推理
│   └── 后续步骤基于错误推理继续,越走越偏
│
├── 3. 工具选择可能错误
│   ├── LLM 可能选择不合适的工具
│   └── 导致 Observation 无用,浪费步骤
│
└── 4. Token 消耗大
    ├── 每步都需要完整的上下文
    └── 步骤越多,上下文越长,Token 越贵

三、Plan-Execute 模式:先规划后执行

3.1 核心思想

ReAct 是"边想边做",Plan-Execute 是"先想好再做"。把规划和执行分为两个独立阶段:

Plan-Execute 流程

├── Phase 1: Plan(规划阶段)
   ├── 根据任务生成执行计划
   ├── 计划包含有序的步骤列表
   └── 每个步骤有明确的输入、工具、预期输出

├── Phase 2: Execute(执行阶段)
   ├── 按计划逐步执行
   ├── 每步执行后记录结果
   └── 如果某步失败,标记异常

└── Phase 3: 总结(可选)
    ├── 汇总所有步骤结果
    └── 生成最终输出

3.2 Plan-Execute 示例

用户:帮我分析这个商机的风险等级

Phase 1: Plan
├── Step 1: 查询商机详情  queryOpportunity
├── Step 2: 查询客户历史  queryCustomerHistory
├── Step 3: 查询行业趋势  queryIndustryTrend
├── Step 4: 综合评估风险  LLM 分析
└── Step 5: 生成风险报告  输出

Phase 2: Execute
├── Step 1:   {客户=XX科技, 金额=50万, ...}
├── Step 2:   {回款率100%, 合作3次, ...}
├── Step 3:   {增长15%, 竞争激烈, ...}
├── Step 4:   {风险等级=中低, 依据=...}
└── Step 5:   风险报告已生成

3.3 代码实现

// 定义计划步骤
public record PlanStep(
    int order,
    String description,
    String toolName,
    Map<String, Object> params,
    String expectedOutput
) {}

// 定义执行计划
public record ExecutionPlan(
    String objective,
    List<PlanStep> steps
) {}

// Plan-Execute Agent
public class PlanExecuteAgent {

    private final ChatClient chatClient;
    private final Map<String, Tool> toolRegistry;

    // Phase 1: 生成计划
    public ExecutionPlan createPlan(String userRequest) {
        return chatClient.prompt()
            .user(u -> u.text("""
                根据用户请求,生成执行计划。
                
                可用工具:{tools}
                用户请求:{request}
                
                输出 JSON 格式的执行计划。
                """)
                .param("tools", getToolDescriptions())
                .param("request", userRequest))
            .call()
            .entity(ExecutionPlan.class);
    }

    // Phase 2: 执行计划
    public Map<Integer, String> execute(ExecutionPlan plan) {
        Map<Integer, String> results = new LinkedHashMap<>();
        
        for (PlanStep step : plan.steps()) {
            try {
                Tool tool = toolRegistry.get(step.toolName());
                String result = tool.execute(step.params());
                results.put(step.order(), result);
            } catch (Exception e) {
                results.put(step.order(), "ERROR: " + e.getMessage());
                // 可以选择终止或继续
            }
        }
        
        return results;
    }

    // Phase 3: 总结
    public String summarize(String userRequest, Map<Integer, String> results) {
        return chatClient.prompt()
            .user(u -> u.text("""
                用户请求:{request}
                
                执行结果:
                {results}
                
                请根据以上结果生成最终回答。
                """)
                .param("request", userRequest)
                .param("results", formatResults(results)))
            .call()
            .content();
    }
}

3.4 Plan-Execute vs ReAct

对比项ReActPlan-Execute
规划方式边做边想先规划后执行
灵活性高(每步动态决策)低(计划确定后较固定)
可预测性低(不知几步完成)高(计划可见)
错误恢复自然(下一步自动修正)需要额外逻辑
适用场景探索性任务步骤明确的任务
效率可能多步试错一次规划,高效执行

四、动态 Plan-Execute:结合两者优势

纯 Plan-Execute 太死板,纯 ReAct 太随机。最佳实践是动态 Plan-Execute

动态 Plan-Execute
│
├── 1. 先生成初始计划(Plan)
│
├── 2. 逐步执行(Execute)
│   ├── 每步执行后检查结果
│   ├── 如果结果符合预期 → 继续
│   └── 如果结果不符合 → 重新规划(Re-Plan)
│
└── 3. 动态调整
    ├── 如果发现需要额外的步骤 → 追加到计划
    └── 如果某些步骤不再需要 → 从计划移除
public class DynamicPlanExecuteAgent {

    private final ChatClient chatClient;
    private final Map<String, Tool> toolRegistry;

    public String execute(String userRequest) {
        // 1. 初始规划
        ExecutionPlan plan = createPlan(userRequest);
        Map<Integer, String> results = new LinkedHashMap<>();
        int currentStep = 0;

        while (currentStep < plan.steps().size()) {
            PlanStep step = plan.steps().get(currentStep);

            // 2. 执行当前步骤
            Tool tool = toolRegistry.get(step.toolName());
            String result;
            try {
                result = tool.execute(step.params());
            } catch (Exception e) {
                result = "ERROR: " + e.getMessage();
            }
            results.put(step.order(), result);

            // 3. 检查是否需要重新规划
            if (result.startsWith("ERROR:") || needsRePlan(results)) {
                plan = rePlan(userRequest, plan, results);
                // rePlan 可能追加/修改/删除步骤
                continue;  // 重新执行(可能从新步骤开始)
            }

            currentStep++;
        }

        // 4. 总结
        return summarize(userRequest, results);
    }

    private ExecutionPlan rePlan(String request, ExecutionPlan oldPlan,
                                  Map<Integer, String> results) {
        return chatClient.prompt()
            .user(u -> u.text("""
                原始请求:{request}
                原计划:{plan}
                已执行结果:{results}
                
                部分步骤执行失败。请根据已有结果重新规划剩余步骤。
                保留已成功完成的步骤,只调整未完成的部分。
                """)
                .param("request", request)
                .param("plan", oldPlan.toString())
                .param("results", results.toString()))
            .call()
            .entity(ExecutionPlan.class);
    }

    private boolean needsRePlan(Map<Integer, String> results) {
        return results.values().stream()
            .anyMatch(r -> r.startsWith("ERROR:"));
    }
}

五、Reflection 模式:自我反思与改进

5.1 核心思想

Reflection 让 Agent 在执行完成后,回顾自己的输出,评估质量并迭代改进。

Reflection 流程
│
├── 1. Generate:生成初始输出
│
├── 2. Critique:自我批评
│   ├── 输出是否完整?
│   ├── 是否有逻辑错误?
│   ├── 是否遗漏关键信息?
│   └── 是否可以更简洁/更准确?
│
├── 3. Revise:根据批评改进
│   └── 生成改进版本
│
└── 4. 重复 2-3,直到质量达标或达到最大迭代次数

5.2 示例

用户:写一个商机风险评估报告

Generate(第 1 版):
"该商机风险等级为中。建议跟进。"

Critique(自我批评):
├── 报告太简略,缺少具体依据
├── 没有说明"中"的判断标准
├── 没有给出具体的风险因素
└── 没有建议应对措施

Revise(第 2 版):
"风险等级:中低。
 依据:1) 客户历史回款率100%;2) 行业增长15%
 风险因素:竞品可能压价
 建议:1) 加快推进速度;2) 提供差异化方案"

Critique(第 2 次批评):
├── 有了具体依据,好多了
├── 但缺少量化评分
└── 建议部分可以更具体

Revise(第 3 版 — 最终):
"风险评分:72/100(中低)
 
 信用维度:85/100 — 历史回款率100%,合作3次0违约
 市场维度:70/100 — 行业增长15%,但竞争激烈
 金额维度:65/100 — 50万中等金额,利润率需确认
 
 关键风险:竞品压价(概率60%,影响高)
 应对建议:1) 3天内完成技术方案;2) 准备差异化演示;
            3) 预留10%价格弹性空间"

5.3 代码实现

public class ReflectionAgent {

    private final ChatClient chatClient;
    private final int maxIterations;

    public String execute(String userRequest) {
        // Step 1: Generate
        String output = generate(userRequest);

        for (int i = 0; i < maxIterations; i++) {
            // Step 2: Critique
            String critique = critique(userRequest, output);

            // 检查是否已足够好
            if (critique.contains("无改进空间") || critique.contains("已达标")) {
                break;
            }

            // Step 3: Revise
            output = revise(userRequest, output, critique);
        }

        return output;
    }

    private String generate(String request) {
        return chatClient.prompt()
            .user(request)
            .call()
            .content();
    }

    private String critique(String request, String output) {
        return chatClient.prompt()
            .user(u -> u.text("""
                请评估以下输出质量:
                
                原始请求:{request}
                当前输出:{output}
                
                评估标准:
                1. 完整性:是否覆盖了请求的所有方面?
                2. 准确性:信息是否正确、有依据?
                3. 清晰度:表达是否清晰、有条理?
                4. 实用性:对读者是否有实际价值?
                
                如果质量已达标,回复"已达标"。
                否则,指出具体需要改进的地方。
                """)
                .param("request", request)
                .param("output", output))
            .call()
            .content();
    }

    private String revise(String request, String currentOutput, String critique) {
        return chatClient.prompt()
            .user(u -> u.text("""
                请根据批评意见改进输出:
                
                原始请求:{request}
                当前输出:{currentOutput}
                批评意见:{critique}
                
                请生成改进版本。保留好的部分,只改进批评中提到的问题。
                """)
                .param("request", request)
                .param("currentOutput", currentOutput)
                .param("critique", critique))
            .call()
            .content();
    }
}

5.4 Reflection 的关键参数

Reflection 参数调优
│
├── 迭代次数
│   ├── 1-2 次:轻微改进,适合简单任务
│   ├── 3-5 次:显著改进,适合复杂任务
│   └── >5 次:收益递减,不建议
│
├── 评估标准
│   ├── 通用标准:完整性、准确性、清晰度
│   └── 定制标准:根据具体任务定义(如代码正确性、文档规范性)
│
└── 停止条件
    ├── Critique 表示"已达标"
    ├── 达到最大迭代次数
    └── 连续两次输出差异小于阈值(改进幅度小)

六、三种模式的组合使用

单一模式都有局限,真实项目中往往需要组合。

6.1 组合架构

完整 Agent 架构
│
├── 1. Grill Me(执行前)
│   └── 澄清需求,确认边界
│
├── 2. Plan-Execute(执行中)
│   ├── 先规划步骤
│   ├── 按计划逐步执行
│   └── 遇到异常动态调整
│
├── 3. ReAct(执行中)
│   ├── 每个步骤内部用 ReAct 模式
│   ├── Thought → Action → Observation
│   └── 确保每步推理有数据支撑
│
└── 4. Reflection(执行后)
    ├── 自我评估输出质量
    ├── 迭代改进直到达标
    └── 最终输出

6.2 组合代码框架

public class CombinedAgent {

    private final ChatClient chatClient;
    private final Map<String, Tool> toolRegistry;

    public Response execute(String userRequest) {
        // 1. Grill Me — 澄清需求
        var grillResult = grill(userRequest);
        if (grillResult.needGrill()) {
            return Response.questions(grillResult.questions());
        }
        String clarifiedRequest = grillResult.clarifiedRequest();

        // 2. Plan-Execute — 规划并执行
        var plan = createPlan(clarifiedRequest);
        var results = executePlan(plan);

        // 3. ReAct — 处理计划中的复杂步骤
        //    (已集成在 executePlan 内部)

        // 4. Reflection — 自我评估
        String output = summarize(clarifiedRequest, results);
        output = reflectAndImprove(clarifiedRequest, output);

        return Response.content(output);
    }

    private String reflectAndImprove(String request, String output) {
        for (int i = 0; i < 3; i++) {
            String critique = critique(request, output);
            if (critique.contains("已达标")) break;
            output = revise(request, output, critique);
        }
        return output;
    }
}

6.3 场景推荐

任务类型推荐组合原因
简单查询直接执行不需要复杂模式
数据分析报告Plan-Execute + Reflection步骤明确 + 需要质量保证
代码生成ReAct + Reflection需要试错 + 需要质量检查
方案设计Grill Me + Plan-Execute + Reflection需求模糊 + 步骤多 + 质量敏感
多步工作流Grill Me + Dynamic Plan-Execute需求模糊 + 可能遇异常

七、模式选择的决策树

选择哪种模式?
│
├── 任务是否模糊?
│   ├── 是 → 先 Grill Me 澄清
│   └── 否 → 继续
│
├── 步骤是否明确?
│   ├── 是 → Plan-Execute
│   │   └── 是否需要动态调整?
│   │       ├── 是 → Dynamic Plan-Execute
│   │       └── 否 → 纯 Plan-Execute
│   └── 否 → ReAct
│
├── 输出质量是否关键?
│   ├── 是 → 加 Reflection
│   └── 否 → 不加
│
└── 是否涉及不可逆操作?
    ├── 是 → 每步执行前确认
    └── 否 → 正常执行

八、成本与性能对比

模式LLM 调用次数延迟Token 消耗适用复杂度
直接执行11-2s基准
ReAct3-105-20s3-10x中-高
Plan-Execute2+N(N=步骤数)5-15s2-5x
Dynamic Plan-Execute2+N+M(M=重规划次数)8-25s3-8x
Reflection1+2K(K=迭代次数)3-10s2-5x
组合模式视组合而定视组合而定视组合而定

成本控制建议:

成本控制策略

├── 1. 根据任务复杂度选模式
   ├── 简单任务用直接执行
   └── 不要为了"高级"而用复杂模式

├── 2. 设置合理的上限
   ├── ReAct: maxIterations  10
   ├── Reflection: maxIterations  3
   └── Plan steps  10

├── 3. 用快速模型做中间步骤
   ├── 规划、批评用快速模型(如 gpt-4o-mini)
   └── 最终输出用高质量模型

└── 4. 缓存中间结果
    ├── 相同的工具调用不重复执行
    └── Reflection 每轮只改需要改的部分

九、最佳实践 Checklist

#实践说明
1根据任务复杂度选择模式简单任务不搞复杂
2ReAct 必须设 maxIterations防止无限循环
3Plan-Execute 先规划再执行避免走一步看一步
4Dynamic Plan-Execute 更实用纯计划太死板,动态调整更灵活
5Reflection 迭代不超过 3 次收益递减,3 次足够
6组合使用而非单独使用每种模式解决不同阶段的问题
7用快速模型做中间推理规划、批评用便宜模型
8监控每步执行结果记录日志,便于调试
9不可逆操作前要确认删除、发送等需要用户确认
10缓存工具调用结果避免重复调用相同工具

下一篇我们聊 Multi-Agent 协作编排:当一个 Agent 搞不定时,如何让多个 Agent 分工协作——主管 Agent、专家 Agent、评审 Agent 各司其职,组合完成复杂任务。


本文是 AI Agent 开发实战系列第 10 篇,系列目录:

  1. AI Agent 核心概念与架构
  2. 三大基石之 LLM 调用与 Prompt 工程
  3. 三大基石之记忆系统
  4. 三大基石之工具调用
  5. Java 生态 Agent 框架横评
  6. 用 Spring AI 搭建第一个 Agent
  7. Harness Engineering 与约束管理
  8. 输出 Schema 约束与结构化输出
  9. Grill Me 反问式规划
  10. 本文:Agent 设计模式(ReAct / Plan-Execute / Reflection)