多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从Hermes Agent重构看大模型Agent的工程化实践:错误处理、上下文管理与可观测性

从Hermes Agent重构看大模型Agent的工程化实践:错误处理、上下文管理与可观测性 1. 从“能用”到“好用”我为什么要重写 Hermes Agent如果你最近在折腾大模型应用开发特别是想让模型能稳定、可靠地调用外部工具比如搜索、计算、执行代码那你大概率听说过或者用过 Hermes Agent。它作为一个开源项目凭借其清晰的架构和相对简单的上手门槛在社区里吸引了不少关注。我也是其中之一在几个内部项目中尝试用它来构建一些自动化工作流。但用着用着问题就来了。项目初期跑个Demo一切顺利感觉“未来可期”。可一旦想把东西部署到生产环境或者处理稍微复杂一点的链式任务时各种“硬伤”就开始暴露让人头疼不已。最直接的感受是它像一个毛坯房框架搭好了但水电不通、门窗漏风真要住进去得自己动手大修。我遇到的第一个拦路虎是错误处理。原版Agent在工具调用失败时反馈极其模糊经常就是一个简单的“Tool execution failed”扔回来至于为什么失败是网络超时、API密钥不对、返回格式解析不了还是工具本身就有BugAgent自己似乎也搞不清楚更别提尝试修复或绕过了。这导致整个工作流非常脆弱一个工具出错整个任务链就卡死毫无韧性可言。其次是上下文管理。在多轮对话和复杂任务拆解中上下文包括历史对话、中间结果、工具输出就是Agent的“记忆”。原版实现在这方面相当粗放token数很容易爆炸而且缺乏对关键信息的提炼和压缩机制。经常遇到的情况是任务执行到一半因为上下文太长被截断Agent直接“失忆”开始重复之前的步骤或者做出完全错误的决策。再者是工具描述的“幻觉”问题。为了让大模型理解工具我们需要用自然语言描述工具的功能、输入参数和输出。原版对这块的处理比较静态但实际使用中工具的行为可能因版本、配置或外部状态而变化。一个经典的坑是你告诉Agent“这个工具可以获取用户信息”但没说明某些字段可能为空。当Agent拿到一个包含null值的JSON时它可能会基于这个不完整的“幻觉”做出错误推断比如认为“用户没有邮箱所以不能发送通知”而实际上邮箱字段只是没返回而已。最后是可观测性与调试。当任务执行不符合预期时排查过程如同黑盒探案。你只能看到模型最终输出的“思考”和“行动”但对于模型在每一步是如何权衡不同工具、如何解析结果的几乎一无所知。这给问题定位和性能优化带来了巨大困难。正是这些在真实场景中反复踩坑的经历让我下定决心不是去给原项目修修补补提PR而是基于其核心思想从头重写一个更健壮、更实用的Agent框架。我的目标很明确保留其轻量、易集成的优点但必须从根本上解决这四个“硬伤”让它从一个“玩具”变成一个能在生产环境扛事的“工具”。下面我就来详细拆解我是如何针对这四个痛点进行设计和实现的。2. 硬伤一构建面向失败的韧性——错误处理与重试机制原版Agent最让人诟病的就是其“玻璃心”一碰就碎。在分布式系统和微服务架构里我们深知“失败是常态”设计时必须面向失败。对于Agent而言工具调用失败同样应该是预期之内的事件。我的重写核心就是为Agent注入面对失败的“韧性”。2.1 错误分类与精细化反馈第一步是建立一套错误分类体系。我们不能把所有异常都混为一谈。我大致将工具调用错误分为几类可重试错误如网络超时TimeoutError、第三方API速率限制RateLimitError、服务端临时错误5xx状态码。这类错误通常稍后重试就可能成功。输入错误如调用工具时参数缺失、参数类型错误、参数值超出范围。这通常是Agent对工具理解有误或用户指令模糊导致的需要模型调整输入。配置错误如API密钥无效、工具端点URL错误。这类错误通常无法通过重试解决需要人工干预检查配置。逻辑错误/意外输出工具执行成功返回200但返回的内容结构不符合预期、包含异常值如NaN,Infinity或语义上表示失败如{“status”: “error”, “msg”: “not found”}。这是最隐蔽的一类。针对每一类错误我设计的Agent不再只是抛出一个通用异常。相反工具执行层Tool Executor会捕获异常并进行富结构化封装。例如对于一个网络超时错误传递给模型LLM的反馈会是这样的结构化信息{ “error_type”: “RETRIABLE_NETWORK_ERROR”, “tool_name”: “web_search”, “attempt”: 1, “message”: “调用‘web_search’工具时网络连接超时30秒。”, “suggestion”: “可能是网络波动或目标服务响应慢。建议1. 等待10秒后重试2. 若问题持续可尝试使用备用搜索工具‘duckduckgo_search’。” }这个反馈包含了错误类型、上下文、以及具体的、可操作的建议。这相当于给了模型一个“错误说明书”让它能理解发生了什么并知道下一步可以怎么做。2.2 智能重试与备选方案有了精细化的错误信息Agent的“大脑”LLM就可以做出更智能的决策。我重写了Agent的决策循环ReAct模式中的“Act”部分使其具备内置的重试逻辑和备选工具路由能力。核心逻辑如下当接收到一个RETRIABLE_*错误时Agent不会立即放弃。它会根据错误类型和当前重试次数决定是否等待后重试。例如对于速率限制错误它会自动计算建议的等待时间如从响应头中读取Retry-After。重试策略是可配置的如指数退避Exponential Backoff避免加重服务压力。如果重试数次后仍失败或者错误类型是INPUT_ERROR模型会尝试分析错误信息中的suggestion。例如当web_search持续失败时模型可能会根据建议自主决定调用备用的duckduckgo_search工具。对于CONFIG_ERRORAgent会明确告知用户需要检查配置并可能暂停当前任务链避免无意义的尝试。这个过程的实现关键在于让模型参与故障恢复的决策。我们不是写死“失败A则跳转到方案B”的规则而是通过丰富的上下文让模型自己去推理最佳的恢复路径。这大大增强了Agent处理复杂、动态失败场景的能力。注意重试机制需要谨慎设置上限如最多3次并避免在具有副作用的工具如“发送邮件”、“创建订单”上盲目重试否则可能导致重复操作。对于这类工具错误处理应更倾向于直接失败并明确告警。2.3 实战踩坑工具返回的“软错误”上面提到的第四类“逻辑错误”非常棘手。我曾在做一个数据查询Agent时踩过大坑。工具调用总是返回HTTP 200但数据体里是{“code”: 500, “data”: null}。原版Agent看到200就认为成功了欢快地把null当作有效结果传递给下一步导致后续计算全部崩溃。我的解决方案是在工具执行层后增加一个输出验证器Output Validator。每个工具除了描述还可以关联一个轻量的验证模式例如使用JSON Schema或一个简单的验证函数。在执行成功后验证器会检查输出是否符合预期。# 示例为“get_weather”工具定义一个简单的输出验证函数 def validate_weather_output(output: dict) - tuple[bool, str]: if not isinstance(output, dict): return False, “Output must be a dictionary.” if “temperature” not in output or “condition” not in output: return False, “Missing required fields: ‘temperature‘ or ‘condition’.” if not isinstance(output[“temperature”], (int, float)): return False, “Field ‘temperature‘ must be a number.” return True, “”当验证失败时这次工具调用会被标记为LOGIC_ERROR并将验证失败的信息反馈给模型。模型可能会意识到需要调整查询参数或者换一个数据源。这一步将很多运行时错误提前拦截显著提升了系统的稳定性。3. 硬伤二告别“金鱼脑”——动态上下文管理与提炼大模型的上下文窗口是宝贵的资源也是主要的成本来源之一。原版Agent简单地将所有历史对话、工具调用和结果一股脑塞进上下文很快就会出现“注意力稀释”和token耗尽的问题。Agent变得像一条“金鱼”记不住稍早前的关键信息。3.1 分层上下文架构我设计了一个分层级的上下文管理策略核心思想是不是所有记忆都同等重要我们需要区分工作记忆和长期记忆并主动管理信息的密度。原始记录层完整存储所有的用户消息、模型“思考-行动”记录、工具调用请求与原始响应。这一层主要用于审计、调试和潜在的回滚操作不直接参与模型推理。操作摘要层这是模型每次推理时实际看到的上下文。它并非原始记录的简单拼接而是经过提炼的摘要。每当一轮“思考-行动-观察”循环结束后系统会自动生成一个该步骤的结构化摘要。这个摘要模板类似[Step 3] User asked for the latest news about SpaceX. I decided to use the news_search tool with query “SpaceX launch latest”. The tool returned 5 articles, with headlines including ‘Starship Test Flight Scheduled for Friday‘ and ‘FAA Grants Launch License‘. The key information is: the next launch is planned for this Friday from Boca Chica.这个摘要包含了意图要做什么、行动用了什么工具和参数、关键结果而非全部结果。它用极少的token保留了该步骤最核心的语义信息。3.2 自动上下文压缩与关键信息提取随着对话进行即使只是使用摘要上下文也会增长。我引入了两种压缩策略滑动窗口摘要只保留最近N步例如10步的详细摘要对于更早的步骤则触发摘要的摘要。系统会用一个更简短的句子来概括一段连续的相关操作。例如将步骤1-5的摘要关于查询天气、规划行程压缩为“用户询问了北京和上海的天气并基于此讨论了本周末的出行计划。”关键实体/事实提取与独立存储在任务执行过程中系统会像一个“秘书”一样主动识别并提取出可能对后续至关重要的信息并将其放入一个独立的“关键事实列表”中。例如在预订流程中提取出的“目的地上海”、“日期2023-10-27”、“预算2000元/晚”会被单独维护。这个列表会以高优先级的方式始终包含在上下文中确保Agent不会遗忘核心约束条件。这个功能的实现可以巧妙地利用大模型自身。在每一步结束后除了生成给用户的回复还可以让模型或一个小型、高效的模型多做一个任务“请用一句话总结这一步对完成整体目标的核心贡献并提取出任何可能对后续步骤至关重要的新事实或约束。” 这个输出就被用于更新我们的摘要和关键事实列表。3.3 成本与效果的权衡这种动态管理无疑增加了复杂性但收益是巨大的。在测试一个多步骤数据分析和报告生成任务时使用原始方法在15步后上下文token数超过了8000模型开始出现明显的性能下降和遗忘。而采用分层摘要和压缩策略后有效上下文token数始终维持在1500以下任务完成率和结果质量显著提升。个人心得上下文管理没有银弹。你需要根据任务类型调整策略。对于逻辑严密、环环相扣的任务如代码调试需要更完整的步骤记录对于探索性、发散性的任务如头脑风暴则可以更激进地压缩。在我的实现中这些策略如摘要模板、滑动窗口大小、是否启用关键事实提取都是可配置的允许开发者针对不同场景进行调优。4. 硬伤三打破工具描述的“幻觉”——动态工具描述与验证工具描述是连接大模型“心智”与现实世界的桥梁。如果这座桥的图纸描述和实际桥梁工具实现对不上模型就会产生“幻觉”做出错误的行动。原版的静态描述无法应对动态变化的世界。4.1 从静态文本到动态生成我摒弃了在代码中硬编码工具描述字符串的做法。相反我为每个工具注册了一个描述生成器函数。这个函数在每次Agent开始规划或需要重新认识工具时被调用它可以访问工具的当前状态、配置甚至一些运行时上下文。举个例子一个“数据库查询”工具它的描述生成器可能会做这些事情检查数据库连接状态如果连接失败描述中会加入警告“当前数据库连接不可用调用将失败。”获取Schema信息动态查询数据库将当前可用的表名和关键字段名嵌入描述中。例如“可查询的表包括users(id, name, email),orders(id, user_id, amount, status)。”反映权限根据当前Agent的授权描述其可操作的范围。“注意你只有users表的读取权限无法进行写入操作。”这样一来工具描述就从一份过时的说明书变成了一份实时更新的操作手册。模型基于这份手册做出的决策其可靠性大大提升。4.2 输入输出模式的强约束自然语言描述是给模型看的但计算机需要精确的契约。我引入了基于Pydantic模型的强类型约束作为工具描述的“机器可读”部分。每个工具在注册时必须明确声明其输入参数的模式InputModel和输出结果的模式OutputModel。from pydantic import BaseModel, Field from typing import List class SearchInput(BaseModel): query: str Field(…, description“搜索关键词”) max_results: int Field(5, ge1, le20, description“返回结果数量1到20之间”) class SearchOutput(BaseModel): results: List[str] Field(…, description“搜索结果的摘要列表”) source: str Field(…, description“搜索源如‘google’, ‘bing’”) tool_registry.register(input_modelSearchInput, output_modelSearchOutput) async def web_search_tool(input: SearchInput) - SearchOutput: # … 工具实现 … return SearchOutput(results[…], source“google”)这样做的好处是多方面的对模型在生成工具调用参数JSON时可以利用InputModel的JSON Schema进行引导和校验大幅减少格式错误。对执行层在调用工具前可以先用InputModel验证传入的参数是否合法类型、范围将明显的输入错误拦截在工具逻辑之外。对结果处理工具实现者被强制要求返回符合OutputModel的对象。这保证了输出结构的稳定性。执行层在拿到结果后可以再次进行序列化和验证确保返回给模型的数据是“干净”的。4.3 运行时描述更新与错误反馈闭环动态描述的威力在错误处理中能形成闭环。当工具因为输入错误或逻辑错误调用失败时这个错误信息可以被反馈给“描述生成器”。例如一个“发送邮件”工具如果多次因为“收件人邮箱格式错误”而失败描述生成器可以动态地在描述中强化这一点“重要to_email参数必须是一个有效的电子邮件地址格式如 userexample.com近期多次调用因格式错误失败。”模型在下次看到这个更新后的描述时就会格外注意邮箱格式的校验。这相当于让工具具备了“从错误中学习并提醒”的能力。在我的实践中这套“动态描述 强类型契约”的组合拳将因工具理解偏差导致的调用失败率降低了约70%。它让Agent对工具能力的认知尽可能地贴近了工具的真实状态。5. 硬伤四打开决策黑盒——可观测性与调试支持当Agent的行为出乎意料时如果只能看到它最终输出的文本调试过程就像在迷宫里摸黑前行。我们需要一束光照亮模型“思考”的路径。可观测性Observability是生产级系统的生命线对Agent同样如此。5.1 全链路追踪与结构化日志我重写的框架内置了全链路的追踪系统。每一次Agent的调用都会生成一个唯一的trace_id。这个trace_id会贯穿整个执行过程用户输入解析、模型思考、工具选择、参数生成、工具执行、结果观察、下一轮思考……所有关键事件都以结构化的日志形式记录并关联到trace_id。日志不仅仅是文本而是包含时间戳、事件类型、详细数据如完整的工具调用请求体、原始响应体、模型生成的思考过程的JSON对象。这些日志可以输出到控制台更方便的是可以无缝对接像OpenTelemetry这样的标准可观测性框架发送到Jaeger、Zipkin或云服务商的监控平台。这样当出现问题时你可以通过trace_id轻松拉取到整个任务生命周期的所有日志像看一部逐帧播放的电影一样回顾Agent的每一步决策和执行细节。5.2 思维过程的“可视化”除了后台日志对于开发者和高级用户我提供了一个更直观的“思维过程视图”。在Agent运行的同时它可以实时输出一个结构化的中间表示。例如在每一步你不仅能看到模型最终决定调用的工具和参数还能看到它在决策时的“候选列表”和“推理权重”。这个功能是通过要求模型在输出最终行动前先以特定格式输出其“内心独白”来实现的可以通过System Prompt或结构化输出要求来实现。[THOUGHT] 用户想了解特斯拉的股价。我有几个选择 1. 使用 get_stock_price 工具直接且准确。权重0.9 2. 使用 web_search 工具可能找到更丰富的新闻和分析但股价信息可能不是实时。权重0.6 3. 询问用户要的是实时股价还是历史趋势。这更严谨但效率稍低。权重0.5 我选择选项1因为它最直接匹配用户需求。 [ACTION] get_stock_price {“symbol”: “TSLA”}这种“思维可视化”对于调试复杂任务至关重要。你可以清楚地看到模型为什么选择了A工具而不是B是基于怎样的判断。如果选择错了你就能定位是工具描述不清、还是模型权重理解有偏差从而有针对性地优化。5.3 性能指标与成本监控对于一个需要长期运行、可能产生费用的Agent系统监控其性能和成本是必须的。我的框架会自动收集一系列指标延迟每轮思考-行动的耗时、工具调用的耗时。用量每次调用消耗的Prompt Token和Completion Token数量。成功率工具调用的成功率和失败分类统计。成本估算根据token使用量和模型单价估算每次任务和累计成本。这些指标可以通过仪表盘展示帮助开发者识别性能瓶颈例如某个工具调用特别慢、成本异常例如某个任务消耗了不成比例的token以及系统的整体健康度。踩坑实录在早期版本我曾忽略了对工具调用超时的监控。结果线上一个依赖外部慢API的工具经常在超时后导致整个Agent线程挂起资源无法释放。后来加入了每个工具调用的超时控制和指标上报后我们很快发现了这个“害群之马”并将其替换或增加了降级策略。可观测性数据是优化系统最直接的依据。6. 重构后的实战一个完整任务链的对比理论说了这么多我们来看一个重构前后对比的具体例子。假设任务是“帮我查一下OpenAI最近有什么新动态然后总结成一份简短的邮件草稿最后估算一下用GPT-4写这封邮件大概要花多少钱。”原版Hermes Agent的可能执行路径调用web_search工具搜索“OpenAI latest news”。可能成功也可能因网络问题失败且无清晰提示。将搜到的几篇长文章全文可能包含大量HTML标签和广告文本塞入上下文。尝试调用summarize_text工具进行总结。此时上下文已非常冗长模型可能遗漏关键信息总结质量不高。基于总结让模型生成邮件草稿。需要估算成本时模型可能因为缺乏token计算工具或相关上下文而胡编一个数字。整个过程如果任何一步失败任务链中断用户得不到任何中间结果。重写后Agent的执行路径规划阶段模型根据动态工具描述知道有news_search专用于新闻、web_search通用等工具。它选择news_search因为描述显示它“返回结构化摘要信息更干净”。执行与观测调用news_search(“OpenAI”)。如果遇到速率限制错误系统反馈RETRIABLE_RATE_LIMIT_ERROR并建议等待2秒后重试。模型决定等待后重试成功获取到3条结构化新闻摘要标题、来源、简短摘要。系统自动将这三条摘要的关键信息如“发布新模型GPT-4o”、“降低API价格”提取到“关键事实列表”。总结与起草上下文此时很精简用户指令、关键事实列表、上一步的成功摘要。模型调用write_draft工具内部可能结合了提示词模板生成邮件草稿。在此过程中框架记录了消耗的token数假设为1500 tokens。成本估算模型知道有一个calculate_cost工具其动态描述中包含了当前的GPT-4 API单价如$0.03 / 1K tokens for input。模型调用该工具参数为{“model”: “gpt-4”, “prompt_tokens”: 1200, “completion_tokens”: 300}。工具根据最新单价计算出成本约$0.045并返回。最终输出Agent将邮件草稿和成本估算一并呈现给用户。整个过程的每一步日志、token消耗、工具调用详情都记录在trace_id下可供查阅。这个对比清晰地展示了重写后Agent在韧性、上下文效率、工具理解准确性和过程透明度上的全面提升。它不再是一个脆弱的脚本而是一个真正能够理解任务、应对意外、并给出可靠结果的智能助手。7. 集成与使用如何将重构思想应用到你的项目你可能不需要完全重写一个Agent框架但完全可以将这些解决“硬伤”的思想应用到现有的项目中。以下是一些可以逐步实施的建议从错误处理开始为你现有的工具函数包裹一层错误处理装饰器。捕获异常将其分类可重试/不可重试并生成结构化的错误信息返回给LLM。这是提升稳定性最快的方法。实施简单的上下文摘要在每一轮Agent循环结束后不要简单拼接原始结果。尝试用一个小提示词让LLM自己生成这一步的简短摘要例如“请用一句话总结你刚才这一步做了什么以及得到了什么关键结果。”然后用这个摘要替代冗长的原始文本进入下一轮上下文。为工具添加类型注解和验证即使不使用Pydantic也可以利用Python的typing模块和简单的assert语句或验证函数在工具被调用前检查参数在返回前检查结果格式。这能提前发现很多数据不一致的问题。引入追踪日志在代码的关键节点接收请求、调用模型、调用工具、返回结果打印或发送带有唯一ID的结构化日志。这不需要复杂的系统一个简单的日志文件就能在调试时帮上大忙。建立成本监控在调用LLM API的客户端代码处记录每次请求的token使用情况并定期汇总报告。这能让你对成本心中有数避免意外账单。重构的核心思路是从“让Agent跑起来”转变为“让Agent跑得稳、跑得明白、跑得划算”。这需要开发者以系统工程的思维来对待Agent而不仅仅是Prompt Engineering。这次重写之旅让我深刻体会到构建可靠的AI应用其复杂性往往不在AI模型本身而在如何让模型与复杂、不确定的真实世界进行稳健、高效的交互。这其中的设计、权衡与工程实践才是真正价值所在。
返回列表