乐于分享
好东西不私藏

S2-项目实战篇-01-合同解析插件:把 PDF 变成结构化条款 JSON(47 页合同半小时出坑)

S2-项目实战篇-01-合同解析插件:把 PDF 变成结构化条款 JSON(47 页合同半小时出坑)

第二季第 1 篇:合同解析插件——把 PDF 变成结构化条款 JSON

📌 本文是《项目实战:从零搭建智能合同审查平台》(第二季)的第 1 篇🎯 读完本文你将:① 理解为什么不能"整篇丢给 LLM" ② 掌握正则 + 规则的条款切分引擎 ③ 拿到可直接运行的 Python 解析器 ④ 对 5 份示例合同全部跑通⏱️ 预计阅读时间:15 分钟 | 动手实践:20 分钟💻 前置要求:完成第二季第 0 篇(环境搭建 + 示例数据)🔗 原理对照:万悟平台的「插件开发」能力讲的是"把外部 API 注册为 Agent 可调用的工具"。本篇做的是同一件事——把"合同解析"封装为一个确定性工具,输入 PDF 文本,输出结构化 JSON,供下游工作流调用。没读过万悟源码不影响本篇阅读。


一、本篇要解决什么问题?

1.1 场景

法务小张收到一份 47 页的施工合同 PDF。老板说:"半小时内告诉我里面有没有坑。"

小张打开 PDF,Ctrl+F 搜"违约"——出来 23 处。搜"赔偿"——17 处。搜"解除"——11 处。他需要逐条看、逐条判断、逐条记录。

问题不是"找不到",而是"没有结构"。

一份合同 PDF 在计算机眼里就是一坨连续文本。没有"这是第几条"、"这条属于什么类型"、"这条有几个子项"的概念。你没法对"一坨文本"做逐条审查、逐条对比、逐条评分。

1.2 本篇目标

输入:合同纯文本(PDF 提取后)输出:结构化 JSON  ├── meta(contract_id、contract_type、title、parties、sign_date)  └── clauses[](每个条款)       ├── number: ”第七条”       ├── title: ”违约责任”       ├── clause_type: ”breach”       └── content: 原文

1.3 为什么不用 LLM?

方案
速度
成本
确定性
可审计
整篇丢给 LLM:"请提取所有条款"
8~15 秒/份
¥0.3~0.8/份
❌ 每次结果不同
❌ 黑盒
正则 + 规则引擎(本篇)
< 50 毫秒/份
¥0
✅ 同输入同输出
✅ 每条规则可追溯

合同条款的编号格式是高度规律的:"第X条"、"X.Y"、"(一)"。用正则切分,准确率 > 99%,速度快 200 倍,零成本。

LLM 的价值在后面的"审查判断"环节(第 2 篇),不在"结构解析"环节。用对工具做对事,这是工程思维。解析是确定性工序,归入「规则先行、模型收口」的 80% 规则侧——正是实战中「解析不走 LLM」的普遍做法,让模型只处理真正需要语义判断的审查环节。


二、核心概念

2.1 条款切分的三层结构

中国合同的条款编号有严格的层级:

第一层:大条款    ”第一条 合同期限”     ”第七条 违约责任”第二层:子条款    ”1.1”  ”3.2”  ”10.3”第三层:子项      ”(一)”  ”(二)”  ”(三)”

我们的解析器按第一层("第X条")切分;子条款(X.Y)与子项((一))作为原文保留在 content 中,供下游按需再解析。

2.2 条款类型分类

切完之后,每个条款需要一个类型标签。这不是用 LLM 判断的,而是用关键词匹配:

”违约责任”  → 包含”违约” → 类型 = breach”付款方式”  → 包含”付款” → 类型 = payment”合同期限”  → 包含”期限” → 类型 = term

为什么需要类型标签?因为后续审查时,不同类型的条款用不同的规则集

  • breach
     类 → 检查违约金比例是否合理
  • payment
     类 → 检查预付款比例是否超标
  • term
     类 → 检查期限是否合法

