Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

上线后的运维与迭代:知识库不是做一次就完了

上线那天是系统最好的状态:管线跑通了,检索质量不错,用户开始用了。半年后再看——新财报发了,新规程也发了;旧准则废止了一批;现场有设备改了名;还有人没搜到答案,关掉页面走了,而你不知道他不满意。库还是原来那个库,世界已经不是原来那个世界。

我体会最深的一句话:建设是一次性的,运维是持续性的。这篇讲上线后最让我头疼的五个运维问题,每个给一个“够用就行“的解法,也把每个解法的边界摆出来。

一、增量更新:加一份 30 页 PDF,要不要重建 5000 份?

先给一个本能答案:扔进管线,全量重跑,干净彻底。这个答案错在成本——5000 份文档重新解析、重新切、重新编码,只是为了一份 30 页的新文件。

正确做法不复杂:新文档单独走一遍完整管线,PDF → clean text → TextBlock → chunk → embedding → 入库。流程和老文档一模一样,只是范围只有这一份。

真正的坑不在流程,在分布。新 chunk 和老 chunk 待在同一个向量空间里,但分布未必一致,两种情况会让它们错开:一是 embedding 模型中途升过版本,新旧向量根本不是一个模型算出来的;二是新文档的语言风格和老文档差得远,比如老库全是规程条文,新来的是口语化的问答汇编。结果是新 chunk 在检索时被系统性地排在老 chunk 前面或后面。这不是检索策略的问题,调 top_k 救不回来——是新增数据改变了向量空间的分布。

怎么发现?新文档入库后跑一轮基础能力测试,看 Recall@5 有没有退化——它是第 14 篇评估体系里那套自有基础能力测试集上的召回指标,这里不展开。退化了,说明分布偏了,再考虑重编码全库或者调整 embedding。【待补:分布漂移的真实运维时间线数据(如“上线 X 个月后 Recall@5 从 a 掉到 b“),素材只有定性描述,若手头有实测记录可补在这里】

二、旧文档废止:删还是留

先别看方案,先看一个后果。旧规程废止,第一反应是“从库里删掉“。删之前先想一件事:上个月有用户问过“跳闸后怎么处理“,系统引用了旧规程第 3.2 条给出回答——这个回答现在还有效吗?

分两种。新规程改了处置流程,旧回答就是过时信息,留着误导人;新规程只是改了编号、内容没动,旧回答仍然成立。也就是说,“废止“不是二元开关,它和“旧回答还有没有效“之间隔着一层判断,而这层判断要求你能回头查到旧回答引用的是哪一条。

所以我的做法是不做“删 chunk“,做“标记废止“:给废止的 chunk 加两个 metadata 字段,status=deprecated 和 superseded_by=new_version_chunk_id。检索时默认过滤 deprecated 的 chunk,除非用户明确要查历史版本;生成回答时如果引用了废止 chunk,在引用处标注“(该条款已被 XX 年版本替代)“,把最后的判断权交给看回答的人。

边界摆在明处:废止关系靠人工标记,而人总是会忘的。但它还是比直接删好——删了就是毁尸灭迹,chunk 永远找不回来,你也永远不知道哪些旧回答引用过它。这不是完美方案,是两害相权。

三、你不知道用户在搜什么、搜没搜到

用户在搜索框打了一句话,系统返回了答案。用户扫一眼,觉得不对,关掉了。整个过程你的日志里只有一条记录:“query 已处理,耗时 320ms”。

这是上线后最大的盲区:你不知道哪个 query 答得不好。建设期盯着评估集,上线后真正值钱的数据从用户手里流过,你没接住。我按采集成本从低到高分三层接住。

第一层,检索日志全量记录。别只记 query 和耗时,记完整链路:原始 query → 改写后的 query → 检索出的 top-10 chunk ID → reranker 重排后的 top-5 → 最终发给 LLM 的 prompt → LLM 的输出。这套数据平时没人看,也不该有人天天看——它是出问题后复盘用的。用户说“答错了“,你能一层层回放:检索就没捞到,还是捞到了 rerank 排丢了,还是 prompt 里有但 LLM 没用上。没有全量日志,这类问题只能靠猜。

