Cerebras (@cerebras)
2026-07-17
D
原文
---
title: "我们如何构建知识库"
author: "Cerebras (@cerebras)"
source_url: "https://x.com/cerebras/status/2077822555159945507"
published_at: "2026-07-16T18:27:55.000Z"
fetched_at: "2026-07-18T09:34:02Z"
updated_at: "2026-07-18T10:15:40Z"
language: "zh"
review_status: "draft"
---

# 我们如何构建知识库
作者:[@hi_im_isaac_](https://x.com/@hi_im_isaac_)、[@learnwdaniel](https://x.com/@learnwdaniel)、[@gaozenghao](https://x.com/@gaozenghao)
*注:完整技术博客的交互版见:* https://www.cerebras.ai/blog/how-we-built-our-knowledge-base
员工每天会向我们的内部知识库提出超过 15,000 个问题。自 3 个月前上线以来,它已经成为公司内部采用最广的工具之一。使用它的不只是人,还有自动化流程和 agent。
在 Cerebras,我们的团队横跨数据中心运维、芯片设计、硬件、训练、推理、云平台等多个领域。每年都有数百名新员工加入,公司的沟通渠道里也不断出现同样的问题:
\- “我在哪里能找到 X?”
\- “谁是 Y 方面的专家?”
\- “Z 是什么?”

我们构建 Cerebras Knowledge,是为了把人和系统连接到有用的信息上。
## 让数据留在它本来所在的地方
在组织内部找信息很难。数据散落在各种工具里,而每隔一个季度左右,总会有人提出同一个“绝妙”方案:把所有东西都记录到一个平台里,让所有信息集中在同一个地方。当然,“单一事实来源”这个理想在实践中很少真正奏效。
信息会在最方便、最顺手的地方产生:文档里的建议修改、Slack 里的对话、GitHub 里的代码引用、Jira 里的状态元数据。这些平台都是为各自领域量身打造的,经过多年产品工程和数据分析优化。要是在 Google Docs 里讨论 pull request,体验会非常糟糕。
所以我们要设计一个系统,尽量不改变大家现有的行为。在数据收集这一侧,这意味着直接从每个平台抽取数据。
### **知识库的组成**
我们的知识库提供三件事:
\- 一个收集和存储内部数据的平台。
\- 一个查询这些数据的平台。
\- 一层用于执行认证和授权的机制,同时提供审计和分析。
核心是一张 Postgres 表,用来保存来自许多来源的 embedding、原始摘要和元数据。系统会持续摄取公司各处的数据,并维护一个随时可查询的数据存储。
我们想要一个足够简单、但能适配大多数数据形态的数据接口。我们也希望 Cerebras 的其他开发者可以构建自定义连接器。最终结果刻意保持简单:从 Slack 对话到 netlist,每一种来源都会进入同一张 embeddings 表;表里的任何内容都能立刻通过同一个接口查询:

每个数据源都会定义数据是什么、如何连接、以及多久抓取一次。无论一行 embedding 来自 Slack、代码仓库、文档系统,还是自定义数据库,它都遵循同一个接口。
## 我们如何处理非结构化 Slack 对话
Slack 是我们最需要重点设计的数据源。公司里最新的工程讨论大多发生在那里。

一开始,我们测试了直接对原始文本做简单 embedding 是否足够好。很快我们就意识到,只靠向量搜索不足以匹配所有相关数据。
Slack 消息有几个挑战:
\- 信息密度差异极大:“hey yeah sure mike”和一段详细的 kernel 解释都是消息。
\- 消息长度不一,而且在余弦相似度上,短消息常常会压过更长、更详细的消息。
\- 一条消息的含义往往取决于它周围的对话。
我们需要一种混合方法。我们构建 Slack 摄取流程,让每段对话都能同时通过多种搜索技术被找回,每种技术都用来弥补其他技术的短板:
\- **全文搜索能抓住那些会被 embedding 混在一起的精确 token:** 错误字符串、flag 名、主机名。当工程师粘贴一条字面错误信息时,精确的词法匹配几乎总是最好的证据,再高的语义相似度也不应该排在它前面。
\- **Embedding 搜索能抓住改写表达。** 提问的人说“restore hangs after manifest load”(manifest 加载后 restore 卡住),回答的人说“checkpoint stalls on the NFS mount”(checkpoint 在 NFS 挂载处停住),两边可能没有任何相同词汇。向量相似度把用不同措辞写出的提问和回答连接起来。(1)
\- **逆文档频率把信号和填充内容区分开。** 一条围绕罕见 token 写成的短消息,比如某个冷门 config flag,应该获得较高排名。“sounds good, thanks!”(听起来不错,谢谢!)在 embedding 空间里会离很多查询都很近,但一旦把词项稀有度算进去,它的得分就接近于零。
\- **年龄衰减体现的是 Slack 答案会过期。** 两段对话可能回答同一个问题,其中一段来自 6 个月前,描述的基础设施也许已经不存在了。当相关性在其他方面相当时,更新的对话胜出。

我们不会单独信任任何一个评分器。每种技术都会对同一个语料库产生自己的排序视图,这些视图会在查询时融合起来(见“重新排序”)。
### **Socket Mode**
为了实时收集数据,我们在工作区里安装了一个 Slack bot,并让它以 Socket Mode 运行。Slack 会通过持久 WebSocket 把每个消息事件推送给我们,所以我们不用轮询 Web API,也不会因此耗尽它的速率限制,就能拿到实时更新。
事件到达时,我们会立刻确认收到,用稳定的 event ID 去重,并把这条消息标记给摄取消费者处理。
摄取消费者不会孤立地保存一条新消息。它会找出这条消息所属的那段对话,并从 Slack API 重新抓取完整对话,包括父消息和所有回复。然后它把整段对话作为一行写回。因此,既有对话里新增一条回复时,系统会重新拉取父消息和所有同级回复,让存储下来的内容、参与者列表和最后活跃时间始终反映完整对话。
系统里的每个 Slack 频道都有自己的数据源。这让我们可以细粒度地调整数据新鲜度。例如,某个团队可以选择更频繁地摄取繁忙的事故频道。
### **对话和消息**
原始 Slack 文本一进入系统就可以按关键词搜索,因为我们在原始内容上维护了一套 Postgres 全文(GIN)索引。不过,为了启用有用的向量搜索,我们还会做一些额外处理。(8)
在蒸馏过程中,LLM 会从完整对话中抽取结构化数据:
\- 工程师真正会拿来搜索的一行问题。
\- 一段简短摘要。
\- 解决方案。
\- 被提到的系统和代码引用。

我们会对这些数据点做 embedding,并写入共享的 embeddings 表。原始对话记录不会被直接嵌入。在我们的实验中,把对话整理成一致格式后,准确率有显著提升。(7,9) 额外的元数据也为语义匹配提供了更有用的信号。
### **Bursting**
到这一步,Slack 搜索已经很好了,但我们不断遇到同一个问题:很长的对话里,有些重要消息并不总是会体现在整段对话的摘要里。
为了增强单条消息带来的信号,我们使用 bursting。一个 burst 指同一作者连续发出的几条消息。我们会在单个 burst 前面加上对话主题作为上下文,再对它做 embedding(2),因为有时答案藏在某条岔开的消息里,而这条消息的词汇从来没有进入对话摘要。Burst embedding 让这条消息本身也能被找到。
为了防止低信号数据进入数据库,每个 burst 都会根据一组加权信号打分,必须超过阈值才会被嵌入:
\- 它包含在整个语料库中相对罕见的 token,IDF 至少为 4.0。
\- 合并后的 burst 至少有 200 个字符。
\- burst 中有一条或多条消息带有表情回应,提供社交信号加成。

蒸馏之后,符合条件的 burst 会被嵌入,并与整段对话层面的记录一起存入 embeddings 表。
## 代码仓库
一开始,我们也讨论过是否真的有必要对代码仓库做 embedding。随着 Claude Code 和其他命令行工具兴起,在“grep 就够了”看起来成立的时候,创建代码 embedding 似乎有点反直觉。后来,我们和业界其他人交流,也读了 Cursor 关于大型代码库语义搜索的发现,决定试一试。
我们有许多内部仓库,其中一些超过 40 GB。我们最关心的问题,是如何高效地让它们保持最新。
### **用 @cocoindex_io 维护代码 embedding**
几轮实验之后,我们选择了 CocoIndex。它是一个开源文档 embedding 框架,专门用于对代码库做向量化。
对于每个仓库,我们会使用按语言定制的正则边界来切分代码,顺序从粗到细。切分器会先尝试类这样的较高层级边界。如果得到的 chunk 仍然太大,就退回到方法边界,再退到更小的块。我们对这些 chunk 做 embedding,并把向量写入 Postgres。同一个文件可能在不同粒度上生成多条 embedding,例如文件级记录和函数级记录。

CocoIndex 会在 Postgres 中跟踪同步元数据。每次 commit 时,它只会重新嵌入并重新导出发生变化的代码 chunk,而不是重新计算整个仓库。这一点对我们尤其有效,因为同步状态和 embedding 存储都在同一个数据库里。
随着代码库数量增加,我们把仓库接入流程搬进了配置文件,让团队可以自己提交,包括文件路径级别的允许列表和拒绝列表。
### **自定义数据源**
有些团队已经有自己的数据库,并不想只是为了接入知识库,就把数据搬进 Slack 或文档系统。他们希望能在既有表上获得同样的查询入口。
为此,我们把自定义来源当作插件脚本处理。团队可以提交一个 pull request,里面包含一个小型 Python 模块;这个模块知道如何从自己的系统读取数据,并产出形状与我们的 embeddings 表一致的行,同时配上对应的数据源条目。
只要脚本按照和其他 embedding 行相同的数据模式写入共享数据库,其余技术栈就无需改变。数据会和 Slack、代码、文档一起变得可查询,系统其他地方不需要做特殊处理。
### **规划和并行调用工具**
对于每个查询,我们首先运行一次简短的规划步骤,让 LLM 判断哪些工具和数据源可能相关。主要工具包括:
\- **subsystem_index**:每个文件的 LLM 摘要。
\- **search**:跨 Slack、wiki、代码和其他已索引来源的统一向量流水线,内部会合并并重新排序。
\- **search_slack**:直接检索 Slack。
\- **search_code**:在源代码仓库上运行 ripgrep。
\- **recent_prs**:与问题相关的近期 pull request。
\- **who_knows**:在某个主题上已经展现出专长的人。
规划器会读取一份简短描述:我们已经索引了哪些项目、每个项目里有哪些来源、每个来源擅长回答什么问题。给定用户查询和当前作用域后,它会输出工具选择;执行器再并行调用这些工具,把结果整理成统一的证据格式,并传给最后负责综合的 LLM。(4)

### 重新排序(Reranking)
一篇文档可能只是因为和查询共享词汇,就排到很靠前的位置,哪怕它回答的是另一个问题。在重新排序之前,我们先用 reciprocal rank fusion(RRF,倒数排名融合)合并各个检索器互不兼容的结果列表。对于每篇文档,只要它出现在某个列表里,我们就加上 weight / (60 + rank);默认 weight 为 1.0,平滑常数为 60。

平滑常数让“共识”比单个强投票更重要:一篇文档如果在多个检索器里都排在前面,就能击败只在一个检索器里排名第一的文档。然后我们把重复 chunk 合并回同一个来源,限制每个文件能贡献的结果数量,最终得到更多样的前二十个结果。
我们把原始查询和这些候选项发给一个小型 reranker 模型。它会给每篇文档打 0 到 10 分,我们保留前十个。(6)
排名确定后,我们会给入选结果补回上下文。例如,如果匹配到一个 wiki 小节,我们会拉入它相邻的两个小节,这样标题、前置条件和注意事项就不会因为分块而丢失。这让读者看到的是完整片段,而不是一个缺少重要上下文的孤零零段落。
所以,search 输出的是一个信息丰富的证据包:结果来自不同检索器的融合,在来源层面去重,按真实问题重新排序,然后才补上周边上下文。
### MCP
在 MCP 集成里,我们把检索构件直接暴露为工具,而不是把它们藏在一个“回答这个问题”的端点后面。这些工具刻意保持简单,并且尽可能不依赖 LLM,这样客户端就能快速、低成本地查询它们。(5)
每个 MCP 工具都对应一个底层检索基础能力,比如 search_slack、search_code、search 或 who_knows。工具的输入和输出范围很窄、结构化且稳定,因此任何客户端或 agent 都能轻松调用,不需要在工具本身再嵌入额外的编排逻辑。
大多数工具只运行一条查询流水线,比如向量搜索、词法搜索或 ripgrep,应用轻量级评分启发式,然后返回原始证据行。
Claude Code,或任何兼容 MCP 的 agent,会成为编排引擎。它决定调用哪些工具、按什么顺序调用,以及如何把结果组装成最终答案或代码修改。检索层本身在服务请求时并不依赖这些 LLM 决策。
### Web UI
在 Web UI 里,同样的工具也存在,但它们连接到一条完整的查询流水线,会为每个用户问题端到端运行。UI agent 负责规划和执行步骤。
\- Planner:一次轻量级 LLM 调用会检查查询和当前项目,然后选择要调用哪些检索工具,例如 search、search_slack 和 subsystem_index。
\- Executor:系统把这些工具调用并行扇出,收集结果,并把它们规范化成共享的证据结构,包含分数、新鲜度和来源提示。
\- Synthesis:最后一次 LLM 调用接收带类型的证据包和原始问题,然后生成 UI 中显示的答案,其中包括引用、限制说明和跨来源综合。
从用户视角看,Web UI 就是“问一个问题,得到一个答案”。在底层,它运行的是同一个“规划器 → 执行器 → 综合器”模式,MCP 客户端也可以显式地复现这套模式。

## 组织方式
随着语料库增长,“到处搜索所有东西”很快就不再有用了。编译器团队的工程师不希望结果里出现基础设施 runbook,反过来也一样。我们靠项目让搜索结果默认就相关。
### **项目和限定范围搜索**
我们引入项目,作为组织查询工作空间的主要方式。一个项目是一组命名的数据源集合:与某个团队或计划相关的特定 Slack 频道、代码仓库、内部数据库和文档空间。
项目被刻意设计得很轻量。同一个数据源,比如共享的事故频道或中央平台仓库,可以被多个项目引用,而不是被复制多份。

### **入职引导和默认值**
在入职引导期间,系统会提示用户选择或创建一个默认项目,使它匹配自己的工作内容,例如 ML 训练基础设施、Compiler 或 Data Center Operations。
这个默认项目会存储在用户资料上,并自动限定查询范围。新工程师不需要先学会哪些 Slack 频道、仓库或文档空间重要,就能得到高度相关的答案。
## 最后的想法
归根结底,这个知识库之所以有效,是因为它贴近信息原本所在的地方,而不是强迫所有东西进入一个僵硬系统。通过组合多种搜索技术,我们可以快速浮现证据。最终得到的搜索体验既足够灵活,能应对真实公司数据,又足够结构化,可以随着 Cerebras 继续增长而保持有用。
如果你读到这里,并且觉得这件事有意思,ai/growth 团队正在招聘;如果你感兴趣,可以联系 @learnwdaniel。
## 参考资料
1. Malkov and Yashunin, [*Efficient and Robust Approximate Nearest Neighbor Search Using Hierarchical Navigable Small World Graphs*](https://arxiv.org/abs/1603.09320), arXiv:1603.09320 / IEEE TPAMI 2018.
2. Anthropic, [*Introducing Contextual Retrieval*](https://www.anthropic.com/news/contextual-retrieval), 2024.
3. Cormack, Clarke, and Büttcher, [*Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods*](https://dl.acm.org/doi/10.1145/1571941.1572114), SIGIR 2009.
4. Li et al., [*Search-o1: Agentic Search-Enhanced Large Reasoning Models*](https://arxiv.org/abs/2501.05366), arXiv:2501.05366, 2025.
5. Anthropic, [*Code Execution with MCP*](https://www.anthropic.com/engineering/code-execution-with-mcp), 2025.
6. Liu et al., [*Lost in the Middle: How Language Models Use Long Contexts*](https://arxiv.org/abs/2307.03172), arXiv:2307.03172, 2023.
7. Anthropic, [*Use XML Tags*](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags).
8. Salesforce/Slack Engineering, *How Slack AI Processes Billions of Messages*.
9. Improving Agents, *Best Nested Data Format*.
10. Cursor, [*Improving Agent with Semantic Search*](https://cursor.com/blog/semsearch), 2025.