先聊一个我最近特别有感的观察做 Agent智能体的朋友越来越多了但大家卡的地方出奇一致不是模型选型不是 Prompt 怎么写而是“让 Agent 真正把一件事做完整”。你给它一个任务它听懂了可一动手就露怯——要么不知道先做什么要么干到一半不知道找哪个工具要么步骤一变就彻底乱套。我做了大半年 Agent 开发之后得出的结论是缺的不是模型能力而是一套能把“能力”拆成可复用、可组合、可管理的“技能体系”。agent-skills这个项目说白了就是干这件事的——它把 Agent 要掌握的每一项能力封装成一个独立“技能模块”每个模块带描述信息、实现逻辑和测试套件Agent 根据任务描述自动决定用哪些技能、按什么顺序组合。项目本身不绑死某一家模型或某个框架核心是一套组织和调用技能的方法论加参考实现。无论你是在搭客服机器人、写代码助手还是在做内部知识问答这套思路都能直接落地。这篇文章我会把项目拆开聊为什么需要技能体系、技能文件怎么组织、完整实现一遍、进阶玩法、以及我踩过的坑。1. 为什么 Agent 需要一套“技能体系”1.1 单模型 Prompt 解决不了复杂任务先说一个最基本的逻辑。很多人刚开始做 Agent 的时候习惯把所有事情塞进一个超长 Prompt指望大模型自己规划、自己调用、自己纠错。结果是什么上下文越堆越长模型注意力被稀释干到第 5 步就忘了第 1 步的目标。这不是模型笨而是把所有知识、规则、工具说明、历史状态全部压在一个上下文窗口里本身就是一种低效的信息组织方式。我打个比方。你让一个新人去办一场线下活动给他的资料是一本 800 页的百科全书。他能办成吗理论上能但实际操作中他连翻目录都要花半天更要命的是他解决“订场地”要用到的信息和解决“安排嘉宾流程”要用到的信息全都混在一起干扰太大。你真正应该做的是把这 800 页拆成几十个独立文件夹每个文件夹对应一个子任务再配一个索引告诉他什么场景去哪个文件夹找什么资料。这就是技能体系最朴素的价值——把信息按任务边界切分降低单个决策点的认知负担。在 agent-skills 的语境里这个“文件夹”就是一个 skill。每个 skill 只负责一件完整的事比如“查询天气”“生成周报”“做代码审查”。模型不需要在总 Prompt 里记住所有细节它只需要知道“有哪些技能可用”真正要用的时候再展开对应技能的描述和逻辑。1.2 Skills 和 Tools 有什么本质区别市面上很多框架都在讲“工具调用”Function Calling / Tools那 Skills 和 Tools 有什么不同我自己的理解是Tools 是单点动作Skills 是完整能力。举个例子一个“发送邮件”的 Tool输入收件人、主题、正文调一下接口结束。而一个“处理客户邮件”的 Skill它内部可能包含“读取邮件内容→分析情绪和意图→生成回复草稿→调用发信工具→记录到 CRM”这一串步骤。也就是说Skill 是构建在 Tools 之上的一层“流程编排单元”。它可以自己调用 LLM也可以调用多个 Tools还可以调用其他 Skills。Agent 调一个 Skill就像你叫了一个外包团队进场干活——你只需要告诉它目标它内部怎么分工、怎么协调你不用操心。这个区别决定了架构上的两个设计决策Skills 必须自带描述文件里面说明“我这个技能是干什么的、适合什么场景、需要哪些参数”这样 Agent 才能做“技能路由”——根据当前任务自动选对技能而不是靠 Prompt 里写死。Skills 必须有标准输入输出协议不能这个 Skill 返回 JSON、那个 Skill 返回 Markdown 散文否则后面的编排全乱套。1.3 项目整体解决的三个核心痛点我总结 agent-skills 这个项目设计之初想解决的痛点基本就是下面三个痛点一Prompt 膨胀失控。所有能力说明都堆在系统提示词里改一个细节就要重新调整个 Prompt而且很容易互相冲突。把能力外置成技能文件之后系统提示词可以精简到“你有以下技能可用按需调用”其余细节全部下沉到技能文件内部。痛点二能力无法复用。同一个“格式化报告”的逻辑在这个机器人里写一遍在下个项目里又要重写。统一成技能模块后复制一个目录就能带走换项目直接导入。痛点三无法测试和度量。纯 Prompt 写的东西没法自动验证对错——你说它能算数它算错了你也测不出来。技能模块可以单独写测试用例、做基准测试Benchmark每个技能好不好用、稳不稳定量化一目了然。这三点基本就构成了 agent-skills 的核心设计动机用工程化的方式把 Agent 的能力拆成可测试、可复用、可演进的最小单元。2. 技能库的目录结构与描述规范2.1 一套能直接抄的目录模板做项目的第一步永远是定结构。agent-skills 推荐的目录组织方式很简洁我直接把实践中沉淀出来的模板放在下面你可以直接抄走改agent-skills/ ├── skills/ │ ├── weather/ │ │ ├── SKILL.md │ │ └── impl.py │ ├── weekly-report/ │ │ ├── SKILL.md │ │ ├── impl.py │ │ └── templates/ │ │ └── weekly.md │ └── code-review/ │ ├── SKILL.md │ ├── impl.py │ └── tests/ │ └── test_basic.py ├── registry.json └── agent/ ├── core.py └── config.py每个技能目录里SKILL.md是这个技能的身份描述impl.py或其他语言的实现文件是实际执行逻辑可选目录如templates/、tests/用来放配套资源和测试代码。registry.json是技能注册表里面登记了所有技能的名称、路径、版本、依赖关系Agent 启动时读这个文件就能知道“自己会哪些活”。可能有朋友会问为什么用目录 文件而不是统一丢数据库我倾向于文件方案原因有三第一文件天然支持 Git 版本管理每个技能改动有记录、能回滚第二文件可读性好一个新同事想了解项目里有哪些能力直接翻目录就好了不用查数据库第三文件之间天然隔离技能和技能不会因为共享一套运行时状态而互相干扰这对安全也很重要。2.2 SKILL.md写“让模型看懂”的描述SKILL.md是整个技能体系的灵魂。我看过很多失败的 Agent 项目问题出在同一个地方技能描述写得太像给人看的文档而不像给模型看的路由信号。模型是靠着描述来判断“这个场景我该不该用这个技能”的描述写得不到位模型要么视而不见要么在所有场景都误调用。一份合格的SKILL.md至少包含这四个部分name技能名简短唯一模型更容易记住。description用 2~5 句话说明这个技能做什么、适合什么场景、不适合什么场景。注意一定要写“不适合什么场景”这句负向说明能有效减少模型在边界情况的误判。parameters声明技能需要哪些输入参数以及每个参数的格式和含义。结构化的参数声明比让模型自由发挥可靠得多。steps技能内部的关键执行步骤概述方便模型在调用前理解这个技能会经历哪些过程避免“黑盒感”。我拿一个“生成项目周报”的技能举例name: weekly-report description: | 根据一周的 Git 提交记录和 Jira 任务状态生成周报。 适用于团队每周总结、个人周报生成场景。 如果用户只需要一句话总结或没有提交数据来源不建议使用。 parameters: since: string # 起始日期如 2025-01-01 until: string # 结束日期如 2025-01-07 scope: string # 可选的过滤范围如 project:agent-skills steps: 1. 读取 Git 日志按作者聚合提交 2. 读取任务系统状态汇总完成/进行中 3. 生成 Markdown 周报按成果/风险/下周计划分组 4. 交由用户确认后输出你看这个描述一旦写清楚模型在遇到“帮我写个周报”时就能精准路由到这里而且从参数里知道该向用户追问“起止日期给了吗范围是哪个项目”——交互就有了抓手不再瞎猜。2.3 impl.py把逻辑藏到描述后面SKILL.md是“给模型看的接口文档”impl.py是“真正干活的函数”。两者需要解耦。我见过有人把一大段执行逻辑直接写在SKILL.md里当成提示词用这样也能跑但测试、循环调用、并发全部会很吃力。更合理的形态是这样的# skills/weekly-report/impl.py def run(params: dict, context: dict) - dict: since params[since] until params[until] scope params.get(scope, ) commits context[git_provider].get_commits(since, until, scope) tasks context[task_provider].get_tasks(since, until) sections { achievements: summarize_achievements(commits, tasks), risks: detect_risks(tasks), next_plan: build_next_plan(tasks), } return {report: render(sections), has_data: bool(commits or tasks)}这里有两个关键设计。第一run()接收params和context两个参数。params是模型传进来的、符合SKILL.md里声明的参数context是运行环境注入的依赖——比如数据库客户端、Git 接口、日志器。背后的逻辑是技能不能自己去 new 一个数据库连接而是由上层 Agent 统一提供这样技能之间不会争抢资源也方便在测试时传入 fake 数据做模拟。第二强制要求返回值结构化的dict。如果一个技能返回的是纯文本下一个技能要消费它就必须做文本解析成本高且容易错。统一成 JSON-like 结构技能之间就能像管道一样直接对接这个决定在后期做技能编排时节省了大量精力。2.4 技能注册表与依赖管理有了技能目录还不够还要有一个显式的“总索引”。这就是registry.json的作用。它的结构类似{ skills: { weather: { path: skills/weather, version: 1.2.0, description: 查询天气并给出生活建议, entry: impl.py:run, dependencies: [http-client, map-tool] }, weekly-report: { path: skills/weekly-report, version: 0.9.1, description: 生成项目周报, entry: impl.py:run, dependencies: [git-provider, task-provider] } } }注册表的存在让 Agent 在启动时可以快速扫描所有可用技能并把它们的description汇总成一份精简的能力清单注入系统提示词。这样模型的系统提示词里只需要说“技能查询天气、生成周报、代码审查、知识检索……”而不用背负每个技能的长篇描述。真正调用某个技能时再动态加载它的SKILL.md全文——这样做的好处是既控制了提示词长度又保证了关键细节不缺失。依赖管理这里有一个容易忽略的坑技能之间的依赖最好是“接口依赖”而不是“代码依赖”。比如“周报”技能依赖“Git 数据源”正确的做法是声明它需要一个实现了get_commits()接口的对象而不是让它直接 import 某个具体数据库的表结构。这样接不同数据源时只需要实现同一个接口、替换context里的注入即可“技能本身零改动”是复用的前提。3. 实操直击从零搭一套 agent-skills 体系3.1 准备基线环境动手之前我先捋一下项目运行的前置条件。这里我们不绑定具体的大模型厂商核心思路是 Agent 需要具备“文本生成”和“工具调用/技能路由”的能力。我建议的最小基线Python 3.10什么都要 Python生态熟悉最重要一个支持工具调用的对话模型接口你也可以用本地模型关键是 API 层兼容Git技能版本管理用一个简单的任务队列或者回调机制后面编排会用到在本地的目录里我们先把刚才的结构创建出来mkdir -p agent-skills/skills/{weather,weekly-report,code-review} mkdir -p agent-skills/agent touch agent-skills/registry.json这里提醒一句不要把目录名取得太泛比如utils、common这种。技能名尽量是“用户可理解的动作”weather好过>name: weather description: | 查询指定城市的实时天气、温度和未来三天的天气预报返回结构化信息。 可以附加一句生活建议如带伞、穿衣提醒。 如果用户询问的是气候统计或历史天气数据不建议使用。 parameters: city: string days: integer (默认 3最大 7)接着写impl.py。这里为了演示我们用一个可替换的天气查询客户端真实项目中你可能换成某个气象 API 的封装# skills/weather/impl.py import httpx from datetime import date, timedelta def run(params: dict, context: dict) - dict: city params[city] days int(params.get(days, 3)) api_key context[config].get(weather_api_key) weather_client context.get(weather_client) if weather_client is None: weather_client create_client(api_key) forecast weather_client.get_forecast(city, days) advice [] for day in forecast[:1]: if day[precipitation_prob] 0.6: advice.append(今天大概率有雨记得带伞) if day[temp_max] - day[temp_min] 8: advice.append(昼夜温差大建议备一件外套) return { city: city, forecast: forecast, advice: .join(advice), updated_at: date.today().isoformat(), }然后把它注册进registry.json。记住注册表里的description字段实际上会拼进系统提示词所以它和SKILL.md里的description可以不完全一样注册表里的偏“路由摘要”SKILL.md 里的偏“完整定义”。我通常的做法是注册表里只写一句话详细的在 SKILL.md 里。3.3 核心 Agent 主循环怎么写搞定单个技能之后我们得把这些技能挂到 Agent 上。主循环的逻辑可以提炼成三步看清单 → 选技能 → 填参数调用。首先是“看清单”。Agent 启动时读取registry.json把技能列表整理成一段文本可用技能 1. weather: 查询实时天气、温度、未来3天预报给出生活建议 2. weekly-report: 根据Git记录和任务系统生成周报 ...然后是“选技能”。我们把技能清单 用户请求一起交给模型让它输出一个结构化的“技能调用草案”包含技能名、参数、以及“为什么选这个技能”的简短理由。这里我强烈建议把“理由”字段加上——不是为了看模型怎么想而是方便调试技能选错了你能从理由里倒推出是描述写得有歧义还是用户需求被误读了。最后是“填参数调用”。模型输出草案后我们需要做一层校验技能名是否在注册表里参数是否齐全、格式是否正确缺失参数时主动向用户追问而不是直接硬跑。校验通过后调用run(params, context)拿到返回的 dict再交给模型组织成最终回答。下面是一段极简的伪代码def handle_request(user_input): skill_index load_registry_summary() draft llm.propose_tool_call( systemf你是一个技能调度器。可用技能{skill_index}, useruser_input ) skill validate_draft(draft) if skill.missing_params: return ask_user(skill.missing_params) result execute_skill(skill.name, skill.params) return llm.format_answer(result)如果你以前写过纯 Prompt 的 Agent这段代码放在那里你可能会感觉“咦怎么模型能自由发挥的空间变小了”。没错这正是我想要的——把“自由发挥”限制在“如何调用技能、如何组织答案”这两个环节上而具体的业务逻辑下沉到确定性的代码里。这样既能利用模型的语义理解优势又能保证关键流程的稳定性。3.4 给技能写测试用例我前面反复强调“可测试”这块必须落地。每个技能的tests/目录里放的是独立于真实服务的测试核心思路是 mock 外部依赖只验证技能内部的逻辑。以 weather 技能为例# skills/weather/tests/test_impl.py class FakeWeatherClient: def get_forecast(self, city, days): return [ {temp_max: 28, temp_min: 18, precipitation_prob: 0.8} ] def test_weather_advice_when_rainy(): result run({city: 北京, days: 1}, {weather_client: FakeWeatherClient()}) assert 带伞 in result[advice]你可能会问这样测的意义是什么意义在于——技能的逻辑改了、接口改了跑一遍测试就知道有没有破坏调用方。AI 项目的最大问题是“不可预测”这种确定性测试是在一片混乱里锚定住“哪些东西是绝对可靠的”。我自己的习惯是给每个技能至少写 3 个用例正常路径、参数缺失、结果为空。成本很低但换来的是重构时的底气。4. 技能编排进阶组合、路由与共享4.1 当一个任务需要多个技能协作时单个技能能解决简单任务现实中的大多数请求需要技能组合。比如用户说“看看明天北京天气怎么样顺便提醒我带不带伞”——这一条指令同时涉及到“天气查询”和“生活建议”但如果你把“带伞提醒”做进了天气技能内部组合自然发生。这是第一种组合方式把一个技能的功能做得足够完整天然涵盖多个子步骤。第二种组合方式是技能串联。最经典的场景是先“知识检索”找文档再“代码生成”写实现最后“代码审查”检查质量。这种场景下Agent 的主循环需要升级成一个“多轮工具调用”的模式第一轮调用knowledge-search拿到文档摘要把摘要作为上下文第二轮调用code-generate生成的代码传给code-review拿到审查意见如果审查发现问题再回到code-generate修复形成闭环。到这里你会发现多技能协作本质上是一个“小的执行引擎”在跑一个任务图Task Graph。但我们在 agent-skills 里不推荐一开始就上复杂的 DAG 编排框架——用代码显式写顺序更可控理解成本也更低。只要你把每个技能的标准接口定义清楚了后续想换成 DAG 引擎、或者接第三方流程编排工具都是可替换的。4.2 技能路由冲突与优先级技能一多路由冲突是必然的。举一个我实际遇到的案例项目里同时有“email-drafting”写邮件和“reply-suggesting”给邮件回复建议两个技能。用户说“帮我处理一下收件箱里的邮件”这时候模型到底该调用哪个两条描述都沾边模型十有八九会乱。解决办法是给技能描述增加负向约束 在注册表里显式标优先级。修改后的描述reply-suggesting: 描述针对单封邮件生成简短的回复建议。 注意如果用户要求批量处理多封邮件或撰写完整邮件请使用 email-drafting。同时在注册表加一个priority字段路由时优先匹配高优先级、并且描述匹配度更高的技能。这个“双保险”的思路能让路由准确率从 70% 拉到 90% 以上。剩下的 10%就需要靠日志和用户反馈去调描述这是一个持续迭代的过程。4.3 多 Agent 场景下的技能共享随着项目变大你可能从“一个 Agent 干所有事”演进成“多个 Agent 各司其职”——比如一个负责客服一个负责代码一个负责数据分析。这时候技能库如何共享agent-skills 的做法是把技能库做成独立于 Agent 的公共资产每位 Agent 通过白名单机制选择自己需要的技能。核心逻辑Agent A客服 加载weather、order-lookup、return-flowAgent B代码助手 加载code-review、code-generate、git-analyze公共技能如knowledge-search可以是共享的因为它是只读、无副作用的。注意多 Agent 共享技能时冲突点刚好反过来——不是“模型不知道选哪个”而是“多个 Agent 同时调用同一个有状态技能”导致资源竞争。我的经验是有副作用写数据、发消息、创建工单的技能尽量设计成单实例串行或者引入简单的互斥锁。比如“发邮件”这个技能如果在两个 Agent 之间共享且不加锁就可能出现重复发送的问题。这个问题在单体应用里不明显一旦多 Agent 并行就立刻暴露。4.4 技能缓存与成本控制最后聊个不那么“炫”但很现实的话题成本。多技能串联最直观的代价是多次 LLM 调用。一次任务如果模型要调度 3 个技能、每个技能内部还要调用一次模型生成那就是 4 次调用Token 消耗直接翻倍。我在 agent-skills 里实践了三种省 Token 的手段技能结果缓存。像 “weather”“股票查询”这类时效性较强但不要求绝对实时的数据缓存 5~15 分钟完全没问题。缓存 key 用城市 日期即可。这个收益立竿见影尤其是客服机器人这类高频场景缓存命中率可能到 40%。技能摘要记忆。不把每个技能的原始返回全部放进最终 Prompt而是把关键字段汇总成一行摘要。比如天气技能的完整 JSON 可能有 40 行但最终给用户的回答只需要“明天多云22~28℃建议带伞”这几个信息中间的一堆原始字段就没有必要全喂给模型做二次生成。跳过不必要的中间模型调用。不是每个技能都必须走大模型。当一个技能的输入到输出是确定性逻辑时比如日期格式化、数字计算直接写代码就行不需要让模型插一脚。这不仅是省 Token更是提高准确率——计算这种事情代码永远比模型的四则运算靠谱。5. 常见问题与排查技巧实录5.1 问题速查表我把实践过程中高频踩坑的经验整理成一张表拿出来跟大家共享症状根本原因解决办法Agent 在错误场景调用技能SKILL.md 描述太泛缺乏负向说明在 description 里明确“不适合什么场景”注册表里增加 priority技能传参频繁缺失参数声明不够细化模型不确定格式parameters 里写清楚默认值、允许范围、示例缺失时主动追问技能 A 的输出到技能 B 里解析失败返回值没有统一结构文本与 JSON 混用约定技能返回值必须是 dict字段语义单一定义多技能串行变慢、Token 翻倍中间结果全部回灌提示词缓存技能结果、用摘要代替原始返回、确定性逻辑不走模型技能更新后别的 Agent 莫名其妙报错依赖了共享技能的内部实现而非接口依赖关系收敛到注册表声明技能间只通过 run() 接口交互Agent 一次调用多个性质冲突的技能路由策略没有做互斥检查技能注册表声明“互斥技能组”路由时做冲突检测5.2 排障方法论三步定位法遇到技能调用异常我有一套固定的排障流程在这里分享给大家第一步看日志里的技能调用链。我们项目里每一次技能调用都会打日志记录“模型选了哪个技能、为什么选、参数是什么、返回是什么”。很多时候问题一眼就能看出来模型选了weather而不是weather-lite直接从理由文本里就能找到描述写得有歧义的地方。第二步单测技能本体。把参数从日志里抠出来直接喂给技能测试脚本绕过模型层。如果单测通过说明问题出在模型路由或参数提取如果单测失败说明问题出在技能内部逻辑。这一步把“模型导致的问题”和“代码导致的问题”一刀切开避免反复调 Prompt 却不知道根因在哪。第三步回归到描述文件。如果是路由问题重新阅读SKILL.md站在一个“完全不了解业务”的模型视角问自己如果一个新人只看这份描述他能在正确场景选中它吗往往改三五个词就能解决路由偏差。5.3 描述文件的“倒逼式”优化法这里多聊一个我用了很久的技巧——给技能做一次“冷启动测试”。每隔几周我会强制自己回到技能库找出几个写得很早的 SKILL.md删掉实现文件然后只凭描述去调用模型看它能不能把该给的参数给对、把流程走通。这个测试的目的是发现“读者假设”和“作者假设”的偏移。举个真实的例子我早期写过一个send-notification的技能描述里写“发送通知给指定用户”但完全没有提“用户 ID 怎么获得、通知渠道是短信还是邮件、如果有多个匹配用户怎么办”。结果模型在调用时乱猜渠道一会儿猜短信、一会儿猜邮件。加了一行“channel 参数可选值为 sms 或 email默认 email由用户在请求中显式指定”之后路由立刻稳定。这种问题只有站在“冷启动视角”才能暴露出来。写在最后的几点心得做 agent-skills 这个项目最深的体会是Agent 开发的复杂度大部分不是来自模型而是来自你如何组织它的能力边界。模型就像一个智商很高但缺乏领域经验的实习生你给他一本写清楚工作流程、工具清单、常见注意点的操作手册他就能稳定干活你只丢给他一个抽象的目标他发挥起来可能很惊艳但也可能摔得很难看。从最早的纯 Prompt 驱动到后来的技能模块化再到现在的多技能编排、共享技能库中间踩过的坑基本都被“更明确的接口契约”、“更充分的负向描述”、“更细致的测试用例”这三件事消化掉了。如果你正在做自己的 Agent 项目我会建议你也试试这套思路——不用一次实现全部先从你反复要用的三个能力开始把它们封装成技能跑通一轮再慢慢扩展。你会发现Agent 突然变得“听话”了不少。希望这篇文章能给你一些可落地的抓手。