第二层,点击行为。回答下面挂着几条引用链接,用户点了哪条、停留多久,都是信号。用户点开第三条引用、停留 20 秒——他在自己对照原文,这通常意味着 LLM 的回答不够直接,用户不放心,要亲自核实。这是用户不开口的质量反馈。

第三层,反馈按钮。每个回答下面放“有用 / 没用“。一定有人说:用户懒得点,数据量不够。我的看法是,哪怕一天只有 5 个人点了,也能看出哪些 query 的“没用“率异常高。5 个数据比 0 个好无数倍——盲调不如有方向地调。

三层分工不同:日志负责复盘单个事故,点击和反馈负责找出哪些 query 值得复盘。缺了哪一层,运维都是在黑灯瞎火里做。

四、设备改名、术语变化、公司名变更

电力系统里,设备是会改名的。“1 号主变“改成“城北站 1 号主变”,“220kV 城东变“变成“220kV 城东智能变”。财报世界里同样的事是公司更名、准则修订——名字换了,旧文档还在。

第一条原则反直觉:不要改旧文档。旧文档里的名称是当时的历史记录,改了就是篡改原文件——今年把“城东变“全替换成“城东智能变“,明年再改一次名,没人说得清原文件长什么样。

名称差异放在两处消化。一处在 metadata 层,加别名映射:

{
  "equipment_name": "城北站 1 号主变",
  "aliases": ["1 号主变", "城北 1 号变", "老 1 号变"],
  "name_effective_date": "2025-03-01"
}

equipment_name 是当前正式名,aliases 收罗历史叫法和现场口头叫法,name_effective_date 记改名生效时间。另一处在 query 层:检索时做改写,把用户 query 里的旧名映射到新名后,新名和别名表两个方向同时搜,结果合并去重。

绕这一圈,是因为用户不知道设备改名了,他还在用旧名字问。这不是用户的错——不是让用户适应系统,是让系统适应用户的表达习惯。第 6 篇讲时间错配时的 query 改写是同一个模式,只不过那里消化的是时间表达,这里消化的是名称漂移。

五、版本管理:不等有人来更新,让管线自己发现

一份规程半年更新一次。半年后知识库里躺着两份文件:2025版调度规程.pdf 和 2025版调度规程-新版.pdf。哪个是当前版本?问一圈没人说得清——发布流程里根本没有“更新知识库“这一步,新文件是某次顺手传上去的。

根因在人,解法别押在人身上。“发新规程时请同步更新知识库”,这种约定写下来容易,守住难。现实解法是让 ingestion 管线自己发现版本关系:新文档入库时,查文档级 metadata 的 regulation_title 和 effective_date;库里已有同 title 的老文档,就把老文档标成 superseded_by=新文档ID,新文档标成 current。同时维护一张文档版本表——一个简单的 SQLite 表就够,记录每个 title 的最新 effective 版本,检索时优先返回 current。人不用自己判断“哪个是最新的“,管线按 effective_date 自动标。

边界也得认:有些规程没有明确的 effective_date,PDF 上就没写,只能人工指定。但混着人工也比什么都不做强——老值班员离职之后,没人知道库里两份规程到底哪个是对的。

五个问题串一遍:增量入库不破坏向量分布,新文档单独走管线;旧文档废止不删只标 deprecated;用户行为靠日志全量加反馈按钮;改名不改旧文档,靠别名映射消化;版本不等人来更新,管线按 effective_date 自动标 current。

这些不是锦上添花。不做的话,三个月后系统悄悄退化——检索照常返回,回答照常生成,只是错的越来越多,而你连退化了多少都不知道。

所以我的判断是:知识库的预算应该按“年“算,不是按“项目“算。按项目算,验收那天预算就花完了;按年算,才留得出增量、废止、日志这些钱和人力。

到这里,这个系列从解析、分片、metadata,一路讲到检索、评估、排查、运维,系统算是能跑、能扛、能查了。还剩最后一个边界问题没碰:是不是所有 query 都该走这套固定管线?什么时候该放开手,让 AI 自己决定怎么搜?下一篇聊这个。