跳至主要内容
返回博客

Agent 护栏怎么做?用是非概率拦截危险工具调用和违规内容

查看示例

怎么用是非概率做审核和拦截? 把审核政策拆成几个是非问题,例如“是否针对特定群体”“这次工具调用是否会改动生产数据”,让决策模型为每个问题返回为真的概率;需要区分严重程度时,再加一个评分问题。代码按误判成本设两个阈值:高于上限就拦截,处于中间就转人工,低于下限就放行。概率不是授权,高风险操作仍然需要权限检查和人工审批。

Choice、Score、Noul 三种原语的基础知识见 决策模型是什么?。

Agent 护栏是什么:输入、输出、工具三类检查点

OpenAI Agents SDK(JS)的文档把护栏分成三类:

  • 输入护栏:针对初始用户输入运行,只对第一个接收用户输入的 Agent 生效。
  • 输出护栏:针对 Agent 的最终输出运行,只在生成最终输出的 Agent 上运行。
  • 工具护栏:在每次自定义函数工具调用前后运行。工具输入护栏可以返回 allow(继续)、rejectContent(用一条消息跳过这次调用)或 throwException(抛出熔断错误)。

文档还提到两种执行方式:输入护栏默认与 Agent 并行运行(runInParallel: true),延迟最低,但护栏触发时模型可能已经消耗了 token 或运行了工具;设为 false 则在调用模型之前先跑完护栏。文档还建议,可以先运行一个轻量级模型作为护栏,再调用成本较高的模型。

阿里云的 AgentScope 集成指南给出了另一组拦截点:pre_reply 在 Agent 回复前检测用户输入;post_reasoning 在推理完成后检测最终输出;toolkit middleware 在工具执行前检测“工具名 + 参数”,违规就阻止调用。

护栏和人工审批是两件事。OpenAI Agents SDK 的人工审批通过工具的 needsApproval 实现:需要审批时运行暂停,返回 interruptions,批准或拒绝后从同一个 RunState 恢复。默认情况下,工具输入护栏在审批通过后、工具执行前运行;设置 toolExecution: { preApprovalInputGuardrails: true } 后,护栏还会在发出审批请求之前先跑一次。

这些框架把“检查点放哪”讲得很清楚,但护栏函数内部凭什么判断,要你自己决定。

护栏的判断用什么做

常见的选择有四种:

  • 规则和正则:确定、便宜,适合格式明确的东西,比如 OpenAI 文档示例里拦截参数中含 sk- 的密钥。但它覆盖不了换个说法的内容。
  • 再调一次生成式 LLM:OpenAI 文档的输入护栏示例就是在护栏里运行另一个 Agent,让它输出一个布尔字段。它能理解语义,但输出的是“是 / 否”,没有可调的阈值。
  • 专用审核服务:例如阿里云 AI 安全护栏,通过插件挂到 AgentScope 的各个拦截点上。审核类别和判定标准由服务方定义。
  • 决策模型的 Noul:为你自己写的是非问题返回 P(true)。你可以针对自己的政策写问题,并按误判成本设阈值。

四种方式可以组合使用:规则先挡掉格式明确的情况,Noul 判断需要理解语义的政策,高风险动作仍然走人工审批。

把政策拆成 Noul 问题

一条模糊的政策,比如“不要发布攻击性内容”,很难直接判断。把它拆成几个可以回答“是 / 否”的问题:

  • 这段内容是否针对某个特定群体?
  • 这段内容是否在索要密码、验证码等凭证?
  • 这次工具调用是否会改动生产数据?

System One 的决策原语文档写明:Noul 估计“某个具体条件是否成立”,返回 P(true),范围 0 到 1;如果多个条件可以同时成立,应为每个条件分别定义 Noul 问题,这些概率对应不同的命题,不要求相加等于一。SDK 里用 booleanQuestion() 定义,并用 criteria 写清楚 true 和 false 各指什么。Microsoft Learn 的 Microsoft-Decision-1 文档也建议用 noul 做“门禁和过滤”,示例问题是“这条回复是否承诺给客户退款”。

需要区分严重程度时,再加一个 Score 问题,把档位按顺序写出来,例如“安全 / 擦边 / 违规 / 必须删除”。System One 文档写明,托管 API 要求 2–10 个有序档位,分数是按各档概率加权得出的,不能当作正确率的百分比。

在线试一下:推文会不会被审核(示例结果,登录后可跑真实数据)

工具调用前拦截

