ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从原理到实践:Agent工具调用实现与排错全解析

从原理到实践:Agent工具调用实现与排错全解析

最早做Agent的时候,一直觉得模型像个"嘴强王者":你说什么它都接得上,逻辑也清楚,但真要它干点实事——查个天气、翻一下数据库、调个外部API——它就只会给你出主意,然后让你自己去操作。后来我把工具调用接上之后,整个Agent才算是真正长了"手脚"。这一篇我就把这块内容完整展开:从原理到一次完整实现,再到我实际踩过的坑,都给捋一遍。

这篇适合两类人:一类是刚接触Agent开发,想把"只会聊天"的Demo变成"能办实事"的助手的人;另一类是已经接上了工具调用,但发现模型老是不按预期选工具、经常报错、不知道从哪里排查的人。全文不涉及某个特定平台,代码风格以目前主流大模型都支持的 Function Calling(工具调用)协议为准,你换成自己用的平台SDK基本能直接平移。

1. 为什么Agent必须长出"手脚"

1.1 聊天机器人与Agent的分界线,其实就是工具调用

很多人对Agent有一个误解:觉得"它能自己思考,所以它就是Agent"。但实际上,大模型本身做的事情非常单一——根据输入文本,预测下一个Token,输出一段文字。它没有真正"执行"任何外部动作:不会真的发起网络请求,不会真的去查数据库,更不会帮你订票或者删文件。你问它"北京今天冷不冷",它可以告诉你"建议穿羽绒服",但它没法替你确认现在到底几度。

那什么才是能干活儿的Agent?我的理解是:它能对外部世界产生真实影响。它读取了一个文件、写入了一条记录、调用了一个搜索接口、操作了一次表格……这些动作,全部需要通过工具调用这个口子去完成。所以我说它是聊天机器人和Agent之间的分界线:没有工具的模型只能提供"建议",有工具的Agent才能提供"结果"。

一个很直观的对比。纯聊天场景下,用户问"我们部门的项目周报该写了",模型能输出一篇周报模板;但如果这个Agent接上了一个"读取本月项目进度"的工具,它就能先把真实进度拉出来,再基于真实数据生成周报草稿。后者才是做事的形态。判断一个Agent雏形成不成熟,先别问它聊得好不好,先问它能不能自己把一件事做完——这个能力基本取决于工具调用设计得怎么样。

1.2 工具调用的本质:模型负责"点菜",应用负责"做菜"

这里有一个必须理清的概念:工具调用并不是"模型自己去执行工具",而是模型按照一种约定好的格式,输出"我要调用哪个工具、传什么参数"的结构化决定。真正去执行函数的,是模型之外的代码。

打个比方。你去一家餐厅,你不认识后厨的人,也不需要亲自生火做饭。你只需要看清楚菜单,告诉服务员"我要一道鱼香肉丝,少辣",剩下的事情由后厨完成,最后菜端上来。在工具调用的架构里,模型就是那个"点菜的顾客",它读到用户的请求后,对照工具清单(菜单),输出一个"我想点这个"的结构化指令;应用层的代码就是"后厨",它解析这个指令,真正运行对应的函数;最后把函数的执行结果(做好的菜)再传回给模型,模型基于结果组织语言回复用户。

这个设计巧妙的地方在于:模型的强项是"理解语义、拆解意图",而不是"稳定执行一段精确的程序"。如果让模型直接输出一大段代码去运行,出错的概率太高;但如果让模型只是从有限工具集合里挑一个,并传几个键值对参数,可靠性和安全性就可控多了。所以工具调用的本质,是把"决策"和"执行"彻底分离。模型负责判别需要什么信息、选哪个工具、参数填什么;代码负责真正执行、获取真实数据、返回结果。这两者通过一套标准化的消息结构完成衔接。

理解这一点之后,你再看后面所有的代码和报错,都会清晰很多。各种诡异问题的根源,大多是"模型以为点了一个菜,后厨却找不到这道菜"或者"后厨做出来的东西,没有按规范端回给服务员"。

2. 核心机制拆解:一次工具调用如何完成

2.1 完整生命周期:从提问到答案的6个环节

