match_analyzer.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. """
  2. 通用的信息匹配分析模块
  3. 分析 <B> 在 <A> 中的字面语义匹配关系
  4. 适用于任何信息匹配场景
  5. 提供两个接口:
  6. 1. match_single(b_content, a_content, model_name, b_context="", a_context="") - 单个匹配
  7. 2. match_batch(b_items, a_content, model_name, b_context="", a_context="") - 批量匹配
  8. 支持可选的 Context 参数:
  9. - b_context: B 的补充上下文(帮助理解 B)
  10. - a_context: A 的补充上下文(帮助理解 A)
  11. - Context 默认为空,不提供时不会出现在 prompt 中
  12. """
  13. import json
  14. from typing import List
  15. from agents import Agent, Runner
  16. from agents.tracing.create import custom_span
  17. from lib.client import get_model
  18. # ========== System Prompt ==========
  19. MATCH_SYSTEM_PROMPT = """
  20. # 任务
  21. 分析 <B> 在 <A> 中的字面语义匹配关系。
  22. ## 输入说明
  23. - **<B></B>**: 待匹配的内容(必选)
  24. - **<A></A>**: 上下文内容(必选)
  25. - **<B_Context></B_Context>**: B 的补充上下文(可选,帮助理解 B)
  26. - **<A_Context></A_Context>**: A 的补充上下文(可选,帮助理解 A)
  27. **重要**:匹配分析发生在 <B> 和 <A> 之间,Context 仅作为补充理解的辅助信息。
  28. ## 分析方法
  29. ### 核心原则:字面语义匹配
  30. 只关注 <B> 和 <A> 在**字面词语和概念**上的重叠度,不考虑抽象关系。
  31. ### 分析步骤
  32. 1. **提取关键词/概念**
  33. - 从 <B> 中提取:关键词语和核心概念
  34. - 从 <A> 中提取:关键词语和核心概念
  35. 2. **识别相同部分**
  36. - 完全相同的词语(字面一致)
  37. - 同义词或近义词
  38. 3. **识别增量部分**
  39. - <B> 中有,但 <A> 中没有的词语/概念
  40. - 这些是 <B> 相对于 <A> 的额外信息
  41. 4. **计算匹配分数**
  42. - 基于相同部分的覆盖度
  43. - 考虑词语/概念的重要性
  44. ---
  45. ## 评分标准(0-1分)
  46. **字面匹配度评分:**
  47. - **0.9-1.0**:<B> 和 <A> 几乎完全一致,词语高度重叠
  48. - **0.7-0.8**:大部分核心词语/概念匹配,少量增量
  49. - **0.5-0.6**:部分核心词语/概念匹配,有一定增量
  50. - **0.3-0.4**:少量词语/概念匹配,大部分不同
  51. - **0.1-0.2**:几乎无字面匹配,仅有概念联系
  52. - **0.0**:完全无关
  53. **重要原则:**
  54. - 如果 <A> 是抽象/元级别的描述,而 <B> 是具体内容,字面上无词语重叠,应给低分(0.1-0.3)
  55. - 优先考虑具体词语的匹配,而非抽象概念的包含关系
  56. ---
  57. ## 输出格式(严格JSON)
  58. ```json
  59. {
  60. "score": 0.75,
  61. "score说明": "简要说明分数是如何计算的,基于哪些词语/概念的匹配",
  62. "相同部分": {
  63. "B中的词1": "与A中的'某词'完全相同",
  64. "B中的词2": "与A中的'某词'同义"
  65. },
  66. "增量部分": {
  67. "B中的词3": "A中无此概念"
  68. }
  69. }
  70. ```
  71. **输出要求**:
  72. 1. 必须严格按照上述JSON格式输出(score 和 score说明在最前面)
  73. 2. 所有字段都必须填写
  74. 3. **score字段**:必须是0-1之间的浮点数,保留2位小数
  75. 4. **score说明**:必须简洁说明评分依据(基于相同部分的覆盖度)
  76. 5. **相同部分**:字典格式,key是<B>中的词语,value说明它与<A>中哪个词的关系(完全相同/同义);如果没有则填写空字典 {}
  77. 6. **增量部分**:字典格式,key是<B>中的词语,value说明为什么是增量(如"A中无此概念");如果没有增量部分,填写空字典 {}
  78. 7. **关键约束**:相同部分和增量部分的key必须只能是<B>中的词语,不能是<A>中的词语
  79. """.strip()
  80. # ========== System Prompt for Name+Definition Match ==========
  81. MATCH_WITH_DEFINITION_PROMPT = """
  82. # 任务
  83. 分析 <B> 与 <A> 的语义匹配关系。<A> 包含"名称"和"定义",定义作为辅助上下文帮助理解名称的含义。
  84. ## 输入说明
  85. - **<B></B>**: 待匹配的内容(灵感点)
  86. - **<A></A>**: 名称(必选)
  87. - **<B_Context></B_Context>**: B 的补充上下文(可选)
  88. - **<A_Context></A_Context>**: A 的补充上下文,包含定义等信息(可选)
  89. **重要**:A_Context 中的定义仅用于辅助理解名称 <A> 的含义,主要分析 <B> 与 <A> 的匹配关系。
  90. ## 分析方法
  91. ### 核心原则:语义概念匹配
  92. 先独立分析 <B> 和 <A> 的语义,再进行匹配,避免强行解释。
  93. ### 分析步骤
  94. **第一步:独立分析 <B> 的语义**
  95. 对 <B>(灵感点)进行语义分解,提取:
  96. **拆分步骤**:
  97. 1. 先识别出所有语义成分(主体、动作/状态、属性、修饰等)
  98. 2. 对每个成分做"去除测试":
  99. - 如果去掉这个成分后,剩余部分的核心语义**没有发生本质变化** → 该成分是**形式**
  100. - 如果去掉这个成分后,剩余部分的核心语义**发生了本质变化** → 该成分是**实质**
  101. 3. 将成分分类到实质或形式中,每个成分只能出现在一个类别
  102. - **实质语义**:核心概念、本质内容(字典格式)
  103. - 通常为1-2个核心概念
  104. - 不要过度拆分复合词,保持核心概念的完整性
  105. - key:语义片段(可以是词组)
  106. - value:**客观地说明这个片段在B的上下文中的具体含义**
  107. - **形式语义**:具体限定、修饰、程度等(字典格式)
  108. - 包括:主体限定词、属性修饰词、程度副词、时间/空间限定等
  109. - key:语义片段
  110. - value:**说明在B的上下文中修饰/限定什么,如何修饰/限定**
  111. **关键原则**:
  112. - 每个语义片段只能出现在一个类别中,不要重复
  113. - 表示主体、场景、对象的限定词通常属于形式,不要合并到实质中
  114. - 不要孤立地解释词语本身,要说明它**在当前B的上下文中**的具体含义
  115. - 不要过度推理,保持客观:描述B字面上说的是什么,而不是可能引发什么
  116. **注意**:纯粹分析 <B> 本身的语义,不要考虑与 <A> 的关系。
  117. **第二步:独立分析 <A> 的语义**
  118. 对 <A>(名称)进行语义分解,提取:
  119. - **实质语义**:核心概念、本质内容(字典格式)
  120. - **判断标准:单独拿出来看,语义跟原来相比没有发生本质变化**
  121. - key:语义片段(可以是词组,保持完整的核心概念)
  122. - value:**客观地说明这个片段在A的上下文中的具体含义和作用**
  123. - **形式语义**:具体限定、修饰等(字典格式)
  124. - **判断标准:去掉后,核心概念本身不会丢失,只是失去了限定、修饰等细节**
  125. - key:语义片段
  126. - value:**说明在A的上下文中修饰/限定什么,如何修饰/限定**
  127. **关键**:
  128. - 可以参考<A_Context>中的定义来辅助理解名称的含义,但只分析名称本身
  129. - 同样要说明语义片段**在当前A的上下文中**的具体含义,不要孤立解释
  130. - 不要过度拆分复合词,保持核心概念的完整性
  131. **注意**:纯粹分析 <A> 本身的语义,不要考虑与 <B> 的关系。
  132. **第三步:建立匹配关系**
  133. 遍历 <B> 的每个语义片段(实质+形式),找到与 <A> 中最接近的语义,判断关系:
  134. 1. **判断是否同一概念**(基于上下文中的作用/本质)
  135. - **不是看字面是否相同**,而是看**在各自上下文中的作用/本质是否相同**
  136. - 同一概念:可以比较上下位/同义关系
  137. - 不同概念:标记为无关
  138. 2. **确定关系类型**(仅对同一概念)
  139. - **同义/近义**:在各自上下文中的含义基本相同
  140. - **上位词**:B在其上下文中的含义 比 A在其上下文中的含义 更抽象、更上位(B ⊇ A)
  141. - **下位词**:B在其上下文中的含义 比 A在其上下文中的含义 更具体、更下位(B ⊆ A)
  142. - **无关**:不同概念,无法比较
  143. 3. **计算匹配分数**
  144. - 关系得分:同义=1.0, 上位词=0.6, 下位词=0.4, 无关=0.0
  145. - 实质语义权重=0.8,形式语义权重=0.2
  146. - 计算公式:
  147. ```
  148. 实质得分 = Σ(实质语义的关系得分) / 实质语义总数
  149. 形式得分 = Σ(形式语义的关系得分) / 形式语义总数
  150. 最终 score = 实质得分 × 0.8 + 形式得分 × 0.2
  151. ```
  152. ---
  153. ## 评分标准(0-1分)
  154. **语义匹配度评分:**
  155. - **0.9-1.0**:<B> 的核心语义与名称/定义几乎完全对应
  156. - **0.7-0.8**:大部分核心语义匹配,少量增量
  157. - **0.5-0.6**:部分核心语义匹配,有一定增量
  158. - **0.3-0.4**:少量语义匹配,大部分概念不同
  159. - **0.1-0.2**:几乎无语义匹配,仅有弱关联
  160. - **0.0**:完全无关
  161. **重要原则:**
  162. - 关注有价值的语义片段,而非孤立的字面词语
  163. - 考虑概念之间的语义关系,不仅是字面匹配
  164. ---
  165. ## 输出格式(严格JSON)
  166. ```json
  167. {
  168. "B语义分析": {
  169. "实质": {
  170. "语义片段1": "说明"
  171. },
  172. "形式": {
  173. "语义片段2": "说明",
  174. "语义片段3": "说明"
  175. }
  176. },
  177. "A语义分析": {
  178. "实质": {
  179. "语义片段1": "说明",
  180. "语义片段2": "说明"
  181. },
  182. "形式": {
  183. "语义片段3": "说明"
  184. }
  185. },
  186. "匹配关系": [
  187. {
  188. "B语义": "语义片段1",
  189. "A语义": "语义片段1",
  190. "是否同一概念": true,
  191. "关系": "下位词",
  192. "说明": "说明两者的关系"
  193. },
  194. {
  195. "B语义": "语义片段2",
  196. "A语义": "语义片段2",
  197. "是否同一概念": false,
  198. "关系": "无关",
  199. "说明": "说明为什么无关"
  200. }
  201. ],
  202. "score": 0.65,
  203. "score说明": "基于评分规则计算得出"
  204. }
  205. ```
  206. **输出要求**:
  207. 1. 必须严格按照上述JSON格式输出
  208. 2. **B语义分析**:必须包含"实质"、"形式"两个字段,其中实质通常为1-2个核心概念
  209. 3. **A语义分析**:必须包含"实质"、"形式"两个字段
  210. 4. **实质和形式**:都是字典格式,key是语义片段,value是对该片段的说明
  211. 5. **匹配关系**:数组格式,包含所有B语义片段的匹配情况,字段顺序为:B语义 → A语义 → 是否同一概念 → 关系 → 说明
  212. 6. **是否同一概念**:布尔值,true或false
  213. 7. **关系**:必须是以下之一:"同义/近义"、"上位词"、"下位词"、"无关"
  214. 8. **score**:0-1之间的浮点数,保留2位小数,按照评分规则计算
  215. 9. **score说明**:说明分数的计算依据
  216. """.strip()
  217. def create_match_agent(model_name: str) -> Agent:
  218. """创建信息匹配分析的 Agent
  219. Args:
  220. model_name: 模型名称
  221. Returns:
  222. Agent 实例
  223. """
  224. agent = Agent(
  225. name="Information Match Expert",
  226. instructions=MATCH_SYSTEM_PROMPT,
  227. model=get_model(model_name),
  228. tools=[],
  229. )
  230. return agent
  231. def parse_match_response(response_content: str) -> dict:
  232. """解析匹配响应
  233. Args:
  234. response_content: Agent 返回的响应内容
  235. Returns:
  236. 解析后的字典
  237. """
  238. try:
  239. # 如果响应包含在 markdown 代码块中,提取 JSON 部分
  240. if "```json" in response_content:
  241. json_start = response_content.index("```json") + 7
  242. json_end = response_content.index("```", json_start)
  243. json_text = response_content[json_start:json_end].strip()
  244. elif "```" in response_content:
  245. json_start = response_content.index("```") + 3
  246. json_end = response_content.index("```", json_start)
  247. json_text = response_content[json_start:json_end].strip()
  248. else:
  249. json_text = response_content.strip()
  250. return json.loads(json_text)
  251. except Exception as e:
  252. print(f"解析响应失败: {e}")
  253. return {
  254. "相同部分": {},
  255. "增量部分": {},
  256. "score": 0.0,
  257. "score说明": f"解析失败: {str(e)}"
  258. }
  259. def _create_batch_agent(model_name: str) -> Agent:
  260. """创建批量匹配的 Agent
  261. Args:
  262. model_name: 模型名称
  263. Returns:
  264. Agent 实例
  265. """
  266. # 批量匹配的 System Prompt(在单个匹配基础上修改输出格式)
  267. batch_prompt = MATCH_SYSTEM_PROMPT.replace(
  268. "## 输出格式(严格JSON)",
  269. "## 输出格式(JSON数组)\n对每个 <B> 输出一个匹配结果:"
  270. ).replace(
  271. "```json\n{",
  272. "```json\n[{"
  273. ).replace(
  274. "}\n```",
  275. "}]\n```"
  276. ) + "\n\n**额外要求**:数组长度必须等于 <B> 的数量,顺序对应"
  277. agent = Agent(
  278. name="Batch Information Match Expert",
  279. instructions=batch_prompt,
  280. model=get_model(model_name),
  281. tools=[],
  282. )
  283. return agent
  284. async def _run_match_agent(
  285. agent: Agent,
  286. b_content: str,
  287. a_content: str,
  288. request_desc: str,
  289. b_context: str = "",
  290. a_context: str = ""
  291. ) -> str:
  292. """运行匹配 Agent 的公共逻辑
  293. Args:
  294. agent: Agent 实例
  295. b_content: B 的内容
  296. a_content: A 的内容
  297. request_desc: 请求描述(如"并输出 JSON 格式"或"并输出 JSON 数组格式")
  298. b_context: B 的上下文(可选)
  299. a_context: A 的上下文(可选)
  300. Returns:
  301. Agent 的原始输出
  302. """
  303. # 构建任务描述
  304. b_section = f"<B>\n{b_content}\n</B>"
  305. if b_context:
  306. b_section += f"\n\n<B_Context>\n{b_context}\n</B_Context>"
  307. a_section = f"<A>\n{a_content}\n</A>"
  308. if a_context:
  309. a_section += f"\n\n<A_Context>\n{a_context}\n</A_Context>"
  310. task_description = f"""## 本次分析任务
  311. {b_section}
  312. {a_section}
  313. 请严格按照系统提示中的要求分析 <B> 在 <A> 中的字面语义匹配关系,{request_desc}的结果。"""
  314. # 构造消息
  315. messages = [{
  316. "role": "user",
  317. "content": [
  318. {
  319. "type": "input_text",
  320. "text": task_description
  321. }
  322. ]
  323. }]
  324. # 使用 custom_span 追踪匹配过程
  325. # 截断显示内容,避免 span name 过长
  326. b_short = (b_content[:40] + "...") if len(b_content) > 40 else b_content
  327. a_short = (a_content[:40] + "...") if len(a_content) > 40 else a_content
  328. with custom_span(
  329. name=f"匹配分析: {b_short} in {a_short}",
  330. data={
  331. "B": b_content,
  332. "A": a_content,
  333. "B_Context": b_context if b_context else None,
  334. "A_Context": a_context if a_context else None,
  335. "模式": request_desc
  336. }
  337. ):
  338. # 运行 Agent
  339. result = await Runner.run(agent, input=messages)
  340. return result.final_output
  341. async def match_single(
  342. b_content: str,
  343. a_content: str,
  344. model_name: str,
  345. b_context: str = "",
  346. a_context: str = ""
  347. ) -> dict:
  348. """单个匹配:分析一个 B 在 A 中的匹配
  349. Args:
  350. b_content: B(待匹配)的内容
  351. a_content: A(上下文)的内容
  352. model_name: 使用的模型名称
  353. b_context: B 的补充上下文(可选,默认为空)
  354. a_context: A 的补充上下文(可选,默认为空)
  355. Returns:
  356. 匹配结果字典:{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""}
  357. """
  358. try:
  359. # 创建 Agent
  360. agent = create_match_agent(model_name)
  361. # 运行匹配
  362. output = await _run_match_agent(
  363. agent, b_content, a_content, "并输出 JSON 格式",
  364. b_context=b_context, a_context=a_context
  365. )
  366. # 解析响应
  367. parsed_result = parse_match_response(output)
  368. return parsed_result
  369. except Exception as e:
  370. return {
  371. "相同部分": {},
  372. "增量部分": {},
  373. "score": 0.0,
  374. "score说明": f"匹配过程出错: {str(e)}"
  375. }
  376. async def match_batch(
  377. b_items: List[str],
  378. a_content: str,
  379. model_name: str,
  380. b_context: str = "",
  381. a_context: str = ""
  382. ) -> List[dict]:
  383. """批量匹配:分析多个 B 在 A 中的匹配(一次调用)
  384. Args:
  385. b_items: B列表(多个待匹配项)
  386. a_content: A(上下文)的内容
  387. model_name: 使用的模型名称
  388. b_context: B 的补充上下文(可选,默认为空)
  389. a_context: A 的补充上下文(可选,默认为空)
  390. Returns:
  391. 匹配结果列表:[{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""}, ...]
  392. """
  393. try:
  394. # 创建批量匹配 Agent
  395. agent = _create_batch_agent(model_name)
  396. # 构建 B 列表字符串
  397. b_list_str = "\n".join([f"- {item}" for item in b_items])
  398. # 运行匹配
  399. output = await _run_match_agent(
  400. agent, b_list_str, a_content, "并输出 JSON 数组格式",
  401. b_context=b_context, a_context=a_context
  402. )
  403. # 解析响应(期望是数组)
  404. parsed_result = parse_match_response(output)
  405. # 如果返回的是数组,直接返回;如果是单个对象,包装成数组
  406. if isinstance(parsed_result, list):
  407. return parsed_result
  408. else:
  409. return [parsed_result]
  410. except Exception as e:
  411. # 返回错误信息(为每个 B 创建一个错误条目)
  412. return [{
  413. "相同部分": {},
  414. "增量部分": {},
  415. "score": 0.0,
  416. "score说明": f"匹配过程出错: {str(e)}"
  417. } for _ in b_items]
  418. async def match_with_definition(
  419. b_content: str,
  420. element_name: str,
  421. element_definition: str,
  422. model_name: str,
  423. b_context: str = "",
  424. a_context: str = ""
  425. ) -> dict:
  426. """名称+定义匹配:分析 B 分别与名称、定义的语义匹配关系
  427. Args:
  428. b_content: B(待匹配)的内容
  429. element_name: 要素名称
  430. element_definition: 要素定义
  431. model_name: 使用的模型名称
  432. b_context: B 的补充上下文(可选,默认为空)
  433. a_context: A 的补充上下文(可选,默认为空)
  434. Returns:
  435. 匹配结果字典:{"名称匹配": {...}, "定义匹配": {...}}
  436. """
  437. try:
  438. # 创建使用新 prompt 的 Agent
  439. agent = Agent(
  440. name="Name+Definition Match Expert",
  441. instructions=MATCH_WITH_DEFINITION_PROMPT,
  442. model=get_model(model_name),
  443. tools=[],
  444. )
  445. # A 内容只包含名称,定义放在 A_Context 中
  446. a_content = element_name
  447. # 将定义添加到 A_Context(如果已有 a_context,则追加)
  448. if element_definition:
  449. definition_context = f"定义:{element_definition}"
  450. if a_context:
  451. full_a_context = f"{a_context}\n{definition_context}"
  452. else:
  453. full_a_context = definition_context
  454. else:
  455. full_a_context = a_context
  456. # 运行匹配
  457. output = await _run_match_agent(
  458. agent, b_content, a_content, "并输出 JSON 格式",
  459. b_context=b_context, a_context=full_a_context
  460. )
  461. # 解析响应
  462. parsed_result = parse_match_response(output)
  463. # 验证返回格式
  464. required_fields = ["B语义分析", "A语义分析", "匹配关系", "score", "score说明"]
  465. missing_fields = [f for f in required_fields if f not in parsed_result]
  466. if missing_fields:
  467. return {
  468. "B语义分析": {
  469. "实质": {},
  470. "形式": {}
  471. },
  472. "A语义分析": {
  473. "实质": {},
  474. "形式": {}
  475. },
  476. "匹配关系": [],
  477. "score": 0.0,
  478. "score说明": f"解析失败:缺少字段 {missing_fields}"
  479. }
  480. return parsed_result
  481. except Exception as e:
  482. return {
  483. "B语义分析": {
  484. "实质": {},
  485. "形式": {}
  486. },
  487. "A语义分析": {
  488. "实质": {},
  489. "形式": {}
  490. },
  491. "匹配关系": [],
  492. "score": 0.0,
  493. "score说明": f"匹配过程出错: {str(e)}"
  494. }