ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills工程化实战:从设计、测试到安全上线的完整指南

Agent Skills工程化实战:从设计、测试到安全上线的完整指南 1. 为什么“写好”和“测好”是两件必须拆开做的事很多人第一次接触 Agent Skills脑子里想的都是“我写个脚本让 Agent 跑起来就完事了”。我一开始也这么想结果上线第二天就被现实教育了。一个 Skill 从能跑到好用中间隔着的不是代码量而是测试维度和权限边界这两道坎。标题里说的“写好、测好、安全上线”其实对应的是三个完全不同的工程阶段混在一起做最后一定是返工。先把概念对齐一下。Agent Skills 本质上是给 Agent 挂载的可复用能力单元它可能是一个脚本、一段提示词模板、一个 API 封装或者几者的组合。它和 Agent 的关系有点像插件和宿主程序Agent 负责决策和调度Skill 负责在特定场景下执行确定性动作。热词里有人问“skill 和 agent 的区别”一句话概括就是——Agent 是大脑Skill 是肌肉记忆。大脑可以灵活应变肌肉记忆必须稳定可靠所以对 Skill 的要求天然比 Agent 更“死板”输入输出要可预期失败要可捕获权限要可收敛。那为什么要把“写好”和“测好”拆开因为写的时候你的目标是表达意图测的时候你的目标是证伪意图。这两个心态是冲突的。写的时候你会不自觉地假设“用户会按我想的方式输入”“环境是干净的”“网络是通的”测的时候你必须假设“用户会乱输”“环境是脏的”“依赖会挂”。我见过太多 Skill 在作者本机跑得飞起一到别人机器上就报权限错误典型的就是热词里那个“你需要来自 administrators 的权限才能删除”——写的时候没考虑权限测的时候才发现删不掉文件。所以这篇内容我按四个层次来拆先讲设计思路怎么写才不容易翻车再讲核心细节每个环节的坑在哪然后是完整实操从零到上线的流程最后是问题排查出事了怎么救。适合已经写过一两个 Skill、但还没形成工程化习惯的人也适合准备把 Skill 交付给团队用的开发者。小白也能看因为我会把每个“为什么”都讲透不堆术语。2. 内容整体设计与思路拆解2.1 先定边界Skill 到底该管多宽写 Skill 第一个决策不是技术选型而是职责边界。我踩过的最大的坑就是一开始把 Skill 写得太“聪明”。比如做一个“文件整理 Skill”我让它自己判断哪些文件该删、哪些该留结果测试时它把一个测试目录整个清空了。后来我改成Skill 只负责“按给定规则移动文件”判断逻辑交给 Agent 或用户。这样 Skill 的输入输出变得极其清晰——输入是规则列表输出是移动结果报告。这个思路背后的逻辑是Skill 的确定性越高测试成本越低。一个 Skill 如果内部有大量条件分支和模糊判断你要覆盖的测试用例会指数级增长。反过来把判断权上移Skill 只做“执行器”测试就变成对输入输出的枚举验证。热词里“自动化测试”和“安全测试”之所以经常一起出现就是因为边界清晰的 Skill 才能被自动化测试覆盖。具体怎么定边界我常用一个“三问法”这个 Skill 的输入能不能用一句话描述清楚如果不能说明它管太宽了。这个 Skill 失败时能不能明确告诉调用方“哪一步失败了、为什么”如果不能说明它内部逻辑太黑盒。这个 Skill 能不能在不依赖外部状态的情况下被重复执行如果不能说明它有隐藏副作用。三个问题都过了边界基本就合理了。2.2 选型取舍脚本、提示词还是混合Agent Skills 的实现形态大致分三类纯脚本型、纯提示词型、混合型。选哪种取决于你的场景对确定性和灵活性的权重。纯脚本型适合确定性要求极高的场景比如文件操作、数据转换、API 调用。优点是行为完全可预测测试就是跑单元测试缺点是灵活性差输入格式一变就得改代码。纯提示词型适合需要语义理解的场景比如“把这段文字改写成正式语气”。优点是灵活缺点是输出不稳定测试要靠断言和人工抽检。混合型是主流做法用提示词做输入解析和意图理解用脚本做实际执行。我个人的经验是能用脚本兜底的地方绝不用提示词。因为提示词的输出是概率性的你没法保证它每次都返回同样的结构。热词里“鹈鹕测试提示词”这类东西本质就是在测提示词的稳定性但再稳的提示词也不如一个if-else可靠。所以我的混合型 Skill 通常是提示词负责“把自然语言转成结构化参数”脚本负责“拿着参数去执行”。这里有个细节要注意提示词和脚本之间的接口契约必须写死。比如提示词必须输出 JSON字段名、类型、必填项都固定脚本只认这个 JSON。这样测试时你可以单独测提示词的输出格式也可以单独测脚本对固定 JSON 的处理两边解耦。2.3 权限设计从第一天就当成一等公民热词里权限相关的词特别多——“文件权限修复”“注册表权限问题”“trustedinstaller 权限怎么获得”“创建视图权限不足”“minio mc 命令给 buckets 设置 public 权限”。这说明权限问题是 Skill 上线后最高频的故障源。我的做法是在写第一行代码之前先把权限矩阵画出来。权限矩阵要回答三个问题问题说明示例Skill 需要读什么列出所有读取的资源配置文件、输入目录、环境变量Skill 需要写什么列出所有写入的资源输出目录、日志文件、临时文件Skill 需要调用什么列出所有外部依赖API、数据库、系统命令画完矩阵后逐项问这个权限是必需的吗能不能用更小的权限替代比如只需要读文件就不要申请写权限只需要调用某个 API 的 GET 接口就不要给它 POST 权限。这就是最小权限原则。我见过一个反面案例某个 Skill 为了“方便”直接申请了管理员权限结果测试时误删了系统文件。后来改成只对特定目录有写权限问题就消失了。热词里“你需要来自 system 的权限才能对此文件夹进行更改”这种报错很多时候就是因为 Skill 的权限设计太粗放要么全给要么全不给没有中间态。3. 核心细节解析与实操要点3.1 输入校验把脏数据挡在门外Skill 的第一道防线是输入校验。我见过太多 Skill 直接拿用户输入去拼命令或拼路径结果遇到特殊字符就崩。比如用户输入一个带空格的路径脚本没做转义直接rm -rf $path后果不堪设想。输入校验要分三层格式校验类型对不对、必填项有没有、长度超没超。比如路径必须是绝对路径参数必须是数字。语义校验值合不合理。比如“删除天数”不能是负数“端口号”必须在 1-65535 之间。安全校验有没有注入风险。路径里有没有..命令里有没有;|这些 shell 元字符。实操上我习惯在 Skill 入口处写一个validate_input函数所有校验逻辑集中在这里校验不过直接返回结构化错误不往下走。这样测试时只需要针对这个函数写用例覆盖各种边界值。注意校验失败的错误信息要足够具体但不要泄露内部路径或敏感信息。比如“路径格式不正确”就够了不要返回“/home/admin/secret/config.yaml 不存在”。3.2 输出契约让调用方知道发生了什么Skill 的输出必须是结构化的不能是一坨自然语言。我推荐统一用 JSON包含三个字段status成功/失败、data成功时的结果、error失败时的原因。这样调用方Agent 或其他系统可以程序化地判断结果而不是去解析文本。为什么这点重要因为 Agent 调度 Skill 时需要根据 Skill 的返回决定下一步。如果 Skill 返回的是“操作完成了但是有几个文件没处理”Agent 根本没法判断该不该继续。结构化输出让 Agent 能做确定性决策。输出契约还要考虑幂等性。同一个输入重复执行结果应该一致。如果 Skill 有副作用比如写文件要保证重复执行不会产生重复数据。常见做法是用唯一 ID 去重或者先检查目标状态再执行。3.3 日志与可观测性出事时能查到原因Skill 上线后你不可能盯着每一次执行。所以日志是唯一的“事后现场”。我的日志规范是入口日志记录输入参数脱敏后、调用时间、调用方标识。关键步骤日志记录每一步的开始和结束以及耗时。出口日志记录输出结果、状态码、总耗时。错误日志记录异常堆栈、上下文信息。日志级别用 DEBUG/INFO/WARN/ERROR 四档。生产环境默认 INFO排查问题时临时开 DEBUG。日志要写到文件或标准输出方便被采集系统收集。热词里“bqueues 查看队列权限”这类操作本质就是在做可观测性检查。Skill 的日志也应该支持类似的查询能力——比如按调用方、按时间段、按状态筛选。3.4 测试分层单元、集成、端到端测试是“测好”的核心。我把 Skill 的测试分成三层单元测试测每个函数尤其是输入校验、核心逻辑、输出格式化。用 mock 隔离外部依赖。集成测试测 Skill 和外部系统的交互比如 API 调用、文件读写、数据库操作。用测试环境或容器。端到端测试模拟真实调用场景从 Agent 发起调用到 Skill 返回结果全链路验证。三层测试的投入比例大概是 6:3:1。单元测试写得越多后面越省心。我见过有人跳过单元测试直接做端到端结果每次改代码都要跑一遍完整流程慢且难定位问题。测试用例的设计要覆盖正常路径、边界值、异常输入、外部依赖失败、并发调用。尤其是外部依赖失败很多人不测这个上线后 API 一挂 Skill 就崩。正确做法是给外部调用加超时和重试测试时用 mock 模拟超时和错误响应。3.5 权限收敛从宽到窄的渐进式收紧权限设计不是一次性的而是渐进式收紧的过程。我的做法是开发阶段给足权限方便调试。测试阶段按权限矩阵收敛到最小集验证功能是否正常。上线阶段用独立的低权限账号运行再次验证。运维阶段定期审计权限使用情况发现多余权限就收回。这个过程中最容易出问题的是“测试阶段能跑上线阶段跑不了”。原因通常是测试环境用了高权限账号上线环境用了低权限账号某些操作被拒绝了。所以测试阶段就要用和上线一致的权限配置别偷懒。热词里“cursor 上怎么完全放开权限”这种需求我理解是想省事但我的建议是开发环境可以放开生产环境必须收紧。而且放开也要有边界比如只对特定目录放开而不是全局放开。4. 实操过程与核心环节实现4.1 从零写一个 Skill 的完整流程我以一个“日志清理 Skill”为例走一遍完整流程。这个 Skill 的功能是给定一个目录和保留天数删除该目录下超过保留天数的日志文件。第一步定义接口契约。输入{ target_dir: /var/log/myapp, retain_days: 7, dry_run: false }输出{ status: success, data: { scanned: 120, deleted: 15, skipped: 105, errors: [] }, error: null }第二步画权限矩阵。资源权限说明target_dir读写需要扫描和删除文件日志文件写记录操作日志系统时间读计算文件年龄不需要的权限网络访问、其他目录读写、系统命令执行。第三步写输入校验。import os import re def validate_input(params): errors [] target_dir params.get(target_dir) if not target_dir or not isinstance(target_dir, str): errors.append(target_dir 必须是非空字符串) elif not os.path.isabs(target_dir): errors.append(target_dir 必须是绝对路径) elif .. in target_dir: errors.append(target_dir 不能包含 ..) retain_days params.get(retain_days) if not isinstance(retain_days, int) or retain_days 1: errors.append(retain_days 必须是大于 0 的整数) dry_run params.get(dry_run, False) if not isinstance(dry_run, bool): errors.append(dry_run 必须是布尔值) return errors第四步写核心逻辑。import time import logging def clean_logs(params): target_dir params[target_dir] retain_days params[retain_days] dry_run params.get(dry_run, False) cutoff time.time() - retain_days * 86400 scanned 0 deleted 0 skipped 0 errors [] for root, dirs, files in os.walk(target_dir): for name in files: if not name.endswith(.log): continue scanned 1 path os.path.join(root, name) try: mtime os.path.getmtime(path) if mtime cutoff: if dry_run: logging.info(f[dry-run] 将删除 {path}) else: os.remove(path) logging.info(f已删除 {path}) deleted 1 else: skipped 1 except PermissionError as e: errors.append({path: path, reason: 权限不足}) logging.error(f权限不足: {path}) except Exception as e: errors.append({path: path, reason: str(e)}) logging.error(f处理失败: {path}, {e}) return { status: success if not errors else partial, data: { scanned: scanned, deleted: deleted, skipped: skipped, errors: errors }, error: None }第五步写测试。import pytest import tempfile import os import time def test_validate_input(): assert validate_input({target_dir: /tmp, retain_days: 7}) [] assert validate_input({target_dir: relative, retain_days: 7}) ! [] assert validate_input({target_dir: /tmp, retain_days: 0}) ! [] assert validate_input({target_dir: /tmp/../etc, retain_days: 7}) ! [] def test_clean_logs_dry_run(): with tempfile.TemporaryDirectory() as tmpdir: old_file os.path.join(tmpdir, old.log) with open(old_file, w) as f: f.write(test) os.utime(old_file, (time.time() - 10*86400, time.time() - 10*86400)) result clean_logs({target_dir: tmpdir, retain_days: 7, dry_run: True}) assert result[data][deleted] 1 assert os.path.exists(old_file) # dry-run 不实际删除第六步集成测试。在容器里跑用低权限账号验证权限不足时的行为。比如把目录权限设为只读看 Skill 是否正确返回错误而不是崩溃。第七步上线前检查。权限矩阵是否最小化日志是否脱敏错误处理是否覆盖所有外部调用是否有超时和重试是否有 dry-run 模式文档是否完整4.2 参数计算与选择过程上面例子里的retain_days为什么用整数天而不是秒因为用户心智模型是“天”用秒容易算错。但内部计算要转成秒retain_days * 86400。这里有个细节86400 是 246060不要硬编码用常量或datetime.timedelta更清晰。再比如dry_run默认值设False还是True我倾向设False因为大多数调用是正常执行。但如果是高风险操作比如删除默认设True更安全强制调用方显式传False才真删。这个取舍取决于操作的危险程度。4.3 上线流程与灰度策略Skill 上线不是“写完就发”而是灰度发布。我的流程是内部环境验证开发者和测试人员用跑一周。小流量灰度选 1-2 个非关键调用方接入观察日志和错误率。扩大灰度逐步增加调用方每次观察 24 小时。全量上线所有调用方接入。回滚预案准备好一键回滚的脚本和文档。灰度期间重点看三个指标错误率、耗时、权限拒绝次数。错误率超过 1% 就暂停耗时超过预期 2 倍就排查权限拒绝次数不为零就检查权限矩阵。热词里“区分年末和年中上线”这个说法我理解是不同时间点的上线策略可能不同。比如业务高峰期上线风险更高应该避开。这个思路可以借鉴Skill 上线要选低峰期并且避开其他系统变更窗口。5. 常见问题与排查技巧实录5.1 权限类问题速查权限问题是最高频的我整理了一个速查表现象可能原因排查方法解决文件删不掉文件被占用或权限不足ls -l看权限lsof看占用改权限或先释放占用目录写不了目录权限或磁盘满df -h看磁盘ls -ld看权限清理磁盘或改权限API 调用 403凭证过期或权限不足看 API 返回的具体错误更新凭证或申请权限注册表改不了权限不足看注册表项权限用管理员权限或改权限创建视图失败数据库权限不足看数据库错误日志授予 CREATE VIEW 权限排查权限问题的通用思路是先确认“谁在什么资源上做什么操作被拒绝了”。这三个要素定位清楚问题就解决了一半。5.2 测试类问题排查测试中最常见的问题是“本地能过CI 不过”。原因通常是环境差异本地有某个依赖CI 没有本地是管理员CI 是普通用户本地网络通CI 不通。解决办法是用容器统一环境把依赖、权限、网络都固化下来。另一个问题是“测试通过但上线失败”。这通常是测试覆盖不足没测到某个边界。我的经验是每次线上出问题都要补一个对应的测试用例防止回归。这样测试集越来越全线上问题越来越少。5.3 上线后问题排查上线后出问题第一件事是看日志。日志里通常有错误堆栈和上下文。如果日志不够就临时开 DEBUG 级别复现问题。复现不了就加埋点等下次出现。第二件事是确认影响范围。是单个调用方受影响还是全部是特定输入触发还是随机范围清楚了才能决定是回滚还是热修。第三件事是回滚。如果影响面大先回滚止损再慢慢排查。回滚要快所以上线前就要准备好回滚脚本别临时写。提示Skill 的版本号要规范每次上线打 tag回滚时直接切到上一个 tag。不要用“最新版”这种模糊的版本标识。5.4 独家避坑技巧几个我踩过坑才总结出来的技巧永远提供 dry-run 模式。任何有副作用的 Skill都要支持“只说不做”让调用方先预览结果。错误信息要可操作。不要只说“失败了”要说“失败了因为 X你可以尝试 Y”。限制并发。Skill 被多个调用方同时调用时要加锁或限流防止资源竞争。超时必设。任何外部调用都要设超时默认 30 秒可配置。日志脱敏。路径、用户名、token 这些敏感信息日志里要打码。测试用真实数据。用 mock 数据测不出真实问题尽量用脱敏后的生产数据。文档和代码同步更新。接口变了文档没变调用方就会踩坑。6. 把 Skill 当成产品来运营写到这里我想说的是Skill 不是写完就结束的代码而是一个需要持续运营的“产品”。它有用户调用方、有版本迭代、有故障线上问题、有生命周期上线到下线。你用做产品的态度对待它它才会上线后少给你惹麻烦。我自己的习惯是给每个 Skill 建一个“健康档案”记录版本历史、已知问题、权限矩阵、测试覆盖率、上线时间、负责人。每次出问题就更新档案每次迭代就回顾档案。这样时间长了你对每个 Skill 的状态都心里有数不会出现“这个 Skill 谁写的、还能不能跑”这种尴尬。最后分享一个小技巧新 Skill 上线前先让一个不了解它的人按文档跑一遍。如果他能跑通说明文档和接口设计没问题如果他跑不通说明你还有隐藏假设没写出来。这个“小白测试”比任何自动化测试都能发现文档和易用性问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进