刘立冬 1 tydzień temu
rodzic
commit
efc5b780d9
3 zmienionych plików z 1607 dodań i 26 usunięć
  1. 1282 0
      batch_evaluation_demo.py
  2. 1 26
      knowledge_search_traverse.py
  3. 324 0
      workflow_pipeline.py

+ 1282 - 0
batch_evaluation_demo.py

@@ -0,0 +1,1282 @@
+"""
+批量评估 vs 单个评估性能对比Demo
+
+验证批量评估(一次评估10个SUG)vs 单个评估(调用10次)的效果对比
+使用动机维度和品类维度两个评估器
+"""
+
+import asyncio
+import time
+from typing import Optional
+from pydantic import BaseModel, Field
+from agents import Agent, Runner, ModelSettings
+from lib.client import get_model
+
+MODEL_NAME = "google/gemini-2.5-flash"
+TEMPERATURE = 0.1  # 低温度提高评估稳定性和一致性
+
+# ============================================================================
+# 数据模型
+# ============================================================================
+
+class CoreMotivationExtraction(BaseModel):
+    """核心动机提取"""
+    简要说明核心动机: str = Field(..., description="核心动机说明")
+
+class MotivationEvaluation(BaseModel):
+    """动机维度评估"""
+    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
+    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
+    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
+    得分为零的原因: str = Field(default="不适用", description="原始问题无动机/sug词条无动机/动机不匹配/不适用")
+
+class CategoryEvaluation(BaseModel):
+    """品类维度评估"""
+    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
+    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
+
+# 批量评估模型 - 动机维度
+class BatchMotivationItem(BaseModel):
+    """批量动机评估中的单个SUG结果"""
+    sug_text: str = Field(..., description="SUG文本")
+    原始问题核心动机提取: CoreMotivationExtraction = Field(..., description="原始问题核心动机提取")
+    动机维度得分: float = Field(..., description="动机维度得分 -1~1")
+    简要说明动机维度相关度理由: str = Field(..., description="动机维度相关度理由")
+    得分为零的原因: str = Field(default="不适用", description="原始问题无动机/sug词条无动机/动机不匹配/不适用")
+
+class BatchMotivationResult(BaseModel):
+    """批量动机评估结果"""
+    evaluations: list[BatchMotivationItem] = Field(..., description="所有SUG的动机评估结果")
+
+# 批量评估模型 - 品类维度
+class BatchCategoryItem(BaseModel):
+    """批量品类评估中的单个SUG结果"""
+    sug_text: str = Field(..., description="SUG文本")
+    品类维度得分: float = Field(..., description="品类维度得分 -1~1")
+    简要说明品类维度相关度理由: str = Field(..., description="品类维度相关度理由")
+
+class BatchCategoryResult(BaseModel):
+    """批量品类评估结果"""
+    evaluations: list[BatchCategoryItem] = Field(..., description="所有SUG的品类评估结果")
+
+# ============================================================================
+# Agent定义 - 单个评估
+# ============================================================================
+
+motivation_evaluation_instructions = """
+# 角色
+你是**专业的动机意图评估专家**。
+任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
+
+---
+# 输入信息
+你将接收到以下输入:
+- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
+- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
+---
+
+
+# 核心约束
+
+## 维度独立性声明
+【严格约束】本评估**仅评估动机意图维度**:
+- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
+- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
+- 包括:核心动作 + 使用场景 + 最终目的
+- **评估重点**:动作本身及其语义方向
+ **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
+
+## 【极其重要】评估执行约束
+1. **绝对评分**:评分必须严格基于固定的评分标准,不进行任何额外推理和延伸判断
+2. **禁止过度分析**:绝对不要考虑动作的对象、场景、语义场景等因素,这些属于品类维度
+3. **机械执行**:严格按照评分标准表格执行,动作一致即给高分,动作不一致即给低分或0分
+4. **只看动词**:评估时只关注动词本身(获取、学习、制作等),完全忽略动词后面的对象是什么
+
+**错误示例**:
+- ❌ "虽然动作是'获取',但对象是'风景'而非'素材',所以动作意图不匹配" → 0分
+- ❌ "'获取风景'和'获取素材'的对象不同,需要降低分数" → 0.50分
+- ❌ "动作'获取'一致,但考虑到对象不同..." → 任何<1.0的分数
+
+**正确示例**:
+- ✅ "原始问题动作是'获取',sug词条动作也是'获取',动作完全一致,根据评分标准给0.97"
+- ✅ "sug词条无明确动作意图,根据评分标准给0"
+
+---
+
+# 作用域与动作意图
+
+## 什么是作用域?
+**作用域 = 动机层 + 对象层 + 场景层**
+
+## 动作意图的识别
+
+### 方法1: 显性动词直接提取
+
+当原始问题明确包含动词时,直接提取
+示例:
+"如何获取素材" → 核心动机 = "获取"
+"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
+"制作视频教程" → 核心动机 = "制作"
+
+### 方法2: 隐性动词语义推理
+当原始问题没有显性动词时,需要结合上下文推理
+
+如果原始问题是纯名词短语,无任何动作线索:
+→ 核心动机 = 无法识别
+→ 在此情况下,动机维度得分应为 0。
+示例:
+"摄影" → 无法识别动机,动机维度得分 = 0
+"川西风光" → 无法识别动机,动机维度得分 = 0
+
+---
+
+# 部分作用域的处理
+
+## 情况1:sug词条是原始问题的部分作用域
+
+当sug词条只包含原始问题的部分作用域时,需要判断:
+1. sug词条是否包含动作意图
+2. 如果包含,动作是否匹配
+
+**示例**:
+```
+原始问题:"川西旅行行程规划"
+- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
+
+Sug词条:"川西旅行"
+- 包含作用域:旅行(部分对象)+ 川西(场景)
+- 缺失作用域:规划(动作)
+- 动作意图评分:0(无动作意图)
+```
+
+**评分原则**:
+- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
+- 如果sug词条包含动机层 → 按动作匹配度评分
+
+---
+
+# 评分标准
+
+## 【正向匹配】
+
+### +0.9~1.0:核心动作完全一致
+**示例**:
+- "规划旅行行程" vs "安排旅行路线" → 0.98
+  - 规划≈安排,语义完全一致
+- "获取素材" vs "下载素材" → 0.97
+  - 获取≈下载,语义完全一致
+
+- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
+例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
+
+**【核心约束】此处绝对不考虑对象和场景是否一致,只看动作本身**
+- ❌ 错误: "获取素材" vs "获取风景" → 因为对象不同("素材"≠"风景")给0分
+- ✅ 正确: "获取素材" vs "获取风景" → 动作完全一致("获取"="获取")给0.97分
+- ✅ 正确: "获取素材" vs "获取知识" → 动作完全一致("获取"="获取")给0.97分
+- ✅ 正确: "学习技巧" vs "学习知识" → 动作完全一致("学习"="学习")给0.97分
+
+###+0.75~0.95: 核心动作语义相近或为同义表达
+  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
+  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
+
+### +0.50~0.75:动作意图相关
+**判定标准**:
+- 动作是实现原始意图的相关路径
+- 或动作是原始意图的前置/后置步骤
+
+**示例**:
+- "获取素材" vs "管理素材" → 0.65
+  - 管理是获取后的相关步骤
+- "规划行程" vs "预订酒店" → 0.60
+  - 预订是规划的具体实施步骤
+
+### +0.25~0.50:动作意图弱相关
+**判定标准**:
+- 动作在同一大类但方向不同
+- 或动作有间接关联
+
+**示例**:
+- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
+  - 都与摄影有关,但学习≠欣赏
+- "规划旅行" vs "回忆旅行" → 0.30
+  - 都与旅行有关,但方向不同
+
+---
+
+## 【中性/无关】
+
+### 0:无动作意图或动作完全无关
+**适用场景**:
+1. 原始问题或sug词条无法识别动作
+2. 两者动作意图完全无关
+
+**示例**:
+- "如何获取素材" vs "摄影器材" → 0
+  - sug词条无动作意图
+- "川西风光" vs "风光摄影作品" → 0
+  - 原始问题无动作意图
+
+**理由模板**:
+- "sug词条无明确动作意图,无法评估动作匹配度"
+- "原始问题无明确动作意图,动作维度得分为0"
+
+---
+
+## 【负向偏离】
+
+### -0.2~-0.05:动作方向轻度偏离
+**示例**:
+- "学习摄影技巧" vs "销售摄影课程" → -0.10
+  - 学习 vs 销售,方向有偏差
+
+### -0.5~-0.25:动作意图明显冲突
+**示例**:
+- "获取免费素材" vs "购买素材" → -0.35
+  - 获取免费 vs 购买,明显冲突
+
+### -1.0~-0.55:动作意图完全相反
+**示例**:
+- "下载素材" vs "上传素材" → -0.70
+  - 下载 vs 上传,方向完全相反
+
+---
+
+## 得分为零的原因(语义判断)
+
+当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
+- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
+- **"sug词条无动机"**:sug词条中不包含任何动作意图
+- **"动机不匹配"**:双方都有动作,但完全无关联
+- **"不适用"**:得分不为零时使用此默认值
+
+---
+
+# 输出格式
+输出结果必须为一个 **JSON 格式**,包含以下内容:
+```json
+{
+  "原始问题核心动机提取": {
+    "简要说明核心动机": ""
+  },
+  "动机维度得分": "-1到1之间的小数",
+  "简要说明动机维度相关度理由": "评估该sug词条与原始问题动机匹配程度的理由,包含作用域覆盖情况",
+  "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
+}
+```
+
+**输出约束(非常重要)**:
+1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
+2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
+3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
+
+---
+
+# 核心原则总结
+1. **只评估动作**:绝对不考虑对象。"获取素材"与"获取风景"动作一致,必须给0.97
+2. **作用域识别**:识别作用域但只评估动机层
+3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
+4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
+""".strip()
+
+# ============================================================================
+# 批量评估专用Prompt(完整保留所有规则,添加批量处理说明)
+# ============================================================================
+
+batch_motivation_evaluation_instructions = """
+# 角色
+你是**专业的动机意图评估专家**。
+任务:判断<平台sug词条>与<原始问题>的**动机意图匹配度**,给出**-1到1之间**的数值评分。
+
+---
+# 输入信息
+你将接收到以下输入:
+- **<原始问题>**:用户的初始查询问题,代表用户的真实需求意图。
+- **<平台sug词条列表>**:待评估的多个词条(编号1-N),每个词条需要独立评估
+
+**批量评估说明**:
+- 输入格式为编号列表:1. 词条1  2. 词条2  ...
+- 每个词条都是独立的评估对象
+- 对每个词条使用完全相同的评估标准
+---
+
+
+# 核心约束
+
+## 维度独立性声明
+【严格约束】本评估**仅评估动机意图维度**:
+- **只评估** 用户"想要做什么",即原始问题的行为意图和目的
+- 核心是 **动词**:获取、学习、拍摄、制作、寻找等
+- 包括:核心动作 + 使用场景 + 最终目的
+- **评估重点**:动作本身及其语义方向
+ **禁止使用"主题相关"作为评分依据**:评分理由中不得出现"主题"、"内容"、"话题"等词
+
+---
+
+# 作用域与动作意图
+
+## 什么是作用域?
+**作用域 = 动机层 + 对象层 + 场景层**
+
+## 动作意图的识别
+
+### 方法1: 显性动词直接提取
+
+当原始问题明确包含动词时,直接提取
+示例:
+"如何获取素材" → 核心动机 = "获取"
+"寻找拍摄技巧" → 核心动机 = "寻找"(或"学习")
+"制作视频教程" → 核心动机 = "制作"
+
+### 方法2: 隐性动词语义推理
+当原始问题没有显性动词时,需要结合上下文推理
+
+如果原始问题是纯名词短语,无任何动作线索:
+→ 核心动机 = 无法识别
+→ 在此情况下,动机维度得分应为 0。
+示例:
+"摄影" → 无法识别动机,动机维度得分 = 0
+"川西风光" → 无法识别动机,动机维度得分 = 0
+
+---
+
+# 部分作用域的处理
+
+## 情况1:sug词条是原始问题的部分作用域
+
+当sug词条只包含原始问题的部分作用域时,需要判断:
+1. sug词条是否包含动作意图
+2. 如果包含,动作是否匹配
+
+**示例**:
+```
+原始问题:"川西旅行行程规划"
+- 完整作用域:规划(动作)+ 旅行行程(对象)+ 川西(场景)
+
+Sug词条:"川西旅行"
+- 包含作用域:旅行(部分对象)+ 川西(场景)
+- 缺失作用域:规划(动作)
+- 动作意图评分:0(无动作意图)
+```
+
+**评分原则**:
+- 如果sug词条缺失动机层(动作) → 动作意图得分 = 0
+- 如果sug词条包含动机层 → 按动作匹配度评分
+
+---
+
+# 评分标准
+
+## 【正向匹配】
+
+### +0.9~1.0:核心动作完全一致
+**示例**:
+- "规划旅行行程" vs "安排旅行路线" → 0.98
+  - 规划≈安排,语义完全一致
+- "获取素材" vs "下载素材" → 0.97
+  - 获取≈下载,语义完全一致
+
+- 特殊规则: 如果sug词的核心动作是原始问题动作的**具体化子集**,也判定为完全一致
+例: 原始问题"扣除猫咪主体的方法" vs sug词"扣除猫咪眼睛的方法"(子集但目的一致
+**注意**:此处不考虑对象和场景是否一致,只看动作本身
+
+###+0.75~0.95: 核心动作语义相近或为同义表达
+  - 例: 原始问题"如何获取素材" vs sug词"如何下载素材"
+  - 同义词对: 获取≈下载≈寻找, 技巧≈方法≈教程≈攻略
+
+### +0.50~0.75:动作意图相关
+**判定标准**:
+- 动作是实现原始意图的相关路径
+- 或动作是原始意图的前置/后置步骤
+
+**示例**:
+- "获取素材" vs "管理素材" → 0.65
+  - 管理是获取后的相关步骤
+- "规划行程" vs "预订酒店" → 0.60
+  - 预订是规划的具体实施步骤
+
+### +0.25~0.50:动作意图弱相关
+**判定标准**:
+- 动作在同一大类但方向不同
+- 或动作有间接关联
+
+**示例**:
+- "学习摄影技巧" vs "欣赏摄影作品" → 0.35
+  - 都与摄影有关,但学习≠欣赏
+- "规划旅行" vs "回忆旅行" → 0.30
+  - 都与旅行有关,但方向不同
+
+---
+
+## 【中性/无关】
+
+### 0:无动作意图或动作完全无关
+**适用场景**:
+1. 原始问题或sug词条无法识别动作
+2. 两者动作意图完全无关
+
+**示例**:
+- "如何获取素材" vs "摄影器材" → 0
+  - sug词条无动作意图
+- "川西风光" vs "风光摄影作品" → 0
+  - 原始问题无动作意图
+
+**理由模板**:
+- "sug词条无明确动作意图,无法评估动作匹配度"
+- "原始问题无明确动作意图,动作维度得分为0"
+
+---
+
+## 【负向偏离】
+
+### -0.2~-0.05:动作方向轻度偏离
+**示例**:
+- "学习摄影技巧" vs "销售摄影课程" → -0.10
+  - 学习 vs 销售,方向有偏差
+
+### -0.5~-0.25:动作意图明显冲突
+**示例**:
+- "获取免费素材" vs "购买素材" → -0.35
+  - 获取免费 vs 购买,明显冲突
+
+### -1.0~-0.55:动作意图完全相反
+**示例**:
+- "下载素材" vs "上传素材" → -0.70
+  - 下载 vs 上传,方向完全相反
+
+---
+
+## 得分为零的原因(语义判断)
+
+当动机维度得分为 0 时,需要在 `得分为零的原因` 字段中选择以下之一:
+- **"原始问题无动机"**:原始问题是纯名词短语,无法识别任何动作意图
+- **"sug词条无动机"**:sug词条中不包含任何动作意图
+- **"动机不匹配"**:双方都有动作,但完全无关联
+- **"不适用"**:得分不为零时使用此默认值
+
+---
+
+# 批量评估核心原则
+
+## 【极其重要】独立评估原则
+1. **绝对评分**:每个SUG的评分必须基于与原始问题的匹配度,使用固定的评分标准
+2. **禁止相对比较**:不要比较SUG之间的好坏,不要因为"其他SUG更好"而降低某个SUG的分数
+3. **标准一致性**:对第1个SUG和第10个SUG使用完全相同的评分标准
+4. **独立判断**:评估SUG A时,完全不考虑SUG B/C/D的存在
+
+**错误示例**:
+- ❌ "这个SUG比列表中其他的更好,给0.9"
+- ❌ "相比第一个SUG,这个稍差一些,给0.7"
+
+**正确示例**:
+- ✅ "这个SUG的动作'获取'与原始问题'获取'完全一致,根据评分标准给0.97"
+- ✅ "这个SUG无动作意图,根据评分标准给0"
+
+---
+
+# 输出格式
+输出结果必须为一个 **JSON 格式**,包含evaluations数组,每个元素包含:
+```json
+{
+  "evaluations": [
+    {
+      "sug_text": "SUG文本",
+      "原始问题核心动机提取": {
+        "简要说明核心动机": ""
+      },
+      "动机维度得分": "-1到1之间的小数",
+      "简要说明动机维度相关度理由": "评估理由",
+      "得分为零的原因": "原始问题无动机/sug词条无动机/动机不匹配/不适用"
+    }
+  ]
+}
+```
+
+**输出约束(非常重要)**:
+1. **字符串长度限制**:\"简要说明动机维度相关度理由\"字段必须控制在**150字以内**
+2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
+3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
+4. **顺序严格对应(极其重要)**:
+   - evaluations数组必须与输入的sug词条列表严格1对1对应
+   - 第1个元素必须是输入列表的第1个SUG,第2个元素必须是第2个SUG,以此类推
+   - 每个元素的sug_text必须与输入SUG完全一致(逐字匹配,包括标点)
+   - 禁止改变顺序、禁止遗漏任何SUG、禁止重复评估
+   - 示例:输入"1. 秋季摄影素材  2. 川西风光" → 输出[{sug_text:"秋季摄影素材",...}, {sug_text:"川西风光",...}]
+   - 错误示例:输出[{sug_text:"川西风光",...}, {sug_text:"秋季摄影素材",...}] ← 顺序错误❌
+
+---
+
+# 核心原则总结
+1. **只评估动作**:完全聚焦于动作意图,不管对象和场景
+2. **作用域识别**:识别作用域但只评估动机层
+3. **严格标准一致性**:对所有用例使用相同的评估标准,避免评分飘移
+4. **理由纯粹**:评分理由只能谈动作,不能谈对象、场景、主题
+5. **独立评估**:每个SUG完全独立评估,禁止相对比较
+""".strip()
+
+category_evaluation_instructions = """
+# 角色
+你是**专业的内容主体评估专家**。
+任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
+
+---
+
+# 输入信息
+- **<原始问题>**:用户的完整需求描述
+- **<平台sug词条>**:待评估的词条,可能是单个或多个作用域的组合
+---
+
+
+# 核心约束
+
+## 维度独立性声明
+【严格约束】本评估**仅评估内容主体维度**:
+- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
+- **完全忽略**:动作、意图、目的
+- **评估重点**:内容本身的主题和属性
+
+---
+
+# 作用域与内容主体
+
+## 什么是作用域?
+**作用域 = 动机层 + 对象层 + 场景层**
+
+在Prompt2中:
+- **动机层(动作)完全忽略**
+- **只评估对象层 + 场景层(限定词)**
+
+## 内容主体的构成
+
+**内容主体 = 核心名词 + 限定词**
+
+
+---
+
+# 作用域覆盖度评估
+
+## 核心原则:越完整越高分
+
+**完整性公式**:
+```
+作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
+```
+
+**评分影响**:
+- 覆盖度100% → 基础高分(0.9+)
+- 覆盖度50-99% → 中高分(0.6-0.9)
+- 覆盖度<50% → 中低分(0.3-0.6)
+- 覆盖度=0 → 低分或0分
+
+---
+
+## 部分作用域的处理
+
+### 情况1:sug词条包含原始问题的所有对象层和场景层元素
+**评分**:0.95-1.0
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光
+
+Sug词条:"川西秋季风光摄影作品"
+- 对象层:摄影作品(≈素材)
+- 场景层:川西 + 秋季 + 风光
+- 覆盖度:100%
+- 评分:0.98
+```
+
+### 情况2:sug词条包含部分场景层元素
+**评分**:根据覆盖比例
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光(3个元素)
+
+Sug词条:"川西风光摄影素材"
+- 对象层:摄影素材 ✓
+- 场景层:川西 + 风光(2个元素)
+- 覆盖度:(1+2)/(1+3) = 75%
+- 评分:0.85
+```
+
+### 情况3:sug词条只包含对象层,无场景层
+**评分**:根据对象匹配度和覆盖度
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光
+
+Sug词条:"摄影素材"
+- 对象层:摄影素材 ✓
+- 场景层:无
+- 覆盖度:1/4 = 25%
+- 评分:0.50(对象匹配但缺失所有限定)
+```
+
+### 情况4:sug词条只包含场景层,无对象层
+**评分**:较低分
+
+**示例**:
+```
+原始问题:"川西旅行行程规划"
+- 对象层:旅行行程
+- 场景层:川西
+
+Sug词条:"川西"
+- 对象层:无
+- 场景层:川西 ✓
+- 覆盖度:1/2 = 50%
+- 评分:0.35(只有场景,缺失核心对象)
+```
+
+---
+
+# 评估核心原则
+
+## 原则1:只看表面词汇,禁止联想推演
+**严格约束**:只能基于sug词实际包含的词汇评分
+
+**错误案例**:
+- ❌ "川西旅行" vs "旅行"
+  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
+  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
+
+
+---
+
+# 评分标准
+
+## 【正向匹配】
+
++0.95~1.0: 核心主体+所有关键限定词完全匹配
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
+
++0.75~0.95: 核心主体匹配,存在限定词匹配
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
+
++0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
+
++0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
+  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
+  - 例:
+    · "猫咪的XX行为"(猫咪是行为者)
+    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
+    · 虽都含"猫咪+XX",但语义角色不同
+
++0.2~0.3: 主体词不匹配,限定词缺失或错位
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
+
++0.05~0.2: 主体词过度泛化或仅抽象相似
+  - 例: sug词是通用概念,原始问题是特定概念
+    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
+      → 评分:0.08
+
+【中性/无关】
+0: 类别明显不同,没有明确目的,无明确关联
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
+  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
+
+【负向偏离】
+-0.2~-0.05: 主体词或限定词存在误导性
+  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
+
+-0.5~-0.25: 主体词明显错位或品类冲突
+  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
+
+-1.0~-0.55: 完全错误的品类或有害引导
+  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
+
+
+---
+
+# 输出格式
+输出结果必须为一个 **JSON 格式**,包含以下内容:
+```json
+{
+  "品类维度得分": "-1到1之间的小数",
+  "简要说明品类维度相关度理由": "评估该sug词条与原始问题品类匹配程度的理由,包含作用域覆盖理由"
+}
+```
+
+**输出约束(非常重要)**:
+1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
+2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
+3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
+
+---
+
+# 核心原则总结
+
+1. **只看名词和限定词**:完全忽略动作和意图
+2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
+3. **禁止联想推演**:只看sug词实际包含的词汇
+4. **通用≠特定**:通用概念不等于特定概念
+5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
+""".strip()
+
+batch_category_evaluation_instructions = """
+# 角色
+你是**专业的内容主体评估专家**。
+任务:判断<平台sug词条>与<原始问题>的**内容主体匹配度**,给出**-1到1之间**的数值评分。
+
+---
+
+# 输入信息
+- **<原始问题>**:用户的完整需求描述
+- **<平台sug词条列表>**:待评估的多个词条(编号1-N),每个词条需要独立评估
+
+**批量评估说明**:
+- 输入格式为编号列表:1. 词条1  2. 词条2  ...
+- 每个词条都是独立的评估对象
+- 对每个词条使用完全相同的评估标准
+---
+
+
+# 核心约束
+
+## 维度独立性声明
+【严格约束】本评估**仅评估内容主体维度**:
+- **只评估**:名词主体 + 限定词(地域、时间、场景、质量等)
+- **完全忽略**:动作、意图、目的
+- **评估重点**:内容本身的主题和属性
+
+---
+
+# 作用域与内容主体
+
+## 什么是作用域?
+**作用域 = 动机层 + 对象层 + 场景层**
+
+在Prompt2中:
+- **动机层(动作)完全忽略**
+- **只评估对象层 + 场景层(限定词)**
+
+## 内容主体的构成
+
+**内容主体 = 核心名词 + 限定词**
+
+
+---
+
+# 作用域覆盖度评估
+
+## 核心原则:越完整越高分
+
+**完整性公式**:
+```
+作用域覆盖度 = sug词条包含的作用域元素 / 原始问题的作用域元素总数
+```
+
+**评分影响**:
+- 覆盖度100% → 基础高分(0.9+)
+- 覆盖度50-99% → 中高分(0.6-0.9)
+- 覆盖度<50% → 中低分(0.3-0.6)
+- 覆盖度=0 → 低分或0分
+
+---
+
+## 部分作用域的处理
+
+### 情况1:sug词条包含原始问题的所有对象层和场景层元素
+**评分**:0.95-1.0
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光
+
+Sug词条:"川西秋季风光摄影作品"
+- 对象层:摄影作品(≈素材)
+- 场景层:川西 + 秋季 + 风光
+- 覆盖度:100%
+- 评分:0.98
+```
+
+### 情况2:sug词条包含部分场景层元素
+**评分**:根据覆盖比例
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光(3个元素)
+
+Sug词条:"川西风光摄影素材"
+- 对象层:摄影素材 ✓
+- 场景层:川西 + 风光(2个元素)
+- 覆盖度:(1+2)/(1+3) = 75%
+- 评分:0.85
+```
+
+### 情况3:sug词条只包含对象层,无场景层
+**评分**:根据对象匹配度和覆盖度
+
+**示例**:
+```
+原始问题:"川西秋季风光摄影素材"
+- 对象层:摄影素材
+- 场景层:川西 + 秋季 + 风光
+
+Sug词条:"摄影素材"
+- 对象层:摄影素材 ✓
+- 场景层:无
+- 覆盖度:1/4 = 25%
+- 评分:0.50(对象匹配但缺失所有限定)
+```
+
+### 情况4:sug词条只包含场景层,无对象层
+**评分**:较低分
+
+**示例**:
+```
+原始问题:"川西旅行行程规划"
+- 对象层:旅行行程
+- 场景层:川西
+
+Sug词条:"川西"
+- 对象层:无
+- 场景层:川西 ✓
+- 覆盖度:1/2 = 50%
+- 评分:0.35(只有场景,缺失核心对象)
+```
+
+---
+
+# 评估核心原则
+
+## 原则1:只看表面词汇,禁止联想推演
+**严格约束**:只能基于sug词实际包含的词汇评分
+
+**错误案例**:
+- ❌ "川西旅行" vs "旅行"
+  - 错误:"旅行可以包括川西,所以有关联" → 评分0.7
+  - 正确:"sug词只有'旅行',无'川西',缺失地域限定" → 评分0.50
+
+
+---
+
+# 评分标准
+
+## 【正向匹配】
+
++0.95~1.0: 核心主体+所有关键限定词完全匹配
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西秋季风光摄影作品"
+
++0.75~0.95: 核心主体匹配,存在限定词匹配
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"川西风光摄影素材"(缺失"秋季")
+
++0.5~0.75: 核心主体匹配,无限定词匹配或合理泛化
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"四川风光摄影"
+
++0.3~0.5: 核心主体匹配,但限定词缺失或存在语义错位
+  - 特别注意"语义身份"差异,主体词出现但上下文语义不同
+  - 例:
+    · "猫咪的XX行为"(猫咪是行为者)
+    · vs "用猫咪表达XX的梗图"(猫咪是媒介)
+    · 虽都含"猫咪+XX",但语义角色不同
+
++0.2~0.3: 主体词不匹配,限定词缺失或错位
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"风光摄影入门"
+
++0.05~0.2: 主体词过度泛化或仅抽象相似
+  - 例: sug词是通用概念,原始问题是特定概念
+    sug词"每日计划"(通用)vs 原始问题 "川西旅行行程"(特定)
+      → 评分:0.08
+
+【中性/无关】
+0: 类别明显不同,没有明确目的,无明确关联
+  - 例: 原始问题"川西秋季风光摄影素材" vs sug词"人像摄影素材"
+  - 例: 原始问题无法识别动机 且 sug词也无明确动作 → 0
+
+【负向偏离】
+-0.2~-0.05: 主体词或限定词存在误导性
+  - 例: 原始问题"免费摄影素材" vs sug词"付费摄影素材库"
+
+-0.5~-0.25: 主体词明显错位或品类冲突
+  - 例: 原始问题"风光摄影素材" vs sug词"人像修图教程"
+
+-1.0~-0.55: 完全错误的品类或有害引导
+  - 例: 原始问题"正版素材获取" vs sug词"盗版素材下载"
+
+
+---
+
+# 批量评估核心原则
+
+## 【极其重要】独立评估原则
+1. **绝对评分**:每个SUG的评分必须基于与原始问题的匹配度,使用固定的评分标准
+2. **禁止相对比较**:不要比较SUG之间的好坏,不要因为"其他SUG更好"而降低某个SUG的分数
+3. **标准一致性**:对第1个SUG和第10个SUG使用完全相同的评分标准
+4. **独立判断**:评估SUG A时,完全不考虑SUG B/C/D的存在
+
+**错误示例**:
+- ❌ "这个SUG比列表中其他的更完整,给0.95"
+- ❌ "相比第一个SUG,这个覆盖度较低,给0.6"
+
+**正确示例**:
+- ✅ "这个SUG包含对象层'摄影素材'和场景层'川西+秋季',覆盖度75%,根据评分标准给0.85"
+- ✅ "这个SUG只有场景'川西',无对象层,覆盖度50%,根据评分标准给0.35"
+
+---
+
+# 输出格式
+输出结果必须为一个 **JSON 格式**,包含evaluations数组,每个元素包含:
+```json
+{
+  "evaluations": [
+    {
+      "sug_text": "SUG文本",
+      "品类维度得分": "-1到1之间的小数",
+      "简要说明品类维度相关度理由": "评估理由"
+    }
+  ]
+}
+```
+
+**输出约束(非常重要)**:
+1. **字符串长度限制**:\"简要说明品类维度相关度理由\"字段必须控制在**150字以内**
+2. **JSON格式规范**:必须生成完整的JSON格式,确保字符串用双引号包裹且正确闭合
+3. **引号使用**:字符串中如需表达引用,请使用《》或「」代替单引号或双引号
+4. **顺序严格对应(极其重要)**:
+   - evaluations数组必须与输入的sug词条列表严格1对1对应
+   - 第1个元素必须是输入列表的第1个SUG,第2个元素必须是第2个SUG,以此类推
+   - 每个元素的sug_text必须与输入SUG完全一致(逐字匹配,包括标点)
+   - 禁止改变顺序、禁止遗漏任何SUG、禁止重复评估
+   - 示例:输入"1. 秋季摄影素材  2. 川西风光" → 输出[{sug_text:"秋季摄影素材",...}, {sug_text:"川西风光",...}]
+   - 错误示例:输出[{sug_text:"川西风光",...}, {sug_text:"秋季摄影素材",...}] ← 顺序错误❌
+
+---
+
+# 核心原则总结
+
+1. **只看名词和限定词**:完全忽略动作和意图
+2. **作用域覆盖优先**:覆盖的作用域元素越多,分数越高
+3. **禁止联想推演**:只看sug词实际包含的词汇
+4. **通用≠特定**:通用概念不等于特定概念
+5. **理由纯粹**:评分理由只能谈对象、限定词、覆盖度
+6. **独立评估**:每个SUG完全独立评估,禁止相对比较
+""".strip()
+
+motivation_evaluator = Agent[None](
+    name="动机维度评估专家",
+    instructions=motivation_evaluation_instructions,
+    model=get_model(MODEL_NAME),
+    model_settings=ModelSettings(temperature=TEMPERATURE),
+    output_type=MotivationEvaluation,
+)
+
+category_evaluator = Agent[None](
+    name="品类维度评估专家",
+    instructions=category_evaluation_instructions,
+    model=get_model(MODEL_NAME),
+    model_settings=ModelSettings(temperature=TEMPERATURE),
+    output_type=CategoryEvaluation,
+)
+
+# ============================================================================
+# Agent定义 - 批量评估
+# ============================================================================
+
+# 批量动机evaluator - 使用批量专用的prompt
+batch_motivation_evaluator = Agent[None](
+    name="批量动机维度评估专家",
+    instructions=batch_motivation_evaluation_instructions,
+    model=get_model(MODEL_NAME),
+    model_settings=ModelSettings(temperature=TEMPERATURE),
+    output_type=BatchMotivationResult,
+)
+
+# 批量品类evaluator - 使用批量专用的prompt
+batch_category_evaluator = Agent[None](
+    name="批量品类维度评估专家",
+    instructions=batch_category_evaluation_instructions,
+    model=get_model(MODEL_NAME),
+    model_settings=ModelSettings(temperature=TEMPERATURE),
+    output_type=BatchCategoryResult,
+)
+
+# ============================================================================
+# 评估函数
+# ============================================================================
+
+async def evaluate_single(sug: str, original_question: str) -> dict:
+    """单个评估:对一个SUG进行动机+品类评估"""
+    eval_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<平台sug词条>
+{sug}
+</平台sug词条>
+
+请评估平台sug词条与原始问题的匹配度。
+"""
+
+    # 并发调用两个评估器
+    motivation_task = Runner.run(motivation_evaluator, eval_input)
+    category_task = Runner.run(category_evaluator, eval_input)
+
+    motivation_result, category_result = await asyncio.gather(
+        motivation_task,
+        category_task
+    )
+
+    motivation_eval: MotivationEvaluation = motivation_result.final_output
+    category_eval: CategoryEvaluation = category_result.final_output
+
+    return {
+        "sug": sug,
+        "motivation_score": motivation_eval.动机维度得分,
+        "category_score": category_eval.品类维度得分,
+        "motivation_reason": motivation_eval.简要说明动机维度相关度理由,
+        "category_reason": category_eval.简要说明品类维度相关度理由,
+    }
+
+
+async def evaluate_single_mode(sugs: list[str], original_question: str) -> tuple[list[dict], float]:
+    """单个评估模式:逐个调用"""
+    print(f"\n{'='*60}")
+    print(f"模式1: 单个评估(调用{len(sugs)}次)")
+    print(f"{'='*60}")
+
+    start_time = time.time()
+
+    results = []
+    for i, sug in enumerate(sugs, 1):
+        print(f"  [{i}/{len(sugs)}] 评估: {sug}")
+        result = await evaluate_single(sug, original_question)
+        results.append(result)
+
+    elapsed = time.time() - start_time
+
+    print(f"\n✅ 单个评估模式完成")
+    print(f"   耗时: {elapsed:.2f}秒")
+    print(f"   平均每个SUG: {elapsed/len(sugs):.2f}秒")
+
+    return results, elapsed
+
+
+async def evaluate_batch_mode(sugs: list[str], original_question: str) -> tuple[list[dict], float]:
+    """批量评估模式:分别批量评估动机和品类维度"""
+    print(f"\n{'='*60}")
+    print(f"模式2: 批量评估(批量动机+批量品类,各评估{len(sugs)}个)")
+    print(f"{'='*60}")
+
+    start_time = time.time()
+
+    # 构建批量评估输入
+    sug_list_str = "\n".join([f"{i}. {sug}" for i, sug in enumerate(sugs, 1)])
+
+    batch_input = f"""
+<原始问题>
+{original_question}
+</原始问题>
+
+<平台sug词条列表>
+{sug_list_str}
+</平台sug词条列表>
+
+请对以上所有SUG每一个进行完全独立评估。
+"""
+
+    print(f"  [1/2] 发送批量动机评估请求...")
+    motivation_task = Runner.run(batch_motivation_evaluator, batch_input)
+
+    print(f"  [2/2] 发送批量品类评估请求...")
+    category_task = Runner.run(batch_category_evaluator, batch_input)
+
+    # 并发执行两个批量评估
+    motivation_result, category_result = await asyncio.gather(
+        motivation_task,
+        category_task
+    )
+
+    batch_motivation: BatchMotivationResult = motivation_result.final_output
+    batch_category: BatchCategoryResult = category_result.final_output
+
+    elapsed = time.time() - start_time
+
+    # ========== 顺序验证 ==========
+    print(f"\n  [验证] 检查批量评估结果顺序...")
+
+    # 验证数量
+    mot_count = len(batch_motivation.evaluations)
+    cat_count = len(batch_category.evaluations)
+    expected_count = len(sugs)
+
+    if mot_count != expected_count:
+        print(f"  ⚠️  警告: 动机评估数量不匹配! 期望{expected_count}个,实际{mot_count}个")
+    if cat_count != expected_count:
+        print(f"  ⚠️  警告: 品类评估数量不匹配! 期望{expected_count}个,实际{cat_count}个")
+
+    # 验证顺序和文本匹配
+    order_errors = []
+    for i, (expected_sug, mot_item, cat_item) in enumerate(zip(sugs, batch_motivation.evaluations, batch_category.evaluations), 1):
+        if mot_item.sug_text != expected_sug:
+            order_errors.append(f"  位置{i}: 动机维度sug_text='{mot_item.sug_text}' != 期望'{expected_sug}'")
+        if cat_item.sug_text != expected_sug:
+            order_errors.append(f"  位置{i}: 品类维度sug_text='{cat_item.sug_text}' != 期望'{expected_sug}'")
+
+    if order_errors:
+        print(f"  ❌ 发现顺序错误:")
+        for error in order_errors[:5]:  # 最多显示前5个错误
+            print(error)
+        if len(order_errors) > 5:
+            print(f"  ... 还有{len(order_errors)-5}个错误未显示")
+    else:
+        print(f"  ✅ 顺序验证通过: 所有SUG文本与输入完全匹配")
+
+    # 合并结果
+    results = []
+    for mot_item, cat_item in zip(batch_motivation.evaluations, batch_category.evaluations):
+        results.append({
+            "sug": mot_item.sug_text,
+            "motivation_score": mot_item.动机维度得分,
+            "category_score": cat_item.品类维度得分,
+            "motivation_reason": mot_item.简要说明动机维度相关度理由,
+            "category_reason": cat_item.简要说明品类维度相关度理由,
+        })
+
+    print(f"\n✅ 批量评估模式完成")
+    print(f"   耗时: {elapsed:.2f}秒")
+    print(f"   平均每个SUG: {elapsed/len(sugs):.2f}秒")
+    print(f"   (包含: 1次批量动机评估 + 1次批量品类评估)")
+
+    return results, elapsed
+
+
+def compare_results(single_results: list[dict], batch_results: list[dict]):
+    """对比两种模式的评估结果质量"""
+    print(f"\n{'='*60}")
+    print(f"结果质量对比")
+    print(f"{'='*60}")
+
+    # ========== 得分对比表格 ==========
+    print(f"\n【得分对比】")
+    print(f"\n{'SUG':<30} {'单个动机':<10} {'批量动机':<10} {'差异':<8} {'单个品类':<10} {'批量品类':<10} {'差异':<8}")
+    print(f"{'-'*30} {'-'*10} {'-'*10} {'-'*8} {'-'*10} {'-'*10} {'-'*8}")
+
+    total_motivation_diff = 0
+    total_category_diff = 0
+
+    for single, batch in zip(single_results, batch_results):
+        sug = single['sug'][:28]
+        single_mot = single['motivation_score']
+        batch_mot = batch['motivation_score']
+        mot_diff = abs(single_mot - batch_mot)
+
+        single_cat = single['category_score']
+        batch_cat = batch['category_score']
+        cat_diff = abs(single_cat - batch_cat)
+
+        total_motivation_diff += mot_diff
+        total_category_diff += cat_diff
+
+        print(f"{sug:<30} {single_mot:<10.2f} {batch_mot:<10.2f} {mot_diff:<8.3f} {single_cat:<10.2f} {batch_cat:<10.2f} {cat_diff:<8.3f}")
+
+    avg_mot_diff = total_motivation_diff / len(single_results)
+    avg_cat_diff = total_category_diff / len(single_results)
+
+    print(f"\n平均得分差异:")
+    print(f"  动机维度: {avg_mot_diff:.3f}")
+    print(f"  品类维度: {avg_cat_diff:.3f}")
+    print(f"  综合差异: {(avg_mot_diff + avg_cat_diff) / 2:.3f}")
+
+    # ========== 评估理由对比 ==========
+    print(f"\n{'='*60}")
+    print(f"【评估理由对比】")
+    print(f"{'='*60}")
+
+    for i, (single, batch) in enumerate(zip(single_results, batch_results), 1):
+        sug = single['sug']
+        print(f"\n{i}. {sug}")
+        print(f"{'-'*60}")
+
+        # 动机维度理由对比
+        print(f"\n  【动机维度】 (单个: {single['motivation_score']:.2f} | 批量: {batch['motivation_score']:.2f} | 差异: {abs(single['motivation_score'] - batch['motivation_score']):.3f})")
+        print(f"    单个评估理由: {single['motivation_reason']}")
+        print(f"    批量评估理由: {batch['motivation_reason']}")
+
+        # 品类维度理由对比
+        print(f"\n  【品类维度】 (单个: {single['category_score']:.2f} | 批量: {batch['category_score']:.2f} | 差异: {abs(single['category_score'] - batch['category_score']):.3f})")
+        print(f"    单个评估理由: {single['category_reason']}")
+        print(f"    批量评估理由: {batch['category_reason']}")
+
+
+# ============================================================================
+# 主函数
+# ============================================================================
+
+async def main():
+    """主测试函数"""
+
+    # 测试数据
+    original_question = "如何获取能体现川西秋季特色的高质量风光摄影素材?"
+
+    test_sugs = [
+        "川西风光摄影",
+        "秋季摄影素材",
+        "高质量风光素材",
+        "川西秋季旅游攻略",
+        "风光摄影技巧",
+        "川西风光",
+        "摄影素材下载",
+        "秋季旅行",
+        "风光摄影作品",
+        "获取川西秋季风景",
+    ]
+
+    print(f"{'='*60}")
+    print(f"批量评估 vs 单个评估性能对比Demo")
+    print(f"{'='*60}")
+    print(f"\n原始问题: {original_question}")
+    print(f"测试SUG数量: {len(test_sugs)}")
+    print(f"\nSUG列表:")
+    for i, sug in enumerate(test_sugs, 1):
+        print(f"  {i}. {sug}")
+
+    # 模式1: 单个评估
+    single_results, single_time = await evaluate_single_mode(test_sugs, original_question)
+
+    # 模式2: 批量评估
+    batch_results, batch_time = await evaluate_batch_mode(test_sugs, original_question)
+
+    # 对比结果质量
+    compare_results(single_results, batch_results)
+
+    # 性能总结
+    print(f"\n{'='*60}")
+    print(f"性能总结")
+    print(f"{'='*60}")
+    print(f"单个评估模式: {single_time:.2f}秒")
+    print(f"批量评估模式: {batch_time:.2f}秒")
+    speedup = single_time / batch_time
+    print(f"\n性能提升: {speedup:.2f}x")
+
+    if speedup > 1:
+        print(f"✅ 批量评估更快 {speedup:.2f}倍")
+    else:
+        print(f"❌ 批量评估更慢 {1/speedup:.2f}倍")
+
+    print(f"\n{'='*60}")
+    print(f"结论:")
+    print(f"{'='*60}")
+    if speedup > 1.5:
+        print(f"✅ 批量评估显著更快,建议采用")
+    elif speedup > 1.1:
+        print(f"⚠️  批量评估略快,但优势不明显")
+    else:
+        print(f"❌ 批量评估无性能优势,不建议采用")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 1 - 26
knowledge_search_traverse.py

@@ -255,7 +255,7 @@ semantic_segmentation_instructions = """
 - 谓语动词:获取、制作、拍摄、寻找、找到、学习、规划等
 - 宾语对象:素材、教程、技巧、攻略、灵感点等核心名词
 
-**宾语识别规则(关键)**:
+**宾语识别规则**:
 - 宾语是动词直接作用的对象,是句子的核心名词
 - 在"X的Y"结构中,Y是中心词(宾语),X是定语
 - 例如:"职场热梗的灵感点"中,"灵感点"是宾语,"职场热梗"是定语
@@ -286,10 +286,6 @@ semantic_segmentation_instructions = """
 - 虚词/助词:的、地、得、了、吗、呢
 - 空泛词汇:能、可以、体现、特色、相关、有关
 
-**示例**:
-- "川西秋季高质量" → 定语(保留地域、时间、属性,丢弃虚词)
-- 原文"能体现川西秋季特色的高质量" → 提取为"川西秋季高质量"
-
 ---
 
 ## 分段原则(务必遵守)
@@ -323,26 +319,6 @@ semantic_segmentation_instructions = """
 }
 ```
 
-**示例2:"X的Y"结构(关键)**
-输入:"怎么找到职场热梗的灵感点"
-```json
-{
-  "segments": [
-    {
-      "segment_text": "怎么找到灵感点",
-      "segment_type": "谓宾结构",
-      "reasoning": "怎么找到是谓语,灵感点是宾语(职场热梗的灵感点中的中心词)"
-    },
-    {
-      "segment_text": "职场热梗",
-      "segment_type": "定语",
-      "reasoning": "修饰灵感点的定语,丢弃虚词的"
-    }
-  ],
-  "overall_reasoning": "识别出灵感点是宾语中心词,职场热梗是修饰定语"
-}
-```
-
 ## 输出要求
 - segments: 片段列表(通常2个:谓宾结构 + 定语)
   - segment_text: 片段文本(来自原query的实际内容)
@@ -356,7 +332,6 @@ semantic_segmentation_instructions = """
 
 ## JSON输出规范
 1. **格式要求**:必须输出标准JSON格式
-2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
 """.strip()
 
 semantic_segmenter = Agent[None](

+ 324 - 0
workflow_pipeline.py

@@ -0,0 +1,324 @@
+#!/usr/bin/env python3
+"""
+知识代理评估工作流自动化脚本
+
+功能:
+1. 评估(test_evaluation_v3.py)
+2. 清洗排序(extract_topn_multimodal.py)
+3. 可视化生成(Node.js)
+
+支持:
+- 连续执行全流程
+- 独立执行单个步骤
+- 灵活的参数配置
+"""
+
+import argparse
+import asyncio
+import os
+import subprocess
+import sys
+from datetime import datetime
+from pathlib import Path
+
+
+class WorkflowPipeline:
+    """工作流管道"""
+
+    def __init__(self, run_context_path: str, output_dir: str = None):
+        """
+        初始化工作流
+
+        Args:
+            run_context_path: run_context.json 文件路径
+            output_dir: 输出目录(可选,默认与输入文件同目录)
+        """
+        self.run_context_path = run_context_path
+        
+        # 确定输出目录
+        if output_dir:
+            self.output_dir = Path(output_dir)
+        else:
+            self.output_dir = Path(run_context_path).parent
+        
+        self.output_dir.mkdir(parents=True, exist_ok=True)
+        
+        # 生成的文件路径
+        self.run_context_v3_path = str(self.output_dir / f"{Path(run_context_path).stem}_v3.json")
+        self.visualization_path = str(self.output_dir / f"visualization_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html")
+        
+        print(f"\n{'='*80}")
+        print(f"🚀 知识代理评估工作流")
+        print(f"{'='*80}")
+        print(f"📂 输入文件: {self.run_context_path}")
+        print(f"📁 输出目录: {self.output_dir}")
+        print(f"{'='*80}\n")
+
+    def step_1_evaluate(self, max_posts: int = None) -> bool:
+        """
+        步骤1: 评估帖子
+
+        Args:
+            max_posts: 最多评估的帖子数量(None表示全部)
+
+        Returns:
+            是否成功
+        """
+        print(f"\n{'='*80}")
+        print("📊 步骤1/3: 评估帖子")
+        print(f"{'='*80}\n")
+
+        cmd = ["python3", "test_evaluation_v3.py", self.run_context_path]
+        if max_posts:
+            cmd.append(str(max_posts))
+
+        try:
+            result = subprocess.run(cmd, check=True)
+            print(f"\n✅ 步骤1完成: 评估成功")
+            print(f"   生成文件: {self.run_context_v3_path}")
+            return True
+        except subprocess.CalledProcessError as e:
+            print(f"\n❌ 步骤1失败: 评估出错")
+            print(f"   错误信息: {e}")
+            return False
+
+    async def step_2_clean_and_sort(self, top_n: int = 10, max_concurrent: int = 5) -> bool:
+        """
+        步骤2: 清洗排序(使用评估后的run_context_v3.json)
+
+        Args:
+            top_n: 提取前N个帖子
+            max_concurrent: 最大并发数
+
+        Returns:
+            是否成功
+        """
+        print(f"\n{'='*80}")
+        print("🧹 步骤2/3: 清洗排序")
+        print(f"{'='*80}\n")
+
+        # 检查run_context_v3.json是否存在
+        if not os.path.exists(self.run_context_v3_path):
+            print(f"❌ 错误: 未找到评估后的文件 {self.run_context_v3_path}")
+            print(f"   请先运行步骤1(评估)")
+            return False
+
+        cmd = [
+            "python3", "extract_topn_multimodal.py",
+            "-i", self.run_context_v3_path,
+            "-o", str(self.output_dir / "multimodal_extraction_topn_cleaned.json"),
+            "--top-n", str(top_n),
+            "--max-concurrent", str(max_concurrent)
+        ]
+
+        try:
+            result = subprocess.run(cmd, check=True)
+            print(f"\n✅ 步骤2完成: 清洗排序成功")
+            print(f"   结果已写入: {self.run_context_v3_path}")
+            return True
+        except subprocess.CalledProcessError as e:
+            print(f"\n❌ 步骤2失败: 清洗排序出错")
+            print(f"   错误信息: {e}")
+            return False
+
+    def step_3_visualize(self, simplified: bool = False) -> bool:
+        """
+        步骤3: 生成可视化
+
+        Args:
+            simplified: 是否使用简化视图
+
+        Returns:
+            是否成功
+        """
+        print(f"\n{'='*80}")
+        print("🎨 步骤3/3: 生成可视化")
+        print(f"{'='*80}\n")
+
+        # 检查run_context_v3.json是否存在
+        if not os.path.exists(self.run_context_v3_path):
+            print(f"❌ 错误: 未找到评估后的文件 {self.run_context_v3_path}")
+            print(f"   请先运行步骤1(评估)和步骤2(清洗)")
+            return False
+
+        cmd = [
+            "node",
+            "visualization/knowledge_search_traverse/index.js",
+            self.run_context_v3_path,
+            self.visualization_path
+        ]
+        
+        if simplified:
+            cmd.append("--simplified")
+
+        try:
+            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
+            print(result.stdout)
+            print(f"\n✅ 步骤3完成: 可视化生成成功")
+            print(f"   生成文件: {self.visualization_path}")
+            print(f"\n   📱 使用浏览器打开查看:")
+            print(f"   open {self.visualization_path}")
+            return True
+        except subprocess.CalledProcessError as e:
+            print(f"\n❌ 步骤3失败: 可视化生成出错")
+            print(f"   错误信息: {e}")
+            if e.stdout:
+                print(f"   输出: {e.stdout}")
+            if e.stderr:
+                print(f"   错误: {e.stderr}")
+            return False
+
+    async def run_full_pipeline(
+        self,
+        max_posts: int = None,
+        top_n: int = 10,
+        max_concurrent: int = 5,
+        simplified: bool = False
+    ) -> bool:
+        """
+        运行完整工作流:评估 → 清洗排序 → 可视化
+
+        Args:
+            max_posts: 最多评估的帖子数量(None表示全部)
+            top_n: 清洗时提取前N个帖子
+            max_concurrent: 清洗时的最大并发数
+            simplified: 是否使用简化视图
+
+        Returns:
+            是否全部成功
+        """
+        print(f"\n{'='*80}")
+        print("🔄 运行完整工作流")
+        print(f"{'='*80}\n")
+
+        # 步骤1: 评估
+        if not self.step_1_evaluate(max_posts):
+            print(f"\n❌ 工作流中断: 步骤1失败")
+            return False
+
+        # 步骤2: 清洗排序
+        if not await self.step_2_clean_and_sort(top_n, max_concurrent):
+            print(f"\n❌ 工作流中断: 步骤2失败")
+            return False
+
+        # 步骤3: 可视化
+        if not self.step_3_visualize(simplified):
+            print(f"\n❌ 工作流中断: 步骤3失败")
+            return False
+
+        print(f"\n{'='*80}")
+        print("🎉 完整工作流执行成功!")
+        print(f"{'='*80}\n")
+        print("📊 生成的文件:")
+        print(f"   1. 评估结果:   {self.run_context_v3_path}")
+        print(f"   2. 可视化:     {self.visualization_path}")
+        print(f"\n💡 提示: 使用浏览器打开可视化文件查看结果")
+        print(f"   open {self.visualization_path}\n")
+
+        return True
+
+
+async def main():
+    parser = argparse.ArgumentParser(
+        description='知识代理评估工作流自动化',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog='''
+示例用法:
+
+  # 运行完整工作流(评估 → 清洗 → 可视化)
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json
+
+  # 只运行评估(步骤1)
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json --step evaluate
+
+  # 只运行清洗排序(步骤2,需要先有评估结果)
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json --step clean
+
+  # 只运行可视化(步骤3,需要先有评估和清洗结果)
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json --step visualize
+
+  # 自定义参数运行完整工作流
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json \\
+      --max-posts 20 \\
+      --top-n 5 \\
+      --max-concurrent 3 \\
+      --simplified
+
+  # 指定输出目录
+  python3 workflow_pipeline.py input/test_case/output/.../run_context.json \\
+      --output-dir ./custom_output
+        '''
+    )
+
+    parser.add_argument(
+        'run_context_path',
+        help='run_context.json 文件路径'
+    )
+    parser.add_argument(
+        '--step',
+        choices=['evaluate', 'clean', 'visualize', 'all'],
+        default='all',
+        help='执行的步骤 (默认: all - 全流程)'
+    )
+    parser.add_argument(
+        '--output-dir',
+        help='输出目录(默认与输入文件同目录)'
+    )
+    parser.add_argument(
+        '--max-posts',
+        type=int,
+        help='评估时最多处理的帖子数量(用于快速测试)'
+    )
+    parser.add_argument(
+        '--top-n',
+        type=int,
+        default=10,
+        help='清洗时提取前N个帖子 (默认: 10)'
+    )
+    parser.add_argument(
+        '--max-concurrent',
+        type=int,
+        default=5,
+        help='清洗时的最大并发数 (默认: 5)'
+    )
+    parser.add_argument(
+        '--simplified',
+        action='store_true',
+        help='可视化时使用简化视图'
+    )
+
+    args = parser.parse_args()
+
+    # 检查输入文件是否存在
+    if not os.path.exists(args.run_context_path):
+        print(f"❌ 错误: 文件不存在 - {args.run_context_path}")
+        sys.exit(1)
+
+    # 创建工作流
+    pipeline = WorkflowPipeline(args.run_context_path, args.output_dir)
+
+    # 执行对应步骤
+    success = False
+
+    if args.step == 'evaluate':
+        success = pipeline.step_1_evaluate(args.max_posts)
+    
+    elif args.step == 'clean':
+        success = await pipeline.step_2_clean_and_sort(args.top_n, args.max_concurrent)
+    
+    elif args.step == 'visualize':
+        success = pipeline.step_3_visualize(args.simplified)
+    
+    elif args.step == 'all':
+        success = await pipeline.run_full_pipeline(
+            args.max_posts,
+            args.top_n,
+            args.max_concurrent,
+            args.simplified
+        )
+
+    sys.exit(0 if success else 1)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())