Bladeren bron

how reason update

liuzhiheng 1 maand geleden
bovenliggende
commit
3cd0094de1

+ 93 - 0
examples_how/overall_derivation/data_process/extract_simple_tree_node.py

@@ -0,0 +1,93 @@
+"""
+从「处理后数据/tree」下的人设树 JSON 提取节点名称与 _ratio,输出为带缩进的纯文本,
+便于大模型阅读。输出:input/{account_name}/处理后数据/simple_tree/simple_tree.txt
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+
+def _format_ratio_paren(ratio: Any) -> str:
+    """概率两位小数,拼在节点名后的括号内;无 _ratio 时为 —。"""
+    if ratio is None:
+        return "—"
+    try:
+        return f"{float(ratio):.3f}"
+    except (TypeError, ValueError):
+        return str(ratio)
+
+
+def _walk_node(name: str, node: dict[str, Any], depth: int, lines: list[str]) -> None:
+    if not isinstance(node, dict):
+        return
+    indent = "  " * depth
+    r = node.get("_ratio")
+    lines.append(f"{indent}{name}({_format_ratio_paren(r)})")
+    children = node.get("children")
+    if not isinstance(children, dict):
+        return
+    for child_name, child in children.items():
+        if isinstance(child, dict):
+            _walk_node(str(child_name), child, depth + 1, lines)
+
+
+def _tree_json_to_lines(data: Any) -> list[str]:
+    lines: list[str] = []
+    if not isinstance(data, dict):
+        return lines
+    for root_name, root_node in data.items():
+        if isinstance(root_node, dict):
+            _walk_node(str(root_name), root_node, 0, lines)
+    return lines
+
+
+def extract_simple_tree_for_account(account_name: str) -> Path:
+    base = Path(__file__).resolve().parents[1]
+    tree_dir = base / "input" / account_name / "处理后数据" / "tree"
+    out_dir = base / "input" / account_name / "处理后数据" / "simple_tree"
+    out_file = out_dir / "simple_tree.txt"
+
+    if not tree_dir.is_dir():
+        raise FileNotFoundError(f"目录不存在: {tree_dir}")
+
+    json_paths = sorted(tree_dir.glob("*.json"))
+    if not json_paths:
+        raise FileNotFoundError(f"未找到 JSON 文件: {tree_dir}")
+
+    out_dir.mkdir(parents=True, exist_ok=True)
+
+    chunks: list[str] = []
+    chunks.append("说明: 每行「节点名(概率)」;概率表示该节点在账号下出现的频率;缩进表示层级;root节点无概率用 — 表示。\n分三颗树:\n实质-内容的核心主题/对象\n形式-内容的呈现形式\n意图-内容的目标/用户意图\n")
+
+    for jp in json_paths:
+        chunks.append("\n" + "=" * 72 + "\n")
+        with jp.open(encoding="utf-8") as f:
+            data = json.load(f)
+        lines = _tree_json_to_lines(data)
+        chunks.append("\n".join(lines))
+        chunks.append("\n")
+
+    text = "".join(chunks)
+    out_file.write_text(text, encoding="utf-8")
+    return out_file
+
+
+def main(account_name) -> None:
+    # p = argparse.ArgumentParser(description="人设树节点简化:节点名与概率 节点(0.00)")
+    # p.add_argument("account_name", help="账号目录名,对应 input/{account_name}/")
+    # args = p.parse_args()
+    try:
+        out = extract_simple_tree_for_account(account_name)
+        print(f"已写入: {out}")
+    except FileNotFoundError as e:
+        print(str(e), file=sys.stderr)
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main(account_name="家有大志")

+ 25 - 19
examples_how/overall_derivation/derivation_main.md

@@ -20,7 +20,7 @@ $system$
 | 日志写入时机 | 每轮匹配判断完成后**立即写入**,禁止延迟到任务结束后统一输出 |
 | 闭眼原则 | 工具返回的「帖子选题点匹配」字段只包含匹配成功的选题点,不含失败项;直接读取该字段判断是否匹配成功,**禁止**引用任何未推导成功的选题点信息 |
 | output 与 matched_post_point 的区别 | 推导路径的 `output` 是**工具返回的节点/元素名称,但不是匹配到的帖子选题点名称**(方法二扩展匹配时为扩展节点名称,而非原 pattern 元素);评估日志的 `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 单独一条路径;**禁止**合并独立推导逻辑 |
@@ -34,7 +34,7 @@ $system$
 ## 任务描述
 根据**当前已推导成功的选题点**(每轮推导后更新),以内容创作者的视角,模仿创作者使用历史 pattern 复用、人设推导、信息搜索等推导方法手段,进行**逻辑递进式**的多轮推导,将选题点串联成一条完整的推导路径。每一轮推导都在上一轮已确认结果的基础上向外延伸,推导方向随积累的成功选题点逐步聚焦收敛。
 
-**主 agent 不读取人设树与 pattern 文件**,而是在执行每一种推导方法时**调用对应工具**获取数据,由工具返回结果后负责整理推导路径、填写 `reason` 并输出推导日志。
+**主 agent 在执行每一种推导方法时**调用对应工具**获取数据,由工具返回结果后负责整理推导路径、填写 `reason`(说明为什么从众多工具返回记录中选择该条数据进行推导的决策理由)并输出推导日志。
 
 **主 agent 不直接接收帖子单帖解构内容**,仅能使用「已推导成功的选题点」进行推导,符合闭眼推导原则。
 
@@ -118,7 +118,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 
 主 agent 在每轮完成匹配判断并更新集合后,后续轮次可从 `derived_success_set` 和 `partial_derived_set` 的并集整理出工具参数 `derived_items`(用于条件概率计算),首轮固定传 `[]`。但推导路径的 `input.derived_nodes` 只能引用 `derived_success_set` 中的选题点。
 
-主 agent 职责:选择推导方法 → 传参调用上述工具(或搜索子 agent)→ 根据工具返回结果(或搜索子 agent 返回的匹配结果)整理本推导路径的 `input`/`output`/`reason`,并写入推导日志。
+主 agent 职责:选择推导方法 → 传参调用上述工具(或搜索子 agent)→ 根据工具返回结果(或搜索子 agent 返回的匹配结果)整理本推导路径的 `input`/`output`/`reason`(reason 须说明从众多工具返回记录中选择该条数据的决策理由),并写入推导日志。
 
 ---
 
@@ -225,7 +225,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "derived_nodes": []
       },
       "output": ["分享"],
-      "reason": "'分享'节点是全局常量(c=true)且整体概率 r=0.913,极高,是账号最核心的创作意图起点。",
+      "reason": "工具返回了多个常量节点,选择'分享'是因为:该节点为全局常量(c=true)且整体概率 r=0.913 为所有常量节点中最高,表明'分享'是该账号最核心的创作意图起点;作为首轮推导,优先选取高概率全局常量节点可为后续推导提供最强的基础锚点。",
       "tools": []
     },
     {
@@ -237,7 +237,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "derived_nodes": []
       },
       "output": ["叙事结构"],
-      "reason": "'叙事结构'是局部常量且概率为0.6949,作为账号创作的结构基石。",
+      "reason": "在工具返回的常量节点中选择'叙事结构',理由是:该节点为局部常量且概率 r=0.6949,在账号人设树中属于内容结构维度的高频节点,与已选的'分享'(创作意图维度)互补,能从不同维度扩展推导覆盖面。",
       "tools": []
     }
 ]
@@ -289,7 +289,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
   | 评估日志 `matched_score` | 括号内的数值 | `0.8149` |
   
   **禁止**将 `→` 右边的帖子选题点名称填入 `output` 或 `derivation_output_point`。
-  - **推导理由(`reason`)要求**:对于扩展匹配,`reason` 中须说明扩展节点与原 pattern 元素的关系(如"夸张构图是构图与布局的子节点")。
+  - **推导理由(`reason`)要求**:须说明从工具返回的众多 pattern 中选择该 pattern 的决策理由(如与已推导选题点的关联程度、条件概率、pattern 长度、对未覆盖维度的补充作用等);对于扩展匹配,还须说明扩展节点与原 pattern 元素的关系(如"夸张构图是构图与布局的子节点")。
 - **优先级**:优先使用条件概率高、pattern 长度(节点数)大的结果;与已推导选题点重合多的 pattern 更优先(工具已自动排序)。
 - 模拟样例(基于上方工具返回数据,假设此时 `derived_success_set` 中已包含 `分享`):
 ```json
@@ -309,7 +309,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "output": [
             "日常物品"
         ],
-        "reason": "根据已推导节点'分享',找到账号 pattern '分享+日常物品'(条件概率=0.2203),pattern 中'分享'已推导成功,由此推导出尚未推导的元素'日常物品'。",
+        "reason": "工具返回了多个 pattern,选择'分享+日常物品'是因为:已推导成功的'分享'出现在该 pattern 中,说明该 pattern 与当前推导路径高度相关;虽然条件概率=0.2203 不是最高,但该 pattern 能直接延伸出尚未推导的'日常物品',有助于覆盖物品维度的选题点。",
         "tools": []
     },
     {
@@ -327,7 +327,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "output": [
             "标题", "夸张构图"
         ],
-        "reason": "根据已推导节点'分享',找到平台库 pattern '分享+构图与布局+标题'(条件概率=0.8571),pattern 中'分享'已推导成功;'标题'直接匹配到帖子选题点;'构图与布局'未直接匹配,但其子节点'夸张构图'匹配到帖子选题点'夸张堆叠'(分数=0.8149),因此将'标题'和'夸张构图'作为推导输出。",
+        "reason": "在工具返回的多个 pattern 中,选择'分享+构图与布局+标题'的理由是:(1)条件概率=0.8571 在所有返回 pattern 中最高,说明该组合在账号历史帖子中共现频率极高;(2)该 pattern 包含 3 个元素(长 pattern),能一次推导出更多候选点;(3)已推导成功的'分享'在其中,剩余的'标题'和'构图与布局'分别对应内容形式和视觉呈现维度,与当前已推导的创作意图维度形成多维度交叉覆盖。其中'构图与布局'未直接匹配,但其子节点'夸张构图'匹配到帖子选题点(分数=0.8149),因此将'标题'和'夸张构图'作为推导输出。",
         "tools": []
     }
 ]
@@ -388,7 +388,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
   - **匹配判断**:读取「帖子选题点匹配」字段——若有值(如 `夸张道具(0.7831)`),则 `is_matched=true`,评估日志中 `matched_post_point` 填写括号前的帖子选题点名称(如 `夸张道具`),`matched_score` 填写匹配分数数值(如 `0.7831`),`matched_reason` 填写匹配分数描述(如 `匹配分数=0.7831`);若字段值为「无」,则 `is_matched=false`。
   - **匹配分数阈值判断**:`matched_score >= 0.78` 为完全推导成功,`matched_score < 0.78` 为部分推导成功。例如 `趣味道具→夸张道具(0.7831)` 中 `matched_score=0.7831 >= 0.78`,属于完全推导成功。
   - ⚠️ **关键区分**:`output` 是人设树节点名称(`趣味道具`),`matched_post_point` 是帖子中真实的选题点名称(`夸张道具`)——两者**可以不同**,加入 `derived_success_set` 或 `partial_derived_set` 的是 `matched_post_point`,而非 `output`。
-  - 推导理由须引用 已推导维度、条件概率 等数据,**禁止**使用大模型自身世界知识联想。
+  - 推导理由须说明从工具返回的众多节点中选择该节点的决策依据,包括:与已推导选题点及已推导维度的关联性、该节点在人设树中的位置与条件概率等数据支撑,**禁止**使用大模型自身世界知识联想。
 - 模拟样例(基于上方工具返回数据):
 ```json
 [
@@ -405,7 +405,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "output": [
             "趣味道具"
         ],
-        "reason": "在已推导出的维度'物品'下,'趣味道具'节点条件概率=0.125,工具返回该节点存在帖子选题点匹配,因此将其作为推导候选。",
+        "reason": "工具返回了多个条件概率节点,选择'趣味道具'的理由是:当前已推导出的维度'物品'下已有匹配点'创意道具',而'趣味道具'与'创意道具'同属物品维度且语义相近,选择该节点可进一步深化物品维度的覆盖;虽然条件概率=0.125 不高,但该节点在人设树中与已推导的'创意道具'构成同维度延伸关系,推导逻辑连贯。",
         "tools": []
     },
     {
@@ -421,7 +421,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "output": [
             "第一人称视角"
         ],
-        "reason": "在已推导出的维度'故事编排'下,'第一人称视角'节点的条件概率=1.0,工具返回该节点存在帖子选题点匹配,因此将其作为推导候选。",
+        "reason": "选择'第一人称视角'的理由是:该节点在已推导维度'故事编排'下,条件概率=1.0 为所有返回节点中最高,表明在已推导选题点组合下该节点几乎必然出现;结合已推导的'拍摄视角'相关选题点,'第一人称视角'是该维度下最强关联的延伸方向。",
         "tools": []
     }
 ]  
@@ -462,7 +462,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
       "derived_nodes": ["图文信息", "夸张呈现"]
     },
     "output": ["家居改造利用", "废旧物品利用"],
-    "reason": "根据已推导出的'图文信息'、'夸张呈现',结合人设中相关的'创意改造'进行外部搜索,搜索结果中主要包含了家居改造利用、废旧物品利用等信息。搜索子 agent 评估返回的匹配结果显示'家居改造利用'匹配到帖子选题点'家居改造'(分数=0.85),'废旧物品利用'匹配到帖子选题点'废物利用'(分数=0.92)。",
+    "reason": "内部方法已连续1轮无新增匹配,选择信息搜索方向的理由是:已推导成功的'图文信息'和'夸张呈现'分别属于内容形式和表达风格维度,而维度分析显示'物品用途'维度尚未覆盖;人设树中'创意改造'节点位于该未覆盖维度下,因此以已推导选题点+该人设节点为方向进行外部搜索。搜索子 agent 返回的匹配结果中,'家居改造利用'匹配分数=0.85、'废旧物品利用'匹配分数=0.92,两者均达到完全推导阈值,且填补了物品用途维度的空白。",
     "tools": [
       {
         "name": "agent(derivation_search)",
@@ -521,7 +521,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 
 #### 内部推导方法阈值动态调整
 内部推导方法二、三的 `conditional_ratio_threshold`(条件概率阈值)、`top_n`(最大返回记录条数)由 agent 动态调整:
-- `top_n` 最小设置 500,可按 500→1000→2000 间隔动态调整;方法二(pattern 复用)的 `top_n` 最小设置 1000
+- `top_n` 最小设置 100,可按 100→200→500 间隔动态调整;方法二(pattern 复用)的 `top_n` 最小设置 200
 - 每轮可动态逐步降低条件概率阈值(**但最小值不能低于 0.2**),或增大最大返回记录条数,尽可能召回更多数据、推导到更多匹配选题点
 
 ---
@@ -560,19 +560,20 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 - **每一条推导路径必须包含**:输入节点、输出节点、推导方法、推导理由。
   - **输入节点**:必须是完全推导成功的选题点(`derived_success_set` 中的帖子选题点名称),或人设树节点、pattern 节点。**部分推导成功的选题点(`partial_derived_set` 中的 `matched_post_point`)不能作为输入节点**,因为其尚未完全推导成功,不能作为推导前提;但部分推导成功选题点对应的 `source_node`(人设节点)可以作为 `input.tree_nodes` 使用。
   - **输出节点**:本次推导产出的候选选题点。
-  - **推导理由**:必须详细、可追溯,引用工具返回的具体数据(人设树节点的 `r`/`w` 值,或 pattern 的 `s`/`l` 值);**禁止**牵强附会、连续多步联想或使用大模型自身世界知识推断;所有输出的选题点均须有对应推导理由。
+  - **推导理由**:必须是一个**决策理由**,说明为什么从工具返回的众多记录中选择该条数据进行推导。理由须结合以下要素:(1)与已推导选题点的关联性——如该数据与已推导选题点是否属于同一维度的延伸、或跨维度互补;(2)与账号人设树结构的契合度——如该数据对应的节点/pattern 在人设树中的位置、所属维度是否为当前未覆盖维度;(3)数据指标作为支撑——如节点概率(`r`/`w`值)、条件概率、pattern 支持度(`s`/`l`值)等可作为理由的一部分,但**不能是唯一理由**。**禁止**将理由简单归结为"该条数据有帖子选题点匹配"或"工具返回了该节点";**禁止**牵强附会、连续多步联想或使用大模型自身世界知识推断;所有输出的选题点均须有对应推导理由。
 
 ### 推导方法的使用约束
 
 1. **闭眼推导(核心约束)**:
    - 工具返回的「帖子选题点匹配」字段只包含本轮匹配成功的帖子选题点,不包含匹配失败项,因此不存在"偷看未推导选题点"的风险。
    - 只有**完全推导成功的选题点**(`derived_success_set` 中的帖子选题点名称)可以在推导路径的 `input.derived_nodes` 中引用。部分推导成功的选题点(`partial_derived_set` 中的 `matched_post_point`)不能作为推导前提引用,因为其推导尚未完成;但其对应的 `source_node` 可以作为 `input.tree_nodes` 使用。
-   - **禁止**在推导理由中引用匹配结果的反馈内容(如"匹配结果显示..."、"上一轮匹配到..."等)。
+   - **禁止**在推导理由中引用匹配结果的反馈内容(如"匹配结果显示..."、"上一轮匹配到..."等),也**禁止**将"该条数据存在帖子选题点匹配"作为选择该数据的推导理由——匹配是推导的结果验证,不是选择该数据的原因
 
 2. **禁止自由联想**:
    - 推导的路径步骤和理由,必须基于**工具返回**的人设树、pattern 或搜索子 agent 返回的具体数据。
    - **禁止**使用大模型自身的世界知识或联想信息进行推导。
-   - 每条推导理由中必须明确引用所使用的数据来源(如:工具返回的节点概率、条件概率、pattern 条件概率或搜索摘要等)。
+   - 每条推导理由必须说明选择该条数据的**决策逻辑**:为什么在工具返回的众多记录中选中这一条,它与已推导选题点、已推导维度、账号人设树结构之间的关联是什么,概率等数据指标如何支撑这一选择。
+   - **禁止**将理由简单写成对工具返回数据字段的罗列(如"该节点概率=0.9,帖子选题点匹配=xxx"),也**禁止**将理由归结为"该条数据有帖子选题点匹配"——匹配结果是推导的产出而非选择该数据的原因。
 
 3. **不强制包含所有选题点**:
    - 可能存在某些选题点无法通过上述推导方法以合理理由推导出。
@@ -614,7 +615,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
         "derived_nodes": ["已推导的选题点名称1", "已推导的选题点名称2"]
       },
       "output": ["本次推导出的选题点名称1", "本次推导出的选题点名称2"],
-      "reason": "推导详细原因,需反映思维链与决策依据,并引用工具返回的具体数据(如节点概率、条件概率、pattern 支持度等)",
+      "reason": "推导决策理由:说明为什么从工具返回的众多记录中选择该条数据进行推导。须结合已推导选题点、已推导维度、账号人设树结构等上下文,阐述选择该数据的决策逻辑,数据中的概率等指标可作为理由的支撑部分",
       "tools": [
         {
           "name": "工具名称(如 agent(derivation_search))",
@@ -645,7 +646,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
   - `input.patterns`: 本路径用到的 pattern 选题点拼接列表(与 `processed_edge_data.json` 中 `i` 格式一致,如 `"名称1+名称2"`)
   - `input.derived_nodes`: 本路径用到的已推导成功选题点名称列表(**只能引用 `derived_success_set` 中完全推导成功的选题点名称**,不能引用 `partial_derived_set` 中部分推导成功的选题点名称)
   - `output`: 本路径产出的待评估选题点名称列表(可多个)
-  - `reason`: 必须详细、可追溯,引用工具返回的具体数据;禁止牵强或凭空联想
+  - `reason`: 推导决策理由,须说明从工具返回的众多记录中**为什么选择该条数据**进行推导,决策依据包括但不限于:与上一轮已推导选题点的关联性(如同维度延伸、跨维度互补)、与账号人设树结构的契合度(如所属维度是否为未覆盖维度)、数据指标支撑(如条件概率、整体概率等);**禁止**将理由简单归结为"该条数据有帖子选题点匹配"或仅记录工具返回的数据字段值;禁止牵强或凭空联想
   - `tools`: 本路径使用的工具列表;若使用搜索工具,必须包含 `query`、`result`(数据摘要或关键内容)、`candidate_points`(评估子 agent 筛选的候选点)和 `match_result`(匹配结果);若未使用工具则为空数组 `[]`
 
 > **原子化要求体现在日志中**:每条推导路径遵循最小输入输出原子化规则——即用最少输入数据推导出哪些必要的选题点;路径中所有输入对产出该路径每个输出点都是必要的;逻辑上可以分开的推导路径不要混在一起。
@@ -730,7 +731,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 1. JSON 格式正确,可以正常解析
 2. 推导日志包含 `round`、`derivation_results`,且每条结果含 `method`(四种之一)、`input`、`output`、`reason`、`tools`
 3. 评估日志包含 `round`、`eval_results`、`derivation_progress`,`is_matched` 为布尔值,`need_next_round` 为布尔值,`matched_reason` 引用工具返回的匹配分数等具体数据,`matched_score` 为数值或 `null`,`is_fully_derived` 为布尔值
-4. 推导理由中不包含对匹配结果反馈的引用(如"匹配结果显示...")
+4. 推导理由中不包含对匹配结果反馈的引用(如"匹配结果显示..."),也不能将理由简单归结为"该条数据有帖子选题点匹配"或仅罗列工具返回的数据字段值,而应体现从众多工具返回记录中选择该条数据的决策逻辑
 5. 每条评估记录包含 `path_id` 和 `item_id` 两个 ID 字段,与推导日志路径对应
 6. 原子化拆分校验:方法一(人设常量)、方法三(人设推导)的每条路径 `input.tree_nodes` 长度为 1 且 `output` 长度为 1;方法二(pattern 复用)的每条路径 `input.patterns` 长度为 1
 7. 多路径择优校验:同一 `matched_post_point` 在同一轮评估日志中不应出现多条记录,只保留 `matched_score` 最高的一条
@@ -738,5 +739,10 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 9. `partial_derived_count` 统计 `matched_score < 0.78` 的部分推导成功选题点
 10. output/derivation_output_point 校验:推导日志中每条路径的 `output` 值必须是工具返回的节点名称或 pattern 元素名称,不得是帖子选题点名称;评估日志中 `derivation_output_point` 必须与对应路径 `output` 中的值逐字一致。若使用方法二(pattern 复用),`output` 中每个值必须是该 pattern 的元素名称(直接匹配)或扩展节点名称(扩展匹配),不得是 `matched_post_point` 的值。
 
+## 账号人设树
+账号人设树是通过对该账号一些帖子进行解构得到大量的选题点,再对这些选题点进行聚类得到的人设树,数据如下:
+
+{account_tree_data}
+
 $user$
 请开始执行 account_name={account_name},帖子ID={帖子ID},log_id={log_id} 的选题点整体推导任务。所有路径均相对于项目根目录。帖子的选题点数量={post_point_count}

+ 23 - 3
examples_how/overall_derivation/overall_derivation_agent_run.py

@@ -216,8 +216,9 @@ def _replace_prompt_placeholders(
     post_id: str,
     log_id: str,
     post_point_count: int,
+    account_tree_data: str,
 ) -> None:
-    """在 messages 的 content 中用 replace 替换 {account_name}, {帖子ID}, {log_id}, {post_point_count}。"""
+    """在 messages 的 content 中用 replace 替换 prompt 占位符。"""
     post_point_count_str = str(post_point_count)
     for m in messages:
         content = m.get("content")
@@ -227,6 +228,7 @@ def _replace_prompt_placeholders(
                 .replace("{帖子ID}", post_id)
                 .replace("{log_id}", log_id)
                 .replace("{post_point_count}", post_point_count_str)
+                .replace("{account_tree_data}", account_tree_data)
             )
         elif isinstance(content, list):
             for part in content:
@@ -237,6 +239,7 @@ def _replace_prompt_placeholders(
                         .replace("{帖子ID}", post_id)
                         .replace("{log_id}", log_id)
                         .replace("{post_point_count}", post_point_count_str)
+                        .replace("{account_tree_data}", account_tree_data)
                     )
 
 
@@ -310,12 +313,29 @@ async def main(account_name, post_id):
     else:
         print(f"   - 未找到选题点文件: {post_topic_path},post_point_count 使用 0")
 
+    # 读取账号树数据,作为 {account_tree_data} prompt 占位符
+    simple_tree_path = base_dir / "input" / account_name / "处理后数据" / "simple_tree" / "simple_tree.txt"
+    account_tree_data = ""
+    if simple_tree_path.exists():
+        with open(simple_tree_path, "r", encoding="utf-8") as f:
+            account_tree_data = f.read()
+        print(f"   - 已读取账号树数据: {simple_tree_path.relative_to(base_dir)}")
+    else:
+        print(f"   - 未找到账号树数据文件: {simple_tree_path},{account_tree_data=} 将使用空字符串")
+
     print("1. 加载 prompt 配置...")
     prompt = SimplePrompt(prompt_path)
 
     print("2. 构建任务消息...")
     messages = prompt.build_messages()
-    _replace_prompt_placeholders(messages, account_name, post_id, log_id, post_point_count)
+    _replace_prompt_placeholders(
+        messages,
+        account_name,
+        post_id,
+        log_id,
+        post_point_count,
+        account_tree_data,
+    )
 
     print("3. 创建 Agent Runner...")
     print(f"   - Skills 目录: {skills_dir}")
@@ -550,4 +570,4 @@ async def main(account_name, post_id):
 if __name__ == "__main__":
     # anthropic/claude-sonnet-4.6
     # google/gemini-3-flash-preview
-    asyncio.run(main(account_name="阿里多多酱", post_id="6915dfc400000000070224d9"))
+    asyncio.run(main(account_name="家有大志", post_id="68fb6a5c000000000302e5de"))

+ 76 - 31
examples_how/overall_derivation/tools/find_pattern.py

@@ -472,6 +472,45 @@ async def find_pattern(
     Returns:
     ToolResult:output 分「账号 pattern」「平台库 pattern」两段;平台段已排除与账号段 pattern 名称完全相同的项。
     """
