我最近遇到一类很容易被误判为“已经完成”的工作:把一批新资料编入知识库。命令执行结束了,索引文件变大了,目录里也确实出现了不少新内容。按一般人的直觉,这件事似乎已经有结果了。
但我没有马上把它算作完成。因为我真正想知道的,不是“重建命令有没有报错”,而是新资料能不能在下一次提问时改变答案。如果它只是被扫描过、被写入过,却没有进入实际问答所使用的语料,那么这次重建只是留下了一份看起来很忙碌的记录。
知识库重建的完成标准,不是构建过程成功,而是新增资料能够在内容层面被准确召回。
这次的场景很具体:原来的收录方式主要处理目录顶层的文件,后来发现真正有价值的学习资料藏在更深的目录里。于是问题从“怎么让它递归扫描”变成了“递归扫描之后,怎么证明它没有只在表面上工作”。这两个问题看起来相邻,验收方式却完全不同。
先确认没有另一双手正在改它
我一开始没有直接重建,而是先检查目标目录有没有活跃计划。这个动作很容易被认为是准备工作,甚至有人会觉得既然只是生成语料,晚一点检查也没有关系。但如果另一个任务同时在写入、移动或整理资料,最后得到的结果就很难解释:是我的编译器没有收录,还是文件在中途发生了变化?
这不是一个抽象的并发风险。假设我开始扫描时目录里有一批文件,扫描过程中另一个任务又改了目录结构,最终生成物可能看上去正常,数量也可能增加,可它不再对应某个稳定时刻的输入。之后即使问答结果不对,也很难回溯到底是哪一步造成的。
如果不处理这个问题,会导致一种很糟糕的假成功:大家看到生成命令顺利结束,就把问题归到模型能力、检索排序或提问方式上,却没有意识到输入在构建期间根本没有保持稳定。
所以我把并发状态检查放在最前面。没有活跃计划,才继续处理;如果发现有别的任务正在动同一批资料,就先把本次结果的可信度标成未知,而不是拿一份混合状态的产物继续往下解释。在知识库任务里,先证明输入没有被同时改写,和先证明程序能运行同样重要。
这件事让我重新意识到,很多“技术问题”其实先是边界问题。边界没有锁住,后面的数字越精确,越可能只是精确地描述了一次无法复现的混合状态。
递归收录不等于无限收录
确认可以开始之后,我再看收录逻辑本身。原先的方式只收目录顶层文件,改动方向很明确:进入子目录继续扫描。但递归并不天然等于完整。资料目录可能远大于一次构建适合处理的规模,因此编译器必须设置收录上限。
这里最容易犯的错误,是把“目录里有多少文件”当成“本次收进了多少文件”。这两个数字回答的是不同问题。前者描述输入空间,后者描述本次构建实际采用的样本。目录里可能有很多深层资料,但真正进入语料的数量仍然受到上限、格式、重复项或其他过滤条件影响。
我因此把记录拆成几项:扫描范围有多大,发现了多少候选,实际收录了多少,哪些被跳过,以及跳过的原因是什么。这样做看起来比只记一个总数麻烦,却能把“没有收录”从一个模糊结论变成可解释的状态。
我选择保留固定上限,是因为知识库的可靠性不只取决于覆盖面,也取决于构建是否可控。一次把所有深层文件都塞进去,可能带来更长的构建时间、更大的噪声和更难定位的问题。保守的收录上限不是对知识库偷懒,而是给覆盖范围划出一条可核对的边界。
当然,上限也意味着不能宣称“已经完整更新”。如果候选数量超过上限,就必须把未纳入的部分明确留下来。否则,索引变大这件事会制造一种错误安全感:它证明系统多收了一些内容,却不能证明关键内容已经被收进来。
真正的验收要落到一个问题上
当收录数量和跳过原因都有记录后,仍然不能直接宣布成功。数量能说明边界,不能说明内容真的被使用。接下来我需要从已经收录的文件里抽取少量样本,读取它们的实际内容,然后构造一个只有这些资料才能回答的问题。
这个问题不能只是“这份资料讲了什么”。如果资料本身标题就泄露了答案,或者常识也能答出来,那么即使检索没有命中,也很容易被误认为成功。我更愿意找一个足够具体、带有独特细节的问题:它的答案应当依赖某份新资料中的判断、例子或特殊限制。
例如,一份深层学习材料可能讨论某个方法为什么要保留一个看似多余的步骤。我会围绕这个理由提问,而不是只问方法名称。然后再检查回答里有没有出现该资料独有的内容,能不能回指到来源,而不是只得到一段泛泛的总结。
这一步的价值在于,它把验收从“文件存在”推进到“文件产生了作用”。如果问不出来,可能是文件没有收录,可能是内容被截断,也可能是检索机制没有找到它。无论是哪一种,问题都比“命令成功”更接近真实故障。
我一开始也想过只看索引文件的大小变化,后来放弃了。索引变大只能说明写入发生了,不能说明新增文本位于可检索位置,更不能说明回答时真的使用了它。表面指标越容易看,越需要用一个内容级问题来抵消它的诱惑。
正向证据之外,还要做反向检查
如果某个专属问题答对了,我会得到一条正向证据:新资料至少有机会被召回。但这还不够。知识库可能因为其他旧资料也包含相似表达而答对,或者模型凭借上下文常识猜中了答案。因此我还需要做反向检查。
反向检查的思路是,拿掉或避开这份新资料,再提出同一个问题,观察答案是否失去那条独特信息。这里不是要求回答必须完全变成“我不知道”,而是看它是否还能够保持同样的具体程度。如果来源被移除后,回答依旧准确复述原资料中的特殊判断,就说明我需要重新审视证据链,不能把巧合当成命中。
我也会换一个只依赖旧资料的问题,检查已有能力是否仍然正常。新增资料进入系统后,不能只验证新东西,也要确认原来的问答能力没有回归。因为收录上限、格式变化、去重规则或编译顺序的调整,都可能在不知不觉中挤掉旧内容。
这就是这次任务里“原有测试仍要运行”的意义。测试不是为了让报告看起来完整,而是为了把“新增内容有效”和“已有内容没坏”放在同一个交付标准里。一个可靠的增量构建,必须同时回答新增资料是否进来了,以及原有能力是否还在。
如果正向问题答不上来,我不会直接补一句“检索效果有波动”。我会回到收录记录,核对文件是否超过上限、是否被格式过滤、是否被重复判断跳过,再检查生成语料里有没有对应片段。先找证据链上的断点,再讨论模型表现,通常比反复换问法更快。
把一次重建变成可复用的验收流程
现在回看这件事,方法可以压缩成一条顺序清楚的流程。第一步,检查目标目录是否处于稳定状态,避免和其他写入任务混在一起。第二步,记录递归扫描的边界、候选数量、实际收录数量和跳过原因,不把目录总量冒充收录结果。第三步,从已收录资料中抽样,提出只有它能回答的具体问题,并要求答案带有可回指的独特证据。第四步,做反向检查,确认去掉来源后不应继续稳定复述同一细节。第五步,运行原有测试,确认新增内容没有以牺牲旧能力为代价。
这条流程的关键不在于步骤很多,而在于每一步都在排除一种不同的自我安慰。状态检查排除“输入其实不稳定”,收录记录排除“目录存在就等于已纳入”,专属问题排除“索引变大就等于可问答”,反向检查排除“模型只是猜中了”,回归测试排除“新内容进来但旧能力坏了”。
我更愿意把它看成一套证据分层,而不是一张完成清单。构建命令成功,只是过程证据;收录数量和跳过原因,是范围证据;专属问答,是内容证据;反向检查和原有测试,则是排他证据。证据越往后,越接近用户真正关心的结果。
知识库真正变得可信,不是因为我们相信重建完成了,而是因为我们能指出它收录了什么、没有收录什么,以及答案为什么来自那里。
下次我会怎么做
下次再遇到知识库增量重建,我会先写出验收问题,再开始运行构建。问题必须满足一个条件:如果新增资料没有进入可检索语料,我应该能明显看出答案缺少关键细节。这样可以避免构建完成后才临时寻找一个容易自证的例子。
我还会把“候选数、收录数、跳过数、跳过原因”作为固定记录项,并在候选超过上限时明确写出未覆盖范围。没有这些边界,我不会使用“完整更新”这样的表述,只会说本次收录覆盖到了哪里。
最后,我会保留一组小而稳定的回归问题:一组专门验证新增资料,一组验证旧资料,一组用来做来源反向检查。它们不需要很多,但必须能在每次重建后重复使用。一个流程成熟的标志,不是它能跑通一次,而是下次面对同类变化时,不再依赖当时那个人的记忆。
这件事让我重新意识到,知识库的价值不在于它保存了多少文件,而在于它能不能让一个具体问题得到有根据的回答。重建只是把资料搬进去,验收才是在确认这些资料已经成为系统的一部分。没有后者,前面的忙碌很可能只是自我安慰。