提起agent-skills这个词很多人的第一反应是这不就是给Agent加几个工具吗。但真做过的人都知道工具调用和技能体系完全是两码事。我参与过的Agent项目里从把搜索、日历、报表塞进一个巨型prompt到最终沉淀出一套按需加载的技能库中间踩过的坑能写满一篇长文。这篇文章就把这套agent-skills体系从设计、落地到排障的完整过程整理出来适合正在做Agent应用、被prompt越堆越失控折磨过的工程师也适合刚接触智能体开发、想知道技能和工具到底差在哪的初学者。1. 从堆提示词到技能化为什么Agent能力必须拆开管1.1 单体提示词的注意力稀释问题一开始做Agent最容易犯的错误是把所有能力塞进一个prompt。我的第一个版本就是典型的全能助理一个系统提示词里写了日程管理、邮件回复、查天气、搜索资料、生成报表五个模块每个模块配了完整工具定义和调用示例整体超过5000 token。当时觉得这是一步到位跑起来才发现问题很严重。模型在生成时注意力是有限且跟上下文长度成反比的。5000 token的指令里真正跟当前任务相关的可能只有几百token的信息其余全是干扰。表现就是用户问明天下午有没有空模型先调了天气工具让它回复邮件它把日历的参数套到邮件接口上。最离谱的一次模型自己发明了一个不存在的search_news接口因为它记住了太多工具名在某个上下文里出现了幻觉拼接。这就是单体prompt的注意力稀释。工具描述越多、示例越复杂模型每次做工具选择时需要考虑的空间就越大选错、混用、空想接口的概率直线上升。不是模型变笨了是我们把太多互不相干的知识堆在一起而模型本质上擅长的是从上下文里找最匹配的局部信息。1.2 技能化打破了越堆越错的循环Agent能力拆成agent-skills之后最直观的变化是上下文一下子瘦了下来。每个技能只在被需要的时候注入上下文其余时间系统提示词里只有一个技能清单和一句话说明根据用户意图选择合适的技能没有匹配项时直接回答或请求澄清。这时候模型每次决策面对的候选集从五个大模块、几千行描述缩减到几个技能名一行描述。注意力可以完全集中在意图理解上。实测下来同样一个Agent单体prompt版本工具选择准确率大概85%拆成技能体系后稳定到96%以上而且上下文开销从平均4000 token降到不足500 token。这不是什么神秘魔法本质上是把让模型一次性理解所有事情改成让模型每次只理解一件小事。技能化的另一个隐性收益是故障隔离某个技能出问题只需要替换那一个技能包不会牵连其他功能。以前改一行邮件模块的prompt得重新测一遍整个对话流程现在邮件技能单独改、单独测风险面小了一个数量级。1.3 单体Prompt与技能体系的关键差异拿我自己项目的对比数据来看两者差异非常明显维度单体Promptagent-skills体系单次调用上下文开销4000~6000 token200~500 token工具选择准确率85%~90%96%以上新增能力的成本改大文件全量回归新增一个技能包独立测试故障影响面全部功能可能受影响仅受影响技能被降级或禁用可观测性依赖整体日志分析按技能维度统计调用、成功率这表格不是理论推演是同一批任务在两个架构下跑出来的真实对比。如果你现在项目里的prompt已经开始超过3000 token并且出现工具混用那基本可以确定技能化是你下一步该走的方向。2. 技能的最小完整定义一份技能描述文件的五个字段2.1 五项要素与一个完整示例在动手搭技能库之前得先搞清楚一个技能到底由什么组成。很多人以为技能就是一段prompt这是最大的误解。一套能稳定运行的agent-skills体系里每个技能至少要具备五样东西名字、描述、输入Schema、执行逻辑、元信息。下面是我项目里网页搜索技能的完整定义可以直接复制去改{ name: web_search, description: 在互联网上搜索公开信息返回标题、摘要与链接。适用于时效性信息、新闻事件、产品比价、陌生概念求证等需要外部资料的查询。不适合本地文件检索、用户个人数据等非公开信息。, input: { type: object, strict: true, properties: { query: { type: string, description: 搜索关键词尽量用用户原话中的核心实体意图, example: 2025年大模型开源协议对比 }, max_results: { type: integer, default: 5, minimum: 1, maximum: 10 } }, required: [query] }, execute: { type: function_call, endpoint: http://internal/search_api, timeout_seconds: 8 }, version: 1.2.0 }这里每个字段都有它的存在理由。name是机器标识不能带空格和特殊符号否则路由和日志全乱。description是给检索器和模型看的决定什么情况下该用它。input是调用契约strict: true意味着模型只能按Schema生成参数不能自由发挥。execute定义了真正执行时调用的后端。version则是技能演化的版本锚点。2.2 描述的作用方式它其实是一份意图投递说明技能描述是agent-skills体系里最重要的文本但也是最容易被写坏的文本。它同时服务两个对象离线检索器决定候选技能和在线模型决定最终是否调用。所以描述必须同时具备检索友好和模型友好两种属性。检索友好意味着描述里要包含足够的同义词和意图变体。比如web_search的描述不能只写搜索网页那样用户说帮我查一下有什么新闻这牌子口碑怎么样都会召回到奇怪的技能上。我一般在描述里刻意塞进三到四类不同的说法直接意图搜索、检索、落地场景新闻、比价、隐含需求求证、了解。模型友好则要求在描述里写清边界。描述的最后一句不适合本地文件检索、用户个人数据等非公开信息就是边界声明。别小看这句话它能让模型在用户问翻一下我上周的文档时果断不调web_search而不是强行把参数塞进来。写描述时记住一个原则写清不做什么比写清做什么更能防止误调用。这就好比给快递员指路告诉他这不是三号楼是隔壁四号楼往往比反复说这是四号楼更有效。还有一个常见问题叫描述over-promise。技能描述里写了获取股票实时行情但实际后端只能提供15分钟延迟数据模型就会因为信任描述而向用户输出实时——这已经不是调错工具的问题而是数据准确性问题。所以描述必须严格反映真实能力边界宁可保守不可夸大。2.3 Schema把模型自由发挥挡在外面输入Schema是Agent技能体系里真正约束行为的硬边界。没有Schema的时候模型可以自由决定参数结构而下游代码根本无法解析这种自由。我遇到过模型把日期传成明天下午而不是时间戳、把数字传成几个、把布尔值传成字符串的各类情况。解决办法是用JSON Schema的strict模式加上类型、枚举、范围约束。除了字段类型我强烈建议加example字段。example的作用不仅是给模型看更重要的是在检索阶段——当用户query与example重合度高时检索器会给出更高相似度得分。这相当于给每个技能内置了原型案例。输出端同样要约束。我在所有技能执行结果里统一包一层{ status: ok, data: {}, error: null }失败时status为error并附上简短原因。这个统一输出结构看似简单却在技能组合时救了我很多次——后一个技能不需要感知前一个技能的具体形态只要读status和data字段即可。3. 技能库的设计目录结构、检索策略与规模化管理3.1 从单文件技能到目录化技能包技能少的时候可以放在一个JSON文件里但随着数量增长单文件会变成灾难。我现在的项目里技能全部按目录组织每个技能一个文件夹skills/ web_search/ SKILL.md schema.json main.py tests/ positive_case_1.json negative_case_1.json calendar/ SKILL.md schema.json main.py tests/ sls_report/ ... common/ memory_reader/ auth_validator/SKILL.md是给人读的设计文档schema.json是给机器读的契约main.py是执行逻辑tests里放验证用例。这种结构的好处是每个技能可以独立版本化、独立CI、独立责任人。对超过50个技能的规模来说没有目录化基本不可能维护。组合技能可以单独开辟一个目录放组合层逻辑它本身也注册为一个技能但execute字段指向的不是后端API而是内部技能调度器。3.2 让检索器能听懂的技能描述习惯技能库建好之后下一步是让系统在用户提问时找到合适的技能。我用的是混合检索向量相似度 关键词覆盖度加权。def recall_skills(user_query: str, k: int 5) - list[ScoredSkill]: query_vec embed(user_query) results [] for skill in skills_registry: desc_vec embed(skill.description) vec_score cosine_similarity(query_vec, desc_vec) kw_score keyword_overlap(user_query, skill.keyword_list) combined 0.7 * vec_score 0.3 * kw_score results.append(ScoredSkill(skillskill, scorecombined)) results.sort(keylambda x: x.score, reverseTrue) return results[:k]这里有个细节容易忽略embedding的对象不是用户query和你描述的整段话直接比较因为描述里有大量跟意图无关的执行细节。更好的做法是给每个技能单独维护一个意图向量库——把描述里的意图部分、场景部分、边界部分分开向量化检索时只对比意图部分。这样技能描述即使写得很长也不会因为掺入了执行细节而稀释意图相似度。关键词部分我用的是精简版BM25索引技能描述里的实义词。效果上混合检索比纯向量召回在技能数量过百时精确率提升明显原因是向量相似度在语义接近但实际不相关的技能之间经常给出虚高分数关键词能够起到否决和校准作用。3.3 注册表、命名空间与技能过时清理技能规模上到几十个以后必须有一个全局注册表。我的做法是生成一个skills_index.json启动时加载到内存包含每个技能的元数据和当前状态{ total: 87, skills: [ {name: web_search, version: 1.2.0, group: basic_search, status: active}, {name: weather_today, version: 0.9.2, group: daily_qa, status: deprecated} ] }命名空间规范也很重要。搜索类技能可能同时有web_search、wiki_search、news_search不做前缀管理会乱。我统一用{领域}_{动作}命名group字段做聚合便于在路由时按组批量禁用。另一个容易忽略的问题是过时技能清理。技能常年0调用不代表没用但应该进入deprecated列表并停止进入检索范围。我每个季度跑一次调用统计把近90天调用次数为零且无明确计划的技能下线。保持技能库精简检索准确率才会稳定。4. 运行时路由与组合编排让技能在正确的时机被调用4.1 三层路由召回、确认、兜底技能检索召回了候选集但要不要真调用不能只看相似度分数。我的线上路由设计成三层def route(user_query: str): hits recall_skills(user_query, k5) if not hits: return None if len(hits) 1 and hits[0].score 0.55: return hits[0].skill confirmed llm_confirm(user_query, [h.skill for h in hits]) if confirmed and confirmed.score 0.35: return confirmed.skill return None第一层是向量关键词混合召回拉出top5候选。第二层是单候选且分数足够高时的快速通道省一次LLM确认调用。第三层是多候选或分数偏低时的LLM二次选择。有人会问为什么相似度都够高了还要LLM确认不是多此一举吗因为向量相似度只能衡量语义接近衡量不了用户意图是否真实落在该技能的输入范围内。用户问帮我搜一下附近有什么餐馆web_search的向量分数很高但也许你的技能库里有个更精确的local_poi_search向量分数稍低。如果直接选最高分Local搜索技能永远得不到调用。LLM确认正好补上这一步它把候选技能描述重新读一遍然后判断用户这句话到底想让我用哪个。这本质上是拿几块钱的推理成本换一次准确率的大幅提升。兜底逻辑同样重要。所有候选技能分数都低于阈值时我不会强行调用任何一个技能。与其调用一个可能不匹配的技能然后把结果搞错不如直接不调用、让模型基于已有上下文回复或继续追问。这个敢于不调用的原则是我踩过无数坑之后才真正想明白的。4.2 技能同台的冲突消解策略技能多了以后最头疼的是冲突。两个技能语义相近描述里都在抢同一个意图。比如news_search和web_search用户说搜下最近的科技新闻两个候选分数差不多LLM确认也可能随机选。我的消解策略是描述里声明专属边界 路由层做优先级 冲突时禁用低频技能。news_search的描述开头就明确写仅用于新闻资讯类查询不处理非新闻类网页信息web_search的描述尾部则加一句新闻类查询请改用news_search。这一步先把大部分冲突按意图分流。路由层再做优先级记录当两个技能进入LLM确认阶段如果模型无法区分就按配置的优先级排序。实测下来这个策略能解决90%以上的同台冲突。剩下10%的情况我会在两个技能里选一个做融合——把它们合并成一个技能用参数区分场景。比如news_search本质上是web_search加了一个source过滤合并后反而更简洁。4.3 组合技能把原子能力编排成流水线单技能能解决一个问题但用户问题往往是一串步骤。组合技能就是为此设计的。我项目里最典型的例子是市场报告生成generate_market_report web_search(query_topic) - data_extract(raw_articles) - chart_paint(clean_numbers, chart_type) - report_compile(analysis_chart, outline)组合技能自身也注册为一个技能description要写得更强——它要在用户直接提出生成一份市场报告时被检索到同时也要避免在用户只问搜索一下XX时被误触发。做组合编排时有一条血泪教训必须在每个环节之间显式对齐Schema。web_search的输出结构是status/data/errordata里有了articlesdata_extract读的必须是articles字段而不是自己重新定义字段。我在每个组合技能里维护一份中间数据契约规定了每个环节的输入输出字段名和类型违反契约直接报错而不是让下一环节猜。中间环节的顺序也是有讲究的。尽可能让最廉价的技能先执行、信息量最大的技能居中、生成类的技能最后。原因很简单先过滤掉无效信息后续每个环节的输入都更干净LLM做判断时也不容易被噪声干扰。5. 一百个技能规模下的踩坑实录四个典型故障排查5.1 技能描述说大话幻觉调用不是模型的问题项目里有个聊天记录总结技能描述里写的是总结所有对话记录。实际上线后发现用户只要说帮我总结下昨晚的聊天模型就会调用这个技能但传给技能的conversation_id指向的只有一个会话片段结果自然乱七八糟。排查链路是这样的先看调用日志发现技能被高频触发但返回结果被用户连续点踩。再看调用参数发现模型每次都传一个最近会话的ID而不是用户真正想总结的完整对话。根因不在模型在技能的description和Schema给了模型一个错误预期——它认为只要涉及总结聊天就是这个技能而这个技能的范围描述又没限定仅限当前会话。修复分两步。第一步把description改成总结指定会话ID的聊天记录仅限当前会话不支持跨会话汇总第二步在Schema里把conversation_id设为required并把字段描述写清楚。改完之后调用量下降了60%但每次调用都精准命中。这件事让我明白一个道理技能出问题第一反应永远先检查自己的描述和Schema而不是怀疑模型能力。5.2 三层调用后上下文膨胀算一笔Token账组合技能跑起来后我一度陷入性能焦虑明明每个环节都挺快但整个链路越跑越慢最后甚至超时。翻日志发现是上下文膨胀。拿市场报告技能链举例模型先跑web_search检索结果回传800 token接着data_extract要把这800 token作为输入然后又要输出自己的800 token到chart_paint时前两个环节的完整输出都在当前上下文中已经是1600 token再到report_compile四个环节的中间结果都堆在那里模型每做一次生成都要重新关注这堆历史产物。Root cause就是我把中间结果全部累积在上下文里。解决办法是每个环节结束后只保留一个压缩摘要。比如data_extract的完整输出700 token我会让它先把自己压缩成150 token的要点列表chart_paint只接收这150 token。三层环环节约下来峰值上下文从4200 token降到900 token单次任务耗时降低接近一半。压缩本身就是技能也有它自己的Schema约束。我在压缩技能里强制要求输出必须是断言式摘要只保留后续环节真正需要的事实不保留根据上文可以看出这类过渡性表述。这招对控制后续环节的幻觉特别有效。5.3 上游接口变更引发的版本漂移这是一个真实事故。某个技能依赖的查询服务升级了接口旧的query参数改成了新的query_t但技能文件里的execute配置还是旧的。结果就是路由正常模型正常调用正常返回却永远是空数据。排查时最迷惑的点就在这里——看起来每一步都没报错但结果从某一天起就出问题了。这类问题的隐患在于技能和外部依赖的耦合是隐式的。技能包里没有记录它所依赖的上游接口版本也没有在调用失败时区分参数错误和正常空结果。我的解决方法是两件事一是在技能元信息里增加dependencies字段显式记录依赖的外部API和版本约束二是给所有外部调用加统一的响应头校验一旦发现上游返回格式异常就直接把技能标记为degraded从路由候选里移除而不是让模型拿着错误结果继续推理。更底层的手段是给技能做CI。我为每个技能维护一组测试用例在CI流水线里真实调用一次外部接口或用mock验证schema和描述仍然有效。任何一次上游变更导致技能测试失败提交直接被拦住。这比线上出了事故再去回滚成本低太多了。5.4 隐性依赖组合技能里最隐蔽的故障源组合技能还有一个深坑隐性依赖。我在维护报告生成技能链时有一天report_compile突然报Schema校验错误报错信息指向web_search的输出字段。查了半天发现不是web_search改了是data_extract在某个版本迭代里把它的输出字段名从source_url改成了origin_url而report_compile的契约还在读source_url。两个技能各自单测都通过组合起来就崩。这就是隐性依赖的典型场景A技能改了字段B技能没跟上而它们之间没有直接的test关联。现在的项目里我强制给每个组合技能维护运行时契约快照——记录每一层的实际输出样例和字段名任何一层输出结构与快照不匹配立即报警。同时我给所有组合链路加一个trace_id贯穿全链路每步输入输出都落盘。出问题时只要按trace_id把链路日志拉出来一眼就能定位是哪一步Schema变了。6. 技能的健康度评估与持续演化6.1 每个技能都要有自己的评测集管理上百个技能最怕的是凭感觉改。我现在的做法是每个技能维护一个评测集3~5个正例应该被该技能匹配的query和2个负例不应该匹配的query。改动任何技能的description或Schema时都要跑一遍带技能路由的完整评测。正例主要验证召回率——优化某个技能后它还能不能稳定匹配到目标场景。负例专门防幻觉——描述改宽了以后是不是把不该接的任务也接进来了。我印象最深的一次某个技能description里加了一句也可以处理数据统计结果是正例全过、负例全崩。没有负例评测这种描述污染根本发现不了。6.2 用健康度指标決定技能去留技能也需要养生。我每个月从日志里拉一批健康度指标按技能维度统计指标含义动作阈值调用成功率技能执行并返回statusok的比例低于90%进入告警参数校验失败率Schema校验拒绝的比例高于10%检查Schema结果使用率模型是否真的引用了技能返回的数据低于40%考虑重写描述零调用时长连续未触发天数超过90天进入deprecated结果使用率是我后来才加上的指标。之前只关注调用成功率后来发现有些技能调用成功了但模型在生成回答时根本没用它的返回结果——这说明描述里承诺的能力和实际输出严重不一致对用户是纯干扰。这个指标比成功率更能反映技能的真实价值。6.3 从技能化到Agent编排我的进化路线回头看我自己的实践路径有一条清晰的进化线单体prompt → 技能化 → 组合技能 → 多Agent编排。技能化解决的是能力如何被正确调用组合技能解决的是多步骤任务如何被组织再往后做多Agent其实是把不同领域的大组合技能分配给专职Agent去执行。我个人目前最实用的体会是不要把技能化当成一步到位的架构革命它更像是一次重构——先把现有的混乱prompt拆成最小可用技能跑通一条链路再逐步扩展。每加一个技能都要过一遍是否真的不可复用、是否能用已有技能组合出来、描述边界是否清晰这三问。技能库不是越庞杂越好而是越准越好。最后分享一个我后来养成的习惯给每个技能写一行发布说明记录它在这个版本里为什么改、改了什么、验证了什么。三个月后再回头翻这行记录比任何文档都好用。如果今年只能给agent-skills项目留一条经验我会留这条。