SAILER启发的法律类案推荐系统:向量检索+结构化要素四维匹配

核心技术: ChromaDB向量相似度 × 结构化要素匹配 = 混合重排序
论文灵感: SAILER (SIGIR 2023) + CaseGNN (EMNLP 2023)
最终公式: final_score = 0.6 × vector_sim + 0.4 × element_sim


一、前言:纯向量检索的"语义漂移"问题

在法律AI中,类案推荐是高频需求——法官需要参照先例,律师需要援引判例。基于向量检索(ChromaDB/Milvus)的方案已经很成熟,但存在一个致命问题:语义漂移

什么是语义漂移?

案例A: 张三诉李四买卖合同纠纷,争议焦点是"违约金计算方式"
案例B: 王五诉赵六买卖合同纠纷,争议焦点是"货物质量不合格"
案例C: 甲公司诉乙公司建设工程合同纠纷,争议焦点是"违约金计算方式"

纯向量检索:A↔B 相似度=0.85(同为"买卖合同",文本重叠大)
实际相关性:A↔C 更相关(同一争议焦点,虽然合同类型不同)

问题本质:向量嵌入捕获了表面语义,但忽略了法律文书的结构化要素。


二、技术方案对比

方案 论文 核心思路 优势 GPU需求
SAILER SIGIR 2023 结构化预训练法律语言模型 深度理解法律结构
CaseGNN EMNLP 2023 构建案例关系图,GNN推理 捕获跨案例关联
Contrastive Learning ACL 2022 对比学习训练case encoder 高精度相似度
混合检索(本文) SAILER启发 向量+结构化要素融合 无GPU,API友好

核心思想借鉴:SAILER 论文的关键洞察是——法律文书可以分解为"事实认定(Fact)"、“争议焦点(Issue)”、"法条引用(Statute)"等结构化要素,基于这些要素的匹配比纯文本语义更准确。

我们不需要预训练SAILER模型,但可以用已有的字段提取结果实现同样的结构化匹配。


三、四维结构化要素匹配算法

3.1 算法总览

┌─────────────────────────────────────────┐
│            文档A (查询文档)              │
│  case_type: civil                       │
│  cause_of_action: "买卖合同纠纷"         │
│  dispute_focus: ["违约金", "合同效力"]    │
│  judgment: "...依照第107条..."           │
└──────────────┬──────────────────────────┘
               │ 对比
┌──────────────▼──────────────────────────┐
│            文档B (候选文档)              │
│  case_type: civil                       │
│  cause_of_action: "合同纠纷"             │
│  dispute_focus: ["违约金计算"]           │
│  judgment: "...依照第107条、第114条..."   │
└─────────────────────────────────────────┘

element_sim=w1⋅Stype+w2⋅Scause+w3⋅Sfocus+w4⋅Slawelement\_sim = w_1 \cdot S_{type} + w_2 \cdot S_{cause} + w_3 \cdot S_{focus} + w_4 \cdot S_{law}element_sim=w1Stype+w2Scause+w3Sfocus+w4Slaw

其中 w1=0.15,w2=0.30,w3=0.25,w4=0.30w_1=0.15, w_2=0.30, w_3=0.25, w_4=0.30w1=0.15,w2=0.30,w3=0.25,w4=0.30

3.2 维度1:案件类型完全匹配 (权重0.15)

# 最简单但最有用的特征
if fields_a.get('case_type') and fields_b.get('case_type'):
    if fields_a['case_type'] == fields_b['case_type']:
        score += 0.15

civil/criminal/administrative 三分类,完全匹配得满分。权重设置较低(0.15)是因为同类型内差异巨大。

3.3 维度2:案由Jaccard相似度 (权重0.30)

法律案由是半结构化文本(如"买卖合同纠纷"、“合同纠纷”),我们使用字符级Jaccard系数

cause_a = set(fields_a.get('cause_of_action', '') or '')
cause_b = set(fields_b.get('cause_of_action', '') or '')
if cause_a and cause_b:
    jaccard = len(cause_a & cause_b) / len(cause_a | cause_b)
    score += 0.30 * jaccard

为什么用字符级而不是词级?

案由1: "买卖合同纠纷"  → 字符集合: {买,卖,合,同,纠,纷}
案由2: "合同纠纷"      → 字符集合: {合,同,纠,纷}

字符Jaccard = |{合,同,纠,纷}| / |{买,卖,合,同,纠,纷}| = 4/6 = 0.667

如果用jieba分词:
案由1: ["买卖", "合同", "纠纷"]
案由2: ["合同", "纠纷"]
词级Jaccard = 2/3 = 0.667  (恰好一样)

但对于: "房屋买卖合同纠纷" vs "买卖合同纠纷"
字符Jaccard = 5/7 = 0.714
词级Jaccard = 2/4 = 0.500  (分词歧义导致差异大)

字符级Jaccard对中文案由更鲁棒。

3.4 维度3:争议焦点重叠度 (权重0.25)

争议焦点是列表形式(如 ["违约金", "合同效力"]),计算集合交集比例:

focus_a = set(fields_a.get('dispute_focus', []) or [])
focus_b = set(fields_b.get('dispute_focus', []) or [])
if focus_a and focus_b:
    overlap = len(focus_a & focus_b) / max(len(focus_a | focus_b), 1)
    score += 0.25 * overlap

3.5 维度4:法条引用重叠度 (权重0.30)

这是法律领域最强的关联信号——引用相同法条的案件几乎必然高度相关。

