Function Calling 与 Skill 全解析:从「明天天气如何」看懂 AI Agent 的工具机制
大语言模型本身只会「出文字」,不会「动手」。当你问 Agent「北京明天天气如何」并得到一个具体答案时,背后是一套 Function Calling 工程协议在运转:它把「想调用工具」的意图表达成结构化 JSON,再由外部代码真正执行。
引言
从底层设计讲起,本文用一个真实的「查询北京明天天气」案例,拆解工具定义、Agent 执行循环、Qwen 模型的接入代码,最后以开源客户端 Cherry Studio 为例,回答两个很多开发者都困惑的问题:多个 Skill 会不会全部塞给模型?多个工具会不会全部放进 tools 数组?
01 / Function Calling 是怎么设计的
LLM 没有「手」,Function Calling 的本质是:约定一种输出协议,让模型把「我想调用工具」的意图用结构化 JSON 表达出来,由外部代码真正执行。
整个设计分四层:
1. 工具声明层:JSON Schema
每个工具就是一个 JSON Schema,核心三要素是工具名、用途描述(description 是模型读懂这个工具的关键,写得越准,模型选得越对)、参数定义。
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市指定日期的天气情况",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,例如 北京" },
"date": { "type": "string", "description": "日期,格式 YYYY-MM-DD,例如 2026-10-24" }
},
"required": ["city", "date"]
}
}
}
2. 请求层:tools 参数
工具定义作为 tools 参数随聊天请求一起发送。模型生成回复时输出两种东西之一:普通文本,或 tool_calls——一段结构化输出,写明「该调用 get_weather,参数是 city=北京, date=2026-10-24」。
3. Agent Loop:最关键的配套逻辑
Agent 需要实现一个循环:
while True:
response = LLM(messages, tools)
if response 有 tool_calls:
for call in response.tool_calls:
func = registry[call.function.name] # 查本地注册表
result = func(**json.loads(call.function.arguments)) # 真正执行
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
else:
return response.content # 模型给出最终自然语言答案
4. 工具执行层:真实能力所在
模型只「报」不「做」——真正请求天气 API 的是 Agent 端的本地函数。安全上需要白名单校验(只执行已注册的工具名)、参数校验(符合 JSON Schema)、权限隔离等。
02 / 让 LLM 回答「明天天气如何」需要哪些配套
这个问句看似简单,其实有两个语义缺口:哪个城市?明天是哪一天? Agent 的配套设计是:
- 工具签名覆盖缺口:用
city + date作为参数,而不是一个模糊的query。
- 声明当前时间:Agent 在系统提示中带上时间戳,模型才能推算出
date=2026-10-24。
- 多轮上下文管理:第一轮用户提问 → 模型输出 tool_call → Agent 执行天气 API 拿原始数据 → 追加为
role="tool"消息 → 第二轮模型基于数据生成总结。
03 / Qwen 模型的代码接入(OpenAI 兼容接口)
Qwen 系列通过阿里云 DashScope 的 OpenAI 兼容端点支持 function calling。调用的就是标准 chat.completions 接口,只是请求里多了 tools 字段。
from openai import OpenAI
import json
client = OpenAI(
api_key="YOUR_DASHSCOPE_API_KEY",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 1. 定义工具 schema
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市指定日期的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名,如 北京"},
"date": {"type": "string", "description": "YYYY-MM-DD,如 2026-10-24"},
},
"required": ["city", "date"]
}
}
}]
# 2. 本地函数注册表(工具的真实实现)
def get_weather(city: str, date: str) -> str:
# 这里调用真实天气 API
return f"{city} {date} 天气晴,12~22°C,微风"
tool_registry = {"get_weather": get_weather}
# 3. Agent Loop:首轮请求
messages = [
{"role": "system", "content": "当前时间是 2026-10-23。查询天气需调用工具。"},
{"role": "user", "content": "北京明天的天气如何?"}
]
response = client.chat.completions.create(
model="qwen3-flash", # 支持工具调用的 Qwen3 系列模型
messages=messages,
tools=tools, # 注册工具的关键参数
tool_choice="auto" # 模型自行决定是否调用
)
# 4. 判断模型是否请求调用工具
if response.choices[0].message.tool_calls:
messages.append(response.choices[0].message) # tool_calls 记录进历史
for call in response.choices[0].message.tool_calls:
args = json.loads(call.function.arguments)
result = tool_registry[call.function.name](**args) # 本地执行
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result
})
final = client.chat.completions.create(model="qwen3-flash", messages=messages)
print(final.choices[0].message.content)
两个关键点:「注册」不是服务端注册——工具 schema 每次请求都要随 tools 带上,模型端无状态、不存储任何工具;真正需要注册的是 Agent 本地的函数表 tool_registry,用来在收到 tool_calls 时执行。tool_choice 可取 auto(模型自选)、required(必须调用)、none(禁用)。
注:本节以 Qwen3 系列 + 阿里云 DashScope 兼容端点为例,代码中的model字段(如qwen3-flash)请按你实际部署的模型填写;不同厂商的兼容端点 base_url 与鉴权方式可能略有差异。
04 / Cherry Studio 的答案:Skill 与 Tools 是不是「全塞给模型」
Cherry Studio 是 GitHub 上 5 万+ Star 的开源 AI 工作台(TypeScript 实现),支持多个助手、工具、Skill 和 MCP 服务器。它的实现方式恰好回答了文章标题的问题。
Skill:文本指令,注入 System Prompt
Cherry Studio 的 Skill 本质上是 Markdown 指令文件(SKILL.md,含 name/description 元信息),遵循业界(Anthropic)推动的 Agent Skills 开放规范。它的工作方式是:
- 不是函数调用协议,而是提示词注入——Skill 的内容会被拼接到该助手的 system prompt 里,随每次请求一起发给模型;
- 是否全部注入,取决于你启用了哪些 Skill:通常助手只挂载自己相关的 Skill,未挂载的不进入请求;主流 Agent Skills 规范还提倡「两阶段加载」——只把每个 Skill 的 name/description(约 100 tokens)常驻上下文,完整 SKILL.md 正文按需激活后才注入;
- Cherry Studio 社区也讨论过「常驻 system prompt 导致每个会话都加载长文本、推高固定 token 成本」的痛点,建议把偶尔使用的任务模板做成可手动插入的片段,而不是永久注入。
对应到代码组织,大致是这样(示意):
// 构建 system prompt:助手基础设定 + 已启用 Skill 的指令文本
function buildSystemPrompt(assistant, enabledSkills) {
const parts = [assistant.basePrompt];
for (const skill of enabledSkills) {
parts.push(`## Skill: ${skill.name}\n${skill.content}`); // SKILL.md 正文
}
return parts.join("\n\n");
}
messages.push({ role: "system", content: buildSystemPrompt(...) });
Tools:函数定义,进 tools 数组
对 Cherry Studio 里通过函数调用或 MCP 接入的工具,答案是:是的,该助手当前会话可用的工具 schema,要全部放进请求的 tools 数组——这是 OpenAI 兼容协议的要求,模型只能在看到的工具里做选择。Cherry Studio 对 MCP 支持良好,MCP 服务器提供的工具会经协议桥接转换成标准 function schema 后进请求。
// 汇总该助手启用的所有工具 schema(内置 + MCP 桥接而来)
const tools = [
...builtinTools.filter(t => assistant.enabledToolIds.includes(t.id)),
...mcpTools.filter(t => assistant.enabledMcpToolIds.includes(t.id)),
];
const response = await client.chat.completions.create({
model, messages,
tools: tools.map(toOpenAIToolSchema), // 全部可用工具,随请求发送
tool_choice: "auto"
});
一张表分清 Skill 和 Tool
| 维度 | Skill | Tool(函数/MCP) |
|---|---|---|
| 本质 | 文本指令(prompt) | 可执行能力(function schema) |
| 怎么进请求 | 拼进 system prompt | 放进 tools 数组 |
| 谁来执行 | 模型自己按指令做 | Agent 端代码真实执行 |
| 模型看到的成本 | 消耗上下文 token | 消耗上下文 token(每个工具定义几十~几百 tokens) |
| 典型用途 | 写作规范、流程经验、格式要求 | 查天气、搜网页、执行代码 |
编者注:本节对 Cherry Studio 的行为描述(Skill 以SKILL.md指令形式注入 System Prompt、工具走 OpenAI 兼容的tools字段、MCP 通过协议桥接),其源码细节以 TypeScript 实现,对外行为可通过官方仓库与文档确认;上方的 TypeScript 代码段为「典型实现示意」,反映业界通用做法,并非逐行源码,引用时请勿当作官方实现。
05 / 常见误区与工程建议
- 「工具越多越聪明」是错觉:每个工具 schema 都会占上下文、干扰选择,
tools只放当前会话真正可能用到的。Skill 同理,按需加载优于全量注入。
- description 就是工具的广告牌:模型选错工具,八成是描述写得模糊。
- 本地注册表是安全边界:只执行白名单内函数,参数先做 schema 校验,防注入、防越权。
- Skill 按开放标准写(最小 frontmatter:name + description),避免厂商锁定,未来在 Claude Code、Cherry Studio 等客户端间迁移成本低。
- Function calling 无状态:别指望模型「记住」你的工具,schema 每次请求都要带。
06 / 结语
Function calling 把「模型出主意」和「代码动手」 cleanly 地分开了:LLM 负责理解意图、决定调什么、传什么参数;Agent 负责真正执行、管理上下文、组织多轮对话。Skill 与 Tool 是同一套思想的两个侧面——前者把「经验」文本化注入,后者把「能力」结构化注册。理解了这条主线,无论是自己写一个最小 Agent,还是用好 Cherry Studio 这样的开源客户端,心里都有一张清晰的地图。