LangChain 怎麼玩? 動態修改運作中的 Chain 設定 / configure chain internals at runtime

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

不知道你有沒有想過 1 個 Chain 要如何做到動態載入不同使用者的資料?或者如何像 ChatGPT 那樣可以切換模型?

這個功能在 LangChain 中稱為 configurable_fieldsconfigurable_alternatives ,可以讓我們動態修改 chain 的設定,或者置換 chain 上的某個 Runnable (例如 prompt, language model 都是 Runnable) 。

能動態變更 chain 設定的功能相當重要,學會使用它也是必要的功課之一,否則我們所開發出來的應用會喪失不少彈性!

本文環境

$ pip install langchain

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

$ ollama pull llama2
$ ollama pull codellama

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

設定 configurable_fields 以動態更新設定

學習如何使用 configurable_fields 之前,先看 1 個問題,以下範例讓 llama2 語言模型說個笑話:

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


llm = Ollama(model='llama2', temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])
chain = prompt | llm
print(chain.invoke({'input': 'Tell me a joke'}))

上述範例執行之後,每次都會出現相同內容(內容如下),這是因為我們載入 llama2 時將 temperature 設定為 0 的緣故,所以語言模型的回應不會有一些隨機性產生,造成每次都回應相同內容:

Sure, here's one:

Why don't scientists trust atoms?
Because they make up everything!

I hope you found that amusing! Do you want to hear another one?

如果我們想動態設定一些隨機性的話,雖然可以每次產生回應都重新載入語言模型並重新設定 temperature 參數,但是也會導致回應時間拉長,因為語言模型的載入會需要一點點時間,也許比較好的方式是讓 chain 具有動態修改設定的能力;又或者我們想做些實驗比較不同 prompt / 參數 / 模型的差異,這時候也需要動態修改設定的能力,讓我們在呼叫 chain 時使用不同的參數,以對結果進行比較;甚至是讓 chain 具有服務多名使用者的能力,針對使用者帳號不同載入不同的語言模型、對話紀錄,就像 ChatGPT 一樣,所有使用者之間不會看到彼此的對話紀錄以確保隱私,而且付費使用者多了可以切換至更強大的語言模型的功能。

針對這些需求, LangChain 為每個 Runnable 提供 1 個稱為 configurable_fields 的方法,讓我們可以動態修改 chain 上的設定值。

下列是能動態設定 temperature 的程式碼範例:

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


llm = Ollama(model='llama2', temperature=0).configurable_fields(
    temperature=ConfigurableField(
        id="llm_temperature",
        name="LLM Temperature",
        description="The temperature of the LLM",
    )
)

prompt = ChatPromptTemplate.from_messages([
    ("user", "{input}"),
])
chain = prompt | llm
print(
    chain.with_config(
        configurable={"llm_temperature": 0.9}
    ).invoke({'input': 'Tell me a joke'})
)

上述範例執行結果如下,可以看到產生的內容已經產生變化:

I'm just an AI, I don't have personal preferences or emotions, but I can certainly try to come up with a joke for you! Here is one:

Why don't scientists trust atoms? Because they make up everything! 😂

I hope that brought a smile to your face! Do you have any specific topics or subjects you would like me to generate jokes about?

以下說明上述範例的重要部分。

為了能夠動態設定 Ollama()temperature 參數,所以在 Ollama(model='llama2', temperature=0) 之後額外呼叫了 configurable_fields 方法,將 temperature 參數設定為 ConfigurableField ,並將它的設定稱為 llm_temperature ,我們之後就可以在使用 chain 時,帶入 llm_temperature 的設定值, LangChain 就會知道要修改 temperature 參數:

llm = Ollama(model='llama2', temperature=0).configurable_fields(
    temperature=ConfigurableField(
        id="llm_temperature",
        name="LLM Temperature",
        description="The temperature of the LLM",
    )
)

使用 chain 時,可以額外呼叫 with_config() 方法設定 temperature :

chain.with_config(
    configurable={"llm_temperature": 0.9}
).invoke({'input': 'Tell me a joke'})

接下來,再看 1 個同時可以設定語言模型與 temperature 的範例:

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


