返回博客列表
技术文章AgentPythonFunction CallMCPSkillLLMAI

Function Call、MCP 和 Skill——Python Agent 工具调用的三层阶梯

2026年06月🇨🇳 中文

这是 AI 开发入门系列 的第 6 篇,讲 Function Call、MCP 与 Skill 在 Python Agent 中的三层工具能力。相关文章还有:

  1. 四种主流的 Agent 架构
  2. function call 与 tool call 的区别
  3. 用 Python 从零写一个 LLM Agent
  4. RAG 是怎么工作的
  5. Prompt、Context、Harness——三种 Engineering 到底有什么不同

一、你写的 Agent 需要"手脚"

第一篇讲了 Agent 是什么:LLM 只能接收文本、输出文本。它不能查天气、不能读文件、不能发请求——它的整个世界就是 prompt 进来、token 出去。

Agent 做的事就是在 LLM 外面包一层循环,给它装上手脚:

思考 → 行动 → 观察 → 思考 → 行动 → ... → 结束

在这个循环里,"行动"就是要调工具。但工具怎么来、怎么连、怎么管,有三种不同层次的组织方式。这篇文章就是要把这三层讲清楚。

先给一个总览。想想你是怎么解决吃饭问题的:

  • Function Call自己做饭——买菜、切菜、炒菜、洗碗,全你一个人干。自由度高,但费劲。
  • MCP叫外卖——别人做好你只管点。不用关心厨房在哪、菜怎么切,但吃什么依赖菜单上有。
  • Skill雇了个厨师——他自带食谱和流程,你说"今天想吃川菜",他知道该用哪些食材、按什么顺序下锅。

映射到 Agent 开发里就是:Function Call 让你能调用单个工具,MCP 让你能发现和连接外部工具,Skill 让你能封装一套完整的任务能力。从下到上,从原子到组合。

这篇文章先逐层拆开讲,再用同一个需求(管理 GitHub Issues)分别用三种方式写一遍,让你直观看到代码量的差异。

二、Function Call:Agent 的原子工具调用

Function Call 是 Agent 工具调用的最底层。LLM 输出的不是一句话,而是一个结构化的 JSON,告诉你的代码"我想调某个工具,参数是这些"。你的代码负责执行,把结果拼回消息列表,继续循环。

用 Python 写的最简例子:

from openai import OpenAI
import json

client = OpenAI()

# 工具注册表:工具名 → (执行函数, JSON Schema)
TOOL_MAP = {
    "get_weather": (
        lambda city: json.dumps({"city": city, "temp": 26, "condition": "晴"}),
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查询指定城市的天气",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "城市名"}
                    },
                    "required": ["city"]
                }
            }
        }
    )
}

def run_agent(user_query):
    # 收集所有工具的 Schema
    tool_schemas = [schema for _, (_, schema) in TOOL_MAP.items()]
    messages = [{"role": "user", "content": user_query}]

    for _ in range(10):
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tool_schemas
        )
        msg = resp.choices[0].message

        # 没有 tool_calls → LLM 直接回答了
        if not msg.tool_calls:
            return msg.content

        # 把 LLM 的回复加入历史
        messages.append(msg.model_dump())

        # 逐个执行工具调用
        for tc in msg.tool_calls:
            func_name = tc.function.name
            func_args = json.loads(tc.function.arguments)

            if func_name not in TOOL_MAP:
                result = f"工具 {func_name} 不存在"
            else:
                try:
                    fn, _ = TOOL_MAP[func_name]
                    result = fn(**func_args)
                except Exception as e:
                    result = f"工具执行出错: {e}"

            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": str(result)
            })

    return "超过最大轮次"

这个模式的几个特点:

所有东西都是你手写的。 工具 Schema 你手写(JSON dict),执行函数你手写(上面那个 lambda),错误处理你手写,结果格式化你也手写。Agent 代码和工具代码在同一个文件里,强耦合。

这在小场景下完全没问题。 如果你的 Agent 只需要两三个工具,逻辑也不需要跨项目共享,这么写是最直接的——没有框架、没有抽象层,出问题一眼能看到哪里报错。

但工具一多就麻烦了。 加一个新工具意味着你要:手写一个执行函数、手写一份 JSON Schema、往 TOOL_MAP 里注册。换一个 LLM Provider(比如 OpenAI 换 Anthropic),Schema 格式不一样,全部要重写。五个项目都要用"查天气",这个函数你要复制粘贴五遍。

Function Call 就像是你在自己盖一间平房——砖是亲手搬的,墙是亲手砌的。住进去很舒服,因为每块砖你都认识。但如果有一天你要盖一栋楼,还用手搬砖就太慢了。

三、MCP:让 Agent 通过标准协议发现和调用外部工具

MCP(Model Context Protocol)是 Anthropic 提出的一套开放协议。它的核心思路很简单:把工具的定义和执行从 Agent 代码里抽出来,变成一个独立的服务。 Agent 通过标准协议跟这个服务通信,自动发现它提供了哪些工具、自动调用。

没用过 MCP 的话,先理解两个角色:

  • MCP Server:工具提供方。一个独立的进程,对外暴露一组工具。Server 的作者负责写工具的实现和描述。
  • MCP Client:工具消费方。你的 Agent 代码里引入一个客户端,通过 JSON-RPC 跟 Server 通信。通信方式通常是 stdio(本地进程)或 SSE(远程 HTTP)。

你的 Agent 作为 MCP Client,做的事非常简单:连接 → 获取工具列表 → 把工具注册进 Agent 循环 → 触发调用时通过 Client 发给 Server 执行。

用 Python 写一个 MCP 客户端接入 Agent 循环,大概长这样:

import asyncio
import json
from mcp import ClientSession
from mcp.client.stdio import stdio_client
from openai import OpenAI

client = OpenAI()

async def run_mcp_agent(user_query, mcp_server_command):
    # 启动 MCP Server(本地命令行方式)
    async with stdio_client(mcp_server_command) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 自动发现 Server 提供的所有工具
            mcp_tools = await session.list_tools()
            print(f"发现 {len(mcp_tools.tools)} 个工具: {[t.name for t in mcp_tools.tools]}")

            # 把 MCP 工具的 Schema 转成 OpenAI 格式
            openai_tools = []
            for t in mcp_tools.tools:
                openai_tools.append({
                    "type": "function",
                    "function": {
                        "name": t.name,
                        "description": t.description or "",
                        "parameters": t.inputSchema
                    }
                })

            # Agent 循环(和 Function Call 的循环逻辑一样)
            messages = [{"role": "user", "content": user_query}]

            for _ in range(10):
                resp = client.chat.completions.create(
                    model="gpt-4o",
                    messages=messages,
                    tools=openai_tools
                )
                msg = resp.choices[0].message

                if not msg.tool_calls:
                    return msg.content

                messages.append(msg.model_dump())

                for tc in msg.tool_calls:
                    func_name = tc.function.name
                    func_args = json.loads(tc.function.arguments)

                    # 通过 MCP Client 执行工具,而不是自己写函数
                    result = await session.call_tool(func_name, func_args)

                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": str(result.content)
                    })

            return "超过最大轮次"

对比 Function Call 的方式,变化只有几个地方:

  1. 你不需要手写任何一个工具的执行函数。get_weather 的实现?Server 作者写的。你的代码里只有 session.call_tool() 这一行。
  2. 你不需要手写工具 Schema。list_tools() 从 Server 那里自动拉回来,遍历一下直接注册。
  3. 加一个新工具,你代码一行不用改。Server 那边更新了,你的 Agent 下次启动自动拿到新工具。

MCP 的最大价值不在"能调工具",而在"工具的提供和消费解耦了"。

假设你写了一个查天气的 MCP Server,发布到了社区。别人写 Agent 时只需要配置一行启动命令:

mcp_server_command = ["python", "weather-server.py"]

