post_evaluator_v3.py 48 KB

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