这是「AI 之路进阶升级指南」第二周 Day 6 的配套练习。你需要先完成 Day 5,再回来动手。

Day 5 你写了 read_file() 函数和批量脚本骨架。现在脚本能识别各种格式的文件了。但还没真正跑过一次完整流程:选场景、调 API、保存结果,这个链路还没串起来。

今天做一件事:选一个真实场景(批量翻译、批量摘要、或批量改写),把整个批量处理流程跑通。


三选一:选你最需要的

批量处理的价值在于重复劳动自动化。三个常见场景:

批量翻译:一批英文文档需要翻成中文,或者反过来。比如产品文档、用户评论、技术笔记。

批量摘要:一堆会议记录、用户反馈、研究论文,需要提炼要点。

批量改写:一批文档需要统一格式或语气——比如把口语化的笔记改成正式的报告,或者统一产品描述的措辞。

选一个你当下最需要的场景,后面跟着做。


批量翻译:一个完整可跑的脚本

以批量翻译为例。假设你有一个 docs/ 文件夹,里面是一批英文产品文档:

docs/
  feature-overview.md
  faq-en.md
  changelog-v2.md

目标:每篇文档翻译成中文,结果存到 translations/ 文件夹。

API 调用有输入长度限制:不能把整篇长文档一次性塞进去。

模型处理超长输入时质量会下降,很多 API 也有 token 上限,硬塞进去可能直接报错。

正确的做法是:分块翻译:把文档切成一段一段,每段单独调 API 翻译,最后拼回去。但翻译和摘要不同——不能按固定字符数截断,必须保证句子完整:截断在句子中间,翻译出来的东西谁也看不懂。

下面的脚本实现了这个逻辑:

import os
import glob
import re
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

api_key = os.environ.get("DEEPSEEK_API_KEY")
if not api_key:
    raise ValueError("未设置 DEEPSEEK_API_KEY,请检查 .env 文件。")

client = OpenAI(
    api_key=api_key,
    base_url="https://api.deepseek.com"
)

MAX_CHARS = 4000  # 每个片段最大字符数

def split_into_sentences(text, max_chars):
    """按句子边界切分文本,保证每个片段不超过 max_chars。
    
    先按段落分割,段内再按句子标点分割,确保不截断在句子中间。"""
    paragraphs = text.split("\n\n")
    chunks = []
    current_chunk = ""

    for para in paragraphs:
        para = para.strip()
        if not para:
            continue

        # 段内按句子标点分割(中英文标点都支持)
        sentences = re.split(r'(?<=[.!?。!?])\s*', para)

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue

            # 如果加上这个句子会超出限制,先保存当前块
            if len(current_chunk) + len(sentence) > max_chars:
                chunks.append(current_chunk)
                current_chunk = sentence
            else:
                if current_chunk:
                    current_chunk += "\n\n" + sentence
                else:
                    current_chunk = sentence

    # 保存最后一个块
    if current_chunk:
        chunks.append(current_chunk)

    return chunks

def translate_chunk(chunk_text, prompt):
    """调用 API 翻译单个片段"""
    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=[
            {"role": "system", "content": prompt},
            {"role": "user", "content": chunk_text}
        ],
        temperature=0.3,
        max_tokens=500
    )
    return response.choices[0].message.content

def translate_file(input_path, output_dir):
    """分块翻译文件,逐块调用 API,拼接结果"""
    prompt = "你是一个翻译助手。将用户提供的文本翻译成中文,保留原始格式。"

    # 内层循环:按句子分块 + 逐块翻译
    with open(input_path, "r", encoding="utf-8") as f:
        text = f.read()

    if not text.strip():
        print("  跳过(内容为空)")
        return

    sentences = split_into_sentences(text, MAX_CHARS)

    if not sentences:
        print("  跳过(内容为空)")
        return

    translated_chunks = []
    for chunk in sentences:
        translation = translate_chunk(chunk, prompt)
        translated_chunks.append(translation)

    # 拼接所有块的翻译结果
    full_translation = "\n\n".join(translated_chunks)

    # 如果原文被分成了多个块,加注记
    if len(translated_chunks) > 1:
        full_translation += f"\n\n> 注:原文较长,已分段翻译,共 {len(translated_chunks)} 个片段。"

    filename = os.path.splitext(os.path.basename(input_path))[0] + ".zh.md"
    output_path = os.path.join(output_dir, filename)
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(full_translation)