他的 Agent 就自动获得了"查天气"能力,不需要知道你用什么 API、怎么解析返回数据。反过来,如果你觉得这个 MCP Server 不好用,换另一个——Agent 代码同样一行不用改。

社区目前已经有几百个现成的 MCP Server:GitHub、Slack、PostgreSQL、文件系统、浏览器自动化、各种数据库。你的 Agent 想接入这些能力,只需要多启一个 Server 进程。

但 MCP 不是银弹。 它只管"工具怎么连接",不管"什么时候调、调完之后怎么办、用什么策略组合多个工具"。这些仍然需要你的 Agent 循环和 prompt 来决策。MCP 就像一个标准化的插座——插上就有电,但用它来灯还是冰箱,是你的电路决定的。

四、Skill:为 Agent 封装一套"知道怎么做事"的任务包

从你每天都在用的 Skill 说起

如果你用 AI 编程工具写代码(Claude Code、Trae、Cursor、Codex 都行),你其实天天在用 Skill,只是可能没意识到。

比如在 Trae 里有一个 Supabase 相关的 Skill。你用 Trae 打开一个 Supabase 项目后,这个 Skill 自动加载了:它告诉 AI 你的项目用的是 Supabase、表结构是什么、认证方式是 JWT。AI 在帮你写数据库查询时,不需要每次都猜你用哪个框架、表名是什么。

再比如前端设计 Skill——它告诉 AI 你的设计规范是什么颜色、用什么组件库、圆角是多少。AI 生成 UI 代码的时候,风格就是一致的,不会有时 Material 有时 Apple。

这些 Skill 的结构拆开来就是三样东西:

  • System Prompt:告诉 AI"你是什么角色、你会什么、你的边界在哪"
  • 上下文注入:项目的规则、代码规范、表结构、设计 token 等
  • 工具列表:Skill 可以用的 MCP 工具、Shell 命令等

一句话总结:AI 编程工具里的 Skill,就是在合适的时机给 AI 装上合适的能力和背景知识。

自己写 Agent 时,你会有同样的需求

好,现在你要自己用 Python 写 Agent 了。写着写着你就会撞上一个问题:

你的 Agent 慢慢变复杂了。先加了查天气、又加了搜网页、然后是操作数据库、然后是读文件、然后是发邮件……工具列表从 3 个涨到了 20 个。

但你发现,"查天气"的任务只需要用 1 个工具,"做代码审查"的任务需要 5 个特定的工具配合,"回答技术问题"又需要另外 3 个。你如果把 20 个工具全部塞进每次 LLM 调用——先不说 Token 成本,LLM 在 20 个工具里选对的那一个,准确率会明显下降。

你要的就是按任务场景裁剪能力集。这就是 Skill 要做的事。

用 Python 写一个 Skill

Skill 在 Python Agent 里不是什么特殊框架概念,就是一个组织模式。你可以用一个简单的类来承载:

class Skill:
    def __init__(self, name, description, tools, system_prompt, rules=None):
        self.name = name
        self.description = description
        self.tools = tools            # 这个 Skill 要用到的工具列表
        self.system_prompt = system_prompt  # 注入到 LLM 上下文的提示
        self.rules = rules or []      # 执行规则/约束

以一个代码审查 Skill 为例:

code_review_skill = Skill(
    name="code_review",
    description="对代码做审查,检查语法、风格、安全问题",
    tools=["read_file", "run_lint", "run_test", "grep_search"],
    system_prompt="""你是一个代码审查专家。当用户让你 review 代码时:

1. 先用 read_file 读取文件内容
2. 用 run_lint 检查语法和风格问题
3. 用 grep_search 查找常见安全模式(eval、exec、硬编码密钥等)
4. 如果有测试文件,用 run_test 跑一下测试

审查时要关注:安全性、可读性、性能、错误处理。""",
    rules=[
        "先读文件再下结论",
        "不要修改代码,只给出建议",
        "如果 lint 报错超过 10 个,先让用户处理 lint 问题"
    ]
)

在 Agent 启动时,根据任务类型加载对应的 Skill:

