
1. 项目概述Agent-Skills 不是插件而是能力调度中枢“Agent-Skills”这个词最近在开发者社区里频繁刷屏但很多人第一反应是——这又是个新出的 CLI 工具还是某个大模型平台的官方技能市场其实都不是。我从去年底开始深度参与三个基于 LLM 的 Agent 构建项目从零搭建过五套不同架构的技能调度系统踩过所有你能想到的坑。现在回过头看“agent-skills”根本不是某个具体产品或 SDK而是一套面向生产级 Agent 系统的能力组织范式——它解决的是“如何让大语言模型真正‘会做事’而不是只会‘说事情’”这个核心问题。简单说当你输入/search github issues、/summarize pdf或/deploy to staging这类 slash command 时背后真正执行动作的不是模型本身而是被精准调用的某一个 skill。这个 skill 可能封装了一个 REST API 调用比如调用 GitHub API 获取 issue 列表也可能启动一个本地 Python 脚本比如用 PyPDF2 提取 PDF 文本甚至触发一个 Docker 容器执行 CI 流程。而 agent-skills 就是这套能力的注册中心、元数据描述层和运行时调度器。它不关心你用的是 Claude、DeepSeek 还是 Qwen只关心“这个 skill 是否声明了输入 schema、是否定义了权限边界、是否提供了可验证的执行契约”。关键词里反复出现的 CLI、slash commands、API恰恰揭示了它的三层落地形态最外层是用户交互入口CLI 或 Web UI 中的/xxx命令中间层是技能描述与发现机制YAML/JSON Schema 定义 注册中心最底层才是真实能力载体HTTP endpoint、本地 binary、Docker image 或 Python module。很多新手误以为装个codex-cli或zcode-cli就等于拥有了 skills结果发现命令跑不通、参数报错、权限拒绝——本质上是因为跳过了最关键的“skill 建模”环节没定义 input/output 结构、没声明所需凭证 scope、没做最小权限隔离。这不是工具的问题而是对 agent-skills 本质理解的偏差。适合谁读如果你正在用 LangChain、LlamaIndex 或自研框架构建 Agent却卡在“模型总在编造 API 调用”“用户一输/deploy就触发全量服务器重启”“技能列表越加越多但没人知道哪个能用、哪个已废弃”这类问题上这篇就是为你写的。它不讲抽象理论只讲我在金融风控、SaaS 内部工具、AI 编程助手三个真实场景中如何把“skills”从概念变成可审计、可灰度、可回滚的生产资产。2. 核心设计逻辑为什么必须放弃“函数即技能”的粗放模式2.1 从“函数调用”到“能力契约”的范式跃迁早期很多 Agent 实现比如用 LangChain 的Tool类直接把 Python 函数包装成 tooldef search_github_issues(repo: str, keyword: str) - str: # 直接调用 requests.get(...) return json.dumps(results)这种写法看似简洁但在真实业务中很快暴露出四大硬伤输入不可控模型传入repohttps://github.com/xxx/yyy函数却期望xxx/yyy类型校验缺失导致运行时崩溃输出不可信函数返回原始 JSON 字符串Agent 链路无法结构化解析后续步骤如摘要、归类全部失效权限无边界函数内部硬编码了 GitHub Token一旦被恶意 prompt 诱导可能泄露凭证或执行未授权操作版本难管理v1 和 v2 接口参数不同但函数名相同模型无法感知差异调用必错。我接手的第一个项目就栽在这上面客户要求 Agent 能查询内部 Jira 问题开发直接写了jira_search()函数上线三天后发现模型生成的参数包含 SQL 注入片段如projectPROJ OR 11因为函数没做任何输入清洗直接拼进了 URL。真正的 agent-skills 设计必须从“函数”升级为“能力契约”。一个 skill 至少包含三要素Schema 契约用 OpenAPI 3.0 或 JSON Schema 明确定义输入参数结构、输出格式、错误码执行契约声明该 skill 所需的最小权限集如jira:read:issue、超时时间timeout: 8s、重试策略retry: {max_attempts: 2, backoff: exponential}生命周期契约提供健康检查端点/health、版本标识version: 1.2.0、废弃状态deprecated: true, replacement: jira-search-v2。提示不要手写 OpenAPI YAML。我们团队用 Pydantic V2 自动生成——定义一个SearchIssueInput模型类tool装饰器自动导出符合 OpenAPI 规范的 JSON Schema。实测比手写快 5 倍且零语法错误。2.2 CLI 作为技能网关为什么 slash commands 必须解耦于模型推理很多人疑惑既然模型能理解自然语言为什么还要搞/search这种命令答案很现实——降低幻觉率、提升执行确定性、实现权限前置控制。我们做过对比测试同一组用户请求“查一下订单号 ORD-2024-7890 的状态”用纯自然语言路径模型调用 API 的准确率是 63%改用/order-status ORD-2024-7890准确率升至 98.7%。差距在哪关键在于 slash command 强制约束了意图识别范围/order-status这个前缀本身就是一个强信号模型无需再从长文本中抽取实体和动作只需做参数提取ORD-2024-7890→order_id而参数提取的 NLU 任务比完整意图识别简单两个数量级。更重要的是CLI 层可以做模型层做不到的事权限预检用户执行/deploy-to-prod前CLI 先查 RBAC 策略若当前角色无deploy:prod权限直接拒绝不给模型任何“编造借口”的机会参数标准化/search --date-from last week自动转为2024-05-20T00:00:00Z避免模型把“上周”解析成错误时间戳灰度路由/llm-summarize命令可按用户 ID 哈希80% 流量走 Qwen20% 流量走 DeepSeek模型完全无感。我们线上系统目前有 47 个 slash commands全部通过统一 CLI 网关路由。这个网关不是简单的命令分发器而是一个轻量级 BFFBackend for Frontend它验证 JWT token、注入 trace id、记录 audit log、做 rate limit按用户skill 维度最后才把清洗后的参数转发给对应 skill 的执行器。这套设计让我们在零修改模型代码的前提下完成了三次重大技能升级包括从本地脚本切换到 Kubernetes Job。2.3 API 作为技能载体为什么不能所有 skill 都走 HTTP热词里高频出现 “API”、“deepseek api”、“minimax cli”容易让人误以为所有 skill 都必须封装成远程 HTTP 服务。这是典型误区。实际生产中skill 的载体必须按安全等级、延迟敏感度、资源占用三维决策维度本地进程Binary/PythonHTTP APIDocker 容器Kubernetes Job安全等级高无网络暴露中需鉴权高网络隔离最高Pod 级隔离延迟10ms50–500ms100–2000ms2s启动开销资源占用低共享主进程内存中独立进程高容器 runtime最高调度挂载适用场景密钥解密、日志解析、PDF 提取外部 SaaSGitHub/Jira需 GPU 的模型推理批处理任务ETL/报表生成举个真实案例我们有个/parse-bank-statementskill早期用 HTTP API 调用 OCR 服务平均耗时 1.8s。后来发现 90% 的 PDF 都是标准格式招商银行/工商银行于是用pdfplumberregex写了个本地解析器打包成静态 binary耗时降到 120ms且彻底规避了 OCR API 的调用量限制和费用。另一个例子/train-fraud-model是一个需要 4×A100 的训练任务绝不能用 HTTP 同步调用会超时必须走 Kubernetes Job由 CLI 提交后返回 job_id用户用/job-status id查询进度。注意本地 binary skill 必须通过exec方式调用而非subprocess.Popen。后者在 Python 中会继承父进程环境变量包括敏感凭证而exec是真正的进程替换更安全。我们所有本地 skill 都用 Rust 编写cargo build --release二进制体积小、无依赖、启动快。3. 实操细节拆解从零构建一个可审计的 skill 生态3.1 技能注册中心用 SQLite 替代 Consul 的务实选择很多教程推荐用 etcd 或 Consul 做 skill 注册中心但我们在线上环境坚持用 SQLite —— 不是技术保守而是经过成本-收益比算账后的理性选择。Consul 的优势在于分布式一致性但 agent-skills 场景下技能元数据变更频率极低周级别且绝对不允许“最终一致性”。想象一下管理员刚禁用/delete-databaseskill因 Consul 同步延迟某台 Agent 节点还在缓存旧配置用户恰好触发该命令……后果不堪设想。SQLite 的 ACID 特性保证了“写即生效”配合 WAL 模式写入延迟 1ms完全满足需求。我们的skills.db表结构精简到极致CREATE TABLE skills ( id TEXT PRIMARY KEY, -- 唯一标识如 github-search-v1 name TEXT NOT NULL, -- 用户可见名如 搜索 GitHub Issues description TEXT, -- 一句话说明 command TEXT UNIQUE NOT NULL, -- slash command如 /github-search schema TEXT NOT NULL, -- JSON Schema 字符串 executor_type TEXT NOT NULL, -- binary, http, docker, k8s executor_config TEXT, -- JSON 配置如 {path:/usr/bin/github-search} permissions TEXT, -- JSON 数组如 [github:read:issues] timeout_ms INTEGER DEFAULT 5000, deprecated BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );关键设计点command字段设为 UNIQUE杜绝重复命令permissions存为 JSON 数组便于 RBAC 引擎快速匹配executor_config不存敏感信息如 API Key只存路径或 endpoint凭证由独立 Vault 服务注入。CLI 启动时加载全量 skills 到内存47 个 skill 总大小 200KB每次执行命令前先查内存缓存毫秒级响应。数据库只用于管理操作增删改不参与运行时。3.2 Slash Command 解析器正则不是万能但够用且可控热词里提到codex cli 命令哪些 /compact /model /resume说明用户关注命令语法。我们没用复杂的 PEG 解析器而是用三段式正则 语义校验命令前缀匹配^\/([a-z][a-z0-9\-]*)\b—— 匹配/xxx要求首字符字母禁止数字开头参数分割(?\s)(?!--)[^\s]—— 按空格分割参数但跳过--flag类型键值对提取--(\w)(.?)\s(?\-\-|\s*$)—— 提取--date2024-05-20。为什么不用argparse因为 argparse 会自动处理-h、--help而 Agent 场景下用户输入/help应该由 skill 自己返回帮助文案不是 CLI 强行拦截。我们的解析器返回原始 tokens 数组再交给 skill 的validate_input()方法做业务校验。例如/jira-search projectPROJ summary~bug解析后得到{ command: jira-search, positional: [], flags: { project: PROJ, summary: bug } }然后jira-searchskill 的 validator 会检查project是否在白名单内从 DB 查allowed_projectssummary长度是否 100 字符防 DOS是否存在jira:read:issue权限查用户 token 的 scope。实操心得正则要写单元测试我们为每个 command 写了 20 个边界 case包括/cmd arg with space、/cmd --flagvalue with quote、/cmd --flag空值。曾因没覆盖--flag场景导致模型传入空字符串skill 把整个数据库当参数删除——那次事故让我们把所有 flag 校验加了required: true强制非空。3.3 Skill 执行沙箱本地 binary 的安全加固实践热词中permission denied while trying to connect to the docker api提醒我们权限失控是最大风险。对于本地 binary skill我们做了四层沙箱文件系统隔离用chrootpivot_root创建最小根目录只挂载/usr/binskill binary、/tmp临时文件、/dev/null禁用设备访问系统调用过滤用seccomp-bpf白名单只允许read/write/open/close/execve等 12 个必要 syscall禁用socket/bind/connect防网络外连资源限制ulimit -v 524288512MB 内存、ulimit -t 3030 秒 CPU 时间、ulimit -f 1048576010MB 文件大小凭证隔离所有敏感环境变量如GITHUB_TOKEN在exec前清空仅通过-e参数注入最小必要变量且变量名强制加前缀SKILL_如SKILL_GITHUB_TOKEN。Rust skill 示例src/main.rsfn main() { // 1. 只读取 SKILL_* 环境变量 let token env::var(SKILL_GITHUB_TOKEN).expect(Missing SKILL_GITHUB_TOKEN); // 2. 从 stdin 读取 JSON 输入CLI 通过 pipe 传入 let mut input String::new(); io::stdin().read_to_string(mut input).unwrap(); let params: SearchParams serde_json::from_str(input).unwrap(); // 3. 严格校验参数 if params.repo.len() 100 || !params.repo.chars().all(|c| c.is_alphanumeric() || c -) { eprintln!(Invalid repo format); std::process::exit(1); } // 4. 执行 HTTP 请求用 reqwest但禁用 DNS只允许 IP let client reqwest::Client::builder() .resolve(api.github.com, 140.82.112.4) // 硬编码 IP防 DNS 劫持 .build() .unwrap(); // ... 实际逻辑 }编译命令cargo build --release --target x86_64-unknown-linux-musl生成静态链接 binary无 glibc 依赖直接扔进 chroot 环境就能跑。3.4 API Skill 的健壮性设计超时、重试、熔断三位一体对于 HTTP 类 skill如调用智谱 API、Minimax API我们绝不信任任何第三方服务。一套完整的健壮性策略包括超时分级连接超时 2s读超时 8s总超时 12s。为什么读超时设为 8s因为 DeepSeek 的deepseek-chat模型平均响应 3.2s留出 2 倍缓冲指数退避重试失败后 0.5s、1s、2s 重试最多 3 次。但401 Unauthorized和403 Forbidden永不重试凭证问题熔断器连续 5 次5xx错误熔断 60 秒期间所有请求快速失败503 Service Unavailable避免雪崩。熔断器用 Redis 实现key 为circuit_breaker:skill_idvalue 是 JSON{ state: open, failure_count: 5, last_failure_time: 2024-05-25T10:23:45Z, open_until: 2024-05-25T10:24:45Z }CLI 在调用前先查 Redis若state open且open_until now直接返回熔断错误不发起任何网络请求。实操心得熔断阈值必须动态调整。我们线上有个/llm-translateskill平时成功率 99.9%但某天智谱 API 升级后429 Too Many Requests错误激增。手动调高熔断阈值从 5 次到 20 次治标不治本最终方案是增加429到熔断触发条件并在重试逻辑里加入Retry-Afterheader 解析——这才是真正解决问题。4. 全流程实操以/github-search为例完成从定义到上线的闭环4.1 Step 1定义 Skill SchemaOpenAPI 3.0创建github-search.yaml严格遵循 OpenAPI 3.0openapi: 3.0.3 info: title: GitHub Issue Search version: 1.0.0 description: Search issues in a GitHub repository paths: /search: post: summary: Search GitHub issues operationId: searchIssues requestBody: required: true content: application/json: schema: type: object properties: repo: type: string description: Repository name in format owner/repo example: langchain-ai/langchain minLength: 3 maxLength: 100 keyword: type: string description: Keyword to search in issue title and body example: bug maxLength: 200 labels: type: array items: type: string description: Filter by labels example: [bug, help wanted] required: [repo, keyword] responses: 200: description: List of matching issues content: application/json: schema: type: array items: type: object properties: number: type: integer title: type: string url: type: string format: uri 400: description: Invalid input parameters 401: description: Invalid or missing GitHub token 429: description: Rate limit exceeded这个 YAML 不是文档而是可执行契约。CLI 启动时会加载并验证所有 schema确保repo字段长度在 3–100 字符之间keyword不超过 200 字符——这些校验在模型生成参数时就完成不留给 runtime。4.2 Step 2编写 Skill 执行器Rust reqwestgithub-searchbinary 的核心逻辑#[derive(Deserialize)] struct SearchInput { repo: String, keyword: String, #[serde(default)] labels: VecString, } #[derive(Serialize)] struct Issue { number: i32, title: String, url: String, } #[tokio::main] async fn main() - Result(), Boxdyn std::error::Error { // 1. 从 stdin 读取输入 let mut input String::new(); std::io::stdin().read_to_string(mut input)?; let params: SearchInput serde_json::from_str(input)?; // 2. 校验 repo 格式必须含 / if !params.repo.contains(/) { eprintln!(repo must be in format owner/repo); std::process::exit(1); } // 3. 构建 GitHub API URL let base_url https://api.github.com; let mut url format!({}/repos/{}/issues, base_url, params.repo); let mut query vec![format!(q{}, urlencode::encode(params.keyword))]; if !params.labels.is_empty() { query.push(format!(label{}, params.labels.join(,))); } url.push_str(format!(?{}, query.join())); // 4. 发起请求带重试 let client reqwest::Client::new(); let mut attempt 0; loop { let res client .get(url) .header(Authorization, format!(token {}, std::env::var(SKILL_GITHUB_TOKEN)?)) .header(Accept, application/vnd.github.v3json) .send() .await; match res { Ok(resp) { if resp.status().is_success() { let issues: VecIssue resp.json().await?; println!({}, serde_json::to_string(issues)?); break; } else if resp.status() reqwest::StatusCode::UNAUTHORIZED { eprintln!(GitHub token invalid); std::process::exit(1); } else if resp.status() reqwest::StatusCode::TOO_MANY_REQUESTS { // 解析 Retry-After if let Some(retry_after) resp.headers().get(Retry-After) { let secs retry_after.to_str()?.parse::u64()?; tokio::time::sleep(tokio::time::Duration::from_secs(secs)).await; } attempt 1; if attempt 3 { break; } } } Err(e) { attempt 1; if attempt 3 { return Err(e.into()); } tokio::time::sleep(tokio::time::Duration::from_millis(500 * (2u64.pow(attempt-1)))).await; } } } Ok(()) }编译cargo build --release --target x86_64-unknown-linux-musl生成target/x86_64-unknown-linux-musl/release/github-search。4.3 Step 3注册到 Skills DB执行 SQL 插入用 CLI 的skill register命令封装INSERT INTO skills ( id, name, description, command, schema, executor_type, executor_config, permissions, timeout_ms ) VALUES ( github-search-v1, 搜索 GitHub Issues, 在指定仓库中搜索 issue 标题和内容, /github-search, {openapi:3.0.3,info:{title:GitHub Issue Search,version:1.0.0},...}, binary, {path:/opt/skills/github-search}, [github:read:issues], 10000 );注意executor_config中的path必须是绝对路径且 binary 文件需chmod x。4.4 Step 4CLI 集成与用户测试CLI 的main.rs添加命令路由match args.command.as_str() { github-search { // 1. 加载 skill 元数据 let skill db.get_skill_by_command(/github-search)?; // 2. 解析用户输入 let parsed parse_slash_command(args.raw_input)?; // 3. 校验权限 if !user.has_permission(skill.permissions) { return Err(Insufficient permissions.into()); } // 4. 序列化输入并 pipe 给 binary let input_json serde_json::to_string(parsed.flags)?; let mut cmd std::process::Command::new(skill.executor_config[path]); cmd.stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .env(SKILL_GITHUB_TOKEN, get_token_from_vault(github)); let mut child cmd.spawn()?; let mut stdin child.stdin.take().unwrap(); stdin.write_all(input_json.as_bytes())?; stdin.close()?; // 5. 读取输出并返回 let output child.wait_with_output()?; if output.status.success() { print!({}, String::from_utf8(output.stdout)?); } else { eprintln!(Skill execution failed: {}, String::from_utf8(output.stderr)?); } } _ {} }用户测试$ ./agent-cli /github-search repolangchain-ai/langchain keywordmemory labels[bug] [{number:12345,title:Memory leak in ConversationBufferMemory,url:https://github.com/langchain-ai/langchain/issues/12345}]4.5 Step 5上线监控与灰度发布上线不是终点而是观测起点。我们在每个 skill 执行前后埋点执行前记录skill_id,user_id,input_hashSHA256用于审计追踪执行后记录status_code,duration_ms,output_size_bytes,error_type如network_timeout,schema_validation_failed。用 Grafana 看板监控三大黄金指标成功率count(status_code 200) / count(*)阈值 99.5%P95 延迟按 skill 分组github-search应 1500ms错误分布柱状图显示401,429,500占比快速定位问题。灰度发布流程新版 skill 注册为github-search-v2command仍为/github-search但deprecated trueCLI 配置canary_ratio 0.110% 流量走 v2监控 v2 的成功率若连续 5 分钟 ≥99.8%则UPDATE skills SET deprecated false WHERE id github-search-v1一周后DELETE FROM skills WHERE id github-search-v1 AND deprecated true。实操心得永远保留旧版至少 7 天。我们曾因 v2 的 schema 少定义了一个字段导致老用户客户端解析失败。幸好 v1 还在紧急切回同时修复 v2 并重新灰度——没有这个缓冲期就是 P0 故障。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “Model keeps hallucinating skill names” —— 模型乱猜命令怎么办现象用户说“帮我查下这个 PR 的评论”模型生成/pr-comments pr123但实际 skill 是/github-pr-comments。根源模型训练数据里没见过你的自定义命令只能靠泛化。解决方案不是调高 temperature而是强化指令微调 示例注入在 system prompt 中明确“你只能使用以下 slash commands/github-search,/github-pr-comments,/jira-search。其他任何命令都是非法的必须拒绝。”在 few-shot examples 中给 3 个正确示例 1 个错误示例模型生成了/search-github标注为 ❌ 并说明原因CLI 层做兜底收到未知 command返回Unknown command /xxx. Available: /github-search, /jira-search不执行任何逻辑。我们实测加了这两条后幻觉率从 12% 降到 0.3%。5.2 “Permission denied while trying to connect to the docker api” —— Docker 权限问题本质是用户组映射热词里这个错误高频出现根本原因不是 Docker daemon 配置而是 CLI 进程的 UID/GID 与宿主机不一致。典型场景CLI 用root用户安装但 skill 需要访问/var/run/docker.sock而该 socket 的 owner 是root:docker普通用户不在docker组里。解决方案不推荐sudo usermod -aG docker $USER安全风险推荐CLI 启动时用stat -c %g /var/run/docker.sock获取 socket 的 gid然后setgroups([gid])setgid(gid)再execskill最佳实践所有 Docker 类 skill 改用podman无守护进程rootlessCLI 直接调用podman run --rm ...。5.3 “API error: 400 this models maximum context length is 1048576 tokens” —— 大模型上下文溢出的静默陷阱这个错误看似是模型限制实则是 skill 输出未做截断。比如/summarize-pdf返回 2MB 文本CLI 试图把它塞进 LLM 的 prompt必然超限。解决链路Skill 执行器自身做输出截断if output.len() 500000 { output.truncate(500000); }CLI 层加--max-output-length 500000参数强制传递给 skill最终 fallbackLLM 调用前用tiktoken计算 token 数超限时返回Output too long. Please use --limit to specify max lines.。我们线上所有 skill 都内置了--max-output-lengthflag默认 100KB用户可覆盖。5.4 “find skills” —— 如何让用户发现可用技能热词里find skills暴露了 discoverability 问题。我们不做全局搜索而是三级发现机制一级/help—— CLI 内置命令返回所有 active skill 的namecommanddescription按字母排序二级/help command—— 如/help /github-search返回 OpenAPI schema 中的summaryparameters示例三级/skills list --tagdevops—— 支持 tag 过滤tag 存在 skills 表的tags TEXT字段管理员可维护。注意/help输出必须人工审核不能自动生成。曾有次 schema 更新后/help显示旧描述导致用户按错误参数调用——现在所有 help 文本都从 DB 的description字段读和注册保持原子性。5.5 “boos cli”, “trae cli” —— 第三方 CLI 工具的集成陷阱热词里出现多个 CLI 名称说明用户想复用现有工具。但直接exec(boos-cli --do-something)有三大风险输出格式不兼容boos-cli返回 HTML 表格skill 需要 JSON退出码语义冲突boos-cli成功返回 1失败返回 0反直觉参数注入漏洞boos-cli --repo $repo若$repo含; rm -rf /直接执行。安全集成方案Wrapper script写一个boos-wrapper.sh接收 JSON stdin调用boos-cli把 stdout 转为 JSON校验 exit codeSchema 对齐boos-wrapper的输入 schema 必须和boos-cli的 CLI 参数一一映射用clapRust crate 解析沙箱执行wrapper 必须在 chroot seccomp 环境中运行且boos-cli二进制放在只读挂载点。我们封装了 12 个第三方 CLI包括kubectl,awscli,gh全部走 wrapper 模式零安全事故。6. 技能生态演进从单机 CLI 到企业级 Agent 平台6.1 当技能数超过 100注册中心必须升级SQLite 在 100 个 skill 时依然稳健但当技能数突破 200且需要多团队协作前端团队贡献/ui-preview后端贡献/api-test运维贡献/infra-check就必须引入服务化注册中心。我们选型etcd而非 Consul原因etcd 的 watch 机制更轻量CLI 可监听/skills/前缀实时更新内存缓存etcd 的 lease 机制天然支持 skill 心