Sfoglia il codice sorgente

how agent 搜索query构建结合维度分析数据

liuzhiheng 1 mese fa
parent
commit
91f9fc5376

+ 8 - 8
examples_how/overall_derivation/derivation_main.md

@@ -1,5 +1,5 @@
 ---
 ---
-model: google/gemini-3-flash-preview
+model: anthropic/claude-sonnet-4.6
 temperature: 0.3
 temperature: 0.3
 ---
 ---
 
 
@@ -86,10 +86,11 @@ $system$
 2. **post_id**:帖子 ID
 2. **post_id**:帖子 ID
 3. **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
 3. **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
 4. **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**(不传 matched_post_point,遵循闭眼原则)
 4. **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**(不传 matched_post_point,遵循闭眼原则)
+5. **dimension_data**:上一轮 `round_pattern_dimension_analyze` 工具返回的当前推导维度数据文本(包括已推导维度及匹配点信息,未推导维度);若为首轮搜索或尚未调用过该工具,传空字符串 `""`
 
 
 调用示例:
 调用示例:
 ```
 ```
-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(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\":\"趣味道具\"}]\ndimension_data=<round_pattern_dimension_analyze 工具返回的维度文本>")
 ```
 ```
 
 
 搜索子 agent 返回的结果格式包含:
 搜索子 agent 返回的结果格式包含:
@@ -143,7 +144,7 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 执行推导前,先明确本轮方向:当前处于广召回阶段还是收敛阶段?上一轮评估结果如何,哪些方向值得延伸或放弃?本轮应选用哪些方法与参数组合?同时检查 `failed_points` 列表,确保本轮不重复已失败的推导方向。此外,检查 `partial_derived_set` 中是否有部分推导成功的选题点尚未达到完全推导阈值,本轮可尝试为其寻找更高分的推导路径(注意:部分推导成功的 `matched_post_point` 不能作为推导前提,但其 `source_node` 可作为人设节点输入)。
 执行推导前,先明确本轮方向:当前处于广召回阶段还是收敛阶段?上一轮评估结果如何,哪些方向值得延伸或放弃?本轮应选用哪些方法与参数组合?同时检查 `failed_points` 列表,确保本轮不重复已失败的推导方向。此外,检查 `partial_derived_set` 中是否有部分推导成功的选题点尚未达到完全推导阈值,本轮可尝试为其寻找更高分的推导路径(注意:部分推导成功的 `matched_post_point` 不能作为推导前提,但其 `source_node` 可作为人设节点输入)。
 
 
 **步骤二:执行推导**
 **步骤二:执行推导**
-以**已推导成功的选题点集合**为基础(首轮为空),按步骤一确定的方法与参数,分条执行推导路径。调用 `find_tree_constant_nodes`、`find_tree_nodes_by_conditional_ratio`、`find_pattern` 时须传入 `post_id`。`find_tree_nodes_by_conditional_ratio` 还须传入当前 **`round`** 与本次运行的 **`log_id`**(推导日志ID)。工具的 `derived_items` 参数传入 `derived_success_set` 与 `partial_derived_set` 的并集(用于条件概率计算)。**总轮次上限为 15 轮**。
+以**已推导成功的选题点集合**为基础(首轮为空),按步骤一确定的方法与参数,分条执行推导路径。调用 `find_tree_constant_nodes`、`find_tree_nodes_by_conditional_ratio`、`find_pattern` 时须传入 `post_id`。`find_tree_nodes_by_conditional_ratio` 还须传入当前 **`round`** 与本次运行的 **`log_id`**(推导日志ID)。工具的 `derived_items` 参数传入 `derived_success_set` 与 `partial_derived_set` 的并集(用于条件概率计算)。**总轮次上限为 10 轮**。
 
 
 **注意**:
 **注意**:
 - 完全推导成功的选题点(`derived_success_set` 中的选题点)不需要再作为推导目标输出;部分推导成功的选题点(`partial_derived_set` 中的选题点)可以继续作为推导目标——如果本轮中出现了更高匹配分数的路径,则更新其记录。
 - 完全推导成功的选题点(`derived_success_set` 中的选题点)不需要再作为推导目标输出;部分推导成功的选题点(`partial_derived_set` 中的选题点)可以继续作为推导目标——如果本轮中出现了更高匹配分数的路径,则更新其记录。
