Pārlūkot izejas kodu

feat: 改进语义匹配 - 只匹配实质语义,形式作为补充

主要改动:
1. 匹配逻辑:从"实质+形式"改为"只匹配实质"
   - 形式语义(修饰词、限定词)不再单独匹配
   - 形式语义脱离实质就失去意义,只作为补充说明

2. 评分规则:简化权重计算
   - 旧:实质 0.8 + 形式 0.2
   - 新:只基于实质语义计算
   - 公式:score = Σ(实质语义的关系得分) / 实质语义总数

3. 匹配关系:增加语义说明字段
   - 添加 B语义说明 和 A语义说明
   - 从前面的语义分析中复述,防止LLM重新解释
   - 确保匹配阶段与分析阶段的理解一致

4. 新增 score_by_code 字段
   - 程序计算的score,用于验证LLM的计算
   - 数据不完整时抛出异常,不使用默认值
   - 只基于实质语义匹配计算

5. 修复排序问题
   - step1结果按score降序排序(之前排序key错误)

6. 新增 --step1-only 参数
   - run_inspiration_analysis.py 支持只执行 Step1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 2 nedēļas atpakaļ
vecāks
revīzija
4f1b314b08
3 mainītis faili ar 131 papildinājumiem un 19 dzēšanām
  1. 89 12
      lib/match_analyzer.py
  2. 33 2
      run_inspiration_analysis.py
  3. 9 5
      step1_inspiration_match.py

+ 89 - 12
lib/match_analyzer.py

@@ -174,9 +174,17 @@ MATCH_WITH_DEFINITION_PROMPT = """
 
 **第三步:建立匹配关系**
 
-遍历 <B> 的每个语义片段(实质+形式),找到与 <A> 中最接近的语义,判断关系:
+**只匹配实质语义**,形式语义作为补充信息不参与匹配。
+
+遍历 <B> 的每个**实质语义**片段,找到与 <A> 中最接近的**实质语义**,判断关系:
+
+**重要**:
+- 在匹配关系中必须复述B语义说明和A语义说明,直接从前面的语义分析中复制过来
+- 形式语义(修饰词、限定词等)不单独匹配,因为它们脱离实质语义就失去意义
+- 形式语义的差异可以在score说明中提及,作为参考信息
 
 1. **判断是否同一概念**(基于上下文中的作用/本质)
+   - **参考B语义说明和A语义说明**,判断在各自上下文中的作用/本质是否相同
    - **不是看字面是否相同**,而是看**在各自上下文中的作用/本质是否相同**
    - 同一概念:可以比较上下位/同义关系
    - 不同概念:标记为无关
@@ -189,13 +197,12 @@ MATCH_WITH_DEFINITION_PROMPT = """
 
 3. **计算匹配分数**
    - 关系得分:同义=1.0, 上位词=0.6, 下位词=0.4, 无关=0.0
-   - 实质语义权重=0.8,形式语义权重=0.2
+   - 只基于实质语义匹配计算score
    - 计算公式:
      ```
-     实质得分 = Σ(实质语义的关系得分) / 实质语义总数
-     形式得分 = Σ(形式语义的关系得分) / 形式语义总数
-     最终 score = 实质得分 × 0.8 + 形式得分 × 0.2
+     score = Σ(实质语义的关系得分) / 实质语义总数
      ```
+   - 在score说明中,可以提及形式语义的差异作为补充参考
 
 ---
 
@@ -239,21 +246,25 @@ MATCH_WITH_DEFINITION_PROMPT = """
   "匹配关系": [
     {
       "B语义": "语义片段1",
+      "B语义说明": "从B实质语义分析中复述该片段的说明",
       "A语义": "语义片段1",
+      "A语义说明": "从A实质语义分析中复述该片段的说明",
       "是否同一概念": true,
       "关系": "下位词",
       "说明": "说明两者的关系"
     },
     {
       "B语义": "语义片段2",
+      "B语义说明": "从B实质语义分析中复述该片段的说明",
       "A语义": "语义片段2",
+      "A语义说明": "从A实质语义分析中复述该片段的说明",
       "是否同一概念": false,
       "关系": "无关",
       "说明": "说明为什么无关"
     }
   ],
-  "score": 0.65,
-  "score说明": "基于评分规则计算得出"
+  "score": 0.4,
+  "score说明": "只基于实质语义计算。B有2个实质语义,第1个是A的下位词(0.4),第2个无关(0.0),最终score = (0.4 + 0.0) / 2 = 0.4。形式语义差异:B有'修饰词x',A有'修饰词y',作为补充参考。"
 }
 ```
 
@@ -262,11 +273,12 @@ MATCH_WITH_DEFINITION_PROMPT = """
 2. **B语义分析**:必须包含"实质"、"形式"两个字段,其中实质通常为1-2个核心概念
 3. **A语义分析**:必须包含"实质"、"形式"两个字段
 4. **实质和形式**:都是字典格式,key是语义片段,value是对该片段的说明
