贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

LangChain 文档学习 No.6 - 链

什么是链

LangChain 中通过 Chain 来链接各个组件和功能,模型之间彼此链接,或模型与其他组件链接

将多个组件相互链接,组合成一个链的想法简单但强大,简化了复杂应用程序的实现,使之更加模块化,能够创建出单一的、连贯的应用程序,从而使调试、维护和改进应用程序变得容易;这也是 LangChain 名称的由来

需要注意,在现版本中 LangChain 已经在主推 LangChain Expression Language(LCEL),逐渐废弃旧版本中手动创建 Chain 的方式以及旧的 Chain 实现,不过这里也参考早期 Chain 的使用方式,LCEL 留在后面进行学习

链的使用主要是以下两个步骤:

  1. 通过设计好的接口,实现一个具体的链的功能;例如 LLM 链(LLMChain)能够接受用户输入,使用 PromptTemplate 对其进行格式化,然后将格式化的响应传递给 LLM;这就相当于把整个Model I/O的流程封装到链里面
  2. 实现了链的具体功能之后,我们可以通过将多个链组合在一起,或者将链与其他组件组合来构建更复杂的链

链可以理解为对一组基础功能的封装,具有一个完整功能的组件,这么看来链其实可以被视为 LangChain 中的一种基本功能单元

LLMChain

LLMChain 围绕着语言模型推理功能又添加了一些功能,整合了PromptTemplate、语言模型(LLM 或聊天模型)和 Output Parser,相当于把 Model I/O 放在一个链中整体操作,它使用提示模板格式化输入,将格式化的字符串传递给 LLM,并返回 LLM 输出

举一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ----第一步 创建提示
from langchain import PromptTemplate
# 原始字符串模板
template = "{name}的外号是?"
# 创建 Prompt 模板
prompt_temp = PromptTemplate.from_template(template)
# 根据模板创建提示
prompt = prompt_temp.format(name='霍金')
# 打印提示的内容
print(prompt)

# ----第二步 创建并调用模型
import Azure
# 创建模型实例
model = Azure.chat_model
# 传入提示,调用模型,返回结果
result = model.predict(prompt)
print(result)
1
2
霍金的外号是?
霍金的外号是“黑洞先生”。

此时 Model I/O 的实现分为两个部分,提示模板的构建和模型的调用独立处理,如果再加上输出解析器则依然需要独立调用

如果使用 LLMChain 则可以将这几部分结合在一起,使过程更加简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
# 导入所需的库
from langchain import PromptTemplate, OpenAI, LLMChain
# 原始字符串模板
template = "{name}的外号是?"
# 创建模型实例
llm = OpenAI(temperature=0)
# 创建 LLMChain
llm_chain = LLMChain(
llm=Azure.chat_model,
prompt=PromptTemplate.from_template(template))
# 调用 LLMChain,返回结果
result = llm_chain("马斯克")
print(result)
1
{'name': '马斯克', 'text': '马斯克的外号是“火箭人”或“铁人”。'}

Chain 的调用方式

在上面的例子中,直接使用 __call__ 方法对 Chain 进行调用,例如 llm_chain("马斯克")

Chain 具有多种调用方式:

  • call:提示模板中包含多个变量时,可以使用字典一次性输入
  • run:等价于 __call__
  • predict:模板变量根据关键字参数赋值而不是字典
  • apply:针对输入列表运行链,一次处理多个输入
  • generate:类似于 apply,只不过返回一个 LLMResult 对象而不是字符串 LLMResult 通常包含模型生成文本过程中的一些相关信息,例如令牌数量、模型名称等

SequentialChain

掌握了 LLMChain 的基本用法,就可以使用 SequentialChain 将几个 LLMChain 串起来,形成一个顺序链

第一个 LLMChain - 推荐电影

1
2
3
4
5
6
7
8
9
llm = Azure.chat_model
template = """
你是一个电影爱好者,请根据用户希望看到的剧情和标签,推荐一部电影。

剧情: {plot}
标签: {tag}
电影爱好者: 这是我推荐的电影:"""
prompt_template = PromptTemplate(input_variables=["plot", "tag"], template=template)
introduction_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="movie_info")

第二个 LLMChain - 评论电影

