liuzhiheng 6 дней назад
Родитель
Сommit
755ee91b23

+ 72 - 85
examples_how/overall_derivation/derivation_main.md

@@ -10,50 +10,37 @@ $system$
 你是专业的图文内容创作者,同时具备内容消费者喜好的感知能力、内容审美判断能力、缜密的逻辑推理能力。
 
 ## 任务描述
-根据**创作者整体人设树**和**创作者创作pattern**,以及**当前已推导成功的选题点**(每轮推导评估后更新),以内容创作者的视角,模仿内容创作者在创作过程中使用 历史创作模式复用/关联联想/信息搜索...等手段方法和过程,将选题点串联成一条完整的选题点推导路径,也即选题点产出的先后依赖步骤。路径由点和有向连线组成,路径中的点表示一个选题点,点A指向点B的有向连线表示创作者由点A通过某种方法(思考/联想/搜索...等)产生了点B。**主 agent 不直接接收帖子单帖解构内容**,仅能使用「已推导成功的选题点」进行推导,符合闭眼推导原则;每轮产出的可能选题点由**评估子 agent** 与帖子解构内容做语义匹配,匹配成功的才加入已推导成功的选题点集合。每轮推导和评估结束后,输出该轮的**推导日志**与**评估日志**到指定目录。
-
-## 输入文件
-
-### 1. 创作者整体人设树
-- **路径**: `input/家有大志/tree/`
-- **文件**: 
-  - `实质_point_tree_how.json`
-  - `形式_point_tree_how.json`
-  - `意图_point_tree_how.json`
-- **作用**: 包含三个维度的人设节点树形结构,用于匹配选题点
-- **节点结构**: 每个节点是一个嵌套的字典结构,包含以下字段:
-  - `t`: 节点类型
-    - `"root"`: 根节点(通常跳过处理)
-    - `"class"`: 分类节点,是多个标签节点根据语义聚合的结果
-    - `"ID"`: 标签节点
-  - `w`: 人设权重分数(浮点数),表示该节点在人设中的重要性
-  - `c`: 是否为全局常量(布尔值)
-    - `true`: 该节点是全局常量,表示节点在当前账号下出现频率极高
-    - `false`: 该节点不是全局常量
-  - `lc`: 是否为局部常量(布尔值)
-    - `true`: 该节点是局部常量,在当前账号中的某个方向下常用
-    - `false`: 该节点不是局部常量
-    - **注意**: 如果`c`为`true`,则不能作为局部常量使用
-  - `r`: 整体概率(浮点数),表示该节点在所有帖子中出现的概率
-  - `n`: 使用该节点的帖子数量(整数)
-  - `ch`: 子节点字典(键为子节点名称,值为子节点对象,结构同父节点)
-- **树形结构**: 
-  - 根节点为维度名称("实质"、"形式"、"意图")
-  - 每个节点可以有多个子节点,形成树形层级结构
-  - 层级从1开始(根节点为0层,通常跳过)
-
-### 2. 创作者创作pattern
-- **路径**: `input/家有大志/pattern/processed_edge_data.json`
-- **作用**: 包含频繁项集模式数据,用于发现选题点之间的关联模式
-- **顶层结构**: 一个 **数组**,元素为 pattern 对象,按 support 降序排列
-- **pattern 对象结构**(每一条):
-  - `s`: 浮点数,支持度(0~1,保留 4 位小数),表示该模式在历史创作中出现的频率
-  - `l`: 整数,模式长度(该 pattern 包含的选题点名称个数)
-  - `i`: 字符串,选题点名称拼接,格式为 `名称1+名称2+名称3`(用 `+` 连接,名称已去重、无顺序区分)
-- **示例**:
-  - `{"s":0.2034,"l":3,"i":"图片文字+产品植入+补充说明式"}` 表示「图片文字、产品植入、补充说明式」三个选题点常一起出现,支持度 0.2034
-  - 使用时可将 `i` 按 `+` 拆分为名称列表,与人设树或已推导选题点做匹配,优先选用 `s` 高、`l` 大的 pattern
+根据**当前已推导成功的选题点**(每轮推导评估后更新),以内容创作者的视角,模仿内容创作者在创作过程中使用 历史创作模式复用/人设推导/信息搜索...等手段方法和过程,将选题点串联成一条完整的选题点推导路径。**主 agent 不在开始时读取人设树与 pattern 文件**,而是在执行每一种推导方法时**调用对应工具**获取数据,由工具返回结果后你负责整理推导路径、填写 `reason` 并输出推导日志。**主 agent 不直接接收帖子单帖解构内容**,仅能使用「已推导成功的选题点」进行推导,符合闭眼推导原则;每轮产出的可能选题点由**评估子 agent**(内部调用 `point_match` 工具)做匹配,匹配成功的才加入已推导成功的选题点集合。每轮推导和评估结束后,输出该轮的**推导日志**与**评估日志**到指定目录。
 
+## 数据获取方式(通过工具)
+
+执行推导方法时,通过以下工具获取数据(**账号名称在本 prompt 最后的 `$user$` 指令中指定**;调用工具时统一使用该 `account_name` 参数)。
+
+### 方法使用前提
+
+- **已推导成功的选题点集合**:由评估子 agent 匹配成功后更新,主 agent 需维护。首轮该集合为空。
+- **人设常量**:不依赖已推导成功选题点,首轮即可调用 `find_tree_constant_nodes`,用于广召回。
+- **人设推导**、**账号pattern复用**:两工具的 `derived_items` **允许为空**。为空时,人设树节点的条件概率取节点自身的 `_ratio`,pattern 的条件概率取自身的 support;非空时按已推导帖子集合计算条件概率。**首轮即可使用**人设推导与账号pattern复用(传空数组或省略即可)。
+- **信息搜索**:任意轮次可调用(通过 derivation_search 子 agent)。
+
+### 工具与参数
+
+| 推导方法     | 调用工具 | 说明 |
+|-------------|----------|------|
+| 人设常量     | `find_tree_constant_nodes` | 仅需 `account_name`,获取人设树的全局/局部常量节点(节点名称、概率、常量类型)。 |
+| 账号pattern复用 | `find_pattern` | 需 `derived_items`(可为空)、条件概率阈值、top_n。为空时条件概率为 pattern 的 support。 |
+| 人设推导     | `find_tree_nodes_by_conditional_ratio` | 需 `derived_items`(可为空)、条件概率阈值、top_n。为空时条件概率为节点的 _ratio。 |
+| 信息搜索     | 调用子 agent | 使用 `agent(task="...", agent_type="derivation_search")`,在 task 中传入本次搜索的 **query**。 |
+
+**derived_items 格式**(当集合非空且需传入时):  
+`find_pattern` 与 `find_tree_nodes_by_conditional_ratio` 的 `derived_items` 可为空数组;非空时每项**建议**为 `topic` + `source_node`(工具不识别 `name`、`node`、`id` 等其它字段):
+- `topic`:已推导成功的**帖子选题点名称**(来自评估匹配后加入集合的名称)
+- `source_node`:该选题点对应的人设树节点名称(推导来源)
+
+示例:`[]`(首轮);或 `[{"topic": "分享", "source_node": "分享"}, {"topic": "图文信息", "source_node": "结构模式"}]`。  
+主 agent 在评估子 agent 返回匹配结果后,按「已推导成功选题点的更新规则」维护集合;后续轮次调用上述两工具时,可从集合及对应推导来源整理出 `derived_items`,首轮可传 `[]`。
+
+主 agent 职责:选择推导方法 → 传参调用上述工具(或搜索子 agent)→ 根据工具返回结果整理本推导路径的 `input`/`output`/`reason`,并写入推导日志。
 
 ## 推导过程
 
