LangChain 怎麼玩? Agents 篇,來整合一些客製化的功能/工具吧
Posted on Feb 24, 2024 in LangChain , Python 程式設計 - 高階 by Amo Chen ‐ 7 min read
你有沒有特別想過如果我們開發的功能要怎麼跟語言模型進行結合?畢竟語言模型如果只能做聊天應用的話,那麼它的應用範圍就相當侷限。
這個問題的解答就是 LangChain 的 Agents 。
Agents 可以讓我們把自己開發的功能接上語言模型,讓語言模型執行我們所開發的功能!
本文同樣以 1 個簡單的範例開始,帶大家建立自己的 Agents 。
本文環境
- macOS
- Python 3
- LangChain
- LangChain-OpenAI
由於 LangChain 與 OpenAI 的整合度較高,而且 OpenAI 的套件在開發 Agents 的難度也確實相當方便,實作門檻相對低很多,因此本文以 OpenAI 的 ChatGPT-3.5-Turbo 語言模型實作,未來如果有開源模型也能夠做到如 OpenAI 這般便捷的話,將另闢一篇新教學文。
p.s. 使用 ChatGPT 3.5 Turbo 的價格相對於 ChatGPT 4 更為低廉
以下指令安裝 LangChain, OpenAI 相關套件:
$ pip install langchain langchain-openai langchainhub
安裝完相關套件之後,仍需至 OpenAI Platform 申請 1 組 API key, 並使用以下指令建立環境變數,如此才能夠讓 OpenAI 自行抓到 API key 以執行 OpenAI 的語言模型:
export OPENAI_API_KEY="<your OpenAI API key here>"
如果你的 OpenAI 帳號可用儲值為零,建議可以儲值最小金額 5 USD 即可。
Agents 簡介
The core idea of agents is to use a language model to choose a sequence of actions to take. In chains, a sequence of actions is hardcoded (in code). In agents, a language model is used as a reasoning engine to determine which actions to take and in which order.
Agents 是能夠使用語言模型進行一系列任務操作的應用,這些操作稱為 actions, 相對於單純使用 chain 只能進行固定的任務操作,使用 Agents 則可以讓語言模型自行決定要以何種順序進行何種任務操作。
簡單來說,就是 Agents 可以呼喚機器人並指派一堆事項,讓機器人自行理解要進行什麼任務,要以何種順序完成任務。
當然事情也沒這麼簡單,你也必須提供 Agents 完成任務所需的工作才行,後續的程式碼你將會看到我們如何提供機器人工具,這個工具我們稱為 Tool 。
p.s. LangChain 也有提供不少內建的工具可以使用,詳見 Tools
Agent 種類 / Agent Types
LangChain 將 Agent 劃分為 7 種:
- OpenAI Tools
- OpenAI Functions
- XML
- Structured Chat
- JSON Chat
- ReAct
- Self Ask With Search
這 7 種 Agents 有各自的適合的應用場景,主要分為能否交互對談,也就是 Chat, 以及單純語言模型(LLM)之用,也就是不具備交互對談能力,只能一問一答,不具備紀錄對話的功能。
這些 Agents 能夠支援的功能也都不同,例如:
- Supports Chat History 是否支援紀錄對話,基本上 Chat 型的 Agents 都支援紀錄對話
- Supports Multi-Input Tools 指的是執行的工具是否支援多個輸入參數,參數越多對語言模型來講難度越高,畢竟它還必須理解哪個參數要放入哪個值才行
- Supports Parallel Function Calling 則是指是否支援平行執行工具的能力,這個功能可以提昇 Agents 的執行效率,不過該功能對語言模型來說也是一項挑戰,目前只有 OpenAI Tools 支援
目前支援最廣的還是 OpenAI Tools Agents, 本文也是使用 OpenAI Tools 作為範例。
關於上述 7 種 Agents 的詳細說明,可以查看 LangChain 官方文件 Agent Types 。
做個能連網的 Agent
如果你在 ChatGPT 3.5 上問以下問題:
Is the site example.com alive?
就會得到類似以下的訊息, ChatGPT 3.5 會告訴你它無從得知網站狀態。
I'm sorry, but I don't have real-time information on the status of specific websites. You may want to try accessing example.com directly in your web browser to see if it is alive.
接下來,我們來幫 ChatGPT 3.5 加入能夠即時得知網站狀態的功能吧!
有 Tools 的 Agent
以下是能夠存取網站狀態的 Agent 程式碼,我們同樣稍後再解釋以下程式碼的:
import requests
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from langchain.agents import tool
from langchain.agents import AgentExecutor
@tool
def check_site_alive(site: str) -> bool:
"""Check a site is alive or not."""
try:
resp = requests.get(f'https://{site}')
resp.raise_for_status()
return True
except Exception:
return False
tools = [check_site_alive, ]
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
llm_with_tools = llm.bind_tools(tools)
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are very powerful assistant, but don't know current events",
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
input_text = input('>>> ')
while input_text.lower() != 'bye':
if input_text:
response = agent_executor.invoke({
'input': input_text,
})
print(response['output'])
input_text = input('>>> ')
上述範例執行之後,你可以再試著問一次以下問題:
Is the site example.com alive?
你將可以看到這次 ChatGPT 3.5 能夠得知網站狀態了!執行過程如下所示,可以看到它推論出要使用參數 {'site': 'example.com'}
去呼叫 check_site_alive
這個 Python 函式,執行得到結果後,再告訴我們答案:
>>> Is the site example.com alive?
> Entering new AgentExecutor chain...
Invoking: `check_site_alive` with `{'site': 'example.com'}`
True Yes, the site example.com is alive.
> Finished chain.
Yes, the site example.com is alive.
p.s. 如果不需要 debug 訊息,可以把程式碼中的 verbose=True
改為 verbose=False
這就是 1 個最簡單的 Agent 。
接著解釋前述範例中幾個重點部分。
載入語言模型
首先是載入 OpenAI 的語言模型,其中 temperature=0
代表語言模型的回應不要添加一些隨機性:
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
定義工具(Tool)
接著,定義好我們要交給 Agent 執行的工具,也就是 Python 函式 check_site_alive()
,該函式只是簡單地呼叫 requests.get()
並查看網站 status code 是否正常,並回傳 True 或 False 代表正常與不正常:
@tool
def check_site_alive(site: str) -> bool:
"""Check a site is alive or not."""
try:
resp = requests.get(f'https://{site}')
resp.raise_for_status()
return True
except Exception:
return False
LangChain 把工具的定義包裝的很簡單,只要使用 @tool
裝飾子就能夠把 Python 函式轉成 Tool 。
其中要注意的是,函式的 docstring 一定要提供,也就是上述 """Check a site is alive or not."""
的部分,否則會出現以下錯誤,這是讓語言模型解析工具用途的重要資訊:
ValueError: Function must have a docstring if description not provided.
另外,根據官方文件,參數的部分建議最好也提供型別註釋(Type hints) 。
綁定工具與語言模型
定義工具之後,要把工具跟語言模型綁定,也就是把工具交給語言模型:
llm_with_tools = llm.bind_tools(tools)
定義 Prompt
語言模型當然少不了定義 Prompt 的步驟,以下是 1 個簡單的 Agent 的 Prompt, 讓它扮演 1 個優秀的助手,其中最重要的是必須提供 MessagesPlaceholder(variable_name="agent_scratchpad")
的 message placeholder, 因為 Agent 會用 agent_scratchpad
儲存中間步驟(intermediate steps)相關資訊,例如 Agent 此次的任務已經執行過哪些步驟以及該步驟的輸出等等,這樣 Agent 才會知道哪些步驟已經做過,我們不需要對它做任何事情。
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are very powerful assistant, but don't know current events",
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
建立 Agent
聚齊 Prompt, LLM, Tools 等元素之後,可以進一步組合成 Agent ,這跟我們先前介紹的流程類似:
輸入 -> Prompt -> 語言模型 -> 輸出
值得注意的是輸入給 prompt 的參數,除了需要我們輸入的訊息 input
之外,還需要 agent_scratchpad
。
此處照抄官方文件即可,主要需要用 format_to_openai_tool_messages()
函式將 x["intermediate_steps"]
轉成 OpenAI 所需要的格式,特別需要注意的是此處 input
與 agent_scratchpad
都是使用 lambda
函式,這是因為 x["intermediate_steps"]
是由 AgentExecutor 所產生並帶給 agent
執行的:
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
建立 Agent Executor
最後,重中之重就是用 AgentExecutor 把 agent
與 tools
組成 Agent Executor:
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
The agent executor is the runtime for an agent. This is what actually calls the agent, executes the actions it chooses, passes the action outputs back to the agent, and repeats.
AgentExecutor 實際上負責呼叫 agent 之外,也負責執行任務,並把任務結果傳給 agent, 如果有多個步驟要執行就會重複這些步驟,直到完成任務,根據官方文件,它的運作類似以下虛擬碼:
next_action = agent.get_action(...)
while next_action != AgentFinish:
observation = run(next_action)
next_action = agent.get_action(..., next_action, observation)
return next_action
總之, AgentExecutor 會幫我們打理關於 x["intermediate_steps"]
相關的事情,我們只要負責 input
即可。
到這步就完成能夠執行客製化功能的機器人了!
又失憶了怎麼辦?把 Chat History 加進去吧
前述範例程式雖然可以執行客製化功能,但是它沒辦法紀錄對話的上下文,所以你告訴它你的名字之後,它下一秒就無法回答你的名字是什麼⋯⋯。
>>> My name is Amo.
Hello Amo! How can I assist you today?
>>> What's my name?
I'm sorry, I don't have access to personal information like your name. How can I assist you today?
如果要讓機器人能夠像前一篇文章一樣能夠紀錄對話上下文,並且針對上下文情境回答的話,就需要將對話紀錄也加到 Prompt 之中。
剛好用 OpenAI Tools 建立的 Agent 是有支援 Chat History 的,所以只要修改 Prompt 增加 1 個 MessagePlaceholder
儲存對話紀錄,並且每次呼叫 Agent Executor 時帶入對話紀錄即可。
修改後的程式碼如下:
import requests
from langchain.globals import set_debug
#set_debug(True)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.messages import AIMessage, HumanMessage
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from langchain.agents import tool
from langchain.agents import AgentExecutor
@tool
def check_site_alive(site: str) -> bool:
"""Check a site is alive or not."""
try:
resp = requests.get(f'https://{site}')
resp.raise_for_status()
return True
except Exception:
return False
tools = [check_site_alive, ]
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
llm_with_tools = llm.bind_tools(tools)
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are very powerful assistant, but don't know current events",
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
"chat_history": lambda x: x["chat_history"],
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True)
chat_history = []
input_text = input('>>> ')
while input_text.lower() != 'bye':
if input_text:
response = agent_executor.invoke({
'input': input_text,
'chat_history': chat_history,
})
chat_history.extend([
HumanMessage(content=input_text),
AIMessage(content=response["output"]),
])
print(response['output'])
input_text = input('>>> ')
上述程式碼修改的部分是:
- Prompt 加了
chat_history
:
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are very powerful assistant, but don't know current events",
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
- Agent 也需要傳
chat_history
到 prompt,chat_history
才能一路傳進語言模型:
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
"chat_history": lambda x: x["chat_history"],
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
- 把每一次對話紀錄存到
chat_history
list 中,並且呼叫 Agent Executor 時附上chat_history
response = agent_executor.invoke({
'input': input_text,
'chat_history': chat_history,
})
chat_history.extend([
HumanMessage(content=input_text),
AIMessage(content=response["output"]),
])
以上,就完成能聊天又能執行特定功能的聊天機器人啦~~!
用別人寫好的 Prompt 吧 — LangChain Hub
LangChain 其實有提供 1 個儲存各種 prompt 的服務,還像 GitHub 那樣支援版本控制,該服務稱為 LangChain Hub ,除了自己寫 prompt 之外,其實也可以用 LangChain Hub 上的 prompt 。
使用方法也很簡單:
from langchain import hub
promot = hub.pull("......")
剛好我們範例使用的 prompt 在 LangChain Hub 也能找到類似的 prompt(頁面在此),所以先前的範例還可以進一步改成:
import requests
from langchain.globals import set_debug
#set_debug(True)
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.messages import AIMessage, HumanMessage
from langchain.agents.format_scratchpad.openai_tools import (
format_to_openai_tool_messages,
)
from langchain.agents.output_parsers.openai_tools import OpenAIToolsAgentOutputParser
from langchain.agents import tool
from langchain.agents import AgentExecutor
@tool
def check_site_alive(site: str) -> bool:
"""Check a site is alive or not."""
try:
resp = requests.get(f'https://{site}')
resp.raise_for_status()
return True
except Exception:
return False
tools = [check_site_alive, ]
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
llm_with_tools = llm.bind_tools(tools)
prompt = hub.pull('hwchase17/openai-functions-agent')
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(
x["intermediate_steps"]
),
"chat_history": lambda x: x["chat_history"],
}
| prompt
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, return_intermediate_steps=True)
chat_history = []
input_text = input('>>> ')
while input_text.lower() != 'bye':
if input_text:
response = agent_executor.invoke({
'input': input_text,
'chat_history': chat_history,
})
chat_history.extend([
HumanMessage(content=input_text),
AIMessage(content=response["output"]),
])
print(response['output'])
input_text = input('>>> ')
看起來是否更簡單了呢!
如果以後想寫某些 prompt, 其實可以先到 LangChain Hub 上找找看有沒有類似的,可以省去自己寫 prompt 的時間。
總結
我們一路從單純的語言模型應用到聊天機器人,再到能夠執行客製化功能的 Agents, 這過程已經對 LangChain 與語言模型的應用有大致的認識,雖然做出好的語言模型應用絕對不是一件簡單的事,但在 LangChain 的幫助下,至少不會是一件超級困難的事了!
接下來的系列文,除了進一步介紹 LangChain 其他功能之外,我們將開始著墨在更多關於 LangChain 的細節上。
以上!
Enjoy!
References
LangChain - Defining Custom Tools