Selaa lähdekoodia

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

刘立冬 3 viikkoa sitten
vanhempi
commit
324f66177b
5 muutettua tiedostoa jossa 298 lisäystä ja 29 poistoa
  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 typing import Dict, List, Any, Optional, Set, Tuple
 from datetime import datetime
 from datetime import datetime
 from concurrent.futures import ThreadPoolExecutor, as_completed
 from concurrent.futures import ThreadPoolExecutor, as_completed
-from itertools import combinations
 
 
 from openrouter_client import OpenRouterClient
 from openrouter_client import OpenRouterClient
 from llm_evaluator import LLMEvaluator
 from llm_evaluator import LLMEvaluator
@@ -1212,35 +1211,18 @@ class EnhancedSearchV2:
 
 
             logger.info(f"    候选词数量: {len(candidate_words)} (限制: {max_candidates})")
             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,
                 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]
             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
             # 保存分组结果 - 每个base_word有自己的available_words
             grouped_results.append({
             grouped_results.append({

+ 287 - 0
llm_evaluator.py

@@ -267,6 +267,293 @@ class LLMEvaluator:
 
 
         return all_results
         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(
     def evaluate_single_note(
         self,
         self,
         original_feature: str,
         original_feature: str,

+ 1 - 1
run_stage7.py

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

+ 1 - 1
stage7_analyzer.py

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

+ 1 - 1
stage7_api_client.py

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