做LLM应用这一年多我拆过不少开源agent项目也亲眼看过团队从一个几百行的agent脚本长成一座没人敢动的屎山。如果你现在正要接触这类系统我建议先别急着看模型调优技巧先把架构摸清楚。今天这篇就是从“架构总览”的角度把Hermes Agent这个典型的模块化智能体框架摊开来讲重点拆它的子系统划分以及一条请求从进入到返回所走过的执行路径地图。适合正在搭agent脚手架、或者想找一个标准参考系来优化自己项目的开发者。内容里会反复出现三个关键词架构、子系统、执行路径。我尽量把它们落到具体场景里不讲空泛概念。1. 架构地图为什么比功能清单更重要1.1 单文件agent失控的真实场景先讲一个我自己的项目。早期给某个内部系统做消息机器人初始版本就是一个while循环里面依次做读新消息、拼接prompt、调模型、解析输出、执行动作、回发消息。听起来很完整对吧一开始确实很爽代码不到三百行。后来需求变成消息里要支持指定频道、要能读附件、要支持定时任务、要限制敏感操作、要记录每次执行日志。于是我在那个大循环里不断塞if分支最后代码到了两千多行每次加功能都要全量回归测试因为任何一个分支都可能影响其他分支的输入输出。这个场景我相信很多做过agent的人都熟悉。问题不在于循环写得不好而在于“改一行代码可能影响整条链路”的耦合结构。Hermes Agent这类框架给我的启发是它宁可多花一些前期成本把各个环节切分成独立子系统让每个子系统都有清晰边界再通过定义好的接口串联。这样单个子系统的变化不会像多米诺骨牌一样倒向全局。从更底层来看agent系统本身就是一种典型的分布式协作结构不同组件处理不同类型的任务组件之间只通过消息或事件进行通信。如果你把所有逻辑都塞进一个进程里本质上就是在用单体应用的方式实现分布式系统的需求那后面大概率会还债。拆分子系统并不只是为了代码好看而是为了把“哪个环节出问题”这件事从玄学变成可定位、可测试、可回滚的工程问题。1.2 三种拆解视角我这次选哪条在社区里聊agent架构经常听到三种切法。第一种按运行形态切。比如bot mode后台常驻机器人模式和CUAComputer Use Agent模拟人类操作桌面软件的模式。这种切法贴近产品因为你确实要面对多种入口但你如果直接按这个维度组织代码很容易做成“每多一种入口就多一套业务实现”的膨胀结构。第二种按功能域切。感知、规划、执行、记忆、反馈五层经典模型。这种切法贴近代码组织模块边界最稳定基本不受业务变化影响。它的缺点是刚开始理解成本高因为你要先抽象出“感知”“决策”这些概念而不是直接看到“发消息”“点按钮”这种具体动作。第三种按时序切。受理、规划、执行、验证、归档按一个任务的完整生命周期来划分。这种切法贴近调试和日志排查问题时特别方便因为你可以像看电影一样按时间轴重放整个任务。实际工程里这三种是叠着用的。我的建议是以功能域为主线因为它和底层代码的对应关系最清晰同时把时序视角用在执行路径分析上把运行形态视角放在运行时状态机里。这篇文章就沿用这个思路。你在后面会看到我讲子系统时按功能域展开讲执行路径时按时序展开bot mode和CUA的差异则放到状态机那一节单独处理。1.3 我对Hermes Agent架构风格的总体印象如果你去看它的整体结构会发现几个鲜明的设计倾向。第一一切入口都要经过统一管道。无论是消息、定时任务、还是桌面自动化操作最终都进入同一个核心处理流程而不是为每个入口各写一套逻辑。这是它能同时支持bot mode和CUA的重要前提。我看到有些项目在接入新入口时直接把新逻辑挂到核心流程的外围绕过了统一管道结果后续每次升级核心流程外围代码就要跟着碎一次。统一管道从短期看多了一层间接从长期看是给整个系统买了一层保险。第二模型调用被当作可替换的组件。模型本身并不直接散落在业务代码里而是被封装在决策引擎后面。这意味着你可以换模型、换本地推理服务、或加一层模型网关并不需要重写上层逻辑。这个设计对今天的大模型环境尤其重要因为模型迭代速度和API变动速度都很快如果你把具体模型的调用方式硬编码到业务逻辑里那模型一换版本你的agent行为就可能漂移。第三记忆不是某个工具的附属品而是独立子系统。这也是它能和Obsidian这类外部知识库良好集成的原因因为记忆接口是标准化的存储后端可插拔。你换掉记忆存储、增加新的知识来源都不需要改动决策引擎的代码。第四工具接口收敛得非常干净。所有工具都注册成统一协议agent主循环只跟协议打交道。这个设计极大降低了后续接新工具的成本也是我在第三部分讲“接入新子系统四步法”时的前置条件。2. 核心子系统详解每个部件到底负责什么2.1 感知接入层入口可以乱内部必须有序感知接入层在项目里可能是最不起眼的子系统但恰恰是它承担了“一切输入进到系统之前最后一道防火墙”的责任。为什么需要单独一层因为输入你控制不了。用户可能发来一句话、一个文件路径、一个屏幕坐标、一封邮件、一段HTML。如果每个下游模块都要自己处理这些原始格式就等于每个模块都变成了解析器。正确的做法是在最外层统一完成一件事把原始输入转成内部任务对象。我这里有个内部任务对象的简化结构你可以参考着设计字段说明示例原始内容未加工的输入“帮我打开项目文件夹并截图”来源类型文本、文件、屏幕、语音等screen触发模式用户主动、定时、事件event上下文引用关联的对话或任务IDtask-42时间戳到达时间2025-06-01 10:23:11这个结构的价值在于下游子系统不关心输入是哪个渠道来的只需要读这些字段。我见过很多人偷懒省略这层结果后续加一个语音输入时所有工具函数都得跟着改一遍。如果你在建自己的agent第一件事就是先把内部任务对象定义清楚越早越好。还有一点容易被忽略感知层不仅仅是解析输入它还要做输入校验和资源限制。比如对超长文本进行截断对非法格式进行拦截对未授权来源进行拒绝。这些安全相关的判断放在感知层是最合适的因为越靠近入口你能越早拦截风险避免它们进入复杂的内部流程。2.2 决策引擎LLM调度的职责与边界决策引擎是整个系统的中枢。很多人以为它就是把prompt发给模型其实真正的重头戏在模型调用之外。我归纳下来一个合格的决策引擎至少管四件事。上下文组装是第一步。系统指令、当前任务对象、相关记忆片段、历史消息这些内容要按一定策略拼装进上下文中。拼装的顺序和裁剪策略会直接影响模型效果。比如历史消息太长时要主动压缩记忆片段要按相关度排序后再放入。这不是模型能自己决定的事必须由决策引擎控制。我自己的经验是上下文组装的优先级顺序应该是“当前任务相关度最高的信息 执行工具返回结果 历史对话摘要 长期记忆片段”而不是简单地按时间顺序堆叠。路由选择是第二步。有些请求模型直接回答就行有些必须调用工具有些需要多轮规划。决策引擎需要在调用模型之后对输出做一次路由判断。常见做法是定义一套结构化的action框架让模型输出动作类型和参数代替自然语言聊天式回复。这样后续环节才能确定性地执行。我见过不少团队为了让模型“自由发挥”而放弃结构化输出结果就是后续代码要处理无数种变体最终变得不可维护。结果校验是第三步。模型输出的JSON可能字段缺失、类型不对、参数超范围决策引擎必须有校验和重试机制。不能假设模型每次输出都合规尤其在做函数调用时。我在项目里常用的一种办法是在校验失败时不直接重发整个prompt而是把“格式错误信息”追加到上下文末尾让模型只修正格式而不是重新生成一遍。这样既省token成功率也更高。成本与并发控制是第四步。模型不是免费无限循环的。一个清晰的设计应该在决策引擎里介入频率限制、超时管理、备用模型切换、token预算。我在项目里遇到过agent疯狂循环调用模型刷token的情况就是因为在决策引擎层面缺少“最大步数”限制。这类问题在功能demo阶段完全暴露不出来一上生产就会变成账单事故。这里我想特别强调“决策引擎”和“模型”的关系。模型是决策者但决策引擎是决策过程的管理者。就像交通警察和司机的关系司机负责开车但走哪条路、要不要掉头、出了事故怎么疏导都由交通警察兜底。很多agent跑不稳定的原因不是模型不够聪明而是交通警察缺位。2.3 工具执行层所有对外动作的唯一出口工具执行层在架构里要解决的问题是agent说了要做某件事怎么保证它真的能做到而且做得安全。这个层级的理想设计是所有工具通过统一接口注册生命周期由专门的管理器维护调用动作经过参数校验。举个例子一个查询订单接口的工具可以这样描述工具名称是query_order入参是字符串类型的order_id输出是一个结构化对象包含订单状态和更新时间。当模型输出“我要调query_order参数是abc123”时工具执行层就去执行真实调用然后把返回结果写回上下文。为什么强调统一接口因为模型的输出并不总是可靠的。如果你允许工具以自然语言形式出现在prompt里模型有概率会在后续回复中“脑补”一个工具结果而不是真的去调用。统一接口配合结果回填机制相当于强行堵住了这个漏洞只有工具执行层真正返回的内容才会进入上下文模型没有机会编造。隔离性也是这个子系统必须考虑的设计。我建议所有高危工具都跑在受限环境中比如子进程、沙箱容器或者至少在调用前做一层权限判断。这一点刚开始做agent的时候容易忽略但工具数量一旦变多出事的概率会快速上升。想象一下你给agent接了一个能删除文件的工具又没做任何权限限制一次误调用就可能酿成不可逆的后果。隔离不是锦上添花是安全底线。工具执行层还应该暴露一组清晰的元数据接口让决策引擎知道哪些工具可用、每个工具有什么参数、适用什么场景。这些元数据可以用于工具选择提示的自动生成而不是每次都在prompt里手工写死。我在接入新工具时通常会先写一份工具元数据描述再写真实实现代码顺序不能反因为后者只是前者的机器可读版本。2.4 记忆与上下文管理短期、中期、长期三层结构记忆子系统是agent能不能越用越聪明的关键。这里我先说一个常见误区把上下文窗口当作全部记忆。上下文窗口只是“工作台”东西摆在上面方便处理但工作台不能无限扩大下班了还得归档到仓库里。理想的记忆系统分三层。短期记忆对应当前任务上下文直接存在对话状态中任务结束或达到上限后清理。中期记忆对应业务会话历史适合放在结构化数据库里按会话ID读取主要用于多轮对话的连续感。长期记忆对应跨会话的知识沉淀包括用户偏好、领域知识、历史决策结果这些内容需要向量化索引支持语义检索或者借助外部知识库系统管理。Hermes Agent与Obsidian的集成方式我更倾向于把它看成“长期记忆的落地载体”agent把值得记录的内容写成Markdown笔记按日期或主题组织成可检索的知识库。这种方案和直接写数据库相比好处是人的可读性好、可以手工编辑修正、方便备份缺点是检索效率和自动索引需要自己维护。如果你同时接入了数据库和向量检索就要注意不同层之间的同步关系避免归档时数据冗余。长期记忆写入的基本策略是什么我建议不要在每轮对话都把内容冗余地存一遍而是设置“值得记录”的判定条件。比如任务完成、用户明确表达了偏好、发生了错误等关键节点再写入。这样可以避免记忆库被噪声淹没。判断条件放哪里答案是放在决策引擎或专门的记忆管理模块里而不是让模型自由发挥。记忆检索也有讲究。单纯的把整段记忆原样塞回上下文效果往往不好。更有效的做法是根据当前任务对象从长期记忆中检索出最相关的若干片段经过重排序后拼接如果检索结果为空就明确标注“无相关记忆”避免模型产生幻觉。这跟人一样回忆是有选择性的不是把所有人生经历都铺在眼前。2.5 运行时状态机bot mode 与 CUA 的统一模型运行时子系统是整个调度体系的底座。如果没有这一层你的agent就像一个没有主循环的程序每个模块做着自己的事却没有人负责把它们串起来。Hermes Agent的巧妙之处在于它没有为bot mode和CUA分别写两套运行流程而是用一套状态机来统一。bot mode运行在“待命 → 接收消息 → 处理 → 待命”的循环里CUA运行在“观察屏幕 → 决策 → 操作 → 观察结果”的循环里。这两个循环看起来不一样但本质上都可以抽象成同一个状态机上的不同触发路径。简单画一下状态机的关键状态。空闲状态是起点收到任何触发后进入解析状态解析完成进入决策状态决策可能产出三种结果直接生成回复、调用单个工具、拆解成子任务调用工具后进入观察状态拿到执行结果再回到决策所有分支完成后进入收尾状态负责归档记忆和清理临时资源。状态触发条件产出物失败处理空闲等待事件无无解析收到输入内部任务对象丢弃非法输入记录错误决策任务对象就绪执行计划或回复校验失败重试超限终止执行决策结果为工具调用工具执行结果重试或回退至重新规划观察工具执行完成结果状态超时重试收尾所有子任务完成归档记忆、返回结果部分失败标记这种设计有什么好处如果你要在系统里增加一种新的运行形态比如语音助手模式不需要推翻主流程只需要让新入口把触发转换成状态机能够识别的事件即可。我复制过这个思路给我自己的调度模块加了一个“定时巡检”入口总共只改了一处触发器注册核心状态机一行没动。这就是统一模型的价值。3. 执行路径地图一个任务是怎么跑完全程的3.1 一条请求从进入到返回的10个关键节点执行路径地图我理解为“当一条输入进来后它会在系统里按什么顺序经过哪些子系统、每个环节产出什么、失败时如何兜底”。这一节我用一个具体例子走一遍用户说“帮我把项目目录下的日志按日期归档”。第一步感知接入层接收原始文本解析成内部任务对象。此时系统还不知道要不要调用工具只是把它当作一项待办。第二步决策引擎做上下文组装把系统指令、任务对象、相关记忆片段打包后发给模型。第三步模型返回执行计划可能是“先用find命令找到日志文件再按日期建立目录然后移动文件”。如果模型输出不合法决策引擎会触发校验重试。第四步任务规划结果被展开成一个执行序列进入运行时状态机。第五步第一个工具调用发起工具执行层完成真实文件操作。第六步执行结果回填上下文观察阶段判断是否成功。第七步决策引擎根据上一步结果决定继续下一步或提前终止。第八步重复第五到第七步直到计划完成。第九步收尾阶段写入长期记忆比如“用户偏好按日期归档日志目录在项目根目录下”。第十步封装最终结果并返回给调用方同时更新会话状态。我建议你把这张十节点流程记在脑子里调bug时按图索骥比漫无目的地加日志有效率得多。这里的关键不是十个步骤本身而是每一步之间都有一个明确的“交接物”任务对象、执行计划、工具结果、记忆记录。只要有交接物你就能在任意节点去验证上一环节是否真正完成。3.2 规划与执行的循环任务拆解、子图展开与失败重试对于复杂任务单次规划往往不够需要多轮“决策-执行-观察-再决策”循环。这背后有一个很关键的机制任务拆解与子图展开。我观察到的主流做法是决策引擎先把大任务拆成若干子任务每个子任务有自己的目标和前提条件子任务之间存在依赖关系。执行时可以按依赖顺序逐个处理也可以把没有依赖关系的子任务并行化。运行到中间如果某个子任务失败有两种兜底策略一种是带上下文重试另一种是回退到决策引擎让它重新规划绕过失败点。系统可以根据失败类型决定采用哪一种。举个例子归档日志的任务如果中途发现某个日志文件被占用无法移动直接重试大概率还是失败。此时更好的策略是回退给决策引擎让它换一个工具比如先复制再删除或者跳过被占用的文件并记录告警。这种动态调整能力本质上依赖决策引擎手里有足够丰富的工具元数据和失败信息。我这里有一个实践建议一定要给整个任务生命周期设置最大执行步数和超时时间。我在生产环境遇到过因为工具执行卡死在外部API上agent反复重试导致整个任务队列堵塞的情况。加上最大步数限制之后至少能让失败快速暴露而不是让整个系统被拖进死循环。这些约束最好下沉到状态机层而不是依赖业务代码自觉遵守。另外子任务展开之后的执行顺序不要只依赖模型在一次输出里给出完整顺序。更稳妥的做法是每完成一个子任务就把结果回填到上下文再让决策引擎决定下一步。这样做虽然多几次模型调用但每一步都是基于真实结果做决策而不是基于预测。3.3 两种模式的路径差异bot mode和CUA虽然共用状态机但实际路径还是有差异的。我把差异整理成对照表对比项bot modeCUA输入源消息流、定时事件、webhook屏幕观察、鼠标键盘事件感知方式文本与结构化消息截图、UI元素识别、坐标定位主要执行工具消息发送、HTTP调用、文件操作点击、键入、滚动、窗口切换上下文特点长对话、历史记录较完整单步信息密度高、需要即时观察结果失败处理倾向于重试或跳过倾向于重新观察后再决策最让我觉得有价值的一个设计是CUA模式下每次执行完操作后系统强制回到观察阶段拿到新的屏幕状态再进行下一步决策。这和人做事的逻辑是一致的你点了一个按钮总得看一眼页面变成了什么样才能决定下一步干什么。很多自动化方案做得差不是因为操作识别不行而是因为缺少“观察-决策”的闭环导致一连串操作全部建立在过时信息上。Hermes Agent这套执行路径地图的厉害之处就是在状态机层面强制了这个闭环。bot mode下也有类似但更轻量的闭环每次工具调用之后决策引擎必须读取执行结果而不是假设结果符合预期。哪怕这个工具只是发一条消息也要确认发送成功再进入下一步。这个习惯能帮你避免很多“看起来成功实际失败”的bug。3.4 实操接入新子系统的四步法讲完路径来一个实操向的问题如果我要接入一个新工具或新子系统应该按什么步骤做才不会打断主路径我的建议是四步走。第一步先在工具执行层做协议适配。新工具无论如何都要封装成统一的工具接口不要在业务代码里直接调用底层SDK。第二步在决策引擎里补充工具描述和能力声明。模型只有看到这个工具的描述、参数结构、适用场景才会在规划阶段正确选择它。这一步经常被忽略导致工具明明已经接好模型却一次都没想起来用它。第三步在运行时状态机中确认该工具属于哪个执行阶段。如果它属于执行工具正常注册进工具执行层即可如果它需要感知外部状态还应该把它对应的观察逻辑挂到观察阶段。第四步为工具增加测试用例覆盖正常调用、参数缺失、外部服务超时三种情况。不要把“工具能用”当成“工具接入完成”测试用例是接入流程的一部分。这套流程看着繁琐但它保证了所有新功能都顺着既有执行路径走。最忌讳的一种做法是为了赶进度在某一个子系统里直接绕过统一接口临时加一段编逻辑。当时确实快但后面架不住查问题和扩展。我自己有一次为了赶一个演示临时绕过感知层直接调决策引擎接口结果演示当天发现有输入校验逻辑没走线上数据显示异常花了整整一天排查才找到原因。从那以后我再也不允许任何入口绕过统一管道。4. 常见问题与排查技巧实录4.1 我在实际接入中常踩的5个坑先说结论再展开讲。这5个坑按出现频率排序事件没有触发感知层输入直接绕过统一入口进了业务代码。上下文无限膨胀没有做裁剪和归档。模型输出非结构化文本导致路由失败或工具调用失效。工具执行结果未回填上下文模型在下一步失去判断依据。长时间运行后状态机卡在某个异常状态没有任何超时兜底。第一个坑最常见的表现是你在某些入口可以直接调用内部模块时间一长模块间的接口约定就开始腐化。解决办法是让所有入口都走统一管道哪怕初期觉得多一层封装没必要。这个坑通常是团队协作时最容易出现的因为每个人都觉得自己只是在“临时调一下”结果积少成多。第二个坑前面说过上下文是工作台不是仓库。解决办法是在决策引擎里做裁剪策略比如在上下文超限时把对话摘要存入中期记忆然后对旧消息做截断。要注意的是摘要本身也会占用token所以摘要策略要权衡不是所有历史都要落地成摘要只有关键转折点才值得记。第三个坑通常是prompt写得不够结构化导致的。解决办法是定义强格式的输出模板并增加校验重试。我在项目里为了让模型稳定输出JSON会在模板里给出示例同时对常见错误格式做正则修复成功率提升非常明显。但也要注意不要过度依赖正则修复因为模型输出错误的方式千奇百怪正则写不完。更可靠的兜底是重试机制。第四个坑会让agent看起来“失忆”。明明执行完一个工具下一步却不知道结果是什么。解决办法是确保工具执行层总是把结构化结果写回上下文并且观察阶段要真正读回这个结果后再交给决策引擎。我在排查这类问题时通常会先检查日志里工具返回结果是否出现在下一步的上下文组装数据中这一眼就能确认问题在哪一侧。第五个坑最隐蔽。状态机卡死不一定报错可能只是没有任何事件推进它。解决办法是给状态机加一个心跳检测和超时回退机制比如规定一个状态最多停留N秒超时自动回到空闲并标记错误。这个N值的设置要结合具体业务来定太小会导致正常耗时任务被误判太大会失去兜底意义。我的习惯是先设置一个较大值运行一段时间后根据日志中实际耗时分布来调整。4.2 由日志倒推执行路径的三步法排查agent问题最怕的是被埋在几十个module的日志里瞎翻。我的习惯是用“由日志倒推执行路径”的方法。具体操作分三步。第一步先找到任务对象在全流程中的唯一ID所有子系统的日志里都要打印这个ID。这个ID最好在感知层生成并且贯穿状态机、决策引擎、工具执行层、记忆子系统。没有这个ID你后续做任何关联分析都是空谈。第二步按时间轴把该ID的日志筛出来你会得到一条完整的执行路径。这里要注意打印日志时不要只打模块名和结果还要打关键字段的摘要比如任务的当前状态、工具名、参数摘要、返回状态。这样筛出来的日志本身就是一条可读的时序故事。第三步在路径上找异常点。通常问题都在两个相邻子系统的交接处而不是某个子系统内部。比如状态从“执行中”变到“空闲”但缺失“收尾”日志那问题大概率出在收尾阶段。比如有一次任务执行到一半停了我筛日志后发现问题出在工具执行层和观察阶段之间的消息队列工具执行层已经返回结果但观察阶段等了一分钟都没收到。继续追查才定位到是事件订阅配置漏了一个事件类型。如果我不按路径倒推而是一个个模块翻日志这个坑可能要查大半天。这个方法听起来简单但真正做到的前提有两个一是每个子系统都要在关键节点埋结构化日志二是一个全局任务ID贯穿始终。这两个基础设施在agent类项目里越早搭越好不要等到出问题了再补。补日志的代价是双份的既要在运行时加代码又要重新验证日志准确性还不如一开始就把日志埋好。4.3 值得长期维护的一张变更记录表最后分享一个我个人的运维习惯。我给agent项目维护了一张变更记录表记录每次对子系统的改动包括改了什么、涉及哪个执行路径节点、验证方式是什么、回滚计划是什么。有两个原因让我觉得这张表很有必要。第一agent系统的行为高度依赖外部模型和工具的变动。模型供应商改一个API工具服务商改一个字段都可能让整条执行路径静默失效。如果没有变更记录你根本没法判断一次行为异常是从哪次变更开始的。第二agent代码的逻辑分散在多个子系统单靠代码review很难看到全局影响。一张变更表能快速建立“变更-路径节点-风险”的映射关系。比如你看一条记录写着“修改了感知层对图片输入的压缩逻辑影响节点为感知→解析验证方式为回归测试案例7”那下次图片相关任务出问题你第一反应就是去查这次变更。我自己在把这套架构视角用到实际项目后最大的一个变化是再遇到问题我会先问这是哪个子系统的职责再问它处于执行路径的哪个节点而不是急着改代码。这个习惯帮我省下了非常多无效调试时间。如果你也在搭agent不妨先画一张自己的子系统与执行路径地图再动一行代码。架构总览不是写文档时才需要它是你面对任何一次报错时心里那条最早的导航线。