资讯动态

LLM Agent工具调用实战:搜索、计算与业务API接入指南

发布时间:2026/10/20 15:53:10 来源:尧图企业网站定制
前阵子我搭了一个客服问答Agent上线第二天就翻车了。用户问“帮我查一下编号A2093的订单啥时候发货”Agent一本正经地回答“您的订单预计在3个工作日内发出请您耐心等待”。实际上它根本看不到任何订单数据那只是大模型典型的“礼貌性幻觉”。这件事让我彻底想明白一个道理在LLM应用里光有一个聪明的“大脑”远远不够Agent要真正替人干活必须长出“手”。所谓工具调用能力就是给模型配备一组可执行的函数——搜索、计算、第三方API等——模型负责决策“什么时候调用哪个工具”程序负责真正执行再把结果喂回给模型做下一步推理。这篇内容就是把搜索、计算、API这三类最常用的工具能力逐一拆开讲清楚原理、给出可直接复制改写的代码、再补上我在实际项目中踩过的坑。适合正在做LLM应用开发、想给聊天机器人或自动化Agent升级能力的开发者。1. 给Agent“装上手”之前先想清楚哪类能力必须靠工具动手写代码之前建议先做一次需求判断。工具调用不是越多越好而是要把模型“天生做不好的事”交给外部程序把模型“擅长的事”留给推理。1.1 模型单纯靠“死记硬背”搞不定的三类任务大语言模型本质是一个文本续写器。它学到的知识是在训练阶段固化下来的推理也建立在概率预测之上。这就决定了下面三类任务单靠模型硬答一定会出问题时效性信息训练数据有截止时间。你问“某城市今天天气怎么样”“目前某股票价格是多少”“某产品最新版本有哪些功能”模型要么编一个要么只会说“我不支持实时信息”。精确计算多位乘除、多步推导、进制转换、单位换算。模型输出是token概率它不是计算器数字越大、步骤越多出错率越高。外部系统交互模型根本不知道你的订单库、用户库、支付系统里有什么数据更没有权限去改任何状态。它只能“说”不能“查”和“改”。用一张表可以看得很直观任务类型模型硬答的表现加入工具后的表现查询实时天气编造一个看起来合理的天气描述搜索工具返回真实预报准确到温度区间计算4567×289大概率给出接近但错误的数字计算工具返回1323863分毫不差查订单A2093状态用通用话术安抚用户实际是幻觉业务API返回真实物流节点附更新时间这个判断逻辑可以复用凡是“结果需要外部事实来校验”的就应该接工具凡是“结果能从模型已有知识推理出来”的比如写作、改写、总结、给建议反而没必要动用工具串否则只会增加调用延迟和成本。1.2 工具调用常见的三种落地形态同样是“给模型加工具”业界有几种不同做法区别在于模型怎么表达“我要调用工具”以及程序怎么解析这个意图。第一种是原生Function Calling协议。部分主流大模型服务商在接口里提供了tools参数调用时声明好工具清单模型在回复时除了普通文本还会多返回一个结构化字段包含要调用的函数名和参数。这是目前我用下来最稳定、最省事的方案参数按JSON Schema传入解析成本很低。第二种是ReAct风格的提示词约定。不依赖平台原生协议而是把工具的使用说明写进system prompt要求模型在需要工具时输出一段约定格式的JSON例如{action: web_search, action_input: 某城市今天天气}。程序自己写解析器和执行器。好处是换模型厂商不用改代码坏处是模型偶尔会格式漂移需要写一套纠错逻辑。第三种是框架封装。像LangChain这类编排框架把上述机制再包了一层内置了搜索、计算等常用工具的Tool类用起来快但中间多了一层抽象出现问题时要排查框架源码学习成本反而更高。形态稳定性可控性上手速度适合场景原生Function Calling高中快应用只绑定单一模型服务商ReAct式JSON约定中高中需要跨服务商切换框架Tool封装中高低最快快速验证原型我自己的偏好是正式项目优先用原生Function Calling因为它让“模型决策”和“程序执行”的边界最清晰如果哪天需要切换底层模型服务商再退到ReAct式约定也不迟。2. 核心机制拆解模型是决策者不是执行者很多人第一次接触工具调用时会有一个误解以为“让Agent用工具”意味着模型内部会运行代码。实际上模型只是输出了一段结构化的“调用意愿”真正执行的是你的代码。理清这个边界后面所有问题都好办。2.1 一次完整调用的五个步骤拿一个最简单的场景举例用户问“某城市今天适合跑步吗”。一次带工具调用的完整流程是这样的程序把用户问题发给模型同时附带tools参数里面声明了web_search和calc两个工具的Schema。模型判断“适合跑步吗”需要天气数据于是返回一个tool_calls结构内容是调用web_search参数为query“某城市今天天气”。程序收到这个结构后不把文本直接返给用户而是自己去执行search函数拿到真实的天气JSON。程序把搜索结果以“工具消息”的身份追加进对话上下文再发给模型。模型看到工具结果后结合天气信息继续推理生成最终答复“上午有小雨建议改到室内跑步”。代码层面用伪代码描述就是# 第一轮带工具声明的对话 resp client.chat( modelMODEL_NAME, messages[ {role: user, content: 某城市今天适合跑步吗} ], toolsTOOLS_SCHEMA, ) # 检查模型是否要求调用工具 if resp.tool_calls: call resp.tool_calls[0] result dispatch_tool(call) # 这里执行你的真实函数 messages.append({role: tool, tool_call_id: call.id, content: result}) # 第二轮把工具结果还给模型让它生成最终答案 final_resp client.chat(modelMODEL_NAME, messagesmessages, toolsTOOLS_SCHEMA) print(final_resp.content)其中dispatch_tool是个简单的分发函数本质就是一个字典查找def dispatch_tool(call): tool_map { web_search: web_search, safe_calc: safe_calc, query_order_status: query_order_status, } func tool_map[call.function.name] return json.dumps(func(**json.loads(call.function.arguments)), ensure_asciiFalse)这个闭环里最容易出错的一环不是工具执行本身而是“参数传递”。模型返回的arguments是JSON字符串需要反序列化后再传参如果模型生成的参数类型不对比如把数字字段传成了字符串执行器要有校验逻辑不能直接崩掉。2.2 工具Schema怎么写直接决定模型选不选得对同一个功能Schema写得含糊和写得具体模型的选择命中率差距很大。工具描述对模型来说就像产品说明书对用户一样——它只能根据你写的说明来决定“什么时候用这个工具”。一个标准的搜索工具Schema长这样{ name: web_search, description: 搜索互联网获取实时信息。当用户询问新闻、天气、实体动态、行情、物流跟踪、人物近况等训练数据可能覆盖不到的内容时使用。, parameters: { type: object, properties: { query: { type: string, description: 精简后的搜索关键词去掉语气词和冗余信息保留核心实体和限定词 } }, required: [query] } }写Schema有三条经验是常规文档里找不到的第一description里要写“使用场景”而不是只写“功能”。光写“搜索网页”模型经常不知道该何时触发写成“当用户询问实时信息时使用”模型就容易识别。第二参数description要写清楚“输入什么形式的内容”。比如search工具的query建议让模型做关键词精简而不是把整段用户问题原封不动传进来不然搜索接口的召回质量会差一大截。第三一次暴露给模型的工具不要超过8个。工具太多模型像在逛一个商品过多的超市容易拿错东西。我一般控制在5个左右确定必须要用的再挂上去。3. 接入搜索工具让Agent拥有“实时信息源”聊完机制进入实战。搜索工具是Agent工具链里最常用的一环它解决的是“模型知识截止时间”和“长尾冷门信息覆盖率不够”这两大短板。3.1 搜索API的选型思路做Agent的搜索能力有几种选型路径方案优点缺点适合场景商用搜索开放API返回结构化结果稳定有配额管理需要付费有调用速率限制中大型Agent产品新闻/专项数据API垂直领域精度高噪音少覆盖范围窄只做新闻、舆情、金融等场景自建爬虫加索引可控数据源无费用增长问题反爬规则复杂合规和稳定性风险高内部知识库闭环系统如果是自己做项目起步不要一上来就自建爬虫先用商用搜索API把流程跑通。等到搜索量大、成本敏感了再考虑混合方案。这里提一个容易忽略的点搜索API返回的结果里标题、链接、摘要这三项是最常用的但很多接口还会返回图片、站点分类、发布时间等扩展字段。保存时可以全量存但喂给模型前一定要裁剪。3.2 一个最小可用的搜索函数实现我写过一个最小实现核心就是把HTTP请求包装成一个工具函数import requests def web_search(query: str, max_results: int 5) - dict: url https://api.example.com/v1/search params { q: query, count: max_results, api_key: SEARCH_API_KEY, } try: resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() except requests.exceptions.Timeout: return {error: 搜索服务超时请稍后重试} except requests.exceptions.HTTPError: return {error: f搜索服务返回错误状态码 {resp.status_code}} results [] for item in data.get(results, []): results.append({ title: item.get(title, ), url: item.get(url, ), snippet: item.get(snippet, )[:160], }) return {results: results}注意两点一是timeout参数一定不能省搜索接口是外部依赖慢后端不能拖死整个Agent会话二是把异常信息做成可读的error字段返回这样模型在下一轮还能基于错误做决策比如换个query重试而不是直接对用户说“我调接口失败了”。3.3 结果瘦身别把原始返回一股脑丢进上下文搜索接口返回的原始JSON很容易突破几千字符。如果直接把整个response传给模型会出现三个问题一是挤占上下文窗口留下给推理的空间变小二是噪音信息干扰模型判断让它分不清哪条才是关键结果三是token消耗成倍上涨成本随之翻倍。我建议在把搜索结果接入对话之前做一个三层瘦身第一层字段裁剪。去掉图片参数、站点统计、竞价信息等扩展字段只留title、url、snippet。第二层摘要截断。snippet超过160个字符就截断截断点尽量放在句号或逗号后面。第三层去重过滤。同一域名下的相似标题只保留一条能显著减少冗余。这一步做完一次搜索返回的文本量能压缩到原来的三分之一左右而关键信息几乎无损。4. 接入计算工具给Agent装一颗“精确的数学脑”搜索解决信息问题计算解决精确性问题。聊到数学能力LLM真的很脆弱这不是参数规模的问题而是概率生成模式决定了它天生不适合做精确运算。4.1 为什么模型算不对数而你却需要一个专门的计算器让模型算4567乘289它可能会给出接近但错误的数字。原因在于模型学到的是“乘法过程的模式”而不是真正执行乘法。它的输出是“最可能的下一个token”一旦数字变大、进位变多、步骤变长某一个token偏离后面全盘皆错。但Agent应用里又经常遇到数学需求优惠计算、拼单分摊、日期差、单位换算、库存核算等等。这时候最好的方案就是给Agent挂一个safe_calc工具让模型把用户的话翻译成数学表达式然后交给Python精确执行。4.2 别直接用eval用AST白名单才能既精确又安全有人图省事直接写eval(expression)这在Agent场景里非常危险。用户和模型生成的表达式都可能包含恶意代码比如__import__(os).system(rm -rf ...)eval会直接执行。正确的做法是做一个基于AST抽象语法树的白名单解析器只允许四则运算和幂运算这些安全节点。import ast import operator ALLOWED_NODES ( ast.Expression, ast.Constant, ast.BinOp, ast.UnaryOp, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.USub, ast.Load, ) OPERATORS { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, } def safe_eval(node): if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.BinOp): left safe_eval(node.left) right safe_eval(node.right) return OPERATORS[type(node.op)](left, right) if isinstance(node, ast.UnaryOp): operand safe_eval(node.operand) return -operand if isinstance(node.op, ast.USub) else operand raise ValueError(不支持的表达式) def safe_calc(expression: str): tree ast.parse(expression, modeeval) for node in ast.walk(tree): if not isinstance(node, ALLOWED_NODES): return {error: 表达式包含不允许的操作} try: result safe_eval(tree.body) return {result: result} except Exception: return {error: 表达式计算失败请检查括号和运算符}这段代码能覆盖加减乘除、括号、负数、幂运算并且从根本上杜绝了代码注入。如果你预算用现成库也可以用SymPy做符号计算它能顺手处理根号、积分等更复杂的数学表达式适合场景更重的Agent。4.3 把计算工具的“边界”设计清楚模型才不会滥用计算工具的一大坑是模型把不该算的东西也丢进来算。比如用户问“某城市今天气温会不会超过30度”模型有可能把这个拆成表达式30 28这其实是可以的但如果模型把“搜索天气”和“温度比较”两件事混在一个工具里就会出现决策混乱。我在实际项目里的做法是给safe_calc工具的description加一句话“仅用于精确数学运算如四则运算、幂运算、比较运算不用于信息查询、文本处理、搜索”。这看起来是小事但能明显降低模型把这个工具当成万金油去用的情况。另外计算前建议让模型先“规范化”表达式把中文数字、百分比、单位转为数学表达这一步可以在提示词里加一条小约束也可以在调用计算工具之前加一个preprocess函数。5. 接入业务API让Agent真正触达你的系统数据搜索和计算属于通用工具第三方API属于定制工具。业务API接入是Agent从“玩具”走向“生产力”的关键一步也是工程上最需要抠细节的地方。5.1 一个实战样例把“查询订单状态”封装成工具假设你的Agent需要回答用户关于订单状态的问题。后端已经有一个HTTP接口GET /api/v1/orders/{order_id}/status返回订单状态、物流节点、更新时间等信息。现在要把它变成一个Agent可调用的工具分三步。第一步定义函数。第二步写好函数和Schema。第三步把Schema挂到工具列表里。函数实现大致是这样import requests def query_order_status(order_id: str) - dict: url fhttps://internal.example.com/api/v1/orders/{order_id}/status headers {Authorization: fBearer {API_TOKEN}} try: resp requests.get(url, headersheaders, timeout5) resp.raise_for_status() data resp.json() return { order_id: data.get(order_id), status: data.get(status), logistics: data.get(logistics, []), updated_at: data.get(updated_at), } except requests.exceptions.HTTPError as exc: code exc.response.status_code if code 404: return {error: f订单 {order_id} 不存在请确认编号是否正确} return {error: f订单服务暂时不可用状态码 {code}} except requests.exceptions.Timeout: return {error: 订单查询超时请稍后再试}对应Schema里description可以这样写“根据订单ID查询订单状态和物流信息。当用户询问订单编号、物流进度、发货时间时使用。订单ID一般是以A或B开头的字符串编号。”5.2 鉴权、超时和错误回传三个必须做对的细节业务API和公开搜索API不一样它涉及鉴权、租户隔离、内部网络踩坑点更多。鉴权方面不要把完整的Token暴露给模型也不要在工具函数里硬编码密钥。更好的是在Agent服务启动时从环境变量或密钥管理服务读取密钥只把它用于HTTP请求头。如果系统支持临时票据优先用短期票据代替长期Token。超时方面业务接口经常比搜索接口还慢。我习惯把下游API超时控制在5秒内如果超过5秒返回error字段宁可不回答也不能挂起会话。因为生产环境里一次对话可能串联多次工具调用每次多等3秒用户体感就会很差。错误回传方面一定要返回“对模型友好”的错误信息。模型不是人你给它返回一长串堆栈它只能干瞪眼你告诉它“订单A2093不存在”它就能继续判断是让用户检查编号还是转人工。这个小差别直接决定了Agent的容错能力。5.3 多租户场景下的参数隔离思路如果你的Agent服务给多个客户共同使用业务API工具要格外注意用户隔离。不能用户A查订单结果函数内部用了用户B的Token和权限。做法是在工具分发层额外注入一个context对象把当前用户的身份ID、会话ID放进去工具函数内部根据这个context来计算请求参数而不是让模型自由传入用户的身份标识。模型只负责传业务参数比如订单号、关键词身份信息由运行环境自动追加。这一步做不好Agent很容易变成越权工具合规风险很大。6. 工具调用循环的控制与四个典型坑把搜索、计算、API这些单个工具都装好之后还有一个全局性的问题需要处理Agent可能会在一次回答里连续调用多个工具也可能调错、调超时、甚至陷入死循环。这部分是上线前必须做好的。6.1 一段可复用的工具调用循环骨架原生的Function Calling接口允许模型在一次响应里返回多个tool_calls也允许连续多轮调用。所以程序端要写一个循环而不是每轮都手动处理。一个骨架结构如下MAX_ITERATIONS 5 def run_agent(user_message: str): messages [{role: user, content: user_message}] for _ in range(MAX_ITERATIONS): resp client.chat(modelMODEL_NAME, messagesmessages, toolsTOOLS_SCHEMA) if not resp.tool_calls: return resp.content for call in resp.tool_calls: result dispatch_tool(call) messages.append({ role: tool, tool_call_id: call.id, content: result, }) return 处理流程过长请简化问题或稍后再试MAX_ITERATIONS建议设在3到5之间。搜索、计算、API三轮基本能覆盖绝大部分需求超过这个次数大概率是模型在循环里打转。6.2 坑一工具返回了模型读不懂的数据第一次实战时我遇到过工具返回的JSON里包含特殊符号、换行符和超长文本模型在下一轮解析时直接报格式错误。解决方法是在返回给模型之前对content做一次JSON序列化保证所有非ASCII字符被转义同时截断超长字段。return json.dumps(result, ensure_asciiTrue, defaultstr)[:2000]ensure_asciiTrue虽然会让内容变得不太好看但能规避掉很多编码解析问题。6.3 坑二工具描述不清导致模型选错工具有一次我把搜索工具的description写得过于大而全结果模型连“计算两笔订单的合计费用”这种纯本地运算也调用了搜索工具搜索接口白白跑一趟返回一堆无关网页。这类问题只能从Schema侧解决每个工具的边界要写成“什么情况用、什么情况不用”并且通过测试集反复调描述文本。改一句话、命中率从50%提到90%这种事真的会发生。6.4 坑三下游API太慢把整个会话拖死搜索接口、业务API都可能慢到几十秒。如果Agent是直连前端用户早就走了。我的方案是给每次外部HTTP调用加timeout并且在工具函数内部做统一降级比如搜索接口超时了就返回一条固定错误信息让模型直接告诉用户“实时搜索暂不可用”而不是卡住不回复。高可用优先于高可用性这句在Agent工具链里是真的。6.5 坑四Agent进入工具调用死循环最棘手的是循环调用。我见过一次事故用户问“某产品最新款”搜索结果里包含了一条“该产品评测”的链接Agent觉得信息不够又调搜索搜索返回还是不够详细它又继续调……直到最大轮次才停下来。用户没有得到任何答复。对付循环有两个手段一个是前面说的MAX_ITERATIONS兜底另一个是在循环里加一个“重复意图检测”如果模型连续两次调用了同一个工具且参数完全相同就强制中断并把结果返给用户或转接人工。6.6 上线前建议从三个工具起步最后给一个很实际的建议不管你的Agent最终要接多少个服务第一版先只挂3个工具——一个搜索、一个计算、一个最核心的业务API。工具数量少模型的选择准确性高调试范围小日志也能看得明白。等这三个跑稳了再逐步扩充每次加一个都要回到测试集上重新跑一遍旧用例防止新增工具挤占旧工具的触发空间。我个人的体会是工具调用能力的工程难度不在于“让模型返回tool_calls”而在于“让模型每一次决策都可解释、可兜底、可观测”。日志里记录下每一次工具选择、参数、耗时、返回结果大小排查问题时会轻松非常多。Agent的“手”装上之后真正考验人的是给它划定清晰的能力边界以及为每一次意外留好后路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