快速回答:检索增强生成(RAG)是把两个想法粘在一起。首先,你把文档嵌入一次,并存储得到的向量。然后,对每个问题,你嵌入这个问题,按语义检索最相近的文本块,并把它们作为上下文交给 LLM,使答案来自你的数据,而不是模型的训练记忆。在 sgcWebSockets 中,整条流水线就是三个组件:用 TsgcAIOpenAIEmbeddings 把文本变成向量,用 TsgcAIDatabaseVectorFile 或 TsgcAIDatabaseVectorPinecone 存储并检索它们,用 TsgcHTTP_API_OpenAI 写出最终的、有依据的答案。
通用聊天模型对世界知道很多,但对你的产品手册、你的支持工单或上个季度的内部报告一无所知。问它这些,它要么拒绝回答,要么更糟,编造一些听上去可信的内容。RAG 在不重新训练任何东西的情况下解决了这个问题:你保持模型原样,在提问时把你自己语料库中正确的段落喂给它。下面是 Delphi 中完整的循环,从头到尾,使用真实的组件名称。
RAG 实际做的是什么
嵌入是一串数字,它捕捉一段文本的语义。关于同一主题的两个段落在这个数值空间中会落得很近,即使它们没有共同的关键词。向量数据库存储这些数字,并在给定一个查询向量时,返回按相似度排序的最近条目。RAG 把这些串联起来:
| 阶段 | 发生了什么 | sgcWebSockets 组件 |
|---|---|---|
| 1. 导入(一次) | 把文档拆成文本块,嵌入每个块,存储向量 | TsgcAIOpenAIEmbeddings.CreateEmbeddingsFromFile |
| 2. 存储 | 把向量保存在本地文件或云端索引中 | TsgcAIDatabaseVectorFile · TsgcAIDatabaseVectorPinecone |
| 3. 检索(每个问题) | 嵌入问题,找出最相近的文本块 | GetEmbedding → QueryData |
| 4. 作答 | 把文本块放进提示词,向模型提问 | TsgcHTTP_API_OpenAI._CreateChatCompletion |
当你的数据发生变化时运行第 1 步和第 2 步。第 3 步和第 4 步在每个用户问题上运行。让我们逐一构建。
第 1 步 — 嵌入你的文档
创建一个 TsgcAIOpenAIEmbeddings,给它一个 OpenAI 密钥,把它的 Database 属性指向一个向量存储,然后调用 CreateEmbeddingsFromFile。这一次调用会读取文件、把它拆成文本块(由 EmbeddingsOptions.ChunkSize 控制)、嵌入每个块,并通过 BeginAddData / AddData / EndAddData 序列替你把向量写入存储。
uses
sgcAI, sgcAI_OpenAI_Embeddings,
sgcAI_DB_Vector, sgcAI_DB_Vector_File, sgcAI_DB_Vector_Pinecone;
var
Embeddings: TsgcAIOpenAIEmbeddings;
DBFile: TsgcAIDatabaseVectorFile;
begin
Embeddings := TsgcAIOpenAIEmbeddings.Create(nil);
Embeddings.OpenAIOptions.ApiKey := 'sk-...';
// local, file-based vector store
DBFile := TsgcAIDatabaseVectorFile.Create(nil);
DBFile.VectorFileOptions.InputFilename := 'corpus.sgcif';
DBFile.VectorFileOptions.VectorFilename := 'corpus.sgcvf';
Embeddings.Database := DBFile;
Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;
这就是整个导入步骤。运行它一次,或者每当你的文档变化时运行。默认的嵌入模型是 text-embedding-3-small;如果你需要其他模型,通过 EmbeddingsOptions.Model 修改它。嵌入组件页面上有更详细的说明。
第 2 步 — 选择向量存放在哪里
两种后端都继承自同一个基础组件 TsgcAIDatabaseVector,所以它们可以互换:把其中一个换成另一个,你的导入和查询代码都不用改。唯一的区别是数据存放在哪里。
对于桌面应用、离线工具或较小的语料库,TsgcAIDatabaseVectorFile 把一切保存在本地文件中,不需要任何外部服务。当索引很大、必须跨进程或跨用户共享、或者需要扩展到一台机器之外时,切换到 TsgcAIDatabaseVectorPinecone,它会通过托管的 Pinecone REST API 对每个文本块执行 upsert:
var
DBPinecone: TsgcAIDatabaseVectorPinecone;
begin
DBPinecone := TsgcAIDatabaseVectorPinecone.Create(nil);
DBPinecone.PineconeOptions.ApiKey := 'pc-...';
DBPinecone.PineconeIndexOptions.IndexName := 'sgc-embeddings';
Embeddings.Database := DBPinecone;
Embeddings.CreateEmbeddingsFromFile('docs.txt');
end;
注意,导入那一行与第 1 步完全相同。这正是共享基类的全部意义。文件后端见向量数据库页面,云端后端见 Pinecone 页面。
第 3 步 — 检索并作答
现在是每个问题的路径。嵌入用户的问题,并在一次调用中找出最相近的已存文本块:GetEmbedding 嵌入文本,并通过数据库的 QueryData 运行它,返回你语料库中最相关的段落。这些段落就是你的上下文。把它们与问题拼接起来,并把整个内容发送给聊天模型:
var
Question, Context, Prompt, Answer: string;
OpenAI: TsgcHTTP_API_OpenAI;
begin
Question := 'How do I enable the WatchDog reconnect?';
// retrieve the closest chunks from your own data
Context := Embeddings.GetEmbedding(Question, '');
// build a grounded prompt
Prompt :=
'Answer the question using only the context below.' + sLineBreak +
'If the context does not contain the answer, say you do not know.' +
sLineBreak + sLineBreak +
'Context:' + sLineBreak + Context + sLineBreak + sLineBreak +
'Question: ' + Question;
// ask the model
OpenAI := TsgcHTTP_API_OpenAI.Create(nil);
OpenAI.OpenAIOptions.ApiKey := 'sk-...';
Answer := OpenAI._CreateChatCompletion('gpt-4o-mini', Prompt);
Memo1.Lines.Text := Answer;
end;
这就是 RAG。模型在训练期间从未见过你的文档,但它却能基于它们作答,因为你在请求时把相关段落摆在了它面前。改变语料库,答案就随之改变,不涉及任何微调。当上下文为空时让模型拒绝回答的指令,正是让模型保持诚实、而不是靠猜的关键。
本地还是云端,代码相同
有一个细节值得重复:在文件存储和 Pinecone 之间的选择是可逆的。由于 TsgcAIDatabaseVectorFile 和 TsgcAIDatabaseVectorPinecone 共享 TsgcAIDatabaseVector 基类,你可以先用本地文件做原型(零基础设施、可离线运行),稍后再通过更换你赋给 Embeddings.Database 的组件迁移到 Pinecone。你的导入或查询代码无需任何改动。最后的 LLM 也是如此:TsgcHTTP_API_OpenAI 上的 _CreateChatCompletion 可以换成 Anthropic 或 Gemini 组件,如果你偏好用不同的模型来写最终答案。
关于分块和质量的说明
检索质量取决于你如何拆分文档。更小的文本块让匹配更精确,但可能丢失上下文;更大的文本块保留上下文,但稀释了匹配。EmbeddingsOptions.ChunkSize 为 CreateEmbeddingsFromFile 控制这一点,所以针对你的材料调优它是值得的。要做更精细的控制,你也可以用 CreateEmbeddings 嵌入单条字符串,并在导入前自行塑造文本块。
开始使用
这三个组件都随 sgcWebSockets 一起提供。获取免费试用版,把嵌入组件和向量存储组件拖到窗体上,把它们指向一个文本文件,你就能在远少于一百行的代码中得到一个可用的 RAG 循环。在 AI & LLM 组件中心浏览完整的 AI 构建模块集合。
对于把这套方案应用到你自己的语料库有疑问吗?联系我们 — 你会收到来自代码编写者本人的回复。
