
最近挺多人问我agent-skills 到底是个什么东西是又一个新造出来的概念还是真能把 AI Agent 从“勉强能跑”拉到“稳定可交付”。我在生产环境里维护自己这套技能库已经大半年了可以负责任地说它是目前我见过让 Agent 行为变得可控的最实在的手段之一。简单讲agent-skills 就是把 Agent 需要反复完成的每一类高频任务打包成一份包含触发条件、调用参数、执行步骤、输出校验的标准技能模板。它解决的核心问题很直接模型太自由、行为太随机、结果没法验收。这篇文章我会从设计思路讲到代码实现再讲排查经验适合正在做 Agent 应用、想降低 prompt 维护成本、提升任务成功率的开发者。1. Agent Skills 核心概念与设计心态1.1 skills 和 tools 的关系为什么不是所有能力都叫 skills很多人第一次接触 agent 开发会先学会 function calling也就是给模型注册一堆函数。比如send_email(to, subject, body)、search_web(query)、create_calendar_event(title, time)。这些是 tools是原子操作模型可以直接调用也能拿到返回值。问题在于真实业务里很少能靠单个原子操作搞定一个任务。举个实际例子“把今天分散在三个文档里的会议记录、项目进度、临时想法整理成日报并归档”如果拆成 tools你会得到 read_file、write_file、extract_keywords、generate_summary 这一堆零散函数。模型每一次都要自己决定先调哪个、再调哪个、中间出了错怎么处理结果就是调用链又长又容易断漏步骤、格式错乱、文件路径写错各种情况都发生过。skills 就是来解决这层问题的。一个 skill 不是一个函数而是一整套“工作方案”内部可以包含多个小步骤也可以调用多个工具或脚本。它对外暴露的是一个清晰目标用户给一份输入技能返回一份结构化的结果。所以我的理解里tools 是扳手skills 是“换轮胎的标准工序卡”。工序卡里可以写明什么时候用扳手、什么时候用千斤顶、按什么顺序操作、做完怎么检查。这套工序卡才是让一个新手稳定完成工作的关键。agent-skills 项目要做的事情就是把这类工序卡系统化地沉淀下来让团队里任何一个 Agent 都能加载、复用、评估和迭代。它的价值不在某一句话术而在分层设计底层是原子工具中间是标准化技能上层才是 Agent 的规划和决策。1.2 像写员工手册一样设计技能我第一次设计 skill 模板时特别喜欢堆细节想把所有可能情况都写进描述里。后来发现效果并不好模型要么因为描述太长而忽略要么被过多的边界条件绕晕。后来我换了个思路就当自己是在给一个刚入职的实习生写岗位手册。给实习生写手册你会怎么写先告诉他这份工作什么时候需要做具体目标和验收标准是什么再告诉他第一步做什么、第二步做什么每个步骤注意什么然后给几个已经做好的样例让他照着样子模仿最后告诉他哪些情况不要做、出了异常找谁。这个结构基本就是一份合格 skill 的骨架。我把骨架提炼成三个原则自包含技能不依赖 Agent 的私密记忆或上下文状态。需要的数据全部通过输入参数传入或者从一个明确的输入目录读取。这样任何 Agent、任何时间加载它行为都是一致的。收敛只做一件事输入输出边界清晰。比如“整理日报”这个技能就只负责把一堆零散文件转成一份日报 markdown不负责发送邮件、不负责提醒日程。想扩展就再建一个技能。可观测执行过程中写日志返回结构化结果。进程退出码、错误码、日志路径都要明确这样上层 Agent 才能知道“技能到底执行成功了没有、卡在哪一步”。还有一个反直觉的经验skill 描述里一定要写清楚“什么时候不要用”。我最初觉得写“不适用场景”会降低技能的曝光度实际恰恰相反。模型在多个技能里做选择时排除法比匹配法更可靠。你告诉它“这个技能不适合处理多语言文档”它遇到外语文件就会主动换别的路径而不是硬把一个中文场景的技能套上去。2. 技能库的整体结构设计2.1 目录与文件规范每个技能都是一个独立工程agent-skills 不是写在单个 prompt 里的东西而是一个完整的目录工程。一个技能在仓库里应该有独立目录目录内部文件职责清楚。我的常用结构长这样agent-skills/ ├── README.md ├── skills/ │ ├── doc-summary/ │ │ ├── skill.md │ │ ├── schema.json │ │ ├── scripts/ │ │ │ ├── run.py │ │ │ └── requirements.txt │ │ ├── assets/ │ │ │ └── template.md │ │ ├── tests/ │ │ │ ├── test_doc_summary.py │ │ │ └── fixtures/ │ │ │ ├── input1.md │ │ │ └── expected1.md │ │ └── examples/ │ │ └── call_example.json │ └── ...每个文件承担一个职责skill.md是给模型看的说明书用自然语言写清楚技能的定位、使用步骤、注意事项和示例。schema.json是给参数定义的 JSON Schema模型根据它生成结构化的调用参数。scripts/存放技能执行代码默认入口统一叫run.py便于加载器识别。assets/放模板、字典、参考文件等静态资源。tests/放自动化测试和测试夹具保证技能在改动后仍然能跑出预期结果。examples/放一个或几个标准调用样例方便人工检查和调试。之所以把每个技能都做成一个独立小工程是为了让“技能”具备工程素养。很多 Agent 项目失败不是因为模型能力不行而是因为技能本身没有版本、没有测试、没有错误处理出了问题只能靠猜。目录规范是第一步它逼你把每个技能的上下文边界固定下来。2.2 元数据设计让模型更容易“选对”技能模型在一个技能库里做选择靠的是元数据的质量而不只是代码质量。skill.md里的描述就是技能在模型眼中的“简历”。我们看一个示例--- name: doc-summary description: 把多份零散文档或笔记整理成一份结构化日报。适用于工作报告、会议纪要、每日反思等场景。不适用于需要翻译、需要情感分析或多语言内容处理的任务。 version: 2.3.0 --- 当用户希望把多个文件中的关键信息汇总成一份 markdown 文档时使用 doc-summary 技能。 执行步骤 1. 从参数 input_paths 中读取所有源文件路径。 2. 逐个读取文件内容提取标题、正文预览。 3. 按统一模板生成日报写入 output_path。 4. 输出 JSON 结果包含输出路径和条目数量。 示例 输入input_paths: [./notes/a.md, ./notes/b.md], output_path: ./report.md 输出{status: ok, output_path: ./report.md, count: 2}这里最容易被忽略的是 description 字段。我早期写得很简陋例如“汇总文档”结果模型在“是否调用”上经常犹豫。后来改成“把多份零散文档或笔记整理成一份结构化日报”并明确写“不适用于翻译、情感分析”调用准确率明显提升。schema.json同样重要。模型要通过它生成合法参数如果字段说明含糊就会出错。我常用的 schema 结构{ $schema: http://json-schema.org/draft-07/schema#, type: object, properties: { input_paths: { type: array, items: { type: string }, description: 要汇总的源文件路径列表必须是绝对路径或相对当前工作目录的路径, minItems: 1 }, output_path: { type: string, description: 输出 markdown 文件的保存路径, pattern: \\.md$ }, style: { type: string, enum: [brief, detailed], default: brief, description: brief 只保留每条摘要前 120 字detailed 保留完整内容 } }, required: [input_paths, output_path] }设计时我会注几个细节枚举值要给口头说明避免模型生成风格不符的值必填字段要明确带 pattern 的字段要说明规则。参数校验这一层等于给模型套了一个“安全带”越早暴露错误后面就越容易排查。3. 从零实现一个高复用技能文档汇总助手3.1 需求痛点与方案选择我最初做 doc-summary 这个技能是因为每天都在处理一堆零散笔记。上午开了个项目会议下午写了一堆技术调研晚上还有几条临时想法散落在不同目录。以前的方式是把所有内容复制到对话框里让模型直接生成日报结果经常出现三种情况内容太长被截断、重要时间信息丢失、输出格式每天都不一样。靠 prompt 反复强调也不是办法因为模型每次都从零推理一致性很难保证。所以我决定把它做成一个 skill。设计思路是“能本地规则化的步骤坚决用代码只有语义理解部分才让模型参与”。如果整条链路都用模型做主仍然会有随机性如果全部用规则又处理不了不同文档之间的语义差异。于是我把任务拆成三层本地规则层读取文件、清理格式、提取基础信息纯脚本完成。模型决策层由 Agent 决定要不要调用这个技能、参数怎么填、要不要继续做后续处理。模板输出层统一生成 markdown 结构保证格式稳定。这里要注意skills 不一定非要调用大模型。很多场景下纯脚本反而是最可靠的。你说到底模型在技能里的角色是“选择入口”和“解释结果”而不是每一步都替代代码。这样设计测试成本大大降低。3.2 核心脚本实现脚本要能独立运行技能的核心脚本放在doc-summary/scripts/run.py。我要求每个技能脚本能单独在命令行里跑通不依赖 Agent 框架。这样调试时可以直接python run.py --input-paths a.md b.md --output-path report.md快速验证脚本本身有没有问题。一个可运行的版本#!/usr/bin/env python3 import argparse import json import pathlib import re import sys from datetime import datetime def load_params_from_args(): parser argparse.ArgumentParser(descriptiondoc-summary skill runner) parser.add_argument(--input-paths, nargs, requiredTrue, help要汇总的源文件路径列表) parser.add_argument(--output-path, requiredTrue, help输出 markdown 文件路径) parser.add_argument(--style, choices[brief, detailed], defaultbrief, help汇总格式brief 只保留摘要detailed 保留完整内容) args parser.parse_args() return { input_paths: args.input_paths, output_path: args.output_path, style: args.style, } def clean_text(text: str) - str: 清理文档去掉空行和首尾空白保证模板输出不乱。 lines [line.strip() for line in text.splitlines() if line.strip()] return \n.join(lines) def extract_title(text: str) - str: 提取标题优先取 markdown 第一个 # 标题否则取第一行。 m re.search(r^#\s(.)$, text, re.MULTILINE) if m: return m.group(1).strip() first_line text.splitlines()[0].strip() return first_line[:20] def build_entry(path, text, style): title extract_title(text) body clean_text(text) if style brief: body body[:120] (... if len(body) 120 else ) return { title: title, path: str(path), preview: body, } def main(): params load_params_from_args() entries [] for p in params[input_paths]: fp pathlib.Path(p) if not fp.exists(): print(json.dumps({ status: error, error: file_not_found, path: p, }, ensure_asciiFalse)) sys.exit(1) text fp.read_text(encodingutf-8) entries.append(build_entry(fp, text, params[style])) output_lines [f# Daily Report {datetime.now():%Y-%m-%d}, ] for i, e in enumerate(entries, 1): output_lines.append(f## {i}. {e[title]}) output_lines.append(f- Source: {e[path]}) output_lines.append() output_lines.append(e[preview]) output_lines.append() out_path pathlib.Path(params[output_path]) out_path.parent.mkdir(parentsTrue, exist_okTrue) out_path.write_text(\n.join(output_lines), encodingutf-8) print(json.dumps({ status: ok, output_path: str(out_path), count: len(entries), }, ensure_asciiFalse)) if __name__ __main__: main()这段脚本有几个刻意的设计输出只用 JSON不上屏打印无关日志。这样上层 Agent 拿到 stdout 就能直接解析不会被日志干扰。文件不存在的错误要单独返回一个file_not_found状态码而不是直接抛异常。模型看到这个错误码才能自己决定要修正路径还是向用户询问。所有路径参数都保留原样由调用方保证位置正确脚本只负责校验文件是否存在。这样脚本和文件系统环境的耦合最小。如果技能需要更复杂的语义理解可以在build_entry或后续步骤里接入模型但建议把模型调用包装成一个独立函数并且设置超时。否则一个技能因为网络调用挂掉整个 Agent 任务都会卡住。3.3 把技能注册进 Agent 主循环脚本能跑还不够得让 Agent 在合适的时候决定调用它。我的主循环加载逻辑非常简单扫描skills/下的每个目录读取skill.md和schema.json组合成一个候选技能表让模型决策。import json import pathlib import subprocess def load_skill(skill_dir: pathlib.Path): return { dir: skill_dir, doc: (skill_dir / skill.md).read_text(encodingutf-8), schema: json.loads((skill_dir / schema.json).read_text(encodingutf-8)), script: skill_dir / scripts / run.py, } def load_all_skills(base_dir: str) - list: root pathlib.Path(base_dir) skills [] for p in root.iterdir(): if (p / skill.md).exists() and (p / schema.json).exists(): skills.append(load_skill(p)) return skills def decide_and_run(agent, user_message: str, skills: list) - dict: skill_descriptions [] for s in skills: freeze { name: s[schema].get(name), description: s[doc].split(---)[2].strip() if s[doc].startswith(---) else s[doc], } skill_descriptions.append(freeze) prompt ( 当前可用技能如下\n json.dumps(skill_descriptions, ensure_asciiFalse) \n用户需求 user_message \n 请返回 JSON{\skill\: \技能名\, \arguments\: {...}}只返回 JSON不要多余解释。 ) # agent.complete 是对你实际使用的模型封装的抽象 decision json.loads(agent.complete(prompt)) target None for s in skills: if s[schema][name] decision[skill]: target s break if target is None: return {status: error, error: skill_not_found} args decision[arguments] cmd [ python, str(target[script]), --output-path, args[output_path], --style, args.get(style, brief), --input-paths, *args[input_paths], ] try: proc subprocess.run(cmd, capture_outputTrue, textTrue, timeout60) except subprocess.TimeoutExpired: return {status: error, error: timeout} if proc.returncode ! 0: return {status: error, error: skill_exec_failed, stderr: proc.stderr[-500:]} return json.loads(proc.stdout)这里的关键不是代码本身而是“模型决策 脚本执行”这个闭环。模型只负责从技能列表里选一个并生成参数脚本负责把结果稳定做出来返回结果用 JSON 结构化。整个过程可追踪、可回放。实际项目里我会额外加一层参数校验比如用jsonschema在脚本执行前验证decision[arguments]提前发现模型生成的非法参数。校验失败时把失败原因返回给模型让它修正后再试一次。这个“试错回环”能让技能调用的成功率从 70% 提到 95% 以上。4. 调优与排查Agent Skills 落地中的坑4.1 技能不生效怎么办问题排查速查表我在实际维护 agent-skills 时踩过不少坑很多问题不是模型笨而是技能本身写得有问题。我把常见问题整理成一张表方便遇到情况时对号入座。症状可能原因排查方向解决思路模型始终不调用技能技能 description 太宽泛或和用户需求匹配度低检查模型决策日志看它选择了什么工具重写 description明确“适用于什么场景”加一个真实示例模型调用了错误的技能技能之间边界不清存在大量重叠描述对比两个技能的 description找出冲突点增加各自的 when_not_to_use区分使用场景参数生成错误schema 中字段说明含糊或类型没有约束查看模型生成的 arguments 是否符合 schema给每个字段写更具体的描述增加枚举值和默认值脚本执行失败脚本依赖了不存在文件、环境变量或第三方包手工运行脚本复现失败在技能目录 requirements.txt 里声明依赖脚本加载时检查环境输出解析不了脚本启用了交互模式或者 print 了多余内容看脚本 stdout 是否严格为 JSON关闭交互日志写入文件stdout 只保留结构化结果技能热更新不生效加载器缓存了旧 skill.md查看加载器是否每次重新读文件用文件哈希做缓存失效或开发模式下禁用缓存多个技能互相干扰同一个操作被拆到多个技能里模型难以区分检查是否有技能重复合并重叠技能或建立技能索引并压缩描述调用超时技能执行内部调用了网络服务或模型查看脚本耗时给脚本设置超时把长时间任务改成异步并返回任务 ID这张表是排查问题的起点不是终点。真正有价值的是把每次故障都记录下来反向补充到技能的skill.md里去。比如某次模型总是把“output_path”填成.txt我就在 schema 的 description 里加了一句“output_path 必须以 .md 结尾”之后这个问题就不再出现。4.2 我的调优路径从 60% 到 95% 成功率如果你刚开始做 agent-skills想快速提升稳定度我建议按下面这套路径走。第一步先建一个黄金测试集。收集 5 到 10 条真实用户请求覆盖这个技能的典型场景和边界场景比如“文件不存在”“空文件”“超长文档”“多个文件合并”等。把这组请求固化成tests/下的 markdown 或 JSON 文件。第二步用脚本批量跑这些用例记录三个指标技能调用率、参数合法率、执行成功率。调用率是指模型是否在应该使用技能时选择了它参数合法率是指模型生成的参数有没有通过 schema 校验执行成功率指脚本本身是否成功返回结构化结果。第三步针对最低的指标专项优化。如果调用率低改 description把它放在决策 prompt 的更前面或者增加一个触发关键词示例。如果参数合法率低升级 schema把模糊字段改成枚举或者给字段加minLength、pattern等约束。如果执行成功率低那多半是脚本逻辑问题断点调试脚本就行和模型没关系。第四步技能版本管理。每次修改后skill.md里的 version 字段要递增。我在仓库里用 git tag 对应技能版本例如doc-summary2.3.0。技能变更后跑一遍整个测试集再决定是否合并到主分支。这听起来很重但一旦技能数量超过十来个没有版本约束一定会乱。第五步让模型给出决策原因。我在决策 prompt 里要求模型在返回 JSON 时附带reason字段比如“因为用户提到要整理多份会议纪要所以选择 doc-summary”。这样即使出错也能从日志里看出模型当时的判断逻辑。这个字段不需要给用户看只用于内部审计和调优。我自己维护的这套技能库从最开始每个技能 60% 上下的小时成功率经过两轮迭代普遍稳定在 95% 左右。剩余 5% 大多发生在用户需求和技能描述差异极大的突发场景这类问题靠堆技能数量解决不了反而应该回到产品设计层面去想是不是该做一个新的技能或者调整任务边界。4.3 给新手的一点避坑建议最后分享几个新手容易忽略的细节。第一技能不要一开始就做得很大很全。我见过有人想做一个“全能办公助手”技能里面塞了文档处理、邮件发送、日程管理、图表生成。结果模型经常选了它却只执行一部分因为技能内部分支太多描述根本覆盖不过来。宁可拆成五六个小技能每个只做一件明确的事。第二脚本的 stdout 要克制。任何print(开始处理...)之类的调试输出都可能在线上成为解析炸弹。我统一规定正常执行只输出一个 JSON 对误码日志写入专用日志文件。第三测试夹具很重要。没有测试夹具你很难判断改动技能是变好了还是变坏了。我习惯在每个tests/fixtures下放一组固定的输入文件和期望输出文件。每次升级技能把夹具跑一遍对比 diff比任何 review 都有效。第四不要完全信任模型返回的参数。即便模型已经把技能选对了参数也可能填错。我在加载器里用jsonschema.validate做一层校验不合法的参数不执行脚本直接返回给模型让它改。这一步至少能挡掉一半的无谓报错。结尾把技能库变成团队的共同资产做 agent-skills 越久我越觉得它的价值不仅仅在于“让 Agent 更聪明”而是让团队的协作方式发生了变化。技能库不再是一两个人的 prompt 草稿而是大家共同维护的标准操作手册。每个技能都有版本、有测试、有说明新同事看一眼就能复用出问题也能快速定位。我个人在实际运行中最受益的一个做法是在每个技能目录里放一个TEST_CASES.md里面手工记录几组“当时为什么会这样设计”的输入输出。这些记录比任何架构文档都真实因为它是实际踩坑沉淀下来的。以后你再往里加新技能或者想重构旧技能一翻这份记录就知道哪些行为是不能动的底线。如果你正打算给自己的 Agent 项目引入技能体系不要急着追求大而全挑两三个最频繁、最痛的任务先做起来让技能先跑通再慢慢沉淀成一套属于你们自己的 agent-skills。