一次标准的工具调用,完整链路如下:

  1. 应用把用户消息、工具定义清单一起发送给模型;
  2. 模型阅读消息,判断是否需要用工具。如果不需要,直接正常回答;如果需要,返回一个名为tool_calls的字段,里面包含工具名和 JSON 格式的参数;
  3. 应用收到tool_calls后,解析出函数名和参数,并在自己的代码里执行对应的函数;
  4. 应用把函数执行结果,以tool角色的消息回传给模型;
  5. 模型结合最初的问题和工具返回的真实结果,生成面向用户的最终回答;
  6. 应用把最终回答展示给用户。

这个流程里最容易被忽略的是第 5 步。你绝对不能在第 4 步往消息列表里塞一条tool内容就算完事——你需要再发起一次模型请求,让模型"看着工具结果说话"。换句话说,工具调用实际上是一个"多次请求-响应"的循环,而不是一次搞定。

为什么非要再问一次模型?因为函数返回的是结构化数据,比如天气接口返回的是 JSON 里的一堆温度、湿度、风力字段。这些字段直接抛给用户会非常难懂,必须由模型整理成自然的回答:"北京当前气温 5 度,湿度 40%,西北风 3 级,体感偏冷,建议穿羽绒服。"这个整理过程只能交给模型完成。我见过新手最容易犯的错,就是把工具返回结果print出来就不管了,用户看到一堆 JSON,体验瞬间崩掉。

2.2 工具定义的JSON Schema:先写好"菜单"

模型是怎么知道有哪些工具可以选的?答案是我们给它一份"菜单"——通常在 API 请求里通过tools参数传递一组 JSON Schema 结构。每个工具的基本结构如下:

{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当前天气情况,适合用户询问气温、降雨、风力时使用", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如北京、上海、广州" } }, "required": ["city"] } } }

别小看这几行 JSON,其中每个字段都有讲究。

name是函数名,模型会拿它跟用户的需求做语义匹配。起名尽量语义化,get_weather就比fn01好一万倍。

description是给模型看的"说明书",写清"什么时候该用这个工具",这句话直接影响触发率。比如上面例子里的"适合用户询问气温、降雨、风力时使用",模型看到"下雨吗"这类问题,就会大概率联想到这个工具。如果你只写"查询天气",触发率会差很多。

parameters里的properties定义了参数格式。参数名要语义清晰,类型和描述要准确。模型不理解业务逻辑,它唯一的依据就是参数名和说明文字。你在里面写一个字段叫city,描述"城市名称",它就会老老实实把城市名填进去;如果你写的是c或者p1,模型生成的参数就很容易是垃圾值。

有一类非常实用的写法是补充enum(枚举)来约束参数取值,比如温度单位限定为"celsius"或"fahrenheit"。枚举能明显降低模型自由发挥的空间。但注意,枚举不能写得过于死板,如果用户问的是"北京今天好穿什么",模型还是得根据城市判断温度,它需要的是一个城市参数,而不是一个固定选项。工具参数能设计得宽松就宽松,宁可少一点限制,让函数内部做校验,也不要让 Schema 把模型的路堵死——这个话题后面单独展开。

3. 从零到一:实现一个会调用工具的最小Agent

3.1 环境准备与基础调用骨架

在动笔之前,先说清楚代码的假想环境。现在各家大模型平台的 SDK 虽然品牌名不同,但接口风格基本都统一到了 Function Calling 这套协议:创建客户端、发 messages、带 tools、收 tool_calls。下面的示例里我用client.chat.completions.create(...)这套调用方式演示,你换成自己平台对应的 SDK 写法就行。

第一步先包装一个最基础的对话函数,它负责接收消息和工具列表,返回模型响应。这是后面所有例子的地基:

import json MODEL_NAME = "your-model-name" def call_model(client, messages, tools=None): kwargs = { "model": MODEL_NAME, "messages": messages, } if tools: kwargs["tools"] = tools resp = client.chat.completions.create(**kwargs) return resp.choices[0].message

然后我定义一套工具分发的入口函数。所有工具函数统一收一个**kwargs字典,这样模型返回的参数字典可以直接传进去:

def dispatch_tool(function_name, arguments): """根据模型选择的工具名,调用本地的实际函数""" if function_name == "get_weather": return get_weather(**arguments) if function_name == "get_onboard_date": return get_onboard_date(**arguments) # 还有其他工具就继续加分支 raise ValueError(f"未知工具: {function_name}")

