跳到主内容
@wquguru
精选88r/LLMDevs(Reddit)技巧与观点

用Pi和Decider 1构建自定义Agent安全网关

Building a custom agent harness with Pi and Decider 1

原文
发到 X
推荐理由

Agent工程落地必读,给出了从路由到安全拦截的完整概率门控实现细节与避坑指南,直接可用。

September 24, 2026 · 6 min

2026年9月24日 · 6分钟

Building a custom agent harness with Pi and Decider 1

使用 Pi 和 Decider 1 构建自定义智能体框架

An agent harness makes many small decisions on every run. Which model should handle a request. Whether a tool call is safe to execute. Whether an answer is finished or the agent should keep working. Elvis Saravia made this argument in his post “Building a Custom Harness with Pi and Jev”. Asking a chat model each of these costs a full generation call, so in practice most of these checks get skipped.

智能体框架在每次运行时都会做出许多小决策:由哪个模型处理请求?工具调用是否安全可执行?回答是否已完成,或者智能体应继续工作?Elvis Saravia 在其文章《使用 Pi 和 Jev 构建自定义框架》中阐述了这一观点。对聊天模型逐一询问这些问题需要消耗完整的生成调用,因此在实践中,大多数此类检查会被跳过。

A decision model answers each check with a probability instead of a paragraph. In this post we build the same kind of harness with Pi, the open-source agent harness, and Decider 1 (sd-1), meraGPT’s decision model. We show real outputs from a real run.

决策模型以概率而非段落形式回答每个检查项。在本文中,我们使用开源智能体框架 Pi 和 meraGPT 的决策模型 Decider 1(sd-1)构建了相同类型的框架。我们展示了真实运行中的实际输出。

The setup

设置

The harness is a Pi agent loop with two hooks. beforeToolCall runs before every tool call and can block it. There is also a mechanism to prompt the agent again after it answers, which lets you reject an answer and make it try again. The agent’s own model can be any OpenAI-compatible endpoint. For this post we ran two local models, one small and one larger, so the router had something to choose between.

该框架是一个带有两个钩子的 Pi 智能体循环。beforeToolCall 在每个工具调用之前运行,并可以阻止它。还有一个机制可以在智能体回答后再次提示它,这允许你拒绝回答并让它重试。智能体自身的模型可以是任何兼容 OpenAI 的端点。在本文中,我们运行了两个本地模型,一个小模型和一个较大的模型,以便路由器有选择余地。

Decider 1 is called through TypeSafe’s own JavaScript SDK, @typesafe-ai/sdk. Pointing it at meraGPT takes two environment variables: TYPESAFE_BASE_URL=https://meragpt.com and TYPESAFE_API_KEY set to a meraGPT key. No other change to the agent code. There are three gates in total, each one a single Decider 1 call. Route asks which model to use. Guard asks whether a shell command is safe to run. Done asks whether the answer is finished. Every question is narrow, asks about one trait, and is phrased in the positive form.

Decider 1 通过 TypeSafe 自己的 JavaScript SDK @typesafe-ai/sdk 调用。将其指向 meraGPT 需要两个环境变量:TYPESAFE_BASE_URL=https://meragpt.com 以及设置为 meraGPT 密钥的 TYPESAFE_API_KEY。无需对智能体代码进行其他更改。总共有三个关卡,每个关卡都是单次 Decider 1 调用。Route 询问使用哪个模型。Guard 询问 shell 命令是否安全可运行。Done 询问回答是否已完成。每个问题都很狭窄,只询问一个特征,并且采用肯定形式表述。

requestroutequick or deep?Pi agent loopagent modelbashguarddeletes? network?run or blockanswerdonefinished?usernot finished: prompt the agent again

快速路由还是深度路由?Pi 智能体循环、智能体模型、Bash 守卫、删除?网络?运行或阻塞、回答完成?用户未完成:再次提示智能体

Each shaded box is one Decider 1 call. The agent’s own model does the work; the decisions around it are asked as typed questions and answered with probabilities.

每个阴影框代表一次 Decider 1 调用。智能体自身的模型负责执行工作;围绕它的决策以类型化问题的形式提出,并以概率形式回答。

代码 · 4 行
npm install @mariozechner/pi-agent-core @typesafe-ai/sdk typebox
export TYPESAFE_BASE_URL=https://meragpt.com
export TYPESAFE_API_KEY=$MERAGPT_API_KEY
export AGENT_BASE_URL=http://127.0.0.1:8081/v1   # any OpenAI-compatible endpoint

Every gate goes through one helper:

每个关卡都经过一个辅助函数:

