CodexQA

Industry & PracticeTools & Frameworks

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

CodexQA 团队11 min read

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

In this piece

工具审批

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)。

Found it useful? Pass it on

WeChat

Scan with WeChat to open it on your phone and forward it.

Subscribe via RSS

Submit a correction