实际的工具函数写在下面。这种统一入口的做法好处是主流程代码不用改,每加一个新工具,只需要在dispatch_tool里多一行分支。工具多了以后可以考虑用注册表模式,但最早期没必要上框架,多几个if完全够用且好排查。

3.2 第一个工具:查询天气

这是最经典的示例,覆盖了工具调用的所有核心环节。先写工具函数本身:

def get_weather(city: str): """模拟查询天气,真实场景这里会调天气API""" # 这里只是演示数据结构,不要把这个结果当真实天气 weather_map = { "北京": {"temperature": 5, "humidity": 40, "wind": "西北风3级", "desc": "晴"}, "上海": {"temperature": 12, "humidity": 65, "wind": "东风2级", "desc": "多云"}, "广州": {"temperature": 20, "humidity": 80, "wind": "南风1级", "desc": "小雨"}, } return weather_map.get(city, {"temperature": None, "desc": "暂不支持该城市"})

工具函数本身很简单,但在真实项目里,这里应该是调用外部天气 API、解析返回结果、再提炼出必要字段。这里有个设计原则:工具的返回值要尽量简洁、结构化。等下我们会在第 4 章详细讲。现在先把完整流程跑通。

给模型发送请求时的tools定义,就是我在 2.2 节写的那份 JSON Schema。把整个流程串起来:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当前天气情况,适合用户询问气温、降雨、风力时使用", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如北京、上海、广州" } }, "required": ["city"] } } } ] messages = [ {"role": "system", "content": "你是一个生活助手,回答用户问题时尽量简洁自然。"}, {"role": "user", "content": "北京现在天气怎么样?要不要穿羽绒服?"} ] # 第一轮:拿到模型消息 assistant_message = call_model(client, messages, tools) print("模型初步响应:", assistant_message)

如果模型判定需要调用天气工具,assistant_message.tool_calls就会有内容。我们把工具名和参数打印出来看:

tool_call = assistant_message.tool_calls[0] print("工具名:", tool_call.function.name) print("参数:", tool_call.function.arguments)

正常情况下,输出是:

工具名: get_weather 参数: {"city": "北京"}

注意arguments是一个字符串形式的 JSON,不是字典对象。需要用json.loads解析。很多踩坑场景都是漏了这一步直接拿去调用函数,导致类型报错。接着执行工具,把结果拼装成tool响应消息,再让模型生成最终回答:

messages.append(assistant_message) parsed_args = json.loads(tool_call.function.arguments or "{}") tool_result = dispatch_tool(tool_call.function.name, parsed_args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(tool_result, ensure_ascii=False) }) final_message = call_model(client, messages, tools) print("最终回答:", final_message.content)

两个细节必须强调。第一,要给消息列表里追加assistant_message,也就是模型那条带tool_calls的消息本身。如果我们不把它放进去,后一次请求时模型就不知道"刚才它自己已经决定调用过工具"了,它的上下文是断裂的。第二,content字段必须传字符串,所以工具返回的字典要用json.dumps序列化。直接把 Python dict 塞进去会让大部分 SDK 直接报错。

到这里,一个工具调用的最小闭环就完成了。第一轮模型决策,应用执行,第二轮模型总结。之后每次需要工具,重复这个循环即可。

3.3 多工具与路由分发:模拟一个内部信息助手

天气示例能跑通之后,多工具的挑战就来了。我拿一个模拟场景来说:做一个 HR 助手的雏形,它需要支持查询员工入职日期、计算工龄、查询当前项目进度。用户问的问题往往需要串联多个工具才能回答。

先定义三个工具函数:

def get_onboard_date(employee_name: str): """模拟查询员工入职日期""" employees = { "张三": "2021-03-15", "李四": "2019-07-01", "王五": "2023-01-10", } date = employees.get(employee_name) if not date: return {"error": "查无此人"} return {"employee_name": employee_name, "onboard_date": date} def calculate_years_of_service(employee_name: str): """模拟根据入职日期计算工龄,真实场景应该查库或调用HR系统""" date_str = get_onboard_date(employee_name).get("onboard_date") if not date_str: return {"error": "查无此人,无法计算工龄"} # 简化演示:以固定日期计算工龄 from datetime import date, datetime onboard = datetime.strptime(date_str, "%Y-%m-%d").date() today = date(2025, 1, 1) years = today.year - onboard.year - ((today.month, today.day) < (onboard.month, onboard.day)) return {"employee_name": employee_name, "years_of_service": years} def get_project_status(project_name: str): """模拟查询项目状态""" projects = { "某跨平台系统": {"status": "开发中", "progress": "65%", "owner": "李四"}, "某图像处理Demo": {"status": "已完成", "progress": "100%", "owner": "王五"}, "模拟项目X": {"status": "测试中", "progress": "85%", "owner": "张三"}, } return projects.get(project_name, {"error": "找不到该项目"})

