Просмотр исходного кода

组合词来源选择所有有实线连接的点、更新检索词生成策略

刘立冬 3 недель назад
Родитель
Сommit
324f66177b
5 измененных файлов с 298 добавлено и 29 удалено
  1. 8 26
      enhanced_search_v2.py
  2. 287 0
      llm_evaluator.py
  3. 1 1
      run_stage7.py
  4. 1 1
      stage7_analyzer.py
  5. 1 1
      stage7_api_client.py

+ 8 - 26
enhanced_search_v2.py

@@ -15,7 +15,6 @@ import subprocess
 from typing import Dict, List, Any, Optional, Set, Tuple
 from datetime import datetime
 from concurrent.futures import ThreadPoolExecutor, as_completed
-from itertools import combinations
 
 from openrouter_client import OpenRouterClient
 from llm_evaluator import LLMEvaluator
@@ -1212,35 +1211,18 @@ class EnhancedSearchV2:
 
             logger.info(f"    候选词数量: {len(candidate_words)} (限制: {max_candidates})")
 
-            # 生成组合(简化策略:只生成 base_word + 1词 和 base_word + 2词)
-            combinations_for_base = []
-            max_additional_words = 2  # 最多额外添加2个词(生成 base_word + 1词 和 base_word + 2词)
-
-            for length in range(1, min(max_additional_words + 1, len(candidate_words) + 1)):
-                for combo in combinations(candidate_words, length):
-                    search_phrase = base_word + ' ' + ' '.join(combo)
-                    combinations_for_base.append({
-                        'search_word': search_phrase,
-                        'base_word': base_word,
-                        'candidate_words': list(combo),
-                        'combo_length': length + 1  # +1 因为包含 base_word
-                    })
-
-            logger.info(f"    生成 {len(combinations_for_base)} 个组合")
-
-            # LLM评估
-            logger.info(f"    开始LLM评估...")
-            evaluated = self.llm_evaluator.evaluate_search_words_in_batches(
+            # LLM生成query(新方式:直接让LLM基于候选词生成query)
+            logger.info(f"    使用LLM生成query(中心词: {base_word})...")
+            evaluated = self.llm_evaluator.generate_queries_from_candidates(
                 original_feature=original_feature,
-                search_words=[c['search_word'] for c in combinations_for_base],
-                batch_size=50,
-                base_word=base_word  # 传递中心词,确保生成的 source_word 包含 base_word
+                base_word=base_word,
+                candidate_words=candidate_words,
+                max_queries=10
             )
 
-            # 选出Top 10
+            # 选出Top 10(已经由LLM生成方法控制数量)
             top_10 = evaluated[:10]
-            max_score = top_10[0]['score'] if top_10 else 0.0
-            logger.info(f"    评估完成,Top 10 最高分: {max_score:.3f}")
+            logger.info(f"    生成完成,共 {len(top_10)} 个query")
 
             # 保存分组结果 - 每个base_word有自己的available_words
             grouped_results.append({

+ 287 - 0
llm_evaluator.py

@@ -267,6 +267,293 @@ class LLMEvaluator:
 
         return all_results
 
+    def generate_queries_from_candidates(
+        self,
+        original_feature: str,
+        base_word: str,
+        candidate_words: List[str],
+        max_queries: int = 10
+    ) -> List[Dict[str, Any]]:
+        """
+        基于中心词和候选词列表,让LLM生成搜索query
+
+        Args:
+            original_feature: 原始特征名称
+            base_word: 中心词
+            candidate_words: 候选词列表
+            max_queries: 最大query数量
+
+        Returns:
+            query数组(与旧格式兼容)
+        """
+        logger.info(f"LLM生成query(中心词: {base_word}, 候选词: {len(candidate_words)}个)")
+
+        candidate_words_str = "、".join(candidate_words)
+
+        prompt = f"""# 角色定位
+你是"内容创作搜索顾问"。任务:
+围绕中心词为主体,并结合待选词中明显的 高频词/高权重词,生成完整、不重不漏、可检索的 query。
+核心流程:
+先判断搜索类型(具体案例 / 案例集合) → 再围绕"中心词 + 高权重词(如果有)"生成 query → 完整覆盖 → 去重 → 输出。
+
+# 输入
+中心词:{base_word}
+待选词:{candidate_words_str}
+
+# 核心原则
+1.中心词优先原则:所有 query 必须围绕中心词构造,中心词在所有 query 的出现率必须 ≥ 80%
+2.高权重词优先构造:对待选词做频次分析:同义/包含关系归并后,出现次数最高的词为"高权重主体词",若存在高权重词 → 所有 query 必须围绕 中心词 + 高权重词,若无高权重词 → 使用中心词 + 去重词合理组合
+3. **去重不漏** - 去重同义、保留关键差异、所有有效组合需覆盖
+4. **query是问题** - 不包含多模态信息,例如XXvlog、视频等,如原始输入中不存在此类信息则不加入query
+5. 组合需有语义逻辑:不能随机堆词,query 必须是自然、可搜索的有真实含义的问题句
+6.主体与场景必须出现之一:若中心词是场景 → 等同主体优先级
+
+# 处理流程
+
+## Step 1: 词汇分析与去重
+对输入的词汇进行分类和合并:
+
+**分类维度**:
+- **主体类**: 内容核心对象(猫咪、美食、旅行地)
+- **手法类**: 创作表现方式(拟人化、对比、测评)
+- **特征类**: 风格特点(反差、温馨、搞笑)
+- **场景类**: 具体情境(穿衣服、戴墨镜、吃饭)
+- **行为类**: 创作动作(分享、记录、教程)
+
+**核心主体识别规则**:
+中心词默认最高优先级
+待选词中出现频次最高者 = 高权重主体词
+Query 必须围绕:中心词 + 高权重词(如果有)
+
+**去重规则**:
+- 同义词: 猫/猫咪 → 猫咪
+- 包含关系归纳: 宠物猫咪/猫咪 → 猫咪
+- 修饰词判断: 若修饰词不改变核心意图则去除,若改变则保留
+
+**原词保留原则**:
+- 所有 query 中语言必须来自清洗后的词汇,允许添加:
+  连接词(的、和、与)
+  必要动词(分享、展示、记录)
+  集合词(有哪些、合集、大全)
+- 禁止任何同义替换。: 猫咪✗改为宠物猫/小猫, 服饰✗改为穿搭/衣服
+
+## Step 2: 词汇关系分析
+**目标**: 确定哪些词汇可以合理组合
+
+**关系判断规则**:
+1. **强关联** 可直接组合:
+- 中心词 + 高权重主体词(必选)
+- 中心词 + 特征词
+- 高权重主体词 + 特征词
+
+2. **中等关联** 需通过主体连接:
+   - 中心词 + 主体 + 特征
+
+3. **禁止组合**
+- 特征 + 特征;特征独立成句;与中心词无关系的随机组合
+
+## Step 3: 判断搜索类型
+
+根据词汇的**具体化程度**判断搜索粒度:
+
+1、THEN → 类型: specific_case (具体案例)
+IF 满足以下任一条件:
+  1. 包含具体场景/道具/动作
+  2. 词汇组合后可想象出明确画面
+  3. 描述足够详细,指向单一呈现形式
+
+**创作者需求**:
+找一个可以直接参考模仿的成品案例,想看"就是这样的内容"
+
+2、THEN → 类型: case_collection (案例集合)
+ELSE IF 满足以下条件:
+  1. 只有主体 + 手法/特征,缺少具体场景
+  2. 词汇组合较抽象,无法想象单一画面
+  3. 需要看到多个变化形式
+
+**创作者需求**:
+了解这一类内容有哪些玩法,想看"这种类型都有什么"
+
+## Step 4: 生成完整Query列表
+
+### 核心原则: 词汇组合完整覆盖
+**中心词权重规则**:
+- 中心词必须出现在≥80%的query中
+**组词逻辑规则**:
+- 每个query必须遵循词汇关系矩阵中的强关联或中等关联
+- 找案例不是找方法,query需明确找案例、案例集、集合、有哪些等适配case和案例集类型的词汇
+
+**严格去重规则**:
+- 提取每个query的核心要素: [主体]+[场景/手法]+[特征/行为]
+- 两个query的核心要素若完全相同或高度重叠(≥2个要素相同),则判定为重复
+- 生成每个新query时立即与已生成的query对比,重复则舍弃
+- 判断标准: 搜索意图是否相同,而非文字是否相同
+
+**覆盖策略**:
+1. **主干组合** - 主体+核心手法/场景 必须覆盖
+2. **特征叠加** - 在主干上叠加不同特征词
+3. **表述多样** - 同一组合用不同表述方式
+4. **避免重复** - 去除语义相同的query
+
+**原词保真规则**:
+- 只能使用去重后词汇清单中的词汇
+- 不允许用同义词替换原词
+- 允许添加的词: 连接词(的、和、与)、必要动词(分享、展示)、集合词(有哪些、合集)
+- 每生成一个query立即检查是否包含不在清单中的新概念词,若有则删除该query
+
+#### A类Query生成规则(具体案例)
+
+**结构**: 主体 + 具体场景/道具 + 手法/特征
+**长度**: 6-15字
+**语言风格**: 描述性、具象化
+
+**数量要求**: 根据去重后词汇丰富度生成,确保覆盖所有有意义的组合
+- 词汇简单(2-8个): 生成2-4个query
+- 词汇中等(9-12个): 生成4-6个query
+- 词汇丰富(12+个): 生成6-10个query
+
+#### B类Query生成规则(案例集合)
+
+**结构**: 主体 + 手法/特征 + 集合词
+**长度**: 6-12字
+
+**数量要求**: 根据去重后词汇丰富度生成
+- 词汇简单(2-8个): 生成2-4个query
+- 词汇中等(8-10个): 生成4-6个query
+- 词汇丰富(10+个): 生成6-10个query
+
+## 质量检查标准
+
+生成query后,必须进行覆盖度检查:
+
+**检查清单**:
+1. 词汇覆盖检查:
+   - 列出所有去重后的词汇
+   - 标注每个词汇出现在哪些query中
+   - 确保去重后每个词汇至少被使用1次
+
+2. 组合覆盖检查:
+- 逐个检查query是否符合词汇关系矩阵
+- 检查是否存在弱关联或无关联的词汇组合
+- 弱关联组合 → 删除或重写
+
+3. 重复检查:
+- 提取每个query的核心要素
+- 两两对比核心要素
+- 核心要素一致 → 删除
+
+4.原词保真检查
+- 拆解每个query的词汇
+- 验证每个实词是否在去重后清单中
+- 允许存在的词: 连接词、动词、集合词
+- 不允许存在的词: 同义替换词、新概念词
+- 发现不允许的词 → 删除该query或替换回原词
+
+5. 补充生成:
+词汇未覆盖 / 关键组合缺失 → 补充生成
+
+# 输出
+最终按以下格式输出结果(JSON数组格式):
+[
+  {{
+    "search_word": "猫咪服饰造型元素有哪些",
+    "中心词": "服饰造型元素",
+    "source_word": "猫 猫咪 服饰造型元素 传递快乐 宠物猫咪 猫咪宠物 猫咪主体",
+    "reasoning": "判断依据说明"
+  }},
+  {{
+    "search_word": "猫咪传递快乐的服饰造型元素",
+    "中心词": "服饰造型元素",
+    "source_word": "猫 猫咪 服饰造型元素 传递快乐 宠物猫咪 猫咪宠物 猫咪主体",
+    "reasoning": "判断依据说明"
+  }}
+]
+
+**source_word规则**(重要):
+1. 格式:空格分隔的词汇
+2. 来源:**必须且只能**从"中心词 + 待选词"中提取
+3. 提取规则:该query实际使用到的所有原始词汇
+4. 禁止:同义替换、添加新词
+5. 必须包含:中心词(如果query中使用了中心词)
+
+# 执行顺序
+词汇分析 → 中心词确定 → 高权重词识别 → 关系分析 → 类型判定 →
+围绕"中心词+高权重词"生成 query → 质量检查 → 补充 → 输出
+
+注意:只返回JSON数组,不要其他内容。"""
+
+        # 调用 LLM
+        llm_results = self.client.chat_json(prompt=prompt, max_retries=3)
+
+        if not llm_results or not isinstance(llm_results, list):
+            logger.error("LLM返回格式错误")
+            return []
+
+        logger.info(f"LLM生成了 {len(llm_results)} 个query")
+
+        # 解析并验证
+        formatted_results = []
+        for rank, item in enumerate(llm_results[:max_queries], 1):
+            validated_source_word = self._validate_and_fix_source_word(
+                llm_source_word=item.get("source_word", ""),
+                query=item.get("search_word", ""),
+                base_word=base_word,
+                candidate_words=candidate_words
+            )
+
+            formatted_results.append({
+                "search_word": item.get("search_word", ""),
+                "source_word": validated_source_word,
+                "score": 0.0,
+                "reasoning": item.get("reasoning", ""),
+                "rank": rank,
+                "original_feature": original_feature
+            })
+
+        return formatted_results
+
+    def _validate_and_fix_source_word(
+        self,
+        llm_source_word: str,
+        query: str,
+        base_word: str,
+        candidate_words: List[str]
+    ) -> str:
+        """
+        验证并修正 LLM 输出的 source_word
+        确保只包含"中心词 + 候选词"中的词
+
+        Args:
+            llm_source_word: LLM 输出的 source_word
+            query: 生成的 search_word
+            base_word: 中心词
+            candidate_words: 候选词列表
+
+        Returns:
+            验证后的 source_word
+        """
+        words = llm_source_word.split()
+        valid_words = []
+
+        # 验证每个词是否在允许列表中
+        for word in words:
+            if word == base_word or word in candidate_words:
+                valid_words.append(word)
+
+        # 确保中心词存在(如果query中包含)
+        if base_word in query and base_word not in valid_words:
+            valid_words.insert(0, base_word)
+
+        # 去重
+        seen = set()
+        deduplicated = []
+        for word in valid_words:
+            if word not in seen:
+                seen.add(word)
+                deduplicated.append(word)
+
+        return ' '.join(deduplicated)
+
     def evaluate_single_note(
         self,
         original_feature: str,

+ 1 - 1
run_stage7.py

@@ -147,7 +147,7 @@ def main():
     parser.add_argument(
         '--timeout',
         type=int,
-        default=600,
+        default=800,
         help='API 超时时间(秒)(默认: 600,即10分钟)'
     )
     parser.add_argument(

+ 1 - 1
stage7_analyzer.py

@@ -36,7 +36,7 @@ class Stage7DeconstructionAnalyzer:
         min_score: float = 8.0,
         skip_count: int = 0,
         sort_by: str = 'score',
-        timeout: int = 30,
+        timeout: int = 800,
         max_retries: int = 3,
         output_dir: str = "output_v2",
         enable_image_download: bool = True,

+ 1 - 1
stage7_api_client.py

@@ -95,7 +95,7 @@ class DeconstructionAPIClient:
     def __init__(
         self,
         api_url: str = "http://192.168.245.150:7000/what/analysis/single",
-        timeout: int = 30,
+        timeout: int = 800,
         max_retries: int = 3
     ):
         """