LangChain的Chain(链)与记忆机制| 豆包MarsCode AI刷题

210 阅读12分钟

一,什么是 Chain(链)

LangChain 中,Chain(链) 是将多个组件组合在一起,以顺序执行的方式,形成一个功能完整的工作流的基本单元。每个链将处理一些特定的任务,并且可以与其他链或组件进行组合,从而构建出更复杂的应用程序。

核心概念

“链”的核心思想就是将多个处理步骤(如模型推理、数据处理等)按照一定的顺序组织在一起,让整个过程更加模块化,易于管理和调试。

通过链,开发者可以:

  • 模块化应用程序:把任务分解成多个组件,方便逐个调试和修改。
  • 简化复杂应用:把多个步骤或模块串联起来,形成一个完整的工作流。

LangChain中的链(Chain)

LangChain 提供了多种类型的链,每种链封装了不同的功能,使得开发者可以更加方便地实现各类应用。每个链都可以与其他链组合,或者与其他组件(如模型、数据处理器、输出解析器等)结合,形成一个更大的工作流。

1. LLMChain - 最简单的链

LLMChain 是最基础、最常用的链类型,专注于处理语言模型(如 GPT-3 或 GPT-4)与输入数据之间的交互。它将以下组件组合在一起:

  • PromptTemplate:构建格式化的提示。
  • LLM(语言模型) :生成文本的模型。
  • OutputParser:解析和处理模型输出。

通过 LLMChain,你可以更方便地处理输入数据和语言模型之间的交互,而不必手动管理每个步骤。例如,生成一朵花的花语时,LLMChain 会自动将提示模板、输入数据传递给语言模型,并返回生成的结果。

代码示例:使用 LLMChain 简化任务

不使用链时,你的代码可能如下所示:


from langchain import PromptTemplate
from langchain import OpenAI

# 创建提示模板
template = "{flower}的花语是?"
prompt_temp = PromptTemplate.from_template(template)
prompt = prompt_temp.format(flower='玫瑰')

# 调用模型
model = OpenAI(temperature=0)
result = model(prompt)
print(result)

使用链之后,代码更加简洁:

from langchain import PromptTemplate, OpenAI, LLMChain

# 创建模型实例
llm = OpenAI(temperature=0)

# 创建 LLMChain
llm_chain = LLMChain(
    llm=llm,
    prompt=PromptTemplate.from_template("{flower}的花语是?")
)

# 调用链并返回结果
result = llm_chain("玫瑰")
print(result)

输出

{'flower': '玫瑰', 'text': '\n\n爱情、浪漫、美丽、永恒、誓言、坚贞不渝。'}

2. SequentialChain - 顺序链

SequentialChain 是一种特殊的链,它允许你将多个链按顺序串联起来,逐步处理数据并产生最终结果。每个链的输出会作为下一个链的输入,从而形成一个处理流程。

示例:将多个链串联生成复杂的文案 假设我们希望创建一个鲜花的文案,过程包括:

  1. 生成鲜花的植物学介绍。
  2. 基于植物学介绍生成评论。
  3. 基于介绍和评论生成社交媒体文案。
from langchain.llms import OpenAI
from langchain.chains import LLMChain, SequentialChain
from langchain.prompts import PromptTemplate

# 设置OpenAI API密钥
import os
os.environ["OPENAI_API_KEY"] = 'your-openai-api-key'

# 第一步:生成鲜花的介绍
llm = OpenAI(temperature=.7)
template = """
你是一个植物学家。给定花的名称和颜色,写出一段关于鲜花的介绍。
花名: {name}
颜色: {color}
介绍:
"""
prompt_template = PromptTemplate(input_variables=["name", "color"], template=template)
introduction_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="introduction")

# 第二步:生成评论
template = """
你是一个鲜花评论家。根据下面的介绍写出对这朵花的评论。
介绍: {introduction}
评论:
"""
prompt_template = PromptTemplate(input_variables=["introduction"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="review")

# 第三步:生成社交媒体文案
template = """
你是花店的社交媒体经理。根据下面的介绍和评论写一篇社交媒体帖子。
介绍: {introduction}
评论: {review}
社交媒体文案:
"""
prompt_template = PromptTemplate(input_variables=["introduction", "review"], template=template)
social_post_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="social_post_text")

# 组合成顺序链
overall_chain = SequentialChain(
    chains=[introduction_chain, review_chain, social_post_chain],
    input_variables=["name", "color"],
    output_variables=["introduction", "review", "social_post_text"],
    verbose=True
)

# 运行链
result = overall_chain({"name": "玫瑰", "color": "黑色"})
print(result)

输出示例

