
我最早写 Agent 的时候脑子里根本没有架构这两个字。当时接到一个客服问答机器人需求很简单用户问订单状态机器人查一下后台再回复。我写了大概一千行代码够用了后面需求一多功能往上叠一千行变三千行三千行变五千行改一处逻辑就要在好几个回调函数里来回跳。最崩溃的一次用户连续追问了十几轮上下文里塞满了历史对话模型开始答非所问日志里只留下一句调用失败我连是哪一步出的错都不知道。后来我把这套东西全部推倒重来参考了我之前在某团队那边沿用过的一套内部架构方案代号叫 Hermes。它不是某个开源框架而是一套专门解决Agent 怎么搭才能稳定、可维护、可观测的设计思路。这篇就把 Hermes 这套架构掰开揉碎讲清楚核心分层是什么、工具和记忆怎么管、任务怎么编排、生产环境会踩哪些坑。适合已经会调大模型接口、想从写脚本进阶到搭系统的开发者也适合正在重构单体 Agent 代码的人参考。1. 单体 Agent 失控的三个现场为什么非得有套架构不可1.1 现场一上下文被对话历史撑爆模型开始失忆很多 Agent 的雏形都是一个无限循环把系统提示词、历史对话、用户输入拼在一起扔给大模型拿到回复再追加到历史里继续下一次请求。这东西跑通 Demo 很容易跑生产就出事。我那个客服机器人就是这么翻车的。用户上来问我的订单为什么还没发货Agent 调了订单查询接口告诉用户您的订单正在拣货中然后用户接着问那大概什么时候能到能不能改地址如果不能改那我拒收要不要手续费一轮接一轮下去每次请求都要把前面所有对话重新发一遍。上下文长度越滚越长模型的注意力被稀释开始忘记最早的订单编号甚至有一次把用户改地址的诉求记成了退货诉求回复牛头不对马嘴。这时候做不做滑动窗口都尴尬。窗口设长了token 成本飙升而且旧信息可能互相矛盾窗口设短了更早的关键信息直接被丢掉。单体代码里的上下文是一个全局变量谁都能往里写谁都不知道里面现在有什么。1.2 现场二工具调用失败后整条链路直接崩溃第二个崩溃现场更隐蔽。Agent 在推理过程中会决定调用某个工具比如查询订单状态返回一个 JSON。正常的路径很顺利但一旦接口超时、字段为空、或者返回格式跟预期不一样单体代码往往就慌了。我当时写的是工具函数抛异常外层 catch 住然后往对话里塞一句抱歉系统异常。听起来不算致命实际上用户那边看到的是一次完整任务的失败——Agent 既没有重试也没有换一个信息源更不会去反思刚才是不是参数传错了。有一次第三方物流接口短暂抖动连续四五个用户来问物流机器人全军覆没全部回复系统异常。这就是单体 Agent 的典型问题决策、执行、记忆、重试、日志全混在一起没有任何一层能独立负责一件事。工具失败了没有降级方案执行出错了没有复盘入口想在某一步插入一个人工审核得先把整个调用链拆开。1.3 现场三出了问题你根本不知道是哪一个环节的锅单体 Agent 还有一个致命问题不可观测。日志里只有最终那句调用失败没有步骤 ID没有那一次调用大模型时的完整输入没有工具返回的中间结果。想复现一个问题你得把用户的原话粘进一个临时脚本里重新跑一遍还得希望大模型的输出是稳定的——它通常不稳定。我后来复盘时才意识到这些问题不是代码写得烂而是结构上就没有把 Agent 系统当作一个需要分层设计的软件来对待。Agent 本质上是一个大模型决策 工具执行 记忆存取 状态流转的组合系统这四个部分如果不物理隔离必然互相污染。这也是 Hermes 架构想解决的核心问题。2. Hermes 骨架拆解控制流与数据流各自为政2.1 五层结构总览Hermes 的总体思路是把 Agent 系统拆成五个职责明确的层次层与层之间通过消息通信而不是通过直接函数调用纠缠在一起。下面是这五层最简版本的分工层级职责关键组件入口层接收用户请求做格式校验与鉴权消息网关、会话绑定编排层拆解任务、路由到对应子 Agent、汇总结果规划器、分发器、聚合器执行层调用具体工具或子任务拿到结构化结果工具注册表、任务执行器记忆层读写短期上下文、工作记忆、长期档案上下文管理器、记忆存储可观测层记录每个步骤的输入输出、耗时、成本追踪器、日志总线这五层不是每个项目都必须物理分成五个服务而是要求在代码层面划清楚边界。你完全可以写在一个进程里但绝不能把工具调用逻辑和记忆读写逻辑放在同一个函数里互相乱摸。2.2 控制流与数据流分离每条消息都带身份证Hermes 里最核心的一个设计是把谁在控制和数据往哪走拆开。控制流由编排层负责它决定下一步该做什么数据流通过消息信封Envelope在层与层之间传递每条消息带三个固定字段任务 IDtask_id、负载payload、状态status。举个例子入口层收到用户消息后不是直接把字符串塞给编排层而是封装成一个 Envelopedataclass class Message: task_id: str sender: str receiver: str payload: dict status: str # created/running/succeeded/failed这个设计的价值在于可回放。任何一步出了问题你都能根据 task_id 把这条消息从创建到结束的完整流转找出来。单体脚本里常见的一个全局变量被到处改在这里被彻底禁止——子任务之间不共享任何全局状态需要什么数据都通过消息的 payload 显式传递。2.3 一次请求在 Hermes 里的完整流转我用一个真实场景串一遍用户发来消息我的订单为什么还没发货。入口层先做基础校验确认这个用户存在然后创建一条 task_id 为 T-10086 的 Envelopepayload 里放用户 ID 和原始文本发给编排层。编排层的规划器把任务拆分了一下先查订单再查物流最后生成回复形成一个三步计划。执行层依次拿到三步计划它调用工具注册表里的查订单函数返回订单号再调用查物流函数返回物流状态。记忆层在处理过程中会把工单号暂存到工作记忆里方便后续步骤引用同时把这次对话的摘要写入长期档案。最后聚合器拿到所有中间结果让大模型生成一段人类友好的回复回复作为最终 payload 原路返回给入口层。整个过程里任何一层的代码想越权访问另一层的内部数据都做不到因为数据都在 Envelope 里而 Envelope 只通过消息总线流动。这就是 Hermes 稳定性的底层保障。3. 工具注册表给 Agent 的双手套上规范和刹车3.1 工具不是函数而是schema 授权 校验 回退的对象很多 Agent 项目的工具层就是一堆 if-else 判断关键词命中天气就调天气 API命中日历就调日历 API。这种写法的天花板很低。一旦工具数量超过十个关键词匹配就会开始打架而且 Agent 真正想要表达的意思往往没有那么直白。Hermes 里每个工具都是一个注册对象。工具不只是函数本身它还携带元信息名称、描述、入参 JSON Schema、权限级别、超时时间、失败降级策略。注册表统一管理这些对象提供给编排层做规划也提供给执行层做调用。我给出一个最小实现的样子tool_registry {} def register_tool(name, schema, permission, timeout10): def decorator(fn): tool_registry[name] { name: name, schema: schema, # JSON Schema fn: fn, permission: permission, # public/internal/confidential timeout: timeout, } return fn return decorator register_tool( query_order, { type: object, properties: { order_id: {type: string} }, required: [order_id] }, permissionpublic, ) def query_order(order_id: str): return {order_id: order_id, status: shipping, eta: 2025-02-10}为什么一定要带 Schema因为大模型在做函数调用时最怕的就是参数猜测。你不给它明确的参数结构它就会自由发挥把orderId传成order_id、把日期传成明天、把必填字段漏掉。有了 Schema执行层可以在调用前做一次机械校验不合法直接打回让规划层重新组织而不是傻傻地把坏参数发给下游系统。3.2 不要把全部工具都塞进提示词我在调优过程中发现一个很反直觉的点工具多了Agent 反而变笨。把几十个工具的说明全塞进系统提示词里token 很贵不说模型在选择工具时还会出现意图漂移——用户问订单它却调了优惠券查询因为工具描述之间有重叠。Hermes 的对策是按场景圈定工具集。入口层根据用户请求的类型先做一个粗粒度的路由把可调用工具锁定到一个小集合里。比如客服场景只暴露订单查询、物流查询、退款申请查询这三个销售场景只暴露商品查询、库存查询、价格修改。工具集合变小之后模型的选择准确率明显上升响应延迟也降下来了。这是一个非常值得复用的经验不是所有工具都有资格出现在每一次请求里。3.3 权限边界与失败降级让失败成为可处理的反馈工具注册表里还有一层容易被忽略的字段权限。不是所有工具都允许 Agent 自动调用。比如修改订单地址这种事我一开始让它自动执行结果 Agent 因为用户一句我想改地址就把地址改了连二次确认都没有。Hermes 的做法是分三类公开工具随便调内部工具经用户确认后再调涉密工具只能触发创建人工工单。当 Agent 需要调用高权限工具时编排层会插入一个人工确认节点把确认请求发给用户拿到确认后再继续。这个机制听起来简单但在单体代码里很难做因为你没有一个统一的编排层去拦截这次调用。失败降级也要配套。工具返回异常时注册表还能提供备用函数或者把一个结构化错误信息返回给编排层让它重新规划。关键不在于不失败而在于失败可以被下一轮决策消化。4. 记忆三分法短期上下文、工作记忆与长期档案各管各的4.1 三种记忆的定义与生命周期很多 Agent 对记忆的理解只有一个维度所有历史对话统一存起来需要时全拿出来。于是又回到第一节的问题上下文爆炸、信息互相污染、关键信息下沉到看不见的位置。Hermes 把记忆拆成三层各管各的生命周期记忆类型生命周期典型内容存储方式短期上下文一次请求内当前用户问题、最近几步的工具返回请求级变量结束后即销毁工作记忆整个任务周期任务拆解出的步骤清单、中间计算结果关联 task_id任务结束后归档长期档案跨会话周期用户偏好、历史订单、之前处理过的偏好数据库 / 向量存储异步写入这个三层的核心逻辑是每个数据都有明确的该活多久和该给谁看。短期上下文不是让你越攒越多而是提醒你用完就丢工作记忆是给编排层用的跟当前任务强相关长期档案才是真正跨会话沉淀的东西。4.2 短期上下文怎么管理保持轻量只放必要的输入我在实际使用中发现短期上下文最大的敌人是舍不得丢。开发者总觉得历史信息越多模型理解越充分实际上模型在长上下文里检索关键信息的效率非常不可靠。Hermes 的做法是每一步只携带当前步骤真正需要的输入而不是把所有历史都挂在身上。比如在客服工单场景中用户问完订单又问物流那查询物流这一步需要的输入仅仅是订单号和一个物流公司编码根本不需要把前面三分钟的完整对话都发一遍。短期上下文管理器会把请求能用的信息压缩成一个结构化对象发到执行层。等这一步完成这些临时数据立刻释放不占任何内存。4.3 长期档案的读写时机先写事实再写摘要长期档案最忌讳的是一股脑写对话原文。对话原文噪声太多写进去之后下次检索出来反而干扰新任务的判断。我在 Hermes 里习惯的做法是任务正常结束后执行一次归档操作。归档分两步第一步把这次任务中确定的硬事实用户 ID、订单号、问题类型、处理结果写入结构化存储第二步把非结构化内容用户吐槽的语气、特殊诉求压缩成一句话摘要写入向量存储备用。这样的好处是下次用户再进线Agent 可以直接通过长期档案查到这个用户上次因为物流延迟发过一次火上次补偿了 10 元优惠券从而在生成回复时自动调整语气和策略。长期记忆不是用来复述历史的而是用来辅助当前决策的。5. 编排层怎么选串行拆解、并行分发还是主从协作5.1 三种编排模式的本质区别编排层是 Hermes 里最灵活的部分因为它要处理的是Agent 的思维路径。同一个任务可以串行一步步拆也可以并行分发多个子 Agent还可以用主从模式让一个总 Agent 协调几个专家 Agent。三种模式各有适用场景模式适用场景优点风险串行拆解步骤之间有强依赖逻辑清晰易追踪慢且一步错步步错并行分发子任务互不依赖快吞吐高聚合结果可能冲突主从协作需要专业分工职责清晰可插拔总 Agent 容易成为瓶颈5.2 规划器怎么生成可执行的计划很多规划器的实现是让大模型把任务拆成一个 JSON 数组。但真跑起来你会发现模型生成的计划经常不落地。它可能拆出一个步骤调用一个不存在的工具也可能把两个可以合并的步骤拆成五步白白浪费 token。Hermes 的处理是规划器生成计划之后增加一个计划校验器。校验器遍历每个步骤检查三件事第一步工具是否存在于注册表第二步入参 Schema 是否能对齐第三步步骤之间的依赖关系是否成环。任何一项不合格就把它作为错误反馈重新提交给规划器最多重试两次。这个生成-校验-重试的循环把计划质量从看起来合理提升到了一定可执行。5.3 并行分发之后聚合时如何消解冲突并行模式最难的其实不是分发是聚合。子 Agent 各自跑完返回结果可能是互相矛盾的。比如一个销售场景需要同时统计本季度销售额和本季度退货率两个子 Agent 一个基于实时数据库、一个基于离线数仓数字对不上。这时候如果直接拼在一起生成报告能出现两个互相矛盾的数据。我的经验是给每个结果加数据源优先级元信息。聚合器先按主数据源的结果为准辅数据源只做趋势参考如果差异超过阈值就触发一次重新校验而不是直接取平均值。另外聚合器在合并结果时要保留每个子 Agent 的原始结论和它的推理摘要这样生成最终回复时总 Agent 可以判断哪条信息更可靠。6. 从零写一个 Hermes 最小雏形客服工单场景全流程6.1 主循环与核心数据结构理论讲得再多不如看一个能跑的最小实现。我以客服工单场景为例走一遍 Hermes 最核心的主循环。整个系统只有四个对象Message、Orchestrator、ToolRegistry、Memory。Orchestrator 是大脑它循环取出消息、规划、执行、观察结果直到任务完成。下面是最简主循环的伪代码框架你可以直接拿去改def run_agent(user_input, user_id): session Memory.new_session(user_id) envelope Message( task_iduuid4(), sendergateway, receiverorchestrator, payload{text: user_input, user_id: user_id}, statuscreated ) orchestrator Orchestrator(tool_registry, session) while envelope.status ! succeeded and orchestrator.step_count MAX_STEPS: if envelope.status created or running: plan orchestrator.plan(envelope) # 生成计划 validated orchestrator.validate(plan) # 校验计划 if not validated.ok: envelope.payload[error] validated.reason continue envelope orchestrator.execute_step(plan.next_step(), envelope) envelope orchestrator.observe(envelope) # 根据工具结果决定下一步 return orchestrator.final_reply()这个循环里最关键的是observe函数它观察工具执行后的结果判断是继续下一步、重新规划、还是直接结束。这个观察-决策的闭环就是 Agent 和普通脚本的本质区别。6.2 客服工单自动处理的一次完整运行我用一个具体输入演示运行路径。用户说你好我 1 月 20 号下的订单到现在都没发货能帮我催一下吗第一步入口层鉴权后把消息交给编排层。规划器通过长期档案发现该用户的历史偏好同时分析出用户意图是物流催促于是生成三步计划查订单详情查物流轨迹生成催单工单。第二步执行层调用query_order得到订单处于shipping状态物流公司显示异常滞留于是第二部工具query_logistics返回在分拨中心滞留 3 天。第三步规划器根据工具结果判断这不是普通咨询而是一个需要介入的投诉于是触发一个创建催单工单的私有工具并在内部记录处理人 ID。整个任务结束后记忆层把用户 1 月订单滞留 3 天用户表示不满写入长期档案。最终用户收到的回复是您的订单目前在分拨中心滞留我这边已经为您创建催单工单预计 24 小时内会有专员跟进。全程没有人工介入但每个步骤都能通过 trace 拉出来审计。6.3 验收标准正确性、成本、延迟、可复现性雏形跑通之后我用四个指标验收正确性看回复是否符合预期成本看单次任务的 token 消耗延迟看用户从发出消息到收到回复的时间可复现性看同一个输入在相同参数下能否得到结构一致的流程。这四个指标里可复现性最容易忽视。我强烈建议从一开始就给每一步记录输入和输出的哈希值因为大模型的输出有随机性不记录中间状态后续排查问题会让你怀疑人生。7. 生产环境里躲不开的五个坑超时、死循环、并发、token 与追踪7.1 死循环与最大步数限制Agent 系统最常见的故障就是死循环模型不断调用工具、不断拿到结果、结果无法让它决定下一步于是再调用一次。这个在单体代码里几乎是无解的因为循环条件隐藏在模型生成的内容里。Hermes 的解法很朴素但是一定要做给主循环一个最大步数硬上限。我在生产环境里设的是 15 步超过直接终止并把任务未完成 已执行的步骤列表返回给人工处理。千万别觉得 15 步很奢侈正常客服工单 4-6 步就能跑完超过上限的几乎都是出了问题。这个上限宁可设置得紧一些也不要没有。7.2 超时与重试策略工具调用超时是另一件必然发生的事。我建议在注册表里给每个工具单独配 timeout不要全局统一。查数据库的 5 秒调用大模型的 30 秒发短信的 3 秒各管各的。重试也要分情况。网络超时可以指数退避重试两次业务逻辑错误比如订单号不存在重试毫无意义应该走降级路径。我在代码里给工具的函数签名加了一个参数retryable和fallback。这样当一个工具不可用时编排层可以拿到一份降级结果而不是收到一坨异常堆栈。7.3 并发写共享状态的冲突并行分发子 Agent 时如果它们共享同一个内存变量会引发经典的竞态问题。两个子 Agent 同时往同一个 memory 对象写入后写的覆盖先写的数据就丢了。Hermes 的做法是每个子任务拥有独立的 memory 副本只有它自己的 Envelope 能读写需要协作的数据通过主 Agent 显式转发给对方。换句话说子 Agent 之间不直接通信所有通信都经由编排层中转。这个不共享任何可变状态的设计彻底回避了加锁问题实现简单排查也简单。7.4 token 成本失控很多 Agent 项目跑着跑着发现账单涨得离谱原因往往出在重复发送大段上下文。每一次循环都要把完整的历史重新发给模型成本呈指数上升。对策有两个一个是在步骤级做上下文裁剪只保留当前步骤真正需要的字段另一个是工具返回结果做截断比如日志查询最多返回 200 行多余内容提示已截断是否继续。我在生产里还加了成本观测面板按 task_id 统计每一步的 token 消耗任何一步超出预算是立刻告警而不是月底收到账单时才发现。7.5 可观测性每个步骤都要留痕最后这个坑可以说是整个生产环境的保命绳可观测性。Agent 系统跟传统 API 服务的最大区别是你无法预判它的执行路径——这一秒还在调订单接口下一秒可能去生成工单了。所以日志不能只记入参 出参必须记录完整的决策轨迹。我给每个任务维护一个 trace里面包含每一步的事件{ task_id: T-10086, step: 3, event: tool_call, tool: create_work_order, input_hash: a3f1..., output_summary: work_order_idW-888, token_cost: 1250, duration_ms: 320 }只要有了这套留痕线上出了任何问题你都可以从 trace 里看到模型是在哪一步开始跑偏的。比如用户说想改地址Agent 却发起了退款申请查出 trace 后你立刻能发现是规划器把意图分类错了从而针对性地修正提示词或路由逻辑。没有 trace 的 Agent 系统就像一个没有行车记录仪的车你只知道自己出了事故但永远不知道事故是怎么发生的。我在实际调优中最大的体会是Agent 系统的架构价值不在正常运行的时候体现而在出问题的时候才真正爆发。单体脚本跑得再好一崩就是连环崩Hermes 这种分层设计每一层都有自己的边界和兜底问题永远可以被隔离、被定位、被重试消费掉。如果你想从脚本进阶到系统我建议不要急着一次性重写全部代码先试试把你现有的 Agent 脚本加一个总控循环再把工具调用逻辑做一层注册封装一步步往 Hermes 的方向靠。等哪一天你需要排查线上问题时会感谢当初这个决定。