首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >规划审核执行的Agent模式实现思考

规划审核执行的Agent模式实现思考

原创
作者头像
礼兴
发布2025-10-08 17:32:26
发布2025-10-08 17:32:26
1450
举报

一、背景

常见的React Agent是推理—行动循环框架,具体思路如下图

ReAct模式
ReAct模式

实现过程中,往往可以在设计实现的时候,往往会出现如下几种:

1、规划-执行模式

Plan-Execute模式
Plan-Execute模式

2、规划-人工检查-执行模式

相比较规划-执行,增加了human-in-the-loop的人工交互,进行审批通过/跳过/拒绝/修改规划,即先完成Plan,然后对计划进行人工检查,在执行每个步骤的结果也进行人工检查,是否符合预期。

3、规划-人工检查-执行-监督评估模式

主要引入监督评估,通过大模型自己做处理,人工检查是可选,实现流程相对比较复杂。

二、实现思路

1、自定义Agent实现逻辑方式

比如一个是PlanAgent,一个是HumanAgent,一个是ExecuteAgent,然后

代码语言:java
复制
    // 简单内存存储:reviewId -> plan
    private final Map<String, ExecutionPlan> planStore = new ConcurrentHashMap<>();
    /**
     * 步骤1:制作计划并创建人工审核会话,返回 reviewId + plan
     */
    @PostMapping("/start")
    public StartResponse start(@RequestBody StartRequest req) {
        PlanMakerAgent planMaker = new PlanMakerAgent(chatModel);
        ExecutionPlan plan = planMaker.createPlan(req.userRequest());
        String reviewId = humanReviewAgent.startReview(plan);
        planStore.put(reviewId, plan);
        return new StartResponse(reviewId, plan);
    }

    /**
     * 步骤2+3:等待审核决定后执行计划(阻塞该请求直到审核结果到达或超时)
     * 建议在前端先调用 HumanReviewController 完成 /approve|/reject,再调用该接口触发执行
     */
    @PostMapping("/{reviewId}/execute")
    public ExecuteResponse execute(@PathVariable String reviewId,
                                   @RequestParam(defaultValue = "PT30M") String timeoutIso) {
        ExecutionPlan plan = planStore.get(reviewId);
        if (plan == null) {
            throw new IllegalArgumentException("reviewId not found: " + reviewId);
        }
        PlanExecutorAgent executor = new PlanExecutorAgent(chatModel, humanReviewAgent);

        // 等待人工审核
        HumanReviewAgent.ReviewResult decision = humanReviewAgent.awaitDecision(reviewId, Duration.parse(timeoutIso));
        if (!decision.approved()) {
            String logPath = txtFileSaveTool.saveWithTimestamp("plan_rejected", "计划被拒绝: " + decision.reason());
            return new ExecuteResponse("REJECTED", "计划被拒绝: " + decision.reason(), null, logPath);
        }

        // 执行计划
        ExecutionResult result = executor.executePlan(plan);

        // 将执行过程整理为文本并保存
        String textLog = buildTextLog(plan, result);
        String logPath = txtFileSaveTool.saveWithTimestamp("plan_run", textLog);

        return new ExecuteResponse(result.getStatus().name(), result.getMessage(), result, logPath);
    }

Plan制定Agent

代码语言:java
复制
/**
 * 计划制作Agent
 * 负责根据用户需求制作详细的执行计划
 */
@Slf4j
public class PlanMakerAgent {

    private final ChatClient chatClient;

    public PlanMakerAgent(OpenAiChatModel chatModel) {
        this.chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }

    /**
     * 根据用户需求创建执行计划
     */
    public ExecutionPlan createPlan(String userRequest) {
        log.info("开始制作计划,用户需求: {}", userRequest);

        String systemPrompt = """
            你是一个专业的计划制作专家。根据用户的需求,制作一个详细的执行计划。

            计划应该包含以下要素:
            1. 计划标题和描述
            2. 详细的执行步骤
            3. 每个步骤的预期结果
            4. 所需的工具和资源
            5. 预估的执行时间

            请以JSON格式返回计划,格式如下:
            {
                "title": "计划标题",
                "description": "计划描述",
                "steps": [
                    {
                        "stepNumber": 1,
                        "title": "步骤标题",
                        "description": "步骤详细描述",
                        "tools": ["工具1", "工具2"],
                        "expectedResult": "预期结果",
                        "estimatedTime": "预估时间"
                    }
                ],
                "totalEstimatedTime": "总预估时间",
                "requiredTools": ["所有需要的工具"],
                "notes": "注意事项"
            }
            """;

        String userPrompt = String.format("""
            用户需求:%s

            请为这个需求制作一个详细的执行计划。
            特别注意:
            1. 如果涉及搜索,请考虑使用GoogleSearchTool或BrowserUseTool
            2. 如果需要总结搜索结果,请使用SearchSummaryTool
            3. 如果需要生成PPT,请使用PPTGenerationTool
            4. 步骤要具体可执行,避免过于抽象
            5. 考虑步骤之间的依赖关系
            """, userRequest);

        try {
            String response = chatClient.prompt()
                    .system(systemPrompt)
                    .user(userPrompt)
                    .call()
                    .content();

            log.info("计划制作完成,响应: {}", response);

            // 解析JSON响应
            ExecutionPlan plan = parseExecutionPlan(response);
            plan.setStatus(ExecutionPlan.PlanStatus.DRAFT);
            plan.setCreatedTime(System.currentTimeMillis());

            return plan;

        } catch (Exception e) {
            log.error("制作计划失败: {}", e.getMessage(), e);
            throw new RuntimeException("制作计划失败: " + e.getMessage(), e);
        }
    }

    /**
     * 解析执行计划JSON
     */
    private ExecutionPlan parseExecutionPlan(String jsonResponse) {
        try {
            // 清理可能的markdown代码块标记
            String cleanJson = jsonResponse.trim();
            if (cleanJson.startsWith("```json")) {
                cleanJson = cleanJson.substring(7);
            }
            if (cleanJson.startsWith("```")) {
                cleanJson = cleanJson.substring(3);
            }
            if (cleanJson.endsWith("```")) {
                cleanJson = cleanJson.substring(0, cleanJson.length() - 3);
            }
            cleanJson = cleanJson.trim();

            return JSON.parseObject(cleanJson, ExecutionPlan.class);

        } catch (Exception e) {
            log.error("解析计划JSON失败: {}", e.getMessage(), e);

            // 创建一个默认的计划
            ExecutionPlan defaultPlan = new ExecutionPlan();
            defaultPlan.setTitle("解析失败的计划");
            defaultPlan.setDescription("由于JSON解析失败,创建了默认计划");

            ExecutionPlan.PlanStep step = new ExecutionPlan.PlanStep();
            step.setStepNumber(1);
            step.setTitle("手动处理");
            step.setDescription("需要手动处理原始响应: " + jsonResponse);
            step.setTools(List.of("手动操作"));
            step.setExpectedResult("完成任务");
            step.setEstimatedTime("未知");

            defaultPlan.setSteps(List.of(step));
            defaultPlan.setTotalEstimatedTime("未知");
            defaultPlan.setRequiredTools(List.of("手动操作"));
            defaultPlan.setNotes("原始响应解析失败,请检查格式");

            return defaultPlan;
        }
    }

}

人工检查Agent

代码语言:java
复制
/**
 * 人工审核服务(HTTP 交互版)
 * - startReview: 创建审核会话并返回 reviewId
 * - awaitDecision: 在服务器端等待人工决策(通过 Controller 提交)
 * - approve/reject/modify: 提交人工决策
 */
@Service
@Slf4j
public class HumanReviewAgent {

    private final Map<String, CompletableFuture<ReviewResult>> sessions = new ConcurrentHashMap<>();

    /**
     * 发起审核并返回 reviewId
     */
    public String startReview(ExecutionPlan plan) {
        String id = UUID.randomUUID().toString();
        sessions.put(id, new CompletableFuture<>());
        return id;
    }

