Files
aiagent/backend/app/agent_runtime/plan_mode.py

264 lines
9.4 KiB
Python
Raw Normal View History

"""
计划模式 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)