qinghuan
← 查找资料并核对引用 / 全部笔记

单元 12 / 查找资料并核对引用

P0 · 必会主线L1 · 基础

把原始笔记变成可追溯的检索片段

先把资料读对、清理好、按意思切成片段并记录版本,再生成向量用于检索。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能保存带出处的片段,比较两种切分方式,并核对关键的跨段证据是否完整保留。

学习理由、准备要求与知识点

为什么学它

如果提取或切分时漏了原文条件,模型后来就拿不到这份依据。先检查资料入库质量,才能谈检索和回答质量。

先读这些内容

知识点清单

  • 提取、转换与写入
  • 原文完整性抽查
  • 文档来源和版本
  • 标题与代码块边界
  • Token 切分预算
  • 片段重叠的代价
  • 稳定片段标识
  • 更新、删除与权限同步

运行前先确认Spring AI切分器源码固定2.0.1;完整有序装箱算法使用Java17与外部给定成本,不实现真实分词。

01先弄懂它是什么

后面的回答要可靠,先得把原始资料读对。PDF、Markdown 或网页进入 RAG 索引前,需要提取文本,并保留标题、代码、表格和来源,再切成便于检索的片段。提取器返回了字符串,不代表内容就完整:重复页眉、表格列错位、代码缩进丢失,都可能变成模型后来使用的错误依据。

片段既影响能否搜到,也关系到权限检查和答案引用。切得太大,会混进多个主题并占用更多上下文;切得太小,又可能把适用条件与结论拆开。可以先问“单看这一段,能否理解它在什么条件下成立”,再决定边界,而不只盯着固定字符数。标题路径和文档版本也应随片段保存,方便回到原文核对。

切分策略还决定更新和删除成本。稳定文档ID、内容版本、片段序号或内容摘要可以帮助追踪变化;如果每次导入都生成随机ID且不清理旧版本,检索会混入过期重复内容。权限变更也必须同步到片段和缓存,不能只更新原文列表。

文中这几个词,先认一认

ETL
提取、转换、加载资料的流程。每一步都应能检查输入是什么、处理后得到什么。
重叠窗口
让相邻片段保留一部分相同内容,减少切开后丢失上下文的问题;代价是多占索引空间和 token。
来源元数据
记录原文位置、版本、授权范围等信息的字段,用来找回来源并判断当前是否可用。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

先按文档结构分出不可随意拆开的段落或代码块,再在预算内组合相邻单元。本题假设每段已有非负且可相加的保守成本,按原顺序尽量装满当前块,放不下则开启新块。成本为二、三、四,预算五时,得到[二,三]和[四],不会重排段落去凑空位。

这里的贪心规则是:按原顺序往当前块里放段落,只要下一段仍放得下,就继续放。为什么它能让块数最少?在顺序固定、成本非负且按本题规则相加的前提下,它的第一块已经装到最远,任何合法方案都不可能用第一块覆盖更多段落;继续比较后面的块,使用相同块数时,它覆盖到的位置也不会落后。因此它能最少用块,但这个结论只针对本题的预算与顺序规则,不表示切出来的文字在语义上最连贯。

不可拆单元自身超限时,需要明确进入专门策略,例如按代码语法或句子进一步拆分,并带回标题与上下文。直接把超长单元塞进块会破坏预算不变量。真实拼接后还要复算Token,因为分词成本不一定严格满足简单相加,连接符和包装也占空间。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart LR
  D["原始文档"] --> E["提取并抽查"]
  E --> S["结构单元与来源"]
  S --> P["有序预算分块"]
  P --> V["最终计量和完整性检查"]
  V --> I["新版本索引"]
  I --> C["验证后切换与清理"]

按顺序看,每一步做了什么

  1. 抽查提取结果与原文的段落、表格及代码一致性。
  2. 记录文档来源、版本、授权和结构边界。
  3. 将不可拆单元按顺序组合,处理自身超限的单元。
  4. 对最终片段复算Token并检查短片段和尾部。
  5. 批量写入新版本,验证后切换并清理旧版本及缓存。

