news 2026/10/8 16:46:05

从工具调用到技能管理:agent-skills 智能体实战拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从工具调用到技能管理:agent-skills 智能体实战拆解

做过智能体项目的朋友应该都有体会:真正难的不是把大模型接进来,而是让模型知道“什么时候该调用什么、调用完了结果怎么处理”。我最早做工具调用时,用的是一张写满函数说明的 tools 列表,十几个工具时还好,一旦功能复杂起来,描述互相打架、检索命中率下降、参数填错重试三连,整个链路就像一盘散沙。后来我把这套能力收敛成一个叫 agent-skills 的技能管理框架,思路一下就顺了——不再是“给模型一堆函数”,而是“给模型一本可以查、可以按需加载的技能手册”。这篇文章就把我的实战过程完整拆开讲,包括技能文件怎么设计、注册表怎么建、检索路由怎么做、执行器怎么隔离,以及我在二十多个技能之后踩到的那些坑。无论你是准备改造已有的 Agent 项目,还是想从零搭一套带技能的智能体系统,这套思路都可以直接拿过去用。

1. 我和 agent-skills 相遇的起点:工具调用走到“技能化”这个路口

先说一个很多人忽略的事实:LLM 的上下文窗口再大,也不是用来给你塞接口文档的。最开始我的 Agent 里放了十几个 function,每个 function 的 description 写得清清楚楚,模型在简单场景下也能选对。但当我把业务拓展到“查询天气、安排日程、生成周报、抓取网页摘要、调用内部审批流”这些混合场景时,tools 列表越来越长,prompt 里的函数说明占了上千 token,模型反而开始犯迷糊:明明该查天气,它去调了日程查询;明明参数缺省,它反复用一个空对象重试。

这正是 agent-skills 这类设计要解决的问题。它做的不是简单地把函数堆给模型,而是把每一个可复用的能力打包成一份自包含的技能:技能有自己的名字、用途描述、参数规范、执行入口,甚至还有测试用例和失败兜底逻辑。模型面对的就不再是上百个平铺的函数 JSON,而是一份可检索的“技能目录”。需要的时候,智能体先通过自然语言检索找到正确技能,再把技能的具体说明加载进来执行。这套“先检索、再加载、后执行”的流程,让工具调用的准确率和可维护性都上了一个台阶。

我当时接触 agent-skills 的第一反应是:这不就是把 RAG 的思路用在了工具调用上吗?后来实践证明,这个类比只说对了一半。技能化比 RAG 更严格的地方在于,它不只是“检索到文本片段”,而是检索到一段可以被安全执行的程序接口。检索质量直接影响执行成败,所以技能描述、参数 schema、路由策略和执行器必须是一个整体设计,而不是各自为政。

1.1 工具多了之后,最先崩掉的三个环节

我在项目里总结过,工具从十几个增长到几十个的过程中,最先出问题的总是三个地方。

第一是选择准确性。函数 A 和函数 B 的 description 如果都提到了“查询”,模型就很容易选错。比如我有个获取城市天气的函数,还有个获取航班状态的函数,两者都含“查询”“城市”“时间”这些词。模型在“明天去北京出差是否需要带伞”这个问题上,居然选了航班状态查询。问题不在模型,而在描述没有写出“这个技能解决什么、不解决什么”。

第二是参数维护。函数多了以后,参数名很难完全统一。有的用 city_id,有的用 city_name,有的用 location。模型在跨技能调用时经常把参数名张冠李戴。如果没有一个统一的参数校验层,错误往往要等到执行时报错才能暴露,而报错信息一长,模型还会被误导,进而产生更多错误的重试。

第三是运维割裂。工具代码改了一版,prompt 里的描述忘了同步,这是最隐蔽的坑。你改了一个函数的入参结构,但 LLM 拿到的还是旧描述,于是模型生成的新参数和真正的接口对不上,线上排查半天才发现是描述缓存未更新。

agent-skills 把技能定义为独立文件、独立版本、独立描述,这三件事就同时解决了。技能文件改版会有一个新的 version 号,注册表会自动更新索引,旧版本也不会被误加载,运维层面就有了确定性。

