""" 通用的信息匹配分析模块 分析 中的字面语义匹配关系 适用于任何信息匹配场景 提供两个接口: 1. match_single(b_content, a_content, model_name, b_context="", a_context="") - 单个匹配 2. match_batch(b_items, a_content, model_name, b_context="", a_context="") - 批量匹配 支持可选的 Context 参数: - b_context: B 的补充上下文(帮助理解 B) - a_context: A 的补充上下文(帮助理解 A) - Context 默认为空,不提供时不会出现在 prompt 中 """ import json from typing import List from agents import Agent, Runner from agents.tracing.create import custom_span from lib.client import get_model # ========== System Prompt ========== MATCH_SYSTEM_PROMPT = """ # 任务 分析 中的字面语义匹配关系。 ## 输入说明 - ****: 待匹配的内容(必选) - ****: 上下文内容(必选) - ****: B 的补充上下文(可选,帮助理解 B) - ****: A 的补充上下文(可选,帮助理解 A) **重要**:匹配分析发生在 之间,Context 仅作为补充理解的辅助信息。 ## 分析方法 ### 核心原则:字面语义匹配 只关注 在**字面词语和概念**上的重叠度,不考虑抽象关系。 ### 分析步骤 1. **提取关键词/概念** - 从 中提取:关键词语和核心概念 - 从 中提取:关键词语和核心概念 2. **识别相同部分** - 完全相同的词语(字面一致) - 同义词或近义词 3. **识别增量部分** - 中有,但 中没有的词语/概念 - 这些是 相对于 的额外信息 4. **计算匹配分数** - 基于相同部分的覆盖度 - 考虑词语/概念的重要性 --- ## 评分标准(0-1分) **字面匹配度评分:** - **0.9-1.0**: 几乎完全一致,词语高度重叠 - **0.7-0.8**:大部分核心词语/概念匹配,少量增量 - **0.5-0.6**:部分核心词语/概念匹配,有一定增量 - **0.3-0.4**:少量词语/概念匹配,大部分不同 - **0.1-0.2**:几乎无字面匹配,仅有概念联系 - **0.0**:完全无关 **重要原则:** - 如果 是抽象/元级别的描述,而 是具体内容,字面上无词语重叠,应给低分(0.1-0.3) - 优先考虑具体词语的匹配,而非抽象概念的包含关系 --- ## 输出格式(严格JSON) ```json { "score": 0.75, "score说明": "简要说明分数是如何计算的,基于哪些词语/概念的匹配", "相同部分": { "B中的词1": "与A中的'某词'完全相同", "B中的词2": "与A中的'某词'同义" }, "增量部分": { "B中的词3": "A中无此概念" } } ``` **输出要求**: 1. 必须严格按照上述JSON格式输出(score 和 score说明在最前面) 2. 所有字段都必须填写 3. **score字段**:必须是0-1之间的浮点数,保留2位小数 4. **score说明**:必须简洁说明评分依据(基于相同部分的覆盖度) 5. **相同部分**:字典格式,key是中的词语,value说明它与中哪个词的关系(完全相同/同义);如果没有则填写空字典 {} 6. **增量部分**:字典格式,key是中的词语,value说明为什么是增量(如"A中无此概念");如果没有增量部分,填写空字典 {} 7. **关键约束**:相同部分和增量部分的key必须只能是中的词语,不能是中的词语 """.strip() # ========== System Prompt for Name+Definition Match ========== MATCH_WITH_DEFINITION_PROMPT = """ # 任务 分析 的语义匹配关系。 包含"名称"和"定义",定义作为辅助上下文帮助理解名称的含义。 ## 输入说明 - ****: 待匹配的内容(灵感点) - ****: 名称(必选) - ****: B 的补充上下文(可选) - ****: A 的补充上下文,包含定义等信息(可选) **重要**:A_Context 中的定义仅用于辅助理解名称 的含义,主要分析 的匹配关系。 ## 分析方法 ### 核心原则:语义概念匹配 先独立分析 的语义,再进行匹配,避免强行解释。 ### 分析步骤 **第一步:独立分析 的语义** 对 (灵感点)进行语义分解,提取: **拆分步骤**: 1. 先识别出所有语义成分(主体、动作/状态、属性、修饰等) 2. 对每个成分做"去除测试": - 如果去掉这个成分后,剩余部分的核心语义**没有发生本质变化** → 该成分是**形式** - 如果去掉这个成分后,剩余部分的核心语义**发生了本质变化** → 该成分是**实质** 3. 将成分分类到实质或形式中,每个成分只能出现在一个类别 - **实质语义**:核心概念、本质内容(字典格式) - 通常为1-2个核心概念 - 不要过度拆分复合词,保持核心概念的完整性 - key:语义片段(可以是词组) - value:**客观地说明这个片段在B的上下文中的具体含义** - **形式语义**:具体限定、修饰、程度等(字典格式) - 包括:主体限定词、属性修饰词、程度副词、时间/空间限定等 - key:语义片段 - value:**说明在B的上下文中修饰/限定什么,如何修饰/限定** **关键原则**: - 每个语义片段只能出现在一个类别中,不要重复 - 表示主体、场景、对象的限定词通常属于形式,不要合并到实质中 - 不要孤立地解释词语本身,要说明它**在当前B的上下文中**的具体含义 - 不要过度推理,保持客观:描述B字面上说的是什么,而不是可能引发什么 **注意**:纯粹分析 本身的语义,不要考虑与 的关系。 **第二步:独立分析 的语义** 对 (名称)进行语义分解,提取: - **实质语义**:核心概念、本质内容(字典格式) - **判断标准:单独拿出来看,语义跟原来相比没有发生本质变化** - key:语义片段(可以是词组,保持完整的核心概念) - value:**客观地说明这个片段在A的上下文中的具体含义和作用** - **形式语义**:具体限定、修饰等(字典格式) - **判断标准:去掉后,核心概念本身不会丢失,只是失去了限定、修饰等细节** - key:语义片段 - value:**说明在A的上下文中修饰/限定什么,如何修饰/限定** **关键**: - 可以参考中的定义来辅助理解名称的含义,但只分析名称本身 - 同样要说明语义片段**在当前A的上下文中**的具体含义,不要孤立解释 - 不要过度拆分复合词,保持核心概念的完整性 **注意**:纯粹分析 本身的语义,不要考虑与 的关系。 **第三步:建立匹配关系** **只匹配实质语义**,形式语义作为补充信息不参与匹配。 遍历 的每个**实质语义**片段,找到与 中最接近的**实质语义**,判断关系: **重要**: - 在匹配关系中必须复述B语义说明和A语义说明,直接从前面的语义分析中复制过来 - 形式语义(修饰词、限定词等)不单独匹配,因为它们脱离实质语义就失去意义 - 形式语义的差异可以在score说明中提及,作为参考信息 1. **判断是否可比较**(基于抽象层级和本体范畴) - **参考B语义说明和A语义说明**,判断两个语义是否处于可比较的层级 - **不可比较的情况**(直接标记为"无关"): - **元概念 vs 具体概念**:元概念(如"概念"、"内容"、"事物"、"要素"、"方面"、"维度"、"属性"等用于描述其他概念的抽象术语)不能与具体概念(如"期待"、"行为"、"猫咪"等实际指代特定事物的词)比较 - **抽象层级相差过大**:相差2级以上(如"存在" vs "猫的行为") - **本体范畴完全不同**:不同的基本范畴(如数学概念 vs 生物实体、物理对象 vs 心理状态等,除非有明确的跨域映射关系) - **可比较的情况**: - 同级具体概念(如"期待" vs "焦虑") - 有明确上下位关系(如"情感" vs "期待") - 有交叉或关联关系(如"工作压力" vs "假期期待") - **输出要求**: - 如果不可比较:输出 "是否可比较": false, "不可比较原因": "说明为什么不可比较", "关系": "无关" - 如果可比较:输出 "是否可比较": true, 继续判断关系类型 2. **确定关系类型**(仅对可比较的语义) - **同义**:在各自上下文中的含义完全相同,可以互相替换 - **近义**:在各自上下文中的含义非常接近,但有细微差异 - **上位词**:B在其上下文中的含义 比 A在其上下文中的含义 更抽象、更上位(B ⊇ A) - **下位词**:B在其上下文中的含义 比 A在其上下文中的含义 更具体、更下位(B ⊆ A) - **无关**:虽然可比较,但没有明确的语义关系 3. **评估语义距离** - 对每对匹配的语义,评估它们之间的语义距离 - **距离分数**:0-1之间的浮点数,保留2位小数 - **1.0**:语义完全相同 - **0.8-0.95**:语义非常接近 - **0.5-0.7**:语义有明确关联或层级关系 - **0.1-0.4**:语义有弱关联 - **0.0**:语义完全无关 - 距离分数只考虑B语义和A语义的语义相似度,不考虑其他因素 4. **计算规则分数** - 关系权重:同义=1.0, 近义=0.7, 上位词=0.8, 下位词=0.6, 无关=0.0 - 计算公式: ``` 关系得分 = 关系权重 × 距离分数 规则分数 = Σ(关系得分) / 实质语义总数 ``` - 例如:关系="近义", 距离分数=0.9 → 关系得分 = 0.7 × 0.9 = 0.63 5. **评估整体相关性** - 在完成上述匹配关系分析后,从宏观角度评估 的整体相关性 - 考虑因素: - 语义距离:核心概念之间的距离 - 上下文契合度:在各自上下文中的关联程度 - 表达意图的一致性 - **评分标准**(严格遵守): - **1.0**: 完全同义,可以互相替换,表达完全相同的含义 - **0.8-0.9**:高度相关,核心语义非常接近,只有细微差异 - **0.6-0.7**:明显相关,有明确的语义重叠或上下位关系 - **0.4-0.5**:一般相关,有一定的语义关联或主题相关性 - **0.2-0.3**:弱相关,只有间接关联(如因果、触发等) - **0.0-0.1**:几乎无关或完全无关 - 给出相关性分数(0-1之间的浮点数,保留2位小数) - 给出相关性说明 6. **计算最终分数** - 最终分数 = 0.8 × 规则分数 + 0.2 × 相关性分数 - 在score说明中说明计算过程,可以提及形式语义的差异作为补充参考 --- ## 评分标准(0-1分) **语义匹配度评分:** - **0.9-1.0**: 的核心语义与名称/定义几乎完全对应 - **0.7-0.8**:大部分核心语义匹配,少量增量 - **0.5-0.6**:部分核心语义匹配,有一定增量 - **0.3-0.4**:少量语义匹配,大部分概念不同 - **0.1-0.2**:几乎无语义匹配,仅有弱关联 - **0.0**:完全无关 **重要原则:** - 关注有价值的语义片段,而非孤立的字面词语 - 考虑概念之间的语义关系,不仅是字面匹配 --- ## 输出格式(严格JSON) ```json { "B语义分析": { "实质": { "语义片段1": "说明" }, "形式": { "语义片段2": "说明", "语义片段3": "说明" } }, "A语义分析": { "实质": { "语义片段1": "说明", "语义片段2": "说明" }, "形式": { "语义片段3": "说明" } }, "匹配关系": [ { "B语义": "语义片段1", "B语义说明": "从B实质语义分析中复述该片段的说明", "A语义": "语义片段1", "A语义说明": "从A实质语义分析中复述该片段的说明", "是否可比较": true, "关系": "近义", "说明": "说明两者的关系", "距离分数": 0.85 }, { "B语义": "语义片段2", "B语义说明": "从B实质语义分析中复述该片段的说明", "A语义": "概念", "A语义说明": "从A实质语义分析中复述该片段的说明", "是否可比较": false, "不可比较原因": "A是元概念(用于描述其他概念的术语),B是具体概念,抽象层级完全不同", "关系": "无关", "说明": "元概念不能与具体概念比较", "距离分数": 0.0 } ], "规则分数": 0.3, "规则分数说明": "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。", "相关性分数": 0.3, "相关性说明": "虽然有部分语义重叠,但整体上下文契合度较低,因为B强调具体事物,A强调抽象概念", "score": 0.3, "score说明": "最终分数 = 0.8 × 0.3 + 0.2 × 0.3 = 0.3。形式语义差异:B有'修饰词x',A有'修饰词y',作为补充参考。" } ``` **输出要求**: 1. 必须严格按照上述JSON格式输出 2. **B语义分析**:必须包含"实质"、"形式"两个字段,其中实质通常为1-2个核心概念 3. **A语义分析**:必须包含"实质"、"形式"两个字段 4. **实质和形式**:都是字典格式,key是语义片段,value是对该片段的说明 5. **匹配关系**:数组格式,**只包含实质语义的匹配**,字段顺序为: - B语义 → B语义说明 → A语义 → A语义说明 → 是否可比较 - 如果是否可比较=false:添加"不可比较原因"字段 - 关系 → 说明 → 距离分数 6. **B语义说明和A语义说明**:必须从前面的实质语义分析中复述,保持一致 7. **是否可比较**:布尔值,true或false 8. **不可比较原因**:字符串,仅在是否可比较=false时提供,说明为什么不可比较 9. **关系**:必须是以下之一:"同义"、"近义"、"上位词"、"下位词"、"无关" 10. **距离分数**:0-1之间的浮点数,保留2位小数,表示B语义和A语义的语义距离/相似度 11. **规则分数**:0-1之间的浮点数,保留2位小数,基于匹配关系计算(关系得分 = 关系权重 × 距离分数) 12. **规则分数说明**:说明规则分数的计算依据,包含每个匹配关系的计算过程 13. **相关性分数**:0-1之间的浮点数,保留2位小数,整体相关性评估 14. **相关性说明**:说明相关性分数的判断依据 15. **score**:0-1之间的浮点数,保留2位小数,= 0.8 × 规则分数 + 0.2 × 相关性分数 16. **score说明**:说明最终分数的计算过程,可以提及形式语义的差异作为补充 """.strip() def create_match_agent(model_name: str) -> Agent: """创建信息匹配分析的 Agent Args: model_name: 模型名称 Returns: Agent 实例 """ agent = Agent( name="Information Match Expert", instructions=MATCH_SYSTEM_PROMPT, model=get_model(model_name), tools=[], ) return agent def calculate_score_by_code(match_result: dict) -> float: """根据匹配关系和相关性分数用代码计算最终score 计算公式: - 关系得分 = 关系权重 × 距离分数 - 规则分数 = Σ(关系得分) / 实质语义总数 - 最终score = 0.8 × 规则分数 + 0.2 × 相关性分数 Args: match_result: 匹配结果字典,包含 B语义分析、匹配关系、相关性分数 Returns: 计算得出的最终score(0-1之间) Raises: ValueError: 当数据不完整时 """ # 关系权重映射 RELATION_WEIGHTS = { "同义": 1.0, "近义": 0.7, "上位词": 0.8, "下位词": 0.6, "无关": 0.0 } b_semantics = match_result.get("B语义分析", {}) match_relations = match_result.get("匹配关系", []) relevance_score = match_result.get("相关性分数") # 验证必需字段 if not match_relations: raise ValueError("匹配关系为空,无法计算score_by_code") if not b_semantics: raise ValueError("B语义分析为空,无法计算score_by_code") if relevance_score is None: raise ValueError("相关性分数为空,无法计算score_by_code") # 提取B的实质语义片段 b_essence = set(b_semantics.get("实质", {}).keys()) if not b_essence: raise ValueError("B语义分析中实质为空,无法计算score_by_code") # 计算规则分数:基于实质语义的匹配关系 relation_scores = [] # 存储每个关系的得分 for relation in match_relations: b_semantic = relation.get("B语义", "") relation_type = relation.get("关系", "无关") distance_score = relation.get("距离分数") if distance_score is None: raise ValueError(f"匹配关系中缺少距离分数: {b_semantic}") # 验证该语义确实属于实质 if b_semantic not in b_essence: raise ValueError(f"匹配关系中包含非实质语义: {b_semantic}") # 计算关系得分 = 关系权重 × 距离分数 weight = RELATION_WEIGHTS.get(relation_type, 0.0) relation_score = weight * distance_score relation_scores.append(relation_score) if not relation_scores: raise ValueError("匹配关系中没有实质语义,无法计算score_by_code") # 计算规则分数(基于匹配关系) rule_score = sum(relation_scores) / len(relation_scores) # 计算最终分数:0.8 × 规则分数 + 0.2 × 相关性分数 final_score = 0.8 * rule_score + 0.2 * relevance_score return round(final_score, 2) def parse_match_response(response_content: str) -> dict: """解析匹配响应 Args: response_content: Agent 返回的响应内容 Returns: 解析后的字典 """ try: # 如果响应包含在 markdown 代码块中,提取 JSON 部分 if "```json" in response_content: json_start = response_content.index("```json") + 7 json_end = response_content.index("```", json_start) json_text = response_content[json_start:json_end].strip() elif "```" in response_content: json_start = response_content.index("```") + 3 json_end = response_content.index("```", json_start) json_text = response_content[json_start:json_end].strip() else: json_text = response_content.strip() return json.loads(json_text) except Exception as e: print(f"解析响应失败: {e}") return { "相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": f"解析失败: {str(e)}" } def _create_batch_agent(model_name: str) -> Agent: """创建批量匹配的 Agent Args: model_name: 模型名称 Returns: Agent 实例 """ # 批量匹配的 System Prompt(在单个匹配基础上修改输出格式) batch_prompt = MATCH_SYSTEM_PROMPT.replace( "## 输出格式(严格JSON)", "## 输出格式(JSON数组)\n对每个 输出一个匹配结果:" ).replace( "```json\n{", "```json\n[{" ).replace( "}\n```", "}]\n```" ) + "\n\n**额外要求**:数组长度必须等于 的数量,顺序对应" agent = Agent( name="Batch Information Match Expert", instructions=batch_prompt, model=get_model(model_name), tools=[], ) return agent async def _run_match_agent( agent: Agent, b_content: str, a_content: str, request_desc: str, b_context: str = "", a_context: str = "" ) -> str: """运行匹配 Agent 的公共逻辑 Args: agent: Agent 实例 b_content: B 的内容 a_content: A 的内容 request_desc: 请求描述(如"并输出 JSON 格式"或"并输出 JSON 数组格式") b_context: B 的上下文(可选) a_context: A 的上下文(可选) Returns: Agent 的原始输出 """ # 构建任务描述 b_section = f"\n{b_content}\n" if b_context: b_section += f"\n\n\n{b_context}\n" a_section = f"\n{a_content}\n" if a_context: a_section += f"\n\n\n{a_context}\n" task_description = f"""## 本次分析任务 {b_section} {a_section} 请严格按照系统提示中的要求分析 中的字面语义匹配关系,{request_desc}的结果。""" # 构造消息 messages = [{ "role": "user", "content": [ { "type": "input_text", "text": task_description } ] }] # 使用 custom_span 追踪匹配过程 # 截断显示内容,避免 span name 过长 b_short = (b_content[:40] + "...") if len(b_content) > 40 else b_content a_short = (a_content[:40] + "...") if len(a_content) > 40 else a_content with custom_span( name=f"匹配分析: {b_short} in {a_short}", data={ "B": b_content, "A": a_content, "B_Context": b_context if b_context else None, "A_Context": a_context if a_context else None, "模式": request_desc } ): # 运行 Agent result = await Runner.run(agent, input=messages) return result.final_output async def match_single( b_content: str, a_content: str, model_name: str, b_context: str = "", a_context: str = "" ) -> dict: """单个匹配:分析一个 B 在 A 中的匹配 Args: b_content: B(待匹配)的内容 a_content: A(上下文)的内容 model_name: 使用的模型名称 b_context: B 的补充上下文(可选,默认为空) a_context: A 的补充上下文(可选,默认为空) Returns: 匹配结果字典:{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""} """ try: # 创建 Agent agent = create_match_agent(model_name) # 运行匹配 output = await _run_match_agent( agent, b_content, a_content, "并输出 JSON 格式", b_context=b_context, a_context=a_context ) # 解析响应 parsed_result = parse_match_response(output) return parsed_result except Exception as e: return { "相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": f"匹配过程出错: {str(e)}" } async def match_batch( b_items: List[str], a_content: str, model_name: str, b_context: str = "", a_context: str = "" ) -> List[dict]: """批量匹配:分析多个 B 在 A 中的匹配(一次调用) Args: b_items: B列表(多个待匹配项) a_content: A(上下文)的内容 model_name: 使用的模型名称 b_context: B 的补充上下文(可选,默认为空) a_context: A 的补充上下文(可选,默认为空) Returns: 匹配结果列表:[{"相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": ""}, ...] """ try: # 创建批量匹配 Agent agent = _create_batch_agent(model_name) # 构建 B 列表字符串 b_list_str = "\n".join([f"- {item}" for item in b_items]) # 运行匹配 output = await _run_match_agent( agent, b_list_str, a_content, "并输出 JSON 数组格式", b_context=b_context, a_context=a_context ) # 解析响应(期望是数组) parsed_result = parse_match_response(output) # 如果返回的是数组,直接返回;如果是单个对象,包装成数组 if isinstance(parsed_result, list): return parsed_result else: return [parsed_result] except Exception as e: # 返回错误信息(为每个 B 创建一个错误条目) return [{ "相同部分": {}, "增量部分": {}, "score": 0.0, "score说明": f"匹配过程出错: {str(e)}" } for _ in b_items] async def match_with_definition( b_content: str, element_name: str, element_definition: str, model_name: str, b_context: str = "", a_context: str = "" ) -> dict: """名称+定义匹配:分析 B 分别与名称、定义的语义匹配关系 Args: b_content: B(待匹配)的内容 element_name: 要素名称 element_definition: 要素定义 model_name: 使用的模型名称 b_context: B 的补充上下文(可选,默认为空) a_context: A 的补充上下文(可选,默认为空) Returns: 匹配结果字典:{"名称匹配": {...}, "定义匹配": {...}} """ try: # 创建使用新 prompt 的 Agent agent = Agent( name="Name+Definition Match Expert", instructions=MATCH_WITH_DEFINITION_PROMPT, model=get_model(model_name), tools=[], ) # A 内容只包含名称,定义放在 A_Context 中 a_content = element_name # 将定义添加到 A_Context(如果已有 a_context,则追加) if element_definition: definition_context = f"定义:{element_definition}" if a_context: full_a_context = f"{a_context}\n{definition_context}" else: full_a_context = definition_context else: full_a_context = a_context # 运行匹配 output = await _run_match_agent( agent, b_content, a_content, "并输出 JSON 格式", b_context=b_context, a_context=full_a_context ) # 解析响应 parsed_result = parse_match_response(output) # 验证返回格式 required_fields = [ "B语义分析", "A语义分析", "匹配关系", "规则分数", "规则分数说明", "相关性分数", "相关性说明", "score", "score说明" ] missing_fields = [f for f in required_fields if f not in parsed_result] if missing_fields: return { "B语义分析": { "实质": {}, "形式": {} }, "A语义分析": { "实质": {}, "形式": {} }, "匹配关系": [], "规则分数": 0.0, "规则分数说明": f"解析失败:缺少字段 {missing_fields}", "相关性分数": 0.0, "相关性说明": f"解析失败:缺少字段 {missing_fields}", "score": 0.0, "score_by_code": 0.0, "score说明": f"解析失败:缺少字段 {missing_fields}" } # 计算程序score(如果数据不完整会抛出异常) score_by_code = calculate_score_by_code(parsed_result) parsed_result["score_by_code"] = score_by_code return parsed_result except Exception as e: return { "B语义分析": { "实质": {}, "形式": {} }, "A语义分析": { "实质": {}, "形式": {} }, "匹配关系": [], "规则分数": 0.0, "规则分数说明": f"匹配过程出错: {str(e)}", "相关性分数": 0.0, "相关性说明": f"匹配过程出错: {str(e)}", "score": 0.0, "score_by_code": 0.0, "score说明": f"匹配过程出错: {str(e)}" }