[[我也希望安全第一]]|第 4/30 天
前三篇一路看資料、對齊與模型內部訊號。到這裡來嘗試往外部看:當模型開始讀檔、執行指令,成敗就不再只由權重決定。
Prompt、Context、Harness 常被當成三波流行語。我會想從這個角度來看:同一個模型接到同一項任務時,這三層分別改變了什麼?
這篇會看一個修 bug 的示範。模型第一次沒有找到檔案,卻照樣交出答案;第二次只多了一小段工作原則,結果便完全不同。把過程拆開,三個名詞也會清楚不少。
這個示範讓 Google 的 Gemma 4 2B 修正 parser.py 裡的 bug,並用 verify.py 驗證結果。兩個檔案都放在模型可以操作的工作目錄中,但第一次沒有提供額外的工作引導。
Gemma 沒有先查看目錄。它從題目裡看到 parser.py 和函式名稱,便自行想像檔案內容,重新寫了一份,最後回報任務完成。
它知道 email parser 大概該怎麼寫,實際卡住的是工作順序:它沒有採取人類工程師很自然的第一步,先看看手邊到底有什麼。
第二次,示範者加入不到八十個字的通用原則:開始前先查看目錄、修改前先讀檔、完成後執行驗證。同一個模型這次依序找到檔案、閱讀內容、修改程式並跑完測試。
模型沒有變聰明,工作環境替它補上了一套做事方式。
把剛才的任務排在一起看,大致會是這個樣子:
Prompt 「修好 parser.py,讓 verify.py 通過」
↓
Context parser.py、verify.py、專案說明、先前操作結果
↓
Harness 查看目錄 → 讀檔 → 修改 → 跑測試 → 根據結果重試
Prompt 給模型一個目標。早期大家最常調整的是問法:角色怎麼設定、步驟怎麼描述、要不要要求模型逐步推理。這些技巧能改善單次回答,卻不會自動把工作目錄和測試結果送進模型。
Context 補上完成任務需要的資訊。對話紀錄、檔案內容、搜尋結果與 RAG 找回來的文件,都屬於這一層。Gemma 第一次只知道檔名,不知道檔案實際內容;有名稱和拿得到內容,是兩回事。
Harness 負責讓整個過程運作起來。它把使用者需求、context、模型與工具接在一起,也安排每輪之間要保留什麼、何時執行工具、失敗後要不要重試,以及最後拿什麼當作完成證據。
三者沒有互相取代。Prompt 仍然說明目標,Context 仍然提供資訊;只是任務從「回答一句話」變成「連續操作一個環境」之後,越來越多成敗落在 Harness。
Harness 聽起來像一套很大的平台,實際上可以從一個很小的 agent loop 開始:把任務與當前狀態交給模型,模型選擇回答或呼叫工具,系統執行後把結果放回下一輪。一直到模型說完成,或是用完步數、時間與預算。
User / Task
↓
Runner / Orchestrator ── Session、State、Checkpoint
↓ ↑
Context Builder → Model → Tool Request
↓
Tool Adapter / MCP
↓
Shell、Browser、API、Database
↓
Result / Error / Evidence
整條路徑另外留下 Trace,必要時停下來交給人。
Runner 是跑這個迴圈的程式;State 是任務現在走到哪裡;Checkpoint 則是可以恢復的中間點。MCP(Model Context Protocol)這類介面解決的是「工具怎麼接進來」,不會自動決定模型該不該用它。
真正實作時,這張圖會往不同方向長。目前常見的不是單一標準架構,而是下面幾種取向:
| 取向 | 架構重點 | 什麼時候有用 |
|---|---|---|
| 線性 agent loop | 對話歷程一路往後加,模型在每輪選擇下一個動作 | 想看懂每一步、做 baseline 或測試模型時 |
| SDK / runner | 把工具呼叫、session、handoff 與 trace 收進通用執行層 | 一般對話、工具使用與多 agent 分工 |
| 圖狀工作流 | 節點、分支、狀態與 checkpoint 都明確表示 | 任務會跑很久、需要中斷後續跑,或特定步驟要人介入 |
| Agent server + 執行環境 | agent 與真正動手的 workspace、container 或 VM 分開 | 寫程式、用瀏覽器或處理多人長時間任務 |
為什麼要分這麼細?因為「agent 卡住」的解法差很多。三步就做完的工具呼叫,用線性 loop 最好查;如果任務會跑幾個小時,半夜因 API 逾時斷在第二十七步,有沒有 checkpoint 就會決定 on-call 的人是按一次恢復,還是重跑整件事。
要看 Harness,我反而不會先選功能最多的框架。SWE-agent/mini-swe-agent 的預設 agent 類別只有約兩百行,核心迴圈更短,正好適合把前面的三層拆開來看。
如果把前面的任務交給它,過程大致如下:
system template +「修好 parser.py,讓 verify.py 通過」
↓
model.query(messages)
↓
action: ls
↓
environment.execute(action)
↓
observation: parser.py verify.py
↓
append to messages
└───── 回到下一輪
run() 開始時會先清空 messages,加入 system template 與任務,然後反覆呼叫 step()。而 step() 實際只做兩件事:先用目前全部 messages 詢問模型,再把模型回傳的 action 交給 environment 執行。執行結果會被格式化成 observation,追加回同一份 messages。
前一輪的 ls 結果,就是後一輪的 context。
| 這篇的名詞 | 在 mini-swe-agent 裡的位置 |
|---|---|
| Prompt | system template 與帶入任務的 instance template |
| Context | 線性增長的 messages,內含模型回應、action 與 observation |
| Harness | run() / step() 迴圈、model 與 environment 介面、上限檢查、錯誤處理與 trajectory 儲存 |
這個實作還有一個容易被忽略的部分。每次查詢模型前,它會先檢查步數、費用與總執行時間;格式連續出錯也會停下來。這些都不是模型的能力,是 Harness 從外面管住一次 run 的方式。
它的好處也是限制。線性歷程很好追,本地、container 或其他執行環境也能替換;但整個工具面主要是 shell,能做什麼很大程度取決於那個環境原本擁有的權限。messages 一直變長時,也得自己處理 context 成本。它會儲存 trajectory,不等於已有圖狀工作流那種明確的分支與 checkpoint 恢復。
這也是我選它當範例的原因:它沒有用大量框架名詞遮住 Harness 的基本工作。等基本迴圈看懂了,再去看 OpenAI Agents SDK 的通用 runner 與 handoff、LangGraph 的狀態與 checkpoint,或 OpenHands 怎麼把 agent server、workspace 與多後端控制中心拆開,會比較知道自己在找什麼。
如果模型只在聊天視窗回答,猜錯一個檔案頂多得到一段不可靠的文字。當它能寫檔、執行程式或操作外部服務,同一種猜測就可能真的改變系統。
因此,Harness 會影響任務能否完成,以及錯誤能走多遠。它可以要求模型先讀再改、跑完測試才回報,也可以決定哪些資訊和操作結果會在下一輪重新送回模型。
寫在工作原則裡的「不要修改正式資料」,仍然只是一句給模型看的指示;資料庫帳號究竟能不能寫入,要由模型外面的權限系統決定。後面會再解釋工作指引、工具邊界與執行流程,以及最小權限、批准與隔離走。