1.2 agent-skills 里的“技能”到底长什么样:带说明书的能力单元

先给一个最直观的认知:在 agent-skills 里,技能不是一段裸函数,而是一个能力单元。它通常包含几个部分:

  • 元信息:技能名称、版本、作者、描述、适用场景、不适用场景;
  • 参数协议:遵循 JSON Schema 或类似规范的结构化入参定义;
  • 执行入口:可以被运行环境实际调用的函数、脚本或 HTTP 端点;
  • 校验与兜底:在执行前校验参数,在执行失败时返回可读的错误信号;
  • 示例:给 LLM/开发者参考的调用示例。

你可以把技能想象成一个产品说明书和接口代码的合体。传统 function calling 里,函数是“裸接口”,描述只是一句话;而在技能体系里,描述是一份结构化文档,参数是精确的 schema,执行入口则被封装成一个规范化的接口。模型要调用一个技能,其实是在三选一:“完全不看细节直接猜”“先看描述再调”“先加载技能文件全文再调”。大多数情况下,我们做得最好的路径是第二种。

另外要强调一点,技能并不局限于“调用某个 API”。它可以是一段固定流程的脚本,比如“把一段会议记录转成行动项”,这类技能内部可能包含多步处理和规则,但它对外暴露的仍然是统一的执行接口。这样的抽象让 agent-skills 可以覆盖的工具类型远远超过传统的 function calling。

2. 技能文件如何定义:把“怎么做一件事”固化成领域语言

刚开始设计技能时,我犯过一个错误:只给技能起了名字、写了一句 description,就直接丢给模型。结果模型经常在多个技能之间犹豫不决。后来我逐步完善了一套技能文件的组织方式,核心就一句话——技能描述要同时回答“什么时候用”“什么时候别用”“参数怎么填”这三个问题。让模型少猜,是技能文件设计的根本目的。

2.1 技能元信息设计:比函数注释多出来的那些字段

一份比较理想的技能元信息,结构大概是这样的:

name: weather_query version: 1.4.0 description: > 查询指定城市在未来 1-3 天的天气情况,包括温度、降雨概率、风力。 适合用户询问出行、穿衣、户外活动时使用。 negative_description: > 不要用于查询历史天气、空气质量指数(AQI)或地震预警; 这些需求应交给 climate_stats 或 aqi_detector。 author: team-core parameters: type: object properties: city: type: string description: 城市中文名或拼音,例如"北京"或"beijing"。 forecast_days: type: integer default: 1 minimum: 1 maximum: 3 required: ["city"] examples: - input: "北京明天会不会下雨" arguments: city: "北京" forecast_days: 1 timeout_seconds: 5

这里有个细节:我额外加了negative_description字段。早期的技能描述只写“能做什么”,模型默认把它当成万能的,什么请求都往上套。加了负面描述后,模型才能真正学会“排除法”。实测下来,三个以上功能相近的技能同时存在时,负面描述能把选择准确率从七成左右拉到九成以上。这个字段并不是 agent-skills 的强制要求,但它是技能体系里最被我推荐加上的字段。

2.2 JSON Schema 编写技巧:给 LLM 少留一点瞎猜的空间

参数 schema 的设计很容易被当成小事,但它在实际使用中的影响非常大。我的经验是,给 LLM 用的 schema 要和给传统 API 用的 schema 做区别对待。给传统后端写 schema,追求的是一种“精确、完备”的标准;但给 LLM 用,更要追求的是“每个字段都有解释、有示例、有约束”。

举个例子。如果我只有一个city字段,不写描述,模型可能会填“北京市”“北京市朝阳区”“北京 Beijing”各种变体。但如果我在描述里明确写“城市中文名或拼音,例如北京或 beijing”,模型就会稳定得多。再比如forecast_days字段,如果不加 maximum 和 minimum,模型可能生成 99 这样的值;加了边界约束后,即使模型生成一个超界值,校验层也可以直接把它钳制到最大值,而不是报错。