@@ -75,31 +62,32 @@ else (否)
   endif
 endif
 
-' 输入文件关联
-note left of 进行单步推导: 输入:人设树文件、人设pattern文件、已推导选题点集合
+' 数据与流程
+note left of 进行单步推导: 输入:已推导选题点集合;人设/pattern 数据通过工具按需获取
 note right of 符合的选题点加入已推导选题点集合: 输出:更新已推导选题点集合
 @enduml
 ```
 
 ### 推导流程
-1. **推导**:以创作者整体人设树、人设 pattern 表、**已推导成功的选题点集合**为输入(不包含帖子单帖解构内容,首轮已推导成功的选题点集合为空),使用下述定义的推导方法,产出**本轮推导出的可能选题点**(含推导过程数据)。
-   - **注意**: 推导的总轮次数是浮动不确定的,不由推导部分决定,而由下面的第2环节验证评估来决定。每轮验证评估结束后,会通知推导环节是否要进行新一轮推导。**总轮次的上限为15轮**。
-2. **评估验证**:调用评估子 agent(evaluate_derivation agent)进行验证。注意:
-   - 使用内置的 `agent` 工具,传入 `agent_type="evaluate_derivation"`,在 `task` 参数中给出:1)历史已推导成功的选题点(JSON);2)本轮推导出的可能选题点(含推导路径ID);3)**帖子ID**。
-   - 评估子 agent 会自动加载本目录下的 skill:`examples_how/overall_derivation/skills/derivation_eval.md` 作为 system prompt;它不直接接收帖子单帖解构选题点数据,而是根据传入的**帖子ID**在内部获取单帖解构选题点数据。
-   - 子 agent 职责:判断本轮推导的选题点与帖子解构选题点是否语义相似或接近,返回匹配结果;判断是否还有未推导成功的选题点,若有则通过 `need_next_round` 告知主 agent。
+1. **推导**:以**已推导成功的选题点集合**为输入(不包含帖子单帖解构内容,首轮为空)。执行某种推导方法时,**先调用对应工具**获取数据,再根据工具返回结果整理并产出**本轮推导出的可能选题点**(含推导过程数据)。  
+   - **首轮**:四种方法均可使用。人设推导、账号pattern复用 的 `derived_items` 可传空数组 `[]`,此时工具分别用节点 _ratio、pattern support 作为条件概率。
+   - **后续轮次**:集合非空后,人设推导、账号pattern复用 可传入 `derived_items`,每项建议为 `{"topic": "帖子选题点名称", "source_node": "人设树节点名称"}`。
+   - **注意**: 推导的总轮次数由第2环节验证评估决定。**总轮次的上限为15轮**。
+2. **评估验证**:调用评估子 agent(evaluate_derivation)进行验证。注意:
+   - 使用内置的 `agent` 工具,传入 `agent_type="evaluate_derivation"`,在 `task` 参数中给出:1)历史已推导成功的选题点(JSON);2)本轮推导出的可能选题点(含推导路径ID);3)**帖子ID**;4)**账号名(account_name)**。
+   - 评估子 agent 会加载 skill `derivation_eval`,在内部**调用 `point_match` 工具**(传入推导选题点列表、账号名、帖子ID)获取匹配结果,再据此整理 `eval_results` 与 `next_round` 并返回。
    - 主 agent 后续动作:根据子 agent 返回的匹配结果更新已推导成功的选题点集合(见「已推导成功选题点的更新规则」);根据 `need_next_round` 决定是否继续推导。
 3. **失败后策略调整**:见「失败恢复与策略调整」章节。
-4. **输出日志**:每轮完成「推导」后,将本轮推导过程按**推导日志**格式写入 `output/家有大志/推导日志/{帖子ID}/%log_id%/{轮次}_推导.json`;每轮完成「评估验证」并收到子 agent 返回后,将返回结果整理为**评估日志**格式,写入 `output/家有大志/推导日志/{帖子ID}/%log_id%/{轮次}_评估.json`。
+4. **输出日志**:每轮完成「推导」后,将本轮推导过程按**推导日志**格式写入 `output/{账号名}/推导日志/{帖子ID}/%log_id%/{轮次}_推导.json`;每轮完成「评估验证」并收到子 agent 返回后,将返回结果整理为**评估日志**格式,写入 `output/{账号名}/推导日志/{帖子ID}/%log_id%/{轮次}_评估.json`。
 
 ### 推导方法的定义
 
 共定义以下 **四种** 推导方法,每条推导路径的 `method` 字段必须使用其中之一:
 
 #### 方法一:人设常量
-- **适用场景**:前几轮推导,已推导成功的选题点集合为空或很少,需要广召回可能的输出选题点
-- **操作方式**:从人设树三个维度中,选择满足以下条件的节点:全局常量(`c=true`)或局部常量(`lc=true`)作为候选输出
-- 模拟样例: 以下是使用“人设常量进行推导所产生的数据解构样例。
+- **适用场景**:前几轮推导,已推导成功的选题点集合为空或很少,需要广召回可能的输出选题点
+- **操作方式**:调用工具 `find_tree_constant_nodes(account_name=account_name)` 获取人设树的全局常量、局部常量节点;根据返回的节点名称、概率、常量类型选择候选输出,整理为本推导路径的 `input`/`output`/`reason`。
+- 模拟样例: 以下是使用「人设常量」进行推导所产生的数据解构样例。
     - 单步推导使用的input信息:
       - 使用的已推导节点:无
       - 使用的pattern:无
@@ -121,13 +109,10 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
       ```
 
 #### 方法二:账号pattern复用
