RAG-丰富提示词

简述

​ RAG的主流程其实比较简单,就是先将用户的提问去向量数据库查询相关文档,再将文档和用户提问组装发送给大模型生成答案,最后将答案返回给用户。其中主要的组件只有向量数据库和大模型,只是两相组合可以玩出花来。

image-20250621160518166

丰富提示词

​ RAG意思是检索增强生成,大模型的生成都是根据提示词生成的,而向量库的存在就是为了丰富我们的提示词,丰富了我们的提示词,大模型才能生成出更理想更丰富合适的答案。那怎么丰富我们的提示词呢?

多版本查询

​ 在我们用问题向量化之后向量去检索向量库得到文档的时候,我们能不能尽可能多的获取相关性的文档,比如说,我的提问是用汉语提问题的,这时候向量化之后一定会带有汉语的向量,导致检索向量库的时候检索到可能都是汉语的文档,其实有些问题的英语文档或者其他语言的文档更全面,如果检索到这些文档,去丰富我们的提示词,生成的答案就会更好。

​ 当然我们在提问的时候不可能每个语言或者其他角度都描述到,这时候我们可以让llm帮我们生成这些版本的问题,去检索到更多更有效的文档。

image-20250621160518166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from langchain.prompts import ChatPromptTemplate

# Multi Query: Different Perspectives
template = """You are an AI language model assistant. Your task is to generate five
different versions of the given user question to retrieve relevant documents from a vector
database. By generating multiple perspectives on the user question, your goal is to help
the user overcome some of the limitations of the distance-based similarity search.
Provide these alternative questions separated by newlines. Original question: {question}"""
prompt_perspectives = ChatPromptTemplate.from_template(template)

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

generate_queries = (
prompt_perspectives
| ChatOpenAI(temperature=0)
| StrOutputParser()
| (lambda x: x.split("\n"))
)

from langchain.load import dumps, loads

def get_unique_union(documents: list[list]):
""" Unique union of retrieved docs """
# Flatten list of lists, and convert each Document to string
flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
# Get unique documents
unique_docs = list(set(flattened_docs))
# Return
return [loads(doc) for doc in unique_docs]

# Retrieve
question = "What is task decomposition for LLM agents?"
retrieval_chain = generate_queries | retriever.map() | get_unique_union
docs = retrieval_chain.invoke({"question":question})
len(docs)

from operator import itemgetter
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough

# RAG
template = """Answer the following question based on this context:

{context}

Question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

llm = ChatOpenAI(temperature=0)

final_rag_chain = (
{"context": retrieval_chain,
"question": itemgetter("question")}
| prompt
| llm
| StrOutputParser()
)

final_rag_chain.invoke({"question":question})

结果融合

​ 上面的过程,我们将多版本检索出来的结果都提交给大模型生成答案了,多版本检索出来的多结果肯定有好有坏,坏的结果一起提交给大模型,肯定也会影响大模型生成答案,那我们要怎么做才能让减少坏结果的影响呢?

​ 在我们搜索的多结果中,我们可以根据文档在每个列表中的排名,通过 RRF 算法为每个文档打分,最终返回一个融合后的排序列表,最后从列表中筛选出前几个我们想要的结果即可。

给定多个文档排名列表(多个检索器、多个查询),RRF 的打分公式如下:

$$
\text{Score}(d) = \sum_{i=1}^{n} \frac{1}{\text{rank}_i + k}
$$

其中:

  • ( \text{rank}_i ):文档 ( d ) 在第 ( i ) 个排序列表中的排名(从 0 开始)
  • ( k ):平滑参数(通常取 60)
  • ( n ):总共融合的列表数量(例如不同查询或不同检索器)

文档出现越多、排名越靠前,其 RRF 得分就越高。

image-20250621160518166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def reciprocal_rank_fusion(results: list[list], k=60):
""" Reciprocal_rank_fusion that takes multiple lists of ranked documents
and an optional parameter k used in the RRF formula """

# Initialize a dictionary to hold fused scores for each unique document
fused_scores = {}

# Iterate through each list of ranked documents
for docs in results:
# Iterate through each document in the list, with its rank (position in the list)
for rank, doc in enumerate(docs):
# Convert the document to a string format to use as a key (assumes documents can be serialized to JSON)
doc_str = dumps(doc)
# If the document is not yet in the fused_scores dictionary, add it with an initial score of 0
if doc_str not in fused_scores:
fused_scores[doc_str] = 0
# Retrieve the current score of the document, if any
previous_score = fused_scores[doc_str]
# Update the score of the document using the RRF formula: 1 / (rank + k)
fused_scores[doc_str] += 1 / (rank + k)

# Sort the documents based on their fused scores in descending order to get the final reranked results
reranked_results = [
(loads(doc), score)
for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]

# Return the reranked results as a list of tuples, each containing the document and its fused score
return reranked_results

提问分解

​ 从版本的角度让我们的提示词更丰富了,但是在语义描述方面,能不能提升一下我们的提示词。在一个复杂困难的问题上我们的提示词可能是不够的,毕竟问题的范围比较大,我们的描述范围有限。如果我们缩小问题的范围,变成小问题,我们的提示词是不是就比较丰富了。在我们经验中,那种复杂的问题,比较好的解决办法就是把它拆分成一个一个的小问题,然后依次去解决这些小问题。在这里我们同样可以套用这种解决方式,把复杂问题甩给大模型让他帮我们拆分成一个一个的小问题,然后我们依次去提问解决这些小问题。正对每一个小问题我们的提示词是不是就丰富了

递归回答

​ 怎么去解决这些小问题,如果这些小问题上下有相关性的话,我们可以使用递归递归回答,就是将上一个问题的答案带入到下一个问题中。

image-20250621160518166

​ 如上图,我们将一个复杂问题拆分成3个小问题,然后依次查询生成解决,将每次解决的结果带入到下一次解决中,最后解决这个复杂问题生成一个完整答案输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from langchain.prompts import ChatPromptTemplate

# Decomposition
template = """You are a helpful assistant that generates multiple sub-questions related to an input question. \n
The goal is to break down the input into a set of sub-problems / sub-questions that can be answers in isolation. \n
Generate multiple search queries related to: {question} \n
Output (3 queries):"""
prompt_decomposition = ChatPromptTemplate.from_template(template)
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# LLM
llm = ChatOpenAI(temperature=0)

# Chain
generate_queries_decomposition = ( prompt_decomposition | llm | StrOutputParser() | (lambda x: x.split("\n")))

# Run
question = "What are the main components of an LLM-powered autonomous agent system?"
questions = generate_queries_decomposition.invoke({"question":question})
# Prompt
template = """Here is the question you need to answer:

\n --- \n {question} \n --- \n

Here is any available background question + answer pairs:

\n --- \n {q_a_pairs} \n --- \n

Here is additional context relevant to the question:

\n --- \n {context} \n --- \n

Use the above context and any background question + answer pairs to answer the question: \n {question}
"""

decomposition_prompt = ChatPromptTemplate.from_template(template)
from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser

def format_qa_pair(question, answer):
"""Format Q and A pair"""

formatted_string = ""
formatted_string += f"Question: {question}\nAnswer: {answer}\n\n"
return formatted_string.strip()

# llm
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

q_a_pairs = ""
for q in questions:

rag_chain = (
{"context": itemgetter("question") | retriever,
"question": itemgetter("question"),
"q_a_pairs": itemgetter("q_a_pairs")}
| decomposition_prompt
| llm
| StrOutputParser())

answer = rag_chain.invoke({"question":q,"q_a_pairs":q_a_pairs})
q_a_pair = format_qa_pair(q,answer)
q_a_pairs = q_a_pairs + "\n---\n"+ q_a_pair
融合回答

​ 如果这些小问题上下文没有相关性或者说没有依赖性,我们又该怎么做呢?可以将每一个小问题单独的去解决生成答案,最后将所有小问题的答案融合生成复杂问题的汇总答案就行了。

image-20250621160518166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Answer each sub-question individually 

from langchain import hub
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# RAG prompt
prompt_rag = hub.pull("rlm/rag-prompt")

def retrieve_and_rag(question,prompt_rag,sub_question_generator_chain):
"""RAG on each sub-question"""

# Use our decomposition /
sub_questions = sub_question_generator_chain.invoke({"question":question})

# Initialize a list to hold RAG chain results
rag_results = []

for sub_question in sub_questions:

# Retrieve documents for each sub-question
retrieved_docs = retriever.get_relevant_documents(sub_question)

# Use retrieved documents and sub-question in RAG chain
answer = (prompt_rag | llm | StrOutputParser()).invoke({"context": retrieved_docs,
"question": sub_question})
rag_results.append(answer)

return rag_results,sub_questions

# Wrap the retrieval and RAG process in a RunnableLambda for integration into a chain
answers, questions = retrieve_and_rag(question, prompt_rag, generate_queries_decomposition)

def format_qa_pairs(questions, answers):
"""Format Q and A pairs"""

formatted_string = ""
for i, (question, answer) in enumerate(zip(questions, answers), start=1):
formatted_string += f"Question {i}: {question}\nAnswer {i}: {answer}\n\n"
return formatted_string.strip()

context = format_qa_pairs(questions, answers)

# Prompt
template = """Here is a set of Q+A pairs:

