两天时间,让AI帮我写了个用AI审阅长文档的工具
当老板扔给我三千页文档
那是一个风和日丽的周一上午,老板笑眯眯地(至少在我的记忆中是笑眯眯的)走过来,拍了拍我的肩膀:
“小李啊,这份文档你先看一下,没什么大问题就过一下。”
我接过文件,定睛一看——好家伙,整整一个三千多页 docx文件。我当时的表情大概是这样的:😱 → 😰 → 🤔 → 😏
前两个表情是因为我被文档的体积震撼了;第三个表情是因为我开始思考人生;最后一个表情,是因为我突然想到:我不是有阿里云送的大模型 Token 吗?不用白不用啊!
方案选型:从"龙虾"到"穷折腾"
面对这个烫手山芋,我有几个选择:
方案A:WPS AI 会员
- 优点:一键搞定,界面友好
- 缺点:要开会员,开会员是不可能的开会员的。而且谁知道它会不会偷偷把我的文档喂给它的训练集
方案B:请出我"养"的"龙虾"(OpenClaw)
- 优点:聪明,真的聪明,本地部署的AI Agent,能自主执行复杂任务
- 缺点:杀鸡焉用牛刀,这 Token 消耗……三千页文档审完,我的钱包可能比文档还要干净
方案C:自己造一个
- 优点:免费(用了阿里云送的 Token),可控,还能写博客吹水
- 缺点:需要写代码
(对于爱折腾的人来说也未必是缺点)
作为一名资深"爱穷折腾"的程序员,我毫不犹豫地选择了方案 C。毕竟,重复造轮子是程序员的浪漫,对吧?(其实是因为穷)
思路设计:LLM 不是万能的,但没有 LLM 是万万不能的
既然决定自己造轮子,就得先想清楚几个关键问题:
问题1:LLM 不认识 docx
大模型只认文本,但Word文档是个二进制压缩包(本质上是个披着.docx皮的zip)。直接丢字节流给AI,它可能会以为自己收到了外星信号。
解法:先转文本,再喂给AI。用第三方库读取段落,保持原有结构。
问题2:文档太大,上下文窗口装不下
即使是GPT-4o或者Qwen-Max,上下文窗口也有上限。三千页文档一次性塞进去,模型可能会"失忆"——前面看了啥?不记得了。
解法:切块。把文档切成一个个小批次,每批约2000字符,分批审阅。这样还有个好处:我可以实时看到处理进度,而不是盯着黑屏发呆两小时。
问题3:AI给建议,我总不能一个个去原文里找吧?
假设AI告诉我"第1523页第三段有个错别字",我得翻到那页找到那段,人工定位,人工修改。这效率比我自己看还低。
解法:让AI格式化输出。不仅要告诉我在哪里错了,还要告诉我原文是什么、应该改成什么、为什么改。而且最重要的是——带上段落索引。
问题4:展示不直观,终端渲染不如原生Word
如果只是在终端高亮显示修改建议,用户(也就是我)还得开着两个窗口对比着看,体验极差。开发一个完整的DOCX渲染器?那我这周就别想睡了。
解法:Word本来就有修订模式(Track Changes)!删除的内容带删除线,新增的内容带下划线,还能一键接受或拒绝修改。这不就是为AI审阅量身定做的吗?查询了一下,docx-revisions 库刚好满足我的需求。完美!
问题5:不同场景需要不同严格程有时候只需要抓错别字("的地得"这种),有时候需要润色表达,有时候需要吹毛求疵到标点符号的全角半角问题。
解法:设计三个档位——“认真的实习生”、“负责的编辑”、“吹毛求疵的专家”。用不同的System Prompt控制AI的校对严格程度。
理论可行,开始写代码(用AI写AI工具)
方案想明白了,但写代码?不,我决定让AI帮我写这个AI工具——这叫"以子之矛攻子之盾",或者叫"Recursive AI Development"。
核心架构
整个工具分三层:
- AI校对引擎(
ai_checker.py):负责和LLM打交道,定义输出格式 - 文档处理器(
document_processor.py):负责切分、调用ai_checker进行校对、应用修订 - 交互界面(
interactive_review.py):基于Textual的TUI,实时显示进度
关键代码
1. 结构化输出:Pydantic的妙用
在ai_checker.py中,我定义了校对结果的Schema:
class ProofreadingError(BaseModel):
para_index: int = Field(description="错误所在的段落索引(从0开始)")
original: str = Field(description="包含错误的原始文本片段")
suggestion: str = Field(description="建议修改后的完整文本片段")
reason: str = Field(description="修改的具体原因")
type: str = Field(description="错误类型", enum=["spell", "grammar", "style"])
这里用到了Function Calling(也叫Tool Calling)。它的核心机制是:强制AI调用report_proofreading_errors这个"工具",而非自由发挥写一段话,把结果填到指定的JSON字段里。这保证了输出格式稳定,方便程序解析。
proofreading_tool = {
"type": "function",
"function": {
"name": "report_proofreading_errors",
"description": "报告文本中发现的拼写、语法或风格错误",
"parameters": ProofreadingResult.model_json_schema(),
},
}
调用OpenAI 异步SDK把数据喂给大模型并且等待输出
async def check_text_chunk_with_tools_async(
paragraphs: list, level: str = None
) -> list:
"""
使用工具调用模式校验文本(异步版本,适用于 TUI 交互模式)
Args:
paragraphs: 段落列表,每个元素为 {'index': 全局索引, 'text': 段落文本}
level: 校对档位标识 (intern/editor/expert),默认为 expert
Returns:
错误列表,每个错误包含 para_index、original、suggestion、reason、type
"""
if not paragraphs:
return []
# 获取档位配置
level = level or DEFAULT_LEVEL
level_config = PROOFREADING_LEVELS.get(level, PROOFREADING_LEVELS[DEFAULT_LEVEL])
system_prompt = level_config["prompt"]
# 构建 JSON 格式的段落列表(避免原文干扰)
paragraphs_json = json.dumps(
[{"index": p["index"], "text": p["text"]} for p in paragraphs],
ensure_ascii=False,
indent=2,
)
try:
response = await async_client.chat.completions.create(
model=Config.MODEL_NAME,
messages=[
{
"role": "system",
"content": system_prompt
+ "\n请调用 report_proofreading_errors 工具返回结果。"
+ "\n重要:每个错误必须包含正确的 para_index(段落索引),该索引对应输入 JSON 中的 index 字段。",
},
{
"role": "user",
"content": f"待校验文本(JSON格式,index为段落索引):\n{paragraphs_json}",
},
],
tools=[proofreading_tool],
tool_choice="required",
temperature=0.1,
timeout=60,
extra_body={
"enable_thinking": False
}
)
# 解析工具调用结果
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
raw_args = tool_calls[0].function.arguments
args = json.loads(raw_args)
errors = args.get("errors", [])
# 核心过滤:只保留建议内容与原文不一致的项,且必须有有效的 para_index
filtered_errors = []
for err in errors:
has_valid_index = "para_index" in err and isinstance(
err["para_index"], int
)
has_valid_content = (
err.get("suggestion")
and err.get("suggestion").strip() != err.get("original", "").strip()
)
if has_valid_index and has_valid_content:
filtered_errors.append(err)
return filtered_errors
else:
return []
except Exception as e:
print(f"[ERROR] API 调用失败: {e}")
return []
2. 三档校对强度
PROOFREADING_LEVELS = {
"intern": {
"name": "认真的实习生",
"prompt": "你是一个认真的文档校对专家。请只检查文本中的拼写错误和明显用词不当的问题。不要关注语法、标点或风格问题。",
},
"editor": {
"name": "负责的编辑",
"prompt": "你是一个负责的文档校对专家。请检查文本中的错别字、拼写错误、语法问题和标点符号误用。",
},
"expert": {
"name": "吹毛求疵的专家",
"prompt": "你是一个极其严格的文档校对专家。即使是标点符号错误或不自然的表达也要指出。",
},
}
用户选哪个档位,就把对应的Prompt塞给System Message。简单,但有效。
3. 智能分块策略
document_processor.py里的分块逻辑:
def _split_into_batches(self) -> List[List[int]]:
batches = []
current_batch = []
current_chars = 0
for i, para in enumerate(self.valid_paras):
current_batch.append(i)
current_chars += len(para.text)
if current_chars >= self.MIN_BATCH_CHARS: # MIN_BATCH_CHARS = 2000
batches.append(current_batch)
current_batch = []
current_chars = 0
这里采用按字符数动态切分的策略,而非简单地每N段分一批。这样能保证每批的大小适合LLM处理,不会超出上下文窗口。
4. 精准定位与修订应用
AI返回的错误带有para_index(段落全局索引),程序直接定位到原文的对应段落:
def _apply_revision(self, error: dict):
original = error.get("original", "")
suggestion = error.get("suggestion", "")
para_index = error.get("para_index")
target_para = self.all_paras[para_index]
rp = RevisionParagraph.from_paragraph(target_para)
start_idx = target_para.text.find(original)
if start_idx != -1:
rp.replace_tracked_at(start_idx, end_idx, suggestion, author="AI校对助手")
这里用到了docx-revisions库,使用它的replace_tracked_at方法能在Word文档中插入标准的修订标记——它会生成带有"删除线"和"新增标记"的修订记录,而不是简单替换原文。这样用户可以在Word里看到每一处修改,选择接受或拒绝。
5. 防呆设计:过滤无效建议
AI有时候会"幻觉",比如建议把"你好"改成"你好"(实际没变化)。ai_checker.py里加了过滤:
filtered_errors = []
for err in errors:
has_valid_index = "para_index" in err and isinstance(err["para_index"], int)
has_valid_content = (
err.get("suggestion")
and err.get("suggestion").strip() != err.get("original", "").strip()
)
if has_valid_index and has_valid_content:
filtered_errors.append(err)
没有有效索引的、建议等于原文的,全部过滤掉。宁可漏杀,不可错杀。
TUI界面:让终端也能高大上
虽然 Word 的修订模式很香,但跑在终端里的工具也有它的尊严。我基于 Textual 框架写了一个 TUI(Text User Interface),支持:
- 实时进度条(显示处理百分比)
- 批次导航(←→键切换查看不同批次)
- 原文高亮(黄色标记待修改片段)
- 修改建议内联显示(
[-原文-]{+建议+}的diff风格)
# Diff风格的显示
modified_paragraphs.append(f" [-{escape(orig_display)}-]")
modified_paragraphs.append(f" [green]{{+{escape(sugg_display)}+}}[/green]")
还支持自动跟随模式:默认UI会自动跳转到AI正在处理的批次,用户也可以关闭自动跟随,手动浏览已完成的批次。
成果展示:两天造出的"赛博校对员"
最终成品的使用体验是这样的:
$ python main.py 产品技术白皮书_v8_final_真的最终版.docx



