去神秘化的RAG:先检索再生成
从分块与索引到带有引用运算符的可靠答案,存在一条清晰的路径可供核查。
可将此内容视为针对操作人员的《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的批处理说明:请将服务提供商密钥移出代码仓库,设定单会话令牌上限,并将日志存储在评估用示例文件旁,以便后续模型更换时保持数据可比性。