刘立冬 3 tuần trước cách đây
mục cha
commit
6bdf26c4bc
1 tập tin đã thay đổi với 351 bổ sung0 xóa
  1. 351 0
      llm_evaluator.py

+ 351 - 0
llm_evaluator.py

@@ -595,6 +595,357 @@ class LLMEvaluator:
         # 尝试从其他字段获取
         return feature_node.get("原始特征名称", feature_node.get("特征名称", ""))
 
+    # ========== Stage 6: 两层评估方法 ==========
+
+    def evaluate_note_with_filter(
+        self,
+        search_query: str,
+        target_feature: str,
+        note_title: str,
+        note_content: str,
+        note_images: List[str],
+        note_index: int = 0
+    ) -> Dict[str, Any]:
+        """
+        两层评估单个笔记(完整Prompt版本)
+
+        第一层:Query相关性过滤
+        第二层:目标特征匹配度评分
+
+        Args:
+            search_query: 搜索Query,如 "外观装扮 发布萌宠内容"
+            target_feature: 目标特征,如 "佩戴"
+            note_title: 笔记标题
+            note_content: 笔记正文
+            note_images: 图片URL列表(会传递给LLM进行视觉分析和OCR)
+            note_index: 笔记索引
+
+        Returns:
+            评估结果字典
+        """
+        # 构建完整的评估Prompt(用户提供的完整版本,一字不改)
+        prompt = f"""# 任务说明
+你需要判断搜索到的案例信息与目标特征的相关性。判断分为两层:第一层过滤与搜索Query无关的结果,第二层评估与目标特征的匹配度。
+
+# 输入信息
+
+搜索Query:{search_query}
+目标特征:{target_feature}
+搜索结果:
+- 标题: {note_title}
+- 正文: {note_content[:800]}
+- 图像: {len(note_images)}张图片(请仔细分析图片内容,包括OCR提取图片中的文字)
+
+# 判断流程
+第一层:Query相关性过滤
+判断标准:搜索结果是否与搜索Query相关
+过滤规则:
+
+✅ 保留:搜索结果的标题、正文或图像内容中包含Query相关的信息
+
+Query的核心关键词在结果中出现
+或结果讨论的主题与Query直接相关
+或结果是Query概念的上位/下位/平行概念
+
+
+❌ 过滤:搜索结果与Query完全无关
+
+Query的关键词完全未出现
+结果主题与Query无任何关联
+仅因搜索引擎误匹配而出现
+
+
+
+示例:
+
+Query "墨镜搭配" → 结果"太阳镜选购指南" ✅ 保留(墨镜=太阳镜)
+Query "墨镜搭配" → 结果"眼镜搭配技巧" ✅ 保留(眼镜是上位概念)
+Query "墨镜搭配" → 结果"帽子搭配技巧" ❌ 过滤(完全无关)
+Query "复古滤镜" → 结果"滤镜调色教程" ✅ 保留(包含滤镜)
+Query "复古滤镜" → 结果"相机推荐" ❌ 过滤(主题不相关)
+
+输出:
+如果判定为 ❌ 过滤,直接输出:
+
+json{{
+  "Query相关性": "不相关",
+  "综合得分": 0,
+  "匹配类型": "过滤",
+  "说明": "搜索结果与Query '{search_query}' 完全无关,建议过滤"
+}}
+
+如果判定为 ✅ 保留,进入第二层评分
+
+第二层:目标特征匹配度评分
+综合考虑语义相似度(概念匹配、层级关系、实操价值)和场景关联度(应用场景、使用语境)进行评分:
+8-10分:完全匹配
+
+语义层面:找到与目标特征完全相同或高度一致的内容,核心概念完全一致
+场景层面:完全适用于同一场景、受众、平台和语境
+实操价值:提供了具体可执行的方法、步骤或技巧
+示例:
+
+目标"复古滤镜" + 小红书穿搭场景 vs 结果"小红书复古滤镜调色教程"
+目标"墨镜" + 时尚搭配场景 vs 结果"时尚墨镜搭配指南"
+
+
+
+6-7分:相似匹配
+
+语义层面:
+
+结果是目标的上位概念(更宽泛)或下位概念(更具体)
+或属于同一概念的不同表现形式
+或属于平行概念(同级不同类)
+
+
+场景层面:场景相近但有差异,需要筛选或调整后可用
+实操价值:有一定参考价值但需要转化应用
+示例:
+
+目标"墨镜" + 时尚搭配 vs 结果"眼镜搭配技巧"(上位概念,需筛选)
+目标"怀旧滤镜" + 人像拍摄 vs 结果"胶片感调色"(不同表现形式)
+目标"日常穿搭" + 街拍 vs 结果"通勤穿搭拍照"(场景相近)
+
+
+
+5-6分:弱相似
+
+语义层面:属于同一大类但具体方向或侧重点明显不同
+场景层面:场景有明显差异,迁移需要较大改造
+实操价值:提供了概念启发但需要较大转化
+示例:
+
+目标"户外运动穿搭" vs 结果"健身房穿搭指南"
+目标"小红书图文笔记" vs 结果"抖音短视频脚本"
+
+
+
+4分及以下:无匹配
+
+语义层面:仅表面词汇重叠,实质关联弱,或概念距离过远
+场景层面:应用场景基本不同或完全不同
+实操价值:实操指导价值有限或无价值
+示例:
+
+目标"墨镜" vs 结果"配饰大全"(概念过于宽泛)
+目标"美食摄影构图" vs 结果"美食博主日常vlog"
+
+
+
+
+概念层级关系说明
+在评分时,需要注意概念层级关系的影响:
+完全匹配(同一概念 + 同场景)→ 8-10分
+目标"墨镜" vs 结果"墨镜搭配",且都在时尚搭配场景
+
+
+上位/下位概念(层级差一层)→ 通常6-7分
+目标"墨镜" vs 结果"眼镜搭配"(结果更宽泛,需筛选)
+目标"眼镜" vs 结果"墨镜选购"(结果更具体,部分适用)
+
+
+平行概念(同级不同类)→ 通常6-7分
+目标"墨镜" vs 结果"近视眼镜"(都是眼镜类,但功能场景不同)
+
+
+远距离概念(层级差两层及以上)→ 4分及以下
+目标"墨镜" vs 结果"配饰"(概念过于宽泛,指导性弱)
+
+
+
+
+匹配结论判断
+根据综合得分判定匹配类型:
+
+8.0-10.0分:✅ 完全匹配
+
+判断:找到了目标特征的直接灵感来源
+置信度:高
+建议:直接采纳为该特征的灵感溯源结果
+
+
+5.0-7.9分:⚠️ 相似匹配
+
+判断:找到了相关的灵感参考,但存在一定差异
+置信度:中
+建议:作为候选结果保留,可与其他结果综合判断或继续搜索更精确的匹配
+
+
+1.0-4.9分:❌ 无匹配
+
+判断:该结果与目标特征关联度不足
+置信度:低
+建议:排除该结果,需要调整搜索策略继续寻找
+
+
+
+
+# 输出格式
+通过Query相关性过滤的结果:
+json{{
+  "Query相关性": "相关",
+  "综合得分": 7.0,
+  "匹配类型": "相似匹配",
+  "置信度": "中",
+  "评分说明": "结果'眼镜搭配技巧'是目标'墨镜'的上位概念,内容涵盖多种眼镜类型。场景都是时尚搭配,但需要从结果中筛选出墨镜相关的内容。概念关系:上位概念(宽泛一层)",
+  "关键匹配点": [
+    "眼镜与脸型的搭配原则(部分适用于墨镜)",
+    "配饰的风格选择方法"
+  ]
+}}
+未通过Query相关性过滤的结果:
+json{{
+  "Query相关性": "不相关",
+  "综合得分": 0,
+  "匹配类型": "过滤",
+  "说明": "搜索结果'帽子搭配技巧'与Query'墨镜搭配'完全无关,建议过滤"
+}}
+
+# 特殊情况处理
+
+复合特征评估:如果目标特征是复合型(如"复古滤镜+第一人称视角"),需要分别评估每个子特征的匹配度,然后取算术平均值作为最终得分
+信息不完整:如果OCR提取的图像文字不完整或正文内容缺失,应在说明中注明,并根据实际可获取的信息进行评分
+上位概念的实用性:当结果是目标的上位概念时,评分应考虑:
+
+内容中目标相关部分的占比
+是否提供了可直接应用于目标的知识
+场景的一致性程度
+如果结果虽是上位概念但完全不涉及目标内容,应降至5-6分或更低
+
+
+Query与目标特征的关系:
+如果Query就是目标特征本身,第一层和第二层判断可以合并考虑
+如果Query是为了探索目标特征而构建的更宽泛查询,第一层更宽松,第二层更严格
+
+
+
+只返回JSON,不要其他内容。"""
+
+        # 调用LLM(传递图片URL进行多模态分析)
+        result = self.client.chat_json(
+            prompt=prompt,
+            images=note_images if note_images else None,  # ✅ 传递图片
+            max_retries=3
+        )
+
+        if result:
+            # 添加笔记索引
+            result['note_index'] = note_index
+            return result
+        else:
+            logger.error(f"  评估笔记 {note_index} 失败: Query={search_query}")
+            return {
+                "note_index": note_index,
+                "Query相关性": "评估失败",
+                "综合得分": 0,
+                "匹配类型": "评估失败",
+                "说明": "LLM评估失败"
+            }
+
+    def batch_evaluate_notes_with_filter(
+        self,
+        search_query: str,
+        target_feature: str,
+        notes: List[Dict[str, Any]],
+        max_notes: int = 20,
+        max_workers: int = 10
+    ) -> Dict[str, Any]:
+        """
+        并行评估多个笔记(两层评估)
+
+        Args:
+            search_query: 搜索Query
+            target_feature: 目标特征
+            notes: 笔记列表
+            max_notes: 最多评估几条笔记
+            max_workers: 最大并发数
+
+        Returns:
+            评估结果汇总(包含统计信息)
+        """
+        if not notes:
+            return {
+                "total_notes": 0,
+                "evaluated_notes": 0,
+                "filtered_count": 0,
+                "statistics": {},
+                "notes_evaluation": []
+            }
+
+        notes_to_eval = notes[:max_notes]
+        evaluated_notes = []
+
+        logger.info(f"    并行评估 {len(notes_to_eval)} 个笔记({max_workers}并发)")
+
+        # 并发评估每个笔记
+        with ThreadPoolExecutor(max_workers=max_workers) as executor:
+            futures = []
+            for idx, note in enumerate(notes_to_eval):
+                note_card = note.get('note_card', {})
+                title = note_card.get('display_title', '')
+                content = note_card.get('desc', '')
+                images = note_card.get('image_list', [])
+
+                future = executor.submit(
+                    self.evaluate_note_with_filter,
+                    search_query,
+                    target_feature,
+                    title,
+                    content,
+                    images,
+                    idx
+                )
+                futures.append(future)
+
+            # 收集结果
+            for future in as_completed(futures):
+                try:
+                    result = future.result()
+                    evaluated_notes.append(result)
+                except Exception as e:
+                    logger.error(f"    评估笔记失败: {e}")
+
+        # 按note_index排序
+        evaluated_notes.sort(key=lambda x: x.get('note_index', 0))
+
+        # 统计信息
+        total_notes = len(notes)
+        evaluated_count = len(evaluated_notes)
+        filtered_count = sum(1 for n in evaluated_notes if n.get('Query相关性') == '不相关')
+
+        # 匹配度分布统计
+        match_distribution = {
+            '完全匹配(8-10)': 0,
+            '相似匹配(6-7)': 0,
+            '弱相似(5-6)': 0,
+            '无匹配(≤4)': 0
+        }
+
+        for note_eval in evaluated_notes:
+            if note_eval.get('Query相关性') == '不相关':
+                continue  # 过滤的不计入分布
+
+            score = note_eval.get('综合得分', 0)
+            if score >= 8.0:
+                match_distribution['完全匹配(8-10)'] += 1
+            elif score >= 6.0:
+                match_distribution['相似匹配(6-7)'] += 1
+            elif score >= 5.0:
+                match_distribution['弱相似(5-6)'] += 1
+            else:
+                match_distribution['无匹配(≤4)'] += 1
+
+        logger.info(f"    评估完成: 过滤{filtered_count}条, 匹配分布: {match_distribution}")
+
+        return {
+            "total_notes": total_notes,
+            "evaluated_notes": evaluated_count,
+            "filtered_count": filtered_count,
+            "statistics": match_distribution,
+            "notes_evaluation": evaluated_notes
+        }
+
 
 def test_evaluator():
     """测试评估器"""