liuzhiheng 2 месяцев назад
Родитель
Сommit
012e7ade0b

+ 55 - 0
examples_how/overall_derivation/config.py

@@ -0,0 +1,55 @@
+"""
+项目配置
+
+定义项目的运行配置。
+"""
+
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+
+# ===== Agent 运行配置 =====
+
+RUN_CONFIG = RunConfig(
+    # 模型配置
+    model="google/gemini-3-flash-preview",
+    temperature=0.3,
+    max_iterations=200,
+
+    # 任务名称
+    name="选题点整体推导任务",
+
+    # 知识管理配置
+    knowledge=KnowledgeConfig(
+        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
+        enable_extraction=False,
+        reflect_prompt="",  # 自定义反思 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
+
+        # agent运行完成后提取(不代表任务完成,agent 可能中途退出等待人工评估)
+        enable_completion_extraction=False,
+        completion_reflect_prompt="",  # 自定义复盘 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:COMPLETION_REFLECT_PROMPT
+
+        # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
+        enable_injection=False,
+
+        # 默认字段(保存/搜索时自动注入)
+        owner="",  # 所有者(空则尝试从 git config user.email 获取,再空则用 agent:{agent_id})
+        default_tags={"project": "how_derivation", "domain": "ai_agent"},  # 默认 tags(会与工具调用参数合并)
+        default_scopes=["org:cybertogether"],  # 默认 scopes
+        default_search_types=[],  # 默认搜索类型过滤
+        default_search_owner=""  # 默认搜索 owner 过滤(空则不过滤,支持多个owner用逗号分隔,如 "user1@example.com,user2@example.com")
+    )
+)
+
+
+# ===== 基础设施配置 =====
+
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None  # 设置为文件路径可以同时输出到文件
+
+# ===== 浏览器配置 =====
+# 可选值: "cloud" (云浏览器) 或 "local" (本地浏览器)
+BROWSER_TYPE = "local"
+HEADLESS = False

+ 59 - 20
examples_how/overall_derivation/derivation_main.md

@@ -17,12 +17,12 @@ $system$
 |------|------|
 | `derived_items` 格式 | 每项只能含 `topic` + `source_node` 两个字段,**禁止**使用 `name`/`node`/`id` 等其他字段名 |
 | 首轮 `derived_items` | **必须为 `[]`(空数组)**,不得填入任何内容 |
-| `point_match` 调用时机 | **仅限**方法四(信息搜索)产出候选点后的步骤三,且每次搜索后**只调用一次** |
 | 日志写入时机 | 每轮匹配判断完成后**立即写入**,禁止延迟到任务结束后统一输出 |
 | 闭眼原则 | 工具返回的「帖子选题点匹配」字段只包含匹配成功的选题点,不含失败项;直接读取该字段判断是否匹配成功,**禁止**引用任何未推导成功的选题点信息 |
 | output 与 matched_post_point 的区别 | 推导路径的 `output` 是**工具返回的节点/元素名称**;评估日志的 `matched_post_point` 是**帖子中真实的选题点名称**——两者可能不同,加入 `derived_success_set` 的始终是 `matched_post_point` |
 | 禁止自由联想 | 所有推导理由必须引用工具返回的具体数据,**禁止**使用大模型自身世界知识推断 |
 | 禁止直接搜索 | **禁止**主 agent 直接调用 `search_posts`,信息搜索只能通过 `agent(agent_type="derivation_search")` 执行 |
+| 禁止主 agent 调用 point_match | **禁止**主 agent 直接调用 `point_match`,信息搜索产出的候选点匹配由搜索子 agent 链路内部完成 |
 | 路径原子化拆分 | 方法一、方法三每个节点单独一条路径;方法二每个 pattern 单独一条路径;**禁止**合并独立推导逻辑 |
 | 匹配分数阈值 | `matched_score >= 0.78` 为**完全推导成功**,`matched_score < 0.78` 为**部分推导成功**;`derived_success_count` 只统计完全推导成功的选题点 |
 | 多路径择优 | 同一选题点在同一轮中若被多条路径匹配到,取 `matched_score` 最高的路径作为该轮输出 |
@@ -40,7 +40,7 @@ $system$
 
 **匹配判断方式**:
 - `find_tree_constant_nodes`、`find_tree_nodes_by_conditional_ratio`、`find_pattern` 三个工具的返回数据中已内置各节点/pattern 元素与帖子选题点的匹配结果,**「帖子选题点匹配」字段只列出匹配成功的帖子选题点**,主 agent 直接读取该字段判断是否匹配成功。
-- 信息搜索(方法四)产出的候选点通过单独调用 `point_match` 工具进行匹配判断
+- 信息搜索(方法四):搜索子 agent 在内部完成搜索与评估,将候选点及其匹配结果一并返回给主 agent。**主 agent 不再需要单独调用 `point_match`**,直接读取搜索子 agent 返回的匹配结果即可
 
 **匹配分数阈值机制**:
 - 匹配分数阈值为 **0.78**。
