第1650篇:法律文书AI辅助系统——合同审查与条款提取的技术实现
第1650篇:法律文书AI辅助系统——合同审查与条款提取的技术实现
有个朋友在一家初创公司做法务,他跟我说了个让我印象很深的事:他们公司每周要处理大量供应商合同,都是几十页的东西,他一个人看,有次因为太累漏掉了一个自动续约条款,结果被绑定了两年不想要的服务,损失不少。
他问我:"能不能做个AI帮我看合同?"
这个需求我觉得很真实。法律行业里大量的工作其实是信息提取和模式匹配——找出关键条款、识别风险点、对比合同版本——这些都是AI相对擅长的。
但法律场景的特殊性不能忽视:误判的代价可能很高,而且不同合同类型、不同行业、不同法域,条款的含义可能完全不同。带着这些认知,我设计了这套系统。
明确能做什么、不能做什么
跟医疗场景一样,法律场景也必须先划清楚边界。
可以做的:
- 合同条款结构化提取(把合同解析成结构化数据)
- 风险条款识别与提示(识别可能有问题的条款)
- 关键信息高亮(付款期限、违约责任、自动续约等)
- 合同版本对比(两个版本之间的差异)
- 常用合同条款检查(是否遗漏关键条款)
不能做的:
- 给出法律意见("这个合同对你有利/不利")
- 预测司法判决
- 替代律师审查
- 处理复杂的法律问题(劳动纠纷、知识产权诉讼等专业场景)
这个边界要在产品里非常清晰地标出来,不只是法律免责声明,而是在每个AI输出旁边都要有明确的"本功能仅供参考,不构成法律意见"提示。
整体架构
合同文档解析
法律文件格式特别多:PDF(扫描版、文字版)、Word、OFD(国内电子合同常见格式)。
扫描版PDF是最头疼的,因为是图片,必须走OCR。法律文件对识别精度要求极高,"不得"和"得"一字之差,意思完全相反。
@Service
public class ContractDocumentParser {
@Autowired
private TesseractOcrService ocrService;
@Autowired
private ChatClient chatClient;
public ParsedContract parse(MultipartFile file) {
String rawText = extractText(file);
// 检查文本质量
if (!isTextQualityAcceptable(rawText)) {
// 可能是扫描版或图片PDF
rawText = ocrService.extractHighQuality(file);
}
// 文本清洗:处理多余空行、页眉页脚、页码等
String cleanedText = cleanContractText(rawText);
// 合同类型识别
ContractType contractType = identifyContractType(cleanedText);
return ParsedContract.builder()
.originalText(cleanedText)
.contractType(contractType)
.pageCount(getPageCount(file))
.extractedAt(Instant.now())
.build();
}
private String cleanContractText(String rawText) {
// 移除页眉页脚(通常是重复出现的文本)
String cleaned = removeHeaderFooter(rawText);
// 合并不应该换行的段落(PDF提取时经常把一行分成几行)
cleaned = mergeWrappedLines(cleaned);
// 标准化条款编号格式(第X条、第X款等)
cleaned = normalizeClauseNumbers(cleaned);
return cleaned;
}
private ContractType identifyContractType(String text) {
// 先用关键词快速识别
if (text.contains("劳动合同") || text.contains("劳动关系") || text.contains("聘用")) {
return ContractType.LABOR;
}
if (text.contains("采购合同") || text.contains("买卖合同") || text.contains("供货")) {
return ContractType.PURCHASE;
}
if (text.contains("服务合同") || text.contains("服务协议")) {
return ContractType.SERVICE;
}
if (text.contains("租赁合同") || text.contains("租赁协议") || text.contains("房屋租赁")) {
return ContractType.LEASE;
}
// ... 更多类型
// 快速识别不了的,用大模型判断
return identifyTypeWithLLM(text.substring(0, 1000));
}
}结构化信息提取
这是整个系统最核心的能力:把合同里的关键信息提取成结构化数据。
不同类型的合同要提取的字段不同,所以我用了配置化的方式:
@Service
public class ContractInfoExtractor {
@Autowired
private ChatClient chatClient;
@Autowired
private ContractFieldConfigRepository fieldConfigRepo;
public ContractStructuredInfo extract(
ParsedContract contract,
ContractType contractType) {
// 加载该合同类型的字段配置
List<ContractFieldConfig> fieldConfigs = fieldConfigRepo
.findByContractType(contractType);
String fieldsDescription = fieldConfigs.stream()
.map(f -> String.format("- %s(%s):%s",
f.getFieldName(),
f.isRequired() ? "必填" : "选填",
f.getDescription()))
.collect(Collectors.joining("\n"));
// 分批提取(合同通常很长,分段处理)
List<String> sections = splitIntoSections(contract.getOriginalText());
Map<String, Object> extractedFields = new HashMap<>();
for (String section : sections) {
Map<String, Object> sectionResult = extractSection(
section, fieldsDescription, contractType
);
// 合并结果(后面的覆盖前面的,但列表字段要追加)
mergeResults(extractedFields, sectionResult);
}
return buildStructuredInfo(extractedFields, contractType);
}
private Map<String, Object> extractSection(
String sectionText,
String fieldsDescription,
ContractType contractType) {
String prompt = """
请从以下合同文本中提取结构化信息:
合同类型:%s
需要提取的字段:
%s
合同文本:
%s
提取要求:
1. 只提取文本中明确包含的信息,不要推断或补充
2. 金额要保留原始数字和货币单位
3. 日期保持原格式,同时转换为标准格式(YYYY-MM-DD)
4. 如果某字段在此段文本中找不到,返回null,不要猜测
5. 百分比、比率等保持原始格式
以JSON格式返回,字段名与需要提取的字段名一致。
""".formatted(
contractType.getLabel(),
fieldsDescription,
sectionText
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseJsonMap(response);
}
}以采购合同为例,提取出来的结构化数据大概是这样的:
{
"parties": {
"buyer": {"name": "XX科技有限公司", "address": "北京市朝阳区XX路XX号"},
"seller": {"name": "XX供应商有限公司", "address": "上海市浦东新区XX路XX号"}
},
"subject": "ERP软件系统实施服务",
"totalAmount": {"value": 500000, "currency": "CNY", "inWords": "伍拾万元整"},
"paymentTerms": [
{"milestone": "合同签署", "percentage": 30, "amount": 150000, "dueDate": null},
{"milestone": "上线验收", "percentage": 60, "amount": 300000, "dueDate": null},
{"milestone": "质保期满", "percentage": 10, "amount": 50000, "dueDate": null}
],
"deliveryDate": "2024-06-30",
"warrantyPeriod": "12个月",
"autoRenewal": false,
"terminationClause": "合同第12条",
"penaltyRate": "0.1%/日"
}这些结构化数据可以直接用于后续的风险分析、合同管理系统等。
风险条款检测
这是最有价值也最难做好的功能。风险检测要避免两个极端:漏掉真正的风险点,或者把所有条款都标记为"风险"(狼来了效应,用户会无视)。
我们的方案是:分层次的风险分类 + 有理由的风险说明。
@Service
public class RiskClauseDetectionService {
@Autowired
private ChatClient chatClient;
// 风险规则库(按合同类型分类)
@Autowired
private RiskRuleRepository riskRuleRepo;
public RiskDetectionResult detectRisks(
ParsedContract contract,
ContractType contractType,
String reviewerPerspective) { // BUYER/SELLER/EMPLOYER/EMPLOYEE
// 加载该场景的风险规则
List<RiskRule> riskRules = riskRuleRepo.findByContractTypeAndPerspective(
contractType, reviewerPerspective
);
String riskRulesStr = riskRules.stream()
.map(r -> String.format(
"风险类型:%s(%s级)\n描述:%s\n常见表现:%s",
r.getRiskType(), r.getSeverity(),
r.getDescription(), r.getCommonPatterns()
))
.collect(Collectors.joining("\n---\n"));
String prompt = """
你是一位经验丰富的合同法律专业人士,请从%s的角度审查以下合同的风险条款。
【风险检查清单】
%s
【合同内容】
%s
请识别合同中的风险点,要求:
1. 只标注确实存在的风险,不要过度警示
2. 每个风险要引用合同原文(哪一条/款)
3. 说明为什么是风险,不要只说"存在风险"
4. 给出具体的修改建议方向(不是具体措辞,而是方向)
5. 按风险严重程度分级:HIGH/MEDIUM/LOW
以JSON数组格式返回,每条包含:
{
"riskType": "风险类型",
"severity": "HIGH/MEDIUM/LOW",
"clauseReference": "合同第X条第X款",
"originalText": "有问题的原文(30字以内)",
"riskExplanation": "为什么有风险(50字以内,说清楚)",
"modificationDirection": "建议修改方向(30字以内)"
}
重要提示:这些仅供参考,最终法律判断需要专业律师确认。
""".formatted(
reviewerPerspective.equals("BUYER") ? "买方/甲方" : "卖方/乙方",
riskRulesStr,
contract.getOriginalText()
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
List<RiskItem> riskItems = parseRiskItems(response);
// 高风险数量统计
long highRiskCount = riskItems.stream()
.filter(r -> r.getSeverity() == Severity.HIGH)
.count();
return RiskDetectionResult.builder()
.riskItems(riskItems)
.highRiskCount((int) highRiskCount)
.overallRiskLevel(calculateOverallRisk(riskItems))
.reviewedAt(Instant.now())
.disclaimer("以上风险提示仅供参考,不构成法律意见,重要合同建议咨询专业律师。")
.build();
}
}我们预定义了各类合同的常见风险规则,以采购合同买方视角为例:
// 示例风险规则(数据库中存储)
List<RiskRule> purchaseBuyerRisks = List.of(
RiskRule.builder()
.riskType("LIABILITY_IMBALANCE")
.severity(Severity.HIGH)
.description("违约责任不对等:卖方违约责任轻于买方")
.commonPatterns("卖方违约只赔定金,买方违约需赔偿损失")
.build(),
RiskRule.builder()
.riskType("AUTO_RENEWAL")
.severity(Severity.MEDIUM)
.description("合同自动续约条款,可能被绑定")
.commonPatterns("到期前X日未提出异议即视为续约")
.build(),
RiskRule.builder()
.riskType("BROAD_FORCE_MAJEURE")
.severity(Severity.MEDIUM)
.description("不可抗力条款过宽,卖方可轻易免责")
.commonPatterns("政策调整、经济波动等被列为不可抗力")
.build(),
RiskRule.builder()
.riskType("PAYMENT_PROTECTION")
.severity(Severity.HIGH)
.description("无质量保障金/尾款扣留条款,验收后无保障")
.commonPatterns("验收合格后XX日内付清全款,无质保留存")
.build(),
RiskRule.builder()
.riskType("IP_OWNERSHIP")
.severity(Severity.HIGH)
.description("知识产权归属不明确,可能引发纠纷")
.commonPatterns("软件、方案等知识产权归属条款缺失")
.build()
);合同版本对比
法律谈判经常需要对比甲方和乙方版本的差异,这个功能极其实用:
@Service
public class ContractComparisonService {
@Autowired
private ChatClient chatClient;
public ContractDiff compare(
ParsedContract contractA,
ParsedContract contractB,
String labelA, // 如"甲方版本"
String labelB) { // 如"我方版本"
// 按条款编号对齐
Map<String, String> clausesA = extractClauses(contractA.getOriginalText());
Map<String, String> clausesB = extractClauses(contractB.getOriginalText());
List<ClauseDiff> diffs = new ArrayList<>();
Set<String> allKeys = new HashSet<>();
allKeys.addAll(clausesA.keySet());
allKeys.addAll(clausesB.keySet());
for (String clauseKey : allKeys) {
String textA = clausesA.get(clauseKey);
String textB = clausesB.get(clauseKey);
if (textA == null) {
diffs.add(ClauseDiff.onlyInB(clauseKey, textB, labelB));
} else if (textB == null) {
diffs.add(ClauseDiff.onlyInA(clauseKey, textA, labelA));
} else if (!textA.equals(textB)) {
// 有差异的条款,用大模型分析差异的影响
ClauseDiffAnalysis analysis = analyzeDiff(
clauseKey, textA, textB, labelA, labelB
);
diffs.add(ClauseDiff.modified(clauseKey, textA, textB, analysis));
}
}
return ContractDiff.builder()
.labelA(labelA)
.labelB(labelB)
.diffs(diffs)
.summaryA(contractA.getContractType().getLabel() + " - " + labelA)
.summaryB(contractA.getContractType().getLabel() + " - " + labelB)
.build();
}
private ClauseDiffAnalysis analyzeDiff(
String clauseKey,
String textA, String textB,
String labelA, String labelB) {
String prompt = """
请分析以下合同条款的两个版本之间的实质差异:
条款编号:%s
%s版本:
%s
%s版本:
%s
请分析:
1. 核心差异是什么(20字以内,简洁明了)
2. 对%s有利还是对%s有利
3. 差异的法律影响(如果有的话,50字以内)
4. 重要性:HIGH/MEDIUM/LOW
以JSON格式返回。注意:只分析事实差异,不要给出接受建议。
""".formatted(
clauseKey,
labelA, textA,
labelB, textB,
labelA, labelB
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseDiffAnalysis(response);
}
}条款完整性检查
除了找风险,还要检查合同是否遗漏了关键条款。没写的条款,往往比写了的条款更危险。
@Service
public class ContractCompletenessChecker {
@Autowired
private ChatClient chatClient;
public CompletenessCheckResult check(
ParsedContract contract,
ContractType contractType) {
// 加载该合同类型的必备条款清单
List<RequiredClause> requiredClauses = requiredClauseRepo
.findByContractType(contractType);
String requiredListStr = requiredClauses.stream()
.map(c -> String.format("- %s(%s):%s",
c.getClauseName(),
c.isStronglyRequired() ? "强制" : "建议",
c.getImportance()))
.collect(Collectors.joining("\n"));
String prompt = """
请检查以下合同是否包含所有必要条款:
合同类型:%s
应包含的条款清单:
%s
合同实际内容:
%s
请逐一检查每个条款是否存在,返回JSON数组:
[
{
"clauseName": "条款名称",
"isPresent": true/false,
"locationHint": "如果存在,大概在哪里(如第X条)",
"isStronglyRequired": true/false,
"missingRisk": "如果缺失,有什么风险(20字)"
}
]
""".formatted(
contractType.getLabel(),
requiredListStr,
contract.getOriginalText()
);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
List<ClausePresenceCheck> checks = parsePresenceChecks(response);
List<ClausePresenceCheck> missingRequired = checks.stream()
.filter(c -> !c.isPresent() && c.isStronglyRequired())
.collect(Collectors.toList());
return CompletenessCheckResult.builder()
.allChecks(checks)
.missingRequiredClauses(missingRequired)
.completenessScore(calculateCompletenessScore(checks))
.build();
}
}审查报告生成
把所有分析结果整合成一份可读的审查报告:
@Service
public class ReviewReportGenerationService {
@Autowired
private ChatClient chatClient;
public ReviewReport generateReport(
ParsedContract contract,
ContractStructuredInfo structuredInfo,
RiskDetectionResult riskResult,
CompletenessCheckResult completenessResult) {
// 生成执行摘要(给没时间看细节的人看)
String executiveSummary = generateExecutiveSummary(
structuredInfo, riskResult, completenessResult
);
// 生成详细风险说明
String riskDetails = formatRiskDetails(riskResult);
// 生成关键条款摘要
String keyClausesSummary = formatKeyClausesSummary(structuredInfo);
return ReviewReport.builder()
.contractName(contract.getFileName())
.contractType(contract.getContractType().getLabel())
.reviewedAt(Instant.now())
.executiveSummary(executiveSummary)
.highRiskCount(riskResult.getHighRiskCount())
.overallRiskLevel(riskResult.getOverallRiskLevel())
.riskDetails(riskDetails)
.missingClausesCount(completenessResult.getMissingRequiredClauses().size())
.keyClausesSummary(keyClausesSummary)
.disclaimer("""
重要说明:本报告由AI系统自动生成,仅供参考,
不构成法律意见。重要合同的最终决策建议咨询持牌律师。
本系统识别的风险可能不完整,不代表合同中没有其他风险。
""")
.build();
}
private String generateExecutiveSummary(
ContractStructuredInfo info,
RiskDetectionResult risk,
CompletenessCheckResult completeness) {
String prompt = """
请根据以下合同分析结果,生成一段执行摘要(150字以内):
合同基本信息:
- 类型:%s
- 金额:%s
- 关键日期:%s
风险状况:
- 高风险项:%d个
- 中风险项:%d个
- 总体风险级别:%s
完整性:
- 缺失必要条款:%d个
- 完整性评分:%d/100
请用业务人员能理解的语言写摘要,重点说:
这份合同总体怎样,最需要关注什么,建议做什么。
不要用法律术语,不要给出"应该签/不应该签"的建议。
""".formatted(
info.getContractType(),
info.getTotalAmountDescription(),
info.getKeyDatesDescription(),
risk.getHighRiskCount(),
risk.getMediumRiskCount(),
risk.getOverallRiskLevel(),
completeness.getMissingRequiredClauses().size(),
completeness.getCompletenessScore()
);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}工程上的一些经验
1. 长文档分块处理很关键
合同通常很长,一些标准合同动辄100页。不能一次性塞进Context,要分块处理,然后合并结果。
分块策略要按条款边界分,不能随机断开,不然上下文丢失会影响理解。
2. 结构化提取要有置信度
不是所有字段都能准确提取,当大模型不确定时,要标记"低置信度",让用户知道这个字段需要人工确认:
// 带置信度的提取结果
@Data
public class ExtractedField {
private String fieldName;
private Object value;
private float confidence; // 0-1
private String evidenceText; // 提取依据(合同原文片段)
public boolean needsManualVerification() {
return confidence < 0.8;
}
}3. 法律领域词汇要特别处理
大模型有时候对法律专业词汇理解不够精准。我们维护了一个法律术语词典,在Prompt里加入必要的术语解释,减少误判。
4. 隐私保护
合同里有大量商业敏感信息。如果用的是云端大模型API,要确认服务提供商的数据使用政策,确保合同内容不会被用于训练。
对于特别敏感的客户,我们部署了私有化大模型,合同内容完全不出企业网络。
5. 持续的规则库更新
风险规则库不是建好就完事的,需要持续更新:
- 法规变化(劳动法修订、合同法解释等)
- 新型合同风险(特别是数字合同、数据服务合同这类新兴领域)
- 用户反馈(被漏掉的真实风险案例)
效果数据
上线4个月,在一家50人的企业做了内测:
- 合同审查时间:从平均45分钟/份降到12分钟(AI提前标记重点,人只看重点)
- 法务人员满意度:4.5/5
- AI识别风险点的准确率:88%(用户确认"确实是风险"的比例)
- 漏掉的重要风险:3.2%(这个是持续改进的重点)
法律AI是我觉得最需要"敬畏心"的场景之一。技术能做到的事情和应该用技术做的事情之间,始终要保持清醒的判断。这套系统的最大价值不是替代法务,而是把法务的注意力从繁琐的信息整理解放出来,聚焦在真正需要专业判断的地方。
