AI Agent 是一種會不斷循環運作的語言模型。它讀取任務、使用像是「讀取這個檔案」或「刪除那個檔案」的工具、檢視結果,然後持續執行直到工作完成。每次模型要求使用工具時,這個請求就稱為 tool call(工具呼叫)。
負責執行這個循環的程式碼稱為 harness。由模型決定要做什麼,而 harness 負責執行,同時也決定模型被允許做什麼。
一個好的 harness 會在過程中做出許多小決定:該由哪個模型處理這個請求?這個 tool call 執行起來安全嗎?這個答案夠好到可以直接回傳嗎?大多數的 harness 都是透過詢問聊天模型並讀取其回覆來回答這些問題。但這樣每次都會消耗一次完整的模型呼叫成本,因此在實務上,大部分檢查往往會被跳過。
來自 TypeSafe AI 的 Jev 是一個專為這些決策打造的小型模型。你描述情境、問幾個問題,它就會用數字逐一回答,而且完全不會產生文字。
當你要建立 自訂 harness(也就是自己寫 Agent 循環,而不是用現成的 Agent)時,這一點最為關鍵。自訂 harness 讓你能選擇要跑哪些模型、Agent 可以碰觸哪些東西,以及怎樣才算完成。Jev 讓這些決策背後的檢查成本低到可以在每一步都執行。
在這份教學中,你會使用 Pi SDK(一套用來建構 Agent 的 TypeScript 工具包)來建立 harness,並在三個地方使用 Jev。最後,你會在即時沙盒中執行完成的 harness,並自己動手調整設定。
在這裡存取完整的互動式教學與遊樂場:
https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
本指南的靈感來自 Sydney Runkle 在 LangChain 部落格發表的 Building a Harness with Jev,該文章將模型路由與工具把關展示為現成的 LangChain 中介軟體。而在這裡,你會用 Pi SDK 自己實作相同的概念,並額外加入兩個模式來處理失敗與檢查答案。
你會建立什麼
這個 harness 分為三個部分。每個部分都會在不同時機點向 Jev 提問,接下來的內容會用這些名稱來稱呼它們。

文中的範例是一個在步道勘查筆記資料夾中工作的 Agent,每次出遊紀錄成一個檔案。它可以讀取、寫入,甚至真的刪除這些筆記——這就是為什麼把關機制如此重要。
Jev 是什麼
TypeSafe 將 Jev 稱為 System One model(系統一模型)。這個名稱源自心理學家 Daniel Kahneman,他描述了兩種思考模式:系統一快速且自動,就像直覺知道平底鍋很燙;系統二緩慢且深思熟慮,就像在做長除法。
在這個 harness 中,一般的語言模型負責讀取檔案和撰寫答案等慢工細活,而 Jev 則負責周邊的快速判斷。Jev 夠便宜也夠快,讓你可以在每一次 tool call 時都詢問它,而不只是針對那些你預期有風險的操作。

每個數字都是介於 0 到 1 之間的機率。0.83 代表 Jev 相當確定答案是「是」;0.03 則代表它相當確定答案是「否」。
三種問題類型
每個 Jev 請求都包含兩個部分。state(狀態)是你希望被判斷的情境,例如某個 tool call 或使用者的請求;questions(問題)則是你想了解的事項。Jev 會在一次呼叫中同時回答所有問題,因此問三個問題跟問一個問題花的時間差不多。
Jev 支援三種問題類型。第一種被 Jev 稱為 noul,也就是是非題。

