
折腾过一阵 AI Agent 的朋友应该都有过这种体会模型再聪明如果没有趁手的工具它也只能跟你“纸上谈兵”。而把这些工具以**技能Skill**的形式沉淀成一个体系让 Agent 能自主发现、调用、编排这件事做得好不好直接决定了你的 Agent 是“能聊”还是“能干”。这篇想聊的agent-skills就是围绕“技能体系”来做文章的项目思路。它把散落在代码里的函数、API 调用、提示词片段统一抽象成带描述、带参数、带权限边界的“技能单元”再通过一套注册和调度机制让 Agent 按需取用。如果你正在搭建自己的智能体或者想把现有的自动化流程升级成会自主学习工具的形态这篇应该能给你一套可以落地参考的框架。1. 整体设计与思路拆解1.1 为什么 Agent 的强弱取决于技能库而不是模型本身先说一个我踩过的坑。早期我搭 Agent 时总想着“换更强的模型”觉得模型聪明了任务就能搞定。结果换了一圈发现笨的不是模型是它根本不知道你能为它提供什么。它很会推理但没有“手”去执行。就像你给一个实习生配了一台顶级电脑却忘了告诉他公司内部有哪些系统、分别怎么登录、什么流程找谁办——他再聪明也只能干坐着。技能库本质上就是 Agent 的“岗位手册”把那些它能调用的外部能力用它可以理解的语言描述清楚。模型负责的是“理解任务、拆解计划、匹配技能”技能库负责的是“我能做什么、怎么做、边界在哪”。两者配合才是一个完整的 Agent。否则每次接到新需求你都只能临时给它提示词让它自己猜结果就是出错率高、可维护性差、换一个场景全得重来。这个项目的设计思路就是把技能的“定义”“注册”“调度”“执行”拆成独立的层次。它不是把代码写死在某一个 Agent 内部而是让技能成为一个可插拔、可复用的独立单元。这就跟软件工程里的微服务思想一脉相承你不需要改整个系统只需要增删一个技能节点系统能力就随之扩展。1.2 技能体系的基本抽象一个技能到底包含什么理解 agent-skills核心是理解“技能”的最小结构。我自己的实践里一个可用的技能至少要包含四样东西第一是技能名必须是语义清晰的短名称比如web_search、calc_price、send_mail让模型一眼就知道它是什么。第二是描述文本用一句到三句话写清楚这个技能解决什么问题、在什么场景下触发、有哪些约束。第三是入参定义声明调用时需要传什么字段类型是什么哪些必填哪些可选。第四是执行函数也就是真正跑逻辑的代码或者 API 调用。这四样东西组合在一起就构成了一张“技能卡”。模型读这张卡就知道要不要用、怎么用。更讲究一点的还会给技能加上权限标注比如这个技能是只读的还是可写的能访问哪些资源。这个下面细说。1.3 从散装函数到技能生态的演进很多人早期的 Agent 项目其实是“散装”的写一堆 Python 函数然后硬编码在逻辑里让模型按分支去调用。这个方式在技能少于十个的时候没问题一旦多起来就开始乱——模型经常选错函数、参数传错、函数之间互相覆盖状态。更麻烦的是你加一个新工具时得去改主逻辑代码越改越脆弱。技能库的抽象解决的正是这个扩展性问题。你把每个能力封装成独立技能之后主逻辑就只剩下一件事匹配与调度。新增能力 新增一个技能文件不需要动调度代码。技能之间也可以互相组合一个技能的输出可以作为另一个技能的输入这就为“编排”留下了空间。用行话讲这叫让 Agent 具备“工具性使用能力”说白了就是让它像熟练工一样知道什么时候拿哪把扳手。2. 核心细节解析与实操要点2.1 技能描述该怎么写这决定了模型找不找得到你我在 agent-skills 项目里最先打磨的不是代码而是技能描述。可以说80%的技能调度问题都出在描述写得太烂。模型是一个文本匹配机器它会根据你的描述去判断“这个技能是不是适合当前任务”。描述模糊它就犹豫描述里堆砌空洞的营销词它就可能乱选。给你看一个反例这个技能可以用来搜索信息...功能非常强大...内容很丰富...——这种描述丢给模型它完全不知道什么时候该调。正确的写法是带“触发条件”和“典型场景”的描述。比如在用户想了解时事新闻、查阅最新资料、获取某个实体的背景信息或需要引用外部来源作为回答依据时使用web_search技能。请将用户问题转化为简明关键字段作为查询参数。注意当用户要求的是数学计算时不要调用此技能。这里的关键是**“正例场景 反例排除”**。把什么时候该用、什么时候不该用都说清楚模型才能形成准确的触发条件。这个思路本质上类似于训练数据里的负样本——模型需要知道“不做什么”才能减少幻觉式调用。还有个技巧描述里可以加入“调用成功后能得到什么”。比如“调用后返回最多 10 条结果每条包含标题、摘要、URL可用于后续总结或直接引用”。让模型预判到调用结果它就能在规划阶段更合理地安排步骤。别小看这句话实测下来能明显减少模型“调了技能之后不知道拿结果干嘛”的空转问题。2.2 参数定义宁可严格不要宽容技能的参数定义是另一个坑很多的地方。我见过不少技能定义里只写一个简单的字符串“query”然后所有复杂结构全用一个 JSON 字符串塞进去。这样对模型来说不是更简单而是更模糊——它得自己解析、拼装很容易出错。建议直接采用严格的 JSON Schema 风格。把参数名、类型、必填性、默认值、约束范围都写清楚。例如一个send_mail技能的参数to: 数组必填收件人邮箱列表每个元素需符合邮箱格式subject: 字符串必填长度不超过 200 字符body: 字符串选填默认空串支持纯文本或 Markdowncc: 数组选填抄送列表priority: 枚举字符串选填可选low、normal、high默认normal为什么这么严格因为模型是概率生成它会倾向“猜”。如果你的参数定义留了太多模糊空间它就会往里面塞一些你不知道会变成什么的东西。参数边界就像施工护栏限制越多出错空间越小。反过来你定义得越清晰模型生成有效调用的概率就越高。另外参数说明里要写“单位”“格式”“约定”。比如时间参数要注明是时间戳还是YYYY-MM-DD HH:mm:ss坐标要注明是经纬度还是火星坐标。这些常识对程序猿来说是“不用说的”但对模型来说这些就是天书。2.3 技能的权限与沙箱边界别让 Agent 拥有超能力再聊一个安全层面的细节。Agent 在拥有技能后权限边界不设清楚后果是很严重的。你的 Agent 可能拥有读取邮箱、发邮件、删文件、调用支付接口的能力——如果这些技能没有任何权限控制一旦提示词被注入或者模型判断失误就是事故。我在技能体系里加了一组“权限标记”readonly、write、admin。读写类技能默认不让 Agent 无限使用而是要求它必须得到显式的配置授权高危操作比如删除、付款、发送消息则在技能执行前增加一道“人工确认钩子”程序只有拿到确认信号才会继续跑。这个设计在单用户场景下会有点繁琐但在生产环境里是必须的。原则很简单技能可以多权限不能松。给 Agent 一个它用不到的高危技能和把家门钥匙交给一个陌生人没有本质区别。另一个容易被忽略的是“副作用声明”。技能执行会不会改变外部系统的状态会不会消耗费用在技能定义里加上副作用字段比如副作用: 发送一封真实邮件。这么做不仅能让人审查时一目了然也能让调度器在编排时考虑组合策略。有的技能适合“先试运行、确认后再提交”有的技能适合“直接执行”差别就在于有没有副作用。3. 实操过程与核心环节实现3.1 仓库结构一个技能就是一个文件夹按 agent-skills 的思路一个技能目录应该长这样skills/ ├── web_search/ │ ├── skill.yaml # 技能元信息名称、描述、参数、权限 │ ├── SKILL.md # 给模型看的完整说明文档 │ └── main.py # 实际执行的函数代码 ├── calc_price/ │ ├── skill.yaml │ ├── SKILL.md │ └── main.py └── send_mail/ ├── skill.yaml ├── SKILL.md └── main.py这种“一个技能一个目录”的组织方式的优势是职责单一、便于测试、便于多人协作。每个技能是独立的包不跟其他技能共享全局状态出了问题可以单独回滚。你甚至可以给不同技能设定不同的开发者和更新频率这跟开源社区里“一个仓库一个包”的思路一致。skill.yaml是技能的“身份证”主要配置项包括name、description、parameters、permissions、side_effects。SKILL.md是给模型读的扩展文档比如常见的错误示例、参数边界说明、返回数据格式参考。main.py是真正干活的代码它接收的参数就是从skill.yaml里定义的字段解析出来的。3.2 从零写一个技能以“查天气并写简报”为例光说概念太虚带你走一个完整例子。假设我们要做一个技能功能是根据城市名查实时天气并生成一段适合汇报的简报文本。那这个技能该怎么做第一步定义skill.yamlname: fetch_weather_report description: 在用户询问某个城市的当前天气、温度、风力、降水概率或需要天气信息来安排出行时 使用本技能。返回天气状况和一段适合直接引用的文字简报。 注意当用户只问“明天天气如何”但没有指定城市时先向用户确认城市不要直接调用。 parameters: city: type: string description: 城市名称使用中文如“北京”“上海”。 required: true unit: type: string description: 温度单位可选 celsius 或 fahrenheit。 required: false default: celsius permissions: mode: readonly第二步写main.pyimport requests def fetch_weather_report(city: str, unit: str celsius): # 调用公开天气 API这里用示例接口地址 resp requests.get( https://api.example.com/weather, params{city: city, unit: unit}, timeout10, ) resp.raise_for_status() data resp.json() temp data[current][temp] cond data[current][condition] wind data[current][wind_level] summary f{city}当前温度为 {temp} 摄氏度天气{cond}风力{wind}级。 if data.get(forecast): tomorrow data[forecast][0] summary f预计明天{ tomorrow[condition] }最高温{ tomorrow[high] }度。 return {raw: data, summary: summary}第三步在SKILL.md里补充模型需要知道的细节# fetch_weather_report - 返回结果的 raw 字段是原始天气数据summary 字段是已生成的文本简报。 - 当接口返回 404 或超时返回错误码 WEATHER_FETCH_FAILED不要自行编造天气数据。 - 生成简报时务必包含城市名、当前温度、天气状况三项再按需补充风力、湿度、体感温度。这里要特别强调一下“返回错误码”的设计。很多技能实现忽略了这个细节导致模型在技能执行失败后居然“脑补”了一个结果继续回答用户这是大忌。技能执行失败时一定要返回结构化的错误标记并让 Agent 知道要如实告知用户而不是瞎编。3.3 技能的注册与挂载让 Agent 看见你的技能写完技能只是第一步还要让 Agent 运行时能发现它。通常有两种挂载方式一种是启动时扫描Agent 启动时遍历skills/目录把每个skill.yaml都读进来生成技能列表然后注入到系统提示词里另一种是动态发现运行过程中按需加载适合技能特别多的场景。启动扫描的实现很直接import yaml from pathlib import Path def load_skills(skills_dir: str): skills [] for skill_file in Path(skills_dir).glob(*/skill.yaml): with open(skill_file, r, encodingutf-8) as f: meta yaml.safe_load(f) skills.append(meta) return skills然后把这些技能的name、description、parameters格式化后拼进系统提示词。你要注意一个问题系统提示词是模型的主工作记忆空间塞满了模型的推理能力会下降。所以不是技能越多越好而是要在每轮对话里只把当前任务可能相关的技能暴露给模型。这就需要一个“技能预选器”——它先根据用户问题从技能库里检索出 top-k 个候选技能再拼进提示词。这个预选可以是简单的关键词匹配也可以用向量检索。我在实际项目中用的是轻量级方案给每个技能维护一组关键词和场景标签然后用 BM25 做检索效果已经足够。这一步就像“先把候选资料放在面试官桌上再让面试官去翻”能大幅减少模型分心。3.4 技能调试你自己要能手动模拟调用技能上线前一定要做一次“人肉调用测试”。方法很简单——写一个命令行脚本模拟 Agent 传参给你的技能然后看输出。比如python -m skills.fetch_weather_report --city 杭州 --unit celsius为什么要强调这个因为很多技能开发者在调试时习惯直接用“Agent 对话”来测一旦出错很难分清是模型的调度问题还是技能本身的 bug。先把技能当普通函数测好再加 Agent 的上层调度这样排查问题的速度能翻倍。没有经过单测的技能就像没有试过水的救生衣看着没问题真用的时候要出大事。4. 常见问题与排查技巧实录4.1 技能很多但模型总是选错技能这个问题我遇到太多次了。技能库里塞了几十个技能模型偏偏在算数学题时去调天气查询。排查下来大部分原因是描述之间“语义距离”太近。比如你有get_weather_by_city和get_city_list两个描述里都反复出现了“城市”“查询”这些词模型就容易混。解决办法有两个方向。第一拉开描述差异把触发场景写得更具体比如给get_weather_by_city专门加一句“仅当用户关注气候、温度、风力时才使用查询城市列表请用另一个技能”。第二在技能预选阶段用向量检索做粗筛而不是让模型从全量技能里去大海捞针。粗筛后再让模型精挑准确率会明显提升。另外还有一种情况是“模型故意不用技能”。很多模型在系统提示词里的技能没有明确强制优先级时会倾向于用自己的知识回答。这时候你可以在提示词里加一条规则“如果技能列表中存在与当前任务直接匹配的技能你必须优先调用技能不能仅凭常识回答。”规则要写得像合同条款一样明确不给模型留模糊空间。4.2 上下文窗口被技能文档撑爆技能写得越详细提示词越大提示词越大可用上下文越少上下文越少模型越容易“健忘”。我把 30 个技能的完整描述塞进提示词之后模型简直就像失忆了一样连用户刚说的话都记不住。这里就引出一个关键原则你以为你是在给模型提供帮助其实你是在压缩它的工作记忆。技能文档不是越多越好而是越精炼越好。我给技能元数据设计了三级摘要一级是一句话的极简描述用于初筛二级是 100 字左右的触发条件和调用约定用于精排三级是完整的SKILL.md仅在技能被选中后通过“后续回合注入”的方式提供。这样一来初始提示词里每个技能只占两三行剩下的大段文档在执行阶段按需加载。“按需加载”执行起来就是Agent 决定调用某个技能时系统再把该技能的完整说明追加到对话里。这个机制用代码实现就是“拦截到技能调用信号再附加上下文”。一开始设计会觉得啰嗦但它是治理上下文膨胀最有用的方法没有之一。4.3 技能执行成功但返回空结果模型依然能编这是最让人哭笑不得的 bug技能确实调了API 也返回了但里面数据是空的比如搜索结果没有命中模型却一本正经地编出一段不存在的结论来回答用户。原因在于技能代码返回的数据里没有“空结果”这个概念模型看到空列表不知道这意味着什么。解决方案是把空结果显式化。技能在返回空数据时必须返回一个result_empty: true的标记并在通知文本里写明“未找到相关结果”。同时在上层提示词里强调当技能返回空结果时必须如实回复用户“未找到信息”禁止补充任何虚构内容。这个规则要跟“不得撒谎”并列写进提示词反复出现。在代码层面你也可以在调度器做一层校验检测到技能返回了result_empty就阻断模型对这段结果的“自由发挥”直接把它引导到“追问用户或建议换关键词”的回复模板上。4.4 多个技能连续调用时状态互相污染当任务复杂到需要先后调用两三个技能时状态污染问题就出来了。最常见的一种情况技能 A 的结果被误当成技能 B 的上下文两个技能全局变量名冲突或者一个技能执行到一半崩溃把另一个技能的中间状态也带没了。我在给技能做隔离时用了三个原则分享出来供参考第一每个技能独立进程或独立子目录运行不要让两个技能共享同一个全局变量第二技能之间的数据传递只通过显式的返回值不要通过共享 DB 或缓存区隐式传递——一旦隐式模型根本不知道数据从哪来出错了你也没法定位第三每个技能调用结束后保留一份调用快照入参、出参、时间戳既能回溯问题也能作为后续优化调度的数据。多技能编排做得好的话Agent 会越来越像一个“流程机器人”先查 A再根据 A 的结果查 B最后汇总 C。但这套链路的稳定性完全建立在技能隔离做得好不好上面。状态一旦混乱什么都白搭。结尾小技巧先做三个能打的技能再谈生态最后再分享一条我个人在实践里的感受。每次启动 agent-skills 这类项目我都会克制住“一次性建一堆技能”的冲动。因为技能写得多不等于 Agent 聪明。真正让它变得有用的是那几个跟你的核心场景强相关的、打磨得足够细致的技能。我的建议是先挑三个最高频、最痛的动作做出来每个技能都写清楚描述、严格定义参数、补好权限边界、加上错误处理。跑通三个技能之后再去看怎么扩充技能库。这个节奏走下来你会发现自己对“技能设计的感觉”会越来越准——到什么程度算描述清晰、什么情况下模型会误判、怎么隔离数据更省心这些手感只有亲手做过了才会有。如果你也正在捣鼓 Agent 的技能体系希望在动手之前停下来想清楚一件事你给 Agent 的不是一堆函数而是一套可以描述、可调度、可进化的能力图谱。从这个角度出发去设计路会比你想的更顺。