-- **适用场景**:已有部分推导成功的选题点,通过 pattern 数据发现与之共现的其他选题点。
-- **操作方式**:参考 pattern 文件,找到包含**至少一个已推导成功选题点**的 pattern,将该 pattern 中**尚未推导成功的其他选题点**作为候选输出。
-- **优先级**:
-  1. 优先使用支持度(`s`)高的 pattern
-  2. 同等支持度下,优先使用包含节点数量(`l`)多的 pattern
-  3. 优先使用与已推导成功选题点重合数量多的 pattern(重合越多,剩余节点出现的条件概率越高)
-- 模拟样例: 以下是使用“pattern复用”进行推导所产生的数据解构样例。
+- **适用场景**:通过 pattern 数据发现选题点共现关系;首轮或集合为空时也可调用(此时条件概率为 pattern 的 support)。
+- **操作方式**:调用工具 `find_pattern(account_name, derived_items, conditional_ratio_threshold, top_n)`。`derived_items` 可为空数组 `[]`(首轮或广召回时);非空时每项建议为 `{"topic":"帖子选题点名称","source_node":"人设树节点名称"}`,不可使用 `name`、`node`、`id` 等其它字段。根据工具返回的 pattern 名称与条件概率,选取条件概率高的 pattern,将其中尚未推导成功的选题点作为候选输出,整理为本推导路径的 `input`/`output`/`reason`。
+- **优先级**:优先使用条件概率高、pattern 长度(节点数)大的结果;与已推导选题点重合多的 pattern 更优先。
+- 模拟样例: 以下是使用「账号pattern复用」进行推导所产生的数据解构样例。
       - 单步推导使用的input信息:
         - 使用的已推导节点:图文信息
         - 使用的pattern:“图文信息+补充说明”
