
有個很簡單的方法可以測出你的 agent 有多自主:把螢幕關掉,出門三天。
回來以後問它一句「這三天發生什麼事」。多數人的 agent 這時候會很誠實地告訴你:它不知道。它甚至不知道自己這三天被叫醒過幾次。
今天要拆的就是這個落差。我把它整理成四個維度,每一個維度都是一次身分轉換:從任務變成職責。
先看一個對照表。左邊是你熟悉的用法,右邊是它自己跑的時候。
| 維度 | 一次性任務 | 持續職責 |
|---|---|---|
| 誰啟動 | 你按 Enter | 時間到了,或某個事件發生 |
| 上下文從哪來 | 你的螢幕、你的記憶 | 得自己有一份 |
| 什麼時候算做完 | 你看到答案就關掉 | 得有人判斷,而那個人不在 |
| 誰批准危險操作 | 你,逐次 | 得預先宣告 |
| 成本誰管 | 你看到貴就停 | 沒人看,燒到你收到帳單 |
這五行右邊的欄位,就是接下來 29 天要一格一格填的東西。
值得先說清楚的是:這五件事不是能力問題。模型完全有能力做完,缺的是它周圍那一圈東西。同一個 Claude,你給它一個空的環境跟給它一個有記憶、有驗收、有權限邊界的環境,行為天差地遠。
那一圈東西有個名字,叫 harness。

