yangxiaohui hai 1 mes
pai
achega
a9b978b5b1
Modificáronse 2 ficheiros con 4006 adicións e 0 borrados
  1. 1844 0
      sug_v6_1_2_3.py
  2. 2162 0
      visualize_steps.py

+ 1844 - 0
sug_v6_1_2_3.py

@@ -0,0 +1,1844 @@
+import asyncio
+import json
+import os
+import argparse
+from datetime import datetime
+
+from agents import Agent, Runner
+from lib.my_trace import set_trace
+from typing import Literal
+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 RunContext(BaseModel):
+    version: str = Field(..., description="当前运行的脚本版本(文件名)")
+    input_files: dict[str, str] = Field(..., description="输入文件路径映射")
+    q_with_context: str
+    q_context: str
+    q: str
+    log_url: str
+    log_dir: str
+
+    # 步骤化日志
+    steps: list[dict] = Field(default_factory=list, description="执行步骤的详细记录")
+
+    # 探索阶段记录(保留用于向后兼容)
+    keywords: list[str] | None = Field(default=None, description="提取的关键词")
+    exploration_levels: list[dict] = Field(default_factory=list, description="每一层的探索结果")
+    level_analyses: list[dict] = Field(default_factory=list, description="每一层的主Agent分析")
+
+    # 最终结果
+    final_candidates: list[str] | None = Field(default=None, description="最终选出的候选query")
+    evaluation_results: list[dict] | None = Field(default=None, description="候选query的评估结果")
+    optimization_result: dict | None = Field(default=None, description="最终优化结果对象")
+    final_output: str | None = Field(default=None, description="最终输出结果(格式化文本)")
+
+
+# ============================================================================
+# Agent 1: 关键词提取专家
+# ============================================================================
+keyword_extraction_instructions = """
+你是关键词提取专家。给定一个搜索问题(含上下文),提取出**最细粒度的关键概念**。
+
+## 提取原则
+
+1. **细粒度优先**:拆分成最小的有意义单元
+   - 不要保留完整的长句
+   - 拆分成独立的、有搜索意义的词或短语
+
+2. **保留核心维度**:
+   - 地域/对象
+   - 时间
+   - 行为/意图:获取、教程、推荐、如何等
+   - 主题/领域
+   - 质量/属性
+
+3. **去掉无意义的虚词**:的、吗、呢等
+
+4. **保留领域专有词**:不要过度拆分专业术语
+   - 如果是常见的组合词,保持完整
+
+## 输出要求
+
+输出关键词列表,按重要性排序(最核心的在前)。
+""".strip()
+
+class KeywordList(BaseModel):
+    """关键词列表"""
+    keywords: list[str] = Field(..., description="提取的关键词,按重要性排序")
+    reasoning: str = Field(..., description="提取理由")
+
+keyword_extractor = Agent[None](
+    name="关键词提取专家",
+    instructions=keyword_extraction_instructions,
+    model=get_model(MODEL_NAME),
+    output_type=KeywordList,
+)
+
+
+# ============================================================================
+# Agent 2: 层级探索分析专家
+# ============================================================================
+level_analysis_instructions = """
+你是搜索空间探索分析专家。基于当前层级的探索结果,决定下一步行动。
+
+## 你的任务
+
+分析当前已探索的词汇空间,判断:
+1. **发现了什么有价值的信号?**
+2. **是否已经可以评估候选了?**
+3. **如果还不够,下一层应该探索什么组合?**
+
+## 分析维度
+
+### 1. 信号识别(最重要)
+
+看推荐词里**出现了什么主题**:
+
+**关键问题:**
+- 哪些推荐词**最接近原始需求**?
+- 哪些推荐词**揭示了有价值的方向**(即使不完全匹配)?
+- 哪些推荐词可以作为**下一层探索的桥梁**?
+- 系统对哪些概念理解得好?哪些理解偏了?
+
+### 2. 组合策略
+
+基于发现的信号,设计下一层探索:
+
+**组合类型:**
+
+a) **关键词直接组合**
+   - 两个关键词组合成新query
+
+b) **利用推荐词作为桥梁**(重要!)
+   - 发现某个推荐词很有价值 → 直接探索这个推荐词
+   - 或在推荐词基础上加其他关键词
+
+c) **跨层级组合**
+   - 结合多层发现的有价值推荐词
+   - 组合成更复杂的query
+
+### 3. 停止条件
+
+**何时可以评估候选?**
+
+满足以下之一:
+- 推荐词中出现了**明确包含原始需求多个核心要素的query**
+- 已经探索到**足够复杂的组合**(3-4个关键词),且推荐词相关
+- 探索了**3-4层**,信息已经足够丰富
+
+**何时继续探索?**
+- 当前推荐词太泛,没有接近原始需求
+- 发现了有价值的信号,但需要进一步组合验证
+- 层数还少(< 3层)
+
+## 输出要求
+
+### 1. key_findings
+总结当前层发现的关键信息,包括:
+- 哪些推荐词最有价值?
+- 系统对哪些概念理解得好/不好?
+- 发现了什么意外的方向?
+
+### 2. promising_signals
+列出最有价值的推荐词(来自任何已探索的query),每个说明为什么有价值
+
+### 3. should_evaluate_now
+是否已经可以开始评估候选了?true/false
+
+### 4. candidates_to_evaluate
+如果should_evaluate_now=true,列出应该评估的候选query
+- 可以是推荐词
+- 可以是自己构造的组合
+
+### 5. next_combinations
+如果should_evaluate_now=false,列出下一层应该探索的query组合
+
+### 6. reasoning
+详细的推理过程
+
+## 重要原则
+
+1. **不要过早评估**:至少探索2层,除非第一层就发现了完美匹配
+2. **充分利用推荐词**:推荐词是系统给的提示,要善用
+3. **保持探索方向的多样性**:不要只盯着一个方向
+4. **识别死胡同**:如果某个方向的推荐词一直不相关,果断放弃
+""".strip()
+
+class PromisingSignal(BaseModel):
+    """有价值的推荐词信号"""
+    query: str = Field(..., description="推荐词")
+    from_level: int = Field(..., description="来自哪一层")
+    reason: str = Field(..., description="为什么有价值")
+
+class LevelAnalysis(BaseModel):
+    """层级分析结果"""
+    key_findings: str = Field(..., description="当前层的关键发现")
+    promising_signals: list[PromisingSignal] = Field(..., description="有价值的推荐词信号")
+    should_evaluate_now: bool = Field(..., description="是否应该开始评估候选")
+    candidates_to_evaluate: list[str] = Field(default_factory=list, description="如果should_evaluate_now=true,要评估的候选query列表")
+    next_combinations: list[str] = Field(default_factory=list, description="如果should_evaluate_now=false,下一层要探索的query组合")
+    reasoning: str = Field(..., description="详细的推理过程")
+
+level_analyzer = Agent[None](
+    name="层级探索分析专家",
+    instructions=level_analysis_instructions,
+    model=get_model(MODEL_NAME),
+    output_type=LevelAnalysis,
+)
+
+
+# ============================================================================
+# Agent 3: 评估专家(简化版:意图匹配 + 相关性评分)
+# ============================================================================
+eval_instructions = """
+你是搜索query评估专家。给定原始问题和推荐query,评估两个维度。
+
+## 评估目标
+
+用这个推荐query搜索,能否找到满足原始需求的内容?
+
+## 两层评分
+
+### 1. intent_match(意图匹配)= true/false
+
+推荐query的**使用意图**是否与原问题一致?
+
+**核心问题:用户搜索这个推荐词,想做什么?**
+
+**判断标准:**
+- 原问题意图:找方法?找教程?找资源/素材?找工具?看作品?
+- 推荐词意图:如果用户搜索这个词,他的目的是什么?
+
+**示例:**
+- 原问题意图="找素材"
+  - ✅ true: "素材下载"、"素材网站"、"免费素材"(都是获取素材)
+  - ❌ false: "素材制作教程"、"如何制作素材"(意图变成学习了)
+
+- 原问题意图="学教程"
+  - ✅ true: "教程视频"、"教学步骤"、"入门指南"
+  - ❌ false: "成品展示"、"作品欣赏"(意图变成看作品了)
+
+**评分:**
+- true = 意图一致,搜索推荐词能达到原问题的目的
+- false = 意图改变,搜索推荐词无法达到原问题的目的
+
+### 2. relevance_score(相关性)= 0-1 连续分数
+
+推荐query在**主题、要素、属性**上与原问题的相关程度?
+
+**评估维度:**
+- 主题相关:核心主题是否匹配?(如:摄影、旅游、美食)
+- 要素覆盖:关键要素保留了多少?(如:地域、时间、对象、工具)
+- 属性匹配:质量、风格、特色等属性是否保留?
+
+**评分参考:**
+- 0.9-1.0 = 几乎完美匹配,所有核心要素都保留
+- 0.7-0.8 = 高度相关,核心要素保留,少数次要要素缺失
+- 0.5-0.6 = 中度相关,主题匹配但多个要素缺失
+- 0.3-0.4 = 低度相关,只有部分主题相关
+- 0-0.2 = 基本不相关
+
+## 评估策略
+
+1. **先判断 intent_match**:意图不匹配直接 false,无论相关性多高
+2. **再评估 relevance_score**:在意图匹配的前提下,计算相关性
+
+## 输出要求
+
+- intent_match: true/false
+- relevance_score: 0-1 的浮点数
+- reason: 详细的评估理由,需要说明:
+  - 原问题的意图是什么
+  - 推荐词的意图是什么
+  - 为什么判断意图匹配/不匹配
+  - 相关性分数的依据(哪些要素保留/缺失)
+""".strip()
+
+class RelevanceEvaluation(BaseModel):
+    """评估反馈模型 - 意图匹配 + 相关性"""
+    intent_match: bool = Field(..., description="意图是否匹配")
+    relevance_score: float = Field(..., description="相关性分数 0-1,分数越高越相关")
+    reason: str = Field(..., description="评估理由,需说明意图判断和相关性依据")
+
+evaluator = Agent[None](
+    name="评估专家",
+    instructions=eval_instructions,
+    model=get_model(MODEL_NAME),
+    output_type=RelevanceEvaluation,
+)
+
+
+# ============================================================================
+# Agent 4: 单个帖子需求满足度评估专家
+# ============================================================================
+note_evaluation_instructions = """
+你是帖子需求满足度评估专家。给定原始问题和一个搜索到的帖子(标题+描述),判断这个帖子能否满足用户的需求。
+
+## 你的任务
+
+评估单个帖子的标题和描述,判断用户点开这个帖子后,能否找到满足原始需求的内容。
+
+## 评估维度
+
+### 1. 标题相关性(title_relevance)= 0-1 连续分数
+
+**评估标准:**
+- 标题是否与原始问题的主题相关?
+- 标题是否包含原始问题的关键要素?
+- 标题是否表明内容能解决用户的问题?
+
+**评分参考:**
+- 0.9-1.0 = 标题高度相关,明确表明能解决问题
+- 0.7-0.8 = 标题相关,包含核心要素
+- 0.5-0.6 = 标题部分相关,有关联但不明确
+- 0.3-0.4 = 标题相关性较低
+- 0-0.2 = 标题基本不相关
+
+### 2. 内容预期(content_expectation)= 0-1 连续分数
+
+**评估标准:**
+- 从描述看,内容是否可能包含用户需要的信息?
+- 描述是否展示了相关的要素或细节?
+- 描述的方向是否与用户需求一致?
+
+**评分参考:**
+- 0.9-1.0 = 描述明确表明内容高度符合需求
+- 0.7-0.8 = 描述显示内容可能符合需求
+- 0.5-0.6 = 描述有一定相关性,但不确定
+- 0.3-0.4 = 描述相关性较低
+- 0-0.2 = 描述基本不相关
+
+### 3. 需求满足度(need_satisfaction)= true/false
+
+**核心问题:用户点开这个帖子后,能否找到他需要的内容?**
+
+**判断标准:**
+- 综合标题和描述,内容是否大概率能满足需求?
+- 如果 title_relevance >= 0.7 且 content_expectation >= 0.6,一般判断为 true
+- 否则判断为 false
+
+### 4. 综合置信度(confidence_score)= 0-1 连续分数
+
+**计算方式:**
+- 可以是 title_relevance 和 content_expectation 的加权平均
+- 标题权重通常更高(如 0.6 * title + 0.4 * content)
+
+## 输出要求
+
+- title_relevance: 0-1 的浮点数
+- content_expectation: 0-1 的浮点数
+- need_satisfaction: true/false
+- confidence_score: 0-1 的浮点数
+- reason: 详细的评估理由,需要说明:
+  - 标题与原问题的相关性分析
+  - 描述的内容预期分析
+  - 为什么判断能/不能满足需求
+  - 置信度分数的依据
+
+## 重要原则
+
+1. **独立评估**:只评估这一个帖子,不考虑其他帖子
+2. **用户视角**:问"我会点开这个帖子吗?点开后能找到答案吗?"
+3. **标题优先**:标题是用户决定是否点击的关键
+4. **保守判断**:不确定时,倾向于给较低的分数
+""".strip()
+
+class NoteEvaluation(BaseModel):
+    """单个帖子评估模型"""
+    title_relevance: float = Field(..., description="标题相关性 0-1")
+    content_expectation: float = Field(..., description="内容预期 0-1")
+    need_satisfaction: bool = Field(..., description="是否满足需求")
+    confidence_score: float = Field(..., description="综合置信度 0-1")
+    reason: str = Field(..., description="详细的评估理由")
+
+note_evaluator = Agent[None](
+    name="帖子需求满足度评估专家",
+    instructions=note_evaluation_instructions,
+    model=get_model(MODEL_NAME),
+    output_type=NoteEvaluation,
+)
+
+
+# ============================================================================
+# Agent 5: 答案生成专家
+# ============================================================================
+answer_generation_instructions = """
+你是答案生成专家。基于一组满足需求的帖子,为原始问题生成一个全面、准确、有价值的答案。
+
+## 你的任务
+
+根据用户的原始问题和一组相关帖子(包含标题、描述、置信度评分),生成一个高质量的答案。
+
+## 输入信息
+
+1. **原始问题**:用户提出的具体问题
+2. **相关帖子列表**:每个帖子包含
+   - 序号(索引)
+   - 标题
+   - 描述
+   - 置信度分数
+
+## 答案要求
+
+### 1. 内容要求
+
+- **直接回答问题**:开门见山,第一段就给出核心答案
+- **结构清晰**:使用标题、列表、分段等组织内容
+- **综合多个来源**:整合多个帖子的信息,不要只依赖一个
+- **信息准确**:基于帖子内容,不要编造信息
+- **实用性**:提供可操作的建议或具体的信息
+
+### 2. 引用规范
+
+- **必须标注来源**:每个关键信息都要标注帖子索引
+- **引用格式**:使用 `[1]`、`[2]` 等标注帖子序号
+- **多来源引用**:如果多个帖子支持同一观点,使用 `[1,2,3]`
+- **引用位置**:在相关句子或段落的末尾标注
+
+### 3. 置信度使用
+
+- **优先高置信度**:优先引用置信度高的帖子
+- **交叉验证**:如果多个帖子提到相同信息,可以提高可信度
+- **标注不确定性**:如果信息来自低置信度帖子,适当标注
+
+### 4. 答案结构建议
+
+```
+【核心答案】
+直接回答用户的问题,给出最核心的信息。[引用]
+
+【详细说明】
+1. 第一个方面/要点 [引用]
+   - 具体内容
+   - 相关细节
+
+2. 第二个方面/要点 [引用]
+   - 具体内容
+   - 相关细节
+
+【补充建议/注意事项】(可选)
+其他有价值的信息或提醒。[引用]
+
+【参考帖子】
+列出所有引用的帖子编号和标题。
+```
+
+## 输出格式
+
+{
+  "answer": "生成的答案内容(Markdown格式)",
+  "cited_note_indices": [1, 2, 3],  # 引用的帖子序号列表
+  "confidence": 0.85,  # 答案的整体置信度 (0-1)
+  "summary": "一句话总结答案的核心内容"
+}
+
+## 重要原则
+
+1. **忠于原文**:不要添加帖子中没有的信息
+2. **引用透明**:让用户知道每个信息来自哪个帖子
+3. **综合性**:尽可能整合多个帖子的信息
+4. **可读性**:使用清晰的Markdown格式
+5. **质量优先**:如果帖子质量不够,可以说明信息有限
+""".strip()
+
+class AnswerGeneration(BaseModel):
+    """答案生成模型"""
+    answer: str = Field(..., description="生成的答案内容(Markdown格式)")
+    cited_note_indices: list[int] = Field(..., description="引用的帖子序号列表")
+    confidence: float = Field(..., description="答案的整体置信度 0-1")
+    summary: str = Field(..., description="一句话总结答案的核心内容")
+
+answer_generator = Agent[None](
+    name="答案生成专家",
+    instructions=answer_generation_instructions,
+    model=get_model(MODEL_NAME),
+    output_type=AnswerGeneration,
+)
+
+
+# ============================================================================
+# 日志辅助函数
+# ============================================================================
+
+def add_step(context: RunContext, step_name: str, step_type: str, data: dict):
+    """添加步骤记录"""
+    step = {
+        "step_number": len(context.steps) + 1,
+        "step_name": step_name,
+        "step_type": step_type,
+        "timestamp": datetime.now().isoformat(),
+        "data": data
+    }
+    context.steps.append(step)
+    return step
+
+
+# ============================================================================
+# 核心函数
+# ============================================================================
+
+async def extract_keywords(q: str, context: RunContext) -> KeywordList:
+    """提取关键词"""
+    print("\n[步骤 1] 正在提取关键词...")
+    result = await Runner.run(keyword_extractor, q)
+    keyword_list: KeywordList = result.final_output
+    print(f"提取的关键词:{keyword_list.keywords}")
+    print(f"提取理由:{keyword_list.reasoning}")
+
+    # 记录步骤
+    add_step(context, "提取关键词", "keyword_extraction", {
+        "input_question": q,
+        "keywords": keyword_list.keywords,
+        "reasoning": keyword_list.reasoning
+    })
+
+    return keyword_list
+
+
+async def explore_level(queries: list[str], level_num: int, context: RunContext) -> dict:
+    """探索一个层级(并发获取所有query的推荐词)"""
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] Level {level_num} 探索:{len(queries)} 个query")
+    print(f"{'='*60}")
+
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+
+    # 并发获取所有推荐词
+    async def get_single_sug(query: str):
+        print(f"  探索: {query}")
+        suggestions = xiaohongshu_api.get_recommendations(keyword=query)
+        print(f"    → {len(suggestions) if suggestions else 0} 个推荐词")
+        return {
+            "query": query,
+            "suggestions": suggestions or []
+        }
+
+    results = await asyncio.gather(*[get_single_sug(q) for q in queries])
+
+    level_data = {
+        "level": level_num,
+        "timestamp": datetime.now().isoformat(),
+        "queries": results
+    }
+
+    context.exploration_levels.append(level_data)
+
+    # 记录步骤
+    add_step(context, f"Level {level_num} 探索", "level_exploration", {
+        "level": level_num,
+        "input_queries": queries,
+        "query_count": len(queries),
+        "results": results,
+        "total_suggestions": sum(len(r['suggestions']) for r in results)
+    })
+
+    return level_data
+
+
+async def analyze_level(level_data: dict, all_levels: list[dict], original_question: str, context: RunContext) -> LevelAnalysis:
+    """分析当前层级,决定下一步"""
+    step_num = len(context.steps) + 1
+    print(f"\n[步骤 {step_num}] 正在分析 Level {level_data['level']}...")
+
+    # 构造输入
+    analysis_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<已探索的所有层级>
+{json.dumps(all_levels, ensure_ascii=False, indent=2)}
+</已探索的所有层级>
+
+<当前层级>
+Level {level_data['level']}
+{json.dumps(level_data['queries'], ensure_ascii=False, indent=2)}
+</当前层级>
+
+请分析当前探索状态,决定下一步行动。
+"""
+
+    result = await Runner.run(level_analyzer, analysis_input)
+    analysis: LevelAnalysis = result.final_output
+
+    print(f"\n分析结果:")
+    print(f"  关键发现:{analysis.key_findings}")
+    print(f"  有价值的信号:{len(analysis.promising_signals)} 个")
+    print(f"  是否评估:{analysis.should_evaluate_now}")
+
+    if analysis.should_evaluate_now:
+        print(f"  候选query:{analysis.candidates_to_evaluate}")
+    else:
+        print(f"  下一层探索:{analysis.next_combinations}")
+
+    # 保存分析结果
+    context.level_analyses.append({
+        "level": level_data['level'],
+        "timestamp": datetime.now().isoformat(),
+        "analysis": analysis.model_dump()
+    })
+
+    # 记录步骤
+    add_step(context, f"Level {level_data['level']} 分析", "level_analysis", {
+        "level": level_data['level'],
+        "key_findings": analysis.key_findings,
+        "promising_signals_count": len(analysis.promising_signals),
+        "promising_signals": [s.model_dump() for s in analysis.promising_signals],
+        "should_evaluate_now": analysis.should_evaluate_now,
+        "candidates_to_evaluate": analysis.candidates_to_evaluate if analysis.should_evaluate_now else [],
+        "next_combinations": analysis.next_combinations if not analysis.should_evaluate_now else [],
+        "reasoning": analysis.reasoning
+    })
+
+    return analysis
+
+
+async def evaluate_candidates(candidates: list[str], original_question: str, context: RunContext) -> list[dict]:
+    """评估候选query(含实际搜索验证)"""
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 评估 {len(candidates)} 个候选query")
+    print(f"{'='*60}")
+
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+    xiaohongshu_search = XiaohongshuSearch()
+
+    # 创建搜索结果保存目录
+    search_results_dir = os.path.join(context.log_dir, "search_results")
+    os.makedirs(search_results_dir, exist_ok=True)
+
+    async def evaluate_single_candidate(candidate: str, candidate_index: int):
+        print(f"\n评估候选:{candidate}")
+
+        # 为当前候选创建子目录
+        # 清理候选名称,移除不适合作为目录名的字符
+        safe_candidate_name = "".join(c if c.isalnum() or c in (' ', '_', '-') else '_' for c in candidate)
+        candidate_dir = os.path.join(search_results_dir, f"candidate_{candidate_index+1}_{safe_candidate_name[:50]}")
+        os.makedirs(candidate_dir, exist_ok=True)
+
+        # 1. 获取推荐词
+        suggestions = xiaohongshu_api.get_recommendations(keyword=candidate)
+        print(f"  获取到 {len(suggestions) if suggestions else 0} 个推荐词")
+
+        if not suggestions:
+            return {
+                "candidate": candidate,
+                "suggestions": [],
+                "evaluations": []
+            }
+
+        # 2. 评估每个推荐词(意图匹配 + 相关性)
+        async def eval_single_sug(sug: str, sug_index: int):
+            # 2.1 先进行意图和相关性评估
+            eval_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<待评估的推荐query>
+{sug}
+</待评估的推荐query>
+
+请评估该推荐query:
+1. intent_match: 意图是否匹配(true/false)
+2. relevance_score: 相关性分数(0-1)
+3. reason: 详细的评估理由
+"""
+            result = await Runner.run(evaluator, eval_input)
+            evaluation: RelevanceEvaluation = result.final_output
+
+            eval_result = {
+                "query": sug,
+                "intent_match": evaluation.intent_match,
+                "relevance_score": evaluation.relevance_score,
+                "reason": evaluation.reason,
+            }
+
+            # 2.2 如果意图匹配且相关性足够高,进行实际搜索验证
+            if evaluation.intent_match and evaluation.relevance_score >= 0.7:
+                print(f"    → 合格候选,进行实际搜索验证: {sug}")
+                try:
+                    search_result = xiaohongshu_search.search(keyword=sug)
+
+                    # 解析result字段(它是JSON字符串,需要先解析)
+                    result_str = search_result.get("result", "{}")
+                    if isinstance(result_str, str):
+                        result_data = json.loads(result_str)
+                    else:
+                        result_data = result_str
+
+                    # 格式化搜索结果:将result字段解析后再保存
+                    formatted_search_result = {
+                        "success": search_result.get("success"),
+                        "result": result_data,  # 保存解析后的数据
+                        "tool_name": search_result.get("tool_name"),
+                        "call_type": search_result.get("call_type"),
+                        "query": sug,
+                        "timestamp": datetime.now().isoformat()
+                    }
+
+                    # 保存格式化后的搜索结果到文件
+                    safe_sug_name = "".join(c if c.isalnum() or c in (' ', '_', '-') else '_' for c in sug)
+                    search_result_file = os.path.join(candidate_dir, f"sug_{sug_index+1}_{safe_sug_name[:30]}.json")
+                    with open(search_result_file, 'w', encoding='utf-8') as f:
+                        json.dump(formatted_search_result, f, ensure_ascii=False, indent=2)
+                    print(f"       搜索结果已保存: {os.path.basename(search_result_file)}")
+
+                    # 提取搜索结果的标题和描述
+                    # 正确的数据路径: result.data.data[]
+                    notes = result_data.get("data", {}).get("data", [])
+                    if notes:
+                        print(f"       开始评估 {len(notes)} 个帖子...")
+
+                        # 对每个帖子进行独立评估
+                        note_evaluations = []
+                        for note_idx, note in enumerate(notes[:10], 1):  # 只评估前10条
+                            note_card = note.get("note_card", {})
+                            title = note_card.get("display_title", "")
+                            desc = note_card.get("desc", "")
+                            note_id = note.get("id", "")
+
+                            # 构造评估输入
+                            eval_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<帖子信息>
+标题: {title}
+描述: {desc}
+</帖子信息>
+
+请评估这个帖子能否满足用户需求。
+"""
+                            # 调用评估Agent
+                            eval_result_run = await Runner.run(note_evaluator, eval_input)
+                            note_eval: NoteEvaluation = eval_result_run.final_output
+
+                            note_evaluation_record = {
+                                "note_index": note_idx,
+                                "note_id": note_id,
+                                "title": title,
+                                "desc": desc[:200],  # 只保存前200字
+                                "evaluation": {
+                                    "title_relevance": note_eval.title_relevance,
+                                    "content_expectation": note_eval.content_expectation,
+                                    "need_satisfaction": note_eval.need_satisfaction,
+                                    "confidence_score": note_eval.confidence_score,
+                                    "reason": note_eval.reason
+                                }
+                            }
+                            note_evaluations.append(note_evaluation_record)
+
+                            # 简单打印进度
+                            if note_idx % 3 == 0 or note_idx == len(notes[:10]):
+                                print(f"         已评估 {note_idx}/{len(notes[:10])} 个帖子")
+
+                        # 统计满足需求的帖子数量
+                        satisfied_count = sum(1 for ne in note_evaluations if ne["evaluation"]["need_satisfaction"])
+                        avg_confidence = sum(ne["evaluation"]["confidence_score"] for ne in note_evaluations) / len(note_evaluations) if note_evaluations else 0
+
+                        eval_result["search_verification"] = {
+                            "total_notes": len(notes),
+                            "evaluated_notes": len(note_evaluations),
+                            "satisfied_count": satisfied_count,
+                            "average_confidence": round(avg_confidence, 2),
+                            "note_evaluations": note_evaluations,
+                            "search_result_file": search_result_file
+                        }
+
+                        print(f"       评估完成: {satisfied_count}/{len(note_evaluations)} 个帖子满足需求, "
+                              f"平均置信度={avg_confidence:.2f}")
+                    else:
+                        eval_result["search_verification"] = {
+                            "total_notes": 0,
+                            "evaluated_notes": 0,
+                            "satisfied_count": 0,
+                            "average_confidence": 0.0,
+                            "note_evaluations": [],
+                            "search_result_file": search_result_file,
+                            "reason": "搜索无结果"
+                        }
+                        print(f"       搜索无结果")
+
+                except Exception as e:
+                    print(f"       搜索验证出错: {e}")
+                    eval_result["search_verification"] = {
+                        "error": str(e)
+                    }
+
+            return eval_result
+
+        evaluations = await asyncio.gather(*[eval_single_sug(s, i) for i, s in enumerate(suggestions)])
+
+        return {
+            "candidate": candidate,
+            "suggestions": suggestions,
+            "evaluations": evaluations
+        }
+
+    results = await asyncio.gather(*[evaluate_single_candidate(c, i) for i, c in enumerate(candidates)])
+
+    # 生成搜索结果汇总文件
+    summary_data = {
+        "original_question": original_question,
+        "timestamp": datetime.now().isoformat(),
+        "total_candidates": len(candidates),
+        "candidates": []
+    }
+
+    for i, result in enumerate(results):
+        candidate_summary = {
+            "index": i + 1,
+            "candidate": result["candidate"],
+            "suggestions_count": len(result["suggestions"]),
+            "verified_queries": []
+        }
+
+        for eval_item in result.get("evaluations", []):
+            if "search_verification" in eval_item and "search_result_file" in eval_item["search_verification"]:
+                sv = eval_item["search_verification"]
+                candidate_summary["verified_queries"].append({
+                    "query": eval_item["query"],
+                    "intent_match": eval_item["intent_match"],
+                    "relevance_score": eval_item["relevance_score"],
+                    "verification": {
+                        "total_notes": sv.get("total_notes", 0),
+                        "evaluated_notes": sv.get("evaluated_notes", 0),
+                        "satisfied_count": sv.get("satisfied_count", 0),
+                        "average_confidence": sv.get("average_confidence", 0.0)
+                    },
+                    "search_result_file": sv["search_result_file"]
+                })
+
+        summary_data["candidates"].append(candidate_summary)
+
+    # 保存汇总文件
+    summary_file = os.path.join(search_results_dir, "summary.json")
+    with open(summary_file, 'w', encoding='utf-8') as f:
+        json.dump(summary_data, f, ensure_ascii=False, indent=2)
+    print(f"\n搜索结果汇总已保存: {summary_file}")
+
+    context.evaluation_results = results
+
+    # 构建详细的步骤记录数据
+    step_data = {
+        "candidate_count": len(candidates),
+        "candidates": candidates,
+        "total_evaluations": sum(len(r['evaluations']) for r in results),
+        "verified_queries": sum(
+            1 for r in results
+            for e in r.get('evaluations', [])
+            if 'search_verification' in e
+        ),
+        "search_results_dir": search_results_dir,
+        "summary_file": summary_file,
+        "detailed_results": []
+    }
+
+    # 为每个候选记录详细信息
+    for result in results:
+        candidate_detail = {
+            "candidate": result["candidate"],
+            "suggestions": result["suggestions"],
+            "evaluations": []
+        }
+
+        for eval_item in result.get("evaluations", []):
+            eval_detail = {
+                "query": eval_item["query"],
+                "intent_match": eval_item["intent_match"],
+                "relevance_score": eval_item["relevance_score"],
+                "reason": eval_item["reason"]
+            }
+
+            # 如果有搜索验证,添加详细信息
+            if "search_verification" in eval_item:
+                verification = eval_item["search_verification"]
+                eval_detail["search_verification"] = {
+                    "performed": True,
+                    "total_notes": verification.get("total_notes", 0),
+                    "evaluated_notes": verification.get("evaluated_notes", 0),
+                    "satisfied_count": verification.get("satisfied_count", 0),
+                    "average_confidence": verification.get("average_confidence", 0.0),
+                    "search_result_file": verification.get("search_result_file"),
+                    "has_error": "error" in verification
+                }
+                if "error" in verification:
+                    eval_detail["search_verification"]["error"] = verification["error"]
+
+                # 保存每个帖子的评估详情
+                if "note_evaluations" in verification:
+                    eval_detail["search_verification"]["note_evaluations"] = verification["note_evaluations"]
+            else:
+                eval_detail["search_verification"] = {
+                    "performed": False,
+                    "reason": "未达到搜索验证阈值(intent_match=False 或 relevance_score<0.7)"
+                }
+
+            candidate_detail["evaluations"].append(eval_detail)
+
+        step_data["detailed_results"].append(candidate_detail)
+
+    # 记录步骤
+    add_step(context, "评估候选query", "candidate_evaluation", step_data)
+
+    return results
+
+
+# ============================================================================
+# 新的独立步骤函数(方案A)
+# ============================================================================
+
+async def step_evaluate_query_suggestions(candidates: list[str], original_question: str, context: RunContext) -> list[dict]:
+    """
+    步骤1: 评估候选query的推荐词
+
+    输入:
+    - candidates: 候选query列表
+    - original_question: 原始问题
+    - context: 运行上下文
+
+    输出:
+    - 每个候选的评估结果列表,包含:
+      - candidate: 候选query
+      - suggestions: 推荐词列表
+      - evaluations: 每个推荐词的意图匹配和相关性评分
+    """
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 评估 {len(candidates)} 个候选query的推荐词")
+    print(f"{'='*60}")
+
+    xiaohongshu_api = XiaohongshuSearchRecommendations()
+
+    async def evaluate_single_candidate(candidate: str):
+        print(f"\n评估候选:{candidate}")
+
+        # 1. 获取推荐词
+        suggestions = xiaohongshu_api.get_recommendations(keyword=candidate)
+        print(f"  获取到 {len(suggestions) if suggestions else 0} 个推荐词")
+
+        if not suggestions:
+            return {
+                "candidate": candidate,
+                "suggestions": [],
+                "evaluations": []
+            }
+
+        # 2. 评估每个推荐词(只做意图匹配和相关性评分)
+        async def eval_single_sug(sug: str):
+            eval_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<待评估的推荐query>
+{sug}
+</待评估的推荐query>
+
+请评估该推荐query:
+1. intent_match: 意图是否匹配(true/false)
+2. relevance_score: 相关性分数(0-1)
+3. reason: 详细的评估理由
+"""
+            result = await Runner.run(evaluator, eval_input)
+            evaluation: RelevanceEvaluation = result.final_output
+
+            return {
+                "query": sug,
+                "intent_match": evaluation.intent_match,
+                "relevance_score": evaluation.relevance_score,
+                "reason": evaluation.reason
+            }
+
+        evaluations = await asyncio.gather(*[eval_single_sug(s) for s in suggestions])
+
+        return {
+            "candidate": candidate,
+            "suggestions": suggestions,
+            "evaluations": evaluations
+        }
+
+    results = await asyncio.gather(*[evaluate_single_candidate(c) for c in candidates])
+
+    # 记录步骤
+    add_step(context, "评估候选query的推荐词", "query_suggestion_evaluation", {
+        "candidate_count": len(candidates),
+        "candidates": candidates,
+        "results": results,
+        "total_evaluations": sum(len(r['evaluations']) for r in results),
+        "qualified_count": sum(
+            1 for r in results
+            for e in r['evaluations']
+            if e['intent_match'] and e['relevance_score'] >= 0.7
+        )
+    })
+
+    return results
+
+
+def step_filter_qualified_queries(evaluation_results: list[dict], context: RunContext, min_relevance_score: float = 0.7) -> list[dict]:
+    """
+    步骤1.5: 筛选合格的推荐词
+
+    输入:
+    - evaluation_results: 步骤1的评估结果
+
+    输出:
+    - 合格的query列表,每个包含:
+      - query: 推荐词
+      - from_candidate: 来源候选
+      - intent_match: 意图匹配
+      - relevance_score: 相关性分数
+      - reason: 评估理由
+    """
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 筛选合格的推荐词")
+    print(f"{'='*60}")
+
+    qualified_queries = []
+    all_queries = []  # 保存所有查询,包括不合格的
+
+    for result in evaluation_results:
+        candidate = result["candidate"]
+        for eval_item in result.get("evaluations", []):
+            query_data = {
+                "query": eval_item["query"],
+                "from_candidate": candidate,
+                "intent_match": eval_item["intent_match"],
+                "relevance_score": eval_item["relevance_score"],
+                "reason": eval_item["reason"]
+            }
+
+            # 判断是否合格
+            is_qualified = (eval_item['intent_match'] is True
+                          and eval_item['relevance_score'] >= min_relevance_score)
+            query_data["is_qualified"] = is_qualified
+
+            all_queries.append(query_data)
+            if is_qualified:
+                qualified_queries.append(query_data)
+
+    # 按相关性分数降序排列
+    qualified_queries.sort(key=lambda x: x['relevance_score'], reverse=True)
+    all_queries.sort(key=lambda x: x['relevance_score'], reverse=True)
+
+    print(f"\n找到 {len(qualified_queries)} 个合格的推荐词 (共评估 {len(all_queries)} 个)")
+    if qualified_queries:
+        print(f"相关性分数范围: {qualified_queries[0]['relevance_score']:.2f} ~ {qualified_queries[-1]['relevance_score']:.2f}")
+        print("\n合格的推荐词:")
+        for idx, q in enumerate(qualified_queries[:5], 1):
+            print(f"  {idx}. {q['query']} (分数: {q['relevance_score']:.2f})")
+        if len(qualified_queries) > 5:
+            print(f"  ... 还有 {len(qualified_queries) - 5} 个")
+
+    # 记录步骤 - 保存所有查询数据
+    add_step(context, "筛选合格的推荐词", "filter_qualified_queries", {
+        "input_evaluation_count": len(all_queries),
+        "min_relevance_score": min_relevance_score,
+        "qualified_count": len(qualified_queries),
+        "qualified_queries": qualified_queries,
+        "all_queries": all_queries  # 新增:保存所有查询数据
+    })
+
+    return qualified_queries
+
+
+async def step_search_qualified_queries(qualified_queries: list[dict], context: RunContext) -> dict:
+    """
+    步骤2: 搜索合格的推荐词
+
+    输入:
+    - qualified_queries: 步骤1.5筛选出的合格query列表,每个包含:
+      - query: 推荐词
+      - from_candidate: 来源候选
+      - intent_match, relevance_score, reason
+
+    输出:
+    - 搜索结果字典,包含:
+      - searches: 每个query的搜索结果列表
+      - search_results_dir: 搜索结果保存目录
+    """
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 搜索 {len(qualified_queries)} 个合格的推荐词")
+    print(f"{'='*60}")
+
+    if not qualified_queries:
+        add_step(context, "搜索合格的推荐词", "search_qualified_queries", {
+            "qualified_count": 0,
+            "searches": []
+        })
+        return {"searches": [], "search_results_dir": None}
+
+    # 创建搜索结果保存目录
+    search_results_dir = os.path.join(context.log_dir, "search_results")
+    os.makedirs(search_results_dir, exist_ok=True)
+
+    xiaohongshu_search = XiaohongshuSearch()
+
+    # 搜索每个合格的query
+    async def search_single_query(query_info: dict, query_index: int):
+        query = query_info['query']
+        print(f"\n搜索 [{query_index+1}/{len(qualified_queries)}]: {query}")
+
+        try:
+            # 执行搜索
+            search_result = xiaohongshu_search.search(keyword=query)
+
+            # 解析result字段
+            result_str = search_result.get("result", "{}")
+            if isinstance(result_str, str):
+                result_data = json.loads(result_str)
+            else:
+                result_data = result_str
+
+            # 格式化搜索结果
+            formatted_search_result = {
+                "success": search_result.get("success"),
+                "result": result_data,
+                "tool_name": search_result.get("tool_name"),
+                "call_type": search_result.get("call_type"),
+                "query": query,
+                "timestamp": datetime.now().isoformat()
+            }
+
+            # 保存到文件
+            safe_query_name = "".join(c if c.isalnum() or c in (' ', '_', '-') else '_' for c in query)
+            query_dir = os.path.join(search_results_dir, f"query_{query_index+1}_{safe_query_name[:50]}")
+            os.makedirs(query_dir, exist_ok=True)
+
+            search_result_file = os.path.join(query_dir, "search_result.json")
+            with open(search_result_file, 'w', encoding='utf-8') as f:
+                json.dump(formatted_search_result, f, ensure_ascii=False, indent=2)
+
+            # 提取帖子列表
+            notes = result_data.get("data", {}).get("data", [])
+
+            print(f"  → 搜索成功,获得 {len(notes)} 个帖子")
+
+            # ⭐ 提取帖子摘要信息用于steps.json
+            notes_summary = []
+            for note in notes[:10]:  # 只保存前10个
+                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", {})
+
+                notes_summary.append({
+                    "note_id": note.get("id", ""),
+                    "title": note_card.get("display_title", ""),
+                    "desc": note_card.get("desc", "")[:200],
+                    "cover_image": image_list[0] if image_list else {},
+                    "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)
+                    },
+                    "user": {
+                        "nickname": user_info.get("nickname", ""),
+                        "user_id": user_info.get("user_id", "")
+                    },
+                    "type": note_card.get("type", "normal")
+                })
+
+            return {
+                "query": query,
+                "from_candidate": query_info['from_candidate'],
+                "intent_match": query_info['intent_match'],
+                "relevance_score": query_info['relevance_score'],
+                "reason": query_info['reason'],
+                "search_result_file": search_result_file,
+                "note_count": len(notes),
+                "notes": notes[:10],  # 只保存前10个用于评估
+                "notes_summary": notes_summary  # ⭐ 保存到steps.json
+            }
+
+        except Exception as e:
+            print(f"  → 搜索失败: {e}")
+            return {
+                "query": query,
+                "from_candidate": query_info['from_candidate'],
+                "intent_match": query_info['intent_match'],
+                "relevance_score": query_info['relevance_score'],
+                "reason": query_info['reason'],
+                "error": str(e),
+                "note_count": 0,
+                "notes": []
+            }
+
+    search_results = await asyncio.gather(*[search_single_query(q, i) for i, q in enumerate(qualified_queries)])
+
+    # 记录步骤
+    add_step(context, "搜索合格的推荐词", "search_qualified_queries", {
+        "qualified_count": len(qualified_queries),
+        "search_results": [
+            {
+                "query": sr['query'],
+                "from_candidate": sr['from_candidate'],
+                "note_count": sr['note_count'],
+                "search_result_file": sr.get('search_result_file'),
+                "has_error": 'error' in sr,
+                "notes_summary": sr.get('notes_summary', [])  # ⭐ 包含帖子摘要
+            }
+            for sr in search_results
+        ],
+        "search_results_dir": search_results_dir
+    })
+
+    return {
+        "searches": search_results,
+        "search_results_dir": search_results_dir
+    }
+
+
+async def step_evaluate_search_notes(search_data: dict, original_question: str, context: RunContext) -> dict:
+    """
+    步骤3: 评估搜索到的帖子
+
+    输入:
+    - search_data: 步骤2的搜索结果,包含:
+      - searches: 搜索结果列表
+      - search_results_dir: 结果目录
+
+    输出:
+    - 帖子评估结果字典,包含:
+      - note_evaluations: 每个query的帖子评估列表
+    """
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 评估搜索到的帖子")
+    print(f"{'='*60}")
+
+    search_results = search_data['searches']
+
+    if not search_results:
+        add_step(context, "评估搜索到的帖子", "evaluate_search_notes", {
+            "query_count": 0,
+            "total_notes": 0,
+            "evaluated_notes": 0,
+            "note_evaluations": []
+        })
+        return {"note_evaluations": []}
+
+    # 对每个query的帖子进行评估
+    async def evaluate_query_notes(search_result: dict, query_index: int):
+        query = search_result['query']
+        notes = search_result.get('notes', [])
+
+        if not notes or 'error' in search_result:
+            return {
+                "query": query,
+                "from_candidate": search_result['from_candidate'],
+                "note_count": 0,
+                "evaluated_notes": [],
+                "satisfied_count": 0,
+                "average_confidence": 0.0
+            }
+
+        print(f"\n评估query [{query_index+1}]: {query} ({len(notes)} 个帖子)")
+
+        # 评估每个帖子
+        note_evaluations = []
+        for note_idx, note in enumerate(notes, 1):
+            note_card = note.get("note_card", {})
+            title = note_card.get("display_title", "")
+            desc = note_card.get("desc", "")
+            note_id = note.get("id", "")
+
+            # ⭐ 提取完整帖子信息用于可视化
+            image_list = note_card.get("image_list", [])
+            cover_image = image_list[0] if image_list else {}
+            interact_info = note_card.get("interact_info", {})
+            user_info = note_card.get("user", {})
+
+            # 调用评估Agent
+            eval_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<帖子信息>
+标题: {title}
+描述: {desc}
+</帖子信息>
+
+请评估这个帖子能否满足用户需求。
+"""
+            eval_result_run = await Runner.run(note_evaluator, eval_input)
+            note_eval: NoteEvaluation = eval_result_run.final_output
+
+            note_evaluations.append({
+                "note_index": note_idx,
+                "note_id": note_id,
+                "title": title,
+                "desc": desc[:200],
+                # ⭐ 新增:完整帖子信息
+                "image_list": image_list,
+                "cover_image": cover_image,
+                "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)
+                },
+                "user": {
+                    "nickname": user_info.get("nickname", ""),
+                    "user_id": user_info.get("user_id", "")
+                },
+                "type": note_card.get("type", "normal"),
+                "note_url": f"https://www.xiaohongshu.com/explore/{note_id}",
+                "evaluation": {
+                    "title_relevance": note_eval.title_relevance,
+                    "content_expectation": note_eval.content_expectation,
+                    "need_satisfaction": note_eval.need_satisfaction,
+                    "confidence_score": note_eval.confidence_score,
+                    "reason": note_eval.reason
+                }
+            })
+
+            if note_idx % 3 == 0 or note_idx == len(notes):
+                print(f"  已评估 {note_idx}/{len(notes)} 个帖子")
+
+        # 统计
+        satisfied_count = sum(1 for ne in note_evaluations if ne["evaluation"]["need_satisfaction"])
+        avg_confidence = sum(ne["evaluation"]["confidence_score"] for ne in note_evaluations) / len(note_evaluations) if note_evaluations else 0
+
+        print(f"  → 完成:{satisfied_count}/{len(note_evaluations)} 个帖子满足需求")
+
+        return {
+            "query": query,
+            "from_candidate": search_result['from_candidate'],
+            "note_count": len(notes),
+            "evaluated_notes": note_evaluations,
+            "satisfied_count": satisfied_count,
+            "average_confidence": round(avg_confidence, 2)
+        }
+
+    # 并发评估所有query的帖子
+    all_evaluations = await asyncio.gather(*[evaluate_query_notes(sr, i) for i, sr in enumerate(search_results, 1)])
+
+    # 记录步骤
+    total_notes = sum(e['note_count'] for e in all_evaluations)
+    total_satisfied = sum(e['satisfied_count'] for e in all_evaluations)
+
+    add_step(context, "评估搜索到的帖子", "evaluate_search_notes", {
+        "query_count": len(search_results),
+        "total_notes": total_notes,
+        "total_satisfied": total_satisfied,
+        "note_evaluations": all_evaluations
+    })
+
+    return {"note_evaluations": all_evaluations}
+
+
+def step_collect_satisfied_notes(note_evaluation_data: dict) -> list[dict]:
+    """
+    步骤4: 汇总所有满足需求的帖子
+
+    输入:
+    - note_evaluation_data: 步骤3的帖子评估结果
+
+    输出:
+    - 所有满足需求的帖子列表,按置信度降序排列
+    """
+    print(f"\n{'='*60}")
+    print(f"汇总满足需求的帖子")
+    print(f"{'='*60}")
+
+    all_satisfied_notes = []
+
+    for query_eval in note_evaluation_data['note_evaluations']:
+        for note in query_eval['evaluated_notes']:
+            if note['evaluation']['need_satisfaction']:
+                all_satisfied_notes.append({
+                    "query": query_eval['query'],
+                    "from_candidate": query_eval['from_candidate'],
+                    "note_id": note['note_id'],
+                    "title": note['title'],
+                    "desc": note['desc'],
+                    # ⭐ 保留完整帖子信息
+                    "image_list": note.get('image_list', []),
+                    "cover_image": note.get('cover_image', {}),
+                    "interact_info": note.get('interact_info', {}),
+                    "user": note.get('user', {}),
+                    "type": note.get('type', 'normal'),
+                    "note_url": note.get('note_url', ''),
+                    # 评估结果
+                    "title_relevance": note['evaluation']['title_relevance'],
+                    "content_expectation": note['evaluation']['content_expectation'],
+                    "confidence_score": note['evaluation']['confidence_score'],
+                    "reason": note['evaluation']['reason']
+                })
+
+    # 按置信度降序排列
+    all_satisfied_notes.sort(key=lambda x: x['confidence_score'], reverse=True)
+
+    print(f"\n共收集到 {len(all_satisfied_notes)} 个满足需求的帖子")
+    if all_satisfied_notes:
+        print(f"置信度范围: {all_satisfied_notes[0]['confidence_score']:.2f} ~ {all_satisfied_notes[-1]['confidence_score']:.2f}")
+
+    return all_satisfied_notes
+
+
+async def step_generate_answer(satisfied_notes: list[dict], original_question: str, context: RunContext) -> dict:
+    """
+    步骤5: 基于满足需求的帖子生成答案
+
+    输入:
+    - satisfied_notes: 步骤4收集的满足需求的帖子列表
+    - original_question: 原始问题
+    - context: 运行上下文
+
+    输出:
+    - 生成的答案及相关信息
+      - answer: 答案内容(Markdown格式)
+      - cited_note_indices: 引用的帖子索引
+      - confidence: 答案置信度
+      - summary: 答案摘要
+      - cited_notes: 被引用的帖子详情
+    """
+    step_num = len(context.steps) + 1
+    print(f"\n{'='*60}")
+    print(f"[步骤 {step_num}] 基于 {len(satisfied_notes)} 个帖子生成答案")
+    print(f"{'='*60}")
+
+    if not satisfied_notes:
+        print("\n⚠️  没有满足需求的帖子,无法生成答案")
+        result = {
+            "answer": "抱歉,未找到能够回答该问题的相关内容。",
+            "cited_note_indices": [],
+            "confidence": 0.0,
+            "summary": "无可用信息",
+            "cited_notes": []
+        }
+
+        add_step(context, "生成答案", "answer_generation", {
+            "original_question": original_question,
+            "input_notes_count": 0,
+            "result": result
+        })
+
+        return result
+
+    # 构建Agent输入
+    notes_info = []
+    for idx, note in enumerate(satisfied_notes, 1):
+        notes_info.append(f"""
+【帖子 {idx}】
+标题: {note['title']}
+描述: {note['desc']}
+置信度: {note['confidence_score']:.2f}
+""".strip())
+
+    agent_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<相关帖子>
+{chr(10).join(notes_info)}
+</相关帖子>
+
+请基于以上帖子,为原始问题生成一个全面、准确的答案。
+记得在答案中使用 [1], [2] 等标注引用的帖子序号。
+""".strip()
+
+    print(f"\n📝 调用答案生成Agent...")
+    print(f"  - 可用帖子: {len(satisfied_notes)} 个")
+    print(f"  - 平均置信度: {sum(n['confidence_score'] for n in satisfied_notes) / len(satisfied_notes):.2f}")
+
+    # 调用Agent生成答案
+    result_run = await Runner.run(answer_generator, agent_input)
+    answer_result: AnswerGeneration = result_run.final_output
+
+    # 提取被引用的帖子详情
+    cited_notes = []
+    for idx in answer_result.cited_note_indices:
+        if 1 <= idx <= len(satisfied_notes):
+            note = satisfied_notes[idx - 1]
+            cited_notes.append({
+                "index": idx,
+                "note_id": note['note_id'],
+                "title": note['title'],
+                "desc": note['desc'],
+                "confidence_score": note['confidence_score'],
+                # ⭐ 新增:完整帖子信息用于可视化
+                "cover_image": note.get('cover_image', {}),
+                "interact_info": note.get('interact_info', {}),
+                "user": note.get('user', {}),
+                "note_url": note.get('note_url', ''),
+                # ⭐ 新增:评估详情
+                "title_relevance": note.get('title_relevance', 0),
+                "content_expectation": note.get('content_expectation', 0),
+                "reason": note.get('reason', '')
+            })
+
+    result = {
+        "answer": answer_result.answer,
+        "cited_note_indices": answer_result.cited_note_indices,
+        "confidence": answer_result.confidence,
+        "summary": answer_result.summary,
+        "cited_notes": cited_notes
+    }
+
+    # 打印结果
+    print(f"\n✅ 答案生成完成")
+    print(f"  - 引用帖子数: {len(answer_result.cited_note_indices)} 个")
+    print(f"  - 答案置信度: {answer_result.confidence:.2f}")
+    print(f"  - 答案摘要: {answer_result.summary}")
+
+    # 记录步骤
+    add_step(context, "生成答案", "answer_generation", {
+        "original_question": original_question,
+        "input_notes_count": len(satisfied_notes),
+        "result": result,
+        "agent_input_preview": agent_input[:500] + "..." if len(agent_input) > 500 else agent_input
+    })
+
+    return result
+
+
+def find_qualified_queries(evaluation_results: list[dict], min_relevance_score: float = 0.7) -> list[dict]:
+    """
+    查找所有合格的query(旧函数,保留兼容性)
+
+    筛选标准:
+    1. intent_match = True(必须满足)
+    2. relevance_score >= min_relevance_score
+
+    返回:按 relevance_score 降序排列
+    """
+    all_qualified = []
+
+    for result in evaluation_results:
+        for eval_item in result.get("evaluations", []):
+            if (eval_item['intent_match'] is True
+                and eval_item['relevance_score'] >= min_relevance_score):
+                all_qualified.append({
+                    "from_candidate": result["candidate"],
+                    **eval_item
+                })
+
+    # 按relevance_score降序排列
+    return sorted(all_qualified, key=lambda x: x['relevance_score'], reverse=True)
+
+
+# ============================================================================
+# 主流程
+# ============================================================================
+
+async def progressive_exploration(context: RunContext, max_levels: int = 4) -> dict:
+    """
+    渐进式探索流程 - 使用独立步骤
+
+    流程:
+    1. 提取关键词 + 渐进式探索(复用旧流程)
+    2. 步骤1: 评估候选query的推荐词
+    3. 步骤2: 搜索合格的推荐词
+    4. 步骤3: 评估搜索到的帖子
+    5. 步骤4: 汇总满足需求的帖子
+    6. 步骤5: 生成答案
+
+    Args:
+        context: 运行上下文
+        max_levels: 最大探索层数,默认4
+
+    返回格式:
+    {
+        "success": True/False,
+        "final_answer": {...},  # 生成的答案
+        "satisfied_notes": [...],  # 满足需求的帖子
+        "message": "..."
+    }
+    """
+
+    # ========== 阶段1:渐进式探索(复用旧流程找到候选query)==========
+
+    # 1.1 提取关键词
+    keyword_result = await extract_keywords(context.q, context)
+    context.keywords = keyword_result.keywords
+
+    # 1.2 渐进式探索各层级
+    current_level = 1
+    candidates_to_evaluate = []
+
+    # Level 1:单个关键词
+    level_1_queries = context.keywords[:7]
+    level_1_data = await explore_level(level_1_queries, current_level, context)
+    analysis_1 = await analyze_level(level_1_data, context.exploration_levels, context.q, context)
+
+    if analysis_1.should_evaluate_now:
+        candidates_to_evaluate.extend(analysis_1.candidates_to_evaluate)
+
+    # Level 2及以后:迭代探索
+    for level_num in range(2, max_levels + 1):
+        prev_analysis: LevelAnalysis = context.level_analyses[-1]["analysis"]
+        prev_analysis = LevelAnalysis(**prev_analysis)
+
+        if not prev_analysis.next_combinations:
+            print(f"\nLevel {level_num-1} 分析后无需继续探索")
+            break
+
+        level_data = await explore_level(prev_analysis.next_combinations, level_num, context)
+        analysis = await analyze_level(level_data, context.exploration_levels, context.q, context)
+
+        if analysis.should_evaluate_now:
+            candidates_to_evaluate.extend(analysis.candidates_to_evaluate)
+
+    if not candidates_to_evaluate:
+        return {
+            "success": False,
+            "final_answer": None,
+            "satisfied_notes": [],
+            "message": "渐进式探索未找到候选query"
+        }
+
+    print(f"\n{'='*60}")
+    print(f"渐进式探索完成,找到 {len(candidates_to_evaluate)} 个候选query")
+    print(f"{'='*60}")
+
+    # ========== 阶段2:新的独立步骤流程 ==========
+
+    # 步骤1: 评估候选query的推荐词
+    evaluation_results = await step_evaluate_query_suggestions(
+        candidates_to_evaluate,
+        context.q,
+        context
+    )
+
+    # 步骤1.5: 筛选合格的推荐词
+    qualified_queries = step_filter_qualified_queries(
+        evaluation_results,
+        context,
+        min_relevance_score=0.7
+    )
+
+    if not qualified_queries:
+        return {
+            "success": False,
+            "final_answer": None,
+            "satisfied_notes": [],
+            "message": "没有合格的推荐词"
+        }
+
+    # 步骤2: 搜索合格的推荐词
+    search_results = await step_search_qualified_queries(
+        qualified_queries,
+        context
+    )
+
+    if not search_results.get('searches'):
+        return {
+            "success": False,
+            "final_answer": None,
+            "satisfied_notes": [],
+            "message": "搜索失败"
+        }
+
+    # 步骤3: 评估搜索到的帖子
+    note_evaluation_data = await step_evaluate_search_notes(
+        search_results,
+        context.q,
+        context
+    )
+
+    # 步骤4: 汇总满足需求的帖子
+    satisfied_notes = step_collect_satisfied_notes(note_evaluation_data)
+
+    if not satisfied_notes:
+        return {
+            "success": False,
+            "final_answer": None,
+            "satisfied_notes": [],
+            "message": "未找到满足需求的帖子"
+        }
+
+    # 步骤5: 生成答案
+    final_answer = await step_generate_answer(
+        satisfied_notes,
+        context.q,
+        context
+    )
+
+    # ========== 返回最终结果 ==========
+
+    return {
+        "success": True,
+        "final_answer": final_answer,
+        "satisfied_notes": satisfied_notes,
+        "message": f"成功找到 {len(satisfied_notes)} 个满足需求的帖子,并生成答案"
+    }
+
+
+# ============================================================================
+# 输出格式化
+# ============================================================================
+
+def format_output(optimization_result: dict, context: RunContext) -> str:
+    """
+    格式化输出结果 - 用于独立步骤流程
+
+    包含:
+    - 生成的答案
+    - 引用的帖子详情
+    - 满足需求的帖子统计
+    """
+    final_answer = optimization_result.get("final_answer")
+    satisfied_notes = optimization_result.get("satisfied_notes", [])
+
+    output = f"原始问题:{context.q}\n"
+    output += f"提取的关键词:{', '.join(context.keywords or [])}\n"
+    output += f"探索层数:{len(context.exploration_levels)}\n"
+    output += f"找到满足需求的帖子:{len(satisfied_notes)} 个\n"
+    output += "\n" + "="*60 + "\n"
+
+    if final_answer:
+        output += "【生成的答案】\n\n"
+        output += final_answer.get("answer", "")
+        output += "\n\n" + "="*60 + "\n"
+
+        output += f"答案置信度:{final_answer.get('confidence', 0):.2f}\n"
+        output += f"答案摘要:{final_answer.get('summary', '')}\n"
+        output += f"引用帖子数:{len(final_answer.get('cited_note_indices', []))} 个\n"
+        output += "\n" + "="*60 + "\n"
+
+        output += "【引用的帖子详情】\n\n"
+        for cited_note in final_answer.get("cited_notes", []):
+            output += f"[{cited_note['index']}] {cited_note['title']}\n"
+            output += f"    置信度: {cited_note['confidence_score']:.2f}\n"
+            output += f"    描述: {cited_note['desc'][:100]}...\n"
+            output += f"    note_id: {cited_note['note_id']}\n\n"
+    else:
+        output += "未能生成答案\n"
+
+    return output
+
+
+# ============================================================================
+# 主函数
+# ============================================================================
+
+
+
+async def main(input_dir: str, max_levels: int = 4):
+    """
+    主函数 - 使用独立步骤流程(方案A)
+    """
+    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')
+
+    q_context = read_file_as_string(input_context_file)
+    q = read_file_as_string(input_q_file)
+    q_with_context = f"""
+<需求上下文>
+{q_context}
+</需求上下文>
+<当前问题>
+{q}
+</当前问题>
+""".strip()
+
+    # 获取当前文件名作为版本
+    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,
+        },
+        q_with_context=q_with_context,
+        q_context=q_context,
+        q=q,
+        log_dir=log_dir,
+        log_url=log_url,
+    )
+
+    # 执行渐进式探索
+    optimization_result = await progressive_exploration(run_context, max_levels=max_levels)
+
+    # 格式化输出
+    final_output = format_output(optimization_result, run_context)
+    print(f"\n{'='*60}")
+    print("最终结果")
+    print(f"{'='*60}")
+    print(final_output)
+
+    # 保存结果
+    run_context.optimization_result = optimization_result
+    run_context.final_output = final_output
+
+    # 记录最终输出步骤(新格式)
+    final_answer = optimization_result.get("final_answer")
+    satisfied_notes = optimization_result.get("satisfied_notes", [])
+
+    add_step(run_context, "生成最终结果", "final_result", {
+        "success": optimization_result["success"],
+        "message": optimization_result["message"],
+        "satisfied_notes_count": len(satisfied_notes),
+        "final_answer": final_answer,
+        "satisfied_notes_summary": [
+            {
+                "note_id": note["note_id"],
+                "title": note["title"],
+                "confidence_score": note["confidence_score"]
+            }
+            for note in satisfied_notes[:10]  # 只保存前10个摘要
+        ] if satisfied_notes else [],
+        "final_output": final_output
+    })
+
+    # 保存 RunContext 到 log_dir(不包含 steps,steps 单独保存)
+    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()
+    context_dict.pop('steps', None)  # 移除 steps,避免数据冗余
+    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}")
+
+    # 保存步骤化日志
+    steps_file_path = os.path.join(run_context.log_dir, "steps.json")
+    with open(steps_file_path, "w", encoding="utf-8") as f:
+        json.dump(run_context.steps, f, ensure_ascii=False, indent=2)
+    print(f"Steps log saved to: {steps_file_path}")
+
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="搜索query优化工具 - v6.1.2.3 独立步骤+答案生成版")
+    parser.add_argument(
+        "--input-dir",
+        type=str,
+        default="input/简单扣图",
+        help="输入目录路径,默认: input/简单扣图"
+    )
+    parser.add_argument(
+        "--max-levels",
+        type=int,
+        default=4,
+        help="最大探索层数,默认: 4"
+    )
+    args = parser.parse_args()
+
+    asyncio.run(main(args.input_dir, max_levels=args.max_levels))