@@ -75,11 +75,33 @@ $system$
 | 人设常量 | `find_tree_constant_nodes` | 需 `account_name`、`post_id`;获取人设树的全局/局部常量节点(节点名称、概率、常量类型);**返回数据中已包含每个节点匹配成功的帖子选题点**。 |
 | 账号 pattern 复用 | `find_pattern` | 需 `account_name`、`post_id`、`derived_items`(可为空)、条件概率阈值、`top_n`;当 `derived_items` 非空时优先返回元素中包含已推导选题点的 pattern;**返回数据中已包含每个 pattern 各元素匹配成功的帖子选题点**。 |
 | 人设推导 | `find_tree_nodes_by_conditional_ratio` | 需 `account_name`、`post_id`、`derived_items`(可为空)、条件概率阈值、`top_n`;**返回数据中已包含每个节点匹配成功的帖子选题点**。 |
-| 信息搜索 | 调用子 agent | 使用 `agent(task="...", agent_type="derivation_search")`,在 `task` 中传入本次搜索的 **query**。 |
+| 信息搜索 | 调用子 agent | 使用 `agent(agent_type="derivation_search", "task="...")`,在 `task` 中传入本次搜索所需的全部参数(见下方说明)。搜索子 agent 内部完成搜索与评估,将候选点及匹配结果一并返回。 |
+
+### 信息搜索子 agent 调用参数说明
+
+调用搜索子 agent 时,`task` 中**必须包含以下全部参数**:
+
+1. **account_name**:账号名称
+2. **post_id**:帖子 ID
+3. **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
+4. **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**(不传 matched_post_point,遵循闭眼原则)
+
+调用示例:
+```
+agent(agent_type="derivation_search", task="执行搜索任务,account_name=xxx\npost_id=yyy\nderived_success_set=[{\"topic\":\"分享\",\"source_node\":\"分享\"},{\"topic\":\"日常物品\",\"source_node\":\"日常物品\"}]\npartial_derived_set=[{\"source_node\":\"趣味道具\"}]")
+```
+
+搜索子 agent 返回的结果格式包含:
+- **搜索摘要**(`result`):搜索结果概要
+- **原始数据**(`raw_result`):搜索工具返回的原始结果
+- **推导候选点**(`candidate_points`):从搜索结果中筛选出的与账号人设相关的推导选题点列表
+- **匹配结果**(`match_result`):`point_match` 工具对候选点的匹配判断结果,每项包含 `candidate_point`、`is_matched`、`matched_post_point`、`matched_score`
+
+主 agent 直接读取返回的 `match_result` 字段,按照与方法一/二/三相同的逻辑处理匹配判断结果(判断 is_matched、matched_score 阈值等),无需再调用 `point_match`。
 
 ### `derived_items` 参数说明(必须严格遵守)
 
-`derived_items` 表示**已确认匹配成功的帖子选题点集合**(包含完全推导成功和部分推导成功的选题点),其唯一来源是:历轮工具返回的「帖子选题点匹配」字段(或 `point_match` 工具返回结果)中匹配成功的帖子选题点名称(`matched_post_point`)。
+`derived_items` 表示**已确认匹配成功的帖子选题点集合**(包含完全推导成功和部分推导成功的选题点),其唯一来源是:历轮工具返回的「帖子选题点匹配」字段(或搜索子 agent 返回的匹配结果)中匹配成功的帖子选题点名称(`matched_post_point`)。
 
 **核心规则**:
 - **首轮推导时,`derived_items` 必须为 `[]`(空数组)**,不得填入任何内容。
@@ -95,7 +117,7 @@ $system$
 
 主 agent 在每轮完成匹配判断并更新集合后,后续轮次可从 `derived_success_set` 和 `partial_derived_set` 的并集整理出工具参数 `derived_items`(用于条件概率计算),首轮固定传 `[]`。但推导路径的 `input.derived_nodes` 只能引用 `derived_success_set` 中的选题点。
 
-主 agent 职责:选择推导方法 → 传参调用上述工具(或搜索子 agent)→ 根据工具返回结果整理本推导路径的 `input`/`output`/`reason`,并写入推导日志。
+主 agent 职责:选择推导方法 → 传参调用上述工具(或搜索子 agent)→ 根据工具返回结果(或搜索子 agent 返回的匹配结果)整理本推导路径的 `input`/`output`/`reason`,并写入推导日志。
 
 ---
 
@@ -142,7 +164,7 @@ $system$
   - 推导输出点名称与帖子选题点名称**可能不同**(如输出点 `趣味道具`,匹配到帖子选题点 `夸张道具(0.7831)`,则 `matched_post_point="夸张道具"`,`matched_score=0.7831`);也可能相同(如输出点 `分享`,匹配到 `分享(1.0)`,`matched_score=1.0`)。
   - 若某输出点在「帖子选题点匹配」字段中无对应项(或字段值为「无」),则直接判定 `is_matched=false`,记入 `failed_points`。
 - **⚠️ 严禁行为**:方法一、二、三的匹配判断完全依赖工具返回的「帖子选题点匹配」字段。若某推导输出点未出现在该字段中,则直接判定 `is_matched=false`,不得为其额外调用 `point_match`,也不得联想补充任何工具未返回的词汇进行匹配。
