这两年做 AI 应用,我越来越确信一件事:同一个底座模型,在不同团队手里,效果差距可以拉到天壤之别。有人用一线大模型做出来的东西依然是“一问一答”的玩具,有人却用同样的模型拼出了能自动跑完整条业务线的智能体。差别往往不在模型本身,而在你怎么把模型能调用的能力组织成一个体系。这个体系,在最近一年的 AI 工程化实践里,慢慢收敛成了一个名字:Agent Skills。
Agent Skills 简单说,就是把大模型完成特定任务时需要的外部能力——查数据库、调接口、操作文件、执行代码、走审批流——统一封装成标准化、可复用、可被模型理解和调度的“技能单元”。它解决的不是“模型能不能生成一段话”,而是“模型怎么稳定地调用真实世界的工具,并且把任务闭环跑完”。这篇文章我想从概念、设计、落地到排障,完整讲一遍我自己在做 Agent 工程时的思路和踩坑记录,适合正在做智能客服、自动化流程、Copilot 类产品,或者想从“写 Prompt 调 API”升级到“做一整套 Agent 工程”的开发者参考。
1. Agent Skills 的本质,不是工具调用的换皮
1.1 技能与工具调用到底差在哪
很多人第一次接触 Agent Skills 时会问:这不就是 function calling 吗?把函数定义塞给模型,模型按 JSON 格式传参,然后执行函数返回结果。流程上确实很像,但技能和工具调用在工程定位上完全不是一回事。
工具调用(Tool Calling)本质是无状态的函数暴露,你给模型一个函数名、一段参数描述,模型生成参数,你执行,你把结果塞回上下文。它解决的是“让模型能按格式调函数”的问题。而 Agent Skills 解决的是“让模型能在复杂任务中正确选择和编排能力”的问题。技能单元里除了输入输出描述,还应该包含:
- 什么时候该用这个技能、什么时候不该用的判定规则;
- 输入参数的完整约束、校验逻辑和默认行为;
- 技能执行失败后的错误码、重试策略和降级路径;
- 技能内部是否有依赖其他技能或外部服务的声明;
- 技能执行结果如何格式化、摘要化,再回填给模型。
简单类比:工具调用是给你一把扳手,技能是给你一整套“拧这颗螺丝”的操作规程,包括用哪把扳手、用多大力、拧不动的备选方案。模型光有扳手不够,它需要知道在什么场景下选什么工具、怎么判断是否成功、失败了下一步怎么办。
1.2 技能的分层:原子、组合、工作流
我在实际项目里会习惯把技能分成三层,因为不同层级的技能在维护方式、调试难度和复用方式上差异很大。
第一层是原子技能(Atomic Skill),指单一、无状态、不可再拆的能力单元。典型例子是“查询实时天气”“读取文件内容”“调用某个内部 API”。原子技能的输入输出都尽量简单,执行时间短,失败原因明确。这一层是技能的基石,数量会随着业务扩展膨胀得很快,所以要格外重视命名规范和参数约束。
第二层是组合技能(Composite Skill),指串联多个原子技能才能完成的业务流程。典型例子是“根据用户收货地址推荐附近门店”,内部需要先调地址解析服务,再调门店库存接口,最后根据距离排序。组合技能的价值在于,把多步编排“封装成一次调用”,让模型不需要理解底层细节,只要表达意图即可。这样能显著降低模型在长链路中的出错概率。
第三层是工作流技能(Workflow Skill),指跨系统、跨角色的长流程任务,通常会包含人工审批、异步等待、状态持久化。典型例子是“发起一笔报销流程”“创建一条发布工单”。工作流技能一般需要额外的状态存储和任务调度支持,不能像前两层那样同步返回,往往要走异步回调或轮询。这一层最复杂,也最容易出问题,但如果做好了,恰恰是 Agent 从“聊天助手”升级成“数字员工”的关键。
1.3 为什么“技能化”是 Agent 落地的分水岭
我自己带团队做 Agent 项目时有个很明显的感受:没有技能化抽象之前,每个 Agent 都是“Prompt + 一堆工具函数”的快餐代码,复制粘贴严重,改动一个逻辑要全局搜索调用点。一旦技能化之后,能力边界、版本、测试、权限都变成了可管理对象。
技能化带来的第一个收益是可测试性。每个技能都能独立做单元测试,模型调度是另一套测试,两层解耦后定位问题快得多。第二个收益是可观测性。技能层可以统一埋点:谁在什么上下文下调用了什么技能、传了什么参数、返回了什么结果、耗时多少,全部有日志可查。第三个收益是可复用性。一个技能可以在多个 Agent 里共用,比如“查询订单状态”这个技能,客服 Agent 能用,售后 Agent 能用,运营数据分析 Agent 也能用。
所以我的结论很明确:如果你只是做个 Demo,直接 function calling 就够了;但如果你想把 Agent 推到生产环境,技能化是绕不开的一步。
2. 设计一套技能系统,选型和规范比写代码更重要
2.1 先分清两种范式:声明式技能与代码式技能
在做技能系统前,你先要决定采用哪种范式。我见过团队在这上面反复横跳,所以想单独拿出来说说。
声明式技能(Declarative Skill)的核心思路是:你只定义输入输出的 Schema、描述文本和校验规则,模型负责根据意图生成调用参数,系统负责执行并返回结果。它的优点是实现成本低、模型可控性强、逻辑透明,适合参数简单、结果能够被文本化的场景,比如查数据、算指标、发通知。
代码式技能(Code-based Skill)的核心思路是:技能本身就是一段可执行代码或插件,模型只负责表达“我想做什么”,执行器在内部承载完整逻辑,甚至内部可以继续调用其他技能或模型。它的优点是能处理复杂、非结构化的操作,比如操作浏览器、处理 Excel、写代码并调试。缺点是可解释性偏弱,调试困难,对执行环境的安全要求更高。
对比一下两种范式的适用情况:
| 维度 | 声明式技能 | 代码式技能 |
|---|---|---|
| 实现成本 | 低,写 Schema 和短逻辑即可 | 高,需要维护独立执行环境 |
| 模型参与度 | 高,模型主导参数生成 | 低,模型只传意图 |
| 适用场景 | 参数清晰、结果可结构化的任务 | 操作复杂、环境依赖多的任务 |
| 调试难度 | 较低,链路短 | 较高,链路长且状态多 |
| 典型代表 | 查天气、查订单、发消息 | 写文件、操作浏览器、跑脚本 |
实际项目中两者通常混用。我推荐的做法是:对外统一暴露“技能”概念,内部用不同执行器适配两种范式,底层框架不感知差异。这样模型侧规范统一,工程侧又能按需选择。
2.2 技能描述怎么写,模型才看得懂
技能描述是技能系统的灵魂。我见过很多团队花大量时间写代码,技能描述却只写一句“查询订单”,结果模型在真实对话里根本不知道该在什么时候用它,或者在可用技能变多后频繁选错。
技能描述至少要回答四个问题:
- 这个技能解决什么问题:用一两句话说明业务目标,不要写技术术语,要写模型能理解的用户意图;
- 什么时候必须用:列出触发条件,例如“当用户提到订单号或查询物流信息时”;
- 什么时候不要用:明确负面条件,例如“用户只是询问退货政策时不要调用订单查询”;
- 调用时需要注意什么:例如“如果订单号缺失,必须向用户确认后再调用”。
我习惯在技能描述里再加一段 few-shot 示例,给出用户话术到技能参数的映射样例。实测下来,增加两到三个示例,可以明显提升模型在真实场景下的参数抽取准确率。模型本质上是在做“文本匹配”任务,你给它看的示例越贴近真实分布,它的表现就越稳。
2.3 技能 Schema 的工程规范:参数、输出与依赖
技能 Schema 是技能的“API 契约”,既要给模型看,也要给执行器用。我建议用 JSON Schema 作为统一格式,因为模型对它的理解已经非常成熟,工程侧解析和校验的生态也很完整。
一个合格技能 Schema 至少包含名字、描述、输入参数定义、输出结构定义、错误码定义、依赖声明。参数定义里不建议只写类型,还要写清枚举值、默认值、格式约束和参数之间的联动关系。比如“寄件地址”和“收件地址”在某些业务里可以缺省,但“寄件时间”一旦填了就必须是未来时间,这些规则最好都写进 Schema,做二次校验时能过滤掉大量模型幻觉参数。
我整理了一个简单的“查询订单”技能 Schema 片段,只为了展示结构,实际项目中可以按需增删字段:
{ "skill_name": "query_order", "description": "查询用户订单的当前状态,包括商品名称、物流轨迹、预计送达时间。当用户询问订单状态、物流进展时使用。", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,通常是数字和字母组合", "minLength": 6, "maxLength": 32 }, "include_logistics": { "type": "boolean", "description": "是否返回物流轨迹明细,默认 false", "default": false } }, "required": ["order_id"] }, "output_schema": { "type": "object", "properties": { "status": { "type": "string", "enum": ["pending", "shipped", "delivered", "cancelled"] }, "items": { "type": "array", "items": { "type": "string" } }, "logistics_trace": { "type": "array", "items": { "type": "string" } } } }, "error_codes": ["ORDER_NOT_FOUND", "ORDER_ID_MISSING", "SERVICE_UNAVAILABLE"], "dependencies": ["order_service"] }不要小看这个 Schema,它同时承担了模型的调用约束、执行器的入参校验、调用方对返回结果的解析依据三重职责。Schema 不严谨,后续所有环节都会埋雷。
3. 实操落地:从零封装一个可复用的技能模块
3.1 最小化技能注册与加载框架
理论讲完,直接进入代码。我尽量保持代码精简,只突出核心结构,方便你迁移到自己的项目里。
技能模块的核心抽象我习惯定义为一个基类,每个具体技能继承并实现注册信息、执行逻辑、校验逻辑。Python 伪代码如下:
from abc import ABC, abstractmethod from typing import Any, Dict, Optional import json class BaseSkill(ABC): # 技能唯一标识 name: str = "" # 技能描述,给模型选技能用的 description: str = "" # 输入参数 JSON Schema input_schema: Dict[str, Any] = {} # 输出 JSON Schema output_schema: Dict[str, Any] = {} @abstractmethod def execute(self, params: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """执行技能核心逻辑,返回统一格式的结果字典""" pass def validate_input(self, params: Dict[str, Any]) -> Dict[str, Any]: """参数校验,基类提供默认实现,子类可覆盖补充""" # 实际项目中这里可以接入 jsonschema 库做严格校验 return params def to_skill_spec(self) -> Dict[str, Any]: """生成给模型看的技能规范,注册到模型配置里""" return { "name": self.name, "description": self.description, "input_schema": self.input_schema, "output_schema": self.output_schema, } class SkillRegistry: def __init__(self): self._skills: Dict[str, BaseSkill] = {} def register(self, skill: BaseSkill): if skill.name in self._skills: raise ValueError(f"Skill {skill.name} already registered") self._skills[skill.name] = skill def get(self, name: str) -> Optional[BaseSkill]: return self._skills.get(name) def all_specs(self) -> list[Dict[str, Any]]: return [skill.to_skill_spec() for skill in self._skills.values()] registry = SkillRegistry()这个框架看着简单,但它是整个技能系统的地基。注册表统一维护所有技能,模型侧配置一次全量技能描述,执行侧通过技能名拿到具体执行器。这样的好处是新增技能时,改动只落在独立的技能文件里,不会污染业务代码。
3.2 请求进入 Agent 后的调度链路
框架有了,接下来是调度链路。我一般把链路拆成五步:意图接收、技能匹配、参数抽取与校验、技能执行、结果回填。
意图接收阶段,模型先理解用户的话术,判断这是一个普通对话还是需要调用技能。技能匹配阶段,模型从注册表里的技能列表中选择最合适的候选,这一步本质是“技能检索 + 语义匹配”,当技能数量超过二十个时,我会额外引入一层关键词标签或者向量检索做预筛,把候选集缩小到三到五个,再交给模型精排。
参数抽取与校验阶段,模型生成 JSON 参数后,执行器先做格式校验和业务规则校验,校验不通过直接返回错误码,不要带着错误参数闯进执行逻辑。技能执行阶段走具体执行器,内部可能调用内部 API、查询数据库或者操作文件。结果回填阶段,把执行结果摘要化后拼装成模型可读的文本,再交给模型生成最终回复。
简化后的调度代码如下:
def handle_agent_request(user_message: str, registry: SkillRegistry, llm_interface): # step 1: 模型判断是否需要技能,并抽取参数 plan = llm_interface.run_with_tools( user_message=user_message, tools=registry.all_specs() ) if not plan.get("need_skill"): return llm_interface.chat(user_message) skill_name = plan["skill_name"] params = plan["params"] skill = registry.get(skill_name) if skill is None: return "抱歉,我暂时无法处理这个操作。" try: # step 2: 参数校验 validated_params = skill.validate_input(params) # step 3: 执行技能 result = skill.execute(validated_params, context={"user_message": user_message}) # step 4: 结果摘要回填 summary = summarize_result(result) return llm_interface.chat(f"技能执行结果摘要:{summary},请根据摘要继续回答用户。") except SkillExecutionError as e: # step 5: 失败处理 return handle_skill_error(e, llm_interface)这只是一个非常简化的骨架,但链路里最关键的一点是:技能执行的结果不要原封不动地塞回上下文,而是先做摘要化处理。理由很简单,如果技能返回的是几十行 JSON,模型在生成回复时会被大量无关字段干扰,既增加 Token 消耗,也容易答非所问。摘要化既能省钱又能提升回复质量。
3.3 上下文管理与技能记忆,别让模型“上一秒学会下一秒就忘”
技能系统的上下文管理,是生产环境中最容易翻车的地方。很多团队的 Agent 一跑长对话就“失忆”,不是模型不行,而是技能调用结果的存储和引用方式不对。
技能调用结果不应该只保存在对话历史里,还应该以结构化方式存入短期记忆。我具体做法是:每次技能执行成功后,把结果按技能名和参数做 Key-Value 缓存,同时把结果摘要写进对话上下文。当模型后续需要引用前面某次查询结果时,它可以直接从上下文摘要中拿,而不用重新触发技能。这样既降低 Token 消耗,也减少重复调用外部服务的压力。
缓存策略上要设 TTL,比如订单状态查询,五分钟内相同订单号直接命中缓存,超过五分钟则重新查询。另外,技能结果被引用后,如果用户更新了条件,旧缓存要及时失效。基础规则是“技能传入参不变且 TTL 未过期,才允许命中缓存”。
上下文里的技能调用历史也要做截断。对话超过一定轮数后,早期技能调用的详细参数可以丢弃,只保留“用户曾经查过订单 A”这种级别的摘要。模型不需要记得完整的订单 JSON,它只需要知道事实和上下文,细节需要时再调技能。
3.4 把技能做成插件层:版本化、灰度与权限
技能系统跑稳之后,下一个问题就是协作和发布。如果一个团队有多个 Agent 共用一个技能注册表,技能变更会直接影响所有调用方,这时就必须做版本化和隔离。
我的做法是技能注册表里每个技能都带版本号,Agent 配置里声明自己依赖的技能版本范围。发布新技能版本时,先在一个低流量 Agent 上灰度,观察错误率和请求耗时,稳定后再全量放开。版本回滚也要能做到秒级,最简单的方式是注册表里保留最近三个版本,切换只是改一个配置项。
权限隔离同样重要。不是所有技能都能给所有 Agent 调用,比如“删除用户数据”这种高风险技能,只允许内部管理 Agent 调用,而且每次调用都要二次确认。我通常会在技能 Schema 里增加allowed_roles或者permission_level字段,执行器调用外部服务前先做身份鉴权,防止模型被恶意提示词诱导去执行越权操作。
插件层的设计原则很简单:技能注册表只做登记和调度,不写业务逻辑;业务逻辑全部收口在技能执行器内部;对外暴露的只有标准化的 Schema 和错误码。这样即使团队从三个人扩到三十个人,也不会因为协作问题把技能系统搞乱。
4. 技能系统运行后,我遇到的四个典型问题
4.1 模型参数抽取不稳定,问题多半出在 Schema 设计
技能上线初期,我遇到最多的问题是模型调用技能时参数填得乱七八糟。比如用户说“帮我看看上海的天气”,模型把“上海”塞到城市字段没问题,但有时候会把“明天”解析成日期格式时写成2025-01-01,有时候又写成明天两个汉字,导致执行器解析失败。
排查了一段时间后,我发现问题根源不在模型,而在 Schema 太粗。日期字段没有给出明确的格式约束和示例值,模型只能凭感觉生成。后来我在参数描述里加了两条:一是明确枚举和格式,二是提供一个真实示例。比如日期字段描述改成“目标日期,格式必须是 YYYY-MM-DD,例如 2025-01-01;当用户说‘明天’时,由模型计算后填入具体日期”。改完之后参数抽取准确率明显提升。
另一个坑是布尔字段。模型经常把“是”“需要”解析成true,把“否”解析成false,看起来没问题,但当用户说“不确定”时模型会乱猜。我后来把所有可能缺失的布尔字段都加了default,同时在校验规则里规定没有明确表达时一律走默认值,避免模型替用户做决定。
4.2 技能太多,模型反而选不准
技能注册表从五个涨到三十个之后,我遇到了一个新的头疼问题:模型开始频繁选错技能。用户明明问的是“退货运费谁承担”,模型却调用了“查询订单”技能;用户想“修改收货地址”,模型却调用了“查询门店”技能。
一开始我以为是模型能力不够,后来统计了一遍技能描述才发现,问题在于多个技能描述里都出现了“订单”“地址”“查询”这些高频词,模型在语义匹配时被干扰了。解决思路有两个方向。
第一个是技能描述做减法,把描述里跟业务目标无关的修饰词全部删掉,尽量用“当用户想 X 时使用”的句式,减少歧义。第二个是引入候选集预筛,在让模型做最终选择前,先用关键词或者向量检索把全量技能缩小到五六个候选。预筛逻辑可以很简单,给每个技能打两到三个业务标签,用户消息先匹配标签,命中标签的技能才进入模型选择范围。实测在技能数超过二十个之后,加预筛能显著提升选择准确率。
4.3 执行失败后,Agent 要学会体面地降级
技能执行不可能永远成功,外部 API 不稳定、数据库超时、参数语义偏差都可能导致失败。最初我的 Agent 在技能失败时会直接把“系统异常”抛给用户,这种体验基本没法看。
后来我设计了一套三级降级策略。第一级是重试:对网络超时、服务暂时不可用这类错误,自动重试一次,往往就能解决。第二级是换路径:同一个目标如果有多条技能实现路径,比如“查天气”既可以走实时接口也可以走缓存,实时接口失败就自动切到缓存。第三级是主动澄清:如果参数不完整或校验不通过,Agent 不要硬试,而是用自然语言向用户确认缺失信息。把“系统失败了”变成“我没有找到你的订单号,可以再提供一下吗”,体验差距非常明显。
降级策略要做成技能执行器内部的标准流程,而不是靠模型临场发挥。因为模型在失败场景下的表现非常不稳定,与其赌模型的临场应变,不如把降级规则写死在代码里。
4.4 安全与权限:不要在技能层面对大模型过度信任
最后聊一个容易被忽视的问题:安全。模型本质上是概率生成器,它的输出不一定符合业务安全规则。技能系统必须在执行层做权限门禁,不能把“模型已表达意图”默认成“用户已授权”。
比如“删除订单”“修改价格”“发送营销短信”这类高风险操作,我要求技能执行器拒绝在单轮对话内执行,必须走带外确认流程,例如通过验证码、二次弹窗或者审批流确认。模型在这种场景下只负责生成待确认的操作内容,不负责直接执行。
另一个安全细节是外部服务的请求参数校验。模型生成的 URL、SQL 语句、文件路径,一律不能直接拼接执行,必须经过白名单校验。URL 只允许访问内网域名白名单,SQL 只允许预编译参数化查询,文件路径只允许访问配置的基础目录。别觉得这是小题大做,生产环境里模型被诱导读取敏感文件的案例已经不少了。
| 问题现象 | 可能原因 | 解决动作 |
|---|---|---|
| 参数格式不稳定 | Schema 缺少格式约束和示例 | 在描述中增加格式说明和示例值 |
| 技能选择准确率下降 | 技能数量多且描述语义重叠 | 增加标签预筛,缩小候选集 |
| 外部接口超时导致回复失败 | 缺少重试和降级策略 | 实现重试、缓存降级、主动澄清策略 |
| 模型调用高风险操作 | 缺少权限门禁 | Schema 增加权限字段,执行器增加白名单校验 |
5. 从技能到“会成长的 Agent”,一点方向性思考
文章写到这,最后聊一点我对技能系统后续演进的看法。现在的技能体系大多是静态设计——技能列表由开发人员手工维护,模型只能从已有技能里选。但这个模式在技能数量进一步膨胀后一定会遇到瓶颈,未来的方向可能是让 Agent 能自己创建、沉淀新技能。
比如客服 Agent 在处理了一百个“查发票”的请求后,能不能自动从对话日志里提炼出一个“发票查询”技能的 Schema?能不能把三个常被串联调用的技能自动合并成一个组合技能?这需要技能系统有更好的埋点、更结构化的日志、以及一个“技能提炼”的后处理流程。短期内手工维护仍然是主流,但底层的数据积累现在就要开始做。
我个人在实际操作中的体会是,技能系统的的核心不是代码写得多漂亮,而是协议定得够不够干净。技能注册表、Schema、错误码、权限模型,这些设计好了,后面的扩展自然顺理成章。最后再分享一个小技巧:给每个技能录一段“使用日志”,不仅记录参数和返回结果,还记录模型当时的决策上下文。这比任何评测集都更能帮你找到模型的盲区,也能为以后技能自动沉淀打基础。