Browse Source

Merge branch 'dev_tao' of https://git.yishihui.com/howard/Agent into dev_tao

max_liu 3 days ago
parent
commit
cb9e82ae72

+ 2 - 1
.claude/settings.local.json

@@ -13,7 +13,8 @@
       "Bash(curl:*)",
       "Bash(tree:*)",
       "Bash(xargs grep:*)",
-      "Bash(npm run:*)"
+      "Bash(npm run:*)",
+      "Bash(sed:*)"
     ],
     "deny": [],
     "ask": []

+ 546 - 0
agent/core/prompts.py

@@ -0,0 +1,546 @@
+"""
+Agent 系统 Prompt 集中管理
+
+本文件集中管理 Agent 系统中使用的所有 prompt 模板,
+包括 runner.py、compaction.py、subagent.py、knowledge.py 等文件中的 prompt。
+
+## 📑 目录索引
+
+### 1. 系统级 Prompt (行 50+)
+- DEFAULT_SYSTEM_PREFIX                      # Agent 基础系统提示
+
+### 2. 研究流程 Prompt (行 60+)
+- RESEARCH_STAGE_PROMPT_TEMPLATE             # 调研阶段引导
+- PLANNING_STAGE_PROMPT                      # 计划阶段引导
+- RESEARCH_DECISION_GUIDE_TEMPLATE           # 调研决策阶段引导
+
+### 3. 压缩相关 Prompt (行 110+)
+- COMPRESSION_EVAL_PROMPT_TEMPLATE           # Level 2 压缩与经验评估
+- REFLECT_PROMPT                             # 经验反思提取
+- SUMMARY_HEADER_TEMPLATE                    # 压缩后的摘要头部
+
+### 4. 工具执行 Prompt (行 180+)
+- TRUNCATION_HINT                            # 响应截断提示
+- TOOL_INTERRUPTED_MESSAGE                   # 工具执行中断提示
+- AGENT_INTERRUPTED_SUMMARY                  # Agent 中断摘要
+- AGENT_CONTINUE_HINT_TEMPLATE               # Agent 继续执行提示
+
+### 5. 任务生成 Prompt (行 200+)
+- TASK_NAME_GENERATION_SYSTEM_PROMPT         # 任务名称生成
+- TASK_NAME_FALLBACK                         # 默认任务名称
+
+### 6. 经验管理 Prompt (行 210+)
+- EXPERIENCE_ENTRY_TEMPLATE                  # 经验条目格式
+- EXPERIENCE_SUMMARY_WITH_RESULTS            # 有结果的经验摘要
+- EXPERIENCE_SUMMARY_NO_RESULTS              # 无结果的经验摘要
+- EXPERIENCE_PARSE_WARNING                   # 经验解析警告
+
+### 7. 辅助函数 - 基础 Prompt 构建 (行 240+)
+- build_research_stage_prompt()              # 构建调研阶段 prompt
+- build_research_decision_guide()            # 构建调研决策引导
+- build_compression_eval_prompt()            # 构建压缩评估 prompt
+- build_summary_header()                     # 构建摘要头部
+- build_tool_interrupted_message()           # 构建工具中断消息
+- build_agent_continue_hint()                # 构建 Agent 继续提示
+- build_experience_entry()                   # 构建经验条目
+
+### 8. 子 Agent 相关 Prompt (行 320+)
+- EVALUATE_PROMPT_TEMPLATE                   # 评估任务 prompt
+- DELEGATE_RESULT_HEADER                     # 委托任务结果头部
+- DELEGATE_SAVED_KNOWLEDGE_HEADER            # 保存知识头部
+- DELEGATE_STATS_HEADER                      # 执行统计头部
+- EXPLORE_RESULT_HEADER                      # 探索结果头部
+- EXPLORE_BRANCH_TEMPLATE                    # 探索分支模板
+- EXPLORE_STATUS_SUCCESS                     # 成功状态
+- EXPLORE_STATUS_FAILED                      # 失败状态
+- EXPLORE_STATUS_ERROR                       # 错误状态
+- EXPLORE_SUMMARY_HEADER                     # 探索总结头部
+
+### 9. 辅助函数 - 子 Agent Prompt 构建 (行 380+)
+- build_evaluate_prompt()                    # 构建评估 prompt
+
+### 10. 知识管理相关 Prompt (行 400+)
+- KNOWLEDGE_SEMANTIC_ROUTE_PROMPT_TEMPLATE   # 知识语义路由
+- KNOWLEDGE_EVOLVE_PROMPT_TEMPLATE           # 知识进化重写
+- KNOWLEDGE_SLIM_PROMPT_TEMPLATE             # 知识库瘦身
+
+### 11. 辅助函数 - 知识管理 Prompt 构建 (行 450+)
+- build_knowledge_semantic_route_prompt()    # 构建知识路由 prompt
+- build_knowledge_evolve_prompt()            # 构建知识进化 prompt
+- build_knowledge_slim_prompt()              # 构建知识瘦身 prompt
+
+## 🔍 快速查找
+
+**按使用场景查找:**
+- 研究流程:第 2 节
+- 对话压缩:第 3 节
+- 工具调用:第 4 节
+- 经验管理:第 6 节
+- 子 Agent:第 8 节
+- 知识管理:第 10 节
+
+**按文件来源查找:**
+- runner.py → 第 1, 2, 4, 5 节
+- compaction.py → 第 3 节
+- subagent.py → 第 8 节
+- knowledge.py → 第 10 节
+
+## ⚠️ 重要提示
+
+1. **变量占位符**:所有 `{变量名}` 格式的占位符必须保留
+2. **特殊标记**:`[[EVALUATION]]`、`[[SUMMARY]]` 等标记不可删除
+3. **输出格式关键字**:代码解析依赖的关键字需要保持一致
+4. **使用辅助函数**:优先使用 `build_*()` 函数而非直接 `.format()`
+"""
+
+# ============================================================
+# 系统级 Prompt
+# ============================================================
+
+DEFAULT_SYSTEM_PREFIX = "你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。"
+
+
+# ============================================================
+# 研究流程 Prompt - 调研阶段
+# ============================================================
+
+RESEARCH_STAGE_PROMPT_TEMPLATE = """## 📚 研究流程 - 执行调研
+
+现有信息不足,需要进行调研。
+
+{research_skill_content}
+
+**重要提示**:
+- 调研完成后,请使用 `save_knowledge` 工具保存调研结果
+- 系统会自动检测到 save_knowledge 调用,并进入下一阶段(计划)
+"""
+
+
+# ============================================================
+# 研究流程 Prompt - 计划阶段
+# ============================================================
+
+PLANNING_STAGE_PROMPT = """## 📋 研究流程 - 制定计划
+
+调研已完成(或无需调研),现在请制定执行计划。
+
+**请立即执行以下操作**:
+1. 使用 `goal` 工具创建目标树
+2. 将任务分解为可执行的子目标
+3. 为每个子目标设置合理的优先级
+
+注意:这是强制步骤,必须创建 goal tree 才能进入执行阶段。
+"""
+
+
+# ============================================================
+# 研究流程 Prompt - 调研决策阶段
+# ============================================================
+
+RESEARCH_DECISION_GUIDE_TEMPLATE = """---
+
+## 🤔 调研决策
+
+{experience_summary}
+
+### 决策指南
+
+**当前状态**:系统已自动检索知识库和经验库,相关内容已注入到上方的 GoalTree 中(查看 Current Goal 下的「📚 相关知识」部分)。
+
+**请根据已注入的知识和经验,选择下一步行动**:
+
+**选项 1: 知识充足,直接制定计划**
+- 如果上方显示的知识和经验已经足够完成任务
+- 直接使用 `goal` 工具制定执行计划
+
+**选项 2: 知识不足,需要调研** ⭐
+- 如果上方没有显示相关知识,或现有知识不足以完成任务
+- **立即调用 `agent` 工具启动调研子任务**:
+
+```python
+agent(
+    task=\"\"\"针对任务「{task_desc}」进行深入调研:
+
+1. 使用 web_search 工具搜索相关技术文档、教程、最佳实践
+2. 搜索关键词建议:
+   - 核心技术名称 + "教程"
+   - 核心技术名称 + "最佳实践"
+   - 核心技术名称 + "示例代码"
+3. 使用 read_file 工具查看项目中的相关文件
+4. 对每条有价值的信息,使用 save_knowledge 工具保存,标签类型选择:
+   - tool: 工具使用方法
+   - definition: 概念定义
+   - usercase: 使用案例
+   - strategy: 策略经验
+
+调研完成后,系统会自动进入计划阶段。
+\"\"\",
+    skills=["research"]  # 注入调研指南
+)
+```
+
+**重要提示**:
+- 如果 GoalTree 中没有显示「📚 相关知识」,说明知识库为空,必须先调研
+- 调研应该简洁高效,最多设立两个 goal
+"""
+
+
+# ============================================================
+# 压缩相关 Prompt - Level 2 压缩与经验评估
+# ============================================================
+
+COMPRESSION_EVAL_PROMPT_TEMPLATE = """请对以上对话历史进行压缩总结,并评价所引用的历史知识/经验。
+### 任务 1:评价已用知识
+本次任务参考了以下知识内容:{ex_reference_list}
+
+请对比"知识建议"与"实际执行轨迹",给出三色打分:
+[[EVALUATION]]
+ID: knowledge-xxx 或 research-xxx | Result: helpful/harmful/mixed | Reason: [优点]... [局限/修正]...
+
+### 任务 2:对话历史摘要
+要求:
+1. 保留关键决策、结论和产出(如创建的文件、修改的代码、得出的分析结论)
+2. 保留重要的上下文(如用户的要求、约束条件、之前的讨论结果)
+3. 省略中间探索过程、重复的工具调用细节
+4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
+5. 控制在 2000 字以内
+格式要求:
+[[SUMMARY]]
+(此处填写结构化的摘要内容)
+
+当前 GoalTree 状态:
+{goal_tree_prompt}
+"""
+
+
+# ============================================================
+# 压缩相关 Prompt - 经验反思
+# ============================================================
+
+REFLECT_PROMPT = """请回顾以上整个执行过程,提取有价值的经验教训。
+你必须将经验与当前的任务意图(Intent)和环境状态(State)挂钩,以便未来精准检索。
+关注以下方面:
+1. 人工干预:用户中途的指令是否说明了原来的执行过程哪里有问题
+2. 弯路:哪些尝试是不必要的,有没有更直接的方法
+3. 好的决策:哪些判断和选择是正确的,值得记住
+4. 工具使用:哪些工具用法是高效的,哪些可以改进
+
+输出格式(严格遵守):
+- 在每条经验前加一个[]中添加自定义的标签,标签要求总结实际的内容为若干词语,包括:
+    - intent: 当前的goal
+    - state: 环境状态(如果与工具相关,可以在标签中加入工具的名称)
+- 经验标签可用自然语言描述
+- 每条经验单独成段,格式固定为:- 当 [条件] 时,应该 [动作](原因:[一句话说明])。具体案例:[案例]
+- 条目之间用一个空行分隔
+- 不输出任何标题、分类、编号、分隔线或其他结构
+- 不使用 markdown 加粗、表格、代码块等格式
+- 每条经验自包含,读者无需上下文即可理解
+- 只提取最有价值的 5-10 条,宁少勿滥
+
+示例(仅供参考格式,不要复制内容):
+- [intent:示例生成 state:用户提醒,指定样本] 当用户说"给我示例"时,应该用真实数据而不是编造(原因:编造的示例无法验证质量)。具体案例:training_samples.json 中的示例全是 LLM 自己编造的,用户明确要求"基于我指定的样本"。
+"""
+
+
+# ============================================================
+# 压缩相关 Prompt - 压缩后的摘要头部
+# ============================================================
+
+SUMMARY_HEADER_TEMPLATE = """## 对话历史摘要(自动压缩)
+
+{summary_text}
+
+---
+请基于以上摘要和当前 GoalTree 继续执行任务。"""
+
+
+# ============================================================
+# 工具执行 Prompt - 响应截断提示
+# ============================================================
+
+TRUNCATION_HINT = """你的响应因为 max_tokens 限制被截断,tool call 参数不完整,未执行。请将大内容拆分为多次小的工具调用(例如用 write_file 的 append 模式分批写入)。"""
+
+
+# ============================================================
+# 工具执行 Prompt - 工具执行中断提示
+# ============================================================
+
+TOOL_INTERRUPTED_MESSAGE = """⚠️ 工具 {tool_name} 执行被中断(进程异常退出),未获得执行结果。请根据需要重新调用。"""
+
+AGENT_INTERRUPTED_SUMMARY = "⚠️ 子Agent执行被中断(进程异常退出)"
+
+AGENT_CONTINUE_HINT_TEMPLATE = '使用 continue_from="{sub_trace_id}" 可继续执行,保留已有进度'
+
+
+# ============================================================
+# 任务生成 Prompt
+# ============================================================
+
+TASK_NAME_GENERATION_SYSTEM_PROMPT = "用中文为以下任务生成一个简短标题(10-30字),只输出标题本身:"
+
+TASK_NAME_FALLBACK = "未命名任务"
+
+
+# ============================================================
+# 经验保存 Prompt - 经验条目格式
+# ============================================================
+
+EXPERIENCE_ENTRY_TEMPLATE = """---
+id: {ex_id}
+trace_id: {trace_id}
+tags: {{intent: {intents}, state: {states}}}
+metrics: {{helpful: 1, harmful: 0}}
+created_at: {created_at}
+---
+{content}
+"""
+
+
+# ============================================================
+# 经验检索 Prompt - 经验摘要格式
+# ============================================================
+
+EXPERIENCE_SUMMARY_WITH_RESULTS = "✅ 已自动检索到 {count} 条相关经验(见上方 GoalTree 中的「📚 相关知识」)\n"
+
+EXPERIENCE_SUMMARY_NO_RESULTS = "❌ 未找到相关经验\n"
+
+
+# ============================================================
+# 经验评估 Prompt - 格式解析警告
+# ============================================================
+
+EXPERIENCE_PARSE_WARNING = "未能解析出符合格式的经验条目,请检查 REFLECT_PROMPT。"
+
+
+# ============================================================
+# 辅助函数:构建特定场景的 Prompt
+# ============================================================
+
+def build_research_stage_prompt(research_skill_content: str) -> str:
+    """构建调研阶段的引导 prompt"""
+    return RESEARCH_STAGE_PROMPT_TEMPLATE.format(
+        research_skill_content=research_skill_content
+    )
+
+
+def build_research_decision_guide(
+    experience_results: list,
+    task_desc: str
+) -> str:
+    """构建调研决策阶段的引导消息"""
+    if experience_results:
+        experience_summary = EXPERIENCE_SUMMARY_WITH_RESULTS.format(
+            count=len(experience_results)
+        )
+    else:
+        experience_summary = EXPERIENCE_SUMMARY_NO_RESULTS
+
+    # 截取任务描述前100字符
+    task_desc_short = task_desc[:100] if len(task_desc) > 100 else task_desc
+
+    return RESEARCH_DECISION_GUIDE_TEMPLATE.format(
+        experience_summary=experience_summary,
+        task_desc=task_desc_short
+    )
+
+
+def build_compression_eval_prompt(
+    goal_tree_prompt: str,
+    ex_reference_list: str
+) -> str:
+    """构建 Level 2 压缩 prompt(含经验评估)"""
+    return COMPRESSION_EVAL_PROMPT_TEMPLATE.format(
+        goal_tree_prompt=goal_tree_prompt,
+        ex_reference_list=ex_reference_list
+    )
+
+
+def build_summary_header(summary_text: str) -> str:
+    """构建压缩后的摘要头部"""
+    return SUMMARY_HEADER_TEMPLATE.format(summary_text=summary_text)
+
+
+def build_tool_interrupted_message(tool_name: str) -> str:
+    """构建工具中断消息"""
+    return TOOL_INTERRUPTED_MESSAGE.format(tool_name=tool_name)
+
+
+def build_agent_continue_hint(sub_trace_id: str) -> str:
+    """构建 Agent 继续执行提示"""
+    return AGENT_CONTINUE_HINT_TEMPLATE.format(sub_trace_id=sub_trace_id)
+
+
+def build_experience_entry(
+    ex_id: str,
+    trace_id: str,
+    intents: list,
+    states: list,
+    created_at: str,
+    content: str
+) -> str:
+    """构建经验条目"""
+    return EXPERIENCE_ENTRY_TEMPLATE.format(
+        ex_id=ex_id,
+        trace_id=trace_id,
+        intents=intents,
+        states=states,
+        created_at=created_at,
+        content=content
+    )
+
+# ============================================================
+# 子 Agent 相关 Prompt - 评估任务
+# ============================================================
+
+EVALUATE_PROMPT_TEMPLATE = """# 评估任务
+
+请评估以下任务的执行结果是否满足要求。
+
+## 目标描述
+
+{goal_description}
+
+## 执行结果
+
+{result_text}
+
+## 输出格式
+
+## 评估结论
+[通过/不通过]
+
+## 评估理由
+[详细说明通过或不通过原因]
+
+## 修改建议(如果不通过)
+1. [建议1]
+2. [建议2]
+"""
+
+
+# ============================================================
+# 子 Agent 相关 Prompt - 结果格式化
+# ============================================================
+
+DELEGATE_RESULT_HEADER = "## 委托任务完成\n"
+
+DELEGATE_SAVED_KNOWLEDGE_HEADER = "**保存的知识** ({count} 条):"
+
+DELEGATE_STATS_HEADER = "**执行统计**:"
+
+EXPLORE_RESULT_HEADER = "## 探索结果\n"
+
+EXPLORE_BRANCH_TEMPLATE = "### 方案 {branch_name}: {task}"
+
+EXPLORE_STATUS_SUCCESS = "**状态**: ✓ 完成"
+
+EXPLORE_STATUS_FAILED = "**状态**: ✗ 失败"
+
+EXPLORE_STATUS_ERROR = "**状态**: ✗ 异常"
+
+EXPLORE_SUMMARY_HEADER = "## 总结"
+
+
+# ============================================================
+# 辅助函数:构建子 Agent 相关 Prompt
+# ============================================================
+
+def build_evaluate_prompt(goal_description: str, result_text: str) -> str:
+    """构建评估 prompt"""
+    return EVALUATE_PROMPT_TEMPLATE.format(
+        goal_description=goal_description,
+        result_text=result_text or "(无执行结果)"
+    )
+
+
+# ============================================================
+# 知识管理相关 Prompt - 语义路由
+# ============================================================
+
+KNOWLEDGE_SEMANTIC_ROUTE_PROMPT_TEMPLATE = """你是一个知识检索专家。根据用户的当前任务需求,从下列原子知识元数据中挑选出最相关的最多 {routing_k} 个知识 ID。
+任务需求:"{query_text}"
+
+可选知识列表:
+{routing_data}
+
+请直接输出 ID 列表,用逗号分隔(例如: knowledge-20260302-001, research-20260302-002)。若无相关项请输出 "None"。
+"""
+
+
+# ============================================================
+# 知识管理相关 Prompt - 知识进化重写
+# ============================================================
+
+KNOWLEDGE_EVOLVE_PROMPT_TEMPLATE = """你是一个 AI Agent 知识库管理员。请根据反馈建议,对现有的知识内容进行重写进化。
+
+【原知识内容】:
+{old_content}
+
+【实战反馈建议】:
+{feedback}
+
+【重写要求】:
+1. 融合知识:将反馈中的避坑指南、新参数或修正后的选择逻辑融入原知识,使其更具通用性和准确性。
+2. 保持结构:如果原内容有特定格式(如 Markdown、代码示例等),请保持该格式。
+3. 语言:简洁直接,使用中文。
+4. 禁止:严禁输出任何开场白、解释语或额外的 Markdown 标题,直接返回重写后的正文。
+"""
+
+
+# ============================================================
+# 知识管理相关 Prompt - 知识库瘦身
+# ============================================================
+
+KNOWLEDGE_SLIM_PROMPT_TEMPLATE = """你是一个 AI Agent 知识库管理员。以下是当前知识库的全部条目,请执行瘦身操作:
+
+【任务】:
+1. 识别语义高度相似或重复的知识,将它们合并为一条更精炼、更通用的知识。
+2. 合并时保留 helpful 最高的那条的 ID 和 metrics(metrics 中 helpful/harmful 取各条之和)。
+3. 对于独立的、无重复的知识,保持原样不动。
+4. 保持原有的知识结构和格式。
+
+【当前知识库】:
+{entries_text}
+
+【输出格式要求】:
+严格按以下格式输出每条知识,条目之间用 === 分隔:
+ID: <保留的id>
+TAGS: <yaml格式的tags>
+METRICS: <yaml格式的metrics>
+SCORE: <评分>
+SCENARIO: <场景描述>
+CONTENT: <知识内容>
+===
+"""
+
+
+# ============================================================
+# 辅助函数:构建知识管理相关 Prompt
+# ============================================================
+
+def build_knowledge_semantic_route_prompt(
+    query_text: str,
+    routing_data: str,
+    routing_k: int
+) -> str:
+    """构建知识语义路由 prompt"""
+    return KNOWLEDGE_SEMANTIC_ROUTE_PROMPT_TEMPLATE.format(
+        query_text=query_text,
+        routing_data=routing_data,
+        routing_k=routing_k
+    )
+
+
+def build_knowledge_evolve_prompt(old_content: str, feedback: str) -> str:
+    """构建知识进化重写 prompt"""
+    return KNOWLEDGE_EVOLVE_PROMPT_TEMPLATE.format(
+        old_content=old_content,
+        feedback=feedback
+    )
+
+
+def build_knowledge_slim_prompt(entries_text: str) -> str:
+    """构建知识库瘦身 prompt"""
+    return KNOWLEDGE_SLIM_PROMPT_TEMPLATE.format(
+        entries_text=entries_text
+    )

