
上个月我把手上的助手项目从“一个系统同一套提示词跑到底”改成“按技能组织逻辑”之后我才真正意识到一个问题大部分Agent做不出来不是模型不够聪明而是压根没有把能力拆成可管理、可复用、可测试的单元。项目标题起得很直白就叫“agent-skills”它解决的问题也很朴素——当你手里这个Agent需要具备搜索、计算、查询、报表、定时推送等十几项能力时靠一份超长系统提示词去约束大模型注定会失控。这篇稿子就是来讲清楚我在这套体系里做了什么、为什么这样做、以及哪些坑是你看文档绝对看不到的。它适合已经跑通过一个简单Agent、想把它推进到生产可用状态的开发者也适合把Agent能力当成产品资产来管理的团队。我理解很多朋友看这类项目时会习惯性问一句不就是给模型多塞几个工具函数吗最开始我也是这么想的但真正把仓库结构建起来之后才发现问题根本不在“多塞几个函数”而在于——你的系统里到底有没有一个东西能回答“这个技能什么时候该被触发、参数怎么才算合法、输出以什么结构返回、失败了往哪儿退”。这才是agent-skills这个概念真正的分量所在。1. Agent的本事不取决于模型取决于“技能系统”先说一个反直觉的结论仅仅换更强的大模型并不会让你的Agent质变。去年我在一个内部自动化场景里做过对照实验同一套任务流程分别用两个不同规格的模型来驱动差距是有但远没有把技能体系理顺前后的差距大。原因是模型负责的是“理解力”而技能负责的是“确定性”。一个Agent能不能稳定地完成“查会议室—订时间—发通知”这样的链条取决于每个环节是否有可验证、可重试、可追溯的执行单元而不是模型的临场发挥。1.1 同样的模型为什么换个做法效果天差地别我当时踩过一个比较典型的场景。最初版本里我把所有能力描述直接堆进系统提示词让模型自己去调用一个唯一的总接口总接口内部再分支。结果就是模型理解得好的时候一切正常理解偏一点就开始传错参数、漏掉必要前置操作、甚至自己编造出不存在的功能名。最折磨人的是这类错误没法复现你在调试环境把上下文补得再完整生产环境的用户输入一复杂问题又冒出来了。所以“技能”这个概念被我重新定义成它不是一个函数而是一个完整的能力单元。每个单元的名字、触发条件、参数结构、输出格式、超时策略、失败回退路径全部单独写在配置里甚至单独维护版本。这样一来模型降级为一个“调度者”它只负责判断该调用哪个技能、该往技能里填什么参数而不再负责“假装记住某个功能的全部实现细节”。1.2 技能的三个层次原子技能、组合技能、工作流我把技能体系分了三个层次这套分法在后来的维护里帮了很大的忙。原子技能不可再拆的基础能力比如“查天气”“查时间”“执行SQL查询”“发送邮件”。这些技能对应到代码里就是一个个独立函数输入输出结构清晰。组合技能由多个原子技能拼装而来的能力比如“生成日报并发送”就包含取数、排版、发信三个原子技能。组合技能内部可以有一些固定的编排顺序。工作流跨多个组合技能的复杂过程比如“每周五下班前生成周报、发给直属领导、并把重点内容同步到群公告”。工作流关心的不只是技能还包括触发时机、冲突处理、人工审批节点。在agent-skills这个项目里我只重点把前两层做扎实了。工作流那一层没有过度设计因为一旦任务复杂到那个程度问题就明显超出了“技能定义”的范畴进入状态机和并发控制的领域了硬塞进技能模块反而会让体系变得臃肿。1.3 什么样才算一个“好技能”标准其实不复杂就五条。一名字要能精确表达能力边界比如“获取可用会议室列表”就比“查询信息”好。二输入参数必须有明确的schema校验宁可拒绝调用也不能接受脏参数。三输出必须是结构化数据而不是一大段散文让模型能稳定提取关键字段。四技能要能单独测试不依赖外部环境的意外状态。五技能要有可观测性调用前、调用后、失败时都要能记录到信息。这五条每一条都是我从失败案例里反推出来的。早期版本里我最轻视的就是输入参数校验结果模型在某个场景里传了个明显不对的日期格式把下游系统搞出脏数据排查了半天。后来我把所有技能的入口统一改成JSON Schema校验不合格的参数直接返回明确错误码错误码本身就是一个结构化输出模型自然就知道该怎么修正了。2. 一套能落地的“技能定义”长什么样聊完理念直接上手给大家看一个技能定义的真实结构。在agent-skills里我没有用特别的DSL而是用了一份非常朴素的YAML加几个函数组合来完成技能注册。这里的关键不是技术选型花哨而是信息完整。skill_name: booking_conference_room description: 根据日期、时段、人数查找并预订可用会议室 category: atom enabled: true trigger_hint: 当用户表达出需要预订或查找会议室时 parameters: required: - date - start_time - end_time - capacity optional: - building - preferences schema: date: { type: string, pattern: \\d{4}-\\d{2}-\\d{2} } start_time: { type: string, pattern: \\d{2}:\\d{2} } end_time: { type: string, pattern: \\d{2}:\\d{2} } capacity: { type: integer, minimum: 1, maximum: 100 } building: { type: string, optional: true } output_schema: success: { status: string, room_id: string, floor: string, confirmed_time: string } failure: { status: string, error_code: string, message: string } fallback: on_timeout: retry_once_then_return_timeout_error on_validation_error: return_schema_error_with_help_message2.1 为什么必须有trigger_hinttrigger_hint这个字段是我自己加的它不参与最终的逻辑判断但会注入到调度提示词里。它的作用是给模型一个明确的触发线索避免模型在相似能力之间摇摆。举个例子系统里如果同时存在“查询会议室”和“预订会议室”两个技能它们的名称很接近模型很容易选错。加上trigger_hint之后模型看到“预订”时会优先走向booking技能看到“查询”时走向query技能。这不是什么高级机制本质上是给模型多喂了一条决策提示但效果非常显著技能命中准确率肉眼可见地提升了。2.2 状态应该被封装在技能内部在设计技能参数时我坚持一个原则尽量把计算所需的状态全部放进参数里不要依赖技能从外部环境读变量。换句话说技能最好是一个无状态函数给它什么输入它返回什么结果不偷偷读取什么全局变量。这在实际工程里意味着如果一个技能需要知道“当前用户是谁”那调用方就应该显式地把user_id放进参数字段而不是让技能从某个全局session里自己抓。这样做的好处是调试时你可以直接拿一条参数去复现问题不用还原整个会话。我见过太多Agent项目死在“本地正常、线上偶发”这个阶段根源十有八九就是隐式全局依赖。agent-skills的理念就是尽量消灭隐性依赖。2.3 技能注册表与校验技能不能只是散落在代码里的函数必须有一个集中注册的地方。我在项目里维护了一份技能注册表每条记录至少包含技能名、入口函数引用、参数Schema、输出Schema、启用状态、版本号。启动时系统会遍历注册表做一次静态校验。这个静态校验我非常重视。它在Agent真正被调用之前就替你拦下一批低级错误比如参数Schema里声明了某个字段必填、但解析代码里根本没用到或者技能入口函数不存在或者输出Schema里缺少必填字段。这些错误如果在运行时炸开会让排查非常痛苦而静态校验把它们变成启动即报错成本极低收益却很高。2.4 再往下走一步组合技能的编排组合技能在agent-skills里的实现方式也很直白一个组合技能内部维护了一个有序的子技能列表每个子技能依赖前一个技能的部分输出作为输入这些映射关系在组合技能的定义里写死。我还是用“生成日报并发送”来举例这个组合技能内部包含三个子技能拉取数据、渲染内容、发送邮件。拉取数据的输出是JSON渲染内容的输入是JSON发送邮件的输入是渲染后的字符串。我指定了明确的字段映射规则驱动器会按顺序执行并在任意一步失败时走该步的fallback。这里有一个容易被忽略的决策组合技能的“顺序”和“失败处理策略”必须写在配置里而不是由模型临场决定。如果让模型临场编排你得不到一个稳定的行为基线。反过来把编排逻辑固定了即使某一次单步的结果不太完美只要每步都可控整个组合技能的行为就是可预期的。3. 技能从定义到调用运行时究竟发生了什么定义写得再好最终都要落到运行链路里。我这里把agent-skills的运行时流程拆成了四段来展开意图路由、技能匹配、参数填充、执行与验证。3.1 意图路由绝不让模型直接执行技能在agent-skills体系里大模型不是直接执行技能它只做决策判断当前用户请求命中了哪个技能然后输出一个结构化的“技能调用请求”。这个请求包含技能名和参数对象然后由运行时代码真正去执行技能。这样分层有三个直接好处。第一安全控制点在执行器这边你可以对特定技能加白名单、限流、权限校验第二技能返回的错误可以被结构化成模型能够理解的下一条消息模型可以基于错误信息自助纠正第三真正消耗token的对模型推理只发生在意图路由阶段具体技能执行不会产生多余token消耗。3.2 技能匹配相似技能的取舍模型在意图阶段输出的技能名不一定和注册表里的完全一致。所以我不让运行时直接拿字符串做严格匹配而是用一个轻量的模糊匹配层先把模型输出的技能名标准化比如去掉空格、转小写然后和注册表里的技能别名做匹配匹配不到时返回一个“技能不存在”的标准错误并附带可用的技能列表。这个设计是我在调试中摸索出来的。没有它之前模型经常输出一个和注册表略有出入的名字比如把get_meeting_info说成get_meeting_details严格匹配直接就断了体验相当糟。加了别名表和模糊匹配后这类问题基本从运行记录里消失了。3.3 参数填充模型只负责填值不负责判断格式参数填充是一个看起来简单、实际最容易出问题的环节。我的做法是先把参数Schema和技能描述一并注入模型上下文让模型按Schema的字段名和类型去提取用户请求里的信息然后运行时再做一次强制校验不合法就拒绝执行并把校验错误返回给模型。之所以要“运行时再校验一次”是因为模型偶尔会输出字符串格式的日期、或者漏掉必填字段。如果直接把这批脏数据传进技能段错误、空指针、脏数据问题会接踵而来。反过来如果约束在入口处拦住整个链路会非常干净。我在这段逻辑里最想强调的是永远不要相信模型的输出格式校验层不能省。3.4 执行与验证结构化输出和错误码设计技能执行完之后返回体必须是标准化JSON结构我在项目里统一成三字段status, data, error_code。status只有success和failure两个值。data在成功时存放结果对象在失败时为空。error_code存放失败原因代码便于上层进行统计和分类。比较关键的是error_code的设计。我参考了HTTP状态码的思路做了几类参数错误、权限错误、上游依赖错误、超时错误、未知错误。这几种错误码在运行时分别走了不同的处理路径参数错误让模型重读Schema权限错误直接终止不再重试上游依赖错误可以做一次重试超时错误按技能配置决定重试次数。这种分类的价值在于错误码不再只是给人看的文案而成为调度系统的决策依据。提示错误码千万别写成自然语言的长句子比如“上游系统连接失败请稍后再试”。这种错误文案看起来友好但程序没法便捷地根据它做分支处理。标准化短码加独立消息文本才是正确做法。4. 我踩过的坑和后来的处理办法这部分是私货时间。每个做Agent技能体系的人最终都会踩到一些自己的坑我这边挑四个比较有代表性的希望能帮大家少走弯路。4.1 技能粒度过粗导致复用困难第一版agent-skills里我把“获取预订信息并生成日历邀请”做成了一个技能。当时觉得整体很顺手但后来要做“只获取预订信息但不生成邀请”的场景时我只能复制一份代码。这类重复代码一旦多了维护成本就会飙升。后来我把技能粒度调整成“一个技能只做一件事”再把“获取预订信息”和“生成日历邀请”拆开用组合技能把两者串起来。这里最重要的是拆分边界要顺应业务变化的方向而不是顺应当前单一需求的方便。多花十分钟拆解后面能省几个小时的重构。4.2 隐性依赖本地一时爽线上火葬场前面提到过隐式全局依赖这里再展开一个具体案例。早期某个技能需要获取当前登录用户我图省事直接读了一个全局变量开发时一切正常。直到某次并发压力测试多个请求同时触发该技能全局变量互相覆盖用户A的数据跑到用户B的会话里去了。排查过程极其曲折因为错误不是每次都出现而且日志里看不出关联。后来我把所有技能入口统一握手为显式参数列表凡是当前用户、当前会话这类信息一律由调用方在参数里传进来。从那以后并发类故障就基本绝迹了。如果你在自己的Agent项目里看到类似“偶发性串号”问题先查有没有隐式全局状态。4.3 上下文污染技能说明塞太多模型反而失去重点这个坑出在提示词工程和技能定义的接缝处。我一度为了让模型更精准调用技能把每个技能的完整字段说明、示例、边界条件全部塞进上下文结果上下文膨胀得厉害模型反而在长文本里失去重点调用准确率不升反降。解决办法是大幅精简注入内容。每个技能在上下文中只保留技能名、一句话描述、trigger_hint、参数名列表、关键约束。其余细节都收进技能定义文件运行时只在模型发起调用请求后才用到它们。这其实也说明了一个底层道理你想让模型做什么决策就只给它那个决策相关的信息信息过载和信息不足一样危险。4.4 评估体系缺失靠手感做事迟早失控在技能体系搭建初期我几乎没有评估环节。每次改一段技能定义都是自己拿十来个测试用例按一遍感觉差不多就上了。直到有次微调了某个技能的trigger_hint结果导致另一个技能命中率明显下降而我只测了改动的那个技能问题完全没被发现。后来我搭了一套非常简单的回归测试集每个技能准备十到二十条真实用户话术记录正确命中的技能名和参数提取结果每次调整后批量跑一遍。这套测试集不需要什么高级框架纯判断调用结果字符串是否等于预期即可。效果却极其显著它保证了我每次重构都不是在赌运气。还有一些零碎经验也值得记一下技能命名要选动宾结构避免两个技能名字都叫“信息查询”之类技能依赖的上游接口要单独做健康检查技能自身可以报错但一定要让错误路径可控所有技能调用必须有日志和trace_id这样排查问题时能还原完整链路而不是面对一堆没有关联的记录。5. 从agent-skills延伸出来的几个实践建议在做完这一整套技能体系之后我对Agent工程化有了几个明显的体会写在这里算是给同样在摸索的开发者一个参考。第一个建议是技能体系一定要和模型解耦。今天你用的是某个模型明天可能换另一个品牌、换一个规格如果你的技能定义全部耦合在提示词细节里迁移成本会高到让你放弃升级。agent-skills的思路是把所有技能的定义、校验、执行、错误处理都下沉到框架层模型只保留一个决策职责这样换模型时技能层基本不动。第二个建议是从第一天起就把技能当资产来管理而不是当临时脚本。技能要有版本号、有作者、有变更记录、有调用统计。如果你只是写给自己用的小工具这套流程看着繁琐可只要你的Agent会在无人值守环境里运行资产的完整程度直接决定你的修复效率。第三个建议是先做出一两个原子技能跑通全链路再扩展技能数量。不要一开始就铺开几十个技能那是给自己挖坑。两个技能跑通以后你会更清楚自己的运行时缺什么、评估集长什么样、错误码够不够用这些经验会直接影响后面的扩展质量。在我个人实测里agent-skills这套方法把“给Agent新增一个能力”这件事从“改提示词然后提心吊胆地观察”变成了“写一个技能定义文件注册进去跑一遍回归测试上线”。这个转变带给人的安心感是之前那种打补丁式开发完全给不了的。如果你现在正被Agent行为不稳定、能力难复用、排查链路长这些问题困扰可以认真考虑把技能系统抽出来独立管理而不是继续在提示词泥潭里打转。