跳到主要内容

工具的默认参数其实是伪装的策略决策

· 阅读需 11 分钟
Tian Pan
Software Engineer

打开任何智能体运行的 trace(追踪),观察一次工具调用。你会看到工具名称以及模型选择传递的参数。你没看到的是它 没有 传递的所有内容。一个仅设置了 querysearch 调用,在运行时仍然具有页面大小、超时时间、结果排序和可见性范围。这些都不是智能体决定的。而是你在几个月前编写工具 schema 时决定的,当时你将这些参数设为可选并留下了默认值。

默认值并非为了方便。它是伪装成合理留白的策略决策。默认的页面大小限制了智能体在一次调用中能看到的范围。默认的超时时间决定了智能体何时放弃并开始即兴发挥。默认的可见性范围决定了“搜索文档”是指公共手册,还是包含未发布路线图在内的整个内部维基。默认的 dry_run 标志决定了智能体的行为是一场演习,还是生产环境中真实的、不可逆转的事件。

这些选择都不会出现在你审查智能体行为的地方。你会审计系统提示词。你会审计工具描述。你会审计模型的推理追踪。但有效参数 —— 即智能体传递的内容与你的默认值合并后的结果 —— 是一个没有所有者的层级,没有任何审查流程会触及它。提示词会有设计文档和三轮反馈。而默认的 limit=50 只要输入一次就再也不会被提及。它们对智能体实际行为的影响力是同等的。

默认值是最可能的值

这是令人不安的部分。对于任何可选参数,默认值并不是最少使用的值。它是 最常 使用的值,因为模型会不断地使用它。

模型调用工具时的行为就像开发者浏览 API:它会填入那些对当前任务明显起支撑作用的参数,而不管剩下的。如果 query 是必填的而 scope 是可选的,智能体每次都会传递 query,而几乎从不传递 scope。因此,你对 scope 的默认设置并不是边缘情况。它就是生产环境的行为。它支配着绝大多数的调用,而且是悄无声息地进行。

这颠覆了我们通常对面向人类 API 默认值的看法。当人类开发者集成 API 时,他们会阅读文档,注意到可选参数,并就哪些参数重要做出慎重的决定。默认值捕获的是他们没有考虑到的情况。而对于智能体,默认值捕获的是智能体没有考虑到的情况 —— 对于任何并非任务核心的参数,几乎都是如此。

因此,对于每个可选参数,问题不在于“什么是合理的后备方案”。而在于“我希望在绝大多数调用中表现出什么行为,因为这就是这个值将产生的结果。”如果你不放心将该值作为明确的策略写在系统提示词中 —— 例如“智能体仅搜索公共文档”、“智能体最多检索 50 行”、“除非明确确认,否则所有破坏性操作均为演习” —— 那么你也不应该放心将其作为隐形的默认值。它们是同一种策略。一个版本是可审查的,而另一个不是。

三个悄悄决定一切的默认值

考虑一下那些看起来最无害、实则引导整个系统的参数。

页面大小和结果限制决定了智能体能知道什么。 一个在 list_tickets 工具上进行推理的智能体,如果默认 limit=20,它并不是在对你的工单队列进行推理。它是在对队列的前 20 行进行推理,且遵循你同样没考虑过的默认排序方式。问它“是否有任何紧急工单”,它会根据一个可能并不包含紧急工单的窗口给出一个自信的回答。智能体并没有错;它只是根据你的默认值交给它的切片进行工作。AWS CLI 也记录了相反的失败情况 —— 设置得 过高 的默认页面大小会导致大型列表调用彻底超时 —— 因此没有可以不去考虑的安全值,只有与数据匹配的值。

超时时间决定了智能体何时放弃现实转而即兴发挥。 一个达到默认超时的工具调用并不会以模型视为终止的方式向模型返回清晰的失败。它返回一个智能体通常会绕过的错误 —— 重试、尝试不同的工具,或者最糟糕的是,假装自己拥有数据继续执行。过短的超时时间会将“缓慢但正确”转化为“快速但捏造”。默认超时时间是“智能体等待真相”与“智能体胡编乱造”之间的界线。这不是一个调节旋钮。这是一个行为契约。

范围和可见性默认值决定了“执行任务”被允许触及的内容。 这是最关键的一点。一个默认范围为“所有已索引内容”的 search_documents 工具,意味着每次检索调用都会触及服务账户可见的所有内容。一个带有默认频道或默认接收者列表的 send_message 工具,意味着一个措辞模糊的请求会落到某个具体的地方。一个默认使用读写连接而不是只读副本的 query_database 工具,意味着一个格式错误的调用可能会改变状态。关于最小权限智能体框架的安全研究对这一原则非常明确:一个工具调用除非被策略及其参数明确允许,否则应被拒绝。而“带有宽泛默认值的可选参数”模式恰恰相反 —— 除非智能体恰好缩小了范围(而智能体很少这样做),否则允许一切。

默认安全对 Agent 而言比对人类更重要

每个 API 设计者都了解“默认安全”原则:最小权限、故障关闭、窄作用域。这一直以来都是良好的实践。但对于 Agent 工具来说,这不再仅仅是良好的实践,而变得具有结构性意义,原因有二。

第一点如上所述:Agent 使用默认值的频率远高于人类集成者,因此不安全的默认设置并非偶然暴露,而是常态。

第二点是 Agent 的调用者并不像人类集成者那样值得信赖。连接工具的人类开发人员阅读过文档、拥有明确意图且负有责任。而从安全角度来看,组装工具调用的模型正在处理不可信的输入——包括对话、检索到的文档、先前的工具输出,其中任何一项都可能包含提示词注入(prompt injection)。现在的标准指导非常明确:将 LLM 提供的工具参数视为不可信,根据白名单进行验证,并且绝不让模型填充敏感参数。模型省略的参数甚至比它提供的参数更不可信,因为它们甚至没有被考虑到——它们纯粹是从你的 Schema 中继承而来的。

这就是为什么最稳健的模式是完全分离两类参数:模型允许选择的参数,以及模型永远看不见的参数。用户身份、权限范围、读写连接、真实运行 vs 模拟运行(dry-run)标志——这些都应该在模型生成调用后、函数运行前,由你的应用程序代码在执行时注入。模型提议,确定性代码裁决。决定操作是否可逆的参数绝不应该是一个模型可以通过猜测来覆盖的默认值。它根本不应该出现在模型的 Schema 中。

对于那些真正必须由模型控制的参数,修复方法比 Agent 本身更古老且更简单:将关键参数设为必填(required)。带有默认值的可选参数是将决策推迟给了一个数值。而必填参数则是强制将决策公开化。如果页面大小会实质性地改变 Agent 的结论,请不要设默认值——将其设为必填,并让调用层或提示词根据每个上下文有目的地进行设置。那种“模型现在每次都必须传递 scope”的轻微烦恼正是意义所在。你已将隐形的继承转化为可见、可评审、可追踪的决策。

记录生效参数,而非传递参数

即使有了默认安全和关键处的必填项,你仍然存在一个可观测性缺口,而这是最容易填补的一个。

大多数 Agent 追踪(tracing)记录的是模型发出的工具调用。这种捕获恰恰遗漏了整个问题的核心:默认值。如果模型传递了 {query: "outage"},而你的工具将其解析为 {query: "outage", limit: 20, scope: "all", timeout_ms: 3000, dry_run: false},那么仅显示第一个对象的追踪记录就在通过遗漏对你撒谎。当你稍后调试为什么 Agent 漏掉了紧急工单时,追踪记录会告诉你它搜索了 "outage",却对真正导致遗漏的“窗口大小为 20”这一信息只字未提。

修复方法只需一行代码的纪律:将“生效参数”(effective arguments)——即函数实际执行时使用的合并后对象——作为一个独立于“传递参数”的字段进行记录。针对 Agent 的审计日志指南已经要求记录传递给每个外部工具的参数;将其扩展到记录解析后的参数。这样,每个默认值在事后都变得可见,每个静默策略都变成了可查询的字段,“Agent 超时并即兴发挥”不再是一个猜测,而是一个你可以过滤的行。

这还为你提供了最终会被要求的审计追踪。当安全或合规审查员询问“这个 Agent 是否可能读取了 X”时,真实的答案存在于其工具调用的有效范围中,而不是存在于提示词或模型的推理中。如果你只记录了模型传递的内容,你无法回答。如果你记录了工具运行时的内容,你就可以回答。

将每个默认值视为经过签署认可的内容

心态上的转变很小,且无需任何成本。停止将默认值视为为了方便而填充的空白。开始将其视为由某人——也就是你——签署认可的一行策略,它将支配 Agent 在该工具上的大部分行为。

在实践中,这意味着三个习惯。首先,当你向 Agent 工具添加可选参数时,用一句话写下其默认值在常见情况下会产生什么行为,并决定你是否愿意在设计评审中为这句话辩护。其次,对于任何涉及安全、可逆性或影响范围(blast radius)的参数——将其设为必填,或者将其从模型的 Schema 中移除,并从可信代码中注入。第三,记录生效参数,以便你保留的默认值至少在事后是可见的。

Agent 使得工具设计变成了一门比以往风险更高的学科,而风险最高的部分往往看起来风险最低。提示词是你写下希望 Agent 做什么的地方。而默认值则是你写下当它没有思考时(这是大部分时间)实际会做什么的地方。给予它们同等的审视,因为它们具有同等的重量。留白的默认值并非推迟的决策。它是一个静默做出并发布的决策。

References:Let's stay in touch and Follow me for more thoughts and updates