大模型应用与LUI(自然语言交互)落地的关键模块——语义路由实现总结
在传统搜索应用中,有一个核心的模块叫意图识别,即识别用户搜索请求时的意图,基于意图不同生成不同的Query或执行不同的逻辑。在RAG应用中,仍然存在着类似的路由模块完成请求分发。这样的路由分发需求无处不在,特别是在自然语言交互的应用中,并且会随着系统支持的场景和功能增多而变得越来越重要。所谓路由就是一个能够根据一段自然语言输入进行意图判断形成离散输出的模块。
典型的场景比如智能客服中的指令任务分发,硬盘搜索助手根据用户需求搜索图片还是搜索文件。下面是一些常见的情景的总结。
1)基于问题不同生成不同Prompt的情况,这和意图识别生成不同Query一样。
2)根据数据存储的多样性分发到不同数据存储和服务中,比如数据库、API等
3)即使存储介质相同,也可能存在多个需要分发的情况。
4)根据问题的类型分发到不同的组件。比如,根据问题的性质将查询分发给向量数据库、Agent或服务等。
对于路由的实现大致分为两类,逻辑路由(Logical Routers)和 自然语言路由。相较于自然语言路由,逻辑路由不依赖于对路由输入的语义理解,而自然语言路由是需要关注语义的。
以下是这些路由的介绍:
1.LLM 路由
利用 LLM 的决策(decision making )能力根据用户的查询分发。
a.LLM生成路由
这类路由利用 LLM Completion接口实现,要求 LLM 从提示的单词选项列表中返回最能描述查询的单个单词。然后,该词可以作为 If/Else 条件的一部分来控制应用程序流程。
在llamaindex及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)
b.LLM 函数调用路由器
利用了 LLM 的function call能力来选择要执行的分支函数。
LlamaIndex中的Pydantic路由就是这个原理。大多数Agent选择要使用的正确工具也是采用这样的方式。它们利用 LLM 的函数调用能力,根据用户的查询选择适合的工具。下面是Pydantic路由的使用例子:
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import PydanticSingleSelector
from llama_index.core.selectors.pydantic_selectors import Pydantic
from llama_index.core.tools import QueryEngineTool
from llama_index.core import VectorStoreIndex, SummaryIndex
# define query engines
...
# initialize tools
list_tool = QueryEngineTool.from_defaults(
query_engine=list_query_engine,
descriptinotallow="Useful for summarization questions related to the data source",
)
vector_tool = QueryEngineTool.from_defaults(
query_engine=vector_query_engine,
descriptinotallow="Useful for retrieving specific context related to the data source",
)
# initialize router query engine (single selection, pydantic)
query_engine = RouterQueryEngine(
selector=PydanticSingleSelector.from_defaults(),
query_engine_tools=[
list_tool,
vector_tool,
],
)
query_engine.query("<query>")
2.语义路由
利用语义相关性检索来选择最佳的分支。
每个路由都有一组与之关联的示例查询,这些查询会被embedding并存储为向量。传入的查询也会被embedding,并针对路由器中的其他示例查询进行相似性搜索。匹配度最高的查询的路由将被选中。
以semantic-router(https://github.com/aurelio-labs/semantic-router)这个项目为例了解其具体细节。例如,设置两个路由,一个用于政治问题的问答,另一个用于一般闲聊类型的问答。对于每个路由都会分配一个通常可能被用来触发该路由分支的问题列表。这些示例查询(utterances)将被embedding,以便可以将它们用于针对用户查询的相似性搜索。
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]
#创建路由层
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 的路由器更快。
3.零样本文本分类路由
零样本文本分类(Zero-shot text classification)是NLP中的一项任务,其中模型在一个标记样本集上进行训练,进而获得能够对来自先前未见过的样本进行分类,比如基于bert的分类器。
而这类路由便是利用零样本分类模型给一段文本打上标签,而这些标签来自于预定义的标签路由。
比如,Haystack 中的 ZeroShotTextRouter便是这种实现路径。具体参考:https://github.com/deepset-ai/haystack/blob/main/haystack/components/routers/zero_shot_text_router.py#L130
4.语言分类路由
这类路由器能够识别查询所使用的语言,并根据该语言路由查询。如果应用程序需要某种多语言解析能力,这将非常有用。
比如,Haystack 中的 TextClassificationRouter,它利用 langdetect 库来检测文本的语言,该库本身使用朴素贝叶斯算法来检测语言。参考:https://github.com/deepset-ai/haystack/blob/main/haystack/components/routers/text_language_router.py#L90
5.关键字路由
该类路由将尝试通过匹配查询和路由列表之间的关键字来选择分支。
这个关键字路由器也可以由 LLM 来识别关键字,或者由其他一些关键字匹配库来实现。
6.逻辑路由
它们使用逻辑检查变量,例如字符串长度、文件名和值比较来处理如何路由查询。它们与编程中使用的典型 If/Else 条件非常相似。它们不是基于必须理解自然语言查询的意图,而是可以根据现有和变量参数做出选择。典型实现如HayStack 中的 ConditionalRouter 和 FileTypeRouter。
小结
不管是RAG应用还是普通的业务系统,都存在着大量的分支判断,这种判断早期由于自然语言技术的落后(准确率和性能)导致大多数实现为语法判断,而随着LLM技术的发展,必然会带动语义判断和分发需求的大发展,另一方面,随着自然语言交互(LUI)的不断普及,路由模块将成为其中核心实现受到更大重视。
参考:
https://towardsdatascience.com/routing-in-rag-driven-applications-a685460a7220
本文转载自 AI工程化,作者: ully