我做的这个agent-skills,本质上是一套给AI Agent编排和复用技能的系统。很多人一上来就把它当成一个简单的"工具函数集合",结果做着做着就变成了维护一坨越来越难用的API清单。这篇我就从技能系统的设计初衷讲起,拆解一个能真正支撑复杂Agent场景的技能库,到底应该长什么样、踩过哪些坑、落地时怎么一步步做出来。
1. 为什么需要一套专门的技能系统
1.1 LLM的能力边界决定了技能系统必须存在
聊到AI Agent的技能系统,先别急着写代码。你对大模型的期待再高,它本质上还是一个"没有手脚"的推理器。你没办法让模型直接去查数据库、写文件、调外部API,更没法让它计算一个需要严格步骤才能跑通的数据任务。大模型能做到的,是在理解需求后,通过一套机制去触发外部工具来完成实际动作。这套机制的载体,就是技能系统。
我自己最早做过一批只用Prompt + Function Calling的Agent,效果短平快,但一到稍微复杂的场景就暴露问题:技能多了,模型开始搞混;返回结构不统一,代码里到处是特判;新加的技能模型总是不主动用,日志里全是"我看看能不能调用……然后失败"。后来想明白了一件事——问题不在模型,而在我的技能根本没有被当成一个系统来管理。它需要命名规范、描述规范、参数Schema、安全边界、可观测性,以及明确的注册和发现机制。把这些补齐,Agent的可靠性才会真正上去。
1.2 从单机函数到可编配技能库的变化
做过传统后端开发的人,把技能理解成"一组函数",其实没什么问题。但一个合格技能库,不只是"把函数包起来"这么简单。它更像是一个可编配的执行单元仓库,在函数之上,还包含角色定位、调用条件、参数约束、资源限额、审计日志等一层信息。同一个函数在不同的Agent场景里,表现可能完全不同。
例如一个"查询库存"的函数,在客服Agent里需要的是简洁语义,在供应链Agent里可能要给足并发限制与耗时上下文。一个好技能库能做到"同一份底层能力,不同维度包装",让上层Agent按需引用。
我当时设计技能库时,最先确定的几个目标:
- 技能的注册必须是声明式的,新增技能不改Agent主流程代码。
- 技能的元信息能动态生成清单,喂给大模型做选择,而不是靠人肉维护。
- 技能的执行要带边界,包括超时、并发、权限、输出过滤。
- 技能的调用要有审计,出了问题能追责、能复盘。
这四个目标,对应到工程实现上,就是一套能够贯穿定义、注册、调度、执行、审计的技能生命周期管理系统。
2. 技能系统的整体设计思路
2.1 一条完整的技能调用链路
在设计技能系统时,一定要从调用链路去理解,而不是从数据模型入手。一次标准的技能调用,链路大概是:
- Agent收到用户请求,结合当前会话上下文,分析目标。
- Agent从注册中心获取"当前可用技能清单",对技能做出选择。
- Agent根据用户意图,结合技能的参数Schema,生成调用参数。
- 调用请求被提交到技能执行运行时,例如HTTP网关或进程内调用。
- 运行时执行参数校验、权限校验、配额校验。
- 技能逻辑执行,拿到结果,并通过结构化输出返回。
- Agent将结果转译成人话,回复给用户,并保存审计日志。
这七步里,每一步都可能成为可靠性黑洞。比如第2步技能清单做得不好,模型很容易选错技能;第3步参数Schema做不好,模型容易生成非法参数;第5步不做权限校验,危险操作可能直接被放行。
所以一个技能系统,不是写好执行函数就完了,它的核心功夫集中在"清单怎么生成、参数怎么定义、校验怎么做、边界怎么控制"这四件事上。这也是整个项目里我最花时间的部分。
2.2 技能的定义与生命周期
我采用了一个非常直接的思路:把每一个技能都定义为一个"描述文件 + 执行入口"的组合。描述文件负责"让大模型和开发者理解这个技能",执行入口负责"真正干活"。两者通过统一的注册器绑定。
生命周期包括:开发调试、注册上线、灰度验证、全量发布、下线归档。这个生命周期里最容易被忽略的是灰度验证。我见过很多团队,技能一写就上线,结果模型调用模型误判率极高,生成的参数方向性错误。正确做法是先让技能在小范围Agent场景里跑几天,把调用日志拉出来,看选择率、失败率、参数校验通过率,再决定是否全量开放。
技能一旦下线,不要立刻物理删除。保留历史版本,这样Agent会话追溯时,才能查出现在这条记录对应的到底是谁、哪个版本的操作。
2.3 技能的注册与发现机制
技能注册与发现机制,在单机Agent里看起来无关紧要,但如果你的Agent同时服务多个业务方,或者同时运行多个场景,这就成了决定系统可扩展性的核心。更复杂一点的系统,还会引入技能分组、标签、版本、权限绑定。比如"客服运营技能组"能看到查询类技能,但看不到"删除用户数据"这类高危技能。这种能力,靠的是基于标签的可见性控制,而不是硬编码。
具体实现上,我用一套"注册中心 + 技能描述索引"的机制。技能上线后,自动进入索引;Agent每次会话开始时,根据角色和业务场景,动态拉取可见的技能集,再配合缓存降低开销。缓存的更新需要接近实时,否则新技能上线后模型迟迟"看不到",也算是一类隐蔽的故障。
3. 核心元信息与技能描述规范
3.1 描述文案:写给LLM看的说明书
技能描述是这里最容易翻车也最容易忽视的细节。很多人写技能描述时,凭直觉写得又短又随意。例如"查询天气"这个技能,简单写成"查询天气",大模型拿到这个描述,大概率不知道什么场景下应该调用它,也不敢在用户说"今天要不要带伞"的时候主动触发。
一个合格的技能描述,至少要包含三层信息:
- 功能边界:这个技能到底能做什么、不能做什么。
- 适用场景:什么情况下应该用这个技能,举一两个具体例子。
- 调用注意:有什么参数约束,比如时间格式、地点格式、单位要求。
举个例子,把"查询天气"改成这样:
查询指定城市在指定日期的实时天气和未来预报,包括温度、降水概率、风速。用户询问"今天适不适合出门""会不会下雨""明天多少度"等问题时使用。城市名必须使用标准中文名称,日期使用YYYY-MM-DD格式。若用户未指定日期,默认查询今天。
这样的描述,模型才能精准命中调用场景。我甚至建议把每个技能的描述文案当作"产品文案"来写,反复测试模型在不同提问方式下的调用率。
3.2 参数Schema:动参和静参分离
技能参数Schema的设计,直接影响模型能不能生成合法调用。最容易犯的错误是:把静态属性误当成动态参数。例如一个"发送通知"技能,通知渠道、通知模板ID其实是系统配置,不该每次由模型生成。正确的做法是把这类字段从参数Schema中拿掉,改为在技能注册时通过"运行时上下文"注入。
动参和静参分离之后,模型需要生成的参数数量大幅减少,准确率显著提升。比如"发送通知"技能,模型只需要生成"接收人ID"和"通知内容",渠道、模板、签名这些由系统侧在运行时自动追加。这种做法,和人类员工入职时配好工位、门禁卡、系统权限,日常工作中只需要干细活,是一个道理。
另外,参数描述要带单位、带边界、带示例值。模型非常擅长"照着示例生成参数",这一点实测下来比任何自然语言描述都有效。
3.3 技能分组与管理字段
技能一旦超过20个,光靠一个平台列表已经很难维护。我建议每个技能元信息里,至少要包含这些字段:
| 字段 | 作用 |
|---|---|
| skill_id | 全局唯一ID,用于日志追踪和调用引用 |
| name | 技能名称,简洁、语义化,方便模型识别 |
| description | 给LLM看的说明书,重点描写适用场景 |
| parameters | JSON Schema,描述参数约束 |
| tags | 标签,用于按场景过滤和分组 |
| visibility | 可见性范围,例如"全部"、"指定Agent角色" |
| timeout_ms | 超时上限,按技能的耗时特性设定 |
| allowed_roles | 允许调用该技能的角色列表 |
| version | 语义化版本,灰度发布时使用 |
| owner | 负责人信息,出问题时找到人 |
这些字段看似简单,但缺一个,后期运维就会乱套。尤其是visibility和allowed_roles,在多人协作、多Agent场景下,是用来防止"技能被乱调"的第一道防线。
4. 技能执行与安全沙箱设计
4.1 能力总线与权限边界
技能执行环境不是"裸奔"的函数执行。每一个技能在系统内部,都运行在一个沙箱化的执行环境里。这个沙箱不一定是虚拟机级别的强隔离,但至少要有这几层约束:
- 资源限制:单次技能执行的CPU时间、内存上限、网络访问白名单。
- 数据边界:技能能读写的目录、数据库表、外部API域名,必须通过配置声明。
- 操作边界:涉及删除、修改、转账、推送等危险操作,必须有二次审批或角色管控。
- 审计留痕:每次执行的输入、输出、耗时、调用方,必须落日志和审计系统。
没有这些边界,技能做得越多,系统意外越大。比如一个"发送邮件"技能,如果没有操作边界,模型被诱导后可能批量发送垃圾内容。我见过不止一个团队,把技能权限设计成"所有Agent都能调用所有技能",结果线上出了安全事件,这才意识到"权限最小化"是底线。
4.2 超时、幂等、重试
技能执行过程的三个核心工程要素是:超时、幂等、重试。这三件事做不好,技能系统在故障面前会非常脆弱。
超时的核心原则是"宁可短一些,不要长到不可控"。一个查询类技能,默认3-5秒超时足够;一个生成类技能,可能需要30秒甚至更长。给技能设置超时,是为了防止"依赖的外部服务挂了,Agent跟着卡死"。我给技能设计了一套超时双保险:技能声明自己的期望超时,执行网关还有一个全局兜底超时,防止个别技能把全局资源拖垮。
幂等性,在技能系统中往往被人忽视。想想看:如果你调用了一个"创建订单"技能,由于网络闪断,Agent重试了一次,结果创建出两个订单,这锅算谁的?解决思路是引入全局幂等键。在Agent生成参数时,系统会自动为每次会话的"用户意图快照"生成一个idempotency_key,技能执行前先查重,命中则直接返回上一次结果。
重试策略一定要配合幂等使用。处理方式分三类:
- 查询类技能,可以自动重试2-3次,指数退避。
- 写操作类技能,默认不自动重试,提示用户确认后再发起。
- 外部API调用类技能,重试时必须携带幂等请求头。
这些机制,在我做的技能执行网关里都是一等公民,而不是附加功能。
4.3 输出的结构化与错误码
技能执行的结果,如果只是返回一段文本,或者直接抛异常,那么Agent的上层逻辑会很难办。我建议所有技能的输出,统一收敛到一个结构里:
{ "status": "success", "data": {...}, "error": { "code": "SKILL_NOT_AVAILABLE", "message": "技能暂不可用,请稍后重试" }, "trace_id": "abc123" }error在成功时可以置空,失败时则必须有结构化的错误码。错误码编码规则建议按模块划分,比如PARAM_INVALID、EXTERNAL_API_TIMEOUT、PERMISSION_DENIED。这样Agent在拿到错误后,可以基于错误码做决策,比如"参数非法就换一种说法再试一次",但"权限不足就不能硬重试"。
我的实操经验是:错误信息里一定要有面向用户的描述和面向开发者的描述两个字段,而不是一锅烩。模型读到简短的用户描述时,能自然地转译成人话,而不是直接把一长串内部异常抛给用户。
5. 实操:一个技能库的落地过程
5.1 技能注册核心代码
我选用了Python作为技能库的主实现语言,原因是Agent生态里Python的库和工具最成熟,团队里协作成本也低。下面这层"技能注册器"是入口:
# skill_registry.py from dataclasses import dataclass, field from typing import Callable, Any, Dict, Optional @dataclass class Skill: skill_id: str name: str description: str parameters: Dict[str, Any] # JSON Schema tags: list[str] = field(default_factory=list) visibility: str = "all" # 例如 all、internal、confidential timeout_ms: int = 3000 allowed_roles: list[str] = field(default_factory=list) version: str = "1.0.0" owner: str = "" handler: Optional[Callable[..., Any]] = None idempotency: bool = True class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] = {} self._tag_index: Dict[str, list[str]] = {} def register(self, skill: Skill): if not skill.skill_id or not skill.handler: raise ValueError("skill_id 和 handler 不能为空") self._skills[skill.skill_id] = skill for tag in skill.tags: self._tag_index.setdefault(tag, []).append(skill.skill_id) def get_visible_skills(self, role: str, tags: Optional[list[str]] = None) -> list[Skill]: result = [] for skill in self._skills.values(): if skill.allowed_roles and role not in skill.allowed_roles: continue if tags and not any(t in skill.tags for t in tags): continue result.append(skill) return result def generate_manifest(self, role: str) -> list[dict]: skills = self.get_visible_skills(role) manifest = [] for skill in skills: manifest.append({ "skill_id": skill.skill_id, "name": skill.name, "description": skill.description, "parameters": skill.parameters }) return manifest这里核心是generate_manifest,它会根据角色和标签动态生成给模型看的技能清单。模型中集成时,我就把这份清单注入到System Prompt里,格式长这样:
Available skills: - [查询天气] skill_id=weather_query : 根据城市与日期查询天气,适用于户外安排、出行决策等场景。 - [发送邮件] skill_id=email_send : 给指定用户发送邮件,注意内容需合规、不使用敏感词。模型看到的是"语义化描述",而不是一堆函数签名。这个差异,直接决定模型调用技能的意愿和准确率。
5.2 技能清单生成与模型注入
技能清单注入是Agent效果好坏的关键环节。我第一次做时,直接把100多个技能的冗长描述全塞进System Prompt,结果模型在长上下文里注意力涣散,经常选错技能,而且Token开销巨大,每次请求光技能列表就消耗了4000多个Token。
后来我改成"动态路由 + 两阶段叫醒"策略。第一阶段,用一个轻量级意图分类器把用户请求划分到技能分组,例如"天气类""订单类""营销类""情感支持类"。第二阶段,只把当前分组相关的5-10个技能清单注入给模型。这样既能控制Token开销,又能大幅提高技能选中准确率。
动态路由的具体实现,可以先从基于关键词的规则开始,跑通之后再引入一个轻量分类模型。
# skill_dispatcher.py SKILL_GROUP_KEYWORDS = { "weather": ["天气", "下雨", "温度", "出门", "带伞", "预报"], "order": ["订单", "购买", "取消", "退款", "物流", "快递"], } def route_skill_group(question: str) -> str: for group, keywords in SKILL_GROUP_KEYWORDS.items(): for keyword in keywords: if keyword in question: return group return "general"规则看似简陋,但已经能解决大多数早期需求。之后再把规则替换成意图分类模型,整体的架构不需要变,这也是一种很好的渐进式演进思路。
5.3 严格执行环境与观测
技能执行网关是最需要工程严谨性的地方。核心功能包括鉴权、限流、超时、审计日志、幂等控制、链路追踪。这部分我只写了个精简版模块,重点是把整个执行链路里的关键痕迹埋好:
# skill_gateway.py import time import uuid from typing import Any class SkillGateway: def __init__(self, registry): self.registry = registry self.timeout_global_ms = 5000 def invoke(self, skill_id: str, parameters: dict, role: str): trace_id = str(uuid.uuid4()) skill = self.registry.get_visible_skill(skill_id, role) if skill is None: return {"status": "failed", "error": {"code": "SKILL_NOT_AVAILABLE", "message": "技能不可用"}} if not self._validate_parameters(skill, parameters): return {"status": "failed", "error": {"code": "PARAM_INVALID", "message": "参数校验失败"}} start = time.time() try: timeout = min(skill.timeout_ms, self.timeout_global_ms) result = self._execute_with_timeout(skill, parameters, timeout) return { "status": "success", "data": result, "trace_id": trace_id, "latency_ms": round((time.time() - start) * 1000, 2), } except TimeoutError: return {"status": "failed", "error": {"code": "TIMEOUT", "message": "技能执行超时"}, "trace_id": trace_id} except Exception as e: app.logger.error(f"技能执行失败: {skill_id}, trace_id={trace_id}, error={e}") return {"status": "failed", "error": {"code": "INTERNAL_ERROR", "message": "内部错误"}, "trace_id": trace_id}这个网关看起来简单,但它决定了你的Agent在故障时是"李逵"还是"李鬼"。执行超时、异常兜底、TraceID追踪,这三样是技能系统可靠性的三根支柱。日志输出时,一定要把trace_id贯穿到Agent会话层、技能网关层、外部服务调用层,否则前面所有设计都会在排障时显得苍白无力。
6. 常见问题与排查技巧实录
6.1 模型就是记不住这个技能
这是我被问得最多的一类问题。新技能明明注册了,清单里也生成了,但模型就是视而不见,怎么问都走默认回复路径。
排查优先级依次是:
- 清单是否真的生成到了模型上下文里?先抓一次完整的请求,确认System Prompt里是否包含这个技能。经常有同学改了注册表但没触发缓存刷新,模型读的其实是旧清单。
- 技能描述是否包含触发场景?描述里没有"用户说xx时使用",模型就很难判断该不该用。
- 技能是否是"手动技能"?如果执行网关限制了调用角色,模型调用后会收到权限不足的错误,次数多了模型宁可不用。
- 技能数量是否过多?一次注入超过20个技能,模型的选择准确率会断崖式下降。此时需要动态路由加持。
- 调用的成本?模型确实更倾向于简洁的响应路径,技能描述如果看起来比默认回答还复杂,模型也可能回避。
根据我的经验,超过一半的"记不住"其实是上下文里没有、或描述得太模糊,而不是模型能力问题。
6.2 参数传错、幻觉参数
模型生成调用参数时,偶尔会给出一些和技能无关的字段,或者在日期、数字上幻觉出错误值。这个问题的根源往往在于参数Schema设计得不够好。
解决办法有四个方向:
- 精简参数:只保留Agent需要生成的必要参数,其余全部后端注入。
- 示例强化:在参数描述里加一个
"examples"字段,模型中注入时附带1-2个典型示例。 - 强制校验:网关层对参数做严格校验,非法参数直接返回
PARAM_INVALID,并提示模型"参数不合法,请重新按规范生成一次"。 - 输出钳制:利用工具调用的底层校验能力,直接在结构化输出层拦截不符合Schema的参数。
线上实测下来,做完这四点后,参数非法率能下降约七成。尤其"精简参数"这一条,效果最立竿见影。
6.3 多个Agent并发调用的问题
当同一套技能系统服务多个Agent的时候,并发问题就会浮出水面。典型场景是:两个Agent同时在同一个外部服务上执行订单操作,互相覆盖状态;或者一个Agent在跑耗时的数据任务,另一个Agent又发起了同技能调用,导致资源争抢。
我的处理策略是给技能接口引入分布式锁 + 队列配额。分布式锁保证同一技能的并发粒度可控,例如"同一个订单ID的修改,同一时刻只允许一个Agent操作";队列配额则是给不同Agent角色分配不同的调用配额,避免某个暴力调用拖垮资源。
再有一个容易被忽略的坑,就是缓存。技能执行结果如果带缓存,缓存键必须把参数全部纳入,否则就是"A查了上海天气,B问北京天气,结果返回上海的数据"。
6.4 调用成功但结果不对
有一类问题特别隐蔽:技能逻辑本身没报错,返回结果也结构化,但和用户问的完全不搭。这通常不是技能的问题,而是Agent的问题——它在技能选择阶段就选错了方向。
例如用户说"帮我看看这个订单到哪了",模型却调了"查询天气"技能,结果天气API返回成功,Agent基于这个无关结果硬编了一段回答。这种情况,在调试时一定要去查完整的会话链路,看重路由阶段的分组是什么,以及模型选择该技能的置信度分数。如果经常出现选择漂移,说明技能描述之间有歧义,需要把两个技能的功能边界写得更加互斥。我通常会在技能描述末尾加一句"该技能不能用于XXX场景",这种反向排除法极其有效。
7. 技能库扩展与维护建议
技能库不是一次性搭完就结束的系统。它能用好用坏、能坚持多久,很大程度上取决于是否建立了良好的维护机制。
新技能审核机制一定要有。每新增一个技能,必须经过三个人的检查:业务方确认逻辑正确、安全方确认权限无越界、技术方确认性能与超时设置合理。任何一环不过,就不允许上线。这个机制不是形式主义,是真的能挽回很多线上事故。
技能调用分析报表也建议定期盯。核心指标包括:
- 热门技能Top10,了解Agent的日常动作。
- 长期零调用技能,排查是描述问题还是功能价值问题。
- 技能失败率趋势,尤其关注超时率的变化。
- 参数校验失败率,过高说明技能描述对模型不友好。
我每个月都会出一份技能健康度报告。做得久了,你会对整个系统的免疫力有非常清晰的认识。
还有一条让我受益很多的建议:技能版本一定要和Agent版本解耦。技能升级时,不要强行要求所有Agent一起升级。用skill manifest的方式每个Agent自己声明依赖什么版本的技能,才能保证线上会话不因技能不兼容而中断。
关于技能系统的调优,我的体会是并不存在"一劳永逸的配置",每次Agent策略调整或业务方提出新需求时,都必须补一轮技能描述调优。模型的能力在变化,Agent的交互方式在变化,技能库自然也要跟着演进。把技能库当成一个活的、持续迭代的产品来运营,而不是一次性交付的工具箱,这套系统才会真正成为Agent规模化落地的底座。