☰
Agent Skills实战:智能体技能系统设计与踩坑实录
2026/10/14 8:45:50 网站建设 项目流程

最近在给一个跨平台系统设计智能体能力层的时候,把agent-skills这套东西完整地过了一遍。很多刚接触智能体开发的朋友会把"技能"理解成"给大模型多写几段提示词",但真正落地之后你会发现,这是完全两码事。skills体系解决的是智能体怎么知道有哪些能力可用、怎么描述能力、怎么安全地调用能力、以及怎么把执行结果接回对话流程的问题。这篇文章主要面向正在设计智能体框架、或者想把现有业务能力开放给大模型调用的开发者,我会从设计思路、最小实现、踩坑实录三个层面展开,尽量把每个决策背后的理由讲清楚。

1. 为什么智能体需要"技能"而不是"提示词"

1.1 提示词解决不了"动手"的问题

先聊一个最典型的场景。假设你想让智能体帮你查询订单状态,最朴素的方案是在系统提示词里写:"你是一个客服助手,当用户问订单时,请查询数据库并回复。"

问题马上来了:大模型本身没有数据库连接,它无法执行任何真实操作。你让它"查询",它只能根据训练数据里的模式去"编"一个结果。哪怕你把查询逻辑写成自然语言塞进提示词,模型输出的时候也只会给你一段文字,而不是一个可执行、可校验的结构化指令。

函数调用(function calling)方案解决了一部分问题:把工具函数的名字、描述、参数结构告诉模型,模型在回答时先输出一个结构化的调用请求,再由外部代码真正执行。这相当于给了模型一双手。但只做到这里是不够的,因为它只解决了"模型输出调用意图"这一步,剩下的能力管理、参数校验、错误回传、技能下线,全都得自己设计。

1.2 技能是函数调用之上的完整封装

我在项目里对"技能"的定义是:把某一种能力打包成可复用的完整单元,它至少包含四部分。

  • 技能元数据:名称、描述、所属领域、版本号、启用状态。
  • 参数定义:一套结构化的输入规范,我用JSON Schema来表达。
  • 执行逻辑:真正干活的代码,这部分和普通业务函数没有本质区别。
  • 反馈协议:执行成功或失败之后,返回给模型的标准结构化结果。

有人会问:这不就是一个函数吗?区别在于函数只需要被代码调用,而技能还需要被模型"理解"和"选择"。同一个能力,用不同的描述方式,模型在合适的场景下能不能选对,效果差异非常大。技能的描述文本本质上是一份写给模型看的"产品说明书",说明书写得不清楚,模型要么不调用,要么在不该调用的地方乱调用。

1.3 没有技能库的智能体是"裸奔"的

做了几个智能体项目之后,我的体会是:如果不加管理机制,智能体代码会迅速腐化。工具函数的调用逻辑堆在一起,参数校验散落在各处,旧技能下线了模型还在调用,日志里全是莫名其妙的报错。技能库存在的意义,就是给这些能力统一"上户口"——注册、上架、下线、升级,每个技能都有清晰的接入点和生命周期。

这套管理与之前做微服务接口管理非常像。你可以把技能库理解成智能体的"网关",所有外部能力都从这里进出,统一鉴权、统一校验、统一埋点。没有这一层,智能体的行为就不可控,后续也没办法做效果评估和问题回溯。

2. 技能系统的核心设计决策:从定义到编排

2.1 技能定义格式:写清楚"给模型看"的说明书

技能定义里最容易被低估的部分是description。代码里的函数注释是给人看的,技能描述是给模型看的。我给团队定过一条要求:每段description必须包含三个信息——这个技能做什么、在什么场景下使用、有什么限制。

举个例子,同样一个查订单功能:

差的描述: 查询订单信息。 好的描述: 当用户询问订单状态、物流进度、订单明细时使用。 输入订单号或用户ID,返回订单当前状态及物流节点。 如果订单号格式不正确,参数校验会出错并返回错误信息。

第二段描述不仅说了"能做什么",还说了"什么时候用"和"什么情况下会失败"。模型选择技能时,本质是在做一次文本匹配,上下文的相关性越高,选对的概率越大。