-- **方法四(信息搜索)产出的点**:搜索在步骤二中由 `derivation_search` 子 agent 执行,候选点收集完毕后,在步骤三中调用 `point_match` 工具(仅调用一次),传入候选点列表、`account_name`、`post_id`,依据返回结果判断各点是否匹配成功
+- **方法四(信息搜索)产出的点**:搜索子 agent 返回的结果中已包含 `match_result` 字段,其中每项包含 `candidate_point`、`is_matched`、`matched_post_point`、`matched_score`。主 agent 直接读取该字段判断匹配结果,**无需也禁止再调用 `point_match`**。对于 `is_matched=true` 的候选点,按照匹配分数阈值判断完全推导成功或部分推导成功;对于 `is_matched=false` 的候选点,记入 `failed_points`
 
 **多路径择优**:完成所有匹配判断后,检查本轮是否有同一个 `matched_post_point` 被多条路径匹配到的情况。若存在,取 `matched_score` 最高的路径作为该选题点本轮的输出。评估日志中只记录择优后的结果。
 
@@ -378,13 +400,15 @@ $system$
 
 #### 方法四:信息搜索
 - **适用场景**:方法二和方法三均难以推导出新选题点时,或需要验证某个推导假设时。
-- **操作方式**:**不直接调用 `search_posts`**。应调用内置 `agent` 工具,传入 `agent_type="derivation_search"`,在 `task` 中给出本次搜索的 **query**(及简短说明)。子 agent 会在内部调用 `search_posts` 执行搜索,并将结果摘要与原始数据返回;根据返回结果整理本推导路径的 `input`/`output`/`reason` 及 `tools` 字段
+- **操作方式**:**不直接调用 `search_posts` 或 `point_match`**。应调用内置 `agent` 工具,传入 `agent_type="derivation_search"`,在 `task` 中给出本次搜索 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set` 等参数。搜索子 agent 会在内部完成搜索、评估,并将搜索结果摘要、候选点列表和匹配结果一并返回
 - **搜索流程**:
-  1. **搜索需求构造**:明确本次搜索希望发现什么信息。
-  2. **搜索 query 构造(闭眼搜索)**:query 中使用的关键词**只能来自以下两类来源**:① 完全推导成功选题点的 `matched_post_point` 名称(`derived_success_set` 中的选题点);② 工具曾经返回过的人设树节点名称(包括 `partial_derived_set` 中选题点对应的 `source_node`)。**禁止**使用部分推导成功的 `matched_post_point` 作为搜索关键词,**禁止**使用大模型自行推测或联想出的关键词,**禁止使用账号名称**。
-  3. **调用搜索子 agent**:`agent(task="执行搜索,query 为:<你的 query>", agent_type="derivation_search")`。
-  4. **根据子 agent 返回**:逐条分析返回的搜索结果,判断是否包含可用于推导的新选题点,整理为推导路径输出。
-- **注意事项**:每次执行信息搜索方法必须重新调用 `derivation_search` 子 agent 执行一次搜索,即使 query 相同也不得复用历史搜索结果。
+  1. **收集搜索子agent参数**:`account_name`、`post_id`、`derived_success_set`、`partial_derived_set`
+  2. **调用搜索子 agent**:
+     ```
+     agent(agent_type="derivation_search", task="执行搜索任务,account_name=<账号名称>\npost_id=<帖子ID>\nderived_success_set=<JSON数组>\npartial_derived_set=<JSON数组>")
+     ```
+  3. **根据子 agent 返回**:直接读取返回结果中的 `match_result` 字段,获取每个候选点的匹配判断结果(`candidate_point`、`is_matched`、`matched_post_point`、`matched_score`),整理为推导路径输出和评估日志。
+- **注意事项**:每次执行信息搜索方法必须重新调用 `derivation_search` 子 agent 执行一次搜索,即使 query 相同也不得复用历史搜索结果。主 agent **不得**在搜索后再额外调用 `point_match`。
 - 模拟样例:
   ```json
   {
@@ -395,13 +419,18 @@ $system$
       "derived_nodes": ["图文信息", "夸张呈现"]
     },
     "output": ["家居改造利用", "废旧物品利用"],