然后把这些工具都放进tools列表里,每个都写清楚 name、description、parameters。在这个模拟助手的使用场景里,我实际调过这样的多工具链路:用户问"某跨平台系统这个项目谁负责的?他入职多久了?"

这个问题的完整处理路径应该是:先用get_project_status查出项目负责人,再拿负责人名字去调get_onboard_date或者calculate_years_of_service。模型在第一个回答里可能只调用了get_project_status;然后我们继续推送结果,模型又发现"负责人有了,但还缺他的入职日期",于是发起第二次工具调用。

所以主流程必须做成一个循环,而不是只调一次就结束。伪代码如下:

def run_agent_loop(client, messages, tools, max_rounds=5): for _ in range(max_rounds): assistant_message = call_model(client, messages, tools) messages.append(assistant_message) if not getattr(assistant_message, "tool_calls", None): # 没有工具调用,说明模型已经可以直接回答用户了 return assistant_message.content for tool_call in assistant_message.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments or "{}") tool_result = dispatch_tool(fn_name, fn_args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(tool_result, ensure_ascii=False) }) return "已达到最大工具调用轮次,提前结束。"

循环是关键。没有这个循环,模型只能做"一步到位的工具调用",一旦遇到需要连查两个工具的场景就废了。同时max_rounds必须存在,防止模型进入死循环——比如它反复调用某个工具但参数总不对,或者工具结果没有消除它的疑问,它就会一直在那转圈。我一般把最大轮次设在 5 到 8 之间,既能解决真实的连续推理需求,又不会让成本和延迟失控。

3.4 并行调用:一次同时查多个城市

工具调用发展到后期,模型不再满足于一次只调一个工具。用户问"北京和上海穿什么衣服合适"时,明明两个城市的天气互不影响,完全可以同时查。如果应用还是一次只能处理一个工具,第二轮如果还有别的信息需求,那延迟就会线性增加,体验很差。

现在的协议普遍支持一个响应里返回多个tool_calls。在代码上,只需要把for tool_call in assistant_message.tool_calls:这个循环换成先批量执行、再统一回传结果:

# 第一遍:先执行所有工具,得到结果 results = [] for tool_call in assistant_message.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments or "{}") tool_result = dispatch_tool(fn_name, fn_args) results.append((tool_call.id, tool_result)) # 第二遍:把所有结果按 id 回传 for tool_call_id, tool_result in results: messages.append({ "role": "tool", "tool_call_id": tool_call_id, "content": json.dumps(tool_result, ensure_ascii=False) })

注意,回传时必须保持tool_call_id和模型给出的值一一对应。一个响应里可能有call_01、call_02两个工具调用,你回传结果时把结果挂错 ID,模型就会分不清哪个结果对应哪个城市,最后给出的回答大概率是张冠李戴。我在前期踩过这个坑,当时查两个城市,第一个城市的湿度被安到了第二个城市头上,用户一眼就看出来不对。

4. 工具设计的原则与进阶技巧

4.1 描述是隐性提示词:直接决定触发率和准确率

很多人都把精力花在系统提示词上反复调教,但忽略了工具列表其实就是一份非常关键的隐性提示词。模型在你给它的一大堆工具里筛选用哪个,完全靠 name 和 description 做语义匹配。description 写得好不好,直接决定了该调用的时候它会不会乱掉、不该调用的时候它会不会硬来。

我的经验是,第三方工具的 description 要回答三个问题:

  • 这个工具是干嘛的?(一句话说清能力边界)
  • 什么时候该用它?(给出典型的用户问题示例)
  • 主要参数是什么、有没有特殊要求?

