DEV Community

Jambo
Jambo

Posted on • Updated on

用 LangChain 構建基於資料庫的問答機器人(一):基礎用法

大家好,我是學生大使 Jambo。在上一個系列中,我們介紹了關於 Azure OpenAI API 的使用。如果你有跟著教程使用過,那麼你應該能感覺到僅僅是調用 API 是非常簡單的,繁瑣的是如何將 API 與你的應用結合起來。接下來,我將會介紹一個名為 LangChain 的庫,它可以幫助你更方便地將 Azure OpenAI 結合到你的應用中。

我也會將這個做成一個系列,最終目標是實現一個可以根據資料源回答問題的聊天機器人。

為什麼要用 LangChain

許多開發者希望將像 GPT 這樣的大語言模型整合到他們的應用中。而這些應用不僅僅是簡單地將用戶的輸入傳遞給 GPT,然後將 GPT 的輸出返回給用戶。

這些應用可能需要根據特定的資料源來回答問題,因此需要考慮如何存儲和查找資料。或者需要整理用戶的輸入,保存以前的消息記錄並提取重點。如果你希望模型按照特定的格式輸出文本,那麼你需要在 prompt(提示)中詳細描述格式,甚至需要提供示例。這些 prompt 通常是應用程序後台進行管理,用戶往往不會注意到它們的存在。對於一些複雜的應用程序,一個問題可能需要多個執行動作。例如聲稱可以自動完成指定項目的 AutoGPT,實際上是根據目標和作者編寫的 prompt 生成所需的動作並以JSON格式輸出,然後程序再執行相應的動作。

LangChain 基本上已經將這些你可能會使用到的功能打包好了,只需要規劃程式邏輯並調用函數即可。此外,LangChain 的這些功能與具體使用的模型API無關,不必為不同的語言模型編寫不同的代碼,只需更換 API 即可。

基本用法

在使用 LangChain 之前,建議先了解 Azure OpenAI API 的調用,否則即使是使用 LangChain,參數和用法也可能不容易理解。具體可以參考我之前的系列教程:用 Python 調用 Azure OpenAi API

LangChain 將由文字續寫(補全)的語言模型稱為 llm ,擁有聊天界面(輸入為聊天記錄)的語言模型稱為聊天模型。接下來我們也會用 Azure OpenAI API 來進行示例。

安裝

因為 LangChain 在調用 OpenAI 的 API 時,實際上會使用 OpenAI 提供的 SDK,因此我們還需要一併安裝 openai

pip install langchain
pip install openai
Enter fullscreen mode Exit fullscreen mode

生成文本

實例化模型對象

在使用 API 之前,我們需要先設置環境變量。如果你使用的是 OpenAI 原生的接口,就只需要設置 api_key;如果是 Azure OpenAI API 則還需要設置 api_versionapi_base ,具體的值與使用 openai 庫調用 Azure API 一樣,可以參考我之前的教程:用 Python 調用 Azure OpenAi API

import os
os.environ["OPENAI_API_KEY"] = ""
os.environ["OPENAI_API_VERSION"] = ""
os.environ["OPENAI_API_BASE"] = ""
Enter fullscreen mode Exit fullscreen mode

當然,這些值也可以在 terminal 中使用 export (在 Linux 下)命令設置,或者在 .env 文件中設置,然後用 python-dotenv 庫導入進環境變量。

LangChain 的大語言模型(llm)的類都封裝在 llms 中,我們需要從中導入 AzureOpenAI 類,並設置相關的參數。其中指定模型的參數名是 deployment_name,剩下的參數就是 OpenAI API 的參數了。事實上,上面在環境變量中設置的 API 信息也可以在這裡作為參數傳入,但考慮到便利性和安全性,仍建議在環境變量中設置 API 信息。

要注意的是,prompt 和 stop 參數並不是在這里傳入的(stop 可以但是會報警告),而是在下面生成文本時傳入。

from langchain.llms import AzureOpenAI
llm = AzureOpenAI(
    deployment_name="text-davinci-003",
    temperature=0.9,
    max_tokens=265,
)
Enter fullscreen mode Exit fullscreen mode

另外,如果你使用的是原生 OpenAI API ,那麼導入的類應該是 OpenAI ,並且指定模型的參數名是 model_name,例如:

from langchain.llms import AzureOpenAI
llm = AzureOpenAI(model_name="text-davinci-003")
Enter fullscreen mode Exit fullscreen mode

