首页 › LLM大语言模型 › Obsidian专栏 › 万字拆解|从0-1搭建企业级知识库

万字拆解|从0-1搭建企业级知识库

知识库的构成​

知识库提供三项功能:​

一个用于收集和存储内部数据的平台。​

一个用于查询这些数据的平台。​

一个负责实施身份认证与访问控制,并提供审计与分析功能的层。​

​

✅

其核心是一个单一的Postgres表,用于存储来自多个数据源的嵌入向量、原始摘要及元数据。该系统持续从公司各业务部门摄取数据,并维护一个可供查询的数据存储。​

​

​

飞书文档 - 图片

​

如何处理非结构化的 Slack 聊天记录​

Slack是我们进行设计时最重要的数据来源。公司内部最前沿的工程讨论都在这里进行。​

​

飞书文档 - 图片

​

​

👀

Slack消息带来了若干挑战:​

•

信息密度差异巨大:无论是“嘿,好的,迈克”这样的简短回复,还是详尽的内核说明,都属于消息。​

•

消息长度各不相同,而在余弦相似度衡量下,较短的消息往往优于较长、更详尽的消息。​

•

消息的含义通常取决于上下文对话。​

​

构建了 Slack 数据的采集机制,使每个对话线程都能通过多种检索方式同时被获取,而每种方式都可弥补其他方式的不足:​

​

🧩

全文搜索能够捕捉到嵌入模型将其模糊处理的精确词元: 错误字符串、标志名称、主机名。当工程师粘贴一段原生错误信息时,精确的词法匹配几乎总是最有力的证据,任何语义相似度都不应凌驾其上。​

​

​

🧩

嵌入式搜索能够捕捉释义。提问者“manifest加载后恢复挂起”与回答者“checkpoint在NFS挂载上停滞”之间,可能从未使用过相同的词汇。向量相似度正是将一个问题与用不同措辞表述的答案联系起来的关键。(1)​

​

​

🧩

逆文档频率能够将有效信号与无关信息区分开来。一条围绕稀有词项(例如某个冷门的配置标志)构建的短消息理应获得较高的排序得分。“听起来不错,谢谢!”在嵌入空间中与许多查询相近,但一旦考虑词项的稀有度,其得分便趋近于零。​

​

​

🧩

年龄衰减机制用于编码 Slack 回答的失效状态。同一问题可能由两条不同的讨论串回答,而六个月前的那条可能描述的是已不复存在的基础设施。在其他条件相同的情况下,较新的讨论串将被优先显示。​

​

没有任何单一的评分器能够单独胜任。每种技术都会针对同一语料库生成各自的排序结果,而这些结果会在查询时进行融合(参见重排序)。​

​

套接字模式​

​

为了实时采集数据,我们在工作区中部署了一个 Slack 机器人,并以 Socket Mode 运行。Slack 会通过持久化的 WebSocket 将每条消息事件推送给我们,因此我们无需轮询 Web API,也避免了因频繁调用而触发其速率限制。​

​

•

当事件到达时,我们会立即对其进行确认,利用稳定的事件ID进行去重,并为摄取消费者标记该消息。​

​

🧩

接收消费者不会单独保存一条新消息。它会确定该消息所属的线程,并从 Slack API 中重新获取整条对话,包括其父消息及所有回复,然后将整条线程作为一行数据写回。因此,对现有讨论串的回复会重新拉取其父主题及所有同级主题,从而确保存储的内容、参与者列表以及最后活动时间戳始终反映完整的对话内容。​

​

我们系统中的每个 Slack 频道都有其专属的数据源。这使得我们可以对数据的更新频率进行精细化调整。例如,某个团队可以选择更频繁地从繁忙的事件频道中获取数据。​

​

飞书文档 - 图片

​

​

帖子和消息​

原始 Slack 文本在进入系统后即可进行关键词搜索,因为我们对原始内容维护着一个 Postgres 全文(GIN)索引。然而,为了支持高效的向量搜索,我们还需进行一些额外的处理。​

​

🧩

在解析过程中,大型语言模型会从整个对话线程中提取结构化数据:​

•

一条工程师实际会搜索的一行问题。​

•

一段简短的摘要。​

•

解决方案。​

•

提到的系统与代码引用。​

​

​

​

我们将这些数据点嵌入,并写入共享的嵌入表中。原始文本并未直接进行嵌入。在我们的实验中,当对话线程被规范化为一致的格式时,准确率显著提升。(7,9)此外,附加的元数据也为语义匹配提供了更有价值的信号。​

​

爆发​

在当时,Slack 的搜索功能还不错,但我们始终遇到同一个问题:在长对话串中,重要消息并不总能在对话串级别的摘要中显示出来。​

​

✅

