1. 从"agent-skills"这个标题能读出什么第一次看到"agent-skills"这个词,我脑子里蹦出来的不是某个具体框架,而是一类正在快速成型的东西:把智能体(agent)的能力拆成一块块可插拔、可复用、可组合的"技能单元"。这个标题本身信息量不大,正文和关键词都是空的,但恰恰是这种"空",说明它更像一个概念容器——一个用来装"智能体技能体系"的容器。我打算基于这个标题,结合当前智能体开发的常见实践,把"agent-skills"这件事从概念到落地讲透。先说清楚它是什么。agent-skills 指的是围绕智能体构建的一套技能抽象层:把"查天气""读文件""发邮件""调数据库""做网页检索"这类具体动作,封装成一个个独立、可描述、可被智能体按需调用的技能模块。它解决的问题很直接——过去我们做一个智能体,往往是把所有能力硬编码在一个大提示词或者一个大函数里,改一处牵动全身,复用基本靠复制粘贴。有了技能层之后,能力变成积木,智能体变成拼装台。它适合谁看?三类人最该关注。第一类是正在做智能体应用但被"提示词越写越长、逻辑越理越乱"折磨的开发者;第二类是想把内部工具、内部系统接入智能体,却不知道怎么抽象接口的工程同学;第三类是对智能体感兴趣、想动手做一个能真正干活的小助手,而不是只会聊天的玩具的爱好者。不管你是哪一类,这篇内容都会给你一套能直接抄作业的思路和代码骨架。我先把结论摆在这:agent-skills 的核心价值不在于"技能多",而在于"技能边界清晰、描述规范、组合可控"。技能写得再多,如果每个技能都含糊其辞、职责重叠,智能体照样会乱调、错调、漏调。所以下面我会重点讲清楚技能怎么定义、怎么描述、怎么被选中、怎么组合、怎么排错,这些都是我实际踩过坑之后总结出来的东西。2. 技能抽象层到底解决了哪些真实痛点2.1 硬编码智能体的三个典型崩溃现场在讲技能层之前,得先讲清楚没有它的时候会出什么事。我见过太多智能体项目死在三个地方。第一个崩溃现场是提示词膨胀。一开始你写个智能体,提示词两百字,能查天气、能算数,挺好。后来要加读文件、加发消息、加查数据库、加调用内部接口,提示词涨到三千字。这时候模型开始"记不住"——前面定义的规则到后面就忘了,工具描述互相打架,调用格式时对时错。你改一个工具的描述,可能把另一个工具的行为带偏。这不是模型不行,是你把所有信息塞进一个上下文里,超出了它稳定处理的边界。第二个崩溃现场是逻辑耦合。所有工具调用逻辑写在一个大函数里,用一堆 if-else 判断该走哪条路。加一个新能力,就要在函数里再插一个分支,还要小心别破坏原有分支。测试的时候更痛苦,你没法单独测"读文件"这个能力,因为它和"发消息"的逻辑缠在一起。这种代码活不过三个月,维护成本指数级上升。第三个崩溃现场是复用为零。你在 A 项目里写了个"解析 CSV 并汇总"的能力,B 项目也需要,怎么办?复制粘贴。粘过去之后两边各自演化,修了 A 的 bug 忘了修 B 的,最后变成两份不一样的代码。团队里三个人做三个智能体,就有三套几乎一样但细节不同的工具实现。2.2 技能层带来的四个结构性改变agent-skills 这套思路,本质上是把上面三个问题一次性解决。它带来四个结构性改变。第一,能力与智能体解耦。技能是独立模块,智能体只是"调用方"。技能不知道谁在调它,智能体也不关心技能内部怎么实现。这种解耦让技能可以单独开发、单独测试、单独部署。你甚至可以让不同的人负责不同的技能,最后拼起来就能用。第二,描述与实现分离。每个技能对外暴露的是一份"说明书"——叫什么名字、干什么用、需要什么参数、返回什么。智能体只看说明书来决定调不调、怎么调。实现细节藏在里面,改实现不影响调用方。这一点极其关键,因为智能体选技能靠的就是这份说明书,说明书质量直接决定调用准确率。第三,组合成为一等公民。技能可以串起来用。先"检索资料",再"提取要点",再"生成摘要",再"发送结果"。每个技能只干一件事,串起来就是一条完整工作流。这种组合能力是硬编码方式很难优雅实现的。第四,可观测、可迭代。每个技能的调用次数、成功率、耗时、错误类型都能单独统计。哪个技能老出错、哪个技能没人用、哪个技能耗时太长,一目了然。有了数据,优化就有方向,而不是凭感觉改提示词。2.3 一个生活化类比:技能层就像餐厅后厨我用一个类比帮你建立直觉。硬编码的智能体,就像一个厨师既要切菜、又要炒菜、又要端盘子、又要收银,忙起来必然出错。agent-skills 就像把后厨分工:切菜的是一个岗位,炒菜的是一个岗位,摆盘的是一个岗位,传菜的是一个岗位。每个岗位有明确的职责说明(技能描述),有明确的输入输出(食材进、菜品出),有明确的协作方式(传菜员从炒菜岗取菜送到前台)。厨师长(智能体)不需要会切菜,它只需要知道"要切菜就找切菜岗,要炒菜就找炒菜岗"。哪个岗位今天出菜慢,单独去优化那个岗位就行,不影响其他岗位。新开一家分店(新项目),把同样的岗位配置搬过去就能跑。这就是技能层的价值。3. 一个技能应该长什么样:字段设计与描述规范3.1 技能描述的最小字段集要让智能体准确选技能,技能描述必须包含几个核心字段。我实测下来,下面这套最小字段集基本够用,缺一个都会出问题。字段作用缺失后果name技能唯一标识无法精确引用,容易重名冲突description一句话说明干什么、什么时候用智能体不知道何时该调它parameters参数名、类型、是否必填、含义调用时参数乱填或漏填returns返回结构说明智能体拿到结果不知道怎么用examples典型调用示例边界情况容易调错name我建议用动词开头的短横线命名,比如read-file、search-web、send-message。别用fileTool这种含糊名字,也别用doEverything这种大杂烩。名字本身就是给智能体看的信号,动词开头能让它快速判断"这是个动作"。description是最容易被写烂的字段。很多人写"读取文件",这等于没写。好的描述要回答三个问题:干什么、什么时候用、什么时候别用。比如"读取指定路径的文本文件内容。当用户需要查看某个已知路径的文件内容时使用。不要用于搜索文件,搜索请用 search-files。"最后那句"不要用于"极其重要,它帮智能体划清技能边界,减少误调。3.2 参数设计里最容易踩的坑参数设计我踩过最深的坑是参数太多、太自由。一开始我设计一个"查询数据"技能,参数有:表名、字段列表、过滤条件、排序、分页、聚合方式……结果智能体经常填错,因为它要一次性决定太多东西。后来我把它拆成三个技能:query-table(只做简单查询)、filter-rows(只做过滤)、aggregate-data(只做聚合)。每个技能参数少、职责单一,调用准确率立刻上去了。另一个坑是参数类型不明确。比如一个参数叫count,智能体不知道是数字还是字符串,有时候传"5"有时候传5。解决办法是在描述里写死类型,并且在代码里做一次容错转换。别指望模型每次都传对类型,自己兜底最稳。还有一个坑是必填和选填不分。所有参数都标必填,智能体就会硬凑参数,凑出一些没意义的默认值。正确做法是明确标出哪些必填、哪些选填,选填的给出合理默认值。这样智能体在信息不足时知道可以省略,而不是瞎编。3.3 用示例锚定技能行为examples这个字段很多人省掉,我觉得特别可惜。示例是给智能体看的"标准答案",能极大降低调用错误率。一个技能配两到三个示例就够:一个最典型的、一个带边界条件的、一个说明"不该这么调"的反例。比如send-message技能,示例可以写:典型调用是"给指定用户发送一条文本消息";边界情况是"消息内容为空时应该报错而不是发送空消息";反例是"不要用它发送文件,发文件请用 send-file"。这三个示例一摆,智能体对这个技能的理解就立体了。我实测过一个对比:同一批技能,加示例和不加示例,调用准确率能差出两成以上。示例的成本很低,收益很高,没有理由不做。4. 技能怎么被智能体选中:路由机制拆解4.1 全量塞进上下文 vs 按需检索技能选中的第一步,是让智能体"看到"有哪些技能可选。这里有两种主流做法,各有取舍。做法一:全量塞进上下文。把所有技能的描述一次性放进系统提示词。优点是简单直接,智能体一眼看全。缺点是技能一多,上下文就爆了。我实测过,技能描述超过三十个,每个平均一百字,就是三千字,再加上对话历史和其他指令,很容易逼近上下文上限,而且模型在长列表里的选择准确率会下降。做法二:按需检索。先把技能做成可检索的索引,用户提问时先检索出最相关的几个技能,再塞进上下文。优点是上下文占用小、可扩展性强。缺点是多了一层检索,检索不准就会漏掉该用的技能。检索可以用关键词匹配,也可以用向量相似度,我一般两者结合:先用关键词粗筛,再用向量精排。我的建议是:技能少于十五个用全量,多于十五个用检索。这个阈值不是绝对的,取决于你技能描述的长度和模型上下文大小。但核心原则是——别让智能体在一堆无关技能里做选择,选择空间越大,选错概率越高。4.2 技能描述质量决定路由上限不管用哪种路由方式,技能描述的质量都是路由准确率的天花板。检索再准,如果描述本身写得含糊,照样选错。我总结了几条描述写作的硬规则。第一,用用户会说的话来描述技能,而不是用技术术语。用户说"帮我看看这个文件写了啥",技能描述里就该有"查看文件内容"这种说法,而不是"执行文件读取操作"。第二,技能之间要有区分度。如果两个技能描述读起来差不多,智能体必然分不清。这时候要么合并,要么把差异点写清楚。第三,描述里带上触发场景。"当用户提到……时使用"这种句式,能显著提升命中率。4.3 多技能冲突时的优先级处理实际使用中经常出现多个技能都"看起来能用"的情况。比如用户说"整理一下这份数据",read-file、parse-csv、aggregate-data都可能被选中。这时候需要一套优先级规则。我的做法是在技能描述里加一个隐式的优先级信号:越具体的技能优先级越高。parse-csv比read-file具体,因为前者限定了文件类型。当用户明确提到 CSV 时,应该优先选parse-csv。这个信号通过描述里的限定词来传递,比如"仅用于 CSV 文件"。另一种做法是在路由层做显式排序,给每个技能配一个优先级权重,冲突时选权重高的。但权重需要人工维护,技能一多就难管。我更倾向用描述里的限定词来自动区分,维护成本低。5. 从零搭一个技能系统:可复现的实操骨架5.1 目录结构与技能注册先给一套我常用的目录结构,简单清晰,扩展方便。agent/ skills/ read_file/ manifest.json # 技能描述 handler.py # 技能实现 search_web/ manifest.json handler.py send_message/ manifest.json handler.py registry.py # 技能注册与检索 agent.py # 智能体主逻辑每个技能一个目录,manifest.json放描述,handler.py放实现。这种结构的好处是技能完全自包含,复制一个目录就能搬走一个技能。manifest.json长这样:{ "name": "read-file", "description": "读取指定路径的文本文件内容。当用户需要查看某个已知路径的文件内容时使用。不要用于搜索文件。", "parameters": { "path": { "type": "string", "required": true, "description": "文件的完整路径" }, "encoding": { "type": "string", "required": false, "default": "utf-8", "description": "文件编码,默认 utf-8" } }, "returns": { "type": "object", "description": "包含 content 字段,为文件文本内容" }, "examples": [ "读取 /data/report.txt 的内容", "查看 config.json 文件" ] }handler.py就是一个普通函数:def handle(path: str, encoding: str = "utf-8"): with open(path, "r", encoding=encoding) as f: content = f.read() return {"content": content}注册器负责扫描skills目录,加载所有 manifest,建立索引:import json import os class SkillRegistry: def __init__(self, skills_dir): self.skills = {} for name in os.listdir(skills_dir): manifest_path = os.path.join(skills_dir, name, "manifest.json") if os.path.exists(manifest_path): with open(manifest_path, encoding="utf-8") as f: manifest = json.load(f) self.skills[manifest["name"]] = manifest def all_descriptions(self): return [ f"{s['name']}: {s['description']}" for s in self.skills.values() ]5.2 技能调用的完整链路一次技能调用的完整链路是这样的:用户输入 → 路由选出候选技能 → 技能描述塞进提示词 → 模型输出调用意图(技能名 + 参数)→ 参数校验 → 执行 handler → 结果回填 → 模型生成最终回复。这里面参数校验是最容易被忽略但最重要的一环。模型输出的参数经常有类型错误、缺字段、多字段。我一般做三层校验:第一层检查必填字段是否齐全,第二层检查类型是否匹配并做容错转换,第三层检查值是否在合理范围内。任何一层不过,就把错误信息回给模型让它重试,而不是直接抛异常。def validate_and_call(skill_name, params, registry): manifest = registry.skills.get(skill_name) if not manifest: return {"error": f"未知技能: {skill_name}"} # 必填校验 for pname, pdef in manifest["parameters"].items(): if pdef.get("required") and pname not in params: return {"error": f"缺少必填参数: {pname}"} # 类型容错 for pname, pdef in manifest["parameters"].items(): if pname in params and pdef["type"] == "string": params[pname] = str(params[pname]) # 执行 handler = load_handler(skill_name) return handler(**params)5.3 让技能支持组合调用单个技能能干活,但真正有价值的是组合。组合有两种实现方式。方式一:让模型自己串。模型先调search-web,拿到结果后再调summarize,再调send-message。这种方式灵活,但依赖模型的多步推理能力,步骤一多容易断链。方式二:预定义工作流。把常见组合固化成"复合技能",比如research-and-report内部依次调用检索、摘要、发送。这种方式稳定,但灵活性差。我的经验是两者结合:高频、固定的组合做成复合技能,低频、多变的组合让模型自己串。复合技能对外还是一个技能,但内部是编排逻辑。这样既保证了稳定性,又保留了灵活性。6. 实测中最容易翻车的几个地方6.1 技能描述里的"隐形重叠"技能描述重叠是最隐蔽的坑。表面上两个技能名字不同、描述不同,但语义上有重叠区,模型就会在这块区域反复横跳。我遇到过一个典型案例:get-weather和get-forecast两个技能,前者查当前天气,后者查未来天气。描述里都写了"查询天气",结果用户问"明天天气怎么样",模型有时候调前者有时候调后者。解决办法是在描述里把时间维度写死。get-weather写"仅用于查询当前实时天气",get-forecast写"仅用于查询未来某天的天气"。加上"仅用于"这种强限定词之后,重叠区就消失了。这个技巧我用了很多次,屡试不爽。6.2 参数默认值引发的静默错误参数默认值是个双刃剑。设了默认值,模型不传参时能兜底;但兜底兜错了,就是静默错误,比报错还难查。我踩过一个坑:send-message的channel参数默认值是"default",结果模型不传 channel 时,消息全发到了默认频道,用户以为发到了指定频道。查了半天才发现是默认值的问题。我的建议是:默认值只用于真正安全的场景。如果某个参数不传会导致行为不确定,就不要设默认值,直接标必填,让模型必须明确传。宁可报错让模型重试,也不要静默走错分支。6.3 技能返回结果太长撑爆上下文技能返回结果直接塞回上下文,如果结果很长,会挤占后续对话空间。我见过一个read-file技能读了一个几万字的文件,结果整个上下文被占满,后续对话全乱套。解决办法是在技能层做结果截断和摘要。返回结果超过一定长度时,只返回前 N 个字符加一个"内容过长已截断"的提示,或者自动调用摘要技能压缩。这个阈值我一般设两千字左右,具体看模型上下文大小。关键是别让单个技能的返回结果失控。6.4 错误信息不回流导致模型瞎猜技能执行失败时,如果只返回一个笼统的"执行失败",模型不知道错在哪,就会瞎猜、瞎重试。正确做法是把具体错误信息回流给模型,比如"文件不存在:/data/report.txt"、"参数 count 必须是数字,收到的是字符串"。模型看到具体错误,才能针对性修正。我一般把错误分成三类:参数错误(模型能自己修)、环境错误(模型修不了,需要告知用户)、权限错误(需要用户介入)。参数错误直接回流让模型重试,环境错误和权限错误转成自然语言告诉用户。这样分工明确,不会让模型在修不了的问题上死循环。7. 技能系统的迭代与观测7.1 每个技能都该有调用日志技能系统上线之后,最重要的不是加新技能,而是看清楚现有技能用得怎么样。我一般给每个技能记三类日志:调用次数、成功率、平均耗时。这三个指标能回答大部分问题。调用次数告诉你哪些技能是热门的、哪些是没人用的。没人用的技能要么删掉,要么描述有问题导致模型不知道何时用。成功率告诉你哪些技能老出错,出错是参数问题还是实现问题。平均耗时告诉你哪些技能拖慢了整体响应,需要优化。import time import logging def logged_call(skill_name, handler, params): start = time.time() try: result = handler(**params) status = "success" except Exception as e: result = {"error": str(e)} status = "error" elapsed = time.time() - start logging.info(f"skill={skill_name} status={status} elapsed={elapsed:.3f}s") return result7.2 用失败案例反推描述问题技能调用失败的时候,别急着改实现,先看是不是描述问题。我统计过自己的项目,技能调用失败里大概六成是描述不清导致的选错技能或填错参数,只有四成是真正的实现 bug。所以每次失败,我都会问三个问题:模型是不是选错了技能?如果是,描述哪里让人误解了?模型是不是填错了参数?如果是,参数描述哪里不清楚?把失败案例攒起来,定期回顾,你会发现描述问题有很强的模式。比如"时间相关技能容易混""文件相关技能容易混""发送类技能容易混"。针对这些模式,统一优化描述写法,比一个个改效率高得多。7.3 技能版本管理与灰度技能是会演进的。改一个技能的描述或参数,可能影响所有调用它的智能体。所以技能需要版本管理。我的做法是 manifest 里加一个version字段,重大变更升大版本,兼容性变更升小版本。智能体引用技能时可以指定版本范围,避免被不兼容的变更搞崩。灰度发布也很重要。新版本技能先让一小部分流量用,观察成功率和耗时没有异常再全量。技能系统的好处就在这里——技能是独立的,灰度可以做得非常细,不影响其他技能。8. 关于 agent-skills 我个人的几点体会做了一段时间技能系统,我最大的体会是:技能设计是门"减法"的手艺,不是"加法"。新手总想多加技能,觉得技能越多能力越强。实际上技能越多,路由越难,冲突越多,维护越累。真正好用的技能系统,往往是十几个职责清晰、边界明确的技能,而不是几十个含糊重叠的技能。第二个体会是描述比实现重要。实现写错了,测试能发现;描述写含糊了,测试很难覆盖,因为它是概率性的问题。我现在的习惯是,写技能先写描述,描述写不清楚说明这个技能职责没想清楚,那就先别写实现。描述是技能的"契约",契约不清,实现再好也白搭。第三个体会是别追求一步到位。技能系统是长出来的,不是设计出来的。先做三五个核心技能跑通链路,用起来之后根据实际调用数据再拆、再合、再优化。我见过太多人一开始就设计一套"完美"的技能体系,结果实际用起来发现根本不是那么回事,推倒重来。小步快跑,用数据说话,比纸上谈兵靠谱得多。最后一个实用技巧:给技能写"负面示例"。就是在描述里明确写"不要用它做 X"。模型对否定指令的遵循度其实不错,加上负面示例能显著减少误调。比如search-web里写"不要用它查询本地文件,本地文件请用 read-file"。这一句话能省掉很多排查时间。
1. 从"agent-skills"这个标题能读出什么第一次看到"agent-skills"这个词,我脑子里蹦出来的不是某个具体框架,而是一类正在快速成型的东西:把智能体(agent)的能力拆成一块块可插拔、可复用、可组合的"技能单元"。这个标题本身信息量不大,正文和关键词都是空的,但恰恰是这种"空",说明它更像一个概念容器——一个用来装"智能体技能体系"的容器。我打算基于这个标题,结合当前智能体开发的常见实践,把"agent-skills"这件事从概念到落地讲透。先说清楚它是什么。agent-skills 指的是围绕智能体构建的一套技能抽象层:把"查天气""读文件""发邮件""调数据库""做网页检索"这类具体动作,封装成一个个独立、可描述、可被智能体按需调用的技能模块。它解决的问题很直接——过去我们做一个智能体,往往是把所有能力硬编码在一个大提示词或者一个大函数里,改一处牵动全身,复用基本靠复制粘贴。有了技能层之后,能力变成积木,智能体变成拼装台。它适合谁看?三类人最该关注。第一类是正在做智能体应用但被"提示词越写越长、逻辑越理越乱"折磨的开发者;第二类是想把内部工具、内部系统接入智能体,却不知道怎么抽象接口的工程同学;第三类是对智能体感兴趣、想动手做一个能真正干活的小助手,而不是只会聊天的玩具的爱好者。不管你是哪一类,这篇内容都会给你一套能直接抄作业的思路和代码骨架。我先把结论摆在这:agent-skills 的核心价值不在于"技能多",而在于"技能边界清晰、描述规范、组合可控"。技能写得再多,如果每个技能都含糊其辞、职责重叠,智能体照样会乱调、错调、漏调。所以下面我会重点讲清楚技能怎么定义、怎么描述、怎么被选中、怎么组合、怎么排错,这些都是我实际踩过坑之后总结出来的东西。2. 技能抽象层到底解决了哪些真实痛点2.1 硬编码智能体的三个典型崩溃现场在讲技能层之前,得先讲清楚没有它的时候会出什么事。我见过太多智能体项目死在三个地方。第一个崩溃现场是提示词膨胀。一开始你写个智能体,提示词两百字,能查天气、能算数,挺好。后来要加读文件、加发消息、加查数据库、加调用内部接口,提示词涨到三千字。这时候模型开始"记不住"——前面定义的规则到后面就忘了,工具描述互相打架,调用格式时对时错。你改一个工具的描述,可能把另一个工具的行为带偏。这不是模型不行,是你把所有信息塞进一个上下文里,超出了它稳定处理的边界。第二个崩溃现场是逻辑耦合。所有工具调用逻辑写在一个大函数里,用一堆 if-else 判断该走哪条路。加一个新能力,就要在函数里再插一个分支,还要小心别破坏原有分支。测试的时候更痛苦,你没法单独测"读文件"这个能力,因为它和"发消息"的逻辑缠在一起。这种代码活不过三个月,维护成本指数级上升。第三个崩溃现场是复用为零。你在 A 项目里写了个"解析 CSV 并汇总"的能力,B 项目也需要,怎么办?复制粘贴。粘过去之后两边各自演化,修了 A 的 bug 忘了修 B 的,最后变成两份不一样的代码。团队里三个人做三个智能体,就有三套几乎一样但细节不同的工具实现。2.2 技能层带来的四个结构性改变agent-skills 这套思路,本质上是把上面三个问题一次性解决。它带来四个结构性改变。第一,能力与智能体解耦。技能是独立模块,智能体只是"调用方"。技能不知道谁在调它,智能体也不关心技能内部怎么实现。这种解耦让技能可以单独开发、单独测试、单独部署。你甚至可以让不同的人负责不同的技能,最后拼起来就能用。第二,描述与实现分离。每个技能对外暴露的是一份"说明书"——叫什么名字、干什么用、需要什么参数、返回什么。智能体只看说明书来决定调不调、怎么调。实现细节藏在里面,改实现不影响调用方。这一点极其关键,因为智能体选技能靠的就是这份说明书,说明书质量直接决定调用准确率。第三,组合成为一等公民。技能可以串起来用。先"检索资料",再"提取要点",再"生成摘要",再"发送结果"。每个技能只干一件事,串起来就是一条完整工作流。这种组合能力是硬编码方式很难优雅实现的。第四,可观测、可迭代。每个技能的调用次数、成功率、耗时、错误类型都能单独统计。哪个技能老出错、哪个技能没人用、哪个技能耗时太长,一目了然。有了数据,优化就有方向,而不是凭感觉改提示词。2.3 一个生活化类比:技能层就像餐厅后厨我用一个类比帮你建立直觉。硬编码的智能体,就像一个厨师既要切菜、又要炒菜、又要端盘子、又要收银,忙起来必然出错。agent-skills 就像把后厨分工:切菜的是一个岗位,炒菜的是一个岗位,摆盘的是一个岗位,传菜的是一个岗位。每个岗位有明确的职责说明(技能描述),有明确的输入输出(食材进、菜品出),有明确的协作方式(传菜员从炒菜岗取菜送到前台)。厨师长(智能体)不需要会切菜,它只需要知道"要切菜就找切菜岗,要炒菜就找炒菜岗"。哪个岗位今天出菜慢,单独去优化那个岗位就行,不影响其他岗位。新开一家分店(新项目),把同样的岗位配置搬过去就能跑。这就是技能层的价值。3. 一个技能应该长什么样:字段设计与描述规范3.1 技能描述的最小字段集要让智能体准确选技能,技能描述必须包含几个核心字段。我实测下来,下面这套最小字段集基本够用,缺一个都会出问题。字段作用缺失后果name技能唯一标识无法精确引用,容易重名冲突description一句话说明干什么、什么时候用智能体不知道何时该调它parameters参数名、类型、是否必填、含义调用时参数乱填或漏填returns返回结构说明智能体拿到结果不知道怎么用examples典型调用示例边界情况容易调错name我建议用动词开头的短横线命名,比如read-file、search-web、send-message。别用fileTool这种含糊名字,也别用doEverything这种大杂烩。名字本身就是给智能体看的信号,动词开头能让它快速判断"这是个动作"。description是最容易被写烂的字段。很多人写"读取文件",这等于没写。好的描述要回答三个问题:干什么、什么时候用、什么时候别用。比如"读取指定路径的文本文件内容。当用户需要查看某个已知路径的文件内容时使用。不要用于搜索文件,搜索请用 search-files。"最后那句"不要用于"极其重要,它帮智能体划清技能边界,减少误调。3.2 参数设计里最容易踩的坑参数设计我踩过最深的坑是参数太多、太自由。一开始我设计一个"查询数据"技能,参数有:表名、字段列表、过滤条件、排序、分页、聚合方式……结果智能体经常填错,因为它要一次性决定太多东西。后来我把它拆成三个技能:query-table(只做简单查询)、filter-rows(只做过滤)、aggregate-data(只做聚合)。每个技能参数少、职责单一,调用准确率立刻上去了。另一个坑是参数类型不明确。比如一个参数叫count,智能体不知道是数字还是字符串,有时候传"5"有时候传5。解决办法是在描述里写死类型,并且在代码里做一次容错转换。别指望模型每次都传对类型,自己兜底最稳。还有一个坑是必填和选填不分。所有参数都标必填,智能体就会硬凑参数,凑出一些没意义的默认值。正确做法是明确标出哪些必填、哪些选填,选填的给出合理默认值。这样智能体在信息不足时知道可以省略,而不是瞎编。3.3 用示例锚定技能行为examples这个字段很多人省掉,我觉得特别可惜。示例是给智能体看的"标准答案",能极大降低调用错误率。一个技能配两到三个示例就够:一个最典型的、一个带边界条件的、一个说明"不该这么调"的反例。比如send-message技能,示例可以写:典型调用是"给指定用户发送一条文本消息";边界情况是"消息内容为空时应该报错而不是发送空消息";反例是"不要用它发送文件,发文件请用 send-file"。这三个示例一摆,智能体对这个技能的理解就立体了。我实测过一个对比:同一批技能,加示例和不加示例,调用准确率能差出两成以上。示例的成本很低,收益很高,没有理由不做。4. 技能怎么被智能体选中:路由机制拆解4.1 全量塞进上下文 vs 按需检索技能选中的第一步,是让智能体"看到"有哪些技能可选。这里有两种主流做法,各有取舍。做法一:全量塞进上下文。把所有技能的描述一次性放进系统提示词。优点是简单直接,智能体一眼看全。缺点是技能一多,上下文就爆了。我实测过,技能描述超过三十个,每个平均一百字,就是三千字,再加上对话历史和其他指令,很容易逼近上下文上限,而且模型在长列表里的选择准确率会下降。做法二:按需检索。先把技能做成可检索的索引,用户提问时先检索出最相关的几个技能,再塞进上下文。优点是上下文占用小、可扩展性强。缺点是多了一层检索,检索不准就会漏掉该用的技能。检索可以用关键词匹配,也可以用向量相似度,我一般两者结合:先用关键词粗筛,再用向量精排。我的建议是:技能少于十五个用全量,多于十五个用检索。这个阈值不是绝对的,取决于你技能描述的长度和模型上下文大小。但核心原则是——别让智能体在一堆无关技能里做选择,选择空间越大,选错概率越高。4.2 技能描述质量决定路由上限不管用哪种路由方式,技能描述的质量都是路由准确率的天花板。检索再准,如果描述本身写得含糊,照样选错。我总结了几条描述写作的硬规则。第一,用用户会说的话来描述技能,而不是用技术术语。用户说"帮我看看这个文件写了啥",技能描述里就该有"查看文件内容"这种说法,而不是"执行文件读取操作"。第二,技能之间要有区分度。如果两个技能描述读起来差不多,智能体必然分不清。这时候要么合并,要么把差异点写清楚。第三,描述里带上触发场景。"当用户提到……时使用"这种句式,能显著提升命中率。4.3 多技能冲突时的优先级处理实际使用中经常出现多个技能都"看起来能用"的情况。比如用户说"整理一下这份数据",read-file、parse-csv、aggregate-data都可能被选中。这时候需要一套优先级规则。我的做法是在技能描述里加一个隐式的优先级信号:越具体的技能优先级越高。parse-csv比read-file具体,因为前者限定了文件类型。当用户明确提到 CSV 时,应该优先选parse-csv。这个信号通过描述里的限定词来传递,比如"仅用于 CSV 文件"。另一种做法是在路由层做显式排序,给每个技能配一个优先级权重,冲突时选权重高的。但权重需要人工维护,技能一多就难管。我更倾向用描述里的限定词来自动区分,维护成本低。5. 从零搭一个技能系统:可复现的实操骨架5.1 目录结构与技能注册先给一套我常用的目录结构,简单清晰,扩展方便。agent/ skills/ read_file/ manifest.json # 技能描述 handler.py # 技能实现 search_web/ manifest.json handler.py send_message/ manifest.json handler.py registry.py # 技能注册与检索 agent.py # 智能体主逻辑每个技能一个目录,manifest.json放描述,handler.py放实现。这种结构的好处是技能完全自包含,复制一个目录就能搬走一个技能。manifest.json长这样:{ "name": "read-file", "description": "读取指定路径的文本文件内容。当用户需要查看某个已知路径的文件内容时使用。不要用于搜索文件。", "parameters": { "path": { "type": "string", "required": true, "description": "文件的完整路径" }, "encoding": { "type": "string", "required": false, "default": "utf-8", "description": "文件编码,默认 utf-8" } }, "returns": { "type": "object", "description": "包含 content 字段,为文件文本内容" }, "examples": [ "读取 /data/report.txt 的内容", "查看 config.json 文件" ] }handler.py就是一个普通函数:def handle(path: str, encoding: str = "utf-8"): with open(path, "r", encoding=encoding) as f: content = f.read() return {"content": content}注册器负责扫描skills目录,加载所有 manifest,建立索引:import json import os class SkillRegistry: def __init__(self, skills_dir): self.skills = {} for name in os.listdir(skills_dir): manifest_path = os.path.join(skills_dir, name, "manifest.json") if os.path.exists(manifest_path): with open(manifest_path, encoding="utf-8") as f: manifest = json.load(f) self.skills[manifest["name"]] = manifest def all_descriptions(self): return [ f"{s['name']}: {s['description']}" for s in self.skills.values() ]5.2 技能调用的完整链路一次技能调用的完整链路是这样的:用户输入 → 路由选出候选技能 → 技能描述塞进提示词 → 模型输出调用意图(技能名 + 参数)→ 参数校验 → 执行 handler → 结果回填 → 模型生成最终回复。这里面参数校验是最容易被忽略但最重要的一环。模型输出的参数经常有类型错误、缺字段、多字段。我一般做三层校验:第一层检查必填字段是否齐全,第二层检查类型是否匹配并做容错转换,第三层检查值是否在合理范围内。任何一层不过,就把错误信息回给模型让它重试,而不是直接抛异常。def validate_and_call(skill_name, params, registry): manifest = registry.skills.get(skill_name) if not manifest: return {"error": f"未知技能: {skill_name}"} # 必填校验 for pname, pdef in manifest["parameters"].items(): if pdef.get("required") and pname not in params: return {"error": f"缺少必填参数: {pname}"} # 类型容错 for pname, pdef in manifest["parameters"].items(): if pname in params and pdef["type"] == "string": params[pname] = str(params[pname]) # 执行 handler = load_handler(skill_name) return handler(**params)5.3 让技能支持组合调用单个技能能干活,但真正有价值的是组合。组合有两种实现方式。方式一:让模型自己串。模型先调search-web,拿到结果后再调summarize,再调send-message。这种方式灵活,但依赖模型的多步推理能力,步骤一多容易断链。方式二:预定义工作流。把常见组合固化成"复合技能",比如research-and-report内部依次调用检索、摘要、发送。这种方式稳定,但灵活性差。我的经验是两者结合:高频、固定的组合做成复合技能,低频、多变的组合让模型自己串。复合技能对外还是一个技能,但内部是编排逻辑。这样既保证了稳定性,又保留了灵活性。6. 实测中最容易翻车的几个地方6.1 技能描述里的"隐形重叠"技能描述重叠是最隐蔽的坑。表面上两个技能名字不同、描述不同,但语义上有重叠区,模型就会在这块区域反复横跳。我遇到过一个典型案例:get-weather和get-forecast两个技能,前者查当前天气,后者查未来天气。描述里都写了"查询天气",结果用户问"明天天气怎么样",模型有时候调前者有时候调后者。解决办法是在描述里把时间维度写死。get-weather写"仅用于查询当前实时天气",get-forecast写"仅用于查询未来某天的天气"。加上"仅用于"这种强限定词之后,重叠区就消失了。这个技巧我用了很多次,屡试不爽。6.2 参数默认值引发的静默错误参数默认值是个双刃剑。设了默认值,模型不传参时能兜底;但兜底兜错了,就是静默错误,比报错还难查。我踩过一个坑:send-message的channel参数默认值是"default",结果模型不传 channel 时,消息全发到了默认频道,用户以为发到了指定频道。查了半天才发现是默认值的问题。我的建议是:默认值只用于真正安全的场景。如果某个参数不传会导致行为不确定,就不要设默认值,直接标必填,让模型必须明确传。宁可报错让模型重试,也不要静默走错分支。6.3 技能返回结果太长撑爆上下文技能返回结果直接塞回上下文,如果结果很长,会挤占后续对话空间。我见过一个read-file技能读了一个几万字的文件,结果整个上下文被占满,后续对话全乱套。解决办法是在技能层做结果截断和摘要。返回结果超过一定长度时,只返回前 N 个字符加一个"内容过长已截断"的提示,或者自动调用摘要技能压缩。这个阈值我一般设两千字左右,具体看模型上下文大小。关键是别让单个技能的返回结果失控。6.4 错误信息不回流导致模型瞎猜技能执行失败时,如果只返回一个笼统的"执行失败",模型不知道错在哪,就会瞎猜、瞎重试。正确做法是把具体错误信息回流给模型,比如"文件不存在:/data/report.txt"、"参数 count 必须是数字,收到的是字符串"。模型看到具体错误,才能针对性修正。我一般把错误分成三类:参数错误(模型能自己修)、环境错误(模型修不了,需要告知用户)、权限错误(需要用户介入)。参数错误直接回流让模型重试,环境错误和权限错误转成自然语言告诉用户。这样分工明确,不会让模型在修不了的问题上死循环。7. 技能系统的迭代与观测7.1 每个技能都该有调用日志技能系统上线之后,最重要的不是加新技能,而是看清楚现有技能用得怎么样。我一般给每个技能记三类日志:调用次数、成功率、平均耗时。这三个指标能回答大部分问题。调用次数告诉你哪些技能是热门的、哪些是没人用的。没人用的技能要么删掉,要么描述有问题导致模型不知道何时用。成功率告诉你哪些技能老出错,出错是参数问题还是实现问题。平均耗时告诉你哪些技能拖慢了整体响应,需要优化。import time import logging def logged_call(skill_name, handler, params): start = time.time() try: result = handler(**params) status = "success" except Exception as e: result = {"error": str(e)} status = "error" elapsed = time.time() - start logging.info(f"skill={skill_name} status={status} elapsed={elapsed:.3f}s") return result7.2 用失败案例反推描述问题技能调用失败的时候,别急着改实现,先看是不是描述问题。我统计过自己的项目,技能调用失败里大概六成是描述不清导致的选错技能或填错参数,只有四成是真正的实现 bug。所以每次失败,我都会问三个问题:模型是不是选错了技能?如果是,描述哪里让人误解了?模型是不是填错了参数?如果是,参数描述哪里不清楚?把失败案例攒起来,定期回顾,你会发现描述问题有很强的模式。比如"时间相关技能容易混""文件相关技能容易混""发送类技能容易混"。针对这些模式,统一优化描述写法,比一个个改效率高得多。7.3 技能版本管理与灰度技能是会演进的。改一个技能的描述或参数,可能影响所有调用它的智能体。所以技能需要版本管理。我的做法是 manifest 里加一个version字段,重大变更升大版本,兼容性变更升小版本。智能体引用技能时可以指定版本范围,避免被不兼容的变更搞崩。灰度发布也很重要。新版本技能先让一小部分流量用,观察成功率和耗时没有异常再全量。技能系统的好处就在这里——技能是独立的,灰度可以做得非常细,不影响其他技能。8. 关于 agent-skills 我个人的几点体会做了一段时间技能系统,我最大的体会是:技能设计是门"减法"的手艺,不是"加法"。新手总想多加技能,觉得技能越多能力越强。实际上技能越多,路由越难,冲突越多,维护越累。真正好用的技能系统,往往是十几个职责清晰、边界明确的技能,而不是几十个含糊重叠的技能。第二个体会是描述比实现重要。实现写错了,测试能发现;描述写含糊了,测试很难覆盖,因为它是概率性的问题。我现在的习惯是,写技能先写描述,描述写不清楚说明这个技能职责没想清楚,那就先别写实现。描述是技能的"契约",契约不清,实现再好也白搭。第三个体会是别追求一步到位。技能系统是长出来的,不是设计出来的。先做三五个核心技能跑通链路,用起来之后根据实际调用数据再拆、再合、再优化。我见过太多人一开始就设计一套"完美"的技能体系,结果实际用起来发现根本不是那么回事,推倒重来。小步快跑,用数据说话,比纸上谈兵靠谱得多。最后一个实用技巧:给技能写"负面示例"。就是在描述里明确写"不要用它做 X"。模型对否定指令的遵循度其实不错,加上负面示例能显著减少误调。比如search-web里写"不要用它查询本地文件,本地文件请用 read-file"。这一句话能省掉很多排查时间。