基于混合语义溯源的法律文档摘要可追溯系统设计与实现

张开发
2026/4/9 3:28:22 15 分钟阅读

分享文章

基于混合语义溯源的法律文档摘要可追溯系统设计与实现
基于混合语义溯源的法律文档摘要可追溯系统设计与实现核心技术: LLM Prompt标注 OpenAI Embedding余弦相似度 双通道融合溯源论文灵感: ALCE (ACL 2023) RARR (EMNLP 2023)开发环境: Python 3.11 FastAPI Vue 3 OpenAI API一、前言为什么法律文档摘要需要溯源在法律AI应用中“幻觉”(Hallucination) 是最致命的问题。当AI摘要系统输出一句法院认定合同有效律师和法官需要立即验证——这句话在原文的哪个位置如果无法溯源AI生成的摘要就毫无法律应用价值。传统摘要系统的痛点信任危机— 生成的要点没有引证用户不敢采信效率低下— 用户需要手动在几十页原文中搜索对应段落责任风险— 错误引用导致的法律后果无法追责本文的解决方案我们设计了一套混合溯源架构每个摘要要点自动关联到原文的精确段落用户点击即可跳转高亮。二、技术方案对比与选型方案论文来源核心思路优势劣势GPU需求NLI溯源ALCE, ACL 2023用NLI模型验证摘要-原文蕴含关系精确度最高需要NLI分类器是Cross-EncoderRARR, EMNLP 2023交叉编码器对摘要-原文对打分高召回率计算量大是Attention归因—利用Transformer注意力矩阵定位来源可解释性强仅限白盒模型是混合溯源(本文)综合ALCERARRLLM标注 Embedding相似度无GPU, API友好依赖Embedding质量否最终选择混合溯源双通道融合— 兼顾精确度和工程可行性纯API方案无需GPU。三、系统架构┌─────────────────────┐ │ 用户上传PDF │ └────────┬────────────┘ ▼ ┌─────────────────────┐ │ 文档分块 编号 │ │ [Block 0] [Block 1] │ └────────┬────────────┘ ▼ ┌──────────────┼──────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ RAG检索 │ │ LLM摘要 │ │ Embedding │ │ (ChromaDB) │ │ (GPT-4) │ │ (3-small) │ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │ │ │ └───────────────┼───────────────┘ ▼ ┌──────────────────────┐ │ LLM标注 [来源: X,Y] │ ← 通道1 └──────────┬───────────┘ ▼ ┌──────────────────────┐ │ 语义cosine匹配 │ ← 通道2 └──────────┬───────────┘ ▼ ┌──────────────────────┐ │ 双通道融合 │ │ → source_mappings │ └──────────┬───────────┘ ▼ ┌──────────────────────┐ │ 前端点击跳转 高亮 │ └──────────────────────┘四、核心算法详解4.1 文档分块编号将原文拆分为编号块嵌入LLM Prompt中def_build_numbered_blocks(doc_id:str,max_blocks:int60)-str:构建带编号的原文块文本blocksget_blocks_by_doc(doc_id)ifnotblocks:returnlines[]fori,binenumerate(blocks[:max_blocks]):contentb[content]ifisinstance(b,dict)elseb.content lines.append(f[Block{i}]{content})return\n.join(lines)设计考量max_blocks60控制上下文长度避免超出LLM窗口编号从0开始与数据库索引对齐4.2 通道1LLM Prompt 标注在角色提示词中加入来源标注指令_SOURCE_INSTRUCTION ## 来源标注要求 在关键要素部分每个要点后面用方括号标注来源原文块编号 格式为 [来源: X] 或 [来源: X, Y]其中 X、Y 为原文块编号从0开始。 例如 - 原告要求被告赔偿损失50万元 [来源: 3, 5] - 法院认定合同有效 [来源: 12] 解析LLM输出中的来源标注def_parse_llm_source_mappings(key_points:list[str])-tuple[list[str],list[dict]]:从 LLM 输出的 [来源: X, Y] 标注中提取映射clean_points[]mappings[]patternre.compile(r\[来源:\s*([\d,\s])\])fori,pointinenumerate(key_points):mpattern.search(point)block_indices[]ifm:numsm.group(1).split(,)block_indices[int(n.strip())forninnumsifn.strip().isdigit()]clean_textpattern.sub(,point).strip()else:clean_textpoint.strip()clean_points.append(clean_text)ifblock_indices:mappings.append({point_index:i,block_indices:block_indices})returnclean_points,mappings局限性LLM标注不总是准确——有时会标注错误的块编号或者完全不标注。这就是为什么需要第二通道。4.3 通道2语义相似度匹配这是系统的核心——用 OpenAI Embedding 计算每个摘要要点与所有原文块的余弦相似度def_get_embeddings(texts:list[str],client:OpenAINone)-list[list[float]]:批量获取 OpenAI embedding带缓存ifclientisNone:client_get_client()uncached[(i,t)fori,tinenumerate(texts)iftnotin_embedding_cache]ifuncached:batch_texts[tfor_,tinuncached]forstartinrange(0,len(batch_texts),100):batchbatch_texts[start:start100]try:respclient.embeddings.create(inputbatch,modeltext-embedding-3-small)forj,emb_datainenumerate(resp.data):_embedding_cache[batch[j]]emb_data.embeddingexceptException:fortinbatch:_embedding_cache[t][]return[_embedding_cache.get(t,[])fortintexts]def_cosine_similarity(a:list[float],b:list[float])-float:余弦相似度 dot(a,b) / (||a|| * ||b||)ifnotaornotb:return0.0a_arrnp.array(a)b_arrnp.array(b)dotnp.dot(a_arr,b_arr)norm_anp.linalg.norm(a_arr)norm_bnp.linalg.norm(b_arr)ifnorm_a0ornorm_b0:return0.0returnfloat(dot/(norm_a*norm_b))关键参数模型text-embedding-3-small(1536维OpenAI最新嵌入模型)批处理每批100条避免API限流缓存dict级内存缓存避免重复请求语义匹配核心逻辑def_semantic_source_mapping(key_points:list[str],doc_id:str,client:OpenAINone,top_k:int3,threshold:float0.45,)-list[dict]: 语义溯源计算每个关键要素与原文块的 cosine similarity 取 similarity threshold 的 top_k 个块作为来源 blocksget_blocks_by_doc(doc_id)ifnotblocksornotkey_points:return[]block_texts[b[content]ifisinstance(b,dict)elseb.contentforbinblocks]# 批量获取所有 embeddings要点 原文块all_textskey_pointsblock_texts all_embeddings_get_embeddings(all_texts,client)point_embeddingsall_embeddings[:len(key_points)]block_embeddingsall_embeddings[len(key_points):]mappings[]fori,point_embinenumerate(point_embeddings):ifnotpoint_emb:continue# 计算与所有块的相似度similarities[(j,_cosine_similarity(point_emb,block_emb))forj,block_embinenumerate(block_embeddings)ifblock_emb]# 按相似度降序排列取 top_k 且 thresholdsimilarities.sort(keylambdax:x[1],reverseTrue)block_indices[idxforidx,siminsimilarities[:top_k]ifsimthreshold]ifblock_indices:mappings.append({point_index:i,block_indices:block_indices,scores:[round(sim,4)foridx,siminsimilarities[:len(block_indices)]],})returnmappings为什么 threshold0.45法律文本中摘要要点和原文块的语义不是完全重叠摘要是概括性表述经实测0.45能保证召回率的同时过滤明显不相关的段落太高(0.7)会漏掉间接引用的段落太低(0.3)会引入噪声4.4 双通道融合最后将两个通道的结果合并def_merge_source_mappings(llm_mappings:list[dict],semantic_mappings:list[dict])-list[dict]: 融合策略 1. 语义匹配结果为基础 2. LLM标注作为高置信度补充覆盖语义结果 3. 去重 上限5个来源/要点 4. 标记融合方法hybrid/llm/semantic result_map:dict[int,dict]{}# 先添加语义匹配的结果forminsemantic_mappings:pim[point_index]result_map[pi]{point_index:pi,block_indices:m[block_indices],method:semantic,}# LLM 标注覆盖语义匹配forminllm_mappings:pim[point_index]ifpiinresult_map:combinedlist(m[block_indices])foridxinresult_map[pi][block_indices]:ifidxnotincombined:combined.append(idx)result_map[pi]{point_index:pi,block_indices:combined[:5],method:hybrid,}else:result_map[pi]{point_index:pi,block_indices:m[block_indices],method:llm,}returnsorted(result_map.values(),keylambdax:x[point_index])五、前端交互实现5.1 Vue 组件关键代码SummaryPanel.vue— 溯源标签 点击事件:li v-for(point, i) in summary.key_points :keyi :class{ has-source: !!getSourceMappingForPoint(i) } clickhandlePointClick(i) {{ point }} span v-ifgetSourceMappingForPoint(i) classsource-tag :classgetSourceMethod(i) || {{ getSourceMethod(i) hybrid ? 混合溯源 : getSourceMethod(i) semantic ? 语义溯源 : 溯源 }} /span /liDocumentView.vue— 自动跳转 高亮动画:functionhandleLocateSource(blockIndices:number[]){// 1. 切换到原文块 tabactiveTab.valueblocks// 2. 高亮目标块highlightedBlocks.valuenewSet(blockIndices)// 3. 滚动到第一个目标块nextTick((){consteldocument.getElementById(block-${blockIndices[0]})el?.scrollIntoView({behavior:smooth,block:center})})// 4. 4秒后清除高亮setTimeout((){highlightedBlocks.value.clear()},4000)}5.2 高亮动画 CSS.highlighted{border-left:3px solidvar(--accent);background:var(--accent-light);animation:pulse 1.5s ease-in-out;}keyframespulse{0%, 100%{opacity:1;}50%{opacity:0.6;}}六、性能分析阶段时间开销说明文档分块编号10ms纯字符串操作LLM摘要生成3-8s取决于文档长度和模型Embedding计算0.3-1sAPI调用60 blocks一批余弦相似度矩阵10msNumPy向量运算融合1ms简单字典操作总增量延迟约0.5-1.5s相比纯LLM摘要七、常见问题Q1: 什么情况下语义通道优于LLM通道A: 当LLM忘记标注来源约30-40%的概率语义通道能兜底。当LLM标注了错误的块号语义通道的结果可以纠正。Q2: 阈值0.45如何调优A: 可以根据具体场景调整。学术论文类文档措辞更精确可提高到0.55合同类文档重复条款多可降低到0.35。Q3: Embedding缓存有多大A: 1536维 × 4字节 × 60块 ≈ 360KB/文档。内存级缓存重启清空。生产环境可改用Redis持久化。八、总结与优化方向本文贡献提出了一种无GPU、纯API的法律文档溯源方案双通道融合设计兼顾了精确度和工程可行性前端点击即溯源的交互大幅提升了用户信任未来优化本地Embedding模型— 使用 text2vec-large-chinese 替代 OpenAI API降低成本增量索引— 新增文档时无需重新计算所有EmbeddingNLI验证层— 对语义匹配结果再过一层NLI分类器精确度可进一步提升

更多文章