← 返回 关于

我们如何构建知识库

2026-07-18 · 原文链接

作者:@hi_im_isaac_@learnwdaniel@gaozenghao 注:完整技术博客的交互式版本见:https://www.cerebras.ai/blog/how-we-built-our-knowledge-base

员工每天向我们的内部知识库提问超过 15,000 次。它在三个月前上线后,已成为公司内部采用最广的工具之一,供人类、自动化流程和智能体使用。

在 Cerebras,我们的团队覆盖数据中心运维、芯片设计、硬件、训练、推理、云平台等多个领域。随着每年数百名新员工加入,沟通渠道里不断涌现同样的问题:

我们构建 Cerebras Knowledge,是为了帮助人和系统接入有用的信息。

让数据留在它原本所在之处

在组织内部寻找信息很难。数据分散在各类工具中,每隔几个月,总会有人提出同一个看似绝妙的解决方案:把所有内容都记录到一个平台,让全部信息集中在同一个地方。当然,“唯一事实来源”的梦想在实践中很少奏效。

信息会在最方便、最符合使用习惯的地方产生:文档中的修订建议、Slack 里的讨论串、GitHub 中的代码引用,以及 Jira 的状态元数据。这些平台都为各自的领域量身打造,经过多年产品工程和分析优化。拿 Google Docs 来讨论一个拉取请求,体验一定很糟。

因此,我们着手设计一个尽量不改变既有工作方式的系统。在数据采集侧,这意味着直接从各个平台提取数据。

知识库的构成

我们的知识库提供三项能力:

其核心是一张 Postgres 表,保存来自多个来源的嵌入向量、原始摘要和元数据。系统持续摄入公司各处的数据,并维护一个随时可查询的数据存储。

我们希望数据接口足够简单,同时又能适用于大多数数据形态;也希望 Cerebras 的其他开发者能够构建自定义连接器。最终方案刻意保持简单:从 Slack 讨论串到网表(netlist),每个来源都写入同一张嵌入表;而表中的任何内容都可立即通过同一接口查询:

每个数据源都定义数据是什么、如何连接,以及多久抓取一次。无论一行嵌入数据来自 Slack、代码仓库、文档系统还是自定义数据库,它都遵循同一套接口。

我们如何处理非结构化的 Slack 对话

Slack 是我们最需要优先设计的数据源:公司中最新的工程讨论大多发生在那里。

起初,我们测试了对原始文本直接生成简单嵌入是否足够好。很快就发现,仅靠向量检索不足以匹配所有相关数据。

Slack 消息带来了几个挑战:

我们需要混合式方案。我们构建 Slack 摄入流程,让每个讨论串能够同时通过多种检索技术找到;每种技术都用来弥补其他技术的弱点:

我们不会单独信任任何一个评分器。每种技术都会为同一语料库产生自己的排序视图,查询时再将这些视图融合(见“重排序”)。

Socket Mode

为了实时采集数据,我们在工作区中安装了一个 Slack 机器人,并以 Socket Mode 运行。Slack 通过持久 WebSocket 将每个消息事件推送给我们,因此无需轮询 Web API、耗尽其速率限制,就能获得实时更新。

事件到达后,我们会立刻确认,使用稳定的事件 ID 去重,并标记该消息交给摄入消费者处理。

摄入消费者不会孤立地保存一条新消息。它会解析消息所属的讨论串,并从 Slack API 重新拉取完整对话,包括父消息和每条回复;随后将整个讨论串作为一行写回。因此,只要现有讨论串新增回复,就会重新拉取父消息和所有同级回复,确保已存储的内容、参与者列表和最后活跃时间戳始终反映完整对话。

系统中的每个 Slack 频道都有自己的数据源。这使我们能够细粒度地调节数据新鲜度;例如,一个团队可以选择更频繁地摄入繁忙的事故响应频道。

讨论串与消息

原始 Slack 文本一落库就可以进行关键词检索,因为我们在原始内容上维护了 Postgres 全文(GIN)索引。不过,要实现有用的向量检索,我们还会做一些额外处理。(8)

在蒸馏阶段,LLM 会从完整讨论串中提取结构化数据:

