
做Agent系统这些年我越来越觉得“能力边界”这个词很虚。你堆了一堆工具函数、写了几百条Prompt真正跑起来还是笨手笨脚——不是不知道调哪个工具就是工具给不到点子上。真正让Agent变得“好用”的反而是一个经常被忽略的工程动作把Agent能做的事拆成一个一个结构清晰、描述完整、可独立验证的技能Skill。我最近在做的这个“agent-skills”项目就是干这件事的。它不是一个大模型也不是一个框架而是一套Agent技能的组织规范和落地实现。你可以把它理解成给Agent配了一本“操作手册目录”每一条技能都写清了什么场景能用、需要什么输入、执行哪些步骤、产出什么结果。这篇文章把我从零搭这套技能体系的过程、踩过的坑、以及沉淀下来的工程经验完整拆开讲希望能给正在做Agent开发、或者正在纠结“工具该拆多细”的朋友一些参考。1. Agent Skills究竟是什么概念拆解与设计逻辑1.1 从工具列表到技能体系的演变早期做Agent最常见的做法是把所有能力塞进一个functions列表里比如send_email、search_db、generate_report每个函数写一段descriptionAgent根据用户意图去匹配。问题很快就暴露了函数一多描述互相干扰Agent经常选错工具更麻烦的是很多任务根本不是“一次函数调用”能搞定的它需要多步推理 多次调用 中间状态判断比如“把昨天的销售数据整理成PDF发给老板”这背后至少要查库、聚合、格式化、生成文件、发送邮件五步。单一工具描述承载不了这种复杂任务。Agent Skills解决的是这个问题。一个Skill不再是一个函数而是一整套输入输出定义 执行指令 参考示例 依赖资源的组合。它告诉Agent在什么场景下启用我、需要哪些信息、按什么流程走、中间可以用哪些底层工具、最后以什么格式交付。这样一来Agent面对复杂任务时不再是“大海捞针式选函数”而是“翻目录找技能”找到之后按技能内部的操作手册一步步执行。1.2 技能化带来的三个核心变化第一个变化是复杂度被收拢了。以前多步逻辑散落在Agent的推理层每做一步都要重新让模型思考“下一步干嘛”。技能化之后多步逻辑固化在Skill内部Agent只需要在技能入口做一次判断后面按说明执行就行推理负担大幅下降稳定性能明显提升。第二个变化是能力可以独立测试和复用了。一个Skill就是一个工程单元我可以单独喂给它各种样例输入验证它在边界情况下的表现不用每次都得把整个Agent跑起来。同事之间共享技能也方便多了把技能目录丢过去对方一注册就能用。第三个变化是失败边界更清晰了。以前Agent做错一步很难定位是模型理解错了还是工具写错了。技能化以后输入校验、执行中间态、输出校验都有了明确的检查点哪一步挂了、为什么挂一眼就能看出来。2. 一个严谨的技能应该长什么样目录结构与规范拆解2.1 核心文件SKILL.md 的内容组织我参考了业界比较通用的一套做法每个技能一个独立目录目录里必须有一个SKILL.md作为技能入口。这个文件不是给人看的README而是给Agent“读”的操作说明书。一个合格的SKILL.md至少要包含五块内容技能名称短、唯一、语义明确例如format_json、send_daily_report不要起花名。适用场景描述写清楚“在什么情况下使用本技能”这部分是Agent做技能匹配的主要依据宁可啰嗦也不要含糊。例如“当用户要求对一段JSON内容进行格式化、校验或压缩时”。输入参数定义列出所有可能用到的参数、类型、是否必填、取值范围、示例值最好用JSON Schema格式这样Agent拿到之后不用猜。执行步骤说明按顺序写清楚执行流程包括中间需要调用哪些子工具、判断条件、异常处理方式。注意这里不是写死代码而是给Agent一个“流程约束”让它在框架内自由发挥。输入输出示例至少给2到3组完整的示例从用户请求到最终输出的全过程。大模型是“少样本学习”动物示例给得好执行准确率能差出一大截。我习惯的目录结构是skills/ format_json/ SKILL.md src/ formatter.py tests/ test_cases.json sample_input.txt assets/ schema_example.jsonsrc里放实际要执行的工具代码tests里放验证用样例assets放辅助资源文件。这个结构的核心思想是描述与实现分离测试与运行分离。2.2 技能参数定义的工程细节参数定义是整个技能里最容易马虎、却最影响效果的部分。很多Agent技能不好用不是模型不行是参数定义让模型没法“对齐”。举个例子我要做一个query_sales技能参数如果只写参数1date_range字符串必填那Agent大概率会在传最近一周还是2025-01-01~2025-01-07之间纠结。但如果我把定义写得稍微细一点date_range: type: object required: true properties: start: type: string format: date desc: 起始日期格式YYYY-MM-DD end: type: string format: date desc: 结束日期格式YYYY-MM-DD不能早于start模型几乎不会理解错。经验是参数类型的约束越具体模型犯错率越低。字符串只有“类型”没有“格式”就等于让模型自由发挥十次里大概率有一两次发挥跑偏的。另外我要强调一点不要让Agent去“硬编码”一些本应由程序决定的参数。比如当前日期、系统路径、API密钥这些应该由运行时环境注入而不是写在技能描述里让模型猜。模型猜不了的就明确标注“运行时自动注入无需在输入中提供”。2.3 技能落盘资源文件与代码管理除了SKILL.md技能还要妥善管理实际执行代码和资源文件。这一块我的经验是三个原则其一执行代码尽量做成与框架无关的纯函数。不要把Agent框架的API耦合进src里的业务代码这样技能才能做到“一次编写多处注册”。比如format_json技能里的formatter.py就是纯Python函数跟Anthropic、OpenAI或者自研框架都没有关系谁调用它都行。其二测试用例要跟技能一起走。每加一个技能我至少会配3到5组测试用例覆盖正常输入、边界输入、异常输入。后续重构技能时跑一遍测试就知道有没有改坏。其三所有技能文件大小要有上限控制。SKILL.md最好控制在8KB以内资源文件不要超过几十KB。因为Agent在每次技能匹配时都会把这些内容读进上下文太大既浪费token又会让模型在关键信息上“分心”。3. 从零实现一个可用技能以JSON格式化工具为例说这么多概念不如直接动手做一个。下面我用一个最简单的format_json技能逐步演示从需求分析到注册上线的完整流程。虽然这个技能看起来很小但它包含了所有核心环节验证方法完全可以横向迁移。3.1 需求定义与边界分析在做之前先想清楚这个技能要覆盖哪些需求场景。format_json至少要应付这么几类请求“把这段JSON排版一下”格式化美化缩进“这个JSON串能不能帮我压缩成一行”压缩去掉空格换行“帮我检查一下这段JSON有没有语法错误”校验报出具体错误这三类行为的输入输出形态不一样格式化和压缩都是以字符串为输入、以字符串为输出校验则是输出结构化结果。为了不把技能做大我在设计时把它们统一到一个入口里——先解析再根据用户意图决定是格式化输出、压缩输出还是只做校验。边界情况也要提前想清楚输入为空或不是JSON怎么办JSON嵌套层数很深比如20层以上还要不要美化输入里包含中文字符和非ASCII字符要不要做ensure_ascii处理我把这些边界的处理方式写进了技能描述里Agent执行时就知道什么样的输入该走哪条逻辑线。3.2 实现核心逻辑与输入校验实际代码非常简单核心就是利用Python标准库json。我这里最想讲的是输入预处理这一环——99%的“JSON技能不好用”都是因为没做预处理。用户给Agent传JSON时经常会出现以下情况用了中文引号“”而不是英文引号字符串里有转义的换行\n被直接换行显示JSON外面套了Markdown代码块json有多余的逗号trailing comma我在formatter.py里写一个preprocess函数先把这些噪音清掉import json import re def preprocess(raw: str) - str: # 去除Markdown代码块标记 raw raw.strip() if raw.startswith(): raw re.sub(r^[a-zA-Z]*\n?, , raw) raw re.sub(r\n?$, , raw) # 将中文引号替换为英文引号仅是示例严谨场景需按位置替换 raw raw.replace(“, ).replace(”, ) # 去掉末尾多余的逗号 raw re.sub(r,\s*([}\]]), r\1, raw) return raw.strip()注意这些替换逻辑在严谨场景下可能不够稳妥比如字符串内容里真的含有中文引号所以这只是示例。但它的工程意义在于技能必须能扛住真实用户的脏输入不只是完美样例。格式化主逻辑def format_json(raw: str, indent: int 2, compact: bool False) - dict: cleaned preprocess(raw) try: obj json.loads(cleaned) except json.JSONDecodeError as e: return {ok: False, error: fJSON解析失败: {e}} if compact: result json.dumps(obj, ensure_asciiFalse, separators(,, :)) else: result json.dumps(obj, ensure_asciiFalse, indentindent) return {ok: True, result: result}这里有一个参数要注意ensure_asciiFalse这样才能保留中文原样输出否则所有中文都会变成\uXXXX用户根本没法看。3.3 编写 SKILL.md让 Agent“看懂”的技能说明这个文件是整个技能的灵魂。我贴一份实际用的精简版# format_json ## 适用场景 当用户要求对JSON内容进行格式化、美化、压缩、校验或提取有效内容且输入以一段JSON字符串为主时使用本技能。 适用于以下用户表达 - “把这段JSON排一下版” - “这个JSON报错了帮我看看” - “把这段JSON压缩成一行” ## 输入参数 source: type: string required: true desc: 原始JSON字符串可以包含多余空格、换行甚至可以带Markdown代码块标记。 mode: type: string required: false enum: [format, compact, validate] default: format desc: 操作模式。format表示美化输出compact表示压缩为一行validate表示只做校验不输出美化结果。 indent: type: integer required: false default: 2 desc: 美化时的缩进空格数仅format模式生效。 ## 执行步骤 1. 对source先做预处理清掉Markdown代码块标记、中文引号、末尾多余逗号。 2. 调用src/formatter.py的format_json函数。 3. 如果返回okfalse把error信息直接反馈给用户指出具体错误位置和原因。 4. 如果oktrue按mode返回对应结果。 ## 输入输出示例 输入用户说 “帮我把这个JSON格式化一下{name:张三,age:18}” 输出 { name: 张三, age: 18 }这份描述的写法有几个讲究。第一适用场景写法要覆盖多组同义表达别只写“当用户要求格式化时”要枚举人类会怎么说话。第二枚举值要写清楚不给模型留自由发挥的类型空间。第三执行步骤里要提示处理脏输入因为这才是实战状态。3.4 注册到你的 Agent 框架中技能文件写好之后还要把它注册进Agent的运行环境。这里我以自己用的一个轻量注册函数为例SKILL_REGISTRY {} def register_skill(skill_id: str, skill_meta: str, runner: callable): SKILL_REGISTRY[skill_id] { meta: skill_meta, runner: runner, } # 注册format_json with open(skills/format_json/SKILL.md, r, encodingutf-8) as f: skill_doc f.read() register_skill( skill_idformat_json, skill_metaskill_doc, runnerlambda params: run_json_formatter(params) )注册之后Agent的调度层在决定是否启用某技能时会把SKILL.md的内容拼接到系统提示词里并附上用户当前请求如果模型判断“适用”框架就把用户请求中提取的参数传给runner执行。整个链路就通了。4. 测试、部署与日常维护4.1 三层测试法从单技能到全链路技能写出来能不能跑不能光靠“感觉”。我自己用的是一套三层测试法每一层解决的问题都不同。第一层是单元测试。针对src里的纯函数验证代码逻辑本身对不对。比如format_json函数我会写一份test_cases.json[ {name: 正常嵌套, input: {a: 1}, expect_ok: true}, {name: 空输入, input: , expect_ok: false}, {name: Markdown包裹, input: json\n{\b\: 2}\n, expect_ok: true}, {name: 中文引号, input: {“name”: “测试”}, expect_ok: true}, {name: 尾逗号, input: {\a\: 1,}, expect_ok: true} ]执行测试只需要写一个简单的脚本遍历这些用例调用format_json比对结果是否符合预期。这一层通过说明代码实现没硬伤。第二层是技能指令测试。这里不做代码调用而是模拟Agent的决策场景把“用户请求 SKILL.md”一并喂给大模型看它能不能正确识别技能、提取参数并输出“要不要用这个技能”的判断。很多问题在这一层才会暴露比如SKILL.md里的适用场景描述覆盖不够、参数定义有歧义等。第三层是全链路集成测试。把技能注册到系统中模拟真实用户对话从用户发请求开始到Agent调用技能、返回最终回答全流程跑一遍。这一层还会有意外惊喜比如发现系统提示词和技能描述之间产生了冲突。三层测试各有不可替代性少了哪层都可能把问题留到线上。4.2 部署与灰度节奏技能部署通常不涉及代码发布那么隆重但也别一把梭全上。我的习惯是先在一个内部对话环境里注册新技能用测试人员的小流量跑几天确认稳定之后再放到生产环境先在低流量Agent上使用观察调用成功率和失败率。这里有个细节技能注册要支持热更新但不能静默热更新。我在注册表里增加了一个version字段每次改技能描述、改代码版本号都要递增。这样如果线上效果波动可以快速对比是哪个版本引入的。此外我在调度层加了一个“技能开关”按技能ID可以随时启用或停用出现问题时能做到秒级止血不用重启服务。4.3 维护迭代技能是活物不是石碑很多人的技能做出来就不管了这是大忌。技能的维护频率跟业务需求的变化频率是对齐的。我主要的维护动作包括丰富示例每次用户请求出现新的表达方式只要技能匹配成功就把它记录为一个新示例定期补充进SKILL.md。这样技能见过的“人类说话方式”越来越多匹配率会越用越高。修正失败模式每次Agent调用技能失败我都会分析是哪个环节出的问题。如果是参数提取错就优化参数定义如果是执行步骤描述不清楚就补充判断条件。清理过时内容技能描述里如果出现已经废弃的工具调用方式或不再支持的格式务必同步删掉别让旧信息污染新能力。我在实际项目中前一个月基本每周都要微调一次技能描述后面就稳定到一个月甚至一季度一调整了。5. 我踩过的坑与解决思路5.1 描述写得太短或太长调用都不稳定早期我写技能描述喜欢“精简”以为是让模型读起来不累。结果技能调用率忽高忽低很多该触发的场景没触发。后来我逐渐明白描述太短模型拿不到足够的匹配依据描述太长关键信息被埋在长文本里反馈一样差。现在的衡量标准是以“中等规模、背景信息充分”为最佳。SKILL.md控制在5~8KB开头的适用场景尽量覆盖多组表达中部的执行步骤只写关键判断不写鸡汤。如果某步需要详细说明用示例代替大段文字模型看示例比看说明快得多。5.2 参数定义过宽Agent 乱传参数有一版技能我把一个参数定义为type: string描述是“用户提供的任意相关信息”。结果Agent每次调用时都自作主张把整段对话历史都塞进去直接把上下文撑爆。教训就是参数边界必须物理化不要用自然语言约束要用类型和格式约束。可以配置枚举就配枚举可以用JSON Schema写pattern就写pattern配不出来的情况才交给模型自由发挥。模型不怕规矩多就怕规矩模糊。5.3 技能之间互相“打架”技能库一旦超过几十个就会出现匹配冲突。比如我既有format_json又有extract_json用户说“帮我从这段文字里把JSON提取出来格式化一下”两个技能都可能匹配模型就会随机选一个结果经常不对劲。解决思路是增加“技能优先级”和“互斥声明”。在SKILL.md里加一段## 排他说明 当用户同时需要提取和格式化时优先选用extract_jsonformat_json只处理已明确给出JSON串的场景。同时在注册表里维护一个技能依赖关系图调度层检查到冲突时可以给模型更明确的引导。5.4 上下文窗口被无关技能塞满当时做技能库扩展一口气加了20个技能每个技能的SKILL.md都塞进系统提示词结果一个会话还没开始光技能描述就占了几千token好在上下文越长Agent的指令遵循能力也会明显下降。后来我做了技能“预筛层”先用一个轻量级embedding模型把技能描述和用户请求都向量化每次请求只检索最相关的3到5个技能把这几个技能的内容动态拼入上下文。预筛层本身也有小概率漏选所以还要设计一个兜底如果所有技能相似度都不超过阈值就返回“未匹配”让Agent进入通用对话模式而不是硬选一个。这个改动之后上下文的峰值占用降低了差不多60%指令遵循的稳定性也大幅回升。5.5 技能数量爆炸后的治理技能超过一定规模后除了检索问题还有管理问题。找技能靠翻目录不可行同名或功能重叠的技能不断出现代码也没人维护。我试过一段时间只靠命名规范来约束毫无用处。真正有效的还是收敛机制新技能上线前必须走“技能评审”发现已有技能功能重叠先考虑扩展现有技能而不是新建技能九十天无调用自动标记为废弃再延迟一段时间清理下线。虽然这些规则执行起来有点繁琐但总比几周后面对一个失控的技能仓库要舒服。6. 大规模技能库的管理思路与后续扩展6.1 技能分级核心、普通、实验技能一多不能一视同仁地维护。我把技能库分成三层核心技能与主业务流程紧密绑定调用频率极高。这类技能要求代码测试覆盖率不低于90%变更要过评审并且尽量由专人维护。普通技能高频或中频使用但不是命脉。测试覆盖要求可以放宽到70%变更走正常Review流程即可。实验技能还在验证阶段效果未知。这类技能不向所有Agent开放只允许特定测试环境或特定用户触发验证通过再升级。这套分级的好处是有限精力花在最重要的技能上又不会扼杀试错空间。曾经有个技能原型很粗糙一开始就挂核心层天天出问题后来降为实验技能反而安静地跑出了效果。6.2 技能质量评估指标技能变多后靠人肉感知逐个技能好坏已经不现实了。我目前会为每个技能单独统计几个关键指标指标说明目标参考值调用成功率技能被调用后正常返回结果的比例不低于95%触发准确率发起调用时技能确实适用于该请求的比例不低于90%平均执行时长从发起调用到拿到结果的耗时取决于任务复杂度需持续观察上下文占用技能描述执行中间态消耗的token数尽量稳定不随输入扩大而膨胀人工介入率需要人工修正或补答的比例越低越好如果某技能触发准确率明显下降我先排查是不是最近改了SKILL.md如果调用成功率下降但触发准确率正常基本可以锁定是业务代码或上游依赖出了问题。这种数据驱动的方式比“感觉模型变笨了”靠谱得多。6.3 后续可以扩展的方向这套技能体系目前还处在“半自动”阶段技能的编写、测试、上线还依赖人工。接下来我想做几件让它更自动化的事一是技能自动生成。拿一组历史对话和对应的工具调用记录让大模型自动生成候选的SKILL.md初稿人工只做审校和测试。这样可以大幅压缩从“需求出现”到“技能上线”的周期。二是技能自我演化的反馈回路。线上每次成功或失败的技能调用都被结构化记录定期把失败样本聚合成新的测试用例再喂回测试集。时间久了技能库的鲁棒性会像滚雪球一样越滚越大。三是跨系统技能共享。目前技能目录还是跟特定Agent系统绑定的。如果把SKILL.md做成一种开放格式并在注册层实现协议剥离那技能库就能在不同Agent系统之间迁移甚至可以做行业内共享。这件事听起来很远但每一步都是往更标准化的方向走。根据我这段时间做agent-skills的体会最核心的一点心得是Agent的能力上限其实是由底层的技能组织方式决定的。模型再聪明如果技能描述含糊、参数粗糙、测试缺失到实战中一样表现拉胯反过来技能工程做扎实了哪怕模型能力普通整体任务完成度也能显著提升。最后再分享一个小技巧写SKILL.md时试着把它当成“给一个聪明但没做过你这行工作的实习生看”的操作手册把那些你以为“不用写”的背景信息、边界情况和判断标准都写进去。用这个标准来要求每一个技能整套系统的高质量就顺理成章了。