news 2026/10/7 1:52:44

Agent技能库设计:从Prompt失控到可控技能编排的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能库设计:从Prompt失控到可控技能编排的实战指南

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_factory=list) 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的失控和混乱困扰,不妨先从一个最简单的注册器加两三个技能开始,跑通链路后再逐步扩展。这个方向,我是试过之后确信值得走下去的。

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

STM32F103入门实战:从开发板认识、环境搭建到烧录调试全流程

1. 准备工作:先把开发板和工具认清楚做嵌入式开发这几年,我最大的感受是:许多新手倒在起跑线上,不是因为代码写不出来,而是因为开发环境没搭好,或者板子都没认清就开始写代码,最后连程序烧不进去…

作者头像 李华
网站建设 2026/10/7 1:47:01

Agent-Reach实战:让智能体从“能聊”到“能用”的完整指南

我去年在一家公司做内部知识库问答的Agent项目,模型本身选得不错,各个模块的prompt也调得挺顺,结果一上生产就卡住了——Agent什么都答得头头是道,但一问“这个月的账单数据是多少”“帮我拉一下昨天的CRM客户名单”,它…

作者头像 李华