Agent Skills实战指南:智能体技能体系的设计、召回与编排
2026/9/24 23:25:15 网站建设 项目流程

这两年做大模型应用的人应该都听过一个词:agent-skills。不少团队其实已经把它用起来了,但网上聊得都比较散,要么是在讲理念、要么是贴论文截图,真正能把“技能”这件事从头到尾讲清楚、说人话的文章很少。这篇文章我就想用自己的实际经验,把 agent-skills 这个东西掰开揉碎聊一遍:它到底解决什么问题、技能目录怎么组织、参数怎么定义、调用链路怎么设计、以及我在实际项目中踩过的几个坑。如果你是做智能体应用开发的工程师,或者正准备把 AI 能力集成进现有系统的技术负责人,这篇应该能帮你少走不少弯路。

1. 为什么智能体需要一套独立的“技能体系”

先聊点背景。大模型本身的推理能力确实强,但单靠一个模型对话窗口,是干不了“打开浏览器订机票再发个日历日程”这类组合操作的。真正的智能体应用,得能调用外部工具、访问外部数据、操作具体系统,这就引出了一个问题:怎么让模型知道“什么场景该干什么事”?

1.1 技能拆分的核心逻辑

我在早期的项目里走过一段弯路。当时的做法非常直接:把所有能调用的函数全写进 System Prompt,让模型自己挑。功能少的时候还好,一旦功能超过二十个,问题就来了——模型经常漏掉某些工具,或者在完全不相干的场景下强行调用某个工具,上下文一长,效果更是断崖式下跌。

后来我换了个思路,就是 agent-skills 的做法:把“能力”从“模型提示词”里抽出来,重新组织成一套独立的、可被选择性加载的技能模块。每个技能模块描述自己“能干什么”“需要什么参数”“如何执行”,而模型只需要在合适的场景下,根据任务描述加载对应技能即可。这背后的逻辑其实有点像微服务改造——把一个大单体拆成多个独立部署的小服务,每个服务职责单一,组合起来才能形成完整业务能力。

1.2 技能和工具、插件到底有什么区别

很多文章把工具、插件、技能这几个概念混着用,但它们其实是有层级关系的:

  • 工具(Tool)是最底层的原子操作,比如“发送HTTP请求”“读取本地文件”,它不关心业务。
  • 技能(Skill)是基于工具的封装,包含了一个完整的“能做什么”的描述、执行逻辑、可能需要的子步骤,甚至还包括该在什么场景下被触发。比如“查询今日天气”就是一个技能,它背后可能调用了两个 HTTP 工具。
  • 插件(Plugin)通常指一组相关的技能集合,比如“出行助手插件”可能包含了“查航班”“订酒店”“查天气”等多个技能。

从调用关系上看,技能是模型感知的最小单位。模型不需要了解底层工具的 HTTP 请求是怎么组装的,它只需要知道“这个技能负责什么”“该传什么参数”,具体执行交给技能内部的程序逻辑处理,这种抽象层级能显著减少模型的决策负担。

2. 技能目录的设计与组织方式

技能体系搭建的第一步,不是写代码,而是设计目录结构。目录结构设计得好不好,直接决定了后续技能扩展顺不顺畅、模型匹配准不准。

2.1 技能清单 Manifest 的设计要点

一个标准的技能目录,通常包含几个核心要素:技能名称、描述、参数定义、触发条件、执行逻辑引用。在工程实现上,我习惯用一个 Manifest 文件来描述技能的元信息,类似下面这种结构:

# manifest.yaml name: weather_query description: 查询指定城市未来N天的天气预报 version: 1.0.0 author: agent-team triggers: - 天气 - 气温 - 下雨 - 带伞 parameters: - name: city type: string required: true description: 城市名称,如“北京”“上海” - name: days type: integer required: false default: 3 description: 查询的天数范围,最大支持7天 execution: type: python module: skills.weather_query entrypoint: run

这里有几个关键点:

  • 描述(description)必须写清楚“这个技能有什么用”。这句话是给模型看的。模型会根据任务描述和技能描述做语义匹配,写得太笼统,模型可能搞不清楚什么时候该用它。比如“查询天气”就比“天气相关的数据处理”要清晰得多。
  • 触发条件(triggers)是给召回用的关键词。实际项目中我发现,光靠描述做匹配会产生不少漏召回。配合一组明确的触发关键词,能大幅提高命中率。这里的关键词可以理解为“如果用户消息里包含这些词,优先把技能拉出来让模型评估”。
  • 参数定义要给足约束。模型在抽取参数时如果缺少约束,很容易丢字段或者多传字段。所以我在参数定义里都加了类型、必填标记和默认值,这能显著降低参数幻觉的概率。

2.2 技能仓库的目录结构参考

一个中型智能体项目的技能目录,我通常是这样组织的:

agent-skills/ ├── manifests/ # 所有技能的注册清单 │ ├── weather_query.yaml │ ├── calendar_create.yaml │ └── email_send.yaml ├── skills/ # 技能的实际执行代码 │ ├── weather_query/ │ │ ├── __init__.py │ │ ├── run.py │ │ └── utils.py │ ├── calendar_create/ │ │ └── ... ├── registry.py # 技能装载器,负责扫描、索引、注册 ├── matcher.py # 技能召回与匹配模块 └── config.py # 全局配置

注册清单和执行代码分开,是一开始就要坚持的结构。如果清单和执行逻辑混在一起,等技能数量超过十个,每次改动都会变成一场灾难。我自己经历过一次重构,就是因为早期图省事把技能描述直接写在代码里,结果要改一个描述得翻半天源码。

3. 技能召回的匹配机制到底怎么选

技能召回是 agent-skills 架构里最容易被低估的一个环节。它解决的核心问题是:面对用户的一句话,系统怎么知道该启用哪些技能?

3.1 关键词匹配与语义匹配的取舍

最初级的方式是关键词匹配:用户提到“天气”就把天气技能拉出来。这种方式效率高、可解释性强,但泛化能力差,用户说“今天出门要不要带伞”就匹配不上了。

进阶一点的做法是用 Embedding 做语义匹配:把用户消息和每个技能的描述向量化,算相似度,相似度高的技能被召回。这种方式泛化能力强,但需要一个好的向量化模型和相似度阈值策略。我在项目中实测下来,RAG 里的向量召回经验完全可以迁移到技能召回上

还有一种混合策略,我更推荐。先用关键词做粗筛,选出候选集,再用语义匹配做精排:

def recall_skills(user_message: str, threshold: float = 0.42): # 第一阶段:关键词硬匹配,用于快速过滤 candidate_scores = {} normalized_msg = normalize(user_message) for name, manifest in registry.iter_manifests(): score = 0.0 # 触发词必须有细微的模糊处理才有效 for trigger in manifest.triggers: if trigger in normalized_msg: score += 0.5 # 第二阶段:语义精排,仅在关键词阶段无法决断时启用 if max(candidate_scores.values(), default=0) < threshold: for name, manifest in registry.iter_manifests(): emb_msg = embed(user_message) emb_desc = embed(manifest.description) semantic_score = cosine_similarity(emb_msg, emb_desc) candidate_scores[name] = max(candidate_scores.get(name, 0), semantic_score) ranked = sorted(candidate_scores.items(), key=lambda x: x[1], reverse=True) # 返回超过阈值且排名前3的技能 return [name for name, score in ranked if score >= threshold][:3]

这套方案的好处是:常见的高频表达靠关键词就能快速命中,成本和延迟都很低;表达方式比较花的,靠语义兜底。实测下来,关键词+语义的组合,召回率能比纯语义提升7%~10%左右。

3.2 阈值设定的经验值

阈值设置是另外一个容易翻车的地方。设得低了,无关技能会被召回,干扰模型判断;设得高了,该召回的没召回,技能就“哑火”了。

我在多个项目里沉淀了一些经验值,供参考:

匹配方式推荐阈值说明
纯关键词匹配直接命中即通过如果命中≥1个trigger,直接进入候选集
语义相似度0.35~0.45低于0.35误召回太多,高于0.5漏召回明显
混合策略综合分0.5左右关键词得分和语义得分加权合并

