Agent Skills实战:构建稳定可扩展的智能体技能注册与调用体系
1. agent-skills 到底是什么先搞清楚我们要解决的问题这两年只要聊到 Agent几乎绕不开一个词agent-skills。很多人第一次听到它第一反应是“这不就是给 Agent 写几个函数吗”如果你也这么想那大概率在项目做到一半的时候就会发现事情没那么简单。我自己的体会是agent-skills 本质上是一套让大模型驱动的智能体能够稳定调用外部能力的方法论和工程体系。它不仅仅是写几个工具函数那么简单而是涵盖了技能的定义、注册、检索、调用、容错、观测和扩展这一整条链路。换句话说你今天写了一个“查天气”的函数这只能算一个 API只有当这个函数能被模型在合适的场景下自动发现、正确调用、并在出错时优雅降级它才配叫一个 skill。这个领域之所以突然变得重要和 Agent 自身的发展阶段密切相关。早期大家用 LangChain 之类的框架链式调用模型每次任务都是写死的工作流模型几乎没有自主决策空间。后来大家发现大模型本身的推理能力已经足够强为什么不把任务拆解和工具选择的权力交还给模型呢于是基于 ReAct 模式、函数调用Function Calling的 Agent 开始流行。但问题也随之而来当 Agent 的技能数量从几个增长到几十个、上百个时靠系统提示词System Prompt硬塞列表已经完全不现实了。技能如何组织、如何被模型准确选中、如何避免技能之间的互相干扰成了真正卡脖子的技术难点。这篇文章我就以“agent-skills”作为主题分享一下我在实际项目里做技能注册中心、技能编排和调用链路的完整经验。不追求教科书式的全面讲解重点放在那些真正能落地、能上生产环境的细节上。如果你正在做自己的 Agent 应用或者准备把现有的框架改造成技能驱动架构这篇文章应该能帮你少踩不少坑。2. 先定架构agent-skills 应该长什么样子2.1 从“函数列表”到“技能库”的思维转变如果只是应付一两个 demo 性质的 Agent你完全可以在 system prompt 里把所有工具的描述列出来让模型慢慢选。但生产环境完全是另一回事。我在项目里处理过最多的场景是技能目录膨胀之后引起的意图混淆和检索失效。模型面对 50 个以上描述相似的技能时选择准确率会明显下降而且你很难通过微调 prompt 来彻底解决。所以第一步要转变的是把它当成一个独立的“技能库”来设计而不是散落在代码里的函数集合。一个完整的 agent-skills 体系在我的实践里通常由四个层次组成技能注册中心Skill Registry统一登记所有可用技能的元数据、版本、依赖和权限要求。技能执行引擎Skill Executor负责实际调用技能对应的实现代码管理入参校验、超时和结果返回。技能发现与路由Skill Discovery根据用户请求和对话上下文选择合适的技能组合并规划调用顺序。技能观测与治理Skill Observability记录每次调用的输入、输出、耗时、失败原因为后续优化提供数据支撑。这套分层设计的好处是让每个组件可以独立演进。比如注册中心的格式调整不会影响到执行引擎的代码新的技能上线只需要在注册中心里登记不需要改动 Agent 的核心逻辑。很多团队上来就把工具调用逻辑和业务代码耦合在一起短期内看着省事到后期加一个技能要改三个模块维护成本直接起飞。2.2 为什么技能注册中心是整个体系的枢纽在整套 agent-skills 架构里我最看重的其实是技能注册中心。它有点像一个项目的接口文档中心但比文档要求更高它既要让人类开发者能看懂也要让模型能精确地理解每个技能的功能边界。注册中心存储的信息通常包括技能 ID、名称、描述、参数 Schema、返回值 Schema、依赖资源、权限标签、版本号等。其中“描述”这一项是最容易被低估的。很多开发者写技能描述时特别随意就一句“这是一个天气查询接口”结果模型在调用时经常判断失误。真正高质量的技能描述应该像一个优秀的同事给你交代任务一样不仅说明这个技能是做什么的还要说清楚它适合什么场景、不适合什么场景、特殊参数怎么传、常见的坑有哪些。我举个例子。一个订单查询技能简单的描述可能是查询订单信息但我推荐写成这样当用户询问特定订单的状态、物流、金额等信息时使用。 需要提供订单号如果用户没有提供订单号err_prompt 会引导用户补充。 如果用户询问的是批量订单统计请使用“订单报表生成”技能而不是本技能。这个差异在生产环境里是决定性的。前者让模型靠猜后者把决策依据摆得明明白白。我把这类“路由指引”叫技能的上下文胶水能显著提高模型在多技能场景下的选择准确率。3. 构建技能注册中心从数据模型到核心 API3.1 技能注册中心的元数据模型设计在动手写注册中心之前先定义清楚技能元数据的数据结构。下面是我在项目里用得比较顺手的一套 JSON 结构兼顾了机器可读和人工可维护性。{ skill_id: order_query, name: 订单查询, version: 2.3.0, description: 当用户询问特定订单明细、物流状态时使用。需要订单号支持电商与线下门店订单查询。, tags: [order, query, ecommerce], author: platform-team, owner_department: 交易中台, permission: user.auth.required, visibility: public, parameters: { type: object, properties: { order_id: { type: string, description: 用户提供的订单号通常为数字字符串, required: true }, detail_level: { type: string, enum: [basic, logistics, payment], default: basic, description: 查询详细程度默认 basic } } }, return_schema: { type: object, properties: { order_status: { type: string }, logistics_trace: { type: array } } }, timeout_ms: 3000, idempotent: true, rate_limit: { window_sec: 60, max_calls: 120 }, changelog: [2025-01-10: 新增物流轨迹查询支持] }有几个字段值得单独说一下。首先是visibility它决定了这个技能对哪些 Agent 可见。在大型组织里有些技能可能是内部运营专用的有些技能需要用户授权后才能调用。如果不设置这个字段后续做技能权限管理时就会非常痛苦。其次是idempotent这个字段表示技能是否可以安全地重复调用。对 Agent 来说模型很可能会因为超时重试而重复提交同一个指令。如果技能本身不是幂等的比如“创建工单”“转账付款”那一旦模型自动重试就会产生重复操作后果很严重。设置了这个字段之后执行引擎可以针对非幂等技能做额外的二次确认或者生成幂等键来去重。3.2 注册中心的读写 API 设计注册中心本质上是一个元数据存储服务我把它的核心 API 设计成下面这样涵盖了技能的增删改查、上下线和版本管理。# 伪代码注册中心核心接口 from typing import Optional from dataclasses import dataclass dataclass class SkillRepository: storage: dict # 实际生产环境建议用 Redis/PostgreSQL 持久化 def register(self, skill_meta: dict) - str: 注册新技能返回 skill_id。 如果 skill_id 已存在则执行覆盖并记录版本变更。 skill_id skill_meta[skill_id] skill_meta[version] skill_meta.get(version, 1.0.0) skill_meta[created_at] skill_meta.get(created_at, now()) self.storage[skill_id] skill_meta return skill_id def get(self, skill_id: str) - Optional[dict]: return self.storage.get(skill_id) def delete(self, skill_id: str) - bool: # 删除前建议检查是否有 Agent 正在引用 return self.storage.pop(skill_id, None) is not None def list_skills(self, tags: Optional[list] None, visibility: str public) - list[dict]: 返回技能列表按 tag 过滤。 路由模块会调用这个接口做技能候选集筛选。 result [] for meta in self.storage.values(): if meta[visibility] not in (public, visibility): continue if tags and not set(tags).intersection(set(meta[tags])): continue result.append(meta) return result在实现注册中心时我强烈建议把“读”和“写”分开。写入操作走管理后台或 CI/CD 流水线频率很低读取操作是在 Agent 每次决策时都要发生的高频操作。所以注册中心的元数据建议在服务启动时加载到本地缓存配合 Redis 做跨节点一致性。每次技能变更后通过版本号或者更新时间戳让缓存失效这一步能显著降低 Agent 决策链路的延迟。3.3 技能描述的 Prompt 工程化处理刚才提到了描述质量很重要那具体怎么把描述写成模型能理解的样子这里有一个技巧用测试对话来反向检验描述质量。我通常会在写完一个技能描述后构造 10 到 20 条典型的用户请求然后让模型只根据描述来决定是否调用这个技能。如果某些请求被错误地路由到了别的技能说明描述存在歧义需要调整。这个过程听起来简单但非常有效。很多时候你觉得一个描述已经写得很明白了但模型就是选了另一个技能原因往往是另一个技能的描述里包含了更高频的关键词。这时候不是怪模型笨而是要对描述做差异化设计在自己的技能描述里明确写上“如果你只是想 XX不要用我”。另一个小技巧是给技能描述加“反向示例”也就是明确说明哪些情况不该调用当前技能。这个在真实场景里比正向描述更能提高路由准确率。模型在二选一的场景下最怕就是两个技能描述既包含正向匹配词又缺少排他性信息。加一句“本技能无法处理 XX 类需求请使用 YY 技能”往往能解决大半问题。4. 技能执行引擎打通从意图到落地的最后一公里4.1 设计技能装饰器把普通函数变成“可被模型调用”的技能注册中心解决的是“技能如何被描述和组织”的问题执行引擎解决的是“技能如何被稳定地跑起来”的问题。在实际编码里我倾向于用 Python 装饰器的方式把已有业务函数快速包装成标准技能。下面是一个简化版的技能装饰器实现import inspect import functools from typing import Callable, Any, get_type_hints def skill( skill_id: str, description: str, tags: list[str] | None None, timeout_ms: int 3000, idempotent: bool True, permission: str user.auth.required, ): 将普通函数包装为一个标准 Agent Skill。 用法示例 skill( order_query, 查询订单信息需要订单号 ) def query_order(order_id: str) - dict: return {status: delivered} def decorator(func: Callable) - Callable: # 自动从函数签名中提取参数 Schema hints get_type_hints(func) signature inspect.signature(func) properties {} required [] for param_name, param in signature.parameters.items(): if param_name in (self, cls, kwargs, args): continue properties[param_name] { type: _map_type(hints.get(param_name, str)), description: f参数 {param_name}, } if param.default is inspect.Parameter.empty: required.append(param_name) # 为函数挂载技能元数据 func.__skill_meta__ { skill_id: skill_id, name: description.split(。)[0] if description else func.__name__, description: description, tags: tags or [], parameters: { type: object, properties: properties, required: required, }, timeout_ms: timeout_ms, idempotent: idempotent, permission: permission, handler: func, } functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator这段代码的核心价值在于它把“函数签名”自动映射成了模型的参数 Schema省去手写 JSON Schema 的繁琐工作。你只需要在函数上面加一行装饰器就能把一个普通函数升级为具备完整元数据的技能。这对团队的协作体验提升非常明显——后端同事不需要理解模型推理细节只需要学会用装饰器就能把内部接口包装成 Agent 可调用的技能。4.2 参数的严格校验与自动补全在实际调用中我发现模型传参经常出现两类问题。一类是类型传错比如文档说order_id应该是字符串但模型可能传了一个数组。另一类是缺少必要参数模型在对话中没拿到信息就直接调用了技能。为了解决这两个问题我在执行引擎里实现了三层参数处理逻辑。第一层是基础类型校验检查参数类型是否匹配如果类型不匹配但能安全转换就自动做转换。第二层是语义补全从对话上下文中提取缺失的必填参数信息比如用户上一轮说了订单号这轮只说了“帮我查一下”技能引擎应该能从记忆里自动填上订单号。第三层是拒绝执行当必填参数确实缺失且上下文无法补全时返回一个固定格式的“缺参请求”给 Agent 的规划模块让它继续向用户提问而不是强行用空参数把技能跑一遍然后拿个错误结果。开放问答场景里语气很重要但技能层返回值一定要统一结构化。我的做法是每个技能返回固定格式的{ success: bool, data: dict, error: dict | None }。这样下游的 Agent 规划模块拿到结果之后不需要再做一层格式解析可以直接拼进上下文继续推理。4.3 技能调用的超时、重试与熔断生产环境里一个 Agent 往往需要按顺序调用多个技能才能完成一个任务。如果其中一个技能依赖的下游服务变慢整个 Agent 的响应时间都会被拖垮。所以技能的治理规则不能不提前设计。我设置了三个层级的保护超时、重试、熔断。超时每个技能在注册中心里定义了timeout_ms执行引擎用异步任务包裹超时直接丢弃结果并返回错误。重试只有idempotent true的技能才允许自动重试。非幂等技能重试逻辑必须由上层业务人工确认。熔断连续失败超过阈值比如 5 次后该技能自动进入“冷却”状态Agent 在路由阶段就会跳过它而不是反复调用失败接口。熔断这个点尤其要提醒。有些开发者觉得 Agent 会自己判断错误并换个方案但实际情况是如果技能列表里仍然存在一个已故障的技能模型很可能会在下一次规划时继续选它。把故障技能从路由候选里隐去比在调用后报错要优雅得多。5. 技能调用决策让模型在正确的时间选到正确的技能5.1 先粗筛后精排技能发现不能全靠模型当技能数量超过一定量级后直接把所有技能 Schema 塞给模型不仅 token 消耗大而且模型在超大候选集里选错概率急剧上升。我在项目里采用的是“粗筛 精排”两阶段路由。第一步粗筛是基于关键词和标签的检索。将用户当前请求和对话历史做一次轻量的实体识别和高频词提取然后和技能标签做匹配挑出 10 到 15 个候选技能。这一步不需要太高的准确率只要保证真正的目标技能不会被过滤掉就行。第二步精排是把候选技能的描述和参数 Schema 拼进 prompt让模型从候选里挑出最合适的 1 到 3 个技能。由于候选数量少token 压力小模型的决策精度也能大幅度提高。下面是一个粗筛函数的示意代码import re def coarse_filter(user_input: str, all_skills: list[dict], top_k: int 12) - list[dict]: 基于简单规则从全量技能中筛选出候选技能。 这里用关键词匹配做示例实际生产可以考虑接入向量检索。 # 抽取用户输入中的名词短语简化处理只提取中文或英文单词 entities set(re.findall(r[\u4e00-\u9fa5]{2,}|[a-zA-Z]{3,}, user_input.lower())) scored [] for skill_meta in all_skills: score 0 # 对技能描述和标签做关键词匹配 text_pool (skill_meta.get(description, ) .join(skill_meta.get(tags, []))).lower() for ent in entities: if ent in text_pool: score 1 if score 0: scored.append((score, skill_meta)) scored.sort(keylambda x: x[0], reverseTrue) return [meta for _, meta in scored[:top_k]]这个方案在中小规模技能库几十个技能下表现稳定。如果技能库膨胀到几百个甚至上千个我会把粗筛这层替换为向量检索配合标题嵌入和描述嵌入混合打分。但不建议一开始就上向量化因为维护向量索引的成本并不低尤其是技能描述经常变化的时候。5.2 系统提示词与技能描述在上下文中的排布在精排阶段系统提示词的结构也颇有讲究。我的经验是不要罗列所有技能而是按“任务类目”分组。把候选技能按业务域组织比如订单域、物流域、售后域然后告诉模型“如果你需要查询订单以下技能可供选择”。分组能极大降低模型的认知负担它不再需要在一长串平铺的列表里做区分而是先想清楚自己需要哪类能力再进入对应的组里选技能。还有一个经常被忽视的细节把对话历史中的关键信息以摘要形式叠加到技能选择 prompt 上。举个例子用户第一句话是“我想查一下昨天买的东西到哪了”模型在后续决策时如果只能看到最新的那轮请求“地址是什么”就很容易困惑。如果能在技能选择阶段附加一句“用户之前提到要查询订单物流”模型的决策质量完全不同。5.3 避免技能幻觉让模型学会说“我不需要技能”很多 Agent 框架从一开始就引导模型“能调就调”导致模型把很多简单的问题也包装成技能调用。比如用户就问了句“你们几点下班”如果技能列表里刚好有个“客服工作时间查询”模型可能也会调一下。这在 demo 阶段显得很聪明但在生产环境里无谓的调用既增加延迟也增加下游系统的负载。在提示词里我会刻意强调“不是每个问题都需要调用技能”。同时给路由模块增加一个自动决策分支当技能候选粗筛阶段的最高分低于某个阈值或者用户请求中的实体与任何技能描述完全不匹配时直接跳过技能调用把原始问题交给模型用常识回答即可。6. 技能观测与质量治理上线之后才是真正的开始6.1 技能调用日志应该记录哪些维度技能上线只是第一步真正决定长期体验的是数据反馈闭环。我给每个技能增加了一套标准的调用日志 JSON核心字段包括用户请求原文路由决策结果选了哪些技能各技能置信度排序技能入参和出参的快照调用耗时、失败原因模型对结果的二次判断判断技能结果是否成功解决了用户问题这些日志有三大用途。第一离线分析路由准确率定位哪些技能的描述需要优化。第二及时发现技能本身的性能问题比如某个技能调用量暴增可能是路由压倒了某个不该覆盖的意图。第三为后续做基于强化学习的路由优化积累数据虽然这在多数项目里是远期目标但先把数据收集起来总没有坏处。6.2 技能回放与回归测试让每一次改动都可追溯技能描述或代码改动最怕的就是“这次改完别的地方出问题”。我在实践里建立了一个小型的技能回放测试集把过去一段时间真实的用户请求按技能分组保存下来每次技能版本升级后在离线环境里对这些请求做一次批量路由和调用测试对比新旧版本的输出差异。如果新版本在某些样本上路由到了不同的技能或者调用结果与预期不符系统会高亮提示人工检查。这个东西本质上就是一套为 Agent 技能准备的回归测试系统实现成本并不高但能在关键时刻救你一命。我有一次优化了一个技能的描述离线回放立刻发现有两成原本能正确路由的请求跑偏了当时如果直接发上线一定会收到一堆用户投诉。这个经验换成传统软件开发其实很自然——没有测试谁敢直接改核心模块但到了 Agent 技能这里很多团队反而偷懒了。因为技能的行为有概率性所以更要做回归验证不能只靠“我觉得描述优化了应该没问题”。6.3 技能废弃与灰度下线的流程技能也会老化。业务方变更了内部接口某个技能长时间无人调用或者被新技能完全取代这些场景都需要有下线的流程。我见过最粗暴的做法是直接从注册中心删除。但这样很危险历史会话里有些还是旧技能在跑删除后这些会话会直接报错。合理的做法是给技能增加生命周期状态active→deprecated→disabled。deprecated状态下技能仍可使用但注册中心会在它的描述里标注“即将下线请转向使用 XX 技能”新会话的路由层会降低对它的选择优先级已经发出但还没完成的调用可以正常跑完。观察一段时间确认没有新增流量后再把它改为disabled。这套流程虽然简单但在协作团队里能避免很多不必要的线上事故。7. 工具链选型在框架、低代码平台与你自己的代码之间做选择7.1 开源框架 vs 自研组件现在做 agent-skills 并不需要一切从零开始市面上已经有相当多成熟的方案可以借力。我自己常见的几种选择组合如下表需求层次可选方案我的选择建议Agent 对话与编排LangChain、LlamaIndex、Dify、Coze技术团队以代码开发为主用 LangChain/LlamaIndex 更灵活业务团队想快速产出用 Dify/Coze函数调用能力OpenAI Tool Calling、Anthropic Tool Use、各类开源模型 Function Calling不必绑定大厂模型自带的结构化输出支持可能更稳定技能注册与治理自研轻量注册中心通用框架自带的工具注册机制偏简单生产级治理建议自研观测与追踪LangSmith、Langfuse、自研日志系统优先用现成的可观测平台重点看对技能调用的追踪支持并不是说这些开源框架不好而是在技能治理这块它们的抽象层次普遍还比较薄。比如 LangChain 的tool装饰器确实能快速定义一个工具但它对技能版本、权限、熔断、观测这些生产级问题的管理能力很有限。我的建议是可以拿框架的 agent runner 做执行骨架但技能注册和治理这一层需要自己攒尤其是当你不止做一个 Agent而是要支撑多个 Agent 共享技能库的时候。7.2 自定义技能库时要不要上 MCPMCPModel Context Protocol是近期比较受关注的一个协议它本质上定义了大模型应用与外部工具/数据源之间的标准化接口。如果你想做一套开放的技能生态让不同团队甚至第三方开发者都能往你的 Agent 里贡献技能那 MCP 是值得考虑的规范。它最大的优势是把“技能提供方”和“技能消费方”解耦一个服务如果实现了 MCP可以做数据库查询技能也可以做文件分析技能。但要说清楚MCP 不是银弹。如果你的 Agent 完全在单一技术栈内部闭环业务方都是自己的后端同学直接走内部 RPC 反而比引入 MCP 多一跳网络开销和协议转换。我的建议是内部技能不必强行 MCP开放生态才值得引入 MCP。很多人听到新协议就往上冲结果只是为了在内部服务之间调一个查库存的接口属实没必要。8. 常见问题与排查技巧实录8.1 模型总是选错技能该怎么办这是我来被问得最多的一个问题。排查这类问题的标准步骤是先看路由日志确认粗筛阶段目标技能是否进入了候选集。如果没有问题出在粗筛规则关键词或向量召回上。如果进入了候选集但模型没选它说明精排 prompt 里技能描述区分度不够。这时候优先做描述差异化增加排他性说明。如果描述看起来已经很清晰再检查候选技能列表的排列顺序。有些模型对列表靠前的技能有明显的选择偏好需要把和目标意图最接近的技能放在靠前位置。最后才是考虑微调或更换模型。绝大多数选错问题在数据和 prompt 层就能解决不值得一上来就上大模型微调。8.2 技能调用超时导致对话中断技能超时不一定会导致整个 Agent 崩溃但如果没有合理的错误处理用户会体验到“机器人突然不说话了”。我在执行引擎里设计了超时返回“该技能暂时不可用”的结构化错误并让规划模块感知到这个错误后自动向用户解释并给出备选方案。如果这个技能是只读查询比较简单如果是写操作还要注意超时后的状态不确定性。比如一个接口实际上写入成功了但响应超时被标记为失败Agent 端如果再提示用户“操作失败请重试”用户一重试就产生了两条相同记录。这个场景建议在业务接口设计里补充幂等键支持。8.3 技能数量膨胀后系统提示词的 token 压力过大当全量技能数据太大时有几个缓解方案。最优先的是做技能分组按部门和场景拆分成多个技能子域在 Agent 启动时只挂载当前场景相关的技能子域。其次是做描述压缩把长描述改成简洁的说明和几个高质量示例词让模型更容易理解。最后才是上向量检索和动态召回这个方案虽然先进但对基础设施的要求也更高。在压缩技能描述时有一点需要特别注意不要为了省 token 把参数 Schema 里的枚举值说明删掉。模型如果不知道某个参数可以填哪些选项很容易编造出非法值。参数说明可以有技巧地压缩比如“type: string, enum: [basic, logistics, payment]”只保留枚举项不加多余解释已经能帮助模型做出稳定选择。9. 最后一个经验把 agent-skills 当成产品来做而不是技术亮点说回整个 agent-skills 的落地。我个人最大的体会是这个体系能不能跑起来技术不是瓶颈组织协同才是。技能不是一次性交付的静态代码而是需要持续更新的活资产。一次成功的 agent-skills 落地背后至少需要三种角色的配合业务方负责定义技能边界和验收标准开发负责把已有接口改造为标准技能算法或 Agent 工程师负责路由和提示词的持续调优。如果你是在一个比较小的团队里单打独斗那么至少要把自己拆成这三个角色来思考。做注册中心时站在平台工程师的角度写技能描述时站在业务运营的角度调路由准确率时站在算法工程师的角度。这种“人格分裂”式的思考方式确实能帮你少走很多弯路。最后分享一个小技巧我每次新增一个技能之前会强制自己先写三条“用户可能会问但我不应该用这个技能回复”的请求然后把它放到技能描述的反向提示里。这个习惯坚持下来之后技能路由准确率提升得非常明显。大家不妨也试试不一定一步到位但至少可以先挑一个技能权重高的场景去跑通整套链路然后再逐步扩展。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →