264 lines
9.4 KiB
Python
264 lines
9.4 KiB
Python
|
|
"""
|
|||
|
|
计划模式 — 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)
|