注意阈值不是拍脑袋定的,要拿真实用户语料去回测。我一般会留出几百条线上真实请求,标注好“期望调用哪个技能”,然后反复调参,直到准确率和召回率的平衡点满足业务要求。

4. 技能执行的链路设计与上下文管理

召回只是第一步,真正体现工程水平的是技能执行的链路设计。这里涉及到技能内部怎么跑、结果怎么回传给模型、上下文怎么保持。

4.1 技能内部执行流程

每个技能的内部执行,我建议统一遵循五步流程:参数校验 → 执行准备 → 调用外部能力 → 结果格式化 → 异常兜底

以日历创建技能为例:

async def run(payload: dict): # 第一步:参数校验 title = payload.get("title") start_time = payload.get("start_time") if not title or not start_time: raise SkillParameterError("缺少必要参数:title/start_time") # 第二步:执行准备,比如初始化SDK客户端 client = CalendarClient() # 第三步:调用外部能力 event_id = await client.create_event( title=title, start_time=start_time, duration=payload.get("duration", 60), attendees=payload.get("attendees", []), ) # 第四步:结果格式化,给模型一个清晰的结构化结果 return SkillResult( status="success", data={"event_id": event_id, "event_url": f"https://cal.example.com/e/{event_id}"}, summary=f"已创建日程: {title}", )

我特别想说一下参数校验这一步。很多同学写技能时默认“模型传的参数一定是对的”,这绝对是天真了。模型在抽取时间时可能给你一个“明天下午三点”,也可能给你一个“后天上午”,这些自然语言必须要做解析和标准化。我的做法是:每种类型的参数都定义一个 parser,日期统一转成 ISO 格式、城市统一走地理编码接口、时长统一转成分钟数,在进入业务逻辑之前把脏数据全部处理干净

4.2 技能执行结果如何回传给模型

执行完技能之后,结果不是直接丢给模型看,而是要经过一层“结果摘要”的处理。原因是:很多外部接口返回的数据对用户来说有用,但对模型做下一步决策来说可能是噪音。

比如天气查询技能,外部接口可能返回温度、湿度、风速、空气质量、日出日落时间等几十个字段,但模型只需要知道“明天北京多云,12~23度,有3级北风”,那么在执行层就应该把结果压缩成简洁的结构化文本。这样模型读起来轻量,后续回答问题的 token 成本也能省不少。

回传的格式上,我建议至少包含:状态(status)、结果对象(data)、给模型看的摘要(summary)。摘要用自然语言写,model 可以直接引用,避免模型对接原始 JSON 时“看不懂、乱解读”。

4.3 多轮对话中的上下文隔离

这是 agent-skills 里一个非常细但非常重要的实操点。

当一次对话中要连续调用多个技能时,比如用户说“帮我查一下北京明天天气,顺便把后天下午三点订一个和客户的会议”,系统可能会先召回到天气查询,再召回到日历创建。这时候第 2 个技能需要拿到第 1 个技能产生的部分信息吗?需要。但它需要完整看到天气接口的原始响应 JSON 吗?不需要。

所以我在设计上把上下文分成了两层:全局会话上下文技能本地上下文。全局上下文保存用户偏好、实体信息(比如用户所在城市)、历史对话摘要;技能本地上下文只包含该技能执行所需的输入输出。技能之间的数据流通,统一通过一个“共享内存”接口来完成:

context = AgentContext() context.set("user.city", "北京") context.set("weather.result", {"city": "北京", "date": "明天", "condition": "多云"}) # 日历技能只读取它关注的成都 user_city = context.get("user.city")

这种隔离设计避免了技能之间的数据污染。早期我踩过一次坑:天气技能把原始 JSON 写进了上下文,结果日历技能在生成会议地址的时候,居然读到了天气 JSON 里的某个字段,把“气温12度”当成了会议备注写进了日程。隔离之后这种问题就再没出现过。

5. 技能编排的容错与降级

任何一个真实系统都绕不开容错。技能调用外部接口,不可能永远成功。外部服务可能挂掉、超时、返回非预期数据,而模型在遇到这些错误时,往往不知道该怎么优雅处理。

