歡迎來到 PocketFlow 教學的第七章!在上一章 異步處理 (Async) 中,我們學習了如何讓我們的節點和流程在處理像是呼叫外部 API 或等待使用者輸入這類耗時的 I/O 操作時,不會阻塞整個應用程式的運行,使得應用程式能更流暢地回應。
然而,到目前為止,我們建立的流程 (Flow) 大多還是遵循著預先定義好的路徑。如果我們希望流程能更「聰明」一點,能像一位經驗豐富的助手那樣,根據當下的情況動態地決定下一步該做什麼呢?例如,我們提出一個問題,它能自行判斷是應該先上網搜尋資料,還是直接嘗試回答?這就是「代理人/智能體 (Agent)」概念要解決的問題。
想像一下,您正在建構一個研究助理的自動化流程。您給它一個問題,例如「2024 年諾貝爾物理學獎的得主是誰?」。一個固定的流程可能只會執行單一操作,例如直接搜尋網路。但一個更智能的代理人可能會:
這種動態決策的能力,就是代理人的核心價值。它讓我們的流程不再是死板的指令序列,而是能根據情境做出判斷的智能實體。
代理人/智能體 (Agent) 是一種設計模式,賦予流程動態決策的能力。就像一位經驗豐富的專案經理,能根據當前情境(上下文資訊)、歷史行動和可用工具(行動空間),自主判斷並選擇下一步最合適的行動。這通常透過一個特殊的節點 (Node)(我們稱之為「代理人節點」)實現,該節點利用大型語言模型(LLM)分析上下文並決定流程的走向,可以實現如搜索、回答、或其他自定義工具調用等複雜行為。
圖示:代理人根據情境選擇下一步行動
在 PocketFlow 中,一個代理人通常由以下幾個關鍵部分組成:
代理人節點 (Agent Node):這是代理人的「大腦」。它通常是一個特殊的節點 (Node),其 exec
方法會:
post
方法的回傳值,以指導流程 (Flow) 的走向。工具節點 (Tool Nodes):這些是執行具體任務的普通節點 (Node)。例如,一個 SearchWebNode
(搜尋網路的節點)、一個 CalculateNode
(執行計算的節點)或一個 AnswerQuestionNode
(產生答案的節點)。代理人節點會決定呼叫哪個工具節點。
流程 (Flow):將代理人節點和工具節點連接起來。流程的設計通常包含:
共享儲存 (Shared Store):用於在代理人節點和工具節點之間傳遞數據,包括:
讓我們來實作一個簡易的研究代理人。它會接收一個問題,然後決定是直接回答,還是先進行網路搜尋。
我們需要兩個工具節點:一個用於搜尋,一個用於回答。
SearchWebNode
(搜尋網路節點)
from pocketflow import Node
import yaml # 用於解析 LLM 的結構化輸出
# 模擬的網路搜尋函數
def search_web_mock(query: str) -> str:
print(f"🔎 模擬搜尋:{query}")
if "諾貝爾物理學獎" in query:
return "研究顯示,2024年諾貝爾物理學獎的資訊尚未廣泛公布。"
return "找不到相關資訊。"
class SearchWebNode(Node):
def prep(self, shared):
# 從共享儲存獲取代理人決定的搜尋詞
return shared.get("current_search_term", "未知搜尋詞")
def exec(self, search_term: str):
return search_web_mock(search_term)
def post(self, shared, prep_res, exec_res: str):
# 將搜尋結果添加到共享儲存的上下文中
history = shared.get("history", [])
history.append({"action": "search", "term": prep_res, "result": exec_res})
shared["history"] = history
print(f"📝 搜尋結果已記錄:{exec_res[:30]}...")
return "action_completed" # 行動完成,返回給代理人決策
prep
: 從共享儲存 (Shared Store) 取得由代理人節點決定的 current_search_term
。exec
: 執行模擬的網路搜尋。post
: 將搜尋的行動和結果記錄到共享儲存 (Shared Store) 的 history
中,並回傳 "action_completed"
,表示此工具已執行完畢,流程應回到代理人節點進行下一步決策。AnswerQuestionNode
(回答問題節點)
# 模擬的 LLM 回答函數
def llm_answer_mock(query: str, context: str) -> str:
print(f"💬 模擬 LLM 根據上下文回答:'{query}' (上下文:'{context[:30]}...')")
if "諾貝爾物理學獎" in query and "尚未廣泛公布" in context:
return "根據目前搜尋到的資訊,2024年諾貝爾物理學獎的得主資訊尚未廣泛公布。"
return "我需要更多資訊才能回答這個問題。"
class AnswerQuestionNode(Node):
def prep(self, shared):
query = shared.get("original_query", "未知問題")
history_str = str(shared.get("history", [])) # 將歷史轉換為字串
return query, history_str
def exec(self, inputs: tuple):
query, history_context = inputs
return llm_answer_mock(query, history_context)
def post(self, shared, prep_res, exec_res: str):
shared["final_answer"] = exec_res
print(f"✅ 最終答案:{exec_res}")
# 預設回傳 "default",通常表示代理任務的一個分支結束
prep
: 從共享儲存 (Shared Store) 獲取原始問題和所有歷史記錄作為上下文。exec
: 呼叫模擬的 LLM 來根據上下文產生答案。post
: 將最終答案儲存到共享儲存 (Shared Store)。這是代理人的核心,我們稱之為 DecideActionNode
。
# 模擬的 LLM 決策函數
def llm_decide_action_mock(prompt: str) -> str:
print("🤔 代理人正在思考...")
print(f"提示 (前100字):{prompt[:100]}...")
# 簡化邏輯:如果歷史記錄為空,則搜尋;否則回答
if "Previous Actions: []" in prompt or "Previous Actions: No previous actions" in prompt :
response_yaml = """
action: search
parameters:
search_term: "2024年諾貝爾物理學獎得主"
"""
else:
response_yaml = """
action: answer
parameters: {}
"""
print(f"代理人 LLM 回應 (模擬):\n{response_yaml}")
return response_yaml
class DecideActionNode(Node):
def prep(self, shared):
query = shared.get("original_query", "沒有提供問題")
history = shared.get("history", []) # 先前行動的歷史
# 建立上下文資訊
context_info = f"原始問題:{query}\n先前行動:{history if history else '無先前行動'}"
return context_info
def exec(self, context: str):
# 建立給 LLM 的提示
prompt = f"""
### 情境
{context}
### 可用行動
1. search: 進行網路搜尋以獲取資訊。
參數:
- search_term (str): 要搜尋的關鍵字。
2. answer: 根據目前擁有的資訊回答問題。
參數: (無)
### 下一步行動
根據當前情境和可用行動,決定下一步。
請以 YAML 格式回傳您的思考過程和決定:
```yaml
thinking: |
(您的思考步驟)
action: <行動名稱>
parameters:
<參數名稱>: <參數值>
""" llm_response_yaml = llm_decide_action_mock(prompt) # 解析 LLM 的 YAML 回應 # 在真實應用中,這裡需要更穩健的 YAML 解析和錯誤處理 try: parsed_response = yaml.safe_load(llm_response_yaml) if not isinstance(parsed_response, dict) or "action" not in parsed_response: # 如果解析失敗或格式不對,預設為一個安全動作,例如再次嘗試或結束 print("⚠️ LLM 回應格式錯誤,將嘗試結束。") return {"action": "error_fallback", "thinking": "LLM 回應格式錯誤"} except yaml.YAMLError: print("⚠️ LLM 回應 YAML 解析失敗,將嘗試結束。") return {"action": "error_fallback", "thinking": "LLM 回應 YAML 解析失敗"}
return parsed_response # 例如:{"action": "search", "parameters": {"search_term": "..."}}
def post(self, shared, prep_res, exec_res: dict):
chosen_action = exec_res.get("action", "error_fallback")
parameters = exec_res.get("parameters", {})
print(f"代理人決定行動:{chosen_action},參數:{parameters}")
if chosen_action == "search":
shared["current_search_term"] = parameters.get("search_term")
# 其他行動的參數也可以在這裡設定到 shared store
# 將 LLM 的思考過程也記錄下來,方便除錯
shared.setdefault("agent_thoughts", []).append(exec_res.get("thinking", ""))
return chosen_action # 回傳 LLM 決定的行動名稱
* `prep`: 從[共享儲存 (Shared Store)](02_共享儲存__shared_store__.md) 收集原始問題和歷史行動。
* `exec`:
1. 建構一個詳細的提示,包含當前情境和可用的行動空間(`search` 和 `answer`)。
2. 呼叫(模擬的)LLM `llm_decide_action_mock`,它會回傳一個 YAML 格式的字串,指明選擇的行動和所需參數。
3. 解析 YAML 回應。
* `post`:
1. 從解析後的 LLM 回應中提取行動名稱和參數。
2. 如果行動是 `search`,則將 `search_term` 存入[共享儲存 (Shared Store)](02_共享儲存__shared_store__.md),供 `SearchWebNode` 使用。
3. 回傳 LLM 選擇的行動名稱(例如 `"search"` 或 `"answer"`)。這個回傳值將被[流程 (Flow)](03_流程__flow__.md) 用來決定下一個執行的節點。
### 步驟 3:建立流程 (Flow) 並連接節點
現在我們將這些節點連接起來,形成一個可以循環決策的流程。
```python
from pocketflow import Flow
# 建立節點實例
decide_node = DecideActionNode()
search_node = SearchWebNode()
answer_node = AnswerQuestionNode()
# 定義轉換規則
# 代理人決定搜尋 -> 執行搜尋節點
decide_node - "search" >> search_node
# 代理人決定回答 -> 執行回答節點
decide_node - "answer" >> answer_node
# 代理人發生錯誤或無法決策 -> 結束 (這裡我們可以連到一個結束節點或不連)
# decide_node - "error_fallback" >> some_end_node
# 搜尋節點完成後 -> 回到代理人節點進行下一步決策
search_node - "action_completed" >> decide_node
# 建立流程,以代理人節點為起始點
research_agent_flow = Flow(start=decide_node)
這個流程的關鍵在於 search_node - "action_completed" >> decide_node
,它使得在執行完搜尋後,流程會回到 decide_node
,讓代理人可以根據新的搜尋結果再次決策。
# 準備共享儲存,放入初始問題
shared_data = {"original_query": "2024年諾貝爾物理學獎得主是誰?"}
print(f"🚀 開始研究代理人流程,問題:{shared_data['original_query']}")
research_agent_flow.run(shared_data)
print("✨ 研究代理人流程結束")
if "final_answer" in shared_data:
print(f"\n💡 代理人的最終答案:{shared_data['final_answer']}")
print(f"📜 代理人歷史行動:{shared_data.get('history')}")
當您運行此程式碼時,您將看到代理人首先決定搜尋,然後執行搜尋,接著根據搜尋結果(在此模擬中,結果表明資訊不足),再次決策並選擇回答。
預期輸出範例(簡化):
🚀 開始研究代理人流程,問題:2024年諾貝爾物理學獎得主是誰?
🤔 代理人正在思考...
提示 (前100字):
### 情境
原始問題:2024年諾貝爾物理學獎得主是誰?
先前行動:無先前行動
### 可用行動
1. search: 進行網路搜尋以獲取資訊。
...
代理人 LLM 回應 (模擬):
action: search
parameters:
search_term: "2024年諾貝爾物理學獎得主"
代理人決定行動:search,參數:{'search_term': '2024年諾貝爾物理學獎得主'}
🔎 模擬搜尋:2024年諾貝爾物理學獎得主
📝 搜尋結果已記錄:研究顯示,2024年諾貝爾物理學獎的資訊尚未廣泛公...
🤔 代理人正在思考...
提示 (前100字):
### 情境
原始問題:2024年諾貝爾物理學獎得主是誰?
先前行動:[{'action': 'search', 'term': '2024年諾貝爾物理學獎得主', 'result': '研究顯示,2024年諾貝爾物理學獎的資訊尚未廣泛公布。'}]
...
代理人 LLM 回應 (模擬):
action: answer
parameters: {}
代理人決定行動:answer,參數:{}
💬 模擬 LLM 根據上下文回答:'2024年諾貝爾物理學獎得主是誰?' (上下文:"{'action': 'search', 'term': '...")
✅ 最終答案:根據目前搜尋到的資訊,2024年諾貝爾物理學獎的得主資訊尚未廣泛公布。
✨ 研究代理人流程結束
💡 代理人的最終答案:根據目前搜尋到的資訊,2024年諾貝爾物理學獎的得主資訊尚未廣泛公布。
📜 代理人歷史行動:[{'action': 'search', 'term': '2024年諾貝爾物理學獎得主', 'result': '研究顯示,2024年諾貝爾物理學獎的資訊尚未廣泛公布。'}]
這個簡單的例子展示了代理人如何透過 LLM 進行動態決策,並在不同的工具節點間導航。
要建立出色的代理人,以下幾點至關重要:
上下文管理 (Context Management):
行動空間設計 (Action Space Design):
read_databases
和 read_csvs
兩個行動。更好的做法可能是將 CSV 匯入資料庫,然後只提供一個統一的資料庫讀取行動。代理人在 PocketFlow 中的實現,並非依賴一個名為 Agent
的特殊類別,而是一種設計模式,巧妙地結合了我們已經學過的節點 (Node)、流程 (Flow) 和共享儲存 (Shared Store)。
非程式碼逐步解析:
DecideActionNode
)。prep
:從共享儲存 (Shared Store) 收集當前的任務描述、歷史行動、以及任何由先前工具節點產生的結果。exec
:exec
方法解析 LLM 的回應。post
:"search"
或 "answer"
)作為此節點的行動指令。decide_node - "search" >> search_node
),決定下一個要執行的工具節點。SearchWebNode
進行網路搜尋)。它會從共享儲存 (Shared Store) 讀取所需的參數,並將其執行結果寫回共享儲存 (Shared Store)。post
方法通常會回傳一個特定的行動指令,該指令會將流程 (Flow) 導回到代理人節點。序列圖 (Sequence Diagram) 概覽:
這個序列圖展示了代理人流程如何協調決策節點、工具節點、LLM 和共享儲存,以完成一個動態的任務。
在本章中,我們深入探索了 PocketFlow 中的「代理人/智能體 (Agent)」設計模式:
代理人模式開啟了自動化流程的全新可能性,讓它們能夠處理更複雜、更需要「智能」的任務。
到目前為止,我們的代理人依賴於即時收集的上下文。但如果我們希望代理人能夠利用一個龐大且預先準備好的知識庫來增強其決策和回答能力呢?這就引出了我們下一章,也是本教學系列最後一章的主題:檢索增強生成 (RAG)。