☰
AI Agent生产级稳定性实战:从状态持久化到故障恢复的基础设施设计
2026/10/7 4:59:01 网站建设 项目流程

1. 先从“会写代码”到“能扛生产”这道坎说起

做 AI Agent 的人应该都有同感:让大模型学会调用工具、写出一段像样的代码、跑通一个 demo,真的不难。难的是把 Agent 从“我本地能跑”变成“线上扛得住”。我自己在早期做 Agent 项目时踩的坑,几乎全集中在稳定性上——不是模型答得不对,而是进程跑着跑着就死了、HTTP 超时、状态对不上、Token 烧爆了、任务重复执行了两遍。这些问题单看都不起眼,凑在一起就是事故。

标题里那句“AI Agent 会写代码还不够,真正难的是让它稳定运行”,我算是用真金白银验证过。今天想分享的 Strands Harness SDK,是一个专门为解决这个问题而设计的开源基础组件。它不帮你写业务逻辑,也不替你调 prompt,它做的是把 Agent 运行时的那些脏活累活——并发调度、状态持久化、调用追踪、预算控制、故障恢复——统一封装起来。简单说,它是一套面向生产环境的 Agent 基础设施,让团队可以把精力花在 Agent 该干的事情上,而不是反复修补底层。

这篇文章适合谁看?如果你正在用 FastAPI、LangChain、LangGraph、Spring AI 这类框架搭 Agent,又动过“要不要自己写个调度器”“重试到底怎么设计”“状态丢了怎么办”这类念头,那这篇就是给你准备的。我会从设计思路、组件拆解、落地参数到排查经验,把 Agent 稳定性这条链路完整过一遍。文章不会太长篇大论地介绍某个 API 的每个参数,而是讲清楚每个设计背后的“为什么”——很多坑,你只有理解了原理才知道怎么避开。

2. 稳定性不是最后一个补丁,而是整套基础设施

2.1 “稳定运行”到底指什么

很多人一提 Agent 稳定,第一反应是“加个重试”或者“多部署几个实例”。但 Agent 场景的稳定性比传统后端服务更微妙。一个普通 Web 接口,请求进来、处理、返回,状态是天然的短生命周期。而 Agent 任务往往要跟大模型做多轮交互,外部工具调来调去,流程可能持续几分钟、几小时,甚至跨天。这中间任何一环断了——网络闪断、第三方 API 限流、内存被撑爆、进程重启——整个任务的状态就悬空了。

更麻烦的是 Agent 的“非确定性”。同一个 prompt、同样的上下文,模型两次输出可能完全不一样。这意味着你不能像调试普通服务那样,靠“复现”来定位问题。你必须把每一步的输入、输出、决策依据都记下来,才能回溯到底是模型理解偏了、工具传参错了,还是状态被污染了。所以,Agent 的稳定运行,本质上是三个问题的叠加:任务能不能被可靠调度、状态能不能被安全保存、过程能不能被完整回溯。这三点单独做都不难,难的是它们交织在一起时,还需要保持性能和成本可控。

2.2 为什么不能靠模型自身保证稳定性

大模型本身不具备事务性。你给它一个任务,它可能中途“觉得差不多了”就停止调用工具;也可能因为上下文太长,把之前的关键信息覆盖掉;还有可能同时发起多个工具调用,其中一部分失败,它自己却没意识到。这些都不是模型“笨”,而是它的工作机制决定的——它是在做概率生成,不是在执行确定性程序。

所以生产级 Agent 必须有一个“外部仪表盘”来兜底:把模型的每一次决策当成一次可观测的事件,把整个任务当作一个有状态的工作流来管理,而不是单纯依赖模型自身的输出。Strands Harness 这类基础设施,本质上就是在模型外围包了一层“飞行记录仪 + 自动驾驶辅助”。模型负责“想”,Harness 负责“记得、重试、限流、隔离、恢复”。

2.3 基础设施的五个稳定面

我把 Agent 基础设施需要覆盖的问题拆成五个层面,Strands Harness 的设计基本也是沿着这个思路展开的:

层面核心问题典型应对手段
调度层任务多了怎么排队、怎么分派队列、并发控制、优先级、背压
执行层单次 Agent 运行怎么保证不崩超时控制、重试机制、沙箱隔离、资源限制
状态层任务中断后怎么恢复状态持久化、检查点、幂等设计
可观测层出了问题怎么定位链路追踪、日志结构化、Token 和成本计量
恢复层失败之后怎么补救死信队列、补偿动作、人审通道

这五个层面是层层递进的关系。调度做不好,高峰期一来就排队堆积;执行层做不好,单个任务里的模型调用和工具调用就会互相拖累;状态层做不好,前两个层做得再好也会在重启时前功尽弃;可观测层不到位,出了问题就只能靠猜。而恢复层,则是兜底中的兜底——有些失败注定无法自动解决,你得给它们留一条体面的出路。

3. Strands Harness 的部件拆解:它到底提供了什么

3.1 Runner:把 Agent 圈在“围栏”里跑

Runner 是 Harness 最核心的部件之一。它的职责非常朴素:拿一个任务描述,执行它,把结果写回,并报告状态。但生产环境里的 Runner 远比听起来复杂。它要处理超时——如果一个 Agent 调用了外部服务,外部服务 10 分钟不响应,Runner 不能跟着一起等到天荒地老;它要处理重试——重试时不能把整个 Agent 从头跑一遍,否则 Token 成本直接翻倍,更合理的做法是只重试失败的那个工具调用;它还要处理资源限制——一个失控循环可能导致 Agent 无限调用工具,把账户里的余额烧干。

在 Strands Harness 的 Runner 设计里,任务被抽象成“步骤序列 + 状态容器”。每一步要么是一次模型推理,要么是一次工具执行,要么是一个条件判断。每一步的执行都有独立的超时预算和重试策略。这样做的好处是:责任边界清晰。模型推理失败和工具调用失败是两码事,处理手段完全不同。模型推理失败,通常换一次采样就能成功;工具调用失败,可能需要对入参做校验或换一个可用节点重试。把它们混在一起处理,极易产生误判。

3.2 Dispatcher:并发上来了,不能靠加机器硬扛

Dispatcher 负责把大量 Agent 任务分发到不同的 Runner 上。这里有个容易被忽视的问题:Agent 任务的并发模型跟 Web 请求完全不同。Web 请求通常几秒钟就结束了,而一个 Agent 任务可能长时间占用一个 Runner。如果简单地按照“一个请求一个 worker”的思路设计,几个长任务就能把整个 worker 池堵死。

合理的做法是引入“Task Queue + Worker Pool”模式。Dispatcher 先把任务放进持久化队列,然后按 Runner 的空闲情况拉取任务。队列长度要设上限,超过上限就触发背压——不再接收新任务,而不是让任务无限堆积。这个设计在 Strands Harness 里还做了优先级支持。同一个用户的大量请求可以归到一个租户桶里,防止某个活跃用户把整个集群的 Runner 占满,其他用户全部饿死。

3.3 State Store:Agent 的“记忆”不能只放在内存里

Agent 运行过程中会产生大量状态:已经执行到第几步、某个工具返回了什么、当前已消耗多少 Token、用户原本的需求是什么。这些状态如果只存在内存里,一旦 Runner 崩溃或被重新调度,任务就断线了。Strands Harness 的解决方案是引入外部 State Store,默认支持 Redis 和 PostgreSQL 两种后端。

这里有个关键设计:状态写入必须做“检查点化”,而不是每条状态都硬同步。Agent 内部每执行完几个步骤,才把当前快照写入一次存储。检查点之间的状态丢失是可以接受的——大不了从那一步重跑。真正不能丢的是检查点本身。所以 State Store 的写入要走事务,防止半截状态污染数据库。幂等性方面,每个任务都会分配一个全局唯一 ID,写入时以此 ID 为键。重试时 Runner 会先检查该 ID 是否已有检查点,有就直接从检查点恢复,而不是重新开始。这一招能省下大量 Token,尤其适合长时间运行的 Agent 任务。

