前言
本文是 Harrison Chase (LangChain 创建者)和吴恩达(Andrew Ng)的视频课程《LangChain for LLM Application Development》(使用 LangChain 进行大模型应用开发)的学习笔记。由于原课程为全英文视频课程,国内访问较慢,同时我整理和替换了部分内容以便于国内学习。阅读本文可快速学习课程内容。
课程介绍
本课程介绍了强大且易于扩展的 LangChain 框架,LangChain 框架是一款用于开发大语言模型(LLM)应用的开源框架,其使用提示词、记忆、链、代理等简化了大语言模型应用的开发工作。由于 LangChain 仍处于快速发展期,部分 API 还不稳定,课程中的部分代码已过时,我使用了目前最新的 v0.2 版本进行讲解,所有代码均可在 v0.2 版本下执行。另外,课程使用的 OpenAI 在国内难以访问,我替换为国内的 Kimi 模型,对于学习没有影响。参考这篇文章来获取 Kimi 的 API 令牌。
- 第一部分
- 第二部分
课程链接
第二部分
链(Chain)
链可以将 LLM 模型及多种类型的组件连起来,以完成更加复杂的任务。
LLMChain
LLMChain 是最简单且强大的链,请看下面的例子。
from langchain.chains import LLMChain
# 还是一样创建 LLM
llm = ChatOpenAI(temperature=0.3, model=llm_model, base_url=base_url, api_key=api_key)
# 创建提示模版
prompt = ChatPromptTemplate.from_template(
"请说出一个中国公司名称,这家公司生产产品 {product} ?"
)
# 构建链
chain = LLMChain(llm=llm, prompt=prompt)
# 输入参数
product = "智能手机"
# 执行链
out = chain.run(product)
print(out)
执行后可获得类似如下的输出。
华为(Huawei)是一家中国公司,生产智能手机。
LLM 每次的输出可能并不相同。
这个例子非常简单,只是把 LLM 和提示词组合起来。我们可以通过提供不同的参数,来获得不同的结果。
SimpleSequentialChain
接下来我们使用 SimpleSequentialChain,它可以按序列顺序执行一系列的链。上一个链的输出是下一个链的输入。SimpleSequentialChain 支持的子链只能有一个输入参数和一个输出参数。
from langchain.chains import SimpleSequentialChain
# 创建 LLM
llm = ChatOpenAI(temperature=0.3, model=llm_model, base_url=base_url, api_key=api_key)
# 提示词模板 1
first_prompt = ChatPromptTemplate.from_template(
"请说出一个中国公司名称,这家公司生产产品 {product} ?"
)
# 链 1
chain_one = LLMChain(llm=llm, prompt=first_prompt)
# 提示词模板 2
second_prompt = ChatPromptTemplate.from_template(
"请写一段 20 字以内的文字介绍公司:{company_name}"
)
# 链 2
chain_two = LLMChain(llm=llm, prompt=second_prompt)
# 构建简单序列链
overall_simple_chain = SimpleSequentialChain(chains=[chain_one, chain_two],
verbose=True
)
# 输入参数
product = "智能手机"
# 执行链
out = overall_simple_chain.run(product)
print(out)
执行后我们会获得类似下面的结果。
上面的序列链包含了两个子链,第一个子链输入参数是产品,输出结果是公司名称,这个输出结果作为了第二个子链的输入。第二个子链输入参数是公司名称,输出公司的介绍。通过 SimpleSequentialChain 就可以将两次执行串联起来了。
SequentialChain
上面的 SimpleSequentialChain 只支持一个输入和一个输出的情况,但实际情况可能需要有多个输入和多个输出,这时候我们就可以使用 SequentialChain。
看下面的例子。
from langchain.chains import SequentialChain
# 创建 LLM
llm = ChatOpenAI(temperature=0.3, model=llm_model, base_url=base_url, api_key=api_key)
# 提示词模板 1
first_prompt = ChatPromptTemplate.from_template(
"请生成一条关于产品 {product} 的评价,30 字以内"
)
# 链 1: input= product output= review
chain_one = LLMChain(llm=llm, prompt=first_prompt,
output_key="review"
)
second_prompt = ChatPromptTemplate.from_template(
"请根据下面产品的评价写一段 30 字以内的回复:"
"\n\n产品:{product}\n\n评价: {review}"
)
# 链 2: input= product, review output= response
chain_two = LLMChain(llm=llm, prompt=second_prompt,
output_key="response"
)
# 合成链: input= product output= product, review, response
overall_chain = SequentialChain(
chains=[chain_one, chain_two],
input_variables=["product"],
output_variables=["product", "review", "response"],
verbose=True
)
# 输入参数
product = "智能手机"
# 执行链
out = overall_chain(product)
print(out)
注意不要搞混输入和输出的变量名。
执行上述代码可得到类似下面的输出。
> Entering new SequentialChain chain...
> Finished chain.
{'product': '智能手机', 'review': '设计时尚,性能卓越,拍照清晰,续航给力,用户体验极佳。', 'response': '感谢您的好评!我们很高兴您喜欢我们的设计、性能和用户体验。'}
上面构建了一个包含两个子链的序列链,其中第二个子链有多个输入参数和输出结果,最终的输出结果也是合并后的综合结果。
我们可以通过链来构建更复杂的应用场景,但目前都是一个接一个执行的顺序场景,接下来我们可以看看实际中更复杂的分支场景。
Router Chain
路由分支好比是一个岔路口,我们有好几条链可以选择。根据不同的条件或场景,我们可以选择不同的分支链继续执行。这样我们就可以构建非常复杂的应用场景了。
看下面的例子。
# 首先定义不同的提示词模板
physics_template = """你是一位著名的物理学教授。 \
你非常擅长以简洁且易于理解的方式解答物理学问题。\
如果你不知道问题的答案你就回答不知道。\
下面是问题:
{input}"""
math_template = """你是一位著名的数学家。 \
你非常擅长解答数学问题。 \
你擅长将复杂的数学问题分解为多个小问题,最后合并解决。 \
下面是问题:
{input}"""
history_template = """你是一位著名的历史学家。\
你对于人类的历史事件有非常丰富的知识和理解。\
你非常擅长思考、总结、讨论过去的事件,并善于使用历史事件作为依据。\
下面是问题:
{input}"""
computerscience_template = """ 你是一位成功的计算机专家。\
你对创造力、协作、前瞻性思维、自信、解决问题的能力充满热情。\
你擅长回答编码问题,你还知道如何\
选择在时间复杂性和空间复杂性之间取得良好平衡的解决方案。\
下面是问题:
{input}"""
prompt_infos = [
{
"name": "物理",
"description": "擅长回答物理学问题",
"prompt_template": physics_template
},
{
"name": "数学",
"description": "擅长回答数学问题",
"prompt_template": math_template
},
{
"name": "历史",
"description": "擅长回答历史问题",
"prompt_template": history_template
},
{
"name": "计算机科学",
"description": "擅长回答计算机科学问题",
"prompt_template": computerscience_template
}
]
# 引入依赖
from langchain.chains.router import MultiPromptChain
from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser
from langchain.prompts import PromptTemplate
# 构建 LLM
llm = ChatOpenAI(temperature=0.3, model=llm_model, base_url=base_url, api_key=api_key)
# 构建链
destination_chains = {}
for p_info in prompt_infos:
name = p_info["name"]
prompt_template = p_info["prompt_template"]
prompt = ChatPromptTemplate.from_template(template=prompt_template)
chain = LLMChain(llm=llm, prompt=prompt)
destination_chains[name] = chain
# 构建候选提示词
destinations = [f"{p['name']}: {p['description']}" for p in prompt_infos]
destinations_str = "\n".join(destinations)
# 默认提示词
default_prompt = ChatPromptTemplate.from_template("{input}")
default_chain = LLMChain(llm=llm, prompt=default_prompt)
# 构建路由提示词模板
MULTI_PROMPT_ROUTER_TEMPLATE = """请为下面的原始输入选择最适合的候选提示词。 \
你将获得候选提示词的提示名称和适合领域的描述。\
你可以对原始输入进行修改来匹配适合的领域。
<< 格式 >>
请返回包含下面 JSON 对象的 Markdown 格式:
```json
{{{{
"destination": string
"next_inputs": string
}}}}
```其中:"destination" 必须是下面候选提示词的名称中的一个或者是 "DEFAULT" 如果没有匹配任何一个候选提示词。
其中:"next_inputs" 可以是原始输入或者是修改后的版本。
<< 候选提示词 >>
{destinations}
<< 原始输入 >>
{{input}}
<< 输出 (记得包含 ```json)>>"""
# 路由提示词
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
destinations=destinations_str
)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
# 构建路由链
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
# 构建链
chain = MultiPromptChain(router_chain=router_chain,
destination_chains=destination_chains,
default_chain=default_chain, verbose=True
)
上面的代码构建了一个路由链,根据提问的类型,选择最适合的目标链来回答。我们首先通过路由链获得要使用的目标链,每个目标链都是普通的 LLM 链。另外我们也需要一个默认的链,用于处理覆盖不到的情况。
接下来我们问一个物理问题。
# 提问
out = chain.run("什么是丁达尔效应?")
print(out)
可能得到如下回答。
> Entering new MultiPromptChain chain...
物理: {'input': '什么是丁达尔效应?'}
> Finished chain.
丁达尔效应(Tyndall effect)是一种物理现象,它描述了当光线通过一个含有悬浮微粒的透明介质(如胶体溶液)时,这些微粒会散射光线,使得光线的路径变得可见。这种现象通常在胶体中观察到,因为胶体粒子的尺寸(通常在1纳米到1微米之间)与可见光的波长(大约400纳米到700纳米)相当,因此能够有效地散射光线。
当光线通过这样的介质时,如果观察角度合适,可以看到一条明亮的光路,这是因为光线被介质中的微粒散射到了观察者的眼睛。这种现象最早由英国物理学家约翰·丁达尔(John Tyndall)在1869年描述,因此得名。
丁达尔效应在日常生活中也很常见,例如在雾中或在空气中悬浮的灰尘中,当阳光或其他光源照射时,可以看到光线的路径。在实验室中,丁达尔效应可以用来区分溶液和胶体,因为溶液中的粒子太小,无法有效散射光线,而胶体中的粒子则可以。
总结来说,丁达尔效应是一种光线散射现象,它在含有适当尺寸悬浮粒子的透明介质中观察到,使得光线的路径变得可见。
如果我们提问一个历史问题。
out = chain.run("什么是九一八事变?")
print(out)
则可能得到这样的回答。
> Entering new MultiPromptChain chain...
历史: {'input': '什么是九一八事变?'}
> Finished chain.
九一八事变,又称柳条湖事件,是1931年9月18日在中国东北地区发生的一起重大历史事件。这一事件标志着日本帝国主义对中国的全面侵略的开始,也是中国抗日战争的起点。
1. 背景:在20世纪初,日本帝国主义对中国的侵略野心日益膨胀。日本在1905年日俄战争后,通过《朴茨茅斯条约》获得了在中国东北的特权,包括铁路、矿山等资源的控制权。此后,日本在东北的势力不断扩张,与当地民众的矛盾日益加剧。
2. 事件经过:1931年9月18日晚,日本关东军在沈阳附近的柳条湖铁路附近制造了一起爆炸事件,然后以此为借口,声称中国军队破坏了铁路,随即发动了对沈阳的进攻。由于当时的中国政府采取了不抵抗政策,日本军队迅速占领了沈阳,并在随后的几个月内占领了整个东北三省。
3. 影响:九一八事变对中国和世界历史产生了深远的影响。首先,它标志着日本对中国的全面侵略的开始,导致了中国东北三省的沦陷,数百万中国人民遭受了巨大的苦难。其次,这一事件也激发了中国人民的民族意识和抗日情绪,为后来的全面抗日战争奠定了基础。最后,九一八事变也是第二次世界大战亚洲战场的序幕,对世界历史产生了重要影响。
4. 历史评价:九一八事变是日本帝国主义侵略中国的重要标志,也是中国人民反抗外来侵略、争取民族独立和解放的重要历史事件。这一事件提醒我们,要警惕外来侵略,维护国家主权和领土完整,同时也要珍惜和平,反对战争。
接着我们再问一个生物问题,会找不到合适的目标链,就会使用默认链来处理。
out = chain.run("为什么细胞中包含 DNA?")
print(out)
> Entering new MultiPromptChain chain...
None: {'input': '为什么细胞中包含 DNA?'}
> Finished chain.
细胞中包含DNA的原因是因为DNA是生物体内遗传信息的主要载体。DNA(脱氧核糖核酸)是一种长链状的生物大分子,由四种核苷酸(腺苷、鸟苷、胞嘧啶和胸腺嘧啶)组成。这些核苷酸按照特定的顺序排列,形成了遗传密码。以下是详细解释为什么细胞中包含DNA的几个步骤:
1. 遗传信息的传递:DNA的主要功能是存储和传递遗传信息。生物体的遗传特征,如形态、生理功能和行为等,都是由DNA中的遗传信息决定的。通过DNA的复制和传递,生物体可以将遗传信息从一代传递到下一代。
2. 蛋白质合成:DNA中的遗传信息通过转录和翻译过程,指导细胞合成特定的蛋白质。蛋白质是生物体内各种生物化学反应的主要参与者,对于维持生物体的正常生理功能至关重要。
3. 细胞分裂和生长:在细胞分裂过程中,DNA需要复制自身,以确保新产生的细胞具有与母细胞相同的遗传信息。这样,生物体才能正常生长和发育。
4. 基因表达调控:DNA中的遗传信息不仅决定了蛋白质的合成,还参与调控基因的表达。基因表达调控对于生物体对环境变化的适应和发育过程中的细胞分化至关重要。
5. 遗传变异和进化:DNA中的遗传信息在复制过程中可能出现错误,导致遗传变异。这些变异可能对生物体产生有利或不利的影响。有利的变异有助于生物体适应环境,从而在自然选择过程中被保留下来,推动生物的进化。
综上所述,细胞中包含DNA是因为DNA是生物体内遗传信息的主要载体,对于生物体的生长、发育、适应环境和进化具有重要作用。
大家可以尝试不同领域的问题,看路由链的选择是否正确。
链不仅可以将多个组件组合起来,也能支持复杂的逻辑场景,是 LangChain 最基本和强大的能力。
(未完待续)