举个例子。一个查订单状态的工具,description 如果只写"查询订单",模型触发准确率会很不稳定;如果写成"查询订单的当前状态、物流进度、预计送达时间。当用户询问订单到哪了、发货没有、什么时候能到时使用。参数 order_id 为订单号,支持 20 位数字形式",效果明显更好。

我见过某个项目里,模型老是把"查天气"工具用在"查空气质量"的问题上。排查下来发现,天气工具的 description 写了"查询城市天气和空气指数",把空气质量这个场景也包进去了,模型自然觉得它能干这个事。后来把空气相关的描述从天气工具里拆出去,模型才停止误用。所以工具之间的描述边界一定要清晰,别让两个工具在语义上重叠,否则模型的选择就是随机行为。

4.2 参数放得宽,返回值收得紧

工具的参数 Schema 有两个相反方向的要求。对输入,要宽松;对输出,要紧凑。

输入宽松的意思是,能用string就尽量别搞复杂结构,能用简单枚举就别搞正则表达式。模型生成参数字段本来就容易出现不确定性,你若在 Schema 里写一个嵌套三层的对象,它很容易生成缺字段、类型错误的结构,函数一收到就是AttributeError。宁可让它传一个自由字符串,然后在你的工具函数内部去解析、校验、容错,你会发现整体成功率反而更高。

返回值紧凑的意思是,工具函数不要返回一坨完整的业务对象给模型。还是那句话,模型的上下文窗口是有限的,而且注意力会被大量无用字段稀释。比如查一个用户信息,数据库里有 40 个字段,但模型只需要知道"部门、入职时间、状态",那你就在工具函数里先取这三个字段再返回,别把 40 个字段全塞回去。

真实项目中我遇到过工具返回一段 8000 字的日志文本,模型收到之后直接开始"看不懂、复述、瞎编",而且那一次请求的费用因为输入 token 暴增而变得很难看。从那以后,我所有工具的返回逻辑都会有一个"提炼层":要么只返回核心字段,要么截断超长文本并附上摘要,要么允许调用方传参控制返回条数。工具是给模型供给信息的管道,不是 dump 数据的仓库。

4.3 安全边界:把"能调用"和"能授权"分开

工具调用赋予 Agent 的实际执行能力,同时也意味着安全风险。一个能调数据库的工具、一个能发文件的工具、一个能执行系统命令的工具,一旦被恶意提示词诱导,影响范围可能远超预期。

在给任何 Agent 加工具之前,先想清楚权限边界。我的固定做法是三条:

  • 最小权限:只给它需要的最小操作集。如果一个工具只需要读,就不要用有写权限的账号来执行。数据库账号给只读权限、文件目录用独立工作区、外部 API 用专用 Token。
  • 白名单化:在dispatch_tool里只允许转向预先登记过的函数,凡是没登记的一律报错。不要写一个反射机制,结果模型传进来的os.system你也能调,那等于敞开了大门。
  • 高危操作确认:涉及删除、覆盖、转账、发布这类动作,要先让 Agent 生成"计划",由用户或人工审核确认后再执行。这一点在自动化流程里很容易被省掉,但省掉的代价是后来哭着补。

工具调用和普通函数调用不一样,普通函数调用是程序内部可控的,工具调用则是模型在替你决定"要不要触发某个外部动作"。模型的决策天生有陌生场景下不够稳定的问题,所以高风险动作必须引入人类确认环节,这是实操层面最值得遵守的一条纪律。

5. 常见问题与排错实录

5.1 模型该调用时不调用,不该调用时乱调用

先看两个我遇到的典型表现。该调用时不调用:用户问"上海天气如何",模型直接回答"根据我了解到的情况……"然后编造一个温度。不该调用时乱调用:用户就问了一句"你好",模型非要调天气工具,结果拿到上海温度然后回答"你好,上海今天 12 度"。

排查顺序我一般这么走:

  • 看工具描述。描述是否覆盖了用户问题里的关键词?有没有明确"什么时候不适用"?
  • 看参数 Schema。是不是某个必填参数模型无法从问题里推断出来,导致它干脆放弃调用?举个例子,工具必须有city,但用户说的是"帮我看看这里天气",没有明确城市名,模型就倾向不调用。此时应该允许city为空,并在函数内部做默认值处理。
  • 看多工具之间的重叠。是不是有两个工具的 description 高度相似,模型挑花了眼?
  • 看系统提示词。如果你在 system 里写了"不要使用不必要的工具"之类的话,模型会更加保守;想让它积极用工具,需要在提示词里显式授权。

