""" 计划模式 — Plan-before-Execute 参考 Claude Code EnterPlanModeTool.ts 设计: - 先生成执行计划(Plan) - 向用户展示计划,等待审批 - 审批通过后逐步执行 - 计划阶段自动限制为只读工具 """ from __future__ import annotations import json import logging from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from app.agent_runtime.schemas import AgentLLMConfig logger = logging.getLogger(__name__) # ──────────────────────────── 数据结构 ──────────────────────────── @dataclass class PlanStep: """单个执行步骤""" action: str # 步骤描述 tool_name: Optional[str] = None # 使用的工具名称 tool_args: Optional[Dict[str, Any]] = None # 工具参数 expected_output: str = "" # 预期产出 dependencies: List[int] = field(default_factory=list) # 依赖的步骤索引 @dataclass class Plan: """执行计划""" goal: str # 目标描述 steps: List[PlanStep] = field(default_factory=list) risks: List[str] = field(default_factory=list) # 风险提示 assumptions: List[str] = field(default_factory=list) # 假设条件 estimated_iterations: int = 5 # 预估 ReAct 步数 def to_dict(self) -> Dict[str, Any]: return { "goal": self.goal, "steps": [ { "index": i + 1, "action": s.action, "tool_name": s.tool_name, "expected_output": s.expected_output, } for i, s in enumerate(self.steps) ], "risks": self.risks, "assumptions": self.assumptions, "estimated_iterations": self.estimated_iterations, } def to_markdown(self) -> str: """渲染计划为 Markdown(用于展示给用户)。""" lines = [f"## 执行计划: {self.goal}\n"] lines.append("### 步骤\n") for i, s in enumerate(self.steps, 1): tool_info = f" (工具: {s.tool_name})" if s.tool_name else "" lines.append(f"{i}. {s.action}{tool_info}") if s.expected_output: lines.append(f" - 预期产出: {s.expected_output}") if self.risks: lines.append("\n### 风险提示\n") for r in self.risks: lines.append(f"- {r}") if self.assumptions: lines.append("\n### 假设条件\n") for a in self.assumptions: lines.append(f"- {a}") lines.append(f"\n*预估需要 {self.estimated_iterations} 次 ReAct 迭代*") return "\n".join(lines) class PlanStatus: PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" EXECUTING = "executing" COMPLETED = "completed" # ──────────────────────────── 计划生成器 ──────────────────────────── class PlanMode: """ 计划模式:先规划,再执行。 用法: plan_mode = PlanMode(llm_config) plan = await plan_mode.generate_plan("重构用户模块", tool_names, history) # 展示给用户,等待审批 if approved: async for step_result in plan_mode.execute_plan(plan, tool_executor): ... """ PLAN_SYSTEM_PROMPT = """你是一个资深软件架构师和分析专家。你的任务是为用户的需求生成详细的执行计划。 请分析用户需求,拆解为具体的执行步骤。每个步骤应包含: 1. 具体操作描述 2. 可能需要的工具 3. 预期产出 注意: - 你处于**计划模式**,只能使用只读工具(搜索、读取文件等)来了解上下文 - 不能修改任何文件或执行破坏性操作 - 优先探索现有的代码和文档,基于实际情况制定计划 - 考虑风险点和假设条件 最终输出一个 JSON 格式的执行计划: ```json { "goal": "目标描述", "steps": [ {"action": "步骤1", "tool_name": "工具名或null", "expected_output": "预期产出"}, ... ], "risks": ["风险1", "风险2"], "assumptions": ["假设1"], "estimated_iterations": 5 } ```""" def __init__(self, llm_config: Optional[AgentLLMConfig] = None): self.llm_config = llm_config or AgentLLMConfig() # 计划模式使用更低的温度以获得更稳定的规划 if self.llm_config.temperature > 0.3: self._plan_temperature = 0.3 else: self._plan_temperature = self.llm_config.temperature async def generate_plan( self, user_input: str, available_tools: List[str], messages_history: Optional[List[Dict[str, Any]]] = None, ) -> Plan: """ 使用 LLM 生成结构化执行计划。 Args: user_input: 用户需求描述 available_tools: 当前可用的工具名称列表 messages_history: 历史消息(可选) Returns: 结构化的执行计划 """ from app.agent_runtime.core import _LLMClient tools_str = ", ".join(available_tools) if available_tools else "无" history_text = "" if messages_history: recent = messages_history[-6:] # 取最近 6 条 history_text = "\n".join( f"[{m.get('role', '?')}]: {str(m.get('content', ''))[:300]}" for m in recent ) user_prompt = ( f"## 用户需求\n{user_input}\n\n" f"## 可用工具\n{tools_str}\n" ) if history_text: user_prompt += f"\n## 对话历史(最近)\n{history_text}\n" user_prompt += "\n请生成 JSON 格式的执行计划。" messages = [ {"role": "system", "content": self.PLAN_SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ] plan_config = AgentLLMConfig( provider=self.llm_config.provider, model=self.llm_config.plan_model or self.llm_config.model, temperature=self._plan_temperature, max_tokens=2000, api_key=self.llm_config.api_key, base_url=self.llm_config.base_url, request_timeout=60.0, ) client = _LLMClient(plan_config) response = await client.chat(messages=messages, tools=None, iteration=0) content = getattr(response, 'content', '') or ( response.get('content', '') if isinstance(response, dict) else str(response) ) return self._parse_plan(content, user_input) def _parse_plan(self, llm_output: str, fallback_goal: str = "") -> Plan: """从 LLM 输出解析为 Plan 对象。""" try: # 提取 JSON 块 json_text = llm_output if "```json" in llm_output: start = llm_output.index("```json") + 7 end = llm_output.index("```", start) json_text = llm_output[start:end] elif "```" in llm_output: start = llm_output.index("```") + 3 end = llm_output.index("```", start) json_text = llm_output[start:end] data = json.loads(json_text.strip()) return Plan( goal=data.get("goal", fallback_goal), steps=[ PlanStep( action=s.get("action", f"步骤 {i+1}"), tool_name=s.get("tool_name"), tool_args=s.get("tool_args"), expected_output=s.get("expected_output", ""), dependencies=s.get("dependencies", []), ) for i, s in enumerate(data.get("steps", [])) ], risks=data.get("risks", []), assumptions=data.get("assumptions", []), estimated_iterations=data.get("estimated_iterations", 5), ) except (json.JSONDecodeError, ValueError, KeyError) as e: logger.warning("计划解析失败,使用降级计划: %s", e) # 降级: 构建一个简单的单步计划 return Plan( goal=fallback_goal or llm_output[:200], steps=[PlanStep(action=llm_output[:500] or "执行用户请求")], risks=["无法解析 LLM 输出为结构化计划"], ) @staticmethod async def present_plan(plan: Plan) -> bool: """ 向用户展示计划并等待审批。 在实际使用中,这会通过回调/事件机制向 UI 发送计划并等待用户决策。 此处提供同步版本的基础实现。 Returns: True = 批准, False = 拒绝 """ # 默认返回 True(在流式模式下由外部决策) # UI 层应展示 plan.to_markdown() 并收集用户决定 logger.info("计划已生成,等待审批:\n%s", plan.to_markdown()) return True @staticmethod def is_read_only_step(step: PlanStep) -> bool: """判断计划步骤是否只读(在计划阶段安全执行)。""" if not step.tool_name: return True from app.agent_runtime.permissions import is_read_only_tool return is_read_only_tool(step.tool_name)