{context}

Use these to synthesize an answer to the question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

final_rag_chain = (
prompt
| llm
| StrOutputParser()
)

final_rag_chain.invoke({"context":context,"question":question})

后退重复生成

​ 对于复杂问题,除了上面说的拆分成子问题有没有其他的方式让我们的提示词变得更丰富了呢?答案肯定是有的,就是后退重复生成。啥意思呢,就是说,我们第一次的提示词不丰富,那我们让第一次生成的结果加到提示词中,这时候我们的提示词是不是更丰富了,在用这个丰富的提示词再去生成,是不是结果更好了呢,如果不够,我们再将生成的结果加入提示词再生成一次呢,一直到满意为止。

image-20250621160518166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# Few Shot Examples
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
examples = [
{
"input": "Could the members of The Police perform lawful arrests?",
"output": "what can the members of The Police do?",
},
{
"input": "Jan Sindel’s was born in what country?",
"output": "what is Jan Sindel’s personal history?",
},
]
# We now transform these to example messages
example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{input}"),
("ai", "{output}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:""",
),
# Few shot examples
few_shot_prompt,
# New question
("user", "{question}"),
]
)
generate_queries_step_back = prompt | ChatOpenAI(temperature=0) | StrOutputParser()
question = "What is task decomposition for LLM agents?"
generate_queries_step_back.invoke({"question": question})
# Response prompt
response_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.

# {normal_context}
# {step_back_context}

# Original Question: {question}
# Answer:"""
response_prompt = ChatPromptTemplate.from_template(response_prompt_template)

chain = (
{
# Retrieve context using the normal question
"normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
# Retrieve context using the step-back question
"step_back_context": generate_queries_step_back | retriever,
# Pass on the question
"question": lambda x: x["question"],
}
| response_prompt
| ChatOpenAI(temperature=0)
| StrOutputParser()
)

chain.invoke({"question": question})

假想文档

​ 如果我们的向量数据库中,没有我们提问的所需要的文档和相关知识怎么办?这时候从向量库的检索是没办法帮我们丰富提示词的,但是我们让llm帮我们生成一个假想文档,这样我们就可以根据这个文档获得跟丰富的提示词从而生成出更丰满的答案了。

image-20250621160518166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from langchain.prompts import ChatPromptTemplate

# HyDE document genration
template = """Please write a scientific paper passage to answer the question
Question: {question}
Passage:"""
prompt_hyde = ChatPromptTemplate.from_template(template)

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

generate_docs_for_retrieval = (
prompt_hyde | ChatOpenAI(temperature=0) | StrOutputParser()
)

# Run
question = "What is task decomposition for LLM agents?"
generate_docs_for_retrieval.invoke({"question":question})
# Retrieve
retrieval_chain = generate_docs_for_retrieval | retriever
retireved_docs = retrieval_chain.invoke({"question":question})
retireved_docs
# RAG
template = """Answer the following question based on this context:

{context}

Question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

final_rag_chain = (
prompt
| llm
| StrOutputParser()
)

final_rag_chain.invoke({"context":retireved_docs,"question":question})