智能体频繁出错,先检查后端接口设计
文章以智能体调用 NestJS 订单后端为例,指出多次往返、冗余响应和模糊错误会增加模型误判与错误写入的风险。面向前端页面设计的 REST 接口,直接暴露为智能体工具可能导致数据过量获取和调用顺序错误。
AI 深度解读
Agent 连到业务后端后,频繁失败不一定是模型不够聪明,也可能是接口一直在让它猜:完成一件事要连续调用 5 次,响应塞满无关字段,失败时只返回一句模糊提示。
作者在一个 NestJS 项目里遇到了这个问题。他的做法是在现有服务上加一层薄工具接口,把输入、输出、错误和执行边界收紧。这篇文章值得读的地方,是把 Agent 的可靠性问题落回了程序员能控制、能测试的接口契约。
为什么前端用着正常的接口,交给 Agent 就容易出错
作者的 Agent 需要创建订单、查询库存和退款。接入后经常失败,他将原因归结为后端接口围绕前端页面设计,没有照顾机器调用的需要。
页面接口通常假定调用方了解业务流程。它可以返回整屏需要的数据,可以按滚动列表分页,也可以把一些判断留给用户。人看到库存提示、订单状态和操作按钮,会结合界面补齐上下文;错误不够明确时,还能换个入口继续操作。
Agent 能依赖的,主要是工具描述和返回内容。接口没有交代的信息,就容易变成模型需要猜测的部分。一旦猜测进入写操作,后果可能是错误订单或错误退款。
作者反对两个极端:直接把面向页面的 REST 接口全部暴露成工具,以及提供一个什么都能做的巨型接口。前者让 Agent 背负页面接口的调用顺序和冗余数据,后者把过多决策空间交给模型,也让测试与追踪变得困难。
这里要抓住的是接口用途。REST 并不天然不适合 Agent,专门包一层工具也不会自动可靠。关键在于:调用方是否知道工具能做什么、需要什么,以及执行后究竟发生了什么。
薄工具层应该承担什么职责
作者保留原有业务服务,只在上面加一层翻译:接收明确的参数,调用服务,再把结果整理成小而稳定的结构。
下面这段代码展示了订单工具层的边界,业务操作仍然交给 OrdersService:
// tools/order.tools.ts
import { OrdersService } from "./orders.service";
export interface OrderItem {
productId: string;
quantity: number;
}
export class OrderTools {
constructor(private readonly orders: OrdersService) {}
@Tool({ name: "create_order", description: "Create an order for a customer" })
async createOrder(
@Param("customerId") customerId: string,
@Param("items") items: OrderItem[],
) {
const order = await this.orders.create({ customerId, items });
return {
orderId: order.id,
status: order.status,
total: order.total,
};
}
@Tool({ name: "check_stock", description: "Check stock for one product" })
async checkStock(@Param("productId") productId: string) {
const stock = await this.orders.checkStock(productId);
return { productId, inStock: stock.quantity > 0, quantity: stock.quantity };
}
@Tool({ name: "issue_refund", description: "Issue a refund for a paid order" })
async issueRefund(
@Param("orderId") orderId: string,
@Param("reason") reason: string,
) {
const refund = await this.orders.refund(orderId, reason);
return { orderId, refundId: refund.id, status: refund.status };
}
}
重点看 this.orders.create({ customerId, items }) 这一行:工具把业务执行委托给服务,随后才挑选返回字段。Tool 和 Param 装饰器来自所用的 Agent 框架,并非 NestJS 自带,原文也明确说这是简化示例。
这种拆法有三个实际作用。
首先,规则集中在服务里。订单是否合法、什么状态允许退款,都由 OrdersService 判断。否则同一项业务会出现两套规则,普通接口一套,Agent 工具另一套,后续很难同步维护。
其次,返回值围绕下一步决策设计。创建订单只返回 orderId、status、total,不用把整个订单及其关联对象交给模型。如果以后确实需要新字段,再明确加入契约。
最后,Agent 只接触限定的工具。作者不让它直接写查询、访问数据库或调用管理后台接口,以免绕过已有业务规则。
工具层负责表达能力边界,服务层负责执行业务规则。
不过,TypeScript 类型不等于运行时校验。示例没有展示如何拦住缺失字段、空数组或非法数量,接入时仍需补齐这一环,不能看到 OrderItem[] 就认为输入已经安全。
错误、日志和测试,为什么也是工具接口的一部分
对于 Agent,错误返回会直接影响下一步行动。
一句「出错了」无法区分应该停止、重试还是补充参数。作者建议统一返回简短错误码和可读消息,错误类别保持少量,例如 not found、out of stock、already refunded、invalid input。
库存不足时,Agent 可以向用户说明原因;已经退款时,它应当停止重复尝试。原文没有给出具体错误对象结构,这些名称也不应被当成某个框架规定的标准枚举。
堆栈、数据库报错和内部服务名称则留在服务端日志。它们未必有助于模型决策,还可能被原样转述给用户。
日志需要记录工具名称、输入、输出和成功与否。用户反馈退错订单时,开发者至少能查到调用了哪个工具、传入哪个订单 ID、提供了什么退款理由。日志能还原调用事实,但是否足以定位全部原因,仍取决于记录是否完整。
写操作还要经过认证、权限检查和服务端校验。作者对退款这类敏感操作提出更严格的流程:模型提出操作,用户明确确认,然后工具执行。示例代码并未实现这个确认机制。
测试也不能只覆盖 OrdersService。工具测试要检查真实调用可能出现的输入,包括缺失字段、空 items、不存在的商品 ID,并断言输出结构。只验证方法没有抛异常,抓不到字段改名后 Agent 读到 undefined 这种问题。
返回结构是 Agent 下一步决策的契约,也应该成为测试对象。
我的判断
我认同作者把接口设计放到可靠性讨论的中心。对已经有 Node 或 NestJS 服务的团队,这条路线很务实:复用业务规则,用少量适配代码把能力暴露清楚,比为 Agent 重写一套后端更容易维护。
但文章没有提供失败率、调用次数变化、延迟或模型对照实验。作者说工具层只占底层服务代码的一小部分,并且让 Agent 变得可靠,这是项目经验,不能据此推导出可量化的收益。
「一个工具只做一件事」也需要按业务动作理解。如果创建订单原本就必须完成多项内部校验,工具可以表达完整的创建动作,由服务协调执行。把每个底层步骤都拆给模型,反而可能重现开头连续调用 5 次才能完成任务的问题。
另外,清晰错误只能帮助模型少犯错,不能替代服务端保障。返回 already refunded 的前提,是服务能正确识别重复操作。用户确认也不能替代权限检查,更不能只靠模型声称已经获得确认。
如果现有 API 已经具备明确输入、精简响应、稳定错误和完整权限控制,直接做工具映射未必有问题。是否另建一层,应看契约差异有多大。作者对两种错误路线的批评有启发,但措辞比文中证据更绝对。
能拿来做什么
- 给现有 Node 或 NestJS 项目接 Agent 时,先挑一个完整业务动作整理工具契约,明确参数、成功返回和失败类别,再复用现有服务执行。
- 检查直接从页面接口映射出来的工具,删除没有明确用途的返回字段,并把必须遵循的调用条件写清楚,减少模型猜测。
- 为订单、退款等工具增加输入边界和输出结构测试,优先覆盖缺失字段、空列表、无效 ID 与字段变更,而不只测试正常调用。
- 在开放写操作前,落实服务端认证、权限、校验与调用日志;涉及退款时,把明确确认做成可验证的执行条件。