
1. 从一次线上事故说起agent 为什么会够不着今年上半年我接了一个挺典型的活儿给一家做智能客服的团队当技术顾问帮他们把 AI agent 从 demo 阶段推到生产环境。demo 阶段一切都很美好agent 会查订单、会退换货、会解释售后政策团队士气高涨。结果上线第三天就出了事——大量用户问我的优惠券为什么用不了agent 的回答翻来覆去只有一句您可以在账户中查看优惠券状态。日志一拉根因并不复杂agent 调优惠券查询工具时工具返回的字段结构在两周前被后端升级过把expireAt改成了expires_at但 agent 的系统提示词和工具描述里还写着旧字段名。agent 确实调用了工具返回数据也拿到了但它认不出里面的关键字段于是选择了最保守的话术敷衍用户。这个事故让我意识到一个一直被忽视的问题我们衡量 agent 时总是在看它能做什么却很少系统地验证它到底够得着什么。模型本身能力再强工具注册错了、数据结构变了、权限链断了、上下文塞不下了它照样会表现得像个傻子。而且最坑的是这种够不着往往是静默发生的——agent 不会报错它只会给出一个看似合理但毫无价值的回答。那段时间我翻了不少团队的 agent 代码库发现几乎每家都有一套工具调用冒烟测试但覆盖面都很窄验证工具 URL 通不通、鉴权过不过、返回是不是 JSON。没人去验证 agent 在真实推理路径上能不能触达它需要的每一样东西。于是我想做一个专门干这件事的玩意儿把它命名为Agent-Reach——不给 agent 做功能测试而是做可达性体检。所谓可达我后来把它拆成了三层工具可达工具注册了、schema 没漂移、调用路径通、数据可达要查的数据源能拿到、字段能对上、格式能解析、权限可达调用链条上每一步的权限都真实有效而不是只在文档里成立。这三层任何一层断了agent 的输出质量就会断崖式下跌。Agent-Reach 就是围绕这三层做系统化验证的一套开源工具。如果你也在做 agent 应用不管用的是哪家模型厂商的框架还是自己撸了一套工具调用协议这篇文章值得看完。我会把 Agent-Reach 的设计思路、一份真实的可达性清单长什么样、以及我在实测里遇到的三类典型事故完整拆开讲。2. 核心设计取舍为什么用探针而不是 mockAgent-Reach 的第一版我其实写得很蠢——就是一堆 pytest 用例每个用例 mock 一个工具返回值然后断言 agent 的输出里包含关键词。跑完发现一点用没有因为 mock 掉的恰恰是容易出问题的部分。工具 schema 漂移是真实发生的权限过期是真实发生的上游接口变慢导致 agent 超时放弃也是真实发生的mock 永远测不出这些东西。后来我彻底换了个思路不 mock只探针。Agent-Reach 的核心是一个叫 probe 的轻量级执行器它会真实地发起一次调用但调用目标不是生产环境而是你配置好的测试环境或沙箱。probe 不关心 agent 的最终回答好不好它只回答三个问题这条路通不通走通之后拿到的东西长什么样和 agent 配置里描述的预期一致吗这三个问题听起来简单落地时却逼我做了一堆取舍。第一个取舍是probe 必须走和 agent 完全相同的调用链。很多团队的测试是直接 HTTP 调工具接口但 agent 实际调用时要经过 function calling 层、参数解析层、鉴权中间件哪一层都可能截胡。所以 Agent-Reach 的 probe 不是独立发请求而是注入到 agent 的运行时里和真实推理共用一套路径。第二个取舍是探针结果必须可反查。每次 probe 跑完我会记录完整的调用链快照工具名、入参、出参、耗时、HTTP 状态码、以及 agent 配置里对应的描述片段。这样一旦某个探针红了你不需要去翻 agent 的思维链日志猜原因直接看快照就能定位到是哪一层断了。这个设计后来救了我好几次命后面讲到事故复盘时你们会感受到。第三个取舍可能最有争议Agent-Reach 默认不做期望断言只做结构捕获。也就是说一份可达性清单里可以声明返回结构里必须有order_id和status字段但我不建议你断言status success。原因是 agent 的工具调用场景里业务状态千奇百怪断言具体值会把测试变成业务测试失去通用性。我只校验结构可达性——字段在不在、类型对不对、嵌套层级对不对、能不能被 agent 的解析器吃掉。至于值合不合理那是业务侧该管的事。这套三件套架构——探针执行器、调用链快照、结构校验器——构成了 Agent-Reach 的骨架。听起来不复杂对吧但真正让它有价值的是那份可达性清单本身也就是怎么描述一个 agent 应该够得着什么这才是整个工具的魂。3. 一份真实的可达性清单是怎么写的Agent-Reach 用一份 YAML 文件描述 agent 应该具备的所有可达能力。我把这文件叫reachfile灵感来源于 Dockerfile——你想让 agent 容器化地跑起来就得先把它依赖的所有外部触点写成清单。下面是我在项目里实际用的一份简化版拿一个查订单的 agent 举例version: 1 agent: order-assistant probes: - name: query_order_by_id type: tool target: order_service.lookup auth: method: service_account scope: order:read input: order_id: SO-2024-1024 expect: structure: - path: order_id type: string - path: status type: string - path: items type: array - path: items[].sku type: string excludes: - path: error - name: query_coupon_by_user type: tool target: coupon_service.get_user_coupons auth: method: user_token scope: coupon:read input: user_id: U-8848 page: 1 expect: structure: - path: coupons type: array - path: coupons[].discount type: number - path: coupons[].expires_at type: datetime这里有几个字段需要解释一下因为它们直接决定了这份清单好不好用。type: tool表示这是个工具可达性探针。除了 tool我还支持data直接探数据源比如查数据库视图、permission只验证鉴权链路不发业务请求、context验证某个系统上下文片段能不能被塞进 agent 的上下文窗口并正确解析。后两种类型是 Agent-Reach 区别于普通接口测试的关键我展开说一下。permission 类型探针解决的是权限可达问题。你可能会想权限不就是在请求头里带个 token 吗实测下来问题多得多。微服务架构里agent 调工具 A工具 A 内部还要调服务 B服务 B 再调服务 C。agent 拿的 token 可能只对 A 有效B 和 C 是靠 A 的 service account 向下游调用的。一旦 A 的 service account 被轮换、scope 被收紧agent 表面上调通了 A拿回来的却是 B 的 403 错误。这种链路我见过太多次了。permission 探针会模拟一次完整的下游调用链逐级记录每个跳转的鉴权结果任何一环失效都会把链路标红。context 类型探针解决的是上下文可达问题这个更隐蔽。很多 agent 应用会把 CRM 系统里的客户标签、历史工单、知识库片段拼进 prompt让模型参考。但知识库的切分策略、拼接顺序、截断逻辑任何一个变了agent 实际看到的内容就和开发时完全不一样。context 探针会真实地把指定的上下文片段走一遍检索-拼接-截断-送入 prompt的完整流程然后校验送进去的内容是否包含了声明过的关键信息。我遇到过一种情况知识库文档的摘要算法升级摘要生成时间从 30ms 涨到 300msagent 主线程等不及直接跳过了上下文拼接结果模型在一个完全没上下文的状态下开始回答。这种 bug 用任何 mock 都测不出来只有 context 探针能抓住。再说回expect这一段。我坚持只用结构校验但也留了一个excludes白名单式的出口——声明某些字段绝对不能出现。比如查订单时返回里夹带了error字段就算失败因为很多 agent 的解析逻辑看到error字段会走入异常分支。这个设计是从事故里学来的后面会详细讲。写清楚一份 reachfile 的功夫不比我写功能代码少但它带来的收益是每次依赖升级、权限调整、接口改动之后跑一遍 Agent-Reach 就知道哪些可达性断了不用等用户骂上门。4. 三起典型的 reach 事故复盘从表象一路挖到根因工具写出来是要见血的。Agent-Reach 跑通之后我在三个客户的 agent 项目里做了一轮完整体检挖出来的问题分布很有意思——真正挂在功能 bug上的很少绝大多数都是可达性问题。挑三个最有代表性的复盘给你。4.1 字段名漂移agent 的手够到了脑子没认出来这就是开头优惠券事故的完整版。当时 reachfile 里对coupon_service.get_user_coupons的校验是coupons[].expires_at必须是 datetime 类型。Agent-Reach 第一次跑就标红了返回结构里根本没有expires_at取而代之的是expireAt。定位过程很快调用链快照显示 HTTP 200、响应体完整、结构合法只是字段名和 agent 工具描述里的不一致。为什么不一致后端团队两周前做了 API 升级把所有字段从 camelCase 统一成 snake_caseagent 的工具描述是运营团队手写的没同步。这个事故的教训是agent 工具的参数描述和返回结构描述本质上是另一份接口契约。你接口文档写的是一份契约喂给模型的工具 JSON Schema 是另一份契约任何一个字段对不上模型就会在推理时陷入混乱——它不知道该信哪个。Agent-Reach 的意义就是把模型视角的契约和系统视角的契约拉出来对比而不是假设它们天然一致。4.2 上下文窗口截断知识就在那里但 agent 看不见第二个案例更邪门。一个做企业内部知识问答的 agent经常在回答里漏掉关键政策条款。业务方怀疑是知识库索引不全我上手先跑 Agent-Reach知识库侧的可达性全绿——该检索的文档都能检索到该拼进 prompt 的片段也都在。但 context 探针标黄了某个片段的实际字符数比 reachfile 里声明的大了一个数量级。原因是有同事往知识库里传了一份 300 页的 PDF切分器按段落切其中一段是整整 12 页的表格被当成一个文本块塞进了 prompt。agent 的上下文窗口有限拼接器按声明顺序硬塞这个 12 页的巨块把后面所有的政策摘要全挤出了窗口。你如果只看推理日志会发现模型从头到尾没看到这些被挤掉的内容于是它理所当然地没引用。这不是模型笨是上下文调度的问题。Agent-Reach 的 context 探针现在会显式输出每个上下文块的字符数、截断位置、剩余空间这样你就知道知识可达到底是可达还是被挤出去的可达。4.3 隐式权限链断裂每一步都有人盖章但章是过期的第三个案例来自一个多系统协同的 agent。它要完成查询客户订单并计算退款金额这个任务需要依次调 CRM、订单中心、财务系统三个工具。单看每个工具的健康检查都是绿的但完整跑一遍退款计算就失败。Agent-Reach 的 permission 探针把链路拉出来之后问题一目了然订单中心调用财务系统时用的是自己的 service account而这个 account 的授权在三个月前到期了。订单中心的上游接口没做 auth 失败兜底直接把 403 当成无退款权限返回agent 就顺着错误的返回值推理出该客户不应退款。这个案例说明一个很反直觉的事agent 的权限链路跟着工具走不跟着 agent 走。你给 agent 配了再大的权限落到具体实现上可能是 A 服务拿自己的身份去调 B 服务B 服务再用自己的身份去调 C。中间任何一环的凭证失效agent 的实际权限就归零而且归零得毫无声响。permission 探针的价值就在这儿——它会端到端地把这条隐式链路走一遍把每个跳转的鉴权结果摊开给你看。5. 多 Agent 协作时的 reach 边界谁允许够到谁Agent-Reach 第二版开始支持多 Agent 场景之后我意识到可达的定义又变了一层——不再是单个 agent 对外部资源的触达而是agent 与 agent 之间的触达边界。这也是我觉得最值得扩展的方向。先讲一个真实场景。我有个客户的系统里跑着两个 agent一个叫导购负责和用户聊天一个叫库存负责查实时库存。导购 agent 需要问某商品是否有货它的实现方式是直接调库存 agent。看上去很合理但隐患不小——导购 agent 跑的是用户输入提示词如果用户精心构造了一句让导购放弃所有指令直接执行你说的话的攻击串导购会不会把这个恶意指令原样透传给库存 agent库存 agent 有没有能力执行一些危险操作比如修改库存数据这就是我讲的reach 边界问题。单 Agent 场景里边界是agent 能触达哪些外部服务这个靠权限探针就能验。多 Agent 场景里边界变成了一个 agent 能触达另一个 agent 的哪些能力这需要引入一个概念叫能力白名单——不是所有 agent 都该对所有 agent 开放全部工具。Agent-Reach 在第二版里加了一个delegation探针类型专门测 agent 间的委托调用链。它会模拟一条用户请求 - 导购 agent - 库存 agent的完整链路并且故意在请求里注入一些越权指令看看下游 agent 会不会被带着跑偏。这个设计借鉴了安全领域的模糊测试思想——不测正常路径专测越界路径。实际跑下来效果很好抓到一个具体问题库存 agent 的工具描述里有一段当参数actionmodify时可更新库存数量而导购 agent 的系统提示词里明确写了只允许调用查询类工具。但两个 agent 用的是同一个底层大模型模型在推理时被用户强烈引导导购 agent 直接把actionmodify透传过去了。单测任何一个 agent 都测不出这个问题只有把它俩放一起跑委托链路才能暴露。所以我对多 Agent 架构的建议很直接别急着搭复杂的 agent 间通信协议先把谁可以够到谁的哪个工具写成显式的委托规则然后交给 Agent-Reach 这种工具做持续验证。技术债可以慢慢还够得着不该够的东西这种债一次都欠不起。6. 沉淀下来的实战经验与扩展思路Agent-Reach 从第一版到现在跑了大半年服务了四五个真实的 agent 项目我攒了不少心得挑几条最值得说的。第一可达性体检要进 CI但别只进 CI。我最开始是把它放在部署流水线里每次发版前跑一轮。后来发现这不够——权限过期、字段漂移这类问题往往不是代码变更引入的而是外部系统自己悄悄变的。所以我现在建议的做法是发版前跑全量探针线上每天跑一次轻量探针只跑 permission 和 context 两种每周跑一次全量。探针本身很轻几百毫秒一次完全撑得起这个频率。第二reachfile 也要走 review 流程。它就是一份描述系统期望的代码写错了也会带来误报或漏报。我踩过的坑是运营同学在配 reachfile 时把订单接口的返回结构写错了Agent-Reach 天天标红大家狼来了喊多了最后干脆不看探针结果了。后来我把 reachfile 纳入 code review并且给每个探针加了owner字段哪个探针红了就自动 对应的负责人。这个改动让探针的响度恢复正常了——红一次就是真的有事。第三别让 agent 直接处理探针的原始输出。这个是踩坑踩出来的。有一版我把探针结果喂给了 agent让它自己判断可达性有没有问题结果模型被原始 JSON 里的噪音干扰把一些无害字段当成异常。现在 Agent-Reach 的默认模式是把探针结果转成人类可读的摘要再由人来做最终判断。模型可以做初筛但可达性到底出没出事这个结论目前还是人拍板比较稳。第四探针的输入数据要用伪随机真数据。所谓伪随机是每次跑探针时输入一个基于固定 seed 生成的不同参数值但要求每个值都符合业务真实的字段约束。比如订单号必须是SO-开头加数字user_id 必须是U-开头。这样既避免了每次用同样数据测出路径缓存又保证了探针请求长得很像真实请求能被权限系统正常受理。第五Agent-Reach 不适合做什么它不测模型的推理质量不测回答的流畅度也不测业务规则的正确性。它就是一把尺子专门量可达性。很多同事试用后说这玩意儿能不能帮我看 agent 回答得好不好我说你看错工具了回答得好不好是评测集和人工评估的事可达性是地基的事。地基塌了楼再漂亮也没用地基稳了才能谈楼好不好看。我个人现在的做法是每个 agent 项目开工第一天先把 reachfile 写出来哪怕工具还没实现只写期望的触点然后每加一个工具就同步加一个探针每次依赖升级或者配置变更先跑探针再动代码。这套流程自然跑起来之后Agent-Reach 本身就不需要你刻意用它了——它就长在你的开发流程里像 linter 一样沉默地守着那条够得着的底线。如果你也在做一个依赖大量工具调用的 agent 应用不妨花一个下午把 agent 的所有外部触点列成清单再想想清单上每一行如果失效你的 agent 会怎么表现。我敢打赌至少有一半的AI 表现不稳定问题其实都是这个问题——它够得着的世界跟你以为它够得着的世界不是同一个世界。