LangChain 怎麼玩? Agents 篇,來整合一些客製化的功能/工具吧

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

你有沒有特別想過如果我們開發的功能要怎麼跟語言模型進行結合?畢竟語言模型如果只能做聊天應用的話,那麼它的應用範圍就相當侷限。

這個問題的解答就是 LangChain 的 Agents 。

Agents 可以讓我們把自己開發的功能接上語言模型,讓語言模型執行我們所開發的功能!

本文同樣以 1 個簡單的範例開始,帶大家建立自己的 Agents 。

本文環境

由於 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-platform.png

如果你的 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 種:

  1. OpenAI Tools
  2. OpenAI Functions
  3. XML
  4. Structured Chat
  5. JSON Chat
  6. ReAct
  7. 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 作為範例。

openai-tools.png

關於上述 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 所需要的格式,特別需要注意的是此處 inputagent_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

最後,重中之重就是用 AgentExecutoragenttools 組成 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('>>> ')

上述程式碼修改的部分是:

  1. 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"),
])
  1. 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()
)
  1. 把每一次對話紀錄存到 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 - Agent Types

LangChain - Agent Concepts

LangChain - Custom agent

LangChain - Defining Custom Tools

LangChain - Tools

對抗久坐職業傷害

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

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

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

贊助我們的創作

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

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