5分鐘理透LangChain的Chain

程序员半支烟發表於2024-06-16

LangChain幾乎是LLM應用開發的第一選擇,它的野心也比較大,它致力於將自己打造成LLM應用開發的最大社群。而LangChain最核心的部分非 Chain 莫屬。

Chain到底是個啥,概念比較模糊,像霧像雨又像風,這篇文章將帶你快速理透 LangChain 中的 Chain 概念。

1. Chain是核心

LangChain的Chain到底是什麼?一句話總結:Chain是指對 LangChain 多個元件的一系列呼叫。

再看看官網的解釋:Chain是指呼叫的序列 - 無論是呼叫 LLM、工具還是資料預處理步驟,主要支援的方法是使用 LCEL。

官網裡還提到了LCEL,LCEL是LangChain 表示式語言,是一種更加高效簡介的連結 LangChain 元件的方式,也是官網推薦的方式。

從下圖官網的描述,也可以看到,Chain可以是從最簡單的“prompt + LLM”鏈 到 最複雜的鏈(執行了包含 100 多個步驟的鏈)。

2. 為什麼需要Chain

我們所期待的LLM是能處理許多複雜任務,而非簡單的一問一答,也不是簡單的處理單一任務。

所以,最終我期待的LLM處理任務的流程應該是這樣,它中間的複雜過程對使用者來說是一個黑盒:

既然定位是完成複雜任務,那自然就需要透過某個機制將多個單一任務串起來,形成一個大的鏈條,多個步驟共同完成某個複雜任務。

***Chain可以將多個步驟連線到一起,最終完成各種複雜繁瑣的任務。***這就是Chain存在的必要性了。我很喜歡LangChain的Logo,很形象地表達了這一思想。

Chain需要對多個元件一系列的呼叫或者一系列的串聯,這樣才能完成複雜任務。當然,我們也可以把 Chain 看作是流水線。透過使用 Chain,你可以將各個步驟定義為獨立的模組,然後按順序串聯起來。這樣不僅大大簡化了程式碼邏輯,也使得整個流程更加直觀和易於管理。

而LCEL的存在,也只是為了讓構建鏈的過程更簡單,讓鏈的表達力更清晰更簡單。

接下來,我將透過一個示例展示沒有 Chain有Chain的2種實現方式,以便更清晰地理解 Chain 的價值。

3. 如果沒有Chain

這裡舉個例子,比如:我們給LLM輸入一段專案描述,讓LLM給這個專案起一個名稱Slogan

如果不使用Chain的話,我們可以這樣實現。

# 本次需求:我們給LLM輸入一段專案描述,讓LLM給這個專案起一個名稱和Slogan
# 以下是實現:

from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


proj_desc = """
    我們本次的專案是去森林裡探險救援,我們有一個10人小隊,
    我們要到達一個叫做“蝴蝶谷”的目的地,去那裡解救一位被困的科學家。
    這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經磨難,才能到達目的地。
    我們的任務是要在5天內到達目的地並且救出探險家,才算完成這次探險,否則任務失敗,我們將受到懲罰。
    出發前我們要各自準備好自己的裝備和乾糧,加油!
"""


def name_slogan_by_desc(project_desc):
    """
    根據專案描述,生成專案名稱和slogan
    """
    str_parser = StrOutputParser()

    promt_template_project_name = "請你根據<desc>標籤裡的關於某個專案的描述,生成一個專案名稱,只需要返回專案名稱。<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 = "請你根據<desc>標籤裡的關於某個專案的描述,和這個專案的名稱{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)

執行結果如下:

{'project_name': '蝴蝶谷救援行動', 'slogan': '拯救科學家,共同合作,蝴蝶谷等你來!'}

可以看到,實現過程比較繁瑣,變數和程式碼也多,不夠直觀,很容易出錯。這還只是簡單場景,如果碰到複雜場景就更麻煩了。

4. 因為有了Chain

接下來,我們使用 LangChain 的 Chain 功能,來實現相同的功能。程式碼如下:

# 本次需求:我們給LLM輸入一段專案描述,讓LLM給這個專案起一個名稱和Slogan
# 以下是實現:

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人小隊,
    我們要到達一個叫做“蝴蝶谷”的目的地,去那裡解救一位被困的科學家。
    這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經磨難,才能到達目的地。
    我們的任務是要在5天內到達目的地並且救出探險家,才算完成這次探險,否則任務失敗,我們將受到懲罰。
    出發前我們要各自準備好自己的裝備和乾糧,加油!
"""

def name_slogan_by_desc(project_desc):
    """
    根據專案描述,生成專案名稱和slogan
    """

    # 第1條鏈
    promt_template_project_name = "請你根據<desc>標籤裡的關於某個專案的描述,生成一個專案名稱,只需要返回專案名稱。<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 = "請你根據<desc>標籤裡的關於某個專案的描述,和這個專案的名稱{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",
    )

    # 串聯兩條鏈
    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)

執行結果如下:

{'project_name': '蝴蝶谷救援行動', 'slogan': '團結合作,共赴蝴蝶谷'}

可以看到程式碼更簡潔,也很直觀,當然,也可以使用LCEL讓整個鏈條更加簡潔清晰。

5. LCEL表示式

LCEL方式的程式碼如下:

# 本次需求:我們給LLM輸入一段專案描述,讓LLM給這個專案起一個名稱和Slogan
# 以下是實現:

from langchain.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

proj_desc = """
    我們本次的專案是去森林裡探險救援,我們有一個10人小隊,
    我們要到達一個叫做“蝴蝶谷”的目的地,去那裡解救一位被困的科學家。
    這期間我們可能會遇到許多危險,我們需要共同合作,互相幫助,歷經磨難,才能到達目的地。
    我們的任務是要在5天內到達目的地並且救出探險家,才算完成這次探險,否則任務失敗,我們將受到懲罰。
    出發前我們要各自準備好自己的裝備和乾糧,加油!
"""

def name_slogan_by_desc(project_desc):
    """
    根據專案描述,生成專案名稱和slogan
    """

    # 第1條鏈
    promt_template_project_name = "請你根據<desc>標籤裡的關於某個專案的描述,生成一個專案名稱,只需要返回專案名稱。<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 = "請你根據<desc>標籤裡的關於某個專案的描述,和這個專案的名稱{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}
    )

    # 串聯兩條鏈
    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方式

6. 總結

本文主要聊了 LangChain 中的 Chain 概念。Chain 是 LangChain 中的核心元件,我們對多個元件的一系列呼叫就是Chain。

使用Chain可以讓構建複雜的任務,更加清晰簡潔。

=====>>>>>> 關於我 <<<<<<=====

本篇完結!歡迎點贊 關注 收藏!!!

原文連結:https://mp.weixin.qq.com/s/IdaO8CeS1TKoQDCcjMqWsg

相關文章