资讯动态

RAG+结构化抽取,终结合同盲区的智能合同审查实践

发布时间:2026/10/17 8:47:47 来源:尧图企业网站定制
“别只会闲聊让大模型是时候干点正事了。”这句话是我最近在某个跨平台系统项目里给某大型企业的法务团队做合同知识库原型时团队负责人反复挂在嘴边的口头语。当时项目组面对的是一摊让人头疼的扫描版合同扫描文件主合同、补充协议、附件条款、机打发票信息散落在不同文件夹里格式千奇百怪条款编号混乱。光靠人工去翻几百份合同翻下来眼睛都能看出重影。更麻烦的是很多风险藏在细节里——比如违约金的百分数写法不统一比如主合同和补充协议的管辖法院约定互相“打架”比如付款条件和发票开具时间对不上。这些肉眼很容易看漏、看错的地方我把它叫做“合同盲区”。我当时的判断是别指望大模型像人一样“读懂”合同也别指望它靠纯粹的记忆去回答。真正可落地的路子是把 RAG检索增强生成和 Structured Extraction结构化信息抽取组合起来前者负责“从大海里捞针”后者负责“把针摆到格子里”。这套组合拳打完标题里说的“终结合同盲区”实际上就是两件事第一把散落在长文档里的关键信息全部找出来第二把这些信息按字段、按约束、按规则校验过一遍输出成机器可读、人可复核的结构化结果。整篇我会把我在这个原型项目里踩过的坑、改过的方案、实测后觉得值得复用的细节全部拆开讲透。1. 内容整体设计与思路拆解1.1 先搞清楚“合同盲区”到底是什么在做技术方案之前必须先定义清楚问题。不同人口中的“合同盲区”不是同一样东西。我在这个项目里把盲区分成了三类每一类对应不同的技术难点。第一类是“被埋没的关键字段”。比如违约责任条款里写的是“每日万分之五”还是“每日百分之五”续签条款里写的是“提前30天书面通知”还是“提前3天电子邮件通知”。这类信息很具体但往往埋在一个几千字的段落中间人眼快速扫过时容易忽略数字和单位。大模型直接读取全文时也会被前后无关内容干扰。第二类是“多文档之间的矛盾”。主合同说“争议提交某仲裁委员会仲裁”补充协议却说“由某法院管辖”主体条款约定付款周期为30天附件里的付款计划却写了45天。这类问题单看一份文档很难发现必须把多份文件拉到同一个上下文里做交叉比对。第三类是“非结构化信息与业务数据的错位”。合同里的金额、日期、税点跟实际开票记录、付款回执能不能对上。这份工作需要把PDF里的一段文字跟数据库里的一行记录对齐纯靠向量检索做不到纯靠规则正则也做不到。所以整个方案不是“用一个RAG解决所有问题”而是“先用RAG解决定位再用结构化抽取解决字段抽取最后用规则和人工复核兜底”。想清楚了这一点后面所有设计都是围绕这个分层思路展开的。1.2 为什么纯聊天式RAG解决不了这个问题如果你用过通用RAG问答应该会有体验把合同文档切块、向量化建一个问答接口然后问“这份合同的违约金怎么约定”大多数时候能回答出个大概。但一旦问“把所有含‘除双方另有约定外’字样的条款全部列出来”或者“对比主合同和补充协议里争议解决条款是否一致”通用RAG就抓瞎了。原因在于通用RAG的定位是“我能找到跟问题相关的内容并组织语言回答”它不保证字段完整性不保证枚举准确性更不保证两个段落之间一致/不一致的判断逻辑稳定。聊天式RAG的输出是自然语言段落没法直接进业务系统、没法做约束校验、没法逐字段复核。我在实测中就遇到过同一个问题连续问三次大模型分别给出“违约金为万分之五”“违约金为5%”“未约定违约金”三种答案细查之下才知道原条款里原文写的是“按未付金额的万分之五”但因为切块边界把小数点弄丢了模型就只能猜。所以结构化抽取的作用不是替代RAG而是在RAG给出候选位置之后把“找候选”和“结构化读字段”这两件事分开。前者靠向量相似度召回后者靠精心设计的提示词和Schema约束逐字段提取。先让RAG把范围缩小到指定篇章再让抽取模型拿着“显微镜”去读准确率会高出不止一个量级。1.3 技术选型的取舍逻辑在方案选型上我做过几轮对比。最初想直接用纯大模型零样本抽取把所有合同丢给上下文窗口硬读。测试下来几页的短合同效果尚可超过20页的长合同模型就开始丢尾部字段还会把不同章节的同名字段串在一起。后来加了RAG做预筛但发现RAG本身对“段落边界”的理解会影响抽取效果——如果切块切得太碎一个完整条款被切成三段模型就可能漏读条款后半部分的百分比。最终落地路线是“先粗筛、再细读、后校验”文档解析层PDF/Word统一转成带页码和章节号的纯文本保留段落索引。切块与向量化按章节切块而不是固定字符切块这样能尽量保全条款语义完整性。检索增强定位针对“要抽取什么字段”构建检索Query用向量召回Top-K个相关块并带上章节上下文。结构化抽取把召回结果交给抽取模型按预定义Schema输出JSON字段缺失时输出null并附上原因。规则校验层用正则、日期解析、枚举映射对JSON字段做二次校验标记风险项。这套设计把“语义理解”和“硬规则校验”分开模型只负责它擅长的语义归纳不负责它不擅长的确定性计算。比如金额字段抽取模型负责从文本中圈出“人民币壹佰贰拾叁万圆整”规则校验负责把它转成1230000.00并核对大小写是否一致。2. 核心细节解析与实操要点2.1 文档解析的预处理细节决定了RAG的上限很多人上手就把PDF转成文本丢给向量库这是最容易踩坑的一步。扫描版PDF转出来的不是文本是图片不先做OCR后面全白搭。就算是电子版PDF直接抽取文本也常常丢失表格结构、页眉页脚、多栏排版顺序。我在项目里用的是多层回退策略优先取PDF内嵌文本层若文本层的字数密度低于阈值就触发OCR引擎做整页识别再把识别结果按坐标还原为段落。Word文档单独处理因为docx里的表格和文本框经常在纯文本转换时被丢到文档末尾需要根据文档XML结构里的readingOrder顺序重排。处理完成后每一段文本我都记录了来源信息和页码{doc_id: CT-2024-001, chap_no: 3, par_no: 17, page: 12, title: 违约责任, text: 除双方另有约定外任何一方迟延履行的每迟延一日应按未付金额的万分之五向守约方支付违约金...}这个“带位置的元数据”在后面的检索和抽取里非常重要。抽取出一个字段时能把“哪一份文件、第几章、第几节、第几页”一并带出来这才叫可以复核的结果而不是凭空冒出来的一句话。提示切块策略务必让每块内部尽量只包含一个完整条款或小节宁可单块短一点也不要跨多个不同主题。固定字符数切块会导致模型在抽取时被无关内容干扰实测中Top-1召回命中率下降约15个百分点。2.2 RAG检索Query的设计不要只用一句话当我拿“违约金比例是多少”去向量检索时检索效果往往一般。原因很简单向量模型在编码短Query和长文档时存在语义鸿沟用户口语化的问法跟合同书面语表达差别太大。我采用了一个“伪文档”策略对每个需要抽取的字段构造三类检索Query合并召回结果再去重。以“违约金比例”为例条款问法违约金 比例 百分比 计算方式当事人问法乙方逾期 未付金额 按日 支付 违约金同义扩展迟延履行 罚则 滞纳金 违约成本把三类Query的召回结果取并集再按相关性得分排序。实测下来同样的向量库这个策略能把关键相关块的召回率从78%拉到93%左右。代价是检索次数多一点但对于离线合同分析任务完全可以接受。顺带说一句索引字段里保留“章节标题”文本查“违约责任”时可以直接利用条目级别的匹配做预过滤把范围限定在违约责任章节再走向量召回。词法粗筛加向量细筛的组合比单独用向量更稳。2.3 Structured Extraction的核心是Schema设计结构化抽取这一层决定质量的不是模型而是Schema定义和提示词约束。Schema要稳定每个字段得包含“类型、描述、示例、取值约束、抽取来源”。我做了一个通用合同要素Schema节选如下{ contract_basic: { contract_name: {type: string, desc: 合同名称, source: 封面或首段}, party_a: {type: string, desc: 甲方全称}, party_b: {type: string, desc: 乙方全称}, sign_date: {type: date, format: YYYY-MM-DD, desc: 签订日期} }, payment_terms: { currency: {type: string, enum: [CNY, USD, EUR]}, total_amount: {type: number, desc: 合同总金额数字格式}, advance_ratio: {type: number, desc: 预付款比例}, settlement_days: {type: integer, desc: 结算周期天数} }, risk_clauses: { liquidated_damages: {type: object, attr: {rate: number, unit: string, trigger: string}}, jurisdiction: {type: object, attr: {authority: string, location: string}} } }Schema设计有几个关键点都是实操里总结出来的尽量把所有金额字段明确为“数字类型”并同时要求输出“原始文本原文”。因为大模型在处理“百分之五”和“5%”时的数值理解有波动保留原文供人工核对数字转换交给规则层。日期字段必须带“解析格式”和“未知值策略”遇到“以实际到账日为准”这种非固定日期必须允许输出描述性文本不要用null糊弄过去。枚举字段务必给出完整可选项比如币种只允许CNY/USD/EUR模型就不会凭空造出一串“RMB”。这不算限制自由度而是把模型输出收敛到业务需要的口径里。抽取提示词里除了Schema我还会要求模型在输出JSON之外同时输出一个“依据原文片段列表”每个字段值后面都要附上一段原文引用并且标明引用来自哪个文档哪一章。这样即使模型抽取错了复核人员顺着引用就能找到原文位置而不是面对一个孤零零的错误值。2.4 多文档一致性比对的实现思路“终结合同盲区”这个目标里最关键也最难做扎实的是多文档比对。主合同和补充协议之间的矛盾其实是这个项目里客户最在意的点。我的做法是在RAG索引上按“合同文档组”维度做检索一个组里包含主合同、附件、补充协议。比对逻辑拆成三步第一步抽取单文档结构化字段得到每个文档自己的“合同要素表”。第二步按字段Key做跨文档水平对齐同一个Key比如“争议解决方式”出现在不同文档里就各自取值。第三步把同一Key的不同取值交给规则与模型联合判断完全一致标记“一致”取值不同但语义兼容标记“需人工确认”取值冲突标记“高风险冲突”。这个第三步我建议以硬规则为优先。比如说管辖法院一方写“某仲裁委”另一方写“某人民法院”这属于枚举值不同直接标记冲突不需要让模型“解释”。只有字段值是自由文本、语义对等比较困难时才交给模型判断比如“甲方某技术有限公司”和“甲方某技术股份公司”这种差异模型可以给出相似度判断再由规则决定是否放行。3. 实操过程与核心环节实现3.1 环境和数据准备这个原型在我本地跑通使用方是一台普通开发机配置不算高。技术栈选的是轻量组合PDF处理用解析库向量库用支持HNSW的内存数据库LLM接口用通用API语言用Python。为了完全复现我把目录结构列在这里contract_ai/ ├── docs/ # 原始合同PDF/Word ├── parsed/ # 预处理后的文本JSON ├── chunks/ # 切块结果 ├── index/ # 向量库持久化目录 ├── schema/ # 抽取Schema定义 ├── scripts/ │ ├── parse_docs.py # 文档解析 │ ├── chunk_and_index.py # 切块与向量化 │ ├── retrieve.py # RAG检索 │ ├── extract.py # 结构化抽取 │ └── validate.py # 规则校验与报告准备测试数据时我并没有用真实客户合同而是用了某公司内部的模拟合同集X12份典型合同覆盖采购、服务、租赁、建设施工几种常见类型并手动埋了8处矛盾点。这样做的好处是能精确评估系统到底找出了几处而不是拿真实数据“凭感觉说效果好”。3.2 文档解析与切块的完整操作解析脚本里最核心的一步是对段落做章节归类。我用一个小技巧先利用文档结构特征将包含“第一章/第一条/1.1”等模式的行识别为标题行再用正则和行距特征把后续段落归到最近的标题节点下。这样生成的结构体天然具有层级node {h: 第三章 违约责任, children: [ {para: 3.1 违约金, text: ...}, {para: 3.2 免责条款, text: ...} ]}切块时直接基于这个树递归每一块记录完整路径而不是简单的字符截断。切块的大小我控制在1000个中文字符左右最长不超过1500。太短则语义不全太长则向量精度下降。chunk_and_index.py里加了过滤逻辑页面页眉页脚、纯页码行、重复的抬头logo文本都先识别并丢弃。这一步非常关键否则向量库里会塞满“某某公司合同模板”这类无意义的高频文本检索时疯狂干扰相关性打分。3.3 RAG检索与候选召回的实测记录检索的代码我单独抽出retrieve.py来跑。为了让效果直观我写了一批测试Query比如Q1: 违约金按未付金额的多少比例计算 Q2: 合同总金额是多少预付款比例 Q3: 主合同和补充协议对管辖法院的约定是否一致 Q4: 合同续签需要提前多少天通知针对Q1单个Query召回的Top-5块里包含“万分之五”的块排在第4位差点没进上下文窗口。用了“三类伪文档”策略后同一块排到了第1位。这个体验很直观不是模型不行是检索方式没喂饱它。我把检索后拼给抽取模型的上下文格式做成如下样式[Doc:CT-2024-001 | Chap:3 | Page:12] 除双方另有约定外任何一方迟延履行的... [Doc:CT-2024-002 | Chap:5 | Page:8] 在第3.1条约定的付款期限内...这种“带文档标识章节标识”的上下文对抽取模型识别“现在读的是哪份文件”有奇效。特别是多文档比对场景如果不带标识模型容易把两份文件的条款内容混在一起抽取结果张冠李戴。3.4 结构化抽取与JSON输出抽取脚本的核心是提示词工程和JSON解析兜底。我用了两层JSON解析第一层让模型直接返回JSON若解析失败则要求模型把JSON放在特定标记内再用解析器剥离标记。不要迷信任何单一提示词格式实测中不同模型对JSON的返回偏好不同。这里贴一个核心抽取逻辑的片段SCHEMA_STR json.dumps(contract_schema, ensure_asciiFalse, indent2) prompt f 请阅读以下合同相关文本片段按Schema抽取信息。 规则 1. 只依据给定文本抽取不要自行推断。 2. 每个字段输出时附带source_ref原文引用和doc_tag。 3. 数值字段输出数字同时保留raw_text。 4. 无法抽取时输出null并在reason字段写明原因。 Schema: {SCHEMA_STR} 合同文本片段: {context} 输出JSON: 生成JSON后我会用规则层做类型校验。比如日期字段检查是否可解析、金额字段检查raw_text里的中文字符与阿拉伯数字是否对得上、枚举字段检查取值是否在enum列表内。校验不通过的直接标记为“需人工复核”而不是悄悄丢字段。3.5 合同盲区报告的生成逻辑最后一步把所有抽取结果、校验结果、比对结果汇聚成一份“合同盲区报告”。报告的格式设计成了一个表格机器和人都能快速定位问题风险类型文档/章节字段抽取值校验结论冲突CT-2024-001 第8条 / CT-2024-002 第2条争议解决某仲裁委 / 某法院冲突格式异常CT-2024-003 第4条违约金比例5% / 万分之五疑似单位不一致信息缺失CT-2024-005 第7条续签通知期限未约定缺失语义兼容CT-2024-001 第3条 / CT-2024-002 第1条付款方式银行转账 / 转账一致报告同时导出JSON和CSV两个版本。JSON给下游系统做自动化对接CSV给法务同事做人工复核。这一步虽然朴素但价值很大——之前客户看技术方案时总觉得“是个黑盒”看到输出表里每一条风险都带文档位置和原文引用信任度立刻上来了。4. 常见问题与排查技巧实录4.1 召回不到关键条款怎么办这是RAG类项目里最经常被问的问题。排查顺序我建议按下面几层来先看切块质量。如果切块里混入大量页眉页脚、目录页码先清洗再做向量化。再看Query构造。单一句子不行就用3.2节里的伪文档策略把“人话问法”和“条款问法”都召一遍。接着看向量模型和领域适配度。通用向量模型在合同专用术语上表现一般条件允许可以收集一批合同文本做领域微调或者直接用更偏向法律语料的向量模型。实测换用领域微调模型后Top-5命中率又提高了约8%。最后还可以加一层关键词过滤保底对“违约金、管辖、续签、付款”这类强关键词做BM25词法检索与向量结果取并集。这个方法土但在Recall不足时非常管用。4.2 模型抽取JSON格式不稳定怎么办不同模型的JSON稳定性差异很大。我的处理方案是“三步走”提示词里给出明确的JSON Schema并附带一个正确的示例。要求模型只输出JSON不输出前后缀。解析失败时用正则提取第一个{到最后一个}之间的内容再做二次解析。如果仍然失败就把这条记录划入“抽取异常队列”等待人工处理而不是反复重试浪费时间和成本。从工程角度说与其跟模型较劲不如接受一定的失败率并做好兜底。4.3 多文档比对时“误报”和“漏报”的平衡比对逻辑里硬规则冲突仲裁vs法院几乎没有误报但“语义兼容”判断容易出问题。比如一份合同写“银行转账”另一份写“电汇”这算不算冲突模型判“一致”但业务上可能觉得“电汇和转账还不是一回事吗”这时候无需执拗于技术判断我给系统设计了一个“决策阈值”参数相似度高于0.85自动标为一致相似度在0.65到0.85之间标为“需人工确认”相似度低于0.65自动标为冲突把阈值做成可配置项让业务方根据自己风格的严苛程度调整。法务团队担心漏掉风险可以把阈值调高合同量太大需要快速过筛可以把阈值调低。这种灵活性比一味追求模型自动判断更实用。4.4 关于大模型幻觉的控制心得所有做抽取的人都会遇到幻觉。我控制幻觉的三个原则是给定位、限来源、留证据。给定位是指抽取前通过RAG把候选范围缩小绝不把整个合同库全都丢给模型。限来源是指在提示词里反复强调“只依据给定文本片段抽取”并让模型输出抽取来源。留证据是指每个字段值都要求同时输出原文引用以便复核。单靠提示词不能根治幻觉但通过“缩小范围来源引用人工复核”三重保障至少能把风险控制到业务可以接受的范围内。5. 实测效果复盘与个人经验沉淀在模拟合同集上最终跑出来的结果让我愿意把方案拿出去见人12份合同里埋的8处矛盾点系统完整找出了7处另外1处是因为补充协议中的日期写法是“二零二四年三月”主合同是“2024年3月”模型没有把中文数字日期转成同一格式被我自己的规则层误标为“需人工确认”。加了一条中文日期归一化规则之后这一处也顺利识别出来了。字段级抽取的准确率在测试集上做了人工复核合同基本信息、金额、日期、违约金比例等核心字段总体准确率接近95%。缺失字段也能正确输出null而不是瞎编一个值。多文档一致性比对的误报率低于5%主要误报集中在“语义兼容但表达不同”的边缘案例上。我的体会是这类项目真正决定成败的不是模型选型而是数据链路的完整度文档解析干不干净、切块规则合不合理、Schema定得细不细、校验规则跟不跟得上业务口径。每一个环节单独挑出来都不复杂但串起来就形成了一套可以稳定交付的合同分析能力。尤其要把大模型的输出当成“半成品”规则校验那一道工序才是最终质量的守门员。最后再分享一个操作层面的经验别一上来就想把整套自动化做完。先挑一个最容易见效的场景比如“争议解决条款的多文档一致性比对”跑通闭环让业务同事看到输出报告的价值再逐步扩展字段和合同类型。需求这种东西永远是做着做着自己变清晰的。只要数据链路搭得稳后面加字段、加规则、加文档类型只是水到渠成的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