资讯动态

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

发布时间:2026/10/8 16:46:06 来源:尧图企业网站定制
做过智能体项目的朋友应该都有体会真正难的不是把大模型接进来而是让模型知道“什么时候该调用什么、调用完了结果怎么处理”。我最早做工具调用时用的是一张写满函数说明的 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、执行入口地址、状态”这些信息。一份技能进入注册表通常会经历四个阶段提交上传技能文件包触发解析和校验校验检查 YAML/JSON 格式、参数 schema 合法性、执行入口是否存在索引对描述文本做向量化和关键词倒排索引上线更新注册表中的状态旧版本进入历史记录。我最初用 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_k5): 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(keylambda x: x[0], reverseTrue) 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 拼接变成了可管理、可检索、可回滚的工程体系。如果你现在正被工具调用准确率不高、描述更新不及时、多技能冲突这些问题困扰我建议也按这个思路整理一下你的技能层先写一份带负面描述的技能文件再做一层混合检索最后把执行器独立出来。这套路不一定非要用开源框架自己动手实现一遍反而能更深地理解每个设计背后的原因。我也还在持续迭代这套系统目前正在尝试把技能的执行结果反馈给索引层做学习让经常被选中的技能排名越来越高有兴趣的话我们后面可以继续展开聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