基于 LangChain1.0 OCR+RAG 搭建法务合同审核 Agent(附源码)
一、技术选型:为什么法务场景需要OCR而非VLM?
在构建文档智能体(Agent)时,我们经常面临一个技术选型的难题:是直接使用视觉语言模型(VLM)"看"文档,还是采用OCR提取文本结合RAG(检索增强生成)的技术路线?
对于政府招投标书(RFP)、行政公文、法律合同等严格格式文档,我们发现OCR+RAG方案在落地效果上远超VLM。这些文档通常动辄数十页,且对条款的位置、内容规范性有极高要求。
核心考量维度如下:
1. 文档长度与Token成本
合同和标书通常是多页长文档。以一份20页的劳动合同为例:
- VLM方案 :若将PDF转为图像输入,每页约消耗 1000-2000 tokens ,整份文档需2-4万tokens,接近许多模型的上下文上限,且图像Token价格昂贵(通常是文本的5-10倍)。
- OCR方案 :提取纯文本通常只需 5000-8000 tokens ,成本降低 80%以上。

2. 精确坐标定位与可追溯性
法务审核的核心需求是精确定位问题条款。
- OCR方案 (如MinerU):解析时返回每个文本块的精确坐标 (bbox:
[x1, y1, x2, y2]),可精确到字符级,支持后续的高亮批注。 - VLM方案 :其返回结果中缺乏精确的坐标映射,通常只能给出模糊的页码或段落描述,无法支持后续的PDF批注、高亮标记等可视化功能。这对于需要生成审核报告并在原文档上标注问题的场景来说是致命缺陷。

3. 表格与复杂格式处理
对于合同中常包含表格 (如付款计划表、违约金计算表)和复杂格式(如条款编号、多级标题)
- OCR方案 :能将其解析为结构化数据(JSON/Markdown),便于后续的计算校验(如金额汇总、日期逻辑检查)。
- VLM方案 :VLM 虽然能识别表格,但其输出往往是描述性文本("表格中显示..."),难以直接用于数值计算和格式校验,需要额外的后处理步骤
| 对比维度 | OCR + RAG 方案 | VLM 方案 |
|---|---|---|
| 长文档处理 | 智能切分,无token上限 | 受限于上下文窗口 |
| 成本 | 纯文本token,成本低 | 图像token,成本高5-10倍 |
| 坐标定位 | 精确到字符级别 | 缺乏精确坐标映射 |
| 可追溯性 | 完整的bbox_list | 仅模糊描述 |
| 规则灵活性 | 支持动态规则库(RAG) | 依赖模型内置知识 |
| 表格处理 | 结构化解析,可计算 | 描述性输出,难计算 |
综上,对于 合同、标书等长文档审核场景,OCR + RAG 方案在 成本、精度、可追溯性、灵活性 等多个维度均优于VLM方案。
VLM 更适合短文档、强视觉依赖的场景 (如票据识别、图文混排的宣传册审核),法务文档审核这类 "重文本、重逻辑、重定位"的任务,OCR+RAG才是最优选择 。
本文将带你从零实现一个完整的文档审核系统。核心特点是保留 PDF 文档的坐标信息,实现可追溯、可定位的文档审核和修改。
核心流程:

二、效果展示
文档审核 Agent
三、核心代码实现
这里仅展示核心代码实现流程,加入 赋范空间 免费领取完整项目源码,还有更多Agent开发、模型微调等项目案例等你来挖掘!

PDF 解析与坐标提取(核心)
这是系统的基石。MinerU 不仅提取 Markdown 文本,还会返回 content_list,其中包含每个文本片段在 PDF 中的坐标信息。
python
import requests
import json
from pathlib import Path
def parse_pdf_with_mineru(pdf_path: str, output_dir: str = "./temp") -> str:
"""
调用 MinerU API 解析 PDF,关键参数 return_content_list=true
"""
Path(output_dir).mkdir(exist_ok=True)
print(f"开始解析 PDF: {pdf_path}")
with open(pdf_path, "rb") as f:
files = [("files", (Path(pdf_path).name, f, "application/pdf"))]
data = {
"backend": "pipeline",
"server_url": VLLM_SERVER_URL,
"parse_method": "auto",
"return_md": "true",
"return_content_list": "true", # 关键:返回坐标信息
}
response = requests.post(MINERU_API_URL, files=files, data=data, timeout=600)
response.raise_for_status()
result = response.json()
# 保存结果
file_name = Path(pdf_path).stem
json_path = Path(output_dir) / f"{file_name}_output.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
return str(json_path)
加入 赋范空间 免费领取完整项目源码及更多项目案例
其中也包括相关的MinerU、PaddleOCR-VL和DeepSeek-OCR的详细介绍以及如何在本地通过vLLM框架启动解析服务

