langChain 基础 04 - 模型链

本文介绍使用 langchain 构建模型链的过程

前面在使用 llm 时,通常需要先构建 prompt,然后再传递到 llm,这还是一个 llm 的情况下,假设使用多个 llm,处理流程就很复杂,langchian 提供 “模型链 (Chain)” 组件,将 prompt、llm 链接起来,完成更复杂的 llm 应用

1
2
3
from langchain import chains
atts=list(filter(lambda att:att.lower().find('chain')>=0,chains.__all__))
print(atts)
1
['APIChain', 'OpenAPIEndpointChain', 'AnalyzeDocumentChain', 'MapReduceDocumentsChain', 'MapRerankDocumentsChain', 'ReduceDocumentsChain', 'RefineDocumentsChain', 'StuffDocumentsChain', 'ConstitutionalChain', 'ConversationChain', 'ChatVectorDBChain', 'ConversationalRetrievalChain', 'FlareChain', 'ArangoGraphQAChain', 'GraphQAChain', 'GraphCypherQAChain', 'FalkorDBQAChain', 'HugeGraphQAChain', 'KuzuQAChain', 'NebulaGraphQAChain', 'NeptuneOpenCypherQAChain', 'NeptuneSparqlQAChain', 'OntotextGraphDBQAChain', 'GraphSparqlQAChain', 'LLMChain', 'LLMCheckerChain', 'LLMMathChain', 'LLMRequestsChain', 'LLMSummarizationCheckerChain', 'load_chain', 'MapReduceChain', 'OpenAIModerationChain', 'NatBotChain', 'create_citation_fuzzy_match_chain', 'create_extraction_chain', 'create_extraction_chain_pydantic', 'create_qa_with_sources_chain', 'create_qa_with_structure_chain', 'create_tagging_chain', 'create_tagging_chain_pydantic', 'QAGenerationChain', 'QAWithSourcesChain', 'RetrievalQAWithSourcesChain', 'VectorDBQAWithSourcesChain', 'create_retrieval_chain', 'LLMRouterChain', 'MultiPromptChain', 'MultiRetrievalQAChain', 'MultiRouteChain', 'RouterChain', 'SequentialChain', 'SimpleSequentialChain', 'create_sql_query_chain', 'load_summarize_chain', 'TransformChain']

Langchian 提供较多 chains, 选择部分类构建类图如下,可以看出基类是 Chain,下文以 LLMChain、SequentialChain、LLMRouterChain 说明路由链的作用

classDiagram
    Chain <|-- LLMChain
    BaseRetrievalQA <|-- RetrievalQA  
    Chain <|-- BaseRetrievalQA
    Chain <|-- RouterChain
    RouterChain <|-- LLMRouterChain
    Chain <|-- SequentialChain

LLMChain

1
2
3
4
5
6
7
8
9
10
11
from langchain.chains import LLMChain
from langchain.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
# 初始化Ollama LLM,注意需要后台开启ollama服务
model_name = "qwen2.5:latest"
llm = ChatOllama(model=model_name,base_url='http://localhost:11434')
prompt = ChatPromptTemplate.from_template("描述制造{product}的一个公司的最佳名称是什么?")
chain = LLMChain(llm=llm, prompt=prompt)
product = "大号床单套装"
response=chain.invoke(product)
print(response)
1
{'product': '大号床单套装', 'text': '为一家专注于制造大号床单套装的公司命名时,可以从以下几个方面考虑:\n\n1. **强调产品特点**:可以使用一些能够反映产品尺寸、品质或用途的名字。例如,“巨宅家居”、“宽舒寝享”等。\n\n2. **品牌定位和文化**:如果公司的理念是提供奢华舒适的睡眠体验,那么可以选择像“贵族寝居”这样的名称;若注重环保与可持续发展,则可以考虑“绿织家”。\n\n3. **易于记忆且具象化**:选择一个简单、朗朗上口的名字有助于品牌传播。比如,“床海拾光”、“大美卧间”等。\n\n4. **地域性元素**:如果公司有特定的市场定位或希望突出地方特色,可以考虑使用相关词汇。如“华夏寝尚”(如果目标市场主要在中国)。\n\n5. **创意与情感连接**:创造一个独特的名称,让人联想到品质、舒适或是梦想中的居住空间。\n\n基于以上几点,这里提供几个建议的名字作为参考:\n- 宽舒良寐\n- 大床盛宴\n- 巨宅寝居\n- 绿织家园\n- 床海拾光\n\n请注意,最终选择的公司名称还需确保不侵犯他人商标权益,并通过相关注册流程。'}

