AI Agent 是一个循环运行的语言模型。它读取任务,调用“读这个文件”或“删那个文件”之类的工具,查看结果,然后继续执行,直到工作完成。模型每次请求使用工具时,这个请求就被称为一次 tool call(工具调用)。
运行这个循环的代码叫做 harness(调度框架)。模型决定自己想做什么;harness 负责执行,同时也决定模型被允许做什么。
一个好的 harness 会在过程中做大量小决策:该由哪个模型处理这个请求?这次 tool call 安全吗?这个答案够不够好、可以交差了吗?大多数 harness 的做法是去问一个聊天模型,再读取它的回复。但这样每次都要消耗一次完整的模型调用,所以在实践中,大部分检查都会被跳过。
来自 TypeSafe AI 的 Jev 是一个专为这些决策而生的小模型。你描述当前情况、提几个问题,它会用数字逐一作答。它从不输出文本。
当你构建 自定义 harness——也就是自己写 Agent 循环,而不是用现成的 Agent 框架时——这一点尤为重要。自定义 harness 让你能决定跑哪些模型、Agent 能碰什么、以及怎样才算“完成”。Jev 让这些决策背后的检查足够便宜,便宜到每一步都可以跑一遍。
在本教程中,你将使用 Pi SDK(一个用于构建 Agents 的 TypeScript 工具包)搭建一个 harness,并在三个地方用到 Jev。最后,你会在真实的沙盒里运行做好的 harness,并亲手调整它的配置。
点击这里访问完整的交互式教程和 playground:
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,文件夹里装的是徒步勘测笔记,每次出行一个文件。它可以读取、写入,甚至真的删除这些笔记——这正是 gate(拦截门)如此重要的原因。
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 只知道你告诉它的东西,所以要用大白话把每个选项、每个层级都描述清楚。这些描述就是 prompt。
环境准备
Jev 可以通过 OpenRouter 访问,这是一个用一个 API Key 就能调用众多 AI 模型的服务。这一个 key 可以同时覆盖语言模型和 Jev。安装两个 Pi 包,然后设置你的 key。
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。

第一道 gate
先从 gate 最小可用的版本开始。在每次 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。不过这道 gate 比较粗糙:创建一个全新文件的得分大约是 0.70,所以也会被误拦。
提问的成本非常低。Jev 每百万输入 token 收费 $0.042,不到这个 harness 使用的两个模型中较便宜的 GLM 5.3 Flash 输入价格的三分之一。
从第一道 gate 到完整 harness
第一道 gate 能用,但留下了四个缺口。
- 阈值埋在 hook 里面,很难调整或测试。
- 无论请求简单还是复杂,都跑在同一个模型上。
- 没有规定连不上 Jev 时该怎么办。
- 没有检查最终答案质量如何。
下面每个编号小节会补上一个缺口。
1. 把阈值集中管理
这一节改进 gate。
阈值决定了 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. 为每个请求挑选模型
这一节加入 router(路由器)。
有些请求很简单,比如读一个文件;有些很复杂,比如排查某个东西为什么坏了。全用最强的模型太烧钱,全用便宜的模型又会让复杂请求得到很弱的答案。router 会把每个请求匹配到合适的 tier(档位),也就是选快速便宜的模型,还是强大昂贵的模型。
请求开始前,router 会在一次调用中问 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};
router 的 policy 是这样使用这两个答案的。
- 如果复杂度评分很高,就用强模型,即使 Jev 选了 fast。
- 如果 Jev 对自己的选择不自信,为了保险也用强模型。
- 否则,就用 Jev 选的模型。
1if (complexity.score >= policy.escalateAtComplexity) return powerful;2if (tierAnswer.confidence < policy.minConfidence) return policy.fallbackTier;3return tierAnswer.choice;
这只是 policy 的一个例子。你可以根据自己的领域给答案赋予不同权重,比如高吞吐的批处理任务偏向便宜模型,或者凡是涉及生产环境的操作一律升级。Jev 只负责提供答案;怎么用这些答案,由你的代码说了算。
为什么只挑一次
router 只在请求开始时挑一次模型,然后一直用到请求结束。这是因为 prompt caching(提示缓存)。
Agent 每走一步,模型都要重新读一遍到目前为止的全部对话。AI 服务商会把最近读过的对话存起来,让重读变得便宜。但每个模型都有自己的缓存。中途换模型,新模型就得按全价把所有内容重读一遍。
Jev 的创始人在一篇关于编程 Agents 的设计文档里算过这笔账:在一次长会话中,从 Claude Opus 切到更便宜的 Sonnet 再切回来,比全程用 Opus 贵了大约 50%。所以要在对话还很短时就把模型定下来,然后坚持用下去。
3. 为 Jev 宕机做准备
这一节同时改动 gate 和 router。
一旦 harness 对每次 tool call 都要问 Jev,Agent 就依赖上了 Jev。和任何在线服务一样,Jev 也可能变慢或宕机。要提前决定各个部分拿不到答案时怎么办。不同部分的正确做法并不相同。
gate 拦截调用。 如果 gate 问不了 Jev,它就不知道这次调用是否安全。放过去可能会删掉文件,所以 gate 拒绝放行。工程师把这叫做 failing closed(故障关闭),就像断电时会自动锁上的门。
router 使用强模型。 如果 router 问不了 Jev,它就不知道请求有多难。强模型什么都能处理,所以请求依然能得到不错的答案,只是你多花了一点钱。这是 failing open(故障开放),让工作继续推进。

