LOADING

wait wait wait...

LLM学习日记 RAG

  • RAG概述
  • RAG评估
  • RAG优化策略:HyDE / Step Back Prompting / Multi Query Retrieval / Decomposition / CoVe / SAFE / DoLa

RAG概述

img

RAG流程

  • 准备知识文档:
    • 文档切片
  • Embedding模型:
    • 核心任务将文本转换为向量形式
    • Word2Vec、BERT、GPT系列等
  • 向量数据库
  • 查询检索
  • 生成回答

RAG分类

Retrieval-Augmented Generation for Large Language Models: A Survey

img

文中将RAG分为下文提及的三类

img

Naive RAG

  • 经典的RAG,主要涉及“检索-阅读”过程
  • 索引:将文档库分割为较短的chunk
  • 检索:根据问题和chunk的相似度检索相关文档片段
  • 生成:以检索到的上下文为条件,生成问题的回答

Advanced RAG

  • 检索前:使用问题的重写、路由和扩充等方式对齐问题和文档块之间的语义差异
  • 检索后:将检索得到的文档库进行重排序

Modular RAG

  • 引入查询搜索引擎等更多功能模块
  • 结合强化学习等技术

RAG评估

RAG难点问题

  • 建立知识向量库:
    • 如何切分不同类型的文件
    • 如何设置chunk-size大小
    • 选用何种向量化工具构建
    • embedding模型是否需要微调
  • 检索优化:
    • 如何应对模糊的、指向不明的问题
  • 归纳总结:
    • 内容全面性
    • 生成内容格式
    • 模型输出内容的可控性不够好

对检索环节的评估

MRR (Mean Reciprocal Rank)

  • 用于评估根据查询返回的多个结果的相关性
  • 定义结果列表中第$i$个结果匹配分数为$\frac{1}{i}$
def mean_reciprocal_rank(ranked_lists):
    mrr = 0.0
    for ranked_list in ranked_lists:
        reciprocal_rank = 0
        for rank, item in enumerate(ranked_list, start=1):
            if item == 1:  # Correct answer found
                reciprocal_rank = 1 / rank
                break
        mrr += reciprocal_rank
    return mrr / len(ranked_lists)

Hits Rate

  • 前K项中包含正确信息的项的占比
  • 可用于评估召回相关文档的比率

开源RAG评估框架

Ragas

Ragas主要评估忠实性、答案相关性、上下文相关性

https://blog.csdn.net/weixin_42608414/article/details/135355723

  • 忠实性:答案应基于给定的上下文
  • 答案相关性
  • 上下文相关性:
    • LLMs处理长篇上下文信息的成本高
    • LLMs对于上下文段落中间提供的信息利用效率低

LangSmith

https://blog.csdn.net/fengshi_fengshi/article/details/144493414

可用来调试、测试、评估和监控基于任何LLM框架构建的chain和Agent

RAG优化

文档分块策略

RAG系统中,文档需要被分成多个文本块之后再进行向量嵌入

  • 固定大小的分块
  • 内容分块:
    • 根据标点符号分块
    • 使用NLTK、spaCy库的句子分割功能
  • 递归分块:
    • 通过重复运用分块规则递归地分解文本
    • 例如:langchain先通过段落换行符(\n\n)进行分割,进一步,对于大小超过阈值的块使用单换行符(\n)进行再次分割
    • 如何制定合理的递归分块规则
  • 特殊结构分块:
    • 针对特定结构化内容的专门分块器
    • langchain提供的特殊分块器:Markdown文件、LaTex文件、各种主流代码语言分块器
  • 分块大小的选择:
    • 不同的嵌入模型有不同的最佳输入大小,例如OpenAI的text-embedding-ada-002模型在256和512的分块上效果最佳
    • 文档类型和用户查询长度以及复杂性也是决定分块大小的重要因素,例如长篇文章和书籍适合较大的分块,社交媒体帖子适合较小的分块

Embedding模型阶段

嵌入模型将文本转换成向量

可以参考Hugging Face给出的嵌入模型排行榜MTEB

https://huggingface.co/spaces/mteb/leaderboard

查询索引阶段(检索召回、重排)

用户的查询问题被转化为向量,检索过程中可能存在的问题:

  • query和doc存在不对称问题
  • query表达不清
  • query过于具体,索引中不存在以回答该具体query为主要内容的doc
  • query偏长尾、复杂,需要多步推理,索引中不存在能够直接提供答案的doc

基于上述存在的问题,我们可以从:

  • Query Expansion
  • StepBack
  • Query Decomposation
  • Multi Query Retrieval

等角度给出RAG优化策略

HyDE

假设文档嵌入

img

https://arxiv.org/pdf/2212.10496

  • 接收到用户提问后,先让LLM在没有外部知识的情况下生成一个假设性回复
  • 然后将这个假设性回复和原始查询一起用于向量检索
  • 假设回复中虽然可能包含虚假信息,但蕴含着LLM认为相关的信息和文档模式,有助于在知识库中寻找类似的文档
  1. 生成伪文档