带坐标的文档智能切分
切分策略
- 按标题层级切分 : 优先在
# H1,## H2,### H3处切分 - 控制片段大小: 每个片段不超过 800 tokens
- 分配坐标: 根据文本内容匹配对应的坐标信息
关键点:切分后的文本片段,必须保留其对应的原始坐标信息。
切分示例
MinerU 返回的原始数据:按照行、段落、表格单元格等自然单位切分的。
json
{
"content_list": [
{"text": "解除劳动合同通知书", "bbox": [359, 95, 638, 120], "page_idx": 0},
{"text": "先生/小姐", "bbox": [199, 164, 373, 181], "page_idx": 0},
{"text": "根据 << 劳动合同法...", "bbox": [170, 200, 847, 255], "page_idx": 0},
// ... 共219个文本块
]
}
重新切分 (chunk_size=800):
- 核心逻辑:一个Chunk → 多个BBox: 每个原始文本块都有自己精确的坐标
json
Chunk 1 (831 tokens):
内容: "解除劳动合同通知书\n\n先生/小姐\n\n根据<<劳动合同法>>..."
bbox_list: [
{bbox: [359, 95, 638, 120], text: "解除劳动合同通知书"},
{bbox: [199, 164, 373, 181], text: "先生/小姐"},
{bbox: [170, 200, 847, 255], text: "根据..."},
// ... 共86个原始文本块的坐标
]
后续的审核逻辑:
json
issue = {
"original": "年 月 日至 年 月 日", # 有问题的原文
"description": "日期填写不完整"
}
# 在chunk的bbox_list中查找匹配的坐标
for bbox_item in chunk['bbox_list']:
if "年 月 日至 年 月 日" in bbox_item['text']:
problem_bbox = bbox_item['bbox'] # 找到了!
problem_page = bbox_item['page_idx']
# 可以在PDF上精确高亮这个位置
核心代码实现
我们实现一个 split_document_with_coords 函数,它会在切分 Markdown 文本的同时,遍历原始的 content_list,将坐标映射到对应的 Chunk 中。
python
from typing import Dict, Any, List
def split_document_with_coords(
md_content: str,
content_list: List[Dict],
chunk_size: int = 800
) -> List[Dict[str, Any]]:
"""
切分文档并精确分配坐标(核心逻辑)
"""
# 1. 简单的按段落切分文本
paragraphs = md_content.split('\n\n')
chunks = []
current_chunk = ""
chunk_start_positions = []
# ... (省略部分基础文本拼接代码,详见完整源码) ...
# 2. 为每个chunk分配坐标
# 逻辑:遍历content_list中的每个bbox,看它的text属于哪个chunk
result = []
for i, chunk_text in enumerate(chunks):
bbox_list = []
for item in content_list:
if item.get('type') == 'text':
text = item.get('text', '').strip()
if not text:
continue
# 在原文中查找这个text的位置
text_pos = md_content.find(text)
if text_pos == -1:
# 尝试规范化匹配
normalized_text = normalize_text(text)
normalized_md = normalize_text(md_content)
text_pos = normalized_md.find(normalized_text)
# 判断这个text属于哪个chunk
if text_pos != -1:
chunk_start = chunk_start_positions[i]
chunk_end = chunk_start + len(chunk_text)
# 如果text在当前chunk的范围内
if chunk_start <= text_pos < chunk_end:
bbox_list.append({
'type': item.get('type'),
'bbox': item.get('bbox'),
'page_idx': item.get('page_idx'),
'text': text,
'content_preview': text[:50]
})
result.append({
'content': chunk_text,
'token_count': estimate_tokens(chunk_text),
'bbox_list': bbox_list # 实现了文本到坐标的绑定
})
return result
构建审核 Agent 的提示词
我们将法务审核标准固化为 Prompt 中的 Context。
这里仅展示一小部分,完整见项目源码,也可根据你的场景进行自定义
python
PROFESSIONAL_CONTRACT_AUDIT_RULES = """
# 合同协议书专业审核规则
## 一、文本规范性审核(P1-P2级)
### 1.1 错别字与形近字检查
- 形近字:"己"/"已"/"以"、"的"/"地"/"得"、"做"/"作"、"账"/"帐"
- 多字、漏字、笔误
- 严重程度:medium
### 1.2 标点符号规范性
- 标点符号正确使用(句号、逗号、顿号、分号、冒号)
- 括号、引号配对
- 合同特殊要求:金额数字后不加顿号、条款编号后统一标点
- 严重程度:low
......
"""
python
PROFESSIONAL_USER_PROMPT = """请对以下合同协议书片段进行专业审核:
【待审核文本】
{text}
【审核要求】
1. 严格按照《合同协议书专业审核规则》逐条审查
2. 重点关注P0级问题(法律风险、权利义务、金额数字、逻辑一致性)
3. 对于每个发现的问题:
- 精确引用原文位置
- 说明违反的具体规则
- 评估严重程度和法律风险
- 给出专业的修改建议
4. 生成修正后的完整文本
5. 编写审核总结
【特别说明】
1. 如果发现书名号显示为 $< <$ 和 $> >$ 等符号,这是OCR识别错误,应归类为【OCR识别问题】而非【法律术语规范性】问题
2. 对于明显的OCR错误(如特殊符号、乱码),请在modifications中提供正确版本,但在issues中标注为"OCR识别错误"
3. 只有当原文确实使用了错误的标点符号时,才归类为【法律术语规范性】问题
请输出审核结果(JSON格式)。
【输出格式】
以JSON格式返回AuditResult对象,确保:
- has_issues: 是否发现问题(布尔值)
- issues: 问题列表(按严重程度排序)
- modifications: 修改映射列表
- corrected_text: 修正后的完整文本
- summary: 审核总结
- overall_risk_level: 整体风险等级(high/medium/low/none)
请开始审核。
"""
基于langChain构建审核 Agent
python
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 创建 LLM
llm = ChatOpenAI(
model="qwen3-max",
temperature=0.1,
)
# 使用结构化输出
structured_llm = llm.with_structured_output(AuditResult)
# 定义 Prompt
audit_prompt = ChatPromptTemplate.from_messages([
("system", PROFESSIONAL_SYSTEM_PROMPT),
("user", PROFESSIONAL_USER_PROMPT)
])
# 创建审核链
audit_chain = audit_prompt | structured_llm
执行审核与结果展示
执行审核代码
将切分好的 Chunk 传入 Agent,即可获得包含法律风险分析、修改建议及原文引用的结构化数据。
python
try:
print("\n正在调用专业审核系统...")
result = audit_chain.invoke({
"rules": PROFESSIONAL_CONTRACT_AUDIT_RULES,
"text": chunks[0]
})
对审核结果进行解析:
python
print("\n审核成功!")
print("\n" + "="*80)
print("【审核结果概览】")
print("="*80)
print(f"是否发现问题: {result.has_issues}")
print(f"整体风险等级: {result.overall_risk_level}")
print(f"问题总数: {len(result.issues)}")
print(f"修改总数: {len(result.modifications)}")
print(f"\n审核总结: {result.summary}")
if result.has_issues:
# 按严重程度分组
high_issues = [i for i in result.issues if i.severity == 'high']
medium_issues = [i for i in result.issues if i.severity == 'medium']
low_issues = [i for i in result.issues if i.severity == 'low']
print("\n" + "="*80)
print("【问题详情】")
print("="*80)
if high_issues:
print(f"\n高风险问题 ({len(high_issues)} 个):")
print("-"*80)
for i, issue in enumerate(high_issues, 1):
print(f"\n{i}. [{issue.rule_category}] {issue.issue_type}")
print(f" 描述: {issue.description}")
print(f" 原文: {issue.original}")
print(f" 建议: {issue.suggestion}")
if issue.legal_risk:
print(f" ⚠️ 法律风险: {issue.legal_risk}")
if medium_issues:
print(f"\n中风险问题 ({len(medium_issues)} 个):")
print("-"*80)
for i, issue in enumerate(medium_issues, 1):
print(f"\n{i}. [{issue.rule_category}] {issue.issue_type}")
print(f" 描述: {issue.description}")
print(f" 原文: {issue.original}")
print(f" 建议: {issue.suggestion}")
if low_issues:
print(f"\n低风险问题 ({len(low_issues)} 个):")
print("-"*80)
for i, issue in enumerate(low_issues, 1):
print(f"\n{i}. [{issue.rule_category}] {issue.issue_type}")
print(f" 描述: {issue.description}")
print(f" 原文: {issue.original}")
print(f" 建议: {issue.suggestion}")
if result.modifications:
print("\n" + "="*80)
print(f"【修改记录】({len(result.modifications)} 处)")
print("="*80)
for i, mod in enumerate(result.modifications, 1):
print(f"\n修改 {i}:")
print(f" 原文: {mod.original}")
print(f" 修改: {mod.modified}")
print(f" 原因: {mod.reason}")
if mod.rule_ref:
print(f" 规则: {mod.rule_ref}")
print("\n" + "="*80)
print("【修正后的文本】")
print("="*80)
print(result.corrected_text)
else:
print("\n文档无问题!")
print("\n" + "="*80)
print("测试通过!数据结构完全兼容!")
print("="*80)
except Exception as e:
print(f"\n测试失败!")
print(f"\n错误类型: {type(e).__name__}")
print(f"错误信息: {e}")
print("\n完整错误堆栈:")
import traceback
traceback.print_exc()
结果展示
text
正在调用专业审核系统...
审核成功!
================================================================================
【审核结果概览】
================================================================================
是否发现问题: True
整体风险等级: high
问题总数: 10
修改总数: 6
审核总结: 本次审核发现该劳动合同相关文书模板存在多项高风险问题:1) 法律术语使用不当,'通知书'应改为'决定书';2) 主体信息缺失,未填写员工姓名和身份证号;3) 经济补偿金表述不规范且适用情形错误;4) 关键日期和金额空白;5) 存在严重歧义性表述。此外还有文本重复、标点不规范等中低风险问题。整体风险等级为high,建议全面修订后再使用。
================================================================================
【问题详情】
================================================================================
高风险问题 (6 个):
--------------------------------------------------------------------------------
1. [法律术语规范性] 法律术语使用不当
描述: 多处使用'解除劳动合同通知书'作为单方通知文件名称,但根据《劳动合同法》,用人单位单方解除劳动合同应出具'解除劳动合同决定书'或类似具有决定性质的文书,'通知书'易被理解为告知而非决定,可能影响法律效力。
原文: 解除劳动合同通知书
建议: 建议统一修改为'解除劳动合同决定书'或'关于解除劳动合同的决定',以体现用人单位单方解除行为的法律性质。
2. [权利义务对等性] 主体信息缺失
描述: 所有文书模板中均未填写员工姓名、身份证号等关键身份信息,仅以'先生/小姐'或空白代替,违反《劳动合同法》关于明确劳动关系主体的要求,可能导致文书无效。
原文: 先生/小姐
建议: 应在每份文书开头明确填写员工全名,并在乙方信息处完整填写身份证号码,确保主体明确。
3. [金额与数字准确性] 经济补偿金表述不规范
描述: 多处经济补偿金条款仅写'计 元',未同时采用大小写金额,且未明确计算依据(如月工资标准),不符合《工资支付暂行规定》关于金额书写规范的要求,易引发争议。
原文: 计 元
建议: 应补充为'计人民币【大写】元整(¥【小写】)',并注明计算基数(如'按离职前12个月平均工资计算')。
4. [必备条款完整性] 关键日期缺失
描述: 所有模板中的日期字段(如'年 月 日')均为空白,未设置默认格式或填写说明,不符合合同成立的基本要件,可能导致文书无效。
原文: 年 月 日
建议: 应统一格式为'____年__月__日',并在使用说明中要求必须填写具体日期。
5. [法律合规性] 经济补偿金适用情形错误
描述: 在'劳动合同到期终止通知书'中直接规定'公司需要支付给您相当于 月工资的经济补偿',但根据《劳动合同法》第46条,劳动合同期满终止时,仅在用人单位不同意续订或降低条件续订而劳动者不同意的情况下才需支付经济补偿。此处表述过于绝对,可能构成违法承诺。
原文: (2)此种情况下:公司需要支付给您相当于 月工资的经济补偿,计元。
建议: 应修改为选择项:'□ 因公司不同意续订劳动合同,需支付经济补偿金...;□ 因公司维持或提高原条件续订而您不同意,无需支付经济补偿金'。
6. [表述清晰度] 歧义性表述
描述: '协商解除劳动合同协议书'第二条'计 元方支付 方经济补偿金 元'存在严重语病和歧义,无法判断支付主体和金额对应关系。
原文: 二、乙方薪资结算至 年 月 日;计 元方支付 方经济补偿金 元;
建议: 应明确为'乙方薪资结算至____年__月__日,共计人民币____元;甲方支付乙方经济补偿金人民币____元'。
中风险问题 (2 个):
--------------------------------------------------------------------------------
1. [逻辑一致性] 条款重复冗余
描述: 文档中'协商解除劳动合同协议书'、'劳动合同到期终止通知书'等模板重复出现两次,内容完全相同,属于明显的文本冗余,可能造成使用混乱。
原文: 协商解除劳动合同协议书...(全文重复)
建议: 删除重复的模板,保留一份即可。
2. [必备条款完整性] 缺少法律依据引用
描述: 部分解除/终止情形未准确引用法律条款,如'过失性解除'选项虽内容正确,但未注明对应《劳动合同法》第39条,不利于员工知悉权利。
原文: (1)过失性解除 口在试用期内证明不符合录用条件的;...
建议: 应在每类解除情形后注明法律依据,如'(依据《劳动合同法》第39条)'。
低风险问题 (2 个):
--------------------------------------------------------------------------------
1. [文本规范性] 标点符号不规范
描述: 多处顿号、逗号使用不规范,如'口不予支付经济补偿金; 口公司需要支付...'中分号应为句号或换行,且选项间应统一使用顿号或分行排列。
原文: 口不予支付经济补偿金; 口公司需要支付...
建议: 每个选项应独立成行,末尾不加标点,或统一使用顿号分隔。
2. [OCR识别问题] 特殊符号错误
描述: 《劳动合同法》书名号显示正常,但部分空格不规范,如'《 劳动合同法 》',属OCR识别导致的多余空格。
原文: 《 劳动合同法 》
建议: 修正为'《劳动合同法》',去除多余空格。
================================================================================
【修改记录】(6 处)
================================================================================
修改 1:
原文: 解除劳动合同通知书
修改:
原因: 法律术语规范性要求
规则: 2.1 法律术语规范性
修改 2:
原文: 先生/小姐
修改:
原因: 明确合同主体
规则: 4.3 必备条款完整性
修改 3:
原文: 计 元
修改:
原因: 金额书写规范
规则: 2.3 金额与数字准确性
修改 4:
原文: 二、乙方薪资结算至 年 月 日;计 元方支付 方经济补偿金 元;
修改:
原因: 消除歧义,明确支付关系
规则: 5.1 歧义性表述
修改 5:
原文: 《 劳动合同法 》
修改:
原因: OCR识别错误修正
规则: OCR识别问题
修改 6:
原文: (2)此种情况下:公司需要支付给您相当于 月工资的经济补偿,计元。
修改:
原因: 符合法律规定,避免违法承诺
规则: 4.1 法律合规性
================================================================================
【修正后的文本】
================================================================================
# 解除劳动合同决定书
【员工姓名】先生/女士
根据《劳动合同法》相关规定,公司依法解除此前您与公司订立的劳动合同(合同期限: ____年__月__日至____年__月__日)。解除您的理由是:
(1)过失性解除(依据《劳动合同法》第39条)
□ 在试用期内证明不符合录用条件的;
□ 严重违反公司依法制定的规章制度的;
□ 严重失职,营私舞弊,给用人单位造成重大损害的;
□ 以欺诈、胁迫的手段使用人单位违背真实意思签订劳动合同致使劳动合同无效的;
□ 员工同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;
□ 被依法追究刑事责任的;
(2)非过失性解除(依据《劳动合同法》第40条)
□ 劳动合同订立时所依据的客观情况发生重大变化,致使原合同无法履行,经当事人协商不能就变更合同达成协议的;
□ 员工不能胜任工作,经过培训或者调整工作岗位,仍不能胜任的;
□ 员工患病或非因工负伤,医疗期满后不能从事原工作,也不能从事由公司安排的其他工作;
(3)经济性裁员(依据《劳动合同法》第41条)
□ 依据企业破产法规定进行重整的;
□ 生产经营发生严重困难的;
□ 公司转产、重大技术革新或者经营方式调整,经变更劳动合同后仍需裁员的;
□ 其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。
您的劳动合同于____年__月__日解除。您需要结算以下薪资和补偿金事项:
(1)您薪资结算至____年__月__日;计人民币【大写】元整(¥【小写】)
(2)此种情况下:
□ 不予支付经济补偿金;
□ 公司需要支付给您相当于【】个月工资的经济补偿,计人民币【大写】元整(¥【小写】)。
您需要按照公司的离职管理制度规定办理离职手续。
通知单位(盖章)
通知时间:____年__月__日
说明:本通知一式二份,送达劳动者一份,用人单位留存一份,涂改无效。
送达记录:受送达人签字: 签收时间:
---
协商解除劳动合同协议书
甲 方:AA公司
乙 方:【员工姓名】(员工身份证号:【身份证号码】)
甲、乙双方于____年__月__日签订了有/无固定期限劳动合同,现由【甲/乙】方提出协商解除劳动合同要求,经甲、乙双方协商一致,同意解除劳动合同,并达成如下协议:
一、解除劳动合同的日期为:____年__月__日;
二、乙方薪资结算至____年__月__日,共计人民币【大写】元整(¥【小写】);甲方支付乙方经济补偿金人民币【大写】元整(¥【小写】);
三、【其他约定事项】;
四、本协议自甲、乙双方签字(盖章)后生效;
五、本协议一式两份,甲、乙双方各执一份,具有同等法律效力,涂改无效。
甲方(盖章): 乙方(签字):
法定代表人或委托代理人(签字盖章):
签约日期:____年__月__日 签约日期:____年__月__日
---
劳动合同到期终止通知书
【员工姓名】先生/女士
公司与您订立的劳动合同(____年__月__日至____年__月__日),即将届满。根据国家和地方相关法律、法规、政策以及劳动合同相关约定,经公司管理层批准,依法与您终止劳动合同。
您需要结算以下薪资和补偿金事项:
(1)您薪资结算至____年__月__日;计人民币【大写】元整(¥【小写】)
(2)此种情况下:
□ 因公司不同意续订劳动合同,需支付经济补偿金,相当于【】个月工资,计人民币【大写】元整(¥【小写】);
□ 因公司维持或提高原劳动合同约定条件续订而您不同意,不予支付经济补偿金。
您需要按照公司的离职管理制度规定办理离职手续。
通知单位(盖章)
通知时间:____年__月__日
说明:本通知一式二份,送达劳动者一份,用人单位留存一份,涂改无效。
送达记录:受送达人签字: 签收时间:
---
[其余文书模板按相同原则修正,删除重复内容,补充主体信息、规范金额表述、明确法律依据]
================================================================================
测试通过!数据结构完全兼容!
================================================================================
四、总结
通过 MinerU (OCR) + Smart Chunking (坐标映射) + LLM (Structured Output) 的组合,我们成功构建了一个具备企业级能力的法务审核 Agent