ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent-Reach:Agent 工具调用的生产级连接治理实践

Agent-Reach:Agent 工具调用的生产级连接治理实践 1. Agent-Reach 要解决的不是能调用而是够得着1.1 从一个被卡住的工单说起Agent-Reach 这个词第一次出现在我的需求文档里是在一次事故复盘之后。我们的客服 Agent 在演示环境里表现得非常漂亮能查订单、能查物流、能发短信通知评审的时候一片叫好。结果上线第三天一个用户问我上周买的那单现在到哪了Agent 转了三圈最后回了一句抱歉我暂时无法查询。排查过程很枯燥订单查询工具挂在一台没人维护的老服务上走了内网域名物流查询走的是另一家供应商的 HTTP 接口密钥写在配置文件里、三个月前轮换过一次但没同步短信通道干脆是运营同学用网页后台手动点的。三个工具、三种接法、三套凭据、三种错误码Agent 面对的是一个拼凑出来的世界它当然会迷路。这就是 Agent-Reach 要处理的核心问题Agent 的够得着能力。Reach 在这里不是抽象的智能而是非常工程化的三件事——它能不能发现有哪些工具可用、能不能安全稳定地把请求送达、送达之后能不能知道发生了什么。一个 Agent 再聪明够不着外部系统它就只是一个会聊天的文本模型。我把 Agent-Reach 定义成一套位于模型层和业务系统之间的连接治理层。它不负责推理不负责话术只负责把模型想做的事翻译成系统能执行的动作并且在中间加上该有的约束。适合参考这套东西的人有三类正在把 Demo 级 Agent 推向生产的开发者、需要统一管理十几个异构接口的团队、以及想给自己搭一套多工具调度框架的个人玩家。1.2 我给 Agent-Reach 划的四条能力边界设计任何中间层最先要做的不是画架构图而是划边界。边界不清最后一定会变成一个什么都往里塞的垃圾桶。我给 Agent-Reach 划了四条发现Discovery所有可被 Agent 调用的能力必须在一个地方能列出来。不是靠人记忆不是靠翻代码而是有一套注册机制。新增一个工具注册完就能被检索到不需要改调度代码。连接Connection从调用意图到真实请求中间的协议转换、参数校验、序列化、网络传输全部由适配层统一处理。模型不需要知道对面是 REST、gRPC 还是命令行。约束Constraint哪些 Agent 能用哪些工具、每个工具每分钟最多调多少次、单次调用最多等多久、失败最多重试几次。这些是策略不是硬编码必须可配置、可热更新。留痕Trace每一次 reach 都要有一条完整记录谁发起的、什么参数、耗时多久、成功还是失败、失败原因是什么。没有留痕的中间层出问题的时候你会比没有它还痛苦。这四条边界之外的事情我一律不放进 Agent-Reach。比如对话历史管理、Prompt 编排、模型选型、结果的自然语言润色这些都属于上层应用的事。中间层最大的诱惑就是顺手多干一点而中间层最大的坑也是这个。1.3 一个容易混淆的点Reach 不等于 Tool Calling很多人会把 Agent-Reach 和模型原生的工具调用能力划等号这是个很危险的误解。模型原生支持的工具调用解决的只是模型输出一段结构化的调用意图这件事。它把 JSON 吐出来了然后呢谁去执行、用什么凭据执行、执行超时了怎么办、返回一个 500 错误怎么给模型解释、同一个工具被两个用户同时调用怎么隔离——这些它一概不管。我见过最典型的翻车场景是开发同学直接把工具调用结果原封不动塞回上下文遇到一段 2000 字的技术栈报错模型被这段噪音带偏开始一本正经地跟用户解释 Java 异常。Agent-Reach 的适配层职责之一就是把错误也做归一化——把HTTP 502 Bad Gateway翻译成订单系统当前繁忙建议稍后重试这种模型能理解、用户能接受的结构化描述。所以准确的定位是模型原生工具调用是发动机Agent-Reach 是传动系统加底盘。发动机再强没有传动轮子也不会转。2. 整体架构设计与选型取舍2.1 三层结构注册层、调度层、适配层Agent-Reach 的骨架我拆成了三层每层只干一件事。注册层负责有哪些工具核心是一个工具契约表。每个工具用一份声明式配置描述自己名字、用途描述、参数 schema、返回结构、超时、重试策略、所需权限标签。这份配置是唯一事实来源文档、代码、模型看到的工具描述全部从它生成。调度层负责这个请求该给谁、按什么规矩给。它接收来自模型的调用意图做三件事查注册表确认工具存在、做权限与配额校验、把请求交给对应的适配器并管理生命周期。超时、重试、熔断、并发控制全在这一层。适配层负责怎么把它送出去。每个协议一个适配器HTTP 适配器、命令行适配器、数据库适配器、消息队列适配器。适配器的输入永远是一致的内部请求对象输出永远是一致的内部响应对象。这一层的价值在于业务代码永远不需要知道对面的技术细节。三层之间的依赖是单向的调度层依赖注册层和适配层适配层只依赖工具契约注册层谁也不依赖。单向依赖带来的好处是我可以单独替换掉某一层而不影响其他层——比如把注册表从本地 YAML 换成配置中心适配层代码一行都不用动。2.2 为什么把工具契约放在第一位动手写代码之前我花了整整两天只做一件事定义工具契约的字段。这个投入后来被证明是最值钱的。契约里最容易被忽略、但最要命的是用途描述。模型的工具选择质量几乎完全取决于这段描述写得好不好。查询订单和根据订单号查询订单的当前状态、物流节点和预计送达时间输入必须是 16 位纯数字订单号这两句描述导致的选择准确率能差出一大截。我后来定了个规矩工具描述必须包含做什么、输入什么格式、输出什么内容、什么情况下不要用四条缺一不可。第二个关键是参数 schema 的严格度。一开始我为了省事把参数都定义成字符串结果模型传了个 下单时间: 昨天工具直接崩。后来改成严格的类型 格式校验 枚举约束并且在描述里给出示例值模型的参数错误率从接近两成降到了百分之一以内。这个数字不夸张因为模型在有明确示例的时候表现比让它自由发挥稳得多。第三个关键是权限标签。我给每个工具打上标签比如read:order、write:notify、pii:contact。调度层根据发起方哪个 Agent、哪个用户的标签集合做交集判断。写操作和涉及个人信息的读操作默认全部拒绝必须显式开白名单。这一条在实际运行中拦下过好几次误操作非常值得。2.3 自研调度与现成编排框架的取舍对比为什么不直接用现成的编排框架是我被问得最多的问题。我的答案不是自研更好而是看你的约束条件。下面这张表是我当时做决策时的实际对比维度供你参考。维度自研轻量调度层通用编排框架接入一个新协议写一个适配器约 100 行通常需要等社区插件或自己写扩展冷启动耗时极低进程内直接调用视框架而定部分场景需要额外运行时权限模型完全按业务定制标签粒度随意多为通用 RBAC定制需要二次开发可观测性需要自己接链路追踪多数内置开箱可用团队学习成本代码量小读一遍就懂需要理解框架的抽象概念长期维护自己负责有隐性人力成本社区承担一部分但升级有破坏性变更风险我的选择是混合方案核心调度自研链路追踪和指标上报用成熟组件。理由是调度逻辑和业务权限耦合太深交给通用框架反而要写大量胶水而可观测性是个纯标准化问题没有必要重复造。这里有个经验值得说自研中间层最怕的不是写不出来而是写着写着变成一个只有原作者能维护的东西。我给自己定的线是——核心代码控制在两千行以内任何一个有经验的开发者半天能读完。超过这条线就该考虑拆出去用现成方案了。3. 核心模块的实现细节3.1 注册中心声明式 schema 驱动一切注册中心的设计目标只有一个新增工具不改代码。为了做到这一点工具定义全部走声明式配置。我用的是 YAML因为运营和产品同学也看得懂评审的时候能直接提意见。一份工具契约大概长这样name: order.query_status version: 1 description: 根据订单号查询订单当前状态、已完成的物流节点和预计送达时间。 输入必须是 16 位纯数字订单号。若用户只提供了模糊描述如上周那单 应先调用 order.search 获取订单号不要直接调用本工具。 adapter: http endpoint: ${ORDER_SERVICE_BASE}/api/v1/orders/{order_id}/status method: GET params: order_id: type: string pattern: ^[0-9]{16}$ required: true example: 2024081512345678 timeout_ms: 3000 retry: max_attempts: 2 backoff_ms: 200 retry_on: [timeout, connect_error, http_5xx] permissions: - read:order tags: [order, logistics, readonly]这份配置被三处消费注册中心启动时加载并建立索引模型侧的工具体现在生成时由它渲染文档站点由它自动生成。三处同源永远不会出现文档写的和实际行为不一致这种老问题。索引我建了两个维度按名字的精确索引和按描述文本的向量索引。前者用于直接调用后者用于模型只说了意图、没指定工具的场景。向量索引这块我踩过一个坑描述文本向量化之后语义相近的工具会互相抢查询订单和查询退款经常被混淆。解决办法是在描述里显式加入不要用的边界说明并且在召回后加一层规则过滤比如用户话里出现退字就优先排除纯订单工具。3.2 执行器超时、重试、幂等与熔断一个都不能少执行器是调度层的心脏也是最容易写出隐患的地方。我把它拆成了四个关注点。超时必须是分层的。我设置了三层超时单次网络请求超时、整个工具调用超时、Agent 单轮对话总预算超时。三层的关系是包含关系最外层最大。为什么要分三层因为只设最外层一个卡住的请求会把整个对话拖死只设最内层重试三次之后总耗时还是会超。重试的前提是幂等。我的规则很硬只有声明了idempotent: true的工具才允许自动重试写操作默认不重试。这不是保守是因为我真实遇到过重复发送通知短信的事故——第一次请求其实成功了只是响应超时重试又发了一遍。用户收到了两条短信第一条说您的订单已发货第二条也是。退避我用的是指数退避加随机抖动基础 200 毫秒最多两次重试。加抖动的目的是避免多个请求在同一时刻同时重试形成新的峰值。这个细节在你只有几十个并发的时候看不出差别上了几百并发就会很明显。熔断是我加得最晚但收益最大的一块。规则很简单某个工具在 30 秒窗口内失败率超过一半且调用次数超过 10 次就进入熔断状态后续请求直接快速失败不再打到底层服务。这个设计的价值在于保护下游——当订单系统已经扛不住的时候继续往里压请求只会让情况更糟。熔断之后每隔 10 秒放一个探测请求成功就恢复。3.3 凭据管理绝不让密钥出现在任何文本里这是我在整个项目里最坚持的一条红线凭据永远不进入模型上下文、不进日志、不进异常堆栈。具体做法是三层隔离。第一层凭据存在独立的密钥管理服务里Agent-Reach 启动时按服务身份拉取内存中只保留引用句柄。第二层适配层在执行请求的最后一刻才用句柄换取真实值换完立刻用完就丢不留变量。第三层所有出站日志和异常信息在序列化之前过一遍脱敏过滤器匹配密钥格式的字符串一律替换成占位符。为什么这么较真因为模型的上下文是一个你无法完全控制边界的区域。工具描述、参数、返回值、错误信息任何一处带上密钥都有可能被模型学到并在后续对话里复述出来。我做过一次内部演练故意在一个工具的返回值里放了一个假密钥结果在第三轮对话里模型真的把它当成订单号念了出来。从那以后脱敏过滤器成了强制环节。提示脱敏要做在数据离开进程之前不要依赖日志采集侧过滤。日志采集侧配置一旦被人改动泄露就是静默的你甚至不会知道。另外一条经验是凭据轮换。我设计了一个平滑轮换机制新凭据先写入同时新旧两套都可用观察 24 小时确认没有旧凭据的调用再删除旧的。这样轮换过程对 Agent 完全透明不需要停服也不需要重启。3.4 可观测性让每一次 reach 都留痕一次 reach 的完整链路我记录了这些东西调用 ID、发起方标识、工具名与版本、入参摘要脱敏后、适配器类型、实际请求耗时、重试次数、最终状态、错误分类、返回体大小。关键设计是调用 ID 贯穿全链路。这个 ID 在调度层生成一路带到适配层、带到下游服务的请求头、带到日志和链路追踪系统。出事的时候我只要拿到用户反馈的一个时间点和一句话就能在链路系统里把整条路径拉出来。错误分类这件事我单独花了时间做。一开始我把错误都归成调用失败结果排查的时候完全看不出规律。后来分成了六类参数校验失败、权限拒绝、连接失败、超时、下游返回错误、解析失败。分类之后我一眼就能看出某个工具最近是不是参数问题变多了、是不是下游在变慢。错误分类不是给机器看的是给半夜被叫起来的人看的。4. 从零跑通Agent-Reach 最小可用版本实操4.1 目录结构与依赖最小可用版本我刻意控制得很轻不引入任何重量级框架。下面的结构可以直接抄agent-reach/ ├── registry/ │ ├── loader.py # 加载 YAML 契约 │ └── index.py # 名字索引 描述索引 ├── core/ │ ├── request.py # 内部统一请求对象 │ ├── response.py # 内部统一响应对象 │ ├── policy.py # 权限、配额、熔断策略 │ └── executor.py # 调度与生命周期管理 ├── adapters/ │ ├── base.py # 适配器抽象 │ ├── http_adapter.py │ └── cli_adapter.py ├── observe/ │ ├── tracer.py │ └── redactor.py # 脱敏过滤器 ├── tools/ # 工具契约 YAML │ └── order.query_status.yaml └── main.py依赖只有四个pyyaml做契约解析、httpx做异步 HTTP、pydantic做参数校验、structlog做结构化日志。这四样都是成熟、稳定、社区活跃的库我不建议在这个层面追求新奇。4.2 定义工具契约并实现一个真实工具先写内部统一对象。这个对象的字段设计决定了后面所有适配器好不好写。# core/request.py from dataclasses import dataclass, field from typing import Any, Dict, Optional dataclass class ReachRequest: trace_id: str tool_name: str tool_version: int caller: str # 哪个 Agent / 哪个用户 params: Dict[str, Any] granted_permissions: set field(default_factoryset) deadline_ms: Optional[int] None credential_handle: Optional[str] None# core/response.py from dataclasses import dataclass from typing import Any, Optional dataclass class ReachResponse: trace_id: str ok: bool data: Optional[Any] None error_type: Optional[str] None # param / permission / connect / timeout / downstream / parse error_hint: Optional[str] None # 给模型看的、人话描述 elapsed_ms: int 0 attempts: int 1注意error_hint这个字段。它不是给开发者看的是给模型看的。我要求所有错误提示都写成一句模型能理解、能据此调整的话比如订单号格式不正确应为 16 位数字请向用户确认。这一条小小的设计让 Agent 在出错后的二次尝试成功率提升非常明显。然后是适配器基类和一个 HTTP 适配器# adapters/base.py from abc import ABC, abstractmethod class BaseAdapter(ABC): abstractmethod async def execute(self, request, contract) - ReachResponse: ...# adapters/http_adapter.py import time, httpx from adapters.base import BaseAdapter from core.response import ReachResponse class HttpAdapter(BaseAdapter): def __init__(self, credential_store, redactor): self.client httpx.AsyncClient() self.credential_store credential_store self.redactor redactor async def execute(self, request, contract) - ReachResponse: started time.monotonic() url contract.render_url(request.params) token self.credential_store.get(request.credential_handle) headers {Authorization: fBearer {token}, X-Trace-Id: request.trace_id} try: resp await self.client.get( url, headersheaders, timeoutcontract.timeout_ms / 1000, paramscontract.remaining_params(request.params), ) except httpx.TimeoutException: return ReachResponse(request.trace_id, False, error_typetimeout, error_hint订单系统响应较慢请稍后重试, elapsed_msint((time.monotonic()-started)*1000)) except httpx.ConnectError: return ReachResponse(request.trace_id, False, error_typeconnect, error_hint暂时无法连接订单系统, elapsed_msint((time.monotonic()-started)*1000)) if resp.status_code 500: return ReachResponse(request.trace_id, False, error_typedownstream, error_hint订单系统当前繁忙, elapsed_msint((time.monotonic()-started)*1000)) if resp.status_code 403: return ReachResponse(request.trace_id, False, error_typepermission, error_hint当前身份无权查询该订单) if resp.status_code ! 200: return ReachResponse(request.trace_id, False, error_typedownstream, error_hint查询未能完成请确认订单号是否正确) data resp.json() return ReachResponse(request.trace_id, True, datacontract.shape_output(data), elapsed_msint((time.monotonic()-started)*1000))这段代码里有一个刻意的设计每一种失败都提前翻译好。我在写适配器的时候就决定了模型会看到什么话而不是把原始异常扔上去让它自己猜。这个决定的代价是要多写十几个分支收益是 Agent 的错误处理不再靠运气。4.3 注册与调度把策略和执行分开调度器我写得比较克制核心就是校验—选适配器—执行—记录四步。# core/executor.py import time class Executor: def __init__(self, registry, adapters, policy, tracer): self.registry registry self.adapters adapters self.policy policy self.tracer tracer async def call(self, request) - ReachResponse: contract self.registry.get(request.tool_name, request.tool_version) if contract is None: return ReachResponse(request.trace_id, False, error_typeparam, error_hint没有找到这个能力请换一种方式描述你的需求) # 1. 权限 if not self.policy.check_permission(contract, request.granted_permissions): return ReachResponse(request.trace_id, False, error_typepermission, error_hint当前身份无权执行该操作) # 2. 熔断 if self.policy.is_open(contract.name): return ReachResponse(request.trace_id, False, error_typedownstream, error_hint该能力暂时不可用请稍后再试) # 3. 参数校验 ok, msg contract.validate(request.params) if not ok: return ReachResponse(request.trace_id, False, error_typeparam, error_hintmsg) # 4. 执行带重试 adapter self.adapters[contract.adapter] attempts 0 last None while attempts contract.max_attempts: attempts 1 started time.monotonic() last await adapter.execute(request, contract) self.tracer.record(request, contract, last) if last.ok or not contract.is_retryable(last.error_type): break self.policy.record(contract.name, last.ok) await self.policy.backoff(attempts) self.policy.record(contract.name, last.ok) last.attempts attempts return lastpolicy模块是策略集中地权限判断、配额计数、熔断状态、退避计算全在这里。把它单独拎出来的好处是改策略不需要动执行流程测试策略也不需要真的发网络请求。4.4 让模型看到的工具描述和契约保持一致模型侧我做过一个对比实验手工维护一份工具描述给模型和从契约自动生成跑同一批测试问题。手工维护的版本在两周后出现了三处和实际行为不一致的地方——某次改接口把参数从订单号改成了流水号描述没更新模型一直在传错参数。自动生成之后这类问题从根本上消失了。生成的时候我会做两件事一是把description里的不要用条件单独抽出来放在描述的末尾因为我在实践中发现模型对末尾的约束更敏感二是把example字段渲染成示例值xxx实测对参数正确率有正向帮助。这个技巧很朴素但确实有效。4.5 第一次跑通的日志长什么样联调那天我把日志级别开到了最详细一条成功的调用记录大概是这个样子{ trace_id: r-8f3a91c2, tool: order.query_status, version: 1, caller: cs-agent-v2, adapter: http, params_digest: order_id2024****5678, attempts: 1, elapsed_ms: 187, status: ok, response_bytes: 412, redacted_fields: [order_id] }params_digest是脱敏后的参数摘要保留前四位和后四位方便人工核对中间打码。response_bytes这个字段是我后来加的它帮我发现了一个问题某个工具返回的 JSON 有一百多 KB全是冗余字段塞进模型上下文之后把对话预算吃掉了将近三分之一。后来我在契约里加了字段裁剪配置只保留必要字段一下就瘦下来了。第一次跑通的那一刻其实没什么特别的就是一个status: ok。但正是这条日志让我确认了整个链路是通的注册表加载成功、权限校验通过、适配器正常、脱敏生效。有了这条基准后面所有的排查都有了参照物。5. 常见问题与排查技巧实录5.1 高频故障速查表下面这张表是我把过去几个月遇到的问题归类整理出来的按出现频率排序。现象大概率原因排查动作处理方式模型选错工具工具描述边界不清看召回记录里的候选列表补充不要用条件加规则过滤参数校验大量失败描述里没有示例值统计失败参数分布补 example收紧 pattern偶发超时下游抖动或超时设置过紧看耗时分位值曲线调整超时加抖动退避同一请求重复执行写操作被当成幂等查 attempts 字段写操作关闭自动重试上下文突然变长返回体过大看 response_bytes 分布契约里加字段裁剪某个工具持续失败下游故障或凭据失效看错误分类分布触发熔断检查凭据轮换权限被频繁拒绝权限标签配置漏了比对调用方标签集合补白名单不要放开默认值5.2 几个关键参数的计算与取值超时值的设定我用的不是拍脑袋而是一个简单的推算。先测出工具在正常情况下的 P99 耗时超时值取 P99 的 2.5 到 3 倍。比如订单查询 P99 是 800 毫秒超时设 2500 毫秒左右比较合适。设成 P99 的 1.2 倍看起来更敏感但会导致大量正常但偏慢的请求被判定为超时反而触发不必要的重试。对话层面的总预算我用的是另一个算法所有可能被串行调用的工具超时之和加上模型推理耗时再留 30% 余量。如果一个对话最多串行调三个工具每个超时 3 秒模型推理按 4 秒算那总预算大概是 (3×34)×1.3 ≈ 17 秒。这个数字直接决定了我对外承诺的响应时间。并发上限我是通过压测定的不是算出来的。方法很土逐步加压观察错误率和 P99 何时开始明显上扬取上扬前的那个值再打个七折。留三成余量是为了应对突发的流量尖峰以及给自己一个缓冲不要贴着极限跑。退避参数我用的是 200 毫秒起步、每次翻倍、加 0 到 100 毫秒的随机抖动、最多两次重试。算下来最坏情况是 200 400 600 毫秒的额外等待加上三次请求本身的耗时仍然在单工具超时预算之内。这个匹配关系很重要——如果退避总时长超过了超时预算重试根本没有机会执行完。5.3 我踩过的三个坑坑一把幂等性判断交给了模型。早期我在工具描述里写如果失败了可以重试结果模型真的会对写操作说我再试一次。后来我把重试决策完全收回到调度层模型只负责表达意图重不重试是策略说了算。这个边界必须划清楚。坑二用返回值里的字段名做业务判断。有个工具返回的 JSON 里有个status字段我们直接拿它做分支判断跑了一个月都正常。结果下游悄悄把status从字符串改成了数字枚举代码没崩但逻辑全错返回给用户的都是错误状态。教训是适配层必须做输出结构校验和显式映射不能把下游的结构直接透传上去。坑三日志里记录了完整的入参。这个坑是审计同学帮我发现的。当时为了排查方便我把原始参数全量打了日志里面有用户手机号。虽然日志访问有权限控制但这仍然是不必要的暴露。后来统一改成摘要 打码排查体验确实下降了但这是必须付的代价。折中办法是加了一个受控的调试开关只对特定 trace_id 开启全量记录且开启动作本身有审计日志。6. 上线后的观测与后续扩展6.1 我每天看的四个指标上线之后我给自己定了四个核心指标每天早上花五分钟看一眼基本就能判断系统健康度。工具调用成功率按工具维度拆分。整体成功率没什么参考价值因为它会被高频的简单工具拉高。我关注的是每个工具的独立成功率低于 95% 就会去查。错误分类分布看结构而不是看总量。如果参数校验失败的占比在上升说明模型的工具体现描述需要调整如果连接失败在上升说明下游或网络有变化。这个指标是我调整契约描述的主要依据。耗时分位值重点看 P95 和 P99。平均值在这里毫无意义一个卡住的请求会被平均掉。P99 一旦接近超时阈值就该考虑是下游变慢了还是阈值设紧了。熔断触发次数。这个指标最好的状态是零。一旦非零说明下游出过问题必须查清楚原因因为熔断虽然保护了系统但用户侧是实实在在的失败。6.2 从工具扩到通道跑稳之后我做的第一个扩展是把 reach 的概念从工具扩大到通道。工具是请求-响应式的同步、有明确的返回值通道是消息式的异步、可能长时间没有回音。比如给用户推送一条通知这件事就没有同步返回值可等只能确认已投递到队列。扩展的关键是把适配器的抽象再往上提一层区分同步适配器和异步适配器。异步适配器交出请求后就返回一个受理凭证后续结果通过回调或查询接口获取。这一层抽象加进来之后Agent 的能力范围明显宽了能做的事情从查询扩展到了通知、订阅、触发流程。我建议的顺序是先把同步工具这一层做扎实跑够至少一个月积累足够多的错误样本和参数分布再去动异步通道。因为在没有足够观测数据的时候做扩展等于闭着眼睛改架构。6.3 灰度与回归别让一次契约变更打穿整个系统工具契约是可以改的但改动必须可控。我给契约加了version字段调度时显式指定版本模型侧的工具描述也按版本渲染。改动流程是这样的新版本先注册但只有内部测试身份能调用跑一轮回归用例覆盖正常参数、边界参数、错误场景观察一天没有异常再把默认版本切过去旧版本保留两周观察没有流量之后再下线。回归用例我是从一个真实对话的采样集里积累的目前有两百多条覆盖了二十多个工具。这套用例最大的价值不是保证新功能没 bug而是保证老场景不会被改坏。中间层最怕的就是这种回归——上线当天没人发现一周后某个低频场景的用户来投诉。注意契约变更时一定要同步检查引用该契约的文档和外部对接方。我就吃过一次亏改了参数格式内部系统全对但一个对接的第三方系统还在按老格式传参两边都没报错只是数据悄悄错了。最后分享一个我在实际维护中的体会Agent-Reach 这类中间层的价值从来不是让 Agent 变得更聪明而是让它的行为变得可预期。聪明是模型层的事可预期是工程层的事。当你半夜两点收到告警能靠一条 trace_id 在五分钟内定位到是哪个工具、哪个参数、哪个环节出了问题这套东西就算搭对了。反过来如果一个中间层让排查变得更困难那它就是在制造问题不是在解决问题。
返回列表