资讯动态

AI Agent生产环境可靠性工程:上下文、检查点与任务恢复机制

发布时间:2026/10/20 8:06:59 来源:尧图企业网站定制
我们有个Agent半夜跑一个工单处理任务跑了接近两个小时在调用内部订单接口之后进程直接卡死。重启之后我盯着日志反复看越看越心虚这个Agent既没有把上下文持久化也没有做过检查点任务恢复的代码是一行都没有资源管控更是空白用户只能含着泪重新提一遍任务。那次事故之后我把Agent运行机制放到了和模型能力同等重要的位置。上下文、检查点、任务恢复、循环执行、资源管控这五件事在23.3这个版本里被我们整体重构了一遍。下面是我拆完整个流程之后整理出来的完整版适合正在做Agent框架、Agent平台或者想把一个Agent Demo变成可运维服务的人。1. 先理顺关系五个机制不是五个孤立模块很多人在接触这套东西的时候下意识把上下文管理、检查点、任务恢复、循环执行、资源管控当成五个独立组件去设计。这个出发点很容易把系统拆碎。实际上它们是同一条流水线上的五个切面只有放在一起看才能理解各自的位置。1.1 一次Agent运行的完整生命周期拿我们23.3版本里一个标准Agent任务来举例一次运行的生命周期是这样的任务接收调度器拿到一个用户目标比如把上周发货失败的订单整理成报告并发送给客服组。这个目标会被转换成一个任务对象写入队列。上下文装载任务被某个Worker领取后运行引擎先做上下文构建。系统指令、任务元信息、长期记忆里检索出的相关历史一起被装进ContextManager。循环决策进入观察-思考-行动状态机。Agent读工具返回结果让模型决定下一步动作执行动作然后把结果写回上下文。每轮结束后检查终止条件。检查点落盘在关键动作前后和每轮末尾运行引擎把当前状态序列化到持久化存储。这不是可选配置在23.3里是强制执行。资源核算与回收Token消耗、API调用次数、单步耗时、总耗时每时每刻都被ResourceBudget记录。任务结束或失败后这些数据进入执行报告用于复盘和计费。我见过不少人把注意力全放在模型选型和Prompt上结果一跑线上就四处漏水。真正决定Agent能不能用的不是它聪明不聪明而是这个流程里的每一环在崩了之后能不能接回来。23.3版本做的最重要的事就是把这五步变成一条可观测、可拦截、可恢复的流水线而不是一段只能跑通的脚本。1.2 23.3版本的核心变化从跑完算数到步步留痕早期版本的核心循环其实就是while agent.is_active(): next_action agent.plan(context) result execute(next_action) context.append(result)写Demo没问题但生产环境里崩溃、超时、限流、上下文溢出是常态这种写法一崩全丢。23.3版本把隐藏的运行时状态显性化成一个对象我习惯叫它ExecutionRecord。每条记录里有任务ID与执行阶段、当前状态机的步骤编号、最新检查点ID、上下文版本指纹、资源预算使用情况。后面所有机制包括检查点、恢复、循环控制、资源管控都是围绕ExecutionRecord展开的。可以把ExecutionRecord想象成飞机上的黑匣子外加仪表盘既记录每一步又实时显示油量和高度。Agent可以出问题但有了这个对象你能知道在哪一步出的问题、当时带了多少上下文、花掉了多少预算而不是对着日志猜。如果你正在从零搭Agent框架我的建议是第一步不要急着写模型调用封装先定义ExecutionRecord这个数据结构。它决定了你的系统能做多深的多层级回溯。当时我们为了偷懒先跑通再补结果就是开头那次凌晨事故。后来重写23.3就是把数据结构放到了第一优先级。2. 上下文管理Agent能记住什么取决于你怎么喂工作记忆先讲一个我见过很多次的错误把上下文当聊天记录处理每个历史回合都往messages里塞。窗口没爆的时候运行良好一旦工具调用链变长上下文很快撑满然后模型开始失忆把几轮前的工具结果忘得干干净净。上下文管理不是简单的消息列表维护它是一套预算、分层、压缩、指纹校验的组合策略。2.1 上下文窗口与Token预算不是把聊天记录全塞进去上下文窗口是一个资源池但输入和输出是共用的。以128K窗口为例我通常只允许输入最多占75%也就是96K剩下的32K必须空着留给接下来工具可能返回的新数据以及模型这轮要生成的输出空间。如果输入塞到120K模型输出会因为超限被截断得到的答复是不完整的。对Agent来说这往往不是少一句话而是整个任务断在半途。所以在ContextManager里我们做了两层检查。第一层是硬性预算任务定义里设置max_input_tokens防止一个步骤把预算吃光第二层是软性预判在调用模型之前预估当前消息数组的Token数如果接近上限就先把上下文压缩一轮再调用而不是直接报错。class ContextManager: def __init__(self, window_size: int, input_budget_ratio: float 0.75): self.window_size window_size self.max_input_tokens int(window_size * input_budget_ratio) self.system_instruction SYSTEM_INSTRUCTION self.working_memory [] self.history [] def prepare_messages(self) - list: msgs [{role: system, content: self.system_instruction}] msgs self.history msgs.append({role: user, content: self._render_working_memory()}) while estimate_tokens(msgs) self.max_input_tokens: if len(self.history) 0: self.history.pop(0) else: break if estimate_tokens(msgs) self.max_input_tokens: self.history compress_history(self.history) return msgs这里的estimate_tokens在生产环境里不要真的每轮跑完整tokenizer太慢。用一个按字符数和语言加权估算的快速函数就够误差控制在10%以内即可。2.2 记忆分层系统指令、工作记忆、长期记忆上下文不是一条messages数组它至少应该分成三层。系统指令层放角色设定、任务边界、工具使用规范优先级最高不允许被裁掉。工作记忆层放当前任务的关键信息比如任务ID、当前目标、已经确认的中间结论、下一步计划每轮由引擎动态渲染放进一条user消息里。长期记忆层放过去任务的结论、知识库检索片段、用户长期偏好按需加载不全量塞入。为什么必须分层模型注意力不是均匀的越靠后的内容越容易被记住但越靠后被塞入的无关内容也越会干扰决策。系统指令放最前面保持稳定工作记忆放最后因为它是模型马上要用的长期记忆穿插中间作为参考。这个顺序不是随便定的是我们在多轮实验后观察到的实际效果工作记忆放在最后时Agent对目标的保持明显更稳。2.3 超长上下文的压缩策略上下文还是会超长这时就要看底层策略。我常用的有三种滑窗截断会丢最久远的历史简单粗暴但会丢掉工具结果的依赖关系比如第3轮查询到的订单号在第10轮还要用丢了整个规划就断了摘要压缩是对一段历史做摘要替换能相对保留依赖关系但摘要会损失细节而且生成摘要本身也要消耗Token和时间向量检索把历史写入外部向量库按相关性取回适合跨会话长期记忆但因为它取的是Top-K可能漏掉当时看起来不相关后来才关键的细节。23.3版本的实际做法是组合使用对话早期历史优先做摘要压缩保留结论和关键参数同一次任务里最近的N轮保留原文用于滑窗长期记忆统一放向量库按需检索。另外有个容易被忽略的点每次压缩后要把原始数据存到检查点里这样恢复任务时如果需要细节还能去检查点翻原文而不是彻底丢干净。2.4 上下文指纹恢复时判断上下文是否还能用这一节很关键但很容易被忽略。上下文快照在检查点里被存下来了可恢复任务时系统指令或任务参数可能已经改了。如果直接照着旧快照继续跑模型用的规则是上一版结果自然不对。所以我在ContextManager里维护了一个指纹对系统指令、任务目标、工具定义列表做哈希。恢复时先比对指纹不一致就重新构建工作记忆只沿用历史消息而不是整份上下文原样恢复。def build_fingerprint(system_instruction, task_meta, tools) - str: raw json.dumps({ system: system_instruction, task: task_meta, tools: tools }, sort_keysTrue) return hashlib.sha256(raw.encode()).hexdigest()[:16]这个指纹要写进每个检查点。恢复逻辑里指纹不一致时宁可多花一轮重建上下文也不能带着旧规则硬跑否则后续所有决策都建立在错误的前提上。3. 检查点与任务恢复Agent崩溃后的续命靠每一步留痕回到开头那次凌晨事故如果当时我们有检查点机制崩溃之后用户是不需要重新发起任务的。任务恢复不是说把Agent重跑一遍而是从它死掉的那一步继续往下走这需要检查点设计得足够细。3.1 检查点里到底存什么很多人以为检查点就是存messages数组。我一开始也这么想后来发现远远不够。一个完整的检查点应该包含任务头包括任务ID、检查点ID、时间戳、Agent版本号执行位置就是当前处于状态机的哪一个阶段已经完成了哪些工具调用上下文快照包括当前messages的压缩版、工作记忆、历史摘要、上下文指纹中间结果缓存也就是已经成功的工具调用返回值这部分是幂等恢复的关键资源计数包括已消耗的Token、调用次数、剩余预算还有下一步意图比如模型最近一次输出的行动计划如果动作已经写入计划但还没执行恢复时可以直接执行。这个结构决定了恢复能做到多细。只存消息最多让你接着聊但状态、预算、中间结果全丢了等于还是半重跑。3.2 写入策略每轮都写 vs 关键节点写全量检查点很贵尤其是上下文长的时候。我们试过每轮都全量写任务跑100轮就写100次大快照IO明显拖慢任务。后来改成两级策略。轻量检查点每轮循环结束写一次只记录执行位置、步数、资源计数、最后操作的指纹体积小写入快。重量检查点在调用外部工具之前和拿到工具结果之后各写一次包含完整上下文快照和中间结果缓存。因为工具调用是副作用集中发生的地方这个节点最需要被恢复。此外任务开始和完成时各写一个归档点。任务完成后归档点数据直接进入执行报告后续复盘可以直接翻。def run_checkpointed_agent(record): for step in state_machine: write_light_checkpoint(record, step) if step.has_tool_call: write_heavy_checkpoint(record, before_tool) result execute_tool(step.tool_call, idempotency_idstep.invocation_id) if step.has_tool_call: write_heavy_checkpoint(record, after_tool, result)这里有个性能经验值重量检查点占两个任务总时长的比例别超过5%。超过就说明快照体积太大了需要把上下文快照改成摘要加引用而不是完整复制。3.3 崩溃恢复的任务状态机状态机比想象中要细。我们在23.3里给每个任务维护了这些状态状态含义pending已入队未开始running正在执行paused因资源不足或人工干预暂停completed正常交付failed无法恢复的失败崩溃发生时任务停留在running。重启后恢复器的逻辑是读取最新检查点把状态置为recovering然后根据检查点里的执行位置重新进入循环。已经标记completed的步骤直接跳过failed的步骤重置后重跑。每恢复一步先写recovering状态再写步骤状态防止再次崩溃陷入重复恢复。3.4 恢复时要避开的坑第一个坑是上下文过期。检查点里存的可能是三个小时前的订单状态外部系统早就变了。恢复时对只读查询类步骤建议先重跑刷新再往下走。我们为此在工具定义里加了refreshable标记。第二个坑是重复执行副作用。恢复后重跑某一步如果这一步是给用户发邮件或者扣减库存就会造成灾难。解决办法是给每个工具调用分配一个invocation_id调用前先查执行记录的中间结果缓存命中就直接返回缓存结果。幂等键必须由任务ID加步骤号加动作名拼接生成并且持久化在检查点里不能是进程内存里的自增ID否则进程重启后去重就失效了。第三个坑是检查点写一半损坏。写入时先写临时文件再原子rename绝对不能直接覆盖主文件。恢复时发现检查点损坏要能自动退到上一个可用检查点而不是直接抛错。4. 循环执行与资源管控给Agent装上油门、刹车和仪表盘Agent的价值在于自主但自主不能无边界。循环执行是动力系统资源管控是刹车系统两者必须同时存在缺一个另一个就会失控。4.1 Agent循环的天然形态观察-思考-行动大多数Agent框架的循环都是三个状态observe把工具结果、环境状态、系统消息收集齐think让模型根据当前上下文决定下一步动作act执行一个工具调用或直接输出。我把这个循环看成一个状态机而不只是while循环是因为它必须能在任意一个状态被打断。资源不够时能暂停超时后能恢复人工可以随时介入。状态机天然能把这种打断变成合法事件而不是异常。4.2 循环终止条件不能只靠模型自觉让Agent自己判断任务完成这种说法太空了。模型可以宣称它完成了但真实交付需要一个可验证的终止条件。我们在LoopController里做了四层检查。第一层是目标判定工作记忆中维护goal_met字段只有满足任务定义里的交付标准才允许为true。第二层是步数上限每个任务创建时会给max_steps默认30步超了强制终止并归档。第三层是无进展检测连续N轮生成的行动计划没有实质变化或者输出内容相似度超过阈值判定为空转强制终止。第四层是人工接管提供暂停和停止接口。class LoopController: def should_stop(self, record) - bool: if record.goal_met: return True if record.step_count record.max_steps: return True if record.no_progress_count 3: return True if self.human_interrupt_raised(record.task_id): return True return False无进展检测的实际实现是记录每轮think的action_signature也就是工具名加关键参数哈希。如果连续3轮signature完全一样且工具返回没有新信息就判定空转。这个检测在长任务里尤其重要因为模型经常会在同一组局部信息上反复绕圈。4.3 资源管控的四张配额卡资源管控我习惯拆成四张卡。第一张Token卡管总Token预算和分阶段预算比如整个任务上限500K Token用到80%就进入降级模式。第二张调用卡管外部API的调用次数和QPS防止一个Agent循环把订单系统打到限流。第三张并发卡管同时运行的任务数和单个Agent内部的工具并行度。第四张时间卡管任务总挂钟时间和单步超时时间单步超时比总超时更容易被抓住排查。每张卡都要有独立的超额处理策略。Token快用完可以压缩继续调用次数快用完就停掉非核心步骤并发不够就排队时间超了就冻结任务隔天再恢复。它们不是都用直接杀掉这种粗暴手段。4.4 超额之后的优雅降级不是硬杀而是缩小目标继续做早期的实现是配额用完了任务直接failed用户损失很大。后来我们改成降级链路。第一步压缩上下文把历史摘要再压缩释放Token空间比如把三次工具调用的详细结果合并成一行结论。第二步缩小目标把当前任务拆成子目标先把最重要的交付做掉次要的标记为spillover等配额恢复再跑。第三步切换模型低风险步骤切换到小模型省Token注意切模型后指纹会变要写在检查点里。第四步暂停以上都不够就暂停任务保存检查点等资源池有余额时恢复。降级不是失败是让任务在受控范围内以更低的成本继续。这也是我坚持把资源管控和循环执行放在一起讲的原因两者分别管能跑多久和跑到何时停配合起来才是一套完整的执行策略。5. 生产环境里最容易翻车的细节与23.3版实测经验把机制写完只是第一步真实跑生产之后会遇到一堆文档里不会写的问题。这一章聊的是23.3版本跑线上任务以来攒下的实测经验。5.1 检查点写太频繁会拖垮性能最开始的版本里我们追求每步都全量持久化结果任务越跑越慢最后发现IO成了瓶颈。后来的策略是轻量检查点每轮都写重量检查点只在工具调用前后和任务结束写。如果用的是远端存储建议把轻量检查点写到本地缓存批量异步上传否则网络延迟会打进任务延迟里。另一个优化是快照体积。重量检查点里不需要存完整的历史原文存摘要加原始消息的引用地址就够了。需要原文时再按需加载这个改动让重量检查点体积直接缩小到原来的三分之一。5.2 恢复任务时上下文过期比崩溃本身更隐蔽上下文过期问题在测试环境很难暴露因为测试数据不会变。生产里恢复一个三小时前的检查点再去查订单状态可能已经改价、取消、甚至退款了。所以我们给每个工具定义里加了一个refetch策略工具类型恢复策略只读查询恢复时重新执行更新上下文副作用写入用invocation_id去重不重复执行依赖外部状态的操作先做前置校验再执行恢复逻辑不是死板地从断点续跑而是先做一轮环境同步把快照里可能过期的外部状态刷新一遍再决定从哪一步继续。这个同步步骤在23.3里叫resync是任务恢复的必经环节。5.3 幂等性恢复重跑不能让副作用翻倍幂等设计里最容易被忽略的是幂等键要随任务走而不是随进程走。如果幂等键是内存里的自增ID恢复后进程重启ID从0开始同一个操作就会变成两个不同ID去重失效。幂等键必须由任务ID加步骤号加动作名拼接生成并且持久化在检查点的中间结果缓存里。恢复时只要中间结果缓存里有对应invocation_id的成功记录就直接返回缓存结果不真正调用外部系统。有些操作天然做不到幂等比如追加一条备注和发送一次通知。这种情况要么改成条件写入先查后写要么在调用前后都打重量级检查点并申请人工确认再重放。不要指望模型自己能识别是不是重复操作模型对它自己刚说过的话是没有记忆的。5.4 资源管控不能一刀切按任务等级差分把所有任务放在同一个资源池里结果就是几十个批量任务把Token和调用次数耗尽重要的实时任务反而被卡住。23.3里我们按任务等级分了资源池任务等级Token预算调用/分钟并发优先级核心任务高高低插队普通任务中中中正常批量任务低低高排队批量任务做长跑但给最少资源可以慢慢跑核心任务数量少但必须保证随时有预算。这个差分策略上线之后线上超时投诉明显降了下来。5.5 可观测性把运行机制暴露成事件流最后一条经验运行机制不应该是黑盒。每一个Agent任务在执行过程中都会生成结构化事件流事件包括上下文构建完成、检查点写入、步骤执行、工具调用完成、预算告警、降级触发、恢复完成。这些事件统一打到日志中心用任务ID做关联。排查问题时只要搜任务ID就能看到它每一步在哪里、花了多少Token、为什么被降级。这一条在早期版本完全没做导致每次出问题都在翻print和猜状态。做了事件流之后大部分线上问题能在半小时内定位到具体步骤而不是从崩溃堆栈里逆向推理。最后再分享一个小技巧在每个重量检查点里额外存一个summary字段内容是当前任务已经完成了什么、下一步打算做什么的模型摘要。恢复任务时先用这个summary快速恢复工作记忆再按需加载详细历史。这个小改动让恢复后的首个思考轮质量明显提升模型不用从一份巨大的旧上下文里翻找线索直接顺着summary往下规划就行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