编译流水线如何为你的知识去噪
大多数知识管理工具把文档直接切块、向量化,然后让 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 数 | 策略 |
|---|---|
| ≤ 3 | K=1,单次 Sonnet 提取 |
| 4–7 | K=2,并行 A/B 字段组 |
| 8–19 | K=3,并行 A/B/C 字段组 |
| ≥ 20 或摘要 > 60K | L2 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/ 目录,代码都是公开的。