+ 44 - 106
agent/core/runner.py

@@ -39,6 +39,29 @@ from agent.memory.models import Skill
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.skill_loader import load_skills_from_dir
 from agent.tools import ToolRegistry, get_tool_registry
+from agent.core.prompts import (
+    DEFAULT_SYSTEM_PREFIX,
+    RESEARCH_STAGE_PROMPT_TEMPLATE,
+    PLANNING_STAGE_PROMPT,
+    RESEARCH_DECISION_GUIDE_TEMPLATE,
+    TRUNCATION_HINT,
+    TOOL_INTERRUPTED_MESSAGE,
+    AGENT_INTERRUPTED_SUMMARY,
+    AGENT_CONTINUE_HINT_TEMPLATE,
+    TASK_NAME_GENERATION_SYSTEM_PROMPT,
+    TASK_NAME_FALLBACK,
+    EXPERIENCE_ENTRY_TEMPLATE,
+    EXPERIENCE_SUMMARY_WITH_RESULTS,
+    EXPERIENCE_SUMMARY_NO_RESULTS,
+    EXPERIENCE_PARSE_WARNING,
+    SUMMARY_HEADER_TEMPLATE,
+    build_research_stage_prompt,
+    build_research_decision_guide,
+    build_summary_header,
+    build_tool_interrupted_message,
+    build_agent_continue_hint,
+    build_experience_entry,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -741,31 +764,10 @@ class AgentRunner:
                 logger.warning(f"无法读取 research.md: {e}")
                 research_skill_content = "(无法加载 research.md 内容)"
 
-            return f"""
-## 📚 研究流程 - 执行调研
-
-现有信息不足,需要进行调研。
-
-{research_skill_content}
-
-**重要提示**:
-- 调研完成后,请使用 `save_knowledge` 工具保存调研结果
-- 系统会自动检测到 save_knowledge 调用,并进入下一阶段(计划)
-"""
+            return build_research_stage_prompt(research_skill_content)
 
         elif stage == "planning":
-            return f"""
-## 📋 研究流程 - 制定计划
-
-调研已完成(或无需调研),现在请制定执行计划。
-
-**请立即执行以下操作**:
-1. 使用 `goal` 工具创建目标树
-2. 将任务分解为可执行的子目标
-3. 为每个子目标设置合理的优先级
-
-注意:这是强制步骤,必须创建 goal tree 才能进入执行阶段。
-"""
+            return PLANNING_STAGE_PROMPT
 
         # research_decision 阶段的引导消息已移到 _build_research_decision_guide
         return ""
@@ -775,60 +777,8 @@ class AgentRunner:
         experience_results = research_state.get("experience_results", [])
         task_desc = research_state.get("task_desc", "")
 
-        # 构建经验摘要
-        experience_summary = ""
-        if experience_results:
-            experience_summary = f"✅ 已自动检索到 {len(experience_results)} 条相关经验(见上方 GoalTree 中的「📚 相关知识」)\n"
-        else:
-            experience_summary = "❌ 未找到相关经验\n"
-
-        return f"""
----
-
-## 🤔 调研决策
-
-{experience_summary}
-
-### 决策指南
-
-**当前状态**:系统已自动检索知识库和经验库,相关内容已注入到上方的 GoalTree 中(查看 Current Goal 下的「📚 相关知识」部分)。
+        return build_research_decision_guide(experience_results, task_desc)
 
-**请根据已注入的知识和经验,选择下一步行动**:
-
-**选项 1: 知识充足,直接制定计划**
-- 如果上方显示的知识和经验已经足够完成任务
-- 直接使用 `goal` 工具制定执行计划
-
-**选项 2: 知识不足,需要调研** ⭐
-- 如果上方没有显示相关知识,或现有知识不足以完成任务
-- **立即调用 `agent` 工具启动调研子任务**:
-
-```python
-agent(
-    task=\"\"\"针对任务「{task_desc[:100]}」进行深入调研:
-
-1. 使用 web_search 工具搜索相关技术文档、教程、最佳实践
-2. 搜索关键词建议:
-   - 核心技术名称 + "教程"
-   - 核心技术名称 + "最佳实践"
-   - 核心技术名称 + "示例代码"
-3. 使用 read_file 工具查看项目中的相关文件
-4. 对每条有价值的信息,使用 save_knowledge 工具保存,标签类型选择:
-   - tool: 工具使用方法
-   - definition: 概念定义
-   - usercase: 使用案例
-   - strategy: 策略经验
-
-调研完成后,系统会自动进入计划阶段。
-\"\"\",
-    skills=["research"]  # 注入调研指南
-)
-```
-
-**重要提示**:
-- 如果 GoalTree 中没有显示「📚 相关知识」,说明知识库为空,必须先调研
-- 调研应该简洁高效,最多设立两个 goal
-"""
 
     async def _handle_research_flow_transition(
         self,
@@ -1284,10 +1234,7 @@ agent(
                     "[Runner] 响应被 max_tokens 截断,跳过 %d 个不完整的 tool calls",
                     len(tool_calls),
                 )
-                truncation_hint = (
-                    "你的响应因为 max_tokens 限制被截断,tool call 参数不完整,未执行。"
-                    "请将大内容拆分为多次小的工具调用(例如用 write_file 的 append 模式分批写入)。"
-                )
+                truncation_hint = TRUNCATION_HINT
                 history.append({
                     "role": "assistant",
                     "content": response_content,
@@ -1541,15 +1488,14 @@ agent(
                     states = [s.strip() for s in state_match.group(1).split(",")] if state_match and state_match.group(1) else []
 
                     ex_id = f"ex_{datetime.now().strftime('%m%d%H%M')}_{_uuid2.uuid4().hex[:4]}"
-                    entry = f"""---
-id: {ex_id}
-trace_id: {trace_id}
-tags: {{intent: {intents}, state: {states}}}
-metrics: {{helpful: 1, harmful: 0}}
-created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
----
-- {content}
-- 经验ID: [{ex_id}]"""
+                    entry = build_experience_entry(
+                        ex_id=ex_id,
+                        trace_id=trace_id,
+                        intents=intents,
+                        states=states,
+                        created_at=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+                        content=f"- {content}\n- 经验ID: [{ex_id}]"
+                    )
                     structured_entries.append(entry)
 
                 if structured_entries:
@@ -1602,7 +1548,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
 
                     logger.info(f"已提取并保存 {saved_count}/{len(structured_entries)} 条结构化经验到知识库")
                 else:
-                    logger.warning("未能解析出符合格式的经验条目,请检查 REFLECT_PROMPT。")
+                    logger.warning(EXPERIENCE_PARSE_WARNING)
                     logger.debug(f"LLM Raw Output:\n{reflection_text}")
             else:
                 logger.warning("LLM 未生成反思内容")
@@ -1673,10 +1619,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
             return history, head_seq, sequence
 
         # --- Step 3: 存储 summary 消息 ---
-        summary_with_header = (
-            f"## 对话历史摘要(自动压缩)\n\n{summary_text}\n\n"
-            "---\n请基于以上摘要和当前 GoalTree 继续执行任务。"
-        )
+        summary_with_header = build_summary_header(summary_text)
 
         summary_msg = Message.create(
             trace_id=trace_id,
@@ -1868,10 +1811,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
                     tc, goal_tree, assistant_msg,
                 )
             else:
-                result_text = (
-                    f"⚠️ 工具 {tool_name} 执行被中断(进程异常退出),"
-                    "未获得执行结果。请根据需要重新调用。"
-                )
+                result_text = build_tool_interrupted_message(tool_name)
 
             synthetic_msg = Message.create(
                 trace_id=trace_id,
@@ -1943,14 +1883,12 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
         result: Dict[str, Any] = {
             "mode": mode,
             "status": "interrupted",
-            "summary": "⚠️ 子Agent执行被中断(进程异常退出)",
+            "summary": AGENT_INTERRUPTED_SUMMARY,
             "task": task,
         }
         if sub_trace_id:
             result["sub_trace_id"] = sub_trace_id
-            result["hint"] = (
-                f'使用 continue_from="{sub_trace_id}" 可继续执行,保留已有进度'
-            )
+            result["hint"] = build_agent_continue_hint(sub_trace_id)
         if stats:
             result["stats"] = stats
 
@@ -2147,7 +2085,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
         return self.tools.get_schemas(tool_names)
 
     # 默认 system prompt 前缀(当 config.system_prompt 和前端都未提供 system message 时使用)
-    DEFAULT_SYSTEM_PREFIX = "你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。"
+    # 注意:此常量已迁移到 agent.core.prompts,这里保留引用以保持向后兼容
 
     async def _build_system_prompt(self, config: RunConfig, base_prompt: Optional[str] = None) -> Optional[str]:
         """构建 system prompt(注入 skills)
@@ -2185,7 +2123,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
             if skills_text:
                 system_prompt += f"\n\n## Skills\n{skills_text}"
         else:
-            system_prompt = self.DEFAULT_SYSTEM_PREFIX
+            system_prompt = DEFAULT_SYSTEM_PREFIX
             if skills_text:
                 system_prompt += f"\n\n## Skills\n{skills_text}"
 
@@ -2206,14 +2144,14 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
         raw_text = " ".join(text_parts).strip()
 
         if not raw_text:
-            return "未命名任务"
+            return TASK_NAME_FALLBACK
 
         # 尝试使用 utility_llm 生成标题
         if self.utility_llm_call:
             try:
                 result = await self.utility_llm_call(
                     messages=[
-                        {"role": "system", "content": "用中文为以下任务生成一个简短标题(10-30字),只输出标题本身:"},
+                        {"role": "system", "content": TASK_NAME_GENERATION_SYSTEM_PROMPT},
                         {"role": "user", "content": raw_text[:2000]},
                     ],
                     model="gpt-4o-mini",  # 使用便宜模型

+ 0 - 2
agent/tools/builtin/__init__.py

@@ -15,7 +15,6 @@ from agent.tools.builtin.file.grep import grep_content
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.subagent import agent, evaluate
-from agent.tools.builtin.experience import get_experience
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
@@ -36,7 +35,6 @@ __all__ = [
     # 系统工具
     "bash_command",
     "skill",
-    "get_experience",
     "search_knowledge",
     "save_knowledge",
     "list_knowledge",

+ 20 - 57
agent/tools/builtin/knowledge.py

@@ -14,6 +14,14 @@ from pathlib import Path
 from typing import List, Dict, Optional, Any
 from agent.tools import tool, ToolResult, ToolContext
 from ...llm.openrouter import openrouter_llm_call
+from agent.core.prompts import (
+    KNOWLEDGE_SEMANTIC_ROUTE_PROMPT_TEMPLATE,
+    KNOWLEDGE_EVOLVE_PROMPT_TEMPLATE,
+    KNOWLEDGE_SLIM_PROMPT_TEMPLATE,
+    build_knowledge_semantic_route_prompt,
+    build_knowledge_evolve_prompt,
+    build_knowledge_slim_prompt,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -332,15 +340,11 @@ async def _route_knowledge_by_llm(query_text: str, metadata_list: List[Dict], k:
         } for m in metadata_list
     ]
 
-    prompt = f"""
-你是一个知识检索专家。根据用户的当前任务需求,从下列原子知识元数据中挑选出最相关的最多 {routing_k} 个知识 ID。
-任务需求:"{query_text}"
-
-可选知识列表:
-{json.dumps(routing_data, ensure_ascii=False, indent=1)}
-
-请直接输出 ID 列表,用逗号分隔(例如: knowledge-20260302-001, research-20260302-002)。若无相关项请输出 "None"。
-"""
+    prompt = build_knowledge_semantic_route_prompt(
+        query_text=query_text,
+        routing_data=json.dumps(routing_data, ensure_ascii=False, indent=1),
+        routing_k=routing_k
+    )
 
     try:
         print(f"\n[Step 1: 知识语义路由] 任务: '{query_text}' | 候选总数: {len(metadata_list)} | 目标提取数: {routing_k}")
@@ -364,20 +368,7 @@ async def _evolve_knowledge_with_llm(old_content: str, feedback: str) -> str:
     """
     使用 LLM 进行知识进化重写(类似经验进化机制)
     """
-    prompt = f"""你是一个 AI Agent 知识库管理员。请根据反馈建议,对现有的知识内容进行重写进化。
-
-【原知识内容】:
-{old_content}
-
-【实战反馈建议】:
-{feedback}
-
-【重写要求】:
-1. 融合知识:将反馈中的避坑指南、新参数或修正后的选择逻辑融入原知识,使其更具通用性和准确性。
-2. 保持结构:如果原内容有特定格式(如 Markdown、代码示例等),请保持该格式。
-3. 语言:简洁直接,使用中文。
-4. 禁止:严禁输出任何开场白、解释语或额外的 Markdown 标题,直接返回重写后的正文。
-"""
+    prompt = build_knowledge_evolve_prompt(old_content, feedback)
     try:
         response = await openrouter_llm_call(
             messages=[{"role": "user", "content": prompt}],
@@ -417,15 +408,11 @@ async def _route_knowledge_by_llm(query_text: str, metadata_list: List[Dict], k:
         } for m in metadata_list
     ]
 
-    prompt = f"""
-你是一个知识检索专家。根据用户的当前任务需求,从下列原子知识元数据中挑选出最相关的最多 {routing_k} 个知识 ID。
-任务需求:"{query_text}"
-
-可选知识列表:
-{json.dumps(routing_data, ensure_ascii=False, indent=1)}
-
-请直接输出 ID 列表,用逗号分隔(例如: knowledge-20260302-001, research-20260302-002)。若无相关项请输出 "None"。
-"""
+    prompt = build_knowledge_semantic_route_prompt(
+        query_text=query_text,
+        routing_data=json.dumps(routing_data, ensure_ascii=False, indent=1),
+        routing_k=routing_k
+    )
 
     try:
         print(f"\n[Step 1: 知识语义路由] 任务: '{query_text}' | 候选总数: {len(metadata_list)} | 目标提取数: {routing_k}")
@@ -1038,31 +1025,7 @@ async def slim_knowledge(
             entries_text += f"Scenario: {data.get('scenario', 'N/A')}\n"
             entries_text += f"Content: {data.get('content', '')[:200]}...\n\n"
 
-        prompt = f"""你是一个 AI Agent 知识库管理员。以下是当前知识库的全部条目,请执行瘦身操作:
-
-【任务】:
-1. 识别语义高度相似或重复的知识,将它们合并为一条更精炼、更通用的知识。
-2. 合并时保留 helpful 最高的那条的 ID 和 metrics(metrics 中 helpful/harmful 取各条之和)。
-3. 对于独立的、无重复的知识,保持原样不动。
-4. 保持原有的知识结构和格式。
-
-【当前知识库】:
-{entries_text}
-
-【输出格式要求】:
-严格按以下格式输出每条知识,条目之间用 === 分隔:
-ID: <保留的id>
-TAGS: <yaml格式的tags>
-METRICS: <yaml格式的metrics>
-SCORE: <评分>
-SCENARIO: <场景描述>
-CONTENT: <合并后的知识内容>
-===
-
-最后一行输出合并报告,格式:
-REPORT: 原有 X 条,合并后 Y 条,精简了 Z 条。
-
-禁止输出任何开场白或解释。"""
+        prompt = build_knowledge_slim_prompt(entries_text)
 
         print(f"\n[知识瘦身] 正在调用 {model} 分析 {len(parsed)} 条知识...")
         response = await openrouter_llm_call(

+ 23 - 35
agent/tools/builtin/subagent.py

@@ -14,6 +14,19 @@ from agent.trace.models import Trace, Messages
 from agent.trace.trace_id import generate_sub_trace_id
 from agent.trace.goal_models import GoalTree
 from agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
+from agent.core.prompts import (
+    EVALUATE_PROMPT_TEMPLATE,
+    DELEGATE_RESULT_HEADER,
+    DELEGATE_SAVED_KNOWLEDGE_HEADER,
+    DELEGATE_STATS_HEADER,
+    EXPLORE_RESULT_HEADER,
+    EXPLORE_BRANCH_TEMPLATE,
+    EXPLORE_STATUS_SUCCESS,
+    EXPLORE_STATUS_FAILED,
+    EXPLORE_STATUS_ERROR,
+    EXPLORE_SUMMARY_HEADER,
+    build_evaluate_prompt,
+)
 
 
 def _make_run_config(**kwargs):
@@ -128,7 +141,7 @@ def _get_allowed_tools(single: bool, context: dict) -> Optional[List[str]]:
 
 def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
     """格式化单任务(delegate)结果"""
-    lines = ["## 委托任务完成\n"]
+    lines = [DELEGATE_RESULT_HEADER]
     summary = result.get("summary", "")
     if summary:
         lines.append(summary)
@@ -138,13 +151,13 @@ def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued:
     saved_knowledge_ids = result.get("saved_knowledge_ids", [])
     if saved_knowledge_ids:
         lines.append("---\n")
-        lines.append(f"**保存的知识** ({len(saved_knowledge_ids)} 条):")
+        lines.append(DELEGATE_SAVED_KNOWLEDGE_HEADER.format(count=len(saved_knowledge_ids)))
         for kid in saved_knowledge_ids:
             lines.append(f"- {kid}")
         lines.append("")
 
     lines.append("---\n")
-    lines.append("**执行统计**:")
+    lines.append(DELEGATE_STATS_HEADER)
     stats = result.get("stats", {})
     if stats:
         lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
@@ -166,7 +179,7 @@ def _format_multi_result(
     tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
 ) -> Dict[str, Any]:
     """格式化多任务(explore)聚合结果"""
-    lines = ["## 探索结果\n"]
+    lines = [EXPLORE_RESULT_HEADER]
     successful = 0
     failed = 0
     total_tokens = 0
@@ -174,15 +187,15 @@ def _format_multi_result(
 
     for i, (task_item, result) in enumerate(zip(tasks, results)):
         branch_name = chr(ord('A') + i)
-        lines.append(f"### 方案 {branch_name}: {task_item}")
+        lines.append(EXPLORE_BRANCH_TEMPLATE.format(branch_name=branch_name, task=task_item))
 
         if isinstance(result, dict):
             status = result.get("status", "unknown")
             if status == "completed":
-                lines.append("**状态**: ✓ 完成")
+                lines.append(EXPLORE_STATUS_SUCCESS)
                 successful += 1
             else:
-                lines.append("**状态**: ✗ 失败")
+                lines.append(EXPLORE_STATUS_FAILED)
                 failed += 1
 
             summary = result.get("summary", "")
@@ -198,13 +211,13 @@ def _format_multi_result(
                 total_tokens += tokens
                 total_cost += cost
         else:
-            lines.append("**状态**: ✗ 异常")
+            lines.append(EXPLORE_STATUS_ERROR)
             failed += 1
 
         lines.append("")
 
     lines.append("---\n")
-    lines.append("## 总结")
+    lines.append(EXPLORE_SUMMARY_HEADER)
     lines.append(f"- 总分支数: {len(tasks)}")
     lines.append(f"- 成功: {successful}")
     lines.append(f"- 失败: {failed}")
@@ -259,32 +272,7 @@ def _build_evaluate_prompt(goal_description: str, messages: Optional[Messages])
                         parts.append(item.get("text", ""))
         result_text = "\n".join(parts)
 
-    lines = [
-        "# 评估任务",
-        "",
-        "请评估以下任务的执行结果是否满足要求。",
-        "",
-        "## 目标描述",
-        "",
-        goal_description,
-        "",
-        "## 执行结果",
-        "",
-        result_text or "(无执行结果)",
-        "",
-        "## 输出格式",
-        "",
-        "## 评估结论",
-        "[通过/不通过]",
-        "",
-        "## 评估理由",
-        "[详细说明通过或不通过原因]",
-        "",
-        "## 修改建议(如果不通过)",
-        "1. [建议1]",
-        "2. [建议2]",
-    ]
-    return "\n".join(lines)
+    return build_evaluate_prompt(goal_description, result_text)
 
 
 def _make_event_printer(label: str):

+ 9 - 48
agent/trace/compaction.py

@@ -18,6 +18,11 @@ from typing import List, Dict, Any, Optional, Set
 
 from .goal_models import GoalTree
 from .models import Message
+from agent.core.prompts import (
+    COMPRESSION_EVAL_PROMPT_TEMPLATE,
+    REFLECT_PROMPT,
+    build_compression_eval_prompt,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -297,53 +302,8 @@ def needs_level2_compression(
 
 
 # ===== Level 2: 压缩 Prompt =====
-
-COMPRESSION_EVAL_PROMPT = """请对以上对话历史进行压缩总结,并评价所引用的历史知识/经验。
-### 任务 1:评价已用知识
-本次任务参考了以下知识内容:{ex_reference_list}
-
-请对比”知识建议”与”实际执行轨迹”,给出三色打分:
-[[EVALUATION]]
-ID: knowledge-xxx 或 research-xxx | Result: helpful/harmful/mixed | Reason: [优点]... [局限/修正]...
-
-### 任务 2:对话历史摘要
-要求:
-1. 保留关键决策、结论和产出(如创建的文件、修改的代码、得出的分析结论)
-2. 保留重要的上下文(如用户的要求、约束条件、之前的讨论结果)
-3. 省略中间探索过程、重复的工具调用细节
-4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
-5. 控制在 2000 字以内
-格式要求:
-[[SUMMARY]]
-(此处填写结构化的摘要内容)
-
-当前 GoalTree 状态:
-{goal_tree_prompt}
-"""
-
-REFLECT_PROMPT = """请回顾以上整个执行过程,提取有价值的经验教训。
-你必须将经验与当前的任务意图(Intent)和环境状态(State)挂钩,以便未来精准检索。
-关注以下方面:
-1. 人工干预:用户中途的指令是否说明了原来的执行过程哪里有问题
-2. 弯路:哪些尝试是不必要的,有没有更直接的方法
-3. 好的决策:哪些判断和选择是正确的,值得记住
-4. 工具使用:哪些工具用法是高效的,哪些可以改进
-
-输出格式(严格遵守):
-- 在每条经验前加一个[]中添加自定义的标签,标签要求总结实际的内容为若干词语,包括:
-    - intent: 当前的goal
-    - state: 环境状态(如果与工具相关,可以在标签中加入工具的名称)
-- 经验标签可用自然语言描述
-- 每条经验单独成段,格式固定为:- 当 [条件] 时,应该 [动作](原因:[一句话说明])。具体案例:[案例]
-- 条目之间用一个空行分隔
-- 不输出任何标题、分类、编号、分隔线或其他结构
-- 不使用 markdown 加粗、表格、代码块等格式
-- 每条经验自包含,读者无需上下文即可理解
-- 只提取最有价值的 5-10 条,宁少勿滥
-
-示例(仅供参考格式,不要复制内容):
-- [intent:示例生成 state:用户提醒,指定样本] 当用户说"给我示例"时,应该用真实数据而不是编造(原因:编造的示例无法验证质量)。具体案例:training_samples.json 中的示例全是 LLM 自己编造的,用户明确要求"基于我指定的样本"。
-"""
+# 注意:这些 prompt 已迁移到 agent.core.prompts
+# COMPRESSION_EVAL_PROMPT 和 REFLECT_PROMPT 现在从 prompts.py 导入
 
 
 def build_compression_prompt(goal_tree: Optional[GoalTree], used_ex_ids: Optional[List[str]] = None) -> str:
@@ -356,7 +316,7 @@ def build_compression_prompt(goal_tree: Optional[GoalTree], used_ex_ids: Optiona
     if used_ex_ids:
         ex_reference = ", ".join(used_ex_ids)
 
-    return COMPRESSION_EVAL_PROMPT.format(
+    return build_compression_eval_prompt(
         goal_tree_prompt=goal_prompt,
         ex_reference_list=ex_reference,
     )
@@ -365,3 +325,4 @@ def build_compression_prompt(goal_tree: Optional[GoalTree], used_ex_ids: Optiona
 def build_reflect_prompt() -> str:
     """构建反思 prompt"""
     return REFLECT_PROMPT
+

+ 1 - 0
examples/restore/run.py

@@ -42,6 +42,7 @@ from agent.llm import create_openrouter_llm_call
 from agent.tools import get_tool_registry
 
 
+os.environ.setdefault("no_proxy", "*")
 # ===== 非阻塞 stdin 检测 =====
 if sys.platform == 'win32':
     import msvcrt

+ 16 - 8
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -820,18 +820,27 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
   const handleNodeClick = useCallback(
     (node: LayoutNode) => {
       if (node.type === "goal") {
-        // 检查是否是子节点 (subgoal)
-        if (node.parentId && onSubTraceClick) {
+        const goalData = node.data as Goal;
+
+        // 只有具有 sub_trace_ids 的子目标节点(agent 委托执行)才触发 trace 切换
+        // 普通的 sub_goal 节点(蓝色节点)没有 sub_trace_ids,应该打开 DetailPanel
+        const hasSubTraces = goalData.sub_trace_ids && goalData.sub_trace_ids.length > 0;
+
+        if (node.parentId && onSubTraceClick && hasSubTraces) {
           const parentNode = layoutData.nodes.find((n) => n.id === node.parentId);
           if (parentNode && parentNode.type === "goal") {
-            // 如果是子节点,触发 onSubTraceClick
-            const entry: SubTraceEntry = { id: (node.data as Goal).id };
+            // 取第一个 sub_trace_id 作为跳转目标(使用 trace_id,而非 goal.id)
+            const firstEntry = goalData.sub_trace_ids![0];
+            const entry: SubTraceEntry =
+              typeof firstEntry === "string"
+                ? { id: firstEntry }
+                : { id: firstEntry.trace_id, mission: firstEntry.mission };
             onSubTraceClick(parentNode.data as Goal, entry);
             return;
           }
         }
 
-        // 主链节点,触发 onNodeClick
+        // 主链节点 或 没有 sub_trace_ids 的普通子目标节点 → 打开 DetailPanel
         setSelectedNodeId(node.id);
         onNodeClick?.(node.data as Goal);
       } else if (node.type === "message") {
@@ -960,9 +969,8 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                     {/* 折叠状态提示徽章 */}
                     {edge.collapsed && (
                       <g
-                        transform={`translate(${(edge.source.x + edge.target.x) / 2},${
-                          (edge.source.y + edge.target.y) / 2
-                        })`}
+                        transform={`translate(${(edge.source.x + edge.target.x) / 2},${(edge.source.y + edge.target.y) / 2
+                          })`}
                         onClick={(e) => {
                           e.stopPropagation();
                           toggleCollapse(edge.id);