03对照实现代码

Spring AI 2.0.1 TokenTextSplitter 源码 (新标签页)↗

Spring AI 2.0.1,Git tag v2.0.1 · TokenTextSplitter.builder / doSplit

下方为原创框架配置片段,需要Spring AI 2.0.1。完整算法另行实现有序段落装箱,不是TokenTextSplitter等价复制;Token成本由外部提供。

核心机制代码 · java
TokenTextSplitter splitter = TokenTextSplitter.builder()
        .withChunkSize(500) // 目标Token大小,不能直接当作任意路径的硬上限。
        .withMinChunkLengthToEmbed(5) // 短片段过滤会影响资料完整性,应抽查。
        .withMaxNumChunks(1000) // 达到该阈值后的剩余文本处理也需核对。
        .build();
// 编码方式需要与实际模型和计量要求一致;默认值不适用于所有模型。

这个版本先用配置的编码器把文本转成 token,再在目标片段附近寻找标点作为切分位置。截断后的文本还会重新编码,用来确定下次从哪里继续。因此最终片段长度同时受目标大小和句子边界影响,不是每段都机械地截成完全相同的长度。

源码达到循环块数上限后,会把剩余文本作为尾部片段处理;因此maxNumChunks和chunkSize不能简单解释为“所有结果绝不超过这些硬限制”。入库前应重新核验每个输出片段。

源码还按字符长度过滤短片段,并处理换行分隔符。过滤可能丢掉短标题或重要条件,是否合适取决于资料类型,不能只检查向量库最终记录数大于零。

本篇有序段落算法使用事先给定且可相加的成本,是可证明的装箱机制。真实分词在文本拼接后可能改变边界,最终输出仍需用实际编码器复算,不能把整数例题当作通用Tokenizer。

代码先调用谁,再调用谁

  1. 文档读取和来源记录
  2. TokenTextSplitter配置编码与目标大小
  3. doSplit编码并选择边界
  4. 检查短片段与剩余文本
  5. 最终片段复算、写入和版本切换

04动手写一个例子

最少块数的有序段落装箱

输入不可重排段落的非负成本及正预算,输出按索引分组的连续块,使每块成本不超预算并让块数最少。超限单段拒绝。Java17程序以main断言自测。

输入 1
成本[2,3,4],预算5
输出 1
[[0,1],[2]]
输入 2
空段落列表
输出 2
[]

维护当前块成本与段落索引;加入下一段前检查它是否超过剩余空间。若超过,封存当前非空块并开始新块,保持所有段落恰好出现一次。

贪心第一块终点不早于任意方案第一块终点。若前j块的处理终点已经不早,下一块从更晚位置开始,相同预算至少能覆盖另一个方案下一块剩下的后缀;归纳得到块数最少。

证明依赖顺序固定、成本非负且可以相加。允许重排就变成装箱类组合问题;加入语义断点代价后,最少块数也不再是唯一目标,应重新建模而非继续引用同一证明。

为什么这样写是对的所有封存块与当前块按原顺序无遗漏无重复地覆盖已处理前缀,每块总成本不超过预算;当前块是从其起点可容纳的最大前缀。

时间 O(n)额外空间 O(n)

n为段落数,每段校验并加入一次;输出保存n个索引。除输出和当前块外,只需常数计数状态,不包括文本提取与分词成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static List<List<Integer>> pack(long[] costs,long limit){
        if(limit<=0)throw new IllegalArgumentException("limit");
        for(long c:costs)if(c<0||c>limit)throw new IllegalArgumentException("paragraph cost"); // 超长段不偷偷塞入。
        List<List<Integer>> out=new ArrayList<>();List<Integer> current=new ArrayList<>();long used=0;
        for(int i=0;i<costs.length;i++){
            if(costs[i]>limit-used){out.add(List.copyOf(current));current.clear();used=0;} // 剩余预算比较防溢出。
            current.add(i);used+=costs[i]; // 原顺序逐项加入,每段恰好一次。
        }
        if(!current.isEmpty())out.add(List.copyOf(current)); // 不制造空尾块。
        return out;
    }
    static void eq(List<List<Integer>> e,List<List<Integer>> a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        eq(List.of(List.of(0,1),List.of(2)),pack(new long[]{2,3,4},5));
        eq(List.of(),pack(new long[]{},5));
        eq(List.of(List.of(0,1)),pack(new long[]{0,0},1));
        eq(List.of(List.of(0),List.of(1)),pack(new long[]{Long.MAX_VALUE,1},Long.MAX_VALUE));
        try{pack(new long[]{6},5);throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{pack(new long[]{-1},5);throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • 单段超预算需要专门拆分或拒绝。
  • 零成本单元仍保留顺序和索引,不自动丢弃。
  • 不允许重排,不能用本题证明一般装箱最优。
进阶选读 · 后续再读:带语义边界代价的动态规划

场景希望块数、长度浪费与跨章节切分共同优化。

做法定义dp[i]为前i段最小代价,枚举上一块起点j,预算合法时用dp[j]+cost(j,i)转移;章节边界可作为硬约束。

复杂度若区间成本可O(1)计算,时间O(n²)、空间O(n)并可恢复分块;语义代价的评估成本需另外计算。

好处与代价动态规划优化的是明确代价函数,不保证读者认为语义最连贯;应由检索集评估其实际收益。

05项目里怎么用

先做少量资料的人工抽查,再跑全量导入。挑选标题短、表格复杂、代码长和末尾很短的文档,看提取和切分是否遗漏关键条件。数量统计只能发现明显缺失,不能证明每个片段都保留了原意,必须配合内容对照。

导入流程应可重复执行,给文档和片段稳定身份,记录处理配置版本。新版本先写入隔离集合并验证,再切换查询指向;删除原文后同步清理旧片段、摘要及缓存,保留适当审计记录,避免检索仍返回已撤回内容。

重叠不是越多越好。它增加索引体积、检索重复和上下文成本,可能让同一证据挤掉其他主题。比较不同切分方案时固定编码器与查询集,分别看召回、引用定位与答案支持,才能判断边界改动是否有收益。

容易踩的坑

  • 把提取成功当作原文完整性已证明。
  • 把chunkSize或最大块数直接解释成所有路径硬上限。
  • 单段超限时直接放行,破坏后续窗口预算。
  • 更新资料只追加新片段,不撤回旧版本。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
固定Token切分实现直接,预算容易估计可能切断条件与代码结构结构弱的普通文本基线
结构优先切分保留章节与代码语义需处理超长结构单元技术笔记与文档
重叠片段减轻边界信息丢失增加重复和成本评测显示跨边界漏检的资料

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01最少块数等于最好检索吗?

展开参考答案与推导

不等于。算法只优化预算下的块数,语义完整、权限和引用定位是额外目标,需要任务评估。

02为什么最终还要复算Token?

展开参考答案与推导

拼接、分隔符和包装可能改变实际分词成本;输入整数是模型,不替代最终请求的编码计量。

03稳定片段ID有什么价值?

展开参考答案与推导

帮助更新、删除、引用和追溯,但ID设计还需结合文档版本,避免内容已变却继续使用旧证据映射。

换个情况,还会处理吗

01导入后答案总缺少前提,怎么排查?

展开参考答案与推导

查看条件与结论是否被切到不同片段,检查标题继承和重叠策略,再用固定问题验证召回,不急于调整生成提示。

02短标题消失了,应查哪项?

展开参考答案与推导

检查提取后的空白处理和最短片段过滤,尤其框架默认阈值;对照原文确认该短文本是否承担重要上下文。

03新旧文档同时被引用怎么办?

展开参考答案与推导

核对版本过滤、稳定ID与切换清理流程;仅重新Embedding不会自动撤回旧记录。

写代码试一试

01相同预算能否把后面小段提前?

输入 / 条件
成本[4,4,1],预算5
输出 / 预期
[[0],[1,2]]

提示 必须保持原顺序。

展开参考答案与推导

不能输出[0,2]再[1],那虽满足容量却破坏连续顺序;题目目标不是可重排装箱。

02为什么使用List.copyOf封存块?

输入 / 条件
封存后current.clear()
输出 / 预期
已输出块保持内容

提示 输出不能共享同一个可变列表。

展开参考答案与推导

若直接保存current引用,再clear会清空所有对应输出;复制容器后才可重用当前缓冲。

03如何发现遗漏末尾?

输入 / 条件
成本[2,2,1],预算4
输出 / 预期
[[0,1],[2]]

提示 循环后检查非空当前块。

展开参考答案与推导

最后一块不会被下一次超限触发封存,必须在循环后追加,同时避免空输入生成空块。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能保存带出处的片段,比较两种切分方式,并核对关键的跨段证据是否完整保留。

本篇重点加练

内容边界与片段身份影响更新后的可追溯性,重点检验局部编辑造成的传播。

  • 标题与代码块边界
  • 稳定片段标识

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    选择一页含标题、表格和代码的自造笔记,定义提取、转换、写入各阶段的输入输出,并制定原文完整性抽查方法。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点提取、转换与写入 · 原文完整性抽查

  2. 作业 02代码推演

    为同一文件的两个版本设计元数据,按标题与代码块边界画切分结果,标出片段与原文位置的映射。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点文档来源和版本 · 标题与代码块边界

  3. 作业 03原理问答

    给片段身份制定一个稳定性合同,分别说明插入前言、移动章节和只改代码注释时,哪些身份需要保持、哪些必须变化。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点标题与代码块边界 · 稳定片段标识

综合应用

  1. 作业 04面试场景

    一段代码超过预算且跨段解释依赖前文。比较两种切分方案的预算、重叠与检索影响,写明哪些内容不能静默丢弃。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点Token 切分预算 · 片段重叠的代价

  2. 作业 05编程实践

    实现小型导入器,支持重复导入、改一段、删文档和改权限,测试稳定片段ID与索引更新的一致性,记录实际受影响条目。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点稳定片段标识 · 更新、删除与权限同步

  3. 作业 06排错实验

    故意在转换阶段丢掉一行代码,设计能发现该缺损的核对记录,再检查错误版本是否已写入检索库。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点提取、转换与写入 · 文档来源和版本 · 稳定片段标识

  4. 作业 07编程实践

    为含两个标题和一个跨行代码块的笔记实现小切分器,依次进行三种局部编辑;记录片段边界、身份和原文位置,并检查声明的稳定性合同。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点标题与代码块边界 · 稳定片段标识

  5. 作业 08排错实验

    两段内容完全相同却处在不同章节,其中一段被删除。构造导入与更新记录,检查基于内容或位置的身份政策能否保留正确来源。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点标题与代码块边界 · 稳定片段标识

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    在相同查询标注集上对照两种切分预算,分开统计跨段证据遗漏、重复候选和上下文成本,不以片段数作为唯一指标。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点标题与代码块边界 · Token 切分预算 · 片段重叠的代价

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    把检索片段的身份与边界设计迁移到增量编译或日志归档的纸面场景,比较哪些“内容相同”仍不能视为同一个实体。

    提交内容
    原场景与目标场景的对应表、可迁移的机制、不能迁移的前提及验证设计。
    怎样算完成
    • 逐项核对两种场景的前提,不只比较相似术语。
    • 至少一个不能直接迁移的边界有具体检验方法。

    练到的知识点标题与代码块边界 · 稳定片段标识

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