5.1 三级降级策略

我在项目中给每个技能都配置了降级策略,这里分享一个通用模板。

故障级别表现处理方式
L1参数解析失败对参数进行纠错,尝试二次抽取;若仍失败,向用户提出澄清问题
L2依赖接口超时返回“暂时无法获取”,并自动将技能标记为“不可用”,避免模型反复尝试同一技能
L3业务执行失败尝试备选技能(比如查天气失败,尝试查天气预警接口),若全部失败,返回结构化错误

这里关键是L2 的处理。模型面对超时错误时,如果没有降级逻辑,它大概率会像“没有感情的机器”一样反复重试同一个失败的技能,既浪费 token 又拖慢响应。在技能框架层做一个“熔断标记”,让同一技能在一次会话内最多只允许被调用两次,第二次失败就标记为不可用,模型会自动选择其他路径,或者坦白告诉用户“这个功能暂时不可用”。这个策略在线上跑下来,用户的“无意义等待”少了很多。

5.2 多技能协作时的回滚机制

多技能协作时,还会出现复杂问题:比如用户先要求创建日程,然后又要求给参会人发邮件。如果创建日程成功、发邮件失败,系统应该怎么办?是把邮件放到“草稿箱”,还是把日程也一并取消?

我的建议是:在设计技能编排时,要区分“可回滚操作”和“不可回滚操作”。

  • 执行业务操作前,先判类型。像“发邮件”“发消息”这类动作原则上不可回滚,应推迟到最后执行。
  • 像“创建日程”“修改配置”这类操作如果后续环节失败,应提供“撤销/取消”的接口或者能力。
  • 如果做不到回滚,至少要给用户提供一个明确的“状态告知”,不让用户误以为所有操作都成功了。

这块不能全靠代码保障,还得在技能描述里写清楚:哪些技能会产生“不可撤销”的副作用,让模型在编排计划的时候就有所顾虑。

6. 技能调试与评估:别等上线了才叫苦

说到调试和评估,这在 agent-skills 社区里讨论得不多,但我觉得恰恰是决定项目能不能持续迭代的核心环节。

6.1 离线回放与单测场景

技能栈的特殊性在于,不仅要测代码逻辑,还得测“模型在什么场景下触发它”。我在团队里推了一套“场景回放测试”:把线上的真实用户请求全部录下来,标注好期望行为,再跑一遍技能匹配和调用链路,对比实际输出和期望输出的差异。相当于把多轮对话系统变成了一套自动化测试集。

def test_weather_skill_triggered_in_daily_query(): user_msg = "明天去杭州出差,穿什么衣服合适?" expected_skills = ["weather_query", "packing_suggestion"] actual_skills = skill_matcher.match(user_msg) assert set(expected_skills).issubset(set(actual_skills)), ( f"期望召回 {expected_skills},实际召回 {actual_skills}" )

一开始这套测试跑起来,百分之四五十都是红的,因为描述写得太抽象,模型经常只召回其中一部分技能。经过几轮对描述、触发词的调优,命中率慢慢到了百分之九十以上。这个过程痛苦,但价值非常大。

6.2 线上请求的“劣化”监控

技能上线后,还要关注两个关键指标:技能召回率技能执行成功率

  • 召回率 = 本次实际正确召回的技能数 / 本次期望召回的技能数。
  • 执行成功率 = 技能执行成功次数 / 技能被调用总次数。

如果召回率低,问题大概率出在描述、触发词和用户实际表达方式的语义鸿沟上;如果执行成功率低,大概率是参数解析、外部依赖的健壮性问题。这些指标要通过日志系统持续统计,效果下降时及时定位是哪些技能在掉链子。

7. 跨平台可移植性与技能生态

最后聊点大的:技能体系跨平台可移植的问题。

现在各家智能体平台都有自己的 agent 框架,但它们对“技能”的抽象并不完全一致。有人用 JSON Schema 定义函数,有人用封装好的 API 插件,有人自己搞一套 DSL。如果团队将来需要从一个平台迁移到另一个平台,或者同时对接多个平台,技能体系的“表达能力”就必须足够通用。