def load_skill_for_task(task, skills):
    # 简单匹配:根据任务描述选择 Skill
    for skill in skills:
        if skill.name in task:
            return skill
    return None

def run_agent_with_skill(user_query, skill):
    messages = [
        {"role": "system", "content": skill.system_prompt},
        {"role": "user", "content": user_query}
    ]

    # 只注册这个 Skill 需要的工具,不是全部
    active_tools = [t for t in ALL_TOOL_SCHEMAS if t["function"]["name"] in skill.tools]

    # 后续的 Agent 循环和之前一样...

你看,Skill 和你用 AI 编程工具时看到的那些 Skill 本质上是一个东西:

AI 编程工具里的 SkillPython Agent 里的 Skill
说明文档(告诉 AI 怎么用)system_prompt
可用工具(MCP、Shell)tools 列表
执行规则/约束rules 列表

Skill 和 MCP 的关系

这两个词经常一起出现,但它们管的事情完全不一样。

MCP 管的是"工具怎么连"。你写了一个查天气的 MCP Server,Agent 怎么发现它、怎么调用它、错误怎么回传——这是 MCP 的范围。

Skill 管的是"工具怎么组合"。查天气只是其中一个工具,Skill 还管"什么时候该查天气、查到结果怎么处理、用户问了什么该用哪个工具组合"。这是 Skill 的范围。

一个好的 Skill 内部完全可以同时包含:两个手写的 Function Call 工具 + 三个 MCP Server 提供的工具。MCP 给 Skill 输送弹药,Skill 决定在什么场景下怎么开火。

五、三者对比总结

先把三者的角色定位用一张表说清楚:

维度Function CallMCPSkill
角色定位单个工具调用的执行机制工具与 Agent 之间的标准连接协议任务能力的封装模块
工具定义谁提供你在 Agent 代码里手写MCP Server 作者提供你在 Skill 里组合
执行逻辑谁写你手写MCP Server 作者写复用已有,加描述和规则
新加一个工具写执行函数 + Schema ~20 行配置一行启动命令写 Skill 描述 ~15 行
跨项目复用复制粘贴函数代码启动同一个 MCP Server加载同一个 Skill 模块
与 Agent 循环的关系循环内的执行环节循环内的通信层循环启动前的能力注入

再看层次关系。这三层不是互斥的,是包含的:

Agent 循环
┌─────────────────────────────┐
│  Skill(任务编排层)          │  ← 注入 Prompt + 选择工具子集
│  ┌───────────────────────┐  │
│  │  MCP(工具连接层)      │  │  ← 标准化协议发现/执行工具
│  │  ┌─────────────────┐  │  │
│  │  │ Function Call    │  │  │  ← 单次工具调用的原子操作
│  │  └─────────────────┘  │  │
│  └───────────────────────┘  │
└─────────────────────────────┘

最底层永远是 Function Call——不管你的工具是通过 MCP 连的还是手动写的,最终 Agent 循环里执行的都是 tool_call 这个动作。MCP 改变了工具的连接方式,但没有改变调用的本质。Skill 改变了工具的加载和组合方式,但底层仍然依赖 Function Call 或 MCP 来实际执行。

六、在 Python Agent 中如何选择和组合

只用 Function Call 就够的场景:工具数量少(≤3 个),逻辑比较简单,不需要跨项目共享。比如一个天气查询 Agent、一个计算器 Agent、一个简单的文档问答。

# 3 个工具以内,手写最直接
TOOL_MAP = {
    "get_weather": (get_weather, get_weather_schema),
    "get_time": (get_time, get_time_schema),
}

该引入 MCP 了:需要接入外部数据源或第三方服务(GitHub、Slack、数据库),社区已经有现成的 MCP Server,你不想把 API 调用的细节写在 Agent 代码里。比如一个运维 Agent,需要连 GitHub 查 issue 状态、连 Slack 发通知、连数据库查部署记录。

# 3 个 MCP Server,Agent 代码只负责循环
servers = [
    ["npx", "github-mcp-server"],
    ["npx", "slack-mcp-server"],
    ["npx", "postgres-mcp-server"],
]

