Delphi 中的 RAG:让 LLM 基于你自己的文档作答

· 组件

快速回答:检索增强生成(RAG)是把两个想法粘在一起。首先,你把文档嵌入一次,并存储得到的向量。然后,对每个问题,你嵌入这个问题,按语义检索最相近的文本块,并把它们作为上下文交给 LLM,使答案来自你的数据,而不是模型的训练记忆。在 sgcWebSockets 中,整条流水线就是三个组件:用 TsgcAIOpenAIEmbeddings 把文本变成向量,用 TsgcAIDatabaseVectorFileTsgcAIDatabaseVectorPinecone 存储并检索它们,用 TsgcHTTP_API_OpenAI 写出最终的、有依据的答案。

通用聊天模型对世界知道很多,但对你的产品手册、你的支持工单或上个季度的内部报告一无所知。问它这些,它要么拒绝回答,要么更糟,编造一些听上去可信的内容。RAG 在不重新训练任何东西的情况下解决了这个问题:你保持模型原样,在提问时把你自己语料库中正确的段落喂给它。下面是 Delphi 中完整的循环,从头到尾,使用真实的组件名称。

RAG 实际做的是什么

嵌入是一串数字,它捕捉一段文本的语义。关于同一主题的两个段落在这个数值空间中会落得很近,即使它们没有共同的关键词。向量数据库存储这些数字,并在给定一个查询向量时,返回按相似度排序的最近条目。RAG 把这些串联起来:

阶段发生了什么sgcWebSockets 组件
1. 导入(一次)把文档拆成文本块,嵌入每个块,存储向量TsgcAIOpenAIEmbeddings.CreateEmbeddingsFromFile
2. 存储把向量保存在本地文件或云端索引中TsgcAIDatabaseVectorFile · TsgcAIDatabaseVectorPinecone
3. 检索(每个问题)嵌入问题,找出最相近的文本块GetEmbeddingQueryData
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 之间的选择是可逆的。由于 TsgcAIDatabaseVectorFileTsgcAIDatabaseVectorPinecone 共享 TsgcAIDatabaseVector 基类,你可以先用本地文件做原型(零基础设施、可离线运行),稍后再通过更换你赋给 Embeddings.Database 的组件迁移到 Pinecone。你的导入或查询代码无需任何改动。最后的 LLM 也是如此:TsgcHTTP_API_OpenAI 上的 _CreateChatCompletion 可以换成 Anthropic 或 Gemini 组件,如果你偏好用不同的模型来写最终答案。

关于分块和质量的说明

检索质量取决于你如何拆分文档。更小的文本块让匹配更精确,但可能丢失上下文;更大的文本块保留上下文,但稀释了匹配。EmbeddingsOptions.ChunkSizeCreateEmbeddingsFromFile 控制这一点,所以针对你的材料调优它是值得的。要做更精细的控制,你也可以用 CreateEmbeddings 嵌入单条字符串,并在导入前自行塑造文本块。

开始使用

这三个组件都随 sgcWebSockets 一起提供。获取免费试用版,把嵌入组件和向量存储组件拖到窗体上,把它们指向一个文本文件,你就能在远少于一百行的代码中得到一个可用的 RAG 循环。在 AI & LLM 组件中心浏览完整的 AI 构建模块集合。

对于把这套方案应用到你自己的语料库有疑问吗?联系我们 — 你会收到来自代码编写者本人的回复。

相关内容