用 LangChain 構建基於資料庫的問答機器人（四）：通過代理使用外部工具
Published: 2023-07-25 00:00:00

上一篇教程我們介紹了 ReAct 系統，這是一個非常強大的行為模式，但它需要編寫大量的示例來告訴 LLM 如何思考、行動，並且為了遵循這個模式，還需要編寫代碼來分析生成文字、調用函數、拼接 prompt 等，這些工作都是十分繁瑣且冗長的。而 LangChain 幫我們把這些步驟都隱藏起來了，將這一系列動作都封裝成 “代理”，我們只需要提供有那些工具可以使用，以及這些工具的功能，就可以讓 LLM 自動完成這些工作了。這樣我們的代碼就會變得簡潔、易讀，並且擁有更高的靈活性。

# 代理（Agent）

所謂代理，就是將一系列的行為和可以使用的工具封裝起來，讓我們可以通過一個簡單的接口來調用這些動作，而不需要關心這些動作是如何完成的。這樣可以讓我們的代碼更簡潔、易讀，並且擁有更高的靈活性。接下來我們就結合 ReAct 和向量數據庫來介紹代理的使用。

## 準備工作

和之前一樣，我們先設置環境變量。

```python
# set the environment variables needed for openai package to know to reach out to azure
import os

os.environ["OPENAI_API_TYPE"] = "azure"
os.environ["OPENAI_API_BASE"] = "https://<your-endpoint.openai.azure.com/"
os.environ["OPENAI_API_KEY"] = "your AzureOpenAI key"
os.environ["OPENAI_API_VERSION"] = "2023-03-15-preview"
```

接下來我們先把 llm 類創建好。

```python
from langchain.chat_models import AzureChatOpenAI
from langchain.embeddings import OpenAIEmbeddin Whether to return only outputs in the
                response. If True, only new keys generated by this chain will be
                returned. If False, both input keys and new keys generated by this
                chain will be returned. Defaults to False.gs

llm = AzureChatOpenAI(deployment_name="gpt-35-turbo", temperature=0)
embeddings = OpenAIEmbeddings(deployment="text-embedding-ada-002", chunk_size=1)
```

因為我們的向量數據庫已經在之前的教程中創建好了，所以我們用 `from_existing_index` 直接連接到數據庫。這裡 `index_name` 是我們之前創建數據庫時指定的名字。另外不同的數據庫所使用的鏈接方法可能不同，具體還是要參考各個數據庫的[文檔](https://python.langchain.com/docs/modules/data_connection/vectorstores/)。

```python
from langchain.vectorstores.redis import Redis

rds = Redis.from_existing_index(
    embeddings, redis_url="redis://localhost:6379", index_name="link"
)
retriever = rds.as_retriever()
```

## 創建工具

雖然代理可以讓 llm 使用外部工具，但我們首先要創建工具，並為工具編寫描述。我們的目的是為了要能夠讓 llm 根據問題從數據庫中檢索出答案，因此我們創建的工具一定是與此相關的。我們可以將檢索器的搜索功能包裝成一個函數，然後以此作為工具，但我們完全可以用之前教程中所介紹的 `RetrievalQA` Chain ，它的輸出本就是字符串，而且已經先將相關的資料進行了整理，這顯然比檢索器更高效。

```python
from langchain.chains import RetrievalQA

qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=retriever)
```

然後用 `Tool` 將上面創建的 Chain 包裝起來。其中 `func` 參數是告訴代理系統要調用哪個函數，`description` 是對這個工具的描述。我們將包裝好的工具放在一個列表中，這樣就是告訴系統他可以使用的工具有哪些了。

```python
from langchain.agents import Tool

tools = [
    Tool(
        name="Contract QA System",
        func=qa.run,
        description="useful for when you need to answer questions about contracts."
    )
]
```

## 創建代理

我們用 `initialize_agent` 函數來創建代理。這裡 `agent` 參數是告訴系統我們要創建的代理類型，這裡我們創建的是 `CHAT_ZERO_SHOT_REACT_DESCRIPTION`，名字中的 `CHAT` 表示這是一個聊天代理，因為我們用的是聊天模型；`ZERO_SHOT` 表示它的 prompt 沒有編寫示例，而是僅通過描述來引導 llm 的行為。

```python
agent_chain = initialize_agent(
    tools, llm, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
```

然後用 `run` 方法來運行代理。

```python
agent_chain.run("在什麼情況下可以單方面解除合同？")
```

```text
> Entering new  chain...
Thought: This question is asking about the circumstances under which a contract can be unilaterally terminated.

Action:
\```
{
  "action": "Contract QA System",
  "action_input": "在什麼情況下可以單方面解除合同？"
}
\```


Observation: 根據文本，可以單方面解除合同的情況包括：

- 在承擔國家重大科研項目期間；
- 掌握重大科技成果關鍵技術和資料，未脫離保密期；
- 被審查期間或因經濟問題未作結案處理之前；
- 合同訂立時所依據的客觀情況發生重大變化，致使合同無法履行，經甲乙雙方協商不能就變更合同達成協議的；
- 甲方提出解除本合同，乙方同意解除的；
- 乙方患病或者非因工負傷，醫療期滿後，不能從事原工作也不能從事由甲方安排的其他工作，甲方單方面解除本合同的；
- 乙方年度考核或者聘期考核不合格，又不同意甲方調整其工作崗位的；
- 在試用期內被證明不符合本崗位要求；
- 連續曠工超過 10 個工作日或者 1 年內累計曠工超過 20 個工作日的；
...

Final Answer: 根據文本，可以單方面解除合同的情況有很多種，包括在承擔國家重大科研項目期間、合同訂立時所依據的客觀情況發生重大變化、甲方提出解除本合同且乙方同意解除等。此外，乙方在試用期內、考入普通高等院校、被錄用或者選調為公務員、依法服兵役的情況下也可以隨時單方面解除合同。如果乙方提出解除合同未能與甲方協商一致，乙方應當堅持正常工作，繼續履行合同；6個月後再次提出解除合同仍未能與甲方協商一致的，即可單方面解除合同。

> Finished chain.

'根據文本，可以單方面解除合同的情況有很多種，包括在承擔國家重大科研項目期間、合同訂立時所依據的客觀情況發生重大變化、甲方提出解除本合同且乙方同意解除等。此外，乙方在試用期內、考入普通高等院校、被錄用或者選調為公務員、依法服兵役的情況下也可以隨時單方面解除合同。如果乙方提出解除合同未能與甲方協商一致，乙方應當堅持正常工作，繼續履行合同；6個月後再次提出解除合同仍未能與甲方協商一致的，即可單方面解除合同。 '
```

這樣我們很容易的完成 ReAct 的流程，並且它確實可以從資料源中檢索出答案。用 LangChain 我們就可以用簡潔的代碼完成如此復雜的任務，並且也不需要編寫 prompt，這樣我們就可以更專注於業務的邏輯了。