跳到主要内容

智能体动作空间的可达性分析:为你从未测试过的分支提供评测覆盖

· 阅读需 13 分钟
Tian Pan
Software Engineer

你的团队第一次意识到 Agent 可以调用 revoke_api_key 是在某个早晨,一位好心的用户输入了:“这个 Token 感觉太旧了,能帮我轮换一下吗?” 这个工具是在六个月前作为认证团队 MCP 服务批量导入的一部分注册的。它通过了 Schema 验证,出现在目录枚举中,然后就一直闲置在那里。没有任何评测(Eval)调用过它,也没有任何生产环境追踪(Trace)触及过它。直到某条提示词(Prompt)、某个规划器(Planner)决策,事件频道(Incident Channel)才发现该工具竟然存在。

这就是隐藏在每一个拥有复杂工具目录的 Agent 中的失效模式。四十个注册函数和一个可以组合它们的规划器,产生了一个你从未观察到的计划可达图的长尾。假设“我们测试了常用路径”掩盖了一个事实:危险的分支几乎从定义上来说就是你从未见过的那一个。

好消息是,这个问题并不新鲜。五十年来,编译器一直在处理类似的问题 —— 控制流图中哪些路径是实际可达的,哪些是死代码。通过调整,这门学科可以迁移到 Agent 动作空间。坏消息是,大多数团队还没有完成这种迁移,因此他们的工具目录增长速度超过了评测覆盖范围,“拥有的工具”与“验证过的工具”之间的差距在悄然扩大。

你的工具目录是一个控制流图

从底层对象开始。工具目录不是一个列表。它是一个图,其节点是工具,边是规划器允许的组合,入口点是用户可能发送的提示词。动作空间是从任何入口点到任何终止状态在这个图上的有限路径集合。

动作空间的大小不是目录的大小。拥有 40 个工具且平均计划深度为 4,不同计划的上限达到数百万个。在实践中,大部分上限是不可达的 —— 规划器的策略、提示词分布以及工具输出依赖关系修剪了大部分边 —— 但“大部分”这个词背后隐藏了大量工作。可达子图是你的评测(Eval)需要覆盖的内容,而它总是比你实际评测覆盖的内容大得多。

在编译器设计中,可达性分析回答了这样一个问题:从程序入口开始,哪些基本块(Basic Blocks)可以通过某些执行被访问到?经典的算法是标记-清除(Mark-and-Sweep) —— 最初将每个块标记为不可达,然后从入口开始遍历,标记触及的所有内容。任何未标记的内容都是“死”的,可以删除。重点不在于死代码是错误的,而在于它是未经审计的:编译器无法推断其影响,测试套件无法执行它,任何到达它的执行路径都是分析未预测到的。

将同样的算法应用于你的 Agent,会得到一些有用的东西。将你的生产环境追踪(Production Traces)和评测追踪(Eval Traces)视为动作图上已观察到的路径集。标记每个追踪执行过的(工具、前任、继任)三元组。未标记的子图就是你未经审计的表面。

“不可达”在 Agent 领域究竟意味着什么

编译器类比需要一个重要的修正。在编译后的程序中,不可达代码被证明永远不会执行;分析是可靠的(Sound),因为语言语义是确定性的。但在 Agent 中,规划器是一个随机策略,其输出分布在你每次更改提示词、更换模型、添加工具或更新系统消息时都会发生偏移。一个从未被观察到的分支并不代表它是不可达的,而是“未观察到的”。这种区别很重要,因为在未观察到的分支和生产环境调用之间,唯一的障碍就是一个将规划器推向它的用户提示词。

因此,正确的模型不是“可达 vs. 死亡”,而是“已练习 vs. 未练习”。每个未练习的分支都带有被触达的概率以及被触达后的爆炸半径(Blast Radius)。这两者的乘积就是该分支的风险评分,而评测的优先级问题也就显而易见了:将覆盖预算花在乘积最大的地方,而不是最容易编写分支的地方。

