从源码拆解Agent Skills与Function Calling,底层实现、核心差异与实操指南
2026年3月31日,Claude Code的源码意外泄露,51.2万行TypeScript代码被公开,这让一直以来蒙着神秘面纱的Agent Skills底层实现终于浮出水面。在此之前,很多开发者对Skills的认知都停留在“高级工具”的层面,认为它和Function Calling只是“封装程度不同”的同类功能。但当我深入研读源码中SkillTool.ts、Tool.ts等核心文件后发现,两者的底层逻辑有着本质区别,Function Calling是Agent的“手脚”,负责执行具体的原子操作;而Skills是Agent的“大脑手册”,负责编排流程、注入指令,甚至改变Agent的执行上下文。
本文将从源码层面拆解Agent Skills的实现逻辑,对比它与Function Calling的异同,结合实际案例讲解两者的协作模式,同时给出自定义Skill的实操建议,帮助开发者真正理解Agent架构的核心设计,摆脱“只懂API用法,不懂底层原理”的局限。
一、先搞懂基础:Function Calling在源码里到底长什么样
要理解Skills与Function Calling的差异,首先要明确Function Calling的底层实现逻辑。在Claude Code泄露的源码中,Function Calling的核心是“工具注册-模型调用-执行管线”的闭环,每一步都有严格的定义和约束,本质是“模型发指令、系统做执行”的标准化流程。
1.1 工具的核心定义:Tool接口与构建流程
源码中,所有工具都必须实现Tool.ts文件第362行定义的Tool接口,这个接口规定了工具的核心字段,缺一不可。我们可以通过一段简化的TypeScript代码,直观看到接口的结构:
// Tool.ts:362 核心接口定义interfaceTool{name:string;// 工具名称,唯一标识description:string;// 自然语言描述,供模型理解功能inputSchema:Zod.Schema;// Zod schema,用于校验输入参数call:(params:any,context:ExecutionContext)=>Promise<ToolResult>;// 执行函数,核心业务逻辑checkPermissions?:(params:any,context:ExecutionContext)=>Promise<boolean>;// 权限检查(可选)}从接口定义可以看出,Function Calling的工具具有极强的结构化特征:输入有明确的校验规则,执行有固定的函数逻辑,权限有可选的检查机制。这种设计保证了工具的可靠性和安全性,避免模型调用时出现参数错误或越权操作。
工具的构建过程通过buildTool()函数完成,这个函数会合并工具的默认配置和用户自定义配置,然后通过toolToAPISchema()函数,将工具序列化为Claude API能识别的JSON Schema格式,最终塞进请求中发给模型。也就是说,模型看到的工具,本质上是一段包含“名称-描述-参数规则”的结构化数据,它只需要决定“是否调用这个工具”以及“传入什么参数”,无需关心工具的底层实现。
1.2 工具执行管线:七步闭环,严格可控
当模型决定调用某个工具时,会输出一个tool_use block,系统拿到这个block后,会进入services/tools/toolExecution.ts第599行的runToolUse()函数,执行一套严格的七步管线,确保工具执行的规范性和可追溯性。这七步流程如下,每一步都不可或缺:
Zod验证:通过inputSchema校验模型传入的参数,确保参数格式、类型符合要求,避免无效参数导致执行失败;
自定义校验:执行用户自定义的校验逻辑,比如参数的范围限制、业务规则校验等;
PreToolUse hooks:执行工具调用前的钩子函数,比如记录日志、初始化执行上下文等;
权限检查:调用checkPermissions()方法,验证当前上下文是否有权限执行该工具;
实际执行:调用工具的call()方法,执行核心业务逻辑,比如调用Bash命令、读取文件等;
结果映射:将call()方法的执行结果,转换为模型能理解的格式(tool_result);
PostToolUse hooks:执行工具调用后的钩子函数,比如更新上下文、上报执行状态等。
整个流程是一个干净的“请求-响应”循环:模型发送工具调用请求,系统按管线执行,执行结果塞回对话历史,模型再根据结果继续推理。这种闭环设计,让Function Calling的每一步都可监控、可调试,适合执行单步、原子化的操作。
1.3 工具注册的小细节:去重逻辑与优先级
源码中还有一个容易被忽略的细节:工具注册通过tools.ts里的assembleToolPool()函数完成,它会合并内置工具和MCP工具,按名称去重,且内置工具优先级高于自定义工具。也就是说,如果你在MCP服务器上定义了一个和内置工具同名的工具(比如都叫“bash”),系统会优先使用内置工具,自定义工具会被忽略。
这个去重逻辑看似简单,却能避免工具冲突,保证Agent执行的稳定性。比如内置的bash工具经过了严格的安全校验,而自定义工具可能存在漏洞,优先使用内置工具能降低安全风险。
二、Skills的真面目:不是高级工具,是提示词注入器
在研读源码之前,我一直默认Skills是“封装了多个工具的高级工具”,但types/command.ts第25行的类型定义,直接打破了这个认知。源码中,Skill的类型是PromptCommand,它的type字段是“prompt”,而不是“tool”——这意味着,Skills本质上不是工具,而是一个“披着工具外衣的提示词生成器”。
2.1 Skill的核心定义:没有执行逻辑,只有提示词模板
与Function Calling的Tool接口不同,Skill没有call()方法,没有inputSchema,也没有结构化的输入输出定义。它的核心是getPromptForCommand()方法,这个方法会读取SKILL.md文件,返回展开后的Markdown内容。我们可以通过一段简化的代码,看Skill的核心结构:
// types/command.ts:25 Skill类型定义typeSkill=PromptCommand&{type:'prompt';// 关键标识,区别于tool类型getPromptForCommand:(args:string[],context:ExecutionContext)=>Promise<string>;// 生成提示词metadata?:SkillMetadata;// 元数据,如路径、描述等};从这段定义可以看出,Skill的核心功能只有一个:生成提示词。它不执行任何业务逻辑,也不直接操作工具,而是通过一段预定义的Markdown模板,告诉模型“该做什么、怎么做”。而SKILL.md文件,就是这个提示词模板的载体,里面可以包含动态变量、内联命令、流程描述等内容。
2.2 桥梁SkillTool:用Function Calling的壳,包提示词注入的核
Skills本身不能被模型直接调用,它需要通过一个叫SkillTool的桥梁,才能接入Function Calling的执行体系。SkillTool定义在tools/SkillTool/SkillTool.ts中,它本身是一个标准的Tool接口实现,拥有完整的Zod schema、call()方法和权限检查逻辑,享有Function Calling的全部基础设施(输入验证、权限检查、hooks、遥测等)。
但SkillTool的call()方法,并没有执行任何业务逻辑,它的核心作用是“上下文注入”。具体来说,当模型调用SkillTool时,传入的参数是“skill名称”和“可选参数”,SkillTool会通过getPromptForCommand()方法,读取对应SKILL.md文件的内容,展开动态变量和内联命令,然后将展开后的内容包装成一条UserMessage,注入到模型的对话上下文的中。
这是一个教科书级的适配器模式:用Function Calling的标准化壳,包裹了提示词注入的核心逻辑。通过这种设计,Skills既能利用Function Calling的基础设施保证安全性和可靠性,又能发挥提示词的灵活性,实现复杂的流程编排。
2.3 Skills的两种执行模式:Inline与Fork,适配不同场景
源码中,Skills有两种执行模式,分别对应不同的使用场景,这也是Skills与Function Calling的核心区别之一。两种模式的逻辑的都定义在SkillTool.ts第580-841行,我们逐一拆解:
2.3.1 Inline模式:注入剧本,让模型自己演(默认模式)
Inline模式是Skills的默认执行模式,也是最能体现其设计哲学的模式。这种模式的核心是“将提示词注入主对话上下文,让模型基于注入的指令,自主调用工具完成流程”。整个流程分为三个关键步骤,环环相扣:
第一步,内容展开。getPromptForCommand()方法读取SKILL.md文件,将里面的动态内容全部替换掉。SKILL.md中可以包含两种动态内容:一是内联Shell命令,比如!git status``,展开时会通过promptShellExecution.ts实际执行这条命令,将输出结果塞回原位;二是变量替换,支持$ARGUMENTS(完整参数)、0(索引参数)和0(索引参数)和0(索引参数)和foo(命名参数,来自YAML frontmatter的arguments字段)。
举个例子,如果SKILL.md中有这样一段内容:“当前仓库状态:!git status,请检查 staged changes,生成符合Conventional Commits规范的commit message”,展开后会变成:“当前仓库状态:On branch main\nYour branch is up to date with ‘origin/main’.\nChanges to be committed:\n (use “git restore --staged …” to unstage)\n\tmodified: README.md,请检查 staged changes,生成符合Conventional Commits规范的commit message”。
这一步其实和我们在CI/CD管线里用模板引擎渲染配置文件,没有本质区别,核心都是“动态替换占位符,生成可执行的指令”。
第二步,消息注入。展开后的Markdown内容,会被包装成一条UserMessage,并且带上isMeta: true标记。这个标记的作用很关键:用户在终端UI里看不到这条消息,但模型在下一轮推理时,能完整“看到”它。说白了,这就相当于有人悄悄在你和Claude的对话中间,插入了一段Claude能看见但你看不见的“指令书”。
第三步,上下文修改。SkillTool返回的结果中,会带有一个contextModifier函数,这个函数可以修改后续的执行上下文,比如注入allowedTools白名单(限制模型只能用特定工具)、覆盖模型(比如指定用Claude Opus)、覆盖effort level(执行强度)。这意味着,一个Skill不仅能告诉模型“做什么”,还能改变模型“能用什么工具”和“用哪个版本的自己”。
更强大的是,Skill甚至可以在调用时注册hooks。比如一个代码格式化Skill,可以附带一条规则:“每次调用写文件工具后,自动跑prettier”。这已经不只是注入提示词了,而是在注入“行为拦截器”,让模型的后续操作都遵循Skill定义的规则。
2.3.2 Fork模式:开一个分身,只拿结果回来
Fork模式与Inline模式完全不同,它的核心是“启动子Agent,在独立上下文里完成任务,只返回最终结果”。当SKILL.md的YAML frontmatter里写了context: fork时,SkillTool会调用runAgent()函数(和Claude Code的多Agent协调系统共享基础设施),启动一个子Agent,然后把展开后的Skill内容扔给子Agent。
子Agent会在独立的token预算和执行上下文里,完整执行Skill定义的流程,比如分析文件、生成报告、执行一系列工具调用等。整个执行过程,对主对话完全透明,主对话最终只拿到一个纯文本结果,没有newMessages,没有contextModifier,也看不到子Agent的任何执行步骤。
这种模式非常适合“只需要结果,不需要过程”的场景,比如“帮我分析这个日志文件,找出错误原因并给出解决方案”。用Fork模式,主对话不会被子Agent的执行过程干扰,拿到的结果也更简洁。
三、实例拆解:一个/commit指令,看清两者的协作关系
理论讲得再多,不如一个实际案例来得直观。我们以Claude Code中最常用的/commit指令为例,拆解它的完整执行轨迹,看看Skills和Function Calling是如何协作的,理解两者的核心分工。
当你在Claude Code的终端里敲下/commit,整个执行过程分为四个步骤,环环相扣,清晰地体现了“Skill编排流程、Function Calling执行步骤”的协作模式:
步骤1:模型调用SkillTool,触发Skill执行
你输入/commit后,模型会首先调用SkillTool,传入的参数是{ skill: “commit”, args: “” }。此时,SkillTool作为桥梁,会读取commit对应的SKILL.md文件,这个文件的核心内容(简化版)如下:
--- name: commit description: 生成符合Conventional Commits规范的commit message,并执行commit操作 arguments: [] --- # Commit流程 1. 执行`git status`,查看当前仓库状态,确认staged changes; 2. 执行`git diff --staged`,查看暂存文件的修改内容; 3. 根据修改内容,生成符合Conventional Commits规范的commit message(格式:类型(范围): 描述); 4. 执行`git commit -m "commit message"`,完成提交。 当前仓库状态:!`git status`步骤2:Skill展开内容,注入主对话上下文
SkillTool调用getPromptForCommand()方法,展开SKILL.md中的动态内容:执行!git status``命令,将输出结果替换到对应位置,然后将展开后的内容,包装成一条带有isMeta: true的UserMessage,注入到主对话上下文。
此时,模型能“看到”这条注入的消息,里面包含了完整的commit流程和当前仓库状态,相当于拿到了一份“行动手册”。
步骤3:模型根据Skill指令,依次调用Function Calling工具
模型读取注入的指令后,开始按流程执行,每一步都调用对应的Function Calling工具:
调用Bash工具,执行命令
git status,确认暂存的修改内容(这是Function Calling的原子操作);调用Bash工具,执行命令
git diff --staged,查看具体的修改细节(再次调用Function Calling);根据前两步的执行结果,生成符合规范的commit message,比如
feat(utils): 新增日期格式化函数;调用Bash工具,执行命令
git commit -m "feat(utils): 新增日期格式化函数",完成提交(最终的原子操作)。
步骤4:执行完成,返回结果
所有Function Calling工具执行完成后,结果会被塞回主对话历史,模型再将最终的提交结果(比如“提交成功,commit hash: xxxxxxx”)返回给用户。整个过程中,Skill负责“定义流程、提供上下文”,Function Calling负责“执行每一步具体操作”,两者分工明确,缺一不可。
一句话总结两者的协作关系:Skill是导演手里的剧本,规定了“该怎么演”;Function Calling是演员的动作,负责“具体怎么干”。导演不亲自上台表演,演员不自己写剧本,两者配合才能完成一场完整的“演出”。
四、深度对比:Skills与Function Calling的异同点
通过前面的源码拆解和实例分析,我们已经能清晰区分Skills和Function Calling的核心差异。下面我们从“底层本质、执行逻辑、使用场景”等多个维度,做一次全面的对比,帮大家彻底理清两者的关系,避免混淆。
4.1 核心相同点:共享Function Calling的基础设施
虽然两者的核心逻辑不同,但Skills依赖Function Calling的基础设施才能运行,这是它们最核心的相同点:
都遵循Tool接口的规范(Skills通过SkillTool间接遵循),都能被模型识别和调用;
都享有输入验证、权限检查、hooks、遥测等基础设施,保证执行的安全性和可追溯性;
最终都服务于Agent的任务执行,都是Agent能力的延伸,缺一不可。
4.2 核心差异点:从本质到使用场景的全面区别
两者的差异贯穿“本质、结构、执行、场景”四个层面,具体如下表所示(清晰对比,一目了然):
| 对比维度 | Function Calling | Agent Skills |
|---|---|---|
| 底层本质 | 结构化工具,负责执行原子操作 | 提示词生成器,负责编排流程、注入指令 |
| 核心结构 | 有call()方法、inputSchema,结构化输入输出 | 无call()方法、无inputSchema,核心是Markdown模板 |
| 执行逻辑 | 模型调用→七步管线执行→返回tool_result→塞回对话 | 模型调用SkillTool→展开模板→注入上下文→模型自主调用工具 |
| 执行模式 | 只有单一步骤执行,无多模式 | 两种模式:Inline(注入主上下文)、Fork(启动子Agent) |
| 上下文影响 | 只返回执行结果,不改变主上下文 | 可通过contextModifier修改主上下文(限制工具、切换模型等) |
| 使用场景 | 单步、原子化操作(读文件、跑命令、调用API等) | 多步、流程化任务(代码审查、部署流程、commit规范检查等) |
| 开发门槛 | 需要写代码,实现Tool接口、call()方法和输入校验 | 无需写代码,只需编写SKILL.md模板,非程序员也能上手 |
| 安全风险 | 风险较低,有严格的参数校验和权限控制 | 风险较高,第三方Skill可能藏恶意指令(prompt injection) |
4.3 关键补充:不要混淆“Skill发现”与“工具注册”
源码中还有一个容易混淆的点:Skills的“发现机制”与Function Calling的“工具注册”是两个完全独立的逻辑。Function Calling的工具注册是“一次性加载、全局可用”,而Skills的发现机制是“动态加载、按需可见”,这也是Skills的一大设计亮点。
首先,Skills有严格的上下文预算控制。在tools/SkillTool/prompt.ts中,定义了两个核心常量:
exportconstSKILL_BUDGET_CONTEXT_PERCENT=0.01;// 上下文窗口的1%exportconstMAX_LISTING_DESC_CHARS=250;// 每条Skill描述最多250字符formatCommandsWithinBudget()函数会根据这个预算,控制Skill列表在prompt中的呈现方式:先尝试完整描述,超预算就截断(bundled skills不截断),再超就只保留名称。这确保了你往~/.claude/skills/里扔再多Skill,也不会把模型的上下文窗口撑爆。
其次,Skills有动态可见性设计。在SKILL.md的YAML frontmatter里,可以通过paths字段指定Skill的适用范围,比如paths: ["src/**/*.ts", "tests/**"]。只有当模型操作了匹配路径的文件(比如编辑src目录下的TypeScript文件),这个Skill才会出现在可用列表中;如果模型在编辑Python文件,这个Skill就会被隐藏,不会干扰模型的决策。
这种动态可见性设计,让Skills变得更加“智能”,避免了大量无关Skill的干扰,也让模型的决策更加高效。而Function Calling的工具,一旦注册成功,就会全局可用,没有这种动态适配的能力。
五、实操指南:怎么写自定义Skill,避坑又高效
理解了Skills的底层实现和与Function Calling的差异后,最实用的问题就是:如何编写自定义Skill?结合源码中的设计逻辑和实际开发经验,我总结了一套实操指南,帮大家避坑,同时最大化发挥Skills的优势。
5.1 先明确:什么时候用Skill,什么时候用Function Calling
很多开发者的误区是“滥用Skill”,明明一个简单的原子操作,非要写一个Skill,反而增加了复杂度。正确的选择逻辑很简单,记住一句话:单步用Function Calling,多步用Skill。
优先用Function Calling的场景:单步原子操作,比如读文件(readFile)、写文件(writeFile)、跑Bash命令(bash)、调用第三方API(比如调用天气API)。这些操作不需要流程编排,直接调用工具就能完成,用Function Calling更高效、更安全。
优先用Skill的场景:多步流程化任务,比如代码审查(先读文件、再检查语法、再给出修改建议)、部署流程(先拉取代码、再构建、再部署)、commit规范检查(先查状态、再生成message、再提交)。这些任务需要编排多个工具调用,用Skill来定义流程,能让模型自主完成,减少人工干预。
5.2 自定义Skill的核心步骤:编写SKILL.md模板
自定义Skill的核心,就是编写SKILL.md文件,它由“YAML frontmatter”和“Markdown指令内容”两部分组成,无需写任何代码,非程序员也能轻松上手。下面我们以“TypeScript代码格式化”为例,一步步讲解如何编写。
第一步:编写YAML frontmatter(元数据)
YAML frontmatter位于SKILL.md的开头,用—包裹,定义Skill的基本信息和配置,核心字段如下:
---name:ts-format# Skill名称,唯一标识,调用时使用description:格式化TypeScript文件,遵循prettier规范,自动修复语法错误arguments:# 可选参数,支持命名参数-name:file# 参数名称description:需要格式化的TypeScript文件路径required:true# 是否必填context:inline# 执行模式,inline或fork,默认inlinepaths:["src/**/*.ts"]# 动态可见性,只在操作TS文件时显示---这里有几个关键注意点:
name字段必须唯一,不能和其他Skill或内置工具重名,否则会被覆盖;
context字段根据场景选择:需要影响主上下文(比如限制工具)用inline,只需要结果用fork;
paths字段尽量具体,避免Skill在无关场景下显示,干扰模型决策。
第二步:编写Markdown指令内容
这部分是Skill的核心,用来定义模型的行动流程,支持动态变量、内联命令和流程描述。我们继续完善上面的ts-format Skill:
# TypeScript代码格式化流程 1. 检查传入的文件路径 $file 是否存在,确保是TypeScript文件(后缀为.ts或.tsx); 2. 执行`prettier --check $file`,检查文件格式是否符合规范; 3. 如果存在格式错误,执行`prettier --write $file`,自动修复格式; 4. 执行`eslint $file --fix`,修复常见的语法错误; 5. 输出格式化结果,告知用户文件是否已修复。 当前文件信息:!`ls -l $file`这段指令中,我们用到了两个核心特性:
变量替换:$file是我们在frontmatter中定义的参数,模型会自动将用户传入的文件路径替换进去;
内联命令:!
ls -l $file会执行ls命令,查看文件信息,将结果注入到指令中,让模型了解文件的基本情况。
第三步:测试与优化Skill
编写完成后,将SKILL.md文件放到~/.claude/skills/目录下(用户级Skill),然后在Claude Code中调用/ts-format file=src/utils.ts,测试Skill的执行效果。测试过程中,重点关注两个点:
指令展开是否正确:内联命令是否执行,变量是否替换成功;
模型是否按流程执行:是否依次调用prettier、eslint工具,执行结果是否符合预期。
如果模型没有按预期执行,大概率是指令描述不够清晰,此时可以优化Markdown内容,比如增加更具体的步骤说明,明确每个步骤需要调用的工具。
5.3 避坑指南:自定义Skill的3个关键注意事项
结合源码中的安全设计和实际踩坑经验,有3个关键注意事项,一定要牢记,避免出现安全问题或执行失败:
注意事项1:警惕第三方Skill的安全风险
源码分析显示,Skills本质上是“全文当作指令执行”,没有“数据 vs 指令”的边界。这意味着,第三方Skill的SKILL.md文件中,很容易藏恶意指令(比如注入恶意Bash命令),比普通的prompt injection更危险。
因此,从Skills Marketplace下载别人写的Skill时,一定要逐行查看SKILL.md文件和它引用的所有脚本,确认没有恶意指令后,再放到自己的Skill目录中。
注意事项2:避免过度依赖Fork模式
Fork模式虽然简洁(只返回结果),但它启动子Agent会消耗更多的token,而且执行过程不透明,不利于调试。如果不是“只需要结果”的场景,尽量用Inline模式,既能看到执行过程,又能灵活修改上下文。
注意事项3:合理控制Skill的复杂度
Skill的优势是“流程编排”,但不要把它写成“万能脚本”。如果一个Skill的流程过于复杂(比如包含十几个步骤,调用十几个工具),会增加模型的推理负担,也容易出现执行偏差。此时可以将复杂流程拆分成多个小Skill,逐一调用,更高效、更易维护。
六、延伸思考:从源码看Agent架构的通用模式——MetaTool Pattern
深入研读Claude Code的Skill系统后,我发现它实现了一种在其他Agent框架中很少见的设计模式,我暂且将其称为“MetaTool Pattern”(元工具模式)。这种模式的核心思路,不仅适用于Claude Code,也能为我们自己搭建Agent系统提供重要的参考。
6.1 MetaTool Pattern的核心逻辑
MetaTool Pattern的核心,是定义一个“元工具”(比如Claude Code中的SkillTool),这个元工具的参数是“子工具名称”(也就是Skill名称),它不执行业务逻辑,而是根据名称查找预定义的提示词模板(SKILL.md),展开模板,将展开后的内容注入到模型的上下文中,让模型基于这些注入的内容,自主推理和行动。
这个模式的优势非常明显,完美平衡了“安全性”和“灵活性”:
可扩展性极强:给Agent添加新能力,只需要添加一个Markdown文件,无需修改核心代码;
非程序员友好:不写代码就能定义复杂工作流,降低Agent的使用门槛;
上下文驱动:模型拥有完整的决策自主权,Skill给的是“建议”而不是“强制指令”,更符合Agent的智能特性;
组合性好:Skill可以调用其他Skill,可以限制可用工具集,可以切换模型版本,灵活组合各种能力。
6.2 为什么这个模式能成为Agent生态的通用标准
2025年10月,Anthropic首次推出Skills功能,12月就将其发布为开放标准,之后OpenAI Codex、Cursor、VS Code等工具陆续跟进支持同一格式。Skills能快速成为跨平台的开放标准,核心原因就是MetaTool Pattern的设计足够简单、通用、安全。
简单:Skill的载体是Markdown + YAML,格式简单,易于编写和解析,任何能处理提示词的Agent都能支持;
通用:不依赖特定的模型或框架,无论是Claude、GPT还是其他大模型,只要能处理工具调用和提示词,就能使用Skills;
安全:通过allowedTools白名单、权限检查等机制,约束模型的工具调用范围,降低安全风险;
解耦:将“能力定义”(Skill的Markdown模板)和“能力执行”(Function Calling的工具)解耦,让Agent的架构更灵活、更易维护。
6.3 如何从零实现这套模式
对于正在搭建Agent系统的开发者来说,MetaTool Pattern非常值得借鉴。如果你想从零实现这套机制,不需要复杂的代码,Victor Dibia的实现教程给出了一个80行Python的最小实现,核心思路如下(简化版):
importyamlimportsubprocessfromtypingimportDict,List# 1. 定义Skill类,读取SKILL.md并展开内容classSkill:def__init__(self,skill_path:str):withopen(skill_path,'r')asf:# 分离YAML frontmatter和Markdown内容content=f.read().split('---',2)self.metadata=yaml.safe_load(content[1])self.prompt_template=content[2]defexpand_prompt(self,args:Dict[str,str])->str:# 替换变量prompt=self.prompt_templateforkey,valueinargs.items():prompt=prompt.replace(f"${key}",value)# 执行内联命令importre command_pattern=r'!`(.*?)`'matches=re.findall(command_pattern,prompt)forcmdinmatches:result=subprocess.check_output(cmd,shell=True,text=True)prompt=prompt.replace(f"!`{cmd}`",result)returnprompt# 2. 定义MetaTool(对应SkillTool)classSkillTool:def__init__(self,skill_dir:str="~/.claude/skills"):self.skills=self.load_skills(skill_dir)defload_skills(self,skill_dir:str)->Dict[str,Skill]:# 加载目录下所有SKILL.md文件importos skill_dir=os.path.expanduser(skill_dir)skills={}forfilenameinos.listdir(skill_dir):iffilename.endswith(".md"):skill_name=filename[:-3]skill=Skill(os.path.join(skill_dir,filename))skills[skill_name]=skillreturnskillsdefcall(self,skill_name:str,args:Dict[str,str])->Dict[str,str]:# 调用Skill,展开提示词,返回注入信息ifskill_namenotinself.skills:raiseValueError(f"Skill{skill_name}not found")skill=self.skills[skill_name]prompt=skill.expand_prompt(args)return{"newMessages":[{"role":"user","content":prompt,"isMeta":True}],"contextModifier":None}# 3. 使用示例if__name__=="__main__":skill_tool=SkillTool()result=skill_tool.call("ts-format",{"file":"src/utils.ts"})print(result["newMessages"][0]["content"])这段代码虽然简单,但已经实现了MetaTool Pattern的核心逻辑:加载Skill、展开提示词、注入上下文。基于这个最小实现,你可以逐步添加权限检查、hooks、动态可见性等功能,搭建自己的Skill系统。
七、总结:Function Calling与Skills,缺一不可的Agent双核心
Claude Code的源码泄露,让我们终于看清了Agent Skills的底层真面目——它不是高级工具,而是一个聪明的提示词注入器,通过MetaTool Pattern,将提示词的灵活性与Function Calling的规范性完美结合。
Function Calling给了Agent“动手”的能力,让它能执行具体的原子操作,是Agent的“手脚”;而Skills给了Agent“思考”的框架,让它能编排复杂流程、自主决策,是Agent的“大脑手册”。两者相辅相成,缺一不可,共同构成了Agent的核心能力体系。
对于开发者来说,理解两者的底层实现和差异,不仅能帮助我们更好地使用现有工具,还能为我们搭建自己的Agent系统提供思路。无论是编写自定义Skill,还是设计Agent架构,记住一句话:用Function Calling做执行,用Skills做编排,用MetaTool Pattern做衔接,才能打造出灵活、安全、高效的AI Agent。
