为AI智能体设计API
摘要
本文认为,为AI智能体设计API需要遵循与人类不同的原则,强调清晰性、明确性,并避免使用默认值,因为智能体可以阅读整个文档并编写大量代码。
暂无内容
查看缓存全文
缓存时间: 2026/07/15 22:46
# 为Agent设计API
来源:https://www.freestyle.sh/blog/opinion/designing-apis-for-agents
为Agent设计API与为人类设计API是不同的。如今大部分API的消费者都是通过Agent编写的代码来使用API的。这在两年前并非事实,而这一变化改变了我们对API设计方式的思考。
我不再相信大多数为人类构建的系统。我现在对大多数软件包持怀疑态度,我反对工具类函数,并且支持使用非常长的名称——这些都是我在过去24个月内彻底改变的观点。
## 适合人类的优秀API
当我为人类设计一个好的API时,第一步是勾勒出最简使用模式、新手引导流程以及前十大用例。从这些出发,我会想象出实现目标时理想中要编写的代码。然后我会尝试使用它,找到那些让我卡住的边缘情况,并在必要时添加好的默认值和配置。
我希望人们能用五十行代码实现一个可用的功能。在此基础上,API应该足够易用,以至于自然地就能想到去调用更多功能——这就是他们寻找的方向。理想情况下,一个只需要二十个字段中某一个的人,只会在他们想要的那个字段的自动补全中注意到其他十九个字段。一个好的SDK应该足够不言自明,以至于你只需稍微浏览文档就能使用它,甚至不用细看。
一些很棒的例子:
```
client.messages.create(
body="Join Earth's mightiest heroes. Like Kevin Bacon.",
from_="+15017122661",
to="+15558675310",
)
```
```
const paymentIntent = await stripe.paymentIntents.create({
amount: 1099,
currency: "usd",
automatic_payment_methods: {
enabled: true,
},
});
```
这些体现了上述价值。我在十四岁时就成功实现了这两个例子,没有任何问题,而且无需了解Stripe的税务选项、什么是ACH,或者Stripe做的其他五十件事——也无需了解Twilio的调度、机器人或其他功能。后来我才了解到这些。但这些SDK让我在不太了解实际运作机制的情况下取得了大量进展。
## 这与为Agent设计API的方式完全相反
AI Agent可以一次性读完我们所有的文档。一次典型的Claude Code提示词使用超过1万个token;在第一次提示时,Agent就能读取你的整个API和所有相关文档。它们还可以在几秒钟内生成数千行代码来实现目标。这改变了一切。
## 适合Agent的优秀API
Agent最需要的是清晰度——也就是说,API的代码读起来就能精确告诉你它做了什么。在AI Agent时代,代码量大大增加,这使得调试更加困难,而解决这个问题的唯一途径就是精确地定义代码预期要做什么。
这意味着:
1. **默认值是有害的。**可以期望Agent读取文档,了解合适的起始值,并在原地全部填写完毕。显式化现在成本很低,而为预期行为带来具体性可以减少错误。上面的例子中各有三十个我没有填写的字段,因为我不理解它们。而Agent应该填写每一个字段。这并不意味着次要字段和重要字段之间不应该用注释分隔——只是填写大量看似不重要字段的成本已经消失,而理解代码做什么的成本却大幅上升了。
2. **错误不是坏事。**我用过的很多优秀API都帮我掩饰了愚蠢:比如接受大写字母用于仅限小写的字段并自动转换,或者接受多个键表示同一个东西,就像PostgreSQL布尔值接受`true`、`yes`、`on`、`1`。这对于Agent代码库来说实际上是有害的。它会导致不同的函数对同一事物使用不同的值,给后续的审查者带来麻烦。现代的代码调试工具可以期望在调试时读取文档——如果你在错误消息中告诉它,那就更好了。不应该发生半吊子的自动转换;让Agent在它自己那边显式地按自己的方式转换值。对用户而言,新手引导中的错误是坏事,你应该尽量减少它们。但对于Agent,它们则是澄清某个东西实际含义的机会。这并不意味着所有错误都是好的。空白的内部错误没有任何指引,会让Agent陷入混乱,但这只能说明坏错误不好,而不是错误本身不好。一个好的错误是一个很棒的工具:它意味着AI对你的API有误解并解决了这个误解。而误解是肯定会发生的。即使把所有内容都放在上下文中,向后兼容性也会破坏,混淆也会发生。能够带来清晰度的错误可以解决这个问题。(引用:> 错误是Agent找到正确路径的最佳表面之一,但前提是错误信息要精确。与人类不同,Agent会按字面意思遵循指令,所以模糊或错误的错误信息会让它们陷入混乱而不是恢复。在我们的数据中,Agent遇到的摩擦中27%来自错误,因此优秀的设计错误信息与优秀的文档同样重要。——Mika Sagindyk,2027.dev联合创始人,来自X)
3. **偏向分布,而非幻觉。**当API不清晰时,Agent倾向于产生幻觉,并按它们见过的类似API来使用它。因此,我更倾向于使用具体的字段名称而不是通用的。以字段`name`为例。有些API用它作为显示名称,有些作为完整ID,有些作为作用域ID。如果你让Agent在十个不同的用例中使用包含`name`的API,它会以五种不同的方式使用它——而根据上面的观点,只有一种可能是正确的。`name`可以包含空格、斜杠、破折号、下划线、表情符号吗?随着代码库增长,这个问题会加剧。未来阅读这段代码的审查者会再次以这五种不同方式解读`name`。更倾向于显式命名:代替`name`,我更喜欢`displayName`、`slug`、`externalId`,或者任何你选择的、不会携带相同假设的词。这些是Agent能够真正理解的词——不会被难住,也不会被误解。文档注释也有助于巩固正确的理解。
4. **事实,而非感觉。**在Agent时代,API的价值在于它提供了一个Agent或其人类团队无法在内部复制的事实。这可以是账单已支付、消息已发送或虚拟机已配置的事实(这里对Freestyle VMs做个无耻的推广)。除此之外的工具功能大多无关紧要,可以用文档和指南来替代,解释如何实现某个目标。基于此,我对大多数SDK持相当怀疑的态度,尤其是那些除了以特定语言的模式暴露API规范之外还做更多事情的SDK——比如将API错误转换为强类型的TypeScript错误。
## 将理论付诸实践
Freestyle在沙箱领域构建了最强大的虚拟机,衡量标准包括虚拟化质量(我们支持其他人不支持的一切功能,如Docker-in-Docker、嵌套虚拟化和高级网络)、规模(我们在公共层允许8倍大的内存占用,企业版更大)、配置(我们支持完整的操作系统、rootFS和网络,包括私有VPC和VPN连接)、稳定性以及实际可用的完整内存快照——所有这些都在400毫秒内配置完成。
这让我们处于一个奇怪的位置。我们与用户一起解决根本性复杂的问题,这些问题开放式、难以调试、是行业内最困难的——人们来找我们是因为其他沙箱做不到。为此,我们尽力不去做太多事情:只是尽可能一致地提供我上面提到的事实。
我们曾经试图尽可能隐藏复杂性。我们构建了一个声明式功能构建系统,结合SDK工具,让我们可以编写带有依赖项的包。当时的想法是:“如果用户想要Bun,就给他们Bun包,让他们不必担心内部实现,因为我们让Bun运行得很好。”但我们并没有做到。它永远不够可配置,问题总是比答案多,Agent不理解并以各种可能的方式误用它,最终尝试使用这些抽象层成为了用户上手的最大难点。看看下面的代码片段有多漂亮——以及你对其中的运作方式了解得有多有限。
```
import { freestyle, VmSpec } from "freestyle";
import { VmBun } from "@freestyle-sh/with-bun";
const { vm } = await freestyle.vms.create({
spec: new VmSpec({ with: { bun: new VmBun() } }),
});
await vm.bun.install({ deps: ["zod"], global: true });
await vm.bun.runCode(`import { z } from "zod"; console.log(z.string().parse("hi"))`);
```
这是围绕我以前的信念优化的:人类需要快速让一些东西工作,之后会深入研究细节。但我们在几个月前就全部去掉了。我们移除了所有复杂的SDK包和间接层,转而使用一个指南。现在,当你想做任何类似复杂的事情时,你告诉Agent去读指南,它获取相关的代码,适应你项目的规范,按照它想要的方式自定义行为,最终获得更清爽的上手体验和更清晰的代码。
```
import { freestyle } from "freestyle";
const { vm } = await freestyle.vms.create();
// 直接执行——安装包、写文件、运行。没有定制的API。
await vm.exec("cd /tmp && /opt/bun/bin/bun add zod");
await vm.exec(`echo 'import { z } from "zod"; console.log(z.string().parse("hi"))' > /tmp/main.ts`);
const { stdout } = await vm.exec("cd /tmp && /opt/bun/bin/bun run main.ts");
```
## 其他API和SDK中的实践
### 🤖 Agent框架
- 🏆**优秀——Flue Framework。**它为Agent核心需求提供了最简约的语义。一切都是可插拔的函数,主要通过与其他Agent部分的交互来定义。Flue完全清楚自己在做什么——而且实际上它做得并不多:它为构建代码调试工具提供了共享语义。`import { defineAgent } from "@flue/runtime"; // 整个Agent就是一个返回配置的函数——每个部分都是一个值。 export default defineAgent(() => ({ model: "anthropic/claude-sonnet-4-6", instructions: "对传入的GitHub issue进行分类。" }));`
- 👍**良好——Vercel AI SDK。**围绕连接Agent提供了共享SDK语义,非常有用。`generateText`和`streamText`是很好、很清晰的函数,让Agent能够将与许多不同推理API的复杂交互视为事实。`import { generateText } from "ai"; import { openai } from "@ai-sdk/openai"; // 一次调用,一个事实输出。 const { text } = await generateText({ model: openai("gpt-5"), prompt: "总结这段转录。" });`
- 😬**中等——Mastra。**Mastra有很多好的调度和工作流;他们的大多数Agent都是由你可以自己实现的可插拔类型构建的。不过,我认为它在很多地方过于繁重且不清晰。例如,它的Workspace、Filesystem和Sandbox语义对其内部实现者极为数据破坏性:一个不支持FUSE的Docker沙箱与一个结合了SQLite文件系统和裸机GPU的沙箱不是一回事。这在Mastra中可以解决,但一个以Agent为中心的设计会移除大量约定,并用如何实现不同模式的指南来替代。
- 💀**糟糕——Eve。**感觉他们就是把Next.js改成了Agent框架:在2016年很好用。据我所知,他们的技能系统是完全单体且不可编程的,所以如果你想改变加载方式,就必须抛弃它去用他们的动态系统。`--- description: 这个团队如何定义收入。在任何收入问题之前加载。 --- Revenue is net of refunds, over the subscription term. Weeks are Monday-anchored, UTC. ---` 整个技能就是这个文件。加载由构建时的那个`description`字符串决定——没有钩子来编程控制何时或如何加载。
### 📦 沙箱API
- 🏆**优秀——Freestyle**(明显的偏见🙄)。Freestyle的API做得不多;它们说到做到。我们有API让所有你想对虚拟机做的事情成为可能,但我们不会像其他提供商那样用同样的小工具来臃肿它们。其他提供商会说你应该使用他们的`box.git.clone`工具,而我们则说让AI自己写一个`gitClone`函数吧,如果它想要的话——选择深度、认证策略,任何东西;它都只是`exec`的一个包装。`import { freestyle } from "freestyle"; const { vm } = await freestyle.vms.create(); // 编写你想要的包装。一切都只是exec。 const gitClone = (url: string, dir: string, depth = 1) => vm.exec(\`git clone --depth ${depth} ${url} ${dir}\`); await gitClone("https://github.com/acme/app", "/app");`
- 👍**良好——E2B。**E2B展现了一定克制——但不多。他们工具函数问题的一个很好的例子是`sbx.runCode`函数:第一个参数接受代码字符串,第二个参数作为可选项,是一个可选配置字段(这是面向人类设计但对Agent有害的教科书式例子)。深入挖掘的话,你可以将语言设置为你想要的任何语言(js、py、R、bash),但并非所有E2B沙箱都拥有所有这些语言。不过在其他方面他们保持了适度克制——保持了SDK相当一致且可调试。`import { Sandbox } from "@e2b/code-interpreter"; const sbx = await Sandbox.create(); // 语言隐藏在选项包里——并不是每个沙箱都有它。 await sbx.runCode("summary(cars)", { language: "r" }); // ……实际上等同于: await sbx.commands.run("Rscript -e 'summary(cars)'");`
- 😬**中等——Daytona。**Daytona在其SDK中内置了VNC、Git、Docker、Web Terminal、LSP等功能——全部不可配置,有些在某些Daytona沙箱中默默不支持。Freestyle也支持所有这些功能,但不会将它们放在不可插拔的SDK方法后面,所以你可以自定义它们(参见指南)。它们大多数情况下是通用的——我只是不会去用,你的Agent也不应该用。Daytona认为这些功能足够重要以至于要内置到SDK中,所以他们用六种语言发布了SDK。这似乎是为人类设计的;理想情况下,Agent应该足够理解API,让这变得无关紧要。`import { Daytona } from "@daytonaio/sdk"; const sandbox = await new Daytona().create({ language: "typescript" }); // 他们认为你太笨而自己做不了的事情 await sandbox.git.clone("https://github.com/acme/app", "/app"); await sandbox.fs.replaceInFiles(["/app/config.ts"], "dev", "prod"); await sandbox.git.add("/app", ["."]); await sandbox.git.commit("/app", "ship it", "bot", "[email protected]"); await sandbox.git.push("/app"); // 或者…… await sandbox.process.executeCommand( "cd /app && sed -i 's/dev/prod/' config.ts && git commit -am 'ship it' && git push", );`
## 结语
为Agent构建终于与为人类工程师构建分道扬镳了。许多核心原则会延续下来:好的文档仍然重要,遵循使用模式、给出清晰的错误仍然重要。但过去很多重要的事情已经不再重要。代码行数、上手时的错误、开始正常工作所需的阅读量,甚至是上手所需的token数,对于评估API对Agent的好坏都已经相当无关紧要。这篇文章里的所有观点在GPT-4.5下可能就不成立了,所以在GPT-6时代可能也不相关,但我很期待看到接下来的发展。
相似文章
构建高效的智能体
Anthropic 发布了构建高效 AI 智能体的工程指南,倡导采用简单、可组合的模式以及直接使用 API,而非依赖复杂的框架。文章区分了工作流与自主智能体,并就何时使用每种架构提供了实用建议。
为什么大家都觉得AI智能体很容易?🚀
一篇反思性文章,质疑人们轻率地认为构建AI智能体很容易的想法,强调了API、RAG、工具调用、记忆和编排等复杂组件,并指出在需要真正的智能体之前,更简单的工作流往往就够了。
为AI网络代理设计支持代理的网站:面向机器可读性、可操作性和决策可靠性的框架
本文介绍了一个为AI网络代理设计“支持代理的网站”框架,该框架提升了AI代理的可读性、可解释性和可操作性。实验中,该框架在多种代理模型上显著提高了成功率和效率。
在开发 AI 智能体时,我意识到构建一个生产就绪的 AI 智能体不应当复杂。
作者回顾了构建 AI 智能体的经验,并主张创建生产就绪的智能体不应当过于复杂。
关于 AI 智能体的真实内情
一位资深从业者分享了将 25 个以上 AI 智能体部署到生产环境的经验教训,指出记忆、编排和可审计性远比模型选择重要。文章详细介绍了上下文丢失、静默成本循环等常见故障模式,并推荐了包含 Claude Sonnet 4、Pydantic AI 以及 Octopodas 等专用记忆层的技术栈。