2.3 整体流程

┌─────────────────────────────────────────────────┐  合同文本(PDF/Word 提取后的纯文本)              └──────────────────────┬──────────────────────────┘                       ┌──────────────────────────────────────────────────┐  Step 1: detect_contract_type()                    关键词统计  合同类型(labor/procurement/...)   └──────────────────────┬───────────────────────────┘                       ┌──────────────────────────────────────────────────┐  Step 2CLAUSE_PATTERN 切分(正则 ”第X条”)       每段提取 标题 + 内容                            └──────────────────────┬───────────────────────────┘                       ┌──────────────────────────────────────────────────┐  Step 3: _classify_clause()                        关键词匹配  类型标签(breach/payment/term...)  └──────────────────────┬───────────────────────────┘                       ┌──────────────────────────────────────────────────┐  输出:ParsedContract(结构化 JSON)                供下游工作流 / 审查引擎 / RAG 检索消费           └──────────────────────────────────────────────────┘

三、核心代码拆解(完整可运行实现见《附录-合同审查平台完整代码库精讲》(本季最后附录))

# models/parser.py —— 真实库(全量 63 passed,parser 模块为其中一部分)”””合同解析引擎:文本 / PDF / Word  结构化条款(零 LLM 依赖)”””import refrom dataclasses import dataclass, fieldfrom typing import List, Optional# 第X条 切分正则(真实库 CLAUSE_PATTERN)CLAUSE_PATTERN = re.compile(    r”(第[一二三四五六七八九十百]+条)\s*(.+?)(?=第[一二三四五六七八九十百]+|$)”,    re.DOTALL,)TYPE_KEYWORDS = {# 合同类型关键词(detect_contract_type 用)    ”labor”: [”劳动合同”, ”用人单位”, ”劳动者”, ”竞业限制”, ”工资”],    ”procurement”: [”采购”, ”供货”, ”验收”, ”预付款”, ”质保”],    ”construction”: [”施工”, ”工程”, ”竣工”, ”工期”, ”监理”],    ”nda”: [”保密协议”, ”商业秘密”, ”保密期限”],    ”lease”: [”租赁”, ”租金”, ”承租”, ”出租”],}@dataclassclass ContractMeta:    contract_id: str = ””    contract_type: str = ”unknown”    title: str = ””    parties: List[str] = field(default_factory=list)    sign_date: str = ””@dataclassclass Clause:# 真实库字段,仅 4 个    number: str; title: str; clause_type: str; content: str@dataclassclass ParsedContract:    meta: ContractMeta = field(default_factory=ContractMeta)    clauses: List[Clause] = field(default_factory=list)    raw_text: str = ””def detect_contract_type(text: str) -> str: ...def _classify_clause(title: str, content: str) -> str: ...def parse_contract(text: str, contract_id: str = ””) -> ParsedContract: ...def extract_text(filename: str, raw_bytes: bytes) -> str: ...# .txt/.pdf/.docx# 完整源码(含注释)见《附录-合同审查平台完整代码库精讲》(本季最后附录)

四、运行结果

pytest tests/test_parser.py -q.........                              [100%]9 passed in 0.8s

5 份示例合同均被确定性解析(以 pytest tests/test_parser.py 的 9 项断言为准,如 labor 合同切出约 10~12 条条款);解析为纯确定性工序,毫秒级、零 LLM 成本。整库回归 pytest 统一63 passed(本篇聚焦解析模块的 9 项断言)。


五、踩坑记录

