资讯动态

Agent技能化封装实践:从裸写Prompt到agent-skills体系

发布时间:2026/10/14 9:37:52 来源:尧图企业网站定制
做Agent开发最头疼的事就是prompt越写越长、逻辑越堆越乱最后整个系统变成一团只有自己能看懂的咒语。我最近在做一个复杂任务自动化的项目发现单纯靠调prompt已经撑不住了于是把智能体的能力拆成一个个可单独封装、测试、复用的技能包这套体系就是标题里写的agent-skills。这篇文章就把我整个设计和落地的过程掰开揉碎讲清楚从为什么要做技能化、skill文件长什么样到怎么执行、怎么调试全部基于真实项目里的取舍和踩坑希望对正在做Agent系统或者准备重构的你有参考价值。1. 先聊为什么非要做skill化从裸写prompt到技能包1.1 裸写prompt的困境一段提示词塞不下所有逻辑早期做Agent最常见的方式就是把所有要求堆进system prompt里告诉模型你是谁、你的任务是什么、有哪些工具、输出格式是什么、遇到异常怎么处理。起步还行一旦任务变复杂问题马上暴露。最典型的翻车现场是一边要求模型做意图识别一边又要它抽取关键字段还要它决定调用哪个工具最后所有指令全都压在一条上下文里。结果模型经常顾此失彼要么丢字段要么格式输出不稳定要么明明该调用工具的时候开始胡说八道。我印象最深的一次是让Agent做一份销售数据清洗加汇总的任务因为prompt里同时塞了清洗规则、汇总口径、输出模板和异常处理逻辑模型处理到一半竟然自己编了几个不存在的字段名出来填进JSON里。表面看是模型能力不够实际上是我把太多职责塞进了一个上下文。人干活的时候都知道把大活儿拆成小步骤Agent也一样——但问题是模型没有拆步骤的自觉你给它一锅炖它就只能一锅乱炖。1.2 技能化之后到底解决了什么问题我后来想明白一件事开发Agent不能只调prompt应该像写代码一样做模块化。把会清洗数据会生成周报会调用某个API并解析结果这些能力各自封装成一个独立的技能包模型每次只加载需要的那些技能在技能内部再去看详细的步骤和约束。这样做下来有四个直接收益可复用清洗数据这个技能封装一次后续任何Agent、任何项目要清洗数据直接挂载这个skill就行不用重新写prompt。可测试每个skill可以单独用测试用例验证就像单测一样。裸写prompt时你很难单独测清洗逻辑对不对但skill化之后可以。可组合不同任务本质上是不同技能的排列组合能在运行时动态决定这次任务需要哪几个技能比每次全量加载聪明得多。可维护prompt单独改会影响全局而技能包的修改范围是封闭的改A技能不会影响B技能。用代码工程的话说这就是把一大坨全能的main函数拆成许多个小而专的函数。模型调度器负责路由分发每个技能内部自治整体可控性会上一个台阶。agent-skills这个项目名的核心含义也在这——给Agent配置的是一套可管理的技能集合而不是一段不断膨胀的prompt。1.3 agent-skills在整体系统里扮演的角色如果你搭过Agent系统应该熟悉那套典型的架构模型负责决策工具Tools/Function Calling负责执行具体动作工作流Workflow负责固定流程编排。那agent-skills的位置在哪里我的理解是它介于模型决策层和工具执行层之间属于能力调度层。工具是原子操作比如调用某API执行某段SQL技能则是一组工具、指令和校验逻辑的组合它描述的是怎么完成一件相对完整的子任务。模型不需要关心技能的底层是怎么一步步调用工具的它只需要知道什么时候该触发哪个技能。所以agent-skills不是要替代工具系统而是把工具的使用姿势从模型临场摸索变成模型直接读取说明书。配合Function Calling使用效果最好Function让模型知道有哪些原子操作可用Skill让模型知道这些原子操作怎么组合起来完成一件事。2. skills系统设计从schema到执行引擎四个核心模块整个系统设计我最纠结的就是一个问题一个skill到底应该包含什么、由谁定义是纯文本还是结构化应该是人为定义还是让模型自己生成最终我把架构固定成四块技能定义模块、技能注册模块、技能调度模块、技能执行模块。下面一个一个拆开讲。2.1 skill的文件结构与定义方式一个skill到底长什么样我参考了业界比较常见的skills定义方式自己重新设计了一套相对通用的schema。一个完整的skill包在文件系统里是这样的skills/ data_cleaning/ SKILL.md rules.yaml scripts/ clean.py examples/ input.json output.json tests/ test_clean.py weekly_report/ SKILL.md rules.yaml ...SKILL.md是灵魂给模型看的说明文档内容包含这个技能是干什么的、什么时候用、什么时候不能用、使用步骤是什么、有哪些注意事项。rules.yaml是结构化元信息给系统看的比如技能名称、版本、描述、需要加载哪些工具、需要注入哪些上下文信息。scripts/放实际执行脚本examples/放输入输出样例tests/放验证用例。关键在设计SKILL.md的结构。我写过一个模板核心几段是目的Purpose一句话说清这个技能解决什么问题。适用场景When to use明确列出适用条件越具体越好方便调度器匹配。不适用场景When not to use同样重要能避免很多误触发。执行步骤Steps用编号列出操作流程每个步骤有输入、处理和输出说明。约束与规则Constraints比如不要猜测缺失字段输出必须是合法JSON。示例Examples给一两个典型场景的输入输出对模型模仿能力很强有样例质量会提升很多。注意一点SKILL.md不是给人看的注释文件而是要直接被塞进模型的上下文里的。所以它必须写得像给一个不太聪明但执行力很强的实习生看的工作手册——清晰、具体、一步一步来别写一堆模棱两可的废话。2.2 技能注册与加载让Agent知道自己会什么技能包只是静态文件真正让它生效需要先注册进Agent系统。注册阶段要做三件事第一扫描技能目录读取每个SKILL.md和rules.yaml拿到技能名称、版本、触发条件等元信息。第二为技能建立索引方便后续检索。第三校验技能定义是否合法比如脚本是否存在、依赖的工具是否有定义。这个阶段我只保留元信息不加载完整的技能正文——全量加载太吃token了。我踩过的第一个坑就是这边一开始图省事启动时把所有技能全文都塞进system prompt里。结果技能多了之后每次请求的token消耗直接翻倍而且模型注意力被无关技能内容分散核心任务反而干不好。后来改成按需加载——每次只注入与当前任务相关的技能描述大幅缓解token压力同时准确率更高了。注册完技能之后还少不了一个技能激活环节。系统启动后会先列一个可用的技能清单给模型看类似tool list让模型知道当前环境里有哪些牌照等到具体任务来了再去按需拉取技能正文。这一步和大模型的Function Calling设计思路是一致的先给你看函数签名真的调用时再走具体实现。2.3 技能调度与执行动态决定这次用哪个技能技能执行流程我把它拆成五个阶段顺序如下获取任务目标接收用户请求提取核心目标可以是多轮对话的最后一轮也可以是系统提交流程里传入的任务参数。技能匹配在已注册技能列表里基于技能名称、描述向量、关键词、规则配置匹配出一个候选技能集合。我实测下来简单的关键词描述匹配已经能应付绝大多数场景没必要每次都用LLM做语义匹配性能代价太高。选择与加载从候选集合里选最优技能读取SKILL.md全文注入到当前对话上下文。执行技能模型根据技能里的步骤说明逐步调用工具、处理数据、输出结果。校验输出按rules.yaml里定义的输出约束检查结果格式和必填字段不合法就回滚或者重新执行一次。技能调度是整个系统性能的关键——选错技能的结果是什么是模型认真学习了一本无关的说明书然后自信地给出一个驴唇不对马嘴的答案。为了把这一步做好我在rules.yaml里加了一个字段叫match_keywords人工维护一批触发关键词。比如数据清洗技能就配了清洗、去重、缺失值、脏数据、标准化等词配合描述里的语义匹配一起用准确率比纯语义匹配高不少。执行阶段还要考虑状态传递的问题。技能不是完全独立的前一个技能的输出常常是下一个技能的输入。我在执行上下文ExecutionContext里搞了一个共享KV缓存区每个技能执行完后可以把结构化产物放进这个区后面的技能按key取用。这个设计很像Unix管道每个skill做好自己的事把产物留给下游大家不用了解彼此实现细节。2.4 为什么这样设计选型与取舍的底层逻辑可能有朋友会问直接用现成的工作流编排框架不香吗非要自己搞一套skill逻辑我的回答是工作流和skill解决的是两个不同的问题。工作流适合固定流程比如每天早上8点拉数据-生成报表-发邮件这个流程十年不变当然可以用工作流写死。但Agent面对的是开放任务比如帮我把这个文件处理一下——你怎么预先定义一个固定流程skill的设计目的是为模型提供应对开放任务的标准化模块它不强制流程而是给模型足够多的材料和脚手架让模型自己做决策。把skill和工作流结合是正解工作流负责稳定的主干skill负责可变的支线。另外一个关键取舍skill的定义到底是让模型自己写还是人写我初期试过全自动方案让模型自己生成技能的SKILL.md最后结果非常不可控——模型生成的语言质量虽然不错但经常写出幻觉指令比如让工具去调用一个根本不存在的能力。后来折中成人写核心流程、模型补充扩展。人工负责定义技能的骨架和边界模型可以根据具体任务生成细分的执行子步骤两边结合起来既有质量保障又有灵活性。3. 实操从零实现一个可用的数据清洗skill概念讲一堆不如直接上手做一个。这一节我用数据清洗为例子完整走一遍一个skill从定义到跑通的全过程。选这个例子是因为它足够典型——有工具调用、有规则约束、有输入输出校验技能化收益非常明显。3.1 需求边界与输入输出定义先明确这个技能要干什么接收一份表格数据JSON或CSV格式执行以下几类操作——剔除重复行、删除全空行、填充/标记缺失值、纠正字段类型比如把数字字符串转成int、标准化字段名映射比如把u_name映射为user_name。最后输出一份清洗后的结构化数据和一个清洗报告。边界要想清楚别什么事都往skill里塞。比如这里的数据是不是有业务含义上的异常不是我这个清洗skill该管的统计报表怎么生成也不是。这个技能只管格式和完整性层面的清洗不要去碰业务逻辑。输入和输出定义我直接写进rules.yamlname: data_cleaning version: 1.0.0 description: 清洗结构化表格数据处理去重、缺失值、类型纠正、字段映射 match_keywords: [清洗, 去重, 缺失值, 脏数据, 标准化] tools_required: [read_table, write_table] output_schema: type: object properties: cleaned_table: { type: array, description: 清洗后的数据行列表 } cleaning_report: { type: object, description: 清洗报告 } required: [cleaned_table, cleaning_report]3.2 SKILL.md的写法与prompt工程细节SKILL.md我是这样写的# Skill: data_cleaning ## Purpose 对结构化表格数据执行基础清洗操作使其满足后续处理与入库要求。 ## When to use - 用户提供CSV/JSON/Excel等表格数据 - 用户要求清洗去重填缺失值标准化格式 - 数据来自多个不同来源需要统一字段与格式 ## When not to use - 数据已经是规范的结构化格式且用户未提出清洗诉求 - 用户需要在清洗基础上做业务分析或可视化这是其他技能的工作 ## Steps 1. 读取完表格数据后先扫描一遍全表结构字段名、行数、每列非空比例。 2. 做字段映射如果存在语义相同但命名不同的列统一命名为目标标准名映射关系记录到清洗报告中。 3. 处理重复行完全重复的行直接删除关键字段重复但其他字段不相同的标记为疑似重复在报告里说明。 4. 处理缺失值数值型列优先用中位数填充类别型列用模式填充如果缺失比例超过50%建议在报告里提示该列是否需要保留。 5. 纠正类型把123这类数字字符串转为int把1,234.56这类千分位字符串去逗号后转float日期字符串统一为YYYY-MM-DD格式。 6. 输出清洗后数据和清洗报告。 ## Constraints - 不要臆测缺失数据的业务含义填充只是技术占位 - cleaning_report必须包含每类操作影响的行数详情 - 当发现字段无法自动映射时保留原始字段名并在报告中标记为warning这里每个步骤都用动作条件产出的结构来写。实际测试中我发现模型非常擅长按照编号步骤逐步执行但前提是每个步骤都给了可判定的操作条件——缺失比例超过50%就要提示这种不带模糊地带。3.3 工具调用与校验逻辑的实现SKILL.md是给模型看的说明书而真正动手处理数据的还是工具和脚本。我把清洗逻辑的主要动作放在一个Python脚本里模型负责按说明书调用脚本、解释结果而不是让模型自己逐行处理大数据这能避免很多不可控的性能和精度问题。核心脚本我做了一个这样的封装# scripts/clean.py import pandas as pd import json, sys def clean_table(df, config): report {removed_duplicates: 0, filled_missing: {}, type_corrections: [], column_mappings: {}, warnings: []} # 字段映射 mapping config.get(column_mapping, {}) df df.rename(columnsmapping) report[column_mappings] mapping # 去重 subset config.get(dedup_on, None) before len(df) df df.drop_duplicates(subsetsubset) report[removed_duplicates] before - len(df) # 缺失值处理 fill_config config.get(fill_strategy, median) for col in df.columns: null_count df[col].isna().sum() if null_count 0: continue if null_count / len(df) 0.5: report[warnings].append(fcolumn {col} missing rate 50%, consider dropping) continue if fill_config median and df[col].dtype in [int64, float64]: df[col] df[col].fillna(df[col].median()) report[filled_missing][col] int(null_count) elif df[col].dtype object: df[col] df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else unknown) report[filled_missing][col] int(null_count) return df, report模型不直接执行脚本里的这些逻辑而是通过一个execute_skill_tool函数去调用脚本然后把结果拿回来。这个设计有两个好处一是脚本可以被单测覆盖二是模型不需要深入了解pandas API它的职责变成理解报告、向用户解释、决定下一步更贴近大模型的强项。工具调用在Agent跑起来后长这样我贴一段简化的伪代码展示实际执行链路# 伪代码Agent环境中执行skill的主流程 def run_skill_in_agent(skill_name, task_input, agent_context): skill_meta skill_registry.get(skill_name) skill_doc read_skill_doc(skill_meta[SKILL.md_path]) agent_context.inject(skill_doc) # 注入SKILL.md agent_context.inject_system_message(现在你将执行data_cleaning技能请先读取输入数据。) # Agent自行决策调用工具 next_action start while next_action ! finish: response agent_context.step(next_action) next_action parse_action(response) # 可能是 call_tool / reply if next_action call_tool: tool_name, tool_args parse_tool_call(response) if tool_name execute_skill_tool: result call_skill_script(skill_meta, task_input, tool_args) agent_context.add_observation(result) else: result call_normal_tool(tool_name, tool_args) agent_context.add_observation(result) elif next_action reply: final_answer response break validate_output(skill_meta[output_schema], final_answer) return final_answer调试时你会看到模型有时候会跳过某个步骤或者自作主张合并两个步骤所以我特意在SKILL.md里加了不要跳过步骤的提示。实际跑下来模型还是大概率遵循编号流程的因为每一步都有可实操的指示——这比写一堆请认真处理数据强太多了。3.4 测试用例与效果验证技能写完不等于完事一定要配测试。我给data_cleaning配了四组测试正常数据清洗、完全没脏数据应该快速通过并给出零操作的报告、极端脏数据比如90%缺失率、所有字段类型都错、带未知字段的数据应该触发warning而不是报错。测试跑起来效果很明显。最关键的发现是有SKILL.md加持的模型在处理未知字段时表现是稳定的——它不会尝试删除这些字段也不会强行映射而是保留并标warning。没有SKILL的时候模型经常自作主张把未知字段丢掉。这个差距直接告诉我技能文档不是摆设它在实打实地约束模型的输出行为。这个skill从需求拆解到测试跑通前后用了大概半天时间。后面我又按同样套路封装了周报生成、自然语言转SQL、网页文本抽取、批量翻译等技能每个都在2-4小时内搞定边际成本比裸写prompt低太多了。4. 避坑经验我在skills落地过程中踩过的7个坑这个章节是我最想写的因为踩过的坑才是真正的财富。每个坑花了不少时间和token希望你能绕过去。4.1 问题速查表坑位现象错误原因正确做法技能描述含糊模型选错技能调度器靠描述匹配描述写得太窄或太业务化描述同时写清适用和不适用场景并配关键词SKILL.md塞满指引模型注意力被稀释正文超长步骤优先级不分文档只保留必要步骤细节放工作流或脚本每次全量加载token消耗翻倍不区分技能元信息和正文元信息常驻正文按需加载技能命名业务化无法复用技能绑定具体项目术语技能命名和描述抽象为通用能力脚本无异常处理中途失败时模型编结果工具调用出错没有回传状态脚本加try-except返回结构化error没有版本管理改坏找不到原因SKILL.md改了不留备份每个技能都用git目录管理带上版本号校验松散输出格式漂移只校验是否返回不校验schema用JSON Schema严格校验输出4.2 第一个坑技能描述写得像论文Agent根本选不对我第一个封装的技能是活动策划给它的描述写了整整两百多字的业务背景、目标用户、策划原则结果调度时模型经常把这个技能匹配给写文章做表格的任务误触发率高得吓人。后来我把描述精简成二十多个字加上5个触发关键词误触发率立刻降下来了。这个教训就是技能描述不是项目文档是给调度器看的标签。要像写搜索索引摘要一样写技能描述精准、短小、关键词明确。越长越模糊模型越没法准确匹配。特别是When not to use这个字段我强烈建议写上它在实际效果上排除误触发的贡献比适用场景还大。4.3 第二个坑SKILL.md里塞了太多组织希望模型知道的背景知识早期我有个执念觉得技能文档信息越全模型发挥越好。结果有一次给一个数据分析技能写满了公司业务的复杂度说明模型在处理开放数据时反而变得畏首畏尾经常在如何分析上过度纠结输出速度明显变慢。后来我把SKILL.md和背景知识库分开了。SKILL.md只负责怎么完成这个技能背景业务知识放到另一个专门的上下文里按任务决定要不要注入。分工明确之后模型在技能执行上更利索在业务理解上也不受影响。这个边界感特别重要。4.4 其他几个坑的补充说明技能命名别太业务化。有个技能我一开始叫订单金额汇总后来发现它本质是按条件做数值聚合改了名字和描述之后其他项目也能复用了。脚本要写异常处理。模型不会因为你脚本抛异常就停下来反思它更可能编造一个结果继续跑。所以脚本里对每个可能出错的地方都要包一层try-except把错误信息结构化地返回给模型。模型看到明确错误信息才能正确选择重试或跳过。版本管理一定要做。我吃过一次亏改SKILL.md的时候把一版已经调得很好的prompt覆盖了还没法回退。后来所有技能都放git里管理每次修改留commit出了问题能diff。输出校验要严格。模型输出的JSON经常出现字段名大小写漂移、多出奇怪的嵌套必须用schema校验。校验失败就回退重试一次连续失败再报错给用户。4.5 调试skills的实用工具围绕skills调试我建了一套轻量工具链都是自己写的开源界也有类似的思路核心还是帮你快速定位问题。技能回放器记录每次Agent调用了哪些技能、每个技能往上下文注入了什么内容、模型是怎么一步步决策的。回放器让我能看到问题出在调度层还是执行层特别好用。匹配打分器给一个输入输出每个技能的匹配得分。调试为什么选错技能的利器。单测脚本每个技能目录底下的tests用pytest跑快速回归验证。改动SKILL.md之后跑一遍能在上Agent前发现prompt变更有没有引入新问题。token消耗监控器统计每次请求里技能正文占用多少token。这个数据会让你知道按需加载到底帮你省了多少钱。调试里最反直觉的一条经验是当模型执行一个技能效果很差时不要急着调prompt先看看是不是技能匹配错了。匹配错了后面怎么调都白搭。我至少有一半的技能效果不好问题最后都定位到是调度阶段把不相关的技能加载进来了。5. 效果评估与演进怎么判断一套skills体系到底好不好5.1 评估指标不只是任务成功率技能化改造完成后我给自己定了几个硬指标来做前后对比直接用数据判断这套体系值不值得。任务成功率改造前裸prompt模式下大概是71%引入skills后提升到了89%。提升主要来自固定步骤带来的稳定性提升传统prompt模式下模型经常漏掉一些隐含步骤。token消耗按需加载和控制注入内容长度之后每次任务平均消耗降低了约40%SKILL.md通常只占上下文中很小的一部分比起把整个技能体系塞进system prompt省得多。平均执行时长如果只是简单任务skill的加载和匹配本身会增加一点延迟但因为出错和重试大幅减少整体耗时反而降了。跨任务复用度这是我最看重的指标。我数了一下6个核心技能里有4个在至少3个不同的项目里被复用。这个指标说明skill封装抽到了真正通用的能力层而不是每个项目都在重复造轮子。5.2 从静态skill到动态技能组合目前版本还是每个技能自己管自己下一步我想做的是让skill支持动态组合。比如数据分析可能由数据清洗数据可视化得到洞察组合而成Agent可以在运行时动态编排这些基础技能而不是人为预先写死一个数据分析大技能。这个方向我在冥想设计时有几个方案可选用LLM直接生成工作流编排脚本或者用预定义的组合模板也可以让skill之间声明依赖关系自动串联。目前更看好的是模板LLM微调的路子——模板保证稳定性LLM做灵活插入防止完全不靠谱的自由发挥。另一个演进方向是给skill加版本语义化比如v1.0.0表示大版本变更v1.1.0表示规则微调这样多个线上项目可以锁定某个技能版本避免上游改动影响下游稳定性。和代码依赖一个道理Agent系统复杂到一定程度这个问题一定躲不掉。我个人做agent-skills这套东西最大的体会是表面看是在写prompt和脚本其实是在梳理人怎么把一件复杂事情拆成可执行的步骤。技能不是越多越好而是每个技能都要有清晰的边界。把边界画清楚了Agent自然就稳了。最后补一个小技巧不要一开始就把全部技能开发完先挑一个高频任务反复打磨出第一个skill把整个流程跑通后续复制起来会快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