第 11 章 · 親手打造你的 Agent:20 行做出迷你 Claude Code
到了收成的時刻。前面十章像是在準備食材,這一章我們開火。你會親手寫出一個會自己讀檔、跑指令、修 code 的 AI Agent——然後親眼發現一件事:
一個 AI Agent 的核心,其實只是「一個 while 迴圈 + 會呼叫工具的 LLM」。 Claude Code、Cursor、Aider 全都是從這個 20 行的核心長出來的。
差別只在工具多寡、權限控管與 context 管理的成熟度——也就是第 9 章的 harness。
💡 這一章需要一點 Python 基礎。看不懂程式也沒關係,重點是理解那條「迴圈」——那是整門課的最終拼圖。這一章我們不追求「功能完整」,而是追求「看穿本質」:把最小、能跑、正確的核心攤開,讓你看見前面每一章都收束在這幾行裡。
學習階梯:從 20 行到 Claude Code
我們用一道階梯,把前面每一章串成一個能跑的成品:
| 階 | 做什麼 | 用到的章節 |
|---|---|---|
| 1 | 純聊天迴圈(只有 LLM) | 第 1 章 |
| 2 | 加第一個工具 → 這一刻才變成 agent | 第 4 章 |
| 3 | Agentic loop:讓模型自己決定何時停 | 第 7 章 |
| 4 | 加 bash + 讀寫檔 → 迷你 Claude Code | 第 4 章 |
| 5 | 加 context 壓縮與記憶 | 第 3 章 |
| 6 | 換 SDK → 免費得到權限、沙箱、subagent、MCP | 第 8、9 章 |
| 7 | 用 eval 驗證你的 agent 會不會壞 | 第 10 章 |
這張表也是一份難度地圖:階 1~4 是本章正文會走完的核心(純手刻),階 5~7 是「懂了本質之後、真實開發怎麼站在別人的肩膀上」,我們在本章後半與延伸挑戰帶到。你不必一次全部做完——先把階 1~4 跑起來,你就已經擁有一個會自己動手的 agent 了。
先備知識:跑之前要準備兩樣東西
在貼程式碼之前,先講清楚兩個新手最常卡住的地方,免得你複製貼上卻跑不起來。
- 裝好官方 SDK:SDK(Software Development Kit,軟體開發套件) 就是官方幫你包好的「一組現成工具函式庫」,讓你用幾行 Python 就能跟模型對話,不用自己處理連線細節。在終端機執行
pip install anthropic即可。 - 設好 API 金鑰:API 金鑰(API key) 是一串像密碼的字串,用來證明「這個請求是你發的、費用算你的」。到 Anthropic 的後台申請一把,然後設成環境變數
ANTHROPIC_API_KEY。程式裡的anthropic.Anthropic()會自動去讀它——所以你不用、也不該把金鑰寫死在程式碼裡。
⚠️ 金鑰等於你的錢包,外洩就是別人拿你的帳號燒錢。永遠用環境變數帶入,絕對不要把金鑰貼進程式碼、更不要上傳到 GitHub。這是第 10 章「成本與安全」在這裡的第一個實戰體現。
核心就是這段(真的能跑)
下面這段 Python,就是一個能讀檔、能跑指令的迷你 coding agent。看懂它,你就看懂了所有 agent:
import anthropic, subprocess
client = anthropic.Anthropic()
# 1) 你的工具:給兩個,就足以成為「迷你 Claude Code」
tools = [
{"name": "read_file", "description": "讀取檔案內容",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "run_bash", "description": "執行 shell 指令",
"input_schema": {"type": "object",
"properties": {"cmd": {"type": "string"}}, "required": ["cmd"]}},
]
def execute(name, args): # ← 權限、沙箱、審批全都在這一層(這就是「harness」)
if name == "read_file":
return open(args["path"]).read()
if name == "run_bash":
return subprocess.run(args["cmd"], shell=True,
capture_output=True, text=True).stdout
# 2) Agent 的核心:就是這個 while 迴圈
messages = [{"role": "user", "content": "把 app.py 裡的 bug 修好"}]
while True:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=4096,
tools=tools, messages=messages)
if resp.stop_reason != "tool_use": # 模型說「做完了」→ 跳出迴圈
break
messages.append({"role": "assistant", "content": resp.content})
results = []
for b in resp.content:
if b.type == "tool_use": # 模型「請求」用工具
results.append({"type": "tool_result", "tool_use_id": b.id,
"content": execute(b.name, b.input)})
messages.append({"role": "user", "content": results}) # 把結果餵回去,繼續迴圈
print(resp.content[-1].text)
這 20 行裡藏著整門課:
tools的定義 → 第 4 章的工具呼叫;- 那個
while→ 第 7 章的 agentic loop; execute()那層 → 第 9 章的 harness(權限、沙箱都加在這裡);- 模型只回傳
tool_use「請求」、真正執行的是你 → 第 4 章那條主線。
💡 先別急著背細節。 這段程式碼只有三個重點:宣告工具、寫一個 while 迴圈、把工具結果餵回去。下面我們把它拆成三塊逐段講——你會發現每一塊都對應前面某一章。
逐段拆解:這幾行,分別是前面哪一章?
上面那段程式碼看起來一氣呵成,其實可以切成三塊功能。我們一塊一塊看,並標出「這對應第幾章」——這就是整門課在這裡收束的證據。
第一塊 · 宣告工具(對應第 4 章:Tool Use)
tools = [
{"name": "read_file", "description": "讀取檔案內容",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "run_bash", "description": "執行 shell 指令",
"input_schema": {"type": "object",
"properties": {"cmd": {"type": "string"}}, "required": ["cmd"]}},
]
還記得第 4 章說的嗎?一個工具就是「名字 + 說明 + 參數格式」三件事。這裡完全對應:
name(read_file):工具的名字,模型用它來指名「我要用哪個」。description(讀取檔案內容):這句話極其關鍵——模型是靠這句白話說明,來判斷「這題該不該用這個工具」。說明寫得越清楚,模型越不會亂用或漏用。input_schema:參數的格式規格(要一個叫path的字串,而且是必填)。這是第 4 章講的「結構化輸出」在這裡的用途——它逼模型吐出「你的程式能直接吃」的乾淨格式,而不是一段要你自己拆解的自然語言。
這一塊還沒有任何「執行」的動作,它只是一張「工具菜單」,告訴模型:「你手上有這兩樣東西可以點。」
💡 呼應第 4 章那條最重要的主線:模型不會、也不能真的去執行任何動作,它只會「產生一個要求」。 這張菜單決定了它能「開口要」什麼;至於要不要、能不能真的做,是下面第三塊的事。
第二塊 · 那個 while 迴圈(對應第 7 章:Agentic Loop)
messages = [{"role": "user", "content": "把 app.py 裡的 bug 修好"}]
while True:
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=4096,
tools=tools, messages=messages)
if resp.stop_reason != "tool_use": # 模型說「做完了」→ 跳出迴圈
break
...
這就是第 7 章講的 agentic loop(代理迴圈)——agent 的靈魂。把它再拆細一點,每一輪迴圈其實只做三件事:
- 呼叫模型:
client.messages.create(...)把「目前為止的完整對話 + 工具菜單」丟給模型,問它:「下一步做什麼?」 - 看它想幹嘛:模型回來時會附一個
stop_reason(停止原因)。這是整個迴圈的方向盤:stop_reason == "tool_use":模型說「我要用工具」→ 繼續往下執行工具。- 其他值(例如
end_turn):模型說「我做完了、我要開口回答你了」→break跳出迴圈。
- 執行、餵回、再問:如果要用工具,就執行它、把結果塞回
messages,然後while讓它從頭再跑一次——模型看到工具結果後,會再決定下一步。
這正是第 7 章那句話的程式版本:模型每一輪只做一個決定——「我要用工具,還是我做完了?」 這個「感知 → 推理 → 行動 → 觀察」不斷循環、直到模型說停,就是把「一次性問答的 LLM」變成「會自己動手的 agent」的全部祕密。
⚠️ 呼應第 7 章的警告:這個
while True是會失控的。 如果模型一直覺得「還沒做完、再用一次工具」,它就會永遠迴圈下去、一輪一輪燒你的 token(燒錢)。正式產品一定會加一道保險,例如「最多跑 N 輪就強制停」。我們在下面的延伸挑戰會帶你補上這道保險。
💡 這裡也藏著第 3 章 Context 工程的影子:那個
messages陣列會越滾越長——每一輪都往裡面塞新的對話與工具結果。它就是模型的「工作記憶」,而工作記憶是有上限的(context window)。跑幾輪還好,但真實的 coding agent 跑久了,這個陣列會爆掉——這就是為什麼需要「context 壓縮」,見階梯的第 5 階。
第三塊 · 執行工具、把結果餵回(對應第 4 章 + 第 9 章:Harness)
def execute(name, args): # ← 權限、沙箱、審批全都在這一層(這就是「harness」)
if name == "read_file":
return open(args["path"]).read()
if name == "run_bash":
return subprocess.run(args["cmd"], shell=True,
capture_output=True, text=True).stdout
messages.append({"role": "assistant", "content": resp.content})
results = []
for b in resp.content:
if b.type == "tool_use": # 模型「請求」用工具
results.append({"type": "tool_result", "tool_use_id": b.id,
"content": execute(b.name, b.input)})
messages.append({"role": "user", "content": results}) # 把結果餵回去,繼續迴圈
這一塊做的事,正是第 4 章那張時序圖裡「真正去執行的是你的程式」那一步。拆開看:
- 先把模型的回答存進對話:
messages.append({"role": "assistant", ...})——這一步很容易漏掉,但非漏不可。因為 API 是「無狀態」的(第 4 章講過,每次都要把完整歷史送回去),你得先把模型「我要用工具」這句話記進對話,等下才能把工具結果對應回去。 - 翻出模型的工具請求:模型回來的
resp.content裡,可能夾著一塊type == "tool_use"的積木,寫著「我要用run_bash,參數是pytest」。 - 真正動手的是
execute():這一行execute(b.name, b.input)才是「按下按鈕」的地方——真的去開檔、真的去跑指令。模型自始至終沒碰過你的檔案,它只是開口要,執行的是你。 - 把結果包成
tool_result餵回去:每個結果都要標上tool_use_id,讓模型知道「這是你剛剛那個請求的回應」。然後把它塞回messages,while再跑一輪。
而 execute() 這一層,就是第 9 章的 harness。 第 9 章說 harness 是「模型外面那層執行環境」——權限控管、沙箱、審批,全都加在這裡。現在的 execute() 是最陽春版:模型說跑什麼就跑什麼、完全不設防。真正的 Claude Code 之所以強又安全,八成是因為它的 execute() 那層做得又厚又完善(會先問你、會隔離在沙箱裡跑)。你在下面的延伸挑戰會親手把這一層加厚一點。
💡 一句話記住這三塊的分工: 第一塊「給菜單」(第 4 章),第二塊「決定何時停」(第 7 章),第三塊「真正動手 + 把結果餵回」(第 4 章的執行 + 第 9 章的 harness)。前面十章講的觀念,到這裡全部變成了可以跑的程式碼。
圖解:那條迴圈就是 agent 的全部
flowchart LR
U[使用者請求] --> M[LLM 推理與決策]
M -->|end_turn 完成| D((回覆使用者))
M -->|tool_use 要用工具| T[執行工具<br/>read/bash]
T --> O[觀察結果]
O -->|tool_result 回饋| M
模型每一輪只做一個決定:「我要用工具,還是我做完了?」 要用工具就執行、把結果餵回、再問一次;直到它說「做完了」(end_turn)才跳出。這條 tool_use → 執行 → tool_result → 再問 的迴圈,就是把「一次性問答的 LLM」變成「會自己動手的 agent」的全部祕密。
把這張圖跟上面三塊對照一下:圖中央的 M(LLM 推理與決策)就是第二塊的 while 迴圈,T(執行工具)就是第三塊的 execute(),而 M 手上能點的工具是第一塊的菜單。整張圖,就是那 20 行程式碼的樣子。
跑起來長什麼樣:一次真實的互動
看程式碼是一回事,看它「活起來」是另一回事。假設你的資料夾裡有一支有 bug 的 app.py:
def add(a, b):
return a - b # ← bug:加法卻寫成減法
你把核心那段跑下去(使用者請求是「把 app.py 裡的 bug 修好」)。迴圈會自己這樣轉,你不必下任何額外指令:
(第 1 輪)
你:把 app.py 裡的 bug 修好
Agent(心裡):我得先看看這支程式長怎樣。
→ stop_reason = "tool_use",請求 read_file(path="app.py")
你的程式:執行 execute(),把 app.py 內容餵回去
(第 2 輪)
Agent(看到內容後,心裡):add() 說是加法卻用了減法,這就是 bug。我來改掉它。
→ stop_reason = "tool_use",請求 run_bash(cmd="sed -i '' 's/a - b/a + b/' app.py")
你的程式:執行 execute(),真的改了檔案,把結果(空字串,代表成功)餵回去
(第 3 輪)
Agent(心裡):改完了,我跑個測試確認一下。
→ stop_reason = "tool_use",請求 run_bash(cmd="python -c 'from app import add; print(add(2,3))'")
你的程式:執行 execute(),跑出 "5",餵回去
(第 4 輪)
Agent(看到 5,心裡):正確!任務完成。
→ stop_reason = "end_turn"(不再用工具)→ while 迴圈 break
最後印出:
「我找到了 app.py 的 bug:add() 原本用減法。我已改成加法並驗證 add(2, 3) 回傳 5,修好了。」
注意看這裡最神奇的地方:你只講了一句話,後面「先讀檔 → 再改 code → 再跑測試驗證」的三步計畫,全是 agent 自己排出來的。 你沒有幫它畫流程圖、沒有告訴它先做什麼——這就是第 7 章講的「自主性」,也是 agent 和第 5 章「工作流」最根本的差別:工作流的路徑是你事先定好的(火車照鐵軌走),agent 的路徑是它臨場決定的(計程車司機自己選路)。 你在螢幕上看到的,就是那條 while 迴圈一圈一圈轉出來的軌跡。
🔧 動手試試:照著上面建一支有 bug 的
app.py,把核心那段跑起來,但先在迴圈裡加一行print(resp.content),把模型每一輪的想法印出來。你會親眼看到它「讀檔 → 推理 → 改 code → 驗證」的完整心路歷程——這比讀十遍文字都有感。
踩雷排解:新手最常撞到的三個坑
第一次跑這段,八成不會一次成功。這裡先幫你把最常見的坑攤開,讓你少走冤枉路——看懂這些錯誤,也是在更深地理解那條迴圈怎麼運作。
坑 1 · 忘了把 assistant 回應存回去 → 報錯 tool_use ids 對不上
症狀:API 回你一個錯誤,抱怨 tool_use 的 id 找不到對應、或訊息順序不對。
原因:你少寫了 messages.append({"role": "assistant", "content": resp.content}) 這一行,直接就把工具結果塞回去了。但 API 是無狀態的(第 4 章)——它不記得上一輪,你每次都要把完整歷史送回去。少了「模型說我要用工具」這一句,工具結果就變成一段沒頭沒尾、對不上請求的東西。
解法:先存 assistant 回應,再存 tool_result。順序是「模型開口要(assistant)→ 你給結果(user 裡的 tool_result)」,一問一答,不能跳。
坑 2 · 迴圈停不下來、一直燒錢 → 忘了設上限
症狀:程式跑起來就停不下來,模型一輪接一輪不停用工具,你的 token 用量直線上升。
原因:第 7 章的警告成真了——while True 只有在模型主動說「做完了」時才會停。如果任務很模糊、或模型鑽牛角尖,它可能永遠覺得「還沒好、再試一次」。
解法:加一道「最多跑 N 輪」的保險。這正是下面延伸挑戰之一,也是真實 harness 的標配。
坑 3 · 工具真的跑了危險指令 → 因為 execute() 完全不設防
症狀:你讓它「清理專案」,結果它請求 run_bash(cmd="rm -rf ..."),而你的 execute() 二話不說就照做了,刪掉了不該刪的東西。
原因:目前的 execute() 是最陽春版——模型說跑什麼就跑什麼,中間沒有任何關卡。它把「模型的請求」直接當成「該執行的命令」。
解法:在 execute() 裡加審批關卡(危險動作先問人),或把它關進沙箱裡跑。這就是第 9 章 harness 的核心價值,也是下面延伸挑戰要你補的第一課。
⚠️ 坑 3 是這三個裡最重要的。它示範了第 4 章那句話為什麼是整個 agent 世界的鐵律:權限與安全,永遠在「你的程式」這一層。 模型只是開口要,要不要真的執行、能不能執行、執行前要不要先問你——全是
execute()那一層決定的。一個 agent 危不危險,看的不是模型,是你的 harness。
三條路:從手刻到 Claude Code 等級
手刻懂了本質之後,真實開發你不需要重造輪子。由淺到深有三條路:
| 路徑 | 你要寫的 | 你免費得到的 | 適合 |
|---|---|---|---|
| A. 手刻 agentic loop(上面那段) | 全部(迴圈、工具、權限) | 完全理解本質 | 學習、完全客製 |
| B. 用框架 / SDK 省樣板 | 只寫工具函式 | 迴圈、重試、串流 | 大多數應用 |
| C. 直接到 Claude Code 等級 | 只寫 prompt + 自訂工具 | 整個 harness | 想快速做出強力 agent |
B|省掉樣板:官方 SDK 的 tool runner 能連 while 都不用寫;或用通用框架 LangGraph、Pydantic AI、OpenAI Agents SDK、smolagents。
C|繼承整個 harness(第 9 章),三種做法:
- Claude Agent SDK——就是驅動 Claude Code 的那個 SDK(TypeScript/Python)。直接給你 agentic loop、檔案/bash 工具、權限審批、context 自動壓縮、subagent、MCP。你只補 prompt 跟自己的工具。
- Anthropic Managed Agents——Anthropic 幫你跑迴圈並代管沙箱;內建工具組就是
bash / read / write / edit / glob / grep(幾乎就是 Claude Code 的工具),你連伺服器都不用顧。 - 魔改開源 agent——直接 fork Claude Code、Aider、OpenHands 來改。
怎麼選? 一句話:手刻是為了「懂」,正式做東西時請往下走。 你已經看穿了本質(路徑 A),接下來把力氣花在「你的工具、你的 prompt、你的 eval」,而不是重寫那個每個人都一樣的 while 迴圈和 harness——那些交給路徑 B / C 就好。
💡 這一章最重要的一句話:你不需要從零重造 harness。手刻一次是為了「懂」,之後站在 SDK 上,把力氣花在你的工具、你的 prompt、你的 eval(第 10 章)。
動手練習(建議照順序做一次)
- 把上面那段跑起來,先只給
read_file一個工具,叫它「讀app.py並解釋這支程式在做什麼」。 - 加上
run_bash,叫它「跑測試看看有沒有過」——觀察它如何自己決定「先讀檔、再跑測試」。 - 在
execute()裡加一個審批關卡:run_bash執行前先input("要執行嗎?")問你(這就是 human-in-the-loop,第 7 章)。 - 換成 Claude Agent SDK 重做一次,體會「同一件事,harness 幫你省了多少」。
- 用第 10 章的方法,準備 3 個測試任務,評估你的 agent 成功率。
延伸挑戰:把陽春版變得更像樣
上面的練習跑順之後,試試把玩具打磨成半個真傢伙。每一個挑戰都對應前面某一章的觀念,做完你會更懂「真實 harness 到底在忙什麼」。
🔧 動手試試(★ 加一個新工具):目前只有
read_file和run_bash,幫它多加一個write_file工具(參數path和content),讓它能真的把改好的內容寫回檔案,而不是只能靠sed這種 shell 技巧繞路。做法就是照第一塊「工具菜單」的格式多加一項,再到execute()裡加一段if name == "write_file"。你會發現:加工具就是「菜單加一行 + execute 加一段」這麼直觀——這正是路徑 B「只寫工具函式」的精神。
🔧 動手試試(★ 加上權限確認):把踩雷坑 3 的解法真的做出來。在
execute()裡,run_bash真正跑之前,先input(f"要執行這行嗎?{args['cmd']}\n(y/n) ")問你一句,你回y才跑、回別的就回傳「使用者拒絕」餵給模型。這就是第 7 章的 human-in-the-loop(人類在迴圈中),也是真實 Claude Code 每次跑危險指令前都會先彈出來問你的那道關卡。做完你就親手把 harness 加厚了一層。
🔧 動手試試(★ 幫迴圈裝上安全帶):把踩雷坑 2 的解法做出來,避免無限迴圈燒錢。把
while True:改成有上限的版本,例如:for turn in range(10): # 最多跑 10 輪就強制停 resp = client.messages.create(...) if resp.stop_reason != "tool_use": break ... else: print("跑了 10 輪還沒完成,先停下來避免燒錢。")這一小段就是第 7 章講的「防失控」在程式裡的樣子——真實 harness 一定有這種保險。
🔧 動手試試(★★ 把危險動作關進沙箱):
run_bash直接在你的電腦上跑指令,其實很危險(模型萬一想rm -rf怎麼辦?)。查一下怎麼用 Docker 容器或一個拋棄式的暫存資料夾,把run_bash的執行隔離起來——就算模型跑了破壞性指令,弄壞的也只是那個沙箱、不是你的系統。這就是第 9 章 harness 裡「沙箱(sandbox)」那一格,也是「讓它能動手、但弄不壞」的關鍵。
🔧 動手試試(★★ 讓它記得住、也塞得下):讓 agent 讀一個很大的檔、或連續改好幾支程式,觀察那個
messages陣列如何越滾越大。想想第 3 章:當它快超過 context window 時,你會怎麼「壓縮」歷史(例如把早期已完成的步驟摘要成一句話)?先不用真的實作,光是在迴圈裡print(len(str(messages)))觀察它怎麼膨脹,你就會對「為什麼真實 agent 需要 context 管理」很有感——這正是階梯的第 5 階。
課程總結:你走過的路
恭喜你走到終點。回頭看,這 11 章其實是一條完整的弧線:
- 懂原理(1–4):模型是「猜下一個字」→ 怎麼跟它講話(prompt)→ 怎麼管它的記憶(context)→ 怎麼讓它動手(工具)。
- 接系統與資料(5–6):用工作流把它串成流水線 → 用 RAG 餵它你的知識。
- 理解自主代理(7–9):agent 就是「會用工具的迴圈」→ 用 MCP 標準接工具 → 用 harness 包成能跑、可控、安全的成品。
- 會評估與防護(10):用評估、追蹤、護欄、成本控制,讓它敢上線。
- 親手做出來(11):把以上全部,收束成一個 20 行的 while 迴圈。
你現在不只「會用 AI」,你懂了它為什麼這樣運作,也知道怎麼親手把它組裝出來。而且經過這一章,你看那 20 行程式碼的眼光已經不一樣了:你能一眼指出「這幾行是工具菜單、這是自主迴圈、這是 harness」——前面每一章,都在這幾行裡找到了自己的位置。 剩下的,就是動手做你自己的東西了。
💡 想更進一步?這門課刻意當「入門」,還有很多值得深入的:微調 vs RAG vs Prompt 的決策、語音 Agent、本地/自架模型與隱私、Coding Agent 專題。挑一個你最有感的,繼續往下鑽吧。