
1. 从一次线上事故说起为什么Agent的“记忆”和“恢复”这么难做去年冬天我负责的一个自动化运维Agent在凌晨三点突然卡死。排查日志发现它在执行一个多步骤的服务器巡检任务时因为某个中间步骤的API超时整个任务链直接崩了。更麻烦的是重启之后它完全不记得之前跑到哪一步了只能从头再来。那次事故让我意识到一个问题大多数Agent项目在Demo阶段跑得挺欢一旦进入生产环境上下文管理、检查点机制、任务恢复和资源管控这四个环节任何一个掉链子都会导致整个系统不可用。这篇文章就是围绕这四个核心问题展开的。我会把Agent运行机制拆成几个关键模块从上下文怎么组织、检查点怎么设计、任务恢复怎么做、循环执行怎么控制、资源怎么管这几个角度把整个流程讲透。如果你正在做Agent开发或者准备把Agent从实验环境推到生产环境这些内容应该能帮你少踩不少坑。先明确一下范围。这里说的Agent指的是基于大模型驱动的智能体能够自主规划、调用工具、执行多步骤任务的那种。不是简单的问答机器人也不是固定流程的脚本。它需要维护自己的“记忆”需要在执行过程中保存状态需要在出错后能够恢复还需要控制自己的资源消耗。这四个能力缺一不可。我见过太多项目上下文管理就是简单地把历史对话拼接到prompt里检查点就是往数据库写个JSON任务恢复就是重新跑一遍资源管控就是加个超时。这些做法在简单场景下能用但一旦任务复杂度上来问题就会集中爆发。接下来我会逐个拆解把每个环节的设计思路和实操细节讲清楚。2. 上下文管理Agent的“工作记忆”到底该怎么组织2.1 上下文不是简单的对话历史拼接很多人对Agent上下文的理解还停留在“把之前的对话记录拼起来塞给模型”这个层面。这种做法在单轮问答里没问题但在多步骤任务执行中会迅速失效。原因很简单Agent的上下文不只是对话历史它包含了任务目标、执行计划、中间结果、工具调用记录、环境状态等多种信息。这些东西如果全部平铺直叙地塞进prompt很快就会超出模型的上下文窗口而且模型也很难从中提取出真正有用的信息。我习惯把Agent的上下文分成四层来管理。第一层是系统指令层包括Agent的角色定义、行为约束、输出格式要求。这一层基本不变放在prompt最前面。第二层是任务状态层记录当前任务的目标、已完成步骤、待执行步骤、关键决策点。这一层是动态更新的但结构相对固定。第三层是工具交互层保存最近几次工具调用的输入输出以及从结果中提取的关键信息。第四层是历史摘要层把更早的交互压缩成摘要保留关键结论和决策依据。这样分层的好处是你可以根据任务阶段动态调整各层的详细程度。比如任务刚开始时任务状态层可以很详细任务执行到后期历史摘要层可以压缩得更狠一些给新信息腾出空间。2.2 上下文窗口的分配策略与压缩技巧上下文窗口是稀缺资源必须精打细算。我的经验是系统指令层控制在总窗口的10%以内任务状态层占20%左右工具交互层占40%历史摘要层占20%剩下10%留给模型生成输出。这个比例不是固定的可以根据任务类型调整。比如需要大量工具调用的任务工具交互层可以适当扩大需要长程推理的任务历史摘要层可以多留一些。压缩技巧方面有几个实操方法很管用。第一个是结构化摘要不要用自然语言写摘要而是用JSON格式记录关键字段比如{step: 3, action: query_database, result: found 5 records, decision: proceed_to_analysis}。这样模型解析起来更高效占用的token也更少。第二个是滑动窗口加锚点保留最近N轮完整交互同时把更早的内容压缩成关键锚点比如“已完成数据采集共获取200条记录”。第三个是按需检索不要把全部历史都塞进上下文而是把历史存到外部存储需要时通过检索召回相关片段。注意压缩上下文时一定要保留任务的“决策链”。模型需要知道为什么走到当前这一步否则很容易做出矛盾的决定。我见过一个Agent因为压缩时丢掉了“用户明确拒绝方案A”这个信息后面又绕回去推荐方案A用户体验极差。2.3 上下文数据流的设计与实现上下文数据流的设计决定了Agent能不能高效地获取和更新信息。我推荐用事件驱动的方式管理上下文。每次工具调用、每次模型输出、每次状态变更都作为一个事件写入上下文存储。上下文管理器订阅这些事件根据事件类型决定更新哪一层上下文。具体实现上可以用一个ContextManager类来统一管理。它对外提供get_context()方法返回当前应该塞给模型的上下文提供update(event)方法接收事件并更新内部状态提供compress()方法在上下文接近窗口上限时触发压缩。内部用几个独立的存储来分别维护四层上下文每层有自己的更新策略和压缩策略。这种设计的好处是解耦。模型调用方不需要关心上下文怎么组织只需要调用get_context()拿到当前上下文就行。上下文管理逻辑的变化不会影响其他模块。而且方便做A/B测试你可以同时跑两套上下文策略对比效果。3. 检查点机制让Agent在崩溃后还能“想起来”自己做过什么3.1 检查点该存什么、什么时候存检查点是Agent任务恢复的基础。没有检查点任务一旦中断就只能从头再来。但检查点也不是随便存的存什么、什么时候存都有讲究。存什么至少要存四类信息任务状态当前在哪个步骤、已完成哪些步骤、待执行哪些步骤、上下文快照当前的四层上下文内容、工具调用记录最近几次调用的输入输出和结果、资源消耗已用的token数、API调用次数、执行时间。这四类信息缺一不可少了任何一个恢复出来的任务都可能跑偏。什么时候存我的经验是关键节点必存常规节点定期存。关键节点包括任务开始时、每个步骤完成后、工具调用返回后、模型做出重要决策后。常规节点可以按时间间隔存比如每30秒存一次或者按token消耗量存比如每消耗1000个token存一次。这样既能保证恢复精度又不会因为频繁写存储而拖慢执行速度。3.2 检查点的存储选型与性能考量存储选型要看任务的特点。如果是短任务几分钟内完成用内存存储就够了比如Redis或者进程内的字典。如果是长任务几小时甚至几天必须用持久化存储比如关系型数据库或者对象存储。我一般用两级存储的方案。热检查点存在Redis里读写快用于任务执行过程中的快速恢复。冷检查点定期同步到数据库或对象存储用于跨机器恢复或长期归档。热检查点的过期时间设短一些比如1小时冷检查点保留时间长一些比如7天。性能方面检查点的写入不能阻塞主流程。我通常用异步写入的方式把检查点数据放到队列里后台线程慢慢写。但要注意异步写入有丢失风险所以关键节点的检查点还是要同步写确保落盘成功后再继续执行。存储类型适用场景读写速度持久性成本内存Redis短任务、热检查点极快低中关系型数据库中等长度任务中等高中对象存储长任务、冷检查点慢极高低本地文件单机任务、调试快中低3.3 检查点与上下文的一致性保证检查点和上下文必须保持一致。如果检查点里存的任务状态是“已完成步骤3”但上下文里还停留在步骤2恢复出来的任务就会混乱。保证一致性的关键是原子性写入把检查点和上下文快照作为一个整体写入要么都成功要么都失败。实现上可以用版本号机制。每次更新检查点时版本号加一。上下文快照也带上版本号。恢复时取版本号最高的那组数据。如果检查点和上下文的版本号不一致说明上次写入过程中断了需要回滚到上一个一致的状态。还有一个细节检查点里存的上下文快照应该是压缩后的不是完整的上下文。完整上下文可能很大存起来慢恢复时加载也慢。压缩策略和前面说的上下文压缩一样保留关键信息即可。4. 任务恢复从检查点重建执行现场4.1 恢复流程的完整拆解任务恢复不是简单地“读取检查点然后继续跑”。完整的恢复流程包括几个步骤加载检查点、重建上下文、校验状态一致性、决定恢复策略、继续执行。加载检查点就是从前面的存储里把最近一次检查点读出来。重建上下文是把检查点里的上下文快照还原成运行时的上下文对象。校验状态一致性是检查任务状态和上下文是否匹配比如任务状态说“步骤3已完成”上下文里应该有步骤3的执行结果。决定恢复策略是根据中断原因和当前状态决定是从头跑、从检查点继续、还是跳过某些步骤。继续执行就是重新进入主循环从恢复点开始跑。这里面最容易出问题的是校验状态一致性。我遇到过好几次检查点里的任务状态和上下文对不上原因是写入过程中断了。所以恢复时必须做校验发现不一致就回滚到上一个有效检查点。4.2 不同中断场景下的恢复策略中断场景不同恢复策略也不同。我把它分成三类可重试中断、不可重试中断、状态不确定中断。可重试中断包括网络超时、API限流、临时性服务不可用。这类中断恢复时直接从检查点继续重试失败的步骤就行。但要注意重试次数限制不能无限重试。我一般设3次重试超过就升级为不可重试中断。不可重试中断包括参数错误、权限不足、资源不存在。这类中断恢复也没用需要人工介入。恢复时应该把任务标记为“需要人工处理”并保存足够的诊断信息。状态不确定中断最麻烦比如进程被kill、机器断电。这类中断发生时你不知道最后一个操作到底成功没有。恢复时要做幂等性检查查一下外部系统确认上一个操作的实际状态再决定怎么继续。比如Agent调用了一个“创建订单”的API中断后恢复时要先查一下订单是否已经创建避免重复创建。4.3 恢复过程中的幂等性设计幂等性是任务恢复的核心保障。没有幂等性恢复出来的任务可能会重复执行某些操作导致数据错乱。设计幂等性有几个常用方法。第一个是唯一操作ID。每次工具调用生成一个唯一ID外部系统根据这个ID判断是否已经处理过。如果已经处理过直接返回之前的结果不重复执行。第二个是状态检查。执行操作前先查一下目标状态如果已经是期望状态就跳过。比如要“把订单状态改为已发货”先查一下当前状态如果已经是已发货就不用再改了。第三个是补偿事务。如果操作已经执行了但需要回滚用补偿操作抵消。比如已经扣了款恢复时发现任务应该取消就执行退款。提示幂等性设计要在工具层面做不要依赖Agent的“记忆”。Agent可能会忘记自己做过什么但外部系统的状态是可靠的。把幂等性逻辑放在工具实现里Agent只需要正常调用就行。5. 循环执行Agent的主循环该怎么写才稳5.1 主循环的基本结构与退出条件Agent的主循环是整个运行机制的心脏。它的基本结构是感知当前状态 - 决定下一步动作 - 执行动作 - 更新状态 - 检查是否结束。这个循环一直跑直到任务完成、任务失败、或者达到某个终止条件。退出条件要设计得明确。我一般设这几个任务成功完成、任务明确失败、达到最大步数限制、达到最大token消耗限制、达到最大执行时间限制、人工终止。这几个条件任何一个满足循环就退出。其中最大步数和最大token消耗是最重要的两个它们防止Agent陷入死循环或者消耗过多资源。主循环的伪代码大概长这样def run_agent(task, max_steps50, max_tokens100000, max_time3600): context init_context(task) checkpoint load_checkpoint(task.id) if checkpoint: context restore_context(checkpoint) step checkpoint.step else: step 0 start_time time.time() total_tokens 0 while step max_steps: if time.time() - start_time max_time: return TaskResult(statustimeout) if total_tokens max_tokens: return TaskResult(statustoken_limit) action decide_next_action(context) if action.type finish: return TaskResult(statussuccess, resultaction.result) if action.type fail: return TaskResult(statusfailed, reasonaction.reason) result execute_action(action) context update_context(context, action, result) total_tokens result.token_usage save_checkpoint(task.id, step, context, total_tokens) step 1 return TaskResult(statusmax_steps)这个结构看起来简单但每个环节都有细节要注意。5.2 循环中的状态更新与检查点写入时机状态更新和检查点写入的时机很关键。我的做法是每个步骤完成后更新状态并写检查点。不要在每个子操作后都写那样太频繁也不要等整个任务完成才写那样中断了就全丢了。具体来说一个“步骤”可以定义为“一次模型调用加一次工具调用”。模型调用决定做什么工具调用执行动作。这两个都完成后算一个步骤完成更新状态并写检查点。如果模型调用后决定直接结束任务那就不需要工具调用直接写最终状态。写检查点的时候要注意先更新状态再写检查点。如果先写检查点再更新状态万一更新状态时崩溃了检查点里的状态就是旧的恢复时会重复执行。先更新状态再写检查点最坏情况是状态更新了但检查点没写恢复时从上一个检查点开始最多重复一个步骤不会错乱。5.3 循环中的异常处理与重试机制循环里必须处理异常。工具调用可能失败模型调用可能超时外部服务可能不可用。我的做法是分层处理工具层面的异常在工具内部处理返回错误信息给AgentAgent层面的异常在主循环里处理决定是重试、跳过还是终止。工具层面的异常处理包括网络超时重试、API限流退避、参数校验。这些在工具实现里做Agent不需要关心。Agent层面的异常处理包括模型输出格式错误、工具返回结果无法解析、任务逻辑矛盾。这些在主循环里做一般策略是重试当前步骤重试3次还不行就终止任务并保存现场。重试的时候要注意退避策略。不要立即重试那样可能加剧问题。我一般用指数退避第一次等1秒第二次等2秒第三次等4秒。如果是因为限流导致的失败退避时间还要更长一些。6. 资源管控别让Agent变成“吞金兽”6.1 Token消耗的监控与限制Token是Agent最直接的资源消耗。不加管控的Agent一个任务跑掉几十万token很正常成本很高。管控Token消耗有几个层面。第一个层面是单次调用限制。每次模型调用的输入和输出都设上限比如输入不超过8000 token输出不超过2000 token。超过就截断或者拒绝。第二个层面是任务总量限制。每个任务设一个总token预算比如10万token。消耗到80%时告警到100%时强制终止。第三个层面是动态调整。根据任务的重要程度和紧急程度动态调整token预算。重要任务给多点普通任务给少点。监控方面要实时记录每次调用的token消耗累计到任务级别和用户级别。任务级别的消耗用于任务管控用户级别的消耗用于配额管理。我一般用Redis的计数器来实现每次调用后INCRBY简单高效。6.2 并发控制与队列管理Agent任务通常是异步执行的多个任务可能同时跑。并发控制不好要么资源被耗尽要么任务排队太久。我的做法是分级队列加动态并发。任务按优先级分几个队列高优先级、普通优先级、低优先级。高优先级队列的任务立即执行普通优先级排队等待低优先级在系统空闲时执行。每个队列设一个最大并发数根据系统负载动态调整。系统负载低时并发数调大负载高时并发数调小。队列管理用消息队列实现比如RabbitMQ或者Redis的List。任务提交时根据优先级放入对应队列Worker从队列里取任务执行。要注意任务超时任务在队列里等待太久应该被丢弃或者降级避免用户等太久。6.3 执行时间的预估与超时处理执行时间也是重要资源。一个任务跑太久会占用Worker资源影响其他任务。所以每个任务都要设超时时间。超时时间怎么定我的经验是根据任务类型和历史数据来定。新任务类型先给一个保守的超时时间比如5分钟。跑一段时间后统计P95执行时间把超时时间设为P95的1.5倍。这样既能覆盖大多数正常任务又能及时终止异常任务。超时处理要分级。软超时比如达到超时时间的80%时给Agent发信号让它尽快收尾。硬超时达到100%时强制终止任务保存检查点标记为超时失败。如果任务支持恢复可以稍后重试如果不支持就通知用户。资源类型监控指标限制策略超限处理Token单次调用量、任务累计量单次上限、任务预算截断、告警、终止并发运行中任务数、队列长度分级队列、动态并发排队、降级、拒绝时间单步耗时、任务总耗时软超时、硬超时收尾信号、强制终止存储检查点大小、上下文大小压缩、过期清理压缩、归档、删除7. 常见问题与排查技巧实录7.1 上下文丢失与错乱的排查思路上下文丢失是Agent开发中最常见的问题。表现是Agent突然“忘记”了之前的信息做出矛盾的决定。排查时按这几个方向查。先查上下文窗口是否超限。如果上下文总长度超过了模型窗口模型会自动截断导致信息丢失。查一下当前上下文的token数对比模型窗口大小。如果超了就要优化压缩策略。再查压缩逻辑是否有bug。压缩时可能误删了关键信息。把压缩前后的上下文打出来对比看看关键字段还在不在。我遇到过压缩时把“用户拒绝方案A”这个信息删了导致Agent又推荐方案A。最后查上下文更新是否及时。工具调用返回后上下文有没有及时更新如果更新逻辑是异步的可能存在延迟。把更新逻辑改成同步的或者加个等待机制。7.2 检查点写入失败的常见原因检查点写入失败会导致任务无法恢复。常见原因有几个。存储连接问题Redis或者数据库连接断了。查一下连接池配置加个重连机制。序列化问题上下文对象太复杂序列化失败。检查一下上下文里有没有不可序列化的对象比如文件句柄、线程锁。存储空间不足磁盘满了或者Redis内存满了。加个监控空间不足时告警。写入超时存储响应太慢写入超时。检查存储性能或者改成异步写入。排查时先看日志确认失败原因。然后针对性解决。如果是偶发问题加重试机制如果是必现问题改代码。7.3 任务恢复后行为异常的调试方法任务恢复后行为异常说明恢复出来的状态和中断前不一致。调试时用对比法把中断前的状态和恢复后的状态打出来逐字段对比找出差异。常见差异点包括上下文版本不一致、检查点版本不一致、工具调用记录缺失、资源消耗计数错误。找到差异后回溯写入逻辑看是哪一步出了问题。我一般会在检查点里加一个调试字段记录写入时的关键信息比如时间戳、版本号、上下文摘要。恢复时把这些信息打出来方便对比。7.4 资源消耗异常的定位与解决资源消耗异常表现为token消耗突然飙升、执行时间突然变长、并发数异常高。定位时先看监控面板确认是哪个指标异常。然后看任务日志找到异常的任务。最后看调用链定位到具体的步骤。Token消耗飙升常见原因是循环调用Agent陷入死循环反复调用同一个工具。解决方法是加最大步数限制或者检测重复调用。执行时间变长常见原因是外部服务变慢工具调用的API响应时间变长。解决方法是加超时和重试。并发数异常高常见原因是任务提交失控上游系统疯狂提交任务。解决方法是加限流和队列。问题现象可能原因排查方法解决方案上下文丢失窗口超限、压缩bug、更新延迟查token数、对比压缩前后、查更新日志优化压缩、修复bug、改同步更新检查点写入失败连接断、序列化失败、空间不足查日志、查存储状态加重连、改序列化、加监控恢复后行为异常状态不一致、版本不匹配对比中断前后状态修复写入逻辑、加版本校验资源消耗异常循环调用、外部变慢、提交失控查监控、查日志、查调用链加限制、加超时、加限流8. 几个我踩过的坑和总结的经验第一个坑是检查点写得太频繁。早期我做检查点每个子操作都写一次结果存储压力很大任务执行速度被拖慢了一倍。后来改成每个步骤写一次速度快了很多恢复精度也够用。所以检查点的粒度要适中不是越细越好。第二个坑是上下文压缩太狠。有一次为了省token把上下文压得很厉害结果Agent丢失了关键决策信息任务跑偏了。后来我定了个规矩压缩可以但决策链不能丢。每个关键决策至少保留“做了什么决定”和“为什么做这个决定”两个信息。第三个坑是恢复时没做幂等性检查。有一次任务恢复后重复执行了一个“发送通知”的操作用户收到了两条一样的通知。后来我在所有写操作的工具里都加了幂等性检查恢复前先查状态避免重复执行。第四个坑是资源限制设得太死。早期我给每个任务设了很严格的token限制结果复杂任务经常跑到一半就被终止。后来改成动态预算简单任务少给点复杂任务多给点根据任务类型和历史数据来定。第五个坑是没有监控和告警。Agent在生产环境跑没有监控就像盲人开车。后来我加了完整的监控体系token消耗、执行时间、成功率、恢复次数都有实时监控和告警。出了问题能第一时间发现而不是等用户投诉。这些经验归结起来就是一句话Agent的运行机制要围绕“可恢复”和“可控”来设计。上下文要可恢复检查点要可恢复任务要可恢复资源要可控循环要可控异常要可控。把这几个点做好了Agent才能从Demo走向生产。