Jev 只知道你告訴它的資訊,所以請用白話文清楚描述每個選項與每個層級。這些描述就是提示詞。
環境設定
你可以透過 OpenRouter 使用 Jev,這項服務讓你用一組 API 金鑰就能存取多種 AI 模型。這一組金鑰可同時用於語言模型與 Jev。安裝兩個 Pi 套件並設定你的金鑰。
1npm install @earendil-works/pi-agent-core @earendil-works/pi-ai
1OPENROUTER_API_KEY=...
Jev 有自己的網址,與一般聊天用的不同。請務必指定確切版本,例如 typesafe/jev-1.13。整個 Jev 客戶端就是一次 fetch 呼叫。
1const JEV = "typesafe/jev-1.13";23async function ask(state: unknown, questions: unknown) {4 const response = await fetch("https://openrouter.ai/api/alpha/decisions", {5 method: "POST",6 headers: {7 "Content-Type": "application/json",8 Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,9 },10 body: JSON.stringify({ model: JEV, state, questions }),11 signal: AbortSignal.timeout(2_000),12 });13 if (!response.ok) throw new Error(await response.text());14 return response.json();15}
Agent 會等待 Jev 回應,因此呼叫會在兩秒後逾時放棄。一般回應大約需要 200 到 400 毫秒。
Jev 的接入點
Pi 的 Agent 類別會幫你執行循環,並讓你的程式碼能在循環中的特定時刻運行。這些位置稱為 hooks(鉤子)。這個 harness 的三個部分各使用了一個 hook。

第一道防線
先從最簡單實用的把關機制開始。在每次 tool call 之前,問 Jev 一個是非題,如果答案看起來像「是」,就阻擋該次呼叫。
1import { Agent } from "@earendil-works/pi-agent-core";23const agent = new Agent({4 initialState: { systemPrompt, model, tools },5 streamFn: models.streamSimple.bind(models),67 beforeToolCall: async ({ toolCall, args }) => {8 const { answers } = await ask(9 { tool: toolCall.name, arguments: args },10 {11 destructive: {12 type: "noul",13 instructions: "This tool call destroys or overwrites data that was not created by this run.",14 criteria: {15 true: "Deletes files, truncates or overwrites existing content, drops data, or force-pushes over history.",16 false: "Reads, lists, creates a new file, or appends to a file this run already created.",17 },18 },19 },20 );2122 if (answers.destructive.noul >= 0.65) {23 return { block: true, reason: "Blocked: this looks destructive.", terminate: true };24 }25 },26});
這裡的 0.65 是 threshold(閾值),也就是 harness 不再信任該次呼叫的分界點。只要 Jev 給出的分數達到或超過這個值,呼叫就會被阻擋。
現在,如果 Agent 試圖刪除你的筆記,動作會在真正刪除前被攔下。刪除筆記的分數約為 0.83,而讀取筆記只有 0.01。不過這道防線有點粗糙:建立全新檔案的分數約為 0.70,所以也會被阻擋。
提問的成本非常低。Jev 每百萬輸入 token 收費 0.042 美元,不到本 harness 使用的兩個模型中較便宜的 GLM 5.3 Flash 輸入價格的三分之一。
從第一道防線到完整 harness
第一道防線確實有用,但留下了四個缺口。
- 閾值埋在 hook 裡面,難以調整或測試。
- 無論難易,每個請求都跑在同一個模型上。
- 沒有定義連不上 Jev 時該怎麼處理。
- 沒有檢查最終答案的品質。
以下每個編號章節會補上一個缺口。
1. 集中管理閾值
本章節會改良把關機制。
閾值決定了 Agent 能做什麼,而當你看到實際結果後,會需要經常調整它們。因此,把它們從 hook 移到一個單純的函式 decideGate() 中。它接收 Jev 的數字,並回傳 verdict(裁決):也就是 harness 對該次呼叫的最終決定。將這些規則集中在一處的做法稱為 policy(策略)。
這個 policy 還加入了中間選項。單一閾值只能決定放行或阻擋,而兩個閾值能產生三種裁決。
- 達到或超過 blockAt,阻擋呼叫。
- 低於 askAt,執行呼叫。
- 介於兩者之間,等待人工核准。
這個中間區間能攔截 Jev 不確定的呼叫,若只用單一分界點,無論怎麼設都一定會誤判其中一邊。