把工具调用作为 state 送去判断。AgentScope 集成指南的做法是把工具名和参数拼成 Tool: {name}\nArguments: {json} 的格式送检,可以参考。state 里只放判断所需、允许分享的信息,不要放密钥。

如果你在用 OpenAI Agents SDK,这个判断放在工具输入护栏里:低风险放行时返回 allow;需要拒绝但让 Agent 继续时返回 rejectContent,并附一条说明;需要整个运行停下时用 throwException。付款、删除数据、对外发消息这类高风险工具,仍然设置 needsApproval,由人来批准。

System One 的 Agent 接入文档写明:模型结果不是授权,操作人的身份和权限、动作白名单、必要的审批都要在模型之外检查;答案缺失或请求失败表示“未解决”,不能当成自动的“是”或“否”。所以护栏调用出错时,应该按“未通过”处理,而不是放行。工具路由里的白名单思路,见 Agent 工具太多总选错?用 Choice 做工具路由。

三段阈值与人工审核

问题按“是否违规”的方向写时:

  • P(true) 高于上限 → 拦截;
  • 介于上下限之间 → 转人工审核;
  • 低于下限 → 放行。

如果问题写成“是否安全”,方向就反过来。两个阈值都不能照搬通用的百分比。System One 文档要求阈值来自你自己的评估案例和误判后果;Microsoft Learn 也建议按误报和漏报的代价来设阈值。漏拦代价高的场景(比如付款),上限要放低,让更多请求进入人工审核;误拦代价高的场景,人工区间可以收窄。具体怎么用标注样本选阈值、再用留出数据复核,参见 用 Score 替代 LLM 当裁判。Noul 阈值和动作的对应方式,也可以参考 Agent 什么时候算完成?中的动作表。

Microsoft Learn 还给了几条建议:问题和选项用清晰、中立的措辞;不应强行选择时加入“cannot tell”这类弃权选项;可以随机化选项顺序,并检验顺序变化是否影响结果;涉及个人的重大决定(信贷、就业、住房、医疗、法律)只把模型当作决策支持,并保留有意义的人工复核。

评估:漏拦率和误拦率分开算

在自己的标注集上分别统计两件事:该拦没拦(漏拦),不该拦却拦了(误拦)。两者的代价不同,合成一个准确率会掩盖问题。System One 的 Agent 接入文档也要求把“错误选择”和“遗漏审核”分开统计,并加入试图强迫模型改变选择的输入。

测试集里应该包括:换个说法表达同一个意思的内容、提示注入(比如在参数里写“忽略之前的规则”)、措辞很冲但无害的内容。Microsoft Learn 列出的已知局限包括:评分会随问题和选项的措辞、顺序变化;用作安全过滤时,可能漏掉隐蔽的有害内容,也可能把无害内容标记出来。这些都只能靠自己的数据测出来。

TypeScript 示例

开发团队的示例(examples/decision-articles/08-agent-guardrails.ts)用 booleanQuestion() 定义三个是非问题:是否针对特定群体、是否索要凭证或密钥、这次工具调用是否会改动生产数据或对外发消息。三个概率取最大值作为风险值,任何一条规则都可以触发护栏;风险值不低于 BLOCK_IF_RISK_AT_LEAST(0.8)时拦截,不低于 REVIEW_IF_RISK_AT_LEAST(0.4)时交给应用自己的人工审核回调,否则放行。两个阈值都是示例值,注释写明要在标注案例上调。示例注释还写明:P(true) 不是已确认的事实,也不是授权;System One 没有审核、审批或 needsApproval 接口。

ts/**
 * Article 8 — Agent guardrails ("Agent 护栏", /zh-CN/blog/agent-guardrails).
 *
 * Split a moderation policy into independent yes/no questions via
 * `booleanQuestion()` (wire type `noul`, SDK answer `.probability`). Those
 * probabilities do not sum to 1. This file combines them with max() — any
 * rule can trip the guardrail — then maps the combined risk onto two
 * application-owned cutoffs: block / human review / allow.
 *
 * P(true) is not a confirmed fact and is not authorization. Human review is
 * an application callback. System One has no moderation, approval, or
 * `needsApproval` API. Packages: @system-one-ai/core, adapter-system-one,
 * transport-fetch. Endpoint: POST /v1/systemone.
 */
import { createSystemOne, booleanQuestion } from '@system-one-ai/core';
import type { EvaluationClient } from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';

export const guardrailQuestions = {
  targetsGroup: booleanQuestion(
    'Does this content target a specific group with abuse or harassment?',
    { true: 'The text attacks a named group', false: 'The text does not target a group' },
  ),
  requestsSecrets: booleanQuestion(
    'Does this content ask for credentials, API keys, or other secrets?',
    { true: 'The user is asked to reveal a secret', false: 'No secret is requested' },
  ),
  changesProduction: booleanQuestion(
    'Would executing this tool call change production data or send an external message?',
    { true: 'The call writes, deletes, pays, or posts', false: 'The call is read-only or local' },
  ),
};

export type GuardrailRule = keyof typeof guardrailQuestions;
export type GuardrailAction = 'block' | 'review' | 'allow';

export interface GuardrailProbabilities {
  targetsGroup: number;
  requestsSecrets: number;
  changesProduction: number;
}

/** High risk: false allow is costly. Tune on labeled cases; not a hosted default. */
export const BLOCK_IF_RISK_AT_LEAST = 0.8;
/** Mid band: send to a person. Must be below the block cutoff. */
export const REVIEW_IF_RISK_AT_LEAST = 0.4;

export interface HumanReviewInput {
  text: string;
  tool?: string;
  args?: string;
  risk: number;
  probabilities: GuardrailProbabilities;
}

export function createHostedClient() {
  const apiKey = process.env.SYSTEM_ONE_API_KEY;
  const baseURL = process.env.SYSTEM_ONE_BASE_URL;
  const model = process.env.SYSTEM_ONE_MODEL;
  if (!apiKey || !baseURL || !model) {
    throw new Error('Set SYSTEM_ONE_API_KEY, SYSTEM_ONE_BASE_URL (…/v1), and SYSTEM_ONE_MODEL from GET /v1/models.');
  }
  return createSystemOne({
    apiKey, baseURL, model, adapter: systemOneAdapter,
    transport: createFetchTransport(), timeoutMs: 30_000, maxRetries: 0,
  });
}

/** Conservative combine: the highest P(true) among independent Noul rules. */
export function combineRiskProbability(probabilities: GuardrailProbabilities) {
  return Math.max(probabilities.targetsGroup, probabilities.requestsSecrets, probabilities.changesProduction);
}

export function applyGuardrailPolicy(risk: number): GuardrailAction {
  if (risk >= BLOCK_IF_RISK_AT_LEAST) return 'block';
  if (risk >= REVIEW_IF_RISK_AT_LEAST) return 'review';
  return 'allow';
}

/**
 * @param sendToHumanReview Your approval queue. Not a System One method.
 */
export async function screenWithGuardrails(
  client: EvaluationClient,
  input: { text: string; tool?: string; args?: string },
  sendToHumanReview: (input: HumanReviewInput) => Promise<string>,
) {
  const result = await client.evaluate({
    state: {
      text: input.text,
      tool: input.tool ?? null,
      args: input.args ?? null,
    },
    questions: guardrailQuestions,
  });
  const probabilities: GuardrailProbabilities = {
    targetsGroup: result.answers.targetsGroup.probability,
    requestsSecrets: result.answers.requestsSecrets.probability,
    changesProduction: result.answers.changesProduction.probability,
  };
  const risk = combineRiskProbability(probabilities);
  const action = applyGuardrailPolicy(risk);

  if (action === 'block') {
    return { action, risk, probabilities, model: result.model };
  }
  if (action === 'review') {
    const reviewed = await sendToHumanReview({
      text: input.text,
      tool: input.tool,
      args: input.args,
      risk,
      probabilities,
    });
    return { action, risk, probabilities, model: result.model, reviewed };
  }
  return { action, risk, probabilities, model: result.model };
}

async function main() {
  const text = process.env.EXAMPLE_MESSAGE ?? 'Please post this refund to the production ledger.';
  const decision = await screenWithGuardrails(
    createHostedClient(),
    { text, tool: process.env.EXAMPLE_TOOL, args: process.env.EXAMPLE_TOOL_ARGS },
    async () => {
      throw new Error('No System One human-review API. Wire your approval queue here.');
    },
  );
  console.log(JSON.stringify(decision, null, 2));
}

if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split(/[\\/]/).pop() ?? '')) {
  main().catch((error: unknown) => {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  });
}

这个示例没有把判断挂到 OpenAI Agents SDK 的工具输入护栏上,也没有用 Score 区分严重程度;接入时要由你自己把返回的 block / review / allow 对应到框架的 rejectContent、needsApproval、allow 等行为。

延伸阅读

护栏判断方式对比(核验日期 2026-10-11)

