Agent SDK 入門:從 API 到 Agentic 應用
本篇是「Claude API & Agent SDK 完全指南」系列的第 9 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。
這一章標誌著這本書的轉折點。
前幾章我們學的是 Messages API——你問一個問題,Claude 給一個答案,任務結束。這個模式能解決很多問題,但它有一個根本限制:Claude 只能做一步。
真實世界的很多任務需要多步驟:找資料、分析資料、根據分析做決定、執行動作、再根據執行結果調整。這類任務靠 Messages API 加上工具(tool use)可以實作,但你要自己寫的膠水代碼很多。
這一章,我們要把這些膠水代碼交給工具去處理,正式踏進 agentic 應用的世界。
先說清楚:Anthropic 沒有一個叫「Agent SDK」的套件
在開始之前,我要先誠實地破除一個常見誤會,免得你照著別的教學去 pip install 結果裝不到東西。
網路上你會看到很多「Agent / Runner / Handoff」風格的範例——定義一個 Agent 類別、用一個 Runner 跑它、再用 handoff() 把任務交接給另一個 agent。那是 OpenAI Agents SDK 的形狀,不是 Anthropic 的。 Anthropic 並沒有發布一個獨立的、長那個樣子的「Agent SDK」套件。如果你照抄,第一行 import 就會失敗。
那麼,用 Claude 到底要怎麼建 agent?真實的路徑有兩條:
- Claude API + Tool Use(你或 SDK 來跑 agentic loop) ← 這本書接下來主要走這條
- 安裝官方 SDK:Python 是
anthropic、TypeScript 是@anthropic-ai/sdk - 用官方 SDK 內建的 tool runner(beta):它自動幫你跑「呼叫工具 → 回灌結果 → 再問」的循環
- 或自己手寫 agentic loop:當你要細控流程(人工審批、條件式執行、自訂 log)時,自己跑一個
while迴圈
- 安裝官方 SDK:Python 是
- Managed Agents(beta):Anthropic 在伺服器端幫你跑 loop、還幫你託管一個執行工具的 container(透過
client.beta.agents/client.beta.sessions)。適合「server 端有狀態、需要 workspace」的場景。
這本書的後續章節以路徑 1 為主軸——也就是用官方 SDK 的 tool runner 跟手寫 loop。Managed Agents 我們只在「何時用什麼」跟多 agent 的段落點到,因為它細節變動快,我不想教你寫到後來不能跑的程式碼。
口語上,我還是會把「帶 system prompt + 一組工具、跑在 agentic loop 裡的 Claude 呼叫」叫做一個 agent——這個詞沒問題,問題只在「不要把它想成某個 Agent(...) 類別」。
先說 Messages API + Tool Use 的痛點
第四章我們講了 Tool Use(工具呼叫)。回顧一下那個循環:
你 → Claude(送問題 + 工具定義)
↓
Claude(決定用什麼工具,回傳 tool_use block)
↓
你的代碼(執行工具,得到結果)
↓
你 → Claude(送工具結果,繼續對話)
↓
Claude(可能再用另一個工具,或直接回答)
↓
...重複直到 Claude 不再呼叫工具
這個循環你需要自己實作。不難,但每個做 agentic 應用的人都要重新寫一遍。而且,隨著任務複雜度增加,你還需要:
- 管理對話歷史(上下文)
- 處理工具執行錯誤
- 限制最大迴圈次數(防止無限循環)
- 支援平行工具呼叫
- 在你的程式碼裡做 agent 之間的路由(routing)
- 在適當時機暫停等待人工確認(human-in-the-loop)
這些功能每個都不難,但全部加起來要寫幾百行代碼,而且容易出 bug。
這正是 tool runner 想幫你消掉的東西——它就是官方 SDK 幫你把那個 while 迴圈封裝好的工具。
真實的心智模型:工具 + tool runner + agentic loop
把前面 OpenAI 風格的「Agent / Runner / Tool / Handoff」忘掉,換成 Claude 真正的三個概念:
1. 工具(Tool)
工具就是你給 Claude 使用的函式。官方 SDK 提供了裝飾器,讓你把一個普通函式直接變成工具,連 JSON Schema 都不用手寫:
from anthropic import beta_tool
@beta_tool
def get_weather(location: str, unit: str = "celsius") -> str:
"""Get current weather for a location.
Args:
location: City and state, e.g., San Francisco, CA.
unit: Temperature unit, either "celsius" or "fahrenheit".
"""
return f"72°F and sunny in {location}"
@beta_tool 會讀你的型別提示跟 docstring 裡的 Args:,自動生成這個工具的 input schema。你定義的是「Claude 能做什麼」,不是某個類別。
2. tool runner(取代你心中的 Runner)
tool runner 是官方 SDK 內建的東西,它負責跑那個工具呼叫循環——呼叫工具、把結果回灌給 Claude、再問下一步,直到 Claude 不再呼叫任何工具為止。你不用自己管那個 while 迴圈:
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic() # 從 ANTHROPIC_API_KEY 讀金鑰
runner = client.beta.messages.tool_runner(
model="claude-opus-4-8",
max_tokens=16000,
tools=[get_weather],
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)
注意:tool_runner 是 client.beta.messages 底下的一個方法,不是一個你要 import 的 Runner 類別。它回傳的東西可以直接迭代,每跑一圈 yield 一個 BetaMessage。
3. agentic loop(那個「自動跑到底」的循環)
上面 tool_runner 之所以省事,就是因為它把 agentic loop 包起來了。所謂 agentic loop,就是「Claude 想用工具 → 跑工具 → 把結果還給 Claude → Claude 看了結果再決定」這個重複過程,自動跑到 Claude 講完話為止。
那「Handoff(交接給另一個 agent)」呢?Claude 沒有這個原語。 多 agent 系統在 Claude 的世界裡,是你在自己的程式碼裡做路由——orchestrator 依判斷去呼叫對應的 subagent 函式。這部分我們在第十一章(多 agent)會展開,這裡先知道「沒有 handoff() 這種魔法、是你自己 route」就好。
安裝
裝的是官方 SDK,不是什麼 agent 專用套件。
Python:
pip install anthropic
TypeScript / Node.js:
npm i @anthropic-ai/sdk
import 的方式:
# Python
import anthropic
from anthropic import beta_tool
// TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
確保你已設定 ANTHROPIC_API_KEY 環境變數,SDK 會自動讀取。
第一個 Agent:查天氣
讓我們從一個最小可跑的例子開始:一個能查天氣的 agent。這裡用 tool runner,讓 SDK 幫我們跑 loop。
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def get_weather(location: str, unit: str = "celsius") -> str:
"""Get current weather for a location.
Args:
location: City and state, e.g., San Francisco, CA.
unit: Temperature unit, either "celsius" or "fahrenheit".
"""
# 真實情況這裡會去打某個天氣 API;這裡先回傳假資料示範
return f"72°F and sunny in {location}"
# tool runner 自動處理 agentic loop:
# 呼叫工具、回灌結果、再問,直到 Claude 不再呼叫工具
runner = client.beta.messages.tool_runner(
model="claude-opus-4-8",
max_tokens=16000,
tools=[get_weather],
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)
# 每個 iteration yield 一個 BetaMessage;Claude 講完就停
for message in runner:
print(message)
執行這段代碼,背後發生的事是:
- Claude 收到問題,判斷需要查天氣,回傳一個 tool_use(要呼叫
get_weather) - tool runner 自動執行你的
get_weather函式,把結果回灌給 Claude - Claude 看到天氣資料,生成一段給人看的回答,不再呼叫工具
- 因為沒有更多 tool_use,loop 自動結束
整個工具呼叫循環你一行都沒寫——這就是 tool runner 的價值。
如果你的工具是 I/O 密集(要打外部 API),可以改用 async 版:把 import 換成 from anthropic import beta_async_tool,工具寫成 async def,其餘形狀一樣。
想自己掌控每一步?手寫 agentic loop
tool runner 很方便,但有時候你需要細控——例如某些工具要先經人工審批、要依條件決定跑不跑、要記自訂 log。這時就自己跑 loop。這也是看清楚「tool runner 到底幫你做了什麼」的最好方式:
import anthropic
client = anthropic.Anthropic()
tools = [...] # 工具的 JSON 定義(見下一段)
messages = [{"role": "user", "content": user_input}]
MAX_ITERATIONS = 10 # 防無限循環:自己加計數器
for _ in range(MAX_ITERATIONS):
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=16000,
tools=tools,
messages=messages,
)
if response.stop_reason == "end_turn":
break # Claude 講完了
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool in tool_use_blocks:
result = execute_tool(tool.name, tool.input) # 你的實作
tool_results.append({
"type": "tool_result",
"tool_use_id": tool.id, # 必須對應 tool_use 的 id
"content": result,
})
messages.append({"role": "user", "content": tool_results})
final_text = next(b.text for b in response.content if b.type == "text")
幾個一定要知道的點:
stop_reason是 loop 的方向盤。可能的值:end_turn(講完了,跳出)、max_tokens(被max_tokens截斷)、tool_use(要呼叫工具,繼續跑)、pause_turn(server 端工具暫停,可續跑)、refusal(安全拒答,看stop_details)。- 工具錯誤:在那筆 tool_result 裡加
"is_error": True,把錯誤訊息塞進content,讓 Claude 知道工具掛了、自己想辦法。 - 防無限循環:手寫 loop 一定要加
MAX_ITERATIONS計數器跳出。tool runner 則是「沒有 tool_use 就自動停」,不會無止盡跑。
那這個手寫 loop 用的工具 JSON 定義長怎樣?沒有裝飾器幫你時,就自己寫:
tools = [{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and state"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}]
# 強制這一回合只能用某工具:tool_choice={"type": "tool", "name": "get_weather"}
# 要嚴格結構化:工具裡加 "strict": True,並讓 input_schema 有 additionalProperties: False
TypeScript 版本
TypeScript 這邊一樣有 tool runner,搭配 betaZodTool 用 Zod 定義工具的 input schema,型別安全又不用手寫 JSON Schema:
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const getWeather = betaZodTool({
name: "get_weather",
description: "Get current weather for a location",
inputSchema: z.object({
location: z.string().describe("City and state, e.g., San Francisco, CA"),
unit: z.enum(["celsius", "fahrenheit"]).optional(),
}),
run: async (input) => `72°F and sunny in ${input.location}`,
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-4-8",
max_tokens: 16000,
tools: [getWeather],
messages: [{ role: "user", content: "What's the weather in Paris?" }],
});
console.log(finalMessage.content);
client.beta.messages.toolRunner(...) 跟 Python 的 tool_runner 是同一件事的 TS 版——一樣自動跑 agentic loop、跑到 Claude 不再呼叫工具為止,回傳最終的 message。
如果你要細控,TS 也能手寫 loop,要點跟 Python 一樣(看 stop_reason === "end_turn" 跳出,把 tool_result 串回 messages):
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
let messages: Anthropic.MessageParam[] = [{ role: "user", content: userInput }];
const MAX_ITERATIONS = 10;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 16000,
tools,
messages,
});
if (response.stop_reason === "end_turn") break;
const toolUseBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const t of toolUseBlocks) {
toolResults.push({
type: "tool_result",
tool_use_id: t.id,
content: await executeTool(t.name, t.input),
});
}
messages.push({ role: "user", content: toolResults });
}
Agent vs Assistant:Agency 的概念
很多人分不清「AI 助手」和「AI Agent」的差異。我的理解是:Agency(代理能力)= 自主決定下一步的能力。
| 特性 | AI 助手 | AI Agent |
|---|---|---|
| 執行步驟 | 一步(問一答一) | 多步(自主決定做什麼) |
| 工具使用 | 手動觸發 | 自主選擇和調用 |
| 目標導向 | 回答問題 | 完成任務 |
| 錯誤恢復 | 需要人干預 | 可以自主嘗試不同方法 |
| 適用場景 | 問答、諮詢 | 研究、執行、自動化 |
從「建立助手」升級到「建立代理」,技術上的關鍵差別其實就一句話:有沒有那個 agentic loop。助手是一次 messages.create 拿一個答案;代理是把 messages.create 放進一個會根據工具結果繼續跑的循環裡。tool runner 幫你跑這個循環,你只要把目標跟工具給它,它會想辦法達成,而不是每一步都要你告訴它怎麼做。
何時手寫 loop、何時用 tool runner、何時上 Managed Agents?
這個問題我被問過很多次,我的判斷框架是這樣的:
用 tool runner 當(多數情況的預設):
- 任務需要動態決策(不知道幾步才能完成),但你不需要在每一步插手
- 你只是想「給目標 + 一組工具,讓它自己跑到底」
- 你想少寫膠水代碼、快速做出可跑的 agent
自己手寫 agentic loop 當:
- 你需要在某些工具執行前插入人工審批
- 你要依條件決定跑哪些工具、或自訂每一步的 log / 中斷邏輯
- 你要做很客製的錯誤處理或重試策略
- 講白了:當 tool runner 的「全自動」不夠細,你需要方向盤
考慮 Managed Agents 當:
- 你需要 server 端有狀態的、長時間執行的 agent,還要一個託管的 workspace / container 來實際執行工具
- 你不想自己維運那個執行環境
而最基本的——只需要一到兩步、不需要任何工具循環的任務(純文字生成、分類、翻譯),根本不用 agent,直接一次 messages.create 最省事、延遲最低。
我的實踐原則:先用最單純的 messages.create;當工具循環邏輯開始出現,就用 tool runner;只有當你發現需要細控每一步時,才退回手寫 loop。 不要一開始就過度工程化。
與 LangChain、LlamaIndex 的比較
如果你之前用過 LangChain 或 LlamaIndex,你可能在想:既然有官方 SDK 的 tool runner,為什麼還會有人用這些框架?
我的觀點:
直接用官方 Anthropic SDK(+ tool runner)的優勢:
- 官方出品,對 Claude 的所有功能(extended thinking、prompt caching 等)支援最直接、最新
- 抽象層少,你看得到底層的
messages/tool_use/tool_result,debug 容易 - 跟 Claude 功能同步更新,不用等第三方框架跟進
- 學習曲線平:tool runner 就是把你已經懂的 Messages API 包了一層 loop
LangChain 的優勢:
- 生態系豐富,有大量現成 integrations(向量資料庫、各種 API)
- 支援多種 LLM(不限於 Claude),抽象層幫你抹平 provider 差異
- 有更成熟的 RAG pipeline 工具
- 社群更大
LlamaIndex 的優勢:
- 專注於資料索引和 RAG,這方面深度更好
- 對結構化資料的支援更豐富
我的建議:如果你的應用 100% 用 Claude,從官方 SDK + tool runner 開始,因為它最直接、抽象最少、最容易 debug。如果你需要豐富的第三方整合,或者未來可能換模型,LangChain 這類框架是合理的選擇。
設計時要記得的幾個原則
在繼續往下蓋之前,幾個會讓你的 agent 程式碼更乾淨、更好維護的原則:
1. 工具盡量寫成純函式、可重複呼叫
agentic loop 裡 Claude 可能因為前一次結果不理想而再呼叫同一個工具,所以你的工具實作最好是無副作用的(或至少冪等),能承受重複呼叫。會「動到外部世界」的工具(送錢、寄信、刪資料)要特別小心,這類最適合放在手寫 loop 裡加人工審批。
2. 狀態在 messages,不在某個物件
Claude 這條路沒有什麼神祕的「agent 記憶體物件」。整段對話的狀態——使用者問了什麼、Claude 呼叫過哪些工具、工具回傳了什麼——全部就是那串 messages 歷史。要做「記憶」,就是把相關歷史串進 messages,或用 memory 類工具把長期記憶外掛出去。這也是為什麼手寫 loop 裡我們一直在 messages.append(...)。
3. async 優先(尤其 I/O 密集)
工具大多是去打外部 API 的 I/O 操作,寫成 async 能讓多個工具呼叫平行跑。Python 用 AsyncAnthropic + beta_async_tool,TypeScript 本來就是 async。
4. 可觀測性:用 SDK log + request id,不要幻想有 tracing 開關
Claude 這邊沒有一個「打開就有漂亮 trace」的開關。要看 agent 每一步在幹嘛,務實的做法是:
# 1) 環境變數開 SDK 詳細 log
# ANTHROPIC_LOG=debug
# 2) 直接印出訊息歷史,看 Claude 呼叫了哪些工具、拿到什麼
print(messages)
# 3) 每個 response 都有 request id,回報問題時附上它
response = client.messages.create(model="claude-opus-4-8", max_tokens=16000,
messages=messages)
print(response._request_id)
手寫 loop 的好處之一,就是你想在哪一步插 log、插 metric、插 breakpoint,完全自由。
5. 結構化輸出與錯誤處理(順手補上)
要 Claude 回一個你能直接用的物件,可以用 parse 搭配 Pydantic:
from pydantic import BaseModel
class Insight(BaseModel):
title: str
detail: str
resp = client.messages.parse(
model="claude-opus-4-8",
max_tokens=16000,
messages=[...],
output_format=Insight,
)
data = resp.parsed_output # 已驗證的 Insight
SDK 也有一整組例外類別可以 catch:anthropic.BadRequestError(400)、AuthenticationError、PermissionDeniedError、NotFoundError、RateLimitError、RequestTooLargeError(413)、APIStatusError、APIConnectionError、APITimeoutError。其中 429 跟 5xx,SDK 預設會自動重試(max_retries 預設 2),所以多數暫時性錯誤你不用自己處理。
理解了「工具 + tool runner + agentic loop」這套真實心智模型之後,是時候動手建一個真正有用的 agent 了。
下一章是這本書技術深度最高的一章:我們要從頭打造一個能搜尋網路、查詢資料庫、生成報告的 Research Agent,涵蓋工具設計、用 messages 管狀態、錯誤處理、測試策略的完整代碼。如果你一直想知道「真實的 agent 應用長什麼樣」,下一章就是答案。