一文详解RAG应用中的路由模式
作者: 老刘说NLP 来源: 老刘说NLP
依据的用户查询意图在 RAG 应用程序使用“路由控制模式”可以帮助我们创建更强大的 RAG 应用程序。我们通常希望用户能够访问的数据可以来自各种来源,如报告、文档、图片、数据库和第三方系统。
对于基于业务的 RAG 应用程序,我们可能还希望用户能够与其它业务系统进行交互,例如:销售、订购和会计等。由于数据来源的多样性,信息的存储方式以及我们想要与之交互的方式也可能是多样的。一些数据可能存储在向量存储器中,一些存储在 SQL 数据库中,而一些可能需要通过 API 调用来访问,因为它存在于第三方系统中。
同样的数据可能也可以设置不同的向量存储,针对不同类型的查询进行优化。例如:可以设置一个向量存储用于回答摘要型问题,另一个用于回答特定的、有针对性的问题。我们可能还希望根据问题的性质,路由到不同的组件类型。例如:我们可能希望将查询传递给代理、向量存储器,或者直接传递给 LLM 进行处理,这完全取决于问题的性质。
我们甚至可能希望根据所问问题来定制提示词模板。
总的来说,有许多原因会导致我们希望改变并引导用户查询在应用程序中的流程。我们的应用程序要实现的业务场景越多,我们在整个应用程序中可能需要的“路由”要求也就越多。“路由器”本质上只是我们使用的 If/Else 语句,用于指导查询的控制流程。但有趣的是,它们需要根据自然语言输入做出决策。因此,我们寻求基于自然语言描述的离散输出。由于许多路由逻辑是基于使用 LLMs 或机器学习算法的,这些算法是非确定性的,我们无法保证“路由器”始终 100%做出正确的选择。此外,我们不太可能能够预测所有进入路由器的不同查询变体。然而,通过不断优化和测试,我们应该能够利用“路由器”来帮助创建更强大的 RAG 应用程序。
自然语言路由器
我们将在这里探讨一些常见的自然语言路由器,它们由一些不同的 RAG 和 LLM 框架和库实现。
-
LLM 补全路由器(LLM Completion Routers)
-
LLM 函数调用路由器(LLM Function Calling Routers)
-
语义路由器(Semantic Routers)
-
零样本分类路由器(Zero Shot Classification Routers)
-
语言分类路由器(Language Classification Routers)
下面的图表描述了这些路由器。图表还包括逻辑路由器,我们将其定义为基于离散逻辑的路由器,例如针对字符串长度、文件名、整数值等条件。换句话说,它们不是基于对自然语言查询意图的理解。
让我们稍微详细地探讨一下这些路由器:
LLM 路由器
这种类型的路由器主要利用 LLM 的决策能力根据用户的查询需求选择一条查询路线。
LLM 补全路由器
这些使用 LLM 完成调用,要求 LLM 返回一个最能描述查询的单词,这个单词来自您传递给其提示的单词选项列表。然后,这个单词可以作为 If/Else 条件的一部分来控制应用程序流程。这就是 LlamaIndex 中的 LLM Selector 路由器的工作原理。这也是 LangChain 文档中给出的一个路由器的示例。让我们看一个代码示例,基于 LangChain 文档中提供的示例,以使这个更加清晰。正如您所看到的,在 LangChain 中自己编写其中之一的代码非常简单。
from langchain_anthropic import ChatAnthropic
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
# Set up the LLM Chain to return a single word based on the query,
# and based on a list of words we provide to it in the prompt template
llm_completion_select_route_chain = (
PromptTemplate.from_template("""
Given the user question below, classify it as either
being about `LangChain`, `Anthropic`, or `Other`.
Do not respond with more than one word.
<question>
{question}
</question>
Classification:"""
)
| ChatAnthropic(model_name="claude-3-haiku")
| StrOutputParser()
)
# We setup an IF/Else condition to route the query to the correct chain
# based on the LLM completion call above
def route_to_chain(route_name):
if "anthropic" == route_name.lower():
return anthropic_chain
elif "langchain" == route_name.lower():
return langchain_chain
else:
return general_chain
...
# Later on in the application, we can use the response from the LLM
# completion chain to control (i.e route) the flow of the application
# to the correct chain via the route_to_chain method we created
route_name = llm_completion_select_route_chain.invoke(user_query)
chain = route_to_chain(route_name)
chain.invoke(user_query)
LLM 函数调用路由器
这种路由器利用 LLM 的函数调用能力来选择要遍历的路线。不同的路线被设置为带有适当描述的函数,在 LLM 函数调用中。然后根据传递给 LLM 的查询,它能够返回正确的函数(即路线),供我们选择。这就是 Pydantic 路由器在 LlamaIndex 内部的工作原理。这也是大多数代理工作的方式,以选择要使用的正确工具。它们利用 LLM 的函数调用能力来根据用户的查询选择适当的工具。
语义路由器
这种路由器类型利用嵌入和相似性搜索来选择最佳的路线。每条路线都有一组与之关联的示例查询,这些查询会成为嵌入并存储为向量。传入的查询也会被嵌入,并对路由器中的其他示例查询进行相似性搜索。与查询最匹配的路线将被选择。事实上,有一个名为 semantic-router 的 Python 包就是这样做的。让我们看一些实现细节,以更好地了解整个工作原理。这些示例直接来自该库的 GitHub 页面。让我们设置两条路线,一条是关于政治问题的问题,另一条是关于一般闲聊类型的问题。对于每条路线,我们分配一个可能经常被问到的问题列表,以触发该路线。这些示例查询称为 utterances。这些 utterances 将被嵌入,以便我们可以将它们用于与用户查询的相似性搜索。
from semantic_router import Route
# we could use this as a guide for our chatbot to avoid political
# conversations
politics = Route(
name="politics",
utterances=[
"isn't politics the best thing ever",
"why don't you tell me about your political opinions",
"don't you just love the president",
"they're going to destroy this country!",
"they will save the country!",
],
)
# this could be used as an indicator to our chatbot to switch to a more
# conversational prompt
chitchat = Route(
name="chitchat",
utterances=[
"how's the weather today?",
"how are things going?",
"lovely weather today",
"the weather is horrendous",
"let's go to the chippy",
],
)
# we place both of our decisions together into single list
routes = [politics, chitchat]
我们将 OpenAI 指定为编码器,尽管任何嵌入库都可以使用。接下来,我们使用路由器和编码器创建我们的路由层。
encoder = OpenAIEncoder()
from semantic_router.layer import RouteLayer
route_layer = RouteLayer(encoder=encoder, routes=routes)
然后,当我们将查询应用于路由器层时,它会返回应该用于查询的路线。
route_layer("don't you love politics?").name
# -> 'politics'
因此,再次总结一下,这种语义路由器利用嵌入和相似性搜索来选择使用用户查询的最佳路线。这种路由器类型应该比其他基于 LLM 的路由器更快,因为它只需要处理一次索引查询,而不是其他类型需要调用 LLM。
零样本分类路由器
“零样本文本分类”是自然语言处理(NLP)中的一个任务,其中模型在一组标记示例上进行训练,但然后能够对先前未见过的新示例进行分类。这些路由器利用零样本分类模型为文本片段分配标签,您将预定义的一组标签传递给路由器。示例:Haystack 中的 ZeroShotTextRouter,它利用了 Hugging Face 的零样本分类模型。查看此处的源代码,了解其中的奥秘所在。
语言分类路由器
这种类型的路由器能够识别查询所在的语言,并根据此进行路由查询。如果您的应用程序需要某种多语言解析能力,则此路由器非常有用。示例:来自 Haystack 的 TextClassificationRouter。它利用 langdetect Python 库来检测文本的语言,该库本身使用朴素贝叶斯算法来检测语言。
关键词路由器
LlamaIndex 的联合创始人 Jerry Liu 在有关 RAG 应用程序内部路由的文章中,除其他选项外,还建议使用关键词路由器,该路由器将尝试通过匹配查询和路由列表之间的关键词来选择路线。此关键词路由器也可以由 LLM 支持来识别关键词,或者由其他关键词匹配库支持。
逻辑路由器
这些路由器使用逻辑检查对变量进行检查,例如字符串长度、文件名和值比较,以处理如何路由查询。它们非常类似于编程中使用的典型的 If/Else 条件。换句话说,它们不是基于理解自然语言查询意图,而是可以根据现有和离散变量做出选择。示例:HayStack 中的 ConditionalRouter 和 FileTypeRouter。
代理 vs 路由器
乍一看,路由器和代理之间确实存在许多相似之处,可能很难区分它们之间的区别。这些相似之处存在是因为代理实际上在其流程中执行路由。他们使用路由机制来选择要用于工作的正确工具。他们经常利用函数调用来选择正确的工具,就像上面描述的 LLM 函数调用路由器一样。不过,与代理相比,路由器更简单,通常只有一个“简单”的任务,即将任务路由到正确的位置,而不是执行与该任务相关的任何逻辑或处理。另一方面,代理通常负责处理逻辑,包括管理它们可以访问的工具所完成的工作。
结论
我们在这里讨论了目前在不同 RAG 和 LLM 框架以及包中找到的一些常见的自然语言路由器。随着时间的推移,围绕路由的概念、包和库肯定会不断增加。构建 RAG 应用程序时,您会发现,查询路由器可以帮助您将自然语言请求路由到您的应用程序的正确位置,能够最好地满足用户的查询需求提高响应速度。相信未来查询路由的方式在构建对用户有用的 RAG 应用程序时将变得无比重要!
参考资料:Routing in RAG-Driven Applications: https://towardsdatascience.com/routing-in-rag-driven-applications-a685460a7220
更多AI工具,参考Github-AiBard123,国内AiBard123