我们对这些数据点生成嵌入,并将其写入共享嵌入表。原始转录文本并不直接嵌入。在实验中,将讨论串归一化为一致格式后,准确率显著提升。(7,9) 额外的元数据也为语义匹配提供了更有用的信号。

突发段(Burst)

此时 Slack 检索已经不错,但我们仍不断遇到同一问题:长讨论串中的重要消息,并不总能被讨论串级摘要所代表。

为了增强单条消息的信号,我们使用 burst。一个 burst 是同一作者连续发送的一段消息。我们会为单个 burst 生成嵌入,并在前面附上讨论串主题作为上下文(2),因为有时答案藏在某条岔开的消息里,它的词汇根本不会出现在讨论串摘要中。Burst 嵌入使这条消息本身也可以被找到。

为了防止低信号数据进入数据库,每个 burst 都会根据加权信号组合评分,只有超过阈值才会生成嵌入:

蒸馏后,符合条件的 burst 会生成嵌入,并与讨论串级记录一同存入嵌入表。

代码仓库

最初,我们曾讨论是否有必要为代码仓库生成嵌入。随着 Claude Code 和其他命令行工具的兴起,在“grep 就够了”的氛围下,构建代码嵌入似乎有些反直觉。但在与业内同行交流、并阅读 Cursor 关于大型代码库语义检索的发现后,我们决定尝试。

我们有许多内部仓库,其中一些超过 40 GB。最主要的顾虑是如何高效地保持它们最新。

使用 @cocoindex_io 维护代码嵌入

经过数次实验,我们选择了 CocoIndex——一个专注于代码库向量化的开源文档嵌入框架。

对于每个仓库,我们使用按由粗到细顺序排列、与语言相关的正则表达式边界来切分代码。切分器会先尝试类等较高层级的边界;如果得到的块仍然过大,就会退回到方法边界,再到更小的代码块。我们对得到的块生成嵌入,并将向量写入 Postgres。单个文件可能生成多个不同具体层次的嵌入,例如文件级和函数级记录。

CocoIndex 会在 Postgres 中跟踪同步元数据。每次提交时,它只会重新生成嵌入并重新导出变更过的代码块,而不是重新计算整个仓库。这一点对我们尤其有效,因为同步状态和嵌入存储位于同一个数据库。

随着代码库数量增加,我们将仓库接入迁移到由团队自行提交的配置文件中,其中包括文件路径级别的允许列表和拒绝列表。

自定义数据源

有些团队本来就有自己的数据库,并不希望为了接入知识库而把数据迁到 Slack 或文档系统。他们希望能够在现有表上使用同样的查询界面。

为此,我们把自定义来源视为插件脚本。团队提交一个拉取请求,其中包含一个小型 Python 模块:它知道如何从自己的系统读取数据,并输出符合我们嵌入表形状的行,以及对应的数据源条目。

只要脚本使用与其他每一行嵌入相同的模式写入共享数据库,栈中的其他部分就无需改变。这些数据就能与 Slack、代码和文档一起查询,系统其他地方无需特殊处理。

规划与工具并发扇出

对于每个查询,我们首先运行一次简短的规划:LLM 决定哪些工具和数据源可能重要。主要工具包括:

规划器面对的是我们已建立索引内容的紧凑描述:有哪些项目、每个项目有哪些来源,以及各来源适合回答什么问题。给定用户查询和当前作用域后,它会产出工具选择;执行器将这些选择并发扇出,规范化为统一证据格式,再交给最终的综合 LLM。(4)

重排序

一份文档可能仅因与查询共享词汇,就出现在靠前位置,却回答的是另一个问题。重排序前,我们先使用倒数排名融合(reciprocal rank fusion,RRF)将各检索器彼此不兼容的结果列表合并。对每一份文档,只要它出现在某个列表中,我们就加上 weight / (60 + rank),默认权重为 1.0,平滑常数为 60。

平滑常数使多方共识比一次强投票更重要:一份在多个检索器中都靠前出现的文档,可以胜过只在其中一个检索器排名第一的文档。然后,我们把重复的数据块合并回一个来源,限制每个文件可贡献的结果数,最终得到更多样的前二十个结果。

