第 11 章 · 親手打造你的 Agent:20 行做出迷你 Claude Code

第 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 了。


先備知識:跑之前要準備兩樣東西

在貼程式碼之前,先講清楚兩個新手最常卡住的地方,免得你複製貼上卻跑不起來。

  1. 裝好官方 SDKSDK(Software Development Kit,軟體開發套件) 就是官方幫你包好的「一組現成工具函式庫」,讓你用幾行 Python 就能跟模型對話,不用自己處理連線細節。在終端機執行 pip install anthropic 即可。
  2. 設好 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 章說的嗎?一個工具就是「名字 + 說明 + 參數格式」三件事。這裡完全對應:

  • nameread_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 的靈魂。把它再拆細一點,每一輪迴圈其實只做三件事:

  1. 呼叫模型client.messages.create(...) 把「目前為止的完整對話 + 工具菜單」丟給模型,問它:「下一步做什麼?」
  2. 看它想幹嘛:模型回來時會附一個 stop_reason(停止原因)。這是整個迴圈的方向盤
    • stop_reason == "tool_use":模型說「我要用工具」→ 繼續往下執行工具。
    • 其他值(例如 end_turn):模型說「我做完了、我要開口回答你了」→ break 跳出迴圈。
  3. 執行、餵回、再問:如果要用工具,就執行它、把結果塞回 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 章那張時序圖裡「真正去執行的是你的程式」那一步。拆開看:

  1. 先把模型的回答存進對話messages.append({"role": "assistant", ...})——這一步很容易漏掉,但非漏不可。因為 API 是「無狀態」的(第 4 章講過,每次都要把完整歷史送回去),你得先把模型「我要用工具」這句話記進對話,等下才能把工具結果對應回去。
  2. 翻出模型的工具請求:模型回來的 resp.content 裡,可能夾著一塊 type == "tool_use" 的積木,寫著「我要用 run_bash,參數是 pytest」。
  3. 真正動手的是 execute():這一行 execute(b.name, b.input) 才是「按下按鈕」的地方——真的去開檔、真的去跑指令。模型自始至終沒碰過你的檔案,它只是開口要,執行的是你。
  4. 把結果包成 tool_result 餵回去:每個結果都要標上 tool_use_id,讓模型知道「這是你剛剛那個請求的回應」。然後把它塞回 messageswhile 再跑一輪。

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 章),三種做法:

  1. Claude Agent SDK——就是驅動 Claude Code 的那個 SDK(TypeScript/Python)。直接給你 agentic loop、檔案/bash 工具、權限審批、context 自動壓縮、subagent、MCP。你只補 prompt 跟自己的工具。
  2. Anthropic Managed Agents——Anthropic 幫你跑迴圈並代管沙箱;內建工具組就是 bash / read / write / edit / glob / grep(幾乎就是 Claude Code 的工具),你連伺服器都不用顧。
  3. 魔改開源 agent——直接 fork Claude Code、Aider、OpenHands 來改。

怎麼選? 一句話:手刻是為了「懂」,正式做東西時請往下走。 你已經看穿了本質(路徑 A),接下來把力氣花在「你的工具、你的 prompt、你的 eval」,而不是重寫那個每個人都一樣的 while 迴圈和 harness——那些交給路徑 B / C 就好。

💡 這一章最重要的一句話:你不需要從零重造 harness。手刻一次是為了「懂」,之後站在 SDK 上,把力氣花在你的工具、你的 prompt、你的 eval(第 10 章)。


動手練習(建議照順序做一次)

  1. 把上面那段跑起來,先只給 read_file 一個工具,叫它「讀 app.py 並解釋這支程式在做什麼」。
  2. 加上 run_bash,叫它「跑測試看看有沒有過」——觀察它如何自己決定「先讀檔、再跑測試」。
  3. execute() 裡加一個審批關卡run_bash 執行前先 input("要執行嗎?") 問你(這就是 human-in-the-loop,第 7 章)。
  4. 換成 Claude Agent SDK 重做一次,體會「同一件事,harness 幫你省了多少」。
  5. 用第 10 章的方法,準備 3 個測試任務,評估你的 agent 成功率。

延伸挑戰:把陽春版變得更像樣

上面的練習跑順之後,試試把玩具打磨成半個真傢伙。每一個挑戰都對應前面某一章的觀念,做完你會更懂「真實 harness 到底在忙什麼」。

🔧 動手試試(★ 加一個新工具):目前只有 read_filerun_bash,幫它多加一個 write_file 工具(參數 pathcontent),讓它能真的把改好的內容寫回檔案,而不是只能靠 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 專題。挑一個你最有感的,繼續往下鑽吧。


← 回到課程首頁