CodexQA

行业与实践工具与框架

Inspect 工具审批:人审、自动放行和桥接智能体

CodexQA 团队阅读约 11 分钟

审批模式打开后,没有被策略处理的工具调用会被拒绝。桥接智能体连续三次被拒绝的回复会终止样本。远程 MCP(execution=remote)在审批策略活动时不能用来生成。

本文目录

工具审批

UK AI Security Institute。Tool Approval。文档页无单独发布日期(检索于 2026-11-02)。许可证:MIT License,Copyright (c) 2024 UK AI Security Institute。

概述

Inspect 的审批模式(approval mode,工具调用在执行前先过一道决定)让你为模型发出的工具调用(tool call,模型请求执行某个工具并带上参数)制定细粒度策略。例如,下面这些都支持:

  1. 全部工具调用由人类操作者批准。
  1. 选出的一部分工具调用由人类操作者批准(其余不经批准就执行)。
  1. 自定义审批器(approver,决定批准、拒绝或交给下一个审批器的函数)决定批准、拒绝,或升级给另一个审批器。升级(escalate)是不自己做最终决定,而是把这一次调用交给链上的下一个审批器。

自定义审批器很灵活,可以实现多种决定方案,包括非正式的启发式规则,以及由模型来评估。它们也可以支持在远程系统上用自定义界面做人工批准(批准通过消息队列发送和接收)。

审批器可以设在评测级,也可以设在任务级。下面的例子演示评测级审批器。任务级见任务审批器。

人工审批器

最简单的审批策略是:所有工具调用都由人交互式批准。用 CLI 选项 --approval human(或传给 eval() 的参数 approval = "human")打开这一策略:

inspect eval browser.py --approval human

这个例子给模型内置的网页浏览器工具,并让它打开一个网页做搜索。

人工审批器默认提供批准、拒绝和终止(human_approver(choices=...) 可以改这些选项)。它不能提供修改,因为操作者没有办法录入一次修改后的工具调用:如果 choices 里包含 "modify",会丢掉并给出警告;如果 "modify" 是唯一选项,human_approver() 会报错。

自动审批器

一旦打开审批模式,所有工具调用都必须以某种方式被处理(否则会被拒绝)。但是逐个批准每一次调用会很烦,也不是每一次调用都值得人来盯。

可以把 human 和 auto 审批器串成一条审批策略(approval policy,按顺序尝试的审批器列表),只批准选中的那些调用。例如,这里的策略只对交互式网页浏览器工具调用要求人工批准:

approvers:
  - name: human
    tools: ["web_browser_click", "web_browser_type"]

  - name: auto
    tools: "*"

导航类网页浏览器工具调用(例如 web_browser_go)由链末尾兜底的 auto 审批器自动批准。在策略里列出审批器时,用一个 glob 或一组 glob 标明它处理哪些工具。这些 glob 按前缀匹配,所以 web_browser_type 同时匹配 web_browser_type 和 web_browser_type_submit。

要用这条策略,把策略 YAML 文件的路径当作审批器传入。例如:

inspect eval browser.py --approval approval.yaml

也可以匹配工具参数(对会分发多种动作类型的工具)。例如,下面是计算机工具的审批策略:允许打字和移动鼠标,但对组合键(例如 Enter 或快捷键)和打字要求批准。原文代码块前面单独有一行文件名 approval.yaml,匹配模式按页面原文没有闭合括号:

approval.yaml
approvers:
  - name: human
    tools:
      - computer(action='key'
      - computer(action='left_click'
      - computer(action='middle_click'
      - computer(action='double_click'

  - name: auto
    tools: "*"

因为这是前缀匹配,而且后面还可能有别的参数,工具匹配模式的末尾不加括号。

在代码里写审批器

上面演示了用 YAML 审批策略文件配置审批器。也可以直接在代码里给出策略(需要更动态时有用)。下面是上一节例子的纯 Python 版本:

from inspect_ai import eval
from inspect_ai.approval import ApprovalPolicy, human_approver, auto_approver

approval = [
    ApprovalPolicy(human_approver(), ["web_browser_click", "web_browser_type*"]),
    ApprovalPolicy(auto_approver(), "*")
]

eval("browser.py", approval=approval, trace=True)

任务审批器

创建 Task 时,可以用 approval 参数在任务级指定审批策略。例如:

from inspect_ai import Task, task
from inspect_ai.scorer import match
from inspect_ai.solver import generate, use_tools
from inspect_ai.tool import bash, python
from inspect_ai.approval import human_approver

@task
def linux_task():
    return Task(
        dataset=read_dataset(),
        solver=[
            use_tools([bash(), python()]),
            generate(),
        ],
        scorer=match(),
        sandbox=("docker", "compose.yaml"),
        approval=human_approver()
    )

和其他 Task 选项一样,评测级定义的 approval 策略会覆盖任务级审批策略。

上下文管理器

可以用 approval() 上下文管理器(context manager,进入一段代码时换上新设置、离开时恢复旧设置的对象),在一次正在跑的评测里临时覆盖审批策略。当求解器或智能体对某一段工具调用需要不同的审批策略时,这很有用:

from inspect_ai.approval import approval, ApprovalPolicy, human_approver, auto_approver

async def my_solver(state):
    # Use human approval for a critical section
    with approval([ApprovalPolicy(human_approver(), "*")]):
        # tool calls within this block require human approval
        ...

    # Outside the block, previous approval policies are restored
    ...

上下文管理器在其持续期间替换当前审批策略,退出时恢复之前的策略。支持嵌套:每一层嵌套的 approval() 上下文设置自己的策略,退出时正确恢复外层策略。

execute_tools() 函数和 react() 智能体也为方便接受 approval 参数,在工具执行期间应用审批策略:

from inspect_ai.model import execute_tools
from inspect_ai.approval import ApprovalPolicy, human_approver

result = await execute_tools(
    messages, tools,
    approval=[ApprovalPolicy(human_approver(), "*")]
)
from inspect_ai.agent import react
from inspect_ai.approval import ApprovalPolicy, human_approver

agent = react(
    tools=[bash(), python()],
    approval=[ApprovalPolicy(human_approver(), "*")]
)

桥接智能体

通过智能体桥接入的智能体自己跑工具循环,所以 Inspect 从不执行它们的工具调用。桥接智能体(bridged agent,工具循环在智能体自己那边、Inspect 只看到模型回复的智能体)的审批策略改为作用在每一次模型回复里的工具调用上,在把该回复交回智能体之前。这同时覆盖 agent_bridge() 和 sandbox_agent_bridge(),以及它们支持的每一种 API 方言。

评测级和任务级策略会自动生效。也可以把策略直接限定在一座桥上:

async with sandbox_agent_bridge(
    state,
    approval=[
        ApprovalPolicy(human_approver(), "bash"),
        ApprovalPolicy(auto_approver(), "*"),
    ],
) as bridge:
    ...

对沙箱桥来说,这个参数是从智能体内部设置策略的唯一可靠办法:桥接生成跑在沙箱服务任务里,看不到在智能体函数体里进入的 approval() 上下文管理器。

各种决定的行为如下:

决定对桥接智能体的行为
approve这次调用交给智能体,由它照常执行。
modify修改后的参数交给智能体。把调用改成另一个函数会使样本以错误失败。没有修改后调用的 modify 当作拒绝。
reject这次调用从不交给智能体。模型被告知被拒绝,并被要求重新生成;智能体只看到替换后的回复。
terminate样本被终止。
escalate交给链上的下一个审批器,和其他地方一样。

有两种行为是桥接智能体特有的:

  1. 一次拒绝会丢掉同一条回复里的每一次工具调用。 每一次调用都会得到一条结果,说明发生了什么;本身没有被拒绝的那些调用会被告知是哪一次调用导致的,这样模型可以单独再发出它们。这和 Inspect 自己执行的工具不同,后者每次调用独立处理。
  1. 连续三次被拒绝的回复会终止样本。 没有这一条,一个不断提出被拒绝调用的模型会无限重试。

因为 Inspect 不执行这些工具调用,不会为它们记录工具事件。记录里显示的是审批决定,而不是一次带有审批的工具调用。读日志时由此有两点:

  • 调用按顺序审批,评测在第一次拒绝处停下。因此,紧挨在被拒绝的兄弟调用之前、已被批准的那一次,即使从未运行,也会记下一次审批。桥接智能体的审批决定记录的是这次调用被允许,不是它已经被执行。
  • auto 审批器的自动批准完全不显示。如果想让每一次决定都有可见记录,就用 auto 以外的审批器。

modify 决定在日志里保留原来的调用:记录下来的模型输出和审批事件显示模型提出了什么,审批事件的 modified 字段显示被替换成了什么。

对带有桥接工具的沙箱桥,决定还决定智能体能在宿主上执行什么。桥接工具只对模型在一次桥接生成里提出的调用运行,每个提议一次,参数是批准后的参数(因此 modify 决定授予的是修改后的调用,不是原来的);没有匹配提议的 tools/call 会被拒绝,智能体把错误交给模型,由模型重新提议。没有配置审批策略时也是这样。用 BridgedToolsSpec(require_proposal=False) 注册的服务器放弃这种对应关系:它的工具对智能体发出的任何 tools/call 都会运行。见执行契约。在 Codex CLI 的代码模式下,模型只提议运行其脚本的那一次 exec 调用,所以策略审查的是这次调用;脚本发出的宿主工具调用不由任何策略审查(而且要能运行,需要 require_proposal=False)。

远程 MCP 服务器

远程 MCP 服务器(execution="remote")的工具在生成期间由模型供应商调用,所以 Inspect 无法批准那些调用。审批策略处于活动状态时,带远程 MCP 服务器去生成会报错,而不是把服务器发给供应商。这对任何策略都适用,包括只点名其他工具的策略,因为没有策略匹配到的工具调用会被拒绝。传给 react()、agent_bridge() 和 sandbox_agent_bridge() 的策略也一样。用 execution="local",服务器的工具调用就会和其他工具一样被审批。

自定义审批器

Inspect 带两个内置审批器:human 在终端做交互式批准,auto 自动批准或拒绝指定工具。也可以自己写审批器,实现几乎任何能想到的方案。

自定义审批器是返回 Approval 的函数。Approval 由一个决定和一段解释组成。下面是 auto 审批器的源代码,它只是把初始化时给定的决定原样返回:

@approver(name="auto")
def auto_approver(decision: ApprovalDecision = "approve") -> Approver:
    
    async def approve(
        message: str,
        call: ToolCall,
        view: ToolCallView,
        history: list[ChatMessage],
    ) -> Approval:
        return Approval(decision=decision, explanation="Automatic decision.")

    return approve

有五种可能的审批决定:

决定说明
approve工具调用被批准
modify工具调用在修改后被批准(修改后的调用放在 Approval 的 modified 字段)
reject工具调用被拒绝(向模型报告这次调用被拒绝,并附上解释)
escalate工具调用应升级给链上的下一个审批器。
terminate由于这次工具调用,当前样本应被终止。

modify 决定必须在 modified 里带上修改后的调用。没有的话,这个决定当作拒绝:工具不运行,模型被告知选了修改,但没有提供修改后的调用。审批事件仍然记录 modify 决定。

modify 决定只能改工具调用的参数。如果修改后的调用点了另一个函数,两个工具都不运行,样本以错误失败,因为这是审批器里的缺陷,而不是要报告给模型的事。对修改后的调用,工具事件记录实际运行的参数,模型输出和审批事件保留模型提出的调用。

下面是一个更复杂的自定义审批器,为 bash 命令实现允许列表。设想这个审批器写在名为 evaltools 的 Python 包里。原文函数体在做出决定处用省略号,这里照样保留:

@approver
def bash_allowlist(
    allowed_commands: list[str],
    allow_sudo: bool = False,
    command_specific_rules: dict[str, list[str]] | None = None,
) -> Approver:
    """Create an approver that checks if a bash command is in an allowed list."""

    async def approve(
        message: str,
        call: ToolCall,
        view: ToolCallView,
        history: list[ChatMessage],
    ) -> Approval:

        # Make approval decision
        
        ...

    return approve

假定已经按 Inspect 扩展正确注册了审批器,就可以在审批策略里使用它:

approvers:
  - name: evaltools/bash_allowlist
    tools: "bash"
    allowed_commands: ["ls", "echo", "cat"]

  - name: human
    tools: "*"

这些审批器对它们被配置来处理的每一次工具调用,会做出下面三种决定之一:

  1. 允许这次工具调用(依据各项已配置的选项)
  1. 不允许这次工具调用(因为在所有条件下它都被认为危险)
  1. 把这次工具调用升级给人工审批器。

人工审批器放在最后,并且绑定全部工具,所以来自 bash 和 python 允许列表审批器的升级,最终会提示人工审批器。

在 Python 包里发布审批器的更多细节,见审批器扩展。

工具视图

默认情况下,一次工具调用拿来给人批准时,会打印工具函数和它的参数。对有些调用这够用,但有些工具可以从增强的呈现里受益。例如:

  1. 网页浏览器工具的交互功能(点击、打字、提交表单等)引用一个 element_id,但这个 ID 本身的上下文不够用来批准或拒绝这次调用。作为补偿,网页浏览器工具提供一些额外上下文(正在交互的 element_id 周围的一段页面)。
  1. bash() 和 python() 工具把输入当成字符串。尤其是多行命令时,这很难读懂。作为补偿,这些工具提供另一种视图,把代码格式化成多行、带语法高亮的代码块。

示例

可以这样为 bash 工具实现一个自定义代码块查看器。原文在文档字符串后以省略号结束:

from inspect_ai.tool import (
    Tool, ToolCall, ToolCallContent, ToolCallView, ToolCallViewer, tool
)

# custom viewer for bash code blocks
def bash_viewer() -> ToolCallViewer:
    def viewer(tool_call: ToolCall) -> ToolCallView:
        code = tool_call.arguments.get("cmd", tool_call.function).strip()
        call = ToolCallContent(
            format="markdown",
            content="**bash**\n\n```bash\n" + code + "\n```\n",
        )
        return ToolCallView(call=call)

    return viewer

@tool(viewer=bash_viewer())
def bash(timeout: int | None = None) -> Tool:
    """Bash shell command execution tool.
    ...

ToolCallViewer 收到 ToolCall,返回 ToolCallView。后者提供 context(帮助理解这次调用的额外信息)和 call(这次调用的另一种渲染)中的一个或两个。对 bash 工具,这里给出要执行的 bash 代码的 markdown 代码块渲染。

context 通常用于有状态的工具,它们需要呈现当前状态里的一些上下文。例如,网页浏览工具提供当前已加载页面的一个片段。

出处:UK AI Security Institute,Tool Approval,文档页无单独发布日期(检索于 2026-11-02),https://inspect.aisi.org.uk/approval.html ,MIT License(Copyright (c) 2024 UK AI Security Institute)。

觉得有用,转给同事

微信扫码

用微信扫一扫,在手机上打开后即可转发。

用 RSS 订阅

提交勘误