把“找缺失”交给机器,把“做判断”留给人——一个合同检查工具的诞生记
“我希望在工作中能提高效率,合同要检查信息齐不齐全,我可以怎么做不用一个个打开合同就能知道这份合同齐全不齐全,并把信息不齐全的合同标注出来,节省工作时间?”
这是一位学生发给我的原话,一个字没改。
学生选修了我的《人工智能商业应用》课程。其工作日常有一项绕不开的活:查合同。注意,不是审合同——条款合不合理、风险大不大,那是另一个层面的事。学生做的是更基础也更磨人的那件事:核对。编号写了没?甲乙双方写清了没?日期、金额、付款方式、违约责任、签字盖章……每一份合同都要打开,从头扫到尾,扫完在心里默默打个勾。
一份两页的合同,还好。一个文件夹几十份呢?
你可以想象那样一个下午:屏幕上开着第七份合同,你已经不太记得第三份里金额写没写大写,只能回头再翻。累的不是任何一份合同,是"还有几十份"这个念头。
我盯着那句原话看了很久。不是因为问题难,而是因为它太典型了——这是几乎每个职场新人都被磨过一遍的坑,而且大多数人磨着磨着,就习惯了。
习惯,才是最贵的成本。

一、动手之前,先听清楚三个"不行"
听学生说完需求,我定下三条硬约束,每一条都带着一个"不行":
第一,不会写代码。 交付物必须双击就能用,不能要求学生装任何编程环境。这意味着我最后给学生的得是一个 exe 文件——Windows 上双击就能跑的那种。
第二,合同是敏感文件。 涉及公司商业信息的文档,不能上传到任何网上服务去"在线检查"。所有活儿,必须在学生自己的电脑上干完,一个字节都不出门。
第三,“什么算齐全”,还没有标准答案。 公司的规定没有细到"每一份合同必须包含哪 17 项"这种程度。这意味着检查标准不能写死在程序里,得让学生自己能改。
这三条像三堵墙,把方案的形状框死了。但有意思的是,墙框完了,方案也就自己浮出来了:
一个免安装、纯本地运行、检查标准可以自己定的小工具。
技术选型,反而成了整个项目里最不需要纠结的部分。好的需求分析就是这样——不是我想清楚了才动手,是约束替我想清楚了。
二、最先做的,不是写代码,是翻译
拿到需求,我最先做的不是打开编辑器,而是回答一个更根本的问题:
"齐全"这两个字,机器怎么听得懂?
"齐不齐全"是人凭感觉做的综合判断。机器不懂感觉,它只懂一件事:有,还是没有。
所以第一步,是把那种模糊的焦虑,翻译成一张可以逐项回答"有 / 没有"的清单。我按常见采购、供货合同的基本要素,整理出 17 项:
- 12 项必需:合同编号、名称、甲乙方、签订日期、金额、金额大写、付款方式、交付条款、违约责任、争议解决、签字盖章……缺了任何一项,报告里亮红色;
- 5 项建议:统一社会信用代码、验收条款、保密条款、附件清单、份数条款——有最好,没有就黄色提醒一下。
这一步看起来平平无奇,但它是整个项目的地基。工具的上限,在写第一行代码之前就决定了——清单定错了,后面代码写得再漂亮,也只是在精确地查错东西。

[配图 1:检查清单编辑界面截图 —— 三列表格:性质 / 检查项 / 怎么判断]
三、三步,拿一份三色报告
工具用起来只有三步:选文件夹 → 点开始 → 打开报告。

报告是一张 Excel 表:每一行是一份合同,每一列是一项要素,单元格按结论上色——绿色齐全,红色缺失,黄色需要人工看一眼。最右边两列直接写明"缺什么"“需复核什么”,而且问题最多的合同排在最上面,从上往下处理就行,连"先看哪份"都不用你想。