3.4 Tracer:没有调用链,排查就是大海捞针

Tracer 是给我惊喜最大的一个组件。Agent 的调用链比传统微服务复杂在哪里?传统服务一条链路是确定的:A 调 B,B 调 C,顺序基本固定。Agent 不同,模型决定下一步调什么工具,链条是动态生成的。同一个任务,你根本不知道它会走哪条分支。所以,Agent 的可观测性必须有两个特殊设计:第一,每条链路要记录“决策依据”,也就是模型为什么选择了这一步,需要把相关的 prompt、模型输出、Token 用量都挂到链路节点上;第二,链路节点要支持“动态展开”,任务跑到哪里,记录到哪里,而不是预先定义链路结构。

Strands Harness 的 Tracer 基于 OpenTelemetry 协议实现,支持导出到 Jaeger、Tempo 等主流追踪系统。每个步骤的 trace span 会带上模型名称、模型版本、prompt 摘要、Token 计数、工具调用入参与出参。这样在排查时,你可以直接回答三个问题:模型在什么上下文下做了这个决定?这个决定花掉了多少成本?工具返回结果是否符合预期?这三个问题,恰恰是 Agent 排障最常遇到的盲区。

3.5 Sandbox:别让“会写代码”变成“能写炸弹”

Sandbox 专门解决工具执行的安全问题。Agent 调用代码解释器、执行 Shell 命令越来越常见——这正是“会写代码”价值所在,但也带来了新风险:模型生成的代码如果误删文件、死循环或者访问了不该访问的数据,后果很严重。Strands Harness 为执行类工具提供了轻量沙箱方案,在 Docker 容器里运行代码,默认只读挂载工作目录、禁用网络出站、限制 CPU 和内存。容器结束后直接销毁,不留残余进程。

沙箱看起来是“额外负担”,但实际上是生产环境的必需品。我在部署自用的 Agent 服务时,曾有模型在一次工具调用中生成了递归删除目录的 Shell 命令,幸好当时跑了沙箱,否则损失的不只是数据,还有一整天的排查时间。你如果自己实现 Agent,可以不用 Strands Harness 这套完整方案,但沙箱隔离这层,强烈不建议省掉。

4. 落地实操:三个真实场景下的参数设计与选择

4.1 场景一:面向 C 端用户的高并发 Agent 服务

这类场景最常见,比如做一个自动答疑客服、内容生成助手,或者类似“小红书自动发布”这类半自动运营工具。特点是一天有大量用户请求,单次任务不一定长,但并发峰值高。你需要的核心参数是:最大并发 Runner 数、任务队列长度、单任务超时上限。

我自己的参考配置是这样一组数值:Dispatcher 用 Redis Stream 作为队列,最大排队任务数设为 2000,超过后直接向客户端返回“系统繁忙”;Runner 池初始 10 个,峰值自动扩到 50 个;单个任务超时设为 120 秒——对大多数对话型 Agent 足够,超过 120 秒的基本可以判断是死循环或外部依赖挂了,与其耗着等结果,不如快速失败并给用户一个可解释的兜底回答。

并发参数需要动态调优。我的做法是先压测再上生产。压测工具用 Locust 模拟 100 个并发用户,观察三个指标:任务排队时间是否超过 5 秒、Runner 的 CPU 是否稳定在 70% 以下、P95 任务完成时间是否小于 30 秒。如果排队时间过长,优先加 Runner;如果单任务完成时间过长,优先查模型推理时间和工具响应时间,而不是无脑扩容。这里容易犯的错是:任务完成慢就以为是不够了,结果机器加了一堆,才发现是某个上游 API 被限流了。调优顺序应该是先看调用链,再碰资源。

4.2 场景二:长时间运行的自动化任务

有些 Agent 不是跟用户实时对话,而是后台慢慢跑。比如定时抓取行业数据并生成分析报告,或者监控行情变化在特定条件下触发策略——有人也拿它做自动化交易策略的辅助回测。这类任务的特点是单次运行长达数十分钟甚至数小时,对状态持久化的要求极高。

对这种场景,检查点频率是核心参数。设太密,写数据库太频繁,额外开销大;设太疏,恢复时丢失的进度太多。我的经验是:按“步骤数”和“时间”双重触发。每执行 10 个步骤写一次检查点,同时如果距上次检查点超过 60 秒也写一次。这样,一旦 Runner 崩溃,最多丢失 1 分钟内的进度,恢复代价可控。

长任务的另一个坑是 Token 预算。一个分析类 Agent 如果循环调用模型,每小时可能烧掉数万 Token。Strands Harness 里有 Token 预算计数器,可以为每个任务设定硬上限,达到上限后强制结束并返回“预算不足”的状态。虽然听起来粗暴,但在生产环境里,“预算用完了”是一个比“跑到一半卡住”好得多的结果。你可以让任务在上限的 80% 时触发一次提前通知,让调用方手动决定是否扩容预算。

4.3 场景三:和 LangGraph / Spring AI 这类框架组合

Strands Harness 不是一个拿来即用的端到端框架,它更像一层“底座”。你完全可以在上面跑 LangGraph 定义的多智能体工作流,也可以让 Spring AI 的 Agent 通过 HTTP 接入它的调度接口。我的做法是:业务逻辑放在 LangGraph 里定义好,运行时则交给 Strands Harness 的 Runner——Runner 不感知业务,只负责把一个“图执行请求”跑起来。

这种组合的分工很清晰:LangGraph 负责描述“有哪些节点、哪些边、如何选择分支”,Harness 负责“这个图怎么并发地跑、状态放哪儿、失败了找谁恢复”。Spring AI 那边则不需要引入任何 Agent 基础设施的概念,只需要写一个 Controller,把用户请求包装成任务提交给 Dispatcher,之后异步轮询任务状态即可。

在这个架构下,有几个参数的跨层传递需要注意。LangGraph 的递归限制默认值是 25 步,很多真实任务会撞上这个天花板。我的经验是调到 100,但也要在 Harness 侧设好整体步骤上限,防止失控循环把两个系统都拖死。另一个容易被忽略的点是:LangGraph 的状态快照和 Strands Harness 的检查点不要重复设计。你只需要让 Harness 的检查点覆盖整个图执行过程即可,图内部的局部状态交给 LangGraph 自己管理,两层各管一段,恢复时逐层回放,不要尝试做“全局一致”的状态同步——那会让系统复杂度爆炸。

5. 现场实录:部署 Strands Harness 的几个关键步骤

5.1 基础设施依赖的选型与安装

Strands Harness SDK 提供 Python 和 Go 两种语言的原生接口。Python 版本适合快速集成到现有的 FastAPI 项目里,Go 版本更适合做高吞吐的调度端。如果你所在的团队本来就是 Python 技术栈,起步阶段可以全部用 Python;等并发上来之后,再考虑把 Dispatcher 替换成 Go 实现。核心 API 是兼容的,切换成本不算太大。

我推荐用 Docker Compose 先把整套依赖拉起来。生产环境我用了三件套:PostgreSQL 存检查点和任务元数据、Redis 存队列和缓存、Jaeger 收追踪数据。有个小建议:PostgreSQL 连接池不要用默认配置,否则任务量大时连接数会打满。我给了一个比较稳的配置参考:最小连接数 5,最大连接数 20,连接空闲回收时间 30 分钟。同时开启 PostgreSQL 的 WAL 归档,方便出问题时做时间点恢复。

5.2 第一个 Agent 任务从提交到完成的全过程

我以一个“总结网页内容并生成要点列表”的任务为例,展示整套流程。首先,客户端调用 Dispatcher 的提交接口,传入任务类型和参数。Dispatcher 生成任务 ID 后,把它推进 Redis 队列。Runner 从队列取出任务后,第一步是加载任务对应的 Agent 定义——包括模型配置、工具列表、执行策略;第二步,Runner 创建执行上下文,里面包含初始状态和一个空的追踪链路。

接下来 Agent 开始运行。模型先收到“抓取网页并总结”的指令,判断需要调用网页抓取工具,于是 Runner 把工具调用请求发给 HTML 抓取执行器。抓取成功返回文本,模型再生成总结要点。这中间每一步,Runner 都会向 Tracer 上报 span、向 State Store 写入检查点。全部完成后,Runner 把最终结果和累计 Token 消耗写回任务记录,并更新任务状态为“成功”。整个过程如果你只关注结果,会觉得很普通;但把这个过程中的任意一步替换成“失败 + 重试 + 恢复”,你才会理解基础设施的价值——它让“意外”变得可控。

5.3 配置一个实用级别的重试策略

重试策略是 Agent 稳定性的重头戏。我的建议是:不要给所有步骤配置统一的重试参数,而要分类型配置。

类型重试次数退避策略备注
模型推理调用2 次指数退避,500ms 起步可配合换模型版本重试
外部 HTTP 工具3 次指数退避,1s 起步,上限 10s区分 429 限流和 5xx 异常
代码执行工具1 次不重试代码类错误重试往往无意义
本地数据库操作0 次-让其直接失败,触发上层补偿

模型推理重试次数别设太多。大模型服务通常价格不低,盲目重试会烧掉大量预算。我见过一个案例,团队给模型调用配了 5 次重试,结果模型服务出现故障时,所有任务都在疯狂重试,几小时内账户余额直接见底。正确做法是:模型调用最多重试 2 次,如果还失败,做“降级”——换一个更小的模型完成这次步骤,或者直接标记失败走补偿。至于 Tool 调用,要区分错误码:429 和 503 值得重试,401 和 400 重试一万次也没用,不如早点暴露问题。

5.4 把 Strands Harness 做成一个“企业级中间件”

如果你不是给自己写工具,而是要交付给团队内部或客户使用,建议再花时间做好三件事。第一,封装一层租户和权限模型。每个任务除了业务参数,还要带上租户 ID、所属项目。Dispatcher 调度时按租户做配额隔离,防止一个租户的任务挤占全部资源。第二,提供任务生命周期管理 API。不只是“提交任务”和“查询任务”,还要有“取消任务”“暂停任务”“重新提交失败任务”的能力。这些操作在日常运维里缺一不可。第三,做好审计日志。谁在什么时间提交了什么任务、调用了哪些工具、消耗了多少 Token,都要能追溯到。这在企业合规场景里几乎是硬性要求,也是遇到纠纷时保护自己的证据链。

6. 我踩过的坑:常见问题与排查速查表

6.1 “任务状态显示成功,但结果不符合预期”

这是 Agent 系统最“阴险”的问题,系统层面完全正常,但业务层面不对。多数原因是模型“自认为完成了”,实际上漏掉了关键步骤。排查重点是查看 Tracer 里的链路——模型在最后阶段是否连续发生多次“没有工具调用”的推理?如果是,大概率是模型提前“收尾”了。我的对策是:在 Agent 定义里增加“结束条件校验”节点。任务生成的最终结果必须经过一个规则检查器,比如“结果文本里是否包含关键字段”“是否满足用户给定约束”,不满足就直接回到工作流里重新修整,而不是宣判完成。

6.2 Runner 进程 OOM 或被 Kill

Agent 进程内存问题往往由两个原因造成:上下文无限增长和工具返回超大负载。上下文方面,你没法控制模型会往上下文里塞多少内容,但可以控制自己的策略——给上下文设一个硬性上限(比如 80 KB),超过就触发“摘要压缩”,把旧的对话记录压缩成一段摘要,再接续执行。工具返回方面,调外部 API 时务必限制响应体大小。一个爬虫工具如果抓回一个 100 MB 的页面,内存不出问题才怪。我的做法是:工具层做了大小裁剪,超过 1 MB 的响应直接截断,附上“内容过长,已截断”提示,让模型知道信息不完整。

6.3 任务卡在“排队中”不执行

