在AI Agent开发这块摸爬滚打了两年多,一个深刻的体会是:光有大模型还不够,真正让Agent“干得了活”的,是它身边那一整套技能系统。很多人一开始接触agent-skills这个概念,以为是给Agent多塞几个工具函数就行,结果真正落地的时候才发现,从技能怎么设计、怎么描述,到怎么编排、怎么容错,每一步都是坑。这篇文章就把我自己在项目里沉淀下来的那套做法完整捋一遍,从概念拆解到代码级的实现细节都给你盘清楚。
1. 为什么Agent需要一套“技能系统”,而不只是几个函数
很多团队第一次做Agent,思路很直接:既然模型会调函数,那就把能想到的功能都写成函数,然后一股脑塞进prompt里。前期Demo确实跑得挺欢,但稍微一上复杂度就露馅——模型开始选错工具、参数传得乱七八糟、两个技能之间互相抢上下文。这不是模型不行,而是你缺了一个“技能管理层”。
1.1 Agent Skill到底是什么
所谓Agent Skill,按照我自己的理解,就是一类可复用、可编排、带自描述能力的原子能力单元。它不只是“一个函数”,而是包含三个部分:
- 能力描述(自然语言+结构化Schema):告诉模型“我是干什么的、在什么场景下用我、参数是什么格式”。
- 执行逻辑(实际代码/API调用):真正干活的这部分,可能是一个Python函数,也可能是一段外部服务的请求。
- 边界约定(前置条件、副作用、超时、错误码):规定清楚什么情况下能用、失败了会怎么样、需不需要人工介入。
打个比方,如果你把Agent想成一个新来的实习生,工具函数就好比是放在抽屉里的螺丝刀,而Skill则是给这个实习生的一张卡片——卡片上写清楚了“这个螺丝刀能拧哪些型号的螺丝、用的时候注意什么、拧不动的时候怎么处理、完成之后要在工作日志里记录什么”。没有卡片,实习生拿到螺丝刀也是一脸懵;有了卡片,他才知道什么场景下该用、用完该怎么交接。
1.2 没有技能系统之前,我们踩过的具体问题
我先说几个实际撞过墙的场景,方便你对号入座。
第一个是选择困难。我们有一个Agent需要同时处理“查天气”和“订会议室”两种操作,当时把它们平铺在工具列表里,模型经常在用户说“明天下午开会要个场地”的时候,先去查了个天气,再订会议室。逻辑上不算错,但执行顺序乱掉之后,后面依赖场地信息的流程就全乱了。这个问题的根源在于,平铺的工具体列表没有“场景优先级”和“依赖关系”的概念。
第二个是参数幻觉。模型在调用一个“发邮件”函数时,把收件人、标题、正文传得都对,但顺手把“抄送”字段给编造了一个邮箱地址出来。函数的schema里字段一多,模型就是会漏或者编。这种情况靠“提示词里写清楚”根本压不住,必须在技能层做参数校验、默认值注入和二次确认。
第三个是状态割裂。一个Agent任务如果拆成“检索资料→生成摘要→发送报告”三步,每一步都是独立的函数调用,那么第二步需要第一步的结果,这个中间状态放哪?传统函数传参你得把数据结构设计得异常小心,不然几个技能一交错,上下文就成一锅粥。
这三个问题指向同一个结论:Agent不能只有“工具”,还得有“技能编排层”。
1.3 什么时候你的Agent真的需要上技能系统
不是所有项目一上来就非得搞一套复杂的技能框架。我的判断标准很简单:
- 工具的数量超过15个,模型开始频繁选错;
- 任务里存在多步依赖,比如A的结果要作为B的输入;
- 同一个能力需要在多个Agent或场景间复用;
- 需要记录和分析Agent到底调了哪些能力、效果如何。
如果你只是做个单轮问答Demo,三五个函数够了,确实不必上重架构。但只要命中上面任意两条,我建议尽早把技能层设计好,不然后面重构成本会高到你怀疑人生。
2. 技能系统的核心设计:从“工具列表”到“可编排技能层”
理顺了Why,接下来就是How。我在实际项目里把agent-skills的落地拆成四层结构:技能元数据层、技能注册中心、技能运行时、技能编排层。每一层都有自己的职责,缺一不可。
2.1 技能元数据层:让模型“看懂”每个技能
这是我认为最反直觉、也最关键的一层。很多开发者把大量精力花在写技能的执行代码上,但对技能的“描述信息”草草了事。实际上,对大模型来说,技能的描述质量直接决定了它的调用准确率。
一个合格的技能元数据包括:
- 技能名称:必须是动词开头的短名字,比如search_web_docs、book_meeting_room,不要叫func_a。
- 一句话说明:说明“能做什么”和“适合什么场景”,例如“在团队知识库里搜索文档,适合用户询问项目规范、历史决策时使用”。
- 参数Schema:每个参数的名称、类型、必填与否、取值范围、示例值。这里有个我强烈建议的细节——在描述里写清楚“参数的单位和格式”,比如时间统一用ISO 8601,不然模型会用各种奇怪格式来填。
- 前置条件和副作用:比如“需要先完成用户身份验证”、“调用后会产生一条外部通知”,这类信息能让模型在编排时做合理决策。
- 典型调用示例:给1-2个完整输入的示例,不是给模型背答案,而是让模型理解参数之间的联动关系。
我见过一个很典型的反面教材:一个“发送短信验证码”的技能,描述里没写“同一个手机号一分钟只能发一次”,结果模型在用户连续点击“重新发送”时,每次都真的去调短信接口,被上游服务限流封了IP。后来我在技能描述里加了这条前置条件,配合后端的频控校验,问题才彻底解决。
2.2 技能注册中心:全局唯一、可搜索、可审计
技能多了之后,注册中心就相当于整个Agent体系的“通讯录”。每个技能在启动时注册进来,包括名称、版本、描述、调用地址、超时时间、健康状态。这个中心至少要做三件事:
- 去重与版本管理:同一个语义的能力不允许注册两份,升级技能的时候保留版本号,方便回滚。
- 动态上下线:某个上游服务挂了,对应的技能能自动标为“不可用”,模型在规划时就会避开它。
- 调用审计:记录谁在什么时间调用了哪个技能、传入什么参数、返回什么结果、耗时多长。这块数据后面做评测和调优时是黄金资产。
实现技术上,我第一版用的是内存字典,第二版换成了带TTL的缓存加数据库持久化,核心逻辑不变,但数据量上来之后,检索和统计就不再卡顿。这里不依赖特别重的基础设施,只要保证查询延时在毫秒级就行。
2.3 技能运行时:真正“干活”的沙盒
技能执行代码本身,我建议做一个独立的运行时沙盒,而不是让Agent直接随意调用系统函数。沙盒的意义不在于安全(当然也有这层考虑),更在于给所有技能提供统一的基础设施——比如日志采集、错误标准化、超时控制、重试策略。
每个技能在执行时都遵循同一个生命周期:
- 接收经过校验的参数(可能是模型直接传的,也可能是编排层加工过的);
- 执行具体逻辑,期间随时上报进度;
- 返回标准结果对象——不只是数据本身,还包括状态码、耗时、消耗的token数(如果有LLM调用)、给用户的提示语;
- 如果失败,返回统一的错误码,并附上“给模型看的失败原因”和“给用户看的友好提示”两个字段。
第二个字段容易被忽略,但特别重要。比如技能失败时,返回给模型的信息是“上游接口超时,错误码504,已重试1次”;返回给用户的信息则是“暂时查不到这个数据,请稍后再试”。两者不能混,不然用户会看到一串技术黑话。
2.4 技能编排层:串起复杂任务的那只手
编排层解决的是“多个技能怎么配合完成一个复杂任务”的问题。我的实践里,编排有两条路走:
- 模型自主编排:把技能列表和任务目标一起给模型,让模型自己决定调哪个、什么顺序调。灵活度最高,但不可控性也强,适合探索性强、没有固定流程的任务。
- 流程硬编排:在代码层预先定义好DAG(有向无环图),比如“查库存→锁库存→生成订单→发起支付→发通知”。每个节点的输入输出在流程里就接好了,模型只在每个节点内做“参数填充”而不是“流程决策”。
稳健做法是两者结合:默认走流程编排,但在流程的每个节点允许模型自主调用“辅助技能”。举个例子,订单流程走到“发起支付”时,如果模型发现用户余额不足,它可以主动调用一个“查优惠券”的技能,看能不能给用户凑个优惠。这个能力在流程编排模型里一开始没设计,但有了辅助技能的兜底,整个流程的完成率提升了不少。
3. 技能编排的工程落地:我用了一个极简的“技能描述”协议
前面讲的是抽象架构,这一节直接上实操。我自己的做法是在团队里定了一个叫SkillSpec的描述协议,用JSON组织,统一了所有技能给模型看的“说明书”格式。你可以直接抄这个结构,再按自己的业务改。
3.1 SkillSpec的字段定义与设计原因
{ "name": "search_knowledge_base", "version": "1.2.0", "description": "在团队知识库中搜索与关键词匹配的文档。适合用户询问项目规范、历史决策、API文档时使用。", "tags": ["search", "knowledge_base", "internal"], "parameters": [ { "name": "query", "type": "string", "required": true, "description": "搜索关键词,建议从用户原话中提取核心名词,不要自行扩写。", "example": "数据库连接超时排查" }, { "name": "top_k", "type": "integer", "required": false, "default": 5, "description": "返回的文档数量,范围1-10。", "example": 5 } ], "requirements": [ "调用前需要用户已登录,若未登录请先调用auth_skill", "同一个query在一分钟内最多调用3次" ], "side_effects": [], "error_codes": { "AUTH_REQUIRED": "用户未登录,需要引导用户先完成身份验证", "RATE_LIMITED": "触发频控,提示用户稍后重试" }, "timeout_ms": 5000 }这个结构看起来平平无奇,但有几个点是踩过坑之后才加上的。tags字段最初没有,后来发现模型在“面对模糊意图”时,会先根据tags缩小候选集,再细读描述,调用准确率提高了不少。example字段最初也没写,但加上之后,参数格式错误率明显下降——模型更擅长模仿一个具体例子,而不是理解一句干巴巴的描述。requirements字段则是让模型在编排时提前知道“得先干个啥”,避免流程跑到一半卡住。
3.2 把SkillSpec注入模型上下文的模板
有了技能说明,还得考虑怎么把这些内容塞给模型。全塞进System Prompt是最简单的,但技能一多就不现实,token暴涨且模型注意力被稀释。我自己用的是“分层路由”的策略:
- 在System Prompt里只放一个“技能索引”——每个技能的名字+一句话+适用关键词,控制在500字以内;
- 当模型判断需要某个技能时,再通过一个专门的“技能详情查询”接口,拉取对应技能的完整SkillSpec内容;
- 模型拿到详情后,再决定是否真正调用,以及怎么填参数。
这样主上下文始终干净,每次只载入相关技能。具体到实现上,我会在System Prompt前段加一段类似这样的索引:
可用技能索引(在调用前如需完整说明,请查询skill_detail工具): - search_knowledge_base:搜索团队知识库文档,适用:项目规范、历史决策、API用法 - book_meeting_room:预定会议室,适用:用户提出开会、约时间 - send_email:发送邮件,适用:用户要求发邮件给某人 - get_user_auth_status:查询当前用户登录态,适用:所有需要用户身份的操作前这段索引生成的规则是:从注册中心取所有启用的技能,自动拼出这一块。后续加新技能时,不需要改prompt模板,注册中心加一条记录就行。
3.3 参数填充与校验:堵住模型“编参数”的毛病
模型填参数永远是个手艺活,我再怎么优化描述,也还是会偶发传错格式的情况。所以我在技能运行前加了一道“参数中间层”。这个中间层干三件事:
- 默认值补全:比如用户没说数量,走的默认5;
- 格式归一化:把时间、日期、百分比之类统一成约定格式;
- 合法性校验:枚举值是否在范围内、ID是否存在、邮箱格式对不对。校验不通过时,不是简单报错,而是回抛给模型一个“修正指令”。
修正指令的写法也很讲究。你不能只说“参数错了”,得告诉模型具体错哪、怎么改,我一般用这样的模板:
调用技能search_knowledge_base时参数格式有误: - top_k=0,但取值范围为1-10,请重新提供 - query为纯数字,看起来不是有效关键词,请从用户原话中提取核心业务词 请修正参数后重新调用,或向用户说明无法处理。这套“说清楚错在哪+给出修正方向”的反馈循环,大概跑了几周后,模型在参数填充上的失误率降到了原先的三分之一。这比单纯加大模型规格管用得多,毕竟这是个“上下文工程+校验兜底”的问题,不是“模型智力”的问题。
3.4 技能编排的DAG模板示例
再放一个编排层的代码思路。我用Python写了一个极简的流程引擎,它接受一个“DAG定义”,依次执行节点,并且自动把上一个节点的输出转成下一个节点的输入占位符。下面是一个“会议安排”流程的骨架:
dag = { "name": "arrange_meeting_flow", "nodes": [ {"id": "check_calendar", "skill": "check_user_calendar"}, {"id": "find_room", "skill": "find_available_room"}, {"id": "book_room", "skill": "book_meeting_room"}, {"id": "send_invites", "skill": "send_email"} ], "edges": [ {"from": "check_calendar", "to": "find_room"}, {"from": "find_room", "to": "book_room"}, {"from": "book_room", "to": "send_invites"} ], "fallback": "notify_human" }执行时,流程引擎先跑check_calendar,拿到用户空闲时间段,塞给find_available_room;find_available_room再基于时间段找空闲会议室并返回room_id;接着book_room去占用它;最后send_invites发给参会人。如果其中某个节点失败,流程不硬往下走,而是触发fallback——比如book_room失败,就转给一个“线下人工协调”技能,而不是假装成功。
这样设计的价值在于:任务的“主干逻辑”是稳定可控的,而每个节点内部又保留了模型微调的弹性。既不会出现流程乱跳,也不会完全死板。
4. 实操环节:从零搭建一个带技能层的Agent
理论部分讲了这么多,我决定把一个最小可运行的Agent技能层完整演示一遍。这个Demo虽然规模不大,但囊括了注册中心、技能描述注入、运行时、编排层这几块核心逻辑,你可以直接把它作为自己项目的起点。
4.1 项目结构与依赖
我的演示环境用一个Python脚本就能跑通,依赖也克制,主要用到:
openai(或任意兼容的模型接口SDK)pydantic(做参数校验和Schema生成)jinja2(渲染技能详情的模板)fastapi(跑一个极简的Skill服务,方便外部调用)
目录结构我习惯这样摆:
agent-skills-demo/ ├── skills/ │ ├── __init__.py │ ├── registry.py # 技能注册中心 │ ├── skill_base.py # 技能基类与SkillSpec定义 │ ├── search_skill.py # 示例技能:知识库搜索 │ └── booking_skill.py # 示例技能:会议室预定 ├── orchestration/ │ ├── executor.py # DAG流程执行器 │ └── planner.py # 模型自主规划的封装 ├── runtime/ │ ├── context.py # Agent上下文管理器 │ ├── middleware.py # 参数校验、日志、审计中间件 │ └── llm_client.py # 统一的大模型调用客户端 └── main.py # 入口,演示一个完整任务4.2 技能基类的实现:把“规范”变成代码约束
一个技能基类,核心是强制所有技能都提供name、description、execute三个要素:
# skill_base.py from pydantic import BaseModel, Field from typing import Any, Optional class SkillSpec(BaseModel): name: str version: str = "1.0.0" description: str tags: list[str] = [] parameters: list[dict] = [] requirements: list[str] = [] side_effects: list[str] = [] error_codes: dict[str, str] = {} timeout_ms: int = 5000 class BaseSkill: spec: SkillSpec async def validate_params(self, params: dict) -> dict: """参数校验与默认值补充,子类按需覆写""" return params async def execute(self, params: dict, context: dict) -> dict: raise NotImplementedError这里用pydantic的BaseModel来定义SkillSpec,好处是后面可以直接调.model_dump_json()把描述序列化成JSON,而且天然支持非法字段拦截。每个具体技能只要继承BaseSkill,实现spec和execute,其他都不用管。
4.3 一个具体技能的实现:搜索知识库
下面这个技能演示了怎么把“前置条件”和“失败原因返回”都写清楚:
# search_skill.py class SearchKnowledgeBase(BaseSkill): spec = SkillSpec( name="search_knowledge_base", version="1.2.0", description="在团队知识库中搜索与关键词匹配的文档。适合用户询问项目规范、历史决策、API文档时使用。", tags=["search", "knowledge_base", "internal"], parameters=[ { "name": "query", "type": "string", "required": True, "description": "搜索关键词,建议从用户原话中提取核心名词。", "example": "数据库连接超时排查", }, { "name": "top_k", "type": "integer", "required": False, "default": 5, "description": "返回的文档数量,范围1-10。", "example": 5, }, ], requirements=["调用前需要用户已登录,未登录请先调用auth_skill"], error_codes={ "AUTH_REQUIRED": "用户未登录,需要引导用户先完成身份验证", "RATE_LIMITED": "触发频控,提示用户稍后重试", }, timeout_ms=5000, ) async def validate_params(self, params: dict) -> dict: if not params.get("query") or not isinstance(params["query"], str): raise ValueError("query必须是有效的字符串") top_k = params.get("top_k", 5) if top_k < 1 or top_k > 10: top_k = 5 # 越界就回到默认值 cleaned = {"query": params["query"].strip(), "top_k": top_k} return cleaned async def execute(self, params: dict, context: dict) -> dict: # 检查前置条件 if not context.get("user_login"): return {"status": "error", "code": "AUTH_REQUIRED", "data": None} # 模拟一个检索,实际项目在这里调用向量数据库或ES results = mock_search(params["query"], top_k=params["top_k"]) return { "status": "success", "code": "OK", "data": results, "message": f"找到{len(results)}条相关文档" }细节上,我把“参数校验”单独拆了一个方法,没有塞进execute里。这样做的好处是中间件可以提前校验,早失败早返回,不会浪费一次模型到再下一次模型的外部调用。
4.4 注册中心与技能索引生成
注册中心我这里用最朴素的方式实现,不引外部依赖:
# registry.py class SkillRegistry: def __init__(self): self._skills = {} self._active = set() def register(self, skill: BaseSkill, active: bool = True): name = skill.spec.name if name in self._skills: raise ValueError(f"技能重复注册: {name}") self._skills[name] = skill if active: self._active.add(name) def unregister(self, name: str): self._skills.pop(name, None) self._active.discard(name) def get(self, name: str): return self._skills.get(name) def list_for_planner(self) -> list[str]: """生成给模型看的技能索引,按名称+一句话+关键词的形式输出""" lines = [] for name in sorted(self._active): skill = self._skills[name] spec = skill.spec line = f"- {spec.name}:{spec.description} 适用:{ '、'.join(spec.tags) }" lines.append(line) return "\n".join(lines)这里比较重要的是list_for_planner这个方法——它生成的不是完整SkillSpec,而是一个精简索引,用于塞进System Prompt。真正的完整描述走“详情查询”的方式动态加载。
4.5 路由与动态载入技能详情
当模型判断需要某个技能时,Agent会调用一个叫get_skill_detail的系统方法,把该技能的完整JSON描述取出来,然后再决定调用。封装代码如下:
# middleware.py async def get_skill_detail(name: str, registry: SkillRegistry): skill = registry.get(name) if not skill: return None return skill.spec.model_dump_json(indent=2) async def validate_and_execute(name: str, params: dict, context: dict, registry: SkillRegistry): skill = registry.get(name) if not skill: return {"status": "error", "code": "SKILL_NOT_FOUND", "data": None} # 参数校验 try: clean_params = await skill.validate_params(params) except ValueError as e: return { "status": "error", "code": "PARAM_INVALID", "data": None, "message": f"参数校验失败: {e}" } # 执行 result = await skill.execute(clean_params, context) return result实际项目中,这里还会加上耗时上报、日志记录、token统计、限流判断。中间件就是干这个的——技能本身不用关心这些横切关注点。
4.6 编排层的最小实现
最后给出一个最简单的“顺序执行器”,它接收一个技能列表,依次执行并把结果存入上下文字典:
# executor.py async def execute_sequence(steps: list[str], initial_context: dict, registry: SkillRegistry): context = dict(initial_context) for step in steps: # step可以是一个技能名,也可以是一个带参数的指令 skill_name = step.get("skill") if isinstance(step, dict) else step params = step.get("params", {}) if isinstance(step, dict) else {} skill = registry.get(skill_name) if not skill: context["error"] = f"技能{skill_name}不存在" return context result = await validate_and_execute(skill_name, params, context, registry) context[f"result_{skill_name}"] = result if result.get("status") == "error": context["error"] = result.get("message") break # 一旦失败就中断,由上层决定要不要走fallback return context这个执行器很简单,但已经足够承载“查日程→找会议室→预定→发邮件”这样的流程。它把每个步骤的输出存到context,后一个步骤能从context里捞到前序结果。要是需要更复杂的条件分支、并行执行,再扩展就行,核心模式是一样的。
5. 高频翻车现场:技能系统运行中的问题排查实录
技能系统跑起来之后,真正的挑战才开始。我把团队在实际运行中遇到的典型问题整理成一份速查表,每一条都是真实踩过的坑。
5.1 模型老是选错技能,怎么排查
这是最多人问的问题。出现模型在多个相似技能之间反复横跳,或者明显该用A却用了B,按下面顺序查:
第一步:查技能索引是否太长。如果你把20个技能的全量JSON塞进了System Prompt,模型注意力被稀释,选错概率暴涨。先压缩成一行一个索引(就是前面说的registery.list_for_planner那种格式),看错误率有没有下来。
第二步:查description是否有“区分度”。两个技能如果描述里都有“查询信息”,模型就分不清。聪明的做法是给每个技能写清楚“什么时候用我、什么时候别用我”。比如搜索技能里可以写“只用于查内部文档,不适用于查天气或查新闻”;天气技能里写“仅当用户明确提到天气、气温、降水时才使用”。
第三步:看是不是缺了反例。一个技能描述里如果能带上一句“不要用它来做什么”,效率会显著提升。比如会议室预定技能写“如果只是要讨论,不一定需要会议室,请先询问用户是否需要预定”,这一段反例能挡住很多误调用。
第四步:反思tags是否覆盖了你的业务词汇。之前有个“查用户手机号”的技能,tags只写了“phone”,用户在对话里说的是“联系方式”、“电话”,模型就匹配不上,后来把这类同义词都塞进tags,命中率立刻上去了。
5.2 参数被模型“编造”或漏传,怎么兜底
模型编参数的问题永远根治不了,但可以大幅降低。我的三层防线是:
- 描述层的示例:每个参数都带一个example,而且example尽量贴近真实用户语境。模型看到“query: 数据库连接超时排查”这种例子,就会明白这是提取用户原话关键词,而不是自由发挥。
- 校验层的修正反馈:校验不过时,把具体错误原因和修正建议返回给模型,让它重新调用。这比直接报“error”然后终止强得多。
- 数据层的二次确认:如果参数影响较大(比如发送、下单、删除这类操作),在执行前加一个“人工确认”或“模型确认”的中间状态,让系统再次问一句“你确认要把邮件发给xxx吗”。宁可多一步确认,也不要让错误参数直接进入生产系统。
这里有个更细的技巧:参数校验出错时的提示,一定要包含“用户原话”的上下文。比如校验query失败时,把“用户的完整原始输入”拼进修正指令里,模型才知道应该从哪提取关键词。没有这段上下文,模型只能瞎猜。
5.3 一个技能失败后,整个流程卡死怎么办
流程硬编排的一个风险,就是某个节点失败后整条流程停摆。我的策略是给每个节点定义“失败后续动作”:
- 重试:对于网络超时、瞬时错误,自动重试1-2次,每次间隔递增。
- 跳过降级:如果某个技能是锦上添花(比如“生成摘要”),失败后可以直接跳过,继续走主干流程。
- 回退事件:如果失败会波及前面已生效的操作(比如会议室已经锁定,但发送邀请失败),要出一个“回滚技能”把锁释放掉。
- 升级人工:关键节点连续失败两次,自动把任务转给人工处理,并附上完整的上下文日志,这样人工接手时不用满头雾水地重新捋。
实践中,“升级人工”一定要设计得尽量靠前。很多团队怕麻烦,把人工兜底放在最后,结果Agent反复重试好几分钟,用户体验已经崩了才转人工。我的标准是:一次失败立即重试,二次失败直接转人工,不要拖。
5.4 技能类型千差万别,怎么统一“结果对象”
有些技能返回列表,有些返回布尔值,有些返回长文本,如果不统一,编排层处理起来极其痛苦。我在前面提过的标准结果结构再展开说一下:
{ "status": "success|error", "code": "OK|AUTH_REQUIRED|TIMEOUT|...", "data": {}, "message": "面向模型备注", "user_message": "面向用户提示", "meta": { "duration_ms": 234, "retries": 1, "log_id": "abc123" } }这个结构强制所有技能遵守。data里面放真正的业务结果,message给模型看的内部信息(可以包含调试细节),user_message给用户看的友好提示(必须通顺、无技术黑话)。meta里的log_id是审计追踪的锚点,人工排查问题时,靠它串起一条调用链的所有日志。
有了这个统一结构,编排层处理起来就省心多了——不管结果是什么类型,先看status,再看code,最后才碰data。各种前端、监控也能基于同一套字段做埋点和告警。
6. 效果调优:让技能调用从“能跑”到“好用”
技能系统跑通只是第一步,真正让它好用,得靠持续调优。我个人最常用也最有效的调优手段有三个:技能调用评测集、输出缓冲器、技能调用反馈闭环。
6.1 建一个评测集,而不是靠肉眼观察
没有评测集,你根本没法判断一次改版到底是变好了还是变差了。我建议至少准备50条真实用户查询或任务指令,覆盖高频场景和边界情况,然后每次改技能描述、调注册中心策略后,都拿这50条重新跑一遍,记录以下几类指标:
- 技能选择准确率:该调A技能时有没有调A。
- 参数完整率:必需参数是否都传了、格式对不对。
- 任务成功率:整个流程是否最终完成了用户目标。
- 平均调用次数:一次任务平均产生多少次技能调用,过多就说明模型在试探。
- 人工介入率:有多少比例的任务最终转给了人工处理。
评测集里的case要定期扩充,尤其把线上出现的那些“明明调错了但用户没投诉”的case也收进来,它们才是你需要修的真实短板。
6.2 输出缓冲器:给“冲动的模型”装一道阀门
我在一些高成本技能前面加了一层“缓冲器”。原理很简单:模型要调用某个技能时,不直接发出去,而是先询问用户确认。这个交互看起来多了一步,但实际体验反而更好——尤其是那些会产生副作用(发消息、下单、改配置)的技能。
async def confirm_before_execute(result: dict, skill_name: str, params: dict): if skill_name in HIGH_IMPACT_SKILLS: return { "status": "need_confirm", "message": f"确认调用{skill_name},参数为:{params}。是否继续?" } return await validate_and_execute(skill_name, params, context, registry)模型拿到need_confirm后,会生成一条自然语言确认消息:“我准备给张三发送一封邮件,主题是‘项目周报’,确认发送吗?”用户回复确认后再真正执行。这一道确认在“自动执行”和“保护用户”之间找了个平衡点。不是所有技能都需要,影响越大越需要。
6.3 反馈闭环:不要让同一个错误出现三次
最后,也是最容易被忽略的一点——把线上错误和用户反馈回灌到技能系统里。我做了两件小事:
第一,每次技能调用失败时,自动记录“失败原因摘要”,每天汇总一次,看Top 10原因分别是什么。如果是某个特定参数经常缺失,就回写进SkillSpec的description里,加上一句更明确的指引。
第二,用户对结果的反馈(点赞点踩、字数评价、工单挽回记录)也关联到当时调用了哪几个技能上。这样一段时间后,你能清清楚楚看到某个技能的调用虽然“成功”了,但用户最终满意度很低——那说明这个技能的结果还需要调优,而不是换描述能解决的。
这套闭环跑了一个多月之后,Agent的线上任务成功率从70%出头爬到了接近90%。进步不是来自某一次大改动,而是靠着每一次小反馈一点点堆出来的。
7. 团队落地skill工程时的几个关键决策
如果你不是一个人单打独斗,而是在一个团队里推行agent-skills这套思路,有几个人和事的坑值得提前想清楚。
7.1 技能需要“负责人”,而不是公共仓库
很多团队把技能注册中心做成了一个公共仓库,谁都能往里面加技能,结果一段时间后就出现了一堆描述不清、互相交集、甚至废弃没下线的技能。模型面对这些“僵尸技能”,选错率直线上升。
我建议每个技能指定一个明确的负责人,新增技能走“提案+评审”。评审不审代码逻辑(那个常规Code Review会管),而是审:这个技能和现有技能是否重复、描述是否清晰、适用边界是否明确、失败后的提示是否友好。看似增加了一点流程负担,但能防止技能库失控。
7.2 版本迭代时,先灰度再全量
技能描述的改动直接影响模型行为,说变就变太危险。我的习惯是给技能版本号,改动后先在“影子环境”里跑评测集,对比新旧版本的效果。指标不劣于旧版本再上全量,而且要保留回滚通道。
影子环境尤其重要——线上流量直接观察成本太高,评测集至少能先兜住一个大方向。等评测集表现稳了,再放5%的线上流量灰测,最后再全量,一步步来,心里不慌。
7.3 别过度追求复杂框架
我见过一些团队一上来就想搞多Agent架构、规划器、反思机制,把技能系统设计成微服务网格。最后人力全耗在基建上,业务技能没写几个。踏踏实实的建议是:第一阶段只用“技能注册+描述注入+参数校验”就够了;等真实流量中出现编排需求,再引入DAG执行器;等问题确实到了单靠规则编排cover不住时,再考虑模型自主规划。技术栈永远跟着问题走,而不是反过来。
我个人的体会是,agent-skills这个方向的核心功夫不在写那几行执行代码,而在“描述设计”和“边界定义”——你得把自己当成一个翻译官,把业务能力翻译成大模型能看懂、能调用、能组合的语言。这个过程没有捷径,只能靠一轮轮评测、观察和细节打磨。希望你沿着这套思路落地时,能少走一些我们当初绕过的弯路,也祝你那些“总选错技能”“参数编得离谱”的老毛病,早早断根。