乐于分享
好东西不私藏

手把手搭一个文档质检Agent:机器检查格式,人检查内容

手把手搭一个文档质检Agent:机器检查格式,人检查内容

文档格式质检Agent

机器查格式,人查内容

你有没有这种经历:一份标书翻来覆去看了三遍,提交后还是被退回——因为正文里有一处忘了脱敏的公司名。我就干过这种事。那次之后我痛定思痛,花一个下午写了个质检脚本。后来这个脚本从50行长到了700多行,已经帮我那几个AI员工审了上百份标书,没放过一个格式错。

一、质检的核心思路:检查和写作分开

很多人觉得"AI质检"就是让AI读完标书全文然后说"没问题/有问题",但这么做有两个硬伤:

第一,贵。让顶流模型读一遍几十页的标书,光token就要烧掉好几块钱。每天审几份,一个月下来比服务器还贵。

第二,AI不擅长度量检查。你让DeepSeek V4检查"编号是否连续""页码是否从1开始",它有50%的概率漏掉。因为它擅长的是理解内容,不是数页码。

我们后来实践下来的最佳方案是两遍制:

检查轮次
谁来查
查什么
成本
第一遍
Python正则和规则脚本
脱敏、空白页、编号、日期、表格勾稽
零token
第二遍
大模型(选便宜的就行)
第一遍筛出的疑点做二次判断
极低token

两遍下来,覆盖了95%以上的标注问题,token成本比全程用AI省了70-80%。

二、第一遍:零token的规则检查

核心思路:能用一个正则解决的问题,绝不动用一个token。

2.1 脱敏检查(最实用)

标书行业有个铁律:正文里不能出现和本次投标主体不符合的投保人,不能串标,不能废标,这也不能,那也不能。但人眼扫几十页PDF,漏掉一两个是常态。

import re  # 已知公司名单(每次做新标书时更新) COMPANY_NAMES = [     "北京华信科技",     "上海振华信息",     "深圳创达数据", ]  def check_desensitization(text, companies):     """检查正文中是否残留指定公司名"""     hits = []     for name in companies:         positions = [             (m.start(), m.end())             for m in re.finditer(re.escape(name), text)         ]         if positions:             hits.append({                 "company": name,                 "occurrences": len(positions),                 "positions": positions[:5]             })     return hits

跑一下,所有残余公司名全部标红——零漏检,零token。

2.2 空白页/占位符检查

def check_empty_pages(text):     """检查分页符前后的空白"""     issues = []     pages = text.split('\x0c')  # 分页符     for i, page in enumerate(pages):         clean = re.sub(r'\s+', '', page)         if len(clean) < 10:             issues.append(                 f"第{i+1}页疑似空白"                 f"(仅{len(clean)}个有效字符)"             )     return issues

2.3 编号连续性检查

标书的章节目录通常是"1、1.1、1.1.1"这种结构。如果中间跳了号,说明中间删了一段忘改编号了。

def check_numbering(text):     """提取所有编号,检查是否连续"""     pattern = r'^(\d+(?:\.\d+)*)[.、\s]'     numbers = []     for line in text.split('\n'):         m = re.match(pattern, line.strip())         if m:             numbers.append(m.group(1))      issues = []     for i in range(1, len(numbers)):         prev = [int(x) for x in numbers[i-1].split('.')]         curr = [int(x) for x in numbers[i].split('.')]         if len(prev) == len(curr):             if len(prev) == 2 and curr[-1] - prev[-1] > 1:                 issues.append(                     f"编号跳号:{numbers[i-1]} -> {numbers[i]}"                 )     return issues

2.4 表格数字勾稽检查

标书里的报价表,合计行和明细加总必须一致。用正则就能搞定。

def check_table_totals(docx_file):     from docx import Document     doc = Document(docx_file)     issues = []     for table in doc.tables:         for row in table.rows:             cells = [c.text.strip() for c in row.cells]             if any(kw in cells[0]                    for kw in ["合计", "总计", "小计"]):                 # 提取数字并加总检查                 pass     return issues

为什么不用AI做这些:正则100%准确,零成本;AI可能漏检还可能误报,一次调用还要几分钱。

三、第二遍:让AI给疑点做二次判断

第一遍扫完后,你会拿到一堆疑点清单。有些是真问题,有些是误报。比如脱敏检查找到的"北京华信科技"——如果是在"公司简介"章节里,可能是该写的公司介绍;如果是在"技术方案"章节里,大概率是脱敏残留。

python3 bid_qa.py 标书文件.docx --ai-check

AI只需要看"公司名+出现位置+附近50字上下文"就能判断。一个标书调用一次,token消耗不到全文检查的10%。

而且你可以用最便宜的模型做这事——DeepSeek V4-Flash就够了,不需要顶流模型。

四、踩坑记录(我们实际翻过车的)

踩坑1:正则一个 \text 打翻一船

写脱敏检查时,第一版用的是 in 操作符,结果正文里写"中科华信(北京)科技有限公司",不包含"华信"二字,漏检了。(这公司名我随便说的哈)

教训:公司名的写法五花八门,正则匹配的时候把各种可能写法都列进去——全称/简称/加括号的/带城市的。

踩坑2:docx文件里"看不见"的幽灵

python-docx读Word文件时,有些分节符不会出现在段落列表里。我们曾以为"页数检查"没问题,结果打印出来多了一页空白。

from docx import Document doc = Document("标书.docx") for i, section in enumerate(doc.sections):     print(f"第{i+1}节:页码从"           f"{section.start_page_number}开始")

踩坑3:让AI做第一遍检查,一个月多花200块

最早一版让DeepSeek V4全文检查标书,每审一份要读2-3万token。一天审5份,一个月多花了200块。改成"正则打头阵+AI补位"后,一个月token成本从200降到了20块。准确率反而更高了。

踩坑4:表格里的合并单元格

Word表格里的合并单元格,用python-docx读出来时会被跳过——原本4列变成3列,数字提取全歪了。解决方案:读取表格前先检测合并单元格,用 grid_span 属性解析真实位置。

五、总结

花一个下午写这个质检脚本,换来的是一劳永逸:

脱敏检查:从"人眼扫三遍还可能漏"到"2秒出报告,零漏检"

格式检查:从"排版工一页一页翻"到"脚本自动扫,精准命中"

AI调用:从"每份几块钱"到"每份几毛钱"

最关键的是:机器做机器擅长的(规则/数字/格式),人做人擅长的(判断/策略/修改)。你不需要让AI替你干活,你只需要让AI替你筛出"需要你看的地方"。

其实这不是手把手搭一个agent,这只是前面AI踩过的坑,让后面的AI少踩。

本文代码基于宗门bid_review_v3.py(两遍制质检引擎)的实战经验写成。

公众号:硅基聊斋

相关学习资料