首页 / 文章 / 去神秘化的RAG:先检索再生成

去神秘化的RAG:先检索再生成

从分块与索引到带有引用运算符的可靠答案,存在一条清晰的路径可供核查。

2179 词

可将此内容视为针对操作人员的《RAG:我希望有人能这样向我解释》一文的重构版本:清晰的阶段划分、有序的代码模块,以及能在交接过程中保留的恢复说明。将概览视作可度量的界面元素使用效果最佳,在扩大范围之前,先记录一份理想的操作流程、一个故障案例以及回滚说明。同时记录正常流程与恢复流程,重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续的优化内容。

1. 令RAG概念一目了然的类比

1. 令RAG系统行之有效的类比:在修改代码之前,先明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应采用小型、可测试的单元。当某个步骤失败时,故障原因应能指向单一责任点,而非复杂的流程链。必须引用实际作为答案依据的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失所致。

2. 每个RAG系统的两大组成部分

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

3. 第一阶段——索引构建:将文档转化为可搜索的内容

对于第3阶段——索引:将文档转化为可搜索的内容,在修改代码之前需明确输入数据、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。应在功能结果旁记录处理时间以及令牌或查询成本。提前显示成本信息,可避免在从演示环境过渡到共享环境时出现意外费用。需注明实际用于得出答案的对应内容。如果没有引用,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

第1步:加载文档

在第一步:加载文档中,应在修改代码之前明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,这样操作人员无需查看整个系统结构即可进行审核。 需注明实际作为答案依据的段落。如果没有引用,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()

print(f"Loaded {len(documents)} pages")

第二步:进行分块处理——这一步的重要性超乎人们想象

第二步:将内容切分——这一步的重要性远超人们的想象。在修改代码之前,需明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品本身的组成部分,而非后续需要补充的功能。 要引用那些真正作为答案依据的段落。如果没有引用,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # characters per chunk
    chunk_overlap=80,    # overlap between consecutive chunks
    separators=["\n\n", "\n", ". ", " "]
)

chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")

第三步:嵌入切分后的内容

在第3步:嵌入分块内容中,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非冗长的脚本。当某一步骤失败时,故障应指向单一责任点,而非复杂的流程链。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

from langchain_openai import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# under the hood, this is what happens per chunk:
vector = embedding_model.embed_query("30-day refund window for annual plans")
print(len(vector))   # e.g. 1536 numbers representing this sentence's meaning
        "refund policy" •
                          \
                           • "money-back guarantee"
   "vacation days" •
                     \
                      • "paid time off"

第4步:将向量存储在向量数据库中

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

from langchain_community.vectorstores import FAISS

vectorstore = FAISS.from_documents(chunks, embedding_model)
vectorstore.save_local("faiss_index")

4. 第二阶段——检索与生成:回答真实问题

对于4. 第二阶段——检索+生成:回答真实问题,在修改代码之前需明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在从演示环境切换到共享环境时出现意外费用。 需注明实际用于生成答案的对应内容。如果没有引用,操作人员就无法区分是幻觉还是索引缺失导致的错误。 对于4. 第二阶段——检索+生成:回答真实问题,在修改代码之前需明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和故障恢复流程。重新检索

这些功能,包括人工干预机制和死信处理,都是产品本身的组成部分,而非后续添加的优化功能。

第一步:使用相同模型嵌入查询

在执行第一步:使用相同模型嵌入查询时,首先明确需求规范:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 建议采用小型、可测试的单元而非庞大的脚本。当某个步骤出现故障时,故障应指向单一的责任模块,而非复杂的流程链。 对稳定的系统指令和工具结构进行缓存。重复发送相同的开头信息是造成资源浪费的常见原因。

query = "Can I get a refund on my annual subscription?"

第二步:获取最相关的内容片段

在执行第2步:获取最相关的片段时,首先写下相关契约:所需输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来衡量召回率。仅仅更换提示词很难解决检索效果不佳的问题。

retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_chunks = retriever.invoke(query)

for chunk in relevant_chunks:
    print(chunk.page_content[:100], "...")

第3步:将片段整合到提示词中

在执行步骤3:将各部分内容整合到提示语中时,首先需列出相关规范:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及代币或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 缓存稳定的系统指令和工具结构。重复发送相同的前置内容是导致资源浪费的常见原因。 在执行步骤3:将各部分内容整合到提示语中时,首先需列出相关规范:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理方式都是产品不可或缺的部分,而非后续需要补充的内容。

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("""
Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have that information."

Context:
{context}

Question: {question}
""")

第4步:将所有部分整合起来

第4步:将所有部分整合起来 若被视为可度量的结构,则效果最佳。在扩大范围之前,先记录一份优秀的处理结果、一个失败案例以及回滚说明。 相比庞大的脚本,应优先选择小型且可测试的单元。当某一步骤出现故障时,故障原因应能明确指向某个具体的责任模块,而非复杂的流程链。 应将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。

from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke("Can I get a refund on my annual subscription?")
print(answer)

5. 为何这比直接将整份文档粘贴到提示词中更有效

5. 为何这种方法优于直接将整份文档粘贴到提示词中:将其视为可衡量的标准能发挥最佳效果。在扩大范围之前,先记录一份理想的输出结果、一个失败案例以及回滚说明。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,杜绝默默完成部分任务的情况。 为每轮对话及每次会话设定token预算。智能工具往往会过度扩展上下文;设置上限可避免演示过程变成意外的费用账单。

6. 常见误区(血的教训)

6. 常见陷阱(血的教训)若将其视为可度量的对象来处理,效果最佳。在扩大范围之前,先记录一份理想的处理结果、一个失败案例以及回滚说明。 在功能结果旁同时记录处理时间以及令牌或查询成本。尽早了解成本情况,可避免在系统从演示环境转向共享环境时出现意外账单。 应将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。 6. 常见陷阱(血的教训)若将其视为可度量的对象来处理,效果最佳。在扩大范围之前,先记录一份理想的处理结果、一个失败案例以及回滚说明。 需同时记录正常流程与故障恢复流程。重试机制、人工审核环节以及死信处理都是产品的一部分,而非后续需要补充的内容。

7. 后续方向

在7. 后续行动方向部分,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应优先使用小型、可测试的单元。当某个步骤失败时,故障原因应指向单一责任点,而非复杂的流程链。需引用实际作为答案依据的段落;没有引用的话,操作人员就无法区分是虚假信息还是索引缺失所致。

操作检查清单

在制定操作检查清单时,首先需写明相关约定:所需输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。

将配置信息置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放,这样操作人员无需查看全部内容即可进行审计。

在调整提示词之前,先在固定的问题集上测试召回率。频繁更换提示词很难解决检索效果不佳的问题。

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

同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理方式都是产品的一部分,而非后续需要补充的内容。

在调整提示词之前,先在固定的问题集上测试召回率。频繁更换提示词很难解决检索效果不佳的问题。

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

关于372916c7a432的批处理说明:请将服务提供商密钥移出代码仓库,设定单会话令牌上限,并将日志存储在评估用示例文件旁,以便后续模型更换时保持数据可比性。