左邊是有人監督的迴圈(SUPERVISED):你按下 Enter,它回答,你看一眼覺得可以,結束。
右邊是沒人監督的迴圈(UNSUPERVISED):時鐘或事件負責觸發,中間那個 HARNESS 負責把昨天的狀態注入進去、同時用虛線框住權限邊界和成本上限,產出交給 VERIFY 判斷,通過就結束,不通過就繞回去重跑,真的卡住才響鈴找人。
左邊那個迴圈裡,「你」同時扮演四個角色:觸發器、記憶體、驗收員、審批人。右邊要把這四個角色都做出來。
多數人做 agent 卡住的地方,是只做了觸發器(排程),然後期待模型自己補上其他三個。
這是最簡單的一個,也是唯一一個大部分人都做對的。
# crontab
0 8 * * * claude -p "檢查昨天的錯誤日誌" >> /var/log/agent.log 2>&1
會動。但這行有兩個坑,我兩個都踩過。
第一個坑:cron 的環境不是你的環境。claude 這個指令在你的 shell 裡找得到,在 cron 裡找不到,因為 PATH 不一樣。用 Homebrew、nvm、volta、bun 裝的 CLI 特別容易中招。解法是寫絕對路徑,或者在程式裡主動去幾個常見位置探測。
第二個坑:沒有互斥。上一輪還在跑,下一輪時間到了又起一個。兩個 agent 同時寫同一個檔案,你會拿到交錯的半行 JSON。這件事在 Day 3 會用一個檔案鎖解決。
claude -p 是無狀態的。每次呼叫都是新對話。
很多人的第一直覺是把歷史全部塞進 prompt:
claude -p "$(cat history.txt)
今天的任務:檢查錯誤日誌"
這會動,直到 history.txt 長到把你的 context window 撐爆,或是燒錢燒到你有感。我量過,光是把三十輪對話原文塞進去,就要 4 萬多個 token,而其中真正有用的資訊大概兩百個字。
Day 4 到 Day 18 有一半的篇幅在處理這件事。核心觀念先放這裡:歷史不等於記憶。歷史是流水帳,記憶是被壓縮、被排序、會遺忘、會被新事實推翻的東西。
這是最容易被輕忽的一個,我認為也是最重要的一個。
互動模式下,「做完」的定義是「你看到答案,覺得可以了」。這個判斷發生在你腦袋裡,沒有寫進任何地方。
自動模式下你必須把它寫出來。而寫出來的第一個版本,通常長這樣:
result = run_agent(task)
if "完成" in result:
mark_done()
這叫做讓考生自己改考卷。模型講「我已完成」的機率,跟它真的完成的機率,是兩件事。我在 Day 20 到 Day 24 會用四篇講怎麼做一個真的能擋住的驗收層,包括我自己那個驗收層曾經有兩個洞的實例。
互動模式下每個危險操作都會跳出來問你。無人模式下這個問句沒有對象。
於是很多人加 --dangerously-skip-permissions,然後就再也沒有任何東西擋在中間了。
比較好的做法是把「逐次批准」換成「預先宣告」:明確列出這個 agent 能用哪些工具、能碰哪些路徑、哪些操作必須留給人。Claude Code 本身就有 --allowedTools 和 --disallowedTools,這是最省力的起點:
claude -p "檢查昨天的錯誤日誌" \
--allowedTools "Read,Grep,Glob" \
--disallowedTools "Bash,Write,Edit"
一個只需要讀日誌的 agent,本來就不該有寫檔和執行 shell 的能力。這一行的防禦效果,比你寫任何 prompt 都好。
沒人看的時候,成本是唯一會無聲增長的東西。
三個實際會發生的燒錢情境:
一個是重試迴圈。任務失敗,程式自動重試,模型每次都失敗,你的迴圈跑了 200 次。我後來在自己的平台裡加了迭代上限(預設 5 次)和 24 小時的牆鐘上限,就是被這個燙到。
一個是 context 膨脹。歷史越塞越長,每次呼叫都比上一次貴一點,曲線是往上翹的。
還有一個最陰險:快取失效。你以為你有 prompt cache,命中率 90%,結果某天你改了系統提示裡的一個字,整條前綴全部重算。Day 29 會講怎麼定位「是哪一塊在破快取」。
把五個缺口濃縮成一個 40 行的骨架,之後每一天都在往裡面填東西。
#!/usr/bin/env python3
import json, subprocess, fcntl, pathlib, datetime
HOME = pathlib.Path.home() / ".myagent"
HOME.mkdir(exist_ok=True)
STATE = HOME / "state.json"
LOCK = HOME / "run.lock"
def load_state() -> dict:
if STATE.exists():
return json.loads(STATE.read_text())
return {"last_run": None, "notes": []}
def save_state(s: dict) -> None:
tmp = STATE.with_suffix(".tmp")
tmp.write_text(json.dumps(s, ensure_ascii=False, indent=2))
tmp.replace(STATE) # 原子寫入,避免半個檔案
def run_once(task: str) -> str:
state = load_state()
context = "\n".join(state["notes"][-5:]) # 只帶最近 5 條,不是全部歷史
prompt = f"# 你昨天記下的事\n{context}\n\n# 今天的任務\n{task}"
proc = subprocess.run(
["claude", "-p", prompt,
"--allowedTools", "Read,Grep,Glob"], # 缺口四:預先宣告
capture_output=True, text=True, timeout=600,
)
if proc.returncode != 0:
raise RuntimeError(f"agent failed: {proc.stderr[:500]}")
out = proc.stdout.strip()
state["last_run"] = datetime.datetime.now().isoformat()
state["notes"].append(out[:200]) # 缺口二:留下狀態
save_state(state)
return out
if __name__ == "__main__":
with open(LOCK, "w") as lk:
fcntl.flock(lk, fcntl.LOCK_EX | fcntl.LOCK_NB) # 缺口一:互斥
print(run_once("檢查昨天的錯誤日誌,有異常就摘要成三行"))
這 40 行處理了缺口一(互斥)、缺口二的粗糙版(狀態檔)、缺口四(工具白名單)。缺口三和缺口五完全沒碰。
這個骨架有三個明顯的問題,我先講在前面,免得有人直接拿去用。
state["notes"] 會無限成長,跑三個月會變成一個很大的 JSON。壓縮策略在 Day 13。
notes[-5:] 這個「最近五條」是拍腦袋決定的。憑什麼是五?憑什麼是最近的而不是最相關的?Day 14 會用遺忘曲線把這個排序做對。
還有,這個骨架完全信任 agent 的輸出。它說什麼就記什麼。如果它產生幻覺,幻覺就進了狀態檔,明天再被當成事實讀回來。這個問題最麻煩,Day 18 專門處理。
明天做一件更基本的事:讓它真的自己醒過來,而且醒來的時候知道自己是誰。