ARTICLE DETAIL

资讯详情

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

AI Agent(五):Tool Use 工具调用——用 TaoToken 统一 Key 打通 Function Calling 与 MCP

AI Agent(五):Tool Use 工具调用——用 TaoToken 统一 Key 打通 Function Calling 与 MCP 1. 从“只会说”到“能动手”AI Agent 工具调用到底卡在哪AI Agent 的 Tool Use工具调用能力说白了就是让大模型从“只会聊天”变成“能真正干活”。Function Calling 负责让 LLM 输出结构化的调用意图MCPModel Context Protocol负责把外部工具标准化地注册进来两者配合才能跑通一条完整的工具调用链路。这套东西适合谁适合已经在写 Agent 项目、被“模型说得好听但啥也干不了”折磨过的开发者也适合刚接触 Function Calling、想搞明白 tool_calls 到底怎么回填结果的新手。我一开始做 Agent 的时候最大的困惑不是模型不会调用工具而是调用链路太碎LLM 输出一个 JSON我得手动解析、手动执行、手动把结果塞回 messages中间任何一环格式对不上整个循环就断了。更麻烦的是每接一个新工具就要改一遍代码工具多了以后维护成本直线上升。后来我把接入点统一到 TaoToken 的 API 通道上用一套 Key 同时跑 Function Calling 和 MCP链路才真正稳定下来。这篇文章不讲空概念直接给你能复制的东西一份工具 schema、一段 MCP 配置、一次端到端调用验证。你跟着走一遍就能在自己的 Agent 项目里把 Tool Use 跑通。核心检索词就三个AI Agent、Tool Use、Function Calling外加 MCP 这个绕不开的协议。先说清楚一个最容易搞混的点LLM 本身不执行工具。它只负责“决定调用哪个工具、传什么参数”真正执行的是你的 Agent 代码。这个分工想明白了后面所有报错你都能定位到是“模型决策层”还是“执行层”的问题。2. TaoToken 前置统一 Key 打通 Function Calling 与 MCP 的接入点在动手写代码之前先把接入层理清楚。Tool Use 链路里有两个地方要发请求一是 LLM 的 chat/completions带 tools 参数二是 MCP Server 的工具注册与调用。如果这两条链路各用一套鉴权和地址调试起来会非常痛苦。我的做法是统一走 TaoToken 的 API 通道Base URL 固定为https://taotoken.net/apiKey 用同一个模型 ID 也在一处管理。先拿 Key。打开 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建一个新 Key复制出来存到环境变量里。注意别把 Key 硬编码进代码后面配置片段里我都用TAOTOKEN_API_KEY这个变量名。export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api模型 ID 这块Function Calling 对模型能力有要求不是所有模型都能稳定输出 tool_calls。我实测下来选支持工具调用的模型在请求里显式带上tools字段模型才会返回结构化的调用意图。如果你不确定当前模型支不支持可以先去模型对话页面https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite手动发一条带工具定义的请求看返回里有没有tool_calls字段这是最快的验证方式。MCP 这一侧TaoToken 的接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里有完整的协议说明和示例配置。我建议你先按文档把 MCP Server 的地址和鉴权方式确认一遍再回到 Agent 代码里对接。很多人卡在 MCP 连不上其实不是协议问题是 Base URL 或 Key 没对齐。这里有个关键点Function Calling 和 MCP 虽然协议不同但都可以复用同一个 API 通道和同一套鉴权。你不需要为 MCP 单独申请一套凭证也不需要维护两个 Base URL。统一之后Agent 代码里只需要一个 client 实例工具注册和模型调用共用链路清晰很多。如果你打算长期做编码类 Agent或者要跑多工具编排的复杂任务可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite它在长链路调用和并发工具执行上更稳一些。不过对于本文这个最小闭环普通 API Key 就够了。3. 可复制配置工具 schema、MCP 配置与 Agent 接入片段这一节是全文最核心的部分所有片段都可以直接复制。我按“工具定义 → MCP 注册 → Agent 接入”的顺序给你照着改路径和参数就行。先看工具 schema。Function Calling 要求工具用 JSON Schema 描述参数描述写得越清楚模型调用越准。下面这个add_device_model工具是我实际项目里用的你可以替换成自己的业务工具{ type: function, function: { name: add_device_model, description: 添加设备机型到数据库。当用户查询未解锁的机型时可以使用此工具将机型添加到数据库中。机型代码应该去除后缀如 IN、CN 等例如 CPH2223IN 应该使用 CPH2223。, parameters: { type: object, properties: { model: { type: string, description: 机型代码例如 CPH2223去除后缀如 IN、CN 等 }, marketName: { type: string, description: 中文营销名例如 OPPO Find N6 } }, required: [model] } } }注意description里我特意写了“去除后缀”的规则因为模型很容易把CPH2223IN原样传进来。工具描述就是给模型的说明书你写得越具体它犯错越少。接下来是 MCP 配置片段。MCP Server 的注册一般放在一个配置文件里路径按你的项目结构来。我用的是mcp/servers.json{ mcpServers: { device-tools: { command: python, args: [-m, mcp_server.device], env: { TAOTOKEN_API_KEY: ${TAOTOKEN_API_KEY}, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }这里command和args是启动 MCP Server 的方式env里把 Key 和 Base URL 透传进去。如果你的 MCP Server 是远程服务把command换成url字段即可。配置里的${TAOTOKEN_API_KEY}会从环境变量读取避免明文写 Key。然后是 Agent 接入片段。我用 Python 写一个最小闭环核心是三步注册工具、发起带 tools 的请求、处理 tool_calls 并回填结果。import os import json import requests BASE_URL os.environ[TAOTOKEN_BASE_URL] API_KEY os.environ[TAOTOKEN_API_KEY] MODEL_ID 你的模型ID def call_llm(messages, tools): resp requests.post( f{BASE_URL}/v1/chat/completions, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json, }, json{ model: MODEL_ID, messages: messages, tools: tools, tool_choice: auto, }, timeout60, ) resp.raise_for_status() return resp.json() def execute_tool(name, arguments): args json.loads(arguments) if name add_device_model: # 这里替换成你的真实业务逻辑 return json.dumps({success: True, message: f已添加 {args[model]}}) return json.dumps({success: False, message: unknown tool})这段代码里tool_choice: auto让模型自己决定要不要调用工具。execute_tool是执行层模型返回的arguments是字符串要先json.loads再处理。执行结果再序列化成字符串回填这是 Function Calling 的硬性要求。Agent Loop 的核心逻辑是这样def run_agent(user_input, tools): messages [{role: user, content: user_input}] for _ in range(10): data call_llm(messages, tools) choice data[choices][0][message] messages.append(choice) tool_calls choice.get(tool_calls) if not tool_calls: return choice.get(content, ) for tc in tool_calls: result execute_tool( tc[function][name], tc[function][arguments], ) messages.append({ role: tool, tool_call_id: tc[id], content: result, }) return 达到最大迭代次数注意messages.append(choice)这一步必须把模型返回的 assistant 消息含 tool_calls原样加进历史否则下一轮模型不知道自己在等工具结果。role: tool的消息要带上tool_call_id和请求里的id对应这是回填的关键。4. 验证请求一次端到端调用与成功结果配置写完了现在跑一次端到端验证。这一步的目的是确认整条链路通了模型输出 tool_calls → Agent 执行工具 → 结果回填 → 模型生成最终回复。先构造请求。把第 3 节的工具 schema 放进tools数组用户输入用一句会触发工具调用的话tools [{ type: function, function: { name: add_device_model, description: 添加设备机型到数据库。机型代码应去除后缀例如 CPH2223IN 使用 CPH2223。, parameters: { type: object, properties: { model: {type: string, description: 机型代码}, marketName: {type: string, description: 中文营销名} }, required: [model] } } }] print(run_agent(帮我把机型 CPH2223 添加到数据库, tools))跑起来之后你会看到模型第一轮返回的不是文本而是tool_calls。结构大概是这样{ choices: [{ message: { role: assistant, content: null, tool_calls: [{ id: call_abc123, type: function, function: { name: add_device_model, arguments: {\model\: \CPH2223\} } }] } }] }看到content是null、tool_calls有内容说明模型正确识别了工具调用意图。这时候 Agent 执行execute_tool把结果回填再发第二轮请求。第二轮模型拿到工具结果后会生成最终文本回复类似“已成功将机型 CPH2223 添加到数据库”。如果你用的是 MCP 方式验证步骤多一层先确认 MCP Server 启动成功再确认 Agent 能列出工具。可以用下面这段代码检查工具注册import requests resp requests.post( f{BASE_URL}/v1/mcp/tools/list, headers{Authorization: fBearer {API_KEY}}, json{server: device-tools}, timeout30, ) print(resp.json())返回里应该能看到add_device_model这个工具的定义。如果列表是空的说明 MCP Server 没注册成功回到第 3 节的servers.json检查command和args路径。实测下来整条链路最容易出问题的地方是arguments的解析。模型返回的是 JSON 字符串不是对象直接当 dict 用会报错。一定要先json.loads。另外tool_call_id必须一一对应多个工具调用时不能串。成功跑通后你会看到类似这样的完整输出[第一轮] 模型返回 tool_calls: add_device_model({model: CPH2223}) [执行] 工具返回: {success: true, message: 已添加 CPH2223} [第二轮] 模型最终回复: 已成功将机型 CPH2223 添加到数据库。这就是 Tool Use 的最小闭环。MCP 只是把工具注册从“代码里硬编码”变成“配置里声明”执行链路是一样的。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth这一节按真实报错来都是我踩过的坑。你对照自己的日志找对应项。401 Unauthorized最常见Key 没传对或者过期了。检查Authorization头是不是Bearer开头中间有空格。如果你用的是环境变量确认TAOTOKEN_API_KEY真的被 export 了Python 里os.environ读不到会直接报 KeyError 或者传空。还有一种情况是 Key 复制时带了换行或空格肉眼看不出来重新复制一遍。local proxy failed / connection refused这个报错通常出现在 MCP Server 启动阶段。servers.json里的command路径不对或者 Python 环境里没装 MCP 依赖。先手动在终端跑一遍python -m mcp_server.device看能不能启动。如果报模块找不到装依赖如果报端口占用换端口。注意别把本地代理配置和这个混在一起MCP 走的是标准输入输出或 HTTP不需要额外网络层。reading choices 报错 / KeyError: choices模型返回体里没有choices字段说明请求本身失败了但你的代码直接去取data[choices]。先打印完整resp.text看错误信息。常见原因是模型 ID 写错或者请求体里tools格式不对导致服务端拒绝。还有一种情况是模型不支持 Function Calling返回了普通文本但没有choices结构换一个支持工具调用的模型 ID 即可。OAuth / 鉴权失败如果你接的是需要 OAuth 的 MCP Serverservers.json里要配auth字段不能只靠 API Key。检查 token 有没有过期scope 对不对。TaoToken 的接入文档里有 OAuth 流程的完整说明按文档走一遍。如果报invalid_grant多半是回调地址和注册时不一致。tool_calls 为空但模型有回复模型没决定调用工具直接给了文本。原因通常是工具描述不够清楚或者用户输入没触发调用意图。把description写具体加上“当用户……时使用此工具”的引导。也可以在 system message 里明确告诉模型“你有工具可用合适时请调用”。arguments 解析失败json.loads报错说明模型返回的参数字符串格式不对。先打印原始字符串看有时候模型会多包一层引号或者带 markdown 代码块标记。可以在解析前做一次清洗去掉json 和标记。排查顺序建议先看 HTTP 状态码再看返回体结构最后看业务逻辑。401 和连接类错误在接入层choices 和 tool_calls 在协议层arguments 在执行层。分层定位别一上来就改代码。6. 语义一致 CTA把 Tool Use 链路固化到你的项目里跑通一次不代表稳定。我的经验是把工具 schema、MCP 配置、Agent Loop 这三样东西版本化管理每次改工具都走一遍端到端验证。工具描述和参数 schema 的改动最容易引入回归改完一定要重新跑第 4 节的验证请求。如果你在接入过程中遇到鉴权或协议问题直接去 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite重新生成一个 Key 试试再对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite检查 Base URL 和请求格式。想先验证模型对工具调用的支持程度用模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite手动发一条带 tools 的请求最快。长期做编码类 Agent 或多工具编排Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite在长链路和并发场景下更省心。最后留一个实用技巧在 Agent Loop 里加日志把每一轮的tool_calls和工具执行结果都打出来。出问题时不用猜直接看日志就知道是模型没调用、参数传错还是执行层报错。这个习惯能帮你省掉大量调试时间。
返回列表