首页 / 文章 / 实用笔记:RAG详解:应用如何让人工智能访问自身的数据

实用笔记:RAG详解:应用如何让人工智能访问自身的数据

《实用笔记》操作指南:详解RAG——应用如何让AI访问自身的合同、支票文件以及适用于采用该模式的团队的即插即用代码模块。

2693 词

可将此内容作为《RAG详解:应用如何让AI访问自身数据》中理念的面向操作人员的简化版本:清晰的阶段划分、有序的代码模块,以及便于交接时参考的恢复说明。在“概览”阶段,若能将其视为可量化的界面来使用效果最佳——在扩大范围之前,先记录一份理想的操作日志、一个故障案例以及对应的回滚说明。相比冗长的脚本,更应采用小型且可测试的单元。当某一步骤出现故障时,故障原因应能明确指向某个具体责任点,而非复杂的流程问题。

什么是检索增强生成(RAG)?

在“什么是检索增强生成”阶段,应在修改代码之前明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分幻觉内容与索引缺失的问题。

Without RAG:
[User Question] ──> [LLM] ──> [Answer based on generic training data]
With RAG:
[User Question] ──> [Search Engine / Vector DB]
                          │
                          └──> [Relevant Documents]
                                     │
[User Question] + [Relevant Documents] ──> [LLM] ──> [Factual Answer]

为何RAG很重要:它解决的局限性

在修改代码之前,首先明确“为何RAG如此重要”这一目标,确定输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。必须注明实际作为答案依据的文本段落;没有引用的话,操作人员就无法区分幻觉内容与索引缺失导致的错误。

RAG在幕后是如何工作的?

在“RAG是如何工作的”这一阶段,应在修改代码之前明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,以便操作人员无需查看整个系统结构即可进行审核。 需注明实际作为答案依据的段落。如果没有引用,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。 在“RAG是如何工作的”这一阶段,应在修改代码之前明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 相较于庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个具体的责任模块,而非整个复杂的流程。

1. Ingestion Pipeline (Offline / Background):
[Raw Documents] ──> [Chunking] ──> [Embedding Model] ──> [Vector Database]
2. Query Pipeline (Runtime):
[User Question] ──> [Embedding Model] ──> [Vector Search in DB]
                                                  │
                                                  ▼
[User Question] + [Top Matching Chunks] ──> [LLM] ──> [Final Answer]

1. 分块处理

在进入分块处理阶段时,首先写下相关契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词往往无法改善较差的检索效果。

2. 嵌入模型

在处理“2个嵌入模型”阶段时,首先写下相关规范:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词很难改善较差的检索效果。

3. 向量存储

在完成三个向量存储阶段的工作时,首先需明确相关规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 应将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中管理,以便操作人员无需查看整个系统结构即可进行审计。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词往往无法解决检索效果不佳的问题。 在完成三个向量存储阶段的工作时,首先需明确相关规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 相比庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤出现故障时,故障点应能明确指向某个具体的功能模块,而非整个复杂的流程。

4. 向量搜索(余弦相似度)

将4向量搜索余弦相似度阶段视为可测量的表面时,其效果最佳。在扩大范围之前,需记录一个成功的案例、一个失败案例以及回滚说明。 将该阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现无声无息的半完成状态。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。

5. 提示词增强

将“5个提示增强阶段”视为可测量的指标时,其效果最佳。在扩大范围之前,先记录一份理想的输出结果、一个失败案例以及回滚说明。在功能结果旁同时记录处理时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外账单。为每轮对话和每次会话设定令牌预算。智能工具往往会大量消耗上下文资源,设置上限能防止演示环境变成令人意外的收费来源。

在 Node.js 中的实际实现

在 Node 阶段进行实际实施时,若能将其视为可度量的对象,则效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。 应将配置与应用程序代码分开。环境文件、密钥存储和功能开关应集中存放于一处,以便操作人员无需查看整个系统结构即可进行审计。 需将分块策略与检索策略区分开来。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。 在 Node 阶段进行实际实施时,若能将其视为可度量的对象,则效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。 相比庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤出现故障时,故障点应指向单一的责任模块,而非复杂的处理流程。

第一步:文档的导入与向量化处理

在第一步的数据摄入与处理阶段,应在修改代码之前明确输入内容、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏的状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

// ingest.js
import { OpenAI } from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// In a real application, you would load these from Markdown files or a CMS
const documents = [
  {
    id: "doc_1",
    text: "Enterprise customers can request a full refund within 30 days of contract signing. Contact enterprise-support@example.com."
  },
  {
    id: "doc_2",
    text: "Monthly self-serve subscriptions are non-refundable once the billing cycle begins. Users can cancel anytime to avoid future charges."
  },
  {
    id: "doc_3",
    text: "Custom engineering work and onboarding packages are strictly non-refundable once the kickoff meeting has taken place."
  }
];
async function createEmbedding(text) {
  const response = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: text,
  });
  return response.data[0].embedding;
}
export async function buildKnowledgeBase() {
  const embeddedDocs = [];
  for (const doc of documents) {
    const vector = await createEmbedding(doc.text);
    embeddedDocs.push({ ...doc, vector });
  }
  return embeddedDocs;
}

第二步:查找相关上下文

在“第二步:查找相关内容”阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前显示成本信息,可避免在从演示环境切换到共享环境时出现意外费用。必须注明实际作为答案依据的段落内容;若没有引用,操作人员就无法区分是幻觉结果还是索引缺失导致的错误。

