构建可靠AI Agent的5条核心法则

以可靠性优先,给出构建生产级 AI Agent Loop 的五条核心设计法则。
文章围绕开源电商售后 Agent Kept 的构建经验,提出让 Agent Loop 在生产环境稳定运行的五条法则:先画状态机再写代码,用终止状态、转移表和前置条件覆盖全部边界;分离循环运行器与宿主,把对话锁、配置、持久化与 trace 导出等不变式交给宿主;将错误转化为结构化结果而非抛出异常,并用封闭失败字母表和 trace 生命周期守卫保障可观测性;保持对话历史原子性,一个 turn 要么全提交要么全不提交;测试须同时断言结果、持久化历史与 trace 形态,并为 LLM provider 建立共享契约套件。这些法则也是审查 Agent 构建成果的检查清单。
当AI Agent从演示走向生产环境,可靠性就从一个技术指标变成了决定成败的生死线。开发者Dragos在构建开源电商售后Agent Kept的过程中,围绕"可靠性优先"这一核心原则,总结出一套让Agent Loop在真实场景中稳定运行的设计法则。这些经验对正在将Agent推向生产的团队具有直接的参考价值。

什么是Agent Loop
Agent本质上是一个循环:向LLM发送消息,执行模型请求的工具调用,再将工具结果返回给模型,如此往复直到模型决定任务完成并准备给出最终答案。每一次模型调用称为一个round,每个round、工具调用和工具结果都被持久化到对话历史中,随后在新一轮中完整发送。一个turn则对应一条客户消息输入和一次结果输出,由一到多个round组成。
作者建议从Amp Code的《How to Build an Agent》教程入手理解基础结构。但真正让Kept区别于普通Demo的地方,在于作者戴上"可靠性眼镜"后的一系列设计决策。
Agent Loop 的技术本质在于把无状态的 LLM 调用工程化成一个可循环、可持久化的有状态流程。LLM 本身没有记忆:每次调用都只看到本次传入的完整消息数组,因此 Agent 的“记忆”完全来自外部持久化的对话历史。所谓 round(轮)就是一次“请求模型 → 执行工具 → 回传结果”的闭合;turn(回合)则是最外层的一次客户交互,可能包含多个 round,直到模型不再请求工具、给出最终答复为止。理解 round 与 turn 的层级关系至关重要,因为限流、预算、锁、原子性写入等可靠性约束往往作用在不同层级上——例如“同一对话同时只有一个请求”约束的是 turn,而 token 预算约束的可能是单个 round。明确这一层级划分,后续状态机、宿主、错误处理的设计才有统一的坐标系。
法则一:先画状态机,再写代码
可靠性视角:我是否知道一个turn所有可能的结束方式?还是等生产环境来告诉我?
第一个关键决策是在写任何代码之前先定义状态机。这提供了一种结构化的方式来考虑边界情况——哪些事件可以在循环的哪个状态下触发,以及哪些终止状态能覆盖这些边界情况。
作者的方法分四步:列出所有事件(谁在等待、每种情况能返回什么)、定义状态(循环在哪里等待)、构建状态机表格(事件为行、状态为列)、定义终止状态(标记所有无法再发起模型调用的单元格)。最终得到一张完整的转移表,其中每个不可能发生的转移都标注了其不变式和强制执行者。
这份状态机直接转化为代码:终止状态变成联合类型,表格变成模型响应上的一个switch语句,不可能发生的单元格变成函数头注释中的前置条件。
法则二:分离循环运行器与宿主
可靠性视角:我的Agent循环把什么当作理所当然?谁来保证这些前提?
循环有一系列来自状态机的不变式,最重要的是"同一对话同时只有一个请求"。这需要对话锁机制,但将这种逻辑折叠进循环运行器会让它过度复杂且难以测试。
作者的方案是将这些不变式转化为循环运行器的前置条件,并把保证它们的责任委托给一个新组件——宿主(Host)。宿主负责:读取环境配置、初始化依赖、在加锁前解析对话身份、持久化turn到对话历史、确保trace在finally块中结束并导出、以及包装HTTP处理器需要的响应信封。
分离后,循环运行器保持清晰的职责:接收一条客户消息和历史,驱动模型和工具直到抵达终止状态,返回结果和待持久化的历史。这种架构对评估(Eval)同样友好——评估运行器只是第二个宿主,用自己的一套依赖组合同一个服务,每个用例在专为它构建的世界中运行未经修改的循环。
**前置条件(precondition)与不变式(invariant)**是此处架构的核心概念。不变式是系统在任何时刻都必须为真的断言,例如“同一对话同时只存在一个进行中的请求”;前置条件是函数被调用前必须已满足的假设,例如“调用方已取得该对话的锁并解析出对话身份”。把不变式维护逻辑塞进循环运行器,会让它既承担业务编排又承担并发与生命周期管理,导致测试必须构造锁、存储、trace 等真实依赖。将不变式的保证责任外移到宿主后,循环运行器的输入契约变得纯粹:一条消息加一段历史,输出则是一组消息加更新后的历史。评估(Eval)之所以天然受益,正是因为评估运行器可以扮演第二个宿主,注入内存版存储与桩依赖,让同一份循环代码在受控世界中运行,避免为测试而分叉出第二套实现。
法则三:错误变成结果
可靠性视角:我的trace是否如实反映了错误?
处理错误的方式对系统可靠性至关重要。作者发现最佳方案是将错误转化为结果(outcome)。未处理的异常会短路turn的执行,可能导致对话历史处于不一致状态、破坏后续消息处理,并留下不完整的trace。
以上下文溢出为例:在Anthropic API中它表现为400错误。如果任其作为错误浮出,Agent会允许更多消息进入对话,每条都失败且无trace记录。更好的做法是将400转化为conversation_full结果,让宿主能声明对话已满并拒绝新消息,同时trace能正确记录。
作者在三个层面处理错误:工具在执行器中运行并settle一切、模型客户端标准化一切、循环捕获剩余部分作为内部失败并记录。Fail-fast仅保留给trace误用守卫和启动时配置验证。
让trace成为生命周期守卫
一个重要的防护模式是强制不变式:已打开的span必须在追踪层关闭。当宿主在turn结束时调用trace.end(),任何仍开放的span都是生命周期违规。在开发环境这是抛出异常,在生产环境它变成保留状态undetermined——既不是error也不是缺失,因为步骤从未报告失败,而缺失的span看起来像从未运行的步骤。
Span 与 Trace 的语义源自分布式追踪模型:一次 turn 是一个 trace,trace 内每个可观测步骤是一个 span,span 必须成对地 open/close,形成有向树结构。可靠性意义在于,“span 未关闭”本身就是一种信号——它意味着某段代码路径既没报告成功也没报告失败,程序可能在中途异常跳出了作用域。若把这种未关闭状态简单记为缺失,外部观测者会误以为该步骤从未运行,从而得出“流程正常提前结束”的错误结论。因此将其标记为独立的 undetermined 状态,等价于在数据层面声明“此处有未知”。这也解释了为什么守卫要分环境:开发期直接抛异常可以让 bug 尽早暴露,生产期则转为保留状态,既不中断用户请求,又保证可观测性不撒谎。
保持失败字母表封闭
常见陷阱是定义几个错误类型,其余全部包装成通用的internal。如果80%的错误落入通用桶,对可观测性的伤害不亚于不完整的trace。作者建议确保失败集是封闭的,特别注意能从internal桶中拉出什么放入自己的类别。在TypeScript中可以用response satisfies never确保新的provider停止原因在构建时失败。
**封闭的失败字母表(closed failure alphabet)**借鉴自类型系统中的“封闭联合”思想:一个错误分类集合若能被穷举,调用方就能被编译器强制处理每一种情况;若存在 internal 这类兜底桶,则任何新错误都会被悄悄吞进桶里,监控告警和统计口径随之失真,出现“80% 流量都是 internal、无法定位根因”的局面。实践做法是:先枚举所有已知的 provider 停止原因与网络失败模式,再借助 response satisfies never 这样的穷尽性检查,确保一旦上游新增一个未覆盖的停止原因,TypeScript 在构建期即报错,而不是运行期悄悄落入兜底。其代价是每次 provider 升级可能需要补类型,收益则是错误分布始终可被观测、可被针对性处理。
法则四:历史是原子的
可靠性视角:失败是否会污染发送给模型的对话历史?
每次模型调用都需要发送完整的对话历史,保持历史干净一致比听起来更棘手。三种典型污染场景:模型在工具调用中途遇到max_tokens,截断的tool_use进入历史却没有tool_result应答;传输在客户消息已追加后失败,重试再次追加;模型拒绝并返回空内容却被存为回复。
解决方案是保持历史原子性:一个turn要么提交所有消息,要么一个都不提交。失败的终止状态原样返回历史,trace记录被丢弃的消息。这样失败的turn既不会毒化对话,也不会消失。
原子性无法撤销已执行的副作用
作者在Kept中遇到一个特殊问题:模型在预算允许的最后一轮请求工具调用,工具调用本身符合预算,但循环还需要额外一轮让模型处理工具响应。解决方案是在触发工具调用之前而非之后检查轮次限制。上下文预算更复杂,预检查最多是估算。Kept的做法是事后检测溢出并转为conversation_full,实际杠杆在上游:通过结果大小和检索top-k限制单个round的增长量。
法则五:每个转换用三种方式断言
可靠性视角:我的测试检查的是Agent做了什么,还是只检查它说了什么?
由于状态机在循环之前就已构建,每个表格单元格就是一个测试夹具。无论测试如何组织,关键是确保它们断言三个层面:结果(客户响应引用了第3.1条)、对话历史中持久化的内容、以及trace的形态及其预期内容。这是对评估的良好预演。
Provider契约测试套件
模型调用是Agent的核心部分,模型客户端是值得特别关注的敏感组件。作者为LLM客户端创建了provider无关的接口,并给它一个共享契约套件,每个provider实现都必须通过。套件覆盖了调用可能结束的每种方式:回复、并行工具调用、max tokens、拒绝、未知停止原因、请求和生成中途溢出、超时、500错误、网络故障。在添加新provider时,同一测试套件的强制接口节省了大量排查provider行为不一致的时间——它为Kept在对话之前捕获了两个漂移bug。
工具的特殊地位
作者特意将工具排除在本文讨论之外,尽管工具的设计和Agent与其交互的方式是Agent系统中不可靠行为最大的来源。工具应在执行器中运行,将每次调用settle为循环可以交给模型的结果,无论内部发生了什么。作者预告了专门讨论工具的文章,将涵盖为什么工具结果有三种状态而非两种、谁决定模型是否可以重试、以及为什么每个结果对模型和trace各有一副面孔。
总结
当Agent产品从演示走向错误操作会造成真金白银损失的生产环境,了解Agent在计划外情况下做什么变得前所未有的重要。我们不再是自己写代码的人,有时甚至不设计架构或审批PR。但仍然是必须为Agent行为承担责任的人。当Agent在生产中行为异常时,工程直觉仍是最宝贵的资源。文中五条法则既可以作为指导编码Agent的简报,也可以作为审查其构建成果的检查清单。
相关推荐

AgentCore 实战:为智能体接入自定义工具与 MCP 服务器
DebuggerCafe 教程演示如何为 Amazon Bedrock AgentCore 智能体接入自定义工具与 MCP 服务器,突破默认工具集限制,并分析工具契约设计、MCP 复用价值及扩展后的工程注意事项。

816B参数只拿39分?开源大模型参数竞赛的性价比真相
Meta 816B Muse Spark Open 在 Artificial Analysis 仅得39分,180B Qwen3.8-Flash-Next 却拿到40分。本文解析11个开源权重模型的参数与分数关系,探讨激活参数、量化体积与部署成本的真实影响。

空中机械臂:无人机从「看」到「改」的下一步
从Reddit讨论出发,分析空中机械臂(Aerial Manipulator)的技术难点、潜在应用场景与落地瓶颈,探讨无人机从环境感知走向物理操作的关键挑战。