瀏覽代碼

批量评估修改前备份

刘立冬 1 周之前
父節點
當前提交
dc331d8482
共有 1 個文件被更改,包括 177 次插入49 次删除
  1. 177 49
      knowledge_search_traverse.py

+ 177 - 49
knowledge_search_traverse.py

@@ -19,6 +19,10 @@ MODEL_NAME = "google/gemini-2.5-flash"
 REQUIRED_SCORE_GAIN = 0.02
 SUG_CACHE_TTL = 24 * 3600  # 24小时
 SUG_CACHE_DIR = os.path.join(os.path.dirname(__file__), "data", "sug_cache")
+# 🆕 评估缓存配置
+EVAL_CACHE_TTL = 7 * 24 * 3600  # 7天(评估结果相对稳定,可以长期缓存)
+EVAL_CACHE_DIR = os.path.join(os.path.dirname(__file__), "data", "eval_cache")
+EVAL_CACHE_FILE = os.path.join(EVAL_CACHE_DIR, "evaluation_cache.json")
 from script.search_recommendations.xiaohongshu_search_recommendations import XiaohongshuSearchRecommendations
 from script.search.xiaohongshu_search import XiaohongshuSearch
 from script.search.xiaohongshu_detail import XiaohongshuDetail
@@ -240,84 +244,116 @@ class SemanticSegmentation(BaseModel):
 
 
 semantic_segmentation_instructions = """
-你是语义分段专家。给定一个搜索query,将其拆分成3种语义维度的片段。
+你是语义分段专家。给定一个搜索query,将其拆分成2种语义维度的片段。
 
 ## 语义定义
 
-### 1. 动作目标
-**定义**:表达"想要做什么"的完整语义单元
+### 1. 谓宾结构
+**定义**:谓语(含疑问词+动词)+ 宾语的完整语义单元
 **包含**:
-- 疑问词:如何、什么、哪些、有没有、怎么
-- 动作词:获取、制作、拍摄、寻找、学习、规划
+- 疑问词:如何、什么、哪里、怎样、怎么(保留,表达方法/教程意图)
+- 谓语动词:获取、制作、拍摄、寻找、找到、学习、规划等
+- 宾语对象:素材、教程、技巧、攻略、灵感点等核心名词
 
-**示例**:
-- "如何获取" → 动作目标
-- "有哪些" → 动作目标(完整疑问表达)
-- "寻找" → 动作目标
+**宾语识别规则(关键)**:
+- 宾语是动词直接作用的对象,是句子的核心名词
+- 在"X的Y"结构中,Y是中心词(宾语),X是定语
+- 例如:"职场热梗的灵感点"中,"灵感点"是宾语,"职场热梗"是定语
 
-**注意**:疑问词和动作词应该作为一个完整的语义单元,不要拆分
+**示例**:
+- "如何获取风光摄影素材" → 谓宾结构(疑问词+动词+宾语完整单元)
+- "怎么找到灵感点" → 谓宾结构(疑问词+动词+宾语)
+- "制作视频教程" → 谓宾结构(动词+宾语)
+- "寻找拍摄技巧" → 谓宾结构(动词+宾语)
 
----
+**注意**:
+- 谓宾结构必须包含宾语,不能只有动词
+- 宾语是动作的直接对象,是句子主干的一部分
+- 复合名词宾语(如"风光摄影素材")保持完整
 
-### 2. 修饰词
-**定义**:对中心名词的限定和修饰的完整语义单元,多个连续的修饰词可以组合成一个片段作为修饰词
-**包含**:"X的Y"结构中"X的"是修饰词,"Y"是中心名词,X只能拆分为一个分段
 ---
 
-### 3. 中心名词
-**定义**:动作的核心对象,被修饰词修饰
+### 2. 定语
+**定义**:对谓宾结构的修饰和限定
 **包含**:
-- 核心名词:素材、梗图、表情包、教程
-- 复合名词:摄影素材、风光摄影素材、表情包梗图
-
----
+- 地域限定:川西、北京、日本、成都
+- 时间限定:秋季、冬季、春节、2024
+- 属性限定:高质量、专业、简单、初级
+- 其他修饰:风格、类型等有搜索价值的实词
 
-## 分段原则(务必遵守)
+**丢弃规则**(重要):
+以下内容必须丢弃,不要作为片段:
+- 虚词/助词:的、地、得、了、吗、呢
+- 空泛词汇:能、可以、体现、特色、相关、有关
 
-1. **语义完整性**:每个片段应该是完整的语义单元
-   - 动作目标:疑问词+动作词应该合并
-   - 修饰词:连续的修饰成分可以合并
-   - 中心名词:复合名词保持完整,一个语句中务必只能分段出一个中心名词
+**示例**:
+- "川西秋季高质量" → 定语(保留地域、时间、属性,丢弃虚词)
+- 原文"能体现川西秋季特色的高质量" → 提取为"川西秋季高质量"
 
-2. **维度互斥**:每个片段只能属于一种维度
+---
 
-3. **保留原文**:片段文本必须保留原query中的字符,不得改写
+## 分段原则(务必遵守)
 
+1. **语义完整性**:谓宾结构必须完整,可独立理解
+2. **定语精简**:定语只保留有搜索价值的实词,丢弃虚词和空泛词汇
+3. **保留原文**:片段文本必须来自原query中的实际内容
 4. **顺序保持**:片段顺序应与原query一致
 
 ---
 
 ## 输出格式(严格遵守)
+
+**示例1:含定语的完整query**
+输入:"如何获取能体现川西秋季特色的高质量风光摄影素材?"
 ```json
 {
   "segments": [
     {
-      "segment_text": "分段结果文本1",
-      "segment_type": "语义类型",
-      "reasoning": ""
+      "segment_text": "如何获取风光摄影素材",
+      "segment_type": "谓宾结构",
+      "reasoning": "如何获取表达方法意图,风光摄影素材是宾语对象"
     },
     {
-      "segment_text": "分段结果文本2",
-      "segment_type": "语义类型",
-      "reasoning": ""
-    },
+      "segment_text": "川西秋季高质量",
+      "segment_type": "定语",
+      "reasoning": "川西是地域定语,秋季是时间定语,高质量是属性定语,丢弃虚词能、体现、特色、的"
+    }
+  ],
+  "overall_reasoning": "将query拆分为谓宾主干和定语修饰两部分"
+}
+```
+
+**示例2:"X的Y"结构(关键)**
+输入:"怎么找到职场热梗的灵感点"
+```json
+{
+  "segments": [
     {
-      "segment_text": "分段结果文本3",
-      "segment_type": "语义类型",
-      "reasoning": ""
+      "segment_text": "怎么找到灵感点",
+      "segment_type": "谓宾结构",
+      "reasoning": "怎么找到是谓语,灵感点是宾语(职场热梗的灵感点中的中心词)"
     },
+    {
+      "segment_text": "职场热梗",
+      "segment_type": "定语",
+      "reasoning": "修饰灵感点的定语,丢弃虚词的"
+    }
   ],
-  "overall_reasoning": ""
+  "overall_reasoning": "识别出灵感点是宾语中心词,职场热梗是修饰定语"
 }
 ```
 
 ## 输出要求
-- segments: 片段列表
-  - segment_text: 片段文本(必须来自原query)
-  - segment_type: 语义维度(动作目标/修饰词/中心名词
+- segments: 片段列表(通常2个:谓宾结构 + 定语)
+  - segment_text: 片段文本(来自原query的实际内容
+  - segment_type: 语义维度(谓宾结构/定语
   - reasoning: 为什么这样分段
 - overall_reasoning: 整体分段思路
 
+## 特殊情况处理
+- 如果query没有明显的定语修饰,只输出谓宾结构
+- 如果query只有名词短语无动词,可以将核心名词作为"谓宾结构",其他作为"定语"
+
 ## JSON输出规范
 1. **格式要求**:必须输出标准JSON格式
 2. **引号规范**:字符串中如需表达引用,使用书名号《》或「」,不要使用英文引号或中文引号""
@@ -342,13 +378,38 @@ class WordSegmentation(BaseModel):
     reasoning: str = Field(..., description="分词理由")
 
 word_segmentation_instructions = """