改 description 是最快见效的。我调过的大多数误用问题,最后都落在描述太笼统或边界不清上。你在描述里把适用场景和排除场景都写出来,模型的选择会稳定很多。

5.2 解析tool_calls时报JSON错误

模型返回的arguments是字符串形式的 JSON,一般可以json.loads直接解析。但在非主流模型或特殊字符场景下,可能遇到解析失败:引号反了、布尔值大小写不对、多了尾逗号等。

我写了一个带兜底的解析函数,遇到异常时先把字符串里的危险片段打印出来人工确认,同时让工具函数收到一个空字典而不是直接崩溃:

def safe_parse_arguments(raw_args): try: return json.loads(raw_args or "{}") except json.JSONDecodeError: # 打印原始内容,方便定位问题;真实项目中接入日志系统 print("JSON解析失败,原始内容:", raw_args) return {}

这个方法不算完美,但它保证了主流程不崩。更彻底的办法是:如果某个模型平台的工具调用结构经常坏,优先换一个协议更稳定的主流模型,而不是在解析层做越来越多的修补。

5.3 上下文越滚越大,Agent死循环

循环调用是工具调用里很隐蔽的效率杀手。模型连续多轮都在调用工具,但每轮执行结果都没有真正促进回答推进,比如某工具连续返回空结果,模型却还在固执地再试一次。

死循环的防护手段我做了三件事:

  • 设置绝对最大轮次,比如 5~8 轮,超了就强制退出并告诉用户"当前问题太复杂,建议拆分";
  • 每轮调用前打印tool_call的函数名和参数摘要,放到日志里。一旦发现某工具被重复调用且参数几乎没变化,基本可以断定模型走进了死胡同;
  • 在工具返回结果里加上"是否真的找到答案"的标记。模型面对空结果时,要能收到一条明确消息:"没有找到匹配记录,请向用户说明现状,不要再次尝试。"有时候模型反复循环,就是因为工具结果模棱两可,它不知道算不算成功。

上下文膨胀还有一个来源:每轮工具结果都很大,几轮下来几千 token 就没了。尤其是工具返回一个长列表的场景,会话第 3 轮开始模型可能已经忘了初始指令。解决方案我在 4.2 里提过——工具内部先做提炼,返回紧凑结果。

5.4 工具结果"喂"回模型的格式错误

最后记录一个看着简单但很常见的低级错误:把工具结果回传给模型的时候,格式不对。协议要求的是tool角色的消息,并且包含正确的tool_call_id,content必须是一个字符串。

容易犯的问题有三个:

  • 忘记把原来的assistant_message追加到messages,模型上下文断裂;
  • tool_call_id拷贝错了,或者被复用,导致模型无法关联工具结果与先前的调用;
  • 直接把dict对象传给content,忘了序列化,SDK 直接抛异常。

我后来把所有"消息组装"逻辑集中到一个函数里,避免在业务代码里反复手拼消息列表。这在项目变大以后非常重要,因为 Agent 的可靠程度,很大程度上取决于消息历史是否干净、合规、不串味。

最后再说两句实操体会

工具调用做久了,最大的感受是它不只是"加一个函数"那么简单,更像是在给模型设计一套可用的外部器官。我把这个项目里积累的习惯分享给各位参考:工具一开始先加最刚需的三五个,别急着铺一大堆,因为工具列表越多,模型的选择负担越大,误用率也越高;每加一个工具都要自问一句——能不能复用已有的工具?其次,每一次误用都要当成一次数据反馈,去改 description 而不是改提示词硬压,我调过最明显的案例,只是把描述里加了"不包括XX场景"一句话,误用率直接降了一半多。

我给自己的调试流程固定成这样一个习惯:每次 Agent 输出异常时,先开日志,把模型的tool_calls原始返回打出来,看它到底选了什么、参数填了什么,再决定改哪里。很多事情不是模型笨,是我们给它提供的工具没有说清楚。

下一篇我会接着聊聊 Agent 的上下文窗口与记忆管理,怎么让工具调用结果在长对话里不丢失、不混乱。如果你在落地工具调用时还有别的怪问题,也欢迎随时交流。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进