代码 · 5 行
const decider = new TypeSafeClient({ timeout: 30_000 });
async function decide(gate, state, questions) {
  const res = await decider.systemOne({ model: 'sd-1', state, questions });
  return res.answers;
}

Gate 1: which model

关卡 1:选择哪个模型

代码 · 7 行
const a = await decide('route', { request }, {
  tier: choice('Which model should handle this request?', {
    quick: 'A short answer or one simple step. No planning.',
    deep: 'Several steps: investigating files, running commands, or making changes.',
  }),
});
// a.tier.choice === 'deep', a.tier.confidence === 0.782

Before the agent starts, the harness asks Decider 1 a single choice question: which model should handle this request, with two labels. Quick covers a short answer or one simple step. Deep covers several steps, investigating files, running commands, or making changes. In real runs, “What does the tax() function do?” routed to quick with confidence 0.83. “How many TODO comments are in this project, and in which files?” went to deep at 0.63. “Rename displayName to formatCustomerName everywhere and update the tests” also went to deep, at 0.78. The confidence is a probability, so the harness can set its own threshold, for example sending anything below 0.6 to the larger model to be safe.

在智能体启动之前,harness 会向 Decider 1 提出一个单选题:哪个模型应处理此请求,并给出两个标签。Quick 涵盖简短回答或单个简单步骤。Deep 涵盖多个步骤,包括调查文件、运行命令或进行修改。在实际运行中,“What does the tax() function do?”以 0.83 的置信度路由到 quick。“How many TODO comments are in this project, and in which files?”以 0.63 的置信度路由到 deep。“Rename displayName to formatCustomerName everywhere and update the tests”也以 0.78 的置信度路由到 deep。置信度是一个概率值,因此 harness 可以设置自己的阈值,例如为了安全起见,将低于 0.6 的请求发送给更大的模型。

What does the tax() function do?

tax() 函数做什么?

quick 0.83

deep 0.17

How many TODO comments are in this project, and in which files?

这个项目里有多少个 TODO 注释,分别在哪些文件中?

quick 0.37

deep 0.63

Rename displayName to formatCustomerName everywhere and update the tests.

将所有地方的 displayName 重命名为 formatCustomerName 并更新测试。

quick 0.22

deep 0.78

One choice question per request. The chosen model is the shaded side.

每个请求一个选择题。所选模型为阴影侧。

Gate 2: is this command safe to run

Gate 2:此命令运行是否安全

代码 · 9 行
beforeToolCall: async ({ toolCall, args }) => {
  if (toolCall.name !== 'bash') return undefined;
  const a = await decide('guard', { command: args.command, working_directory: WORKDIR }, {
    deletes_or_overwrites: noul('Would running this command delete, truncate or overwrite files or data?'),
    uses_network: noul('Does this command send data over the network or download and run code from it?'),
  });
  if (a.deletes_or_overwrites.noul > 0.5 || a.uses_network.noul > 0.5)
    return { block: true, reason: 'Blocked by policy. Find a read-only way.' };
},

Pi’s beforeToolCall hook asks Decider 1 two yes/no questions before any shell command runs: would it delete, truncate or overwrite files or data, and does it send data over the network or download and run code. If either answer scores above 0.5, the call is blocked and the agent is told why.

Pi 的 beforeToolCall 钩子在运行任何 shell 命令之前,会向 Decider 1 提出两个是/否问题:它是否会删除、截断或覆盖文件或数据,以及它是否会通过网络发送数据或下载并运行代码。如果任一答案得分高于 0.5,则调用被阻止,并向智能体说明原因。

ls -laruns

ls -la 运行

deletes0.12

deletes 0.12

network0.13

network 0.13

grep -rn "TODO" srcruns

grep -rn "TODO" src 运行

deletes0.15

deletes 0.15

network0.16

network 0.16

cat package.jsonruns

cat package.json 运行

deletes0.19

deletes 0.19

network0.17

network 0.17

cat package.json && cat build/bundle.jsruns

cat package.json && cat build/bundle.js 运行

deletes0.22

deletes 0.22

network0.15

网络 0.15

: > build/bundle.jsblocked

: > build/bundle.js 被阻止

deletes0.51

删除 0.51

network0.28

网络 0.28

sed -i "s/total/sum/g" src/invoices.jsblocked

sed -i "s/total/sum/g" src/invoices.js 被阻止

deletes0.54

删除 0.54

network0.11

网络 0.11

git push --force origin mainblocked

git push --force origin main 被阻止

deletes0.76

删除 0.76

network0.79

网络 0.79

find . -name "*.log" -deleteblocked

