~/blog/dgx-spark-agent-harness-not-weights

DGX Spark · part 35

[AI Agent] 本機 agent 生圖一直發瘋,問題在工具不在模型

2026-06-024 分鐘閱讀#ai-agent#aci#harness#comfyuiEnglish
cat --toc

TL;DR

我有個本機 agent sib 叫 hikari(Qwen3.6-35B-A3B abliterated,跑在 DGX Spark 上),叫它生圖生影片時常常發瘋 —— 用 raw Python 手刻整條 ComfyUI pipeline、丟失狀態、把同一支影片重送一遍。我另一個 sib yui 生圖生影片好好的,所以我第一直覺是「35B 當 agent 太弱了,拿去微調」。但 yui 的大腦是 ChatGPT Codex —— 根本不是公平比較 —— 所以在花好幾天微調之前,我先讀了 hikari 的 state.db tool-call log:格式 0% 出錯,選錯工具 0.5%(2327 次裡 11 次)。模型是個很好的 agent。真兇是一個 ComfyUI plugin 的工具錯了 15 次,模型放棄它、改成自己手刻 —— 那才是發瘋的來源。修法是一個乾淨的 ACI skill(gx10-media:一個 gen.py call → 一個檔案),再把壞工具從 config 砍掉,讓模型沒辦法再掉回去。harness > weights。

白話版:agent 不是笨,是它的工具壞了

我在家裡一台桌上型 AI 機器上,養了兩個自己的 AI 助理。它們共用同一顆腦 —— 同一個本機模型。其中一個,我叫它「幫我生一張圖」或「生一段短影片」時,會明顯卡住:試一次、失敗、換個方法、把同一個檔案抓兩次,然後開始懷疑自己、重來。另一個就做得好好的。同一顆腦,兩種表現。

我第一個念頭很直覺:這模型太小,當助理不夠力,我應該把它訓練得更好。但訓練一個模型要花好幾天。所以動手之前,我先做了件無聊的事 —— 把這個助理做過的每一個動作的紀錄打開,數一數它到底錯了幾次。

它幾乎沒錯過。它送出的每一個指令格式都對。「發瘋」根本不是助理笨,是它的某個工具壞了。那個本來該負責生圖的工具一直當掉,助理為了把事情做完,只好繞過它、自己用程式把整套流程拼出來 —— 而那坨臨時拼湊的東西,才是它迷路的地方。做比較多生圖工作的那個助理,撞到壞工具的次數多,所以看起來比較笨。

修法不是換一顆更聰明的腦。是給助理一顆乾淨的按鈕、然後把壞掉那顆整個拔掉,讓它不會再去按。一個指令進去,一張圖或一段影片出來。


前言

老 sysadmin 都有一個反射:使用者說他「不會用」之前,先看他到底點了哪顆按鈕,再決定他是不是真的不會。十次有九次,是按鈕標錯了。

這篇是 Part 33Part 34 低調的續集。那兩篇談的是 weights —— 把一顆 NVFP4 的 LLM 跟一顆 NVFP4 的影片模型,同時又快又常駐塞進一台 DGX Spark。這篇談的是 harness:同一台機器養著我幾個 agent sib(hikari、kiriha、yui),而其中一個 —— hikari —— 偏偏在 Part 33–34 剛打通的那些創作任務上一直崩潰。

有意思的不是這個 bug。是我差點修錯一整層 —— 而擋住我的,是逼自己在相信直覺之前先把資料讀完。

症狀:agent 自己用手刻了整條 ComfyUI pipeline

hikari 跑的是 Qwen3.6-35B-A3B abliterated,就是那顆 W4A4 NVFP4 daily。我在 Telegram 上叫它生圖或生短影片,它常常會打轉:叫一個工具、拿不到有用的東西回來、改成自己手寫 Python、抓一個檔案、再抓一次、對著同一支 clip 宣布「這是一支新影片」,然後又開始懷疑自己。yui(大腦是 ChatGPT Codex 的另一個 sib)做同樣的事卻乾淨俐落。最順手的解讀馬上冒出來:hikari 的模型就是比較弱。先記著這個念頭。

