Genkit Go Skills 中间件实战:从 pirate 技能文件的 SKILL.md 格式到 use_skill 加载机制
【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit
本文以 Genkit Go 示例库中的 pirate 技能文件 为切入点,完整拆解 Genkit Go Skills 中间件的运作方式:一个技能文件(SKILL.md)应如何组织 YAML frontmatter 与正文指令、中间件如何扫描并解析这些文件、又如何通过注入系统提示词与注册use_skill工具,让模型在运行时按需加载"海盗语气"这类人格化指令。读完后你能直接写出自己的技能库并理解其底层加载链路。
一、pirate 技能文件:一个最小而完整的技能定义
Skills 中间件中的"技能"本质就是一个目录 + 一个 SKILL.md 文件。pirate 技能位于示例目录go/samples/basic-middleware/skills/skills/下,与haiku、shakespeare、eli5三个技能并列(见 main.go 头注释)。其完整内容如下:
--- name: pirate description: Respond in the voice of a swashbuckling pirate. --- Ahoy! From here on, ye be speakin' only like a proper swashbuckling pirate. Sprinkle in "aye", "arr", "matey", "ye", "'tis", and similar pirate idioms liberally. Refer to problems as "troubles at sea", to tasks as "quests", and to files or data as "treasure". Stay in character for the entire response. Keep it punchy and fun.这份文件由两部分构成,两部分对应中间件的两条处理路径:
- YAML frontmatter(
---围栏之间):包含name: pirate与description: Respond in the voice of a swashbuckling pirate.两个字段。描述文本会被写进注入给模型的系统提示词,是模型"看见"这个技能的主要依据——pirate 的 description 一句话点明用途(以海盗口吻回应),模型据此判断用户请求是否匹配。 - 正文指令:frontmatter 之后的 Markdown 文本,是技能的真正"载荷"。pirate 的正文给出了可执行的风格规范:全程使用 "aye"、"arr"、"matey"、"ye"、"'tis" 等海盗习语;把问题称为 "troubles at sea"(海上麻烦)、把任务称为 "quests"(任务)、把文件与数据称为 "treasure"(宝藏);并且要求整个回答保持人设、短促有趣。这段正文只有在模型调用
use_skill之后才会进入对话上下文,这正是 Skills 中间件"按需加载"设计的核心(见下文第四节)。
二、示例应用如何接入 pirate 技能
理解这个技能文件最直接的方式是看消费它的示例程序 basic-middleware/skills/main.go。该程序做了四件事:
注册 Middleware 插件。
genkit.Init时通过genkit.WithPlugins(&googlegenai.GoogleAI{}, &middleware.Middleware{})挂载内置中间件插件,插件在 plugin.go 中暴露了 Retry、Fallback、ToolApproval、Skills、Filesystem 五种中间件,其中 Skills 的注册描述为 "Expose a local library of skills as loadable system instructions"(将本地技能库暴露为可加载的系统指令)。声明技能库路径。
const skillsDir = "./skills"指向工作目录下的技能库(main.go#L62-L64)。路径相对于工作目录,go run .正好落在该目录,因此./skills/下的pirate/、haiku/等子目录都会被扫描到。定义 askFlow 流式流程。流程中通过
ai.WithUse(&middleware.Skills{SkillPaths: []string{skillsDir}})启用中间件,并用ai.WithSystem(...)明确提示模型"回答前先判断列出的技能是否匹配,匹配则先调用use_skill"(main.go#L99-L125)。默认问题指向 pirate。
AskRequest.Question字段的jsonschema默认值被设为"Explain how a rainbow forms in the voice of a pirate."——不带任何请求体的调用就会被导向 pirate 技能,返回 "Arr, matey!" 式的回答而不是普通段落(main.go#L66-L70)。源码注释还特意提醒:jsonschema标签以逗号分隔,默认值中不能包含逗号,否则值会被静默截断。
另外值得注意的是流式回调中的过滤逻辑:use_skill那一轮模型调用也会流经流式通道但不产生文本,示例用chunk.Text() != ""过滤空块,避免回答看起来"迟到"而非"空白"(main.go#L112-L119)。
三、SKILL.md 的格式规范:目录约定与 frontmatter 解析
以下格式约束并非文档约定,而是由中间件源码 skills.go 直接实现的扫描与解析行为决定的:
3.1 目录结构约定
- 每个技能是技能路径下的一个直接子目录,目录内必须含有名为
SKILL.md的文件;缺少该文件的子目录会被跳过。 - 扫描不递归:只枚举
skills/的一层子目录(scanSkills中仅对每个条目检查entry.IsDir()后拼接目录名/SKILL.md,见 skills.go#L168-L186)。 - 以
.开头的隐藏目录被忽略;技能名(调用use_skill时使用的名称)取目录名,而非 frontmatter 里的name字段——frontmatter 的name实际上不参与查找,写错也不会导致加载失败,但description会影响模型的选择判断。 - 未配置
SkillPaths时默认扫描"skills"目录(defaultSkillsPath,见 skills.go#L32-L33)。 - 路径不存在或不可读时会被跳过;如果该路径是调用方显式配置的,跳过会以
warn级别日志提示(很可能配错了),仅当是未配置时的默认路径则只打debug日志(skills.go#L148-L156)。
3.2 frontmatter 解析规则
parseFrontmatter(skills.go#L194-L212)实现了严格的围栏格式要求:
- 文件开头必须是
---,且其后必须紧跟换行符(\n或\r\n),允许文件带 BOM(会先剥离\ufeff); - 结束围栏是独立成行的
---,解析器在剩余文本中查找第一个\n---; - 围栏之间的文本按 YAML 解析,只提取
name与description两个字段; - 无 frontmatter 或解析失败时返回零值,不报错——此时技能仍会被暴露给模型,只是描述缺省。描述为空时会填充占位文本
"No description provided."(skillsMissingDescription),而该占位文本在系统提示词中会被省略,只列出技能名(skills.go#L229-L236)。
pirate 的 frontmatter 正是符合这一规范的标准写法:两行---之间恰好是name/description两个键值对。
四、运行时加载链路:从扫描到 use_skill 工具返回
中间件在每次ai.Generate调用时完成一次扫描,并把结果固化到返回的ai.Hooks中,保证WrapGenerate与use_skill工具看到同一份技能集合(skills.go#L87-L133)。整条链路分为三步:
4.1 注入 系统提示词
buildSkillsPrompt把扫描结果渲染为如下结构的系统提示词片段(以示例中四个技能为例,按字母序排列):
<skills> You have access to a library of skills that serve as specialized instructions/personas. Strongly prefer to use them when working on anything related to them. Only use them once to load the context. Here are the available skills: - eli5 - Explain concepts in very simple terms suitable for a five-year-old. - haiku - Respond as a single traditional haiku with a 5-7-5 syllable structure. - pirate - Respond in the voice of a swashbuckling pirate. - shakespeare - Respond in the style of William Shakespeare — early modern English, poetic cadence. </skills>可以看到 pirate 技能暴露给模型的正是其 description 一行。技能按名称排序,保证多次运行输出稳定。
injectSkillsPrompt(skills.go#L245-L285)负责把该片段放入请求的系统消息中,且是幂等的:每个注入的文本 Part 都带有Metadata["skills-instructions"] = true标记(skillsMarker),后续多轮工具循环重放历史时,中间件会原地刷新已存在的标记 Part 而不是重复追加;若历史里没有标记 Part,则追加到已有的 system 消息末尾,或在最前面新建一条 system 消息。这个设计保证了带use_skill工具循环的多轮对话中,<skills>块始终只出现一次。
若扫描结果为空(没有任何技能),wrapGenerate直接放行请求、不做任何注入(skills.go#L121-L127)。
4.2 注册 use_skill 工具
中间件同时注册名为use_skill的工具(useSkillToolName常量注明其命名刻意与 JS 版实现保持一致,便于提示词与评测在两种运行时之间移植)。工具接收单个参数skillName,执行逻辑非常直接:在扫描结果 map 中按名称查找,找不到返回"skill %q not found"错误;找到则os.ReadFile读取该技能的 SKILL.md完整原始内容(含 frontmatter 在内的全文)作为工具输出返回给模型(skills.go#L103-L119)。
由此形成完整的两阶段加载时序:
- 模型先看到
<skills>清单(只有名称 + 一行描述),这是"目录页",token 开销极小; - 判断请求匹配后调用
use_skill(skillName="pirate"),pirate 的 SKILL.md 全文作为工具响应进入对话历史; - 模型基于已加载的正文指令,以海盗口吻完成最终回答。
4.3 测试用例对行为的印证
skills_test.go 中的五个测试覆盖了上述全部行为,可视为格式与行为的"可执行规范":
TestSkillsInjectsSystemPrompt:断言系统提示词同时包含带描述的python - A python expert skill与无 frontmatter 时仅列名称的javascript;TestSkillsRegistersUseSkillTool:模拟模型首轮请求调用use_skill,断言工具响应中完整包含 SKILL.md 正文("Python prompt content");TestSkillsUnknownSkillReturnsError:未知技能名返回包含not found的错误;TestSkillsPromptInjectionIsIdempotent:重放历史后断言<skills>块在系统消息中仍然只有 1 个,验证"原地刷新而非重复注入";TestSkillsNoopWhenNoSkillsFound:空目录时不注入任何系统消息。
五、与其余三个示例技能对照
pirate 所在的技能库共有四个技能,风格各不相同,便于肉眼观察加载效果:
| 技能 | description(注入提示词) | 正文指令要点 |
|---|---|---|
| pirate | 以冒险海盗口吻回应 | 使用 "aye/arr/matey" 等习语;问题称 "troubles at sea"、任务称 "quests"、文件数据称 "treasure";全程保持人设 |
| haiku | 以 5-7-5 音节的传统俳句回应 | 每次只输出俳句三行,无前言、标题或解释 |
| shakespeare | 以莎士比亚风格回应(早期现代英语) | 使用 thou/thee/hast 等词汇,鼓励抑扬格节奏,称读者为 "gentle reader" |
| eli5 | 用五岁儿童能懂的语言解释概念 | 短句子、生活化类比、避免术语、全文不超过十句 |
对比可见 pirate 技能的写法特点:它的正文不仅定义语气,还给出领域词汇映射规则(问题→troubles at sea 等),这类"替换规则"比单纯说"说海盗话"更能约束模型输出的一致性。
六、动手运行与 HTTP 调用
在示例目录下运行(工作目录必须是go/samples/basic-middleware/skills/,因为skillsDir = "./skills"是相对路径):
go run .服务监听127.0.0.1:8080,所有 flow 自动挂载为POST /<flow名>。要触发 pirate 技能,可以不传任何 body(默认问题就是海盗口吻问彩虹):
curl -N -X POST 'http://localhost:8080/askFlow?stream=true' \ -H "Content-Type: application/json" \ -d '{"data": {"question": "Write a haiku about debugging code."}}'把question换成"Explain recursion to me like I am five."则会加载 eli5 技能。若要观察use_skill调用本身,可用 Genkit CLI 启动 Dev UI 并在http://localhost:4000/traces查看每次运行的 trace(启动方式为genkit start -- go run .,CLI 一次性安装即可)。
七、编写自己的 SKILL.md:要点清单
综合源码行为与 pirate 示例,一份健壮的 SKILL.md 应满足:
- 目录名即技能名:把技能放在
<skillsDir>/<技能名>/SKILL.md,目录名小写、无空格,这是模型调用use_skill时使用的唯一标识; - frontmatter 围栏严格:首行
---后必须紧跟换行,结束---独立成行,键值只写name与description; - description 面向选择:一句话写清"何时该用这个技能",它会原样出现在
<skills>清单中,是模型决策的唯一线索; - 正文面向执行:写可验证的行为规则(词汇表、结构约束、长度上限、输出格式),正文全文会在
use_skill后被逐字读入对话,写多少模型就遵守多少; - 控制正文长度:正文只在该技能被加载时才进入上下文,但这正是"按需加载"的意义所在——把重指令留在 SKILL.md 里,而不是全部塞进常驻系统提示词。
小结
pirate 技能文件虽然只有十行,却完整展示了 Genkit Go Skills 中间件的设计:frontmatter 的description构成低成本"目录页",正文指令通过use_skill工具在模型需要时按需载入对话;扫描、解析、幂等注入与工具注册均由 skills.go 实现并经 skills_test.go 全量验证。掌握这一文件约定后,你可以为任何模型流程挂上可组合、可版本化的"人格/风格库",而不必修改流程代码本身。
【免费下载链接】genkitOpen-source framework for building agentic apps in JavaScript, Go, Dart, and Python, built and used in production by Google项目地址: https://gitcode.com/GitHub_Trending/ge/genkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考