1. 为什么我开始做 agent-skills:智能体最容易被低估的一块拼图
先说个我自己的经历。大概在几个月前,我在折腾一个能自动整理会议纪、跟进待办事项的个人助理型 Agent,刚开始所有逻辑都堆在 System Prompt 里:定义角色、给示例、描述工具怎么用、告诉它什么时候该调用哪个函数…… Prompt 越写越长,从 800 字膨胀到 3000 字,最后到了 5000 字。模型的表现却越来越差:经常漏掉关键动作,偶尔还会自作主张地“忘记”某个规则。最崩溃的一次是它把“发送会议邀请”这个动作理解为“给用户发一封摘要邮件”,整个流程完全走偏。
排查下来,问题出在“技能”这个维度上。过去我们设计 Agent 的时候喜欢把“能力”理解为“工具”,比如一个 function、一个 API 调用,但这远远不够。一个真正的技能应该是一套完整的行为模式:它包含触发条件、输入参数、执行步骤、校验规则、失败兜底,甚至还有对输出质量的自我评估。它比“一个函数”大得多,也比“一段 Prompt 指令”结构清晰得多。
这就是我后来把整个项目命名为agent-skills的原因。这个项目不是做大模型训练,也不是做 RAG 框架,它要解决的是“智能体如何规范地获取、组织和执行一项完整能力”的问题。更直白地说,它是一个 Agent 的“技能操作系统”,让能力可以被定义、注册、调度、升级和复用。
如果你也在做 Agent 类应用,尤其是那些需要复杂决策链路的场景,比如客服、办公助手、代码生成、数据分析助理,你会发现自己迟早会撞上同样一面墙:功能越来越多,系统越改越乱,模型该学会的“本事”到底是放在 Prompt 里、函数里还是代码逻辑里,边界越来越模糊。agent-skills 这套实践,就是要把这面墙拆掉,让智能体的能力构建回到一条清晰可控的主线上。
这篇文章,我会把这套方案的设计思路、核心细节、实操过程和踩坑记录,一次性写透。内容适合两类人看:一类是自己在搭 Agent 应用的独立开发者,另一类是团队里负责 Agent 工程化的技术人员,无论你用 LangChain、LlamaIndex 还是自己手写逻辑,都能从中找到直接可落地的参考。
2. 核心设计思路:技能库不是工具列表,而是一套能力管理范式
2.1 为什么“技能库”比“工具列表”更适合复杂 Agent
拆解 agent-skills 之前,我们先把一个基础概念掰清楚:什么是“技能”?它和“工具”到底有什么区别?
一张对比表格能说明问题:
| 维度 | 工具(Tool) | 技能(Skill) |
|---|---|---|
| 粒度 | 一次函数调用 | 一组有序的动作组合 |
| 输入 | 结构化参数 | 目标导向的自然语言意图 + 结构化参数 |
| 执行逻辑 | 通常是确定性的 | 可能包含中间判断、分支、重试 |
| 失败处理 | 抛异常 | 有兜底策略与降级方案 |
| 前置条件 | 通常无 | 可能依赖其他技能或上下文状态 |
| 输出 | 原始结果 | 经过校验、规范化后的结果 |
工具解决的是“能做某个原子动作”,技能解决的是“能完成一件完整的任务”。比如“发送邮件”是工具,而“给客户写一封跟进邮件并发送、同时抄送直属领导、还要在 CRM 里标记联系记录”就是一个技能。这个技能背后至少涉及三次工具调用:生成邮件内容、调用发送接口、写入 CRM 记录。如果中间某一步失败,还得决定是重试、跳过还是通知用户。
有了这层理解,再回头看 agent-skills 的设计目标就很清楚了:我需要为 Agent 定义一套技能管理框架,让它把“工具”组织成“技能”,把技能作为独立的模块来开发、注册和调用。
这个思路和微服务改造非常像。早期单体应用把所有业务混在一个工程里,后来按领域拆服务,每个服务有自己的接口、存储和部署边界。Agent 的能力系统也需要这样的拆解,否则随着玩法变复杂,Prompt 会失控,函数列表会膨胀,模型根本不知道该优先关注什么。
2.2 技能的四层抽象
我在实际设计中,把技能归纳为四层:
- 表现层(Manifest):技能的名字、描述、触发场景、所属领域、版本号。这层负责让 Agent(和大模型)知道“何时该用这个技能”。
- 契约层(Contract):技能的输入 Schema、输出 Schema、异常类型、执行前置条件。这层负责让技能“可被正确调用”。
- 执行层(Executor):具体的执行逻辑,可能是编排多次模型调用、多次工具调用,也可能是调用一个外部脚本。
- 评估层(Evaluator):技能执行完以后,如何判断结果是否符合预期。这一步很关键,很多 Agent 系统烂就烂在“做了”和“做对了”分不清。
我就拿一个我自己项目里的真实技能来举例:技能名叫“会议纪要与任务提取”。
它的 Manifest 大概长这样:
{ "name": "meeting_minutes_extractor", "description": "从会议录音转写文本中提取会议纪要,并识别待办事项、负责人与截止时间", "triggers": ["会议结束", "转写文本", "待办提取"], "domain": "office-assistant", "version": "1.2.0" }Contract 层定义输入是一个字符串(转写文本),输出是一个 JSON,包含summary、action_items、decision三个字段。执行层内部会先对转写内容做分段,再调用一次大模型生成纪要,然后调用一个规则引擎提取时间相关的短语,最后合并输出。Evaluator 会校验:action_items 是否为空,如果为空就要触发二次处理——因为一个没有待办事项的会议纪要在办公场景里几乎等于没写。
这套四层抽象是我在实践里反复打磨出来的,它有一个非常明显的好处:Agent 的主循环不关心一个技能内部做了多少次模型推理、调了几个工具,它只关心怎么匹配技能、怎么传入参数、怎么校验输出。整个系统的复杂度被控制在技能内部,不会外溢到主流程上去。
2.3 为什么选择“显式声明”而不是“模型自主涌现”
有一种流派认为,Agent 的规划能力足够强的时候,你不需要显式定义技能,模型看到工具列表,自然就会组合出来。这条路听起来很美,但实际跑下来问题很多。
模型自主编排的最大问题是不确定性。你可以让模型在 10 个工具里自己选,但当你面对 200 个工具时,选择的准确率会断崖式下跌。而且模型的组合逻辑往往不稳定,同一类任务今天走 A 路径,明天走 B 路径,出了问题的排查成本极高。
agent-skills 的方式是显式声明:每个技能都经过人工设计、注册和测试,Agent 要从一个已注册的技能库里去“匹配”任务,而不是每次都在工具层面自由发挥。匹配过程和工具调用类似,但粒度更大、意图更明确,模型做这个判断的负担要小得多。
实际测试下来,在一组 15 个技能的技能库里,模型准确匹配技能的比率在 90% 以上,而且几乎不需要在 Prompt 里堆大量示例。这比让模型直接面对 80 个工具的选择准确率高出一大截。
3. 技能的组织与编排:从“有个技能”到“可用、好管、能迭代”
3.1 技能注册与仓库结构
讲完了抽象层次,再落回工程实现。任何一个技能要能被 Agent 使用,都需要注册。注册的本质是把技能从“开发环境”产物变成“运行时”可发现的对象。
我在 agent-skills 项目里设计的技能仓库结构是这样的:
skills/ ├── meeting_minutes_extractor/ │ ├── manifest.json │ ├── contract.json │ ├── executor.py │ ├── evaluator.py │ ├── requirements.txt │ └── tests/ │ └── test_executor.py ├── email_draft_and_send/ │ ├── manifest.json │ ├── contract.json │ ├── executor.py │ └── evaluator.py └── ...每个技能都是一个独立目录,自带元信息、代码和测试。这样的好处是依赖隔离、责任清晰。你甚至可以给不同技能指定不同版本的 Python 运行环境,或者用不同模型供应商的 API,互不干扰。
注册流程上,我并没有走“运行时扫描目录”这条捷径,而是建了一个注册中心,启动时读取一个registry.yaml文件:
skills: - name: meeting_minutes_extractor path: ./skills/meeting_minutes_extractor enabled: true version: 1.2.0 max_retry: 2 - name: email_draft_and_send path: ./skills/email_draft_and_send enabled: false version: 0.9.1为什么不用自动扫描?因为自动扫描虽然省事,但当技能数量超过 20 个之后,你根本不知道哪些技能是可以上生产环境的,哪些还在实验阶段。显式配置让你一眼看懂当前系统的能力集,而且可以做灰度——先让 5% 的流量启用新技能,观察反馈,再逐步放开。
3.2 技能之间的依赖关系与编排
复杂任务很少由一个技能独立完成,大部分情况是多个技能协作。比如处理一封“请求改期”的邮件,Agent 可能需要先调用“邮件理解”技能判断意图,再调用“日程查询”技能看时间冲突,最后调用“邮件起草与发送”技能完成回复。
技能编排有两种做法:
- 主 Agent 编排:主 Agent 的 ReAct 循环里,每步都去技能库匹配一次技能,动态决定下一步执行哪个。灵活度高,但每次都要调用大模型做路由决策,延迟和成本都会上升。
- 技能内编排:一个高层技能内部,直接指定依赖多个低层技能,按预设顺序执行。稳定、可预测、调试方便。
我两个都在用。对于探索性任务,走主 Agent 编排的路线;对于高频且链路固定的任务,直接把它封装成一个复合技能,内部把链路写死。比如“会议纪要与任务提取”在实测稳定之后,我就把“分段预处理 → 纪要生成 → 待办识别 → 通知发送”全部固化,内部不再需要大模型做路由判断,速度快了大约 40%,还大大降低了模型误判的风险。
这个取舍背后的原则是:一直在变化的链路交给模型动态决策,已经跑通的链路用代码固化下来。这也是 agent-skills 的核心哲学——让模型做它擅长的事(理解不确定的输入),让代码做它擅长的事(执行确定性的流程)。
3.3 技能的版本管理与灰度发布
前面提到版本号,就会引出升级问题。技能迭代是一个非常容易被低估复杂度的事。模型在变、外部 API 在变、用户需求也在变,技能不可能不变。但技能一旦更新,就可能破坏依赖它的上层流程。
我采用的办法是给技能加上语义化版本:
- 主版本号:不兼容的变更,比如输入 Schema 变了。
- 次版本号:向后兼容的功能新增。
- 补丁版本号:内部实现修正。
技能的 Manifest 里会记录依赖的最低版本,运行时有一个依赖检查器,如果发现冲突,直接拒绝加载并给出报告。举个例子,meeting_minutes_extractor依赖text_splitter技能的 1.0.0 以上版本,如果当前注册的是 0.9.x,注册中心会在启动阶段就报错,而不是等运行时才爆雷。
灰度发布方面,我在注册中心加了一个简单的流量染色机制:新版本技能先标记为candidate,只有带有指定 session 标识的请求才会路由到新版本,其余全部走旧版本。聚合几天的评估数据后再决定是全量切换还是回滚。这个机制实现起来大约一天时间,却省掉了后面无数的线上事故。
4. 核心环节实操:技能从 0 到 1 的完整落地流程
4.1 定义技能的输入输出契约
写技能的第一步不是写代码,而是先写契约。我吃过不写契约的亏:早期图省事,直接把技能做成“传一个字符串进去,返回一个结果”的自由格式,结果下游解析时各种踩坑,字段时有时无,类型说变就变。
现在我坚持用 JSON Schema 定义每个技能的契约,并且写进了团队规范。拿会议纪要技能的 Contract 举例:
{ "name": "meeting_minutes_extractor", "input_schema": { "type": "object", "properties": { "transcript": { "type": "string", "description": "会议转写文本,支持中英文混合", "minLength": 20 }, "language": { "type": "string", "enum": ["zh", "en"], "default": "zh" } }, "required": ["transcript"] }, "output_schema": { "type": "object", "properties": { "summary": { "type": "string" }, "action_items": { "type": "array", "items": { "type": "object", "properties": { "task": { "type": "string" }, "owner": { "type": "string" }, "deadline": { "type": "string", "format": "date" } }, "required": ["task"] } }, "decisions": { "type": "array", "items": { "type": "string" } } }, "required": ["summary", "action_items"] } }这个契约的价值不只是约束输入输出,它还可以被用来做自动校验。我在 Executor 执行完以后,会跑一遍输出校验,如果某个字段缺失或者类型不对,技能立即标记失败,而不是把脏数据继续往下游传。
有一个小坑要提醒:模型的输出很容易不遵守你定义的 JSON Schema,尤其是required字段。我的经验是不要完全依赖“提示词约束”,而是要在代码里做一次显式校验,必要时用 “One-shot 纠错”机制——把不符合 Schema 的输出连同错误信息一起送回给模型,让它修正一次。这个办法的成功率非常高,基本能解决九成以上的格式问题。
4.2 执行器的设计:把“不靠谱”变成“可重试”
技能的执行器是整个系统里最核心也最容易出问题的部分,因为每一次可能涉及多次模型调用、多次外部 API 请求。任何一个环节都可能波动,设计执行器时必须有“容错”的本能。
我总结了一套执行器的推荐流程:
- 参数预处理:把外部传入的参数做归一化,补全默认值。
- 上下文装配:从会话上下文或全局上下文中补充必要信息。
- 子任务拆解:如果需要,调用内部规划器拆解步骤。
- 执行子步骤:按序执行,每个子步骤都复用同一个错误处理机制。
- 结果聚合与规范化:汇总各子步骤的结果,按输出契约加工。
- 自我评估:调用评估器,判断结果是否合格,不合格触发重试或降级。
以邮件起草技能为例,它的执行器伪代码大致长这样:
def execute(input_data: dict, context: dict) -> dict: # 1. 参数预处理 body = input_data["body"] recipient = input_data["recipient"] tone = input_data.get("tone", "professional") # 2. 调用模型生成邮件草稿 draft = llm_generate( system_prompt="You are an email assistant...", user_prompt=f"Write an email to {recipient}. Body: {body}. Tone: {tone}" ) # 3. 校验草稿是否合规(比如长度、敏感词) validation = validate_email_draft(draft) if not validation["passed"]: # 带错误信息重试一次 draft = llm_generate( system_prompt="You are an email assistant. Fix the issues: ...", user_prompt=... ) # 4. 写入发送队列或直接发送 send_result = email_api.send(draft, recipient) return { "draft_id": send_result["id"], "recipient": recipient, "status": "sent" }每个技能的执行器都不太一样,但核心原则一致:把每个外部调用都视为可能失败、可能返回无效数据,然后针对性地做校验和重试。千万别假设外部 API 和大模型第一次就能给你完美结果,那是灾难的开始。
4.3 技能召回与路由:如何让 Agent“找对技能”
技能库建好了,定义清楚了,剩下的问题就是:Agent 收到一个用户请求,怎么找到正确的技能?
我尝试过三种方式,直接说结论:
- 纯文本描述匹配:把每个技能的 description 拼接后和用户输入一起做相似度匹配。简单,但精度有限,遇到语义接近的技能容易混淆。
- 向量检索召回:给每个技能的 manifest 生成向量,用户请求也向量化,用余弦相似度做召回。效果比纯文本好不少,尤其适合技能数量多的场景。
- 重排序 + 阈值过滤:向量召回 Top 10,再由一个大模型做一次精细选择,选出最终要用的技能。准确率最高,成本也可控。
我目前在线上用的是第三种。具体来说,先做向量召回,然后让模型从候选列表里选一个,并且必须输出“选择理由”。如果模型的判断跟向量召回的第一名不一致,系统会记录一次日志,用于后面优化学法。
路由信息除了技能名,还包括一个置信度分数。如果分数低于某个阈值,我会让 Agent 主动向用户澄清而不是硬猜。比如用户说“帮我发个通知”,这个意图至少可以匹配“邮件通知”“短信通知”“IM 群通知”三个技能,硬猜很可能选错,不如问一句。别小看这个细节——让 Agent 学会“承认不确定”,比让它硬着头皮猜重要得多。
4.4 评估器的落地:结果好不好,要拿数据说话
很多 Agent 系统跑着跑着就失控了,核心原因是没有一套反馈闭环。执行完一个技能之后,系统并不知道这次执行到底算成功还是失败。倒不是没有信息,而是信息被浪费了。
我在每个技能的 Executor 之后接了一个 Evaluator,它干两件事:
- 结构化校验:检查输出是否符合契约。
- 质量评估:对任务完成质量打分。
质量评估的实现方式五花八门,我实际用过两种有效的:
- 规则评估:比如会议纪要技能,如果完全没有提取到任何
action_items,直接记为失败。 - 模型评估:用一个专门的评估模型,按你提供的标准给结果打分。比如邮件技能,评估标准可以是“逻辑是否通顺、语气是否专业、是否包含必要的信息要素”。
设计评估器有一个关键原则:评估标准必须在写技能的时候就同步定义,而不是事后补。很多朋友是先写了执行逻辑,跑起来再琢磨“怎么判断结果好不好”,这时候再来定标准已经晚了,因为执行结果已经影响了下游,评估器形同虚设。
另外,评估结果一定要落盘。我每天会拉一次所有技能的执行评估报告,用这些数据决定哪些技能要升版本,哪些技能要下线,哪些技能的 Prompt 需要改。如果没有这套数据闭环,你做任何优化都是拍脑袋,优化完也不知道是否真的有效。
5. 常见问题与排查技巧实录:那些坑,能避一个是一个
5.1 技能匹配错乱:把“发邮件”理解成了“更新日程”
这个问题在技能数量超过 10 个之后特别容易出现。排查步骤,我建议按顺序来:
- 第一步:检查向量检索的召回结果,看用户输入和技能 Description 的相似度分布。很多时候是技能的 Description 写得太泛,导致多个技能分数接近。
- 第二步:检查重排序模型的 Prompt,确认它是否能看到候选技能之间的区别性信息。如果候选技能的描述都差不多,模型压根没法判断。
- 第三步:手动高亮技能的“触发场景”。比如邮件技能加上,“当用户提到发信、回复、抄送、附件等关键词时优先选择”,日程技能则强调“改时间、预约、会议冲突”等场景。
最有效的长期解法是:把线上误匹配的案例回流到技能库里,给技能逐步补充典型的触发示例。这不是一次性工作,而是持续维护的过程。
5.2 技能执行超时:一次调用全家等待
智能体常见的性能杀手就是串行调用多个外部 API,任何一个慢都会拖垮总耗时。我实测下来,一个复合技能如果包含 4 次大模型调用和 2 次外部 API,在最差情况下可能耗时 30 秒以上,这几乎不可接受。
几个实用的优化手段:
- 并行化:无依赖的子任务并发执行。比如“提取待办”和“提取决策”可以同时发两个大模型请求,再合并结果。能省掉近一半时间。
- 超时熔断:所有外部调用统一设置超时时间,比如 10 秒,超过直接走降级逻辑,而不是无限等。
- 轻量探测:对于外部 API,可以先发一个极轻量的健康检查请求(或者直接用最近缓存),确认可用再执行。
我在某个版本的迭代中,把“会议纪要”技能从全串行改成“并行+超时”,平均耗时从 22 秒降到了 11 秒,效果立竿见影。这个收益比换更好的模型还明显。
5.3 输出严重偏离预期:动作做了,结果不对
这种情况最考验排查功力。表面上看技能执行成功,但结果完全不是用户想要的。比如“生成一封简短的确认邮件”,结果生成了一封 1000 字的长文。
我的排查路径是这样的:
- 先看中间结果:把 Executor 内部每个子步骤的输出都打点记录,定位问题出在模型生成阶段还是后处理阶段。
- 再看模型的输入:有没有把用户的原始意图完整传进提示词。很多时候问题出在参数预处理阶段,把关键信息搞丢了。
- 然后检查评估器:如果评估器没有把“简洁”纳入评估标准,这个偏差永远不会被发现。
这个经验的核心是:日志要留足,中间状态一定要可追溯。Agent 调试比传统代码调试难得多,因为每次模型输出都不一样,无法靠断点单步调试。唯一的办法就是让整个执行链路的所有中间状态都记录下来,出问题才能回溯。
5.4 技能版本升级后下游崩了
这是个典型的依赖管理问题。技能 A 升级了输出格式,删掉了一个旧字段,结果依赖技能 A 的技能 B 直接解析失败。
我每次都强调那两点:Semantic Versioning 和注册中心的依赖检查。如果你的项目里还没有做,建议至少先做依赖检查——每个技能声明它依赖的其他技能及其版本范围。注册的时候做静态检查,不满足就不允许加载。这条规则看起来简单,但能拦住绝大多数升级引发的问题。
5.5 频率限制耗尽:技能一跑起来 API 配额当天见底
最后提一个工程层面的坑。技能内部的大模型调用会被计费,外部 API 调用有频控限制。你一不小心写了递归重试,或者在执行器里循环很多次,API 配额很快就会用光。
我的做法是在技能层加一个“预算控制”:
if context.get("request_count", 0) > MAX_MODEL_CALLS: raise SkillBudgetError("技能调用次数超限")这是在保证技能不失控的最底线。没有这层保护,一个错误的 Prompt 就可能让一个技能在一个小时内调用上千次大模型。别问我怎么知道的。
写在最后的实操体会
做 agent-skills 这段时间,我最大的一个感受是:Agent 工程难的从来不是跑通一个 Demo,而是把系统的边界定义清楚。模型的自由度要控制,技能的边界要清晰,每一层都要有明确的责任和校验。市面上讨论 Agent 的火热话题都是「能不能够解决的问题」,但真实工程里更关键的往往是「什么时候该停下来、什么时候该问人、什么时候该认错」这些不性感但必须做好的事情。
对想尝试 agent-skills 这套思路的朋友,我建议从一个小场景切入,比如一个复合技能——“会议纪要提取”或“邮件自动归档”,把你现有的一个工具升级成完整技能,加上契约、评估和日志,然后跑两周看看。我敢说,你再回去看之前那种“所有逻辑都堆在 Prompt 里”的方案,肯定不想回头了。
技能库越用越厚,Agent 就越用越稳。这一步跨出去了,后面的路就顺了。