
1. 为什么Agent必须拥有自己的技能库1.1 从一次对话失控说起我在做客服场景的Agent时踩过一个大坑最初把查订单、退换货、改地址、开发票这些能力全部写进一个巨大的System Prompt让模型自己理解判断。功能一开始跑得很顺但随着业务方不断往里面塞规则Prompt膨胀到几千行结果就是同一个问题今天答对、明天答错模型开始在各种能力边界上疯狂试探。后来我们把这套逻辑彻底重构把每个能力封装成独立可注册的技能skill让Agent在收到用户请求后先做技能选择、再做参数填充、最后执行调用。这次重构之后系统才真正变得可控。这里说的agent-skills本质上是一种面向LLM应用的能力组织方式把Agent可执行的任务封装成一个个带元信息、带参数契约、带执行函数的独立模块再通过一个注册中心统一管理让模型在推理时按需发现和调用。它不是某个特定框架的专有概念更像是一种工程约定——只要能让你把模型思考和工具执行清晰解耦都可以叫技能。这篇文章适合正在做AI Agent落地、被模型乱调用工具或Prompt无限膨胀困扰的开发者。我会把技能系统的设计思路、最小实现、实测调优经验一起讲清楚没有太多花架子都是我亲手验证过的方案。1.2 单体Prompt与技能集的本质区别很多人问过我我直接用function calling不就行了为什么还要自己做一套技能体系我的理解是function calling解决的是模型如何输出结构化调用请求这一层问题它不管你的函数是怎么组织、怎么描述、怎么被发现的。而agent-skills解决的是更上游的问题——你如何让模型在几十个候选能力里快速、稳定地选出正确的那一个。对比一下两种做法维度单体Prompt 多函数技能化架构能力扩展改Prompt、改函数列表容易互相干扰新增一个技能文件注册即生效模型决策负担一次性看到所有函数定义和规则先看技能索引需要时再看详情故障影响一个函数报错可能污染整体输出技能独立执行异常可隔离上下文成本函数定义越多token消耗越大索引化加载按需展开复用性函数散落在prompt里无法跨项目技能包可导入导出跨Agent复用在实际运行中前者最大的问题不是模型不会选而是模型会选错。尤当函数数量超过20个、描述语义相近时模型很容易被相近的函数签名带偏。技能化架构通过给每个技能写一段高质量的自然语言描述并让模型先看目录再看详情大幅降低了选择难度。1.3 agent-skills要解决的三个真实问题第一个问题是能力可观测性。在单体Prompt里你很难回答这个Agent到底会做什么。但技能化之后一个技能注册表就是一份能力清单业务方对着清单提需求开发对着清单排优先级测试对着清单写用例整条链路都清晰了。第二个问题是故障隔离。一个技能挂了不应该影响其他技能。比如订单查询服务超时如果这段逻辑被写死在Prompt里模型可能整个会话都变得异常如果封装在skill里执行失败只需要返回一个当前不可用的错误结构Agent可以转而走替代方案。第三个问题是持续迭代的边界。业务需求永远在变今天要支持多语言明天要加价格对比。没有技能边界的时候每加一个需求都是在已有的Prompt沼泽里打补丁。有技能边界之后新需求就是新技能老技能一个字符都不用动。这一点在长期维护的项目里价值极大。2. 技能系统的架构定位与核心原语2.1 技能描述、入参与出参的规范化设计一个技能要做成什么样才能让模型稳定选用我经过大量测试后总结出一个四要素结构技能名、自然语言描述、参数Schema、执行函数。技能名建议用反向域名风格或项目前缀风格比如order.query_status、crm.customer.fetch避免不同模块的技能重名。命名时不要用过于抽象的词do_stuff这种名字等于没写。自然语言描述是最容易被低估的部分。模型并不理解你的函数内部逻辑它只知道描述文本。描述写得好不好直接决定技能命中率。我把描述拆成三个部分触发场景用户什么样的问题应该选这个技能尽量给用户原话示例功能说明这个技能做什么能返回什么边界条件什么情况下不该选它比如数据范围、时效限制举个例子。这是我从一个天气查询技能里提炼出来的描述模板当用户询问今天天气明天会不会下雨某地气温多少近期适合出行吗 等与天气状况、气温、降水概率相关的实时查询问题时调用本技能获取 指定城市和日期的天气数据。本技能仅支持国内地级市以上城市 不回答历史天气原因分析类问题。参数Schema我建议直接使用JSON Schema标准这个格式模型输出兼容性最好。每个参数都要写清楚类型、是否必填、枚举范围、默认值。我见过很多项目参数Schema写得极其随意结果模型生成的参数经常缺字段、错类型最后还得靠代码兜底反而更麻烦。出参格式同样要规范。我习惯约定所有技能最后都返回一个统一结构{ success: true, data: {...}, error: null, took_ms: 123 }这个结构乍看简单但能统一处理成功、失败、超时三种情况后续做落盘和重试也方便。2.2 技能注册器与路由分发机制技能注册器是这个系统里的总机。所有技能启动时调用register把自己登记进去注册器维护一张内存映射表供Agent按名字查找、按描述检索。注册器看起来很简单但有几个设计细节值得注意。第一个是支持覆盖与版本标记。同一个技能名被重复注册时注册器应该允许新版本覆盖旧版本同时保留旧版本的元信息方便回滚。第二个是延迟加载。不是所有技能都需要在进程启动时把执行函数载入内存对大体积的技能包比如加载机器学习模型注册器可以先登记元信息第一次被调用时才真正初始化。路由分发机制我会在后面的最小实现里给出代码示例这里先讲关键思路Agent收到用户请求后先根据问题做意图识别再通过技能索引表找到候选技能。这个过程本身也是由LLM驱动的——模型扮演一个调度员输入是技能目录输出是选中的技能名和参数。相比在Prompt里罗列所有技能这种目录按需展开的分发方式能大幅节省上下文token同时减少模型选择时的干扰。2.3 技能与工具调用、工作流的边界划分这三个概念非常容易被混为一谈我在团队内部定了一个划分标准工具Tool/Function原子能力只有一个动作。比如HTTP请求、读数据库、发送邮件。它没有业务语义也不关心上一个调用是什么。技能Skill面向任务的组合能力。它可以编排多个工具内部包含业务规则。比如查询订单退换货进度里面会先查订单、再查物流、再查售后状态。工作流Workflow面向完整业务链路的编排由多个技能按固定顺序或条件分支串起来。比如客户投诉处理流程先识别工单再分派客服再发送回执。这个边界意味着工具层保持轻薄稳定技能层承载业务变化工作流层处理跨场景协作。如果一开始就分不清很容易做出一个失控系统——技能里塞了完整业务流程工作流里又复制了技能逻辑最后改一处坏两处。3. 动手落地一套agent-skills的最小实现3.1 技能目录结构与元信息定义我通常用这样一个目录结构组织技能项目skills/ ├── registry.py ├── base.py ├── order/ │ ├── __init__.py │ ├── skill.json │ └── handler.py └── weather/ ├── __init__.py ├── skill.json └── handler.py每个技能包由skill.json和handler.py组成。skill.json声明元信息handler.py实现执行函数。这个结构的好处是技能包可以整体导出、导入换项目时复制目录即可。看一个skill.json的真实示例{ name: weather.query_today, description: 当用户询问今天天气、气温、降水概率、是否适合出行等实时天气问题时调用本技能查询指定城市当天的天气情况。支持国内地级市以上城市。, parameters: { type: object, properties: { city: {type: string, description: 城市名称如北京、上海}, date: {type: string, description: 日期格式YYYY-MM-DD默认当天} }, required: [city] }, timeout_ms: 3000, version: 1.2.0 }关于description我再多说一句请务必在里面写用户可能会怎么问而不是写函数怎么实现。模型是通过理解用户问题来选择技能的你写从数据库查询并返回到天气字段这种话完全没用模型不知道用户什么场景会触发它。要写成当用户询问……时调用这是模型最好理解的形式。3.2 注册器实现从登记到调用的完整链路下面是一个精简但完整的注册器实现基于Python的数据类完成import json import time from dataclasses import dataclass, field from typing import Any, Callable, Dict, Optional dataclass class Skill: name: str description: str parameters: dict handler: Callable[..., Any] version: str 1.0.0 timeout_ms: int 3000 tags: list field(default_factorylist) class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill, allow_override: bool False): if skill.name in self._skills and not allow_override: raise ValueError(f技能已存在: {skill.name}) self._skills[skill.name] skill def unregister(self, skill_name: str): self._skills.pop(skill_name, None) def get(self, skill_name: str) - Optional[Skill]: return self._skills.get(skill_name) def list_skills(self) - list: return [ { name: s.name, description: s.description, parameters: s.parameters } for s in self._skills.values() ] def execute(self, skill_name: str, params: dict) - dict: skill self._skills.get(skill_name) if not skill: return {success: False, data: None, error: skill_not_found, took_ms: 0} start time.time() try: result skill.handler(**params) elapsed int((time.time() - start) * 1000) return {success: True, data: result, error: None, took_ms: elapsed} except Exception as e: elapsed int((time.time() - start) * 1000) return {success: False, data: None, error: str(e), took_ms: elapsed}这个实现有三个要点。第一execute永远返回统一结构调用方不需要再try/except技能内部异常。第二list_skills输出的正是给Agent看的技能目录不暴露内部字段。第三注册时默认不允许覆盖防止多个技能包冲突时静默覆盖造成线上事故。3.3 让Agent自主选择技能的Prompt策略注册器只是骨架真正让Agent学会选用技能的是Prompt策略。我踩过好几次坑后才稳定下来目前用的是三级递进策略。第一级只给模型技能目录不给完整细节。目录形式是索引列表每个技能只有名称和一句话描述。此时模型的任务是判断哪个技能可能有用而不是直接输出调用参数。第二级模型选定技能后我们再从注册器拉取该技能的完整参数Schema连同用户原始问题一起给模型让它填充参数。这一步是关键的按需展开技能很多时也不会撑爆上下文。第三级把填充好的参数交给execute执行执行结果回传给模型作最终回答。这级可以让模型基于技能返回内容组织面向用户的自然语言回答。一个可参考的调度Prompt骨架如下你是一个技能调度员请根据用户问题从技能目录中选出一个最合适 的技能并输出JSON格式的选择结果 {skill: 技能名, reason: 简要说明选择理由} 技能目录 {skill_catalog} 如果所有技能都不合适请直接输出 {skill: null, reason: 当前技能无法处理}这个策略的最大收益在于模型从一次性阅读所有细节并决策变成逐级缩小范围决策链路上的信息噪音显著下降。实测中技能数量超过100个时这种递进策略的准确率远高于全量展示。4. 实测验证技能数量从10涨到100时发生了什么4.1 技能描述写得不好时模型真实表现是怎么样的我们团队曾经做过一组对照实验同样50个技能第一版描述全部是开发照着函数签名随便写的第二版按照触发场景功能说明边界条件重写。模拟用户问题200条第一版技能命中准确率只有67%很多请求被路由到语义相似的相邻技能第二版命中率提升到91%。这个差异让我意识到描述文本是技能系统里性价比最高的优化点。用错了技能比不调用技能更危险。比如用户问我的快递什么时候到如果系统错误调用了查询订单技能返回的是订单创建时间这就不是没有答案的问题而是给出错误答案的问题用户感知极差。我建议每个新技能上线前做一次对抗性测试——专门挑一些边界问题、相近意图问题、含糊表述问题去问看模型会不会选错。比如这个订单多久能退到钱这句话既涉及订单查询又涉及退款进度如果系统里这两个技能都存在描述必须能帮助模型区分。4.2 技能冲突与优先级同名、近义、重叠该怎么办随着技能增多冲突问题不可避免。主要有三类冲突同名冲突、近义冲突、领域重叠冲突。同名冲突最简单注册器里的allow_override参数就是用它兜底的原则业务上明确是迭代升级才允许覆盖否则直接报错。近义冲突麻烦一点。比如订单查询和订单纠纷查询从用户问题我的订单出问题了来看两个都很像。处理方案是给技能加priority字段在目录展示时让高优先级技能排在前面并在描述中尽量写明本技能不处理什么。比如纠纷查询的描述里加上仅限渠道投诉、退款纠纷类问题不处理普通订单状态查询此类请找订单查询技能。领域重叠冲突就属于架构层面的问题了。如果发现两个技能频繁同时出现在模型候选列表里且用户问题又总是落到中间地带这时不应该靠调描述硬分而应该考虑把两个技能合并成一个组合技能——内部按规则拆分支执行。我在处理订单和物流查询时就走了这条路先合为一个订单物流综合查询再在handler内部根据参数区分只查订单、只查物流、还是都查。4.3 故障隔离与回退策略技能挂了Agent不能跟着挂Agent系统有一个隐性问题技能依赖的下游服务总会有故障。如果任一个技能报错都会导致整个Agent对话失去逻辑那这个系统的可用性就无从谈起。我采用的故障隔离策略是三级回退。技能A执行失败后Agent先尝试技能A的降级参数比如超时不查实时数据改用缓存数据降级也失败则搜索是否有语义近似的替代技能B都没有再返回精心设计的兜底话术当前暂时无法获取该信息建议稍后重试或联系人工客服。这个流程体现在代码里就是一层执行包的封装def execute_with_fallback(agent, user_intent, primary_skill, params): result registry.execute(primary_skill, params) if result[success]: return result fallback_skills agent.suggest_fallback(user_intent, primary_skill) for candidate in fallback_skills: result registry.execute(candidate[name], candidate[params]) if result[success]: return result return default_neglect_response(user_intent)还有一个细节容易忽略执行失败后不能直接把裸错误信息返回给用户比如KeyError: amount。模型会把这种内部报错复述出来用户看到会一头雾水甚至担心系统脆红。我在一级回退之后、二级回退之前会先把错误码映射成一句业务可读的信息再交给模型组织话术。5. 这个方案在实际项目里的三个进阶问题5.1 技能多了之后索引本身该怎么管理当技能数量超过50个时给模型展示完整目录已经开始变得低效。token开销大、模型注意力分散、描述相互干扰。这时候我对技能做了分类索引先展示技能所属的领域分类订单域、物流域、售后域、账户域模型根据用户问题先选领域再在看领域内技能细目。这个做法其实有点类似传统软件里的二级路由只让模型做粗粒度判断细粒度判断用规则或代码来完成。比如用户说我要投诉快递领域分类层直接把候选范围缩小到物流域售后域模型再来二选一或三选一正确率明显更高。如果想再激进一点可以引入离线训练的分类器或向量检索作为粗筛器但我的建议是不要一上来就上重方案。先用分类规则的方式跑起来当数据积累到一定量、确实遇到瓶颈时再考虑。毕竟agent-skills的核心目标是让系统可控而不是最快。5.2 动态技能注册运行时热加载怎么做生产环境里业务方经常要求这个技能今天下午就要上。如果每次加技能都要重新部署那技能化架构的价值会大打折扣。我的做法是支持配置中心的动态加载——技能元信息放在配置中心新技能发布时只需上传一个新的技能包服务通过监听配置变更热加载注册器。热加载涉及的一个核心问题是原子性加载新技能的过程中不能让一次请求读到半初始化的状态。我的做法是用一个Snapshot对象保存注册表引用更新时先构建新注册表完成后替换引用保证所有请求在任意时刻看到的是完整旧表或完整新表。class DynamicRegistry: def __init__(self): self._snapshot SkillRegistry() def reload(self, skills: list): new_registry SkillRegistry() for s in skills: new_registry.register(s) self._snapshot new_registry property def current(self): return self._snapshot这段代码看起来平平无奇但self._snapshot new_registry这一步在Python里是引用赋值是原子的。线上实测热加载几百次没有出现过一次半更新状态。5.3 技能执行的链路追踪与成本核算技能化架构最后一个容易被忽视的问题是链路的可观测性。一次Agent对话可能触发三个技能每个技能内部调用多个外部接口一旦出了问题你怎么快速定位是哪个环节我的习惯是给每一次技能执行分配一个trace_id从外部请求进入Agent开始生成贯穿所有技能调用。每次execute时把trace_id带入日志记录技能名、入参摘要、出参状态、耗时、调用链。这个日志同时用于成本核算——每轮对话触发了多少技能、多少个外部调用、消耗了多少LLM token都能拆到具体技能包上。这么做还有一个额外收益技能使用频率数据会告诉你哪些技能几乎是废的哪些技能是高频主干。废技能可以考虑下线清理主干技能值得投入更多资源做降级和缓存。我做过的项目里数据分析之后砍掉了线上30%几乎没人调用的僵尸技能Agent平均响应时间提升了12%。这也是技能化架构对运营决策的一个正向反馈。从我个人的实践体会来说agent-skills不是某个框架的专属概念而是一种让AI应用在真实业务中活下去的工程手段。它真正解决的问题不是模型能不能调用工具而是一个不断变化的业务系统如何持续保持可控。如果你正在被Agent的失控和混乱困扰不妨先从一个最简单的注册器加两三个技能开始跑通链路后再逐步扩展。这个方向我是试过之后确信值得走下去的。