Forráskód Böngészése

v3:新的评估方案及可视化方案

刘立冬 2 hónapja
szülő
commit
0b598dfafb
42 módosított fájl, 7236 hozzáadás és 94659 törlés
  1. 61 41
      knowledge_search_traverse.py
  2. 18 9
      post_evaluator_v2.py
  3. 1317 0
      post_evaluator_v3.py
  4. 484 0
      script/search/test_xiaohongshu_search_api.py
  5. 48 21
      script/search/xiaohongshu_search.py
  6. 0 1061
      sug_v6_1_2_10.py
  7. 0 1039
      sug_v6_1_2_11.py
  8. 0 1103
      sug_v6_1_2_114.py
  9. 0 1459
      sug_v6_1_2_115.py
  10. 0 1645
      sug_v6_1_2_116.py
  11. 0 1464
      sug_v6_1_2_117.py
  12. 0 1508
      sug_v6_1_2_118.py
  13. 0 1690
      sug_v6_1_2_119.py
  14. 0 1690
      sug_v6_1_2_120.py
  15. 0 2367
      sug_v6_1_2_121.py
  16. 0 2756
      sug_v6_1_2_122.py
  17. 0 2439
      sug_v6_1_2_123.py
  18. 0 2756
      sug_v6_1_2_124.py
  19. 0 3899
      sug_v6_1_2_125.py
  20. 0 3894
      sug_v6_1_2_126.py
  21. 0 3941
      sug_v6_1_2_127.py
  22. 0 851
      sug_v6_1_2_8.py
  23. 0 990
      sug_v6_1_2_8_流程分析.md
  24. 0 1027
      sug_v6_1_2_9.py
  25. 3 2
      test_evaluation_v2.py
  26. 298 0
      test_evaluation_v3.py
  27. 0 22463
      visualization.html
  28. 0 887
      visualization/knowledge_search_traverse/convert_v8_to_graph_v2.js
  29. 24 3
      visualization/knowledge_search_traverse/convert_v8_to_graph_v3.js
  30. 4763 24414
      visualization/knowledge_search_traverse/debug_component.jsx
  31. 220 112
      visualization/knowledge_search_traverse/index.js
  32. 0 7
      visualization/knowledge_search_traverse/test_component.jsx
  33. 0 107
      visualization/sug_v6_1_2_6/README.md
  34. 0 1966
      visualization/sug_v6_1_2_6/index.js
  35. 0 25
      visualization/sug_v6_1_2_6/package.json
  36. 0 200
      visualization/sug_v6_1_2_8/README.md
  37. 0 321
      visualization/sug_v6_1_2_8/convert_v8_to_graph.js
  38. 0 887
      visualization/sug_v6_1_2_8/convert_v8_to_graph_v2.js
  39. 0 939
      visualization/sug_v6_1_2_8/convert_v8_to_graph_v3.js
  40. 0 2471
      visualization/sug_v6_1_2_8/index.js
  41. 0 25
      visualization/sug_v6_1_2_8/package.json
  42. 0 2180
      visualize_steps_v6_1_2_3.py

+ 61 - 41
knowledge_search_traverse.py

@@ -18,7 +18,7 @@ REQUIRED_SCORE_GAIN = 0.02
 from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
 from script.search.xiaohongshu_search import XiaohongshuSearch
 # from multimodal_extractor import extract_post_images  # 内容提取流程已断开
-from post_evaluator_v2 import evaluate_post_v2, apply_evaluation_v2_to_post
+from post_evaluator_v3 import evaluate_post_v3, apply_evaluation_v3_to_post
 
 
 # ============================================================================
@@ -146,23 +146,24 @@ class Post(BaseModel):
     note_id: str = ""
     note_url: str = ""
 
-    # 评估字段(顶层 - 快速访问)
-    is_knowledge: bool | None = None  # 是否是知识内容
-    knowledge_reason: str = ""  # 知识判定原因(简短)
-    knowledge_score: float | None = None  # 知识评分(0-100)
-    knowledge_level: int | None = None  # 知识星级(1-5)
+    # V3评估字段(顶层 - 快速访问)
+    is_knowledge: bool | None = None  # Prompt1: 是否是知识内容
+    is_content_knowledge: bool | None = None  # Prompt2: 是否是内容知识
+    knowledge_score: float | None = None  # Prompt2: 知识评分(0-100)
 
-    relevance_score: float | None = None  # 相关性综合得分(0-100)
-    relevance_level: str = ""  # 相关性分级:"高度相关" | "中度相关" | "低度相关"
-    relevance_reason: str = ""  # 相关性评分原因(简短)
-    relevance_conclusion: str = ""  # 匹配结论: "高度匹配"/"基本匹配"/"部分匹配"/"不匹配"
+    purpose_score: int | None = None  # Prompt3: 目的性得分(0-100)
+    category_score: int | None = None  # Prompt4: 品类得分(0-100)
+    final_score: float | None = None  # 综合得分: purpose*0.7 + category*0.3 (保留2位小数)
+    match_level: str = ""  # 匹配等级: "高度匹配"/"基本匹配"/"部分匹配"/"弱匹配"/"不匹配"
 
     evaluation_time: str = ""  # 评估时间戳
-    evaluator_version: str = "v2.0"  # 评估器版本
+    evaluator_version: str = "v3.0"  # 评估器版本
 
-    # 评估字段(嵌套 - 详细信息)
-    knowledge_evaluation: dict | None = None  # 知识评估详情
-    relevance_evaluation: dict | None = None  # 相关性评估详情
+    # V3评估字段(嵌套 - 详细信息)
+    knowledge_evaluation: dict | None = None  # Prompt1: 知识判断详情
+    content_knowledge_evaluation: dict | None = None  # Prompt2: 内容知识评估详情
+    purpose_evaluation: dict | None = None  # Prompt3: 目的性匹配详情
+    category_evaluation: dict | None = None  # Prompt4: 品类匹配详情
 
 
 class Search(Sug):
@@ -2707,7 +2708,8 @@ async def run_round(
     context: RunContext,
     xiaohongshu_api: XiaohongshuSearchRecommendations,
     xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
+    sug_threshold: float = 0.7,
+    enable_evaluation: bool = False
 ) -> tuple[list[Q], list[Seed], list[Search]]:
     """
     运行一轮
@@ -2877,15 +2879,18 @@ async def run_round(
         search_list = await asyncio.gather(*search_tasks)
 
         # 评估搜索结果中的帖子
-        print(f"\n[评估] 评估搜索结果中的帖子...")
-        for search in search_list:
-            if search.post_list:
-                print(f"  评估来自 '{search.text}' 的 {len(search.post_list)} 个帖子")
-                # 对每个帖子进行评估 (V2)
-                for post in search.post_list:
-                    knowledge_eval, relevance_eval = await evaluate_post_v2(post, o, semaphore=None)
-                    if knowledge_eval and relevance_eval:
-                        apply_evaluation_v2_to_post(post, knowledge_eval, relevance_eval)
+        if enable_evaluation:
+            print(f"\n[评估] 评估搜索结果中的帖子...")
+            for search in search_list:
+                if search.post_list:
+                    print(f"  评估来自 '{search.text}' 的 {len(search.post_list)} 个帖子")
+                    # 对每个帖子进行评估 (V3)
+                    for post in search.post_list:
+                        knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = await evaluate_post_v3(post, o, semaphore=None)
+                        if knowledge_eval:
+                            apply_evaluation_v3_to_post(post, knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
+        else:
+            print(f"\n[评估] 实时评估已关闭 (使用 --enable-evaluation 启用)")
     else:
         print(f"  没有高分建议词,search_list为空")
 
@@ -3173,7 +3178,8 @@ async def run_round(
 async def iterative_loop(
     context: RunContext,
     max_rounds: int = 2,
-    sug_threshold: float = 0.7
+    sug_threshold: float = 0.7,
+    enable_evaluation: bool = False
 ):
     """主迭代循环"""
 
@@ -3215,7 +3221,8 @@ async def iterative_loop(
             context=context,
             xiaohongshu_api=xiaohongshu_api,
             xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
+            sug_threshold=sug_threshold,
+            enable_evaluation=enable_evaluation
         )
 
         all_search_list.extend(search_list)
@@ -3379,7 +3386,8 @@ async def run_round_v2(
     context: RunContext,
     xiaohongshu_api: XiaohongshuSearchRecommendations,
     xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
+    sug_threshold: float = 0.7,
+    enable_evaluation: bool = False
 ) -> tuple[list[Q], list[Search], dict]:
     """
     v121 Round N 执行
@@ -3529,15 +3537,18 @@ async def run_round_v2(
             # extraction_results.update(extractions)  # 内容提取流程已断开
 
         # 评估搜索结果中的帖子
-        print(f"\n[评估] 评估搜索结果中的帖子...")
-        for search in search_list:
-            if search.post_list:
-                print(f"  评估来自 '{search.text}' 的 {len(search.post_list)} 个帖子")
-                # 对每个帖子进行评估 (V2)
-                for post in search.post_list:
-                    knowledge_eval, relevance_eval = await evaluate_post_v2(post, o, semaphore=None)
-                    if knowledge_eval and relevance_eval:
-                        apply_evaluation_v2_to_post(post, knowledge_eval, relevance_eval)
+        if enable_evaluation:
+            print(f"\n[评估] 评估搜索结果中的帖子...")
+            for search in search_list:
+                if search.post_list:
+                    print(f"  评估来自 '{search.text}' 的 {len(search.post_list)} 个帖子")
+                    # 对每个帖子进行评估 (V3)
+                    for post in search.post_list:
+                        knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = await evaluate_post_v3(post, o, semaphore=None)
+                        if knowledge_eval:
+                            apply_evaluation_v3_to_post(post, knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
+        else:
+            print(f"\n[评估] 实时评估已关闭 (使用 --enable-evaluation 启用)")
 
     # 步骤4: 生成N域组合
     print(f"\n[步骤4] 生成{round_num}域组合...")
@@ -3771,7 +3782,8 @@ async def run_round_v2(
 async def iterative_loop_v2(
     context: RunContext,
     max_rounds: int = 4,
-    sug_threshold: float = 0.7
+    sug_threshold: float = 0.7,
+    enable_evaluation: bool = False
 ):
     """v121 主迭代循环"""
 
@@ -3810,7 +3822,8 @@ async def iterative_loop_v2(
             context=context,
             xiaohongshu_api=xiaohongshu_api,
             xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
+            sug_threshold=sug_threshold,
+            enable_evaluation=enable_evaluation
         )
 
         all_search_list.extend(search_list)
@@ -3838,7 +3851,7 @@ async def iterative_loop_v2(
 # 主函数
 # ============================================================================
 
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
+async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False, enable_evaluation: bool = False):
     """主函数"""
     current_time, log_url = set_trace()
 
@@ -3889,7 +3902,8 @@ async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7,
         all_search_list = await iterative_loop_v2(  # 不再接收提取结果
             run_context,
             max_rounds=max_rounds,
-            sug_threshold=sug_threshold
+            sug_threshold=sug_threshold,
+            enable_evaluation=enable_evaluation
         )
 
         # 格式化输出
@@ -4002,6 +4016,12 @@ if __name__ == "__main__":
         default=True,
         help="运行完成后自动生成可视化HTML"
     )
+    parser.add_argument(
+        "--enable-evaluation",
+        action="store_true",
+        default=False,
+        help="是否启用实时评估功能,默认: 关闭"
+    )
     args = parser.parse_args()
 
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))
+    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize, enable_evaluation=args.enable_evaluation))

+ 18 - 9
post_evaluator_v2.py

@@ -464,10 +464,10 @@ async def evaluate_post_v2(
     semaphore: Optional[asyncio.Semaphore] = None
 ) -> tuple[Optional[KnowledgeEvaluation], Optional[RelevanceEvaluation]]:
     """
-    并发评估帖子(知识 + 相关性)
+    串行评估帖子(先知识,分数>40再评估相关性)
 
     Returns:
-        (KnowledgeEvaluation, RelevanceEvaluation) 或 (None, None)
+        (KnowledgeEvaluation, RelevanceEvaluation) 或 (Knowledge, None) 或 (None, None)
     """
     if post.type == "video":
         print(f"      ⊗ 跳过视频帖子: {post.note_id}")
@@ -475,16 +475,25 @@ async def evaluate_post_v2(
 
     print(f"      🔍 开始评估帖子: {post.note_id}")
 
-    # 并发调用两个评估
-    knowledge_task = evaluate_knowledge_v2(post, semaphore)
-    relevance_task = evaluate_relevance_v2(post, original_query, semaphore)
+    # 第一步:先评估知识
+    knowledge_eval = await evaluate_knowledge_v2(post, semaphore)
 
-    knowledge_eval, relevance_eval = await asyncio.gather(knowledge_task, relevance_task)
+    if not knowledge_eval:
+        print(f"      ⚠️  知识评估失败: {post.note_id}")
+        return None, None
+
+    # 第二步:只有知识分数>40才评估相关性
+    relevance_eval = None
+    if knowledge_eval.weighted_score > 40:
+        print(f"      ✅ 知识:{knowledge_eval.weighted_score:.1f}分({knowledge_eval.level}⭐) - 继续评估相关性")
+        relevance_eval = await evaluate_relevance_v2(post, original_query, semaphore)
 
-    if knowledge_eval and relevance_eval:
-        print(f"      ✅ 评估完成: {post.note_id} | 知识:{knowledge_eval.weighted_score:.1f}分 {knowledge_eval.level}星 | 相关性:{relevance_eval.total_score:.1f}分 {relevance_eval.conclusion}")
+        if relevance_eval:
+            print(f"      ✅ 评估完成 | 相关性:{relevance_eval.total_score:.1f}分({relevance_eval.conclusion})")
+        else:
+            print(f"      ⚠️  相关性评估失败")
     else:
-        print(f"      ⚠️  部分评估失败: {post.note_id}")
+        print(f"      ⊗ 知识:{knowledge_eval.weighted_score:.1f}分({knowledge_eval.level}⭐) - 分数≤40,跳过相关性评估")
 
     return knowledge_eval, relevance_eval
 

+ 1317 - 0
post_evaluator_v3.py

@@ -0,0 +1,1317 @@
+"""
+帖子评估模块 V3 - 4步串行+并行评估系统
+
+改进:
+1. Prompt1: 判断是知识 (is_knowledge)
+2. Prompt2: 判断是否是内容知识 (is_content_knowledge)
+3. Prompt3 & Prompt4: 并行执行 - 目的性(70%) + 品类(30%)
+4. 代码计算综合得分: final_score = purpose × 0.7 + category × 0.3
+5. 完全替代V2评估结果
+"""
+
+import asyncio
+import json
+import os
+from datetime import datetime
+from typing import Optional
+from pydantic import BaseModel, Field
+import requests
+
+MODEL_NAME = "google/gemini-2.5-flash"
+MAX_IMAGES_PER_POST = 10
+MAX_CONCURRENT_EVALUATIONS = 5
+API_TIMEOUT = 120
+
+# ============================================================================
+# 数据模型
+# ============================================================================
+
+class KnowledgeEvaluation(BaseModel):
+    """Prompt1: 判断是知识 - 评估结果"""
+    is_knowledge: bool = Field(..., description="是否是知识内容")
+    quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
+    title_layer: dict = Field(default_factory=dict, description="标题层判断")
+    image_layer: dict = Field(default_factory=dict, description="图片层判断(核心)")
+    text_layer: dict = Field(default_factory=dict, description="正文层判断(辅助)")
+    judgment_logic: str = Field(..., description="综合判定逻辑")
+    core_evidence: list[str] = Field(default_factory=list, description="核心证据")
+    issues: list[str] = Field(default_factory=list, description="不足或疑虑")
+    conclusion: str = Field(..., description="结论陈述")
+
+
+class ContentKnowledgeEvaluation(BaseModel):
+    """Prompt2: 判断是否是内容知识 - 评估结果"""
+    is_content_knowledge: bool = Field(..., description="是否属于内容知识")
+    final_score: int = Field(..., description="最终得分(0-100)")
+    level: str = Field(..., description="判定等级")
+    quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
+    dimension_scores: dict = Field(default_factory=dict, description="分层评分详情")
+    core_evidence: list[str] = Field(default_factory=list, description="核心证据")
+    issues: list[str] = Field(default_factory=list, description="不足之处")
+    summary: str = Field(..., description="总结陈述")
+
+
+class PurposeEvaluation(BaseModel):
+    """Prompt3: 目的性匹配 - 评估结果"""
+    purpose_score: int = Field(..., description="目的动机得分(0-100整数)")
+    core_motivation: str = Field(..., description="原始需求核心动机")
+    image_value: str = Field(..., description="图片提供的价值")
+    title_intention: str = Field(..., description="标题体现的意图")
+    text_content: str = Field(..., description="正文补充的内容")
+    match_level: str = Field(..., description="匹配度等级")
+    core_basis: str = Field(..., description="核心依据")
+
+
+class CategoryEvaluation(BaseModel):
+    """Prompt4: 品类匹配 - 评估结果"""
+    category_score: int = Field(..., description="品类匹配得分(0-100整数)")
+    original_category: dict = Field(default_factory=dict, description="原始需求品类")
+    actual_category: dict = Field(default_factory=dict, description="帖子实际品类")
+    match_level: str = Field(..., description="匹配度等级")
+    subject_match: str = Field(..., description="主体匹配情况")
+    qualifier_match: str = Field(..., description="限定词匹配情况")
+    core_basis: str = Field(..., description="核心依据")
+
+
+# ============================================================================
+# Prompt 定义
+# ============================================================================
+
+PROMPT1_IS_KNOWLEDGE = """# 知识判定系统 v1.0
+
+## 角色定义
+你是一个多模态内容评估专家,专门判断社交媒体帖子是否提供了"知识"。
+
+## 知识定义
+**知识**是指经过验证的、具有可靠性和真实性的信息,能够被理解、学习、传播和应用。
+
+### 知识类型
+- ✅ **事实性知识**: 客观事实、数据、现象描述
+- ✅ **原理性知识**: 规律、原理、理论、因果关系
+- ✅ **方法性知识**: 技能、流程、步骤、操作方法
+- ✅ **经验性知识**: 总结提炼的经验、教训、最佳实践
+- ✅ **概念性知识**: 定义、分类、框架、体系
+- ✅ **应用性知识**: 解决方案、工具使用、实践指南
+
+### 非知识类型(严格排除)
+- ❌ **纯观点/立场**: 未经验证的个人观点、偏好表达
+- ❌ **情感表达**: 纯粹的情绪抒发、心情分享
+- ❌ **单纯展示**: 作品展示、生活记录、打卡(无知识提炼)
+- ❌ **娱乐内容**: 段子、搞笑、八卦(无信息价值)
+- ❌ **纯营销/广告**: 单纯的产品推销
+- ❌ **虚假/未验证信息**: 谣言、伪科学、未经证实的说法
+
+---
+
+## 输入信息
+- **标题**: {title}
+- **正文**: {body_text}
+- **图片**: {num_images}张
+
+---
+
+## 判断流程
+
+### 第一步: 快速排除(任一为"是"则判定为非知识)
+
+1. 是否为纯情感表达/生活记录/打卡?
+2. 是否为单纯的作品/产品展示(无知识提炼)?
+3. 是否为娱乐搞笑/八卦/纯营销内容?
+4. 是否包含虚假信息或伪科学?
+5. 是否完全没有新信息(纯重复常识)?
+
+**排除判定**: □ 是(判定为非知识) / □ 否(继续评估)
+
+---
+
+### 第二步: 分层知识判断
+
+## 🏷️ 标题层
+
+**判断:标题是否指向知识?**
+- ✅ 明确传达知识主题(如何/为什么/什么是/XX方法/XX原理)
+- ⚠️ 描述性标题,但暗示有知识内容
+- ❌ 展示型(我的XX/今天XX)或情感型标题
+
+**结果**: □ 有知识指向 / □ 无知识指向
+
+---
+
+## 🖼️ 图片层(信息主要承载层)
+
+**判断1: 图片知识呈现方式**
+- ✅ 包含信息图表(数据、流程图、对比图、结构图)
+- ✅ 有知识性标注(解释、说明、步骤、原理)
+- ✅ 多图形成知识体系(步骤序列、案例对比)
+- ❌ 纯作品展示、美图、氛围图
+
+**判断2: 图片教育价值**
+- ✅ 图片能教会他人方法、技能或原理
+- ✅ 提供可学习的步骤或解决方案
+- ❌ 图片无教学意义
+
+**判断3: 图片结构化程度**
+- ✅ 有清晰的逻辑组织(序号、分层、框架)
+- ✅ 有步骤、要点的结构化呈现
+- ❌ 碎片化、无逻辑的展示
+
+**判断4: 图片实用性**
+- ✅ 提供可直接应用的方法或工具
+- ✅ 能帮助解决实际问题
+- ❌ 纯观赏性,无实际应用价值
+
+**判断5: 图片信息密度**
+- ✅ 包含≥3个独立知识点
+- ⚠️ 包含1-2个知识点
+- ❌ 无明确知识点
+
+**图片层综合评估**: □ 图片传递了知识 / □ 图片无知识价值
+
+---
+
+## 📝 正文层(辅助判断)
+
+**判断1: 信息增量**
+- ✅ 提供了明确的新信息、新认知或新方法
+- ❌ 无新信息,只是个人记录或情感表达
+
+**判断2: 可验证性**
+- ✅ 基于事实、数据、可验证的经验
+- ❌ 纯主观观点或感受,无依据
+
+**判断3: 知识类型归属**
+- ✅ 能归入至少一种知识类型(事实/原理/方法/经验/概念/应用)
+- ❌ 无法归类为任何知识类型
+
+**正文层综合评估**: □ 正文提供了知识支撑 / □ 正文无知识价值
+
+---
+
+### 第三步: 综合判定
+
+#### 判定规则
+
+**直接判定为"非知识"(任一成立)**:
+- 未通过快速排除
+- 图片层 = 无知识价值 且 正文层 = 无知识支撑
+- 正文判断3(知识类型)= 无法归类
+
+**判定为"是知识"(需同时满足)**:
+1. 通过快速排除
+2. 图片层 = 传递了知识 或 正文层 = 提供了知识支撑
+3. 正文判断1(信息增量)= 有新信息
+4. 正文判断3(知识类型)= 可归类
+
+**特别说明**:
+- 社交媒体帖子以图片为主要信息载体,图片层权重最高
+- 标题为辅助判断,正文为补充验证
+
+---
+
+## 输出格式
+
+请严格按照以下JSON格式输出:
+
+{{
+  "is_knowledge": true/false,
+  "quick_exclude": {{
+    "result": "通过/未通过",
+    "reason": "如未通过,说明原因"
+  }},
+  "title_layer": {{
+    "has_knowledge_direction": true/false,
+    "reason": "一句话说明"
+  }},
+  "image_layer": {{
+    "knowledge_presentation": {{"match": true/false, "reason": "简述"}},
+    "educational_value": {{"has_value": true/false, "reason": "简述"}},
+    "structure_level": {{"structured": true/false, "reason": "简述"}},
+    "practicality": {{"practical": true/false, "reason": "简述"}},
+    "information_density": {{"level": "高/中/低", "reason": "简述"}},
+    "overall": "传递知识/无知识价值"
+  }},
+  "text_layer": {{
+    "information_gain": {{"has_gain": true/false, "reason": "简述"}},
+    "verifiability": {{"verifiable": true/false, "reason": "简述"}},
+    "knowledge_type": {{"type": "具体类型/无法归类", "reason": "简述"}},
+    "overall": "有知识支撑/无知识价值"
+  }},
+  "judgment_logic": "说明是否满足判定规则,关键依据是什么",
+  "core_evidence": [
+    "最强支持证据1",
+    "最强支持证据2"
+  ],
+  "issues": [
+    "不足或疑虑(如无则为空数组)"
+  ],
+  "conclusion": "用1-2句话说明判定结果和核心理由"
+}}
+
+## 判断原则
+1. **图片优先**: 社交媒体以图片为主要信息载体,图片层是核心判断依据,标题和正文信息辅助
+2. **严格性**: 宁可误判为"非知识",也不放过无价值内容
+3. **证据性**: 基于明确的视觉和文本证据判断
+4. **价值导向**: 优先判断内容对读者是否有实际学习价值
+"""
+
+PROMPT2_IS_CONTENT_KNOWLEDGE = """# 内容知识判定系统 v2.0
+
+## 角色定义
+你是一个多模态内容评估专家,专门判断社交媒体帖子是否属于"内容知识"类别。
+
+## 前置条件
+该帖子已通过知识判定,确认提供了知识。现在需要进一步判断是否属于"内容知识"。
+
+---
+
+## 内容知识定义
+
+**内容知识**是指与创作/制作/设计相关的、具有实操性和可迁移性的知识,帮助创作者提升创作能力。
+
+### 内容知识的范畴
+- ✅ **创作原理**: 设计原理、创作逻辑、美学规律、构图法则(通用的,普适的)
+- ✅ **制作方法**: 操作流程、技术步骤、工具使用方法
+- ✅ **创意技巧**: 灵感方法、创意思路、表现手法、风格技法
+- ✅ **体系框架**: 完整的创作体系、方法论、思维框架
+- ✅ **案例提炼**: 从多个案例中总结的通用创作规律
+
+### 非内容知识(严格排除)
+- ❌ **单案例展示**: 仅展示单个作品,无方法论提炼
+- ❌ **作品集合**: 纯作品展示集合,无创作方法讲解
+- ❌ **单点元素**: 只展示配色/字体/素材,无使用方法
+- ❌ **单次操作**: 只讲某个项目的特定操作,无通用性
+- ❌ **非创作领域**: 健康、财经、生活、科普等非创作制作领域的知识
+
+---
+
+## 输入信息
+- **标题**: {title}
+- **正文**: {body_text}
+- **图片数量**: {num_images}张
+
+---
+
+## 判断流程
+
+### 第一步: 快速排除判断(任一为"是"则判定为非内容知识)
+
+1. 标题是否为纯展示型?("我的XX作品"、"今天做了XX"、"作品分享")
+2. 图片是否全为作品展示,无任何方法/原理/步骤说明?
+3. 是否只讲单个项目的特定操作,完全无通用性?
+4. 是否为纯元素展示,无创作方法?(仅展示配色、字体、素材)
+
+**排除判定**: □ 是(判定为非内容知识) / □ 否(继续评估)
+
+---
+
+### 第二步: 分层打分评估(满分100分)
+
+## 🖼️ 图片层评估(权重70%,满分70分)
+
+> **说明**: 社交媒体以图片为主要信息载体,图片层是核心判断依据
+
+#### 维度1: 创作方法呈现(20分)
+**评分依据**: 图片是否清晰展示了具体的创作/制作方法、技巧、技法
+
+- **20分**: 图片详细展示≥3个具体可操作的创作方法/技巧,有明确的操作指引
+- **15分**: 图片展示2个创作方法,方法较为具体
+- **10分**: 图片展示1个创作方法,但不够详细
+- **5分**: 图片暗示有方法,但未明确展示
+- **0分**: 图片无任何方法展示,纯作品呈现
+
+**得分**: __/20
+
+---
+
+#### 维度2: 知识体系化程度(15分)
+**评分依据**: 多图是否形成完整的知识体系或逻辑链条
+
+- **15分**: 多图形成完整体系(步骤1→2→3,或原理→方法→案例),逻辑清晰
+- **12分**: 多图有知识关联性,形成部分体系
+- **8分**: 多图展示多个知识点,但关联性弱
+- **4分**: 多图仅为同类案例堆砌,无体系
+- **0分**: 单图或多图无逻辑关联
+
+**得分**: __/15
+
+---
+
+#### 维度3: 教学性标注与说明(15分)
+**评分依据**: 图片是否包含教学性的视觉元素(标注、序号、箭头、文字说明)
+
+- **15分**: 大量教学标注(序号、箭头、高亮、文字说明、对比标记等),清晰易懂
+- **12分**: 有明显的教学标注,但不够完善
+- **8分**: 有少量标注或说明
+- **4分**: 仅有简单文字,无视觉教学元素
+- **0分**: 无任何教学标注,纯视觉展示
+
+**得分**: __/15
+
+---
+
+#### 维度4: 方法可复用性(10分)
+**评分依据**: 图片展示的方法是否可迁移到其他创作场景/项目
+
+- **10分**: 明确展示通用方法,可应用于多种场景(配公式/模板/框架)
+- **8分**: 方法有一定通用性,可迁移到类似场景
+- **5分**: 方法通用性一般,需要改造才能应用
+- **2分**: 方法仅适用于特定项目
+- **0分**: 无可复用方法
+
+**得分**: __/10
+
+---
+
+#### 维度5: 原理与案例结合(10分)
+**评分依据**: 图片是否将创作原理与实际案例有效结合
+
+- **10分**: 原理+多案例验证,清晰展示原理如何应用
+- **8分**: 原理+案例,有一定结合
+- **5分**: 有原理或有案例,但结合不够
+- **2分**: 仅有案例,无原理提炼
+- **0分**: 纯案例展示或纯理论
+
+**得分**: __/10
+
+---
+
+**🖼️ 图片层总分**: __/70
+
+---
+
+## 📝 正文层评估(权重20%,满分20分)
+
+> **说明**: 正文作为辅助判断,补充图片未完整呈现的知识信息
+
+#### 维度6: 方法/步骤描述(10分)
+**评分依据**: 正文是否描述了具体的创作方法或操作步骤
+
+- **10分**: 有完整的步骤描述(≥3步)或详细的方法说明
+- **7分**: 有步骤或方法描述,但不够系统
+- **4分**: 有零散的方法提及
+- **0分**: 无方法/步骤,纯叙事或展示性文字
+
+**得分**: __/10
+
+---
+
+#### 维度7: 知识总结与提炼(10分)
+**评分依据**: 正文是否对创作经验/规律进行总结提炼
+
+- **10分**: 有明确的知识总结、归纳、框架化输出
+- **7分**: 有一定的经验总结或要点提炼
+- **4分**: 有零散的心得,但未成体系
+- **0分**: 无任何知识提炼
+
+**得分**: __/10
+
+---
+
+**📝 正文层总分**: __/20
+
+---
+
+## 🏷️ 标题层评估(权重10%,满分10分)
+
+> **说明**: 标题作为内容导向,辅助判断内容主题
+
+#### 维度8: 标题内容指向性(10分)
+**评分依据**: 标题是否明确指向创作/制作相关的知识内容
+
+- **10分**: 标题明确包含方法/教程/技巧/原理类词汇("XX教程"、"XX技巧"、"如何XX"、"XX方法")
+- **7分**: 标题包含整理型词汇("合集"、"总结"、"分享XX方法")
+- **4分**: 描述性标题,暗示有创作知识
+- **0分**: 纯展示型标题("我的作品"、"今天做了XX")或与创作无关
+
+**得分**: __/10
+
+---
+
+**🏷️标题层总分**: __/10
+
+---
+
+### 第三步: 综合评分与判定
+
+**总分计算**:
+总分 = 图片层总分(70分) + 正文层总分(20分) + 标题层总分(10分)
+
+**最终得分**: __/100分
+
+---
+
+**判定等级**:
+- **85-100分**: ⭐⭐⭐⭐⭐ 优质内容知识 - 强烈符合
+- **70-84分**: ⭐⭐⭐⭐ 良好内容知识 - 符合
+- **55-69分**: ⭐⭐⭐ 基础内容知识 - 基本符合
+- **40-54分**: ⭐⭐ 弱内容知识 - 不太符合
+- **0-39分**: ⭐ 非内容知识 - 不符合
+
+---
+
+## 输出格式
+
+请严格按照以下JSON格式输出:
+
+{{
+  "is_content_knowledge": true/false,
+  "final_score": 85,
+  "level": "⭐⭐⭐⭐⭐ 优质内容知识",
+  "quick_exclude": {{
+    "result": "是/否",
+    "reason": "原因"
+  }},
+  "dimension_scores": {{
+    "image_layer": {{
+      "creation_method": {{"score": 20, "reason": "简述"}},
+      "knowledge_system": {{"score": 15, "reason": "简述"}},
+      "teaching_annotation": {{"score": 15, "reason": "简述"}},
+      "method_reusability": {{"score": 10, "reason": "简述"}},
+      "principle_case": {{"score": 10, "reason": "简述"}},
+      "subtotal": 70
+    }},
+    "text_layer": {{
+      "method_description": {{"score": 10, "reason": "简述"}},
+      "knowledge_summary": {{"score": 10, "reason": "简述"}},
+      "subtotal": 20
+    }},
+    "title_layer": {{
+      "content_direction": {{"score": 10, "reason": "简述"}},
+      "subtotal": 10
+    }}
+  }},
+  "core_evidence": [
+    "证据1",
+    "证据2"
+  ],
+  "issues": [
+    "问题1(如无则为空数组)"
+  ],
+  "summary": "总结陈述(2-3句话)"
+}}
+
+## 判断原则
+1. **图片主导原则**: 图片占70%权重,是核心判断依据;标题和正文为辅助
+2. **创作领域限定**: 必须属于创作/制作/设计领域,其他领域知识不属于内容知识
+3. **方法优先原则**: 重点评估是否提供了可操作的创作方法,而非纯作品展示
+4. **通用性要求**: 优先考虑方法的可复用性和可迁移性
+5. **严格性原则**: 宁可误判为"非内容知识",也不放过纯展示型内容
+6. **证据性原则**: 评分需基于明确的视觉和文本证据,可量化衡量
+"""
+
+PROMPT3_PURPOSE_MATCH = """# Prompt 1: 多模态内容目的动机匹配评估
+
+## 角色定义
+你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**目的动机匹配度**,能够精准判断帖子是否满足用户的核心意图。
+
+---
+
+## 任务说明
+你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文)
+请**仅评估目的动机维度**的匹配度,输出0-100分的量化得分。
+
+---
+
+## 输入格式
+
+**原始搜索需求:**
+{original_query}
+
+**多模态帖子内容:**
+- **图片:** {num_images}张
+- **标题:** {title}
+- **正文:** {body_text}
+
+---
+
+## 评估维度:目的动机匹配
+
+### 核心评估逻辑
+
+**目的动机 = 用户想做什么 = 核心动词/意图**
+
+常见动机类型:
+- **获取型**:寻找、下载、收藏、获取
+- **学习型**:教程、学习、了解、掌握
+- **决策型**:推荐、对比、评测、选择
+- **创作型**:拍摄、制作、设计、生成
+- **分享型**:晒单、记录、分享、展示
+
+---
+
+## 评估流程
+
+### 第一步:识别原始需求的核心动机
+- 提取**核心动词**(如果是纯名词短语,识别隐含意图)
+- 判断用户的**最终目的**是什么
+
+### 第二步:分析帖子提供的价值(重点看图片)
+
+**图片分析(权重70%):**
+- 图片展示的是什么类型的内容?
+- 图片是否直接解答了需求的目的?
+- 图片的信息完整度和实用性如何?
+
+**标题分析(权重15%):**
+- 标题是否明确了内容的目的?
+
+**正文分析(权重15%):**
+- 正文是否提供了实质性的解答内容?
+
+### 第三步:判断目的匹配度
+- 帖子是否**实质性地满足**了需求的动机?
+- 内容是否**实用、完整、可执行**?
+
+---
+
+## 评分标准(0-100分)
+
+### 高度匹配区间
+
+**90-100分:完全满足动机,内容实用完整**
+- 图片直接展示解决方案/教程步骤/对比结果
+- 内容完整、清晰、可直接使用
+- 例:需求"如何拍摄夜景" vs 图片展示完整的夜景拍摄参数设置和效果对比
+
+**75-89分:基本满足动机,信息较全面**
+- 图片提供了核心解答内容
+- 信息相对完整但深度略有不足
+- 例:需求"推荐旅行路线" vs 图片展示了路线图但缺少详细说明
+
+**60-74分:部分满足动机,有参考价值**
+- 图片提供了相关内容但不够直接
+- 需要结合文字才能理解完整意图
+
+### 中度相关区间
+
+**40-59分:弱相关,核心目的未充分满足**
+- 图片内容与动机有关联但不是直接解答
+- 实用性较低
+- 例:需求"如何拍摄" vs 图片只展示成品照片,无教程内容
+
+
+### 不相关/负向区间
+
+**20-39分:微弱关联,基本未解答**
+- 图片仅有外围相关性
+- 对满足需求帮助极小
+
+**1-19分:几乎无关**
+- 图片与需求动机关联极弱
+
+**0分:完全不相关**
+- 图片与需求动机无任何关联
+
+**负分不使用**(目的动机维度不设负分)
+
+---
+
+## 输出格式(JSON)
+```json
+{{
+  "目的动机评估": {{
+    "目的动机得分": 0-100的整数,
+    "原始需求核心动机": "识别出的用户意图(一句话)",
+    "图片提供的价值": "图片展示了什么,如何满足动机",
+    "标题体现的意图": "标题说明了什么",
+    "正文补充的内容": "正文是否有实质解答",
+    "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
+    "核心依据": "为什么给这个分数(100字以内)"
+  }}
+}}
+```
+
+---
+
+## 评估原则
+
+1. **图片优先**:图片权重70%,是判断的主要依据
+2. **实用导向**:不看表面相关,看实际解答程度
+3. **严格标准**:宁可低估,避免虚高
+4. **客观量化**:基于可观察的内容特征打分
+
+---
+
+## 特别注意
+
+- 本评估**只关注目的动机维度**,不考虑品类是否匹配
+- 输出的分数必须是**0-100的整数**
+- 不要自行计算综合分数,只输出目的动机分数
+- 评分依据要具体、可验证
+"""
+
+PROMPT4_CATEGORY_MATCH = """# Prompt 2: 多模态内容品类匹配评估
+
+## 角色定义
+你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**品类匹配度**
+能够精准判断帖子的内容主体是否与用户需求一致。
+
+---
+
+## 任务说明
+你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文),请**仅评估品类维度**的匹配度,输出0-100分的量化得分。
+
+---
+
+## 输入格式
+
+**原始搜索需求:**
+{original_query}
+
+**多模态帖子内容:**
+- **图片:** {num_images}张
+- **标题:** {title}
+- **正文:** {body_text}
+
+---
+
+## 评估维度:品类匹配
+
+### 核心评估逻辑
+
+**品类 = 核心主体(名词)+ 限定词**
+
+- **核心主体**:具体的内容对象(风光摄影、旅行攻略、美食推荐)
+- **限定词**:
+  - 地域:川西、成都、日本
+  - 时间:秋季、夏天、2024
+  - 类型:免费、高清、入门级
+  - 风格:小清新、复古、简约
+
+---
+
+## 评估流程
+
+### 第一步:提取原始需求的品类信息
+- 识别**核心主体名词**
+- 识别**关键限定词**(地域/时间/类型/风格等)
+
+### 第二步:从帖子中提取品类信息(重点看图片)
+
+**图片识别(权重70%):**
+- 图片展示的核心主体是什么?
+- 图片中可识别的限定特征(地域标志、季节特征、类型属性、风格特点)
+
+**标题提取(权重15%):**
+- 标题明确的品类名词和限定词
+
+**正文提取(权重15%):**
+- 正文描述的品类信息
+
+### 第三步:对比匹配度
+- 核心主体是否一致?
+- 限定词匹配了几个?
+- 是否存在泛化或偏移?
+
+---
+
+## 评分标准(0-100分)
+
+### 高度匹配区间
+
+**90-100分:核心主体+关键限定词完全匹配**
+- 图片展示的主体与需求精准一致
+- 关键限定词全部匹配(地域、时间、类型等)
+- 例:需求"川西秋季风光" vs 图片展示川西秋季风景
+
+**75-89分:核心主体匹配,大部分限定词匹配**
+- 图片主体一致
+- 存在1-2个限定词缺失但不影响核心匹配
+- 例:需求"川西秋季风光" vs 图片展示川西风光(缺秋季)
+
+**60-74分:核心主体匹配,部分限定词匹配**
+- 图片主体在同一大类
+- 限定词部分匹配或有合理上下位关系
+- 例:需求"川西秋季风光" vs 图片展示四川风光
+
+### 中度相关区间
+
+**40-59分:核心主体匹配,限定词大量缺失**
+- 图片主体相同但上下文不同
+- 限定词严重缺失或不匹配
+- 例:需求"川西风光摄影" vs 图片展示风光照但无地域特征
+
+### 不相关/负向区间
+
+**20-39分:主体过度泛化**
+- 图片主体是通用概念,需求是特定概念
+- 仅有抽象类别相似
+- 例:需求"川西旅行攻略" vs 图片展示普通旅行场景
+
+**1-19分:品类关联极弱**
+- 图片主体与需求差异明显
+
+**0分:品类完全不同**
+- 图片主体类别完全不同
+- 例:需求"风光摄影" vs 图片展示美食
+
+**负分不使用**(品类维度不设负分)
+
+---
+
+## 输出格式(JSON)
+```json
+{{
+  "品类评估": {{
+    "原始需求品类": {{
+      "核心主体": "提取的主体名词",
+      "关键限定词": ["限定词1", "限定词2"]
+    }},
+    "帖子实际品类": {{
+      "图片主体": "图片展示的核心主体",
+      "图片限定特征": ["从图片识别的限定词"],
+      "标题品类": "标题提及的品类",
+      "正文品类": "正文描述的品类"
+    }},
+    "品类匹配得分": 0-100的整数,
+    "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
+    "主体匹配情况": "主体是否一致",
+    "限定词匹配情况": "哪些限定词匹配/缺失",
+    "核心依据": "为什么给这个分数(100字以内)"
+  }}
+}}
+```
+
+---
+
+## 评估原则
+
+1. **图片优先**:图片权重70%,是判断的主要依据
+2. **表面匹配**:只看实际展示的内容,禁止推测联想
+3. **通用≠特定**:通用概念不等于特定概念,需明确区分
+4. **严格标准**:宁可低估,避免虚高
+5. **客观量化**:基于可观察的视觉特征和文字信息打分
+
+---
+
+## 特别注意
+
+- 本评估**只关注品类维度**,不考虑目的是否匹配
+- 输出的分数必须是**0-100的整数**
+- 不要自行计算综合分数,只输出品类分数
+- 禁止因为"可能相关"就给分,必须有明确视觉证据
+"""
+
+
+# ============================================================================
+# 辅助函数
+# ============================================================================
+
+def _clean_json_response(content_text: str) -> str:
+    """清理API返回的JSON内容"""
+    content_text = content_text.strip()
+    if content_text.startswith("```json"):
+        content_text = content_text[7:]
+    elif content_text.startswith("```"):
+        content_text = content_text[3:]
+    if content_text.endswith("```"):
+        content_text = content_text[:-3]
+    return content_text.strip()
+
+
+async def _call_openrouter_api(
+    prompt_text: str,
+    image_urls: list[str],
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> dict:
+    """
+    调用OpenRouter API的通用函数
+
+    Args:
+        prompt_text: Prompt文本
+        image_urls: 图片URL列表
+        semaphore: 并发控制信号量
+
+    Returns:
+        解析后的JSON响应
+    """
+    api_key = os.getenv("OPENROUTER_API_KEY")
+    if not api_key:
+        raise ValueError("OPENROUTER_API_KEY environment variable not set")
+
+    content = [{"type": "text", "text": prompt_text}]
+    for url in image_urls:
+        content.append({"type": "image_url", "image_url": {"url": url}})
+
+    payload = {
+        "model": MODEL_NAME,
+        "messages": [{"role": "user", "content": content}],
+        "response_format": {"type": "json_object"}
+    }
+
+    headers = {
+        "Authorization": f"Bearer {api_key}",
+        "Content-Type": "application/json"
+    }
+
+    async def _make_request():
+        loop = asyncio.get_event_loop()
+        response = await loop.run_in_executor(
+            None,
+            lambda: requests.post(
+                "https://openrouter.ai/api/v1/chat/completions",
+                headers=headers,
+                json=payload,
+                timeout=API_TIMEOUT
+            )
+        )
+
+        if response.status_code != 200:
+            raise Exception(f"API error: {response.status_code} - {response.text[:200]}")
+
+        result = response.json()
+        content_text = result["choices"][0]["message"]["content"]
+        content_text = _clean_json_response(content_text)
+        return json.loads(content_text)
+
+    async def _execute_with_retry():
+        """执行API请求,失败时自动重试最多2次"""
+        MAX_RETRIES = 2
+
+        for attempt in range(MAX_RETRIES + 1):
+            try:
+                return await _make_request()
+            except Exception as e:
+                if attempt < MAX_RETRIES:
+                    wait_time = 2 * (attempt + 1)  # 2秒, 4秒
+                    print(f"      ⚠️  API调用失败,{wait_time}秒后重试 (第{attempt + 1}/{MAX_RETRIES}次重试) - {str(e)[:50]}")
+                    await asyncio.sleep(wait_time)
+                else:
+                    # 最后一次尝试也失败,抛出异常
+                    raise
+
+    if semaphore:
+        async with semaphore:
+            return await _execute_with_retry()
+    else:
+        return await _execute_with_retry()
+
+
+# ============================================================================
+# 核心评估函数
+# ============================================================================
+
+async def evaluate_is_knowledge(
+    post,
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> Optional[KnowledgeEvaluation]:
+    """
+    Prompt1: 判断是知识
+
+    Args:
+        post: Post对象
+        semaphore: 并发控制信号量
+
+    Returns:
+        KnowledgeEvaluation 或 None(失败时)
+    """
+    if post.type == "video":
+        return None
+
+    image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
+
+    try:
+        prompt_text = PROMPT1_IS_KNOWLEDGE.format(
+            title=post.title,
+            body_text=post.body_text or "",
+            num_images=len(image_urls)
+        )
+
+        data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
+
+        return KnowledgeEvaluation(
+            is_knowledge=data.get("is_knowledge", False),
+            quick_exclude=data.get("quick_exclude", {}),
+            title_layer=data.get("title_layer", {}),
+            image_layer=data.get("image_layer", {}),
+            text_layer=data.get("text_layer", {}),
+            judgment_logic=data.get("judgment_logic", ""),
+            core_evidence=data.get("core_evidence", []),
+            issues=data.get("issues", []),
+            conclusion=data.get("conclusion", "")
+        )
+    except Exception as e:
+        print(f"      ❌ Prompt1评估失败: {post.note_id} - {str(e)[:100]}")
+        return None
+
+
+async def evaluate_is_content_knowledge(
+    post,
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> Optional[ContentKnowledgeEvaluation]:
+    """
+    Prompt2: 判断是否是内容知识
+
+    Args:
+        post: Post对象
+        semaphore: 并发控制信号量
+
+    Returns:
+        ContentKnowledgeEvaluation 或 None(失败时)
+    """
+    if post.type == "video":
+        return None
+
+    image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
+
+    try:
+        prompt_text = PROMPT2_IS_CONTENT_KNOWLEDGE.format(
+            title=post.title,
+            body_text=post.body_text or "",
+            num_images=len(image_urls)
+        )
+
+        data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
+
+        # 判定是否是内容知识:得分 >= 55 分
+        final_score = data.get("final_score", 0)
+        is_content_knowledge = final_score >= 55
+
+        return ContentKnowledgeEvaluation(
+            is_content_knowledge=is_content_knowledge,
+            final_score=final_score,
+            level=data.get("level", ""),
+            quick_exclude=data.get("quick_exclude", {}),
+            dimension_scores=data.get("dimension_scores", {}),
+            core_evidence=data.get("core_evidence", []),
+            issues=data.get("issues", []),
+            summary=data.get("summary", "")
+        )
+    except Exception as e:
+        print(f"      ❌ Prompt2评估失败: {post.note_id} - {str(e)[:100]}")
+        return None
+
+
+async def evaluate_purpose_match(
+    post,
+    original_query: str,
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> Optional[PurposeEvaluation]:
+    """
+    Prompt3: 目的性匹配评估
+
+    Args:
+        post: Post对象
+        original_query: 原始搜索query
+        semaphore: 并发控制信号量
+
+    Returns:
+        PurposeEvaluation 或 None(失败时)
+    """
+    if post.type == "video":
+        return None
+
+    image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
+
+    try:
+        prompt_text = PROMPT3_PURPOSE_MATCH.format(
+            original_query=original_query,
+            title=post.title,
+            body_text=post.body_text or "",
+            num_images=len(image_urls)
+        )
+
+        data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
+
+        # Prompt3的输出在"目的动机评估"键下
+        purpose_data = data.get("目的动机评估", {})
+
+        return PurposeEvaluation(
+            purpose_score=purpose_data.get("目的动机得分", 0),
+            core_motivation=purpose_data.get("原始需求核心动机", ""),
+            image_value=purpose_data.get("图片提供的价值", ""),
+            title_intention=purpose_data.get("标题体现的意图", ""),
+            text_content=purpose_data.get("正文补充的内容", ""),
+            match_level=purpose_data.get("匹配度等级", ""),
+            core_basis=purpose_data.get("核心依据", "")
+        )
+    except Exception as e:
+        print(f"      ❌ Prompt3评估失败: {post.note_id} - {str(e)[:100]}")
+        return None
+
+
+async def evaluate_category_match(
+    post,
+    original_query: str,
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> Optional[CategoryEvaluation]:
+    """
+    Prompt4: 品类匹配评估
+
+    Args:
+        post: Post对象
+        original_query: 原始搜索query
+        semaphore: 并发控制信号量
+
+    Returns:
+        CategoryEvaluation 或 None(失败时)
+    """
+    if post.type == "video":
+        return None
+
+    image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
+
+    try:
+        prompt_text = PROMPT4_CATEGORY_MATCH.format(
+            original_query=original_query,
+            title=post.title,
+            body_text=post.body_text or "",
+            num_images=len(image_urls)
+        )
+
+        data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
+
+        # Prompt4的输出在"品类评估"键下
+        category_data = data.get("品类评估", {})
+
+        return CategoryEvaluation(
+            category_score=category_data.get("品类匹配得分", 0),
+            original_category=category_data.get("原始需求品类", {}),
+            actual_category=category_data.get("帖子实际品类", {}),
+            match_level=category_data.get("匹配度等级", ""),
+            subject_match=category_data.get("主体匹配情况", ""),
+            qualifier_match=category_data.get("限定词匹配情况", ""),
+            core_basis=category_data.get("核心依据", "")
+        )
+    except Exception as e:
+        print(f"      ❌ Prompt4评估失败: {post.note_id} - {str(e)[:100]}")
+        return None
+
+
+def calculate_final_score(purpose_score: int, category_score: int) -> tuple[float, str]:
+    """
+    计算综合得分和匹配等级
+
+    Args:
+        purpose_score: 目的性得分 (0-100整数)
+        category_score: 品类得分 (0-100整数)
+
+    Returns:
+        (final_score, match_level)
+        - final_score: 保留2位小数
+        - match_level: 匹配等级字符串
+    """
+    # 计算综合得分: 目的性70% + 品类30%
+    final = round(purpose_score * 0.7 + category_score * 0.3, 2)
+
+    # 判定匹配等级
+    if final >= 85:
+        level = "高度匹配"
+    elif final >= 70:
+        level = "基本匹配"
+    elif final >= 50:
+        level = "部分匹配"
+    elif final >= 30:
+        level = "弱匹配"
+    else:
+        level = "不匹配"
+
+    return final, level
+
+
+async def evaluate_post_v3(
+    post,
+    original_query: str,
+    semaphore: Optional[asyncio.Semaphore] = None
+) -> tuple:
+    """
+    V3评估主函数(4步流程)
+
+    流程:
+    1. Prompt1: 判断是知识 → 如果不是知识,停止
+    2. Prompt2: 判断是否是内容知识 → 如果不是内容知识,停止
+    3. Prompt3 & Prompt4: 并行执行目的性和品类匹配
+    4. 计算综合得分
+
+    Returns:
+        (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
+        任一步骤失败,后续结果为None
+    """
+    if post.type == "video":
+        print(f"      ⊗ 跳过视频帖子: {post.note_id}")
+        return (None, None, None, None, None, None)
+
+    print(f"      🔍 开始V3评估: {post.note_id}")
+
+    # Step 1: 判断是知识
+    print(f"      📝 Step 1/4: 判断是知识...")
+    knowledge_eval = await evaluate_is_knowledge(post, semaphore)
+
+    if not knowledge_eval:
+        print(f"      ❌ Step 1失败,停止评估")
+        return (None, None, None, None, None, None)
+
+    if not knowledge_eval.is_knowledge:
+        print(f"      ⊗ 非知识内容,停止后续评估")
+        return (knowledge_eval, None, None, None, None, None)
+
+    print(f"      ✅ Step 1: 是知识内容")
+
+    # Step 2: 判断是否是内容知识
+    print(f"      📝 Step 2/4: 判断是否是内容知识...")
+    content_eval = await evaluate_is_content_knowledge(post, semaphore)
+
+    if not content_eval:
+        print(f"      ❌ Step 2失败,停止评估")
+        return (knowledge_eval, None, None, None, None, None)
+
+    if not content_eval.is_content_knowledge:
+        print(f"      ⊗ 非内容知识,停止后续评估 (得分: {content_eval.final_score})")
+        return (knowledge_eval, content_eval, None, None, None, None)
+
+    print(f"      ✅ Step 2: 是内容知识 (得分: {content_eval.final_score})")
+
+    # Step 3 & 4: 并行执行目的性和品类匹配
+    print(f"      📝 Step 3&4/4: 并行执行目的性和品类匹配...")
+    purpose_task = evaluate_purpose_match(post, original_query, semaphore)
+    category_task = evaluate_category_match(post, original_query, semaphore)
+
+    purpose_eval, category_eval = await asyncio.gather(purpose_task, category_task)
+
+    if not purpose_eval or not category_eval:
+        print(f"      ❌ Step 3或4失败")
+        return (knowledge_eval, content_eval, purpose_eval, category_eval, None, None)
+
+    print(f"      ✅ Step 3: 目的性得分 = {purpose_eval.purpose_score}")
+    print(f"      ✅ Step 4: 品类得分 = {category_eval.category_score}")
+
+    # Step 5: 计算综合得分
+    final_score, match_level = calculate_final_score(
+        purpose_eval.purpose_score,
+        category_eval.category_score
+    )
+
+    print(f"      ✅ 综合得分: {final_score} ({match_level})")
+
+    return (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
+
+
+def apply_evaluation_v3_to_post(
+    post,
+    knowledge_eval: Optional[KnowledgeEvaluation],
+    content_eval: Optional[ContentKnowledgeEvaluation],
+    purpose_eval: Optional[PurposeEvaluation],
+    category_eval: Optional[CategoryEvaluation],
+    final_score: Optional[float],
+    match_level: Optional[str]
+):
+    """
+    将V3评估结果应用到Post对象(覆盖原有字段)
+
+    Args:
+        post: Post对象
+        knowledge_eval: Prompt1结果
+        content_eval: Prompt2结果
+        purpose_eval: Prompt3结果
+        category_eval: Prompt4结果
+        final_score: 综合得分
+        match_level: 匹配等级
+    """
+    # Prompt1: 判断是知识
+    if knowledge_eval:
+        post.is_knowledge = knowledge_eval.is_knowledge
+        post.knowledge_evaluation = {
+            "quick_exclude": knowledge_eval.quick_exclude,
+            "title_layer": knowledge_eval.title_layer,
+            "image_layer": knowledge_eval.image_layer,
+            "text_layer": knowledge_eval.text_layer,
+            "judgment_logic": knowledge_eval.judgment_logic,
+            "core_evidence": knowledge_eval.core_evidence,
+            "issues": knowledge_eval.issues,
+            "conclusion": knowledge_eval.conclusion
+        }
+
+    # Prompt2: 判断是否是内容知识
+    if content_eval:
+        post.is_content_knowledge = content_eval.is_content_knowledge
+        post.knowledge_score = float(content_eval.final_score)
+        post.content_knowledge_evaluation = {
+            "is_content_knowledge": content_eval.is_content_knowledge,
+            "final_score": content_eval.final_score,
+            "level": content_eval.level,
+            "quick_exclude": content_eval.quick_exclude,
+            "dimension_scores": content_eval.dimension_scores,
+            "core_evidence": content_eval.core_evidence,
+            "issues": content_eval.issues,
+            "summary": content_eval.summary
+        }
+
+    # Prompt3: 目的性匹配
+    if purpose_eval:
+        post.purpose_score = purpose_eval.purpose_score
+        post.purpose_evaluation = {
+            "purpose_score": purpose_eval.purpose_score,
+            "core_motivation": purpose_eval.core_motivation,
+            "image_value": purpose_eval.image_value,
+            "title_intention": purpose_eval.title_intention,
+            "text_content": purpose_eval.text_content,
+            "match_level": purpose_eval.match_level,
+            "core_basis": purpose_eval.core_basis
+        }
+
+    # Prompt4: 品类匹配
+    if category_eval:
+        post.category_score = category_eval.category_score
+        post.category_evaluation = {
+            "category_score": category_eval.category_score,
+            "original_category": category_eval.original_category,
+            "actual_category": category_eval.actual_category,
+            "match_level": category_eval.match_level,
+            "subject_match": category_eval.subject_match,
+            "qualifier_match": category_eval.qualifier_match,
+            "core_basis": category_eval.core_basis
+        }
+
+    # 综合得分
+    if final_score is not None and match_level is not None:
+        post.final_score = final_score
+        post.match_level = match_level
+
+    # 设置评估时间和版本
+    post.evaluation_time = datetime.now().isoformat()
+    post.evaluator_version = "v3.0"
+
+
+async def batch_evaluate_posts_v3(
+    posts: list,
+    original_query: str,
+    max_concurrent: int = MAX_CONCURRENT_EVALUATIONS
+) -> int:
+    """
+    批量评估多个帖子(V3版本)
+
+    Args:
+        posts: Post对象列表
+        original_query: 原始搜索query
+        max_concurrent: 最大并发数
+
+    Returns:
+        成功评估的帖子数量
+    """
+    semaphore = asyncio.Semaphore(max_concurrent)
+
+    print(f"\n📊 开始批量评估 {len(posts)} 个帖子(并发限制: {max_concurrent})...")
+
+    tasks = [evaluate_post_v3(post, original_query, semaphore) for post in posts]
+    results = await asyncio.gather(*tasks)
+
+    success_count = 0
+    for i, result in enumerate(results):
+        knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = result
+
+        # 只要有Prompt1结果就算部分成功
+        if knowledge_eval:
+            apply_evaluation_v3_to_post(
+                posts[i],
+                knowledge_eval,
+                content_eval,
+                purpose_eval,
+                category_eval,
+                final_score,
+                match_level
+            )
+            success_count += 1
+
+    print(f"✅ 批量评估完成: {success_count}/{len(posts)} 帖子已评估")
+
+    return success_count

+ 484 - 0
script/search/test_xiaohongshu_search_api.py

@@ -0,0 +1,484 @@
+#!/usr/bin/env python3
+"""
+小红书搜索API测试脚本
+用于测试和调试 xhs_note_search API 接口
+"""
+
+import requests
+import json
+import time
+import sys
+from datetime import datetime
+from typing import Dict, Any, List, Tuple
+from xiaohongshu_search import XiaohongshuSearch
+
+
+# 颜色输出
+class Colors:
+    """终端颜色输出"""
+    GREEN = '\033[92m'
+    RED = '\033[91m'
+    YELLOW = '\033[93m'
+    BLUE = '\033[94m'
+    CYAN = '\033[96m'
+    RESET = '\033[0m'
+    BOLD = '\033[1m'
+
+
+class TestResult:
+    """测试结果记录"""
+    def __init__(self):
+        self.total = 0
+        self.passed = 0
+        self.failed = 0
+        self.errors = []
+        self.details = []
+
+
+class XiaohongshuAPITester:
+    """小红书API测试器"""
+
+    BASE_URL = "http://47.84.182.56:8001"
+    API_ENDPOINT = "/tools/call/xhs_note_search"
+
+    def __init__(self, verbose: bool = True):
+        """
+        初始化测试器
+
+        Args:
+            verbose: 是否输出详细日志
+        """
+        self.verbose = verbose
+        self.result = TestResult()
+        self.client = XiaohongshuSearch()
+
+    def log(self, message: str, color: str = Colors.RESET):
+        """输出日志"""
+        if self.verbose:
+            print(f"{color}{message}{Colors.RESET}")
+
+    def log_section(self, title: str):
+        """输出章节标题"""
+        print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}")
+        print(f"{Colors.BOLD}{Colors.CYAN}{title}{Colors.RESET}")
+        print(f"{Colors.BOLD}{Colors.CYAN}{'='*70}{Colors.RESET}\n")
+
+    def test_raw_api_request(
+        self,
+        keyword: str,
+        content_type: str = "不限",
+        sort_type: str = "综合",
+        publish_time: str = "不限",
+        cursor: str = "",
+        timeout: int = 30
+    ) -> Tuple[bool, Dict[str, Any], float]:
+        """
+        直接测试原始API请求(不使用封装类)
+
+        Returns:
+            (是否成功, 响应数据, 响应时间)
+        """
+        url = f"{self.BASE_URL}{self.API_ENDPOINT}"
+        payload = {
+            "keyword": keyword,
+            "content_type": content_type,
+            "sort_type": sort_type,
+            "publish_time": publish_time,
+            "cursor": cursor
+        }
+
+        self.log(f"\n{Colors.YELLOW}[请求详情]{Colors.RESET}")
+        self.log(f"URL: {url}")
+        self.log(f"Method: POST")
+        self.log(f"Headers: {{'Content-Type': 'application/json'}}")
+        self.log(f"Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
+
+        start_time = time.time()
+        try:
+            response = requests.post(
+                url,
+                json=payload,
+                timeout=timeout,
+                headers={"Content-Type": "application/json"}
+            )
+            elapsed = time.time() - start_time
+
+            self.log(f"\n{Colors.YELLOW}[响应详情]{Colors.RESET}")
+            self.log(f"状态码: {response.status_code}")
+            self.log(f"响应时间: {elapsed:.2f}秒")
+            self.log(f"响应头: {dict(response.headers)}")
+
+            # 尝试解析JSON
+            try:
+                result = response.json()
+                self.log(f"响应体预览: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...")
+
+                # 检查HTTP状态码
+                if response.status_code == 200:
+                    return True, result, elapsed
+                else:
+                    self.log(f"{Colors.RED}HTTP错误: {response.status_code}{Colors.RESET}")
+                    return False, {"error": f"HTTP {response.status_code}", "detail": result}, elapsed
+
+            except json.JSONDecodeError as e:
+                self.log(f"{Colors.RED}JSON解析失败: {e}{Colors.RESET}")
+                self.log(f"原始响应: {response.text[:500]}")
+                return False, {"error": "JSON解析失败", "detail": str(e), "raw": response.text}, elapsed
+
+        except requests.exceptions.Timeout:
+            elapsed = time.time() - start_time
+            self.log(f"{Colors.RED}请求超时({timeout}秒){Colors.RESET}")
+            return False, {"error": "Timeout", "timeout": timeout}, elapsed
+
+        except requests.exceptions.ConnectionError as e:
+            elapsed = time.time() - start_time
+            self.log(f"{Colors.RED}连接错误: {e}{Colors.RESET}")
+            return False, {"error": "ConnectionError", "detail": str(e)}, elapsed
+
+        except requests.exceptions.RequestException as e:
+            elapsed = time.time() - start_time
+            self.log(f"{Colors.RED}请求异常: {e}{Colors.RESET}")
+            return False, {"error": "RequestException", "detail": str(e)}, elapsed
+
+    def run_test(self, test_name: str, test_func, *args, **kwargs) -> bool:
+        """
+        运行单个测试
+
+        Returns:
+            是否通过
+        """
+        self.result.total += 1
+        self.log(f"\n{Colors.BLUE}[测试 {self.result.total}] {test_name}{Colors.RESET}")
+
+        try:
+            success, data, elapsed = test_func(*args, **kwargs)
+
+            if success:
+                self.result.passed += 1
+                self.log(f"{Colors.GREEN}✓ 通过{Colors.RESET} (耗时: {elapsed:.2f}秒)")
+                self.result.details.append({
+                    "name": test_name,
+                    "status": "PASS",
+                    "elapsed": elapsed
+                })
+                return True
+            else:
+                self.result.failed += 1
+                error_info = {
+                    "name": test_name,
+                    "error": data.get("error", "Unknown"),
+                    "detail": data.get("detail", "")
+                }
+                self.result.errors.append(error_info)
+                self.result.details.append({
+                    "name": test_name,
+                    "status": "FAIL",
+                    "elapsed": elapsed,
+                    "error": error_info
+                })
+                self.log(f"{Colors.RED}✗ 失败{Colors.RESET}")
+                self.log(f"{Colors.RED}错误: {error_info}{Colors.RESET}")
+                return False
+
+        except Exception as e:
+            self.result.failed += 1
+            error_info = {
+                "name": test_name,
+                "error": "Exception",
+                "detail": str(e)
+            }
+            self.result.errors.append(error_info)
+            self.result.details.append({
+                "name": test_name,
+                "status": "ERROR",
+                "error": error_info
+            })
+            self.log(f"{Colors.RED}✗ 异常: {e}{Colors.RESET}")
+            return False
+
+    def test_basic_keywords(self):
+        """测试基础关键词搜索"""
+        self.log_section("1. 基础关键词测试")
+
+        # 测试简单关键词
+        self.run_test(
+            "简单关键词: 美食",
+            self.test_raw_api_request,
+            keyword="美食"
+        )
+
+        # 测试复杂中文关键词
+        self.run_test(
+            "复杂关键词: 表情包怎么制作",
+            self.test_raw_api_request,
+            keyword="表情包怎么制作"
+        )
+
+        # 测试英文关键词
+        self.run_test(
+            "英文关键词: food",
+            self.test_raw_api_request,
+            keyword="food"
+        )
+
+        # 测试带特殊符号的关键词
+        self.run_test(
+            "特殊符号: Python编程#入门",
+            self.test_raw_api_request,
+            keyword="Python编程#入门"
+        )
+
+    def test_content_types(self):
+        """测试不同内容类型"""
+        self.log_section("2. 内容类型测试")
+
+        keyword = "美食"
+
+        for content_type in ["不限", "视频", "图文"]:
+            self.run_test(
+                f"内容类型: {content_type}",
+                self.test_raw_api_request,
+                keyword=keyword,
+                content_type=content_type
+            )
+
+    def test_sort_types(self):
+        """测试不同排序方式"""
+        self.log_section("3. 排序方式测试")
+
+        keyword = "旅游"
+
+        for sort_type in ["综合", "最新", "最多点赞", "最多评论"]:
+            self.run_test(
+                f"排序方式: {sort_type}",
+                self.test_raw_api_request,
+                keyword=keyword,
+                sort_type=sort_type
+            )
+
+    def test_publish_times(self):
+        """测试不同发布时间筛选"""
+        self.log_section("4. 发布时间筛选测试")
+
+        keyword = "健身"
+
+        for publish_time in ["不限", "一天内", "一周内", "半年内"]:
+            self.run_test(
+                f"发布时间: {publish_time}",
+                self.test_raw_api_request,
+                keyword=keyword,
+                publish_time=publish_time
+            )
+
+    def test_edge_cases(self):
+        """测试边缘情况"""
+        self.log_section("5. 边缘情况测试")
+
+        # 空关键词
+        self.run_test(
+            "空关键词",
+            self.test_raw_api_request,
+            keyword=""
+        )
+
+        # 超长关键词
+        self.run_test(
+            "超长关键词(100字符)",
+            self.test_raw_api_request,
+            keyword="如何" * 50
+        )
+
+        # 纯空格
+        self.run_test(
+            "纯空格关键词",
+            self.test_raw_api_request,
+            keyword="   "
+        )
+
+        # 超时测试(设置1秒超时)
+        self.run_test(
+            "超时测试(1秒)",
+            self.test_raw_api_request,
+            keyword="美食",
+            timeout=1
+        )
+
+    def test_invalid_parameters(self):
+        """测试无效参数"""
+        self.log_section("6. 无效参数测试")
+
+        # 无效的content_type
+        self.run_test(
+            "无效content_type: 音频",
+            self.test_raw_api_request,
+            keyword="音乐",
+            content_type="音频"
+        )
+
+        # 无效的sort_type
+        self.run_test(
+            "无效sort_type: 随机",
+            self.test_raw_api_request,
+            keyword="音乐",
+            sort_type="随机"
+        )
+
+    def test_using_wrapper_class(self):
+        """测试使用封装类"""
+        self.log_section("7. 封装类测试")
+
+        def test_wrapper(keyword: str, **kwargs) -> Tuple[bool, Dict, float]:
+            """使用XiaohongshuSearch类进行测试"""
+            start_time = time.time()
+            try:
+                result = self.client.search(keyword, **kwargs)
+                elapsed = time.time() - start_time
+                return True, result, elapsed
+            except Exception as e:
+                elapsed = time.time() - start_time
+                return False, {"error": str(e)}, elapsed
+
+        self.run_test(
+            "封装类: 基础搜索",
+            test_wrapper,
+            keyword="美食"
+        )
+
+        self.run_test(
+            "封装类: 完整参数",
+            test_wrapper,
+            keyword="旅游",
+            content_type="视频",
+            sort_type="最新",
+            publish_time="一周内"
+        )
+
+    def generate_report(self) -> Dict[str, Any]:
+        """生成测试报告"""
+        self.log_section("测试报告")
+
+        report = {
+            "timestamp": datetime.now().isoformat(),
+            "summary": {
+                "total": self.result.total,
+                "passed": self.result.passed,
+                "failed": self.result.failed,
+                "pass_rate": f"{(self.result.passed/self.result.total*100):.1f}%" if self.result.total > 0 else "N/A"
+            },
+            "details": self.result.details,
+            "errors": self.result.errors
+        }
+
+        # 打印摘要
+        print(f"\n{Colors.BOLD}测试摘要:{Colors.RESET}")
+        print(f"  总计: {self.result.total}")
+        print(f"  {Colors.GREEN}通过: {self.result.passed}{Colors.RESET}")
+        print(f"  {Colors.RED}失败: {self.result.failed}{Colors.RESET}")
+        print(f"  通过率: {report['summary']['pass_rate']}")
+
+        if self.result.errors:
+            print(f"\n{Colors.RED}{Colors.BOLD}失败详情:{Colors.RESET}")
+            for i, error in enumerate(self.result.errors, 1):
+                print(f"{Colors.RED}  {i}. {error['name']}{Colors.RESET}")
+                print(f"     错误: {error['error']}")
+                if error.get('detail'):
+                    print(f"     详情: {error['detail'][:200]}")
+
+        return report
+
+    def save_report(self, filepath: str = None):
+        """保存测试报告"""
+        if filepath is None:
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+            filepath = f"test_report_{timestamp}.json"
+
+        report = self.generate_report()
+
+        with open(filepath, 'w', encoding='utf-8') as f:
+            json.dump(report, f, ensure_ascii=False, indent=2)
+
+        print(f"\n{Colors.GREEN}报告已保存: {filepath}{Colors.RESET}")
+        return filepath
+
+    def run_all_tests(self):
+        """运行所有测试"""
+        print(f"{Colors.BOLD}{Colors.CYAN}")
+        print("=" * 70)
+        print("小红书搜索API测试")
+        print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+        print("=" * 70)
+        print(Colors.RESET)
+
+        # 运行所有测试
+        self.test_basic_keywords()
+        self.test_content_types()
+        self.test_sort_types()
+        self.test_publish_times()
+        self.test_edge_cases()
+        self.test_invalid_parameters()
+        self.test_using_wrapper_class()
+
+        # 生成并保存报告
+        return self.save_report()
+
+
+def run_quick_test(keyword: str = "表情包怎么制作"):
+    """
+    快速测试单个关键词
+
+    Args:
+        keyword: 要测试的关键词
+    """
+    tester = XiaohongshuAPITester(verbose=True)
+
+    print(f"{Colors.BOLD}{Colors.CYAN}")
+    print("=" * 70)
+    print(f"快速测试: {keyword}")
+    print("=" * 70)
+    print(Colors.RESET)
+
+    tester.run_test(
+        f"测试关键词: {keyword}",
+        tester.test_raw_api_request,
+        keyword=keyword
+    )
+
+    tester.generate_report()
+
+
+def main():
+    """主函数"""
+    import argparse
+
+    parser = argparse.ArgumentParser(description='小红书搜索API测试工具')
+    parser.add_argument(
+        '--mode',
+        type=str,
+        choices=['full', 'quick'],
+        default='quick',
+        help='测试模式: full(完整测试) 或 quick(快速测试)'
+    )
+    parser.add_argument(
+        '--keyword',
+        type=str,
+        default='表情包怎么制作',
+        help='快速测试的关键词'
+    )
+    parser.add_argument(
+        '--verbose',
+        action='store_true',
+        default=True,
+        help='输出详细日志'
+    )
+
+    args = parser.parse_args()
+
+    if args.mode == 'full':
+        tester = XiaohongshuAPITester(verbose=args.verbose)
+        tester.run_all_tests()
+    else:
+        run_quick_test(args.keyword)
+
+
+if __name__ == "__main__":
+    main()

+ 48 - 21
script/search/xiaohongshu_search.py

@@ -8,6 +8,7 @@ import requests
 import json
 import os
 import argparse
+import time
 from datetime import datetime
 from typing import Dict, Any
 
@@ -40,14 +41,16 @@ class XiaohongshuSearch:
     def search(
         self,
         keyword: str,
-        content_type: str = "不限",
+        content_type: str = "图文",
         sort_type: str = "综合",
         publish_time: str = "不限",
         cursor: str = "",
-        timeout: int = 30
+        timeout: int = 30,
+        max_retries: int = 3,
+        retry_delay: int = 2
     ) -> Dict[str, Any]:
         """
-        搜索小红书笔记
+        搜索小红书笔记(带重试机制)
 
         Args:
             keyword: 搜索关键词
@@ -56,12 +59,14 @@ class XiaohongshuSearch:
             publish_time: 发布时间筛选,可选值:不限、一天内、一周内、半年内,默认为'不限'
             cursor: 翻页游标,第一页默认为空,下一页的游标在上一页的返回值中获取
             timeout: 请求超时时间(秒),默认30秒
+            max_retries: 最大重试次数,默认3次
+            retry_delay: 重试间隔时间(秒),默认2秒
 
         Returns:
             API响应的JSON数据
 
         Raises:
-            requests.exceptions.RequestException: 请求失败时抛出异常
+            requests.exceptions.RequestException: 所有重试都失败时抛出异常
         """
         payload = {
             "keyword": keyword,
@@ -71,23 +76,45 @@ class XiaohongshuSearch:
             "cursor": cursor
         }
 
-        try:
-            response = requests.post(
-                self.api_url,
-                json=payload,
-                timeout=timeout,
-                headers={"Content-Type": "application/json"}
-            )
-            response.raise_for_status()
-            result = response.json()
-
-            # 预处理返回数据:提取 image_list 中的 URL 字符串
-            self._preprocess_response(result)
-
-            return result
-        except requests.exceptions.RequestException as e:
-            print(f"请求失败: {e}")
-            raise
+        last_exception = None
+
+        # 重试循环:最多尝试 max_retries 次
+        for attempt in range(1, max_retries + 1):
+            try:
+                if attempt > 1:
+                    print(f"    重试第 {attempt - 1}/{max_retries - 1} 次: {keyword}")
+
+                response = requests.post(
+                    self.api_url,
+                    json=payload,
+                    timeout=timeout,
+                    headers={"Content-Type": "application/json"}
+                )
+                response.raise_for_status()
+                result = response.json()
+
+                # 预处理返回数据:提取 image_list 中的 URL 字符串
+                self._preprocess_response(result)
+
+                if attempt > 1:
+                    print(f"    ✓ 重试成功")
+
+                return result
+
+            except requests.exceptions.RequestException as e:
+                last_exception = e
+
+                if attempt < max_retries:
+                    # 还有重试机会,等待后继续
+                    print(f"    ✗ 请求失败 (第{attempt}次尝试): {e}")
+                    print(f"    等待 {retry_delay} 秒后重试...")
+                    time.sleep(retry_delay)
+                else:
+                    # 已达最大重试次数,抛出异常
+                    print(f"    ✗ 请求失败 (已达最大重试次数 {max_retries}): {e}")
+
+        # 所有重试都失败,抛出最后一次的异常
+        raise last_exception
 
     def _preprocess_response(self, result: Dict[str, Any]) -> None:
         """

+ 0 - 1061
sug_v6_1_2_10.py

@@ -1,1061 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 相关度评估专家
-class RelevanceEvaluation(BaseModel):
-    """相关度评估"""
-    reason: str = Field(..., description="评估理由")
-    relevance_score: float = Field(..., description="相关性分数 -1~1")
-
-relevance_evaluation_instructions = """
-# 角色定义
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的相关度满足度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 两大评估维度
-本评估系统始终围绕 **两个核心维度** 进行:
-
-### 1. 动机维度(权重70%)
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 2. 品类维度(权重30%)
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 初始权重 = 0
-→ 相关度评估以品类匹配为主
-示例:
-"摄影" → 无法识别动机,初始权重=0
-"川西风光" → 无法识别动机,初始权重=0
-
-
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【综合相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的相关度
-    └→ 输出: -1到1之间的数值 + 分维度得分 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估(权重70%)
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.8~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"素材下载教程"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
-维度2: 品类维度评估(权重30%)
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,大部分限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.7: 核心主体匹配,少量限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.45: 仅主体词匹配,限定词全部缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.15: 主题领域相关但品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 主体词部分相关但类别明显不同
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
-综合得分计算与规则调整
-步骤1: 应用依存性规则
-规则A: 动机高分保护机制
-
-如果 动机维度得分 ≥ 0.8:
-   → 品类得分即使为0或轻微负向(-0.2~0)
-   → 最终得分 = max(初步得分, 0.55)
-
-解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-规则B: 动机低分限制机制
-如果 动机维度得分 ≤ 0.2:
-   → 无论品类得分多高
-   → 最终得分 = min(初步得分, 0.4)
-
-解释: 目的不符时,品类匹配的价值有限
-
-规则C: 动机负向决定机制
-如果 动机维度得分 < 0:
-   → 最终得分 = min(初步得分, 0)
-
-解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-步骤3: 输出最终得分
-
-#基础加权计算
-应用规则后的调整得分 = 目的动机维度得分 × 0.7 + 品类维度得分 × 0.3
-取值范围: -1.0 ~ +1.0
-
----
-
-# 得分档位解释
-
-高度相关】+0.8 ~ +1.0
-相关性高度契合,用户可直接使用
-动机和品类均高度匹配
-典型场景: 动机≥0.85 且 品类≥0.7
-【中高相关】+0.6 ~ +0.79
-相关性较好,用户基本满意
-动机匹配但品类有泛化,或反之
-典型场景: 动机≥0.8 且 品类≥0.3
-【中度相关】+0.3 ~ +0.59
-部分相关,用户需要调整搜索策略
-动机或品类存在一定偏差
-典型场景: 动机0.4-0.7 且 品类0.3-0.7
-【弱相关】+0.01 ~ +0.29
-关联微弱,参考价值有限
-仅有表层词汇重叠
-【无关】0
-无明确关联
-原始问题无法识别动机 且 sug词无明确动作
-没有目的性且没有品类匹配
-【轻度负向】-0.29 ~ -0.01
-产生轻微误导或干扰
-【中度负向】-0.69 ~ -0.3
-存在明显冲突或误导
-【严重负向】-1.0 ~ -0.7
-完全违背意图或产生有害引导
-
----
-
-# 输出要求
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-
-#注意事项:
-始终围绕两个核心维度:所有评估都基于"动机"和"品类"两个维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题无明确关联,既不相关也不冲突时给予零分
-分维度独立评分:
-先提取原始问题核心动机
-分别计算动机维度(含两个子维度)和品类维度得分
-按70:30加权得到初步得分
-应用规则调整得到最终得分
-动机优先原则:当动机高度一致时,品类的合理泛化或具体化不应导致低评分
-技巧类需求特殊对待:包含"技巧/方法/教程"等词的需求,对动作一致性要求更严格
-
-## 输出
-- reason: 详细理由
-- relevance_score: -1到1的相关性分数
-""".strip()
-
-relevance_evaluator = Agent[None](
-    name="相关度评估专家",
-    instructions=relevance_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=RelevanceEvaluation,
-)
-
-
-# Agent 3: 加词选择专家
-class WordSelection(BaseModel):
-    """加词选择结果"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-word_selection_instructions = """
-你是加词选择专家。
-
-## 任务
-从候选词列表中选择一个最合适的词,与当前seed组合成新的query。
-
-## 原则
-1. 选择与当前seed最相关的词
-2. 组合后的query要语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-## 输出
-- selected_word: 选中的词
-- combined_query: 组合后的新query
-- reasoning: 选择理由
-""".strip()
-
-word_selector = Agent[None](
-    name="加词选择专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelection,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id", ""),
-        title=note_card.get("display_title", ""),
-        body_text=note_card.get("desc", ""),
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    Returns:
-        tuple[float, str]: (相关度分数, 评估理由)
-    """
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前文本>
-{text}
-</当前文本>
-
-请评估当前文本与原始问题的相关度。
-"""
-    result = await Runner.run(relevance_evaluator, eval_input)
-    evaluation: RelevanceEvaluation = result.final_output
-    return evaluation.relevance_score, evaluation.reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o)
-        return seg
-
-    if seg_list:
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1
-    print(f"\n[步骤3] 构建word_list_1...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1: {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Word], list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list: 当前的word列表
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (word_list_next, q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "input_word_list_size": len(word_list),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug
-    async def evaluate_sug(sug: Sug) -> Sug:
-        sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o)
-        return sug
-
-    if all_sugs:
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建word_list_next: word_list -> word_list_next(先直接复制)
-    print(f"\n[步骤4] 构建word_list_next(暂时直接复制)...")
-    word_list_next = word_list.copy()
-
-    # 5. 构建q_list_next
-    print(f"\n[步骤5] 构建q_list_next...")
-    q_list_next = []
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-
-    # 5.1 对于seed_list中的每个seed,从word_list_next中选一个未加过的词
-    print(f"\n  5.1 为每个seed加词...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 简单过滤:找出不在seed.text中且未被添加过的词
-        candidate_words = []
-        for word in word_list_next:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词: {[w.text for w in candidate_words]}")
-
-        # 使用Agent选择最合适的词
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{', '.join([w.text for w in candidate_words])}
-</候选词列表>
-
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-        result = await Runner.run(word_selector, selection_input)
-        selection: WordSelection = result.final_output
-
-        # 验证选择的词是否在候选列表中
-        if selection.selected_word not in [w.text for w in candidate_words]:
-            print(f"      ✗ Agent选择的词 '{selection.selected_word}' 不在候选列表中,跳过")
-            continue
-
-        print(f"      ✓ 选择词: {selection.selected_word}")
-        print(f"      ✓ 新query: {selection.combined_query}")
-        print(f"      理由: {selection.reasoning}")
-
-        # 评估新query
-        new_q_score, new_q_reason = await evaluate_with_o(selection.combined_query, o)
-        print(f"      新query评分: {new_q_score:.2f}")
-
-        # 创建新的q
-        new_q = Q(
-            text=selection.combined_query,
-            score_with_o=new_q_score,
-            reason=new_q_reason,
-            from_source="add"
-        )
-        q_list_next.append(new_q)
-
-        # 更新seed的added_words
-        seed.added_words.append(selection.selected_word)
-
-        # 保存到add_word_details
-        if seed.text not in add_word_details:
-            add_word_details[seed.text] = []
-        add_word_details[seed.text].append({
-            "text": selection.combined_query,
-            "score": new_q_score,
-            "reason": new_q_reason,
-            "selected_word": selection.selected_word
-        })
-
-    # 5.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next
-    print(f"\n  5.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 6. 更新seed_list
-    print(f"\n[步骤6] 更新seed_list...")
-    seed_list_next = seed_list.copy()  # 保留原有的seed
-
-    # 对于sug_list_list中,每个sug分数大于来源query分数的,且没在seed_list中出现过的,加入
-    existing_seed_texts = {seed.text for seed in seed_list_next}
-    for sug in all_sugs:
-        # 新逻辑:sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"  ✓ 新seed: {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "word_list_next_size": len(word_list_next),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],  # 下一轮种子列表
-        "sug_details": sug_details,  # 每个Q对应的sug列表
-        "add_word_details": add_word_details,  # 每个seed对应的组合词列表
-        "search_results": search_results_data  # 搜索结果(包含帖子详情)
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  seed数量: {len(seed_list_next)}")
-
-    return word_list_next, q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        word_list, q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list=word_list,
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 执行迭代
-    all_search_list = await iterative_loop(
-        run_context,
-        max_rounds=max_rounds,
-        sug_threshold=sug_threshold
-    )
-
-    # 格式化输出
-    output = f"原始需求:{run_context.c}\n"
-    output += f"原始问题:{run_context.o}\n"
-    output += f"总搜索次数:{len(all_search_list)}\n"
-    output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-    output += "\n" + "="*60 + "\n"
-
-    if all_search_list:
-        output += "【搜索结果】\n\n"
-        for idx, search in enumerate(all_search_list, 1):
-            output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-            output += f"   帖子数: {len(search.post_list)}\n"
-            if search.post_list:
-                for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                    output += f"   {post_idx}) {post.title}\n"
-                    output += f"      URL: {post.note_url}\n"
-            output += "\n"
-    else:
-        output += "未找到搜索结果\n"
-
-    run_context.final_output = output
-
-    print(f"\n{'='*60}")
-    print("最终结果")
-    print(f"{'='*60}")
-    print(output)
-
-    # 保存日志
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-    context_dict = run_context.model_dump()
-    with open(context_file_path, "w", encoding="utf-8") as f:
-        json.dump(context_dict, f, ensure_ascii=False, indent=2)
-    print(f"\nRunContext saved to: {context_file_path}")
-
-    # 保存详细的搜索结果
-    search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-    search_results_data = [s.model_dump() for s in all_search_list]
-    with open(search_results_path, "w", encoding="utf-8") as f:
-        json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-    print(f"Search results saved to: {search_results_path}")
-
-    # 可视化
-    if visualize:
-        import subprocess
-        output_html = os.path.join(run_context.log_dir, "visualization.html")
-        print(f"\n🎨 生成可视化HTML...")
-
-        # 获取绝对路径
-        abs_context_file = os.path.abspath(context_file_path)
-        abs_output_html = os.path.abspath(output_html)
-
-        # 运行可视化脚本
-        result = subprocess.run([
-            "node",
-            "visualization/sug_v6_1_2_8/index.js",
-            abs_context_file,
-            abs_output_html
-        ])
-
-        if result.returncode == 0:
-            print(f"✅ 可视化已生成: {output_html}")
-        else:
-            print(f"❌ 可视化生成失败")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.8 轮次迭代版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 2"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1039
sug_v6_1_2_11.py

@@ -1,1039 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2.1: 动机维度评估专家
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    motivation_score: float = Field(..., description="动机维度得分 -1~1")
-    reason: str = Field(..., description="动机评估理由")
-
-
-# Agent 2.2: 品类维度评估专家
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    category_score: float = Field(..., description="品类维度得分 -1~1")
-    reason: str = Field(..., description="品类评估理由")
-
-motivation_evaluation_instructions = """
-# 角色定义
-你是 **动机维度评估专家**。你的任务是:评估 <平台sug词条> 与 <原始问题> 的**动机匹配度**,给出 **-1 到 1 之间** 的数值评分。
-
-## 核心任务
-评估对象:<平台sug词条> 与 <原始问题> 的需求动机匹配度
-核心要素:**动词** - 获取、学习、拍摄、制作、寻找等
-
-## 如何识别核心动机
-
-**核心动机必须是动词**:
-
-### 方法1: 显性动词直接提取
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-"川西秋天风光摄影" → 隐含动作="拍摄"
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 得分 = 0
-示例:
-"摄影" → 无法识别动机,得分=0
-"川西风光" → 无法识别动机,得分=0
-
-## 评分标准
-
-【正向匹配】
-+1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: sug词的核心动作是原始问题动作的具体化子集,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"
-
-+0.8~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"素材下载教程"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
-## 输出
-- motivation_score: -1到1的动机得分
-- reason: 详细评估理由(说明核心动作识别和匹配情况)
-""".strip()
-
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-)
-
-
-category_evaluation_instructions = """
-# 角色定义
-你是 **品类维度评估专家**。你的任务是:评估 <平台sug词条> 与 <原始问题> 的**品类匹配度**,给出 **-1 到 1 之间** 的数值评分。
-
-## 核心任务
-评估对象:<平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-核心要素:**名词+限定词** - 川西、秋季、风光摄影、素材
-
-## 评分标准
-
-【正向匹配】
-+1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,大部分限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.7: 核心主体匹配,少量限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.45: 仅主体词匹配,限定词全部缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.15: 主题领域相关但品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 主体词部分相关但类别明显不同
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-## 输出
-- category_score: -1到1的品类得分
-- reason: 详细评估理由(说明主体词和限定词匹配情况)
-""".strip()
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-)
-
-
-# Agent 3: 加词选择专家
-class WordSelection(BaseModel):
-    """加词选择结果"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-word_selection_instructions = """
-你是加词选择专家。
-
-## 任务
-从候选词列表中选择一个最合适的词,与当前seed组合成新的query。
-
-## 原则
-1. 选择与当前seed最相关的词
-2. 组合后的query要语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-## 输出
-- selected_word: 选中的词
-- combined_query: 组合后的新query
-- reasoning: 选择理由
-""".strip()
-
-word_selector = Agent[None](
-    name="加词选择专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelection,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id", ""),
-        title=note_card.get("display_title", ""),
-        body_text=note_card.get("desc", ""),
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-def apply_score_rules(base_score: float, motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则调整得分
-
-    Args:
-        base_score: 基础加权得分 (motivation*0.7 + category*0.3)
-        motivation_score: 动机维度得分
-        category_score: 品类维度得分
-
-    Returns:
-        调整后的最终得分
-    """
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.55)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.4)
-
-    # 规则C: 动机负向决定机制
-    if motivation_score < 0:
-        # 动作意图冲突时,推荐具有误导性,不应为正相关
-        return min(base_score, 0)
-
-    # 无规则调整
-    return base_score
-
-
-async def evaluate_with_o(text: str, o: str) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 并发调用两个评估器
-    motivation_task = Runner.run(motivation_evaluator, eval_input)
-    category_task = Runner.run(category_evaluator, eval_input)
-
-    motivation_result, category_result = await asyncio.gather(
-        motivation_task,
-        category_task
-    )
-
-    # 获取分维度评估结果
-    motivation_eval: MotivationEvaluation = motivation_result.final_output
-    category_eval: CategoryEvaluation = category_result.final_output
-
-    # 计算基础加权得分
-    base_score = motivation_eval.motivation_score * 0.7 + category_eval.category_score * 0.3
-
-    # 应用规则调整
-    final_score = apply_score_rules(
-        base_score,
-        motivation_eval.motivation_score,
-        category_eval.category_score
-    )
-
-    # 组合评估理由
-    combined_reason = (
-        f"【动机维度 {motivation_eval.motivation_score:.2f}】{motivation_eval.reason}\n"
-        f"【品类维度 {category_eval.category_score:.2f}】{category_eval.reason}\n"
-        f"【基础得分 {base_score:.2f}】动机*0.7 + 品类*0.3\n"
-        f"【最终得分 {final_score:.2f}】"
-    )
-
-    # 如果应用了规则,添加规则说明
-    if final_score != base_score:
-        if motivation_eval.motivation_score >= 0.8 and final_score > base_score:
-            combined_reason += "(应用规则A:动机高分保护)"
-        elif motivation_eval.motivation_score <= 0.2 and final_score < base_score:
-            combined_reason += "(应用规则B:动机低分限制)"
-        elif motivation_eval.motivation_score < 0 and final_score < base_score:
-            combined_reason += "(应用规则C:动机负向决定)"
-
-    return final_score, combined_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o)
-        return seg
-
-    if seg_list:
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1
-    print(f"\n[步骤3] 构建word_list_1...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1: {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Word], list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list: 当前的word列表
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (word_list_next, q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "input_word_list_size": len(word_list),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug
-    async def evaluate_sug(sug: Sug) -> Sug:
-        sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o)
-        return sug
-
-    if all_sugs:
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建word_list_next: word_list -> word_list_next(先直接复制)
-    print(f"\n[步骤4] 构建word_list_next(暂时直接复制)...")
-    word_list_next = word_list.copy()
-
-    # 5. 构建q_list_next
-    print(f"\n[步骤5] 构建q_list_next...")
-    q_list_next = []
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-
-    # 5.1 对于seed_list中的每个seed,从word_list_next中选一个未加过的词
-    print(f"\n  5.1 为每个seed加词...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 简单过滤:找出不在seed.text中且未被添加过的词
-        candidate_words = []
-        for word in word_list_next:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词: {[w.text for w in candidate_words]}")
-
-        # 使用Agent选择最合适的词
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{', '.join([w.text for w in candidate_words])}
-</候选词列表>
-
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-        result = await Runner.run(word_selector, selection_input)
-        selection: WordSelection = result.final_output
-
-        # 验证选择的词是否在候选列表中
-        if selection.selected_word not in [w.text for w in candidate_words]:
-            print(f"      ✗ Agent选择的词 '{selection.selected_word}' 不在候选列表中,跳过")
-            continue
-
-        print(f"      ✓ 选择词: {selection.selected_word}")
-        print(f"      ✓ 新query: {selection.combined_query}")
-        print(f"      理由: {selection.reasoning}")
-
-        # 评估新query
-        new_q_score, new_q_reason = await evaluate_with_o(selection.combined_query, o)
-        print(f"      新query评分: {new_q_score:.2f}")
-
-        # 创建新的q
-        new_q = Q(
-            text=selection.combined_query,
-            score_with_o=new_q_score,
-            reason=new_q_reason,
-            from_source="add"
-        )
-        q_list_next.append(new_q)
-
-        # 更新seed的added_words
-        seed.added_words.append(selection.selected_word)
-
-        # 保存到add_word_details
-        if seed.text not in add_word_details:
-            add_word_details[seed.text] = []
-        add_word_details[seed.text].append({
-            "text": selection.combined_query,
-            "score": new_q_score,
-            "reason": new_q_reason,
-            "selected_word": selection.selected_word
-        })
-
-    # 5.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next
-    print(f"\n  5.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 6. 更新seed_list
-    print(f"\n[步骤6] 更新seed_list...")
-    seed_list_next = seed_list.copy()  # 保留原有的seed
-
-    # 对于sug_list_list中,每个sug分数大于来源query分数的,且没在seed_list中出现过的,加入
-    existing_seed_texts = {seed.text for seed in seed_list_next}
-    for sug in all_sugs:
-        # 新逻辑:sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"  ✓ 新seed: {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "word_list_next_size": len(word_list_next),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],  # 下一轮种子列表
-        "sug_details": sug_details,  # 每个Q对应的sug列表
-        "add_word_details": add_word_details,  # 每个seed对应的组合词列表
-        "search_results": search_results_data  # 搜索结果(包含帖子详情)
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  seed数量: {len(seed_list_next)}")
-
-    return word_list_next, q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        word_list, q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list=word_list,
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 执行迭代
-    all_search_list = await iterative_loop(
-        run_context,
-        max_rounds=max_rounds,
-        sug_threshold=sug_threshold
-    )
-
-    # 格式化输出
-    output = f"原始需求:{run_context.c}\n"
-    output += f"原始问题:{run_context.o}\n"
-    output += f"总搜索次数:{len(all_search_list)}\n"
-    output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-    output += "\n" + "="*60 + "\n"
-
-    if all_search_list:
-        output += "【搜索结果】\n\n"
-        for idx, search in enumerate(all_search_list, 1):
-            output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-            output += f"   帖子数: {len(search.post_list)}\n"
-            if search.post_list:
-                for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                    output += f"   {post_idx}) {post.title}\n"
-                    output += f"      URL: {post.note_url}\n"
-            output += "\n"
-    else:
-        output += "未找到搜索结果\n"
-
-    run_context.final_output = output
-
-    print(f"\n{'='*60}")
-    print("最终结果")
-    print(f"{'='*60}")
-    print(output)
-
-    # 保存日志
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-    context_dict = run_context.model_dump()
-    with open(context_file_path, "w", encoding="utf-8") as f:
-        json.dump(context_dict, f, ensure_ascii=False, indent=2)
-    print(f"\nRunContext saved to: {context_file_path}")
-
-    # 保存详细的搜索结果
-    search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-    search_results_data = [s.model_dump() for s in all_search_list]
-    with open(search_results_path, "w", encoding="utf-8") as f:
-        json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-    print(f"Search results saved to: {search_results_path}")
-
-    # 可视化
-    if visualize:
-        import subprocess
-        output_html = os.path.join(run_context.log_dir, "visualization.html")
-        print(f"\n🎨 生成可视化HTML...")
-
-        # 获取绝对路径
-        abs_context_file = os.path.abspath(context_file_path)
-        abs_output_html = os.path.abspath(output_html)
-
-        # 运行可视化脚本
-        result = subprocess.run([
-            "node",
-            "visualization/sug_v6_1_2_8/index.js",
-            abs_context_file,
-            abs_output_html
-        ])
-
-        if result.returncode == 0:
-            print(f"✅ 可视化已生成: {output_html}")
-        else:
-            print(f"❌ 可视化生成失败")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.8 轮次迭代版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 2"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1103
sug_v6_1_2_114.py

@@ -1,1103 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 相关度评估专家
-class RelevanceEvaluation(BaseModel):
-    """相关度评估"""
-    reason: str = Field(..., description="评估理由")
-    relevance_score: float = Field(..., description="相关性分数 -1~1")
-
-relevance_evaluation_instructions = """
-# 角色定义
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的相关度满足度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 两大评估维度
-本评估系统始终围绕 **两个核心维度** 进行:
-
-### 1. 动机维度(权重70%)
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 2. 品类维度(权重30%)
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 初始权重 = 0
-→ 相关度评估以品类匹配为主
-示例:
-"摄影" → 无法识别动机,初始权重=0
-"川西风光" → 无法识别动机,初始权重=0
-
-
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【综合相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的相关度
-    └→ 输出: -1到1之间的数值 + 分维度得分 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估(权重70%)
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.8~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"素材下载教程"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
-维度2: 品类维度评估(权重30%)
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,大部分限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.7: 核心主体匹配,少量限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.45: 仅主体词匹配,限定词全部缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.15: 主题领域相关但品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 主体词部分相关但类别明显不同
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
-综合得分计算与规则调整
-步骤1: 应用依存性规则
-规则A: 动机高分保护机制
-
-如果 动机维度得分 ≥ 0.8:
-   → 品类得分即使为0或轻微负向(-0.2~0)
-   → 最终得分 = max(初步得分, 0.55)
-
-解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-规则B: 动机低分限制机制
-如果 动机维度得分 ≤ 0.2:
-   → 无论品类得分多高
-   → 最终得分 = min(初步得分, 0.4)
-
-解释: 目的不符时,品类匹配的价值有限
-
-规则C: 动机负向决定机制
-如果 动机维度得分 < 0:
-   → 最终得分 = min(初步得分, 0)
-
-解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-步骤3: 输出最终得分
-
-#基础加权计算
-应用规则后的调整得分 = 目的动机维度得分 × 0.7 + 品类维度得分 × 0.3
-取值范围: -1.0 ~ +1.0
-
----
-
-# 得分档位解释
-
-高度相关】+0.8 ~ +1.0
-相关性高度契合,用户可直接使用
-动机和品类均高度匹配
-典型场景: 动机≥0.85 且 品类≥0.7
-【中高相关】+0.6 ~ +0.79
-相关性较好,用户基本满意
-动机匹配但品类有泛化,或反之
-典型场景: 动机≥0.8 且 品类≥0.3
-【中度相关】+0.3 ~ +0.59
-部分相关,用户需要调整搜索策略
-动机或品类存在一定偏差
-典型场景: 动机0.4-0.7 且 品类0.3-0.7
-【弱相关】+0.01 ~ +0.29
-关联微弱,参考价值有限
-仅有表层词汇重叠
-【无关】0
-无明确关联
-原始问题无法识别动机 且 sug词无明确动作
-没有目的性且没有品类匹配
-【轻度负向】-0.29 ~ -0.01
-产生轻微误导或干扰
-【中度负向】-0.69 ~ -0.3
-存在明显冲突或误导
-【严重负向】-1.0 ~ -0.7
-完全违背意图或产生有害引导
-
----
-
-# 输出要求
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-
-#注意事项:
-始终围绕两个核心维度:所有评估都基于"动机"和"品类"两个维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题无明确关联,既不相关也不冲突时给予零分
-分维度独立评分:
-先提取原始问题核心动机
-分别计算动机维度(含两个子维度)和品类维度得分
-按70:30加权得到初步得分
-应用规则调整得到最终得分
-动机优先原则:当动机高度一致时,品类的合理泛化或具体化不应导致低评分
-技巧类需求特殊对待:包含"技巧/方法/教程"等词的需求,对动作一致性要求更严格
-
-## 输出格式(严格遵守)
-
-必须输出标准 JSON 格式,包含以下两个字段:
-
-```json
-{
-  "reason": "详细的评估理由说明",
-  "relevance_score": 0.85
-}
-```
-
-### 字段说明
-- **reason** (string): 详细的评估理由,说明动机维度和品类维度的匹配情况
-- **relevance_score** (number): -1.0 到 1.0 之间的数值,保留2位小数
-
-### 重要约束
-1. **reason 字段内容规则**:
-   - 禁止使用英文双引号 `"`,必须使用替代符号
-   - 推荐使用中文书名号:《》或【】
-   - 或使用中文引号:「」或『』
-   - 示例:使用"核心动机是《制作攻略图》"而不是"核心动机是"制作攻略图""
-
-2. **JSON 格式要求**:
-   - 必须是合法的 JSON 格式
-   - relevance_score 必须是数字类型,不能是字符串
-   - 字段名必须用双引号包裹
-
-### 输出示例
-
-✅ **正确示例**:
-```json
-{
-  "reason": "原始问题的核心动机是《制作攻略图》,包含【拼接】图片等操作。sug词条【拼接】与原始问题中的具体操作完全一致,动机维度匹配度为中等。品类维度相关性较弱。",
-  "relevance_score": 0.22
-}
-```
-
-❌ **错误示例**(会导致解析失败):
-```json
-{
-  "reason": "原始问题的核心动机是"制作攻略图",包含"拼接"图片",
-  "relevance_score": "0.22"
-}
-```
-错误原因:reason 中使用了未转义的英文双引号,relevance_score 是字符串而非数字
-""".strip()
-
-relevance_evaluator = Agent[None](
-    name="相关度评估专家",
-    instructions=relevance_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=RelevanceEvaluation,
-)
-
-
-# Agent 3: 加词选择专家
-class WordSelection(BaseModel):
-    """加词选择结果"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-word_selection_instructions = """
-你是加词选择专家。
-
-## 任务
-从候选词列表中选择一个最合适的词,与当前seed组合成新的query。
-
-## 原则
-1. 选择与当前seed最相关的词
-2. 组合后的query要语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-## 输出
-- selected_word: 选中的词
-- combined_query: 组合后的新query
-- reasoning: 选择理由
-""".strip()
-
-word_selector = Agent[None](
-    name="加词选择专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelection,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id", ""),
-        title=note_card.get("display_title", ""),
-        body_text=note_card.get("desc", ""),
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    Returns:
-        tuple[float, str]: (相关度分数, 评估理由)
-    """
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前文本>
-{text}
-</当前文本>
-
-请评估当前文本与原始问题的相关度。
-"""
-    result = await Runner.run(relevance_evaluator, eval_input)
-    evaluation: RelevanceEvaluation = result.final_output
-    return evaluation.relevance_score, evaluation.reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o)
-        return seg
-
-    if seg_list:
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1
-    print(f"\n[步骤3] 构建word_list_1...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1: {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Word], list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list: 当前的word列表
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (word_list_next, q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "input_word_list_size": len(word_list),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug
-    async def evaluate_sug(sug: Sug) -> Sug:
-        sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o)
-        return sug
-
-    if all_sugs:
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建word_list_next: word_list -> word_list_next(先直接复制)
-    print(f"\n[步骤4] 构建word_list_next(暂时直接复制)...")
-    word_list_next = word_list.copy()
-
-    # 5. 构建q_list_next
-    print(f"\n[步骤5] 构建q_list_next...")
-    q_list_next = []
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-
-    # 5.1 对于seed_list中的每个seed,从word_list_next中选一个未加过的词
-    print(f"\n  5.1 为每个seed加词...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 简单过滤:找出不在seed.text中且未被添加过的词
-        candidate_words = []
-        for word in word_list_next:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词: {[w.text for w in candidate_words]}")
-
-        # 使用Agent选择最合适的词
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{', '.join([w.text for w in candidate_words])}
-</候选词列表>
-
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-        result = await Runner.run(word_selector, selection_input)
-        selection: WordSelection = result.final_output
-
-        # 验证选择的词是否在候选列表中
-        if selection.selected_word not in [w.text for w in candidate_words]:
-            print(f"      ✗ Agent选择的词 '{selection.selected_word}' 不在候选列表中,跳过")
-            continue
-
-        print(f"      ✓ 选择词: {selection.selected_word}")
-        print(f"      ✓ 新query: {selection.combined_query}")
-        print(f"      理由: {selection.reasoning}")
-
-        # 评估新query
-        new_q_score, new_q_reason = await evaluate_with_o(selection.combined_query, o)
-        print(f"      新query评分: {new_q_score:.2f}")
-
-        # 创建新的q
-        new_q = Q(
-            text=selection.combined_query,
-            score_with_o=new_q_score,
-            reason=new_q_reason,
-            from_source="add"
-        )
-        q_list_next.append(new_q)
-
-        # 更新seed的added_words
-        seed.added_words.append(selection.selected_word)
-
-        # 保存到add_word_details
-        if seed.text not in add_word_details:
-            add_word_details[seed.text] = []
-        add_word_details[seed.text].append({
-            "text": selection.combined_query,
-            "score": new_q_score,
-            "reason": new_q_reason,
-            "selected_word": selection.selected_word
-        })
-
-    # 5.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next
-    print(f"\n  5.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 6. 更新seed_list
-    print(f"\n[步骤6] 更新seed_list...")
-    seed_list_next = seed_list.copy()  # 保留原有的seed
-
-    # 对于sug_list_list中,每个sug分数大于来源query分数的,且没在seed_list中出现过的,加入
-    existing_seed_texts = {seed.text for seed in seed_list_next}
-    for sug in all_sugs:
-        # 新逻辑:sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"  ✓ 新seed: {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "word_list_next_size": len(word_list_next),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],  # 下一轮种子列表
-        "sug_details": sug_details,  # 每个Q对应的sug列表
-        "add_word_details": add_word_details,  # 每个seed对应的组合词列表
-        "search_results": search_results_data  # 搜索结果(包含帖子详情)
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  seed数量: {len(seed_list_next)}")
-
-    return word_list_next, q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        word_list, q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list=word_list,
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 执行迭代
-    all_search_list = await iterative_loop(
-        run_context,
-        max_rounds=max_rounds,
-        sug_threshold=sug_threshold
-    )
-
-    # 格式化输出
-    output = f"原始需求:{run_context.c}\n"
-    output += f"原始问题:{run_context.o}\n"
-    output += f"总搜索次数:{len(all_search_list)}\n"
-    output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-    output += "\n" + "="*60 + "\n"
-
-    if all_search_list:
-        output += "【搜索结果】\n\n"
-        for idx, search in enumerate(all_search_list, 1):
-            output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-            output += f"   帖子数: {len(search.post_list)}\n"
-            if search.post_list:
-                for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                    output += f"   {post_idx}) {post.title}\n"
-                    output += f"      URL: {post.note_url}\n"
-            output += "\n"
-    else:
-        output += "未找到搜索结果\n"
-
-    run_context.final_output = output
-
-    print(f"\n{'='*60}")
-    print("最终结果")
-    print(f"{'='*60}")
-    print(output)
-
-    # 保存日志
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-    context_dict = run_context.model_dump()
-    with open(context_file_path, "w", encoding="utf-8") as f:
-        json.dump(context_dict, f, ensure_ascii=False, indent=2)
-    print(f"\nRunContext saved to: {context_file_path}")
-
-    # 保存详细的搜索结果
-    search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-    search_results_data = [s.model_dump() for s in all_search_list]
-    with open(search_results_path, "w", encoding="utf-8") as f:
-        json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-    print(f"Search results saved to: {search_results_path}")
-
-    # 可视化
-    if visualize:
-        import subprocess
-        output_html = os.path.join(run_context.log_dir, "visualization.html")
-        print(f"\n🎨 生成可视化HTML...")
-
-        # 获取绝对路径
-        abs_context_file = os.path.abspath(context_file_path)
-        abs_output_html = os.path.abspath(output_html)
-
-        # 运行可视化脚本
-        result = subprocess.run([
-            "node",
-            "visualization/sug_v6_1_2_8/index.js",
-            abs_context_file,
-            abs_output_html
-        ])
-
-        if result.returncode == 0:
-            print(f"✅ 可视化已生成: {output_html}")
-        else:
-            print(f"❌ 可视化生成失败")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.8 轮次迭代版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 2"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1459
sug_v6_1_2_115.py

@@ -1,1459 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-### 2. 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.5: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词不匹配,品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建两个评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:只有得分大于种子得分的组合词才加入下一轮
-            if comb['score'] <= seed.score_with_o:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed.score_with_o:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:只有得分大于种子得分的组合词才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] <= seed_score:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed_score:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed_score:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1645
sug_v6_1_2_116.py

@@ -1,1645 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元,目标是通过分词的不断递进组合来检索到问题来源问题的相关知识
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-如果是双标行为,单独分词 不拆分,如果有如何两个字 不要
-
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt - 第一轮版本(来自 sug_v6_1_2_115.py)
-motivation_evaluation_instructions_round1 = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 动机评估 prompt - 后续轮次版本(当前 116 版本)
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 动机评估核心原则(必读)
-
-### 动机 = 动作 + 对象 + 场景
-评估时必须同时考虑三要素,不能只看动词:
-- **动作**:制定、规划、获取、拍摄等
-- **对象**:旅行行程 vs 每日计划、风光照片 vs 证件照
-- **场景**:旅游 vs 日常、摄影 vs 办公
-
-### 关键判断:动词相同 ≠ 动机匹配
-
-错误:只看动词相同就给高分
-- "制定旅行行程" vs "制定每日计划" → 给0.95 错误
-- "拍摄风光" vs "拍摄证件照" → 给0.95 错误
-
-正确:检查对象和场景是否匹配
-- 对象不同领域 → 降至0.3左右
-- 场景不同 → 降至0.3左右
-
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-# 维度独立性警告
-【严格约束】本评估**只评估动机维度**:
-**禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 动作+对象+场景完全一致
-  - 要求:动词、对象、场景都必须匹配,不能只看动词
-  - "制定旅行行程" vs "制定每日计划"
-     虽然动词相同,但对象和场景完全不同,不属于高分
-  - 特殊规则: 如果sug词的核心动作是原始问题动作在动作+对象+场景一致下的**具体化子集**,也判定为完全一致
-
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.25~0.4: 动词相同但对象或场景明显不同(弱相关)
-  - 判断要点:动词一致,但对象不同领域或场景不同
-  - 关键:不要因为动词相同就给0.95,必须检查对象!
-
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0
-
- 特别注意 - 禁止的错误理由:
-  - 禁止: "虽然没有动作,但主题相关,所以给0.2"
-  - 禁止:"内容有参考价值,所以给0.15"
-  - 禁止: "都提到了XX(名词),所以不是完全无关"
-  - 正确理由:"sug词条无动作意图,与原始问题的'XX'动机完全无关"
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-  
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例: 
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
----
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建评估 Agent - 第一轮使用
-motivation_evaluator_round1 = Agent[None](
-    name="动机维度评估专家(第一轮)",
-    instructions=motivation_evaluation_instructions_round1,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-# 创建评估 Agent - 后续轮次使用
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None, round_num: int = 1) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-        round_num: 轮次编号,第一轮使用 round1 prompt,后续使用标准 prompt
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 根据轮次选择不同的 motivation evaluator
-            # 第一轮使用 round1 版本,后续使用标准版本
-            selected_motivation_evaluator = motivation_evaluator_round1 if round_num == 1 else motivation_evaluator
-
-            # 并发调用两个评估器
-            motivation_task = Runner.run(selected_motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:只有得分大于种子得分的组合词才加入下一轮
-            if comb['score'] <= seed.score_with_o:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed.score_with_o:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:只有得分大于种子得分的组合词才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] <= seed_score:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed_score:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed_score:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.5, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1464
sug_v6_1_2_117.py

@@ -1,1464 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-如果是双标行为,单独分词 不拆分,如果有如何两个字 不要
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- "方法论"类问题,如果是方法/工具/教程/步骤等方法论信息(如:方法、软件、平台、教程、技巧),则核心动作相同,同属于此定义
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-### 2. 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.5: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词不匹配,品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
--0.5~-0.25: 主体具象化偏离
-  - 例: 原始问题"表情包" vs sug词"表情包gif"
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-
-""".strip()
-
-# 创建两个评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:只有得分大于种子得分的组合词才加入下一轮
-            if comb['score'] <= seed.score_with_o:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed.score_with_o:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:只有得分大于种子得分的组合词才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] <= seed_score:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed_score:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed_score:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.5, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1508
sug_v6_1_2_118.py

@@ -1,1508 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-如果是双标行为,单独分词 不拆分,如果有如何两个字 不要
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- "方法论"类问题,如果是方法/工具/教程/步骤等方法论信息(如:方法、软件、平台、教程、技巧),则核心动作相同,同属于此定义
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-### 2. 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.5: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词不匹配,品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
--0.5~-0.25: 主体具象化偏离
-  - 例: 原始问题"表情包" vs sug词"表情包gif"
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-
-""".strip()
-
-# 创建两个评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(第2轮及以后生效)
-    pruned_query_texts = set()
-    if round_num >= 2:
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 第{round_num}轮不应用剪枝策略")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:只有得分大于种子得分的组合词才加入下一轮
-            if comb['score'] <= seed.score_with_o:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed.score_with_o:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:只有得分大于种子得分的组合词才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] <= seed_score:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} ≤ 种子{seed_score:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed_score:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1690
sug_v6_1_2_119.py

@@ -1,1690 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.05
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt - 第一轮版本(来自 sug_v6_1_2_115.py)
-motivation_evaluation_instructions_round1 = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 动机评估 prompt - 后续轮次版本(当前 116 版本)
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 动机评估核心原则(必读)
-
-### 动机 = 动作 + 对象 + 场景
-评估时必须同时考虑三要素,不能只看动词:
-- **动作**:制定、规划、获取、拍摄等
-- **对象**:旅行行程 vs 每日计划、风光照片 vs 证件照
-- **场景**:旅游 vs 日常、摄影 vs 办公
-
-### 关键判断:动词相同 ≠ 动机匹配
-
-错误:只看动词相同就给高分
-- "制定旅行行程" vs "制定每日计划" → 给0.95 错误
-- "拍摄风光" vs "拍摄证件照" → 给0.95 错误
-
-正确:检查对象和场景是否匹配
-- 对象不同领域 → 降至0.3左右
-- 场景不同 → 降至0.3左右
-
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-# 维度独立性警告
-【严格约束】本评估**只评估动机维度**:
-**禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 动作+对象+场景完全一致
-  - 要求:动词、对象、场景都必须匹配,不能只看动词
-  - "制定旅行行程" vs "制定每日计划"
-     虽然动词相同,但对象和场景完全不同,不属于高分
-  - 特殊规则: 如果sug词的核心动作是原始问题动作在动作+对象+场景一致下的**具体化子集**,也判定为完全一致
-
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.25~0.4: 动词相同但对象或场景明显不同(弱相关)
-  - 判断要点:动词一致,但对象不同领域或场景不同
-  - 关键:不要因为动词相同就给0.95,必须检查对象!
-
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0
-
- 特别注意 - 禁止的错误理由:
-  - 禁止: "虽然没有动作,但主题相关,所以给0.2"
-  - 禁止:"内容有参考价值,所以给0.15"
-  - 禁止: "都提到了XX(名词),所以不是完全无关"
-  - 正确理由:"sug词条无动作意图,与原始问题的'XX'动机完全无关"
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
----
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建评估 Agent - 第一轮使用
-motivation_evaluator_round1 = Agent[None](
-    name="动机维度评估专家(第一轮)",
-    instructions=motivation_evaluation_instructions_round1,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-# 创建评估 Agent - 后续轮次使用
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None, round_num: int = 1) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-        round_num: 轮次编号,第一轮使用 round1 prompt,后续使用标准 prompt
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 根据轮次选择不同的 motivation evaluator
-            # 第一轮使用 round1 版本,后续使用标准版本
-            selected_motivation_evaluator = motivation_evaluator_round1 if round_num == 1 else motivation_evaluator
-
-            # 并发调用两个评估器
-            motivation_task = Runner.run(selected_motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(第2轮及以后生效)
-    pruned_query_texts = set()
-    if round_num >= 2:
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 第{round_num}轮不应用剪枝策略")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 1690
sug_v6_1_2_120.py

@@ -1,1690 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt - 第一轮版本(来自 sug_v6_1_2_115.py)
-motivation_evaluation_instructions_round1 = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 动机评估 prompt - 后续轮次版本(当前 116 版本)
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 动机评估核心原则(必读)
-
-### 动机 = 动作 + 对象 + 场景
-评估时必须同时考虑三要素,不能只看动词:
-- **动作**:制定、规划、获取、拍摄等
-- **对象**:旅行行程 vs 每日计划、风光照片 vs 证件照
-- **场景**:旅游 vs 日常、摄影 vs 办公
-
-### 关键判断:动词相同 ≠ 动机匹配
-
-错误:只看动词相同就给高分
-- "制定旅行行程" vs "制定每日计划" → 给0.95 错误
-- "拍摄风光" vs "拍摄证件照" → 给0.95 错误
-
-正确:检查对象和场景是否匹配
-- 对象不同领域 → 降至0.3左右
-- 场景不同 → 降至0.3左右
-
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-# 维度独立性警告
-【严格约束】本评估**只评估动机维度**:
-**禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 动作+对象+场景完全一致
-  - 要求:动词、对象、场景都必须匹配,不能只看动词
-  - "制定旅行行程" vs "制定每日计划"
-     虽然动词相同,但对象和场景完全不同,不属于高分
-  - 特殊规则: 如果sug词的核心动作是原始问题动作在动作+对象+场景一致下的**具体化子集**,也判定为完全一致
-
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.25~0.4: 动词相同但对象或场景明显不同(弱相关)
-  - 判断要点:动词一致,但对象不同领域或场景不同
-  - 关键:不要因为动词相同就给0.95,必须检查对象!
-
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0
-
- 特别注意 - 禁止的错误理由:
-  - 禁止: "虽然没有动作,但主题相关,所以给0.2"
-  - 禁止:"内容有参考价值,所以给0.15"
-  - 禁止: "都提到了XX(名词),所以不是完全无关"
-  - 正确理由:"sug词条无动作意图,与原始问题的'XX'动机完全无关"
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
----
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建评估 Agent - 第一轮使用
-motivation_evaluator_round1 = Agent[None](
-    name="动机维度评估专家(第一轮)",
-    instructions=motivation_evaluation_instructions_round1,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-# 创建评估 Agent - 后续轮次使用
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-
-# Agent 3: 加词选择专家
-class WordCombination(BaseModel):
-    """单个词组合"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-class WordSelectionTop5(BaseModel):
-    """加词选择结果(Top 5)"""
-    combinations: list[WordCombination] = Field(
-        ...,
-        description="选择的Top 5组合(不足5个则返回所有)",
-        min_items=1,
-        max_items=5
-    )
-    overall_reasoning: str = Field(..., description="整体选择思路")
-
-word_selection_instructions = """
-你是加词组合专家。
-
-## 任务
-从候选词列表中选择5个最合适的词,分别与当前seed组合成新的query。如果候选词不足5个,则返回所有。
-
-## 选择原则
-1. **相关性**:选择与当前seed最相关的词
-2. **语义通顺**:组合后的query要符合搜索习惯
-3. **扩展范围**:优先选择能扩展搜索范围的词
-4. **多样性**:5个词应该覆盖不同的方面(如:时间、地点、类型、用途等)
-
-## 组合约束(严格执行)
-**CRITICAL: 以下约束必须100%遵守,违反任何一条都是错误**
-
-1. **必须完整保留seed的所有文本内容**
-   - seed的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换seed中的任何部分
-   - 即使某些字看起来不重要,也必须保留
-
-2. **必须完整保留word的所有文本内容**
-   - word的每一个字都必须出现在组合结果中
-   - 禁止删除、省略、替换word中的任何部分
-
-3. **禁止添加任何额外内容**
-   - 不能添加连接词(如"的"、"和"、"与"、"在"等)
-   - 不能添加任何其他词或字符
-
-4. **组合方式仅限以下三种**
-   - seed在前:seed的文本 + word的文本(如:制作梗图 + 猫咪 = 制作梗图猫咪)
-   - word在前:word的文本 + seed的文本(如:猫咪 + 制作梗图 = 猫咪制作梗图)
-   - word插入:将word插入seed中间合适位置(如:制作 + 猫咪 + 梗图 = 制作猫咪梗图)
-
-5. **验证检查清单**(在输出前必须自查)
-   ☑ 组合结果包含seed的所有字符?
-   ☑ 组合结果包含word的所有字符?
-   ☑ 组合结果没有额外的字符?
-   ☑ 只使用了三种组合方式之一?
-
-## 正确示例(必须参考)
-✓ seed="制作梗图" + word="猫咪" → "制作梗图猫咪"(seed在前)
-✓ seed="制作梗图" + word="猫咪" → "猫咪制作梗图"(word在前)
-✓ seed="制作梗图" + word="猫咪" → "制作猫咪梗图"(word插入中间)
-✓ seed="川西" + word="秋季" → "川西秋季"(seed在前)
-✓ seed="川西" + word="秋季" → "秋季川西"(word在前)
-✓ seed="摄影" + word="技巧" → "摄影技巧"(seed在前)
-✓ seed="摄影" + word="技巧" → "技巧摄影"(word在前)
-
-## 错误示例(严禁出现)
-✗ seed="制作梗图" + word="猫咪" → "猫咪梗图"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "梗图猫咪"(❌ 缺少"制作")
-✗ seed="制作梗图" + word="猫咪" → "制作猫咪表情包"(❌ 加了"表情包")
-✗ seed="川西" + word="秋季" → "川西的秋季"(❌ 加了"的")
-✗ seed="川西" + word="秋季" → "川西秋季风光"(❌ 加了"风光")
-✗ seed="摄影" + word="技巧" → "摄影拍摄技巧"(❌ 加了"拍摄")
-✗ seed="摄影" + word="技巧" → "影技巧"(❌ 缺少"摄")
-
-## 输出要求
-- 最多返回5个组合(如果候选词不足5个,返回所有)
-- 每个组合包含:
-  * selected_word: 选择的词(必须在候选词列表中)
-  * combined_query: 组合后的新query(只包含seed和word的原始文本,不多不少)
-  * reasoning: 选择理由(说明为什么选这个词)
-- overall_reasoning: 整体选择思路(说明这5个词的选择逻辑)
-
-## JSON输出规范
-1. **格式要求**:必须输出标准的、完整的JSON格式
-2. **字符限制**:不要在JSON中使用任何不可见的特殊字符或控制字符
-3. **引号规范**:字符串中如需表达引用或强调,使用书名号《》或单书名号「」,不要使用英文引号或中文引号""
-4. **编码规范**:所有文本使用UTF-8编码,不要包含二进制或转义序列
-5. **完整性**:确保JSON的开始和结束括号完整匹配,所有字段都正确闭合
-""".strip()
-
-word_selector = Agent[None](
-    name="加词组合专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelectionTop5,
-    model_settings=ModelSettings(temperature=0.2),
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None, round_num: int = 1) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-        round_num: 轮次编号,第一轮使用 round1 prompt,后续使用标准 prompt
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 根据轮次选择不同的 motivation evaluator
-            # 第一轮使用 round1 版本,后续使用标准版本
-            selected_motivation_evaluator = motivation_evaluator_round1 if round_num == 1 else motivation_evaluator
-
-            # 并发调用两个评估器
-            motivation_task = Runner.run(selected_motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 5
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代
-        all_search_list = await iterative_loop(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_8/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.115 广度遍历版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 2367
sug_v6_1_2_121.py

@@ -1,2367 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.05
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. 疑问引导:引导查询意图的元素,如疑问词(原理:表示意图类型,如过程求解或信息查询)。
-2. 核心动作:核心动作或关系谓词,如动词(原理:谓词是语义框架的核心,定义动作或状态)。
-3. 目标对象:动作的目标或实体中心对象,如名词短语(承载谓词的作用对象助词)。
-4. 修饰限定:对目标对象的修饰和限定、对核心动作的限定。
-
-## 分段原则:严格遵守以下规则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-5. **修饰限定合并规则**
-- 定义:在同一个"目标对象"之前的所有"修饰限定"片段,如果它们之间没有插入"疑问引导"、"核心动作"或"目标对象",就必须合并为一个片段
-- 判断标准:
-* 步骤1:找到"目标对象"在哪里
-* 步骤2:向前查看,把所有修饰和限定这个目标对象的词都合并,修辞和限定词包括数量词、地域词、时间词、描述词、程度词、方式词、助词等
-
-## 输出要求
-
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt - 第一轮版本(来自 sug_v6_1_2_115.py)
-motivation_evaluation_instructions_round1 = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 动机评估 prompt - 后续轮次版本(当前 116 版本)
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 动机评估核心原则(必读)
-
-### 动机 = 动作 + 对象 + 场景
-评估时必须同时考虑三要素,不能只看动词:
-- **动作**:制定、规划、获取、拍摄等
-- **对象**:旅行行程 vs 每日计划、风光照片 vs 证件照
-- **场景**:旅游 vs 日常、摄影 vs 办公
-
-### 关键判断:动词相同 ≠ 动机匹配
-
-错误:只看动词相同就给高分
-- "制定旅行行程" vs "制定每日计划" → 给0.95 错误
-- "拍摄风光" vs "拍摄证件照" → 给0.95 错误
-
-正确:检查对象和场景是否匹配
-- 对象不同领域 → 降至0.3左右
-- 场景不同 → 降至0.3左右
-
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-# 维度独立性警告
-【严格约束】本评估**只评估动机维度**:
-**禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 动作+对象+场景完全一致
-  - 要求:动词、对象、场景都必须匹配,不能只看动词
-  - "制定旅行行程" vs "制定每日计划"
-     虽然动词相同,但对象和场景完全不同,不属于高分
-  - 特殊规则: 如果sug词的核心动作是原始问题动作在动作+对象+场景一致下的**具体化子集**,也判定为完全一致
-
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.25~0.4: 动词相同但对象或场景明显不同(弱相关)
-  - 判断要点:动词一致,但对象不同领域或场景不同
-  - 关键:不要因为动词相同就给0.95,必须检查对象!
-
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0
-
- 特别注意 - 禁止的错误理由:
-  - 禁止: "虽然没有动作,但主题相关,所以给0.2"
-  - 禁止:"内容有参考价值,所以给0.15"
-  - 禁止: "都提到了XX(名词),所以不是完全无关"
-  - 正确理由:"sug词条无动作意图,与原始问题的'XX'动机完全无关"
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
----
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(统一使用标准评估策略)
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度
-            segment.score_with_o, segment.reason = await evaluate_with_o(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            comb.score_with_o, comb.reason = await evaluate_with_o(
-                comb.text, o, context.evaluation_cache
-            )
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    print(f"  评估完成,共{len(domain_combinations)}个组合:")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next = []
-
-    # 6.1 添加高分组合
-    high_score_combinations = [comb for comb in domain_combinations if comb.score_with_o > REQUIRED_SCORE_GAIN]
-    for comb in high_score_combinations:
-        # 生成域字符串,如 "D0,D3"
-        domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-
-        q = Q(
-            text=comb.text,
-            score_with_o=comb.score_with_o,
-            reason=comb.reason,
-            from_source="domain_comb",
-            type_label=comb.type_label,
-            domain_type=domains_str  # 添加域信息
-        )
-        q_list_next.append(q)
-
-    print(f"  添加 {len(high_score_combinations)} 个高分组合")
-
-    # 6.2 添加高分SUG(满足增益条件)
-    high_gain_sugs = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            q_list_next.append(q)
-            high_gain_sugs.append(sug)
-
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 > {REQUIRED_SCORE_GAIN})")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [{"text": q.text, "score": q.score_with_o, "type_label": q.type_label, "type": "combination"} for q in q_list_next if q.from_source == "domain_comb"],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next)
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 2756
sug_v6_1_2_122.py

@@ -1,2756 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. **疑问引导**:如何、怎么、什么、哪里等疑问词
-2. **核心动作**:关键动词,如获取、制作、拍摄、寻找等
-3. **修饰短语**:形容词、副词等修饰成分
-4. **中心名词**:核心名词
-5. **逻辑连接**:并且、或者、以及等连接词(较少出现)
-
-## 分段原则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-
-
-## 输出要求
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型(从5种类型中选择)
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-    得分为零的原因: Literal["原始问题无动机", "sug词条无动机", "动机不匹配", "不适用"] = Field(default="不适用", description="当得分为0时的原因分类")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-class ExtensionWordEvaluation(BaseModel):
-    """延伸词评估"""
-    延伸词得分: float = Field(..., ge=-1, le=1, description="延伸词得分 -1~1")
-    简要说明延伸词维度相关度理由: str = Field(..., description="延伸词维度相关度理由")
-
-# 动机评估 prompt(统一版本)
-motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:sug词条是原始问题的部分作用域
-
-当sug词条只包含原始问题的部分作用域时,需要判断:
-1. sug词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-Sug词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或sug词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-## 得分为零的原因(语义判断)
-
-当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
-- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
-- **"sug词条无动机"**:sug词条中不包含任何动作意图
-- **"动机不匹配"**:双方都有动作,但完全无关联
-- **"不适用"**:得分不为零时使用此默认值
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-# 角色
-你是**专业的内容主体评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
-
----
-
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估内容主体维度**:
-- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
-- **完全忽略**:动作、意图、目的
-- **评估重点**:内容本身的主题和属性
-
----
-
-# 作用域与内容主体
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-在Prompt2中:
-- **动机层(动作)完全忽略**
-- **只评估对象层 + 场景层(限定词)**
-
-## 内容主体的构成
-
-**内容主体 = 核心名词 + 限定词**
-
-
----
-
-# 作用域覆盖度评估
-
-## 核心原则:越完整越高分
-
-**完整性公式**:
-```
-作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
-```
-
-**评分影响**:
-- 覆盖度100% → 基础高分(0.9+)
-- 覆盖度50-99% → 中高分(0.6-0.9)
-- 覆盖度<50% → 中低分(0.3-0.6)
-- 覆盖度=0 → 低分或0分
-
----
-
-## 部分作用域的处理
-
-### 情况1:sug词条包含原始问题的所有对象层和场景层元素
-**评分**:0.95-1.0
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"川西秋季风光摄影作品"
-- 对象层:摄影作品(≈素材)
-- 场景层:川西 + 秋季 + 风光
-- 覆盖度:100%
-- 评分:0.98
-```
-
-### 情况2:sug词条包含部分场景层元素
-**评分**:根据覆盖比例
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光(3个元素)
-
-Sug词条:"川西风光摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:川西 + 风光(2个元素)
-- 覆盖度:(1+2)/(1+3) = 75%
-- 评分:0.85
-```
-
-### 情况3:sug词条只包含对象层,无场景层
-**评分**:根据对象匹配度和覆盖度
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:无
-- 覆盖度:1/4 = 25%
-- 评分:0.50(对象匹配但缺失所有限定)
-```
-
-### 情况4:sug词条只包含场景层,无对象层
-**评分**:较低分
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西"
-- 对象层:无
-- 场景层:川西 ✓
-- 覆盖度:1/2 = 50%
-- 评分:0.35(只有场景,缺失核心对象)
-```
-
----
-
-# 评估核心原则
-
-## 原则1:只看表面词汇,禁止联想推演
-**严格约束**:只能基于sug词实际包含的词汇评分
-
-**错误案例**:
-- ❌ "川西旅行" vs "旅行"
-  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
-  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **只看名词和限定词**:完全忽略动作和意图
-2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
-3. **禁止联想推演**:只看sug词实际包含的词汇
-4. **通用≠特定**:通用概念不等于特定概念
-5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
-""".strip()
-
-# 延伸词评估 prompt
-extension_word_evaluation_instructions = """
-# 角色
-你是**专业的延伸词语义评估专家**。
-任务:识别<平台sug词条>中的延伸词,评估其对原始问题作用域的补全度和目的贡献度,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-# 核心概念
-
-## 什么是延伸词?
-**延伸词**:<平台sug词条>中出现,但不属于<原始问题>作用域范围内的词汇或概念
-
-**关键判断**:
-```
-IF sug词的词汇属于原始问题的作用域元素(动机/对象/场景):
-   → 不是延伸词,是作用域内的词
-
-IF sug词的词汇不属于原始问题的作用域:
-   → 是延伸词
-   → 由Prompt3评估
-```
-
----
-
-# 作用域与延伸词
-
-## 作用域
-**作用域 = 动机层 + 对象层 + 场景层**
-
-**非延伸词示例**(属于作用域内):
-```
-原始问题:"川西旅行行程规划"
-作用域:
-- 动机层:规划
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西旅行行程规划攻略"
-- "川西"→ 属于场景层,不是延伸词
-- "旅行"→ 属于对象层,不是延伸词
-- "行程"→ 属于对象层,不是延伸词
-- "规划"→ 属于动机层,不是延伸词
-- "攻略"→ 与"规划"同义,不是延伸词
-- 结论:无延伸词
-```
-
-**延伸词示例**(不属于作用域):
-```
-原始问题:"川西旅行行程规划"
-作用域:规划 + 旅行行程 + 川西
-
-Sug词条:"川西旅行行程规划住宿推荐"
-- "住宿推荐"→ 不属于原始问题任何作用域
-- 结论:延伸词 = ["住宿推荐"]
-```
-
----
-
-# 延伸词识别方法
-
-## 步骤1:提取原始问题的作用域元素
-```
-动机层:提取动作及其同义词
-对象层:提取核心名词及其同义词
-场景层:提取所有限定词
-```
-
-## 步骤2:提取sug词条的所有关键词
-```
-提取sug词条中的所有实词(名词、动词、形容词)
-```
-
-## 步骤3:匹配判定
-```
-FOR 每个sug词条关键词:
-   IF 该词 ∈ 原始问题作用域元素(包括同义词):
-      → 不是延伸词
-   ELSE:
-      → 是延伸词
-```
-
-## 步骤4:同义词/相近词判定规则
-
-### 不算延伸词的情况:
-**同义词**:
-- 行程 ≈ 路线 ≈ 安排 ≈ 计划
-- 获取 ≈ 下载 ≈ 寻找 ≈ 收集
-- 技巧 ≈ 方法 ≈ 教程 ≈ 攻略
-- 素材 ≈ 资源 ≈ 作品 ≈ 内容
-
-**具体化/细化**:
-- 原始:"川西旅游" + sug词:"稻城亚丁"(川西的具体地点)→ 不算延伸
-- 原始:"摄影技巧" + sug词:"风光摄影"(摄影的细化)→ 不算延伸
-- 原始:"素材" + sug词:"高清素材"(素材的质量细化)→ 不算延伸
-
-**判定逻辑**:
-```
-IF sug词的概念是原始问题概念的子集/下位词/同义词:
-   → 不算延伸词
-   → 视为对原问题的细化或重述
-```
-
----
-
-### 算延伸词的情况:
-
-**新增维度**:原始问题未涉及的信息维度
-- 原始:"川西旅行" + sug词:"住宿" → 延伸词
-- 原始:"摄影素材" + sug词:"版权" → 延伸词
-
-**新增限定条件**:原始问题未提及的约束
-- 原始:"素材获取" + sug词:"免费" → 延伸词
-- 原始:"旅行行程" + sug词:"7天" → 延伸词
-
-**扩展主题**:相关但非原问题范围
-- 原始:"川西行程" + sug词:"美食推荐" → 延伸词
-- 原始:"摄影技巧" + sug词:"后期修图" → 延伸词
-
-**工具/方法**:原始问题未提及的具体工具
-- 原始:"视频剪辑" + sug词:"PR软件" → 延伸词
-- 原始:"图片处理" + sug词:"PS教程" → 延伸词
-
----
-
-# 延伸词类型与评分
-
-## 核心评估维度:对原始问题作用域的贡献
-
-### 维度1:作用域补全度
-延伸词是否帮助sug词条更接近原始问题的完整作用域?
-
-
-### 维度2:目的达成度
-延伸词是否促进原始问题核心目的的达成?
----
-####类型1:作用域增强型
-**定义**:延伸词是原始问题核心目的,或补全关键作用域
-**得分范围**:+0.12~+0.20
-
-**判定标准**:
-- 使sug词条更接近原始问题的完整需求
----
-
-####类型2:作用域辅助型
-**定义**:延伸词对核心目的有辅助作用,但非必需
-
-**得分范围**:+0.05~+0.12
-
-**判定标准**:
-- sug词条更丰富但不改变原始需求核心
-
----
-
-####类型3:作用域无关型
-**定义**:延伸词与核心目的无实质关联
-
-**得分**:0
-
-**示例**:
-- 原始:"如何拍摄风光" + 延伸词:"相机品牌排行"
-  - 评分:0
-  - 理由:品牌排行与拍摄技巧无关
-
----
-
-####类型4:作用域稀释型(轻度负向)
-**定义**:延伸词稀释原始问题的聚焦度,降低内容针对性
-
-**得分范围**:-0.08~-0.18
-
-**判定标准**:
-- 引入无关信息,分散注意力
-- 降低内容的专注度和深度
-- 使sug词条偏离原始问题的核心
-
-**示例**:
-- 原始:"专业风光摄影技巧" + 延伸词:"手机拍照"
-  - 评分:-0.12
-  - 理由:手机拍照与专业摄影需求不符,稀释专业度
-
-- 原始:"川西深度游攻略" + 延伸词:"周边一日游"
-  - 评分:-0.10
-  - 理由:一日游与深度游定位冲突,稀释深度
-
-
----
-
-# 特殊情况处理
-
-## 情况1:多个延伸词同时存在
-**处理方法**:分别评估每个延伸词,然后综合
-
-**综合规则**:
-```
-延伸词总得分 = Σ(每个延伸词得分) / 延伸词数量
-
-考虑累积效应:
-- 多个增强型延伸词 → 总分可能超过单个最高分,但上限+0.25
-- 正负延伸词并存 → 相互抵消
-- 多个冲突型延伸词 → 总分下限-0.60
-```
-
-**示例**:
-```
-原始:"川西旅行行程"
-Sug词条:"川西旅行行程住宿美食推荐"
-延伸词识别:
-- "住宿推荐"→ 增强型,+0.18
-- "美食推荐"→ 辅助型,+0.10
-总得分:(0.18 + 0.10) / 2 = 0.14
-```
-
----
-
-## 情况2:无延伸词
-**处理方法**:
-```
-IF sug词条无延伸词:
-   延伸词得分 = 0
-   理由:"sug词条未引入延伸词,所有词汇均属于原始问题作用域范围"
-```
-
----
-
-## 情况3:延伸词使sug词条更接近原始问题
-**特殊加成**:
-```
-IF 延伸词是原始问题隐含需求的显式化:
-   → 额外加成 +0.05
-```
-
-**示例**:
-```
-原始:"川西旅行" (隐含需要行程规划)
-Sug词条:"川西旅行行程规划"
-- "行程规划"可能被识别为延伸词,但它显式化了隐含需求
-- 给予额外加成
-```
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "延伸词得分": "-1到1之间的小数",
-  "简要说明延伸词维度相关度理由": "评估延伸词对作用域的影响"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明延伸词维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **严格区分**:作用域内的词 ≠ 延伸词
-2. **同义词/细化词不算延伸**:属于作用域范围的词由其他prompt评估
-3. **作用域导向**:评估延伸词是否使sug词条更接近原始问题的完整作用域
-4. **目的导向**:评估延伸词是否促进核心目的达成
-5. **分类明确**:准确判定延伸词类型
-6. **理由充分**:每个延伸词都要说明其对作用域和目的的影响
-7. **谨慎负分**:仅在明确冲突或有害时使用负分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-extension_word_evaluator = Agent[None](
-    name="延伸词评估专家",
-    instructions=extension_word_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=ExtensionWordEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(
-    motivation_score: float,
-    category_score: float,
-    extension_score: float,
-    zero_reason: str,
-    extension_reason: str = ""
-) -> tuple[float, str]:
-    """
-    三维评估综合打分
-
-    实现动态权重分配:
-    - 情况1:标准情况 → 动机50% + 品类40% + 延伸词10%
-    - 情况2:原始问题无动机 → 品类70% + 延伸词30%
-    - 情况3:sug词条无动机 → 品类80% + 延伸词20%
-    - 情况4:无延伸词 → 动机70% + 品类30%
-    - 规则3:负分传导 → 核心维度严重负向时上限为0
-    - 规则4:完美匹配加成 → 双维度≥0.95时加成+0.10
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-        extension_score: 延伸词得分 -1~1
-        zero_reason: 当motivation_score=0时的原因
-        extension_reason: 延伸词评估理由,用于判断是否无延伸词
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    # 情况2:原始问题无动作意图
-    if motivation_score == 0 and zero_reason == "原始问题无动机":
-        W1, W2, W3 = 0.0, 0.70, 0.30
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况2:原始问题无动作意图,权重调整为 品类70% + 延伸词30%"
-
-    # 情况3:sug词条无动作意图(但原始问题有)
-    elif motivation_score == 0 and zero_reason == "sug词条无动机":
-        W1, W2, W3 = 0.0, 0.80, 0.20
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况3:sug词条无动作意图,权重调整为 品类80% + 延伸词20%"
-
-    # 情况4:无延伸词
-    elif extension_score == 0:
-        W1, W2, W3 = 0.70, 0.30, 0.0
-        base_score = motivation_score * W1 + category_score * W2
-        rule_applied = "情况4:无延伸词,权重调整为 动机70% + 品类30%"
-
-    else:
-        # 情况1:标准权重
-        W1, W2, W3 = 0.50, 0.40, 0.10
-        base_score = motivation_score * W1 + category_score * W2 + extension_score * W3
-        rule_applied = ""
-
-    # 规则4:完美匹配加成
-    if motivation_score >= 0.95 and category_score >= 0.95:
-        base_score += 0.10
-        rule_applied += (" + " if rule_applied else "") + "规则4:双维度完美匹配,加成+0.10"
-
-    # 规则3:负分传导
-    if motivation_score <= -0.5 or category_score <= -0.5:
-        base_score = min(base_score, 0)
-        rule_applied += (" + " if rule_applied else "") + "规则3:核心维度严重负向,上限=0"
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, base_score))
-
-    return final_score, rule_applied
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用三个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-            extension_task = Runner.run(extension_word_evaluator, eval_input)
-
-            motivation_result, category_result, extension_result = await asyncio.gather(
-                motivation_task,
-                category_task,
-                extension_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-            extension_eval: ExtensionWordEvaluation = extension_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-            extension_score = extension_eval.延伸词得分
-            zero_reason = motivation_eval.得分为零的原因
-
-            # 应用规则计算最终得分
-            final_score, rule_applied = calculate_final_score(
-                motivation_score, category_score, extension_score, zero_reason,
-                extension_eval.简要说明延伸词维度相关度理由
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-            extension_reason = extension_eval.简要说明延伸词维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【延伸词维度 {extension_score:.2f}】{extension_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度
-            segment.score_with_o, segment.reason = await evaluate_with_o(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            comb.score_with_o, comb.reason = await evaluate_with_o(
-                comb.text, o, context.evaluation_cache
-            )
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    print(f"  评估完成,共{len(domain_combinations)}个组合:")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(组合得分 > 所有来源词)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 2439
sug_v6_1_2_123.py

@@ -1,2439 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.05
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. 疑问引导:引导查询意图的元素,如疑问词(原理:表示意图类型,如过程求解或信息查询)。
-2. 核心动作:核心动作或关系谓词,如动词(原理:谓词是语义框架的核心,定义动作或状态)。
-3. 目标对象:动作的目标或实体中心对象,如名词短语(承载谓词的作用对象助词)。
-4. 修饰限定:对目标对象的修饰和限定、对核心动作的限定。
-
-## 分段原则:严格遵守以下规则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-5. **修饰限定合并规则**
-- 定义:在同一个"目标对象"之前的所有"修饰限定"片段,如果它们之间没有插入"疑问引导"、"核心动作"或"目标对象",就必须合并为一个片段
-- 判断标准:
-* 步骤1:找到"目标对象"在哪里
-* 步骤2:向前查看,把所有修饰和限定这个目标对象的词都合并,修辞和限定词包括数量词、地域词、时间词、描述词、程度词、方式词、助词等
-
-## 输出要求
-
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-# 动机评估 prompt - 第一轮版本(来自 sug_v6_1_2_115.py)
-motivation_evaluation_instructions_round1 = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0。
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 动机评估 prompt - 后续轮次版本(当前 116 版本)
-motivation_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 动机评估核心原则(必读)
-
-### 动机 = 动作 + 对象 + 场景
-评估时必须同时考虑三要素,不能只看动词:
-- **动作**:制定、规划、获取、拍摄等
-- **对象**:旅行行程 vs 每日计划、风光照片 vs 证件照
-- **场景**:旅游 vs 日常、摄影 vs 办公
-
-### 关键判断:动词相同 ≠ 动机匹配
-
-错误:只看动词相同就给高分
-- "制定旅行行程" vs "制定每日计划" → 给0.95 错误
-- "拍摄风光" vs "拍摄证件照" → 给0.95 错误
-
-正确:检查对象和场景是否匹配
-- 对象不同领域 → 降至0.3左右
-- 场景不同 → 降至0.3左右
-
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **动机维度** 进行:
-
-# 维度独立性警告
-【严格约束】本评估**只评估动机维度**:
-**禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
----
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 动作+对象+场景完全一致
-  - 要求:动词、对象、场景都必须匹配,不能只看动词
-  - "制定旅行行程" vs "制定每日计划"
-     虽然动词相同,但对象和场景完全不同,不属于高分
-  - 特殊规则: 如果sug词的核心动作是原始问题动作在动作+对象+场景一致下的**具体化子集**,也判定为完全一致
-
-
-+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.25~0.4: 动词相同但对象或场景明显不同(弱相关)
-  - 判断要点:动词一致,但对象不同领域或场景不同
-  - 关键:不要因为动词相同就给0.95,必须检查对象!
-
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-  - 如果原始问题无法识别动机,则动机维度得分为0
-
- 特别注意 - 禁止的错误理由:
-  - 禁止: "虽然没有动作,但主题相关,所以给0.2"
-  - 禁止:"内容有参考价值,所以给0.15"
-  - 禁止: "都提到了XX(名词),所以不是完全无关"
-  - 正确理由:"sug词条无动作意图,与原始问题的'XX'动机完全无关"
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由"
-}
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由"
-}
----
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(motivation_score: float, category_score: float) -> float:
-    """
-    应用依存性规则计算最终得分
-
-    步骤1: 基础加权计算
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    步骤2: 极值保护规则
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        最终得分 -1~1
-    """
-    # 基础加权得分
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则C: 动机负向决定机制(最高优先级)
-    if motivation_score < 0:
-        return 0.0
-
-    # 规则A: 动机高分保护机制
-    if motivation_score >= 0.8:
-        # 当目的高度一致时,品类的泛化不应导致"弱相关"
-        return max(base_score, 0.7)
-
-    # 规则B: 动机低分限制机制
-    if motivation_score <= 0.2:
-        # 目的不符时,品类匹配的价值有限
-        return min(base_score, 0.5)
-
-    # 无规则调整,返回基础得分
-    return base_score
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(统一使用标准评估策略)
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 计算基础得分
-            base_score = motivation_score * 0.7 + category_score * 0.3
-
-            # 应用规则计算最终得分
-            final_score = calculate_final_score(motivation_score, category_score)
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【基础得分 {base_score:.2f}】= 动机({motivation_score:.2f})*0.7 + 品类({category_score:.2f})*0.3\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 如果应用了规则,添加规则说明
-            if final_score != base_score:
-                if motivation_score < 0:
-                    combined_reason += "(应用规则C:动机负向决定机制)"
-                elif motivation_score >= 0.8:
-                    combined_reason += "(应用规则A:动机高分保护机制)"
-                elif motivation_score <= 0.2:
-                    combined_reason += "(应用规则B:动机低分限制机制)"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度
-            segment.score_with_o, segment.reason = await evaluate_with_o(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            comb.score_with_o, comb.reason = await evaluate_with_o(
-                comb.text, o, context.evaluation_cache
-            )
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    print(f"  评估完成,共{len(domain_combinations)}个组合:")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(超过所有来源词得分)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 2756
sug_v6_1_2_124.py

@@ -1,2756 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. **疑问引导**:如何、怎么、什么、哪里等疑问词
-2. **核心动作**:关键动词,如获取、制作、拍摄、寻找等
-3. **修饰短语**:形容词、副词等修饰成分
-4. **中心名词**:核心名词
-5. **逻辑连接**:并且、或者、以及等连接词(较少出现)
-
-## 分段原则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-
-
-## 输出要求
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型(从5种类型中选择)
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-    得分为零的原因: Literal["原始问题无动机", "sug词条无动机", "动机不匹配", "不适用"] = Field(default="不适用", description="当得分为0时的原因分类")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-class ExtensionWordEvaluation(BaseModel):
-    """延伸词评估"""
-    延伸词得分: float = Field(..., ge=-1, le=1, description="延伸词得分 -1~1")
-    简要说明延伸词维度相关度理由: str = Field(..., description="延伸词维度相关度理由")
-
-# 动机评估 prompt(统一版本)
-motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:sug词条是原始问题的部分作用域
-
-当sug词条只包含原始问题的部分作用域时,需要判断:
-1. sug词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-Sug词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或sug词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-## 得分为零的原因(语义判断)
-
-当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
-- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
-- **"sug词条无动机"**:sug词条中不包含任何动作意图
-- **"动机不匹配"**:双方都有动作,但完全无关联
-- **"不适用"**:得分不为零时使用此默认值
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-# 角色
-你是**专业的内容主体评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
-
----
-
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估内容主体维度**:
-- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
-- **完全忽略**:动作、意图、目的
-- **评估重点**:内容本身的主题和属性
-
----
-
-# 作用域与内容主体
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-在Prompt2中:
-- **动机层(动作)完全忽略**
-- **只评估对象层 + 场景层(限定词)**
-
-## 内容主体的构成
-
-**内容主体 = 核心名词 + 限定词**
-
-
----
-
-# 作用域覆盖度评估
-
-## 核心原则:越完整越高分
-
-**完整性公式**:
-```
-作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
-```
-
-**评分影响**:
-- 覆盖度100% → 基础高分(0.9+)
-- 覆盖度50-99% → 中高分(0.6-0.9)
-- 覆盖度<50% → 中低分(0.3-0.6)
-- 覆盖度=0 → 低分或0分
-
----
-
-## 部分作用域的处理
-
-### 情况1:sug词条包含原始问题的所有对象层和场景层元素
-**评分**:0.95-1.0
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"川西秋季风光摄影作品"
-- 对象层:摄影作品(≈素材)
-- 场景层:川西 + 秋季 + 风光
-- 覆盖度:100%
-- 评分:0.98
-```
-
-### 情况2:sug词条包含部分场景层元素
-**评分**:根据覆盖比例
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光(3个元素)
-
-Sug词条:"川西风光摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:川西 + 风光(2个元素)
-- 覆盖度:(1+2)/(1+3) = 75%
-- 评分:0.85
-```
-
-### 情况3:sug词条只包含对象层,无场景层
-**评分**:根据对象匹配度和覆盖度
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:无
-- 覆盖度:1/4 = 25%
-- 评分:0.50(对象匹配但缺失所有限定)
-```
-
-### 情况4:sug词条只包含场景层,无对象层
-**评分**:较低分
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西"
-- 对象层:无
-- 场景层:川西 ✓
-- 覆盖度:1/2 = 50%
-- 评分:0.35(只有场景,缺失核心对象)
-```
-
----
-
-# 评估核心原则
-
-## 原则1:只看表面词汇,禁止联想推演
-**严格约束**:只能基于sug词实际包含的词汇评分
-
-**错误案例**:
-- ❌ "川西旅行" vs "旅行"
-  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
-  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **只看名词和限定词**:完全忽略动作和意图
-2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
-3. **禁止联想推演**:只看sug词实际包含的词汇
-4. **通用≠特定**:通用概念不等于特定概念
-5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
-""".strip()
-
-# 延伸词评估 prompt
-extension_word_evaluation_instructions = """
-# 角色
-你是**专业的延伸词语义评估专家**。
-任务:识别<平台sug词条>中的延伸词,评估其对原始问题作用域的补全度和目的贡献度,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-# 核心概念
-
-## 什么是延伸词?
-**延伸词**:<平台sug词条>中出现,但不属于<原始问题>作用域范围内的词汇或概念
-
-**关键判断**:
-```
-IF sug词的词汇属于原始问题的作用域元素(动机/对象/场景):
-   → 不是延伸词,是作用域内的词
-
-IF sug词的词汇不属于原始问题的作用域:
-   → 是延伸词
-   → 由Prompt3评估
-```
-
----
-
-# 作用域与延伸词
-
-## 作用域
-**作用域 = 动机层 + 对象层 + 场景层**
-
-**非延伸词示例**(属于作用域内):
-```
-原始问题:"川西旅行行程规划"
-作用域:
-- 动机层:规划
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西旅行行程规划攻略"
-- "川西"→ 属于场景层,不是延伸词
-- "旅行"→ 属于对象层,不是延伸词
-- "行程"→ 属于对象层,不是延伸词
-- "规划"→ 属于动机层,不是延伸词
-- "攻略"→ 与"规划"同义,不是延伸词
-- 结论:无延伸词
-```
-
-**延伸词示例**(不属于作用域):
-```
-原始问题:"川西旅行行程规划"
-作用域:规划 + 旅行行程 + 川西
-
-Sug词条:"川西旅行行程规划住宿推荐"
-- "住宿推荐"→ 不属于原始问题任何作用域
-- 结论:延伸词 = ["住宿推荐"]
-```
-
----
-
-# 延伸词识别方法
-
-## 步骤1:提取原始问题的作用域元素
-```
-动机层:提取动作及其同义词
-对象层:提取核心名词及其同义词
-场景层:提取所有限定词
-```
-
-## 步骤2:提取sug词条的所有关键词
-```
-提取sug词条中的所有实词(名词、动词、形容词)
-```
-
-## 步骤3:匹配判定
-```
-FOR 每个sug词条关键词:
-   IF 该词 ∈ 原始问题作用域元素(包括同义词):
-      → 不是延伸词
-   ELSE:
-      → 是延伸词
-```
-
-## 步骤4:同义词/相近词判定规则
-
-### 不算延伸词的情况:
-**同义词**:
-- 行程 ≈ 路线 ≈ 安排 ≈ 计划
-- 获取 ≈ 下载 ≈ 寻找 ≈ 收集
-- 技巧 ≈ 方法 ≈ 教程 ≈ 攻略
-- 素材 ≈ 资源 ≈ 作品 ≈ 内容
-
-**具体化/细化**:
-- 原始:"川西旅游" + sug词:"稻城亚丁"(川西的具体地点)→ 不算延伸
-- 原始:"摄影技巧" + sug词:"风光摄影"(摄影的细化)→ 不算延伸
-- 原始:"素材" + sug词:"高清素材"(素材的质量细化)→ 不算延伸
-
-**判定逻辑**:
-```
-IF sug词的概念是原始问题概念的子集/下位词/同义词:
-   → 不算延伸词
-   → 视为对原问题的细化或重述
-```
-
----
-
-### 算延伸词的情况:
-
-**新增维度**:原始问题未涉及的信息维度
-- 原始:"川西旅行" + sug词:"住宿" → 延伸词
-- 原始:"摄影素材" + sug词:"版权" → 延伸词
-
-**新增限定条件**:原始问题未提及的约束
-- 原始:"素材获取" + sug词:"免费" → 延伸词
-- 原始:"旅行行程" + sug词:"7天" → 延伸词
-
-**扩展主题**:相关但非原问题范围
-- 原始:"川西行程" + sug词:"美食推荐" → 延伸词
-- 原始:"摄影技巧" + sug词:"后期修图" → 延伸词
-
-**工具/方法**:原始问题未提及的具体工具
-- 原始:"视频剪辑" + sug词:"PR软件" → 延伸词
-- 原始:"图片处理" + sug词:"PS教程" → 延伸词
-
----
-
-# 延伸词类型与评分
-
-## 核心评估维度:对原始问题作用域的贡献
-
-### 维度1:作用域补全度
-延伸词是否帮助sug词条更接近原始问题的完整作用域?
-
-
-### 维度2:目的达成度
-延伸词是否促进原始问题核心目的的达成?
----
-####类型1:作用域增强型
-**定义**:延伸词是原始问题核心目的,或补全关键作用域
-**得分范围**:+0.12~+0.20
-
-**判定标准**:
-- 使sug词条更接近原始问题的完整需求
----
-
-####类型2:作用域辅助型
-**定义**:延伸词对核心目的有辅助作用,但非必需
-
-**得分范围**:+0.05~+0.12
-
-**判定标准**:
-- sug词条更丰富但不改变原始需求核心
-
----
-
-####类型3:作用域无关型
-**定义**:延伸词与核心目的无实质关联
-
-**得分**:0
-
-**示例**:
-- 原始:"如何拍摄风光" + 延伸词:"相机品牌排行"
-  - 评分:0
-  - 理由:品牌排行与拍摄技巧无关
-
----
-
-####类型4:作用域稀释型(轻度负向)
-**定义**:延伸词稀释原始问题的聚焦度,降低内容针对性
-
-**得分范围**:-0.08~-0.18
-
-**判定标准**:
-- 引入无关信息,分散注意力
-- 降低内容的专注度和深度
-- 使sug词条偏离原始问题的核心
-
-**示例**:
-- 原始:"专业风光摄影技巧" + 延伸词:"手机拍照"
-  - 评分:-0.12
-  - 理由:手机拍照与专业摄影需求不符,稀释专业度
-
-- 原始:"川西深度游攻略" + 延伸词:"周边一日游"
-  - 评分:-0.10
-  - 理由:一日游与深度游定位冲突,稀释深度
-
-
----
-
-# 特殊情况处理
-
-## 情况1:多个延伸词同时存在
-**处理方法**:分别评估每个延伸词,然后综合
-
-**综合规则**:
-```
-延伸词总得分 = Σ(每个延伸词得分) / 延伸词数量
-
-考虑累积效应:
-- 多个增强型延伸词 → 总分可能超过单个最高分,但上限+0.25
-- 正负延伸词并存 → 相互抵消
-- 多个冲突型延伸词 → 总分下限-0.60
-```
-
-**示例**:
-```
-原始:"川西旅行行程"
-Sug词条:"川西旅行行程住宿美食推荐"
-延伸词识别:
-- "住宿推荐"→ 增强型,+0.18
-- "美食推荐"→ 辅助型,+0.10
-总得分:(0.18 + 0.10) / 2 = 0.14
-```
-
----
-
-## 情况2:无延伸词
-**处理方法**:
-```
-IF sug词条无延伸词:
-   延伸词得分 = 0
-   理由:"sug词条未引入延伸词,所有词汇均属于原始问题作用域范围"
-```
-
----
-
-## 情况3:延伸词使sug词条更接近原始问题
-**特殊加成**:
-```
-IF 延伸词是原始问题隐含需求的显式化:
-   → 额外加成 +0.05
-```
-
-**示例**:
-```
-原始:"川西旅行" (隐含需要行程规划)
-Sug词条:"川西旅行行程规划"
-- "行程规划"可能被识别为延伸词,但它显式化了隐含需求
-- 给予额外加成
-```
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "延伸词得分": "-1到1之间的小数",
-  "简要说明延伸词维度相关度理由": "评估延伸词对作用域的影响"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明延伸词维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **严格区分**:作用域内的词 ≠ 延伸词
-2. **同义词/细化词不算延伸**:属于作用域范围的词由其他prompt评估
-3. **作用域导向**:评估延伸词是否使sug词条更接近原始问题的完整作用域
-4. **目的导向**:评估延伸词是否促进核心目的达成
-5. **分类明确**:准确判定延伸词类型
-6. **理由充分**:每个延伸词都要说明其对作用域和目的的影响
-7. **谨慎负分**:仅在明确冲突或有害时使用负分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-extension_word_evaluator = Agent[None](
-    name="延伸词评估专家",
-    instructions=extension_word_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=ExtensionWordEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(
-    motivation_score: float,
-    category_score: float,
-    extension_score: float,
-    zero_reason: str,
-    extension_reason: str = ""
-) -> tuple[float, str]:
-    """
-    三维评估综合打分
-
-    实现动态权重分配:
-    - 情况1:标准情况 → 动机50% + 品类40% + 延伸词10%
-    - 情况2:原始问题无动机 → 品类70% + 延伸词30%
-    - 情况3:sug词条无动机 → 品类80% + 延伸词20%
-    - 情况4:无延伸词 → 动机70% + 品类30%
-    - 规则3:负分传导 → 核心维度严重负向时上限为0
-    - 规则4:完美匹配加成 → 双维度≥0.95时加成+0.10
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-        extension_score: 延伸词得分 -1~1
-        zero_reason: 当motivation_score=0时的原因
-        extension_reason: 延伸词评估理由,用于判断是否无延伸词
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    # 情况2:原始问题无动作意图
-    if motivation_score == 0 and zero_reason == "原始问题无动机":
-        W1, W2, W3 = 0.0, 0.70, 0.30
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况2:原始问题无动作意图,权重调整为 品类70% + 延伸词30%"
-
-    # 情况3:sug词条无动作意图(但原始问题有)
-    elif motivation_score == 0 and zero_reason == "sug词条无动机":
-        W1, W2, W3 = 0.0, 0.80, 0.20
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况3:sug词条无动作意图,权重调整为 品类80% + 延伸词20%"
-
-    # 情况4:无延伸词
-    elif extension_score == 0:
-        W1, W2, W3 = 0.70, 0.30, 0.0
-        base_score = motivation_score * W1 + category_score * W2
-        rule_applied = "情况4:无延伸词,权重调整为 动机70% + 品类30%"
-
-    else:
-        # 情况1:标准权重
-        W1, W2, W3 = 0.50, 0.40, 0.10
-        base_score = motivation_score * W1 + category_score * W2 + extension_score * W3
-        rule_applied = ""
-
-    # 规则4:完美匹配加成
-    if motivation_score >= 0.95 and category_score >= 0.95:
-        base_score += 0.10
-        rule_applied += (" + " if rule_applied else "") + "规则4:双维度完美匹配,加成+0.10"
-
-    # 规则3:负分传导
-    if motivation_score <= -0.5 or category_score <= -0.5:
-        base_score = min(base_score, 0)
-        rule_applied += (" + " if rule_applied else "") + "规则3:核心维度严重负向,上限=0"
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, base_score))
-
-    return final_score, rule_applied
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用三个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-            extension_task = Runner.run(extension_word_evaluator, eval_input)
-
-            motivation_result, category_result, extension_result = await asyncio.gather(
-                motivation_task,
-                category_task,
-                extension_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-            extension_eval: ExtensionWordEvaluation = extension_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-            extension_score = extension_eval.延伸词得分
-            zero_reason = motivation_eval.得分为零的原因
-
-            # 应用规则计算最终得分
-            final_score, rule_applied = calculate_final_score(
-                motivation_score, category_score, extension_score, zero_reason,
-                extension_eval.简要说明延伸词维度相关度理由
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-            extension_reason = extension_eval.简要说明延伸词维度相关度理由
-
-            combined_reason = (
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【延伸词维度 {extension_score:.2f}】{extension_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度
-            segment.score_with_o, segment.reason = await evaluate_with_o(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            comb.score_with_o, comb.reason = await evaluate_with_o(
-                comb.text, o, context.evaluation_cache
-            )
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    print(f"  评估完成,共{len(domain_combinations)}个组合:")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(组合得分 > 所有来源词)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 3899
sug_v6_1_2_125.py

@@ -1,3899 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal, Optional
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-    # 历史词/组合得分追踪(用于Round 2+计算系数)
-    word_score_history: dict[str, float] = Field(default_factory=dict)
-    # key: 词/组合文本, value: 最终得分
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. **疑问引导**:如何、怎么、什么、哪里等疑问词
-2. **核心动作**:关键动词,如获取、制作、拍摄、寻找等
-3. **修饰短语**:形容词、副词等修饰成分
-4. **中心名词**:核心名词
-5. **逻辑连接**:并且、或者、以及等连接词(较少出现)
-
-## 分段原则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-
-
-## 输出要求
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型(从5种类型中选择)
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-    得分为零的原因: Optional[Literal["原始问题无动机", "sug词条无动机", "动机不匹配", "不适用"]] = Field(None, description="当得分为0时的原因分类(可选,仅SUG评估使用)")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-class ExtensionWordEvaluation(BaseModel):
-    """延伸词评估"""
-    延伸词得分: float = Field(..., ge=-1, le=1, description="延伸词得分 -1~1")
-    简要说明延伸词维度相关度理由: str = Field(..., description="延伸词维度相关度理由")
-
-# 动机评估 prompt(统一版本)
-motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:sug词条是原始问题的部分作用域
-
-当sug词条只包含原始问题的部分作用域时,需要判断:
-1. sug词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-Sug词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或sug词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-## 得分为零的原因(语义判断)
-
-当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
-- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
-- **"sug词条无动机"**:sug词条中不包含任何动作意图
-- **"动机不匹配"**:双方都有动作,但完全无关联
-- **"不适用"**:得分不为零时使用此默认值
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-# 角色
-你是**专业的内容主体评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
-
----
-
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估内容主体维度**:
-- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
-- **完全忽略**:动作、意图、目的
-- **评估重点**:内容本身的主题和属性
-
----
-
-# 作用域与内容主体
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-在Prompt2中:
-- **动机层(动作)完全忽略**
-- **只评估对象层 + 场景层(限定词)**
-
-## 内容主体的构成
-
-**内容主体 = 核心名词 + 限定词**
-
-
----
-
-# 作用域覆盖度评估
-
-## 核心原则:越完整越高分
-
-**完整性公式**:
-```
-作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
-```
-
-**评分影响**:
-- 覆盖度100% → 基础高分(0.9+)
-- 覆盖度50-99% → 中高分(0.6-0.9)
-- 覆盖度<50% → 中低分(0.3-0.6)
-- 覆盖度=0 → 低分或0分
-
----
-
-## 部分作用域的处理
-
-### 情况1:sug词条包含原始问题的所有对象层和场景层元素
-**评分**:0.95-1.0
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"川西秋季风光摄影作品"
-- 对象层:摄影作品(≈素材)
-- 场景层:川西 + 秋季 + 风光
-- 覆盖度:100%
-- 评分:0.98
-```
-
-### 情况2:sug词条包含部分场景层元素
-**评分**:根据覆盖比例
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光(3个元素)
-
-Sug词条:"川西风光摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:川西 + 风光(2个元素)
-- 覆盖度:(1+2)/(1+3) = 75%
-- 评分:0.85
-```
-
-### 情况3:sug词条只包含对象层,无场景层
-**评分**:根据对象匹配度和覆盖度
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:无
-- 覆盖度:1/4 = 25%
-- 评分:0.50(对象匹配但缺失所有限定)
-```
-
-### 情况4:sug词条只包含场景层,无对象层
-**评分**:较低分
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西"
-- 对象层:无
-- 场景层:川西 ✓
-- 覆盖度:1/2 = 50%
-- 评分:0.35(只有场景,缺失核心对象)
-```
-
----
-
-# 评估核心原则
-
-## 原则1:只看表面词汇,禁止联想推演
-**严格约束**:只能基于sug词实际包含的词汇评分
-
-**错误案例**:
-- ❌ "川西旅行" vs "旅行"
-  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
-  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **只看名词和限定词**:完全忽略动作和意图
-2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
-3. **禁止联想推演**:只看sug词实际包含的词汇
-4. **通用≠特定**:通用概念不等于特定概念
-5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
-""".strip()
-
-# 延伸词评估 prompt
-extension_word_evaluation_instructions = """
-# 角色
-你是**专业的延伸词语义评估专家**。
-任务:识别<平台sug词条>中的延伸词,评估其对原始问题作用域的补全度和目的贡献度,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-# 核心概念
-
-## 什么是延伸词?
-**延伸词**:<平台sug词条>中出现,但不属于<原始问题>作用域范围内的词汇或概念
-
-**关键判断**:
-```
-IF sug词的词汇属于原始问题的作用域元素(动机/对象/场景):
-   → 不是延伸词,是作用域内的词
-
-IF sug词的词汇不属于原始问题的作用域:
-   → 是延伸词
-   → 由Prompt3评估
-```
-
----
-
-# 作用域与延伸词
-
-## 作用域
-**作用域 = 动机层 + 对象层 + 场景层**
-
-**非延伸词示例**(属于作用域内):
-```
-原始问题:"川西旅行行程规划"
-作用域:
-- 动机层:规划
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西旅行行程规划攻略"
-- "川西"→ 属于场景层,不是延伸词
-- "旅行"→ 属于对象层,不是延伸词
-- "行程"→ 属于对象层,不是延伸词
-- "规划"→ 属于动机层,不是延伸词
-- "攻略"→ 与"规划"同义,不是延伸词
-- 结论:无延伸词
-```
-
-**延伸词示例**(不属于作用域):
-```
-原始问题:"川西旅行行程规划"
-作用域:规划 + 旅行行程 + 川西
-
-Sug词条:"川西旅行行程规划住宿推荐"
-- "住宿推荐"→ 不属于原始问题任何作用域
-- 结论:延伸词 = ["住宿推荐"]
-```
-
----
-
-# 延伸词识别方法
-
-## 步骤1:提取原始问题的作用域元素
-```
-动机层:提取动作及其同义词
-对象层:提取核心名词及其同义词
-场景层:提取所有限定词
-```
-
-## 步骤2:提取sug词条的所有关键词
-```
-提取sug词条中的所有实词(名词、动词、形容词)
-```
-
-## 步骤3:匹配判定
-```
-FOR 每个sug词条关键词:
-   IF 该词 ∈ 原始问题作用域元素(包括同义词):
-      → 不是延伸词
-   ELSE:
-      → 是延伸词
-```
-
-## 步骤4:同义词/相近词判定规则
-
-### 不算延伸词的情况:
-**同义词**:
-- 行程 ≈ 路线 ≈ 安排 ≈ 计划
-- 获取 ≈ 下载 ≈ 寻找 ≈ 收集
-- 技巧 ≈ 方法 ≈ 教程 ≈ 攻略
-- 素材 ≈ 资源 ≈ 作品 ≈ 内容
-
-**具体化/细化**:
-- 原始:"川西旅游" + sug词:"稻城亚丁"(川西的具体地点)→ 不算延伸
-- 原始:"摄影技巧" + sug词:"风光摄影"(摄影的细化)→ 不算延伸
-- 原始:"素材" + sug词:"高清素材"(素材的质量细化)→ 不算延伸
-
-**判定逻辑**:
-```
-IF sug词的概念是原始问题概念的子集/下位词/同义词:
-   → 不算延伸词
-   → 视为对原问题的细化或重述
-```
-
----
-
-### 算延伸词的情况:
-
-**新增维度**:原始问题未涉及的信息维度
-- 原始:"川西旅行" + sug词:"住宿" → 延伸词
-- 原始:"摄影素材" + sug词:"版权" → 延伸词
-
-**新增限定条件**:原始问题未提及的约束
-- 原始:"素材获取" + sug词:"免费" → 延伸词
-- 原始:"旅行行程" + sug词:"7天" → 延伸词
-
-**扩展主题**:相关但非原问题范围
-- 原始:"川西行程" + sug词:"美食推荐" → 延伸词
-- 原始:"摄影技巧" + sug词:"后期修图" → 延伸词
-
-**工具/方法**:原始问题未提及的具体工具
-- 原始:"视频剪辑" + sug词:"PR软件" → 延伸词
-- 原始:"图片处理" + sug词:"PS教程" → 延伸词
-
----
-
-# 延伸词类型与评分
-
-## 核心评估维度:对原始问题作用域的贡献
-
-### 维度1:作用域补全度
-延伸词是否帮助sug词条更接近原始问题的完整作用域?
-
-
-### 维度2:目的达成度
-延伸词是否促进原始问题核心目的的达成?
----
-####类型1:作用域增强型
-**定义**:延伸词是原始问题核心目的,或补全关键作用域
-**得分范围**:+0.12~+0.20
-
-**判定标准**:
-- 使sug词条更接近原始问题的完整需求
----
-
-####类型2:作用域辅助型
-**定义**:延伸词对核心目的有辅助作用,但非必需
-
-**得分范围**:+0.05~+0.12
-
-**判定标准**:
-- sug词条更丰富但不改变原始需求核心
-
----
-
-####类型3:作用域无关型
-**定义**:延伸词与核心目的无实质关联
-
-**得分**:0
-
-**示例**:
-- 原始:"如何拍摄风光" + 延伸词:"相机品牌排行"
-  - 评分:0
-  - 理由:品牌排行与拍摄技巧无关
-
----
-
-####类型4:作用域稀释型(轻度负向)
-**定义**:延伸词稀释原始问题的聚焦度,降低内容针对性
-
-**得分范围**:-0.08~-0.18
-
-**判定标准**:
-- 引入无关信息,分散注意力
-- 降低内容的专注度和深度
-- 使sug词条偏离原始问题的核心
-
-**示例**:
-- 原始:"专业风光摄影技巧" + 延伸词:"手机拍照"
-  - 评分:-0.12
-  - 理由:手机拍照与专业摄影需求不符,稀释专业度
-
-- 原始:"川西深度游攻略" + 延伸词:"周边一日游"
-  - 评分:-0.10
-  - 理由:一日游与深度游定位冲突,稀释深度
-
-
----
-
-# 特殊情况处理
-
-## 情况1:多个延伸词同时存在
-**处理方法**:分别评估每个延伸词,然后综合
-
-**综合规则**:
-```
-延伸词总得分 = Σ(每个延伸词得分) / 延伸词数量
-
-考虑累积效应:
-- 多个增强型延伸词 → 总分可能超过单个最高分,但上限+0.25
-- 正负延伸词并存 → 相互抵消
-- 多个冲突型延伸词 → 总分下限-0.60
-```
-
-**示例**:
-```
-原始:"川西旅行行程"
-Sug词条:"川西旅行行程住宿美食推荐"
-延伸词识别:
-- "住宿推荐"→ 增强型,+0.18
-- "美食推荐"→ 辅助型,+0.10
-总得分:(0.18 + 0.10) / 2 = 0.14
-```
-
----
-
-## 情况2:无延伸词
-**处理方法**:
-```
-IF sug词条无延伸词:
-   延伸词得分 = 0
-   理由:"sug词条未引入延伸词,所有词汇均属于原始问题作用域范围"
-```
-
----
-
-## 情况3:延伸词使sug词条更接近原始问题
-**特殊加成**:
-```
-IF 延伸词是原始问题隐含需求的显式化:
-   → 额外加成 +0.05
-```
-
-**示例**:
-```
-原始:"川西旅行" (隐含需要行程规划)
-Sug词条:"川西旅行行程规划"
-- "行程规划"可能被识别为延伸词,但它显式化了隐含需求
-- 给予额外加成
-```
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "延伸词得分": "-1到1之间的小数",
-  "简要说明延伸词维度相关度理由": "评估延伸词对作用域的影响"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明延伸词维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **严格区分**:作用域内的词 ≠ 延伸词
-2. **同义词/细化词不算延伸**:属于作用域范围的词由其他prompt评估
-3. **作用域导向**:评估延伸词是否使sug词条更接近原始问题的完整作用域
-4. **目的导向**:评估延伸词是否促进核心目的达成
-5. **分类明确**:准确判定延伸词类型
-6. **理由充分**:每个延伸词都要说明其对作用域和目的的影响
-7. **谨慎负分**:仅在明确冲突或有害时使用负分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-extension_word_evaluator = Agent[None](
-    name="延伸词评估专家",
-    instructions=extension_word_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=ExtensionWordEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# Round 0 专用 Agent(v124新增 - 需求1)
-# ============================================================================
-
-# Round 0 动机评估 prompt(不含延伸词)
-round0_motivation_evaluation_instructions = """
-#角色
-你是**专业的动机意图评估专家**
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:词条是原始问题的部分作用域
-
-当词条只包含原始问题的部分作用域时,需要判断:
-1. 词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
-
----
-
-#评分标准:
-
-【正向匹配】
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs 词条"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.90: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs 词条"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与原始问题动机匹配程度的理由"
-}
-```
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# Round 0 品类评估 prompt(不含延伸词)
-round0_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于词条实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs 词条:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "词条只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF 词条是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: 词条是通用概念,原始问题是特定概念
-    词条"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 词条也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs 词条"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs 词条"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs 词条"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与原始问题品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建 Round 0 评估 Agent
-round0_motivation_evaluator = Agent[None](
-    name="Round 0动机维度评估专家",
-    instructions=round0_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-round0_category_evaluator = Agent[None](
-    name="Round 0品类维度评估专家",
-    instructions=round0_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# 域内/域间 专用 Agent(v124新增 - 需求2&3)
-# ============================================================================
-
-# 域内/域间 动机评估 prompt(不含延伸词)
-scope_motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<词条>与<同一作用域词条>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
----
-# 评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-# 核心约束
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-当前任务:
-- **只提取动机层**:动作意图(获取、学习、规划、拍摄等)
-
-## 动作意图的识别
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - 词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与该条作用域匹配程度的理由",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 域内/域间 品类评估 prompt(不含延伸词)
-scope_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
----
-#判定流程
-#评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-#相关度评估维度详解
-
-##评估对象: <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与同一作用域词条品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建域内/域间评估 Agent
-scope_motivation_evaluator = Agent[None](
-    name="域内动机维度评估专家",
-    instructions=scope_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-   model_settings=ModelSettings(temperature=0.2)
-)
-
-scope_category_evaluator = Agent[None](
-    name="域内品类维度评估专家",
-    instructions=scope_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(
-    motivation_score: float,
-    category_score: float,
-    extension_score: float,
-    zero_reason: Optional[str],
-    extension_reason: str = ""
-) -> tuple[float, str]:
-    """
-    三维评估综合打分
-
-    实现动态权重分配:
-    - 情况1:标准情况 → 动机50% + 品类40% + 延伸词10%
-    - 情况2:原始问题无动机 → 品类70% + 延伸词30%
-    - 情况3:sug词条无动机 → 品类80% + 延伸词20%
-    - 情况4:无延伸词 → 动机70% + 品类30%
-    - 规则3:负分传导 → 核心维度严重负向时上限为0
-    - 规则4:完美匹配加成 → 双维度≥0.95时加成+0.10
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-        extension_score: 延伸词得分 -1~1
-        zero_reason: 当motivation_score=0时的原因(可选)
-        extension_reason: 延伸词评估理由,用于判断是否无延伸词
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    # 情况2:原始问题无动作意图
-    if motivation_score == 0 and zero_reason == "原始问题无动机":
-        W1, W2, W3 = 0.0, 0.70, 0.30
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况2:原始问题无动作意图,权重调整为 品类70% + 延伸词30%"
-
-    # 情况3:sug词条无动作意图(但原始问题有)
-    elif motivation_score == 0 and zero_reason == "sug词条无动机":
-        W1, W2, W3 = 0.0, 0.80, 0.20
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况3:sug词条无动作意图,权重调整为 品类80% + 延伸词20%"
-
-    # 情况4:无延伸词
-    elif extension_score == 0:
-        W1, W2, W3 = 0.70, 0.30, 0.0
-        base_score = motivation_score * W1 + category_score * W2
-        rule_applied = "情况4:无延伸词,权重调整为 动机70% + 品类30%"
-
-    else:
-        # 情况1:标准权重
-        W1, W2, W3 = 0.50, 0.40, 0.10
-        base_score = motivation_score * W1 + category_score * W2 + extension_score * W3
-        rule_applied = ""
-
-    # 规则4:完美匹配加成
-    if motivation_score >= 0.95 and category_score >= 0.95:
-        base_score += 0.10
-        rule_applied += (" + " if rule_applied else "") + "规则4:双维度完美匹配,加成+0.10"
-
-    # 规则3:负分传导
-    if motivation_score <= -0.5 or category_score <= -0.5:
-        base_score = min(base_score, 0)
-        rule_applied += (" + " if rule_applied else "") + "规则3:核心维度严重负向,上限=0"
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, base_score))
-
-    return final_score, rule_applied
-
-
-def calculate_final_score_v2(
-    motivation_score: float,
-    category_score: float
-) -> tuple[float, str]:
-    """
-    两维评估综合打分(v124新增 - 需求1)
-
-    用于Round 0分词评估和域内/域间评估,不含延伸词维度
-
-    基础权重:动机70% + 品类30%
-
-    应用规则:
-    - 规则A:动机高分保护机制
-      IF 动机维度得分 ≥ 0.8:
-         品类得分即使为0或轻微负向(-0.2~0)
-         → 最终得分应该不低于0.7
-      解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-    - 规则B:动机低分限制机制
-      IF 动机维度得分 ≤ 0.2:
-         无论品类得分多高
-         → 最终得分不高于0.5
-      解释: 目的不符时,品类匹配的价值有限
-
-    - 规则C:动机负向决定机制
-      IF 动机维度得分 < 0:
-         → 最终得分为0
-      解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    rule_applied = ""
-
-    # 规则C:动机负向决定机制
-    if motivation_score < 0:
-        final_score = 0.0
-        rule_applied = "规则C:动机负向,最终得分=0"
-        return final_score, rule_applied
-
-    # 基础加权计算: 动机70% + 品类30%
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则A:动机高分保护机制
-    if motivation_score >= 0.8:
-        if base_score < 0.7:
-            final_score = 0.7
-            rule_applied = f"规则A:动机高分保护(动机{motivation_score:.2f}≥0.8),最终得分下限=0.7"
-        else:
-            final_score = base_score
-            rule_applied = f"规则A:动机高分保护生效(动机{motivation_score:.2f}≥0.8),实际得分{base_score:.2f}已≥0.7"
-
-    # 规则B:动机低分限制机制
-    elif motivation_score <= 0.2:
-        if base_score > 0.5:
-            final_score = 0.5
-            rule_applied = f"规则B:动机低分限制(动机{motivation_score:.2f}≤0.2),最终得分上限=0.5"
-        else:
-            final_score = base_score
-            rule_applied = f"规则B:动机低分限制生效(动机{motivation_score:.2f}≤0.2),实际得分{base_score:.2f}已≤0.5"
-
-    # 无规则触发
-    else:
-        final_score = base_score
-        rule_applied = ""
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, final_score))
-
-    return final_score, rule_applied
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用三个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-            extension_task = Runner.run(extension_word_evaluator, eval_input)
-
-            motivation_result, category_result, extension_result = await asyncio.gather(
-                motivation_task,
-                category_task,
-                extension_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-            extension_eval: ExtensionWordEvaluation = extension_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-            extension_score = extension_eval.延伸词得分
-            zero_reason = motivation_eval.得分为零的原因
-
-            # 应用规则计算最终得分
-            final_score, rule_applied = calculate_final_score(
-                motivation_score, category_score, extension_score, zero_reason,
-                extension_eval.简要说明延伸词维度相关度理由
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-            extension_reason = extension_eval.简要说明延伸词维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【延伸词维度 {extension_score:.2f}】{extension_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_with_o_round0(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """Round 0专用评估函数(v124新增 - 需求1)
-
-    用于评估segment和word与原始问题的相关度
-    不含延伸词维度,使用Round 0专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本(segment或word)
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"round0:{text}:{o}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ Round0缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(round0_motivation_evaluator, eval_input)
-            category_task = Runner.run(round0_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  Round0评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ Round0评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"Round0评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_within_scope(text: str, scope_text: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """域内/域间专用评估函数(v124新增 - 需求2&3)
-
-    用于评估词条与作用域词条(单域或域组合)的相关度
-    不含延伸词维度,使用域内专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的词条
-        scope_text: 作用域词条(可以是单域词条或域组合词条)
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"scope:{text}:{scope_text}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ 域内缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<同一作用域词条>
-{scope_text}
-</同一作用域词条>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(scope_motivation_evaluator, eval_input)
-            category_task = Runner.run(scope_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 作用域词条"{scope_text}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  域内评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ 域内评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"域内评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# v125 新增辅助函数(用于新评分逻辑)
-# ============================================================================
-
-def get_source_word_score(
-    word_text: str,
-    segment: Segment,
-    context: RunContext
-) -> float:
-    """
-    查找来源词的得分
-
-    查找顺序:
-    1. 先查 segment.word_scores (Round 0的单个词)
-    2. 再查 context.word_score_history (Round 1+的组合)
-
-    Args:
-        word_text: 词文本
-        segment: 该词所在的segment
-        context: 运行上下文
-
-    Returns:
-        词的得分,找不到返回0.0
-    """
-    # 优先查Round 0的词得分
-    if word_text in segment.word_scores:
-        return segment.word_scores[word_text]
-
-    # 其次查历史组合得分
-    if word_text in context.word_score_history:
-        return context.word_score_history[word_text]
-
-    # 都找不到
-    print(f"  ⚠️  警告: 未找到来源词得分: {word_text}")
-    return 0.0
-
-
-async def evaluate_domain_combination_round1(
-    comb: DomainCombination,
-    segments: list[Segment],
-    context: RunContext
-) -> tuple[float, str]:
-    """
-    Round 1 域内组合评估(新逻辑)
-
-    最终得分 = 品类得分 × 原始域得分
-
-    Args:
-        comb: 域内组合对象
-        segments: 所有segment列表
-        context: 运行上下文
-
-    Returns:
-        (最终得分, 评估理由)
-    """
-    # 获取所属segment
-    domain_idx = comb.domains[0] if comb.domains else 0
-    segment = segments[domain_idx] if 0 <= domain_idx < len(segments) else None
-
-    if not segment:
-        return 0.0, "错误: 无法找到所属segment"
-
-    # 拼接作用域文本
-    scope_text = segment.text
-
-    # 准备输入
-    eval_input = f"""
-<同一作用域词条>
-{scope_text}
-</同一作用域词条>
-
-<词条>
-{comb.text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-    # 只调用品类评估器
-    try:
-        category_result = await Runner.run(scope_category_evaluator, eval_input)
-        category_eval: CategoryEvaluation = category_result.final_output
-        category_score = category_eval.品类维度得分
-        category_reason = category_eval.简要说明品类维度相关度理由
-    except Exception as e:
-        print(f"  ❌ Round 1品类评估失败: {e}")
-        return 0.0, f"评估失败: {str(e)[:100]}"
-
-    # 计算最终得分
-    domain_score = segment.score_with_o
-    final_score = category_score * domain_score
-
-    # 组合评估理由
-    combined_reason = (
-        f'【Round 1 域内评估】\n'
-        f'【评估对象】组合"{comb.text}" vs 作用域"{scope_text}"\n'
-        f'【品类得分】{category_score:.2f} - {category_reason}\n'
-        f'【原始域得分】{domain_score:.2f}\n'
-        f'【计算公式】品类得分 × 域得分 = {category_score:.2f} × {domain_score:.2f}\n'
-        f'【最终得分】{final_score:.2f}'
-    )
-
-    return final_score, combined_reason
-
-
-async def evaluate_domain_combination_round2plus(
-    comb: DomainCombination,
-    segments: list[Segment],
-    context: RunContext
-) -> tuple[float, str]:
-    """
-    Round 2+ 域间组合评估(新逻辑)
-
-    步骤:
-    1. 用现有逻辑评估得到 base_score
-    2. 计算加权系数 = Σ(来源词得分) / Σ(域得分)
-    3. 最终得分 = base_score × 系数,截断到1.0
-
-    Args:
-        comb: 域间组合对象
-        segments: 所有segment列表
-        context: 运行上下文
-
-    Returns:
-        (最终得分, 评估理由)
-    """
-    # 步骤1: 现有逻辑评估(域内评估)
-    scope_text = "".join(comb.from_segments)
-
-    base_score, base_reason = await evaluate_within_scope(
-        comb.text,
-        scope_text,
-        context.evaluation_cache
-    )
-
-    # 步骤2: 计算加权系数
-    total_source_score = 0.0
-    total_domain_score = 0.0
-    coefficient_details = []
-
-    for domain_idx, source_words_list in zip(comb.domains, comb.source_words):
-        # 获取segment
-        segment = segments[domain_idx] if 0 <= domain_idx < len(segments) else None
-        if not segment:
-            continue
-
-        domain_score = segment.score_with_o
-        total_domain_score += domain_score
-
-        # 如果该域贡献了多个词(组合),需要拼接后查找
-        if len(source_words_list) == 1:
-            # 单个词
-            source_word_text = source_words_list[0]
-        else:
-            # 多个词组合
-            source_word_text = "".join(source_words_list)
-
-        # 查找来源词得分
-        source_score = get_source_word_score(source_word_text, segment, context)
-        total_source_score += source_score
-
-        coefficient_details.append(
-            f"  域{domain_idx}[{segment.type}]: \"{source_word_text}\"得分={source_score:.2f}, 域得分={domain_score:.2f}"
-        )
-
-    # 计算系数
-    if total_domain_score > 0:
-        coefficient = total_source_score / total_domain_score
-    else:
-        coefficient = 0.0
-
-    # 步骤3: 计算最终得分并截断
-    final_score = base_score * total_source_score
-    final_score = min(1.0, max(-1.0, final_score))  # 截断到[-1.0, 1.0]
-
-    # 组合评估理由
-    coefficient_detail_str = "\n".join(coefficient_details)
-    combined_reason = (
-        f'【Round 2+ 域间评估】\n'
-        f'【评估对象】组合"{comb.text}"\n'
-        f'{base_reason}\n'
-        f'【加权系数计算】\n'
-        f'{total_source_score}\n'
-        f'  来源词总得分: {total_source_score:.2f}\n'
-        f'  系数: {total_source_score:.2f}'
-        f'【计算公式】base_score × 系数 = {base_score:.2f} × {total_source_score:.2f}\n'
-        f'【最终得分(截断后)】{final_score:.2f}'
-    )
-
-    return final_score, combined_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度(使用Round 0专用评估)
-            segment.score_with_o, segment.reason = await evaluate_with_o_round0(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度(使用Round 0专用评估)
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o_round0(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    # 🆕 存储Round 0的所有word得分到历史记录
-    print(f"\n[存储Round 0词得分到历史记录]")
-    for segment in segment_list:
-        for word, score in segment.word_scores.items():
-            context.word_score_history[word] = score
-            print(f"  {word}: {score:.2f}")
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            # 🆕 根据轮次选择评估逻辑
-            if round_num == 1:
-                # Round 1: 域内评估(新逻辑)
-                comb.score_with_o, comb.reason = await evaluate_domain_combination_round1(
-                    comb, segments, context
-                )
-            else:
-                # Round 2+: 域间评估(新逻辑)
-                comb.score_with_o, comb.reason = await evaluate_domain_combination_round2plus(
-                    comb, segments, context
-                )
-
-            # 🆕 存储组合得分到历史记录
-            context.word_score_history[comb.text] = comb.score_with_o
-
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    evaluation_strategy = 'Round 1 域内评估(品类×域得分)' if round_num == 1 else 'Round 2+ 域间评估(加权系数调整)'
-    print(f"  评估完成,共{len(domain_combinations)}个组合 [策略: {evaluation_strategy}]")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(组合得分 > 所有来源词)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 3894
sug_v6_1_2_126.py

@@ -1,3894 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal, Optional
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果 - 谓语+作用域方式)"""
-    text: str  # 片段文本
-    type: str  # 类型: interrogative/scope(scopes已包含谓语)
-    scope_type: Optional[str] = None  # 如果type是scope,记录作用域类型(predicate_feature/predicate_object/predicate_full/feature/object/full/location/time/composite等)
-    is_complete: Optional[bool] = None  # 如果type是scope,记录是否语义完整
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, float] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1 - 改为谓语+作用域方式)
-class ScopeFragment(BaseModel):
-    """作用域片段(不要求语义完整)"""
-    text: str = Field(..., description="作用域文本")
-    scope_type: str = Field(..., description="类型:feature/object/full/location/time/composite等")
-    is_complete: bool = Field(..., description="是否语义完整(仅标注,不强制要求)")
-    reasoning: str = Field(..., description="为什么识别为此作用域片段")
-
-
-class QuerySegmentation(BaseModel):
-    """Query分段结果(谓语+作用域方式)"""
-    interrogative: Optional[str] = Field(None, description="疑问词(如何、怎么等),可选")
-    predicate: Optional[str] = Field(None, description="谓语动词,可选(纯名词短语时为空)")
-    scopes: list[ScopeFragment] = Field(default_factory=list, description="作用域片段列表,可以有多个层次")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,识别谓语和作用域。
-
-## 核心概念
-- **谓语**:核心动词,表示"做什么动作"(如:制作、获取、学习)
-- **作用域**:谓语的作用对象及修饰成分,可以有多个层次
-- **疑问引导**:疑问词(如何、怎么、什么等)
-
-## 分段原则
-1. **识别疑问词**(可选):如"如何"、"怎么"、"为什么"
-2. **识别谓语**:核心动词
-3. **识别作用域的多个层次**:
-   - ❗ 不要求语义完整
-   - ✅ 只要有独立的检索价值即可
-   - ✅ 可以是修饰短语、名词短语、或完整宾语
-   - ✅ 可以识别出多个不同粒度的作用域片段
-
-## 作用域类型
-
-### 有谓语时(scopes包含谓语):
-- **predicate_feature**: 谓语+特征修饰(如"制作反映人类双标行为")
-- **predicate_object**: 谓语+核心对象(如"制作猫咪表情包梗图")
-- **predicate_full**: 谓语+完整作用域(如"制作反映人类双标行为的猫咪表情包梗图")
-
-### 无谓语时(纯名词短语):
-- **location**: 地点限定(如"川西")
-- **time**: 时间限定(如"秋季")
-- **feature**: 主题特征(如"风光")
-- **object**: 核心对象(如"摄影素材")
-- **composite**: 复合片段(如"川西风光"、"风光摄影素材")
-- **full**: 完整query(如"川西秋季风光摄影素材")
-
-## 示例1
-Input: "如何制作反映人类双标行为的猫咪表情包梗图"
-
-Output:
-{
-  "interrogative": "如何",
-  "predicate": "制作",
-  "scopes": [
-    {
-      "text": "制作反映人类双标行为",
-      "scope_type": "predicate_feature",
-      "is_complete": false,
-      "reasoning": "谓语+特征修饰,虽然缺少名词但有独立检索价值,可用于查找制作所有反映双标行为的内容"
-    },
-    {
-      "text": "制作猫咪表情包梗图",
-      "scope_type": "predicate_object",
-      "is_complete": true,
-      "reasoning": "谓语+核心对象,明确的检索目标"
-    },
-    {
-      "text": "制作反映人类双标行为的猫咪表情包梗图",
-      "scope_type": "predicate_full",
-      "is_complete": true,
-      "reasoning": "谓语+完整作用域,包含所有修饰和核心对象,最精确的匹配"
-    }
-  ],
-  "overall_reasoning": "Query包含疑问引导词和谓语,scopes直接包含谓语与其作用域的组合,分为特征层、对象层和完整层三个检索粒度"
-}
-
-## 示例2
-Input: "川西秋季风光摄影素材"
-
-Output:
-{
-  "interrogative": null,
-  "predicate": null,
-  "scopes": [
-    {
-      "text": "川西",
-      "scope_type": "location",
-      "is_complete": false,
-      "reasoning": "地点限定,独立检索价值"
-    },
-    {
-      "text": "秋季",
-      "scope_type": "time",
-      "is_complete": false,
-      "reasoning": "时间限定,独立检索价值"
-    },
-    {
-      "text": "风光",
-      "scope_type": "feature",
-      "is_complete": false,
-      "reasoning": "主题特征"
-    },
-    {
-      "text": "摄影素材",
-      "scope_type": "object",
-      "is_complete": true,
-      "reasoning": "核心对象"
-    },
-    {
-      "text": "川西风光",
-      "scope_type": "composite",
-      "is_complete": false,
-      "reasoning": "地点+主题的组合,常见检索组合"
-    },
-    {
-      "text": "风光摄影素材",
-      "scope_type": "composite",
-      "is_complete": true,
-      "reasoning": "主题+对象的组合"
-    },
-    {
-      "text": "川西秋季风光摄影素材",
-      "scope_type": "full",
-      "is_complete": true,
-      "reasoning": "完整query,最精确匹配"
-    }
-  ],
-  "overall_reasoning": "纯名词短语,无谓语,包含地点、时间、主题、对象等多个维度,提供多个检索粒度"
-}
-
-## 示例3
-Input: "获取素材"
-
-Output:
-{
-  "interrogative": null,
-  "predicate": "获取",
-  "scopes": [
-    {
-      "text": "获取素材",
-      "scope_type": "predicate_object",
-      "is_complete": true,
-      "reasoning": "谓语+核心对象,简单的动宾结构"
-    }
-  ],
-  "overall_reasoning": "简单的动宾结构,scopes直接包含谓语与对象的组合"
-}
-
-## 输出要求
-- interrogative: 疑问词(可选)
-- predicate: 谓语动词(可选,纯名词短语时为null)
-- scopes: 作用域片段列表,包含多个检索粒度
-  - text: 作用域文本
-  - scope_type: 类型标注
-  - is_complete: 是否语义完整(标注用,不影响检索)
-  - reasoning: 识别理由
-- overall_reasoning: 整体分段思路
-
-## 注意事项
-1. 作用域可以重叠(如"川西"和"川西风光"都是有效的)
-2. 不要求每个作用域都语义完整
-3. 优先识别有实际检索价值的片段
-4. 避免过度拆分(如不要拆成单字)
-5. JSON中使用《》或「」代替引号
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=QuerySegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-    得分为零的原因: Optional[Literal["原始问题无动机", "sug词条无动机", "动机不匹配", "不适用"]] = Field(None, description="当得分为0时的原因分类(可选,仅SUG评估使用)")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-class ExtensionWordEvaluation(BaseModel):
-    """延伸词评估"""
-    延伸词得分: float = Field(..., ge=-1, le=1, description="延伸词得分 -1~1")
-    简要说明延伸词维度相关度理由: str = Field(..., description="延伸词维度相关度理由")
-
-# 动机评估 prompt(统一版本)
-motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:sug词条是原始问题的部分作用域
-
-当sug词条只包含原始问题的部分作用域时,需要判断:
-1. sug词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-Sug词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或sug词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-## 得分为零的原因(语义判断)
-
-当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
-- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
-- **"sug词条无动机"**:sug词条中不包含任何动作意图
-- **"动机不匹配"**:双方都有动作,但完全无关联
-- **"不适用"**:得分不为零时使用此默认值
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-# 角色
-你是**专业的内容主体评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
-
----
-
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估内容主体维度**:
-- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
-- **完全忽略**:动作、意图、目的
-- **评估重点**:内容本身的主题和属性
-
----
-
-# 作用域与内容主体
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-在Prompt2中:
-- **动机层(动作)完全忽略**
-- **只评估对象层 + 场景层(限定词)**
-
-## 内容主体的构成
-
-**内容主体 = 核心名词 + 限定词**
-
-
----
-
-# 作用域覆盖度评估
-
-## 核心原则:越完整越高分
-
-**完整性公式**:
-```
-作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
-```
-
-**评分影响**:
-- 覆盖度100% → 基础高分(0.9+)
-- 覆盖度50-99% → 中高分(0.6-0.9)
-- 覆盖度<50% → 中低分(0.3-0.6)
-- 覆盖度=0 → 低分或0分
-
----
-
-## 部分作用域的处理
-
-### 情况1:sug词条包含原始问题的所有对象层和场景层元素
-**评分**:0.95-1.0
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"川西秋季风光摄影作品"
-- 对象层:摄影作品(≈素材)
-- 场景层:川西 + 秋季 + 风光
-- 覆盖度:100%
-- 评分:0.98
-```
-
-### 情况2:sug词条包含部分场景层元素
-**评分**:根据覆盖比例
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光(3个元素)
-
-Sug词条:"川西风光摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:川西 + 风光(2个元素)
-- 覆盖度:(1+2)/(1+3) = 75%
-- 评分:0.85
-```
-
-### 情况3:sug词条只包含对象层,无场景层
-**评分**:根据对象匹配度和覆盖度
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:无
-- 覆盖度:1/4 = 25%
-- 评分:0.50(对象匹配但缺失所有限定)
-```
-
-### 情况4:sug词条只包含场景层,无对象层
-**评分**:较低分
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西"
-- 对象层:无
-- 场景层:川西 ✓
-- 覆盖度:1/2 = 50%
-- 评分:0.35(只有场景,缺失核心对象)
-```
-
----
-
-# 评估核心原则
-
-## 原则1:只看表面词汇,禁止联想推演
-**严格约束**:只能基于sug词实际包含的词汇评分
-
-**错误案例**:
-- ❌ "川西旅行" vs "旅行"
-  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
-  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **只看名词和限定词**:完全忽略动作和意图
-2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
-3. **禁止联想推演**:只看sug词实际包含的词汇
-4. **通用≠特定**:通用概念不等于特定概念
-5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
-""".strip()
-
-# 延伸词评估 prompt
-extension_word_evaluation_instructions = """
-# 角色
-你是**专业的延伸词语义评估专家**。
-任务:识别<平台sug词条>中的延伸词,评估其对原始问题作用域的补全度和目的贡献度,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-# 核心概念
-
-## 什么是延伸词?
-**延伸词**:<平台sug词条>中出现,但不属于<原始问题>作用域范围内的词汇或概念
-
-**关键判断**:
-```
-IF sug词的词汇属于原始问题的作用域元素(动机/对象/场景):
-   → 不是延伸词,是作用域内的词
-
-IF sug词的词汇不属于原始问题的作用域:
-   → 是延伸词
-   → 由Prompt3评估
-```
-
----
-
-# 作用域与延伸词
-
-## 作用域
-**作用域 = 动机层 + 对象层 + 场景层**
-
-**非延伸词示例**(属于作用域内):
-```
-原始问题:"川西旅行行程规划"
-作用域:
-- 动机层:规划
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西旅行行程规划攻略"
-- "川西"→ 属于场景层,不是延伸词
-- "旅行"→ 属于对象层,不是延伸词
-- "行程"→ 属于对象层,不是延伸词
-- "规划"→ 属于动机层,不是延伸词
-- "攻略"→ 与"规划"同义,不是延伸词
-- 结论:无延伸词
-```
-
-**延伸词示例**(不属于作用域):
-```
-原始问题:"川西旅行行程规划"
-作用域:规划 + 旅行行程 + 川西
-
-Sug词条:"川西旅行行程规划住宿推荐"
-- "住宿推荐"→ 不属于原始问题任何作用域
-- 结论:延伸词 = ["住宿推荐"]
-```
-
----
-
-# 延伸词识别方法
-
-## 步骤1:提取原始问题的作用域元素
-```
-动机层:提取动作及其同义词
-对象层:提取核心名词及其同义词
-场景层:提取所有限定词
-```
-
-## 步骤2:提取sug词条的所有关键词
-```
-提取sug词条中的所有实词(名词、动词、形容词)
-```
-
-## 步骤3:匹配判定
-```
-FOR 每个sug词条关键词:
-   IF 该词 ∈ 原始问题作用域元素(包括同义词):
-      → 不是延伸词
-   ELSE:
-      → 是延伸词
-```
-
-## 步骤4:同义词/相近词判定规则
-
-### 不算延伸词的情况:
-**同义词**:
-- 行程 ≈ 路线 ≈ 安排 ≈ 计划
-- 获取 ≈ 下载 ≈ 寻找 ≈ 收集
-- 技巧 ≈ 方法 ≈ 教程 ≈ 攻略
-- 素材 ≈ 资源 ≈ 作品 ≈ 内容
-
-**具体化/细化**:
-- 原始:"川西旅游" + sug词:"稻城亚丁"(川西的具体地点)→ 不算延伸
-- 原始:"摄影技巧" + sug词:"风光摄影"(摄影的细化)→ 不算延伸
-- 原始:"素材" + sug词:"高清素材"(素材的质量细化)→ 不算延伸
-
-**判定逻辑**:
-```
-IF sug词的概念是原始问题概念的子集/下位词/同义词:
-   → 不算延伸词
-   → 视为对原问题的细化或重述
-```
-
----
-
-### 算延伸词的情况:
-
-**新增维度**:原始问题未涉及的信息维度
-- 原始:"川西旅行" + sug词:"住宿" → 延伸词
-- 原始:"摄影素材" + sug词:"版权" → 延伸词
-
-**新增限定条件**:原始问题未提及的约束
-- 原始:"素材获取" + sug词:"免费" → 延伸词
-- 原始:"旅行行程" + sug词:"7天" → 延伸词
-
-**扩展主题**:相关但非原问题范围
-- 原始:"川西行程" + sug词:"美食推荐" → 延伸词
-- 原始:"摄影技巧" + sug词:"后期修图" → 延伸词
-
-**工具/方法**:原始问题未提及的具体工具
-- 原始:"视频剪辑" + sug词:"PR软件" → 延伸词
-- 原始:"图片处理" + sug词:"PS教程" → 延伸词
-
----
-
-# 延伸词类型与评分
-
-## 核心评估维度:对原始问题作用域的贡献
-
-### 维度1:作用域补全度
-延伸词是否帮助sug词条更接近原始问题的完整作用域?
-
-
-### 维度2:目的达成度
-延伸词是否促进原始问题核心目的的达成?
----
-####类型1:作用域增强型
-**定义**:延伸词是原始问题核心目的,或补全关键作用域
-**得分范围**:+0.12~+0.20
-
-**判定标准**:
-- 使sug词条更接近原始问题的完整需求
----
-
-####类型2:作用域辅助型
-**定义**:延伸词对核心目的有辅助作用,但非必需
-
-**得分范围**:+0.05~+0.12
-
-**判定标准**:
-- sug词条更丰富但不改变原始需求核心
-
----
-
-####类型3:作用域无关型
-**定义**:延伸词与核心目的无实质关联
-
-**得分**:0
-
-**示例**:
-- 原始:"如何拍摄风光" + 延伸词:"相机品牌排行"
-  - 评分:0
-  - 理由:品牌排行与拍摄技巧无关
-
----
-
-####类型4:作用域稀释型(轻度负向)
-**定义**:延伸词稀释原始问题的聚焦度,降低内容针对性
-
-**得分范围**:-0.08~-0.18
-
-**判定标准**:
-- 引入无关信息,分散注意力
-- 降低内容的专注度和深度
-- 使sug词条偏离原始问题的核心
-
-**示例**:
-- 原始:"专业风光摄影技巧" + 延伸词:"手机拍照"
-  - 评分:-0.12
-  - 理由:手机拍照与专业摄影需求不符,稀释专业度
-
-- 原始:"川西深度游攻略" + 延伸词:"周边一日游"
-  - 评分:-0.10
-  - 理由:一日游与深度游定位冲突,稀释深度
-
-
----
-
-# 特殊情况处理
-
-## 情况1:多个延伸词同时存在
-**处理方法**:分别评估每个延伸词,然后综合
-
-**综合规则**:
-```
-延伸词总得分 = Σ(每个延伸词得分) / 延伸词数量
-
-考虑累积效应:
-- 多个增强型延伸词 → 总分可能超过单个最高分,但上限+0.25
-- 正负延伸词并存 → 相互抵消
-- 多个冲突型延伸词 → 总分下限-0.60
-```
-
-**示例**:
-```
-原始:"川西旅行行程"
-Sug词条:"川西旅行行程住宿美食推荐"
-延伸词识别:
-- "住宿推荐"→ 增强型,+0.18
-- "美食推荐"→ 辅助型,+0.10
-总得分:(0.18 + 0.10) / 2 = 0.14
-```
-
----
-
-## 情况2:无延伸词
-**处理方法**:
-```
-IF sug词条无延伸词:
-   延伸词得分 = 0
-   理由:"sug词条未引入延伸词,所有词汇均属于原始问题作用域范围"
-```
-
----
-
-## 情况3:延伸词使sug词条更接近原始问题
-**特殊加成**:
-```
-IF 延伸词是原始问题隐含需求的显式化:
-   → 额外加成 +0.05
-```
-
-**示例**:
-```
-原始:"川西旅行" (隐含需要行程规划)
-Sug词条:"川西旅行行程规划"
-- "行程规划"可能被识别为延伸词,但它显式化了隐含需求
-- 给予额外加成
-```
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "延伸词得分": "-1到1之间的小数",
-  "简要说明延伸词维度相关度理由": "评估延伸词对作用域的影响"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明延伸词维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **严格区分**:作用域内的词 ≠ 延伸词
-2. **同义词/细化词不算延伸**:属于作用域范围的词由其他prompt评估
-3. **作用域导向**:评估延伸词是否使sug词条更接近原始问题的完整作用域
-4. **目的导向**:评估延伸词是否促进核心目的达成
-5. **分类明确**:准确判定延伸词类型
-6. **理由充分**:每个延伸词都要说明其对作用域和目的的影响
-7. **谨慎负分**:仅在明确冲突或有害时使用负分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-extension_word_evaluator = Agent[None](
-    name="延伸词评估专家",
-    instructions=extension_word_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=ExtensionWordEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# Round 0 专用 Agent(v124新增 - 需求1)
-# ============================================================================
-
-# Round 0 动机评估 prompt(不含延伸词)
-round0_motivation_evaluation_instructions = """
-#角色
-你是**专业的动机意图评估专家**
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:词条是原始问题的部分作用域
-
-当词条只包含原始问题的部分作用域时,需要判断:
-1. 词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
-
----
-
-#评分标准:
-
-【正向匹配】
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs 词条"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.90: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs 词条"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与原始问题动机匹配程度的理由"
-}
-```
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# Round 0 品类评估 prompt(不含延伸词)
-round0_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于词条实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs 词条:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "词条只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF 词条是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: 词条是通用概念,原始问题是特定概念
-    词条"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 词条也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs 词条"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs 词条"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs 词条"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与原始问题品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建 Round 0 评估 Agent
-round0_motivation_evaluator = Agent[None](
-    name="Round 0动机维度评估专家",
-    instructions=round0_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-round0_category_evaluator = Agent[None](
-    name="Round 0品类维度评估专家",
-    instructions=round0_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# 域内/域间 专用 Agent(v124新增 - 需求2&3)
-# ============================================================================
-
-# 域内/域间 动机评估 prompt(不含延伸词)
-scope_motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<词条>与<同一作用域词条>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
----
-# 评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-# 核心约束
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-当前任务:
-- **只提取动机层**:动作意图(获取、学习、规划、拍摄等)
-
-## 动作意图的识别
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - 词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与该条作用域匹配程度的理由",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 域内/域间 品类评估 prompt(不含延伸词)
-scope_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
----
-#判定流程
-#评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-#相关度评估维度详解
-
-##评估对象: <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与同一作用域词条品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建域内/域间评估 Agent
-scope_motivation_evaluator = Agent[None](
-    name="域内动机维度评估专家",
-    instructions=scope_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-   model_settings=ModelSettings(temperature=0.2)
-)
-
-scope_category_evaluator = Agent[None](
-    name="域内品类维度评估专家",
-    instructions=scope_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(
-    motivation_score: float,
-    category_score: float,
-    extension_score: float,
-    zero_reason: Optional[str],
-    extension_reason: str = ""
-) -> tuple[float, str]:
-    """
-    三维评估综合打分
-
-    实现动态权重分配:
-    - 情况1:标准情况 → 动机50% + 品类40% + 延伸词10%
-    - 情况2:原始问题无动机 → 品类70% + 延伸词30%
-    - 情况3:sug词条无动机 → 品类80% + 延伸词20%
-    - 情况4:无延伸词 → 动机70% + 品类30%
-    - 规则3:负分传导 → 核心维度严重负向时上限为0
-    - 规则4:完美匹配加成 → 双维度≥0.95时加成+0.10
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-        extension_score: 延伸词得分 -1~1
-        zero_reason: 当motivation_score=0时的原因(可选)
-        extension_reason: 延伸词评估理由,用于判断是否无延伸词
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    # 情况2:原始问题无动作意图
-    if motivation_score == 0 and zero_reason == "原始问题无动机":
-        W1, W2, W3 = 0.0, 0.70, 0.30
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况2:原始问题无动作意图,权重调整为 品类70% + 延伸词30%"
-
-    # 情况3:sug词条无动作意图(但原始问题有)
-    elif motivation_score == 0 and zero_reason == "sug词条无动机":
-        W1, W2, W3 = 0.0, 0.80, 0.20
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况3:sug词条无动作意图,权重调整为 品类80% + 延伸词20%"
-
-    # 情况4:无延伸词
-    elif extension_score == 0:
-        W1, W2, W3 = 0.70, 0.30, 0.0
-        base_score = motivation_score * W1 + category_score * W2
-        rule_applied = "情况4:无延伸词,权重调整为 动机70% + 品类30%"
-
-    else:
-        # 情况1:标准权重
-        W1, W2, W3 = 0.50, 0.40, 0.10
-        base_score = motivation_score * W1 + category_score * W2 + extension_score * W3
-        rule_applied = ""
-
-    # 规则4:完美匹配加成
-    if motivation_score >= 0.95 and category_score >= 0.95:
-        base_score += 0.10
-        rule_applied += (" + " if rule_applied else "") + "规则4:双维度完美匹配,加成+0.10"
-
-    # 规则3:负分传导
-    if motivation_score <= -0.5 or category_score <= -0.5:
-        base_score = min(base_score, 0)
-        rule_applied += (" + " if rule_applied else "") + "规则3:核心维度严重负向,上限=0"
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, base_score))
-
-    return final_score, rule_applied
-
-
-def calculate_final_score_v2(
-    motivation_score: float,
-    category_score: float
-) -> tuple[float, str]:
-    """
-    两维评估综合打分(v124新增 - 需求1)
-
-    用于Round 0分词评估和域内/域间评估,不含延伸词维度
-
-    基础权重:动机70% + 品类30%
-
-    应用规则:
-    - 规则A:动机高分保护机制
-      IF 动机维度得分 ≥ 0.8:
-         品类得分即使为0或轻微负向(-0.2~0)
-         → 最终得分应该不低于0.7
-      解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-    - 规则B:动机低分限制机制
-      IF 动机维度得分 ≤ 0.2:
-         无论品类得分多高
-         → 最终得分不高于0.5
-      解释: 目的不符时,品类匹配的价值有限
-
-    - 规则C:动机负向决定机制
-      IF 动机维度得分 < 0:
-         → 最终得分为0
-      解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    rule_applied = ""
-
-    # 规则C:动机负向决定机制
-    if motivation_score < 0:
-        final_score = 0.0
-        rule_applied = "规则C:动机负向,最终得分=0"
-        return final_score, rule_applied
-
-    # 基础加权计算: 动机70% + 品类30%
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则A:动机高分保护机制
-    if motivation_score >= 0.8:
-        if base_score < 0.7:
-            final_score = 0.7
-            rule_applied = f"规则A:动机高分保护(动机{motivation_score:.2f}≥0.8),最终得分下限=0.7"
-        else:
-            final_score = base_score
-            rule_applied = f"规则A:动机高分保护生效(动机{motivation_score:.2f}≥0.8),实际得分{base_score:.2f}已≥0.7"
-
-    # 规则B:动机低分限制机制
-    elif motivation_score <= 0.2:
-        if base_score > 0.5:
-            final_score = 0.5
-            rule_applied = f"规则B:动机低分限制(动机{motivation_score:.2f}≤0.2),最终得分上限=0.5"
-        else:
-            final_score = base_score
-            rule_applied = f"规则B:动机低分限制生效(动机{motivation_score:.2f}≤0.2),实际得分{base_score:.2f}已≤0.5"
-
-    # 无规则触发
-    else:
-        final_score = base_score
-        rule_applied = ""
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, final_score))
-
-    return final_score, rule_applied
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用三个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-            extension_task = Runner.run(extension_word_evaluator, eval_input)
-
-            motivation_result, category_result, extension_result = await asyncio.gather(
-                motivation_task,
-                category_task,
-                extension_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-            extension_eval: ExtensionWordEvaluation = extension_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-            extension_score = extension_eval.延伸词得分
-            zero_reason = motivation_eval.得分为零的原因
-
-            # 应用规则计算最终得分
-            final_score, rule_applied = calculate_final_score(
-                motivation_score, category_score, extension_score, zero_reason,
-                extension_eval.简要说明延伸词维度相关度理由
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-            extension_reason = extension_eval.简要说明延伸词维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【延伸词维度 {extension_score:.2f}】{extension_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_with_o_round0(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """Round 0专用评估函数(v124新增 - 需求1)
-
-    用于评估segment和word与原始问题的相关度
-    不含延伸词维度,使用Round 0专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本(segment或word)
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"round0:{text}:{o}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ Round0缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(round0_motivation_evaluator, eval_input)
-            category_task = Runner.run(round0_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  Round0评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ Round0评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"Round0评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_within_scope(text: str, scope_text: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """域内/域间专用评估函数(v124新增 - 需求2&3)
-
-    用于评估词条与作用域词条(单域或域组合)的相关度
-    不含延伸词维度,使用域内专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的词条
-        scope_text: 作用域词条(可以是单域词条或域组合词条)
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"scope:{text}:{scope_text}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ 域内缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<同一作用域词条>
-{scope_text}
-</同一作用域词条>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(scope_motivation_evaluator, eval_input)
-            category_task = Runner.run(scope_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 作用域词条"{scope_text}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  域内评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ 域内评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"域内评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7,
-    searched_texts: set[str] = None
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-        searched_texts: 已搜索过的词集合(跨轮次去重)
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    if searched_texts is None:
-        searched_texts = set()
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 跨轮次去重:过滤掉已搜索过的词
-        unsearched_sugs = []
-        for sug in high_score_sugs:
-            if sug.text in searched_texts:
-                print(f"  ⊗ 跳过已搜索: {sug.text}")
-            else:
-                unsearched_sugs.append(sug)
-
-        if not unsearched_sugs:
-            print(f"  所有高分建议词都已搜索过,search_list为空")
-        else:
-            print(f"  将搜索 {len(unsearched_sugs)} 个未搜索过的词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        if unsearched_sugs:
-            search_tasks = [search_for_sug(sug) for sug in unsearched_sugs]
-            search_list = await asyncio.gather(*search_tasks)
-            # 将本轮搜索的词添加到searched_texts
-            for sug in unsearched_sugs:
-                searched_texts.add(sug.text)
-        else:
-            search_list = []
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 跨轮次搜索去重集合
-    searched_texts = set()
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold,
-            searched_texts=searched_texts  # 传递已搜索词集合
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段(谓语+作用域方式)
-    print(f"\n[步骤1] 语义分段(谓语+作用域)...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: QuerySegmentation = result.final_output
-
-    # 统计作用域数量
-    total_scopes = len(segmentation.scopes)
-    has_interrogative = segmentation.interrogative is not None
-    has_predicate = segmentation.predicate is not None
-
-    print(f"语义分段结果:")
-    print(f"  - 疑问词: {segmentation.interrogative or '无'}")
-    print(f"  - 谓语: {segmentation.predicate or '无'}")
-    print(f"  - 作用域片段: {total_scopes} 个")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-
-    # 1.1 处理疑问词
-    if segmentation.interrogative:
-        segment = Segment(
-            text=segmentation.interrogative,
-            type="interrogative",
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [interrogative] {segment.text}")
-
-    # 1.2 处理作用域片段(scopes已经包含谓语)
-    for scope_item in segmentation.scopes:
-        segment = Segment(
-            text=scope_item.text,
-            type="scope",
-            scope_type=scope_item.scope_type,
-            is_complete=scope_item.is_complete,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [scope:{scope_item.scope_type}] {segment.text} (完整性: {scope_item.is_complete})")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度(使用Round 0专用评估)
-            segment.score_with_o, segment.reason = await evaluate_with_o_round0(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度(使用Round 0专用评估)
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o_round0(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(新增scope_type和is_complete字段)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "scope_type": seg.scope_type,  # 新增:作用域类型
-            "is_complete": seg.is_complete,  # 新增:是否完整
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "scope_type": seg.scope_type,  # 新增
-                "is_complete": seg.is_complete,  # 新增
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7,
-    searched_texts: set[str] = None
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-        searched_texts: 已搜索过的词集合(跨轮次去重)
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    if searched_texts is None:
-        searched_texts = set()
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    # 跨轮次去重:过滤掉已搜索过的词
-    unsearched_sugs = []
-    for sug in high_score_sugs:
-        if sug.text in searched_texts:
-            print(f"  ⊗ 跳过已搜索: {sug.text}")
-        else:
-            unsearched_sugs.append(sug)
-
-    if not unsearched_sugs:
-        print(f"  所有高分SUG都已搜索过,search_list为空")
-    else:
-        print(f"  将搜索 {len(unsearched_sugs)} 个未搜索过的词")
-
-    search_list = []
-    if len(unsearched_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in unsearched_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-        # 将本轮搜索的词添加到searched_texts
-        for sug in unsearched_sugs:
-            searched_texts.add(sug.text)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            # 使用域内评估:组合词条与拼接的segments作为作用域进行评估
-            # 拼接所有参与组合的segments文本
-            scope_text = "".join(comb.from_segments)
-
-            comb.score_with_o, comb.reason = await evaluate_within_scope(
-                comb.text,      # 组合结果,如 "获取川西"
-                scope_text,     # 拼接的segments,如 "获取川西秋季风光摄影素材"
-                context.evaluation_cache
-            )
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    print(f"  评估完成,共{len(domain_combinations)}个组合:")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(组合得分 > 所有来源词)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 跨轮次搜索去重集合
-    searched_texts = set()
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold,
-            searched_texts=searched_texts  # 传递已搜索词集合
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 3941
sug_v6_1_2_127.py

@@ -1,3941 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal, Optional
-
-from agents import Agent, Runner, ModelSettings
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-# 得分提升阈值:sug或组合词必须比来源query提升至少此幅度才能进入下一轮
-REQUIRED_SCORE_GAIN = 0.02
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 日志工具类
-# ============================================================================
-
-class TeeLogger:
-    """同时输出到控制台和日志文件的工具类"""
-    def __init__(self, stdout, log_file):
-        self.stdout = stdout
-        self.log_file = log_file
-
-    def write(self, message):
-        self.stdout.write(message)
-        self.log_file.write(message)
-        self.log_file.flush()  # 实时写入,避免丢失日志
-
-    def flush(self):
-        self.stdout.flush()
-        self.log_file.flush()
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词(旧版)- v120使用"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-# ============================================================================
-# 新架构数据模型 (v121)
-# ============================================================================
-
-class Segment(BaseModel):
-    """语义片段(Round 0语义分段结果)"""
-    text: str  # 片段文本
-    type: str  # 语义类型: 疑问标记/核心动作/修饰短语/中心名词/逻辑连接
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-    words: list[str] = Field(default_factory=list)  # 该片段拆分出的词列表(Round 0拆词结果)
-    word_scores: dict[str, float] = Field(default_factory=dict)  # 词的评分 {word: score}
-    word_reasons: dict[str, str] = Field(default_factory=dict)  # 词的评分理由 {word: reason}
-
-
-class DomainCombination(BaseModel):
-    """域组合(Round N的N域组合结果)"""
-    text: str  # 组合后的文本
-    domains: list[int] = Field(default_factory=list)  # 参与组合的域索引列表(对应segments的索引)
-    type_label: str = ""  # 类型标签,如 [疑问标记+核心动作+中心名词]
-    source_words: list[list[str]] = Field(default_factory=list)  # 来源词列表,每个元素是一个域的词列表,如 [["猫咪"], ["梗图"]]
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_segments: list[str] = Field(default_factory=list)  # 来源segment的文本列表
-    source_word_details: list[dict] = Field(default_factory=list)  # 词及其得分信息 [{"domain_index":0,"segment_type":"","words":[{"text":"","score":0.0}]}]
-    source_scores: list[float] = Field(default_factory=list)  # 来源词的分数列表(扁平化)
-    max_source_score: float | None = None  # 来源词的最高分
-    is_above_source_scores: bool = False  # 组合得分是否超过所有来源词
-
-
-# ============================================================================
-# 旧架构数据模型(保留但不使用)
-# ============================================================================
-
-# class Word(BaseModel):
-#     """词(旧版)- v120使用,v121不再使用"""
-#     text: str
-#     score_with_o: float = 0.0  # 与原始问题的评分
-#     from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # v120: seg/sug/add; v121新增: segment/domain_comb/sug
-    type_label: str = ""  # v121新增:域类型标签(仅用于domain_comb来源)
-    domain_index: int = -1  # v121新增:域索引(word来源时有效,-1表示无域)
-    domain_type: str = ""  # v121新增:域类型(word来源时表示所属segment的type,如"中心名词")
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子(旧版)- v120使用,v121不再使用"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug/add
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # v121新增:语义分段结果
-    segments: list[dict] = Field(default_factory=list)  # Round 0的语义分段结果
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-    # 评估缓存:避免重复评估相同文本
-    evaluation_cache: dict[str, tuple[float, str]] = Field(default_factory=dict)
-    # key: 文本, value: (score, reason)
-
-    # 历史词/组合得分追踪(用于Round 2+计算系数)
-    word_score_history: dict[str, float] = Field(default_factory=dict)
-    # key: 词/组合文本, value: 最终得分
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# ============================================================================
-# v121 新增 Agent
-# ============================================================================
-
-# Agent: 语义分段专家 (Prompt1)
-class SemanticSegment(BaseModel):
-    """单个语义片段"""
-    segment_text: str = Field(..., description="片段文本")
-    segment_type: str = Field(..., description="语义类型(疑问标记/核心动作/修饰短语/中心名词/逻辑连接)")
-    reasoning: str = Field(..., description="分段理由")
-
-
-class SemanticSegmentation(BaseModel):
-    """语义分段结果"""
-    segments: list[SemanticSegment] = Field(..., description="语义片段列表")
-    overall_reasoning: str = Field(..., description="整体分段思路")
-
-
-semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成不同语义类型的片段。
-
-## 语义类型定义
-1. **疑问引导**:如何、怎么、什么、哪里等疑问词
-2. **核心动作**:关键动词,如获取、制作、拍摄、寻找等
-3. **修饰短语**:形容词、副词等修饰成分
-4. **中心名词**:核心名词
-5. **逻辑连接**:并且、或者、以及等连接词(较少出现)
-
-## 分段原则
-1. **语义完整性**:每个片段应该是一个完整的语义单元
-2. **类型互斥**:每个片段只能属于一种类型
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
-4. **顺序保持**:片段顺序应与原query一致
-
-
-## 输出要求
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义类型(从5种类型中选择)
-  - reasoning: 为什么这样分段
-- overall_reasoning: 整体分段思路
-
-## JSON输出规范
-1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
-""".strip()
-
-semantic_segmenter = Agent[None](
-    name="语义分段专家",
-    instructions=semantic_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=SemanticSegmentation,
-)
-
-
-# ============================================================================
-# v120 保留 Agent
-# ============================================================================
-
-# Agent 1: 分词专家(v121用于Round 0拆词)
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 动机维度评估专家 + 品类维度评估专家(两阶段评估)
-
-# 动机评估的嵌套模型
-class CoreMotivationExtraction(BaseModel):
-    """核心动机提取"""
-    简要说明核心动机: str = Field(..., description="核心动机说明")
-
-class MotivationEvaluation(BaseModel):
-    """动机维度评估"""
-    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
-    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
-    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
-    得分为零的原因: Optional[Literal["原始问题无动机", "sug词条无动机", "动机不匹配", "不适用"]] = Field(None, description="当得分为0时的原因分类(可选,仅SUG评估使用)")
-
-class CategoryEvaluation(BaseModel):
-    """品类维度评估"""
-    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
-    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
-
-class ExtensionWordEvaluation(BaseModel):
-    """延伸词评估"""
-    延伸词得分: float = Field(..., ge=-1, le=1, description="延伸词得分 -1~1")
-    简要说明延伸词维度相关度理由: str = Field(..., description="延伸词维度相关度理由")
-
-# 动机评估 prompt(统一版本)
-motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:sug词条是原始问题的部分作用域
-
-当sug词条只包含原始问题的部分作用域时,需要判断:
-1. sug词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-Sug词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或sug词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-## 得分为零的原因(语义判断)
-
-当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
-- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
-- **"sug词条无动机"**:sug词条中不包含任何动作意图
-- **"动机不匹配"**:双方都有动作,但完全无关联
-- **"不适用"**:得分不为零时使用此默认值
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 品类评估 prompt
-category_evaluation_instructions = """
-# 角色
-你是**专业的内容主体评估专家**。
-任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
-
----
-
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估内容主体维度**:
-- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
-- **完全忽略**:动作、意图、目的
-- **评估重点**:内容本身的主题和属性
-
----
-
-# 作用域与内容主体
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-在Prompt2中:
-- **动机层(动作)完全忽略**
-- **只评估对象层 + 场景层(限定词)**
-
-## 内容主体的构成
-
-**内容主体 = 核心名词 + 限定词**
-
-
----
-
-# 作用域覆盖度评估
-
-## 核心原则:越完整越高分
-
-**完整性公式**:
-```
-作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
-```
-
-**评分影响**:
-- 覆盖度100% → 基础高分(0.9+)
-- 覆盖度50-99% → 中高分(0.6-0.9)
-- 覆盖度<50% → 中低分(0.3-0.6)
-- 覆盖度=0 → 低分或0分
-
----
-
-## 部分作用域的处理
-
-### 情况1:sug词条包含原始问题的所有对象层和场景层元素
-**评分**:0.95-1.0
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"川西秋季风光摄影作品"
-- 对象层:摄影作品(≈素材)
-- 场景层:川西 + 秋季 + 风光
-- 覆盖度:100%
-- 评分:0.98
-```
-
-### 情况2:sug词条包含部分场景层元素
-**评分**:根据覆盖比例
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光(3个元素)
-
-Sug词条:"川西风光摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:川西 + 风光(2个元素)
-- 覆盖度:(1+2)/(1+3) = 75%
-- 评分:0.85
-```
-
-### 情况3:sug词条只包含对象层,无场景层
-**评分**:根据对象匹配度和覆盖度
-
-**示例**:
-```
-原始问题:"川西秋季风光摄影素材"
-- 对象层:摄影素材
-- 场景层:川西 + 秋季 + 风光
-
-Sug词条:"摄影素材"
-- 对象层:摄影素材 ✓
-- 场景层:无
-- 覆盖度:1/4 = 25%
-- 评分:0.50(对象匹配但缺失所有限定)
-```
-
-### 情况4:sug词条只包含场景层,无对象层
-**评分**:较低分
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西"
-- 对象层:无
-- 场景层:川西 ✓
-- 覆盖度:1/2 = 50%
-- 评分:0.35(只有场景,缺失核心对象)
-```
-
----
-
-# 评估核心原则
-
-## 原则1:只看表面词汇,禁止联想推演
-**严格约束**:只能基于sug词实际包含的词汇评分
-
-**错误案例**:
-- ❌ "川西旅行" vs "旅行"
-  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
-  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **只看名词和限定词**:完全忽略动作和意图
-2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
-3. **禁止联想推演**:只看sug词实际包含的词汇
-4. **通用≠特定**:通用概念不等于特定概念
-5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
-""".strip()
-
-# 延伸词评估 prompt
-extension_word_evaluation_instructions = """
-# 角色
-你是**专业的延伸词语义评估专家**。
-任务:识别<平台sug词条>中的延伸词,评估其对原始问题作用域的补全度和目的贡献度,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-- **<原始问题>**:用户的完整需求描述
-- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
----
-
-# 核心概念
-
-## 什么是延伸词?
-**延伸词**:<平台sug词条>中出现,但不属于<原始问题>作用域范围内的词汇或概念
-
-**关键判断**:
-```
-IF sug词的词汇属于原始问题的作用域元素(动机/对象/场景):
-   → 不是延伸词,是作用域内的词
-
-IF sug词的词汇不属于原始问题的作用域:
-   → 是延伸词
-   → 由Prompt3评估
-```
-
----
-
-# 作用域与延伸词
-
-## 作用域
-**作用域 = 动机层 + 对象层 + 场景层**
-
-**非延伸词示例**(属于作用域内):
-```
-原始问题:"川西旅行行程规划"
-作用域:
-- 动机层:规划
-- 对象层:旅行行程
-- 场景层:川西
-
-Sug词条:"川西旅行行程规划攻略"
-- "川西"→ 属于场景层,不是延伸词
-- "旅行"→ 属于对象层,不是延伸词
-- "行程"→ 属于对象层,不是延伸词
-- "规划"→ 属于动机层,不是延伸词
-- "攻略"→ 与"规划"同义,不是延伸词
-- 结论:无延伸词
-```
-
-**延伸词示例**(不属于作用域):
-```
-原始问题:"川西旅行行程规划"
-作用域:规划 + 旅行行程 + 川西
-
-Sug词条:"川西旅行行程规划住宿推荐"
-- "住宿推荐"→ 不属于原始问题任何作用域
-- 结论:延伸词 = ["住宿推荐"]
-```
-
----
-
-# 延伸词识别方法
-
-## 步骤1:提取原始问题的作用域元素
-```
-动机层:提取动作及其同义词
-对象层:提取核心名词及其同义词
-场景层:提取所有限定词
-```
-
-## 步骤2:提取sug词条的所有关键词
-```
-提取sug词条中的所有实词(名词、动词、形容词)
-```
-
-## 步骤3:匹配判定
-```
-FOR 每个sug词条关键词:
-   IF 该词 ∈ 原始问题作用域元素(包括同义词):
-      → 不是延伸词
-   ELSE:
-      → 是延伸词
-```
-
-## 步骤4:同义词/相近词判定规则
-
-### 不算延伸词的情况:
-**同义词**:
-- 行程 ≈ 路线 ≈ 安排 ≈ 计划
-- 获取 ≈ 下载 ≈ 寻找 ≈ 收集
-- 技巧 ≈ 方法 ≈ 教程 ≈ 攻略
-- 素材 ≈ 资源 ≈ 作品 ≈ 内容
-
-**具体化/细化**:
-- 原始:"川西旅游" + sug词:"稻城亚丁"(川西的具体地点)→ 不算延伸
-- 原始:"摄影技巧" + sug词:"风光摄影"(摄影的细化)→ 不算延伸
-- 原始:"素材" + sug词:"高清素材"(素材的质量细化)→ 不算延伸
-
-**判定逻辑**:
-```
-IF sug词的概念是原始问题概念的子集/下位词/同义词:
-   → 不算延伸词
-   → 视为对原问题的细化或重述
-```
-
----
-
-### 算延伸词的情况:
-
-**新增维度**:原始问题未涉及的信息维度
-- 原始:"川西旅行" + sug词:"住宿" → 延伸词
-- 原始:"摄影素材" + sug词:"版权" → 延伸词
-
-**新增限定条件**:原始问题未提及的约束
-- 原始:"素材获取" + sug词:"免费" → 延伸词
-- 原始:"旅行行程" + sug词:"7天" → 延伸词
-
-**扩展主题**:相关但非原问题范围
-- 原始:"川西行程" + sug词:"美食推荐" → 延伸词
-- 原始:"摄影技巧" + sug词:"后期修图" → 延伸词
-
-**工具/方法**:原始问题未提及的具体工具
-- 原始:"视频剪辑" + sug词:"PR软件" → 延伸词
-- 原始:"图片处理" + sug词:"PS教程" → 延伸词
-
----
-
-# 延伸词类型与评分
-
-## 核心评估维度:对原始问题作用域的贡献
-
-### 维度1:作用域补全度
-延伸词是否帮助sug词条更接近原始问题的完整作用域?
-
-
-### 维度2:目的达成度
-延伸词是否促进原始问题核心目的的达成?
----
-####类型1:作用域增强型
-**定义**:延伸词是原始问题核心目的,或补全关键作用域
-**得分范围**:+0.12~+0.20
-
-**判定标准**:
-- 使sug词条更接近原始问题的完整需求
----
-
-####类型2:作用域辅助型
-**定义**:延伸词对核心目的有辅助作用,但非必需
-
-**得分范围**:+0.05~+0.12
-
-**判定标准**:
-- sug词条更丰富但不改变原始需求核心
-
----
-
-####类型3:作用域无关型
-**定义**:延伸词与核心目的无实质关联
-
-**得分**:0
-
-**示例**:
-- 原始:"如何拍摄风光" + 延伸词:"相机品牌排行"
-  - 评分:0
-  - 理由:品牌排行与拍摄技巧无关
-
----
-
-####类型4:作用域稀释型(轻度负向)
-**定义**:延伸词稀释原始问题的聚焦度,降低内容针对性
-
-**得分范围**:-0.08~-0.18
-
-**判定标准**:
-- 引入无关信息,分散注意力
-- 降低内容的专注度和深度
-- 使sug词条偏离原始问题的核心
-
-**示例**:
-- 原始:"专业风光摄影技巧" + 延伸词:"手机拍照"
-  - 评分:-0.12
-  - 理由:手机拍照与专业摄影需求不符,稀释专业度
-
-- 原始:"川西深度游攻略" + 延伸词:"周边一日游"
-  - 评分:-0.10
-  - 理由:一日游与深度游定位冲突,稀释深度
-
-
----
-
-# 特殊情况处理
-
-## 情况1:多个延伸词同时存在
-**处理方法**:分别评估每个延伸词,然后综合
-
-**综合规则**:
-```
-延伸词总得分 = Σ(每个延伸词得分) / 延伸词数量
-
-考虑累积效应:
-- 多个增强型延伸词 → 总分可能超过单个最高分,但上限+0.25
-- 正负延伸词并存 → 相互抵消
-- 多个冲突型延伸词 → 总分下限-0.60
-```
-
-**示例**:
-```
-原始:"川西旅行行程"
-Sug词条:"川西旅行行程住宿美食推荐"
-延伸词识别:
-- "住宿推荐"→ 增强型,+0.18
-- "美食推荐"→ 辅助型,+0.10
-总得分:(0.18 + 0.10) / 2 = 0.14
-```
-
----
-
-## 情况2:无延伸词
-**处理方法**:
-```
-IF sug词条无延伸词:
-   延伸词得分 = 0
-   理由:"sug词条未引入延伸词,所有词汇均属于原始问题作用域范围"
-```
-
----
-
-## 情况3:延伸词使sug词条更接近原始问题
-**特殊加成**:
-```
-IF 延伸词是原始问题隐含需求的显式化:
-   → 额外加成 +0.05
-```
-
-**示例**:
-```
-原始:"川西旅行" (隐含需要行程规划)
-Sug词条:"川西旅行行程规划"
-- "行程规划"可能被识别为延伸词,但它显式化了隐含需求
-- 给予额外加成
-```
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "延伸词得分": "-1到1之间的小数",
-  "简要说明延伸词维度相关度理由": "评估延伸词对作用域的影响"
-}
-```
-
-**输出约束(非常重要)**:
-1. **字符串长度限制**:\"简要说明延伸词维度相关度理由\"字段必须控制在**150字以内**
-2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
-3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
-
----
-
-# 核心原则总结
-
-1. **严格区分**:作用域内的词 ≠ 延伸词
-2. **同义词/细化词不算延伸**:属于作用域范围的词由其他prompt评估
-3. **作用域导向**:评估延伸词是否使sug词条更接近原始问题的完整作用域
-4. **目的导向**:评估延伸词是否促进核心目的达成
-5. **分类明确**:准确判定延伸词类型
-6. **理由充分**:每个延伸词都要说明其对作用域和目的的影响
-7. **谨慎负分**:仅在明确冲突或有害时使用负分
-""".strip()
-
-# 创建评估 Agent
-motivation_evaluator = Agent[None](
-    name="动机维度评估专家(后续轮次)",
-    instructions=motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation)
-
-category_evaluator = Agent[None](
-    name="品类维度评估专家",
-    instructions=category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation
-)
-
-extension_word_evaluator = Agent[None](
-    name="延伸词评估专家",
-    instructions=extension_word_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=ExtensionWordEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# Round 0 专用 Agent(v124新增 - 需求1)
-# ============================================================================
-
-# Round 0 动机评估 prompt(不含延伸词)
-round0_motivation_evaluation_instructions = """
-#角色
-你是**专业的动机意图评估专家**
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的需求动机匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-# 核心约束
-
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-## 动作意图的识别
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 在此情况下,动机维度得分应为 0。
-示例:
-"摄影" → 无法识别动机,动机维度得分 = 0
-"川西风光" → 无法识别动机,动机维度得分 = 0
-
----
-
-# 部分作用域的处理
-
-## 情况1:词条是原始问题的部分作用域
-
-当词条只包含原始问题的部分作用域时,需要判断:
-1. 词条是否包含动作意图
-2. 如果包含,动作是否匹配
-
-**示例**:
-```
-原始问题:"川西旅行行程规划"
-- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
-
-词条:"川西旅行"
-- 包含作用域:旅行(部分对象)+ 川西(场景)
-- 缺失作用域:规划(动作)
-- 动作意图评分:0(无动作意图)
-```
-
-**评分原则**:
-- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
-- 如果sug词条包含动机层 → 按动作匹配度评分
-
-
----
-
-#评分标准:
-
-【正向匹配】
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs 词条"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.90: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs 词条"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - sug词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "sug词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与原始问题动机匹配程度的理由"
-}
-```
-
-#注意事项:
-始终围绕动机维度:所有评估都基于"动机"维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题动机产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题动机无明确关联,既不相关也不冲突时给予零分,或原始问题无法识别动机时。
-""".strip()
-
-# Round 0 品类评估 prompt(不含延伸词)
-round0_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <原始问题> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于词条实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs 词条:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "词条只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF 词条是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<原始问题>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-
-相关度评估维度详解
-维度2: 品类维度评估
-评估对象: <词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: 词条是通用概念,原始问题是特定概念
-    词条"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs 词条"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 词条也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs 词条"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs 词条"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs 词条"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与原始问题品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建 Round 0 评估 Agent
-round0_motivation_evaluator = Agent[None](
-    name="Round 0动机维度评估专家",
-    instructions=round0_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-round0_category_evaluator = Agent[None](
-    name="Round 0品类维度评估专家",
-    instructions=round0_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# 域内/域间 专用 Agent(v124新增 - 需求2&3)
-# ============================================================================
-
-# 域内/域间 动机评估 prompt(不含延伸词)
-scope_motivation_evaluation_instructions = """
-# 角色
-你是**专业的动机意图评估专家**。
-任务:判断<词条>与<同一作用域词条>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
----
-# 评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【动机维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的需求动机匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
-# 核心约束
-## 维度独立性声明
-【严格约束】本评估**仅评估动机意图维度**:
-- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-- **评估重点**:动作本身及其语义方向
- **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
-
----
-
-# 作用域与动作意图
-
-## 什么是作用域?
-**作用域 = 动机层 + 对象层 + 场景层**
-
-当前任务:
-- **只提取动机层**:动作意图(获取、学习、规划、拍摄等)
-
-## 动作意图的识别
-
-### 1. 动机维度
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-当原始问题没有显性动词时,需要结合上下文推理
-
-
----
-
-# 评分标准
-
-## 【正向匹配】
-
-### +0.9~1.0:核心动作完全一致
-**示例**:
-- "规划旅行行程" vs "安排旅行路线" → 0.98
-  - 规划≈安排,语义完全一致
-- "获取素材" vs "下载素材" → 0.97
-  - 获取≈下载,语义完全一致
-
-- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
-**注意**:此处不考虑对象和场景是否一致,只看动作本身
-
-###+0.75~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-### +0.50~0.75:动作意图相关
-**判定标准**:
-- 动作是实现原始意图的相关路径
-- 或动作是原始意图的前置/后置步骤
-
-**示例**:
-- "获取素材" vs "管理素材" → 0.65
-  - 管理是获取后的相关步骤
-- "规划行程" vs "预订酒店" → 0.60
-  - 预订是规划的具体实施步骤
-
-### +0.25~0.50:动作意图弱相关
-**判定标准**:
-- 动作在同一大类但方向不同
-- 或动作有间接关联
-
-**示例**:
-- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
-  - 都与摄影有关,但学习≠欣赏
-- "规划旅行" vs "回忆旅行" → 0.30
-  - 都与旅行有关,但方向不同
-
----
-
-## 【中性/无关】
-
-### 0:无动作意图或动作完全无关
-**适用场景**:
-1. 原始问题或词条无法识别动作
-2. 两者动作意图完全无关
-
-**示例**:
-- "如何获取素材" vs "摄影器材" → 0
-  - 词条无动作意图
-- "川西风光" vs "风光摄影作品" → 0
-  - 原始问题无动作意图
-
-**理由模板**:
-- "词条无明确动作意图,无法评估动作匹配度"
-- "原始问题无明确动作意图,动作维度得分为0"
-
----
-
-## 【负向偏离】
-
-### -0.2~-0.05:动作方向轻度偏离
-**示例**:
-- "学习摄影技巧" vs "销售摄影课程" → -0.10
-  - 学习 vs 销售,方向有偏差
-
-### -0.5~-0.25:动作意图明显冲突
-**示例**:
-- "获取免费素材" vs "购买素材" → -0.35
-  - 获取免费 vs 购买,明显冲突
-
-### -1.0~-0.55:动作意图完全相反
-**示例**:
-- "下载素材" vs "上传素材" → -0.70
-  - 下载 vs 上传,方向完全相反
-
----
-
-# 输出格式
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "原始问题核心动机提取": {
-    "简要说明核心动机": ""
-  },
-  "动机维度得分": "-1到1之间的小数",
-  "简要说明动机维度相关度理由": "评估该词条与该条作用域匹配程度的理由",
-  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
-}
-```
-
----
-
-# 核心原则总结
-1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
-2. **作用域识别**:识别作用域但只评估动机层
-3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
-4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
-""".strip()
-
-# 域内/域间 品类评估 prompt(不含延伸词)
-scope_category_evaluation_instructions = """
-#角色
-你是一个 **专业的语言专家和语义相关性评判专家**。
-你的任务是:判断我给你的 <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度,给出 **-1 到 1 之间** 的数值评分。
-
----
-# 输入信息
-你将接收到以下输入:
-- **<同一作用域词条>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
----
-#判定流程
-#评估架构
-
-输入: <同一作用域词条> + <词条>
-         ↓
-【品类维度相关性判定】
-    ├→ 步骤1: 评估<词条>与<同一作用域词条>的内容主体和限定词匹配度
-    └→ 输出: -1到1之间的数值 + 判定依据
-
----
-
-# 核心概念与方法论
-
-## 评估维度
-本评估系统围绕 **品类维度** 进行:
-
-#  维度独立性警告
-【严格约束】本评估**只评估品类维度**,,必须遵守以下规则:
-1. **只看名词和限定词**:评估时只考虑主体、限定词的匹配度
-2. **完全忽略动词**:动作意图、目的等动机信息对本维度评分无影响
-
-### 品类维度
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
-## ⚠️ 品类评估核心原则(必读)
-
-### 原则1:只看词条表面,禁止联想推演
-- 只能基于sug词实际包含的词汇评分
-- 禁止推测"可能包含"、"可以理解为"
-
-**错误示例:**
-原始问题:"川西旅行行程" vs sug词:"每日计划"
-- 错误 "每日计划可以包含旅行规划,所以有关联" → 这是不允许的联想
-- 正确: "sug词只有'每日计划',无'旅行'字眼,品类不匹配" → 正确判断
-
-### 原则2:通用概念 ≠ 特定概念
-- **通用**:计划、方法、技巧、素材(无领域限定)
-- **特定**:旅行行程、摄影技巧、烘焙方法(有明确领域)
-
-IF sug词是通用 且 原始问题是特定:
-   → 品类不匹配 → 评分0.05~0.1
-关键:通用概念不等于特定概念,不能因为"抽象上都是规划"就给分
-
----
-#相关度评估维度详解
-
-##评估对象: <词条> 与 <同一作用域词条> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+0.95~1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,存在限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
-  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
-  - 例:
-    · "猫咪的XX行为"(猫咪是行为者)
-    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
-    · 虽都含"猫咪+XX",但语义角色不同
-
-+0.2~0.3: 主体词不匹配,限定词缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.2: 主体词过度泛化或仅抽象相似
-  - 例: sug词是通用概念,原始问题是特定概念
-    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
-      → 评分:0.08
-
-【中性/无关】
-0: 类别明显不同,没有明确目的,无明确关联
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
----
-
-# 输出要求
-
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-```json
-{
-  "品类维度得分": "-1到1之间的小数",
-  "简要说明品类维度相关度理由": "评估该词条与同一作用域词条品类匹配程度的理由"
-}
-```
----
-
-#注意事项:
-始终围绕品类维度:所有评估都基于"品类"维度,不偏离
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当词条对原始问题品类产生误导、冲突或有害引导时给予负分
-零分使用原则:当词条与原始问题品类无明确关联,既不相关也不冲突时给予零分
-""".strip()
-
-# 创建域内/域间评估 Agent
-scope_motivation_evaluator = Agent[None](
-    name="域内动机维度评估专家",
-    instructions=scope_motivation_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=MotivationEvaluation,
-   model_settings=ModelSettings(temperature=0.2)
-)
-
-scope_category_evaluator = Agent[None](
-    name="域内品类维度评估专家",
-    instructions=scope_category_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=CategoryEvaluation,
-    model_settings=ModelSettings(temperature=0.2)
-)
-
-
-# ============================================================================
-# v120 保留但不使用的 Agent(v121不再使用)
-# ============================================================================
-
-# # Agent 3: 加词选择专家(旧版 - v120使用,v121不再使用)
-# class WordCombination(BaseModel):
-#     """单个词组合"""
-#     selected_word: str = Field(..., description="选择的词")
-#     combined_query: str = Field(..., description="组合后的新query")
-#     reasoning: str = Field(..., description="选择理由")
-
-# class WordSelectionTop5(BaseModel):
-#     """加词选择结果(Top 5)"""
-#     combinations: list[WordCombination] = Field(
-#         ...,
-#         description="选择的Top 5组合(不足5个则返回所有)",
-#         min_items=1,
-#         max_items=5
-#     )
-#     overall_reasoning: str = Field(..., description="整体选择思路")
-
-# word_selection_instructions 已删除 (v121不再使用)
-
-# word_selector = Agent[None](
-#     name="加词组合专家",
-#     instructions=word_selection_instructions,
-#     model=get_model(MODEL_NAME),
-#     output_type=WordSelectionTop5,
-#     model_settings=ModelSettings(temperature=0.2),
-# )
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-# ============================================================================
-# v121 新增辅助函数
-# ============================================================================
-
-def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
-    """
-    生成words的所有有序子集(可跳过但不可重排)
-
-    使用 itertools.combinations 生成索引组合,保持原始顺序
-
-    Args:
-        words: 词列表
-        min_len: 子集最小长度
-
-    Returns:
-        所有可能的有序子集列表
-
-    Example:
-        words = ["川西", "秋季", "风光"]
-        结果:
-        - 长度1: ["川西"], ["秋季"], ["风光"]
-        - 长度2: ["川西", "秋季"], ["川西", "风光"], ["秋季", "风光"]
-        - 长度3: ["川西", "秋季", "风光"]
-        共 C(3,1) + C(3,2) + C(3,3) = 3 + 3 + 1 = 7种
-    """
-    from itertools import combinations
-
-    subsets = []
-    n = len(words)
-
-    # 遍历所有可能的长度(从min_len到n)
-    for r in range(min_len, n + 1):
-        # 生成长度为r的所有索引组合
-        for indices in combinations(range(n), r):
-            # 按照原始顺序提取词
-            subset = [words[i] for i in indices]
-            subsets.append(subset)
-
-    return subsets
-
-
-def generate_domain_combinations(segments: list[Segment], n_domains: int) -> list[DomainCombination]:
-    """
-    生成N域组合
-
-    步骤:
-    1. 从len(segments)个域中选择n_domains个域(组合,保持顺序)
-    2. 对每个选中的域,生成其words的所有有序子集
-    3. 计算笛卡尔积,生成所有可能的组合
-
-    Args:
-        segments: 语义片段列表
-        n_domains: 参与组合的域数量
-
-    Returns:
-        所有可能的N域组合列表
-
-    Example:
-        有4个域: [疑问标记, 核心动作, 修饰短语, 中心名词]
-        n_domains=2时,选择域的方式: C(4,2) = 6种
-
-        假设选中[核心动作, 中心名词]:
-        - 核心动作的words: ["获取"], 子集: ["获取"]
-        - 中心名词的words: ["风光", "摄影", "素材"], 子集: 7种
-        则该域选择下的组合数: 1 * 7 = 7种
-    """
-    from itertools import combinations, product
-
-    all_combinations = []
-    n = len(segments)
-
-    # 检查参数有效性
-    if n_domains > n or n_domains < 1:
-        return []
-
-    # 1. 选择n_domains个域(保持原始顺序)
-    for domain_indices in combinations(range(n), n_domains):
-        selected_segments = [segments[i] for i in domain_indices]
-
-        # 新增:如果所有域都只有1个词,跳过(单段落单词不组合)
-        if all(len(seg.words) == 1 for seg in selected_segments):
-            continue
-
-        # 2. 为每个选中的域生成其words的所有有序子集
-        domain_subsets = []
-        for seg in selected_segments:
-            if len(seg.words) == 0:
-                # 如果某个域没有词,跳过该域组合
-                domain_subsets = []
-                break
-            subsets = get_ordered_subsets(seg.words, min_len=1)
-            domain_subsets.append(subsets)
-
-        # 如果某个域没有词,跳过
-        if len(domain_subsets) != n_domains:
-            continue
-
-        # 3. 计算笛卡尔积
-        for word_combination in product(*domain_subsets):
-            # word_combination 是一个tuple,每个元素是一个词列表
-            # 例如: (["获取"], ["风光", "摄影"])
-
-            # 计算总词数
-            total_words = sum(len(words) for words in word_combination)
-
-            # 如果总词数<=1,跳过(组词必须大于1个词)
-            if total_words <= 1:
-                continue
-
-            # 将所有词连接成一个字符串
-            combined_text = "".join(["".join(words) for words in word_combination])
-
-            # 生成类型标签
-            type_labels = [selected_segments[i].type for i in range(n_domains)]
-            type_label = "[" + "+".join(type_labels) + "]"
-
-            # 创建DomainCombination对象
-            comb = DomainCombination(
-                text=combined_text,
-                domains=list(domain_indices),
-                type_label=type_label,
-                source_words=[list(words) for words in word_combination],  # 保存来源词
-                from_segments=[seg.text for seg in selected_segments]
-            )
-            all_combinations.append(comb)
-
-    return all_combinations
-
-
-def extract_words_from_segments(segments: list[Segment]) -> list[Q]:
-    """
-    从 segments 中提取所有 words,转换为 Q 对象列表
-
-    用于 Round 1 的输入:将 Round 0 的 words 转换为可用于请求SUG的 query 列表
-
-    Args:
-        segments: Round 0 的语义片段列表
-
-    Returns:
-        list[Q]: word 列表,每个 word 作为一个 Q 对象
-    """
-    q_list = []
-
-    for seg_idx, segment in enumerate(segments):
-        for word in segment.words:
-            # 从 segment.word_scores 获取该 word 的评分
-            word_score = segment.word_scores.get(word, 0.0)
-            word_reason = segment.word_reasons.get(word, "")
-
-            # 创建 Q 对象
-            q = Q(
-                text=word,
-                score_with_o=word_score,
-                reason=word_reason,
-                from_source="word",  # 标记来源为 word
-                type_label=f"[{segment.type}]",  # 保留域信息
-                domain_index=seg_idx,  # 添加域索引
-                domain_type=segment.type  # 添加域类型(如"中心名词"、"核心动作")
-            )
-            q_list.append(q)
-
-    return q_list
-
-
-# ============================================================================
-# v120 保留辅助函数
-# ============================================================================
-
-def calculate_final_score(
-    motivation_score: float,
-    category_score: float,
-    extension_score: float,
-    zero_reason: Optional[str],
-    extension_reason: str = ""
-) -> tuple[float, str]:
-    """
-    三维评估综合打分
-
-    实现动态权重分配:
-    - 情况1:标准情况 → 动机50% + 品类40% + 延伸词10%
-    - 情况2:原始问题无动机 → 品类70% + 延伸词30%
-    - 情况3:sug词条无动机 → 品类80% + 延伸词20%
-    - 情况4:无延伸词 → 动机70% + 品类30%
-    - 规则3:负分传导 → 核心维度严重负向时上限为0
-    - 规则4:完美匹配加成 → 双维度≥0.95时加成+0.10
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-        extension_score: 延伸词得分 -1~1
-        zero_reason: 当motivation_score=0时的原因(可选)
-        extension_reason: 延伸词评估理由,用于判断是否无延伸词
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    # 情况2:原始问题无动作意图
-    if motivation_score == 0 and zero_reason == "原始问题无动机":
-        W1, W2, W3 = 0.0, 0.70, 0.30
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况2:原始问题无动作意图,权重调整为 品类70% + 延伸词30%"
-
-    # 情况3:sug词条无动作意图(但原始问题有)
-    elif motivation_score == 0 and zero_reason == "sug词条无动机":
-        W1, W2, W3 = 0.0, 0.80, 0.20
-        base_score = category_score * W2 + extension_score * W3
-        rule_applied = "情况3:sug词条无动作意图,权重调整为 品类80% + 延伸词20%"
-
-    # 情况4:无延伸词
-    elif extension_score == 0:
-        W1, W2, W3 = 0.70, 0.30, 0.0
-        base_score = motivation_score * W1 + category_score * W2
-        rule_applied = "情况4:无延伸词,权重调整为 动机70% + 品类30%"
-
-    else:
-        # 情况1:标准权重
-        W1, W2, W3 = 0.50, 0.40, 0.10
-        base_score = motivation_score * W1 + category_score * W2 + extension_score * W3
-        rule_applied = ""
-
-    # 规则4:完美匹配加成
-    if motivation_score >= 0.95 and category_score >= 0.95:
-        base_score += 0.10
-        rule_applied += (" + " if rule_applied else "") + "规则4:双维度完美匹配,加成+0.10"
-
-    # 规则3:负分传导
-    if motivation_score <= -0.5 or category_score <= -0.5:
-        base_score = min(base_score, 0)
-        rule_applied += (" + " if rule_applied else "") + "规则3:核心维度严重负向,上限=0"
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, base_score))
-
-    return final_score, rule_applied
-
-
-def calculate_final_score_v2(
-    motivation_score: float,
-    category_score: float
-) -> tuple[float, str]:
-    """
-    两维评估综合打分(v124新增 - 需求1)
-
-    用于Round 0分词评估和域内/域间评估,不含延伸词维度
-
-    基础权重:动机70% + 品类30%
-
-    应用规则:
-    - 规则A:动机高分保护机制
-      IF 动机维度得分 ≥ 0.8:
-         品类得分即使为0或轻微负向(-0.2~0)
-         → 最终得分应该不低于0.7
-      解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-    - 规则B:动机低分限制机制
-      IF 动机维度得分 ≤ 0.2:
-         无论品类得分多高
-         → 最终得分不高于0.5
-      解释: 目的不符时,品类匹配的价值有限
-
-    - 规则C:动机负向决定机制
-      IF 动机维度得分 < 0:
-         → 最终得分为0
-      解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-    Args:
-        motivation_score: 动机维度得分 -1~1
-        category_score: 品类维度得分 -1~1
-
-    Returns:
-        (最终得分, 规则说明)
-    """
-
-    rule_applied = ""
-
-    # 规则C:动机负向决定机制
-    if motivation_score < 0:
-        final_score = 0.0
-        rule_applied = "规则C:动机负向,最终得分=0"
-        return final_score, rule_applied
-
-    # 基础加权计算: 动机70% + 品类30%
-    base_score = motivation_score * 0.7 + category_score * 0.3
-
-    # 规则A:动机高分保护机制
-    if motivation_score >= 0.8:
-        if base_score < 0.7:
-            final_score = 0.7
-            rule_applied = f"规则A:动机高分保护(动机{motivation_score:.2f}≥0.8),最终得分下限=0.7"
-        else:
-            final_score = base_score
-            rule_applied = f"规则A:动机高分保护生效(动机{motivation_score:.2f}≥0.8),实际得分{base_score:.2f}已≥0.7"
-
-    # 规则B:动机低分限制机制
-    elif motivation_score <= 0.2:
-        if base_score > 0.5:
-            final_score = 0.5
-            rule_applied = f"规则B:动机低分限制(动机{motivation_score:.2f}≤0.2),最终得分上限=0.5"
-        else:
-            final_score = base_score
-            rule_applied = f"规则B:动机低分限制生效(动机{motivation_score:.2f}≤0.2),实际得分{base_score:.2f}已≤0.5"
-
-    # 无规则触发
-    else:
-        final_score = base_score
-        rule_applied = ""
-
-    # 边界处理
-    final_score = max(-1.0, min(1.0, final_score))
-
-    return final_score, rule_applied
-
-
-def clean_json_string(text: str) -> str:
-    """清理JSON中的非法控制字符(保留 \t \n \r)"""
-    import re
-    # 移除除了 \t(09) \n(0A) \r(0D) 之外的所有控制字符
-    return re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', text)
-
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # ========== 调试日志 START ==========
-    note_id = note.get("id", "")
-    raw_title = note_card.get("display_title")  # 不提供默认值
-    raw_body = note_card.get("desc")
-    raw_type = note_card.get("type")
-
-    # 打印原始值类型和内容
-    print(f"\n[DEBUG] 处理帖子 {note_id}:")
-    print(f"  raw_title 类型: {type(raw_title).__name__}, 值: {repr(raw_title)}")
-    print(f"  raw_body 类型: {type(raw_body).__name__}, 值: {repr(raw_body)[:100] if raw_body else repr(raw_body)}")
-    print(f"  raw_type 类型: {type(raw_type).__name__}, 值: {repr(raw_type)}")
-
-    # 检查是否为 None
-    if raw_title is None:
-        print(f"  ⚠️  WARNING: display_title 是 None!")
-    if raw_body is None:
-        print(f"  ⚠️  WARNING: desc 是 None!")
-    if raw_type is None:
-        print(f"  ⚠️  WARNING: type 是 None!")
-    # ========== 调试日志 END ==========
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id") or "",
-        title=note_card.get("display_title") or "",
-        body_text=note_card.get("desc") or "",
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    采用两阶段评估 + 代码计算规则:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    if cache is not None and text in cache:
-        cached_score, cached_reason = cache[text]
-        print(f"  ⚡ 缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<平台sug词条>
-{text}
-</平台sug词条>
-
-请评估平台sug词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用三个评估器
-            motivation_task = Runner.run(motivation_evaluator, eval_input)
-            category_task = Runner.run(category_evaluator, eval_input)
-            extension_task = Runner.run(extension_word_evaluator, eval_input)
-
-            motivation_result, category_result, extension_result = await asyncio.gather(
-                motivation_task,
-                category_task,
-                extension_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-            extension_eval: ExtensionWordEvaluation = extension_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-            extension_score = extension_eval.延伸词得分
-            zero_reason = motivation_eval.得分为零的原因
-
-            # 应用规则计算最终得分
-            final_score, rule_applied = calculate_final_score(
-                motivation_score, category_score, extension_score, zero_reason,
-                extension_eval.简要说明延伸词维度相关度理由
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-            extension_reason = extension_eval.简要说明延伸词维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【延伸词维度 {extension_score:.2f}】{extension_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[text] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)  # 等待1秒后重试
-            else:
-                print(f"  ❌ 评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_with_o_round0(text: str, o: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """Round 0专用评估函数(v124新增 - 需求1)
-
-    用于评估segment和word与原始问题的相关度
-    不含延伸词维度,使用Round 0专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的文本(segment或word)
-        o: 原始问题
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"round0:{text}:{o}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ Round0缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与原始问题的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(round0_motivation_evaluator, eval_input)
-            category_task = Runner.run(round0_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 原始问题"{o}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  Round0评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ Round0评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"Round0评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-async def evaluate_within_scope(text: str, scope_text: str, cache: dict[str, tuple[float, str]] | None = None) -> tuple[float, str]:
-    """域内/域间专用评估函数(v124新增 - 需求2&3)
-
-    用于评估词条与作用域词条(单域或域组合)的相关度
-    不含延伸词维度,使用域内专用Prompt和新评分逻辑
-
-    采用两维评估:
-    1. 动机维度评估(权重70%)
-    2. 品类维度评估(权重30%)
-    3. 应用规则A/B/C调整得分
-
-    Args:
-        text: 待评估的词条
-        scope_text: 作用域词条(可以是单域词条或域组合词条)
-        cache: 评估缓存(可选),用于避免重复评估
-
-    Returns:
-        tuple[float, str]: (最终相关度分数, 综合评估理由)
-    """
-    # 检查缓存
-    cache_key = f"scope:{text}:{scope_text}"  # 添加前缀以区分不同评估类型
-    if cache is not None and cache_key in cache:
-        cached_score, cached_reason = cache[cache_key]
-        print(f"  ⚡ 域内缓存命中: {text} -> {cached_score:.2f}")
-        return cached_score, cached_reason
-
-    # 准备输入
-    eval_input = f"""
-<同一作用域词条>
-{scope_text}
-</同一作用域词条>
-
-<词条>
-{text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-    # 添加重试机制
-    max_retries = 2
-    last_error = None
-
-    for attempt in range(max_retries):
-        try:
-            # 并发调用两个评估器(不含延伸词)
-            motivation_task = Runner.run(scope_motivation_evaluator, eval_input)
-            category_task = Runner.run(scope_category_evaluator, eval_input)
-
-            motivation_result, category_result = await asyncio.gather(
-                motivation_task,
-                category_task
-            )
-
-            # 获取评估结果
-            motivation_eval: MotivationEvaluation = motivation_result.final_output
-            category_eval: CategoryEvaluation = category_result.final_output
-
-            # 提取得分
-            motivation_score = motivation_eval.动机维度得分
-            category_score = category_eval.品类维度得分
-
-            # 应用新规则计算最终得分
-            final_score, rule_applied = calculate_final_score_v2(
-                motivation_score, category_score
-            )
-
-            # 组合评估理由
-            core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-            motivation_reason = motivation_eval.简要说明动机维度相关度理由
-            category_reason = category_eval.简要说明品类维度相关度理由
-
-            combined_reason = (
-                f'【评估对象】词条"{text}" vs 作用域词条"{scope_text}"\n'
-                f"【核心动机】{core_motivation}\n"
-                f"【动机维度 {motivation_score:.2f}】{motivation_reason}\n"
-                f"【品类维度 {category_score:.2f}】{category_reason}\n"
-                f"【最终得分 {final_score:.2f}】"
-            )
-
-            # 添加规则说明
-            if rule_applied:
-                combined_reason += f"\n【规则说明】{rule_applied}"
-
-            # 存入缓存
-            if cache is not None:
-                cache[cache_key] = (final_score, combined_reason)
-
-            return final_score, combined_reason
-
-        except Exception as e:
-            last_error = e
-            error_msg = str(e)
-
-            if attempt < max_retries - 1:
-                print(f"  ⚠️  域内评估失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:150]}")
-                print(f"  正在重试...")
-                await asyncio.sleep(1)
-            else:
-                print(f"  ❌ 域内评估失败 (已达最大重试次数): {error_msg[:150]}")
-
-    # 所有重试失败后,返回默认值
-    fallback_reason = f"域内评估失败(重试{max_retries}次): {str(last_error)[:200]}"
-    print(f"  使用默认值: score=0.0, reason={fallback_reason[:100]}...")
-    return 0.0, fallback_reason
-
-
-# ============================================================================
-# v125 新增辅助函数(用于新评分逻辑)
-# ============================================================================
-
-def get_source_word_score(
-    word_text: str,
-    segment: Segment,
-    context: RunContext
-) -> float:
-    """
-    查找来源词的得分
-
-    查找顺序:
-    1. 先查 segment.word_scores (Round 0的单个词)
-    2. 再查 context.word_score_history (Round 1+的组合)
-
-    Args:
-        word_text: 词文本
-        segment: 该词所在的segment
-        context: 运行上下文
-
-    Returns:
-        词的得分,找不到返回0.0
-    """
-    # 优先查Round 0的词得分
-    if word_text in segment.word_scores:
-        return segment.word_scores[word_text]
-
-    # 其次查历史组合得分
-    if word_text in context.word_score_history:
-        return context.word_score_history[word_text]
-
-    # 都找不到
-    print(f"  ⚠️  警告: 未找到来源词得分: {word_text}")
-    return 0.0
-
-
-async def evaluate_domain_combination_round1(
-    comb: DomainCombination,
-    segments: list[Segment],
-    context: RunContext
-) -> tuple[float, str]:
-    """
-    Round 1 域内组合评估(新逻辑)
-
-    最终得分 = 品类得分 × 原始域得分
-
-    Args:
-        comb: 域内组合对象
-        segments: 所有segment列表
-        context: 运行上下文
-
-    Returns:
-        (最终得分, 评估理由)
-    """
-    # 获取所属segment
-    domain_idx = comb.domains[0] if comb.domains else 0
-    segment = segments[domain_idx] if 0 <= domain_idx < len(segments) else None
-
-    if not segment:
-        return 0.0, "错误: 无法找到所属segment"
-
-    # 拼接作用域文本
-    scope_text = segment.text
-
-    # 准备输入
-    eval_input = f"""
-<同一作用域词条>
-{scope_text}
-</同一作用域词条>
-
-<词条>
-{comb.text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-    # 只调用品类评估器
-    try:
-        category_result = await Runner.run(scope_category_evaluator, eval_input)
-        category_eval: CategoryEvaluation = category_result.final_output
-        category_score = category_eval.品类维度得分
-        category_reason = category_eval.简要说明品类维度相关度理由
-    except Exception as e:
-        print(f"  ❌ Round 1品类评估失败: {e}")
-        return 0.0, f"评估失败: {str(e)[:100]}"
-
-    # 计算最终得分
-    domain_score = segment.score_with_o
-    final_score = category_score * domain_score
-
-    # 组合评估理由
-    combined_reason = (
-        f'【Round 1 域内评估】\n'
-        f'【评估对象】组合"{comb.text}" vs 作用域"{scope_text}"\n'
-        f'【品类得分】{category_score:.2f} - {category_reason}\n'
-        f'【原始域得分】{domain_score:.2f}\n'
-        f'【计算公式】品类得分 × 域得分 = {category_score:.2f} × {domain_score:.2f}\n'
-        f'【最终得分】{final_score:.2f}'
-    )
-
-    return final_score, combined_reason
-
-
-async def evaluate_domain_combination_round2plus(
-    comb: DomainCombination,
-    segments: list[Segment],
-    context: RunContext
-) -> tuple[float, str]:
-    """
-    Round 2+ 域间组合评估(新逻辑 - 两步评估相乘)
-
-    步骤:
-    1. 计算全域组合得分 A: 全域组合 vs 原始query(动机+品类两维)
-    2. 计算部分组合得分 B: 部分组合 vs 全域组合(域内评估)
-    3. 最终得分 = A × B,截断到1.0
-
-    Args:
-        comb: 域间组合对象
-        segments: 所有segment列表
-        context: 运行上下文
-
-    Returns:
-        (最终得分, 评估理由)
-    """
-    # 全域组合文本 = 拼接所有参与组合的segments
-    full_domain_text = "".join(comb.from_segments)
-
-    # 步骤1: 计算全域组合得分 A
-    # 全域组合 vs 原始问题(动机+品类两维评估)
-    score_A, reason_A = await evaluate_with_o_round0(
-        full_domain_text,
-        context.o,  # 原始问题
-        context.evaluation_cache
-    )
-
-    # 步骤2: 计算部分组合得分 B
-    # 部分组合 vs 全域组合(域内评估)
-    # 🆕 手动评估,以支持动机得分为0时的特殊权重规则
-
-    # 检查缓存
-    cache_key = f"scope:{comb.text}:{full_domain_text}"
-    if cache_key in context.evaluation_cache:
-        score_B, reason_B = context.evaluation_cache[cache_key]
-        print(f"  ⚡ 域内缓存命中: {comb.text} -> {score_B:.2f}")
-    else:
-        # 准备评估输入
-        eval_input_step2 = f"""
-<同一作用域词条>
-{full_domain_text}
-</同一作用域词条>
-
-<词条>
-{comb.text}
-</词条>
-
-请评估词条与同一作用域词条的匹配度。
-"""
-
-        # 并发调用动机和品类评估器
-        motivation_task = Runner.run(scope_motivation_evaluator, eval_input_step2)
-        category_task = Runner.run(scope_category_evaluator, eval_input_step2)
-
-        motivation_result, category_result = await asyncio.gather(
-            motivation_task,
-            category_task
-        )
-
-        # 获取评估结果
-        motivation_eval: MotivationEvaluation = motivation_result.final_output
-        category_eval: CategoryEvaluation = category_result.final_output
-
-        # 提取得分
-        motivation_score_B = motivation_eval.动机维度得分
-        category_score_B = category_eval.品类维度得分
-
-        # 🆕 根据动机得分调整权重
-        if motivation_score_B == 0:
-            # 动机为0,只使用品类得分
-            score_B = category_score_B
-            weight_rule = "动机得分=0,权重调整为:品类100%"
-        else:
-            # 动机不为0,使用标准规则
-            score_B, rule_applied = calculate_final_score_v2(
-                motivation_score_B, category_score_B
-            )
-            weight_rule = rule_applied if rule_applied else "标准权重:动机70% + 品类30%"
-
-        # 组合评估理由
-        core_motivation = motivation_eval.原始问题核心动机提取.简要说明核心动机
-        motivation_reason = motivation_eval.简要说明动机维度相关度理由
-        category_reason = category_eval.简要说明品类维度相关度理由
-
-        reason_B = (
-            f'【评估对象】词条"{comb.text}" vs 作用域词条"{full_domain_text}"\n'
-            f"【核心动机】{core_motivation}\n"
-            f"【动机维度 {motivation_score_B:.2f}】{motivation_reason}\n"
-            f"【品类维度 {category_score_B:.2f}】{category_reason}\n"
-            f"【权重规则】{weight_rule}\n"
-            f"【最终得分 {score_B:.2f}】"
-        )
-
-        # 缓存结果
-        context.evaluation_cache[cache_key] = (score_B, reason_B)
-
-    # 步骤3: 计算最终得分
-    final_score = score_A * score_B
-    final_score = min(1.0, max(-1.0, final_score))  # 截断到[-1.0, 1.0]
-
-    # 组合评估理由
-    combined_reason = (
-        f'【Round 2+ 域间评估(两步评估相乘)】\n'
-        f'【评估对象】部分组合 "{comb.text}"\n'
-        f'\n'
-        f'【步骤1: 全域组合得分 A】\n'
-        f'  全域组合文本: "{full_domain_text}"\n'
-        f'  评估方式: 全域组合 vs 原始问题(动机+品类两维)\n'
-        f'  {reason_A}\n'
-        f'  得分A = {score_A:.2f}\n'
-        f'\n'
-        f'【步骤2: 部分组合得分 B】\n'
-        f'  部分组合文本: "{comb.text}"\n'
-        f'  评估方式: 部分组合 vs 全域组合(域内评估)\n'
-        f'  {reason_B}\n'
-        f'  得分B = {score_B:.2f}\n'
-        f'\n'
-        f'【最终得分】A × B = {score_A:.2f} × {score_B:.2f} = {score_A * score_B:.2f}\n'
-        f'【截断后】{final_score:.2f}'
-    )
-
-    return final_score, combined_reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(使用信号量限制并发数)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    MAX_CONCURRENT_SEG_EVALUATIONS = 10
-    seg_semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEG_EVALUATIONS)
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        async with seg_semaphore:
-            # 初始化阶段的分词评估使用第一轮 prompt (round_num=1)
-            seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o, context.evaluation_cache, round_num=1)
-            return seg
-
-    if seg_list:
-        print(f"  开始评估 {len(seg_list)} 个分词(并发限制: {MAX_CONCURRENT_SEG_EVALUATIONS})...")
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1(固定词库)
-    print(f"\n[步骤3] 构建word_list_1(固定词库)...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1(固定): {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list_1: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list_1: 固定的词库(第0轮分词结果)
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
-        "input_word_list_1_size": len(word_list_1),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug(使用信号量限制并发数)
-    # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def evaluate_sug(sug: Sug) -> Sug:
-        async with semaphore:  # 限制并发数
-            # 根据轮次选择 prompt: 第一轮使用 round1 prompt,后续使用标准 prompt
-            sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o, context.evaluation_cache, round_num=round_num)
-            return sug
-
-    if all_sugs:
-        print(f"  开始评估 {len(all_sugs)} 个建议词(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 2.4 剪枝判断(已禁用 - 保留所有分支)
-    pruned_query_texts = set()
-    if False:  # 原: if round_num >= 2:  # 剪枝功能已禁用,保留代码以便后续调整
-        print(f"\n[剪枝判断] 第{round_num}轮开始应用剪枝策略...")
-        for i, q in enumerate(q_list):
-            q_sug_list = sug_list_list[i]
-
-            if len(q_sug_list) == 0:
-                continue  # 没有sug则不剪枝
-
-            # 剪枝条件1: 所有sug分数都低于query分数
-            all_lower_than_query = all(sug.score_with_o < q.score_with_o for sug in q_sug_list)
-            # 剪枝条件2: 所有sug分数都低于0.5
-            all_below_threshold = all(sug.score_with_o < 0.5 for sug in q_sug_list)
-
-            if all_lower_than_query and all_below_threshold:
-                pruned_query_texts.add(q.text)
-                max_sug_score = max(sug.score_with_o for sug in q_sug_list)
-                print(f"  🔪 剪枝: {q.text} (query分数:{q.score_with_o:.2f}, sug最高分:{max_sug_score:.2f}, 全部<0.5)")
-
-        if pruned_query_texts:
-            print(f"  本轮共剪枝 {len(pruned_query_texts)} 个query")
-        else:
-            print(f"  本轮无query被剪枝")
-    else:
-        print(f"\n[剪枝判断] 剪枝功能已禁用,保留所有分支")
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建q_list_next
-    print(f"\n[步骤4] 构建q_list_next...")
-    q_list_next = []
-    existing_q_texts = set()  # 用于去重
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-    all_seed_combinations = []  # 保存本轮所有seed的组合词(用于后续构建seed_list_next)
-
-    # 4.1 对于seed_list中的每个seed,从word_list_1中选词组合,产生Top 5
-    print(f"\n  4.1 为每个seed加词(产生Top 5组合)...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 剪枝检查:跳过被剪枝的seed
-        if seed.text in pruned_query_texts:
-            print(f"      ⊗ 跳过被剪枝的seed: {seed.text}")
-            continue
-
-        # 从固定词库word_list_1筛选候选词
-        candidate_words = []
-        for word in word_list_1:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词数量: {len(candidate_words)}")
-
-        # 调用Agent一次性选择并组合Top 5(添加重试机制)
-        candidate_words_text = ', '.join([w.text for w in candidate_words])
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{candidate_words_text}
-</候选词列表>
-
-请从候选词列表中选择最多5个最合适的词,分别与当前seed组合成新的query。
-"""
-
-        # 重试机制
-        max_retries = 2
-        selection_result = None
-        for attempt in range(max_retries):
-            try:
-                result = await Runner.run(word_selector, selection_input)
-                selection_result = result.final_output
-                break  # 成功则跳出
-            except Exception as e:
-                error_msg = str(e)
-                if attempt < max_retries - 1:
-                    print(f"      ⚠️  选词失败 (尝试 {attempt+1}/{max_retries}): {error_msg[:100]}")
-                    await asyncio.sleep(1)
-                else:
-                    print(f"      ❌ 选词失败,跳过该seed: {error_msg[:100]}")
-                    break
-
-        if selection_result is None:
-            print(f"      跳过seed: {seed.text}")
-            continue
-
-        print(f"      Agent选择了 {len(selection_result.combinations)} 个组合")
-        print(f"      整体选择思路: {selection_result.overall_reasoning}")
-
-        # 并发评估所有组合的相关度
-        async def evaluate_combination(comb: WordCombination) -> dict:
-            combined = comb.combined_query
-
-            # 验证:组合结果必须包含完整的seed和word
-            # 检查是否包含seed的所有字符
-            seed_chars_in_combined = all(char in combined for char in seed.text)
-            # 检查是否包含word的所有字符
-            word_chars_in_combined = all(char in combined for char in comb.selected_word)
-
-            if not seed_chars_in_combined or not word_chars_in_combined:
-                print(f"        ⚠️  警告:组合不完整")
-                print(f"          Seed: {seed.text}")
-                print(f"          Word: {comb.selected_word}")
-                print(f"          组合: {combined}")
-                print(f"          包含完整seed? {seed_chars_in_combined}")
-                print(f"          包含完整word? {word_chars_in_combined}")
-                # 返回极低分数,让这个组合不会被选中
-                return {
-                    'word': comb.selected_word,
-                    'query': combined,
-                    'score': -1.0,  # 极低分数
-                    'reason': f"组合不完整:缺少seed或word的部分内容",
-                    'reasoning': comb.reasoning
-                }
-
-            # 正常评估,根据轮次选择 prompt
-            score, reason = await evaluate_with_o(combined, o, context.evaluation_cache, round_num=round_num)
-            return {
-                'word': comb.selected_word,
-                'query': combined,
-                'score': score,
-                'reason': reason,
-                'reasoning': comb.reasoning
-            }
-
-        eval_tasks = [evaluate_combination(comb) for comb in selection_result.combinations]
-        top_5 = await asyncio.gather(*eval_tasks)
-
-        print(f"      评估完成,得到 {len(top_5)} 个组合")
-
-        # 将Top 5全部加入q_list_next(去重检查 + 得分过滤)
-        for comb in top_5:
-            # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-            if comb['score'] < seed.score_with_o + REQUIRED_SCORE_GAIN:
-                print(f"        ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-                continue
-
-            # 去重检查
-            if comb['query'] in existing_q_texts:
-                print(f"        ⊗ 跳过重复: {comb['query']}")
-                continue
-
-            print(f"        ✓ {comb['query']} (分数: {comb['score']:.2f} > 种子: {seed.score_with_o:.2f})")
-
-            new_q = Q(
-                text=comb['query'],
-                score_with_o=comb['score'],
-                reason=comb['reason'],
-                from_source="add"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(comb['query'])  # 记录到去重集合
-
-            # 记录已添加的词
-            seed.added_words.append(comb['word'])
-
-        # 保存到add_word_details
-        add_word_details[seed.text] = [
-            {
-                "text": comb['query'],
-                "score": comb['score'],
-                "reason": comb['reason'],
-                "selected_word": comb['word'],
-                "seed_score": seed.score_with_o,  # 添加原始种子的得分
-                "type": "add"
-            }
-            for comb in top_5
-        ]
-
-        # 保存到all_seed_combinations(用于构建seed_list_next)
-        # 附加seed_score,用于后续过滤
-        for comb in top_5:
-            comb['seed_score'] = seed.score_with_o
-        all_seed_combinations.extend(top_5)
-
-    # 4.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next(去重检查)
-    print(f"\n  4.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            print(f"    ⊗ 跳过来自被剪枝query的sug: {sug.text} (来源: {sug.from_q.text})")
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才能加入下一轮
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            # 去重检查
-            if sug.text in existing_q_texts:
-                print(f"    ⊗ 跳过重复: {sug.text}")
-                continue
-
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            existing_q_texts.add(sug.text)  # 记录到去重集合
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5. 构建seed_list_next(关键修改:不保留上一轮的seed)
-    print(f"\n[步骤5] 构建seed_list_next(不保留上轮seed)...")
-    seed_list_next = []
-    existing_seed_texts = set()
-
-    # 5.1 加入本轮所有组合词(只加入得分提升的)
-    print(f"  5.1 加入本轮所有组合词(得分过滤)...")
-    for comb in all_seed_combinations:
-        # 得分过滤:组合词必须比种子提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        seed_score = comb.get('seed_score', 0)
-        if comb['score'] < seed_score + REQUIRED_SCORE_GAIN:
-            print(f"    ⊗ 跳过低分: {comb['query']} (分数{comb['score']:.2f} < 种子{seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-            continue
-
-        if comb['query'] not in existing_seed_texts:
-            new_seed = Seed(
-                text=comb['query'],
-                added_words=[],  # 新seed的added_words清空
-                from_type="add",
-                score_with_o=comb['score']
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(comb['query'])
-            print(f"    ✓ {comb['query']} (分数: {comb['score']:.2f} >= 种子: {seed_score:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 5.2 加入高分sug
-    print(f"  5.2 加入高分sug...")
-    for sug in all_sugs:
-        # 剪枝检查:跳过来自被剪枝query的sug
-        if sug.from_q and sug.from_q.text in pruned_query_texts:
-            continue
-
-        # sug必须比来源query提升至少REQUIRED_SCORE_GAIN才作为下一轮种子
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} >= 来源query: {sug.from_q.score_with_o:.2f} + {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "total_combinations": len(all_seed_combinations),
-        "pruned_query_count": len(pruned_query_texts),
-        "pruned_queries": list(pruned_query_texts),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
-        "sug_details": sug_details,
-        "add_word_details": add_word_details,
-        "search_results": search_results_data
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  组合词数量: {len(all_seed_combinations)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  下轮seed数量: {len(seed_list_next)}")
-
-    return q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list_1, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list_1=word_list_1,  # 传递固定词库
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# v121 新架构核心流程函数
-# ============================================================================
-
-async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
-    """
-    v121 Round 0 初始化阶段
-
-    流程:
-    1. 语义分段: 调用 semantic_segmenter 将原始问题拆分成语义片段
-    2. 拆词: 对每个segment调用 word_segmenter 进行拆词
-    3. 评估: 对每个segment和词进行评估
-    4. 不进行组合(Round 0只分段和拆词)
-
-    Returns:
-        语义片段列表 (Segment)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round 0: 初始化阶段(语义分段 + 拆词)")
-    print(f"{'='*60}")
-
-    # 1. 语义分段
-    print(f"\n[步骤1] 语义分段...")
-    result = await Runner.run(semantic_segmenter, o)
-    segmentation: SemanticSegmentation = result.final_output
-
-    print(f"语义分段结果: {len(segmentation.segments)} 个片段")
-    print(f"整体分段思路: {segmentation.overall_reasoning}")
-
-    segment_list = []
-    for seg_item in segmentation.segments:
-        segment = Segment(
-            text=seg_item.segment_text,
-            type=seg_item.segment_type,
-            from_o=o
-        )
-        segment_list.append(segment)
-        print(f"  - [{segment.type}] {segment.text}")
-
-    # 2. 对每个segment拆词并评估
-    print(f"\n[步骤2] 对每个segment拆词并评估...")
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    async def process_segment(segment: Segment) -> Segment:
-        """处理单个segment: 拆词 + 评估segment + 评估词"""
-        async with semaphore:
-            # 2.1 拆词
-            word_result = await Runner.run(word_segmenter, segment.text)
-            word_segmentation: WordSegmentation = word_result.final_output
-            segment.words = word_segmentation.words
-
-            # 2.2 评估segment与原始问题的相关度(使用Round 0专用评估)
-            segment.score_with_o, segment.reason = await evaluate_with_o_round0(
-                segment.text, o, context.evaluation_cache
-            )
-
-            # 2.3 评估每个词与原始问题的相关度(使用Round 0专用评估)
-            word_eval_tasks = []
-            for word in segment.words:
-                async def eval_word(w: str) -> tuple[str, float, str]:
-                    score, reason = await evaluate_with_o_round0(w, o, context.evaluation_cache)
-                    return w, score, reason
-                word_eval_tasks.append(eval_word(word))
-
-            word_results = await asyncio.gather(*word_eval_tasks)
-            for word, score, reason in word_results:
-                segment.word_scores[word] = score
-                segment.word_reasons[word] = reason
-
-            return segment
-
-    if segment_list:
-        print(f"  开始处理 {len(segment_list)} 个segment(并发限制: {MAX_CONCURRENT_EVALUATIONS})...")
-        process_tasks = [process_segment(seg) for seg in segment_list]
-        await asyncio.gather(*process_tasks)
-
-    # 打印步骤1结果
-    print(f"\n[步骤1: 分段及拆词 结果]")
-    for segment in segment_list:
-        print(f"  [{segment.type}] {segment.text} (分数: {segment.score_with_o:.2f})")
-        print(f"    拆词: {segment.words}")
-        for word in segment.words:
-            score = segment.word_scores.get(word, 0.0)
-            print(f"      - {word}: {score:.2f}")
-
-    # 保存到context(保留旧格式以兼容)
-    context.segments = [
-        {
-            "text": seg.text,
-            "type": seg.type,
-            "score": seg.score_with_o,
-            "reason": seg.reason,
-            "words": seg.words,
-            "word_scores": seg.word_scores,
-            "word_reasons": seg.word_reasons
-        }
-        for seg in segment_list
-    ]
-
-    # 保存 Round 0 到 context.rounds(新格式用于可视化)
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "segments": [
-            {
-                "text": seg.text,
-                "type": seg.type,
-                "domain_index": idx,
-                "score": seg.score_with_o,
-                "reason": seg.reason,
-                "words": [
-                    {
-                        "text": word,
-                        "score": seg.word_scores.get(word, 0.0),
-                        "reason": seg.word_reasons.get(word, "")
-                    }
-                    for word in seg.words
-                ]
-            }
-            for idx, seg in enumerate(segment_list)
-        ]
-    })
-
-    # 🆕 存储Round 0的所有word得分到历史记录
-    print(f"\n[存储Round 0词得分到历史记录]")
-    for segment in segment_list:
-        for word, score in segment.word_scores.items():
-            context.word_score_history[word] = score
-            print(f"  {word}: {score:.2f}")
-
-    print(f"\n[Round 0 完成]")
-    print(f"  分段数: {len(segment_list)}")
-    total_words = sum(len(seg.words) for seg in segment_list)
-    print(f"  总词数: {total_words}")
-
-    return segment_list
-
-
-async def run_round_v2(
-    round_num: int,
-    query_input: list[Q],
-    segments: list[Segment],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Q], list[Search]]:
-    """
-    v121 Round N 执行
-
-    正确的流程顺序:
-    1. 为 query_input 请求SUG
-    2. 评估SUG
-    3. 高分SUG搜索
-    4. N域组合(从segments生成)
-    5. 评估组合
-    6. 生成 q_list_next(组合 + 高分SUG)
-
-    Args:
-        round_num: 轮次编号 (1-4)
-        query_input: 本轮的输入query列表(Round 1是words,Round 2+是上轮输出)
-        segments: 语义片段列表(用于组合)
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: SUG搜索阈值
-
-    Returns:
-        (q_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"Round {round_num}: {round_num}域组合")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "n_domains": round_num,
-        "input_query_count": len(query_input)
-    }
-
-    MAX_CONCURRENT_EVALUATIONS = 5
-    semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
-
-    # 步骤1: 为 query_input 请求SUG
-    print(f"\n[步骤1] 为{len(query_input)}个输入query请求SUG...")
-    all_sugs = []
-    sug_details = {}
-
-    for q in query_input:
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-        if suggestions:
-            print(f"  {q.text}: 获取到 {len(suggestions)} 个SUG")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                all_sugs.append(sug)
-        else:
-            print(f"  {q.text}: 未获取到SUG")
-
-    print(f"  共获取 {len(all_sugs)} 个SUG")
-
-    # 步骤2: 评估SUG
-    if len(all_sugs) > 0:
-        print(f"\n[步骤2] 评估{len(all_sugs)}个SUG...")
-
-        async def evaluate_sug(sug: Sug) -> Sug:
-            async with semaphore:
-                sug.score_with_o, sug.reason = await evaluate_with_o(
-                    sug.text, o, context.evaluation_cache
-                )
-                return sug
-
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-        # 打印结果
-        for sug in all_sugs:
-            print(f"    {sug.text}: {sug.score_with_o:.2f}")
-            if sug.from_q:
-                if sug.from_q.text not in sug_details:
-                    sug_details[sug.from_q.text] = []
-                sug_details[sug.from_q.text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason,
-                    "type": "sug"
-                })
-
-    # 步骤3: 搜索高分SUG
-    print(f"\n[步骤3] 搜索高分SUG(阈值 > {sug_threshold})...")
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-    print(f"  找到 {len(high_score_sugs)} 个高分SUG")
-
-    search_list = []
-    if len(high_score_sugs) > 0:
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-
-    # 步骤4: 生成N域组合
-    print(f"\n[步骤4] 生成{round_num}域组合...")
-    domain_combinations = generate_domain_combinations(segments, round_num)
-    print(f"  生成了 {len(domain_combinations)} 个组合")
-
-    if len(domain_combinations) == 0:
-        print(f"  无法生成{round_num}域组合")
-        # 即使无法组合,也返回高分SUG作为下轮输入
-        q_list_next = []
-        for sug in all_sugs:
-            if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-                q = Q(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    reason=sug.reason,
-                    from_source="sug",
-                    type_label=""
-                )
-                q_list_next.append(q)
-
-        round_data.update({
-            "domain_combinations_count": 0,
-            "sug_count": len(all_sugs),
-            "high_score_sug_count": len(high_score_sugs),
-            "search_count": len(search_list),
-            "sug_details": sug_details,
-            "q_list_next_size": len(q_list_next)
-        })
-        context.rounds.append(round_data)
-        return q_list_next, search_list
-
-    # 步骤5: 评估所有组合
-    print(f"\n[步骤5] 评估{len(domain_combinations)}个组合...")
-
-    async def evaluate_combination(comb: DomainCombination) -> DomainCombination:
-        async with semaphore:
-            # 🆕 根据轮次选择评估逻辑
-            if round_num == 1:
-                # Round 1: 域内评估(新逻辑)
-                comb.score_with_o, comb.reason = await evaluate_domain_combination_round1(
-                    comb, segments, context
-                )
-            else:
-                # Round 2+: 域间评估(新逻辑)
-                comb.score_with_o, comb.reason = await evaluate_domain_combination_round2plus(
-                    comb, segments, context
-                )
-
-            # 🆕 存储组合得分到历史记录
-            context.word_score_history[comb.text] = comb.score_with_o
-
-            return comb
-
-    eval_tasks = [evaluate_combination(comb) for comb in domain_combinations]
-    await asyncio.gather(*eval_tasks)
-
-    # 排序 - 已注释,保持原始顺序
-    # domain_combinations.sort(key=lambda x: x.score_with_o, reverse=True)
-
-    # 打印所有组合(保持原始顺序)
-    evaluation_strategy = 'Round 1 域内评估(品类×域得分)' if round_num == 1 else 'Round 2+ 域间评估(加权系数调整)'
-    print(f"  评估完成,共{len(domain_combinations)}个组合 [策略: {evaluation_strategy}]")
-    for i, comb in enumerate(domain_combinations, 1):
-        print(f"    {i}. {comb.text} {comb.type_label} (分数: {comb.score_with_o:.2f})")
-
-    # 为每个组合补充来源词分数信息,并判断是否超过所有来源词得分
-    for comb in domain_combinations:
-        word_details = []
-        flat_scores: list[float] = []
-        for domain_index, words in zip(comb.domains, comb.source_words):
-            segment = segments[domain_index] if 0 <= domain_index < len(segments) else None
-            segment_type = segment.type if segment else ""
-            segment_text = segment.text if segment else ""
-            items = []
-            for word in words:
-                score = 0.0
-                if segment and word in segment.word_scores:
-                    score = segment.word_scores[word]
-                items.append({
-                    "text": word,
-                    "score": score
-                })
-                flat_scores.append(score)
-            word_details.append({
-                "domain_index": domain_index,
-                "segment_type": segment_type,
-                "segment_text": segment_text,
-                "words": items
-            })
-        comb.source_word_details = word_details
-        comb.source_scores = flat_scores
-        comb.max_source_score = max(flat_scores) if flat_scores else None
-        comb.is_above_source_scores = bool(flat_scores) and all(
-            comb.score_with_o > score for score in flat_scores
-        )
-
-    # 步骤6: 构建 q_list_next(组合 + 高分SUG)
-    print(f"\n[步骤6] 生成下轮输入...")
-    q_list_next: list[Q] = []
-
-    # 6.1 添加高增益SUG(满足增益条件),并按分数排序
-    sug_candidates: list[tuple[Q, Sug]] = []
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o >= sug.from_q.score_with_o + REQUIRED_SCORE_GAIN:
-            q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug",
-                type_label=""
-            )
-            sug_candidates.append((q, sug))
-
-    sug_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in sug_candidates])
-    high_gain_sugs = [item[1] for item in sug_candidates]
-    print(f"  添加 {len(high_gain_sugs)} 个高增益SUG(增益 ≥ {REQUIRED_SCORE_GAIN:.2f})")
-
-    # 6.2 添加高分组合(需超过所有来源词得分),并按分数排序
-    combination_candidates: list[tuple[Q, DomainCombination]] = []
-    for comb in domain_combinations:
-        if comb.is_above_source_scores and comb.score_with_o > 0:
-            domains_str = ','.join([f'D{d}' for d in comb.domains]) if comb.domains else ''
-            q = Q(
-                text=comb.text,
-                score_with_o=comb.score_with_o,
-                reason=comb.reason,
-                from_source="domain_comb",
-                type_label=comb.type_label,
-                domain_type=domains_str  # 添加域信息
-            )
-            combination_candidates.append((q, comb))
-
-    combination_candidates.sort(key=lambda item: item[0].score_with_o, reverse=True)
-    q_list_next.extend([item[0] for item in combination_candidates])
-    high_score_combinations = [item[1] for item in combination_candidates]
-    print(f"  添加 {len(high_score_combinations)} 个高分组合(组合得分 > 所有来源词)")
-
-    # 保存round数据(包含完整帖子信息)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    round_data.update({
-        "input_queries": [{"text": q.text, "score": q.score_with_o, "from_source": q.from_source, "type": "input", "domain_index": q.domain_index, "domain_type": q.domain_type} for q in query_input],
-        "domain_combinations_count": len(domain_combinations),
-        "domain_combinations": [
-            {
-                "text": comb.text,
-                "type_label": comb.type_label,
-                "score": comb.score_with_o,
-                "reason": comb.reason,
-                "domains": comb.domains,
-                "source_words": comb.source_words,
-                "from_segments": comb.from_segments,
-                "source_word_details": comb.source_word_details,
-                "source_scores": comb.source_scores,
-                "is_above_source_scores": comb.is_above_source_scores,
-                "max_source_score": comb.max_source_score
-            }
-            for comb in domain_combinations
-        ],
-        "high_score_combinations": [
-            {
-                "text": item[0].text,
-                "score": item[0].score_with_o,
-                "type_label": item[0].type_label,
-                "type": "combination",
-                "is_above_source_scores": item[1].is_above_source_scores
-            }
-            for item in combination_candidates
-        ],
-        "sug_count": len(all_sugs),
-        "sug_details": sug_details,
-        "high_score_sug_count": len(high_score_sugs),
-        "high_gain_sugs": [{"text": q.text, "score": q.score_with_o, "type": "sug"} for q in q_list_next if q.from_source == "sug"],
-        "search_count": len(search_list),
-        "search_results": search_results_data,
-        "q_list_next_size": len(q_list_next),
-        "q_list_next_sections": {
-            "sugs": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "sug"
-                }
-                for item in sug_candidates
-            ],
-            "domain_combinations": [
-                {
-                    "text": item[0].text,
-                    "score": item[0].score_with_o,
-                    "from_source": "domain_comb",
-                    "is_above_source_scores": item[1].is_above_source_scores
-                }
-                for item in combination_candidates
-            ]
-        }
-    })
-    context.rounds.append(round_data)
-
-    print(f"\nRound {round_num} 总结:")
-    print(f"  输入Query数: {len(query_input)}")
-    print(f"  域组合数: {len(domain_combinations)}")
-    print(f"  高分组合: {len(high_score_combinations)}")
-    print(f"  SUG数: {len(all_sugs)}")
-    print(f"  高分SUG数: {len(high_score_sugs)}")
-    print(f"  高增益SUG: {len(high_gain_sugs)}")
-    print(f"  搜索数: {len(search_list)}")
-    print(f"  下轮Query数: {len(q_list_next)}")
-
-    return q_list_next, search_list
-
-
-async def iterative_loop_v2(
-    context: RunContext,
-    max_rounds: int = 4,
-    sug_threshold: float = 0.7
-):
-    """v121 主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始v121迭代循环(语义分段跨域组词版)")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # Round 0: 初始化(语义分段 + 拆词)
-    segments = await initialize_v2(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 准备 Round 1 的输入:从 segments 提取所有 words
-    query_input = extract_words_from_segments(segments)
-    print(f"\n提取了 {len(query_input)} 个词作为 Round 1 的输入")
-
-    # Round 1-N: 迭代循环
-    num_segments = len(segments)
-    actual_max_rounds = min(max_rounds, num_segments)
-    round_num = 1
-
-    while query_input and round_num <= actual_max_rounds:
-        query_input, search_list = await run_round_v2(
-            round_num=round_num,
-            query_input=query_input,  # 传递上一轮的输出
-            segments=segments,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-
-        # 如果没有新的query,提前结束
-        if not query_input:
-            print(f"\n第{round_num}轮后无新query生成,提前结束迭代")
-            break
-
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  实际轮数: {round_num}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 创建日志目录
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    # 配置日志文件
-    log_file_path = os.path.join(run_context.log_dir, "run.log")
-    log_file = open(log_file_path, 'w', encoding='utf-8')
-
-    # 重定向stdout到TeeLogger(同时输出到控制台和文件)
-    original_stdout = sys.stdout
-    sys.stdout = TeeLogger(original_stdout, log_file)
-
-    try:
-        print(f"📝 日志文件: {log_file_path}")
-        print(f"{'='*60}\n")
-
-        # 执行迭代 (v121: 使用新架构)
-        all_search_list = await iterative_loop_v2(
-            run_context,
-            max_rounds=max_rounds,
-            sug_threshold=sug_threshold
-        )
-
-        # 格式化输出
-        output = f"原始需求:{run_context.c}\n"
-        output += f"原始问题:{run_context.o}\n"
-        output += f"总搜索次数:{len(all_search_list)}\n"
-        output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-        output += "\n" + "="*60 + "\n"
-
-        if all_search_list:
-            output += "【搜索结果】\n\n"
-            for idx, search in enumerate(all_search_list, 1):
-                output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-                output += f"   帖子数: {len(search.post_list)}\n"
-                if search.post_list:
-                    for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                        output += f"   {post_idx}) {post.title}\n"
-                        output += f"      URL: {post.note_url}\n"
-                output += "\n"
-        else:
-            output += "未找到搜索结果\n"
-
-        run_context.final_output = output
-
-        print(f"\n{'='*60}")
-        print("最终结果")
-        print(f"{'='*60}")
-        print(output)
-
-        # 保存上下文文件
-        context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-        context_dict = run_context.model_dump()
-        with open(context_file_path, "w", encoding="utf-8") as f:
-            json.dump(context_dict, f, ensure_ascii=False, indent=2)
-        print(f"\nRunContext saved to: {context_file_path}")
-
-        # 保存详细的搜索结果
-        search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-        search_results_data = [s.model_dump() for s in all_search_list]
-        with open(search_results_path, "w", encoding="utf-8") as f:
-            json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-        print(f"Search results saved to: {search_results_path}")
-
-        # 可视化
-        if visualize:
-            import subprocess
-            output_html = os.path.join(run_context.log_dir, "visualization.html")
-            print(f"\n🎨 生成可视化HTML...")
-
-            # 获取绝对路径
-            abs_context_file = os.path.abspath(context_file_path)
-            abs_output_html = os.path.abspath(output_html)
-
-            # 运行可视化脚本
-            result = subprocess.run([
-                "node",
-                "visualization/sug_v6_1_2_121/index.js",
-                abs_context_file,
-                abs_output_html
-            ])
-
-            if result.returncode == 0:
-                print(f"✅ 可视化已生成: {output_html}")
-            else:
-                print(f"❌ 可视化生成失败")
-
-    finally:
-        # 恢复stdout
-        sys.stdout = original_stdout
-        log_file.close()
-        print(f"\n📝 运行日志已保存: {log_file_path}")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.121 语义分段跨域组词版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 4"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 851
sug_v6_1_2_8.py

@@ -1,851 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    reason: str = ""  # 评分理由
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 相关度评估专家
-class RelevanceEvaluation(BaseModel):
-    """相关度评估"""
-    relevance_score: float = Field(..., description="相关性分数 0-1")
-    reason: str = Field(..., description="评估理由")
-
-relevance_evaluation_instructions = """
-你是相关度评估专家。
-
-## 任务
-评估当前文本与原始问题的匹配程度。
-
-## 评估标准
-- 主题相关性
-- 要素覆盖度
-- 意图匹配度
-
-## 输出
-- relevance_score: 0-1的相关性分数
-- reason: 详细理由
-""".strip()
-
-relevance_evaluator = Agent[None](
-    name="相关度评估专家",
-    instructions=relevance_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=RelevanceEvaluation,
-)
-
-
-# Agent 3: 加词选择专家
-class WordSelection(BaseModel):
-    """加词选择结果"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-word_selection_instructions = """
-你是加词选择专家。
-
-## 任务
-从候选词列表中选择一个最合适的词,与当前seed组合成新的query。
-
-## 原则
-1. 选择与当前seed最相关的词
-2. 组合后的query要语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-## 输出
-- selected_word: 选中的词
-- combined_query: 组合后的新query
-- reasoning: 选择理由
-""".strip()
-
-word_selector = Agent[None](
-    name="加词选择专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelection,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # 提取图片URL - 使用新的字段名 image_url
-    images = []
-    for img in image_list:
-        if isinstance(img, dict):
-            # 尝试新字段名 image_url,如果不存在则尝试旧字段名 url_default
-            img_url = img.get("image_url") or img.get("url_default")
-            if img_url:
-                images.append(img_url)
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id", ""),
-        title=note_card.get("display_title", ""),
-        body_text=note_card.get("desc", ""),
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str) -> tuple[float, str]:
-    """评估文本与原始问题o的相关度
-
-    Returns:
-        tuple[float, str]: (相关度分数, 评估理由)
-    """
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前文本>
-{text}
-</当前文本>
-
-请评估当前文本与原始问题的相关度。
-"""
-    result = await Runner.run(relevance_evaluator, eval_input)
-    evaluation: RelevanceEvaluation = result.final_output
-    return evaluation.relevance_score, evaluation.reason
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        seg.score_with_o, seg.reason = await evaluate_with_o(seg.text, o)
-        return seg
-
-    if seg_list:
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1
-    print(f"\n[步骤3] 构建word_list_1...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1: {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            reason=seg.reason,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Word], list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list: 当前的word列表
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (word_list_next, q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "input_word_list_size": len(word_list),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug
-    async def evaluate_sug(sug: Sug) -> Sug:
-        sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o)
-        return sug
-
-    if all_sugs:
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o,
-                    "reason": sug.reason
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建word_list_next: word_list -> word_list_next(先直接复制)
-    print(f"\n[步骤4] 构建word_list_next(暂时直接复制)...")
-    word_list_next = word_list.copy()
-
-    # 5. 构建q_list_next
-    print(f"\n[步骤5] 构建q_list_next...")
-    q_list_next = []
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-
-    # 5.1 对于seed_list中的每个seed,从word_list_next中选一个未加过的词
-    print(f"\n  5.1 为每个seed加词...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 简单过滤:找出不在seed.text中且未被添加过的词
-        candidate_words = []
-        for word in word_list_next:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词: {[w.text for w in candidate_words]}")
-
-        # 使用Agent选择最合适的词
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{', '.join([w.text for w in candidate_words])}
-</候选词列表>
-
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-        result = await Runner.run(word_selector, selection_input)
-        selection: WordSelection = result.final_output
-
-        # 验证选择的词是否在候选列表中
-        if selection.selected_word not in [w.text for w in candidate_words]:
-            print(f"      ✗ Agent选择的词 '{selection.selected_word}' 不在候选列表中,跳过")
-            continue
-
-        print(f"      ✓ 选择词: {selection.selected_word}")
-        print(f"      ✓ 新query: {selection.combined_query}")
-        print(f"      理由: {selection.reasoning}")
-
-        # 评估新query
-        new_q_score, new_q_reason = await evaluate_with_o(selection.combined_query, o)
-        print(f"      新query评分: {new_q_score:.2f}")
-
-        # 创建新的q
-        new_q = Q(
-            text=selection.combined_query,
-            score_with_o=new_q_score,
-            reason=new_q_reason,
-            from_source="add"
-        )
-        q_list_next.append(new_q)
-
-        # 更新seed的added_words
-        seed.added_words.append(selection.selected_word)
-
-        # 保存到add_word_details
-        if seed.text not in add_word_details:
-            add_word_details[seed.text] = []
-        add_word_details[seed.text].append({
-            "text": selection.combined_query,
-            "score": new_q_score,
-            "reason": new_q_reason,
-            "selected_word": selection.selected_word
-        })
-
-    # 5.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next
-    print(f"\n  5.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                reason=sug.reason,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 6. 更新seed_list
-    print(f"\n[步骤6] 更新seed_list...")
-    seed_list_next = seed_list.copy()  # 保留原有的seed
-
-    # 对于sug_list_list中,每个sug分数大于来源query分数的,且没在seed_list中出现过的,加入
-    existing_seed_texts = {seed.text for seed in seed_list_next}
-    for sug in all_sugs:
-        # 新逻辑:sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"  ✓ 新seed: {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 序列化搜索结果数据(包含帖子详情)
-    search_results_data = []
-    for search in search_list:
-        search_results_data.append({
-            "text": search.text,
-            "score_with_o": search.score_with_o,
-            "post_list": [
-                {
-                    "note_id": post.note_id,
-                    "note_url": post.note_url,
-                    "title": post.title,
-                    "body_text": post.body_text,
-                    "images": post.images,
-                    "interact_info": post.interact_info
-                }
-                for post in search.post_list
-            ]
-        })
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "word_list_next_size": len(word_list_next),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],  # 下一轮种子列表
-        "sug_details": sug_details,  # 每个Q对应的sug列表
-        "add_word_details": add_word_details,  # 每个seed对应的组合词列表
-        "search_results": search_results_data  # 搜索结果(包含帖子详情)
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  seed数量: {len(seed_list_next)}")
-
-    return word_list_next, q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        word_list, q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list=word_list,
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 执行迭代
-    all_search_list = await iterative_loop(
-        run_context,
-        max_rounds=max_rounds,
-        sug_threshold=sug_threshold
-    )
-
-    # 格式化输出
-    output = f"原始需求:{run_context.c}\n"
-    output += f"原始问题:{run_context.o}\n"
-    output += f"总搜索次数:{len(all_search_list)}\n"
-    output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-    output += "\n" + "="*60 + "\n"
-
-    if all_search_list:
-        output += "【搜索结果】\n\n"
-        for idx, search in enumerate(all_search_list, 1):
-            output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-            output += f"   帖子数: {len(search.post_list)}\n"
-            if search.post_list:
-                for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                    output += f"   {post_idx}) {post.title}\n"
-                    output += f"      URL: {post.note_url}\n"
-            output += "\n"
-    else:
-        output += "未找到搜索结果\n"
-
-    run_context.final_output = output
-
-    print(f"\n{'='*60}")
-    print("最终结果")
-    print(f"{'='*60}")
-    print(output)
-
-    # 保存日志
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-    context_dict = run_context.model_dump()
-    with open(context_file_path, "w", encoding="utf-8") as f:
-        json.dump(context_dict, f, ensure_ascii=False, indent=2)
-    print(f"\nRunContext saved to: {context_file_path}")
-
-    # 保存详细的搜索结果
-    search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-    search_results_data = [s.model_dump() for s in all_search_list]
-    with open(search_results_path, "w", encoding="utf-8") as f:
-        json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-    print(f"Search results saved to: {search_results_path}")
-
-    # 可视化
-    if visualize:
-        import subprocess
-        output_html = os.path.join(run_context.log_dir, "visualization.html")
-        print(f"\n🎨 生成可视化HTML...")
-
-        # 获取绝对路径
-        abs_context_file = os.path.abspath(context_file_path)
-        abs_output_html = os.path.abspath(output_html)
-
-        # 运行可视化脚本
-        result = subprocess.run([
-            "node",
-            "visualization/sug_v6_1_2_8/index.js",
-            abs_context_file,
-            abs_output_html
-        ])
-
-        if result.returncode == 0:
-            print(f"✅ 可视化已生成: {output_html}")
-        else:
-            print(f"❌ 可视化生成失败")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.8 轮次迭代版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=4,
-        help="最大轮数,默认: 2"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 0 - 990
sug_v6_1_2_8_流程分析.md

@@ -1,990 +0,0 @@
-# sug_v6_1_2_8.py 流程分析文档
-
-## 📋 概述
-
-`sug_v6_1_2_8.py` 是一个基于 LLM Agent 的智能搜索查询优化工具,主要用于小红书平台的搜索优化。通过多轮迭代的方式,从原始查询出发,逐步扩展和优化搜索词,最终获取高质量的搜索结果。
-
-**版本**: v6.1.2.8
-**核心模型**: google/gemini-2.5-flash
-**主要特性**:
-- 🔄 多轮迭代优化
-- 🤖 多 Agent 协作
-- 📊 相关度评分系统
-- 🔍 小红书搜索集成
-- 📈 可视化支持
-
----
-
-## 🏗️ 整体架构
-
-### 架构图
-
-```
-原始问题(o)
-    ↓
-[初始化阶段]
-    ├─ 分词 → seg_list
-    ├─ 评估分词相关度
-    ├─ 构建 word_list_1
-    ├─ 构建 q_list_1
-    └─ 构建 seed_list
-    ↓
-[第1轮迭代]
-    ├─ 请求 sug (建议词)
-    ├─ 评估 sug 相关度
-    ├─ 构建 search_list (高分sug搜索)
-    ├─ 为 seed 加词 → q_list_next
-    ├─ 更新 seed_list
-    └─ 保存搜索结果
-    ↓
-[第2轮迭代] ...
-    ↓
-[第N轮迭代] ...
-    ↓
-[输出结果 + 可视化]
-```
-
-### 核心组件
-
-1. **数据模型层** - 定义所有数据结构(Seg, Word, Q, Sug, Seed, Post, Search)
-2. **Agent 层** - 三个专家 Agent(分词、相关度评估、加词选择)
-3. **流程控制层** - 初始化、轮次迭代、主循环
-4. **外部服务层** - 小红书 API 集成(搜索推荐、搜索)
-
----
-
-## 📦 数据模型
-
-### 核心数据结构
-
-#### 1. Seg (分词)
-```python
-class Seg(BaseModel):
-    text: str                    # 分词文本
-    score_with_o: float = 0.0    # 与原始问题的评分
-    reason: str = ""             # 评分理由
-    from_o: str = ""             # 原始问题
-```
-
-**用途**: 存储原始问题分词后的每个词单元
-
-#### 2. Word (词)
-```python
-class Word(BaseModel):
-    text: str                    # 词文本
-    score_with_o: float = 0.0    # 与原始问题的评分
-    from_o: str = ""             # 原始问题
-```
-
-**用途**: 词库,用于后续组合新的查询词
-
-#### 3. Q (查询)
-```python
-class Q(BaseModel):
-    text: str                    # 查询文本
-    score_with_o: float = 0.0    # 与原始问题的评分
-    reason: str = ""             # 评分理由
-    from_source: str = ""        # 来源: seg/sug/add
-```
-
-**用途**: 待处理的查询队列,每轮从 q_list 中取 query 进行处理
-
-#### 4. Sug (建议词)
-```python
-class Sug(BaseModel):
-    text: str                    # 建议词文本
-    score_with_o: float = 0.0    # 与原始问题的评分
-    reason: str = ""             # 评分理由
-    from_q: QFromQ | None        # 来自哪个 q
-```
-
-**用途**: 存储从小红书 API 获取的建议词
-
-#### 5. Seed (种子)
-```python
-class Seed(BaseModel):
-    text: str                    # 种子文本
-    added_words: list[str]       # 已添加的词
-    from_type: str = ""          # 来源: seg/sug
-    score_with_o: float = 0.0    # 与原始问题的评分
-```
-
-**用途**: 用于加词扩展的基础词,记录已经添加过的词以避免重复
-
-#### 6. Post (帖子)
-```python
-class Post(BaseModel):
-    title: str                   # 标题
-    body_text: str               # 正文
-    type: str = "normal"         # 类型: video/normal
-    images: list[str]            # 图片URL列表
-    video: str = ""              # 视频URL
-    interact_info: dict          # 互动信息(点赞/收藏/评论/分享)
-    note_id: str                 # 笔记ID
-    note_url: str                # 笔记URL
-```
-
-**用途**: 存储小红书搜索结果的帖子详情
-
-#### 7. Search (搜索结果)
-```python
-class Search(Sug):
-    post_list: list[Post]        # 搜索到的帖子列表
-```
-
-**用途**: 继承 Sug,附加实际搜索到的帖子数据
-
-#### 8. RunContext (运行上下文)
-```python
-class RunContext(BaseModel):
-    version: str                 # 版本号
-    input_files: dict            # 输入文件路径
-    c: str                       # 原始需求
-    o: str                       # 原始问题
-    log_url: str                 # 日志URL
-    log_dir: str                 # 日志目录
-    rounds: list[dict]           # 每轮的详细数据
-    final_output: str | None     # 最终结果
-```
-
-**用途**: 记录整个运行过程的上下文信息和中间结果
-
----
-
-## 🤖 Agent 系统
-
-### Agent 1: 分词专家 (word_segmenter)
-
-**功能**: 将原始问题拆分成有意义的最小单元
-
-**输入**: 原始查询文本
-**输出**:
-```python
-class WordSegmentation:
-    words: list[str]        # 分词结果列表
-    reasoning: str          # 分词理由
-```
-
-**分词原则**:
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-**示例**:
-- 输入: "如何获取能体现川西秋季特色的高质量风光摄影素材?"
-- 输出: ["川西", "秋季", "风光摄影", "素材"]
-
-### Agent 2: 相关度评估专家 (relevance_evaluator)
-
-**功能**: 评估文本与原始问题的匹配程度
-
-**输入**: 原始问题 + 待评估文本
-**输出**:
-```python
-class RelevanceEvaluation:
-    relevance_score: float  # 0-1的相关性分数
-    reason: str            # 评估理由
-```
-
-**评估标准**:
-- 主题相关性
-- 要素覆盖度
-- 意图匹配度
-
-**示例**:
-- 原始问题: "川西秋季摄影"
-- 待评估: "川西旅游攻略"
-- 输出: score=0.75, reason="与川西相关但缺少秋季和摄影要素"
-
-### Agent 3: 加词选择专家 (word_selector)
-
-**功能**: 从候选词中选择最合适的词与 seed 组合
-
-**输入**: 原始问题 + 当前 seed + 候选词列表
-**输出**:
-```python
-class WordSelection:
-    selected_word: str       # 选择的词
-    combined_query: str      # 组合后的新query
-    reasoning: str           # 选择理由
-```
-
-**选择原则**:
-1. 选择与当前 seed 最相关的词
-2. 组合后的 query 语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-**示例**:
-- seed: "川西"
-- 候选词: ["秋季", "摄影", "旅游"]
-- 输出: selected_word="秋季", combined_query="川西秋季"
-
----
-
-## 🔄 核心流程
-
-### 阶段 0: 初始化 (initialize)
-
-**目标**: 从原始问题创建初始数据结构
-
-**流程**:
-
-```
-步骤1: 分词
-o → [word_segmenter] → WordSegmentation → seg_list
-
-步骤2: 评估分词
-for each seg in seg_list:
-    seg + o → [relevance_evaluator] → score + reason
-    更新 seg.score_with_o, seg.reason
-
-步骤3: 构建 word_list_1
-seg_list → word_list_1 (直接转换)
-
-步骤4: 构建 q_list_1
-seg_list → q_list_1 (from_source="seg")
-
-步骤5: 构建 seed_list
-seg_list → seed_list (from_type="seg")
-```
-
-**输入**:
-- `o`: 原始问题(例如: "如何获取川西秋季风光摄影素材?")
-
-**输出**:
-- `seg_list`: 分词结果列表
-- `word_list_1`: 初始词库
-- `q_list_1`: 第一轮待处理查询列表
-- `seed_list`: 初始种子列表
-
-**示例数据流**:
-```
-o = "川西秋季摄影素材"
-    ↓
-seg_list = [
-    Seg(text="川西", score_with_o=0.85),
-    Seg(text="秋季", score_with_o=0.90),
-    Seg(text="摄影", score_with_o=0.88),
-    Seg(text="素材", score_with_o=0.75)
-]
-    ↓
-word_list_1 = [Word("川西"), Word("秋季"), ...]
-q_list_1 = [Q("川西"), Q("秋季"), ...]
-seed_list = [Seed("川西"), Seed("秋季"), ...]
-```
-
----
-
-### 阶段 N: 轮次迭代 (run_round)
-
-**目标**: 基于当前 q_list 扩展搜索,生成下一轮的数据
-
-**输入**:
-- `round_num`: 轮次编号
-- `q_list`: 当前轮的查询列表
-- `word_list`: 当前词库
-- `seed_list`: 当前种子列表
-- `sug_threshold`: 建议词阈值(默认 0.7)
-
-**输出**:
-- `word_list_next`: 下一轮词库
-- `q_list_next`: 下一轮查询列表
-- `seed_list_next`: 下一轮种子列表
-- `search_list`: 本轮搜索结果
-
-#### 步骤1: 请求建议词
-
-```python
-for each q in q_list:
-    sug_texts = xiaohongshu_api.get_recommendations(q.text)
-    for sug_text in sug_texts:
-        sug_list.append(Sug(
-            text=sug_text,
-            from_q=QFromQ(text=q.text, score=q.score_with_o)
-        ))
-```
-
-**并发处理**: 所有 q 的请求可以并发执行
-
-**数据流**:
-```
-q_list = [Q("川西"), Q("秋季")]
-    ↓ [小红书API]
-sug_list_list = [
-    [Sug("川西旅游"), Sug("川西攻略"), ...],  # 来自 "川西"
-    [Sug("秋季景色"), Sug("秋季摄影"), ...]   # 来自 "秋季"
-]
-```
-
-#### 步骤2: 评估建议词
-
-```python
-async def evaluate_sug(sug: Sug) -> Sug:
-    sug.score_with_o, sug.reason = await evaluate_with_o(sug.text, o)
-    return sug
-
-# 并发评估所有 sug
-await asyncio.gather(*[evaluate_sug(sug) for sug in all_sugs])
-```
-
-**评估标准**: 使用 relevance_evaluator Agent
-
-**数据流**:
-```
-Sug("川西旅游") + o → score=0.75, reason="..."
-Sug("秋季摄影") + o → score=0.92, reason="..."
-```
-
-#### 步骤3: 构建 search_list(搜索高分建议词)
-
-```python
-high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-async def search_for_sug(sug: Sug) -> Search:
-    result = xiaohongshu_search.search(sug.text)
-    posts = process_notes(result)
-    return Search(text=sug.text, post_list=posts, ...)
-
-search_list = await asyncio.gather(*[search_for_sug(sug) for sug in high_score_sugs])
-```
-
-**阈值过滤**: 只搜索评分 > `sug_threshold` 的建议词
-
-**并发搜索**: 所有高分 sug 并发搜索
-
-**数据流**:
-```
-high_score_sugs = [Sug("秋季摄影", score=0.92), ...]
-    ↓ [小红书搜索API]
-search_list = [
-    Search(text="秋季摄影", post_list=[Post(...), ...])
-]
-```
-
-#### 步骤4: 构建 word_list_next
-
-```python
-word_list_next = word_list.copy()  # 暂时直接复制
-```
-
-**说明**: 当前版本词库保持不变,未来可扩展从 sug 中提取新词
-
-#### 步骤5: 构建 q_list_next
-
-**5.1 为每个 seed 加词**
-
-```python
-for each seed in seed_list:
-    # 过滤候选词
-    candidate_words = [w for w in word_list_next
-                       if w.text not in seed.text
-                       and w.text not in seed.added_words]
-
-    # Agent 选词
-    selection_input = f"""
-    原始问题: {o}
-    当前Seed: {seed.text}
-    候选词: {candidate_words}
-    """
-    result = await Runner.run(word_selector, selection_input)
-
-    # 创建新 query
-    new_q = Q(
-        text=result.combined_query,
-        score_with_o=...,
-        from_source="add"
-    )
-    q_list_next.append(new_q)
-
-    # 更新 seed
-    seed.added_words.append(result.selected_word)
-```
-
-**关键逻辑**:
-- 避免重复: 词不在 seed.text 中且未被添加过
-- Agent 智能选择: 使用 word_selector 选择最佳组合
-- 评估新 query: 评估组合后的 query 与原始问题的相关度
-
-**示例**:
-```
-seed = Seed("川西", added_words=[])
-candidate_words = ["秋季", "摄影"]
-    ↓ [word_selector]
-selected_word = "秋季"
-combined_query = "川西秋季"
-    ↓ [relevance_evaluator]
-new_q = Q("川西秋季", score=0.88, from_source="add")
-```
-
-**5.2 高分 sug 加入 q_list_next**
-
-```python
-for sug in all_sugs:
-    if sug.score_with_o > sug.from_q.score_with_o:
-        new_q = Q(
-            text=sug.text,
-            score_with_o=sug.score_with_o,
-            from_source="sug"
-        )
-        q_list_next.append(new_q)
-```
-
-**条件**: sug 分数 > 来源 query 分数
-
-**示例**:
-```
-sug = Sug("秋季摄影技巧", score=0.92, from_q=Q("秋季", score=0.85))
-    ↓ (0.92 > 0.85)
-q_list_next.append(Q("秋季摄影技巧", score=0.92, from_source="sug"))
-```
-
-#### 步骤6: 更新 seed_list
-
-```python
-seed_list_next = seed_list.copy()  # 保留原有 seed
-
-for sug in all_sugs:
-    if (sug.score_with_o > sug.from_q.score_with_o
-        and sug.text not in existing_seed_texts):
-        new_seed = Seed(
-            text=sug.text,
-            from_type="sug",
-            score_with_o=sug.score_with_o
-        )
-        seed_list_next.append(new_seed)
-```
-
-**条件**:
-1. sug 分数 > 来源 query 分数
-2. sug 未在 seed_list 中出现过
-
-**示例**:
-```
-sug = Sug("川西秋季攻略", score=0.90, from_q=Q("川西", score=0.85))
-    ↓ (0.90 > 0.85 且未重复)
-seed_list_next.append(Seed("川西秋季攻略", from_type="sug"))
-```
-
----
-
-### 主循环 (iterative_loop)
-
-**流程控制**:
-
-```python
-# 初始化
-seg_list, word_list, q_list, seed_list = await initialize(o, context)
-
-# 迭代
-round_num = 1
-while q_list and round_num <= max_rounds:
-    word_list, q_list, seed_list, search_list = await run_round(
-        round_num, q_list, word_list, seed_list, ...
-    )
-    all_search_list.extend(search_list)
-    round_num += 1
-
-return all_search_list
-```
-
-**终止条件**:
-1. `q_list` 为空(没有更多查询需要处理)
-2. 达到 `max_rounds` 限制
-
-**数据累积**: 所有轮次的 search_list 合并到 `all_search_list`
-
----
-
-## 📊 数据流图
-
-### 完整数据流
-
-```
-输入:
-├─ input_dir/context.md  (原始需求 c)
-└─ input_dir/q.md        (原始问题 o)
-    ↓
-[初始化]
-o → seg_list → word_list_1, q_list_1, seed_list
-    ↓
-[第1轮]
-q_list_1 → sug_list_1 → search_list_1
-         → q_list_2, seed_list_2 (通过加词+高分sug)
-    ↓
-[第2轮]
-q_list_2 → sug_list_2 → search_list_2
-         → q_list_3, seed_list_3
-    ↓
-[第N轮] ...
-    ↓
-输出:
-├─ all_search_list (所有搜索结果)
-├─ log_dir/run_context.json (运行上下文)
-├─ log_dir/search_results.json (详细搜索结果)
-└─ log_dir/visualization.html (可视化HTML)
-```
-
-### 每轮数据变化
-
-```
-轮次输入                          轮次输出
-┌─────────────────┐             ┌─────────────────┐
-│ q_list          │──┐          │ q_list_next     │
-│ word_list       │  │          │ word_list_next  │
-│ seed_list       │  │          │ seed_list_next  │
-└─────────────────┘  │          │ search_list     │
-                     │          └─────────────────┘
-                     ↓
-            ┌──────────────────┐
-            │   run_round()    │
-            │                  │
-            │ 1. 请求sug       │
-            │ 2. 评估sug       │
-            │ 3. 搜索高分sug   │
-            │ 4. 为seed加词    │
-            │ 5. 构建q_next    │
-            │ 6. 更新seed_list │
-            └──────────────────┘
-```
-
----
-
-## 🎯 关键算法
-
-### 1. 相关度评分机制
-
-**评分函数**: `evaluate_with_o(text, o)`
-
-**输入**:
-- `text`: 待评估文本
-- `o`: 原始问题
-
-**输出**: `(score, reason)`
-
-**实现**:
-```python
-async def evaluate_with_o(text: str, o: str) -> tuple[float, str]:
-    eval_input = f"""
-    <原始问题>{o}</原始问题>
-    <当前文本>{text}</当前文本>
-    请评估当前文本与原始问题的相关度。
-    """
-    result = await Runner.run(relevance_evaluator, eval_input)
-    return result.final_output.relevance_score, result.final_output.reason
-```
-
-**应用场景**:
-- 评估分词与原始问题的相关度
-- 评估 sug 与原始问题的相关度
-- 评估新组合 query 与原始问题的相关度
-
-### 2. 加词策略
-
-**目标**: 从词库中为 seed 选择最佳词进行组合
-
-**候选词过滤**:
-```python
-candidate_words = [
-    w for w in word_list
-    if w.text not in seed.text           # 词不在seed中
-    and w.text not in seed.added_words   # 词未被添加过
-]
-```
-
-**智能选择**:
-```python
-selection_input = f"""
-<原始问题>{o}</原始问题>
-<当前Seed>{seed.text}</当前Seed>
-<候选词列表>{', '.join([w.text for w in candidate_words])}</候选词列表>
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-result = await Runner.run(word_selector, selection_input)
-```
-
-**验证和评估**:
-```python
-# 验证选择的词在候选列表中
-if selection.selected_word not in [w.text for w in candidate_words]:
-    continue
-
-# 评估组合后的query
-new_q_score, new_q_reason = await evaluate_with_o(
-    selection.combined_query, o
-)
-```
-
-### 3. Sug 晋升机制
-
-**晋升到 q_list 的条件**:
-```python
-if sug.score_with_o > sug.from_q.score_with_o:
-    q_list_next.append(Q(
-        text=sug.text,
-        score_with_o=sug.score_with_o,
-        from_source="sug"
-    ))
-```
-
-**晋升到 seed_list 的条件**:
-```python
-if (sug.score_with_o > sug.from_q.score_with_o
-    and sug.text not in existing_seed_texts):
-    seed_list_next.append(Seed(
-        text=sug.text,
-        from_type="sug",
-        score_with_o=sug.score_with_o
-    ))
-```
-
-**逻辑**: 只有当 sug 的评分超过其来源 query 时,才认为 sug 是更优的查询词
-
-### 4. 搜索阈值过滤
-
-**目标**: 只搜索高质量的建议词
-
-**实现**:
-```python
-high_score_sugs = [
-    sug for sug in all_sugs
-    if sug.score_with_o > sug_threshold
-]
-
-# 并发搜索
-search_list = await asyncio.gather(*[
-    search_for_sug(sug) for sug in high_score_sugs
-])
-```
-
-**默认阈值**: 0.7(可通过 `--sug-threshold` 参数调整)
-
----
-
-## 🔧 外部服务集成
-
-### 1. 小红书搜索推荐 API
-
-**类**: `XiaohongshuSearchRecommendations`
-
-**方法**: `get_recommendations(keyword: str) -> list[str]`
-
-**功能**: 获取指定关键词的搜索建议词
-
-**使用场景**: 在每轮中为 q_list 中的每个 query 请求建议词
-
-### 2. 小红书搜索 API
-
-**类**: `XiaohongshuSearch`
-
-**方法**: `search(keyword: str) -> dict`
-
-**功能**: 搜索指定关键词,返回帖子列表
-
-**返回数据处理**:
-```python
-def process_note_data(note: dict) -> Post:
-    note_card = note.get("note_card", {})
-    return Post(
-        note_id=note.get("id"),
-        title=note_card.get("display_title"),
-        body_text=note_card.get("desc"),
-        type=note_card.get("type", "normal"),
-        images=[img.get("image_url") for img in note_card.get("image_list", [])],
-        interact_info={
-            "liked_count": ...,
-            "collected_count": ...,
-            "comment_count": ...,
-            "shared_count": ...
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id')}"
-    )
-```
-
----
-
-## 📝 日志和输出
-
-### 运行上下文 (run_context.json)
-
-**保存内容**:
-```json
-{
-  "version": "sug_v6_1_2_8.py",
-  "input_files": {...},
-  "c": "原始需求",
-  "o": "原始问题",
-  "log_dir": "...",
-  "log_url": "...",
-  "rounds": [
-    {
-      "round_num": 0,
-      "type": "initialization",
-      "seg_list": [...],
-      "word_list_1": [...],
-      "q_list_1": [...],
-      "seed_list": [...]
-    },
-    {
-      "round_num": 1,
-      "input_q_list": [...],
-      "sug_count": 20,
-      "high_score_sug_count": 5,
-      "search_count": 5,
-      "total_posts": 50,
-      "sug_details": {...},
-      "add_word_details": {...},
-      "search_results": [...]
-    },
-    ...
-  ],
-  "final_output": "..."
-}
-```
-
-### 搜索结果 (search_results.json)
-
-**保存内容**:
-```json
-[
-  {
-    "text": "秋季摄影",
-    "score_with_o": 0.92,
-    "reason": "...",
-    "from_q": {
-      "text": "秋季",
-      "score_with_o": 0.85
-    },
-    "post_list": [
-      {
-        "note_id": "...",
-        "note_url": "...",
-        "title": "...",
-        "body_text": "...",
-        "images": [...],
-        "interact_info": {...}
-      },
-      ...
-    ]
-  },
-  ...
-]
-```
-
-### 可视化 HTML
-
-**生成方式**:
-```python
-subprocess.run([
-    "node",
-    "visualization/sug_v6_1_2_8/index.js",
-    abs_context_file,
-    abs_output_html
-])
-```
-
-**依赖**: Node.js + React + esbuild
-
-**生成的文件**: `log_dir/visualization.html`
-
----
-
-## 🚀 使用方法
-
-### 命令行参数
-
-```bash
-python3 sug_v6_1_2_8.py \
-  --input-dir "input/旅游/如何获取川西秋季风光摄影素材?" \
-  --max-rounds 4 \
-  --sug-threshold 0.7 \
-  --visualize
-```
-
-**参数说明**:
-
-| 参数 | 类型 | 默认值 | 说明 |
-|------|------|--------|------|
-| `--input-dir` | str | `input/旅游-逸趣玩旅行/...` | 输入目录路径 |
-| `--max-rounds` | int | 4 | 最大迭代轮数 |
-| `--sug-threshold` | float | 0.7 | 建议词评分阈值 |
-| `--visualize` | flag | True | 是否生成可视化 |
-
-### 输入文件结构
-
-```
-input_dir/
-├── context.md   # 原始需求描述
-└── q.md         # 原始问题
-```
-
-### 输出文件结构
-
-```
-input_dir/output/sug_v6_1_2_8/{timestamp}/
-├── run_context.json      # 运行上下文
-├── search_results.json   # 详细搜索结果
-└── visualization.html    # 可视化页面
-```
-
----
-
-## 🎨 并发优化
-
-### 并发点
-
-1. **分词评估**: 所有 seg 并发评估
-   ```python
-   await asyncio.gather(*[evaluate_seg(seg) for seg in seg_list])
-   ```
-
-2. **Sug 评估**: 所有 sug 并发评估
-   ```python
-   await asyncio.gather(*[evaluate_sug(sug) for sug in all_sugs])
-   ```
-
-3. **搜索**: 所有高分 sug 并发搜索
-   ```python
-   await asyncio.gather(*[search_for_sug(sug) for sug in high_score_sugs])
-   ```
-
-### 串行点
-
-1. **分词**: 必须先完成分词才能评估
-2. **轮次迭代**: 必须按顺序执行各轮
-3. **加词选择**: 每个 seed 的加词必须等待 Agent 返回
-
----
-
-## 🔍 核心特点
-
-### 1. 迭代扩展
-- 从原始问题出发,逐轮扩展搜索词
-- 通过 seed + word 组合生成新查询
-- 通过 sug 晋升机制引入新的高质量查询
-
-### 2. 智能评分
-- 所有文本与原始问题的相关度都通过 LLM 评估
-- 评分结果用于过滤、排序、晋升决策
-
-### 3. 多 Agent 协作
-- 分词专家: 拆分原始问题
-- 相关度评估专家: 统一评分标准
-- 加词选择专家: 智能组合词汇
-
-### 4. 数据驱动
-- 完整记录每轮的输入输出
-- 可追溯每个 query/sug 的来源
-- 支持可视化分析
-
-### 5. 高并发
-- 利用 asyncio 实现高并发
-- 评估、搜索等操作并行执行
-- 提升整体执行效率
-
----
-
-## 🐛 潜在问题和改进方向
-
-### 1. 词库静态
-**问题**: word_list 在初始化后不再更新,可能错过新的有价值的词
-
-**改进方向**:
-- 从高分 sug 中提取新词加入 word_list
-- 从搜索结果的标题/正文中提取关键词
-
-### 2. 加词盲目性
-**问题**: 每个 seed 每轮必须加一个词,即使候选词质量不高
-
-**改进方向**:
-- 增加加词的评分阈值
-- 允许 seed 在某轮跳过加词
-
-### 3. Sug 重复
-**问题**: 不同 query 可能返回相同的 sug,导致重复搜索
-
-**改进方向**:
-- 全局去重 sug
-- 记录已搜索的 query,避免重复搜索
-
-### 4. 搜索结果未利用
-**问题**: 搜索到的帖子内容没有被进一步分析和利用
-
-**改进方向**:
-- 分析帖子标题/内容提取新的关键词
-- 评估帖子质量,作为 query 质量的反馈
-
-### 5. 阈值固定
-**问题**: sug_threshold 固定,可能导致某些轮次没有搜索结果
-
-**改进方向**:
-- 动态调整阈值
-- 保证每轮至少有一定数量的搜索
-
----
-
-## 📈 性能分析
-
-### 时间复杂度
-
-假设:
-- 每轮 q_list 大小: `Q`
-- 每个 q 的 sug 数量: `S`
-- 每轮 seed 数量: `K`
-- 最大轮数: `R`
-
-**每轮时间复杂度**:
-- 请求 sug: `O(Q)`(并发)
-- 评估 sug: `O(Q * S)`(并发)
-- 搜索: `O(高分sug数量)`(并发)
-- 加词: `O(K * word_list大小)`(串行,但每个加词操作并发评估)
-
-**总时间复杂度**: `O(R * (Q + Q*S + K*W))`
-
-### 空间复杂度
-
-- `seg_list`: `O(分词数)`
-- `word_list`: `O(分词数)`(当前版本)
-- `q_list`: `O(Q)` 每轮
-- `seed_list`: `O(K)` 每轮
-- `sug_list`: `O(Q * S)` 每轮
-- `search_list`: `O(高分sug数) * O(每个搜索的帖子数)`
-
-**总空间复杂度**: `O(R * (Q*S + 高分sug数*帖子数))`
-
----
-
-## 🎯 总结
-
-`sug_v6_1_2_8.py` 是一个设计精良的搜索查询优化系统,具有以下特点:
-
-### 优势
-1. ✅ **模块化设计**: 数据模型、Agent、流程控制分离清晰
-2. ✅ **智能化**: 利用多个 LLM Agent 实现分词、评估、选词
-3. ✅ **可扩展**: 通过迭代机制不断扩展搜索范围
-4. ✅ **高性能**: 大量使用并发优化执行效率
-5. ✅ **可追溯**: 完整记录每轮数据,支持可视化分析
-
-### 核心流程
-```
-原始问题 → 分词 → 评估 → 迭代(请求sug → 评估 → 搜索 → 加词 → 更新) → 输出结果
-```
-
-### 关键机制
-- **评分机制**: 统一的相关度评估标准
-- **晋升机制**: 高分 sug 晋升为 query 和 seed
-- **扩展机制**: seed + word 组合生成新 query
-- **过滤机制**: 阈值过滤低质量 sug
-
-### 适用场景
-- 搜索查询扩展和优化
-- 关键词发现和探索
-- 内容检索和推荐
-- 搜索效果分析
-
----
-
-**文档生成时间**: 2025-11-03
-**代码版本**: sug_v6_1_2_8.py
-**作者**: Knowledge Agent Team

+ 0 - 1027
sug_v6_1_2_9.py

@@ -1,1027 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-import argparse
-from datetime import datetime
-from typing import Literal
-
-from agents import Agent, Runner
-from lib.my_trace import set_trace
-from pydantic import BaseModel, Field
-
-from lib.utils import read_file_as_string
-from lib.client import get_model
-MODEL_NAME = "google/gemini-2.5-flash"
-from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
-from script.search.xiaohongshu_search import XiaohongshuSearch
-
-
-# ============================================================================
-# 数据模型
-# ============================================================================
-
-class Seg(BaseModel):
-    """分词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class Word(BaseModel):
-    """词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_o: str = ""  # 原始问题
-
-
-class QFromQ(BaseModel):
-    """Q来源信息(用于Sug中记录)"""
-    text: str
-    score_with_o: float = 0.0
-
-
-class Q(BaseModel):
-    """查询"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_source: str = ""  # seg/sug/add(加词)
-
-
-class Sug(BaseModel):
-    """建议词"""
-    text: str
-    score_with_o: float = 0.0  # 与原始问题的评分
-    from_q: QFromQ | None = None  # 来自的q
-
-
-class Seed(BaseModel):
-    """种子"""
-    text: str
-    added_words: list[str] = Field(default_factory=list)  # 已经增加的words
-    from_type: str = ""  # seg/sug
-    score_with_o: float = 0.0  # 与原始问题的评分
-
-
-class Post(BaseModel):
-    """帖子"""
-    title: str = ""
-    body_text: str = ""
-    type: str = "normal"  # video/normal
-    images: list[str] = Field(default_factory=list)  # 图片url列表,第一张为封面
-    video: str = ""  # 视频url
-    interact_info: dict = Field(default_factory=dict)  # 互动信息
-    note_id: str = ""
-    note_url: str = ""
-
-
-class Search(Sug):
-    """搜索结果(继承Sug)"""
-    post_list: list[Post] = Field(default_factory=list)  # 搜索得到的帖子列表
-
-
-class RunContext(BaseModel):
-    """运行上下文"""
-    version: str
-    input_files: dict[str, str]
-    c: str  # 原始需求
-    o: str  # 原始问题
-    log_url: str
-    log_dir: str
-
-    # 每轮的数据
-    rounds: list[dict] = Field(default_factory=list)  # 每轮的详细数据
-
-    # 最终结果
-    final_output: str | None = None
-
-
-# ============================================================================
-# Agent 定义
-# ============================================================================
-
-# Agent 1: 分词专家
-class WordSegmentation(BaseModel):
-    """分词结果"""
-    words: list[str] = Field(..., description="分词结果列表")
-    reasoning: str = Field(..., description="分词理由")
-
-word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
-
-## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等)
-
-## 输出要求
-返回分词列表和分词理由。
-""".strip()
-
-word_segmenter = Agent[None](
-    name="分词专家",
-    instructions=word_segmentation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSegmentation,
-)
-
-
-# Agent 2: 相关度评估专家
-class RelevanceEvaluation(BaseModel):
-    """相关度评估"""
-    reason: str = Field(..., description="评估理由")
-    relevance_score: float = Field(..., description="相关性分数 -1~1")
-   
-
-relevance_evaluation_instructions = """
-# 角色定义
-你是一个 **专业的语言专家和语义相关性评判专家**。你的任务是:判断我给你的 <平台sug词条> 与 <原始问题> 的相关度满足度,给出 **-1 到 1 之间** 的数值评分。
-
----
-
-# 核心概念与方法论
-
-## 两大评估维度
-本评估系统始终围绕 **两个核心维度** 进行:
-
-### 1. 动机维度(权重70%)
-**定义:** 用户"想要做什么",即原始问题的行为意图和目的
-- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
-- 包括:核心动作 + 使用场景 + 最终目的
-
-### 2. 品类维度(权重30%)
-**定义:** 用户"关于什么内容",即原始问题的主题对象和限定词
-- 核心是 **名词+限定词**:川西秋季风光摄影素材
-- 包括:核心主体 + 地域限定 + 时间限定 + 质量限定等
-
----
-
-## 如何识别原始问题的核心动机
-
-**核心动机必须是动词**,识别方法如下:
-
-### 方法1: 显性动词直接提取
-
-当原始问题明确包含动词时,直接提取
-示例:
-"如何获取素材" → 核心动机 = "获取"
-"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
-"制作视频教程" → 核心动机 = "制作"
-
-### 方法2: 隐性动词语义推理
-
-当原始问题没有显性动词时,需要结合上下文推理
-示例:
-例: "川西秋天风光摄影" → 隐含动作="拍摄"
-→ 需结合上下文判断
-
-如果原始问题是纯名词短语,无任何动作线索:
-→ 核心动机 = 无法识别
-→ 初始权重 = 0
-→ 相关度评估以品类匹配为主
-示例:
-"摄影" → 无法识别动机,初始权重=0
-"川西风光" → 无法识别动机,初始权重=0
-
-
-
-# 输入信息
-你将接收到以下输入:
-- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
-- **<平台sug词条>**:平台推荐的词条列表,每个词条需要单独评估。
-
-
-#判定流程
-#评估架构
-
-输入: <原始问题> + <平台sug词条>
-         ↓
-【综合相关性判定】
-    ├→ 步骤1: 评估<sug词条>与<原始问题>的相关度
-    └→ 输出: -1到1之间的数值 + 分维度得分 + 判定依据
-
-
-相关度评估维度详解
-维度1: 动机维度评估(权重70%)
-评估对象: <平台sug词条> 与 <原始问题> 的需求动机匹配度
-说明: 核心动作是用户需求的第一优先级,决定了推荐的基本有效性
-
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心动作完全一致
-  - 例: 原始问题"如何获取素材" vs sug词"素材获取方法"
-  - 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
-    · 例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致)
-
-+0.8~0.95: 核心动作语义相近或为同义表达
-  - 例: 原始问题"如何获取素材" vs sug词"素材下载教程"
-  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
-
-+0.5~0.75: 核心动作相关但非直接对应(相关实现路径)
-  - 例: 原始问题"如何获取素材" vs sug词"素材管理整理"
-
-+0.2~0.45: 核心动作弱相关(同领域不同动作)
-  - 例: 原始问题"如何拍摄风光" vs sug词"风光摄影欣赏"
-
-【中性/无关】
-0: 没有明确目的,动作意图无明确关联
-  - 例: 原始问题"如何获取素材" vs sug词"摄影器材推荐"
-  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
-
-【负向偏离】
--0.2~-0.05: 动作意图轻度冲突或误导
-  - 例: 原始问题"如何获取素材" vs sug词"素材版权保护须知"
-
--0.5~-0.25: 动作意图明显对立
-  - 例: 原始问题"如何获取免费素材" vs sug词"如何售卖素材"
-
--1.0~-0.55: 动作意图完全相反或产生严重负面引导
-  - 例: 原始问题"免费素材获取" vs sug词"付费素材强制推销"
-
-维度2: 品类维度评估(权重30%)
-评估对象: <平台sug词条> 与 <原始问题> 的内容主体和限定词匹配度
-
-评分标准:
-
-【正向匹配】
-+1.0: 核心主体+所有关键限定词完全匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
-
-+0.75~0.95: 核心主体匹配,大部分限定词匹配
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
-
-+0.5~0.7: 核心主体匹配,少量限定词匹配或合理泛化
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
-
-+0.2~0.45: 仅主体词匹配,限定词全部缺失或错位
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
-
-+0.05~0.15: 主题领域相关但品类不同
-  - 例: 原始问题"风光摄影素材" vs sug词"人文摄影素材"
-
-【中性/无关】
-0: 主体词部分相关但类别明显不同
-  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
-
-【负向偏离】
--0.2~-0.05: 主体词或限定词存在误导性
-  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
-
--0.5~-0.25: 主体词明显错位或品类冲突
-  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
-
--1.0~-0.55: 完全错误的品类或有害引导
-  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
-
-
-综合得分计算与规则调整
-步骤1: 应用依存性规则
-规则A: 动机高分保护机制
-
-如果 动机维度得分 ≥ 0.8:
-   → 品类得分即使为0或轻微负向(-0.2~0)
-   → 最终得分 = max(初步得分, 0.55)
-
-解释: 当目的高度一致时,品类的泛化不应导致"弱相关"
-
-规则B: 动机低分限制机制
-如果 动机维度得分 ≤ 0.2:
-   → 无论品类得分多高
-   → 最终得分 = min(初步得分, 0.4)
-
-解释: 目的不符时,品类匹配的价值有限
-
-规则C: 动机负向决定机制
-如果 动机维度得分 < 0:
-   → 最终得分 = min(初步得分, 0)
-
-解释: 动作意图冲突时,推荐具有误导性,不应为正相关
-
-步骤3: 输出最终得分
-
-#基础加权计算
-应用规则后的调整得分 = 目的动机维度得分 × 0.7 + 品类维度得分 × 0.3
-取值范围: -1.0 ~ +1.0
-
----
-
-# 得分档位解释
-
-高度相关】+0.8 ~ +1.0
-相关性高度契合,用户可直接使用
-动机和品类均高度匹配
-典型场景: 动机≥0.85 且 品类≥0.7
-【中高相关】+0.6 ~ +0.79
-相关性较好,用户基本满意
-动机匹配但品类有泛化,或反之
-典型场景: 动机≥0.8 且 品类≥0.3
-【中度相关】+0.3 ~ +0.59
-部分相关,用户需要调整搜索策略
-动机或品类存在一定偏差
-典型场景: 动机0.4-0.7 且 品类0.3-0.7
-【弱相关】+0.01 ~ +0.29
-关联微弱,参考价值有限
-仅有表层词汇重叠
-【无关】0
-无明确关联
-原始问题无法识别动机 且 sug词无明确动作
-没有目的性且没有品类匹配
-【轻度负向】-0.29 ~ -0.01
-产生轻微误导或干扰
-【中度负向】-0.69 ~ -0.3
-存在明显冲突或误导
-【严重负向】-1.0 ~ -0.7
-完全违背意图或产生有害引导
-
----
-
-# 输出要求
-输出结果必须为一个 **JSON 格式**,包含以下内容:
-
-#注意事项:
-始终围绕两个核心维度:所有评估都基于"动机"和"品类"两个维度,不偏离
-核心动机必须是动词:在评估前,必须先提取原始问题的核心动机(动词),这是整个评估的基础
-严格标准一致性:对所有用例使用相同的评估标准,避免评分飘移
-负分使用原则:仅当sug词条对原始问题产生误导、冲突或有害引导时给予负分
-零分使用原则:当sug词条与原始问题无明确关联,既不相关也不冲突时给予零分
-分维度独立评分:
-先提取原始问题核心动机
-分别计算动机维度(含两个子维度)和品类维度得分
-按70:30加权得到初步得分
-应用规则调整得到最终得分
-动机优先原则:当动机高度一致时,品类的合理泛化或具体化不应导致低评分
-技巧类需求特殊对待:包含"技巧/方法/教程"等词的需求,对动作一致性要求更严格
-
-## 输出
-- reason: 详细理由
-- relevance_score: 0-1的相关性分数
-""".strip()
-
-relevance_evaluator = Agent[None](
-    name="相关度评估专家",
-    instructions=relevance_evaluation_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=RelevanceEvaluation,
-)
-
-
-# Agent 3: 加词选择专家
-class WordSelection(BaseModel):
-    """加词选择结果"""
-    selected_word: str = Field(..., description="选择的词")
-    combined_query: str = Field(..., description="组合后的新query")
-    reasoning: str = Field(..., description="选择理由")
-
-word_selection_instructions = """
-你是加词选择专家。
-
-## 任务
-从候选词列表中选择一个最合适的词,与当前seed组合成新的query。
-
-## 原则
-1. 选择与当前seed最相关的词
-2. 组合后的query要语义通顺
-3. 符合搜索习惯
-4. 优先选择能扩展搜索范围的词
-
-## 输出
-- selected_word: 选中的词
-- combined_query: 组合后的新query
-- reasoning: 选择理由
-""".strip()
-
-word_selector = Agent[None](
-    name="加词选择专家",
-    instructions=word_selection_instructions,
-    model=get_model(MODEL_NAME),
-    output_type=WordSelection,
-)
-
-
-# ============================================================================
-# 辅助函数
-# ============================================================================
-
-def process_note_data(note: dict) -> Post:
-    """处理搜索接口返回的帖子数据"""
-    note_card = note.get("note_card", {})
-    image_list = note_card.get("image_list", [])
-    interact_info = note_card.get("interact_info", {})
-    user_info = note_card.get("user", {})
-
-    # 提取图片URL
-    images = []
-    for img in image_list:
-        if isinstance(img, dict) and "url_default" in img:
-            images.append(img["url_default"])
-
-    # 判断类型
-    note_type = note_card.get("type", "normal")
-    video_url = ""
-    if note_type == "video":
-        video_info = note_card.get("video", {})
-        if isinstance(video_info, dict):
-            # 尝试获取视频URL
-            video_url = video_info.get("media", {}).get("stream", {}).get("h264", [{}])[0].get("master_url", "")
-
-    return Post(
-        note_id=note.get("id", ""),
-        title=note_card.get("display_title", ""),
-        body_text=note_card.get("desc", ""),
-        type=note_type,
-        images=images,
-        video=video_url,
-        interact_info={
-            "liked_count": interact_info.get("liked_count", 0),
-            "collected_count": interact_info.get("collected_count", 0),
-            "comment_count": interact_info.get("comment_count", 0),
-            "shared_count": interact_info.get("shared_count", 0)
-        },
-        note_url=f"https://www.xiaohongshu.com/explore/{note.get('id', '')}"
-    )
-
-
-async def evaluate_with_o(text: str, o: str) -> float:
-    """评估文本与原始问题o的相关度"""
-    eval_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前文本>
-{text}
-</当前文本>
-
-请评估当前文本与原始问题的相关度。
-"""
-    result = await Runner.run(relevance_evaluator, eval_input)
-    evaluation: RelevanceEvaluation = result.final_output
-    return evaluation.relevance_score
-
-
-# ============================================================================
-# 核心流程函数
-# ============================================================================
-
-async def initialize(o: str, context: RunContext) -> tuple[list[Seg], list[Word], list[Q], list[Seed]]:
-    """
-    初始化阶段
-
-    Returns:
-        (seg_list, word_list_1, q_list_1, seed_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"初始化阶段")
-    print(f"{'='*60}")
-
-    # 1. 分词:原始问题(o) ->分词-> seg_list
-    print(f"\n[步骤1] 分词...")
-    result = await Runner.run(word_segmenter, o)
-    segmentation: WordSegmentation = result.final_output
-
-    seg_list = []
-    for word in segmentation.words:
-        seg_list.append(Seg(text=word, from_o=o))
-
-    print(f"分词结果: {[s.text for s in seg_list]}")
-    print(f"分词理由: {segmentation.reasoning}")
-
-    # 2. 分词评估:seg_list -> 每个seg与o进行评分(并发)
-    print(f"\n[步骤2] 评估每个分词与原始问题的相关度...")
-
-    async def evaluate_seg(seg: Seg) -> Seg:
-        seg.score_with_o = await evaluate_with_o(seg.text, o)
-        return seg
-
-    if seg_list:
-        eval_tasks = [evaluate_seg(seg) for seg in seg_list]
-        await asyncio.gather(*eval_tasks)
-
-    for seg in seg_list:
-        print(f"  {seg.text}: {seg.score_with_o:.2f}")
-
-    # 3. 构建word_list_1: seg_list -> word_list_1
-    print(f"\n[步骤3] 构建word_list_1...")
-    word_list_1 = []
-    for seg in seg_list:
-        word_list_1.append(Word(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_o=o
-        ))
-    print(f"word_list_1: {[w.text for w in word_list_1]}")
-
-    # 4. 构建q_list_1:seg_list 作为 q_list_1
-    print(f"\n[步骤4] 构建q_list_1...")
-    q_list_1 = []
-    for seg in seg_list:
-        q_list_1.append(Q(
-            text=seg.text,
-            score_with_o=seg.score_with_o,
-            from_source="seg"
-        ))
-    print(f"q_list_1: {[q.text for q in q_list_1]}")
-
-    # 5. 构建seed_list: seg_list -> seed_list
-    print(f"\n[步骤5] 构建seed_list...")
-    seed_list = []
-    for seg in seg_list:
-        seed_list.append(Seed(
-            text=seg.text,
-            added_words=[],
-            from_type="seg",
-            score_with_o=seg.score_with_o
-        ))
-    print(f"seed_list: {[s.text for s in seed_list]}")
-
-    return seg_list, word_list_1, q_list_1, seed_list
-
-
-async def run_round(
-    round_num: int,
-    q_list: list[Q],
-    word_list: list[Word],
-    seed_list: list[Seed],
-    o: str,
-    context: RunContext,
-    xiaohongshu_api: XiaohongshuSearchRecommendations,
-    xiaohongshu_search: XiaohongshuSearch,
-    sug_threshold: float = 0.7
-) -> tuple[list[Word], list[Q], list[Seed], list[Search]]:
-    """
-    运行一轮
-
-    Args:
-        round_num: 轮次编号
-        q_list: 当前轮的q列表
-        word_list: 当前的word列表
-        seed_list: 当前的seed列表
-        o: 原始问题
-        context: 运行上下文
-        xiaohongshu_api: 建议词API
-        xiaohongshu_search: 搜索API
-        sug_threshold: suggestion的阈值
-
-    Returns:
-        (word_list_next, q_list_next, seed_list_next, search_list)
-    """
-    print(f"\n{'='*60}")
-    print(f"第{round_num}轮")
-    print(f"{'='*60}")
-
-    round_data = {
-        "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "input_word_list_size": len(word_list),
-        "input_seed_list_size": len(seed_list)
-    }
-
-    # 1. 请求sug:q_list -> 每个q请求sug接口 -> sug_list_list
-    print(f"\n[步骤1] 为每个q请求建议词...")
-    sug_list_list = []  # list of list
-    for q in q_list:
-        print(f"\n  处理q: {q.text}")
-        suggestions = xiaohongshu_api.get_recommendations(keyword=q.text)
-
-        q_sug_list = []
-        if suggestions:
-            print(f"    获取到 {len(suggestions)} 个建议词")
-            for sug_text in suggestions:
-                sug = Sug(
-                    text=sug_text,
-                    from_q=QFromQ(text=q.text, score_with_o=q.score_with_o)
-                )
-                q_sug_list.append(sug)
-        else:
-            print(f"    未获取到建议词")
-
-        sug_list_list.append(q_sug_list)
-
-    # 2. sug评估:sug_list_list -> 每个sug与o进评分(并发)
-    print(f"\n[步骤2] 评估每个建议词与原始问题的相关度...")
-
-    # 2.1 收集所有需要评估的sug,并记录它们所属的q
-    all_sugs = []
-    sug_to_q_map = {}  # 记录每个sug属于哪个q
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            for sug in q_sug_list:
-                all_sugs.append(sug)
-                sug_to_q_map[id(sug)] = q_text
-
-    # 2.2 并发评估所有sug
-    async def evaluate_sug(sug: Sug) -> Sug:
-        sug.score_with_o = await evaluate_with_o(sug.text, o)
-        return sug
-
-    if all_sugs:
-        eval_tasks = [evaluate_sug(sug) for sug in all_sugs]
-        await asyncio.gather(*eval_tasks)
-
-    # 2.3 打印结果并组织到sug_details
-    sug_details = {}  # 保存每个Q对应的sug列表
-    for i, q_sug_list in enumerate(sug_list_list):
-        if q_sug_list:
-            q_text = q_list[i].text
-            print(f"\n  来自q '{q_text}' 的建议词:")
-            sug_details[q_text] = []
-            for sug in q_sug_list:
-                print(f"    {sug.text}: {sug.score_with_o:.2f}")
-                # 保存到sug_details
-                sug_details[q_text].append({
-                    "text": sug.text,
-                    "score": sug.score_with_o
-                })
-
-    # 3. search_list构建
-    print(f"\n[步骤3] 构建search_list(阈值>{sug_threshold})...")
-    search_list = []
-    high_score_sugs = [sug for sug in all_sugs if sug.score_with_o > sug_threshold]
-
-    if high_score_sugs:
-        print(f"  找到 {len(high_score_sugs)} 个高分建议词")
-
-        # 并发搜索
-        async def search_for_sug(sug: Sug) -> Search:
-            print(f"    搜索: {sug.text}")
-            try:
-                search_result = xiaohongshu_search.search(keyword=sug.text)
-                result_str = search_result.get("result", "{}")
-                if isinstance(result_str, str):
-                    result_data = json.loads(result_str)
-                else:
-                    result_data = result_str
-
-                notes = result_data.get("data", {}).get("data", [])
-                post_list = []
-                for note in notes[:10]:  # 只取前10个
-                    post = process_note_data(note)
-                    post_list.append(post)
-
-                print(f"      → 找到 {len(post_list)} 个帖子")
-
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=post_list
-                )
-            except Exception as e:
-                print(f"      ✗ 搜索失败: {e}")
-                return Search(
-                    text=sug.text,
-                    score_with_o=sug.score_with_o,
-                    from_q=sug.from_q,
-                    post_list=[]
-                )
-
-        search_tasks = [search_for_sug(sug) for sug in high_score_sugs]
-        search_list = await asyncio.gather(*search_tasks)
-    else:
-        print(f"  没有高分建议词,search_list为空")
-
-    # 4. 构建word_list_next: word_list -> word_list_next(先直接复制)
-    print(f"\n[步骤4] 构建word_list_next(暂时直接复制)...")
-    word_list_next = word_list.copy()
-
-    # 5. 构建q_list_next
-    print(f"\n[步骤5] 构建q_list_next...")
-    q_list_next = []
-    add_word_details = {}  # 保存每个seed对应的组合词列表
-
-    # 5.1 对于seed_list中的每个seed,从word_list_next中选一个未加过的词
-    print(f"\n  5.1 为每个seed加词...")
-    for seed in seed_list:
-        print(f"\n    处理seed: {seed.text}")
-
-        # 简单过滤:找出不在seed.text中且未被添加过的词
-        candidate_words = []
-        for word in word_list_next:
-            # 检查词是否已在seed中
-            if word.text in seed.text:
-                continue
-            # 检查词是否已被添加过
-            if word.text in seed.added_words:
-                continue
-            candidate_words.append(word)
-
-        if not candidate_words:
-            print(f"      没有可用的候选词")
-            continue
-
-        print(f"      候选词: {[w.text for w in candidate_words]}")
-
-        # 使用Agent选择最合适的词
-        selection_input = f"""
-<原始问题>
-{o}
-</原始问题>
-
-<当前Seed>
-{seed.text}
-</当前Seed>
-
-<候选词列表>
-{', '.join([w.text for w in candidate_words])}
-</候选词列表>
-
-请从候选词中选择一个最合适的词,与当前seed组合成新的query。
-"""
-        result = await Runner.run(word_selector, selection_input)
-        selection: WordSelection = result.final_output
-
-        # 验证选择的词是否在候选列表中
-        if selection.selected_word not in [w.text for w in candidate_words]:
-            print(f"      ✗ Agent选择的词 '{selection.selected_word}' 不在候选列表中,跳过")
-            continue
-
-        print(f"      ✓ 选择词: {selection.selected_word}")
-        print(f"      ✓ 新query: {selection.combined_query}")
-        print(f"      理由: {selection.reasoning}")
-
-        # 评估新query
-        new_q_score = await evaluate_with_o(selection.combined_query, o)
-        print(f"      新query评分: {new_q_score:.2f}")
-
-        # 创建新的q
-        new_q = Q(
-            text=selection.combined_query,
-            score_with_o=new_q_score,
-            from_source="add"
-        )
-        q_list_next.append(new_q)
-
-        # 更新seed的added_words
-        seed.added_words.append(selection.selected_word)
-
-        # 保存到add_word_details
-        if seed.text not in add_word_details:
-            add_word_details[seed.text] = []
-        add_word_details[seed.text].append({
-            "text": selection.combined_query,
-            "score": new_q_score,
-            "selected_word": selection.selected_word
-        })
-
-    # 5.2 对于sug_list_list中,每个sug大于来自的query分数,加到q_list_next
-    print(f"\n  5.2 将高分sug加入q_list_next...")
-    for sug in all_sugs:
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o:
-            new_q = Q(
-                text=sug.text,
-                score_with_o=sug.score_with_o,
-                from_source="sug"
-            )
-            q_list_next.append(new_q)
-            print(f"    ✓ {sug.text} (分数: {sug.score_with_o:.2f} > {sug.from_q.score_with_o:.2f})")
-
-    # 6. 更新seed_list
-    print(f"\n[步骤6] 更新seed_list...")
-    seed_list_next = seed_list.copy()  # 保留原有的seed
-
-    # 对于sug_list_list中,每个sug分数大于来源query分数的,且没在seed_list中出现过的,加入
-    existing_seed_texts = {seed.text for seed in seed_list_next}
-    for sug in all_sugs:
-        # 新逻辑:sug分数 > 对应query分数
-        if sug.from_q and sug.score_with_o > sug.from_q.score_with_o and sug.text not in existing_seed_texts:
-            new_seed = Seed(
-                text=sug.text,
-                added_words=[],
-                from_type="sug",
-                score_with_o=sug.score_with_o
-            )
-            seed_list_next.append(new_seed)
-            existing_seed_texts.add(sug.text)
-            print(f"  ✓ 新seed: {sug.text} (分数: {sug.score_with_o:.2f} > 来源query: {sug.from_q.score_with_o:.2f})")
-
-    # 记录本轮数据
-    round_data.update({
-        "sug_count": len(all_sugs),
-        "high_score_sug_count": len(high_score_sugs),
-        "search_count": len(search_list),
-        "total_posts": sum(len(s.post_list) for s in search_list),
-        "q_list_next_size": len(q_list_next),
-        "seed_list_next_size": len(seed_list_next),
-        "word_list_next_size": len(word_list_next),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "from": q.from_source} for q in q_list_next],
-        "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],  # 下一轮种子列表
-        "sug_details": sug_details,  # 每个Q对应的sug列表
-        "add_word_details": add_word_details  # 每个seed对应的组合词列表
-    })
-    context.rounds.append(round_data)
-
-    print(f"\n本轮总结:")
-    print(f"  建议词数量: {len(all_sugs)}")
-    print(f"  高分建议词: {len(high_score_sugs)}")
-    print(f"  搜索数量: {len(search_list)}")
-    print(f"  帖子总数: {sum(len(s.post_list) for s in search_list)}")
-    print(f"  下轮q数量: {len(q_list_next)}")
-    print(f"  seed数量: {len(seed_list_next)}")
-
-    return word_list_next, q_list_next, seed_list_next, search_list
-
-
-async def iterative_loop(
-    context: RunContext,
-    max_rounds: int = 2,
-    sug_threshold: float = 0.7
-):
-    """主迭代循环"""
-
-    print(f"\n{'='*60}")
-    print(f"开始迭代循环")
-    print(f"最大轮数: {max_rounds}")
-    print(f"sug阈值: {sug_threshold}")
-    print(f"{'='*60}")
-
-    # 初始化
-    seg_list, word_list, q_list, seed_list = await initialize(context.o, context)
-
-    # API实例
-    xiaohongshu_api = XiaohongshuSearchRecommendations()
-    xiaohongshu_search = XiaohongshuSearch()
-
-    # 保存初始化数据
-    context.rounds.append({
-        "round_num": 0,
-        "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o} for s in seg_list],
-        "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
-    })
-
-    # 收集所有搜索结果
-    all_search_list = []
-
-    # 迭代
-    round_num = 1
-    while q_list and round_num <= max_rounds:
-        word_list, q_list, seed_list, search_list = await run_round(
-            round_num=round_num,
-            q_list=q_list,
-            word_list=word_list,
-            seed_list=seed_list,
-            o=context.o,
-            context=context,
-            xiaohongshu_api=xiaohongshu_api,
-            xiaohongshu_search=xiaohongshu_search,
-            sug_threshold=sug_threshold
-        )
-
-        all_search_list.extend(search_list)
-        round_num += 1
-
-    print(f"\n{'='*60}")
-    print(f"迭代完成")
-    print(f"  总轮数: {round_num - 1}")
-    print(f"  总搜索次数: {len(all_search_list)}")
-    print(f"  总帖子数: {sum(len(s.post_list) for s in all_search_list)}")
-    print(f"{'='*60}")
-
-    return all_search_list
-
-
-# ============================================================================
-# 主函数
-# ============================================================================
-
-async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7, visualize: bool = False):
-    """主函数"""
-    current_time, log_url = set_trace()
-
-    # 读取输入
-    input_context_file = os.path.join(input_dir, 'context.md')
-    input_q_file = os.path.join(input_dir, 'q.md')
-
-    c = read_file_as_string(input_context_file)  # 原始需求
-    o = read_file_as_string(input_q_file)  # 原始问题
-
-    # 版本信息
-    version = os.path.basename(__file__)
-    version_name = os.path.splitext(version)[0]
-
-    # 日志目录
-    log_dir = os.path.join(input_dir, "output", version_name, current_time)
-
-    # 创建运行上下文
-    run_context = RunContext(
-        version=version,
-        input_files={
-            "input_dir": input_dir,
-            "context_file": input_context_file,
-            "q_file": input_q_file,
-        },
-        c=c,
-        o=o,
-        log_dir=log_dir,
-        log_url=log_url,
-    )
-
-    # 执行迭代
-    all_search_list = await iterative_loop(
-        run_context,
-        max_rounds=max_rounds,
-        sug_threshold=sug_threshold
-    )
-
-    # 格式化输出
-    output = f"原始需求:{run_context.c}\n"
-    output += f"原始问题:{run_context.o}\n"
-    output += f"总搜索次数:{len(all_search_list)}\n"
-    output += f"总帖子数:{sum(len(s.post_list) for s in all_search_list)}\n"
-    output += "\n" + "="*60 + "\n"
-
-    if all_search_list:
-        output += "【搜索结果】\n\n"
-        for idx, search in enumerate(all_search_list, 1):
-            output += f"{idx}. 搜索词: {search.text} (分数: {search.score_with_o:.2f})\n"
-            output += f"   帖子数: {len(search.post_list)}\n"
-            if search.post_list:
-                for post_idx, post in enumerate(search.post_list[:3], 1):  # 只显示前3个
-                    output += f"   {post_idx}) {post.title}\n"
-                    output += f"      URL: {post.note_url}\n"
-            output += "\n"
-    else:
-        output += "未找到搜索结果\n"
-
-    run_context.final_output = output
-
-    print(f"\n{'='*60}")
-    print("最终结果")
-    print(f"{'='*60}")
-    print(output)
-
-    # 保存日志
-    os.makedirs(run_context.log_dir, exist_ok=True)
-
-    context_file_path = os.path.join(run_context.log_dir, "run_context.json")
-    context_dict = run_context.model_dump()
-    with open(context_file_path, "w", encoding="utf-8") as f:
-        json.dump(context_dict, f, ensure_ascii=False, indent=2)
-    print(f"\nRunContext saved to: {context_file_path}")
-
-    # 保存详细的搜索结果
-    search_results_path = os.path.join(run_context.log_dir, "search_results.json")
-    search_results_data = [s.model_dump() for s in all_search_list]
-    with open(search_results_path, "w", encoding="utf-8") as f:
-        json.dump(search_results_data, f, ensure_ascii=False, indent=2)
-    print(f"Search results saved to: {search_results_path}")
-
-    # 可视化
-    if visualize:
-        import subprocess
-        output_html = os.path.join(run_context.log_dir, "visualization.html")
-        print(f"\n🎨 生成可视化HTML...")
-
-        # 获取绝对路径
-        abs_context_file = os.path.abspath(context_file_path)
-        abs_output_html = os.path.abspath(output_html)
-
-        # 运行可视化脚本
-        result = subprocess.run([
-            "node",
-            "visualization/sug_v6_1_2_8/index.js",
-            abs_context_file,
-            abs_output_html
-        ])
-
-        if result.returncode == 0:
-            print(f"✅ 可视化已生成: {output_html}")
-        else:
-            print(f"❌ 可视化生成失败")
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.8 轮次迭代版")
-    parser.add_argument(
-        "--input-dir",
-        type=str,
-        default="input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?",
-        help="输入目录路径,默认: input/旅游-逸趣玩旅行/如何获取能体现川西秋季特色的高质量风光摄影素材?"
-    )
-    parser.add_argument(
-        "--max-rounds",
-        type=int,
-        default=3,
-        help="最大轮数,默认: 2"
-    )
-    parser.add_argument(
-        "--sug-threshold",
-        type=float,
-        default=0.7,
-        help="suggestion阈值,默认: 0.7"
-    )
-    parser.add_argument(
-        "--visualize",
-        action="store_true",
-        default=True,
-        help="运行完成后自动生成可视化HTML"
-    )
-    args = parser.parse_args()
-
-    asyncio.run(main(args.input_dir, max_rounds=args.max_rounds, sug_threshold=args.sug_threshold, visualize=args.visualize))

+ 3 - 2
test_evaluation_v2.py

@@ -86,10 +86,11 @@ async def test_evaluation_v2(run_context_path: str, max_posts: int = 10):
         print(f"  [{i}/{len(tasks)}] 评估: {post.note_id}")
         knowledge_eval, relevance_eval = await task
 
-        if knowledge_eval and relevance_eval:
+        if knowledge_eval:
+            # 应用评估结果(可能只有知识评估,没有相关性评估)
             apply_evaluation_v2_to_post(post, knowledge_eval, relevance_eval)
             results.append((round_idx, search_idx, post_id, post, knowledge_eval, relevance_eval))
-            print(f"      ✅ 知识:{post.knowledge_score:.0f}分({post.knowledge_level}⭐) | 相关:{post.relevance_score:.0f}分({post.relevance_conclusion})")
+            # 输出已经在 evaluate_post_v2 中打印过了,这里不重复打印
         else:
             print(f"      ❌ 评估失败")
 

+ 298 - 0
test_evaluation_v3.py

@@ -0,0 +1,298 @@
+"""
+测试评估V3模块
+从现有run_context.json读取帖子,使用V3评估模块重新评估,生成统计报告
+"""
+
+import asyncio
+import json
+import sys
+from pathlib import Path
+from datetime import datetime
+from collections import defaultdict
+
+# 导入必要的模块
+from knowledge_search_traverse import Post
+from post_evaluator_v3 import evaluate_post_v3, apply_evaluation_v3_to_post
+
+
+async def test_evaluation_v3(run_context_path: str, max_posts: int = 10):
+    """
+    测试V3评估模块
+
+    Args:
+        run_context_path: run_context.json路径
+        max_posts: 最多评估的帖子数量(用于快速测试)
+    """
+    print(f"\n{'='*80}")
+    print(f"📊 评估V3测试 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    print(f"{'='*80}\n")
+
+    # 读取run_context.json
+    print(f"📂 读取: {run_context_path}")
+    with open(run_context_path, 'r', encoding='utf-8') as f:
+        run_context = json.load(f)
+
+    # 提取原始query
+    original_query = run_context.get('o', '')
+    print(f"🔍 原始Query: {original_query}\n")
+
+    # 提取所有帖子 (从rounds -> search_results -> post_list)
+    post_data_list = []
+    rounds = run_context.get('rounds', [])
+
+    for round_idx, round_data in enumerate(rounds):
+        search_results = round_data.get('search_results', [])
+        for search_idx, search in enumerate(search_results):
+            post_list = search.get('post_list', [])
+            for post_idx, post_data in enumerate(post_list):
+                # 生成唯一ID
+                post_id = f"r{round_idx}_s{search_idx}_p{post_idx}"
+                post_data_list.append((round_idx, search_idx, post_id, post_data))
+
+    total_posts = len(post_data_list)
+    print(f"📝 找到 {total_posts} 个帖子 (来自 {len(rounds)} 轮)")
+
+    # 限制评估数量(快速测试)
+    if max_posts and max_posts < total_posts:
+        post_data_list = post_data_list[:max_posts]
+        print(f"⚡ 快速测试模式: 仅评估前 {max_posts} 个帖子\n")
+    else:
+        print()
+
+    # 将post_data转换为Post对象
+    posts = []
+    for round_idx, search_idx, post_id, post_data in post_data_list:
+        post = Post(
+            note_id=post_data.get('note_id', post_id),
+            title=post_data.get('title', ''),
+            body_text=post_data.get('body_text', ''),
+            images=post_data.get('images', []),
+            type=post_data.get('type', 'normal')
+        )
+        posts.append((round_idx, search_idx, post_id, post))
+
+    # 批量评估
+    print(f"🚀 开始并行评估 (最多{len(posts)}个任务,并发限制: 5)...\n")
+
+    semaphore = asyncio.Semaphore(5)
+    tasks = []
+
+    # 1. 创建所有任务
+    for round_idx, search_idx, post_id, post in posts:
+        task = evaluate_post_v3(post, original_query, semaphore)
+        tasks.append((round_idx, search_idx, post_id, post, task))
+
+    # 2. 并行执行所有任务
+    task_coroutines = [task for _, _, _, _, task in tasks]
+    all_eval_results = await asyncio.gather(*task_coroutines)
+
+    # 3. 处理结果
+    results = []
+    print(f"📊 处理评估结果...\n")
+    for i, ((round_idx, search_idx, post_id, post, _), eval_result) in enumerate(zip(tasks, all_eval_results), 1):
+        knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = eval_result
+
+        print(f"  [{i}/{len(tasks)}] {post.note_id}", end="")
+        if knowledge_eval:
+            if final_score is not None:
+                print(f" → {match_level} ({final_score:.1f}分)")
+            elif content_eval and not content_eval.is_content_knowledge:
+                print(f" → 非内容知识")
+            elif knowledge_eval and not knowledge_eval.is_knowledge:
+                print(f" → 非知识")
+            else:
+                print(f" → 评估未完成")
+
+            # 应用评估结果
+            apply_evaluation_v3_to_post(
+                post,
+                knowledge_eval,
+                content_eval,
+                purpose_eval,
+                category_eval,
+                final_score,
+                match_level
+            )
+            results.append((round_idx, search_idx, post_id, post))
+        else:
+            print(f" → ❌ 评估失败")
+
+    print(f"\n✅ 评估完成: {len(results)}/{len(posts)} 成功\n")
+
+    # 更新run_context.json中的帖子数据
+    print("💾 更新 run_context.json...")
+    for round_idx, search_idx, post_id, post in results:
+        # 定位到对应的post_list
+        if round_idx < len(rounds):
+            search_results = rounds[round_idx].get('search_results', [])
+            if search_idx < len(search_results):
+                post_list = search_results[search_idx].get('post_list', [])
+
+                # 找到对应的帖子并更新
+                for p in post_list:
+                    if p.get('note_id') == post.note_id:
+                        # 更新V3顶层字段
+                        p['is_knowledge'] = post.is_knowledge
+                        p['is_content_knowledge'] = post.is_content_knowledge
+                        p['knowledge_score'] = post.knowledge_score
+
+                        p['purpose_score'] = post.purpose_score
+                        p['category_score'] = post.category_score
+                        p['final_score'] = post.final_score
+                        p['match_level'] = post.match_level
+
+                        p['evaluation_time'] = post.evaluation_time
+                        p['evaluator_version'] = post.evaluator_version
+
+                        # 更新V3嵌套字段
+                        p['knowledge_evaluation'] = post.knowledge_evaluation
+                        p['content_knowledge_evaluation'] = post.content_knowledge_evaluation
+                        p['purpose_evaluation'] = post.purpose_evaluation
+                        p['category_evaluation'] = post.category_evaluation
+                        break
+
+    # 保存更新后的run_context.json
+    output_path = run_context_path.replace('.json', '_v3.json')
+    with open(output_path, 'w', encoding='utf-8') as f:
+        json.dump(run_context, f, ensure_ascii=False, indent=2)
+    print(f"✅ 已保存: {output_path}\n")
+
+    # 生成统计报告
+    print(f"\n{'='*80}")
+    print("📊 统计报告")
+    print(f"{'='*80}\n")
+
+    # Prompt1: 是否是知识
+    is_knowledge_counts = defaultdict(int)
+    for _, _, _, post in results:
+        if post.is_knowledge:
+            is_knowledge_counts['是知识'] += 1
+        else:
+            is_knowledge_counts['非知识'] += 1
+
+    total = len(results)
+    print("🔍 Prompt1 - 是否是知识:")
+    print(f"  是知识: {is_knowledge_counts['是知识']:3d} / {total} ({is_knowledge_counts['是知识']/total*100:.1f}%)")
+    print(f"  非知识: {is_knowledge_counts['非知识']:3d} / {total} ({is_knowledge_counts['非知识']/total*100:.1f}%)")
+    print()
+
+    # Prompt2: 是否是内容知识
+    is_content_knowledge_counts = defaultdict(int)
+    knowledge_scores = []
+    for _, _, _, post in results:
+        if post.is_content_knowledge is not None:
+            if post.is_content_knowledge:
+                is_content_knowledge_counts['是内容知识'] += 1
+            else:
+                is_content_knowledge_counts['非内容知识'] += 1
+
+        if post.knowledge_score is not None:
+            knowledge_scores.append(post.knowledge_score)
+
+    if is_content_knowledge_counts:
+        content_total = sum(is_content_knowledge_counts.values())
+        print("📚 Prompt2 - 是否是内容知识:")
+        print(f"  是内容知识: {is_content_knowledge_counts['是内容知识']:3d} / {content_total} ({is_content_knowledge_counts['是内容知识']/content_total*100:.1f}%)")
+        if is_content_knowledge_counts['非内容知识'] > 0:
+            print(f"  非内容知识: {is_content_knowledge_counts['非内容知识']:3d} / {content_total} ({is_content_knowledge_counts['非内容知识']/content_total*100:.1f}%)")
+        print()
+
+    if knowledge_scores:
+        avg_score = sum(knowledge_scores) / len(knowledge_scores)
+        print(f"  知识平均得分: {avg_score:.1f}分")
+        print(f"  知识最高得分: {max(knowledge_scores):.0f}分")
+        print(f"  知识最低得分: {min(knowledge_scores):.0f}分")
+        print()
+
+    # Prompt3 & Prompt4: 目的性和品类匹配
+    purpose_scores = []
+    category_scores = []
+    final_scores = []
+    match_level_counts = defaultdict(int)
+
+    for _, _, _, post in results:
+        if post.purpose_score is not None:
+            purpose_scores.append(post.purpose_score)
+        if post.category_score is not None:
+            category_scores.append(post.category_score)
+        if post.final_score is not None:
+            final_scores.append(post.final_score)
+        if post.match_level:
+            match_level_counts[post.match_level] += 1
+
+    if purpose_scores:
+        avg_purpose = sum(purpose_scores) / len(purpose_scores)
+        print("🎯 Prompt3 - 目的性匹配:")
+        print(f"  平均得分: {avg_purpose:.1f}分")
+        print(f"  最高得分: {max(purpose_scores):.0f}分")
+        print(f"  最低得分: {min(purpose_scores):.0f}分")
+        print()
+
+    if category_scores:
+        avg_category = sum(category_scores) / len(category_scores)
+        print("🏷️  Prompt4 - 品类匹配:")
+        print(f"  平均得分: {avg_category:.1f}分")
+        print(f"  最高得分: {max(category_scores):.0f}分")
+        print(f"  最低得分: {min(category_scores):.0f}分")
+        print()
+
+    if final_scores:
+        avg_final = sum(final_scores) / len(final_scores)
+        print("🔥 综合得分 (目的性70% + 品类30%):")
+        print(f"  平均得分: {avg_final:.2f}分")
+        print(f"  最高得分: {max(final_scores):.2f}分")
+        print(f"  最低得分: {min(final_scores):.2f}分")
+        print()
+
+    if match_level_counts:
+        print("📊 匹配等级分布:")
+        for level in ['高度匹配', '基本匹配', '部分匹配', '弱匹配', '不匹配']:
+            count = match_level_counts.get(level, 0)
+            if count > 0:
+                bar = '█' * int(count / total * 50)
+                print(f"  {level:8s}: {count:3d} / {total} ({count/total*100:.1f}%) {bar}")
+        print()
+
+    # 综合分析
+    print("🌟 高质量内容统计:")
+
+    # 是知识 + 是内容知识
+    is_quality_knowledge = sum(
+        1 for _, _, _, post in results
+        if post.is_knowledge and post.is_content_knowledge
+    )
+    print(f"  知识内容: {is_quality_knowledge} / {total} ({is_quality_knowledge/total*100:.1f}%)")
+
+    # 是知识 + 是内容知识 + 高度匹配
+    high_match = sum(
+        1 for _, _, _, post in results
+        if post.is_knowledge and post.is_content_knowledge and post.match_level == '高度匹配'
+    )
+    print(f"  高度匹配: {high_match} / {total} ({high_match/total*100:.1f}%)")
+
+    # 是知识 + 是内容知识 + 综合得分>=70
+    high_score = sum(
+        1 for _, _, _, post in results
+        if post.is_knowledge and post.is_content_knowledge and post.final_score and post.final_score >= 70
+    )
+    print(f"  得分≥70:  {high_score} / {total} ({high_score/total*100:.1f}%)")
+    print()
+
+    print(f"{'='*80}\n")
+
+    return results
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 2:
+        print("用法: python3 test_evaluation_v3.py <run_context.json路径> [最大评估数量]")
+        print()
+        print("示例:")
+        print("  python3 test_evaluation_v3.py input/test_case/output/knowledge_search_traverse/20251112/173512_dc/run_context.json")
+        print("  python3 test_evaluation_v3.py input/test_case/output/knowledge_search_traverse/20251112/173512_dc/run_context.json 20")
+        sys.exit(1)
+
+    run_context_path = sys.argv[1]
+    max_posts = int(sys.argv[2]) if len(sys.argv) > 2 else None
+
+    asyncio.run(test_evaluation_v3(run_context_path, max_posts))

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 22463
visualization.html


+ 0 - 887
visualization/knowledge_search_traverse/convert_v8_to_graph_v2.js

@@ -1,887 +0,0 @@
-/**
- * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构
- */
-
-function convertV8ToGraphV2(runContext, searchResults) {
-  const nodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  nodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true
-  };
-
-  iterations[0] = [rootId];
-
-  // 处理每一轮
-  rounds.forEach((round, roundIndex) => {
-    if (round.type === 'initialization') {
-      // Round 0: 初始化阶段
-      const roundNum = 0;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum} (初始化)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '初始化',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: '初始化'
-      });
-
-      if (!iterations[roundNum]) iterations[roundNum] = [];
-      iterations[roundNum].push(roundId);
-
-      // 创建分词步骤节点
-      const segStepId = `step_seg_r${roundNum}`;
-      nodes[segStepId] = {
-        type: 'step',
-        query: `步骤:分词 (${round.seg_list?.length || 0}个分词)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '分词',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: segStepId,
-        edge_type: 'round_to_step',
-        strategy: '分词'
-      });
-
-      iterations[roundNum].push(segStepId);
-
-      // 添加分词结果作为步骤的子节点
-      round.seg_list?.forEach((seg, segIndex) => {
-        const segId = `seg_${seg.text}_${roundNum}_${segIndex}`;
-        nodes[segId] = {
-          type: 'seg',
-          query: seg.text,
-          level: roundNum + 1,
-          relevance_score: seg.score || 0,
-          evaluationReason: seg.reason || '',
-          strategy: '分词结果',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: segStepId,
-          to: segId,
-          edge_type: 'step_to_data',
-          strategy: '分词结果'
-        });
-
-        if (!iterations[roundNum + 1]) iterations[roundNum + 1] = [];
-        iterations[roundNum + 1].push(segId);
-      });
-
-    } else {
-      // 普通轮次
-      const roundNum = round.round_num;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum}`,
-        level: roundNum * 10, // 使用10的倍数作为层级
-        relevance_score: 0,
-        strategy: `第${roundNum}轮`,
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: `第${roundNum}轮`
-      });
-
-      if (!iterations[roundNum * 10]) iterations[roundNum * 10] = [];
-      iterations[roundNum * 10].push(roundId);
-
-      // 步骤1: 请求&评估推荐词
-      if (round.sug_details && Object.keys(round.sug_details).length > 0) {
-        const sugStepId = `step_sug_r${roundNum}`;
-        const totalSugs = Object.values(round.sug_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[sugStepId] = {
-          type: 'step',
-          query: `步骤1: 请求&评估推荐词 (${totalSugs}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '请求&评估推荐词',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: sugStepId,
-          edge_type: 'round_to_step',
-          strategy: '推荐词'
-        });
-
-        iterations[roundNum * 10].push(sugStepId);
-
-        // 为每个 Q 创建节点
-        Object.keys(round.sug_details).forEach((qText, qIndex) => {
-          // 从q_list_1中查找对应的q获取分数和理由
-          // Round 0: 从q_list_1查找; Round 1+: 从input_q_list查找
-          let qData = {};
-          if (roundNum === 0) {
-            qData = round.q_list_1?.find(q => q.text === qText) || {};
-          } else {
-            // 从当前轮的input_q_list中查找
-            qData = round.input_q_list?.find(q => q.text === qText) || {};
-          }
-          const qId = `q_${qText}_r${roundNum}_${qIndex}`;
-          nodes[qId] = {
-            type: 'q',
-            query: qText,
-            level: roundNum * 10 + 2,
-            relevance_score: qData.score || 0,
-            evaluationReason: qData.reason || '',
-            strategy: 'Query',
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: sugStepId,
-            to: qId,
-            edge_type: 'step_to_q',
-            strategy: 'Query'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(qId);
-
-          // 为每个 Q 的 sug 创建节点
-          const sugs = round.sug_details[qText] || [];
-          sugs.forEach((sug, sugIndex) => {
-            const sugId = `sug_${sug.text}_r${roundNum}_q${qIndex}_${sugIndex}`;
-            nodes[sugId] = {
-              type: 'sug',
-              query: sug.text,
-              level: roundNum * 10 + 3,
-              relevance_score: sug.score || 0,
-              evaluationReason: sug.reason || '',
-              strategy: '推荐词',
-              iteration: roundNum,
-              is_selected: true
-            };
-
-            edges.push({
-              from: qId,
-              to: sugId,
-              edge_type: 'q_to_sug',
-              strategy: '推荐词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(sugId);
-          });
-        });
-      }
-
-      // 步骤2: 筛选并执行搜索
-      const searchStepId = `step_search_r${roundNum}`;
-      const searchCountText = round.search_count > 0
-        ? `筛选${round.high_score_sug_count}个高分词,搜索${round.search_count}次,${round.total_posts}个帖子`
-        : `无高分推荐词,未执行搜索`;
-
-      nodes[searchStepId] = {
-        type: 'step',
-        query: `步骤2: 筛选并执行搜索 (${searchCountText})`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '筛选并执行搜索',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: searchStepId,
-        edge_type: 'round_to_step',
-        strategy: '搜索'
-      });
-
-      iterations[roundNum * 10].push(searchStepId);
-
-      // 只有在有搜索结果时才添加搜索词和帖子
-      // 优先使用 round.search_results(新格式),否则使用外部传入的 searchResults(兼容旧版本)
-      const roundSearchResults = round.search_results || searchResults;
-      if (round.search_count > 0 && roundSearchResults) {
-        if (Array.isArray(roundSearchResults)) {
-          roundSearchResults.forEach((search, searchIndex) => {
-            const searchWordId = `search_${search.text}_r${roundNum}_${searchIndex}`;
-            nodes[searchWordId] = {
-              type: 'search_word',
-              query: search.text,
-              level: roundNum * 10 + 2,
-              relevance_score: search.score_with_o || 0,
-              strategy: '搜索词',
-              iteration: roundNum,
-              is_selected: true
-            };
-
-            edges.push({
-              from: searchStepId,
-              to: searchWordId,
-              edge_type: 'step_to_search_word',
-              strategy: '搜索词'
-            });
-
-            if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-            iterations[roundNum * 10 + 2].push(searchWordId);
-
-            // 添加帖子
-            if (search.post_list && search.post_list.length > 0) {
-              search.post_list.forEach((post, postIndex) => {
-                const postId = `post_${post.note_id}_${searchIndex}_${postIndex}`;
-
-                // 准备图片列表,将URL字符串转换为对象格式供轮播图使用
-                const imageList = (post.images || []).map(url => ({
-                  image_url: url
-                }));
-
-                nodes[postId] = {
-                  type: 'post',
-                  query: post.title,
-                  level: roundNum * 10 + 3,
-                  relevance_score: 0,
-                  strategy: '帖子',
-                  iteration: roundNum,
-                  is_selected: true,
-                  note_id: post.note_id,
-                  note_url: post.note_url,
-                  body_text: post.body_text || '',
-                  images: post.images || [],
-                  image_list: imageList,
-                  interact_info: post.interact_info || {}
-                };
-
-                edges.push({
-                  from: searchWordId,
-                  to: postId,
-                  edge_type: 'search_word_to_post',
-                  strategy: '搜索结果'
-                });
-
-                if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-                iterations[roundNum * 10 + 3].push(postId);
-              });
-            }
-          });
-        }
-      }
-
-      // 步骤3: 加词生成新查询
-      if (round.add_word_details && Object.keys(round.add_word_details).length > 0) {
-        const addWordStepId = `step_add_r${roundNum}`;
-        const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[addWordStepId] = {
-          type: 'step',
-          query: `步骤3: 加词生成新查询 (${totalAddWords}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '加词生成新查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: addWordStepId,
-          edge_type: 'round_to_step',
-          strategy: '加词'
-        });
-
-        iterations[roundNum * 10].push(addWordStepId);
-
-        // 为每个 Seed 创建节点
-        Object.keys(round.add_word_details).forEach((seedText, seedIndex) => {
-          const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`;
-
-          // 查找seed的来源信息 - 从Round 0的seed_list查找基础种子的from_type
-          const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
-          const seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
-          const fromType = seedInfo.from_type || 'unknown';
-
-          // 根据来源设置strategy
-          let strategy;
-          if (fromType === 'seg') {
-            strategy = '初始分词';
-          } else if (fromType === 'add') {
-            strategy = '加词';
-          } else if (fromType === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed';  // 默认灰色
-          }
-
-          nodes[seedId] = {
-            type: 'seed',
-            query: seedText,
-            level: roundNum * 10 + 2,
-            relevance_score: 0,
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: addWordStepId,
-            to: seedId,
-            edge_type: 'step_to_seed',
-            strategy: 'Seed'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(seedId);
-
-          // 为每个 Seed 的组合词创建节点
-          const combinedWords = round.add_word_details[seedText] || [];
-          combinedWords.forEach((word, wordIndex) => {
-            const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`;
-            nodes[wordId] = {
-              type: 'add_word',
-              query: word.text,
-              level: roundNum * 10 + 3,
-              relevance_score: word.score || 0,
-              evaluationReason: word.reason || '',
-              strategy: '加词生成',
-              iteration: roundNum,
-              is_selected: true,
-              selected_word: word.selected_word
-            };
-
-            edges.push({
-              from: seedId,
-              to: wordId,
-              edge_type: 'seed_to_add_word',
-              strategy: '组合词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(wordId);
-          });
-        });
-      }
-
-      // 步骤4: 筛选推荐词进入下轮
-      const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || [];
-      if (filteredSugs.length > 0) {
-        const filterStepId = `step_filter_r${roundNum}`;
-        nodes[filterStepId] = {
-          type: 'step',
-          query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '筛选推荐词进入下轮',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: filterStepId,
-          edge_type: 'round_to_step',
-          strategy: '筛选'
-        });
-
-        iterations[roundNum * 10].push(filterStepId);
-
-        // 添加筛选出的sug
-        filteredSugs.forEach((sug, sugIndex) => {
-          const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`;
-          nodes[sugId] = {
-            type: 'filtered_sug',
-            query: sug.text,
-            level: roundNum * 10 + 2,
-            relevance_score: sug.score || 0,
-            strategy: '进入下轮',
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: filterStepId,
-            to: sugId,
-            edge_type: 'step_to_filtered_sug',
-            strategy: '进入下轮'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(sugId);
-        });
-      }
-
-      // 步骤5: 构建下一轮
-      const nextRoundStepId = `step_next_round_r${roundNum}`;
-      const nextQCount = round.output_q_list?.length || 0;
-      const nextSeedCount = round.seed_list_next_size || 0;
-
-      nodes[nextRoundStepId] = {
-        type: 'step',
-        query: `步骤5: 构建下一轮 (${nextQCount}个查询, ${nextSeedCount}个种子)`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '构建下一轮',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: nextRoundStepId,
-        edge_type: 'round_to_step',
-        strategy: '构建下一轮'
-      });
-
-      iterations[roundNum * 10].push(nextRoundStepId);
-
-      // 5.1: 构建下轮查询
-      if (round.output_q_list && round.output_q_list.length > 0) {
-        const nextQStepId = `step_next_q_r${roundNum}`;
-        nodes[nextQStepId] = {
-          type: 'step',
-          query: `构建下轮查询 (${nextQCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextQStepId,
-          edge_type: 'step_to_step',
-          strategy: '查询'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextQStepId);
-
-        // 添加下轮查询列表
-        round.output_q_list.forEach((q, qIndex) => {
-          const nextQId = `next_q_${q.text}_r${roundNum}_${qIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (q.from === 'seg') {
-            strategy = '初始分词';
-          } else if (q.from === 'add') {
-            strategy = '加词';
-          } else if (q.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Query'; // 默认
-          }
-
-          nodes[nextQId] = {
-            type: 'next_q',
-            query: q.text,
-            level: roundNum * 10 + 3,
-            relevance_score: q.score || 0,
-            evaluationReason: q.reason || '',
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: q.from
-          };
-
-          edges.push({
-            from: nextQStepId,
-            to: nextQId,
-            edge_type: 'step_to_next_q',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextQId);
-        });
-      }
-
-      // 5.2: 构建下轮种子(如果有数据的话)
-      if (nextSeedCount > 0 && round.seed_list_next) {
-        const nextSeedStepId = `step_next_seed_r${roundNum}`;
-        nodes[nextSeedStepId] = {
-          type: 'step',
-          query: `构建下轮种子 (${nextSeedCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮种子',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextSeedStepId,
-          edge_type: 'step_to_step',
-          strategy: '种子'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextSeedStepId);
-
-        // 添加下轮种子列表
-        round.seed_list_next.forEach((seed, seedIndex) => {
-          const nextSeedId = `next_seed_${seed.text}_r${roundNum}_${seedIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (seed.from === 'seg') {
-            strategy = '初始分词';
-          } else if (seed.from === 'add') {
-            strategy = '加词';
-          } else if (seed.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed'; // 默认
-          }
-
-          nodes[nextSeedId] = {
-            type: 'next_seed',
-            query: seed.text,
-            level: roundNum * 10 + 3,
-            relevance_score: seed.score || 0,
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: seed.from
-          };
-
-          edges.push({
-            from: nextSeedStepId,
-            to: nextSeedId,
-            edge_type: 'step_to_next_seed',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextSeedId);
-        });
-      }
-    }
-  });
-
-  return {
-    nodes,
-    edges,
-    iterations
-  };
-}
-
-/**
- * 简化版转换:专注于query和post的演化
- * - 合并所有query节点(不区分seg/sug/add_word)
- * - 合并相同的帖子节点
- * - 步骤信息放在边上
- * - 隐藏Round/Step节点
- */
-function convertV8ToGraphSimplified(runContext, searchResults) {
-  const mergedNodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  mergedNodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true,
-    occurrences: [{round: 0, role: 'root', score: 1.0}]
-  };
-  iterations[0] = [rootId];
-
-  // 用于记录节点之间的演化关系
-  const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
-  const postMap = {}; // {note_id: {...}}
-
-  // 第一遍:收集所有query和post
-  rounds.forEach((round, roundIndex) => {
-    const roundNum = round.round_num || roundIndex;
-
-    if (round.type === 'initialization') {
-      // Round 0: 收集分词结果
-      (round.q_list_1 || []).forEach(q => {
-        if (!queryEvolution[q.text]) {
-          queryEvolution[q.text] = {
-            occurrences: [],
-            parentTexts: new Set([o]), // 来自原始问题
-            childTexts: new Set()
-          };
-        }
-        queryEvolution[q.text].occurrences.push({
-          round: roundNum,
-          role: 'segmentation',
-          strategy: '分词',
-          score: q.score,
-          reason: q.reason
-        });
-      });
-    } else {
-      // Round 1+
-
-      // 收集sug_details (推荐词)
-      Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
-        sugs.forEach(sug => {
-          if (!queryEvolution[sug.text]) {
-            queryEvolution[sug.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[sug.text].occurrences.push({
-            round: roundNum,
-            role: 'sug',
-            strategy: '调用sug',
-            score: sug.score,
-            reason: sug.reason
-          });
-          queryEvolution[sug.text].parentTexts.add(parentText);
-          if (queryEvolution[parentText]) {
-            queryEvolution[parentText].childTexts.add(sug.text);
-          }
-        });
-      });
-
-      // 收集add_word_details (加词结果)
-      Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
-        words.forEach(word => {
-          if (!queryEvolution[word.text]) {
-            queryEvolution[word.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[word.text].occurrences.push({
-            round: roundNum,
-            role: 'add_word',
-            strategy: '加词',
-            score: word.score,
-            reason: word.reason,
-            selectedWord: word.selected_word
-          });
-          queryEvolution[word.text].parentTexts.add(seedText);
-          if (queryEvolution[seedText]) {
-            queryEvolution[seedText].childTexts.add(word.text);
-          }
-        });
-      });
-
-      // 收集搜索结果和帖子
-      const roundSearchResults = round.search_results || searchResults;
-      if (roundSearchResults && Array.isArray(roundSearchResults)) {
-        roundSearchResults.forEach(search => {
-          const searchText = search.text;
-
-          // 标记这个query被用于搜索
-          if (queryEvolution[searchText]) {
-            queryEvolution[searchText].occurrences.push({
-              round: roundNum,
-              role: 'search',
-              strategy: '执行搜索',
-              score: search.score_with_o,
-              postCount: search.post_list ? search.post_list.length : 0
-            });
-          }
-
-          // 收集帖子
-          if (search.post_list && search.post_list.length > 0) {
-            search.post_list.forEach(post => {
-              if (!postMap[post.note_id]) {
-                postMap[post.note_id] = {
-                  ...post,
-                  foundByQueries: new Set(),
-                  foundInRounds: new Set()
-                };
-              }
-              postMap[post.note_id].foundByQueries.add(searchText);
-              postMap[post.note_id].foundInRounds.add(roundNum);
-
-              // 建立query到post的关系
-              if (!queryEvolution[searchText].posts) {
-                queryEvolution[searchText].posts = new Set();
-              }
-              queryEvolution[searchText].posts.add(post.note_id);
-            });
-          }
-        });
-      }
-    }
-  });
-
-  // 第二遍:创建合并后的节点
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    // 获取最新的分数
-    const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
-    const hasSearchResults = data.posts && data.posts.size > 0;
-
-    mergedNodes[nodeId] = {
-      type: 'query',
-      query: text,
-      level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
-      relevance_score: latestOccurrence.score || 0,
-      evaluationReason: latestOccurrence.reason || '',
-      strategy: data.occurrences.map(o => o.strategy).join(' + '),
-      primaryStrategy: latestOccurrence.strategy || '未知',  // 添加主要策略字段
-      iteration: Math.max(...data.occurrences.map(o => o.round), 0),
-      is_selected: true,
-      occurrences: data.occurrences,
-      hasSearchResults: hasSearchResults,
-      postCount: data.posts ? data.posts.size : 0,
-      selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || ''
-    };
-
-    // 添加到对应的轮次
-    const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
-    const iterKey = maxRound * 10 + 2;
-    if (!iterations[iterKey]) iterations[iterKey] = [];
-    iterations[iterKey].push(nodeId);
-  });
-
-  // 创建帖子节点
-  Object.entries(postMap).forEach(([noteId, post]) => {
-    const postId = `post_${noteId}`;
-
-    const imageList = (post.images || []).map(url => ({
-      image_url: url
-    }));
-
-    mergedNodes[postId] = {
-      type: 'post',
-      query: post.title,
-      level: 100, // 放在最后
-      relevance_score: 0,
-      strategy: '帖子',
-      iteration: Math.max(...Array.from(post.foundInRounds)),
-      is_selected: true,
-      note_id: post.note_id,
-      note_url: post.note_url,
-      body_text: post.body_text || '',
-      images: post.images || [],
-      image_list: imageList,
-      interact_info: post.interact_info || {},
-      foundByQueries: Array.from(post.foundByQueries),
-      foundInRounds: Array.from(post.foundInRounds)
-    };
-
-    if (!iterations[100]) iterations[100] = [];
-    iterations[100].push(postId);
-  });
-
-  // 第三遍:创建边
-  // 1. 原始问题 -> 分词结果
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-    const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
-
-    if (segOccurrence && data.parentTexts.has(o)) {
-      edges.push({
-        from: rootId,
-        to: nodeId,
-        edge_type: 'segmentation',
-        strategy: '分词',
-        label: '分词',
-        round: 0
-      });
-    }
-  });
-
-  // 2. Query演化关系
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    data.parentTexts.forEach(parentText => {
-      if (parentText === o) return; // 跳过原始问题(已处理)
-
-      const parentNodeId = `query_${parentText}`;
-      if (!mergedNodes[parentNodeId]) return;
-
-      // 找到这个演化的策略和轮次
-      const occurrence = data.occurrences.find(o =>
-        o.role === 'sug' || o.role === 'add_word'
-      );
-
-      edges.push({
-        from: parentNodeId,
-        to: nodeId,
-        edge_type: occurrence?.role || 'evolution',
-        strategy: occurrence?.strategy || '演化',
-        label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
-        round: occurrence?.round || 0
-      });
-    });
-  });
-
-  // 3. Query -> Post (搜索关系)
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    if (data.posts && data.posts.size > 0) {
-      const searchOccurrence = data.occurrences.find(o => o.role === 'search');
-
-      data.posts.forEach(noteId => {
-        const postId = `post_${noteId}`;
-        edges.push({
-          from: nodeId,
-          to: postId,
-          edge_type: 'search',
-          strategy: '搜索',
-          label: `搜索 (${data.posts.size}个帖子)`,
-          round: searchOccurrence?.round || 0
-        });
-      });
-    }
-  });
-
-  return {
-    nodes: mergedNodes,
-    edges,
-    iterations
-  };
-}
-
-module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };

+ 24 - 3
visualization/knowledge_search_traverse/convert_v8_to_graph_v3.js

@@ -470,7 +470,18 @@ function convertV8ToGraphV2(runContext, searchResults, extractionData) {
                   relevance_level: post.relevance_level || '',
                   relevance_reason: post.relevance_reason || '',
                   relevance_conclusion: post.relevance_conclusion || '',
-                  relevance_evaluation: post.relevance_evaluation || null
+                  relevance_evaluation: post.relevance_evaluation || null,
+
+                  // 评估数据 (V3)
+                  is_content_knowledge: post.is_content_knowledge !== undefined ? post.is_content_knowledge : null,
+                  purpose_score: post.purpose_score !== undefined ? post.purpose_score : null,
+                  category_score: post.category_score !== undefined ? post.category_score : null,
+                  final_score: post.final_score !== undefined ? post.final_score : null,
+                  match_level: post.match_level || '',
+                  evaluator_version: post.evaluator_version || '',
+                  content_knowledge_evaluation: post.content_knowledge_evaluation || null,
+                  purpose_evaluation: post.purpose_evaluation || null,
+                  category_evaluation: post.category_evaluation || null
                 };
 
                 edges.push({
@@ -982,12 +993,22 @@ function convertV8ToGraphSimplified(runContext, searchResults, extractionData) {
       foundInRounds: Array.from(post.foundInRounds),
       // 附加多模态提取数据
       extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null,
-      // 评估数据
+      // 评估数据 (V2)
       is_knowledge: post.is_knowledge !== undefined ? post.is_knowledge : null,
       knowledge_reason: post.knowledge_reason || '',
       post_relevance_score: post.relevance_score !== undefined ? post.relevance_score : null,
       relevance_level: post.relevance_level || '',
-      relevance_reason: post.relevance_reason || ''
+      relevance_reason: post.relevance_reason || '',
+      // 评估数据 (V3)
+      is_content_knowledge: post.is_content_knowledge !== undefined ? post.is_content_knowledge : null,
+      purpose_score: post.purpose_score !== undefined ? post.purpose_score : null,
+      category_score: post.category_score !== undefined ? post.category_score : null,
+      final_score: post.final_score !== undefined ? post.final_score : null,
+      match_level: post.match_level || '',
+      evaluator_version: post.evaluator_version || '',
+      content_knowledge_evaluation: post.content_knowledge_evaluation || null,
+      purpose_evaluation: post.purpose_evaluation || null,
+      category_evaluation: post.category_evaluation || null
     };
 
     if (!iterations[100]) iterations[100] = [];

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 4763 - 24414
visualization/knowledge_search_traverse/debug_component.jsx


+ 220 - 112
visualization/knowledge_search_traverse/index.js

@@ -494,6 +494,7 @@ function QueryNode({ id, data, sourcePosition, targetPosition }) {
 // 笔记节点组件 - 卡片样式,带轮播图
 function NoteNode({ id, data, sourcePosition, targetPosition }) {
   const [currentImageIndex, setCurrentImageIndex] = useState(0);
+  const [showEvalDetails, setShowEvalDetails] = useState(false);
   const expanded = true;
   const hasImages = data.imageList && data.imageList.length > 0;
 
@@ -543,41 +544,162 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
           cursor: 'pointer',
         }}
       >
-        {/* 笔记标题 */}
-        <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '16px' }}>
-          <div style={{ flex: 1 }}>
-            <div style={{
-              fontSize: '40px',
-              fontWeight: '600',
-              color: '#831843',
-              lineHeight: '1.4',
-              marginBottom: '8px',
-            }}>
-              {data.title}
-            </div>
-          </div>
-        </div>
-
-        {/* 🆕 原始问题展示 - 在标题下方 */}
+        {/* 🆕 原始问题展示 - 最顶部 */}
         {data.originalQuestion && (
           <div style={{
-            marginBottom: '24px',
-            paddingBottom: '24px',
+            marginBottom: '20px',
+            paddingBottom: '20px',
             borderBottom: '2px solid #fce7f3',
           }}>
             <div style={{
-              fontSize: '40px',
+              fontSize: '36px',
               color: '#6b21a8',
               lineHeight: '1.4',
               fontWeight: '600',
             }}>
-              <strong style={{ fontWeight: '700' }}>[原始需求问题]</strong> {data.originalQuestion}
+              <span style={{ fontWeight: '700' }}>[原始需求问题]</span> {data.originalQuestion}
             </div>
           </div>
         )}
 
-        {/* 评估信息区域 (V2) */}
-        {(data.knowledge_score !== undefined || data.post_relevance_score !== undefined || data.is_knowledge !== undefined) && (
+        {/* 帖子标题 - 明确标注 */}
+        <div style={{ marginBottom: '20px', paddingBottom: '16px', borderBottom: '2px solid #fce7f3' }}>
+          <div style={{ fontSize: '38px', fontWeight: '600', color: '#831843', lineHeight: '1.4' }}>
+            <span style={{ fontSize: '32px', color: '#831843', fontWeight: '500' }}>帖子标题: </span>
+            {data.title.replace(/^\[R\]\s*/, '')}
+          </div>
+        </div>
+
+        {/* V3评估信息 - 可展开 */}
+        {data.evaluator_version === 'v3.0' && (
+          <div style={{ marginBottom: '20px', paddingBottom: '16px', borderBottom: '2px solid #fce7f3' }}>
+            {/* 第1行:知识判定 + 内容知识 + 星级 */}
+            <div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '10px', flexWrap: 'wrap' }}>
+              <span style={{ fontSize: '28px', fontWeight: '600', color: data.is_knowledge ? '#166534' : '#991b1b' }}>
+                {data.is_knowledge ? '✓ 是知识' : '✗ 非知识'}
+              </span>
+              {data.is_content_knowledge !== null && data.is_content_knowledge !== undefined && (
+                <>
+                  <span style={{ fontSize: '28px', fontWeight: '600', color: data.is_content_knowledge ? '#166534' : '#991b1b' }}>
+                    {data.is_content_knowledge ? '✓ 是内容知识' : '✗ 非内容知识'}
+                  </span>
+                  {data.is_content_knowledge && data.content_knowledge_evaluation?.knowledge_score != null && (
+                    <span style={{ fontSize: '24px', lineHeight: '1' }}>
+                      {'⭐'.repeat(Math.min(5, Math.ceil(data.content_knowledge_evaluation.knowledge_score / 20)))}
+                    </span>
+                  )}
+                  {data.is_content_knowledge && data.content_knowledge_evaluation?.knowledge_score != null && (
+                    <span style={{ fontSize: '26px', fontWeight: '600', color: '#166534' }}>
+                      {data.content_knowledge_evaluation.knowledge_score}分
+                    </span>
+                  )}
+                </>
+              )}
+            </div>
+
+            {/* 第2行:匹配度得分 + 详情 + 展开按钮 */}
+            {data.is_content_knowledge && data.final_score !== null && data.final_score !== undefined && (
+              <div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap', marginBottom: '12px' }}>
+                <span style={{ fontSize: '32px', fontWeight: '700', color: data.final_score >= 60 ? '#166534' : '#ea580c' }}>
+                  匹配度得分 {data.final_score.toFixed(1)}分
+                </span>
+                <span style={{
+                  padding: '4px 16px',
+                  borderRadius: '20px',
+                  fontSize: '26px',
+                  fontWeight: '600',
+                  background: data.final_score >= 85 ? '#dcfce7' : data.final_score >= 60 ? '#fef3c7' : '#fee2e2',
+                  color: data.final_score >= 85 ? '#166534' : data.final_score >= 60 ? '#854d0e' : '#991b1b'
+                }}>
+                  {data.match_level}
+                </span>
+                {data.purpose_score != null && (
+                  <span style={{ fontSize: '26px', color: '#9f1239' }}>
+                    目的{data.purpose_score}分
+                  </span>
+                )}
+                {data.category_score != null && (
+                  <span style={{ fontSize: '26px', color: '#9f1239' }}>
+                    品类{data.category_score}分
+                  </span>
+                )}
+                <button
+                  onClick={(e) => { e.stopPropagation(); setShowEvalDetails(!showEvalDetails); }}
+                  style={{
+                    fontSize: '24px',
+                    padding: '6px 16px',
+                    borderRadius: '12px',
+                    border: '2px solid #ec4899',
+                    background: 'white',
+                    color: '#ec4899',
+                    cursor: 'pointer',
+                    fontWeight: '600',
+                    transition: 'all 0.2s'
+                  }}
+                >
+                  {showEvalDetails ? '收起详情 ▲' : '展开详情 ▼'}
+                </button>
+              </div>
+            )}
+
+            {/* 详细内容(展开后显示) */}
+            {showEvalDetails && (
+              <div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid #f3f4f6' }}>
+                {/* 1. 知识评估 */}
+                {data.is_knowledge !== null && (
+                  <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
+                    <div style={{ fontSize: '26px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
+                      1️⃣ 知识评估
+                    </div>
+                    <div style={{ fontSize: '24px', color: '#9f1239', lineHeight: '1.4' }}>
+                      {data.knowledge_evaluation?.conclusion || '无评估信息'}
+                    </div>
+                  </div>
+                )}
+
+                {/* 2. 内容知识评估 */}
+                {data.is_content_knowledge && data.content_knowledge_evaluation && (
+                  <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
+                    <div style={{ fontSize: '26px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
+                      2️⃣ 内容知识评估 ({data.knowledge_score || 0}分)
+                    </div>
+                    <div style={{ fontSize: '24px', color: '#9f1239', lineHeight: '1.4' }}>
+                      {data.content_knowledge_evaluation.summary || '无评估信息'}
+                    </div>
+                  </div>
+                )}
+
+                {/* 3. 与原始需求匹配 */}
+                {(data.purpose_evaluation || data.category_evaluation) && (
+                  <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
+                    <div style={{ fontSize: '26px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
+                      3️⃣ 与原始需求匹配
+                    </div>
+                    {data.purpose_evaluation && (
+                      <div style={{ fontSize: '24px', color: '#9f1239', lineHeight: '1.4', marginBottom: '12px' }}>
+                        <div style={{ fontWeight: '600', marginBottom: '6px' }}>
+                          目的性匹配({data.purpose_score}分,占比70%)
+                        </div>
+                        <div>{data.purpose_evaluation.core_basis || '无评估信息'}</div>
+                      </div>
+                    )}
+                    {data.category_evaluation && (
+                      <div style={{ fontSize: '24px', color: '#9f1239', lineHeight: '1.4' }}>
+                        <div style={{ fontWeight: '600', marginBottom: '6px' }}>
+                          品类匹配({data.category_score}分,占比30%)
+                        </div>
+                        <div>{data.category_evaluation.core_basis || '无评估信息'}</div>
+                      </div>
+                    )}
+                  </div>
+                )}
+              </div>
+            )}
+          </div>
+        )}
+
+        {/* V2评估信息 - 兼容旧数据 */}
+        {data.evaluator_version !== 'v3.0' && (data.knowledge_score !== undefined || data.post_relevance_score !== undefined || data.is_knowledge !== undefined) && (
           <div style={{
             marginBottom: '20px',
             paddingBottom: '16px',
@@ -587,13 +709,11 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
             {(data.knowledge_score !== undefined || data.is_knowledge !== undefined) && (
               <div style={{ marginBottom: '16px' }}>
                 <div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '8px' }}>
-                  {/* 星级评分 */}
                   {data.knowledge_level && (
                     <span style={{ fontSize: '24px', lineHeight: '1' }}>
                       {'⭐'.repeat(data.knowledge_level)}
                     </span>
                   )}
-                  {/* 综合得分 */}
                   {data.knowledge_score != null && (
                     <span style={{
                       fontSize: '34px',
@@ -603,7 +723,6 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
                       知识: {data.knowledge_score.toFixed(0)}分
                     </span>
                   )}
-                  {/* 兼容旧版: 知识判定标签 */}
                   {!data.knowledge_score && data.is_knowledge !== undefined && (
                     <span style={{
                       display: 'inline-block',
@@ -618,25 +737,13 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
                     </span>
                   )}
                 </div>
-                {/* 知识评估总结 */}
                 {data.knowledge_evaluation?.summary && (
-                  <div style={{
-                    fontSize: '30px',
-                    color: '#9f1239',
-                    lineHeight: '1.4',
-                    marginTop: '8px',
-                  }}>
+                  <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4', marginTop: '8px' }}>
                     {data.knowledge_evaluation.summary}
                   </div>
                 )}
-                {/* 兼容旧版: 知识理由 */}
                 {!data.knowledge_evaluation?.summary && data.knowledge_reason && (
-                  <div style={{
-                    fontSize: '30px',
-                    color: '#9f1239',
-                    lineHeight: '1.4',
-                    marginTop: '8px',
-                  }}>
+                  <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4', marginTop: '8px' }}>
                     {data.knowledge_reason}
                   </div>
                 )}
@@ -646,87 +753,34 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
             {/* 相关性评估 (V2) */}
             {data.post_relevance_score != null && (
               <div>
-                <div style={{
-                  display: 'flex',
-                  alignItems: 'center',
-                  gap: '12px',
-                  marginBottom: '8px',
-                }}>
-                  {/* V2: 0-100分制 (统一显示分数) */}
-                  <span style={{
-                    fontSize: '34px',
-                    fontWeight: '600',
-                    color: '#9f1239',
-                  }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
+                  <span style={{ fontSize: '34px', fontWeight: '600', color: '#9f1239' }}>
                     相关性: {data.post_relevance_score.toFixed(0)}分
                   </span>
-                  {/* V2结论标签 */}
                   {data.relevance_conclusion && (
                     <span style={{
                       padding: '4px 16px',
                       borderRadius: '20px',
                       fontSize: '30px',
                       fontWeight: '600',
-                      background:
-                        data.relevance_conclusion.includes('高度') ? '#dcfce7' :
-                        data.relevance_conclusion.includes('中度') ? '#fef3c7' : '#fee2e2',
-                      color:
-                        data.relevance_conclusion.includes('高度') ? '#166534' :
-                        data.relevance_conclusion.includes('中度') ? '#854d0e' : '#991b1b',
+                      background: data.relevance_conclusion.includes('高度') ? '#dcfce7' : data.relevance_conclusion.includes('中度') ? '#fef3c7' : '#fee2e2',
+                      color: data.relevance_conclusion.includes('高度') ? '#166534' : data.relevance_conclusion.includes('中度') ? '#854d0e' : '#991b1b',
                     }}>
                       {data.relevance_conclusion}
                     </span>
                   )}
-                  {/* V1兼容: 显示旧的相关性等级 */}
-                  {!data.relevance_conclusion && data.relevance_level && (
-                    <span style={{
-                      padding: '4px 16px',
-                      borderRadius: '20px',
-                      fontSize: '30px',
-                      fontWeight: '600',
-                      background:
-                        data.relevance_level === '高度相关' ? '#dcfce7' :
-                        data.relevance_level === '中度相关' ? '#fef3c7' : '#fee2e2',
-                      color:
-                        data.relevance_level === '高度相关' ? '#166534' :
-                        data.relevance_level === '中度相关' ? '#854d0e' : '#991b1b',
-                    }}>
-                      {data.relevance_level}
-                    </span>
-                  )}
                 </div>
-                {/* 相关性评估总结 (V2) */}
                 {data.relevance_evaluation?.summary && (
-                  <div style={{
-                    fontSize: '30px',
-                    color: '#9f1239',
-                    lineHeight: '1.4',
-                  }}>
+                  <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4' }}>
                     {data.relevance_evaluation.summary}
                   </div>
                 )}
-                {/* 目的性和品类得分 (V2) */}
                 {data.relevance_evaluation?.purpose_score != null && data.relevance_evaluation?.category_score != null && (
-                  <div style={{
-                    fontSize: '28px',
-                    color: '#9f1239',
-                    marginTop: '6px',
-                    opacity: 0.8,
-                  }}>
+                  <div style={{ fontSize: '28px', color: '#9f1239', marginTop: '6px', opacity: 0.8 }}>
                     目的性:{data.relevance_evaluation.purpose_score.toFixed(0)}分(70%) |
                     品类:{data.relevance_evaluation.category_score.toFixed(0)}分(30%)
                   </div>
                 )}
-                {/* 兼容旧版: 相关性理由 */}
-                {!data.relevance_evaluation?.summary && data.relevance_reason && (
-                  <div style={{
-                    fontSize: '30px',
-                    color: '#9f1239',
-                    lineHeight: '1.4',
-                  }}>
-                    {data.relevance_reason}
-                  </div>
-                )}
               </div>
             )}
           </div>
@@ -1273,21 +1327,40 @@ function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, on
   // 计算字体颜色:根据分数提升幅度判断
   let fontColor = '#374151'; // 默认颜色
   if (node.type === 'note') {
-    // V2评估:基于知识得分和相关性得分判断颜色
-    const knowledgeScore = node.data.knowledge_score;
-    const relevanceScore = node.data.post_relevance_score;
-
-    if (knowledgeScore != null && relevanceScore != null) {
-      if (knowledgeScore <= 40) {
-        fontColor = '#ef4444'; // 红色 - 知识得分低
-      } else if (knowledgeScore > 40 && relevanceScore > 40) {
-        fontColor = '#22c55e'; // 绿色 - 知识和相关性都高
-      } else {
-        fontColor = '#eab308'; // 黄色 - 知识得分高但相关性低
+    const evaluatorVersion = node.data.evaluator_version || '';
+
+    if (evaluatorVersion === 'v3.0') {
+      // V3评估:基于is_knowledge, is_content_knowledge和final_score判断颜色
+      const isKnowledge = node.data.is_knowledge;
+      const isContentKnowledge = node.data.is_content_knowledge;
+      const finalScore = node.data.final_score;
+
+      if (!isKnowledge || !isContentKnowledge) {
+        fontColor = '#ef4444'; // 红色 - 非知识或非内容知识
+      } else if (finalScore !== null && finalScore !== undefined) {
+        if (finalScore >= 60) {
+          fontColor = '#22c55e'; // 绿色 - 内容知识且高分
+        } else {
+          fontColor = '#eab308'; // 黄色 - 内容知识但分数偏低
+        }
       }
     } else {
-      // V1兼容:如果没有V2评估数据,使用matchLevel判断
-      fontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
+      // V2评估:基于知识得分和相关性得分判断颜色
+      const knowledgeScore = node.data.knowledge_score;
+      const relevanceScore = node.data.post_relevance_score;
+
+      if (knowledgeScore != null && relevanceScore != null) {
+        if (knowledgeScore <= 40) {
+          fontColor = '#ef4444'; // 红色 - 知识得分低
+        } else if (knowledgeScore > 40 && relevanceScore > 40) {
+          fontColor = '#22c55e'; // 绿色 - 知识和相关性都高
+        } else {
+          fontColor = '#eab308'; // 黄色 - 知识得分高但相关性低
+        }
+      } else {
+        // V1兼容:如果没有V2评估数据,使用matchLevel判断
+        fontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
+      }
     }
   } else if (node.data.seed_score !== undefined) {
     const parentScore = parseFloat(node.data.seed_score);
@@ -1450,11 +1523,36 @@ function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, on
                 minWidth: '35px',
                 textAlign: 'right',
               }}>
-                {score.toFixed(2)}
+                {node.type === 'note' && node.data.evaluator_version === 'v3.0' && node.data.final_score !== null && node.data.final_score !== undefined
+                  ? node.data.final_score.toFixed(1)
+                  : score.toFixed(2)}
               </span>
             )}
           </div>
 
+          {/* V3评估信息行 - 仅对note类型且有V3评估数据显示 */}
+          {node.type === 'note' && node.data.evaluator_version === 'v3.0' && (
+            <div style={{ fontSize: '10px', color: '#2563eb', marginTop: '2px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
+              <span style={{ fontWeight: '600', color: '#2563eb' }}>评估结论:</span>
+              <span style={{ color: node.data.is_knowledge ? '#16a34a' : '#dc2626', fontWeight: '500' }}>
+                {node.data.is_knowledge ? '✓ 是知识' : '✗ 非知识'}
+              </span>
+              {node.data.is_content_knowledge !== null && node.data.is_content_knowledge !== undefined && (
+                <span style={{ color: node.data.is_content_knowledge ? '#16a34a' : '#dc2626', fontWeight: '500' }}>
+                  | {node.data.is_content_knowledge ? '✓ 是内容知识' : '✗ 非内容知识'}
+                </span>
+              )}
+              {node.data.is_content_knowledge && node.data.final_score !== null && node.data.final_score !== undefined && (
+                <>
+                  <span style={{ fontWeight: '500', color: '#2563eb' }}>| {node.data.match_level}</span>
+                  <span style={{ fontWeight: '600', color: node.data.final_score >= 60 ? '#16a34a' : '#ea580c' }}>
+                    | {node.data.final_score.toFixed(1)}分
+                  </span>
+                </>
+              )}
+            </div>
+          )}
+
           {/* 域组合的来源词得分(树状视图,右对齐) */}
           {isDomainCombination && sourceSummary && (
             <div style={{
@@ -1733,6 +1831,16 @@ function transformData(data) {
             relevance_reason: node.relevance_reason || '',
             relevance_conclusion: node.relevance_conclusion || '',
             relevance_evaluation: node.relevance_evaluation || null,
+            // 🆕 评估字段 (V3)
+            is_content_knowledge: node.is_content_knowledge !== undefined ? node.is_content_knowledge : null,
+            purpose_score: node.purpose_score !== undefined ? node.purpose_score : null,
+            category_score: node.category_score !== undefined ? node.category_score : null,
+            final_score: node.final_score !== undefined ? node.final_score : null,
+            match_level: node.match_level || '',
+            evaluator_version: node.evaluator_version || '',
+            content_knowledge_evaluation: node.content_knowledge_evaluation || null,
+            purpose_evaluation: node.purpose_evaluation || null,
+            category_evaluation: node.category_evaluation || null,
             // 🆕 原始问题
             originalQuestion: originalQuestion
           },

+ 0 - 7
visualization/knowledge_search_traverse/test_component.jsx

@@ -1,7 +0,0 @@
-import React from 'react';
-
-function TestComponent() {
-  return <div>知识内容测试</div>;
-}
-
-export default TestComponent;

+ 0 - 107
visualization/sug_v6_1_2_6/README.md

@@ -1,107 +0,0 @@
-# sug_v6_1_2_6 可视化工具
-
-这是 `sug_v6_1_2_6.py` 的配套可视化工具,用于将 `query_graph.json` 转换为交互式 HTML 可视化页面。
-
-## 📁 文件结构
-
-```
-visualization/
-└── sug_v6_1_2_6/           # 与主脚本名称对应
-    ├── index.js            # 主可视化脚本(统一命名)
-    ├── package.json        # 依赖配置文件
-    ├── node_modules/       # 所有依赖包(通过 npm install 安装)
-    │   ├── esbuild/
-    │   ├── @xyflow/
-    │   ├── react/
-    │   ├── react-dom/
-    │   ├── dagre/
-    │   └── ...
-    └── README.md           # 本文件
-```
-
-## 📦 安装依赖
-
-首次使用或克隆代码后,需要安装依赖包:
-
-```bash
-cd visualization/sug_v6_1_2_6
-npm install
-```
-
-这将安装所有必需的依赖包(约 33MB)。
-
-## ✨ 特性
-
-- **独立运行**:所有依赖都包含在文件夹内,无需外部 node_modules
-- **React Flow 可视化**:基于 @xyflow/react 的现代化图形展示
-- **交互功能**:
-  - 节点拖拽
-  - 层级筛选
-  - 节点搜索
-  - 路径高亮
-  - 目录树导航
-  - 面包屑路径
-  - 值得搜索的查询列表
-- **节点类型区分**:
-  - 🔍 查询节点(query)
-  - 📝 帖子节点(note)
-- **状态标识**:
-  - 未选中查询:红色文字
-  - 不满足的帖子(match_level: unsatisfied):红色文字
-
-## 🚀 使用方式
-
-### 方式1:通过 sug_v6_1_2_6.py 调用
-
-```bash
-# 运行主程序并自动生成可视化
-python sug_v6_1_2_6.py --input-dir "input/某目录" --visualize
-
-# 仅生成可视化(不运行主程序)
-python sug_v6_1_2_6.py --visualize-only "path/to/query_graph.json"
-```
-
-### 方式2:直接调用脚本
-
-```bash
-node visualization/sug_v6_1_2_6/index.js <query_graph.json> [output.html]
-```
-
-## 📊 输入/输出
-
-**输入**:`query_graph.json` - 包含节点和边信息的 JSON 文件
-```json
-{
-  "nodes": {
-    "node_id": {
-      "type": "query" | "note",
-      "query": "...",
-      "level": 1,
-      "relevance_score": 0.95,
-      ...
-    }
-  },
-  "edges": [
-    {
-      "from": "node_id_1",
-      "to": "node_id_2",
-      "edge_type": "..."
-    }
-  ]
-}
-```
-
-**输出**:`visualization.html` - 独立的 HTML 文件,包含所有 JavaScript 和 CSS
-
-## 📦 依赖大小
-
-- 总大小:约 33MB
-- 所有依赖已打包,无需额外安装
-
-## 🔧 技术栈
-
-- Node.js 运行环境
-- esbuild(打包工具)
-- React + React DOM
-- @xyflow/react(流程图库)
-- dagre(图布局算法)

+ 0 - 1966
visualization/sug_v6_1_2_6/index.js

@@ -1,1966 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs');
-const path = require('path');
-const { build } = require('esbuild');
-
-// 读取命令行参数
-const args = process.argv.slice(2);
-if (args.length === 0) {
-  console.error('Usage: node index.js <path-to-query_graph.json> [output.html]');
-  process.exit(1);
-}
-
-const inputFile = args[0];
-const outputFile = args[1] || 'query_graph_output.html';
-
-// 读取输入数据
-const data = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
-
-// 创建临时 React 组件文件
-const reactComponentPath = path.join(__dirname, 'temp_flow_component_v2.jsx');
-const reactComponent = `
-import React, { useState, useCallback, useMemo, useEffect } from 'react';
-import { createRoot } from 'react-dom/client';
-import {
-  ReactFlow,
-  Controls,
-  Background,
-  useNodesState,
-  useEdgesState,
-  Handle,
-  Position,
-  useReactFlow,
-  ReactFlowProvider,
-} from '@xyflow/react';
-import '@xyflow/react/dist/style.css';
-
-const data = ${JSON.stringify(data, null, 2)};
-
-// 查询节点组件 - 卡片样式
-function QueryNode({ id, data, sourcePosition, targetPosition }) {
-  // 所有节点默认展开
-  const expanded = true;
-
-  return (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: '#667eea', width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '12px',
-          borderRadius: '8px',
-          border: data.isHighlighted ? '3px solid #667eea' :
-                  data.isCollapsed ? '2px solid #667eea' :
-                  data.isSelected === false ? '2px dashed #d1d5db' :
-                  data.level === 0 ? '2px solid #8b5cf6' : '1px solid #e5e7eb',
-          background: data.isHighlighted ? '#eef2ff' :
-                      data.isSelected === false ? '#f9fafb' : 'white',
-          minWidth: '200px',
-          maxWidth: '280px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(102, 126, 234, 0.25), 0 4px 16px rgba(102, 126, 234, 0.4)' :
-                     data.isCollapsed ? '0 4px 12px rgba(102, 126, 234, 0.15)' :
-                     data.level === 0 ? '0 4px 12px rgba(139, 92, 246, 0.15)' : '0 2px 6px rgba(0, 0, 0, 0.06)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-          position: 'relative',
-          opacity: data.isSelected === false ? 0.6 : 1,
-        }}
-      >
-        {/* 折叠当前节点按钮 - 左边 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            left: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: '#f59e0b',
-            color: 'white',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            e.stopPropagation();
-            if (data.onHideSelf) {
-              data.onHideSelf();
-            }
-          }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.background = '#d97706';
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.background = '#f59e0b';
-          }}
-          title="隐藏当前节点"
-        >
-          ×
-        </div>
-
-        {/* 聚焦按钮 - 右上角 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            right: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: data.isFocused ? '#10b981' : '#e5e7eb',
-            color: data.isFocused ? 'white' : '#6b7280',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            e.stopPropagation();
-            if (data.onFocus) {
-              data.onFocus();
-            }
-          }}
-          onMouseEnter={(e) => {
-            if (!data.isFocused) {
-              e.currentTarget.style.background = '#d1d5db';
-            }
-          }}
-          onMouseLeave={(e) => {
-            if (!data.isFocused) {
-              e.currentTarget.style.background = '#e5e7eb';
-            }
-          }}
-          title={data.isFocused ? '取消聚焦' : '聚焦到此节点'}
-        >
-          🎯
-        </div>
-
-        {/* 折叠/展开子节点按钮 - 右边第二个位置 */}
-        {data.hasChildren && (
-          <div
-            style={{
-              position: 'absolute',
-              top: '6px',
-              right: '30px',
-              width: '20px',
-              height: '20px',
-              borderRadius: '50%',
-              background: data.isCollapsed ? '#667eea' : '#e5e7eb',
-              color: data.isCollapsed ? 'white' : '#6b7280',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              fontSize: '11px',
-              fontWeight: 'bold',
-              cursor: 'pointer',
-              transition: 'all 0.2s ease',
-              zIndex: 10,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              data.onToggleCollapse();
-            }}
-            title={data.isCollapsed ? '展开子节点' : '折叠子节点'}
-          >
-            {data.isCollapsed ? '+' : '−'}
-          </div>
-        )}
-
-        {/* 卡片内容 */}
-        <div>
-          {/* 标题行 */}
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px', paddingLeft: '24px', paddingRight: data.hasChildren ? '54px' : '28px' }}>
-            <div style={{ flex: 1 }}>
-              <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
-                <div style={{
-                  fontSize: '13px',
-                  fontWeight: data.level === 0 ? '700' : '600',
-                  color: data.level === 0 ? '#6b21a8' : '#1f2937',
-                  lineHeight: '1.3',
-                  flex: 1,
-                }}>
-                  {data.title}
-                </div>
-                {data.isSelected === false && (
-                  <div style={{
-                    fontSize: '9px',
-                    padding: '1px 4px',
-                    borderRadius: '3px',
-                    background: '#fee2e2',
-                    color: '#991b1b',
-                    fontWeight: '500',
-                    flexShrink: 0,
-                  }}>
-                    未选中
-                  </div>
-                )}
-              </div>
-            </div>
-          </div>
-
-        {/* 展开的详细信息 - 始终显示 */}
-        <div style={{ fontSize: '11px', lineHeight: 1.4 }}>
-            <div style={{ display: 'flex', gap: '4px', marginBottom: '6px', flexWrap: 'wrap' }}>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#eff6ff',
-                color: '#3b82f6',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                Lv.{data.level}
-              </span>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#f0fdf4',
-                color: '#16a34a',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                {data.score}
-              </span>
-              {data.strategy && data.strategy !== 'root' && (
-                <span style={{
-                  display: 'inline-block',
-                  padding: '1px 6px',
-                  borderRadius: '10px',
-                  background: '#fef3c7',
-                  color: '#92400e',
-                  fontSize: '10px',
-                  fontWeight: '500',
-                }}>
-                  {data.strategy}
-                </span>
-              )}
-            </div>
-
-            {data.parent && (
-              <div style={{ color: '#6b7280', fontSize: '10px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #f3f4f6' }}>
-                <strong>Parent:</strong> {data.parent}
-              </div>
-            )}
-            {data.evaluationReason && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                color: '#6b7280',
-                lineHeight: '1.5',
-              }}>
-                <strong style={{ color: '#4b5563' }}>评估:</strong>
-                <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-              </div>
-            )}
-          </div>
-        </div>
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#667eea', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-// 笔记节点组件 - 卡片样式,带轮播图
-function NoteNode({ id, data, sourcePosition, targetPosition }) {
-  const [currentImageIndex, setCurrentImageIndex] = useState(0);
-  const expanded = true;
-  const hasImages = data.imageList && data.imageList.length > 0;
-
-  const nextImage = (e) => {
-    e.stopPropagation();
-    if (hasImages) {
-      setCurrentImageIndex((prev) => (prev + 1) % data.imageList.length);
-    }
-  };
-
-  const prevImage = (e) => {
-    e.stopPropagation();
-    if (hasImages) {
-      setCurrentImageIndex((prev) => (prev - 1 + data.imageList.length) % data.imageList.length);
-    }
-  };
-
-  return (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '14px',
-          borderRadius: '20px',
-          border: data.isHighlighted ? '3px solid #ec4899' : '2px solid #fce7f3',
-          background: data.isHighlighted ? '#eef2ff' : 'white',
-          minWidth: '220px',
-          maxWidth: '300px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(236, 72, 153, 0.25), 0 4px 16px rgba(236, 72, 153, 0.4)' : '0 4px 12px rgba(236, 72, 153, 0.15)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-        }}
-      >
-        {/* 笔记图标和标题 */}
-        <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '8px' }}>
-          <span style={{ fontSize: '16px', marginRight: '8px' }}>📝</span>
-          <div style={{ flex: 1 }}>
-            <div style={{
-              fontSize: '13px',
-              fontWeight: '600',
-              color: '#831843',
-              lineHeight: '1.4',
-              marginBottom: '4px',
-            }}>
-              {data.title}
-            </div>
-          </div>
-        </div>
-
-        {/* 轮播图 */}
-        {hasImages && (
-          <div style={{
-            position: 'relative',
-            marginBottom: '8px',
-            borderRadius: '12px',
-            overflow: 'hidden',
-          }}>
-            <img
-              src={data.imageList[currentImageIndex].image_url}
-              alt={\`Image \${currentImageIndex + 1}\`}
-              style={{
-                width: '100%',
-                height: '160px',
-                objectFit: 'cover',
-                display: 'block',
-              }}
-              onError={(e) => {
-                e.target.style.display = 'none';
-              }}
-            />
-            {data.imageList.length > 1 && (
-              <>
-                {/* 左右切换按钮 */}
-                <button
-                  onClick={prevImage}
-                  style={{
-                    position: 'absolute',
-                    left: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ‹
-                </button>
-                <button
-                  onClick={nextImage}
-                  style={{
-                    position: 'absolute',
-                    right: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ›
-                </button>
-                {/* 图片计数 */}
-                <div style={{
-                  position: 'absolute',
-                  bottom: '4px',
-                  right: '4px',
-                  background: 'rgba(0, 0, 0, 0.6)',
-                  color: 'white',
-                  padding: '2px 6px',
-                  borderRadius: '10px',
-                  fontSize: '10px',
-                }}>
-                  {currentImageIndex + 1}/{data.imageList.length}
-                </div>
-              </>
-            )}
-          </div>
-        )}
-
-        {/* 标签 */}
-        <div style={{ display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' }}>
-          <span style={{
-            display: 'inline-block',
-            padding: '2px 8px',
-            borderRadius: '12px',
-            background: '#fff1f2',
-            color: '#be123c',
-            fontSize: '10px',
-            fontWeight: '500',
-          }}>
-            {data.matchLevel}
-          </span>
-          <span style={{
-            display: 'inline-block',
-            padding: '2px 8px',
-            borderRadius: '12px',
-            background: '#fff7ed',
-            color: '#c2410c',
-            fontSize: '10px',
-            fontWeight: '500',
-          }}>
-            Score: {data.score}
-          </span>
-        </div>
-
-        {/* 描述 */}
-        {expanded && data.description && (
-          <div style={{
-            fontSize: '11px',
-            color: '#9f1239',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            {data.description}
-          </div>
-        )}
-
-        {/* 评估理由 */}
-        {expanded && data.evaluationReason && (
-          <div style={{
-            fontSize: '10px',
-            color: '#831843',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            marginTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            <strong style={{ color: '#9f1239' }}>评估:</strong>
-            <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-          </div>
-        )}
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-const nodeTypes = {
-  query: QueryNode,
-  note: NoteNode,
-};
-
-// 根据 score 获取颜色
-function getScoreColor(score) {
-  if (score >= 0.7) return '#10b981'; // 绿色 - 高分
-  if (score >= 0.4) return '#f59e0b'; // 橙色 - 中分
-  return '#ef4444'; // 红色 - 低分
-}
-
-// 截断文本,保留头尾,中间显示省略号
-function truncateMiddle(text, maxLength = 20) {
-  if (!text || text.length <= maxLength) return text;
-  const headLength = Math.ceil(maxLength * 0.4);
-  const tailLength = Math.floor(maxLength * 0.4);
-  const head = text.substring(0, headLength);
-  const tail = text.substring(text.length - tailLength);
-  return \`\${head}...\${tail}\`;
-}
-
-// 根据策略获取颜色
-function getStrategyColor(strategy) {
-  const strategyColors = {
-    '初始分词': '#10b981',
-    '调用sug': '#06b6d4',
-    '同义改写': '#f59e0b',
-    '加词': '#3b82f6',
-    '抽象改写': '#8b5cf6',
-    '基于部分匹配改进': '#ec4899',
-    '结果分支-抽象改写': '#a855f7',
-    '结果分支-同义改写': '#fb923c',
-  };
-  return strategyColors[strategy] || '#9ca3af';
-}
-
-// 树节点组件
-function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) {
-  const hasChildren = children && children.length > 0;
-  const score = node.data.score ? parseFloat(node.data.score) : 0;
-  const strategy = node.data.strategy || '';
-  const strategyColor = getStrategyColor(strategy);
-
-  return (
-    <div style={{ marginLeft: level * 12 + 'px' }}>
-      <div
-        style={{
-          padding: '6px 8px',
-          borderRadius: '4px',
-          cursor: 'pointer',
-          background: 'transparent',
-          border: isSelected ? '1px solid #3b82f6' : '1px solid transparent',
-          display: 'flex',
-          alignItems: 'center',
-          gap: '6px',
-          transition: 'all 0.2s ease',
-          position: 'relative',
-          overflow: 'visible',
-        }}
-        onMouseEnter={(e) => {
-          if (!isSelected) e.currentTarget.style.background = '#f9fafb';
-        }}
-        onMouseLeave={(e) => {
-          if (!isSelected) e.currentTarget.style.background = 'transparent';
-        }}
-      >
-        {/* 策略类型竖线 */}
-        <div style={{
-          width: '3px',
-          height: '20px',
-          background: strategyColor,
-          borderRadius: '2px',
-          flexShrink: 0,
-          position: 'relative',
-          zIndex: 1,
-        }} />
-
-        {hasChildren && (
-          <span
-            style={{
-              fontSize: '10px',
-              color: '#6b7280',
-              cursor: 'pointer',
-              width: '16px',
-              textAlign: 'center',
-              position: 'relative',
-              zIndex: 1,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              onToggle();
-            }}
-          >
-            {isCollapsed ? '▶' : '▼'}
-          </span>
-        )}
-        {!hasChildren && <span style={{ width: '16px', position: 'relative', zIndex: 1 }}></span>}
-
-        <div
-          style={{
-            flex: 1,
-            fontSize: '12px',
-            color: '#374151',
-            position: 'relative',
-            zIndex: 1,
-            minWidth: 0,
-            display: 'flex',
-            flexDirection: 'column',
-            gap: '4px',
-          }}
-          onClick={onSelect}
-        >
-          <div style={{
-            display: 'flex',
-            alignItems: 'center',
-            gap: '8px',
-          }}>
-            {/* 节点类型图标 */}
-            <span style={{
-              fontSize: '12px',
-              flexShrink: 0,
-            }}>
-              {node.type === 'note' ? '📝' : '🔍'}
-            </span>
-
-            <div style={{
-              fontWeight: level === 0 ? '600' : '400',
-              maxWidth: '180px',
-              flex: 1,
-              minWidth: 0,
-              color: (node.type === 'note' ? node.data.matchLevel === 'unsatisfied' : node.data.isSelected === false) ? '#ef4444' : '#374151',
-            }}
-            title={node.data.title || node.id}
-            >
-              {truncateMiddle(node.data.title || node.id, 18)}
-            </div>
-
-            {/* 分数显示 */}
-            <span style={{
-              fontSize: '11px',
-              color: '#6b7280',
-              fontWeight: '500',
-              flexShrink: 0,
-            }}>
-              {score.toFixed(2)}
-            </span>
-          </div>
-
-          {/* 分数下划线 */}
-          <div style={{
-            width: (score * 100) + '%',
-            height: '2px',
-            background: getScoreColor(score),
-            borderRadius: '1px',
-          }} />
-        </div>
-      </div>
-
-      {hasChildren && !isCollapsed && (
-        <div>
-          {children}
-        </div>
-      )}
-    </div>
-  );
-}
-
-// 使用 dagre 自动布局
-function getLayoutedElements(nodes, edges, direction = 'LR') {
-  console.log('🎯 Starting layout with dagre...');
-  console.log('Input:', nodes.length, 'nodes,', edges.length, 'edges');
-
-  // 检查 dagre 是否加载
-  if (typeof window === 'undefined' || typeof window.dagre === 'undefined') {
-    console.warn('⚠️ Dagre not loaded, using fallback layout');
-    // 降级到简单布局
-    const levelGroups = {};
-    nodes.forEach(node => {
-      const level = node.data.level || 0;
-      if (!levelGroups[level]) levelGroups[level] = [];
-      levelGroups[level].push(node);
-    });
-
-    Object.entries(levelGroups).forEach(([level, nodeList]) => {
-      const x = parseInt(level) * 350;
-      nodeList.forEach((node, index) => {
-        node.position = { x, y: index * 150 };
-        node.targetPosition = 'left';
-        node.sourcePosition = 'right';
-      });
-    });
-
-    return { nodes, edges };
-  }
-
-  try {
-    const dagreGraph = new window.dagre.graphlib.Graph();
-    dagreGraph.setDefaultEdgeLabel(() => ({}));
-
-    const isHorizontal = direction === 'LR';
-    dagreGraph.setGraph({
-      rankdir: direction,
-      nodesep: 120,   // 垂直间距 - 增加以避免节点重叠
-      ranksep: 280,  // 水平间距 - 增加以容纳更宽的节点
-    });
-
-    // 添加节点 - 根据节点类型设置不同的尺寸
-    nodes.forEach((node) => {
-      let nodeWidth = 280;
-      let nodeHeight = 180;
-
-      // note 节点有轮播图,需要更大的空间
-      if (node.type === 'note') {
-        nodeWidth = 320;
-        nodeHeight = 350;  // 增加高度以容纳轮播图
-      }
-
-      dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
-    });
-
-    // 添加边
-    edges.forEach((edge) => {
-      dagreGraph.setEdge(edge.source, edge.target);
-    });
-
-    // 计算布局
-    window.dagre.layout(dagreGraph);
-    console.log('✅ Dagre layout completed');
-
-    // 更新节点位置和 handle 位置
-    nodes.forEach((node) => {
-      const nodeWithPosition = dagreGraph.node(node.id);
-
-      if (!nodeWithPosition) {
-        console.warn('Node position not found for:', node.id);
-        return;
-      }
-
-      node.targetPosition = isHorizontal ? 'left' : 'top';
-      node.sourcePosition = isHorizontal ? 'right' : 'bottom';
-
-      // 根据节点类型获取尺寸
-      let nodeWidth = 280;
-      let nodeHeight = 180;
-      if (node.type === 'note') {
-        nodeWidth = 320;
-        nodeHeight = 350;
-      }
-
-      // 将 dagre 的中心点位置转换为 React Flow 的左上角位置
-      node.position = {
-        x: nodeWithPosition.x - nodeWidth / 2,
-        y: nodeWithPosition.y - nodeHeight / 2,
-      };
-    });
-
-    console.log('✅ Layout completed, sample node:', nodes[0]);
-    return { nodes, edges };
-  } catch (error) {
-    console.error('❌ Error in dagre layout:', error);
-    console.error('Error details:', error.message, error.stack);
-
-    // 降级处理
-    console.log('Using fallback layout...');
-    const levelGroups = {};
-    nodes.forEach(node => {
-      const level = node.data.level || 0;
-      if (!levelGroups[level]) levelGroups[level] = [];
-      levelGroups[level].push(node);
-    });
-
-    Object.entries(levelGroups).forEach(([level, nodeList]) => {
-      const x = parseInt(level) * 350;
-      nodeList.forEach((node, index) => {
-        node.position = { x, y: index * 150 };
-        node.targetPosition = 'left';
-        node.sourcePosition = 'right';
-      });
-    });
-
-    return { nodes, edges };
-  }
-}
-
-function transformData(data) {
-  const nodes = [];
-  const edges = [];
-
-  const originalIdToCanvasId = {}; // 原始ID -> 画布ID的映射
-  const canvasIdToNodeData = {}; // 避免重复创建相同的节点
-
-  // 创建节点
-  Object.entries(data.nodes).forEach(([originalId, node]) => {
-    if (node.type === 'query') {
-      // 使用 query_level 作为唯一ID
-      const canvasId = node.query + '_' + node.level;
-      originalIdToCanvasId[originalId] = canvasId;
-
-      // 如果这个 canvasId 还没有创建过节点,则创建
-      if (!canvasIdToNodeData[canvasId]) {
-        canvasIdToNodeData[canvasId] = true;
-        nodes.push({
-          id: canvasId, // 使用 query_level 格式
-          originalId: originalId, // 保留原始ID用于调试
-          type: 'query',
-          data: {
-            title: node.query,
-            level: node.level,
-            score: node.relevance_score.toFixed(2),
-            strategy: node.strategy,
-            parent: node.parent_query,
-            isSelected: node.is_selected,
-            evaluationReason: node.evaluation_reason || '',
-          },
-          position: { x: 0, y: 0 }, // 初始位置,会被 dagre 覆盖
-        });
-      }
-    } else if (node.type === 'note') {
-      // note节点直接使用原始ID
-      originalIdToCanvasId[originalId] = originalId;
-
-      if (!canvasIdToNodeData[originalId]) {
-        canvasIdToNodeData[originalId] = true;
-        nodes.push({
-          id: originalId,
-          originalId: originalId,
-          type: 'note',
-          data: {
-            title: node.title,
-            matchLevel: node.match_level,
-            score: node.relevance_score.toFixed(2),
-            description: node.desc,
-            isSelected: node.is_selected !== undefined ? node.is_selected : true,
-            imageList: node.image_list || [], // 添加图片列表
-            noteUrl: node.note_url || '', // 添加帖子链接
-            evaluationReason: node.evaluation_reason || '', // 添加评估理由
-          },
-          position: { x: 0, y: 0 },
-        });
-      }
-    }
-  });
-
-  // 创建边 - 使用虚线样式,映射到画布ID
-  data.edges.forEach((edge, index) => {
-    const edgeColors = {
-      '初始分词': '#10b981',
-      '调用sug': '#06b6d4',
-      '同义改写': '#f59e0b',
-      '加词': '#3b82f6',
-      '抽象改写': '#8b5cf6',
-      '基于部分匹配改进': '#ec4899',
-      '结果分支-抽象改写': '#a855f7',
-      '结果分支-同义改写': '#fb923c',
-      'query_to_note': '#ec4899',
-    };
-
-    const color = edgeColors[edge.strategy] || edgeColors[edge.edge_type] || '#d1d5db';
-    const isNoteEdge = edge.edge_type === 'query_to_note';
-
-    edges.push({
-      id: \`edge-\${index}\`,
-      source: originalIdToCanvasId[edge.from], // 使用画布ID
-      target: originalIdToCanvasId[edge.to],   // 使用画布ID
-      type: 'simplebezier', // 使用简单贝塞尔曲线
-      animated: isNoteEdge,
-      style: {
-        stroke: color,
-        strokeWidth: isNoteEdge ? 2.5 : 2,
-        strokeDasharray: isNoteEdge ? '5,5' : '8,4',
-      },
-      markerEnd: {
-        type: 'arrowclosed',
-        color: color,
-        width: 20,
-        height: 20,
-      },
-    });
-  });
-
-  // 使用 dagre 自动计算布局 - 从左到右
-  return getLayoutedElements(nodes, edges, 'LR');
-}
-
-function FlowContent() {
-  const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
-    console.log('🔍 Transforming data...');
-    const result = transformData(data);
-    console.log('✅ Transformed:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
-    return result;
-  }, []);
-
-  // 初始化:找出所有有子节点的节点,默认折叠(画布节点)
-  const initialCollapsedNodes = useMemo(() => {
-    const nodesWithChildren = new Set();
-    initialEdges.forEach(edge => {
-      nodesWithChildren.add(edge.source);
-    });
-    // 排除根节点(level 0),让根节点默认展开
-    const rootNode = initialNodes.find(n => n.data.level === 0);
-    if (rootNode) {
-      nodesWithChildren.delete(rootNode.id);
-    }
-    return nodesWithChildren;
-  }, [initialNodes, initialEdges]);
-
-  // 树节点的折叠状态需要在树构建后初始化
-  const [collapsedNodes, setCollapsedNodes] = useState(() => initialCollapsedNodes);
-  const [collapsedTreeNodes, setCollapsedTreeNodes] = useState(new Set());
-  const [selectedNodeId, setSelectedNodeId] = useState(null);
-  const [hiddenNodes, setHiddenNodes] = useState(new Set()); // 用户手动隐藏的节点
-  const [focusMode, setFocusMode] = useState(false); // 全局聚焦模式,默认关闭
-  const [focusedNodeId, setFocusedNodeId] = useState(null); // 单独聚焦的节点ID
-
-  // 获取 React Flow 实例以控制画布
-  const { setCenter, fitView } = useReactFlow();
-
-  // 获取某个节点的所有后代节点ID
-  const getDescendants = useCallback((nodeId) => {
-    const descendants = new Set();
-    const queue = [nodeId];
-
-    while (queue.length > 0) {
-      const current = queue.shift();
-      initialEdges.forEach(edge => {
-        if (edge.source === current && !descendants.has(edge.target)) {
-          descendants.add(edge.target);
-          queue.push(edge.target);
-        }
-      });
-    }
-
-    return descendants;
-  }, [initialEdges]);
-
-  // 获取直接父节点
-  const getDirectParents = useCallback((nodeId) => {
-    const parents = [];
-    initialEdges.forEach(edge => {
-      if (edge.target === nodeId) {
-        parents.push(edge.source);
-      }
-    });
-    return parents;
-  }, [initialEdges]);
-
-  // 获取直接子节点
-  const getDirectChildren = useCallback((nodeId) => {
-    const children = [];
-    initialEdges.forEach(edge => {
-      if (edge.source === nodeId) {
-        children.push(edge.target);
-      }
-    });
-    return children;
-  }, [initialEdges]);
-
-  // 切换节点折叠状态
-  const toggleNodeCollapse = useCallback((nodeId) => {
-    setCollapsedNodes(prev => {
-      const newSet = new Set(prev);
-      const descendants = getDescendants(nodeId);
-
-      if (newSet.has(nodeId)) {
-        // 展开:移除此节点,但保持其他折叠的节点
-        newSet.delete(nodeId);
-      } else {
-        // 折叠:添加此节点
-        newSet.add(nodeId);
-      }
-
-      return newSet;
-    });
-  }, [getDescendants]);
-
-  // 过滤可见的节点和边,并重新计算布局
-  const { nodes, edges } = useMemo(() => {
-    const nodesToHide = new Set();
-
-    // 判断使用哪个节点ID进行聚焦:优先使用单独聚焦的节点,否则使用全局聚焦模式的选中节点
-    const effectiveFocusNodeId = focusedNodeId || (focusMode ? selectedNodeId : null);
-
-    // 聚焦模式:只显示聚焦节点、其父节点和直接子节点
-    if (effectiveFocusNodeId) {
-      const visibleInFocus = new Set([effectiveFocusNodeId]);
-
-      // 添加所有父节点
-      initialEdges.forEach(edge => {
-        if (edge.target === effectiveFocusNodeId) {
-          visibleInFocus.add(edge.source);
-        }
-      });
-
-      // 添加所有直接子节点
-      initialEdges.forEach(edge => {
-        if (edge.source === effectiveFocusNodeId) {
-          visibleInFocus.add(edge.target);
-        }
-      });
-
-      // 隐藏不在聚焦范围内的节点
-      initialNodes.forEach(node => {
-        if (!visibleInFocus.has(node.id)) {
-          nodesToHide.add(node.id);
-        }
-      });
-    } else {
-      // 非聚焦模式:使用原有的折叠逻辑
-      // 收集所有被折叠节点的后代
-      collapsedNodes.forEach(collapsedId => {
-        const descendants = getDescendants(collapsedId);
-        descendants.forEach(id => nodesToHide.add(id));
-      });
-    }
-
-    // 添加用户手动隐藏的节点
-    hiddenNodes.forEach(id => nodesToHide.add(id));
-
-    const visibleNodes = initialNodes
-      .filter(node => !nodesToHide.has(node.id))
-      .map(node => ({
-        ...node,
-        data: {
-          ...node.data,
-          isCollapsed: collapsedNodes.has(node.id),
-          hasChildren: initialEdges.some(e => e.source === node.id),
-          onToggleCollapse: () => toggleNodeCollapse(node.id),
-          onHideSelf: () => {
-            setHiddenNodes(prev => {
-              const newSet = new Set(prev);
-              newSet.add(node.id);
-              return newSet;
-            });
-          },
-          onFocus: () => {
-            // 切换聚焦状态
-            if (focusedNodeId === node.id) {
-              setFocusedNodeId(null); // 如果已经聚焦,则取消聚焦
-            } else {
-              // 先取消之前的聚焦,然后聚焦到当前节点
-              setFocusedNodeId(node.id);
-
-              // 延迟聚焦视图到该节点
-              setTimeout(() => {
-                fitView({
-                  nodes: [{ id: node.id }],
-                  duration: 800,
-                  padding: 0.3,
-                });
-              }, 100);
-            }
-          },
-          isFocused: focusedNodeId === node.id,
-          isHighlighted: selectedNodeId === node.id,
-        }
-      }));
-
-    const visibleEdges = initialEdges.filter(
-      edge => !nodesToHide.has(edge.source) && !nodesToHide.has(edge.target)
-    );
-
-    // 重新计算布局 - 只对可见节点
-    if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') {
-      try {
-        const dagreGraph = new window.dagre.graphlib.Graph();
-        dagreGraph.setDefaultEdgeLabel(() => ({}));
-
-        dagreGraph.setGraph({
-          rankdir: 'LR',
-          nodesep: 120,   // 垂直间距 - 增加以避免节点重叠
-          ranksep: 280,  // 水平间距 - 增加以容纳更宽的节点
-        });
-
-        visibleNodes.forEach((node) => {
-          let nodeWidth = 280;
-          let nodeHeight = 180;
-
-          // note 节点有轮播图,需要更大的空间
-          if (node.type === 'note') {
-            nodeWidth = 320;
-            nodeHeight = 350;
-          }
-
-          dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
-        });
-
-        visibleEdges.forEach((edge) => {
-          dagreGraph.setEdge(edge.source, edge.target);
-        });
-
-        window.dagre.layout(dagreGraph);
-
-        visibleNodes.forEach((node) => {
-          const nodeWithPosition = dagreGraph.node(node.id);
-          if (nodeWithPosition) {
-            // 根据节点类型获取对应的尺寸
-            let nodeWidth = 280;
-            let nodeHeight = 180;
-            if (node.type === 'note') {
-              nodeWidth = 320;
-              nodeHeight = 350;
-            }
-
-            node.position = {
-              x: nodeWithPosition.x - nodeWidth / 2,
-              y: nodeWithPosition.y - nodeHeight / 2,
-            };
-            node.targetPosition = 'left';
-            node.sourcePosition = 'right';
-          }
-        });
-
-        console.log('✅ Dynamic layout recalculated for', visibleNodes.length, 'visible nodes');
-      } catch (error) {
-        console.error('❌ Error in dynamic layout:', error);
-      }
-    }
-
-    return { nodes: visibleNodes, edges: visibleEdges };
-  }, [initialNodes, initialEdges, collapsedNodes, hiddenNodes, focusMode, focusedNodeId, getDescendants, toggleNodeCollapse, selectedNodeId]);
-
-  // 构建树形结构 - 允许一个节点有多个父节点
-  const buildTree = useCallback(() => {
-    const nodeMap = new Map();
-    initialNodes.forEach(node => {
-      nodeMap.set(node.id, node);
-    });
-
-    // 为每个节点创建树节点的副本(允许多次出现)
-    const createTreeNode = (nodeId, pathKey) => {
-      const node = nodeMap.get(nodeId);
-      if (!node) return null;
-
-      return {
-        ...node,
-        treeKey: pathKey, // 唯一的树路径key,用于React key
-        children: []
-      };
-    };
-
-    // 构建父子关系映射:记录每个节点的所有父节点,去重边
-    const parentToChildren = new Map();
-    const childToParents = new Map();
-
-    initialEdges.forEach(edge => {
-      // 记录父->子关系(去重:同一个父节点到同一个子节点只记录一次)
-      if (!parentToChildren.has(edge.source)) {
-        parentToChildren.set(edge.source, []);
-      }
-      const children = parentToChildren.get(edge.source);
-      if (!children.includes(edge.target)) {
-        children.push(edge.target);
-      }
-
-      // 记录子->父关系(用于判断是否有多个父节点,也去重)
-      if (!childToParents.has(edge.target)) {
-        childToParents.set(edge.target, []);
-      }
-      const parents = childToParents.get(edge.target);
-      if (!parents.includes(edge.source)) {
-        parents.push(edge.source);
-      }
-    });
-
-    // 递归构建树
-    const buildSubtree = (nodeId, pathKey, visitedInPath) => {
-      // 避免循环引用:如果当前路径中已经访问过这个节点,跳过
-      if (visitedInPath.has(nodeId)) {
-        return null;
-      }
-
-      const treeNode = createTreeNode(nodeId, pathKey);
-      if (!treeNode) return null;
-
-      const newVisitedInPath = new Set(visitedInPath);
-      newVisitedInPath.add(nodeId);
-
-      const children = parentToChildren.get(nodeId) || [];
-      treeNode.children = children
-        .map((childId, index) => buildSubtree(childId, pathKey + '-' + childId + '-' + index, newVisitedInPath))
-        .filter(child => child !== null);
-
-      return treeNode;
-    };
-
-    // 找出所有根节点(没有入边的节点)
-    const hasParent = new Set();
-    initialEdges.forEach(edge => {
-      hasParent.add(edge.target);
-    });
-
-    const roots = [];
-    initialNodes.forEach((node, index) => {
-      if (!hasParent.has(node.id)) {
-        const treeNode = buildSubtree(node.id, 'root-' + node.id + '-' + index, new Set());
-        if (treeNode) roots.push(treeNode);
-      }
-    });
-
-    return roots;
-  }, [initialNodes, initialEdges]);
-
-  const treeRoots = useMemo(() => buildTree(), [buildTree]);
-
-  // 初始化树节点折叠状态
-  useEffect(() => {
-    const getAllTreeKeys = (nodes) => {
-      const keys = new Set();
-      const traverse = (node) => {
-        if (node.children && node.children.length > 0) {
-          // 排除根节点
-          if (node.data.level !== 0) {
-            keys.add(node.treeKey);
-          }
-          node.children.forEach(traverse);
-        }
-      };
-      nodes.forEach(traverse);
-      return keys;
-    };
-
-    setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
-  }, [treeRoots]);
-
-  const renderTree = useCallback((treeNodes, level = 0) => {
-    return treeNodes.map(node => {
-      // 使用 treeKey 来区分树中的不同实例
-      const isCollapsed = collapsedTreeNodes.has(node.treeKey);
-      const isSelected = selectedNodeId === node.id;
-
-      return (
-        <TreeNode
-          key={node.treeKey}
-          node={node}
-          level={level}
-          isCollapsed={isCollapsed}
-          isSelected={isSelected}
-          onToggle={() => {
-            setCollapsedTreeNodes(prev => {
-              const newSet = new Set(prev);
-              if (newSet.has(node.treeKey)) {
-                newSet.delete(node.treeKey);
-              } else {
-                newSet.add(node.treeKey);
-              }
-              return newSet;
-            });
-          }}
-          onSelect={() => {
-            const nodeId = node.id;
-
-            // 展开所有祖先节点
-            const ancestorIds = [nodeId];
-            const findAncestors = (id) => {
-              initialEdges.forEach(edge => {
-                if (edge.target === id && !ancestorIds.includes(edge.source)) {
-                  ancestorIds.push(edge.source);
-                  findAncestors(edge.source);
-                }
-              });
-            };
-            findAncestors(nodeId);
-
-            // 如果节点或其祖先被隐藏,先恢复它们
-            setHiddenNodes(prev => {
-              const newSet = new Set(prev);
-              ancestorIds.forEach(id => newSet.delete(id));
-              return newSet;
-            });
-
-            setSelectedNodeId(nodeId);
-
-            // 获取选中节点的直接子节点
-            const childrenIds = [];
-            initialEdges.forEach(edge => {
-              if (edge.source === nodeId) {
-                childrenIds.push(edge.target);
-              }
-            });
-
-            setCollapsedNodes(prev => {
-              const newSet = new Set(prev);
-              // 展开所有祖先节点
-              ancestorIds.forEach(id => newSet.delete(id));
-              // 展开选中节点本身
-              newSet.delete(nodeId);
-              // 展开选中节点的直接子节点
-              childrenIds.forEach(id => newSet.delete(id));
-              return newSet;
-            });
-
-            // 延迟聚焦,等待节点展开和布局重新计算
-            setTimeout(() => {
-              fitView({
-                nodes: [{ id: nodeId }],
-                duration: 800,
-                padding: 0.3,
-              });
-            }, 300);
-          }}
-        >
-          {node.children && node.children.length > 0 && renderTree(node.children, level + 1)}
-        </TreeNode>
-      );
-    });
-  }, [collapsedTreeNodes, selectedNodeId, nodes, setCenter, initialEdges, setCollapsedNodes, fitView]);
-
-  console.log('📊 Rendering with', nodes.length, 'visible nodes and', edges.length, 'visible edges');
-
-  if (nodes.length === 0) {
-    return (
-      <div style={{ padding: 50, color: 'red', fontSize: 20 }}>
-        ERROR: No nodes to display!
-      </div>
-    );
-  }
-
-  return (
-    <div style={{ width: '100vw', height: '100vh', background: '#f9fafb', display: 'flex', flexDirection: 'column' }}>
-      {/* 顶部面包屑导航栏 */}
-      <div style={{
-        minHeight: '48px',
-        maxHeight: '120px',
-        background: 'white',
-        borderBottom: '1px solid #e5e7eb',
-        display: 'flex',
-        alignItems: 'flex-start',
-        padding: '12px 24px',
-        zIndex: 1000,
-        boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
-        flexShrink: 0,
-        overflowY: 'auto',
-      }}>
-        <div style={{ width: '100%' }}>
-          {selectedNodeId ? (
-            <div style={{ fontSize: '12px', color: '#6b7280' }}>
-              {/* 面包屑导航 - 显示所有路径 */}
-              {(() => {
-                const selectedNode = nodes.find(n => n.id === selectedNodeId);
-                if (!selectedNode) return null;
-
-                // 找到所有从根节点到当前节点的路径
-                const findAllPaths = (targetId) => {
-                  const paths = [];
-
-                  const buildPath = (nodeId, currentPath) => {
-                    const node = initialNodes.find(n => n.id === nodeId);
-                    if (!node) return;
-
-                    const newPath = [node, ...currentPath];
-
-                    // 找到所有父节点
-                    const parents = initialEdges.filter(e => e.target === nodeId).map(e => e.source);
-
-                    if (parents.length === 0) {
-                      // 到达根节点
-                      paths.push(newPath);
-                    } else {
-                      // 递归处理所有父节点
-                      parents.forEach(parentId => {
-                        buildPath(parentId, newPath);
-                      });
-                    }
-                  };
-
-                  buildPath(targetId, []);
-                  return paths;
-                };
-
-                const allPaths = findAllPaths(selectedNodeId);
-
-                // 去重:将路径转换为字符串进行比较
-                const uniquePaths = [];
-                const pathStrings = new Set();
-                allPaths.forEach(path => {
-                  const pathString = path.map(n => n.id).join('->');
-                  if (!pathStrings.has(pathString)) {
-                    pathStrings.add(pathString);
-                    uniquePaths.push(path);
-                  }
-                });
-
-                return (
-                  <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
-                    {uniquePaths.map((path, pathIndex) => (
-                      <div key={pathIndex} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
-                        {pathIndex > 0 && <span style={{ color: '#d1d5db', marginRight: '4px' }}>或</span>}
-                        {path.map((node, index) => {
-                          // 获取节点的 score、strategy 和 isSelected
-                          const nodeScore = node.data.score ? parseFloat(node.data.score) : 0;
-                          const nodeStrategy = node.data.strategy || '';
-                          const strategyColor = getStrategyColor(nodeStrategy);
-                          const nodeIsSelected = node.type === 'note' ? node.data.matchLevel !== 'unsatisfied' : node.data.isSelected !== false;
-
-                          return (
-                          <React.Fragment key={node.id + '-' + index}>
-                            <span
-                              onClick={() => {
-                                const nodeId = node.id;
-
-                                // 找到所有祖先节点
-                                const ancestorIds = [nodeId];
-                                const findAncestors = (id) => {
-                                  initialEdges.forEach(edge => {
-                                    if (edge.target === id && !ancestorIds.includes(edge.source)) {
-                                      ancestorIds.push(edge.source);
-                                      findAncestors(edge.source);
-                                    }
-                                  });
-                                };
-                                findAncestors(nodeId);
-
-                                // 如果节点或其祖先被隐藏,先恢复它们
-                                setHiddenNodes(prev => {
-                                  const newSet = new Set(prev);
-                                  ancestorIds.forEach(id => newSet.delete(id));
-                                  return newSet;
-                                });
-
-                                // 展开目录树中到达该节点的路径
-                                // 需要找到所有包含该节点的树路径的 treeKey,并展开它们的父节点
-                                setCollapsedTreeNodes(prev => {
-                                  const newSet = new Set(prev);
-                                  // 清空所有折叠状态,让目录树完全展开到选中节点
-                                  // 这样可以确保选中节点在目录中可见
-                                  return new Set();
-                                });
-
-                                setSelectedNodeId(nodeId);
-                                setTimeout(() => {
-                                  fitView({
-                                    nodes: [{ id: nodeId }],
-                                    duration: 800,
-                                    padding: 0.3,
-                                  });
-                                }, 100);
-                              }}
-                              style={{
-                                padding: '6px 8px',
-                                borderRadius: '4px',
-                                background: 'white',
-                                border: index === path.length - 1 ? '2px solid #3b82f6' : '1px solid #d1d5db',
-                                color: '#374151',
-                                fontWeight: index === path.length - 1 ? '600' : '400',
-                                width: '180px',
-                                cursor: 'pointer',
-                                transition: 'all 0.2s ease',
-                                position: 'relative',
-                                display: 'inline-flex',
-                                flexDirection: 'column',
-                                gap: '4px',
-                              }}
-                              onMouseEnter={(e) => {
-                                e.currentTarget.style.opacity = '0.8';
-                              }}
-                              onMouseLeave={(e) => {
-                                e.currentTarget.style.opacity = '1';
-                              }}
-                              title={\`\${node.data.title || node.id} (Score: \${nodeScore.toFixed(2)}, Strategy: \${nodeStrategy}, Selected: \${nodeIsSelected})\`}
-                            >
-                              {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
-                              <div style={{
-                                display: 'flex',
-                                alignItems: 'center',
-                                gap: '6px',
-                              }}>
-                                {/* 策略类型竖线 */}
-                                <div style={{
-                                  width: '3px',
-                                  height: '16px',
-                                  background: strategyColor,
-                                  borderRadius: '2px',
-                                  flexShrink: 0,
-                                }} />
-
-                                {/* 节点类型图标 */}
-                                <span style={{
-                                  fontSize: '11px',
-                                  flexShrink: 0,
-                                }}>
-                                  {node.type === 'note' ? '📝' : '🔍'}
-                                </span>
-
-                                {/* 节点文字 */}
-                                <span style={{
-                                  flex: 1,
-                                  fontSize: '12px',
-                                  color: nodeIsSelected ? '#374151' : '#ef4444',
-                                }}>
-                                  {truncateMiddle(node.data.title || node.id, 18)}
-                                </span>
-
-                                {/* 分数显示 */}
-                                <span style={{
-                                  fontSize: '10px',
-                                  color: '#6b7280',
-                                  fontWeight: '500',
-                                  flexShrink: 0,
-                                }}>
-                                  {nodeScore.toFixed(2)}
-                                </span>
-                              </div>
-
-                              {/* 分数下划线 */}
-                              <div style={{
-                                width: (nodeScore * 100) + '%',
-                                height: '2px',
-                                background: getScoreColor(nodeScore),
-                                borderRadius: '1px',
-                                marginLeft: '9px',
-                              }} />
-                            </span>
-                            {index < path.length - 1 && <span style={{ color: '#9ca3af' }}>›</span>}
-                          </React.Fragment>
-                        )})}
-                      </div>
-                    ))}
-                  </div>
-                );
-              })()}
-            </div>
-          ) : (
-            <div style={{ fontSize: '13px', color: '#9ca3af', textAlign: 'center' }}>
-              选择一个节点查看路径
-            </div>
-          )}
-        </div>
-      </div>
-
-      {/* 主内容区:目录 + 画布 */}
-      <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
-        {/* 左侧目录树 */}
-        <div style={{
-          width: '320px',
-          background: 'white',
-          borderRight: '1px solid #e5e7eb',
-          display: 'flex',
-          flexDirection: 'column',
-          flexShrink: 0,
-        }}>
-          <div style={{
-            padding: '12px 16px',
-            borderBottom: '1px solid #e5e7eb',
-            display: 'flex',
-            justifyContent: 'space-between',
-            alignItems: 'center',
-          }}>
-            <span style={{
-              fontWeight: '600',
-              fontSize: '14px',
-              color: '#111827',
-            }}>
-              节点目录
-            </span>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedTreeNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="展开全部节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const getAllTreeKeys = (nodes) => {
-                    const keys = new Set();
-                    const traverse = (node) => {
-                      if (node.children && node.children.length > 0) {
-                        keys.add(node.treeKey);
-                        node.children.forEach(traverse);
-                      }
-                    };
-                    nodes.forEach(traverse);
-                    return keys;
-                  };
-                  setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="折叠全部节点"
-              >
-                全部折叠
-              </button>
-            </div>
-          </div>
-          <div style={{
-            flex: 1,
-            overflowX: 'auto',
-            overflowY: 'auto',
-            padding: '8px',
-          }}>
-            <div style={{ minWidth: 'fit-content' }}>
-              {renderTree(treeRoots)}
-            </div>
-          </div>
-        </div>
-
-        {/* 画布区域 */}
-        <div style={{ flex: 1, position: 'relative' }}>
-
-          {/* 右侧图例 */}
-          <div style={{
-            position: 'absolute',
-            top: '20px',
-            right: '20px',
-            background: 'white',
-            padding: '16px',
-            borderRadius: '12px',
-            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
-            zIndex: 1000,
-            maxWidth: '260px',
-            border: '1px solid #e5e7eb',
-          }}>
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
-          <h3 style={{ fontSize: '14px', fontWeight: '600', color: '#111827', margin: 0 }}>图例</h3>
-          <button
-            onClick={() => setFocusMode(!focusMode)}
-            style={{
-              fontSize: '11px',
-              padding: '4px 8px',
-              borderRadius: '4px',
-              border: '1px solid',
-              borderColor: focusMode ? '#3b82f6' : '#d1d5db',
-              background: focusMode ? '#3b82f6' : 'white',
-              color: focusMode ? 'white' : '#6b7280',
-              cursor: 'pointer',
-              fontWeight: '500',
-            }}
-            title={focusMode ? '关闭聚焦模式' : '开启聚焦模式'}
-          >
-            {focusMode ? '🎯 聚焦' : '📊 全图'}
-          </button>
-        </div>
-
-        <div style={{ fontSize: '12px' }}>
-          {/* 画布节点展开/折叠控制 */}
-          <div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>节点控制</div>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="展开画布中所有节点的子节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const allNodeIds = new Set(initialNodes.map(n => n.id));
-                  setCollapsedNodes(allNodeIds);
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="折叠画布中所有节点的子节点"
-              >
-                全部折叠
-              </button>
-            </div>
-          </div>
-
-          <div style={{ paddingTop: '12px', borderTop: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>策略类型</div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#10b981', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>初始分词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#06b6d4', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>调用sug</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#f59e0b', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>同义改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#3b82f6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>加词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#8b5cf6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#ec4899', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>基于部分匹配改进</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#a855f7', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#fb923c', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-同义改写</span>
-            </div>
-          </div>
-
-          <div style={{
-            marginTop: '12px',
-            paddingTop: '12px',
-            borderTop: '1px solid #f3f4f6',
-            fontSize: '11px',
-            color: '#9ca3af',
-            lineHeight: '1.5',
-          }}>
-            💡 点击节点左上角 × 隐藏节点
-          </div>
-
-          {/* 隐藏节点列表 - 在图例内部 */}
-          {hiddenNodes.size > 0 && (
-            <div style={{
-              marginTop: '12px',
-              paddingTop: '12px',
-              borderTop: '1px solid #f3f4f6',
-            }}>
-              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
-                <h4 style={{ fontSize: '12px', fontWeight: '600', color: '#111827' }}>已隐藏节点</h4>
-                <button
-                  onClick={() => setHiddenNodes(new Set())}
-                  style={{
-                    fontSize: '10px',
-                    color: '#3b82f6',
-                    background: 'none',
-                    border: 'none',
-                    cursor: 'pointer',
-                    textDecoration: 'underline',
-                  }}
-                >
-                  全部恢复
-                </button>
-              </div>
-              <div style={{ fontSize: '12px', maxHeight: '200px', overflow: 'auto' }}>
-                {Array.from(hiddenNodes).map(nodeId => {
-                  const node = initialNodes.find(n => n.id === nodeId);
-                  if (!node) return null;
-                  return (
-                    <div
-                      key={nodeId}
-                      style={{
-                        display: 'flex',
-                        justifyContent: 'space-between',
-                        alignItems: 'center',
-                        padding: '6px 8px',
-                        margin: '4px 0',
-                        background: '#f9fafb',
-                        borderRadius: '6px',
-                        fontSize: '11px',
-                      }}
-                    >
-                      <span
-                        style={{
-                          flex: 1,
-                          overflow: 'hidden',
-                          textOverflow: 'ellipsis',
-                          whiteSpace: 'nowrap',
-                          color: '#374151',
-                        }}
-                        title={node.data.title || nodeId}
-                      >
-                        {node.data.title || nodeId}
-                      </span>
-                      <button
-                        onClick={() => {
-                          setHiddenNodes(prev => {
-                            const newSet = new Set(prev);
-                            newSet.delete(nodeId);
-                            return newSet;
-                          });
-                        }}
-                        style={{
-                          marginLeft: '8px',
-                          fontSize: '10px',
-                          color: '#10b981',
-                          background: 'none',
-                          border: 'none',
-                          cursor: 'pointer',
-                          flexShrink: 0,
-                        }}
-                      >
-                        恢复
-                      </button>
-                    </div>
-                  );
-                })}
-              </div>
-            </div>
-          )}
-          </div>
-          </div>
-
-          {/* React Flow 画布 */}
-          <ReactFlow
-            nodes={nodes}
-            edges={edges}
-            nodeTypes={nodeTypes}
-            fitView
-            fitViewOptions={{ padding: 0.2, duration: 500 }}
-            minZoom={0.1}
-            maxZoom={1.5}
-            nodesDraggable={true}
-            nodesConnectable={false}
-            elementsSelectable={true}
-            defaultEdgeOptions={{
-              type: 'smoothstep',
-            }}
-            proOptions={{ hideAttribution: true }}
-            onNodeClick={(event, clickedNode) => {
-              setSelectedNodeId(clickedNode.id);
-            }}
-          >
-            <Controls style={{ bottom: '20px', left: 'auto', right: '20px' }} />
-            <Background variant="dots" gap={20} size={1} color="#e5e7eb" />
-          </ReactFlow>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function App() {
-  return (
-    <ReactFlowProvider>
-      <FlowContent />
-    </ReactFlowProvider>
-  );
-}
-
-const root = createRoot(document.getElementById('root'));
-root.render(<App />);
-`;
-
-fs.writeFileSync(reactComponentPath, reactComponent);
-
-// 使用 esbuild 打包
-console.log('🎨 Building modern visualization...');
-
-build({
-  entryPoints: [reactComponentPath],
-  bundle: true,
-  outfile: path.join(__dirname, 'bundle_v2.js'),
-  format: 'iife',
-  loader: {
-    '.css': 'css',
-  },
-  minify: false,
-  sourcemap: 'inline',
-  // 强制所有 React 引用指向同一个位置,避免多副本
-  alias: {
-    'react': path.join(__dirname, 'node_modules/react'),
-    'react-dom': path.join(__dirname, 'node_modules/react-dom'),
-    'react/jsx-runtime': path.join(__dirname, 'node_modules/react/jsx-runtime'),
-    'react/jsx-dev-runtime': path.join(__dirname, 'node_modules/react/jsx-dev-runtime'),
-  },
-}).then(() => {
-  // 读取打包后的 JS
-  const bundleJs = fs.readFileSync(path.join(__dirname, 'bundle_v2.js'), 'utf-8');
-
-  // 读取 CSS
-  const cssPath = path.join(__dirname, 'node_modules/@xyflow/react/dist/style.css');
-  const css = fs.readFileSync(cssPath, 'utf-8');
-
-  // 生成最终 HTML
-  const html = `<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>查询图可视化</title>
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
-    <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
-    <script>
-      // 过滤特定的 React 警告
-      const originalError = console.error;
-      console.error = (...args) => {
-        if (typeof args[0] === 'string' && args[0].includes('Each child in a list should have a unique "key" prop')) {
-          return;
-        }
-        originalError.apply(console, args);
-      };
-    </script>
-    <style>
-        * {
-            margin: 0;
-            padding: 0;
-            box-sizing: border-box;
-        }
-        body {
-            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-            overflow: hidden;
-            -webkit-font-smoothing: antialiased;
-            -moz-osx-font-smoothing: grayscale;
-        }
-        #root {
-            width: 100vw;
-            height: 100vh;
-        }
-        ${css}
-
-        /* 自定义样式覆盖 */
-        .react-flow__edge-path {
-            stroke-linecap: round;
-        }
-        .react-flow__controls {
-            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-            border: 1px solid #e5e7eb;
-            border-radius: 8px;
-        }
-        .react-flow__controls-button {
-            border: none;
-            border-bottom: 1px solid #e5e7eb;
-        }
-        .react-flow__controls-button:hover {
-            background: #f9fafb;
-        }
-    </style>
-</head>
-<body>
-    <div id="root"></div>
-    <script>${bundleJs}</script>
-</body>
-</html>`;
-
-  // 写入输出文件
-  fs.writeFileSync(outputFile, html);
-
-  // 清理临时文件
-  fs.unlinkSync(reactComponentPath);
-  fs.unlinkSync(path.join(__dirname, 'bundle_v2.js'));
-
-  console.log('✅ Visualization generated: ' + outputFile);
-  console.log('📊 Nodes: ' + Object.keys(data.nodes).length);
-  console.log('🔗 Edges: ' + data.edges.length);
-}).catch(error => {
-  console.error('❌ Build error:', error);
-  process.exit(1);
-});

+ 0 - 25
visualization/sug_v6_1_2_6/package.json

@@ -1,25 +0,0 @@
-{
-  "name": "sug-v6-1-2-6-visualization",
-  "version": "1.0.0",
-  "description": "可视化工具 for sug_v6_1_2_6.py - React Flow based query graph visualization",
-  "main": "index.js",
-  "scripts": {
-    "visualize": "node index.js"
-  },
-  "dependencies": {
-    "react": "^19.2.0",
-    "react-dom": "^19.2.0",
-    "esbuild": "^0.25.11",
-    "@xyflow/react": "^12.9.1",
-    "dagre": "^0.8.5",
-    "zustand": "^5.0.2"
-  },
-  "keywords": [
-    "visualization",
-    "react-flow",
-    "query-graph",
-    "dagre"
-  ],
-  "author": "",
-  "license": "ISC"
-}

+ 0 - 200
visualization/sug_v6_1_2_8/README.md

@@ -1,200 +0,0 @@
-# sug_v6_1_2_8 可视化工具
-
-## 概述
-
-这是 `sug_v6_1_2_8.py` 的配套可视化工具。**基于 v6.1.2.5 的 React Flow 可视化引擎**,通过数据转换层将 v6.1.2.8 的轮次数据转换为图结构,实现美观的交互式可视化。
-
-## 🎯 核心特性
-
-### 1. 智能数据转换
-- 自动检测数据格式(v6.1.2.5 或 v6.1.2.8)
-- 将轮次数据(rounds)转换为节点-边图结构
-- 保留完整的轮次信息和来源追踪
-
-### 2. 多类型节点支持
-| 节点类型 | 颜色 | 说明 |
-|---------|------|------|
-| `root` | 紫色 (#6b21a8) | 原始问题根节点 |
-| `seg` | 绿色 (#10b981) | 初始分词结果 |
-| `q` | 蓝色 (#3b82f6) | 查询节点 |
-| `search` | 深紫 (#8b5cf6) | 搜索操作节点 |
-| `note` | 粉色 (#ec4899) | 帖子结果节点 |
-
-### 3. 来源标识
-清楚展示每个Query的来源:
-- **seg** - 来自分词
-- **add** - 加词生成
-- **sug** - 建议词生成
-
-### 4. 交互功能
-继承 v6.1.2.5 的所有交互功能:
-- ✅ 节点拖拽和缩放
-- ✅ 按层级筛选
-- ✅ 节点搜索
-- ✅ 路径高亮
-- ✅ 目录树导航
-- ✅ 节点折叠/展开
-- ✅ 聚焦功能
-
-## 📁 文件结构
-
-```
-visualization/sug_v6_1_2_8/
-├── index.js                  # 主可视化脚本(支持格式检测)
-├── convert_v8_to_graph.js   # 数据转换层
-├── package.json             # 依赖配置
-├── node_modules/            # 依赖包(React Flow, esbuild等)
-└── README.md               # 本文档
-```
-
-## 🚀 使用方式
-
-### 方式1:通过主脚本自动生成
-
-```bash
-# 运行脚本并自动生成可视化
-python sug_v6_1_2_8.py --max-rounds 2 --sug-threshold 0.5 --visualize
-```
-
-### 方式2:手动生成可视化
-
-```bash
-# 从 run_context.json 生成可视化
-node visualization/sug_v6_1_2_8/index.js \
-  input/简单扣图/output/sug_v6_1_2_8/20251031/164016_6e/run_context.json \
-  output.html
-```
-
-## 📊 数据转换说明
-
-### 输入格式(v6.1.2.8)
-
-```json
-{
-  "o": "快速进行图片背景移除和替换",
-  "rounds": [
-    {
-      "round_num": 0,
-      "type": "initialization",
-      "seg_list": [
-        {"text": "快速", "score": 0.1},
-        {"text": "图片", "score": 0.1}
-      ],
-      "q_list_1": [...]
-    },
-    {
-      "round_num": 1,
-      "input_q_list": [...],
-      "output_q_list": [
-        {"text": "快速图片", "score": 0.2, "from": "add"}
-      ],
-      "search_count": 3
-    }
-  ]
-}
-```
-
-### 转换后格式(图结构)
-
-```json
-{
-  "nodes": {
-    "root_o": {
-      "type": "root",
-      "query": "快速进行图片背景移除和替换",
-      "level": 0
-    },
-    "seg_快速_0": {
-      "type": "seg",
-      "query": "快速",
-      "level": 1
-    },
-    "q_快速图片_r2_0": {
-      "type": "q",
-      "query": "快速图片",
-      "level": 2,
-      "from_source": "add"
-    }
-  },
-  "edges": [
-    {
-      "from": "root_o",
-      "to": "seg_快速_0",
-      "edge_type": "root_to_seg"
-    },
-    {
-      "from": "seg_快速_0",
-      "to": "q_快速_r1",
-      "edge_type": "seg_to_q"
-    }
-  ]
-}
-```
-
-## 🎨 可视化效果
-
-### 图布局
-- **水平布局**:从左到右按轮次展开
-- **层次化**:相同轮次的节点纵向排列
-- **自动布局**:使用 dagre 算法自动计算最佳位置
-
-### 节点样式
-- **边框颜色**:根据节点类型区分
-- **标签内容**:显示 Query 文本、分数、策略
-- **特殊标识**:
-  - 未选中的节点:红色 "未选中" 标签
-  - Search 节点:显示搜索次数和帖子数
-
-### 边样式
-- **虚线**:表示不同类型的关系
-- **颜色**:根据策略类型着色
-- **箭头**:指示数据流向
-
-## 📦 依赖
-
-所有依赖已包含在 `node_modules/` 中:
-- `react` + `react-dom` - UI 框架
-- `@xyflow/react` - 流程图库
-- `dagre` - 图布局算法
-- `esbuild` - 打包工具
-- `zustand` - 状态管理
-
-总大小:约 33MB
-
-## 🔧 技术实现
-
-### 转换层(convert_v8_to_graph.js)
-- 解析 rounds 数据
-- 创建图节点和边
-- 维护轮次信息(iterations)
-
-### 可视化层(index.js)
-- 格式检测和自动转换
-- React Flow 渲染
-- 交互功能实现
-
-### 优势
-- ✅ 复用成熟的可视化引擎
-- ✅ 保持视觉一致性
-- ✅ 完整的交互功能
-- ✅ 易于维护和扩展
-
-## 📝 更新日志
-
-### v1.0.0 (2025-10-31)
-- 基于 v6.1.2.5 可视化引擎
-- 实现轮次数据到图结构的转换
-- 支持新的节点类型(root, seg, q, search)
-- 自动格式检测和转换
-- 完整的交互功能
-
-## 🤝 兼容性
-
-| 版本 | 支持 | 说明 |
-|------|------|------|
-| v6.1.2.5 | ✅ | 直接渲染 query_graph.json |
-| v6.1.2.8 | ✅ | 自动转换 run_context.json |
-
-## 📧 问题反馈
-
-如有问题或建议,请查看主项目 README 或提交 Issue。

+ 0 - 321
visualization/sug_v6_1_2_8/convert_v8_to_graph.js

@@ -1,321 +0,0 @@
-/**
- * 将 v6.1.2.8 的 run_context.json 转换成图结构
- */
-
-function convertV8ToGraph(runContext) {
-  const nodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  nodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_terminated: false,
-    no_suggestion_rounds: 0,
-    evaluation_reason: '用户输入的原始问题',
-    is_selected: true
-  };
-
-  iterations[0] = [rootId];
-
-  // 处理每一轮
-  rounds.forEach((round, roundIndex) => {
-    if (round.type === 'initialization') {
-      // 初始化阶段:处理 seg_list
-      const roundNum = 0;
-      if (!iterations[roundNum]) iterations[roundNum] = [];
-
-      round.seg_list.forEach((seg, segIndex) => {
-        const segId = `seg_${seg.text}_${roundNum}`;
-        nodes[segId] = {
-          type: 'seg',
-          query: seg.text,
-          level: 1,
-          relevance_score: seg.score || 0,
-          strategy: '初始分词',
-          iteration: roundNum,
-          is_terminated: false,
-          evaluation_reason: '分词专家分词结果',
-          is_selected: true,
-          parent_query: o
-        };
-
-        // 添加边:root -> seg
-        edges.push({
-          from: rootId,
-          to: segId,
-          edge_type: 'root_to_seg',
-          strategy: '初始分词'
-        });
-
-        iterations[roundNum].push(segId);
-      });
-
-      // 将 seg 作为第一轮的 q
-      round.q_list_1.forEach((q, qIndex) => {
-        const qId = `q_${q.text}_r1`;
-        const segId = `seg_${q.text}_0`;
-
-        nodes[qId] = {
-          type: 'q',
-          query: q.text,
-          level: 1,
-          relevance_score: q.score || 0,
-          strategy: '来自分词',
-          iteration: 1,
-          is_terminated: false,
-          evaluation_reason: '初始Query',
-          is_selected: true,
-          parent_query: q.text,
-          from_source: 'seg'
-        };
-
-        // 添加边:seg -> q
-        if (nodes[segId]) {
-          edges.push({
-            from: segId,
-            to: qId,
-            edge_type: 'seg_to_q',
-            strategy: '作为Query'
-          });
-        }
-
-        if (!iterations[1]) iterations[1] = [];
-        iterations[1].push(qId);
-      });
-
-    } else {
-      // 普通轮次
-      const roundNum = round.round_num;
-      const nextRoundNum = roundNum + 1;
-
-      if (!iterations[nextRoundNum]) iterations[nextRoundNum] = [];
-
-      // 添加本轮的操作步骤节点
-      const stepNodes = [];
-
-      // 获取第一个输入Query作为所有操作节点的连接点(代表本轮输入)
-      // 注意:需要从已创建的节点中查找,因为节点ID可能带有index后缀
-      let firstInputQId = null;
-      if (round.input_q_list && round.input_q_list.length > 0) {
-        const firstInputQ = round.input_q_list[0];
-        // 尝试查找匹配的节点(考虑可能有index后缀)
-        firstInputQId = Object.keys(nodes).find(id => {
-          const node = nodes[id];
-          return node.type === 'q' &&
-                 node.query === firstInputQ.text &&
-                 node.iteration === roundNum;
-        });
-      }
-
-      // 步骤1:请求sug操作节点
-      if (round.sug_count > 0) {
-        const requestSugId = `operation_request_sug_r${roundNum}`;
-        nodes[requestSugId] = {
-          type: 'operation',
-          query: `步骤1: 请求建议词 (${round.sug_count}个)`,
-          level: roundNum,
-          relevance_score: 0,
-          strategy: '请求建议词',
-          iteration: roundNum,
-          operation_type: 'request_sug',
-          detail: `为${round.input_q_list?.length || 0}个Query请求建议词,获得${round.sug_count}个建议`,
-          is_selected: true
-        };
-        stepNodes.push(requestSugId);
-
-        // 平级连接:从第一个输入Query连接
-        if (firstInputQId && nodes[firstInputQId]) {
-          edges.push({
-            from: firstInputQId,
-            to: requestSugId,
-            edge_type: 'q_to_operation',
-            strategy: '请求建议词'
-          });
-        }
-      }
-
-      // 步骤2:评估sug操作节点
-      if (round.sug_count > 0) {
-        const evaluateSugId = `operation_evaluate_sug_r${roundNum}`;
-        nodes[evaluateSugId] = {
-          type: 'operation',
-          query: `步骤2: 评估建议词 (高分:${round.high_score_sug_count})`,
-          level: roundNum,
-          relevance_score: 0,
-          strategy: '评估建议词',
-          iteration: roundNum,
-          operation_type: 'evaluate_sug',
-          detail: `评估${round.sug_count}个建议词,${round.high_score_sug_count}个达到阈值`,
-          is_selected: true
-        };
-        stepNodes.push(evaluateSugId);
-
-        // 平级连接:从第一个输入Query连接
-        if (firstInputQId && nodes[firstInputQId]) {
-          edges.push({
-            from: firstInputQId,
-            to: evaluateSugId,
-            edge_type: 'q_to_operation',
-            strategy: '评估建议词'
-          });
-        }
-      }
-
-      // 步骤3:执行搜索操作节点
-      if (round.search_count > 0) {
-        const searchOpId = `operation_search_r${roundNum}`;
-        nodes[searchOpId] = {
-          type: 'operation',
-          query: `步骤3: 执行搜索 (${round.search_count}次)`,
-          level: roundNum,
-          relevance_score: 0,
-          strategy: '执行搜索',
-          iteration: roundNum,
-          operation_type: 'search',
-          search_count: round.search_count,
-          total_posts: round.total_posts,
-          detail: `搜索${round.search_count}个高分建议词,找到${round.total_posts}个帖子`,
-          is_selected: true
-        };
-        stepNodes.push(searchOpId);
-
-        // 平级连接:从第一个输入Query连接
-        if (firstInputQId && nodes[firstInputQId]) {
-          edges.push({
-            from: firstInputQId,
-            to: searchOpId,
-            edge_type: 'q_to_operation',
-            strategy: '执行搜索'
-          });
-        }
-      }
-
-      // 步骤4:加词操作节点
-      const addWordCount = round.output_q_list?.filter(q => q.from === 'add').length || 0;
-      if (addWordCount > 0) {
-        const addWordId = `operation_add_word_r${roundNum}`;
-        nodes[addWordId] = {
-          type: 'operation',
-          query: `步骤4: 智能加词 (${addWordCount}个)`,
-          level: roundNum,
-          relevance_score: 0,
-          strategy: '智能加词',
-          iteration: roundNum,
-          operation_type: 'add_word',
-          detail: `为Seed选择词并组合,生成${addWordCount}个新Query`,
-          is_selected: true
-        };
-        stepNodes.push(addWordId);
-
-        // 平级连接:从第一个输入Query连接
-        if (firstInputQId && nodes[firstInputQId]) {
-          edges.push({
-            from: firstInputQId,
-            to: addWordId,
-            edge_type: 'q_to_operation',
-            strategy: '智能加词'
-          });
-        }
-      }
-
-      // 步骤5:筛选高分sug操作节点
-      const sugCount = round.output_q_list?.filter(q => q.from === 'sug').length || 0;
-      if (sugCount > 0) {
-        const filterSugId = `operation_filter_sug_r${roundNum}`;
-        nodes[filterSugId] = {
-          type: 'operation',
-          query: `步骤5: 筛选高分sug (${sugCount}个)`,
-          level: roundNum,
-          relevance_score: 0,
-          strategy: '筛选高分sug',
-          iteration: roundNum,
-          operation_type: 'filter_sug',
-          detail: `筛选出${sugCount}个分数高于来源Query的建议词`,
-          is_selected: true
-        };
-        stepNodes.push(filterSugId);
-
-        // 平级连接:从第一个输入Query连接
-        if (firstInputQId && nodes[firstInputQId]) {
-          edges.push({
-            from: firstInputQId,
-            to: filterSugId,
-            edge_type: 'q_to_operation',
-            strategy: '筛选高分sug'
-          });
-        }
-      }
-
-      // 将操作节点添加到当前轮次
-      stepNodes.forEach(nodeId => {
-        if (!iterations[roundNum]) iterations[roundNum] = [];
-        iterations[roundNum].push(nodeId);
-      });
-
-      // 处理输出的 q_list
-      if (round.output_q_list) {
-        round.output_q_list.forEach((q, qIndex) => {
-          const qId = `q_${q.text}_r${nextRoundNum}_${qIndex}`;
-
-          nodes[qId] = {
-            type: 'q',
-            query: q.text,
-            level: nextRoundNum,
-            relevance_score: q.score || 0,
-            strategy: q.from === 'add' ? '加词生成' : q.from === 'sug' ? '建议词' : '未知',
-            iteration: nextRoundNum,
-            is_terminated: false,
-            evaluation_reason: `来源: ${q.from}`,
-            is_selected: true,
-            from_source: q.from
-          };
-
-          // 连接到对应的操作节点
-          if (q.from === 'add') {
-            // 加词生成的Query连接到加词操作节点
-            const addWordId = `operation_add_word_r${roundNum}`;
-            if (nodes[addWordId]) {
-              edges.push({
-                from: addWordId,
-                to: qId,
-                edge_type: 'operation_to_q',
-                strategy: '加词生成'
-              });
-            }
-          } else if (q.from === 'sug') {
-            // sug生成的Query连接到筛选sug操作节点
-            const filterSugId = `operation_filter_sug_r${roundNum}`;
-            if (nodes[filterSugId]) {
-              edges.push({
-                from: filterSugId,
-                to: qId,
-                edge_type: 'operation_to_q',
-                strategy: '建议词生成'
-              });
-            }
-          }
-
-          iterations[nextRoundNum].push(qId);
-        });
-      }
-    }
-  });
-
-  return {
-    nodes,
-    edges,
-    iterations
-  };
-}
-
-module.exports = { convertV8ToGraph };

+ 0 - 887
visualization/sug_v6_1_2_8/convert_v8_to_graph_v2.js

@@ -1,887 +0,0 @@
-/**
- * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构
- */
-
-function convertV8ToGraphV2(runContext, searchResults) {
-  const nodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  nodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true
-  };
-
-  iterations[0] = [rootId];
-
-  // 处理每一轮
-  rounds.forEach((round, roundIndex) => {
-    if (round.type === 'initialization') {
-      // Round 0: 初始化阶段
-      const roundNum = 0;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum} (初始化)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '初始化',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: '初始化'
-      });
-
-      if (!iterations[roundNum]) iterations[roundNum] = [];
-      iterations[roundNum].push(roundId);
-
-      // 创建分词步骤节点
-      const segStepId = `step_seg_r${roundNum}`;
-      nodes[segStepId] = {
-        type: 'step',
-        query: `步骤:分词 (${round.seg_list?.length || 0}个分词)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '分词',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: segStepId,
-        edge_type: 'round_to_step',
-        strategy: '分词'
-      });
-
-      iterations[roundNum].push(segStepId);
-
-      // 添加分词结果作为步骤的子节点
-      round.seg_list?.forEach((seg, segIndex) => {
-        const segId = `seg_${seg.text}_${roundNum}_${segIndex}`;
-        nodes[segId] = {
-          type: 'seg',
-          query: seg.text,
-          level: roundNum + 1,
-          relevance_score: seg.score || 0,
-          evaluationReason: seg.reason || '',
-          strategy: '分词结果',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: segStepId,
-          to: segId,
-          edge_type: 'step_to_data',
-          strategy: '分词结果'
-        });
-
-        if (!iterations[roundNum + 1]) iterations[roundNum + 1] = [];
-        iterations[roundNum + 1].push(segId);
-      });
-
-    } else {
-      // 普通轮次
-      const roundNum = round.round_num;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum}`,
-        level: roundNum * 10, // 使用10的倍数作为层级
-        relevance_score: 0,
-        strategy: `第${roundNum}轮`,
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: `第${roundNum}轮`
-      });
-
-      if (!iterations[roundNum * 10]) iterations[roundNum * 10] = [];
-      iterations[roundNum * 10].push(roundId);
-
-      // 步骤1: 请求&评估推荐词
-      if (round.sug_details && Object.keys(round.sug_details).length > 0) {
-        const sugStepId = `step_sug_r${roundNum}`;
-        const totalSugs = Object.values(round.sug_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[sugStepId] = {
-          type: 'step',
-          query: `步骤1: 请求&评估推荐词 (${totalSugs}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '请求&评估推荐词',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: sugStepId,
-          edge_type: 'round_to_step',
-          strategy: '推荐词'
-        });
-
-        iterations[roundNum * 10].push(sugStepId);
-
-        // 为每个 Q 创建节点
-        Object.keys(round.sug_details).forEach((qText, qIndex) => {
-          // 从q_list_1中查找对应的q获取分数和理由
-          // Round 0: 从q_list_1查找; Round 1+: 从input_q_list查找
-          let qData = {};
-          if (roundNum === 0) {
-            qData = round.q_list_1?.find(q => q.text === qText) || {};
-          } else {
-            // 从当前轮的input_q_list中查找
-            qData = round.input_q_list?.find(q => q.text === qText) || {};
-          }
-          const qId = `q_${qText}_r${roundNum}_${qIndex}`;
-          nodes[qId] = {
-            type: 'q',
-            query: qText,
-            level: roundNum * 10 + 2,
-            relevance_score: qData.score || 0,
-            evaluationReason: qData.reason || '',
-            strategy: 'Query',
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: sugStepId,
-            to: qId,
-            edge_type: 'step_to_q',
-            strategy: 'Query'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(qId);
-
-          // 为每个 Q 的 sug 创建节点
-          const sugs = round.sug_details[qText] || [];
-          sugs.forEach((sug, sugIndex) => {
-            const sugId = `sug_${sug.text}_r${roundNum}_q${qIndex}_${sugIndex}`;
-            nodes[sugId] = {
-              type: 'sug',
-              query: sug.text,
-              level: roundNum * 10 + 3,
-              relevance_score: sug.score || 0,
-              evaluationReason: sug.reason || '',
-              strategy: '推荐词',
-              iteration: roundNum,
-              is_selected: true
-            };
-
-            edges.push({
-              from: qId,
-              to: sugId,
-              edge_type: 'q_to_sug',
-              strategy: '推荐词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(sugId);
-          });
-        });
-      }
-
-      // 步骤2: 筛选并执行搜索
-      const searchStepId = `step_search_r${roundNum}`;
-      const searchCountText = round.search_count > 0
-        ? `筛选${round.high_score_sug_count}个高分词,搜索${round.search_count}次,${round.total_posts}个帖子`
-        : `无高分推荐词,未执行搜索`;
-
-      nodes[searchStepId] = {
-        type: 'step',
-        query: `步骤2: 筛选并执行搜索 (${searchCountText})`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '筛选并执行搜索',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: searchStepId,
-        edge_type: 'round_to_step',
-        strategy: '搜索'
-      });
-
-      iterations[roundNum * 10].push(searchStepId);
-
-      // 只有在有搜索结果时才添加搜索词和帖子
-      // 优先使用 round.search_results(新格式),否则使用外部传入的 searchResults(兼容旧版本)
-      const roundSearchResults = round.search_results || searchResults;
-      if (round.search_count > 0 && roundSearchResults) {
-        if (Array.isArray(roundSearchResults)) {
-          roundSearchResults.forEach((search, searchIndex) => {
-            const searchWordId = `search_${search.text}_r${roundNum}_${searchIndex}`;
-            nodes[searchWordId] = {
-              type: 'search_word',
-              query: search.text,
-              level: roundNum * 10 + 2,
-              relevance_score: search.score_with_o || 0,
-              strategy: '搜索词',
-              iteration: roundNum,
-              is_selected: true
-            };
-
-            edges.push({
-              from: searchStepId,
-              to: searchWordId,
-              edge_type: 'step_to_search_word',
-              strategy: '搜索词'
-            });
-
-            if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-            iterations[roundNum * 10 + 2].push(searchWordId);
-
-            // 添加帖子
-            if (search.post_list && search.post_list.length > 0) {
-              search.post_list.forEach((post, postIndex) => {
-                const postId = `post_${post.note_id}_${searchIndex}_${postIndex}`;
-
-                // 准备图片列表,将URL字符串转换为对象格式供轮播图使用
-                const imageList = (post.images || []).map(url => ({
-                  image_url: url
-                }));
-
-                nodes[postId] = {
-                  type: 'post',
-                  query: post.title,
-                  level: roundNum * 10 + 3,
-                  relevance_score: 0,
-                  strategy: '帖子',
-                  iteration: roundNum,
-                  is_selected: true,
-                  note_id: post.note_id,
-                  note_url: post.note_url,
-                  body_text: post.body_text || '',
-                  images: post.images || [],
-                  image_list: imageList,
-                  interact_info: post.interact_info || {}
-                };
-
-                edges.push({
-                  from: searchWordId,
-                  to: postId,
-                  edge_type: 'search_word_to_post',
-                  strategy: '搜索结果'
-                });
-
-                if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-                iterations[roundNum * 10 + 3].push(postId);
-              });
-            }
-          });
-        }
-      }
-
-      // 步骤3: 加词生成新查询
-      if (round.add_word_details && Object.keys(round.add_word_details).length > 0) {
-        const addWordStepId = `step_add_r${roundNum}`;
-        const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[addWordStepId] = {
-          type: 'step',
-          query: `步骤3: 加词生成新查询 (${totalAddWords}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '加词生成新查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: addWordStepId,
-          edge_type: 'round_to_step',
-          strategy: '加词'
-        });
-
-        iterations[roundNum * 10].push(addWordStepId);
-
-        // 为每个 Seed 创建节点
-        Object.keys(round.add_word_details).forEach((seedText, seedIndex) => {
-          const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`;
-
-          // 查找seed的来源信息 - 从Round 0的seed_list查找基础种子的from_type
-          const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
-          const seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
-          const fromType = seedInfo.from_type || 'unknown';
-
-          // 根据来源设置strategy
-          let strategy;
-          if (fromType === 'seg') {
-            strategy = '初始分词';
-          } else if (fromType === 'add') {
-            strategy = '加词';
-          } else if (fromType === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed';  // 默认灰色
-          }
-
-          nodes[seedId] = {
-            type: 'seed',
-            query: seedText,
-            level: roundNum * 10 + 2,
-            relevance_score: 0,
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: addWordStepId,
-            to: seedId,
-            edge_type: 'step_to_seed',
-            strategy: 'Seed'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(seedId);
-
-          // 为每个 Seed 的组合词创建节点
-          const combinedWords = round.add_word_details[seedText] || [];
-          combinedWords.forEach((word, wordIndex) => {
-            const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`;
-            nodes[wordId] = {
-              type: 'add_word',
-              query: word.text,
-              level: roundNum * 10 + 3,
-              relevance_score: word.score || 0,
-              evaluationReason: word.reason || '',
-              strategy: '加词生成',
-              iteration: roundNum,
-              is_selected: true,
-              selected_word: word.selected_word
-            };
-
-            edges.push({
-              from: seedId,
-              to: wordId,
-              edge_type: 'seed_to_add_word',
-              strategy: '组合词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(wordId);
-          });
-        });
-      }
-
-      // 步骤4: 筛选推荐词进入下轮
-      const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || [];
-      if (filteredSugs.length > 0) {
-        const filterStepId = `step_filter_r${roundNum}`;
-        nodes[filterStepId] = {
-          type: 'step',
-          query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '筛选推荐词进入下轮',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: filterStepId,
-          edge_type: 'round_to_step',
-          strategy: '筛选'
-        });
-
-        iterations[roundNum * 10].push(filterStepId);
-
-        // 添加筛选出的sug
-        filteredSugs.forEach((sug, sugIndex) => {
-          const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`;
-          nodes[sugId] = {
-            type: 'filtered_sug',
-            query: sug.text,
-            level: roundNum * 10 + 2,
-            relevance_score: sug.score || 0,
-            strategy: '进入下轮',
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: filterStepId,
-            to: sugId,
-            edge_type: 'step_to_filtered_sug',
-            strategy: '进入下轮'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(sugId);
-        });
-      }
-
-      // 步骤5: 构建下一轮
-      const nextRoundStepId = `step_next_round_r${roundNum}`;
-      const nextQCount = round.output_q_list?.length || 0;
-      const nextSeedCount = round.seed_list_next_size || 0;
-
-      nodes[nextRoundStepId] = {
-        type: 'step',
-        query: `步骤5: 构建下一轮 (${nextQCount}个查询, ${nextSeedCount}个种子)`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '构建下一轮',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: nextRoundStepId,
-        edge_type: 'round_to_step',
-        strategy: '构建下一轮'
-      });
-
-      iterations[roundNum * 10].push(nextRoundStepId);
-
-      // 5.1: 构建下轮查询
-      if (round.output_q_list && round.output_q_list.length > 0) {
-        const nextQStepId = `step_next_q_r${roundNum}`;
-        nodes[nextQStepId] = {
-          type: 'step',
-          query: `构建下轮查询 (${nextQCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextQStepId,
-          edge_type: 'step_to_step',
-          strategy: '查询'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextQStepId);
-
-        // 添加下轮查询列表
-        round.output_q_list.forEach((q, qIndex) => {
-          const nextQId = `next_q_${q.text}_r${roundNum}_${qIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (q.from === 'seg') {
-            strategy = '初始分词';
-          } else if (q.from === 'add') {
-            strategy = '加词';
-          } else if (q.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Query'; // 默认
-          }
-
-          nodes[nextQId] = {
-            type: 'next_q',
-            query: q.text,
-            level: roundNum * 10 + 3,
-            relevance_score: q.score || 0,
-            evaluationReason: q.reason || '',
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: q.from
-          };
-
-          edges.push({
-            from: nextQStepId,
-            to: nextQId,
-            edge_type: 'step_to_next_q',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextQId);
-        });
-      }
-
-      // 5.2: 构建下轮种子(如果有数据的话)
-      if (nextSeedCount > 0 && round.seed_list_next) {
-        const nextSeedStepId = `step_next_seed_r${roundNum}`;
-        nodes[nextSeedStepId] = {
-          type: 'step',
-          query: `构建下轮种子 (${nextSeedCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮种子',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextSeedStepId,
-          edge_type: 'step_to_step',
-          strategy: '种子'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextSeedStepId);
-
-        // 添加下轮种子列表
-        round.seed_list_next.forEach((seed, seedIndex) => {
-          const nextSeedId = `next_seed_${seed.text}_r${roundNum}_${seedIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (seed.from === 'seg') {
-            strategy = '初始分词';
-          } else if (seed.from === 'add') {
-            strategy = '加词';
-          } else if (seed.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed'; // 默认
-          }
-
-          nodes[nextSeedId] = {
-            type: 'next_seed',
-            query: seed.text,
-            level: roundNum * 10 + 3,
-            relevance_score: seed.score || 0,
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: seed.from
-          };
-
-          edges.push({
-            from: nextSeedStepId,
-            to: nextSeedId,
-            edge_type: 'step_to_next_seed',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextSeedId);
-        });
-      }
-    }
-  });
-
-  return {
-    nodes,
-    edges,
-    iterations
-  };
-}
-
-/**
- * 简化版转换:专注于query和post的演化
- * - 合并所有query节点(不区分seg/sug/add_word)
- * - 合并相同的帖子节点
- * - 步骤信息放在边上
- * - 隐藏Round/Step节点
- */
-function convertV8ToGraphSimplified(runContext, searchResults) {
-  const mergedNodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  mergedNodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true,
-    occurrences: [{round: 0, role: 'root', score: 1.0}]
-  };
-  iterations[0] = [rootId];
-
-  // 用于记录节点之间的演化关系
-  const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
-  const postMap = {}; // {note_id: {...}}
-
-  // 第一遍:收集所有query和post
-  rounds.forEach((round, roundIndex) => {
-    const roundNum = round.round_num || roundIndex;
-
-    if (round.type === 'initialization') {
-      // Round 0: 收集分词结果
-      (round.q_list_1 || []).forEach(q => {
-        if (!queryEvolution[q.text]) {
-          queryEvolution[q.text] = {
-            occurrences: [],
-            parentTexts: new Set([o]), // 来自原始问题
-            childTexts: new Set()
-          };
-        }
-        queryEvolution[q.text].occurrences.push({
-          round: roundNum,
-          role: 'segmentation',
-          strategy: '分词',
-          score: q.score,
-          reason: q.reason
-        });
-      });
-    } else {
-      // Round 1+
-
-      // 收集sug_details (推荐词)
-      Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
-        sugs.forEach(sug => {
-          if (!queryEvolution[sug.text]) {
-            queryEvolution[sug.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[sug.text].occurrences.push({
-            round: roundNum,
-            role: 'sug',
-            strategy: '调用sug',
-            score: sug.score,
-            reason: sug.reason
-          });
-          queryEvolution[sug.text].parentTexts.add(parentText);
-          if (queryEvolution[parentText]) {
-            queryEvolution[parentText].childTexts.add(sug.text);
-          }
-        });
-      });
-
-      // 收集add_word_details (加词结果)
-      Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
-        words.forEach(word => {
-          if (!queryEvolution[word.text]) {
-            queryEvolution[word.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[word.text].occurrences.push({
-            round: roundNum,
-            role: 'add_word',
-            strategy: '加词',
-            score: word.score,
-            reason: word.reason,
-            selectedWord: word.selected_word
-          });
-          queryEvolution[word.text].parentTexts.add(seedText);
-          if (queryEvolution[seedText]) {
-            queryEvolution[seedText].childTexts.add(word.text);
-          }
-        });
-      });
-
-      // 收集搜索结果和帖子
-      const roundSearchResults = round.search_results || searchResults;
-      if (roundSearchResults && Array.isArray(roundSearchResults)) {
-        roundSearchResults.forEach(search => {
-          const searchText = search.text;
-
-          // 标记这个query被用于搜索
-          if (queryEvolution[searchText]) {
-            queryEvolution[searchText].occurrences.push({
-              round: roundNum,
-              role: 'search',
-              strategy: '执行搜索',
-              score: search.score_with_o,
-              postCount: search.post_list ? search.post_list.length : 0
-            });
-          }
-
-          // 收集帖子
-          if (search.post_list && search.post_list.length > 0) {
-            search.post_list.forEach(post => {
-              if (!postMap[post.note_id]) {
-                postMap[post.note_id] = {
-                  ...post,
-                  foundByQueries: new Set(),
-                  foundInRounds: new Set()
-                };
-              }
-              postMap[post.note_id].foundByQueries.add(searchText);
-              postMap[post.note_id].foundInRounds.add(roundNum);
-
-              // 建立query到post的关系
-              if (!queryEvolution[searchText].posts) {
-                queryEvolution[searchText].posts = new Set();
-              }
-              queryEvolution[searchText].posts.add(post.note_id);
-            });
-          }
-        });
-      }
-    }
-  });
-
-  // 第二遍:创建合并后的节点
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    // 获取最新的分数
-    const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
-    const hasSearchResults = data.posts && data.posts.size > 0;
-
-    mergedNodes[nodeId] = {
-      type: 'query',
-      query: text,
-      level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
-      relevance_score: latestOccurrence.score || 0,
-      evaluationReason: latestOccurrence.reason || '',
-      strategy: data.occurrences.map(o => o.strategy).join(' + '),
-      primaryStrategy: latestOccurrence.strategy || '未知',  // 添加主要策略字段
-      iteration: Math.max(...data.occurrences.map(o => o.round), 0),
-      is_selected: true,
-      occurrences: data.occurrences,
-      hasSearchResults: hasSearchResults,
-      postCount: data.posts ? data.posts.size : 0,
-      selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || ''
-    };
-
-    // 添加到对应的轮次
-    const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
-    const iterKey = maxRound * 10 + 2;
-    if (!iterations[iterKey]) iterations[iterKey] = [];
-    iterations[iterKey].push(nodeId);
-  });
-
-  // 创建帖子节点
-  Object.entries(postMap).forEach(([noteId, post]) => {
-    const postId = `post_${noteId}`;
-
-    const imageList = (post.images || []).map(url => ({
-      image_url: url
-    }));
-
-    mergedNodes[postId] = {
-      type: 'post',
-      query: post.title,
-      level: 100, // 放在最后
-      relevance_score: 0,
-      strategy: '帖子',
-      iteration: Math.max(...Array.from(post.foundInRounds)),
-      is_selected: true,
-      note_id: post.note_id,
-      note_url: post.note_url,
-      body_text: post.body_text || '',
-      images: post.images || [],
-      image_list: imageList,
-      interact_info: post.interact_info || {},
-      foundByQueries: Array.from(post.foundByQueries),
-      foundInRounds: Array.from(post.foundInRounds)
-    };
-
-    if (!iterations[100]) iterations[100] = [];
-    iterations[100].push(postId);
-  });
-
-  // 第三遍:创建边
-  // 1. 原始问题 -> 分词结果
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-    const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
-
-    if (segOccurrence && data.parentTexts.has(o)) {
-      edges.push({
-        from: rootId,
-        to: nodeId,
-        edge_type: 'segmentation',
-        strategy: '分词',
-        label: '分词',
-        round: 0
-      });
-    }
-  });
-
-  // 2. Query演化关系
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    data.parentTexts.forEach(parentText => {
-      if (parentText === o) return; // 跳过原始问题(已处理)
-
-      const parentNodeId = `query_${parentText}`;
-      if (!mergedNodes[parentNodeId]) return;
-
-      // 找到这个演化的策略和轮次
-      const occurrence = data.occurrences.find(o =>
-        o.role === 'sug' || o.role === 'add_word'
-      );
-
-      edges.push({
-        from: parentNodeId,
-        to: nodeId,
-        edge_type: occurrence?.role || 'evolution',
-        strategy: occurrence?.strategy || '演化',
-        label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
-        round: occurrence?.round || 0
-      });
-    });
-  });
-
-  // 3. Query -> Post (搜索关系)
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    if (data.posts && data.posts.size > 0) {
-      const searchOccurrence = data.occurrences.find(o => o.role === 'search');
-
-      data.posts.forEach(noteId => {
-        const postId = `post_${noteId}`;
-        edges.push({
-          from: nodeId,
-          to: postId,
-          edge_type: 'search',
-          strategy: '搜索',
-          label: `搜索 (${data.posts.size}个帖子)`,
-          round: searchOccurrence?.round || 0
-        });
-      });
-    }
-  });
-
-  return {
-    nodes: mergedNodes,
-    edges,
-    iterations
-  };
-}
-
-module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };

+ 0 - 939
visualization/sug_v6_1_2_8/convert_v8_to_graph_v3.js

@@ -1,939 +0,0 @@
-/**
- * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构
- * v3: 增加 [Q] 和 [SUG] 标识前缀
- */
-
-function convertV8ToGraphV2(runContext, searchResults) {
-  const nodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  nodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true
-  };
-
-  iterations[0] = [rootId];
-
-  // 处理每一轮
-  rounds.forEach((round, roundIndex) => {
-    if (round.type === 'initialization') {
-      // Round 0: 初始化阶段
-      const roundNum = 0;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum} (初始化)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '初始化',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: '初始化'
-      });
-
-      if (!iterations[roundNum]) iterations[roundNum] = [];
-      iterations[roundNum].push(roundId);
-
-      // 创建分词步骤节点
-      const segStepId = `step_seg_r${roundNum}`;
-      nodes[segStepId] = {
-        type: 'step',
-        query: `步骤:分词 (${round.seg_list?.length || 0}个分词)`,
-        level: roundNum,
-        relevance_score: 0,
-        strategy: '分词',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: segStepId,
-        edge_type: 'round_to_step',
-        strategy: '分词'
-      });
-
-      iterations[roundNum].push(segStepId);
-
-      // 添加分词结果作为步骤的子节点
-      round.seg_list?.forEach((seg, segIndex) => {
-        const segId = `seg_${seg.text}_${roundNum}_${segIndex}`;
-        nodes[segId] = {
-          type: 'seg',
-          query: seg.text,
-          level: roundNum + 1,
-          relevance_score: seg.score || 0,
-          evaluationReason: seg.reason || '',
-          strategy: '分词结果',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: segStepId,
-          to: segId,
-          edge_type: 'step_to_data',
-          strategy: '分词结果'
-        });
-
-        if (!iterations[roundNum + 1]) iterations[roundNum + 1] = [];
-        iterations[roundNum + 1].push(segId);
-      });
-
-    } else {
-      // 普通轮次
-      const roundNum = round.round_num;
-      const roundId = `round_${roundNum}`;
-
-      // 创建 Round 节点
-      nodes[roundId] = {
-        type: 'round',
-        query: `Round ${roundNum}`,
-        level: roundNum * 10, // 使用10的倍数作为层级
-        relevance_score: 0,
-        strategy: `第${roundNum}轮`,
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: rootId,
-        to: roundId,
-        edge_type: 'root_to_round',
-        strategy: `第${roundNum}轮`
-      });
-
-      if (!iterations[roundNum * 10]) iterations[roundNum * 10] = [];
-      iterations[roundNum * 10].push(roundId);
-
-      // 步骤1: 请求&评估推荐词
-      if (round.sug_details && Object.keys(round.sug_details).length > 0) {
-        const sugStepId = `step_sug_r${roundNum}`;
-        const totalSugs = Object.values(round.sug_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[sugStepId] = {
-          type: 'step',
-          query: `步骤1: 请求&评估推荐词 (${totalSugs}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '请求&评估推荐词',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: sugStepId,
-          edge_type: 'round_to_step',
-          strategy: '推荐词'
-        });
-
-        iterations[roundNum * 10].push(sugStepId);
-
-        // 为每个 Q 创建节点
-        Object.keys(round.sug_details).forEach((qText, qIndex) => {
-          // 从q_list_1中查找对应的q获取分数和理由
-          // Round 0: 从q_list_1查找; Round 1+: 从input_q_list查找
-          let qData = {};
-          if (roundNum === 0) {
-            qData = round.q_list_1?.find(q => q.text === qText) || {};
-          } else {
-            // 从当前轮的input_q_list中查找
-            qData = round.input_q_list?.find(q => q.text === qText) || {};
-          }
-          const qId = `q_${qText}_r${roundNum}_${qIndex}`;
-          nodes[qId] = {
-            type: 'q',
-            query: '[Q] ' + qText,
-            level: roundNum * 10 + 2,
-            relevance_score: qData.score || 0,
-            evaluationReason: qData.reason || '',
-            strategy: 'Query',
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: sugStepId,
-            to: qId,
-            edge_type: 'step_to_q',
-            strategy: 'Query'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(qId);
-
-          // 为每个 Q 的 sug 创建节点
-          const sugs = round.sug_details[qText] || [];
-          const qScore = qData.score || 0;  // 获取父Q的得分
-
-          sugs.forEach((sug, sugIndex) => {
-            const sugScore = sug.score || 0;
-            // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色
-            const scoreColor = sugScore > qScore ? '#22c55e' : '#ef4444';
-
-            const sugId = `sug_${sug.text}_r${roundNum}_q${qIndex}_${sugIndex}`;
-            nodes[sugId] = {
-              type: 'sug',
-              query: '[SUG] ' + sug.text,
-              level: roundNum * 10 + 3,
-              relevance_score: sugScore,
-              evaluationReason: sug.reason || '',
-              strategy: '推荐词',
-              iteration: roundNum,
-              is_selected: true,
-              scoreColor: scoreColor,  // 新增:用于控制文字颜色
-              parentQScore: qScore     // 新增:保存父Q得分用于调试
-            };
-
-            edges.push({
-              from: qId,
-              to: sugId,
-              edge_type: 'q_to_sug',
-              strategy: '推荐词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(sugId);
-          });
-        });
-      }
-
-      // 步骤2: 筛选并执行搜索
-      const searchStepId = `step_search_r${roundNum}`;
-      const searchCountText = round.search_count > 0
-        ? `筛选${round.high_score_sug_count}个高分词,搜索${round.search_count}次,${round.total_posts}个帖子`
-        : `无高分推荐词,未执行搜索`;
-
-      nodes[searchStepId] = {
-        type: 'step',
-        query: `步骤2: 筛选并执行搜索 (${searchCountText})`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '筛选并执行搜索',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: searchStepId,
-        edge_type: 'round_to_step',
-        strategy: '搜索'
-      });
-
-      iterations[roundNum * 10].push(searchStepId);
-
-      // 只有在有搜索结果时才添加搜索词和帖子
-      // 优先使用 round.search_results(新格式),否则使用外部传入的 searchResults(兼容旧版本)
-      const roundSearchResults = round.search_results || searchResults;
-      if (round.search_count > 0 && roundSearchResults) {
-        if (Array.isArray(roundSearchResults)) {
-          roundSearchResults.forEach((search, searchIndex) => {
-            const searchWordId = `search_${search.text}_r${roundNum}_${searchIndex}`;
-            nodes[searchWordId] = {
-              type: 'search_word',
-              query: '[SEARCH] ' + search.text,
-              level: roundNum * 10 + 2,
-              relevance_score: search.score_with_o || 0,
-              strategy: '搜索词',
-              iteration: roundNum,
-              is_selected: true
-            };
-
-            edges.push({
-              from: searchStepId,
-              to: searchWordId,
-              edge_type: 'step_to_search_word',
-              strategy: '搜索词'
-            });
-
-            if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-            iterations[roundNum * 10 + 2].push(searchWordId);
-
-            // 添加帖子
-            if (search.post_list && search.post_list.length > 0) {
-              search.post_list.forEach((post, postIndex) => {
-                const postId = `post_${post.note_id}_${searchIndex}_${postIndex}`;
-
-                // 准备图片列表,将URL字符串转换为对象格式供轮播图使用
-                const imageList = (post.images || []).map(url => ({
-                  image_url: url
-                }));
-
-                nodes[postId] = {
-                  type: 'post',
-                  query: '[R] ' + post.title,
-                  level: roundNum * 10 + 3,
-                  relevance_score: 0,
-                  strategy: '帖子',
-                  iteration: roundNum,
-                  is_selected: true,
-                  note_id: post.note_id,
-                  note_url: post.note_url,
-                  body_text: post.body_text || '',
-                  images: post.images || [],
-                  image_list: imageList,
-                  interact_info: post.interact_info || {}
-                };
-
-                edges.push({
-                  from: searchWordId,
-                  to: postId,
-                  edge_type: 'search_word_to_post',
-                  strategy: '搜索结果'
-                });
-
-                if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-                iterations[roundNum * 10 + 3].push(postId);
-              });
-            }
-          });
-        }
-      }
-
-      // 步骤3: 加词生成新查询
-      if (round.add_word_details && Object.keys(round.add_word_details).length > 0) {
-        const addWordStepId = `step_add_r${roundNum}`;
-        const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0);
-
-        nodes[addWordStepId] = {
-          type: 'step',
-          query: `步骤3: 加词生成新查询 (${totalAddWords}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '加词生成新查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: addWordStepId,
-          edge_type: 'round_to_step',
-          strategy: '加词'
-        });
-
-        iterations[roundNum * 10].push(addWordStepId);
-
-        // 为每个 Seed 创建节点
-        Object.keys(round.add_word_details).forEach((seedText, seedIndex) => {
-          const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`;
-
-          // 查找seed的来源信息和分数 - 动态从正确的轮次查找
-          let seedInfo = {};
-          if (roundNum === 1) {
-            // Round 1:种子来自 Round 0 的 seed_list
-            const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
-            seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
-          } else {
-            // Round 2+:种子来自前一轮的 seed_list_next
-            const prevRound = rounds.find(r => r.round_num === roundNum - 1);
-            seedInfo = prevRound?.seed_list_next?.find(s => s.text === seedText) || {};
-          }
-          const fromType = seedInfo.from_type || seedInfo.from || 'unknown';
-
-          // 根据来源设置strategy
-          let strategy;
-          if (fromType === 'seg') {
-            strategy = '初始分词';
-          } else if (fromType === 'add') {
-            strategy = '加词';
-          } else if (fromType === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed';  // 默认灰色
-          }
-
-          nodes[seedId] = {
-            type: 'seed',
-            query: seedText,
-            level: roundNum * 10 + 2,
-            relevance_score: seedInfo.score || 0,  // 从seedInfo读取种子的得分
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true
-          };
-
-          edges.push({
-            from: addWordStepId,
-            to: seedId,
-            edge_type: 'step_to_seed',
-            strategy: 'Seed'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(seedId);
-
-          // 为每个 Seed 的组合词创建节点
-          const combinedWords = round.add_word_details[seedText] || [];
-          combinedWords.forEach((word, wordIndex) => {
-            const wordScore = word.score || 0;
-            const seedScore = word.seed_score || 0;
-            // 比较得分决定颜色:组合词得分 > 种子得分 → 绿色,否则 → 红色
-            const scoreColor = wordScore > seedScore ? '#22c55e' : '#ef4444';
-
-            const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`;
-            nodes[wordId] = {
-              type: 'add_word',
-              query: '[Q] ' + word.text,
-              level: roundNum * 10 + 3,
-              relevance_score: wordScore,
-              evaluationReason: word.reason || '',
-              strategy: '加词生成',
-              iteration: roundNum,
-              is_selected: true,
-              selected_word: word.selected_word,
-              seed_score: seedScore,  // 原始种子的得分
-              scoreColor: scoreColor  // 用于控制文字颜色
-            };
-
-            edges.push({
-              from: seedId,
-              to: wordId,
-              edge_type: 'seed_to_add_word',
-              strategy: '组合词'
-            });
-
-            if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-            iterations[roundNum * 10 + 3].push(wordId);
-          });
-        });
-      }
-
-      // 步骤4: 筛选推荐词进入下轮
-      const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || [];
-      if (filteredSugs.length > 0) {
-        const filterStepId = `step_filter_r${roundNum}`;
-        nodes[filterStepId] = {
-          type: 'step',
-          query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`,
-          level: roundNum * 10 + 1,
-          relevance_score: 0,
-          strategy: '筛选推荐词进入下轮',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: roundId,
-          to: filterStepId,
-          edge_type: 'round_to_step',
-          strategy: '筛选'
-        });
-
-        iterations[roundNum * 10].push(filterStepId);
-
-        // 添加筛选出的sug
-        filteredSugs.forEach((sug, sugIndex) => {
-          const sugScore = sug.score || 0;
-
-          // 尝试从sug_details中找到这个sug对应的父Q得分
-          let parentQScore = 0;
-          if (round.sug_details) {
-            for (const [qText, sugs] of Object.entries(round.sug_details)) {
-              const matchingSug = sugs.find(s => s.text === sug.text);
-              if (matchingSug) {
-                // 找到父Q的得分
-                let qData = {};
-                if (roundNum === 0) {
-                  qData = round.q_list_1?.find(q => q.text === qText) || {};
-                } else {
-                  qData = round.input_q_list?.find(q => q.text === qText) || {};
-                }
-                parentQScore = qData.score || 0;
-                break;
-              }
-            }
-          }
-
-          // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色
-          const scoreColor = sugScore > parentQScore ? '#22c55e' : '#ef4444';
-
-          const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`;
-          nodes[sugId] = {
-            type: 'filtered_sug',
-            query: '[SUG] ' + sug.text,
-            level: roundNum * 10 + 2,
-            relevance_score: sugScore,
-            strategy: '进入下轮',
-            iteration: roundNum,
-            is_selected: true,
-            scoreColor: scoreColor,       // 新增:用于控制文字颜色
-            parentQScore: parentQScore    // 新增:保存父Q得分用于调试
-          };
-
-          edges.push({
-            from: filterStepId,
-            to: sugId,
-            edge_type: 'step_to_filtered_sug',
-            strategy: '进入下轮'
-          });
-
-          if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-          iterations[roundNum * 10 + 2].push(sugId);
-        });
-      }
-
-      // 步骤5: 构建下一轮
-      const nextRoundStepId = `step_next_round_r${roundNum}`;
-      const nextQCount = round.output_q_list?.length || 0;
-      const nextSeedCount = round.seed_list_next_size || 0;
-
-      nodes[nextRoundStepId] = {
-        type: 'step',
-        query: `步骤5: 构建下一轮 (${nextQCount}个查询, ${nextSeedCount}个种子)`,
-        level: roundNum * 10 + 1,
-        relevance_score: 0,
-        strategy: '构建下一轮',
-        iteration: roundNum,
-        is_selected: true
-      };
-
-      edges.push({
-        from: roundId,
-        to: nextRoundStepId,
-        edge_type: 'round_to_step',
-        strategy: '构建下一轮'
-      });
-
-      iterations[roundNum * 10].push(nextRoundStepId);
-
-      // 5.1: 构建下轮查询
-      if (round.output_q_list && round.output_q_list.length > 0) {
-        const nextQStepId = `step_next_q_r${roundNum}`;
-        nodes[nextQStepId] = {
-          type: 'step',
-          query: `构建下轮查询 (${nextQCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮查询',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextQStepId,
-          edge_type: 'step_to_step',
-          strategy: '查询'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextQStepId);
-
-        // 添加下轮查询列表
-        round.output_q_list.forEach((q, qIndex) => {
-          const nextQId = `next_q_${q.text}_r${roundNum}_${qIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (q.from === 'seg') {
-            strategy = '初始分词';
-          } else if (q.from === 'add') {
-            strategy = '加词';
-          } else if (q.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Query'; // 默认
-          }
-
-          nodes[nextQId] = {
-            type: 'next_q',
-            query: '[Q] ' + q.text,
-            level: roundNum * 10 + 3,
-            relevance_score: q.score || 0,
-            evaluationReason: q.reason || '',
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: q.from
-          };
-
-          edges.push({
-            from: nextQStepId,
-            to: nextQId,
-            edge_type: 'step_to_next_q',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextQId);
-        });
-      }
-
-      // 5.2: 构建下轮种子(如果有数据的话)
-      if (nextSeedCount > 0 && round.seed_list_next) {
-        const nextSeedStepId = `step_next_seed_r${roundNum}`;
-        nodes[nextSeedStepId] = {
-          type: 'step',
-          query: `构建下轮种子 (${nextSeedCount}个)`,
-          level: roundNum * 10 + 2,
-          relevance_score: 0,
-          strategy: '下轮种子',
-          iteration: roundNum,
-          is_selected: true
-        };
-
-        edges.push({
-          from: nextRoundStepId,
-          to: nextSeedStepId,
-          edge_type: 'step_to_step',
-          strategy: '种子'
-        });
-
-        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
-        iterations[roundNum * 10 + 2].push(nextSeedStepId);
-
-        // 添加下轮种子列表
-        round.seed_list_next.forEach((seed, seedIndex) => {
-          const nextSeedId = `next_seed_${seed.text}_r${roundNum}_${seedIndex}`;
-
-          // 根据来源设置strategy
-          let strategy;
-          if (seed.from === 'seg') {
-            strategy = '初始分词';
-          } else if (seed.from === 'add') {
-            strategy = '加词';
-          } else if (seed.from === 'sug') {
-            strategy = '调用sug';
-          } else {
-            strategy = 'Seed'; // 默认
-          }
-
-          nodes[nextSeedId] = {
-            type: 'next_seed',
-            query: seed.text,
-            level: roundNum * 10 + 3,
-            relevance_score: seed.score || 0,
-            strategy: strategy,
-            iteration: roundNum,
-            is_selected: true,
-            from_source: seed.from
-          };
-
-          edges.push({
-            from: nextSeedStepId,
-            to: nextSeedId,
-            edge_type: 'step_to_next_seed',
-            strategy: strategy
-          });
-
-          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
-          iterations[roundNum * 10 + 3].push(nextSeedId);
-        });
-      }
-    }
-  });
-
-  return {
-    nodes,
-    edges,
-    iterations
-  };
-}
-
-/**
- * 简化版转换:专注于query和post的演化
- * - 合并所有query节点(不区分seg/sug/add_word)
- * - 合并相同的帖子节点
- * - 步骤信息放在边上
- * - 隐藏Round/Step节点
- */
-function convertV8ToGraphSimplified(runContext, searchResults) {
-  const mergedNodes = {};
-  const edges = [];
-  const iterations = {};
-
-  const o = runContext.o || '原始问题';
-  const rounds = runContext.rounds || [];
-
-  // 添加原始问题根节点
-  const rootId = 'root_o';
-  mergedNodes[rootId] = {
-    type: 'root',
-    query: o,
-    level: 0,
-    relevance_score: 1.0,
-    strategy: '原始问题',
-    iteration: 0,
-    is_selected: true,
-    occurrences: [{round: 0, role: 'root', score: 1.0}]
-  };
-  iterations[0] = [rootId];
-
-  // 用于记录节点之间的演化关系
-  const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
-  const postMap = {}; // {note_id: {...}}
-
-  // 第一遍:收集所有query和post
-  rounds.forEach((round, roundIndex) => {
-    const roundNum = round.round_num || roundIndex;
-
-    if (round.type === 'initialization') {
-      // Round 0: 收集分词结果
-      (round.q_list_1 || []).forEach(q => {
-        if (!queryEvolution[q.text]) {
-          queryEvolution[q.text] = {
-            occurrences: [],
-            parentTexts: new Set([o]), // 来自原始问题
-            childTexts: new Set()
-          };
-        }
-        queryEvolution[q.text].occurrences.push({
-          round: roundNum,
-          role: 'segmentation',
-          strategy: '分词',
-          score: q.score,
-          reason: q.reason
-        });
-      });
-    } else {
-      // Round 1+
-
-      // 收集sug_details (推荐词)
-      Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
-        sugs.forEach(sug => {
-          if (!queryEvolution[sug.text]) {
-            queryEvolution[sug.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[sug.text].occurrences.push({
-            round: roundNum,
-            role: 'sug',
-            strategy: '调用sug',
-            score: sug.score,
-            reason: sug.reason
-          });
-          queryEvolution[sug.text].parentTexts.add(parentText);
-          if (queryEvolution[parentText]) {
-            queryEvolution[parentText].childTexts.add(sug.text);
-          }
-        });
-      });
-
-      // 收集add_word_details (加词结果)
-      Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
-        words.forEach(word => {
-          if (!queryEvolution[word.text]) {
-            queryEvolution[word.text] = {
-              occurrences: [],
-              parentTexts: new Set(),
-              childTexts: new Set()
-            };
-          }
-          queryEvolution[word.text].occurrences.push({
-            round: roundNum,
-            role: 'add_word',
-            strategy: '加词',
-            score: word.score,
-            reason: word.reason,
-            selectedWord: word.selected_word,
-            seedScore: word.seed_score  // 添加原始种子的得分
-          });
-          queryEvolution[word.text].parentTexts.add(seedText);
-          if (queryEvolution[seedText]) {
-            queryEvolution[seedText].childTexts.add(word.text);
-          }
-        });
-      });
-
-      // 收集搜索结果和帖子
-      const roundSearchResults = round.search_results || searchResults;
-      if (roundSearchResults && Array.isArray(roundSearchResults)) {
-        roundSearchResults.forEach(search => {
-          const searchText = search.text;
-
-          // 标记这个query被用于搜索
-          if (queryEvolution[searchText]) {
-            queryEvolution[searchText].occurrences.push({
-              round: roundNum,
-              role: 'search',
-              strategy: '执行搜索',
-              score: search.score_with_o,
-              postCount: search.post_list ? search.post_list.length : 0
-            });
-          }
-
-          // 收集帖子
-          if (search.post_list && search.post_list.length > 0) {
-            search.post_list.forEach(post => {
-              if (!postMap[post.note_id]) {
-                postMap[post.note_id] = {
-                  ...post,
-                  foundByQueries: new Set(),
-                  foundInRounds: new Set()
-                };
-              }
-              postMap[post.note_id].foundByQueries.add(searchText);
-              postMap[post.note_id].foundInRounds.add(roundNum);
-
-              // 建立query到post的关系
-              if (!queryEvolution[searchText].posts) {
-                queryEvolution[searchText].posts = new Set();
-              }
-              queryEvolution[searchText].posts.add(post.note_id);
-            });
-          }
-        });
-      }
-    }
-  });
-
-  // 第二遍:创建合并后的节点
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    // 获取最新的分数
-    const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
-    const hasSearchResults = data.posts && data.posts.size > 0;
-
-    mergedNodes[nodeId] = {
-      type: 'query',
-      query: '[Q] ' + text,
-      level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
-      relevance_score: latestOccurrence.score || 0,
-      evaluationReason: latestOccurrence.reason || '',
-      strategy: data.occurrences.map(o => o.strategy).join(' + '),
-      primaryStrategy: latestOccurrence.strategy || '未知',  // 添加主要策略字段
-      iteration: Math.max(...data.occurrences.map(o => o.round), 0),
-      is_selected: true,
-      occurrences: data.occurrences,
-      hasSearchResults: hasSearchResults,
-      postCount: data.posts ? data.posts.size : 0,
-      selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || '',
-      seed_score: data.occurrences.find(o => o.seedScore)?.seedScore  // 添加原始种子的得分
-    };
-
-    // 添加到对应的轮次
-    const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
-    const iterKey = maxRound * 10 + 2;
-    if (!iterations[iterKey]) iterations[iterKey] = [];
-    iterations[iterKey].push(nodeId);
-  });
-
-  // 创建帖子节点
-  Object.entries(postMap).forEach(([noteId, post]) => {
-    const postId = `post_${noteId}`;
-
-    const imageList = (post.images || []).map(url => ({
-      image_url: url
-    }));
-
-    mergedNodes[postId] = {
-      type: 'post',
-      query: '[R] ' + post.title,
-      level: 100, // 放在最后
-      relevance_score: 0,
-      strategy: '帖子',
-      iteration: Math.max(...Array.from(post.foundInRounds)),
-      is_selected: true,
-      note_id: post.note_id,
-      note_url: post.note_url,
-      body_text: post.body_text || '',
-      images: post.images || [],
-      image_list: imageList,
-      interact_info: post.interact_info || {},
-      foundByQueries: Array.from(post.foundByQueries),
-      foundInRounds: Array.from(post.foundInRounds)
-    };
-
-    if (!iterations[100]) iterations[100] = [];
-    iterations[100].push(postId);
-  });
-
-  // 第三遍:创建边
-  // 1. 原始问题 -> 分词结果
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-    const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
-
-    if (segOccurrence && data.parentTexts.has(o)) {
-      edges.push({
-        from: rootId,
-        to: nodeId,
-        edge_type: 'segmentation',
-        strategy: '分词',
-        label: '分词',
-        round: 0
-      });
-    }
-  });
-
-  // 2. Query演化关系
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    data.parentTexts.forEach(parentText => {
-      if (parentText === o) return; // 跳过原始问题(已处理)
-
-      const parentNodeId = `query_${parentText}`;
-      if (!mergedNodes[parentNodeId]) return;
-
-      // 找到这个演化的策略和轮次
-      const occurrence = data.occurrences.find(o =>
-        o.role === 'sug' || o.role === 'add_word'
-      );
-
-      edges.push({
-        from: parentNodeId,
-        to: nodeId,
-        edge_type: occurrence?.role || 'evolution',
-        strategy: occurrence?.strategy || '演化',
-        label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
-        round: occurrence?.round || 0
-      });
-    });
-  });
-
-  // 3. Query -> Post (搜索关系)
-  Object.entries(queryEvolution).forEach(([text, data]) => {
-    const nodeId = `query_${text}`;
-
-    if (data.posts && data.posts.size > 0) {
-      const searchOccurrence = data.occurrences.find(o => o.role === 'search');
-
-      data.posts.forEach(noteId => {
-        const postId = `post_${noteId}`;
-        edges.push({
-          from: nodeId,
-          to: postId,
-          edge_type: 'search',
-          strategy: '搜索',
-          label: `搜索 (${data.posts.size}个帖子)`,
-          round: searchOccurrence?.round || 0
-        });
-      });
-    }
-  });
-
-  return {
-    nodes: mergedNodes,
-    edges,
-    iterations
-  };
-}
-
-module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };

+ 0 - 2471
visualization/sug_v6_1_2_8/index.js

@@ -1,2471 +0,0 @@
-#!/usr/bin/env node
-
-const fs = require('fs');
-const path = require('path');
-const { build } = require('esbuild');
-const { convertV8ToGraph } = require('./convert_v8_to_graph');
-const { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v3');
-
-// 读取命令行参数
-const args = process.argv.slice(2);
-if (args.length === 0) {
-  console.error('Usage: node index.js <path-to-run_context.json> [output.html] [--simplified]');
-  process.exit(1);
-}
-
-const inputFile = args[0];
-const outputFile = args[1] || 'query_graph_output.html';
-const useSimplified = args.includes('--simplified');
-
-// 读取输入数据
-const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
-
-// 检测数据格式并转换
-let data;
-if (inputData.rounds && inputData.o) {
-  // v6.1.2.8 格式,需要转换
-  console.log('✨ 检测到 v6.1.2.8 格式,正在转换为图结构...');
-
-  // 尝试读取 search_results.json(兼容旧版本)
-  let searchResults = null;
-  const searchResultsPath = path.join(path.dirname(inputFile), 'search_results.json');
-  if (fs.existsSync(searchResultsPath)) {
-    console.log('📄 读取外部搜索结果数据(兼容模式)...');
-    searchResults = JSON.parse(fs.readFileSync(searchResultsPath, 'utf-8'));
-  } else {
-    console.log('✅ 使用 run_context.json 中的内嵌搜索结果');
-  }
-
-  // 选择转换函数
-  let graphData;
-  let fullData = null; // 用于目录的完整数据
-
-  if (useSimplified) {
-    console.log('🎨 使用简化视图(合并query节点)');
-    // 生成简化版用于画布
-    graphData = convertV8ToGraphSimplified(inputData, searchResults);
-    // 生成完整版用于目录
-    const fullGraphData = convertV8ToGraphV2(inputData, searchResults);
-    fullData = {
-      nodes: fullGraphData.nodes,
-      edges: fullGraphData.edges,
-      iterations: fullGraphData.iterations
-    };
-    console.log(`✅ 简化版: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
-    console.log(`📋 完整版(用于目录): ${Object.keys(fullData.nodes).length} 个节点`);
-  } else {
-    console.log('📊 使用详细视图(完整流程)');
-    graphData = convertV8ToGraphV2(inputData, searchResults);
-    console.log(`✅ 转换完成: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
-  }
-
-  data = {
-    nodes: graphData.nodes,
-    edges: graphData.edges,
-    iterations: graphData.iterations,
-    fullData: fullData // 传递完整数据
-  };
-} else if (inputData.nodes && inputData.edges) {
-  // v6.1.2.5 格式,直接使用
-  console.log('✨ 检测到 v6.1.2.5 格式,直接使用');
-  data = inputData;
-} else {
-  console.error('❌ 无法识别的数据格式');
-  process.exit(1);
-}
-
-// 创建临时 React 组件文件
-const reactComponentPath = path.join(__dirname, 'temp_flow_component_v2.jsx');
-const reactComponent = `
-import React, { useState, useCallback, useMemo, useEffect } from 'react';
-import { createRoot } from 'react-dom/client';
-import {
-  ReactFlow,
-  Controls,
-  Background,
-  useNodesState,
-  useEdgesState,
-  Handle,
-  Position,
-  useReactFlow,
-  ReactFlowProvider,
-} from '@xyflow/react';
-import '@xyflow/react/dist/style.css';
-
-const data = ${JSON.stringify(data, null, 2)};
-
-// 根据节点类型获取边框颜色
-function getNodeTypeColor(type) {
-  const typeColors = {
-    'root': '#6b21a8',        // 紫色 - 根节点
-    'round': '#7c3aed',       // 深紫 - Round节点
-    'step': '#f59e0b',        // 橙色 - 步骤节点
-    'seg': '#10b981',         // 绿色 - 分词
-    'q': '#3b82f6',           // 蓝色 - Query
-    'sug': '#06b6d4',         // 青色 - Sug建议词
-    'seed': '#84cc16',        // 黄绿 - Seed
-    'add_word': '#22c55e',    // 绿色 - 加词生成
-    'search_word': '#8b5cf6', // 紫色 - 搜索词
-    'post': '#ec4899',        // 粉色 - 帖子
-    'filtered_sug': '#14b8a6',// 青绿 - 筛选的sug
-    'next_q': '#2563eb',      // 深蓝 - 下轮查询
-    'next_seed': '#65a30d',   // 深黄绿 - 下轮种子
-    'search': '#8b5cf6',      // 深紫 - 搜索(兼容旧版)
-    'operation': '#f59e0b',   // 橙色 - 操作节点(兼容旧版)
-    'query': '#3b82f6',       // 蓝色 - 查询(兼容旧版)
-    'note': '#ec4899',        // 粉色 - 帖子(兼容旧版)
-  };
-  return typeColors[type] || '#9ca3af';
-}
-
-// 查询节点组件 - 卡片样式
-function QueryNode({ id, data, sourcePosition, targetPosition }) {
-  // 所有节点默认展开
-  const expanded = true;
-
-  // 获取节点类型颜色
-  const typeColor = getNodeTypeColor(data.nodeType || 'query');
-
-  return (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: typeColor, width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '12px',
-          borderRadius: '8px',
-          border: data.isHighlighted ? \`3px solid \${typeColor}\` :
-                  data.isCollapsed ? \`2px solid \${typeColor}\` :
-                  data.isSelected === false ? '2px dashed #d1d5db' :
-                  \`2px solid \${typeColor}\`,
-          background: data.isHighlighted ? '#eef2ff' :
-                      data.isSelected === false ? '#f9fafb' : 'white',
-          minWidth: '200px',
-          maxWidth: '280px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(102, 126, 234, 0.25), 0 4px 16px rgba(102, 126, 234, 0.4)' :
-                     data.isCollapsed ? '0 4px 12px rgba(102, 126, 234, 0.15)' :
-                     data.level === 0 ? '0 4px 12px rgba(139, 92, 246, 0.15)' : '0 2px 6px rgba(0, 0, 0, 0.06)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-          position: 'relative',
-          opacity: data.isSelected === false ? 0.6 : 1,
-        }}
-      >
-        {/* 折叠当前节点按钮 - 左边 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            left: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: '#f59e0b',
-            color: 'white',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            e.stopPropagation();
-            if (data.onHideSelf) {
-              data.onHideSelf();
-            }
-          }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.background = '#d97706';
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.background = '#f59e0b';
-          }}
-          title="隐藏当前节点"
-        >
-          ×
-        </div>
-
-        {/* 聚焦按钮 - 右上角 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            right: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: data.isFocused ? '#10b981' : '#e5e7eb',
-            color: data.isFocused ? 'white' : '#6b7280',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            e.stopPropagation();
-            if (data.onFocus) {
-              data.onFocus();
-            }
-          }}
-          onMouseEnter={(e) => {
-            if (!data.isFocused) {
-              e.currentTarget.style.background = '#d1d5db';
-            }
-          }}
-          onMouseLeave={(e) => {
-            if (!data.isFocused) {
-              e.currentTarget.style.background = '#e5e7eb';
-            }
-          }}
-          title={data.isFocused ? '取消聚焦' : '聚焦到此节点'}
-        >
-          🎯
-        </div>
-
-        {/* 折叠/展开子节点按钮 - 右边第二个位置 */}
-        {data.hasChildren && (
-          <div
-            style={{
-              position: 'absolute',
-              top: '6px',
-              right: '30px',
-              width: '20px',
-              height: '20px',
-              borderRadius: '50%',
-              background: data.isCollapsed ? '#667eea' : '#e5e7eb',
-              color: data.isCollapsed ? 'white' : '#6b7280',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              fontSize: '11px',
-              fontWeight: 'bold',
-              cursor: 'pointer',
-              transition: 'all 0.2s ease',
-              zIndex: 10,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              data.onToggleCollapse();
-            }}
-            title={data.isCollapsed ? '展开子节点' : '折叠子节点'}
-          >
-            {data.isCollapsed ? '+' : '−'}
-          </div>
-        )}
-
-        {/* 卡片内容 */}
-        <div>
-          {/* 标题行 */}
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px', paddingLeft: '24px', paddingRight: data.hasChildren ? '54px' : '28px' }}>
-            <div style={{ flex: 1 }}>
-              <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
-                <div style={{
-                  fontSize: '13px',
-                  fontWeight: data.level === 0 ? '700' : '600',
-                  color: data.level === 0 ? '#6b21a8' : '#1f2937',
-                  lineHeight: '1.3',
-                  flex: 1,
-                }}>
-                  {data.title}
-                </div>
-                {data.isSelected === false && (
-                  <div style={{
-                    fontSize: '9px',
-                    padding: '1px 4px',
-                    borderRadius: '3px',
-                    background: '#fee2e2',
-                    color: '#991b1b',
-                    fontWeight: '500',
-                    flexShrink: 0,
-                  }}>
-                    未选中
-                  </div>
-                )}
-              </div>
-            </div>
-          </div>
-
-        {/* 展开的详细信息 - 始终显示 */}
-        <div style={{ fontSize: '11px', lineHeight: 1.4 }}>
-            <div style={{ display: 'flex', gap: '4px', marginBottom: '6px', flexWrap: 'wrap' }}>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#eff6ff',
-                color: '#3b82f6',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                Lv.{data.level}
-              </span>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#f0fdf4',
-                color: '#16a34a',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                {data.score}
-              </span>
-              {data.strategy && data.strategy !== 'root' && (
-                <span style={{
-                  display: 'inline-block',
-                  padding: '1px 6px',
-                  borderRadius: '10px',
-                  background: '#fef3c7',
-                  color: '#92400e',
-                  fontSize: '10px',
-                  fontWeight: '500',
-                }}>
-                  {data.strategy}
-                </span>
-              )}
-            </div>
-
-            {data.parent && (
-              <div style={{ color: '#6b7280', fontSize: '10px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #f3f4f6' }}>
-                <strong>Parent:</strong> {data.parent}
-              </div>
-            )}
-            {data.selectedWord && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                color: '#6b7280',
-                lineHeight: '1.5',
-              }}>
-                <strong style={{ color: '#4b5563' }}>选择词:</strong>
-                <span style={{ marginLeft: '4px', color: '#3b82f6', fontWeight: '500' }}>{data.selectedWord}</span>
-                {data.seed_score !== undefined && (
-                  <div style={{ marginTop: '4px' }}>
-                    <strong style={{ color: '#4b5563' }}>种子得分:</strong>
-                    <span style={{ marginLeft: '4px', color: '#16a34a', fontWeight: '500' }}>
-                      {typeof data.seed_score === 'number' ? data.seed_score.toFixed(2) : data.seed_score}
-                    </span>
-                  </div>
-                )}
-              </div>
-            )}
-            {data.evaluationReason && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                color: '#6b7280',
-                lineHeight: '1.5',
-              }}>
-                <strong style={{ color: '#4b5563' }}>评估:</strong>
-                <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-              </div>
-            )}
-            {data.occurrences && data.occurrences.length > 1 && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                color: '#6b7280',
-              }}>
-                <strong style={{ color: '#4b5563' }}>演化历史 ({data.occurrences.length}次):</strong>
-                <div style={{ marginTop: '4px' }}>
-                  {data.occurrences.map((occ, idx) => (
-                    <div key={idx} style={{ marginTop: '2px', paddingLeft: '8px' }}>
-                      <span style={{ color: '#3b82f6', fontWeight: '500' }}>R{occ.round}</span>
-                      {' · '}
-                      <span>{occ.strategy}</span>
-                      {occ.score !== undefined && (
-                        <span style={{ color: '#16a34a', marginLeft: '4px' }}>
-                          ({typeof occ.score === 'number' ? occ.score.toFixed(2) : occ.score})
-                        </span>
-                      )}
-                    </div>
-                  ))}
-                </div>
-              </div>
-            )}
-            {data.hasSearchResults && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                background: '#fef3c7',
-                padding: '4px 6px',
-                borderRadius: '4px',
-                color: '#92400e',
-                fontWeight: '500',
-              }}>
-                🔍 找到 {data.postCount} 个帖子
-              </div>
-            )}
-          </div>
-        </div>
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#667eea', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-// 笔记节点组件 - 卡片样式,带轮播图
-function NoteNode({ id, data, sourcePosition, targetPosition }) {
-  const [currentImageIndex, setCurrentImageIndex] = useState(0);
-  const expanded = true;
-  const hasImages = data.imageList && data.imageList.length > 0;
-
-  const nextImage = (e) => {
-    e.stopPropagation();
-    if (hasImages) {
-      setCurrentImageIndex((prev) => (prev + 1) % data.imageList.length);
-    }
-  };
-
-  const prevImage = (e) => {
-    e.stopPropagation();
-    if (hasImages) {
-      setCurrentImageIndex((prev) => (prev - 1 + data.imageList.length) % data.imageList.length);
-    }
-  };
-
-  return (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '14px',
-          borderRadius: '20px',
-          border: data.isHighlighted ? '3px solid #ec4899' : '2px solid #fce7f3',
-          background: data.isHighlighted ? '#eef2ff' : 'white',
-          minWidth: '220px',
-          maxWidth: '300px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(236, 72, 153, 0.25), 0 4px 16px rgba(236, 72, 153, 0.4)' : '0 4px 12px rgba(236, 72, 153, 0.15)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-        }}
-      >
-        {/* 笔记标题 */}
-        <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '8px' }}>
-          <div style={{ flex: 1 }}>
-            <div style={{
-              fontSize: '13px',
-              fontWeight: '600',
-              color: '#831843',
-              lineHeight: '1.4',
-              marginBottom: '4px',
-            }}>
-              {data.title}
-            </div>
-          </div>
-        </div>
-
-        {/* 轮播图 */}
-        {hasImages && (
-          <div style={{
-            position: 'relative',
-            marginBottom: '8px',
-            borderRadius: '12px',
-            overflow: 'hidden',
-          }}>
-            <img
-              src={data.imageList[currentImageIndex].image_url}
-              alt={\`Image \${currentImageIndex + 1}\`}
-              style={{
-                width: '100%',
-                height: '160px',
-                objectFit: 'cover',
-                display: 'block',
-              }}
-              onError={(e) => {
-                e.target.style.display = 'none';
-              }}
-            />
-            {data.imageList.length > 1 && (
-              <>
-                {/* 左右切换按钮 */}
-                <button
-                  onClick={prevImage}
-                  style={{
-                    position: 'absolute',
-                    left: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ‹
-                </button>
-                <button
-                  onClick={nextImage}
-                  style={{
-                    position: 'absolute',
-                    right: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ›
-                </button>
-                {/* 图片计数 */}
-                <div style={{
-                  position: 'absolute',
-                  bottom: '4px',
-                  right: '4px',
-                  background: 'rgba(0, 0, 0, 0.6)',
-                  color: 'white',
-                  padding: '2px 6px',
-                  borderRadius: '10px',
-                  fontSize: '10px',
-                }}>
-                  {currentImageIndex + 1}/{data.imageList.length}
-                </div>
-              </>
-            )}
-          </div>
-        )}
-
-        {/* 互动数据 */}
-        {data.interact_info && (
-          <div style={{
-            display: 'flex',
-            gap: '8px',
-            marginBottom: '8px',
-            flexWrap: 'wrap',
-            fontSize: '11px',
-            color: '#9f1239',
-          }}>
-            {data.interact_info.liked_count > 0 && (
-              <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
-                ❤️ {data.interact_info.liked_count}
-              </span>
-            )}
-            {data.interact_info.collected_count > 0 && (
-              <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
-                ⭐ {data.interact_info.collected_count}
-              </span>
-            )}
-            {data.interact_info.comment_count > 0 && (
-              <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
-                💬 {data.interact_info.comment_count}
-              </span>
-            )}
-            {data.interact_info.shared_count > 0 && (
-              <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
-                🔗 {data.interact_info.shared_count}
-              </span>
-            )}
-          </div>
-        )}
-
-        {/* 被哪些query找到 */}
-        {data.foundByQueries && data.foundByQueries.length > 0 && (
-          <div style={{
-            marginBottom: '8px',
-            padding: '6px 8px',
-            background: '#f0fdf4',
-            borderRadius: '6px',
-            fontSize: '10px',
-          }}>
-            <strong style={{ color: '#16a34a' }}>🔍 被找到:</strong>
-            <div style={{ marginTop: '4px', display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
-              {data.foundByQueries.map((query, idx) => (
-                <span key={idx} style={{
-                  display: 'inline-block',
-                  padding: '2px 6px',
-                  background: '#dcfce7',
-                  color: '#166534',
-                  borderRadius: '4px',
-                  fontSize: '9px',
-                }}>
-                  {query}
-                </span>
-              ))}
-            </div>
-            {data.foundInRounds && data.foundInRounds.length > 0 && (
-              <div style={{ marginTop: '4px', color: '#6b7280' }}>
-                出现在: Round {data.foundInRounds.join(', ')}
-              </div>
-            )}
-          </div>
-        )}
-
-        {/* 标签 */}
-        {(data.matchLevel || data.score) && (
-          <div style={{ display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' }}>
-            {data.matchLevel && (
-              <span style={{
-                display: 'inline-block',
-                padding: '2px 8px',
-                borderRadius: '12px',
-                background: '#fff1f2',
-                color: '#be123c',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                {data.matchLevel}
-              </span>
-            )}
-            {data.score && (
-              <span style={{
-                display: 'inline-block',
-                padding: '2px 8px',
-                borderRadius: '12px',
-                background: '#fff7ed',
-                color: '#c2410c',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                Score: {data.score}
-              </span>
-            )}
-          </div>
-        )}
-
-        {/* 描述 */}
-        {expanded && data.description && (
-          <div style={{
-            fontSize: '11px',
-            color: '#9f1239',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            {data.description}
-          </div>
-        )}
-
-        {/* 评估理由 */}
-        {expanded && data.evaluationReason && (
-          <div style={{
-            fontSize: '10px',
-            color: '#831843',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            marginTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            <strong style={{ color: '#9f1239' }}>评估:</strong>
-            <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-          </div>
-        )}
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-const nodeTypes = {
-  query: QueryNode,
-  note: NoteNode,
-};
-
-// 根据 score 获取颜色
-function getScoreColor(score) {
-  if (score >= 0.7) return '#10b981'; // 绿色 - 高分
-  if (score >= 0.4) return '#f59e0b'; // 橙色 - 中分
-  return '#ef4444'; // 红色 - 低分
-}
-
-// 截断文本,保留头尾,中间显示省略号
-function truncateMiddle(text, maxLength = 20) {
-  if (!text || text.length <= maxLength) return text;
-  const headLength = Math.ceil(maxLength * 0.4);
-  const tailLength = Math.floor(maxLength * 0.4);
-  const head = text.substring(0, headLength);
-  const tail = text.substring(text.length - tailLength);
-  return \`\${head}...\${tail}\`;
-}
-
-// 根据策略获取颜色
-// 智能提取主要策略的辅助函数
-function getPrimaryStrategy(nodeData) {
-  // 优先级1: 使用 primaryStrategy 字段
-  if (nodeData.primaryStrategy) {
-    return nodeData.primaryStrategy;
-  }
-
-  // 优先级2: 从 occurrences 数组中获取最新的策略
-  if (nodeData.occurrences && Array.isArray(nodeData.occurrences) && nodeData.occurrences.length > 0) {
-    const latestOccurrence = nodeData.occurrences[nodeData.occurrences.length - 1];
-    if (latestOccurrence && latestOccurrence.strategy) {
-      return latestOccurrence.strategy;
-    }
-  }
-
-  // 优先级3: 拆分组合策略字符串,取第一个
-  if (nodeData.strategy && typeof nodeData.strategy === 'string') {
-    const strategies = nodeData.strategy.split(' + ');
-    if (strategies.length > 0 && strategies[0]) {
-      return strategies[0].trim();
-    }
-  }
-
-  // 默认返回原始strategy或未知
-  return nodeData.strategy || '未知';
-}
-
-function getStrategyColor(strategy) {
-  const strategyColors = {
-    '初始分词': '#10b981',
-    '调用sug': '#06b6d4',
-    '同义改写': '#f59e0b',
-    '加词': '#3b82f6',
-    '抽象改写': '#8b5cf6',
-    '基于部分匹配改进': '#ec4899',
-    '结果分支-抽象改写': '#a855f7',
-    '结果分支-同义改写': '#fb923c',
-    // v6.1.2.8 新增策略
-    '原始问题': '#6b21a8',
-    '来自分词': '#10b981',
-    '加词生成': '#ef4444',
-    '建议词': '#06b6d4',
-    '执行搜索': '#8b5cf6',
-    // 添加简化版本的策略映射
-    '分词': '#10b981',
-    '推荐词': '#06b6d4',
-  };
-  return strategyColors[strategy] || '#9ca3af';
-}
-
-// 树节点组件
-function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) {
-  const hasChildren = children && children.length > 0;
-  const score = node.data.score ? parseFloat(node.data.score) : 0;
-  const strategy = getPrimaryStrategy(node.data);  // 使用智能提取函数
-  const strategyColor = getStrategyColor(strategy);
-  const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
-
-  // 计算字体颜色:根据分数提升幅度判断
-  let fontColor = '#374151'; // 默认颜色
-  if (node.type === 'note') {
-    fontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
-  } else if (node.data.seed_score !== undefined) {
-    const parentScore = parseFloat(node.data.seed_score);
-    const gain = score - parentScore;
-    fontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
-  } else if (node.data.isSelected === false) {
-    fontColor = '#ef4444';
-  }
-
-  return (
-    <div style={{ marginLeft: level * 12 + 'px' }}>
-      <div
-        style={{
-          padding: '6px 8px',
-          borderRadius: '4px',
-          cursor: 'pointer',
-          background: 'transparent',
-          border: isSelected ? '1px solid #3b82f6' : '1px solid transparent',
-          display: 'flex',
-          alignItems: 'center',
-          gap: '6px',
-          transition: 'all 0.2s ease',
-          position: 'relative',
-          overflow: 'visible',
-        }}
-        onMouseEnter={(e) => {
-          if (!isSelected) e.currentTarget.style.background = '#f9fafb';
-        }}
-        onMouseLeave={(e) => {
-          if (!isSelected) e.currentTarget.style.background = 'transparent';
-        }}
-      >
-        {/* 策略类型竖线 */}
-        <div style={{
-          width: '3px',
-          height: '20px',
-          background: strategyColor,
-          borderRadius: '2px',
-          flexShrink: 0,
-          position: 'relative',
-          zIndex: 1,
-        }} />
-
-        {hasChildren && (
-          <span
-            style={{
-              fontSize: '10px',
-              color: '#6b7280',
-              cursor: 'pointer',
-              width: '16px',
-              textAlign: 'center',
-              position: 'relative',
-              zIndex: 1,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              onToggle();
-            }}
-          >
-            {isCollapsed ? '▶' : '▼'}
-          </span>
-        )}
-        {!hasChildren && <span style={{ width: '16px', position: 'relative', zIndex: 1 }}></span>}
-
-        <div
-          style={{
-            flex: 1,
-            fontSize: '12px',
-            color: '#374151',
-            position: 'relative',
-            zIndex: 1,
-            minWidth: 0,
-            display: 'flex',
-            flexDirection: 'column',
-            gap: '4px',
-          }}
-          onClick={onSelect}
-        >
-          <div style={{
-            display: 'flex',
-            alignItems: 'center',
-            gap: '8px',
-          }}>
-            <div style={{
-              fontWeight: level === 0 ? '600' : '400',
-              maxWidth: '180px',
-              flex: 1,
-              minWidth: 0,
-              color: node.data.scoreColor || fontColor,
-            }}
-            title={node.data.title || node.id}
-            >
-              {truncateMiddle(node.data.title || node.id, 18)}
-            </div>
-
-            {/* 分数显示 - 步骤和轮次节点不显示分数 */}
-            {nodeActualType !== 'step' && nodeActualType !== 'round' && (
-              <span style={{
-                fontSize: '11px',
-                color: '#6b7280',
-                fontWeight: '500',
-                flexShrink: 0,
-              }}>
-                {score.toFixed(2)}
-              </span>
-            )}
-          </div>
-
-          {/* 分数下划线 - 步骤和轮次节点不显示 */}
-          {nodeActualType !== 'step' && nodeActualType !== 'round' && (
-            <div style={{
-              width: (score * 100) + '%',
-              height: '2px',
-              background: getScoreColor(score),
-              borderRadius: '1px',
-            }} />
-          )}
-        </div>
-      </div>
-
-      {hasChildren && !isCollapsed && (
-        <div>
-          {children}
-        </div>
-      )}
-    </div>
-  );
-}
-
-// 使用 dagre 自动布局
-function getLayoutedElements(nodes, edges, direction = 'LR') {
-  console.log('🎯 Starting layout with dagre...');
-  console.log('Input:', nodes.length, 'nodes,', edges.length, 'edges');
-
-  // 检查 dagre 是否加载
-  if (typeof window === 'undefined' || typeof window.dagre === 'undefined') {
-    console.warn('⚠️ Dagre not loaded, using fallback layout');
-    // 降级到简单布局
-    const levelGroups = {};
-    nodes.forEach(node => {
-      const level = node.data.level || 0;
-      if (!levelGroups[level]) levelGroups[level] = [];
-      levelGroups[level].push(node);
-    });
-
-    Object.entries(levelGroups).forEach(([level, nodeList]) => {
-      const x = parseInt(level) * 350;
-      nodeList.forEach((node, index) => {
-        node.position = { x, y: index * 150 };
-        node.targetPosition = 'left';
-        node.sourcePosition = 'right';
-      });
-    });
-
-    return { nodes, edges };
-  }
-
-  try {
-    const dagreGraph = new window.dagre.graphlib.Graph();
-    dagreGraph.setDefaultEdgeLabel(() => ({}));
-
-    const isHorizontal = direction === 'LR';
-    dagreGraph.setGraph({
-      rankdir: direction,
-      nodesep: 120,   // 垂直间距 - 增加以避免节点重叠
-      ranksep: 280,  // 水平间距 - 增加以容纳更宽的节点
-    });
-
-    // 添加节点 - 根据节点类型设置不同的尺寸
-    nodes.forEach((node) => {
-      let nodeWidth = 280;
-      let nodeHeight = 180;
-
-      // note 节点有轮播图,需要更大的空间
-      if (node.type === 'note') {
-        nodeWidth = 320;
-        nodeHeight = 350;  // 增加高度以容纳轮播图
-      }
-
-      dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
-    });
-
-    // 添加边
-    edges.forEach((edge) => {
-      dagreGraph.setEdge(edge.source, edge.target);
-    });
-
-    // 计算布局
-    window.dagre.layout(dagreGraph);
-    console.log('✅ Dagre layout completed');
-
-    // 更新节点位置和 handle 位置
-    nodes.forEach((node) => {
-      const nodeWithPosition = dagreGraph.node(node.id);
-
-      if (!nodeWithPosition) {
-        console.warn('Node position not found for:', node.id);
-        return;
-      }
-
-      node.targetPosition = isHorizontal ? 'left' : 'top';
-      node.sourcePosition = isHorizontal ? 'right' : 'bottom';
-
-      // 根据节点类型获取尺寸
-      let nodeWidth = 280;
-      let nodeHeight = 180;
-      if (node.type === 'note') {
-        nodeWidth = 320;
-        nodeHeight = 350;
-      }
-
-      // 将 dagre 的中心点位置转换为 React Flow 的左上角位置
-      node.position = {
-        x: nodeWithPosition.x - nodeWidth / 2,
-        y: nodeWithPosition.y - nodeHeight / 2,
-      };
-    });
-
-    console.log('✅ Layout completed, sample node:', nodes[0]);
-    return { nodes, edges };
-  } catch (error) {
-    console.error('❌ Error in dagre layout:', error);
-    console.error('Error details:', error.message, error.stack);
-
-    // 降级处理
-    console.log('Using fallback layout...');
-    const levelGroups = {};
-    nodes.forEach(node => {
-      const level = node.data.level || 0;
-      if (!levelGroups[level]) levelGroups[level] = [];
-      levelGroups[level].push(node);
-    });
-
-    Object.entries(levelGroups).forEach(([level, nodeList]) => {
-      const x = parseInt(level) * 350;
-      nodeList.forEach((node, index) => {
-        node.position = { x, y: index * 150 };
-        node.targetPosition = 'left';
-        node.sourcePosition = 'right';
-      });
-    });
-
-    return { nodes, edges };
-  }
-}
-
-function transformData(data) {
-  const nodes = [];
-  const edges = [];
-
-  const originalIdToCanvasId = {}; // 原始ID -> 画布ID的映射
-  const canvasIdToNodeData = {}; // 避免重复创建相同的节点
-
-  // 创建节点
-  Object.entries(data.nodes).forEach(([originalId, node]) => {
-    // 统一处理所有类型的节点
-    const nodeType = node.type || 'query';
-
-    // 直接使用originalId作为canvasId,避免冲突
-    const canvasId = originalId;
-
-    originalIdToCanvasId[originalId] = canvasId;
-
-    // 如果这个 canvasId 还没有创建过节点,则创建
-    if (!canvasIdToNodeData[canvasId]) {
-      canvasIdToNodeData[canvasId] = true;
-
-      // 根据节点类型创建不同的数据结构
-      if (nodeType === 'note' || nodeType === 'post') {
-        nodes.push({
-          id: canvasId,
-          originalId: originalId,
-          type: 'note',
-          data: {
-            title: node.query || node.title || '帖子',
-            matchLevel: node.match_level,
-            score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
-            description: node.body_text || node.desc || '',
-            isSelected: node.is_selected !== undefined ? node.is_selected : true,
-            imageList: node.image_list || [],
-            noteUrl: node.note_url || '',
-            evaluationReason: node.evaluationReason || node.evaluation_reason || '',
-            interact_info: node.interact_info || {},
-            nodeType: nodeType,
-          },
-          position: { x: 0, y: 0 },
-        });
-      } else {
-        // query, seg, q, search, root 等节点
-        let displayTitle = node.query || originalId;
-
-        nodes.push({
-          id: canvasId,
-          originalId: originalId,
-          type: 'query', // 使用 query 组件渲染所有非note节点
-          data: {
-            title: displayTitle,
-            level: node.level || 0,
-            score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
-            strategy: node.strategy || '',
-            parent: node.parent_query || '',
-            isSelected: node.is_selected !== undefined ? node.is_selected : true,
-            evaluationReason: node.evaluationReason || node.evaluation_reason || '',
-            nodeType: nodeType, // 传递实际节点类型用于样式
-            searchCount: node.search_count, // search 节点特有
-            totalPosts: node.total_posts, // search 节点特有
-            selectedWord: node.selected_word || '', // 加词节点特有 - 显示选择的词
-            scoreColor: node.scoreColor || null,        // SUG节点的颜色标识
-            parentQScore: node.parentQScore || 0,       // 父Q得分(用于调试)
-          },
-          position: { x: 0, y: 0 },
-        });
-      }
-    }
-  });
-
-  // 创建边 - 使用虚线样式,映射到画布ID
-  data.edges.forEach((edge, index) => {
-    const edgeColors = {
-      '初始分词': '#10b981',
-      '调用sug': '#06b6d4',
-      '同义改写': '#f59e0b',
-      '加词': '#3b82f6',
-      '抽象改写': '#8b5cf6',
-      '基于部分匹配改进': '#ec4899',
-      '结果分支-抽象改写': '#a855f7',
-      '结果分支-同义改写': '#fb923c',
-      'query_to_note': '#ec4899',
-    };
-
-    const color = edgeColors[edge.strategy] || edgeColors[edge.edge_type] || '#d1d5db';
-    const isNoteEdge = edge.edge_type === 'query_to_note';
-
-    edges.push({
-      id: \`edge-\${index}\`,
-      source: originalIdToCanvasId[edge.from], // 使用画布ID
-      target: originalIdToCanvasId[edge.to],   // 使用画布ID
-      type: 'simplebezier', // 使用简单贝塞尔曲线
-      animated: isNoteEdge,
-      style: {
-        stroke: color,
-        strokeWidth: isNoteEdge ? 2.5 : 2,
-        strokeDasharray: isNoteEdge ? '5,5' : '8,4',
-      },
-      markerEnd: {
-        type: 'arrowclosed',
-        color: color,
-        width: 20,
-        height: 20,
-      },
-    });
-  });
-
-  // 使用 dagre 自动计算布局 - 从左到右
-  return getLayoutedElements(nodes, edges, 'LR');
-}
-
-function FlowContent() {
-  // 画布使用简化数据
-  const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
-    console.log('🔍 Transforming data for canvas...');
-    const result = transformData(data);
-    console.log('✅ Canvas data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
-    return result;
-  }, []);
-
-  // 目录使用完整数据(如果存在)
-  const { nodes: fullNodes, edges: fullEdges } = useMemo(() => {
-    if (data.fullData) {
-      console.log('🔍 Transforming full data for tree directory...');
-      const result = transformData(data.fullData);
-      console.log('✅ Directory data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
-      return result;
-    }
-    // 如果没有 fullData,使用简化数据
-    return { nodes: initialNodes, edges: initialEdges };
-  }, [initialNodes, initialEdges]);
-
-  // 初始化:找出所有有子节点的节点,默认折叠(画布节点)
-  const initialCollapsedNodes = useMemo(() => {
-    const nodesWithChildren = new Set();
-    initialEdges.forEach(edge => {
-      nodesWithChildren.add(edge.source);
-    });
-    // 排除根节点(level 0),让根节点默认展开
-    const rootNode = initialNodes.find(n => n.data.level === 0);
-    if (rootNode) {
-      nodesWithChildren.delete(rootNode.id);
-    }
-    return nodesWithChildren;
-  }, [initialNodes, initialEdges]);
-
-  // 树节点的折叠状态需要在树构建后初始化
-  const [collapsedNodes, setCollapsedNodes] = useState(() => initialCollapsedNodes);
-  const [collapsedTreeNodes, setCollapsedTreeNodes] = useState(new Set());
-  const [selectedNodeId, setSelectedNodeId] = useState(null);
-  const [hiddenNodes, setHiddenNodes] = useState(new Set()); // 用户手动隐藏的节点
-  const [focusMode, setFocusMode] = useState(false); // 全局聚焦模式,默认关闭
-  const [focusedNodeId, setFocusedNodeId] = useState(null); // 单独聚焦的节点ID
-  const [sidebarWidth, setSidebarWidth] = useState(400); // 左侧目录宽度
-  const [isResizing, setIsResizing] = useState(false); // 是否正在拖拽调整宽度
-
-  // 拖拽调整侧边栏宽度的处理逻辑
-  const handleMouseDown = useCallback(() => {
-    setIsResizing(true);
-  }, []);
-
-  useEffect(() => {
-    if (!isResizing) return;
-
-    const handleMouseMove = (e) => {
-      const newWidth = e.clientX;
-      // 限制宽度范围:300px - 700px
-      if (newWidth >= 300 && newWidth <= 700) {
-        setSidebarWidth(newWidth);
-      }
-    };
-
-    const handleMouseUp = () => {
-      setIsResizing(false);
-    };
-
-    document.addEventListener('mousemove', handleMouseMove);
-    document.addEventListener('mouseup', handleMouseUp);
-
-    return () => {
-      document.removeEventListener('mousemove', handleMouseMove);
-      document.removeEventListener('mouseup', handleMouseUp);
-    };
-  }, [isResizing]);
-
-  // 获取 React Flow 实例以控制画布
-  const { setCenter, fitView } = useReactFlow();
-
-  // 获取某个节点的所有后代节点ID
-  const getDescendants = useCallback((nodeId) => {
-    const descendants = new Set();
-    const queue = [nodeId];
-
-    while (queue.length > 0) {
-      const current = queue.shift();
-      initialEdges.forEach(edge => {
-        if (edge.source === current && !descendants.has(edge.target)) {
-          descendants.add(edge.target);
-          queue.push(edge.target);
-        }
-      });
-    }
-
-    return descendants;
-  }, [initialEdges]);
-
-  // 获取直接父节点
-  const getDirectParents = useCallback((nodeId) => {
-    const parents = [];
-    initialEdges.forEach(edge => {
-      if (edge.target === nodeId) {
-        parents.push(edge.source);
-      }
-    });
-    return parents;
-  }, [initialEdges]);
-
-  // 获取直接子节点
-  const getDirectChildren = useCallback((nodeId) => {
-    const children = [];
-    initialEdges.forEach(edge => {
-      if (edge.source === nodeId) {
-        children.push(edge.target);
-      }
-    });
-    return children;
-  }, [initialEdges]);
-
-  // 切换节点折叠状态
-  const toggleNodeCollapse = useCallback((nodeId) => {
-    setCollapsedNodes(prev => {
-      const newSet = new Set(prev);
-      const descendants = getDescendants(nodeId);
-
-      if (newSet.has(nodeId)) {
-        // 展开:移除此节点,但保持其他折叠的节点
-        newSet.delete(nodeId);
-      } else {
-        // 折叠:添加此节点
-        newSet.add(nodeId);
-      }
-
-      return newSet;
-    });
-  }, [getDescendants]);
-
-  // 过滤可见的节点和边,并重新计算布局
-  const { nodes, edges } = useMemo(() => {
-    const nodesToHide = new Set();
-
-    // 判断使用哪个节点ID进行聚焦:优先使用单独聚焦的节点,否则使用全局聚焦模式的选中节点
-    const effectiveFocusNodeId = focusedNodeId || (focusMode ? selectedNodeId : null);
-
-    // 聚焦模式:只显示聚焦节点、其父节点和直接子节点
-    if (effectiveFocusNodeId) {
-      const visibleInFocus = new Set([effectiveFocusNodeId]);
-
-      // 添加所有父节点
-      initialEdges.forEach(edge => {
-        if (edge.target === effectiveFocusNodeId) {
-          visibleInFocus.add(edge.source);
-        }
-      });
-
-      // 添加所有直接子节点
-      initialEdges.forEach(edge => {
-        if (edge.source === effectiveFocusNodeId) {
-          visibleInFocus.add(edge.target);
-        }
-      });
-
-      // 隐藏不在聚焦范围内的节点
-      initialNodes.forEach(node => {
-        if (!visibleInFocus.has(node.id)) {
-          nodesToHide.add(node.id);
-        }
-      });
-    } else {
-      // 非聚焦模式:使用原有的折叠逻辑
-      // 收集所有被折叠节点的后代
-      collapsedNodes.forEach(collapsedId => {
-        const descendants = getDescendants(collapsedId);
-        descendants.forEach(id => nodesToHide.add(id));
-      });
-    }
-
-    // 添加用户手动隐藏的节点
-    hiddenNodes.forEach(id => nodesToHide.add(id));
-
-    const visibleNodes = initialNodes
-      .filter(node => !nodesToHide.has(node.id))
-      .map(node => ({
-        ...node,
-        data: {
-          ...node.data,
-          isCollapsed: collapsedNodes.has(node.id),
-          hasChildren: initialEdges.some(e => e.source === node.id),
-          onToggleCollapse: () => toggleNodeCollapse(node.id),
-          onHideSelf: () => {
-            setHiddenNodes(prev => {
-              const newSet = new Set(prev);
-              newSet.add(node.id);
-              return newSet;
-            });
-          },
-          onFocus: () => {
-            // 切换聚焦状态
-            if (focusedNodeId === node.id) {
-              setFocusedNodeId(null); // 如果已经聚焦,则取消聚焦
-            } else {
-              // 先取消之前的聚焦,然后聚焦到当前节点
-              setFocusedNodeId(node.id);
-
-              // 延迟聚焦视图到该节点
-              setTimeout(() => {
-                fitView({
-                  nodes: [{ id: node.id }],
-                  duration: 800,
-                  padding: 0.3,
-                });
-              }, 100);
-            }
-          },
-          isFocused: focusedNodeId === node.id,
-          isHighlighted: selectedNodeId === node.id,
-        }
-      }));
-
-    const visibleEdges = initialEdges.filter(
-      edge => !nodesToHide.has(edge.source) && !nodesToHide.has(edge.target)
-    );
-
-    // 重新计算布局 - 只对可见节点
-    if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') {
-      try {
-        const dagreGraph = new window.dagre.graphlib.Graph();
-        dagreGraph.setDefaultEdgeLabel(() => ({}));
-
-        dagreGraph.setGraph({
-          rankdir: 'LR',
-          nodesep: 120,   // 垂直间距 - 增加以避免节点重叠
-          ranksep: 280,  // 水平间距 - 增加以容纳更宽的节点
-        });
-
-        visibleNodes.forEach((node) => {
-          let nodeWidth = 280;
-          let nodeHeight = 180;
-
-          // note 节点有轮播图,需要更大的空间
-          if (node.type === 'note') {
-            nodeWidth = 320;
-            nodeHeight = 350;
-          }
-
-          dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
-        });
-
-        visibleEdges.forEach((edge) => {
-          dagreGraph.setEdge(edge.source, edge.target);
-        });
-
-        window.dagre.layout(dagreGraph);
-
-        visibleNodes.forEach((node) => {
-          const nodeWithPosition = dagreGraph.node(node.id);
-          if (nodeWithPosition) {
-            // 根据节点类型获取对应的尺寸
-            let nodeWidth = 280;
-            let nodeHeight = 180;
-            if (node.type === 'note') {
-              nodeWidth = 320;
-              nodeHeight = 350;
-            }
-
-            node.position = {
-              x: nodeWithPosition.x - nodeWidth / 2,
-              y: nodeWithPosition.y - nodeHeight / 2,
-            };
-            node.targetPosition = 'left';
-            node.sourcePosition = 'right';
-          }
-        });
-
-        console.log('✅ Dynamic layout recalculated for', visibleNodes.length, 'visible nodes');
-      } catch (error) {
-        console.error('❌ Error in dynamic layout:', error);
-      }
-    }
-
-    return { nodes: visibleNodes, edges: visibleEdges };
-  }, [initialNodes, initialEdges, collapsedNodes, hiddenNodes, focusMode, focusedNodeId, getDescendants, toggleNodeCollapse, selectedNodeId]);
-
-  // 构建树形结构 - 允许一个节点有多个父节点
-  // 为目录构建树(使用完整数据)
-  const buildTree = useCallback(() => {
-    // 使用完整数据构建目录树
-    const nodeMap = new Map();
-    fullNodes.forEach(node => {
-      nodeMap.set(node.id, node);
-    });
-
-    // 为每个节点创建树节点的副本(允许多次出现)
-    const createTreeNode = (nodeId, pathKey) => {
-      const node = nodeMap.get(nodeId);
-      if (!node) return null;
-
-      return {
-        ...node,
-        treeKey: pathKey, // 唯一的树路径key,用于React key
-        children: []
-      };
-    };
-
-    // 构建父子关系映射:记录每个节点的所有父节点,去重边
-    const parentToChildren = new Map();
-    const childToParents = new Map();
-
-    fullEdges.forEach(edge => {
-      // 记录父->子关系(去重:同一个父节点到同一个子节点只记录一次)
-      if (!parentToChildren.has(edge.source)) {
-        parentToChildren.set(edge.source, []);
-      }
-      const children = parentToChildren.get(edge.source);
-      if (!children.includes(edge.target)) {
-        children.push(edge.target);
-      }
-
-      // 记录子->父关系(用于判断是否有多个父节点,也去重)
-      if (!childToParents.has(edge.target)) {
-        childToParents.set(edge.target, []);
-      }
-      const parents = childToParents.get(edge.target);
-      if (!parents.includes(edge.source)) {
-        parents.push(edge.source);
-      }
-    });
-
-    // 递归构建树
-    const buildSubtree = (nodeId, pathKey, visitedInPath) => {
-      // 避免循环引用:如果当前路径中已经访问过这个节点,跳过
-      if (visitedInPath.has(nodeId)) {
-        return null;
-      }
-
-      const treeNode = createTreeNode(nodeId, pathKey);
-      if (!treeNode) return null;
-
-      const newVisitedInPath = new Set(visitedInPath);
-      newVisitedInPath.add(nodeId);
-
-      const children = parentToChildren.get(nodeId) || [];
-      treeNode.children = children
-        .map((childId, index) => buildSubtree(childId, pathKey + '-' + childId + '-' + index, newVisitedInPath))
-        .filter(child => child !== null);
-
-      return treeNode;
-    };
-
-    // 找出所有根节点(没有入边的节点)
-    const hasParent = new Set();
-    fullEdges.forEach(edge => {
-      hasParent.add(edge.target);
-    });
-
-    const roots = [];
-    fullNodes.forEach((node, index) => {
-      if (!hasParent.has(node.id)) {
-        const treeNode = buildSubtree(node.id, 'root-' + node.id + '-' + index, new Set());
-        if (treeNode) roots.push(treeNode);
-      }
-    });
-
-    return roots;
-  }, [fullNodes, fullEdges]);
-
-  const treeRoots = useMemo(() => buildTree(), [buildTree]);
-
-  // 生成树形文本结构(使用完整数据)
-  const generateTreeText = useCallback(() => {
-    const lines = [];
-
-    // 递归生成树形文本
-    const traverse = (nodes, prefix = '', isLast = true, depth = 0) => {
-      nodes.forEach((node, index) => {
-        const isLastNode = index === nodes.length - 1;
-        const nodeData = fullNodes.find(n => n.id === node.id)?.data || {};
-        const nodeType = nodeData.nodeType || node.data?.nodeType || 'unknown';
-        const title = nodeData.title || node.data?.title || node.id;
-
-        // 优先从node.data获取score,然后从nodeData获取
-        let score = null;
-        if (node.data?.score !== undefined && node.data?.score !== null) {
-          score = node.data.score;
-        } else if (node.data?.relevance_score !== undefined && node.data?.relevance_score !== null) {
-          score = node.data.relevance_score;
-        } else if (nodeData.score !== undefined && nodeData.score !== null) {
-          score = nodeData.score;
-        } else if (nodeData.relevance_score !== undefined && nodeData.relevance_score !== null) {
-          score = nodeData.relevance_score;
-        }
-
-        const strategy = nodeData.strategy || node.data?.strategy || '';
-
-        // 构建当前行 - score可能是数字或字符串,step/round节点不显示分数
-        const connector = isLastNode ? '└─' : '├─';
-        let scoreText = '';
-        if (nodeType !== 'step' && nodeType !== 'round' && score !== null && score !== undefined) {
-          // score可能已经是字符串格式(如 "0.05"),也可能是数字
-          const scoreStr = typeof score === 'number' ? score.toFixed(2) : score;
-          scoreText = \` (分数: \${scoreStr})\`;
-        }
-        const strategyText = strategy ? \` [\${strategy}]\` : '';
-
-        lines.push(\`\${prefix}\${connector} \${title}\${scoreText}\${strategyText}\`);
-
-        // 递归处理子节点
-        if (node.children && node.children.length > 0) {
-          const childPrefix = prefix + (isLastNode ? '   ' : '│  ');
-          traverse(node.children, childPrefix, isLastNode, depth + 1);
-        }
-      });
-    };
-
-    // 添加标题
-    const rootNode = fullNodes.find(n => n.data?.level === 0);
-    if (rootNode) {
-      lines.push(\`📊 查询扩展树形结构\`);
-      lines.push(\`原始问题: \${rootNode.data.title || rootNode.data.query}\`);
-      lines.push('');
-    }
-
-    traverse(treeRoots);
-
-    return lines.join('\\n');
-  }, [treeRoots, fullNodes]);
-
-  // 复制树形结构到剪贴板
-  const copyTreeToClipboard = useCallback(async () => {
-    try {
-      const treeText = generateTreeText();
-      await navigator.clipboard.writeText(treeText);
-      alert('✅ 树形结构已复制到剪贴板!');
-    } catch (err) {
-      console.error('复制失败:', err);
-      alert('❌ 复制失败,请手动复制');
-    }
-  }, [generateTreeText]);
-
-  // 初始化树节点折叠状态
-  useEffect(() => {
-    const getAllTreeKeys = (nodes) => {
-      const keys = new Set();
-      const traverse = (node) => {
-        if (node.children && node.children.length > 0) {
-          // 排除根节点
-          if (node.data.level !== 0) {
-            keys.add(node.treeKey);
-          }
-          node.children.forEach(traverse);
-        }
-      };
-      nodes.forEach(traverse);
-      return keys;
-    };
-
-    setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
-  }, [treeRoots]);
-
-  // 映射完整节点ID到画布简化节点ID
-  const mapTreeNodeToCanvasNode = useCallback((treeNodeId) => {
-    // 如果是简化模式,需要映射
-    if (data.fullData) {
-      // 从完整数据中找到节点
-      const fullNode = fullNodes.find(n => n.id === treeNodeId);
-      if (!fullNode) return treeNodeId;
-
-      // 根据节点类型和文本找到画布上的简化节点
-      const nodeText = fullNode.data.title || fullNode.data.query;
-      const nodeType = fullNode.data.nodeType || fullNode.type;
-
-      // Query类节点:找 query_xxx
-      if (['q', 'seg', 'sug', 'add_word', 'query'].includes(nodeType)) {
-        const canvasNode = initialNodes.find(n =>
-          (n.data.title === nodeText || n.data.query === nodeText) &&
-          ['query'].includes(n.type)
-        );
-        return canvasNode ? canvasNode.id : treeNodeId;
-      }
-
-      // Post节点:按note_id查找
-      if (nodeType === 'post' || nodeType === 'note') {
-        const noteId = fullNode.data.note_id;
-        if (noteId) {
-          const canvasNode = initialNodes.find(n => n.data.note_id === noteId);
-          return canvasNode ? canvasNode.id : treeNodeId;
-        }
-      }
-
-      // 其他节点类型(Round/Step等):直接返回
-      return treeNodeId;
-    }
-
-    // 非简化模式,直接返回
-    return treeNodeId;
-  }, [data.fullData, fullNodes, initialNodes]);
-
-  const renderTree = useCallback((treeNodes, level = 0) => {
-    return treeNodes.map(node => {
-      // 使用 treeKey 来区分树中的不同实例
-      const isCollapsed = collapsedTreeNodes.has(node.treeKey);
-      const isSelected = selectedNodeId === node.id;
-
-      return (
-        <TreeNode
-          key={node.treeKey}
-          node={node}
-          level={level}
-          isCollapsed={isCollapsed}
-          isSelected={isSelected}
-          onToggle={() => {
-            setCollapsedTreeNodes(prev => {
-              const newSet = new Set(prev);
-              if (newSet.has(node.treeKey)) {
-                newSet.delete(node.treeKey);
-              } else {
-                newSet.add(node.treeKey);
-              }
-              return newSet;
-            });
-          }}
-          onSelect={() => {
-            // 将目录节点ID映射到画布节点ID
-            const treeNodeId = node.id;
-            const canvasNodeId = mapTreeNodeToCanvasNode(treeNodeId);
-
-            // 检查画布上是否存在这个节点
-            const canvasNodeExists = initialNodes.some(n => n.id === canvasNodeId);
-            if (!canvasNodeExists) {
-              console.warn(\`节点 \${canvasNodeId} 在画布上不存在(可能被简化了)\`);
-              return;
-            }
-
-            const nodeId = canvasNodeId;
-
-            // 展开所有祖先节点
-            const ancestorIds = [nodeId];
-            const findAncestors = (id) => {
-              initialEdges.forEach(edge => {
-                if (edge.target === id && !ancestorIds.includes(edge.source)) {
-                  ancestorIds.push(edge.source);
-                  findAncestors(edge.source);
-                }
-              });
-            };
-            findAncestors(nodeId);
-
-            // 如果节点或其祖先被隐藏,先恢复它们
-            setHiddenNodes(prev => {
-              const newSet = new Set(prev);
-              ancestorIds.forEach(id => newSet.delete(id));
-              return newSet;
-            });
-
-            setSelectedNodeId(nodeId);
-
-            // 获取选中节点的直接子节点
-            const childrenIds = [];
-            initialEdges.forEach(edge => {
-              if (edge.source === nodeId) {
-                childrenIds.push(edge.target);
-              }
-            });
-
-            setCollapsedNodes(prev => {
-              const newSet = new Set(prev);
-              // 展开所有祖先节点
-              ancestorIds.forEach(id => newSet.delete(id));
-              // 展开选中节点本身
-              newSet.delete(nodeId);
-              // 展开选中节点的直接子节点
-              childrenIds.forEach(id => newSet.delete(id));
-              return newSet;
-            });
-
-            // 延迟聚焦,等待节点展开和布局重新计算
-            setTimeout(() => {
-              fitView({
-                nodes: [{ id: nodeId }],
-                duration: 800,
-                padding: 0.3,
-              });
-            }, 300);
-          }}
-        >
-          {node.children && node.children.length > 0 && renderTree(node.children, level + 1)}
-        </TreeNode>
-      );
-    });
-  }, [collapsedTreeNodes, selectedNodeId, nodes, setCenter, initialEdges, setCollapsedNodes, fitView, mapTreeNodeToCanvasNode, initialNodes, setHiddenNodes]);
-
-  console.log('📊 Rendering with', nodes.length, 'visible nodes and', edges.length, 'visible edges');
-
-  if (nodes.length === 0) {
-    return (
-      <div style={{ padding: 50, color: 'red', fontSize: 20 }}>
-        ERROR: No nodes to display!
-      </div>
-    );
-  }
-
-  return (
-    <div style={{ width: '100vw', height: '100vh', background: '#f9fafb', display: 'flex', flexDirection: 'column' }}>
-      {/* 顶部面包屑导航栏 */}
-      <div style={{
-        minHeight: '48px',
-        maxHeight: '120px',
-        background: 'white',
-        borderBottom: '1px solid #e5e7eb',
-        display: 'flex',
-        alignItems: 'flex-start',
-        padding: '12px 24px',
-        zIndex: 1000,
-        boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
-        flexShrink: 0,
-        overflowY: 'auto',
-      }}>
-        <div style={{ width: '100%' }}>
-          {selectedNodeId ? (
-            <div style={{ fontSize: '12px', color: '#6b7280' }}>
-              {/* 面包屑导航 - 显示所有路径 */}
-              {(() => {
-                const selectedNode = nodes.find(n => n.id === selectedNodeId);
-                if (!selectedNode) return null;
-
-                // 找到所有从根节点到当前节点的路径
-                const findAllPaths = (targetId) => {
-                  const paths = [];
-
-                  const buildPath = (nodeId, currentPath) => {
-                    const node = initialNodes.find(n => n.id === nodeId);
-                    if (!node) return;
-
-                    const newPath = [node, ...currentPath];
-
-                    // 找到所有父节点
-                    const parents = initialEdges.filter(e => e.target === nodeId).map(e => e.source);
-
-                    if (parents.length === 0) {
-                      // 到达根节点
-                      paths.push(newPath);
-                    } else {
-                      // 递归处理所有父节点
-                      parents.forEach(parentId => {
-                        buildPath(parentId, newPath);
-                      });
-                    }
-                  };
-
-                  buildPath(targetId, []);
-                  return paths;
-                };
-
-                const allPaths = findAllPaths(selectedNodeId);
-
-                // 去重:将路径转换为字符串进行比较
-                const uniquePaths = [];
-                const pathStrings = new Set();
-                allPaths.forEach(path => {
-                  const pathString = path.map(n => n.id).join('->');
-                  if (!pathStrings.has(pathString)) {
-                    pathStrings.add(pathString);
-                    uniquePaths.push(path);
-                  }
-                });
-
-                return (
-                  <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
-                    {uniquePaths.map((path, pathIndex) => (
-                      <div key={pathIndex} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
-                        {pathIndex > 0 && <span style={{ color: '#d1d5db', marginRight: '4px' }}>或</span>}
-                        {path.map((node, index) => {
-                          // 获取节点的 score、strategy 和 isSelected
-                          const nodeScore = node.data.score ? parseFloat(node.data.score) : 0;
-                          const nodeStrategy = getPrimaryStrategy(node.data);  // 使用智能提取函数
-                          const strategyColor = getStrategyColor(nodeStrategy);
-                          const nodeIsSelected = node.type === 'note' ? node.data.matchLevel !== 'unsatisfied' : node.data.isSelected !== false;
-                          const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
-
-                          // 计算路径节点字体颜色:根据分数提升幅度判断
-                          let pathFontColor = '#374151'; // 默认颜色
-                          if (node.type === 'note') {
-                            pathFontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
-                          } else if (node.data.seed_score !== undefined) {
-                            const parentScore = parseFloat(node.data.seed_score);
-                            const gain = nodeScore - parentScore;
-                            pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
-                          } else if (index > 0) {
-                            const prevNode = path[index - 1];
-                            const prevScore = prevNode.data.score ? parseFloat(prevNode.data.score) : 0;
-                            const gain = nodeScore - prevScore;
-                            pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
-                          } else if (node.data.isSelected === false) {
-                            pathFontColor = '#ef4444';
-                          }
-
-                          return (
-                          <React.Fragment key={node.id + '-' + index}>
-                            <span
-                              onClick={() => {
-                                const nodeId = node.id;
-
-                                // 找到所有祖先节点
-                                const ancestorIds = [nodeId];
-                                const findAncestors = (id) => {
-                                  initialEdges.forEach(edge => {
-                                    if (edge.target === id && !ancestorIds.includes(edge.source)) {
-                                      ancestorIds.push(edge.source);
-                                      findAncestors(edge.source);
-                                    }
-                                  });
-                                };
-                                findAncestors(nodeId);
-
-                                // 如果节点或其祖先被隐藏,先恢复它们
-                                setHiddenNodes(prev => {
-                                  const newSet = new Set(prev);
-                                  ancestorIds.forEach(id => newSet.delete(id));
-                                  return newSet;
-                                });
-
-                                // 展开目录树中到达该节点的路径
-                                // 需要找到所有包含该节点的树路径的 treeKey,并展开它们的父节点
-                                setCollapsedTreeNodes(prev => {
-                                  const newSet = new Set(prev);
-                                  // 清空所有折叠状态,让目录树完全展开到选中节点
-                                  // 这样可以确保选中节点在目录中可见
-                                  return new Set();
-                                });
-
-                                setSelectedNodeId(nodeId);
-                                setTimeout(() => {
-                                  fitView({
-                                    nodes: [{ id: nodeId }],
-                                    duration: 800,
-                                    padding: 0.3,
-                                  });
-                                }, 100);
-                              }}
-                              style={{
-                                padding: '6px 8px',
-                                borderRadius: '4px',
-                                background: 'white',
-                                border: index === path.length - 1 ? '2px solid #3b82f6' : '1px solid #d1d5db',
-                                color: '#374151',
-                                fontWeight: index === path.length - 1 ? '600' : '400',
-                                width: '180px',
-                                cursor: 'pointer',
-                                transition: 'all 0.2s ease',
-                                position: 'relative',
-                                display: 'inline-flex',
-                                flexDirection: 'column',
-                                gap: '4px',
-                              }}
-                              onMouseEnter={(e) => {
-                                e.currentTarget.style.opacity = '0.8';
-                              }}
-                              onMouseLeave={(e) => {
-                                e.currentTarget.style.opacity = '1';
-                              }}
-                              title={\`\${node.data.title || node.id} (Score: \${nodeScore.toFixed(2)}, Strategy: \${nodeStrategy}, Selected: \${nodeIsSelected})\`}
-                            >
-                              {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
-                              <div style={{
-                                display: 'flex',
-                                alignItems: 'center',
-                                gap: '6px',
-                              }}>
-                                {/* 策略类型竖线 */}
-                                <div style={{
-                                  width: '3px',
-                                  height: '16px',
-                                  background: strategyColor,
-                                  borderRadius: '2px',
-                                  flexShrink: 0,
-                                }} />
-
-                                {/* 节点文字 */}
-                                <span style={{
-                                  flex: 1,
-                                  fontSize: '12px',
-                                  color: pathFontColor,
-                                }}>
-                                  {truncateMiddle(node.data.title || node.id, 18)}
-                                </span>
-
-                                {/* 分数显示 - 步骤和轮次节点不显示分数 */}
-                                {nodeActualType !== 'step' && nodeActualType !== 'round' && (
-                                  <span style={{
-                                    fontSize: '10px',
-                                    color: '#6b7280',
-                                    fontWeight: '500',
-                                    flexShrink: 0,
-                                  }}>
-                                    {nodeScore.toFixed(2)}
-                                  </span>
-                                )}
-                              </div>
-
-                              {/* 分数下划线 - 步骤和轮次节点不显示 */}
-                              {nodeActualType !== 'step' && nodeActualType !== 'round' && (
-                                <div style={{
-                                  width: (nodeScore * 100) + '%',
-                                  height: '2px',
-                                  background: getScoreColor(nodeScore),
-                                  borderRadius: '1px',
-                                  marginLeft: '9px',
-                                }} />
-                              )}
-                            </span>
-                            {index < path.length - 1 && <span style={{ color: '#9ca3af' }}>›</span>}
-                          </React.Fragment>
-                        )})}
-                      </div>
-                    ))}
-                  </div>
-                );
-              })()}
-            </div>
-          ) : (
-            <div style={{ fontSize: '13px', color: '#9ca3af', textAlign: 'center' }}>
-              选择一个节点查看路径
-            </div>
-          )}
-        </div>
-      </div>
-
-      {/* 主内容区:目录 + 画布 */}
-      <div style={{
-        display: 'flex',
-        flex: 1,
-        overflow: 'hidden',
-        cursor: isResizing ? 'col-resize' : 'default',
-        userSelect: isResizing ? 'none' : 'auto',
-      }}>
-        {/* 左侧目录树 */}
-        <div style={{
-          width: \`\${sidebarWidth}px\`,
-          background: 'white',
-          borderRight: '1px solid #e5e7eb',
-          display: 'flex',
-          flexDirection: 'column',
-          flexShrink: 0,
-        }}>
-          <div style={{
-            padding: '12px 16px',
-            borderBottom: '1px solid #e5e7eb',
-            display: 'flex',
-            justifyContent: 'space-between',
-            alignItems: 'center',
-          }}>
-            <span style={{
-              fontWeight: '600',
-              fontSize: '14px',
-              color: '#111827',
-            }}>
-              节点目录
-            </span>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedTreeNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="展开全部节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const getAllTreeKeys = (nodes) => {
-                    const keys = new Set();
-                    const traverse = (node) => {
-                      if (node.children && node.children.length > 0) {
-                        keys.add(node.treeKey);
-                        node.children.forEach(traverse);
-                      }
-                    };
-                    nodes.forEach(traverse);
-                    return keys;
-                  };
-                  setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="折叠全部节点"
-              >
-                全部折叠
-              </button>
-              <button
-                onClick={copyTreeToClipboard}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #3b82f6',
-                  background: '#3b82f6',
-                  color: 'white',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  transition: 'all 0.2s',
-                }}
-                onMouseEnter={(e) => e.currentTarget.style.background = '#2563eb'}
-                onMouseLeave={(e) => e.currentTarget.style.background = '#3b82f6'}
-                title="复制树形结构为文本格式"
-              >
-                📋 复制树形结构
-              </button>
-            </div>
-          </div>
-          <div style={{
-            flex: 1,
-            overflowX: 'auto',
-            overflowY: 'auto',
-            padding: '8px',
-          }}>
-            <div style={{ minWidth: 'fit-content' }}>
-              {renderTree(treeRoots)}
-            </div>
-          </div>
-        </div>
-
-        {/* 可拖拽的分隔条 */}
-        <div
-          onMouseDown={handleMouseDown}
-          style={{
-            width: '4px',
-            cursor: 'col-resize',
-            background: isResizing ? '#3b82f6' : 'transparent',
-            transition: isResizing ? 'none' : 'background 0.2s',
-            flexShrink: 0,
-            position: 'relative',
-          }}
-          onMouseEnter={(e) => e.currentTarget.style.background = '#e5e7eb'}
-          onMouseLeave={(e) => {
-            if (!isResizing) e.currentTarget.style.background = 'transparent';
-          }}
-        >
-          {/* 拖拽提示线 */}
-          <div style={{
-            position: 'absolute',
-            top: '50%',
-            left: '50%',
-            transform: 'translate(-50%, -50%)',
-            width: '1px',
-            height: '40px',
-            background: '#9ca3af',
-            opacity: isResizing ? 1 : 0.3,
-          }} />
-        </div>
-
-        {/* 画布区域 */}
-        <div style={{ flex: 1, position: 'relative' }}>
-
-          {/* 右侧图例 */}
-          <div style={{
-            position: 'absolute',
-            top: '20px',
-            right: '20px',
-            background: 'white',
-            padding: '16px',
-            borderRadius: '12px',
-            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
-            zIndex: 1000,
-            maxWidth: '260px',
-            border: '1px solid #e5e7eb',
-          }}>
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
-          <h3 style={{ fontSize: '14px', fontWeight: '600', color: '#111827', margin: 0 }}>图例</h3>
-          <button
-            onClick={() => setFocusMode(!focusMode)}
-            style={{
-              fontSize: '11px',
-              padding: '4px 8px',
-              borderRadius: '4px',
-              border: '1px solid',
-              borderColor: focusMode ? '#3b82f6' : '#d1d5db',
-              background: focusMode ? '#3b82f6' : 'white',
-              color: focusMode ? 'white' : '#6b7280',
-              cursor: 'pointer',
-              fontWeight: '500',
-            }}
-            title={focusMode ? '关闭聚焦模式' : '开启聚焦模式'}
-          >
-            {focusMode ? '🎯 聚焦' : '📊 全图'}
-          </button>
-        </div>
-
-        <div style={{ fontSize: '12px' }}>
-          {/* 画布节点展开/折叠控制 */}
-          <div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>节点控制</div>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="展开画布中所有节点的子节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const allNodeIds = new Set(initialNodes.map(n => n.id));
-                  setCollapsedNodes(allNodeIds);
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="折叠画布中所有节点的子节点"
-              >
-                全部折叠
-              </button>
-            </div>
-          </div>
-
-          <div style={{ paddingTop: '12px', borderTop: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>策略类型</div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#10b981', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>初始分词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#06b6d4', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>调用sug</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#f59e0b', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>同义改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#3b82f6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>加词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#8b5cf6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#ec4899', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>基于部分匹配改进</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#a855f7', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#fb923c', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-同义改写</span>
-            </div>
-          </div>
-
-          <div style={{
-            marginTop: '12px',
-            paddingTop: '12px',
-            borderTop: '1px solid #f3f4f6',
-            fontSize: '11px',
-            color: '#9ca3af',
-            lineHeight: '1.5',
-          }}>
-            💡 点击节点左上角 × 隐藏节点
-          </div>
-
-          {/* 隐藏节点列表 - 在图例内部 */}
-          {hiddenNodes.size > 0 && (
-            <div style={{
-              marginTop: '12px',
-              paddingTop: '12px',
-              borderTop: '1px solid #f3f4f6',
-            }}>
-              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
-                <h4 style={{ fontSize: '12px', fontWeight: '600', color: '#111827' }}>已隐藏节点</h4>
-                <button
-                  onClick={() => setHiddenNodes(new Set())}
-                  style={{
-                    fontSize: '10px',
-                    color: '#3b82f6',
-                    background: 'none',
-                    border: 'none',
-                    cursor: 'pointer',
-                    textDecoration: 'underline',
-                  }}
-                >
-                  全部恢复
-                </button>
-              </div>
-              <div style={{ fontSize: '12px', maxHeight: '200px', overflow: 'auto' }}>
-                {Array.from(hiddenNodes).map(nodeId => {
-                  const node = initialNodes.find(n => n.id === nodeId);
-                  if (!node) return null;
-                  return (
-                    <div
-                      key={nodeId}
-                      style={{
-                        display: 'flex',
-                        justifyContent: 'space-between',
-                        alignItems: 'center',
-                        padding: '6px 8px',
-                        margin: '4px 0',
-                        background: '#f9fafb',
-                        borderRadius: '6px',
-                        fontSize: '11px',
-                      }}
-                    >
-                      <span
-                        style={{
-                          flex: 1,
-                          overflow: 'hidden',
-                          textOverflow: 'ellipsis',
-                          whiteSpace: 'nowrap',
-                          color: '#374151',
-                        }}
-                        title={node.data.title || nodeId}
-                      >
-                        {node.data.title || nodeId}
-                      </span>
-                      <button
-                        onClick={() => {
-                          setHiddenNodes(prev => {
-                            const newSet = new Set(prev);
-                            newSet.delete(nodeId);
-                            return newSet;
-                          });
-                        }}
-                        style={{
-                          marginLeft: '8px',
-                          fontSize: '10px',
-                          color: '#10b981',
-                          background: 'none',
-                          border: 'none',
-                          cursor: 'pointer',
-                          flexShrink: 0,
-                        }}
-                      >
-                        恢复
-                      </button>
-                    </div>
-                  );
-                })}
-              </div>
-            </div>
-          )}
-          </div>
-          </div>
-
-          {/* React Flow 画布 */}
-          <ReactFlow
-            nodes={nodes}
-            edges={edges}
-            nodeTypes={nodeTypes}
-            fitView
-            fitViewOptions={{ padding: 0.2, duration: 500 }}
-            minZoom={0.1}
-            maxZoom={1.5}
-            nodesDraggable={true}
-            nodesConnectable={false}
-            elementsSelectable={true}
-            defaultEdgeOptions={{
-              type: 'smoothstep',
-            }}
-            proOptions={{ hideAttribution: true }}
-            onNodeClick={(event, clickedNode) => {
-              setSelectedNodeId(clickedNode.id);
-            }}
-          >
-            <Controls style={{ bottom: '20px', left: 'auto', right: '20px' }} />
-            <Background variant="dots" gap={20} size={1} color="#e5e7eb" />
-          </ReactFlow>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function App() {
-  return (
-    <ReactFlowProvider>
-      <FlowContent />
-    </ReactFlowProvider>
-  );
-}
-
-const root = createRoot(document.getElementById('root'));
-root.render(<App />);
-`;
-
-fs.writeFileSync(reactComponentPath, reactComponent);
-
-// 使用 esbuild 打包
-console.log('🎨 Building modern visualization...');
-
-build({
-  entryPoints: [reactComponentPath],
-  bundle: true,
-  outfile: path.join(__dirname, 'bundle_v2.js'),
-  format: 'iife',
-  loader: {
-    '.css': 'css',
-  },
-  minify: false,
-  sourcemap: 'inline',
-  // 强制所有 React 引用指向同一个位置,避免多副本
-  alias: {
-    'react': path.join(__dirname, 'node_modules/react'),
-    'react-dom': path.join(__dirname, 'node_modules/react-dom'),
-    'react/jsx-runtime': path.join(__dirname, 'node_modules/react/jsx-runtime'),
-    'react/jsx-dev-runtime': path.join(__dirname, 'node_modules/react/jsx-dev-runtime'),
-  },
-}).then(() => {
-  // 读取打包后的 JS
-  const bundleJs = fs.readFileSync(path.join(__dirname, 'bundle_v2.js'), 'utf-8');
-
-  // 读取 CSS
-  const cssPath = path.join(__dirname, 'node_modules/@xyflow/react/dist/style.css');
-  const css = fs.readFileSync(cssPath, 'utf-8');
-
-  // 生成最终 HTML
-  const html = `<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>查询图可视化</title>
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
-    <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
-    <script>
-      // 过滤特定的 React 警告
-      const originalError = console.error;
-      console.error = (...args) => {
-        if (typeof args[0] === 'string' && args[0].includes('Each child in a list should have a unique "key" prop')) {
-          return;
-        }
-        originalError.apply(console, args);
-      };
-    </script>
-    <style>
-        * {
-            margin: 0;
-            padding: 0;
-            box-sizing: border-box;
-        }
-        body {
-            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-            overflow: hidden;
-            -webkit-font-smoothing: antialiased;
-            -moz-osx-font-smoothing: grayscale;
-        }
-        #root {
-            width: 100vw;
-            height: 100vh;
-        }
-        ${css}
-
-        /* 自定义样式覆盖 */
-        .react-flow__edge-path {
-            stroke-linecap: round;
-        }
-        .react-flow__controls {
-            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-            border: 1px solid #e5e7eb;
-            border-radius: 8px;
-        }
-        .react-flow__controls-button {
-            border: none;
-            border-bottom: 1px solid #e5e7eb;
-        }
-        .react-flow__controls-button:hover {
-            background: #f9fafb;
-        }
-    </style>
-</head>
-<body>
-    <div id="root"></div>
-    <script>${bundleJs}</script>
-</body>
-</html>`;
-
-  // 写入输出文件
-  fs.writeFileSync(outputFile, html);
-
-  // 清理临时文件
-  fs.unlinkSync(reactComponentPath);
-  fs.unlinkSync(path.join(__dirname, 'bundle_v2.js'));
-
-  console.log('✅ Visualization generated: ' + outputFile);
-  console.log('📊 Nodes: ' + Object.keys(data.nodes).length);
-  console.log('🔗 Edges: ' + data.edges.length);
-}).catch(error => {
-  console.error('❌ Build error:', error);
-  process.exit(1);
-});

+ 0 - 25
visualization/sug_v6_1_2_8/package.json

@@ -1,25 +0,0 @@
-{
-  "name": "sug-v6-1-2-5-visualization",
-  "version": "1.0.0",
-  "description": "可视化工具 for sug_v6_1_2_5.py - React Flow based query graph visualization",
-  "main": "index.js",
-  "scripts": {
-    "visualize": "node index.js"
-  },
-  "dependencies": {
-    "react": "^19.2.0",
-    "react-dom": "^19.2.0",
-    "esbuild": "^0.25.11",
-    "@xyflow/react": "^12.9.1",
-    "dagre": "^0.8.5",
-    "zustand": "^5.0.2"
-  },
-  "keywords": [
-    "visualization",
-    "react-flow",
-    "query-graph",
-    "dagre"
-  ],
-  "author": "",
-  "license": "ISC"
-}

+ 0 - 2180
visualize_steps_v6_1_2_3.py

@@ -1,2180 +0,0 @@
-#!/usr/bin/env python3
-"""
-Steps 可视化工具
-将 steps.json 转换为 HTML 可视化页面
-"""
-
-import json
-import argparse
-from pathlib import Path
-from datetime import datetime
-
-
-HTML_TEMPLATE = """<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Query Optimization Steps 可视化</title>
-    <style>
-        * {
-            margin: 0;
-            padding: 0;
-            box-sizing: border-box;
-        }
-
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-            background: #f5f5f5;
-            color: #333;
-            line-height: 1.6;
-            display: flex;
-            margin: 0;
-            padding: 0;
-        }
-
-        /* 左侧导航 */
-        .sidebar {
-            width: 280px;
-            background: white;
-            height: 100vh;
-            position: fixed;
-            left: 0;
-            top: 0;
-            overflow-y: auto;
-            box-shadow: 2px 0 8px rgba(0,0,0,0.1);
-            z-index: 100;
-        }
-
-        .sidebar-header {
-            padding: 20px;
-            background: #2563eb;
-            color: white;
-            font-size: 18px;
-            font-weight: 600;
-        }
-
-        .toc {
-            padding: 10px 0;
-        }
-
-        .toc-item {
-            padding: 10px 20px;
-            cursor: pointer;
-            transition: background 0.2s;
-            border-left: 3px solid transparent;
-        }
-
-        .toc-item:hover {
-            background: #f0f9ff;
-        }
-
-        .toc-item.active {
-            background: #eff6ff;
-            border-left-color: #2563eb;
-            color: #2563eb;
-            font-weight: 600;
-        }
-
-        .toc-item-level-0 {
-            font-weight: 600;
-            color: #1a1a1a;
-            font-size: 14px;
-        }
-
-        .toc-item-level-1 {
-            padding-left: 35px;
-            font-size: 13px;
-            color: #666;
-        }
-
-        .toc-item-level-2 {
-            padding-left: 50px;
-            font-size: 12px;
-            color: #999;
-        }
-
-        .toc-toggle {
-            display: inline-block;
-            width: 16px;
-            height: 16px;
-            margin-left: 5px;
-            cursor: pointer;
-            transition: transform 0.2s;
-            float: right;
-        }
-
-        .toc-toggle.collapsed {
-            transform: rotate(-90deg);
-        }
-
-        .toc-children {
-            display: none;
-        }
-
-        .toc-children.expanded {
-            display: block;
-        }
-
-        .container {
-            margin-left: 280px;
-            width: calc(100% - 280px);
-            padding: 20px;
-        }
-
-        .header {
-            background: white;
-            padding: 30px;
-            border-radius: 12px;
-            margin-bottom: 30px;
-            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-        }
-
-        .header h1 {
-            font-size: 32px;
-            margin-bottom: 20px;
-            color: #1a1a1a;
-        }
-
-        .question-box {
-            background: #f0f9ff;
-            padding: 20px;
-            border-radius: 8px;
-            margin-bottom: 20px;
-            border-left: 4px solid #0284c7;
-        }
-
-        .question-label {
-            font-size: 14px;
-            color: #0369a1;
-            margin-bottom: 8px;
-            font-weight: 600;
-        }
-
-        .question-text {
-            font-size: 18px;
-            color: #1a1a1a;
-            line-height: 1.6;
-        }
-
-        .overview {
-            display: flex;
-            gap: 30px;
-            flex-wrap: wrap;
-        }
-
-        .overview-item {
-            flex: 1;
-            min-width: 150px;
-        }
-
-        .overview-label {
-            font-size: 14px;
-            color: #666;
-            margin-bottom: 5px;
-        }
-
-        .overview-value {
-            font-size: 28px;
-            font-weight: bold;
-            color: #2563eb;
-        }
-
-        .step-section {
-            background: white;
-            padding: 30px;
-            border-radius: 12px;
-            margin-bottom: 30px;
-            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
-        }
-
-        .step-header {
-            border-bottom: 3px solid #2563eb;
-            padding-bottom: 15px;
-            margin-bottom: 20px;
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-        }
-
-        .step-title {
-            font-size: 26px;
-            color: #1a1a1a;
-        }
-
-        .step-type {
-            background: #e0e7ff;
-            color: #4338ca;
-            padding: 6px 15px;
-            border-radius: 20px;
-            font-size: 13px;
-            font-weight: 600;
-            font-family: monospace;
-        }
-
-        .step-content {
-            margin-top: 20px;
-        }
-
-        .info-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
-            gap: 20px;
-            margin-bottom: 20px;
-        }
-
-        .info-item {
-            background: #f8f9fa;
-            padding: 15px;
-            border-radius: 8px;
-        }
-
-        .info-label {
-            font-size: 13px;
-            color: #666;
-            margin-bottom: 5px;
-        }
-
-        .info-value {
-            font-size: 20px;
-            font-weight: bold;
-            color: #1a1a1a;
-        }
-
-        .posts-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
-            gap: 20px;
-            margin-top: 20px;
-            padding-top: 100px;
-            margin-top: -80px;
-        }
-
-        .post-card {
-            background: white;
-            border-radius: 8px;
-            overflow: visible;
-            transition: transform 0.2s, box-shadow 0.2s;
-            border: 1px solid #e5e7eb;
-            cursor: pointer;
-            position: relative;
-        }
-
-        .post-card:hover {
-            transform: translateY(-4px);
-            box-shadow: 0 6px 16px rgba(0,0,0,0.15);
-        }
-
-        .post-image-wrapper {
-            width: 100%;
-            background: #f3f4f6;
-            position: relative;
-            padding-top: 133.33%; /* 3:4 aspect ratio */
-            overflow: hidden;
-            border-radius: 8px 8px 0 0;
-        }
-
-        .post-image {
-            position: absolute;
-            top: 0;
-            left: 0;
-            width: 100%;
-            height: 100%;
-            object-fit: cover;
-        }
-
-        .no-image {
-            position: absolute;
-            top: 50%;
-            left: 50%;
-            transform: translate(-50%, -50%);
-            color: #9ca3af;
-            font-size: 14px;
-        }
-
-        .post-type-badge {
-            position: absolute;
-            top: 10px;
-            right: 10px;
-            background: rgba(0, 0, 0, 0.7);
-            color: white;
-            padding: 4px 10px;
-            border-radius: 15px;
-            font-size: 11px;
-            font-weight: 600;
-        }
-
-        .post-info {
-            padding: 15px;
-            position: relative;
-            overflow: visible;
-        }
-
-        .post-title {
-            font-size: 14px;
-            font-weight: 600;
-            margin-bottom: 8px;
-            color: #1a1a1a;
-            display: -webkit-box;
-            -webkit-line-clamp: 2;
-            -webkit-box-orient: vertical;
-            overflow: hidden;
-        }
-
-        .post-desc {
-            font-size: 12px;
-            color: #6b7280;
-            margin-bottom: 10px;
-            display: -webkit-box;
-            -webkit-line-clamp: 2;
-            -webkit-box-orient: vertical;
-            overflow: hidden;
-        }
-
-        .post-meta {
-            display: flex;
-            gap: 15px;
-            margin-bottom: 8px;
-            font-size: 12px;
-            color: #9ca3af;
-        }
-
-        .post-meta-item {
-            display: flex;
-            align-items: center;
-            gap: 4px;
-        }
-
-        .post-author {
-            font-size: 12px;
-            color: #6b7280;
-            margin-bottom: 8px;
-        }
-
-        .post-id {
-            font-size: 10px;
-            color: #9ca3af;
-            font-family: monospace;
-        }
-
-        .evaluation-reason {
-            position: absolute;
-            bottom: calc(100% + 10px);
-            left: 50%;
-            transform: translateX(-50%);
-            background: #2d3748;
-            color: white;
-            padding: 12px 16px;
-            border-radius: 8px;
-            font-size: 12px;
-            line-height: 1.5;
-            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
-            z-index: 1000;
-            display: none;
-            white-space: normal;
-            width: 280px;
-        }
-
-        /* Tooltip 箭头 - 指向下方进度条 */
-        .evaluation-reason::after {
-            content: '';
-            position: absolute;
-            top: 100%;
-            left: 50%;
-            transform: translateX(-50%);
-            border: 6px solid transparent;
-            border-top-color: #2d3748;
-        }
-
-        .confidence-bar:hover .evaluation-reason {
-            display: block !important;
-        }
-
-        /* Debug: 让进度条更明显可悬停 */
-        .confidence-bar {
-            min-height: 32px;
-        }
-
-        .evaluation-reason strong {
-            color: #fbbf24;
-            font-size: 13px;
-        }
-
-        .evaluation-scores {
-            display: flex;
-            gap: 10px;
-            margin-top: 10px;
-            font-size: 12px;
-            flex-wrap: wrap;
-        }
-
-        .score-item {
-            background: rgba(255, 255, 255, 0.15);
-            padding: 5px 10px;
-            border-radius: 12px;
-            color: #fbbf24;
-            border: 1px solid rgba(251, 191, 36, 0.3);
-        }
-
-        .confidence-bar {
-            width: 100%;
-            height: 32px;
-            background: #f3f4f6;
-            position: relative;
-            cursor: help;
-            display: flex;
-            align-items: center;
-            border-radius: 0 0 8px 8px;
-            overflow: hidden;
-        }
-
-        .confidence-bar-fill {
-            height: 100%;
-            transition: width 0.5s ease-out;
-            display: flex;
-            align-items: center;
-            padding: 0 12px;
-            position: relative;
-        }
-
-        .confidence-bar-fill.confidence-low {
-            background: linear-gradient(90deg, #ef4444, #f87171);
-        }
-
-        .confidence-bar-fill.confidence-medium {
-            background: linear-gradient(90deg, #f59e0b, #fbbf24);
-        }
-
-        .confidence-bar-fill.confidence-high {
-            background: linear-gradient(90deg, #10b981, #34d399);
-        }
-
-        .confidence-bar-text {
-            color: white;
-            font-size: 12px;
-            font-weight: 600;
-            white-space: nowrap;
-            position: relative;
-            z-index: 1;
-            text-shadow: 0 1px 2px rgba(0,0,0,0.2);
-        }
-
-        /* 保留旧的badge样式用于兼容 */
-        .confidence-badge {
-            background: #10b981;
-            color: white;
-            padding: 4px 10px;
-            border-radius: 15px;
-            font-size: 12px;
-            font-weight: bold;
-            display: inline-block;
-            margin-bottom: 10px;
-            position: relative;
-            cursor: help;
-        }
-
-        .confidence-low {
-            background: #ef4444;
-        }
-
-        .confidence-medium {
-            background: #f59e0b;
-        }
-
-        .confidence-high {
-            background: #10b981;
-        }
-
-        .query-list {
-            background: #f8f9fa;
-            padding: 20px;
-            border-radius: 8px;
-            margin-top: 15px;
-        }
-
-        .query-item {
-            background: white;
-            padding: 15px;
-            border-radius: 6px;
-            margin-bottom: 10px;
-            border-left: 3px solid #2563eb;
-        }
-
-        .query-text {
-            font-size: 15px;
-            font-weight: 600;
-            color: #1a1a1a;
-            margin-bottom: 5px;
-        }
-
-        .query-meta {
-            font-size: 13px;
-            color: #666;
-        }
-
-        .answer-box {
-            background: #f0fdf4;
-            border: 2px solid #10b981;
-            border-radius: 8px;
-            padding: 25px;
-            margin-top: 20px;
-        }
-
-        .answer-header {
-            font-size: 18px;
-            color: #059669;
-            margin-bottom: 15px;
-            font-weight: 600;
-        }
-
-        .answer-content {
-            font-size: 15px;
-            line-height: 1.8;
-            color: #1a1a1a;
-            white-space: pre-wrap;
-        }
-
-        .answer-meta {
-            margin-top: 15px;
-            padding-top: 15px;
-            border-top: 1px solid #d1fae5;
-            display: flex;
-            gap: 20px;
-            font-size: 13px;
-            color: #059669;
-        }
-
-        .keyword-tags {
-            display: flex;
-            flex-wrap: wrap;
-            gap: 10px;
-            margin-top: 15px;
-        }
-
-        .keyword-tag {
-            background: #dbeafe;
-            color: #1e40af;
-            padding: 6px 12px;
-            border-radius: 15px;
-            font-size: 13px;
-            font-weight: 500;
-        }
-
-        .level-analysis {
-            background: #fef3c7;
-            border-left: 4px solid #f59e0b;
-            padding: 20px;
-            border-radius: 6px;
-            margin-top: 15px;
-        }
-
-        .level-analysis-title {
-            font-size: 16px;
-            color: #92400e;
-            margin-bottom: 10px;
-            font-weight: 600;
-        }
-
-        .level-analysis-text {
-            font-size: 14px;
-            color: #78350f;
-            line-height: 1.8;
-        }
-
-        .timestamp {
-            font-size: 12px;
-            color: #9ca3af;
-            margin-top: 10px;
-        }
-
-        a {
-            color: #2563eb;
-            text-decoration: none;
-        }
-
-        a:hover {
-            text-decoration: underline;
-        }
-
-        /* 模态框样式 */
-        .modal-overlay {
-            display: none;
-            position: fixed;
-            top: 0;
-            left: 0;
-            right: 0;
-            bottom: 0;
-            background: rgba(0, 0, 0, 0.85);
-            z-index: 1000;
-            align-items: center;
-            justify-content: center;
-            padding: 20px;
-            overflow-y: auto;
-        }
-
-        .modal-overlay.active {
-            display: flex;
-        }
-
-        .modal-content {
-            background: white;
-            border-radius: 12px;
-            max-width: 1000px;
-            width: 100%;
-            max-height: 90vh;
-            overflow-y: auto;
-            position: relative;
-            animation: modalSlideIn 0.3s;
-        }
-
-        @keyframes modalSlideIn {
-            from { opacity: 0; transform: translateY(-30px); }
-            to { opacity: 1; transform: translateY(0); }
-        }
-
-        .modal-close {
-            position: sticky;
-            top: 0;
-            right: 0;
-            background: white;
-            border: none;
-            font-size: 36px;
-            color: #6b7280;
-            cursor: pointer;
-            padding: 15px 25px;
-            z-index: 10;
-            text-align: right;
-            border-bottom: 2px solid #e5e7eb;
-            transition: color 0.2s;
-        }
-
-        .modal-close:hover {
-            color: #1f2937;
-        }
-
-        .modal-body {
-            padding: 30px;
-        }
-
-        .modal-title {
-            font-size: 26px;
-            font-weight: 700;
-            color: #1a1a1a;
-            margin-bottom: 15px;
-            line-height: 1.4;
-        }
-
-        .modal-meta {
-            display: flex;
-            gap: 20px;
-            flex-wrap: wrap;
-            margin-bottom: 25px;
-            padding-bottom: 20px;
-            border-bottom: 1px solid #e5e7eb;
-        }
-
-        .modal-meta-item {
-            display: flex;
-            align-items: center;
-            gap: 6px;
-            font-size: 14px;
-            color: #6b7280;
-        }
-
-        .modal-images {
-            margin-bottom: 25px;
-        }
-
-        .modal-images-grid {
-            display: grid;
-            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-            gap: 12px;
-        }
-
-        .modal-image-item {
-            border-radius: 8px;
-            overflow: hidden;
-            border: 2px solid #e5e7eb;
-            transition: border-color 0.2s;
-            cursor: pointer;
-        }
-
-        .modal-image-item:hover {
-            border-color: #2563eb;
-        }
-
-        .modal-image {
-            width: 100%;
-            height: auto;
-            display: block;
-            max-height: 250px;
-            object-fit: cover;
-        }
-
-        .modal-section {
-            margin-bottom: 25px;
-        }
-
-        .modal-section-title {
-            font-size: 17px;
-            font-weight: 600;
-            color: #374151;
-            margin-bottom: 12px;
-        }
-
-        .modal-text-content {
-            font-size: 15px;
-            color: #1f2937;
-            line-height: 1.8;
-            white-space: pre-wrap;
-            background: #f9fafb;
-            padding: 18px;
-            border-radius: 8px;
-        }
-
-        .modal-evaluation {
-            background: #fef3c7;
-            border-left: 4px solid #f59e0b;
-            padding: 18px;
-            border-radius: 6px;
-        }
-
-        .modal-link {
-            margin-top: 25px;
-            padding-top: 25px;
-            border-top: 2px solid #e5e7eb;
-            text-align: center;
-        }
-
-        .modal-link-btn {
-            display: inline-flex;
-            align-items: center;
-            gap: 10px;
-            padding: 12px 28px;
-            background: #2563eb;
-            color: white;
-            text-decoration: none;
-            border-radius: 8px;
-            font-size: 15px;
-            font-weight: 600;
-            transition: all 0.2s;
-        }
-
-        .modal-link-btn:hover {
-            background: #1d4ed8;
-            transform: translateY(-2px);
-            box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
-        }
-
-        /* 卡片上的图片轮播指示器 */
-        .carousel-arrow {
-            position: absolute;
-            top: 50%;
-            transform: translateY(-50%);
-            background: rgba(0, 0, 0, 0.6);
-            color: white;
-            border: none;
-            width: 36px;
-            height: 36px;
-            border-radius: 50%;
-            font-size: 20px;
-            cursor: pointer;
-            z-index: 15;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            transition: all 0.2s;
-            opacity: 0;
-        }
-
-        .post-image-wrapper:hover .carousel-arrow {
-            opacity: 1;
-        }
-
-        .carousel-arrow:hover {
-            background: rgba(0, 0, 0, 0.8);
-            transform: translateY(-50%) scale(1.1);
-        }
-
-        .carousel-arrow.left {
-            left: 8px;
-        }
-
-        .carousel-arrow.right {
-            right: 8px;
-        }
-
-        /* 可折叠区域样式 */
-        .collapsible-section {
-            margin: 20px 0;
-        }
-
-        .collapsible-header {
-            background: #f3f4f6;
-            padding: 12px 15px;
-            border-radius: 8px;
-            cursor: pointer;
-            display: flex;
-            align-items: center;
-            gap: 10px;
-            transition: background 0.2s;
-            user-select: none;
-        }
-
-        .collapsible-header:hover {
-            background: #e5e7eb;
-        }
-
-        .collapsible-toggle {
-            font-size: 14px;
-            transition: transform 0.2s;
-        }
-
-        .collapsible-toggle.collapsed {
-            transform: rotate(-90deg);
-        }
-
-        .collapsible-title {
-            font-weight: 600;
-            font-size: 16px;
-            color: #374151;
-        }
-
-        .collapsible-content {
-            max-height: 10000px;
-            overflow: hidden;
-            transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
-            opacity: 1;
-        }
-
-        .collapsible-content.collapsed {
-            max-height: 0;
-            opacity: 0;
-        }
-    </style>
-</head>
-<body>
-    <!-- 左侧导航 -->
-    <div class="sidebar">
-        <div class="sidebar-header">📑 目录</div>
-        <div class="toc" id="toc"></div>
-    </div>
-
-    <!-- 主内容区 -->
-    <div class="container">
-        {content}
-    </div>
-
-    <!-- 模态框 -->
-    <div id="postModal" class="modal-overlay" onclick="if(event.target === this) closeModal()">
-        <div class="modal-content" onclick="event.stopPropagation()">
-            <button class="modal-close" onclick="closeModal()">&times;</button>
-            <div class="modal-body" id="modalBody">
-                <!-- 动态内容 -->
-            </div>
-        </div>
-    </div>
-
-    <script>
-        // 模态框功能
-        function openModal(postData) {
-            const modal = document.getElementById('postModal');
-            const modalBody = document.getElementById('modalBody');
-
-            // 构建图片网格
-            let imagesHtml = '';
-            if (postData.images && postData.images.length > 0) {
-                imagesHtml = '<div class="modal-images"><div class="modal-images-grid">';
-                postData.images.forEach((img, idx) => {
-                    imagesHtml += `<div class="modal-image-item"><img src="${img}" class="modal-image" alt="图片 ${idx + 1}"></div>`;
-                });
-                imagesHtml += '</div></div>';
-            }
-
-            // 构建评估详情
-            let evalHtml = '';
-            if (postData.evaluation) {
-                evalHtml = `
-                <div class="modal-section">
-                    <div class="modal-section-title">💡 评估详情</div>
-                    <div class="modal-evaluation">
-                        <div style="margin-bottom: 12px;"><strong>评估理由:</strong></div>
-                        <div style="color: #78350f; line-height: 1.8;">${postData.evaluation.reason || '无'}</div>
-                        <div class="evaluation-scores" style="margin-top: 12px;">
-                            <span class="score-item">📌 标题相关性: ${postData.evaluation.title_relevance?.toFixed(2) || '0.00'}</span>
-                            <span class="score-item">📄 内容期望: ${postData.evaluation.content_expectation?.toFixed(2) || '0.00'}</span>
-                            <span class="score-item">🎯 置信度: ${postData.evaluation.confidence_score?.toFixed(2) || '0.00'}</span>
-                        </div>
-                    </div>
-                </div>`;
-            }
-
-            modalBody.innerHTML = `
-                <div class="modal-title">${postData.title}</div>
-                <div class="modal-meta">
-                    <div class="modal-meta-item">👤 ${postData.user}</div>
-                    <div class="modal-meta-item">❤️ ${postData.likes}</div>
-                    <div class="modal-meta-item">⭐ ${postData.collects}</div>
-                    <div class="modal-meta-item">💬 ${postData.comments}</div>
-                    ${postData.type === 'video' ? '<div class="modal-meta-item">📹 视频</div>' : ''}
-                </div>
-                ${imagesHtml}
-                <div class="modal-section">
-                    <div class="modal-section-title">📝 描述</div>
-                    <div class="modal-text-content">${postData.desc || '无描述'}</div>
-                </div>
-                ${evalHtml}
-                <div class="modal-link">
-                    <a href="${postData.url}" target="_blank" class="modal-link-btn">
-                        🔗 在小红书中查看
-                    </a>
-                </div>
-            `;
-
-            modal.classList.add('active');
-            document.body.style.overflow = 'hidden';
-        }
-
-        function closeModal() {
-            const modal = document.getElementById('postModal');
-            modal.classList.remove('active');
-            document.body.style.overflow = '';
-        }
-
-        // ESC键关闭模态框
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape') {
-                closeModal();
-            }
-        });
-
-        // 卡片上的图片轮播(使用左右箭头按钮)
-        function initCarousels() {
-            document.querySelectorAll('.post-card').forEach(card => {
-                const images = JSON.parse(card.dataset.images || '[]');
-                if (images.length <= 1) return;
-
-                let currentIndex = 0;
-                const imgElement = card.querySelector('.post-image');
-                const leftArrow = card.querySelector('.carousel-arrow.left');
-                const rightArrow = card.querySelector('.carousel-arrow.right');
-
-                // 左箭头点击
-                if (leftArrow) {
-                    leftArrow.addEventListener('click', function(e) {
-                        e.stopPropagation();
-                        currentIndex = (currentIndex - 1 + images.length) % images.length;
-                        if (imgElement) {
-                            imgElement.src = images[currentIndex];
-                        }
-                    });
-                }
-
-                // 右箭头点击
-                if (rightArrow) {
-                    rightArrow.addEventListener('click', function(e) {
-                        e.stopPropagation();
-                        currentIndex = (currentIndex + 1) % images.length;
-                        if (imgElement) {
-                            imgElement.src = images[currentIndex];
-                        }
-                    });
-                }
-            });
-        }
-
-        // 生成目录(显示步骤和可折叠的子项)
-        function generateTOC() {
-            const toc = document.getElementById('toc');
-            const sections = document.querySelectorAll('.step-section');
-
-            sections.forEach((section, index) => {
-                const title = section.querySelector('.step-title')?.textContent || `步骤 ${index + 1}`;
-                const id = `step-${index}`;
-                section.id = id;
-
-                // 查找该section下的直接子可折叠项(不包括嵌套的)
-                const collapsibleSections = section.querySelectorAll(':scope > .step-content > .collapsible-section[id]');
-
-                // 创建步骤项
-                const stepItem = document.createElement('div');
-                stepItem.className = 'toc-item toc-item-level-0';
-
-                if (collapsibleSections.length > 0) {
-                    // 如果有子项,添加展开/折叠图标(箭头放在右侧)
-                    const toggleId = `toc-toggle-${index}`;
-                    stepItem.innerHTML = `<span>${title}</span><span class="toc-toggle" id="${toggleId}">▼</span>`;
-
-                    const toggle = stepItem.querySelector('.toc-toggle');
-                    const childrenId = `toc-children-${index}`;
-                    toggle.onclick = (e) => {
-                        e.stopPropagation();
-                        toggle.classList.toggle('collapsed');
-                        const children = document.getElementById(childrenId);
-                        if (children) {
-                            children.classList.toggle('expanded');
-                        }
-                    };
-                } else {
-                    stepItem.textContent = title;
-                }
-
-                stepItem.onclick = (e) => {
-                    if (!e.target.classList.contains('toc-toggle')) {
-                        scrollToSection(id);
-                    }
-                };
-
-                toc.appendChild(stepItem);
-
-                // 添加子项目录(支持嵌套)
-                if (collapsibleSections.length > 0) {
-                    const childrenContainer = document.createElement('div');
-                    childrenContainer.id = `toc-children-${index}`;
-                    childrenContainer.className = 'toc-children expanded';
-
-                    collapsibleSections.forEach(collapsible => {
-                        const subTitle = collapsible.getAttribute('data-title') || '子项';
-                        const subId = collapsible.id;
-
-                        const subItem = document.createElement('div');
-                        subItem.className = 'toc-item toc-item-level-1';
-                        subItem.textContent = subTitle;
-                        subItem.onclick = () => scrollToSection(subId);
-
-                        childrenContainer.appendChild(subItem);
-
-                        // 查找该可折叠区域内的嵌套可折叠区域
-                        const nestedCollapsibles = collapsible.querySelectorAll(':scope > .collapsible-content > .collapsible-section[id]');
-                        if (nestedCollapsibles.length > 0) {
-                            nestedCollapsibles.forEach(nested => {
-                                const nestedTitle = nested.getAttribute('data-title') || '子项';
-                                const nestedId = nested.id;
-
-                                const nestedItem = document.createElement('div');
-                                nestedItem.className = 'toc-item toc-item-level-2';
-                                nestedItem.textContent = nestedTitle;
-                                nestedItem.onclick = () => scrollToSection(nestedId);
-
-                                childrenContainer.appendChild(nestedItem);
-                            });
-                        }
-                    });
-
-                    toc.appendChild(childrenContainer);
-                }
-            });
-        }
-
-        // 滚动到指定section
-        function scrollToSection(id) {
-            const element = document.getElementById(id);
-            if (element) {
-                const offset = 80;
-                const elementPosition = element.getBoundingClientRect().top;
-                const offsetPosition = elementPosition + window.pageYOffset - offset;
-
-                window.scrollTo({
-                    top: offsetPosition,
-                    behavior: 'smooth'
-                });
-
-                // 更新active状态
-                document.querySelectorAll('.toc-item').forEach(item => item.classList.remove('active'));
-                event.target.classList.add('active');
-            }
-        }
-
-        // 滚动时高亮当前section
-        function updateActiveTOC() {
-            const sections = document.querySelectorAll('.step-section');
-            const tocItems = document.querySelectorAll('.toc-item');
-
-            let currentIndex = -1;
-            sections.forEach((section, index) => {
-                const rect = section.getBoundingClientRect();
-                if (rect.top <= 100) {
-                    currentIndex = index;
-                }
-            });
-
-            tocItems.forEach((item, index) => {
-                item.classList.toggle('active', index === currentIndex);
-            });
-        }
-
-        // 初始化可折叠区域
-        function initCollapsibles() {
-            document.querySelectorAll('.collapsible-header').forEach(header => {
-                header.addEventListener('click', function() {
-                    const toggle = this.querySelector('.collapsible-toggle');
-                    const content = this.nextElementSibling;
-
-                    if (content && content.classList.contains('collapsible-content')) {
-                        toggle.classList.toggle('collapsed');
-                        content.classList.toggle('collapsed');
-                    }
-                });
-            });
-        }
-
-        // 页面加载完成后初始化
-        document.addEventListener('DOMContentLoaded', function() {
-            initCarousels();
-            generateTOC();
-            initCollapsibles();
-            window.addEventListener('scroll', updateActiveTOC);
-            updateActiveTOC();
-        });
-    </script>
-</body>
-</html>
-"""
-
-
-def make_collapsible(title, content, collapsed=True, section_id=None):
-    """创建可折叠区域的HTML"""
-    collapsed_class = " collapsed" if collapsed else ""
-    id_attr = f' id="{section_id}"' if section_id else ""
-    # 添加 data-title 属性用于目录生成
-    title_attr = f' data-title="{title}"' if section_id else ""
-    return f"""
-    <div class="collapsible-section"{id_attr}{title_attr}>
-        <div class="collapsible-header">
-            <span class="collapsible-toggle{collapsed_class}">▼</span>
-            <span class="collapsible-title">{title}</span>
-        </div>
-        <div class="collapsible-content{collapsed_class}">
-            {content}
-        </div>
-    </div>
-    """
-
-
-def get_confidence_class(score):
-    """根据置信度分数返回CSS类"""
-    if score >= 0.7:
-        return "confidence-high"
-    elif score >= 0.5:
-        return "confidence-medium"
-    else:
-        return "confidence-low"
-
-
-def escape_js_string(s):
-    """转义JavaScript字符串"""
-    import json
-    return json.dumps(str(s) if s else "")
-
-
-def build_post_json_data(note, evaluation=None):
-    """构建帖子的JSON数据用于模态框"""
-    import json
-
-    image_list = note.get('image_list', [])
-    if not image_list and note.get('cover_image'):
-        cover = note.get('cover_image')
-        # cover_image 可能是字典或字符串
-        if isinstance(cover, dict):
-            image_list = [cover.get('image_url', '')]
-        else:
-            image_list = [cover]
-
-    # image_list 现在已经是 URL 字符串列表(由搜索API预处理)
-    images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
-
-    interact = note.get('interact_info', {})
-    user = note.get('user', {})
-
-    data = {
-        'title': note.get('title', '无标题'),
-        'desc': note.get('desc', ''),
-        'user': user.get('nickname', '未知'),
-        'likes': interact.get('liked_count', 0),
-        'collects': interact.get('collected_count', 0),
-        'comments': interact.get('comment_count', 0),
-        'type': note.get('type', 'normal'),
-        'url': note.get('note_url', ''),
-        'images': images
-    }
-
-    if evaluation:
-        data['evaluation'] = {
-            'reason': evaluation.get('reason', ''),
-            'title_relevance': evaluation.get('title_relevance', 0),
-            'content_expectation': evaluation.get('content_expectation', 0),
-            'confidence_score': evaluation.get('confidence_score', 0)
-        }
-
-    return json.dumps(data, ensure_ascii=False)
-
-
-def render_header(steps_data):
-    """渲染页面头部"""
-    # 获取基本信息
-    first_step = steps_data[0] if steps_data else {}
-    last_step = steps_data[-1] if steps_data else {}
-
-    original_question = ""
-    keywords = []
-    total_steps = len(steps_data)
-    satisfied_notes = 0
-
-    # 提取关键信息
-    for step in steps_data:
-        if step.get("step_type") == "keyword_extraction":
-            original_question = step.get("data", {}).get("input_question", "")
-            keywords = step.get("data", {}).get("keywords", [])
-        elif step.get("step_type") == "final_result":
-            satisfied_notes = step.get("data", {}).get("satisfied_notes_count", 0)
-
-    keywords_html = "".join([f'<span class="keyword-tag">{k}</span>' for k in keywords])
-
-    html = f"""
-    <div class="header">
-        <h1>🔍 Query Optimization Steps</h1>
-        <div class="question-box">
-            <div class="question-label">原始问题</div>
-            <div class="question-text">{original_question}</div>
-        </div>
-        {f'<div class="keyword-tags">{keywords_html}</div>' if keywords else ''}
-        <div class="overview">
-            <div class="overview-item">
-                <div class="overview-label">总步骤数</div>
-                <div class="overview-value">{total_steps}</div>
-            </div>
-            <div class="overview-item">
-                <div class="overview-label">满足需求的帖子</div>
-                <div class="overview-value">{satisfied_notes}</div>
-            </div>
-        </div>
-    </div>
-    """
-    return html
-
-
-def render_keyword_extraction(step):
-    """渲染关键词提取步骤"""
-    data = step.get("data", {})
-    keywords = data.get("keywords", [])
-    reasoning = data.get("reasoning", "")
-
-    keywords_html = "".join([f'<span class="keyword-tag">{k}</span>' for k in keywords])
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="keyword-tags">{keywords_html}</div>
-            {f'<p style="margin-top: 15px; color: #666; font-size: 14px;">{reasoning}</p>' if reasoning else ''}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_level_exploration(step):
-    """渲染层级探索步骤"""
-    data = step.get("data", {})
-    level = data.get("level", 0)
-    query_count = data.get("query_count", 0)
-    results = data.get("results", [])
-
-    queries_html = ""
-    for result in results:
-        query = result.get("query", "")
-        suggestions = result.get("suggestions", [])
-
-        # 使用标签样式显示推荐词
-        suggestions_tags = ""
-        for suggestion in suggestions:
-            suggestions_tags += f'<span class="keyword-tag" style="margin: 3px;">{suggestion}</span>'
-
-        queries_html += f"""
-        <div class="query-item">
-            <div class="query-text">{query}</div>
-            <div style="margin-top: 10px;">
-                <div style="color: #666; font-size: 13px; margin-bottom: 5px;">推荐词 ({len(suggestions)} 个):</div>
-                <div style="display: flex; flex-wrap: wrap; gap: 5px;">
-                    {suggestions_tags}
-                </div>
-            </div>
-        </div>
-        """
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: Level {level} 探索</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item">
-                    <div class="info-label">探索query数</div>
-                    <div class="info-value">{query_count}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">获得推荐词总数</div>
-                    <div class="info-value">{data.get('total_suggestions', 0)}</div>
-                </div>
-            </div>
-            <div class="query-list">{queries_html}</div>
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_level_analysis(step):
-    """渲染层级分析步骤"""
-    data = step.get("data", {})
-    level = data.get("level", 0)
-    key_findings = data.get("key_findings", "")
-    should_evaluate = data.get("should_evaluate_now", False)
-    promising_signals_count = data.get("promising_signals_count", 0)
-    next_combinations = data.get("next_combinations", [])
-    promising_signals = data.get("promising_signals", [])
-    reasoning = data.get("reasoning", "")
-    step_num = step['step_number']
-
-    # 渲染推理过程
-    reasoning_html = ""
-    if reasoning:
-        reasoning_html = f"""
-        <div style="margin-top: 20px;">
-            <div class="level-analysis">
-                <div class="level-analysis-title">💭 推理过程</div>
-                <div class="level-analysis-text">{reasoning}</div>
-            </div>
-        </div>
-        """
-
-    # 渲染下一层探索
-    next_html = ""
-    if next_combinations:
-        next_items = "".join([f'<span class="keyword-tag">{q}</span>' for q in next_combinations])
-        next_html = f'<div style="margin-top: 15px;"><strong>下一层探索:</strong><div class="keyword-tags" style="margin-top: 10px;">{next_items}</div></div>'
-
-    # 渲染有价值的信号
-    signals_html = ""
-    if promising_signals:
-        signals_items = ""
-        for signal in promising_signals:
-            query = signal.get("query", "")
-            from_level = signal.get("from_level", "")
-            reason = signal.get("reason", "")
-
-            signals_items += f"""
-            <div class="query-item" style="border-left: 3px solid #10b981; padding-left: 15px;">
-                <div class="query-text" style="font-weight: 600;">{query}</div>
-                <div style="margin-top: 8px; color: #666; font-size: 13px;">
-                    <span style="color: #10b981;">来自 Level {from_level}</span>
-                </div>
-                <div style="margin-top: 8px; color: #555; font-size: 14px; line-height: 1.5;">
-                    {reason}
-                </div>
-            </div>
-            """
-
-        signals_html = make_collapsible(
-            f"💡 有价值的信号 ({len(promising_signals)} 个)",
-            f'<div style="display: flex; flex-direction: column; gap: 15px; margin-top: 10px;">{signals_items}</div>',
-            collapsed=True,
-            section_id=f"step{step_num}-signals"
-        )
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: Level {level} 分析</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="level-analysis">
-                <div class="level-analysis-title">🔎 关键发现</div>
-                <div class="level-analysis-text">{key_findings}</div>
-            </div>
-            <div class="info-grid" style="margin-top: 20px;">
-                <div class="info-item">
-                    <div class="info-label">有价值信号数</div>
-                    <div class="info-value">{promising_signals_count}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">是否开始评估</div>
-                    <div class="info-value">{'是' if should_evaluate else '否'}</div>
-                </div>
-            </div>
-            {signals_html}
-            {reasoning_html}
-            {next_html}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_search_results(step):
-    """渲染搜索结果步骤"""
-    data = step.get("data", {})
-    search_results = data.get("search_results", [])
-
-    posts_html = ""
-    step_num = step['step_number']
-    for idx, sr in enumerate(search_results):
-        query = sr.get("query", "")
-        note_count = sr.get("note_count", 0)
-        notes_summary = sr.get("notes_summary", [])
-
-        # 渲染该query的帖子
-        posts_cards = ""
-        for note in notes_summary:
-            # 获取封面图
-            image_list = note.get('image_list', [])
-            if image_list:
-                # image_list 已经是 URL 字符串列表,第一张就是封面
-                cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
-            else:
-                cover = note.get("cover_image", {})
-                cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
-
-            interact = note.get("interact_info", {})
-            user = note.get("user", {})
-
-            # image_list 现在已经是 URL 字符串列表
-            images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
-
-            # 构建帖子数据用于模态框
-            post_data = build_post_json_data(note)
-            images_json = json.dumps(images)
-
-            image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
-
-            type_badge = ""
-            if note.get("type") == "video":
-                type_badge = '<div class="post-type-badge">📹 视频</div>'
-
-            # 轮播箭头按钮
-            arrows_html = ""
-            if len(images) > 1:
-                arrows_html = '''
-                    <button class="carousel-arrow left" onclick="event.stopPropagation()">‹</button>
-                    <button class="carousel-arrow right" onclick="event.stopPropagation()">›</button>
-                '''
-
-            posts_cards += f"""
-            <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
-                <div class="post-image-wrapper">
-                    {image_html}
-                    {type_badge}
-                    {arrows_html}
-                </div>
-                <div class="post-info">
-                    <div class="post-title">{note.get('title', '无标题')}</div>
-                    <div class="post-desc">{note.get('desc', '')}</div>
-                    <div class="post-meta">
-                        <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
-                        <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
-                        <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
-                    </div>
-                    <div class="post-author">👤 {user.get('nickname', '未知')}</div>
-                    <div class="post-id">{note.get('note_id', '')}</div>
-                </div>
-            </div>
-            """
-
-        # 使用可折叠区域包装每个query的搜索结果,添加唯一ID
-        query_content = f'<div class="posts-grid">{posts_cards}</div>'
-        posts_html += make_collapsible(
-            f"🔎 {query} (找到 {note_count} 个帖子)",
-            query_content,
-            collapsed=True,
-            section_id=f"step{step_num}-search-{idx}"
-        )
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: 搜索结果</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item">
-                    <div class="info-label">搜索query数</div>
-                    <div class="info-value">{data.get('qualified_count', 0)}</div>
-                </div>
-            </div>
-            {posts_html}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_note_evaluations(step):
-    """渲染帖子评估步骤"""
-    data = step.get("data", {})
-    note_evaluations = data.get("note_evaluations", [])
-    total_satisfied = data.get("total_satisfied", 0)
-
-    evals_html = ""
-    step_num = step["step_number"]
-    for idx, query_eval in enumerate(note_evaluations):
-        query = query_eval.get("query", "")
-        satisfied_count = query_eval.get("satisfied_count", 0)
-        evaluated_notes = query_eval.get("evaluated_notes", [])
-
-        # 分离满足和不满足需求的帖子
-        satisfied_notes = [n for n in evaluated_notes if n.get('evaluation', {}).get('need_satisfaction')]
-        unsatisfied_notes = [n for n in evaluated_notes if not n.get('evaluation', {}).get('need_satisfaction')]
-
-        # 渲染满足需求的帖子
-        satisfied_cards = ""
-        for note in satisfied_notes:
-            # 获取封面图
-            image_list = note.get('image_list', [])
-            if image_list:
-                cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
-            else:
-                cover = note.get("cover_image", {})
-                cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
-
-            interact = note.get("interact_info", {})
-            user = note.get("user", {})
-            evaluation = note.get("evaluation", {})
-            confidence = evaluation.get("confidence_score", 0)
-
-            # image_list 现在已经是 URL 字符串列表
-            images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
-
-            # 构建帖子数据用于模态框
-            post_data = build_post_json_data(note, evaluation)
-            images_json = json.dumps(images)
-
-            image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
-
-            type_badge = ""
-            if note.get("type") == "video":
-                type_badge = '<div class="post-type-badge">📹 视频</div>'
-
-            # 轮播箭头按钮
-            arrows_html = ""
-            if len(images) > 1:
-                arrows_html = '''
-                    <button class="carousel-arrow left" onclick="event.stopPropagation()">‹</button>
-                    <button class="carousel-arrow right" onclick="event.stopPropagation()">›</button>
-                '''
-
-            # 评估详情
-            eval_reason = evaluation.get("reason", "")
-            title_rel = evaluation.get("title_relevance", 0)
-            content_exp = evaluation.get("content_expectation", 0)
-
-            eval_details = ""
-
-            # 置信度百分比
-            confidence_percent = int(confidence * 100)
-
-            satisfied_cards += f"""
-            <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
-                <div class="post-image-wrapper">
-                    {image_html}
-                    {type_badge}
-                    {arrows_html}
-                </div>
-                <div class="post-info">
-                    <div class="post-title">{note.get('title', '无标题')}</div>
-                    <div class="post-desc">{note.get('desc', '')}</div>
-                    <div class="post-meta">
-                        <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
-                        <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
-                        <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
-                    </div>
-                    <div class="post-author">👤 {user.get('nickname', '未知')}</div>
-                    <div class="post-id">{note.get('note_id', '')}</div>
-                </div>
-                <div class="confidence-bar">
-                    <div class="confidence-bar-fill {get_confidence_class(confidence)}" style="width: {confidence_percent}%">
-                        <span class="confidence-bar-text">置信度: {confidence:.2f}</span>
-                    </div>
-                    {eval_details}
-                </div>
-            </div>
-            """
-
-        # 渲染不满足需求的帖子
-        unsatisfied_cards = ""
-        for note in unsatisfied_notes:
-            # 获取封面图
-            image_list = note.get('image_list', [])
-            if image_list:
-                cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
-            else:
-                cover = note.get("cover_image", {})
-                cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
-
-            interact = note.get("interact_info", {})
-            user = note.get("user", {})
-            evaluation = note.get("evaluation", {})
-            confidence = evaluation.get("confidence_score", 0)
-
-            # image_list 现在已经是 URL 字符串列表
-            images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
-
-            post_data = build_post_json_data(note, evaluation)
-            images_json = json.dumps(images)
-
-            image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
-
-            type_badge = ""
-            if note.get("type") == "video":
-                type_badge = '<div class="post-type-badge">📹 视频</div>'
-
-            arrows_html = ""
-            if len(images) > 1:
-                arrows_html = '''
-        <button class="carousel-arrow left" onclick="event.stopPropagation()">‹</button>
-        <button class="carousel-arrow right" onclick="event.stopPropagation()">›</button>
-    '''
-
-            eval_reason = evaluation.get("reason", "")
-            title_rel = evaluation.get("title_relevance", 0)
-            content_exp = evaluation.get("content_expectation", 0)
-
-            eval_details = ""
-
-            confidence_percent = int(confidence * 100)
-
-            unsatisfied_cards += f"""
-            <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
-                <div class="post-image-wrapper">
-                    {image_html}
-                    {type_badge}
-                    {arrows_html}
-                </div>
-                <div class="post-info">
-                    <div class="post-title">{note.get('title', '无标题')}</div>
-                    <div class="post-desc">{note.get('desc', '')}</div>
-                    <div class="post-meta">
-                        <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
-                        <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
-                        <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
-                    </div>
-                    <div class="post-author">👤 {user.get('nickname', '未知')}</div>
-                    <div class="post-id">{note.get('note_id', '')}</div>
-                </div>
-                <div class="confidence-bar">
-                    <div class="confidence-bar-fill {get_confidence_class(confidence)}" style="width: {confidence_percent}%">
-                        <span class="confidence-bar-text">置信度: {confidence:.2f}</span>
-                    </div>
-                    {eval_details}
-                </div>
-            </div>
-            """
-
-        # 构建该query的评估结果,使用嵌套可折叠区域
-        query_sections = ""
-        if satisfied_cards:
-            query_sections += make_collapsible(
-                f"✅ 满足需求 ({len(satisfied_notes)} 个帖子)",
-                f'<div class="posts-grid">{satisfied_cards}</div>',
-                collapsed=True,
-                section_id=f"step{step_num}-eval-{idx}-satisfied"
-            )
-        if unsatisfied_cards:
-            query_sections += make_collapsible(
-                f"❌ 不满足需求 ({len(unsatisfied_notes)} 个帖子)",
-                f'<div class="posts-grid">{unsatisfied_cards}</div>',
-                collapsed=True,
-                section_id=f"step{step_num}-eval-{idx}-unsatisfied"
-            )
-
-        if query_sections:
-            # 使用可折叠区域包装每个query的评估结果
-            evals_html += make_collapsible(
-                f"📊 {query} ({satisfied_count}/{len(evaluated_notes)} 个满足需求)",
-                query_sections,
-                collapsed=True,
-                section_id=f"step{step_num}-eval-{idx}"
-            )
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: 帖子评估结果</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item">
-                    <div class="info-label">评估的query数</div>
-                    <div class="info-value">{data.get('query_count', 0)}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">总帖子数</div>
-                    <div class="info-value">{data.get('total_notes', 0)}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">满足需求的帖子</div>
-                    <div class="info-value">{total_satisfied}</div>
-                </div>
-            </div>
-            {evals_html}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_answer_generation(step):
-    """渲染答案生成步骤"""
-    data = step.get("data", {})
-    result = data.get("result", {})
-    answer = result.get("answer", "")
-    confidence = result.get("confidence", 0)
-    summary = result.get("summary", "")
-    cited_notes = result.get("cited_notes", [])
-
-    # 渲染引用的帖子
-    cited_html = ""
-    for note in cited_notes:
-        # 获取封面图
-        image_list = note.get('image_list', [])
-        if image_list:
-            cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
-        else:
-            cover = note.get("cover_image", {})
-            cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
-
-        interact = note.get("interact_info", {})
-        user = note.get("user", {})
-
-        # image_list 现在已经是 URL 字符串列表
-        images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
-
-        # 构建帖子数据用于模态框(包含评估信息)
-        eval_data = {
-            'reason': note.get("reason", ""),
-            'title_relevance': note.get("title_relevance", 0),
-            'content_expectation': note.get("content_expectation", 0),
-            'confidence_score': note.get('confidence_score', 0)
-        }
-        post_data = build_post_json_data(note, eval_data)
-        images_json = json.dumps(images)
-
-        image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
-
-        # 类型标识
-        type_badge = ""
-        if note.get("type") == "video":
-            type_badge = '<div class="post-type-badge">📹 视频</div>'
-
-        # 轮播箭头按钮
-        arrows_html = ""
-        if len(images) > 1:
-            arrows_html = '''
-        <button class="carousel-arrow left" onclick="event.stopPropagation()">‹</button>
-        <button class="carousel-arrow right" onclick="event.stopPropagation()">›</button>
-    '''
-
-        # 评估详情
-        eval_reason = note.get("reason", "")
-        title_rel = note.get("title_relevance", 0)
-        content_exp = note.get("content_expectation", 0)
-
-        eval_details = ""
-
-        # 置信度百分比
-        note_confidence = note.get('confidence_score', 0)
-        confidence_percent = int(note_confidence * 100)
-
-        cited_html += f"""
-        <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
-            <div class="post-image-wrapper">
-                {image_html}
-                {type_badge}
-                {arrows_html}
-            </div>
-            <div class="post-info">
-                <div class="post-title">[{note.get('index')}] {note.get('title', '无标题')}</div>
-                <div class="post-desc">{note.get('desc', '')}</div>
-                <div class="post-meta">
-                    <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
-                    <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
-                    <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
-                </div>
-                <div class="post-author">👤 {user.get('nickname', '未知')}</div>
-                <div class="post-id">{note.get('note_id', '')}</div>
-            </div>
-            <div class="confidence-bar">
-                <div class="confidence-bar-fill {get_confidence_class(note_confidence)}" style="width: {confidence_percent}%">
-                    <span class="confidence-bar-text">置信度: {note_confidence:.2f}</span>
-                </div>
-                {eval_details}
-            </div>
-        </div>
-        """
-
-    # 使用可折叠区域包装引用的帖子
-    step_num = step['step_number']
-    cited_section = ""
-    if cited_html:
-        cited_section = make_collapsible(
-            f"📌 引用的帖子 ({len(cited_notes)} 个)",
-            f'<div class="posts-grid">{cited_html}</div>',
-            collapsed=True,
-            section_id=f"step{step_num}-cited"
-        )
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: 生成答案</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="answer-box">
-                <div class="answer-header">📝 生成的答案</div>
-                <div class="answer-content">{answer}</div>
-                <div class="answer-meta">
-                    <div><strong>置信度:</strong> {confidence:.2f}</div>
-                    <div><strong>引用帖子:</strong> {len(cited_notes)} 个</div>
-                </div>
-            </div>
-            {f'<p style="margin-top: 15px; color: #666;"><strong>摘要:</strong> {summary}</p>' if summary else ''}
-            {cited_section}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_final_result(step):
-    """渲染最终结果步骤"""
-    data = step.get("data", {})
-    success = data.get("success", False)
-    message = data.get("message", "")
-    satisfied_notes_count = data.get("satisfied_notes_count", 0)
-
-    status_color = "#10b981" if success else "#ef4444"
-    status_text = "✅ 成功" if success else "❌ 失败"
-
-    html = f"""
-    <div class="step-section" style="border: 3px solid {status_color};">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item" style="background: {status_color}20;">
-                    <div class="info-label">状态</div>
-                    <div class="info-value" style="color: {status_color};">{status_text}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">满足需求的帖子</div>
-                    <div class="info-value">{satisfied_notes_count}</div>
-                </div>
-            </div>
-            <p style="margin-top: 20px; font-size: 15px; color: #666;">{message}</p>
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_query_suggestion_evaluation(step):
-    """渲染候选query推荐词评估步骤"""
-    data = step.get("data", {})
-    candidate_count = data.get("candidate_count", 0)
-    results = data.get("results", [])
-
-    results_html = ""
-    step_num = step['step_number']
-    for idx, result in enumerate(results):
-        candidate = result.get("candidate", "")
-        suggestions = result.get("suggestions", [])
-        evaluations = result.get("evaluations", [])
-
-        # 渲染每个候选词的推荐词评估
-        eval_cards = ""
-        for evaluation in evaluations:
-            query = evaluation.get("query", "")
-            intent_match = evaluation.get("intent_match", False)
-            relevance_score = evaluation.get("relevance_score", 0)
-            reason = evaluation.get("reason", "")
-
-            intent_badge = "✅ 意图匹配" if intent_match else "❌ 意图不匹配"
-            intent_class = "confidence-high" if intent_match else "confidence-low"
-
-            eval_cards += f"""
-            <div class="query-item" style="margin: 10px 0; padding: 15px; background: white; border: 1px solid #e5e7eb; border-radius: 8px;">
-                <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
-                    <div class="query-text" style="flex: 1;">{query}</div>
-                    <div style="display: flex; gap: 10px; align-items: center;">
-                        <span class="confidence-badge {intent_class}" style="margin: 0;">{intent_badge}</span>
-                        <span class="confidence-badge confidence-medium" style="margin: 0;">相关性: {relevance_score:.2f}</span>
-                    </div>
-                </div>
-                <div style="color: #666; font-size: 13px; line-height: 1.6; background: #f8f9fa; padding: 10px; border-radius: 4px;">
-                    {reason}
-                </div>
-            </div>
-            """
-
-        if eval_cards:
-            # 使用可折叠区域包装每个候选词的推荐词列表,添加唯一ID
-            results_html += make_collapsible(
-                f"候选词: {candidate} ({len(evaluations)} 个推荐词)",
-                eval_cards,
-                collapsed=True,
-                section_id=f"step{step_num}-candidate-{idx}"
-            )
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item">
-                    <div class="info-label">候选query数</div>
-                    <div class="info-value">{candidate_count}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">总推荐词数</div>
-                    <div class="info-value">{sum(len(r.get('evaluations', [])) for r in results)}</div>
-                </div>
-            </div>
-            {results_html}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_filter_qualified_queries(step):
-    """渲染筛选合格推荐词步骤"""
-    data = step.get("data", {})
-    input_count = data.get("input_evaluation_count", 0)
-    qualified_count = data.get("qualified_count", 0)
-    min_relevance = data.get("min_relevance_score", 0.7)
-    all_queries = data.get("all_queries", [])
-
-    # 如果没有all_queries,使用旧的qualified_queries
-    if not all_queries:
-        all_queries = data.get("qualified_queries", [])
-
-    # 分离合格和不合格的查询
-    qualified_html = ""
-    unqualified_html = ""
-
-    for item in all_queries:
-        query = item.get("query", "")
-        from_candidate = item.get("from_candidate", "")
-        intent_match = item.get("intent_match", False)
-        relevance_score = item.get("relevance_score", 0)
-        reason = item.get("reason", "")
-        is_qualified = item.get("is_qualified", True)  # 默认为True以兼容旧数据
-
-        intent_badge = "✅ 意图匹配" if intent_match else "❌ 意图不匹配"
-        intent_class = "confidence-high" if intent_match else "confidence-low"
-
-        # 根据相关性分数确定badge颜色
-        if relevance_score >= 0.8:
-            score_class = "confidence-high"
-        elif relevance_score >= 0.6:
-            score_class = "confidence-medium"
-        else:
-            score_class = "confidence-low"
-
-        # 确定边框颜色和背景色
-        if is_qualified:
-            border_color = "#10b981"
-            bg_color = "#f0fdf4"
-            border_left_color = "#10b981"
-        else:
-            border_color = "#e5e7eb"
-            bg_color = "#f9fafb"
-            border_left_color = "#9ca3af"
-
-        query_html = f"""
-        <div class="query-item" style="margin: 15px 0; padding: 15px; background: white; border: 2px solid {border_color}; border-radius: 8px;">
-            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
-                <div style="flex: 1;">
-                    <div class="query-text">{query}</div>
-                    <div style="color: #9ca3af; font-size: 12px; margin-top: 5px;">来自候选词: {from_candidate}</div>
-                </div>
-                <div style="display: flex; gap: 10px; align-items: center;">
-                    <span class="confidence-badge {intent_class}" style="margin: 0;">{intent_badge}</span>
-                    <span class="confidence-badge {score_class}" style="margin: 0;">相关性: {relevance_score:.2f}</span>
-                </div>
-            </div>
-            <div style="color: #666; font-size: 13px; line-height: 1.6; background: {bg_color}; padding: 10px; border-radius: 4px; border-left: 3px solid {border_left_color};">
-                {reason}
-            </div>
-        </div>
-        """
-
-        if is_qualified:
-            qualified_html += query_html
-        else:
-            unqualified_html += query_html
-
-    # 构建HTML - 使用可折叠区域
-    step_num = step['step_number']
-    qualified_section = make_collapsible(
-        f"✅ 合格的推荐词 ({qualified_count})",
-        qualified_html,
-        collapsed=True,
-        section_id=f"step{step_num}-qualified"
-    ) if qualified_html else ''
-
-    unqualified_section = make_collapsible(
-        f"❌ 不合格的推荐词 ({input_count - qualified_count})",
-        unqualified_html,
-        collapsed=True,
-        section_id=f"step{step_num}-unqualified"
-    ) if unqualified_html else ''
-
-    html = f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        <div class="step-content">
-            <div class="info-grid">
-                <div class="info-item">
-                    <div class="info-label">输入推荐词数</div>
-                    <div class="info-value">{input_count}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">合格推荐词数</div>
-                    <div class="info-value">{qualified_count}</div>
-                </div>
-                <div class="info-item">
-                    <div class="info-label">最低相关性</div>
-                    <div class="info-value">{min_relevance:.2f}</div>
-                </div>
-            </div>
-            {qualified_section}
-            {unqualified_section}
-        </div>
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-    return html
-
-
-def render_generic_step(step):
-    """通用步骤渲染"""
-    data = step.get("data", {})
-
-    # 提取数据的简单展示
-    data_html = ""
-    if data:
-        data_html = "<div class='step-content'><pre style='background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px;'>"
-        import json
-        data_html += json.dumps(data, ensure_ascii=False, indent=2)[:500]  # 限制长度
-        if len(json.dumps(data)) > 500:
-            data_html += "\n..."
-        data_html += "</pre></div>"
-
-    return f"""
-    <div class="step-section">
-        <div class="step-header">
-            <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
-            <div class="step-type">{step['step_type']}</div>
-        </div>
-        {data_html}
-        <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
-    </div>
-    """
-
-
-def render_step(step):
-    """根据步骤类型渲染对应的HTML"""
-    step_type = step.get("step_type", "")
-
-    renderers = {
-        "keyword_extraction": render_keyword_extraction,
-        "level_exploration": render_level_exploration,
-        "level_analysis": render_level_analysis,
-        "query_suggestion_evaluation": render_query_suggestion_evaluation,
-        "filter_qualified_queries": render_filter_qualified_queries,
-        "search_qualified_queries": render_search_results,
-        "evaluate_search_notes": render_note_evaluations,
-        "answer_generation": render_answer_generation,
-        "final_result": render_final_result,
-    }
-
-    renderer = renderers.get(step_type)
-    if renderer:
-        return renderer(step)
-    else:
-        # 使用通用渲染显示数据
-        return render_generic_step(step)
-
-
-def generate_html(steps_json_path, output_path=None):
-    """生成HTML可视化文件"""
-    # 读取 steps.json
-    with open(steps_json_path, 'r', encoding='utf-8') as f:
-        steps_data = json.load(f)
-
-    # 生成内容
-    content_parts = [render_header(steps_data)]
-
-    for step in steps_data:
-        content_parts.append(render_step(step))
-
-    content = "\n".join(content_parts)
-
-    # 生成最终HTML(使用replace而不是format来避免CSS中的花括号问题)
-    html = HTML_TEMPLATE.replace("{content}", content)
-
-    # 确定输出路径
-    if output_path is None:
-        steps_path = Path(steps_json_path)
-        output_path = steps_path.parent / "steps_visualization.html"
-
-    # 写入文件
-    with open(output_path, 'w', encoding='utf-8') as f:
-        f.write(html)
-
-    return output_path
-
-
-def main():
-    parser = argparse.ArgumentParser(description="Steps 可视化工具")
-    parser.add_argument("steps_json", type=str, help="steps.json 文件路径")
-    parser.add_argument("-o", "--output", type=str, help="输出HTML文件路径(可选)")
-
-    args = parser.parse_args()
-
-    # 生成可视化
-    output_path = generate_html(args.steps_json, args.output)
-
-    print(f"✅ 可视化生成成功!")
-    print(f"📄 输出文件: {output_path}")
-    output_abs = Path(output_path).absolute() if isinstance(output_path, str) else output_path.absolute()
-    print(f"\n💡 在浏览器中打开查看: file://{output_abs}")
-
-
-if __name__ == "__main__":
-    main()

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott