智能合同审查搭建教程:低质量PDF怎么处理?先解析清洗,再分路审阅(附GitHub项目地址)
项目介绍:这是一个开箱即用的合同风险检测工具。支持上传 PDF/Word 格式的购销、租赁、服务等合同文件,自动识别主体信息缺失、标的物不明、违约责任不完整等法律风险,并输出结构化审查意见与修改建议,结果可溯源至原文页码。适用于企业法务合规审查、业务合同自查及交易对手风险筛查。 GitHub 项目地址:https://github.com/intsig-textin/xparse-sample-projects 接下来我们将讨论实现方法。 当我们的目标是把扫描件、图片件、带水印合同做成可交付的审查工具,重点不是“让模型看合同”,而是先把输入清洗干净,再把不同审查目标拆成不同输出结构。 这类工具真正要解决的是下面几个问题: 因此更合理的目标不是“总结合同”,而是把流程拆成下面这条链路: 建议把这类合同审查工具拆成四层: 这四层分别解决: 真正调用的仍然是 TextIn 的二进制解析接口,而不是 对应代码里的请求方式如下: 这里值得单独解释三个参数: 这一层最重要的输出仍然是: 如果前端要上传文件,通常会在本地后端包一层 合同审查里最常见的误区,是把 OCR 之后的原始文本直接丢给模型,然后希望一次生成所有意见。这样做表面上简单,工程上却不稳。 先统一成 也就是说,PDF、DOC、DOCX、扫描件在进入审阅层之前,都会收敛成同一种输入对象。 合同审查至少有两类完全不同的目标: 如果把这两类问题塞进同一个 Prompt,模型输出很容易混杂,后续展示和报告导出也很难稳定。更合理的做法是:同一份 前端编排层的核心就是并行调用: 这一步的重点不是“并发”本身,而是把两个不同目标明确拆开。 条款风险审阅的 Prompt 先把审查范围写死: 接着把每条问题的输出字段写死: 最终输出结构固定为: 为什么要把 规范审阅的目标完全不同,所以 Prompt 也必须单独定义: 对应输出结构也固定为: 这里多出来的 把这类工具做稳,至少要先对齐下面三件事: 两条审阅链路共用同一份合同 也就是说,Prompt 不是为了“多说几句提示”,而是为了把返回 JSON 的形状提前定下来。 页面展示和 Word 报告导出都依赖这两棵固定 JSON 树: 只有输入和输出先对齐,导出层才不会再做一轮混乱的二次解析。 全文摘要很容易把法律风险、格式问题、修订建议全部混在一起,后续既不方便统计,也不方便导出。 规则很适合检查编号、日期、金额一致性,但对责任边界、违约责任、知识产权、争议解决这些语义问题不够。 更合理的边界是: 以上是一种“先解析清洗、再双链路审阅”的合同审查实践方案。核心思路是把低质量合同(扫描件、拍照件、水印 PDF)先通过 TextIn 统一成干净的 markdown + pages 中间层,再将法律语义风险与文本规范问题拆成两条独立的审阅链路,分别输出固定 JSON,最终合并生成结构化审查报告。方案已发布在 GitHub,欢迎大家在项目中与我们交流。如果你在实际处理合同时遇到更复杂的场景(如手写体干扰、印章遮挡、多版本修订痕迹等),或者有不同的架构思路,欢迎留言或私信探讨。
一、先把目标定义清楚
markdown + pages二、架构应该怎么拆

三、先把解析层输入输出定义对
form-data:POST https://api.textin.com/ai/service/v1/pdf_to_markdownheaders = {
"x-ti-app-id": TEXTIN_APP_ID,
"x-ti-secret-code": TEXTIN_SECRET_CODE,
"Content-Type": "application/octet-stream",
}
params = {
"parse_mode": "auto",
"page_count": 200,
"dpi": 144,
"table_flavor": "html",
"apply_document_tree": 1,
"markdown_details": 1,
"page_details": 1,
"apply_merge": 1,
"crop_dewarp": 1,
"remove_watermark": 1,
"get_image": "both",
}
resp = await client.post(
"https://api.textin.com/ai/service/v1/pdf_to_markdown",
headers=headers,
params=params,
content=file_bytes,
)crop_dewarp=1:用于切边和矫正,适合扫描件、拍照件remove_watermark=1:对带水印合同尤其重要get_image=both:保留页面图像信息,后续做证据页展示更方便{
"code": 200,
"result": {
"markdown": "合同正文 markdown",
"pages": []
}
}/api/parse;但需要记住,真正的解析接口协议仍然是“二进制文件流 + 解析参数”。四、为什么这里一定要先做统一中间层
markdown + pages 有三个直接好处:markdown 负责承载正文、标题、条款结构pages 负责承载分页信息,后续可以挂页面预览或证据定位五、为什么要拆成两条审阅链路
markdown,两份不同 Prompt,两棵不同 JSON 树。callLLM(CLAUSE_PROMPT, markdown)
callLLM(NORM_PROMPT, markdown)六、Prompt 应该怎么写,为什么这么写
1. 条款风险审阅 Prompt
重点审查以下5类风险点:
1. 责任条款
2. 违约条款
3. 知识产权
4. 保密条款
5. 争议解决- issue_type: 必须是 "风险"、"缺失"、"冲突"、"建议" 之一
- description: 问题的详细描述,不超过100字
- value: 从合同中精确引用的相关原文片段
- suggestion: 具体的修改或补充建议{
"clause_review": {
"责任条款": [{"issue_type":"","description":"","value":"","suggestion":""}],
"违约条款": [],
"知识产权": [],
"保密条款": [],
"争议解决": []
},
"parties": {
"party_a": {"name":"","role":"甲方"},
"party_b": {"name":"","role":"乙方"}
}
}parties 也放在这一条链路里?因为条款风险判断天然依赖主体关系,很多责任边界和义务归属都离不开甲乙方身份。2. 规范审阅 Prompt
检查以下4类规范性问题:
1. 错漏
2. 一致性
3. 格式
4. 修订{
"norm_review": {
"错漏": [{"description":"","value":"","suggestion":"","severity":""}],
"一致性": [],
"格式": [],
"修订": []
}
}severity 很重要,因为规范问题后续往往需要分优先级展示和导出。七、输入、Prompt、输出是怎么一一对应的
1. 输入层
markdown。这意味着解析层只做一次,后续所有审阅都面向同一个中间层。2. Prompt 层
clause_review + partiesnorm_review3. 输出层
clause_review 适合按风险类别分组展示norm_review 适合按严重程度和问题类型展示parties 则可以直接进入报告首页或摘要部分八、和传统做法相比,差别在哪里
1. 不是 OCR 后直接全文摘要
2. 不是只靠规则审查
