你好,我是专注于AI应用开发的技术博主。在上一篇中,我们探讨了Agent和Tool Use的基本概念,让AI助手从“只会说”进化到“知道能做什么”。今天,我们将进入实战环节,手把手教你如何为模型装上“手”,让它真正具备调用外部工具的能力,完成从查询时间到复杂计算等一系列任务。无论你是想入门AI应用开发,还是希望为自己的项目增加智能体能力,这篇从环境搭建到代码实现的完整指南都将为你提供清晰的路径。
1. 核心概念回顾与本文目标
在深入代码之前,我们快速回顾并明确几个关键概念,这有助于理解后续的每一步操作。
Agent(智能体):一个能够感知环境、进行决策并执行动作以达成目标的系统。在我们的上下文中,通常指以大语言模型(LLM)为“大脑”,能够规划和使用工具的程序。
Tool Use / Function Calling(工具调用/函数调用):这是赋予LLM行动力的关键机制。它允许LLM根据用户请求,识别出需要调用哪个预定义的工具(函数),并生成符合要求的调用参数(通常为JSON格式),然后由系统执行该函数并将结果返回给LLM,最终由LLM整合信息回复用户。
JSON Schema:一种描述JSON数据结构的标准。在工具调用中,我们使用JSON Schema来严格定义每个工具所需的输入参数(名称、类型、描述、是否必填等)。LLM根据这个“说明书”来生成格式正确的调用参数。
本文目标:我们将构建一个简单的AI助手,它能够理解用户需求,并动态调用两个核心工具:get_current_time(获取当前时间)和calculator(执行数学计算)。通过这个案例,你将掌握工具调用的完整流程:定义工具、构建Agent逻辑、处理模型响应并执行工具。
2. 环境准备与项目初始化
我们将使用Python作为开发语言,并利用OpenAI的API(兼容其函数调用格式)以及LangChain框架来简化流程。LangChain提供了优秀的Agent和Tool抽象,让开发更高效。
2.1 基础环境要求
- 操作系统:Windows 10/11, macOS 或 Linux (Ubuntu 20.04+)
- Python版本:3.8 或更高版本 (推荐 3.9+)
- 包管理工具:pip
2.2 创建项目与安装依赖
首先,创建一个新的项目目录并初始化虚拟环境,这能有效隔离项目依赖。
# 创建项目目录并进入 mkdir ai_assistant_agent && cd ai_assistant_agent # 创建虚拟环境 (以venv为例) python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活虚拟环境后,命令行提示符前通常会显示(venv)。接下来,安装核心依赖库。
# 安装依赖 pip install openai langchain langchain-openai python-dotenvopenai/langchain-openai: OpenAI官方SDK及LangChain集成,用于调用GPT模型。langchain: 核心框架,提供Agent、Tool、Chain等高级抽象。python-dotenv: 用于从.env文件加载环境变量(如API密钥),避免硬编码。
2.3 配置API密钥
为了调用OpenAI的模型,你需要一个有效的API Key。请前往 OpenAI平台 注册并获取。
在项目根目录下创建一个名为.env的文件,用于安全存储密钥。
# .env 文件内容 OPENAI_API_KEY=你的实际API密钥重要安全提示:务必在.gitignore文件中添加.env,切勿将此文件提交到版本控制系统。
2.4 项目结构预览
完成后的简易项目结构如下:
ai_assistant_agent/ ├── .env # 环境变量文件(保密) ├── .gitignore # Git忽略文件 ├── requirements.txt # 依赖列表(可通过 `pip freeze > requirements.txt` 生成) ├── tools/ # 工具模块目录 │ ├── __init__.py │ └── custom_tools.py # 自定义工具实现 └── main.py # 主程序入口3. 核心工具(Tool)的定义与实现
工具的本质是一个Python函数,辅以清晰的元数据描述(名称、描述、参数schema),以便LLM理解和使用它。
3.1 创建工具模块
在项目根目录下创建tools文件夹和custom_tools.py文件。
# tools/custom_tools.py import json from datetime import datetime from typing import Type, Optional from pydantic import BaseModel, Field from langchain.tools import BaseTool # --- 工具1:获取当前时间 --- class GetCurrentTimeInput(BaseModel): """获取当前时间的输入参数。此工具无需输入,但为保持结构统一,我们定义一个空模型。""" # 这是一个无参数的工具,所以模型内部是空的。 pass class GetCurrentTimeTool(BaseTool): name: str = "get_current_time" description: str = "当用户询问当前时间、现在几点、今天日期时,使用此工具。" args_schema: Type[BaseModel] = GetCurrentTimeInput def _run(self) -> str: """执行获取当前时间的逻辑。""" now = datetime.now() # 格式化为易读的字符串 return now.strftime("%Y年%m月%d日 %H时%M分%S秒") async def _arun(self) -> str: """异步执行(本例中简单同步实现)。""" return self._run() # --- 工具2:计算器 --- class CalculatorInput(BaseModel): """计算器的输入参数模型。""" expression: str = Field( ..., description="一个合法的数学表达式,例如:'3 + 5 * (2 - 1)' 或 'sin(45) + log(100)'。支持加减乘除(+-*/)、括号和常见数学函数。", ) class CalculatorTool(BaseTool): name: str = "calculator" description: str = "用于执行数学计算。当用户提出涉及算术、数学表达式求解的问题时使用此工具。" args_schema: Type[BaseModel] = CalculatorInput def _run(self, expression: str) -> str: """执行计算。 警告:直接使用eval有安全风险,仅用于演示。 生产环境应使用更安全的表达式求值库(如 `asteval`)。 """ try: # 注意:eval 在生产环境中非常危险,可能执行任意代码。 # 此处仅为演示,实际项目务必替换! result = eval(expression, {"__builtins__": None}, {}) return f"表达式 `{expression}` 的计算结果是:{result}" except Exception as e: return f"计算表达式 `{expression}` 时出错:{e}" async def _arun(self, expression: str) -> str: return self._run(expression)代码解析与关键点:
- Pydantic模型 (
BaseModel):用于严格定义工具的输入参数。这直接对应了JSON Schema,确保了LLM生成的参数类型正确。 BaseTool类:来自LangChain,是构建工具的基类。我们需要定义name、description和args_schema,并实现_run方法。description的重要性:这是LLM决定是否调用该工具的主要依据。描述必须清晰、准确,涵盖工具的用途和典型使用场景。- 安全警告:
CalculatorTool中使用了eval,这在实际项目中是极其危险的,因为它会执行字符串中的任何Python代码。这里仅作演示,后文会讨论安全替代方案。
3.2 理解JSON Schema的生成
LangChain会在底层自动将args_schema(即Pydantic模型)转换为JSON Schema。例如,CalculatorInput会被转换为类似下面的结构,并发送给LLM:
{ "type": "object", "properties": { "expression": { "type": "string", "description": "一个合法的数学表达式,例如:'3 + 5 * (2 - 1)' 或 'sin(45) + log(100)'。支持加减乘除(+-*/)、括号和常见数学函数。" } }, "required": ["expression"] }LLM正是根据这个Schema来生成格式正确的{"expression": "3 + 5 * 2"}参数。
4. 构建智能体(Agent)与主流程
我们将使用LangChain的create_openai_tools_agent来构建一个Agent。这个Agent的核心是一个LLM和一系列Tools。
4.1 编写主程序
在项目根目录创建main.py。
# main.py import os from dotenv import load_dotenv from langchain import hub from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_openai import ChatOpenAI from tools.custom_tools import GetCurrentTimeTool, CalculatorTool # 1. 加载环境变量 load_dotenv() # 2. 初始化LLM (使用GPT-3.5-turbo,成本较低且支持工具调用) llm = ChatOpenAI( model="gpt-3.5-turbo-1106", # 或 "gpt-4-turbo-preview",确保模型支持工具调用 temperature=0, # 设置为0使输出更确定,适合工具调用场景 api_key=os.getenv("OPENAI_API_KEY") ) # 3. 实例化工具列表 tools = [GetCurrentTimeTool(), CalculatorTool()] # 4. 获取Agent的Prompt模板 # LangChain Hub上预置了优秀的Agent提示词模板,我们直接拉取。 prompt = hub.pull("hwchase17/openai-tools-agent") # 你可以打印prompt.template查看其内容,它指导LLM如何思考和使用工具。 # 5. 创建Agent agent = create_openai_tools_agent(llm, tools, prompt) # 6. 创建Agent执行器 agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, # 设置为True可以看到Agent的思考过程,调试非常有用! handle_parsing_errors=True, # 优雅处理解析错误 max_iterations=5, # 限制最大迭代次数,防止死循环 ) # 7. 交互循环 def main(): print("AI助手已启动!我可以帮你查询时间和进行数学计算。输入‘退出’或‘quit’结束对话。") while True: try: user_input = input("\n你: ") if user_input.lower() in ["退出", "quit", "exit"]: print("助手: 再见!") break if not user_input.strip(): continue # 调用Agent执行器 response = agent_executor.invoke({"input": user_input}) print(f"助手: {response['output']}") except KeyboardInterrupt: print("\n\n程序被中断。") break except Exception as e: print(f"发生错误: {e}") if __name__ == "__main__": main()4.2 关键组件解析
ChatOpenAI:这是LangChain对OpenAI聊天模型的封装。temperature=0使得模型输出更稳定,减少随机性,这对于需要精确触发工具调用的场景很重要。hub.pull(“hwchase17/openai-tools-agent”):从LangChain Hub拉取一个社区维护的、专门为OpenAI工具调用优化的提示词模板。这省去了我们自己编写复杂Prompt的麻烦。该Prompt会指示模型以特定的JSON格式(tool_calls)返回工具调用请求。create_openai_tools_agent:此函数将LLM、工具列表和Prompt组合起来,形成一个具备工具调用决策能力的Agent对象。AgentExecutor:这是驱动Agent运行的引擎。它负责:- 将用户输入和对话历史传递给Agent。
- 解析Agent的输出(可能是最终答案,也可能是工具调用请求)。
- 如果解析到工具调用,则执行对应的工具。
- 将工具执行结果作为新的上下文再次传递给Agent,进行下一轮思考。
- 循环此过程,直到Agent返回最终答案或达到最大迭代次数。
verbose=True会打印出详细的思考链(Chain of Thought),是调试和理解Agent行为的利器。
5. 运行与效果验证
现在,让我们启动助手并看看它的实际表现。
- 确保你在虚拟环境中,且
.env文件已正确配置API密钥。 - 在终端运行程序:
python main.py - 你将看到类似以下的交互过程(
verbose=True时的输出):
AI助手已启动!我可以帮你查询时间和进行数学计算。输入‘退出’或‘quit’结束对话。 你: 现在几点了? > 进入新的Agent执行链... 我可以用获取当前时间的工具来回答这个问题。 动作:get_current_time 动作输入:{} 观察:2024年5月20日 15时30分25秒 思考:我已经获得了当前时间,可以回答用户了。 最终答案:现在是2024年5月20日 15点30分25秒。 助手: 现在是2024年5月20日 15点30分25秒。 你: 请计算一下 (15 + 7) * 3 的值是多少? > 进入新的Agent执行链... 用户要求计算一个数学表达式。我应该使用计算器工具。 动作:calculator 动作输入:{"expression": "(15 + 7) * 3"} 观察:表达式 `(15 + 7) * 3` 的计算结果是:66 思考:我已经得到了计算结果,可以回复用户了。 最终答案:(15 + 7) * 3 的计算结果是 66。 助手: (15 + 7) * 3 的计算结果是 66。 你: 先告诉我时间,然后计算时间的小时数加上分钟数是多少。 > 进入新的Agent执行链... 用户有两个请求:先获取时间,然后进行一个计算。我需要按顺序使用工具。 动作:get_current_time 动作输入:{} 观察:2024年5月20日 15时31分10秒 思考:我得到了时间“2024年5月20日 15时31分10秒”。接下来需要计算小时数(15)加上分钟数(31)。 动作:calculator 动作输入:{"expression": "15 + 31"} 观察:表达式 `15 + 31` 的计算结果是:46 思考:我完成了两个步骤,现在可以给出最终答案。 最终答案:当前时间是15点31分10秒。小时数(15)加上分钟数(31)等于46。 助手: 当前时间是15点31分10秒。小时数(15)加上分钟数(31)等于46。效果分析:
- Agent成功理解了用户的自然语言指令。
- 它能正确选择工具(
get_current_time或calculator)。 - 它能从观察结果中提取信息,并用于后续的思考或计算(如第三个例子)。
verbose日志清晰展示了Agent的“思考-行动-观察”循环,这正是其智能的体现。
6. 进阶话题:安全、扩展与最佳实践
一个基础的Agent跑起来了,但要用于实际项目,我们还需要考虑更多。
6.1 计算器工具的安全强化
如前所述,eval是危险的。我们可以使用更安全的库,如asteval或numexpr。
# 安全计算器工具示例 (需安装 pip install asteval) from asteval import Interpreter class SafeCalculatorTool(BaseTool): name = “safe_calculator” description = “用于安全地执行数学计算。支持数字、基本运算符(+-*/)、括号和常见数学函数如sin, cos, sqrt等。” args_schema = CalculatorInput # 复用之前的输入模型 def _run(self, expression: str) -> str: try: aeval = Interpreter() result = aeval(expression) if aeval.error: return f“计算错误: {aeval.error}” return f“表达式 `{expression}` 的计算结果是:{result}” except Exception as e: return f“计算表达式 `{expression}` 时出错:{e}”asteval提供了一个受限的求值环境,大幅提升了安全性。
6.2 扩展更多工具
你可以遵循相同的模式添加任意工具,例如:
- 网络搜索工具:集成SerpAPI或DuckDuckGo搜索。
- 数据库查询工具:连接数据库执行SQL查询(需严格控制权限)。
- 文件操作工具:读写特定目录下的文件。
- API调用工具:调用天气预报、股票、翻译等第三方API。
关键原则:每个工具的描述(description)必须精准,输入模型(args_schema)必须严谨。
6.3 错误处理与Agent稳定性
handle_parsing_errors:当模型返回的JSON格式无法解析时,此参数允许执行器尝试修复或给出友好提示。max_iterations:必须设置。防止Agent陷入“工具调用-观察-再调用”的死循环。max_execution_time:可以设置最大执行时间,避免长时间挂起。- 结构化输出:对于复杂任务,可以要求模型以更结构化的格式(如JSON)输出最终答案,便于后续程序处理。
6.4 提示词工程优化
虽然我们使用了Hub上的模板,但在复杂场景下,你可能需要自定义Prompt。
- 系统消息:在Prompt中明确Agent的角色、能力和限制。
- 少样本示例:在Prompt中提供几个“用户提问-Agent思考并调用工具”的示例,可以显著提升模型使用工具的准确性。
- 格式强调:反复强调输出格式必须是特定的JSON。
7. 常见问题与排查思路
在开发过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
错误:OpenAI API key not provided | 1..env文件未创建或路径不对。2. 环境变量未正确加载。 3. 虚拟环境未激活。 | 1. 检查.env文件是否在项目根目录,且名称正确。2. 在 main.py开头打印os.getenv(“OPENAI_API_KEY”)前几位,确认是否加载成功。3. 确认终端提示符前有 (venv)。 |
| Agent不调用工具,直接回答问题 | 1. 工具description描述不清,模型无法匹配。2. Prompt模板不适合。 3. 模型温度( temperature)过高,导致输出随机。 | 1. 优化工具描述,确保涵盖用户可能的所有问法。 2. 尝试不同的Prompt模板或自定义Prompt。 3. 将 temperature设为0。启用verbose=True查看模型原始思考。 |
错误:...is not valid JSON | 1. 模型生成的工具调用参数格式错误。 2. Pydantic模型定义与Schema不匹配。 | 1. 检查verbose输出,看模型生成的“动作输入”是否合规。2. 确保 args_schema中的字段类型(如str,int)定义正确。简化复杂的嵌套模型。 |
| 工具被错误调用或多次调用 | 1. Agent对任务理解有偏差。 2. 工具描述有歧义或重叠。 | 1. 在Prompt中提供更明确的任务指令和示例。 2. 仔细区分不同工具的 description,避免功能重叠。设置max_iterations防止循环。 |
计算器执行eval报安全错误 | 表达式包含非法字符或函数。 | 立即停止使用eval,按照6.1节替换为asteval等安全库。 |
8. 总结与展望
至此,我们已经完成了一个具备工具调用能力的AI助手从零到一的构建。回顾整个流程:
- 定义工具:使用Pydantic模型和
BaseTool类,清晰定义功能、描述和输入规范。 - 构建Agent:利用LangChain框架,将LLM、工具集和Prompt模板组合成智能体。
- 执行与交互:通过
AgentExecutor驱动思考-行动循环,并处理用户交互。 - 安全与优化:替换危险函数、增加错误处理、优化提示词,使系统更健壮。
这个简单的“查时间+算算术”助手,已经展示了AI Agent的核心范式。你可以以此为基石,接入更强大的工具(如搜索引擎、知识库、业务系统API),构建出能真正处理复杂任务的智能助理。
未来的学习方向可以聚焦于:
- 记忆:为Agent添加对话历史记忆,使其能进行多轮复杂对话。
- 规划:让Agent能够分解复杂任务,制定多步执行计划(Plan-and-Execute)。
- 多智能体协作:创建多个各司其职的Agent,让它们通过协作解决更宏大的问题。
- 与RAG结合:让Agent在调用工具的同时,也能从你提供的专属文档库中检索信息,实现知识增强。
动手尝试修改代码,添加一个新工具(比如一个查询城市天气的模拟工具),是巩固学习的最佳方式。开发过程中,多利用verbose=True模式观察Agent的思考链,这能帮你精准定位问题并理解其工作原理。