
我最早入坑 Agent 开发的时候总以为只要把大模型的上下文塞满工具描述、再配个 ReAct 提示词就能让智能体乖乖干活。结果呢模型是调用了工具但经常选错工具、传错参数、甚至同一个功能换个说法就完全不会用了。那时候我意识到问题不在模型而在我们给模型的东西太散了——没有结构、没有边界、没有标准。后来接触了 agent-skills 这个思路等于把散落一地的工具函数升级成了有目录、有说明、有使用规范的标准件。这篇我就把这块的实践经验完整拆开讲从概念到设计再到落地希望能给正在搭 Agent 应用的朋友省点弯路。你如果现在正在做 AI Agent 相关的东西不管你是工程师、产品经理还是独立开发者只要你的智能体要干的不止聊两句而是真正完成一连串任务那技能Skills这一层迟早要补上。它能解决最核心的三个问题让模型在正确的时候用正确的能力、让能力的输入输出稳定可控、让复杂任务能被拆成可复用的小模块。我下面会用一个完整的销售月报技能案例把从设计到调用的全流程走一遍。1. Agent Skills到底是什么从乱拳打死老师傅到按章办事1.1 先搞清楚Skill、Tool、Prompt三者的边界很多朋友第一次接触 agent-skills 时最困惑的就是它和 Tool、Prompt 到底有什么区别。我一开始也混着用后来踩了坑才慢慢把边界摸清楚。先说 Tool工具函数。它是一个具体的执行单元比如查询数据库、调用某个 API、发一封邮件。它接收参数返回结果本身不包含决策逻辑。Tool 解决的是**能做什么**的问题。再说 Prompt提示词。它是给模型的自然语言指令。Prompt 解决的是**怎么用**的问题告诉模型你要干什么、按什么格式输出、参考什么上下文。那 Skill 是什么我后来想通了一件事Skill 是 Tool Prompt 触发条件 输入输出规范 的组合封装。它不只是给模型一个函数而是告诉模型这个技能适合在什么场景下用、调它之前需要准备什么、它会用哪些工具、最后以什么格式给你结果。我习惯用一个生活化的类比来解释三者关系。你把 Tool 想象成一把螺丝刀Prompt 是使用说明书而 Skill 是一整套更换灯泡的标准作业流程。它定义了什么时候该换灯泡触发条件、需要哪些工具依赖工具、按什么顺序操作执行流程、换完怎么验收输出规范。模型不缺工具缺的是一套告诉它怎么把工具用对的标准流程Skill 补的就是这一层。三者对比我整理了一个表方便你快速看明白维度ToolPromptSkill核心内容可执行的函数自然语言指令组合封装层回答的问题能做什么怎么用什么时候用、用什么、怎么配合稳定性高代码执行低模型输出中高通过约束降低随机性复用粒度单一操作单一场景完整任务/流程模块间关系独立无结构可编排、可依赖1.2 为什么说技能抽象层是Agent落地的关键讲真如果你的 Agent 只需要干一两件简单的事不搞 skill 体系完全没问题。你直接写个 system prompt塞两个 tool就能跑。但一旦你的 Agent 要处理的任务多起来——比如既有数据分析又有邮件撰写还有日历管理——协议的复杂度就上来了。我实测下来没有技能抽象层的 Agent 会有三个典型症状第一个症状是模型频繁选错工具。你没有给工具足够的应用场景上下文模型只能靠猜。它可能用写邮件的能力去回复客服工单或者用分析图表的能力去处理纯文本数据。结果是输出结果离预期差得十万八千里。第二个症状是参数传递全靠运气。我之前见过一个项目同一个生成周报需求有些用户问帮我把这周工作整理一下有些问写个周总结。模型有时候把项目列表传进总结进度参数有时候把工作描述传进负责人参数。不是模型笨是我们没定义一个稳定的输入 Schema。第三个症状是完全无法编排复杂任务。真实业务里没有几个任务是单工具能搞定的。你要做一份销售月报可能要查 CRM、拉数据、算环比、生成柱状图、写分析结论、最后按格式导出。没有技能层模型每一步都在自由发挥你根本无法预期它会先干什么后干什么。加了一层 Skill 之后你其实是在给模型做确定性兜底。把那些容易出错、容易自由发挥的环节都用标准化的输入输出限定死。模型只需要做一件事判断当前该调用哪个技能然后把参数填对。决策范围变小了错误率自然就降下来了。所以我一直觉得Agent 能否落地的关键不在于模型能力多强而在于你有没有把业务约束转化成一套清晰的技能协议。模型再聪明也需要一套规则告诉它在我们这个系统里应该怎么干活。2. 设计一套Agent技能体系从命名到协议的全过程2.1 技能描述的黄金五要素理解了什么 skill 下一步自然是怎么设计。我前前后后改了四版方案最后沉淀出一个黄金五要素框架每个技能描述都围绕这五个维度来写缺一不可。第一个要素是技能身份Identity。它包括技能的唯一 ID、名称、一句话描述。名称要短让人一眼看懂描述要明确说明这个技能解决什么问题最好带上使用场景的关键词。比如sales_monthly_report就比report_tool强得多。我一般会要求描述控制在 20 个字以内太长反而干扰模型的判断。第二个要素是触发条件Triggers。这是很多设计者最容易忽略的。你需要明确写出什么情况下该用这个技能什么情况下不该用。我通常会在描述里直接写明当用户要求生成月度销售汇总报告时使用但仅含单个产品的日销售数据时不要使用这种情况请用 daily_sales_summary。触发条件写得越明确模型选错技能的概率就越低。第三个要素是输入 SchemaInput Schema。定义了调用技能时需要传哪些参数每个参数的类型、是否必填、取值范围。比如销售月报技能需要数据时间范围、对比基准、输出格式三个参数。注意Schema 不只是给模型看的还要在代码层做校验双保险。第四个要素是执行流程Execution Flow。说明技能内部是如何一步一步执行的。比如第一步查数据、第二步算指标、第三步生成图表、第四步组装报告。这样即使模型需要中途打断或跳过某一步它也能理解整个任务的进度。这里我的建议是流程描述用编号列表每步尽量不超过一行。第五个要素是输出规范Output Spec。规定了技能返回的数据结构包括格式JSON、Markdown、HTML 等、必含字段、甚至输出长度的范围。输出规范最大的价值是让 Agent 在拿到结果后有足够的信息决定下一步动作。比如报告生成完成包含 summary、charts、metrics 三个字段模型看到这个就知道下一步可以执行发送邮件技能了。我实践中发现这五个要素里触发条件和输出规范最影响成功率也最容易被忽视。你宁可输入 Schema 写糙一点也要把这两个写扎实。2.2 技能粒度怎么定太粗没用太细崩溃技能粒度是我认为整个 agent-skills 设计中最难平衡的问题。太粗了比如一个处理所有文档任务的技能等于没做抽象模型还是在黑暗里摸索太细了比如把段落一的第三行字体改成 14 磅这种技能既麻烦又难以维护而且模型大概率会选错。我自己总结了一个经验原则一个技能应该对应一个完整的业务结果而不是一个技术操作。换句话说模型或者用户调用完这个技能后应该得到一件可以直接用的东西——一份报告、一张图表、一封草稿——而不是一个中间状态的碎片。举几个例子。把查询销售数据库立项成技能就不合适因为查询完数据你还得做分析、做报告中间状态太多了。更合理的做法是把生成销售分析报告这个整体立项成技能内部去调用查询数据库的工具。但反过来也不是说技能越大越好。如果一个技能内部要做的事超过五六个步骤或者它要同时操作三个以上互相独立的工具那我建议你再拆一层。比如给客户回邮件这个技能如果它既要查客户历史记录、又要分析情绪、还要写回信、还要抄送销售总监、还要进 CRM 系统这就太复杂了。更好的结构是拆成查客户档案和生成回信草稿两个技能让主 Agent 来编排。我自己的实操心得是先按业务结果拆再按工具边界拆最后按错误率拆。如果你的 Agent 频繁在某个环节出错那个环节就值得单独拎出来做成技能单独做输入输出约束。2.3 技能注册表与依赖管理技能多了之后管理就成了新问题。你不能把这些技能描述直接全塞进 system prompt——上下文根本装不下而且模型会在海量信息里迷失重点。这时候就需要一份技能注册表Skill Registry。注册表本质上是一个索引文件记录了所有技能的信息、当前版本、依赖关系、启用状态。Agent 启动时只加载注册表当用户请求触发某个领域的需求时再按需加载对应的技能详情。这种先查目录、再取内容的方案能有效控制上下文长度。我通常会用一个 YAML 文件来维护注册表skills: - id: sales_monthly_report name: 销售月报生成 version: 2.3.0 depends_on: - crm_data_fetcher - chart_generator enabled: true - id: email_draft name: 邮件草稿生成 version: 1.1.0 depends_on: - customer_profile_lookup enabled: true依赖管理这块很多人一开始不会注意等到技能之间出现冲突就晚了。比如说销售月报生成技能内部要用到图表生成工具如果你同时还注册了一个独立的生成图表技能模型可能会绕开月报技能直接调用图表工具结果数据没查全、报告格式也不对。我的做法是给每个技能标注清晰的depends_on并且在技能描述里明确写本技能已包含图表生成能力请勿单独调用 chart_generator。这种显式的互斥说明能大幅降低模型搞混的概率。3. 实操手把手设计一个销售月报技能3.1 技能描述文件怎么写理论再多不如上手来一遍。下面我拿一个我实际搭过的销售月报生成技能当例子从零到一完整走一遍。技能的目标很明确输入一个时间段输出一份包含关键指标、环比趋势、结论建议的月度销售报告。第一步是写技能描述文件。我会用 YAML因为它可读性好模型解析起来也稳定。下面是一个完整的sales_monthly_report_skill.yamlskill: id: sales_monthly_report name: 销售月报生成 version: 2.3.0 description: 根据 CRM 销售数据生成月度销售汇总报告。 适合在用户请求这个月的销售情况月度汇总上月对比等场景时使用。 注意本技能已包含数据查询和图表生成能力请勿同时调用 crm_data_fetcher 或 chart_generator。 triggers: on: 用户要求生成月度销售报告或询问某时间段的销售绩效汇总。 off: 用户只询问单笔订单状态、或仅需某一天的数据明细时不要使用本技能 这种情况下请使用 order_lookup 技能。 input_schema: type: object properties: period: type: string description: 报告周期格式 YYYY-MM例如 2025-06 required: true baseline_period: type: string description: 对比基准周期格式 YYYY-MM默认上一月 required: false output_format: type: string enum: [markdown, pdf, html] default: markdown required: false execution_flow: - 1. 调用 crm_data_fetcher 获取 period 内所有订单数据 - 2. 计算总销售额、订单量、客单价、同比/环比变化率 - 3. 调用 chart_generator 生成销售额趋势图 - 4. 按 output_format 组装最终报告 - 5. 返回包含 summary、metrics、charts、conclusions 的 JSON 对象 output_spec: type: object properties: summary: type: string description: 一句话总结该月销售表现 metrics: type: object description: 核心指标计算值含总销售额、订单量、客单价、环比变化 charts: type: array description: 图表数据的 base64 或文件路径列表 conclusions: type: array description: 自动生成的亮点与风险点结论 generated_at: type: string description: 报告生成时间 ISO 86013.2 技能核心逻辑实现描述文件解决了模型怎么知道这个技能的问题接下来还要解决技能本身怎么干活的问题。这个技能的核心逻辑我拆成五个函数每个函数对应执行流程里的一步。我惯用的方式是写一个 Python 模块把每个技能封装成独立的类方便做单元测试。import json from datetime import datetime from typing import List, Dict class SalesMonthlyReportSkill: def __init__(self): self.skill_id sales_monthly_report self.version 2.3.0 def fetch_crm_data(self, period: str) - List[Dict]: 从 CRM 拉取订单数据。实际项目中这里会连数据库或调 API。 # 伪代码records crm_client.query(order_date_betweenperiod) return [ {order_id: A1001, amount: 15200.50, date: 2025-06-03, product: 企业版}, {order_id: A1002, amount: 8900.00, date: 2025-06-11, product: 专业版}, # ... 实际可能有几千条这里省去 ] def calc_metrics(self, records: List[Dict], baseline_records: List[Dict]) - Dict: 计算核心指标总销售额、订单量、客单价、环比变化。 total_sales sum(r[amount] for r in records) order_count len(records) avg_order_value round(total_sales / order_count, 2) if order_count else 0.0 baseline_sales sum(r[amount] for r in baseline_records) mom_change round((total_sales - baseline_sales) / baseline_sales * 100, 2) if baseline_sales else 0.0 return { total_sales: total_sales, order_count: order_count, avg_order_value: avg_order_value, mom_change_percent: mom_change, } def generate_chart(self, records: List[Dict]) - List[str]: 生成销售趋势图返回图表二进制文件路径或 base64 列表。 # 伪代码chart chart_generator.create_trend_chart(records) return [/tmp/sales_trend_2025_06.png] def build_conclusions(self, metrics: Dict) - List[str]: 基于指标计算自动给出结论。这是最容易踩坑的地方见文末避坑说明。 conclusions [] if metrics[mom_change_percent] 0: conclusions.append(f本月销售额环比上升 {metrics[mom_change_percent]}%增长势头良好。) elif metrics[mom_change_percent] 0: conclusions.append(f本月销售额环比下降 {metrics[mom_change_percent]}%需关注原因。) else: conclusions.append(本月销售额与上月持平。) return conclusions def execute(self, period: str, baseline_period: str None, output_format: str markdown) - Dict: if not baseline_period: # 默认上一月简单用日期字符串推进 baseline_period self._prev_month(period) records self.fetch_crm_data(period) baseline_records self.fetch_crm_data(baseline_period) metrics self.calc_metrics(records, baseline_records) charts self.generate_chart(records) conclusions self.build_conclusions(metrics) return { summary: f{period} 销售总额 {metrics[total_sales]} 元环比变化 {metrics[mom_change_percent]}%。, metrics: metrics, charts: charts, conclusions: conclusions, generated_at: datetime.utcnow().isoformat(), } def _prev_month(self, period: str) - str: 根据 YYYY-MM 计算上一月含跨年处理。 y, m map(int, period.split(-)) if m 1: return f{y - 1}-12 return f{y}-{m - 1:02d}这个类有几个细节值得注意。第一fetch_crm_data里我留了接口实际项目只要替换成真实的数据源就行外部逻辑不受影响。第二calc_metrics里做了除零保护这是真实数据场景必须处理的——你永远不知道某个月的订单量会不会是 0。第三每个函数都有类型注解和 docstring一方面方便自己维护另一方面可以直接把函数名和描述暴露给模型做增量学习。3.3 注册进Agent并完成一次端到端调用技能内部逻辑写完之后需要把它注册到 Agent 的运行时里。我用的框架是自研的一个轻量调度器核心逻辑是启动时加载技能注册表收到用户请求后做一次意图匹配选出候选技能再把候选技能的完整描述注入上下文最后由模型决定调用哪个、传什么参数。注册的代码大致长这样from skill_registry import SkillRegistry from sales_monthly_report import SalesMonthlyReportSkill registry SkillRegistry(./skills_registry.yaml) registry.register(SalesMonthlyReportSkill()) # 启动 Agent 主循环 def handle_user_message(message: str, context: Dict): # 1. 从注册表检索候选技能 candidates registry.match(message) # 2. 把候选技能的完整描述注入 system prompt skill_descriptions \n\n.join(c.to_prompt_block() for c in candidates) # 3. 构建对话消息 system_prompt build_base_prompt(skill_descriptions) response llm.chat(messages[system_prompt] context[history] [user_message]) # 4. 如果 LLM 返回了技能调用则执行 if response.has_tool_calls: for call in response.tool_calls: result candidates[call.skill_id].execute(**call.args) context[history].append({role: tool, content: json.dumps(result, ensure_asciiFalse)}) return final_answer(context[history])我模拟一次真实调用用户说帮我看看 2025 年 6 月的销售情况跟上月比比。这个请求会触发意图匹配sales_monthly_report进入候选列表。模型读取技能描述后构造出的工具调用参数基本是这样的{ period: 2025-06, baseline_period: 2025-05, output_format: markdown }技能执行完返回给模型的输出是一个结构清晰的 JSON{ summary: 2025-06 销售总额 24100 元环比变化 12.4%。, metrics: { total_sales: 24100.0, order_count: 2, avg_order_value: 12050.0, mom_change_percent: 12.4 }, charts: [/tmp/sales_trend_2025_06.png], conclusions: [本月销售额环比上升 12.4%增长势头良好。], generated_at: 2025-07-01T08:00:00Z }模型拿到这个结构化的结果就能顺畅地组织成自然语言回复给用户6 月销售总额 2.41 万元环比上升 12.4%整体增长势头良好。我给你生成了一张趋势图…… 整个过程是稳定的、可预期的不会出现模型凭空臆造数据的情况。4. 常见问题与排查技巧实录4.1 现象Agent死活不调用技能技能明明注册了描述也写得很详细但模型就是不用。这是我最常被问的问题。排查下来九成情况出在触发条件写得过于抽象。我之前犯过的错误是写当用户有销售数据需求时使用这种描述跟没写一样。模型看到一个需求根本判断不出月度汇总算不算销售数据需求。后来我把触发器改成了具体句式当用户提到本月上月月度汇总环比同比增长等关键词且需要输出完整报告时使用匹配率明显提升。另一种常见原因是候选技能太多描述太长。几十个技能的 description 堆在一起模型看不过来就会跳过技能直接瞎猜。我建议一个请求最多让模型在 3-5 个技能之间做选择其他技能一律不进上下文。这需要你的注册表带一套比较好的检索逻辑不能纯靠全量塞。还有一个小技巧在技能描述的最后加一个 Use Case 示例。比如用户帮我看看 6 月卖得怎么样。Agent调用 sales_monthly_report参数 period2025-06。这个示例对模型有很强的引导作用实测能把调用率从 60% 拉到 90% 以上。4.2 现象技能输出让Agent精神分裂有一次我遇到的情况很有意思技能正确执行了数据也是对的但模型在组织回复时凭空又算了一遍指标算出来的数字跟技能返回的对不上。整个对话感觉像精神分裂一样前后说法矛盾。排查了半天原因是技能返回的 JSON 里面有太多未加工的数据模型看着看着就忍不住自己推理了一遍。解决办法很粗暴把技能返回的 summary 字段写得足够完整让模型没有额外推理的欲望。同时在 prompt 里明确写工具的 summary 字段是最终结论不得自行修改或重新计算。另外一个常见问题是输出里塞的东西太多。比如 charts 字段里放了五六张图的 base64直接把上下文撑爆了。我的建议是技能输出只保留最终结果和必要的中介状态能省则省。像图表这种存到临时文件给个路径就够了别把图片数据也塞给模型。4.3 现象多个技能互相打架技能多了以后你会发现一个任务可能被多个技能同时看上。比如生成客户回信这个需求既匹配email_draft技能又匹配customer_profile_lookup技能。模型一犹豫就可能调用错。我在注册表里专门加了一个conflict_rules字段明确定义技能间的互斥和优先级conflict_rules: - primary: sales_monthly_report exclude: [chart_generator, crm_data_fetcher] reason: 本技能已包含图表生成与数据查询能力重复调用会产生冗余或冲突。 - primary: email_draft exclude: [customer_profile_lookup] reason: 本技能会自动获取客户档案无需单独调用查询技能。同时在系统提示词中把这条规则用通俗语言写一遍当销售月报技能生效时不要单独调用图表和数据查询工具。这个双重约束基本能消掉 90% 的冲突问题。还有一个实战技巧输出字段里加上 skill_version 和 processing_log。一旦出了问题你能立刻定位是哪个版本的技能、走了什么流程、在哪个环节出的错。没有这个跨版本排查会非常痛苦。4.4 技能描述里的看不见的坑最后说几个我在写技能描述时反复踩的坑都是文档里不会写的东西。第一个坑是描述里用了否定句式。像如果没有要求不要做 XX这种话模型理解起来容易出偏差。更稳的描述方式是直接给正向指令仅在用户显式提到 XX 的时候才做。少用如果…就…否则多用当…时模型处理起来更稳定。第二个坑是参数名跟自然语言不一致。比如你在 Schema 里定义了period但用户说的是时间范围模型需要额外做一层映射就容易出错。一个取巧的办法在参数描述里把可能出现的自然语言变体写全。比如 period 的描述写成报告周期格式 YYYY-MM用户可能口语化为这个月上个月6月请统一转换为标准格式。第三个坑是技能描述过时。技能改了一版但描述文件没同步更新。模型还在按旧逻辑调用结果参数对不上。我现在给每个技能加了一个 CI 检查每次改代码必须同时更新描述文件并跑一致性测试不然不让合并。5. 最后分享三个让我少走很多弯路的小习惯如果你准备开始做自己的技能库愿意的话可以听听我踩坑攒下来的几个习惯。第一把所有技能当成产品来做。我见过太多人把技能写成一次性脚本能用就行完全不考虑复用。但真正的 agent-skills 体系是一个会持续生长的库每一个技能都要有明确的版本、负责人、测试用例和变更日志。我个人的标准是如果一个技能没办法写 10 条以上的测试用例说明它的边界还太模糊不适合进库。第二用最小闭环验证技能设计。不要一上来就想做全能助手。你先让一个技能跑通一个端到端流程——从用户请求、到技能匹配、到参数补全、到执行、到输出——这个闭环跑通了再复制到第二个技能上。我的第一个技能花了两周才跑顺后面的技能每个基本一两天就能上线核心就是模式已经沉淀下来了。第三定期做技能体检。每个月我会把生产环境的 Agent 日志拉出来统计每个技能的调用次数、成功率、平均延迟、错误类型。调用次数少且成功率低的技能要么是触发条件写得有问题要么是它本身可以用别的方式替代。发现这种技能我倾向直接下线或者合并避免它继续混淆模型判断。做 agent-skills 这件事说到底是在模型的随机性和业务的确定性之间搭一座桥。你没法消除模型的随机性但你可以用清晰的协议把随机空间压缩到可控范围内。这套方法在我自己的项目里已经跑了半年多多个技能并行工作整体稳定性和可维护性都上了几个台阶。每次看到新入坑的朋友还在用堆工具 长 prompt的老路子硬撑我都想把这些经验塞过去。希望你读完这篇能少走我走过的那些弯路。