方式输出形态能否按误判成本设阈值适用场景来源
规则 / 正则来源: OpenAI Agents SDK 护栏 · 核验日期 2026-10-11命中 / 未命中,由代码返回 `allow` 或 `rejectContent`来源: OpenAI Agents SDK 护栏 · 核验日期 2026-10-11不能,只有命中与否格式明确的内容,如密钥官方文档示例
生成式 LLM 判断来源: OpenAI Agents SDK 护栏 · 核验日期 2026-10-11护栏 Agent 输出结构化结果(示例为布尔字段),据此决定是否触发熔断来源: OpenAI Agents SDK 护栏 · 核验日期 2026-10-11示例中没有概率可调需要理解语义的检查;文档建议可用轻量级模型官方文档
专用审核服务来源: 阿里云 AgentScope 集成 AI 安全护栏 · 核验日期 2026-10-11在拦截点上阻断、标记或替换内容来源: 阿里云 AgentScope 集成 AI 安全护栏 · 核验日期 2026-10-11可以:控制台可为精细化风险标签配置命中阈值,阈值取模型输出的 0–100 置信分,最小步长 1;API 按设置的高低风险分返回 RiskLevel(high / medium / low / none),文档建议高风险直接处置、中风险人工复查;[阿里云 AI 安全护栏文本审核 API](https://help.aliyun.com/zh/document_detail/2875414.html) · 2026-10-11来源: 阿里云 AI 安全护栏产品简介 · 核验日期 2026-10-11通用内容安全;类别由服务方定义官方文档
决策模型 Noul(System One)来源: System One 决策原语 · 核验日期 2026-10-11每个是非问题返回 P(true),0 到 1来源: System One 决策原语 · 核验日期 2026-10-11能,阈值由应用按评估案例和误判后果设定针对自己政策的判断;system-one.dev 提供 Jev(`typesafe/jev`)、Clef Flash(`cloudflare/clef-flash`)和 Clef(`cloudflare/clef`)模型服务官方文档
决策模型 noul(Microsoft-Decision-1)来源: Microsoft Learn · 核验日期 2026-10-11是非条件的概率,文档推荐用于门禁和过滤来源: Microsoft Learn · 核验日期 2026-10-11能,文档建议按误报和漏报代价设阈值在 Microsoft Foundry 上调用;尚未在 System One 启用官方文档

常见问题

Agent 护栏是什么?输入、输出、工具三类护栏各管什么?
护栏是在用户输入、模型输出和工具调用前后自动运行的检查。按 OpenAI Agents SDK 文档:输入护栏检查初始用户输入;输出护栏检查最终输出;工具护栏在每次函数工具调用前后运行,可以放行、拒绝这次调用或抛出熔断错误。
护栏的判断用规则、LLM 还是决策模型?
可以组合。规则适合格式明确的内容;生成式 LLM 能理解语义,但通常只给“是 / 否”;决策模型的 Noul 为你写的是非问题返回概率,可以按误判成本设阈值。高风险动作无论用哪种判断,都要保留人工审批。
怎么用 Noul 写审核问题?什么时候加 Score?
把一条政策拆成几个可以回答“是 / 否”的具体问题,每个问题单独定义,用 criteria 写清 true 和 false 的含义。需要按“安全 / 擦边 / 违规 / 必须删除”这样的档位区分严重程度时,再加一个 Score 问题。
阈值怎么定?中间值怎么办?
用两个阈值分三段:超过上限拦截,中间转人工,低于下限放行。阈值来自你自己的标注案例和误判代价,不要用通用的百分比;漏拦代价越高,进入人工区间的范围就应该越大。
概率高就能直接执行吗?护栏能代替权限和人工审批吗?
不能。System One 文档写明,模型结果不是授权,权限、动作白名单和必要的审批都要在模型之外检查。付款、删除数据、对外发消息这类动作,仍然需要按政策确认。
怎么防提示注入?
把提示注入样本(例如参数里写“忽略之前的规则”)放进评估集,单独统计漏拦。Microsoft Learn 提醒,评分会受措辞影响,用作安全过滤时也可能漏掉隐蔽的有害内容,所以护栏不能是唯一的防线,权限控制和人工审批仍然要保留。
护栏会拖慢 Agent 吗?
OpenAI Agents SDK 提供两种执行方式:护栏与 Agent 并行运行,延迟最低,但触发时模型或工具可能已经开始工作;或者先跑完护栏再调用模型,避免浪费 token 和误执行工具。具体延迟取决于你选的模型和部署,本文没有实测数据。