还有一个技巧是把examples直接写进 schema 的字段描述里。模型在生成参数时会对示例非常敏感。两个字段描述的区别大概是这样的:

{ "city": { "type": "string", "description": "城市名" } }

对比:

{ "city": { "type": "string", "description": "城市名,中文或拼音。示例:北京、beijing、上海、shanghai。不要加“市”字后缀的变体。" } }

后者的参数生成稳定度明显优于前者。我专门统计过一次,加了示例和边界约束之后,参数校验一次通过率从 62% 提升到了 88%。

2.3 兜底与人机协同槽位:技能不能只有一条执行路径

技能文件里还需要考虑“执行失败怎么办”。很多 Agent 框架里,工具调用失败会返回一个 error 字符串,模型再拿错误信息重试。这种方式在复杂技能上效果很差,因为模型根本看不懂一堆 traceback。

我习惯在每个技能里预置三件套:

  • 一个参数级的前置校验函数,在真正执行前拦截非法参数,返回结构化错误码;
  • 一个降级策略,比如天气接口超时,是否允许返回基于历史数据的预测;
  • 一个触发人工介入的槽位,当技能执行结果置信度低或参数缺失时,技能可以主动返回“需要用户补充信息”的指令,而不是硬跑。

这部分可以写进技能文件里,作为fallback配置。它让技能不只是机械地被调用,而是拥有像人一样“问清楚再做”的能力。在客户现场演示时,智能体遇到模糊指令时主动反问“您是需要本周还是下周的日程”,会让人明显感觉到系统“有脑子”而不是“会套模板”。

3. 技能的注册、检索与执行:agent-skills 的运行时链路

技能文件设计得再好,没有一条顺畅的运行时链路也白搭。一条完整的链路包含三个环节:注册表负责管理技能文件,检索层负责把人话翻译成技能 ID,执行器负责把技能 ID 变成真实结果。三个环节各自独立又必须协同,我把这套设计称为“技能三角”。

3.1 技能注册表:一本可以被索引的“技能目录”

注册表本质上是一个存储技能元数据和索引的服务。它不直接保存技能的源代码,而是保存“技能名、版本、描述向量、参数 schema、执行入口地址、状态”这些信息。一份技能进入注册表,通常会经历四个阶段:

  1. 提交:上传技能文件包,触发解析和校验;
  2. 校验:检查 YAML/JSON 格式、参数 schema 合法性、执行入口是否存在;
  3. 索引:对描述文本做向量化和关键词倒排索引;
  4. 上线:更新注册表中的状态,旧版本进入历史记录。

我最初用 Redis 加 JSON 文件存技能元数据,技能量少的时候完全够用。后来技能多了,就换成了 PostgreSQL 加 pgvector。注册表设计上有一个关键点:技能状态必须包含“禁用”和“灰度”。灰度技能只允许特定 agent 实例加载,这样在技能改版时可以小范围试跑,而不是一上线就让所有 agent 跟着变风格。

注册表还需要提供按版本回溯的能力。技能的调用记录里会带上skill_version,哪天线上效果突然变差,直接比对当前版本和上一版本的描述差异,通常很快就能定位是不是技能描述改坏了。

3.2 语义检索与路由:不只是关键词匹配

技能检索是我花时间最多的地方。早期我直接用关键词匹配,发现用户问“明天降温吗”这种口语化表达很难命中weather_query这样的技能名。后来升级成向量检索,效果立刻好转,但也引入了一个新问题:只靠向量相似度,两个能力接近的技能很容易互相抢占排名。

我现在用的是混合检索加排序融合。第一层同时跑关键词检索和向量检索,各自取 top 20;第二层用一套轻量级排序规则,把两路结果合并排序。排序规则里权重最高的是“负面描述匹配度”:如果用户 query 命中了某个技能的 negative_description,这个技能就会被直接降权甚至剔除。这个规则非常管用,比如“明天北京的空气适合跑步吗”会同时命中天气技能和 AQI 技能,但 AQI 技能的 negative_description 里写了“不要用于预测户外运动适宜度”,于是它就会被压到后面。

