跳到主要内容

上下文经济学 — 分块与标记怎么算

这一章讲三件事: 模型的「一次能看多长」是一笔要精打细算的预算; 文档太长放不下时,怎么切才不把它切碎; 以及几种切法各自的代价。 对应原书第 3 章中段,是第 09 章(向量数据库)和第 08 章(文档链)的共同地基。

1. 这一章讲什么

第 03 章介绍了上下文窗口——模型一次能考虑的文字总量。这一章把它当一笔预算来管:写提示的人,本质是这笔预算的会计。

主走查:一篇 497 页的营销学教材。 它远超任何模型的窗口。我们要把它切成能处理的小块,并亲眼看到:随手切(每 200 字符一刀)会把词切断,而书里推荐的切法如何让每块都保持完整的意思。每一步旁边都有真实的输出。

先建立量感:一本书约合 18.5 万标记(第 03 章给过这个参照),而当时主流模型的窗口从 8,192 到 12.8 万标记不等(个别新模型号称百万)——整本书塞进去,多数模型装不下,这就是必须动刀的原因。

2. 顶层全景:先算账,再动刀

预算:窗口总标记数 = 输入标记 + 输出标记
│ 超了:请求直接被拒,或回答被腰斩

文档太长 → 分块(把长文本切成小段)

├─ 切错:词断成两截,意思丢了一半
└─ 切对:每块是完整的句子/段落,还带一点重叠


每块单独喂给模型(摘要),或存起来按需取用(第 09 章)

图说:分块不是文本预处理的小事,是「窗口预算」逼出来的核心工程动作[^1]。

3. 核心原理

3.1 先把账算准:标记怎么数

第 02 章讲过标记是切词的最小单位。动手前的第一件实事:你自己写的这段提示,到底多少个标记?

书里用 OpenAI 的 tiktoken 库来数1。它内含几套切分方案(编码),不同的模型用不同的那套——cl100k_base 对应 GPT-4 和 GPT-3.5-turbo,p50k_baser50k_base 对应更老的模型。用错编码,数出来的账就不对。

聊天接口还有一笔隐藏费用。你以为发三条消息就是三段文字的字数?不对:每条消息额外收 3 个标记的「包装费」(消息头尾的角色标记),整个回合的结尾再加 3 个2。书里给的函数 num_tokens_from_messages 就是干这个的:把消息列表逐条数完、加上包装费,才是这次调用真实的花销。提示工程做到省钱这一步,靠的就是这本账。

顺带记住聊天消息的结构:三种角色——system(给模型立规矩)、user(用户的话)、assistant(模型自己的回答);少样本例子在接口里的标准写法,是伪造几轮「用户问、模型答」的历史消息3

3.2 为什么要分块,什么时候别分

分块(chunking)就是把长文本切成较小、可管理的小段4。书里给的好处清单,归根到底是三条:塞得进窗口(不被拒绝、不被腰斩)、省钱(只取要紧的段落)、更快5

不是什么都该切。书里同样给了「别切」的情形:文档本来就短、分析本来就简单、全文只讲一件事——切了反而平添复杂6。这条「何时不切」的清单和「何时切」同样重要,因为每一次切分都在制造边界,而边界就是意思可能丢失的地方。

3.3 切错是什么样:一个词断成两截

主走查进入正题。书里先演示最笨的切法——每 200 个字符硬切一刀。一篇讲数字营销的英文博客,切出来的其中一块长这样7:

…products or services related to what yo
--------------------
u offer.

图说:「you」被劈成「yo」和「u」两半,分属两块。
这是真实输出,不是示意。

词断了还是小事——更常见的是句子拦腰断:前半句的论点在上一块,结论在下一块,两块各自都没头没尾。书里管好的切法有三条标准:保住完整的词(最好保住完整的句子和论点)、跨页的长句别劈开、每块的标记数要配合窗口预算8

按什么单位切,书里给了一张六策略对照表,压缩如下9:

切法适合代价
按句摘要、翻译、判断褒贬超长内容效率低
按段文档分析、主题提取粒度粗,可能漏掉段内细节
按主题分类、推荐要先识别主题
按长度超长文档的粗加工丢上下文
按标记数精确配合窗口预算要先数标记
按难度分级阅读类场景难度的衡量本身就难

句子边界检测有现成工具(书里用 spaCy,一个老牌文本处理库,能把英文文本切成句子)10。这比「见到句号就切」可靠——缩写(Mr.)、小数(3.5)里的点都不是句尾。

3.4 滑动窗口:用重叠换上下文

固定切块有个结构性缺陷:任何落在边界上的意思都会受损。书里的对策是滑动窗口——切块之间故意重叠11:

原文:This is an example of sliding window text chunking.
窗口 20 字符,步长 5 字符,切出来(真实输出):

Chunk 1: This is an example o
Chunk 2: is an example of sli
Chunk 3: example of sliding
Chunk 4: ple of sliding windo
……

图说:相邻两块共享 15 个字符。任何一句话即使被边界劈开,
在相邻块里还有一份完整的。

两个参数管这个窗口:窗口大小管每块多长,步长管每次往前挪多远;重叠量 = 窗口 − 步长。书里的对照:窗口 5、步长 1 时相邻块共享 80% 内容;步长加到 2,共享立刻减半12重叠越多,上下文保得越全,代价是块数变多、处理和调用的总量变大。 准确性优先就加大重叠,省钱优先就减小——这是一个可以明着调的旋钮。

3.5 递归切分:先保段落,再保句子,最后才动词

书里在 LangChain 一章给出的实用默认方案,名字直白:递归字符切分(RecursiveCharacterTextSplitter)——递归就是「不行就换个更细的尺子再试一遍」的自我重复13

它的切法按一把递减的分隔清单来:先试双换行(段落边界),块还太大就换单换行(行边界),还大就换空格(词边界),实在不行才按字符硬切14。效果是:只要有可能,块边界永远落在段落、行、词的自然边界上,而不是随机位置。

主走查收尾:那本 497 页的营销教材,按标记数(每块 500 标记、重叠 50)切完是 776 块15。每一块都是尽量完整的段落或句子——它们就是第 09 章要存进向量数据库、按需取用的单位。

4. 作者的判断与证据

  • 滑动窗口的两张对照图(窗口 5/步长 1 对窗口 4/步长 2)、200 字符硬切的断词输出、497 页→776 块,都是书里真实跑出的结果;
  • 六种切法的优缺点表(原书表 3-1)是作者的整理,没有实验数据支撑——当经验清单用;
  • 书里对工具的选择有明确的偏好声明:tiktoken 是因为它快且对口 OpenAI 模型;spaCy 是句界检测的现成方案。这些是工程取舍,不是「唯一正解」。

判断(我们的,不是书里的): 这一章最容易被低估的一句话是「分块策略和后面『按意思找文本』的质量是同一件事」。切块是「埋点」:你今天切的每一块,都是未来某次查找能取回的最小单位——切碎了,取回来的就是碎片;切大了,取回来的半块是噪音。分块不是预处理,是后续查找质量的上限。 如果错,会错在: 如果窗口大到「整本书随便塞」且足够便宜准确,分块的重要性会下降。书里 2024 年就在讲百万标记窗口了,但「塞得进」不等于「找得准」——长窗口里的查找衰减是另一类问题,本书没有覆盖;我们书架上《RAG with Python Cookbook》的拆解有展开——这套做法叫检索增强生成:先按意思查出相关资料、再让模型照资料回答;缩写 RAG,第 09 章专讲。

