ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零实现AI工具调用:让大语言模型拥有执行能力

从零实现AI工具调用:让大语言模型拥有执行能力 你是不是也遇到过这样的场景想让你的AI助手帮你查一下明天的天气或者算一下这个月的开销结果它只能回复你“我是一个语言模型无法访问实时数据或进行计算。” 就像是一个知识渊博但被捆住手脚的顾问空有一身本领却无法真正帮你做事。这正是当前许多AI应用面临的“最后一公里”问题。模型很聪明能说会道但缺乏与现实世界交互的“手”。而Tool Use工具调用就是为模型装上这双“手”的关键技术。它让AI从“只会动嘴”的聊天机器人进化成“能查时间、算算术、调接口”的智能助手。上篇文章我们探讨了Agent和Tool Use的基础概念与架构。今天我们将彻底落地手把手教你从零实现一个具备工具调用能力的AI助手。你将看到如何定义工具、如何让模型理解并选择工具、如何安全地执行工具调用并最终构建一个能查询时间、进行数学计算的实用Agent。本文不仅提供可运行的完整代码更会深入探讨工程实践中的核心问题如何设计安全的工具执行环境如何处理工具调用失败如何让模型学会“组合使用”多个工具这些正是将AI从演示玩具变为生产级应用必须跨越的鸿沟。1. 这篇文章真正要解决的问题让AI拥有“可执行”的能力很多开发者初次接触Agent和Tool Use时容易陷入两个误区误区一认为这只是另一个API调用封装。不就是把函数调用包装一下吗实际上Tool Use的核心是让模型具备“意图识别”和“工具选择”的决策能力。模型需要理解用户的自然语言请求将其映射到最合适的工具上并自动生成正确的调用参数。这涉及到提示工程、函数描述、参数解析和错误处理的完整链路。误区二认为所有模型都能轻松实现工具调用。虽然OpenAI的GPT系列、Anthropic的Claude等主流模型原生支持function calling但如果你使用开源模型或特定领域的微调模型可能需要额外的训练或适配层。本文将重点讲解兼容性更强的通用实现方案。我们真正要解决的是如何在一个标准的开发流程中为你的AI应用注入“行动力”。具体来说你将学会定义工具如何用清晰的结构化描述如JSON Schema告诉模型“这个工具能做什么、需要什么参数”。集成决策如何让模型在对话中根据上下文自动判断“是否需要使用工具、使用哪个工具”。安全执行如何构建一个隔离、可控的工具执行环境防止任意代码执行等安全风险。处理结果如何将工具执行的结果“翻译”回自然语言并继续流畅的对话。无论你是想开发一个智能客服机器人、一个自动化办公助手还是一个复杂的AI工作流掌握Tool Use都是实现其真正实用价值的关键一步。2. 核心概念回顾Agent、Tool与Function Calling在深入代码之前我们先快速厘清几个关键概念确保我们在同一频道上。Agent智能体本文中Agent指的是一个能够感知环境用户输入、上下文、进行决策思考、规划、执行动作调用工具并从中学习的软件实体。它是整个系统的“大脑”。Tool工具指任何可以被Agent调用来完成特定任务的功能单元。它可以是一个简单的函数如计算器、一个Web API调用如天气查询、一个数据库查询甚至是一个操作系统的命令。工具是Agent的“手”和“脚”。Function Calling函数调用这是大语言模型如GPT-4原生支持的一种特殊格式的交互方式。开发者向模型提供一组函数工具的描述模型可以在认为需要时在回复中输出一个结构化的“调用请求”而不是自然语言。然后由应用程序解析这个请求真正执行函数并将结果返回给模型由模型生成最终回答。Function Calling是Tool Use的一种高效实现协议。JSON Schema一种用于描述JSON数据结构的标准。在Tool Use中我们用它来精确描述一个工具它的名称、描述、所需参数名称、类型、描述、是否必填等。这种结构化的描述比自然语言更能被模型准确理解。它们之间的关系你构建一个Agent它内部包含一个LLM大脑和一个Tool集合工具箱。当用户提出请求时Agent使用LLM分析如果判断需要工具则通过Function Calling机制或其他方式选择并格式化调用某个Tool执行后将结果反馈给LLM生成最终回答。整个过程的核心是让LLM理解和操作结构化的工具描述常使用JSON Schema。3. 环境准备与项目初始化我们将使用Python进行开发这是目前AI应用开发最活跃的生态。确保你的环境满足以下要求Python版本 3.8包管理工具pipLLM API本文将使用OpenAI的Chat Completions API作为示例因其对function calling支持完善。你也可以替换为其他支持类似功能的API如Azure OpenAI, Anthropic Claude等。第一步创建项目目录并初始化虚拟环境mkdir ai-assistant-with-tools cd ai-assistant-with-tools python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate第二步安装核心依赖我们将使用openai库与模型交互pydantic和json库来处理数据结构和工具描述。pip install openai pydantic第三步准备你的API密钥在项目根目录创建一个.env文件确保将其加入.gitignore并填入你的OpenAI API密钥。# .env OPENAI_API_KEY你的-api-key-here然后我们可以通过python-dotenv来加载它可选但推荐。pip install python-dotenv4. 核心架构设计一个可扩展的Tool Use系统一个健壮的Tool Use系统通常包含以下组件Tool基类/接口定义所有工具必须实现的方法如execute。Tool Registry工具注册表集中管理所有可用工具方便动态添加和查找。Agent核心集成LLM负责对话管理、决策是否及如何调用工具。Function Calling 处理器解析模型的工具调用请求路由到对应的Tool执行并格式化结果。安全沙箱/执行器可选但重要为不受信任的工具代码提供隔离的执行环境。我们将采用一种清晰、面向对象的方式来实现。首先定义我们的工具基类。5. 第一步定义工具基类与工具注册表我们创建一个tool.py文件。# tool.py import inspect import json from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Type, get_type_hints from pydantic import BaseModel, Field class Tool(ABC): 所有工具的抽象基类。 name: str description: str parameters_schema: Dict[str, Any] # 存储JSON Schema def __init__(self, name: str, description: str): self.name name self.description description self.parameters_schema self._generate_schema() abstractmethod def _generate_schema(self) - Dict[str, Any]: 生成该工具参数的JSON Schema。子类必须实现。 pass abstractmethod async def execute(self, **kwargs) - str: 执行工具的核心方法。子类必须实现。 pass def to_function_dict(self) - Dict[str, Any]: 将工具转换为OpenAI Function Calling所需的格式。 return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters_schema } } class ToolRegistry: 简单的工具注册表用于管理和查找工具。 def __init__(self): self._tools: Dict[str, Tool] {} def register(self, tool: Tool): 注册一个工具。 if tool.name in self._tools: raise ValueError(fTool with name {tool.name} is already registered.) self._tools[tool.name] tool def get_tool(self, name: str) - Optional[Tool]: 根据名称获取工具。 return self._tools.get(name) def get_all_tools(self) - List[Tool]: 获取所有已注册的工具。 return list(self._tools.values()) def get_all_function_definitions(self) - List[Dict[str, Any]]: 获取所有工具的Function Calling定义。 return [tool.to_function_dict() for tool in self._tools.values()]代码解释Tool是一个抽象基类强制每个具体工具都必须有name,description并能生成自己的参数模式(_generate_schema)和执行逻辑(execute)。to_function_dict方法将工具对象转换成OpenAI API要求的格式。ToolRegistry是一个简单的单例模式这里简化为实例管理器方便我们集中添加和获取工具。6. 第二步实现两个具体的工具查时间、算算术现在我们来创建两个简单的工具作为示例。创建tools/目录并在其中创建time_tool.py和calculator_tool.py。工具一获取当前时间 (tools/time_tool.py)这个工具不需要参数它返回当前系统时间。# tools/time_tool.py import datetime from typing import Dict, Any from ..tool import Tool class GetCurrentTimeTool(Tool): 一个用于获取当前日期和时间的工具。 def __init__(self): super().__init__( nameget_current_time, description获取当前的系统日期和时间。当用户询问时间、日期、现在几点、今天星期几时使用此工具。 ) def _generate_schema(self) - Dict[str, Any]: # 此工具不需要参数所以schema中properties为空 return { type: object, properties: {}, required: [] } async def execute(self, **kwargs) - str: # 执行工具逻辑 now datetime.datetime.now() # 返回一个结构化的字符串便于模型理解 return f当前时间是{now.strftime(%Y年%m月%d日 %H时%M分%S秒)}星期{[一,二,三,四,五,六,日][now.weekday()]}。工具二简单计算器 (tools/calculator_tool.py)这个工具需要参数我们使用Pydantic模型来定义参数并自动生成Schema。# tools/calculator_tool.py import math from typing import Dict, Any from pydantic import BaseModel, Field from ..tool import Tool # 首先用Pydantic定义计算器工具的参数模型 class CalculatorInput(BaseModel): operation: str Field( ..., description要执行的算术运算。支持add加, subtract减, multiply乘, divide除, power幂, sqrt平方根。, enum[add, subtract, multiply, divide, power, sqrt] ) a: float Field(None, description第一个运算数。对于sqrt操作这是唯一需要的数。) b: float Field(None, description第二个运算数。不适用于sqrt操作。) class CalculatorTool(Tool): 一个执行基本算术运算的计算器工具。 def __init__(self): super().__init__( namecalculator, description执行基本的数学运算如加、减、乘、除、幂和平方根。当用户需要进行数学计算时使用此工具。 ) def _generate_schema(self) - Dict[str, Any]: # 利用Pydantic模型的schema_json方法自动生成JSON Schema return CalculatorInput.schema() async def execute(self, **kwargs) - str: # 验证并解析输入参数 try: inputs CalculatorInput(**kwargs) except Exception as e: return f参数错误{e} op inputs.operation a inputs.a b inputs.b try: if op add: result a b return f{a} {b} {result} elif op subtract: result a - b return f{a} - {b} {result} elif op multiply: result a * b return f{a} × {b} {result} elif op divide: if b 0: return 错误除数不能为零。 result a / b return f{a} ÷ {b} {result} elif op power: result math.pow(a, b) return f{a} ^ {b} {result} elif op sqrt: if a 0: return 错误不能对负数求平方根。 result math.sqrt(a) return f√{a} {result} else: return f不支持的运算{op} except Exception as e: return f计算过程中发生错误{e}关键点参数建模使用Pydantic的BaseModel来定义工具参数可以自动生成高质量、类型安全的JSON Schema极大简化开发。描述清晰description字段至关重要它直接指导模型何时选择该工具。要写得具体、场景化。错误处理在execute方法中我们对输入验证和计算过程都进行了异常捕获返回友好的错误信息而不是让程序崩溃。7. 第三步构建Agent核心与对话循环现在我们创建agent.py文件实现Agent的核心逻辑。它将负责与LLM交互、管理对话历史、处理工具调用。# agent.py import json import asyncio from typing import List, Dict, Any, Optional import openai from dotenv import load_dotenv import os from tool import ToolRegistry from tools.time_tool import GetCurrentTimeTool from tools.calculator_tool import CalculatorTool # 加载环境变量 load_dotenv() class AssistantAgent: AI助手Agent具备工具调用能力。 def __init__(self, model: str gpt-3.5-turbo): self.model model self.client openai.OpenAI(api_keyos.getenv(OPENAI_API_KEY)) self.conversation_history: List[Dict[str, str]] [] # 初始化工具注册表并注册工具 self.tool_registry ToolRegistry() self.tool_registry.register(GetCurrentTimeTool()) self.tool_registry.register(CalculatorTool()) # 获取所有工具的函数定义用于后续对话 self.available_functions self.tool_registry.get_all_function_definitions() # 创建一个工具名称到工具对象的映射便于快速执行 self.tool_map {tool.name: tool for tool in self.tool_registry.get_all_tools()} def _add_message(self, role: str, content: str): 向对话历史添加一条消息。 self.conversation_history.append({role: role, content: content}) async def process_user_input(self, user_input: str) - str: 处理用户输入可能涉及多轮工具调用。 # 将用户输入加入历史 self._add_message(user, user_input) # 开始与模型交互的循环直到模型返回最终答案或出错 final_response await self._chat_cycle() return final_response async def _chat_cycle(self) - str: 执行一轮可能包含多次工具调用的对话循环。 max_turns 5 # 防止无限循环设置最大工具调用轮次 for _ in range(max_turns): # 准备发送给API的消息包括历史对话和工具定义 messages_for_api self.conversation_history.copy() try: response self.client.chat.completions.create( modelself.model, messagesmessages_for_api, toolsself.available_functions, # 关键告诉模型有哪些工具可用 tool_choiceauto, # 让模型自动决定是否以及调用哪个工具 ) except Exception as e: return f调用模型API时出错{e} response_message response.choices[0].message tool_calls response_message.tool_calls # 将模型的回复无论是普通消息还是工具调用请求加入历史 self.conversation_history.append(response_message.to_dict()) # 情况1模型直接给出了最终回复 if not tool_calls: final_content response_message.content return final_content if final_content else 模型返回了空内容。 # 情况2模型要求调用工具 # 可能有多个工具调用并行我们按顺序处理 for tool_call in tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) # 查找并执行工具 tool_to_call self.tool_map.get(function_name) if not tool_to_call: tool_response f错误工具 {function_name} 未找到或未注册。 else: # 执行工具 tool_response await tool_to_call.execute(**function_args) # 将工具执行结果作为一条“工具”角色的消息加入历史供模型参考 self.conversation_history.append({ role: tool, tool_call_id: tool_call.id, content: tool_response, }) # 工具调用结果已加入历史循环继续模型将基于新历史生成下一轮回复 # 如果循环达到最大次数仍未返回最终答案 return 对话轮次过多可能陷入了循环。请检查工具逻辑或用户请求。 def clear_history(self): 清空对话历史。 self.conversation_history.clear()核心逻辑解析初始化Agent初始化时注册所有可用工具并准备好工具定义(available_functions)。对话循环 (_chat_cycle)这是核心函数。它将当前对话历史和工具定义发送给LLM。LLM可能返回两种结果A) 直接给出最终答案content不为空tool_calls为空。B) 返回一个或多个工具调用请求tool_calls不为空。如果是情况A直接返回答案循环结束。如果是情况B则遍历每个工具调用请求解析函数名和参数 - 从注册表中找到对应工具 -安全地执行- 将执行结果以特定格式(role: tool)追加到对话历史中。关键点将工具执行结果追加后立即开始下一轮循环。模型会看到“用户提问 - 我要求调用工具 - 工具返回了结果”的完整上下文从而基于工具结果生成面向用户的最终回答。这个过程可能重复多次直到模型认为不需要再调用工具为止。8. 第四步编写主程序并测试最后我们创建一个main.py来运行我们的AI助手。# main.py import asyncio from agent import AssistantAgent async def main(): print( * 50) print(AI助手启动成功) print(我已装备以下工具) print( 1. get_current_time - 查询当前时间) print( 2. calculator - 进行数学计算 (加/减/乘/除/幂/平方根)) print(你可以尝试问我现在几点了 或 计算一下125乘以88等于多少) print(输入 quit 或 退出 结束对话。) print( * 50) agent AssistantAgent() while True: try: user_input input(\n你).strip() if user_input.lower() in [quit, 退出, exit]: print(AI助手再见) break if not user_input: continue print(AI助手思考中...) response await agent.process_user_input(user_input) print(fAI助手{response}) except KeyboardInterrupt: print(\n\n检测到中断退出程序。) break except Exception as e: print(f\n程序运行出错{e}) # 可以选择清空历史或继续 # agent.clear_history() if __name__ __main__: asyncio.run(main())9. 运行结果与效果验证现在让我们运行程序并测试几个典型场景。启动程序python main.py测试场景1查询时间你现在几点了 AI助手思考中... AI助手当前时间是2023年10月27日 14时30分15秒星期五。验证点模型正确识别了查询时间的意图选择了get_current_time工具该工具无需参数执行后返回了结构化的时间信息模型将其组织成自然语言回复。测试场景2简单计算你125乘以88等于多少 AI助手思考中... AI助手125 × 88 11000.0验证点模型识别出数学计算意图选择了calculator工具并正确地将“乘以”映射为operation: multiply参数a:125, b:88。工具执行计算并返回公式和结果模型将其呈现给用户。测试场景3复杂请求与多轮对话你先帮我计算一下15的平方根然后告诉我现在的时间。 AI助手思考中... AI助手√15 3.872983346207417。当前时间是2023年10月27日 14时32分01秒星期五。验证点这是一个组合请求。模型展示了规划能力。它可能先调用calculator工具计算平方根将结果存入上下文再调用get_current_time工具获取时间最后将两个工具的结果整合成一句连贯的回复。这体现了Agent的初步推理能力。测试场景4处理错误你计算5除以0。 AI助手思考中... AI助手错误除数不能为零。验证点工具内部的错误处理逻辑生效返回了友好的错误信息模型直接将该信息传递给了用户。10. 常见问题与排查思路在实际开发中你可能会遇到以下问题问题现象可能原因排查方式解决方案模型不调用工具直接回答1. 工具描述(description)不清晰或与问题不匹配。2. 模型版本不支持或对tool use支持不佳。3. 用户问题太简单模型认为无需工具。1. 检查available_functions是否正确传递给API。2. 查看模型的原始回复 (response_message)确认是否有tool_calls字段。3. 优化工具描述使其更精准覆盖用户意图。1. 使用明确支持function calling的模型如gpt-3.5-turbo或gpt-4。2. 重写工具描述使用“当用户询问...时使用此工具”的句式。3. 在系统提示词中明确鼓励模型使用工具。工具调用参数错误1. 生成的JSON Schema与模型解析不匹配。2. 用户问题模糊模型参数推断错误。1. 打印function_args查看模型生成的参数。2. 验证CalculatorInput等Pydantic模型是否能成功解析这些参数。1. 确保JSON Schema格式完全符合OpenAI规范。2. 在工具描述中明确参数类型和示例。3. 在execute方法开头加强参数验证和类型转换。无限循环或多次调用1. 工具执行结果格式不佳模型无法理解。2. 工具未能解决用户问题模型反复尝试。1. 设置最大循环次数如代码中的max_turns。2. 打印每一轮的对话历史观察模型和工具的交互过程。1. 确保工具返回的结果是清晰、结构化的文本。2. 在工具返回中可加入“指令”如“根据以上信息请回答用户...”。3. 优化系统提示词指导模型何时停止。API调用超时或失败1. 网络问题。2. API密钥无效或额度不足。3. 请求频率过高。1. 检查网络连接。2. 检查OpenAI控制台确认密钥和额度。1. 增加请求超时设置。2. 实现重试机制和退避策略。3. 使用异步请求避免阻塞。安全风险工具执行任意代码工具execute方法中直接执行了不可信的代码如eval。审查所有工具的execute方法实现。绝对禁止在工具中直接eval用户输入。对于需要执行代码的工具必须使用严格的沙箱环境如Docker、seccomp。11. 最佳实践与工程建议将Tool Use投入生产环境需要考虑更多工程化细节工具设计原则单一职责一个工具只做一件事。描述精准name和description要清晰无歧义这是模型选择的依据。结果结构化工具返回的结果应尽量结构化、机器可读同时包含自然语言描述便于模型和用户理解。幂等性尽可能让工具执行幂等操作避免因重复调用产生副作用。安全与沙箱输入验证在工具执行前必须严格验证所有参数类型、范围、格式。权限控制为不同工具分配不同的执行权限避免工具过度访问系统资源。沙箱执行对于执行外部命令、运行代码的工具务必在隔离的沙箱环境如Docker容器中运行并设置资源限制和超时。性能与可靠性异步执行如示例所示使用async/await处理I/O密集型工具如网络请求避免阻塞主线程。超时与重试为工具调用和API调用设置合理的超时并实现重试逻辑。缓存对频繁调用、结果不变或变化缓慢的工具如某些数据查询引入缓存机制。可观测性与调试详细日志记录完整的对话历史、工具调用请求、参数、执行结果和耗时。这对调试复杂问题至关重要。链路追踪为每个用户会话生成唯一ID便于追踪一个请求的完整处理链路。监控告警监控工具调用失败率、延迟和模型API的消耗。进阶模式动态工具注册支持在运行时加载、注册和卸载工具实现热更新。工具组合与规划实现更高级的Agent能够自动将复杂任务分解为多个工具调用的子任务并排序规划。工具学习记录模型成功和失败的工具调用案例用于优化工具描述或微调模型。通过本文的实践你已经掌握了为AI模型装上“手”的核心方法。从定义工具、集成决策到安全执行我们构建了一个完整且可扩展的Tool Use系统。这不仅仅是让AI“能干活”更是打开了连接数字世界无限服务的大门。下一步你可以尝试集成更强大的工具如网络搜索、数据库查询、发送邮件或探索开源模型如Llama 3、Qwen的本地化Tool Use方案打造真正属于你自己的、功能强大的AI智能体。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进