尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

自研合同审查引擎:规则引擎与大模型协同的工程实践

自研合同审查引擎:规则引擎与大模型协同的工程实践 把合同审查从“人肉活”变成“流水线”这个念头在我维护合同库的第12天彻底扎了根。当时面对几百份待审合同条款比对、风险标记、版本核对全靠手动错漏几乎是必然的。所以当“合同审查引擎”这个方向定下来时我心里很清楚这不是写几个规则脚本糊弄过去的事而是要做一个能真正理解合同结构、能跑出审查结论的工程化系统。这篇Day12的分享就把我在这套引擎开发里踩过的技术坑、验证过的方案、以及最终落地的那套架构原原本本讲清楚。如果你也打算自研一套合同审查工具或者正在纠结规则引擎和大模型到底怎么分工这篇应该能给你省下不少弯路。1. 整体设计思路拆解为什么非要自研一套审查引擎1.1 合同审查的核心需求到底是什么很多人一提合同审查第一反应就是“用AI读合同”。但真正接触业务后你会发现一份合同要审明白至少涉及三个层次的工作第一层是结构化检查比如必备条款是否齐全、金额大小写是否一致、付款期限和违约责任有没有约定这些是确定性规则逻辑严格、可枚举。第二层是一致性比对比如把待审合同和公司模板做逐条差异分析看业务方私自改了哪些条款这些改动是否经过授权。第三层是语义风险识别比如“全额预付”“单方解除权”“自动续约且不可撤销”这类表述语法上没毛病但站在公司立场就是明显的风险点必须靠语义理解才能抓出来。如果只用正则表达式做第一层那叫“关键词扫描器”不叫“审查引擎”。如果只靠大模型做第三层那又容易产生幻觉经常把没问题的条款标红。真正可用的引擎必须把这三层能力按合理的权重组合起来各管一段。1.2 方案选型考量规则引擎与LLM如何分工我在初期调研时市面上确实有现成的合同审查API和开源项目但接入之后发现几个硬伤。首先是定制化能力不足。每家公司的合同模板、审查要点、风险偏好都不一样通用API返回的风险点很多是“法官视角”而不是“公司法务视角”。比如竞业限制条款有的公司希望严格禁止有的公司只要求限制高管层级这类业务规则差异外购系统根本没法通过配置满足。其次是数据安全问题。合同涉及商业机密、履约细节、合作方信息直接调用公有云大模型接口法务部门那一关就过不去。本地化部署和私有化模型是硬指标这条直接把很多SaaS方案排除了。最终我选定的技术路线是以规则引擎为骨架以本地化部署的LLM为大脑中间用标准化的提示词管道连接。规则引擎负责确定性强的检查项LLM负责语义风险识别和条款偏离度分析。两边并行跑最后统一聚合结果。这个路线的核心思路是让规则引擎兜底保证“确定的问题一定报出来”让大模型提升覆盖率把“潜在的问题尽量报出来”。两者不是替代关系而是配合关系。1.3 系统整体架构与技术栈选择整条审查链路我拆成了5个模块文档解析层、规则引擎层、语义审查层、结果聚合层、前端可视化层。技术栈方面后端我用的是Java 17 Spring Boot 3.x主要考虑到文档解析库在Java生态里最成熟POI、docx4j、PDFBox都是经过大量项目验证的。规则引擎没有引入Drools而是自己写了一套轻量级规则配置器原因后面细说。数据库用了PostgreSQL存储合同原文、规则配置、审查记录。Elasticsearch用来做条款全文检索方便后续建立条款库。LLM服务通过ONNX Runtime部署了量化后的开源模型实现完全离线推理。前端用的是Vue3 Element Plus核心页面就两块审查结果总览和条款级风险明细。听起来简单实际做起来这块反而是前端工作量最大的部分因为合同原文和审查结果要双向联动定位。2. 规则引擎实现轻量级方案比重型框架更实用2.1 为什么没选Drools这类重型规则引擎Drools是Java领域最出名的规则引擎功能强大支持复杂的规则流、决策表、CEP事件处理。但真上手做合同审查后我发现它有点“杀鸡用牛刀”而且重量级框架带来的问题远多于收益。首先是配置成本高。Drools的DRL语法需要单独学习业务人员根本看不懂法务部门想调整一条审查规则不可能每次拉上开发改代码。其次是部署体积和启动速度都不占优势而合同审查引擎需要频繁更新规则不可能每次更新都走完整发布流程。所以我自己实现了一套基于JSON配置的轻量级规则系统规则本质上就是“条件动作”的结构{ ruleId: R-001, ruleName: 付款条款缺少违约金约定, scope: payment_terms, conditions: [ { field: has_penalty_clause, operator: equals, value: false } ], action: { level: warning, message: 付款条款未约定逾期付款违约金建议增加每日万分之五的违约金条款, suggestion: 模板条款编号: T-PAY-003 } }这套配置的好处是规则的增删改可以直接做成管理后台界面法务人员通过下拉框配置条件不需要碰代码。规则的IO、存储、热加载全部自己做数据量级在几百条规则内性能完全不是问题。2.2 条款库管理与匹配算法合同审查规则的核心依赖是条款库把公司所有标准条款按类型、用途、风险等级打上标签规则引擎才能在此基础上做比对。条款库结构我设计成四层分类合同类型采购、销售、劳务、保密、条款类型付款、违约、保密、解除、管辖、条款要素粒度是否含违约金比例、是否含争议解决方式、标准模板正文。匹配算法这部分最开始我用的是关键词 正则后来发现命中率太差。比如“甲方逾期付款的每逾期一日按应付未付金额的万分之三向乙方支付违约金”和“迟延履行付款义务的违约方应每天支付未付金额的0.03%作为违约金”语义一样字面完全不同。后来我把匹配升级成了两步先用ES做BM25召回候选条款再用文本向量相似度做精排。向量模型用的是本地部署的bge-base-zh-v1.5效果很稳同义句召回率从原来的不到60%提升到了90%以上。2.3 为什么说Word解析比规则本身更考验功底这套系统开发到一半我才真正意识到最大的坑不在规则引擎而在文档解析。合同大多是Word格式但Word内部的复杂性远超想象。一个看似简单的docx文件里面可能是标准段落、嵌套表格、页眉页脚、批注、修订模式、文本框、嵌入式对象的大杂烩。如果只是用POI按段落提取文本遇到表格内容就会乱序审查结果自然不准。我处理方案是优先用docx4j按文档对象模型(DOM)遍历把段落、表格、批注分别映射成结构化的文档节点再按视觉阅读顺序重排。这段代码是整个项目的工程量黑洞陆续写了接近2000行反复处理了表格嵌套、跨页段落、修订痕迹合并等一堆边角问题。这里给大家一个非常关键的提示合同解析时不要只抽文本一定要保留段落编号和坐标信息。否则前端联动定位功能做不出来用户点击风险项时没法定位到合同原文的具体位置。3. 智能审查模块LLM本地部署与提示词工程实战3.1 LLM选型与本地化部署的取舍大模型选型是争议最大的一块。最初我用的是公有云API识别效果确实好但法务部门对数据出域完全不能接受。后来转向本地部署先试了ChatGLM系列再试了Qwen系列最后定的是Qwen2.5-14B-Instruct的4bit量化版。选型逻辑有三个硬性指标第一是中文理解能力必须过硬合同里的长难句、否定句、双层嵌套约定很多模型能力不够就容易抓错重点第二是推理延迟要在可接受范围内一张A100显卡上14B量化模型单次审查大约3到5秒完全能接受第三是输出格式可控这个依赖后续的提示词设计。本地部署的技术栈用的是vLLM做张量并行和连续批处理吞吐量比原生Transformers推理高很多。模型加载后开了8个并发worker压测下来单份2万字合同从解析到出结果全链路大约20秒这个性能已经达到业务可用水平。关于量化我的经验是4bit AWQ量化对最终审查准确率的影响非常小但显存占用直接砍一半可以省下不少部署成本。如果机器资源充裕可以考虑6bit量化效果更稳但没有质的差别。3.2 提示词管道单次调用还是多轮拆分LLM直接读整份合同出审查结论这是第一个想到的方案也是最快翻车的方案。2万字的合同直接塞给模型上下文窗口虽然够但模型会“迷失在中间”开头结尾的内容关注度明显高于中间段落审查结果严重不均匀。而且一次性输出所有风险点格式杂乱解析困难经常出现漏项。我最终采用的方法是“分件审查 聚合汇总”两段式管道。第一阶段是按条款类型拆分审查维度比如付款条款、违约责任条款、知识产权条款、保密条款每个维度单独发起一次模型调用。每次调用输入的内容包括审查规则说明、条款原文、审查要求。第二阶段是把第一阶段的审查结果聚合统一格式后返回给规则引擎合并。提示词模板的核心设计是这样的你是一位经验丰富的合同审查专员现在需要审查以下{条款类型}。 审查要求 1. 识别条款中是否存在以下风险{风险类型列表} 2. 对每个风险输出风险等级high/medium/low 3. 对每个风险输出风险描述、具体问题原文引用、修改建议 4. 严格按JSON格式输出不要输出其他内容 条款原文 {条款原文} 输出格式 {risks: [{level: ..., desc: ..., quote: ..., suggestion: ...}]}用这种结构化输出的好处是解析稳定不会出现模型自由发挥导致后端无法处理的情况。而且模板里明确要求“不要输出其他内容”实测能把无效输出的比例压到1%以下。3.3 规则引擎与LLM的结果如何合并去重规则引擎和LLM各自跑完就会产生同一漏洞被两边同时命中的情况。比如合同里没有约定违约责任规则引擎会报警LLM也会报警如果直接累加展示用户看到的是一堆重复信息。合并策略我做了三层处理。第一层是对齐统一风险分类标准规则引擎输出的风险类型和LLM输出的风险类型必须使用同一套枚举值比如“missing_penalty_clause”“ambiguous_termination_right”这样两边才能按分类做匹配。第二层是置信度加权。规则引擎命中的结果确定性高置信度直接设为1.0LLM命中的结果根据模型输出概率和规则命中情况综合打分。两边命中同一条时保留规则引擎的结论LLM的补充描述作为增强信息拼接上。第三层是展示去重前端拿到合并后的数据按条款ID做分组同一条款下的风险项整合到同一张卡片里展示用户不用来回跳转。4. 文档解析与前端交互细节决定审查体验4.1 不同文件格式的解析方案合同文件除了Word还经常有PDF和扫描件。我在这块踩了不少坑简单分享三种格式的处理重点。Worddocx用docx4j按DOM解析保留段落、表格、批注的层级关系。doc格式是老古董POI的HWPF支持有限我的方案是先调用LibreOffice无头模式批量转换为docx再解析。PDF分两种文字型PDF用PDFBox抽取文本和坐标优点是速度快、内容准扫描型PDF则依赖OCR我用的PaddleOCR设置的是中文英文识别模型识别准确率在清晰扫描件上能达到95%以上。4.2 表格提取合同审查里最容易翻车的地方合同里的付款计划、价格明细、交付里程碑大量信息都放在表格里。表格解析的难点在于表格可能跨页、可能有合并单元格、可能嵌套在文本框中。POI对Word表格的原生解析API非常难用尤其是跨页表格多个表格片段在物理上是独立的逻辑上却是同一个表。我写了一个表格合并算法通过比对表头行内容和列数量把连续的同结构表格片段拼回完整表格。表格数据提取后还要做一次语义转换。比如“付款里程碑”表格里的“预付款”“到货款”“验收款”这些列名要和规则引擎里的字段映射起来。映射规则可以简单枚举但遇到不同业务方用了不同列名时就得靠bge向量模型做相似度匹配了。4.3 前端实时审查回传与风险定位联动审查引擎跑完一份合同通常要10到20秒如果让用户干等体验会很差。我的做法是改成任务式审查前端提交合同后立刻返回任务ID后端在审查过程中分批推送进度事件前端用WebSocket监听并动态刷新进度。进度事件分三阶段解析完成30%、规则审查完成60%、语义审查完成90%、全部完成100%。这样用户能直观看到引擎正在干什么而不是面对一个转圈图标。风险定位联动是整个前端交互的核心亮点。审查结果里每一条风险项都有一个anchorId对应解析阶段记录的文档节点唯一标识。点击风险项时前端根据anchorId找到对应的文档节点滚动定位并高亮显示风险所在的条款原文。实现要点是审查结果必须在解析阶段就绑定好锚点。规则引擎和LLM返回的quote都要在文档节点上做一次“原文定位”操作找到最匹配的段落或表格行。这个匹配我用的是编辑距离算法允许一定的字符偏差因为OCR或模型输出可能会有细微文字差异。5. 常见问题与排查技巧实录5.1 高频问题速查表开发过程中遇到的问题可以整理成一张速查表后面再遇到类似场景可以快速定位。问题现象可能原因解决方案审查结果里表格内容乱序表格与段落混合解析顺序错乱改用docx4j DOM遍历按阅读顺序重排节点LLM输出JSON解析失败模型输出夹带解释性文字提示词强制“只输出JSON”后端增加容错解析规则命中率异常偏低合同名称不规范规则scope匹配失败增加同义词映射用向量召回替代精确匹配风险定位跳转到错误段落anchorId偏移文档节点编号不稳定解析阶段冻结节点编号审查阶段用原文坐标双重定位PDF扫描件识别率差扫描清晰度低、表格线干扰预处理做降噪和二值化识别模型换高精度版长合同审查超时单条款文本过长LLM推理延迟高增加文本截断策略分块审查后合并结果5.2 独家避坑合同文本归一化的三个细节文本归一化是影响整体效果最隐蔽的坑。表面上合同文本看起来没问题但一进算法各种变体就会让匹配全部失效。第一个坑是全角半角混用。合同里经常出现全角括号、全角逗号、冒号中英文混排直接做字符串匹配必挂。我的做法是统一转成半角同时保留原文本用于展示。第二个坑是数字表达不统一。“壹佰万元整”“100万元”“1000000元”“1,000,000元”在合同里都很常见规则引擎做金额一致性校验前必须先写一个金额解析器把所有表达统一转成数值。第三个坑是条款编号不规范。有的合同用“第一条”有的用“1.”还有的用“1.1.1”提取条款结构时很容易漏。我写了一套层级编号识别器同时匹配中文序号和阿拉伯数字序号再按缩进层级补全父子关系。5.3 性能瓶颈与优化实践整套系统上线后遇到最大的性能瓶颈不是LLM推理而是文档解析。docx4j解析一份2万字的合同耗时大约2到3秒看起来不慢但加上PDF解析和OCR并行处理高峰期堆积的任务还是会撑满线程池。我做了两个优化一是文档解析阶段改成并行流水线Word和PDF同时跑OCR单独走GPU队列二是准备一份解析缓存相同哈希值的合同在24小时内复用解析结果避免重复解析。LLM推理这块也有一个优化点同一份合同在调整规则后需要重新审查时不需要重跑LLM直接复用上一次的语义审查结果只把规则引擎部分重跑一遍。因为规则调整不影响阶段一的LLM输出这个优化能把大部分二次审查耗时从20秒压缩到3秒以内。6. 实测效果与后续扩展空间6.1 试运行结果规则命中率与人工复核一致率系统上线试运行后我拿历史审结的50份合同做了回测对比。规则引擎LLM的组合方案对已知风险点的命中率是92%比纯规则方案提高了18个百分点比纯LLM方案提高了7个百分点。人工复核一致率方面系统标记的“高风险”项中法务确认属实的有86%这个数字在辅助审查场景已经具备实用价值。当然还有一些问题没有完全解决。比如某些复杂的多重嵌套保证担保条款模型理解起来还是很吃力部分业务方刻意的表述模糊化比如用“适当补偿”代替具体金额策略型规避行为系统识别率偏低。这些问题我目前的处理方式是识别不了就标记为“需人工关注”至少提醒法务重点查看。6.2 后续演进从“审查”到“谈判辅助”的扩展想法按照目前的技术底座后续可以做很多扩展。我比较看好的方向一是反向生成修改建议稿结合模板库对风险条款做自动替换出具“修改版合同”二是风险统计报表按业务方、合同类型、条款类型做多维统计分析反哺公司合同模板迭代三是把规则配置器开放给法务让业务侧直接维护审查策略真正实现审查引擎的“业务自驱”。还有一个我很想尝试的方向是把这套引擎接入合同生命周期管理从起草、审查、签署、履约到归档全程自动截取审查点而不只是做一个事后的审查工具。6.3 一些来自实操的个人体会做完这个项目我最大的感受是合同审查引擎这类系统真正的技术难点从来不是“AI读合同”这个炫酷的外壳而是数据清洗、结构化解析、规则与模型协同这些藏在底层的脏活累活。模型选型、提示词优化、规则编写这些网上教程很多方法论也相对成熟。但docx表格跨页解析、金额表达归一化、前后端锚点定位这些细节才是决定系统好不好用的关键。每一个都要大量的真实数据驱动迭代没有任何捷径可走。如果让我给准备做同类系统的人三条建议我会说第一文档解析模块一定要预留足够多的排期它是所有上层能力的根基第二规则引擎坚持下去不要被“大模型能解决一切”带偏确定性问题让规则做效果和速度都更可靠第三从第一天就设计好锚点定位机制否则后面做风险联动、数据回流、二次审查时会发现前期数据结构设计根本没有留出扩展口。
返回列表