    /**
     * 等待人工决策,超时抛出异常
     */
    public ReviewResult awaitDecision(String reviewId, Duration timeout) {
        CompletableFuture<ReviewResult> future = sessions.get(reviewId);
        if (future == null) {
            throw new IllegalArgumentException("reviewId not found: " + reviewId);
        }
        try {
            return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            throw new RuntimeException("await decision failed: " + e.getMessage(), e);
        } finally {
            sessions.remove(reviewId);
        }
    }

    public void approve(String reviewId, String reason) {
        complete(reviewId, new ReviewResult(true, reason, "APPROVE"));
    }

    public void reject(String reviewId, String reason) {
        complete(reviewId, new ReviewResult(false, reason, "REJECT"));
    }

    public void modify(String reviewId, String suggestion) {
        complete(reviewId, new ReviewResult(false, suggestion, "MODIFY"));
    }

    private void complete(String reviewId, ReviewResult result) {
        CompletableFuture<ReviewResult> future = sessions.get(reviewId);
        if (future == null) {
            throw new IllegalArgumentException("reviewId not found: " + reviewId);
        }
        future.complete(result);
    }
    /**
     * 失败后简单确认:是否继续。
     * 备注:最小实现先直接返回 true(继续),避免阻塞;
     * 若需要真正的人机交互,可扩展为使用 CompletableFuture 并通过 Controller 完成决策。
     */
    public boolean simpleConfirm(String message) {
        log.warn("[Confirm] {} -> default=continue(true)", message);
        return true;
    }

    /**
     * 审核结果
     */
    public record ReviewResult(boolean approved, String reason, String action) { }
}

2、通过graph的方式

例如LangGraph,SpringAI Graph等实现方式,好处就是可以借助现成实现,例如HumanNode,StateGraph的方式组件执行图以及保存状态。

代码语言:java
复制
import com.alibaba.cloud.ai.graph.*;
import com.alibaba.cloud.ai.graph.action.AsyncNodeAction;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.config.SaverConfig;
import com.alibaba.cloud.ai.graph.checkpoint.constant.SaverConstant;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.alibaba.cloud.ai.graph.node.HumanNode;
import com.alibaba.cloud.ai.graph.state.StateSnapshot;

/**
 * 使用 Alibaba Cloud AI Graph(StateGraph) 实现的 计划-人工-执行 控制器
 * - start: 生成计划并创建人工审核会话
 * - execute: 等待人工审核后,使用 StateGraph 为每个步骤构建节点并顺序执行
 */
@RestController
@RequestMapping("/api/graph-workflow")
@Slf4j
public class PlanHumanExecuteAgentController {
    private final CompiledGraph compiledGraph;

    // 内存存储:reviewId -> plan
    private final Map<String, ExecutionPlan> planStore = new ConcurrentHashMap<>();
    
    
    /**
     * 构建 StateGraph(包含 HumanNode)
     */
    private CompiledGraph buildGraph() throws Exception {
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .defaultToolCallbacks(ToolBuilder.getPlanReviewExecuteAgentToolCallbacks())
                .build();

        ReactAgent executorAgent = new ReactAgent(
                "planExecutor",
                chatClient,
                ToolBuilder.getPlanReviewExecuteAgentToolCallbacks(),
                6);
        executorAgent.getAndCompileGraph();

        // 创建 HumanNode 并包装为 AsyncNodeAction
        HumanNode humanNode = new HumanNode();

        // 构建 StateGraph:START -> HumanNode -> ExecutorAgent -> END
        OverAllStateFactory stateFactory = OverAllState::new;
        StateGraph graph = new StateGraph(stateFactory)
                .addNode("human", AsyncNodeAction.node_async(humanNode))
                .addNode("executor", executorAgent.asAsyncNodeAction("input", "final_output"))
                .addEdge(StateGraph.START, "human")
                .addEdge("human", "executor")
                .addEdge("executor", StateGraph.END);

        // 添加 PlantUML 打印
        GraphRepresentation representation = graph.getGraph(GraphRepresentation.Type.PLANTUML,
                "human flow");
        log.info("\n=== expander UML Flow ===");
        log.info(representation.content());
        log.info("==================================\n");

        // 配置 MemorySaver 和中断点
        SaverConfig saverConfig = SaverConfig.builder()
                .register(SaverConstant.MEMORY, new MemorySaver())
                .build();

        return graph.compile(CompileConfig.builder()
                .saverConfig(saverConfig)
                .interruptBefore("human")  // 在 human 节点前中断
                .build());
    }

