match_analyzer.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  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. **只匹配实质语义**,形式语义作为补充信息不参与匹配。
  134. 遍历 <B> 的每个**实质语义**片段,找到与 <A> 中最接近的**实质语义**,判断关系:
  135. **重要**:
  136. - 在匹配关系中必须复述B语义说明和A语义说明,直接从前面的语义分析中复制过来
  137. - 形式语义(修饰词、限定词等)不单独匹配,因为它们脱离实质语义就失去意义
  138. - 形式语义的差异可以在score说明中提及,作为参考信息
  139. 1. **判断是否可比较**(基于抽象层级和本体范畴)
  140. - **参考B语义说明和A语义说明**,判断两个语义是否处于可比较的层级
  141. - **不可比较的情况**(直接标记为"无关"):
  142. - **元概念 vs 具体概念**:元概念(如"概念"、"内容"、"事物"、"要素"、"方面"、"维度"、"属性"等用于描述其他概念的抽象术语)不能与具体概念(如"期待"、"行为"、"猫咪"等实际指代特定事物的词)比较
  143. - **抽象层级相差过大**:相差2级以上(如"存在" vs "猫的行为")
  144. - **本体范畴完全不同**:不同的基本范畴(如数学概念 vs 生物实体、物理对象 vs 心理状态等,除非有明确的跨域映射关系)
  145. - **可比较的情况**:
  146. - 同级具体概念(如"期待" vs "焦虑")
  147. - 有明确上下位关系(如"情感" vs "期待")
  148. - 有交叉或关联关系(如"工作压力" vs "假期期待")
  149. - **输出要求**:
  150. - 如果不可比较:输出 "是否可比较": false, "不可比较原因": "说明为什么不可比较", "关系": "无关"
  151. - 如果可比较:输出 "是否可比较": true, 继续判断关系类型
  152. 2. **确定关系类型**(仅对可比较的语义)
  153. - **同义**:在各自上下文中的含义完全相同,可以互相替换
  154. - **近义**:在各自上下文中的含义非常接近,但有细微差异
  155. - **上位词**:B在其上下文中的含义 比 A在其上下文中的含义 更抽象、更上位(B ⊇ A)
  156. - **下位词**:B在其上下文中的含义 比 A在其上下文中的含义 更具体、更下位(B ⊆ A)
  157. - **无关**:虽然可比较,但没有明确的语义关系
  158. 3. **评估语义距离**
  159. - 对每对匹配的语义,评估它们之间的语义距离
  160. - **距离分数**:0-1之间的浮点数,保留2位小数
  161. - **1.0**:语义完全相同
  162. - **0.8-0.95**:语义非常接近
  163. - **0.5-0.7**:语义有明确关联或层级关系
  164. - **0.1-0.4**:语义有弱关联
  165. - **0.0**:语义完全无关
  166. - 距离分数只考虑B语义和A语义的语义相似度,不考虑其他因素
  167. 4. **计算规则分数**
  168. - 关系权重:同义=1.0, 近义=0.7, 上位词=0.8, 下位词=0.6, 无关=0.0
  169. - 计算公式:
  170. ```
  171. 关系得分 = 关系权重 × 距离分数
  172. 规则分数 = Σ(关系得分) / 实质语义总数
  173. ```
  174. - 例如:关系="近义", 距离分数=0.9 → 关系得分 = 0.7 × 0.9 = 0.63
  175. 5. **评估整体相关性**
  176. - 在完成上述匹配关系分析后,从宏观角度评估 <B> 和 <A> 的整体相关性
  177. - 考虑因素:
  178. - 语义距离:核心概念之间的距离
  179. - 上下文契合度:在各自上下文中的关联程度
  180. - 表达意图的一致性
  181. - **评分标准**(严格遵守):
  182. - **1.0**:<B> 和 <A> 完全同义,可以互相替换,表达完全相同的含义
  183. - **0.8-0.9**:高度相关,核心语义非常接近,只有细微差异
  184. - **0.6-0.7**:明显相关,有明确的语义重叠或上下位关系
  185. - **0.4-0.5**:一般相关,有一定的语义关联或主题相关性
  186. - **0.2-0.3**:弱相关,只有间接关联(如因果、触发等)
  187. - **0.0-0.1**:几乎无关或完全无关
  188. - 给出相关性分数(0-1之间的浮点数,保留2位小数)
  189. - 给出相关性说明
  190. 6. **计算最终分数**
  191. - 最终分数 = 0.8 × 规则分数 + 0.2 × 相关性分数
  192. - 在score说明中说明计算过程,可以提及形式语义的差异作为补充参考
  193. ---
  194. ## 评分标准(0-1分)
  195. **语义匹配度评分:**
  196. - **0.9-1.0**:<B> 的核心语义与名称/定义几乎完全对应
  197. - **0.7-0.8**:大部分核心语义匹配,少量增量
  198. - **0.5-0.6**:部分核心语义匹配,有一定增量
  199. - **0.3-0.4**:少量语义匹配,大部分概念不同
  200. - **0.1-0.2**:几乎无语义匹配,仅有弱关联
  201. - **0.0**:完全无关
  202. **重要原则:**
  203. - 关注有价值的语义片段,而非孤立的字面词语
  204. - 考虑概念之间的语义关系,不仅是字面匹配
  205. ---
  206. ## 输出格式(严格JSON)
  207. ```json
  208. {
  209. "B语义分析": {
  210. "实质": {
  211. "语义片段1": "说明"
  212. },
  213. "形式": {
  214. "语义片段2": "说明",
  215. "语义片段3": "说明"
  216. }
  217. },
  218. "A语义分析": {
  219. "实质": {
  220. "语义片段1": "说明",
  221. "语义片段2": "说明"
  222. },
  223. "形式": {
  224. "语义片段3": "说明"
  225. }
  226. },
  227. "匹配关系": [
  228. {
  229. "B语义": "语义片段1",
  230. "B语义说明": "从B实质语义分析中复述该片段的说明",
  231. "A语义": "语义片段1",
  232. "A语义说明": "从A实质语义分析中复述该片段的说明",
  233. "是否可比较": true,
  234. "关系": "近义",
  235. "说明": "说明两者的关系",
  236. "距离分数": 0.85
  237. },
  238. {
  239. "B语义": "语义片段2",
  240. "B语义说明": "从B实质语义分析中复述该片段的说明",
  241. "A语义": "概念",
  242. "A语义说明": "从A实质语义分析中复述该片段的说明",
  243. "是否可比较": false,
  244. "不可比较原因": "A是元概念(用于描述其他概念的术语),B是具体概念,抽象层级完全不同",
  245. "关系": "无关",
  246. "说明": "元概念不能与具体概念比较",
  247. "距离分数": 0.0
  248. }
  249. ],
  250. "规则分数": 0.3,
  251. "规则分数说明": "B有2个实质语义。第1个:关系=近义(权重0.7),距离分数=0.85,关系得分=0.7×0.85=0.595。第2个:关系=无关(权重0.0),距离分数=0.0,关系得分=0.0。规则分数 = (0.595 + 0.0) / 2 = 0.3。",
  252. "相关性分数": 0.3,
  253. "相关性说明": "虽然有部分语义重叠,但整体上下文契合度较低,因为B强调具体事物,A强调抽象概念",
  254. "score": 0.3,
  255. "score说明": "最终分数 = 0.8 × 0.3 + 0.2 × 0.3 = 0.3。形式语义差异:B有'修饰词x',A有'修饰词y',作为补充参考。"
  256. }
  257. ```
  258. **输出要求**:
  259. 1. 必须严格按照上述JSON格式输出
  260. 2. **B语义分析**:必须包含"实质"、"形式"两个字段,其中实质通常为1-2个核心概念
  261. 3. **A语义分析**:必须包含"实质"、"形式"两个字段
  262. 4. **实质和形式**:都是字典格式,key是语义片段,value是对该片段的说明
  263. 5. **匹配关系**:数组格式,**只包含实质语义的匹配**,字段顺序为:
  264. - B语义 → B语义说明 → A语义 → A语义说明 → 是否可比较
  265. - 如果是否可比较=false:添加"不可比较原因"字段
  266. - 关系 → 说明 → 距离分数
  267. 6. **B语义说明和A语义说明**:必须从前面的实质语义分析中复述,保持一致
  268. 7. **是否可比较**:布尔值,true或false
  269. 8. **不可比较原因**:字符串,仅在是否可比较=false时提供,说明为什么不可比较
  270. 9. **关系**:必须是以下之一:"同义"、"近义"、"上位词"、"下位词"、"无关"
  271. 10. **距离分数**:0-1之间的浮点数,保留2位小数,表示B语义和A语义的语义距离/相似度
  272. 11. **规则分数**:0-1之间的浮点数,保留2位小数,基于匹配关系计算(关系得分 = 关系权重 × 距离分数)
  273. 12. **规则分数说明**:说明规则分数的计算依据,包含每个匹配关系的计算过程
  274. 13. **相关性分数**:0-1之间的浮点数,保留2位小数,整体相关性评估
  275. 14. **相关性说明**:说明相关性分数的判断依据
  276. 15. **score**:0-1之间的浮点数,保留2位小数,= 0.8 × 规则分数 + 0.2 × 相关性分数
  277. 16. **score说明**:说明最终分数的计算过程,可以提及形式语义的差异作为补充
  278. """.strip()
  279. def create_match_agent(model_name: str) -> Agent:
  280. """创建信息匹配分析的 Agent
  281. Args:
  282. model_name: 模型名称
  283. Returns:
  284. Agent 实例
  285. """
  286. agent = Agent(
  287. name="Information Match Expert",
  288. instructions=MATCH_SYSTEM_PROMPT,
  289. model=get_model(model_name),
  290. tools=[],
  291. )
  292. return agent
  293. def calculate_score_by_code(match_result: dict) -> float:
  294. """根据匹配关系和相关性分数用代码计算最终score
  295. 计算公式:
  296. - 关系得分 = 关系权重 × 距离分数
  297. - 规则分数 = Σ(关系得分) / 实质语义总数
  298. - 最终score = 0.8 × 规则分数 + 0.2 × 相关性分数
  299. Args:
  300. match_result: 匹配结果字典,包含 B语义分析、匹配关系、相关性分数
  301. Returns:
  302. 计算得出的最终score(0-1之间)
  303. Raises:
  304. ValueError: 当数据不完整时
  305. """
  306. # 关系权重映射
  307. RELATION_WEIGHTS = {
  308. "同义": 1.0,
  309. "近义": 0.7,
  310. "上位词": 0.8,
  311. "下位词": 0.6,
  312. "无关": 0.0
  313. }
  314. b_semantics = match_result.get("B语义分析", {})
  315. match_relations = match_result.get("匹配关系", [])
  316. relevance_score = match_result.get("相关性分数")
  317. # 验证必需字段
  318. if not match_relations:
  319. raise ValueError("匹配关系为空,无法计算score_by_code")
  320. if not b_semantics:
  321. raise ValueError("B语义分析为空,无法计算score_by_code")
  322. if relevance_score is None:
  323. raise ValueError("相关性分数为空,无法计算score_by_code")
  324. # 提取B的实质语义片段
  325. b_essence = set(b_semantics.get("实质", {}).keys())
  326. if not b_essence:
  327. raise ValueError("B语义分析中实质为空,无法计算score_by_code")
  328. # 计算规则分数:基于实质语义的匹配关系
  329. relation_scores = [] # 存储每个关系的得分
  330. for relation in match_relations:
  331. b_semantic = relation.get("B语义", "")
  332. relation_type = relation.get("关系", "无关")
  333. distance_score = relation.get("距离分数")
  334. if distance_score is None:
  335. raise ValueError(f"匹配关系中缺少距离分数: {b_semantic}")
  336. # 验证该语义确实属于实质
  337. if b_semantic not in b_essence:
  338. raise ValueError(f"匹配关系中包含非实质语义: {b_semantic}")
  339. # 计算关系得分 = 关系权重 × 距离分数
  340. weight = RELATION_WEIGHTS.get(relation_type, 0.0)
  341. relation_score = weight * distance_score
  342. relation_scores.append(relation_score)
  343. if not relation_scores:
  344. raise ValueError("匹配关系中没有实质语义,无法计算score_by_code")
  345. # 计算规则分数(基于匹配关系)
  346. rule_score = sum(relation_scores) / len(relation_scores)
  347. # 计算最终分数:0.8 × 规则分数 + 0.2 × 相关性分数
  348. final_score = 0.8 * rule_score + 0.2 * relevance_score
  349. return round(final_score, 2)
  350. def parse_match_response(response_content: str) -> dict:
  351. """解析匹配响应
  352. Args:
  353. response_content: Agent 返回的响应内容
  354. Returns:
  355. 解析后的字典
  356. """
  357. try:
  358. # 如果响应包含在 markdown 代码块中,提取 JSON 部分
  359. if "```json" in response_content:
  360. json_start = response_content.index("```json") + 7
  361. json_end = response_content.index("```", json_start)
  362. json_text = response_content[json_start:json_end].strip()
  363. elif "```" in response_content:
  364. json_start = response_content.index("```") + 3
  365. json_end = response_content.index("```", json_start)
  366. json_text = response_content[json_start:json_end].strip()
  367. else:
  368. json_text = response_content.strip()
  369. return json.loads(json_text)
  370. except Exception as e:
  371. print(f"解析响应失败: {e}")
  372. return {
  373. "相同部分": {},
  374. "增量部分": {},
  375. "score": 0.0,
  376. "score说明": f"解析失败: {str(e)}"
  377. }
  378. def _create_batch_agent(model_name: str) -> Agent:
  379. """创建批量匹配的 Agent
  380. Args:
  381. model_name: 模型名称
  382. Returns:
  383. Agent 实例
  384. """
  385. # 批量匹配的 System Prompt(在单个匹配基础上修改输出格式)
  386. batch_prompt = MATCH_SYSTEM_PROMPT.replace(
  387. "## 输出格式(严格JSON)",
  388. "## 输出格式(JSON数组)\n对每个 <B> 输出一个匹配结果:"
  389. ).replace(
  390. "```json\n{",
  391. "```json\n[{"
  392. ).replace(
  393. "}\n```",
  394. "}]\n```"
  395. ) + "\n\n**额外要求**:数组长度必须等于 <B> 的数量,顺序对应"
  396. agent = Agent(
  397. name="Batch Information Match Expert",
  398. instructions=batch_prompt,
  399. model=get_model(model_name),
  400. tools=[],
  401. )
  402. return agent
  403. async def _run_match_agent(
  404. agent: Agent,
  405. b_content: str,
  406. a_content: str,
  407. request_desc: str,
  408. b_context: str = "",
  409. a_context: str = ""
  410. ) -> str:
  411. """运行匹配 Agent 的公共逻辑
  412. Args:
  413. agent: Agent 实例
  414. b_content: B 的内容
  415. a_content: A 的内容
  416. request_desc: 请求描述(如"并输出 JSON 格式"或"并输出 JSON 数组格式")
  417. b_context: B 的上下文(可选)
  418. a_context: A 的上下文(可选)
  419. Returns:
  420. Agent 的原始输出
  421. """
  422. # 构建任务描述
  423. b_section = f"<B>\n{b_content}\n</B>"
  424. if b_context:
  425. b_section += f"\n\n<B_Context>\n{b_context}\n</B_Context>"
  426. a_section = f"<A>\n{a_content}\n</A>"
  427. if a_context:
  428. a_section += f"\n\n<A_Context>\n{a_context}\n</A_Context>"
  429. task_description = f"""## 本次分析任务
  430. {b_section}
  431. {a_section}
  432. 请严格按照系统提示中的要求分析 <B> 在 <A> 中的字面语义匹配关系,{request_desc}的结果。"""
  433. # 构造消息
  434. messages = [{
  435. "role": "user",
  436. "content": [
  437. {
  438. "type": "input_text",
  439. "text": task_description
  440. }
  441. ]
  442. }]
  443. # 使用 custom_span 追踪匹配过程
  444. # 截断显示内容,避免 span name 过长
  445. b_short = (b_content[:40] + "...") if len(b_content) > 40 else b_content
  446. a_short = (a_content[:40] + "...") if len(a_content) > 40 else a_content
  447. with custom_span(
  448. name=f"匹配分析: {b_short} in {a_short}",
  449. data={
  450. "B": b_content,
  451. "A": a_content,
  452. "B_Context": b_context if b_context else None,
  453. "A_Context": a_context if a_context else None,
  454. "模式": request_desc
  455. }
  456. ):
  457. # 运行 Agent
  458. result = await Runner.run(agent, input=messages)
  459. return result.final_output
  460. async def match_single(
  461. b_content: str,
  462. a_content: str,
  463. model_name: str,
  464. b_context: str = "",
  465. a_context: str = ""
  466. ) -> dict:
  467. """单个匹配:分析一个 B 在 A 中的匹配
  468. Args:
  469. b_content: B(待匹配)的内容
  470. a_content: A(上下文)的内容
  471. model_name: 使用的模型名称
  472. b_context: B 的补充上下文(可选,默认为空)
  473. a_context: A 的补充上下文(可选,默认为空)
  474. Returns:
  475. 匹配结果字典:{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""}
  476. """
  477. try:
  478. # 创建 Agent
  479. agent = create_match_agent(model_name)
  480. # 运行匹配
  481. output = await _run_match_agent(
  482. agent, b_content, a_content, "并输出 JSON 格式",
  483. b_context=b_context, a_context=a_context
  484. )
  485. # 解析响应
  486. parsed_result = parse_match_response(output)
  487. return parsed_result
  488. except Exception as e:
  489. return {
  490. "相同部分": {},
  491. "增量部分": {},
  492. "score": 0.0,
  493. "score说明": f"匹配过程出错: {str(e)}"
  494. }
  495. async def match_batch(
  496. b_items: List[str],
  497. a_content: str,
  498. model_name: str,
  499. b_context: str = "",
  500. a_context: str = ""
  501. ) -> List[dict]:
  502. """批量匹配:分析多个 B 在 A 中的匹配(一次调用)
  503. Args:
  504. b_items: B列表(多个待匹配项)
  505. a_content: A(上下文)的内容
  506. model_name: 使用的模型名称
  507. b_context: B 的补充上下文(可选,默认为空)
  508. a_context: A 的补充上下文(可选,默认为空)
  509. Returns:
  510. 匹配结果列表:[{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""}, ...]
  511. """
  512. try:
  513. # 创建批量匹配 Agent
  514. agent = _create_batch_agent(model_name)
  515. # 构建 B 列表字符串
  516. b_list_str = "\n".join([f"- {item}" for item in b_items])
  517. # 运行匹配
  518. output = await _run_match_agent(
  519. agent, b_list_str, a_content, "并输出 JSON 数组格式",
  520. b_context=b_context, a_context=a_context
  521. )
  522. # 解析响应(期望是数组)
  523. parsed_result = parse_match_response(output)
  524. # 如果返回的是数组,直接返回;如果是单个对象,包装成数组
  525. if isinstance(parsed_result, list):
  526. return parsed_result
  527. else:
  528. return [parsed_result]
  529. except Exception as e:
  530. # 返回错误信息(为每个 B 创建一个错误条目)
  531. return [{
  532. "相同部分": {},
  533. "增量部分": {},
  534. "score": 0.0,
  535. "score说明": f"匹配过程出错: {str(e)}"
  536. } for _ in b_items]
  537. async def match_with_definition(
  538. b_content: str,
  539. element_name: str,
  540. element_definition: str,
  541. model_name: str,
  542. b_context: str = "",
  543. a_context: str = ""
  544. ) -> dict:
  545. """名称+定义匹配:分析 B 分别与名称、定义的语义匹配关系
  546. Args:
  547. b_content: B(待匹配)的内容
  548. element_name: 要素名称
  549. element_definition: 要素定义
  550. model_name: 使用的模型名称
  551. b_context: B 的补充上下文(可选,默认为空)
  552. a_context: A 的补充上下文(可选,默认为空)
  553. Returns:
  554. 匹配结果字典:{"名称匹配": {...}, "定义匹配": {...}}
  555. """
  556. try:
  557. # 创建使用新 prompt 的 Agent
  558. agent = Agent(
  559. name="Name+Definition Match Expert",
  560. instructions=MATCH_WITH_DEFINITION_PROMPT,
  561. model=get_model(model_name),
  562. tools=[],
  563. )
  564. # A 内容只包含名称,定义放在 A_Context 中
  565. a_content = element_name
  566. # 将定义添加到 A_Context(如果已有 a_context,则追加)
  567. if element_definition:
  568. definition_context = f"定义:{element_definition}"
  569. if a_context:
  570. full_a_context = f"{a_context}\n{definition_context}"
  571. else:
  572. full_a_context = definition_context
  573. else:
  574. full_a_context = a_context
  575. # 运行匹配
  576. output = await _run_match_agent(
  577. agent, b_content, a_content, "并输出 JSON 格式",
  578. b_context=b_context, a_context=full_a_context
  579. )
  580. # 解析响应
  581. parsed_result = parse_match_response(output)
  582. # 验证返回格式
  583. required_fields = [
  584. "B语义分析", "A语义分析", "匹配关系",
  585. "规则分数", "规则分数说明",
  586. "相关性分数", "相关性说明",
  587. "score", "score说明"
  588. ]
  589. missing_fields = [f for f in required_fields if f not in parsed_result]
  590. if missing_fields:
  591. return {
  592. "B语义分析": {
  593. "实质": {},
  594. "形式": {}
  595. },
  596. "A语义分析": {
  597. "实质": {},
  598. "形式": {}
  599. },
  600. "匹配关系": [],
  601. "规则分数": 0.0,
  602. "规则分数说明": f"解析失败:缺少字段 {missing_fields}",
  603. "相关性分数": 0.0,
  604. "相关性说明": f"解析失败:缺少字段 {missing_fields}",
  605. "score": 0.0,
  606. "score_by_code": 0.0,
  607. "score说明": f"解析失败:缺少字段 {missing_fields}"
  608. }
  609. # 计算程序score(如果数据不完整会抛出异常)
  610. score_by_code = calculate_score_by_code(parsed_result)
  611. parsed_result["score_by_code"] = score_by_code
  612. return parsed_result
  613. except Exception as e:
  614. return {
  615. "B语义分析": {
  616. "实质": {},
  617. "形式": {}
  618. },
  619. "A语义分析": {
  620. "实质": {},
  621. "形式": {}
  622. },
  623. "匹配关系": [],
  624. "规则分数": 0.0,
  625. "规则分数说明": f"匹配过程出错: {str(e)}",
  626. "相关性分数": 0.0,
  627. "相关性说明": f"匹配过程出错: {str(e)}",
  628. "score": 0.0,
  629. "score_by_code": 0.0,
  630. "score说明": f"匹配过程出错: {str(e)}"
  631. }