[配图 2:三色报告截图]
有三个设计上的"克制",是我特意留的:
第一,扫描件不硬查。 纸质合同扫描成的 PDF,在电脑眼里是一张图片,工具认不出里面的字。它不会装作检查过了,而是老老实实标黄:"这份我看不到,请人工打开。"一台知道自己边界的机器,比一台什么都敢说的机器可信得多。
第二,老格式不硬吃。 遇到老版 .doc 文件,不冒险解析,提示一句"另存为 .docx 再放回来"。
第三,不替人下结论。 所有的黄色都只是"提醒",所有的判断权都留在人手里。这个后面细说。
四、故事的转折:改清单这件事,最后交给了学生本人
初版方案里,这 17 项清单放在一个配置文件里(JSON,一种记录信息的文本格式,你可以理解成"机器用的表格")。我当时想得很美:以后要增删检查项,让公司里懂技术的同事改改文件就行。
写完我自己先皱了眉:谁来改?
“什么算齐全"从来就不是技术判断。哪种写法算"付款条款写了”、哪些词能代表"盖过章了"——这是天天摸合同的人才有的业务判断。技术同事既不了解,也不该由他拍板。而指望学生本人去改一个机器格式的文件?他会打开,看两行,然后关掉,再也不会碰。
那一版的问题,一句话就能说清:
配置文件只解决了"可配置",没解决"谁来配"。
于是我推翻重来:给清单做了一个窗口,让学生自己改。并且立了一条死规矩——“配置”“正则”“JSON”"关键词"这些词,在界面上一律禁止出现。
取而代之的是四个问题。新增一项检查,学生要做的只是回答:
| 序号 | 界面上的问题 | 举例 |
|---|---|---|
| ① | 这一项叫什么? | 发票要求 |
| ② | 缺了它,算不算问题?(必需 / 建议) | 必需 |
| ③ | 合同里出现什么字,就说明这一项有了? | 发票、增值税专用发票、开票 |
| ④ | 想让它怎么提醒你?合同里通常长什么样? | 合同里说清楚要开什么发票了吗? |
最有意思的是第 ③ 问。它在技术上对应的是"正则表达式"——程序员的复杂搜索规则。但学生看到的,只是"填几个你熟悉的词,用、隔开就行"。他填"盖章、公章、签字",合同里出现任意一个就算通过。
界面背后,这两套语言是这样共存的——同一条检查规则,机器读左半边,人看右半边:
"name": "合同编号",
"patterns": ["合同编号", "合同号", "协议编号", "No."], ← 机器用它搜
"ask": "这份合同的编号写在哪里?在合同里能找到它吗?", ← 人看这个
"example": "示例:合同编号:HT-2026-018",
"why": "没有编号,日后查档、对账时很难快速定位是哪一份合同。"
光"能改"还不够,还得让其不怕改坏。所以配了三个让人放心的设计:
- 保存前自动体检:哪一项没填名字、没填"找什么",当场拦下,不许存一份坏清单;
- 保存前自动备份:旧清单另存一份 .bak 文件,改坏了随时找回;
- 一键恢复出厂:怎么改都能回到默认的 17 项。
最后再加一道小小的仪式感:第一次打开工具,它会先弹出这份清单,请你过目——标题写着"先确认:你要检查合同的哪些内容?“,按钮也不是冷冰冰的"保存”,而是"保存并开始检查"。点下去那一刻,这份检查标准就是他自己的了。以后每次生成的报告末尾,还会附一张"本次检查清单",把当时用的标准留档备查。
一句话:检查标准,你自己定。