+    def _split_by_post_match(
+        items: list[dict[str, Any]],
+    ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
+        matched: list[dict[str, Any]] = []
+        unmatched: list[dict[str, Any]] = []
+        for x in items:
+            if isinstance(x.get("帖子选题点匹配"), list):
+                matched.append(x)
+            else:
+                unmatched.append(x)
+        return matched, unmatched
+
+    def _pick_with_quota(
+        items: list[dict[str, Any]],
+        target_count: int,
+    ) -> list[dict[str, Any]]:
+        return items[:max(0, int(target_count))]
+
+    def _mix_by_ratio(
+        items: list[dict[str, Any]],
+        target_count: int,
+    ) -> list[dict[str, Any]]:
+        if target_count <= 0:
+            return []
+        matched, unmatched = _split_by_post_match(items)
+        matched_quota = target_count // 2
+        unmatched_quota = target_count - matched_quota
+        selected = _pick_with_quota(matched, matched_quota)
+        selected.extend(_pick_with_quota(unmatched, unmatched_quota))
+
+        if len(selected) < target_count:
+            selected_names = {str(x.get("pattern名称", "")) for x in selected}
+            fallback_pool = [
+                x for x in items
+                if str(x.get("pattern名称", "")) not in selected_names
+            ]
+            selected.extend(_pick_with_quota(fallback_pool, target_count - len(selected)))
+        return selected
+
     pattern_path = _pattern_file(account_name)
     if not pattern_path.is_file():
         return ToolResult(
@@ -482,11 +521,19 @@ async def find_pattern(
     try:
         derived_list = _parse_derived_list(derived_items or [])
         thr = float(match_score_threshold)
+        total_top_n = max(0, int(top_n))
+        account_top_n = int(total_top_n * 0.6)
+        platform_top_n = total_top_n - account_top_n
+        # 候选池适当放大,避免按“有/无匹配”分桶后数量不足
+        candidate_top_n = max(total_top_n * 4, total_top_n + 100)
 
         # ---------- 账号 pattern(原逻辑:match_data + 子节点/兄弟扩展)----------
         items_account = get_patterns_by_conditional_ratio(
-            account_name, derived_list, conditional_ratio_threshold, top_n, post_id
+            account_name, derived_list, conditional_ratio_threshold, candidate_top_n, post_id
         )
+        if not post_id:
+            for item in items_account:
+                item["帖子选题点匹配"] = "无"
         if items_account and post_id:
             all_elements: list[str] = []
             seen_elements: set[str] = set()
@@ -520,11 +567,6 @@ async def find_pattern(
                     pattern_matches if distinct_post_points >= 2 else "无"
                 )
 
-        items_account = [
-            x for x in items_account
-            if isinstance(x.get("帖子选题点匹配"), list)
-        ]
-
         if items_account and post_id:
             node_info_map = _build_node_info(account_name)
             all_candidates_set: set[str] = set()
@@ -572,6 +614,8 @@ async def find_pattern(
                                 if sc > best_sc:
                                     best_cand, best_pp, best_sc = cand, pp, sc
                         if best_cand is not None:
+                            if not isinstance(item.get("帖子选题点匹配"), list):
+                                item["帖子选题点匹配"] = []
                             item["帖子选题点匹配"].append({
                                 "pattern元素": elem,
                                 "帖子选题点": best_pp,
@@ -591,33 +635,34 @@ async def find_pattern(
                     best_by_pp[pp] = m
             item["帖子选题点匹配"] = list(best_by_pp.values())
 
+        items_account = _mix_by_ratio(items_account, account_top_n)
         account_pattern_names = {str(x.get("pattern名称", "")).strip() for x in items_account}
 
         # ---------- 平台库 pattern(xiaohongshu/tree 条件概率 + xiaohongshu/match_data 匹配)----------
         items_platform: list[dict[str, Any]] = []
+        items_platform = get_platform_patterns_by_conditional_ratio(
+            derived_list, conditional_ratio_threshold / 5, candidate_top_n, post_id
+        )
         if post_id:
-            items_platform = get_platform_patterns_by_conditional_ratio(
-                derived_list, conditional_ratio_threshold / 5, top_n, post_id
-            )
             _attach_platform_pattern_post_matches(items_platform, post_id, thr)
-            items_platform = [
-                x for x in items_platform
-                if isinstance(x.get("帖子选题点匹配"), list)
-            ]
-            items_platform = [
-                x for x in items_platform
-                if str(x.get("pattern名称", "")).strip() not in account_pattern_names
-            ]
+        else:
             for item in items_platform:
-                matches = item.get("帖子选题点匹配")
-                if not isinstance(matches, list):
-                    continue
-                best_by_pp: dict[str, dict] = {}
-                for m in matches:
-                    pp = m["帖子选题点"]
-                    if pp not in best_by_pp or m["匹配分数"] > best_by_pp[pp]["匹配分数"]:
-                        best_by_pp[pp] = m
-                item["帖子选题点匹配"] = list(best_by_pp.values())
+                item["帖子选题点匹配"] = "无"
+        items_platform = [
+            x for x in items_platform
+            if str(x.get("pattern名称", "")).strip() not in account_pattern_names
+        ]
+        for item in items_platform:
+            matches = item.get("帖子选题点匹配")
+            if not isinstance(matches, list):
+                continue
+            best_by_pp: dict[str, dict] = {}
+            for m in matches:
+                pp = m["帖子选题点"]
+                if pp not in best_by_pp or m["匹配分数"] > best_by_pp[pp]["匹配分数"]:
+                    best_by_pp[pp] = m
+            item["帖子选题点匹配"] = list(best_by_pp.values())
+        items_platform = _mix_by_ratio(items_platform, platform_top_n)
 
         def _format_pattern_block(xs: list[dict[str, Any]]) -> list[str]:
             lines: list[str] = []
@@ -647,7 +692,7 @@ async def find_pattern(
         lines_out.append("—— 账号 pattern ——")
         if not items_account:
             lines_out.append(
-                f"(无:未找到条件概率 >= {conditional_ratio_threshold} 且满足帖子选题点匹配条件的 pattern)"
+                f"(无:未找到条件概率 >= {conditional_ratio_threshold} 的 pattern)"
             )
         else:
             lines_out.extend(_format_pattern_block(items_account))
@@ -686,17 +731,17 @@ def main() -> None:
     """本地测试:用家有大志账号、已推导选题点,查询符合条件概率阈值的 pattern(含帖子匹配)。"""
     import asyncio
 
-    account_name = "创业邦"
-    post_id = "694a6caf000000001f00e112"
+    account_name = "家有大志"
+    post_id = "68fb6a5c000000000302e5de"
     # 已推导选题点,每项:已推导的选题点 + 推导来源人设树节点
     # derived_items = [
     #     {"topic": "分享", "source_node": "分享"},
     #     {"topic": "植入方式", "source_node": "植入方式"},
     #     {"topic": "叙事结构", "source_node": "叙事结构"},
     # ]
-    derived_items = derived_items = [{"topic":"推广","source_node":"推广"},{"topic":"视觉调性","source_node":"视觉调性"}]
+    derived_items = derived_items = []
     conditional_ratio_threshold = 0.2
-    top_n = 2000
+    top_n = 200
 
     # 1)直接调用核心函数(不含帖子匹配,仅验证排序逻辑)
     # derived_list = _parse_derived_list(derived_items)

+ 159 - 68
examples_how/overall_derivation/tools/find_tree_node.py

@@ -258,20 +258,20 @@ def _platform_tree_dir() -> Path:
     return _BASE_INPUT / "xiaohongshu" / "tree"
 
 
-def get_platform_nodes_by_conditional_ratio(
+def _collect_platform_scored_tuples(
     derived_list: list[tuple[str, str]],
     threshold: float,
-    top_n: int,
-) -> list[dict[str, Any]]:
+    max_nodes: int = 12000,
+) -> list[tuple[str, float, str, str]]:
     """
-    平台库人设树节点条件概率筛选,计算方式与 get_nodes_by_conditional_ratio 一致
-   (同一套 calc_node_conditional_ratio / _post_ids 规则,索引来自 xiaohongshu/tree)。
-    derived_list 为空时用节点 _ratio。
+    平台库人设树:条件概率 >= threshold 的节点全量收集,按条件概率降序。
+    max_nodes 防止极端大树占满内存;截断发生在全局排序之后(保留高分段)。
     """
     tree_dir = _platform_tree_dir()
     if not tree_dir.is_dir():
         return []
 
+    thr = float(threshold)
     scored: list[tuple[str, float, str, str]] = []
 
     if not derived_list:
@@ -282,11 +282,8 @@ def get_platform_nodes_by_conditional_ratio(
                     if not isinstance(child, dict):
                         continue
                     ratio = child.get("_ratio")
-                    if ratio is None:
-                        r = 0.0
-                    else:
-                        r = float(ratio)
-                    if r >= threshold:
+                    r = 0.0 if ratio is None else float(ratio)
+                    if r >= thr:
                         scored.append((name, r, parent_name, dim_name))
                     walk(name, child)
 
@@ -312,13 +309,30 @@ def get_platform_nodes_by_conditional_ratio(
                 node_name,
                 base_dir=_BASE_INPUT,
                 node_post_index=node_post_index,
-                target_ratio=threshold,
+                target_ratio=thr,
             )
-            if ratio >= threshold:
+            if ratio >= thr:
                 scored.append((node_name, ratio, parent_name, dim_name))
 
     scored.sort(key=lambda x: x[1], reverse=True)
-    top = scored[:top_n]
+    if max_nodes > 0 and len(scored) > max_nodes:
+        scored = scored[:max_nodes]
+    return scored
+
+
+def get_platform_nodes_by_conditional_ratio(
+    derived_list: list[tuple[str, str]],
+    threshold: float,
+    top_n: int,
+) -> list[dict[str, Any]]:
+    """
+    平台库人设树节点条件概率筛选,计算方式与 get_nodes_by_conditional_ratio 一致
+   (同一套 calc_node_conditional_ratio / _post_ids 规则,索引来自 xiaohongshu/tree)。
+    derived_list 为空时用节点 _ratio。
+    """
+    n = max(0, int(top_n))
+    scored = _collect_platform_scored_tuples(derived_list, threshold)
+    top = scored[:n]
     return [
         {
             "节点名称": name,
@@ -435,51 +449,103 @@ def _platform_node_belonging_dim_from_anchor_nodes(
     return dim_map
 
 
-def _load_platform_match_nodes(
+def _load_platform_nodes_split(
     post_id: str,
     derived_list: list[tuple[str, str]],
     conditional_ratio_threshold: float,
     match_score_threshold: float,
     top_n: int,
     node_belonging_dim_platform: Optional[dict[str, str]] = None,
-) -> list[dict[str, Any]]:
+) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
     """
-    平台库人设树:先按与账号一致的条件概率筛选(get_platform_nodes_by_conditional_ratio),
-    再仅保留在 xiaohongshu 匹配文件中、且单条 match_score >= match_score_threshold 的帖子选题点;
-    无达标选题点匹配的节点丢弃。
+    平台库人设树:用 _collect_platform_scored_tuples 得到条件概率达标的节点,
+    再按 xiaohongshu/match_data 分为「有帖子选题点匹配 / 无匹配」两类,**两类各自按条件概率取 Top 池**(同一全局 TopN 不会挤掉另一类),
+    最后分别组装返回:
+      - matched:有 match_score >= match_score_threshold 的帖子选题点匹配的节点
+      - unmatched:无达标帖子选题点匹配的节点
+    两组均要求节点在 node_belonging_dim_platform 中有有效的所属维度(不为「—」)。
     """
-    candidates = get_platform_nodes_by_conditional_ratio(
+    matched: list[dict[str, Any]] = []
+    unmatched: list[dict[str, Any]] = []
+
+    topic_map: dict[tuple[str, str], dict[str, float]] = {}
+    if post_id:
+        topic_map = _platform_match_topics_by_node(post_id, float(match_score_threshold))
+    # 维度标签可能与树侧不完全一致:保留一个按节点名聚合的兜底索引,避免误判为“无匹配”。
+    topic_map_by_name: dict[str, dict[str, float]] = {}
+    for (_dim, n), topics in topic_map.items():
+        bucket = topic_map_by_name.setdefault(str(n).strip(), {})
+        for t, sc in (topics or {}).items():
+            prev = bucket.get(t)
+            if prev is None or sc > prev:
+                bucket[t] = sc
+
+    # 有 match_data 命中与无命中两类分开按条件概率取 Top,避免混在一个全局 TopN 里挤掉某一类。
+    all_scored = _collect_platform_scored_tuples(
         derived_list,
         float(conditional_ratio_threshold),
-        int(top_n),
     )
-    if not candidates or not post_id:
-        return []
+    if not all_scored:
+        return matched, unmatched
+
+    matched_tuples: list[tuple[str, float, str, str]] = []
+    unmatched_tuples: list[tuple[str, float, str, str]] = []
+    for name, ratio, parent, dim in all_scored:
+        lookup_dim = str(dim).strip()
+        key = (lookup_dim, str(name).strip())
+        topics = topic_map.get(key) or topic_map_by_name.get(str(name).strip()) or {}
+        if topics:
+            matched_tuples.append((name, ratio, parent, dim))
+        else:
+            unmatched_tuples.append((name, ratio, parent, dim))
+
+    _pool = max(int(top_n), min(2000, max(500, int(top_n) * 5)))
+    matched_tuples = matched_tuples[:_pool]
+    unmatched_tuples = unmatched_tuples[:_pool]
+
+    def _emit_tuple_rows(
+        tuples: list[tuple[str, float, str, str]],
+        *,
+        has_topics: bool,
+    ) -> None:
+        for name, ratio, parent, dim in tuples:
+            row = {
+                "节点名称": name,
+                "条件概率": ratio,
+                "父节点名称": parent,
+                "所属维度": dim,
+            }
+            name_s = str(row.get("节点名称") or "").strip()
+            out_dim = "—"
+            if node_belonging_dim_platform is not None:
+                out_dim = node_belonging_dim_platform.get(name_s) or "—"
+            if node_belonging_dim_platform is not None and out_dim == "—":
+                continue
+            row_out = dict(row)
+            row_out["所属维度"] = out_dim
+
+            lookup_dim = str(row.get("所属维度") or "").strip()
+            key2 = (lookup_dim, name_s)
+            topics = topic_map.get(key2) or topic_map_by_name.get(name_s) or {}
+            if has_topics:
+                if not topics:
+                    continue
+                topic_items = sorted(topics.items(), key=lambda x: x[1], reverse=True)
+                row_out["帖子选题点匹配"] = [{"帖子选题点": t, "匹配分数": sc} for t, sc in topic_items]
+                matched.append(row_out)
+            else:
+                if topics:
+                    continue
+                row_out["帖子选题点匹配"] = "无"
+                unmatched.append(row_out)
 
-    topic_map = _platform_match_topics_by_node(post_id, float(match_score_threshold))
-    out: list[dict[str, Any]] = []
-    for row in candidates:
-        lookup_dim = str(row.get("所属维度") or "").strip()
-        name = str(row.get("节点名称") or "").strip()
-        key = (lookup_dim, name)
-        topics = topic_map.get(key) or {}
-        if not topics:
-            continue
-        topic_items = sorted(topics.items(), key=lambda x: x[1], reverse=True)
-        match_list = [{"帖子选题点": t, "匹配分数": sc} for t, sc in topic_items]
-        out_dim = "—"
-        if node_belonging_dim_platform is not None:
-            out_dim = node_belonging_dim_platform.get(name) or "—"
-        if out_dim == "—":
-            continue
-        row_out = dict(row)
-        row_out["所属维度"] = out_dim
-        row_out["帖子选题点匹配"] = match_list
-        out.append(row_out)
-    return out
+    _emit_tuple_rows(matched_tuples, has_topics=True)
+    _emit_tuple_rows(unmatched_tuples, has_topics=False)
+
+    return matched, unmatched
 
 
-def build_platform_tree_section_items(
+def build_platform_tree_section_items_split(
     post_id: str,
     derived_list: list[tuple[str, str]],
     conditional_ratio_threshold: float,
@@ -487,14 +553,16 @@ def build_platform_tree_section_items(
     top_n: int,
     exclude_node_names: set[str],
     node_belonging_dim_platform: Optional[dict[str, str]] = None,
-) -> list[dict[str, Any]]:
+) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
     """
-    平台库人设树节点:条件概率 + xiaohongshu/match_data 匹配,并排除与账号段重复的节点名称。
+    平台库人设树节点:条件概率 + xiaohongshu/match_data 匹配,排除与账号段重复的节点名称,
+    返回 (有帖子选题点匹配的节点列表, 无帖子选题点匹配的节点列表)。
     供 find_tree_nodes_by_conditional_ratio 聚合输出使用。
     """
     if not post_id:
-        return []
-    plat = _load_platform_match_nodes(
+        return [], []
+    ex = {str(n).strip() for n in exclude_node_names}
+    matched, unmatched = _load_platform_nodes_split(
         post_id=post_id,
         derived_list=derived_list,
         conditional_ratio_threshold=float(conditional_ratio_threshold),
@@ -502,11 +570,9 @@ def build_platform_tree_section_items(
         top_n=int(top_n),
         node_belonging_dim_platform=node_belonging_dim_platform,
     )
-    ex = {str(n).strip() for n in exclude_node_names}
-    return [
-        p for p in plat
-        if str(p.get("节点名称", "")).strip() not in ex
-    ]
+    matched_filtered = [p for p in matched if str(p.get("节点名称", "")).strip() not in ex]
+    unmatched_filtered = [p for p in unmatched if str(p.get("节点名称", "")).strip() not in ex]
+    return matched_filtered, unmatched_filtered
 
 
 # ---------------------------------------------------------------------------
@@ -594,14 +660,17 @@ async def find_tree_nodes_by_conditional_ratio(
     """
     按条件概率阈值筛选节点:先账号人设树(优先使用),再平台库人设树;两段不合并。
     条件概率计算对两棵树使用同一套规则(calc_node_conditional_ratio / 节点 _post_ids)。
-    「帖子选题点匹配」仅保留匹配分 >= match_score_threshold 的选题点;无达标匹配的节点不返回。
+    返回结果按以下配额分配(合计 top_n 条):
+      - 账号人设树节点占 60%,其中有帖子选题点匹配的记录和无帖子选题点匹配的记录各占一半;
+      - 平台库人设树节点占 40%,其中有帖子选题点匹配的记录和无帖子选题点匹配的记录各占一半。
+    「帖子选题点匹配」仅收录匹配分 >= match_score_threshold 的选题点。
 
     Args:
     account_name : 账号名,用于定位该账号的人设树数据。
     post_id : 帖子ID,用于加载帖子选题点并与各节点做匹配判断。
     derived_items : 已推导选题点列表,可为空。非空时每项为字典,需含 topic(或「已推导的选题点」)与 source_node(或「推导来源人设树节点」)
     conditional_ratio_threshold : 条件概率阈值,仅返回条件概率 >= 该值的节点。
-    top_n : 返回条数上限(账号段、平台段各自取前 top_n 条条件概率结果后再按匹配过滤)
+    top_n : 最终返回总条数上限,按 账号60%/平台40%、有匹配/无匹配各半 分配
     round : 推导轮次。
     log_id : 推导日志ID
     match_score_threshold : 帖子选题点匹配分阈值,与 point_match 默认一致。
@@ -678,22 +747,36 @@ async def find_tree_nodes_by_conditional_ratio(
                 matches = node_match_map.get(item["节点名称"], [])
                 item["帖子选题点匹配"] = matches if matches else "无"
 
-        # [临时] 仅保留有帖子选题点匹配的记录(过滤掉「无」),方便后续删除
-        items = [x for x in items if isinstance(x.get("帖子选题点匹配"), list)]
-
-        # 2)平台库人设树(条件概率 + xiaohongshu 匹配文件;与账号节点同名则剔除)
-        account_node_names = {str(x.get("节点名称", "")).strip() for x in items}
+        # 账号配额:占 top_n 的 60%,有/无匹配各一半
+        account_quota = int(top_n * 0.6 + 0.5)
+        account_with_n = account_quota // 2
+        account_without_n = account_quota - account_with_n
+        items_with_match = [x for x in items if isinstance(x.get("帖子选题点匹配"), list)]
+        items_without_match = [x for x in items if not isinstance(x.get("帖子选题点匹配"), list)]
+        items = items_with_match[:account_with_n] + items_without_match[:account_without_n]
+
+        # 2)平台库人设树(条件概率 + xiaohongshu 匹配文件)
+        # 平台配额:占 top_n 的 40%,有/无匹配各一半
+        platform_quota = top_n - account_quota
+        platform_with_n = platform_quota // 2
+        platform_without_n = platform_quota - platform_with_n
+        # 平台「有匹配」排除账号侧已有帖子选题点匹配的节点名(与账号段去重)。
+        # 平台「无匹配」排除已在账号段输出里出现过的节点名(避免重复罗列无新信息的同名节点)。
+        account_matched_names = {str(x.get("节点名称", "")).strip() for x in items if isinstance(x.get("帖子选题点匹配"), list)}
+        account_all_names = {str(x.get("节点名称", "")).strip() for x in items}
         platform_items: list[dict[str, Any]] = []
         if post_id:
-            platform_items = build_platform_tree_section_items(
+            p_matched_raw, p_unmatched_raw = _load_platform_nodes_split(
                 post_id=post_id,
                 derived_list=derived_list,
                 conditional_ratio_threshold=float(conditional_ratio_threshold),
                 match_score_threshold=float(match_score_threshold),
                 top_n=top_n,
-                exclude_node_names=account_node_names,
                 node_belonging_dim_platform=node_belonging_dim_platform,
             )
+            p_matched = [p for p in p_matched_raw if str(p.get("节点名称", "")).strip() not in account_matched_names]
+            p_unmatched = [p for p in p_unmatched_raw if str(p.get("节点名称", "")).strip() not in account_all_names]
+            platform_items = p_matched[:platform_with_n] + p_unmatched[:platform_without_n]
 
         def _format_node_line(x: dict[str, Any]) -> str:
             match_info = x.get("帖子选题点匹配", "无")
@@ -709,20 +792,20 @@ async def find_tree_nodes_by_conditional_ratio(
 
         lines: list[str] = []
         lines.append(
-            "【优先使用】第一节为账号人设树中条件概率达标的节点;"
-            "第二节为平台库人设树中条件概率达标的节点;"
+            "【优先使用】第一节为账号人设树中条件概率达标的节点(占60%配额,有/无帖子匹配各半);"
+            "第二节为平台库人设树中条件概率达标的节点(占40%配额,有/无帖子匹配各半);"
         )
         lines.append("")
         lines.append("—— 账号人设树节点 ——")
         if not items:
-            lines.append(f"(无:未找到条件概率 >= {conditional_ratio_threshold} 且与帖子选题点有匹配的节点)")
+            lines.append(f"(无:未找到条件概率 >= {conditional_ratio_threshold} 的节点)")
         else:
             lines.extend(_format_node_line(x) for x in items)
         lines.append("")
         lines.append("—— 平台库人设树节点 ——")
         if not platform_items:
             lines.append(
-                "(无:未找到条件概率达标且存在达标帖子选题点匹配的节点)"
+                "(无:未找到条件概率达标的节点)"
             )
         else:
             lines.extend(_format_node_line(x) for x in platform_items)
@@ -736,6 +819,14 @@ async def find_tree_nodes_by_conditional_ratio(
                 "threshold": conditional_ratio_threshold,
                 "match_score_threshold": float(match_score_threshold),
                 "top_n": top_n,
+                "quota": {
+                    "account_quota": account_quota,
+                    "account_with_match": len([x for x in items if isinstance(x.get("帖子选题点匹配"), list)]),
+                    "account_without_match": len([x for x in items if not isinstance(x.get("帖子选题点匹配"), list)]),
+                    "platform_quota": platform_quota,
+                    "platform_with_match": len([x for x in platform_items if isinstance(x.get("帖子选题点匹配"), list)]),
+                    "platform_without_match": len([x for x in platform_items if not isinstance(x.get("帖子选题点匹配"), list)]),
+                },
                 "account_tree_count": len(items),
                 "platform_tree_count": len(platform_items),
                 "count": len(items) + len(platform_items),
@@ -770,7 +861,7 @@ def main() -> None:
     # ]
     derived_items = [{"topic":"推广","source_node":"推广"},{"topic":"视觉调性","source_node":"视觉调性"}]
     conditional_ratio_threshold = 0.2
-    top_n = 2000
+    top_n = 200
 
     # # 1)常量节点(核心函数,无匹配)
     # constant_nodes = get_constant_nodes(account_name)

+ 5 - 5
examples_how/overall_derivation/tools/search_and_eval.py

@@ -333,11 +333,11 @@ async def search_and_eval(
         query_list,
     )
 
-    # if True:
-    #     return ToolResult(
-    #         title="搜索评估工具不可用",
-    #         output="搜索评估工具不可用"
-    #     )
+    if True:
+        return ToolResult(
+            title="搜索评估工具不可用",
+            output="搜索评估工具不可用"
+        )
 
     if not query_list:
         return ToolResult(