7.1 技能与特定框架解耦

我的做法是:技能的核心执行逻辑,尽量写成普通 Python 模块,不依附于任何特定 agent 框架。Manifest 文件里只描述元信息,执行代码里不带任何框架 SDK 的调用。框架相关的适配逻辑单独抽一层适配器。

比如同样是一个“搜索网页”技能,在适配器层可能有LangChainAdapterOpenAIAdapter自研AgentAdapter三种实现,但它们都调用同一个核心技能逻辑。这样框架升级、切换,都不需要重写技能本身,只需要更换适配器。

7.2 技能市场的可组合性

技能复用做到一定程度后,自然会出现“技能市场”的诉求——团队内部共享技能、跨项目复用技能、甚至对外发布技能。

我在团队内部搭过一个简单的技能索引服务,每个技能上线时提交一份 manifest 和对应测试集,服务自动完成质量评估并登记入库。别的项目要用,直接通过 registry 拉取对应技能包即可。这套机制跑起来之后,技能的平均开发周期从原来的两周缩短到了四五天,因为很多基础能力都是现成的,只要做组合和微调就行。

从长远看,我觉得 agent-skills 的场景容器化特征已经越来越明显。基础性技能(比如查天气、查日历、发邮件)会逐步标准化,而真正有价值的,是在这些技能之上组合出的业务闭环。

8. 几个老生常谈但总会踩的坑

说了一堆设计思路,最后再把我在真实项目里踩过、改过、总结过的几个高频问题列出来,相当于一个快速避坑清单。

8.1 Manifest 写的技术词汇太多,模型“看不懂”

这是出现频率最高的问题。很多工程师写技能描述时会用“获取用户地理坐标并检索周边POI”这种描述。模型也许能理解,但召回效果通常不如“查询附近的餐厅、商场、地铁站”这种偏用户视角的描述。我的建议是:描述里至少包含两个用户会直接使用的动词和名词,而不是纯技术术语。

8.2 参数复用导致上下文爆炸

在多技能协作时,模型可能需要把同一个参数传给多个技能。比如建日程时用到的“参会人列表”,发邮件时也要用到。如果每个技能都把完整参数写在会话里,多轮过后上下文就会非常臃肿。我的做法是用“参数引用”代替“参数拷贝”,比如让日历技能输出一个entity.id,发邮件技能通过这个 ID 去共享上下文里取参会人列表,而不是把完整列表再次塞进对话历史。

8.3 一个技能试图干太多事

“技能职责单一”这条边界很容易被突破。比如一开始“天气查询”只管查天气,后来有人要求它顺便推荐穿衣搭配,再后来又要求它推荐附近避雨的地点,结果这个技能的描述越来越庞大,触发场景越来越多,匹配准确率一路下滑。后来我把它重新拆成了“天气查询”“穿衣建议”“避雨场所推荐”三个独立技能,用编排链串起来,整体效果反而更好了。技能可以组合使用,但单个技能的职责边界必须清晰。

8.4 日志里看不到“为什么调用了这个技能”

线上调试最头疼的就是黑盒问题:用户发了一句“帮我看下明天出行安排”,系统居然调用了天气查询而不是日程查询。如果日志里只有模型最终调用的结果,没有记录召回阶段的得分、候选排序,排查将会非常痛苦。

我在框架里强制要求:每一步召回和调用,都记录结构化日志日志,包括候选技能、匹配得分、模型最终选择、调用结果。出问题时直接翻日志链路,基本 5 分钟内能定位到是哪一环出了问题。这个习惯强烈建议从一开始就养成。

聊到这,其实 agent-skills 的核心要点也就这么些:技能描述要清晰、召回策略要稳健、执行链路要标准化、上下文要可控、技能边界要清晰、日志要完整。这套方法论我在好几个真实项目中反复实践过,不能说毫无问题,但整体稳定性和迭代效率是看得见的提升。如果你正在搭自己的智能体应用,不妨从一个小技能开始跑通这套体系,再逐步扩展,应该会少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询