RAG应用

一、RAG简介

目前实现 RAG 的主流框架就是 LangChain 和 LlamaIndex,LangChain 更适合需要复杂对话流程、上下文管理、以及多步骤任务的应用场景,如聊天机器人、任务自动化等。LlamaIndex 当应用场景主要涉及大量数据的快速检索和查询时,LlamaIndex更加适用,如企业知识问答系统、文档搜索引擎等。

二、llamaIndex构建RAG服务

LlamaIndex最初被称为GPT Index, 后来大语言模型的快速发展,改名为LlamaIndex。它就像一个多功能的工具,可以在处理数据和大型语言模型的各个阶段提供帮助

首先,它有助于“摄取”数据,这意味着将数据从原始来源获取到系统中。其次,它有助于“结构化”数据,这意味着以语言模型易于理解的方式组织数据。第三,它有助于“检索”,这意味着在需要时查找和获取正确的数据。最后,它简化了“集成”,使您更容易将数据与各种应用程序框架融合在一起。

img
img

首先安装llamaIndex

1
pip install llama-index llama_index.llms.ollama llama_index.embeddings.huggingface -i https://pypi.tuna.tsinghua.edu.cn/simple

可以通过环境变量LLAMA_INDEX_CACHE_DIR控制llamaIndex的cache存储位置,包括huggingface,nltk等包的位置

llamaIndex快速构建RAG

llamaIndex可以快速构建起一个文档索引问答的demo,全部采用默认配置,只需要5行代码

1
2
3
4
5
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("Summarize the documents.")

一般使用时,我们还需要修改llm,embed模型,以及修改prompt来生成中文输出

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
from llama_index.core import Settings
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, PromptTemplate
# from llama_index.llms.ollama import Ollama
# from llama_index.llms.openai import OpenAI
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# 从指定目录下面去加载文档
documents = SimpleDirectoryReader("../docs").load_data()
# 不设置llm则默认使用的是openai的gpt-3.5-turbo,可以指定ollama模型,也可以继承CustomLLM自定义模型
# llm=Ollama(model="llama3", request_timeout=120.0)
# llm = OpenAI(temperature=0.1, model="gpt-4o")
# Settings.llm = llm
# 不设置embed模型则默认openai的text-embedding-ada-002
embed_model = HuggingFaceEmbedding( model_name="BAAI/bge-large-en-v1.5", trust_remote_code=False)
Settings.embed_model = embed_model
# 将文档保存到(内存)索引中,这里可以指定文本被分割的大小(transformations),使用的embed模型(embed_model)等
index = VectorStoreIndex.from_documents(documents, show_progress=True)
# 对上述索引创建一个查询引擎,可以查询上面的文档,可以配置查询引擎的多个参数
query_engine = index.as_query_engine(similarity_top_k=3,streaming=True)
qa_prompt_tmpl_str = (
"请结合给出的参考知识,回答用户的问题。"
"参考知识如下:\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"用户的问题如下:\n"
"human: {query_str}\n"
"Assistant: "
)
qa_prompt_tmpl = PromptTemplate(qa_prompt_tmpl_str)
query_engine.update_prompts(
{"response_synthesizer:text_qa_template": qa_prompt_tmpl}
)
# 发起查询
response = query_engine.query("浏览器主页被篡改怎么办?")
print(response)
# 初始化对话引擎,使用对话的方式来问答
chat_engine = index.as_chat_engine(
similarity_top_k=5, # 设置匹配相似度前 5 的知识库片段
chat_mode="condense_plus_context" # 设置聊天模型
)
# res = chat_engine.chat('介绍一下小米 14 ultra')
res = chat_engine.stream_chat('小米 14 ultra 电池容量有多大?')
for text in res.response_gen:
print(text, end='', flush=True)

文本分析

文档分析即是chunking过程。 LLamaIndex将输入文档分解为节点的较小块。这个分块是由NodeParser完成的。默认情况下,使用SimpleNodeParser,它将文档分块成句子。
分块过程如下:

  1. 用户pdf,md等文档首先被分割为Document结构,(默认按最大字数或者页码来决定分多少个Document)
  2. NodeParser接收一个Document对象列表;
  3. 使用spaCy的句子分割将每个文档的文本分割成句子;
  4. 每个句子都包装在一个TextNode对象中,该对象表示一个节点;
  5. TextNode包含句子文本,以及元数据,如文档ID、文档中的位置等;
  6. 返回TextNode对象的列表。

