
昨天有个朋友跑来问我Agent 到底是怎么调用工具的模型不是只会生成文字吗它凭什么知道该调哪个 API参数怎么填调用完拿到结果之后又是怎么顺着话头继续回答用户的这个问题问得特别好因为它正好戳中了 Agent 开发里最核心、也最容易让人绕晕的一环——工具调用与结果回填。很多人跑过框架示例、调过几轮 prompt但始终没搞清楚模型输出一段 JSON 之后到底发生了什么遇到 Agent 卡住、报错、答非所问就完全不知道从哪下手。这篇文章就用一个最小可跑的案例把整条链路拆开讲清楚适合正在学 Agent 开发、准备做自己的智能体项目的朋友。1. Agent 和普通对话有什么本质区别1.1 对话助手只会说Agent 会做传统聊天机器人或者说纯对话式 AI本质上就是你把问题发过去模型给你一段文字回复。它当然也能写诗、能讲道理、能解释概念但它的世界仅限于训练数据和上下文里写明的信息。你问它今天北京天气怎么样它要么说我无法实时获取天气信息要么凭训练记忆瞎编一个因为它的训练数据里根本没有今天的气象数据。Agent 不一样。Agent 的核心特征是能对外部世界采取行动它可以通过调用天气 API 拿到实时数据可以通过搜索引擎接口查最新资料可以执行一段 SQL 去查数据库可以调用代码解释器算一道题甚至可以调用另一个 Agent 去完成任务。这里的调用不是模型自己执行而是模型在对话里提出申请由工程代码去实际执行再把执行结果交还给模型。这就是 Agent 与普通对话的本质区别模型负责思考和决策外部代码负责行动和反馈两者循环往复直到问题解决。1.2 工具调用在 Agent 工作流里处于什么位置一个完整的 Agent 工作流通常可以拆成五步理解用户意图、规划任务拆解、选择工具、执行工具、整合结果。这里最容易被人忽略的一点是工具调用并不是独立的一步它夹在决策和整合之间是整个循环的发动机。我见过不少初学者画流程图画得很漂亮意图识别→任务规划→工具选择→执行→生成回答看着很顺。但实际写代码的时候才发现这五步根本不是五个独立的模块而是共用同一个模型、在同一个对话上下文中反复进行的。模型每输出一次内容要么是调工具的请求要么是给用户的最终回答程序要做的只是判断这次输出是哪种然后决定是去执行工具还是把结果返回给用户。理解了这一点后面所有代码就都顺了。2. 工具调用背后的核心原理一次完整的请求-决策-执行-反馈闭环2.1 模型不执行工具它只输出调用申请先把这个最关键的概念说透大模型本身不会执行任何工具它连最简单的加法都算不对或者说计算方式和我们不一样更不可能自己去访问网络、查数据库。当你在 API 里开启了 tool calling / function calling 功能后模型做的事其实是在回复中输出一段结构化的调用申请通常是一段 JSON里面写明工具名和参数。举个例子用户问北京天气怎么样模型内部会经历一个推理过程用户想知道天气→我有一个 get_weather 工具可以查天气→工具需要城市名参数→北京对应参数值是 beijing。然后它在回复里输出类似这样的内容{name: get_weather, arguments: {city: beijing}}。程序拿到这段 JSON 后才会真正去调用你写好的 get_weather 函数把 city 参数传进去执行完拿到结果。整个过程中模型始终只是个提出请求的人真正干活的是你的代码。这个边界如果不清楚后面排查问题时会非常痛苦——很多人以为是模型执行出错其实是自己代码没执行或者结果没正确回填。2.2 工具描述Tool Schema就是给模型看的使用说明书模型怎么知道有哪些工具、每个工具需要什么参数靠的就是你在请求里传给它的工具描述官方叫 tools 参数也有人叫 function schema。你可以把它理解成给模型的一份工具使用说明书每个工具都包含三个关键信息工具叫什么名字、这个工具是干什么的、需要哪些参数以及参数格式。这块是工具调用能不能成功的核心。工具描述写得太笼统模型就不知道该什么时候调参数说明写得不清晰模型就会传错参数或者漏传必填项。我见过最典型的错误是有人把 description 写成For internal use之类的话模型根本不知道这个工具能干嘛自然永远不调用它。描述应该用当用户想要查询某个城市的天气时使用务必填写城市中文名例如北京、上海这种明确、带触发条件和示例的写法。2.3 执行结果如何喂回给模型工具执行完成后结果必须以一种特殊格式回填到对话历史里这一环是整个继续回答的关键。在 OpenAI 兼容接口里这个结果是一条 role 为 tool 的消息并且必须携带 tool_call_id用来对应之前模型的某次工具调用请求。为什么需要这个 id因为一次对话里可能有多个工具并行调用模型需要知道哪条结果对应哪个请求。回填之后模型会看到完整的对话链用户问了什么→模型决定调哪个工具→工具返回了什么结果。基于这些信息它才能继续推理要么生成最终回答要么发现结果还不够继续发起下一轮工具调用。整个对话历史就像一场连续剧模型每次读到的上下文都包含最新的剧情进展。3. 从 0 到 1 手写一个带工具调用的 Agent3.1 最简方案不依赖框架直接用官方 API现在市面上有 LangChain、LlamaIndex、Dify、Coze 等各种框架和平台封装得很完善但我的建议是第一次学工具调用一定不要用框架就用最原始的 API 手写一遍循环。因为框架把底层逻辑都藏起来了模型输出、工具执行、结果回填这些关键动作你根本看不见出了问题也不知道是哪一环的锅。手写一遍哪怕代码丑一点你也能把整条链路刻在脑子里。下面我用 OpenAI 兼容接口的 Python SDK 为例这个接口已经是事实标准国内外很多大模型厂商的 API 都兼容它你只需要改 base_url 和 api_key 就能切换到其他模型。3.2 定义工具和执行函数先定义两个最简单的工具一个查天气一个做计算够用但不啰嗦import json from openai import OpenAI client OpenAI( api_key你的key, base_url你的接口地址 # 用兼容OpenAI接口的服务 ) tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气情况。当用户询问天气、温度、是否下雨等问题时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市中文名例如北京、上海、广州 } }, required: [city] } } }, { type: function, function: { name: calculate, description: 执行四则运算当用户提出数学计算需求时使用。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如12.5 * 4 2 } }, required: [expression] } } } ] def get_weather(city: str) - str: # 真实项目里这里会调用天气服务商的API # 这里用模拟数据演示 weather_map { 北京: 晴气温26℃东南风2级, 上海: 多云气温28℃湿度65%, 广州: 阵雨气温30℃体感较热 } return weather_map.get(city, f{city}暂无数据可以尝试其他城市) def calculate(expression: str) - str: # 注意生产环境请使用安全可靠的表达式解析库 # 不要直接用eval处理用户输入这里仅为演示 try: result eval(expression) return f{expression} {result} except Exception as e: return f计算失败{str(e)} # 工具名到函数的映射表 TOOL_MAP { get_weather: get_weather, calculate: calculate }这里有个容易被忽略的细节工具描述里一定要写清楚什么时候用、参数格式是什么、有没有示例值模型不是人它判断是否调用工具全靠这段描述。我在实际项目中会把 description 写到 50 字以上把触发条件、典型场景、注意事项全塞进去宁可啰嗦也不要含糊。3.3 主循环判断模型是想调用工具还是想直接回答核心的 Agent 循环其实就是一个 while 循环每次迭代做三件事让模型基于当前对话历史生成回复、判断回复里有没有工具调用请求、有就执行并回填结果然后继续循环没有就当作最终答案返回给用户。def run_agent(user_query: str, max_iterations: int 5): messages [ {role: system, content: 你是一个乐于助人的智能助手可以通过工具获取实时信息。}, {role: user, content: user_query} ] for step in range(max_iterations): response client.chat.completions.create( model你使用的模型名, messagesmessages, toolstools, ) assistant_message response.choices[0].message messages.append(assistant_message) # 没有工具调用请求说明这就是最终回答 if not assistant_message.tool_calls: return assistant_message.content print(f第{step 1}轮模型请求调用 {len(assistant_message.tool_calls)} 个工具) # 执行每一个工具调用 for tool_call in assistant_message.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) print(f - 调用 {fn_name}参数{fn_args}) # 从映射表里找到对应的执行函数 if fn_name in TOOL_MAP: result TOOL_MAP[fn_name](**fn_args) else: result f错误未知工具 {fn_name} # 关键一步把执行结果以tool角色回填到对话历史 messages.append({ role: tool, tool_call_id: tool_call.id, content: str(result) }) # 继续下一轮循环模型会基于最新的tool结果继续推理 return 已达到最大迭代次数任务未能完成请优化问题后重试。 if __name__ __main__: print(run_agent(北京天气怎么样顺便帮我算一下 123.45 * 678))你只要跑一遍这个代码就会看到完整的调用过程模型先输出一个工具调用申请程序打印调用 get_weather参数{city: 北京}然后把结果回填模型继续请求调 calculate再回填最后模型把两段信息整合成一句完整的回答返回。这整段代码的核心就一个判断if not assistant_message.tool_calls。它决定了 Agent 是继续干活还是交作业。我刚开始写的时候每次都要打印 messages 看全部上下文确认模型到底收到了什么强烈建议你也这么做比任何框架日志都直观。4. 实战中的关键参数与细节调优4.1 温度、轮数上限这些参数怎么设参数看起来不起眼实际影响却非常大。温度temperature控制模型输出的随机性做工具调用时我 Generally 设成 0 或者 0.2 以下。为什么因为工具调用本质上是结构化决策需要模型稳定地输出合法的 JSON 调用申请而不是发挥创造力。温度太高模型可能输出格式不规范、参数张冠李戴甚至凭空编一个工具名出来。最大迭代轮数max_iterations是另一个必须设的参数。工具调用的循环如果没有上限模型可能陷入调工具→看结果→再调工具的死循环。我见过最夸张的情况是一个 Agent 连续调了二十多轮工具最后返回的还是工具调用请求完全停不下来。给个 3 到 8 轮的上限比较合理既能完成多步骤任务又不会让用户等太久、花太多 token。4.2 工具结果太长、格式异常怎么办工具返回的结果五花八门可能是几十万字的网页正文可能是格式乱掉的 CSV可能是带转义字符的 JSON。这些内容直接回填给模型一方面浪费 token另一方面会严重干扰模型的注意力。我的习惯是回填之前先做一轮结果预处理。预处理通常包括三步截断、结构化、错误标注。超长文本截断到几千字以内把关键信息提取出来结构化数据转成简洁的文本描述比如 JSON 转成字段值的列表如果工具执行失败就把错误信息包装成标准格式返回给模型让模型知道刚才那步没成功你可以换个方式重试或如实告诉用户。这里有我踩过的一个坑刚开始我把工具错误直接抛异常整个 Agent 就崩了也就是很多人遇到的agent execution terminated due to error。后来改成把错误信息当作普通 tool 结果回填模型反而能优雅地处理比如告诉用户查询失败了请检查网络或者换个工具重试。记住一个原则工具可以失败但 Agent 不能因为工具失败而死掉。错误也是信息把它交还给模型让模型决定下一步。5. 常见问题与排查技巧实录5.1 模型不调用工具或乱调用工具这是出现频率最高的问题。模型面对该调工具的场景就是不调或者明明有合适的工具却偏要胡编乱造一个答案。我排查这类问题的顺序很固定先看工具描述是不是够清楚。描述里有没有写明白触发条件参数说明里有没有给示例我会把所有工具的描述打出来逐字读一遍看它是给同事看的需求文档还是给模型看的说明书。其次看模型本身是否支持工具调用不是所有模型都支持 function calling有些旧版模型你传了 tools 参数它也只会忽略掉。最后看是不是上下文里已经有足够信息如果模型觉得历史消息里已经有答案了它就不会再去调工具这时候你可以让系统提示词明确要求必须基于最新工具结果回答。5.2 参数解析失败和工具执行报错模型输出的 arguments 是一段 JSON 字符串但模型偶尔会生成不标准的 JSON比如漏掉引号、多了个逗号。json.loads 直接解析就会报错。我的处理方式是写一个容错解析函数去掉 markdown 代码块标记尝试修复常见格式问题解析不了再返回错误信息让模型自己修正。工具执行报错也是家常便饭API 超时、网络抖动、数据为空、第三方服务限流。之前说过了不要抛异常把错误信息转成字符串回填给模型并附上一些指导性的话比如该工具暂时不可用建议尝试其他方式。模型很擅长顺着提示调整方案你给它的错误信息越完整它下一步的决策就越靠谱。5.3 多工具协作和上下文管理复杂任务往往需要多个工具配合比如先查天气再根据天气推荐穿搭或者先搜索资料再总结成报告。模型会在一次回复里发起多个工具调用parallel tool calls程序要逐个执行把每条结果用正确的 tool_call_id 回填。这个 id 的对应关系出了问题模型就会把上海的天气当成北京天气的计算结果回答自然全乱了。上下文管理同样要注意。每轮工具调用都会往对话历史里追加两条消息几十轮下来上下文会越来越长既烧 token 又可能超出模型上下文窗口。我常用的做法是做历史消息压缩或裁剪把早期的工具调用记录折叠成一句摘要只保留最近几轮的完整细节。具体怎么折可以在系统提示词里让模型定期总结前面的关键信息。常见问题排查方向解决办法模型从不调用工具工具描述不清、模型不支持工具调用重写描述增加触发条件与示例更换支持 function calling 的模型模型胡编参数参数 schema 不严格、缺少校验增加 required 字段、枚举约束执行前校验参数工具结果被忽略结果格式混乱、太长预处理截断、结构化、突出关键信息Agent 陷入死循环缺少轮数上限、结果不明确设置 max_iterations让工具结果包含是否需要继续的判断依据工具报错导致任务中断异常直接抛出把错误转为 tool 消息回填让模型自主决策下一步多工具结果串线tool_call_id 对应错误确保每条 tool 消息都带正确 id打印日志核对6. 一些实操体会和后续扩展方向把最小案例跑通只是第一步真正要做一个能用的 Agent还有几个方向值得继续深入一是接入长期记忆让 Agent 在多次对话之间记住用户偏好和历史决策这属于 Agent 记忆体系里的长期记忆部分短期记忆靠对话上下文长期记忆需要向量数据库或者外部存储二是做工具调用的权限控制和结果校验生产环境一定要对模型传入的参数做白名单校验不要把 eval、shell 这类高危操作直接暴露给模型安全这根弦不能松三是可以用 ReAct 的思维链模式替代隐式的推理让模型把想法显式输出出来调试起来会直观得多。回到最开始那个问题Agent 怎么调用工具并根据执行结果继续回答答案就一句话——模型负责输出工具调用申请程序负责执行并把结果作为 tool 消息回填到对话历史里模型读取完整历史后继续推理如此循环直到它不再请求工具、直接给出最终回答。这个循环听起来简单但每一个环节都有它的细节和坑。我个人最大的体会是别急着上框架先把循环手写跑通把 messages 打出来亲眼看一遍模型到底收到了什么你对 Agent 的理解会瞬间上一个台阶。之后再去用框架你会发现那些封装好的 Agent 平台底层逻辑其实也就是这么回事。