    /**
     * 步骤1:制作计划并创建人工审核会话,返回 reviewId + plan
     */
    @PostMapping("/start")
    public StartResponse start(@RequestBody StartRequest req) {
        PlanMakerAgent planMaker = new PlanMakerAgent(chatModel);
        ExecutionPlan plan = planMaker.createPlan(req.userRequest());
        String reviewId = UUID.randomUUID().toString();
        planStore.put(reviewId, plan);
        return new StartResponse(reviewId, plan);
    }

    /**
     * 步骤2:启动执行流程(会在 HumanNode 前中断,等待人工审核)
     */
    @PostMapping("/{reviewId}/execute")
    public ExecuteResponse execute(@PathVariable String reviewId,
                                   @RequestParam(defaultValue = "PT30M") String timeoutIso) {
        ExecutionPlan plan = planStore.get(reviewId);
        if (plan == null) {
            throw new IllegalArgumentException("reviewId not found: " + reviewId);
        }

        try {
            // 准备初始输入
            Map<String, Object> inputs = new HashMap<>();
            inputs.put("input", buildExecutorPrompt(plan));

            // 使用 reviewId 作为 threadId
            RunnableConfig runnableConfig = RunnableConfig.builder()
                    .threadId(reviewId)
                    .build();

            log.info("启动 StateGraph 执行,将在 HumanNode 前中断,reviewId={}", reviewId);

            // 执行到 HumanNode 前会自动中断
            compiledGraph.invoke(inputs, runnableConfig);

            return new ExecuteResponse("PENDING",
                    "执行已启动,等待人工审核。请调用 /approve、/reject 或 /modify 接口进行审核",
                    null,
                    null);

        } catch (Exception e) {
            log.error("Graph 执行失败", e);
            String msg = e.getMessage();
            String logPath = txtFileSaveTool.saveWithTimestamp("plan_error", "执行异常: " + msg);
            return new ExecuteResponse("FAILED", "执行异常: " + msg, null, logPath);
        }
    }
    
    /**
     * 批准审核并恢复执行
     */
    @PostMapping("/reviews/{reviewId}/approve")
    public ExecuteResponse approve(@PathVariable String reviewId, @RequestBody(required = false) ReasonRequest req) {
        String reason = req != null ? req.reason() : "";
        log.info("收到批准请求,reviewId={}, reason={}", reviewId, reason);

        try {
            // 恢复执行
            return resumeExecution(reviewId, true, reason);
        } catch (Exception e) {
            log.error("批准后恢复执行失败", e);
            return new ExecuteResponse("FAILED", "批准后恢复执行失败: " + e.getMessage(), null, null);
        }
    }

