大家好,我是學生大使 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
生成文本
實例化模型對象
在使用 API 之前,我們需要先設置環境變量。如果你使用的是 OpenAI 原生的接口,就只需要設置 api_key
;如果是 Azure OpenAI API 則還需要設置 api_version
和 api_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"] = ""
當然,這些值也可以在 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,
)
另外,如果你使用的是原生 OpenAI API ,那麼導入的類應該是 OpenAI
,並且指定模型的參數名是 model_name
,例如:
from langchain.llms import AzureOpenAI
llm = AzureOpenAI(model_name="text-davinci-003")
序列化 LLM 配置
假如你需要對多個場景有不同的 llm 配置,那麼將配置寫在代碼中就會不那麼簡單靈活。在這種情況下,將 llm 配置保存在文件中顯然會更方便。
from langchain.llms import OpenAI
from langchain.llms.loading import load_llm
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"
}
那麼我們可以使用 load_llm
函數將其轉換成 llm 對象,具體使用的是什麼語言模型是使用 _type
參數定義。
llm = load_llm("llm.json")
# llm = load_llm("llm.yaml")
當然你也可以從 llm 對象導出成配置文件。
llm.save("llm.json")
llm.save("llm.yaml")
從文本生成文本
接下來我們就要使用上面實例化的模型對象來生成文本。 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
如果只是想單純的從文字續寫(生成)文字的話,推薦用 predict()
方法,因為這種最方便也最直觀。
聊天模型
實例化模型對象
與上面的生成模型一樣,我們需要先設置環境變量
import os
os.environ["OPENAI_API_KEY"] = ""
os.environ["OPENAI_API_VERSION"] = ""
os.environ["OPENAI_API_BASE"] = ""
LangChain 的聊天模型包裝在 langchain.chat_models
下,我們這裡一樣使用 Azure OpenAI 進行演示,導入的是 AzureChatOpenAI
類。
如果你有讀過我之前直接調用 API 的教程,那麼應該清楚我們對聊天模型輸入的 prompt 不再是文字,而是消息記錄,消息記錄中是用戶和模型輪流對話的內容,這些消息被 LangChain 包裝為 AIMessage
、HumanMessage
、SystemMessage
,分別對應原先 API 中的 assistant
、user
、system
。
from langchain.chat_models import AzureChatOpenAI
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage
)
chat = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
我們先構建一個初始的消息記錄,當然 SystemMessage
並不是必須的。聊天模型用直接調用對象的方式生成消息,而他返回的會是一個 AIMessage
對象。
messages = [
SystemMessage(content="你是一名翻譯員,將中文翻譯成英文"),
HumanMessage(content="你好世界")
]
chat(messages)
AIMessage(content='Hello world.', additional_kwargs={}, example=False)
和之前一樣,聊天模型的 generate()
方法也支持對多個聊天記錄生成消息,不過這裡的返回值是一個 LLMResult
對象。
chat.generate([messages, messages])
LLMResult
上面說 llm 的 generate()
方法返回的是一個 LLMResult
對象,它由三個部分組成:generations
儲存生成的內容和對應的信息、llm_output
儲存 token 使用量和使用的模型、run
儲存了唯一的 run_id
,這是為了方便在生成過程中調用回調函數。通常我只需要關注 generations
和 llm_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
因為 LLMResult
是繼承自 Pydantic 的 BaseModel
,因此可以用 json()
將其格式化為 JSON :
print(llm_result.json())
{
"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"
}
}
可以看到 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')))
Prompt 模板
很多時候我們並不會把用戶的輸入直接丟給模型,可能會需要在前後文進行補充信息,而這個補充的信息就是“模板”。下面是一個簡單的例子,這個 prompt 包含一個輸入變量 product
,:
template = """
我希望你擔任顧問,幫忙為公司想名字。
這個公司生產{product},有什麼好名字?
"""
我們可以用 PromptTemplate
將這個帶有輸入變量的 prompt 包裝成一個模板。
from langchain import PromptTemplate
prompt_template = PromptTemplate(
input_variables=["product"],
template=template,
)
prompt_template.format(product="運動襯衫")
# -> 我希望你擔任顧問,幫忙為公司想名字。
# -> 這個公司生成運動襯衫,有什麼好名字?
當然,如果 prompt 中沒有輸入變量,也可以將其用 PromptTemplate
包裝,只是 input_variables
參數輸入的是空列表。
如果你不想手動指定 input_variables
,也可以使用 from_template()
方法自動推導。
prompt_template = PromptTemplate.from_template(template)
prompt_template.input_variables
# -> ['product']
你還可以將模板保存到本地文件中,目前 LangChain 只支持 json 和 yaml 格式,它可以通過文件後綴自動判斷文件格式。
prompt_template.save("awesome_prompt.json") # 保存為 json
也可以從文件中讀取
from langchain.prompts import load_prompt
prompt_template = load_prompt("prompt.json")
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}
輸出:
""",
)
接下來我們可以通過 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
如果 prompt 中有多個輸入變量,可以使用字典一次將它們傳入。
print(chain.run({"input": "HeLLo"}))
# -> hEllO
Debug 模式
上面都是些簡單的例子,只牽扯到少量的輸入變量,但在實際使用中可能會有大量的輸入變量,並且 llm 的輸出還是不固定的,這就使得我們很難從最重的結果反推問題所在。為了解決這個問題,LangChain 提供了 verbose
模式,它會將每個階段的輸入輸出都打印出來,這樣就可以很方便地找到問題所在。
chain_verbose = LLMChain(llm=llm, prompt=prompt, verbose=True)
print(chain_verbose.run({"input": "HeLLo"}))
> Entering new chain...
Prompt after formatting:
將給定的字符串進行大小寫轉換。
例如:
輸入: ABCdef
輸出: abcDEF
輸入: AbcDeF
輸出: aBCdEf
輸入: HeLLo
輸出:
> Finished chain.
hEllO
組合 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)
> Entering new chain...
9
IX
> Finished chain.
'IX'
LangChain 已經預先準備了許多不同 Chain 的組合,具體可以參考官方文檔,這裡就先不展開了。
Top comments (0)