顺序链 (SequentialChains)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain.chains import SimpleSequentialChain
# 提示模板 1 :这个提示将接受产品并返回最佳名称来描述该公司
first_prompt = ChatPromptTemplate.from_template(
"描述制造{product}的一个公司的最好的名称是什么"
)
chain_one = LLMChain(llm=llm, prompt=first_prompt)
# 提示模板 2 :接受公司名称,然后输出该公司的长为20个单词的描述
second_prompt = ChatPromptTemplate.from_template(
"写一个20字的描述对于下面这个\
公司:{company_name}的"
)
chain_two = LLMChain(llm=llm, prompt=second_prompt)
simple_sequential_chain = SimpleSequentialChain(chains=[chain_one, chain_two],verbose=True)
product = "大号床单套装"
response=simple_sequential_chain.invoke(product)
print(response)
1
2
3
4
5
6
7
8
9
10
11
> Entering new SimpleSequentialChain chain...
为一家专门制造大号床单套装的公司起名时,可以考虑其产品特点、品牌定位以及目标客户群体。以下是一些建议:
1. **巨匠睡语**:结合“巨大”与“睡眠”的概念,传达出产品的特色和使用体验。
2. **浩瀚寝居**:体现床单的大尺寸和宽广舒适感,适合追求高品质生活的消费者。
3. **云端之梦**:营造一种高雅、舒适的氛围,适合注重生活品质的客户群体。
4. **大宅奢享**:直接点明产品为高端市场设计,强调奢华与享受。
5. **宏博寝居**:名字简洁有力,易于记忆,传达出宏大和宽广的空间感。
选择公司名称时,还需考虑品牌命名的相关法律法规、商标注册情况以及是否有负面含义等因素。在确定最终名称之前,建议进行相关的法律咨询和市场调研。
为制造大号床单套装的公司起名时,可考虑“浩瀚寝居”,展现宽广舒适感与高品质生活追求。
> Finished chain.
{'input': '大号床单套装', 'output': '为制造大号床单套装的公司起名时,可考虑“浩瀚寝居”,展现宽广舒适感与高品质生活追求。'}

当只有一个输入和一个输出时,简单顺序链(SimpleSequentialChain)即可实现。当有多个输入或多个输出时,我们则需要使用顺序链(SequentialChain)来实现

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
33
34
35
36
37
38
39
40
41
42
43
44
45
import pandas as pd
from langchain.chains import SequentialChain
from langchain.prompts import ChatPromptTemplate #导入聊天提示模板
from langchain.chains import LLMChain #导入LLM链。
#子链1
# prompt模板 1: 翻译成英语(把下面的review翻译成英语)
first_prompt = ChatPromptTemplate.from_template(
"把下面的评论review翻译成英文:"
"\n\n{Review}"
)
# chain 1: 输入:Review 输出:英文的 Review
chain_one = LLMChain(llm=llm, prompt=first_prompt, output_key="English_Review")
#子链2
# prompt模板 2: 用一句话总结下面的 review
second_prompt = ChatPromptTemplate.from_template(
"请你用一句话来总结下面的评论review:"
"\n\n{English_Review}"
)
# chain 2: 输入:英文的Review 输出:总结
chain_two = LLMChain(llm=llm, prompt=second_prompt, output_key="summary")
#子链3
# prompt模板 3: 下面review使用的什么语言
third_prompt = ChatPromptTemplate.from_template(
"下面的评论review使用的什么语言:\n\n{Review}"
)
# chain 3: 输入:Review 输出:语言
chain_three = LLMChain(llm=llm, prompt=third_prompt, output_key="language")
#子链4
# prompt模板 4: 使用特定的语言对下面的总结写一个后续回复
fourth_prompt = ChatPromptTemplate.from_template(
"使用特定的语言对下面的总结写一个后续回复:"
"\n\n总结: {summary}\n\n语言: {language}"
)
# chain 4: 输入: 总结, 语言 输出: 后续回复
chain_four = LLMChain(llm=llm, prompt=fourth_prompt, output_key="followup_message")
#输入:review
#输出:英文review,总结,后续回复
sequential_chain = SequentialChain(
chains=[chain_one, chain_two, chain_three, chain_four],
input_variables=["Review"],
output_variables=["English_Review","summary", "language","followup_message"],
verbose=True
)
response=sequential_chain.invoke({"Review":"我觉得味道很差。泡沫不够持久,感觉很奇怪。我在商店里买同样的产品味道要好很多……是陈货还是假货?!"})
print(response)
1
2
3
> Entering new SequentialChain chain...
> Finished chain.
{'Review': '我觉得味道很差。泡沫不够持久,感觉很奇怪。我在商店里买同样的产品味道要好很多……是陈货还是假货?!', 'English_Review': 'I think the taste is very poor. The foam does not last long, and it feels strange. The same product tastes much better when I buy it from the store... Is it stale or fake?!', 'summary': '这款产品的味道很糟糕,泡沫也不持久,感觉怪怪的,同样的产品在店里却好吃很多,这是变质了吗?还是假货?', 'language': '这段评论使用的是中文。', 'followup_message': '这段评论表达了一些消费者对于某款产品的不满和疑虑。针对这种情况,您可以这样回复:\n\n您好!感谢您对我们产品的反馈。我们非常重视您的意见,并希望能够帮助您解决遇到的问题。关于您提到的味道和泡沫情况,可能是由于保存条件或开封时间过长导致的口感变化。为了确保产品的新鲜度,请尽量在开封后尽快食用完毕。如果您购买的产品确实存在质量问题或者与店内体验不符的情况,欢迎您随时联系我们客服部门,我们会提供相应的解决方案,如退换货服务等。\n\n希望上述信息能够帮助到您,如果有任何疑问或需要进一步的帮助,请随时告知我们。期待您的回复!'}

