在AI应用开发中,大模型本身就像一个知识渊博但“手无寸铁”的思考者。它能理解你的问题,也能给出精彩的推理,但当任务涉及查询实时天气、执行计算、操作数据库或调用外部API时,它就无能为力了。这正是“工具调用”技术要解决的核心痛点——让大模型学会“使用工具”,从而将强大的认知能力与丰富的现实世界操作能力结合起来,构建出真正智能的AI智能体。
本文将深入探讨大模型工具调用的完整技术栈。无论你是希望为聊天机器人添加查天气、订机票功能的开发者,还是正在构建复杂自动化工作流的技术负责人,亦或是对AI安全(AI红队)中工具滥用风险感兴趣的研究者,本文都将为你提供从核心概念、主流框架实现到安全实践的全套指南。我们将从零开始,手把手实现一个能调用外部工具的AI助手,并深入分析其背后的机制、潜在风险及防护策略。
1. 工具调用的核心概念与价值
在深入代码之前,我们必须厘清几个关键概念,理解为什么工具调用是构建实用AI应用的关键一跃。
1.1 什么是工具调用?
工具调用,在技术语境下常被称为Function Calling或Tool Calling,是指大语言模型根据用户请求,识别出需要调用某个外部函数或工具来完成任务的意图,并以结构化格式(通常是JSON)输出调用该工具所需的参数。随后,应用程序解析这个输出,实际执行对应的函数,并将执行结果返回给大模型,由大模型整合信息后生成最终回复给用户。
一个简单的类比:想象大模型是一个经验丰富的指挥官(大脑),它知道要完成“轰炸目标”这个任务,需要调用“空军”(工具)。指挥官不会自己去开飞机,而是下达一份包含坐标、弹药类型等详细参数的指令(结构化调用)。地勤人员(应用程序)接收指令,指挥真正的飞机(执行函数)完成任务,并将战果报告(执行结果)反馈给指挥官,由指挥官向总部(用户)汇报最终情况。
1.2 为什么需要工具调用?——突破大模型的固有局限
- 突破知识时效性:大模型的训练数据有截止日期,无法获知实时信息(如股票价格、新闻、天气)。
- 弥补计算与逻辑能力:大模型不擅长精确计算(如
(3.14 * 15.2^2) / 2)、逻辑推理(如复杂数据库查询)或执行确定性算法。 - 连接外部系统与服务:大模型无法直接操作数据库、发送邮件、调用企业内部的CRM/ERP系统API。
- 降低幻觉与错误:对于需要精确数据的任务(如查询账户余额),让大模型“编造”不如让它调用一个返回真实数据的工具更可靠。
1.3 核心参与角色与工作流程
一次完整的工具调用涉及三个核心角色:
- 大模型:理解意图,规划步骤,生成工具调用请求。
- 应用程序:提供工具定义,解析模型请求,安全执行工具,管理上下文。
- 工具:执行具体操作的函数、API或服务。
其标准工作流程如下图所示(以OpenAI格式为例):
用户: “旧金山现在的天气怎么样?” ↓ 应用程序将[用户消息 + 工具定义列表]发送给大模型。 ↓ 大模型返回结构化响应: { “role”: “assistant”, “content”: null, “tool_calls”: [{ “id”: “call_123”, “type”: “function”, “function”: { “name”: “get_current_weather”, “arguments”: “{ \”location\”: \”San Francisco\”, \”unit\”: \”celsius\” }” } }] } ↓ 应用程序解析`tool_calls`,执行本地函数 `get_current_weather(“San Francisco”, “celsius”)`, 获得结果 `{“temperature”: 22, “condition”: “Sunny”}`。 ↓ 应用程序将[工具执行结果]作为新消息附加到对话历史,再次发送给大模型。 ↓ 大模型整合信息,生成最终回复: “旧金山现在天气晴朗,气温22摄氏度。” ↓ 应用程序将最终回复返回给用户。2. 环境准备与主流框架选择
在开始实战前,我们需要搭建开发环境。本文将以Python生态为主,因为其拥有最丰富的大模型工具调用库和社区支持。
2.1 基础环境配置
- 操作系统:Windows 10/11, macOS 或 Linux (Ubuntu 20.04+)。本文示例在Linux/macOS的终端环境下演示。
- Python版本:推荐使用Python 3.10或3.11。避免使用Python 3.12等较新版本,可能遇到某些库的兼容性问题。
- 包管理工具:使用
pip或更推荐的uv、poetry。
首先,创建一个干净的虚拟环境并安装核心库:
# 创建项目目录并进入 mkdir ai-tool-calling-demo && cd ai-tool-calling-demo # 创建虚拟环境 (以venv为例) python -m venv venv # 激活虚拟环境 # Linux/macOS: source venv/bin/activate # Windows: # venv\Scripts\activate # 升级pip pip install --upgrade pip2.2 选择你的“武器库”:主流框架对比
实现工具调用有多种路径,选择取决于你的需求:
- 原生API(如OpenAI):最直接,功能强大,但需付费且依赖网络。
- 库:
openai - 优点:官方支持,功能最全,性能稳定。
- 缺点:产生API费用,数据出境需合规考量。
- 库:
- 本地大模型+适配框架:数据私密,可控性强,适合企业内部部署。
- 库:
ollama+langchain/litellm - 优点:数据完全本地,无网络延迟,成本固定(硬件)。
- 缺点:模型能力可能弱于顶级闭源模型,需要一定的运维知识。
- 库:
- 高级抽象框架(如LangChain):提供一站式解决方案,简化开发,但抽象度高。
- 库:
langchain,langchain-community - 优点:封装复杂流程,支持多种模型和工具,生态丰富。
- 缺点:学习曲线较陡,有时为了灵活性需要深入底层。
- 库:
本文策略:为了透彻理解原理,我们将先从OpenAI原生API开始,实现最基础的工具调用。然后,再使用LangChain框架重构,展示其如何提升开发效率。最后,会简要介绍如何接入本地Ollama模型。
安装核心依赖:
# 安装OpenAI Python SDK和LangChain pip install openai langchain langchain-openai langchain-community # 安装requests用于示例中的工具函数 pip install requests2.3 获取API密钥(如使用OpenAI)
如果你选择使用OpenAI的模型,需要准备API密钥。
- 访问 OpenAI平台 。
- 登录后,点击右上角个人头像,选择 “View API keys”。
- 点击 “Create new secret key” 创建一个新的密钥,并妥善保存。
安全提示:切勿将API密钥直接硬编码在代码中或提交到版本控制系统(如Git)。应使用环境变量管理。
# 在终端中设置环境变量 (临时) export OPENAI_API_KEY='你的-api-key-here' # Windows (PowerShell): $env:OPENAI_API_KEY='你的-api-key-here'3. 核心原理与API拆解:OpenAI Function Calling
OpenAI的Chat Completions API在2023年6月左右引入了function calling功能(后续更新中与tools调用合并),这是工具调用普及的关键推动力。我们来详细拆解其核心组件。
3.1 工具定义:如何告诉模型“你有什么工具”
你必须以JSON Schema格式清晰地定义每个工具(函数)。模型依靠这个定义来理解何时以及如何调用它。
一个完整的工具定义包含:
type: 固定为"function"。function: 一个对象,包含:name: 函数名,是模型在输出中引用的标识符。description:至关重要!用自然语言描述这个函数的作用。模型主要靠这个描述来判断是否需要调用它。parameters: 遵循JSON Schema格式,定义函数需要的参数,包括类型、描述、是否必需等。
# 这是一个工具定义的Python字典示例 weather_tool = { “type”: “function”, “function”: { “name”: “get_current_weather”, “description”: “获取指定城市的当前天气情况”, # 清晰的描述是关键! “parameters”: { “type”: “object”, “properties”: { “location”: { “type”: “string”, “description”: “城市名称,例如:北京, San Francisco”, }, “unit”: { “type”: “string”, “enum”: [“celsius”, “fahrenheit”], “description”: “温度单位”, } }, “required”: [“location”], # 指定必需参数 }, }, }3.2 模型响应:模型如何表达“我要用这个工具”
当模型决定调用工具时,它不会在常规的content字段中输出文本,而是会在tool_calls字段中返回一个或多个结构化的调用请求。
关键响应字段解析:
role:“assistant”content: 通常为null,因为回复内容由工具调用结果决定。tool_calls: 一个列表,包含每个调用请求。id: 本次调用的唯一ID,用于后续将执行结果关联回此次调用。type:“function”function: 包含name(函数名) 和arguments(参数字符串,是合法的JSON)。
3.3 对话历史管理:多轮交互的关键
工具调用通常是多轮对话的一部分。应用程序需要维护一个“消息列表”,其中不仅包含用户和助理的对话,还要包含“工具”角色的消息。
消息类型:
{“role”: “user”, “content”: “用户输入”}{“role”: “assistant”, “content”: null, “tool_calls”: […]}(模型请求调用工具){“role”: “tool”, “content”: “工具执行结果JSON字符串”, “tool_call_id”: “对应调用的ID”}(应用程序返回结果){“role”: “assistant”, “content”: “基于工具结果的最终回答”}
应用程序的责任是维护这个列表,并在每次API调用时将其作为messages参数传入。
4. 完整实战案例:构建一个多功能AI助手
现在,我们将综合以上知识,构建一个能处理天气查询、计算和百科搜索的AI助手。
4.1 项目结构设计
ai-tool-calling-demo/ ├── main_openai.py # 使用OpenAI原生API的实现 ├── main_langchain.py # 使用LangChain框架的实现 ├── tools.py # 所有工具函数的定义 ├── requirements.txt # 项目依赖 └── .env # 环境变量文件(需自行创建,不要提交)4.2 定义工具函数 (tools.py)
首先,我们实现三个具体的工具函数。这些函数就是模型将要调用的“手”和“脚”。
# tools.py import json import math import requests from datetime import datetime def get_current_weather(location: str, unit: str = “celsius”) -> str: “”” 模拟获取天气的函数。 在实际应用中,这里应该调用如OpenWeatherMap、和风天气等第三方API。 “”” # 模拟API返回数据 weather_data = { “location”: location, “temperature”: 22 if unit == “celsius” else 72, “unit”: unit, “condition”: “Sunny”, “humidity”: 65, “wind_speed”: 15, “feels_like”: 24 if unit == “celsius” else 75, “observation_time”: datetime.now().strftime(“%Y-%m-%d %H:%M:%S”) } print(f”[工具调用] 执行 get_current_weather, 参数: location={location}, unit={unit}”) return json.dumps(weather_data, ensure_ascii=False) def calculator(expression: str) -> str: “”” 计算数学表达式。 警告:在生产环境中直接使用eval是极度危险的,容易导致代码注入。 此处仅用于演示,实际应使用安全的表达式解析库(如`asteval`)。 “”” print(f”[工具调用] 执行 calculator, 参数: expression={expression}”) try: # 安全限制:移除危险的内置函数和属性访问 allowed_names = {‘__builtins__’: None} result = eval(expression, {“__builtins__”: None}, {“math”: math}) return json.dumps({“result”: result, “expression”: expression}) except Exception as e: return json.dumps({“error”: str(e), “expression”: expression}) def search_wikipedia(query: str, sentences: int = 3) -> str: “”” 使用Wikipedia API进行搜索。 这是一个真实的工具调用示例。 “”” print(f”[工具调用] 执行 search_wikipedia, 参数: query={query}, sentences={sentences}”) url = “https://en.wikipedia.org/w/api.php” params = { “action”: “query”, “format”: “json”, “list”: “search”, “srsearch”: query, “utf8”: 1, “srlimit”: 3 } try: response = requests.get(url, params=params, timeout=10) data = response.json() search_results = data.get(“query”, {}).get(“search”, []) if not search_results: return json.dumps({“results”: [], “message”: “No results found.”}) # 获取第一个结果的摘要 page_id = search_results[0][“pageid”] params_detail = { “action”: “query”, “format”: “json”, “pageids”: page_id, “prop”: “extracts”, “exintro”: True, “explaintext”: True, “exsentences”: sentences } response_detail = requests.get(url, params=params_detail, timeout=10) data_detail = response_detail.json() pages = data_detail.get(“query”, {}).get(“pages”, {}) extract = pages.get(str(page_id), {}).get(“extract”, “No extract available.”) return json.dumps({ “query”: query, “title”: search_results[0][“title”], “summary”: extract.strip() }, ensure_ascii=False) except requests.exceptions.RequestException as e: return json.dumps({“error”: f“Network error: {e}”})4.3 使用OpenAI原生API实现 (main_openai.py)
这是最基础、最透明的实现方式,帮助你理解底层机制。
# main_openai.py import os import json from openai import OpenAI from tools import get_current_weather, calculator, search_wikipedia from dotenv import load_dotenv # 加载环境变量,从.env文件读取OPENAI_API_KEY load_dotenv() # 初始化OpenAI客户端 client = OpenAI(api_key=os.getenv(“OPENAI_API_KEY”)) # 1. 定义工具列表 (对应tools.py中的函数) tools = [ { “type”: “function”, “function”: { “name”: “get_current_weather”, “description”: “获取指定城市的当前天气信息”, “parameters”: { “type”: “object”, “properties”: { “location”: { “type”: “string”, “description”: “城市或地区名称,例如:北京, 纽约”, }, “unit”: { “type”: “string”, “enum”: [“celsius”, “fahrenheit”], “description”: “温度单位,默认为摄氏度(celsius)”, } }, “required”: [“location”], }, }, }, { “type”: “function”, “function”: { “name”: “calculator”, “description”: “计算一个数学表达式的结果,支持加减乘除、乘方和常见数学函数。表达式需为字符串格式。”, “parameters”: { “type”: “object”, “properties”: { “expression”: { “type”: “string”, “description”: “数学表达式,例如:’3 + 5 * 2′, ‘math.sqrt(16)’”, } }, “required”: [“expression”], }, }, }, { “type”: “function”, “function”: { “name”: “search_wikipedia”, “description”: “搜索维基百科并返回相关条目的摘要”, “parameters”: { “type”: “object”, “properties”: { “query”: { “type”: “string”, “description”: “搜索关键词”, }, “sentences”: { “type”: “number”, “description”: “返回摘要的句子数量,默认为3”, “default”: 3 } }, “required”: [“query”], }, }, } ] # 工具名称到实际函数的映射 available_functions = { “get_current_weather”: get_current_weather, “calculator”: calculator, “search_wikipedia”: search_wikipedia, } def run_conversation(user_input: str, model=“gpt-3.5-turbo”, max_turns=5): “”” 运行一个支持工具调用的对话。 “”” messages = [{“role”: “user”, “content”: user_input}] # 初始化对话历史 turn_count = 0 while turn_count < max_turns: turn_count += 1 print(f“\n[对话轮次 {turn_count}] 发送给模型的消息:”) # print(json.dumps(messages, indent=2, ensure_ascii=False)) # 调试用 # 2. 调用Chat Completions API,传入消息和工具定义 response = client.chat.completions.create( model=model, messages=messages, tools=tools, tool_choice=“auto”, # 让模型自行决定是否调用工具 ) response_message = response.choices[0].message # print(f“模型原始响应: {response_message}”) # 调试用 # 3. 检查模型是否要求调用工具 tool_calls = response_message.tool_calls if tool_calls: # 4. 模型要求调用工具 print(f“模型请求调用 {len(tool_calls)} 个工具。”) # 将模型的响应(包含tool_calls)添加到对话历史 messages.append(response_message) # 5. 遍历并执行每个被请求的工具 for tool_call in tool_calls: function_name = tool_call.function.name function_to_call = available_functions.get(function_name) if not function_to_call: # 如果请求的工具不存在,返回错误信息 tool_response = json.dumps({“error”: f“Function {function_name} not found”}) else: # 解析工具参数 function_args = json.loads(tool_call.function.arguments) # 执行工具函数 function_response = function_to_call(**function_args) tool_response = function_response # 6. 将工具执行结果作为新消息添加到对话历史 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “content”: tool_response, }) # 循环继续,将包含工具结果的新历史再次发送给模型 else: # 7. 模型没有调用工具,直接生成最终回复 print(“模型生成最终回复。”) final_response = response_message.content messages.append({“role”: “assistant”, “content”: final_response}) return final_response return “对话轮次过多,已终止。” if __name__ == “__main__”: # 测试不同的用户查询 test_queries = [ “北京和上海的天气怎么样?用摄氏度告诉我。”, “计算一下圆周率乘以10的平方,再加上15。”, “搜索一下人工智能的发展历史。”, “先查一下伦敦的天气(用华氏度),然后告诉我爱因斯坦的主要贡献是什么。” # 多工具调用测试 ] for query in test_queries: print(f“\n{‘=’*50}”) print(f“用户查询: {query}”) print(f“{‘-‘*50}”) result = run_conversation(query) print(f“\n助手回复: {result}”) print(f“{‘=’*50}\n”)运行与观察:
- 在项目根目录创建
.env文件,写入OPENAI_API_KEY=sk-你的密钥。 - 在终端执行
python main_openai.py。 - 观察控制台输出,你会清晰地看到“对话轮次”、“工具调用”和模型最终回复的完整流程。对于多工具请求(如最后一个测试查询),模型会规划顺序,依次调用。
4.4 使用LangChain框架重构 (main_langchain.py)
LangChain将上述繁琐的对话历史管理、工具执行循环封装成了更简洁的接口。我们使用最新的LangChain版本(>=0.1.0)的LCEL(LangChain Expression Language)语法。
# main_langchain.py import os from langchain_openai import ChatOpenAI from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.tools import Tool from tools import get_current_weather, calculator, search_wikipedia from dotenv import load_dotenv load_dotenv() # 1. 将Python函数包装成LangChain Tool对象 tools = [ Tool( name=“Weather”, func=get_current_weather, description=“获取指定城市的当前天气信息。输入应包含’location’(城市名)和可选的’unit’(’celsius’或’fahrenheit’)。”, ), Tool( name=“Calculator”, func=calculator, description=“计算一个数学表达式。输入应为包含表达式的字符串,例如 ‘3 + 5’ 或 ‘math.sqrt(9)’。”, ), Tool( name=“Wikipedia”, func=search_wikipedia, description=“搜索维基百科。输入应包含’query’(搜索词)和可选的’sentences’(摘要句子数)。”, ), ] # 2. 初始化大模型 llm = ChatOpenAI(model=“gpt-3.5-turbo”, temperature=0, api_key=os.getenv(“OPENAI_API_KEY”)) # 3. 构建提示词模板 prompt = ChatPromptTemplate.from_messages([ (“system”, “你是一个乐于助人的助手,可以调用工具来回答问题。请根据用户问题,决定是否需要以及调用哪个工具。如果不需要工具,请直接回答。”), MessagesPlaceholder(variable_name=“chat_history”), # 预留位置存放历史消息 (“user”, “{input}”), MessagesPlaceholder(variable_name=“agent_scratchpad”), # 预留位置存放Agent的思考过程(工具调用和结果) ]) # 4. 创建Agent agent = create_tool_calling_agent(llm, tools, prompt) # 5. 创建Agent执行器 agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True) # 6. 运行对话 if __name__ == “__main__”: from langchain_core.messages import HumanMessage, AIMessage chat_history = [] # 用于存储多轮对话历史 def chat_with_agent(user_input: str): global chat_history print(f“\n用户: {user_input}”) # 调用执行器 result = agent_executor.invoke({“input”: user_input, “chat_history”: chat_history}) response = result[“output”] print(f“助手: {response}”) # 更新对话历史 (简化处理,实际生产环境需更精细管理) chat_history.append(HumanMessage(content=user_input)) chat_history.append(AIMessage(content=response)) # 防止历史过长,可在此处截断 if len(chat_history) > 10: chat_history = chat_history[-6:] # 保留最近3轮对话 return response # 测试 test_inputs = [ “今天杭州的天气如何?”, “123乘以456等于多少?”, “告诉我Python编程语言是谁发明的。”, ] for inp in test_inputs: chat_with_agent(inp)运行与对比: 执行python main_langchain.py。你会看到LangChain以更结构化的方式输出每一步的思考(verbose=True时),包括“Action”(决定调用哪个工具)、“Action Input”(参数)、“Observation”(工具结果)和“Final Answer”。代码量更少,且更容易扩展到更复杂的Agent工作流。
5. 进阶话题:安全、本地模型与工程化
掌握了基础用法后,我们需要关注更实际的问题。
5.1 AI红队视角:工具调用的安全风险与防护
赋予大模型调用工具的能力,也打开了新的攻击面。从“AI红队”(专注于AI系统安全的团队)角度看,主要风险包括:
工具滥用与越权:
- 风险:模型被诱导调用高权限或危险工具(如
delete_database,send_email)。 - 防护:
- 最小权限原则:只为模型提供完成当前任务所必需的工具。
- 工具沙箱化:在隔离环境(如Docker容器、无网络沙箱)中执行不可信的工具调用。
- 运行时鉴权:在执行工具前,增加一层基于用户身份、会话上下文的权限检查。
- 敏感操作确认:对于删除、发送、修改等操作,要求人工确认或二次验证。
- 风险:模型被诱导调用高权限或危险工具(如
提示注入与参数操纵:
- 风险:攻击者通过精心设计的用户输入,让模型将恶意参数传递给工具(如SQL注入、命令注入)。
- 防护:
- 输入净化与验证:在工具函数内部,对所有输入参数进行严格的类型检查、长度限制、内容过滤(如防止SQL特殊字符)。
- 使用参数化查询/安全API:对于数据库、系统命令调用,务必使用参数化查询(如SQL的
?占位符)或安全的子进程调用库(如subprocesswithshell=False)。 - 避免
eval:如我们示例中的calculator函数,生产环境必须替换为安全的表达式解析库。
信息泄露:
- 风险:工具返回的结果可能包含敏感信息(如数据库错误信息、内部文件路径),被模型泄露给用户。
- 防护:
- 结果过滤:在将工具结果返回给模型前,过滤掉错误详情、堆栈跟踪、内部标识等敏感信息。
- 统一错误处理:返回给模型的错误信息应为用户友好的通用提示。
资源耗尽与拒绝服务:
- 风险:模型被诱导反复调用计算密集型或网络IO密集型工具,耗尽系统资源。
- 防护:
- 调用频率限制:对每个用户/会话的工具调用次数、频率进行限制。
- 超时控制:为每个工具执行设置严格的超时时间。
- 预算控制:对涉及费用的工具(如发送短信、调用付费API)设置每日预算。
安全增强的Tool Wrapper示例:
# secure_tools.py import time from functools import wraps from typing import Callable, Any class ToolSecurityManager: def __init__(self): self.call_count = {} self.last_call_time = {} def rate_limit(self, tool_name: str, max_calls_per_minute: int = 10): “””装饰器:限制工具调用频率””” def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): current_time = time.time() key = f“{tool_name}_{kwargs.get(‘user_id’, ‘default’)}” # 初始化或清理过期记录 if key not in self.call_count: self.call_count[key] = [] # 移除一分钟前的记录 self.call_count[key] = [t for t in self.call_count[key] if current_time - t < 60] if len(self.call_count[key]) >= max_calls_per_minute: raise PermissionError(f“工具 {tool_name} 调用过于频繁,请稍后再试。”) # 记录本次调用 self.call_count[key].append(current_time) return func(*args, **kwargs) return wrapper return decorator def require_auth(self, required_role: str): “””装饰器:检查调用权限””” def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): user_role = kwargs.get(‘user_role’, ‘guest’) if user_role != required_role: raise PermissionError(f“需要 {required_role} 权限才能执行此操作。”) # 移除装饰器添加的参数,避免传递给原函数 kwargs.pop(‘user_role’, None) return func(*args, **kwargs) return wrapper return decorator security_mgr = ToolSecurityManager() @security_mgr.rate_limit(tool_name=“weather”, max_calls_per_minute=5) def secure_get_weather(location: str, unit: str = “celsius”, user_id: str = “unknown”, **kwargs) -> str: # 参数验证 if not location or len(location) > 100: raise ValueError(“地点名称无效或过长。”) if unit not in [“celsius”, “fahrenheit”]: raise ValueError(“温度单位必须是 ‘celsius’ 或 ‘fahrenheit’。”) # … 调用真实的天气API … return json.dumps({“temperature”: 22, “condition”: “Sunny”}) # 在定义给模型的工具时,使用安全版本,并通过描述告知模型需要的参数 secure_tools_for_model = [ { “type”: “function”, “function”: { “name”: “secure_get_weather”, “description”: “获取天气。需要参数: location(字符串), unit(可选, ‘celsius’或’fahrenheit’)。调用时需提供user_id。”, # … } } ]5.2 接入本地大模型(Ollama + LiteLLM)
对于数据敏感或希望控制成本的场景,可以使用本地部署的模型。Ollama是一个流行的本地大模型运行工具,而LiteLLM是一个统一的模型调用抽象层。
# 首先,安装Ollama并拉取一个模型,例如Llama 3.1 # 访问 https://ollama.com/ 下载安装 # 在终端运行: ollama pull llama3.1 # 安装litellm,它可以将Ollama的API模拟成OpenAI格式 pip install litellm# main_ollama.py import os from langchain_community.chat_models import ChatOllama from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain.tools import Tool from tools import calculator # 使用之前定义的工具 # 1. 使用LangChain的ChatOllama包装器 # 确保Ollama服务正在运行 (默认 http://localhost:11434) llm = ChatOllama( model=“llama3.1”, # 你拉取的模型名称 temperature=0, base_url=“http://localhost:11434”, # Ollama API地址 ) # 2. 定义工具(同上) tools = [ Tool( name=“Calculator”, func=calculator, description=“计算数学表达式。”, ), # 可以添加更多工具,但注意本地模型可能对复杂工具调用的支持不如GPT-4 ] # 3. 创建Agent和执行器(与OpenAI版本几乎相同) from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder prompt = ChatPromptTemplate.from_messages([ (“system”, “你是一个助手。请使用工具来帮助用户。如果不需要工具,请直接回答。”), MessagesPlaceholder(variable_name=“chat_history”), (“user”, “{input}”), MessagesPlaceholder(variable_name=“agent_scratchpad”), ]) agent = create_tool_calling_agent(llm, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True) # 4. 运行测试 result = agent_executor.invoke({“input”: “计算一下 (15 + 27) * 3 等于多少?”}) print(result[“output”])注意:本地模型的工具调用能力(特别是函数参数解析的准确性)因模型而异。Llama 3.1、Qwen等较新模型支持较好,但可能仍需更精确的提示词或微调。
5.3 工程化最佳实践
工具设计:
- 单一职责:每个工具只做一件事。
- 描述清晰:工具的描述是模型理解的唯一依据,务必准确、无歧义。
- 强类型参数:在JSON Schema中明确定义参数类型、枚举值和默认值。
- 健壮性:工具函数内部要有完善的错误处理和日志记录。
系统架构:
- 状态管理:对于多轮对话,妥善管理对话历史、用户会话和工具调用状态。考虑使用数据库或Redis。
- 异步处理:对于耗时的工具调用(如网络请求),使用异步框架(如
asyncio, FastAPI)避免阻塞。 - 可观测性:记录所有工具调用的输入、输出、耗时和错误,便于监控和调试。
提示工程:
- 系统提示词:在
system消息中明确告知模型可用的工具及其用途,并设定行为规范(如“未经确认不得执行删除操作”)。 - 少样本学习:在对话历史中提供几个正确使用工具的示例(Few-Shot),能显著提升模型调用工具的准确性。
- 系统提示词:在
6. 常见问题与排查思路
在开发过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 模型不调用工具,直接回答 | 1. 工具描述不清晰或与问题不匹配。 2. 模型能力不足(如使用 gpt-3.5-turbo处理复杂任务)。3. 系统提示词未引导模型使用工具。 | 1. 优化工具description,使其更贴近自然语言查询。2. 升级到更强的模型,如 gpt-4-turbo。3. 在 system提示中明确指令,如“请优先使用工具来获取准确信息”。 |
| 模型调用了错误的工具或参数 | 1. 工具间描述相似,导致混淆。 2. 参数 description不明确。3. 用户查询存在歧义。 | 1. 区分工具描述,突出其独特用途。 2. 在参数描述中提供示例值。 3. 考虑让模型与用户进行澄清性对话(多轮交互)。 |
| 解析工具参数时JSON解码错误 | 1. 模型生成的arguments字符串不是合法JSON。2. 字符串中包含未转义的特殊字符。 | 1. 使用json.loads()时增加strict=False参数或进行try-catch。2. 在调用模型时,可以尝试设置 response_format={ “type”: “json_object” }(部分模型支持)来约束输出格式。 |
| 工具执行超时或失败 | 1. 工具函数本身有bug或依赖服务不可用。 2. 网络问题。 3. 资源不足。 | 1. 为工具调用添加超时和重试机制。 2. 记录详细的错误日志,包括输入参数和异常堆栈。 3. 实现熔断器模式,避免连续失败拖垮系统。 |
| 对话历史过长导致API令牌超限或性能下降 | 1. 未对历史消息进行摘要或截断。 2. 工具调用轮次过多。 | 1. 实现历史消息的摘要功能:将过去的对话压缩成一段总结性文字。 2. 设置最大历史轮次,保留最近的N条消息。 3. 使用支持更长上下文的模型。 |
| 使用本地模型时工具调用格式错误 | 1. 本地模型未对齐OpenAI的tool_calls格式。2. 提示词未针对本地模型优化。 | 1. 使用litellm这类兼容层来标准化API格式。2. 查阅该本地模型关于工具调用的特定文档,调整提示词和输出解析逻辑。 |
7. 总结与展望
工具调用技术是大模型从“对话者”迈向“执行者”的核心桥梁。通过本文的梳理,你应该已经掌握了:
- 核心原理:理解了工具调用的基本流程——定义、识别、执行、整合。
- 动手能力:能够使用OpenAI原生API和LangChain框架,构建一个具备多工具调用能力的AI助手。
- 安全意识:从AI红队视角认识了工具调用可能带来的风险,并学习了基础的防护策略,如权限控制、输入验证和频率限制。
- 扩展思路:了解了如何接入本地大模型,以及工程化开发中需要考虑的状态管理、异步处理和可观测性。
下一步的学习方向:
- 深入Agent框架:探索更复杂的Agent架构,如ReAct、Plan-and-Execute、Multi-Agent系统。LangGraph是构建有状态、多分支工作流的强大工具。
- 工具学习:研究如何让大模型自动学习使用新工具的文档,甚至通过少量示例生成工具的描述和参数模式。
- 评估与测试:建立对工具调用系统的评估体系,包括工具选择准确率、参数填充正确率、任务完成度等。
- 与RAG结合:将工具调用与检索增强生成结合,让模型既能利用外部知识库,又能执行具体操作,构建更强大的应用。
工具调用正在快速演进,从简单的函数调用走向复杂的工作流编排。掌握这项技术,意味着你能让大模型真正融入现有的软件生态系统,自动化处理那些需要认知判断与实际行动相结合的任务。