偷懶的解讀就擺在那:hikari 比較笨,35B 當 agent 就是撐不起多步驟的媒體工作。接著很自然就想偷懶:微調它。蒐集 agent traces、SFT 模型讓它更會用工具,也許掛個小 LoRA。好幾天的工,而且真的有風險 —— 這是一顆 abliterated 模型,重訓有可能把我費了一番功夫弄掉的 abliteration 又補回來一部分。

這種又貴又聽起來很合理的計畫,正是該先花五分鐘看資料的時候。

我那個直覺,被一個很便宜的檢查殺掉了

每個 sib 都把完整歷史存在跑它的那台機器上的一個 SQLite 檔裡。schema 故意很無聊:

hikari → state.db   (35 MB)
kiriha → state.db   (63 MB)

messages(session_id, role, content, tool_call_id, tool_calls JSON)

每個帶 tool_calls 陣列的 assistant turn,都有一個用 tool_call_id 對起來的 role=tool 結果。所以我可以把每一個 call 跟它的結果配起來,把失敗誠實分成三類:

  • malformed JSON args —— 模型的錯(它連 call 都格不出來)
  • exec error —— 環境的錯(工具自己爆了)
  • 選錯工具 —— 模型的錯(挑錯工具做這件事)

這裡咬了我兩口,兩個都值得偷走:

  1. 我第一版 classifier 把成功當成失敗。 "error": nullexit_code: 0 的結果是成功;一個很笨的「結果裡有沒有 error 這個字」判法,會把它們全 flag 起來。我第一輪跑出一個嚇人的失敗率,大半是乖乖跑完的 terminal 輸出。要讀真正的成功標記,不是讀「error」這個字。
  2. 你一攤平成總平均,真正要追的尖峰就被洗掉了。 發瘋是一陣一陣、跟任務綁在一起的。攤平到 2327 個 call 上,模型看起來好好的;崩潰是某一個 session 裡它連續撞壞工具十次。你得把某一個發瘋 session 的完整 tool-call 序列撈出來 —— 用媒體關鍵字 filter、按時間排 —— 像讀逐字稿一樣讀它。

先量:tool-call 格式 0% 出錯

classifier 修好之後,數字毫不含糊:

類別比率誰的錯
tool 參數 JSON 格式錯0%(兩個 sib)——
挑錯工具0.5%(11 / 2327)模型
看起來那 ~23% 的「失敗」其餘環境

Qwen3.6-35B-A3B abliterated 的 tool call 格式完美。兩個 sib 加起來零個格式錯。那 0.5% 選錯工具的也都是雞毛蒜皮(該用 vision 卻用文字 reader 去讀圖檔;一個 memory action 動詞是空的)。用唯一重要的那把尺量,模型是個好 agent。微調它最多換來那 0.5% —— 還要冒 abliteration 的險、花好幾天。

那個戲劇化的「23% 失敗」感覺是哪來的?兩個跟模型無關的來源:環境相依套件掛掉(Playwright 的 browser binary 從來沒裝,所以每個 browser_* call 都失敗;execute_code sandbox 少了 PIL),以及 —— 真正的問題 —— 模型自己在手刻 ComfyUI。

真兇:壞掉的工具引誘模型即興發揮

把一個 session 攤開來看,失敗的迴圈長這樣:

sib 有一個從 ComfyUI plugin 來的工具 mcp_comfyui_run_workflow。它很不穩 —— 在我撈的那個 session 裡,它失敗了 15 次。錯夠多次之後,模型做了一件夠聰明的 agent 會做的事:它放棄那個壞工具,換個方法解。它打開 execute_code,用 raw Python 把整條 ComfyUI pipeline 自己刻出來 —— 把 graph 送去 /prompt、poll /history、從 /view 抓結果 —— 一刻就是十幾個臨時湊出來的 call。

那裡才是好 agent 看起來像瘋了的地方。一條手工拼出來、又臭又長的流程沒有任何狀態管理:模型搞不清楚自己送了哪個 prompt、把已經抓好的檔案再抓一次、把同一支 clip 送兩遍,然後 —— 很合理地 —— 懷疑自己到底有沒有產出新東西。「發瘋」其實是一個夠強的模型,拿著一個太低階的工具去繞過壞工具時,做出的合理反應。

