自定义工具实战:让AI智能体真正“动起来”的完整指南
2026/9/7 9:41:21 网站建设 项目流程

很多人在学 AI 编程与智能体开发时会遇到一个很典型的问题:模型对话很流畅,一到“让智能体真正做点事”就卡住了。比如让它查一下当前系统时间、算一笔账、读取一个本地文件,它只会给你一段文字,不会真的去执行。原因是很多初学者还没有意识到——大模型只是一个非常强的“文本推理大脑”,它不会主动调用外部函数,也不会访问你的本地环境。智能体之所以能“动起来”,关键在工具(Tool)这一层。

在厦门大学林子雨老师的《AI编程与智能体开发》课程中,8.8.3 小节单独安排了一次“自定义工具实操”。这个章节放在整个课程的实操阶段,目的很明确:帮助学习者跨过智能体开发中最核心的一道坎,把自己写好的普通函数,变成模型可以理解并调用的工具,再把它挂载到智能体里完成真实任务。

本篇文章围绕这个课程节点展开,内容分为三块:第一,讲清楚自定义工具的原理与设计方式;第二,给出从零到一的最小可运行示例;第三,展示接入真实大模型和主流框架时的实操代码、验证方法、常见问题与工程建议。读完这篇文章,你会理解“会写函数”和“会写工具”之间的区别,也能亲自动手扩展一个能查时间、能计算、能处理简单任务的智能体。

1. 自定义工具解决的是智能体的“行为能力”问题

先看一个最常见的场景。你在 Agent 开发平台里创建了一个智能体,然后在对话框里输入:“帮我计算一下 3.5 乘以 7.2 再除以 2 的结果。”

模型很聪明,它可能会直接给出计算过程,甚至算出结果。但这里有一个关键问题:这个结果是模型“猜”出来的,还是真正经过计算程序算出来的?如果是简单乘法,模型大概率算得对,但如果是复杂到几十个变量的公式、需要读取实时数据、需要调用内部接口的任务,模型就无能为力了。

这就是智能体开发中经常说的“模型会想,但不会做”。它擅长的是语言理解与文本生成,不擅长执行真实动作。而“自定义工具”要解决的,恰恰是这个问题。

引入工具之后,整个流程会发生变化:

  • 用户提出需求;
  • 模型判断这个需求需要调用哪个工具;
  • 模型按工具的规则生成参数;
  • 程序真正执行工具函数;
  • 执行结果返回给模型;
  • 模型根据结果组织最终回答。

在这个流程里,模型扮演的是“决策者”的角色,工具函数扮演的是“执行者”。前面负责理解意图、拆解任务、决定调用什么;后面负责把结果真实计算出来。

这就是为什么在 AI 编程与智能体开发课程中,“自定义工具实操”会被单独拿出来讲。它不是一个可选项,而是从“会对话的机器人”走向“能办事的智能体”的关键一步。

这里也顺便纠正一个常见误区:很多人以为工具就是把函数写出来然后叫它“工具”而已。实际上,自定义工具必须包含两层结构:一层是给模型看的“说明书”,告诉模型这个工具是干什么的、有哪些参数;另一层是给程序用的“执行逻辑”,也就是真正运行的函数本体。缺了第一层,模型不知道怎么调用;缺了第二层,调用了也无法真正执行。

2. 工具与函数调用:核心概念与工作机制

要掌握自定义工具,先要理解两个词:Tool(工具)和 Function Calling(函数调用)。在大多数智能体开发框架和模型接口中,这两者经常同时出现。

Tool 是工具在模型层面的表达形式。它本质上是一个 JSON 结构,里面包含工具名称、工具说明、参数定义等信息。模型拿到这个结构之后,就会知道“我这里有哪些工具可以用,每个工具该怎么传参数”。

Function Calling 是模型的一种能力。启用函数调用之后,模型在回答用户问题时,不再只能输出纯文本,而是可以输出一个“我想调用某个工具”的结构化结果。这个结果包含两条关键信息:调用的工具名,以及传给工具的参数。

两者结合起来,就构成了自定义工具的基本工作机制。

我们看一个典型的工具定义。以 JSON Schema 形式描述一个查询天气的工具:

{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:厦门、北京" } }, "required": ["city"] } } }

这个 JSON 结构就是给模型看的“说明书”。它的含义是:系统里有一个工具叫get_weather,它可以查询天气,查询时你需要传入一个参数city,参数类型是字符串。

当用户问“厦门今天天气怎么样”时,模型看到这个工具定义,会返回一条类似下面的调用意图:

{ "name": "get_weather", "arguments": "{\"city\": \"厦门\"}" }

注意,模型到这里并没有真正查询天气。它只是告诉程序:我决定使用get_weather这个工具,传给它的参数是“厦门”。真正调用天气接口、获取结果、返回数据的工作,仍然要由程序代码完成。

为了帮助大家记忆,可以把一个自定义工具拆成五个要素:

要素作用示例
工具名称模型和程序共同识别的唯一标识get_weather
工具描述告诉模型什么时候该用这个工具“查询指定城市的当前天气”
参数定义指定模型需要填哪些参数、参数类型是什么city,字符串,必填
执行函数真正运行的程序逻辑调用天气服务接口并返回结果
返回结果回传给模型的文本或结构化数据

还有一个容易混淆的概念:工具、插件与 API 有什么区别?

简单理解,API 是外部系统提供的接口,工具是模型可以调用的函数封装,插件则通常是包含多个工具的完整功能包。一个工具内部可以调用多个 API,一个插件又可以把多个工具组合在一起。开发时不必纠结定义,只要抓住“工具是模型与程序之间的桥梁”这个本质即可。

3. 环境准备与开发框架选型

在进入代码之前,先准备好开发环境。下面推荐的组合可以覆盖课程实操和日常练习,版本信息以实际安装为准,本文不绑定过死的版本号,重点演示通用思路。

3.1 运行环境

本机建议安装 Python 3.9 或更高版本。在命令行执行python --version可以确认版本。为了方便管理依赖,建议为项目创建独立的虚拟环境:

mkdir custom-tool-lab cd custom-tool-lab python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate

3.2 选型建议

当前开发自定义工具主要有三条路线:

  • 直接使用大模型服务商的 SDK,以原生 Function Calling 方式开发。这种方式最透明,也最容易理解底层原理。
  • 使用 LangChain 等 Agent 开发框架,通过@tool装饰器快速定义工具。这种方式开发效率高,适合工程化项目。
  • 使用 Coze、Dify 等低代码平台,在界面中配置工具。这种方式门槛最低,适合原型验证,但与代码开发有点距离。

从课程实操角度看,建议先走第一条路线,搞懂原理后再用框架。本文也会依次演示。

3.3 API Key 管理

接入真实大模型时,通常需要设置 API Key。这里必须强调一个安全规范:不要把 API Key 硬编码到代码中,也不要提交到 Git 仓库。

推荐使用环境变量或.env文件。可以先安装python-dotenv来读取配置:

pip install python-dotenv openai

然后在项目根目录创建.env文件:

# 文件路径:.env # 请将下方的 YOUR_API_KEY 替换为你自己的密钥 # 请将下方的 YOUR_MODEL_NAME 替换为你实际使用的模型名称 OPENAI_API_KEY=YOUR_API_KEY OPENAI_MODEL_NAME=YOUR_MODEL_NAME

之后在 Python 代码中加载:

# 文件路径:config.py import os from dotenv import load_dotenv load_dotenv() API_KEY = os.getenv("OPENAI_API_KEY") MODEL_NAME = os.getenv("OPENAI_MODEL_NAME")

如果你使用的是国内大模型服务,接口通常也兼容 OpenAI 风格的调用方式,只需要在创建客户端时修改base_urlmodel即可。具体地址和模型名称以你实际使用的服务商文档为准。

4. 自定义工具实操:从本地可运行的最小示例开始

正式写代码之前,先做一个不依赖大模型 API 的最小示例。这个示例的目的不是替代真实模型,而是让你直观看到“模型决定调用工具 → 程序执行工具 → 结果返回”这个完整链路。

4.1 定义两个基础工具函数

我们定义两个非常简单的工具:一个获取当前本地时间,一个执行四则运算。演示时用普通 Python 函数即可。

# 文件路径:tools.py """ 工具函数实现部分。 真实项目中,这里的每个函数都可以替换为读取数据库、 调用外部 API、操作文件等真实业务逻辑。 """ from datetime import datetime def get_current_local_time() -> str: """返回当前本地时间,格式为 YYYY-MM-DD HH:MM:SS""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def calculate(expression: str) -> str: """ 计算简单的四则运算表达式。 注意:为了保证演示安全,这里只允许输入数字和 + - * / 符号。 生产环境中请使用专业计算库或自行实现解析器,不要直接 eval 任意字符串。 """ allowed_chars = set("0123456789+-*/(). ") if not set(expression).issubset(allowed_chars): return "非法表达式,只能包含数字、括号和四则运算符号" try: # 仅用于教学演示,真实项目应避免直接 eval 用户输入 result = eval(expression) return str(result) except Exception as e: return f"表达式计算失败: {e}"

这里有两个细节值得注意。第一,每个函数都写了清晰的文档字符串,这份注释在后面会变成模型看到的工具描述。第二,calculate对输入做了字符白名单校验,避免随意执行用户传入的代码。这个习惯在自定义工具开发中非常重要。

4.2 为模型准备工具定义

有了函数实现,还需要准备一份模型能读懂的“说明书”。

# 文件路径:tool_schema.py tools_schema = [ { "type": "function", "function": { "name": "get_current_local_time", "description": "获取当前本地时间,返回格式为 YYYY-MM-DD HH:MM:SS", "parameters": { "type": "object", "properties": {}, "required": [] } } }, { "type": "function", "function": { "name": "calculate", "description": "计算四则运算表达式,例如 3 + 5 * 2,结果返回数字字符串", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "需要计算的数学表达式" } }, "required": ["expression"] } } } ]

可以看到,没有参数的函数,它的properties就是一个空对象;有参数的函数,需要把每个参数的类型和说明写清楚。required表示哪些参数是必填的。

4.3 用模拟模型演示工具调用主流程

在没有接入真实模型之前,先写一个简单的规则函数,模拟模型选择工具的过程。逻辑很简单:用户输入包含“时间”就调用时间工具,包含“计算”就调用计算工具。

# 文件路径:mock_agent.py import json from tools import get_current_local_time, calculate from tool_schema import tools_schema # 工具名到实际函数的映射表 TOOL_MAP = { "get_current_local_time": get_current_local_time, "calculate": calculate, } def mock_llm_call(prompt: str): """模拟模型判断:返回一个工具调用意图""" if "时间" in prompt: return { "name": "get_current_local_time", "arguments": {} } if "计算" in prompt: expression = prompt.replace("计算", "").strip() return { "name": "calculate", "arguments": {"expression": expression} } return None def run_agent(prompt: str): print(f"用户提问:{prompt}") intent = mock_llm_call(prompt) if intent is None: print("模型判断:不需要调用工具") print("模型回答:抱歉,我暂时无法处理这个问题。") return tool_name = intent["name"] arguments = intent["arguments"] print(f"模型选择工具:{tool_name}") print(f"工具参数:{json.dumps(arguments, ensure_ascii=False)}") # 执行工具 func = TOOL_MAP[tool_name] result = func(**arguments) print(f"工具执行结果:{result}") # 正常情况下,这里的执行结果会再次传给模型,由模型生成最终回答 final_answer = f"根据工具返回结果,我的回答是:{result}" print(f"模型最终回答:{final_answer}") if __name__ == "__main__": run_agent("帮我计算 3 + 5 * 2") print() run_agent("现在几点了")

在项目根目录执行:

python mock_agent.py

预期输出大致如下:

用户提问:帮我计算 3 + 5 * 2 模型选择工具:calculate 工具参数:{"expression": "3 + 5 * 2"} 工具执行结果:13 模型最终回答:根据工具返回结果,我的回答是:13 用户提问:现在几点了 模型选择工具:get_current_local_time 工具参数:{} 工具执行结果:2025-xx-xx xx:xx:xx 模型最终回答:根据工具返回结果,我的回答是:2025-xx-xx xx:xx:xx

这个小例子已经把自定义工具的完整链路走通了。你可能会说:“这个模型选择逻辑太简单了,根本不是真正的大模型。” 没错,这个 mock 函数只是为了让你看清楚流程。真正的模型,只是在“判断调用哪个工具、生成什么参数”这一步做了更聪明的决策,后面的执行逻辑和回传逻辑完全一致。

5. 接入真实大模型:Function Calling 版完整代码

理解了原理之后,下面把 mock 部分替换成真实的大模型调用。这里使用 OpenAI 风格的 SDK 接口,国内多数兼容接口也可以按同样方式接入。

5.1 安装依赖与客户端初始化

如果你还没有安装依赖,先执行:

pip install openai python-dotenv

然后在代码中初始化客户端。客户端默认读取环境变量中的 API Key,如果你的服务商需要自定义地址,可以显式传入base_url

# 文件路径:real_agent.py import json import os from openai import OpenAI from dotenv import load_dotenv from tools import get_current_local_time, calculate from tool_schema import tools_schema load_dotenv() client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), # 如果使用的是兼容 OpenAI 接口的国内服务,取消下面这行注释并填写服务商地址 # base_url="https://你的服务商地址/v1", ) MODEL_NAME = os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini") TOOL_MAP = { "get_current_local_time": get_current_local_time, "calculate": calculate, }

注意,如果你使用的是国内模型,模型名称必须改为你实际可用的模型 ID。这里不写死,是为了适应不同读者的环境。

5.2 执行工具并生成最终回答

下面定义两个函数:一个负责根据模型返回的工具名执行对应的 Python 函数,另一个负责把工具结果回传给模型。

# 继续编写 real_agent.py def execute_tool_call(tool_call): """根据模型返回的 tool_call 执行工具函数""" tool_name = tool_call.function.name arguments = json.loads(tool_call.function.arguments) print(f"模型选择工具:{tool_name}") print(f"工具参数:{json.dumps(arguments, ensure_ascii=False)}") func = TOOL_MAP[tool_name] result = func(**arguments) print(f"工具执行结果:{result}") return { "role": "tool", "tool_call_id": tool_call.id, "content": str(result), } def run_agent(prompt: str, max_steps: int = 3): messages = [{"role": "user", "content": prompt}] print(f"用户提问:{prompt}") for step in range(max_steps): response = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=tools_schema, ) message = response.choices[0].message # 如果模型没有返回工具调用,说明它可以基于已有内容回答 if not message.tool_calls: print(f"模型最终回答:{message.content}") return # 先把模型的决策追加到消息列表 messages.append(message) # 依次执行模型请求的所有工具调用 for tool_call in message.tool_calls: tool_result = execute_tool_call(tool_call) messages.append(tool_result) print("已达到最大调用轮数,停止继续调用。") if __name__ == "__main__": run_agent("帮我计算 3 + 5 * 2 的结果")

这段代码有几个重要的设计点:

第一,模型返回的tool_calls是一个列表,说明一次回答可能请求调用多个工具。遍历执行并把每个结果都按role: "tool"的方式追加回消息列表,模型才能继续工作。

第二,messages中必须包含之前的用户提问、模型决策和工具结果。整个对话上下文是连在一起的,工具结果如果漏掉,模型会无法判断下一步应该怎么回答。

第三,循环需要设置最大轮数。真实场景中,模型可能反复调用工具甚至陷入循环,设置max_steps可以避免程序无休止运行。

执行前,在.env中填好 API Key 和模型名称,然后运行:

python real_agent.py

如果一切正常,你会看到模型先返回一个工具调用意图,然后程序执行calculate函数,随后模型基于计算结果给出最终答案。这个版本已经是一个真正的自定义工具小项目了。

6. 使用 LangChain 简化自定义工具开发

如果你在工程化项目中使用 LangChain,定义工具可以更简洁。LangChain 提供了@tool装饰器,只需要写好函数和文档字符串,框架会自动帮你生成 JSON Schema。

6.1 用 @tool 装饰器定义工具

# 文件路径:langchain_tools.py from datetime import datetime from langchain_core.tools import tool @tool def get_current_local_time() -> str: """返回当前的本地时间,格式为 YYYY-MM-DD HH:MM:SS""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") @tool def calculate(expression: str) -> str: """ 计算四则运算表达式。 参数 expression: 需要计算的数学表达式。 例如:3 + 5 * 2 """ allowed_chars = set("0123456789+-*/(). ") if not set(expression).issubset(allowed_chars): return "非法表达式" return str(eval(expression))

注意,函数名和文档字符串会直接影响模型的调用行为。文档字符串里的说明就是模型读到的工具描述,写得不清楚,模型就会在错误的时候调用错误工具。

6.2 工具注册与挂载到 Agent

在 LangChain 中,创建 Agent 时把工具列表传进去即可。不同版本的 LangChain API 差异较大,这里给出一个较常见的示意写法。具体以你当前安装版本的官方文档为准。

# 文件路径:langchain_agent_demo.py from langchain_tools import get_current_local_time, calculate # 注册工具列表 tools = [get_current_local_time, calculate] print(tools) # 将 tools 传入 Agent 创建方法 # 不同版本 LangChain 的 Agent 创建方式不同,这里不展开 # 常见做法: # agent = create_agent(model, tools) # agent.invoke({"messages": [("user", "帮我计算 3 + 5 * 2")]})

为什么不在这里给出完整的 Agent 创建代码?因为 LangChain 的create_agentinitialize_agent等方法在不同版本中的参数和类名差异较大,写死某一个版本反而容易误导读者。你需要做的是理解两件事:第一,@tool装饰器会自动把函数转成工具对象;第二,在 Agent 初始化时传入tools列表,框架就会在模型推理时自动附加工具定义。

7. 运行结果与验证方法

不管使用原生 Function Calling 还是 LangChain,运行后的验证逻辑都是一致的。下面给出一个标准的验证清单。

7.1 启动命令

本地最小示例:

python mock_agent.py

真实模型接入:

python real_agent.py

LangChain 示例:

python langchain_agent_demo.py

7.2 预期输出

real_agent.py为例,正常输出应该包含四个阶段:

用户提问:帮我计算 3 + 5 * 2 的结果 模型选择工具:calculate 工具参数:{"expression": "3 + 5 * 2"} 工具执行结果:13 模型最终回答:3 + 5 * 2 的计算结果是 13。

这四个阶段缺一不可。特别要关注“工具执行结果”,它必须由代码真实计算出来,而不是模型直接“猜”出来的。

7.3 判断标准

如何确定你的自定义工具成功了?可以从三个角度验证:

  • 功能维度:用户输入自然语言,模型能自动选择正确工具,并且参数传得准确。
  • 执行维度:工具函数真实执行并返回结果,返回值正确。
  • 反馈维度:模型读取工具结果后,能基于结果生成合理回答,而不是忽略工具结果自说自话。

如果第三个维度出现问题,比如模型返回了None或重新说了一段不相干的内容,优先检查messages列表中的tool_call_id是否对得上、工具结果是否按role: "tool"正确追加。

失败时先不要急着怀疑框架。第一步看日志里模型是否输出了tool_calls;第二步看参数是否被正确解析;第三步看工具函数是否抛出异常;第四步看最终回答是否基于工具结果。按照这个顺序排查,大部分问题都能定位。

8. 自定义工具常见问题与排查思路

在实践过程中,下面几个问题几乎每个人都会遇到。这里用表格整理成排查清单。

问题现象可能原因排查方式解决方案
模型一直不调用工具工具描述不清晰,模型判断不出何时使用查看打印出的工具定义,检查 description 是否具体改写描述,明确触发场景和示例
模型返回的 arguments 解析报错模型输出了非标准 JSON打印原始 arguments 字符串用 try-except 包裹 json.loads,失败时提示模型重新生成参数
工具函数执行结果报错参数类型不匹配,或函数内部异常打印实际传入的参数,确认类型在函数入口做参数类型校验和异常捕获
模型忽略工具结果工具结果未按role: "tool"回传,或tool_call_id不匹配打印 messages 列表,检查消息结构修正消息组装逻辑
API Key 未加载环境变量没有设置,或 .env 文件未读取在代码中临时打印 os.getenv("OPENAI_API_KEY")确认 .env 文件路径和变量名
工具调用陷入死循环模型不断返回工具调用,且没有终止条件查看日志,确认工具调用轮数设置 max_steps 最大轮数,超过后强制退出
使用框架时提示 tools 格式错误LangChain 版本之间 API 不兼容查看框架当前版本文档按官方文档调整创建 Agent 的方式

在这些问题中,最容易被忽略的是“工具描述”的质量。很多初学者把工具描述写得很随意,例如“计算用的工具”,模型自然不知道什么时候该调用。更好的写法是明确场景:比如“当用户需要计算数学表达式时使用,例如 3 加 5 乘以 2,用户输入包含加、减、乘、除、括号等计算需求时,将表达式整理后传入”。

9. 自定义工具开发最佳实践

如果你准备把自定义工具应用到真实项目中,下面的实践建议值得认真对待。

9.1 描述要具体,不要只说“是什么”

工具描述应该包含三部分:工具的用途、触发条件、参数填写规则。最好给出一个示例。模型对示例的敏感度很高,一个精心写的示例往往比长篇说明更有效。

9.2 返回结果要精简、结构化

工具返回值最终会拼接到上下文中继续传给模型。如果返回几万行日志,模型不仅处理速度慢,还可能被无关信息干扰。更推荐的做法是:工具内部做好裁剪、汇总和格式化,返回给模型的是一句话或一份结构化摘要。

9.3 工具内部必须做参数校验

模型生成参数时也可能出错。不要假设参数一定合法。在工具函数入口处做好类型校验、范围校验和异常捕获,避免把异常直接抛出导致整个智能体崩溃。

9.4 谨慎处理有副作用的操作

如果工具涉及发送邮件、写入数据库、删除文件、调用付费接口等操作,必须设置权限边界。更稳妥的做法是把操作设计成“先生成操作内容、用户确认后再执行”,或者至少记录完整的操作日志。

9.5 给工具设置超时和重试

调用外部 API 时可能遇到网络波动。工具内部可以设置合理的超时时间、重试次数和兜底返回。这样模型拿到的始终是一段可读的结果,而不是一个无意义的异常堆栈。

9.6 用日志记录每次工具调用

在开发阶段,把用户输入、模型选择、参数、执行结果、最终回答全部打印或写入日志。这能极大提升调试效率。生产环境则需要对日志做脱敏处理,避免敏感信息泄漏。

9.7 控制工具数量,避免“选择困难”

不是工具越多越好。一个智能体挂载几十个工具时,模型反而更容易选错。如果工具数量持续增长,可以考虑按业务领域拆分成多个智能体,每个智能体只维护自己领域内的少量工具。

10. 总结与下一步学习建议

自定义工具是 AI 智能体开发的“基本功”。本文围绕厦门大学林子雨老师课程中的 8.8.3 节“自定义工具实操”展开,把工具原理、本地最小示例、真实模型接入、LangChain 快速开发、运行验证、常见问题和最佳实践都梳理了一遍。你可以把本文当作课程实验的配套笔记,也可以把它当成一个最小可运行的自定义工具模板。

下一步,建议你尝试三个方向:第一,把文章中的get_current_local_timecalculate工具替换成你自己熟悉领域的功能,比如读取本地 CSV 文件、查询数据库、调用某个内部接口;第二,设计一个需要一次调用多个工具才能完成的任务,观察模型如何拆解和编排;第三,研究当前的工具调用协议标准,比如 MCP(Model Context Protocol),把工具从“本地函数”升级为“可复用的标准化服务”。

如果这篇文章对你有帮助,建议收藏备用。尤其是当你刚开始学 AI 编程与智能体开发时,工具这块打通了,后面的多工具编排、记忆系统、RAG、多智能体协作才有实质性的基础。写代码的过程中遇到任何报错,先看日志,再回看第 8 节的排查表格,大部分问题都能找到答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询