【langchain实战笔记】6、路由链(RouterChain)与图形表示

本系列文章是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、自定义处理链进阶(并行和运行时配置)

相关推荐
用户67570498850230 分钟前
告别数据库瓶颈!用这个技巧让你的程序跑得飞快!
后端
千|寻1 小时前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱1 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯1 小时前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响1 小时前
枚举在实际开发中的使用小Tips
后端
wuhunyu1 小时前
基于 langchain4j 的简易 RAG
后端
techzhi1 小时前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
春末的南方城市2 小时前
Ctrl-Crash 助力交通安全:可控生成逼真车祸视频,防患于未然
人工智能·计算机视觉·自然语言处理·aigc·音视频
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员