| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317 |
- """
- 帖子评估模块 V3 - 4步串行+并行评估系统
- 改进:
- 1. Prompt1: 判断是知识 (is_knowledge)
- 2. Prompt2: 判断是否是内容知识 (is_content_knowledge)
- 3. Prompt3 & Prompt4: 并行执行 - 目的性(70%) + 品类(30%)
- 4. 代码计算综合得分: final_score = purpose × 0.7 + category × 0.3
- 5. 完全替代V2评估结果
- """
- import asyncio
- import json
- import os
- from datetime import datetime
- from typing import Optional
- from pydantic import BaseModel, Field
- import requests
- MODEL_NAME = "google/gemini-2.5-flash"
- MAX_IMAGES_PER_POST = 10
- MAX_CONCURRENT_EVALUATIONS = 5
- API_TIMEOUT = 120
- # ============================================================================
- # 数据模型
- # ============================================================================
- class KnowledgeEvaluation(BaseModel):
- """Prompt1: 判断是知识 - 评估结果"""
- is_knowledge: bool = Field(..., description="是否是知识内容")
- quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
- title_layer: dict = Field(default_factory=dict, description="标题层判断")
- image_layer: dict = Field(default_factory=dict, description="图片层判断(核心)")
- text_layer: dict = Field(default_factory=dict, description="正文层判断(辅助)")
- judgment_logic: str = Field(..., description="综合判定逻辑")
- core_evidence: list[str] = Field(default_factory=list, description="核心证据")
- issues: list[str] = Field(default_factory=list, description="不足或疑虑")
- conclusion: str = Field(..., description="结论陈述")
- class ContentKnowledgeEvaluation(BaseModel):
- """Prompt2: 判断是否是内容知识 - 评估结果"""
- is_content_knowledge: bool = Field(..., description="是否属于内容知识")
- final_score: int = Field(..., description="最终得分(0-100)")
- level: str = Field(..., description="判定等级")
- quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
- dimension_scores: dict = Field(default_factory=dict, description="分层评分详情")
- core_evidence: list[str] = Field(default_factory=list, description="核心证据")
- issues: list[str] = Field(default_factory=list, description="不足之处")
- summary: str = Field(..., description="总结陈述")
- class PurposeEvaluation(BaseModel):
- """Prompt3: 目的性匹配 - 评估结果"""
- purpose_score: int = Field(..., description="目的动机得分(0-100整数)")
- core_motivation: str = Field(..., description="原始需求核心动机")
- image_value: str = Field(..., description="图片提供的价值")
- title_intention: str = Field(..., description="标题体现的意图")
- text_content: str = Field(..., description="正文补充的内容")
- match_level: str = Field(..., description="匹配度等级")
- core_basis: str = Field(..., description="核心依据")
- class CategoryEvaluation(BaseModel):
- """Prompt4: 品类匹配 - 评估结果"""
- category_score: int = Field(..., description="品类匹配得分(0-100整数)")
- original_category: dict = Field(default_factory=dict, description="原始需求品类")
- actual_category: dict = Field(default_factory=dict, description="帖子实际品类")
- match_level: str = Field(..., description="匹配度等级")
- subject_match: str = Field(..., description="主体匹配情况")
- qualifier_match: str = Field(..., description="限定词匹配情况")
- core_basis: str = Field(..., description="核心依据")
- # ============================================================================
- # Prompt 定义
- # ============================================================================
- PROMPT1_IS_KNOWLEDGE = """# 知识判定系统 v1.0
- ## 角色定义
- 你是一个多模态内容评估专家,专门判断社交媒体帖子是否提供了"知识"。
- ## 知识定义
- **知识**是指经过验证的、具有可靠性和真实性的信息,能够被理解、学习、传播和应用。
- ### 知识类型
- - ✅ **事实性知识**: 客观事实、数据、现象描述
- - ✅ **原理性知识**: 规律、原理、理论、因果关系
- - ✅ **方法性知识**: 技能、流程、步骤、操作方法
- - ✅ **经验性知识**: 总结提炼的经验、教训、最佳实践
- - ✅ **概念性知识**: 定义、分类、框架、体系
- - ✅ **应用性知识**: 解决方案、工具使用、实践指南
- ### 非知识类型(严格排除)
- - ❌ **纯观点/立场**: 未经验证的个人观点、偏好表达
- - ❌ **情感表达**: 纯粹的情绪抒发、心情分享
- - ❌ **单纯展示**: 作品展示、生活记录、打卡(无知识提炼)
- - ❌ **娱乐内容**: 段子、搞笑、八卦(无信息价值)
- - ❌ **纯营销/广告**: 单纯的产品推销
- - ❌ **虚假/未验证信息**: 谣言、伪科学、未经证实的说法
- ---
- ## 输入信息
- - **标题**: {title}
- - **正文**: {body_text}
- - **图片**: {num_images}张
- ---
- ## 判断流程
- ### 第一步: 快速排除(任一为"是"则判定为非知识)
- 1. 是否为纯情感表达/生活记录/打卡?
- 2. 是否为单纯的作品/产品展示(无知识提炼)?
- 3. 是否为娱乐搞笑/八卦/纯营销内容?
- 4. 是否包含虚假信息或伪科学?
- 5. 是否完全没有新信息(纯重复常识)?
- **排除判定**: □ 是(判定为非知识) / □ 否(继续评估)
- ---
- ### 第二步: 分层知识判断
- ## 🏷️ 标题层
- **判断:标题是否指向知识?**
- - ✅ 明确传达知识主题(如何/为什么/什么是/XX方法/XX原理)
- - ⚠️ 描述性标题,但暗示有知识内容
- - ❌ 展示型(我的XX/今天XX)或情感型标题
- **结果**: □ 有知识指向 / □ 无知识指向
- ---
- ## 🖼️ 图片层(信息主要承载层)
- **判断1: 图片知识呈现方式**
- - ✅ 包含信息图表(数据、流程图、对比图、结构图)
- - ✅ 有知识性标注(解释、说明、步骤、原理)
- - ✅ 多图形成知识体系(步骤序列、案例对比)
- - ❌ 纯作品展示、美图、氛围图
- **判断2: 图片教育价值**
- - ✅ 图片能教会他人方法、技能或原理
- - ✅ 提供可学习的步骤或解决方案
- - ❌ 图片无教学意义
- **判断3: 图片结构化程度**
- - ✅ 有清晰的逻辑组织(序号、分层、框架)
- - ✅ 有步骤、要点的结构化呈现
- - ❌ 碎片化、无逻辑的展示
- **判断4: 图片实用性**
- - ✅ 提供可直接应用的方法或工具
- - ✅ 能帮助解决实际问题
- - ❌ 纯观赏性,无实际应用价值
- **判断5: 图片信息密度**
- - ✅ 包含≥3个独立知识点
- - ⚠️ 包含1-2个知识点
- - ❌ 无明确知识点
- **图片层综合评估**: □ 图片传递了知识 / □ 图片无知识价值
- ---
- ## 📝 正文层(辅助判断)
- **判断1: 信息增量**
- - ✅ 提供了明确的新信息、新认知或新方法
- - ❌ 无新信息,只是个人记录或情感表达
- **判断2: 可验证性**
- - ✅ 基于事实、数据、可验证的经验
- - ❌ 纯主观观点或感受,无依据
- **判断3: 知识类型归属**
- - ✅ 能归入至少一种知识类型(事实/原理/方法/经验/概念/应用)
- - ❌ 无法归类为任何知识类型
- **正文层综合评估**: □ 正文提供了知识支撑 / □ 正文无知识价值
- ---
- ### 第三步: 综合判定
- #### 判定规则
- **直接判定为"非知识"(任一成立)**:
- - 未通过快速排除
- - 图片层 = 无知识价值 且 正文层 = 无知识支撑
- - 正文判断3(知识类型)= 无法归类
- **判定为"是知识"(需同时满足)**:
- 1. 通过快速排除
- 2. 图片层 = 传递了知识 或 正文层 = 提供了知识支撑
- 3. 正文判断1(信息增量)= 有新信息
- 4. 正文判断3(知识类型)= 可归类
- **特别说明**:
- - 社交媒体帖子以图片为主要信息载体,图片层权重最高
- - 标题为辅助判断,正文为补充验证
- ---
- ## 输出格式
- 请严格按照以下JSON格式输出:
- {{
- "is_knowledge": true/false,
- "quick_exclude": {{
- "result": "通过/未通过",
- "reason": "如未通过,说明原因"
- }},
- "title_layer": {{
- "has_knowledge_direction": true/false,
- "reason": "一句话说明"
- }},
- "image_layer": {{
- "knowledge_presentation": {{"match": true/false, "reason": "简述"}},
- "educational_value": {{"has_value": true/false, "reason": "简述"}},
- "structure_level": {{"structured": true/false, "reason": "简述"}},
- "practicality": {{"practical": true/false, "reason": "简述"}},
- "information_density": {{"level": "高/中/低", "reason": "简述"}},
- "overall": "传递知识/无知识价值"
- }},
- "text_layer": {{
- "information_gain": {{"has_gain": true/false, "reason": "简述"}},
- "verifiability": {{"verifiable": true/false, "reason": "简述"}},
- "knowledge_type": {{"type": "具体类型/无法归类", "reason": "简述"}},
- "overall": "有知识支撑/无知识价值"
- }},
- "judgment_logic": "说明是否满足判定规则,关键依据是什么",
- "core_evidence": [
- "最强支持证据1",
- "最强支持证据2"
- ],
- "issues": [
- "不足或疑虑(如无则为空数组)"
- ],
- "conclusion": "用1-2句话说明判定结果和核心理由"
- }}
- ## 判断原则
- 1. **图片优先**: 社交媒体以图片为主要信息载体,图片层是核心判断依据,标题和正文信息辅助
- 2. **严格性**: 宁可误判为"非知识",也不放过无价值内容
- 3. **证据性**: 基于明确的视觉和文本证据判断
- 4. **价值导向**: 优先判断内容对读者是否有实际学习价值
- """
- PROMPT2_IS_CONTENT_KNOWLEDGE = """# 内容知识判定系统 v2.0
- ## 角色定义
- 你是一个多模态内容评估专家,专门判断社交媒体帖子是否属于"内容知识"类别。
- ## 前置条件
- 该帖子已通过知识判定,确认提供了知识。现在需要进一步判断是否属于"内容知识"。
- ---
- ## 内容知识定义
- **内容知识**是指与创作/制作/设计相关的、具有实操性和可迁移性的知识,帮助创作者提升创作能力。
- ### 内容知识的范畴
- - ✅ **创作原理**: 设计原理、创作逻辑、美学规律、构图法则(通用的,普适的)
- - ✅ **制作方法**: 操作流程、技术步骤、工具使用方法
- - ✅ **创意技巧**: 灵感方法、创意思路、表现手法、风格技法
- - ✅ **体系框架**: 完整的创作体系、方法论、思维框架
- - ✅ **案例提炼**: 从多个案例中总结的通用创作规律
- ### 非内容知识(严格排除)
- - ❌ **单案例展示**: 仅展示单个作品,无方法论提炼
- - ❌ **作品集合**: 纯作品展示集合,无创作方法讲解
- - ❌ **单点元素**: 只展示配色/字体/素材,无使用方法
- - ❌ **单次操作**: 只讲某个项目的特定操作,无通用性
- - ❌ **非创作领域**: 健康、财经、生活、科普等非创作制作领域的知识
- ---
- ## 输入信息
- - **标题**: {title}
- - **正文**: {body_text}
- - **图片数量**: {num_images}张
- ---
- ## 判断流程
- ### 第一步: 快速排除判断(任一为"是"则判定为非内容知识)
- 1. 标题是否为纯展示型?("我的XX作品"、"今天做了XX"、"作品分享")
- 2. 图片是否全为作品展示,无任何方法/原理/步骤说明?
- 3. 是否只讲单个项目的特定操作,完全无通用性?
- 4. 是否为纯元素展示,无创作方法?(仅展示配色、字体、素材)
- **排除判定**: □ 是(判定为非内容知识) / □ 否(继续评估)
- ---
- ### 第二步: 分层打分评估(满分100分)
- ## 🖼️ 图片层评估(权重70%,满分70分)
- > **说明**: 社交媒体以图片为主要信息载体,图片层是核心判断依据
- #### 维度1: 创作方法呈现(20分)
- **评分依据**: 图片是否清晰展示了具体的创作/制作方法、技巧、技法
- - **20分**: 图片详细展示≥3个具体可操作的创作方法/技巧,有明确的操作指引
- - **15分**: 图片展示2个创作方法,方法较为具体
- - **10分**: 图片展示1个创作方法,但不够详细
- - **5分**: 图片暗示有方法,但未明确展示
- - **0分**: 图片无任何方法展示,纯作品呈现
- **得分**: __/20
- ---
- #### 维度2: 知识体系化程度(15分)
- **评分依据**: 多图是否形成完整的知识体系或逻辑链条
- - **15分**: 多图形成完整体系(步骤1→2→3,或原理→方法→案例),逻辑清晰
- - **12分**: 多图有知识关联性,形成部分体系
- - **8分**: 多图展示多个知识点,但关联性弱
- - **4分**: 多图仅为同类案例堆砌,无体系
- - **0分**: 单图或多图无逻辑关联
- **得分**: __/15
- ---
- #### 维度3: 教学性标注与说明(15分)
- **评分依据**: 图片是否包含教学性的视觉元素(标注、序号、箭头、文字说明)
- - **15分**: 大量教学标注(序号、箭头、高亮、文字说明、对比标记等),清晰易懂
- - **12分**: 有明显的教学标注,但不够完善
- - **8分**: 有少量标注或说明
- - **4分**: 仅有简单文字,无视觉教学元素
- - **0分**: 无任何教学标注,纯视觉展示
- **得分**: __/15
- ---
- #### 维度4: 方法可复用性(10分)
- **评分依据**: 图片展示的方法是否可迁移到其他创作场景/项目
- - **10分**: 明确展示通用方法,可应用于多种场景(配公式/模板/框架)
- - **8分**: 方法有一定通用性,可迁移到类似场景
- - **5分**: 方法通用性一般,需要改造才能应用
- - **2分**: 方法仅适用于特定项目
- - **0分**: 无可复用方法
- **得分**: __/10
- ---
- #### 维度5: 原理与案例结合(10分)
- **评分依据**: 图片是否将创作原理与实际案例有效结合
- - **10分**: 原理+多案例验证,清晰展示原理如何应用
- - **8分**: 原理+案例,有一定结合
- - **5分**: 有原理或有案例,但结合不够
- - **2分**: 仅有案例,无原理提炼
- - **0分**: 纯案例展示或纯理论
- **得分**: __/10
- ---
- **🖼️ 图片层总分**: __/70
- ---
- ## 📝 正文层评估(权重20%,满分20分)
- > **说明**: 正文作为辅助判断,补充图片未完整呈现的知识信息
- #### 维度6: 方法/步骤描述(10分)
- **评分依据**: 正文是否描述了具体的创作方法或操作步骤
- - **10分**: 有完整的步骤描述(≥3步)或详细的方法说明
- - **7分**: 有步骤或方法描述,但不够系统
- - **4分**: 有零散的方法提及
- - **0分**: 无方法/步骤,纯叙事或展示性文字
- **得分**: __/10
- ---
- #### 维度7: 知识总结与提炼(10分)
- **评分依据**: 正文是否对创作经验/规律进行总结提炼
- - **10分**: 有明确的知识总结、归纳、框架化输出
- - **7分**: 有一定的经验总结或要点提炼
- - **4分**: 有零散的心得,但未成体系
- - **0分**: 无任何知识提炼
- **得分**: __/10
- ---
- **📝 正文层总分**: __/20
- ---
- ## 🏷️ 标题层评估(权重10%,满分10分)
- > **说明**: 标题作为内容导向,辅助判断内容主题
- #### 维度8: 标题内容指向性(10分)
- **评分依据**: 标题是否明确指向创作/制作相关的知识内容
- - **10分**: 标题明确包含方法/教程/技巧/原理类词汇("XX教程"、"XX技巧"、"如何XX"、"XX方法")
- - **7分**: 标题包含整理型词汇("合集"、"总结"、"分享XX方法")
- - **4分**: 描述性标题,暗示有创作知识
- - **0分**: 纯展示型标题("我的作品"、"今天做了XX")或与创作无关
- **得分**: __/10
- ---
- **🏷️标题层总分**: __/10
- ---
- ### 第三步: 综合评分与判定
- **总分计算**:
- 总分 = 图片层总分(70分) + 正文层总分(20分) + 标题层总分(10分)
- **最终得分**: __/100分
- ---
- **判定等级**:
- - **85-100分**: ⭐⭐⭐⭐⭐ 优质内容知识 - 强烈符合
- - **70-84分**: ⭐⭐⭐⭐ 良好内容知识 - 符合
- - **55-69分**: ⭐⭐⭐ 基础内容知识 - 基本符合
- - **40-54分**: ⭐⭐ 弱内容知识 - 不太符合
- - **0-39分**: ⭐ 非内容知识 - 不符合
- ---
- ## 输出格式
- 请严格按照以下JSON格式输出:
- {{
- "is_content_knowledge": true/false,
- "final_score": 85,
- "level": "⭐⭐⭐⭐⭐ 优质内容知识",
- "quick_exclude": {{
- "result": "是/否",
- "reason": "原因"
- }},
- "dimension_scores": {{
- "image_layer": {{
- "creation_method": {{"score": 20, "reason": "简述"}},
- "knowledge_system": {{"score": 15, "reason": "简述"}},
- "teaching_annotation": {{"score": 15, "reason": "简述"}},
- "method_reusability": {{"score": 10, "reason": "简述"}},
- "principle_case": {{"score": 10, "reason": "简述"}},
- "subtotal": 70
- }},
- "text_layer": {{
- "method_description": {{"score": 10, "reason": "简述"}},
- "knowledge_summary": {{"score": 10, "reason": "简述"}},
- "subtotal": 20
- }},
- "title_layer": {{
- "content_direction": {{"score": 10, "reason": "简述"}},
- "subtotal": 10
- }}
- }},
- "core_evidence": [
- "证据1",
- "证据2"
- ],
- "issues": [
- "问题1(如无则为空数组)"
- ],
- "summary": "总结陈述(2-3句话)"
- }}
- ## 判断原则
- 1. **图片主导原则**: 图片占70%权重,是核心判断依据;标题和正文为辅助
- 2. **创作领域限定**: 必须属于创作/制作/设计领域,其他领域知识不属于内容知识
- 3. **方法优先原则**: 重点评估是否提供了可操作的创作方法,而非纯作品展示
- 4. **通用性要求**: 优先考虑方法的可复用性和可迁移性
- 5. **严格性原则**: 宁可误判为"非内容知识",也不放过纯展示型内容
- 6. **证据性原则**: 评分需基于明确的视觉和文本证据,可量化衡量
- """
- PROMPT3_PURPOSE_MATCH = """# Prompt 1: 多模态内容目的动机匹配评估
- ## 角色定义
- 你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**目的动机匹配度**,能够精准判断帖子是否满足用户的核心意图。
- ---
- ## 任务说明
- 你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文)
- 请**仅评估目的动机维度**的匹配度,输出0-100分的量化得分。
- ---
- ## 输入格式
- **原始搜索需求:**
- {original_query}
- **多模态帖子内容:**
- - **图片:** {num_images}张
- - **标题:** {title}
- - **正文:** {body_text}
- ---
- ## 评估维度:目的动机匹配
- ### 核心评估逻辑
- **目的动机 = 用户想做什么 = 核心动词/意图**
- 常见动机类型:
- - **获取型**:寻找、下载、收藏、获取
- - **学习型**:教程、学习、了解、掌握
- - **决策型**:推荐、对比、评测、选择
- - **创作型**:拍摄、制作、设计、生成
- - **分享型**:晒单、记录、分享、展示
- ---
- ## 评估流程
- ### 第一步:识别原始需求的核心动机
- - 提取**核心动词**(如果是纯名词短语,识别隐含意图)
- - 判断用户的**最终目的**是什么
- ### 第二步:分析帖子提供的价值(重点看图片)
- **图片分析(权重70%):**
- - 图片展示的是什么类型的内容?
- - 图片是否直接解答了需求的目的?
- - 图片的信息完整度和实用性如何?
- **标题分析(权重15%):**
- - 标题是否明确了内容的目的?
- **正文分析(权重15%):**
- - 正文是否提供了实质性的解答内容?
- ### 第三步:判断目的匹配度
- - 帖子是否**实质性地满足**了需求的动机?
- - 内容是否**实用、完整、可执行**?
- ---
- ## 评分标准(0-100分)
- ### 高度匹配区间
- **90-100分:完全满足动机,内容实用完整**
- - 图片直接展示解决方案/教程步骤/对比结果
- - 内容完整、清晰、可直接使用
- - 例:需求"如何拍摄夜景" vs 图片展示完整的夜景拍摄参数设置和效果对比
- **75-89分:基本满足动机,信息较全面**
- - 图片提供了核心解答内容
- - 信息相对完整但深度略有不足
- - 例:需求"推荐旅行路线" vs 图片展示了路线图但缺少详细说明
- **60-74分:部分满足动机,有参考价值**
- - 图片提供了相关内容但不够直接
- - 需要结合文字才能理解完整意图
- ### 中度相关区间
- **40-59分:弱相关,核心目的未充分满足**
- - 图片内容与动机有关联但不是直接解答
- - 实用性较低
- - 例:需求"如何拍摄" vs 图片只展示成品照片,无教程内容
- ### 不相关/负向区间
- **20-39分:微弱关联,基本未解答**
- - 图片仅有外围相关性
- - 对满足需求帮助极小
- **1-19分:几乎无关**
- - 图片与需求动机关联极弱
- **0分:完全不相关**
- - 图片与需求动机无任何关联
- **负分不使用**(目的动机维度不设负分)
- ---
- ## 输出格式(JSON)
- ```json
- {{
- "目的动机评估": {{
- "目的动机得分": 0-100的整数,
- "原始需求核心动机": "识别出的用户意图(一句话)",
- "图片提供的价值": "图片展示了什么,如何满足动机",
- "标题体现的意图": "标题说明了什么",
- "正文补充的内容": "正文是否有实质解答",
- "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
- "核心依据": "为什么给这个分数(100字以内)"
- }}
- }}
- ```
- ---
- ## 评估原则
- 1. **图片优先**:图片权重70%,是判断的主要依据
- 2. **实用导向**:不看表面相关,看实际解答程度
- 3. **严格标准**:宁可低估,避免虚高
- 4. **客观量化**:基于可观察的内容特征打分
- ---
- ## 特别注意
- - 本评估**只关注目的动机维度**,不考虑品类是否匹配
- - 输出的分数必须是**0-100的整数**
- - 不要自行计算综合分数,只输出目的动机分数
- - 评分依据要具体、可验证
- """
- PROMPT4_CATEGORY_MATCH = """# Prompt 2: 多模态内容品类匹配评估
- ## 角色定义
- 你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**品类匹配度**
- 能够精准判断帖子的内容主体是否与用户需求一致。
- ---
- ## 任务说明
- 你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文),请**仅评估品类维度**的匹配度,输出0-100分的量化得分。
- ---
- ## 输入格式
- **原始搜索需求:**
- {original_query}
- **多模态帖子内容:**
- - **图片:** {num_images}张
- - **标题:** {title}
- - **正文:** {body_text}
- ---
- ## 评估维度:品类匹配
- ### 核心评估逻辑
- **品类 = 核心主体(名词)+ 限定词**
- - **核心主体**:具体的内容对象(风光摄影、旅行攻略、美食推荐)
- - **限定词**:
- - 地域:川西、成都、日本
- - 时间:秋季、夏天、2024
- - 类型:免费、高清、入门级
- - 风格:小清新、复古、简约
- ---
- ## 评估流程
- ### 第一步:提取原始需求的品类信息
- - 识别**核心主体名词**
- - 识别**关键限定词**(地域/时间/类型/风格等)
- ### 第二步:从帖子中提取品类信息(重点看图片)
- **图片识别(权重70%):**
- - 图片展示的核心主体是什么?
- - 图片中可识别的限定特征(地域标志、季节特征、类型属性、风格特点)
- **标题提取(权重15%):**
- - 标题明确的品类名词和限定词
- **正文提取(权重15%):**
- - 正文描述的品类信息
- ### 第三步:对比匹配度
- - 核心主体是否一致?
- - 限定词匹配了几个?
- - 是否存在泛化或偏移?
- ---
- ## 评分标准(0-100分)
- ### 高度匹配区间
- **90-100分:核心主体+关键限定词完全匹配**
- - 图片展示的主体与需求精准一致
- - 关键限定词全部匹配(地域、时间、类型等)
- - 例:需求"川西秋季风光" vs 图片展示川西秋季风景
- **75-89分:核心主体匹配,限定词匹配度百分之80**
- - 图片主体一致
- - 存在1-2个限定词缺失但不影响核心匹配
- - 例:需求"川西秋季风光" vs 图片展示川西风光(缺秋季)
- **60-74分:核心主体匹配,限定词匹配度百分之60**
- - 图片主体在同一大类
- - 限定词部分匹配或有合理上下位关系
- - 例:需求"川西秋季风光" vs 图片展示四川风光
- ### 中度相关区间
- **40-59分:核心主体匹配,限定词完全不匹配**
- - 图片主体相同但上下文不同
- - 限定词严重缺失或不匹配
- - 例:需求"猫咪表情包梗图" vs 女孩表情包
- ### 不相关/负向区间
- **20-39分:主体过度泛化**
- - 图片主体是通用概念,需求是特定概念
- - 仅有抽象类别相似
- - 例:需求"川西旅行攻略" vs 图片展示普通旅行场景
- **1-19分:品类关联极弱**
- - 图片主体与需求差异明显
- **0分:品类完全不同**
- - 图片主体类别完全不同
- - 例:需求"风光摄影" vs 图片展示美食
- **负分不使用**(品类维度不设负分)
- ---
- ## 输出格式(JSON)
- ```json
- {{
- "品类评估": {{
- "原始需求品类": {{
- "核心主体": "提取的主体名词",
- "关键限定词": ["限定词1", "限定词2"]
- }},
- "帖子实际品类": {{
- "图片主体": "图片展示的核心主体",
- "图片限定特征": ["从图片识别的限定词"],
- "标题品类": "标题提及的品类",
- "正文品类": "正文描述的品类"
- }},
- "品类匹配得分": 0-100的整数,
- "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
- "主体匹配情况": "主体是否一致",
- "限定词匹配情况": "哪些限定词匹配/缺失",
- "核心依据": "为什么给这个分数(100字以内)"
- }}
- }}
- ```
- ---
- ## 评估原则
- 1. **图片优先**:图片权重70%,是判断的主要依据
- 2. **表面匹配**:只看实际展示的内容,禁止推测联想
- 3. **通用≠特定**:通用概念不等于特定概念,需明确区分
- 4. **严格标准**:宁可低估,避免虚高
- 5. **客观量化**:基于可观察的视觉特征和文字信息打分
- ---
- ## 特别注意
- - 本评估**只关注品类维度**,不考虑目的是否匹配
- - 输出的分数必须是**0-100的整数**
- - 不要自行计算综合分数,只输出品类分数
- - 禁止因为"可能相关"就给分,必须有明确视觉证据
- """
- # ============================================================================
- # 辅助函数
- # ============================================================================
- def _clean_json_response(content_text: str) -> str:
- """清理API返回的JSON内容"""
- content_text = content_text.strip()
- if content_text.startswith("```json"):
- content_text = content_text[7:]
- elif content_text.startswith("```"):
- content_text = content_text[3:]
- if content_text.endswith("```"):
- content_text = content_text[:-3]
- return content_text.strip()
- async def _call_openrouter_api(
- prompt_text: str,
- image_urls: list[str],
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> dict:
- """
- 调用OpenRouter API的通用函数
- Args:
- prompt_text: Prompt文本
- image_urls: 图片URL列表
- semaphore: 并发控制信号量
- Returns:
- 解析后的JSON响应
- """
- api_key = os.getenv("OPENROUTER_API_KEY")
- if not api_key:
- raise ValueError("OPENROUTER_API_KEY environment variable not set")
- content = [{"type": "text", "text": prompt_text}]
- for url in image_urls:
- content.append({"type": "image_url", "image_url": {"url": url}})
- payload = {
- "model": MODEL_NAME,
- "messages": [{"role": "user", "content": content}],
- "response_format": {"type": "json_object"}
- }
- headers = {
- "Authorization": f"Bearer {api_key}",
- "Content-Type": "application/json"
- }
- async def _make_request():
- loop = asyncio.get_event_loop()
- response = await loop.run_in_executor(
- None,
- lambda: requests.post(
- "https://openrouter.ai/api/v1/chat/completions",
- headers=headers,
- json=payload,
- timeout=API_TIMEOUT
- )
- )
- if response.status_code != 200:
- raise Exception(f"API error: {response.status_code} - {response.text[:200]}")
- result = response.json()
- content_text = result["choices"][0]["message"]["content"]
- content_text = _clean_json_response(content_text)
- return json.loads(content_text)
- async def _execute_with_retry():
- """执行API请求,失败时自动重试最多2次"""
- MAX_RETRIES = 2
- for attempt in range(MAX_RETRIES + 1):
- try:
- return await _make_request()
- except Exception as e:
- if attempt < MAX_RETRIES:
- wait_time = 2 * (attempt + 1) # 2秒, 4秒
- print(f" ⚠️ API调用失败,{wait_time}秒后重试 (第{attempt + 1}/{MAX_RETRIES}次重试) - {str(e)[:50]}")
- await asyncio.sleep(wait_time)
- else:
- # 最后一次尝试也失败,抛出异常
- raise
- if semaphore:
- async with semaphore:
- return await _execute_with_retry()
- else:
- return await _execute_with_retry()
- # ============================================================================
- # 核心评估函数
- # ============================================================================
- async def evaluate_is_knowledge(
- post,
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> Optional[KnowledgeEvaluation]:
- """
- Prompt1: 判断是知识
- Args:
- post: Post对象
- semaphore: 并发控制信号量
- Returns:
- KnowledgeEvaluation 或 None(失败时)
- """
- if post.type == "video":
- return None
- image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
- try:
- prompt_text = PROMPT1_IS_KNOWLEDGE.format(
- title=post.title,
- body_text=post.body_text or "",
- num_images=len(image_urls)
- )
- data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
- return KnowledgeEvaluation(
- is_knowledge=data.get("is_knowledge", False),
- quick_exclude=data.get("quick_exclude", {}),
- title_layer=data.get("title_layer", {}),
- image_layer=data.get("image_layer", {}),
- text_layer=data.get("text_layer", {}),
- judgment_logic=data.get("judgment_logic", ""),
- core_evidence=data.get("core_evidence", []),
- issues=data.get("issues", []),
- conclusion=data.get("conclusion", "")
- )
- except Exception as e:
- print(f" ❌ Prompt1评估失败: {post.note_id} - {str(e)[:100]}")
- return None
- async def evaluate_is_content_knowledge(
- post,
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> Optional[ContentKnowledgeEvaluation]:
- """
- Prompt2: 判断是否是内容知识
- Args:
- post: Post对象
- semaphore: 并发控制信号量
- Returns:
- ContentKnowledgeEvaluation 或 None(失败时)
- """
- if post.type == "video":
- return None
- image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
- try:
- prompt_text = PROMPT2_IS_CONTENT_KNOWLEDGE.format(
- title=post.title,
- body_text=post.body_text or "",
- num_images=len(image_urls)
- )
- data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
- # 判定是否是内容知识:得分 >= 55 分
- final_score = data.get("final_score", 0)
- is_content_knowledge = final_score >= 55
- return ContentKnowledgeEvaluation(
- is_content_knowledge=is_content_knowledge,
- final_score=final_score,
- level=data.get("level", ""),
- quick_exclude=data.get("quick_exclude", {}),
- dimension_scores=data.get("dimension_scores", {}),
- core_evidence=data.get("core_evidence", []),
- issues=data.get("issues", []),
- summary=data.get("summary", "")
- )
- except Exception as e:
- print(f" ❌ Prompt2评估失败: {post.note_id} - {str(e)[:100]}")
- return None
- async def evaluate_purpose_match(
- post,
- original_query: str,
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> Optional[PurposeEvaluation]:
- """
- Prompt3: 目的性匹配评估
- Args:
- post: Post对象
- original_query: 原始搜索query
- semaphore: 并发控制信号量
- Returns:
- PurposeEvaluation 或 None(失败时)
- """
- if post.type == "video":
- return None
- image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
- try:
- prompt_text = PROMPT3_PURPOSE_MATCH.format(
- original_query=original_query,
- title=post.title,
- body_text=post.body_text or "",
- num_images=len(image_urls)
- )
- data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
- # Prompt3的输出在"目的动机评估"键下
- purpose_data = data.get("目的动机评估", {})
- return PurposeEvaluation(
- purpose_score=purpose_data.get("目的动机得分", 0),
- core_motivation=purpose_data.get("原始需求核心动机", ""),
- image_value=purpose_data.get("图片提供的价值", ""),
- title_intention=purpose_data.get("标题体现的意图", ""),
- text_content=purpose_data.get("正文补充的内容", ""),
- match_level=purpose_data.get("匹配度等级", ""),
- core_basis=purpose_data.get("核心依据", "")
- )
- except Exception as e:
- print(f" ❌ Prompt3评估失败: {post.note_id} - {str(e)[:100]}")
- return None
- async def evaluate_category_match(
- post,
- original_query: str,
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> Optional[CategoryEvaluation]:
- """
- Prompt4: 品类匹配评估
- Args:
- post: Post对象
- original_query: 原始搜索query
- semaphore: 并发控制信号量
- Returns:
- CategoryEvaluation 或 None(失败时)
- """
- if post.type == "video":
- return None
- image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
- try:
- prompt_text = PROMPT4_CATEGORY_MATCH.format(
- original_query=original_query,
- title=post.title,
- body_text=post.body_text or "",
- num_images=len(image_urls)
- )
- data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
- # Prompt4的输出在"品类评估"键下
- category_data = data.get("品类评估", {})
- return CategoryEvaluation(
- category_score=category_data.get("品类匹配得分", 0),
- original_category=category_data.get("原始需求品类", {}),
- actual_category=category_data.get("帖子实际品类", {}),
- match_level=category_data.get("匹配度等级", ""),
- subject_match=category_data.get("主体匹配情况", ""),
- qualifier_match=category_data.get("限定词匹配情况", ""),
- core_basis=category_data.get("核心依据", "")
- )
- except Exception as e:
- print(f" ❌ Prompt4评估失败: {post.note_id} - {str(e)[:100]}")
- return None
- def calculate_final_score(purpose_score: int, category_score: int) -> tuple[float, str]:
- """
- 计算综合得分和匹配等级
- Args:
- purpose_score: 目的性得分 (0-100整数)
- category_score: 品类得分 (0-100整数)
- Returns:
- (final_score, match_level)
- - final_score: 保留2位小数
- - match_level: 匹配等级字符串
- """
- # 计算综合得分: 目的性70% + 品类30%
- final = round(purpose_score * 0.7 + category_score * 0.3, 2)
- # 判定匹配等级
- if final >= 85:
- level = "高度匹配"
- elif final >= 70:
- level = "基本匹配"
- elif final >= 50:
- level = "部分匹配"
- elif final >= 30:
- level = "弱匹配"
- else:
- level = "不匹配"
- return final, level
- async def evaluate_post_v3(
- post,
- original_query: str,
- semaphore: Optional[asyncio.Semaphore] = None
- ) -> tuple:
- """
- V3评估主函数(4步流程)
- 流程:
- 1. Prompt1: 判断是知识 → 如果不是知识,停止
- 2. Prompt2: 判断是否是内容知识 → 如果不是内容知识,停止
- 3. Prompt3 & Prompt4: 并行执行目的性和品类匹配
- 4. 计算综合得分
- Returns:
- (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
- 任一步骤失败,后续结果为None
- """
- if post.type == "video":
- print(f" ⊗ 跳过视频帖子: {post.note_id}")
- return (None, None, None, None, None, None)
- print(f" 🔍 开始V3评估: {post.note_id}")
- # Step 1: 判断是知识
- print(f" 📝 Step 1/4: 判断是知识...")
- knowledge_eval = await evaluate_is_knowledge(post, semaphore)
- if not knowledge_eval:
- print(f" ❌ Step 1失败,停止评估")
- return (None, None, None, None, None, None)
- if not knowledge_eval.is_knowledge:
- print(f" ⊗ 非知识内容,停止后续评估")
- return (knowledge_eval, None, None, None, None, None)
- print(f" ✅ Step 1: 是知识内容")
- # Step 2: 判断是否是内容知识
- print(f" 📝 Step 2/4: 判断是否是内容知识...")
- content_eval = await evaluate_is_content_knowledge(post, semaphore)
- if not content_eval:
- print(f" ❌ Step 2失败,停止评估")
- return (knowledge_eval, None, None, None, None, None)
- if not content_eval.is_content_knowledge:
- print(f" ⊗ 非内容知识,停止后续评估 (得分: {content_eval.final_score})")
- return (knowledge_eval, content_eval, None, None, None, None)
- print(f" ✅ Step 2: 是内容知识 (得分: {content_eval.final_score})")
- # Step 3 & 4: 并行执行目的性和品类匹配
- print(f" 📝 Step 3&4/4: 并行执行目的性和品类匹配...")
- purpose_task = evaluate_purpose_match(post, original_query, semaphore)
- category_task = evaluate_category_match(post, original_query, semaphore)
- purpose_eval, category_eval = await asyncio.gather(purpose_task, category_task)
- if not purpose_eval or not category_eval:
- print(f" ❌ Step 3或4失败")
- return (knowledge_eval, content_eval, purpose_eval, category_eval, None, None)
- print(f" ✅ Step 3: 目的性得分 = {purpose_eval.purpose_score}")
- print(f" ✅ Step 4: 品类得分 = {category_eval.category_score}")
- # Step 5: 计算综合得分
- final_score, match_level = calculate_final_score(
- purpose_eval.purpose_score,
- category_eval.category_score
- )
- print(f" ✅ 综合得分: {final_score} ({match_level})")
- return (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
- def apply_evaluation_v3_to_post(
- post,
- knowledge_eval: Optional[KnowledgeEvaluation],
- content_eval: Optional[ContentKnowledgeEvaluation],
- purpose_eval: Optional[PurposeEvaluation],
- category_eval: Optional[CategoryEvaluation],
- final_score: Optional[float],
- match_level: Optional[str]
- ):
- """
- 将V3评估结果应用到Post对象(覆盖原有字段)
- Args:
- post: Post对象
- knowledge_eval: Prompt1结果
- content_eval: Prompt2结果
- purpose_eval: Prompt3结果
- category_eval: Prompt4结果
- final_score: 综合得分
- match_level: 匹配等级
- """
- # Prompt1: 判断是知识
- if knowledge_eval:
- post.is_knowledge = knowledge_eval.is_knowledge
- post.knowledge_evaluation = {
- "quick_exclude": knowledge_eval.quick_exclude,
- "title_layer": knowledge_eval.title_layer,
- "image_layer": knowledge_eval.image_layer,
- "text_layer": knowledge_eval.text_layer,
- "judgment_logic": knowledge_eval.judgment_logic,
- "core_evidence": knowledge_eval.core_evidence,
- "issues": knowledge_eval.issues,
- "conclusion": knowledge_eval.conclusion
- }
- # Prompt2: 判断是否是内容知识
- if content_eval:
- post.is_content_knowledge = content_eval.is_content_knowledge
- post.knowledge_score = float(content_eval.final_score)
- post.content_knowledge_evaluation = {
- "is_content_knowledge": content_eval.is_content_knowledge,
- "final_score": content_eval.final_score,
- "level": content_eval.level,
- "quick_exclude": content_eval.quick_exclude,
- "dimension_scores": content_eval.dimension_scores,
- "core_evidence": content_eval.core_evidence,
- "issues": content_eval.issues,
- "summary": content_eval.summary
- }
- # Prompt3: 目的性匹配
- if purpose_eval:
- post.purpose_score = purpose_eval.purpose_score
- post.purpose_evaluation = {
- "purpose_score": purpose_eval.purpose_score,
- "core_motivation": purpose_eval.core_motivation,
- "image_value": purpose_eval.image_value,
- "title_intention": purpose_eval.title_intention,
- "text_content": purpose_eval.text_content,
- "match_level": purpose_eval.match_level,
- "core_basis": purpose_eval.core_basis
- }
- # Prompt4: 品类匹配
- if category_eval:
- post.category_score = category_eval.category_score
- post.category_evaluation = {
- "category_score": category_eval.category_score,
- "original_category": category_eval.original_category,
- "actual_category": category_eval.actual_category,
- "match_level": category_eval.match_level,
- "subject_match": category_eval.subject_match,
- "qualifier_match": category_eval.qualifier_match,
- "core_basis": category_eval.core_basis
- }
- # 综合得分
- if final_score is not None and match_level is not None:
- post.final_score = final_score
- post.match_level = match_level
- # 设置评估时间和版本
- post.evaluation_time = datetime.now().isoformat()
- post.evaluator_version = "v3.0"
- async def batch_evaluate_posts_v3(
- posts: list,
- original_query: str,
- max_concurrent: int = MAX_CONCURRENT_EVALUATIONS
- ) -> int:
- """
- 批量评估多个帖子(V3版本)
- Args:
- posts: Post对象列表
- original_query: 原始搜索query
- max_concurrent: 最大并发数
- Returns:
- 成功评估的帖子数量
- """
- semaphore = asyncio.Semaphore(max_concurrent)
- print(f"\n📊 开始批量评估 {len(posts)} 个帖子(并发限制: {max_concurrent})...")
- tasks = [evaluate_post_v3(post, original_query, semaphore) for post in posts]
- results = await asyncio.gather(*tasks)
- success_count = 0
- for i, result in enumerate(results):
- knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = result
- # 只要有Prompt1结果就算部分成功
- if knowledge_eval:
- apply_evaluation_v3_to_post(
- posts[i],
- knowledge_eval,
- content_eval,
- purpose_eval,
- category_eval,
- final_score,
- match_level
- )
- success_count += 1
- print(f"✅ 批量评估完成: {success_count}/{len(posts)} 帖子已评估")
- return success_count
|