我们将原始查询和这些候选项发送给一个小型重排序模型。它为每份文档打 0 到 10 分,我们保留前十项。(6)

排名确定后,我们会把上下文补回胜出的结果。例如,若匹配到 wiki 的一个章节,就会拉入相邻的两个章节,避免因切块而拆散的标题、前置条件和注意事项丢失。这样读者拿到的是完整片段,而不是缺少重要上下文的孤立段落。

因此,检索的输出是一个丰富的证据包:来自不同检索器并经融合的结果,在来源级别去重,针对实际问题重排序,最后再扩展周边上下文。

MCP

在 MCP 集成中,我们将检索构件直接暴露为工具,而不是把它们隐藏在一个“回答这个问题”的端点后面。这些工具有意保持简单,并尽可能不依赖 LLM,从而让客户端能够快速、低成本地查询。(5)

每个 MCP 工具都对应一个底层检索原语,例如 search_slack、search_code、search 或 who_knows。工具的输入和输出范围窄、结构化且稳定,因此任何客户端或智能体都能轻松调用,而无需把额外的编排逻辑塞进工具本身。

大多数工具运行一条查询管线,例如向量检索、词法检索或 ripgrep,应用轻量评分启发式规则,并返回原始证据行。

Claude Code 或任何兼容 MCP 的智能体,会成为编排引擎:决定调用哪些工具、以什么顺序调用,以及如何将结果组装为最终答案或代码改动。检索层本身无需依赖这些 LLM 决策,也能提供请求服务。

Web UI

Web UI 中也存在同样的工具,但它们被连接到一个完整的查询管线,会对每个用户问题端到端运行。UI 智能体负责规划器和执行器两个步骤。

从用户角度,Web UI 只是“提出一个问题,然后得到答案”。但在底层,它运行的是同一种 planner → executor → synthesizer 模式,MCP 客户端可以显式地复现它。

组织

随着语料库增长,“搜索所有地方的一切内容”很快就不再有用。编译器团队的工程师不希望在结果中看到基础设施运行手册,反之亦然。项目是我们让检索默认保持相关性的方式。

项目与作用域检索

我们将项目引入为组织查询工作区的主要方式。一个项目是具名的数据源组合:与某团队或计划相关的特定 Slack 频道、代码仓库、内部数据库和文档空间。

项目有意保持轻量。共享事故频道或中央平台仓库等同一个数据源,可以被多个项目引用,而无需重复。

接入与默认设置

在接入过程中,用户会被引导选择或创建一个符合其工作方式的默认项目,例如机器学习训练基础设施、编译器或数据中心运维。

默认项目会存储在用户档案中,并自动限定查询范围。新工程师无需先了解哪些 Slack 频道、仓库或文档空间重要,也能获得高信噪比的答案。

最后的思考

归根结底,这个知识库之所以有效,是因为它让信息留在人们本来就在使用的地方,而不是强迫所有内容进入一个僵化系统。通过组合多种检索技术,我们能够迅速呈现证据。最终得到的检索体验,既足够灵活以适应真实的公司数据,也足够结构化,能在 Cerebras 持续成长时保持有用。

如果你读到了这里,并且对此感兴趣,AI/Growth 团队正在招聘;有兴趣请联系 @learnwdaniel。

参考资料

  1. Malkov 和 Yashunin,使用分层可导航小世界图进行高效且稳健的近似最近邻搜索,arXiv:1603.09320 / IEEE TPAMI 2018。
  2. Anthropic,介绍上下文检索,2024。
  3. Cormack、Clarke 和 Büttcher,倒数排名融合优于 Condorcet 与单项排序学习方法,SIGIR 2009。
  4. Li 等,Search-o1:由智能体驱动、以检索增强的大型推理模型,arXiv:2501.05366,2025。
  5. Anthropic,使用 MCP 执行代码,2025。
  6. Liu 等,迷失在中间:语言模型如何使用长上下文,arXiv:2307.03172,2023。
  7. Anthropic,使用 XML 标签
  8. Salesforce/Slack Engineering,如何让 Slack AI 处理数十亿条消息。
  9. Improving Agents,最佳嵌套数据格式。
  10. Cursor,用语义检索改进智能体,2025。