前情回顾
在上一篇文章中,我们用混合搜索解决了纯向量检索对关键词不敏感的问题,让我们的“召回”能力又上了一个台阶。现在,我们手上已经有了一个经过“粗排+精排”或者“混合搜索”得到的高质量相关文档列表。
接下来我们该怎么做?
按照我们之前在第八篇里学到的方法,我们会把这些文档块(Chunks)一股脑儿地塞进一个Prompt模板里,然后交给LLM去生成答案。
这种“一波流”的做法简单直接,但它有几个明显的缺点:
- 上下文窗口限制:如果召回的文档太多、太长,很容易就会超出LLM的上下文窗口限制,导致信息被截断。
- “迷失在中间”问题:研究表明,当LLM处理一个很长的上下文时,它对开头和结尾部分的信息最敏感,中间部分的信息很容易被“忽略”。
- 信息综合能力差:简单地将一堆文档块堆砌在一起,可能会让LLM难以理清头绪,特别是当答案需要综合多个文档块的信息时。
有没有一种更像“人”的思考方式?我们人类在写报告时,通常是先看第一份资料,写个初稿;再看第二份资料,在初稿的基础上补充和修改;再看第三份……不断地迭代和精炼。
今天,我们就来学习这种模拟人类思考过程的高级RAG策略——迭代式精炼(Refine)。
什么是Refine策略?
Refine策略是一种串行处理多个文档的RAG工作流。它不再是一次性地将所有上下文都抛给LLM,而是逐一“喂”给LLM,并让LLM在每一步都对之前的答案进行“反思和改进”。
它的具体流程如下:
- 从检索器获取一个有序的文档列表(比如Top 3:
[Doc1, Doc2, Doc3]
)。 - 第一步:使用一个“初始问答Prompt”,将第一个文档(Doc1)和用户问题结合,生成一个初步答案。
- 第二步:使用一个“精炼答案Prompt”,将第二个文档(Doc2)、用户问题、以及上一步生成的初步答案结合,让LLM在已有答案的基础上,利用新文档的信息来优化和完善答案。
- 第三步:继续使用“精炼答案Prompt”,将第三个文档(Doc3)、用户问题、以及第二步生成的精炼答案结合,再次进行优化。
- …循环往复,直到处理完所有文档,得到最终答案。
核心:两种不同的Prompt模板
实现Refine策略的关键,在于我们需要两个不同的Prompt模板。
1. 初始问答模板 (Initial Prompt)
这个模板用于处理第一个文档,和我们之前学的标准RAG Prompt很像。
INITIAL_PROMPT_TEMPLATE = """
根据下面的上下文信息,回答问题。
---
上下文:
{context}
---
问题:
{question}
---
答案:
"""
2. 精炼答案模板 (Refine Prompt)
这个模板是Refine策略的灵魂,它告诉LLM要扮演一个“编辑”的角色,在已有答案的基础上进行改进。
REFINE_PROMPT_TEMPLATE = """
原始问题是:{question}
我们已经有了一个初步的答案:{existing_answer}
现在我们有了一些额外的上下文信息,希望能进一步完善这个答案。
---
额外上下文:
{context}
---
请结合新的上下文,改进原有的答案。如果新的上下文没有提供有用的信息,就直接返回原有的答案。
完善后的答案:
"""
上手实战:用代码实现Refine循环
让我们通过一个场景来感受Refine的威力。假设用户问“RAG的优缺点是什么?”,而我们召回的文档中,一个讲了优点,另一个讲了缺点。
# 这是一个模拟的LLM调用函数
def call_llm(prompt):
print("--- 正在调用LLM ---")
print(f"Prompt: {prompt[:200]}...") # 只打印部分prompt
# 手动模拟LLM的回答
if "优点" in prompt and "缺点" not in prompt:
return "RAG的优点是能够利用外部知识,减少模型幻觉,并提高答案的实时性。"
elif "缺点" in prompt:
return "RAG的优点是能够利用外部知识,减少模型幻觉,并提高答案的实时性。其缺点则包括检索质量对最终效果影响大,以及系统延迟较高等问题。"
else:
return "模拟回答..."
# --- 1. 准备工作 ---
# 用户问题
question = "RAG的优缺点是什么?"
# 召回的有序文档列表
documents = [
"RAG,即检索增强生成,其主要优点是能将实时、动态的外部知识库引入到大型语言模型中,从而显著减少模型产生事实性错误的‘幻觉’现象。", # 文档1: 只讲优点
"尽管RAG很强大,但它也有缺点。系统的最终效果高度依赖于检索模块的质量,如果检索出错,生成环节也无能为力。此外,检索、生成两阶段的流程也引入了额外的系统延迟。" # 文档2: 只讲缺点
]
# --- 2. 实现Refine循环 ---
# 处理第一个文档
initial_prompt = INITIAL_PROMPT_TEMPLATE.format(context=documents[0], question=question)
current_answer = call_llm(initial_prompt)
print(f"--- 初步答案 (基于文档1) ---\n{current_answer}\n")
# 循环处理剩余的文档
for doc in documents[1:]:
refine_prompt = REFINE_PROMPT_TEMPLATE.format(
question=question,
existing_answer=current_answer,
context=doc
)
current_answer = call_llm(refine_prompt)
print(f"--- 精炼后答案 (结合新文档) ---\n{current_answer}\n")
final_answer = current_answer
print(f"--- 最终答案 ---\n{final_answer}")
结果分析:
通过观察打印出的流程,你会清晰地看到答案是如何一步步“成长”的:
- 第一次调用后,答案只包含了RAG的优点。
- 第二次调用时,LLM接收到了包含“缺点”信息的新上下文,以及上一步的答案。在
REFINE_PROMPT_TEMPLATE
的指导下,它将新旧信息整合,输出了一个同时包含优点和缺点的、更全面的答案。
Refine策略的优缺点
- 优点:
- 能处理大量文档:可以突破单次Prompt的上下文窗口限制。
- 答案更全面:通过迭代综合,能生成质量更高、信息更完整的答案。
- 减轻“迷失”问题:每次只聚焦一小块新信息,LLM的注意力更集中。
- 缺点:
- 高延迟:需要进行N次串行的LLM调用(N是文档数量),耗时很长,不适用于实时问答场景。
- 顺序敏感:文档的顺序可能会影响最终结果。
总结与预告
今日小结:
- Refine(迭代式精炼) 是一种通过串行处理多个文档,逐步完善答案的高级RAG策略。
- 它依赖于“初始问答”和“精炼答案”两种不同的Prompt模板来指导LLM的行为。
- Refine能生成更全面的高质量答案,但代价是极高的延迟,适用于对答案质量要求苛刻的离线任务。
Refine策略的串行调用机制决定了它的速度瓶颈。那么,有没有一种可以并行处理多个文档,最后再汇总结果的策略呢?这样不是能大大节省时间吗?
明天预告:RAG 每日一技(十二):多路并进,融会贯通!聊聊Map-Reduce RAG策略
明天,我们将学习另一种源自大数据处理思想的经典RAG策略:Map-Reduce。我们将看看它是如何通过“分而治之”再“合并归一”的方式,来高效地处理海量文档的。敬请期待!