当老板扔给我三千页文档

那是一个风和日丽的周一上午,老板笑眯眯地(至少在我的记忆中是笑眯眯的)走过来,拍了拍我的肩膀:

“小李啊,这份文档你先看一下,没什么大问题就过一下。”

我接过文件,定睛一看——好家伙,整整一个三千多页 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"。

核心架构

整个工具分三层:

  1. AI校对引擎ai_checker.py):负责和LLM打交道,定义输出格式
  2. 文档处理器document_processor.py):负责切分、调用ai_checker进行校对、应用修订
  3. 交互界面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

在这里插入图片描述

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/0a271f5365a54ae890715ac7976bc97a.png
在这里插入图片描述

  1. 启动界面显示三个档位,我选了"吹毛求疵的专家"
  2. 文档被自动切成156个批次,每批约2000字符
  3. 进度条开始滚动,批次状态实时更新
  4. 处理完成后,生成两个文件:
    • 产品技术白皮书_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 解决的问题,千万不要用手;能自动化的事情,千万不要用眼睛。

毕竟,程序员的终极目标不就是——让机器干活,自己喝咖啡吗?

项目开源地址https://gitee.com/zhendongdong/ai_revisions

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