[配图 3:新增检查项弹窗截图 —— 四个编号问题]
五、掀开引擎盖:三段代码,其实都不难
项目总共不到两千行代码(1951 行),加上四个自动化测试脚本 467 行。挑三段最有意思的,讲给不怕代码的你听——就算跳过这部分,也不影响你读懂这个故事。
① 读合同,连表格里的字都不放过
读 Word 文档,最直觉的做法是把段落文字一段段抓出来。但合同有个特点:要素最爱藏在表格里——甲乙方信息、金额、签订日期,经常整整齐齐码在一张表里。只读段落,等于查了个寂寞:
# 正文段落,一段段收进来
for para in document.paragraphs:
if para.text.strip():
chunks.append(para.text.strip())
# 表格也不能放过 —— 合同要素最爱藏在表格里
for table in document.tables:
for row in table.rows:
cells = [c.text.strip() for c in row.cells if c.text.strip()]
就这样,段落加表格,一份合同的"全文"才算是凑齐了。
② 永远不让学生因为"填错"而受罚
前面说学生填的"找什么词",程序里会把它编译成搜索规则。但万一手滑填了个奇怪的写法呢?很多程序会选择报错罢工,这段代码选了另一条路:
for pat in item.patterns: # 学生填的"找什么词"
try:
compiled.append(re.compile(pat)) # 试着当成高级搜索规则
except re.error: # 写法太怪,程序读不懂?
compiled.append( # 那就降级成普通文字查找
re.compile(re.escape(pat))) # ——总之,不让他的填写作废
它想传达的姿态是:工具去适应人,而不是人去迁就工具。 填得规范,我搜得聪明些;填得朴素,我就老老实实做文字查找——反正,不会白填。
③ "壹拾捌万陆仟伍佰元整"是多少钱?
合同里金额要写两遍:数字一遍(¥186,500.00),大写一遍(壹拾捌万陆仟伍佰元整)。两遍对不上,是最常见也最要命的错误之一。工具会自动核对,办法是把中文大写金额换算回数字——"壹拾捌"是 18,遇到"万"就乘以一万,"陆仟伍佰"接着累加……几行代码,就是一个微型的算盘:
elif ch == "万": # 遇到"万",结算这一段
section = (section + number) * 10000
total += section
section = 0
number = 0
但真正难的不是换算,是别把不是钱的东西当成钱。合同里数字遍地都是:合同编号是一串数字,统一社会信用代码是 18 位数字,日期、电话、传真全是数字。怎么分辨?靠的不是看数字本身,而是看它的"邻居":
# 一个数字像不像金额,不能只看它自己,要看它前后说了什么
if re.search(r"(编号|合同号|信用代码|电话|日期…)", 上下文):
continue # 邻居说明它不是钱,跳过
这招实测有效:我故意造了一份样例,数字金额写 53,800,大写金额写成"伍万捌仟叁佰元整"(58,300),工具准确报出"金额大小写可能不一致,请人工核对"。
六、踩过的坑,比代码更值钱
三天里踩的坑不少,挑四个说说——都是"看别人的故事疼自己"的那种。
坑一:在我电脑上明明好好的
第一版打包完,在我电脑上跑得好好的,一放进一个干净的空文件夹——启动就崩,报错"没有找到检查清单文件"。
原因说来简单:打包成 exe,相当于把程序连同行李箱一起寄出去。我的代码里有一处去找"家里柜子"的路径,在本机当然找得到,可在学生的电脑上,哪来的"家"?这个 bug 在开发机上永远不会出现——它只在你模拟"学生拿到手的第一秒"时才现形。
修法不复杂:让程序学会从"行李箱内部"读自带资源。但教训更值钱:测试要在最坏情况下做——干净目录、只给一个 exe,当作你从没存在过来测。
坑二:那个永远不会出现的弹窗
这个坑最有戏剧性。上面说的"首次打开先确认清单"的弹窗,功能做了、逻辑测了、测试也通过了——但交付出去的版本里,它永远不会出现。
排查过程像个侦探故事:弹窗的条件是清单里一个叫 confirmed(已确认)的字段为"未确认"。可发出去的清单里,这个字段赫然是"已确认"。为什么?因为开发时测试弹窗流程,点过"保存"——保存出的那份"已确认"的文件,直接被我打包进了交付件。测试的脚印,留在了出厂的产品里。
修完之后,我把当时那句"出厂清单默认是已确认"的注释,改成了一道断言——以后再有谁把测试产物带进交付,测试当场就红。这件事给我的教训,值得单独一行:
"我自己测过"和"它能交付"之间,隔着一条叫"出厂状态"的河。
坑三:18 位数字冒充金额
上面说的金额核对,最初版本闹过笑话:一份合同的统一社会信用代码(18 位数字)被当成了金额,报告里信誓旦旦说"大小写不一致"。数字没有错,错的是我让它孤零零地做判断。加上"看邻居"的上下文过滤之后,误报消失。机器看世界的方式和人不一样——人会看语境,机器只看字符。所以跟机器讲道理,得替它把语境翻译出来。
坑四:改完代码,忘打包
最朴素的一个:改完代码直接去验证 exe,测了个寂寞——新代码还在源码里躺着,exe 还是上一轮打包的旧版本。后来定了一条机械的规矩:每次打包后,核对一遍文件修改时间,exe 必须晚于所有改过的源文件,否则白干。
七、它"不做什么",和它"做什么"一样重要
如果只介绍到上面为止,你可能会以为这是个"替人查合同"的工具。恰恰相反——它从设计第一天起,就拒绝替人做判断。
它只查"形式齐全":该有的项目,有没有。至于金额算得对不对、付款条款吃不吃亏、违约责任是不是霸王条款——它一个字都不说,全部留给人。报告里所有黄色都只是"提醒",所有红色都只是"让你少翻几十份文件、直接走到需要你判断的那一份面前"。
这个边界不是能力不足的遮羞布,而是刻意为之的设计。工具存在的意义,不是替代人的判断,而是把人从不需要判断的地方解放出来——
把找缺失交给机器,把做判断留给人。
尾声:18MB 里装的是什么
复盘一下这个小项目:不到两千行代码,17 项检查,119 个"找什么"关键词,四个自动化测试脚本,三天往返,交付物是一个 18MB 的 exe。
顺带说一句为什么是 18MB——那里面不光有我写的代码,还装着一整个 Python 运行环境和读 Word、读 PDF 的各种零件。打包就是把这一切塞进一个行李箱,让学生那边双击就能用,什么都不用装。
还有一个细节想说:这个工具,是我和 AI 协作完成的。我出方案、AI写代码、跑测试,我提需求、做判断、把关每一步的取舍。三天里我印象最深的瞬间,反而是它写完第一版后,我自己追问的那句"改清单这件事,谁来干?“——机器能干得飞快,但"这件事该由谁来负责、边界划在哪”,得由人想明白。这大概就是这个时代做事的新样子:一个人带着一个真问题,也能调动起一支看不见的团队。
而那个学生提出的问题,本身就是最值钱的部分。学生没有说"我忍一忍",也没有说"这就是命",学生问的是——“我可以怎么做?”
所以,那 18MB 的文件里,装的其实不是代码。
是一句看不见的话:
你不必把生命耗在这种事情上。
愿每一个在某个下午被几十份合同磨着的人,都能在下一份重复劳动面前,想起换个问法:这件事,能不能交给机器?然后,把省下来的那个下午,还给真正需要你的判断、你的耐心、你的那点不服气的事情上去。
——把重复的交给机器,把判断的尊严,留给自己。
如果你的工作里也有这种"一份份打开、一行行核对"的重复劳动,欢迎在留言区聊聊——说不定下一个被翻译成工具的,就是它。
- 点赞
- 收藏
- 关注作者
评论(0)