-5. **匹配关系**:数组格式,包含所有B语义片段的匹配情况,字段顺序为:B语义 → A语义 → 是否同一概念 → 关系 → 说明
-6. **是否同一概念**:布尔值,true或false
-7. **关系**:必须是以下之一:"同义/近义"、"上位词"、"下位词"、"无关"
-8. **score**:0-1之间的浮点数,保留2位小数,按照评分规则计算
-9. **score说明**:说明分数的计算依据
+5. **匹配关系**:数组格式,**只包含实质语义的匹配**,字段顺序为:B语义 → B语义说明 → A语义 → A语义说明 → 是否同一概念 → 关系 → 说明
+6. **B语义说明和A语义说明**:必须从前面的实质语义分析中复述,保持一致
+7. **是否同一概念**:布尔值,true或false
+8. **关系**:必须是以下之一:"同义/近义"、"上位词"、"下位词"、"无关"
+9. **score**:0-1之间的浮点数,保留2位小数,只基于实质语义匹配计算
+10. **score说明**:说明分数的计算依据,可以提及形式语义的差异作为补充
 """.strip()
 
 
@@ -289,6 +301,65 @@ def create_match_agent(model_name: str) -> Agent:
     return agent
 
 
+def calculate_score_by_code(match_result: dict) -> float:
+    """根据匹配关系用代码计算score(只基于实质语义)
+
+    Args:
+        match_result: 匹配结果字典,包含 B语义分析、A语义分析、匹配关系
+
+    Returns:
+        计算得出的score(0-1之间)
+
+    Raises:
+        ValueError: 当匹配关系为空或数据不完整时
+    """
+    # 关系得分映射
+    RELATION_SCORES = {
+        "同义/近义": 1.0,
+        "上位词": 0.6,
+        "下位词": 0.4,
+        "无关": 0.0
+    }
+
+    b_semantics = match_result.get("B语义分析", {})
+    match_relations = match_result.get("匹配关系", [])
+
+    if not match_relations:
+        raise ValueError("匹配关系为空,无法计算score_by_code")
+
+    if not b_semantics:
+        raise ValueError("B语义分析为空,无法计算score_by_code")
+
+    # 提取B的实质语义片段
+    b_essence = set(b_semantics.get("实质", {}).keys())
+
+    if not b_essence:
+        raise ValueError("B语义分析中实质为空,无法计算score_by_code")
+
+    # 只计算实质语义的得分
+    essence_scores = []
+
+    for relation in match_relations:
+        b_semantic = relation.get("B语义", "")
+        relation_type = relation.get("关系", "无关")
+        score = RELATION_SCORES.get(relation_type, 0.0)
+
+        # 验证该语义确实属于实质
+        if b_semantic in b_essence:
+            essence_scores.append(score)
+        else:
+            # 如果匹配关系中包含了非实质语义,报错
+            raise ValueError(f"匹配关系中包含非实质语义: {b_semantic}")
+
+    if not essence_scores:
+        raise ValueError("匹配关系中没有实质语义,无法计算score_by_code")
+
+    # 只基于实质语义计算平均分
+    final_score = sum(essence_scores) / len(essence_scores)
+
+    return round(final_score, 2)
+
+
 def parse_match_response(response_content: str) -> dict:
     """解析匹配响应
 
@@ -585,9 +656,14 @@ async def match_with_definition(
                 },
                 "匹配关系": [],
                 "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:
@@ -602,5 +678,6 @@ async def match_with_definition(
             },
             "匹配关系": [],
             "score": 0.0,
+            "score_by_code": 0.0,
             "score说明": f"匹配过程出错: {str(e)}"
         }

+ 33 - 2
run_inspiration_analysis.py

