五分鐘理透 LangChain 的 Chain
LangChain幾乎是LLM應(yīng)用開發(fā)的第一選擇,它的野心也比較大,它致力于將自己打造成LLM應(yīng)用開發(fā)的最大社區(qū)。而LangChain最核心的部分非 Chain 莫屬。
那Chain到底是個啥,概念比較模糊,像霧像雨又像風(fēng),這篇文章將帶你快速理透 LangChain 中的 Chain 概念。
1. Chain是核心
LangChain的Chain到底是什么?一句話總結(jié):Chain是指對 LangChain 多個組件的一系列調(diào)用。
再看看官網(wǎng)的解釋:Chain是指調(diào)用的序列 - 無論是調(diào)用 LLM、工具還是數(shù)據(jù)預(yù)處理步驟,主要支持的方法是使用 LCEL。
官網(wǎng)里還提到了LCEL,LCEL是LangChain 表達式語言,是一種更加高效簡介的鏈接 LangChain 組件的方式,也是官網(wǎng)推薦的方式。
從下圖官網(wǎng)的描述,也可以看到,Chain可以是從最簡單的“prompt + LLM”鏈 到 最復(fù)雜的鏈(運行了包含 100 多個步驟的鏈)。
2. 為什么需要Chain
我們所期待的LLM是能處理許多復(fù)雜任務(wù),而非簡單的一問一答,也不是簡單的處理單一任務(wù)。
所以,最終我期待的LLM處理任務(wù)的流程應(yīng)該是這樣,它中間的復(fù)雜過程對用戶來說是一個黑盒:
既然定位是完成復(fù)雜任務(wù),那自然就需要通過某個機制將多個單一任務(wù)串起來,形成一個大的鏈條,多個步驟共同完成某個復(fù)雜任務(wù)。
Chain可以將多個步驟連接到一起,最終完成各種復(fù)雜繁瑣的任務(wù)。這就是Chain存在的必要性了。我很喜歡LangChain的Logo,很形象地表達了這一思想。
Chain需要對多個組件一系列的調(diào)用或者一系列的串聯(lián),這樣才能完成復(fù)雜任務(wù)。當(dāng)然,我們也可以把 Chain 看作是流水線。通過使用 Chain,你可以將各個步驟定義為獨立的模塊,然后按順序串聯(lián)起來。這樣不僅大大簡化了代碼邏輯,也使得整個流程更加直觀和易于管理。
而LCEL的存在,也只是為了讓構(gòu)建鏈的過程更簡單,讓鏈的表達力更清晰更簡單。
接下來,我將通過一個示例展示沒有 Chain 和有Chain的2種實現(xiàn)方式,以便更清晰地理解 Chain 的價值。
3. 如果沒有Chain
這里舉個例子,比如:我們給LLM輸入一段項目描述,讓LLM給這個項目起一個名稱和Slogan。
如果不使用Chain的話,我們可以這樣實現(xiàn)。
# 本次需求:我們給LLM輸入一段項目描述,讓LLM給這個項目起一個名稱和Slogan
# 以下是實現(xiàn):
from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
proj_desc = """
我們本次的項目是去森林里探險救援,我們有一個10人小隊,
我們要到達一個叫做“蝴蝶谷”的目的地,去那里解救一位被困的科學(xué)家。
這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經(jīng)磨難,才能到達目的地。
我們的任務(wù)是要在5天內(nèi)到達目的地并且救出探險家,才算完成這次探險,否則任務(wù)失敗,我們將受到懲罰。
出發(fā)前我們要各自準(zhǔn)備好自己的裝備和干糧,加油!
"""
def name_slogan_by_desc(project_desc):
"""
根據(jù)項目描述,生成項目名稱和slogan
"""
str_parser = StrOutputParser()
promt_template_project_name = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,生成一個項目名稱,只需要返回項目名稱。<desc>{project_desc}</desc>"
promt_project_name = PromptTemplate.from_template(promt_template_project_name)
final_promt_project_name = promt_project_name.invoke({"project_desc": project_desc})
res_project_name = model.invoke(final_promt_project_name)
parsed_res_project_name = str_parser.invoke(res_project_name)
promt_template_slogan = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,和這個項目的名稱{project_name},給這個項目起一個slogan,slogan要求干脆簡潔積極向上,只返回slogan。<desc>{project_desc}</desc>"
promt_slogan = PromptTemplate.from_template(promt_template_slogan)
final_promt_slogan = promt_slogan.invoke(
{"project_desc": project_desc, "project_name": parsed_res_project_name}
)
response_slogan = model.invoke(final_promt_slogan)
parsed_response_slogan = str_parser.invoke(response_slogan)
final_result = {
"project_name": parsed_res_project_name,
"slogan": parsed_response_slogan,
}
return final_result
# 輸入項目描述,輸出項目名稱和slogan
result = name_slogan_by_desc(proj_desc)
print(result)
執(zhí)行結(jié)果如下:
{'project_name': '蝴蝶谷救援行動', 'slogan': '拯救科學(xué)家,共同合作,蝴蝶谷等你來!'}
可以看到,實現(xiàn)過程比較繁瑣,變量和代碼也多,不夠直觀,很容易出錯。這還只是簡單場景,如果碰到復(fù)雜場景就更麻煩了。
4. 因為有了Chain
接下來,我們使用 LangChain 的 Chain 功能,來實現(xiàn)相同的功能。代碼如下:
# 本次需求:我們給LLM輸入一段項目描述,讓LLM給這個項目起一個名稱和Slogan
# 以下是實現(xiàn):
from operator import itemgetter
from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chains import LLMChain, SequentialChain
proj_desc = """
我們本次的項目是去森林里探險救援,我們有一個10人小隊,
我們要到達一個叫做“蝴蝶谷”的目的地,去那里解救一位被困的科學(xué)家。
這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經(jīng)磨難,才能到達目的地。
我們的任務(wù)是要在5天內(nèi)到達目的地并且救出探險家,才算完成這次探險,否則任務(wù)失敗,我們將受到懲罰。
出發(fā)前我們要各自準(zhǔn)備好自己的裝備和干糧,加油!
"""
def name_slogan_by_desc(project_desc):
"""
根據(jù)項目描述,生成項目名稱和slogan
"""
# 第1條鏈
promt_template_project_name = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,生成一個項目名稱,只需要返回項目名稱。<desc>{project_desc}</desc>"
chain_one = LLMChain(
llm=model,
prompt=PromptTemplate.from_template(promt_template_project_name),
output_parser=StrOutputParser(),
output_key="project_name",
)
# 第2條鏈
promt_template_slogan = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,和這個項目的名稱{project_name},給這個項目起一個slogan,slogan要求干脆簡潔積極向上,只返回slogan。<desc>{project_desc}</desc>"
chain_two = LLMChain(
llm=model,
prompt=PromptTemplate.from_template(promt_template_slogan),
output_parser=StrOutputParser(),
output_key="slogan",
)
# 串聯(lián)兩條鏈
sequential_chain = SequentialChain(
chains=[chain_one, chain_two],
input_variables=["project_desc"],
output_variables=["project_name", "slogan"],
)
final_res = sequential_chain(project_desc)
final_result = {
"project_name": final_res["project_name"],
"slogan": final_res["slogan"],
}
return final_result
# 輸入項目描述,輸出項目名稱和slogan
result = name_slogan_by_desc(proj_desc)
print(result)
執(zhí)行結(jié)果如下:
{'project_name': '蝴蝶谷救援行動', 'slogan': '團結(jié)合作,共赴蝴蝶谷'}
可以看到代碼更簡潔,也很直觀,當(dāng)然,也可以使用LCEL讓整個鏈條更加簡潔清晰。
5. LCEL表達式
LCEL方式的代碼如下:
# 本次需求:我們給LLM輸入一段項目描述,讓LLM給這個項目起一個名稱和Slogan
# 以下是實現(xiàn):
from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
proj_desc = """
我們本次的項目是去森林里探險救援,我們有一個10人小隊,
我們要到達一個叫做“蝴蝶谷”的目的地,去那里解救一位被困的科學(xué)家。
這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經(jīng)磨難,才能到達目的地。
我們的任務(wù)是要在5天內(nèi)到達目的地并且救出探險家,才算完成這次探險,否則任務(wù)失敗,我們將受到懲罰。
出發(fā)前我們要各自準(zhǔn)備好自己的裝備和干糧,加油!
"""
def name_slogan_by_desc(project_desc):
"""
根據(jù)項目描述,生成項目名稱和slogan
"""
# 第1條鏈
promt_template_project_name = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,生成一個項目名稱,只需要返回項目名稱。<desc>{project_desc}</desc>"
chain_one = (
PromptTemplate.from_template(promt_template_project_name)
| model
| {"project_name": StrOutputParser(), "project_desc": lambda x: project_desc}
)
# 第2條鏈
promt_template_slogan = "請你根據(jù)<desc>標(biāo)簽里的關(guān)于某個項目的描述,和這個項目的名稱{project_name},給這個項目起一個slogan,slogan要求干脆簡潔積極向上,只返回slogan。<desc>{project_desc}</desc>"
chain_two = (
PromptTemplate.from_template(promt_template_slogan)
| model
| {"slogan": StrOutputParser(), "project_info": lambda x: chain_one}
)
# 串聯(lián)兩條鏈
final_chain = chain_one | chain_two
final_res = final_chain.invoke({"project_desc": project_desc})
final_result = {
"project_name": final_res["project_info"]["project_name"],
"slogan": final_res["slogan"],
}
return final_result
# 輸入項目描述,輸出項目名稱和slogan
result = name_slogan_by_desc(proj_desc)
print(result)
普通方式和LCEL方式的核心代碼對比:
普通方式:
LCEL方式: