读:从端点到行动——面向 AI 代理的后端设计
目录
传统 API 设计有一个很少被明说、但无处不在的前提: 调用者知道自己在干什么 。调用者知道该调哪个端点,知道请求体长什么样,知道怎么处理返回的错误。前端调后端、服务间调用,这套假设基本成立,毕竟调用代码是人写的,人会看文档。
但 AI 代理不是这样。Satyam Nikhra 在 DZone 上的文章 From APIs to Actions: Rethinking Back-End Design for Agents 问了一个值得认真对待的问题:当你的 API 调用者是一个会推理、会猜测、会犯错的 AI 代理时,传统的端点驱动设计还够用吗?
传统 API 的隐含假设,AI 代理不满足
REST、GraphQL、RPC,具体形式不同,但都基于同一套假设:调用者知道端点( /users 、 /graphql );调用者知道输入 schema,包括字段名、类型、必填还是可选;调用者知道输出格式,能从中提取需要的信息;调用者知道怎么处理各种错误码,400、401、500。
这四条对确定性客户端来说理所当然。确定性客户端,就是每次行为可预测的调用者:前端代码、后端微服务、定时任务,同样的输入一定产生同样的请求。人写的代码按文档来。文档写漏了怎么办?开发者打开浏览器 DevTools 看网络请求、多试几组参数、读错误响应里的提示,怎么都能摸出正确的调用方式。
但 AI 代理不是确定性客户端。它不是在"调用 API",它是在"尝试完成一个目标"。Nikhra 总结了代理容易犯的四类错误:
- 误解 API 合约。把字段的含义搞错,或者不理解字段之间的约束关系。
- 发送不完整或格式错误的输入。少传了必填字段,或传了后端不认识的值。
- 为任务选择了错误的端点。该调
PATCH /users/{id}却调了POST /users。 - 收到错误后不知道如何恢复。遇到HTTP CODE 400 就停住了,不会根据错误信息调整请求。
问题不在模型质量。即使是最好的模型也会犯这些错,因为根子在 接口设计 上。传统 API 是为"不会犯错的调用者"设计的,当调用者是一个概率性推理系统,接口本身就是不匹配的。
核心转变:从"暴露什么端点"到"代理能执行什么行动"
Nikhra 的方案很简单:换一个问题。不要问"我该暴露哪些 API",问"代理应该能执行哪些行动"。
传统设计是这样思考的:
POST /users
PATCH /users/{id}
POST /users/{id}/verify
三个端点,调用者需要知道:先调第一个创建用户,拿到 ID,再调第二个更新资料,最后调第三个触发验证。顺序不能错,参数要自己拼。
而行动驱动的设计是这样命名:
CreateUser UpdateUserProfile VerifyUserIdentity
每个行动封装了一个完整的意图,内部可以跨多个服务、包含异步流程、处理重试和异常。外部看来就是一个操作:"请你验证这个用户的身份"。代理不需要知道内部有几个步骤、调了几个下游服务。
行动代表意图,不代表实现细节 。这个区分是整篇文章的基石。
为什么行动更适合 agent?作者给了三个理由:
- 行动匹配意图。代理的思维是目标驱动的("创建一个已验证身份的用户"),不是步骤驱动的("先调 A,再调 B,再调 C")。行动把意图封装成一个操作单元,跟代理的推理方式对齐。
- 行动隐藏实现复杂性。后端可能有多个微服务、异步工作流、外部集成、重试逻辑。人类工程师能把它们串起来,但代理在多个端点之间管理状态很容易出错。行动在内部编排这些复杂性,对外只暴露一个干净的接口。
- ,代理管理的步骤越多,失败概率越高。三个独立 API 调用,每一步都可能出错,代理需要在步骤之间维护状态。一个
OnboardUserWithVerification行动把 N 个步骤合成一个操作,出错的环节也随之减少。
构建行动接口的 5 项实践
这 5 条大部分是 API 设计的已知原则,但放在 AI 代理的语境下,每条都有了新的含义。
行动合约要清晰。
每个行动需要一个意图驱动的命名(名字本身就说清楚这个行动做什么)、严格定义的输入 schema、可预测的输出 schema。Nikhra 给了一个贷款申请的示例:
Action: CreateLoanApplication
Input: {
"userId": "string",
"income": "number",
"loanAmount": "number"
}
Output: {
"applicationId": "string",
"status": "pending | approved | rejected"
}
这不只是文档注释。这是代理用来推理的接口定义。代理读完这个合约就知道:创建一个贷款申请,需要用户 ID、收入和贷款金额,返回申请 ID 和状态。不用翻 API 文档猜哪个端点对应哪个操作。
Schema 是唯一真相源。
自然语言描述对代理不可靠,自然语言的歧义太多。严格用 JSON Schema 定义输入输出,在执行业务逻辑之前先验证输入,拒绝模糊或不完整的数据。
在行动层建立防护。
代理会犯错,系统必须预期这一点。每个行动都应该加上输入验证、鉴权检查、速率限制、安全默认值。这层是后端外层的"安全包装",就算代理传了奇怪的东西进来,也不会穿透到核心业务逻辑。
让行动幂等。
这点在agent语境下尤其关键,因为代理会大量重试。如果一个行动不是幂等的(每次执行都产生不同的副作用),重试可能产生重复记录、触发意外副作用、破坏数据一致性。行动设计的目标是:重复执行同一操作,产生相同的结果。
错误信息要有用。
传统 API 返回 400 Bad Request 就完事了。但代理不是人,它看到 400 不知道该改什么。代理需要的错误格式是结构化的:错误码 InvalidIncomeValue 、可读消息"收入必须大于 0"、以及这个错误是否可以重试。有了这些,代理才能根据错误信息调整请求,而不是直接失败。
编排层:被忽略的关键一环
切换到行动设计后,一个之前不太显眼的层变得不可或缺:编排层。它负责把代理的意图映射到具体行动的执行,处理复杂工作流的编排(多行动组合、顺序依赖),应用业务规则和策略。
没有编排层,你只是在暴露更好的 API。有了它,系统才真正是"代理感知"的。它理解代理想做什么,并负责把意图变成正确的结果。
现实中的 action-driven design
原文是概念分析,没有给代码示例。但"行动驱动"这个模式在现实中早有实际应用,只是不一定叫这个名字。
MCP 协议(Model Context Protocol)是 Anthropic 定义的 AI 代理与外部工具交互的协议。它的 tool 定义格式就是一个标准的 action contract:
{
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名称" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
每个 tool 由三部分组成: name (意图驱动的命名)、 description (代理用来判断该不该调用)、 inputSchema (严格定义输入格式)。这就是一个 action contract,和 Nikhra 的 CreateLoanApplication 示例结构完全一致。
OpenAI 的 function calling 也是同样的模式:定义 function 的 name、description、parameters(JSON Schema),模型决定是否调用、传什么参数。两家设计思路高度一致,说明这不是某一家公司的偏好,代理与后端交互的通用模式正在形成。
在运维领域,Ansible module 的设计也有类似的思路。一个好的 Ansible module,命名表达了操作意图( ansible.builtin.user 而不是 POST /user ),幂等(同一个 playbook 跑两次不会创建两个用户),返回值结构化( changed 告诉调用者是否做了实际变更, failed 说明是否出错)。这些正好对应 Nikhra 5 项实践中的要点。区别只在于,Ansible 是为人设计的(人写 playbook),MCP 和 function calling 是为代理设计的(代理决定调哪个 tool)。底层的基本原则是共通的。
什么情况下传统 API 仍然够用
原文的论证偏向"代理时代必须转向行动设计",但这个转变有成本,每个行动都需要在 API 之上增加编排层。不是所有场景都该这么做。
调用者是确定性的(前端页面渲染、定时任务、服务间 RPC),或者操作是简单 CRUD 不需要跨多个服务编排,传统端点驱动的 API 完全够用。
action-driven design 是在 REST/GraphQL 之上加一层面向代理的封装。底层仍然是 API,上层是行动。代理通过行动层与后端交互,人和传统服务继续通过 API 调用。
什么时候值得加这层封装?一个简单的判断标准: 如果你的 API 需要代理在多个端点之间做决策和编排,就应该把这部分逻辑收到行动层里 。不要让会犯错的推理系统去管理多步骤的状态,把它做成一个行动,让可靠的后端代码处理编排。
思维转变
Nikhra 在结尾问了一个问题:代理能不能调你的 API?这不是个好问题,更好的问题应该是:你的系统能不能理解 agent 想干什么?
这个问题的背后是后端工程师需要做的四个思维转变:
- 按能力而非端点来思考(不是我暴露了什么数据,而是代理能完成什么任务)
- 为不可靠的概率性客户端设计(不要假设调用者会按你的文档来)
- 构建自纠正、有弹性的系统(代理会出错,系统应该能消化而不是崩溃)
- 把 schema 当合约而非建议(自然语言描述对代理来说等于没有描述)。