unified_match_analyzer.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 统一匹配分析模块 (v4 - 优化版)
  5. 使用单个prompt同时完成标签匹配和分类匹配,一步到位。
  6. 输出格式:当前标签列表中每个标签的匹配结果。
  7. """
  8. from typing import List, Dict, Optional
  9. from agents import Agent, Runner, ModelSettings
  10. from agents.tracing.create import custom_span
  11. from lib.client import get_model
  12. from lib.utils import parse_json_from_text
  13. # ========== System Prompt ==========
  14. UNIFIED_MATCH_SYSTEM_PROMPT = """
  15. # 任务
  16. 对"当前标签列表"中的每个标签,与"人设标签组合"进行综合匹配分析。
  17. ## 输入说明
  18. - **当前标签列表**: 需要匹配的标签列表
  19. - **人设标签组合**: 包含标签名称及其分类的组合
  20. - 每个标签有:标签名称、所属分类(多层级,从具体到抽象)
  21. - 分类是树状结构,按数组顺序从具体到抽象排列
  22. ## 匹配策略
  23. 对当前标签列表中的**每个标签**:
  24. **重要约束 - 分类排他性**:
  25. - 如果某个人设标签已经被标签匹配,则该标签的所有所属分类都不能再被其他当前标签使用
  26. **匹配优先级和提前终止**:
  27. 1. 优先进行标签匹配,如果匹配成功则立即停止,不再进行分类匹配
  28. 2. 如果标签匹配失败,则进行分类匹配
  29. 3. 分类匹配按层级从下到上(从具体到抽象),一旦某层匹配成功则立即停止,不再检查更抽象的层级
  30. ### 1. 标签匹配(同义关系)
  31. - **逐个判断**每个人设标签
  32. - **核心判断**: "A 和 B 是同一个东西吗?是同义词吗?"
  33. - **输出**: 是否匹配(true/false)
  34. - **严格要求**: 必须是同义词或几乎相同的表述才能匹配
  35. - **如果匹配成功**: 立即返回结果,不再进行分类匹配
  36. ### 2. 分类匹配(从属关系)
  37. - **仅在标签匹配全部失败时进行**
  38. - **按层级从下到上**遍历分类(从具体到抽象)
  39. - **每层判断所有分类**
  40. - **核心判断**: "当前标签 本身就是 {分类} 的一种吗?"
  41. - **输出**:
  42. - 该层候选分类:列出该层所有分类名称
  43. - 该层匹配结果:对该层每个分类逐个判断,输出分类名称、从属关系判断、是否有从属关系、相似度分析、语义相似度
  44. - **严格要求**: 必须是直接从属关系,不能是间接关系或关联关系
  45. - **禁止**:
  46. - ✗ "A 可能会有 B"(间接推理)
  47. - ✗ "A 与 B 有关"(关联不等于从属)
  48. - **语义相似度计算规则**:
  49. - **重要**:语义相似度和从属关系是两个完全独立的维度!
  50. * 从属关系判断:"A 本身就是 B 的一种吗?"(层级关系)
  51. * 语义相似度:"A 和 B 这两个词本身像吗?"(词义距离)
  52. - **核心原则**:计算语义相似度时,**完全不考虑**从属关系的判断结果
  53. - **判断方法**:想象你不知道这两个词之间有任何关系,只是单独看这两个词的字面含义,它们像吗?
  54. - **禁止思路**:不要因为"A 是 B 的一种"就给高相似度
  55. - 计算标准:
  56. * 两个词几乎是同义词:0.8-1.0
  57. * 两个词意思比较接近:0.5-0.7
  58. * 两个词意思差距较大:0.2-0.4
  59. * 两个词意思完全不同:0.0-0.1
  60. - **相似度分析**:说明两个词本身的字面含义有多相似(30字以内),不要提及从属关系
  61. - **如果某层匹配成功**: 立即返回该层的匹配结果,不再检查更抽象的层级
  62. ## 输出格式 (严格JSON数组)
  63. ```json
  64. [
  65. {
  66. "当前标签": "<标签名称>",
  67. "匹配过程": {
  68. "标签匹配": [
  69. {
  70. "人设标签": "<标签名称>",
  71. "是否匹配": <true|false>
  72. }
  73. ],
  74. "分类匹配_按层级": [
  75. {
  76. "该层候选分类": ["<分类1>", "<分类2>", "..."],
  77. "该层匹配结果": [
  78. {
  79. "分类名称": "<分类1>",
  80. "从属关系判断": "<判断过程和理由>",
  81. "是否有从属关系": <true|false>,
  82. "相似度分析": "<两个词本身的相似度分析>",
  83. "语义相似度": <0到1之间的数值>
  84. },
  85. {
  86. "分类名称": "<分类2>",
  87. "从属关系判断": "<判断过程和理由>",
  88. "是否有从属关系": <true|false>,
  89. "相似度分析": "<两个词本身的相似度分析>",
  90. "语义相似度": <0到1之间的数值>
  91. }
  92. ]
  93. }
  94. ]
  95. },
  96. "匹配结果": {
  97. "匹配类型": "<标签匹配|分类匹配|无匹配>",
  98. "匹配到": "<标签或分类名称,无匹配时为null>",
  99. "语义相似度": <0到1之间的数值>
  100. }
  101. }
  102. ]
  103. ```
  104. ## 要求
  105. 1. **数组长度必须等于当前标签列表的长度**
  106. 2. **标签匹配**: 对人设组合中每个标签都要输出判断结果(true/false)
  107. 3. **提前终止**:
  108. - 如果标签匹配成功,则"分类匹配_按层级"为空数组[],不进行分类匹配
  109. - 如果标签匹配失败,进行分类匹配:
  110. * 从第一层开始逐层判断,每层都输出到"分类匹配_按层级"数组
  111. * 每层的"该层匹配结果"数组长度必须等于"该层候选分类"数组长度,每个分类都要判断
  112. * 一旦某层有匹配成功的分类(是否有从属关系=true),该层之后的层级不再输出
  113. * 例如:第2层匹配成功,则数组长度=2(包含第1层和第2层)
  114. 4. **匹配结果**:
  115. - 标签匹配成功时:匹配类型="标签匹配",语义相似度=1.0
  116. - 分类匹配成功时:匹配类型="分类匹配",语义相似度为该分类的语义相似度
  117. - 都不成功时:匹配类型="无匹配",语义相似度=0
  118. 5. **严格遵守分类排他性约束**
  119. """.strip()
  120. def create_unified_match_agent(model_name: str) -> Agent:
  121. """创建统一匹配的Agent"""
  122. return Agent(
  123. name="Unified Match Expert",
  124. instructions=UNIFIED_MATCH_SYSTEM_PROMPT,
  125. model=get_model(model_name),
  126. model_settings=ModelSettings(
  127. temperature=0.0,
  128. max_tokens=65536,
  129. ),
  130. tools=[],
  131. )
  132. async def unified_match(
  133. current_tags: List[str],
  134. persona_combination: List[Dict],
  135. model_name: Optional[str] = None
  136. ) -> List[Dict]:
  137. """
  138. 统一匹配函数 - 一次调用完成所有层级的匹配
  139. 返回当前标签列表中每个标签的匹配结果
  140. Args:
  141. current_tags: 当前标签列表,如 ["立冬", "教资查分", "时间巧合"]
  142. persona_combination: 人设标签组合(带分类),如:
  143. [
  144. {"标签名称": "猫孩子", "所属分类": ["宠物亲子化", "宠物情感", "实质"]},
  145. {"标签名称": "被拿捏住的无奈感", "所属分类": ["宠物关系主导", "宠物情感", "实质"]}
  146. ]
  147. model_name: 模型名称
  148. Returns:
  149. List[Dict]: 每个当前标签的匹配结果
  150. [
  151. {
  152. "当前标签": "立冬",
  153. "最终得分": 0.7,
  154. "匹配层级": "第一层分类匹配",
  155. "匹配到": "节气习俗",
  156. "匹配详情": {...},
  157. "综合说明": "..."
  158. },
  159. ...
  160. ]
  161. """
  162. if model_name is None:
  163. from lib.client import MODEL_NAME
  164. model_name = MODEL_NAME
  165. # 提取人设标签和分类信息
  166. persona_tags = [f.get("特征名称", f.get("标签名称")) for f in persona_combination]
  167. # 收集所有分类
  168. all_categories = set()
  169. for feature in persona_combination:
  170. categories = feature.get("所属分类", [])
  171. all_categories.update(categories)
  172. # 创建Agent
  173. agent = create_unified_match_agent(model_name)
  174. # 构建任务描述
  175. task_description = f"""## 本次匹配任务
  176. <当前标签列表>
  177. {', '.join(current_tags)}
  178. </当前标签列表>
  179. <人设标签组合>
  180. {persona_combination}
  181. </人设标签组合>
  182. **重要提醒**:
  183. 1. **标签匹配**: 对人设组合中每个"特征名称"逐个判断是否与当前标签同义(true/false)
  184. 2. **提前终止机制**:
  185. - 如果标签匹配成功,立即停止,"分类匹配_按层级"输出空数组[]
  186. - 如果标签匹配失败,进行分类匹配
  187. 3. **分类匹配**: 按层级(从具体到抽象)逐层判断
  188. - 分类在"所属分类"数组中的顺序就是从具体到抽象
  189. - 从第一层开始,判断该层所有分类
  190. - 在"分类匹配_按层级"数组中,按顺序输出每一层的判断结果
  191. - **重要**:每层的"该层匹配结果"必须对"该层候选分类"中的每个分类逐一判断
  192. - 一旦某层有匹配成功的分类(是否有从属关系=true),该层后面不再输出更多层级
  193. - 示例:如果第2层匹配成功,则只输出第1层和第2层,不输出第3层及以后
  194. 4. **语义相似度(核心规则)**:
  195. - ⚠️ **严格要求**:语义相似度和从属关系是**完全独立**的两个维度!
  196. - 从属关系看层级:判断"A 是不是 B 的一种"
  197. - 语义相似度看词义:判断"A 和 B 这两个词本身像不像"
  198. - **禁止**:不要因为"是一种"就给高相似度!
  199. 5. **匹配结果**:
  200. - 标签匹配成功:匹配类型="标签匹配",语义相似度=1.0
  201. - 分类匹配成功:匹配类型="分类匹配",语义相似度为该分类的语义相似度
  202. - 都不成功:匹配类型="无匹配",语义相似度=0
  203. 请对当前标签列表中的**每个标签**(共{len(current_tags)}个)进行匹配评估。
  204. 输出JSON数组,长度必须等于{len(current_tags)},顺序与当前标签列表一一对应。
  205. """
  206. messages = [{
  207. "role": "user",
  208. "content": [{"type": "input_text", "text": task_description}]
  209. }]
  210. with custom_span(
  211. name=f"统一匹配: 当前{len(current_tags)}个标签 vs 人设组合{persona_tags}",
  212. data={
  213. "当前标签列表": current_tags,
  214. "人设标签": persona_tags,
  215. "可用分类": list(all_categories)
  216. }
  217. ):
  218. result = await Runner.run(agent, input=messages)
  219. # 解析响应
  220. parsed_result = parse_json_from_text(result.final_output)
  221. if not parsed_result:
  222. # 解析失败,返回默认结果
  223. print("警告: JSON解析失败,返回默认结果")
  224. return [
  225. {
  226. "当前标签": tag,
  227. "匹配过程": {
  228. "标签匹配": [],
  229. "分类匹配_按层级": []
  230. },
  231. "匹配结果": {
  232. "匹配类型": "无匹配",
  233. "匹配到": None,
  234. "语义相似度": 0
  235. }
  236. }
  237. for tag in current_tags
  238. ]
  239. # 确保返回的是列表
  240. if not isinstance(parsed_result, list):
  241. print(f"警告: 返回结果不是列表,转换中: {type(parsed_result)}")
  242. parsed_result = [parsed_result]
  243. # 验证结果数量
  244. if len(parsed_result) != len(current_tags):
  245. print(f"警告: 返回结果数量({len(parsed_result)})与当前标签数量({len(current_tags)})不匹配")
  246. # 补齐或截断
  247. while len(parsed_result) < len(current_tags):
  248. parsed_result.append({
  249. "当前标签": current_tags[len(parsed_result)],
  250. "最终得分": 0,
  251. "匹配层级": "无匹配",
  252. "匹配到": None,
  253. "匹配详情": {},
  254. "综合说明": "结果数量不匹配,自动补齐"
  255. })
  256. parsed_result = parsed_result[:len(current_tags)]
  257. return parsed_result