序列化 LLM 配置

假如你需要對多個場景有不同的 llm 配置,那麼將配置寫在代碼中就會不那麼簡單靈活。在這種情況下,將 llm 配置保存在文件中顯然會更方便。

from langchain.llms import OpenAI
from langchain.llms.loading import load_llm
Enter fullscreen mode Exit fullscreen mode

LangChain 支持將 llm 配置以 json 或 yaml 的格式讀取或保存。假設我現在有一個 llm.json 文件,內容如下:

{
    "model_name": "text-davinci-003",
    "temperature": 0.7,
    "max_tokens": 256,
    "top_p": 1.0,
    "frequency_penalty": 0.0,
    "presence_penalty": 0.0,
    "n": 1,
    "best_of": 1,
    "request_timeout": null,
    "_type": "openai"
}
Enter fullscreen mode Exit fullscreen mode

那麼我們可以使用 load_llm 函數將其轉換成 llm 對象,具體使用的是什麼語言模型是使用 _type 參數定義。

llm = load_llm("llm.json")
# llm = load_llm("llm.yaml")
Enter fullscreen mode Exit fullscreen mode

當然你也可以從 llm 對象導出成配置文件。

llm.save("llm.json")
llm.save("llm.yaml")
Enter fullscreen mode Exit fullscreen mode

從文本生成文本

接下來我們就要使用上面實例化的模型對象來生成文本。 LangChain 的 llm 類有三種方法從 String 生成文本:predict() 方法、generate()方法、像函數一樣直接調用對象(__call__)。

看上去途徑很多,但其實都只是 generate() 一種而已。具體來說,perdict() 簡單檢查後調用了 __call__ ,而 __call__ 簡單檢查後調用了 generate()generate() 方法與其他兩種途徑最大的區別在於 prompt 的傳入和返回的內容:generate() 傳入的是包含 prompt 的 list 並返回一個 LLMResult 對象,而其他兩種方法傳入的是 prompt 本身的 string ,返回生成文本的 string。意思是 generate() 可以一次對多個 prompt 獨立生成對應的文本。

prompt = "1 + 1 = "
stop = ["\n"]
# 下面三種生成方法是等價的
res1 = llm(prompt, stop=stop)
res2 = llm.predict(prompt, stop=stop)
res3 = llm.generate([prompt], stop=stop).generations[0][0].text
Enter fullscreen mode Exit fullscreen mode

如果只是想單純的從文字續寫(生成)文字的話,推薦用 predict() 方法,因為這種最方便也最直觀。

聊天模型

實例化模型對象

與上面的生成模型一樣,我們需要先設置環境變量

import os
os.environ["OPENAI_API_KEY"] = ""
os.environ["OPENAI_API_VERSION"] = ""
os.environ["OPENAI_API_BASE"] = ""
Enter fullscreen mode Exit fullscreen mode

LangChain 的聊天模型包裝在 langchain.chat_models 下,我們這裡一樣使用 Azure OpenAI 進行演示,導入的是 AzureChatOpenAI 類。

如果你有讀過我之前直接調用 API 的教程,那麼應該清楚我們對聊天模型輸入的 prompt 不再是文字,而是消息記錄,消息記錄中是用戶和模型輪流對話的內容,這些消息被 LangChain 包裝為 AIMessageHumanMessageSystemMessage,分別對應原先 API 中的 assistantusersystem

from langchain.chat_models import AzureChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
Enter fullscreen mode Exit fullscreen mode

我們先構建一個初始的消息記錄,當然 SystemMessage 並不是必須的。聊天模型用直接調用對象的方式生成消息,而他返回的會是一個 AIMessage 對象。

messages = [
    SystemMessage(content="你是一名翻譯員,將中文翻譯成英文"),
    HumanMessage(content="你好世界")
]
chat(messages)
Enter fullscreen mode Exit fullscreen mode
AIMessage(content='Hello world.', additional_kwargs={}, example=False)
Enter fullscreen mode Exit fullscreen mode

和之前一樣,聊天模型的 generate() 方法也支持對多個聊天記錄生成消息,不過這裡的返回值是一個 LLMResult 對象。

chat.generate([messages, messages])
Enter fullscreen mode Exit fullscreen mode

LLMResult

上面說 llm 的 generate() 方法返回的是一個 LLMResult 對象,它由三個部分組成:generations 儲存生成的內容和對應的信息、llm_output 儲存 token 使用量和使用的模型、run 儲存了唯一的 run_id,這是為了方便在生成過程中調用回調函數。通常我只需要關注 generationsllm_output

