Skip to content

编译流水线如何为你的知识去噪

大多数知识管理工具把文档直接切块、向量化,然后让 LLM 从原始碎片中回答问题。KaaS 反其道而行之:先把原始内容「编译」成结构化的 Wiki 文章,再用 LLM 按需检索这些文章

这一设计的代价是需要一条可靠的编译流水线。问题随之而来:当用户一次性提交几十个文件时,某一个 LLM 调用挂起、或者多个文件「不约而同」提取出同名概念,编译结果就会出现噪声(超时残留)和重复(wiki 里冒出两篇几乎相同的文章)。本文拆解 KaaS 的 4 阶段编译流水线,重点讲清它在哪两个关键节点做去噪和去重。

要解决什么问题

KaaS 编译面对两类工程难题:

1. 噪声:单个 LLM 调用可能卡死整条流水线

Extract 阶段对每份文档发起 LLM 调用。一旦某个调用因网络或模型侧原因挂起,16 个并发 worker 中的一个就会长期占坑,最终拖死整批任务。

2. 重复:并行分类会独立「发明」相同文章

Classify 阶段将多个文件分组并行处理。不同组看到同一主题会各自在 create_new 列表里产生同名条目,最终写入阶段就会创建两篇内容高度重叠的 Wiki 文章。

Phase 1:并行提取(Extract)——去噪的两道防线

compile.py 的 Phase 1 用 ThreadPoolExecutor 对所有文件并发调用 extract_knowledge_chunked()。去噪逻辑在两层:

防线一:每个 Extract 调用独立超时 180s

extract.py 中的 @_with_extract_timeout 装饰器在调用 LLM 前把全局超时切换为 180 秒,调用结束后恢复原值。这保证任何一次 Extract LLM 调用最多占用 180 秒,不会因超时传播影响其他 worker。

防线二:分块摘要压缩,控制 Phase 2 输入体积

对于长文档,extract_knowledge_chunked() 先按 4000 token(约 16000 字符)切块,再用 extract_knowledge_summarized() 做两阶段处理:

  • Phase A:并行对每个 chunk 调用 Haiku 生成摘要(200–500 词)
  • Phase B:把摘要拼接后交给 Sonnet 做结构化提取

当 chunk 数 ≥ 20 或拼接后摘要超过 60,000 字符时,还会触发 L2 层级合并:以 fanout=5 的 Haiku 批次把多个 chunk 摘要再压缩一轮,再进入 Phase B。这样不论原始文档多长,进入 Sonnet 提取的输入始终有上界,避免 prompt 过大导致的失败。

K-adaptive 分派规则一览:

成功 chunk 数策略
≤ 3K=1,单次 Sonnet 提取
4–7K=2,并行 A/B 字段组
8–19K=3,并行 A/B/C 字段组
≥ 20 或摘要 > 60KL2 Haiku 合并 → K=3

Phase 2a:分类(Classify)——去重的两道闸

classify_article() 让 Sonnet 决定:把当前内容「新建文章」还是「合并进已有文章」。但这里有两道去重闸。

闸一:单组内的 title 词重叠去重

每次分类完成后,dedup_create_new() 立即检查 LLM 输出的 create_new 列表里的标题,与已有文章做词重叠对比。_title_words() 将标题转为小写词集合,重叠率 = 交集大小 / 两者中较小集合的大小。阈值 0.7 意味着标题单词有 70% 相同就直接合并,不新建文章。

闸二:并行组之间的 cross-group dedup

所有 Classify 组并行完成后,还有一道 Phase 1.5:把各组 create_new 列表里的文章聚合,再逐一与其他组的输出做 dedup_create_new()。这段代码把「其他组已计划新建的文章」临时加入 cross_existing,再做一次词重叠比对。重复的条目从 create_new 移入 merge_into,避免两篇相同文章进入写入阶段。

_cluster_by_topic_overlap() 负责在进入 Classify 之前就把话题相近的文件归入同一组,组内的去重由 dedup_create_new() 串行处理;跨组的去重由 Phase 1.5 兜底。

Phase 2b:并行写入(Write)

分类去重后,article_ops 按目标文章路径聚合操作。写入时每个目标文章独立为一个任务,提交到 ThreadPoolExecutor,最多 16 个 worker 并行写入。

  • 如果目标文章已存在 → merge_into_article()(增量 diff 合并)
  • 如果不存在 → create_new_article()(全量生成)

Phase 3:索引(Index)

写入完成后,update_markdown_index() 遍历 wiki/ 下所有 Markdown 文件,生成三份纯文本索引:

  • master-index.md:按文章标题字母排序的全量清单
  • topic-index.md:出现频率达阈值的主题标签分组
  • topic-index-longtail.md:低频长尾标签

这三份索引正是检索阶段 LLM「先看目录,再翻全文」的入口。

结论

KaaS 选择「先编译再检索」而非「直接 RAG」,意味着编译质量直接决定了检索质量。去噪(180s 超时隔离 + L2 摘要压缩)和去重(title 词重叠闸 + cross-group dedup)是维持这条流水线稳定运行的两根支柱。

当前去重的 title-overlap 方案是纯字符串运算,不依赖向量或嵌入,在资源受限的本地部署场景下尤其友好。代价是它对同义词无感:「API Gateway」和「网关」会被视为完全不同的条目。这是一个已知的权衡,也是后续版本可以探索的方向。

感兴趣的话可以在 GitHub 里翻 py/src/kb_ai/ 目录,代码都是公开的。