@@ -415,10 +416,10 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 - **适用场景**:方法二和方法三均难以推导出新选题点时,或需要验证某个推导假设时。
 - **适用场景**:方法二和方法三均难以推导出新选题点时,或需要验证某个推导假设时。
 - **操作方式**:**不直接调用 `search_posts` 或 `point_match`**。应调用内置 `agent` 工具,传入 `agent_type="derivation_search"`,在 `task` 中给出本次搜索 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set` 等参数。搜索子 agent 会在内部完成搜索、评估,并将搜索结果摘要、候选点列表和匹配结果一并返回。
 - **操作方式**:**不直接调用 `search_posts` 或 `point_match`**。应调用内置 `agent` 工具,传入 `agent_type="derivation_search"`,在 `task` 中给出本次搜索 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set` 等参数。搜索子 agent 会在内部完成搜索、评估,并将搜索结果摘要、候选点列表和匹配结果一并返回。
 - **搜索流程**:
 - **搜索流程**:
-  1. **收集搜索子agent参数**:`account_name`、`post_id`、`derived_success_set`、`partial_derived_set`
+  1. **收集搜索子agent参数**:`account_name`、`post_id`、`derived_success_set`、`partial_derived_set`,以及上一轮 `round_pattern_dimension_analyze` 返回的 `dimension_data`(无则传 `""`)
   2. **调用搜索子 agent**:
   2. **调用搜索子 agent**:
      ```
      ```
-     agent(agent_type="derivation_search", task="执行搜索任务,account_name=<账号名称>\npost_id=<帖子ID>\nderived_success_set=<JSON数组>\npartial_derived_set=<JSON数组>")
+     agent(agent_type="derivation_search", task="执行搜索任务,account_name=<账号名称>\npost_id=<帖子ID>\nderived_success_set=<JSON数组>\npartial_derived_set=<JSON数组>\ndimension_data=<维度数据文本>")
      ```
      ```
   3. **根据子 agent 返回**:直接读取返回结果中的 `match_result` 字段,获取每个候选点的匹配判断结果(`candidate_point`、`is_matched`、`matched_post_point`、`matched_score`),整理为推导路径输出和评估日志。
   3. **根据子 agent 返回**:直接读取返回结果中的 `match_result` 字段,获取每个候选点的匹配判断结果(`candidate_point`、`is_matched`、`matched_post_point`、`matched_score`),整理为推导路径输出和评估日志。
 - **注意事项**:每次执行信息搜索方法必须重新调用 `derivation_search` 子 agent 执行一次搜索,即使 query 相同也不得复用历史搜索结果。主 agent **不得**在搜索后再额外调用 `point_match`。
 - **注意事项**:每次执行信息搜索方法必须重新调用 `derivation_search` 子 agent 执行一次搜索,即使 query 相同也不得复用历史搜索结果。主 agent **不得**在搜索后再额外调用 `point_match`。
@@ -484,7 +485,6 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 
 
 - **优先内部,pattern 复用优先**:每轮推导以内部方法(方法一、二、三)为主,其中方法二(账号 pattern 复用)应在每轮都被使用,充分挖掘 pattern 数据中的共现信号;方法三(人设推导)作为补充,覆盖 pattern 之外的关联维度
 - **优先内部,pattern 复用优先**:每轮推导以内部方法(方法一、二、三)为主,其中方法二(账号 pattern 复用)应在每轮都被使用,充分挖掘 pattern 数据中的共现信号;方法三(人设推导)作为补充,覆盖 pattern 之外的关联维度
 - **搜索触发时机**:内部方法连续 2 轮无法产出有效新候选点时,触发信息搜索(方法四)
 - **搜索触发时机**:内部方法连续 2 轮无法产出有效新候选点时,触发信息搜索(方法四)
-- **搜索关键词约束**:只能使用完全推导成功选题点的 `matched_post_point` 名称(`derived_success_set` 中的选题点),或工具曾返回过的人设树节点名称(包括 `partial_derived_set` 中选题点对应的 `source_node`)构造 query;**禁止**使用部分推导成功的 `matched_post_point` 作为搜索关键词;**禁止**使用大模型自行推测或联想出的关键词;**禁止使用账号名称**
 - **搜索后的跟进**:每次搜索后至少安排 1~2 轮内部方法推导,将搜索发现的新方向优先在 pattern 库中验证,再结合人设树延伸
 - **搜索后的跟进**:每次搜索后至少安排 1~2 轮内部方法推导,将搜索发现的新方向优先在 pattern 库中验证,再结合人设树延伸
 
 
 #### 内部推导结果重合处理
 #### 内部推导结果重合处理