4. 校验答案
这一节加入 verifier(校验器)。在 Agent harness 中,verifier 是在 Agent 的工作被认定为完成之前,对其进行检查的那一步。
Agent 给出的最终答案可能漏掉了什么,或者声称了它根本没在文件里核实过的内容。读答案的人往往看不出来。在返回答案前先做检查,能在 Agent 还有机会重试时把问题揪出来。
verifier 会把最终答案,以及它所依据的文件和工具结果一起发给 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 留给代码无法做出的判断。
gate 只看 tool call 本身,也就是工具名和输入参数。这有助于防范 prompt injection(提示注入)——藏在文件或网页里的文本诱骗模型去做有害的事。gate 看不到骗局,但它能看到骗局引出的那次有害调用。
把每个决策连同背后的数字都记进日志。日志能解释某次调用为什么被拦截,也能提供真实数据来设定阈值。这个 harness 会把每个决策写成一行,存入名为 decisions.jsonl 的文件。gate 能看到 Agent 尝试做的所有事,所以日志会隐藏邮箱地址和密钥,并把过长的输入截短。
动手试试
下面的沙盒运行着本教程完成的 harness,并连接着真实的 Jev。它操作的是那个装着徒步勘测笔记的文件夹,而它的 delete_path 工具是真的能把文件删掉的。
点击这里体验:https://academy.dair.ai/resources/jev-decisions-in-a-pi-sdk-harness
为什么要自建 harness
现成的 Agent 会用内置规则来做这些决策。自定义 harness 则把它们放进你的代码里。你来挑模型、设阈值、决定什么时候让人介入,还能在日志里看清每次调用究竟为什么发生。
Jev 让这一切变得可行。每个决策只需几百毫秒,花费不到一美分的零头,所以你可以在任何需要的地方加一道检查,而不只是在负担得起完整模型调用的地方。本教程的三部分只是一个起点。针对你自己领域的自定义 harness,可以向 Jev 提出任何在那里真正重要的问题。
其他用途
同样的三部分在编程 Agents 之外也适用。
- 代码审查机器人。 给每条修改建议打分,只把值得看的展示给人。
- 记录清理。 合并前先问两条记录是不是在说同一件事,拿不准的留给人工。
- 文档流水线。 给提取出的每一页打分,只重跑低分的那些。
- 审批队列。 只有落在“问人”区间的调用才会到达人工审核者。
在所有这些场景里,语言模型负责开放式的工作,Jev 回答围绕它的小问题。
本教程中的问题、阈值和 policy 都是用于学习的示例,并非调优过的生产配置。我们正在对这些 harness 改动如何影响成本和答案质量进行基准测试,包含测试结果的后续指南很快就会发布。
我花了一晚上时间,和 Opus 5.5 一起搭出了这份指南和沙盒。如果你遇到任何问题,请私信我。欢迎复制这篇文章喂给你的 Agents,继续试验这些想法。





