从零构建合同智能审查Agent:架构设计、代码实现与生产落地
发布时间:2026/10/2 4:57:39来源:尧图网络
1. 合同智能审查 Agent 是什么为什么值得动手做1.1 先搞清楚 Agent 和普通“Prompt 大模型”的区别这两年“Agent”这个词被炒得厉害很多朋友跑来问我我写一个 Prompt把合同贴进去让大模型给意见是不是就是 Agent我的回答是那只是“大模型问答”离 Agent 还差得远。Agent 的核心特征是“能自己规划步骤、调用工具、读取中间结果、再决定下一步做什么”。放到合同智能审查这个场景里最直观的差别是普通 Prompt 是“一次性把所有活都丢给模型”模型可能看一遍就给你一段泛泛的总结而 Agent 会先拆任务——先抽取合同主体信息再逐条核对付款条款、违约责任、争议解决条款发现金额大小写不一致就去调一个专门的大小写比对工具拿不准某个法律概念时还可以去检索条款库。整个过程是“边干边看”而不是“一次拍脑袋”。说白了普通 Prompt 是让一个很聪明但很懒的实习生直接交报告Agent 是给这个实习生配上检查清单、计算器、法条库并且每一步都要求他留下依据。合同审查本身就是一个强流程、强规则、强校验的领域天生就适合 Agent 这种“可拆解、可追溯、可干预”的玩法。1.2 合同审查场景为什么特别适合 Agent 化我做合同相关系统前前后后也有七八年了早期大家喜欢做“合同管理 OA”核心是审批流和归档文本本身的审查基本靠人肉。后来有团队尝试用正则表达式硬扫合同比如匹配“违约金”后面跟的数字匹配“争议解决”几个字确实能解决一部分机械问题但一旦遇到同义词、倒装句、表格化条款正则就崩了。大模型出来后很多人以为终于解放了结果发现直接丢给大模型也有三个问题第一合同动辄几千上万字上下文一长模型就开始“丢三落四”前面说甲方后面理解成乙方第二模型输出不稳定同一个条款问两次一次说有风险一次说没风险第三审查意见没有依据模型说“可能存在风险”却不说清是第几条哪句话法务根本不敢信。Agent 化正好能把这三件事分开解决文字理解交给模型规则校验交给代码条款定位交给检索工具最后再由模型汇总成结构化报告。每一环都有明确边界出了问题能定位、能修复而不是像纯 Prompt 那样只能靠“换个 Prompt 再试一次”。这也是我为什么强烈建议做合同审查的朋友直接奔 Agent 架构去而不是继续堆提示词。1.3 适合谁参考、能解决什么问题这篇实战内容适合三类人一是公司的法务或合规人员想用技术手段降低合同初审工作量二是做企业服务软件的产品经理或研发准备把合同审查做成一个可交付的功能模块三是刚开始接触 Agent 开发的工程师想找一个业务逻辑清楚、数据敏感度低的练手项目。合同文本不像医疗数据那么敏感用脱敏后的模拟合同完全可以把整套流程跑通。这套方案最终能交付的东西是一个能自动完成“合同解析、风险识别、条款抽取、报告生成”的审查 Agent。它不替代律师做最终判断但能把审一份合同从半小时压缩到两三分钟把那些“漏看一条违约责任”的低级错误降到最低。接下来我会从方案设计、代码实现、提示词细节到生产落地完整走一遍。2. 整体方案设计与审查流程拆解2.1 先把需求拆成原子能力很多人一上来就问我用哪个 Agent 框架我的回答永远是先把需求拆成原子能力再谈技术选型。合同智能审查看起来是一个需求拆开之后大概是下面几块文本接入支持上传 Word、PDF或者直接粘贴文本能从中抽取干净的正文内容。基础解析识别合同标题、甲乙双方、合同金额、签署日期、合同期限等结构化字段。条款定位能找到“付款”“违约责任”“保密”“知识产权”“争议解决”等关键条款所在的原文位置。规则校验对金额一致性、日期逻辑、前后称呼一致性、必填项缺失这类“硬规则”做确定性检查。语义审查对条款是否公平、是否完整、是否存在常见风险点做大模型判断。报告生成把规则校验和语义审查的结果合并输出带原文引用、风险等级、修改建议的结构化报告。这六块其实就是 Agent 的“工具集”。设计阶段不急着写代码先把这些能力列出来后面每一步都是往这些格子里填东西。你会发现真正需要大模型的地方并不多大部分工作其实是规则和流程。2.2 方案选型自研编排 vs Agent 框架现在市面上有 LangChain、LlamaIndex、Dify 这类框架也有各大模型厂商自带的 Agent 能力选型很容易让人纠结。我的建议是分情况如果你只是想快速验证业务逻辑可以用现成框架的 Agent 编排能力但如果是要做生产级合同审查我更推荐“轻框架 自研调度”。原因很实在。合同审查业务的特点是流程稳定、工具明确、输出格式需要严格约束不太需要那种“让 Agent 自由发挥”的探索式能力。用 LangChain 这类通用框架封装层厚升级模型或换供应商时反而容易踩兼容性的坑。我自己的生产项目里最后只保留了模型 SDK 和函数调用Function Calling再加上一段自己的状态机代码反而更稳定、更好排查问题。技术栈上解析 PDF 用 PyMuPDF 或 pdfplumber文本处理用 Python模型接口用 OpenAI 兼容的 Function Calling结构化输出用 Pydantic 做校验。方不方便核心不在于框架多花哨而在于每一步是不是可控。后面你会看到这套自研方案代码量并不大但每一步都能说得清为什么。2.3 审查流程的主线设计我自己在项目里用的是一套五步状态机每次跑合同都会严格走完这五个阶段预处理解析文档、去噪、分节生成“干净文本 章节索引”。粗抽取用模型抽取合同主体、金额、日期、关键条款名称把结果落入结构化字段。规则审查代码直接检查硬规则比如金额大小写是否一致、日期是否早于今天、前后主体称呼是否统一。语义审查针对“违约责任是否对等”“付款条件是否明确”“争议解决是否完整”这类需要理解的项逐条调用模型分析。汇总复核把规则审查和语义审查的结果合并去重、按风险等级排序生成最终报告。这个顺序是有讲究的。规则审查放在语义审查前面是因为规则结果确定、便宜、可解释能帮我们先把低级的硬伤捞出来语义审查放在后面可以让模型聚焦在真正需要“理解”的问题上而不是浪费 token 去数金额。汇总复核阶段还会做一次“前后一致性”的二次检查比如规则模块发现金额不一致语义模块也发现付款条款风险报告里就合并成一条避免同一个问题被重复报。3. 核心实现从解析到结构化风险报告的完整代码3.1 文档解析与文本清洗合同文本来源非常杂有 PDF、Word、扫描件、网页复制文本。我建议第一版先只处理“纯文本 PDF”其他格式后补。PDF 用 PyMuPDF 提取最简单import fitz # PyMuPDF def extract_text_from_pdf(path: str) - str: doc fitz.open(path) pages [] for page in doc: pages.append(page.get_text()) return \n.join(pages)这里有个坑很多 PDF 看着有文字其实是用图片形式存储的扫描件get_text()提取出来是空白。生产环境必须加一道“提取字数检测”如果一页提取出来的字符数少于阈值就标记为疑似扫描件转接 OCR 服务。实际项目里我会用page.get_text(words)判断每页单词数量低于 20 就认为这一页基本是图片。文本清洗也不能省。我见过最混乱的合同里有页眉页脚、水印、批注、表格碎片直接喂给大模型会影响抽取质量。常用的清洗策略包括去掉连续空白字符、去掉重复页眉、修正断行比如把“付\n款”合并成“付款”、识别表格区域并转换成“列名值”的平铺格式。表格转换这步尤其重要因为合同里大量关键信息都藏在付款计划表、交付物清单里直接按纯文本读行与列的关系会丢失。3.2 定义审查结果模型结构化输出是整个项目的生命线。我习惯先用 Pydantic 定义一套结果模型后面所有模型调用、规则校验都往这个模型里塞。核心模型大概是这样的from pydantic import BaseModel, Field from typing import List, Optional class RiskItem(BaseModel): category: str Field(description风险类别如主体信息、付款条款、违约责任等) risk_level: str Field(description风险等级high / medium / low) clause_title: str Field(description涉及条款的名称) clause_content: str Field(description对应的原文关键内容必须原文引用) issue_description: str Field(description问题描述) suggestion: str Field(description修改建议) confidence: float Field(description模型对该风险的置信度0到1) class ContractBasicInfo(BaseModel): contract_title: str party_a: str party_b: str total_amount: Optional[str] None contract_term: Optional[str] None sign_date: Optional[str] None class ReviewReport(BaseModel): basic_info: ContractBasicInfo Field(default_factoryContractBasicInfo) risks: List[RiskItem] Field(default_factorylist) summary: str 字段设计有几个原则clause_content必须是原文引用不允许模型自己改写因为法务复核时要靠它定位confidence是给自己人看的不是给最终用户看的目的是让低置信度的结果默认不展示或降级risk_level用枚举字符串比自由文本好做统计。这套模型定下来之后后面所有模块都用它对接整个项目的骨架就稳了。3.3 Agent 工具与函数调用实现我这里的工具设计务求简单。第一版只做了三个工具get_full_text获取完整合同文本、find_clause按条款名定位原文、check_amount_consistency校验金额大小写一致性。工具的本质就是一个 JSON 描述加一个 Python 函数模型根据用户目标和当前上下文决定要不要调用、传什么参数。用 OpenAI 兼容接口的 Function Calling 来演示先定义工具描述tools [ { type: function, function: { name: find_clause, description: 在合同原文中查找指定名称的条款返回条款原文和所在位置, parameters: { type: object, properties: { clause_name: { type: string, description: 条款名称如付款、违约责任、争议解决 } }, required: [clause_name] } } }, { type: function, function: { name: check_amount_consistency, description: 检查合同中金额的小写数字与大写中文数字是否一致, parameters: { type: object, properties: { amount_small: {type: string}, amount_big: {type: string} }, required: [amount_small, amount_big] } } } ]Agent 主循环就是经典的“模型请求 - 如果要求调用工具则执行工具 - 把工具结果返回给模型 - 再请求”直到模型不再要求调用工具为止。我用一个最大轮数限制来防止死循环一般是 5 轮。在合同审查场景里工具调用不需要太多轮超过 5 轮往往是模型在瞎折腾直接中断反而好。3.4 审查主流程编排与报告生成主流程我用一套极简的编排函数串起来。第一步抽出基础信息和疑似风险点第二步逐个执行规则校验第三步调用一次模型做语义审查第四步合并结果。简化代码如下import json from openai import OpenAI client OpenAI() def run_agent(contract_text: str, tools: list, system_prompt: str, user_prompt: str): messages [ {role: system, content: system_prompt}, {role: user, content: user_prompt} ] for step in range(5): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, tool_choiceauto, ) msg resp.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg) for call in msg.tool_calls: result execute_tool(call.function.name, call.function.arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) raise RuntimeError(Agent reached max tool call rounds) def execute_tool(name: str, args_json: str): args json.loads(args_json) if name find_clause: return find_clause(contract_text, args[clause_name]) if name check_amount_consistency: return check_amount_consistency(args[amount_small], args[amount_big]) return {error: unknown tool}审查的 system prompt 我会明确告诉模型“你是一个合同审查助理请先通过 find_clause 定位关键条款再逐条分析风险最终输出 JSON 格式的 ReviewReport不要给出 JSON 之外的解释。”这里的技巧是不要把“分析过程”和“最终报告”混在一次调用里先让模型在对话里思考最后单独要求“只输出 JSON”。如果你想让模型稳定输出建议开一个分支在最后一轮强制指定response_format{type: json_object}这样返回格式基本不会乱。报告生成后不要直接展示给用户。我先做一次“后处理”过滤掉confidence 0.6的风险项把同样条款下的同类问题合并成一条再按high - medium - low排序。这一步听起来简单但在实际交付时特别提升体验因为模型很容易把同一个“付款方式不明确”的问题在三四个条款下重复报三遍。4. 提示词与审查规则设计决定审查质量的细节4.1 让模型稳定输出 JSON模型输出不稳定是合同审查落地时最大的痛点。很多人觉得“我已经在 Prompt 里写了只输出 JSON 啊为什么还乱来”原因是 Prompt 约束不够硬。我自己总结了一套组合拳第一模型统一使用支持response_format或 function calling 的接口用结构约束代替文本约束。第二Prompt 里给一个完整的输出示例并且明确用 json 包裹让模型模仿。第三要求所有引用字段必须“一字不差复制原文”禁止转述。第四在代码里用 Pydantic 校验结果校验失败就自动重试一次而不是直接抛错。这里特别想强调不要只依赖模型自觉。合同审查报告是要给法务签字确认的格式错了、字段缺了后面流程全乱。所以代码层面的“校验 重试 兜底”比 Prompt 本身更重要。我在生产环境里还加了一个降级策略如果模型连续两次都解析不出合法 JSON就放弃该条风险点改为在报告里标记“此条款自动审查失败请人工复核”。对业务来说宁可漏一条也不能让一条错误结果混进正式报告。4.2 常见合同风险点清单与判断逻辑审查规则决定了 Agent 到底能查出什么。我整理了一份自己常用的风险点清单不一定全面但覆盖了企业合同里 80% 的常见问题风险类别判断逻辑风险等级参考主体信息缺失甲乙双方名称、统一社会信用代码是否为空或明显不完整high金额大小写不一致小写数字金额与大写中文金额比对不一致high付款条件模糊付款时间未写具体日期只写“适时”“以后”等medium违约责任缺失合同通篇没有“违约金”“赔偿责任”表述high违约条款不对等甲方违约只写“协商解决”乙方违约写“承担全部损失”medium争议解决无效仲裁条款和诉讼条款同时出现或管辖约定不明high保密条款缺失涉及商业秘密但全文没有“保密”条款medium知识产权归属不清委托开发/定制类合同未约定成果归属high合同期限自动续约自动续约条款缺少退出机制或提前通知天数low这里要说明一下规则分“硬规则”和“软规则”。硬规则用代码写死比如金额大小写一致性我会写一个数字转换函数去比对而不是让模型判断软规则才交给模型比如“违约责任是否对等”。这个切分非常关键硬规则用模型去做又慢又不可靠软规则用代码去做根本做不了只有合理分工整个系统才既快又准。4.3 防幻觉与不确定性处理合同审查最怕模型“一本正经地胡说八道”。比如合同里根本没有“违约金”条款模型却编造了一句“合同中约定违约金为合同总额的 30%”还煞有其事地给了修改建议。这在法务眼里是不能接受的。我的处理方式有三个层次。第一层是“原文引用约束”前面已经提过要求模型给出的任何条款内容都必须能从原文里找到代码里再用关键词匹配做一次抽查如果匹配不上就把该条风险打回重审。第二层是“允许说不知道”在 Prompt 里明确写“如果合同原文中没有涉及该审查项请跳过不要推测”。大多数模型在得到明确许可后反而不会硬编了。第三层是“交叉验证”对高风险项比如金额、日期、管辖法院用规则引擎跑一次独立校验模型的结果和规则结果不一致时以规则结果为准并提示人工关注。这些兜底手段做完我不敢说一点幻觉都没有但至少能把“错误结果混入正式结论”的概率降到很低。对这个场景来说少报一条风险是损失错报一条风险是事故两件事的严重程度完全不同。5. 常见问题与排查技巧实录5.1 模型不按 Schema 输出怎么办这是所有做 Agent 的人第一个会撞上的问题。症状通常是你在 Prompt 里写了一大段“请输出以下 JSON”模型偏偏给你加一句“好的根据您的需求以下是审查结果”然后才输出 JSON直接把你的解析器搞崩。排查顺序我建议这样第一步看是不是模型版本太老或接口没有开启response_format第二步看你的 schema 是不是太复杂嵌套三层以上的列表很容易让模型出错能拆平的就拆平第三步看示例是否足够贴近真实合同很多模型是“模仿示例”的高手你的示例越接近真实输出它就越稳定第四步检查是不是输出长度限制不够风险项多的时候模型会被max_tokens截断结果只剩半个 JSON这种情况就别怪模型了把长度放宽即可。如果上面四步都做完了还是不稳定我的终极方案是改用 Function Calling。让工具的参数名直接对应报告字段模型把审查结果放到“工具调用参数”里返回这种结构化程度比纯文本输出高一个量级基本可以解决乱输出问题。5.2 工具调用出现死循环或重复调用怎么办Agent 默认是“自由”的但自由过头就是灾难。我有一次在测试时发现Agent 反复调用find_clause查找“违约责任”查完一次没找到居然用同样的参数又查了五次白白烧了一堆 token。原因通常是工具返回结果太简单模型不知道下一步该怎么办。解决思路有两个一是把工具返回结果写得更“可行动”比如返回“未找到指定条款可能性1. 合同确实缺失该条款2. 条款名称不同如‘违约条款’。请检查后决定是否继续检索”给模型下一步指引二是在最高层加轮数限制和重复调用检测同一个工具相同参数出现两次就直接打断提示模型进入汇总阶段。还有一类情况要留意就是工具返回的内容太长把上下文撑爆了。合同原文本来就长再让工具把整段条款内容返回给模型很容易把后面真正需要的分析空间挤没。所以工具返回前要先做截断只保留条款名称、页码和首尾各 200 字让模型知道“去哪里查”而不必每次都把全文摆在桌面上。5.3 长合同与上下文窗口冲突怎么处理合同文本一长前后矛盾就特别多但这恰恰是审查的重点。模型上下文不够的时候不能简单截断否则你连结尾的争议解决条款都看不到。我给一个非常实用的估算公式中文一个字大概占 1 到 2 个 token一万字的合同大概是 1.5 万到 2 万 token。如果你的模型上下文是 128k大部分合同都能一次性放进去如果只有 32k超过两万字的合同就必须做分段处理。分段不能按字符数硬切要按章节语义切比如“付款条款”是一段“违约责任”是一段然后对每一段做本地审查最后再做一次跨段汇总。跨段汇总时我会把第一阶段抽取的基础信息、金额、日期作为额外输入一并传给模型让模型能基于全局信息判断“这一段的表述是否和前面冲突”。这里有个容易忽略的坑分段审查时如果只把“付款条款”单独丢给模型模型不知道合同总金额是多少很可能会漏掉“付款比例加起来不等于 100%”这类问题。所以给每一段的上下文除了本段原文还必须附上全局关键信息列表。5.4 成本控制与性能优化合同审查 Agent 的成本大头永远在模型调用上。一套全流程跑下来短合同可能要调用五六次模型长合同可能超过二十次每次还要附带合同原文费用蹭蹭往上走。我控制成本的办法有三招。第一招是“规则前置”能用正则和代码解决的问题绝不让模型做比如金额一致性、日期合法性、必填字段缺失这些用规则引擎跑一遍只要几毫秒。第二招是“模型分级”基础抽取用便宜快速的小模型比如 gpt-4o-mini 这一档只有最终汇总审查意见时再用能力更强的大模型。第三招是“结果缓存”同一份模板合同、同一个版本审查结果应该可以直接复用没必要每次都让模型从头跑。合同修改后也只做增量审查把变化的部分重新跑一遍其余部分沿用上次结论。性能优化方面最容易立竿见影的是把文档解析和文本清洗做成异步或预计算。用户上传合同后立刻解析成功放在缓存里用户真正点“开始审查”时Agent 直接拉缓存文本而不是现场解析 PDF。这个小改动能省下大量等待时间。6. 生产落地经验从 Demo 到真正能用6.1 永远保留人工复核闭环如果让我给一条最想强调的落地原则那就是合同审查 Agent 永远只能做人机协同不能做全自动。法务复核不是流程的负担而是整个系统的安全阀。具体做法是在报告页面上给每条风险项加三个按钮“确认”、“误报”、“转人工”。用户每次点击都是一次标注这些标注数据积累下来就是你的黄金测试集。我见过很多团队把精力花在调提示词上却忽略了用户反馈这个最便宜、最准确的优化信号。其实你只要跑一个月把用户标记为“误报”的那些案例收集起来找规律改规则比你看一百篇 Prompt 教程都管用。人机协同的边界也很清楚机器负责“全覆盖扫描”和“低风险提醒”人负责“复杂判断”和“最终决策”。任何高风险意见机器给的是“建议”不能是“结论”。6.2 用“黄金测试集”做回归合同审查 Agent 是一个天然适合用测试集驱动的项目因为审查规则相对稳定预期结果可以人工标注。我建议从第一天起就建立一个包含 30 到 50 份合同的测试集覆盖常见类型采购合同、销售合同、服务合同、保密协议、劳动合同。每份合同都人工标注好“应该查出哪些风险”然后每次改 Prompt、改规则、换模型都拿这套数据回归一遍。没有测试集的 Agent 项目后期维护就是噩梦。你改了付款条款的提示词结果把保密条款的误报率抬高了如果没有回归测试这个问题可能上线一个月都没人发现。有了测试集你每次改动后能立刻看到“发现问题数”和“误报数”两个指标的变化心里非常有底。我自己的经验是测试集不在多而在“狠”。里面故意放几份已经签过的烂合同放几份前后矛盾的合同放几份表述特别绕的合同让 Agent 见过各种妖魔鬼怪它才不至于在真实场景里一碰就碎。6.3 我踩过的几个坑和最后一点心得最后分享几个我真实踩过的坑。第一个坑是“过早优化”第一版就上了向量数据库做条款语义检索结果发现合同审查里关键词定位已经能解决 90% 的需求向量检索反而引入了大量噪声。现在我的第一版方案里工具越简单越好复杂检索都是后面数据量大了再考虑的。第二个坑是“太相信模型的修改建议”。有一次模型对一份销售合同的付款条款给出“建议改为货到付款”听起来很合理但业务背景是供应商强势根本不可能同意。后来我在提示词里加了一句话“修改建议应基于合同双方地位和行业惯例避免提出对方明显不可接受的方案。”即便如此模型建议也只能当参考最终修改方案一定要业务人员确认。第三个坑是“没有日志”。早期排查问题时我都得靠肉眼复现 Agent 的每一步特别痛苦。现在所有工具调用、模型输入输出、耗时、token 消耗全部落到日志里。出问题先看日志三分钟就能定位是模型抽风还是规则写错。合同审查 Agent 这个项目看起来是技术活实际上是“流程设计 质量工程”的活。模型能力每年都在涨但能把模型稳定地嵌进业务流、并且让业务人员愿意用才是真正值钱的部分。我到现在仍然坚持一个观点不要让 Agent 做一个“更聪明的实习生”而要让它做一个“每一步都有记录、每一句话都有出处、不敢乱下结论的助理”。沿着这个方向做就算模型后面换了好几代你的这套架构也依然管用。
网站建设高端定制企业官网