1export function decideGate(signals: GateSignals, policy = DEFAULT_GATE_POLICY): GateVerdict {2 const worst = Math.max(3 signals.destructive.noul,4 signals.irreversible.noul,5 signals.outsideWorkspace.noul,6 );7 if (worst >= policy.blockAt) return { action: "block", ... };8 if (worst >= policy.askAt) return { action: "ask", ... };9 return { action: "allow", ... };10}
因為它只是一個普通函式,你可以直接傳入像上面那樣的數字來測試,完全不需要連接實際模型。
2. 依請求挑選模型
本章節會加入路由器。
有些請求很簡單,例如讀取一個檔案;有些則很困難,例如追查某件事壞掉的原因。全部用最強的模型跑會浪費錢,但全部用便宜的模型跑,又會讓困難請求得到薄弱的答案。路由器會將每個請求配對到合適的 tier(層級),也就是快速便宜的模型,或是強大昂貴的模型。
在請求開始前,路由器會在一次呼叫中問 Jev 兩個問題:一個 choice(選擇)用來挑層級,一個 score(分數)用來評估請求的複雜度。

1const ROUTER_QUESTIONS = {2 tier: choice("Which model tier should handle this request?", {3 fast: "Reading one file, pulling a fact out of it, or a small edit in a single place.",4 powerful: "Work that spans several files, or a failure with no obvious cause.",5 }),6 complexity: score("How much reasoning does this request need?", [7 "Mechanical. One step, no judgement.",8 "Localized. A few steps inside one area.",9 "Architectural. Many moving parts or an unknown root cause.",10 ]),11};
路由器的 policy 會這樣運用這兩個答案。
- 如果複雜度分數很高,就用強大的模型,即使 Jev 選了快速模型。
- 如果 Jev 對自己的選擇沒信心,為了保險起見就用強大的模型。
- 否則,就用 Jev 挑選的模型。
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
這只是一種 policy 範例。你可以根據自身領域,用不同方式權衡這些答案,例如在大量批次作業中偏向便宜方案,或是一律升級任何涉及正式環境的操作。Jev 只負責提供答案,怎麼運用則由你的程式碼決定。
為什麼只挑一次
路由器只在請求開始時挑選一次模型,並沿用到請求結束。這是因為 prompt caching(提示快取)。
Agent 每執行一步,模型都要重新讀取到目前為止的整段對話。AI 供應商會儲存最近讀取過的對話,讓重新讀取的成本變低。但每個模型都有自己的儲存空間,如果中途切換模型,新模型就得用原價重新讀取所有內容。
Jev 的創辦人在一份關於程式設計 Agent 的設計文件中算過這筆帳:在一次長時間的對話中,從 Claude Opus 切換到較便宜的 Sonnet 再切回來,成本比全程使用 Opus 高出約 50%。所以要在對話還很短的時候就先選好模型,然後一路用到底。
3. 預想 Jev 掛掉的情況
本章節會同時修改把關機制與路由器。
一旦 harness 對每個 tool call 都詢問 Jev,Agent 就會依賴 Jev。而就像任何線上服務一樣,Jev 也可能變慢或掛掉。請事先決定每個部分在得不到回應時該怎麼做,而各部分的正確做法並不相同。
把關機制會阻擋呼叫。 如果把關機制無法詢問 Jev,它就無從得知這次呼叫是否安全。放行可能會導致檔案被刪,因此它會拒絕。工程師稱這種做法為 failing closed(失效關閉),就像停電時會自動上鎖的門。
路由器會使用強大的模型。 如果路由器無法詢問 Jev,它就不知道請求有多困難。強大的模型什麼都能處理,所以請求依然能得到好答案,只是你要多付一點錢。這就是 failing open(失效開放),讓工作繼續進行。