为了增强单条消息的信号,我们采用突发机制。所谓突发,是指来自同一作者的一系列连续消息。我们在每个突发前附加线程主题作为上下文(2),因为有时答案隐藏在某条偏离主线的回复中,而该回复所使用的词汇往往不会出现在线程摘要里。突发嵌入使该消息能够独立被找到。​

​

为防止弱信号数据进入数据库,每个突发信号都会根据一组加权的信号指标进行评分,并且必须通过阈值检验后方可嵌入:​

​

🧩

•

它在语料库中包含一个相对罕见的词元,其逆文档频率(IDF)至少为4.0。​

​

​

​

​

代码仓库​

对于每个代码库,我们按照从粗到细的顺序,利用各语言特有的正则表达式边界来拆分代码。​

•

拆分器首先尝试较高级别的边界,例如类;如果拆分后的代码块仍然过大,则退而求其次,采用方法边界,最后再细化到更小的代码块。​

•

我们将生成的片段进行嵌入,并将向量写入 Postgres。一个文件可能会在不同粒度上生成多个嵌入,例如文件级和函数级记录。​

​

​

随着代码库数量的增加,我们将代码库的接入流程迁移到了由各团队自行提交的配置文件中,其中包括按文件路径级别的白名单和黑名单。​

​

自定义数据源​

将自定义数据源视为插件脚本。​

团队会提交一个包含小型 Python 模块的拉取请求,该模块能够从其系统中读取数据,并以与我们嵌入向量表结构一致的格式输出数据行,同时附带一条对应的数据源条目。​

​

✅

只要脚本以与其他嵌入行相同的模式写入共享数据库,整个技术栈的其余部分便无需任何改动。数据将与 Slack、代码和文档并列,可在系统中直接查询,且无需在其他任何地方进行特殊处理。​

​

规划与工具扇出​

对于每一次查询,我们首先执行一轮简短的规划流程,由大型语言模型判断哪些工具和数据源可能至关重要。主要工具包括:​

​

✅

subsystem_index:针对每个文件的LLM摘要。​

search:跨Slack、维基、代码及其他已索引 来源的统一向量检索管道,内部经过合并与重排序。​

search_slack:直接从Slack中检索。​

search_code:在源代码仓库中使用ripgrep进行搜索。​

recent_prs:与问题相关的近期拉取请求。​

​

​

✅

规划器基于我们已建立索引的精简描述进行工作:即存在哪些项目、每个项目中有哪些可用的数据源,以及每个数据源擅长回答哪些问题。根据用户的查询和当前作用域,它会生成一系列工具调用,由执行器并行展开,统一为一种通用的证据格式,并传递给最终的综合型大语言模型。​

​

​

​

​

MCP​

在 MCP 集成中,我们将检索构建块以直接工具的形式公开,而非将其隐藏在单一的“回答此问题”端点之后。这些工具刻意保持简单,并尽可能不依赖大型语言模型,以便客户能够快速且低成本地进行查询。​

​

✅

每个MCP工具都对应一个底层检索原语,例如search_slack、search_code、search或who_knows。工具的输入与输出均具有高度的专一性、结构化且稳定,因此无论从何种客户端或代理调用,都无需在工具内部嵌入额外的编排逻辑。​

​

大多数工具仅运行一条查询管道,例如向量搜索、词法搜索或 ripgrep,应用轻量级的评分启发式方法,并返回原始证据行。​

​

​

​

Web界面​

在Web界面中,同样存在这些工具,但它们与一个完整的查询处理管道相连接,该管道会为用户的每一个问题从头到尾全程运行。UI代理负责规划器和执行器这两个步骤。​

​

👀

•

计划器:一个轻量级的大型语言模型遍历查询和当前项目,然后选择调用哪些检索工具,例如 search、search_slack 和 subsystem_index。​

•

执行器:系统并行调用这些工具,收集其结果,并将其规范化为包含得分、时效性及来源提示的统一证据模式。​

​

​

✅

综合:最后由大型语言模型对输入的证据集合与原始问题进行处理,生成显示在用户界面中的答案,并附上引用、注意事项以及跨来源的综合分析。​

​

从用户的角度来看,Web 界面就是“提出问题并获得答案”。在底层,它同样遵循规划器→执行器→合成器的模式,而 MCP 客户端也可以显式地复现这一流程。​

​

​

​

项目与范围搜索​

引入了“项目”这一概念,作为组织查询运行所在工作区的主要方式。项目是一个命名的数据源集合,包含与某个团队或特定计划相关的特定 Slack 频道、代码仓库、内部数据库以及文档空间。​

同一数据源,例如共享的事件通道或中央平台仓库,可以被多个项目引用,而无需重复复制。​

​

​

​