☰
AI Agent 工程化实战:工具描述、上下文管理与错误处理
2026/10/13 9:49:15 网站建设 项目流程

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, retries=2) 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%,提升非常可观。

这个领域没有一劳永逸的方案,模型在变,工具在变,业务需求也在变。保持对失败案例的敏感,持续小步迭代,比追求一个完美的初始设计更实际。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询