参数定义方面,我建议坚持使用JSON Schema,不要自创格式。一方面模型对JSON Schema的掌握程度远高于自定义模板,另一方面校验层可以直接复用现成的校验库,省去自己写解析器的麻烦。参数定义里有两类字段特别容易踩坑,下面单独说。

2.2 技能注册与发现:统一管理能力的户口

技能注册机制解决的是"系统里有哪些技能可用"的问题。我采用的是装饰器加全局注册表的方案,把技能定义和执行逻辑绑定在一起,模块加载时自动注册。

# skill_registry.py from typing import Dict, List, Optional class SkillRegistry: _skills: Dict[str, "BaseSkill"] = {} @classmethod def register(cls, skill: "BaseSkill") -> None: if skill.name in cls._skills: raise ValueError(f"技能 {skill.name} 已存在,请检查是否需要覆盖注册") cls._skills[skill.name] = skill @classmethod def get(cls, name: str) -> Optional["BaseSkill"]: return cls._skills.get(name) @classmethod def all_skills(cls) -> List["BaseSkill"]: return list(cls._skills.values()) @classmethod def unregister(cls, name: str) -> None: cls._skills.pop(name, None)

注册表的好处是集中管控。技能上线、下线、灰度,都可以通过注册表的状态位控制。比如想临时下线一个异常技能,不需要改动任何调用代码,直接从注册表摘除即可,模型下一次请求就看不到它了。

这里还隐藏了一个问题:技能动态上下线之后,已经生成好的模型请求上下文里可能还带着这个技能。所以我在项目里把技能列表的快照与请求绑定,每次请求独立生成,避免请求之间相互污染。

2.3 技能编排:什么时候把技能讲给模型听

技能编排是我认为整个系统里最讲究的部分。核心矛盾是:技能池越大,模型选择错误的概率越高;但把所有技能都塞进提示词,又会撑爆上下文窗口,还增加推理开销和延迟。

我使用的策略是分层注入。第一层是常驻技能,数量控制在十个以内,这些是各个对话场景都会用到的基础能力,比如获取当前时间、读取用户基本信息。第二层是条件触发技能,根据对话意图识别结果、用户所在页面、最近提到的实体,动态追加相关技能。第三层是搜索技能,当技能库超过一定规模时,用向量检索的方式,从技能描述库里召回最相关的二十个技能,再注入提示词。

这套方案在模拟项目X里跑下来的效果是:技能库从二十几个扩充到一百多个之后,模型平均决策质量没有明显下滑。如果不管不顾地全量注入,模型开始出现频繁选错技能的情况,而且首字响应时间显著拉长。

3. 从零实现一套agent-skills的实操过程

3.1 最小技能单元:一个可运行的示例

下面这个例子是一个查询模拟商品库存的技能,完整演示技能定义、注册和执行的闭环。

# inventory_skill.py import json from datetime import datetime from typing import Dict, Any class BaseSkill: name: str = "" description: str = "" parameters_schema: Dict[str, Any] = {} version: str = "1.0.0" def execute(self, **kwargs) -> str: raise NotImplementedError class QueryInventorySkill(BaseSkill): name = "query_inventory" description = ( "当用户询问商品是否有货、库存数量、剩余存货时使用。" "输入商品SKU编码,返回当前库存余量。" "如果SKU编码不存在,返回错误信息ERR_INVALID_SKU。" ) parameters_schema = { "type": "object", "properties": { "sku": { "type": "string", "description": "商品SKU编码,例如 SKU-10001", } }, "required": ["sku"], "additionalProperties": False, } version = "1.2.0" def execute(self, **kwargs) -> str: sku = kwargs.get("sku", "") # 模拟库存服务的查询逻辑 stock_map = { "SKU-10001": {"name": "标准版", "stock": 56}, "SKU-10002": {"name": "进阶版", "stock": 0}, } if sku not in stock_map: return json.dumps({"code": "ERR_INVALID_SKU", "msg": "SKU不存在"}) info = stock_map[sku] return json.dumps({ "code": "OK", "sku": sku, "name": info["name"], "stock": info["stock"], "checked_at": datetime.now().isoformat(), })

