
早先把Agent接到线上业务时我最头疼的不是模型能力不够而是每次给Agent布置新任务都要把一大段人设、步骤、规则、输出格式全部塞进system prompt里。塞到第四个正经功能的时候prompt长得离谱模型开始答非所问有时候连最基本的指令都会执行错。后来我换了agent-skills这套思路——把一个能力封装成一个独立技能模块按需加载、按需执行。看起来只是写法变了实际上把整个Agent的扩展和维护方式都改了过来这套结构我已经用了一年多今天把完整的设计思路、落地细节和踩过的坑一次性说清楚。这篇文章适合正在做Agent应用开发的工程师尤其是被提示词越堆越长、功能越加越乱折磨过的人。我会从技能的拆分原则、结构设计、主循环集成、组合编排几个层面展开最后分享几个我认为比较关键的避坑经验。1. 为什么要把Agent的能力拆成技能而不是继续堆提示词1.1 长提示词带来的三个隐形问题做Agent的过程中我发现最影响系统稳定性的往往不是模型本身而是我们把太多东西硬塞进一个提示词里面。我早期做过一个内部工具一个system prompt里包含项目管理问答、代码变更分析、会议纪要总结三个功能每个功能又带了自己的few-shot示例和输出格式。刚开始还能跑后面随着需求叠加出现了三个问题。第一个问题是意图干扰。三个功能共用一段上下文用户问一个项目进度模型却很容易被代码分析那部分的示例带跑偏输出一些不相关的内容。第二个问题是上下文长度压力。每个功能的关键信息都要常驻在上下文里加上历史对话很快就把窗口打满导致更早的信息被丢弃。第三个问题是维护困难。改任何一个功能都要动整条prompt测试几轮下来经常会发现这个功能修好了、另一个功能又坏了。1.2 skill的核心思路像工具箱一样管理能力agent-skills的核心思路和堆prompt完全不同。每个技能是一个独立的模块有自己的描述、输入参数、执行指令和返回格式。Agent运行时先判断用户意图再决定调用哪个技能——用到了才加载用不到完全不占上下文。我第一次体会到这个思路的差别是在做一个自动排期助手的时候。当时需要在系统里加入根据项目紧急程度重新安排时间块的能力。如果按老办法我得把排期规则、星期格式、优先级判断标准全部写进主提示词。但用skill的方式我只需要在外面写一个日期处理模块再配一套完整的执行指令Agent只在用户表达排期需求时才把这段指令加载进来。整体prompt的长度立刻降了一半而且排期逻辑的修改完全不影响其他功能。1.3 skill与tool、function call到底有什么区别很多人会把skill和function call混为一谈我最初也踩过这个误区。function call本质上是给模型暴露一个可调用的函数接口模型根据对话内容决定要不要调、传什么参数。它解决的只是模型如何触发外部动作的问题。而skill更像是一个完整的能力包。它不仅包含触发判断还包括内部的执行策略、需要的工具调用链、输出格式化、异常处理。一个skill可以内部调用多个function call也可以纯靠提示词引导模型完成某项分析甚至可以组合另一个skill。换句话说function是技能内部的零件技能是Agent面对一类任务时的完整解决方案。提示如果你只做简单的天气查询、计算器这类功能function call就够了。但当你做的是一套需要稳定复用的业务能力比如周报生成项目风险扫描行程冲突检测就值得升级成skill的封装方式。2. Skill到底长什么样拆解一个技能模块的完整结构2.1 技能声明决定什么时候被触发的部分我当时设计技能结构的时候最先确定下来的是技能声明区。这部分相当于技能的门牌信息Agent看到它才知道这个技能是干什么的、什么情况下该调用。一个合格的技能声明至少包含技能名称、一句话描述、触发场景关键词。我第一次做的时候描述写得太泛比如处理日期相关需求结果用户问这周三是几号也会触发这个技能——这本来应该直接用模型知识回答的白白消耗了一次调用。后来改成更精确的描述用仅用于项目时间表冲突检测与调整建议误触发率明显下降。我自己用的是name、description、keywords三段式。keywords不是必需的但我在实测中发现加上它能显著提升Agent在意图模糊时的选择准确率。比如冲突检测这个关键词就能在用户说周三下午和评审撞了这种口语化表达时把技能准确匹配上。2.2 输入定义给Skill装上参数校验的漏斗技能的第二层是输入定义。系统会根据对话内容提取参数填充到技能里面再执行。这一步特别容易出现模型自由发挥的情况所以我把输入设计的重点放在约束上。每个参数都有三个属性名称、类型、约束范围。拿我做的排期冲突检测技能来举例它接收的参数包括任务ID、开始时间、结束时间、优先级。其中优先级字段我会限制枚举值只允许填入最低、普通、最高三档而不是让模型随便写比较急下周要交这种模糊表述。开始和结束时间则强制要求ISO 8601格式不接受任何别的日期格式。这种约束看着繁琐实际帮了大忙。有一次测试时模型传了一个“下周三下午”这种相对时间描述因为在输入定义里强制了绝对时间格式校验层直接把请求拦下来让上游重新解析。如果没有这个门槛技能拿到半结构化数据往下跑后面的所有逻辑都得跟着错。2.3 执行指令真正干活的提示词模板技能的执行指令部分是替代主提示词工作量的核心。我把它理解成一个独立的临时system prompt只在技能被激活后注入到模型上下文里。设计原则是自包含、可独立运行、边界清晰。还是拿排期技能举例。执行指令里有这么几层逻辑先要求模型输出一个时间线总览把现有任务按时间顺序排列再要求定位出两两冲突的任务对然后才要求生成调整建议而且明确要求建议按照调整低优先级任务时间优先于整体后移的顺序来提供。这些细节我全部放在技能内部主Agent完全不知道这些规则的存在自然也不会被它们干扰。一个技能的好坏很大程度上取决于这个执行指令写得够不够具体。我的经验是宁可写细一点也不要让模型在执行过程中去猜。比如调整低优先级任务时间这句话后面我还补了一段说明标注了模型必须考虑任务之间的依赖关系、不能把高优先级任务移到周末。这段规则在外面看起来是废话但在模型执行时是实实在在的边界约束。2.4 输出规约让复杂结构稳定落地技能最后一部分是输出规约也就是规定返回给主Agent的内容格式。这块做得越规范后面的编排和组合就越省力。我用JSON Schema来描述返回结构每个技能的输出都被限制在一个固定格式里。排期技能的输出会包含冲突列表、建议列表、剩余可用时间块三个字段。模型执行完之后还有一个校验步骤——检查关键字段是否存在、枚举值是否合法、时间先后顺序是否成立。如果格式不对会触发一次修正重试最多重试两次。下面是我保存的一个技能模板结构基本固定自定义空间都在prompt和skill_params里{ skill_name: schedule_conflict_detector, description: 仅用于项目时间表冲突检测与调整建议在用户提出日程撞期、时间冲突等问题时触发, keywords: [冲突, 撞期, 排期调整, 时间块], input_schema: { type: object, properties: { task_ids: { type: array, items: {type: string}, description: 需要检查的任务ID列表 }, time_range: { type: string, format: date-range, description: 检查的时间范围格式YYYY-MM-DD/YYYY-MM-DD }, priority_floor: { type: string, enum: [最低, 普通, 最高], description: 只检查该优先级及以上的任务 } }, required: [task_ids, time_range] }, execution_prompt: 你是一名排期冲突分析专家..., output_schema: { type: object, properties: { conflict_pairs: { type: array, items: { type: object, properties: { task_a: {type: string}, task_b: {type: string}, conflict_window: {type: string} } } }, suggestions: {type: array, items: {type: string}}, remaining_blocks: {type: array, items: {type: string}} }, required: [conflict_pairs, suggestions, remaining_blocks] }, max_retries: 2, timeout_ms: 60000 }3. 把Skill接入Agent主循环技能路由与参数填充的关键步骤3.1 技能路由让Agent知道该摸哪个工具技能拆好了下一步是让Agent在运行时能够准确选择该调用哪个技能。这一步我称之为技能路由。我实验过三种路由方式基于规则的关键词匹配、基于embedding的语义相似度、以及让LLM自己直接选择。直接上LLM选择看起来最智能实际效果却未必最好。因为当技能数量上来之后LLM面对一大串技能描述仍然会陷入选择困难甚至被相似描述误导。我现在的方案是混合路由先用关键词和embedding做一轮粗排筛出2到3个候选技能再让主LLM在这几个候选里做最终决定成本低准确率也够高。路由这一步有个很重要的细节技能描述的质量直接决定路由准确率。建议把什么时候不用这个技能也写进描述里。比如排期技能的描述末尾可以加一句如果只是询问当前时间请不要调用此技能。这种负面排除条件对减少误触发非常有帮助。3.2 参数填充最容易出幻觉参数的地方路由确定之后进入参数填充阶段。系统要从对话历史和上下文里提取出技能需要的各项参数。这一步是我整个开发过程中遇到问题最多的环节。最常见的问题是参数幻觉。模型会自信地填入对话里根本没有提过的内容。我有一次测试数据统计技能用户问的是看一下文档里的项目数据模型居然自己生成了一个上周发布的待办清单作为参数传了进去——这个词从未在上下文里出现过。我的解决方案有两层。第一层是定义中尽量使用enum、format这类硬约束让模型没有自由发挥的空间。第二层是加一个参数来源标注每条参数不仅要给值还要给出是从哪句话里提取的依据。如果找不到依据就标记为未明确由系统决策是向用户追问还是使用默认值。这一步直接把参数幻觉率降到了可接受的范围。3.3 技能的两种执行模式工具型与文本型接入主循环前还要分清技能的两种执行模式。第一种是工具型技能执行的时候可以调用外部API、数据库、代码函数我直接接一个executor在本地跑。第二种是推理型技能它不调用外部资源只是通过一段独立的prompt让模型专注于分析某个具体任务比如代码变更风险评估。这两种模式在代码里只需要做一层抽象就能兼容。我的做法是给每个技能定义一个execution_mode字段工具型的走API调度器推理型的走一个独立的LLM调用流程。关键是两者的输出都要经过同一个output schema校验器这样上游调用方可以无差别地拿结果。3.4 一个完整的调用链长什么样把前面这些环节串起来一次技能调用的完整流程分别指向五步任务进来后先做意图识别粗筛出候选技能填充参数并校验然后执行技能最后把结果格式化成统一结构返回。下面这段伪代码描述了这个过程def route_and_execute(messages, skills): # 第一步候选过滤用关键词和embedding粗筛 candidates keyword_rough_filter(messages[-3:], skills) candidates embedding_filter(messages[-3:], candidates) # 第二步让主LLM从候选中选择最终技能 selected llm_select_skill(messages, candidates) # 第三步抽取参数并校验参数来源 params llm_extract_params(messages, selected.input_schema) params validate_params(params, selected.input_schema) # 第四步执行技能工具型走executor推理型走独立prompt if selected.execution_mode tool: result execute_tool_skill(selected, params) else: result execute_llm_skill(selected, params) # 第五步校验输出格式失败则最多重试两次 result validate_output(result, selected.output_schema) return result这个流程跑通之后我再往系统里加新功能就省心多了。不需要去改动主prompt只需要新增一个技能包文件然后系统启动时自动注册进来就行。4. 从单点技能到技能编排复杂任务怎么拆和怎么合4.1 为什么需要编排单技能解决不了复杂任务技能拆得越细就越会碰到一个问题一个完整的业务任务往往需要多个技能协作才能完成。比如做一个生成项目周报的功能至少涉及三件事拉取本周的代码提交记录、分析提交信息里的变更点、生成一份符合格式的周报文本。如果我把这三件事塞进一个技能里技能会变成一个臃肿的大杂烩失去拆分的意义。所以我在设计时把这类功能拆成多个独立技能再由一个编排者来指挥它们。编排者本身也是一个特殊技能只不过它不干具体活只负责规划把任务拆解成步骤决定每一步用哪个子技能、按什么顺序执行、上一个技能的输出如何传给下一个。这种母技能管子技能的设计让每个技能保持小而专的同时又具备处理复杂任务的能力。4.2 三种基础编排模式我实践的编排模式有三种串行、并行、条件分支。串行是最常见的上一个技能的输出作为下一个技能的输入适合流水线式任务。并行适用于彼此不依赖的子任务比如同时拉取代码记录和统计未关闭的issue然后再合并结果。条件分支则依赖上一步的决策结果。比如风险评估技能返回了高风险标记编排者才会去调用风险升级通知技能如果是低风险就直接进入下一步。编排者不需要在系统里显式写if-else逻辑只要把分支处理的能力提示词写清楚——模型会根据上一步的实际输出来决定走哪条路径。4.3 输出契约编排能否成功的分水岭选好编排模式只是第一步真正决定编排成功率的是子技能之间的输出契约对齐。也就是说上一个技能输出的结构必须恰好是下一个技能期望接收的结构。我在这里吃过很大的亏。有次做自动排期编排上游任务优先级评估技能输出的是普通文本下游冲突检测技能的input_schema要求的是严格JSON。结果每次传递都要靠模型现场转换格式不是少了字段就是多嵌套了一层整个链路的成功率只有六成左右。后来我做了两件事一是把所有技能的输入输出都统一到JSON Schema二是为那些需要跨技能传递的关键字段比如任务ID、时间块、状态值建立了全局统一命名规范。下游技能直接复用上游字段名几乎不需要额外的映射说明。改完以后整条链路的成功率直接提升到九成以上。4.4 一个实际编排的完整流程示例以自动生成周报这个场景为例我设计的是一个三步串行编排。第一步调用代码提交聚合技能拉取本周的提交记录并按模块归类第二步调用变更影响分析技能把归类后的提交记录转换成功能变更、问题修复、性能优化三类摘要第三步调用周报格式化技能把摘要填进公司的周报模板。其中第二和第三步之间有一个关键信息传递第二步的输出摘要第三步的input_schema期望它包含summary_type和content两个字段。我只需要在第二步技能的输出规约里也定义这两个字段整个链路就顺畅了。如果中途我改了模板样式只动第三步技能前两个技能完全不受影响。提示在技能编排场景下给每个技能的输出字段起名时多想一步这个字段会不会被别的技能消费。如果会建议起一个全局统一的名字别为了省事在各技能里各叫各的——这是我在实战里认为性价比最高的一个习惯。5. 实测中的坑技能误触发、参数幻觉与上下文污染的真实处理过程5.1 误触发描述里的一个单词引发的连锁反应我最早设计技能声明时给日期解析技能写了一句把用户表达的时间转换为标准格式。这本来是很直白的描述但上线之后出现了大量误触发。用户只要提到上周月底明天这些词Agent就会调用这个技能去转换时间哪怕用户只是在闲聊里顺带说了一句明天的会变成周三开。排查链路是这样的我先查看了路由日志发现触发时embedding分数并不高但关键词匹配那关直接把相关词全命中了。然后我意识到问题出在keywords字段上——我把明天上周月底这些都列成了keywords结果它们每次出现都会被粗筛捞起来交给LLM确认而LLM在候选数量少、描述不够明确的时候倾向于选择看起来跟时间有关的技能。修复方案分两步。第一步从keywords里删掉所有常见时间表达只保留业务强相关的词比如解析时间转换格式日期规范化。第二步在description末尾加排除条件如果用户只是提及时间进行陈述不涉及结构化转换需求请勿调用此技能。改完之后误触发率下降了快七成。5.2 参数幻觉模型填了一个不存在的ID参数幻觉问题我在3.2里提过这里讲一个完整的处理过程。测试任务状态更新技能时用户对话里根本没有具体任务只是说把该办的事情办一下。结果模型在填充task_id参数时凭空捏造了一个ABC-123传了进来。如果执行端不校验这个ID和真实任务列表的映射关系系统就会对一个不存在的任务执行更新——后果很严重。我第一次只是简单加了非空校验发现没有用模型知道task_id不能为空于是更努力地去编造甚至从上下文里模模糊糊提到过的那个需求里推断出一个ID。那段时间这类问题反复出现。后来我把校验逻辑从非空提升到存在性校验。执行端在收到task_id后第一次调用前先查一次任务列表如果ID不存在立即返回参数无效-REQUIRE_FIELD_CHECK并附上当前可选的任务ID列表。由于我的input_schema里声明了参数必须来自用户明确提及内容模型在下一次生成时会看到真实的候选列表基本上就不会再凭空瞎编了。5.3 上下文污染技能返回值太大把主线任务冲没技能执行完会把结果返回给主Agent继续对话。这里有个性能陷阱如果技能返回内容太长尤其是一些工具型技能一次性返回了大量日志或明细主Agent的上下文里就被塞满了无关细节导致对话质量明显下降。我遇到的一个典型场景是数据拉取技能。它返回了五十多条原始记录而用户其实只想知道样本有没有异常。主Agent拿到这一大坨数据之后反而把用户的核心意图给忽略了甚至开始根据日志里的细节编造分析结论。解决方案是给每个技能的输出规约里加一个summary_first原则技能返回的内容规定只包含摘要信息最多附带三条代表性明细的链接或引用。如果用户明确要求更多细节再由Agent在后续轮次里发起一个明细查询技能。自那之后技能返回的平均体积直接降了一个数量级主Agent的执行稳定度提升明显。5.4 技能版本管理改坏了不会拖累整个Agent最后一个值得记录的坑是版本管理。技能是一段会被反复调用的结构一旦某个技能上线后会出现幻觉或者逻辑错误如果我没有版本控制全系统的Agent都会受影响。后来我把每个技能模块都纳入版本库执行日志里记录技能版本号和具体的执行时间。线上出了一个涉及技能的问题我可以在编排器的日志里直接定位到是哪个版本的哪个技能导致然后决定回滚。这个习惯在团队协作时尤其重要——其他同事更新了一个技能我至少能明确知道系统行为的变化点在哪。6. 我现在的技能设计原则与收尾心得经过这些坑之后我总结出几条支撑整个构建过程的设计原则。第一条每个技能只解决一类问题而不是一个问题。同类问题的细节差异通过参数去调节。比如排期冲突检测和排期自动调整一定是两个技能前者只负责分析后者才负责输出调整建议。混在一起的结果就是执行指令里出现两个目标模型会顾此失彼。第二条技能描述既写明什么时候用也写明什么时候不要用。负面排除信息看起来降低了所谓灵活性但显著提高了路由准确率。这其实不算严格限制更像是在降低模型的选择成本。第三条所有技能的输入输出都走结构化校验。哪怕内部逻辑再完美只要衔接处有一个字段对不上整条链路就会崩塌。我在实践中给所有技能包的输出都加了一道格式验证流程没有通过验证的一律重新执行这个小小的强制机制比我在prompt里写十句请严格按格式输出都管用。第四条技能包应该像代码一样管理。有版本号、有更新日志、有责任人。Agent技能本质上也是一种代码逻辑只是表现形式是提示词加配置完全应该用对待后端服务的态度来对待。如果再往远了想技能编排这个方向还可以继续扩展出技能间的超时与熔断机制动态扩展现有技能的参数范围多Agent共享技能库等玩法这些东西我还在陆续尝试之中。就目前的实际收益来看用agent-skills的模式来组织能力无论从系统稳定性、维护成本还是扩展速度上看都比过去堆长提示词的方式好太多。如果你也在做Agent应用不妨从手头那个功能开始试着把它拆成一个独立的技能模块跑一遍应该很快就能感受到差别。