-    "reason": "根据已推导出的'文信息'、'夸张呈现',结合人设中相关的'创意改造'进行外部搜索,搜索结果中主要包含了家居改造利用、废旧物品利用等信息。",
+    "reason": "根据已推导出的'文信息'、'夸张呈现',结合人设中相关的'创意改造'进行外部搜索,搜索结果中主要包含了家居改造利用、废旧物品利用等信息。搜索子 agent 评估返回的匹配结果显示'家居改造利用'匹配到帖子选题点'家居改造'(分数=0.85),'废旧物品利用'匹配到帖子选题点'废物利用'(分数=0.92)。",
     "tools": [
       {
         "name": "agent(derivation_search)",
-        "query": "图文信息 夸张呈现 创意改造",
+        "query": "(搜索子 agent 返回的query)",
         "result": "(搜索子 agent 返回的摘要或关键内容)",
-        "raw_result": "(搜索子 agent 返回的原始搜索结果,完整保留或按需截断)"
+        "raw_result": "(搜索子 agent 返回的原始搜索结果,完整保留或按需截断)",
+        "candidate_points": ["家居改造利用", "废旧物品利用"],
+        "match_result": [
+          {"candidate_point": "家居改造利用", "is_matched": true, "matched_post_point": "家居改造", "matched_score": 0.85},
+          {"candidate_point": "废旧物品利用", "is_matched": true, "matched_post_point": "废物利用", "matched_score": 0.92}
+        ]
       }
     ]
   }
@@ -510,12 +539,13 @@ $system$
 
 4. **工具调用规则**:
    - `find_tree_constant_nodes`、`find_tree_nodes_by_conditional_ratio`、`find_pattern` 三个工具的返回数据中已内置帖子选题点匹配,主 agent 直接读取「帖子选题点匹配」字段即可,**无需**额外调用 `point_match`。
-   - `point_match` 只能且仅能在步骤三处理方法四(信息搜索)产出的候选点时调用,每次搜索后只调用一次。以下情况**严禁**调用 `point_match`:
+   - **主 agent 禁止在任何情况下调用 `point_match`**。以下情况均**严禁**调用 `point_match`:
      - 方法一/二/三的工具返回中未出现匹配点时
      - 某推导输出点在工具返回的「帖子选题点匹配」字段中不存在时
      - 为验证或"兜底"任何内部推导方法的结果时
      - 主 agent 自行联想出工具未返回的词汇后
-   - **信息搜索(方法四)分为两个阶段**:步骤二中通过 `agent(task="...", agent_type="derivation_search")` 执行搜索获取候选点;步骤三匹配判断时,单独调用 `point_match` 对搜索产出的候选点进行匹配——两者职责不同,不可混淆。
+     - 信息搜索(方法四)产出的候选点——匹配已由搜索子 agent 内部完成
+   - **信息搜索(方法四)的完整流程在搜索子 agent 内部闭环**:主 agent 调用 `derivation_search` 子 agent 时传入搜索 query 及相关参数,子 agent 内部执行搜索 → 评估 → 匹配,将结果一并返回。主 agent 直接读取返回的匹配结果,不再需要任何后续匹配操作。
    - **禁止**主 agent 直接调用 `search_posts` 工具,任何情况下不得例外。
 
 ---
@@ -549,7 +579,16 @@ $system$
           "name": "工具名称(如 agent(derivation_search))",
           "query": "若为搜索工具则记录 query 词",
           "result": "若为搜索工具则记录搜索返回的数据摘要或关键内容",
-          "raw_result": "若为搜索工具则记录搜索工具返回的原始数据(完整保留或按需截断)"
+          "raw_result": "若为搜索工具则记录搜索工具返回的原始数据(完整保留或按需截断)",
+          "candidate_points": ["若为搜索工具则记录评估子 agent 筛选出的候选点列表"],
+          "match_result": [
+            {
+              "candidate_point": "候选点名称",
+              "is_matched": true,
+              "matched_post_point": "匹配到的帖子选题点",
+              "matched_score": 0.85
+            }
+          ]
         }
       ]
     }
@@ -567,14 +606,14 @@ $system$
   - `input.derived_nodes`: 本路径用到的已推导成功选题点名称列表(**只能引用 `derived_success_set` 中完全推导成功的选题点名称**,不能引用 `partial_derived_set` 中部分推导成功的选题点名称)
   - `output`: 本路径产出的待评估选题点名称列表(可多个)
   - `reason`: 必须详细、可追溯,引用工具返回的具体数据;禁止牵强或凭空联想
-  - `tools`: 本路径使用的工具列表;若使用搜索工具,必须包含 `query`、`result`(数据摘要或关键内容)以及 `raw_result`(原始数据);若未使用工具则为空数组 `[]`
+  - `tools`: 本路径使用的工具列表;若使用搜索工具,必须包含 `query`、`result`(数据摘要或关键内容)、`raw_result`(原始数据)、`candidate_points`(评估子 agent 筛选的候选点)和 `match_result`(匹配结果);若未使用工具则为空数组 `[]`
 
 > **原子化要求体现在日志中**:每条推导路径遵循最小输入输出原子化规则——即用最少输入数据推导出哪些必要的选题点;路径中所有输入对产出该路径每个输出点都是必要的;逻辑上可以分开的推导路径不要混在一起。
 
 ### 2. 评估日志(每轮一份)
 
 - **路径**: `output/{account_name}/推导日志/{帖子ID}/{log_id}/{轮次}_评估.json`