- 启动界面显示三个档位,我选了"吹毛求疵的专家"
- 文档被自动切成156个批次,每批约2000字符
- 进度条开始滚动,批次状态实时更新
- 处理完成后,生成两个文件:
产品技术白皮书_v8_final_真的最终版_修订版.docx:带修订标记的Word文档产品技术白皮书_v8_final_真的最终版_修订记录.json:详细的JSON修订记录
打开Word文档,每一处修改都以标准的修订模式呈现:红色删除线标记删除内容,彩色下划线标记新增内容,批注框显示修改原因。我只需要坐在椅子上,喝着咖啡,逐条点击"接受"或"拒绝"即可。
技术反思:LLM工程的几个心得
1. 结构化输出是王道
不要信任LLM的"自由发挥",用Function Calling/Tool Calling强制JSON输出,配合Pydantic做类型校验,能避免90%的解析错误。
2. 上下文管理是艺术
不是越大越好。大文档分块处理,虽然增加了API调用次数,但提升了准确率,也降低了单点失败的风险。
3. 人机协作的边界
AI负责"找问题"和"提建议",人类负责"做决策"。工具的真正价值在于将人从繁琐的"查找-定位-修改"流程中解放出来,专注于判断,而非取而代之。
这里有个小插曲: 最初我构思的方案其实想用的是另一个更轻量的开源库(具体名字不提了,免得像给它打广告),我也确实在 PyPI 上找到了并安装好了。但我的 AI 编程助手很笃定地告诉我:“这个库不存在,你应该是看错了。”
我说:“真的有,不信你联网搜索!”
AI:“我搜索了,真没有相关记录。”
我:“我都 pip install 成功了,就在我环境里!”
AI:“抱歉,我确实没找到这个库的任何信息,可能是名称有误。”
我(看着终端里已经装好的包,叹气):“……行吧,按你说的 docx-revisions 来吧。”
AI(愉快):“好嘞!”
那个库确实存在,只是太新了,网上资料太少。你看,AI 不是全知全能的(特别是对新事物)。最好的协作模式是:AI 负责快速搭建和验证,人类负责拍板——哪怕拍的是"将就着用"的板。有时候坚持己见是对的,有时候妥协也是对的,关键是要做出能跑起来的东西。
4. 防御性编程
AI可能会幻觉、可能会返回无效索引、可能会建议无效修改。做好过滤、做好try-except、做好日志,让工具在真实世界的混沌中也能稳健运行。
5. "氛围编程"需要架构师坐镇
最近很流行"氛围编程"(Vibe Coding)——你只管提需求看结果,代码怎么跑的全靠AI自由发挥,甚至不用看源码。听起来很美好,像雇了个全能实习生。
但实际操作中,AI有时候会像无头苍蝇一样钻牛角尖。比如让它优化一个函数,它可能陷入"加个缓存→缓存过期有问题→加个锁→死锁了→换个方案→还是第一版好"的无限循环,Token哗哗地烧,问题还在原地打转。这时候就需要人及时喊停:"别折腾了,先用最简单的方式实现。"往往一句话就能让AI回到正轨,事半功倍。
所以关键是:你不需要懂每一处细节(我确实不懂Textual用法),但你要有清晰的产品思维和架构思维。你要像产品经理+架构师一样,框定AI的行为边界——既不能让它过度封装(为了"优雅"而封装八层抽象),也不能让它"写成一坨"(所有逻辑塞在一个函数里)。
毕竟,优雅永不过时,但过度工程和代码屎山同样让人崩溃。AI负责搬砖,你负责设计蓝图,这才是"氛围编程"的正确打开方式。
结语:AI时代的"懒人哲学"
这个工具我写了两天——第一天搭框架让AI写初版,第二天调Prompt修Bug做优化。如果让我自己看那份三千页的文档,可能需要两周;如果让实习生看,可能需要一个月(还得考虑他会不会离职)。
在AI时代,“偷懒"是一种美德,但"科学地偷懒"是一种能力。与其和重复性劳动死磕,不如花两天时间造个工具,让AI成为我们的"赛博牛马”,而我们做那个聪明的"监工"。
毕竟,我们的价值不在于能看多少页文档,而在于能造出多聪明的工具。
如果你也面临着上千页文档的噩梦,希望这篇博客能给你一些灵感。记住:能用 AI 解决的问题,千万不要用手;能自动化的事情,千万不要用眼睛。
毕竟,程序员的终极目标不就是——让机器干活,自己喝咖啡吗?
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)