12 因子 Agent:构建真正可交付 AI 系统的框架
那些真正在为生产环境客户交付可靠 AI 智能体的团队,大多并未使用智能体框架。他们选择自研。
这一观察源自与 100 多位技术创始人的交流,也是 12 要素智能体(12-Factor Agents)框架那个令人不安的起点——这是一份旨在构建能够投入生产、而非永远停滞在 80% 质量水平的 LLM 驱动型软件的宣言。该框架刻意借鉴了塑造了一代 Web 服务的原始 12 要素应用(12-Factor App)方法论。这种类比是成立的:正如 12 要素应用为团队提供了构建可部署 Web 服务的原则性方法,12 要素智能体也为构建可靠、可观测的 AI 系统提供了原则。
这个拥有 19,000 颗星的 GitHub 仓库记录了表现最出色的生产团队独立摸索出的经验。以下是他们的共识。
80% 的瓶颈并非偶然
在深入探讨具体要素之前,有必要了解该框架旨在打破的失 败模式。过程通常是这样的:
- 决定构建一个智能体
- 为了快点行动,抓取一个框架——LangChain、CrewAI、AutoGen
- 很快达到 70-80% 的质量标准
- 意识到 80% 的质量对于面向客户的功能来说还不够好
- 意识到要突破 80% 的瓶颈,需要对框架的提示词(prompts)、状态管理和控制流进行逆向工程
- 从头开始
瓶颈的出现并非巧合。它恰恰对应于框架抽象开始隐藏你需要直接控制的行为的地方。框架的系统提示词针对平均情况进行了优化。它的状态管理基于某些假设,而这些假设在你的特定工作流中会失效。它的控制循环在工具选择和工具调用之间没有预留人工确认的钩子——而这正是最危险的操作发生的时刻。
Gartner 估计,85% 的 AI 项目未能投入生产。企业分析显示,AI 智能体特有的失败率高达 88%。即使是顶级的智能体解决方案,在真实的 CRM 任务中,目标完成率也低于 55%。这些数字与模型能力无关——Claude 和 GPT-4 在技术上令人印象深刻。它们反映的是演示原型与生产系统之间的差距。
这 12 个要素正是为了直接填补这一差距。
核心洞察:智能体本质上大都是软件
在深入探讨每个要素之前,需要内化一个观点:在生产环境中运行良好的智能体,并非你听到“AI 智能体”时所想象的那样。它们不是一个带着一袋工具在循环中运行、自主决定下一步做什么的 LLM。它们大多是确 定性的代码,在恰到好处的关键点插入 LLM 步骤,以处理需要语言理解或推理的部分。
LLM 被用作结构化转换函数:输入自然语言,输出类型化的数据结构。其他一切——路由、重试、状态管理、人工移交——都是常规软件。这不是一种局限,而是一种设计模式,它使系统变得可观测、可测试且可交付。
12 要素
1. 从自然语言到工具调用
最可靠的智能体模式:将用户输入转换为类型明确的工具调用,然后用确定性的代码执行该调用。例如,“为 Terri 创建一个 750 美元的付款链接”变为 CreatePaymentLink(amount=750, recipient="Terri")。LLM 处理意图理解,函数处理执行。
这建立了一个清晰的接口边界。LLM 的职责是产生有效的结构化输出。代码的职责是根据输出进行分发。测试该接口非常直接——对分发逻辑进行单元测试,对 LLM 的提取准确性进行评估(evals)。
2. 掌控你的提示词
不要将提示词工程外包给框架。一些框架采用黑盒方法:Agent(role="customer support", goal="resolve tickets", tools=[...])。这会给你一个还不错的初始提示词,但它是不透明的。你无法审计输入模型的具体 Token,无法为提示词行为编写回归测试,如果不逆向工程框架内部机制,你也无法对其进行微调。
将提示词视为一等公民代码。对它们进行版本控制。为它们编写评估。掌控每一个 Token。这在前期需要更多工作,但当生产环境出现问题时(这一定会发生),它将带来回报。
3. 掌控你的上下文窗口
在智能体循环的任何给定点,你都在向模型发送:系统指令、检索到的文档、过去的工具调用及其结果、相关会话的记忆以及输出格式指令。这就是“上下文工程(context engineering)”——这门学科出现于 2025 年,现在被认为与提示词工程同样重要。
该要素主张你不应被锁定在标准的消息数组格式中。自定义上下文结构(通常是包装在单个用户消息中的 XML 或专用格式)可以更节省 Token,并为你的特定用例产生更好的推理质量。IBM 记录了一个消耗了 2000 万个 Token 且反复失败的工作流;同样的工作流使用压缩内存指针后,仅消耗 1,234 个 Token 就获得了成功。上下文工程是唯一的变量。
4. 工具仅仅是结构化输出
工具调用 (Tool calling) 并非魔法。它只是模型生成了一个符合特定 Schema 的 JSON 对象。你的代码根据它进行分发。这种去神秘化在 实践中非常重要:即使框架的原生工具调用出现异常,你也可以使用纯结构化输出;你可以在分发前添加验证;你可以独立于 LLM 对分发逻辑进行单元测试。
当你将工具调用视为结构化输出时,你还可以构建自定义的分发逻辑,实现框架无法做到的功能:对特定工具进行限流、为特定的调用签名添加人工审批关口,或者根据环境将相同的工具调用路由到不同的实现。
5. 统一执行状态与业务状态
许多 Agent 系统维护两个状态存储:执行状态(当前处于哪一步、重试次数、等待状态)和业务状态(对话历史、工具调用结果)。这两者之间的状态分歧是生产环境 Bug 的常见来源——Agent 认为它已经完成了第 3 步,但数据库却显示在第 2 步。恢复过程简直是场噩梦。
核心见解:执行状态几乎总能从已发生事件的日志中推导出来。让事件日志成为唯一的单一事实来源。传递给每次 LLM 调用的“状态”只是该日志的一个视图。这使得 Agent 线程可以轻而易举地实现序列化、调试,并从任何点恢复。
6. 通过简单的 API 启动、暂停和恢复
你应该能够通过 API 调用启动 Agent,在等待缓慢的外部操作(人类回复、长时间运行的任务、24 小时的批处理过程)时将其挂起,并通过 Webhook 恢复它——而无需从头开始。
关键细节:暂停必须能够在工具选择 (Selection) 和工具调用 (Invocation) 之间进行,而不仅仅是在完整的 Agent 轮次之间。那个间隙——在模型决定采取高风险行动之后,在行动执行之前——正是人工审批最重要的地方。不支持在该点暂停的框架无法安全地获得生产系统的写入权限。
7. 通过工具调用联系人类
人机回环 (Human-in-the-loop) 不应是架构设计中的事后补救。它就是一个工具调用。RequestHumanApproval(action="delete_customer_record", context="...", urgency="high") 在结构上与 QueryDatabase(sql="...") 完全相同。Agent 调用它,线程序列化,触发 Slack 或邮件通知,执行过程等待 Webhook 响应。
这使得人工监督成为一等公民:可审计、可测试,并能与其他所有因素组合。另一种方案——在框架上硬塞进“我在这里是否应该询问人类?”的临时逻辑——既脆弱又对可观测性工具不可见。
8. 掌控你的控制流
编写你自己的 Agent 循环。那个调用 LLM、处理工具分发并附加结果的 while 循环足够短,完全可以显示在一个屏幕内。它也非常重要,以至于你需要直接控制它。
自定义控制流让你能够:在工具选择和调用之间添加断点,在分发前对结构化输出实施“LLM 作为裁判” (LLM-as-judge) 的机制,在接近限制时添加上下文窗口压缩,监控每一步的延迟和成本,实现客户端限流,以及在不保持进程开启的情况下添加持久化休眠。
这是框架用户最常请求的功能:能够中断正在运行的 Agent 并在稍后恢复。当你掌握了循环时,实现这一点易如反掌。而要在你无法控制的框架中对其进行改造,通常是不可能的。
9. 将错误压缩进上下文窗口
当工具调用失败时,不要只是静默重试。捕获异常,清晰地格式化它,并在下一次 LLM 调用之前将其附加到上下文窗口。LLM 非常擅长阅读错误消息并在下一次尝试中进行调整——前提是它们能看到错误。
此外:实现一个连续错误计数器。在同一个工具上连续失败三次后,跳出循环并通过“准则 7”上报给人类。Agent 在失败的工具调用上不停打转——消耗 Token、产生 API 费用、在日志中堆积——是已证实的生产环境故障模式。明确的错误压缩加上上限设置,能将静默空转转变为可恢复、可观测的事件。
10. 小型、专注的 Agent
构建只做好一件事的 Agent,理想情况下在 3 到 10 步内完成。Databricks Mosaic 的研究发现,当上下文超过 3.2 万个 Token 后,模 型的正确性就开始下降,Agent 会更倾向于重复增长的历史记录中的动作,而不是采取正确的下一步。随着上下文的增长,注意力会随之退化。
实践中的设计启示:将复杂的流程分解为一系列小型专业 Agent,而不是一个单体 Agent。每个小型 Agent 都更容易评估、更容易调试且更容易改进。随着模型能力的提升,你可以慢慢扩大其范围,而不是进行重写。
11. 随时随地触发,在用户所在之处与其互动
如果你实现了准则 6(暂停/恢复)和准则 7(将人工联系视为工具调用),你的 Agent 就已经具备了渠道无关性。从 Slack、电子邮件、Webhook 或 Cron 任务触发,只是调用同一个启动 API 的不同传输方式。在同一个渠道上回复,也只是路由 Webhook 响应而已。
这开启了“外层循环” (Outer loop) Agent 模式:Agent 在复杂任务上运行 5 到 90 分钟,然后在用户所在的任何地方、在关键决策点浮现——而不需要用户一直停留在聊天窗口中。
12. 将你的 Agent 构建为无状态的 Reducer
这是最终的模式。Agent 是一个纯函数:给定当前事件日志和新输入,它返回更新后的事件日志。这是应 用于 Agent 设计的 foldl(左折叠)。每一步:(state, event) → new_state。
无状态 Reducer 极其易于测试。你可以通过构建输入状态并对输出进行断言来编写单元测试。你可以通过事件日志回放任何生产运行。你可以在任何时间点分叉(fork)一个线程进行调试或 A/B 测试。即便单个 LLM 调用是不确定的,整个 Agent 系统从外部看也是确定性的。
这在实践中究竟是什么样子
这些要素(Factors)并不是独立的清单项目。它们是相互组合的。一个拥有其上下文窗口(要素 3)的 Agent 可以正确实现错误压缩(要素 9)。一个拥有统一状态模型(要素 5)的 Agent 可以轻松实现暂停/恢复(要素 6)。一个无状态 Reducer(要素 12)使人工接管(要素 7)变得干净利落,因为没有内存中的状态需要保存。
参考实现是一个你可以记在大脑里的循环:接收事件,从事件日志构建上下文,调用 LLM,获取结构化输出,验证它,执行它,捕获任何错误并将其追加到日志中,检查结果是否为人工联系工具,如果是则暂停并序列化,否则继续循环。
这个循环,在应用了这 12 条原则后,正是那些真正在生产环境中交付 Agent 的团队独立总结出的方案 —— 在任何人写下这些原则之前,他们就已经在这样做了。