注意execute方法返回的是一个JSON字符串,而不是Python对象。原因在于,最终这个结果要拼接到模型的下一次请求上下文里,字符串形式可以直接嵌入,省去一次序列化转换。返回结构里我固定放了code字段,调用方可以据此判断是否成功。

3.2 参数校验与容错:别让模型乱传参

模型生成的参数经常不按套路出牌。常见的问题有:必填参数缺失、参数类型错误、多传了schema里没定义的字段。针对这些问题,我在执行逻辑之前加了一层严格校验。

# validator.py from jsonschema import Draft7Validator, FormatChecker def validate_skill_params(skill: BaseSkill, params: Dict[str, Any]) -> None: validator = Draft7Validator(skill.parameters_schema, format_checker=FormatChecker()) errors = list(validator.iter_errors(params)) if errors: first = errors[0] raise SkillParamError( f"参数校验失败: {first.message},路径={list(first.path)}" )

校验失败的异常会在上层被捕获,转换成模型能看懂的错误反馈,例如:

调用技能 query_inventory 时参数有误:'sku' is a required property。 请确认用户提供了商品SKU编码后再发起调用。

这一步很关键。很多团队把校验失败当成普通异常直接抛给用户,模型根本不知道发生了什么,只会笼统地道歉。让模型看到具体错误原因,它才能在下一轮对话里补问信息或者换个思路。

我还建议在参数传递时做一层"宽容化"处理。比如模型可能把数字类型传成字符串"123"、把时间传成"2025-01-01",这类常见格式偏差可以先用协同类型转换再校验,能明显降低整体失败率。但枚举值、业务ID这类关键字段不建议宽容,错了就是错了。

3.3 执行反馈闭环:结果要能回到对话里

技能执行完成之后,结果需要回到模型那里,成为生成最终回复的依据。我采用的闭包结构是这样的:

  1. 技能执行成功,把JSON结果追加到消息历史。
  2. 模型基于技能结果生成面向用户的自然语言回复。
  3. 如果技能执行失败,把错误信息追加到消息历史,模型尝试修正或询问用户。

伪代码流程如下:

# agent_loop.py messages = [{"role": "user", "content": user_input}] for step in range(3): available_skills = assemble_skills_by_context(messages) response = model.chat(messages, skills=available_skills) if response.has_tool_call: skill_name = response.tool_call.name params = response.tool_call.arguments skill = SkillRegistry.get(skill_name) try: result = skill.execute(**params) except SkillParamError as e: result = str(e) except Exception as e: result = json.dumps({"code": "ERR_UNKNOWN", "msg": str(e)}) messages.append(response.assistant_message) messages.append({ "role": "tool", "tool_call_id": response.tool_call.id, "content": result, }) continue else: final_reply = response.content break

这个循环最多允许三步工具调用。限制步数是为了防止模型陷入无意义的循环追问,比如反复调用同一个失败技能。超过限制后直接返回当前回复,减少用户等待时间。

关于执行超时,我给每个技能单独配置了超时阈值,默认五秒。因为模型等待技能返回结果期间,整个会话是阻塞的,某个技能卡住会影响全局体验。超时之后返回一个固定的"技能执行超时"提示,模型会自行决定是重试还是给出降级答复。

4. 常见问题与排查技巧实录

4.1 模型编造了不存在的技能

这是我在项目里遇到最多的问题。明明技能库里只有十几个技能,模型却输出一个从来没注册过的技能名,导致调用阶段直接抛异常。

排查下来原因通常有两个。一是模型在训练数据里见过类似命名的工具,产生了"惯性输出";二是注入的技能描述与用户问题匹配度不高,模型开始凭感觉编。

我的处理办法分三层。第一,在系统提示词里加一句明确的约束:"只能调用可用技能列表中的技能,不要自行创建工具。"第二,在解析模型输出时,如果技能名不在注册表中,不直接报错,而是把错误信息回传给模型:"技能 xxx 不存在,请从可用技能列表中选择。"让模型有机会修正。第三,给每次请求都注入当前技能清单摘要,降低模型遗忘的可能性。

