聊起企业智能体,我这几年最大的感受就是:项目死在模型身上的少,死在 API 身上的多。大家兴致勃勃接好一个大模型,结果 agent 一调业务接口就翻车,要么把参数看错,要么把删除接口当修改接口用,要么返回一大堆根本没法解析的报错堆栈。所以我在企业智能体工程体系 v1.1 里专门把“工具接口层”拿出来重新设计,这一期的核心就是一个词:Agent-First——让 API 为 Agent 可读、可判、可拦。
传统 API 设计时考虑的是前端页面,字段要精简、响应要快、文档给人看;但 agent 消费接口的方式完全不同,它在读描述、猜语义、做判断。你要是继续拿给人看的 API 去喂 agent,就等于让一个实习生对着没有注释的代码干活,全靠猜。这篇文章我会讲清楚 Agent-First 工具接口到底怎么落地,从工具注册、语义描述、副作用声明,到网关拦截和审计追踪,全程结合我在企业里的实操经验,适合后端工程师、AI 应用开发者和技术架构师参考。
1. 为什么传统 API 在智能体面前会“失效”
1.1 智能体需要的不是接口文档,而是能力说明书
先说个特别常见的场景。我们早期让 agent 对接内部订单系统,开发同事拿出一份标准的 RESTful 接口文档,路径、方法、参数、响应码写得清清楚楚。结果 agent 一上线就闹笑话:订单状态字段叫order_status,取值有P、S、C,文档里写着“P=paid, S=shipped, C=cancelled”。人看得懂,但 agent 在上下文里看到的只有order_status: "P",它需要自己推断这个 P 是什么意思。当时模型还“聪明地”猜成了 Pending,于是用户问“订单支付了吗”,它回答“还在处理中”,用户体验直接崩了。
这种事情不是个例。本质原因在于:传统 API 是给程序调用的,而程序里的字段名、枚举值、返回结构都是开发者和前端约定好的,人能通过上下文补齐信息。但 agent 本质上是一个“读自然语言做决策”的系统,它拿到的所有接口信息都来自工具描述和返回内容。如果这些信息本身是残缺的、缩写的、语义模糊的,那再强的模型也救不回来。智能体需要的不是接口文档,而是“能力说明书”——不仅要告诉它接口能干什么,还要告诉它什么时候能用、什么时候不能用、调用后会有什么后果。
1.2 三个真实痛点:语义缺失、错误不可读、控制不住风险
我在做企业智能体工具层之前,专门复盘了团队踩过的坑,核心痛点集中在三个方面。
第一个痛点是语义缺失。上面那个订单状态的例子就是典型,还有更多类似的:时间戳单位是毫秒还是秒、金额单位是分还是元、用户 ID 是数字还是带前缀的字符串,这些信息散落在文档里,agent 根本找不全。一旦猜错,后续逻辑全错。
第二个痛点是错误返回不可读。很多老接口出错时直接返回500加一段堆栈,或者返回{"code": -1, "msg": "系统异常"}。对 agent 来说,这种错误信息毫无价值,它既不知道是参数传错了,还是权限不够,还是服务挂了,于是只能反复重试同一个请求,烧掉大量 token,还把错误原样抛给用户。
第三个痛点是控制不住风险。agent 跟普通程序不一样,它的调用链是动态生成的。用户说“把测试数据清一下”,agent 可能真的去调用批量删除接口。你说 agent 有什么恶意吗?没有,它就是字面理解了用户需求。如果工具层没有风险标识和拦截机制,一次误操作可能就是生产事故。这三个痛点放在一起,结论很清晰:我们不能只把 API 暴露给 agent,而是要把 API“翻译”成 agent 能正确理解和安全使用的东西。
1.3 Agent-First 工具接口的三个关键词
所以我提出 Agent-First 工具接口,核心就是三个词:可读、可判、可拦。
可读,是让 agent 在没有任何人工提示的情况下,仅凭工具描述和返回内容就能理解这个 API 是干什么的、参数怎么传、返回值怎么解析。可判,是让 agent 在调用之前就知道调用条件、副作用和风险等级,从而做出合理决策,比如用户只是想查询,它就不会去调删除接口。可拦,是让企业侧在 agent 和真实系统之间加一道“刹车”,权限不够就拦、预算超了就拦、敏感操作就拦,而且拦截结果要让 agent 看得懂,能自动调整策略。
打个比方,传统 API 是给系统开了一扇窗户,人可以从这儿看到数据;Agent-First 工具接口是给 agent 递了一套带标签的工具箱,每个工具上写着用途、用法、注意事项,工具箱外面还上了一把企业可控的锁。接下来我拆开讲这套体系到底怎么建。
2. 可读性落地:让 API 长出一张“Agent 能看懂的脸”
2.1 工具注册表:先把所有能力盘出来
可读性的第一步不是写描述,而是盘点。企业里的 API 散落在各个业务系统里,光知道有多少个接口还不够,你得把跟 agent 场景相关的接口全部拉出来,填进一张“工具注册表”。我在团队里做的第一件事就是建了一张表,字段包括:工具名、所属系统、调用方式、数据敏感度、业务负责人、当前状态。状态分三种:可直接接入、需要适配、不建议接入。
这个盘点过程比你想象中重要。因为很多团队是“谁需要 agent 接什么就接什么”,今天加一个查订单,明天加一个改价格,最后工具列表混乱到 agent 自己都分不清哪个是哪个。而 Agent-First 要求先有一份全局视图,我们这里是业务方提需求,平台组统一审核注册,谁都不许绕过注册表直接给 agent 塞新接口。建完注册表之后,你会发现真正高频且适合 agent 调用的接口其实不多,我们当时从四百多个接口里筛出来六十多个,第一批先接入二十个就足够跑通核心场景了。
2.2 工具描述怎么写才能减少模型误调用
工具描述是整个 Agent-First 改造里性价比最高的一件事。很多人以为描述就是抄文档上的“功能说明”,实际上完全不是。我给团队定的描述模板是五段式:
第一段,这个工具是干什么的,用一句话说清楚;第二段,在什么业务场景下使用,列举典型用户意图;第三段,什么时候不要用,这个特别关键,要主动写清边界;第四段,关键参数的格式与单位,越具体越好;第五段,调用后会产生什么可见影响,比如是否发通知、是否产生费用。
我举个例子,改之前的描述是“创建采购订单接口”,改完之后是这样:
工具名:purchase.create_order 用途:创建一笔采购订单,保存采购商品、数量、单价和供应商信息。 适用场景:当用户明确表示要“新增采购”“提交采购申请”“下采购单”,或从购物车流程进入结算时使用。 不要使用:不要用它查询订单状态(用 purchase.get_order_status);不要用它修改已存在的订单(用 purchase.update_order);如果用户只是询价或比价,不要创建订单。 参数说明: - supplier_id 必须是企业内部供应商编号,8 位数字,非外部税号。 - items 中每个商品的 price 单位是“分”,不是元。 - auto_approve 默认为 false,只有用户明确要求自动审批时才设为 true。 可见影响:创建成功后会触发审批流,并向采购负责人发送通知。这段描述看起来啰嗦,但实际跑下来效果极好。模型最怕的不是信息多,而是关键信息缺失。你明确说了“不要使用”的边界之后,模型在意图判断上会保守很多,宁可多问用户一句,也不会贸然调用。我们对比过,加了“不要使用”段落之后,误调用次数下降了差不多一半。
2.3 参数与返回结构标准化:减少模型解析负担
描述解决的是“该不该调用”的问题,参数和返回结构解决的是“调用后怎么处理”的问题。企业存量接口千奇百怪,有的参数叫userId,有的叫user_id,有的返回嵌套三层,有的直接返回一个自定义字符串。要让 agent 不出错,工具层就必须做标准化。
参数层我要求每个参数至少包含:名称、类型、必填、描述、示例、枚举值或格式约束。能列出枚举值的就列枚举值,比如支付方式只有alipay、wechat、bank_card三种,你就写清楚,不要把“参见支付字典”这种话留给 agent 去猜。返回层则统一包装成{ "success": true, "code": 0, "message": "ok", "data": ... }结构,错误时success为 false,code是稳定的业务错误码,message是面向用户的可读信息。
还有一点容易被忽略:返回数据的体积控制。大模型上下文窗口虽然越来越长,现在很多模型支持百万 token,但我们实测下来,工具返回如果塞入大量无关字段,模型的理解准确率反而会下降。而且上下文总长度是有限的,你让 agent 把一张八百行的订单明细全读进去,它后续的推理能力就会受到干扰,甚至直接报maximum context length之类的错误。所以工具网关要做“字段裁剪”和“摘要返回”,列表类的数据先返回总数和前二十条明细,用户需要更多再调一个翻页工具。
3. 可判性设计:让 Agent 在调用前就想清楚
3.1 副作用声明:给工具打上风险等级
可判性的第一步,是让 agent 在调用前知道“这个操作做了会怎样”。我给每个工具加了一个字段叫side_effect,标明风险等级。级别分成四档:
| 风险等级 | 含义 | 典型工具 |
|---|---|---|
| L0 | 只读操作,不会改变任何业务数据 | 查询订单、查询库存、获取天气 |
| L1 | 会产生数据变更,但可恢复、可审计 | 保存草稿、更新备注、修改个人设置 |
| L2 | 会产生不可逆或影响较大的变更 | 删除记录、批量修改、覆盖配置文件 |
| L3 | 涉及资金、对外通知、法律效力 | 转账、支付确认、发送营销短信 |
这个风险等级不只是给模型看的,更重要的用途是配合后面的拦截网关。但在可判这层,有和没有差别非常大。我见过一个很典型的案例:用户对 agent 说“帮我把这个商品下架”,L0 工具里没有下架接口,但有一个“批量更新商品状态”的接口,agent 发现这个接口能改状态,就调用了,结果把商品直接改成“删除”状态。如果没有副作用声明,agent 根本意识不到“批量更新状态”可能是 L2 风险操作。加上之后就稳多了,agent 遇到高风险操作会先跟用户确认,而不是自己直接执行。
3.2 前置条件、互斥关系和配额信息
除了风险等级,可判还包含三个信息维度:前置条件、互斥关系和配额。
前置条件,是指调用这个工具前必须先满足什么条件。比如“提交发货单”的前置条件是“订单已付款”,“发送报表”的前置条件是“用户已绑定邮箱”。我把这些条件直接写在工具元数据里,调用前网关会校验,同时也会在工具描述中告诉 agent“如果前置条件不满足,不要调用,先引导用户完成前置操作”。
互斥关系,指的是哪些工具不能同时或在同一状态下调用。典型例子是“编辑草稿”和“提交审批”,草稿提交之后就不能再编辑了。如果 agent 在审批通过之后又去调编辑接口,网关会直接拒绝,并返回互斥错误。
配额信息则更实际。企业里的 API 不可能是无限量的,尤其是涉及第三方服务或者付费大模型的接口,一次调用就是钱。我们在工具描述里暴露了额度信息,比如“本接口每分钟限调 30 次”“当前账号本月剩余短信发送额度 1520 条”。agent 看到额度不足时,会自动切换到替代方案,或者提前跟用户说明,而不是等到调用失败才一脸懵。
3.3 让错误反馈自带“下一步建议”
可判的最后一个细节是反馈。如果 agent 还是判断错了,工具层不能只说“不行”,要告诉它“怎么才行”。我们设计了一套结构化错误返回,包含四个字段:code(错误码)、message(人话描述)、agent_hint(给 agent 的下一步建议)、customer_message(给用户的可见文案)。
举个例子:
{ "success": false, "code": "PARAM_FORMAT_ERROR", "message": "supplier_id 格式错误", "agent_hint": "supplier_id 应为 8 位数字,当前收到的是 'SUP-1024'。请提取纯数字部分 00102400 后重试,或向用户索要正确的供应商编号。", "customer_message": "供应商编号格式有误,请确认后重新提交。" }这个设计的好处是,agent 拿到agent_hint后可以直接自我修正,不需要瞎猜。我们统计过,在返回结构里加入agent_hint之后,同一轮会话内 agent 自动修复参数错误的比例超过七成,用户几乎感知不到中间出过错。这比让模型从原始报错里自己琢磨要高效得多,也省 token。
4. 可拦性实现:模型不能说走就走
4.1 智能体调用链中的“刹车”必须独立于模型
讲了可读和可判,接下来是最核心的企业诉求:可拦。很多人有个误解,觉得只要模型足够聪明,它就不会做危险操作。这是把安全寄托在概率上,企业生产环境不能这么赌。模型是概率系统,即使工具描述写得再清楚,它仍然可能误判、可能被用户绕晕、可能被精心构造的提示词带偏。所以必须有一条独立于模型的“刹车系统”。
我们的调用链是:Agent 应用 → 工具网关 → 企业内部系统。工具网关独立部署,所有 agent 调用工具都必须经过它。网关不只是一个反向代理,它还承担了身份认证、参数校验、风险拦截、配额控制和审计记录这些事。最关键的一点:模型本身没有权限绕过网关,因为底层 API 地址从不直接暴露给 agent 应用,agent 能拿到的最多就是网关地址和工具 ID。这样即使模型产生幻觉,胡编了一个工具调用,网关照样能拦下来。
4.2 三层拦截策略:权限、预算、行为
我们在网关里把拦截逻辑做成三层,每层职责单一,出了问题也好排查。
第一层是权限拦截。agent 在发起对话时必须带上用户身份,网关通过身份拿到角色和数据权限。比如普通员工调“查看全员薪资”的接口,网关直接拒绝,不管模型怎么说都放不过去。这一层还有环境校验,比如只允许内网 IP 调用某些敏感工具,外部流量一律拒绝。
第二层是预算拦截。面向 agent 的资源也要算成本账。我们在网关上给每个用户、每个部门、每个 agent 应用都配置了额度,维度包括每分钟调用次数、每日 token 用量、按次计费的接口费用上限。超了就拦截,返回QUOTA_EXCEEDED。预算拦截最大的价值是防止失控,我们出现过一次 agent 在循环任务里反复调用某第三方付费接口的情况,如果没有配额,一个晚上能烧掉几千块,有了配额之后立刻熔断,只多花了不到十块。
第三层是行为拦截。这是最贴近业务的一层,规则由业务负责人和平台组一起配置。比如:L2 及以上风险操作必须二次确认;同一用户在十分钟内调用删除接口超过五次就临时锁定;敏感字段不得出现在返回报文中。行为拦截本质上是把企业制度翻译成代码,让 agent 在制度边界内行动。
4.3 审计与追踪:出事之后能看得清
可拦不只是“拦住”,还要“留痕”。每次 agent 调用工具,网关都会生成一条审计记录,包含四个要素:谁(用户身份、agent 应用 ID)、什么时间、调用了哪个工具、传入什么参数、返回什么结果、有没有被拦截、被哪条规则拦截。我们用统一trace_id贯穿整条链路,Agent 应用的日志、网关日志、业务系统日志全部挂着这个 ID。这样一旦出了线上问题,从用户问题开始到最终操作结果,整条链路的每一步都能回放出来。
这块在金融、政务类项目里是硬性要求,不做没法过合规评审。但即使你的行业没那么严格,我也建议把审计日志做好。因为在 agent 场景下,操作可能是模型自动决策的,用户本人未必清楚发生了什么。没有审计,出了纠纷你连证据都拿不出来。我们后来有个真实案例:某个用户声称自己没下过删除指令,但是数据被删了。我们拉出审计日志一看,发现是 agent 在解释“帮我清理无用条目”时,把一批用户手工标记为“可清理”的数据执行了删除。虽然操作确实符合当时的字面语义,但事后复盘发现是工具描述里缺少“清理需二次确认”的约束。这就全依赖审计日志才能把问题定位清楚。
4.4 拦截结果必须对 Agent 友好
很多系统做拦截,返回一个冷冰冰的403 Forbidden完事。但对 agent 来说,这种响应意味着“我不理解为什么被拒绝,也不知道该怎么办”。所以我们的可拦性设计里有一条硬要求:所有拦截响应都必须遵循前面说的结构化错误格式,并且要带上agent_hint。
比如用户想查薪资,但权限不够,网关返回:
{ "success": false, "code": "PERMISSION_DENIED", "message": "当前用户无权访问薪资数据", "agent_hint": "用户没有该权限,不要重试。应该向用户说明需要联系 HR 或上级申请权限,并提供申请入口链接。", "customer_message": "抱歉,您暂时没有权限查看薪资数据,请联系 HR 申请开通。" }模型拿到这个返回后,不会再傻乎乎地重试,而是会自动切换成用户话术。我们实测,在没有agent_hint的时候,agent 收到 403 后会反复重试两三次才肯放弃;加上agent_hint之后,一次就明白了。这个改进几乎没有成本,收益却是实打实的,既省 token,又提升了用户体验。
5. 实操记录:60 个存量 API 的 Agent-First 改造
5.1 存量 API 盘点与筛选
前面讲了这么多理论和设计,这一节我把自己团队的实操过程完整还原一遍,给想落地的朋友一个可直接参考的路径。
第一步是盘点。我们把公司内部所有接口拉了一遍清单,对照业务方提上来的智能体需求场景,按三个维度打分:调用频次(高/中/低)、业务价值(核心/辅助/边缘)、改造难度(简单/中等/复杂)。最后形成了三级决策:优先接入、二期接入、暂时不接入。优先接的是那些高频且价值大、改造难度又低的接口,比如订单查询、用户信息、库存查询;暂时不接的是那些低频、手工操作密集、改造又贵的接口,等业务真正需要再说。
这里有一条经验:不要追求“所有 API 都一次性接入”。智能体项目的价值在于把高频场景跑顺,不在于接口数量多。我们第一批只接了 20 个,已经能覆盖大约 80% 的用户诉求。接入太多反而会让模型在选择工具时犯迷糊,上下文也被工具列表撑爆。
5.2 适配层怎么搭
我们选了不改原系统架构,加一层工具网关做适配的方式。原因很现实:几十个存量系统属于不同团队,改他们的代码周期太长,业务方等不起。适配层只做三件事:鉴权统一、协议转换、工具描述注册。
技术选型上我们用的 FastAPI,因为 Python 生态对模型工具描述生成的支持很成熟,写起来也快。下面是网关中最核心的一段伪代码,给大家一个直观印象:
from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel app = FastAPI() class ToolCall(BaseModel): tool_id: str params: dict @app.post("/v1/tool/execute") async def execute_tool(call: ToolCall, x_user_id: str = Header(...)): # 1. 校验用户身份 user = auth_service.get_user(x_user_id) if not user: return structured_error("AUTH_REQUIRED", "未登录或登录已过期") # 2. 查找工具元数据 tool = registry.get_tool(call.tool_id) if not tool: return structured_error("TOOL_NOT_FOUND", "工具不存在") # 3. 权限拦截 if not policy.check_permission(user, tool): return structured_error("PERMISSION_DENIED", "无权访问") # 4. 配额拦截 if not quota.consume(user, tool): return structured_error("QUOTA_EXCEEDED", "配额已用尽") # 5. 行为拦截 if tool.risk_level >= 2 and not call.params.get("confirmed"): return structured_error("RISK_NEED_CONFIRM", "高风险操作需二次确认") # 6. 执行真实调用(内部业务地址) result = await backend_call(tool.backend_url, call.params) # 7. 标准化返回 return normalize_result(tool, result)这段代码是简化版,但核心流程已经体现了可拦的思路:所有拦截都发生在真实调用之前,一旦发现风险,直接返回结构化错误。实际落地中,registry、policy、quota都是从注册表加载配置的,改规则不需要改代码,运营同学在配置后台点一点就行。
5.3 完整工具定义示例
工具注册表中的定义,最终会被翻译成模型可读的 JSON Schema 和自然语言描述。下面是我们一个真实工具的简化版 YAML 定义:
tool_id: order.create name: 创建销售订单 description: | 创建一笔新的销售订单。当用户明确表示要下单、购买、提交订单时使用。 不要用于查询订单状态,不要用于修改已有订单,不要在用户仅咨询价格时调用。 risk_level: L1 side_effect: - 生成订单号 - 锁定库存 - 向订单中心发送消息 auth_required: true rate_limit: per_minute: 60 parameters: - name: customer_id type: string required: true description: 客户编号,形如 CUS-2024-0001 example: "CUS-2024-0001" - name: items type: array required: true description: 商品列表 items: type: object properties: sku_id: type: string description: 商品 SKU 编号 quantity: type: integer description: 购买数量,大于 0 price_cents: type: integer description: 单价,单位是分,不是元 minimum: 0 - name: auto_approve type: boolean required: false default: false description: 是否自动提交审批,默认 false response_schema: type: object properties: order_id: type: string description: 生成的订单号 total_amount_cents: type: integer description: 订单总金额,单位分 estimated_ship_date: type: string description: 预计发货日期,ISO 8601 格式我们后来把这些 YAML 交给平台自动生成工具描述文本并注入到 agent 的系统提示词里。这样业务方只维护一份注册表,模型侧的工具列表由平台自动同步,省掉了手工复制粘贴带来的文档漂移问题。这里尤其要注意price_cents这种单位细节:模型如果不知道单位是分,很容易把 100 元传成 100 分,这个错误你只靠模型自己永远发现不了。写清楚参数单位,比你在提示词里苦口婆心强调“注意单位”有效得多。
5.4 改造后的效果:一组值得参考的数据
改造上线跑了大概一个月,我们对比了改造前后的核心指标。先说调用成功率:之前 agent 直接调裸 API 时,因为参数格式、枚举值、权限问题导致的失败,一次任务里平均要出 2.3 次;改造后这个数字降到了 0.8 次。工具网关拦截下来的高风险操作,第一周就有 17 次,其中既有模型误判,也有用户故意诱导,如果没有网关,这 17 次里有好几次结果都不可逆。
Token 消耗也同样值得关注。过去 agent 拿到一个错误后就来回试错,一次简单查询经常要烧掉几万 token。改造后,工具返回标准化、错误提示带下一步建议,单次任务的平均 token 消耗大约降了 35%。省钱只是一方面,更重要的是用户体验变好了:用户不再需要盯着 agent 反复转圈,一个响应的平均时间从 14 秒降到了 8 秒。
这组数据不是要说明我们做得有多好,而是想告诉大家,Agent-First 工具层不是一个纯概念,它带来的收益是可测量的。如果你的 agent 项目里有类似“调用成功率低”“上下文爆炸”“模型老调错工具”的问题,大概率就是工具层这部分没做到位。
6. 常见问题与排查技巧实录
6.1 Agent 不按描述调用工具,专挑老接口
现象:明明工具注册表里已经把order.create描述得清清楚楚,模型有时候还是会去调一个老的“创建订单V1”接口,结果那个接口参数要求完全不一样,直接报错。我们排查后发现,原因是老接口还在注册表里,而且模型在训练数据或历史对话中“见过”它,形成了路径依赖。
解法:老接口不要和网关新接口并存。我们在网关层统一做了“熔断”:所有存量接口地址在网关之外一律拒绝外部调用,agent 应用发起的请求只能访问工具网关注册过的工具 ID。老接口不在注册列表里,模型就算猜到老的路径,也调不通。让模型“没得选”,比让它“好好选”更可靠。
6.2 上下文被工具返回塞满,直接超长
现象:某个工具返回了一张大列表,比如一次查出一千条订单明细,agent 把整个列表都放进了上下文,紧接着就报类似maximum context length的错误,对话直接中断。我们一开始还以为是模型窗口太小,后来发现是返回内容控制没做好。
解法:工具网关对返回数据做两层处理。第一层是裁剪,默认只返回前 20 条明细和总数,后续数据通过一个“加载更多”的工具获取;第二层是摘要,对于超长文本字段,网关会先做一个摘要提取,只把关键摘要给模型,完整原文放在附件或数据库里,用户需要时再拉取。这样既不影响模型理解,也不浪费上下文空间。
6.3 Agent 把删除接口当修改接口用
现象:用户说“把这条记录的标题改一下”,agent 直接调了删除接口,业务数据差点没了。这是最吓人的一类问题。原因是描述里虽然写了“删除”和“修改”,但模型有时会根据用户句式的相似度匹配工具,尤其当修改接口的描述不够清晰时。
解法:双管齐下。第一,工具描述里增加“相似工具对比”段落,明确写清“修改用 update_record,不要用 delete_record;删除会彻底移除记录且不可恢复”。第二,网关对delete类工具实行强制二次确认,不管模型是否确认过,只要风险等级是 L2 且用户没有在上下文里明确表达“删除”意图,就返回RISK_NEED_CONFIRM,要求 agent 必须让用户再次确认。这套组合拳落地之后,没有再出现过删除误触发。
6.4 报错信息直接透传给用户,体验差
现象:agent 调用工具失败后,直接把原始错误拼进回复里,用户看到一堆“api error: 400”“login failed”之类的技术信息,一头雾水。这种情况在早期特别常见,因为很多模型默认会把收到的错误信息当成可回复内容。
解法:工具网关在返回错误时,除了message和agent_hint,还要带上customer_message字段。这个字段是给用户看的文案,必须简洁、友善、可操作。我们还在网关层做了过滤,凡是内部错误消息里出现堆栈、类名、数据库语句等内容的,一律不放进给 agent 的工具结果里,防止技术细节意外泄露给用户。模型策略上也要在系统提示词里加一句“用户问题只能用 customer_message 回复”。
6.5 Agent 拿到鉴权错误后无限重试
现象:用户登录状态过期之后,agent 还在继续调用需要鉴权的工具,每次返回 401,它就换着方式重试,三番五次之后才肯停下来。这既浪费 token,又让用户干着急。
解法:在工具元数据里加上auth_required: true的标记,同时网关对鉴权错误返回固定错误码AUTH_EXPIRED,agent_hint明确写“不要重试,请引导用户重新登录,登录地址为 https://sso.example.com/login”。另外我们在 agent 应用侧也加了一个机制:单次任务中出现三次鉴权错误,网关直接对该用户的任务链熔断,必须要前端重新登录后才放行。这两个措施加一起,基本根治了无限重试的问题。
6.6 Agent-First 工具层常见问题速查
我把上面这些问题整理成一张表,方便团队内部快速排查:
| 问题现象 | 可能根因 | 快速解法 |
|---|---|---|
| 模型调用老接口 | 旧工具未下线,模型路径依赖 | 网关统一收口,只暴露注册工具 ID |
| 上下文超长或爆炸 | 工具返回未裁剪、未摘要 | 列表默认返回 20 条,长文本先摘要 |
| 删除/更新误用 | 描述边界不清,缺少对比说明 | 描述加“相似工具对比”,网关加二次确认 |
| 用户看到技术报错 | 错误信息透传,缺少用户话术 | 网关统一customer_message,过滤堆栈 |
| 鉴权失败后无限重试 | 缺少可读错误结构和熔断机制 | 返回AUTH_EXPIRED,任务级熔断 |
7. 基于这些实操经验,再说几句掏心窝的话
这个体系从设计到落地,我个人最大的体会是:Agent-First 不是给模型用的,是给企业用的。它的价值不是让模型“更聪明”,而是让企业“敢放手”。整套工具层设计完,我的团队在运营 agent 时安全感高了很多,因为即使模型出现幻觉或者被诱导,网关上还有最后一道硬拦截,不至于直接产生不可逆的后果。
最后分享一个小技巧:把每次模型调用出错都当作一次“工具描述迭代”的素材。我们团队有个习惯,每周复盘一次 agent 的失败调用日志,凡是模型理解偏差导致的错误,都要回到工具描述或者注册表里找原因,然后做增量修改。这个习惯坚持下来以后,工具的可用性会像滚雪球一样越滚越好。三个月之后,我再回去看第一版工具描述,几乎每一段都被重写过,而每次重写都源于一次真实的事故或误判。这就是 Agent-First 工具接口打磨的真实过程:它不是一次性改造,而是一种持续运营。