当输入是一篇报告、一本说明书或一组会议记录时,直接把全部内容塞进一次请求并不稳妥:文本可能超过上下文窗口,也可能让模型难以抓住重点。处理长文本的基本路线是“分块、分别处理、汇总”:先把原文拆成大小合适的片段,再为每个片段生成中间结果,最后让模型根据这些中间结果完成总摘要。本篇只聚焦这条路线,并用 Python 写一个可以先离线验证、再接入模型的最小示例。

为什么不能直接发送整篇文本

模型一次请求能看到的内容有上限,而且输入和预留输出共用上下文窗口。长文本即使勉强放得下,也会带来三个问题:

  • 不可控:文档稍微变长,请求就可能因超限失败。
  • 难验证:一次调用失败后,很难判断是哪个部分导致问题。
  • 信息拥挤:重要段落和大量细节混在一起,摘要质量不一定随输入长度提升。

因此,长文本处理不是简单地“把窗口调大”,而是要设计数据流。一个常见的 Map-Reduce 结构如下:Map 阶段分别处理每个分块,Reduce 阶段合并这些结果。中间结果通常比原文短,最终汇总所需的上下文也更可控。

先定义分块规则

最小实践不需要立即引入向量数据库或复杂框架。可以先按段落切分,再设定每块的最大字符数。这里的字符数只是 token 数的近似指标,适合演示和预处理;生产环境仍应使用目标模型对应的 tokenizer 或接口返回的 usage 校准。

分块时要满足三个原则:

  1. 尽量在段落边界切开,不要从句子中间截断。
  2. 单块留出输出空间,不能把上下文窗口全部用于输入。
  3. 每块都保留必要的标题、编号等来源信息,便于追溯。

下面的函数只使用 Python 标准库。它会先按空行识别段落;如果某个段落本身太长,再按字符数切开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from __future__ import annotations


def split_long_paragraph(paragraph: str, limit: int) -> list[str]:
"""把单个超长段落按字符切开,避免产生空块。"""
return [
paragraph[start:start + limit]
for start in range(0, len(paragraph), limit)
]


def chunk_text(text: str, max_chars: int = 800) -> list[str]:
"""优先按段落合并,超过上限时再切分。"""
paragraphs = [part.strip() for part in text.split("\n\n") if part.strip()]
chunks: list[str] = []
current: list[str] = []
current_size = 0

for paragraph in paragraphs:
pieces = (split_long_paragraph(paragraph, max_chars)
if len(paragraph) > max_chars else [paragraph])
for piece in pieces:
extra = len(piece) + (2 if current else 0)
if current and current_size + extra > max_chars:
chunks.append("\n\n".join(current))
current = []
current_size = 0
current.append(piece)
current_size += len(piece) + (2 if len(current) > 1 else 0)

if current:
chunks.append("\n\n".join(current))
return chunks


if __name__ == "__main__":
document = """第一段介绍项目背景和目标。

第二段记录已经完成的工作,以及目前发现的风险。

第三段说明下一步计划和负责人。"""
for number, chunk in enumerate(chunk_text(document, max_chars=30), 1):
print(f"--- 分块 {number}{len(chunk)} 字符)---")
print(chunk)

这段程序不访问网络,运行它就能验证分块数量、边界和最大长度。current 保存正在构造的块;如果再加入一个段落会超过上限,就先提交当前块。值得注意的是,代码没有默认使用重叠窗口,因为摘要场景首先应保持分块边界清晰。问答场景若担心跨段语义被截断,可以在相邻块之间复制少量上下文,但必须把重叠内容计入长度预算。

为每个分块生成中间摘要

分块只是预处理,真正的摘要需要模型参与。以当前 OpenAI Python SDK 的 Responses API 为例,官方 SDK README 展示了 client.responses.create(...)response.output_text 的用法。密钥仍然只从环境变量读取,模型名也通过环境变量提供:

1
2
3
python -m pip install openai
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_NAME="替换为你可用的模型名"

可以把每个分块包装成明确的任务。下面的函数不打印密钥,也不把密钥写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import os
from openai import OpenAI


def summarize_chunk(client: OpenAI, model: str, chunk: str, number: int) -> str:
prompt = f"""你正在处理长文档的第 {number} 个分块。
只提取事实、关键结论和待办事项,使用中文要点回答,不要补充原文没有的信息。

分块内容:
{chunk}"""
response = client.responses.create(
model=model,
instructions="你是严谨的文档摘要助手。",
input=prompt,
)
return response.output_text.strip()


def summarize_document(text: str, max_chars: int = 6000) -> str:
api_key = os.environ.get("OPENAI_API_KEY")
model = os.environ.get("MODEL_NAME")
if not api_key or not model:
raise RuntimeError("请先设置 OPENAI_API_KEY 和 MODEL_NAME")

client = OpenAI(api_key=api_key)
chunks = chunk_text(text, max_chars=max_chars)
partials = [
summarize_chunk(client, model, chunk, index)
for index, chunk in enumerate(chunks, 1)
]
combined = "\n\n".join(
f"分块 {index} 摘要:\n{summary}"
for index, summary in enumerate(partials, 1)
)
final_prompt = (
"请根据下面的分块摘要写出一份完整中文总结。保留关键事实、结论和待办事项,"
"去除重复内容;如果分块之间存在冲突,请明确指出,不要自行猜测。\n\n"
+ combined
)
response = client.responses.create(
model=model,
instructions="你是负责整合文档摘要的编辑。",
input=final_prompt,
)
return response.output_text.strip()

这里设置了两层调用:第一层把原文压缩为多个局部摘要,第二层只处理局部摘要。若原文极长,局部摘要合起来仍可能超限,可以再做一轮分组汇总,或者限制每个局部摘要的长度。实际项目还应保存分块编号和原文位置,让用户能从摘要回看来源。

常见问题

按字符数切分是不是等于按 token 数切分? 不是。中英文、代码和标点的 token 密度都不同。字符数切分适合作为简单起点;需要严格控制上下文时,应使用目标模型的 tokenizer,并给输出和系统指令预留空间。

为什么不把所有分块摘要直接拼起来? 拼接后的摘要仍有长度上限,而且重复内容会累积。应先测量 combined 的长度,必要时分组汇总,不能假设摘要一定足够短。

分块后句子被截断怎么办? 先提高段落优先级,只有单个段落超过上限时才硬切。对问答任务可以加入少量 overlap;对摘要任务则要记录切分位置,避免同一事实被重复统计。

模型调用中途失败怎么办? 不要丢弃已经完成的结果。把每个分块的编号和摘要落盘,重试时只处理缺失分块;同时为请求设置超时和重试策略。这是下一阶段错误处理与工程化内容的一部分。

小结

长文本处理的核心不是一次发送更多内容,而是把任务拆成可测量、可恢复的步骤:先按段落分块,再分别摘要,最后汇总中间结果。Python 标准库足以完成第一版分块器,模型 SDK 只负责局部摘要和最终整合。字符数可以帮助快速验证流程,但严格的 token 预算、分层汇总、来源追踪和失败恢复,才是把原型变成可靠功能时需要继续补上的工程细节。