{
    'name': '玫瑰',
    'color': '黑色',
    'introduction': '黑色玫瑰,这是一种对传统玫瑰花的颠覆...',
    'review': '黑色玫瑰,不仅仅是一朵花,它是一种艺术表现...',
    'social_post_text': '欢迎来到我们的自媒体平台,今天,我们要向您展示的是黑色玫瑰...'
}

链的调用方式

  1. 直接调用:直接调用链对象,实际上是调用 __call__ 方法。

    result = llm_chain({"flower": "玫瑰", "season": "夏季"})
    
  2. 通过 run 方法:等价于直接调用 __call__ 方法。

    result = llm_chain.run("玫瑰")
    
  3. 通过 predict 方法:类似 run,但使用关键字参数。

    result = llm_chain.predict(flower="玫瑰")
    
  4. 通过 apply 方法:一次处理多个输入(批量处理)。

    input_list = [{"flower": "玫瑰", "season": "夏季"}, {"flower": "百合", "season": "春季"}]
    result = llm_chain.apply(input_list)
    
  5. 通过 generate 方法:返回一个包含更多信息的结果对象(如模型的令牌使用情况)。

    result = llm_chain.generate(input_list)
    

总结

在 LangChain 中, 是一个核心概念,它将多个组件(如模型、提示模板、输出解析器等)按顺序组合在一起,形成一个连贯的工作流。通过使用链,开发者可以更加简洁、模块化地构建复杂的应用程序。LangChain 提供了多种类型的链,最基础的是 LLMChain,用于处理与语言模型的交互,而 SequentialChain 等类型则帮助我们将多个链串联起来,实现更复杂的任务。

二,任务设定

在这个任务中,我们需要构建一个能够自动处理用户问题并根据问题类型选择合适回答的智能客服系统。假设鲜花运营智能客服ChatBot通常会接到两大类问题:

  1. 鲜花养护(保持花的健康、如何浇水、施肥等)
  2. 鲜花装饰(如何搭配花、如何装饰场地等)

我们需要构建两个不同的目标链:一个用于处理鲜花养护问题,另一个用于处理鲜花装饰问题。为了实现这一点,我们会使用LangChain库中的RouterChain和MultiPromptChain来自动引导大语言模型选择不同的模板。

整体框架

RouterChain能动态选择用于给定输入的下一个链。我们会根据用户的问题内容,首先使用RouterChain确定问题更适合哪个处理模板,然后将问题发送到该处理模板进行回答。如果问题不适合任何已定义的处理模板,它会被发送到默认链。

在这里,我们会用LLMRouterChain和MultiPromptChain(也是一种路由链)组合实现路由功能,该MultiPromptChain会调用LLMRouterChain选择与给定问题最相关的提示,然后使用该提示回答问题。

具体步骤

  1. 构建处理模板:为鲜花护理和鲜花装饰分别定义两个字符串模板。
  2. 提示信息:使用一个列表来组织和存储这两个处理模板的关键信息,如模板的键、描述和实际内容。
  3. 初始化语言模型:导入并实例化语言模型。
  4. 构建目标链:根据提示信息中的每个模板构建对应的LLMChain,并存储在一个字典中。
  5. 构建LLM路由链:这是决策的核心部分。首先,它根据提示信息构建了一个路由模板,然后使用这个模板创建了一个LLMRouterChain。
  6. 构建默认链:如果输入不适合任何已定义的处理模板,这个默认链会被触发。
  7. 构建多提示链:使用MultiPromptChain将LLM路由链、目标链和默认链组合在一起,形成一个完整的决策系统。

具体实现

1. 构建提示信息的模板

首先,我们针对两种场景,构建两个提示信息的模板。

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

flower_deco_template = """你是一位网红插花大师,擅长解答关于鲜花装饰的问题。
                        下面是需要你来回答的问题:
                        {input}"""

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

2. 初始化语言模型

接下来,我们初始化语言模型。

# 初始化语言模型
from langchain.llms import OpenAI
import os

os.environ["OPENAI_API_KEY"] = '你的OpenAI Key'
llm = OpenAI()

3. 构建目标链

下面,我们循环prompt_infos这个列表,构建出两个目标链,分别负责处理不同的问题。

# 构建目标链
from langchain.chains.llm import LLMChain
from langchain.prompts import PromptTemplate

chain_map = {}
for info in prompt_infos:
    prompt = PromptTemplate(template=info['template'], input_variables=["input"])
    chain = LLMChain(llm=llm, prompt=prompt, verbose=True)
    chain_map[info["key"]] = chain

4. 构建路由链

下面,我们构建路由链,负责查看用户输入的问题,确定问题的类型。

# 构建路由链
from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser
from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE as RounterTemplate

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

