现代大型语言模型 (LLM) 令人印象深刻,但它们有一个主要限制:它们的知识固定在权重中,这使得更新和扩展它们的知识变得困难。检索增强生成 (RAG) 是一种旨在解决此问题的方法。它由 Meta 于 2020 年推出,将语言模型连接到外部知识库(例如,一组文档),以便它可以将最新和特定的信息整合到其响应中。在实践中,对于每个提出的问题,RAG 系统首先从其文档库中提取相关内容,然后通过将检索到的上下文与 LLM 的语言能力相结合来生成响应。
注意:本文中提到的示例项目的完整源代码可在 GitHub 上找到。
文章大纲
-
什么是 RAG 以及为什么要使用它?
-
RAG 系统的架构
-
使用 TypeScript 的实践实现
- 使用 Bun 进行项目设置
- LangChain 集成
- Ollama 和 Qdrant 配置
-
代码分析和最佳实践
-
技术栈的优势
- Bun 相对于 Node.js 的性能
- LangChain 的简单性
- Ollama 的灵活性
- Qdrant 的可扩展性
-
更进一步
什么是 RAG 以及为什么要使用它?
检索增强生成 (RAG) 字面意思是“通过检索增强的生成”。其思想是将知识与模型分离。我们不是试图将所有信息都整合到 LLM 的参数中(通过昂贵的微调),也不是设计一个从数据中预测响应的经典模型,而是让主模型生成文本,并通过信息检索的中间步骤来增强它。典型的 RAG 管道工作流程如下:
- 用户查询 – 用户以自然语言提出问题或提供查询(例如,“这个项目中 X 类是用来做什么的?”)。
- 搜索相关文档 – 系统将此问题转换为向量表示(嵌入),然后查询向量数据库以检索语义上与查询最相似的文档或段落。这可以识别相关上下文(例如,文档、代码或与问题对应的文章的摘录)。
- 上下文 + 问题组合 – 检索到的文档或摘录随后作为上下文提供给语言模型。在实践中,它们被插入到 LLM 的提示中,通常通过系统消息或在用户问题前加上找到的文档文本。
- 响应生成 – 语言模型 (LLM) 然后根据问题和提供的上下文生成响应。响应应包含来自文档的信息,并借助 LLM 的能力连贯地表述出来。
这个过程允许模型在生成时依赖特定的外部知识,而无需永久记住它。这可以比作一个人在面对问题时,会在回答之前查阅书籍或参考文件:LLM 在说话之前“搜索其图书馆”。
RAG 的具体用例
每当对话助手需要处理不断发展或庞大的知识库时,RAG 方法就特别有用。以下是一些 RAG 相对于传统方法表现出色的具体用例示例:
文档聊天机器人:由公司的技术文档驱动的助手,能够通过直接从手册、内部知识库甚至源代码中提取信息来回答开发人员或客户的问题。例如,模型可以连接到 API 规范或开源项目代码,以解释函数的工作原理或某种设计的原因。
动态常见问题解答:在客户支持场景中,RAG 聊天机器人可以根据最新的政策或产品数据回答常见问题 (FAQ)。如果政策(例如退货条件)发生变化,您只需更新参考文档,机器人就会立即将其考虑在内,而无需重新训练。这会产生始终保持最新的常见问题解答,并能够提供信息来源以支持答案。
法律助手:助手可以通过在法律、判例法或合同数据库中查找给定问题的相关段落,然后用自然语言表述答案,来帮助律师或法律专业人士。模型不需要记住整部民法典;它只需要查找适当的条款。这同样适用于医疗助手,它可以查询科学出版物或医疗协议数据库,以提供基于最新临床知识的答案。
编程助手:这就是我们示例项目的情况——一个了解代码库内容并能回答有关此代码(架构、模块作用、潜在错误等)问题的助手。我们不是训练专门的编程模型,而是使用通过在存储库中搜索相关代码文件来增强的通用 LLM。
RAG 系统的架构
基本组件
一个完整的 RAG 系统通常包括以下组件:
-
索引和存储
- 文档处理器(提取、清理、分块)
- 嵌入生成器(转换为向量)
- 向量数据库(存储和搜索)
-
查询管道
-
生成和后处理
数据流
1// RAG 系统中数据流的简化示例
2async function processQuery(query: string): Promise<string> {
3 // 1. 将查询转换为嵌入
4 const queryEmbedding = await embedder.embedQuery(query);
5
6 // 2. 搜索相关文档
7 const relevantDocs = await vectorStore.similaritySearch(queryEmbedding);
8
9 // 3. 使用上下文构建提示
10 const prompt = buildPromptWithContext(query, relevantDocs);
11
12 // 4. 使用 LLM 生成响应
13 const response = await llm.generate(prompt);
14
15 return response;
16}
技术选择
对于我们的实现,我们选择了一个现代且高性能的技术栈:
- Bun:超快的 JavaScript 运行时,非常适合服务器应用程序
- TypeScript:静态类型,提高可维护性
- LangChain:用于构建基于 LLM 的应用程序的框架
- Ollama:用于在本地运行语言模型的工具
- Qdrant:高性能且易于部署的向量数据库
这种组合在性能、开发易用性和灵活性之间提供了极好的平衡。
使用 TypeScript 的实践实现
使用 Bun 进行项目设置
让我们从初始化我们的项目开始:
1# 创建项目
2mkdir rag-code-assistant
3cd rag-code-assistant
4bun init
5
6# 安装依赖项
7bun add langchain @langchain/community @langchain/openai
8bun add qdrant-js ollama
9bun add -d typescript @types/node
基本配置
1// src/config.ts
2export const CONFIG = {
3 // Ollama 配置
4 OLLAMA_BASE_URL: 'http://localhost:11434',
5 OLLAMA_MODEL_NAME: 'llama3',
6
7 // Qdrant 配置
8 QDRANT_URL: 'http://localhost:6333',
9 QDRANT_COLLECTION_NAME: 'code_repository',
10
11 // 索引配置
12 CHUNK_SIZE: 1000,
13 CHUNK_OVERLAP: 200,
14
15 // 要索引的代码库路径
16 REPOSITORY_PATH: './repository',
17};
文档索引
索引是 RAG 系统中的关键步骤。它涉及将原始文档转换为适当大小的块,然后为每个块生成嵌入。
1// src/indexer.ts
2import { DirectoryLoader } from 'langchain/document_loaders/fs/directory';
3import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
4import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
5import { QdrantClient } from 'qdrant-js';
6import { CONFIG } from './config';
7
8export async function indexRepository() {
9 // 1. 加载存储库文件
10 const loader = new DirectoryLoader(
11 CONFIG.REPOSITORY_PATH,
12 {
13 '.ts': (path) => new TextLoader(path),
14 '.js': (path) => new TextLoader(path),
15 '.tsx': (path) => new TextLoader(path),
16 '.jsx': (path) => new TextLoader(path),
17 '.md': (path) => new TextLoader(path),
18 }
19 );
20
21 const docs = await loader.load();
22 console.log(`加载 ${docs.length} 个文件完成`);
23
24 // 2. 分割成块
25 const textSplitter = new RecursiveCharacterTextSplitter({
26 chunkSize: CONFIG.CHUNK_SIZE,
27 chunkOverlap: CONFIG.CHUNK_OVERLAP,
28 });
29
30 const chunks = await textSplitter.splitDocuments(docs);
31 console.log(`创建 ${chunks.length} 个块完成`);
32
33 // 3. 生成嵌入
34 const embeddings = new OllamaEmbeddings({
35 baseUrl: CONFIG.OLLAMA_BASE_URL,
36 model: CONFIG.OLLAMA_MODEL_NAME,
37 });
38
39 // 4. 存储在 Qdrant 中
40 const qdrant = new QdrantClient({ url: CONFIG.QDRANT_URL });
41
42 // 如果集合不存在则创建
43 try {
44 await qdrant.createCollection(CONFIG.QDRANT_COLLECTION_NAME, {
45 vectors: {
46 size: 1536, // 嵌入向量的大小
47 distance: 'Cosine',
48 },
49 });
50 } catch (e) {
51 console.log('集合已存在或出错:', e.message);
52 }
53
54 // 插入文档及其嵌入
55 for (let i = 0; i < chunks.length; i += 100) {
56 const batch = chunks.slice(i, i + 100);
57 const batchEmbeddings = await embeddings.embedDocuments(
58 batch.map(chunk => chunk.pageContent)
59 );
60
61 await qdrant.upsert(CONFIG.QDRANT_COLLECTION_NAME, {
62 points: batchEmbeddings.map((vector, idx) => ({
63 id: `${i + idx}`,
64 vector,
65 payload: {
66 text: batch[idx].pageContent,
67 metadata: batch[idx].metadata,
68 },
69 })),
70 });
71
72 console.log(`处理批次 ${i}-${i + batch.length} 完成`);
73 }
74
75 console.log('索引成功完成!');
76}
搜索和响应生成
1// src/rag.ts
2import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
3import { QdrantClient } from 'qdrant-js';
4import { Ollama } from '@langchain/community/llms/ollama';
5import { CONFIG } from './config';
6
7export async function generateResponse(query: string): Promise<string> {
8 // 1. 初始化组件
9 const embeddings = new OllamaEmbeddings({
10 baseUrl: CONFIG.OLLAMA_BASE_URL,
11 model: CONFIG.OLLAMA_MODEL_NAME,
12 });
13
14 const qdrant = new QdrantClient({ url: CONFIG.QDRANT_URL });
15
16 const llm = new Ollama({
17 baseUrl: CONFIG.OLLAMA_BASE_URL,
18 model: CONFIG.OLLAMA_MODEL_NAME,
19 });
20
21 // 2. 生成查询嵌入
22 const queryEmbedding = await embeddings.embedQuery(query);
23
24 // 3. 搜索相关文档
25 const searchResults = await qdrant.search(CONFIG.QDRANT_COLLECTION_NAME, {
26 vector: queryEmbedding,
27 limit: 5,
28 });
29
30 const relevantDocs = searchResults.map(result => result.payload.text).join('\n\n');
31
32 // 4. 使用上下文构建提示
33 const prompt = `
34 您是一位专家编程助手,帮助理解项目的源代码。
35 使用以下上下文来回答用户的问题。
36 如果您在上下文中找不到信息,请明确说明。
37
38 上下文:
39 ${relevantDocs}
40
41 问题: ${query}
42
43 回答:
44 `;
45
46 // 5. 生成响应
47 const response = await llm.call(prompt);
48
49 return response;
50}
简单的用户界面
1// src/index.ts
2import { indexRepository } from './indexer';
3import { generateResponse } from './rag';
4
5async function main() {
6 // 用于索引或查询的参数
7 const args = process.argv.slice(2);
8
9 if (args[0] === 'index') {
10 console.log('开始索引...');
11 await indexRepository();
12 } else if (args[0] === 'query') {
13 const query = args.slice(1).join(' ');
14 if (!query) {
15 console.log('用法: bun run src/index.ts query "您关于代码的问题"');
16 process.exit(1);
17 }
18
19 console.log(`问题: ${query}`);
20 console.log('正在生成响应...');
21 const response = await generateResponse(query);
22 console.log('\n回答:');
23 console.log(response);
24 } else {
25 console.log('用法: bun run src/index.ts [index|query "您的问题"]');
26 }
27}
28
29main().catch(console.error);
代码分析和最佳实践
高效分块
将文档分割成块是直接影响结果质量的关键步骤。一些最佳实践:
- 适当的大小:块应足够大以包含上下文,但又不能太大以保持相关性(通常在 500 到 1500 个字符之间)。
- 重叠:块之间的重叠可防止在边界处丢失上下文。
- 语义分割:理想情况下,分割应尊重文档的语义结构(段落、函数等)。
搜索优化
语义搜索的质量至关重要:
- 元数据过滤器:使用元数据(文件类型、日期、作者)来细化搜索。
- 重新排序:应用第二级过滤以提高相关性。
- 多样性:确保结果的多样性以涵盖问题的不同方面。
高级提示
提示构建是一门艺术,它强烈影响响应的质量:
1// 更高级提示的示例
2const prompt = `
3# 角色
4您是一位专门从事源代码分析的专家编程助手。
5
6# 上下文
7${relevantDocs}
8
9# 说明
10- 仅根据提供的上下文回答用户的问题
11- 如果上下文不包含必要的信息,请明确指出
12- 在您的回答中引用相关的代码摘录
13- 以教学的方式解释代码
14- 如果发现任何问题,请提出改进建议
15
16# 问题
17${query}
18`;
技术栈的优势
Bun 相对于 Node.js 的性能
Bun 为此类应用程序提供了显著优势:
- 快速启动:启动时间比 Node.js 快 4 倍
- 优化执行:卓越的执行性能,尤其是在 I/O 操作方面
- 集成打包器:简化开发工作流程
LangChain 的简单性
LangChain 极大地促进了基于 LLM 的应用程序的开发:
- 抽象:为不同的模型和提供商提供统一的接口
- 可重用组件:即用型链、代理和工具
- 既定模式:常见用例的参考实现
Ollama 的灵活性
Ollama 允许以极大的灵活性在本地运行语言模型:
- 本地模型:不依赖外部 API
- 隐私:数据保留在您的基础架构上
- 定制:可以根据您的需求调整模型
Qdrant 的可扩展性
Qdrant 是专为语义搜索设计的现代向量数据库:
- 性能:针对快速相似性搜索进行了优化
- 过滤:对元数据的高级过滤功能
- 灵活部署:可在嵌入模式下使用或作为服务使用
更进一步
高级优化
- 混合搜索:结合向量搜索和关键字搜索
- 分层分块:对块使用不同的粒度级别
- 缓存:缓存搜索结果和频繁响应
评估和指标
衡量 RAG 系统质量:
- 相关性:检索到的文档是否与问题相关?
- 忠实性:答案是否忠实于源文档?
- 有用性:答案是否有效解决了用户的问题?
技术替代方案
- 框架:Haystack、LlamaIndex 作为 LangChain 的替代方案
- 向量数据库:Pinecone、Weaviate、Milvus 作为 Qdrant 的替代方案
- 模型:不同的本地模型(Llama、Mistral)或 API(OpenAI、Anthropic)
结论
检索增强生成代表了我们在特定用例中利用语言模型方式的重大进步。通过将知识与生成模型分离,RAG 能够创建更准确、更新、更透明的 AI 助手。
我们使用 TypeScript、Bun、LangChain、Ollama 和 Qdrant 的实现表明,现在可以使用现代且易于访问的技术构建高性能的 RAG 系统。这种方法为新一代 AI 助手铺平了道路,这些助手能够基于特定知识库进行推理,同时保持大型语言模型的流畅性和连贯性。
欢迎在 GitHub 上探索完整的源代码,并根据您自己的用例进行调整。RAG 是一项不断发展的技术,在这个激动人心的领域存在着大量的创新机会。