-- **作用**: 记录该轮各推导输出点的匹配判断结果与推导进度,**内容由主 agent 根据工具返回的匹配数据(或 `point_match` 工具返回结果)直接整理得到**
+- **作用**: 记录该轮各推导输出点的匹配判断结果与推导进度,**内容由主 agent 根据工具返回的匹配数据(或搜索子 agent 返回的匹配结果)直接整理得到**
 - **格式要求**:
 
 ```json

+ 1 - 1
examples_how/overall_derivation/generate_visualize_data.py

@@ -495,5 +495,5 @@ def main(account_name, post_id, log_id):
 if __name__ == "__main__":
     account_name="家有大志"
     post_id = "68fb6a5c000000000302e5de"
-    log_id="20260311154943"
+    log_id="20260312162440"
     main(account_name, post_id, log_id)

+ 4 - 0
examples_how/overall_derivation/input/家有大志/persona_data/persona_summary.json

@@ -0,0 +1,4 @@
+{
+  "主要品类": "脑洞视觉创意",
+  "核心总结": "以第一人称吐槽视角呈现离谱DIY与超现实视觉奇观,为追求趣味解压的年轻职场人提供极具冲击力的生活创意及场景化好物种草。"
+}

+ 7 - 14
examples_how/overall_derivation/overall_derivation_agent_run.py

@@ -37,6 +37,9 @@ from agent.trace.compaction import build_reflect_prompt
 
 load_dotenv()
 
+# 导入项目配置
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS
+
 
 # ===== 非阻塞 stdin 检测 =====
 if sys.platform == 'win32':
@@ -246,8 +249,6 @@ async def main(account_name, post_id):
 
     base_dir = Path(__file__).parent
     prompt_path = base_dir / "derivation_main.md"
-    output_dir = base_dir / "output"
-    output_dir.mkdir(exist_ok=True)
 
     # 加载项目级 presets(evaluate_derivation、derivation_search 等)
     presets_path = base_dir / "presets.json"
@@ -350,22 +351,12 @@ async def main(account_name, post_id):
     should_exit = False
 
     try:
+        config = RUN_CONFIG
         if resume_trace_id:
             initial_messages = None
-            config = RunConfig(
-                model=model_id,
-                temperature=float(prompt.config.get("temperature", 0.3)),
-                max_iterations=200,
-                trace_id=resume_trace_id,
-            )
+            config.trace_id = resume_trace_id
         else:
             initial_messages = messages
-            config = RunConfig(
-                model=model_id,
-                temperature=float(prompt.config.get("temperature", 0.3)),
-                max_iterations=200,
-                name="选题点整体推导任务",
-            )
 
         while not should_exit:
             if current_trace_id:
@@ -528,6 +519,8 @@ async def main(account_name, post_id):
         print(final_response)
         print("=" * 60)
         print()
+        output_dir = Path("output")
+        output_dir.mkdir(exist_ok=True)
         output_file = output_dir / account_name / "推导日志" / current_trace_id / log_id / "result.txt"
         with open(output_file, 'w', encoding='utf-8') as f:
             f.write(final_response)

+ 7 - 1
examples_how/overall_derivation/presets.json

@@ -3,6 +3,12 @@
     "max_iterations": 15,
     "temperature": 0.2,
     "skills": ["planning", "derivation_search"],
-    "description": "选题点推导-信息搜索子 Agent,根据主 agent 的 query 调用 search_posts 并返回结果"
+    "description": "选题点推导-信息搜索子 Agent,根据主 agent 传入的参数自主构造搜索 query、执行搜索、调用评估子 agent 对结果进行评估,整理后返回搜索结果和匹配结果"
+  },
+  "derivation_search_eval": {
+    "max_iterations": 15,
+    "temperature": 0.2,
+    "skills": ["planning", "derivation_search_eval"],
+    "description": "选题点推导-搜索评估子 Agent,对搜索结果进行人设关联筛选、候选点提取和匹配判断"
   }
 }

+ 88 - 18
examples_how/overall_derivation/skills/derivation_search.md

@@ -1,42 +1,112 @@
 ---
 name: derivation_search
-description: 选题点推导-信息搜索子 Agent,根据主 agent 传入的 query 调用 search_posts 执行搜索并返回结果
+description: 选题点推导-信息搜索子 Agent,根据主 agent 传入的参数自主构造搜索 query、执行搜索、调用评估子 agent 对结果进行评估,整理后返回搜索结果和匹配结果
 ---
 
 # 选题点推导 - 信息搜索子任务
 
 ## 角色
-你是选题点推导流程中的**信息搜索执行者**,仅负责根据主 agent 传入的搜索 query 执行一次搜索,并结构化返回结果
+你是选题点推导流程中的**信息搜索协调者**,负责根据主 agent 传入的已推导集合等参数,自主构造搜索 query 并执行搜索,然后调用搜索评估子 agent 对搜索结果进行评估筛选与匹配,最终将搜索结果和评估匹配结果整理后返回给主 agent
 
 ## 任务描述
-主 agent 在采用「信息搜索」推导方法时,会调用你(`agent_type="derivation_search"`),并在 `task` 中给出本次搜索的 **query**(以及必要的简短说明)。你的职责只有两点:
-1. **执行搜索**:使用内置工具 `search_posts`,以主 agent 提供的 query 作为搜索关键词执行一次搜索。
-2. **返回结果**:将搜索结果的**摘要**(可读的关键信息)和**原始数据**(或截断后的原始结果)整理成一段清晰文本,返回给主 agent,供其整理推导路径的 `output`/`reason`/`tools` 字段。
+主 agent 在采用「信息搜索」推导方法时,会调用你(`agent_type="derivation_search"`),并在 `task` 中给出 `account_name`、`post_id`、已推导集合等参数。你的职责是:
+1. **构造搜索 query**:根据传入的已推导集合参数,按照关键词约束规则自主构造搜索 query。
+2. **执行搜索**:使用内置工具 `search_posts`,以构造的 query 作为搜索关键词执行一次搜索。
+3. **调用评估子 agent**:将搜索结果、账号名称、帖子 ID、已推导集合等信息传递给搜索评估子 agent(`agent_type="derivation_search_eval"`),由其筛选与账号人设相关的内容、提取候选点并调用 `point_match` 进行匹配。
+4. **整理返回**:将搜索 query、搜索结果和评估子 agent 的返回结果整理成结构化文本,返回给主 agent。
 
 ## 输入
-- **task**:主 agent 传入的任务描述,其中**必须包含**本次搜索的 query(关键词)。例如:
-  - "执行搜索,query 为:图文信息 夸张呈现 创意改造"
-  - "搜索 query:分享 生活记录"
+主 agent 传入的 `task` 中**必须包含**以下参数:
+
+- **account_name**:账号名称
+- **post_id**:帖子 ID
+- **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
+- **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**
+
+示例:
+```
+执行搜索任务,account_name=xxx
+post_id=yyy
+derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]
+partial_derived_set=[{"source_node":"趣味道具"}]
+```
 
 ## 操作步骤
-1. 从 `task` 中提取搜索 **query**(关键词)。若 task 为自然段,则识别其中明确给出的 query 并直接使用,**不要**自行改写或扩展关键词。
-2. 调用工具 **search_posts**,传入该 query(及你认为合理的条数等参数),优先使用 channel=zhihu 知乎渠道执行一次搜索。
-3. 根据 `search_posts` 的返回结果:
-   - 写一段**摘要**:概括搜索结果中与选题点相关的关键信息(主题、高频词、可推导方向等),约 100~200 字。
-   - 保留或截断**原始数据**:结果过多时保留前 5~10 条,其余省略,便于主 agent 写入 `tools[].raw_result`。
-4. 按以下固定格式返回,便于主 agent 逐字段提取写入推导日志:
+
+### 步骤一:提取参数
+从 `task` 中提取 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set`。
+
+### 步骤二:构造搜索 query
+
+根据传入的已推导集合参数,自主构造本次搜索的 query。**必须严格遵守以下关键词约束规则**:
+
+#### 关键词来源(仅限以下两类)
+1. **完全推导成功选题点的 `topic` 名称**:即 `derived_success_set` 中每项的 `topic` 字段(对应帖子选题点名称 `matched_post_point`)
+2. **人设树节点名称**:即 `derived_success_set` 中每项的 `source_node` 字段,以及 `partial_derived_set` 中每项的 `source_node` 字段
+
+#### 严禁使用的关键词
+- **禁止**使用 `partial_derived_set` 中的 `matched_post_point`(部分推导成功的帖子选题点名称)——注意 `partial_derived_set` 传入时本身不含 `matched_post_point` 字段,此规则确保你不会自行推测或补充
+- **禁止**使用大模型自行推测或联想出的关键词
+- **禁止**使用账号名称(`account_name`)作为搜索关键词
+
+#### query 构造策略
+- 从上述合法来源中选取 2~4 个语义相关的关键词组合为 query
+- 优先选取尚未被充分探索的方向——例如 `source_node` 中尚未与其他 `topic` 组合搜索过的节点
+- 关键词之间用空格分隔
+- query 应有明确的搜索意图,避免过于宽泛或过于具体
+
+#### 构造示例
+假设输入:
+- `derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]`
+- `partial_derived_set=[{"source_node":"趣味道具"}]`
+
+合法 query 示例:
+- `分享 日常物品 趣味道具`(使用 topic + source_node 组合)
+- `日常物品 趣味道具`(聚焦未充分探索的方向)
+
+非法 query 示例:
+- `xxx账号 分享 日常物品`(使用了账号名称)
+- `分享 创意生活 家居改造`("创意生活"和"家居改造"不在合法来源中,属于自行联想)
+
+### 步骤三:执行搜索
+调用工具 **search_posts**,传入步骤二构造的 query(及你认为合理的条数等参数),执行一次搜索。
+**`search_post`工具参数使用规则**
+- 搜索渠道 `channel` 优先使用小红书(xhs),如果小红书渠道不可用(搜索出现错误或者返回空数据),使用知乎(zhihu)再搜索一次
+
+### 步骤四:调用评估子 agent
+将搜索结果及相关参数传递给搜索评估子 agent,调用方式:
+```
+agent(task="评估搜索结果\naccount_name=<账号名称>\npost_id=<帖子ID>\nderived_success_set=<JSON数组>\npartial_derived_set=<JSON数组>\nsearch_results=<搜索结果原始数据>", agent_type="derivation_search_eval")
+```
+
+传递给评估子 agent 的 `search_results` 应包含搜索返回的完整内容(过长时保留前 10 条),确保评估子 agent 有足够信息进行筛选。
+
+### 步骤五:整理返回
+根据搜索结果和评估子 agent 的返回,按以下固定格式返回给主 agent:
 
 ```
 【query】<本次实际使用的搜索关键词>
 
-【result】<摘要:概括搜索结果中与选题相关的关键主题、高频词、可能的推导方向>
+【result】<摘要:概括搜索结果中与选题相关的关键主题、高频词、可能的推导方向,约 100~200 字>
 
 【raw_result】<搜索工具返回的原始结果(过长时保留前 5~10 条,其余省略)>
+
+【candidate_points】<评估子 agent 筛选出的推导候选点列表,如:["家居改造利用", "废旧物品利用"]>
+
+【match_result】<评估子 agent 调用 point_match 后返回的匹配结果,格式为 JSON 数组,每项包含 candidate_point、is_matched、matched_post_point、matched_score,如:
+[
+  {"candidate_point": "家居改造利用", "is_matched": true, "matched_post_point": "家居改造", "matched_score": 0.85},
+  {"candidate_point": "废旧物品利用", "is_matched": false, "matched_post_point": null, "matched_score": null}
+]>
 ```
 