路由层还会维护一张“最近命中表”,记录过去一段时间哪些技能被高频使用。出现多技能候选分数接近的情况时,优先选择高频技能,可以减少模型在相似技能间的随机漂移。这个启发式策略并不完美,但在生产系统里非常实用。

3.3 执行器:参数校验、环境隔离、结果回传

执行器是实际跑技能的环节,我把它设计成独立进程,不随 Agent 主进程跑。这样做的原因很简单:如果技能代码有 bug,或者技能需要访问外部网络,至少要把它隔在一个受控环境里,不能让它拖垮整个 Agent 服务。

执行器内部流程:

  • 从注册表加载技能文件,获取参数 schema 和执行入口配置;
  • 对模型生成的参数做严格校验,失败则返回错误码并附带“该怎么改”的提示;
  • 在受控环境中执行技能逻辑,设置超时和资源限制;
  • 把结果整理成统一格式返回,包括执行耗时、结果数据和可读摘要。

这里有一个值得强调的细节:给模型返回的结果格式一定要“紧凑”且“结构化”。你给模型返回一坨 JSON 原文,模型会迷失在字段里;但你返回“温度 22 度,降雨概率 30%,建议携带薄外套”这样一段可读摘要,模型就能直接基于它做后续决策。所以我现在给每个技能增加了result_summarizer配置,执行器拿到原始结果后,先经过一层摘要逻辑,再返回给 Agent 主对话。这个设计极大提升了端到端生成质量。

4. 从零搭建一个 agent-skills 最小落地系统:代码级拆解

光讲概念容易飘,我直接给一套能跑通的最小实现思路。这里不会把完整代码贴成几千行,而是把关键模块的结构和核心逻辑讲清楚,你照着搭就能有个雏形。

4.1 技能仓库的最小目录结构

我自己会按下面这样组织技能仓库:

skills/ ├── registry/ # 注册表服务 │ ├── storage.py # 技能元数据存储 │ ├── indexer.py # 描述向量索引 │ └── router.py # 混合检索与排序 ├── executor/ # 执行器服务 │ ├── validator.py # 参数校验 │ └── runner.py # 技能进程隔离执行 ├── skill_definitions/ # 技能文件 │ ├── weather_query/ │ │ ├── skill.yaml # 元信息与参数协议 │ │ ├── main.py # 执行入口 │ │ └── tests/ # 本地测试用例 │ └── calendar_query/ │ ├── skill.yaml │ ├── main.py │ └── tests/ └── agent_adapter/ # 与大模型框架对接的适配层 └── openai_functions.py

这个结构的好处是职责非常清晰:技能是独立包,注册表是中央管理服务,执行器是独立进程,适配层只负责把技能描述转换成模型需要的工具格式。改任何一个技能,不需要动 Agent 主程序的代码。

4.2 注册与检索核心逻辑:混合检索的简化实现

下面是我抽出来的检索层核心代码,保留了最关键的部分:

# router.py import json from sklearn.feature_extraction.text import TfidfVectorizer from sentence_transformers import SentenceTransformer model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") vectorizer = TfidfVectorizer() class SkillRouter: def __init__(self, skills): self.skills = skills self.vectors = [model.encode(s["description"]) for s in skills] self.tfidf_matrix = vectorizer.fit_transform([s["description"] for s in skills]) def retrieve(self, query, top_k=5): q_vec = model.encode(query) vec_scores = [cosine_sim(q_vec, v) for v in self.vectors] q_tfidf = vectorizer.transform([query]) key_scores = (self.tfidf_matrix * q_tfidf.T).toarray().flatten() combined = [] for i, skill in enumerate(self.skills): # 命中负面描述直接重罚 penalty = 0.0 if any(neg in query for neg in skill.get("negative_keywords", [])): penalty = 0.5 score = 0.7 * vec_scores[i] + 0.3 * key_scores[i] - penalty combined.append((score, skill)) combined.sort(key=lambda x: x[0], reverse=True) return [s for _, s in combined[:top_k]]

