“agent-skills”,有人把它看作是Agent框架里的一个插件系统,有人把它理解成Prompt Engineering的升级版,但我在把几个项目推倒重做之后,越来越倾向于一个更朴素的判断:它是把大模型从“只会聊天”推向“真正干活”的那双“手”。
做AI应用的同学应该都有这种体验:模型对话能力再强,一旦要它查个数据库、调个API、算个报表,它就卡住了,要么一本正经地编数据,要么给出一个“步骤说明”让你自己动手。而“agent-skills”要解决的正是这个问题——把一个个可复用的能力封装成技能,让智能体在需要时主动检索、调用、组合,真正完成端到端的任务。
这篇文章我会结合自己的实操经历,聊清楚agent-skills的设计思路、实现一个最小可用闭环的具体步骤,以及我踩过的坑。不管你是在做客服机器人、数据分析助手,还是内部自动化流程,这套方法都有直接的参考价值。
1. agent-skills到底解决了什么问题
1.1 从“会聊天”到“会办事”,Agent缺的从来不是模型
先说个最直观的事。你让一个裸的大模型帮你“查一下上个月华东区的销售额,然后按周生成趋势图”,它大概率会回你一段SQL、一段Python代码,外加一句“你可以把这段代码放到某某环境里运行”。这不是模型不聪明,而是它缺少两个东西:实时访问数据的能力和自动执行动作的出口。
agent-skills的思路,就是提前把这些“能力出口”准备好,用一套统一的规范把它们封装起来。比如:
query_sales_data(date_range, region):查销售数据generate_trend_chart(data, chart_type):画趋势图send_email(to, subject, body):发邮件汇报
当模型发现用户的需求落在某个技能的能力范围内,就调用对应技能,传参、执行、拿结果、再基于结果继续推理。整个过程里,模型负责理解和决策,技能负责可靠地执行,各司其职。
所以我说agent-skills解决的本质问题,是给大模型补齐“行动能力”。它让模型不再只是“建议者”,而变成“执行者”。在实际落地时,这对业务的价值是质的飞跃——一个能自动出周报的Agent,和只能告诉你“周报应该怎么写”的Agent,完全不是同一种产品。
1.2 技能和提示词、工具调用到底有什么不一样
很多同学会问:这跟把指令写进System Prompt里有什么区别?跟用Function Calling又有什么区别?我把三者的差别打成了一张表,方便理解:
| 维度 | 提示词(Prompt) | 工具调用(Function Calling) | Agent技能(Skills) |
|---|---|---|---|
| 复用性 | 一次一写,换个场景就失效 | 可复用,但偏底层接口 | 可复用,且自带触发条件和使用说明 |
| 智能化程度 | 靠模型猜 | 靠模型选,容易选错 | 靠描述+规范+检索,选得准 |
| 粒度 | 粗,整段指令控制 | 细,单次函数调用 | 中,一个完整的能力单元 |
| 可管理性 | 难,一长就乱 | 一般,函数清单越堆越长 | 强,可分类、可启用禁用、可版本管理 |
| 失败处理 | 模型强行编 | 靠代码判断 | 技能自身带校验、重试、降级逻辑 |
说得再直白一点:提示词是“告诉模型怎么做”,工具调用是“给模型一个函数”,而agent-skills是“给模型一本操作手册+一个工具箱”。手册里写清楚每个工具什么时候用、什么时候不要用、参数该怎么填、返回结果怎么解读、失败了怎么办。模型从“猜着用”变成了“看着手册用”,准确率和稳定性自然就不一样了。
2. 一套实用的技能库该怎么设计
2.1 技能单元的骨架:元数据、执行体和说明书
我见过不少团队的技能库,最初就是一堆乱七八糟的函数堆在一起,模型经常选错。后来发现,任何一个合格的技能单元,都必须包含三块东西:元数据、执行体、说明书。
元数据就是技能的名片,通常包括name、description、parameters三个字段。description极其重要,它不是给人看的注释,而是给模型看的“触发指南”。我习惯在描述里写清楚三件事:这个技能负责什么、什么时候优先调用、什么时候千万别调用。
执行体是技能真正干活的代码,可以是一个Python函数、一个Shell脚本、一次HTTP请求,甚至是一段编排好的多步工作流。执行体要尽量保持“黑盒”——外部只看得到输入输出,内部怎么实现无所谓。
说明书则负责兜底,它包含技能的前置条件、使用示例、返回格式、异常约定等。说明书不一定喂给模型看,但一定要给调用框架看,用来做参数校验、错误提示和结果解析。
下面是我常用的技能定义结构,用JSON做元数据,用Python函数做执行体:
{ "name": "query_sales_data", "description": "查询指定时间范围和区域维度下的销售明细汇总数据,适用于基于真实数据回答销售额、订单量、环比等指标类问题,不要用此技能生成虚构的销售数据", "parameters": { "type": "object", "properties": { "date_from": {"type": "string", "description": "起始日期,格式YYYY-MM-DD"}, "date_to": {"type": "string", "description": "结束日期,格式YYYY-MM-DD"}, "region": {"type": "string", "enum": ["华东", "华北", "华南", "西南"], "default": "全国"} }, "required": ["date_from", "date_to"] } }对应的执行体就是一个普通函数:参数严格从JSON Schema里来,返回值统一用字典包一层状态码和数据:
def query_sales_data(date_from: str, date_to: str, region: str = "全国"): # 内部调用数据仓库/业务API,详细实现略 result = warehouse.query(...) return {"status": "success", "data": result, "unit": "元"}2.2 技能设计的三条铁律
第一条铁律:一个技能只做一件事。我见过有人写了个handle_customer_request,里面又查订单又发优惠券又更新标签,结果模型根本不知道该不该用它,因为描述没法精确刻画它的边界。拆成query_order、apply_coupon、update_user_tag三个技能,模型的选择准确率明显上升。
第二条铁律:描述写得像给新同事的工作交接文档。不要写“查询数据的函数”,而要写“当用户想了解某段时间内各区域销售额时可调用,注意仅返回已支付订单数据,不包含退款单;如果用户询问退货情况,请改用query_refund_data”。模型是靠描述来匹配意图的,描述越具体,选错概率越低。
第三条铁律:参数必须严格校验,绝不信模型的两张嘴。模型在抽取参数时偶尔会编值,尤其枚举型参数容易传错。所有参数在进入执行体之前,先过一遍JSON Schema校验,不符合直接报错,不要试图在函数里做“猜测式修正”。
3. 从零实现一套agent-skills的最小闭环
3.1 先手写闭环,再上框架
第一次接触agent-skills时,我直接上了LangChain之类的框架,技能注册倒是方便,可一旦出了Bug,就会被框架的抽象层挡住,排查很痛苦。后来我把框架丢掉,从裸的模型API开始手写了一套最小闭环,跑通之后再去理解框架,一切都清晰了。
一套最简闭环大概长这样:
- 模型接收用户输入,判断是否需要调用技能
- 如果需要,从技能库里按语义相似度或模型能力检索候选技能
- 把候选技能的描述和参数Schema拼进Prompt,让模型选择技能并抽取参数
- 对参数做校验和权限检查
- 调用技能执行体,把结果返回给模型
- 模型基于结果生成最终回答,或决定继续调用下一个技能
3.2 关键代码实现:一个可运行的技能调用循环
我用Python实现一个极简版本,只依赖requests和标准库,方便你直接抄去改。先定义技能注册表:
SKILL_REGISTRY = {} def register_skill(schema, handler): SKILL_REGISTRY[schema["name"]] = { "schema": schema, "handler": handler } # 用上面的JSON Schema注册一个技能 register_skill(query_sales_data_schema, query_sales_data)然后实现参数校验,我直接用jsonschema这个库,省得自己写一堆if-else:
pip install jsonschemaimport jsonschema def validate_parameters(schema, params): try: jsonschema.validate(params, schema["parameters"]) return None except jsonschema.ValidationError as e: return f"参数校验失败: {e.message}"接着是核心的“意图识别+参数抽取”函数。这里以兼容OpenAI格式的模型接口为例,实际用哪个模型无所谓,原理一致:
def choose_and_call_skill(user_input, available_skills): # 1. 组装技能描述 skills_text = "\n".join( f"技能名: {s['schema']['name']}\n" f"说明: {s['schema']['description']}\n" f"参数: {json.dumps(s['schema']['parameters'])}" for s in available_skills.values() ) messages = [ {"role": "system", "content": f"你是技能调度助手。根据用户需求,从以下技能中选择一个并输出JSON,格式为{{\"skill\": 技能名, \"params\": 参数对象}}。若没有匹配技能,输出{{\"skill\": \"none\"}}。\n\n{skills_text}"}, {"role": "user", "content": user_input} ] # 2. 调用模型(示意代码,请求参数按实际模型调整) resp = requests.post( MODEL_ENDPOINT, headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": MODEL_NAME, "messages": messages, "response_format": {"type": "json_object"}} ) decision = resp.json()["choices"][0]["message"]["content"] decision = json.loads(decision) # 3. 执行调度 if decision["skill"] == "none": return "没有可用技能处理该需求" skill = available_skills.get(decision["skill"]) if not skill: return f"技能 {decision['skill']} 不存在" err = validate_parameters(skill["schema"], decision.get("params", {})) if err: return err # 4. 执行并返回结果 result = skill["handler"](**decision["params"]) return result实际落地时,第2步的模型输出不一定严格是JSON,建议用正则把{...}部分摘出来再解析,或者在Prompt里要求“只输出JSON,不要解释”。这一步看着小,实训时特别容易翻车。
3.3 别让Agent裸奔:权限、审计和降级
很多人的Agent在Demo里跑得好好的,一放生产就出事,核心问题不是模型变笨了,而是权限边界失控。我的经验是,给每个技能打上权限标签:只读、内网写、外网发、高危操作等;在调用前做一次检查,高危操作必须二次确认。
代码里可以这样处理,给技能定义加一个permission字段:
{ "name": "send_email", "permission": "high_risk", "description": "发送邮件给指定收件人,仅当用户明确要求发送时才调用", "parameters": {...} }调用时判断:
def check_permission(skill_name, session_user): permission = SKILL_REGISTRY[skill_name]["schema"].get("permission", "read") if permission == "high_risk" and not session_user.get("confirmed"): return {"status": "need_confirm", "message": "该操作风险较高,请用户确认"} return None我强烈建议所有的技能执行都保留日志,每一条调用记录下“谁在什么时间请求了什么技能,参数是什么,结果是什么”。排查问题时,这比翻模型对话记录高效得多。
另外,所有外部依赖都可能有超时,技能执行体里务必加超时机制。我一般用functools配合线程池,或者直接用requests的timeout参数。宁可让Agent告诉用户“系统忙,稍后再试”,也不能让它卡在那里空等。
4. 实操里的高频问题与排查技巧
4.1 Agent调用了技能,但结果就是不对,怎么排查
这个问题出现过太多次了。我总结了一个排查链路:
- 先看技能返回有没有被模型“加工”:模型拿到技能结果后,经常忍不住润色,润色就可能出错。建议重要数值类结果直接让模型“不要复述,原样引用”,或者在Prompt里要求它引用结果时保留原字段名。
- 检查错误信息是否被当成业务数据:有些技能出错时返回
{"status": "error", "message": "..."},如果模型没识别status字段,会把错误详情当答案告诉用户。解决方法是,在设计技能时统一返回结构,同时在Prompt里强调“status为error时必须说明执行失败并给出原因”。 - 翻看技能调用日志:对比模型传入的参数和实际执行的参数,很多“结果不对”其实是最开始抽取参数就错了。
我给大家一个排查案例。有一次Agent查库存,用户说“查一下北京仓的口罩库存”,模型抽出的参数是warehouse = "北京"、sku = "口罩",但技能要求仓库存的是仓库编号,结果返回空。这个问题不在模型,而在技能描述里没写明“仓库参数必须使用编号,如BJ001”。改完描述,准确率立刻上来了。
4.2 模型总选错技能,怎么办
选错技能九成是描述没写清。我试过最快见效的两个办法:
- 在描述末尾加“不要使用”清单。比如
query_sales_data这个技能,我在描述后面加了一句:“不要用它查询退款单数据,退款问题请使用query_refund_data”。模型选错的概率立刻降了一截。 - 给容易混淆的技能加使用场景样例。比如“当用户输入包含‘周报’、‘汇总’、‘趋势’等词时,优先选择query_sales_data”,用直接示例给模型锚定意图。
如果调整描述后还是选错,再考虑用Embedding做语义检索,从技能库里先召回Top 3候选,再让模型从候选里选。这相当于在模型决策前加了一道粗筛,可靠性会高很多,代价是接口调用多一次。
4.3 技能越来越多,上下文被撑爆了怎么管
技能库从几个涨到上百个时,把全部技能描述塞进Prompt会占掉大量上下文窗口。我建议做两层管理:
第一层,给技能分类打标,比如“数据分析”“客户管理”“消息通知”“运维操作”。先用一个轻量分类模型或Embedding把用户输入映射到某几个分类,只加载这些分类下的技能。
第二层,动态精简技能数量,每类下热门技能保留,冷门技能可以采用“技能树”的方式,先把父技能暴露出来,模型确定要用了,再加载子技能列表。这有点像导航站点,先到分类页,再进详情页,而不是一次性把所有链接都平铺出来。
4.4 稳定运行的底线:超时、重试和限额
技能执行是真实世界里的动作,数据库会抖动、API会限流、文件会被占用。我踩过最惨的一次是批量调用外部短信接口,对方限流,结果Agent一遍遍地重试,把额度刷光了还发出去一堆重复短信。从那之后我立了规矩:
- 所有外部调用必须有超时时间,超时就按失败处理
- 失败重试最多2次,且用指数退避,间隔至少翻倍
- 重试必须判断错误码,只有可重试错误才重试,参数错误和权限错误直接放弃
- 对高消耗技能(短信、邮件、支付)设置每日/每小时限额
用代码框架来说,就是在调用执行体时加一个统一的包装器,把所有技能的异常、超时、重试逻辑集中管理,而不是让每个技能函数自己处理。
5. 再往前走一步:技能组合、评测与共享
5.1 从单个技能到“技能链”
单个技能只能完成单一动作,真正复杂的业务往往需要多个技能接力。比如“生成销售周报并发给相关人员”这个场景,需要:查询销售数据、生成图表、生成文档、发送邮件,这就是一条技能链。
我实现的编排方式很简单,用一份配置文件描述步骤和依赖:
{ "name": "weekly_sales_report", "steps": [ {"task": "query_sales_data", "params": {...}}, {"task": "generate_chart", "params": {"source": "step1.result"}}, {"task": "compose_email", "params": {"chart": "step2.result"}}, {"task": "send_email", "params": {"content": "step3.result"}} ] }框架拿到这份配置后,按顺序调度,上一步的结果自动作为下一步的输入。这里的关键点在于,每一步之间的数据传递要约定好格式,我统一用JSON字典传参,每个技能出参都带一个data字段,下一步只取data内容,避免把模型附近的文本或调试信息误传进去。
技能链能不能跑得顺,核心是每一条链都至少跑通一次真实场景,别指望配置写完就万事大吉。
5.2 怎么评测一个技能库的好坏
技能库不是“能用就行”,它需要持续迭代和回归验证。我给自己定了一套指标:
| 指标 | 说明 | 目标参考值 |
|---|---|---|
| 技能命中率 | 测试请求中正确选中技能的比例 | 大于90% |
| 参数抽取正确率 | 抽取出的参数与标注是否一致 | 大于85% |
| 任务完成率 | 端到端任务走通的比例 | 大于80% |
| 失败回退率 | 技能失败后Agent能兜底处理的比例 | 越高越好 |
| 平均调用轮次 | 完成一个任务平均对话轮数 | 越小越好 |
评测集我一般从真实对话记录里截取500条,人工标注“期望技能+期望参数”,然后定期回归跑一遍。只要改技能描述或新增技能,就重跑一次看指标变化,防止“修好一个技能、带崩三个技能”。这一步很多人嫌麻烦不做,实际上恰恰是Agent质量稳定最值得投入的部分。
5.3 技能的打包与团队共享
当团队里多个项目共用技能库时,我建议把每个技能做成独立目录,包含一个YAML元数据文件、一个Python执行体文件、一份README和一组测试用例,整体走Git管理,版本打Tag。结构大致如下:
skills/ query_sales_data/ skill.yaml handler.py README.md tests/ test_parameters.json这样每个技能可以独立测试、独立发布,团队里其他人拉到代码就能注册使用。技能描述写得好的话,一个几十人团队可以沉淀出一套相当可观的内部技能库,新来的同学直接面向技能库开发,不用再重复踩“模型又瞎编数据”的坑。
我见过有的团队更进一步,搭了内部技能市场,技能上传后自动跑一遍评测集,分数合格才能上架。到了那个阶段,Agent开发就真的从“写死逻辑”进化成了“装配能力”。
我个人的体会是,agent-skills看着是个技术概念,实质是一种工程思路的转变:不再死磕模型的推理能力,而是把精力花在给模型配好一套可信赖、可管理、可评测的能力集合。先从小场景入手,跑通闭环,再逐步扩充技能数量和组合复杂度,稳定前进。
最后分享一个我调试时最爱用的小技巧:每个技能的描述里,故意写上一两句“千万不要”的负面示例。比如“此技能不能用于未支付订单退款计算,相关需求请调用query_refund_data”,反向约束比正向描述更能降低模型的误选率。你不要觉得这是废话,实测下来这个改动往往能让命中率提升好几个百分点。