AI Agent 工程化实战:工具描述、上下文管理与错误处理
1. 从会聊天的模型到能办事的Agent这道坎到底卡在哪很多人第一次接触 AI Agent 这个概念时脑子里浮现的画面是科幻电影里那种能自己思考、自己规划、自己动手的智能体。但真上手搭一个你会发现现实骨感得多——模型能跟你聊得头头是道可一旦让它去查个数据、调个接口、跑个流程它就开始胡言乱语要么编一个不存在的函数名要么把参数填得驴唇不对马嘴。这个落差不是模型不够聪明而是对话和执行之间隔着一整套工程化的东西。对话是开放式的语言生成执行是封闭式的状态流转。前者允许模糊后者要求精确。Agent 的本质就是把模型的模糊语言能力约束到一个精确的执行框架里。我见过太多人卡在这一步Demo 跑通了感觉良好一上真实任务就崩。崩的原因五花八门但归根结底就三类——工具描述没写清楚、上下文管理失控、错误处理形同虚设。这三件事听起来简单做起来全是细节而且每个细节都能让你调一整天。这篇内容适合谁看如果你已经用过大模型 API写过简单的对话应用现在想让模型真正动起来去完成一些自动化任务那接下来的内容就是为你准备的。我会从工具设计、上下文管理、错误恢复、多步规划这几个维度把 Agent 从玩具变成工具的关键节点一个个拆开讲。需要提前说明的是Agent 这个领域变化极快今天的最佳实践明天可能就被新方案取代。所以我不会给你一套死板的标准答案而是把背后的设计逻辑和取舍思路讲透让你能根据自己手头的模型能力和业务场景灵活调整。2. 工具描述写不好Agent 就是个摆设2.1 为什么工具描述比工具本身更重要很多人搭 Agent 的第一步是写工具函数写完就扔给模型觉得函数逻辑对了就行。这是个巨大的误区。对模型来说它看不到你的函数实现它只能看到你给它的工具描述。描述写得含糊模型就不知道什么时候该用、怎么用、参数填什么。我做过一个对比实验同一个查询天气的工具一份描述写获取天气信息另一份写根据城市名称查询该城市当前的气温、湿度和天气状况参数 city 为城市中文名如北京。前者模型十次有三次不调用或者调用时参数乱填后者调用准确率明显提升。工具没变变的只是描述。工具描述的本质是给模型看的 API 文档。你写文档时怎么让新同事快速上手就该怎么写工具描述。它需要回答三个问题这个工具做什么、什么时候用、参数怎么填。2.2 工具描述的三层结构我习惯把工具描述拆成三层来写实测下来模型理解准确率最高。第一层是功能概述一句话说清楚这个工具的核心能力。比如查询指定城市的实时天气就比天气工具好得多。概述里要包含动作和对象让模型能快速匹配用户意图。第二层是使用场景明确告诉模型什么情况下该调用这个工具。这一层最容易被忽略但恰恰最关键。因为模型经常在该不该调用上犯迷糊。你可以写当用户询问某地天气、气温、是否下雨时使用此工具这样模型就有了明确的触发条件。第三层是参数说明每个参数的类型、含义、格式、示例都要写清楚。特别是格式模型对格式极其敏感。如果参数是日期你要写格式为 YYYY-MM-DD如 2024-01-15如果是枚举值要把所有可选值列出来。# 工具描述示例以 JSON Schema 形式定义 weather_tool { name: get_weather, description: 查询指定城市的实时天气信息。当用户询问某地天气、气温、降水情况时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市中文名称如北京、上海不要带市字 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认摄氏度 } }, required: [city] } }2.3 参数设计的几个坑参数设计里有几个坑我踩过不止一次这里列出来帮你省时间。坑一参数太多太细。有些人恨不得把工具的所有配置项都暴露给模型结果模型面对十几个参数直接懵了要么漏填要么填错。我的经验是必填参数控制在三个以内其余全部给默认值。模型只需要填最核心的信息剩下的在函数内部处理。坑二参数名用缩写。你写q模型可能理解成 query也可能理解成 quantity。写city_name就比cn清楚得多。参数名要自解释别让模型猜。坑三没有参数校验。模型填的参数不一定合法你的工具函数必须做校验。城市名传了个空字符串、日期格式不对、枚举值传了不存在的选项——这些都要在函数里拦住返回明确的错误信息给模型让它有机会重试。提示工具函数返回的错误信息也要写给模型看。不要返回Error 500要返回城市名称不能为空请提供有效的城市中文名。模型看到具体错误才知道怎么改。2.4 工具数量与选择策略工具不是越多越好。我试过给 Agent 挂二十多个工具结果模型选择困难经常调错。后来精简到五六个核心工具准确率反而上去了。这里有个经验值单次对话中可用的工具控制在七个以内。超过这个数模型的工具选择准确率会明显下降。如果业务确实需要很多工具可以分组根据对话上下文动态加载相关的那一组。另外工具之间功能不要重叠。如果你有两个工具都能查天气模型就会纠结用哪个。功能重叠的工具要么合并要么在描述里明确区分使用场景。3. 上下文管理Agent 的记忆怎么管才不崩3.1 上下文窗口是稀缺资源Agent 跑多步任务时每一步的工具调用结果、模型的思考过程、用户的原始指令全都要塞进上下文窗口。几轮下来窗口就满了。窗口一满要么报错要么模型开始遗忘前面的关键信息。我见过最典型的翻车场景Agent 第一步查到了用户 ID第三步要用这个 ID 去查订单结果上下文里用户 ID 被挤掉了模型编了一个 ID 出来。这种错误极其隐蔽因为模型编的 ID 格式往往是对的不仔细看根本发现不了。上下文管理的核心思路是分层把信息按重要程度和时效性分开重要的常驻次要的按需加载。3.2 三层上下文结构我通常把 Agent 的上下文分成三层来管理。第一层是系统指令层包含 Agent 的角色设定、行为规范、可用工具列表。这一层内容固定每轮都完整保留。它相当于 Agent 的人格和操作手册不能丢。第二层是任务状态层记录当前任务的进度、已获取的关键信息、待办事项。这一层是动态更新的但只保留精华。比如用户 ID、订单号这种关键标识单独拎出来放在一个结构化的状态对象里不跟大段的对话历史混在一起。第三层是对话历史层包含用户消息、模型回复、工具调用记录。这一层最容易膨胀需要做压缩和截断。我的做法是保留最近 N 轮完整记录更早的记录做摘要压缩。# 上下文分层管理的简化示意 context { system: 你是一个订单查询助手可以调用工具查询订单状态..., task_state: { user_id: U12345, # 关键信息常驻 current_order: O67890, completed_steps: [验证用户, 查询订单列表] }, history: [ # 最近几轮完整保留 {role: user, content: 帮我查一下订单}, {role: assistant, content: ...}, # 更早的历史压缩成摘要 {role: system, content: [历史摘要] 用户已提供用户ID已查询到三个订单...} ] }3.3 摘要压缩的实操细节摘要压缩说起来简单做起来有几个细节要注意。摘要的时机不要等窗口快满了才压缩那样容易丢信息。我一般在历史记录超过十轮时就开始压缩最早的几轮。留出足够的缓冲空间。摘要的内容摘要不是简单概括而是提取关键实体和状态变更。比如用户提供了用户 ID U12345查询到订单 O67890 状态为已发货就比用户查询了订单有用得多。摘要要保留后续步骤可能用到的所有标识符和结论。摘要的生成可以用模型来生成摘要但要用一个独立的、简单的提示词不要让主 Agent 分心。摘要生成的质量直接影响后续步骤的准确性值得单独调优。注意摘要压缩是有损的。如果某个信息后续一定会用到不要指望摘要能完整保留直接放进任务状态层。摘要只用来保留可能有用的上下文氛围。3.4 工具结果的裁剪工具返回的结果往往很长比如一个查询接口返回了几十行 JSON。全塞进上下文是浪费。我的做法是在工具函数内部就做裁剪只返回模型需要的关键字段。比如查询订单的工具返回结果里可能有几十个字段但模型只需要订单号、状态、金额、时间这四个。那就在工具函数里只提取这四个字段返回。这样既省上下文又减少模型被无关信息干扰的概率。如果确实需要返回大量数据可以考虑分页或者让模型主动请求更多细节。但大多数场景下裁剪到关键字段就够了。4. 错误处理Agent 翻车时怎么救回来4.1 错误是常态不是异常传统程序里错误是异常情况需要特殊处理。但在 Agent 场景里错误是常态。模型会调错工具、填错参数、误解结果这些都是预期内的。你的错误处理机制不是以防万一而是日常必备。我刚开始搭 Agent 时工具调用失败就直接抛异常整个流程中断。后来发现这样根本没法用因为模型十次里总有一两次会出点小问题。正确的做法是把错误信息返回给模型让它自己修正。4.2 错误分类与应对策略错误大致分三类每类的应对方式不同。第一类是参数错误比如城市名传空、日期格式不对。这类错误最好处理直接把具体的错误原因返回给模型模型通常能自己改对。关键是错误信息要具体告诉模型哪个参数错了、错在哪、正确格式是什么。第二类是工具执行错误比如接口超时、服务不可用。这类错误模型自己解决不了需要重试或者降级。我的做法是工具函数内部做有限次重试重试还失败就返回一个明确的服务暂时不可用信息让模型决定是换工具还是告知用户。第三类是逻辑错误比如模型用错误的参数调用了正确的工具或者误解了工具返回的结果。这类错误最隐蔽也最难处理。我的经验是在关键步骤后加一个验证环节让模型自己检查结果是否合理。# 工具函数中的错误处理示例 def get_weather(city: str, unit: str celsius): # 参数校验 if not city or not city.strip(): return {error: 城市名称不能为空请提供有效的城市中文名} if unit not in [celsius, fahrenheit]: return {error: f温度单位 {unit} 不支持请使用 celsius 或 fahrenheit} try: # 实际调用带重试 result call_weather_api(city, unit, retries2) return {success: True, data: result} except TimeoutError: return {error: 天气服务响应超时请稍后重试或换一个城市查询} except Exception as e: return {error: f查询失败{str(e)}请检查城市名称是否正确}4.3 重试的边界重试不是无限次的。我一般设置最多两次重试而且重试之间要有策略变化。比如第一次用原参数重试第二次换一个等价的工具或者调整参数重试。无脑重试同样的操作大概率还是失败还浪费时间和 token。另外重试要有全局上限。如果整个任务已经重试了五次以上说明可能不是偶发问题而是任务本身有问题。这时候应该中断流程把情况报告给用户而不是继续死磕。4.4 让模型学会放弃这一点反直觉但很重要。Agent 需要知道什么时候该放弃。有些任务就是完不成的比如用户要查一个不存在的订单或者调用的服务就是挂了。这时候模型应该明确告诉用户我做不到原因是XXX而不是编一个假结果出来。我在系统指令里会明确写如果连续两次尝试后仍无法获取所需信息请如实告知用户当前遇到的困难不要编造数据。 这句话能显著减少模型胡编的情况。5. 多步规划把大任务拆成小步骤5.1 为什么模型不擅长长链条规划让模型一次性规划一个十步的任务它往往会在中间某步跑偏。这不是模型能力不够而是长链条规划对上下文一致性和逻辑连贯性要求太高模型在生成过程中容易忘记前面的约束。我的做法是把规划粒度做细不要一次规划十步而是每次只规划下一步。走完一步根据结果再规划下一步。这样每步的决策都基于最新的上下文准确率高得多。5.2 ReAct 模式的实操要点ReAct推理行动是目前最常用的 Agent 执行模式。核心循环是思考当前状态、决定下一步行动、执行行动、观察结果、继续循环。听起来简单但实操中有几个关键点。思考步骤要显式输出。让模型把推理过程写出来不仅方便调试也能提高后续步骤的准确性。模型在想的过程中会自我纠正如果直接让它输出行动反而容易出错。行动要具体到工具和参数。不要让模型输出查询用户信息这种模糊描述要让它输出结构化的调用指令包含工具名和完整参数。这样执行层才能准确执行。观察结果要反馈给模型。工具执行完结果要完整地返回给模型让它基于结果决定下一步。不要替模型做判断比如查询成功这种结论让模型自己得出。# ReAct 循环的简化伪代码 while not task_complete: # 1. 模型思考 thought model.think(context) # 2. 模型决定行动 action model.decide_action(thought, available_tools) # 3. 执行行动 if action.type tool_call: result execute_tool(action.tool_name, action.parameters) elif action.type final_answer: return action.answer # 4. 观察结果更新上下文 context.add_observation(result) # 5. 检查是否超步数限制 if step_count max_steps: return 任务步骤过多已中断5.3 步数限制与中断机制Agent 必须有步数上限。我见过 Agent 陷入死循环反复调用同一个工具几十次烧了一堆 token 什么也没干成。设置一个合理的最大步数比如十步或十五步超过就中断并报告。中断时要把当前状态和已完成的部分告诉用户让用户知道进行到哪了。不要直接抛一个任务失败那样用户完全不知道发生了什么。5.4 任务分解的粒度任务分解的粒度是个经验活。分得太粗单步太复杂模型搞不定分得太细步骤太多容易累积误差。我的经验是每个步骤对应一次工具调用或一次明确的判断。如果一个步骤需要调用两个工具那就拆成两步。另外步骤之间要有明确的依赖关系。后一步依赖前一步的输出这样模型知道为什么要按这个顺序走。如果步骤之间没有依赖那可能可以并行但并行会增加复杂度初期建议还是串行。6. 实测中那些文档不会告诉你的坑6.1 模型对否定指令的理解偏差系统指令里写不要编造数据模型有时候反而会过度谨慎该调用工具的时候不调用直接说我无法获取这个信息。后来我改成正面表述请通过调用工具获取真实数据如果工具无法提供再告知用户。 效果好了很多。模型对否定句的处理确实不如肯定句。写指令时尽量用要做什么代替不要做什么。6.2 工具返回格式的一致性工具返回的数据格式要统一。有的工具返回{data: {...}}有的返回{result: {...}}模型在解析时容易混淆。我后来统一了所有工具的返回格式成功时返回{success: true, data: ...}失败时返回{success: false, error: ...}。格式统一后模型对结果的理解准确率明显提升。6.3 温度参数的取舍Agent 场景下温度参数建议调低。创意写作需要高温但 Agent 执行任务需要的是稳定和准确。我一般把温度设在 0.1 到 0.3 之间。温度太高模型容易发挥创意编造工具名或者参数。6.4 日志记录的重要性Agent 的调试比普通程序难得多因为中间过程是模型生成的不可预测。完整的日志记录是排查问题的唯一依靠。我一般记录每一轮的完整上下文、模型的思考输出、工具调用参数和返回结果。出问题时回看日志基本都能定位到是哪一步跑偏了。日志要包含时间戳和步骤编号方便按顺序查看。如果 token 消耗是个关注点日志里也记上每轮的 token 用量方便优化。6.5 用户预期的管理最后说一个非技术但很重要的点管理用户预期。Agent 不是万能的它会在某些任务上失败。如果用户以为 Agent 什么都能干失败时就会很失望。我在产品层面会明确告诉用户 Agent 能做什么、不能做什么以及在什么情况下可能需要人工介入。这个预期管理不仅影响用户体验也影响你自己对 Agent 的调优方向。知道边界在哪才知道该往哪个方向优化。7. 从能跑到好用持续迭代的思路Agent 搭到能跑通 Demo 只是起点真正难的是让它稳定可靠地处理真实任务。我的迭代思路是先收集失败案例再针对性优化。每次 Agent 失败我都把完整的日志存下来定期回顾。失败案例积累多了就能看出模式是某类工具描述不清楚还是某类任务规划有问题还是某个环节的错误处理缺失。针对模式去改比盲目调提示词有效得多。另外Agent 的评估不能只看单次成功率。多步任务的成功率是逐步衰减的如果每步成功率是 90%十步下来整体成功率就只有 35%。所以优化时要关注每一步的成功率而不是只看最终结果。把每步的成功率从 90% 提到 95%整体成功率就能从 35% 提到 60%提升非常可观。这个领域没有一劳永逸的方案模型在变工具在变业务需求也在变。保持对失败案例的敏感持续小步迭代比追求一个完美的初始设计更实际。