尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大模型工具调用实战:从零构建Agent助手,打通AI与外部系统连接

大模型工具调用实战:从零构建Agent助手,打通AI与外部系统连接 这类项目最值得先看的不是功能列表而是能不能在普通环境里稳定跑起来。从只会“动嘴”到能“动手”核心是让大模型学会调用外部工具比如查时间、算算术、查天气、调API。这听起来简单但新手最容易卡在三个地方一是模型本身不支持工具调用二是工具描述JSON Schema写不对三是调用后的结果处理逻辑混乱。我建议先从最小样例开始把单次“提问-调用-返回”的链路跑通再考虑复杂的Agent循环和记忆。下面按实际落地顺序拆一遍。1. 先搞清楚“工具调用”到底在做什么很多人一上来就找框架、看文档但没想清楚工具调用的本质。它不是一个独立功能而是连接大模型“思考”和外部“执行”的桥梁。模型负责理解用户意图、选择工具、生成调用参数外部工具负责执行具体任务并返回结果模型再根据结果组织回答。1.1 核心流程从用户问题到最终回答一个完整的工具调用流程可以拆成五步用户提问比如“现在几点了”或“123乘以456等于多少”模型决策模型分析问题判断是否需要调用工具以及调用哪个工具。这依赖于你提前告诉模型有哪些工具可用。生成调用指令模型按照预定格式通常是JSON生成工具调用请求包含工具名和参数。外部执行你的程序接收到调用指令去真正执行查时间、计算等操作拿到结果。结果整合把执行结果返回给模型模型将其组织成自然语言回答给用户。整个过程里模型始终是“大脑”它不执行代码只发出指令。你的程序是“手”负责执行。工具调用就是让“大脑”能指挥“手”。1.2 关键依赖模型能力与工具描述不是所有模型都支持工具调用。你需要确认你用的模型无论是云端API还是本地部署是否开放了“Function Calling”或“Tool Use”能力。通常较新的GPT-4、Claude 3、GLM-4、DeepSeek等主流模型都支持。如果你用纯开源模型需要检查其是否针对工具调用做过微调。另一个关键是工具描述。你必须用模型能理解的方式告诉它“我这里有这些工具每个工具叫什么、干什么用、需要什么参数。”这个描述通常用JSON Schema来定义。描述写得不清楚模型就可能选错工具或生成错误的参数。2. 环境准备与最小验证跑通第一个工具调用不要一上来就想做复杂的Agent。先确保基础链路是通的。这里以使用OpenAI兼容的API或支持工具调用的本地模型为例。2.1 基础环境与依赖你需要一个Python环境建议3.8以上以及能调用大模型的客户端库。这里以openai库为例但它也兼容许多提供同类接口的本地模型服务。pip install openai如果你打算用本地模型可能需要额外安装对应的客户端库或运行一个兼容OpenAI API的模型服务如vLLM、Ollama、LM Studio等并确保其开启了工具调用支持。这是第一个容易踩坑的地方很多本地模型服务默认不开启或需要特定参数启用工具调用。2.2 定义你的第一个工具获取当前时间我们从最简单的工具开始一个返回当前时间的函数。这个工具不需要外部API容易验证。首先用JSON Schema描述这个工具{ type: function, function: { name: get_current_time, description: 获取当前的日期和时间。, parameters: { type: object, properties: { timezone: { type: string, description: 时区例如 Asia/Shanghai。如果不提供则使用系统默认时区。 } }, required: [] } } }关键点name: 工具的唯一标识模型调用时就靠这个名字。description:必须写清楚。模型靠这个描述来决定是否调用此工具。写“获取时间”就不如“获取当前的日期和时间”明确。parameters: 定义参数。这里timezone是可选的required列表为空。description字段对每个参数也同样重要。2.3 编写执行工具的函数在Python中我们需要一个能实际执行get_current_time的函数。import json from datetime import datetime import pytz # 需要安装pip install pytz def get_current_time(timezoneNone): 根据提供的时区返回当前时间。 if timezone: try: tz pytz.timezone(timezone) current_time datetime.now(tz) except pytz.exceptions.UnknownTimeZoneError: return f错误未知时区 {timezone}。 else: current_time datetime.now() # 返回一个结构化的结果方便模型读取 return { current_time: current_time.strftime(%Y-%m-%d %H:%M:%S), timezone: timezone if timezone else system default }2.4 组装并发送请求现在把工具描述传给模型并处理模型的响应。from openai import OpenAI # 初始化客户端如果是本地模型base_url需要改为本地服务地址 # client OpenAI(base_urlhttp://localhost:1234/v1, api_keynot-needed) client OpenAI() # 默认使用OpenAI官方API需要设置环境变量OPENAI_API_KEY # 1. 将工具描述准备好 tools [{ type: function, function: { name: get_current_time, description: 获取当前的日期和时间。, parameters: { type: object, properties: { timezone: { type: string, description: 时区例如 Asia/Shanghai。如果不提供则使用系统默认时区。 } }, required: [] } } }] # 2. 发送用户消息并告知模型可用的工具 response client.chat.completions.create( modelgpt-3.5-turbo, # 或你使用的其他模型 messages[ {role: user, content: 现在几点了} ], toolstools, # 关键在这里传入工具列表 tool_choiceauto, # 让模型自动决定是否调用工具 ) message response.choices[0].message print(模型原始响应:, message)运行这段代码你会看到模型的响应message内容。如果模型决定调用工具message对象会包含一个tool_calls列表里面是它想要发起的工具调用信息。2.5 处理工具调用并返回结果检查模型是否发起了工具调用如果有就执行对应的函数然后把结果返回给模型让模型生成最终回答。# 接上面的代码 if message.tool_calls: # 假设我们只处理第一个工具调用简单场景 tool_call message.tool_calls[0] tool_name tool_call.function.name tool_args_json tool_call.function.arguments # 根据工具名执行对应的函数 if tool_name get_current_time: # 解析参数 try: arguments json.loads(tool_args_json) except json.JSONDecodeError: arguments {} # 执行工具函数 tool_result get_current_time(**arguments) # 注意结果需要是字符串所以我们将字典转为JSON字符串 tool_result_str json.dumps(tool_result) else: tool_result_str 错误未知的工具。 # 3. 将工具执行结果作为新的消息追加并再次请求模型 second_response client.chat.completions.create( modelgpt-3.5-turbo, messages[ {role: user, content: 现在几点了}, message, # 包含工具调用的消息 { role: tool, content: tool_result_str, tool_call_id: tool_call.id # 必须对应之前的调用ID } ], toolstools, # 这里可以继续提供但通常第二次就不需要了 ) final_answer second_response.choices[0].message.content print(AI的最终回答:, final_answer) else: # 模型没有调用工具直接给出了回答 print(AI直接回答:, message.content)如果一切顺利你会看到类似“AI的最终回答: 现在是2024年5月15日 下午2点30分。”的输出。恭喜你完成了第一次工具调用。实测注意点第一次跑建议先用一个简单明确的问题如“现在几点了”。不要用“帮我看看时间”这种模糊表述初期测试要减少变量。如果模型没有调用工具而直接回答了时间比如它根据训练数据猜了一个这很正常。可以尝试更复杂的、必须调用工具才能回答的问题比如“纽约现在几点了”并在工具描述里强调时区参数。本地模型如果没反应首先检查服务是否支持工具调用其次检查tools参数格式是否正确。3. 扩展能力增加计算器与处理复杂逻辑跑通一个工具后增加第二个就简单了。我们加一个计算器工具并处理模型可能连续调用多个工具或一个工具多次调用的场景。3.1 定义计算器工具在tools列表里再添加一个工具描述{ type: function, function: { name: calculator, description: 执行数学计算。支持加()、减(-)、乘(*)、除(/)、乘方(**)等基本运算。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 (12 34) * 5.6。请确保表达式是明确且可计算的。 } }, required: [expression] } } }对应的执行函数def calculator(expression): 安全地评估数学表达式。 警告直接使用eval有安全风险仅用于演示。生产环境应用更安全的评估器。 try: # 极其简化的安全过滤生产环境请使用ast.literal_eval或专用库 allowed_chars set(0123456789-*/.() ) if not all(c in allowed_chars for c in expression): return 错误表达式中包含不安全字符。 result eval(expression) return {result: result, expression: expression} except Exception as e: return {error: str(e), expression: expression}3.2 处理多工具调用与循环当用户问“现在时间乘以2是多少”这种混合问题时模型可能需要先调用get_current_time再用结果调用calculator。这就需要我们实现一个循环直到模型不再发起新的工具调用为止。def run_conversation_with_tools(user_query, available_tools, tool_functions): 运行一个支持多轮工具调用的对话。 user_query: 用户问题 available_tools: 工具描述列表 tool_functions: 字典键为工具名值为对应的Python函数 messages [{role: user, content: user_query}] max_turns 5 # 防止无限循环 for turn in range(max_turns): # 1. 发送请求给模型 response client.chat.completions.create( modelgpt-3.5-turbo, messagesmessages, toolsavailable_tools, tool_choiceauto, ) message response.choices[0].message messages.append(message) # 将模型的响应加入历史 # 2. 检查是否调用了工具 if not message.tool_calls: # 没有工具调用对话结束 return message.content # 3. 处理所有工具调用 for tool_call in message.tool_calls: tool_name tool_call.function.name tool_args_json tool_call.function.arguments print(f工具调用: {tool_name}, 参数: {tool_args_json}) # 执行工具 if tool_name in tool_functions: try: arguments json.loads(tool_args_json) except json.JSONDecodeError: arguments {} # 调用对应的函数 tool_result tool_functions[tool_name](**arguments) tool_result_str json.dumps(tool_result) else: tool_result_str json.dumps({error: f工具 {tool_name} 未定义。}) # 4. 将工具执行结果加入消息历史 messages.append({ role: tool, content: tool_result_str, tool_call_id: tool_call.id }) # 循环继续模型将基于所有历史消息包含工具结果生成下一步响应 return 对话轮次过多可能陷入循环。 # 准备工具 available_tools [...] # 包含get_current_time和calculator的描述 tool_functions { get_current_time: get_current_time, calculator: calculator } # 测试复杂查询 final_answer run_conversation_with_tools( 先告诉我现在时间然后计算这个时间的小时数乘以60是多少, available_tools, tool_functions ) print(最终回答:, final_answer)这个循环结构是大多数简单Agent的核心。它让模型能根据上下文包含之前工具调用的结果决定下一步行动。3.3 参数验证与错误处理模型生成的参数可能不符合预期。比如它可能给计算器传一个“12点”这样的非数学表达式。因此在执行工具前和后都要有健壮的错误处理。执行前在工具函数内部对参数进行类型检查和有效性验证。例如计算器函数可以检查表达式是否只包含数字和运算符。执行后如果工具执行出错如除零错误将清晰的错误信息返回给模型。模型通常能理解错误并调整策略例如在回答中说“无法计算”。def robust_calculator(expression): 更健壮的计算器示例 if not isinstance(expression, str): return {error: 参数 expression 必须是字符串。} # 更严格的安全检查示例 import re safe_pattern r^[\d\s\\-\*\/\.\(\)]$ # 仅允许数字、空格、基础运算符和括号 if not re.match(safe_pattern, expression): return {error: 表达式包含不安全或无效字符。} try: result eval(expression) # 检查结果是否为有限数 if not isinstance(result, (int, float)) or not math.isfinite(result): return {error: 计算结果无效或非有限数。} return {result: result, expression: expression} except ZeroDivisionError: return {error: 除以零错误。} except Exception as e: return {error: f计算失败: {str(e)}}4. 走向实用构建可复用的Agent助手框架当工具增多、逻辑变复杂后就需要一个更结构化的框架。这里不依赖特定Agent框架如LangChain我们自己搭建一个轻量但清晰的结构理解其原理。4.1 设计工具注册与管理机制我们需要一个中心化的地方来注册、描述和调用工具。class Tool: 工具类封装描述和执行逻辑 def __init__(self, name, description, parameters_schema, func): self.name name self.description description self.parameters_schema parameters_schema self.func func def to_json_schema(self): 生成模型所需的JSON Schema格式 return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters_schema } } def execute(self, **kwargs): 执行工具并处理异常 try: result self.func(**kwargs) return {success: True, data: result} except Exception as e: return {success: False, error: str(e)} class ToolRegistry: 工具注册表 def __init__(self): self.tools {} def register(self, tool): self.tools[tool.name] tool def get_tool(self, name): return self.tools.get(name) def get_all_schemas(self): return [tool.to_json_schema() for tool in self.tools.values()] # 使用示例 registry ToolRegistry() # 注册时间工具 time_tool Tool( nameget_current_time, description获取当前的日期和时间。, parameters_schema{ type: object, properties: { timezone: {type: string, description: 时区如Asia/Shanghai} }, required: [] }, funcget_current_time ) registry.register(time_tool) # 注册计算器工具 calc_tool Tool( namecalculator, description执行数学计算。, parameters_schema{ type: object, properties: { expression: {type: string, description: 数学表达式} }, required: [expression] }, funcrobust_calculator ) registry.register(calc_tool)4.2 实现Agent执行循环基于注册表我们可以实现一个更清晰的Agent循环。class SimpleAgent: def __init__(self, model_client, tool_registry): self.client model_client self.registry tool_registry def run(self, user_input, max_turns10): messages [{role: user, content: user_input}] available_tools self.registry.get_all_schemas() for turn in range(max_turns): # 调用模型 response self.client.chat.completions.create( modelgpt-3.5-turbo, messagesmessages, toolsavailable_tools, tool_choiceauto, ) assistant_message response.choices[0].message messages.append(assistant_message) # 如果没有工具调用返回最终回答 if not assistant_message.tool_calls: return assistant_message.content # 处理每个工具调用 for tool_call in assistant_message.tool_calls: tool_name tool_call.function.name tool self.registry.get_tool(tool_name) if not tool: result json.dumps({error: f工具 {tool_name} 未找到。}) else: # 解析并执行 try: arguments json.loads(tool_call.function.arguments) except: arguments {} execution_result tool.execute(**arguments) # 将执行结果转为字符串 result json.dumps(execution_result) # 将结果加入消息历史 messages.append({ role: tool, content: result, tool_call_id: tool_call.id }) return 对话达到最大轮次可能未完成。 # 使用Agent agent SimpleAgent(client, registry) answer agent.run(计算一下(北京时间的小时数加上5)的平方。) print(answer)这个SimpleAgent类封装了对话管理、工具查找和执行循环。你可以轻松地通过registry.register()添加新工具而无需修改核心循环逻辑。4.3 处理复杂输出与状态管理现实中的工具可能返回复杂数据如图表、列表。模型需要能理解这些数据并组织回答。通常我们将结果以清晰的结构化格式如JSON返回并在工具描述中说明返回值的结构。此外Agent可能需要“记忆”。在上述循环中messages列表天然构成了对话历史记忆。但对于长对话可能需要压缩或总结历史以避免token超限。更复杂的Agent还会涉及“规划”先做什么后做什么和“反思”检查结果是否正确这通常需要更高级的提示工程或框架支持。5. 生产环境考量与常见问题排查当你把玩具Demo推向实际应用时会遇到一系列新问题。5.1 性能与稳定性超时控制工具执行可能很慢如调用外部API。要为每个工具调用设置超时防止整个Agent被卡住。重试机制对于可能临时失败的工具如网络请求实现有限次数的重试。限流与降级如果使用付费API注意调用频率限制。对于非关键工具要有降级方案如返回缓存数据或默认值。5.2 安全与权限工具权限隔离不是所有工具都应被所有用户调用。需要建立权限模型在调用前检查当前用户是否有权执行该操作。输入验证与净化永远不要相信模型生成的参数直接用于敏感操作如数据库查询、系统命令。必须进行严格的验证、转义或使用参数化查询。沙箱环境对于执行代码或访问文件系统的工具应在沙箱中运行。5.3 调试与监控详细日志记录每一次模型请求、响应、工具调用和结果。这对于排查问题至关重要。可观察性监控工具调用成功率、延迟、Token消耗等指标。测试套件为每个工具编写单元测试并为常见用户问题编写端到端集成测试。5.4 常见问题排查清单当你的Agent表现不如预期时按以下顺序排查模型根本不调用工具检查模型是否支持工具调用查阅其API文档。检查tools参数格式是否正确特别是JSON Schema是否符合规范。检查工具描述是否清晰。尝试将description写得更具体、更具指向性。尝试将tool_choice参数设为{type: function, function: {name: xxx}}来强制调用特定工具测试链路是否通畅。模型调用了错误工具或参数工具描述description和参数description是否足够区分不同工具避免使用模糊词汇。检查模型生成的参数JSON是否能被正确解析。打印出来看看格式。考虑在系统提示词systemmessage中更明确地指导模型如何使用工具。工具执行出错在工具函数内部添加更详细的日志和异常捕获。检查参数类型模型生成的字符串参数你的函数是否期待的是整数或浮点数检查依赖和环境工具函数依赖的外部服务是否可用本地命令是否存在Agent陷入循环或逻辑混乱限制最大对话轮次max_turns。检查工具返回的结果是否过于复杂或难以理解导致模型无法做出下一步决策。简化返回数据结构。在系统提示词中要求模型“逐步思考”或者实现一个“检查步骤是否合理”的验证环节。本地模型响应慢或不稳定确认本地模型的硬件资源GPU显存、内存是否充足。检查是否开启了正确的推理优化参数。考虑使用更轻量的模型或者将工具调用能力卸载到云端模型本地只做简单任务。5.5 进阶方向记忆、规划与多Agent协作基础工具调用之上是构建更智能Agent的方向记忆Memory让Agent记住之前的对话和工具调用结果。可以是简单的窗口记忆也可以是向量数据库存储的长期记忆。规划Planning对于复杂目标让模型先输出一个计划如“先调用A工具再用结果调用B工具”再逐步执行。这能提高复杂任务的完成率。多Agent协作创建多个具备不同工具集的Agent让它们通过对话协作解决问题。这需要更复杂的编排逻辑。我个人更建议先把单Agent、多工具调用的基础打牢。确保工具描述清晰、执行可靠、错误处理完善。这个基础稳固后再叠加记忆、规划等高级能力会水到渠成。很多项目的问题不是出在高级特性上而是基础的工具调用链路在边界情况下崩溃了。
返回列表