AI Agent技能库设计:从工具调用到可编排的能力体系
最近在折腾个人项目时我一直在想一个问题为什么很多 Agent 项目 Demo 跑得飞起一旦真要塞进业务里就变得又脆又难维护后来我发现问题往往不是模型不够强而是我们压根没有把 Agent 的“手艺活儿”——也就是技能——当成正经工程来做。agent-skills就是这样一个切入点围绕 Agent 的技能库设计、注册、调度与迭代把零散的工具调用整理成一套可复用、可观测、可演进的方法体系。这篇文章我会从设计思路拆起结合一个具体的技能实现案例聊清楚技能接口规范、描述文件怎么写、调度策略怎么定、错误与降级怎么设计最后再分享一些踩坑后的排查技巧。如果你正在做 AI Agent 落地、工具调用封装或者只是想把一个会调 API 的 Demo 变成能稳定交付的系统这篇内容应该对你有用。1. 内容整体设计与思路拆解1.1 为什么把“技能”拎出来单独做先聊一个很实际的现象大多数人刚开始做 Agent都是直接在 Prompt 里堆功能描述或者干脆把一堆函数一股脑塞给模型。结果模型经常分不清边界一会儿调错了工具一会儿又拿着鸡毛当令箭。我之前见过一个内部工具所有操作全写在一个超级函数里参数列表长到要翻三屏每次改需求都像拆弹。把技能单独抽出来本质上是在做“能力封装 边界治理”。技能不是简单的函数而是一个可以被模型理解、被系统调度、被业务复用的最小能力单元。每个技能都有自己的名字、描述、参数约束、执行逻辑、返回格式甚至故障预案。这样模型不用去猜“这函数到底干不干这活”系统也能按图索骥知道哪些能力是可控的、可观测的。从工程角度看技能化的好处立竿见影第一每个技能可以独立开发、独立测试、独立上版本不会出现改一处崩全盘的情况第二能力可编排Agent 可以根据任务目标动态组合同一技能库里的多个技能而不是每次都写死逻辑第三可观测性大大提升每次调用都有清晰边界哪一步出错、为什么出错日志一查就能定位不用对着模型输出猜谜。1.2 技能设计与工具调用的本质区别很多人会把“技能”等同于“工具调用”这个理解不算错但不够全。工具调用更偏底层解决的是“模型如何触发一个函数”技能则要解决“模型应该何时、为何、以什么参数触发这个函数”。换句话说工具是胳膊技能是带脑子指挥的胳膊。举个直白的例子一个天气查询 API 是工具但如果只把 API 暴露给模型模型很可能不知道什么时候该用、结果怎么解读。而一个“从文本中提取地理位置并查询实时天气”的技能内部封装了地点解析、API 调用、结果过滤还带了一套“如果查询失败就返回模糊天气提示”的降级策略。模型看到的是一个语义完整、边界清晰的技能描述而不是一堆裸函数。所以我的设计原则有三条单一职责、自描述、容错兜底。每条技能只做一件事技能描述要能让模型一看就懂“我能用你干什么、怎么用最合适”所有可能失败的地方都必须在技能内部或者调度层给出降级方案。这个设计看起来朴素实际落地时能挡住 80% 的线上翻车事件。1.3 适合的应用场景与目标读者这套思路适用的场景覆盖面很广私域知识库问答里的检索增强、CRM 系统的自动跟进与数据分析、智能工单处理、个人助理类的多步操作、甚至一些工业控制里的指令解析都可以套用。核心不在于业务多酷而在于你是否有“一堆可复用的原子能力 一个需要理解自然语言的调度中枢”的组合体。目标读者我觉得有三类第一类是刚入门想给 LLM 应用加技能体系的开发者第二类是已经在做 Agent 但被“非结构化工具调用”搞到头大的工程师第三类是产品经理或技术负责人想搞清楚技能库规范化能带来什么实际收益。就算你只做前端、算法这篇文章里关于边界设计和错误处理的思想对你设计通用模块也照样有用。2. 核心细节解析与实操要点2.1 技能描述文件让模型一眼看懂的关键技能库能不能好用很大程度上取决于“技能描述文件”写得好不好。这个文件不直接参与逻辑执行但它决定了模型能不能在关键时刻想起“还有这号能力”。我比较推荐的描述结构是 YAML 或 JSON包含五类字段技能名、概述、适用场景、参数说明、返回说明。概述必须一句话讲清“这技能是干什么的”用白话不要混术语。适用场景要举两三个典型问题帮模型建立“用户说这种话时就该调用我”的关联。参数说明这块特别关键需要写清楚每个参数的类型、取值范围、必填性、以及什么情况下该填什么。模型不是图灵完备的程序员它靠的是语义模式匹配所以把参数范例写具体很有用。比如一个“发送邮箱”的技能光写“收件人: string”远远不够最好写出“收件人: string, 必填, 格式为合法email, 从用户对话中提取, 不要自行编造”。这看起来啰嗦但实测下来能把参数幻觉率压下去一大截。返回说明也要提前设计好。明确告诉模型成功时拿到什么结构、失败时拿到什么错误码和错误信息。我通常在返回里附加一个 machine_readable 的 code 字段如OK/PARAM_ERROR/API_TIMEOUT这样调度层可以根据 code 做精准分支而不是去字符串匹配“抱歉”。别小看这个习惯它能让上层逻辑的健壮性提升一个量级。2.2 技能注册与发现机制技能库不是堆在文件夹里就完事了整个系统还需要一个“注册与发现”的中间层。你可以把它理解成一个技能目录所有技能启动时向目录中心登记调度器需要能力时去目录里按语义检索。叫我实现的话我会给每个技能建一个SkillMeta类里面存着技能名、版本、描述、标签、入参 JSON Schema、超时时间、幂等性标志。目录中心维护一个进程内索引同时定期把索引同步到向量数据库里。这样有两个好处一是进程内索引可以保证高频调用的低延迟二是向量索引能做语义召回当模型不确定选哪个技能时通过描述文本相关性补一手。注册这块比较容易忽略的是“版本管理”。技能升级时老版本不能立刻删要像 API 一样有兼容窗口。我在线上吃过亏一个数据查询技能改了返回字段名结果旧对话的上下文里还残留着老格式缓存模型一拿到就理解错位导致整个流程崩掉。后来我立了规矩技能发布新版本必须带deprecated_at字段旧版本至少再存活一个完整发布周期。2.3 技能接口规范与协议设计接口规范是整个技能库的骨架我强烈建议你用统一的协议去包一层而不是让每个技能各写各的。一个比较成熟的协议应当包含请求上下文、业务参数、可选的追踪 ID、超时设置响应里则包含状态码、业务数据、诊断信息、耗时统计。实际定义上可以参考类似 OpenAI function calling 的参数描述格式再包一层自己的业务对象。为什么非要套一层因为这样调度器可以用一套通用代码处理所有技能的结果成功就往下走失败就进兜底逻辑所有技能的行为模式完全一致。这就好比所有家电都用同一个三角插头虽然里面电压电流各不同但你只需要一种插座就能全部接通。为了保证安全技能调用前还要做“参数白名单校验”和“敏感操作确认”。凡是涉及外部副作用发消息、改数据、花钱的技能调度层必须要求技能标注confirm_required: true由用户确认后再真正执行。这个机制看起来挺笨的但能在 AI 犯糊涂的时候保住底线。2.4 技能分层与编排技能建多了以后自然会出现层次关系。基础技能是原子操作比如“查天气”“发邮件”“读文件”复合技能是基础技能的有机组合比如“根据目标城市和出差时间安排会议”。高效的做法是不要让模型直接面对全部基础技能而是让上层复合技能把基础技能串成流程模型只需要决策“调用哪个复合技能”。这好像点菜你不必跟后厨每个厨师说“先切姜再爆香”你只需要告诉服务员点“鱼香肉丝”。这种分层设计能显著降低模型的决策难度也方便做权限治理前台客服 Agent 只暴露查询类技能运营 Agent 才可调用写操作技能。我这里再强调一个“编排即代码”的原则凡是成熟的、固定的流程别让模型临时推理直接在技能内部用代码编排留给模型的是那些需要语义理解才能拆分任务的场景。两相结合系统性能和稳定性都能兼顾模型也不会被过于庞大的技能列表沖昏头脑。3. 实操过程与核心环节实现3.1 搭建技能库基础骨架这部分我把一个迷你但完整的agent-skills实现思路写出来你在本地就能复现。整体目录结构我是这样设计的agent-skills/ ├── core/ │ ├── registry.py # 技能注册中心 │ ├── schema.py # 协议与数据模型 │ └── dispatcher.py # 调度器 ├── skills/ │ ├── weather.py # 示例技能天气查询 │ └── email_sender.py # 示例技能发邮件 ├── prompts/ │ ├── system_prompt.py # 系统提示词告诉模型技能规则 │ └── skill_descriptions.yaml # 技能描述目录 └── tests/ └── test_skills.py # 技能单测核心的registry.py我简化一下大致长这样class SkillRegistry: def __init__(self): self._skills {} def register(self, skill): meta skill.meta if meta.name in self._skills: raise RuntimeError(fduplicated skill: {meta.name}) self._skills[meta.name] skill def get(self, name): return self._skills.get(name) def list_skill_metas(self): return [s.meta.dict() for s in self._skills.values()] def semantic_search(self, query, top_k5): # 这里可以接向量检索先用关键词粗筛做演示 scored [] for skill in self._skills.values(): score self._calc_score(query, skill.meta) scored.append((score, skill)) scored.sort(keylambda x: x[0], reverseTrue) return [skill for _, skill in scored[:top_k]]每个技能类必须实现meta属性和execute()方法。为了保证统一我的meta用 Pydantic 定义class SkillMeta(BaseModel): name: str version: str description: str tags: list[str] [] parameters: dict {} confirm_required: bool False deprecated_at: Optional[str] None timeout: float 10.0这样做的价值在于所有技能都以同一种方式暴露元数据注册中心不用关心具体类型调度器只需要按协议调用完全解耦。3.2 实现一个技能从描述到执行我拿“天气查询”技能来示范。这个技能要做的事是从用户的话里抽城市名查天气最后把结果转成模型容易理解的摘要。技能描述在 YAML 里是这么写的name: weather_query version: 1.0.0 description: 根据城市名称查询实时天气信息适合用户询问某地当前温度、天气状况时使用 tags: [weather, query] parameters: city: type: string required: true description: 城市名如“北京”“上海”需要从对话中准确提取 forecast_days: type: integer required: false default: 0 description: 未来预报天数如果用户问“未来三天”则为 3否则为 0 returns: code: string data: object这个描述文件的价值体现得很明显模型就算不想看提示词里那一大堆规则光扫一眼 YAML 也能弄清楚调用方式。然后实现类这样写class WeatherSkill: meta SkillMeta.parse_obj(_YAML_CONTENT) async def execute(self, params: dict, context: dict): city params.get(city) if not city or not isinstance(city, str): return {code: PARAM_ERROR, message: 城市名称不能为空} try: weather_data await self._fetch_weather(city) return {code: OK, data: weather_data} except TimeoutError: return {code: TIMEOUT, message: 查询超时已返回兜底数据} except Exception as e: return {code: INTERNAL_ERROR, message: str(e)}技能内部有完整的错误捕获和返回码规划外层调度器见到非OK的结果就知道不能继续依赖 data 做后续判断。这是技能能和业务真正集成的前提因为实际上模型返回的“我认为没问题”不可信只有技能返回值里的 code 才可信。再说一个容易被忽略的细节技能超时时间必须小于 Agent 整体一次回复的可用时长。我一般把技能超时调在 5 到 15 秒之间如果天气接口超过 8 秒还没回就立即返回降级结果。否则会把模型响应拖到“用户已经关掉页面”的程度。3.3 调度器当模型说“我要用天气技能”调度器连接模型和技能库大致的调用逻辑是先在系统 Prompt 里注入所有技能描述再让模型判断需要哪个技能并输出结构化调用意图接着调度器校验参数执行技能把返回结果拼成新的上下文反馈给模型让模型基于真实数据组织回答。class Dispatcher: def __init__(self, registry: SkillRegistry, llm_func): self.registry registry self.llm llm_func async def run(self, user_input: str): skills_desc self._format_meta(self.registry.list_skill_metas()) intent await self.llm.ask_for_intent(user_input, skills_desc) if not intent or intent.get(skill) is None: return await self.llm.answer_without_skill(user_input) skill self.registry.get(intent[skill]) if not skill: return {code: SKILL_NOT_FOUND} validate_result self._validate_params(skill.meta, intent.get(arguments, {})) if not validate_result.ok: return await self.llm.reask(user_input, validate_result.error_hint) skill_result await skill.execute(validate_result.params, context{}) return await self.llm.answer_with_skill_result(user_input, skill_result)这个调度流程看着简单实战里最大的挑战是“模型选错技能”和“参数填错”。我除了在描述文件里下功夫还会在系统 Prompt 里加一句硬规则“如果你无法确定用户意图匹配哪个技能请直接向用户确认不要猜测。”这一句话能把错误率降不少因为模型在不确定时更容易选择“安全行为”而不是硬挑一个技能。3.4 给模型的安全兜底链路安全兜底要分两层做。第一层是在调用前调度器按技能 meta 里的confirm_required判断是否走用户确认环节。确认不是简单的 yes/no而是要把关键动作、关键参数直接呈现比如“将向 testexample.com 发送一封主题为‘会议邀请’的邮件确认发送吗”确认以后才真正调用。第二层是结果出来后如果code异常但对话还必须继续调度器可以把错误信息直接塞给模型并追加一句“基于以上错误提示请向用户礼貌解释并提供替代方案”。这能让 Agent 在技能挂掉时仍然保持高情商而不是甩出一段 Python 堆栈。我的习惯是每个技能都写明“fallback 提示语”例如天气查询失败就回退到“暂时无法获取实时天气我为你播报昨天同期数据仅供参考”。这不是复杂的技术但能把失败体验从“系统坏了”变成“试试其他方式”。好产品和小 Demo 的差距往往就在这些边界细节里。3.5 测试技能像测函数一样测技能技能可以理解为给模型用的“函数”所以必须像函数一样去测试。我给技能库安排三层测试单测、语义测试、场景回放。单测直接调用技能类验证不同参数时返回码和数据结构是否正确语义测试用一批真实用户句子跑通“模型选技能 → 调度器执行 → 模型回答”链路检查准确率场景回放则是把线上日志重新输入系统保证升级后行为不回归。单测部分我特别关注“异常输入”比如参数类型乱给、city 传了一个空字符串、并发重复调用等问题。有些技能不是幂等的我在 meta 里标idempotent: false然后在调度层加 request_id 去重防止用户点重或模型重复调用造成重复扣款、重复发信。这类问题测试期看不出来一上量就会炸。语义测试要特别关注“相似意图的区分度”。比如“查天气”和“找攻略”都是旅游场景下的高频技能用户如果说“北京适合穿什么”它能被正确路由到天气而不是攻略技能。如果测试准确率低于 90%我建议不要急着上线先把描述文件里的话术调得更贴近用户真实表达再不行就在系统提示词里加一次“意图粗分类”前置步骤。4. 常见问题与排查技巧实录4.1 模型总是选错技能这是最常遇到的问题。现象是描述文件明明写了“该技能用于查询天气”用户问“今天该不该带伞”模型却掉头去调了“地图导航”。我排查的思路一般分成三步。第一步检查描述文件的表达是否和用户自然语言有交集“该不该带伞”在语义上确实不等于“天气查询”但如果描述里能提到“由降雨概率判断是否带伞”这个场景分支召回率就会明显提高。第二步检查技能数量是否太多模型面对 30 个以上的技能时注意力会被分散我会考虑加一级“分组路由”先让模型判断大方向再进到具体技能列表里去选。第三步实在不行就在提示词里增加 few-shot 示例把容易混的意图和正确技能对齐写清楚效果立竿见影。4.2 参数幻觉与参数遗漏模型经常自作主张地把不存在的参数填上去或者把用户没说清楚的字段留空。面对 ID 类的参数模型甚至可能按记忆编一个出来那个风险相当大。我现在的做法是两层校验兜底调度层先按 JSON Schema 强校验必填缺失或格式不合法直接返回错误并附上“请向用户询问缺失参数”的引导语。技能内部还要做业务校验比如用户ID是否存在、金额是否为正。双保险下参数类事故基本能堵住。此外一个很实用的小操作定义参数时尽量使用enum或带明确格式约束模型受到的约束越具体编造空间就越小。4.3 技能内部 API 超时与不稳定第三方 API 不稳定是常态技能层不能假装稳定。我现在每个外部依赖接口都做了三件事超时熔断、结果缓存、假数据兜底。超时熔断就是连续失败 N 次后短时间内直接走降级不再打爆上游结果缓存则是把最近一段时间的成功响应缓存起来比如天气数据 30 分钟内有效直接复用假数据兜底是针对“缓存里也没有”的场景返回一份预设好的示例数据同时在 message 里明确标注“数据异常仅供参考”。这样做的好处在于 Agent 不会因为一个旁路功能失败而整个卡死用户感知到的只是“这条信息可能不太准”而不是“机器人挂了”。在真实产品里让用户降低预期比让用户完全得不到响应要好得多。4.4 技能库膨胀后如何做路由优化技能数量一旦超过 30 个模型的选择准确率会直线下降。我的做法是引入两级路由先通过一个轻量分类 Prompt 或者向量检索把技能列表缩小到 top 5再让模型在这几个候选里做最终选择。好比是去一个很大的图书馆先让检索系统帮你把书架定位到 A3 区你再在 A3 区两三米范围内找书效率和准确率自然都上来了。另外我还会统计每个技能的实际调用频次把高频技能排在描述列表前面或者让它走直达通道低频但重要的技能单独放进“需要明确触发”的提示段落里。通过这层“热门排序”策略即使用户意图没那么明确系统也更容易命中真正常用的能力。4.5 调试技能库的日志规范技能库里的日志不能像普通后端日志那么简单因为它涉及“用户意图、技能决策、执行结果、模型回答”四个环节必须串成同一条 trace 才能快速复盘。我的做法是在调度器入口生成session_id所有流程共享这个 ID通过结构化日志打印用户原话、模型选中的技能、意图置信度、参数解析结果、返回码和耗时。复盘的时候最有用的是“意图与技能结果对比表”每次调用都记一行可以清晰看到某技能是不是频繁被选中却频繁失败。这类统计做长了以后你就能很客观地决定该优化技能描述、降级 API还是干脆把某个技能拆成两个。没有日志支撑的 Agent 系统优化基本靠猜有了日志你做的每次调整都能得到验证。5. 从技能到能力生态一点长期思考技能库不是一次性工程它更像企业内部的能力生态需要持续演化和迭代。今天你可能只有天气、邮件、日历三个技能但半年后可能会增长到上百个业务技能一定要从第一天就考虑好扩展性、权限边界和可观测性否则到了后期就得推倒重来。我个人在实践里一个比较大的体会是技能设计是“成本前置、回报后置”的事情。写一个技能可能一半时间花在描述和边界设计上真正调用逻辑只占另一半甚至更少。但回报会在后续每次接入新 Agent、每次业务方说“这个功能我们也要能自动做”的时候逐渐兑现。没有技能层的系统每次接新场景几乎都是重新开发有技能层的系统很多场景其实就是把已有技能重新编排一遍。最后再分享一个我的习惯每次给 Agent 增加新技能之前先把一句话写出来——“用户最可能在什么对话状态下想到这个技能”。如果这句话写不清楚那我大概率还没理解用户需求这时候我不会写代码而是先回去做用户访谈或者需求文档。技能描述写的其实是“用户的表达习惯”和“系统能力”的桥梁把这座桥搭稳了后续技术上的调度、路由、容错才有意义。