// search.js
function cosineSimilarity(vecA, vecB) {
  let dotProduct = 0;
  let normA = 0;
  let normB = 0;
  for (let i = 0; i < vecA.length; i++) {
    dotProduct += vecA[i] * vecB[i];
    normA += vecA[i] * vecA[i];
    normB += vecB[i] * vecB[i];
  }
  return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
export function findTopMatches(queryVector, knowledgeBase, topK = 2) {
  return knowledgeBase
    .map(doc => ({
      ...doc,
      score: cosineSimilarity(queryVector, doc.vector)
    }))
    .sort((a, b) => b.score - a.score)
    .slice(0, topK);
}

第三步:优化提示词并生成答案

在第三步“增强阶段”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,以便操作人员无需查看整个流程即可进行审计。 当下一步是代码执行或工具调用时,宜使用具有结构化格式且经过模式验证的输出,而非自由形式的文字描述。 在第三步“增强阶段”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 相比庞大的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应能明确指向某个特定功能,而非整个复杂的流程。

// answer.js
import { OpenAI } from "openai";
import { createEmbedding, buildKnowledgeBase } from "./ingest.js";
import { findTopMatches } from "./search.js";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function askKnowledgeBase(userQuestion, knowledgeBase) {
  // 1. Convert user question into an embedding
  const questionVector = await createEmbedding(userQuestion);
  // 2. Retrieve top matching chunks
  const matches = findTopMatches(questionVector, knowledgeBase, 1);
  const contextText = matches.map(m => m.text).join("\n---\n");
  // 3. Augment prompt with retrieved context
  const systemPrompt = `
You are a helpful customer service assistant.
Answer the user's question using ONLY the context provided below.
If the context does not contain the answer, say "I do not have enough information to answer that."
Context:
${contextText}
`;
  // 4. Generate answer
  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: systemPrompt },
      { role: "user", content: userQuestion }
    ],
    temperature: 0.2, // Low temperature minimizes creative liberties
  });
  return completion.choices[0].message.content;
}
// Example usage:
const knowledgeBase = await buildKnowledgeBase();
const answer = await askKnowledgeBase("I signed an enterprise deal 2 weeks ago, can I get my money back?", knowledgeBase);
console.log(answer);
// Output: Yes, enterprise customers can request a full refund within 30 days of contract signing. You can email enterprise-support@example.com to initiate the process.

开发者在RAG应用中常犯的错误

在处理“开发者常犯的错误”这一阶段时,首先需明确相关规范:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关成果命名,定义成功判定标准,并杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词往往无法改善较差的检索效果。

1. 天真的分块方式

在处理第一阶段的“朴素分块”时,首先写下相关规范:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试召回率。仅仅更换提示词很难改善较差的检索效果。

2. 仅依赖向量搜索

在完成“完全依赖”阶段的开发时,首先需明确相关约定:所需的输入参数、成功判定标准以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 应将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中管理,以便操作人员无需查看整个系统结构即可进行审计。 在调整提示词之前,先使用固定的问题集来测试系统的召回率。仅仅更换提示词往往无法解决检索效果不佳的问题。 在完成“完全依赖”阶段的开发时,首先需明确相关约定:所需的输入参数、成功判定标准以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 相比庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个具体的功能模块,而非整个复杂的流程。

3. 注入过多上下文(中间丢失问题)

“注入过多上下文”这一阶段若被视为可量化的标准,则效果最佳。在扩大范围之前,先收集一份理想的文本样本、一个失败案例以及回滚说明。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,杜绝默许的半完成状态。 应将分块策略与检索策略分开。当质量指标发生变化时,调整其中一项不应强制重新编写另一项。

何时应使用RAG而非微调?

“何时使用”阶段若被视为可度量的指标,则效果最佳。在扩大范围之前,先记录一份优秀的处理结果、一个失败案例以及回滚说明。在功能结果旁同时记录处理时间以及令牌或查询成本。提前了解成本情况,就能避免在系统从演示环境转向共享环境时出现意外费用。应将分块策略与检索策略分开处理,当质量指标发生变化时,修改其中一项无需强制重写另一项。

结论

将“结论阶段”视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。 应将配置与应用程序代码分开。环境文件、密钥存储和功能开关应集中存放,以便操作人员无需查看整个系统结构即可进行审计。 应将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。 将“结论阶段”视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想的处理结果、一个故障案例以及回滚说明。 相比庞大的脚本,更应采用小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向单一责任模块,而非复杂的流程链。

操作检查清单

在处理操作检查清单阶段时,首先写下合同条款:所需的输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。

同时记录正常流程和恢复流程。重试机制、人工审核环节以及错误处理方式都是产品本身的一部分,而非后续的完善工作。

在调整提示词之前,先使用固定的问题集来衡量召回率。仅仅更换提示词很难解决检索效果不佳的问题。

锁定依赖项的版本,并记录用于演示的图像摘要。可重复性比经验知识更为重要。

优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤出现故障时,故障应指向单一的责任模块,而非复杂的流程链。

在调整提示词之前,先使用固定的问题集来衡量召回率。仅仅更换提示词很难解决检索效果不佳的问题。

在推广该技术栈之前,应先冻结版本,为关键路径生成标准记录,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其展示花哨的一次性演示,不如注重扎实的可靠性。

关于2c8785c21af6的批注:请将提供商密钥移出代码仓库,设定单会话令牌上限,并将记录存储在评估用示例文件旁,以便后续模型更换时仍能保持对比性。