@@ -516,8 +516,8 @@ agent(agent_type="derivation_search", task="执行搜索任务,account_name=xx
 
 
 当满足以下任一条件时,停止推导:
 当满足以下任一条件时,停止推导:
 
 
-- `consecutive_zero_rounds >= 5`(**注意:是连续 5 轮匹配率为 0%,不是累计出现了 5 轮匹配率为 0%**)
-- 达到总轮次上限 15
+- `consecutive_zero_rounds >= 3`(**注意:是连续 3 轮匹配率为 0%,不是累计出现了 3 轮匹配率为 0%**)
+- 达到总轮次上限 10
 - 提前完成:当前待推导的帖子选题点总数量 = `{post_point_count}`,如果**完全推导成功**的帖子选题点(`derived_success_set` 的长度,不含 `partial_derived_set`)占总数量的 85% 以上,且连续 2 轮匹配率为 0%,可提前终止
 - 提前完成:当前待推导的帖子选题点总数量 = `{post_point_count}`,如果**完全推导成功**的帖子选题点(`derived_success_set` 的长度,不含 `partial_derived_set`)占总数量的 85% 以上,且连续 2 轮匹配率为 0%,可提前终止
 
 
 ---
 ---

+ 37 - 40
examples_how/overall_derivation/generate_visualize_data.py

@@ -362,20 +362,10 @@ def build_visualize_edges(
 
 
     edge_list = []
     edge_list = []
     round_output_seen: set[tuple[int, str]] = set()  # (round_num, node_name) 本轮已作为某边的 output
     round_output_seen: set[tuple[int, str]] = set()  # (round_num, node_name) 本轮已作为某边的 output
-    best_score_by_node: dict[str, float] = {}  # node_name -> 已出现过的最高 matched_score
-    fully_derived_nodes: set[str] = set()
-    current_round: int | None = None
+    prev_best_by_node: dict[str, tuple[float, bool]] = {}  # node_name -> (score, is_fully) of last included round
 
 
     for round_idx, derivation in enumerate(derivations):
     for round_idx, derivation in enumerate(derivations):
         round_num = derivation.get("round", round_idx + 1)
         round_num = derivation.get("round", round_idx + 1)
-        if current_round is None:
-            current_round = round_num
-        elif round_num != current_round:
-            # 一轮结束后,将本轮 is_fully_derived=true 的节点加入 fully_derived_nodes,用于后续轮次过滤
-            for (rn, name), (score, is_fully, _out_item, _method) in best_node_info_by_round_mp.items():
-                if rn == current_round and is_fully:
-                    fully_derived_nodes.add(name)
-            current_round = round_num
         for dr in derivation.get("derivation_results") or []:
         for dr in derivation.get("derivation_results") or []:
             output_list = dr.get("output") or []
             output_list = dr.get("output") or []
             path_id = dr.get("id")
             path_id = dr.get("id")
@@ -391,16 +381,19 @@ def build_visualize_edges(
             if not matched:
             if not matched:
                 continue
                 continue
 
 
-            # 同一轮内 output 节点不重复;若前面轮次该节点匹配分更高则本轮不保留
+            # 同一轮内 output 节点不重复;若前面轮次该节点已完全推导,或分数未提升且未从 false 变 true,则本轮跳过
             # 并且只保留与 node_list 中该轮该节点的最高分记录一致的边
             # 并且只保留与 node_list 中该轮该节点的最高分记录一致的边
             output_names_this_edge = []
             output_names_this_edge = []
             for mp, reason, score, is_fully, out_item in matched:
             for mp, reason, score, is_fully, out_item in matched:
                 if (round_num, mp) in round_output_seen:
                 if (round_num, mp) in round_output_seen:
                     continue
                     continue
-                if mp in fully_derived_nodes:
-                    continue
-                if score <= best_score_by_node.get(mp, -1.0):
-                    continue
+                prev = prev_best_by_node.get(mp)
+                if prev is not None:
+                    prev_score, prev_fully = prev
+                    if prev_fully:
+                        continue
+                    if not is_fully and score <= prev_score:
+                        continue
                 best_info = best_node_info_by_round_mp.get((round_num, mp))
                 best_info = best_node_info_by_round_mp.get((round_num, mp))
                 if not best_info or score < best_info[0]:
                 if not best_info or score < best_info[0]:
                     continue
                     continue
@@ -409,9 +402,11 @@ def build_visualize_edges(
             if not output_names_this_edge:
             if not output_names_this_edge:
                 continue
                 continue
 
 
-            for mp, _r, score, _f, _o in output_names_this_edge:
+            for mp, _r, score, is_fully, _o in output_names_this_edge:
                 round_output_seen.add((round_num, mp))
                 round_output_seen.add((round_num, mp))
-                best_score_by_node[mp] = max(best_score_by_node.get(mp, -1.0), score)
+                prev = prev_best_by_node.get(mp)
+                if prev is None or (not prev[1] and (is_fully or score > prev[0])):
+                    prev_best_by_node[mp] = (score, is_fully)
 
 
             input_data = dr.get("input") or {}
             input_data = dr.get("input") or {}
             derived_nodes = input_data.get("derived_nodes") or []
             derived_nodes = input_data.get("derived_nodes") or []
@@ -454,28 +449,30 @@ def build_visualize_edges(
                 "detail": detail,
                 "detail": detail,
             })
             })
 
 
-    # 处理最后一轮的 fully_derived_nodes
-    if current_round is not None:
-        for (rn, name), (score, is_fully, _out_item, _method) in best_node_info_by_round_mp.items():
-            if rn == current_round and is_fully:
-                fully_derived_nodes.add(name)
-
     # 根据按 (round, mp) 聚合后的最佳信息生成 node_list
     # 根据按 (round, mp) 聚合后的最佳信息生成 node_list
-    # 规则:若某节点在某轮已经 is_fully_derived=True,则之后轮次即便分数更高也不再保留该节点
-    first_full_round_by_name: dict[str, int] = {}
-    for (round_num, mp), (_score, is_fully, _out_item, _method) in best_node_info_by_round_mp.items():
-        if not is_fully:
-            continue
-        prev = first_full_round_by_name.get(mp)
-        if prev is None or round_num < prev:
-            first_full_round_by_name[mp] = round_num
-
+    # 规则:节点首次出现保留;is_fully_derived 从 false 变 true 时保留;
+    # is_fully_derived=false 且分数高于之前已保留轮次时保留;其余情况跳过
+    prev_node_best: dict[str, tuple[float, bool]] = {}  # mp -> (score, is_fully) of last included round
     node_list: list[dict] = []
     node_list: list[dict] = []
-    for (round_num, mp), (score, is_fully, out_item, method) in best_node_info_by_round_mp.items():
-        full_round = first_full_round_by_name.get(mp)
-        # 若存在更早的 fully 轮次,且当前轮次在其之后,则不再保留
-        if full_round is not None and round_num > full_round:
+    for (round_num, mp), (score, is_fully, out_item, method) in sorted(
+        best_node_info_by_round_mp.items(), key=lambda x: (x[0][0], x[0][1])
+    ):
+        prev = prev_node_best.get(mp)
+        if prev is None:
+            should_include = True
+        else:
+            prev_score, prev_fully = prev
+            if prev_fully:
+                should_include = False
+            elif is_fully:
+                should_include = True
+            elif score > prev_score:
+                should_include = True
+            else:
+                should_include = False
+        if not should_include:
             continue
             continue
+        prev_node_best[mp] = (score, is_fully)
         base = dict(topic_by_name.get(mp, {"name": mp, "point": "", "dimension": "", "root_source": "", "root_sources_desc": ""}))
         base = dict(topic_by_name.get(mp, {"name": mp, "point": "", "dimension": "", "root_source": "", "root_sources_desc": ""}))
         base["level"] = round_num
         base["level"] = round_num
         base.setdefault("original_word", base.get("name", mp))
         base.setdefault("original_word", base.get("name", mp))
@@ -558,9 +555,9 @@ if __name__ == "__main__":
     account_name="家有大志"
     account_name="家有大志"
 
 
     items = [
     items = [
-        {"post_id":"68fb6a5c000000000302e5de","log_id":"20260318220540"},
-        {"post_id":"69185d49000000000d00f94e","log_id":"20260318221136"},
-        {"post_id":"6921937a000000001b0278d1","log_id":"20260318221538"}
+        {"post_id":"68fb6a5c000000000302e5de","log_id":"20260319134630"},
+        {"post_id":"69185d49000000000d00f94e","log_id":"20260319140603"},
+        {"post_id":"6921937a000000001b0278d1","log_id":"20260319141843"}
     ]
     ]
     for item in items:
     for item in items:
         post_id = item["post_id"]
         post_id = item["post_id"]

+ 20 - 8
examples_how/overall_derivation/skills/derivation_search.md

@@ -21,6 +21,7 @@ description: 选题点推导-信息搜索子 Agent,根据主 agent 传入的
 - **post_id**:帖子 ID
 - **post_id**:帖子 ID
 - **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
 - **derived_success_set**:完全推导成功的选题点列表,每项包含 `topic`(matched_post_point)和 `source_node`
 - **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**
 - **partial_derived_set**:部分推导成功的选题点列表,每项**只包含 `source_node`**
+- **dimension_data**:主 agent 从上一轮 `round_pattern_dimension_analyze` 获取的维度数据文本,包含各节点所属维度信息;若为空字符串则忽略
 
 
 示例:
 示例:
 ```
 ```
@@ -28,37 +29,46 @@ description: 选题点推导-信息搜索子 Agent,根据主 agent 传入的
 post_id=yyy
 post_id=yyy
 derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]
 derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]
 partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具"}]
 partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具"}]
+dimension_data=已推导维度:分享(分享)、物品(日常物品);未推导维度:故事编排、构图与布局
 ```
 ```
 
 
 ## 操作步骤
 ## 操作步骤
 
 
 ### 步骤一:提取参数
 ### 步骤一:提取参数
-从 `task` 中提取 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set`。
+从 `task` 中提取 `account_name`、`post_id`、`derived_success_set`、`partial_derived_set`、`dimension_data`
 
 
 ### 步骤二:构造多个搜索 query
 ### 步骤二:构造多个搜索 query
 
 
 根据传入的已推导集合参数,自主构造**多个** query 组成 `query_list`。**必须严格遵守以下关键词约束规则**:
 根据传入的已推导集合参数,自主构造**多个** query 组成 `query_list`。**必须严格遵守以下关键词约束规则**:
 
 
-#### 关键词来源(仅限以下类)
+#### 关键词来源(仅限以下类)
 1. **完全推导成功选题点的 `topic` 名称**:即 `derived_success_set` 中每项的 `topic` 字段(对应帖子选题点名称 `matched_post_point`)
 1. **完全推导成功选题点的 `topic` 名称**:即 `derived_success_set` 中每项的 `topic` 字段(对应帖子选题点名称 `matched_post_point`)
 2. **人设树节点名称**:即 `derived_success_set` 中每项的 `source_node` 字段,以及 `partial_derived_set` 中每项的 `source_node` 字段
 2. **人设树节点名称**:即 `derived_success_set` 中每项的 `source_node` 字段,以及 `partial_derived_set` 中每项的 `source_node` 字段
+3. **维度字段**:从 `dimension_data` 中解析出与 `topic` `source_node` 关联的维度名称——当某个 `topic` `source_node` 在 `dimension_data` 中明确归属某维度时,该维度名称可作为辅助关键词参与 query 组合
 
 
 #### 严禁使用的关键词
 #### 严禁使用的关键词
 - **禁止**使用 `partial_derived_set` 中的 `matched_post_point`(部分推导成功的帖子选题点名称)——注意 `partial_derived_set` 传入时本身不含 `matched_post_point` 字段,此规则确保你不会自行推测或补充
 - **禁止**使用 `partial_derived_set` 中的 `matched_post_point`(部分推导成功的帖子选题点名称)——注意 `partial_derived_set` 传入时本身不含 `matched_post_point` 字段,此规则确保你不会自行推测或补充
 - **禁止**使用大模型自行推测或联想出的关键词
 - **禁止**使用大模型自行推测或联想出的关键词
 - **禁止**使用账号名称(`account_name`)作为搜索关键词
 - **禁止**使用账号名称(`account_name`)作为搜索关键词
+- **禁止**将 `dimension_data` 中与 `topic` `source_node` 无明确关联的维度名称用作关键词
 
 
 #### query 构造策略(核心规则)
 #### query 构造策略(核心规则)
 
 
 **以 `partial_derived_set` 中的 `source_node` 为核心构建每个 query**:
 **以 `partial_derived_set` 中的 `source_node` 为核心构建每个 query**:
 - 每个 query 应以一个 `partial_derived_set` 中的 `source_node` 作为**主关键词**
 - 每个 query 应以一个 `partial_derived_set` 中的 `source_node` 作为**主关键词**
 - 然后从 `derived_success_set` 中选取 1~2 个 `topic` 字段作为**辅助关键词**进行组合
 - 然后从 `derived_success_set` 中选取 1~2 个 `topic` 字段作为**辅助关键词**进行组合
+- 若 `dimension_data` 非空,可进一步从中提取该 `topic` `source_node` 所关联的维度名称,将维度名称追加为额外辅助关键词,使 query 更具语义方向性
 - 这样构建的 query 才更有机会搜索出帖子中其他未推导出的选题点
 - 这样构建的 query 才更有机会搜索出帖子中其他未推导出的选题点
 
 
+**`dimension_data` 的使用方式**:
+- 解析 `dimension_data` 文本,找出各 `topic` `source_node` 所属的维度名称(如"趣味道具"属于"物品"维度)
+- 构造以该 `source_node` 为主关键词的 query 时,可将其所属维度名称(如"物品")作为补充关键词加入 query
+- 维度名称只是**辅助**,不强制每个 query 都使用;当维度信息能提升 query 精准度时再加入
+
 **query 数量**:
 **query 数量**:
 - 为 `partial_derived_set` 中的**每个 `source_node`** 至少构建 1 个 query
 - 为 `partial_derived_set` 中的**每个 `source_node`** 至少构建 1 个 query
-- 同一个 `source_node` 可搭配不同的 `topic` 组合构建多个 query,以覆盖更多搜索方向
-- 总 query 数量建议在 1~5 个之间(根据 `partial_derived_set` 大小灵活调整)
+- 同一个 `source_node` 可搭配不同的 `topic` 或维度名称组合构建多个 query,以覆盖更多搜索方向
+- 总 query 数量建议在 1~3 个之间(根据 `partial_derived_set` 大小灵活调整)
 
 
 **单个 query 格式**:
 **单个 query 格式**:
 - 每个 query 由 2~4 个关键词组成,用空格分隔
 - 每个 query 由 2~4 个关键词组成,用空格分隔
@@ -68,17 +78,19 @@ partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具
 假设输入:
 假设输入:
 - `derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]`
 - `derived_success_set=[{"topic":"分享","source_node":"分享"},{"topic":"日常物品","source_node":"日常物品"}]`
 - `partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具"}]`
 - `partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具"}]`
+- `dimension_data=已推导维度:分享、物品(日常物品);未推导维度:故事编排、构图与布局`(从中可知"趣味道具"属于"物品"维度)
 
 
 合法 query_list 示例:
 合法 query_list 示例:
-- `["趣味道具 分享", "趣味道具 日常物品", "创意玩具 分享", "创意玩具 日常物品"]`
-  - 每个 query 以 partial_derived_set 的 source_node 为核心,搭配 derived_success_set 的 topic
+- `["趣味道具 分享", "趣味道具 日常物品 物品", "创意玩具 分享", "创意玩具 日常物品"]`
+  - "趣味道具 日常物品 物品":以 source_node 为核心,搭配 topic,并补充关联维度"物品"
 
 
 简化版(当 partial_derived_set 较少时):
 简化版(当 partial_derived_set 较少时):
-- `["趣味道具 分享 日常物品", "创意玩具 分享"]`
+- `["趣味道具 分享 物品", "创意玩具 分享"]`
 
 
 非法 query 示例:
 非法 query 示例:
 - `xxx账号 分享 日常物品`(使用了账号名称)
 - `xxx账号 分享 日常物品`(使用了账号名称)
 - `分享 创意生活 家居改造`("创意生活"和"家居改造"不在合法来源中,属于自行联想)
 - `分享 创意生活 家居改造`("创意生活"和"家居改造"不在合法来源中,属于自行联想)
