AI Agent工具调用实战:第一关计算器闭环的细节与避坑

📅 发布时间:2026/10/17 3:32:03
AI Agent工具调用实战:第一关计算器闭环的细节与避坑
去年年底我跟着一套 AI agent 实战课程做练习第一关就把我从“会用提示词聊天”拉回到“让模型真正动手干活”的处境里。这套课程是某 AI 教育团队参考某实验室官方 agent 教程做的整体分为好几个渐进式任务我这次完成的 TASK1 就是一个非常典型的工具调用闭环模型在对话里主动判断需不需要调用外部函数再根据工具返回结果生成最终回答。不夸张地说这个任务让我把“预先给模型写死步骤”和“让模型自己决定调用步骤”之间的差别彻底弄明白了也更清楚一个 agent 从 demo 到能用之间到底隔了多少细节。如果你正在磕 agent 开发或者准备照着类似课程上手但还没找到节奏这篇应该能帮你少走几小时弯路。1. TASK1到底要我做什么从题目到验收条件1.1 拆解任务书一个“可调用工具的问答器”TASK1 的字面目标很简单让模型在回答用户问题时能够根据问题内容调用一个预设的数学计算工具然后基于计算结果给出答案。听起来像是“给模型加个计算器”但真正的验收点藏在三个细项里。第一模型必须自己判断什么情况需要调用工具比如问“1/0 等于多少”就应该触发除法工具而不是硬着头皮编一个结果第二工具的输入参数必须由模型从用户问句里抽取出来并填进标准的 JSON 结构不能靠人在代码里写死第三模型拿到工具返回结果后要能组织成自然语言回复而且多轮对话里每轮都要保持状态一致。这个任务放到真实开发里本质上就是一次模型和外部系统的“握手”。课程配套的模型 API 支持函数调用格式也就是你在请求里声明一个函数列表模型输出时可以选择输出普通文本也可以输出一个“我要调用某个函数参数如下”的特殊结构。我把它理解成提示词不再只是给模型描述语气而是给模型一张“能干事情的清单”模型根据用户需求挑一件来做。1.2 为什么第一关就要碰工具调用很多同学一开始觉得第一关应该先教提示词工程毕竟那是基础中的基础。但 TASK1 的设计偏偏反着来直接上工具调用。我后来琢磨了一下其实是因为 agent 的核心能力就是“决策 分解 使用工具”。如果连一次工具调用都没接通后面所有多步任务编排都是空中楼阁。这一关把最细的链路打通了用户输入 → 模型决策 → 参数抽取 → 工具执行 → 结果回填 → 模型总结回答。后面所有复杂 agent 都是在反复放大这个基础循环。而且第一关直接调用计算器还有一个好处工具本身简单可靠不需要联网、不需要权限、不存在外部环境不稳定。你可以把全部注意力放在“模型如何决定调用、调用格式如何正确”这些真正核心的问题上而不是被天气接口的 key 失效之类的事分心。等到后面任务换成搜索、数据库等真实工具时这个基础循环依然不会变。2. Agent最小闭环从API到循环的一次性搭通2.1 消息循环的主干逻辑我在动手前先画了一个最简单的循环把用户消息发给模型模型如果决定调用工具就返回一个工具调用请求代码里执行对应工具把工具结果作为一条新消息追加进会话再发给模型模型如果觉得信息够了就返回给用户的最终文本。整个循环可以限定次数比如最多 5 轮防止模型陷入无限调工具。核心代码我用 Python 写了个精简骨架方便调试from some_sdk import client, ToolCall messages [{role: user, content: user_input}] for _ in range(MAX_ITER): resp client.chat( modelmodel-v1, messagesmessages, tools[calculator_tool_schema], max_tokens1024, ) content resp.content[0] if content.type text: print(content.text) break if content.type tool_use: tool_name content.name tool_args content.input result run_tool(tool_name, tool_args) messages.append({ role: assistant, content: [ {type: tool_use, id: content.id, name: tool_name, input: tool_args} ] }) messages.append({ role: user, content: [ {type: tool_result, tool_use_id: content.id, content: result} ] })这段代码看起来并不复杂但有几个位置一旦写错整个循环就会当场崩掉。第一个就是工具调用结果的角色必须是user角色下的tool_result类型消息并且要用tool_use_id和模型发起的那次工具调用关联起来。第二个是循环里要把模型上一次返回的tool_use消息原样塞回上下文不能只塞工具结果否则模型缺少“我刚才请求了什么函数”的信息。第三个就是工具结果的content字段必须是字符串很多教程都忽略写str(result)导致模型拿到的是 JSON 对象解析直接报错。2.2 工具如何被“看见”和“使用”让模型看见工具的入口是一份工具 schema也就是给模型看的“函数说明书”。计算器这个任务我定义了两个工具calculator_tool_schema { name: calculate, description: 执行基础的加减乘除运算适合处理用户在问题中出现的数学计算需求。, input_schema: { type: object, properties: { expression: { type: string, description: 要计算的数学表达式例如 12 * 5 3 } }, required: [expression] } }第一次运行时我发现一个很有意思的现象模型大部分时候会调用calculate工具但偶尔会把“计算”理解成“给一个估算文本”。我反复对比后发现问题不在模型而在我的描述写得太窄。我最初写的是“执行加法、减法、乘法、除法运算参数为一个包含两个运算数的表达式”模型面对12 * 5 3就开始犯含糊因为它不是标准的两数运算。改成“执行基础算术运算支持长表达式”之后调用准确率一下就上来了。这个细节让我意识到工具描述本质上是在帮模型做“触发条件匹配”描述越贴近真实用户的自然表达模型越容易正确触发。2.3 第一次跑通的关键校验点跑通整个循环后我给自己列了一份校验清单每一条都对应一个实际输出。第一当用户输入“15 加 27 等于多少”时模型应该输出工具调用而不是直接写答案。第二当用户输入“你好介绍一下你自己”时模型应该走纯文本回复不调用任何工具。第三工具返回结果后模型最终回答要引用计算结果并且把“原始表达式”和“计算结果”说得一致不能自己篡改数字。第四连续两问交替使用计算和问候时模型不会因为历史消息里有工具调用而“惯性”调工具。这份清单里的第三条尤其重要。我见过很多人只验证“模型有没有调用工具”却忽略了“模型拿到结果后会不会正确转述”。第一次搭完我的模型在算完“15 加 27”后回答里写的是“42.0”但用户问的是“15 加 27”正确答案应该是“42”或者“42.0”都能接受重点是它没有写成“15 27 41”这种离谱结果。为了保险起见我在工具 result 字符串里会带上原始表达式和结果比如{expression: 1527, result: 42.0}模型参考的信息越结构化编错概率越低。3. 三个让我卡到凌晨的细节3.1 工具描述里的一句话直接决定模型用不用这部分我必须单独拿出来说因为实在太容易踩坑了。我一开始天真地认为工具 schema 只要字段齐全模型就能正确使用。但实际测下来description写得好不好直接影响模型是“爽快调用”还是“犹犹豫豫编答案”。我做了两组对比实验第一组 description 写“计算一个数学表达式”第二组写“当用户问题涉及数值计算时你需要调用此工具获得准确结果再基于结果回答用户”。同样是“25 * 4 ?”第一组模型的调用率只有 60%第二组稳定在 95% 以上。为什么会差这么多原因在于模型对工具的选择依赖“语义匹配”。第一组描述只说明了工具能干什么没有告诉模型“什么时候应该用它”。第二组描述补上了触发条件还把“获得准确结果”和“基于结果回答”两个动作串了起来相当于给了模型一个完整决策路径。后来我把所有工具描述都改成“用途 触发条件 使用后要做什么”三段式结构效果立竿见影。3.2 解析模型输出时的strip陷阱模型返回的工具调用参数是一个 JSON 对象大部分情况下可以直接用。但我在处理用户输入“100/0”时发现工具调用参数里的表达式会莫名其妙多出一个换行符或者有时参数名不是严格的expression而变成了带空格的形式。排查了很久才明白这不是模型抽风而是我在组装 prompt 时把用户输入直接拼进了一个模板片段模型为了对齐上下文格式会在参数值里加入一些看似多余的空白字符。解决办法是在执行工具前做一层参数清洗。我写了一个简单函数把参数值里的所有空白符压缩并去掉首尾空格再解析成标准字符串。另外我还养成了一个习惯所有传给calculate工具的表达式先做合法性校验只允许数字、四则运算符、小数点和括号其他字符一律拒绝。这样做不是为了防恶意输入而是计算表达式本身很容易因为模型输出了类似“100/0 ”带空格的东西而报错。清洗这一步虽然不起眼但能省下调试时候的大量崩溃瞬间。3.3 连续多轮调用工具时的状态保存TASK1 虽然只是单个工具但多轮对话场景下依然有个隐蔽问题当用户在第二轮问“如果换成 20 加 30 呢”模型需要结合历史消息里的“第一轮算过 15 加 27”这个上下文来理解新的表达式。如果把每一轮的工具调用和工具结果都完整塞进 messages模型能正确理解“换成”指代哪个背景。但一旦我只保留最后一轮消息模型就会把“20 加 30”当成一个孤立问题虽然结果也能算对但没法理解“换成”这个词的语义。这个问题的本质是工具调用消息也是对话上下文的一部分不能把它当成临时变量丢掉。我在循环里专门用一个列表维护所有历史消息包括工具调用和工具结果。实测下来只要上下文里有完整的工具调用链模型对指代的理解基本不会出错。还有一个小坑是有些 SDK 会在消息里保留自动生成的tool_use_id但如果你为了省 token 截断历史只留最后一条tool_result丢失了对应的tool_use模型就会因为找不到来源而产生幻觉。所以多轮状态下我建议要么全量保留要么干脆做摘要后再拼接新问题不要随便半截半留。4. TASK1的评测方法与测试用例设计4.1 测试集怎么构造完成基本功能后我又给自己加了一轮评测因为只靠几个手写 case 根本说明不了问题。我按四个维度构造了一个小测试集一共 20 条用例第一类是纯计算题覆盖加减乘除、小数、括号、连续运算第二类是混合意图比如“你会编程吗顺便算一下 12*8”第三类是无效表达式比如“计算一下今天的天气”这种无法用计算器解决的问题第四类是多轮切换从一个计算问题切换到主题问题再切回计算问题。每个用例我都提前标好了期望行为区分是“应该调用工具”还是“应该拒绝调用”。这看起来是在做测试其实是在逼我把“工具调度边界”想清楚。比如用户说“你能算一下明天会不会下雨吗”模型应该识别出计算器工具不适用而不是强行调用后给一个残缺答案。为了测试这种边界我甚至故意在测试集里加入了一些略带误导性的句子像“帮我算算运气好不好”模型如果错误调用工具就会被计为失败。4.2 从失败case反查问题根源跑完第一版测试集我的通过率只有 75%失败主要集中在三类。第一类是“混合意图”里的工具调用时机延迟模型先回复了“可以”再调用工具导致回答结构松散第二类是无效表达式问题模型会把“10 加上苹果”自动解析成10 apple然后报错第三类是连续计算场景下模型偶尔会在回答里直接写“约等于 42”而不是先调用工具。针对第一类问题出在我没有在系统提示里明确规定“只有在需要计算结果才能回答用户问题时才调用工具禁止先说话再补充调用”。加上这句后模型就直接输出工具调用不会再啰嗦。针对第二类我加强了工具描述的约束明确写“表达式只允许数字、运算符和括号如果用户输入包含非数字内容不要调用此工具”。针对第三类我怀疑是生成参数里的温度太高把确定性任务变成了发散任务。后来把temperature降到 0.2问题基本消失。这些经验说明一件事agent 问题不一定都靠模型版本解决很多根因藏在提示词和参数配置里。4.3 一个简易的自动评测脚本为了不让自己瞎猜改进效果我写了一个很小的自动评测脚本。它本质上就是一个回归测试器先读取 JSON 格式的测试用例循环发给同一个 agent 函数然后根据“是否触发工具调用”和“结果数值是否正确”两个维度输出通过或失败。test_suite [ {input: 15加27等于多少, should_call: True, answer: 42}, {input: 你是谁, should_call: False, answer: None}, {input: 10除以0呢, should_call: True, answer: DivError}, ] for case in test_suite: result run_agent_turn(case[input]) called result.get(tool_called, False) passed called case[should_call] if case[should_call] and passed: passed check_result(case[answer], result[final_text]) print(case[input], PASS if passed else FAIL)这个脚本帮我在后续每次修改 prompt 或解析逻辑后都能快速跑一遍回归。别看它简陋里面那个check_result是真正确认数值是否匹配的而不是简单判断字符串包含。因为模型可能输出“42.0”也可能输出“42”我统一转成浮点数再对比误差控制在 1e-6 以内。这也是我比较推荐的评测思路agent 类任务前期不要追求端到端打分先针对工具调用正确性做约束性测试比任何花哨评估都实在。5. 做完成TASK1后我对Agent开发的体会5.1 “输出格式”比“模型聪明程度”更决定体验以前我总觉得 agent 好不好用取决于模型智商高不高但完成 TASK1 后我发现在工具调用这个环节真正决定体验的反而是输出格式的稳定程度。只要模型输出的 tool_use 结构有一点偏差比如字段名大小写不同、参数类型不对整个循环就会断掉。而那些让模型看起来“很聪明”的表现比如自动判断不合适的输入并拒绝调用工具其实大多可以通过精心编写的系统提示和工具描述实现。所以我现在设计 agent 的第一原则变了先锁死输入输出协议再优化提示词最后才考虑换更聪明的模型。工具调用说到底是一种结构化通信模型只是这个通信链路里负责“决策”的脑但舌头是协议协议不清晰再聪明的大脑也会结巴。5.2 下一步扩展从单工具到任务编排TASK1 只用了计算器一个工具看起来简单但它已经把 agent 的基本血液循环建立起来了。后面如果要扩展我脑子里已经有几条路线第一加入更多工具比如天气查询、数据库检索让模型在多个工具之间选择第二加入工具结果的自动格式化让模型能在多个结果之间做合并和对比第三给循环加入“人类确认”节点在工具执行前先征求用户同意这个适合更严肃的业务场景第四把单一工具调用扩展为多步规划模型先列出要依次执行哪些工具再逐步执行。这些方向无论哪一条最终考验的还是第一关里那些基础能力判断是否调用工具、正确填充参数、完整传回结果、保持多轮状态。我现在越来越觉得agent 开发要学的东西其实不是某个新框架而是把模型和外部世界连接时的接口设计能力以及无数个像 TASK1 这样的小细节。最后再分享一个小技巧在我完成 TASK1 之后我会把每一次调试失败的 message 序列完整保存下来标上“失败原因”和“修复动作”后面排错时回看特别有用。因为 agent 的偶发问题不像普通代码那样必现很多时候模型这次错了下次就对了只有留下足够多的现场记录你才能从概率中找出真正需要修的地方。这个习惯延续到了后面的任务里帮我省了很多无意义的重复试验。