看队列长度和 Runner 数量。如果队列堆积,Runner 全忙,通常不是机器不够,而是有长任务占住了 Runner。这时要检查是否有“僵尸任务”——Runner 已经失联,但任务状态没有更新。Strands Harness 会在 Runner 心跳超时后,自动将该 Runner 上的任务重新放回队列。如果这个机制没有触发,多半是你自己扩展 Runner 时没有正确维护心跳。扩展到多台机器时,千万别把 Redis 连接配成单机模式,否则所有 Runner 共享一个连接实例,一旦连接池耗尽,整个集群的调度就瘫痪了。我的排查顺序是:先看队列长度变化趋势,再看活跃 Runner 数,最后拉最近一小时的任务状态分布。

6.4 检查点写入导致性能瓶颈

当任务量大时,检查点写入会影响吞吐。这时候不要盲目加 Postgre 配置,先看是不是检查点太频繁了。我之前在压测时发现,每步都写检查点会让整体吞吐下降 30% 以上。后来改成每 10 步写一次,同时用“异步批量写”的方式把多个任务的检查点合并成一次事务,性能立刻回弹。代价是崩溃时可能丢失 10 步内的进度——对于大多数业务场景完全可以接受。另外,检查点的数据别一股脑全存:重上下文、历史消息这类体积大且可以重建的内容,放到对象存储里,数据库只存引用和关键状态字段。

6.5 多实例部署下同一任务被执行两次

这在“至少一次”投递语义下很常见。Runner 崩溃后任务被重新分发给另一个 Runner,但原 Runner 又活过来了,两边同时跑。解决办法是引入分布式锁和幂等表。每次任务开始执行前,先获取基于任务 ID 的分布式锁,同时在数据库幂等表里插入一条状态为“执行中”的记录。执行完成后,把记录更新为“已完成”。重新分发时,如果发现幂等表里已有“已完成”记录,直接丢弃任务。特别注意:幂等表去重不能只靠内存,重启后缓存会清空,必须落在 Postgre 或 Redis 持久化里。

6.6 排查问题时的三定法

最后分享一个我自己总结的排查方法,叫“三定法”:定链路、定成本、定边界。定链路,是把任务 ID 丢进 Jaeger,还原整个执行路径,看哪一步耗时最长、哪一步报错;定成本,是看每一步的 Token 消耗,定位有没有不必要的模型调用在烧钱;定边界,是看每一步的入参出参,确认数据在被送入下一步之前是否符合预期。这个方法对 Agent 类问题尤其有效,因为它的非线性执行路径决定了,任何靠“猜”的排查方式都是在浪费时间。

7. 别急着上框架,先把稳定性五问自测一遍

如果看完了前面的内容,你想在自己的项目里引入类似的机制,我建议不要第一步就想着“上框架”。先回答这五个问题,你才知道自己缺什么。

第一问:如果我部署的服务在凌晨三点突然崩溃,重启之后,正在进行的 Agent 任务会自动恢复到崩溃前的进度吗?第二问:如果某个外部工具连续失败 10 分钟,我的系统是会把所有任务都拖死,还是能快刀斩乱麻地隔离故障?第三问:用户投诉“结果不对”时,我能不能拿出一条完整的调用链,指出是哪一步决策出了问题?第四问:这个月跑了几万个 Agent 任务,我能不能说出每个任务花了多少 Token、调用了几次模型?第五问:如果某个任务触发死循环,我的系统最快能多快发现并终止它,而不是让它烧一晚上的钱?

我见过不少团队,一开始都充满自信,觉得这些问题“到时候再说”。但真的是到生产环境以后,才意识到 Agent 不只是“给模型写 prompt”,而是构建一套能让不确定性变得可控的系统。Strands Harness 这类开源 SDK 的价值,恰恰在于把很多团队已经验证过的稳定性方案工程化、抽象出来,让后来的人不用从零开始踩坑。

我个人在实际落地中的体会是:基础设施的投入越早越好,但不必一步到位。你可以先从一个 Runner 加超时开始,再加一个状态持久化,然后再上队列和追踪。每加一层,系统都更皮实一点。等这些都有了,你会发现,让 AI Agent 在后台真正“干活”,不是因为它更聪明了,而是因为它背后有一套不犯错的基础设施在托底。这也是“生产级”三个字真正的分量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询