post_evaluator_v3.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317
  1. """
  2. 帖子评估模块 V3 - 4步串行+并行评估系统
  3. 改进:
  4. 1. Prompt1: 判断是知识 (is_knowledge)
  5. 2. Prompt2: 判断是否是内容知识 (is_content_knowledge)
  6. 3. Prompt3 & Prompt4: 并行执行 - 目的性(70%) + 品类(30%)
  7. 4. 代码计算综合得分: final_score = purpose × 0.7 + category × 0.3
  8. 5. 完全替代V2评估结果
  9. """
  10. import asyncio
  11. import json
  12. import os
  13. from datetime import datetime
  14. from typing import Optional
  15. from pydantic import BaseModel, Field
  16. import requests
  17. MODEL_NAME = "google/gemini-2.5-flash"
  18. MAX_IMAGES_PER_POST = 10
  19. MAX_CONCURRENT_EVALUATIONS = 5
  20. API_TIMEOUT = 120
  21. # ============================================================================
  22. # 数据模型
  23. # ============================================================================
  24. class KnowledgeEvaluation(BaseModel):
  25. """Prompt1: 判断是知识 - 评估结果"""
  26. is_knowledge: bool = Field(..., description="是否是知识内容")
  27. quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
  28. title_layer: dict = Field(default_factory=dict, description="标题层判断")
  29. image_layer: dict = Field(default_factory=dict, description="图片层判断(核心)")
  30. text_layer: dict = Field(default_factory=dict, description="正文层判断(辅助)")
  31. judgment_logic: str = Field(..., description="综合判定逻辑")
  32. core_evidence: list[str] = Field(default_factory=list, description="核心证据")
  33. issues: list[str] = Field(default_factory=list, description="不足或疑虑")
  34. conclusion: str = Field(..., description="结论陈述")
  35. class ContentKnowledgeEvaluation(BaseModel):
  36. """Prompt2: 判断是否是内容知识 - 评估结果"""
  37. is_content_knowledge: bool = Field(..., description="是否属于内容知识")
  38. final_score: int = Field(..., description="最终得分(0-100)")
  39. level: str = Field(..., description="判定等级")
  40. quick_exclude: dict = Field(default_factory=dict, description="快速排除判定")
  41. dimension_scores: dict = Field(default_factory=dict, description="分层评分详情")
  42. core_evidence: list[str] = Field(default_factory=list, description="核心证据")
  43. issues: list[str] = Field(default_factory=list, description="不足之处")
  44. summary: str = Field(..., description="总结陈述")
  45. class PurposeEvaluation(BaseModel):
  46. """Prompt3: 目的性匹配 - 评估结果"""
  47. purpose_score: int = Field(..., description="目的动机得分(0-100整数)")
  48. core_motivation: str = Field(..., description="原始需求核心动机")
  49. image_value: str = Field(..., description="图片提供的价值")
  50. title_intention: str = Field(..., description="标题体现的意图")
  51. text_content: str = Field(..., description="正文补充的内容")
  52. match_level: str = Field(..., description="匹配度等级")
  53. core_basis: str = Field(..., description="核心依据")
  54. class CategoryEvaluation(BaseModel):
  55. """Prompt4: 品类匹配 - 评估结果"""
  56. category_score: int = Field(..., description="品类匹配得分(0-100整数)")
  57. original_category: dict = Field(default_factory=dict, description="原始需求品类")
  58. actual_category: dict = Field(default_factory=dict, description="帖子实际品类")
  59. match_level: str = Field(..., description="匹配度等级")
  60. subject_match: str = Field(..., description="主体匹配情况")
  61. qualifier_match: str = Field(..., description="限定词匹配情况")
  62. core_basis: str = Field(..., description="核心依据")
  63. # ============================================================================
  64. # Prompt 定义
  65. # ============================================================================
  66. PROMPT1_IS_KNOWLEDGE = """# 知识判定系统 v1.0
  67. ## 角色定义
  68. 你是一个多模态内容评估专家,专门判断社交媒体帖子是否提供了"知识"。
  69. ## 知识定义
  70. **知识**是指经过验证的、具有可靠性和真实性的信息,能够被理解、学习、传播和应用。
  71. ### 知识类型
  72. - ✅ **事实性知识**: 客观事实、数据、现象描述
  73. - ✅ **原理性知识**: 规律、原理、理论、因果关系
  74. - ✅ **方法性知识**: 技能、流程、步骤、操作方法
  75. - ✅ **经验性知识**: 总结提炼的经验、教训、最佳实践
  76. - ✅ **概念性知识**: 定义、分类、框架、体系
  77. - ✅ **应用性知识**: 解决方案、工具使用、实践指南
  78. ### 非知识类型(严格排除)
  79. - ❌ **纯观点/立场**: 未经验证的个人观点、偏好表达
  80. - ❌ **情感表达**: 纯粹的情绪抒发、心情分享
  81. - ❌ **单纯展示**: 作品展示、生活记录、打卡(无知识提炼)
  82. - ❌ **娱乐内容**: 段子、搞笑、八卦(无信息价值)
  83. - ❌ **纯营销/广告**: 单纯的产品推销
  84. - ❌ **虚假/未验证信息**: 谣言、伪科学、未经证实的说法
  85. ---
  86. ## 输入信息
  87. - **标题**: {title}
  88. - **正文**: {body_text}
  89. - **图片**: {num_images}张
  90. ---
  91. ## 判断流程
  92. ### 第一步: 快速排除(任一为"是"则判定为非知识)
  93. 1. 是否为纯情感表达/生活记录/打卡?
  94. 2. 是否为单纯的作品/产品展示(无知识提炼)?
  95. 3. 是否为娱乐搞笑/八卦/纯营销内容?
  96. 4. 是否包含虚假信息或伪科学?
  97. 5. 是否完全没有新信息(纯重复常识)?
  98. **排除判定**: □ 是(判定为非知识) / □ 否(继续评估)
  99. ---
  100. ### 第二步: 分层知识判断
  101. ## 🏷️ 标题层
  102. **判断:标题是否指向知识?**
  103. - ✅ 明确传达知识主题(如何/为什么/什么是/XX方法/XX原理)
  104. - ⚠️ 描述性标题,但暗示有知识内容
  105. - ❌ 展示型(我的XX/今天XX)或情感型标题
  106. **结果**: □ 有知识指向 / □ 无知识指向
  107. ---
  108. ## 🖼️ 图片层(信息主要承载层)
  109. **判断1: 图片知识呈现方式**
  110. - ✅ 包含信息图表(数据、流程图、对比图、结构图)
  111. - ✅ 有知识性标注(解释、说明、步骤、原理)
  112. - ✅ 多图形成知识体系(步骤序列、案例对比)
  113. - ❌ 纯作品展示、美图、氛围图
  114. **判断2: 图片教育价值**
  115. - ✅ 图片能教会他人方法、技能或原理
  116. - ✅ 提供可学习的步骤或解决方案
  117. - ❌ 图片无教学意义
  118. **判断3: 图片结构化程度**
  119. - ✅ 有清晰的逻辑组织(序号、分层、框架)
  120. - ✅ 有步骤、要点的结构化呈现
  121. - ❌ 碎片化、无逻辑的展示
  122. **判断4: 图片实用性**
  123. - ✅ 提供可直接应用的方法或工具
  124. - ✅ 能帮助解决实际问题
  125. - ❌ 纯观赏性,无实际应用价值
  126. **判断5: 图片信息密度**
  127. - ✅ 包含≥3个独立知识点
  128. - ⚠️ 包含1-2个知识点
  129. - ❌ 无明确知识点
  130. **图片层综合评估**: □ 图片传递了知识 / □ 图片无知识价值
  131. ---
  132. ## 📝 正文层(辅助判断)
  133. **判断1: 信息增量**
  134. - ✅ 提供了明确的新信息、新认知或新方法
  135. - ❌ 无新信息,只是个人记录或情感表达
  136. **判断2: 可验证性**
  137. - ✅ 基于事实、数据、可验证的经验
  138. - ❌ 纯主观观点或感受,无依据
  139. **判断3: 知识类型归属**
  140. - ✅ 能归入至少一种知识类型(事实/原理/方法/经验/概念/应用)
  141. - ❌ 无法归类为任何知识类型
  142. **正文层综合评估**: □ 正文提供了知识支撑 / □ 正文无知识价值
  143. ---
  144. ### 第三步: 综合判定
  145. #### 判定规则
  146. **直接判定为"非知识"(任一成立)**:
  147. - 未通过快速排除
  148. - 图片层 = 无知识价值 且 正文层 = 无知识支撑
  149. - 正文判断3(知识类型)= 无法归类
  150. **判定为"是知识"(需同时满足)**:
  151. 1. 通过快速排除
  152. 2. 图片层 = 传递了知识 或 正文层 = 提供了知识支撑
  153. 3. 正文判断1(信息增量)= 有新信息
  154. 4. 正文判断3(知识类型)= 可归类
  155. **特别说明**:
  156. - 社交媒体帖子以图片为主要信息载体,图片层权重最高
  157. - 标题为辅助判断,正文为补充验证
  158. ---
  159. ## 输出格式
  160. 请严格按照以下JSON格式输出:
  161. {{
  162. "is_knowledge": true/false,
  163. "quick_exclude": {{
  164. "result": "通过/未通过",
  165. "reason": "如未通过,说明原因"
  166. }},
  167. "title_layer": {{
  168. "has_knowledge_direction": true/false,
  169. "reason": "一句话说明"
  170. }},
  171. "image_layer": {{
  172. "knowledge_presentation": {{"match": true/false, "reason": "简述"}},
  173. "educational_value": {{"has_value": true/false, "reason": "简述"}},
  174. "structure_level": {{"structured": true/false, "reason": "简述"}},
  175. "practicality": {{"practical": true/false, "reason": "简述"}},
  176. "information_density": {{"level": "高/中/低", "reason": "简述"}},
  177. "overall": "传递知识/无知识价值"
  178. }},
  179. "text_layer": {{
  180. "information_gain": {{"has_gain": true/false, "reason": "简述"}},
  181. "verifiability": {{"verifiable": true/false, "reason": "简述"}},
  182. "knowledge_type": {{"type": "具体类型/无法归类", "reason": "简述"}},
  183. "overall": "有知识支撑/无知识价值"
  184. }},
  185. "judgment_logic": "说明是否满足判定规则,关键依据是什么",
  186. "core_evidence": [
  187. "最强支持证据1",
  188. "最强支持证据2"
  189. ],
  190. "issues": [
  191. "不足或疑虑(如无则为空数组)"
  192. ],
  193. "conclusion": "用1-2句话说明判定结果和核心理由"
  194. }}
  195. ## 判断原则
  196. 1. **图片优先**: 社交媒体以图片为主要信息载体,图片层是核心判断依据,标题和正文信息辅助
  197. 2. **严格性**: 宁可误判为"非知识",也不放过无价值内容
  198. 3. **证据性**: 基于明确的视觉和文本证据判断
  199. 4. **价值导向**: 优先判断内容对读者是否有实际学习价值
  200. """
  201. PROMPT2_IS_CONTENT_KNOWLEDGE = """# 内容知识判定系统 v2.0
  202. ## 角色定义
  203. 你是一个多模态内容评估专家,专门判断社交媒体帖子是否属于"内容知识"类别。
  204. ## 前置条件
  205. 该帖子已通过知识判定,确认提供了知识。现在需要进一步判断是否属于"内容知识"。
  206. ---
  207. ## 内容知识定义
  208. **内容知识**是指与创作/制作/设计相关的、具有实操性和可迁移性的知识,帮助创作者提升创作能力。
  209. ### 内容知识的范畴
  210. - ✅ **创作原理**: 设计原理、创作逻辑、美学规律、构图法则(通用的,普适的)
  211. - ✅ **制作方法**: 操作流程、技术步骤、工具使用方法
  212. - ✅ **创意技巧**: 灵感方法、创意思路、表现手法、风格技法
  213. - ✅ **体系框架**: 完整的创作体系、方法论、思维框架
  214. - ✅ **案例提炼**: 从多个案例中总结的通用创作规律
  215. ### 非内容知识(严格排除)
  216. - ❌ **单案例展示**: 仅展示单个作品,无方法论提炼
  217. - ❌ **作品集合**: 纯作品展示集合,无创作方法讲解
  218. - ❌ **单点元素**: 只展示配色/字体/素材,无使用方法
  219. - ❌ **单次操作**: 只讲某个项目的特定操作,无通用性
  220. - ❌ **非创作领域**: 健康、财经、生活、科普等非创作制作领域的知识
  221. ---
  222. ## 输入信息
  223. - **标题**: {title}
  224. - **正文**: {body_text}
  225. - **图片数量**: {num_images}张
  226. ---
  227. ## 判断流程
  228. ### 第一步: 快速排除判断(任一为"是"则判定为非内容知识)
  229. 1. 标题是否为纯展示型?("我的XX作品"、"今天做了XX"、"作品分享")
  230. 2. 图片是否全为作品展示,无任何方法/原理/步骤说明?
  231. 3. 是否只讲单个项目的特定操作,完全无通用性?
  232. 4. 是否为纯元素展示,无创作方法?(仅展示配色、字体、素材)
  233. **排除判定**: □ 是(判定为非内容知识) / □ 否(继续评估)
  234. ---
  235. ### 第二步: 分层打分评估(满分100分)
  236. ## 🖼️ 图片层评估(权重70%,满分70分)
  237. > **说明**: 社交媒体以图片为主要信息载体,图片层是核心判断依据
  238. #### 维度1: 创作方法呈现(20分)
  239. **评分依据**: 图片是否清晰展示了具体的创作/制作方法、技巧、技法
  240. - **20分**: 图片详细展示≥3个具体可操作的创作方法/技巧,有明确的操作指引
  241. - **15分**: 图片展示2个创作方法,方法较为具体
  242. - **10分**: 图片展示1个创作方法,但不够详细
  243. - **5分**: 图片暗示有方法,但未明确展示
  244. - **0分**: 图片无任何方法展示,纯作品呈现
  245. **得分**: __/20
  246. ---
  247. #### 维度2: 知识体系化程度(15分)
  248. **评分依据**: 多图是否形成完整的知识体系或逻辑链条
  249. - **15分**: 多图形成完整体系(步骤1→2→3,或原理→方法→案例),逻辑清晰
  250. - **12分**: 多图有知识关联性,形成部分体系
  251. - **8分**: 多图展示多个知识点,但关联性弱
  252. - **4分**: 多图仅为同类案例堆砌,无体系
  253. - **0分**: 单图或多图无逻辑关联
  254. **得分**: __/15
  255. ---
  256. #### 维度3: 教学性标注与说明(15分)
  257. **评分依据**: 图片是否包含教学性的视觉元素(标注、序号、箭头、文字说明)
  258. - **15分**: 大量教学标注(序号、箭头、高亮、文字说明、对比标记等),清晰易懂
  259. - **12分**: 有明显的教学标注,但不够完善
  260. - **8分**: 有少量标注或说明
  261. - **4分**: 仅有简单文字,无视觉教学元素
  262. - **0分**: 无任何教学标注,纯视觉展示
  263. **得分**: __/15
  264. ---
  265. #### 维度4: 方法可复用性(10分)
  266. **评分依据**: 图片展示的方法是否可迁移到其他创作场景/项目
  267. - **10分**: 明确展示通用方法,可应用于多种场景(配公式/模板/框架)
  268. - **8分**: 方法有一定通用性,可迁移到类似场景
  269. - **5分**: 方法通用性一般,需要改造才能应用
  270. - **2分**: 方法仅适用于特定项目
  271. - **0分**: 无可复用方法
  272. **得分**: __/10
  273. ---
  274. #### 维度5: 原理与案例结合(10分)
  275. **评分依据**: 图片是否将创作原理与实际案例有效结合
  276. - **10分**: 原理+多案例验证,清晰展示原理如何应用
  277. - **8分**: 原理+案例,有一定结合
  278. - **5分**: 有原理或有案例,但结合不够
  279. - **2分**: 仅有案例,无原理提炼
  280. - **0分**: 纯案例展示或纯理论
  281. **得分**: __/10
  282. ---
  283. **🖼️ 图片层总分**: __/70
  284. ---
  285. ## 📝 正文层评估(权重20%,满分20分)
  286. > **说明**: 正文作为辅助判断,补充图片未完整呈现的知识信息
  287. #### 维度6: 方法/步骤描述(10分)
  288. **评分依据**: 正文是否描述了具体的创作方法或操作步骤
  289. - **10分**: 有完整的步骤描述(≥3步)或详细的方法说明
  290. - **7分**: 有步骤或方法描述,但不够系统
  291. - **4分**: 有零散的方法提及
  292. - **0分**: 无方法/步骤,纯叙事或展示性文字
  293. **得分**: __/10
  294. ---
  295. #### 维度7: 知识总结与提炼(10分)
  296. **评分依据**: 正文是否对创作经验/规律进行总结提炼
  297. - **10分**: 有明确的知识总结、归纳、框架化输出
  298. - **7分**: 有一定的经验总结或要点提炼
  299. - **4分**: 有零散的心得,但未成体系
  300. - **0分**: 无任何知识提炼
  301. **得分**: __/10
  302. ---
  303. **📝 正文层总分**: __/20
  304. ---
  305. ## 🏷️ 标题层评估(权重10%,满分10分)
  306. > **说明**: 标题作为内容导向,辅助判断内容主题
  307. #### 维度8: 标题内容指向性(10分)
  308. **评分依据**: 标题是否明确指向创作/制作相关的知识内容
  309. - **10分**: 标题明确包含方法/教程/技巧/原理类词汇("XX教程"、"XX技巧"、"如何XX"、"XX方法")
  310. - **7分**: 标题包含整理型词汇("合集"、"总结"、"分享XX方法")
  311. - **4分**: 描述性标题,暗示有创作知识
  312. - **0分**: 纯展示型标题("我的作品"、"今天做了XX")或与创作无关
  313. **得分**: __/10
  314. ---
  315. **🏷️标题层总分**: __/10
  316. ---
  317. ### 第三步: 综合评分与判定
  318. **总分计算**:
  319. 总分 = 图片层总分(70分) + 正文层总分(20分) + 标题层总分(10分)
  320. **最终得分**: __/100分
  321. ---
  322. **判定等级**:
  323. - **85-100分**: ⭐⭐⭐⭐⭐ 优质内容知识 - 强烈符合
  324. - **70-84分**: ⭐⭐⭐⭐ 良好内容知识 - 符合
  325. - **55-69分**: ⭐⭐⭐ 基础内容知识 - 基本符合
  326. - **40-54分**: ⭐⭐ 弱内容知识 - 不太符合
  327. - **0-39分**: ⭐ 非内容知识 - 不符合
  328. ---
  329. ## 输出格式
  330. 请严格按照以下JSON格式输出:
  331. {{
  332. "is_content_knowledge": true/false,
  333. "final_score": 85,
  334. "level": "⭐⭐⭐⭐⭐ 优质内容知识",
  335. "quick_exclude": {{
  336. "result": "是/否",
  337. "reason": "原因"
  338. }},
  339. "dimension_scores": {{
  340. "image_layer": {{
  341. "creation_method": {{"score": 20, "reason": "简述"}},
  342. "knowledge_system": {{"score": 15, "reason": "简述"}},
  343. "teaching_annotation": {{"score": 15, "reason": "简述"}},
  344. "method_reusability": {{"score": 10, "reason": "简述"}},
  345. "principle_case": {{"score": 10, "reason": "简述"}},
  346. "subtotal": 70
  347. }},
  348. "text_layer": {{
  349. "method_description": {{"score": 10, "reason": "简述"}},
  350. "knowledge_summary": {{"score": 10, "reason": "简述"}},
  351. "subtotal": 20
  352. }},
  353. "title_layer": {{
  354. "content_direction": {{"score": 10, "reason": "简述"}},
  355. "subtotal": 10
  356. }}
  357. }},
  358. "core_evidence": [
  359. "证据1",
  360. "证据2"
  361. ],
  362. "issues": [
  363. "问题1(如无则为空数组)"
  364. ],
  365. "summary": "总结陈述(2-3句话)"
  366. }}
  367. ## 判断原则
  368. 1. **图片主导原则**: 图片占70%权重,是核心判断依据;标题和正文为辅助
  369. 2. **创作领域限定**: 必须属于创作/制作/设计领域,其他领域知识不属于内容知识
  370. 3. **方法优先原则**: 重点评估是否提供了可操作的创作方法,而非纯作品展示
  371. 4. **通用性要求**: 优先考虑方法的可复用性和可迁移性
  372. 5. **严格性原则**: 宁可误判为"非内容知识",也不放过纯展示型内容
  373. 6. **证据性原则**: 评分需基于明确的视觉和文本证据,可量化衡量
  374. """
  375. PROMPT3_PURPOSE_MATCH = """# Prompt 1: 多模态内容目的动机匹配评估
  376. ## 角色定义
  377. 你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**目的动机匹配度**,能够精准判断帖子是否满足用户的核心意图。
  378. ---
  379. ## 任务说明
  380. 你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文)
  381. 请**仅评估目的动机维度**的匹配度,输出0-100分的量化得分。
  382. ---
  383. ## 输入格式
  384. **原始搜索需求:**
  385. {original_query}
  386. **多模态帖子内容:**
  387. - **图片:** {num_images}张
  388. - **标题:** {title}
  389. - **正文:** {body_text}
  390. ---
  391. ## 评估维度:目的动机匹配
  392. ### 核心评估逻辑
  393. **目的动机 = 用户想做什么 = 核心动词/意图**
  394. 常见动机类型:
  395. - **获取型**:寻找、下载、收藏、获取
  396. - **学习型**:教程、学习、了解、掌握
  397. - **决策型**:推荐、对比、评测、选择
  398. - **创作型**:拍摄、制作、设计、生成
  399. - **分享型**:晒单、记录、分享、展示
  400. ---
  401. ## 评估流程
  402. ### 第一步:识别原始需求的核心动机
  403. - 提取**核心动词**(如果是纯名词短语,识别隐含意图)
  404. - 判断用户的**最终目的**是什么
  405. ### 第二步:分析帖子提供的价值(重点看图片)
  406. **图片分析(权重70%):**
  407. - 图片展示的是什么类型的内容?
  408. - 图片是否直接解答了需求的目的?
  409. - 图片的信息完整度和实用性如何?
  410. **标题分析(权重15%):**
  411. - 标题是否明确了内容的目的?
  412. **正文分析(权重15%):**
  413. - 正文是否提供了实质性的解答内容?
  414. ### 第三步:判断目的匹配度
  415. - 帖子是否**实质性地满足**了需求的动机?
  416. - 内容是否**实用、完整、可执行**?
  417. ---
  418. ## 评分标准(0-100分)
  419. ### 高度匹配区间
  420. **90-100分:完全满足动机,内容实用完整**
  421. - 图片直接展示解决方案/教程步骤/对比结果
  422. - 内容完整、清晰、可直接使用
  423. - 例:需求"如何拍摄夜景" vs 图片展示完整的夜景拍摄参数设置和效果对比
  424. **75-89分:基本满足动机,信息较全面**
  425. - 图片提供了核心解答内容
  426. - 信息相对完整但深度略有不足
  427. - 例:需求"推荐旅行路线" vs 图片展示了路线图但缺少详细说明
  428. **60-74分:部分满足动机,有参考价值**
  429. - 图片提供了相关内容但不够直接
  430. - 需要结合文字才能理解完整意图
  431. ### 中度相关区间
  432. **40-59分:弱相关,核心目的未充分满足**
  433. - 图片内容与动机有关联但不是直接解答
  434. - 实用性较低
  435. - 例:需求"如何拍摄" vs 图片只展示成品照片,无教程内容
  436. ### 不相关/负向区间
  437. **20-39分:微弱关联,基本未解答**
  438. - 图片仅有外围相关性
  439. - 对满足需求帮助极小
  440. **1-19分:几乎无关**
  441. - 图片与需求动机关联极弱
  442. **0分:完全不相关**
  443. - 图片与需求动机无任何关联
  444. **负分不使用**(目的动机维度不设负分)
  445. ---
  446. ## 输出格式(JSON)
  447. ```json
  448. {{
  449. "目的动机评估": {{
  450. "目的动机得分": 0-100的整数,
  451. "原始需求核心动机": "识别出的用户意图(一句话)",
  452. "图片提供的价值": "图片展示了什么,如何满足动机",
  453. "标题体现的意图": "标题说明了什么",
  454. "正文补充的内容": "正文是否有实质解答",
  455. "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
  456. "核心依据": "为什么给这个分数(100字以内)"
  457. }}
  458. }}
  459. ```
  460. ---
  461. ## 评估原则
  462. 1. **图片优先**:图片权重70%,是判断的主要依据
  463. 2. **实用导向**:不看表面相关,看实际解答程度
  464. 3. **严格标准**:宁可低估,避免虚高
  465. 4. **客观量化**:基于可观察的内容特征打分
  466. ---
  467. ## 特别注意
  468. - 本评估**只关注目的动机维度**,不考虑品类是否匹配
  469. - 输出的分数必须是**0-100的整数**
  470. - 不要自行计算综合分数,只输出目的动机分数
  471. - 评分依据要具体、可验证
  472. """
  473. PROMPT4_CATEGORY_MATCH = """# Prompt 2: 多模态内容品类匹配评估
  474. ## 角色定义
  475. 你是一位专业的多模态内容评估专家,擅长分析社交媒体UGC平台帖子的**品类匹配度**
  476. 能够精准判断帖子的内容主体是否与用户需求一致。
  477. ---
  478. ## 任务说明
  479. 你将收到一个**原始搜索需求**和一条**多模态帖子**(包含图片、标题、正文),请**仅评估品类维度**的匹配度,输出0-100分的量化得分。
  480. ---
  481. ## 输入格式
  482. **原始搜索需求:**
  483. {original_query}
  484. **多模态帖子内容:**
  485. - **图片:** {num_images}张
  486. - **标题:** {title}
  487. - **正文:** {body_text}
  488. ---
  489. ## 评估维度:品类匹配
  490. ### 核心评估逻辑
  491. **品类 = 核心主体(名词)+ 限定词**
  492. - **核心主体**:具体的内容对象(风光摄影、旅行攻略、美食推荐)
  493. - **限定词**:
  494. - 地域:川西、成都、日本
  495. - 时间:秋季、夏天、2024
  496. - 类型:免费、高清、入门级
  497. - 风格:小清新、复古、简约
  498. ---
  499. ## 评估流程
  500. ### 第一步:提取原始需求的品类信息
  501. - 识别**核心主体名词**
  502. - 识别**关键限定词**(地域/时间/类型/风格等)
  503. ### 第二步:从帖子中提取品类信息(重点看图片)
  504. **图片识别(权重70%):**
  505. - 图片展示的核心主体是什么?
  506. - 图片中可识别的限定特征(地域标志、季节特征、类型属性、风格特点)
  507. **标题提取(权重15%):**
  508. - 标题明确的品类名词和限定词
  509. **正文提取(权重15%):**
  510. - 正文描述的品类信息
  511. ### 第三步:对比匹配度
  512. - 核心主体是否一致?
  513. - 限定词匹配了几个?
  514. - 是否存在泛化或偏移?
  515. ---
  516. ## 评分标准(0-100分)
  517. ### 高度匹配区间
  518. **90-100分:核心主体+关键限定词完全匹配**
  519. - 图片展示的主体与需求精准一致
  520. - 关键限定词全部匹配(地域、时间、类型等)
  521. - 例:需求"川西秋季风光" vs 图片展示川西秋季风景
  522. **75-89分:核心主体匹配,限定词匹配度百分之80**
  523. - 图片主体一致
  524. - 存在1-2个限定词缺失但不影响核心匹配
  525. - 例:需求"川西秋季风光" vs 图片展示川西风光(缺秋季)
  526. **60-74分:核心主体匹配,限定词匹配度百分之60**
  527. - 图片主体在同一大类
  528. - 限定词部分匹配或有合理上下位关系
  529. - 例:需求"川西秋季风光" vs 图片展示四川风光
  530. ### 中度相关区间
  531. **40-59分:核心主体匹配,限定词完全不匹配**
  532. - 图片主体相同但上下文不同
  533. - 限定词严重缺失或不匹配
  534. - 例:需求"猫咪表情包梗图" vs 女孩表情包
  535. ### 不相关/负向区间
  536. **20-39分:主体过度泛化**
  537. - 图片主体是通用概念,需求是特定概念
  538. - 仅有抽象类别相似
  539. - 例:需求"川西旅行攻略" vs 图片展示普通旅行场景
  540. **1-19分:品类关联极弱**
  541. - 图片主体与需求差异明显
  542. **0分:品类完全不同**
  543. - 图片主体类别完全不同
  544. - 例:需求"风光摄影" vs 图片展示美食
  545. **负分不使用**(品类维度不设负分)
  546. ---
  547. ## 输出格式(JSON)
  548. ```json
  549. {{
  550. "品类评估": {{
  551. "原始需求品类": {{
  552. "核心主体": "提取的主体名词",
  553. "关键限定词": ["限定词1", "限定词2"]
  554. }},
  555. "帖子实际品类": {{
  556. "图片主体": "图片展示的核心主体",
  557. "图片限定特征": ["从图片识别的限定词"],
  558. "标题品类": "标题提及的品类",
  559. "正文品类": "正文描述的品类"
  560. }},
  561. "品类匹配得分": 0-100的整数,
  562. "匹配度等级": "完全匹配/高度匹配/基本匹配/弱匹配/不匹配",
  563. "主体匹配情况": "主体是否一致",
  564. "限定词匹配情况": "哪些限定词匹配/缺失",
  565. "核心依据": "为什么给这个分数(100字以内)"
  566. }}
  567. }}
  568. ```
  569. ---
  570. ## 评估原则
  571. 1. **图片优先**:图片权重70%,是判断的主要依据
  572. 2. **表面匹配**:只看实际展示的内容,禁止推测联想
  573. 3. **通用≠特定**:通用概念不等于特定概念,需明确区分
  574. 4. **严格标准**:宁可低估,避免虚高
  575. 5. **客观量化**:基于可观察的视觉特征和文字信息打分
  576. ---
  577. ## 特别注意
  578. - 本评估**只关注品类维度**,不考虑目的是否匹配
  579. - 输出的分数必须是**0-100的整数**
  580. - 不要自行计算综合分数,只输出品类分数
  581. - 禁止因为"可能相关"就给分,必须有明确视觉证据
  582. """
  583. # ============================================================================
  584. # 辅助函数
  585. # ============================================================================
  586. def _clean_json_response(content_text: str) -> str:
  587. """清理API返回的JSON内容"""
  588. content_text = content_text.strip()
  589. if content_text.startswith("```json"):
  590. content_text = content_text[7:]
  591. elif content_text.startswith("```"):
  592. content_text = content_text[3:]
  593. if content_text.endswith("```"):
  594. content_text = content_text[:-3]
  595. return content_text.strip()
  596. async def _call_openrouter_api(
  597. prompt_text: str,
  598. image_urls: list[str],
  599. semaphore: Optional[asyncio.Semaphore] = None
  600. ) -> dict:
  601. """
  602. 调用OpenRouter API的通用函数
  603. Args:
  604. prompt_text: Prompt文本
  605. image_urls: 图片URL列表
  606. semaphore: 并发控制信号量
  607. Returns:
  608. 解析后的JSON响应
  609. """
  610. api_key = os.getenv("OPENROUTER_API_KEY")
  611. if not api_key:
  612. raise ValueError("OPENROUTER_API_KEY environment variable not set")
  613. content = [{"type": "text", "text": prompt_text}]
  614. for url in image_urls:
  615. content.append({"type": "image_url", "image_url": {"url": url}})
  616. payload = {
  617. "model": MODEL_NAME,
  618. "messages": [{"role": "user", "content": content}],
  619. "response_format": {"type": "json_object"}
  620. }
  621. headers = {
  622. "Authorization": f"Bearer {api_key}",
  623. "Content-Type": "application/json"
  624. }
  625. async def _make_request():
  626. loop = asyncio.get_event_loop()
  627. response = await loop.run_in_executor(
  628. None,
  629. lambda: requests.post(
  630. "https://openrouter.ai/api/v1/chat/completions",
  631. headers=headers,
  632. json=payload,
  633. timeout=API_TIMEOUT
  634. )
  635. )
  636. if response.status_code != 200:
  637. raise Exception(f"API error: {response.status_code} - {response.text[:200]}")
  638. result = response.json()
  639. content_text = result["choices"][0]["message"]["content"]
  640. content_text = _clean_json_response(content_text)
  641. return json.loads(content_text)
  642. async def _execute_with_retry():
  643. """执行API请求,失败时自动重试最多2次"""
  644. MAX_RETRIES = 2
  645. for attempt in range(MAX_RETRIES + 1):
  646. try:
  647. return await _make_request()
  648. except Exception as e:
  649. if attempt < MAX_RETRIES:
  650. wait_time = 2 * (attempt + 1) # 2秒, 4秒
  651. print(f" ⚠️ API调用失败,{wait_time}秒后重试 (第{attempt + 1}/{MAX_RETRIES}次重试) - {str(e)[:50]}")
  652. await asyncio.sleep(wait_time)
  653. else:
  654. # 最后一次尝试也失败,抛出异常
  655. raise
  656. if semaphore:
  657. async with semaphore:
  658. return await _execute_with_retry()
  659. else:
  660. return await _execute_with_retry()
  661. # ============================================================================
  662. # 核心评估函数
  663. # ============================================================================
  664. async def evaluate_is_knowledge(
  665. post,
  666. semaphore: Optional[asyncio.Semaphore] = None
  667. ) -> Optional[KnowledgeEvaluation]:
  668. """
  669. Prompt1: 判断是知识
  670. Args:
  671. post: Post对象
  672. semaphore: 并发控制信号量
  673. Returns:
  674. KnowledgeEvaluation 或 None(失败时)
  675. """
  676. if post.type == "video":
  677. return None
  678. image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
  679. try:
  680. prompt_text = PROMPT1_IS_KNOWLEDGE.format(
  681. title=post.title,
  682. body_text=post.body_text or "",
  683. num_images=len(image_urls)
  684. )
  685. data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
  686. return KnowledgeEvaluation(
  687. is_knowledge=data.get("is_knowledge", False),
  688. quick_exclude=data.get("quick_exclude", {}),
  689. title_layer=data.get("title_layer", {}),
  690. image_layer=data.get("image_layer", {}),
  691. text_layer=data.get("text_layer", {}),
  692. judgment_logic=data.get("judgment_logic", ""),
  693. core_evidence=data.get("core_evidence", []),
  694. issues=data.get("issues", []),
  695. conclusion=data.get("conclusion", "")
  696. )
  697. except Exception as e:
  698. print(f" ❌ Prompt1评估失败: {post.note_id} - {str(e)[:100]}")
  699. return None
  700. async def evaluate_is_content_knowledge(
  701. post,
  702. semaphore: Optional[asyncio.Semaphore] = None
  703. ) -> Optional[ContentKnowledgeEvaluation]:
  704. """
  705. Prompt2: 判断是否是内容知识
  706. Args:
  707. post: Post对象
  708. semaphore: 并发控制信号量
  709. Returns:
  710. ContentKnowledgeEvaluation 或 None(失败时)
  711. """
  712. if post.type == "video":
  713. return None
  714. image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
  715. try:
  716. prompt_text = PROMPT2_IS_CONTENT_KNOWLEDGE.format(
  717. title=post.title,
  718. body_text=post.body_text or "",
  719. num_images=len(image_urls)
  720. )
  721. data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
  722. # 判定是否是内容知识:得分 >= 55 分
  723. final_score = data.get("final_score", 0)
  724. is_content_knowledge = final_score >= 55
  725. return ContentKnowledgeEvaluation(
  726. is_content_knowledge=is_content_knowledge,
  727. final_score=final_score,
  728. level=data.get("level", ""),
  729. quick_exclude=data.get("quick_exclude", {}),
  730. dimension_scores=data.get("dimension_scores", {}),
  731. core_evidence=data.get("core_evidence", []),
  732. issues=data.get("issues", []),
  733. summary=data.get("summary", "")
  734. )
  735. except Exception as e:
  736. print(f" ❌ Prompt2评估失败: {post.note_id} - {str(e)[:100]}")
  737. return None
  738. async def evaluate_purpose_match(
  739. post,
  740. original_query: str,
  741. semaphore: Optional[asyncio.Semaphore] = None
  742. ) -> Optional[PurposeEvaluation]:
  743. """
  744. Prompt3: 目的性匹配评估
  745. Args:
  746. post: Post对象
  747. original_query: 原始搜索query
  748. semaphore: 并发控制信号量
  749. Returns:
  750. PurposeEvaluation 或 None(失败时)
  751. """
  752. if post.type == "video":
  753. return None
  754. image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
  755. try:
  756. prompt_text = PROMPT3_PURPOSE_MATCH.format(
  757. original_query=original_query,
  758. title=post.title,
  759. body_text=post.body_text or "",
  760. num_images=len(image_urls)
  761. )
  762. data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
  763. # Prompt3的输出在"目的动机评估"键下
  764. purpose_data = data.get("目的动机评估", {})
  765. return PurposeEvaluation(
  766. purpose_score=purpose_data.get("目的动机得分", 0),
  767. core_motivation=purpose_data.get("原始需求核心动机", ""),
  768. image_value=purpose_data.get("图片提供的价值", ""),
  769. title_intention=purpose_data.get("标题体现的意图", ""),
  770. text_content=purpose_data.get("正文补充的内容", ""),
  771. match_level=purpose_data.get("匹配度等级", ""),
  772. core_basis=purpose_data.get("核心依据", "")
  773. )
  774. except Exception as e:
  775. print(f" ❌ Prompt3评估失败: {post.note_id} - {str(e)[:100]}")
  776. return None
  777. async def evaluate_category_match(
  778. post,
  779. original_query: str,
  780. semaphore: Optional[asyncio.Semaphore] = None
  781. ) -> Optional[CategoryEvaluation]:
  782. """
  783. Prompt4: 品类匹配评估
  784. Args:
  785. post: Post对象
  786. original_query: 原始搜索query
  787. semaphore: 并发控制信号量
  788. Returns:
  789. CategoryEvaluation 或 None(失败时)
  790. """
  791. if post.type == "video":
  792. return None
  793. image_urls = post.images[:MAX_IMAGES_PER_POST] if post.images else []
  794. try:
  795. prompt_text = PROMPT4_CATEGORY_MATCH.format(
  796. original_query=original_query,
  797. title=post.title,
  798. body_text=post.body_text or "",
  799. num_images=len(image_urls)
  800. )
  801. data = await _call_openrouter_api(prompt_text, image_urls, semaphore)
  802. # Prompt4的输出在"品类评估"键下
  803. category_data = data.get("品类评估", {})
  804. return CategoryEvaluation(
  805. category_score=category_data.get("品类匹配得分", 0),
  806. original_category=category_data.get("原始需求品类", {}),
  807. actual_category=category_data.get("帖子实际品类", {}),
  808. match_level=category_data.get("匹配度等级", ""),
  809. subject_match=category_data.get("主体匹配情况", ""),
  810. qualifier_match=category_data.get("限定词匹配情况", ""),
  811. core_basis=category_data.get("核心依据", "")
  812. )
  813. except Exception as e:
  814. print(f" ❌ Prompt4评估失败: {post.note_id} - {str(e)[:100]}")
  815. return None
  816. def calculate_final_score(purpose_score: int, category_score: int) -> tuple[float, str]:
  817. """
  818. 计算综合得分和匹配等级
  819. Args:
  820. purpose_score: 目的性得分 (0-100整数)
  821. category_score: 品类得分 (0-100整数)
  822. Returns:
  823. (final_score, match_level)
  824. - final_score: 保留2位小数
  825. - match_level: 匹配等级字符串
  826. """
  827. # 计算综合得分: 目的性70% + 品类30%
  828. final = round(purpose_score * 0.7 + category_score * 0.3, 2)
  829. # 判定匹配等级
  830. if final >= 85:
  831. level = "高度匹配"
  832. elif final >= 70:
  833. level = "基本匹配"
  834. elif final >= 50:
  835. level = "部分匹配"
  836. elif final >= 30:
  837. level = "弱匹配"
  838. else:
  839. level = "不匹配"
  840. return final, level
  841. async def evaluate_post_v3(
  842. post,
  843. original_query: str,
  844. semaphore: Optional[asyncio.Semaphore] = None
  845. ) -> tuple:
  846. """
  847. V3评估主函数(4步流程)
  848. 流程:
  849. 1. Prompt1: 判断是知识 → 如果不是知识,停止
  850. 2. Prompt2: 判断是否是内容知识 → 如果不是内容知识,停止
  851. 3. Prompt3 & Prompt4: 并行执行目的性和品类匹配
  852. 4. 计算综合得分
  853. Returns:
  854. (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
  855. 任一步骤失败,后续结果为None
  856. """
  857. if post.type == "video":
  858. print(f" ⊗ 跳过视频帖子: {post.note_id}")
  859. return (None, None, None, None, None, None)
  860. print(f" 🔍 开始V3评估: {post.note_id}")
  861. # Step 1: 判断是知识
  862. print(f" 📝 Step 1/4: 判断是知识...")
  863. knowledge_eval = await evaluate_is_knowledge(post, semaphore)
  864. if not knowledge_eval:
  865. print(f" ❌ Step 1失败,停止评估")
  866. return (None, None, None, None, None, None)
  867. if not knowledge_eval.is_knowledge:
  868. print(f" ⊗ 非知识内容,停止后续评估")
  869. return (knowledge_eval, None, None, None, None, None)
  870. print(f" ✅ Step 1: 是知识内容")
  871. # Step 2: 判断是否是内容知识
  872. print(f" 📝 Step 2/4: 判断是否是内容知识...")
  873. content_eval = await evaluate_is_content_knowledge(post, semaphore)
  874. if not content_eval:
  875. print(f" ❌ Step 2失败,停止评估")
  876. return (knowledge_eval, None, None, None, None, None)
  877. if not content_eval.is_content_knowledge:
  878. print(f" ⊗ 非内容知识,停止后续评估 (得分: {content_eval.final_score})")
  879. return (knowledge_eval, content_eval, None, None, None, None)
  880. print(f" ✅ Step 2: 是内容知识 (得分: {content_eval.final_score})")
  881. # Step 3 & 4: 并行执行目的性和品类匹配
  882. print(f" 📝 Step 3&4/4: 并行执行目的性和品类匹配...")
  883. purpose_task = evaluate_purpose_match(post, original_query, semaphore)
  884. category_task = evaluate_category_match(post, original_query, semaphore)
  885. purpose_eval, category_eval = await asyncio.gather(purpose_task, category_task)
  886. if not purpose_eval or not category_eval:
  887. print(f" ❌ Step 3或4失败")
  888. return (knowledge_eval, content_eval, purpose_eval, category_eval, None, None)
  889. print(f" ✅ Step 3: 目的性得分 = {purpose_eval.purpose_score}")
  890. print(f" ✅ Step 4: 品类得分 = {category_eval.category_score}")
  891. # Step 5: 计算综合得分
  892. final_score, match_level = calculate_final_score(
  893. purpose_eval.purpose_score,
  894. category_eval.category_score
  895. )
  896. print(f" ✅ 综合得分: {final_score} ({match_level})")
  897. return (knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level)
  898. def apply_evaluation_v3_to_post(
  899. post,
  900. knowledge_eval: Optional[KnowledgeEvaluation],
  901. content_eval: Optional[ContentKnowledgeEvaluation],
  902. purpose_eval: Optional[PurposeEvaluation],
  903. category_eval: Optional[CategoryEvaluation],
  904. final_score: Optional[float],
  905. match_level: Optional[str]
  906. ):
  907. """
  908. 将V3评估结果应用到Post对象(覆盖原有字段)
  909. Args:
  910. post: Post对象
  911. knowledge_eval: Prompt1结果
  912. content_eval: Prompt2结果
  913. purpose_eval: Prompt3结果
  914. category_eval: Prompt4结果
  915. final_score: 综合得分
  916. match_level: 匹配等级
  917. """
  918. # Prompt1: 判断是知识
  919. if knowledge_eval:
  920. post.is_knowledge = knowledge_eval.is_knowledge
  921. post.knowledge_evaluation = {
  922. "quick_exclude": knowledge_eval.quick_exclude,
  923. "title_layer": knowledge_eval.title_layer,
  924. "image_layer": knowledge_eval.image_layer,
  925. "text_layer": knowledge_eval.text_layer,
  926. "judgment_logic": knowledge_eval.judgment_logic,
  927. "core_evidence": knowledge_eval.core_evidence,
  928. "issues": knowledge_eval.issues,
  929. "conclusion": knowledge_eval.conclusion
  930. }
  931. # Prompt2: 判断是否是内容知识
  932. if content_eval:
  933. post.is_content_knowledge = content_eval.is_content_knowledge
  934. post.knowledge_score = float(content_eval.final_score)
  935. post.content_knowledge_evaluation = {
  936. "is_content_knowledge": content_eval.is_content_knowledge,
  937. "final_score": content_eval.final_score,
  938. "level": content_eval.level,
  939. "quick_exclude": content_eval.quick_exclude,
  940. "dimension_scores": content_eval.dimension_scores,
  941. "core_evidence": content_eval.core_evidence,
  942. "issues": content_eval.issues,
  943. "summary": content_eval.summary
  944. }
  945. # Prompt3: 目的性匹配
  946. if purpose_eval:
  947. post.purpose_score = purpose_eval.purpose_score
  948. post.purpose_evaluation = {
  949. "purpose_score": purpose_eval.purpose_score,
  950. "core_motivation": purpose_eval.core_motivation,
  951. "image_value": purpose_eval.image_value,
  952. "title_intention": purpose_eval.title_intention,
  953. "text_content": purpose_eval.text_content,
  954. "match_level": purpose_eval.match_level,
  955. "core_basis": purpose_eval.core_basis
  956. }
  957. # Prompt4: 品类匹配
  958. if category_eval:
  959. post.category_score = category_eval.category_score
  960. post.category_evaluation = {
  961. "category_score": category_eval.category_score,
  962. "original_category": category_eval.original_category,
  963. "actual_category": category_eval.actual_category,
  964. "match_level": category_eval.match_level,
  965. "subject_match": category_eval.subject_match,
  966. "qualifier_match": category_eval.qualifier_match,
  967. "core_basis": category_eval.core_basis
  968. }
  969. # 综合得分
  970. if final_score is not None and match_level is not None:
  971. post.final_score = final_score
  972. post.match_level = match_level
  973. # 设置评估时间和版本
  974. post.evaluation_time = datetime.now().isoformat()
  975. post.evaluator_version = "v3.0"
  976. async def batch_evaluate_posts_v3(
  977. posts: list,
  978. original_query: str,
  979. max_concurrent: int = MAX_CONCURRENT_EVALUATIONS
  980. ) -> int:
  981. """
  982. 批量评估多个帖子(V3版本)
  983. Args:
  984. posts: Post对象列表
  985. original_query: 原始搜索query
  986. max_concurrent: 最大并发数
  987. Returns:
  988. 成功评估的帖子数量
  989. """
  990. semaphore = asyncio.Semaphore(max_concurrent)
  991. print(f"\n📊 开始批量评估 {len(posts)} 个帖子(并发限制: {max_concurrent})...")
  992. tasks = [evaluate_post_v3(post, original_query, semaphore) for post in posts]
  993. results = await asyncio.gather(*tasks)
  994. success_count = 0
  995. for i, result in enumerate(results):
  996. knowledge_eval, content_eval, purpose_eval, category_eval, final_score, match_level = result
  997. # 只要有Prompt1结果就算部分成功
  998. if knowledge_eval:
  999. apply_evaluation_v3_to_post(
  1000. posts[i],
  1001. knowledge_eval,
  1002. content_eval,
  1003. purpose_eval,
  1004. category_eval,
  1005. final_score,
  1006. match_level
  1007. )
  1008. success_count += 1
  1009. print(f"✅ 批量评估完成: {success_count}/{len(posts)} 帖子已评估")
  1010. return success_count