十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

智能体开发自定义工具实操:从Function Calling到多工具调试

智能体开发自定义工具实操:从Function Calling到多工具调试 自定义工具在智能体开发里是一个绕不开的实操环节。很多新手学 AI 编程和智能体开发一开始觉得把提示词写好就够了等到真要做一个能查数据、能计算、能调接口的 agent 时才发现模型只负责“想”真正干活的还得靠外部函数。这些可以注册给模型调用的函数就是自定义工具。这篇文章就围绕“自定义工具实操”这个主题把整条链路拆开讲先理解模型和工具怎么配合再动手写一个工具然后处理参数、返回、多工具协作和调试。适合正在学智能体开发、或者已经会调模型接口但想把功能做实的开发者阅读。先给个结论自定义工具本身不复杂真正容易踩坑的是四个地方——工具描述写得太模糊、参数类型和说明不清晰、错误返回没有给模型可理解的提示、多个工具同时挂载后命名混乱。下面按实操顺序逐个拆开给出一套可以直接照着做的思路。1. 自定义工具在智能体里到底扮演什么角色1.1 模型不是万能的工具就是模型的“手”大语言模型本质上是一个文本生成模型。它能理解你的问题能生成看起来很合理的回复但它没有实时获取外部数据的能力也没有直接操作业务系统的能力。举一个最常见的例子。你问“北京现在天气怎么样”模型如果只靠训练数据它没有办法告诉你真实天气。你问“帮我算一下这个季度销售总额”如果数字比较复杂模型直接算很容易出错。这时候就需要给模型配工具。工具的本质是一段普通代码可以是一个函数、一个类方法、一个 HTTP 接口封装甚至是一条命令行脚本。关键在于它能不能在合适的时机被模型调用并把执行结果正确返回给模型。1.2 完整的工具调用链路这里先把概念说清楚。很多人第一次接触 Function Calling 时容易误以为模型会直接执行代码。实际不是这样。整个链路是用户输入问题。应用把用户输入、系统提示词、可用工具列表一起发给模型。模型不直接执行工具而是返回一个“调用意图”告诉应用它想调用哪个工具、参数是什么。应用在本地执行对应的函数。函数执行结果作为一条 tool 消息发回给模型。模型根据工具返回的结果生成最终回复。“模型只决定调用代码由应用执行”这个设计很关键。它把大模型的语义理解能力和普通代码的确定性执行分离开来了。模型只要学会“什么时候用哪个工具”剩下的计算、查询、文件操作等动作全部由稳定可靠的代码完成。1.3 自定义工具和内置工具的区别很多智能体框架会自带一些内置工具比如网页搜索、计算器、代码执行器等。内置工具用起来方便但实际项目里总有业务特有的操作是内置工具覆盖不了的。自定义工具就是在自己项目里定义的函数。常见的例子查询公司内部业务数据库调用某个内部 API并把返回数据整理成指定格式执行一段本地 Python 脚本比如批量重命名文件解析用户上传的 Excel 或 CSV 文件生成一张图表并保存到指定目录这些业务代码原本就在系统里。自定义工具要做的就是把这些函数暴露给模型让模型能通过自然语言触发它们。理解了“把一个普通函数注册成模型能理解和调用的工具”这件事后面不管换什么框架、什么模型都只是包装形式不同核心原理是一样的。2. 动手前先把环境和核心概念准备好2.1 需要一个支持工具调用的模型接口工具调用在接口层的名字一般叫 Function Calling。现在主流模型接口基本都支持不同服务商的接入方式略有差异但核心概念一致请求体里带上一个 tools 参数声明有哪些函数可用。第一次实践时建议先使用一个支持工具调用的在线模型接口做最小验证。跑通链路后再考虑开源模型或本地部署。原因是工具调用对模型的指令跟随能力有要求部分开源小模型的工具调用成功率波动较大。如果你用一个小模型测试发现模型总是不调工具先不要急着怀疑代码很可能是模型本身能力不够。建议第一次实践时把模型 temperature 调到 0 或 0.1减少生成随机性方便排查问题。2.2 本地开发环境建议我建议按下面这个组合准备通用性比较强Python 3.10 或以上一个支持工具调用的模型接口 SDK或者直接使用对应模型服务商的 SDKrequests 库后续调用外部 HTTP 接口会用到一个清晰的项目目录tools/ 放工具函数config/ 放配置main.py 做调用入口如果你用的是 LangChain、LangGraph 等框架也可以直接安装对应依赖。但我不建议第一遍就一头扎进框架的 Agent 封装。先把原生接口的工具调用格式完整写一遍你对原理的理解会扎实很多。2.3 工具定义的四个核心字段在原生接口里一个工具的定义通常长这样{ type: function, function: { name: get_weather, description: 根据城市名称查询当前天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 } }, required: [city] } } }字段不多但每个都很关键name工具唯一标识模型会在返回结果里引用这个名字。description模型的“使用说明书”决定模型在什么场景下选择这个工具。parameters用 JSON Schema 描述参数结构模型的调用参数会按这个结构生成。required标记哪些参数必须传。对应真正执行的函数可以非常简单def get_weather(city: str): data {北京: 晴25°C, 上海: 多云28°C} return data.get(city, f暂无{city}的天气数据)函数和 JSON 元数据放在一起看就能理解自定义工具的全貌一个是模型看到的行为描述一个是应用真正执行的逻辑。3. 写第一个自定义工具从定义到调用闭环3.1 最小可运行的完整示例这里写一个不依赖复杂框架的最小示例方便你先复现再扩展。import json from openai import OpenAI client OpenAI() def get_weather(city: str) - str: 模拟天气查询函数。 data {北京: 晴25°C, 上海: 多云28°C} return data.get(city, f暂无{city}的天气数据) 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: 北京今天天气怎么样} ] response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools ) message response.choices[0].message print(模型返回内容, message)运行后你会看到模型返回的 message 里带有一个 tool_calls 字段。它只是表达了“调用意图”还没有真正执行函数。接下来需要在应用层手动执行并把结果回传给模型。3.2 执行工具并回传结果if message.tool_calls: tool_call message.tool_calls[0] function_name tool_call.function.name arguments json.loads(tool_call.function.arguments) print(f模型选择调用{function_name}) print(f参数为{arguments}) function_result get_weather(**arguments) messages.append(message) messages.append({ role: tool, tool_call_id: tool_call.id, content: function_result }) final_response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools ) print(最终回答, final_response.choices[0].message.content)这个闭环跑通后你就掌握了自定义工具的核心机制。后面不管用 LangChain 的 tool 装饰器还是其他框架的工具注册方式原理都是同一个模型发出调用申请应用执行函数再把结果回传。3.3 第一次实操最容易遇到的三个问题第一个模型完全不返回 tool_calls。大概率是描述或系统提示词里没有给出足够的调用理由。检查 description 是否明确写了工具的适用场景系统提示词里是否允许模型自主调用。第二个模型返回的参数和你函数定义对不上。要么是 required 字段没写清楚要么是参数的 description 不够具体。试着把参数说明写成“城市名称中文全称例如北京、上海”通常会有明显改善。第三个回传 tool 结果时漏掉 tool_call_id。新版本 SDK 一般会严格要求每条工具结果对应一个 tool_call_id漏掉会直接报错。4. 参数设计和返回结果设计决定工具好不好用4.1 参数设计要“少而明确”设计自定义工具的参数时最常见的错误是贪多。一个函数动辄七八个参数模型要把每个参数都猜对难度相当高。更推荐的做法是一个工具只做一件足够明确的事参数控制在两到三个以内。如果业务逻辑确实复杂可以考虑把多个参数打包成一个 JSON 字符串参数或者把任务拆成多个粒度更小的工具。举个例子# 不推荐参数过多模型容易传错 def query_sales(region: str, product_type: str, start_date: str, end_date: str, channel: str, group_by: str): pass # 推荐参数少范围明确 def query_sales_by_region(region: str, date: str): pass参数少模型判断负担就小工具被正确调用的概率自然高。4.2 返回结果要方便模型“再阅读”工具执行完后结果会以文本形式返回给模型。所以返回字符串的质量直接影响最终回复的质量。好的返回结果通常满足三点简洁但包含回答用户问题所需的核心信息不要把内部异常堆栈直接返回给模型失败时返回的提示语要能帮助模型决定是向用户解释还是尝试其他工具一个规范示例def query_stock_price(code: str) - str: if not code or len(code) ! 6: return 错误股票代码必须是6位数字例如 600519 # 实际查询逻辑 return 股票代码 600519收盘价 1715.00 元涨跌幅 0.82%模型收到这个结果后能直接提取“600519”“1715.00”“0.82%”这些信息组织成最终回答不需要再猜测。4.3 错误处理不能漏工具执行中一定会出现异常常见的有参数不合法、接口超时、数据不存在、依赖服务报错。这些异常如果不处理直接抛出到上层整个 agent 程序可能会崩掉。更合理的做法是在工具内部捕获异常并把可读的错误信息返回给模型def call_remote_api(params: dict) - str: try: resp requests.get(https://example.com/api, paramsparams, timeout5) if resp.status_code 200: return json.dumps(resp.json(), ensure_asciiFalse) return f请求失败HTTP状态码{resp.status_code} except requests.Timeout: return 错误外部接口请求超时 except Exception as exc: return f错误接口调用失败详情{exc}这样即使出错链路还是完整的。模型会收到一条错误说明可以继续引导用户或换一种处理方式。4.4 返回用纯文本还是 JSON数据量小、结构简单时用纯文本没有问题。数据字段多、结构复杂时建议返回 JSON 字符串并在函数说明里提前告知模型返回结构。return json.dumps({ code: 600519, price: 1715.0, change_percent: 0.82, updated_at: 2025-01-15 15:00:00 }, ensure_asciiFalse)需要注意不要在返回结果里混入大量日志或调试信息。模型会把整段文本当作回答素材无关内容越多最终回答被带偏的概率越大。5. 多工具场景注册、命名、任务拆分5.1 多个工具注册时的命名规范一个智能体通常不止一个工具。比如查询类工具可能同时有 get_weather、query_stock_price、get_news 三个。工具一多命名就开始影响调用准确性。命名建议采用“动词 对象”的结构。动词说明动作对象说明作用域。例如get_weatherquery_stock_pricesend_emaillist_recent_files尽量避免 create、handle、do 这种过于泛化的动词。模型看到 do_something很难判断应该什么时候调用。5.2 描述信息不要互相覆盖工具描述是模型做选择的主要依据。如果两个工具的 description 都写成“查询信息”模型很容易选错。好的描述至少包含三个信息这个工具是什么什么场景下使用什么情况下不要使用{ name: get_weather, description: 查询指定城市的当前天气。当用户询问天气、气温、降水时使用。如果是历史天气不要使用本工具。 }5.3 工具数量不是越多越好很多框架支持给不同的智能体配置不同的工具集。比如前台客服智能体只挂工单查询工具后台数据分析智能体才挂数据库查询工具。这样做的好处非常明显工具越少模型判断越准。工具列表过长时模型不仅决策变慢还容易出现相互干扰。我在一次测试里挂载了 12 个工具模型频繁选错。后来把工具按场景拆分成两组每组只放 5 到 6 个调用准确率立刻上来了。如果你发现工具总选错优先考虑减工具而不是改描述。5.4 工具之间互相调用怎么处理有些场景下工具 A 需要工具 B 的结果。一种做法是在工具 A 内部直接调用工具 B 的函数另一种是让模型先调 B 再调 A。我更推荐第一种在代码层面直接复用函数。原因是模型的多步调用不可控一旦中间某一步出错后续流程全部乱掉。代码层面直接嵌套稳定得多。def query_stock_and_news(code: str) - str: price_result query_stock_price(code) news_result get_stock_news(code) return f{price_result}\n{news_result}这种复合工具适合当做一个新工具注册给模型而不是依赖模型在对话里自动串联两个工具。6. 测试和排查把智能体当普通程序来调试6.1 先测函数再测模型层很多人在模型调用环节反复报错最后发现是工具函数本身就有 bug。建议把测试顺序固定成三层。第一层直接调用函数分别传入正常参数和异常参数看返回结果是否符合预期。python -c from tools.weather import get_weather; print(get_weather(北京))第二层单独检查工具元数据确认 name、description、parameters 格式正确。可以写一个小脚本校验 JSON Schema 是否合法。第三层再走完整的模型调用链路用几种不同说法的问题测试模型是否能选对工具。6.2 日志要记录四个关键信息排查智能体问题时最麻烦的情况是用户说“它答错了”但你看不到模型当时选了哪个工具、传了什么参数、工具返回了什么。所以日志里至少要记录四个字段用户原始输入模型返回的工具名称和参数工具执行结果最终回答内容有了这些记录大部分问题都能快速定位到是哪一层出错。6.3 常见问题排查表现象优先排查方向常见原因模型完全不调用工具工具描述、系统提示词描述里没有触发场景或提示词限制调用调用了但参数错误参数 description、required参数说明模糊模型猜测了错误格式工具执行报错函数输入验证没有兼容 None、空字符串、非预期格式返回内容模型没用上返回结果格式返回夹杂日志结构不清晰多个工具选错工具描述边界描述重叠模型无法区分回传 tool 结果报错tool_call_id 是否匹配漏传或错传 tool_call_id6.4 从“对话式排查”变成“数据式排查”不要只看最终回答来判断智能体是否正确。你应该先看模型到底调用了哪个工具。没有调用问题在模型选择层调用了但结果不对问题在工具函数或参数层。这样逐层拆解比反复修改提示词效率高得多。遇到卡住或者返回异常时按下面顺序检查看日志里模型调用了哪个工具。看传入参数是否合法。手动执行这个函数确认函数本身没有 bug。检查返回结果是不是模型容易理解的形式。最后再考虑调描述、调提示词。6.5 一个真实的排查复盘我在一次测试中遇到过这种情况用户问“查一下上个月的销售数据”智能体返回了一段“接口报错”。日志显示模型选择了 query_sales_data参数是 {month: 上个月}。显然模型没有把“上个月”换算成具体日期。问题不在函数而是参数描述不够严格。我把参数描述改成“月份格式为 YYYY-MM例如2025-01。如果用户说上个月你需要先计算上个月的日期再作为参数传入”并在系统提示词里补了一句“涉及日期计算时必须先计算出具体日期”。之后再测试模型就能正确传入 2025-01 这类值了。这种问题很常见。它说明的不是模型不行而是你的工具描述没有覆盖到模型会遇到的真实情况。7. 从课程实操到真实项目还需要关注什么7.1 工具粒度不是越细越好颗粒度太粗的工具比如一个“处理所有数据”的函数内部逻辑会非常复杂模型也难以判断调用时机。颗粒度太细的工具比如只做字符串转小写又会让工具列表变得很长模型选择负担加重。比较合适的粒度是一个工具能独立完成一次业务动作输入输出都有清晰边界。课程练习里的天气预报工具、股票查询工具本质上都是这个思路。7.2 工具执行时间和并发要考虑自定义工具在真实项目里还要考虑执行时间。有些工具是快速查询几十毫秒返回有些工具要几秒比如调外部接口或处理大文件。如果模型要等多个工具串行返回用户等待体感会很差。处理方法有两个方向。一是控制工具复杂度尽量让每个工具在几秒内完成。二是增加超时控制避免工具永久卡住。课程示例可以只关注功能是否跑通但进入生产环境超时、重试、失败隔离都是必须考虑的。7.3 从 Demo 到生产的最小路径如果已经跑通了示例代码想继续往生产方向走我建议按这个顺序补充给所有工具加上超时和错误返回。增加日志记录至少能回答“模型选了什么工具、传了什么参数”。给工具函数写单元测试。控制工具列表长度按场景分组挂载。上线前用一批真实输入做回归统计工具调用成功率。7.4 框架和原生接口怎么选如果你只是学习或者做原型原生接口完全够用而且能帮你把原理搞清楚。如果要做复杂的智能体编排、多步规划、状态持久化框架会省很多事。但我的建议是无论用哪个框架都要保留对工具元数据的控制权。工具的描述、参数、返回结果才是智能体质量的真正决定因素。框架只是把调用循环封装好了真正会不会被正确调用还是取决于你写工具时是否足够细致。自定义工具实操练到最后就是练两件事一是让模型知道“你有哪些能力”二是让代码知道“模型要你做什么”。把这两件事做得清晰、可测、可控就是一个靠谱的智能体。
返回列表