在LlamaIndex框架中,Document和Node是核心的数据抽象概念,它们帮助处理和组织各种类型的数据以便于索引、查询和分析。这两个概念提供了一种灵活和高效的方式来处理来自不同数据源的信息。一旦数据被摄取并表示为文档,就可以选择将这些文档进一步处理为节点。节点是更细粒度的数据实体,表示源文档的“块”,可以是文本块、图像或其他类型的数据。它们还携带元数据和与其他节点的关系信息,这有助于构建更加结构化和关系型的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core import DocumentSummaryIndex
def load_document_test():
llm = OpenAI(temperature=0.1, model="gpt-4o")
Settings.llm = llm
# 从指定目录下面去加载文档,默认按页码分割,多少页就分多少个documents
documents = SimpleDirectoryReader(input_files=["../docs/简易测试劳动合同.pdf"]).load_data()
print('len=',len(documents))
for document in documents:
print(document.text)
print("\n\n===================================\n\n")
# 将documents进行进一步分割成chunk,使用TextNode结构进行封装。
parser = SentenceSplitter(chunk_size=100,chunk_overlap=20)
nodes = parser.get_nodes_from_documents(documents)
# print(nodes)
for node in nodes:
print("\n\n======================\n==============================\n\n")
print(node.text)

在core.node_parser中,有诸多文本分割工具如:SentenceSplitter,MarkdownNodeParser,TextSplitter,NodeParser等。

索引构建

索引可以有多种索引,如DocumentSummaryIndex,VectorStoreIndex。效果上DocumentSummaryIndex更优一些

1
2
3
4
5
6
7
8
9
10
11
12
13
# 直接通过nodes来构建索引
doc_summary_index = DocumentSummaryIndex(nodes=nodes)
# 构建查询引擎,默认使用summary的embedding引擎,也可以通过配置retriever_mode="llm"来使用llm检索
query_engine = doc_summary_index.as_query_engine(
response_mode="tree_summarize", use_async=True
)
response = query_engine.query("乙方的名字是什么,工资有多少?试用期多长?")
print(response)
# 构建查询引擎,使用向量查询引擎
index = VectorStoreIndex(nodes=nodes)
index_engine = index.as_query_engine(similarity_top_k=3)
response = index_engine.query("乙方的名字是什么,工资有多少?试用期多长?")
print(response)

文章摘要索引:DocumentSummaryIndex

通常,大多数用户以以下方式开发基于LLM的QA系统:

  • 获取源文档并将其分成文本块。
  • 然后将文本块存储在矢量数据库中。
  • 在查询期间,通过使用相似度和/或关键字过滤器进行Embedding来检索文本块。
  • 执行整合后的响应。

然而,这种方法存在一些影响检索性能的局限性:

  • 文本块没有完整的全局上下文,这通常限制了问答过程的有效性。
  • 需要仔细调优top-k /相似性分数阈值,因为过小的值可能会导致错过相关上下文,而过大的值可能会增加不相关上下文的成本和延迟。
  • Embeddings可能并不总是为一个问题选择最合适的上下文,因为这个过程本质上是分别决定文本和上下文的。
  • 为了增强检索结果,一些开发人员添加了关键字过滤器。然而,这种方法有其自身的挑战,例如通过手工或使用NLP关键字提取/主题标记模型为每个文档确定适当的关键字,以及从查询中推断正确的关键字。

img
img

这就是 LlamaIndex 引入文档摘要索引的原因,它可以为每份文档提取非结构化文本摘要并编制索引,从而提高检索性能,超越现有方法。该索引比单一文本块包含更多信息,比关键字标签具有更多语义。它还允许灵活的检索,包括基于 LLM 和嵌入的方法。在构建期间,该索引接收文档并使用 LLM 从每个文档中提取摘要。在查询时,它会根据摘要使用以下方法检索相关文档:

  • 基于 LLM 的检索:获取文档摘要集合并请求 LLM 识别相关文档+相关性得分
  • 基于嵌入的检索:利用摘要嵌入相似性来检索相关文档,并对检索结果的数量施加顶k限制。
    文档摘要索引的检索类为任何选定的文档检索所有节点,而不是在节点级返回相关块。

