1. 从 Function Call 到 Skill:为什么零散工具调用总在重复造轮子
如果你正在做 Agent 开发,大概率经历过这个阶段:一开始用 Function Call 把几个工具函数塞进请求体,模型能调通,感觉挺爽。但工具一多,问题就来了——每个项目都要重新写一遍工具描述、参数 Schema、异常处理,换个模型还得改格式。后来 MCP 出来了,工具终于能标准化注册和发现,可新的痛点又冒出来:MCP 解决的是“工具怎么被调用”,但没解决“一个复杂任务该怎么按步骤用好这些工具”。
举个具体例子。你要做一个“分析销售 CSV 并输出报告”的任务。Function Call 模式下,你得手动把read_csv、describe、groupby这些函数一个个写进 tools 数组,模型每次调用都要重新理解你的业务规则。MCP 模式下,工具通过 Server 标准化暴露,模型能发现它们,但“先看数据概况、再按列统计、最后生成报告”这套 SOP 仍然散落在你的 prompt 里,换个任务就得重写。
Skill 模块化封装要解决的就是这个问题:把“领域知识 + 操作规范 + 专用工具”打包成一个独立目录,像插件一样注册、按需加载、跨项目复用。而 TaoToken 在这里的角色是统一接入层——不管你底层走 MCP 还是 Function Call,Key 和 API 通道统一走 TaoToken,省去多套凭证管理的麻烦。
这篇文章会带你从零封装一个 DataAnalyst Skill,给出可复制的config.toml和settings.json骨架,并完成一次完整的注册与调用验证。适合已经用过 Function Call 或 MCP、想把零散调用沉淀为可迁移模块的开发者。
2. TaoToken 前置:统一 Key 与 API 通道的接入层配置
在动手写 Skill 之前,先把接入层理清楚。Agent 开发里最烦的事情之一,就是不同工具、不同模型、不同协议各有一套 Key 和 endpoint。Function Call 走一个通道,MCP Server 走另一个通道,调试的时候光切换配置就够呛。
TaoToken 的思路是提供一个统一的 API 通道,你只需要维护一份 Key,模型对话、工具调用、MCP 桥接都走同一个入口。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
实际操作上,你需要先拿到 API Key。登录后进入控制台,在 API Keys 页面创建一个新 Key,建议按项目命名,比如skill-agent-dev,方便后续排查。创建完成后复制保存,后面配置文件里要用。
这里有个细节值得注意:TaoToken 的 API 通道同时兼容 OpenAI 风格的 Function Call 请求格式和 MCP 的标准化调用。也就是说,你的 Skill 内部工具既可以走 Function Call 的tools参数,也可以走 MCP 的 Server 注册,底层都通过同一个 Key 鉴权。这对模块化封装很关键——Skill 的工具实现不用关心上层用哪种协议,统一走 TaoToken 就行。
如果你还没创建 Key,可以先去控制台操作:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完 Key 后,建议在项目根目录建一个.env文件存放,不要硬编码到代码里。
3. 可复制配置:config.toml 与 settings.json 骨架
接下来是本文的核心交付物。我会给出两份配置文件:config.toml用于定义 Skill 的元信息和工具注册,settings.json用于 Agent 运行时的接入参数。你可以直接复制到项目里改。
先看config.toml。这份配置放在项目根目录,负责声明 Skill 的加载路径、TaoToken 接入信息、以及 MCP 与 Function Call 的协议开关。
# config.toml - Skill 模块化封装配置骨架 [taotoken] api_base = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不要硬编码 default_model = "gpt-4o" timeout_seconds = 60 [skill] root_dir = "skills" registry_file = "skills/registry.json" progressive_loading = true # 渐进式加载:先读 meta,激活后再读 instruction 和 tools [skill.protocol] # 工具调用协议:可选 "function_call" 或 "mcp" # function_call 适合轻量单文件工具,mcp 适合需要独立进程的复杂工具 tool_protocol = "function_call" mcp_server_command = "python -m mcp_server" mcp_server_port = 8765 [skill.loader] auto_generate_schema = true # 自动从函数签名生成 tools schema instruction_max_tokens = 2000 # instruction 注入上限,防止撑爆上下文这份配置的关键点在于progressive_loading和tool_protocol。渐进式加载让系统启动时只读meta.json,只有 Skill 被匹配选中后才加载instruction.md和tools.py,这样你可以注册几十个 Skill 而不占用额外上下文。tool_protocol则决定你的工具走 Function Call 还是 MCP,切换时只需要改这一行。
再看settings.json。这份配置放在 Agent 运行时目录,负责定义会话级参数和 Skill 路由规则。
{ "agent": { "name": "skill-agent", "version": "1.0.0", "taotoken": { "api_base": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY" }, "skill_routing": { "enabled": true, "match_strategy": "keyword_first", "fallback_skill": null, "max_active_skills": 3 }, "context": { "instruction_inject_mode": "on_activate", "tools_schema_inject_mode": "on_activate", "release_after_execution": true } }, "skills": [ { "name": "DataAnalyst", "path": "skills/DataAnalyst", "enabled": true, "intent_keywords": ["数据分析", "CSV 分析", "数据预览", "数据统计"] } ] }settings.json里的skill_routing控制意图匹配策略,context控制上下文注入时机。release_after_execution设为 true 后,Skill 执行完毕会释放注入的 instruction 和 tools schema,进一步节省上下文。
两份配置配合使用:config.toml管全局接入和协议,settings.json管会话和路由。你可以把config.toml提交到仓库,settings.json按环境区分。
4. Skill 注册与调用验证:一次完整动作
配置就绪后,我们来完成一次完整的 Skill 注册与调用。目标是让 Agent 识别“分析这份销售数据”这个意图,自动加载 DataAnalyst Skill,调用工具读取 CSV,并返回数据概况。
4.1 创建 Skill 目录与三个核心文件
在skills/DataAnalyst/下创建三个文件。先是meta.json,负责快速索引:
{ "name": "DataAnalyst", "description": "数据分析师技能,读取、预览、分析 CSV 数据", "intent_keywords": ["数据分析", "CSV 分析", "数据预览", "数据统计"], "version": "1.0.0", "dependencies": ["pandas"] }然后是instruction.md,这是专家级 SOP,只在 Skill 激活时注入:
你是一名严谨的数据分析师,严格遵循以下流程: 1. 始终优先调用 inspect_csv 查看数据概况,不允许跳过; 2. 确认列名和结构后,再选择统计工具; 3. 遇到空值或格式错误,必须在报告中注明; 4. 输出自然语言报告,包含核心结论和关键数据。最后是tools.py,实现具体工具函数:
import pandas as pd def inspect_csv(file_path: str, n_rows: int = 3) -> str: """读取 CSV 表头和前 n 行,用于快速查看数据概况。 Args: file_path: CSV 文件路径 n_rows: 预览行数,默认 3 Returns: 包含列名和数据预览的字符串 """ try: df = pd.read_csv(file_path) cols = f"数据列名:{list(df.columns)}\n" preview = f"数据预览(前{n_rows}行):\n{df.head(n_rows).to_string(index=False)}" return cols + preview except FileNotFoundError: return f"错误:未找到文件 {file_path},请检查路径。" except Exception as e: return f"读取失败:{str(e)}"4.2 编写 SkillLoader 并注册
SkillLoader 负责动态加载 Skill 资源并自动生成 tools schema。核心逻辑是:读 meta → 读 instruction → 动态导入 tools 模块 → 用inspect提取函数签名生成 schema。
import json import importlib.util import os import inspect class SkillLoader: def __init__(self, skill_name: str, root_dir: str = "skills"): self.skill_dir = os.path.join(root_dir, skill_name) if not os.path.exists(self.skill_dir): raise FileNotFoundError(f"未找到 Skill:{skill_name}") self.meta = self._load_json("meta.json") self.instruction = self._load_text("instruction.md") self.tools_module = self._load_tools() self.tools_schema = self._generate_schema() def _load_json(self, filename): with open(os.path.join(self.skill_dir, filename), "r", encoding="utf-8") as f: return json.load(f) def _load_text(self, filename): with open(os.path.join(self.skill_dir, filename), "r", encoding="utf-8") as f: return f.read() def _load_tools(self): tools_path = os.path.join(self.skill_dir, "tools.py") spec = importlib.util.spec_from_file_location( f"skills.{self.meta['name']}.tools", tools_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _generate_schema(self): schema = [] for name, func in vars(self.tools_module).items(): if callable(func) and not name.startswith("_"): sig = inspect.signature(func) params = {} for pname, param in sig.parameters.items(): ptype = param.annotation.__name__ if param.annotation != inspect.Parameter.empty else "string" params[pname] = {"type": ptype, "description": f"{pname} 参数"} schema.append({ "type": "function", "function": { "name": name, "description": (func.__doc__ or "").strip().split("\n")[0], "parameters": { "type": "object", "properties": params, "required": [p for p, v in sig.parameters.items() if v.default == inspect.Parameter.empty] } } }) return schema def get_context(self): return {"instruction": self.instruction, "tools_schema": self.tools_schema}注册动作很简单:实例化SkillLoader("DataAnalyst"),调用get_context()拿到 instruction 和 tools schema,然后注入到 TaoToken 的请求里。
4.3 发起调用验证
用 TaoToken 的 API 通道发起一次请求,把 Skill 的 tools schema 放进tools参数,instruction 放进 system message。请求体大致如下:
import os import requests loader = SkillLoader("DataAnalyst") ctx = loader.get_context() payload = { "model": "gpt-4o", "messages": [ {"role": "system", "content": ctx["instruction"]}, {"role": "user", "content": "帮我看看 sales.csv 的数据概况"} ], "tools": ctx["tools_schema"], "tool_choice": "auto" } resp = requests.post( "https://taotoken.net/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json=payload, timeout=60 ) print(resp.json())预期结果是模型返回一个tool_calls,调用inspect_csv,参数file_path为sales.csv。你执行工具函数后把结果回传,模型会基于数据概况生成自然语言报告。整个链路走通,说明 Skill 注册和调用验证成功。
5. 本篇常见错排查:MCP 与 Function Call 的调用差异踩坑
实际接入时,MCP 和 Function Call 的差异会导致几类高频报错。我整理成对照表,方便你快速定位。
| 现象 | Function Call 原因 | MCP 原因 | 处理方式 |
|---|---|---|---|
| 模型不调用工具 | tools schema 格式不对,缺type: function | Server 未启动或端口不通 | 检查 schema 结构;确认 MCP Server 进程和端口 |
| 参数类型报错 | 注解缺失导致 schema 推断为 string | MCP 工具入参未按 JSON Schema 声明 | 给函数参数加类型注解;MCP 侧补全 inputSchema |
| 工具调用后无响应 | 未回传 tool 结果或 role 写错 | MCP 返回格式未按协议封装 | 确认回传 role 为tool;检查 MCP 响应结构 |
| 上下文超限 | instruction 和 tools 全量注入 | 所有 Skill 的 tools 一次性注册 | 开启渐进式加载,按需注入 |
| 鉴权失败 | Key 未从环境变量读取 | MCP Server 未透传 TaoToken Key | 统一走TAOTOKEN_API_KEY环境变量 |
几个关键差异值得展开说。Function Call 是“请求内联”模式,tools schema 跟着每次请求走,好处是简单,坏处是工具多了请求体膨胀。MCP 是“独立进程”模式,工具通过 Server 暴露,Agent 通过协议发现和调用,好处是解耦,坏处是多了一层进程管理和端口配置。
另一个容易踩的坑是参数类型推断。Function Call 下如果你不写类型注解,inspect拿不到类型,生成的 schema 里参数类型会变成string,模型传数字时就会报错。MCP 下则需要在 Server 端显式声明inputSchema,否则调用方无法正确序列化参数。
还有一个隐蔽问题:TaoToken 的 Key 在 MCP 场景下需要透传给 Server 进程。如果你用python -m mcp_server启动,记得在启动脚本里读取TAOTOKEN_API_KEY环境变量,否则 Server 侧调用模型时会鉴权失败。
6. 把 Skill 沉淀为可迁移模块:下一步怎么做
走到这里,你已经完成了一个 Skill 从配置、封装、注册到调用验证的完整闭环。回头看,Skill 模块化封装的核心价值不在于“多了一个抽象层”,而在于把“领域知识 + 操作规范 + 专用工具”变成了可复制、可迁移的目录包。新增一个 Skill,你只需要复制目录、改三个文件,不用动主程序。
如果你想把这条链路继续用起来,几个方向可以参考。一是把 Skill 注册到长期编码或 Agent 工作流里,配合 Coding Plan 做持续迭代,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。二是如果你还在验证模型和工具调用的兼容性,可以先用模型对话页面快速试,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。三是接入文档里有更完整的协议说明和示例,遇到配置细节可以对照查:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
最后留一个实操建议:先把config.toml里的tool_protocol固定为function_call跑通全链路,再切到mcp对比差异。这样出问题时你能快速判断是 Skill 封装的问题还是协议切换的问题。等你手上有三五个 Skill 能稳定复用,再考虑 Sub-Agent 协同的事,那时候模块化的价值会真正显现出来。