经典软件测试文献多年来一直以“基于风险的覆盖”为名进行此类论证。核心发现 —— 风险加权测试比均匀覆盖在单位测试时间内能发现更多缺陷 —— 对 Agent 而言更加正确,因为动作空间巨大,且为每个分支编写评测的成本极高。你无法测试所有内容,但你可以测试那些一旦失败就会产生严重后果的内容。

从追踪(Traces)中构建动作图

大多数团队并没有明确的动作图。他们拥有注册的工具目录、OpenTelemetry 跨度(Span)流以及有机增长的评测套件。第一项工作是推导出团队隐式运行的图。

构建过程是机械化的。从工具目录中枚举节点。从规划器公开的功能(Affordances)中推导出边 —— 哪些工具可以衔接哪些工具,哪些需要依赖先前输出的参数形式,哪些策略层禁止某些组合。从追踪流中,将每个观察到的计划映射到图中,并递增它穿过的每条边的访问计数。从评测套件中,进行同样的映射,并标记被任何评测覆盖的边。

现在,图有了注解。每条边都有生产频率、评测覆盖标记以及工具团队在注册工具时设置的静态爆炸半径标签。没有追踪的边是未观察到的。有追踪但没有评测的边是“已观察但未受保护”的 —— 仅由工具作者测试过的内容覆盖,对于许多工具来说,这仅仅是能让该季度的集成测试通过的内容。

此时最有用的产物不是指标,而是热力图。布置好图表,将高爆炸半径的边着色为红色,低爆炸半径的边着色为绿色,并对未观察且未受保护的边使用阴影图案。红色加阴影的单元格就是你的评测待办事项,其排序依据比工具注册的先后顺序要科学得多。

风险加权评估优先级

使得这一概念具体化的风险分值计算非常直观:risk = 爆炸半径 × 调用概率。这两个因素可以自然地进行分解。

爆炸半径是工具本身的属性,最好在工具注册时分配,而不是在评估时。一个发送 Webhook 的工具与一个读取配置值的工具具有不同的爆炸半径,而后者又与一个写入支付账本的工具不同。分类可以比较粗略——对于大多数目录来说,只读 (read)、写入 (write)、外部副作用 (external_side_effect)、不可逆 (irreversible) 就足够了——但它必须由了解工具功能的人来设定,而不是从函数签名中推断。工具作者是合适的责任人;策略层是合适的执行点。

调用概率则更难确定。诚实的回答是,对于未观察到的分支,团队并不知情。一个可行的替代方案是与已观察分支的结构相似性:如果已有七个计划通过 (搜索 → 总结 → 邮件) 进行路由,而一个假设的计划将通过 (搜索 → 总结 → 发送至 Slack) 路由,那么该概率就足够高,值得为 Slack 分支编写一个评估。对于没有相近观察结构的分支,默认设置一个非零的底限——例如,通过合成评估来排查任何明显的失败模式——并随着生产环境证据的积累重新排名。

由此产生的优先级规则是:首先为那些爆炸半径为 外部副作用不可逆 且结构相似性概率高于底限的分支编写评估。其他所有内容都放入待办事项中,按相同的乘积排序。这听起来显而易见,直到你将其与大多数团队的实际做法进行对比,即“为 PM 在上次评审中提到的分支编写评估”。

评估集构建:推动规划器走向未曾触达的分支

风险加权优先级告诉你应该覆盖哪些分支。更难的问题是如何构建能够真正执行这些分支的评估。

有两种技术起到了核心作用。第一种是合成对抗提示:手动编写或由模型生成旨在将规划器推向特定目标工具或组合的提示。这是智能体的模糊测试(fuzzing)模拟——目标不是验证正常的流程,而是确认当提示确实转向高爆炸半径分支时,正确的策略门控能够触发,并且正确的工具参数能够通过验证。

第二种是规划器扰动评估。保持提示词不变,并改变工具目录:隐藏一个工具、重命名一个、交换两个、添加一个签名略有不同的近乎重复的工具。规划器是否仍然能正确选择?策略层是否仍能捕获重命名工具的调用?当预期工具缺失时,智能体是否能优雅地降级?这些评估挑战了一个假设,即规划器的行为对目录漂移(catalog drift)具有鲁棒性,而这正是每次认证团队添加新的 MCP 服务器时都会发生的情况。

