ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自定义工具实操:从函数定义到智能体API服务化完整链路

自定义工具实操:从函数定义到智能体API服务化完整链路 智能体开发里有一个常见误区以为把大模型 API 接进去就算完成了一个智能体。实际上大模型只负责“想”真正让智能体“做”的是工具。没有工具的 Agent 只是聊天机器人有了自定义工具它才能查天气、读文件、调接口、算数据、发通知。这次我们围绕“自定义工具实操”展开从工具函数定义、注册、接入智能体到封装成 API 服务把这条链路完整走一遍。这篇文章的内容对应厦门大学林子雨老师的“AI编程与智能体开发”课程中 8.8.3 自定义工具实操部分。我们不讲空概念直接落到代码上。读完你能得到四样东西自定义工具的标准写法、工具注册到智能体的完整流程、一个可以跑通的多工具 Agent 示例、一套批量调用工具服务的工程化思路。本文适合谁正在学智能体开发、想用 LangChain 或 Qwen Agent 做落地项目、需要给自己的 Agent 接私有数据或内部接口的开发者。如果你只是了解概念也可以先看核心能力速览再挑实操章节看。1. 自定义工具是什么智能体的“可执行能力”智能体的工作流程可以简化成一条链理解任务 - 拆解步骤 - 选择工具 - 执行工具 - 汇总结果。大模型负责理解和拆解真正落地执行的那一步靠的是工具。所谓自定义工具就是开发者自己写的函数再按照框架要求的格式注册给模型。大模型在推理时看到用户问题会判断“这个问题需要调用哪个工具”然后生成一个结构化的调用请求。框架收到请求后执行对应的 Python 函数把结果返回给模型模型再基于结果组织自然语言回复。一次完整的工具调用包含三个关键要素要素作用对应代码函数本体实际执行逻辑def get_weather(city): ...函数描述告诉模型“这个工具是干什么的什么时候用”查询指定城市的实时天气参数定义说明工具需要什么参数参数的格式和含义city: str以及参数注释很多新手第一次写自定义工具失败不是因为函数有 bug而是因为描述写得太含糊。模型是“看描述选工具”的描述不清楚它就不会选。这点后面会专门展开。2. 核心能力速览能力项说明适用方向AI编程、智能体开发、Function Calling 工具接入核心功能将任意 Python 函数封装为可供模型调用的工具框架支持LangChain、Qwen Agent、OpenAI Function Calling 等硬件需求纯 API 调用模式无需 GPU本地模型模式需要按模型要求配置显存占用取决于模型使用云端 API 几乎不占显存本地 7B 模型通常 6G 显存起启动方式脚本启动 / Jupyter 分步执行 / FastAPI 服务化接口能力可封装为 HTTP API支持外部系统调用批量任务支持可串行可并发建议加队列和重试适合场景数据查询、办公自动化、内容生产、私有接口接入从材料看自定义工具这一节最关键的实践点是把一个普通函数变成模型能主动调用的工具。做到这一步后续的工具调用链、多智能体协作才有基础。3. 适用场景与使用边界自定义工具适合解决一类问题用户用自然语言提出需求程序需要到外部系统拿数据或执行操作。典型场景包括天气查询、新闻检索、商品比价。读取本地文件、Excel 处理、PDF 解析。调用企业内部 API如订单查询、库存查询。执行数学计算、代码运行、数据库查询。发送邮件、推送消息、创建日程。不适合的场景也要说清楚。如果任务不需要外部环境纯靠模型内部知识就能回答就不需要上工具如果任务的失败成本很高比如直接操作生产数据库、转账、删除文件就不能让模型全自动调用必须加人工确认环节。使用边界第一条是授权边界。工具本质上是替模型“伸手”去访问外部资源所以凡是涉及他人数据、版权内容、人脸信息、声音信息的操作必须确认有合法授权。第二个边界是权限边界。给模型挂的工具应该遵循最小权限原则。测试时给的 API Key 尽量只开只读权限不要拿生产环境的管理员 Key 去调试。第三个边界是审计边界。每次工具调用的入参和返回值都要落日志否则出了问题无法回溯。4. 环境准备与前置条件自定义工具本身不挑环境Windows、macOS、Linux 都能跑。如果你用的是 Windows建议直接装 Anaconda 或 Miniconda创建一个独立环境避免和现有 Python 环境冲突。4.1 基础环境要求建议先确认以下条件Python 3.9 或更高版本。pip 能够正常安装依赖包。有一个可用的模型 API Key比如 OpenAI、通义千问、DeepSeek 等用于跑模型调用部分。不需要 GPU。使用云端 API 时工具调用链路只消耗少量 CPU 内存。4.2 创建虚拟环境打开终端执行以下命令# 创建独立环境python 版本按本机实际可用的 3.9 选择 conda create -n agent-tool python3.10 -y conda activate agent-tool4.3 安装依赖包本实操示例主要依赖 LangChain 生态安装命令如下pip install langchain langchain-openai python-dotenv如果使用 Qwen Agent可以再装pip install qwen-agent具体安装哪个框架看你自己在学哪一条技术路线。核心思想是一致的写函数 - 描述函数 - 注册给模型。4.4 配置 API Key在项目目录下创建.env文件把你的 Key 填进去OPENAI_API_KEYsk-你的密钥 OPENAI_BASE_URLhttps://api.openai.com/v1注意.env文件不要提交到 Git 仓库建议在.gitignore里加上它。5. 第一个自定义工具从普通函数到可调用工具我们从一个最朴素的需求开始让模型能够查询指定城市的天气。真实天气数据需要接第三方 API这里先用一个模拟函数演示工具定义、注册和调用的完整机制。理解了机制换成真实 API 只是替换函数体的问题。5.1 先写普通函数工具的本质是函数所以第一步先写一个正常的 Python 函数def get_weather(city: str) - str: 查询指定城市的实时天气。 参数 city: 城市名称例如厦门、上海、北京。 返回 该城市当前的天气描述和温度。 # 演示环境使用模拟数据实际项目中替换为真实天气 API weather_data { 厦门: 多云26 摄氏度东南风 3 级, 上海: 小雨22 摄氏度东风 2 级, 北京: 晴18 摄氏度北风 4 级, } return weather_data.get(city, f暂未收录 {city} 的天气数据)注意这里已经出现了工具三要素中的两个函数本体和文档字符串描述。文档字符串里的内容模型会读到所以你写清楚“参数是什么、返回什么、什么时候用”比代码本身更重要。5.2 用 LangChain 的 tool 装饰器注册LangChain 提供tool装饰器把一个普通函数变成工具对象from langchain_core.tools import tool tool def get_weather(city: str) - str: 查询指定城市的实时天气。 weather_data { 厦门: 多云26 摄氏度东南风 3 级, 上海: 小雨22 摄氏度东风 2 级, 北京: 晴18 摄氏度北风 4 级, } return weather_data.get(city, f暂未收录 {city} 的天气数据)定义变量时不要用get_weather()括号会把函数直接执行掉。要传函数本身weather_tool get_weather这句代码去看 LangChain 源码实现tool做的事情就是把函数、参数 schema、描述信息打包成一个BaseTool实例。框架会自动从类型注解city: str生成参数说明。5.3 再写一个计算器工具一个 Agent 通常需要多个工具。我们再注册一个可以安全的数学表达式计算器import ast import operator tool def safe_calculator(expression: str) - str: 计算数学表达式的值。 参数 expression: 一个数学表达式字符串例如 (1 2) * 3。 返回 计算结果。 # 用 ast 解析表达式只允许基本四则运算避免 eval 带来的注入风险 allowed_operators { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, } def _eval(node): if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.BinOp) and type(node.op) in allowed_operators: left _eval(node.left) right _eval(node.right) return allowed_operators[type(node.op)](left, right) if isinstance(node, ast.UnaryOp) and type(node.op) in allowed_operators: operand _eval(node.operand) return allowed_operators[type(node.op)](operand) raise ValueError(不支持的表达式) try: tree ast.parse(expression, modeeval) result _eval(tree.body) return str(result) except Exception as e: return f计算失败: {e}说明一下为什么使用ast而不是eval。直接eval(__import__(os).system(rm -rf /))这类危险表达式在真实环境里是可能被模型生成的虽然概率低但不能赌。ast解析配合白名单操作符只允许数学运算从机制上阻断任意代码执行。这个思路适用于所有用户输入参与执行的工具。5.4 验证工具是否能被框架识别写一个独立脚本test_tools.py把两个工具打印出来看看from tools import get_weather, safe_calculator print(get_weather.name) print(get_weather.description) print(get_weather.args_schema.schema()) print(---) print(safe_calculator.name) print(safe_calculator.description) print(safe_calculator.args_schema.schema())命令行运行python test_tools.py预期输出里能看到 LangChain 自动生成了工具的 JSON Schema包含参数名、类型和是否必填。只要这一步没问题工具定义就成功了下面进入智能体接入环节。6. 把工具接入智能体让模型自动决定调用工具定义好之后需要被模型“看见”。LangChain 里bind_tools或create_react_agent都可以把工具列表传给模型。下面用一个最小 Agent 来测试工具调用链路。6.1 编写最小 Agent创建agent_demo.pyimport os from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import PromptTemplate from tools import get_weather, safe_calculator load_dotenv() model ChatOpenAI( modelgpt-4o-mini, temperature0, api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) tools [get_weather, safe_calculator] prompt PromptTemplate.from_template( 你是一个能调用工具的智能体。请根据用户的问题选择并调用合适的工具。 工具列表 {tools} 工具名称格式 {tool_names} 思考过程 {agent_scratchpad} 用户问题 {input} ) agent create_react_agent(llmmodel, toolstools, promptprompt) executor AgentExecutor(agentagent, toolstools, verboseTrue, handle_parsing_errorsTrue)然后写一个测试入口连续问三个问题questions [ 厦门今天天气怎么样, 计算 (12 34) * 5 的结果, 你好介绍一下你自己, ] for q in questions: print( * 40) print(f用户提问{q}) result executor.invoke({input: q}) print(f最终回答{result[output]})运行python agent_demo.py如果你的模型和 Key 配置正确观察点有两个前两个问题会触发“工具调用”日志中会出现Action: get_weather或Action: safe_calculator。第三个问题没有任何工具可以调用Agent 会直接回答这说明模型具备“不调用工具”的判断能力。6.2 多轮对话中的工具调用上面的 ReAct Agent 每次invoke都是独立的一次调用不会记住上轮内容。如果你需要多轮记忆最简单的做法是把历史记录拼进 promptfrom langchain_core.messages import HumanMessage, AIMessage history [] def chat(message: str): history.append(HumanMessage(contentmessage)) response executor.invoke({input: message, chat_history: history}) history.append(AIMessage(contentresponse[output])) return response[output] print(chat(厦门天气怎么样)) print(chat(那上海呢))这里第二问如果模型足够聪明可以理解“那上海呢”指的是“上海天气怎么样”并复用get_weather工具。真实项目中历史记忆可以直接交给 LangGraph 等框架来管理比手动拼接更稳定。7. 用 FastAPI 封装接口工具服务的工程化落地工具接入 Agent 并且能跑通之后下一步就是服务化。把 Agent 包成一个 HTTP 接口其他系统就可以通过curl或requests调用你的智能体能力。这也是把自定义工具接入业务系统的最常用方式。7.1 创建 FastAPI 服务安装依赖pip install fastapi uvicorn创建api_server.pyimport os from dotenv import load_dotenv from fastapi import FastAPI from pydantic import BaseModel from langchain_openai import ChatOpenAI from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import PromptTemplate from tools import get_weather, safe_calculator load_dotenv() app FastAPI(title自定义工具 Agent 服务) model ChatOpenAI( modelgpt-4o-mini, temperature0, api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) tools [get_weather, safe_calculator] prompt PromptTemplate.from_template( 你是一个能调用工具的智能体。请根据用户的问题选择并调用合适的工具。 工具列表 {tools} 工具名称格式 {tool_names} 思考过程 {agent_scratchpad} 用户问题 {input} ) agent create_react_agent(llmmodel, toolstools, promptprompt) executor AgentExecutor(agentagent, toolstools, verboseTrue, handle_parsing_errorsTrue) class ChatRequest(BaseModel): message: str class ChatResponse(BaseModel): reply: str app.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): result executor.invoke({input: req.message}) return ChatResponse(replyresult[output]) if __name__ __main__: import uvicorn uvicorn.run(app, host127.0.0.1, port8000)7.2 启动与测试接口启动服务python api_server.py另开一个终端用 curl 验证curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {message: 厦门天气怎么样}返回结果类似{ reply: 厦门当前天气为多云26 摄氏度东南风 3 级。 }再用 Python requests 调用一次import requests resp requests.post( http://127.0.0.1:8000/chat, json{message: 计算 (12 34) * 5 的结果}, timeout60, ) print(resp.json())接口能跑通意味着自定义工具已经变成了一种可交付的服务能力。接下来可以接给前端页面、企业微信机器人、内部系统等。7.3 批量任务调用当你有大量问题要交给 Agent 处理时不能每次同步等待要用批量任务模式。最简单的实现是串行循环import time import requests questions [ 北京天气怎么样, 上海天气怎么样, 计算 123 * 456 的结果, 厦门天气怎么样, ] results [] for q in questions: start time.time() try: resp requests.post( http://127.0.0.1:8000/chat, json{message: q}, timeout60, ) resp.raise_for_status() results.append({question: q, answer: resp.json()[reply], status: ok}) except Exception as e: results.append({question: q, answer: str(e), status: failed}) elapsed time.time() - start print(f问题{q} | 耗时{elapsed:.2f}s) # 将结果持久化保存 import json with open(batch_results.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)批量任务切记两点一是记录每个任务的耗时和状态失败的要能定位二是控制并发不要过大API 有速率限制盲目开线程很可能触发限流。8. 资源占用与性能观察很多读者关心自定义工具跑起来到底占多少资源。这里把情况说清楚。如果你使用的是云端模型 API本地进程只是做“用户问题转发 - 模型返回 - 执行工具函数 - 结果返回模型”这几步CPU 占用很低内存一般不超过 300MB不占用 GPU 显存。核心瓶颈在网络请求延迟和模型响应时间上。如果你使用的是本地部署模型资源占用主要由模型和推理框架决定。比如本地跑 7B 模型显存占用通常在 6G 到 10G 之间跑 14B 模型建议 12G 以上。这部分占用跟自定义工具本身无关模型加载进显存后工具函数只是普通 Python 调用。需要观察的资源指标有三个观察项观察方式关注点单次请求耗时在接口里记录时间戳工具调用越多耗时越长因为多了一到两轮模型往返内存占用top或任务管理器Python 进程内存是否持续增长异常增长要查是否有资源未释放API 调用次数在 Agent 日志中统计一次工具调用通常会产生多轮模型请求直接决定费用工具数量对性能的影响也要注意。每多一个工具模型的 token 消耗就会增加因为工具描述、参数 schema 都要作为上下文传给模型。生产环境里工具数量控制在 10 个以内描述尽量精简。9. 常见问题与排查方法自定义工具看起来简单实际写起来容易踩坑。下面是按经验整理的高频问题。问题现象可能原因排查方式解决方案模型从不用某个工具工具描述不清晰模型不知道何时调用查看工具 description 是否明确说明适用场景重写描述给出典型的调用示例模型调用工具但参数传错参数 schema 生成错误或用户输入缺少信息检查 args_schema.schema() 的字段定义给参数加 description工具内部做默认值处理工具函数报错导致整个对话失败Agent 没有做异常捕获错误直接抛出看控制台 Traceback 定位报错函数在工具函数内部 try/except返回友好错误信息依赖安装失败Python 版本和包版本不匹配pip list查看当前版本的依赖使用虚拟环境按官方文档指定版本重装API Key 报 401 错误.env未加载或 Key 过期在代码中打印 os.getenv 检查是否读到了确认.env文件位置、格式和 Key 有效性FastAPI 无法启动端口被占用8000 端口被其他进程占用Windows: netstat -anofindstr 8000Linux:lsof -i:8000批量任务中途卡住某个工具调用超时或网络阻塞在批量脚本中加超时参数和日志为每个请求设置 timeout失败重试 2 次模型陷入工具循环反复调用同一工具工具返回结果不满足模型预期查看 Agent 日志中的多次 Action 序列检查工具返回内容是否清晰必要时设置最大迭代次数中文输出乱码控制台编码问题检查终端编码格式Windows 下运行chcp 65001切换 UTF-8最值得警惕的是“工具循环”。模型可能因为工具返回值不明确连续调用同一个工具五六次空转消耗 token。解决方法是第一工具返回信息要直接、结构化第二Agent 配置里设置max_iterations比如 3 到 5 次后强制停止。10. 最佳实践与使用建议自定义工具做完能跑只是第一步。要投入到真实项目还需要注意下面这些点。10.1 工具设计原则一个工具只做一件事。不要写一个“万能函数”然后靠参数分支判断走不同逻辑。模型是依据描述选工具的工具越多、职责越单一模型选择越准确。工具描述里要写出“什么时候用、什么时候不用”。比如tool def get_weather(city: str) - str: 仅在用户明确询问天气时使用。查询股票、新闻等无关信息时不要调用本工具。不要小看这句补充描述它能把很多误调用直接挡掉。10.2 安全边界设计自定义工具是智能体最危险的组件因为它能执行真实操作。建议按这个标准设计所有工具函数内部必须做入参校验不能信任模型生成的内容。涉及文件读写时路径要限制在白名单目录内防止路径穿越。涉及网络请求时目标 URL 要校验域名防止 SSRF。涉及删除、修改等危险操作时接口层增加人工确认参数。记录每次调用的入参、返回值和耗时日志保留至少 30 天。10.3 开发流程建议推荐按下面的顺序开发不要把工具和 Agent 一次写完再调先用纯 Python 测函数本身确认输入输出正确。再注册到框架打印 schema确认参数能被识别。然后接一个最小 Agent测单轮工具调用。测多轮对话和工具组合。最后才封装 API 和批量任务。每一步都是上一层的“最小可验证单元”这样出了问题能快速定位到函数层、描述层、还是编排层。10.4 合规提醒涉及人脸、声音、版权素材、个人数据的工具调用必须确认授权链条完整。企业内部的工具服务不建议直接暴露到公网至少要加接口鉴权。批量爬取外部数据再通过工具提供给模型需要评估目标网站的版权政策和 robots 协议不要触碰法律风险边界。11. 总结与下一步自定义工具实操这条线核心就三件事写函数、写描述、注册给模型。函数是执行能力描述是让模型理解能力注册是把能力暴露给模型。三者缺一不可。建议你先拿天气查询这种带参数、带返回值的小函数练手跑通后再逐步增加文件工具、接口工具和数据库工具。最先要验证的不是 Agent 能不能回答复杂问题而是模型能不能在对话中正确选中工具、传对参数、拿到结果。这三点验证通过整个智能体的工程基础就稳了。最容易踩的坑有两个第一是工具描述不清晰导致模型永远不调用第二是直接eval用户输入导致安全风险。前者影响体验后者影响安全遇到要优先处理。后续可以继续扩展的方向把工具调用链抽成公共方法统一处理日志、限流、重试用 LangGraph 改造 Agent支持多节点、多分支的复杂流程再接一个多智能体协作层让多个 Agent 各自持有不同的自定义工具分工完成一个大任务。建议把这篇文章的示例代码保存下来做成你自己的工具脚手架后面每写一个 Agent 项目都可以直接复用。
返回列表