这段代码虽然简单,但体现了几个关键设计:向量分和关键词分按 7:3 加权;负面描述命中直接减 0.5;排序结果送入生成层后,再让模型从 top 5 的候选中确定最终技能。真实系统里还会加上技能历史频率、用户当前上下文等特征,但核心骨架就是这个。

4.3 执行器的一次完整调用:查天气并安排行程

假设用户说“北京明天下雨吗,帮我看看要不要改周五的会议时间”。这条请求会经过三个技能的协同:weather_query负责查天气,calendar_query负责查周五日程,meeting_reschedule负责生成改期建议。三个技能通过执行器链式调用,第一个技能的结果作为第二个技能的输入补充。

实际跑出来的链路大概是这样:

用户输入 → 路由层命中 [weather_query(0.92), calendar_query(0.81), meeting_reschedule(0.67)] → weather_query 执行: {"city": "北京", "forecast_days": 2} → 返回: 明天小雨,降雨概率70%,气温18-22℃ → 原始结果经 summarizer 转为: "北京明天有小雨,户外活动建议改期" → 继续路由: calendar_query 查询周五日程 → 发现 15:00 室外会议 → meeting_reschedule 建议: 将会议转为线上或改至室内

整个过程中,模型自始至终没有接触过技能的原始 JSON 接口,接口细节全被技能封装和执行器摘要处理掉了。这正是 agent-skills 这套设计在生产环境的价值所在:它让模型只需要“思考业务”,而不需要“理解工程”。

5. 把技能真正接进业务前,我替你们踩过这些坑

技能框架搭好只是开始,真正让人崩溃的往往是接入后的细节。我在这套系统上踩了不少坑,挑几个印象最深的展开说。

5.1 技能描述互相打架:作用域和优先级要显式声明

有一段时间,我同时有text_summarize(通用文本摘要)和meeting_minutes_summarize(会议纪要摘要)两个技能。用户让模型“总结一下昨天例会的要点”,模型十次里有六次选了通用摘要技能,导致输出的格式不符合会议纪要规范。后来我在通用摘要技能里明确加了negative_description:不用于会议纪类摘要,会议纪要请用meeting_minutes_summarize,并在路由排序里给了会议技能更高优先级。改完之后准确率立刻上去。

这里的关键教训是:相近技能之间必须显式声明“边界”,不能让模型自己去领悟。描述里写清楚“本技能不处理什么、什么场景请找另一个技能”,比单纯强化本技能的描述有效得多。

5.2 参数校验失败被模型“吞掉”的问题

另一个非常隐蔽的坑是:技能执行器返回了参数错误,模型却把这个错误当成了正常回答的一部分,直接说“抱歉,我暂时无法查询天气”。排查下来,发现原因是错误码格式太“程序化”,模型根本不知道这是可修复的问题。后来我把执行器的错误信息统一改成三段式:错误码 + 可读错误 + 修复建议。比如“PARAM_MISSING | 缺少日期参数 | 请补充日期后再调用”。“修复建议”被模型看到后,会自动进入修正流程,而不是直接放弃。

5.3 技能改版后,旧索引还在“迷惑”模型

技能文件更新了,但注册表里的向量索引还没重新生成,这种状态我是吃了亏才发现的。某个技能描述从“查询天气”改成了“查询 7 天天气趋势”,模型还是会按旧描述理解,检索到最后的结果跟实际能力不匹配。解决办法也很简单:任何技能文件变更都必须触发重新索引任务,并把变更记录写入变更日志。我在 CI 流程里加了一步校验,只要 skill.yaml 的 version 和 git commit 不一致,就不允许上线。

5.4 技能超过 50 个之后,检索质量会迅速下降

当技能数量达到五十个以上时,单纯依赖向量检索的问题越来越明显:分数普遍被拉高,区分度变差。我的应对思路是给技能加“业务域”一级的分组。先把技能划分到weather、calendar、文档处理、审批流等域,路由时先用一个轻量分类器判断用户请求属于哪个域,再在域内做技能检索。这样域的粒度把候选集缩小到 5-10 个,检索质量问题迎刃而解。这个“域优先”的思路,本质上是把路由粒度分层,避免让上层模型直接面对上百个技能。