向量检索

构建索引可以用高级方法如

1
2
3
4
5
6
7
8
9
doc_summary_index = DocumentSummaryIndex(nodes=nodes)
# 构建查询引擎,默认使用summary的embedding引擎,也可以通过配置retriever_mode="llm"来使用llm检索
query_engine = doc_summary_index.as_query_engine(
response_mode="tree_summarize", use_async=True
)
nodes_with_scores = query_engine.retrieve(QueryBundle("这篇文章主要讲了什么"))
for node_score in nodes_with_scores:
print("node_score:",node_score)
response = query_engine.query("乙方的名字是什么,工资有多少?试用期多长?")

也可以使用底层api,使用LLM进行索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from llama_index.core.indices.document_summary import DocumentSummaryIndexLLMRetriever

retriever = DocumentSummaryIndexLLMRetriever(
doc_summary_index,
# choice_select_prompt=None,
# choice_batch_size=10,
# choice_top_k=1,
# format_node_batch_fn=None,
# parse_choice_select_answer_fn=None,
)
retrieved_nodes = retriever.retrieve("What are the sports teams in Toronto?")
print(len(retrieved_nodes))
20
print(retrieved_nodes[0].score)
print(retrieved_nodes[0].node.get_text())
# 生成查询引擎
query_engine = RetrieverQueryEngine.from_args(retriever) #,node_postprocessors=[reranker]

使用嵌入向量进行索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from llama_index.core.indices.document_summary import (
DocumentSummaryIndexEmbeddingRetriever,
)

