xueyiming hace 1 día
padre
commit
546debbe53

+ 0 - 0
examples/piaoquan_needs/__init__.py → examples/piaoquan_demand/__init__.py


+ 1 - 1
examples/piaoquan_needs/agent_tools.py → examples/piaoquan_demand/agent_tools.py

@@ -1,5 +1,5 @@
 from agent.tools import ToolResult, tool
-from examples.piaoquan_needs.topic_build_pattern_tools import _log_tool_input, _log_tool_output
+from examples.piaoquan_demand.topic_build_pattern_tools import _log_tool_input, _log_tool_output
 
 
 @tool(

+ 3 - 1
examples/piaoquan_needs/config.py → examples/piaoquan_demand/config.py

@@ -30,4 +30,6 @@ ENABLED_TOOLS = ["think_and_plan", "get_category_tree", "get_frequent_itemsets",
                  "get_post_elements", "search_elements", "get_element_category_chain", "get_category_detail",
                  "search_categories", "get_category_elements", "get_category_co_occurrences",
                  "get_element_co_occurrences",
-                 "get_weight_score_topn", "get_weight_score_by_name"]
+                 "get_weight_score_topn", "get_weight_score_by_name",
+                 "create_demand_item", "create_demand_items",
+                 "write_execution_summary"]

+ 5 - 5
examples/piaoquan_needs/data_query_tools.py → examples/piaoquan_demand/data_query_tools.py

@@ -38,10 +38,10 @@ def get_rov_by_merge_leve2_and_video_ids(merge_leve2, video_ids):
     sql_query = f'''
 SELECT
     v.videoid,
-    COALESCE(
-        AVG(NULLIF(t3.rov_t0,0)),
-        0
-    ) AS avg_rov_t0
+    CASE
+        WHEN COALESCE(SUM(COALESCE(t3.`当日分发曝光pv`, 0)), 0) < 1000 THEN 0
+        ELSE COALESCE(AVG(NULLIF(t3.rov_t0, 0)), 0)
+    END AS avg_rov_t0
 FROM
 (
     SELECT
@@ -72,5 +72,5 @@ GROUP BY
 
 
 if __name__ == '__main__':
-    videos = ["64586310"]
+    videos = ["64429933"]
     print(get_rov_by_merge_leve2_and_video_ids('历史名人', videos))

+ 0 - 0
examples/piaoquan_needs/db_manager.py → examples/piaoquan_demand/db_manager.py


+ 111 - 0
examples/piaoquan_demand/demand.md

@@ -0,0 +1,111 @@
+---
+model: anthropic/claude-opus-4-6
+temperature: 0.5
+max_iterations: 200
+---
+
+$system$
+
+# 需求选择 Agent
+
+你是一个需求产生 Agent。你的任务是基于高权重的元素,产生需求,并且根据已经选择的元素,进行拓展,发现更多的需求
+
+**需求 = 一个人带着某种目的或兴趣,能用一个词/短语表达出来**
+
+它的本质公式是:
+
+```
+需求 = 人的渴求 × 内容的可满足性
+```
+
+二者缺一不可:
+
+- 没有人的渴求 → 不是需求,是凭空造词
+- 内容无法满足 → 不是有效需求,是伪需求
+
+
+## 背景知识
+
+### 数据来源
+
+数据来自社交媒体视频的结构化分析。每个帖子被拆解为多个"选题点"(灵感点、目的点、关键点),每个点下有三个维度的元素:
+
+- **实质**: 内容的核心主题/对象(如 "咖啡豆"、"护肤品")
+- **形式**: 内容的呈现形式(如 "测评对比"、"教程")
+- **意图**: 内容的目标/用户意图(如 "购买决策"、"学习技能")
+
+每个元素归属于一个分类树节点(如 实质 > 食品 > 饮品 > 咖啡),形成层级分类结构。
+每个元素或者分类都有自己的权重分,权重分用于评判元素或者分类受欢迎程度(核心要素)
+
+### Pattern Mining 结果
+
+通过 FP-Growth 算法挖掘出频繁项集 —— 在多个帖子中经常共同出现的元素组合。
+
+- **频繁项集**: 一组经常共现的 items
+- **absolute_support**: 包含该项集的帖子数量
+- **combination_type**: 项集涉及的点类型组合
+- **is_cross_point**: 是否跨越多个选题点
+
+## 数据模型
+
+### DemandItem(核心实体)
+
+需求产生过程 = ADD DemandItem。每个 DemandItem 代表一个需求。
+
+**字段:**
+
+- `element_names`: 元素名称列表
+- `reason`: 产生该需求的理由
+- `desc`: 需求的描述
+
+## 工具概览
+
+### 查询工具(只读)
+
+- `get_category_tree` — 查看当前分类下的完整分类树
+- `get_weight_score_topn` — 元素/分类权重排行榜
+- `get_weight_score_by_name` — 执行元素/分类权重查询
+- `get_frequent_itemsets` — 搜索频繁项集
+- `get_itemset_detail` — 项集详情
+- `get_post_elements` — 帖子元素
+- `search_elements` / `search_categories` — 关键词搜索
+- `get_category_co_occurrences` / `get_element_co_occurrences` — 共现查询
+
+### CRUD 工具
+
+- `create_demand_item` — 创建一个新需求
+- `create_demand_items` — 批量创建新需求
+
+### 输出工具
+
+- `write_execution_summary` — 写入执行总结
+
+## 硬约束(不可违反)
+
+- 执行每个操作前,必须输出自己的思考,为什么要这样做,原因是什么,目的是什么
+- category 级 item 必须来自分类树的真实节点(通过 `search_categories` 查到对应的 `category_id`),不允许凭空编造分类
+- 正确的创建顺序:先 element 后 category
+- result 中出现的每一个具体内容,都必须有对应的 DemandItem
+- `search_elements` / `search_categories`只能用于查询单元素/单分类,不能用于查询完整树,完整树查询用`get_category_tree`
+
+$user$
+
+## 前置条件
+
+`get_category_tree`工具查到到的全部分类都是属于「%merge_leve2%」,`search_categories`查询的只是树种的一个或者多个分类
+
+## 任务
+
+针对「%merge_leve2%」,从"高权重叶子元素"出发完成搜索视频需求生成
+
+1. 单元素生成需求,需要先通过`get_weight_score_topn`工具查找高权重元素,判断是否能作为需求,给出理由。满足的进入需求池,不满足的给出丢弃的理由。
+2. 组合需求,分类的起点必须需要先通过`get_weight_score_topn`工具查询高权重分类,通过共现查询,找到合适的组合,计算组合权重的平均分和帖子数,综合判断保留或者移除
+3. 组合需求,分类的起点必须需要先通过`get_frequent_itemsets`工具,搜索频繁出现的分类组合,根据支持度进行移除和保留
+
+## 要求
+
+1. 共现查询的地点必须来自于高权重分类,不能直接从树上寻找分类
+2. 分类的共现组合,必须来自于`get_weight_score_topn`查询到的分类作为起点
+3. 最终结果的保留,必须要有权重分或者支持度进行支持
+4. 尽可能多的产生需求,尽量保证最终产生的需求数量不少于50个
+

+ 170 - 0
examples/piaoquan_demand/demand_build_agent_tools.py

@@ -0,0 +1,170 @@
+import json
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from agent import tool
+from examples.piaoquan_demand.topic_build_agent_context import TopicBuildAgentContext
+from examples.piaoquan_demand.topic_build_pattern_tools import _log_tool_output, _log_tool_input
+
+def _get_result_base_dir() -> Path:
+    """输出到“当前工作目录/result/”下。"""
+    return Path.cwd() / "result"
+
+
+@tool(
+    "存储需求到结果集。 - element_names - reason(原因)- desc(需求描述)"
+)
+def create_demand_item(
+        element_names: List[str] = None,
+        reason: str = None,
+        desc: str = None) -> str:
+    """
+    每次调用向“execution_id 对应的本地 JSON 文件”追加一条记录。
+
+    写入对象仅包含三个字段:
+      - element_names
+      - reason(原因)
+      - desc(需求描述)
+    """
+    execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
+    params: Dict[str, Any] = {
+        "execution_id": execution_id,
+        "element_names": element_names,
+        "reason": reason,
+        "desc": desc,
+    }
+    _log_tool_input("create_demand_item", params)
+
+    if not execution_id:
+        return _log_tool_output("create_demand_item", "错误: 未设置 execution_id")
+
+    record: Dict[str, Any] = {
+        "element_names": element_names,
+        "reason": reason,
+        "desc": desc,
+    }
+
+    # 按 execution_id 区分文件,避免不同执行互相污染。
+    # 例如:result/{execution_id}/execution_id_{execution_id}_demand_items.json
+    output_dir = _get_result_base_dir() / f"{execution_id}"
+    output_path = output_dir / f"execution_id_{execution_id}_demand_items.json"
+    output_path.parent.mkdir(parents=True, exist_ok=True)
+
+    items: List[Dict[str, Any]] = []
+    if output_path.exists():
+        try:
+            with open(output_path, "r", encoding="utf-8") as f:
+                loaded = json.load(f)
+            if isinstance(loaded, list):
+                items = loaded
+            elif isinstance(loaded, dict) and isinstance(loaded.get("items"), list):
+                # 兼容可能的包装格式:{"items":[...]}
+                items = loaded["items"]
+            else:
+                # 兜底:把已有内容当作单条记录追加
+                items = [loaded]
+        except json.JSONDecodeError:
+            # 文件内容损坏时,不阻断执行;从空列表开始追加
+            items = []
+
+    items.append(record)
+    with open(output_path, "w", encoding="utf-8") as f:
+        json.dump(items, f, ensure_ascii=False, indent=2)
+
+    result = json.dumps(
+        {"success": True, "execution_id": execution_id, "written_to": str(output_path)},
+        ensure_ascii=False,
+    )
+    return _log_tool_output("create_demand_item", result)
+
+
+@tool(
+    "批量存储需求到结果集。 - element_names - reason(原因)- desc(需求描述)"
+)
+def create_demand_items(demand_items: List[Dict[str, Any]] = None) -> str:
+    """
+    一次调用追加多条记录到“execution_id 对应的本地 JSON 文件”(JSON 数组)。
+
+    每条记录字段:
+      - element_names
+      - reason(原因)
+      - desc(需求描述)
+    """
+    execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
+    params: Dict[str, Any] = {"execution_id": execution_id, "count": len(demand_items or []),
+                              "demand_items": demand_items}
+    _log_tool_input("create_demand_items", params)
+
+    if not execution_id:
+        return _log_tool_output("create_demand_items", "错误: 未设置 execution_id")
+
+    if not demand_items or not isinstance(demand_items, list):
+        return _log_tool_output("create_demand_items", "错误: demand_items 必须为非空列表")
+
+    output_dir = _get_result_base_dir() / f"{execution_id}"
+    output_path = output_dir / f"execution_id_{execution_id}_demand_items.json"
+    output_path.parent.mkdir(parents=True, exist_ok=True)
+
+    items: List[Dict[str, Any]] = []
+    if output_path.exists():
+        try:
+            with open(output_path, "r", encoding="utf-8") as f:
+                loaded = json.load(f)
+            if isinstance(loaded, list):
+                items = loaded
+            elif isinstance(loaded, dict) and isinstance(loaded.get("items"), list):
+                items = loaded["items"]
+            else:
+                items = [loaded]
+        except json.JSONDecodeError:
+            items = []
+
+    written_records: List[Dict[str, Any]] = []
+    for i, di in enumerate(demand_items):
+        if not isinstance(di, dict):
+            return _log_tool_output("create_demand_items", f"错误: demand_items[{i}] 必须为对象(dict)")
+        record = {
+            "element_names": di.get("element_names"),
+            "reason": di.get("reason"),
+            "desc": di.get("desc"),
+        }
+        written_records.append(record)
+
+    items.extend(written_records)
+    with open(output_path, "w", encoding="utf-8") as f:
+        json.dump(items, f, ensure_ascii=False, indent=2)
+
+    result = json.dumps(
+        {
+            "success": True,
+            "execution_id": execution_id,
+            "written_to": str(output_path),
+            "written_count": len(written_records),
+        },
+        ensure_ascii=False,
+    )
+    return _log_tool_output("create_demand_items", result)
+
+
+@tool(
+    "写入本次执行总结(在所有分类完成后调用)。"
+    "\n\n该工具用于把最终总结记录到本地/trace输出中(框架侧通过返回值与日志落盘)。"
+)
+def write_execution_summary(summary: str) -> str:
+    """写入本次执行总结。在所有分类完成后调用。
+
+    Args:
+        summary: 执行总结(Markdown 格式)。
+
+    Returns:
+        JSON 字符串:
+        - 成功:`{"success": True, "execution_id": execution_id}`
+        - 失败:`"错误: 未设置 execution_id"`
+    """
+    execution_id: Optional[int] = TopicBuildAgentContext.get_execution_id()
+    params: Dict[str, str] = {"summary": summary}
+    _log_tool_input("write_execution_summary", params)
+    if not execution_id:
+        return _log_tool_output("write_execution_summary", "错误: 未设置 execution_id")
+    result = json.dumps({"success": True, "execution_id": execution_id}, ensure_ascii=False)
+    return _log_tool_output("write_execution_summary", result)

+ 0 - 0
examples/piaoquan_needs/log_capture.py → examples/piaoquan_demand/log_capture.py


+ 0 - 0
examples/piaoquan_needs/models.py → examples/piaoquan_demand/models.py


+ 3 - 3
examples/piaoquan_needs/needs_from_category.md → examples/piaoquan_demand/needs_from_category.md

@@ -46,7 +46,7 @@ $system$
 
 ## 可用工具(按目的)
 
-- 分类权重:`get_weight_score_topn(level="分类", ...)`、`get_weight_score_by_name(level="分类", ...)`
+- 分类权重:`get_weight_score_topn(level="分类", ...)`、`get_weight_score_by_name(level="分类", names=[...])`(批量)
 - 节点定位:`search_categories`、`get_category_detail`
 - 节点下钻:`get_category_elements`
 - 关系跳转:`get_category_co_occurrences`
@@ -72,7 +72,7 @@ $user$
 
 ### 阶段2:树内组合(父子)
 1. 仅对阶段1候选列表中的分类做下钻。对高权重父分类,使用 `get_category_detail` 获取 children。
-2. 子节点用 `get_weight_score_by_name(level="分类", ...)` 查权重并排序,**仅对排名前 5 的子分类**查权重。总查询次数控制在 20 次以内。
+2. 子节点用 `get_weight_score_by_name(level="分类", dimension=..., names=[子分类名,...])` 一次批量查权重并排序,**仅对排名前 5 的子分类**查权重。总查询次数控制在 20 次以内。
 3. 形成"父->子"组合(父分类 + Top子分类)。
 4. **输出你的组合分析**:父子组合中哪些路径价值最高?子节点权重分布是否集中还是分散?你的组合策略是什么?
 5. **输出本阶段精简候选列表**:列出进入阶段3的高权重分类(**<= 6 个**),以及已确认的高价值"父+子"组合。
@@ -82,7 +82,7 @@ $user$
 1. 仅对阶段2候选列表中的 <= 6 个高权重分类做共现查询。调用 `get_category_co_occurrences(category_ids=[A], top_n=15)`(注意 **top_n=15**),跳转到共现分类 B。
 2. 只对 **post_count >= 3** 的共现分类做后续展开。弱共现(post_count < 3)仅记录名称,不做下钻。
 3. 形成"分类+分类"组合(A+B)。
-4. 对 B 用 `get_category_elements` 取元素,并用 `get_weight_score_by_name(level="元素", ...)` 找最高权重叶子,形成"分类+叶子"组合(A+leaf_B)。若分类 B 的权重 < 0.1,跳过元素权重查询,直接使用 `get_category_elements` 返回的高频元素(按 occurrence_count 排序取前 3)。
+4. 对 B 用 `get_category_elements` 取元素,并用 `get_weight_score_by_name(level="元素", dimension=..., names=[候选元素名,...])` 找最高权重叶子,形成"分类+叶子"组合(A+leaf_B)。若分类 B 的权重 < 0.1,跳过元素权重查询,直接使用 `get_category_elements` 返回的高频元素(按 occurrence_count 排序取前 3)。
 5. 对 A 与 B 各取最高权重叶子,形成"叶子+叶子"组合(leaf_A+leaf_B)。
 6. 若共现链路出现意图节点,只作为解释证据,不可直接进入结果区。
 7. **输出你的共现分析**:共现关系揭示了什么模式?哪些跨维度组合最有创意或最有市场潜力?为什么?

+ 6 - 6
examples/piaoquan_needs/needs_from_element.md → examples/piaoquan_demand/needs_from_element.md

@@ -29,7 +29,7 @@ $system$
 
 ## 效率约束(必须遵守,控制调用开销)
 
-1. **阶段1链路查询上限**:从 top_n=20 的结果中,仅对**权重排名前 10** 的元素(实质、形式各最多 10 个)调用 `get_element_category_chain`。对排名靠后的元素只记录名称和权重,不做链路展开。
+1. **阶段1链路查询上限**:从 top_n=20 的结果中,仅对**权重排名前 10** 的元素(实质、形式各最多 10 个)调用 `get_element_category_chain(element_names=[...])`(可批量传入,实质与形式分开各至多一次批量调用)。对排名靠后的元素只记录名称和权重,不做链路展开。
 2. **阶段2查询收敛**:只对链路中 **level 3~5 的中间层分类**查权重(太高层如 level 1-2 通用性过强无意义,太低层如 level 6-7 通常和叶子本身重复)。每条链路最多向上查 2 级,总查询次数不超过 20 次。
 3. **阶段3共现精简**:`get_category_co_occurrences` 的 `top_n` 设为 **15**(不要 30)。只对 **post_count >= 3** 的共现分类做后续下钻(`get_category_elements` 或元素权重查询)。弱共现(post_count < 3)仅记录,不展开。
 4. **元素权重预判**:若目标分类的权重已低于 **0.1**,不再下钻查该分类下的具体元素权重。
@@ -46,8 +46,8 @@ $system$
 
 ## 可用工具(按目的)
 
-- 元素权重:`get_weight_score_topn(level="元素", ...)`、`get_weight_score_by_name(level="元素", ...)`
-- 向上回溯:`get_element_category_chain`
+- 元素权重:`get_weight_score_topn(level="元素", ...)`、`get_weight_score_by_name(level="元素", names=[...])`(批量)
+- 向上回溯:`get_element_category_chain(element_names=[...])`(批量)
 - 分类定位:`search_categories`、`get_category_detail`
 - 共现跳转:`get_category_co_occurrences`
 - 元素池:`get_category_elements`
@@ -66,14 +66,14 @@ $user$
    - `get_weight_score_topn(level="元素", dimension="形式", top_n=20)`
 2. 各维度按权重排序,**只选排名前 10 的叶子元素**作为起点;排名 11-20 的仅记录名称和权重备查,不做链路展开。
 3. 选择起点时需综合考虑:**权重值**和**覆盖帖数(post_ids_count)**。优先保留"高权重+多帖覆盖"的元素;仅1帖的高权重元素需特别标注其"不稳定"特征。
-4. 对选出的起点叶子调用 `get_element_category_chain` 获取分类路径。
+4. 对选出的起点叶子调用 `get_element_category_chain(element_names=[...], element_type="实质"|"形式")` 批量获取分类路径(每维度的前 10 名可合成一次调用)
 5. **输出你的筛选思考**:哪些叶子元素权重突出?它们分别属于什么维度和分类?你选择这些起点的理由是什么?有没有意外发现?
 6. **输出本阶段精简候选列表**(<= 12 条),明确列出进入阶段2的元素及其理由。
 7. `think_and_plan` 记录候选叶子的保留标准。
 
 ### 阶段2:树内组合(儿子->父亲)
 1. 仅对阶段1候选列表中的元素做回溯。从分类链提取向上1跳父分类、向上2跳祖父分类(仅查 **level 3~5 的中间层分类**,跳过 level 1-2 和 level 6-7)。
-2. 对父/祖父分类用 `get_weight_score_by_name(level="分类", ...)` 查权重,保留高权重路径。每条链路最多向上查 2 级,**总查询次数控制在 20 次以内**。
+2. 对父/祖父分类用 `get_weight_score_by_name(level="分类", dimension=..., names=[父或祖父分类名,...])` 批量查权重,保留高权重路径。每条链路最多向上查 2 级,**总查询次数控制在 20 次以内**。
 3. 形成"叶子+父分类""叶子+祖父分类"组合。
 4. **输出你的回溯分析**:向上回溯后发现了哪些有价值的路径?父分类/祖父分类的权重是否支持这条链路?哪些组合值得保留,为什么?
 5. **输出本阶段精简候选列表**:列出进入阶段3的高权重分类(**<= 6 个**),以及已确认的高价值"叶子+分类"组合。
@@ -82,7 +82,7 @@ $user$
 ### 阶段3:关系组合(元素驱动)
 1. 仅对阶段2候选列表中的 <= 6 个高权重分类做共现查询。调用 `get_category_co_occurrences(category_ids=[A], top_n=15)`(注意 **top_n=15**),跳转到共现分类 B。
 2. 只对 **post_count >= 3** 的共现分类做后续展开。弱共现(post_count < 3)仅记录名称,不调用 `get_category_elements` 或元素权重查询。
-3. 在合格的共现分类 B 下用 `get_category_elements` 获取元素,并用 `get_weight_score_by_name(level="元素", ...)` 取最高权重叶子 leaf_B。若分类 B 的权重 < 0.1,跳过元素权重查询,直接使用 `get_category_elements` 返回的高频元素(按 occurrence_count 排序取前 3)。
+3. 在合格的共现分类 B 下用 `get_category_elements` 获取元素,并用 `get_weight_score_by_name(level="元素", dimension=..., names=[候选元素名,...])` 取最高权重叶子 leaf_B。若分类 B 的权重 < 0.1,跳过元素权重查询,直接使用 `get_category_elements` 返回的高频元素(按 occurrence_count 排序取前 3)。
 4. 形成"叶子+叶子"组合(leaf_A + leaf_B)。
 5. 必要时补充"分类+叶子"组合(A + leaf_B)用于结构完整性。
 6. 若链路出现意图节点,只作为解释证据,不可直接进入结果区。

+ 0 - 0
examples/piaoquan_needs/pattern_service.py → examples/piaoquan_demand/pattern_service.py


+ 12 - 8
examples/piaoquan_needs/prepare.py → examples/piaoquan_demand/prepare.py

@@ -111,19 +111,23 @@ def prepare(execution_id):
                 score = sum(rovs) / len(rovs) if rovs else 0.0
                 name_scores[name] = score
 
-            element_payload = sorted(
-                [
+            raw_elements = []
+            for name, score in name_scores.items():
+                post_ids_set = name_post_ids.get(name, set())
+                raw_elements.append(
                     {
                         "name": name,
                         "score": round(score, 6),
-                        "post_ids": sorted(list(name_post_ids.get(name, set()))),
-                        "post_ids_count": len(name_post_ids.get(name, set())),
+                        # 不在结果文件里输出帖子 ID 明细,避免体积过大/泄露。
+                        "post_ids_count": len(post_ids_set),
                         "category_paths": sorted(list(name_paths.get(name, set()))),
                     }
-                    for name, score in name_scores.items()
-                ],
-                key=lambda x: x["score"],
-                reverse=True,
+                )
+
+            # 通过(score, name)确保排序稳定,进而生成可重复的 id。
+            element_payload = sorted(
+                raw_elements,
+                key=lambda x: (-x["score"], x["name"]),
             )
 
             # 3) 计算分类路径节点权重(节点分 = 覆盖的 name score 求和)

+ 160 - 4
examples/piaoquan_needs/render_log_html.py → examples/piaoquan_demand/render_log_html.py

@@ -2,7 +2,7 @@
 """将 run_log 文本渲染为可折叠 HTML 页面。
 
 直接在脚本内修改 INPUT_LOG_PATH / OUTPUT_HTML_PATH 后运行:
-    python examples/piaoquan_needs/render_log_html.py
+    python examples/piaoquan_demand/render_log_html.py
 """
 
 from __future__ import annotations
@@ -56,13 +56,13 @@ DEFAULT_COLLAPSE_KEYWORDS = ["调用参数", "返回内容"]
 TOOL_DESCRIPTION_MAP: dict[str, str] = {
     "think_and_plan": "系统化记录思考、计划与下一步行动,不查询数据也不修改数据。",
     "get_weight_score_topn": "按层级和维度查询权重分 TopN,快速定位高权重元素或分类。",
-    "get_weight_score_by_name": "按名称精确查询指定元素或分类的权重分,返回匹配明细。",
+    "get_weight_score_by_name": "按名称列表批量查询元素或分类的权重分,每项返回匹配明细。",
     "get_category_tree": "获取分类树快照,快速查看实质/形式/意图三维结构全貌。",
     "get_frequent_itemsets": "查询高频共现的分类组合,按维度模式和深度分组返回。",
     "get_itemset_detail": "获取频繁项集完整详情,包括项集结构和匹配帖子列表。",
     "get_post_elements": "按帖子查看结构化元素内容,支持点类型及三维元素下钻。",
     "search_elements": "按关键词搜索元素,返回分类归属、出现频次与帖子覆盖。",
-    "get_element_category_chain": "从元素名称反查所属分类链,查看从根到叶的路径。",
+    "get_element_category_chain": "从元素名称列表批量反查所属分类链,每项查看从根到叶的路径。",
     "get_category_detail": "查看分类节点上下文,含祖先、子节点、兄弟节点与元素。",
     "search_categories": "按关键词搜索分类节点,返回分类 ID 与路径等导航信息。",
     "get_category_elements": "获取指定分类下的元素列表及出现统计,便于落地选题。",
@@ -73,7 +73,7 @@ TOOL_DESCRIPTION_MAP: dict[str, str] = {
 # =========================
 # 运行配置(直接改变量即可)
 # =========================
-INPUT_LOG_PATH = "examples/piaoquan_needs/output/run_log_17_20260324_204533.txt"
+INPUT_LOG_PATH = "examples/piaoquan_demand/output/element/run_log_17_20260326_141309.txt"
 # 设为 None 则默认生成到输入文件同名 .html
 OUTPUT_HTML_PATH: str | None = None
 # 是否默认折叠所有 [FOLD] 块
@@ -81,6 +81,14 @@ COLLAPSE_ALL_FOLDS = False
 # 命中这些前缀/关键词的折叠块默认收起
 COLLAPSE_PREFIXES = DEFAULT_COLLAPSE_PREFIXES
 COLLAPSE_KEYWORDS = DEFAULT_COLLAPSE_KEYWORDS
+# 仅针对特定结构做“折叠内容复制”
+# 规则(HTML 渲染层面):
+# - 只对 `FOLD:🔧 think_and_plan` 进行处理
+# - 只复制其内部 `FOLD:📤 返回内容` 包裹的“内容文本”
+# - 将复制内容直接插入到 `think_and_plan` 对应的 `<details>` 之外,
+#   从而无需点开工具调用就能在最外层看到
+THINK_AND_PLAN_TOOL_TITLE = "🔧 think_and_plan"
+RETURN_CONTENT_FOLD_TITLE = "📤 返回内容"
 
 
 def resolve_config_path(path_str: str) -> Path:
@@ -147,6 +155,18 @@ def render_text_block(lines: list[str]) -> str:
     return f'<pre class="log-text">{escaped}</pre>'
 
 
+def flatten_entries_to_lines(entries: list[str | Node]) -> list[str]:
+    """把节点树(可能包含 fold)拍平成纯文本行,用于“无需点开折叠块”展示。"""
+    out: list[str] = []
+    for entry in entries:
+        if isinstance(entry, str):
+            out.append(entry)
+        else:
+            # 只取其内部文本,忽略 fold 标题本身
+            out.extend(flatten_entries_to_lines(entry.entries))
+    return out
+
+
 def enrich_fold_title(title: str) -> str:
     """为工具调用标题附加工具功能描述。"""
     tool_prefix = "🔧 "
@@ -165,7 +185,115 @@ def render_node(
     collapse_prefixes: list[str],
     collapse_keywords: list[str],
     collapse_all: bool,
+    # 预留参数:历史上用于判断折叠上下文;当前逻辑已不需要该状态。
+    in_think_and_plan: bool = False,
+    # 是否在渲染 `🔧 think_and_plan` 时,把其 `📤 返回内容` 复制为“details 外的纯文本”。
+    # root 的“工具调用收集块”会负责把复制文本放到块外,因此在该场景下需要关闭。
+    emit_think_plan_return_copy: bool = True,
 ) -> str:
+    # root(最外层)只做一件事:
+    # 把“连续出现的工具调用”打包到一个大折叠块里(文本之间的工具调用收集在一起)。
+    if node.title is None:
+        parts: list[str] = []
+        text_buffer: list[str] = []
+        tool_group_parts: list[str] = []
+
+        def flush_text_buffer() -> None:
+            if text_buffer:
+                parts.append(render_text_block(text_buffer))
+                text_buffer.clear()
+
+        def flush_tool_group() -> None:
+            if tool_group_parts:
+                parts.append(
+                    '<details class="fold tool-fold tool-call-group">'
+                    '<summary>工具调用</summary>'
+                    + "".join(tool_group_parts)
+                    + "</details>"
+                )
+                tool_group_parts.clear()
+
+        def render_fold_details(fold_node: Node) -> str:
+            """把一个 fold 节点整体包装为 <details>(不仅渲染它的子内容)。"""
+            title = fold_node.title or ""
+            is_collapsed = should_collapse(
+                title=title,
+                collapse_prefixes=collapse_prefixes,
+                collapse_keywords=collapse_keywords,
+                collapse_all=collapse_all,
+            )
+            folded_class = "fold tool-fold" if is_collapsed else "fold normal-fold"
+            open_attr = "" if is_collapsed else " open"
+            display_title = enrich_fold_title(title)
+            inner = render_node(
+                fold_node,
+                collapse_prefixes=collapse_prefixes,
+                collapse_keywords=collapse_keywords,
+                collapse_all=collapse_all,
+                emit_think_plan_return_copy=False,
+            )
+            rendered = (
+                f'<details class="{folded_class}"{open_attr}>'
+                f"<summary>{html.escape(display_title)}</summary>"
+                f"{inner}"
+                "</details>"
+            )
+
+            return rendered
+
+        for entry in node.entries:
+            if isinstance(entry, str):
+                # 空白行不算“文本”,不打断连续工具调用收集。
+                if entry.strip() == "":
+                    if tool_group_parts:
+                        # 工具块之间的空白忽略
+                        continue
+                    text_buffer.append(entry)
+                    continue
+
+                # 真正的文本出现:结束当前工具调用收集,并输出文本
+                flush_tool_group()
+                text_buffer.append(entry)
+                continue
+
+            child = entry
+            child_title = child.title or ""
+            is_tool_call = child.is_fold and child_title.startswith("🔧 ")
+
+            if is_tool_call:
+                # 工具调用开始:先把前面的文本输出,再收集到工具组里
+                flush_text_buffer()
+                tool_group_parts.append(render_fold_details(child))
+
+                # 关键:`🔧 think_and_plan` 复制出来的 `📤 返回内容` 也算文本,
+                # 需要放在工具调用收集块之外,从而打断相邻工具调用的收集分组。
+                if child_title == THINK_AND_PLAN_TOOL_TITLE:
+                    return_nodes = [
+                        e
+                        for e in child.entries
+                        if isinstance(e, Node) and e.is_fold and (e.title or "") == RETURN_CONTENT_FOLD_TITLE
+                    ]
+                    if return_nodes:
+                        flattened_lines = flatten_entries_to_lines(return_nodes[0].entries)
+                        flush_tool_group()
+                        parts.append(render_text_block(flattened_lines))
+            else:
+                # 遇到非工具调用(或非工具 fold):结束当前工具组,再按原样输出
+                flush_tool_group()
+                flush_text_buffer()
+                parts.append(
+                    render_node(
+                        child,
+                        collapse_prefixes=collapse_prefixes,
+                        collapse_keywords=collapse_keywords,
+                        collapse_all=collapse_all,
+                    )
+                )
+
+        flush_tool_group()
+        flush_text_buffer()
+        return "".join(parts)
+
     parts: list[str] = []
     text_buffer: list[str] = []
 
@@ -204,6 +332,19 @@ def render_node(
                 f"{inner}"
                 "</details>"
             )
+            # 关键点:只把 `📤 返回内容` 复制到 `think_and_plan` 的 <details> 之外
+            # 但当 root 工具调用收集块要负责“details 外展示”时,需要关闭此复制。
+            if emit_think_plan_return_copy and title == THINK_AND_PLAN_TOOL_TITLE:
+                return_nodes = [
+                    e
+                    for e in child.entries
+                    if isinstance(e, Node) and e.is_fold and (e.title or "") == RETURN_CONTENT_FOLD_TITLE
+                ]
+                if return_nodes:
+                    # 一般只会有一个 `📤 返回内容`,取第一个
+                    return_node = return_nodes[0]
+                    flattened_lines = flatten_entries_to_lines(return_node.entries)
+                    parts.append(render_text_block(flattened_lines))
 
     flush_text_buffer()
 
@@ -315,6 +456,14 @@ def build_html(body: str, source_name: str) -> str:
       font-size: 13px;
       font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
     }}
+    .fold-duplicate {{
+      margin: 6px 0 12px 0;
+      padding-left: 10px;
+      border-left: 2px solid rgba(110, 168, 254, 0.6);
+    }}
+    .tool-call-group {{
+      margin-top: 10px;
+    }}
   </style>
 </head>
 <body>
@@ -335,6 +484,13 @@ def build_html(body: str, source_name: str) -> str:
     document.getElementById("collapse-tools").addEventListener("click", () => {{
       toolFolds.forEach((el) => (el.open = false));
     }});
+
+    // 每个“工具调用”大折叠块:打开外层时,里面的子工具调用保持收起(不自动展开)
+    Array.from(document.querySelectorAll("details.tool-call-group")).forEach((group) => {{
+      group.addEventListener("toggle", () => {{
+        group.querySelectorAll("details.tool-fold").forEach((el) => (el.open = false));
+      }});
+    }});
   </script>
 </body>
 </html>

+ 23 - 22
examples/piaoquan_needs/run.py → examples/piaoquan_demand/run.py

@@ -1,4 +1,4 @@
-"""piaoquan_needs 示例的最小可运行入口。"""
+"""piaoquan_demand 示例的最小可运行入口。"""
 
 import asyncio
 import copy
@@ -10,8 +10,8 @@ from pathlib import Path
 
 from dotenv import load_dotenv
 
-from examples.piaoquan_needs.config import LOG_LEVEL, ENABLED_TOOLS
-from examples.piaoquan_needs.topic_build_agent_context import TopicBuildAgentContext
+from examples.piaoquan_demand.config import LOG_LEVEL, ENABLED_TOOLS
+from examples.piaoquan_demand.topic_build_agent_context import TopicBuildAgentContext
 
 # Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
 os.environ.setdefault("no_proxy", "*")
@@ -36,27 +36,28 @@ from agent.utils import setup_logging
 from log_capture import build_log, log
 
 # 导入项目配置
-from examples.piaoquan_needs.config import DEBUG, LOG_FILE, LOG_LEVEL, RUN_CONFIG, TRACE_STORE_PATH
+from examples.piaoquan_demand.config import DEBUG, LOG_FILE, LOG_LEVEL, RUN_CONFIG, TRACE_STORE_PATH
 
 
 CUSTOM_TOOL_MODULES = {
-    # piaoquan_needs 示例:严格按工具名白名单加载对应模块
-    "think_and_plan": "examples.piaoquan_needs.agent_tools",
-    "get_category_tree": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_frequent_itemsets": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_itemset_detail": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_post_elements": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "search_elements": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_element_category_chain": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_category_detail": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "search_categories": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_category_elements": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_category_co_occurrences": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_element_co_occurrences": "examples.piaoquan_needs.topic_build_pattern_tools",
-    "get_weight_score_topn": "examples.piaoquan_needs.weight_score_query_tools",
-    "get_weight_score_by_name": "examples.piaoquan_needs.weight_score_query_tools",
-
-
+    # piaoquan_demand 示例:严格按工具名白名单加载对应模块
+    "think_and_plan": "examples.piaoquan_demand.agent_tools",
+    "get_category_tree": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_frequent_itemsets": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_itemset_detail": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_post_elements": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "search_elements": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_element_category_chain": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_category_detail": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "search_categories": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_category_elements": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_category_co_occurrences": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_element_co_occurrences": "examples.piaoquan_demand.topic_build_pattern_tools",
+    "get_weight_score_topn": "examples.piaoquan_demand.weight_score_query_tools",
+    "get_weight_score_by_name": "examples.piaoquan_demand.weight_score_query_tools",
+    "create_demand_item": "examples.piaoquan_demand.demand_build_agent_tools",
+    "create_demand_items": "examples.piaoquan_demand.demand_build_agent_tools",
+    "write_execution_summary": "examples.piaoquan_demand.demand_build_agent_tools",
 }
 
 
@@ -101,7 +102,7 @@ async def run_once() -> str:
     setup_logging(level=LOG_LEVEL, file=LOG_FILE)
     register_selected_tools(ENABLED_TOOLS)
 
-    prompt = SimplePrompt(base_dir / "needs_from_element.md")
+    prompt = SimplePrompt(base_dir / "demand.md")
 
     model = resolve_model(prompt)
 

+ 0 - 0
examples/piaoquan_needs/topic_build_agent_context.py → examples/piaoquan_demand/topic_build_agent_context.py


+ 42 - 17
examples/piaoquan_needs/topic_build_pattern_tools.py → examples/piaoquan_demand/topic_build_pattern_tools.py

@@ -12,7 +12,7 @@ Pattern 数据查询工具
   - get_itemset_detail: 单个项集详情
   - get_post_elements: 帖子结构化元素
   - search_elements: 按关键词搜索元素
-  - get_element_category_chain: 元素反查分类链
+  - get_element_category_chain: 元素名称列表批量反查分类链
   - get_category_detail: 分类节点详情
   - search_categories: 按关键词搜索分类
   - get_category_elements: 分类下的元素列表
@@ -23,7 +23,7 @@ import json
 from typing import Any
 
 from agent import tool
-from examples.piaoquan_needs.topic_build_agent_context import TopicBuildAgentContext
+from examples.piaoquan_demand.topic_build_agent_context import TopicBuildAgentContext
 from log_capture import log
 import pattern_service
 
@@ -50,7 +50,7 @@ def _log_tool_output(tool_name: str, result: str) -> str:
 # ============================================================================
 
 @tool(
-    "获取分类树结构快照。分类树是所有数据的骨架——帖子中的元素按'实质/形式/意图'三个维度归类到树形分类节点中。"
+    "获取二级品类分类树结构快照。分类树是所有数据的骨架——帖子中的元素按'实质/形式/意图'三个维度归类到树形分类节点中。"
     "\n\n返回紧凑文本格式,包含每个分类节点的名称、层级和元素数量。这是理解数据整体结构的起点。"
     "\n\n使用场景:"
     "\n- 启动时调用,了解数据涵盖哪些主题领域和维度"
@@ -250,35 +250,60 @@ def search_elements(keyword: str, element_type: str = None, limit: int = 50,
     }, ensure_ascii=False, indent=2)
     return _log_tool_output("search_elements", result)
 
-@tool("从元素名称反查其所属的完整分类链。一个元素可能归属于多个分类节点,此工具返回每个归属分类从根到叶的完整祖先路径。"
+@tool("从元素名称列表批量反查各自所属的完整分类链。每个元素可能归属于多个分类节点,每项返回每个归属分类从根到叶的完整祖先路径。"
     "\n\n使用场景:"
-    "\n- 知道一个元素名称,想了解它在分类树中的位置"
+    "\n- 知道多个元素名称,想了解它们在分类树中的位置"
     "\n- 从元素出发向上回溯分类层级,获取 category_id 用于 get_frequent_itemsets"
     "\n- 理解同一元素在不同维度下的分类归属差异")
-def get_element_category_chain(element_name: str, element_type: str = None) -> str:
-    """从元素名称反查其所属分类链。返回该元素出现在哪些分类下,以及每个分类的完整祖先路径(从根到叶)。
+def get_element_category_chain(element_names: list[str], element_type: str = None) -> str:
+    """从元素名称批量反查所属分类链。对每个名称返回其出现在哪些分类下,以及每个分类的完整祖先路径(从根到叶)。
 
     使用场景:
-    - 知道一个元素名称,想了解它在分类树中的位置
+    - 知道多个元素名称,想了解它们在分类树中的位置
     - 从元素出发,向上回溯分类层级,为泛化推理提供路径
 
     Args:
-        element_name: 元素名称(精确匹配)。
-        element_type: 按维度筛选:实质/形式/意图。不传则查所有维度。
+        element_names: 元素名称列表(精确匹配,顺序与返回 results 一一对应)。
+        element_type: 按维度筛选:实质/形式/意图。不传则查所有维度(对所有名称共用)
 
     Returns:
-        分类链列表的JSON字符串,每条含 category_id、category_path、ancestors 链(从根到叶)。
+        JSON 字符串。含 query_count 与 results,每项含 element_name、category_chains(含 category_id、category_path、ancestors 等)。
     """
     execution_id = TopicBuildAgentContext.get_execution_id()
-    params = {"execution_id": execution_id, "element_name": element_name,
+    params = {"execution_id": execution_id, "element_names": element_names,
               "element_type": element_type}
     _log_tool_input("get_element_category_chain", params)
 
-    data = pattern_service.get_element_category_chain(execution_id, element_name, element_type=element_type)
-    result = json.dumps({
-        "element_name": element_name,
-        "category_chains": data,
-    }, ensure_ascii=False, indent=2)
+    if not element_names:
+        return _log_tool_output("get_element_category_chain", "错误: element_names 不能为空列表")
+    if not isinstance(element_names, list):
+        return _log_tool_output(
+            "get_element_category_chain",
+            f"错误: element_names 必须为列表,当前类型: {type(element_names).__name__}",
+        )
+
+    stripped: list[str] = []
+    for i, n in enumerate(element_names):
+        if n is None or (isinstance(n, str) and not n.strip()):
+            return _log_tool_output(
+                "get_element_category_chain",
+                f"错误: element_names[{i}] 不能为空",
+            )
+        stripped.append(str(n).strip())
+
+    results = []
+    for name in stripped:
+        data = pattern_service.get_element_category_chain(
+            execution_id, name, element_type=element_type
+        )
+        results.append({"element_name": name, "category_chains": data})
+
+    out = {
+        "element_type": element_type,
+        "query_count": len(stripped),
+        "results": results,
+    }
+    result = json.dumps(out, ensure_ascii=False, indent=2)
     return _log_tool_output("get_element_category_chain", result)
 
 

+ 59 - 28
examples/piaoquan_needs/weight_score_query_tools.py → examples/piaoquan_demand/weight_score_query_tools.py

@@ -3,15 +3,15 @@
 """
 权重分查询工具
 
-从 examples/piaoquan_needs/data/{execution_id} 目录读取权重分 JSON,
-支持按元素/分类查询 TopN,以及按名称精确查询权重分。
+从 examples/piaoquan_demand/data/{execution_id} 目录读取权重分 JSON,
+支持按元素/分类查询 TopN,以及按名称列表批量查询权重分。
 """
 import json
 from pathlib import Path
 
 from agent import tool
-from examples.piaoquan_needs.topic_build_agent_context import TopicBuildAgentContext
-from examples.piaoquan_needs.topic_build_pattern_tools import _log_tool_input, _log_tool_output
+from examples.piaoquan_demand.topic_build_agent_context import TopicBuildAgentContext
+from examples.piaoquan_demand.topic_build_pattern_tools import _log_tool_input, _log_tool_output
 
 _VALID_LEVELS = {"元素", "分类"}
 _VALID_DIMENSIONS = {"实质", "形式", "意图"}
@@ -50,75 +50,98 @@ def _validate_params(level: str, dimension: str):
         raise ValueError(f"dimension 参数非法: {dimension},可选值: {sorted(_VALID_DIMENSIONS)}")
 
 
-@tool("查询元素或分类权重分 topN。参数:level(元素/分类)、dimension(实质/形式/意图)、top_n。")
-def get_weight_score_topn(level: str, dimension: str, top_n: int = 10) -> str:
-    """查询元素或分类权重分 topN
+@tool("查询元素或分类权重分排名区间。参数:level(元素/分类)、dimension(实质/形式/意图)、start(起始排名,含,从1开始)、end(结束排名,含,从1开始)。")
+def get_weight_score_topn(level: str, dimension: str, start: int = 1, end: int = 10) -> str:
+    """查询元素或分类权重分排名区间
 
     Args:
         level: 查询层级,元素 或 分类。
         dimension: 查询维度,实质 / 形式 / 意图。
-        top_n: 返回数量,默认 10。
+        start: 起始排名(包含),从 1 开始。
+        end: 结束排名(包含),从 1 开始。
 
     Returns:
-        JSON 字符串,包含查询参数、总量和 topN 数据。
+        JSON 字符串,包含查询参数、总量和区间数据。
     """
     execution_id = TopicBuildAgentContext.get_execution_id()
     params = {
         "execution_id": execution_id,
         "level": level,
         "dimension": dimension,
-        "top_n": top_n,
+        "start": start,
+        "end": end,
     }
     _log_tool_input("get_weight_score_topn", params)
 
     try:
         _validate_params(level=level, dimension=dimension)
-        if top_n <= 0:
-            return _log_tool_output("get_weight_score_topn", f"错误: top_n 必须大于 0,当前值: {top_n}")
+        if start < 1 or end < 1:
+            return _log_tool_output(
+                "get_weight_score_topn",
+                f"错误: start/end 必须为大于等于 1 的整数,当前值: start={start}, end={end}",
+            )
+        if start > end:
+            return _log_tool_output(
+                "get_weight_score_topn",
+                f"错误: start 不能大于 end,当前值: start={start}, end={end}",
+            )
 
         data = _load_weight_data(level=level, dimension=dimension)
         sorted_data = sorted(data, key=lambda x: float(x.get("score", 0)), reverse=True)
-        top_items = sorted_data[:top_n]
+        # 用户输入为 1-based 且 end 为包含边界,需转换为 Python 切片
+        ranged_items = sorted_data[start - 1 : end]
 
         result = {
             "level": level,
             "dimension": dimension,
-            "top_n": top_n,
+            "start": start,
+            "end": end,
             "total_count": len(data),
-            "items": top_items,
+            "matched_count": len(ranged_items),
+            "items": ranged_items,
         }
         return _log_tool_output("get_weight_score_topn", json.dumps(result, ensure_ascii=False, indent=2))
     except Exception as e:
         return _log_tool_output("get_weight_score_topn", f"查询失败: {e}")
 
 
-@tool("查询指定名称的权重分。参数:level(元素/分类)、dimension(实质/形式/意图)、name。")
-def get_weight_score_by_name(level: str, dimension: str, name: str) -> str:
-    """查询指定名称的权重分。
+@tool("批量查询指定名称的权重分。参数:level(元素/分类)、dimension(实质/形式/意图)、names(名称列表)。")
+def get_weight_score_by_name(level: str, dimension: str, names: list[str]) -> str:
+    """批量查询指定名称的权重分。
 
     Args:
         level: 查询层级,元素 或 分类。
         dimension: 查询维度,实质 / 形式 / 意图。
-        name: 要查询的名称(元素名或分类名)。
+        names: 要查询的名称列表(元素名或分类名),顺序与返回 results 一一对应
 
     Returns:
-        JSON 字符串。若命中,返回匹配记录;否则返回未找到提示
+        JSON 字符串,含每个名称的 matched_count 与 items
     """
     execution_id = TopicBuildAgentContext.get_execution_id()
     params = {
         "execution_id": execution_id,
         "level": level,
         "dimension": dimension,
-        "name": name,
+        "names": names,
     }
     _log_tool_input("get_weight_score_by_name", params)
 
     try:
         _validate_params(level=level, dimension=dimension)
-        if not name or not name.strip():
-            return _log_tool_output("get_weight_score_by_name", "错误: name 不能为空")
+        if not names:
+            return _log_tool_output("get_weight_score_by_name", "错误: names 不能为空列表")
+        if not isinstance(names, list):
+            return _log_tool_output("get_weight_score_by_name", f"错误: names 必须为列表,当前类型: {type(names).__name__}")
+
+        stripped: list[str] = []
+        for i, n in enumerate(names):
+            if n is None or (isinstance(n, str) and not n.strip()):
+                return _log_tool_output(
+                    "get_weight_score_by_name",
+                    f"错误: names[{i}] 不能为空",
+                )
+            stripped.append(str(n).strip())
 
-        target_name = name.strip()
         data = _load_weight_data(level=level, dimension=dimension)
 
         if level == "元素":
@@ -126,14 +149,22 @@ def get_weight_score_by_name(level: str, dimension: str, name: str) -> str:
         else:
             key = "category"
 
-        matched = [item for item in data if str(item.get(key, "")).strip() == target_name]
+        results = []
+        for target_name in stripped:
+            matched = [item for item in data if str(item.get(key, "")).strip() == target_name]
+            results.append(
+                {
+                    "name": target_name,
+                    "matched_count": len(matched),
+                    "items": matched,
+                }
+            )
 
         result = {
             "level": level,
             "dimension": dimension,
-            "name": target_name,
-            "matched_count": len(matched),
-            "items": matched,
+            "query_count": len(stripped),
+            "results": results,
         }
         return _log_tool_output("get_weight_score_by_name", json.dumps(result, ensure_ascii=False, indent=2))
     except Exception as e:

+ 0 - 24
examples/piaoquan_needs/needs.md

@@ -1,24 +0,0 @@
----
-model: anthropic/claude-sonnet-4w.5
-temperature: 0.5
-max_iterations: 200
----
-
-$system$
-该文件已改为 Prompt 入口说明,不再承载具体执行流程。
-
-$user$
-请按任务目标选择以下 prompt:
-
-1. `examples/piaoquan_needs/needs_from_category.md`
-   - 从分类节点出发
-   - 适合先做抽象方向筛选,再做树关系与共现跳转
-
-2. `examples/piaoquan_needs/needs_from_element.md`
-   - 从叶子元素出发
-   - 适合先做具象素材筛选,再向上回溯并做共现扩展
-
-统一要求(两者都遵守):
-- 结果区只输出树上节点与节点组合
-- 其他解释统一放在 `other_fields`
-- 意图维度不直接参与需求生成,仅在共现关系中辅助使用