問一次 Jev,同一條連線從第二次開始,中位數 248 毫秒。同一個問題放進 hook 裡問,要 1.7 秒。
差的那 1.4 秒不是模型,是「每次都從零開始」。
這三天量到三件事:Cursor 會等 hook 跑完(Day 10);現在的 hook 已經讓每一步多花 1 到 2 秒(Day 11);同時跑的 hook 會把紀錄蓋掉(Day 12)。今天回到 Day 9 預告的那個問題:模型到底該放在哪裡。
先量最好的情況:同一個 client,連續問 20 次,每次間隔 0.2 秒。
jev-1.13.0
billing.py 前後版本,後一版拿掉了 amount <= 0 的檢查第 1 次(含建立連線) 763 ms
第 2~20 次 p50 248 ms p90 308 ms min 236 ms max 352 ms
回的機率 0.94 ~ 0.95
模型本身不慢。248 毫秒,比 Cursor 叫起一次 hook 的那一段(約 0.7 秒)還快。
但 hook 用不到「第 2 次」。Cursor 每觸發一次 hook,就開一個新的 python 行程,跑完就結束。所以放在 hook 裡的模型呼叫,每一次都得從頭來:載入 SDK、讀 key、建立連線、再問。
我照這個條件,開三個全新的行程各問一次:
第 1 次 整個行程 5,393 ms(import 4,057 ms,呼叫 1,104 ms)
第 2 次 整個行程 1,669 ms(import 649 ms,呼叫 887 ms)
第 3 次 整個行程 1,669 ms(import 611 ms,呼叫 902 ms)
拆開看,import 約 0.6 秒,第一次呼叫約 0.9 秒。第一個行程特別慢,是因為檔案還沒進快取。
現在的 audit_edit.py 單獨跑一次是 117 毫秒(Day 11 量的)。在 hook 裡直接問 Jev,會變成 1.7 秒,是原來的 14 倍。Day 10 又量到 Cursor 會等 hook 跑完,所以多出來的 1.6 秒,會原封不動加在每一次改檔上。
Day 9 我寫了一個升級判斷式,原則是「不要每筆都問模型,只問確定性層判不出來的」。只有三種情況才升級:
unparsable)l1_only)l1_silent)Day 9 的文章只寫了 QUIET_KINDS、LOUD_KINDS 這兩個名字,沒寫定義。今天照 audit_edit.py 的分類補上:QUIET 是「只動註解、無結構變化」;LOUD 是「動到執行邏輯、動到函式或類別、移除斷言、改了斷言的預期值」。
然後原封不動套在真實紀錄上:9/16 到 9/24,206 筆改檔。
全部 206 筆 要升級 163 筆(79%)
解析不動(unparsable) 122
regex 沒喊、AST 說有動(l1_silent) 41
不用升級 43
只看 .py 的 84 筆 要升級 41 筆(49%)
紙上很省的判斷式,放到真實紀錄上,每 5 筆有 4 筆要問模型。
拆開看,這個數字大半是我自己造成的。
第一個問題:把「不適用」當成「判不出來」。 122 筆「解析不動」全是非 Python 檔:.md 63 筆、.txt 20 筆、.json 12 筆,還有 .html、.png、.csv。AST 本來就只看 Python,判斷式卻把它們全部當成疑難雜症往上送。改一條「非 Python 檔只在 regex 喊了才升級」,比例降到 36%。
第二個問題:AST 從來沒看過舊版本。 84 筆 .py 改檔,在 hook 裡全部叫 Write,而且舊內容全是空的。AST 等於拿新檔去跟空檔比,所以 84 筆裡有 69 筆被判成「動到函式或類別」。
真正帶著新舊字串的,是另一個事件 afterFileEdit:
afterFileEdit 的欄位:conversation_id, generation_id, model, file_path,
edits[{old_string, new_string}], session_id, hook_event_name, ...
我的 audit_edit.py 卻因為這個事件沒有 tool_name,把它當成「不是改檔工具」跳過了。這就是 Day 4 寫過的「接錯欄位」:那時候發現了,到今天還沒修。
所以真正的升級比例,修好之前我給不出來。今天的 79% 只說明一件事:判斷式照原樣不能用。
三個數字都有了,Day 9 預告的三種模式可以直接算:
| 模式 | 做法 | 每次改檔多等多久 | 代價 |
|---|---|---|---|
| inline | hook 裡直接問 Jev | 約 +1.6 秒,冷的時候 +5 秒以上 | 每一步都慢,Agent 一定等 |
| sample | 只有要升級的那幾筆在 hook 裡問 | 升級比例 × 1.6 秒;判斷式照原樣是 79%,平均 +1.2 秒 | 判斷式不修,省不到多少 |
| deferred | hook 照舊只寫紀錄,另一支常駐程式讀紀錄、慢慢問 | 跟現在一樣 | 答案晚幾秒到;常駐程式要自己顧 |
我選 deferred,理由有四個:
sample 還是有用,只是不該拿來省延遲。它留在常駐程式裡,負責省錢、省人看的量,前提是先把判斷式修好。
常駐程式要做的事,我先列出來,還沒寫:
audit-findings.jsonl 新增的那幾行。jev-1.13.0,不寫 jev-latest。一、量你的模型呼叫在 hook 裡的真實成本。 不要只量「同一個 client 連問 20 次」,那是常駐程式的數字。hook 付的是新行程的價錢,import 也算在帳上:
import os
import time
t0 = time.perf_counter()
from typesafe_sdk import Noul, TypeSafeClient # import 也算在 hook 的帳上
t_import = time.perf_counter()
with TypeSafeClient(api_key=os.environ["TYPESAFE_API_KEY"], model="jev-1.13.0") as client:
client.system_one(
state={"version_before": "def f(x):\n if x <= 0:\n raise ValueError\n return x\n",
"version_after": "def f(x):\n return x\n"},
questions={"changes_logic": Noul(instructions="Does this change alter what the code does at runtime?")},
)
t_call = time.perf_counter()
print(f"import {(t_import - t0) * 1000:.0f} ms,第一次呼叫 {(t_call - t_import) * 1000:.0f} ms")
每次都用一個新的 python 行程跑,跑三次,第一次通常最慢。
二、把判斷式套在真實紀錄上算一次。 紙上推演的升級比例不算數。這是改過一條之後的版本:
def escalate_fixed(rec):
"""非 Python 檔只在 regex 喊了的時候才升級,不再一律算解析失敗。"""
if not str(rec["path"]).lower().endswith(".py"):
return "non_py_l1" if sum((rec.get("l1_regex") or {}).values()) > 0 else None
return escalate_reason(rec) # Day 9 那一版
三、檢查你的 audit hook 有沒有讀 afterFileEdit。 新舊字串在那裡,不在 postToolUse 的 Write 裡。
.md 63 筆);加上 Write 沒有舊內容的 bug,79% 不是真實的升級比例。明天換一個問題。Agent 交件時說「做完了」,真的做完了嗎?我給它 5 條待辦,其中 2 條不可能做到,看它怎麼勾。
今天對應的威脅: T3(藉 Agent 之手規避控制)。偵測放錯位置,只有兩種下場:拖慢每一步,最後被關掉;或是判斷式亂升級,把人要看的量灌爆,重要的那幾筆被淹掉。兩種都會讓事後舉不出證。
實驗與程式: 2026-09-24 跑。day10-lab/jev_latency.py(同一個 client 連問 20 次)、jev_cold.py(新行程的完整成本)、escalation_ratio.py(升級比例,以及改一條之後的比例)。資料是這個專案的 audit-findings.jsonl(206 筆改檔)。模型 jev-1.13.0。本篇沒有引用外部來源。