+- `趣味道具 故事编排`("故事编排"与"趣味道具"无明确关联,不得使用)
 
 
 ### 步骤三:调用 search_and_eval 工具
 ### 步骤三:调用 search_and_eval 工具
 调用工具 **search_and_eval**,传入以下参数:
 调用工具 **search_and_eval**,传入以下参数:
@@ -136,6 +148,6 @@ partial_derived_set=[{"source_node":"趣味道具"},{"source_node":"创意玩具
 
 
 ## 约束
 ## 约束
 - **仅调用一次 search_and_eval**:每次被调用只调用一次 `search_and_eval` 工具(传入 `query_list`),工具内部并发处理多个 query,不要多轮搜索或合并历史结果。
 - **仅调用一次 search_and_eval**:每次被调用只调用一次 `search_and_eval` 工具(传入 `query_list`),工具内部并发处理多个 query,不要多轮搜索或合并历史结果。
-- **闭眼搜索**:query 中的关键词**只能来自** `derived_success_set` 的 `topic`/`source_node` 以及 `partial_derived_set` 的 `source_node`,不得自行编造或联想新关键词,不得使用账号名称。
+- **闭眼搜索**:query 中的关键词**只能来自** `derived_success_set` 的 `topic`/`source_node`、`partial_derived_set` 的 `source_node`,以及 `dimension_data` 中与对应 `topic` `source_node` 明确关联的维度名称,不得自行编造或联想新关键词,不得使用账号名称。
 - **不替主 agent 做推导**:你只负责构造 query、调用工具、整理返回结果。不判断"能推导出哪些选题点"或"该选题点是否应加入推导集合";由主 agent 根据你的返回整理推导路径。
 - **不替主 agent 做推导**:你只负责构造 query、调用工具、整理返回结果。不判断"能推导出哪些选题点"或"该选题点是否应加入推导集合";由主 agent 根据你的返回整理推导路径。
 - **不直接调用 search_posts 或 point_match**:搜索、评估、匹配均由 `search_and_eval` 工具内部完成,你不得单独调用这些工具。
 - **不直接调用 search_posts 或 point_match**:搜索、评估、匹配均由 `search_and_eval` 工具内部完成,你不得单独调用这些工具。

+ 2 - 2
examples_how/overall_derivation/tools/find_tree_node.py

@@ -427,7 +427,7 @@ async def find_tree_nodes_by_conditional_ratio(
                     match_str = str(match_info)
                     match_str = str(match_info)
                 dim_label = x.get("所属维度", "—")
                 dim_label = x.get("所属维度", "—")
                 lines.append(
                 lines.append(
-                    f"- {x['节点名称']}\t条件概率={x['条件概率']}\t父节点={x['父节点名称']}\t所属维度={dim_label}\t帖子选题点匹配={match_str}"
+                    f"- {x['节点名称']}\t条件概率={x['条件概率']}\t所属维度={dim_label}\t帖子选题点匹配={match_str}"
                 )
                 )
             output = "\n".join(lines)
             output = "\n".join(lines)
         return ToolResult(
         return ToolResult(
@@ -465,7 +465,7 @@ def main() -> None:
     #     {"topic": "分享", "source_node": "分享"},
     #     {"topic": "分享", "source_node": "分享"},
     #     {"topic": "叙事结构", "source_node": "叙事结构"},
     #     {"topic": "叙事结构", "source_node": "叙事结构"},
     # ]
     # ]
-    derived_items = [{"topic":"分享","source_node":"分享"},{"topic":"叙事结构","source_node":"叙事编排"},{"topic":"幽默化标题","source_node":"幽默化标题"},{"source_node":"叙事结构","topic":"叙事结构"},{"topic":"夸张堆叠","source_node":"夸张转化"},{"topic":"居家生活场景","source_node":"生活场景"},{"topic":"图片文字","source_node":"图片文字"},{"source_node":"补充说明式","topic":"补充说明式"},{"topic":"标题","source_node":"标题"},{"topic":"递进式","source_node":"递进式"},{"source_node":"荒诞夸张","topic":"夸张堆叠"},{"topic":"图片文字","source_node":"图文组合"},{"topic":"补充说明式","source_node":"解读说明"},{"source_node":"分步骤","topic":"递进式"},{"source_node":"视觉证据","topic":"拖鞋物证"},{"topic":"鞋架","source_node":"家居器具"},{"topic":"柴犬形象","source_node":"形象演绎"},{"topic":"叙事结构","source_node":"结构编排"},{"topic":"图片文字","source_node":"图文编排"},{"source_node":"形象","topic":"柴犬形象"},{"source_node":"版面结构","topic":"叙事结构"},{"source_node":"夸张变形","topic":"夸张堆叠"},{"source_node":"夸张造型","topic":"夸张堆叠"},{"topic":"夸张堆叠","source_node":"夸张穿戴法"}]
+    derived_items = [{"topic":"分享","source_node":"分享"},{"topic":"叙事结构","source_node":"叙事编排"},{"topic":"幽默化标题","source_node":"幽默化标题"},{"source_node":"叙事结构","topic":"叙事结构"},{"topic":"夸张堆叠","source_node":"夸张转化"},{"topic":"居家生活场景","source_node":"生活场景"},{"topic":"图片文字","source_node":"图片文字"},{"source_node":"补充说明式","topic":"补充说明式"},{"topic":"标题","source_node":"标题"},{"topic":"递进式","source_node":"递进式"}]
     conditional_ratio_threshold = 0.2
     conditional_ratio_threshold = 0.2
     top_n = 2000
     top_n = 2000
 
 

+ 3 - 3
examples_how/overall_derivation/tools/pattern_dimension_analyze.py

@@ -1014,9 +1014,9 @@ if __name__ == "__main__":
     test_round = 1
     test_round = 1
 
 
     items = [
     items = [
-        {"post_id": "68fb6a5c000000000302e5de", "log_id": "20260318220540"},
-        {"post_id": "69185d49000000000d00f94e", "log_id": "20260318221136"},
-        {"post_id": "6921937a000000001b0278d1", "log_id": "20260318221538"}
+        {"post_id": "68fb6a5c000000000302e5de", "log_id": "20260319134630"},
+        {"post_id": "69185d49000000000d00f94e", "log_id": "20260319140603"},
+        {"post_id": "6921937a000000001b0278d1", "log_id": "20260319141843"}
     ]
     ]
 
 
     if run_round_pattern_test:
     if run_round_pattern_test:

+ 9 - 7
examples_how/overall_derivation/tools/search_and_eval.py

@@ -106,7 +106,8 @@ async def _do_search(query: str, channel: str) -> Optional[List[dict]]:
 
 
 async def _search_posts(query: str) -> List[dict]:
 async def _search_posts(query: str) -> List[dict]:
     """优先用 xhs 搜索,失败或空则用 zhihu,返回帖子列表"""
     """优先用 xhs 搜索,失败或空则用 zhihu,返回帖子列表"""
-    posts = await _do_search(query, "xhs")
+    xhs_query = query.replace(" ", "")
+    posts = await _do_search(xhs_query, "xhs")
     if posts:
     if posts:
         logger.info("_search_posts: using xhs, %d posts for query=%s", len(posts), query)
         logger.info("_search_posts: using xhs, %d posts for query=%s", len(posts), query)
         return posts
         return posts
@@ -295,12 +296,7 @@ async def _search_and_eval_single_query(
     return results
     return results
 
 
 
 
-@tool(
-    description=(
-        "搜索帖子并评估是否与账号人设匹配,提取帖子关键词并与帖子选题点进行匹配。"
-        "参数:account_name 账号名称;post_id 帖子ID;query_list 搜索词列表。"
-    )
-)
+@tool()
 async def search_and_eval(
 async def search_and_eval(
     account_name: str,
     account_name: str,
     post_id: str,
     post_id: str,
@@ -337,6 +333,12 @@ async def search_and_eval(
         query_list,
         query_list,
     )
     )
 
 
+    # if True:
+    #     return ToolResult(
+    #         title="搜索评估工具不可用",
+    #         output="搜索评估工具不可用"
+    #     )
+
     if not query_list:
     if not query_list:
         return ToolResult(
         return ToolResult(
             title="搜索评估: 空 query_list",
             title="搜索评估: 空 query_list",