该引入 Skill 了:Agent 承担多种不同类型的任务——查资料的、写代码的、做审查的、管项目的。每种任务的能力要求不一样,不能让 LLM 每次都面对 20 个工具做决策。把每种任务封装成独立的 Skill,按需加载。

三者完全可以共存。一个成熟的 Agent 通常长这样:

class MyAgent:
    def __init__(self):
        self.skills = {
            "code_review": CodeReviewSkill(),
            "github_manage": GitHubSkill(use_mcp=True),
            "doc_writer": DocWriterSkill(),
        }

    def run(self, user_input):
        skill = self.match_skill(user_input)
        # skill.tools 里面可能同时有手写的 Function Call 工具
        # 和通过 MCP Server 连接的远程工具
        return self.agent_loop(user_input, tools=skill.tools,
                               system_prompt=skill.system_prompt)

七、实战:一个"GitHub Issues 管理 Agent"的三个版本

用同一个需求写三个版本,你对比一下代码量和可读性。

需求:一个 Python Agent,能完成三件事——列出我的 open issues、创建一个新 issue、关闭指定 issue。

版本 A:纯 Function Call

每个工具手写 Schema + 手写执行函数。每个执行函数里要自己拼 HTTP 请求、管 token、处理错误。

import requests
from openai import OpenAI
import json

GITHUB_TOKEN = "ghp_xxx"
REPO = "user/repo"
HEADERS = {"Authorization": f"token {GITHUB_TOKEN}"}

def list_issues():
    r = requests.get(
        f"https://api.github.com/repos/{REPO}/issues?state=open",
        headers=HEADERS
    )
    return json.dumps([{"number": i["number"], "title": i["title"]}
                       for i in r.json()[:10]])

def create_issue(title, body=""):
    r = requests.post(
        f"https://api.github.com/repos/{REPO}/issues",
        headers=HEADERS,
        json={"title": title, "body": body}
    )
    data = r.json()
    return json.dumps({"number": data.get("number"), "url": data.get("html_url")})

def close_issue(number):
    r = requests.patch(
        f"https://api.github.com/repos/{REPO}/issues/{number}",
        headers=HEADERS,
        json={"state": "closed"}
    )
    return json.dumps({"number": number, "closed": r.status_code == 200})

# 手写三份 Schema
def make_tool_schema(name, desc, props, required):
    return {"type": "function", "function": {
        "name": name, "description": desc,
        "parameters": {"type": "object", "properties": props,
                       "required": required}
    }}

TOOL_MAP = {
    "list_issues":  (list_issues,  make_tool_schema(
        "list_issues", "列出仓库 open 状态的 Issues", {}, [])),
    "create_issue": (create_issue, make_tool_schema(
        "create_issue", "创建一个新 Issue",
        {"title": {"type": "string", "description": "标题"},
         "body":  {"type": "string", "description": "内容"}},
        ["title"])),
    "close_issue":  (close_issue,  make_tool_schema(
        "close_issue", "关闭指定 Issue",
        {"number": {"type": "integer", "description": "Issue 编号"}},
        ["number"])),
}

加上 Agent 循环,整个文件大概 60 行。能跑,但问题也很明显:GitHub API 的 URL、认证方式、返回格式全硬写在工具函数里。如果要支持 GitLab,这三个函数要全部重写。

版本 B:MCP 方式

用社区现成的 GitHub MCP Server。Agent 只负责连接 + 循环。

import asyncio
import json
from mcp import ClientSession
from mcp.client.stdio import stdio_client
from openai import OpenAI

client = OpenAI()