1
2
3
4
5
6
7
8
9
llm = Azure.chat_model
template = """
你是一位电影评论家,给你一个电影信息,你需要为这部电影写一篇100字左右的评论。

电影信息:
{movie_info}
电影评论人对上述电影的评论:"""
prompt_template = PromptTemplate(input_variables=["movie_info"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="movie_review")

第三个 LLMChain - 根据上面的产出写出一篇自媒体的文案

1
2
3
4
5
6
7
8
9
10
11
12
template = """
你是一个评论电影的自媒体文案撰稿人。给定一部电影的介绍和评论,你需要为这部电影写一篇社交媒体的帖子,200字左右。

电影介绍:
{movie_info}
电影评论人对上述花的评论:
{movie_review}

社交媒体帖子:
"""
prompt_template = PromptTemplate(input_variables=["movie_info", "movie_review"], template=template)
social_post_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="social_post_text")

使用顺序链链接起来

1
2
3
4
5
6
7
8
9
10
# 按顺序运行这三个链
overall_chain = SequentialChain(
chains=[introduction_chain, review_chain, social_post_chain],
input_variables=["plot", "tag"],
output_variables=["movie_info", "movie_review", "social_post_text"],
verbose=True)

# 运行链,并打印结果
result = overall_chain({"plot": "激烈的太空战斗", "tag": "中国、科幻"})
print(result)
1
2
3
4
> Entering new SequentialChain chain...

> Finished chain.
{'plot': '激烈的太空战斗', 'tag': '中国、科幻', 'movie_info': '《流浪地球》\n\n《流浪地球》是一部中国科幻电影,它将带您进入一个激烈的太空战斗的世界。故事发生在未来,太阳即将灭亡,人类必须寻找新的家园。为了拯救地球,人类发起了一项大胆的计划,将地球变成了一个巨大的太空船,向外太空迁徙。然而,在这个危险的旅程中,他们面临着各种挑战和太空战斗。电影中充满了紧张刺激的场景和惊人的视觉效果,同时也展现了人类的勇气和团结精神。\n\n《流浪地球》是一部非常成功的中国科幻电影,它获得了广泛的赞誉和票房成功。它不仅仅是一部充满动作和太空战斗的电影,还探讨了人类对于未来和生存的思考。如果您喜欢激烈的太空战斗和科幻题材,这部电影将会是您的不错选择。', 'movie_review': '《流浪地球》是一部令人惊叹的中国科幻电影。它以壮观的视觉效果和扣人心弦的剧情吸引着观众。故事背景设定在太阳即将灭亡的未来,人类为了生存不得不将地球变成太空船,展开了一场惊险的迁徙之旅。电影中的太空战斗场景令人震撼,同时也展现了人类的勇气和团结精神。这部电影在票房上取得了巨大成功,同时也获得了广泛的赞誉。如果你喜欢科幻和动作片,那么《流浪地球》绝对是你不容错过的选择。', 'social_post_text': '大家好!今天我要给大家推荐一部非常精彩的电影,《流浪地球》!这是一部中国科幻电影,带领我们进入一个激烈的太空战斗的世界。\n\n故事发生在未来,太阳即将灭亡,人类为了生存必须寻找新的家园。他们发起了一项大胆的计划,将地球变成了一个巨大的太空船,向外太空迁徙。然而,在这个危险的旅程中,他们面临着各种挑战和太空战斗。\n\n这部电影充满了紧张刺激的场景和惊人的视觉效果,同时也展现了人类的勇气和团结精神。它不仅仅是一部充满动作和太空战斗的电影,还探讨了人类对于未来和生存的思考。\n\n《流浪地球》是一部非常成功的中国科幻电影,它获得了广泛的赞誉和票房成功。如果你喜欢激烈的太空战斗和科幻题材,这部电影将会是你的不错选择。\n\n快来看看这部令人惊叹的中国科幻电影,《流浪地球》,一定会让你大呼过瘾!#流浪地球 #科幻电影'}

使用不同的 variables 再试一次

1
2
result = overall_chain({"plot": "剑与魔法的世界", "tag": "奇幻、冒险"})
print(result)
1
2
3
4
> Entering new SequentialChain chain...

> Finished chain.
{'plot': '剑与魔法的世界', 'tag': '奇幻、冒险', 'movie_info': '《魔戒三部曲:指环王》。这是一部奇幻冒险电影系列,剧情发生在一个充满剑与魔法的世界中。故事讲述了一个拯救中土世界的冒险旅程,包括勇敢的英雄、邪恶的魔法师和各种神秘的生物。这部电影系列融合了精彩的剧情、惊人的特效和精美的场景,一定会让您沉浸在这个奇幻的世界中。', 'movie_review': '《魔戒三部曲:指环王》是一部令人沉浸的奇幻冒险电影系列。剧情发生在一个充满剑与魔法的世界中,其中包括了勇敢的英雄、邪恶的魔法师和各种神秘的生物。这部电影系列不仅拥有精彩的剧情,还有惊人的特效和精美的场景。观众们可以完全沉浸在这个奇幻的世界中,感受到其中的魔力。无论是对于魔戒系列的粉丝还是对于奇幻冒险电影的爱好者来说,这部电影都是一次不容错过的视觉盛宴。', 'social_post_text': '大家好!今天给大家推荐一部令人沉浸的奇幻冒险电影系列——《魔戒三部曲:指环王》!这部电影发生在一个充满剑与魔法的世界中,故事中有勇敢的英雄、邪恶的魔法师和各种神秘的生物。这部电影系列不仅有精彩的剧情,更有惊人的特效和精美的场景。你一定会完全沉浸在这个奇幻的世界中,感受到其中的魔力。无论你是魔戒系列的粉丝还是奇幻冒险电影的爱好者,这部电影都是一次不容错过的视觉盛宴。快来和我一起探索这个充满剑与魔法的世界吧!#魔戒指环王# #奇幻冒险电影#'}