router_prompt = PromptTemplate(
    template=router_template,
    input_variables=["input"],
    output_parser=RouterOutputParser()
)

router_chain = LLMRouterChain.from_llm(llm, router_prompt, verbose=True)

5. 构建默认链

如果路由链没有找到适合的链,那么,就以默认链进行处理。

# 构建默认链
from langchain.chains import ConversationChain

default_chain = ConversationChain(llm=llm, output_key="text", verbose=True)

6. 构建多提示链

最后,我们使用MultiPromptChain类把前几个链整合在一起,实现路由功能。

# 构建多提示链
from langchain.chains.router import MultiPromptChain

chain = MultiPromptChain(
    router_chain=router_chain,
    destination_chains=chain_map,
    default_chain=default_chain,
    verbose=True
)

运行路由链

现在开始提出各种问题,测试一下我们的链。

# 测试A
print(chain.run("如何为玫瑰浇水?"))

# 测试B
print(chain.run("如何为婚礼场地装饰花朵?"))

# 测试C
print(chain.run("如何考入哈佛大学?"))

这三个测试,分别被路由到了三个不同的目标链,其中两个是我们预设的“专家类型”目标链,而第三个问题:“如何考入哈佛大学?”被模型一眼看穿,并不属于任何鲜花运营业务场景,路由链把它抛入了一个 default chain —— ConversationChain 去解决。

三,LangChain的记忆机制

1. ConversationBufferMemory (缓冲记忆)

  • 原理:它会将整个对话历史保存下来,并将其传递给LLM(大型语言模型)。这种方式确保了模型每次生成回答时都能参考到之前的对话内容。
  • 优点:最简单的记忆方式,容易理解和实现,适用于对话历史较短、不会超过Token限制的场景。
  • 缺点:随着对话轮次增加,记忆体积会迅速增长,导致Token数急剧增加,最终可能导致性能下降和成本上升。

2. ConversationBufferWindowMemory (缓冲窗口记忆)

  • 原理:这种机制设置了一个窗口大小 k,只保留最近的 k 次对话历史。当新的对话加入时,最旧的对话内容会被丢弃,从而控制记忆的大小。
  • 优点:有效控制Token的消耗,适用于对话周期较短且不需要记住全部历史的场景。
  • 缺点:由于它只保留最近 k 次的对话历史,因此对于长对话或者需要了解较远对话背景的情况,这种方法就不适用。

3. ConversationSummaryMemory (对话总结记忆)

  • 原理:对话历史并不完全保存,而是通过使用LLM对之前的对话内容进行汇总和总结。每次新的对话加入时,模型会基于汇总内容生成新的对话历史。
  • 优点:非常适合长对话,因为它通过总结有效减少了Token使用的数量。
  • 缺点:对于短对话,它可能会生成较为冗长的总结,从而导致Token消耗过高;而且汇总的内容可能会有信息丢失的风险,尤其是一些细节会被忽略。

4. ConversationSummaryBufferMemory (对话总结缓冲记忆)

  • 原理:结合了总结记忆和缓冲窗口的特点,它会根据对话的长度动态选择是否保留原始对话内容或进行总结。当对话内容过长时,模型会自动对早期的对话进行总结,从而减小Token的占用。
  • 优点:既能保留最新的对话细节,又能对较长历史进行总结,适合长对话且需要在保持对话连贯性的同时节省Token的场景。
  • 缺点:虽然它能在大部分情况下有效管理Token,但在对话刚开始时,它仍可能会浪费一些Token。

总结

针对不同的应用场景,选择合适的记忆机制非常重要:

  • 短期对话、简单场景:如果对话的历史不长,ConversationBufferMemory 会比较合适,它会完整地保留对话历史,确保模型可以依赖完整上下文回答问题。
  • 频繁更新或不太依赖远期对话的场景ConversationBufferWindowMemory 是一个非常好的选择,通过控制历史长度来减少Token的消耗。
  • 长对话、需要总结信息的场景ConversationSummaryMemoryConversationSummaryBufferMemory 都是针对长对话优化Token使用的良好策略。如果对话持续时间较长,并且需要保留一定的上下文背景信息,但又不希望Token过度增长,ConversationSummaryBufferMemory 是最佳选择。

思考题回答:

假设你设计一个客服聊天机器人,并告知用户:“亲,我的记忆能力有限,只能记住和你的最近10次对话哦。如果我忘了之前的对话,请你体谅我。”

  • 记忆机制选择:考虑到这类客服机器人的设计需求是管理最近的对话而不依赖长时间的历史信息,ConversationBufferWindowMemory 会是一个很好的选择。你可以通过设置 k=10 来确保每次只有最近10轮对话会被记住。