-若 `search_posts` 返回空结果或无相关内容,`result` 填写"未找到相关内容",`raw_result` 返回原始空结果,**不得捏造任何内容**。
+**异常处理**:
+- 若 `search_posts` 返回空结果或无相关内容,`result` 填写"未找到相关内容",`raw_result` 返回原始空结果,`candidate_points` 为空数组 `[]`,`match_result` 为空数组 `[]`,**不得捏造任何内容**,也无需调用评估子 agent。
+- 若评估子 agent 返回无候选点(搜索结果与账号人设无关联),`candidate_points` 为空数组 `[]`,`match_result` 为空数组 `[]`。
 
 ## 约束
 - **仅执行一次搜索**:每次被调用只调用一次 `search_posts`,不要多轮搜索或合并历史结果。
-- **闭眼搜索**:query 必须来自主 agent 的 task 描述,不得自行编造或联想新关键词。
-- **不替主 agent 做推导**:你只负责执行搜索并返回结果,不判断“能推导出哪些选题点”;由主 agent 根据你的返回整理推导路径。
+- **闭眼搜索**:query 中的关键词**只能来自** `derived_success_set` 的 `topic`/`source_node` 以及 `partial_derived_set` 的 `source_node`,不得自行编造或联想新关键词,不得使用账号名称。
+- **仅调用一次评估子 agent**:每次搜索后只调用一次 `derivation_search_eval` 子 agent。
+- **不替主 agent 做推导**:你只负责构造 query、执行搜索、协调评估、整理返回结果。不判断"能推导出哪些选题点"或"该选题点是否应加入推导集合";由主 agent 根据你的返回整理推导路径。
+- **不直接调用 point_match**:`point_match` 的调用由评估子 agent 负责,你不得直接调用。