那個騙到我的 yui 比較,也乾乾淨淨地解開了:從來就不是大腦的問題。hikari 做的媒體生成比其他 sib 多很多,所以撞到壞工具的次數也多很多、看起來像比較笨的那個。真正證明是 harness 不是 weights 的,是下一步:同一顆 35B 換上乾淨的工具,發瘋就停了 —— 那 ~20 個臨時湊的 call 變成一個。

修法:一個 gen.py call,不是二十個 —— 一個乾淨的 ACI skill

這裡的教訓直接出自 SWE-agent 那套東西:Agent-Computer Interface(ACI) —— agent 是透過哪些工具、什麼樣的介面去做事。那篇論文的重點是:模型不換,光改 ACI,agent 表現就差很多。給一個夠強的模型一條明確、可靠的路,它就不會即興。同一個團隊的 mini-swe-agent 更極端,幾乎只給 bash。重點不是工具多,而是工具少、穩、邊界清楚 —— 一小撮乾淨的工具,贏過一大坨半殘的工具。

所以我做了一個:一個叫 gx10-media 的 skill。Hermes 的 skill 跟 Claude Code 用同一套 SKILL.md 慣例 —— 一個帶 frontmatter 的 SKILL.md 加上附帶的 script —— 直接丟進每個 sib 的 skill 目錄就能用。三種媒體類型全收在同一支 script 後面:

# 一個 call = 一個做好的媒體檔,路徑印在最後一行
python scripts/gen.py --type image --prompt "a calico cat in a quiet Japanese alley"
python scripts/gen.py --type video --prompt "..." --seconds 5
python scripts/gen.py --type i2v   --prompt "..." --image first_frame.png --seconds 5

gen.py 在內部一次做完模型以前要即興的所有事:把對的 workflow 送去 ComfyUI 的 /prompt、poll /history 直到跑完、從 /view 下載,然後只印一行 —— 輸出路徑。狀態是 script 的問題,不是模型的問題。它對 /system_stats 做健康檢查,內建 420 秒 timeout,免得模型冷載被讀成卡死。後面掛三個 workflow JSON —— image.json(Z-Image Turbo NVFP4)、video.jsoni2v.json(Sulphur 2 NVFP4 帶音訊)。

SKILL.md 把規矩寫得很白,因為重點就是把「想即興」的空間整個拿掉:

gen.py不要自己手寫 ComfyUI API call。不要用任何 comfyui plugin。送一次,讀它印出來的路徑就好。

改完之後,整條 ~20 個 call 的手刻 pipeline 就縮成一個 call。從 yui 連到 DGX Spark 量:一張圖 94 秒(冷,含 model load)、一支 warm 影片 26 秒 —— 各自回一行乾淨的路徑。(skill 用 manifest mtime 追蹤,新的會自動被發現 —— 不用重啟,模型自己也列得出來。)

砍掉壞工具,不是只在旁邊加一個好的

光加一個乾淨的 skill 還不夠。模型本來就會去探索 —— 把壞掉的 mcp_comfyui_run_workflow 留在那,它遲早會把它再挖出來、又掉回那坨手刻流程。壞掉的工具比沒工具還糟,因為它會主動把模型帶回那條繞路。

麻煩的是:那個 ComfyUI plugin 不只是壞掉的影片工具 —— 它同時撐著生圖(image_gen: provider: comfyui,就是 Z-Image 那條)。我不能直接砍掉它,不然連能用的生圖也一起拔掉。順序很重要:先把 Z-Image 做進 skill,再砍 plugin。gx10-media 連生圖也包進去了,我才清 config:

# 每個 sib 的 config.yaml
plugins:
  enabled: []          # 原本是 [comfyui]
# image_gen: 整段移除

(一個小陷阱:兩個 sib 的 YAML 縮排不一樣 —— hikari 用 4 格空白的 list dash、kiriha 用 2 格 —— 所以純文字的搜尋取代要分開做。我先備份成 config.yaml.bak-pre-mcp-clean-20260602。)各跑一次 launchctl kickstart -k 重啟、grep comfyui 抓不到東西,兩個 sib 都恢復正常,而且之後要生圖、生影片都只剩同一條路。

我從這件事拿到什麼