async def run_github_agent(user_query):
    # 启动 GitHub MCP Server(社区现成的)
    server_cmd = {
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-github"],
        "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"}
    }

    async with stdio_client(server_cmd) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 自动发现所有 GitHub 工具
            tools = await session.list_tools()
            print(f"拿到 {len(tools.tools)} 个 GitHub 工具")

            # 转成 OpenAI 格式
            openai_tools = [{"type": "function", "function": {
                "name": t.name, "description": t.description or "",
                "parameters": t.inputSchema
            }} for t in tools.tools]

            messages = [{"role": "user", "content": user_query}]

            for _ in range(10):
                resp = client.chat.completions.create(
                    model="gpt-4o", messages=messages, tools=openai_tools)
                msg = resp.choices[0].message
                if not msg.tool_calls:
                    return msg.content
                messages.append(msg.model_dump())
                for tc in msg.tool_calls:
                    args = json.loads(tc.function.arguments)
                    result = await session.call_tool(tc.function.name, args)
                    messages.append({"role": "tool", "tool_call_id": tc.id,
                                     "content": str(result.content)})
            return "max turns"

Agent 代码只有 30 行。关键是你没写任何 GitHub API 调用——list_issuescreate_issueclose_issue 这些函数是 MCP Server 自带的。而且你自动拿到了创建 PR、查看 PR diff、搜索仓库等超过 20 个 GitHub 工具,不只是你手动封装的 3 个。

版本 C:Skill 方式

把版本 B 的 MCP 工具包进一个 Skill,加上使用说明和规则:

github_issues_skill = Skill(
    name="github_issues",
    description="管理 GitHub Issues",
    tools=["github_list_issues", "github_create_issue", "github_close_issue",
           "github_add_comment"],
    system_prompt="""你是 GitHub Issues 管理助手。当用户提到 Issue 相关操作时:
1. 创建 Issue 前确认标题不为空
2. 关闭 Issue 前先列出当前 open 的 issues,让用户确认编号
3. 操作完成后总结做了什么""",
    rules=[
        "不要关闭用户没提到的 Issue",
        "创建 Issue 时 body 默认为空字符串",
        "如果用户说'我有哪些 issue',默认列出 open 状态的"
    ]
)

Agent 侧的调用代码更简洁:

async def run_skill_agent(user_query):
    skill = github_issues_skill

    # 连接 MCP Server,但只注册 Skill 里声明的工具
    async with stdio_client(mcp_server_cmd) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            all_tools = await session.list_tools()

            # 只取 Skill 需要的工具
            filtered = [t for t in all_tools.tools if t.name in skill.tools]

            messages = [
                {"role": "system", "content": skill.system_prompt},
                {"role": "user", "content": user_query}
            ]
            # ... 后续循环相同

三个版本总结

版本 A:Function Call版本 B:MCP版本 C:Skill
Agent 代码量~60 行~30 行Agent ~25 行 + Skill ~20 行
工具数量你手写的 3 个Server 提供的全部(20+)你在 Skill 里选的 4 个
加一个新工具写函数 + Schema ~20 行代码不改Skill 加一行工具名
换人维护得读懂 GitHub API 代码只需理解 MCP 连接读 Skill 描述就够了
适用场景快速原型,用完就丢需要完整 GitHub 能力交给别人用的长期 Agent

版本 B 适合你自己用的 Agent,功能全、改的少。版本 C 适合你写给别人用的 Agent——Skill 里写好了 System Prompt 和规则,别人不需要研究"这个 Agent 怎么用 GitHub",直接问就行。

八、结语

Function Call、MCP、Skill,三者不是在比谁"更好",而是在 Agent 的复杂程度增长时,你不自觉得一层层往上走。

  • 刚开始,工具少,Function Call 手写最直接。你写一个工具字典,Agent 调用,跑通了再说。
  • 工具多了,你不想每个都手写调 API,MCP 把你从胶水代码里解放出来。启动一个 Server,Agent 自动获得一批工具。
  • 场景复杂了,你不希望 Agent 每次都面对所有工具做选择,Skill 帮你按任务打包能力。不同任务加载不同的 Skill,Agent 只在当前任务的能力集里做决策。

做 Agent 开发,不在于你用了多少工具,而在于你的 Agent 在合适的场景下能加载到合适的能力。Function Call、MCP、Skill——这三层阶梯,就是给它装上合适能力的全部武器。