從零寫出 Agent 迴圈
把第一堂的白話流程寫成真正能執行的 Python 程式:呼叫模型、執行它要求的工具、把結果交回去,直到它完成任務為止,並加上步數上限避免無限迴圈。
這是這個單元最核心的一堂課。前兩堂你已經知道 Agent 是「模型+工具+迴圈」,也寫好了工具說明書,看到模型回傳 tool_use。現在要補上最後一塊:真的執行工具,並把結果交回去。
完成後,你會有一個不到一百行、不依賴任何框架的 Agent。程式有點長,但每一段都很單純,我們一段一段來。
先看懂一來一回的格式
還記得 LLM API 單元的多輪對話嗎?每一輪都把整份 messages 送出去。Agent 迴圈也一樣,只是多了兩種新的內容區塊:
工具結果是放在 user 訊息裡送回去的,並且用 id 對應到當初的請求。
送回工具結果的格式是這樣:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01...", # 抄模型給的 block.id
"content": "檔案內容……",
}
],
}
如果模型一次要求好幾個工具,就把所有 tool_result 放在同一則 user 訊息的清單裡一起送回。
準備工作資料夾
在程式旁邊建立一個 workspace 資料夾,放一個 notes.txt,內容隨意,例如:
本週待辦
- 週三前交出季報
- 預約牙醫
- 幫媽媽訂生日蛋糕
Agent 只能讀這個資料夾裡的東西,這是我們的第一道安全措施。
完整程式
建立 agent.py:
from pathlib import Path
from dotenv import load_dotenv
import anthropic
load_dotenv()
MODEL = "claude-sonnet-5-5" # 可以換成其他模型 ID
client = anthropic.Anthropic()
WORKSPACE = Path("workspace").resolve()
MAX_STEPS = 10 # 最多跑幾輪,避免無限迴圈
SYSTEM = "你是一個檔案助理,只能透過工具查看 workspace 資料夾。用繁體中文回答。"
TOOLS = [
{
"name": "list_files",
"description": "列出 workspace 資料夾裡所有檔案的相對路徑。想知道有哪些檔案時先用這個。",
"input_schema": {"type": "object", "properties": {}, "additionalProperties": False},
},
{
"name": "read_file",
"description": "讀取 workspace 裡一個文字檔的內容。path 是 list_files 回傳的相對路徑。",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string", "description": "相對路徑,例如 notes.txt"}},
"required": ["path"],
"additionalProperties": False,
},
},
]
def safe_path(relative: str) -> Path:
"""把相對路徑轉成真實路徑,並確認它沒有跑出 workspace。"""
path = (WORKSPACE / relative).resolve()
if not path.is_relative_to(WORKSPACE):
raise ValueError("不允許存取 workspace 以外的路徑")
return path
def run_tool(name: str, args: dict) -> str:
if name == "list_files":
files = [str(p.relative_to(WORKSPACE)) for p in WORKSPACE.rglob("*") if p.is_file()]
return "\n".join(files) or "(資料夾是空的)"
if name == "read_file":
return safe_path(args["path"]).read_text(encoding="utf-8")
raise ValueError(f"沒有這個工具:{name}")
def run_agent(task: str) -> str:
messages = [{"role": "user", "content": task}]
for step in range(1, MAX_STEPS + 1):
response = client.messages.create(
model=MODEL,
max_tokens=16000,
system=SYSTEM,
tools=TOOLS,
messages=messages,
)
# 1. 模型的完整回應原封不動存回歷史
messages.append({"role": "assistant", "content": response.content})
# 2. 模型說完了:回傳最後的文字
if response.stop_reason == "end_turn":
return "".join(b.text for b in response.content if b.type == "text")
# 3. 不是要用工具、也不是說完了(例如被截斷或拒絕):停下來
if response.stop_reason != "tool_use":
return f"(Agent 意外停止,原因:{response.stop_reason})"
# 4. 執行模型要求的每一個工具
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"[第 {step} 步] 呼叫 {block.name},參數 {block.input}")
try:
result = run_tool(block.name, block.input)
is_error = False
except Exception as e:
result = f"錯誤:{e}"
is_error = True
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
"is_error": is_error,
})
# 5. 所有結果放在同一則 user 訊息送回去,進入下一輪
messages.append({"role": "user", "content": tool_results})
return "(已達步數上限,任務沒有完成)"
if __name__ == "__main__":
task = input("請輸入任務:")
print("\n" + run_agent(task))
執行看看:
python agent.py
輸入「幫我看看資料夾裡有什麼,整理出所有待辦事項」,你會看到類似:
[第 1 步] 呼叫 list_files,參數 {}
[第 2 步] 呼叫 read_file,參數 {'path': 'notes.txt'}
你這週有三件待辦:……
模型自己決定先列檔案、再讀檔案、最後回答。你沒有寫死這個順序。這就是 Agent。
逐段拆解
run_tool:真正動手的地方。模型只給名稱和參數,由這個函式決定怎麼執行。
safe_path:把模型給的路徑轉成真實路徑後,檢查它還在 workspace 裡面。模型給的參數不能完全信任,例如 ../../重要文件,這一行會把它擋下來。
步驟 1:把 response.content 整份存回去。上一個單元說過原因,這裡更重要:模型下一輪必須看到自己剛剛提出的 tool_use,才能對上你送回的結果。
步驟 3:除了 tool_use 和 end_turn,還可能出現 max_tokens(回答被截斷)或 refusal(拒絕執行)。遇到這些情況,直接停下來比盲目繼續安全。
步驟 4 的 try:檔案不存在、路徑不合法時,不要讓整個程式崩潰。把錯誤訊息交回給模型,並標記 is_error,它通常會自己換個方法,例如先用 list_files 確認正確檔名。
MAX_STEPS:如果模型一直重複呼叫工具、遲遲不結束,迴圈最多跑十輪就停。每一輪都是一次付費的 API 呼叫,這個上限保護你的帳單。
這就是框架在做的事
你剛剛寫的,就是各種 Agent 框架的核心。Anthropic SDK 也有一個 tool runner 可以幫你跑這個迴圈,OpenAI、Google 的 SDK 也有類似功能。差別只在於它們幫你處理了更多細節。現在你知道裡面在發生什麼了,之後無論用哪一套,遇到問題都能看懂。
這是我手寫的 Agent 迴圈 agent.py。請幫我新增一個 search_text 工具:在 workspace 所有 .txt 檔裡搜尋關鍵字,回傳出現的檔名與那一行內容。請沿用現有的 safe_path 檢查,並更新 TOOLS 與 run_tool。
- 建立
workspace和notes.txt,執行agent.py,觀察模型用了哪些工具、順序為何。 - 在
workspace裡多放兩個檔案,例如shopping.txt和ideas.txt,問一個需要讀好幾個檔案才答得出來的問題。 - 請 Agent 讀一個不存在的檔案,觀察它收到錯誤後怎麼調整。
- 把
MAX_STEPS改成1再執行,確認程式會在一輪後停下並印出提示。
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。