5. 边界与局限

  • 这一章只讲「怎么切」,不讲「切多大小合适」——书里自己承认:没有完美的块大小,先用直觉起一个,再建测试集去评16;
  • 特殊内容(# 标签、@、链接、表格)会让按字符的切分出格,书里点了名但没有给解法——遇到表格类文档,需要表格感知的切分器;
  • 标记计数函数里的「每条 +3」是 2023–2024 年 OpenAI 接口的实现细节,供应商可能调整——这类常量要以官方文档当期值为准,书里也这么提醒2

6. 可带走的

  1. 窗口预算 = 输入 + 输出;聊天接口每条消息 +3 标记包装费,整回合再 +3;
  2. 数标记用模型自家那套编码(GPT-4/3.5 用 cl100k_base),拿错编码账就错;
  3. 先问「该不该切」:短文、简单活、单一主题,别切;
  4. 永远别按固定长度硬切——词会断,句会劈;
  5. 要保上下文就用滑动窗口:重叠是旋钮,越大越全、越贵;
  6. 工程默认:递归切分(段落→行→词→字符逐级退让),497 页教材切成 776 块就是这样来的;
  7. 句子边界交给 spaCy 这类工具,「见句号就切」会被缩写和小数坑;
  8. 块的标记数要和窗口预算配套设计——切块是给查找和摘要备料,不是孤立的一步。

7. 原文地图

主题原书章原文位置
分块的好处与时机Ch.3 标准做法text/09-fm-benefits-of-chunking-text.txt:5(搜「Fitting within」) · text/10-fm-scenarios-for-chunking-text.txt:5(搜「When to chunk」)
坏切法与三条标准Ch.3 标准做法text/11-fm-poor-chunking-example.txt:12(搜「isolated words」) · text/11-fm-poor-chunking-example.txt:201(搜「Preserves entire words」)
六策略表Ch.3 标准做法text/11-fm-poor-chunking-example.txt:58(搜「Splitting by sentence」)
滑动窗口Ch.3 标准做法text/11-fm-poor-chunking-example.txt:209(搜「overlapping chunks」) · text/11-fm-poor-chunking-example.txt:252(搜「Chunk 1」)
200 字符硬切Ch.3 标准做法text/11-fm-poor-chunking-example.txt:171(搜「200」)
tiktoken 与编码表Ch.3 标准做法text/12-fm-understanding-the-tokenization-of-strings.txt:17(搜「cl100k_base」)
消息包装费 +3Ch.3 标准做法text/12-fm-understanding-the-tokenization-of-strings.txt:73(搜「tokens_per_message」)
三种角色与少样本消息结构Ch.3 标准做法text/12-fm-understanding-the-tokenization-of-strings.txt:150(搜「system message」) · text/12-fm-understanding-the-tokenization-of-strings.txt:152(搜「system」,指角色清单行)
递归切分Ch.4 LangChaintext/28-fm-selecting-few-shot-examples-by-length.txt:377(搜「character list comprises」)
497 页→776 块Ch.4 LangChaintext/28-fm-selecting-few-shot-examples-by-length.txt:371(搜「776」)
没有完美块大小Ch.4 LangChaintext/28-fm-selecting-few-shot-examples-by-length.txt:293(搜「perfect document size」)

Footnotes

  1. 出处:「Ch.3 标准做法」第 17 段(text/12-fm-understanding-the-tokenization-of-strings.txt:17,搜「cl100k_base」)。tiktoken 是 OpenAI 的 BPE 切分库,查切分效果还可以用网页工具 OpenAI Tokenizer(第 9 段)。

  2. 出处:「Ch.3 标准做法」第 73 段(text/12-fm-understanding-the-tokenization-of-strings.txt:73,搜「tokens_per_message」)与第 99 段(text/12-fm-understanding-the-tokenization-of-strings.txt:99,搜「primed」)。 2

  3. 出处:「Ch.3 标准做法」第 105 段(text/12-fm-understanding-the-tokenization-of-strings.txt:105,搜「chat history」)与第 150 段(text/12-fm-understanding-the-tokenization-of-strings.txt:150,搜「system message」)。

  4. 出处:「Ch.3 标准做法」第 437 段(text/08-fm-mock-csv-data.txt:437,搜「chunking」)。

  5. 出处:「Ch.3 标准做法」第 9 段(text/09-fm-benefits-of-chunking-text.txt:9,搜「Reducing cost」)。

  6. 出处:「Ch.3 标准做法」第 19 段(text/10-fm-scenarios-for-chunking-text.txt:19,搜「When not to chunk」)。

  7. 出处:「Ch.3 标准做法」第 181 段(text/11-fm-poor-chunking-example.txt:181,搜「what yo」)。

  8. 出处:「Ch.3 标准做法」第 201 段(text/11-fm-poor-chunking-example.txt:201,搜「Preserves entire words」)。

  9. 出处:「Ch.3 标准做法」第 58 段(text/11-fm-poor-chunking-example.txt:58,搜「Splitting by sentence」),原书表 3-1。

  10. 出处:「Ch.3 标准做法」第 130 段(text/11-fm-poor-chunking-example.txt:130,搜「spaCy」)。

  11. 出处:「Ch.3 标准做法」第 209 段(text/11-fm-poor-chunking-example.txt:209,搜「overlapping chunks」)。

  12. 出处:「Ch.3 标准做法」第 217 段(text/11-fm-poor-chunking-example.txt:217,搜「window size of 5」)与第 225 段(text/11-fm-poor-chunking-example.txt:225,搜「window size of 4」)。

  13. 出处:「Ch.4 LangChain」第 377 段(text/28-fm-selecting-few-shot-examples-by-length.txt:377,搜「character list comprises」)。默认分隔清单是双换行、单换行、空格、空字符。

  14. 出处:「Ch.4 LangChain」第 381 段(text/28-fm-selecting-few-shot-examples-by-length.txt:381,搜「recursively」)。

  15. 出处:「Ch.4 LangChain」第 371 段(text/28-fm-selecting-few-shot-examples-by-length.txt:371,搜「776」)。原文:497 页的书,按 500 标记/块、50 重叠,产出 776 个文档对象。

  16. 出处:「Ch.4 LangChain」第 293 段(text/28-fm-selecting-few-shot-examples-by-length.txt:293,搜「perfect document size」)。