def extract_law_refs(text):
    """从判决书/事实认定中提取法条引用"""
    if not text:
        return set()
    pattern = r'第[\d一二三四五六七八九十百千]+条'
    return set(re.findall(pattern, str(text)))

text_a = f"{fields_a.get('judgment', '')} {fields_a.get('facts', '')}"
text_b = f"{fields_b.get('judgment', '')} {fields_b.get('facts', '')}"
laws_a = extract_law_refs(text_a)
laws_b = extract_law_refs(text_b)
if laws_a and laws_b:
    law_overlap = len(laws_a & laws_b) / max(len(laws_a | laws_b), 1)
    score += 0.30 * law_overlap

正则解析: r'第[\d一二三四五六七八九十百千]+条' 同时支持阿拉伯数字和中文数字。


四、混合检索流程

4.1 候选扩展 + 重排序

@router.get("/similar/{doc_id}")
async def get_similar_cases(doc_id: str, top_k: int = 5):
    # Step 1: 向量检索 — 多取3倍候选
    candidates = search_similar_docs(query, exclude_doc_id=doc_id, top_k=top_k * 3)
    
    # Step 2: 结构化要素计算
    alpha, beta = 0.6, 0.4
    ranked = []
    for r in candidates:
        vec_dist = r.get("distance", 1.0)
        vec_sim = max(0, 1 - vec_dist / 2)  # ChromaDB cosine distance → similarity
        elem_sim = _compute_element_similarity(fields_a_dict, fields_b)
        final_score = alpha * vec_sim + beta * elem_sim
        ranked.append({...scores: {vector_similarity, element_similarity, final_score}...})
    
    # Step 3: 按融合分数重排序
    ranked.sort(key=lambda x: x["scores"]["final_score"], reverse=True)
    return {"similar_cases": ranked[:top_k]}

4.2 ChromaDB距离转相似度

ChromaDB 使用 cosine distance(范围 [0, 2]),需要转换:

vector_similarity=max⁡(0,1−cosine_distance2)vector\_similarity = \max(0, 1 - \frac{cosine\_distance}{2})vector_similarity=max(0,12cosine_distance)

4.3 融合权重选择

final_score=0.6×vector_sim+0.4×element_simfinal\_score = 0.6 \times vector\_sim + 0.4 \times element\_simfinal_score=0.6×vector_sim+0.4×element_sim

为什么是 0.6/0.4 而不是 0.5/0.5?

  • 向量语义已经编码了大量信息(包括部分结构化信息)
  • 结构化要素提供的是"硬约束"型信号(法条匹配、类型匹配)
  • 如果字段提取质量不高,过大的element权重会引入噪声
  • 实测:0.6/0.4 在3个测试集上的 NDCG@5 最高

五、前端展示

5.1 案例卡片 + 分数条

<div v-for="(c, i) in cases" :key="i" class="case-card">
  <div class="card-header">
    <span class="case-number">{{ c.case_number }}</span>
    <span class="type-badge" :class="c.case_type">{{ caseTypeMap[c.case_type] }}</span>
    <span class="final-score" :style="{ color: scoreColor(c.scores.final_score) }">
      综合 {{ pct(c.scores.final_score) }}
    </span>
  </div>
  
  <!-- 分数分解条 -->
  <div class="score-breakdown">
    <div class="score-item">
      <span class="score-label">语义相似度</span>
      <div class="score-bar" :style="{ width: pct(c.scores.vector_similarity) }"></div>
      <span class="score-val">{{ pct(c.scores.vector_similarity) }}</span>
    </div>
    <div class="score-item">
      <span class="score-label">要素匹配</span>
      <div class="score-bar" :style="{ width: pct(c.scores.element_similarity) }"></div>
      <span class="score-val">{{ pct(c.scores.element_similarity) }}</span>
    </div>
  </div>
</div>

帮助用户理解为什么这个案例被推荐——是因为文本语义像(向量高),还是因为法律结构像(要素高)。


六、实战案例分析

假设查询文档是一份"民事买卖合同纠纷"判决书,引用了《合同法》第107条和第114条:

候选案例 向量相似度 要素相似度 融合分数 排名变化
买卖合同纠纷A (同法条) 0.78 0.72 0.756 ↑ 1→1
买卖合同纠纷B (不同法条) 0.82 0.15 0.552 ↓ 2→3
承揽合同纠纷C (同法条) 0.65 0.60 0.630 ↑ 3→2

关键发现: 案例B虽然向量相似度最高(同为"买卖合同",表面用词相似),但法条不匹配。案例C引用了相同法条,虽然合同类型不同但争议实质相同,融合后排名上升。


七、常见问题

Q1: 如果字段提取不完整怎么办?
A: 缺失的维度自动得0分,不会影响其他维度。这就是独立加权的好处。

Q2: 为什么不用更细粒度的法条匹配(考虑具体条款)?
A: 法条编号的正则已经够用了。"第107条"级别的匹配已经提供了很强的信号。条款级别需要法律知识库支撑。

Q3: 文档数量太少(<10)时效果如何?
A: 候选池小时,向量检索和要素匹配都会退化。建议至少上传20份文书以上才能获得有意义的推荐。


八、总结

本文贡献

  1. 将 SAILER 论文的结构化思想应用于无GPU的工程系统
  2. 四维要素匹配提供了可解释的法律关联度量
  3. 前端分数分解让用户理解推荐理由

优化方向

  • 更多维度: 加入当事人类型、判决结果相似度等
  • 权重学习: 用用户点击反馈自动调整四维权重
  • 跨库检索: 接入裁判文书网公开数据,扩大候选池
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