LangChain 怎麼玩? LCEL (LangChain Expression Language) 篇,一定要認識的 LangChain 核心

Posted on  Feb 27, 2024  in  LangChain , Python 程式設計 - 高階  by  Amo Chen  ‐ 5 min read

LangChain 怎麼玩?入門教學篇 雖然有提到 LCEL(LangChain Expression Language), 不過並沒有深入理解到底什麼是 LCEL 。

LCEL 是 LangChain 的核心,如果要能夠設計出更複雜的 LangChain 應用,甚至是將 LangChain 應用轉為 API 對外服務,那麼 LCEL 是一定要認識/理解的。

本文環境

$ pip install langchain

本文需要 Ollama 指令與 Meta 公司提供的 llama2 模型(model),ollama 指令請至 Ollama 官方頁面下載安裝,安裝完成之後即可執行指令安裝 llama2 模型,其安裝指令如下:

$ ollama run llama2

p.s. 執行上述模型需要至少 8GB 記憶體, 3.8G 硬碟空間

LCEL (LangChain Expression Language)

到底什麼是 LCEL? 以下 LangChain 對 LCEL 的功用說明:

LCEL makes it easy to build complex chains from basic components, and supports out of the box functionality such as streaming, parallelism, and logging.

LCEL 可以讓我們用各種基本元件,就能輕易的開發複雜的 chain 應用,而且可以支援:

  • Streaming: 一有結果就先回傳,不用等所有的結果都產生完才回傳
  • Parallelism: 同時接受多個輸入,並同時產生多個輸出
  • logging
  • 其他功能

To make it as easy as possible to create custom chains, we’ve implemented a “Runnable” protocol.

更切確來說, LCEL 有 2 個重要的目的。

統一的協定(Protocol)

1. A unified interface: Every LCEL object implements the Runnable interface, which defines a common set of invocation methods (invoke, batch, stream, ainvoke, …). This makes it possible for chains of LCEL objects to also automatically support these invocations. That is, every chain of LCEL objects is itself an LCEL object.

LangChain 制定的協定(protocol),每個元件例如(Prompt, ChatModel, LLM, OutputParser, Retriever, Tool 這些都是 LangChain 基本元件)都必須實作 1 個稱為 “Runnable” 的協定,這個協定至少包含以下方法(這些方法很重要,如果要將 Chain 應用變成 API 開放給他人使用,也可能需要實作以下方法):

  • invoke (支援單一輸入、單一輸出)
  • batch (支援多個輸入、多個輸出)
  • stream (支援有部分結果就輸出的模式)
  • ainvoke (async 版本的 invoke)
  • abatch (async 版本的 batch)
  • astream (async 版本的 stream)

更詳細定義可以看 “Runnable” 官方文件。

幾個重要的元件如下圖:

components.png 引用來源 LangChain 官網

提供組合原型(Composition primitives),讓 Chain 的開發變得簡易

2. Composition primitives: LCEL provides a number of primitives that make it easy to compose chains, parallelize components, add fallbacks, dynamically configure chain internal, and more.

LCEL 提供元件組合的功能,也就是本系列文章中經常使用到的 | 運算子,例如:

prompt | llm

組合出來的 Chain 原型(primitives)分為 2 種:

  • RunnableSequence
  • RunnableParallel

RunnableSequence

RunnableSequence 會依序執行每個 Runnable , 例如 prompt | llm | output 裡的 prompt, llm, output 都是 Runnable , 它們透過 | 運算子串起來之後,就是 RunnableSequence ,執行上是依序執行,例如下列程式碼範例,可以體驗到何謂 RunnableSequence :

from langchain_community.llms import Ollama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

llm = Ollama(model='llama2')

prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

prompt_output = prompt.invoke({"input": 'Hi there'})
llm_output = llm.invoke(prompt_output)
answer = StrOutputParser().invoke(llm_output)

print(answer)

RunnableParallel

RunnableParallel 則是可以同時執行 Runnable, 它的範例可以用 LangChain 的 RunnableLambda 體驗:

from langchain_core.runnables import RunnableLambda

def add_one(x: int) -> int:
    return x + 1

def add_two(x: int) -> int:
    return x + 2


runnable_1 = RunnableLambda(add_one)
runnable_2 = RunnableLambda(add_two)

parallel = {"runnable_1": runnable_1, "runnable_2": runnable_2}

chain = RunnableLambda(lambda x: x) | parallel
answer = chain.invoke(1)

print(answer)

上述範例程式執行結果如下,可以看到結果同時包含 2 個 Runnable 的輸出:

{'runnable_1': 2, 'runnable_2': 3}

在此說明一下,前述範例中的 {"runnable_1": runnable_1, "runnable_2": runnable_2} 部分就是 RunnableParallel ,當我們執行 chain.invoke(1) 時, parallel 中的 2 個 Runnable 都會各自執行,並輸出其結果,化為圖片的話如下所示:

runnable-parallel

所以要將多個 Runnable 變為 RunnableParallel 的話只要以字典(dict)表示,或者使用 RunnableParallel 類別也可以:

from langchain_core.runnables import RunnableParallel
from langchain_core.runnables import RunnableLambda

def add_one(x: int) -> int:
    return x + 1

def add_two(x: int) -> int:
    return x + 2


runnable_1 = RunnableLambda(add_one)
runnable_2 = RunnableLambda(add_two)

parallel = RunnableParallel(r1=runnable_1, r2=runnable_2)

chain = RunnableLambda(lambda x: x) | parallel
answer = chain.invoke(1)

print(answer)

有了 RunnableParallel, 你就可以做到用它組合多個語言模型同時執行!但那不在本章要介紹的範疇內。

RunnableBranch

目前為止,我們已經接觸 Runnable, RunnableSequence, RunnableParallel, RunnableLambda, 還有個變化型稱為 RunnableBranch

RunnableBranch 可以讓我們動態決定要走哪個分支執行下 1 個 Runnable, 例如你可以透過使用者不同的輸入,而採用不同的語言模型,例如:

from langchain_community.llms import Ollama
from langchain_core.runnables import RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

llm = Ollama(model='llama2')

prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

default_chain = prompt | llm # you can customize the chain here
python_chain = prompt | llm  # you can customize the chain here

def route(x):
    if 'python' in x['input']:
        return python_chain
    return default_chain

chain = RunnableLambda(route)
print(chain.invoke({"input": "python is the best"}))

LangChain 官網推薦使用 RunnableLambda 做完判斷之後,回傳新的 Runnable 就能夠做到 RunnableBranch 的效果。

如果你想要直接使用 RunnableBranch 的話,可以參考RunnableBranch 文件

RunnablePassthrough

Runnable to passthrough inputs unchanged or with additional keys.

如果你想對輸入做些加工的話,或者對前者 Runnable 輸出的結果做修改的話,例如修改使用者 prompt 加入關鍵詞,則可以使用 RunnablePassthrough

下列是修改使用者 prompt 的範例:

from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

chain = RunnablePassthrough.assign(input=lambda x: x['input'] + ' this is important to me.') | prompt
print(chain.invoke({"input": "python is the best."}))

上述範例執行結果如下,可以看到我們成功修改使用者的輸入:

messages=[HumanMessage(content='python is the best. this is important to me.')]

為什麼要認識/使用 LCEL?

LCEL 透過前述章節所提到的設計,讓 chain 的實作變得簡潔、簡單,如果沒有 LCEL 的話,整個 chain 將會變得異常複雜,為此 LangChain 也提供幾個有 LCEL, 沒有 LCEL 的對照,詳細可以閱讀 Why use LCEL 一文。

輸入/輸出 (Input & Output Schema)

從各個範例可以得知每個 Runnable 都有輸入與輸出,可是要怎麼知道它到底需要輸入什麼結構的資料,以及它會輸出什麼結構的資料呢?

這時可以查看 Runnable 的輸入與輸出的 schema, 這些 schema 被存在 input_schemaoutput_schema 2 個屬性中,只要是 Runnable 都可以查看其輸入與輸出結構。

舉下列為例:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

如果想查看 prompt 的輸入是什麼,可以使用:

print(prompt.input_schema.schema())

上述執行結果如下,從結果可以看到它的輸入必須是 1 個 object, 且該 object 含有 1 型別為字串的屬性(properties) input :

{'title': 'PromptInput', 'type': 'object', 'properties': {'input': {'title': 'Input', 'type': 'string'}}}

如果想知道其輸出爲何,則可以使用:

print(prompt.output_schema.schema())

甚至連 chain 也可以查看其輸入與輸出結構,因為 chain 也屬於 Runnable :

from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate


llm = Ollama(model='llama2')
prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

chain = prompt | llm
print(chain.input_schema.schema())

列印 Chain 的樣子

除了 input / output schema 的資訊, LangChain 也有提供 1 個方便的函式可以查看 chain 的長相,該方法為 <chain>.get_graph().print_ascii()

舉下列範例為例:

from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate


llm = Ollama(model='llama2')
prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])

chain = prompt | llm
chain.get_graph().print_ascii()

上述範例輸出為:

    +-------------+
    | PromptInput |
    +-------------+
           *
           *
           *
+--------------------+
| ChatPromptTemplate |
+--------------------+
           *
           *
           *
      +--------+
      | Ollama |
      +--------+
           *
           *
           *
   +--------------+
   | OllamaOutput |
   +--------------+

這張圖的資訊對複雜的 chain 進行除錯來說相當實用。

至此,你已經對 LCEL 有一層深入的理解!

總結

認識 LCEL 是相當重要的一件事,畢竟 LangChain 有很多概念需要理解 LCEL, 甚至有些範例程式使用的命名就是 LCEL 中的術語,其中就包括下一篇文章要介紹的 LangServe, LangServe 可以讓開發者把 chain 透過 FastAPI 開放給他人使用,如此一來,開發者就能解除在本機上開發/使用 LangChain 應用的限制,能夠讓他人不需要自行架設語言模型應用,也能夠透過 API 使用語言模型應用。

我們下篇見!

以上!

Enjoy!

References

LangChain Expression Language (LCEL)

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!