@@ -148,22 +133,19 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
     }
     ```
 
-#### 方法三:人设联想
-- **适用场景**:已有部分推导成功的选题点,通过人设树的层级结构联想到相关节点。
-- **操作方式**:参考人设树文件,基于以下关系进行推导:
-  - **父→子联想**:已推导成功的选题点A是人设树中某节点,其子节点B在父节点中占比高(`w` 值高或 `r` 值高),则A可推导出B
-  - **兄弟联想**:已推导成功的选题点A与节点B是同一父节点下的兄弟节点,且B的占比高,则可从A+父节点推导出B
-- **注意事项**:联想必须基于人设树中的具体数据(节点关系、权重、频率),不得使用大模型自身的世界知识进行联想。
-- 模拟样例: 以下是使用“人设联想”进行推导所产生的数据解构样例。
+#### 方法三:人设推导
+- **适用场景**:通过人设树条件概率关联推导相关节点;首轮或集合为空时也可调用(此时条件概率为节点自身的 _ratio)。
+- **操作方式**:调用工具 `find_tree_nodes_by_conditional_ratio(account_name, derived_items, conditional_ratio_threshold, top_n)`。`derived_items` 可为空数组 `[]`(首轮或广召回时);非空时每项建议为 `{"topic":"帖子选题点名称","source_node":"人设树节点名称"}`,不可使用 `name`、`node`、`id` 等其它字段。根据工具返回的节点名称、条件概率、父节点名称,选取条件概率高的节点作为候选输出,整理为本推导路径的 `input`/`output`/`reason`。推导理由须引用条件概率等数据,不得使用大模型自身世界知识联想。
+- 模拟样例: 以下是使用「人设推导」进行推导所产生的数据解构样例。
     - 单步推导使用的input信息:
       - 使用的已推导节点:图文信息
       - 使用的pattern:无
       - 使用的人设树节点:图文信息
-    - 使用的推导方法:人设联想
+    - 使用的推导方法:人设推导
     - 单步推导产出的output节点:补充说明
       ```json
       {
-        "method": "人设联想",
+        "method": "人设推导",
         "input": {
           "tree_nodes": ["图文信息"],
           "patterns": [],
@@ -177,13 +159,14 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 
 #### 方法四:信息搜索
 - **适用场景**:方法二和方法三均难以推导出新选题点时,或需要验证某个推导假设时。
-- **操作方式**:调用内置搜索工具 `search_posts`,通过社交平台搜索获取信息
+- **操作方式**:**不直接调用 `search_posts`**。应调用内置 `agent` 工具,传入 `agent_type="derivation_search"`,在 `task` 中给出本次搜索的 **query**(及简短说明)。子 agent 会在内部调用 `search_posts` 执行搜索,并将结果摘要与原始数据返回给你;你根据返回结果整理本推导路径的 `input`/`output`/`reason` 及 `tools` 字段(记录 query、result、raw_result)
 - **搜索流程**:
   1. **搜索需求构造**:明确本次搜索希望发现什么信息。
-  2. **搜索 query 构造(闭眼搜索)**:query 中使用的关键词**只能来自**已推导成功的选题点名称或人设树中的节点名称。**禁止**使用大模型自行推测或联想出的关键词。**禁止使用账号名称**
-  3. **搜索结果评估**:逐条分析搜索结果,判断是否包含可用于推导的新选题点信息。若不包含,尝试构造新的 query(仍需闭眼)或请求更多结果。
-- **注意事项**:每次执行信息搜索方法必须调用 `search_posts` 执行搜索,而不能把历史搜索结果拿出来再次使用
-- 模拟样例: 以下是使用“信息搜索”进行推导所产生的数据解构样例。
+  2. **搜索 query 构造(闭眼搜索)**:query 中使用的关键词**只能来自**已推导成功的选题点名称或人设树中的节点名称。**禁止**使用大模型自行推测或联想出的关键词。**禁止使用账号名称**。
+  3. **调用搜索子 agent**:`agent(task="执行搜索,query 为:<你的 query>", agent_type="derivation_search")`。
+  4. **根据子 agent 返回**:逐条分析返回的搜索结果,判断是否包含可用于推导的新选题点,整理为推导路径输出。
+- **注意事项**:每次执行信息搜索方法必须通过调用 `derivation_search` 子 agent 执行一次搜索,不得复用历史搜索结果。
+- 模拟样例: 以下是使用「信息搜索」进行推导所产生的数据解构样例。
     - 单步推导使用的input信息:
       - 使用的已推导节点:图文信息、夸张呈现
       - 使用的pattern:无
@@ -202,10 +185,10 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
         "reason": "根据已推导出的“图文信息”、“夸张呈现”,结合人设中相关的“创意改造”进行外部搜索,搜索结果中主要包含了家居改造利用、废旧物品利用等信息",
         "tools": [
           {
-            "name": "search_posts",
+            "name": "agent(derivation_search)",
             "query": "图文信息 夸张呈现 创意改造",
-            "result": "(对原始搜索结果的总结,若为搜索工具则记录搜索返回的数据摘要或关键内容)",
-            "raw_result": "(每条搜索原始结果,可能是帖子、视频等,若为搜索工具则记录搜索工具返回的原始数据(完整保留或按需截断)"
+            "result": "(搜索子 agent 返回的摘要或关键内容)",
+            "raw_result": "(搜索子 agent 返回的原始搜索结果,完整保留或按需截断)"
           }
         ]
       }
@@ -213,6 +196,10 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 
 ### 推导策略
 
+#### 首轮与后续轮次
+- **首轮**:四种推导方法均可使用。人设推导、账号pattern复用 可传 `derived_items=[]`,此时条件概率分别为节点 _ratio、pattern support。
+- **后续轮次**:集合非空后,人设推导、pattern 复用可传入非空 `derived_items`(每项 `topic`+`source_node`),获得基于已推导帖子的条件概率;仍可传空以做广召回。
+
 #### 前期广召回,逐步收敛
 - 前期推导因为缺少方向,先尽可能输出多的选题点,利用评估子agent的匹配结果,再逐步收敛
 
@@ -248,7 +235,7 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 - **多种推导方法循环交替使用**: 
   - 不应在一轮中只使用单一方法。每轮推导应至少尝试2种不同的方法,以增加覆盖面。
   - 在推导过程中,使用某种推导方法无法推导出新的选题点时,则尝试调用另外的推导方法。若能推导出新的选题点,则可以再重新尝试使用之前无法推导出新选题点的方法进行推导尝试。
-- **信息搜索的触发调用时机**: 在推导过程中,若使用“pattern模式复用”方法及“人设联想”方法都无法推导出新的帖子选题点时,需要调用“信息搜索”方法来尝试推导。
+- **信息搜索的触发调用时机**: 在推导过程中,若使用「账号pattern复用」与「人设推导」都无法推导出新的帖子选题点时,需调用「信息搜索」子 agent 尝试推导。
 - **每一推导步骤需尽可能多尝试**: 包括选起点的所有推导步骤,均要尝试调用尽可能多的推导方法,并且每种方法要尝试多种输入信息的可能,输入信息的组合可以不局限于1-2种可能性。
 - **避免重复推导**: 不要重复推导已经在之前轮次中被判定为「不匹配」的选题点(除非使用了完全不同的推导方法和输入组合)。维护一个失败选题点列表,避免无效重复。
   - 维护一个 failed_points 列表,每轮推导前先检查,已失败的点不得再次输出
@@ -268,9 +255,9 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
    - **禁止**在推导理由中引用评估子 agent 的反馈内容(如"评估子agent提示..."、"上一轮评估显示..."等)。
 
 2. **禁止自由联想**:
-   - 推导的路径步骤和理由,必须基于人设树文件和创作者 pattern 文件中的具体数据。
+   - 推导的路径步骤和理由,必须基于**工具返回**的人设树、pattern 或搜索子 agent 返回的具体数据。
    - **禁止**使用大模型自身的世界知识或联想信息进行推导。
-   - 每条推导理由中必须明确引用所使用的数据来源(如:某节点的 `r` 值、某 pattern 的 `s` 值等)。
+   - 每条推导理由中必须明确引用所使用的数据来源(如:工具返回的节点概率、条件概率、pattern 条件概率或搜索摘要等)。
 
 3. **不强制包含所有选题点**:
    - 可能存在某些选题点无法通过上述推导方法以合理理由推导出。
@@ -282,7 +269,7 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 每轮推导结束后写入**推导日志**,每轮评估子 agent 返回后写入**评估日志**。路径中的 `{帖子ID}`、`log_id`(运行前生成并替换)、`{轮次}` 由实际执行时替换。
 
 ### 1. 推导日志(每轮一份)
-- **路径**: `output/家有大志/推导日志/{帖子ID}/%log_id%/{轮次}_推导.json`
+- **路径**: `output/{账号名}/推导日志/{帖子ID}/%log_id%/{轮次}_推导.json`
 - **作用**: 记录该轮推导的详细过程,便于追溯与可解释性
 - **格式要求**:
 ```json
@@ -328,7 +315,7 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 **注意**: 每条推导路径遵循最小输入输出原子化规则:即用最少输入数据可以推导出哪些必要的选题点;从数据上看,每一条推导路径中的所有输入都是输出每个选题点的必要输入数据;逻辑上可以分开推导路径不要混在一起。
 
 ### 2. 评估日志(每轮一份)
-- **路径**: `output/家有大志/推导日志/{帖子ID}/%log_id%/{轮次}_评估.json`
+- **路径**: `output/{账号名}/推导日志/{帖子ID}/%log_id%/{轮次}_评估.json`
 - **作用**: 记录该轮评估结果与推导进度,**内容由调用评估子 agent 的返回结果整理得到**
 - **格式要求**:
 ```json
@@ -387,4 +374,4 @@ note right of 符合的选题点加入已推导选题点集合: 输出:更新
 4. 推导理由中不包含对评估子 agent 反馈的引用
 
 $user$
-请开始执行 帖子ID=68fb6a5c000000000302e5de 的选题点整体推导任务。所有路径均相对于项目根目录。
+请开始执行 账号名称=家有大志,帖子ID=68fb6a5c000000000302e5de 的选题点整体推导任务。所有路径均相对于项目根目录。

+ 22 - 7
examples_how/overall_derivation/overall_derivation_agent_run.py

@@ -23,7 +23,6 @@ from pathlib import Path
 sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 
 from dotenv import load_dotenv
-load_dotenv()
 
 from agent.llm.prompts import SimplePrompt
 from agent.core.runner import AgentRunner, RunConfig
@@ -36,6 +35,8 @@ from agent.trace import (
 from agent.llm import create_openrouter_llm_call
 from agent.trace.compaction import build_reflect_prompt
 
+load_dotenv()
+
 
 # ===== 非阻塞 stdin 检测 =====
 if sys.platform == 'win32':
@@ -214,12 +215,11 @@ async def main():
     args = parser.parse_args()
 
     base_dir = Path(__file__).parent
-    project_root = base_dir.parent.parent
     prompt_path = base_dir / "derivation_main.md"
     output_dir = base_dir / "output"
     output_dir.mkdir(exist_ok=True)
 
-    # 加载项目级 presets(evaluate_derivation 等)
+    # 加载项目级 presets(evaluate_derivation、derivation_search 等)
     presets_path = base_dir / "presets.json"
     if presets_path.exists():
         import json
@@ -229,6 +229,21 @@ async def main():
             register_preset(name, AgentPreset(**cfg))
         print(f"   - 已加载项目 presets: {list(project_presets.keys())}")
 
+    # 注册选题点推导专用工具(主 agent 与评估子 agent 会调用)
+    import importlib.util
+    tools_dir = base_dir / "tools"
+    for mod_name, file_name in [
+        ("find_tree_node", "find_tree_node.py"),
+        ("find_pattern", "find_pattern.py"),
+        ("point_match", "point_match.py"),
+    ]:
+        path = tools_dir / file_name
+        if path.is_file():
+            spec = importlib.util.spec_from_file_location(f"overall_derivation.{mod_name}", path)
+            mod = importlib.util.module_from_spec(spec)
+            spec.loader.exec_module(mod)
+            print(f"   - 已注册推导工具: {mod_name}")
+
     skills_dir = str(base_dir / "skills")
 
     print("=" * 60)
@@ -315,7 +330,7 @@ async def main():
                 check_trace = await store.get_trace(current_trace_id)
                 if check_trace and check_trace.status in ("completed", "failed"):
                     if check_trace.status == "completed":
-                        print(f"\n[Trace] ✅ 已完成")
+                        print("\n[Trace] ✅ 已完成")
                         print(f"  - Total messages: {check_trace.total_messages}")
                         print(f"  - Total cost: ${check_trace.total_cost:.4f}")
                     else:
@@ -383,14 +398,14 @@ async def main():
                         if item.status == "running":
                             print(f"[Trace] 开始: {item.trace_id[:8]}...")
                         elif item.status == "completed":
-                            print(f"\n[Trace] ✅ 完成")
+                            print("\n[Trace] ✅ 完成")
                             print(f"  - Total messages: {item.total_messages}")
                             print(f"  - Total tokens: {item.total_tokens}")
                             print(f"  - Total cost: ${item.total_cost:.4f}")
                         elif item.status == "failed":
                             print(f"\n[Trace] ❌ 失败: {item.error_message}")
                         elif item.status == "stopped":
-                            print(f"\n[Trace] ⏸️ 已停止")
+                            print("\n[Trace] ⏸️ 已停止")
 
                     elif isinstance(item, Message):
                         current_sequence = item.sequence
@@ -401,7 +416,7 @@ async def main():
                                 tool_calls = content.get("tool_calls")
                                 if text and not tool_calls:
                                     final_response = text
-                                    print(f"\n[Response] Agent 回复:")
+                                    print("\n[Response] Agent 回复:")
                                     print(text)
                                 elif text:
                                     preview = text[:150] + "..." if len(text) > 150 else text

+ 7 - 1
examples_how/overall_derivation/presets.json

@@ -3,6 +3,12 @@
     "max_iterations": 30,
     "temperature": 0.2,
     "skills": ["planning", "derivation_eval"],
-    "description": "选题点推导评估子 Agent,用于判断推导选题点与帖子解构选题点是否语义匹配,并判断是否需下一轮推导"
+    "description": "选题点推导评估子 Agent,调用 point_match 获取匹配结果并整理为规定 JSON,判断是否需下一轮推导"
+  },
+  "derivation_search": {
+    "max_iterations": 15,
+    "temperature": 0.2,
+    "skills": ["planning", "derivation_search"],
+    "description": "选题点推导-信息搜索子 Agent,根据主 agent 的 query 调用 search_posts 并返回结果"
   }
 }

+ 39 - 56
examples_how/overall_derivation/skills/derivation_eval.md

@@ -1,49 +1,45 @@
 ---
 name: derivation_eval
-description: 选题点推导评估任务 - 判断推导产出的选题点与帖子解构选题点是否语义一致
+description: 选题点推导评估任务 - 调用 point_match 工具获取匹配结果并整理为规定 JSON
 ---
 
 # 选题点推导评估任务
 
 ## 角色
-你是选题点匹配评估专家,负责判断「推导产出的选题点」与「帖子解构内容选题点」是否语义一致或高度接近
+你是选题点推导的**评估执行者**,负责调用 **point_match** 工具得到「推导选题点」与「帖子选题点」的匹配结果,并据此整理成主 agent 要求的 JSON,**不**由模型自行判断语义相似度
 
 ## 任务描述
-根据**本轮推导出的可能选题点**(含推导过程数据)和**帖子解构内容选题点**,逐条判断:本轮推导的每个选题点是否对应帖子解构内容中的某个选题点。语义相似或接近即视为匹配,不要求字面完全一致。
-
-## 输入
-
-### 1. 历史已推导成功的选题点
-由主 agent 传入,用于计算推导进度、判断是否需要进行下一轮推导。
-
-### 2. 本轮推导出的可能选题点(含推导路径ID)
-由主 agent 传入。
-仅基于「本轮推导出的选题点」的语义与帖子解构选题点做匹配判断。
-
-### 3. 帖子解构内容选题点
-- **来源**:本评估子 agent **不直接接收**帖子解构内容选题点数据。主 agent 传入的是**帖子ID**;你需根据帖子ID 自行读取文件:`input/家有大志/post_topic/{帖子ID}.json` 帖子解构内容选题点数据。
-- **作用**: 该 JSON 包含帖子的选题点数据。
-
-## 匹配判定标准
-
-### 匹配(is_matched = true)
-
-两个选题点之间存在以下任一关系时,判定为匹配:
-
-1. **完全一致**:字面相同或仅有细微措辞差异(如「递进式」↔「递进式」)。
-2. **同义表述**:用不同词汇表达相同含义(如「受损状态」↔「被啃坏」,两者都描述物品被破坏的状态)。
-3. **同一对象的不同侧面描述**:两个词指向同一个具体事物或元素,只是描述角度不同(如「柴犬主角」↔「柴犬形象」,都指帖子中的柴犬这一核心元素)。
-4. **同一语义域内的上下位关系,下位词可以匹配帖子中的上位词,但上位词不能匹配帖子中的下位词**:如推导出的选题点「柴犬」可以匹配帖子中的「宠物」,但推导出的选题点「宠物」不能匹配帖子中的「柴犬」。
-
-### 判定原则
-
-- **从严判定**:当不确定是否匹配时,倾向于判定为不匹配。宁可漏匹配,不可错匹配。错误的匹配会污染主 agent 的已推导成功集合,影响后续推导质量。
-- **一对一匹配**:每个帖子选题点最多被一个推导选题点匹配。若本轮有多个推导选题点都可能对应同一个帖子选题点,只保留语义最接近的一个,其余判为不匹配。
-- **不受历史影响**:每个推导选题点独立判定,不因前几轮的匹配结果而放宽或收紧标准。
+主 agent 会传入:1)历史已推导成功的选题点(JSON);2)本轮推导出的可能选题点(含推导路径 ID);3)**帖子ID**;4)**账号名**。你需要:
+1. 从本轮推导出的可能选题点中提取**所有待评估的选题点名称**(即每条推导路径的 `output` 列表展平),并保留每个选题点对应的推导路径 **id**。
+2. **调用工具 `point_match`**:传入 `derivation_output_points`(本轮待评估选题点名称列表)、`account_name`、`post_id`。工具会读取帖子选题点,用相似度计算匹配,返回匹配成功的列表(每项含 推导选题点、帖子选题点、匹配分数)。
+3. 根据 **point_match 的返回** 和「帖子选题点总数」(见下)整理出规定的 **eval_results** 与 **next_round**,**仅输出一段合法 JSON**,不输出任何其它说明文字。
+
+## 输入(由主 agent 在 task 中提供)
+- 历史已推导成功的选题点(JSON 或列表)
+- 本轮推导出的可能选题点:通常为列表,每项含 `id`(推导路径ID)、`output`(选题点名称列表)
+- 帖子ID、账号名
+
+## 操作步骤
+
+1. **解析 task**:从主 agent 的 task 中提取 历史已推导成功选题点、本轮推导选题点(含 id 与 output)、帖子ID、账号名。
+2. **构造待评估列表**:将本轮每条推导路径的 `output` 展平为选题点名称列表,并记录每个名称对应的路径 `id`(同一路径下多个 output 共用同一 id)。
+3. **调用 point_match**:
+   - `derivation_output_points`:上一步得到的选题点名称列表(可含重复 id 的多个点)
+   - `account_name`:主 agent 传入的账号名
+   - `post_id`:主 agent 传入的帖子ID
+4. **获取帖子选题点总数**:为计算 `need_next_round`,需知道帖子选题点总数 N。可调用 **read_file** 读取 `examples_how/overall_derivation/input/{账号名}/post_topic/{帖子ID}.json`(路径相对项目根),该文件为选题点名称的 JSON 数组,N = 数组长度。**不得在最终返回的 JSON 中包含 N 或 post_topic_count**。
+5. **整理 eval_results**:
+   - 对「本轮每一个推导选题点」(按 id + derivation_topic_name)各一条记录。
+   - 若该选题点出现在 **point_match 返回的匹配列表**中(匹配列表项中的「推导选题点」等于该名称),则:`is_matched: true`,`matched_post_topic` 填该匹配项中的「帖子选题点」,`matched_reason` 可填 `"point_match 工具匹配,分数={匹配分数}"`。
+   - 若未出现在匹配列表中,则:`is_matched: false`,`matched_post_topic: null`,`matched_reason: null`。
+   - `id` 与主 agent 传入的推导路径 ID 一致;`derivation_topic_name` 为本轮推导的选题点名称。
+6. **整理 next_round**:
+   - `derived_success_count` = 历史已推导成功数量 + 本轮 `is_matched` 为 true 的数量。
+   - `need_next_round` = (`derived_success_count` < 帖子选题点总数 N)。**不要在 JSON 中输出 N**。
 
 ## 输出要求
 
-**必须返回合法 JSON**,供主 agent 解析并更新「已推导成功的选题点」集合。不要输出任何 JSON 之外的说明文字
+**必须返回合法 JSON**,且**仅输出该 JSON**,供主 agent 解析。不要在 JSON 前后添加任何说明、markdown 代码块或注释。
 
 ### JSON 结构
 
@@ -55,7 +51,7 @@ description: 选题点推导评估任务 - 判断推导产出的选题点与帖
       "derivation_topic_name": "本轮推导的选题点名称",
       "is_matched": true,
       "matched_post_topic": "帖子解构中匹配到的选题点名称",
-      "matched_reason": "匹配成功理由,适用规则"
+      "matched_reason": "point_match 工具匹配,分数=0.xx"
     },
     {
       "id": 2,
@@ -73,27 +69,14 @@ description: 选题点推导评估任务 - 判断推导产出的选题点与帖
 ```
 
 ### 字段说明
+- **eval_results**:对本轮每个推导选题点各一条;是否匹配**完全依据 point_match 工具的返回**,不要用模型自行判断语义。
+- **next_round.derived_success_count**:截至本轮结束的累计已推导成功数量。
+- **next_round.need_next_round**:是否还需下一轮推导(由 derived_success_count 与帖子选题点总数 N 比较得出,N 不写入 JSON)。
 
-- **eval_results**:对「本轮推导出的每个选题点」各一条记录。
-  - `id`:整数,主 agent 传递的推导路径 ID。注意:同一个 `id` 下可能有多个输出选题点,需为每个输出选题点各生成一条记录,`id` 保持与推导路径一致。
-  - `derivation_topic_name`:字符串,本轮推导的选题点名称(与主 agent 传入的 output 字段中的选题点名称一致)。
-  - `is_matched`:布尔值(`true` 或 `false`),表示是否匹配成功。
-  - `matched_post_topic`:字符串或 `null`。匹配成功时填写帖子解构中对应的选题点名称;不匹配时为 `null`。
-  - `matched_reason`:字符串或 `null`。只有匹配成功才需要输出理由和适用规则,不匹配时为 `null`。
-
-- **next_round**:推导进度与续推判断。
-  - `derived_success_count`:整数,截至本轮结束时,累计已推导成功的选题点数量(= 历史已推导成功数量 + 本轮新匹配成功数量)。
-  - `need_next_round`:布尔值(`true` 或 `false`),当 `derived_success_count` 小于帖子选题点总数时为 `true`,否则为 `false`。
-
-### 关于 next_round 的信息隔离要求
-
-- **禁止**在返回结果中包含帖子选题点总数(`post_topic_count`)。该数字会泄露帖子信息规模,辅助主 agent 推断未匹配选题点。主 agent 只需要知道「是否继续」,不需要知道「还差多少」。
-- **禁止**在返回结果中包含任何未推导成功的帖子选题点信息,包括:选题点名称、数量提示、分类提示、难度提示等任何形式的暗示。
-- **禁止**在返回结果中添加任何文字说明、建议、提示或评论。返回内容必须且仅为上述 JSON 结构。
+### 信息隔离
+- **禁止**在返回结果中包含帖子选题点总数、未推导成功的选题点名称或任何暗示。
+- **禁止**在返回结果中添加任何文字说明、建议或评论。
 
 ## 约束
-
-1. **仅做匹配判断**:不修改、不补充、不评价帖子解构内容。
-2. **不臆造选题点**:若推导点无法对应到任一帖子选题点,判为不匹配。不得为了提高匹配率而降低判定标准。
-3. **严格信息隔离**:返回内容中不得包含任何可帮助主 agent 推断未匹配选题点的信息。这是本评估任务的核心安全约束。
-4. **输出纯 JSON**:输出必须为合法 JSON,且仅输出该 JSON,便于主 agent 直接解析。不要在 JSON 前后添加任何说明文字、markdown 代码块标记或注释。
+1. **匹配结果以 point_match 为准**:不直接用模型评估语义相似度;只有 point_match 返回的匹配才记为 `is_matched: true`。
+2. **输出纯 JSON**:仅输出上述结构的一段 JSON,便于主 agent 直接解析。

+ 35 - 0
examples_how/overall_derivation/skills/derivation_search.md

@@ -0,0 +1,35 @@
+---
+name: derivation_search
+description: 选题点推导-信息搜索子 Agent,根据主 agent 传入的 query 调用 search_posts 执行搜索并返回结果
+---
+
+# 选题点推导 - 信息搜索子任务
+
+## 角色
+你是选题点推导流程中的**信息搜索执行者**,仅负责根据主 agent 传入的搜索 query 执行一次搜索,并结构化返回结果。
+
+## 任务描述
+主 agent 在采用「信息搜索」推导方法时,会调用你(`agent_type="derivation_search"`),并在 `task` 中给出本次搜索的 **query**(以及必要的简短说明)。你的职责只有两点:
+1. **执行搜索**:使用内置工具 `search_posts`,以主 agent 提供的 query 作为搜索关键词执行一次搜索。
+2. **返回结果**:将搜索结果的**摘要**(可读的关键信息)和**原始数据**(或截断后的原始结果)整理成一段清晰文本,返回给主 agent,供其整理推导路径的 `output`/`reason`/`tools` 字段。
+
+## 输入
+- **task**:主 agent 传入的任务描述,其中**必须包含**本次搜索的 query(关键词)。例如:
+  - "执行搜索,query 为:图文信息 夸张呈现 创意改造"
+  - "搜索 query:分享 生活记录"
+
+## 操作步骤
+1. 从 `task` 中提取搜索 **query**(关键词)。若 task 为自然段,则识别其中明确给出的 query 并直接使用,**不要**自行改写或扩展关键词。
+2. 调用工具 **search_posts**,传入该 query(及你认为合理的条数等参数),执行一次搜索。
+3. 根据 `search_posts` 的返回结果:
+   - 写一段**摘要**:概括搜索结果中与选题点相关的关键信息(主题、高频词、可推导方向等)。
+   - 保留或截断**原始数据**:便于主 agent 写入推导日志的 `tools[].raw_result`。
+4. 在回复中明确给出:
+   - **query**:本次实际使用的搜索关键词。
+   - **result**:上述摘要。
+   - **raw_result**:原始搜索结果(可适当截断以控制长度)。
+
+## 约束
+- **仅执行一次搜索**:每次被调用只调用一次 `search_posts`,不要多轮搜索或合并历史结果。
+- **闭眼搜索**:query 必须来自主 agent 的 task 描述,不得自行编造或联想新关键词。
+- **不替主 agent 做推导**:你只负责执行搜索并返回结果,不判断“能推导出哪些选题点”;由主 agent 根据你的返回整理推导路径。

+ 21 - 19
examples_how/overall_derivation/tools/find_pattern.py

@@ -126,6 +126,7 @@ def get_patterns_by_conditional_ratio(
 ) -> list[dict[str, Any]]:
     """
     从 pattern 库中获取条件概率 >= 阈值的 pattern,按条件概率降序(同分按 length 降序),返回 top_n 条。
+    derived_list 为空时,条件概率使用 pattern 自身的 support(s)。
     返回每项:pattern名称(nameA+nameB+nameC)、条件概率。
     """
     merged = _load_and_merge_patterns(account_name)
@@ -133,14 +134,21 @@ def get_patterns_by_conditional_ratio(
         return []
     base_dir = _BASE_INPUT
     scored: list[tuple[dict, float]] = []
-    for p in merged:
-        # calc_pattern_conditional_ratio 需要 pattern 含 "i" 与 "post_count"
-        ratio = calc_pattern_conditional_ratio(
-            account_name, derived_list, p, base_dir=base_dir
-        )
-        if ratio >= conditional_ratio_threshold:
-            scored.append((p, ratio))
-    # 条件概率从高到低;相等按 length 降序
+
+    if not derived_list:
+        # derived_items 为空:条件概率取 pattern 本身的 support (s)
+        for p in merged:
+            ratio = float(p.get("s", 0))
+            if ratio >= conditional_ratio_threshold:
+                scored.append((p, ratio))
+    else:
+        for p in merged:
+            ratio = calc_pattern_conditional_ratio(
+                account_name, derived_list, p, base_dir=base_dir
+            )
+            if ratio >= conditional_ratio_threshold:
+                scored.append((p, ratio))
+
     scored.sort(key=lambda x: (-x[1], -x[0]["l"]))
     result = []
     for p, ratio in scored[:top_n]:
@@ -153,7 +161,8 @@ def get_patterns_by_conditional_ratio(
 
 @tool(
     description="从 pattern 库中获取符合条件概率阈值的 pattern。"
-    "输入:账号名、已推导选题点列表(DerivedItem)、条件概率阈值、topN。"
+    "输入:账号名、已推导选题点列表(可为空)、条件概率阈值、topN。"
+    "derived_items 为空时,条件概率使用 pattern 自身的 support。"
     "返回:pattern 名称(nameA+nameB+nameC)及条件概率,按条件概率从高到低最多 topN 条。"
 )
 async def find_pattern(
@@ -165,9 +174,8 @@ async def find_pattern(
 ) -> ToolResult:
     """
     从 pattern 库中获取符合条件概率阈值的 pattern。
-
-    已推导选题点 derived_items:每项为 {"topic": "已推导选题点", "source_node": "推导来源人设树节点"}。
-    流程:读取 pattern 库 → 合并去重 → 计算条件概率 → 筛选 ≥ 阈值 → 按条件概率降序(同分按 length 降序)→ 返回 top_n 条。
+    derived_items:可为空;非空时每项为 {"topic": "已推导选题点", "source_node": "推导来源人设树节点"}。
+    当 derived_items 为空时,各 pattern 的条件概率取其 support(s);非空时按已推导帖子集合计算条件概率。
     返回每条:pattern名称(nameA+nameB+nameC)、条件概率。
     """
     pattern_path = _pattern_file(account_name)
@@ -178,13 +186,7 @@ async def find_pattern(
             error="Pattern file not found",
         )
     try:
-        derived_list = _parse_derived_list(derived_items)
-        if not derived_list:
-            return ToolResult(
-                title="参数无效",
-                output="derived_items 不能为空,且每项需包含 topic 与 source_node(或 已推导的选题点 与 推导来源人设树节点)",
-                error="Invalid derived_items",
-            )
+        derived_list = _parse_derived_list(derived_items or [])
         items = get_patterns_by_conditional_ratio(
             account_name, derived_list, conditional_ratio_threshold, top_n
         )

+ 27 - 20
examples_how/overall_derivation/tools/find_tree_node.py

@@ -113,21 +113,32 @@ def get_nodes_by_conditional_ratio(
 ) -> list[dict[str, Any]]:
     """
     获取人设树中条件概率 >= threshold 的节点,按条件概率降序,返回前 top_n 个。
-    derived_list: 已推导列表,每项 (已推导的选题点, 推导来源人设树节点)。
+    derived_list: 已推导列表,每项 (已推导的选题点, 推导来源人设树节点);为空时使用节点自身的 _ratio 作为条件概率
     返回列表项:节点名称、条件概率、父节点名称。
     """
     base_dir = _BASE_INPUT
-    node_to_parent: dict[str, str] = {}
-    for node_name, parent_name, _ in _iter_all_nodes(account_name):
-        node_to_parent[node_name] = parent_name
-
     scored: list[tuple[str, float, str]] = []
-    for node_name, parent_name in node_to_parent.items():
-        ratio = calc_node_conditional_ratio(
-            account_name, derived_list, node_name, base_dir=base_dir
-        )
-        if ratio >= threshold:
-            scored.append((node_name, ratio, parent_name))
+
+    if not derived_list:
+        # derived_items 为空:条件概率取节点本身的 _ratio
+        for node_name, parent_name, node in _iter_all_nodes(account_name):
+            ratio = node.get("_ratio")
+            if ratio is None:
+                ratio = 0.0
+            else:
+                ratio = float(ratio)
+            if ratio >= threshold:
+                scored.append((node_name, ratio, parent_name))
+    else:
+        node_to_parent: dict[str, str] = {}
+        for node_name, parent_name, _ in _iter_all_nodes(account_name):
+            node_to_parent[node_name] = parent_name
+        for node_name, parent_name in node_to_parent.items():
+            ratio = calc_node_conditional_ratio(
+                account_name, derived_list, node_name, base_dir=base_dir
+            )
+            if ratio >= threshold:
+                scored.append((node_name, ratio, parent_name))
 
     scored.sort(key=lambda x: x[1], reverse=True)
     top = scored[:top_n]
@@ -195,7 +206,8 @@ async def find_tree_constant_nodes(
 
 @tool(
     description="获取人设树中条件概率不低于阈值的节点,按条件概率从高到低返回 topN。"
-    "输入:账号名、已推导选题点列表、条件概率阈值、topN。"
+    "输入:账号名、已推导选题点列表(可为空)、条件概率阈值、topN。"
+    "derived_items 为空时,条件概率使用节点自身的 _ratio。"
 )
 async def find_tree_nodes_by_conditional_ratio(
     account_name: str,
@@ -206,7 +218,8 @@ async def find_tree_nodes_by_conditional_ratio(
 ) -> ToolResult:
     """
     获取人设树中符合条件概率阈值的节点。
-    已推导选题点 derived_items:每项为 {\"topic\": \"已推导选题点\", \"source_node\": \"推导来源人设树节点\"}。
+    derived_items:可为空;非空时每项为 {\"topic\": \"已推导选题点\", \"source_node\": \"推导来源人设树节点\"}。
+    当 derived_items 为空时,各节点的条件概率取其自身的 _ratio;非空时按已推导帖子集合计算条件概率。
     返回:节点名称、条件概率、父节点名称,按条件概率降序最多 top_n 条。
     """
     tree_dir = _tree_dir(account_name)
@@ -217,13 +230,7 @@ async def find_tree_nodes_by_conditional_ratio(
             error="Directory not found",
         )
     try:
-        derived_list = _parse_derived_list(derived_items)
-        if not derived_list:
-            return ToolResult(
-                title="参数无效",
-                output="derived_items 不能为空,且每项需包含 topic 与 source_node(或 已推导的选题点 与 推导来源人设树节点)",
-                error="Invalid derived_items",
-            )
+        derived_list = _parse_derived_list(derived_items or [])
         items = get_nodes_by_conditional_ratio(
             account_name, derived_list, conditional_ratio_threshold, top_n
         )