llm = Ollama(model='llama2', temperature=0).configurable_fields(
    temperature=ConfigurableField(
        id="temperature",
        name="LLM Temperature",
        description="The temperature of the LLM",
    ),
    model=ConfigurableField(
        id="model",
        name="The Model",
        description="The language model",
    ),
)

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

print(chain.with_config(
    configurable={
        "model": "codellama",
        "temperature": 0.9
    }
).invoke({'input': 'Tell me a joke'}))

上述範例透過 configurable_fields() 方法設定 2 個可動態調整的參數,分別為 temperaturemodel ,並且在呼叫 chain 時設定 2 個參數, with_config(configurable={"model": "codellama", "temperature": 0.9})

用 configurable_alternatives 動態置換 Runnable

先前的範例都是使用 LLaMA 系列的語言模型,這些系列的語言模型都可以使用 Ollama 載入,所以可以使用 configurable_fields() 動態設定語言模型,不過 OpenAI 的語言模型就不能使用 Ollama 載入,這使得我們無法使用 configurable_fields() 方法動態切換語言模型。

LangChain 還有 1 個稱為 configurable_alternatives() 的方法,可以動態置換 Runnable, 也就是說我們可以透過 configurable_alternatives() 方法,動態把 Ollama(model='llama2') 換成 ChatOpenAI(model="gpt-3.5-turbo", temperature=0), 其範例如下:

from langchain_community.llms import Ollama
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import ConfigurableField

llm = Ollama(model='llama2').configurable_alternatives(
    ConfigurableField(id="llm"),
    default_key='llama2',
    gpt35=ChatOpenAI(model="gpt-3.5-turbo", temperature=0),
)

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

print(chain.with_config(configurable={"llm": "gpt35"}).invoke({'input': 'Tell me a joke'}))

上述範例透過 configurable_alternatives() 設定 1 個稱為 llm 的參數,該參數提供 llama2gpt35 2 個選項的設定值,其中 llama2 是預設值,該預設值代表 Ollama(model='llama2') ,而 gpt35 代表 ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

所以我們可以在呼叫 chain 的時候,同樣透過 with_config() 方法將 llm 參數設定為 gpt35 或者 llama2 ,也就是以下重點部分:

(略).with_config(configurable={"llm": "gpt35"})

如此一來我們就能夠動態切換任何語言模型,或者 Runnable 。

最後再看 1 個用 configurable_alternatives() 動態置換 Prompt 的範例:

prompt = PromptTemplate.from_template(
    "Tell me a joke about {topic}"
).configurable_alternatives(
    ConfigurableField(id="prompt"),
    default_key="joke",
    poem=PromptTemplate.from_template("Write a short poem about {topic}"),
)

chain.with_config(configurable={"prompt": "poem"}).invoke({"topic": "Earth"})

動態載入不同使用者的對話紀錄

我們在使用 ChatGPT 時, ChatGPT 會幫我們保留不同的對話紀錄,而且不會看到不屬於我們的對話紀錄,這也是做任何應用重要的功能—針對不同使用者載入專屬的設定、資料。

學會使用 configurable_fields()configurable_alternatives() 之後,我們就可以動態載入使用者的相關設定與資料。

舉對話紀錄(chat history)為例, LangChain 稱此功能為 memory, 我們可以將對話紀錄存在各種 memory 中,例如檔案、資料庫等等, LangChain 提供多種整合方案可以選擇,詳細可以閱讀此文件

本文僅展示以 Python dictionary 儲存對話紀錄作為教學範例。

LangChain 其實有提供 1 個 Runnable 稱為 RunnableWithMessageHistory, 這個類別其實就是使用 configurable_fields() 方法,預設讓我們可以動態設定 session_id 藉此載入不同對話紀錄。

以下是使用 RunnableWithMessageHistory 的範例程式碼:

from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_community.llms import Ollama


chat001 = ChatMessageHistory()
chat001.add_user_message('My name is Amo.')

store = {
    'chat001': chat001,
}


def get_chat_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


llm = Ollama(model='llama2')

prompt = ChatPromptTemplate.from_messages([
    ('system', 'You are a good assistant.'),
    MessagesPlaceholder(variable_name='chat_history'),
    ('user', '{input}'),
])

chain = prompt | llm

