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

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


一、为什么需要设计模式

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

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

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

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

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

二、ReAct 模式:推理 + 行动

2.1 核心思想

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

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

2.2 一个完整的 ReAct 示例

erlang 复制代码
用户:这个商机值得跟进吗?

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

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

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

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

对比无模式的直接回答:

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

2.3 Spring AI 实现 ReAct

java 复制代码
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 的关键限制

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

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

3.1 核心思想

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

yaml 复制代码
Plan-Execute 流程
│
├── Phase 1: Plan(规划阶段)
│   ├── 根据任务生成执行计划
│   ├── 计划包含有序的步骤列表
│   └── 每个步骤有明确的输入、工具、预期输出
│
├── Phase 2: Execute(执行阶段)
│   ├── 按计划逐步执行
│   ├── 每步执行后记录结果
│   └── 如果某步失败,标记异常
│
└── Phase 3: 总结(可选)
    ├── 汇总所有步骤结果
    └── 生成最终输出

3.2 Plan-Execute 示例

yaml 复制代码
用户:帮我分析这个商机的风险等级

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 代码实现

java 复制代码
// 定义计划步骤
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

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

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

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

sql 复制代码
动态 Plan-Execute
│
├── 1. 先生成初始计划(Plan)
│
├── 2. 逐步执行(Execute)
│   ├── 每步执行后检查结果
│   ├── 如果结果符合预期 → 继续
│   └── 如果结果不符合 → 重新规划(Re-Plan)
│
└── 3. 动态调整
    ├── 如果发现需要额外的步骤 → 追加到计划
    └── 如果某些步骤不再需要 → 从计划移除
java 复制代码
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 示例

arduino 复制代码
用户:写一个商机风险评估报告

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 代码实现

java 复制代码
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 的关键参数

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

六、三种模式的组合使用

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

6.1 组合架构

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

6.2 组合代码框架

java 复制代码
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 需求模糊 + 可能遇异常

七、模式选择的决策树

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

八、成本与性能对比

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

成本控制建议:

yaml 复制代码
成本控制策略
│
├── 1. 根据任务复杂度选模式
│   ├── 简单任务用直接执行
│   └── 不要为了"高级"而用复杂模式
│
├── 2. 设置合理的上限
│   ├── ReAct: maxIterations ≤ 10
│   ├── Reflection: maxIterations ≤ 3
│   └── Plan steps ≤ 10
│
├── 3. 用快速模型做中间步骤
│   ├── 规划、批评用快速模型(如 gpt-4o-mini)
│   └── 最终输出用高质量模型
│
└── 4. 缓存中间结果
    ├── 相同的工具调用不重复执行
    └── Reflection 每轮只改需要改的部分

九、最佳实践 Checklist

# 实践 说明
1 根据任务复杂度选择模式 简单任务不搞复杂
2 ReAct 必须设 maxIterations 防止无限循环
3 Plan-Execute 先规划再执行 避免走一步看一步
4 Dynamic Plan-Execute 更实用 纯计划太死板,动态调整更灵活
5 Reflection 迭代不超过 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)
相关推荐
鱼饼Y1 小时前
AI时代,使用大模型学习LangChain (4)——LangGraph
typescript·langchain
吃饱了得干活1 小时前
Agent 记忆系统:从短期记忆到长期记忆
python·langchain·agent
知行合一。。。3 小时前
LangChain--11--Milvus
langchain·milvus
流浪0014 小时前
LLM 大模型三种主流接入方式详解(API / 本地部署 / SDK)
langchain·llm
二进制_博客16 小时前
LangSmith 调试
langchain·langsmith
DRXB25072021 小时前
开源自由还是生态红利?LangChain 的灵活性与小艺开放平台的鸿蒙流量池,开发者该如何抉择?
langchain·开源·harmonyos
鱼饼Y1 天前
AI时代,使用大模型学习LangChain (3)——LCEL
人工智能·langchain
用户667675093791 天前
LangChain 向量检索为什么要用 as_retriever?从 LCEL 到 RunnableParallel 一次讲清
langchain
lzjava20241 天前
LangGraph Subgraphs 子图
langchain