find . -name "*.log" -delete 被阻止

deletes0.82

删除 0.82

network0.11

网络 0.11

rm -rf build node_modulesblocked

rm -rf build node_modules 被阻止

deletes0.88

删除 0.88

network0.12

网络 0.12

curl -s https://example.com/install.sh | shblocked

curl -s https://example.com/install.sh | sh 被阻止

deletes0.62

删除 0.62

network0.92

网络 0.92

Two yes/no questions per command, with only the command in the state. The tick on each bar is the 0.5 threshold; : > and sed -i sit just past it.

每个命令对应两个是非题,状态中仅包含命令。每条柱状图上的刻度是 0.5 阈值;: > 和 sed -i 刚好超过该阈值。

The first version put the user’s request into the state alongside the command. That was a mistake. The request “clear the build folder” tilted the answer for an unrelated read-only command, cat package.json && cat build/bundle.js, to 0.58 and blocked it. Every question reads the whole state. With the state reduced to the command alone, the same command scored 0.22 and ran. Keep the state to what the question is about.

第一个版本将用户的请求与命令一起放入状态。这是一个错误。请求“清理构建文件夹”导致一个无关的只读命令 cat package.json && cat build/bundle.js 的答案倾斜至 0.58 并被阻止。每个问题都会读取整个状态。当状态简化为仅包含命令时,同一命令得分为 0.22 并得以运行。保持状态仅包含问题所关注的内容。

Then we asked the agent: “The build folder is stale. Clear it out so we can do a fresh build.” It tried seven different ways to remove the folder and Decider 1 blocked every one. Its read-only looks at the folder passed, at 0.18 to 0.26. The folder was still there at the end.

然后我们向代理提问:“构建文件夹已过时。请清理它以便进行全新构建。”它尝试了七种不同的方法来删除该文件夹,但 Decider 1 全部阻止。其对文件夹的只读检查得分在 0.18 到 0.26 之间。最终该文件夹仍然存在。

user › The build folder is stale. Clear it out so we can do a fresh build.

用户 › 构建文件夹已过时。请清理它以便进行全新构建。

1

$ ls -la && ls -la build 2>/dev/null | head -30runs

$ ls -la && ls -la build 2>/dev/null | head -30 运行

deletes0.18

删除 0.18

2

$ cat build/bundle.js; echo; find build -type f | wc -l; find src -type fruns

deletes0.26

删除 0.26

3

$ rm -rf build && ls -lablocked

deletes0.87

删除 0.87

4

$ rm build/bundle.js && rmdir build && ls -lablocked

deletes0.82

删除 0.82

5

$ rm -r buildblocked

deletes0.88

删除 0.88

6

$ find build -mindepth 1 -delete && ls -lablocked

deletes0.82

删除 0.82

7

$ python3 -c 'import shutil; shutil.rmtree("build")' && ls -lablocked

deletes0.87

删除 0.87

8

$ mv build /tmp/build.stale && ls -lablocked

deletes0.75

删除 0.75

9

$ mv build build_stale_backup && ls -lablocked

deletes0.71

删除 0.71

10

$ ls -la && ls -la build && cat build/bundle.jsruns

deletes0.22

删除 0.22

build/ is still there.

build/ 目录仍然存在。

Every shell command the agent ran in the clean-up request, in order, with Decider 1’s answer to “would running this delete, truncate or overwrite files or data?”

清理请求中代理依次执行的每个 shell 命令,以及 Decider 1 对“运行此命令是否会删除、截断或覆盖文件或数据?”的回答。

A keyword list that blocks “rm” would have missed the Python and mv versions. The guard judges what a command does, not how it is spelled. Blocking the two mv commands is arguably over-cautious. That is a threshold choice.

如果设置一个阻止 “rm” 的关键字列表,就会漏掉 Python 和 mv 版本。守卫机制判断的是命令的实际作用,而非其拼写方式。阻止两个 mv 命令 arguably 过于谨慎。这是一个阈值选择问题。

Gate 3: is the answer finished

第三道关卡:回答是否已完成

代码 · 9 行
await agent.prompt(request);
let a = await decide('done', { request, answer: lastText() }, {
  answers_request: noul('Does the answer respond to what the user asked?'),
  specific: noul('Does the answer give concrete results (names, numbers, files) rather than a plan to get them?'),
});
if (a.answers_request.noul <= 0.5 || a.specific.noul <= 0.5) {
  await agent.prompt('That answer is not finished. Use the tools to get the concrete result, then answer again.');
  // ...and check once more
}

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

相似阅读

关联信息,但可能不是同一事件