match_analyzer.py 25 KB

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