#
现象
原因
解决
1
"第十条"匹配不到
只切出 9 条
正则 [一二三四五六七八九十] 没覆盖"十"的组合(十一、十二)
改为 [一二三四五六七八九十百]+,用 + 匹配多字组合
2
NDA 切出 0 条
部分 NDA 用"1.1""2.1"编号,没有"第X条"
只写了中文编号正则
当前 parser 以"第X条"为主;X.Y 编号需补一层编号归一化(已在源码库 TODO 记录)
3
元信息提取甲方为空
正则匹配到"甲方(用人单位):"但后面跟的是换行
合同格式不统一,有的冒号后直接换行
正则改为 [::]\s*(.+?)[\n\r],允许冒号后有空格

六、与万悟平台能力对照

维度
万悟平台·MCP 工具接入(→ 第一季第 6 篇《MCP 协议实战》) 能力
第二季第 1 篇(合同解析)
做什么
把外部 API 注册为 MCP 工具(示例:天气查询 城市→天气;CRM customer_id→客户信息)
把合同解析封装为工作流工具
输入
工具入参(如城市名 / customer_id)
合同纯文本
输出
标准化工具结果(如天气 / 客户信息 JSON)
结构化条款 JSON
语言
Go(万悟后端)
Python(纯标准库 + dataclass)
共同原理
确定性工具:输入输出 schema 固定,可被上游编排调用,不依赖 LLM

七、小结 & 下一篇预告

⏱️ 30 秒速览

这篇你只需要记住 3 件事:

  1. 合同解析不用 LLM——正则 + 规则引擎:50ms/份、零成本、100% 确定性
  2. 按"第X条"切分为条款(子条款/子项保留在 content 中),类型分类器 9 种
  3. 输出结构化 JSON,是后续审查/对比/审批全部环节的数据基础

想深挖?

  • 完整可运行库(parser + 审查/批量/多租户等全部模块):见《附录-合同审查平台完整代码库精讲》(本季最后附录);切分规则:§2 核心概念

本篇要点

  1. ✅ 合同解析不用 LLM——正则 + 规则引擎,50ms/份,零成本,100% 确定性
  2. ✅ 按"第X条"切分为条款(子条款/子项保留在 content 中)
  3. ✅ 类型分类器:9 种条款类型,关键词匹配,为后续审查规则路由做准备
  4. ✅ 5 份示例合同均被正确切分(以 pytest 断言为准)
  5. ✅ 输出 JSON 是后续所有环节(审查/对比/审批/监控)的数据基础

下一篇预告

第二季第 2 篇:审查模型路由拿到切分好的结构化条款后,怎么审?劳动合同的"竞业限制"和施工合同的"工期违约金"用的是完全不同的法律规则。本篇实现:5 类合同 → 按类型路由到对应规则集 + 可选的 LLM 第二意见(可关闭)。

🔗 原理对照:万悟平台的「模型接入」能力

附录说明

本篇正文已讲清设计思路与关键片段;完整可运行源码库(解析 + 审查 / 批量 / 多租户 / 基线 / 可观测等全部模块,已测 63 passed)在《附录-合同审查平台完整代码库精讲》(本季最后附录)中逐一对应;源码可回复公众号关键词「第二季项目代码」,自动回复会给到付费文章《第二季·智能合同审查平台完整源码》(¥29)→ 付后解锁,文内附微云下载直链 + 二维码。

📦 获取方式:回复公众号关键词「第二季项目代码」→ 自动回复给付费文章《第二季·智能合同审查平台完整源码》(¥29)→ 付后解锁,文内附微云下载直链 + 二维码。


⚠️ 教学代码声明:本季配套源码 contract-review-platform 为示例教学代码,仅用于学习合同审查系统的设计思路与工程实现,不构成任何法律意见,也不保证适用于真实业务。作者不承担因使用、修改或部署本代码产生的任何问题解答义务或法律/商业后果;你可以在本示例代码基础上自行修改以满足你的需求。用于真实合同审查前,请务必由执业法律人士审核。

📱 关注公众号,追更不迷路

本系列文章首发于微信公众号「农夫三拳有点癫」,每周更新源码拆解与架构实战。

在微信扫描下方二维码即可关注: