在与大语言模型(LLM)交互时,我们常常会遇到它的能力边界:它不知道实时信息,也无法直接操作外部系统。为了解决这个问题,“工具调用”(Tool Calling / Function Calling)应运而生。
本文将剥离复杂的框架(暂不涉及 MCP 协议),通过一个实际的 AI Copilot 项目代码,带你搞懂工具调用背后的核心逻辑,并一步步实现让大模型具备查天气、搜文档的能力。
一、 工具调用的核心原理
很多人对 Function Calling 有一个误解,认为“大模型在运行我的代码”。实际上,大模型只负责动脑,不负责动手。
一个完整的工具调用流程包含以下四个步骤:
定义工具 (Define):开发者向大模型描述有哪些工具可用,以及这些工具需要什么参数。
模型决策 (Decide):大模型分析用户的提问。如果发现需要使用工具,它会暂停生成普通文本,而是输出一个要求调用某工具的 JSON 数据。
本地执行 (Execute):我们的服务端代码拦截到这个请求,在本地执行相应的函数(比如调 API、查数据库),拿到结果。
回传总结 (Synthesize):我们将执行结果以特定格式再次发给大模型,大模型基于这些真实数据,最终生成给用户的自然语言回答。
二、 实践步骤:从零手搓工具链
在我们的项目中,为了让 AI Copilot 更加智能,我们设计了一个工具注册中心。下面我们将结合代码,看看如何具体落地。
步骤 1:精确描述工具的“说明书” (Schema Definition)
大模型看不懂底层代码,它只能通过你提供的“说明书”来判断何时使用工具。在项目中,我们使用 Zod 来严格定义输入参数的类型和描述:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { z } from "zod";
export const lookupWeatherSchema = z.object({ location: z .string() .min(1) .max(80) .describe("要查询天气的地点名称,例如 Shanghai、Tokyo、New York"), });
export type LookupWeatherInput = z.infer<typeof lookupWeatherSchema>;
|
实践心得:describe 中的描述越清晰,大模型的触发就越精准。
步骤 2:编写本地执行逻辑 (Execution Logic)
当大模型说:“我要查 Shanghai 的天气”,我们的服务器就要真正去拉取数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
export async function lookupWeather({ location }: LookupWeatherInput): Promise<string> { const safeLocation = sanitizeLocation(location); console.log(`[Function Calling] Weather lookup: ${safeLocation}`);
const geoRes = await fetchWithRetry( `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(safeLocation)}&count=1&language=zh&format=json`, { method: "GET" } ); if (!geoRes.ok) return `查询 ${safeLocation} 天气失败`; const geoData = await geoRes.json(); const place = geoData.results?.[0];
if (!place) return `未找到地点:${safeLocation}`;
const weatherRes = await fetchWithRetry( `https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t=temperature_2m,weather_code&timezone=auto`, { method: "GET" } );
const weatherData = await weatherRes.json(); return [ `地点:${place.name}`, `时间:${weatherData.current.time}`, `气温:${weatherData.current.temperature_2m}°C`, ].join("\n"); }
|
步骤 3:高阶玩法——将 RAG 封装为工具
工具调用不仅能查天气,还能结合向量数据库(如 Pinecone)用来检索私有知识库。在项目中,我们将对 React 源码的搜索也封装成了一个 Tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
export const searchReactDocsSchema = z.object({ searchQuery: z.string().describe("用来去向量数据库检索的精准搜索关键词"), });
export async function searchReactDocs({ searchQuery }: SearchReactDocsInput): Promise<string> { const embedRes = await fetchWithRetry("https://api.gptsapi.net/v1/embeddings", { }); const queryVector = embedRes.data[0].embedding;
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY }); const index = pinecone.index(process.env.PINECONE_INDEX_NAME); const queryResponse = await index.query({ vector: queryVector, topK: 3, includeMetadata: true, });
const contextDocs = queryResponse.matches .map((match) => match.metadata?.text || "") .join("\n\n---\n\n");
return contextDocs || "未检索到相关内容"; }
|
这种模式非常强大:大模型不再是被动接受我们塞给它的文档,而是主动决定“我现在需要搜什么关键词”,极大提升了检索的灵活性。
为了让系统易于扩展,我们在 index.ts 中将所有的工具集中注册。这种配置化的写法也是未来对接 MCP (Model Context Protocol) 协议的基础:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { lookupWeather, lookupWeatherSchema } from "./lookup-weather"; import { searchReactDocs, searchReactDocsSchema } from "./search-react-docs";
export const SCRAPE_TOOLS = { search_react_docs: { description: "当用户提问关于 React 源码、API 用法或具体技术细节时,必须调用此工具去知识库中检索背景信息。", schema: searchReactDocsSchema, execute: searchReactDocs, }, lookup_weather: { description: "当用户提问某个地点的实时天气时,调用此工具查询该地点当前天气。", schema: lookupWeatherSchema, execute: lookupWeather, }, } as const;
|
结语
通过 Function Calling,大模型完成了从“懂很多道理的旁观者”到“能帮你干活的数字员工”的蜕变。理解了 定义 -> 决策 -> 本地执行 -> 总结 这个闭环,你就可以将企业内部的任何 API(如 ERP 系统、工单系统)接入到 LLM 中。
在后续的文章中,我们将进一步探讨如何引入 MCP(Model Context Protocol)协议,让这套工具调用的流程更加标准化、跨平台化。敬请期待!
源码已托管至🫱 GitHub,期待你的 Star🌟。