第三种技术较少受到关注,即反转评估方向。不是问“给定这个提示,智能体是否选择了正确的工具”,而是问“给定这个工具,能够调用它的最廉价提示是什么”。如果最廉价的提示是用户可能会输入的合理内容,那么该分支就值得进行评估;如果最廉价的提示是一个刻意设计的对抗字符串,那么该分支至少是受限的。这样做会让人感到不安,因为它需要承认某些分支可以通过团队从未想象过的提示触达,但这是诚实估计概率因素的唯一方法。

目录累积陷阱

有一种组织模式会让这一切变得更糟。工具目录通过累积而增长。一个团队构建了包含 12 个工具的智能体并发布,下一个季度,认证团队贡献了 3 个,分析团队贡献了 4 个,支持团队贡献了 7 个。集成在技术上是干净的——每个工具都有模式(schema)、描述和快乐路径测试。目录现在有了 30 个工具。但自最初的 12 个工具以来,没有人运行过可达性分析。

累积陷阱以三种可预测的方式表现出来。首先,无人调用的工具在未经过审计的情况下不断累积,因为缺乏调用被误认为是缺乏风险。其次,每当新工具的描述以某种方式编写,使其成为此前路由到其他地方的提示词的竞争匹配时,规划器的行为就会发生变化——这种现象有时被称为描述漂移(description drift),它会导致生产流量的静默重路由。第三,策略层落后了,因为每个工具团队都为自己的工具编写门控,但没有人负责跨团队工具组合的门控。

解决办法与几十年前编译器驱动的项目所趋同的方法一致:分析必须在每次变更时运行,而不是只运行一次。每次工具注册都会触发动作图(action graph)的重新计算以及与先前图的差异对比(diff)。具有高爆炸半径且没有评估覆盖的新路径将阻止注册。构建失败。有人编写评估。目录以团队能够跟上的方式增长,而不是以在集成进度的幌子下隐藏风险的方式增长。

架构结论:动作空间即公共 API

这种观点之所以重要的深层原因是:Agent 的动作空间(Action Space)就是一个公共 API。每一个可达的计划都是与 Prompt 发送者之间的一份契约:在这些条件下,Agent 将以这些参数形式调用这一系列工具。这份契约虽非出自任何人之手,但在系统上线提供服务的那一刻起,它就具有了约束力。“我们从未测试过那个分支”这句话的分量,等同于“我们从未测试过那个 REST 接口”——也就是说,当接口已在生产环境中运行且用户数据被误改时,这种辩解毫无意义。

这种视角的转变具有实际的影响。API 契约有版本管理;动作空间的变更也应该有版本管理。API 契约有弃用策略;当要移除已注册的工具时,应经历一段弃用过渡期——在注销工具之前,先更新 Planner 的策略以停止选择该工具,而不是在注销之后。API 契约有向下兼容性测试;每当工具目录(Catalog)发生变化时,评估套件(Eval Suites)应包含对先前已验证分支的回归覆盖。我们已经熟知的标准 API 工程规范同样适用,而缺乏这些规范正是让“动作空间覆盖率”看起来像个未解难题的原因。

对于大多数运行复杂 Agent 的团队来说,未来一年的工作重点是闭环“目录注册”与“评估覆盖”之间的逻辑。其机制并不神秘:一份保存在版本控制系统中的动作图谱(Action Graph)、每次目录变更时的覆盖率增量检查、驱动评估编写优先级的风险加权待办列表,以及一个对分析未覆盖的分支默认拒绝的策略层。这些都不需要任何研究突破,只需要承认动作空间就是产品的暴露面(Surface Area),并对其给予相应的重视。

你在六个月前注册的某个工具第一次在生产环境中被触发时,不应该是你的故障通报频道第一次知道该工具存在的时刻。只要认真应用可达性分析,这种情况就不会发生——因为要么评估系统会先捕获到该分支,要么策略层会在没有明确授权的情况下拒绝让 Planner 选择它。在此之前,你的工具目录与评估覆盖率之间的差距,就是下一次事故的规模。

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