本文通过为 Arbitrum DAO 开发治理仪表板的实战经验,详细介绍了构建 RAG(检索增强生成)系统的全流程。内容涵盖了数据摄取、文本分块、向量嵌入、存储检索及生成等核心环节,并重点讨论了如何通过命中率、忠实度等指标对检索和生成质量进行科学评估与优化。

在过去的几个月里,我们一直在深入研究 AI。最近我们发布了关于转向 AI 原生以太坊开发栈的文章。我们的计划不仅是学习如何有效使用 AI 工具,还要深入了解其内部工作原理,重点关注所谓的 AI 工程,特别是训练后(post-training)技术。
去年,我们开始为 Arbitrum DAO 构建一个治理仪表板,用于跟踪 Discourse 讨论、Snapshot 投票和 Tally 链上执行的提案。治理数据分散在这些不同的系统中,每个系统都有自己的 API 和模式。仪表板的目标是将这些信息统一到提案生命周期的单一视图中。一旦我们拥有了这些聚合数据集,我们意识到这是一个尝试新事物的绝佳机会:我们希望让用户能够用自然语言询问有关提案的问题,并获得有用的回答。这正是 RAG (检索增强生成) 系统的天然用武之地。
在本文中,我们将介绍我们是如何构建它的,涵盖 RAG 系统的主要组成部分,展示部分代码,并分享我们在此过程中的心得。
LLM (大语言模型) 基本上是一个“冻结的快照”。它从截止日期前的训练数据中学习模式,对昨天发生的事情一无所知。它从未见过你的内部文档、论坛帖子或数据库。
这就是为什么大多数现实世界的 LLM 产品并不直接交付原始模型,而是交付一个围绕模型的“外壳 (harness)”。所谓“外壳”,是指将模型转化为应用程序的组件:它们决定在用户 Prompt 中加入什么上下文、模型可以调用哪些工具(网页搜索、文件读写等)、允许做什么,以及如何保持 LLM 的可靠性。ChatGPT、Claude Code 等都是这种外壳的例子。
当你向原始模型询问有关你的数据的问题时,它通常会做两件事之一:
第二种情况更糟。它听起来很正确,如果不亲自检查来源,你无法判断它是错的,这违背了询问模型的初衷。
这就是 RAG 的用武之地。在模型回答之前,你先在文档中搜索相关的段落,并将它们粘贴到 Prompt 中。即:检索 (Retrieve),再 生成 (Generate)。模型不需要“知道”任何事情,它只需要阅读正确的上下文并整理出答案。RAG 只是外壳的一部分,你可能已经在不知不觉中使用过它了——当你向 ChatGPT 上传 PDF 并提问时,底层运行的就是 RAG。
以下是 RAG 的思维模型:

听起来很简单,但我们发现每个步骤都可能出错。
现在的模型拥有 128k、200k 甚至 1M Token 的上下文窗口。那么显而易见的问题是:为什么不跳过检索,直接把整个知识库粘贴到 Prompt 里?
对于小规模数据这没问题,但随着数据增长,这种方法就会失效。有些公司拥有 100k+ 的文档,远超任何上下文窗口的容量。即使放得下,200k Token 的单次查询成本也非常高。而一个只发送约 15 个相关分块(约 3k-5k Token)的检索系统,成本仅为前者的零头。此外,Token 越多意味着响应越慢,用户体验会变差。
更严重的问题是“中间迷失”现象。斯坦福大学的研究表明,LLM 对长上下文的开头和结尾关注较多,但容易忽略中间的内容。如果你粘贴 500 页内容,答案在第 247 页,模型可能会漏掉它。这不是因为窗口不够大,而是因为注意力分配不均。
此外,当你把所有东西都塞进去时,你实际上是在要求模型的注意力机制为你做检索工作,而这并非它的强项。使用 Embeddings 和相似度搜索的专用检索系统在寻找相关内容方面表现更好。
Anthropic 在其文档中提到:如果你的知识库在 200k Token 以内,有时可以跳过 RAG;但一旦超过这个阈值,就需要检索。即使在阈值以下,检索通常也能提供更好的答案,因为它能精准定位段落,而不是让模型在文字海洋中捞针。
典型的 RAG 流水线如下所示:

让我们逐步分析。
原始数据并不直接适用于检索。在我们的 Arbitrum 仪表板中,一个提案的数据散落在三个系统中:论坛有讨论帖,Snapshot 有投票元数据,Tally 有链上状态。如果直接嵌入原始 API 响应,检索效果会很差。例如,用户问“谁提出了 STIP 提案?”,如果作者字段埋在从未被索引为可搜索文本的 JSON 块中,系统就无法回答。
因此,Ingestion 步骤是将原始数据塑造成真正值得搜索的文档。我们构建了两种类型的文档:
我们选择按帖子拆分,是因为像“谁对时间表提出了担忧?”这类问题需要归因。如果只嵌入提案级别的摘要,就会丢失“谁说了什么”的信号。
有了文档后,需要将其拆分成适合检索的小块。
不能嵌入整个文档的原因是:Embeddings 向量本质上是平均值。当你嵌入一篇长文时,得到的向量代表了全文的平均含义。如果一篇 3000 字的文章在第 2 段谈论预算,在第 8 段谈论技术风险,生成的向量可能无法很好地代表其中任何一个主题。
Chunking 将文档拆分为更小的部分,使每一块都有更集中的含义,从而让 Embeddings 向量能更准确地表达其内容。
这里存在权衡:
我们采用了 512 Token 大小和 50 Token 重叠。重叠可以确保跨越分块边界的信息不会丢失。
Embeddings 模型将一段文本映射到高维空间中的一个点(例如 1536 维的向量)。核心在于:含义相似的文本在空间中的位置也相互接近。
这是 RAG 工作的核心。用户问“代表们提出了什么反对意见?”,源文档可能写着“对实施时间表和问责制表示担忧”。关键词搜索(词法搜索)可能找不到它,但 Embeddings 搜索(语义搜索)可以,因为两者的含义相近。
我们使用 OpenAI 的 text-embedding-3-large 模型,并使用 余弦相似度 (Cosine Similarity) 来衡量向量间的距离。
我们选择 pgvector 来存储向量,因为 Arbitrum 仪表板已经在使用 Postgres。pgvector 只是一个扩展,允许向量与关系数据共存。
我们使用 LlamaIndex 的 PGVectorStore 来管理向量表。这样我们就有两个系统在同一个数据库中并存:应用使用 Drizzle ORM 处理关系数据(提案、用户等),LlamaIndex 管理向量表。
每个分块都带有结构化元数据(如 proposal_id、author_name),这允许我们进行过滤检索,例如:“仅显示来自活跃提案的分块”。
当用户提问时,我们需要找到正确的块。最简单的版本是:嵌入用户的问题,找到 Top-K 个最相似的块。
为了解决纯向量检索在处理特定标识符(如“AIP-42”)时的不足,可以使用 混合搜索 (Hybrid Search),结合向量搜索和传统的关键字搜索 (BM25)。此外,还可以使用 重排序 (Reranking) 进一步优化结果。
检索到相关块后,LLM 阅读它们并给出答案。这里有两点需要注意:
RAG 有两个独立的子系统可能失效:检索失效(找错了块)和生成失效(找对了块但模型胡说八道)。
text-embedding-3-large 或 Voyage-3-largeSentenceSplitter (512 Token,50 Token 重叠)
- 原文链接: x.com/buidlguidl/status/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!