路由链

以上例子的所有链都接受输入,假设要实现根据用户输入选择执行的链,应该如何做呢?
通过定义路由器实现选择链路,路由器通过两个组件实现:

  • 路由链(Router Chain):路由器链本身,负责选择要调用的下一个链,本身也是 LLM,通过分析输入与 destination_chains 各链路描述的差异,确定选择的链路
  • destination_chains:路由器链可以路由到的链
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
33
34
35
36
37
from langchain.chains.router import MultiPromptChain  #导入多提示链
from langchain.chains.router.llm_router import LLMRouterChain,RouterOutputParser
from langchain.prompts import PromptTemplate
# 中文
#第一个提示适合回答物理问题
physics_template = """你是一个非常聪明的物理专家。 \
你擅长用一种简洁并且易于理解的方式去回答问题。\
当你不知道问题的答案时,你承认\
你不知道.
这是一个问题:
{input}"""
#第二个提示适合回答数学问题
math_template = """你是一个非常优秀的数学家。 \
你擅长回答数学问题。 \
你之所以如此优秀, \
是因为你能够将棘手的问题分解为组成部分,\
回答组成部分,然后将它们组合在一起,回答更广泛的问题。
这是一个问题:
{input}"""
#第三个适合回答历史问题
history_template = """你是以为非常优秀的历史学家。 \
你对一系列历史时期的人物、事件和背景有着极好的学识和理解\
你有能力思考、反思、辩证、讨论和评估过去。\
你尊重历史证据,并有能力利用它来支持你的解释和判断。
这是一个问题:
{input}"""
#第四个适合回答计算机问题
computerscience_template = """ 你是一个成功的计算机科学专家。\
你有创造力、协作精神、\
前瞻性思维、自信、解决问题的能力、\
对理论和算法的理解以及出色的沟通技巧。\
你非常擅长回答编程问题。\
你之所以如此优秀,是因为你知道 \
如何通过以机器可以轻松解释的命令式步骤描述解决方案来解决问题,\
并且你知道如何选择在时间复杂性和空间复杂性之间取得良好平衡的解决方案。
这还是一个输入:
{input}"""