為了展示 LLMResult 的結果,這裡我們重新創建一個 llm 對象,並設置參數 n=2 ,代表模型對於每個 prompt 會生成兩次結果,這個值默認是 n=1

llm = AzureOpenAI(deployment_name="text-davinci-003", temperature=0, n=2)
llm_result = llm.generate([f"{i}**2 =" for i in range(1, 11)], stop="\n")
print(len(llm_result.generations))
# -> 10
print(len(llm_result.generations[0]))
# -> 2
Enter fullscreen mode Exit fullscreen mode

因為 LLMResult 是繼承自 Pydantic 的 BaseModel ,因此可以用 json() 將其格式化為 JSON :

print(llm_result.json())
Enter fullscreen mode Exit fullscreen mode
{
    "generations": [
        [
            {
                "text": " 1",
                "generation_info": {
                    "finish_reason": "stop",
                    "logprobs": null
                }
            },
            {
                "text": " 1",
                "generation_info": {
                    "finish_reason": "stop",
                    "logprobs": null
                }
            }
        ],
        ...
    ],
    "llm_output": {
        "token_usage": {
            "prompt_tokens": 40,
            "total_tokens": 58,
            "completion_tokens": 18
        },
        "model_name": "text-davinci-003"
    },
    "run": {
        "run_id": "cf7fefb2-2e44-474d-918f-b8695a514646"
    }
}
Enter fullscreen mode Exit fullscreen mode

可以看到 generations 是一個二維數組,第一維的每個元素代表對應的 prompt 生成的結果,第二維的每個元素代表這個 prompt 的一次生成結果,因為我們設置了 n=2 ,所以每個 prompt 會生成兩次結結果。

對於生成(補全)模型,生成的結果會是一個字典,具體生成的內容在 text 字段。而對於聊天模型,它會把結果包裝成 ChatGeneration 對象,其中 text 字段是生成的文字,message 字段則是文字對應的 AIMessage 對象,例如:

LLMResult(generations=[
        [
            ChatGeneration(
                text='Hello world.', 
                generation_info=None, 
                message=AIMessage(
                    content='Hello world.', 
                    additional_kwargs={}, 
                    example=False)
            )
        ]
    ], 
    llm_output={
        'token_usage': {
            'completion_tokens': 6, 'prompt_tokens': 80, 'total_tokens': 86
        }, 
    'model_name': 'gpt-3.5-turbo'
    }, 
    run=RunInfo(run_id=UUID('fffa5a38-c738-4eef-bdc4-0071511d1422')))
Enter fullscreen mode Exit fullscreen mode

Prompt 模板

很多時候我們並不會把用戶的輸入直接丟給模型,可能會需要在前後文進行補充信息,而這個補充的信息就是“模板”。下面是一個簡單的例子,這個 prompt 包含一個輸入變量 product,:

template = """
我希望你擔任顧問,幫忙為公司想名字。
這個公司生產{product},有什麼好名字?
"""
Enter fullscreen mode Exit fullscreen mode

我們可以用 PromptTemplate 將這個帶有輸入變量的 prompt 包裝成一個模板。

from langchain import PromptTemplate

prompt_template = PromptTemplate(
    input_variables=["product"],
    template=template,
)
prompt_template.format(product="運動襯衫")
# -> 我希望你擔任顧問,幫忙為公司想名字。
# -> 這個公司生成運動襯衫,有什麼好名字?
Enter fullscreen mode Exit fullscreen mode

當然,如果 prompt 中沒有輸入變量,也可以將其用 PromptTemplate 包裝,只是 input_variables 參數輸入的是空列表。

如果你不想手動指定 input_variables ,也可以使用 from_template() 方法自動推導。

prompt_template = PromptTemplate.from_template(template)
prompt_template.input_variables
# -> ['product']
Enter fullscreen mode Exit fullscreen mode

你還可以將模板保存到本地文件中,目前 LangChain 只支持 json 和 yaml 格式,它可以通過文件後綴自動判斷文件格式。

prompt_template.save("awesome_prompt.json") # 保存為 json
Enter fullscreen mode Exit fullscreen mode

也可以從文件中讀取

from langchain.prompts import load_prompt
prompt_template = load_prompt("prompt.json")
Enter fullscreen mode Exit fullscreen mode

Chain

