出海合同关键条款体检
定位与服务边界
这个 Skill 只做一件事——在合同签下去之前,把「关键条款有没有写」和「条款之间有没有自相矛盾」这两类问题一次挑干净。
它不是法律审查。同一句话,站在卖方和买方立场上结论可以是完全相反的;条款对你是否有利、会不会被对方所在地法律推翻、对方有没有履约能力,这三件事脚本明确判不了,也不打算假装能判。
它能判的是那些不需要法律知识也能确定的问题:
- 全文找不到不可抗力条款——写没写,一目了然;
- 同一段既写「提交仲裁」又写「可向人民法院起诉」——这是自相矛盾,不是倾向问题;
- 只写「FOB NINGBO」,不写 Incoterms 版本年份——2010 与 2020 在费用和风险分界上不同;
- 合同金额大写肆万陆仟捌佰,小写 46,000——这是算术,不是法律。
三分钟上手
第一步:把合同导出成文本
支持 .txt .md .markdown .doc .rtf。pdf 不解析(版式提取会丢条款编号,而条款编号是本脚本的定位依据)。Word 直接另存为 .txt 即可。
直接把 --path 指向 pdf 会被明确拒绝(退出码 2),不会给你一份看不出问题的乱码报告。
第二步:先看 demo,知道它能查出什么
python scripts/clause_audit.py --demo --out ./out
会生成一份埋了 11 个问题的示例合同并出报告。
第三步:跑自己的合同
python scripts/clause_audit.py --path ./合同.md --out ./out
批量体检一个目录:
python scripts/clause_audit.py --path ./合同目录 --out ./out
核对合同主体(可选,但签约前值得做):
python scripts/clause_audit.py --path ./合同.md --out ./out --parties "买方公司名,卖方公司名"
传了合同里没有的名称,会报「当事方未在文本中出现」——大小写、简称差异、英文名与注册名不一致都能这样揪出来。
第四步:拿不准的条款,打印人工确认清单
python scripts/clause_audit.py --checklist
输出契约
合同 1 份,问题:阻断 1 / 警告 7 / 提示 4
已出报告:C:/out/合同条款体检报告-20261003-095159.md
报告抬头的问题合计、正文「结论先行」、JSON 的 counts 三处必须一致,自检里有断言盯着。
曾经这三者里只有 stdout 是对的,报告抬头永远显示「阻断 0 / 警告 0 / 提示 0」,
还堂而皇之写「无阻断级问题」——发现问题是靠这条比漏报一条条款重要得多。
产出两个文件(文件名带时间戳):
| 文件 | 用途 |
|---|---|
合同条款体检报告-<时间戳>.md |
给人看。结论先行 → 文件清单 → 问题清单 → 必须人工确认的部分 |
合同条款体检报告-<时间戳>.json |
给程序看。counts + 每份文件的 clauses / lines / issues |
问题分三级:
| 级别 | 含义 | 处理 |
|---|---|---|
| 阻断 | 缺了生效/执行要件,或条款之间自相矛盾 | 签之前必须改 |
| 警告 | 条款写法不完整或不可执行 | 确认后补 |
| 提示 | 完善性建议 | 有余力就改 |
报告章节编号按实际内容动态生成,某章无内容就不出现,编号不会断档。
报告末尾固定有一段边界声明:它是交稿前自检,不是法律审查。
退出码封闭为三个值:
| 码 | 含义 |
|---|---|
| 0 | 体检完成(无论有没有发现问题) |
| 2 | 参数 / 数据错误(路径不存在、参数冲突、目录内无合同、--parties 含非法字符) |
| 3 | 环境错误(输出目录不可写、报告写入失败) |
--fail-on-block 且存在阻断时,退出码为 2。
检查项清单
条款存在性——缺了就报,级别按「缺了会不会导致无法执行」定,不是按「重不重要」排
| 级别 | 条款 |
|---|---|
| 阻断 | 不可抗力、争议解决、价款与支付 |
| 警告 | 签署、交货、法律适用、贸易术语、检验与异议期、风险转移、合同期限、违约责任 |
| 提示 | 包装与唛头、所有权转移、不可撤销、文本效力 |
自相矛盾与数值
| 检查项 | 判据 | 级别 |
|---|---|---|
| 争议解决方式自相矛盾 | 同一条款内同时出现仲裁与诉讼 | 阻断 |
| 仲裁未指名机构与仲裁地 | 只写「仲裁」,机构缩写按词边界匹配 | 阻断 |
| 仲裁已指名机构但缺仲裁地 | 中文「仲裁地」或英文机构后的地点都没有 | 警告 |
| 金额大小写不一致 | 同一条款内比对,每个大写金额都要能在小写金额里找到对应 | 阻断 |
写法可执行性与形式问题
| 检查项 | 判据 | 级别 |
|---|---|---|
| 贸易术语未标版本 | 出现术语码(不区分大小写)但无 Incoterms 年份;逐条判 | 警告 |
| 术语与费用/风险描述错配 | 如 EXW 同时写卖方承担运输 | 提示 |
| 付款方式无比例与账期 | 写了付款方式但没有百分比与天数 | 警告 |
| 信用证条款不完整 | 缺开证、议付、最迟装运期、有效期 | 警告 |
| 违约金只引法律未写标准 | 「按法律规定执行」而无比例 | 警告 |
| 违约金无累计上限 | 有比例但无上限措辞 | 提示 |
| 待定内容残留 | TODO / 待补充 / 待定 / 待确认,否定语境自动降级 | 阻断 |
| 条款编号重复 / 非升序 | 编号解析后比对 | 警告 / 提示 |
| 文末未见签署栏、签署日期、签订地点 | 末尾 1200 字内检索 | 警告 / 提示 |
| 未见附件清单 | 全文无附件字样 | 提示 |
| 模糊表述 | 尽快、适当、合理、as soon as possible、etc. | 警告 |
| 文件带 UTF-8 BOM | 部分签署系统会把它当首字符 | 警告 |
| 全文无条款编号 | 行数多但只切出一段 | 警告 |
条款编号识别:中文「第十二条」、英文「Article 12」、数字「12.」。
标题行本身参与检查——「Article 2 Delivery」里的 Delivery 算数,
别只把关键词写在标题里就算交差,正文里也写一遍,签署方按条款引用时更稳。
触发与不触发
会触发
- 「这份合同帮我看看有没有漏条款」
- 「合同条款有没有问题」「签之前想体检一下」
- 「Incoterms 要不要写年份」
- 「争议解决怎么写」「仲裁条款行不行」
- 「违约金怎么约定」
- 「合同金额大小写对不对」
不会触发
| 场景 | 为什么不做 | 该用谁 |
|---|---|---|
| 起草 / 代写合同条款 | 那是创作,不是体检 | 律师 / 商务 |
| 判断某条款对己方是否有利 | 同一句话买卖双方结论相反 | 律师 |
| 对方主体资格、履约能力、涉诉记录 | 需要外部工商与征信数据 | 尽调 / 第三方 |
| 与对方母国法律是否冲突 | 需要法域知识,脚本明确不内置 | 当地律师 |
| 出口单证之间的一致性 | 属另一个 Skill | overseas-shipping-docs-audit |
| 报价单横向比价 | 属另一个 Skill | 见「实战案例二」提到的比价思路 |
边界原则:拿不准就说「机器判不了,列进人工确认清单」,不猜。
猜错一次比漏报一次危险——它会让你以为已经检查过了。
常见错误与应对
| 症状 | 真实原因 | 怎么办 |
|---|---|---|
| 报「未识别到条款编号」 | 合同没用「第十二条」「Article 12」「12.」这类编号 | 按编号重排一遍;否则按条款定位会全部失效 |
| 条款定位到错误的行 | 同一条款跨页,或编号格式混用 | 统一编号格式(一律用 Article N 或一律用「第N条」) |
| 报「争议解决方式自相矛盾」但你认为不矛盾 | 同一段里确实同时出现了仲裁和诉讼 | 二选一。真到争议时这条无法执行,不是立场问题 |
| 报「贸易术语未标版本」但合同只有一页 | 只写 FOB 没写年份就是会被报 | 补 Incoterms 2020 + 交货地点 |
| 报「金额大小写不一致」 | 大写与小写真的对不上 | 以能读出实际付款金额的那一个为准;本项只在同一条款内比对 |
| 报「付款方式无比例与账期」 | 只写了 T/T 没写几成、几天 | 补预付比例 / 尾款比例 / 账期天数 |
| 「缺少签署条款」误报 | 签字盖章页在独立附件里 | 这是警告不是阻断;确认附件在即可 |
| 模糊表述报了一堆 | 英文合同里 appropriate / etc. 很常见 |
逐条看,确实含糊的改;属于行业惯例的可忽略 |
| 目录里 pdf 全被跳过 | 脚本不解析 pdf | 另存为 .txt 再跑 |
| 输出目录写不进去 | 权限问题 | 退出码 3,换目录 |
| 报「金额大小写不一致」,可大写明明是对的 | 同一条款里既有单价又有总价,早期版本拿第一个小写(往往是单价)去比大写总额 | 已修:判据改为「每个大写都要能在小写里找到对应」。真不一致仍会报,且会列出本条款内所有小写金额 |
| 写了仲裁机构,还是报「未指名机构」;或反过来,没写机构却不报 | 早期版本用子串匹配缩写,calculate 命中 lc、Typically 命中 ica,把最重的检查静默跳过了 |
已修:机构缩写按词边界匹配,且不再把 LC(信用证)当机构。仍建议写机构全称 |
合同里明明写了 Fob Ningbo 却不报版本问题 |
早期版本只认全大写术语码 | 已修:术语码不区分大小写 |
| 全文没有贸易术语,却不报「缺少贸易术语」 | 早期版本子串匹配,principal 含 cip、specified 含 cif,被误判为已有 |
已修:短 ASCII 术语按词边界 |
| 有多个条款写了术语,只报了第一条 | 早期版本判完第一条就退出 | 已修:逐条判,每条都给行号 |
| 报告抬头显示「阻断 0 / 警告 0 / 提示 0」,但命令行明明报了 1 个 | 早期版本汇总时按错下标取级别,取到的是文件名 | 已修;抬头、结论先行、JSON 三处一致。若再见到全 0 而命令行非 0,说明报告被旧版覆盖,重跑一次 |
| 报告里的行号跟原文对不上 | 早期版本标题行不进正文,行号整体少 1 | 已修:标题行作为正文首行,行号与真实行一致 |
| 报告里某条问题写的是绝对路径,其他都是文件名 | 早期版本空文件那条漏出本机绝对路径 | 已修:统一用相对名,不把本机目录结构带进交付物 |
异常与兜底
| 情况 | 行为 |
|---|---|
| 文件为空(导出失败) | 报一条阻断「文件为空」,继续处理下一份,不中断整批 |
| 路径不存在 | 退出码 2,不创建任何目录 |
| 目录内没有可检查的文本合同 | 退出码 2,并列出支持的扩展名 |
| 某份文件读取失败 | 进该文件的问题清单为「文件读取失败」,其余文件照常体检 |
| 合同是二进制乱码 | 编码兜底链 utf-8-sig → utf-8 → gbk → utf-16,全失败按 utf-8 替换字符解析,不崩 |
--parties 含斜杠等非法字符 |
退出码 2(顺带防路径注入) |
--demo 与 --path 同时给 |
退出码 2,不静默丢掉其中一个 |
| 金额大写用词不规范(如「肆万陆仟捌佰元整」写成「肆万陆仟捌佰圆整」) | 两种都认;实在认不出就跳过该项检查,并在报告里不谎报「已核对」 |
| 中文数字解析不了(如写成「肆拾陆仟捌佰」这类不规整写法) | 返回 None,跳过该项——宁可漏报也不给错数字 |
--help / 参数写错 |
退出码 0 / 2,被顶层兜底显式捕获(SystemExit 是 BaseException,except Exception 抓不到) |
| 报告写入失败 | 退出码 3 |
--demo 时输出目录建不出来 |
退出码 3。早期版本这里返回裸 int、成功路径返回二元组,解包抛 TypeError,被顶层兜底吞成退出码 2 加一句「未预期异常」——约定的环境错误码被吃掉了 |
--path 直接指向 pdf 等不支持的类型 |
退出码 2,列出支持的扩展名。与扫描目录时的过滤口径一致:早期版本目录里的 pdf 被跳过、直接指定的 pdf 却被当文本解析出一份全是乱码的报告,拿到的人不知道报告是废的 |
| 条款标题行只写在标题里 | 标题行作为正文首行参与全部检查,不丢 |
| 报告合计与命令行不一致 | 自检有断言:抬头合计、结论先行 措辞、JSON counts 三处与实际检出一致 |
自检:python scripts/clause_audit.py --selftest —— 92 项断言,覆盖中文大写金额解析(含「肆万陆仟捌佰美元整」这种带币种字的写法)、金额大小写一致性(含单价+总价不误报)、条款切分与标题行入正文、行号不错位、仲裁与诉讼并存、仲裁机构词边界、贸易术语版本(含大小写与逐条判)、付款与违约金规则、否定式上下文、退出码封闭、结构不变式(单文件 5 元组 / 汇总 6 元组,两种都要能数对)。
实战案例
案例一:一份合同,挖出 1 个阻断 7 个警告
需求:客户从对方拿回一份英文销售合同,签之前想知道有没有明显的坑。
选型:全文扫描 + 条款级定位。
数据:示例合同(--demo 生成),10 个条款,中英混排。
命令:
python scripts/clause_audit.py --path ./合同.md --out ./out
实际产出(已跑通,真实输出):
合同 1 份,问题:阻断 1 / 警告 7 / 提示 4
| 级别 | 查出什么 | 处置 |
|---|---|---|
| 阻断 | 争议解决方式自相矛盾:同一处既写提交 CIETAC 北京仲裁、又写可向人民法院起诉 | 二选一。这是能否执行的问题,不是立场问题 |
| 警告 | 贸易术语未标版本:只写 FOB NINGBO |
补 Incoterms 2020 |
| 警告 | 付款方式无比例与账期:只写 T/T deposit, balance |
补 30%/70% 与天数 |
| 警告 | 违约金只引法律未写标准:违约金按法律规定执行 |
写清比例与计算方式 |
| 警告 | 缺法律适用条款 | 补准据法法域 |
| 警告 | 缺风险转移条款 | 补风险转移点 |
| 警告 | 模糊表述:Delivery time: 尽快 |
换成具体天数 |
| 警告 | 文末未见签署栏 | 确认附件里有签字盖章页 |
| 提示 | 缺所有权转移 / 不可撤销 / 文本效力条款 | 有余力就补 |
| 提示 | 违约金无上限:逾期交付按合同总额 5% 赔付 |
补一个累计上限 |
关键点:客户最担心的「条款对自己不利」脚本判不了;能判出来的是那条自相矛盾的争议解决——
它跟立场无关,写成那样在争议发生时谁都无法援引,这一条就值回检查成本。
最后那条提示来自标题:Article 10 Late Delivery Penalty 的 Penalty 也在标题里。
早期版本丢掉标题行,这条例外就永远漏了——现在标题行参与检查。
案例二:金额大小写不一致,抓到的是真金白银
需求:财务说合同金额是 46,000 美元,业务说合同写的是 46,800。
选型:同一条款内做中文大写与阿拉伯数字的折算比对。
数据:条款里同时写 Total price: USD 46,000.00 与 大写金额:肆万陆仟捌佰美元整。
命令:
python scripts/clause_audit.py --path ./合同.md --out ./out
实际产出(已跑通):
- **[阻断] 金额大小写不一致:中文大写折算 46800.00 vs 阿拉伯数字 46000.00
关键点:这一条是纯算术,不请律师也能确认。而它一旦在付款环节被发现,损失的是真金白银和一段合作关系。
案例三:单价与总价同条款,不被误判成写错
需求:一份内销转外销的设备报价合同,同一条款里先写单价再写总价,业务担心体检工具会把单价当成总价去比对大写金额。
选型:金额大小写检查判的是「每一个大写金额都要能在本条款的小写金额里找到对得上的」,
而不是「第一个小写 == 第一个大写」。
数据(两条款,第二条的 Penalty 只写在标题里):
Article 2 Price
Unit price: USD 390.00 per set, Total price: USD 46,800.00
大写金额:肆万陆仟捌佰美元整
Article 10 Late Delivery Penalty
逾期交付按合同总额 5% 赔付。
命令:
python scripts/clause_audit.py --path ./合同.md --out ./out
实际产出(已跑通):金额大小写不报错(阻断 2 条是缺不可抗力与缺争议解决,与金额无关),
并照常提示:
- **[提示] 违约金无上限(行 4):Article 10 Late Delivery Penalty
逾期交付按合同总额 5% 赔付。 → 约定过高的违约金可能被调整,写一个累计上限更安全
把大写改成「肆万陆仟元整」后重跑,立刻报出:
- **[阻断] 金额大小写不一致(行 1):中文大写折算 46000.00 vs 阿拉伯数字 46800.00
(本条款内小写金额:390.00、46800.00)
关键点:这条检查的代价不对称——误报一条不存在的阻断,会让人拿着报告去质问对方写错金额,
那比漏报更能破坏一段合作关系。所以宁可放宽到「能对上一个就算对」,也不拿第一个数去猜。
已验证清单
以下都是本机实跑结果,不是设计意图:
--selftest92/92 通过- 错误路径 12 条退出码全部正确(无参数 / 缺 out / demo 与 path 互斥 / 路径不存在 / 目录内无合同 / parties 含斜杠 / 单文件 / checklist / help / fail-on-block / demo 输出目录不可写 / path 输出目录不可写)
- 输出目录不可创建时
--demo与--path都返回 3(不是 2) --fail-on-block在有阻断时返回 2--demo稳定产出 1 阻断 + 7 警告 + 4 提示- 报告抬头合计、
结论先行措辞、JSONcounts三处与实际检出一致 - 标题行参与检查:
Article 2 Delivery不再误报「缺少交货」;
Article 10 Late Delivery Penalty的标题让「违约金无上限」能被检出 - 行号端到端正确:
待定在第 2 行报行 2,尽快在第 4 行报行 4 - 同一条款内「单价 390 + 总价 46800 + 大写 46800」不报金额不一致;
改成大写肆万陆仟后立即报阻断,并列出本条款内全部小写金额 - 批量 4 份(正常 / 空文件 / 乱码二进制 / 带 BOM)一次跑完不中断,
空文件那条用相对名2_empty.md,不漏本机绝对路径 - 中文大写金额解析实测通过:
壹=1、贰拾伍=25、肆万陆仟捌佰=46800、壹拾贰万叁仟肆佰伍拾陆=123456、壹亿贰仟万=120000000 - 带币种字的大写(
肆万陆仟捌佰美元整/人民币整/元整)均可解析 一篮子这类非数字串返回 None(不误判为金额)- 金额大小写不一致能被检出,一致时不报
- 仲裁 + 诉讼并存报阻断;只写「仲裁」不指机构报阻断;
at the CIETAC in Beijing不误报缺仲裁地 - 只写
FOB/CIF不写年份会报「贸易术语未标版本」;写了Incoterms 2020不报 - 报告章节编号连续无断档(4 章)
- 报告固定包含「不提供法律意见」的边界声明
- 否定语境中的「待定」不误报
- 空文件被检出为阻断,且不影响同批次其他文件解析
- 所有问题项结构一致为 5 元组(混进 dict 会导致下游静默取错值)
- 编码兜底
utf-8-sig排第一 - 仲裁机构缩写按词边界:
calculated/Typically不再冒充机构名;CIETAC 仍认得 - 英文短术语按词边界:
principal不冒充 CIP、specified不冒充 CIF - 贸易术语码不分大小写:
Fob Ningbo与FOB NINGBO同样判版本 - 多个条款写术语时逐条判,不再只判第一条