+ 2162 - 0
visualize_steps.py

@@ -0,0 +1,2162 @@
+#!/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(280px, 1fr));
+            gap: 15px;
+        }
+
+        .modal-image-item {
+            border-radius: 8px;
+            overflow: hidden;
+            border: 2px solid #e5e7eb;
+            transition: border-color 0.2s;
+        }
+
+        .modal-image-item:hover {
+            border-color: #2563eb;
+        }
+
+        .modal-image {
+            width: 100%;
+            height: auto;
+            display: block;
+        }
+
+        .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-dots {
+            position: absolute;
+            bottom: 8px;
+            left: 50%;
+            transform: translateX(-50%);
+            display: flex;
+            gap: 6px;
+            z-index: 15;
+        }
+
+        .carousel-dot {
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.5);
+            border: 1px solid rgba(0, 0, 0, 0.2);
+            transition: all 0.2s;
+        }
+
+        .carousel-dot.active {
+            background: white;
+            width: 24px;
+            border-radius: 4px;
+        }
+
+        /* 可折叠区域样式 */
+        .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 dots = card.querySelectorAll('.carousel-dot');
+
+                card.addEventListener('click', function(e) {
+                    // 如果点击的是图片区域,切换图片
+                    if (e.target.closest('.post-image-wrapper')) {
+                        e.stopPropagation();
+                        currentIndex = (currentIndex + 1) % images.length;
+                        if (imgElement) {
+                            imgElement.src = images[currentIndex];
+                        }
+                        // 更新指示器
+                        dots.forEach((dot, idx) => {
+                            dot.classList.toggle('active', idx === 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'):
+        image_list = [note.get('cover_image')]
+
+    images = [img.get('image_url', '') for img in image_list if img.get('image_url')]
+
+    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:
+            cover = note.get("cover_image", {})
+            cover_url = cover.get("image_url", "") if cover else ""
+            interact = note.get("interact_info", {})
+            user = note.get("user", {})
+
+            # 获取所有图片用于轮播
+            image_list = note.get('image_list', [])
+            if not image_list and cover:
+                image_list = [cover]
+            images = [img.get('image_url', '') for img in image_list if img.get('image_url')]
+
+            # 构建帖子数据用于模态框
+            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>'
+
+            # 轮播指示器
+            dots_html = ""
+            if len(images) > 1:
+                dots_html = '<div class="carousel-dots">'
+                for i in range(len(images)):
+                    active_class = ' active' if i == 0 else ''
+                    dots_html += f'<div class="carousel-dot{active_class}"></div>'
+                dots_html += '</div>'
+
+            posts_cards += f"""
+            <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
+                <div class="post-image-wrapper">
+                    {image_html}
+                    {type_badge}
+                    {dots_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:
+            cover = note.get("cover_image", {})
+            cover_url = cover.get("image_url", "") if cover else ""
+            interact = note.get("interact_info", {})
+            user = note.get("user", {})
+            evaluation = note.get("evaluation", {})
+            confidence = evaluation.get("confidence_score", 0)
+
+            # 获取所有图片用于轮播
+            image_list = note.get('image_list', [])
+            if not image_list and cover:
+                image_list = [cover]
+            images = [img.get('image_url', '') for img in image_list if img.get('image_url')]
+
+            # 构建帖子数据用于模态框
+            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>'
+
+            # 轮播指示器
+            dots_html = ""
+            if len(images) > 1:
+                dots_html = '<div class="carousel-dots">'
+                for i in range(len(images)):
+                    active_class = ' active' if i == 0 else ''
+                    dots_html += f'<div class="carousel-dot{active_class}"></div>'
+                dots_html += '</div>'
+
+            # 评估详情
+            eval_reason = evaluation.get("reason", "")
+            title_rel = evaluation.get("title_relevance", 0)
+            content_exp = evaluation.get("content_expectation", 0)
+
+            eval_details = f"""
+            <div class="evaluation-reason">
+                <strong>💡 评估理由:</strong><br>
+                {eval_reason}
+                <div class="evaluation-scores">
+                    <span class="score-item">📌 标题相关性: {title_rel:.2f}</span>
+                    <span class="score-item">📄 内容期望: {content_exp:.2f}</span>
+                </div>
+            </div>
+            """ if eval_reason else ""
+
+            # 置信度百分比
+            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}
+                    {dots_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:
+            cover = note.get("cover_image", {})
+            cover_url = cover.get("image_url", "") if cover else ""
+            interact = note.get("interact_info", {})
+            user = note.get("user", {})
+            evaluation = note.get("evaluation", {})
+            confidence = evaluation.get("confidence_score", 0)
+
+            image_list = note.get('image_list', [])
+            if not image_list and cover:
+                image_list = [cover]
+            images = [img.get('image_url', '') for img in image_list if img.get('image_url')]
+
+            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>'
+
+            dots_html = ""
+            if len(images) > 1:
+                dots_html = '<div class="carousel-dots">'
+                for i in range(len(images)):
+                    active_class = ' active' if i == 0 else ''
+                    dots_html += f'<div class="carousel-dot{active_class}"></div>'
+                dots_html += '</div>'
+
+            eval_reason = evaluation.get("reason", "")
+            title_rel = evaluation.get("title_relevance", 0)
+            content_exp = evaluation.get("content_expectation", 0)
+
+            eval_details = f"""
+            <div class="evaluation-reason">
+                <strong>💡 评估理由:</strong><br>
+                {eval_reason}
+                <div class="evaluation-scores">
+                    <span class="score-item">📌 标题相关性: {title_rel:.2f}</span>
+                    <span class="score-item">📄 内容期望: {content_exp:.2f}</span>
+                </div>
+            </div>
+            """ if eval_reason else ""
+
+            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}
+                    {dots_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:
+        cover = note.get("cover_image", {})
+        cover_url = cover.get("image_url", "") if cover else ""
+        interact = note.get("interact_info", {})
+        user = note.get("user", {})
+
+        # 获取所有图片用于轮播
+        image_list = note.get('image_list', [])
+        if not image_list and cover:
+            image_list = [cover]
+        images = [img.get('image_url', '') for img in image_list if img.get('image_url')]
+
+        # 构建帖子数据用于模态框(包含评估信息)
+        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>'
+
+        # 轮播指示器
+        dots_html = ""
+        if len(images) > 1:
+            dots_html = '<div class="carousel-dots">'
+            for i in range(len(images)):
+                active_class = ' active' if i == 0 else ''
+                dots_html += f'<div class="carousel-dot{active_class}"></div>'
+            dots_html += '</div>'
+
+        # 评估详情
+        eval_reason = note.get("reason", "")
+        title_rel = note.get("title_relevance", 0)
+        content_exp = note.get("content_expectation", 0)
+
+        eval_details = f"""
+        <div class="evaluation-reason">
+            <strong>💡 评估理由:</strong><br>
+            {eval_reason}
+            <div class="evaluation-scores">
+                <span class="score-item">📌 标题相关性: {title_rel:.2f}</span>
+                <span class="score-item">📄 内容期望: {content_exp:.2f}</span>
+            </div>
+        </div>
+        """ if eval_reason else ""
+
+        # 置信度百分比
+        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}
+                {dots_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()