-你是分词专家。给定一个query,将其拆分成有意义的最小单元。
+你是分词专家。给定一个query,将其拆分成有意义的搜索单元。
 
 ## 分词原则
-1. 保留有搜索意义的词汇
-2. 拆分成独立的概念
-3. 保留专业术语的完整性
-4. 去除虚词(的、吗、呢等),但保留疑问词(如何、为什么、怎样等)
+
+1. **互不重叠原则**:分词必须是互不重叠的最小单元
+   - 每个词不能包含其他词的字符
+   - 所有词连起来应该覆盖原query的全部有效字符
+   - 后续系统会自动生成各种组合,无需在此阶段重复
+
+2. **不可分割的完整单元**:以下组合作为最小单元,不可再拆分
+   - 疑问词+动词:怎么找到、如何获取、怎样制作、如何学习
+   - 独立概念的复合词:表情包、灵感点、攻略
+
+3. **可拆分的复合词**:以下组合应拆分到最小有意义单元
+   - 多概念名词:风光摄影素材 → ["风光", "摄影", "素材"]
+   - 地域+时间:川西秋季 → ["川西", "秋季"]
+
+4. **去除虚词**:的、地、得、了、吗、呢等虚词应该丢弃
+
+## 示例
+
+**输入1**: "怎么找到灵感点"
+**输出**: ["怎么找到", "灵感点"]
+**理由**: "怎么找到"作为不可分割的疑问+动词单元,"灵感点"是独立概念,二者互不重叠。系统会自动生成组合。
+
+**输入2**: "如何获取风光摄影素材"
+**输出**: ["如何获取", "风光", "摄影", "素材"]
+**理由**: "如何获取"是不可分割单元,"风光摄影素材"拆分为最小单元。系统会自动组合出"风光摄影"、"摄影素材"等。
+
+**输入3**: "川西秋季高质量"
+**输出**: ["川西", "秋季", "高质量"]
+**理由**: 三个独立的修饰词,互不重叠。系统会自动组合出"川西秋季"等。
 
 ## 输出要求
 返回分词列表和分词理由。
