guantao hace 6 días
padre
commit
dd4cb465d4

+ 203 - 122
agent/core/runner.py

@@ -26,7 +26,7 @@ from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal,
 from agent.trace.models import Trace, Message
 from agent.trace.models import Trace, Message
 from agent.trace.protocols import TraceStore
 from agent.trace.protocols import TraceStore
 from agent.trace.goal_models import GoalTree
 from agent.trace.goal_models import GoalTree
-from agent.tools.builtin.experience import _get_structured_experiences, _batch_update_experiences
+from agent.tools.builtin.knowledge import _get_structured_knowledge, _batch_update_knowledge
 from agent.trace.compaction import (
 from agent.trace.compaction import (
     CompressionConfig,
     CompressionConfig,
     filter_by_goal_status,
     filter_by_goal_status,
@@ -751,70 +751,8 @@ class AgentRunner:
 **请立即执行以下操作**:
 **请立即执行以下操作**:
 1. 使用 `get_experience(query="{task_desc[:100]}", k=3)` 检索经验库
 1. 使用 `get_experience(query="{task_desc[:100]}", k=3)` 检索经验库
 2. 结合知识库和经验库的结果,评估是否需要进行调研
 2. 结合知识库和经验库的结果,评估是否需要进行调研
-3. 如果知识+经验足够,可以跳过调研直接进入计划阶段
-
+3. 如果经验和知识库不足时,必须调用子agent来执行调研
 注意:这是强制步骤,必须检索经验库。
 注意:这是强制步骤,必须检索经验库。
-"""
-
-        elif stage == "research_decision":
-            knowledge_results = research_state.get("knowledge_results", [])
-            experience_results = research_state.get("experience_results", [])
-
-            # 构建知识库摘要
-            knowledge_summary = "### 知识库检索结果\n\n"
-            if knowledge_results:
-                knowledge_summary += f"找到 {len(knowledge_results)} 条相关知识:\n\n"
-                for i, item in enumerate(knowledge_results[:5], 1):  # 最多显示 5 条
-                    tags = item.get("tags", [])
-                    summary = item.get("summary", "")
-                    knowledge_summary += f"{i}. [{', '.join(tags)}] {summary[:150]}...\n"
-                if len(knowledge_results) > 5:
-                    knowledge_summary += f"\n(还有 {len(knowledge_results) - 5} 条知识未显示)\n"
-            else:
-                knowledge_summary += "❌ 未找到相关知识\n"
-
-            # 构建经验库摘要
-            experience_summary = "\n### 经验库检索结果\n\n"
-            if experience_results:
-                experience_summary += f"找到 {len(experience_results)} 条相关经验:\n\n"
-                for i, item in enumerate(experience_results[:5], 1):  # 最多显示 5 条
-                    exp_id = item.get("id", "unknown")
-                    content = item.get("content", "")
-                    # 提取第一行作为摘要
-                    first_line = content.split('\n')[0] if content else ""
-                    experience_summary += f"{i}. [{exp_id}] {first_line[:150]}...\n"
-                if len(experience_results) > 5:
-                    experience_summary += f"\n(还有 {len(experience_results) - 5} 条经验未显示)\n"
-            else:
-                experience_summary += "❌ 未找到相关经验\n"
-
-            return f"""
-## 🤔 研究流程 - 阶段 3: 决定是否调研
-
-当前任务: {task_desc}
-
-{knowledge_summary}
-{experience_summary}
-
----
-
-**现在请你评估并明确回答以下问题**:
-
-1. **现有信息是否足够完成任务?**
-   - 如果知识库和经验库的信息已经足够,请回答:"无需调研,现有信息充足"
-   - 如果信息不足或缺少关键知识,请回答:"需要调研"
-
-2. **如果需要调研,请列出具体需要调研的问题**:
-   - 缺少哪些工具或方法的信息?
-   - 缺少哪些案例或最佳实践?
-   - 需要了解哪些定义或原理?
-
-**重要**:
-- 请在回复中明确包含"无需调研"或"需要调研"这几个字,以便系统自动判断下一步操作
-- **本阶段只需要回复判断结果,不要调用任何工具(包括 goal 工具)**
-- 系统会根据你的判断自动进入下一阶段(调研或计划)
-
-根据你的判断,系统会自动进入下一阶段(调研或计划)。
 """
 """
 
 
         elif stage == "research":
         elif stage == "research":
@@ -836,41 +774,100 @@ class AgentRunner:
 
 
 现有信息不足,需要进行调研。
 现有信息不足,需要进行调研。
 
 
----
-
 {research_skill_content}
 {research_skill_content}
 
 
----
-
-**请按照上述 research.md 中的方法执行调研**:
-
-1. **拆解搜索维度**:使用 `goal(add="...")` 创建调研子目标
-2. **多维度搜索**:使用 `search_posts` 搜索相关信息
-3. **结构化记录**:使用 `save_knowledge` 保存每条有价值的知识
-4. **完成标记**:调研完成后,使用 `goal(done="...")` 标记完成
-
-**注意**:
-- 每发现一条有价值的信息,立即使用 `save_knowledge` 保存
-- 调研完成后会自动进入计划阶段
+**重要提示**:
+- 调研完成后,请使用 `save_knowledge` 工具保存调研结果
+- 系统会自动检测到 save_knowledge 调用,并进入下一阶段(计划)
 """
 """
 
 
         elif stage == "planning":
         elif stage == "planning":
             return f"""
             return f"""
 ## 📋 研究流程 - 阶段 5: 制定计划
 ## 📋 研究流程 - 阶段 5: 制定计划
 
 
-调研已完成,现在需要制定执行计划。
+调研已完成(或无需调研),现在请制定执行计划。
 
 
-**请使用 goal 工具制定计划**:
-1. 参考 `agent/memory/skills/planning.md` 中的计划方法
-2. 使用 `goal(add="...")` 创建目标树
-3. 将任务拆解为可执行的子目标
-4. 使用 `goal(focus="...")` 聚焦到第一个目标
+**请立即执行以下操作**:
+1. 使用 `goal` 工具创建目标树
+2. 将任务分解为可执行的子目标
+3. 为每个子目标设置合理的优先级
 
 
-计划完成后会自动打印 goal tree 并进入执行阶段。
+注意:这是强制步骤,必须创建 goal tree 才能进入执行阶段。
 """
 """
 
 
+        # research_decision 阶段的引导消息已移到 _build_research_decision_guide
         return ""
         return ""
 
 
+    def _build_research_decision_guide(self, research_state: Dict[str, Any]) -> str:
+        """构建调研决策阶段的引导消息(追加在经验检索消息后)"""
+        knowledge_results = research_state.get("knowledge_results", [])
+        experience_results = research_state.get("experience_results", [])
+        task_desc = research_state.get("task_desc", "")
+
+        # 构建知识库摘要
+        knowledge_summary = "### 知识库检索结果\n\n"
+        if knowledge_results:
+            knowledge_summary += f"找到 {len(knowledge_results)} 条相关知识:\n\n"
+            for i, item in enumerate(knowledge_results[:5], 1):  # 最多显示 5 条
+                tags = item.get("tags", [])
+                summary = item.get("summary", "")
+                knowledge_summary += f"{i}. [{', '.join(tags)}] {summary[:150]}...\n"
+            if len(knowledge_results) > 5:
+                knowledge_summary += f"\n(还有 {len(knowledge_results) - 5} 条知识未显示)\n"
+        else:
+            knowledge_summary += "❌ 未找到相关知识\n"
+
+        # 构建经验库摘要
+        experience_summary = "\n### 经验库检索结果\n\n"
+        if experience_results:
+            experience_summary += f"找到 {len(experience_results)} 条相关经验\n"
+        else:
+            experience_summary += "❌ 未找到相关经验\n"
+
+        return f"""
+---
+
+## 🤔 调研决策
+
+{knowledge_summary}
+{experience_summary}
+
+### 决策选项
+
+**选项 1: 无需调研,直接制定计划**
+- 如果现有知识和经验已经足够
+- 直接使用 `goal` 工具制定执行计划
+
+**选项 2: 需要深入调研** ⭐ 
+- 如果现有知识不足,需要进行网络搜索、文档查阅等调研
+- **立即调用 `agent` 工具启动调研子任务**:
+- 如果没有经验/知识,必须要先调研才可以继续执行
+- 无论任务描述是否确定。都需要先调研
+```python
+agent(
+    task=\"\"\"针对任务「{task_desc[:100]}」进行深入调研:
+
+1. 使用 web_search 工具搜索相关技术文档、教程、最佳实践
+2. 搜索关键词建议:
+   - 核心技术名称 + "教程"
+   - 核心技术名称 + "最佳实践"
+   - 核心技术名称 + "示例代码"
+3. 使用 read_file 工具查看项目中的相关文件
+4. 对每条有价值的信息,使用 save_knowledge 工具保存,标签类型选择:
+   - tool: 工具使用方法
+   - definition: 概念定义
+   - usercase: 使用案例
+   - strategy: 策略经验
+
+调研完成后,系统会自动进入计划阶段。
+\"\"\"
+)
+```
+
+**重要提示**:
+- 如果知识库和经验库都没有找到相关内容,必须选择调研!但只需要简单调研,最多设立两个goal。
+"""
+
     async def _handle_research_flow_transition(
     async def _handle_research_flow_transition(
         self,
         self,
         trace_id: str,
         trace_id: str,
@@ -928,15 +925,26 @@ class AgentRunner:
         # 阶段 3: 调研决策(通过 assistant 的文本回复判断)
         # 阶段 3: 调研决策(通过 assistant 的文本回复判断)
         # 这个阶段的转换在 assistant 回复后处理,不在这里
         # 这个阶段的转换在 assistant 回复后处理,不在这里
 
 
-        # 阶段 4: 调研完成(检测到 save_knowledge 调用)
-        elif stage == "research" and tool_name == "save_knowledge":
-            # 标记调研完成,进入计划阶段
-            self._update_research_stage(
-                trace_id,
-                "planning",
-                research_completed=True
-            )
-            logger.info(f"[Research Flow] 调研完成,进入计划阶段")
+        # 阶段 4: 调研完成
+        # 情况 1: 检测到 save_knowledge 调用(直接调研)
+        # 情况 2: 检测到 agent 工具执行完成(子 agent 调研)
+        elif stage == "research":
+            if tool_name == "save_knowledge":
+                # 直接调研:检测到 save_knowledge 调用
+                self._update_research_stage(
+                    trace_id,
+                    "planning",
+                    research_completed=True
+                )
+                logger.info(f"[Research Flow] 调研完成(直接调研),进入计划阶段")
+            elif tool_name == "agent":
+                # 子 agent 调研:agent 工具执行完成
+                self._update_research_stage(
+                    trace_id,
+                    "planning",
+                    research_completed=True
+                )
+                logger.info(f"[Research Flow] 调研完成(子 agent 调研),进入计划阶段")
 
 
         # 阶段 5: 计划完成(检测到 goal 工具调用)
         # 阶段 5: 计划完成(检测到 goal 工具调用)
         elif stage == "planning" and tool_name == "goal":
         elif stage == "planning" and tool_name == "goal":
@@ -1101,6 +1109,19 @@ class AgentRunner:
             # 构建 LLM messages(注入上下文)
             # 构建 LLM messages(注入上下文)
             llm_messages = list(history)
             llm_messages = list(history)
 
 
+            # 收集需要持久化的消息
+            user_messages_to_persist = []  # 研究流程引导和经验检索改为 user 消息
+            system_messages_to_persist = []  # 上下文注入保持为 system 消息
+
+            # 研究流程引导(仅在启用且处于研究阶段时)- 改为 user 消息
+            research_state = self._get_research_state(trace_id)
+            if research_state and research_state["stage"] != "execution":
+                research_guide = self._build_research_guide(research_state)
+                if research_guide:
+                    user_msg = {"role": "user", "content": research_guide}
+                    llm_messages.append(user_msg)
+                    user_messages_to_persist.append(("研究流程引导", user_msg))
+
             # 先对历史消息应用 Prompt Caching(在注入动态内容之前)
             # 先对历史消息应用 Prompt Caching(在注入动态内容之前)
             # 这样可以确保历史消息的缓存点固定,不受动态注入影响
             # 这样可以确保历史消息的缓存点固定,不受动态注入影响
             llm_messages = self._add_cache_control(
             llm_messages = self._add_cache_control(
@@ -1125,10 +1146,11 @@ class AgentRunner:
                 current_goal = goal_tree.find(current_goal_id)
                 current_goal = goal_tree.find(current_goal_id)
                 if current_goal:
                 if current_goal:
                     try:
                     try:
-                        relevant_exps = await _get_structured_experiences(
+                        relevant_exps = await _get_structured_knowledge(
                             query_text=current_goal.description,
                             query_text=current_goal.description,
                             top_k=3,
                             top_k=3,
-                            context={"runner": self}
+                            context={"runner": self},
+                            tags_filter=["strategy"]  # 只检索经验(strategy 标签)
                         )
                         )
                         if relevant_exps:
                         if relevant_exps:
                             self.used_ex_ids = [exp['id'] for exp in relevant_exps]
                             self.used_ex_ids = [exp['id'] for exp in relevant_exps]
@@ -1142,15 +1164,62 @@ class AgentRunner:
                             )
                             )
                         else:
                         else:
                             _cached_exp_text = ""
                             _cached_exp_text = ""
+                            logger.info(
+                                "经验检索: goal='%s', 未找到相关经验",
+                                current_goal.description[:40],
+                            )
                     except Exception as e:
                     except Exception as e:
                         logger.warning("经验检索失败: %s", e)
                         logger.warning("经验检索失败: %s", e)
                         _cached_exp_text = ""
                         _cached_exp_text = ""
 
 
-            # 经验注入:goal切换时注入相关历史经验
-            if _cached_exp_text:
-                system_msg = {"role": "system", "content": _cached_exp_text}
-                llm_messages.append(system_msg)
-                system_messages_to_persist.append(("经验检索", system_msg))
+                    # 如果处于 experience_search 阶段,自动转换到 research_decision 阶段
+                    research_state = self._get_research_state(trace_id)
+                    if research_state and research_state["stage"] == "experience_search":
+                        self._update_research_stage(
+                            trace_id,
+                            "research_decision",
+                            experience_found=len(relevant_exps) > 0 if relevant_exps else False,
+                            experience_results=relevant_exps if relevant_exps else []
+                        )
+                        logger.info(f"[Research Flow] 经验检索完成(自动触发),找到 {len(relevant_exps) if relevant_exps else 0} 条经验,进入调研决策阶段")
+
+            # 经验注入:goal切换时注入相关历史经验 - 改为 user 消息
+            # 或者在 research_decision 阶段注入调研决策引导
+            if _cached_exp_text or (research_state and research_state["stage"] == "research_decision"):
+                exp_content = _cached_exp_text if _cached_exp_text else ""
+
+                # 如果处于 research_decision 阶段,追加引导消息
+                if research_state and research_state["stage"] == "research_decision":
+                    if exp_content:
+                        exp_content += "\n\n"
+                    exp_content += self._build_research_decision_guide(research_state)
+
+                if exp_content:  # 确保有内容才注入
+                    user_msg = {"role": "user", "content": exp_content}
+                    llm_messages.append(user_msg)
+                    user_messages_to_persist.append(("经验检索", user_msg))
+
+            # 持久化 user 消息到 trace 和 history
+            for label, usr_msg in user_messages_to_persist:
+                # 添加到 history(这样会被包含在后续的对话中)
+                history.append(usr_msg)
+
+                # 保存到 trace store
+                if self.trace_store:
+                    # 在 content 前添加标签,这样会自动出现在 description 中
+                    labeled_content = f"[{label}]\n{usr_msg['content']}"
+                    user_message = Message.create(
+                        trace_id=trace_id,
+                        role="user",
+                        sequence=sequence,
+                        goal_id=current_goal_id,
+                        parent_sequence=head_seq if head_seq > 0 else None,
+                        content=labeled_content,
+                    )
+                    await self.trace_store.add_message(user_message)
+                    yield user_message
+                    head_seq = sequence
+                    sequence += 1
 
 
             # 持久化 system 消息到 trace 和 history
             # 持久化 system 消息到 trace 和 history
             for label, sys_msg in system_messages_to_persist:
             for label, sys_msg in system_messages_to_persist:
@@ -1159,14 +1228,15 @@ class AgentRunner:
 
 
                 # 保存到 trace store
                 # 保存到 trace store
                 if self.trace_store:
                 if self.trace_store:
+                    # 在 content 前添加标签,这样会自动出现在 description 中
+                    labeled_content = f"[{label}]\n{sys_msg['content']}"
                     system_message = Message.create(
                     system_message = Message.create(
                         trace_id=trace_id,
                         trace_id=trace_id,
                         role="system",
                         role="system",
                         sequence=sequence,
                         sequence=sequence,
                         goal_id=current_goal_id,
                         goal_id=current_goal_id,
                         parent_sequence=head_seq if head_seq > 0 else None,
                         parent_sequence=head_seq if head_seq > 0 else None,
-                        content=sys_msg["content"],
-                        description=f"[{label}]"  # 添加标签说明这是什么类型的 system 消息
+                        content=labeled_content,
                     )
                     )
                     await self.trace_store.add_message(system_message)
                     await self.trace_store.add_message(system_message)
                     yield system_message
                     yield system_message
@@ -1196,24 +1266,35 @@ class AgentRunner:
             research_state = self._get_research_state(trace_id)
             research_state = self._get_research_state(trace_id)
             research_decision_handled = False
             research_decision_handled = False
             if research_state and research_state["stage"] == "research_decision":
             if research_state and research_state["stage"] == "research_decision":
-                # 根据 assistant 的回复判断是否需要调研
-                response_lower = response_content.lower()
-                if "需要调研" in response_content or "need research" in response_lower:
-                    self._update_research_stage(trace_id, "research", need_research=True)
-                    logger.info(f"[Research Flow] 决定需要调研,进入调研阶段")
-                    research_decision_handled = True
-                    # 如果 Agent 错误地调用了工具,忽略它们
-                    if tool_calls:
-                        logger.warning(f"[Research Flow] 在 research_decision 阶段不应调用工具,已忽略 {len(tool_calls)} 个工具调用")
-                        tool_calls = None
-                elif "无需调研" in response_content or "不需要调研" in response_content or "信息充足" in response_content:
-                    self._update_research_stage(trace_id, "planning", need_research=False)
-                    logger.info(f"[Research Flow] 决定无需调研,直接进入计划阶段")
-                    research_decision_handled = True
-                    # 如果 Agent 错误地调用了工具,忽略它们
-                    if tool_calls:
-                        logger.warning(f"[Research Flow] 在 research_decision 阶段不应调用工具,已忽略 {len(tool_calls)} 个工具调用")
-                        tool_calls = None
+                # 检查是否调用了 agent 工具进行调研
+                if tool_calls:
+                    has_agent_call = any(
+                        tc.get("function", {}).get("name") == "agent"
+                        for tc in tool_calls
+                    )
+                    if has_agent_call:
+                        # LLM 决定使用子 agent 进行调研
+                        self._update_research_stage(trace_id, "research", need_research=True)
+                        logger.info(f"[Research Flow] LLM 决定使用子 agent 进行调研,进入调研阶段")
+                        research_decision_handled = True
+                        # 继续执行 agent 工具调用
+                    else:
+                        # 检查是否调用了 goal 工具(直接进入计划)
+                        has_goal_call = any(
+                            tc.get("function", {}).get("name") == "goal"
+                            for tc in tool_calls
+                        )
+                        if has_goal_call:
+                            self._update_research_stage(trace_id, "planning", need_research=False)
+                            logger.info(f"[Research Flow] LLM 决定无需调研,直接进入计划阶段")
+                            research_decision_handled = True
+                else:
+                    # 根据 assistant 的文本回复判断
+                    response_lower = response_content.lower()
+                    if "无需调研" in response_content or "不需要调研" in response_content or "信息充足" in response_content:
+                        self._update_research_stage(trace_id, "planning", need_research=False)
+                        logger.info(f"[Research Flow] LLM 决定无需调研,直接进入计划阶段")
+                        research_decision_handled = True
 
 
             # 按需自动创建 root goal
             # 按需自动创建 root goal
             if goal_tree and not goal_tree.goals and tool_calls:
             if goal_tree and not goal_tree.goals and tool_calls:
@@ -1571,7 +1652,7 @@ created_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
                             elif result == "mixed":
                             elif result == "mixed":
                                 update_map[ex_id] = {"action": "helpful", "feedback": ""}
                                 update_map[ex_id] = {"action": "helpful", "feedback": ""}
                     if update_map:
                     if update_map:
-                        count = await _batch_update_experiences(update_map, context={"runner": self})
+                        count = await _batch_update_knowledge(update_map, context={"runner": self})
                         logger.info("经验评估完成,更新了 %s 条经验", count)
                         logger.info("经验评估完成,更新了 %s 条经验", count)
             except Exception as e:
             except Exception as e:
                 logger.warning("经验评估解析失败(不影响压缩): %s", e)
                 logger.warning("经验评估解析失败(不影响压缩): %s", e)

+ 636 - 81
agent/tools/builtin/knowledge.py

@@ -47,7 +47,7 @@ async def save_knowledge(
     Args:
     Args:
         scenario: 任务描述(在什么情景下 + 要完成什么目标 + 得到能达成一个什么结果)
         scenario: 任务描述(在什么情景下 + 要完成什么目标 + 得到能达成一个什么结果)
         content: 核心内容
         content: 核心内容
-        tags_type: 知识类型标签,可选:tool, usercase, definition, plan
+        tags_type: 知识类型标签,可选:tool, usercase, definition, plan, strategy
         urls: 参考来源链接列表(论文/GitHub/博客等)
         urls: 参考来源链接列表(论文/GitHub/博客等)
         agent_id: 执行此调研的 agent ID
         agent_id: 执行此调研的 agent ID
         score: 初始评分 1-5(默认 3)
         score: 初始评分 1-5(默认 3)
@@ -120,6 +120,7 @@ async def update_knowledge(
     add_helpful_case: Optional[Dict[str, str]] = None,
     add_helpful_case: Optional[Dict[str, str]] = None,
     add_harmful_case: Optional[Dict[str, str]] = None,
     add_harmful_case: Optional[Dict[str, str]] = None,
     update_score: Optional[int] = None,
     update_score: Optional[int] = None,
+    evolve_feedback: Optional[str] = None,
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     更新已有的原子知识的评估反馈
     更新已有的原子知识的评估反馈
@@ -129,19 +130,28 @@ async def update_knowledge(
         add_helpful_case: 添加好用的案例 {"case_id": "...", "scenario": "...", "result": "...", "timestamp": "..."}
         add_helpful_case: 添加好用的案例 {"case_id": "...", "scenario": "...", "result": "...", "timestamp": "..."}
         add_harmful_case: 添加不好用的案例 {"case_id": "...", "scenario": "...", "result": "...", "timestamp": "..."}
         add_harmful_case: 添加不好用的案例 {"case_id": "...", "scenario": "...", "result": "...", "timestamp": "..."}
         update_score: 更新评分(1-5)
         update_score: 更新评分(1-5)
+        evolve_feedback: 经验进化反馈(当提供时,会使用 LLM 重写知识内容)
 
 
     Returns:
     Returns:
         更新结果
         更新结果
     """
     """
     try:
     try:
-        # 查找文件
+        # 查找文件(支持 JSON 和 MD 格式)
         knowledge_dir = Path(".cache/knowledge_atoms")
         knowledge_dir = Path(".cache/knowledge_atoms")
-        file_path = knowledge_dir / f"{knowledge_id}.md"
-
-        if not file_path.exists():
+        json_path = knowledge_dir / f"{knowledge_id}.json"
+        md_path = knowledge_dir / f"{knowledge_id}.md"
+
+        file_path = None
+        if json_path.exists():
+            file_path = json_path
+            is_json = True
+        elif md_path.exists():
+            file_path = md_path
+            is_json = False
+        else:
             return ToolResult(
             return ToolResult(
                 title="❌ 文件不存在",
                 title="❌ 文件不存在",
-                output=f"未找到知识文件: {file_path}",
+                output=f"未找到知识文件: {knowledge_id}",
                 error="文件不存在"
                 error="文件不存在"
             )
             )
 
 
@@ -149,78 +159,50 @@ async def update_knowledge(
         with open(file_path, "r", encoding="utf-8") as f:
         with open(file_path, "r", encoding="utf-8") as f:
             content = f.read()
             content = f.read()
 
 
+        # 解析数据
+        if is_json:
+            data = json.loads(content)
+        else:
+            # 解析 YAML frontmatter
+            yaml_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
+            if not yaml_match:
+                return ToolResult(
+                    title="❌ 格式错误",
+                    output=f"无法解析知识文件格式: {file_path}",
+                    error="格式错误"
+                )
+            data = yaml.safe_load(yaml_match.group(1))
+
         # 更新内容
         # 更新内容
         updated = False
         updated = False
-        import re
+        summary = []
 
 
         if add_helpful_case:
         if add_helpful_case:
-            # 增加 helpful 计数
-            helpful_match = re.search(r"helpful: (\d+)", content)
-            current_helpful = int(helpful_match.group(1)) if helpful_match else 0
-            content = re.sub(
-                r"helpful: \d+",
-                f"helpful: {current_helpful + 1}",
-                content
-            )
-
-            # 添加案例到 helpful_history
-            case_yaml = f"""    - case_id: {add_helpful_case.get('case_id', 'unknown')}
-      scenario: "{add_helpful_case.get('scenario', '')}"
-      result: "{add_helpful_case.get('result', '')}"
-      timestamp: {add_helpful_case.get('timestamp', datetime.now().isoformat())}"""
-
-            if "helpful_history: []" in content:
-                content = content.replace(
-                    "helpful_history: []",
-                    f"helpful_history:\n{case_yaml}"
-                )
-            else:
-                # 在 helpful_history 后追加
-                content = re.sub(
-                    r"(helpful_history:.*?)(\n  harmful)",
-                    f"\\1\n{case_yaml}\\2",
-                    content,
-                    flags=re.DOTALL
-                )
+            data["eval"]["helpful"] += 1
+            data["eval"]["helpful_history"].append(add_helpful_case)
+            data["metrics"]["helpful"] += 1
+            summary.append(f"添加 helpful 案例: {add_helpful_case.get('case_id')}")
             updated = True
             updated = True
 
 
         if add_harmful_case:
         if add_harmful_case:
-            # 增加 harmful 计数
-            harmful_match = re.search(r"harmful: (\d+)", content)
-            current_harmful = int(harmful_match.group(1)) if harmful_match else 0
-            content = re.sub(
-                r"harmful: \d+",
-                f"harmful: {current_harmful + 1}",
-                content
-            )
-
-            # 添加案例到 harmful_history
-            case_yaml = f"""    - case_id: {add_harmful_case.get('case_id', 'unknown')}
-      scenario: "{add_harmful_case.get('scenario', '')}"
-      result: "{add_harmful_case.get('result', '')}"
-      timestamp: {add_harmful_case.get('timestamp', datetime.now().isoformat())}"""
-
-            if "harmful_history: []" in content:
-                content = content.replace(
-                    "harmful_history: []",
-                    f"harmful_history:\n{case_yaml}"
-                )
-            else:
-                # 在 harmful_history 后追加
-                content = re.sub(
-                    r"(harmful_history:.*?)(\nmetrics)",
-                    f"\\1\n{case_yaml}\\2",
-                    content,
-                    flags=re.DOTALL
-                )
+            data["eval"]["harmful"] += 1
+            data["eval"]["harmful_history"].append(add_harmful_case)
+            data["metrics"]["harmful"] += 1
+            summary.append(f"添加 harmful 案例: {add_harmful_case.get('case_id')}")
             updated = True
             updated = True
 
 
         if update_score is not None:
         if update_score is not None:
-            content = re.sub(
-                r"score: \d+",
-                f"score: {update_score}",
-                content
-            )
+            data["eval"]["score"] = update_score
+            summary.append(f"更新评分: {update_score}")
+            updated = True
+
+        # 经验进化机制
+        if evolve_feedback:
+            old_content = data.get("content", "")
+            evolved_content = await _evolve_knowledge_with_llm(old_content, evolve_feedback)
+            data["content"] = evolved_content
+            data["metrics"]["helpful"] += 1
+            summary.append(f"知识进化: 基于反馈重写内容")
             updated = True
             updated = True
 
 
         if not updated:
         if not updated:
@@ -230,17 +212,18 @@ async def update_knowledge(
                 long_term_memory="尝试更新原子知识但未指定更新内容"
                 long_term_memory="尝试更新原子知识但未指定更新内容"
             )
             )
 
 
-        # 保存更新
-        with open(file_path, "w", encoding="utf-8") as f:
-            f.write(content)
+        # 更新时间戳
+        data["updated_at"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
 
 
-        summary = []
-        if add_helpful_case:
-            summary.append(f"添加 helpful 案例: {add_helpful_case.get('case_id')}")
-        if add_harmful_case:
-            summary.append(f"添加 harmful 案例: {add_harmful_case.get('case_id')}")
-        if update_score:
-            summary.append(f"更新评分: {update_score}")
+        # 保存更新
+        if is_json:
+            with open(file_path, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+        else:
+            # 重新生成 YAML frontmatter
+            meta_str = yaml.dump(data, allow_unicode=True).strip()
+            with open(file_path, "w", encoding="utf-8") as f:
+                f.write(f"---\n{meta_str}\n---\n")
 
 
         return ToolResult(
         return ToolResult(
             title="✅ 原子知识已更新",
             title="✅ 原子知识已更新",
@@ -374,13 +357,111 @@ async def _route_knowledge_by_llm(query_text: str, metadata_list: List[Dict], k:
         return []
         return []
 
 
 
 
-async def _get_structured_knowledge(query_text: str, top_k: int = 5, min_score: int = 3) -> List[Dict]:
+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 标题,直接返回重写后的正文。
+"""
+    try:
+        response = await openrouter_llm_call(
+            messages=[{"role": "user", "content": prompt}],
+            model="google/gemini-2.0-flash-001"
+        )
+
+        evolved_content = response.get("content", "").strip()
+
+        # 简单安全校验:如果 LLM 返回太短或为空,回退到原内容+追加
+        if len(evolved_content) < 5:
+            raise ValueError("LLM output too short")
+
+        return evolved_content
+
+    except Exception as e:
+        logger.warning(f"知识进化失败,采用追加模式回退: {e}")
+        timestamp = datetime.now().strftime('%Y-%m-%d')
+        return f"{old_content}\n\n---\n[Update {timestamp}]: {feedback}"
+
+
+async def _route_knowledge_by_llm(query_text: str, metadata_list: List[Dict], k: int = 5) -> List[str]:
+    """
+    第一阶段:语义路由。
+    让 LLM 挑选出 2*k 个语义相关的 ID。
+    """
+    if not metadata_list:
+        return []
+
+    # 扩大筛选范围到 2*k
+    routing_k = k * 2
+
+    routing_data = [
+        {
+            "id": m["id"],
+            "tags": m["tags"],
+            "scenario": m["scenario"][:100]  # 只取前100字符
+        } for m in metadata_list
+    ]
+
+    prompt = f"""
+你是一个知识检索专家。根据用户的当前任务需求,从下列原子知识元数据中挑选出最相关的最多 {routing_k} 个知识 ID。
+任务需求:"{query_text}"
+
+可选知识列表:
+{json.dumps(routing_data, ensure_ascii=False, indent=1)}
+
+请直接输出 ID 列表,用逗号分隔(例如: research-20260302-001, research-20260302-002)。若无相关项请输出 "None"。
+"""
+
+    try:
+        print(f"\n[Step 1: 知识语义路由] 任务: '{query_text}' | 候选总数: {len(metadata_list)} | 目标提取数: {routing_k}")
+
+        response = await openrouter_llm_call(
+            messages=[{"role": "user", "content": prompt}],
+            model="google/gemini-2.0-flash-001"
+        )
+
+        content = response.get("content", "").strip()
+        selected_ids = [idx.strip() for idx in re.split(r'[,\s]+', content) if idx.strip().startswith("research-")]
+
+        print(f"[Step 1: 知识语义路由] LLM 初选 ID ({len(selected_ids)}个): {selected_ids}")
+        return selected_ids
+    except Exception as e:
+        logger.error(f"LLM 知识路由失败: {e}")
+        return []
+
+
+async def _get_structured_knowledge(
+    query_text: str,
+    top_k: int = 5,
+    min_score: int = 3,
+    context: Optional[Any] = None,
+    tags_filter: Optional[List[str]] = None
+) -> List[Dict]:
     """
     """
-    语义检索原子知识
+    语义检索原子知识(包括经验)
 
 
     1. 解析知识库文件(支持 JSON 和 YAML 格式)
     1. 解析知识库文件(支持 JSON 和 YAML 格式)
     2. 语义路由:提取 2*k 个 ID
     2. 语义路由:提取 2*k 个 ID
     3. 质量精排:基于评分筛选出最终的 k 个
     3. 质量精排:基于评分筛选出最终的 k 个
+
+    Args:
+        query_text: 查询文本
+        top_k: 返回数量
+        min_score: 最低评分过滤
+        context: 上下文(兼容 experience 接口)
+        tags_filter: 标签过滤(如 ["strategy"] 只返回经验)
     """
     """
     knowledge_dir = Path(".cache/knowledge_atoms")
     knowledge_dir = Path(".cache/knowledge_atoms")
 
 
@@ -431,9 +512,19 @@ async def _get_structured_knowledge(query_text: str, top_k: int = 5, min_score:
             scenario = metadata.get("scenario", "").strip()
             scenario = metadata.get("scenario", "").strip()
             content_text = metadata.get("content", "").strip()
             content_text = metadata.get("content", "").strip()
 
 
+            # 标签过滤
+            tags = metadata.get("tags", {})
+            if tags_filter:
+                # 检查 tags.type 是否包含任何过滤标签
+                tag_types = tags.get("type", [])
+                if isinstance(tag_types, str):
+                    tag_types = [tag_types]
+                if not any(tag in tag_types for tag in tags_filter):
+                    continue  # 跳过不匹配的标签
+
             meta_item = {
             meta_item = {
                 "id": kid,
                 "id": kid,
-                "tags": metadata.get("tags", {}),
+                "tags": tags,
                 "scenario": scenario,
                 "scenario": scenario,
                 "score": metadata.get("eval", {}).get("score", 3),
                 "score": metadata.get("eval", {}).get("score", 3),
                 "helpful": metadata.get("metrics", {}).get("helpful", 0),
                 "helpful": metadata.get("metrics", {}).get("helpful", 0),
@@ -557,3 +648,467 @@ async def search_knowledge(
             output=f"错误: {str(e)}",
             output=f"错误: {str(e)}",
             error=str(e)
             error=str(e)
         )
         )
+
+
+# ===== 批量更新功能(类似经验机制)=====
+
+async def _batch_update_knowledge(
+    update_map: Dict[str, Dict[str, Any]],
+    context: Optional[Any] = None
+) -> int:
+    """
+    内部函数:批量更新知识(兼容 experience 接口)
+
+    Args:
+        update_map: 更新映射 {knowledge_id: {"action": "helpful/harmful/evolve", "feedback": "..."}}
+        context: 上下文(兼容 experience 接口)
+
+    Returns:
+        成功更新的数量
+    """
+    if not update_map:
+        return 0
+
+    knowledge_dir = Path(".cache/knowledge_atoms")
+    if not knowledge_dir.exists():
+        return 0
+
+    success_count = 0
+    evolution_tasks = []
+    evolution_registry = {}  # task_idx -> (file_path, data)
+
+    for knowledge_id, instr in update_map.items():
+        try:
+            # 查找文件
+            json_path = knowledge_dir / f"{knowledge_id}.json"
+            md_path = knowledge_dir / f"{knowledge_id}.md"
+
+            file_path = None
+            is_json = False
+            if json_path.exists():
+                file_path = json_path
+                is_json = True
+            elif md_path.exists():
+                file_path = md_path
+                is_json = False
+            else:
+                continue
+
+            # 读取并解析
+            with open(file_path, "r", encoding="utf-8") as f:
+                content = f.read()
+
+            if is_json:
+                data = json.loads(content)
+            else:
+                yaml_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
+                if not yaml_match:
+                    continue
+                data = yaml.safe_load(yaml_match.group(1))
+
+            # 更新 metrics
+            action = instr.get("action")
+            feedback = instr.get("feedback", "")
+
+            # 处理 mixed 中间态
+            if action == "mixed":
+                data["metrics"]["helpful"] = data.get("metrics", {}).get("helpful", 0) + 1
+                action = "evolve"
+
+            if action == "helpful":
+                data["metrics"]["helpful"] = data.get("metrics", {}).get("helpful", 0) + 1
+            elif action == "harmful":
+                data["metrics"]["harmful"] = data.get("metrics", {}).get("harmful", 0) + 1
+            elif action == "evolve" and feedback:
+                # 注册进化任务
+                old_content = data.get("content", "")
+                task = _evolve_knowledge_with_llm(old_content, feedback)
+                evolution_tasks.append(task)
+                evolution_registry[len(evolution_tasks) - 1] = (file_path, data, is_json)
+                data["metrics"]["helpful"] = data.get("metrics", {}).get("helpful", 0) + 1
+
+            data["updated_at"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+            # 如果不需要进化,直接保存
+            if action != "evolve" or not feedback:
+                if is_json:
+                    with open(file_path, "w", encoding="utf-8") as f:
+                        json.dump(data, f, ensure_ascii=False, indent=2)
+                else:
+                    meta_str = yaml.dump(data, allow_unicode=True).strip()
+                    with open(file_path, "w", encoding="utf-8") as f:
+                        f.write(f"---\n{meta_str}\n---\n")
+                success_count += 1
+
+        except Exception as e:
+            logger.error(f"更新知识失败 {knowledge_id}: {e}")
+            continue
+
+    # 并发进化
+    if evolution_tasks:
+        import asyncio
+        print(f"🧬 并发处理 {len(evolution_tasks)} 条知识进化...")
+        evolved_results = await asyncio.gather(*evolution_tasks)
+
+        # 回填进化结果
+        for task_idx, (file_path, data, is_json) in evolution_registry.items():
+            data["content"] = evolved_results[task_idx].strip()
+
+            if is_json:
+                with open(file_path, "w", encoding="utf-8") as f:
+                    json.dump(data, f, ensure_ascii=False, indent=2)
+            else:
+                meta_str = yaml.dump(data, allow_unicode=True).strip()
+                with open(file_path, "w", encoding="utf-8") as f:
+                    f.write(f"---\n{meta_str}\n---\n")
+            success_count += 1
+
+    return success_count
+
+
+@tool()
+async def batch_update_knowledge(
+    feedback_list: List[Dict[str, Any]],
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    批量反馈知识的有效性(类似经验机制)
+
+    Args:
+        feedback_list: 评价列表,每个元素包含:
+            - knowledge_id: (str) 知识 ID
+            - is_effective: (bool) 是否有效
+            - feedback: (str, optional) 改进建议,若有效且有建议则触发知识进化
+
+    Returns:
+        批量更新结果
+    """
+    try:
+        if not feedback_list:
+            return ToolResult(
+                title="⚠️ 反馈列表为空",
+                output="未提供任何反馈",
+                long_term_memory="批量更新知识: 反馈列表为空"
+            )
+
+        knowledge_dir = Path(".cache/knowledge_atoms")
+        if not knowledge_dir.exists():
+            return ToolResult(
+                title="❌ 知识库不存在",
+                output="知识库目录不存在",
+                error="知识库不存在"
+            )
+
+        success_count = 0
+        failed_items = []
+
+        for item in feedback_list:
+            knowledge_id = item.get("knowledge_id")
+            is_effective = item.get("is_effective")
+            feedback = item.get("feedback", "")
+
+            if not knowledge_id:
+                failed_items.append({"id": "unknown", "reason": "缺少 knowledge_id"})
+                continue
+
+            try:
+                # 查找文件
+                json_path = knowledge_dir / f"{knowledge_id}.json"
+                md_path = knowledge_dir / f"{knowledge_id}.md"
+
+                file_path = None
+                is_json = False
+                if json_path.exists():
+                    file_path = json_path
+                    is_json = True
+                elif md_path.exists():
+                    file_path = md_path
+                    is_json = False
+                else:
+                    failed_items.append({"id": knowledge_id, "reason": "文件不存在"})
+                    continue
+
+                # 读取并解析
+                with open(file_path, "r", encoding="utf-8") as f:
+                    content = f.read()
+
+                if is_json:
+                    data = json.loads(content)
+                else:
+                    yaml_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
+                    if not yaml_match:
+                        failed_items.append({"id": knowledge_id, "reason": "格式错误"})
+                        continue
+                    data = yaml.safe_load(yaml_match.group(1))
+
+                # 更新 metrics
+                if is_effective:
+                    data["metrics"]["helpful"] = data.get("metrics", {}).get("helpful", 0) + 1
+                    # 如果有反馈建议,触发进化
+                    if feedback:
+                        old_content = data.get("content", "")
+                        evolved_content = await _evolve_knowledge_with_llm(old_content, feedback)
+                        data["content"] = evolved_content
+                else:
+                    data["metrics"]["harmful"] = data.get("metrics", {}).get("harmful", 0) + 1
+
+                data["updated_at"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+                # 保存
+                if is_json:
+                    with open(file_path, "w", encoding="utf-8") as f:
+                        json.dump(data, f, ensure_ascii=False, indent=2)
+                else:
+                    meta_str = yaml.dump(data, allow_unicode=True).strip()
+                    with open(file_path, "w", encoding="utf-8") as f:
+                        f.write(f"---\n{meta_str}\n---\n")
+
+                success_count += 1
+
+            except Exception as e:
+                failed_items.append({"id": knowledge_id, "reason": str(e)})
+                continue
+
+        output_lines = [f"成功更新 {success_count} 条知识"]
+        if failed_items:
+            output_lines.append(f"\n失败 {len(failed_items)} 条:")
+            for item in failed_items:
+                output_lines.append(f"  - {item['id']}: {item['reason']}")
+
+        return ToolResult(
+            title="✅ 批量更新完成",
+            output="\n".join(output_lines),
+            long_term_memory=f"批量更新知识: 成功 {success_count} 条,失败 {len(failed_items)} 条"
+        )
+
+    except Exception as e:
+        logger.error(f"批量更新知识失败: {e}")
+        return ToolResult(
+            title="❌ 批量更新失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+
+
+# ===== 知识库瘦身功能(类似经验机制)=====
+
+@tool()
+async def slim_knowledge(
+    model: str = "anthropic/claude-sonnet-4.5",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    知识库瘦身:调用顶级大模型,将知识库中语义相似的知识合并精简
+
+    Args:
+        model: 使用的模型(默认 claude-sonnet-4.5)
+        context: 工具上下文
+
+    Returns:
+        瘦身结果报告
+    """
+    try:
+        knowledge_dir = Path(".cache/knowledge_atoms")
+
+        if not knowledge_dir.exists():
+            return ToolResult(
+                title="📂 知识库不存在",
+                output="知识库目录不存在,无需瘦身",
+                long_term_memory="知识库瘦身: 目录不存在"
+            )
+
+        # 获取所有文件
+        json_files = list(knowledge_dir.glob("*.json"))
+        md_files = list(knowledge_dir.glob("*.md"))
+        files = json_files + md_files
+
+        if len(files) < 2:
+            return ToolResult(
+                title="📂 知识库过小",
+                output=f"知识库仅有 {len(files)} 条,无需瘦身",
+                long_term_memory=f"知识库瘦身: 仅有 {len(files)} 条"
+            )
+
+        # 解析所有知识
+        parsed = []
+        for file_path in files:
+            try:
+                with open(file_path, "r", encoding="utf-8") as f:
+                    content = f.read()
+
+                if file_path.suffix == ".json":
+                    data = json.loads(content)
+                else:
+                    yaml_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
+                    if not yaml_match:
+                        continue
+                    data = yaml.safe_load(yaml_match.group(1))
+
+                parsed.append({
+                    "file_path": file_path,
+                    "data": data,
+                    "is_json": file_path.suffix == ".json"
+                })
+            except Exception as e:
+                logger.error(f"解析文件失败 {file_path}: {e}")
+                continue
+
+        if len(parsed) < 2:
+            return ToolResult(
+                title="📂 有效知识过少",
+                output=f"有效知识仅有 {len(parsed)} 条,无需瘦身",
+                long_term_memory=f"知识库瘦身: 有效知识 {len(parsed)} 条"
+            )
+
+        # 构造发给大模型的内容
+        entries_text = ""
+        for p in parsed:
+            data = p["data"]
+            entries_text += f"[ID: {data.get('id')}] [Tags: {data.get('tags', {})}] "
+            entries_text += f"[Metrics: {data.get('metrics', {})}] [Score: {data.get('eval', {}).get('score', 3)}]\n"
+            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 条。
+
+禁止输出任何开场白或解释。"""
+
+        print(f"\n[知识瘦身] 正在调用 {model} 分析 {len(parsed)} 条知识...")
+        response = await openrouter_llm_call(
+            messages=[{"role": "user", "content": prompt}],
+            model=model
+        )
+        content = response.get("content", "").strip()
+        if not content:
+            return ToolResult(
+                title="❌ 大模型返回为空",
+                output="大模型返回为空,瘦身失败",
+                error="大模型返回为空"
+            )
+
+        # 解析大模型输出
+        report_line = ""
+        new_entries = []
+        blocks = [b.strip() for b in content.split("===") if b.strip()]
+
+        for block in blocks:
+            if block.startswith("REPORT:"):
+                report_line = block
+                continue
+
+            lines = block.split("\n")
+            kid, tags, metrics, score, scenario, content_lines = None, {}, {}, 3, "", []
+            current_field = None
+
+            for line in lines:
+                if line.startswith("ID:"):
+                    kid = line[3:].strip()
+                    current_field = None
+                elif line.startswith("TAGS:"):
+                    try:
+                        tags = yaml.safe_load(line[5:].strip()) or {}
+                    except Exception:
+                        tags = {}
+                    current_field = None
+                elif line.startswith("METRICS:"):
+                    try:
+                        metrics = yaml.safe_load(line[8:].strip()) or {}
+                    except Exception:
+                        metrics = {"helpful": 0, "harmful": 0}
+                    current_field = None
+                elif line.startswith("SCORE:"):
+                    try:
+                        score = int(line[6:].strip())
+                    except Exception:
+                        score = 3
+                    current_field = None
+                elif line.startswith("SCENARIO:"):
+                    scenario = line[9:].strip()
+                    current_field = "scenario"
+                elif line.startswith("CONTENT:"):
+                    content_lines.append(line[8:].strip())
+                    current_field = "content"
+                elif current_field == "scenario":
+                    scenario += "\n" + line
+                elif current_field == "content":
+                    content_lines.append(line)
+
+            if kid and content_lines:
+                new_data = {
+                    "id": kid,
+                    "tags": tags,
+                    "scenario": scenario,
+                    "content": "\n".join(content_lines).strip(),
+                    "metrics": metrics,
+                    "eval": {
+                        "score": score,
+                        "helpful": 0,
+                        "harmful": 0,
+                        "helpful_history": [],
+                        "harmful_history": []
+                    },
+                    "updated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+                }
+                new_entries.append(new_data)
+
+        if not new_entries:
+            return ToolResult(
+                title="❌ 解析失败",
+                output="解析大模型输出失败,知识库未修改",
+                error="解析失败"
+            )
+
+        # 删除旧文件
+        for p in parsed:
+            try:
+                p["file_path"].unlink()
+            except Exception as e:
+                logger.error(f"删除旧文件失败 {p['file_path']}: {e}")
+
+        # 写入新文件(统一使用 JSON 格式)
+        for data in new_entries:
+            file_path = knowledge_dir / f"{data['id']}.json"
+            with open(file_path, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+
+        result = f"瘦身完成:{len(parsed)} → {len(new_entries)} 条知识"
+        if report_line:
+            result += f"\n{report_line}"
+
+        print(f"[知识瘦身] {result}")
+        return ToolResult(
+            title="✅ 知识库瘦身完成",
+            output=result,
+            long_term_memory=f"知识库瘦身: {len(parsed)} → {len(new_entries)} 条"
+        )
+
+    except Exception as e:
+        logger.error(f"知识库瘦身失败: {e}")
+        return ToolResult(
+            title="❌ 瘦身失败",
+            output=f"错误: {str(e)}",
+            error=str(e)
+        )
+

+ 32 - 0
examples/restore/input/images/img_1_invariant_features.json

@@ -0,0 +1,32 @@
+{
+  "composition_layout": {
+    "subject_position": "人物和画架位于画面右侧和中心偏右,占据了画面的大部分空间,形成一个不对称但平衡的构图。人物背对观众,专注于绘画。",
+    "perspective": "中景视角,略微仰视,使得人物和画架显得较为突出,背景的树木和草地则作为衬托。",
+    "depth_of_field": "景深较浅,人物和画架清晰锐利,是画面的焦点。背景的树木和草地则呈现出明显的虚化效果,突出了主体。"
+  },
+  "color_distribution": {
+    "main_tones": "画面以绿色和白色为主色调。绿色主要来自背景的草地和树木,以及画板上的颜料。白色则主要来自人物的衣着和画板上未完成的画作中的白色元素。",
+    "color_areas": "画面下方是鲜亮的绿色草地,上方是深浅不一的绿色树木。人物身着白色长裙,与周围的绿色形成对比。画板上则有丰富的色彩,包括蓝色、绿色、粉色、红色等,显示出绘画的活力。"
+  },
+  "lighting_effects": {
+    "light_source_direction": "光线似乎来自画面的左前方或上方,使得人物的右侧和画板上的颜料有明显的光照,而人物的左侧和部分背景则略显阴影。",
+    "light_dark_contrast": "整体对比度适中。人物的白色衣物在光照下显得明亮,与背景的绿色形成对比。画板上的颜料色彩鲜艳,也增加了画面的明暗对比。",
+    "highlights_shadows": "人物的头发和衣物边缘有高光,显示出材质的柔软和光泽。背景的树木在光照下呈现出不同的明暗层次,增加了画面的立体感。草地上也有细微的光影变化。"
+  },
+  "figure_pose_features": {
+    "body_posture": "人物站立,身体略微向右倾斜,头部转向画板,呈现出专注于绘画的姿态。右臂抬起,手持画笔,左手拿着调色板。",
+    "clothing_details": "人物身穿一件白色长袖连衣裙,材质轻盈,裙摆在微风中略微飘动,显得优雅而自然。衣领和袖口设计简洁。",
+    "action": "人物正在用画笔在画板上作画,动作专注而投入。"
+  },
+  "object_details": {
+    "easel": "画架是木质的,三脚架结构,颜色为原木色,支撑着画板。画架上还系着一个黑色的包或带子。",
+    "palette": "调色板呈椭圆形,上面沾满了各种颜色的油画颜料,包括绿色、蓝色、粉色、红色、黄色等,颜料堆叠,显示出使用痕迹。",
+    "brush": "画笔细长,笔尖沾有颜料,正被人物用来在画板上描绘细节。",
+    "other_props": "画架上还放着一朵白色的玫瑰花,花瓣和叶子清晰可见,为画面增添了一丝浪漫和艺术气息。"
+  },
+  "background_features": {
+    "depth_of_field_effect": "背景呈现出明显的虚化效果,与前景的人物和画架形成对比,突出了主体。",
+    "blur_level": "背景的模糊程度较高,使得树木和草地的细节变得柔和,形成一片绿色的光斑。",
+    "background_elements": "背景主要是茂密的绿色树木和广阔的绿色草地,营造出一种自然、宁静的户外环境。远处可能还有一些模糊的建筑或景观,但细节不清晰。"
+  }
+}

+ 80 - 0
examples/restore/input/images/img_7_invariant_features.json

@@ -0,0 +1,80 @@
+{
+  "人物姿态骨架": {
+    "头部": {
+      "位置": "画面右侧偏上,侧脸朝向左侧,略微向下倾斜",
+      "角度": "侧面,眼睛微闭,鼻子和嘴唇轮廓清晰"
+    },
+    "肩部": {
+      "位置": "右肩可见,位于画面右下角,左肩被身体遮挡",
+      "角度": "右肩略微向后,呈现放松状态"
+    },
+    "手臂": {
+      "位置": "左手臂大部分可见,位于画面左下角,右手部分可见",
+      "角度": "左手臂弯曲,手部托举玫瑰花茎,右手轻扶花茎"
+    },
+    "手部": {
+      "位置": "左手位于画面左下角,右手位于左手上方,靠近玫瑰花茎",
+      "角度": "左手手指轻柔地握住花茎,右手食指和拇指轻触花茎,指甲涂有蓝色指甲油"
+    }
+  },
+  "色彩分布图": {
+    "背景绿色区域": {
+      "位置": "画面大部分区域,尤其是左侧和上方",
+      "颜色": "中等饱和度的绿色,呈现草地纹理,有模糊的景深效果"
+    },
+    "人物白色区域": {
+      "位置": "人物上衣,主要集中在画面右下角和中部",
+      "颜色": "纯白色,有褶皱和光影变化"
+    },
+    "玫瑰花位置": {
+      "位置": "画面左侧中部,靠近人物面部",
+      "颜色": "纯白色花瓣,绿色花茎和叶子"
+    },
+    "人物肤色": {
+      "位置": "人物面部、颈部和手部",
+      "颜色": "自然偏白的肤色,带有健康的红润感"
+    },
+    "人物头发": {
+      "位置": "人物头部后方和右侧",
+      "颜色": "深棕色,有光泽"
+    },
+    "唇色": {
+      "位置": "人物嘴唇",
+      "颜色": "红色"
+    },
+    "耳环": {
+      "位置": "人物右耳",
+      "颜色": "金色"
+    },
+    "项链": {
+      "位置": "人物颈部",
+      "颜色": "银色"
+    }
+  },
+  "深度图信息": {
+    "前景": {
+      "物体": "人物的左手和玫瑰花茎下部",
+      "特征": "最清晰,细节丰富"
+    },
+    "中景": {
+      "物体": "人物面部、玫瑰花朵、人物上衣",
+      "特征": "清晰,细节可见"
+    },
+    "背景": {
+      "物体": "绿色草地",
+      "特征": "模糊,景深效果明显,呈现虚化状态"
+    }
+  },
+  "边缘和轮廓": {
+    "人物面部轮廓": "清晰,尤其是鼻子、嘴唇和下巴的线条",
+    "玫瑰花朵轮廓": "清晰,花瓣层次分明",
+    "人物上衣轮廓": "清晰,衣领和袖子的边缘可见",
+    "手部轮廓": "清晰,手指和指甲的形状明确",
+    "背景草地边缘": "模糊,与前景和中景形成对比"
+  },
+  "光源和阴影": {
+    "光源位置": "推测为画面左上方或前方,光线柔和",
+    "高光区域": "人物面部(尤其是鼻梁、额头和脸颊)、玫瑰花瓣、人物头发、耳环、项链",
+    "阴影区域": "人物颈部下方、头发深处、衣物褶皱处、玫瑰花叶子下方"
+  }
+}

+ 16 - 9
examples/restore/input/images/set_invariant_features.json

@@ -1,11 +1,18 @@
 {
 {
-  "viewing_angle": 90,
-  "person_restoration": 90,
-  "props_restoration": 90,
-  "background": 80,
-  "composition": 90,
-  "weighted_total": 89,
-  "main_issues": [
-    "The background bokeh is present but the circular light spots are not as prominent or well-defined as in the original images."
-  ]
+  "共同视觉风格": {
+    "色调": "整体色调偏向清新、自然,以绿色(草地、树木)和白色(人物服装、玫瑰花)为主,辅以柔和的暖色调(阳光、人物肤色)。",
+    "光影": "光线柔和,多为自然光。第一张和第二张图片中,阳光从侧面或背面照射,营造出一种温暖、宁静的氛围,并带有一定的逆光效果,使人物边缘有光晕。第三张图片光线均匀,突出人物面部细节。",
+    "景深": "景深较浅,背景虚化明显,突出前景的人物和主体,营造出柔和的视觉效果,使画面更具艺术感和聚焦性。"
+  },
+  "共同主题元素": {
+    "人物": "一位年轻女性,穿着白色连衣裙,长发。在第一张和第二张图片中,她正在户外绘画;在第三张图片中,她正在闻一朵白玫瑰。",
+    "道具": "画架、画布、调色板、画笔、白色玫瑰花。",
+    "场景": "户外草地,背景有绿色的树木,暗示着自然环境。"
+  },
+  "不同图片之间的差异": {
+    "视角": "第一张图片是侧后方视角,展示了人物绘画的动作和画布上的画作。第二张图片是正后方视角,更强调人物的背影和环境光线。第三张图片是侧面特写视角,聚焦于人物的面部表情和手中的玫瑰花。",
+    "构图": "第一张图片采用斜线构图,人物身体和画架形成对角线,引导视线。第二张图片采用中心构图,人物背影居中,画架在右侧。第三张图片采用特写构图,人物面部和玫瑰花占据画面大部分,背景虚化。",
+    "细节": "第一张图片展示了画布上已完成的画作细节(一个穿着白裙的女孩),以及调色板上的颜料。第二张图片强调了阳光透过树叶的光斑效果。第三张图片则清晰展现了人物的妆容、耳环、项链以及指甲油的颜色,还有玫瑰花的纹理。"
+  },
+  "叙事连贯性": "这组图片具有较强的叙事连贯性,讲述了一个关于艺术、自然与美的故事。从第一张和第二张图片中,我们可以看到一位女性在户外享受绘画的乐趣,她将自然的美景和内心的感受融入画作。第三张图片则展现了她在绘画之余,沉浸于自然(玫瑰花)的芬芳,享受片刻的宁静与美好。整个系列通过描绘女性与艺术、自然之间的互动,营造出一种文艺、优雅、充满生活情趣的氛围,暗示着艺术创作与生活感悟的紧密联系。"
 }
 }

+ 616 - 0
examples/restore/input/paragraphs_overview.json

@@ -0,0 +1,616 @@
+{
+  "帖子名称": "写生油画",
+  "图片总数": 9,
+  "图片列表": [
+    {
+      "图片": "img_1",
+      "段落": [
+        {
+          "段落ID": "段落1",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上使用画架和调色板进行绘画,背景是绿色的树木和草地。",
+          "子段落": [
+            {
+              "段落ID": "段落1.1",
+              "名称": "人物",
+              "描述": "一名女性,侧身背对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落1.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,部分散落在肩上。"
+                },
+                {
+                  "段落ID": "段落1.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落1.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,袖子宽松。"
+                    },
+                    {
+                      "段落ID": "段落1.1.2.2",
+                      "名称": "画笔",
+                      "描述": "女性右手握持的细长画笔。"
+                    },
+                    {
+                      "段落ID": "段落1.1.2.3",
+                      "名称": "调色板",
+                      "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落1.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落1.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。"
+                },
+                {
+                  "段落ID": "段落1.2.2",
+                  "名称": "玫瑰花",
+                  "描述": "画架下方放置的一朵白色玫瑰花。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落1.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和近处的草地。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_2",
+      "段落": [
+        {
+          "段落ID": "段落2",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上使用画架和调色板进行绘画,背景是绿色的树木和阳光。",
+          "子段落": [
+            {
+              "段落ID": "段落2.1",
+              "名称": "人物",
+              "描述": "一名女性,背对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落2.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,披散在背部。"
+                },
+                {
+                  "段落ID": "段落2.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落2.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,露背设计。"
+                    },
+                    {
+                      "段落ID": "段落2.1.2.2",
+                      "名称": "画笔",
+                      "描述": "女性右手握持的细长画笔。"
+                    },
+                    {
+                      "段落ID": "段落2.1.2.3",
+                      "名称": "调色板",
+                      "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落2.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落2.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落2.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和草地,有阳光透过树叶。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_3",
+      "段落": [
+        {
+          "段落ID": "段落3",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上跪坐,使用画架和调色板进行绘画,背景是绿色的树木和远处的建筑。",
+          "子段落": [
+            {
+              "段落ID": "段落3.1",
+              "名称": "人物",
+              "描述": "一名女性,背对镜头,跪坐在草地上。",
+              "子段落": [
+                {
+                  "段落ID": "段落3.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,披散在背部。"
+                },
+                {
+                  "段落ID": "段落3.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落3.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,露背设计。"
+                    },
+                    {
+                      "段落ID": "段落3.1.2.2",
+                      "名称": "调色板",
+                      "描述": "女性左手旁放置的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落3.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落3.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落3.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和草地,以及远处的城市建筑。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_4",
+      "段落": [
+        {
+          "段落ID": "段落4",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上站立,使用画架和调色板进行绘画,背景是绿色的树木。",
+          "子段落": [
+            {
+              "段落ID": "段落4.1",
+              "名称": "人物",
+              "描述": "一名女性,侧身面对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落4.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,部分散落在肩上。"
+                },
+                {
+                  "段落ID": "段落4.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落4.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,袖子宽松。"
+                    },
+                    {
+                      "段落ID": "段落4.1.2.2",
+                      "名称": "画笔",
+                      "描述": "女性右手握持的细长画笔。"
+                    },
+                    {
+                      "段落ID": "段落4.1.2.3",
+                      "名称": "调色板",
+                      "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落4.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落4.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅空白画布。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落4.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和草地。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_5",
+      "段落": [
+        {
+          "段落ID": "段落5",
+          "名称": "户外绘画场景",
+          "描述": "画面展示了户外绘画的局部场景,主要聚焦于人物手持调色板和部分身体,以及背景的草地和画架。",
+          "子段落": [
+            {
+              "段落ID": "段落5.1",
+              "名称": "人物",
+              "描述": "画面中部的女性,穿着白色服装,正在进行绘画活动。",
+              "子段落": [
+                {
+                  "段落ID": "段落5.1.1",
+                  "名称": "手臂",
+                  "描述": "人物露出的手臂部分,包括左臂和右臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落5.1.1.1",
+                      "名称": "左臂",
+                      "描述": "人物左侧手臂,部分可见,手持画笔。",
+                      "子段落": [
+                        {
+                          "段落ID": "段落5.1.1.1.1",
+                          "名称": "画笔",
+                          "描述": "人物左手持有的细长画笔。"
+                        }
+                      ]
+                    },
+                    {
+                      "段落ID": "段落5.1.1.2",
+                      "名称": "右臂",
+                      "描述": "人物右侧手臂,手持调色板,佩戴手镯。",
+                      "子段落": [
+                        {
+                          "段落ID": "段落5.1.1.2.1",
+                          "名称": "手镯",
+                          "描述": "佩戴在右腕上的银色手镯。"
+                        }
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "段落ID": "段落5.1.2",
+                  "名称": "服装",
+                  "描述": "人物穿着的白色长袖衬衫和裙子。"
+                },
+                {
+                  "段落ID": "段落5.1.3",
+                  "名称": "调色板",
+                  "描述": "人物右手持有的椭圆形调色板,上面沾满了各种颜色的颜料。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落5.1.3.1",
+                      "名称": "颜料",
+                      "描述": "调色板上混合的多种颜色的颜料,包括绿色、蓝色、红色等。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落5.2",
+              "名称": "画架",
+              "描述": "画面左侧部分可见的木质画架。"
+            },
+            {
+              "段落ID": "段落5.3",
+              "名称": "背景",
+              "描述": "画面后方的绿色草地。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_6",
+      "段落": [
+        {
+          "段落ID": "段落6",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上使用画架和调色板进行绘画,特写镜头。",
+          "子段落": [
+            {
+              "段落ID": "段落6.1",
+              "名称": "人物",
+              "描述": "一名女性,侧身背对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落6.1.1",
+                  "名称": "耳朵",
+                  "描述": "女性的右耳,佩戴金色耳环。"
+                },
+                {
+                  "段落ID": "段落6.1.2",
+                  "名称": "头发",
+                  "描述": "棕色长发,部分散落在肩上。"
+                },
+                {
+                  "段落ID": "段落6.1.3",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落6.1.3.1",
+                      "名称": "服装",
+                      "描述": "白色上衣,露背设计。"
+                    },
+                    {
+                      "段落ID": "段落6.1.3.2",
+                      "名称": "画笔",
+                      "描述": "女性右手握持的细长画笔,正在画布上作画。"
+                    },
+                    {
+                      "段落ID": "段落6.1.3.3",
+                      "名称": "调色板",
+                      "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落6.2",
+              "名称": "画架",
+              "描述": "木质画架的一部分,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落6.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落6.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_7",
+      "段落": [
+        {
+          "段落ID": "段落7",
+          "名称": "人物与玫瑰花",
+          "描述": "画面主体是一位女性侧身闻着一朵白色玫瑰花,背景是绿色的草地。",
+          "子段落": [
+            {
+              "段落ID": "段落7.1",
+              "名称": "人物",
+              "描述": "画面右侧一位女性的侧面特写,闭着眼睛,面部表情安详,正在闻着玫瑰花。",
+              "子段落": [
+                {
+                  "段落ID": "段落7.1.1",
+                  "名称": "头部",
+                  "描述": "女性的侧脸,闭着眼睛,表情安详,长发披肩。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落7.1.1.1",
+                      "名称": "头发",
+                      "描述": "深棕色长发,自然垂落。"
+                    },
+                    {
+                      "段落ID": "段落7.1.1.2",
+                      "名称": "面部",
+                      "描述": "女性的侧脸,闭着眼睛,鼻梁高挺,嘴唇微张,涂有红色口红。"
+                    },
+                    {
+                      "段落ID": "段落7.1.1.3",
+                      "名称": "耳饰",
+                      "描述": "右耳佩戴着一个金色的几何形状耳环。"
+                    }
+                  ]
+                },
+                {
+                  "段落ID": "段落7.1.2",
+                  "名称": "身体",
+                  "描述": "女性的肩部和部分躯干,穿着白色上衣,佩戴项链。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落7.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色宽松上衣,领口设计独特。"
+                    },
+                    {
+                      "段落ID": "段落7.1.2.2",
+                      "名称": "项链",
+                      "描述": "一条细长的银色项链,佩戴在颈部。"
+                    }
+                  ]
+                },
+                {
+                  "段落ID": "段落7.1.3",
+                  "名称": "手部",
+                  "描述": "女性的双手捧着玫瑰花的茎部,指甲涂有蓝色指甲油。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落7.1.3.1",
+                      "名称": "指甲",
+                      "描述": "指甲涂有亮蓝色指甲油。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落7.2",
+              "名称": "玫瑰花",
+              "描述": "一朵盛开的白色玫瑰花,带有绿色的茎和叶子,被女性双手捧着。",
+              "子段落": [
+                {
+                  "段落ID": "段落7.2.1",
+                  "名称": "花朵",
+                  "描述": "一朵洁白的玫瑰花,花瓣层叠。"
+                },
+                {
+                  "段落ID": "段落7.2.2",
+                  "名称": "茎叶",
+                  "描述": "绿色的玫瑰花茎和叶子。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落7.3",
+              "名称": "背景",
+              "描述": "模糊的绿色草地。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_8",
+      "段落": [
+        {
+          "段落ID": "段落8",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上站立,使用画架和调色板进行绘画,背景是绿色的树木。",
+          "子段落": [
+            {
+              "段落ID": "段落8.1",
+              "名称": "人物",
+              "描述": "一名女性,侧身面对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落8.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,部分散落在肩上。"
+                },
+                {
+                  "段落ID": "段落8.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落8.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,袖子宽松。"
+                    },
+                    {
+                      "段落ID": "段落8.1.2.2",
+                      "名称": "画笔",
+                      "描述": "女性右手握持的细长画笔。"
+                    },
+                    {
+                      "段落ID": "段落8.1.2.3",
+                      "名称": "调色板",
+                      "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落8.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落8.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅空白画布。"
+                },
+                {
+                  "段落ID": "段落8.2.2",
+                  "名称": "玫瑰花",
+                  "描述": "画架下方放置的一朵白色玫瑰花。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落8.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和草地。"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "图片": "img_9",
+      "段落": [
+        {
+          "段落ID": "段落9",
+          "名称": "户外绘画场景",
+          "描述": "一名女性在户外草地上站立,使用画架进行绘画,背景是绿色的树木和远处的建筑。",
+          "子段落": [
+            {
+              "段落ID": "段落9.1",
+              "名称": "人物",
+              "描述": "一名女性,背对镜头,正在进行绘画。",
+              "子段落": [
+                {
+                  "段落ID": "段落9.1.1",
+                  "名称": "头发",
+                  "描述": "棕色长发,披散在背部。"
+                },
+                {
+                  "段落ID": "段落9.1.2",
+                  "名称": "身体",
+                  "描述": "女性的躯干和手臂。",
+                  "子段落": [
+                    {
+                      "段落ID": "段落9.1.2.1",
+                      "名称": "服装",
+                      "描述": "白色长裙,露背设计。"
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              "段落ID": "段落9.2",
+              "名称": "画架",
+              "描述": "木质三脚画架,支撑着画布。",
+              "子段落": [
+                {
+                  "段落ID": "段落9.2.1",
+                  "名称": "画布",
+                  "描述": "画架上的一幅未完成的画作,描绘了风景。"
+                }
+              ]
+            },
+            {
+              "段落ID": "段落9.3",
+              "名称": "背景",
+              "描述": "远处的绿色树木和草地,以及远处的建筑和木质凉亭。"
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 0 - 35
examples/restore/input/person_invariant_features.json

@@ -1,35 +0,0 @@
-{
-  "人物姿态特征": {
-    "身体姿势": "身体略微侧向右方,上半身挺直。",
-    "头部朝向": "头部向左上方微抬,视线望向远方。",
-    "手臂位置": "左手自然下垂,右手抬起,手持画笔,手腕微弯,手掌朝向前方。左手托着调色板,调色板略微倾斜。",
-    "腿部姿势": "未完全展示,但从裙摆看,可能站立或轻微侧身站立。"
-  },
-  "面部特征": {
-    "脸型": "鹅蛋脸,下巴尖细。",
-    "五官": "眉毛自然,眼睛细长,眼尾略上扬,双眼皮明显,眼妆精致。鼻梁高挺,鼻头小巧。嘴唇饱满,唇形优美,涂有红色口红。",
-    "表情": "神情专注,略带沉思,眼神温柔。"
-  },
-  "发型特征": {
-    "长度": "长发及腰。",
-    "颜色": "深棕色,带有自然光泽。",
-    "质感": "发质柔顺,有光泽。",
-    "造型": "中分,大部分头发自然垂落在身体两侧,少量发丝散落在额头和脸颊旁。"
-  },
-  "着装特征": {
-    "服装款式": "白色连衣裙,上衣部分宽松,袖子为七分袖,袖口有褶皱。裙摆为A字形长裙,材质轻盈。",
-    "颜色": "纯白色。",
-    "材质": "看起来是棉麻或雪纺等轻薄透气的面料,有自然垂坠感。",
-    "细节": "上衣腰部有收腰设计,裙摆有自然褶皱。"
-  },
-  "肤色特征": {
-    "色调": "白皙偏暖调。",
-    "质感": "皮肤细腻,有光泽。"
-  },
-  "配饰特征": {
-    "耳环": "佩戴一对小巧的银色或白色耳环,形状为环状。",
-    "项链": "佩戴一条细长的银色项链,吊坠为圆形或椭圆形。",
-    "手链": "右手腕佩戴一条细手链。"
-  },
-  "整体气质和风格": "优雅、艺术、知性、温柔。整体风格清新自然,充满艺术气息,仿佛一位正在户外写生的画家。"
-}

+ 7 - 50
examples/restore/production.prompt

@@ -14,60 +14,17 @@ $system$
 
 
 $user$
 $user$
 **任务目标**
 **任务目标**
-基于 `examples/restore/input/` 中的解构数据,还原帖子「《秋日际遇》写生油画」的图片内容。
+基于 `examples/restore/input/` 中的解构数据,还原一组图片内容。
 
 
 **输入数据说明**
 **输入数据说明**
-- `input/images/`:原始图片(仅用于评估对比,严禁修改)
-  - `person.jpg`:人物参考图
-  - `image_01.jpg` ~ `image_09.jpg`:9张原始图片,用于与生成图进行视觉对比评分
-- `input/paragraphs/`:按段落拆分后的解构数据
-  - `01_图片分段_*.json`:9张图片的分段结果(含子段落名称、描述、顶点坐标),按 group_id 分为 3 组(g1 户外绘画全景 7张、g2 户外绘画近景 2张、g3 人物与玫瑰花特写 1张)
-  - `02_图片形式_*.json`:9张图片各自的形式分析(构图方式、色彩分布、光影方向、景深等)
-  - `03_图片制作点实质结果.json`:6个跨图片聚合的实质制作点(人物 95分、画布 80分、调色板 75分、背景 70分)
-  - `04_图片制作点形式结果.json`:32个跨图片聚合的形式特征(姿态、着装、发型、肤色、构图、色彩、光影等)
+- `input/readme.md`:数据结构说明文档
+- `input/images/`:原始图片(用于参考和评估对比)
+- `input/paragraphs/`:解构数据文件
 
 
 **执行要求**
 **执行要求**
+请先阅读 `input/readme.md` 了解数据结构和任务背景,然后制定执行计划完成图片还原任务。(最好预先调研一下)
 
 
-请使用 goal 工具制定执行计划,将任务拆解为以下阶段,并按 goal 顺序执行:
-
-**阶段 1: 数据分析与规划**
-- 读取并分析所有解构数据文件
-- 理解图片分组结构(g1/g2/g3)和制作点权重
-- 制定评估标准(基于制作点权重)
-- 规划迭代策略(按 group 分批生成)
-
-**阶段 2: 特征提取与控制图生成**
-- 使用 Gemini Pro 分析 `person.jpg`,提取人物姿势骨架坐标
-- 结合解构数据中的分段坐标,生成控制图
-- 输出到 `output_1/resource/` 目录
-
-**阶段 3: 迭代生成与评估**
-- 按 group 分批执行(g1 → g2 → g3)
-- 每个 Round 包含:
-  - **生成**:调用 Nano Banana 生成图像,输出到 `round_N/cache/`
-  - **评估**:使用 Gemini Pro 对比原图与生成图,按制作点权重评分
-  - **修正**:根据评估反馈更新生成指令,输出到 `round_N/evaluation/`
-- 如果评估未达标,自动进入下一个 Round
-
-**输出目录结构**
-```
-output_1/
-├── resource/
-│   ├── evaluation_criteria.md  # 评估标准
-│   ├── control_images/         # 控制图
-│   └── features.json           # 提取的特征数据
-├── round_1/
-│   ├── cache/                  # 生成的图片
-│   └── evaluation/             # 评估报告
-├── round_2/
-│   └── ...
-└── plan.md                     # 执行计划记录
-```
-
-**重要提示**
-1. 使用 `goal(add="...")` 创建子目标,使用 `goal(focus="...")` 切换当前目标
-2. 每个 goal 完成后使用 `goal(done="...")` 标记完成
-3. 严格按照 goal 顺序执行,不要跳过任何阶段
-4. 所有输出文件必须保存到 `examples/restore/output_1` 目录下
+**输出目录**
+所有输出文件保存到 `examples/restore/output_1` 目录下。
 
 
 请立即开始执行。
 请立即开始执行。