ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自研轻量级Agent运行时:消息级控制力与工程实践

自研轻量级Agent运行时:消息级控制力与工程实践 最近几个月做了一些AI Agent方向的项目越做越觉得现成的编排框架在真实业务里容易卡脖子。不是跑不通而是出了问题之后你很难在消息层面去控制它。于是我自己动手写了一个轻量级的Agent运行时取名hermes-agent。名字取自希腊神话里的信使赫尔墨斯定位就是一个在用户意图和工具执行之间传递消息的信使。这篇东西不是晒代码而是把我从设计动机、核心抽象、工具层、上下文管理到多Agent协作的完整思考过程记录下来包括踩过的几个比较隐蔽的坑。希望对正在纠结要不要自己写Agent框架或者现有框架到底够不够用的朋友有点参考价值。1. 先回答一个扎心问题市面框架这么多为什么还要自己写先说结论不是现有框架不好而是当你需要消息级的控制力时它们的抽象层级往往帮不上忙。1.1 当你需要消息级控制力的时候我之前用主流编排框架做过一个内部知识库问答机器人。单看演示效果跑得很顺工具调用、记忆回传都有。但一旦进入生产环境问题就来了某个工具返回了一万多字的JSON直接把上下文窗口撑爆模型开始胡言乱语。模型判断用户意图时把查天气和查库存搞混因为两个工具描述里有相似的关键词框架层根本不管这种歧义。多轮对话里用户说那换成明天的框架把整段历史都塞给模型结果模型把之前的错误结论当成了事实。这些问题的根源很一致框架把Agent当成大模型工具列表的简单封装消息怎么路由、记忆怎么取舍、工具怎么选择全丢给模型自己处理。模型在这种自助餐式的上下文里出错概率远比你想象的高。所以我想要的其实很朴素——一个能让我在消息层面插手干预的Agent运行时。1.2 hermes-agent给自己定的三条红线动手之前我给自己定了三条规矩整个项目都是围绕这三条来的消息是第一公民。所有状态变化都通过消息流转来体现包括用户输入、模型输出、工具调用、子任务委派。这样出了问题我可以把整条链路的消息打出来逐条检查。工具是显式注册的。Agent不靠模型自己猜有哪些工具而是由系统根据当前会话动态注入工具清单并且注入之前先做一轮裁剪。记忆不是缓存是有取舍的。上下文管理不是简单拼字符串而是按会话层级维护并有显式的压缩策略。这三条红线听起来不复杂但实现起来牵扯到的细节远比我预想的多。后面几节我会把每个模块的设计思路和关键代码逻辑拆开讲。2. 核心抽象Agent不是一个类而是一个信使循环很多人写Agent第一反应是定义一个Agent类里面放一个LLM客户端一个工具列表然后写个run方法。hermes-agent不是这个路子它的核心是一个循环Agent只是这个循环的外壳。2.1 三种基础对象Message、Tool、Agent整个内核只依赖三个基础对象Message统一的消息结构包含roleuser、assistant、tool、system、content内容、meta元数据比如工具调用ID、会话ID、时间戳。Tool工具的抽象包含name、description、input_schema、handler四件套。handler是真正执行工具逻辑的函数。Agent一个循环的执行体持有LLM客户端、当前会话的上下文、可用的工具集合。关键设计在于Agent不直接持有所有工具而是持有一个工具提供者ToolProvider每次循环之前向提供者请求当前这一步可以用哪些工具。这个设计是后面做工具裁剪的基础也是和普通框架最大的区别。2.2 一次任务请求在内核中的完整流转用大白话描述一次请求的流转大概是这样的用户消息进入Agent被包装成Message写入会话上下文。Agent把上下文提交给LLM同时附上当前可用的工具清单。LLM返回两种可能之一要么是最终答案要么是一个工具调用请求。如果是工具调用请求Agent解析出工具名和参数从ToolProvider里取出对应handler执行把结果封装成tool角色的Message重新放回上下文回到第2步。如果LLM直接给出最终答案循环结束返回结果。这个循环本身不复杂复杂的是几个边界条件循环最大轮数、工具执行超时、模型重复调用同一个失败工具、并发工具调用的结果排序。这些我在第6节单独讲踩坑经历。从实现角度这个信使循环的核心价值在于每一步都有完整的日志记录。我给这个循环加了一个可选的hook机制允许你在工具调用前和工具调用后插入自定义逻辑。比如自动给敏感操作加人工确认或者在某个工具连续失败两次后自动切换备用方案。这些都是消息级控制力带来的直接好处。3. 工具注册与动态选择让模型看清每个工具而不是猜Agent的能力上限很大程度上取决于工具清单喂得对不对。模型不是你肚子里的蛔虫你塞给它50个工具描述它大概率会挑花眼。3.1 工具描述怎么写才不容易被模型误解这是我在实践里最深的体会工具注册时description字段的质量直接决定Agent的准确率而且这个影响会被多轮对话放大。我总结出一套工具描述模板现在团队内部的新工具都按这个格式写第一句这个工具做什么一句话说清避免术语堆砌。第二句明确适用的前提条件比如仅当用户明确询问库存数量时调用。第三句明确的否定边界比如本工具不处理价格查询。举一个真实例子。同样是查天气一个工具描述是获取天气数据另一个描述是查询指定城市当天的天气情况包括温度、湿度、风力仅当用户明确提到地点和日期时调用不提供空气质量信息空气质量请调用environment_query。实测下来后者的正确路由率能高出将近20个百分点。input_schema方面我强烈建议用JSON Schema而不是简单的参数名注释。因为JSON Schema可以表达嵌套结构、必填项、枚举值模型对结构化的约束理解得更好。比如一个创建订单的工具status字段加上枚举约束模型就不会再编出一个PAID以外的状态值。3.2 工具裁剪从50个工具里选出5个工具描述写得再好一次塞50个给模型效果也会急剧下降。一方面是上下文窗口被吃掉一大截另一方面是模型在长工具列表里做选择的出错率显著上升。hermes-agent的ToolProvider内置了两级裁剪策略静态裁剪按会话类型过滤。比如客服会话和运维会话使用的基础工具集完全不同这个在Agent初始化时通过白名单配置。动态裁剪按当前消息语义做相关性排序。我用一个轻量的embedding模型对当前用户消息最近一条助手消息做向量化然后和所有工具描述向量做相似度排序取Top N个。N默认是8可配置。这个方案的效果非常明显。我对比过裁剪前后的结果同样50个工具的场景下裁剪后模型调用工具的准确率从大概76%提升到92%以上单轮调用的Token消耗也降了差不多40%。有一点要提醒动态裁剪依赖embedding的相似度遇到工具描述高度相似的情况比如查询订单和查询售后单Top N的排序可能不稳定。我的解法是在工具描述里刻意做差异化表达把订单和售后单的适用场景、对象归属写清楚让向量能分开。4. 上下文管理记忆不是缓存是有取舍的工程这一节可能是整个项目里最反直觉的地方。很多人觉得上下文管理就是把历史消息全部存下来需要用的时候一股脑交给模型。实际这么做多轮对话超过五轮就开始出问题。4.1 会话记忆的分层设计hermes-agent把记忆分成了四层工作记忆Working Memory当前任务内的消息列表有长度上限超限后触发压缩。摘要层Summary Layer对更早的历史对话做结构化摘要保留事实性信息丢弃寒暄和噪声。事实层Fact Layer从对话中抽取的关键实体和关系类似用户所在地上海用户当前订单号abc123以键值对形式存储。长期记忆Long-term Memory跨会话的用户画像和偏好必须显式读写不走自动摘要。实际取上下文时组装顺序是事实层 摘要层 工作记忆。这个顺序有讲究事实层保证模型不会把关键状态搞丢摘要层提供背景工作记忆负责当前任务的连贯性。4.2 上下文压缩的三条规则当工作记忆超过上限时我设置了一个压缩器规则有三条按顺序执行优先丢弃工具返回的原始大JSON替换成工具xxx返回了N条数据其中关键字段为...这样的占位描述。这一步通常能砍掉一半的Token。合并连续的同类消息。比如用户连续问了三个问题模型连续回答了三次可以在摘要层合并成用户咨询了A、B、C三个问题回答要点分别是...。删除冗余的中间修正。如果模型在对话中先给了错误答案又被纠正只保留最终纠正后的结论。这三条规则看起来简单但每条规则的触发条件我都做了可配置项。比如第一条我设置了单条工具返回超过2000字符就自动做裁剪。这里有个血泪教训压缩动作本身不能改变事实层的状态。我早期实现压缩器时直接让它重写整个上下文结果把用户订单号这种关键事实给丢了后续所有工具调用全部拿不到订单参数。后来我把事实层独立出来压缩器只能碰工作记忆问题才彻底解决。5. 多Agent协作信使如何不传错话单个Agent的能力有边界所以hermes-agent支持把任务拆给多个子Agent执行。这一节是我觉得整个项目里最有赫尔墨斯气质的地方——消息怎么传话怎么递直接影响协作结果。5.1 从单兵到小组委派任务的协议多Agent协作我采用了一个非常简单但有效的协议父Agent通过发出一个特殊类型的task_delegate消息来委派任务消息里包含task_id子任务唯一IDtarget_agent子Agent的名称task_payload子任务的具体说明包含目标、约束、可用的工具白名单callback_topic子Agent完成后结果回传的地址标识子Agent接收消息后在自己的循环里独立执行执行完把结果封装成task_result消息发回。父Agent不等待同步返回而是通过消息队列订阅结果。这个异步设计的直接好处是多个子任务可以并行跑而且某个子Agent卡住不会拖垮整个流程。5.2 结果回收与错误传播协作场景里最容易出问题的是结果回收和错误传播。结果回收我遇到的坑是子Agent返回的结果格式不统一。有的返回文本总结有的返回结构化的JSON父Agent在合并时非常痛苦。后来我统一规定所有task_result消息必须包含一个result_summary字段给父Agent看的文本总结和一个result_data字段结构化数据。父Agent默认只看result_summary只有需要精确数据时才解析result_data。错误传播方面我的原则是子任务的错误不要静默吞掉。子Agent执行失败时必须返回一个task_failed消息带error_code和error_detail。父Agent收到后有两种处理策略要么重试支持配置最大重试次数要么降级换一个子Agent或者走兜底逻辑。这样整条链路即使出错了也能在消息日志里清楚地看到是哪一环断的。这套设计跑了一段时间后我的感觉是多Agent协作能不能成功七成取决于消息协议定得好不好而不是每个子Agent内部的提示词写得多漂亮。协议定了协作就是插拔式的协议没定好后面所有子Agent写起来都是各说各话。6. 实测中的五个坑以及我怎么填的最后分享几个在真实使用中踩过的坑。每一个都花了至少半天排查写出来帮大家省点时间。6.1 模型陷入失败循环不自知现象某个工具因为参数校验失败反复报错模型不换方案同一组参数连续调用五六次浪费Token且拖慢响应。排查过程我把消息日志打出来发现模型每一次工具调用返回的错误信息都一样但模型就是不停重试。根本原因是错误信息太干了模型不知道该怎么修正。解决办法在工具返回的异常信息里增加修正建议字段。比如参数校验失败时错误信息带上缺少必填参数status可选值为pending、paid、cancelled。这之后模型基本都能在一两次内自我纠正很少再陷入循环。6.2 并行工具调用的结果错位现象模型一次请求返回两个工具调用比如查天气和查机票我把它们并行执行结果某个调用的结果回到了另一个调用的上下文位置导致Agent把机票价格当成天气数据用。排查过程发现问题是并行工具调用的结果没有绑定各自的tool_call_id回填上下文时按顺序硬塞一旦其中一个执行较慢顺序就乱了。解决办法每次工具调用在发出时就生成唯一ID结果返回时严格按tool_call_id与请求关联最终回填上下文前再按原始顺序重排。现在我要求所有支持并行调用的工具结果都带tool_call_id字段没有例外。6.3 流式响应被上下文压缩打断现象开启流式输出后用户还在看答案滚动这时上下文压缩器被触发把正在生成的会话给重置了用户看到输出突然中断。排查过程这个坑最隐蔽。原因是压缩器监测到工作记忆超限异步启动压缩但没判断当前是否正在流式生成中直接把上下文里属于当前生成的半截内容给清了。解决办法给上下文加了一个简单的读写锁流式生成期间禁止压缩器碰工作记忆压缩任务挂起直到生成完毕。另外流式模式下我建议把压缩触发阈值调高一些宁可多消耗一点Token也别在输出中段搞出断片。6.4 工具超时导致的连锁拖垮现象某个外部API响应很慢工具handler没有设置超时整个Agent循环被卡住后续所有请求排队越来越慢。排查过程看日志发现单次工具调用的耗时高达40秒而外部API的正常P99只有3秒。解决办法所有工具handler强制要求支持超时中断默认5秒超时后返回一个tool_timeout错误。同时Agent循环里加了单次工具的耗时上限超限就记录警告并跳过该工具结果。这两个措施叠加之后最慢的请求从40秒降到了8秒以内。6.5 多个Agent共享上下文导致事实串台现象两个子Agent在协作时一个Agent把另一个Agent对话里的事实比如用户名当成了自己的上下文导致工具调用参数出错。排查过程定位到问题出在共享的全局上下文对象上子Agent把用户的历史事实误读为当前任务的输入。解决办法每个Agent实例强制挂独立的上下文子Agent只能通过task_payload获取父Agent下发的任务信息不允许直接读父Agent的内部状态。这个边界规定在项目文档里被标为最高优先级约束。7. 一些使用上的个人经验补充分享写完上面这些再补充几句实操层面的体会这部分可能更软但对真正想上手的人会有帮助。第一日志是你的第一调试工具不是最后的手段。hermes-agent的信使循环设计让每一步消息流都可追踪我排查绝大多数问题都是先打开消息日志看模型在每一步到底看到了什么、调用了什么、工具返回了什么。很多模型不听话的问题最后都追溯到模型根本没看到某个信息。第二提示词和工具描述要一起迭代不要只调提示词。我发现很多Agent效果不好第一反应是改提示词但实际瓶颈往往是工具描述不够清晰。把工具描述的写作当作文档工程来做和调提示词同等重要。第三先小范围跑通一个真实场景再横向扩展。hermes-agent早期我选了一个非常窄的场景——订单售后客服。这个场景只有五个工具、三种意图跑通之后才逐步加入其他业务域。别一上来就搞全能助理那样只会让排错难度翻倍。我自己目前还在持续迭代hermes-agent最近的重点是让动态工具裁剪的embedding模型可以按业务域做微调以及把多Agent的task_delegate协议改成可以嵌套子任务树的形式。这两个方向都是使用中被真实需求逼出来的后续有进展我会继续写文章同步。如果你也在做Agent方向或者正准备自研框架欢迎从这篇文章里的思路出发根据你自己的业务形态做取舍。框架的名字不重要重要的是你能不能在消息层面真正掌控Agent的每一步。
返回列表