@@ -77,7 +77,8 @@ async def run_full_analysis(
     max_tasks: int = None,
     force: bool = False,
     current_time: str = None,
-    log_url: str = None
+    log_url: str = None,
+    step1_only: bool = False
 ) -> dict:
     """执行完整的灵感分析流程(Step1 + Step2)
 
@@ -88,6 +89,7 @@ async def run_full_analysis(
         force: 是否强制重新执行(跳过文件存在检查)
         current_time: 当前时间戳
         log_url: 日志链接
+        step1_only: 是否只执行 Step1,跳过 Step2
 
     Returns:
         包含文件路径和状态的字典
@@ -138,6 +140,21 @@ async def run_full_analysis(
     step1_element = step1_top1.get("业务信息", {}).get("匹配要素", "")
     print(f"Top1 匹配要素: {step1_element}, score: {step1_score:.2f}")
 
+    # 如果只执行 Step1,直接返回
+    if step1_only:
+        print(f"\n{'=' * 80}")
+        print(f"Step1 执行完成(跳过 Step2)")
+        print(f"{'=' * 80}")
+        print(f"\n结果文件:")
+        print(f"  Step1: {step1_file}\n")
+
+        return {
+            "step1_file": step1_file,
+            "step2_file": None,
+            "summary_file": None,
+            "status": "step1_only"
+        }
+
     # ========== Step2: 增量词匹配 ==========
     print(f"\n{'─' * 80}")
     print(f"Step2: 增量词在人设中的匹配")
@@ -237,6 +254,9 @@ async def main():
 
   # 处理前10个灵感,step1只处理前20个任务
   python run_inspiration_analysis.py --count 10 --max-tasks 20
+
+  # 只执行 Step1,跳过 Step2
+  python run_inspiration_analysis.py --count 5 --step1-only
         """
     )
 
@@ -271,11 +291,18 @@ async def main():
         help="随机选择灵感,而不是按顺序"
     )
 
+    parser.add_argument(
+        "--step1-only",
+        action="store_true",
+        help="只执行 Step1,跳过 Step2"
+    )
+
     args = parser.parse_args()
 
     persona_dir = args.dir
     force = args.force
     shuffle = args.shuffle
+    step1_only = args.step1_only
 
     # 处理 max_tasks
     max_tasks = None if args.max_tasks == "all" else int(args.max_tasks)
@@ -305,6 +332,9 @@ async def main():
     if shuffle:
         print(f"随机模式: 随机选择灵感")
 
+    if step1_only:
+        print(f"Step1 Only: 只执行 Step1,跳过 Step2")
+
     # 选择要处理的灵感列表
     if shuffle:
         # 随机打乱灵感列表后选择
@@ -336,7 +366,8 @@ async def main():
                 max_tasks=max_tasks,
                 force=force,
                 current_time=insp_time,
-                log_url=insp_log_url
+                log_url=insp_log_url,
+                step1_only=step1_only
             )
 
         results.append(result)

+ 9 - 5
step1_inspiration_match.py

@@ -18,9 +18,16 @@ from lib.match_analyzer import match_with_definition
 from lib.data_loader import load_persona_data, load_inspiration_list, select_inspiration
 
 # 模型配置
+MODEL_NAME = 'x-ai/grok-code-fast-1'
+MODEL_NAME = 'anthropic/claude-sonnet-4.5'
+MODEL_NAME = 'google/gemini-2.5-flash'
+MODEL_NAME = 'openai/gpt-5'
+MODEL_NAME = 'deepseek/deepseek-chat-v3-0324'
+MODEL_NAME = 'openai/gpt-4.1'
 MODEL_NAME = "google/gemini-2.5-pro"
 
 
+
 def build_context_str(perspective_name: str, level1_name: str = None) -> str:
     """构建上下文字符串
 
@@ -200,12 +207,9 @@ async def process_inspiration_match(
             show_progress=True
         )
 
-        # 按名称匹配 score 降序排序(主要)+ 定义匹配 score(次要)
+        # 按 score 降序排序
         results.sort(
-            key=lambda x: (
-                x.get('匹配结果', {}).get('名称匹配', {}).get('score', 0),
-                x.get('匹配结果', {}).get('定义匹配', {}).get('score', 0)
-            ),
+            key=lambda x: x.get('匹配结果', {}).get('score', 0),
             reverse=True
         )