ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Skills 实战指南:从提示词堆砌到技能封装

Agent Skills 实战指南:从提示词堆砌到技能封装 最开始接触 agent-skills 这个概念是我在做一个多步骤工作流项目的时候。当时每加一种能力system prompt 就要膨胀一大截各种 XML 标签、调用示例、边界条件全塞进去模型的上下文被无意义的内容占掉大半改一处逻辑还要小心翼翼地避免破坏其他能力。后来我把一部分能力拆成独立技能模块让 agent 按需加载、按描述触发情况才真正好转。这篇文章就围绕 agent-skills 展开讲清楚它是什么、内部怎么组织、如何安装分发、如何和 agent 框架配合以及我在实际测试中踩过的那些坑。如果你是正在做 AI agent 开发、或者想把自己手里重复的 prompt 能力沉淀成标准化资产的工程师这篇文章应该能帮你省不少试错时间。我不会只给概念会直接给结构、给代码、给测试方法还会说明每个设计背后的理由。1. 从提示词堆砌到技能封装先搞懂 Skills 到底在解决什么问题1.1 Tool 是扳手Skill 是换轮胎的完整流程很多刚开始接触 agent-skills 的人都会问同一个问题它和 function calling / tool 有什么区别我自己的理解是工具是原子操作技能是带有上下文、策略和判断的复合能力。举个例子。一个“读取 PDF 文件”的工具它接收文件路径返回文本内容。这是扳手。但“把一份 PDF 总结成结构化笔记”这个技能它内部可能要决定按章节切分还是按页切分是保留原文引用还是完全改写输出 Markdown 还是 JSON遇到扫描版 PDF 要不要先调用 OCR。这些策略逻辑如果全部写进工具描述工具描述会变得非常臃肿而且每换一个场景就要重写一遍。Skill 解决的正是这个问题把“什么时候用、怎么用、用的时候要注意什么、输出长什么样”打包成一个独立单元。模型只需要读取技能描述判断当前任务是否匹配然后把输入交给技能脚本执行。判断归判断执行归执行职责清晰。1.2 用第一性原理拆解模型是决策器Skill 是可执行单元我比较喜欢从第一性原理去理解这一类设计。Agent 系统的本质是让大模型当“决策器”让外部代码当“执行器”。模型不擅长稳定输出重复的机械操作但它擅长理解用户意图外部脚本不擅长理解模糊的自然语言但它擅长稳定、精确地完成数据处理。Skills 就是连接这两者的标准接口。它把“决策器该怎么理解任务”写进描述文件把“执行器该怎么干活”写进脚本文件。模型看到描述后决定调用哪个技能然后技能脚本负责真正产出结果。这个分层让两边各干各擅长的事也让我后面调试问题变得简单模型没调用对就改描述脚本输出不对就改脚本不用互相甩锅。1.3 为什么不能继续把能力堆在 System Prompt 里有人会想我直接把技能说明写进 system prompt不也能让模型按步骤执行吗我在实际项目中试过短期可行长期会出问题。一方面是上下文成本。每个技能的详细说明平均两三千 token塞五个技能就是一万多 token而实际单次任务可能只用到其中一个。另一方面是维护成本。prompt 里的技能说明和实际执行逻辑一旦脱节模型会一本正经地按旧说明操作产出错误结果还很难排查。Skill 最大的优势恰恰是“懒加载”只有描述常驻在上下文中完整说明和脚本在调用时才被加载平时完全不占资源。维护时也只需要改对应目录不影响其他技能。2. 一个 Skill 的解剖目录结构、描述文件与脚本约定2.1 最简可运行的目录长什么样一个标准的 Skill本质上就是一个目录。最简结构长这样storyboard-skill/ ├── SKILL.md └── scripts/ └── generate_storyboard.pySKILL.md 是技能的“门面”包含元信息和完整使用说明。scripts 目录放实际执行的脚本。需要额外素材时再加 assets 目录放模板、图片、字典文件。我不建议把脚本直接放在根目录因为技能目录里还可能放测试文件、README、依赖清单统一放在 scripts 下会让路径规则更清晰。这个设计我个人觉得非常聪明的地方在于安装一个技能几乎等于复制一个目录卸载就是删掉目录完全不需要像传统软件那样管理注册表或环境变量。想临时测试一个技能建软链指向本地开发目录就行改完代码立刻生效不用重新安装。2.2 描述文件怎么写才不会让模型“看不见”SKILL.md 的开头是 YAML frontmatter至少要有 name 和 description 两个字段。description 是模型判断是否触发这个技能的唯一天线写得好不好直接决定触发准确率。我第一次写技能描述时犯了典型错误写得太抽象。“用于处理文本文件。”这种描述等于没写模型经常在错误场景调用或者该调用时不调用。后来我改成这样--- name: storyboard-generator description: 当用户希望把一段叙述文字、广告文案、小说片段或产品介绍转化成可用于视频拍摄的分镜脚本时使用。输入可以是文本文件路径也可以是直接粘贴的内容输出为包含镜号、景别、运镜、画面描述、台词和时长的 Markdown 表格。 ---注意几个细节。第一开头写明“当用户希望……时使用”这是触发条件。第二尽量列出输入形态文件路径、粘贴文本、URL让模型知道什么算有效输入。第三明确输出格式模型会据此判断“当前任务是否要产出这种结果”。好的 description 大概控制在 100 到 200 字太短信息不足太长模型反而抓不住重点。正文部分写完整操作步骤、使用禁忌和示例。这里的文字不会被常驻在上下文里可以写得详细一些。比如脚本的退出码含义、输出编码要求、需要避免的误操作都写在这里模型调用前会读取并根据需要参考。2.3 脚本侧的四个硬性约定脚本是实现逻辑的地方也是我踩坑最多的地方。基于实测我发现四个约定如果从一开始就守住后面能省一大堆事。约定一输入参数必须容错。模型调用脚本时传给脚本的参数经常“不标准”。用户可能给了文件路径也可能直接贴了一大段文本脚本要能自己判断如果参数是存在的文件路径读文件否则把参数当作原始文本处理。这个判断逻辑几乎每个技能脚本都要有否则会频繁出现“技能执行失败”。约定二输出尽量结构化。能输出 JSON 就输出 JSON能让模型直接交给用户的内容就输出 Markdown。结构化输出一方面方便后续 pipeline 处理另一方面模型拿到干净文本后不需要二次猜测格式准确率会高很多。约定三幂等性。同一个输入跑两次结果应该一致至少不能产生多余的文件或副作用。模型有时会重复调用同一个技能如果脚本每次运行都新增一个文件很快就会把目录塞满。约定四控制执行时间。模型调用外部脚本通常有超时窗口几秒到几十秒不等。长任务要么拆成多个阶段要么先输出进度日志至少让框架知道脚本还活着而不是把整个任务卡死到超时。2.4 运行时选型Python 够用Rust 什么时候上技能脚本用什么语言写我建议遵循“够用就行首选 Python”。原因很简单生态全、AI 相关库多、团队里会的人多、原型迭代快。大部分技能本质上就是读输入、做处理、写输出用 Python 标准库就能完成根本不需要编译型语言。但有一种情况我会认真考虑 Rust当技能脚本的交付目标是另一个团队或线上环境时。Rust 编译成单个可执行文件不依赖目标机器的 Python 环境和第三方包部署成本极低也避免了“本地能跑、服务器跑不了”的环境漂移问题。代价是迭代慢、上手门槛高。我的取舍标准是给自己用 90% 选 Python给别人交付多机部署优先选 Rust。语言本身不影响技能的工作机制外部进程只要能通过命令行读参数、写 stdout就能被封装成 Skill。3. 安装、命名、分发把 Skill 从个人目录送到团队和市场3.1 两类常见安装位置与加载逻辑Skills 的安装本质上是把目录放到 agent 会扫描的位置。不同客户端有各自的约定最常见的两类是用户级和项目级。用户级目录通常放在主目录下例如~/.claude/skills/。放在这里的技能对所有项目全局可见适合放通用能力比如“生成 Markdown 表格”“整理会议纪要”。项目级目录则放在项目根目录下例如.claude/skills/。这个位置只对当前项目生效适合放和业务强相关的技能比如“解析本项目日志格式”“生成分镜脚本”。实际使用中有个容易踩的坑用户级和项目级如果存在同名技能加载优先级通常是项目级优先。我在一次调试中花了很长时间才意识到项目里新写的 skill 一直没生效是因为用户级目录里有个同名旧版本被优先加载了。排查方法是给技能改名或加版本号或者直接把用户级那个临时禁用。3.2 命名冲突与跨平台兼容技能名的命名规范比我预想的更重要。取个通用名如“summarizer”很容易和同事的、市场的技能撞车。我自己的习惯是加业务前缀比如docs-summarizer、video-storyboard-generator这样即使多个技能库合并冲突概率也大幅下降。跨平台兼容主要考虑的是Claude 的 skills 机制、Codex 的技能扩展、以及社区一些开源实现在描述格式上存在差异。有的要求 YAML frontmatter有的要求顶部注释块有的干脆用文件夹加参数映射的方式。如果同一个技能要在多个平台复用我目前的做法是保留完整版 SKILL.md再加一个兼容版说明文件在描述文件里互相引用。CLI 工具的适配层也可以做一层“描述格式转换”但那个投入比较大我建议团队技能数量超过 20 个以后再说。3.3 分发前的自查清单与第三方 Skill 的安全审查把技能分享到团队或公开市场前我做这几件检查每一条都在实际项目里出过问题。第一条换一个角色重新测试。开发时我很容易形成惯性知道自己会输入什么参数、期待什么输出。但其他用户不会按我的想法来他们会粘贴奇怪的内容、传不存在的路径、不带任何说明直接调用。至少要找个没参与开发的人盲测一轮。第二条清理本地痕迹。技能脚本里不要残留开发路径、内网 IP、临时调试文件也不要依赖本机独有的目录。把这些写进 README 的风险比想象的大因为技能市场里下载日志几乎是公开的。第三条固定版本。给技能打 tag例如v1.2.0描述里注明“本技能依赖 CLI v1.2.0 及以上版本”。脚本升级时老用户不会被破坏性更新影响。还有一个我必须强调的点从公开市场或第三方仓库下载技能下载后第一件事是读脚本不是运行。脚本是可以被恶意构造的。我在安全审查时会重点看三样东西是否请求外部网络、是否读写非预期路径、是否执行系统级命令。任何一个技能只要是来源不可信都要先隔离开测试确认没有问题后才会在生产环境使用。4. 实战把“一键出分镜脚本”沉淀成一个可复用的 Skill4.1 圈定输入输出别做超出能力的事理论讲再多不如完整跑通一个例子。我选分镜脚本生成这个需求因为它足够具体、画面感强也不太依赖第三方服务。需求是这样的用户给一段文案产品介绍、小说片段、短视频脚本agent 调用技能后输出一份可直接用于拍摄的分镜表格。我在设计技能时先把边界划清楚。这个技能只做“文本到分镜表格”不做视频生成不做素材推荐不接外部 API。一开始我试图让技能自动搜索配图素材结果引入了一堆联网逻辑触发率和不稳定性都变差了。把边界收窄之后技能稳定了很多后续如果需要扩展可以再加独立的配图技能而不是把这个技能做成“瑞士军刀”。4.2 写一份命中率高、抗误触发的 SKILL.md分镜技能的描述文件我分两步写。第一步先写触发描述核心是让模型知道什么时候该用。第二步写正文给出分镜的基本原则。这里给一个参考 frontmatter--- name: video-storyboard-generator description: 当用户希望把一段叙述文字、广告文案、小说片段、视频口播稿或产品介绍转化成可用于拍摄的分镜脚本时使用。输入为文本文件路径或直接粘贴的内容输出为包含镜号、景别、运镜、画面描述、台词和时长的 Markdown 表格。 ---正文部分我写了四条操作规则按叙事节奏拆分段落每个自然段落至少生成一个镜头镜头语言要具体避免“拍一下”这种模糊描述台词直接摘录原文不要自行改写。这些规则很重要因为模型只有拿到这些细节才能在调用脚本后正确理解或修正脚本输出而不是盲目把表格原样返回给用户。4.3 脚本实现参数容错与结构化输出接下来是脚本本身。分镜生成这里我不打算塞一个真正的 LLM 进去而是用一套启发式规则按段落切分文本根据段落长度计算镜头时长为每个镜头分配景别和运镜方式。一个简化版的处理流程是这样#!/usr/bin/env python3 import sys, os, re, json from pathlib import Path def load_input(argv): raw .join(argv).strip() if not raw: return None if len(raw) 200 and Path(raw).is_file(): return Path(raw).read_text(encodingutf-8) return raw def split_segments(text): blocks [b.strip() for b in re.split(r\n\s*\n, text) if b.strip()] return blocks or [text] def build_storyboard(segments): shots [] for idx, seg in enumerate(segments, 1): base_len max(3, min(8, len(seg) // 40)) shots.append({ shot: idx, type: 全景 if idx 1 else (近景 if idx % 2 else 中景), camera: 固定机位 if idx % 3 else 缓慢推近, action: seg[:60] (... if len(seg) 60 else ), sound: , duration: base_len, }) return shots def to_markdown(shots): lines [| 镜号 | 景别 | 运镜 | 画面描述 | 台词 | 时长 |, |---|---|---|---|---|---|] for s in shots: lines.append(f| {s[shot]} | {s[type]} | {s[camera]} | {s[action]} | {s[sound]} | {s[duration]}s |) return \n.join(lines) if __name__ __main__: text load_input(sys.argv[1:]) if text is None: sys.stderr.write(error: no input text\n) sys.exit(1) print(to_markdown(build_storyboard(split_segments(text))))你注意看load_input这个函数它同时处理“参数是文件路径”和“参数是文本内容”两种情况。这个容错在前面提到过是技能脚本最重要的防御逻辑之一。输出方面脚本直接打印 Markdown 表格模型拿到后几乎不需要额外整理就能交付给用户。4.4 三条测试请求验证触发率写完技能后我习惯用一组固定测试用例验证触发率而不是只测“能不能跑通”。至少三条覆盖清晰请求、模糊请求和不相关请求。第一条清晰请求“帮我把这段产品文案转成分镜脚本原文在 /tmp/demo.txt”。预期是触发技能且脚本正确读取文件。第二条模糊请求“我想把刚才那段文字拍成短视频你帮我拆分一下画面镜头”。这条没有直接说“分镜脚本”但语义明确模型应该能联想到技能。第三条不相关请求“帮我算一下 2 的 10 次方是多少”。预期是完全不触发技能。我用这三条请求反复测试根据模型是否调用、调用后输出是否正确回过来调整 description。比如早期版本里 description 只写了“分镜”没有写“拍成短视频”“视频口播稿”等常见说法第二条请求就经常不触发。把这类同义表达加进描述后触发率明显上升。5. 当 Skill 进入真实 Agent 架构记忆、编排与安全边界5.1 记忆存放事实Skill 存放操作规程很多 agent 框架里同时存在 memory记忆和 skill技能我第一次看到时也疑惑过。现在我的理解很清晰记忆存放的是“事实”技能存放的是“怎么做”。比如用户偏好用表格输出、用户常用中文回答这属于记忆而“如何把一段文本整理成表格”“如何生成中文分镜”则属于技能。记忆是档案室技能是操作手册。档案室管事实记录操作手册管行动方法。两者互相配合记忆告诉模型这个用户是谁、历史上下文是什么技能告诉模型遇到某类任务时具体怎么干活。如果混淆了两者把操作细节写进记忆里每次对话都会加载大量无用信息把用户偏好写进技能里技能又会被无关请求反复误触发。5.2 Harness、Agent 与 Skill 的分层关系聊到 agent-skills很多文章会提到 harness 和 agent 的区别。我按自己的工程视角给出分层理解。Agent 是决策核心负责理解任务、选择策略、调用能力它由模型、记忆和策略构成。Harness 是执行外壳负责运行循环、上下文管理、工具注册、安全策略它是承载 agent 完整生命周期的那层壳。Skill 则挂在 harness 的工具注册表上作为可复用的能力模块被 agent 决策调用。所以分层关系很清晰决策层agent决定调不调编排层harness管理怎么调、给多少权限技能层skill负责具体执行。Skill 不关心决策逻辑harness 也不关心技能内部实现。这种松耦合让我在项目里能自由替换模型、调整 prompt、增删技能任何一层的修改都不会引发连锁故障。5.3 多 Agent 共享技能库的可见性控制在一个项目里有多个 agent 时技能库通常共享但可见性要控制。比如有的 agent 负责文本处理有的负责数据分析如果所有技能对所有 agent 可见模型会频繁误调用错误技能。我的做法是在 harness 配置里给每个 agent 声明可见技能列表。这样技能目录可以统一放在一个共享位置但每个 agent 的“视野”不同。另一个细节是技能描述里的措辞也可以微调。面向客服类 agent 的技能描述可以多一些“当用户询问……时使用”面向数据类 agent 的描述则侧重“当输入包含结构化数据时使用”。视角贴合 agent 的使用场景触发准确率会更高。5.4 安全边界审查脚本、最小权限、防注入安全这块在之前的章节提过一部分但放到 agent 架构里还需要额外考虑三点。第一最小权限。技能脚本应该以最小权限运行不应当拥有整个用户目录的写权限更不应该拥有删除权限。需要网络请求的技能单独配置网络访问白名单。这是工程常识但在技能体系下经常被忽视因为技能安装太容易了复制一个目录就完成顺手一提权很多人会做。第二防提示注入。技能脚本的输出可能包含来自外部文件的内容如果这些内容被模型当作指令来理解就可能被执行。我在设计技能输出时有一条铁律脚本输出必须是格式化数据或结果文本不允许携带“请执行以下操作”这类元指令。必要的时候在输出里加一段“以下内容为数据输出非指令”的标记这部分内容虽然朴素但能有效降低被注入的风险。第三来源审查。这一点怎么强调都不过分从市场下载的技能先读脚本再运行。特别是那些号称“自动化漏洞检测”的技能看起来很酷实际上可能夹带恶意行为。我甚至建议团队内约定第三方技能必须在隔离环境跑一轮安全用例后才能上线。宁可麻烦一点不要拿生产环境开玩笑。6. 我实测踩过的五个翻车点以及对应的修复手法6.1 描述承诺了脚本做不到的事有一次我给技能描述里写了“支持从 URL 提取内容”但脚本实际只处理本地文件。结果模型每次遇到 URL 都自信调用脚本直接报错整个任务卡死。问题根源是描述和实现脱节。修复方法很简单要么把 URL 下载逻辑加进脚本要么把描述里关于 URL 的部分删掉。我后来养成了习惯每改一次 description就重新跑一遍全部测试用例确保描述承诺的能力和脚本实际能力完全一致。6.2 环境依赖漂移本地开发时机器上装了 pandas、requests脚本跑得很顺。放到 agent 的运行环境里发现没有这些依赖技能立刻失效。这类问题特别隐蔽因为开发环境通常会比运行环境“富”很多。我现在会在每个技能目录里放一个 requirements.txt并写一个自检命令在技能被加载时检查关键依赖是否存在缺了就提示安装命令而不是让脚本在报错中死掉。6.3 参数解析不兼容长输入用户把一整篇带 Markdown 标记的文本直接粘贴给 agentagent 原样传给脚本。我的脚本用空格拆参数瞬间被撑爆。后来改成前面展示过的写法先拼接全部参数再统一判断是文件还是文本。这样无论用户传一行短文本还是几千字的长内容都能正确处理。从那以后我要求所有技能脚本的入口函数都采用这种“先聚合再判断”的参数解析模式完全避免了参数长度和格式的问题。6.4 长任务没有进度输出有一个技能需要处理大量文件跑一次要几十秒。模型以为脚本卡死了反复调用了几次最后还输出“技能执行失败”。后来我在脚本里加了分批处理每处理完一批就在 stderr 打印一行进度日志。框架能把 stderr 日志转发给上层模型的判断立刻准确了它会知道脚本正在工作也能看到大概还要多久。这个改动很小但效果立竿见影。6.5 忽略退出码这是最基础也最容易忽略的一点。技能脚本出问题时很多初版习惯用print(failed)来报错不设置非零退出码。但 agent 框架判断脚本是否成功主要看退出码而不是看输出文本。脚本打印了“failed”但退出码是 0框架会认为执行成功把“failed”当作正常结果交给模型模型再一本正经地把错误结果交付给用户。我在所有脚本的异常分支里统一加了sys.exit(1)并且约定写到 stderr 的是诊断信息写到 stdout 的是结果数据两类内容严格分开上层就能准确区分成功和失败。这几年我参与过的 agent 项目几乎每一个最终都会走到“把经验固化成技能”这一步。核心原因很简单模型会换、提示词会变但一套定义清晰的技能库是稳定资产。新项目接入时直接把技能目录复制过去旧项目的积累就能立刻复用这种复利效应是纯粹堆 prompt 完全给不了的。最后分享一个小技巧给技能脚本统一加一个--dry-run参数。执行时不产生任何副作用只打印“将执行哪些步骤、读取哪些文件、输出什么格式”。调试触发逻辑时用它验证描述是否正确排查问题时用它快速复现输入输出链路省时省力。agent-skills 这套思路的门槛不高但细节不少希望这篇分享能让你少走几段弯路。
返回列表