4.2 参数类型不对齐

模型输出参数经常是字符串类型,比如把数字写成"3"、把布尔值写成"true"、把数组写成"item1,item2"。这种问题在参数schema里如果只是声明type而不做格式约束,模型很容易出错。

我的经验是,在做严格校验之前,先跑一层类型归一化。数字字符串尝试转数字,逗号分隔的字符串尝试拆数组,但转换失败时绝不静默吞掉,而是抛出明确的校验错误。另外,对于日期、时间、手机号这类有固定格式的字段,在schema里标注format并启用format_checker,校验器会自动帮忙做格式检查。

还有一个容易忽略的细节:如果schema里设置了"additionalProperties": false,模型一旦多传一个字段就会整体校验失败。我发现模型在一些模糊场景下会多传同义字段,比如既传了user_id又传了uid。这个问题的处理办法是约束模型严格按schema输出,同时在错误提示里明确指出"不接受的额外字段名",比笼统的"参数校验失败"有效得多。

4.3 技能池膨胀导致选择困难

技能数量超过五十个以后,模型的选择准确率明显下降。我做过一次对照测试:二十个技能时选对率接近九成,一百二十个技能全量注入时,选对率掉到七成以下,而且响应延迟涨了一倍。

前面提到的分层注入和检索召回就是针对这个问题的。另外还有一个技巧:对技能名称做前缀分组,比如订单类技能统一以"order_"开头,用户类技能统一以"user_"开头。这样在构建检索索引时,查询向量可以根据用户的意图先定位到领域子集,再精确匹配技能,效果比全局检索稳定。

如果上下文窗口比较紧张,我还会把每个技能的参数schema做压缩摘要,只保留字段名和类型,不保留长描述。模型调用技能时主要靠技能名和description做决策,参数细节等真正生成调用参数时再参考完整schema。这种"决策用摘要、执行用全量"的策略,能省下大量上下文空间。

4.4 排查流程与日志设计

技能调用链路涉及模型、校验、执行、反馈四个环节,出问题之后如果日志不完整,排查起来非常痛苦。我给所有技能增加了一条统一的结构化埋点,字段如下:

字段含义
trace_id一次会话的唯一链路ID
skill_name被调用的技能名
params模型生成的原始参数
validate_result校验结果,pass或fail及原因
execute_latency执行耗时,单位毫秒
execute_result_code返回结果里的code字段
retry_count本轮调用重试次数
model_request_snapshot注入该技能时的描述快照

这里特别说一下model_request_snapshot。它记录的是当前模型请求里实际注入了哪些技能描述,而不是技能库里全部技能。这样如果模型明明应该看到某技能却没调用,可以反查是不是注入逻辑丢了。

排查时我会先看validate_result,如果大量校验失败,问题大概率在参数schema定义或者模型生成质量上。如果校验全过但code非OK,问题在执行逻辑或者下游服务。如果模型根本没输出工具调用,问题通常出在技能描述和用户意图的匹配上,这时候需要回头改description。

5. 一点个人体会与后续扩展

我们这套agent-skills从设计到落地,前后迭代了三版。第一版只做了技能定义和注册,调用时把全部技能灌进提示词,效果在二十个技能以内还算能用,规模一大就露馅。第二版引入了JSON Schema校验和结构化反馈,稳定性提高不少,但模型还是经常"听不懂"技能该在什么时候用。第三版加上分层注入与检索召回之后,才真正解决了技能池膨胀的问题。

我个人最大的体会是:技能系统的设计顺序应该从"如何让模型正确选择"出发,而不是从"如何把代码封装得漂亮"出发。技术分层可以慢慢优化,但描述与选择机制在一开始就要想清楚,因为它直接决定智能体能力的上限。

如果后续要继续扩展,我会往两个方向走。一是技能效果评估,把每个技能的调用率、调用成功率、修正轮次做成日报,让技能质量可度量。二是多智能体之间的技能共享,把技能库升级成服务化组件,让不同智能体实例既能隔离又能复用同一套能力。这两个方向做完,agent-skills才算是真正从"工具集"进化为"能力平台"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询