4. 驗證答案
本章節會加入驗證器。在 Agent harness 中,verifier(驗證器)是在工作被視為完成前,檢查 Agent 成果的步驟。
Agent 可能會給出一個漏東漏西的答案,或者聲稱了某些它其實從未在檔案中查證過的事情,而閱讀的人往往看不出來。在回傳前先檢查答案,就能在 Agent 還能重試時抓出這些問題。
驗證器會將完成的答案,連同其依據的檔案與工具執行結果一起送給 Jev。Jev 會為答案品質打分,並判斷其中的主張是否 grounded(有根據),也就是是否有 Agent 實際讀取的內容作為佐證。

1const VERIFY_QUESTIONS = {2 quality: score("How well does the answer satisfy the request?", [3 "Does not answer the request.",4 "Partly answers it, with a gap the reader would notice.",5 "Fully answers the request.",6 ]),7 grounded: noul("Every factual claim is supported by the files or tool results in the transcript."),8};
有兩條規則可避免 Agent 無限重試:總共最多嘗試兩次;而當 Jev 對自己打的分数沒信心時,harness 會直接接受答案,而不是花錢再試一次。
安全性與日誌記錄
Jev 給你的是機率,而機率有可能出錯。因此,凡是能用一般程式碼明確檢查的事,都應該用程式碼檢查。在這個 harness 中,所有檔案工具都會拒絕專案資料夾以外的任何路徑,不管 Jev 怎麼說。把 Jev 留給程式碼無法做出的判斷。
把關機制只看 tool call 本身,也就是工具名稱與其輸入參數。這有助於防範 prompt injection(提示注入)——藏在檔案或網頁中的文字誘使模型做出有害行為。把關機制看不到那些欺騙手法,但仍能看到它所導致的有害呼叫。
請記錄每個決策及其背後的數字。日誌能解釋某件事為何被阻擋,並提供真實數據來設定閾值。這個 harness 會將每個決策寫成一行,存入名為 decisions.jsonl 的檔案。由於把關機制會看到 Agent 嘗試做的所有事,因此日誌會隱藏電子郵件地址與金鑰,並縮減過長的輸入。
動手試試
下方的沙盒會執行本教學完成的 harness,並連接真實的 Jev。它會在步道勘查筆記資料夾上運作,而其 delete_path 工具真的可以刪除這些檔案。
立即體驗:https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
為什麼要自建 harness
現成的 Agent 是用內建規則來做這些決定,而自訂 harness 則把它們放進你的程式碼裡。由你挑選模型、設定閾值、決定何時需要人工介入,並能從日誌中清楚讀出每次呼叫的確切原因。
Jev 讓這一切變得可行。每個決策只需幾百毫秒,成本不到一美分的零頭,因此你可以在任何需要的地方加上檢查,而不僅限於負擔得起完整模型呼叫的地方。本教學的三個部分只是起點,針對你自己領域打造的自訂 harness,可以向 Jev 提出任何重要的問題。
其他用途
同樣的三個部分也能用在程式設計 Agent 之外。
- 程式碼審查機器人。 為每個建議的修改評分,只把值得看的顯示給人看。
- 資料清理。 合併前先詢問兩筆紀錄是否描述同一件事,把不確定的配對留給人工處理。
- 文件處理流程。 為每個擷取出來的頁面評分,只重新處理分數低的。
- 核准佇列。 只有落在「需人工確認」區間的呼叫才會送到人類審查者手上。
在上述每種情況中,語言模型負責開放式的核心工作,而 Jev 回答周邊的小問題。
本教學中的問題、閾值與 policy 都是供學習用的範例,並非調校過的正式環境設定。我們正在測試這些 harness 改動對成本與答案品質的影響,後續很快會推出公布結果的指南。
我花了一個晚上和 Opus 5.5 一起完成這份指南與沙盒。如果你遇到任何問題,歡迎私訊我。也請隨意複製這篇文章餵給你的 Agents,繼續實驗這些想法。