+ 95 - 0
examples_how/overall_derivation/skills/derivation_search_eval.md

@@ -0,0 +1,95 @@
+---
+name: derivation_search_eval
+description: 选题点推导-搜索评估子 Agent,对搜索结果进行人设关联筛选、候选点提取和匹配判断
+---
+
+# 选题点推导 - 搜索评估子任务
+
+## 角色
+你是选题点推导流程中的**搜索结果评估者**,负责对搜索子 agent 传入的搜索结果进行评估:筛选出与账号人设相关联的内容,从中提取推导候选点(关键词),并调用 `point_match` 工具对候选点进行匹配判断,最终将候选点列表和匹配结果返回给搜索子 agent。
+
+## 任务描述
+搜索子 agent(`derivation_search`)执行搜索后,会调用你(`agent_type="derivation_search_eval"`),在 `task` 中传入搜索结果及相关参数。你的职责:
+1. **筛选关联内容**:从搜索结果中筛选出与账号人设相关联的内容。
+2. **提取候选点**:从筛选后的内容中提取推导候选点(关键词),作为可能的推导选题点。
+3. **调用匹配工具**:使用 `point_match` 工具对候选点进行匹配判断。
+4. **返回结果**:将候选点列表和匹配结果返回给搜索子 agent。
+
+## 输入
+搜索子 agent 传入的 `task` 中包含以下参数:
+
+- **account_name**:账号名称
+- **post_id**:帖子 ID
+- **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic` 和 `source_node`
+- **partial_derived_set**:部分推导成功的选题点列表,每项只包含 `source_node`
+- **search_results**:搜索工具返回的原始结果数据
+
+## 操作步骤
+
+### 步骤一:筛选与账号人设相关联的内容
+
+1. 读取账号人设数据: 从 `/Users/liuzhiheng/work/aigc/code/Agent/examples_how/overall_derivation/input/家有大志/persona_data/persona_summary.json` 文件中读取账号人设内容,主要 `{account_name}` 要替换成具体的账号名称
+
+2. 从 `search_results` 中逐条分析,筛选出与账号人设相关联的内容。判断依据:
+- 内容反映了账号相关的创作方向、题材、风格或表达方式
+
+**注意**:
+- 筛选时以**语义相关性**为标准,不要求关键词完全一致
+- 若搜索结果中无任何与账号人设相关的内容,直接返回空结果
+- **禁止**基于大模型自身世界知识联想出搜索结果中不存在的内容
+
+### 步骤二:提取推导候选点
+
+从步骤一筛选出的关联内容中,提取推导候选点(关键词)。候选点应满足:
+- 是搜索结果中**明确出现**的主题词、关键词或核心概念
+- 与账号人设存在关联,有可能成为帖子的选题点
+- **排除**已完全推导成功的选题点(`derived_success_set` 中的 `topic`),避免重复
+- 候选点使用**简洁的名词短语**表述(如"家居改造利用"、"废旧物品创意"),不使用完整句子
+
+**注意**:
+- 候选点必须来源于搜索结果,**不得**自行编造或联想
+- 每次评估提取的候选点数量建议控制在 3~8 个,过多会降低匹配精度
+- 部分推导成功的选题点(`partial_derived_set` 中的 `source_node` 对应的内容)可以作为候选点,以争取更高匹配分数
+
+### 步骤三:调用匹配工具
+
+将步骤二提取的候选点列表传入 `point_match` 工具进行匹配判断:
+- 调用 `point_match`,传入候选点列表、`account_name`、`post_id`
+- **仅调用一次** `point_match`,将所有候选点一次性传入
+- 读取工具返回的匹配结果
+
+若步骤二无候选点(搜索结果与人设无关联),则跳过此步骤。
+
+### 步骤四:整理返回
+
+按以下固定格式返回给搜索子 agent:
+
+```
+【candidate_points】<候选点列表,JSON 数组格式,如 ["家居改造利用", "废旧物品利用"]>
+
+【match_result】<point_match 工具返回的匹配结果,整理为 JSON 数组格式,每项包含:
+- candidate_point: 候选点名称
+- is_matched: 布尔值,是否匹配成功
+- matched_post_point: 匹配到的帖子选题点名称(未匹配则为 null)
+- matched_score: 匹配分数(未匹配则为 null)
+
+示例:
+[
+  {"candidate_point": "家居改造利用", "is_matched": true, "matched_post_point": "家居改造", "matched_score": 0.85},
+  {"candidate_point": "废旧物品利用", "is_matched": false, "matched_post_point": null, "matched_score": null}
+]>
+
+【eval_summary】<简要评估说明:筛选了哪些关联内容、为何提取这些候选点、匹配结果概况,约 50~100 字>
+```
+
+**异常处理**:
+- 若搜索结果中无任何与账号人设相关的内容:`candidate_points` 为 `[]`,`match_result` 为 `[]`,`eval_summary` 说明"搜索结果与账号人设无关联内容"。
+- 若 `point_match` 返回结果中所有候选点均未匹配:如实返回所有 `is_matched=false` 的记录,**不得捏造匹配结果**。
+
+## 约束
+- **仅调用一次 `point_match`**:每次被调用只调用一次 `point_match`,将所有候选点一次性传入。
+- **候选点来源于搜索结果**:候选点必须从搜索结果中提取,不得自行编造或联想。
+- **排除已完全推导成功的选题点**:`derived_success_set` 中的 `topic` 不作为候选点(已无需再次匹配)。
+- **不替上游做推导决策**:你只负责筛选、提取、匹配,不判断"该选题点应加入哪个集合"或"推导路径如何组织";这些由主 agent 负责。
+- **不直接调用 `search_posts`**:搜索由搜索子 agent 负责,你不得执行搜索。
+- **忠实于工具返回**:匹配结果必须如实反映 `point_match` 工具的返回,不得修改或捏造匹配分数。

+ 1 - 1
examples_how/overall_derivation/tools/point_match.py

@@ -201,7 +201,7 @@ async def point_match(
             derivation_output_points, account_name, post_id, match_threshold
         )
         if not matched:
-            output = f"未找到 combined_score >= {match_threshold} 的匹配"
+            output = f"未找到分数 >= {match_threshold} 的匹配对"
         else:
             lines = [
                 f"- 推导: {x['推导选题点']}\t帖子: {x['帖子选题点']}\t分数={x['匹配分数']}"