from langchain.prompts import ChatPromptTemplate

# HyDE document generation
template = """请撰写一段科学论文内容来回答以下问题。
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 = "LLM代理的任务分解是什么?"
generate_docs_for_retrieval.invoke({"question": question})
  1. 检索
# Retrieve
retrieval_chain = generate_docs_for_retrieval | retriever
retrieved_docs = retrieval_chain.invoke({"question": question})
retrieved_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": retrieved_docs, "question": question})

Step Back Prompting

退后提示

img

https://arxiv.org/pdf/2310.06117

  • 原始查询太复杂、返回的信息太广泛,我们可以选择生成一个抽象层次更高的“退后问题”
  • 将“退后问题”与原始问题一起用于检索,以增加返回结果的数量
  • 例如“ABC在两年前就读于哪所学校”,可以给出退后问题:ABC的教育历史
  1. 构造few-shot
# 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,
)
  1. 构造prompt
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", """你是一位世界知识领域的专家。你的任务是退一步,将问题改写为更通用的、便于回答的"退一步"问题。以下是一些示例:""",
        ),
        # Few shot examples
        few_shot_prompt,
        # New question
        ("user", "{question}"),
    ]
)

generate_queries_step_back = prompt | ChatOpenAI(temperature=0) | StrOutputParser()
question = "LLM代理的任务分解是什么?"
generate_queries_step_back.invoke({"question": question})

# Response prompt
response_prompt_template = """你是一位世界知识领域的专家。我将向你提问一个问题。你的回答应当全面,并且在相关情况下不得与以下内容矛盾。如果这些内容与问题无关,则可以忽略它们。

# {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})

Multi Query Retrieval

多查询检索/多路召回

https://python.langchain.com/docs/how_to/MultiQueryRetriever/

  • 使用LLM生成多个搜索查询
  • 适用于一个问题需要依赖多个子问题
  1. Indexing
# Load blog
import bs4
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")
        )
    ),
)
blog_docs = loader.load()

# Split
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=300,
    chunk_overlap=50)

# Make splits
splits = text_splitter.split_documents(blog_docs)

# Index
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
vectorstore = Chroma.from_documents(documents=splits,embedding=OpenAIEmbeddings())

retriever = vectorstore.as_retriever()
  1. Prompt构造
from langchain.prompts import ChatPromptTemplate

# Multi Query: Different Perspectives
template = """你是一款AI语言模型助手。你的任务是为用户提供的问题生成五个不同版本的改写,以便从向量数据库中检索相关文档。通过从多个较读改写用户问题,你的目标是帮助用户克服基于距离的相似性搜索的一些局限性。
请将这些改写问题用换行符分隔。原始问题:{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"))
)
  1. 检索文档
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)
  1. 内容生成
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})

Decomposition

img

https://arxiv.org/pdf/2305.14283

  • 将一个复杂问题分解成多个子问题
  • 可以按顺序串行解决(使用第一个问题的检索来回答第二个问题)
  • 可以按并行解决(每个答案合并为最终答案),向下分解
  1. 构造答案迭代式回答的prompt
from langchain.prompts import ChatPromptTemplate

# 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: """

decomposition_prompt = ChatPromptTemplate.from_template(template)
  1. 生成答案
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

生成回答阶段

  • 减少模型产生主观回答和幻觉,RAG系统中的提示词应明确指出回答仅基于搜索结果,例如:“你是一名智能客服。你的目标是提供准确的信息,并尽可能帮助提问者解决问题。你应保持友善,但不要过于啰嗦。请根据提供的上下文信息,在不考虑已有知识的情况下,回答相关查询”
  • 可以使用few-shot的方法,将想要的问答例子加入提示词中,从而指导LLM如何利用检索到的知识,提高模型在特定情境下的实用性

链式验证方法 (CoVe)

https://arxiv.org/pdf/2309.11495

img

https://zhuanlan.zhihu.com/p/675085581

  1. 模型首先生成基准回答(baseline answer)
  2. 生成验证问题来核实生成的结果
  3. 模型独立回答上述问题
  4. 最终生成验证后的回答

img

搜索增强事实 (SAFE)

https://arxiv.org/pdf/2403.18802

https://github.com/google-deepmind/long-form-factuality

img

  • CoVe的升级方法,在链式验证中引入检索增强
  • 通过将LLM生成的response拆分成多个事实,剔除与问题无关的事实内容
  • 对每个相关事实进行检索增强,判断检索结果是否支持该事实

img

层对比解码 (Decoding by Contrasting Layers, DoLa)

https://arxiv.org/pdf/2309.03883

https://github.com/voidism/DoLa

img

  • 在生成结果解码时同时关注Transformer高层与底层的知识
  • 强调较高层中的知识并淡化低层中的知识
  • 不检索外部知识或进行额外微调的情况下,有效减少语言模型的幻觉

img