with_message_history = RunnableWithMessageHistory(
    chain,
    get_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

session_id = 'chat001'
input_text = input('>>> ')
while input_text.lower() != 'bye':
    if input_text:
        response = with_message_history.with_config(
            configurable={'session_id': session_id}
        ).invoke({'input': input_text})
        print(response)
    input_text = input('>>> ')

上述範例的重點部分在於使用 RunnableWithMessageHistory()chain 進行設定,設定 1 個方法接受 session_id ,並回傳對話紀錄,也就是函式 get_chat_history , 並且設定這些對話紀錄要放進 prompt 的 chat_history message placeholder 中:

with_message_history = RunnableWithMessageHistory(
    chain,
    get_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

如此一來,我們就可以設定要讀取哪個 session_id 的對話紀錄,以新增/延續對話。

只是預設的 get_chat_history() 還是無法針對不同使用者進行設定,幸好 RunnableWithMessageHistory 還提供 history_factory_config 參數,可以客製化 get_chat_history() 所接受的參數,例如下列範例以 history_factory_config 參數設定 user_idconversation_id 必須傳入給 get_chat_history() 方法,以藉此取得不同使用者的特定對話紀錄:

from langchain_core.messages import HumanMessage
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.runnables import ConfigurableFieldSpec
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings


chat001 = ChatMessageHistory()
chat001.add_user_message('My name is Amo.')

chat002 = ChatMessageHistory()
chat002.add_user_message('My name is Jeff.')

store = {
    ('amo', 'chat001'): chat001,
    ('jeff', 'chat002'): chat002,
}


def get_chat_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:
    key = (user_id, conversation_id, )
    if key not in store:
        store[key] = ChatMessageHistory()
    return store[key]


llm = Ollama(model='llama2')

prompt = ChatPromptTemplate.from_messages([
    ('system', 'You are a good assistant.'),
    MessagesPlaceholder(variable_name='chat_history'),
    ('user', '{input}'),
])

chain = prompt | llm

with_message_history = RunnableWithMessageHistory(
    chain,
    get_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    history_factory_config=[
        ConfigurableFieldSpec(
            id="user_id",
            annotation=str,
            name="User Id",
            description="Unique identifier for the user.",
            default="",
            is_shared=True,
        ),
        ConfigurableFieldSpec(
            id="conversation_id",
            annotation=str,
            name="Conversation Id",
            description="Unique identifier for the conversation.",
            default="",
            is_shared=True,
        ),
    ],
)

user_id = 'jeff'
conversation_id = 'chat002'
input_text = input('>>> ')
while input_text.lower() != 'bye':
    if input_text:
        response = with_message_history.with_config(
            configurable={
                'user_id': user_id,
                'conversation_id': conversation_id
            }
        ).invoke({'input': input_text})
        print(response)
    input_text = input('>>> ')

上述範例的重點在於我們在 history_factory_config 參數中以 ConfigurableFieldSpec() 指定 2 個設定,分別為 user_idconversation_id ,這 2 個設定都是字串型態:

    history_factory_config=[
        ConfigurableFieldSpec(
            id="user_id",
            annotation=str,
            name="User Id",
            description="Unique identifier for the user.",
            default="",
            is_shared=True,
        ),
        ConfigurableFieldSpec(
            id="conversation_id",
            annotation=str,
            name="Conversation Id",
            description="Unique identifier for the conversation.",
            default="",
            is_shared=True,
        ),
    ],

如此就可以在呼叫 chain 時,帶入 user_idconversation_id 2 個設定,做到動態載入使用者資料的功能(當然 get_chat_history 函式也需要做相對應的邏輯修改):

with_message_history.with_config(
    configurable={
        'user_id': user_id,
        'conversation_id': conversation_id
    }
).invoke({'input': input_text})

至此,你已經理解如何做到 1 個 chain 服務多個使用者的基本架構。

總結

動態修改 chain 的設定是 LangChain 重要的一項技術,學會此技術才能夠做到動態載入使用者設定、資料等功能。

以上!

Enjoy!

References

LangChain - Configure chain internals at runtime

LangChain - Add message history (memory)

對抗久坐職業傷害

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

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

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

贊助我們的創作

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

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