**最花時間的地方:**不是修,是「忍住不先量」的那股衝動。微調那個計畫感覺很對,而微調動輒好幾天。最有價值的一小時,是花在寫一個用完即丟的 SQLite query、而不是開始那個計畫。classifier 的假陽性(把 exit_code: 0 當失敗)也差點害我去追一個根本不存在的 23% 模型錯誤率。

**這套診斷法可以直接拿來用:**任何一個「感覺很笨」的 agent,把它的 raw tool-call log 撈出來,把失敗分成 模型的錯(格式錯、選錯工具)跟 環境的錯(工具自己爆了)。這兩種要修的地方完全不一樣。而且一陣一陣的行為絕對不能信總平均 —— 把某一個壞掉的 session 從頭到尾讀一遍。該修的那一層,幾乎從來都不在你直覺指的地方。

**一句話:**壞掉的工具,會把好 agent 變成爛 agent。與其怪 weights,先修 harness —— 而且修的時候要把壞掉那條路砍掉,不是只在旁邊並排放一條乾淨的。

TL;DR

  1. agent 在某任務上「發瘋」→ 動模型之前先讀 tool-call log。
  2. 把失敗分兩邊:模型的錯(格式錯 / 選錯工具)vs 環境的錯(工具爆了 / 相依沒裝)。不同層、不同修法。
  3. 先修你的 classifier —— exit_code: 0"error": null成功
  4. 一攤平成總平均,一陣一陣的崩就看不到 —— 撈一個壞 session 的完整序列讀。
  5. 「模型繞過壞工具靠即興」→ 給它一個乾淨的 ACI 就好:一個高階 call、狀態包在裡面、一行輸出。
  6. 把壞工具砍掉,不是只在旁邊加好的 —— 夠強的模型遲早又會翻回去用那條舊路。

本系列其他文章:Part 33 — NVFP4 W4A4 在 DGX Spark MoE 上贏 FP8 · Part 34 — NVFP4 把影片模型縮小 33%

常見問題

本機 LLM agent 一直在某個任務上亂試,該不該微調它變成更好的 agent?
先量再說。我微調前先撈了 agent 的 tool-call log,發現它送出的 call 格式 0% 出錯 —— 模型本身是個很好的 agent。發瘋的真兇是一個壞掉的工具錯了 15 次,逼模型自己手刻整條 pipeline、然後丟失狀態。修那個工具花一個下午;微調是好幾天,而且修錯了層。
什麼是 ACI(Agent-Computer Interface),為什麼它比模型大小更重要?
ACI 是 agent 透過哪些工具、用什麼介面去做事。SWE-agent 論文(Yang et al., 2024)提出這個說法,證明在模型固定不動的前提下,光是工具介面的設計就會大幅改變 agent 做得好不好。夠強的模型,給它一條明確可靠的路就不會亂試;同一個模型給它一個會壞的工具,它會繞過去然後發瘋。我這個生圖的 case,把壞掉的 ComfyUI plugin 換成一支乾淨的 script,模型從手刻 ~20 個 call 變成叫一次。
本機 agent 在某任務上發瘋、另一個 sib 卻沒事 —— 是模型太弱嗎?
先別急著怪模型。我的 sib yui 生圖生影片好好的,而我那顆 Qwen3.6-35B 的 sib hikari 卻一直崩 —— 這很容易讓我以為 35B 當 agent 比較弱。但 yui 的大腦是 ChatGPT Codex,根本不是公平比較。hikari 自己的 tool-call log 顯示格式 0% 出錯 —— 它是個好 agent。它會發瘋是因為媒體工作做最多、一直撞到那個壞掉的 ComfyUI 工具。同一顆 35B 配一個乾淨的工具就不發瘋了 —— 所以是 harness 的問題,不是 weights。
對 LLM agent 來說,一個壞掉的工具是不是比沒工具還糟?
是。會壞的工具會引誘模型繞過去自己想辦法 —— 我這邊是模型用 raw Python 手刻 ComfyUI 的 submit/poll/download,刻到丟失狀態。把壞工具整個拿掉、只留一個乾淨的高階 skill 當唯一路徑,比留一個半殘的工具乾淨 —— 不然模型遲早會把那條壞路再挖出來。