写了两年多Agent应用我越来越确定一件事Agent的上限不由模型参数决定而是由它“能可靠调用多少技能”决定。早期我把技能想简单了以为多写几个函数、把函数塞进工具列表就完事。直到三个项目里出现三份几乎一样的PDF解析逻辑我才开始重新拆解这个问题最终沉淀出一套叫agent-skills的技能管理设计。这篇文章就把这套设计的完整思路、文件规范、运行链路和踩坑过程都摊开讲适合正在做Agent应用、或者想给团队成员沉淀公共能力的开发者参考。1. 我为什么放着现成工具不用非要做一套agent-skills1.1 工具调用不是“加几个函数”那么简单先说我最早遇到问题的场景。那会儿我在维护一个文档问答Agent刚开始工具只有三个搜知识库、读链接、解析PDF。模型调用得还挺准。后来团队决定把内部十几个系统的查询能力全部接进来工具列表一下子膨胀到四十多个。Prompt里的工具描述越来越长模型开始频繁选错工具有时候明明该查订单它却调了库存接口返回结果和用户问题完全对不上。这让我意识到一个很本质的问题工具调用这件事表面上是在“给模型加函数”实际上是在“让模型理解每个能力的使用边界”。函数签名只告诉模型“这个函数有哪些参数”但没有告诉它“什么时候该用、什么时候不该用、什么参数是合法的”。当工具数量少的时候模型还能靠函数名和参数名猜个大概工具一多这种隐性的“使用说明书”缺位就会变成灾难。所以我开始把“工具”重新定义成“技能”。一个技能不只是接口定义而是完整的能力封装包含一段面向模型的使用说明、一份严格的参数Schema、一个执行体以及围绕这个技能的错误处理、权限、日志等外圈逻辑。agent-skills这个名字就是想强调这里做的是“模型能用得明白的能力包”而不是“普通的函数注册表”。1.2 现成的Function Calling框架缺了什么说老实话市面上并不缺工具调用方案。OpenAI的Function Calling很成熟LangChain有tool装饰器CrewAI也有自己的工具基类。我也都试过。它们解决了一个共同的问题让模型在对话过程中生成为一个结构化的函数调用参数。但真正落到生产环境我遇到了几个不好绕过去的点。第一个痛点是“工具描述的管理成本和代码耦合在一起”。用LangChain的tool装饰器描述就写在函数定义旁边的docstring里看着方便但一旦工具数量多了谁来更新描述描述改了一个字是不是要发一版代码我更希望把技能描述、参数约束、版本号这些信息独立到声明式文件里让团队里的同学可以像写配置一样维护技能而不是每次改动都去动代码。第二个痛点是“执行之后缺一层收口”。Function Calling只负责把参数从模型手里拿过来但它不关心执行结果是否合格、失败后给模型返回什么样的错误信息。实际跑下来模型经常因为一个含糊的报错文本而进入“道歉循环”反反复复调用同一个工具。如果技能层能把错误码、可重试标志、面向模型的错误描述标准化这个问题就能缓解很多。第三个痛点是“技能组合几乎靠Prompt硬写”。很多时候我们想要的不是单个技能而是“先搜索再摘要再存入笔记”这种流程。用裸Function Calling当然可以在前端代码里编排但编排逻辑写死在业务里技能本身无法复用。我想要的是能让组合关系也变成技能描述的一部分。1.3 agent-skills的边界它不是一个Agent框架做agent-skills的时候我给自己立了一条规矩不重复造Agent框架的轮子。市面上已经有LangGraph、AutoGen、CrewAI这些规划调度框架它们负责的是“Agent怎么思考、怎么决定下一步动作”而agent-skills只负责“技能的声明、注册、加载、调用、评估”这一层。两者不是竞争关系更像是乐高底座和乐高零件的关系。到了落地这一步我更倾向于把agent-skills作为独立的技能仓库放到任何Agent框架里都能接入。底座框架决定Agent的行动策略agent-skills决定Agent手里有哪些趁手的工具、这些工具能不能稳定工作。现在我的项目里规划层用的是LangGraph但技能层完全由agent-skills管理两边通过标准接口对接互相不绑架。这个边界感是做好这套设计的关键。2. 一份技能文件里到底要写什么2.1 最简技能三个字段就能跑agent-skills里一份技能包是一个独立目录里面至少有一个manifast文件和一个执行体。我不要求一开始就把所有字段写满三个字段就可以注册一个最简技能name、description、parameters。但不要被“最简”骗了真正决定技能质量的是description怎么写。先看一个不合格的描述description: 查天气再看一个合格的描述name: weather_query description: - 查询指定城市当天及未来三天的天气情况。适合用户问“今天上海冷吗”、 “北京明天会不会下雨”这类问题。当用户没有明确给出城市时不要使用本技能 应该先通过用户定位或对话上下文补齐城市信息。 parameters: city: type: string description: 城市名称例如“北京”“上海”。两段描述的差别在于只是把函数名翻译成人话还是把“使用边界”写明白了。模型做工具选择的本质是一个文本匹配任务。你给它一段清晰、带触发条件的描述它才能在下游问题到来时把自己正在处理的用户意图和这段描述做匹配。我在几千条真实调用记录里统计过描述里明确写了“什么时候不要用”的技能误召率至少能降三成。2.2 参数Schema要当API文档写而不是当类型定义写第二个容易忽略的地方是参数Schema。理解一句话就够这份Schema不仅仅是给程序做运行时类型检查的它同时是给模型看的一份“参数填写指南”。所以只写type是远远不够的。我最早用Python类型注解定义参数比如def query(a: str, b: int)结果模型一会儿把省份和城市合并填一会儿日期填成“明天”。后来改成JSON Schema并且每个参数都补全了description、examples、enum行为立刻稳定很多。{ type: object, properties: { city: { type: string, description: 城市名要求是标准行政区划名称如北京市、上海市。, examples: [北京市, 上海市] }, date: { type: string, description: 查询日期格式必须是YYYY-MM-DD例如2025-04-15。, pattern: ^\\d{4}-\\d{2}-\\d{2}$ } }, required: [city, date] }这段Schema看起来多写了几个字段但每个字段都有实际作用。description告诉模型语义上该怎么填examples给了一个可以直接套用的模板pattern约束了格式。一条核心经验参数Schema的严格程度应该比后端代码的校验更高因为模型生成参数是一个生成过程它天然倾向于“自由发挥”你必须在Schema层面把自由度收敛住。当然也不是越严格越好。如果enum只给三个固定值而真实业务有几十种可能那模型会频繁生成非法参数反而增加失败率。这里的平衡点是约束住格式和明显的类别错误但对开放值保持适当的宽容。2.3 技能执行体能力与副作用分离声明式文件只描述“这个技能是什么”真正的动作逻辑放在执行体里。agent-skills对执行体只有一个约定实现一个标准函数接收上下文和参数返回统一结构的结果。# skills/weather_query/code.py def execute(ctx, args): city args[city] result ctx.clients.weather.fetch(city, args[date]) return { success: True, data: result, error_code: None, human_message: f{city}在{args[date]}的天气是{result[condition]} }为什么要有这个统一返回结构因为Agent在拿到技能执行结果之后还要把结果“翻译”成自然语言回复给用户。如果你的技能抛出一个原始Exception模型要么看到一个看不懂的堆栈要么只能道歉。而human_message字段是给模型“照抄也无妨”的用户可读文本data是给模型进一步分析的结构化数据error_code则可以用来触发重试或降级策略。同时我把副作用标注进manifest每个技能声明自己是只读的还是写操作side_effect: readonly # 可选 readonly / write / internal_state_change这一步看似多余但当你把技能库里塞进“发送邮件”“创建工单”这类有副作用的操作时它就能作为权限层的依据。Agent在自主决策时如果拿到的是一个写操作技能执行前必须经过授权检查不能像无头苍蝇一样到处乱发。2.4 组合技能把多个技能拼成一个新技能单个技能解决不了复杂任务组合技能才是效率武器。agent-skills里的组合技能不用写代码用steps来声明name: research_report type: composite steps: - skill: web_search args: query: {user_query} output: search_results - skill: extract_content args: urls: {search_results.urls} output: extracted_text - skill: summarize args: content: {extracted_text} output: report final_output: report每一步的输出通过{placeholder}映射到下一步的输入。这相当于把Agent的规划结果“固化”成了一个可复用的流程。组合技能的价值在于你不必每次对话都依赖模型现场编排把那些稳定不变的多步操作变成模式模型选择一次、执行链路就能完整跑完。但组合技能的坑也很多这个放到后面专门讲。先记住一点组合技能越好用越容易被人当“又黑又硬”的流程引擎来用所以一定要给组合技能加上状态记录至少要能说清楚“当前执行到哪一步了”。3. 从加载到执行agent-skills的完整链路3.1 启动时的技能发现与注册agent-skills在进程启动时扫描技能目录每个技能一个子目录以manifest.yaml和code.py为核心文件。目录结构大概是这样的skills/ ├── weather_query/ │ ├── manifest.yaml │ └── code.py ├── web_search/ │ ├── manifest.yaml │ └── code.py └── research_report/ ├── manifest.yaml └── code.py启动时做的事只有四件扫描目录、解析manifest、校验Schema、检查执行体引用。如果某个技能包缺文件或者参数Schema不合法不会启动失败而是会打印警告并跳过该技能。为什么是警告而不是报错因为技能库里可能有旧技能正在逐步迁移一个技能崩了不应该拖垮整个Agent。但这条策略在正式环境会自动变成“告警且监控系统记录”避免技能悄悄丢失而不自知。注册表里存的不只是技能对象还会缓存两份信息一份是给模型看的精简描述另一份是给执行器用的完整配置。精简描述用于技能召回完整配置用于校验和调用两边分开能节省不少上下文的token。3.2 给LLM生成候选技能轻量路由还是全量塞入技能少的时候把所有技能的name、description直接塞进system prompt也是可以的。但一旦技能数量超过二十个全量塞入就会让模型“看不过来”而且描述越长越稀释注意力。我的做法分两层。第一层是粗筛用embeddings召回把用户最近一条消息和技能描述加上几个触发示例做余弦相似度召回TopK个候选技能。这个阶段不在乎精确只求别把完全无关的技能漏掉。第二层是精排把TopK个候选技能的描述塞给模型让模型通过Function Calling的形式选出要调用的技能、生成参数。这里有个很多人问的参数经验TopK到底取多少我测下来取8到12个效果比较好。太少容易漏太多等于没筛。同时要定期看召回的命中率如果某段时间模型经常选中的技能不在召回候选里说明技能描述该更新了。3.3 执行期的三件小事参数校验、权限校验、结果清洗模型生成参数后进入执行器。我最开始跳过了参数校验直接把args丢给执行函数结果各种KeyError和TypeError。后来补上三步链路稳定了一大截。第一步是参数校验用JSON Schema跑一次校验不合格就让模型重新生成参数给一个明确的纠错提示。注意这里不是简单地把校验失败抛给用户而是把校验失败原因翻译成“模型能听懂的指令”比如“date字段格式不对请生成YYYY-MM-DD格式的日期”。这一步能用规则解决就不用模型重试因为重试也占token。第二步是权限校验从manifest的side_effect字段判断操作级别。如果是写操作检查当前对话是否已经拿到对应的用户授权如果没授权不能执行而应该返回一个“请用户确认”的消息。这个设计看起来像在限制模型其实是保护用户和系统避免模型“好心办坏事”。第三步是结果清洗当执行函数返回大段文本、HTML、日志时不能全量回传给模型。我会先把长度截断同时做一次脱敏最后再把清洗后的内容交给后续的LLM处理。模型并不需要看原始文档里的全部字符它只需要足够的信息来做回答。3.4 超时与失败的降级策略单一技能执行必须设置超时时间agent-skills的默认值是5秒但允许技能在manifest里声明自己需要更长的时间比如timeout: 20用于那些真正需要跑长任务的场景。超时之后不能把原始异常作为结果而要转换成标准失败响应。关于失败我踩过一个很深的坑早期对失败结果一律返回error_code: EXECUTION_FAILED模型收到这个错误后总是道歉。后来我把错误码细分成了几类错误码含义模型应该怎么做INVALID_ARGUMENT参数不合规根据纠正信息重新生成参数UNAUTHORIZED权限不足向用户请求授权不硬执行EXECUTION_TIMEOUT执行超时判断是否需要缩小范围后重试DEPENDENCY_FAILED依赖服务失败不要无脑重试建议用户稍后再试BUSINESS_RULE_REJECTED业务规则不允许用human_message说明原因模型看到细分后的错误码至少知道下一步是重试、换技能还是向用户求助。这比把十几个单词的异常堆栈丢给它强太多。4. 我在真实Agent项目里反复踩的四个坑4.1 技能描述太详细模型反而不会选我一开始写技能描述时老想着“把话说清楚”动不动写几百字把边界条件、内部实现、参数细节全塞进去。结果模型在实际调用时经常漏掉重要技能反而选中一个看似相关但并不适用的技能。后来我对照模型日志做了归因发现问题不是模型笨而是长描述稀释了关键信息。模型做工具选择时注意力是有限的。它不读你写的小作文它是在快速检索“这段描述和用户当前意图是否匹配”。所以我现在写description有一个“三条线”原则第一句说明技能是什么答一个能做什么第二句写典型的触发场景用“当用户问……”开头的句式第三句写明确的反例用“当……时不要调用”。三段总字数控制在200字以内剩下的空间留给examples。对比一下description: 查询订单状态。 当用户询问已下单商品的物流、发货状态、签收时间时使用。 当用户只是在泛泛地问“你们发货快不快”时不要调用本技能这是咨询类问题。这样写模型很容易把用户问题映射到“要查询”还是“要咨询”。把边界写清楚比把功能写详细更重要。4.2 参数Schema太宽松报错全堆给用户还有一次线上事故让我印象很深一个日期查询技能的日期参数只写了type: string没加任何格式约束。用户问“明天有没有会议”模型生成参数{date: 明天}执行体解析日期失败返回了一个datetime解析异常给用户。用户看到的回复是“抱歉我处理不了”体验极差。后来我把所有参数的约束全部收紧。日期统一要求YYYY-MM-DD枚举类参数尽量给enum数量范围给minimum、maximum。如果模型收到的参数不符合约束agent-skills会直接拦截并返回一句“请在参数date中填入格式为YYYY-MM-DD的日期”。这样模型只需要重新生成一次正确的参数而不是让用户背锅。我的体会是参数Schema是Agent系统里性价比最高的“守门员”。它写起来枯燥但能挡掉大量模型“灵性发挥”带来的问题。一个合理的Schema应该让一个不熟悉业务的新人看了也知道该怎么给模型填参数。4.3 组合技能失败的中间状态怎么处理组合技能看起来只是把几个技能串起来但一旦失败麻烦就来了。举个例子我有一个“生成周报并发送邮件”的组合技能流程是“读项目进度 → 格式化周报 → 调用邮件发送”。第一次运行时第三封邮件发送成功但模型因为超时没有收到返回于是重试了整个组合流程导致同一封邮件被发了两次。这个坑让我明白组合技能不能无脑重试必须带幂等和状态记录。agent-skills的做法是给每个组合技能实例生成一个execution_id同时支持声明哪些步骤是幂等的。如果某一步失败需要重试就只重试那一步如果整个组合要重跑就先把已经生成的邮件内容缓存起来直接复用而不是再次调用邮件接口。如果你也在做组合技能我建议尽早把“到底哪些动作会产生外部副作用”这个账算清楚把有副作用的步骤和没有副作用的步骤分开。没副作用的步骤随便重试有副作用的一律加幂等键。4.4 只测正常路径上线后模型一通“灵性操作”最后一个坑来自测试方法。早期我写技能测试时用的都是我自己手动构造的“标准参数”测试用例跟模型的真实输出完全脱节。结果技能开发的时候跑得通一上线模型立刻给我表演了一波“灵性操作”把城市填成经纬度、把百分比填成小数、把“昨天”当日期传进来全是靠正常路径测试永远发现不了的问题。后来我建立了一个回归测试集做法很简单把线上模型每次真实调用的入参和结果全部记录下来每周抽一遍失败的案例整理成“丑参数集”跑一遍agent-skills看校验层能不能挡住执行体能不能处理。如果某个案例被新版本Schema挡住并给出合适的纠正提示就说明这个坑被有效堵住了。这个“用真实生成数据反哺测试”的过程和普通的单元测试完全是两码事。我的建议是任何技能上线前先跑一遍历史真实调用记录用至少30条失败样本做回归。如果没有历史样本那就先运行两三天不开量攒够样本再放量。5. agent-skills的下一个要做的和给你落地的最小路径5.1 让技能库学会复盘失败日志驱动的技能迭代agent-skills目前有一套简单的日志回放机制每个技能的调用记录会包括是否成功、耗时、错误码、模型重试次数、最终是否解决。这些数据我不只看还会用来做周期性复盘。比如每个周末跑一个排行找出调用量最大但成功率最低的技能优先优化。这个复盘机制已经帮我发现过一个很有意思的问题。某个搜索技能成功率只有60%大部分失败来自超时。检查日志发现是模型经常把一个很宽泛的搜索词扔进去导致后端检索了太多内容。优化方式是给搜索词参数加一个max_length限制同时提示模型拆分成多个更具体的搜索。改完之后成功率从60%升到85%以上。技能优化不一定靠重写代码很多时候是改参数约束、改描述、改错误处理的工作。下一步我想做的是“自动建技能”。当日志里连续出现多次相同的失败模式比如模型反复说“没有工具可以处理这个问题”由系统提取相关的工具调用意图生成一个候选技能描述交给开发者确认。这算是一个轻量级的自学习闭环目前还在验证阶段但至少方向是对的技能不能靠一次开发管终生它得跟着使用情况一起演化。5.2 如果你想照着做第一周应该只做这几件事很多人会问我agent-skills看起来不复杂我到底该怎么开始我一般给出一个很收敛的最小路径避免别人一上来就做大而全的技能管理平台。第一周只做三件事。第一挑你当前业务里最常被模型调用的三个能力不管是用Function Calling还是裸函数写的把它改造成“manifest.yaml code.py”的标准结构。第二写一个最简单的注册器启动时扫描目录把描述打印出来确认能加载就够。第三把它接到现有Agent的工具调用层跑一周只做日志采集不急着做权限、组合、沙箱这些加分项。等跑了一周你大概率会遇到几类问题有些技能模型总也选不准、有些参数一直生成失败。带着这些真实的问题去迭代比闷头写一个复杂的框架靠谱得多。我在实际项目中见过太多团队一开始就想做一个“全功能Agent平台”结果三个月后连第一个技能都没上线。5.3 说点掏心窝的维护心得最后分享一个个人体会。技能库这种东西一旦跑起来它就会像一座慢慢变大的城市。新增技能很容易但每个技能都有生命周期会有过时、废弃、被新技能替代的一天。我会定期给技能标注状态active、deprecated、retired。deprecated的技能不再给模型候选但代码还留着方便回溯retired的直接删除同时保留一份归档索引。技能不是写得越多越好。模型的能力边界很大程度取决于它会不会在正确的时间调用正确的技能一个臃肿的技能列表反而会干扰模型决策。我后来养成的习惯是每次不管多小的Agent改动都会先问一句这个能力如果下个月还要用它该以什么形态留在agent-skills里把技能当作产品来维护而不是当作一次性胶水代码这是我折腾完这套东西之后最想分享的一句话。