AI Agent工具调用实战:用JSON Schema赋予大模型“动手”能力

📅 2026/8/24 3:25:24
AI Agent工具调用实战:用JSON Schema赋予大模型“动手”能力
这次我们来看一个 AI 助手开发的核心进阶主题如何让一个只会“动嘴”的大语言模型真正拥有调用外部工具的“手”。这不仅仅是概念而是决定你的 AI 助手能否从聊天机器人升级为自动化工作流执行者的关键一步。本文将聚焦于Agent 的 Tool Use工具调用能力特别是其实现核心——JSON Schema的定义与使用。如果你正在开发一个能查天气、算汇率、发邮件、操作数据库的智能助手或者想理解主流 AI 平台如 OpenAI、DeepSeek、智谱等的 Function Calling 背后原理这篇文章将提供一套从理论到实践的完整路径。我们会重点关注如何定义工具、如何让模型理解并选择工具、以及如何安全地执行工具调用最终构建一个能真正“动手”的 AI Agent。1. 核心能力速览在深入代码之前我们先快速了解实现 AI 助手工具调用的核心要素和门槛。能力项说明与要求核心目标让大语言模型LLM能够理解用户意图并调用预设的外部工具函数/API来执行具体任务。技术基石JSON Schema用于以结构化、机器可读的方式向模型描述工具的功能、输入参数及格式。模型要求需支持 Function Calling / Tool Use 能力的模型。例如GPT-4/3.5-Turbo, Claude 3, DeepSeek-V2-Chat, GLM-4, Qwen-Max 等。本地模型如 Llama 3.1 等经过微调的版本也可能支持。开发门槛中低。主要工作是定义清晰的工具 Schema 和编写可靠的工具函数无需复杂的机器学习知识。硬件门槛无特定要求。工具调用逻辑运行在您的应用服务器上主要消耗的是调用大模型 API 或运行本地模型的资源。对于纯工具调用框架部分普通 CPU 服务器即可。关键输出模型返回一个结构化的Tool Call请求包含要调用的工具名和参数由您的程序解析并执行。安全边界必须严格定义工具的可访问范围和参数校验防止模型越权操作如删除文件、调用危险 API。2. 适用场景与使用边界适合谁AI 应用开发者希望为自己的产品增加自动化任务处理能力。技术爱好者/研究者想要深入理解 Agent 和工具调用的工作原理。企业流程自动化将重复的、规则明确的查询或操作如数据检索、报告生成交给 AI 助手处理。能解决什么问题信息实时性让模型能获取训练数据截止日期之后的信息如查天气、股价。精准计算让模型执行复杂的数学运算、单位换算、日期计算等避免其“臆造”答案。执行具体操作连接现实世界如发送邮件、创建日历事件、查询数据库、控制智能设备。突破文本限制处理非文本任务如图像生成、语音合成、代码执行在沙箱中。不适合什么场景完全开放、无约束的决策工具调用应基于明确、安全的预设工具集不适合让模型自由探索未知系统。替代所有人工流程复杂、需要深层领域知识或主观判断的任务仍需要人工监督。实时性要求极高的控制如工业控制、自动驾驶目前的延迟和可靠性尚不足以胜任。安全与合规边界权限最小化每个工具只授予完成其功能所需的最小权限。输入验证对所有从模型接收的参数进行严格的类型、范围、格式校验。沙箱环境对于执行代码、访问文件系统等高风险操作必须在隔离的沙箱环境中进行。用户确认对于发送邮件、修改数据、支付等关键操作应设计用户确认环节。审计日志记录所有的工具调用请求、参数和执行结果便于追踪和复盘。3. 环境准备与前置条件在开始编码前请确保你的开发环境已就绪。Python 环境推荐 Python 3.8 及以上版本。使用conda或venv创建独立的虚拟环境。# 创建虚拟环境 python -m venv agent_env # 激活环境 (Linux/macOS) source agent_env/bin/activate # 激活环境 (Windows) agent_env\Scripts\activate大模型访问权限方案AAPI调用准备一个支持 Function Calling 的模型 API Key如 OpenAI , DeepSeek , 智谱AI , 百度千帆 等。这是最快捷的方式。方案B本地模型如果你使用本地部署的模型如通过ollama,vLLM,LM Studio部署的 Llama 3.1、Qwen 等需确认该模型版本支持工具调用并准备好相应的推理服务地址。核心依赖库我们将使用openai兼容的客户端库它已成为行业标准接口。pip install openai如果你使用其他兼容 OpenAI API 的平-台可能还需要安装其特定的 SDK但核心的openai库通常足够。可选工具库根据你计划实现的工具类型可能需要安装# 用于 HTTP 请求例如查询天气 API pip install requests # 用于日期时间计算 pip install python-dateutil # 用于数学计算如果需要更复杂的计算 pip install numpy4. 核心概念从 Prompt 到 JSON Schema在传统的提示词工程中我们通过自然语言描述来“教”模型做事。但对于工具调用我们需要更精确、无歧义的描述方式。这就是JSON Schema的用武之地。JSON Schema是一种用于描述 JSON 数据结构的标准。在工具调用中我们用它来定义工具Tool是什么一个函数或一个 API。工具需要什么输入Parameters每个参数的名称、类型、描述、是否必填。工具会返回什么Returns返回值的类型和结构。4.1 一个简单的工具定义示例假设我们要定义一个“获取当前时间”的工具。用自然语言描述是“请告诉我现在的时间”。但用 JSON Schema 定义则是{ type: function, function: { name: get_current_time, description: 获取当前的日期和时间。当用户询问时间、日期、现在几点、今天星期几时调用此工具。, parameters: { type: object, properties: { timezone: { type: string, description: 时区例如 Asia/Shanghai 或 UTC。如果用户未指定默认为 Asia/Shanghai。, default: Asia/Shanghai }, format: { type: string, description: 时间输出格式。可选值full (包含日期和时间), time_only (仅时间), date_only (仅日期)。默认为 full。, enum: [full, time_only, date_only], default: full } }, required: [] } } }关键点解析name: 工具的唯一标识符模型在决定调用时会使用这个名字。description:至关重要用清晰、具体的自然语言描述工具的功能和调用时机。这是模型理解工具用途的主要依据。parameters: 定义输入参数。type: “object”: 表示参数是一个 JSON 对象。properties: 定义对象中的各个字段。每个字段都有type,description, 还可以有default(默认值),enum(枚举值) 等约束。required: 列出哪些参数是调用时必须提供的。本例中两个参数都有默认值所以都不是必填。4.2 为什么 JSON Schema 比自然语言 Prompt 更有效结构化模型可以精确地解析出参数名和类型生成格式正确的调用请求。无歧义enum和type限制了输入的范围避免了模型“自由发挥”。自动化后端程序可以依据 Schema 自动进行参数校验和类型转换。生态兼容OpenAI 的 Function Calling、Google 的 Tool Calling 等都采用类似的 Schema 格式学习一次多处适用。5. 实战构建一个具备工具调用能力的 AI 助手我们将一步步构建一个能查时间、做算术、查天气的简易 AI 助手。5.1 步骤一定义工具集首先我们创建三个工具的 JSON Schema 定义。# tools_schema.py import json def get_tools_definition(): 返回我们定义的三个工具的Schema列表 tools [ { type: function, function: { name: get_current_time, description: 获取当前的日期和时间。当用户询问时间、日期、现在几点、今天星期几时调用此工具。, parameters: { type: object, properties: { timezone: { type: string, description: 时区例如 Asia/Shanghai 或 UTC。如果用户未指定默认为 Asia/Shanghai。, default: Asia/Shanghai }, format: { type: string, description: 时间输出格式。可选值full (包含日期和时间), time_only (仅时间), date_only (仅日期)。默认为 full。, enum: [full, time_only, date_only], default: full } }, required: [] } } }, { type: function, function: { name: calculator, description: 执行数学计算。支持加()、减(-)、乘(*)、除(/)、乘方(^)等基本运算。当用户需要进行数学计算时调用。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 2 3 * (5 - 1)。请确保表达式是明确且可计算的。 } }, required: [expression] } } }, { type: function, function: { name: get_weather, description: 查询指定城市的当前天气情况。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如 北京、Shanghai。 }, country_code: { type: string, description: 国家代码用于更精确地定位城市例如 CN。可选。, default: CN } }, required: [city] } } } ] return tools # 可以打印出来看看结构 if __name__ __main__: print(json.dumps(get_tools_definition(), indent2, ensure_asciiFalse))5.2 步骤二实现工具函数接下来为每个 Schema 编写实际执行的 Python 函数。# tool_functions.py import datetime import pytz # 需要安装: pip install pytz import math import requests import json # ---------- 工具1获取当前时间 ---------- def execute_get_current_time(timezoneAsia/Shanghai, formatfull): 根据时区和格式返回当前时间 try: tz pytz.timezone(timezone) now datetime.datetime.now(tz) if format full: result now.strftime(%Y-%m-%d %H:%M:%S %Z%z) elif format date_only: result now.strftime(%Y-%m-%d) elif format time_only: result now.strftime(%H:%M:%S) else: result f未知格式: {format} return {success: True, result: result, message: f当前时间 ({timezone}, {format}): {result}} except Exception as e: return {success: False, result: None, message: f获取时间失败: {str(e)}} # ---------- 工具2计算器 ---------- def execute_calculator(expression): 安全地计算数学表达式 # 安全考虑移除可能危险的字符仅允许基本数学运算符、数字、括号和空格 allowed_chars set(0123456789-*/.()^% ) if not all(c in allowed_chars for c in expression): return {success: False, result: None, message: 表达式包含不安全字符} # 替换乘方符号 expression_safe expression.replace(^, **) try: # 警告使用 eval 存在风险仅用于演示。生产环境应使用更安全的表达式求值库如 ast.literal_eval 配合自定义解析。 # 此处我们做了简单的字符过滤但依然不绝对安全。 result eval(expression_safe, {__builtins__: {}}, {math: math}) return {success: True, result: result, message: f{expression} {result}} except Exception as e: return {success: False, result: None, message: f计算失败: {str(e)}} # ---------- 工具3查询天气模拟 ---------- def execute_get_weather(city, country_codeCN): 模拟查询天气实际应调用真实API # 这里模拟一个API响应。真实场景下你需要调用如和风天气、OpenWeatherMap等服务的API。 # 示例使用 requests.get(fhttps://api.weatherapi.com/...?keyYOUR_KEYq{city}) # 为了演示我们返回模拟数据 weather_data { Beijing: {temp_c: 22, condition: Sunny, humidity: 40}, Shanghai: {temp_c: 25, condition: Cloudy, humidity: 65}, Guangzhou: {temp_c: 28, condition: Rainy, humidity: 85}, } city_key city.capitalize() if city_key in weather_data: data weather_data[city_key] message f{city}的天气温度 {data[temp_c]}°C{data[condition]}湿度 {data[humidity]}%。 return {success: True, result: data, message: message} else: # 模拟API调用失败或城市不存在 return {success: False, result: None, message: f无法获取{city}的天气信息。} # 工具名称到执行函数的映射 TOOL_EXECUTORS { get_current_time: execute_get_current_time, calculator: execute_calculator, get_weather: execute_get_weather, } def execute_tool(tool_name, tool_arguments): 根据工具名称和参数执行对应的工具 if tool_name not in TOOL_EXECUTORS: return {success: False, result: None, message: f未知工具: {tool_name}} executor TOOL_EXECUTORS[tool_name] # tool_arguments 是一个字典例如 {timezone: UTC, format: full} return executor(**tool_arguments)5.3 步骤三与大模型交互的主循环这是核心部分我们将工具 Schema 提供给模型处理模型的响应并执行工具调用。# agent_core.py import os from openai import OpenAI from tools_schema import get_tools_definition from tool_functions import execute_tool import json # 初始化客户端 # 方式1: 使用 OpenAI 官方 API client OpenAI(api_keyos.environ.get(OPENAI_API_KEY)) # 方式2: 使用兼容 OpenAI API 的本地服务或其它平台 # client OpenAI(base_urlhttp://localhost:11434/v1, api_keyollama) # Ollama 示例 # client OpenAI(base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, api_keyYOUR_DASHSCOPE_KEY) # 通义千问示例 def chat_with_tools(user_input, conversation_history[]): 与支持工具调用的模型进行一轮对话。 Args: user_input: 用户当前输入 conversation_history: 之前的对话历史格式为 [{role: user/assistant/tool, content: ...}, ...] Returns: tuple: (assistant_response_text, updated_conversation_history, tool_calls_info) # 1. 将用户输入加入历史 new_history conversation_history.copy() new_history.append({role: user, content: user_input}) # 2. 获取工具定义 tools get_tools_definition() # 3. 调用模型并传入工具定义 try: response client.chat.completions.create( modelgpt-3.5-turbo, # 或 gpt-4, deepseek-chat, qwen-max 等 messagesnew_history, toolstools, tool_choiceauto, # 让模型自行决定是否调用工具 max_tokens1000, ) except Exception as e: return f调用模型API失败: {str(e)}, new_history, None # 4. 获取模型的响应消息 message response.choices[0].message new_history.append(message.to_dict()) # 将助手的响应加入历史 tool_calls_info [] # 5. 检查模型是否要求调用工具 if message.tool_calls: # 模型可能要求调用多个工具 for tool_call in message.tool_calls: tool_name tool_call.function.name try: # 解析模型传来的参数JSON字符串 tool_args json.loads(tool_call.function.arguments) except json.JSONDecodeError: tool_args {} print(f警告无法解析工具 {tool_name} 的参数) # 执行工具 print(f[Agent] 正在执行工具: {tool_name}, 参数: {tool_args}) tool_result execute_tool(tool_name, tool_args) # 将工具执行结果以特定格式追加到对话历史中供模型在下轮参考 new_history.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(tool_result, ensure_asciiFalse), # content 字段通常包含工具执行的结果信息模型会读取它来生成最终回答 }) tool_calls_info.append({ name: tool_name, args: tool_args, result: tool_result }) # 6. 工具执行后需要将包含工具结果的历史再次发送给模型让它生成面向用户的最终回答 try: second_response client.chat.completions.create( modelgpt-3.5-turbo, messagesnew_history, # 此时历史包含了工具执行结果 max_tokens1000, ) final_message second_response.choices[0].message new_history.append(final_message.to_dict()) final_text final_message.content except Exception as e: final_text f模型处理工具结果失败: {str(e)} else: # 模型没有调用工具直接返回文本响应 final_text message.content return final_text, new_history, tool_calls_info # 简单的主循环用于测试 def run_simple_cli(): print(AI 助手已启动支持工具调用时间、计算、天气。输入 quit 退出。) history [] while True: try: user_input input(\nYou: ).strip() except (EOFError, KeyboardInterrupt): print(\n再见) break if user_input.lower() in [quit, exit, q]: print(再见) break if not user_input: continue response, history, tool_calls chat_with_tools(user_input, history) print(fAssistant: {response}) if tool_calls: print(f[调试] 本轮调用了 {len(tool_calls)} 个工具。) if __name__ __main__: # 设置你的 API Key (临时方式生产环境请使用环境变量或配置管理) # os.environ[OPENAI_API_KEY] your-api-key-here run_simple_cli()5.4 步骤四运行与测试设置 API Key在运行前请确保设置了正确的环境变量。# Linux/macOS export OPENAI_API_KEYyour-api-key-here # Windows (PowerShell) $env:OPENAI_API_KEYyour-api-key-here运行程序python agent_core.py测试对话You: 现在几点了 [Agent] 正在执行工具: get_current_time, 参数: {} Assistant: 现在是 2024-05-27 14:30:15 CST0800。 You: 帮我计算一下 (15 7) * 3 等于多少 [Agent] 正在执行工具: calculator, 参数: {expression: (15 7) * 3} Assistant: (15 7) * 3 的计算结果是 66。 You: 北京天气怎么样 [Agent] 正在执行工具: get_weather, 参数: {city: 北京} Assistant: 北京的天气温度 22°CSunny湿度 40%。 You: 用UTC时间告诉我日期。 [Agent] 正在执行工具: get_current_time, 参数: {timezone: UTC, format: date_only} Assistant: 当前UTC日期是 2024-05-27。效果验证成功标志1模型能正确识别用户意图并选择对应的工具如问时间调用get_current_time。成功标志2模型能正确提取并结构化参数如从“用UTC时间告诉我日期”中提取{“timezone”: “UTC”, “format”: “date_only”}。成功标志3工具函数被成功执行并将结果返回给模型。成功标志4模型能基于工具返回的结果生成自然、流畅的最终回答给用户。6. 接口 API 与批量任务设计将上述核心逻辑封装成 Web API 服务是将其集成到其他应用的标准做法。同时我们也可以设计批量任务处理机制。6.1 基于 FastAPI 的 Web API 服务# app.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional import uvicorn from agent_core import chat_with_tools # 导入我们之前写好的核心函数 app FastAPI(titleAI Agent with Tool Use API) class ChatRequest(BaseModel): message: str conversation_id: Optional[str] None # 用于跟踪不同会话 # 可以扩展更多参数如 model_name, temperature 等 class ChatResponse(BaseModel): response: str conversation_id: str tool_calls: Optional[List[dict]] None # 简单的内存会话存储生产环境应使用Redis或数据库 conversation_store {} app.post(/chat, response_modelChatResponse) async def chat_endpoint(request: ChatRequest): 处理单轮对话 conv_id request.conversation_id or default_session # 获取或初始化该会话的历史 history conversation_store.get(conv_id, []) # 调用核心逻辑 response_text, updated_history, tool_calls chat_with_tools(request.message, history) # 保存更新后的历史 conversation_store[conv_id] updated_history return ChatResponse( responseresponse_text, conversation_idconv_id, tool_callstool_calls ) app.post(/chat/batch) async def batch_chat_endpoint(requests: List[ChatRequest]): 批量处理对话请求顺序执行 responses [] for req in requests: try: resp await chat_endpoint(req) # 复用单个处理逻辑 responses.append(resp.dict()) except Exception as e: responses.append({error: str(e), conversation_id: req.conversation_id}) return {results: responses} app.get(/tools) async def list_tools(): 列出所有可用的工具及其Schema from tools_schema import get_tools_definition return {tools: get_tools_definition()} if __name__ __main__: # 启动服务 uvicorn app:app --host 0.0.0.0 --port 8000 --reload uvicorn.run(app, host0.0.0.0, port8000)启动与调用# 安装 FastAPI 和 Uvicorn pip install fastapi uvicorn # 启动服务 uvicorn app:app --host 127.0.0.1 --port 8000 --reloadAPI 调用示例 (使用 curl)# 单轮对话 curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {message: 计算 2 的 10 次方, conversation_id: test_user_1} # 查看可用工具 curl http://127.0.0.1:8000/tools6.2 批量任务处理建议对于需要处理大量独立任务的场景如处理 CSV 文件中的每行查询建议任务队列使用CeleryRedis或RQ将任务放入队列异步执行避免 HTTP 请求超时。限流与重试对模型 API 的调用进行限流并为失败的任务设计指数退避重试机制。结果持久化将每个任务的结果用户输入、模型响应、工具调用详情、最终输出存储到数据库如 SQLite、PostgreSQL中便于追踪和审计。进度反馈对于长时间运行的批量任务提供任务状态查询接口。7. 资源占用与性能观察工具调用框架本身的资源消耗很低性能瓶颈主要在于大模型 API 调用。延迟主要来源网络延迟与模型 API 服务端的往返时间。模型推理时间模型生成响应和思考是否调用工具的时间。工具执行时间如果工具需要调用外部 API如查询真实天气则受该 API 速度影响。优化建议缓存对频繁查询且结果变化不快的工具如天气可缓存 10 分钟添加缓存层。并行工具调用如果模型一次性返回多个互不依赖的tool_calls可以并行执行这些工具。精简上下文在长时间对话中适时总结或裁剪过长的历史消息以减少发送给模型的 token 数量降低成本和延迟。选择合适模型对于工具调用场景gpt-3.5-turbo通常已表现良好且成本更低。如果逻辑非常复杂再考虑gpt-4。监控指标Token 使用量监控输入和输出的 token 数控制成本。工具调用准确率记录模型是否正确识别了需要调用工具的意图。工具执行成功率记录工具函数本身是否执行成功。8. 常见问题与排查方法问题现象可能原因排查方式解决方案模型不调用工具总是直接回答1. 工具描述 (description) 不清晰。2. 模型能力不支持工具调用。3. 提示词历史干扰。1. 检查工具description是否明确描述了调用时机。2. 确认模型是否支持tools参数。3. 开启 API 调用的日志查看原始响应。1. 重写description包含典型用户问法。2. 更换为明确支持 Function Calling 的模型。3. 在新会话中测试。模型调用了错误的工具工具功能描述有重叠或歧义。分析用户 query 和模型选择的工具。细化每个工具的description明确其职责边界。使用enum限制参数。工具调用参数解析错误1. 模型生成的参数 JSON 格式错误。2. 参数类型不匹配。打印tool_call.function.arguments原始字符串。1. 在代码中添加更健壮的 JSON 解析和错误处理。2. 在 Schema 中明确参数类型和格式。工具执行失败1. 工具函数内部错误。2. 依赖的外部服务不可用。3. 参数值超出范围。查看工具函数返回的错误信息。检查网络和外部 API 状态。1. 在工具函数内添加详细的异常捕获和日志。2. 实现重试和降级机制。3. 在 Schema 和函数入口都进行参数校验。API 返回InvalidRequestError(如unknown argument ‘tools’)使用的模型或 API 端点不支持工具调用。查阅对应模型平台的官方文档确认其是否支持tools或functions参数。更换模型或使用平台特定的工具调用方式。对话历史混乱工具调用逻辑出错没有正确处理role为tool的消息格式。检查conversation_history的结构确保tool角色的消息包含tool_call_id。严格按照 OpenAI 的消息格式规范构建历史。参考本文agent_core.py中的实现。9. 最佳实践与使用建议从简单工具开始先实现 1-2 个功能明确、逻辑简单的工具如时间、计算器验证整个流程跑通。编写高质量的description这是模型理解工具的关键。用英文或中文清晰描述“在什么情况下调用这个工具”、“这个工具是做什么的”。可以包含例子。实施严格的参数校验永远不要信任模型直接传来的参数。在工具函数内部对参数的类型、范围、格式进行二次校验。工具结果格式化工具函数返回给模型的结果应该是清晰、简洁的文本或结构化数据。模型需要读懂这个结果来组织最终回答。处理多轮工具调用复杂任务可能需要模型连续调用多个工具。确保你的主循环能够处理message.tool_calls包含多个工具的情况并妥善管理对话历史。为工具添加监控和日志记录每一次工具调用的详细信息用户输入、工具名、参数、结果、耗时这对于调试和优化至关重要。设计用户反馈机制对于重要或不可逆的操作如发送邮件即使在自动化流程中也应考虑加入用户确认步骤或者设计审核机制。版本化管理工具 Schema当工具更新时如新增参数、修改功能考虑对 Schema 进行版本管理避免影响已上线的客户端。10. 总结与下一步通过本文的实践你已经掌握了为 AI 助手赋予“动手”能力的核心方法使用 JSON Schema 定义工具并搭建一个循环框架来处理模型的工具调用请求。这套模式是构建实用 AI Agent 的基石。最值得尝试的下一步连接真实服务将示例中的模拟天气查询替换为调用真实的天气 API如和风天气。尝试连接数据库、发送邮件、调用企业内部系统。探索复杂 Agent 框架本文实现了一个基础的 Agent 循环。对于更复杂的场景如规划、记忆、多 Agent 协作可以探索成熟的框架如 LangChain、LlamaIndex、AutoGen 等。它们提供了更高级的抽象和工具集成。优化提示词与工具设计思考如何设计工具和提示词能让模型更好地进行任务分解和规划。例如创建一个“规划器”工具让模型先输出步骤再逐步执行。本地模型集成尝试使用支持工具调用的本地大模型如通过 Ollama 部署的特定版本构建完全离线的 AI 助手。最容易踩的坑工具描述模糊导致模型无法正确选择工具。历史消息管理混乱忘记添加role: “tool”的消息导致模型丢失上下文。安全漏洞如计算器工具中直接使用eval()生产环境必须替换为安全的表达式解析库。工具调用将大语言模型从“世界知识”的复读机变成了可以操作数字世界、获取实时信息、执行具体任务的智能体。从定义一个清晰的 JSON Schema 开始你的 AI 项目将获得质的飞跃。建议收藏本文在开发下一个智能功能时随时回来查阅这套标准流程。