@@ -1837,6 +1898,66 @@ def get_suggestions_with_cache(keyword: str, api: XiaohongshuSearchRecommendatio
     return suggestions
 
 
+# ============================================================================
+# 评估缓存持久化函数
+# ============================================================================
+
+def _ensure_eval_cache_dir():
+    """确保评估缓存目录存在"""
+    os.makedirs(EVAL_CACHE_DIR, exist_ok=True)
+
+
+def load_eval_cache() -> dict[str, tuple[float, str]]:
+    """从持久化缓存中读取评估结果
+
+    Returns:
+        dict[str, tuple[float, str]]: {文本: (得分, 理由)}
+    """
+    if not os.path.exists(EVAL_CACHE_FILE):
+        print(f"📦 评估缓存文件不存在,将创建新缓存")
+        return {}
+
+    try:
+        # 检查缓存文件年龄
+        file_age = time.time() - os.path.getmtime(EVAL_CACHE_FILE)
+        if file_age > EVAL_CACHE_TTL:
+            print(f"⚠️  评估缓存已过期({file_age / 86400:.1f}天),清空缓存")
+            return {}
+
+        with open(EVAL_CACHE_FILE, 'r', encoding='utf-8') as f:
+            data = json.load(f)
+
+        # 转换回tuple格式
+        cache = {k: tuple(v) for k, v in data.items()}
+        print(f"📦 加载评估缓存: {len(cache)} 条记录(年龄: {file_age / 3600:.1f}小时)")
+        return cache
+
+    except Exception as e:
+        print(f"⚠️  评估缓存加载失败: {e},使用空缓存")
+        return {}
+
+
+def save_eval_cache(cache: dict[str, tuple[float, str]]):
+    """保存评估缓存到磁盘
+
+    Args:
+        cache: {文本: (得分, 理由)}
+    """
+    try:
+        _ensure_eval_cache_dir()
+
+        # 转换为可序列化格式
+        data = {k: list(v) for k, v in cache.items()}
+
+        with open(EVAL_CACHE_FILE, 'w', encoding='utf-8') as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+        print(f"💾 评估缓存已保存: {len(cache)} 条记录 -> {EVAL_CACHE_FILE}")
+
+    except Exception as e:
+        print(f"⚠️  评估缓存保存失败: {e}")
+
+
 def get_ordered_subsets(words: list[str], min_len: int = 1) -> list[list[str]]:
     """
     生成words的所有有序子集(可跳过但不可重排)
@@ -2942,7 +3063,7 @@ async def run_round(
 
     # 2.2 并发评估所有sug(使用信号量限制并发数)
     # 每个 evaluate_sug 内部会并发调用 2 个 LLM,所以这里限制为 5,实际并发 LLM 请求为 10
-    MAX_CONCURRENT_EVALUATIONS = 5
+    MAX_CONCURRENT_EVALUATIONS = 30  # 🚀 性能优化:从5提升到30,并发评估能力提升6倍
     semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
 
     async def evaluate_sug(sug: Sug) -> Sug:
@@ -3447,7 +3568,7 @@ async def initialize_v2(o: str, context: RunContext) -> list[Segment]:
     # 2. 对每个segment拆词并评估
     print(f"\n[步骤2] 对每个segment拆词并评估...")
 
-    MAX_CONCURRENT_EVALUATIONS = 5
+    MAX_CONCURRENT_EVALUATIONS = 30  # 🚀 性能优化:从5提升到30,并发评估能力提升6倍
     semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
 
     async def process_segment(segment: Segment) -> Segment:
@@ -3591,7 +3712,7 @@ async def run_round_v2(
         "input_query_count": len(query_input)
     }
 
-    MAX_CONCURRENT_EVALUATIONS = 5
+    MAX_CONCURRENT_EVALUATIONS = 30  # 🚀 性能优化:从5提升到30,并发评估能力提升6倍
     semaphore = asyncio.Semaphore(MAX_CONCURRENT_EVALUATIONS)
 
     # 步骤1: 为 query_input 请求SUG
@@ -4061,6 +4182,9 @@ async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7,
     # 日志目录
     log_dir = os.path.join(input_dir, "output", version_name, current_time)
 
+    # 🆕 加载持久化评估缓存
+    evaluation_cache = load_eval_cache()
+
     # 创建运行上下文
     run_context = RunContext(
         version=version,
@@ -4073,6 +4197,7 @@ async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7,
         o=o,
         log_dir=log_dir,
         log_url=log_url,
+        evaluation_cache=evaluation_cache,  # 🆕 使用加载的缓存
     )
 
     # 创建日志目录
@@ -4180,6 +4305,9 @@ async def main(input_dir: str, max_rounds: int = 2, sug_threshold: float = 0.7,
                 print(f"❌ 可视化生成失败")
 
     finally:
+        # 🆕 保存评估缓存
+        save_eval_cache(run_context.evaluation_cache)
+
         # 恢复stdout
         sys.stdout = original_stdout
         log_file.close()