Y.UWON

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 的配套设计是:

  1. 工具签名覆盖缺口:用 city + date 作为参数,而不是一个模糊的 query。
  1. 声明当前时间:Agent 在系统提示中带上时间戳,模型才能推算出 date=2026-10-24。
  1. 多轮上下文管理:第一轮用户提问 → 模型输出 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 开放规范。它的工作方式是:

  1. 不是函数调用协议,而是提示词注入——Skill 的内容会被拼接到该助手的 system prompt 里,随每次请求一起发给模型;
  1. 是否全部注入,取决于你启用了哪些 Skill:通常助手只挂载自己相关的 Skill,未挂载的不进入请求;主流 Agent Skills 规范还提倡「两阶段加载」——只把每个 Skill 的 name/description(约 100 tokens)常驻上下文,完整 SKILL.md 正文按需激活后才注入;
  1. 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

维度SkillTool(函数/MCP)
本质文本指令(prompt)可执行能力(function schema)
怎么进请求拼进 system prompt放进 tools 数组
谁来执行模型自己按指令做Agent 端代码真实执行
模型看到的成本消耗上下文 token消耗上下文 token(每个工具定义几十~几百 tokens)
典型用途写作规范、流程经验、格式要求查天气、搜网页、执行代码
编者注:本节对 Cherry Studio 的行为描述(Skill 以 SKILL.md 指令形式注入 System Prompt、工具走 OpenAI 兼容的 tools 字段、MCP 通过协议桥接),其源码细节以 TypeScript 实现,对外行为可通过官方仓库与文档确认;上方的 TypeScript 代码段为「典型实现示意」,反映业界通用做法,并非逐行源码,引用时请勿当作官方实现。

05 / 常见误区与工程建议

  1. 「工具越多越聪明」是错觉:每个工具 schema 都会占上下文、干扰选择,tools 只放当前会话真正可能用到的。Skill 同理,按需加载优于全量注入。
  1. description 就是工具的广告牌:模型选错工具,八成是描述写得模糊。
  1. 本地注册表是安全边界:只执行白名单内函数,参数先做 schema 校验,防注入、防越权。
  1. Skill 按开放标准写(最小 frontmatter:name + description),避免厂商锁定,未来在 Claude Code、Cherry Studio 等客户端间迁移成本低。
  1. Function calling 无状态:别指望模型「记住」你的工具,schema 每次请求都要带。

06 / 结语

Function calling 把「模型出主意」和「代码动手」 cleanly 地分开了:LLM 负责理解意图、决定调什么、传什么参数;Agent 负责真正执行、管理上下文、组织多轮对话。Skill 与 Tool 是同一套思想的两个侧面——前者把「经验」文本化注入,后者把「能力」结构化注册。理解了这条主线,无论是自己写一个最小 Agent,还是用好 Cherry Studio 这样的开源客户端,心里都有一张清晰的地图。