Chain 是 LangChain 裡非常重要的概念(畢竟都放在名字裡了),它與管道(Pipeline)類似,就是將多個操作組裝成一個函數(流水線),從而使得代碼更加簡潔方便。

比如我們執行一個完整的任務週期,需要先生成 prompt ,將 prompt 給 llm 生成文字,然後可能還要對生成的文字進行其他處理。更進一步,我們或許還要記錄任務每個階段的日誌,或者更新其他數據。這些操作如果都寫出來,那麼代碼會非常冗長,而且不容易復用,但是如果使用 Chain ,你就可以將這些工作都打包起來,並且代碼邏輯也更清晰。你還可以將多個 Chain 組合成一個更複雜的 Chain 。

我們首先創建一個 llm 對象和一個 prompt 模板。

from langchain.llms import AzureOpenAI
from langchain import PromptTemplate

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
prompt = PromptTemplate(
    input_variables=["input"],
    template="""
    將給定的字符串進行大小寫轉換。
    例如:
    輸入: ABCdef
    輸出: abcDEF

    輸入: AbcDeF
    輸出: aBCdEf

    輸入: {input}
    輸出: 
    """,
)
Enter fullscreen mode Exit fullscreen mode

接下來我們可以通過 LLMChain 將 llm 和 prompt 組合成一個 Chain 。這個 Chain 可以接受用戶輸入,然後將輸入填入 prompt 中,最後將 prompt 交給 llm 生成結果。另外如果你用的是聊天模型,那麼使用的 Chain 是 ConversationChain

from langchain.chains import LLMChain

chain = LLMChain(llm=chat, prompt=prompt)
print(chain.run("HeLLo"))
# -> hEllO
Enter fullscreen mode Exit fullscreen mode

如果 prompt 中有多個輸入變量,可以使用字典一次將它們傳入。

print(chain.run({"input": "HeLLo"}))
# -> hEllO
Enter fullscreen mode Exit fullscreen mode

Debug 模式

上面都是些簡單的例子,只牽扯到少量的輸入變量,但在實際使用中可能會有大量的輸入變量,並且 llm 的輸出還是不固定的,這就使得我們很難從最重的結果反推問題所在。為了解決這個問題,LangChain 提供了 verbose 模式,它會將每個階段的輸入輸出都打印出來,這樣就可以很方便地找到問題所在。

chain_verbose = LLMChain(llm=llm, prompt=prompt, verbose=True)
print(chain_verbose.run({"input": "HeLLo"}))
Enter fullscreen mode Exit fullscreen mode
> Entering new  chain...
Prompt after formatting:

    將給定的字符串進行大小寫轉換。
    例如:
    輸入: ABCdef
    輸出: abcDEF

    輸入: AbcDeF
    輸出: aBCdEf

    輸入: HeLLo
    輸出: 


> Finished chain.
hEllO
Enter fullscreen mode Exit fullscreen mode

組合 Chain

一個 Chain 對像只能完成一個很簡單的任務,但我們可以像搭積木一樣將多個簡單的動作組合在一起,就可以完成更複雜的任務。其中最簡單的是順序鏈 SequentialChain,它將多個 Chain 串聯起來,前一個 Chain 的輸出將作為後一個 Chain 的輸入。不過要注意的是,因為這只是個最簡單的鏈,所以它不會對輸入進行任何處理,也不會對輸出進行任何處理,所以你需要保證每個 Chain 的輸入輸出都是兼容的,並且它要求每個 Chain 的 prompt 都只有一個輸入變量。

下面,我們先計算輸入數字的平方,然後將平方數轉換成羅馬數字。

from langchain.chains import SimpleSequentialChain

chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
prompt1 = PromptTemplate(
    input_variables=["base"], template="{base}的平方是: "
)
chain1 = LLMChain(llm=chat, prompt=prompt1)

prompt2 = PromptTemplate(input_variables=["input"], template="將{input}寫成羅馬數字是:")
chain2 = LLMChain(llm=chat, prompt=prompt2)

overall_chain = SimpleSequentialChain(chains=[chain1, chain2], verbose=True)
overall_chain.run(3)
Enter fullscreen mode Exit fullscreen mode
> Entering new  chain...


9
IX

> Finished chain.
'IX'
Enter fullscreen mode Exit fullscreen mode

LangChain 已經預先準備了許多不同 Chain 的組合,具體可以參考官方文檔,這裡就先不展開了。

Top comments (0)