对提示模板进行命名和描述,目的是为每个链路增加描述信息,后续 LLM 根据这些信息和用户输入比较,选择要执行的链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
prompt_infos = [
{
"名字": "物理学",
"描述": "擅长回答关于物理学的问题",
"提示模板": physics_template
},
{
"名字": "数学",
"描述": "擅长回答数学问题",
"提示模板": math_template
},
{
"名字": "历史",
"描述": "擅长回答历史问题",
"提示模板": history_template
},
{
"名字": "计算机科学",
"描述": "擅长回答计算机科学问题",
"提示模板": computerscience_template
}
]

基于提示模版信息创建相应目标链,目标链是由路由链调用的链,每个目标链都是一个语言模型链

1
2
3
4
5
6
7
8
9
destination_chains = {}
for p_info in prompt_infos:
name = p_info["名字"]
prompt_template = p_info["提示模板"]
prompt = ChatPromptTemplate.from_template(template=prompt_template)
chain = LLMChain(llm=llm, prompt=prompt)
destination_chains[name] = chain
destinations = [f"{p['名字']}: {p['描述']}" for p in prompt_infos]
destinations_str = "\n".join(destinations)

定义默认目标链路,除了目标链之外,我们还需要一个默认目标链。这是一个当路由器无法决定使用哪个子链时调用的链。在上面的示例中,当输入问题与物理、数学、历史或计算机科学无关时,可能会调用它。

1
2
default_prompt = ChatPromptTemplate.from_template("{input}")
default_chain = LLMChain(llm=llm, prompt=default_prompt)

通过多提示模板路构建模板路由链路,实际上它是 LLM 根据用户输入,在可选输出内选择一个输出(或者默认输出)

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
33
34
35
36
37
38
39
40
41
42
# 多提示路由模板
MULTI_PROMPT_ROUTER_TEMPLATE = """给语言模型一个原始文本输入,\
让其选择最适合输入的模型提示。\
系统将为您提供可用提示的名称以及最适合改提示的描述。\
如果你认为修改原始输入最终会导致语言模型做出更好的响应,\
你也可以修改原始输入。
<< 格式 >>
返回一个带有JSON对象的markdown代码片段,该JSON对象的格式如下:
\```json
{{{{
"destination": 字符串 \ 使用的提示名字或者使用 "DEFAULT"
"next_inputs": 字符串 \ 原始输入的改进版本
}}}}
记住:“destination”必须是下面指定的候选提示名称之一,\
或者如果输入不太适合任何候选提示,\
则可以是 “DEFAULT” 。
记住:如果您认为不需要任何修改,\
则 “next_inputs” 可以只是原始输入。
<< 候选提示 >>
{destinations}
<< 输入 >>
{{input}}
<< 输出 (记得要包含 \```json)>>
样例:
<< 输入 >>
"什么是黑体辐射?"
<< 输出 >>
\```json
{{{{
"destination": 字符串 \ 使用的提示名字或者使用 "DEFAULT"
"next_inputs": 字符串 \ 原始输入的改进版本
}}}}
"""
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)

构建整体链路

1
2
3
4
5
6
7
8
#多提示链
chain = MultiPromptChain(router_chain=router_chain, #l路由链路
destination_chains=destination_chains, #目标链路
default_chain=default_chain, #默认链路
verbose=True
)
response=chain.run("什么是黑体辐射?")
response
1
2
3
4
> Entering new MultiPromptChain chain...
物理学: {'input': '什么是黑体辐射?'}
> Finished chain.
'黑体辐射是指在特定温度下,理想化的完全吸收所有入射电磁波(包括可见光)而不反射任何光线的物体所发射的电磁辐射现象。简单来说,就是一种能够完美吸收热量并且按照其温度特征进行辐射发热的物体。\n\n当这种物体加热到足够高的温度时,它会发出连续谱的电磁辐射,从无线电波、红外线、可见光、紫外线一直到X射线等不同波长的光线都会被辐射出来。普朗克在研究黑体辐射问题时提出了量子化的概念,这是物理学中一个重要的转折点。\n\n实际生活中虽然不存在完美的黑体,但我们可以用某些材料或物体近似模拟其特性来研究这个问题。'
1
2
response=chain.run("2+2等于多少?")
response
1
2
3
4
> Entering new MultiPromptChain chain...
数学: {'input': '2+2等于多少?'}
> Finished chain.
'2+2等于4。这个问题是一个基本的算术运算,不需要分解成更小的部分来解答。直接相加即可得到答案。如果您有更多复杂的数学问题需要帮助,请随时告诉我!'