资讯动态

Agent转换工具全解析:从函数到模型可调用的工具

发布时间:2026/10/17 4:15:47 来源:尧图企业网站定制
第一次听到“Agent转换工具”这个词的时候我脑子里冒出来的画面是把一堆Agent代码“转换”成另一个什么东西。等到自己动手写完几个项目才反应过来这个词真正指的其实是另一件事把你已经写好的普通函数、接口、脚本通过一套标准化的包装变成大模型能够理解、能够主动调用、还能拿回结果的“工具”。说白了Agent转换工具就是给大模型装手和脚的适配器。我在实际开发中遇到过这样的典型困境业务逻辑明明写得很顺函数也封装得干干净净但一接入大模型就卡住——模型不知道该调用哪个函数、参数格式对不上、返回的数据解析不出来最后只能在代码里写一大堆if-else把模型吐出来的字符串硬塞给函数维护起来简直想掀桌子。后来真正理解了Agent转换工具这个问题被大大简化。它解决的问题非常具体让模型“看得到”你的函数、“看得懂”怎么调、“拿得回”执行结果。这篇文章我会用一个完整的工程示例把转换工具的原理、代码和避坑经验全部拆开讲。代码示例会非常详细你甚至可以直接复制回去改一改就跑起来。适合刚接触Agent开发、或者已经被工具调用搞得头大的开发者带点Python基础就能跟下来。1. 为什么你需要一个Agent转换工具1.1 先别急着写Agent搞懂“工具”到底是什么很多新手在接触大模型Agent的时候最容易犯的一个错误就是把Agent想得特别玄乎。什么“智能体”“自主决策”“多步推理”听着高大上其实落到工程上一次Agent调用的核心流程就三件事模型读你的话、模型决定调用什么函数、函数返回结果让模型组织回答。所以这里说的“工具”就是那个“函数”。你写一个query_weather(city)在普通程序里它就是给人调用的但在Agent世界里它得让模型知道这个函数存在、功能是什么、参数有几个、每个参数什么类型、哪些必传哪些可省略。Agent转换工具干的事情就是把“人能看懂的Python函数”翻译成“模型能看懂的JSON Schema”。我见过很多人问我直接把函数名告诉模型不就行了不行。大模型本身没有执行代码的能力它只能在你提供的工具列表里“挑选”合适的工具然后生成一段符合规范的调用参数。它不会真的去跑你的Python代码执行这一步永远发生在你的本地环境。所以你和模型之间必须有一个约定俗成的“协议”这个协议的载体就是函数转换出来的工具定义。1.2 Agent转换工具解决的三个核心痛点第一个痛点叫“格式不对没法聊”。模型返回的调用参数是一段JSON字符串比如{city: 北京}。你如果手写解析就要考虑参数顺序、缺失字段、类型错误一个字段写错全链路崩掉。转换工具帮你把函数的签名、类型注解、默认值全部自动解析出来模型按照规范的schema生成参数你按规范执行格式就对齐了。第二个痛点叫“模型根本不知道你有这个函数”。如果你只是把函数埋在代码深处模型永远不知道它存在。转换工具会把函数描述、参数说明、返回值结构提取出来组成一份“技能清单”直接塞给模型。模型看到清单才知道原来你可以帮它查天气、算价格、查数据库。第三个痛点叫“没有统一管理的入口”。业务一多你会有几十个函数需要暴露给模型。如果没有统一的注册和调用机制代码会很快变成一团乱麻今天在这里调一下query_weather明天在那里手动处理get_stock_price每个调用的错误处理还不一样。转换工具配合注册中心可以让所有工具都走同一套调度逻辑后续维护成本直线下降。1.3 哪些人在什么场景下最需要它从我看到的项目经验来说有三类人最需要这个东西。第一类是做业务系统集成的开发者比如要把内部订单查询、库存查询接口接入对话式助手。这类接口通常已经封装好了就差一个“翻译层”让模型能调用。第二类是在做私有知识库问答的开发者需要把检索、重排、引用的函数暴露给模型让模型自己决定是直接回答还是先查资料。第三类是刚入门大模型开发、想真正理解“工具调用”机制的学习者。与其用某个重量级框架的黑盒封装不如自己手写一遍转换逻辑你会对整个链路有非常清晰的认识。我自己属于第二类和第三类叠加。自己写过一遍转换工具之后再看其他框架的源码基本一眼就能看懂它们在做什么。2. 拆开看原理普通函数是怎么变成“Agent技能”的2.1 一切转换的核心参数Schema我们要搞清楚转换工具最核心的产物是什么。不是一个新的Python对象不是一段神秘的代码而是一个JSON Schema结构。就拿查询天气的函数来说转换完成后你得到的东西大致是下面这个样子{ type: function, function: { name: query_weather, description: 查询指定城市在指定日期的天气情况, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京 }, date: { type: string, description: 日期格式YYYY-MM-DD, default: today } }, required: [city] } } }这个结构是什么意思name是工具名模型靠它来识别调用谁description是功能说明模型靠它来判断什么时候该用这个工具parameters是参数规则模型严格按照这个规则来生成调用参数。required里的字段模型必须给不在里面的可以省略。你注意到没有这段JSON没有任何Python特有的语法它是纯文本。正因为是纯文本模型才能理解。转换工具本质上是做了一次“语言翻译”把Python的函数签名翻译成了模型圈的通用语言。有些平台的API会要求把type: function和function对象直接放到tools数组里有些则要求稍作整理。但底层思想完全一致。理解了这一点后面不管换什么平台、什么框架你都能快速上手。2.2 函数即工具装饰器与包装的艺术明白了schema接下来要回答的问题是这个schema怎么来手工写当然可以但函数一多就太痛苦了而且容易写错——函数改了参数schema忘记更新模型照着旧schema传参直接报错。更靠谱的做法是通过代码自动生成。Python的inspect模块能拿到函数签名、参数默认值、类型注解typing.get_type_hints能拿到参数类型函数自带的__doc__能提取描述信息。把这些信息组合起来一个通用的转换器就出来了。这里我把常见的三种转换方式放在一起对比一下方式优点缺点适合场景手写JSON Schema完全可控维护成本高容易和代码脱节极少数固定不变的简单接口装饰器自动转换声明式写法代码即定义对复杂类型支持需要额外处理大多数业务函数最推荐框架自带转换器开箱即用支持丰富可能引入大量依赖和隐藏约定快速搭建原型、大型团队装饰器的方式写出来很优雅比如这样agent_tool def query_weather(city: str, date: str today) - str: 查询指定城市在指定日期的天气情况 ...这个agent_tool装饰器在背后做的事情就是读取函数签名、生成schema、注册到全局工具列表。你平时写函数怎么舒服就怎么写只多了一行装饰器工具就自动有了。我自己最喜欢这种方式因为它把“业务逻辑”和“工具化”之间的割裂感降到了最低。2.3 转换后的大模型交互链路工具转换只是准备工作真正的“魔法”发生在你调用大模型的时候。以当下常见的对话补全接口为例完整的交互链路是这样的用户输入一句自然语言比如“北京明天天气怎么样”你把用户消息和tools列表一起发给大模型tools列表就是转换工具产出的schema集合。模型内部推理发现“需要查询北京天气”于是返回一个特殊的响应tool_calls里面包含了工具名query_weather和参数{city: 北京, date: 2025-01-06}被JSON字符串编码。收到这个响应后你的代码通过注册中心找到对应函数真实执行。把执行结果作为role: tool的消息发回给模型。模型基于工具的返回结果组织一段给用户的自然语言回答。这个链路里转换工具的角色是第3步“工具定义”和第4步“找到并执行函数”的桥梁。没有它你在第3步没法给模型提供结构化的工具清单没有它你在第4步得靠硬编码的if-else来找函数。所以可以这么理解转换工具让整个链路的每一步都变得标准化、自动化了。3. 从零手写Agent转换Demo代码全解析3.1 环境准备与最小依赖清单这个Demo不需要重量级框架核心依赖就两个一个requests用来调大模型APIPython标准库的inspect、json、typing用来做转换。Python版本建议3.9以上主要是为了类型注解的便利性。先安装依赖pip install requests然后准备一个可以调用的模型服务地址。我这里不绑定任何具体平台你自己替换成实际可用的API地址和密钥就行。在代码里先用环境变量读取不要硬编码。import os API_ENDPOINT os.getenv(LLM_API_ENDPOINT, https://your-api-endpoint/v1/chat/completions) API_KEY os.getenv(LLM_API_KEY, YOUR_API_KEY) MODEL_NAME os.getenv(LLM_MODEL_NAME, your-model)环境变量不在代码里写死好处是你换模型服务时不用改动业务代码。3.2 第一步写一个朴素的业务函数既然是演示我准备两个非常典型的函数一个查天气一个算表达式。它们足够简单但已经能覆盖“字符串参数”“默认参数”“返回值是结构化文本”这些常见场景。def query_weather(city: str, date: str today) - str: 查询指定城市在指定日期的天气情况 mock_data { (北京, today): 晴 12~24℃, (上海, today): 多云 18~26℃, (广州, today): 雷阵雨 24~30℃, } return mock_data.get((city, date), f暂无{city}在{date}的天气数据) def calculate(expression: str) - str: 计算一个数学表达式的值例如 12*3结果保留两位小数 # 注意这里仅作演示生产环境不要直接使用eval推荐用安全的表达式解析库 result eval(expression) return f{result:.2f}这里我故意写了一行注释eval在真实项目中风险很高我后面避坑部分会专门说。但为了让Demo先跑通用它做演示最直接。3.3 第二步写一个通用的create_tool转换器接下来是重头戏自己实现一个最小可用的转换工具。import inspect import json from typing import Any, get_type_hints, Callable _TYPE_MAP { str: string, int: integer, float: number, bool: boolean, list: array, dict: object, type(None): null, } def _convert_type(t): if t in _TYPE_MAP: return _TYPE_MAP[t] if hasattr(t, __origin__) and t.__origin__ is list: return array return string class Tool: 一个工具对象包含schema描述和真实执行函数 def __init__(self, schema: dict, executor: Callable): self.schema schema self.executor executor property def name(self): return self.schema[function][name] def run(self, arguments: dict): return self.executor(**arguments) def create_tool(func: Callable) - Tool: sig inspect.signature(func) hints get_type_hints(func) doc inspect.getdoc(func) or description doc.split(\n)[0] if doc else func.__name__ properties {} required [] for name, param in sig.parameters.items(): ptype hints.get(name, Any) prop { type: _convert_type(ptype), description: f参数 {name}类型 {ptype.__name__}, } if param.default is not inspect.Parameter.empty: prop[default] param.default properties[name] prop if param.default is inspect.Parameter.empty: required.append(name) schema { type: function, function: { name: func.__name__, description: description, parameters: { type: object, properties: properties, required: required, }, }, } return Tool(schema, func)这个转换器做了四件事解析函数签名拿到参数名和默认值。解析类型注解把Python类型映射成JSON Schema类型。从docstring里提取第一行作为工具描述。把原函数包装进Tool对象保留执行能力。来验证一下效果weather_tool create_tool(query_weather) print(json.dumps(weather_tool.schema, ensure_asciiFalse, indent2))你会看到输出正是前面章节展示的那个schema结构。注意date参数因为带默认值所以没有出现在required里而且包含default字段模型拿到这个信息之后就知道日期可传可不传。3.4 第三步让大模型自动调用工具并返回结果工具转换好了接下来就是接入模型。我写一个通用的请求函数和一个“Agent”入口函数把整个链路串起来。import requests def call_llm(messages, toolsNone): payload { model: MODEL_NAME, messages: messages, tools: tools or [], tool_choice: auto, temperature: 0.2, } resp requests.post(API_ENDPOINT, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json, }, jsonpayload, timeout30) resp.raise_for_status() return resp.json()这里有个细节值得注意tools参数我传的是schema对象列表也就是类似{type: function, function: {...}}的结构。不同模型平台对tools格式的要求略有差异有的要求只传function对象本身有的要求带上外层的type。我建议你先用一个固定的schema试一次确认平台格式再批量接入。然后是主流程def run_agent(user_input, registry): messages [{role: user, content: user_input}] resp call_llm(messages, registry.schemas()) msg resp[choices][0][message] if not msg.get(tool_calls): return msg[content] for call in msg[tool_calls]: tool registry.get(call[function][name]) args json.loads(call[function][arguments]) print(f 模型调用工具: {tool.name}, 参数: {args}) result tool.run(args) print(f 工具返回: {result}) messages.append(msg) messages.append({ role: tool, tool_call_id: call[id], content: json.dumps(result, ensure_asciiFalse), }) final_resp call_llm(messages, registry.schemas()) return final_resp[choices][0][message][content]这个函数支持一次响应里模型发起多个工具调用tool_calls是数组逐个执行完后再把结果汇总给模型。下面再补一个简单的注册中心class ToolRegistry: def __init__(self): self._tools {} def register(self, tool: Tool): self._tools[tool.name] tool def get(self, name: str): return self._tools.get(name) def schemas(self): return [t.schema for t in self._tools.values()]然后注册两个工具跑一次完整对话registry ToolRegistry() registry.register(create_tool(query_weather)) registry.register(create_tool(calculate)) answer run_agent(帮我查一下上海今天的天气, registry) print(最终回答:, answer)如果模型正确选用了天气工具你会看到类似这样的中间输出 模型调用工具: query_weather, 参数: {city: 上海, date: today} 工具返回: 上海 today多云 18~26℃ 最终回答: 上海今天多云气温18到26摄氏度。到这里一个自己写的Agent转换工具Demo就完成了。这个工作量其实不大但它的价值在于你亲手把“函数”到“Agent工具”的每一层包装都看清楚了。3.5 进阶多工具注册、异常兜底与路由优化上面是能跑的最小闭环。但放到真实项目里还需要几个进阶处理。第一个是异常兜底。工具执行过程很可能出错比如模型传了一个不存在的城市、算式的字符串里混入了非法字符。如果异常直接冒泡到上层整个Agent对话就崩了。正确做法是在Tool.run内部捕获异常返回一条“工具执行失败xxx”的消息给模型让模型自己决定是重试、换参数还是直接告诉用户。class Tool: def run(self, arguments: dict): try: return self.executor(**arguments) except Exception as e: return f工具执行失败: {type(e).__name__}: {e}第二个是参数的二次校验。模型生成的参数有时候会漏字段、多字段、类型不对。你可以在run_agent里对参数做一次轻量校验确保必填字段齐全。def safe_run(tool, args): schema_required tool.schema[function][parameters].get(required, []) for key in schema_required: if key not in args: return f缺少必要参数: {key} return tool.run(args)第三个是路由优化。当工具数量特别多的时候每次都把所有工具的schema发送给模型会消耗大量输入token。一个常见做法是给工具打上分类标签先用一个“粗粒度分类工具”让模型确定需求属于哪一类再只发送那一类下的详细工具列表。这个优化对于几十个工具的场景收益非常明显。4. 踩坑实录Agent工具转换的常见问题与排查技巧4.1 参数类型黑洞模型把城市名传成了数组我有一次调一个查询接口函数签名是def search(user_name: str)结果模型传了个{user_name: [张, 三]}进来。原因是模型的“想象力”太丰富了它觉得一个“用户姓名”字段可能是一个包含多个分词的数组。你如果事先不做校验函数内部的字符串方法直接崩掉。解决思路有两个方向。第一在schema的description里写清楚格式比如姓名字符串类型无需拆分给模型明确指令。第二在Tool.run里做类型强校验如果参数类型不符合预期就返回一条友好的错误消息让模型重新组织。千万不要因为怕麻烦就忽略这一步真实场景里模型传错参数的比例比我预期的高得多。4.2 描述信息太敷衍模型干脆不调用工具如果你给工具写的description是“查询天气”模型很可能在用户问“今天出门要不要带伞”的时候不调用它因为模型认为“带伞”和“天气”是两个话题。但如果你把description写成“查询指定城市和日期的天气情况包括气温、降水、风力等信息用于回答与天气、穿衣、出行相关的问题”模型就会精准匹配。这个经验我反复踩过。description写得好不好直接决定工具被调用的准确率。建议在description里包含两个部分工具的能力边界、适合回答的问题类型。不要写空话要写模型能“按图索骥”的触发词。4.3 返回结果花式报错类型、长度与序列化模型调用工具之后工具返回的结果要作为消息的一部分再次发给模型。这个返回内容的格式非常讲究。如果你返回的是Python对象比如datetime、Decimal直接进JSON序列化会报错如果你返回的内容特别长比如从数据库查出来的几百行记录又会占用大量token甚至超出模型的上下文限制。我的习惯是工具统一返回字符串如果需要结构化数据就用JSON字符串格式。同时在工具内部做一次“瘦身”只保留最重要的字段把结果控制在合理长度内。比如查询订单列表你可以只返回订单号、金额、状态三列详情让模型追问的时候再查。4.4 常见问题速查表我把实际操作中搜集到的高频问题整理成一个速查表方便你排查问题现象可能原因处理建议模型从不调用工具description不明确或tools传参格式错误重写description检查tools参数结构调用时报缺少参数必填字段没有出现在required里检查函数默认值确认是否需要required参数类型总是错schema类型定义不准确用get_type_hints检查类型注解工具执行报错但用户看到崩溃信息异常没有被捕获在Tool.run内包一层try-except返回结果太长模型回答混乱结果内容过多精简输出字段或者分段返回多工具同时调用时执行顺序乱未处理tool_calls数组循环遍历并逐步执行模型循环调用同一个工具工具返回结果无法满足用户需求检查工具返回值是否有实际信息量4.5 一个必须警惕的安全问题别让模型直接执行危险代码前面Demo里我用了eval这里要再次强调生产环境千万不要这么做。大模型的输入来自用户如果用户构造一段恶意字符串诱导模型调用“表达式计算”工具eval可以让任何代码在你的机器上运行。安全的替代方案是用专用的表达式解析库比如asteval它只解析数学表达式不执行系统调用。如果你只是处理四则运算甚至可以自己写一个几十行的递归下降解析器。安全这个问题怎么强调都不为过Agent工具赋予了模型调用真实世界资源的能力也就等于赋予了模型执行风险动作的通道必须层层设防。5. 我对Agent转换工具的几个实操心得5.1 我的选用原则轻量优先在实际项目里我不会一上来就引入重量级Agent编排框架。很多场景下一个create_tool函数加一个注册中心几百行代码就能覆盖80%的需求。这样做的最大好处是每一行逻辑我都完全清楚出了问题能直接定位。等到工具数量真的多到需要缓存、权限控制、多Agent协作的时候再考虑引入框架也不迟而且那时候你已经理解了底层原理用起框架来会顺手得多。另外我强烈建议保留一份标准化的schema定义。无论底层模型怎么换schema结构是通用的。这意味着你的工具层写得越标准将来迁移成本就越低。我见过不少团队因为工具定义和框架深度绑定换一次模型就要重写一遍工具层非常痛苦。5.2 这个小工具后续还能怎么扩展如果你愿意这个mini版转换工具还能往几个方向扩展支持pydantic模型作为参数类型这样复杂嵌套参数也能自动生成schema增加工具调用的日志埋点把每一次“模型选了哪个工具、传了什么参数、返回了什么结果”都记录下来方便复盘模型的表现做到一定规模后加入权限校验不同用户角色只能调用不同工具。我个人最推荐先做日志埋点这个扩展因为Agent开发本质上是一个“不断观察模型决策、持续优化提示和工具定义”的过程。没有日志你就像蒙着眼睛调模型效率极低。把观测做好再回头优化工具描述和参数设计效果会立竿见影。最后再分享一个小技巧给每个工具加一个元信息字段比如owner、version、更新时间。这样工具清单越来越长之后你依然能知道每个工具是谁维护的、上次改了什么。这个习惯帮我在多个项目里省下了大量排查时间建议你从第一天就养成。

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

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

免费获取报价 →
↑