上述示例通过了三个 LLMChain 和一个 SequentialChain 的结合,由推荐电影到最终写出电影文案的过程

进阶

这里使用极客时间的例子,制作一个简单的鲜花运营智能客服

假设这个鲜花运营智能客服通常会接到两大类问题:

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

这就引入了新的需求, 如果接到的是第一类问题,需要给出 A 指示;如果接到第二类的问题,需要给出 B 指示

在实现的过程,可以根据这两个场景来构建两个不同的目标链;遇到不同类型的问题,LangChain 会通过 RouterChain 来自动引导大语言模型选择不同的模板

模板准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 构建两个场景的模板
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,
}]

# 构建提示目标链链
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"])
print("目标提示:\n", prompt)
chain = LLMChain(llm=Azure.chat_model, prompt=prompt, verbose=True)
chain_map[info["key"]] = chain

LLMRouterChain

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

创建一个路由链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 构建路由链
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))
print("路由模板:\n", router_template)
print("------------------------------------")
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(), )
print("路由提示:\n", router_prompt)
router_chain = LLMRouterChain.from_llm(llm=Azure.chat_model,
prompt=router_prompt,
verbose=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
路由模板:
Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.

<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
```json
{{
"destination": string \ name of the prompt to use or "DEFAULT"
"next_inputs": string \ a potentially modified version of the original input
}}
```

REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any modifications are needed.

<< CANDIDATE PROMPTS >>
flower_care: 适合回答关于鲜花护理的问题
flower_decoration: 适合回答关于鲜花装饰的问题

<< INPUT >>
{input}

<< OUTPUT (must include ```json at the start of the response) >>
<< OUTPUT (must end with ```) >>

------------------------------------
路由提示:
input_variables=['input'] output_parser=RouterOutputParser() template='Given a raw text input to a language model select the model prompt best suited for the input. You will be given the names of the available prompts and a description of what the prompt is best suited for. You may also revise the original input if you think that revising it will ultimately lead to a better response from the language model.\n\n<< FORMATTING >>\nReturn a markdown code snippet with a JSON object formatted to look like:\n```json\n{{\n "destination": string \\ name of the prompt to use or "DEFAULT"\n "next_inputs": string \\ a potentially modified version of the original input\n}}\n```\n\nREMEMBER: "destination" MUST be one of the candidate prompt names specified below OR it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.\nREMEMBER: "next_inputs" can just be the original input if you don\'t think any modifications are needed.\n\n<< CANDIDATE PROMPTS >>\nflower_care: 适合回答关于鲜花护理的问题\nflower_decoration: 适合回答关于鲜花装饰的问题\n\n<< INPUT >>\n{input}\n\n<< OUTPUT (must include ```json at the start of the response) >>\n<< OUTPUT (must end with ```) >>\n'

通过上面的示例可以看到路由链是如何进行路由的,如何引导 LLM 进行路由判断,以及后续的路由提示:

  • 路由模板
    • 引言:简单的引导语句,提醒模型它将获得各种模型提示的名称和描述,以及可以更改原始输入以获得更好的响应
    • 格式说明(<< FORMATTING >>):指导模型如何格式化其输出,使其以特定的方式返回结果(Markdown 代码片段,其中包含一个特定格式的 JSON 对象) 下面的代码块显示了期望的 JSON 结构,其中 destination 是模型选择的提示名称(或 DEFAULT),而 next\_inputs 是可能被修订的原始输入
    • 额外的说明和要求
      • 提醒模型 destination 字段的值必须是下面列出的提示之一或是 DEFAULT
      • 再次强调,除非模型认为有必要,否则原始输入不需要修改
    • 候选提示(<< CANDIDATE PROMPTS >>):列出了两个示例模型提示及其描述
      • flower_care:适合回答关于鲜花护理的问题”,适合处理与花卉护理相关的问题。
      • flower_decoration:适合回答关于鲜花装饰的问题”,适合处理与花卉装饰相关的问题。
    • 输入输出:为模型提供了一个格式化的框架,其中它将接收一个名为 {input} 的输入,并在此后的部分输出结果
  • 路由提示:根据路由模板,生成了具体传递给 LLM 的路由提示信息
    • 其中 input_variables 指定模板接收的输入变量名,这里只有 input
    • output_parser 是一个用于解析模型输出的对象,它有一个默认的目的地和一个指向下一输入的键
    • template 是实际的路由模板,用于给模型提供指示;这就是刚才详细解释的模板内容
    • template_format 指定模板的格式,这里是 f-string
    • validate_template 是一个布尔值,如果为 True,则会在使用模板前验证其有效性

概括路由链的逻辑,允许将用户的原始输入送入路由器,然后路由器会决定将该输入发送到哪个具体的模型提示,或者是否需要对输入进行修订以获得最佳的响应

默认链

准备一个默认链,如果路由链没有找到适合的链就以默认链进行处理

1
2
3
4
# 构建默认链
default_chain = ConversationChain(llm=Azure.chat_model,
output_key="text",
verbose=True)

MultiPromptChain

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

MultiPromptChain 是一个多路选择链,它使用一个路由链在多个提示之间进行选择

关键属性:

  • router_chain(类型 RouterChain):这是用于决定目标链和其输入的链;当给定某个输入时,由 router_chain 决定哪一个 destination_chain 应该被选中,以及具体输入是什么
  • destination_chains(类型 Mapping[str, LLMChain]):这是一个映射,将名称映射到可以将输入路由到的候选链。例如,你可能有多种处理文本输入的方法(或链),每种方法针对特定类型的问题;destination_chains 可以是这样一个字典: {'weather': weather_chain, 'news': news_chain};weather_chain 可能专门处理与天气相关的问题,而 news_chain 处理与新闻相关的问题
  • default_chain(类型 LLMChain):当 router_chain 无法将输入映射到 destination_chains 中的任何一个链时,LLMChain 将使用此默认链;这是一个备选方案,确保即使路由器不能决定正确的链也总有一个链可以处理输入

工作流程如下:

  1. 输入首先传递 router_chain
  2. router_chain 根据某些标准或逻辑决定应该使用哪一个 destination_chain
  3. 输入随后被路由到选定的 destination_chain,该链进行处理并返回结果
  4. 如果 router_chain 不能决定正确的 destination_chain,则输入会被传递给 default_chain

如此 MultiPromptChain 就提供了一个在多个处理链之间动态路由输入的机制,以得到最相关或最优的输出

1
2
3
4
5
multi_prompt_chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=chain_map,
default_chain=default_chain,
verbose=True)