retriever = DocumentSummaryIndexEmbeddingRetriever(
doc_summary_index,
# similarity_top_k=1,
)
retrieved_nodes = retriever.retrieve("What are the sports teams

in Toronto?")
len(retrieved_nodes)
20
print(retrieved_nodes[0].node.get_text())

混合检索之融合检索(Fusion Retrieval)

一文说清大模型RAG应用中的两种高级检索模式:你还只知道向量检索吗?: https://www.53ai.com/news/qianyanjishu/2024060723184.html

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
if os.path.exists(temp_dir):
loader = SimpleDirectoryReader(
input_dir = temp_dir,
required_exts=[".pdf"],
recursive=True
)
else:
return
# 设置LLM
llm = OpenAI(temperature=0.1, model="gpt-4o")
Settings.llm = llm
# 设置embed模型,默认openai的text-embedding-ada-002,可以指定本地模型
embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-large-en-v1.5", trust_remote_code=True)
Settings.embed_model = embed_model
# 加载成document结构
docs = loader.load_data()
# 将documents进行进一步分割成chunk,使用TextNode结构进行封装。
parser = SentenceSplitter(chunk_size=100,chunk_overlap=20)
nodes = parser.get_nodes_from_documents(docs)
# 可以构造多个索引index
vector_index = VectorStoreIndex(nodes=nodes)
# 定义融合检索器
retriever = QueryFusionRetriever(
[vector_index.as_retriever()],
similarity_top_k=10,
num_queries=4, # 重写生成多个问题, set this to 1 to disable query generation
use_async=True,
verbose=True,
# query_gen_prompt="...", # we could override the query generation prompt here
)
# 构造rerank模型
reranker = SentenceTransformerRerank(model='BAAI/bge-reranker-large', top_n=2)
# 生成查询引擎
query_engine = RetrieverQueryEngine.from_args(retriever,node_postprocessors=[reranker])
# 执行检索查询
query_str = "月工资有多少"
nodes_with_scores = query_engine.retrieve(QueryBundle(query_str))
# print(nodes_with_scores)
for node_score in nodes_with_scores:
print("node_score:",node_score)
response = query_engine.query(query_str)
print(response)

混合检索之递归检索(Recursive Retrieval)

https://www.53ai.com/news/qianyanjishu/2024060489721.html

使用Llama index构建多代理 RAG https://blog.csdn.net/tMb8Z9Vdm66wH68VX1/article/details/134213080

Llama index概述了使用多代理RAG的具体示例:

  • 文档代理——在单个文档中执行QA和摘要。
  • 向量索引——为每个文档代理启用语义搜索。
  • 摘要索引——允许对每个文档代理进行摘要。
  • 高阶(TOP-LEVEL)代理——编排文档代理以使用工具检索回答跨文档的问题。
    对于多文档QA,比单代理RAG基线显示出真正的优势。由顶级代理协调的专门文档代理提供基于特定文档的更集中、更相关的响应。

rerank使用

由于考虑召回速度,在执行向量搜索的时候存在一定随机性就会牺牲一点准确性,RAG中第一次召回的结果排序往往不太准确,具体可以参考 Rerank——RAG中百尺竿头更进一步的神器,从原理到解决方案。所以这时候就需要 rerank 一下,来对召回的结果重新排序。这里 LlamaIndex 的给了两个方案,一个是基于大模型的 LLMRerank 类,一个是第三方的 rerank 模型。

  1. 使用llm的rerank
1
2
3
4
5
6
7
8
9
10
11
from llama_index.core.postprocessor import LLMRerank

reranker = LLMRerank(
top_n=2, llm=llm,
parse_choice_select_answer_fn=self.custom_parse_choice_select_answer_fn
)

chat_engine = index.as_chat_engine(
similarity_top_k=5, chat_mode="condense_plus_context",
node_postprocessors=[postprocessor, reranker]
)
  1. 使用自定义模型
1
2
3
from llama_index.core.postprocessor import SentenceTransformerRerank
reranker = SentenceTransformerRerank(model='BAAI/bge-reranker-large', top_n=2)
query_engine = RetrieverQueryEngine.from_args(retriever,node_postprocessors=[reranker])

效果,咨询问题为:月工资多少。不加rerank召回为

1
2
3
4
5
6
7
8
9
10
11
12
node_score: Node ID: 08ce241b-8f30-4f5d-b075-3352c09aacdd
Text: 第十一条 本合同自甲乙双方签字或者盖章之日起生效。 本 合同一式二份,甲乙双方各执一份。 甲方(公章)
Score: 0.644

node_score: Node ID: 651bc570-8d6a-4be0-9708-435bc622a032
Text: 第四条 甲方于每月 5 日前以现金或 银行代发 形式及时 足额支付乙方工资,工资标准为 : 1.月工资 10086
元。 2.计件工资。
Score: 0.643

node_score: Node ID: 92b10150-f03b-433d-8035-d8cb75daf2b3
Text: 符合《劳动合同法》有关规定情形的, 甲方应当依法支付乙方经济补偿。 第八条 双方约定的其它事项:
Score: 0.622

加了rerank,召回为:

1
2
3
4
5
6
7
8
node_score: Node ID: 20a75e46-052b-47d0-9051-f434c251835c
Text: 第四条 甲方于每月 5 日前以现金或 银行代发 形式及时 足额支付乙方工资,工资标准为 : 1.月工资 10086
元。 2.计件工资。
Score: 0.284

node_score: Node ID: 6b927ec0-fb80-4f98-bb25-41baaf1bdbb2
Text: 符合《劳动合同法》有关规定情形的, 甲方应当依法支付乙方经济补偿。 第八条 双方约定的其它事项:
Score: 0.015

LLMRerank 这个类的原理是,将向量数据库查询的 top 片段,拼装成提示词给大模型,等待大模型按一定的格式返回,然后再将排序之后的片段作为用户提问的上下文,再次拼装成提示词给大模型回答。这里就会有两次请求大模型,就会造成响应明显变慢。所以一般都是采用第三方的 rerank 模型,替换大模型排序这个操作。

召回测评

https://ihey.cc/rag/融合检索-rerank-在-rag-应用中的效果评测/

结论:

  • 对最终召回成功率提升最大的是提高 Recall@N 的数量。但这对 LLM 的 Context Length 和 In-Context learning 要求在提高
  • Rerank 对检索结果的提升是直接的、确定的(10%~15%),但还是不够理想。对具体某个 query 案例,rerank 小概率会降低召回成功率
  • 融合检索比单纯的 Sparse 或 Dense 检索都好,但优势不显著。可能需要针对不同 query 采用不同的融合排序的权重
  • RAG 方案最终拼的还是检索能力。对 LLM 来说是 garbage in garbage out。是否是 garbage,取决于检索

langchain构建RAG应用

ragflow构建RAG应用

高级RAG多智能体

智能体(Langchain[35] 和 LlamaIndex[36] 都支持)自从第一个 LLM API 发布以来就已存在。其核心理念是为具备推理能力的 LLM 提供一套工具和一个待完成的任务。这些工具可能包括确定性函数(如代码功能或外部 API)或其他智能体。正是这种 LLM 链接的思想促成了 LangChain 的命名。

智能体本身是一个非常广泛的领域,在 RAG 概述中无法深入探讨,因此我将直接继续讨论基于智能体的多文档检索案例,并简要介绍 OpenAI 助手。OpenAI 助手是在最近的 OpenAI 开发者大会上作为 GPTs 提出的相对较新的概念[37],并在下面描述的 RAG 系统中发挥作用。

OpenAI Assistants[38]集成了许多围绕LLM必需的工具,这些工具我们以前在开源项目中已经见过 —— 包括聊天记录管理、知识库存储、文档上传界面,以及最关键的,功能调用 API[39]。这个 API 的重要功能是能够将自然语言请求转换为外部工具或数据库查询的 API 调用。

在 LlamaIndex 中,OpenAIAgent[40] 类融合了这些高级功能,并与 ChatEngine 和 QueryEngine 类结合,提供了基于知识的、具有上下文感知能力的聊天体验。此外,它还能在一次对话交互中调用多个 OpenAI 功能,从而真正实现智能代理行为。

接下来让我们来了解一下多文档代理方案[41] —— 这是一个相当复杂的设计,涉及到在每个文档上初始化一个代理(OpenAIAgent),这个代理不仅能进行文档摘要处理,还能执行经典的问答流程。还有一个顶级代理,负责将查询任务分配给各个文档代理,并合成最终答案。

每个文档代理配备了两种工具 —— 向量存储索引和摘要索引,它会根据接收到的查询决定使用哪个工具。对于顶级代理来说,所有文档代理都是其工具,可供其调度使用。

这个方案展示了一个高级的 RAG 架构,涉及每个代理做出的复杂路由决策。这种架构的优势在于它可以比较不同文档中描述的不同解决方案或实体,以及它们的摘要,同时也支持经典的单文档摘要处理和问答流程 —— 这实际上覆盖了大多数与文档集合交互的常见用例。

img
img

这个复杂方案的一个缺点可以通过图像来理解 —— 由于需要在代理内部与大语言模型(LLM)进行多轮迭代,因此处理速度较慢。需要注意的是,在 RAG 架构中,调用 LLM 总是最耗时的步骤,而搜索则是出于设计考虑而优化了速度。因此,对于涉及大量文档的存储系统,我建议对这一方案进行简化,以提高其扩展性。

该方案可参考:https://discuss.nebula-graph.com.cn/t/topic/14848

参考

  1. 使用 LlamaIndex 框架搭建 RAG 应用基础实践 https://juejin.cn/post/7341210909068574760
  2. Rerank——RAG中百尺竿头更进一步的神器,从原理到解决方案 https://luxiangdong.com/2023/11/06/rerank/
  3. 大模型应用开发,必看的高级 RAG 技术 https://juejin.cn/post/7352146423276568616
  4. 【RAG实践】Rerank,让RAG更近一步 魔搭+llamaIndex+Qwen+DAG+Rerank https://zhuanlan.zhihu.com/p/691661819
  5. LlamaIndex官方文档 https://docs.llamaindex.ai/en/stable/
  6. langchain中文网 https://www.langchain.com.cn/
  7. 最全的RAG概览 https://discuss.nebula-graph.com.cn/t/topic/14848