本系列文章是langchain框架v0.3版本的学习实战笔记。
前言
在前面的章节中,我们介绍了如何创建一个简单的大模型应用,通过自定义工具,扩展大模型获取信息的能力,通过自定义处理链,构建可以执行多个处理步骤实现复杂功能的大模型应用,以及如何在运行时对处理链进行配置,本篇文章我们将介绍如何自定义路由链。
基于模型决策的路由链
langchain 允许开发者通过创建自定义路由链来实现非确定性处理链的开发,当我们在开发功能复杂的大模型应用时非常有用,例如我们可以通过自定义的路由链来让大模型根据问题帮我们选择合适的提示词模板或者执行链,这让开发者可以在某些领域细分任务下的处理流程进行单独优化。
langchain框架提供了两种方式创建自定义处理链:
- 使用 RunnableLambda(推荐)
- 使用 RunnableBranch(遗留)
接下来我们用官方推荐的方式创建一个自定义路由链,非推荐方式后续会逐步弃用不再介绍。我们以一个支持天气查询和旅行规划的多功能个人助手为例。首先我们先创建两个自定义链:天气助手链和旅行规划链。
(1)天气助手链
python
import os
os.environ['OPENAI_API_KEY'] = "XXXXXX"
os.environ["OPENAI_API_BASE"] = "XXXXXX"
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from operator import itemgetter
from langchain_core.runnables import chain
import requests
from langchain_core.runnables import RunnablePassthrough
model = ChatOpenAI(
model_name="gpt-4o",
temperature=0.0
)
# 天气情况chain
@chain
def weather_search(dict):
query = dict["question"]
top_k = dict["top_k"]
url = f"https://www.googleapis.com/customsearch/v1?key={YOU_KEY}&cx={YOU_CX}&q={query}".format(
query=query)
response = requests.get(url)
# 只保留有用信息
result = []
for item in response.json()["items"]:
if len(result) >= top_k:
break
result.append({"Title": item['title'], "Snippet": item['snippet'], "Link": item['link']})
return result
# 天气助手
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你的工作是根据用户提供的上线文信息总结指定城市的天气情况,并给出对应的穿衣指数建议,请使用以下回答模板\n 穿衣指数:...|||",
),
("human", "content:{content},question:{question}"),
]
)
weather_assistant = (
{"content": {"question": RunnablePassthrough(), "top_k": lambda x: 3} | weather_search,
"question": RunnablePassthrough()
}
| prompt
| model
| StrOutputParser()
)
weather_assistant.invoke("北京今天天气怎么样?")
text
根据北京市气象局的最新天气预报:
今天白天,北京的最高气温为4℃,天气晴间多云,风力为3级左右转1级,北转南风。今天夜间,最低气温为-5℃,天气晴转多云,风力为1、2级,偏南风。
穿衣指数:
建议穿着厚外套、毛衣等保暖衣物,外出时注意防寒保暖。
(2)旅行规划链
python
# 旅行助手
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你的工作是根据用户输入的城市和旅行天数给出对应的旅行规划建议,请使用以下回答模板\n 规划建议:...|||",
),
("human", "{question}"),
]
)
travel_assistant = (
{
"question": RunnablePassthrough()
}
| prompt
| model.bind(stop="|||")
| StrOutputParser()
)
travel_assistant.invoke("北京3天旅行计划")
(3)默认对话链
python
# 默认处理链
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你一个聊天机器人",
),
("human", "{question}"),
]
)
default_chain = (
{
"question": RunnablePassthrough()
}
| prompt
| model.bind(stop="|||")
| StrOutputParser()
)
default_chain.invoke("你好")
(3)定义路由链
路由链需要基于用户问题进行分类,并通过分类结果进行路由决策,我先首先定义一个问题分类的子链:
python
# 问题分类
prompt = ChatPromptTemplate.from_template(
"""你是一个决策模型,下面会给出一个用户问题,你需要判断该问题是属于'天气'还是'旅行'类型并给出分类结果,你只能回答'天气'、'旅行'或者'其他'
给出的用户问题为:{question}
""")
classification_chain = (
{"question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
定义根据分类结果进行决策的路由链
python
from langchain_core.runnables import RunnableLambda
from operator import itemgetter
def route(info: dict):
print(info)
if "天气" in info['classification']:
print("route weather_assistant")
return itemgetter("question") | weather_assistant
elif "旅行" in info['classification']:
print("route travel_assistant")
return itemgetter("question") | travel_assistant
else:
print("route default_chain")
return itemgetter("question") | default_chain
chat_assistant = (
{
"classification": RunnablePassthrough() | classification_chain,
"question": RunnablePassthrough()
}
| RunnableLambda(route)
)
route链的入参必须为 dict 类型,因为它既要接受分类结果,又要透传用户输入,route的输出为Runnable对象,这里需要我们将 itemgetter("question") 提取出来做为子链的输入。我们可以用验证路由结果是否符合我们的预期。
python
chat_assistant.invoke("北京天气怎么样?")
# route 入参:{'classification': '天气', 'question': '北京天气怎么样?'}
# route weather_assistant
# 输出结果:'北京天气预报显示,今天白天最高气温为2℃,多云,山区有零星小雪,风力为2、3级偏南风。今天夜间最低气温为-6℃,多云,山区有零星小雪,风力为1、2级南转。\n\n穿衣指数: 建议穿着厚重的冬季衣物,如羽绒服、毛衣、保暖内衣等,以防寒保暖。同时,注意防风,外出时可以佩戴围巾、手套和帽子。'
chat_assistant.invoke("请你给出一份北京3天旅行的规划")
# route 入参: {'classification': '旅行', 'question': '请你给出一份北京3天旅行的规划'}
# route travel_assistant
# '规划建议:\n\n**第一天:**\n- **天安门广场**: 早上参观天安门广场,观看升旗仪式,感受中国的历史和文化。\n- **故宫博物院**: 从天安门广场步行至故宫,参观这座世界上最大的古代宫殿建筑群,了解明清两代的皇宫历史。\n- **景山公园**: 下午前往景山公园,登上景山,俯瞰整个故宫和北京城的美景。\n- **王府井大街**: 晚上可以去王府井大街逛街购物,品尝北京的特色小吃。\n\n**第二天:**\n- **八达岭长城**: 早上出发前往八达岭长城,体验"不到长城非好汉"的豪情壮志,建议提前预定门票和交通。\n- **十三陵**: 下午参观明十三陵,了解明朝皇帝的陵寝文化。\n- **鸟巢和水立方**: 返回市区后,可以去奥林匹克公园参观鸟巢和水立方,感受2008年北京奥运会的辉煌。\n\n**第三天:**\n- **颐和园**: 早上前往颐和园,游览这座保存完好的皇家园林,欣赏昆明湖和万寿山的美景。\n- **圆明园**: 下午可以去圆明园遗址公园,了解这座曾经辉煌的皇家园林的历史和遭遇。\n- **南锣鼓巷**: 晚上可以去南锣鼓巷,感受老北京的胡同文化,品尝地道的北京小吃。\n\n希望这份规划能让你在北京度过一个愉快而充实的三天旅行!'
chat_assistant.invoke("你好")
# route 入参: {'classification': '其他', 'question': '你好'}
# route default_chain
# 你好!有什么我可以帮忙的吗?
上述例子我们为了更详细的展示路由链的工作原理,将分类于路由决策进行了拆分,当然也可以将分类环节融合到路由链中,以减少参数传递的步骤,让代码更加简洁,大家可以自行尝试。
基于语义相关的路由链
语义相关性的路由主要应用于提示词模板和提示词样本的选择,首先需要将提示词模板进行向量化,并构建提示词模板的向量召回池,将用户输入进行向量化,通过向量计算用户输入与各个提示词模板之间的相关性,召回与用户输入最相关的提示词模板,我们可以通过langchain 提供的官方案例学习这种方法。
python
from langchain_community.utils.math import cosine_similarity
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
physics_template = """You are a very smart physics professor. \
You are great at answering questions about physics in a concise and easy to understand manner. \
When you don't know the answer to a question you admit that you don't know.
Here is a question:
{query}"""
math_template = """You are a very good mathematician. You are great at answering math questions. \
You are so good because you are able to break down hard problems into their component parts, \
answer the component parts, and then put them together to answer the broader question.
Here is a question:
{query}"""
# 提示词模板Embdding
embeddings = OpenAIEmbeddings()
prompt_templates = [physics_template, math_template]
prompt_embeddings = embeddings.embed_documents(prompt_templates)
def prompt_router(input):
query_embedding = embeddings.embed_query(input["query"])
similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
most_similar = prompt_templates[similarity.argmax()]
print("Using MATH" if most_similar == math_template else "Using PHYSICS")
return PromptTemplate.from_template(most_similar)
chat_chain = (
{"query": RunnablePassthrough()}
| RunnableLambda(prompt_router)
| model
| StrOutputParser()
)
chat_chain.invoke("What's a black hole")
通常情况下,我们无法确保所有用户按照系统所期待的方式进行输入,因此我们无法确保提示词模板可以涵盖用户感兴趣的所有领域,因此在实际生产中,我们通常会设置相关性指标阈值以及一个默认的提示词模板,从而确保用户不会得到一个完全不相关的预设提示模板。
图形表示
使用LangChain创建应用后,我们可能需要检查搭建的处理链路是否是我们预期的流程,Langchain提供了工具帮助打印调用链的图形表示,以更直观的展示可运行对象之间的关系。 首先需要我们安装相关依赖:
shell
pip install grandalf
我们以基于模型决策的路由链为例,展示其调用链的图形表示。
python
chat_assistant.get_graph().print_ascii()
输出结果如下:
text
+----------------------------------------+
| Parallel<classification,question>Input |
+----------------------------------------+
*** ***
** **
** **
+-------------+ **
| Passthrough | *
+-------------+ *
* *
* *
* *
+-------------+ *
| Passthrough | *
+-------------+ *
* *
* *
* *
+--------------------+ *
| ChatPromptTemplate | *
+--------------------+ *
* *
* *
* *
+------------+ *
| ChatOpenAI | *
+------------+ *
* *
* *
* *
+-----------------+ +-------------+
| StrOutputParser | | Passthrough |
+-----------------+ +-------------+
*** ***
** **
** **
+-----------------------------------------+
| Parallel<classification,question>Output |
+-----------------------------------------+
*
*
*
+-------------+
*| route_input |****
****** +-------------+** *********
****** *** **********
****** *** *********
*** **** *********
+---------------------------------+ ** *****
| Parallel<content,question>Input | * *
+---------------------------------+ * *
*** **** * *
*** **** * *
** **** * *
+-------------------------------+ ** * *
| Parallel<question,top_k>Input | * * *
+-------------------------------+ * * *
*** ** * * *
* ** * * *
** * * * *
+-------------+ +--------+ * * *
| Passthrough | | Lambda | * * *
+-------------+ +--------+ * * *
*** ** * * *
* ** * * *
** * * * *
+--------------------------------+ * * *
| Parallel<question,top_k>Output | * * *
+--------------------------------+ * * *
* * * *
* * * *
* * * *
+----------------+ +-------------+ * *
| weather_search | | Passthrough | * *
+----------------+ *+-------------+ * *
*** **** * *
*** **** * *
** ** * *
+----------------------------------+ +-------------+ +-------------+
| Parallel<content,question>Output | | Passthrough | | Passthrough |
+----------------------------------+ +-------------+ +-------------+
* * *
* * *
* * *
+--------------------+ +--------------------+ +--------------------+
| ChatPromptTemplate | | ChatPromptTemplate | | ChatPromptTemplate |
+--------------------+ +--------------------+ +--------------------+
* * *
* * *
* * *
+------------+ +------------+ +------------+
| ChatOpenAI | | ChatOpenAI | | ChatOpenAI |
+------------+ +------------+ +------------+
* * *
* * *
* * *
+-----------------+ +-----------------+ +-----------------+
| StrOutputParser | | StrOutputParser | ****| StrOutputParser |
+-----------------+***** +-----------------+********* +-----------------+
****** *** *********
****** **** *********
*** ** *****
+--------------+
| route_output |
+--------------+
后记
在本文中,我们详细介绍了自定义路由链的构建原理,包括基于大模型决策的路由链、基于向量相关性检索的路由链,以帮助大家理解实现路由决策的底层机制。其实在早期的版本中Langchain框架提供了构建路由链的封装工具,以协助开发者快速搭建自定义路由链,例如LLMRouterChain 和 MultiPromptChain
- LLMRouterChain 将用户输入放进大语言模型,通过Prompt的形式让大语言模型来进行路由。类似本文中第一个例子。
- MultiPromptChain 通过向量搜索的方式,将用户输入与预设的提示词模板进行匹配选择。类似本文的第二个例子。
由于MultiPromptChain不支持常见的聊天模型功能,例如消息角色和工具调用,在LangChain后续的版本更新中,正计划逐步弃用上述定义方法,改用LangGraph支持,因此我们不再详细介绍上述方式的构建。
LangGraph是一种基于图架构进行大模型应用处理链构建管理的框架,在本系列文章后续我们会详细介绍。相比于MultiPromptChain这种方式,LangGraph实现为解决这类问题带来了许多优点:
- 支持聊天提示模板,包括与system其他角色的消息;
- 支持使用工具调用构建流程;
- 支持单步和流式传输的调用方式。 具体改造方式可以参考官方迁移文档:从 MultiPromptChain 迁移
系列文章
【langchain实战笔记】3、构建自定义多工具智能体
【langchain实战笔记】4、自定义处理链基础(基于天气信息的旅行助手)
【langchain实战笔记】5、自定义处理链进阶(并行和运行时配置)