Stage 3 — 工具使用與第一個 Agent Loop ⭐¶
這一關要做一件事:讓模型填一張「工具工作單」,再由你的程式檢查、執行並把結果送回去。這個來回就是你的第一個 Agent Loop。
📌 學習目標¶
完成後,你可以:
- 說出
schema → call → execute → result → answer五個步驟。 - 定義一個工具,檢查參數,再安全地執行對應函式。
- 不靠 framework,寫出有次數上限和停止條件的 Agent Loop。
- 分清 Function Calling 與 Structured Output,不再把兩者當成同一件事。
- 用固定題目比較 schema 或模型,而不是靠一次結果下結論。
🚪 進入條件¶
你能執行一個 Python 檔、看懂 function 與 dict,並完成 Stage 02,就可以開始。環境還沒好時,先回 Stage 00。
🧩 先認識八個核心詞¶
Tool Use(工具使用)¶
模型需要外部資料或動作時,先提出工具請求。像孩子請大人幫忙開高處的盒子;模型提出要做什麼,程式才真正動手。本章用它查天氣和做計算。模型本身不會執行你的 client tool。
Function Calling(函式呼叫)¶
模型按照約定格式,回傳要呼叫的函式名稱與參數。像填一張有固定欄位的工作單。本章用它把自然語言問題變成程式能讀的請求。不同供應商的訊息格式不完全相同。
Tool Schema(工具綱要)¶
JSON(JavaScript Object Notation) 是程式交換資料的文字格式。
Schema 是工具的說明卡:名字、用途、可填欄位和資料型別。像點餐單告訴客人能點什麼。本章會用 JSON Schema 描述工具。Schema 能約束外形,但程式仍要驗證數值、權限與業務規則。
Tool Call(工具請求)¶
Tool Call 是模型填好的工作單,包含工具名稱、call ID 和參數。像「請查台北,單位用攝氏」。本章的程式會先讀它,再從 allowlist 找到合法函式。它是請求,不是執行結果。
Tool Result(工具結果)¶
Tool Result 是程式做完事後交回的資料,並用 call ID 對回原請求。像廚房把完成的餐點放回正確桌號。本章會把成功或錯誤結果送回模型。外部結果可能不可信,不能當成最高優先指令。
Agent Loop(Agent 執行迴圈)¶
程式重複「問模型 → 執行工具 → 回傳結果」,直到得到答案或碰到上限。像照食譜一步一步做,完成就停。完整來回是 model → tool call → execute → tool result → model。本章的 working definition 是 模型 + 工具 + 有界迴圈;這是學習用定義,不是所有 Agent 的唯一學術定義。
ReAct(Reasoning + Acting)¶
ReAct 會交替決定下一步、採取 action、查看 observation,再繼續。像找鑰匙時先看桌上,沒看到再查抽屜。本章寫的是 ReAct-inspired 的可觀察工具迴圈;不要求模型公開私人 Chain-of-Thought。
Structured Output(結構化輸出)¶
模型直接交回固定形狀的資料,例如符合 schema 的 JSON。像把答案填進表格。本章用它和 Function Calling 對照:前者要資料,後者要程式採取動作。即使外形合法,內容仍可能錯、被拒答或被截斷。
先選對方法¶
| 你要什麼 | 先用什麼 | 例子 |
|---|---|---|
| 只要文字答案 | 一般模型回答 | 改寫一封信 |
| 要固定形狀的資料 | Structured Output | 抽出姓名與日期 |
| 要查即時資料或採取動作 | Function Calling / Tool Use | 查天氣、建立工單 |
⚠️ 寫第一個 Agent 前的五條底線¶
- 只執行 allowlist 裡的工具,不用模型輸出的名字做任意函式呼叫。
- 把工具參數當成不可信輸入;先檢查型別、範圍和權限。
- 工具只拿完成任務需要的最小權限。
- 刪除、付款、寄信等高風險動作,執行前要讓人確認。
- 設定最大輪數、timeout 和費用上限;不能讓 Agent 無限繞圈。
📚 必修閱讀¶
依序讀:
- Ollama Tool Calling ⭐⭐⭐⭐⭐ — 先看 single tool 與 multi-turn loop。
- Anthropic — How Tool Use Works ⭐⭐⭐⭐⭐ — 看清楚模型、應用程式和 tool result 各自負責什麼。
- ReAct paper ⭐⭐⭐⭐ — 先讀 abstract;知道 Reasoning + Acting 的來源,不必一次讀完公式。
展開先備知識、環境、時間與預算
先備知識:能執行 Python、看懂 list/dict/function,並完成 Stage 02。
本機主路徑:Ollama + qwen2.5:3b。這是專案依使用者安裝驗證保留的入門模型,不代表它在每個 schema 都最好。
ollama pull qwen2.5:3b
ollama serve
python -m pip install "openai>=3.3,<4"
雲端比較路徑:Anthropic + pinned Haiku model ID。
$env:ANTHROPIC_API_KEY="貼上你的金鑰"
python -m pip install "anthropic>=1.0,<2"
macOS/Linux 設定方式是 export ANTHROPIC_API_KEY="貼上你的金鑰"。金鑰不要寫進程式或 commit。
時間:先跑練習 1–3 約 2–3 小時;再做練習 4–6 約 3–5 小時。完整主動路線合計約 5–8 小時。
費用計算方式:
費用 = 輸入 tokens ÷ 1,000,000 × input price
+ 輸出 tokens ÷ 1,000,000 × output price
2026-08-27 查核時,Claude Haiku 4.5 是 $1 / $5(input / output,每百萬 tokens)。若一次請求用 2,000 input + 1,000 output,範例費用約 $0.007。工具迴圈會發出多次請求;每題先保留 $0.05、全章五輪實驗先設 $1 provider spend limit。這是保守上限,不是帳單保證。
Path A 的 API 費用是 $0;仍會使用你的硬體、記憶體與電力。
Agent 的經典範式(thinking patterns)¶
展開 CoT、ReAct、Reflection 與 Planning 的差別
| 名詞 | 白話用途 | 放在哪裡學 |
|---|---|---|
| Chain-of-Thought(CoT) | 早期 prompt 技巧常要求寫出中間推理。現在不把完整私人思維鏈當成通用輸出要求;需要檢查時,看最後答案與簡短、可驗證的理由 | Stage 02 |
| ReAct | 在迴圈中交替採取 action、讀 observation、再決定下一步 | 本章練習 3 |
| Reflection | 用一次回饋改下一次嘗試的廣義做法 | 本章下方路由 |
| Reflexion/Self-Refine | 有明確 Actor/Critic 或自我回饋流程的研究 pattern | 本章概念;持久記憶版到 Stage 06 |
| Planning | 先拆成多步,再依結果調整計畫 | Stage 07.5 |
這些詞描述不同解題方式,不是 Agent 的唯一判定表。Computer-use、CodeAct 和 workflow agent 也可能使用不同 loop。
🛠 動手練習¶
先完成練習 1–3。練習 4–6 用來讓迴圈更穩,不需要一天全部做完。
練習 1:Function Calling(一個工具、一次呼叫)¶
完成後,你會看到模型先產生 get_weather Tool Call,程式執行它,再由模型用 Tool Result 回答。
如果你偏好用檔案實作,可直接開啟練習 1 完整資料夾。
第一步:複製並執行 ollama pull qwen2.5:3b。接著展開 Path A,把完整程式直接複製成 hello_tool.py。
Path A:Ollama 完整可複製範例(API 費 $0)
import json
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
TOOLS = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "取得指定城市的示範天氣資料",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名稱,例如台北"},
"unit": {"type": "string", "enum": ["celsius"]},
},
"required": ["city", "unit"],
"additionalProperties": False,
},
},
}]
def get_weather(city: str, unit: str) -> dict:
if unit != "celsius":
raise ValueError("只接受 celsius")
return {"city": city, "temperature": 26, "unit": unit}
messages = [{"role": "user", "content": "台北現在幾度?"}]
first = client.chat.completions.create(
model="qwen2.5:3b", messages=messages, tools=TOOLS
)
assistant = first.choices[0].message
messages.append(assistant.model_dump(exclude_none=True))
for call in assistant.tool_calls or []:
if call.function.name != "get_weather":
raise ValueError(f"不允許的工具:{call.function.name}")
args = json.loads(call.function.arguments)
if (
not isinstance(args, dict)
or set(args) != {"city", "unit"}
or not isinstance(args["city"], str)
or not args["city"].strip()
or args["unit"] != "celsius"
):
raise ValueError("city 必須是非空字串,unit 必須是 celsius")
result = get_weather(args["city"], args["unit"])
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
})
if not assistant.tool_calls:
raise RuntimeError("模型沒有呼叫工具;請檢查模型與 schema")
final = client.chat.completions.create(
model="qwen2.5:3b", messages=messages, tools=TOOLS
)
print(final.choices[0].message.content)
assert assistant.tool_calls[0].function.name == "get_weather"
assert any(message["role"] == "tool" for message in messages)
python hello_tool.py
這裡用的是 OpenAI Python SDK 連到 Ollama 的 compatible Chat Completions endpoint,沒有把資料送到 OpenAI 雲端。additionalProperties: false 對 schema 很有幫助,但 Ollama 和 OpenAI strict mode 的保證不能畫上等號;程式仍要驗證。
若模型沒有呼叫工具,先保持問題、模型和 schema 不變重跑三次,記錄成功次數;不要用一次失敗宣布模型「不支援」。
Path B:Anthropic 完整來回(每次先保留 $0.05)
import json
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
tools = [{
"name": "get_weather",
"description": "取得指定城市的示範天氣資料",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
}]
def get_weather(city: str) -> dict:
return {"city": city, "temperature": 26, "unit": "celsius"}
messages = [{"role": "user", "content": "台北現在幾度?"}]
first = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=512,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": first.content})
tool_results = []
for block in first.content:
if block.type == "tool_use":
if block.name != "get_weather":
raise ValueError(f"不允許的工具:{block.name}")
if (
set(block.input) != {"city"}
or not isinstance(block.input["city"], str)
or not block.input["city"].strip()
):
raise ValueError("get_weather 需要一個字串 city")
result = get_weather(block.input["city"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False),
})
if not tool_results:
raise RuntimeError(f"沒有工具請求;stop_reason={first.stop_reason}")
messages.append({"role": "user", "content": tool_results})
final = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=512,
tools=tools,
messages=messages,
)
print("\n".join(block.text for block in final.content if block.type == "text"))
Anthropic client tool 的失敗結果要使用對應 tool_use_id,並加上 "is_error": true。不要把工具結果插進 system prompt。
練習 2:多工具選擇¶
完成後,模型會在 calculator 和 get_weather 中選一個,程式只派發 allowlist 內的名稱。
第一步:直接複製這行執行 mock test,不需要金鑰:
python examples/stage-3/02-multi-tool-selection/test.py
展開 Path A/Path B、觀察重點與預算
- Path A README(Ollama):執行
python starter.py。 - 同一資料夾的
starter_anthropic.py是 Path B;執行python test_anthropic.py可先用 mock 驗證訊息形狀。 - 觀察
tool_calls[0].function.name,再確認程式是否拒絕未知名稱。 - 不要使用
globals()[model_name]()或eval()派發工具。
Path A 的 API 費用是 $0;Path B 一輪先保留 $0.05。
結構化輸出(Structured Outputs / JSON mode)⭐ function calling 的孿生兄弟¶
Function Calling 是「請程式做事」;Structured Output 是「請模型把資料放進固定形狀」。兩者都用 schema,但目的不同。
展開 strict mode、JSON mode 與常見限制
- JSON mode 通常只保證可解析成 JSON,不一定符合你的欄位規則。
- Structured Output 會依供應商支援限制 schema 外形;仍可能遇到 refusal、截斷或語意錯誤。
- OpenAI strict mode 要求每個 object 設
additionalProperties: false,並把 properties 全列為 required;Chat Completions 預設仍不是 strict。 - Anthropic strict tool use 與 OpenAI 的 schema 格式、訊息格式不同,不要直接複製旗標名稱。
- Ollama/其他 compatible endpoint 支援範圍取決於模型與版本。以固定 eval 驗證,不以「compatible」推論完全相同。
想用 Python model 管理 schema,可看 567-labs/instructor;想研究 constrained decoding,可看 dottxt-ai/outlines。不論使用哪一個,程式都要處理解析與語意錯誤。
練習 3:從零實作 ReAct(不用 framework)¶
完成後,你會有一個最小 Agent Loop:模型可以多次叫工具,但超過上限一定停止。
第一步:先跑不需要金鑰的測試:
python examples/stage-3/03-react-from-scratch/test.py
展開 13 行 loop、雙路徑與完成條件
for step in range(MAX_STEPS):
response = ask_model(messages, tools)
calls = read_tool_calls(response)
if not calls:
return read_final_text(response)
for call in calls:
name, args, call_id = validate_call(call)
result = TOOL_IMPL[name](**args)
messages.append(make_tool_result(call_id, result))
raise RuntimeError(f"Agent 超過 {MAX_STEPS} 步,已停止")
真正的程式還要把 assistant 的 Tool Call 放回 history,並處理 refusal、max tokens、timeout、未知工具、JSON 解析與工具例外。完整雙路徑在 03-react-from-scratch。
把 trace 記成 action / observation / final 或簡短可驗證摘要即可;不要把私人 Chain-of-Thought 當 log 契約。
Path A 的 API 費用是 $0;Path B 一次 loop 先保留 $0.05。完成條件:測試能證明「沒有 tool call 就停」與「超過 MAX_STEPS 會報錯」。
練習 4:多步驟推理任務¶
完成後,同一個 loop 會先查資料、再計算,且每一步都有對應 call ID 和結果。
第一步:複製測試命令:
python examples/stage-3/04-multi-step-reasoning/test.py
展開任務、比較方法與預算
任務範例:「查台北溫度,再換算成華氏。」工具分成 get_weather 與 celsius_to_fahrenheit。不要把兩個步驟偷偷合成一個假工具,這題要觀察模型是否會接續使用前一個結果。
完整雙路徑在 04-multi-step-reasoning。比較模型時,固定 prompt、tools、schema、MAX_STEPS 和測試題,至少重跑五次,再記成功率與失敗類型。
Path A 的 API 費用是 $0;Path B 多輪請求先保留 $0.10。較大的模型可能比較穩,也可能只是更貴;用 eval 決定。
練習 5:錯誤處理¶
完成後,程式會把「可以讓模型修正的工具錯誤」送回去,同時對 transport、解析或超出上限的錯誤明確停止。
第一步:先跑兩條 mock test:
python examples/stage-3/05-error-handling/test.py
python examples/stage-3/05-error-handling/test_anthropic.py
展開錯誤分類、bounded retry 與預算
| 錯誤 | 程式先做什麼 | 是否送回模型 |
|---|---|---|
| 網路 timeout/rate limit | 有上限地 retry;記錄錯誤 | 通常先不要 |
| Tool Call JSON 解析失敗 | 不執行工具;回報格式錯誤 | 可以,用 error result |
| 未知工具/未授權參數 | 拒絕執行;留下 audit log | 可以,但不能放寬權限 |
| 工具查無資料 | 回傳明確、最小的語意錯誤 | 可以,讓模型改查詢或放棄 |
達到 MAX_STEPS/費用上限 |
立刻停止 | 不再 retry |
Anthropic 的失敗 tool_result 使用 "is_error": true。OpenAI-compatible 路徑可在 role: tool 的 content 放結構化錯誤,但應用程式仍要自己限制 retry。
完整雙路徑在 05-error-handling。Path A 的 API 費用是 $0;Path B 一輪錯誤復原先保留 $0.10。
練習 6:Function schema 設計(壞 schema 修到好)¶
完成後,你會用同一組題目比較兩個 schema,並指出描述、欄位、enum 或限制哪裡改善。
第一步:直接跑壞版和好版的 mock test:
python examples/stage-3/06-schema-design/test.py
python examples/stage-3/06-schema-design/test_anthropic.py
展開五條規則、eval 卡與預算
- 工具名稱用清楚的動詞加名詞,例如
get_weather。 - Description 說明何時用,也說明何時不要用。
- 每個欄位都有清楚名稱、型別與例子。
- 能用
enum、範圍和additionalProperties: false就明確限制。 - Schema 只負責介面;權限、業務規則和資料安全仍在程式驗證。
完整雙路徑在 06-schema-design,速查表在 resources/schema-design-cheatsheet.md。
直接複製這張結果卡,不必先畫空表:
固定題目:________________
壞 schema|成功 __ / 5|主要錯誤:________________
好 schema|成功 __ / 5|主要改善:________________
結論|哪個欄位幫助最大:________________
不要寫「某模型幾乎必錯」。Path A 的 API 費用是 $0;Path B 五輪比較先保留 $0.25。
🎒 推薦小專案:安全的天氣小幫手¶
把練習 1–6 接起來,只保留兩個只讀工具:get_weather 和 convert_temperature。加入 allowlist、參數驗證、MAX_STEPS、timeout、錯誤結果和五題 eval。
最小成果是 agent.py、test_agent.py、eval_cases.json 和一張結果卡。先讓 mock tests 通過,再跑本機模型;不要先接付款、刪檔或寄信工具。
🪞 反思(Reflexion / Self-Refine)— 概念 + 路由¶
展開 Reflection、Reflexion、Self-Refine 與記憶的關係
- Reflection 是廣義名稱:看上一輪結果,再改善下一輪。
- Reflexion 常把失敗、回饋與下一次策略寫進可重用的文字記錄。
- Self-Refine 常用「產生 → 批評 → 改寫」循環改善同一份輸出。
- 這些都是 ReAct 的 sibling patterns,不等於 Tool Use,也不一定需要持久記憶。
本章只理解 single-session loop。需要跨 session 保存失敗經驗時,進 Stage 06 的 Reflection Memory;需要更完整的 planning、verification 與長時執行,進 Stage 07.5。
🎯 精選 Projects¶
先完成一條五星路線:官方文件 → 練習 1–3 → 一個從零實作。完整表是工具箱,不是 21 筆待辦清單。
資源查核:2026-08-27 UTC
推薦度是本 Stage 的學習優先順序,不是人氣排名:
⭐⭐⭐⭐⭐=跳過會卡住本章路線;⭐⭐⭐⭐=建議優先;⭐⭐⭐=有需要再看;⭐⭐=歷史或少數情境。
| 分類 | 資源 | 先做什麼 | 狀態/授權 | 推薦度 |
|---|---|---|---|---|
| 官方核心文件 | Anthropic — How Tool Use Works | 先看 client tool 的五步來回。 | 官方文件 | ⭐⭐⭐⭐⭐ |
| Anthropic — Handle Tool Calls | 看 call ID、result 與 is_error。 | 官方文件 | ⭐⭐⭐⭐⭐ | |
| Ollama — Tool Calling | 照 single tool 和 agent loop 範例跑一次。 | 官方文件 | ⭐⭐⭐⭐⭐ | |
| OpenAI — Function Calling | 比較 function schema 與 strict mode。 | 官方文件 | ⭐⭐⭐⭐ | |
| Google Gemini — Function Calling | 需要 Gemini 時比較 sequential/parallel call。 | 官方文件 | ⭐⭐⭐ | |
| ReAct paper | 先讀 abstract 與方法圖。 | 原始論文;arXiv | ⭐⭐⭐⭐ | |
| 官方課程與範例 | Anthropic Courses — Tool Use | 看舊版 Tool Use notebook;動手時對照下一列 Cookbook。 | 已封存的官方課程;上游未提供 SPDX | ⭐⭐⭐⭐ |
| Anthropic Tool Use Cookbook | 從單工具讀到平行工具。 | 維護中;MIT | ⭐⭐⭐⭐⭐ | |
| Anthropic Quickstarts | 練習後看完整應用怎麼接工具。 | 維護中;MIT | ⭐⭐⭐⭐ | |
| Microsoft AI Agents for Beginners | 需要另一條完整課程時選讀一章。 | 維護中;MIT | ⭐⭐⭐⭐ | |
| 從零實作 | pguso/ai-agents-from-scratch | 用 Ollama 對照練習 3 的 loop。 | 維護中;MIT | ⭐⭐⭐⭐⭐ |
| arunpshankar/react-from-scratch | 需要 Gemini/Reflection 變體時再看。 | 更新放緩(最後 push 2025-05);Apache-2.0 | ⭐⭐⭐ | |
| mattambrogi/agent-implementation | 只用來逐行看最小教學玩具。 | 歷史參考(最後 push 2024-01);上游未提供 SPDX | ⭐⭐ | |
| lsdefine/GenericAgent | 想看小型 framework 時再比較。 | 維護中;MIT | ⭐⭐⭐ | |
| Framework/CodeAct 對照 | Hugging Face Smolagents | 完成 JSON-tool loop 後比較 CodeAct。 | 維護中;Apache-2.0 | ⭐⭐⭐⭐ |
| QuantaLogic | 需要第二個 CodeAct 實作時再看。 | 更新較慢(最後 push 2025-12);Apache-2.0 | ⭐⭐⭐ | |
| LangChain ReAct Agent | 看 framework 如何包住自己寫過的 loop。 | 維護中;MIT | ⭐⭐⭐ | |
| 中文章節式教材 | datawhalechina/hello-agents | 需要完整中文章節時走這條主線。 | 維護中;上游 metadata 未提供 SPDX | ⭐⭐⭐⭐⭐ |
| jjyaoao/HelloAgents | 配合上面教材跑程式;先確認對應分支。 | 維護中;上游 metadata 未提供 SPDX | ⭐⭐⭐⭐⭐ | |
| Structured Output 工具 | 567-labs/instructor | 想用 typed model、驗證與 retry 時看。 | 原 jxnl/instructor 已轉址;MIT | ⭐⭐⭐⭐ |
| dottxt-ai/outlines | 研究本機 constrained decoding 時看。 | 維護中;Apache-2.0 | ⭐⭐⭐⭐ |
✅ 進 Stage 4 前的自我檢查¶
- 我能用自己的話說出 schema → call → execute → result → answer。
- 我能分清 Tool Call、Tool Result 與 Structured Output。
- 我的程式只派發 allowlist 工具,會驗證參數,也有
MAX_STEPS。 - 我跑過練習 1–3,並看過至少一次成功和一次錯誤路徑。
- 我比較模型或 schema 時使用同一組題目與明確分數。
都做到後,進入 Stage 4 — Workflow Graph 與 Agent 框架。如果還說不出完整來回,先重跑練習 1;不需要把整章重新讀一遍。