执行

准备好了整个链条上的所有组件,概括一下:

  • 模板和目标 LLMChain:不同路径的提示和基础链
  • LLMRouterChain:进行路由选择的链
  • 默认链 ConversationChain(LLMChain 的实现,处理简单对话):如果没有对应路由,则使用默认链
  • MultiPromptChain:组合起上述三种类型的组件,成为调用的入口

那么针对创建出的 MultiPromptChain 进行调用

1
print(multi_prompt_chain.run("如何为向日葵浇水?"))

通过 verboss=True 可以观察到过程日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> Entering new MultiPromptChain chain...

> Entering new LLMRouterChain chain...

> Finished chain.
flower_care: {'input': '如何为向日葵浇水?'}

> Entering new LLMChain chain...
Prompt after formatting:
你是一个经验丰富的园丁,擅长解答关于养花育花的问题。
下面是需要你来回答的问题:
如何为向日葵浇水?

> Finished chain.

> Finished chain.
# ...
# 下面是回答,太长了就不放了

可以看到路由链输出了 flower_care: {'input': '如何为向日葵浇水?'},多路选择链根据路由返回使用了养花育花的模板

使用默认链测试一下

1
print(multi_prompt_chain.run("可乐鸡翅的做法"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> Entering new MultiPromptChain chain...

> Entering new LLMRouterChain chain...

> Finished chain.
None: {'input': '可乐鸡翅的做法'}

> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

Human: 可乐鸡翅的做法
AI:

> Finished chain.

> Finished chain.
# ...

可以看到路由链输出了 None: {'input': '可乐鸡翅的做法'},并且在后续处理的选择中使用了默认提供的 ConversationChain

参考

LangChain 实战课_LangChain_AI_大模型_语言模型_ChatGPT_prompt_接口_提示工程_模型_开发-极客时间 (geekbang.org)