资讯动态

【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 5 篇】

发布时间:2026/8/13 9:53:00 来源:尧图企业网站定制
上下文压缩与错误恢复长对话不失控、故障不宕机导读Agent 跑得越久上下文越膨胀、故障越频繁。本文拆解 learn-hermes-agent 阶段 1 收官代码三层递进压缩把 50K tokens 压回可控范围四类错误路径让 Agent 从崩溃边缘自动恢复。读完你也能写出不失控、不宕机的单 Agent。你的 Agent 正在被两个问题杀死先看两个真实场景。场景一你让 Agent「读取 agents 目录下的所有文件」。它开始 read_file一个文件塞进上下文又一个……读到第 4 个文件上下文估算撞线 50000 tokens。压缩触发中间几十条 tool 输出被压成摘要。但摘要没写清楚「还剩哪些文件」模型只能重新规划甚至重复读已读文件。于是——压缩、重读、再压缩、再重读。死循环。场景二Agent 跑得好好的突然 API 返回 429。主循环没做任何错误处理直接崩。你盯着终端前面 20 分钟的工作白费了。或者更糟模型输出被截断finish_reason: lengthAgent 以为任务完成了实际只做了一半。这两个问题一个叫上下文膨胀一个叫故障崩溃。它们是生产级 Agent 绕不过去的两座山。今天拆解的s05_context_compression.py28731 字节和s06_error_recovery.py21177 字节就是专门翻这两座山的。这也是 Hermes 系列阶段 1 的收官——一个能压缩上下文、能从错误中恢复的单 Agent。上下文窗口不是无限的先看清敌人长什么样。你的 Agent 每轮对话messages 数组里堆着系统提示、用户请求、每轮工具调用的输入输出、模型回复。读大文件输出几万字跑长命令结果几百行多轮工具调用旧结果越积越多。三个后果模型注意力被淹没——重要信息淹没在旧输出里模型「忘」了最初的目标API 越来越贵——每轮都把所有历史发给模型token 费用线性上涨撞上限——最终触发上下文窗口上限任务中断s05 用三个常量定义压缩策略COMPRESSION_THRESHOLD50000# 估算 token 超过这个阈值就触发压缩TAIL_TOKEN_BUDGET20000# 尾部预算从后往前累加直到撞线COMPRESSION_MIN_SHRINK0.9# 压缩后必须降到原 token 的 90% 以下阈值 50K尾部保留 20K压缩后必须缩小到 90% 以下。这三个数字定义了「什么时候压、留多少、压到什么程度」。第 1 层裁剪旧工具输出不花钱最便宜的压缩是裁剪。prune_old_tool_results做的事很简单把旧的工具输出消息的 content 替换成占位符[Old tool output cleared]。注意一个关键细节不删除消息本身。为什么因为 assistant 的 tool_call 消息和 tool 的 response 消息必须配对——靠tool_call_id关联。你删了 tool 消息assistant 那边还引用着这个 IDAPI 直接报错。所以裁剪只替换 content保留消息结构和 ID。token 省了配对关系还在。这一层不调用任何 LLM不花钱纯字符串操作。能压多少压多少压不动再上第二层。第 2 层保护头尾找边界裁剪不够就得动真格的了。find_boundaries(protect_first, tail_token_budget)的策略是头部前 N 条消息不动通常是系统提示和最初的用户请求尾部从后往前累加 token直到撞上 20000 的预算。头部是任务的「初始条件」动了它模型就不知道自己在干嘛。尾部是「最近记忆」模型刚做完的事、刚拿到的结果都在这里动了它就断片。中间那段就是要被压缩的「历史包袱」。但切分有个坑切点可能落在 tool 消息中间。想象一下一条 assistant 消息说「我要调用 read_file」对应的 tool 消息返回了文件内容。如果你把切点放在这两条消息之间就制造了一个「孤儿 tool 消息」——assistant 的调用没有对应的返回或者 tool 的返回没有对应的调用。API 直接报配对错误。_align_to_assistant_boundary就是干这个的把切点吸附到最近的、非 tool 消息的 assistant 消息上。宁可多保留几条也不能切出孤儿。第 3 层辅助 LLM 摘要最贵但最有效裁剪和切分都是「暴力手段」——信息直接丢了。真正能保留信息的压缩是摘要。summarize_middle用结构化模板做摘要Goal目标Progress进展Key Decisions关键决策Files Modified改过的文件Next Steps下一步这个模板不是随便定的。它覆盖了「任务连续性」所需的全部信息维度目标是什么、做到哪了、决定了什么、动了哪些文件、接下来干嘛。更妙的是增量更新传入原始用户请求 上一份摘要生成新的摘要。不是从头总结全部历史而是在旧摘要基础上叠加新进展。信息熵不衰减——每次压缩都基于上次压缩的结果而不是把历史全部抹掉重来。还有一个反直觉的细节摘要消息的 role 必须是assistant。为什么如果 role 是user模型会把它理解成新的用户指令触发重新规划——Agent 可能抛开原任务开始执行「摘要」里的内容。如果 role 是system部分模型会忽略中段的 system 消息。所以 s05 给摘要加了个前缀[CONTEXT COMPACTION - system-generated summary of earlier turns, not a new instruction]明确告诉模型这是系统生成的摘要不是新指令。别当真。压缩卡死直接早退压缩不是万能的。有一种情况头部本身就太大——比如用户第一条消息就塞了 10 万字的文档。保护头部不动中间可压缩区太小压完还是超过 90% 的阈值。这就是CompressionStuckError的用武之地。主循环捕获这个异常后直接早退告诉用户「会话已无法压缩请新开 session」。为什么要早退而不是继续循环看看 issue #2 客户报告的真实问题「一直压缩、一直循环」。压缩不达标 → 再压 → 还不达标 → 再压……直到 MAX_ITERATIONS 才停。浪费大量 API 调用最后任务还是失败。压缩失败就承认失败让用户新开会话。这是工程上的务实选择。TaskState摘要救不了的用不可压缩区来救摘要是有损的。这是本质缺陷。回到开头的场景Agent 读 agents 目录下的所有文件读到第 4 个撞阈值中间几十条 tool 输出被压成摘要。摘要没写「还剩哪些文件」模型只能重新规划甚至重复读已读文件。摘要救不了任务连续性。因为它是有损的而任务状态需要无损。s05 的答案是TaskState——一个不可压缩区dataclassclassTaskState:goal:strtodos:list[dict]field(default_factorylist)# 每个 todo: {id, subject, status: pending|in_progress|completed}这个对象不进 messages 数组压缩算法根本看不见它永远不会被裁剪或摘要。每轮 API 调用前task_state.render()把它拼到 system prompt 末尾格式是# Task State (live, never compressed) ## Goal 读取 agents 下所有文件 ## TODO [x] 读取 config.py [~] 读取 s05_context_compression.py [ ] 读取 s06_error_recovery.py三个工具维护这个状态task_set_goal(goal)设目标、todo_write(items)写清单、todo_update(id, status)更新状态。压缩随便压TaskState 永远无损。模型每轮都能看到「还剩哪些文件没读」不会重复劳动。注意区分TaskState 和后面 s07 的 memory 是两回事。TaskState 是单次任务内的活跃状态会话结束就丢memory 是跨会话的持久化记忆用户偏好、历史教训。一个管当下一个管长期。上下文救回来了故障呢压缩解决了「对话越长越贵」的问题。但 Agent 跑在生产环境还要面对另一个残酷现实API 会出错。模型输出截断、上下文太长 400、网络超时、限流 429、API key 过期 401、模型不存在 404……多提供商场景下200 模型错误种类更多措辞还不一样。没有恢复机制主循环在第一个错误上就崩了。s06 的答案是先把错误分类再按类处理。四类错误路径分类器把状态码翻译成决策classify_error返回一个结构化决策 dict{reason:rate_limit|context_overflow|server_error|auth|model_not_found|unknown,retryable:bool,should_compress:bool,should_fallback:bool}映射关系很清晰429→ rate_limit可重试400 context→ context_overflow该压缩500/502/503→ server_error可重试401/403→ auth该故障转移404→ model_not_found该故障转移其他→ unknown全 False为什么要把错误分类因为不同的错误需要完全不同的处理策略。限流和服务器错误是暂时的重试就好上下文太长是可修复的压缩再试认证失败和模型不存在是不可恢复的重试一万次也没用得换路。输出截断续写模型生成到一半token 用完了finish_reason: length。这不是错误是「话没说完」。s06 的处理发一条续写消息Please continue from where you left off.让模型接着写。最多续写 3 次。但有个陷阱thinking-budget 检测。有些模型把 token 全花在「思考」上了输出几乎为空finish_reason 也是 length。这时候续写没有意义——模型不是在生成内容是在空转。s06 会检测这种情况直接报错不浪费续写次数。退避重试加抖动别撞车429 限流、500 服务器错误这类临时故障的处理方式是退避重试。sleep时间按指数退避 随机抖动delaymin(base_delay*(2**(attempt-1)),max_delay)# 5s→10s→20s...jitterrandom.uniform(0,delay*0.5)returndelayjitter指数退避给服务器喘息时间——第一次等 5 秒第二次 10 秒第三次 20 秒封顶。抖动是干嘛的想象你有 10 个 Gateway 会话同时收到 429如果大家都按 5s→10s→20s 的节奏重试第 3 次重试时大家又同时撞上去——这就是thundering herd惊群效应。加随机抖动让每个会话的重试时间错开避免二次撞车。故障转移换一组配置而已401/403/404 这类不可恢复错误重试没用得换路。switch_to_fallback做的事如果配置了FALLBACK_MODEL就换 base_url 和 api_key重建 OpenAI 客户端。因为 Hermes 走的是 OpenAI 兼容接口故障转移本质上只是换一组配置——换 provider、换模型、换 key。API 调用代码一行不用改。更妙的是主模型恢复下一轮run_conversation会尝试切回主模型。故障转移是临时的不是永久的。主模型缓过来了就切回去别赖在备用模型上不走。主循环现在同时维护三件事把压缩和错误恢复接入主循环后Hermes 的 Agent 主循环现在同时干三件事任务推进——正常的工具调用、模型推理上下文预算——每轮发请求前检查 token超了就压缩错误恢复——try/except 捕获异常分类、决策、执行恢复路径每条路径有自己的重试预算续写最多 3 次、退避最多 MAX_RETRIES 次、压缩有 COMPRESSION_MIN_SHRINK 阈值。不会无限重试不会死循环。这是阶段 1 的收官。一个单 Agent 现在具备了任务执行Loop、工具调用、持久化、提示词管理、上下文压缩、错误恢复。可以上生产了。阶段 2 开始补智能层记忆MEMORY.md、技能SKILL.md、安全、委派、配置。避坑指南初学者最容易犯的 11 个错把 s05 和 s06 的坑合并精选给你一份避坑清单上下文压缩s05裁剪时删除 tool 消息——破坏了 assistant↔tool 配对API 报错。只替换 content不删消息切点落在 tool 消息中间——制造孤儿 tool 消息。用_align_to_assistant_boundary吸附切点摘要 role 用 user——模型把摘要当新指令触发重新规划。必须用 assistant 前缀声明压缩失败还硬压——头部太大压不动死循环。抛CompressionStuckError早退TaskState 放 messages 里——被压缩算法看见就白设计了。放独立数据结构不进 messagestoken 估算太精确——用字符数 // 4 的粗略估算就够了中文偏高估偏保守够用错误恢复s06把所有错误当一种——429 和 401 的处理策略完全不同。先分类再决策没有重试预算——无限重试等于死循环。每条路径设上限续写提示太模糊——「继续」不如「Please continue from where you left off」明确退避不加抖动——多会话同时重试撞车。加随机抖动故障转移后不恢复主模型——永远留在备用模型上。每轮尝试切回阶段 1 收官下一篇进入智能层上下文压缩和错误恢复是 Agent 从「能跑」到「能生产」的分水岭。压缩解决的是经济问题——token 是钱上下文是稀缺资源。错误恢复解决的是可靠性问题——Agent 不能一碰就碎。两件事合在一起你的 Agent 才能长时间稳定运行。这是生产级 Agent 的底线。下一篇第 6 篇进入阶段 2 的第一块拼图记忆与技能。MEMORY.md 让 Agent 跨会话记住用户偏好SKILL.md 让 Agent 把常用操作固化成可复用技能。如果说本文解决的是「不失控、不宕机」下一篇解决的是「越用越聪明」。你的 Agent 现在能处理多长的对话有没有遇到过「一直压缩、一直循环」的坑评论区聊聊你的实战经历。参考文献Hermes Agent 教学仓库agents/s05_context_compression.py本文代码素材28731 字节真实可运行Hermes Agent 教学仓库agents/s06_error_recovery.py本文代码素材21177 字节真实可运行Hermes Agent 教学仓库docs/zh/s05-context-compression.md与docs/zh/s06-error-recovery.md三层压缩、TaskState、四类错误路径详解源码获取如需本系列全部源码请在以下链接克隆https://gitcode.com/ganxin7932508/learn-hermes-agent.gitHermes 架构原理剖析系列第 1 篇整体架构与设计哲学第 2 篇Agent 主循环与工具调用第 3 篇工具注册与动态调度第 4 篇持久化与提示词管理第 5 篇上下文压缩与错误恢复本文第 6 篇记忆与技能下一篇

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

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

免费获取报价