这是 AI 开发入门系列 的第 6 篇,讲 Function Call、MCP 与 Skill 在 Python Agent 中的三层工具能力。相关文章还有:
一、你写的 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 的方式,变化只有几个地方:
- 你不需要手写任何一个工具的执行函数。
get_weather的实现?Server 作者写的。你的代码里只有session.call_tool()这一行。 - 你不需要手写工具 Schema。
list_tools()从 Server 那里自动拉回来,遍历一下直接注册。 - 加一个新工具,你代码一行不用改。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 编程工具里的 Skill | Python 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 Call | MCP | Skill |
|---|---|---|---|
| 角色定位 | 单个工具调用的执行机制 | 工具与 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_issues、create_issue、close_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——这三层阶梯,就是给它装上合适能力的全部武器。