    /**
     * 拒绝审核
     */
    @PostMapping("/reviews/{reviewId}/reject")
    public Map<String,Object> reject(@PathVariable String reviewId, @RequestBody(required = false) ReasonRequest req) {
        String reason = req != null ? req.reason() : "";
        log.info("收到拒绝请求,reviewId={}, reason={}", reviewId, reason);

        ExecutionPlan plan = planStore.get(reviewId);
        if (plan != null) {
            plan.setStatus(ExecutionPlan.PlanStatus.REJECTED);
        }

        String logPath = txtFileSaveTool.saveWithTimestamp("plan_rejected", "计划被拒绝: " + reason);
        return Map.of("status", "REJECTED", "reviewId", reviewId, "reason", reason, "logPath", logPath);
    }
    /**
     * 恢复执行(从 HumanNode 继续)
     */
    private ExecuteResponse resumeExecution(String reviewId, boolean approved, String reason) throws Exception {
        ExecutionPlan plan = planStore.get(reviewId);
        if (plan == null) {
            throw new IllegalArgumentException("reviewId not found: " + reviewId);
        }

        RunnableConfig runnableConfig = RunnableConfig.builder()
                .threadId(reviewId)
                .build();

        // 获取当前状态
        StateSnapshot stateSnapshot = compiledGraph.getState(runnableConfig);
        OverAllState state = stateSnapshot.state();
        state.withResume();

        // 设置人工反馈
        Map<String, Object> feedbackData = new HashMap<>();
        feedbackData.put("approved", approved);
        feedbackData.put("reason", reason);
        state.withHumanFeedback(new OverAllState.HumanFeedback(feedbackData, reason));

        log.info("恢复执行,reviewId={}, approved={}", reviewId, approved);

        // 从当前节点继续执行
        var snapshot = compiledGraph.invoke(state.data(), runnableConfig).get();
        String finalOut = Objects.toString(snapshot.data().get("final_output"), null);

        // 整理为 ExecutionResult
        ExecutionResult execResult = new ExecutionResult(plan);
        StepResult sr = new StepResult(0, "Agent 执行器");
        sr.setSuccess(finalOut != null);
        sr.setResult(finalOut);
        execResult.addStepResult(sr);
        execResult.setStatus(ExecutionResult.Status.COMPLETED);
        execResult.setMessage("计划执行完成");
        plan.setStatus(ExecutionPlan.PlanStatus.COMPLETED);

        // 保存文本日志
        String textLog = buildTextLog(plan, execResult);
        String logPath = txtFileSaveTool.saveWithTimestamp("plan_run", textLog);

        return new ExecuteResponse(execResult.getStatus().name(), execResult.getMessage(), execResult, logPath);
    }


    private String buildTextLog(ExecutionPlan plan, ExecutionResult result) {
        StringBuilder sb = new StringBuilder();
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        sb.append("# ").append(plan.getTitle() != null ? plan.getTitle() : "执行计划").append("\n\n");
        sb.append("时间:").append(LocalDateTime.now().format(fmt)).append("\n");
        if (plan.getDescription() != null) sb.append("描述:").append(plan.getDescription()).append("\n\n");
        sb.append("## 步骤结果\n");
        if (result != null && result.getStepResults() != null) {
            result.getStepResults().forEach(sr -> {
                sb.append(String.format("[StepResult] %s | success=%s | result=%s\n",
                        sr.getStepTitle(), sr.isSuccess(), sr.getResult()));
            });
        }
        sb.append("\n状态:").append(result != null ? result.getStatus() : "N/A").append("\n");
        if (result != null && result.getMessage() != null) sb.append("消息:").append(result.getMessage()).append("\n");
        return sb.toString();
    }

    private String buildExecutorPrompt(ExecutionPlan plan) {
        StringBuilder sb = new StringBuilder();
        sb.append("你是一个执行代理,请根据以下计划逐步执行各个步骤;可以按需调用已注册的工具(如 GoogleSearchTool、BrowserUseTool、SearchSummaryTool、FileSaverTool)。\n");
        sb.append("要求:\n");
        sb.append("- 严格逐步执行,不要跳步;\n");
        sb.append("- 每一步尽量给出清晰结果;\n");
        sb.append("- 如果需要将文本保存到文件,请调用 FileSaverTool 对应方法;\n");
        sb.append("- 最终请输出所有步骤结果的汇总说明。\n\n");
        sb.append("计划标题:").append(plan.getTitle()).append("\n");
        sb.append("计划描述:").append(plan.getDescription()).append("\n\n");
        sb.append("执行步骤:\n");
        for (ExecutionPlan.PlanStep step : plan.getSteps()) {
            sb.append(String.format("步骤%d:%s\n", step.getStepNumber(), step.getTitle()));
            sb.append(String.format("  描述:%s\n", step.getDescription()));
            sb.append(String.format("  预期结果:%s\n", step.getExpectedResult()));
            if (step.getTools() != null && !step.getTools().isEmpty()) {
                sb.append(String.format("  可用工具:%s\n", String.join(", ", step.getTools())));
            }
        }
        return sb.toString();
    }
}    

三、展示效果

人工审核交互
人工审核交互

参考资料:https://cloud.tencent.com/developer/news/3021564

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、实现思路
  • 三、展示效果
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档