
1. 从会聊天到能干活Agent Skills要解决的现实问题如果你最近在折腾AI Agent大概率撞上过同一个怪圈大模型的推理能力越来越强聊天体验越来越好可一旦让它去把某件事办了它就卡在第一个动作上——要么不知道该调哪个工具要么调了工具却用错参数要么干脆一本正经地编一个结果出来。我前后做了小半年的Agent项目才想明白问题不在模型而在我们没给Agent一套像样的技能体系。圈子里管它叫Agent Skills中文一般翻成智能体技能。它不是一个框架的封闭概念而是一整套把领域能力沉淀成Agent可复用操作单元的方法论。下面这份总结来自我从零搭建agent-skills模块的真实经历涉及概念拆解、技能设计、注册加载、编排协作、评测迭代五个环节。适合正在做Agent应用、或者准备把大模型真正接进业务流的开发者参考。1.1 一个带不动工具的Agent本质上缺的不是模型先讲个我印象很深的例子。去年我接了一个内部知识库问答的需求客户已经把大模型接好文档也灌进了向量库看起来万事俱备。上线第一天就翻车用户问本周三下午的会议纪要在哪Agent先答非所问好不容易定位到文件又把周三下午理解成周三上午最后给出的是一个压根不存在的文件路径。问题出在哪模型不聪明不是底层是当时最好的商用大模型。真正的病灶是Agent只有聊天和检索两个动作中间没有任何东西约束它、引导它、帮它把检索这个动作做对。它缺少一套会议纪要归档规范之类的领域技能。这个案例让我意识到一个关键点Agent能干活的前提不是模型聪明而是任务空间被收敛了。Skills解决的不是模型懂不懂的问题而是模型能不能每次都稳定做对的问题。模型负责判断该做什么技能负责告诉你具体怎么做、做到什么程度算好。没有技能约束模型每次都会在同一个地方用不同的错误姿势摔倒。1.2 Skills、Tools、Actions先厘清概念边界做Agent开发的人常被三个词绕晕Skills、Tools、Actions。网上很多文章混着用导致设计方案也跟着乱。我按自己的理解整理了一个分层已经内化成了团队的标准用语概念层级定义举例Tools底层单个外部能力接口一次调用、一次返回无内部状态搜索引擎、发邮件API、数据库查询Skills中间建立在Tools之上的复合操作单元封装做一件完整事情的方法会议纪要整理、客服工单路由、行业报告生成Actions执行层Agent在任务中实际执行的最小步骤调用搜索引擎、读取上一步结果、生成一段回复用生活化的方式说Tools就像厨房里的锅、刀、灶Skills是一份红烧肉怎么做的菜谱Actions是你一步步切肉、下锅、加调料。只给模型一堆锅碗瓢盆它也能做菜但能不能稳定复现同一个味道就看有没有菜谱。这个区分不是书呆子式较真。它直接决定了代码放哪、提示词放哪、校验逻辑放哪。如果工具层和技能层的代码混在一个函数里后面维护成本会指数级上升。工具变了技能要跟着动技能变了编排要跟着动。边界清晰后面每一步都能独立演进。另外在定义接口时我建议三个层都走统一的对象协议比如所有工具返回统一的结果对象带状态码、原始数据、错误信息技能的输入输出则统一走JSON SchemaActions则由Agent框架统一记录执行轨迹。这样做的收益在技能数量超过十个之后会非常明显。2. 技能的内在结构设计一个可复用Skill的关键组成2.1 Skill描述给Agent一张技能查询卡片设计Skill的第一件事是写一份能让模型和维护人员一眼就明白这技能是干什么的描述。这个描述不是给人看的简介而是Agent在决定要不要调用技能时的判断依据。我把Skill描述做成Markdown文件统一放在每个技能目录的SKILL.md里重点包含六类信息技能名称与版本号、一句用途说明、适用场景与边界、依赖的工具、输入输出摘要、以及一两个真实使用示例。这里有个非常反直觉的经验描述越具体Agent调用越准确。你写处理文档模型会懵你写将会议转写文本整理成结构化会议纪要并提取待办事项与负责人模型遇到会议纪要任务时就会想到它。原理是模型的意图识别本质是语义匹配你的描述与用户请求在语义空间里离得越近命中率越高。我举一个改造前后的对比糟糕的描述This skill helps with meeting documents. 这种描述放进任何检索系统都是灾难因为它等于没说。可用的描述把会议转写文本整理成结构化会议纪要提取会议结论、待办事项、负责人与截止时间适用于用户提供会议录音转录稿、要求生成纪要或总结的场景当用户只是闲聊时不要触发。把when to use和when NOT to use都写清楚能极大减少误触发。因为大模型在判断要不要用某技能时它其实不太会主动推理更多是在做模式匹配。你给了否定边界相当于提前帮它排掉一批错误选项。一份完整的SKILL.md长这样# SKILL: meeting_summary ## name / version meeting_summary / 1.2.0 ## purpose 将会议转写文本整理为结构化的会议纪要 提取会议结论、待办事项、负责人和截止时间。 ## when to use - 用户提供会议转写稿、录音转录文本要求生成纪要或总结 - 用户询问上次会议说了什么有什么待办时配合检索工具使用 ## when NOT to use - 用户只是在闲聊没有明确的会议素材或会议相关诉求 - 用户要的是普通写作或翻译任务 ## depends_on - transcript_to_text: 音频转写工具 - calendar_api: 读取会议时间与参会人信息 ## input / output - input: transcript, attendees, timezone - output: summary, decisions, action_items这份文件本身不包含处理逻辑它只是给Agent做技能选择用的。真正的处理逻辑在下面这个环节。2.2 技能逻辑把指令拆成可执行的最小动作单元技能内部到底写什么这是我和很多初学开发者分歧最大的地方。有人习惯把技能做成一个超级提示词把整个处理流程塞进一大段Prompt里也有人习惯纯代码化写一个Python函数输入进、输出出。我的实践结论是不要走极端把一个Skill的逻辑拆成规则型步骤 模型型步骤的混合体。规则型步骤适合做确定性工作解析JSON、字段校验、调用固定API、映射枚举。模型型步骤适合做需要理解和生成的活总结要点、提取待办、判断情绪。一个成熟的Skill应该让代码承担80%的确定性工作让模型只负责20%的创造性工作并且模型那部分必须用明确的输入输出协议约束住。拿meeting_summary当例子内部流程可以拆成四步规则型步骤接收transcript和attendees清洗原始文本去掉语气词、合并重复内容按发言人切分成段落模型型步骤把清洗后的文本交给模型按照固定的提示词模板提取会议结论和待办事项。这里提示词模板只负责提炼不负责排版规则型步骤把模型产出解析成JSON走输入输出Schema校验。如果必填字段缺失触发一次自动追问或改用备选模型重跑规则型步骤调用calendar_api补全会议时间和参会人邮箱最终渲染成符合公司纪要模板的Markdown文档。模型型步骤的提示词模板大概是这种感觉你是一名会议纪要整理助手。以下是某次会议的转写文本分隔符。 文本中的发言者是说话人标识内容是发言内容。 任务1提炼本次会议的结论每条不超过30字。 任务2提取所有待办事项输出为JSON数组字段为owner/task/deadline。 只允许引用分隔符内出现的信息禁止编造。 分隔符 {cleaned_transcript}把这个模板放进技能目录的prompt_template.txt里由代码读取后填充而不是硬编码在Python函数中。因为运营同学可能想调提示词让他直接改文件比改代码友好得多。混合拆分有一个直接好处可测。规则部分可以写单元测试模型部分可以单独评测。到了后期迭代你不会再遇到改了一段Prompt结果另一个环节莫名变差的玄学问题因为每个环节的输入输出边界都是清楚的。2.3 输入输出契约技能与外部世界的契约Skill要能被复用前提是输入输出稳定。我见过太多团队做Agent技能函数的参数列表完全看心情今天传string明天传dict后天传一个带着一堆上下文的魔术对象。等技能数量超过10个全系统就乱成一锅粥。我们在项目里为每个Skill都定义了JSON Schema作为外部世界的契约。还是用meeting_summary举例{ name: meeting_summary, version: 1.2.0, input_schema: { type: object, required: [transcript], properties: { transcript: {type: string, description: 会议转写原文}, attendees: { type: array, items: {type: string}, description: 参会人列表可为空 }, timezone: {type: string, default: Asia/Shanghai} } }, output_schema: { type: object, required: [summary, decisions, action_items], properties: { summary: {type: string}, decisions: {type: array, items: {type: string}}, action_items: { type: array, items: { type: object, properties: { owner: {type: string}, task: {type: string}, deadline: {type: string, format: date} } } } } } }这个Schema有两个作用。第一给模型当操作手册模型在调用技能之前能看清该准备什么数据、会得到什么回报。第二给运行时当校验器技能启动前对输入做校验结果返回后对输出做校验不合格的直接拦截重试。不要让模型带着残缺参数往下跑否则后面的错误会层层放大。我实测过在纯基于LLM的Agent里引入输入校验任务成功率提升了大约15个百分点主要减少的就是参数传错导致工具白调这类低级错误。3. 技能注册与动态加载从写在Prompt里到跑在清单上3.1 技能注册清单把上下文污染问题拆掉早期的Agent项目通常把所有技能描述写进一个巨大的系统Prompt。技能少的时候还行技能一旦超过五个问题立刻出现模型开始晕技能——明明某技能完全合适它偏要选另一个。原因很简单相关性被稀释了。我们把这种问题叫上下文污染。我们的做法是引入技能注册清单skills registry把每个技能的描述从Prompt里剥离存成结构化文件。Agent在运行时按需把一批技能描述放进上下文而不是全量塞入。一个技能清单项长这样- name: meeting_summary version: 1.2.0 path: ./skills/meeting_summary/ description: | 将会议转写文本整理为结构化会议纪要提取结论、待办、负责人与截止时间。 depends_on: - transcript_to_text - calendar_api tags: [meeting, document, productivity] enabled: true清单文件本身也要做Schema校验防止有人提交一个缺字段的技能导致整个注册表崩溃。我们用Pydantic写了一个Validator在CI里跑有问题的技能直接标红不允许合入。这里分享一个精简版from pydantic import BaseModel, Field, validator class SkillEntry(BaseModel): name: str Field(patternr^[a-z][a-z0-9_]*$) version: str Field(patternr^\d\.\d\.\d$) path: str description: str Field(min_length20) depends_on: list[str] [] tags: list[str] [] enabled: bool True validator(path) def path_must_exist(cls, v): import os if not os.path.isdir(v): raise ValueError(fskill path not found: {v}) return v有了注册清单技能描述就不再是Prompt工程的一部分而变成了配置资产。任何人新增技能只需要遵循清单格式提交目录和文件剩下的加载流程由框架处理。这带来的协作体验提升是质的飞跃之前每次调整Prompt都要全量验证现在改单个技能只影响它自己。3.2 按需加载、冲突消解与运行时隔离有了清单后最关键的是按需加载。我一直觉得如果一个同学把所有技能灌进Prompt多半是因为懒但后面一定会付出惨痛代价。技能加载应该是一个主动选择的过程。我的实现是在Agent框架里维护一个技能选择器当用户请求进来先做意图分类再根据分类给候选技能打分最后结合依赖检查确定加载集合。候选技能的打分我一般用混合策略先做关键词与标签的粗筛再做embedding向量相似度精排。这里提醒一点如果只用描述做embeddings召回遇到两个描述相似的技能很容易选错。我给技能描述、标签、示例输入三个维度各分配了权重其中示例输入的权重最高。原因很好理解技能真正回答的是什么业务请求能命中它而语义最接近业务请求的往往不是抽象描述而是真实输入样例。下面是一段简化的技能选择逻辑def select_skills(query: str, registry: list[SkillEntry]) - list[SkillEntry]: # 粗筛标签命中或关键词命中 candidates [s for s in registry if s.enabled and (query_has_tag(query, s.tags) or keyword_hit(query, s.description))] # 精排示例输入向量相似度为主描述为辅 scored [(s, embed(query, s.example_inputs) * 0.6 embed(query, s.description) * 0.4) for s in candidates] scored.sort(keylambda x: x[1], reverseTrue) # 依赖检查加载技能前解析依赖树 selected [] for s, _ in scored: if resolve_dependencies(s, registry) and not skill_conflict(s, selected): selected.append(s) return selected[:max_skills]依赖检查必须做在前面。A技能依赖B工具但B工具当前没启用如果加载A时不检查依赖执行到一半就会报工具不存在然后Agent开始自由发挥编造一个结果。我们在选择阶段会解析整个依赖树缺依赖就提前降级到另一个技能或者返回明确的错误提示。冲突消解是个容易被忽略的点。当两个技能都能处理同一类请求时比如meeting_summary和email_reply都可能处理会议相关请求我采用精确度优先策略先看意图分类置信度两个都超过阈值时比较描述与query的相似度分谁高选谁。这套规则是写死的绝不留给模型自由发挥。否则同一个请求今天走A技能明天走B技能用户会觉得产品时灵时不灵。运行时隔离也很重要。每个技能在加载后尽量跑在独立执行上下文里技能内部的中间变量、临时文件、状态不能污染其他技能。我们用轻量容器加临时工作目录实现稍微重一点但稳定性收益值回票价。尤其在技能需要写文件或调用外部命令时隔离能防止一个技能把整个Agent进程搞挂。4. 技能编排让多个Skill协作完成复杂任务4.1 串行、并行与条件路由编排的三种基本姿势技能是一个个能力单元但真实业务很少只用一个技能。一次用户请求往往触发多个技能按特定顺序协作。我总结最常见的三种编排模式三种元模式足够覆盖绝大多数业务流。串行Pipeline前一个技能的输出直接作为后一个技能的输入。适合流水线式任务。比如先做语音转文字再做会议纪要最后派发待办。并行Parallel多个独立技能同时对同一份输入执行最后合并结果。适合信息收集类任务。比如用户问帮我准备明天的客户拜访资料你可以同时触发客户档案查询历史订单汇总竞品信息检索三个技能再把三块内容拼装成一份简报。条件路由Router根据输入内容的不同属性把请求分派到不同的技能。适合分支较多的任务。比如客服工单系统根据工单类型路由到退款处理技术排查还是投诉安抚。这三个模式一定要做成框架层的显式机制不能指望模型在长文本Prompt里自由发挥编排。显式编排让整个执行流程可观测、可回放、可调试。某一步失败时你能明确知道是哪个技能、哪个输入、哪次调用出了问题。模型自由编排最大的问题是不可解释——它走了哪条路径为什么要走这条路径完全没有记录。用一个简单的YAML配置来定义串行Pipelineworkflow: meeting_archive_pipeline steps: - skill: meeting_summary input_from: user_request.transcript output_to: intermediate.summary - skill: task_router input_from: intermediate.summary.action_items output_to: intermediate.routed_items - skill: im_notifier input_from: [intermediate.summary, intermediate.routed_items] output_to: final.result这种描述式编排的好处是非工程师也能看懂流程同时框架可以针对每个step做超时、重试、日志采集。我把编排定义也纳入版本管理因为流程本身也是业务逻辑会随业务规则变化。4.2 实战案例一个会议纪要归档待办派发的复合流程拿我们最近上线的流程举例。用户对Agent说帮我把上午的产品评审会记录整理一下给每个待办分配负责人并同步到项目群。这条请求同时涉及三个技能meeting_summary、task_router、im_notifier。显式编排定义如下串行执行meeting_summary得到结构化纪要和action_items把action_items输入task_router技能根据预设规则按模块、按现有排期给每个待办分配负责人和截止时间串行调用im_notifier把最终结果和纪要发到项目群整个过程的中间数据落盘到执行日志便于事后复盘。这里有个非常关键的实践点编排过程不能允许task_router改写任务描述本身。task_router只能调整负责人和优先级不能改动用户在会议里明确承诺的task内容。为什么一旦允许模型自由改写它在第三步可能为了让结果看起来更合理而悄悄篡改事实等用户拿着被改过的待办去追责任时问题就大了。我们在技能层面用只读字段和可写字段来约束。meeting_summary产出的action_items.json里task字段标记为read_onlyowner和deadline字段标记为writable。任务路由时只能改writable字段。这是数据完整性层面的护栏属于架构的一部分。我见过不少团队等到出了数据不一致事故才想起来补护栏代价远高于一开始就设计好。执行日志要多详细我的经验是除了记录每一步的入参出参还要记录技能内部调用了哪些工具、工具的返回状态码、以及模型生成时的温度参数。这样万一出问题你可以直接复现那一次的调用快照。5. 评测与迭代如何证明技能真的好用5.1 搭一套最小可行的技能评测集我在技能开发上踩过最大的坑是感觉好用就上线。技能改动太容易了改一段提示词、调一个参数当下效果看起来好了一些可过几天同一个技能在另一个用户的输入上突然变差你根本不知道是哪次改动导致的回归。后来我养成了硬性习惯每个技能必须配一套评测集golden set。评测集一般包含50到200条真实任务输入每条输入都标注了期望输出里的关键要素。比如meeting_summary的评测集就是一批真实会议转写文本每条标注期望的结论要点、待办数量、负责人清单等。评测主要看四个指标指标含义合格线参考任务完成率是否在限定步数内产出了合法结果大于等于90%关键信息准确率结论、待办、负责人与标注是否一致大于等于85%格式合规率输出一次通过Schema校验的比例大于等于95%幻觉引入率输出是否包含标注之外的信息小于等于5%加CI之后每次技能更新自动跑一遍回归测试。团队改技能不再靠拍脑袋先看评测集指标变化再决定是否合入。有一次我为了优化处理长文本的速度改了模型温度参数结果关键信息准确率掉了8个点。如果没这套评测集我根本发现不了这个问题。评测集自身的质量也要维护。最好从线上日志里抽真实case再让领域专家标注期望结果。不要自己编造太多理想输入——编出来的样例往往太干净测不出真实用户输入的脏乱差。5.2 从失败模式反推技能设计缺陷评测集不只是用来卡通过的它真正价值在于暴露失败模式。我整理过几类高频失败以及对应的设计缺陷分享给同行参考失败表现技能召回成功但处理输入时频繁要求用户补充信息。根因通常是技能描述没写清输入格式或者Schema定义的必填字段与真实场景不符。治本方法是回填评测集里缺失的字段定义而不是教用户必须按照某种格式提问。失败表现模型编造了工具返回里根本没有的数据。根因是技能流程里读取工具结果与生成最终输出之间没有显式隔离。我强制在技能内部加一道仅使用给定上下文的约束步骤必要时做实体比对防止输出中出现工具结果之外的事件名、人名、金额等。失败表现两个技能单独测试都通过组合起来就出乱子。根因往往是技能之间存在隐式依赖比如A技能假设B技能已经清洗过数据但编排层没有保证执行顺序。解决办法是把隐式依赖写进技能描述里的depends_on或者直接在编排定义里显式声明先后关系。我建议大家每个季度把线上失败case重新过一遍把共性模式抽象成新的设计规范。比如我们团队后来规定所有技能必须自带字段级权限标注这条规矩就是从评测复盘里长出来的。6. 最后聊聊我的实操体会到这里Agent Skills的完整链路就讲得差不多了。我再掏几句压箱底的经验。第一个体会技能设计必须从业务结果倒推而不是从模型能力正推。别先问模型能不能做总结要问我到底想要一个什么样的稳定结果。想清楚结果长什么样再决定哪些步骤用代码写死、哪些步骤交给模型、最后才写Skill描述。顺序反了容易做出演示很惊艳、生产不好用的花架子。第二个体会给技能做版本管理比代码版本管理还重要。技能直接影响Agent在真实任务里的行为一次覆盖旧版本可能导致正在执行的任务全部切换逻辑下游数据格式不兼容就是线上事故。我们现在的技能全部走在registry文件里任何改动都要更新版本号、写变更记录、跑完评测集才能发布。第三个体会别指望一次设计完美。技能是长出来的不是设计出来的。上线后重点收集失败案例每周做一轮复盘把高频失败沉淀成新的约束规则或子技能。我这边最稳定的meeting_summary已经迭代了二十多版每一版变化的都不是模型而是输入输出契约和流程控制。如果你正在做Agent建议先从业务里最高频、最重复的那件手工操作入手把它固化成第一个Skill。跑通这条路径后面的技能体系会顺理成章地长出来。