5.5 技能不是越多越好:定期淘汰和合并

最后一条经验可能有点反直觉:技能不是越多越强大,反而有维护成本。我在系统运行半年后做过一次清理,把使用率低、且和其他技能重叠度高的技能合并或下线。比如原先有weekly_report_summarize和daily_report_summarize,后来合并成一个report_generate,内部通过参数控制周期。这个操作让技能总数减少了三成,而整体检索准确率反而提高了。技能库是一个要持续经营的系统,不是一次性写完就完事的。

就我个人实践来说,agent-skills 这套方法极大改善了我维护智能体项目的体验。它最大的价值并不在于某个具体的检索算法或执行器设计,而在于它把“模型怎么选工具”这件事,从不可控的 prompt 拼接变成了可管理、可检索、可回滚的工程体系。如果你现在正被工具调用准确率不高、描述更新不及时、多技能冲突这些问题困扰,我建议也按这个思路整理一下你的技能层:先写一份带负面描述的技能文件,再做一层混合检索,最后把执行器独立出来。这套路不一定非要用开源框架,自己动手实现一遍反而能更深地理解每个设计背后的原因。我也还在持续迭代这套系统,目前正在尝试把技能的执行结果反馈给索引层做学习,让经常被选中的技能排名越来越高,有兴趣的话我们后面可以继续展开聊。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 16:45:49

Claude跨会话记忆神器claude-mem:MCP服务器原理与实战

1. 得先承认一个尴尬事实:AI助手没有长期记忆1.1 你看似在跟同一个AI聊天,其实每次都是陌生人如果你跟Claude聊过几次,大概率会遇到这样的场景:昨天刚跟它敲定的项目架构方案,今天打开新会话再问,它一脸茫然…

作者头像 李华
网站建设 2026/10/8 16:45:26

AI副驾驶如何重塑脑机接口控制权分配|老马精读

我一直觉得,脑机接口领域需要分清两件事:把脑信号“读出来”,和把读出结果“用好”。过去二十年,大家绝大多数力气花在前者——刷准确率、刷信息传输率,但一到真实环境,系统就像个紧张的新手司机&#xff1…

作者头像 李华
网站建设 2026/10/8 16:44:53

Text-to-CAD实战:从一句话描述到可制造的CAD模型

我第一次注意到“text-to-cad”这个词,是在一个3D打印社群里。有人发了一段话,说“帮我做一个能卡在桌沿的理线架,开口要 12 毫米宽”,然后贴出了一张渲染图。底下人第一反应是“你用什么软件画的”,结果回答是“我没画…

作者头像 李华
网站建设 2026/10/8 16:41:48

AI日报实战:TPU、智能体与Claude Code/Codex工具链配置指南

1. 从一份日报说起:AI 圈每天都在发生什么 做 AI 方向的内容或者工程,最头疼的一件事就是信息太碎。今天 TPU 出了新版本,明天 OpenAI 的 Codex 命令行工具更新了安装方式,后天 Claude 的桌面端又改了配置逻辑,再往后智…

作者头像 李华
网站建设 2026/10/8 16:40:37

WorkBuddy + Hypit 实战:爆款短视频结构拆解与脚本自动化生成

1. 这套组合到底在解决什么问题 刷到一条爆款视频,画面节奏、转场、文案钩子都踩在点上,你想复刻一条类似的,但打开剪辑软件就懵了——从哪一帧开始切、文案怎么改、配乐怎么卡点,全靠感觉硬怼,最后做出来的东西自己都…

作者头像 李华
网站建设 2026/10/8 16:38:28

重装系统后上不了网?驱动备份与WiFi配置恢复全攻略

先抛个问题:有多少人重装完系统,开开心心等它进桌面,结果右下角网络图标直接给你画个红叉,或者打个小黄叹号?那一刻真的能把人气笑了。系统能用但上不了网,等于一个残疾人坐在电脑前,什么都干不…

作者头像 李华