1. 工具运行时的核心设计哲学1.1 为什么“失败”值得被当作一等公民做 Agent 开发的人都有一个共识工具调用是整个系统里最容易出问题的环节。模型再聪明一旦进入 toolRun 阶段面对的就是真实世界的混沌——网络抖动、参数格式错误、第三方接口限流、文件路径不存在、权限不足甚至模型自己“幻觉”出一个根本不存在的工具名。我见过太多项目工具调用失败后直接抛异常整个 agent execution 终止前端弹出一句“agent execution terminated due to error”用户一脸懵开发者翻日志翻半天。这种设计思路的本质是把失败当成了“异常”而不是“正常输出”。“失败是数据”这个理念的核心转变在于工具运行时的每一次失败都应该被结构化地捕获、包装、返回给模型让模型像处理正常结果一样处理失败信息从而自主决策下一步——重试、换参数、换工具、还是向用户求助。这个思路不是拍脑袋想出来的。你去看主流 agent 框架的设计无论是 ReAct 循环还是 Plan-and-Execute 架构工具节点的输出最终都会拼进 context 里喂回模型。如果失败信息只是一句“Error: something went wrong”模型根本没法做出有效决策。但如果返回的是{status: failed, error_type: invalid_parameter, message: 参数 date 格式应为 YYYY-MM-DD实际收到 2024/1/5, suggestion: 请将日期格式转换为 YYYY-MM-DD 后重试}模型就能立刻明白问题在哪自动修正。1.2 失败数据的分类体系不是所有失败都一样。我在实际项目中把工具运行时的失败分成四大类每类的处理策略完全不同失败类型典型场景是否可重试返回给模型的建议参数错误格式不对、缺必填字段、类型不匹配可立即重试指出具体哪个参数有问题给出正确格式环境错误网络超时、服务不可用、限流可延迟重试告知暂时不可用建议稍后重试或换工具权限错误无访问权限、token 过期需先修复明确告知需要什么权限不要盲目重试逻辑错误工具不存在、前置条件不满足不可重试建议换工具或调整执行计划这个分类直接决定了你的 toolRun 层怎么写。参数错误要在 schema 校验阶段就拦截环境错误要有重试机制和退避策略权限错误要触发认证刷新流程逻辑错误要反馈给 planner 重新规划。1.3 JSON Schema 在其中的关键角色热词里出现了 JSON Schema这不是偶然。JSON Schema 是工具运行时的契约。它定义了每个工具接受什么参数、什么类型、哪些必填、取值范围是什么。有了这份契约你才能在模型调用工具之前就做一层校验把大量参数错误拦截在真正执行之前。我习惯给每个工具定义两份 schema一份是给模型看的用于 function calling 的参数描述一份是给运行时校验用的更严格包含 format、pattern、min/max 等约束。模型看到的 schema 可以稍微宽松但运行时校验必须严格。这样模型生成参数后先过校验层不通过就直接返回结构化的参数错误根本不用去调真实接口。# 运行时校验的 schema 示例 tool_schema { name: query_weather, parameters: { type: object, properties: { city: {type: string, minLength: 1}, date: {type: string, pattern: ^\\d{4}-\\d{2}-\\d{2}$}, unit: {type: string, enum: [celsius, fahrenheit]} }, required: [city, date] } }校验不通过时返回的错误信息要精确到字段级别而不是笼统地说“参数错误”。这一点后面会详细展开。2. toolRun 层的结构化实现2.1 统一返回格式的设计工具运行时的输出格式必须统一。不管成功还是失败返回给上层的结构应该长一个样只是 status 字段不同。我通常用这样的结构{ status: success | failed, tool_name: query_weather, duration_ms: 234, data: { ... }, error: { type: invalid_parameter, code: DATE_FORMAT_ERROR, message: 日期格式不正确, detail: 期望 YYYY-MM-DD收到 2024/1/5, retryable: true, suggestion: 请转换日期格式后重试 } }这个结构有几个关键设计点。retryable字段告诉上层这个失败能不能重试避免模型在不可重试的错误上浪费轮次。suggestion字段是给模型看的自然语言建议直接影响模型下一步决策的质量。duration_ms用于监控和性能分析后面排查问题时会用到。注意error.detail里不要放堆栈信息。堆栈是给开发者看的模型不需要而且堆栈里可能包含敏感路径信息。堆栈应该单独打到日志系统里通过 trace_id 关联。2.2 参数校验层的实现细节参数校验是拦截失败的第一道防线。我的做法是在 toolRun 入口处放一个校验中间件所有工具调用都先过这一层。import jsonschema def validate_params(tool_name, params, schema): try: jsonschema.validate(instanceparams, schemaschema) return None except jsonschema.ValidationError as e: field_path ..join(str(p) for p in e.absolute_path) return { type: invalid_parameter, code: SCHEMA_VALIDATION_FAILED, message: f参数 {field_path} 校验失败, detail: e.message, retryable: True, suggestion: f请检查参数 {field_path} 的格式和取值 }这里有个经验jsonschema库的报错信息有时候比较晦涩比如2024/1/5 does not match ^\\d{4}-\\d{2}-\\d{2}$。直接把这个丢给模型模型大概率能理解但如果你想让建议更友好可以针对常见错误类型做一层翻译。比如日期格式错误、枚举值不在范围内、必填字段缺失这三类占了参数错误的八成以上值得单独处理。2.3 超时与重试的工程实践环境类错误需要重试但重试不是无脑循环。我踩过的坑早期版本对超时错误直接重试三次结果遇到下游服务整体不可用时三次重试全部超时白白浪费了 30 秒用户等得想砸键盘。正确的做法是指数退避 抖动import random import time def retry_with_backoff(func, max_retries3, base_delay0.5): for attempt in range(max_retries): try: return func() except RetryableError as e: if attempt max_retries - 1: raise delay base_delay * (2 ** attempt) random.uniform(0, 0.1) time.sleep(delay)base_delay 设 0.5 秒三次重试的等待时间大约是 0.5s、1s、2s加上抖动避免多个请求同时重试造成惊群。对于限流类错误HTTP 429要读取响应头里的Retry-After按服务端指示的等待时间重试而不是用自己的退避策略。还有一个细节重试要在 toolRun 层内部完成不要把重试逻辑暴露给模型。模型不需要知道底层重试了几次它只需要看到一个最终结果。如果重试全部失败返回给模型的是一个已经标记为retryable: false的环境错误告诉它“该工具暂时不可用建议换用其他方式”。2.4 工具不存在与幻觉处理模型幻觉出一个不存在的工具这在 agent 开发里太常见了。尤其是工具数量多的时候模型容易把相似的工具名搞混或者凭空捏造一个听起来很合理的工具名。处理方式很直接在 toolRun 层维护一个工具注册表调用前先查表。不存在就返回逻辑错误{ status: failed, error: { type: logic_error, code: TOOL_NOT_FOUND, message: 工具 search_web_v2 不存在, detail: 当前可用工具search_web, query_weather, send_email, retryable: false, suggestion: 请从可用工具列表中选择最接近的是 search_web } }把可用工具列表直接放进错误信息里模型下一轮就能纠正。这个技巧实测非常有效比单纯说“工具不存在”的纠正率高出一大截。3. 失败数据如何驱动 Agent 自主恢复3.1 把失败信息拼进 context 的正确姿势工具返回失败后这条失败信息会作为 tool result 拼进对话历史。拼的时候要注意格式用结构化的 JSON 字符串而不是自然语言描述。原因很简单JSON 的字段边界清晰模型解析起来不容易出错。我见过有的实现把失败信息写成“调用工具 X 失败了原因是 Y你可以试试 Z”这种自然语言形式模型也能理解但信息密度低而且容易在长对话中被稀释。结构化 JSON 的好处是每个字段都有明确语义模型在后续推理中可以精确引用。拼进 context 的时候建议在 tool result 外面包一层标记比如tool_result toolquery_weather statusfailed {error: {type: invalid_parameter, ...}} /tool_result这样模型能清楚知道这是工具返回的结果而不是用户说的话。3.2 模型自主恢复的三种典型模式观察大量实际运行日志后我发现模型面对工具失败时自主恢复主要有三种模式模式一参数修正重试。这是最常见的。模型看到参数格式错误后自动修正参数再调一次。比如日期格式从2024/1/5改成2024-01-05。这种恢复的成功率很高前提是你的错误信息足够精确。模式二换工具。当某个工具不可用时模型会尝试用功能相近的工具替代。比如搜索工具 A 超时它会试搜索工具 B。这要求你的错误信息里包含可用工具列表否则模型不知道该换什么。模式三降级处理。当所有工具都失败时模型会基于已有信息给出一个不完美但可用的回答并告知用户哪些信息没能获取到。这是最后的手段但比直接报错终止要好得多。3.3 防止无限重试的熔断机制模型自主恢复有个风险它可能陷入“失败-重试-再失败-再重试”的死循环。我实测遇到过模型连续 8 次用同样的错误参数调用同一个工具每次都被同样的错误打回来但它就是不改。熔断机制是必须的。我的做法是在 toolRun 层记录每个工具在当前会话中的连续失败次数超过阈值比如 3 次就强制返回一个不可重试的错误并在 suggestion 里明确写“该工具已连续失败多次请停止重试改用其他方式或告知用户”。failure_counter {} def check_circuit_breaker(tool_name, session_id): key f{session_id}:{tool_name} count failure_counter.get(key, 0) if count 3: return { type: circuit_breaker, code: TOO_MANY_FAILURES, message: f工具 {tool_name} 连续失败 {count} 次已熔断, retryable: False, suggestion: 请停止重试该工具改用其他工具或直接回复用户 } return None熔断状态在工具成功调用后重置。这个机制救过我很多次尤其是在模型陷入局部最优解的时候。4. 可观测性与调试实战4.1 失败数据的日志规范失败数据不仅要返回给模型还要完整记录到日志系统。日志的字段设计要和返回结构对齐方便关联分析。我通常记录这些字段trace_id、session_id、tool_name、params脱敏后、error_type、error_code、duration_ms、retry_count、timestamp。params 脱敏很重要。工具参数里可能包含用户隐私信息、API key、文件路径等。我的做法是维护一个敏感字段列表记录日志时把这些字段的值替换成***。日志级别要区分参数错误用 WARNING环境错误用 WARNING权限错误用 ERROR逻辑错误用 ERROR。这样在监控面板上能快速定位问题类型分布。4.2 常见问题速查表现象可能原因排查方向解决方案模型反复用错误参数重试错误信息不够具体检查 error.detail 是否精确到字段细化错误信息加 suggestion工具调用全部超时下游服务不可用或网络问题检查下游健康状态和网络连通性启用熔断返回不可重试错误模型幻觉工具名工具列表太长或命名相似检查工具注册表命名规范统一命名前缀错误信息带可用列表失败后 agent 直接终止异常未被捕获检查 toolRun 是否有全局异常处理所有异常包装成结构化失败返回重试次数过多拖慢响应退避策略不合理检查 base_delay 和 max_retries调整退避参数设置总超时上限4.3 一个真实的排查案例有一次线上反馈说某个 agent 处理用户请求特别慢平均要 40 多秒。我拉日志一看发现大量工具调用在重试。具体是一个查询数据库的工具因为连接池配置太小高并发时大量请求排队超时然后触发重试重试又加剧了连接池压力形成恶性循环。排查过程先看 duration_ms 的分布发现大量 5000ms 以上的调用超时阈值设的 5 秒再看 retry_count平均每个请求重试 2.3 次最后看 error_type全是 timeout。定位到连接池问题后把连接池从 10 调到 50同时把该工具的重试策略从 3 次改成 1 次因为数据库查询重试意义不大不如快速失败让模型换策略问题解决。这个案例的教训是重试策略要因工具而异。幂等的查询类工具可以重试但重试次数不宜多非幂等的写入类工具要谨慎重试最好不重试避免重复写入。4.4 监控指标的设计失败数据是宝贵的监控素材。我通常关注这几个指标工具失败率按工具维度统计失败率突然升高的工具要重点排查失败类型分布参数错误占比高说明 schema 描述不清或模型能力不足环境错误占比高说明下游不稳定平均恢复轮次从失败到成功的平均轮次反映模型的自主恢复能力熔断触发次数频繁触发说明模型陷入了某种固定错误模式这些指标做成看板能提前发现很多问题。比如参数错误率突然上升往往是某个工具的 schema 改了但模型侧的描述没同步更新。5. 从失败数据到系统进化5.1 用失败数据反哺 schema 优化失败数据最大的价值在于指导 schema 迭代。我每个月会拉一次参数错误的聚合分析看看哪些字段最容易出错。如果某个字段的错误率特别高通常意味着 schema 描述有问题。举个例子有个工具的priority字段schema 里写的是{type: integer, minimum: 1, maximum: 5}但模型经常传 0 或者 10。分析发现模型对这个字段的语义理解有偏差它以为 0 表示最低优先级10 表示最高。后来我在字段描述里明确写了“1 表示最高优先级5 表示最低优先级”错误率立刻降下来了。这就是失败数据驱动的 schema 优化用实际错误反推描述歧义针对性改进。5.2 失败模式库的积累把常见的失败模式整理成库新项目可以直接复用处理策略。我的失败模式库里目前有几十条每条包含错误特征、根因分析、处理策略、返回给模型的建议模板。比如“日期格式错误”这条特征error_code DATE_FORMAT_ERROR 根因模型生成的日期格式与 schema pattern 不匹配 处理策略返回精确的格式要求附带一个正确示例 建议模板日期格式应为 YYYY-MM-DD例如 2024-01-15。请转换后重试。这个库的价值在于新工具接入时不用从零设计错误处理直接匹配已有模式即可。而且随着库的积累覆盖的失败场景越来越全系统的健壮性自然就上来了。5.3 给模型看的错误信息与给人看的错误信息要分开这是一个容易被忽视的点。返回给模型的错误信息目标是让模型能自主恢复所以要包含 suggestion、retryable、可用替代方案等。而记录到日志、展示给开发者的错误信息目标是让人能快速定位问题所以要包含堆栈、trace_id、上下游链路信息。这两类信息不要混在一起。我见过有的实现把堆栈直接返回给模型结果模型被一堆文件路径和行号搞晕完全不知道该怎么恢复。正确的做法是在 toolRun 层做分流一份精简的、面向模型的错误结构返回给 agent 循环一份完整的、面向开发者的错误记录进日志系统。5.4 一个反直觉的经验最后分享一个反直觉的经验不要试图消除所有失败。早期我总想把工具失败率降到零后来发现这不现实也没必要。真实世界的工具调用必然有失败关键不是消除失败而是让失败变得“可恢复”。一个失败率 5% 但每次失败都能被模型自主恢复的系统比一个失败率 1% 但失败就终止的系统要好得多。前者用户几乎无感知后者用户会频繁遇到中断。所以资源应该花在提升失败信息的质量和恢复机制上而不是一味追求降低失败率。这个理念转变之后我的 agent 项目稳定性上了一个台阶。用户反馈从“经常报错”变成了“偶尔慢一点但都能出结果”体验完全不一样。
