AI实践:使用Runnable实现RouterChain | 豆包MarsCode AI刷题

99 阅读8分钟

在LangChain的学习小册中,按照课程要求使用LLMChain会出现弃用提示。后来通过官方文档和网上资料学习到了如何使用Runnable来实现课程中的各种链任务。

LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17 and will be removed in 1.0. Use RunnableSequence, e.g., `prompt | llm` instead.

Runnable

LangChain中的Runnable是一个强大的抽象层,它允许开发者以一种统一的方式定义和执行各种类型的任务。以下是Runnable的一些主要优势:

  1. 统一接口Runnable提供了一个统一的接口来定义和执行任务,无论这些任务是简单的函数调用、复杂的链式操作,还是与其他系统的集成。这使得代码更加模块化和可重用。
  2. 灵活性Runnable可以组合和嵌套,允许你构建复杂的任务流程。你可以将多个Runnable对象组合成一个链(Chain),或者将它们嵌套在其他Runnable中,从而实现高度灵活的任务编排。
  3. 异步支持Runnable天然支持异步操作,这意味着你可以在任务中使用异步函数,并在需要时并发执行多个任务。
  4. 可扩展性Runnable的设计允许你轻松地扩展和自定义任务的执行逻辑。你可以通过继承或组合现有的Runnable来创建新的任务类型。
  5. 调试和监控Runnable提供了丰富的调试和监控功能,你可以轻松地跟踪任务的执行流程,捕获中间结果,并在需要时进行干预。

简单示例

以下是一个简单的示例,展示了如何使用Runnable来定义和执行一个任务链。

from langchain import Runnable, RunnablePassthrough

# 定义一个简单的Runnable,它将输入字符串转换为大写
class UppercaseRunnable(Runnable):
    def run(self, input):
        return input.upper()

# 定义另一个Runnable,它将输入字符串反转
class ReverseRunnable(Runnable):
    def run(self, input):
        return input[::-1]

# 创建一个任务链,将输入字符串先转换为大写,然后反转
chain = UppercaseRunnable() | ReverseRunnable()

# 执行任务链
result = chain.run("hello")

print(result)  # 输出: "OLLEH"

RunnableBranch 和 RunnableLambda

RunnableBranch 的用法

RunnableBranch 是 LangChain 中用于实现条件分支逻辑的组件。它允许你根据不同的条件选择不同的 Runnable 来执行。RunnableBranch 的核心思想是根据输入数据的不同条件,动态选择并执行相应的 Runnable

示例

from langchain.prompts import PromptTemplate
from langchain_core.runnables import RunnableBranch

# 定义两个不同的提示模板
template1 = PromptTemplate.from_template("这是模板1,输入是: {input}")
template2 = PromptTemplate.from_template("这是模板2,输入是: {input}")

# 定义一个 RunnableBranch,根据输入的条件选择不同的模板
branch = RunnableBranch(
    (lambda x: x["condition"] == "case1", template1),
    (lambda x: x["condition"] == "case2", template2),
)

# 执行 RunnableBranch
result = branch.invoke({"condition": "case1", "input": "Hello"})
print(result)  # 输出: "这是模板1,输入是: Hello"

result = branch.invoke({"condition": "case2", "input": "Hello"})
print(result)  # 输出: "这是模板2,输入是: Hello"

解释

  1. 定义条件和对应的 Runnable:在 RunnableBranch 中,我们定义了两个条件和对应的 Runnable。每个条件是一个函数,它接受输入数据并返回一个布尔值。如果条件为真,则选择对应的 Runnable 执行。
  2. 执行 RunnableBranch:通过调用 branch.invoke,我们传递一个包含条件和输入数据的字典。RunnableBranch 会根据条件选择合适的 Runnable 并执行。

RunnableLambda 的用法

RunnableLambda 是 LangChain 中用于包装普通函数或 lambda 表达式的组件。它允许你将一个函数或 lambda 表达式转换为一个 Runnable,从而可以在任务链中使用。

示例

from langchain_core.runnables import RunnableLambda

# 定义一个简单的函数
def print_info(info: str):
    print(f"info: {info}")
    return info

# 将函数包装为 RunnableLambda
lambda_runnable = RunnableLambda(print_info)

# 执行 RunnableLambda
result = lambda_runnable.invoke("Hello")
print(result)  # 输出: "Hello"

解释

  1. 定义函数:我们定义了一个简单的函数 print_info,它接受一个字符串并打印出来,然后返回该字符串。
  2. 包装为 RunnableLambda:通过 RunnableLambda,我们将 print_info 函数包装为一个 Runnable
  3. 执行 RunnableLambda:通过调用 lambda_runnable.invoke,我们传递一个字符串给 print_info 函数,并执行它。

结合使用 RunnableBranch 和 RunnableLambda

在实际应用中,RunnableBranch 和 RunnableLambda 可以结合使用,以实现更复杂的任务链。例如,我们可以在 RunnableBranch 中使用 RunnableLambda 来处理不同的条件分支。

示例

from langchain.prompts import PromptTemplate
from langchain_core.runnables import RunnableBranch, RunnableLambda

# 定义两个不同的提示模板
template1 = PromptTemplate.from_template("这是模板1,输入是: {input}")
template2 = PromptTemplate.from_template("这是模板2,输入是: {input}")

# 定义一个简单的函数
def print_info(info: str):
    print(f"info: {info}")
    return info