def main():
    input_dir = "docs"
    output_dir = "translations"

    if not os.path.exists(input_dir):
        print(f"错误:找不到输入目录 '{input_dir}'")
        return

    files = glob.glob(os.path.join(input_dir, "*.md"))

    if not files:
        print(f"错误:'{input_dir}' 里没有 .md 文件")
        return

    os.makedirs(output_dir, exist_ok=True)

    print(f"准备处理 {len(files)} 个文件...")
    print(f"输入: {input_dir}/")
    print(f"输出: {output_dir}/")
    print("-" * 40)

    # 外层循环:遍历所有文件
    for i, filepath in enumerate(files, 1):
        filename = os.path.basename(filepath)
        print(f"[{i}/{len(files)}] {filename}", end=" ")

        translate_file(filepath, output_dir)
        print("OK")

    print("-" * 40)
    print(f"完成!结果保存在 {output_dir}/")

if __name__ == "__main__":
    main()

脚本的关键逻辑:

翻译指令system 提示词指定"翻译成中文,保留原始格式"。

句子边界切分split_into_sentences() 函数先按段落分割(\n\n),段内再按中英文句子标点(.!?。!?)切分,保证不截断在句子中间。这是翻译分块的核心——不能像摘要那样随便截。

进度显示[1/4], [2/4]… 批量跑的时候知道进度,比干等着强。

输出文件名加 .zh 后缀:比如 feature-overview.mdfeature-overview.zh.md,在原名和扩展名之间插入 .zh 标记翻译文件。

分块翻译:按句子分块,每块调 API 翻译,循环直到文件读完。长文件会被切成多个片段分别翻译,结果拼接在一起。超过 4000 字符时会在输出中加注记提醒读者。


批量摘要:改两行就能跑

摘要场景和翻译脚本结构一样,区别只在两处:

1. 提示词换成摘要指令:

prompt = "你是一个摘要助手。将用户提供的文本提炼为简洁的要点,保留关键信息。"

2. 输出目录改为:

output_dir = "summaries"

一个重要的区别:摘要可以使用固定字符数分块:翻译必须尊重句子边界,因为半句话在任何语言里都看不懂。但摘要是有意丢失信息的——你本来就在精简内容。按固定字符数(比如每 4000 字符)切分,逐块摘要,最后得到的是"摘要的摘要",边界精度没那么重要。

如果想简化,可以为摘要场景把 split_into_sentences() 替换成简单的定长分块器。不过保留句子分块也行——只是会更保守一些。

批量改写:统一文档语气

改写的场景也很常见——比如你把会议笔记快速写成草稿,但语气太随意,需要 AI 帮你改成正式报告的风格。

同样可以用上面的脚本,改两处就行:

提示词改成:

prompt = "你是一个文案改写助手。将用户提供的文本改写成正式、专业的书面报告风格,保留所有关键信息。"

输出目录:output_dir = "rewrites"


动手试试

docs/ 文件夹里放几篇你的真实英文文档,然后运行:

uv run python batch_translate.py

你会看到进度条式输出,每个文件处理完显示 OK。到 translations/ 文件夹看结果。

试试换一个场景——把提示词改成摘要或改写,再跑一次。看看同一个脚本,换个提示词,效果差多少。


进阶挑战

如果你手上有英文电子书(.epub 格式),可以试着用 AI 写一段代码来翻译它。

思路:先搜一下 “Python 读 epub 文件”,找个现成的库把 .epub 里每一章的 HTML 内容抽出来,然后接进今天的翻译脚本。split_into_sentences() 需要稍作修改——HTML 里的句子被 <p><br> 等标签打断,不能直接用 \n\n 分段。

这是 Day 6 没覆盖的场景,但用的是今天学到的同一个模式:分块、调 API、拼接:你已经有模板了,试试能不能跑起来。


今天做到的事

  • 选了一个批量处理场景(翻译/摘要/改写)
  • 写了完整的批量脚本,能遍历文件夹、逐个处理、保存结果
  • 加了进度显示,跑批处理时知道进度
  • 理解了 API 有长度限制,长文件需要分块处理

下一步:脚本跑起来后你会发现一些问题——网络超时、API 报错、单个文件处理失败导致整个批处理中断。Day 7 给脚本加上错误处理:超时重试、限流等待、异常日志。


脚本跑不起来?先看错误信息(不要贴 API Key),检查 .env 文件位置、文件夹名、网络连接。