
最近在给团队搭建智能体Agent应用时把大量精力花在了一个容易被忽视的环节上agent-skills也就是智能体的技能系统设计。之前我们做 Agent 的方式很“原始”把所有能力和指令塞进一个超长 Prompt 里结果模型经常答非所问工具调用乱成一团后面吸取教训把能力拆成一个一个结构化的 Skill技能才真正跑通了。这篇就把我在这个过程中的设计思路、踩坑记录和一些可直接复用的方案整理出来。这个项目解决的问题很具体当你的 Agent 需要处理文件、查询数据、操作外部系统时怎样让大模型准确、高效地调用这些能力而不是靠“猜”。它适合正在做 Agent 开发、想给智能体重构能力层、或者想沉淀一个可复用技能库的开发者和技术负责人。无论你用的是 Claude、GPT、开源模型还是自研的编排框架这套设计思路都适用。我们当时落地了一个“技能目录 描述卡片 可执行脚本 注册表”四层结构跑通之后稳定性提升非常明显。1. 内容整体设计与思路拆解1.1 先搞清楚Agent Skill 到底是什么Agent Skill 本质上是一种将“能力”结构化、标准化、可复用化的封装单元。它不能简单等同于一个 Python 函数也不只是一段 Prompt 提示词。我的理解是一个 Skill 应该包含三样东西它知道“自己在什么场景下该被使用”描述它定义了“调用它需要哪些输入”参数 Schema它还有真正干活的“执行体”脚本或服务。很多人会问既然我们已经有各种工具调用了为什么还要多此一举搞 Skill 层这个问题的答案我是踩了坑才明白的。之前我们用 Function Calling 裸调工具每个工具都是独立的维护起来没太大问题但当工具数量增加到几十个时大模型经常“乱点鸳鸯谱”。把相近的能力聚合为一个 Skill配合清晰的语义描述和边界约束模型“误用工具”的概率会大幅下降。我当时给团队画的简化公式是工具Tool 一个函数技能Skill 一个完整的能力域包含场景判断、入参校验、执行流程、结果规整举个例子“发送邮件”是一个工具而“处理邮件相关事务”是一个 Skill它可以内部调度发邮件、按联系人分组、自动生成主题这多个子动作。1.2 为什么不建议把所有能力都塞进 Prompt有一种“偷懒”方案是把 Agent 会用到的事项全部写进系统提示词里比如“如果你收到文件相关的任务就执行文件整理脚本”。初期效果看似还行但一旦业务复杂问题就暴露得特别明显上下文被污染大量的能力说明把上下文撑成大杂烩关键指令反而被稀释模型决策质量直线下降。无法复用一套流程写死在 A 场景里B 场景想调用完全相同的逻辑只能复制再改一遍。没法测试能力行为的边界不清晰回归测试都不知道该测什么。把“能力的定义”从“对话提示层”下沉到“可编程的技能层”是我这次重构的核心决策。技能层是可编程的就意味着可以用 Git 做版本管理、可以用单元测试做回归验证、可以给不同环境开发/预发/生产部署不同的技能集。这下运维模型就从“调 Prompt”变成了“管理代码”。1.3 整体架构的四个层次我们最终落地的方案分成了四层技能定义层每个 Skill 目录下的描述文档SKILL.md和参数 Schema 文件。执行实现层真正干活的脚本、二进制、API 封装或者工作流定义。注册与发现层所有 Skill 的索引注册表负责加载、去重、权限控制。调度与编排层Agent 内核读取注册表结合任务语义选择 Skill并进行多步编排。这四个层次相互独立的好处是你可以只替换执行实现层而不影响定义比如想从本地执行换成一个远程服务改动非常小。这个设计对后期迭代太关键了不然每次需求变更都要动到调度器。2. 核心细节解析与实操要点2.1 一个 Skill 的“身份证”名称与描述怎么写Skill 的描述是决定大模型能不能正确调用它的第一因素。我见过太多人写描述写得很随意比如“tools for sending emails”结果大模型根本不知道什么时候该用它。一个合格的描述应该包含四个部分核心职责这个技能主要负责做什么。适用场景什么类型的任务应该触发它。边界与禁忌什么情况下不要使用。输入输出概览大致需要什么参数会返回什么结果。我自己常用的模板是处理【XX领域】任务的技能。当用户请求涉及【场景A】、【场景B】时使用。不接受【场景C】的请求。需要提供【参数X】和【参数Y】返回【结果描述】。举个例子“会议纪要辅助 Skill”的描述将会议录音或转写文本整理为结构化会议纪要。适用于周会、项目复盘、客户访谈等场景。可提取待办事项、风险项和决策项。不适用于实时字幕翻译。输入为转写文本内容输出为 Markdown 格式纪要文件。这段描述很长吗不长但信息密度非常高。实测下来描述里写清楚“不适用场景”非常有效能挡住很多误触发。2.2 参数 Schema 设计的三个关键原则少而精能用 3 个参数解决的绝不用 5 个参数越多模型猜测和编造的概率越大。类型要严格参数类型必须明确宁可要严格字符串枚举也不要让模型“自由发挥”格式。比如时间参数要求必须是 ISO8601 格式字符串而不是“大概下午三点”。默认值兜底给参数设合理的默认值避免模型在缺参数时直接报错。我当时做一个文件归档技能初始参数设计了 7 个来源路径、目标路径、归档方式、是否递归、是否保留结构、是否压缩、删除原文件。后来在实际测试中发现模型经常会漏传“目标路径”而且“保留结构”和“删除原文件”之间还有隐含冲突。后来我把参数砍成 3 个source_dir: string, required, 待归档目录 target_dir: string, optional, 默认 /archive, 归档目标目录 mode: enum[string], optional, 默认 move, 可选值 move/copy/compress砍完之后调用成功率肉眼可见地提升了。2.3 技能实现形态脚本、函数还是服务技能内部实现我试过三种路径各有适合的场景单文件脚本Python/Node/Bash最快落地适合逻辑简单的技能日志和错误处理自己写。平台函数如云函数适合需要弹性伸缩或跟云资源打交道的场景但调试麻烦一点。独立微服务适合重计算、需要会话状态、或对外部系统有复杂调用链的技能但引入的运维负担最重。我们的原则是先脚本跑通再按需服务化。不要一上来就整微服务Agent Skill 的痛点往往不是性能而是逻辑准确性。脚本能表达清楚的逻辑微服务不一定更快。2.4 目录结构是技能的骨架我建议每个 Skill 独立目录不要杂糅。一个标准目录结构长这样这个是我们内部的模板skills/ pdf_extract/ SKILL.md schema.yaml implement.py requirements.txt tests/ test_implement.py email_handle/ SKILL.md schema.yaml implement.py目录名就是技能名必须简短、无空格便于注册表索引。所有说明归 SKILL.md所有参数定义归 schema.yaml实现归 implement.py测试归 tests。不要搞“放一个 doc 里顺便把脚本也塞进去”的那种结构后期维护会非常痛苦。3. 实操过程与核心环节实现3.1 从零实现一个「文件归档技能」我们用一个真实的技能来演示完整流程。假设我们要做一个可以自动把项目文件按分类归档、并生成索引清单的技能。第一步先写 SKILL.md# archive_files 将散落的项目文件按类型归档到指定目录并生成一份 index.md 索引清单。 适用于“帮我整理一下文件夹”、“把下载目录的文件归档”这类请求。 不接受需要重命名内容主体或批量修改文件内部内容的请求。 参数 - source_dir: 要整理的源目录必填 - target_dir: 归档目标目录选填默认 ./archive - file_type: 需要归档的文件类型选填默认 all) 输出一份包含归档位置和文件数量的 Markdown 索引文件。这里“不接受重命名内容主体”这种边界描述是为了防止模型拿这个技能去干它不该干的事。接下来写 schema.yamlname: archive_files description: 整理并归档项目文件生成索引清单 parameters: source_dir: type: string required: true description: 待整理的源目录绝对路径 target_dir: type: string required: false default: ./archive description: 归档目标目录 file_type: type: string required: false default: all enum: [all, doc, image, code] description: 按类型筛选归档3.2 实现脚本的编写与要点写 implement.py 时我特别注意了一点脚本要在没有 Agent 的时候也能独立运行。这样语法检查、单测、手动验证都很方便。脚本头部做一个命令行参数解析Agent 调用时传 JSON 序列化参数手动调试时直接传命令行参数。import argparse import json import os import shutil from pathlib import Path TYPE_MAP { doc: [.pdf, .docx, .txt, .md], image: [.png, .jpg, .jpeg, .gif], code: [.py, .js, .ts, .go, .java], } def archive_files(source_dir, target_dir./archive, file_typeall): source_path Path(source_dir).resolve() target_path Path(target_dir).resolve() if not source_path.exists(): raise FileNotFoundError(f源目录不存在: {source_path}) extensions None if file_type ! all and file_type in TYPE_MAP: extensions TYPE_MAP[file_type] moved_count 0 index_entries [] for item in source_path.iterdir(): if item.is_file(): if extensions and item.suffix.lower() not in extensions: continue relative_target target_path / (item.suffix.lstrip(.) or misc) relative_target.mkdir(parentsTrue, exist_okTrue) dest relative_target / item.name shutil.move(str(item), str(dest)) moved_count 1 index_entries.append(f- {item.name} - {dest}) index_path target_path / index.md with index_path.open(w, encodingutf-8) as f: f.write(# 归档索引\n\n) f.write(f共归档 {moved_count} 个文件。\n\n) f.write(\n.join(index_entries)) return {moved_count: moved_count, index_path: str(index_path)} if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--source_dir, requiredTrue) parser.add_argument(--target_dir, default./archive) parser.add_argument(--file_type, defaultall) args parser.parse_args() result archive_files(args.source_dir, args.target_dir, args.file_type) print(json.dumps(result, ensure_asciiFalse))这个脚本看起来简单里面有一个关键设计按扩展名分目录归档而不是直接全部倾倒在一个目录。这样索引文件和分类逻辑清晰用户拿到结果后能直接看到“哪些文件去了哪里”。返回值统一用 JSON 序列化方便 Agent 解析成结构化结果。提示实际生产中不要直接 shutil.move最好先复制到临时目录确认无误后再切换。我当时为了赶进度直接 move结果归档到一半磁盘报错文件处于“两边都不在”的状态教训很深刻。3.3 技能注册与加载流程写好了技能文件下一步就是注册。我设计了一个 registry.json集中登记可用技能{ version: 1.0, skills: [ { name: archive_files, path: skills/archive_files, description_summary: 归档并整理项目文件, enabled: true } ] }加载器启动时扫描 registry.json逐个读取 SKILL.md 和 schema.yaml把它们组装成一个“技能描述包”发给模型。这时候有个性能优化点给模型看的描述是摘要版不是完整文档。我知道很多框架会把整个文档塞给模型但那样上下文开销太大。摘要版包含技能名、一句话职责、参数名列表就足够模型做选择了。真正的完整文档在模型选定技能之后由执行器按需读取。3.4 Agent 调用技能的执行流程执行链路是这样走的用户提出“帮我整理下载文件夹”。调度器把任务语义向量化与会话中的技能摘要做匹配。模型返回“调用 archive_files”的意图附带参数 {source_dir: /Users/xx/Downloads}。执行器校验参数填充默认值调用 implement.py。脚本执行完毕返回 JSON 结果。Agent 把结果整理成自然语言回复用户。这一步里最容易被忽略的是第 4 步的“执行器校验”。很多情况下模型给出的 source_dir 根本不存在或者路径是相对路径但基准目录不对。我的做法是在执行器里统一做“路径规范化 存在性检查”不通过就直接返回建议信息而不是把异常抛给大模型。4. 常见问题与排查技巧实录4.1 模型选错技能怎么办这个问题几乎所有人都会遇到。我们遇到过用户问“帮我总结一下这个 PDF”模型却调用了“pdf_extract”而不是“pdf_summarize”。后来排查发现两个技能的描述里都出现了“PDF”关键词模型很难区分边界。解决方案是在描述里增加“使用规则”段落明确说“pdf_extract 只负责提取文本/图片内容不负责理解或总结若用户需要总结应调用 pdf_summarize”。描述里少写修饰性词语多写“什么情况下用我”和“什么情况下别用我”能有效降低混淆率。4.2 参数传错类型或缺失模型传错参数类型常见的处理方法是让 Schema 解析严格抛错但我建议框架层做一层“软修复”如果缺的是可选参数直接填默认值如果类型不匹配尝试做一次保守转换只有必填参数缺失或者转换失败才向用户确认。这样用户体验会平滑很多不打断对话流程。但这层“软修复”逻辑要写清楚日志否则错误会隐藏得很深。4.3 执行超时与结果截断Agent 技能的耗时天差地别快的几十毫秒慢的几分钟。如果调度器没有明确的超时控制会出现“模型任务还没等到结果对话会话已经超时”。我的经验是给每个技能设定预期耗时和硬超时两个值。预期耗时用来告诉用户“稍等一下”硬超时到了就主动返回部分结果比如“文件已归档 20 个剩余还在处理中索引会稍后生成”好过一直卡死。4.4 并发冲突与资源占用多个技能同时执行时最容易出问题的是资源竞争比如两个任务同时写同一个索引文件、同时操作同一个临时目录。我的建议是给每个执行会话分配独立的工作目录所有输出先写到工作目录结束时再按规则 merge 到目标位置。这个方案能挡住一大半并发问题而且排查起来非常直观。另一个经验是不要在技能内部依赖全局环境变量技能应该是自包含的参数来源必须显式声明。5. 后续还能怎么扩展做完这个技能库之后我最大的感受是整个项目最花时间的不是写代码而是打磨技能的描述边界和参数规范。代码好不好用一目了然但“模型能不能准确理解技能是干嘛的”这件事需要反复测试和迭代。后来我养成了一个习惯每次新增技能先不看它的功能实现而是只看描述和参数 Schema想象自己是一个不了解业务的人能否仅凭文档就正确使用它。下一阶段我准备做两件事一是给技能加“效果反馈”机制记录每次调用的结果好坏反过来修正描述二是把技能注册表改造成动态加载支持从远端仓库热更新不用每次改动都重启整个 Agent 服务。这些方向目前还在验证中欢迎有同样在搞 Agent 技能层的朋友多交流。最后再分享一个小技巧调试 Skill 描述时不要只看成功率要分类汇总“模型误用场景”。我用一个简单的 JSONL 日志记录每次调用请求里的技能名称和实际任务需求每周复盘一次很快就能定位到描述里的歧义词。这个过程比调代码本身更能提升整体系统的稳定性。