# 定义一个 RunnableBranch,根据输入的条件选择不同的模板,并在每个分支中使用 RunnableLambda
branch = RunnableBranch(
    (lambda x: x["condition"] == "case1", RunnableLambda(print_info) | template1),
    (lambda x: x["condition"] == "case2", RunnableLambda(print_info) | template2),
)

# 执行 RunnableBranch
result = branch.invoke({"condition": "case1", "input": "Hello"})
print(result)  # 输出: "这是模板1,输入是: Hello"

result = branch.invoke({"condition": "case2", "input": "Hello"})
print(result)  # 输出: "这是模板2,输入是: Hello"

解释

  1. 定义条件和对应的 Runnable:在 RunnableBranch 中,我们定义了两个条件和对应的 Runnable。每个条件是一个函数,它接受输入数据并返回一个布尔值。如果条件为真,则选择对应的 Runnable 执行。
  2. 使用 RunnableLambda:在每个分支中,我们使用 RunnableLambda 包装 print_info 函数,并在每个分支中执行它。
  3. 执行 RunnableBranch:通过调用 branch.invoke,我们传递一个包含条件和输入数据的字典。RunnableBranch 会根据条件选择合适的 Runnable 并执行。

通过这种方式,我们可以灵活地组合和使用 RunnableBranch 和 RunnableLambda,以实现复杂的任务链和条件分支逻辑。

RouterChain实现

完整代码

from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.runnables import RunnableBranch,RunnableLambda
import os
from operator import itemgetter
from langchain.chains.router.llm_router import RouterOutputParser
from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE as RounterTemplate

# 构建两个场景的模板
flower_care_template = """你是一个经验丰富的园丁,擅长解答关于养花育花的问题。
                        下面是需要你来回答的问题:
                        {input}"""

flower_deco_template = """你是一位网红插花大师,擅长解答关于鲜花装饰的问题。
                        下面是需要你来回答的问题:
                        {input}
                        """
# 一个默认模板
other_template =    """
                        你是一个AI助手,你会回答一下问题。
                        具体问题如下:
                        {input}
                        """


# 构建提示信息
prompt_infos = [
    {
        "key": "flower_care",
        "description": "适合回答关于鲜花护理的问题",
        "template": flower_care_template,
    },
    {
        "key": "flower_decoration",
        "description": "适合回答关于鲜花装饰的问题",
        "template": flower_deco_template,
    }
]


llm = ChatOpenAI(model=os.environ.get("LLM_MODELEND"))


destinations = [f"{p['key']}: {p['description']}" for p in prompt_infos]
router_template = RounterTemplate.format(destinations="\n".join(destinations))

router_prompt = PromptTemplate.from_template(router_template,output_parser=RouterOutputParser())
# print("路由提示:\n",router_prompt)
router_chain = router_prompt | llm | JsonOutputParser()

def print_info(info: str):
    print(f"info: {info}")
    return info

prompt = RunnableBranch(
    (lambda x: "flower_care" == x["topic"]['destination'], PromptTemplate.from_template(flower_care_template)),
    (lambda x: "flower_decoration" == x["topic"]['destination'], PromptTemplate.from_template(flower_deco_template)),
    PromptTemplate.from_template(other_template)
)

chain = ({"topic": router_chain, "input": itemgetter('input')} | 
            RunnableLambda(print_info) |
            prompt |  RunnableLambda(print_info)| llm | StrOutputParser())

print(chain.invoke({"input": "如何为婚礼场地装饰花朵?"}))
print(chian.invoke({"input": "如何学习好LangChian?"}))

代码解释

我使用LangChain的Runnable来实现一个类似于RouterChain的功能,通过动态选择合适的提示模板来处理不同类型的问题。

首先,我定义了三个不同的提示模板:

  1. flower_care_template:用于回答关于鲜花护理的问题。
  2. flower_deco_template:用于回答关于鲜花装饰的问题。
  3. other_template:作为默认模板,用于处理其他类型的问题。

接下来,我创建了一个包含这些提示模板的列表prompt_infos,每个模板都有一个唯一的键(key)和描述(description)。

然后,我初始化了一个ChatOpenAI模型,并使用这些提示模板的描述来构建一个路由提示模板。这个路由提示模板的作用是根据输入的问题类型,选择合适的提示模板。

为了实现路由功能,我定义了一个router_chain,它由路由提示模板、语言模型(LLM)和输出解析器组成。这个链的作用是根据输入的问题,动态选择合适的提示模板。

接下来,我定义了一个RunnableBranch,它根据路由链的输出(即选择的提示模板)来选择合适的提示模板。具体来说,如果路由链选择的是flower_care,则使用flower_care_template;如果选择的是flower_decoration,则使用flower_deco_template;否则,使用默认的other_template

最后,我构建了一个完整的任务链chain,它首先通过路由链选择合适的提示模板,然后将输入传递给选择的提示模板,并最终通过语言模型生成回答。在任务链的执行过程中,我还添加了一个打印信息的步骤,以便在执行过程中输出一些调试信息。

通过这种方式,我可以根据输入的问题类型,动态选择合适的提示模板,并生成相应的回答。例如,当输入的问题是“如何为婚礼场地装饰花朵?”时,系统会选择flower_deco_template,并生成关于鲜花装饰的回答;而当输入的问题是“如何学习好LangChain?”时,系统会选择默认的other_template,并生成关于学习LangChain的回答。