最近好几个朋友跑来问我同一个问题:为什么我照着网上教程搭的 Agent,只会跟我聊天?让它帮忙查一下快递到哪了、把项目里的代码按模块梳理一遍、每天定时把周报发到群里,它要么答非所问,要么干到一半就卡住。我说这不怪你,因为你搭的不是 Agent,是套着 Agent 外壳的 Prompt 聊天框。真正的 Agent 和应用层之间,隔着一层很多人忽略的东西——技能层。agent-skills 这个概念,说的就是这件事。
Agent 技能体系不是一个新框架、不是一个新模型,而是一整套让大模型“学会干活”的组织方式:怎么定义技能、怎么注册技能、怎么让模型在合适的时机调用合适的技能、怎么管理技能之间的协作和演化。这篇文章我就拿我自己维护 agent-skills 这套体系的实战经验,把从零到一的过程拆开讲清楚。不管你是想把 Agent 接入真实业务、给团队搭建内部智能助手,还是纯粹想搞清楚“怎么让模型真正干成事”,这篇都值得往下看。
1. 为什么 Agent 会卡在“能聊天不会干活”:技能体系要解决的真实痛点
先聊一个大家可能都踩过的场景。你给 ChatGPT 或者某个开源模型套了一个壳,接上了 API,然后发现它很擅长“说”但不擅长“做”。你跟它说“帮我分析这份销售数据”,它回你一段长篇大论,告诉你应该怎么分析,但不会真的去读你那个 Excel 文件。你加了个 Function Calling,教它一个read_excel函数,它确实能调用了,但一旦任务变复杂,比如“把这三张表比对一下,找出异常订单,然后生成一份带图表的报告”,它就乱了:要么不调函数,要么同时调用五六个函数把参数传得乱七八糟。
这个问题的根源在于:我们一直在教模型“调用工具”,却没有给工具建立体系。零散的函数调用就像把一堆电器扔在房间里却不装插座、不布线,模型拿到一个工具列表,只能靠猜。而 agent-skills 做的事,是把这些工具组织成一套“技能库”,让每个技能有清晰的名字、边界、输入输出模板和调用说明,再配一个调度层去决定什么时候用什么技能。
1.1 大模型原生能力的边界决定了技能层必须存在
大模型再强,它原生能力也有限制。第一,它的知识截止到训练数据,查不了实时信息;第二,它没有手,做不了任何物理世界或系统层面的操作;第三,它的上下文窗口是有限的,你不可能把一段完整业务逻辑塞进 Prompt 里让它照做。所以 Agent 必须“外挂能力”,这是共识。但外挂能力的方式很讲究——你当然可以把每个工具都写成一个函数塞进去,但当工具从 5 个涨到 50 个,模型的选择就变得非常不稳定。
我在项目初期犯过一个典型错误:给 Agent 挂了十几个 Python 函数,包括查数据库、发邮件、读文件、调接口。结果模型经常在发邮件的时候去调用读文件那个函数,或者把数据库查询的参数传成文件路径。不是说大模型笨,而是工具本身没有“使用说明”的边界,模型只能从一堆名字里猜。技能体系的核心作用,就是把这种“猜”变成“有依据的判断”。
1.2 Prompt 堆砌与技能资产的矛盾
也有人会说,那我用 Prompt 把每个工具怎么用写清楚不就行了?我试过。给模型写了一个 5000 字的系统提示词,里面详细描述了每个工具的用法、注意点、参数格式。代价是什么呢?第一,Token 开销巨大,每次对话都带着这 5000 字,成本肉眼可见地涨;第二,模型对长提示词的服从度会随着关键信息的位置变化而波动,放在前面的容易注意力衰减,放在后面的容易被截断;第三,你新增一个工具,就得改一次 Prompt,改完还不能保证旧的工具描述没被挤掉。
更本质的问题是,Prompt 里的工具描述和真实执行层之间缺乏绑定关系。模型在 Prompt 里看到的是一段文字描述,它自己决定要不要调用,但这个描述和实际函数可能已经不一致了——你更新了函数的参数,忘了改 Prompt,模型照着旧格式传参,直接报错。技能体系把这个关系显式化:技能注册的时候自动生成描述,执行的时候走统一调度,描述和执行永远由同一份元数据驱动。
1.3 技能体系的三个核心收益
归纳下来,我认为一套完整的 agent-skills 体系能带来三个别的方案给不了的好处。第一是可控性:每个技能有明确的输入输出契约,模型的自由度被限制在“选择哪个技能”而不是“自由发挥参数”,大幅降低乱调用的概率。第二是可维护性:技能是独立注册、独立更新的资产,加一个新技能不用动系统提示词,改一个旧技能也不用担心影响其他功能。第三是可组合性:复杂任务可以被拆成多个基础技能的组合,Agent 以编排的方式复用技能,而不是每次重新定义行为。
说实话,这三个收益听起来抽象,但你在连续维护一个 Agent 三周之后就能真切感受到。当你的技能数从个位数涨到几十个,没有一套体系兜底,光靠 Prompt 和函数列表根本撑不住。
2. 技能的定义:agent-skills 如何把“抽象能力”变成“可复用资产”
聊完为什么,接下来是项目中最关键的设计问题:一个技能到底长什么样。很多人以为技能就是一个函数加上一段描述,但真正放进 agent-skills 体系里的技能,至少需要五层信息:姓名(name)、说明(description)、参数(parameters)、返回值(returns)、示例(examples)。
这五个字段缺一不可。name 是模型识别技能的主键,description 是模型理解技能适用场景的依据,parameters 是调用时的“合同”,returns 是模型判断执行结果是否可用的依据,examples 则是模型学会这个技能的最短路径。我把这套模板固定下来之后,一个典型的技能定义大概长这样:
{ "name": "query_logistics", "description": "查询电商订单的物流轨迹。当用户询问订单物流状态、快递到哪了、什么时候能到,或提供运单号要求追踪时使用。", "parameters": { "type": "object", "properties": { "tracking_id": { "type": "string", "description": "快递运单号" }, "courier": { "type": "string", "enum": ["顺丰", "圆通", "中通", "德邦"] } }, "required": ["tracking_id"] }, "returns": { "type": "object", "properties": { "status": { "type": "string", "enum": ["in_transit", "delivered", "exception"] }, "current_location": { "type": "string" }, "estimated_delivery": { "type": "string", "description": "预计送达时间" } } }, "examples": [ { "input": "我的顺丰快递 SF1234567890 到哪了", "output": "该技能被调用,tracking_id=SF1234567890" } ] }2.1 技能说明为什么是决定成败的字段
很多人写技能的时候只写一句“查询物流”,然后就完事了。但模型要做的不是一个简单的匹配,而是在用户的自然语言里判断该不该调用这个技能。你给它的说明越精准,它的判断就越准。
我总结出一个原则:技能描述要写“触发条件”,不要只写“功能定义”。“查询物流”是一句功能定义,而上面那个 description 里的“当用户询问订单物流状态、快递到哪了、什么时候能到,或提供运单号要求追踪时使用”,是触发条件。功能定义告诉模型“我能做什么”,触发条件告诉模型“什么时候该做”。后者才是模型真正需要的。
这个细节我是在一次线上事故里领悟的。当时我给一个客服 Agent 定义了查询订单技能,description 只写了“查询用户订单信息”。结果用户说“我找不到我的订单记录”,模型也去调这个技能,其实用户是想反馈页面 Bug。加了“仅当用户需要查看订单详情、状态、金额时使用;订单页面本身的问题不要调用”之后,误调用率明显下降。还是那句话,模型不是不聪明,是你不给它足够的信息。
2.2 参数设计与模型友好度
参数 Schema 的设计直接决定了技能的鲁棒性。我在 agent-skills 里要求所有参数必须给出 description,哪怕只有一个参数也要写。为什么?因为模型在决定参数值的时候,需要一个语义锚点。比如说两个技能都有date参数,一个是“发货日期”,一个是“到货日期”,如果 description 不写清楚,模型很容易把用户的“收货时间”传进“发货日期”那个技能。
还有两个细节值得注意。第一,能用 enum 约束的参数尽量用 enum,模型在有限选项里选择远比自由填空准确。第二,不要把太多逻辑参数暴露给用户。比如query_logistics里其实还有一个verbose参数控制返回信息的详细程度,我把它设成默认值,故意不暴露在 Schema 里。暴露太多非业务参数,只会增加模型困惑和出错的机会。
2.3 返回值设计:给模型的“反馈回路”
returns 这个字段很多设计者会忽略,但它其实非常重要。Agent 调用技能之后,模型需要根据返回值决定下一步动作。如果返回结构不稳定、不可预测,模型的后续判断就是无源之水。
我在设计 returns 时给自己定了一个规矩:每个技能返回值的第一层结构永远固定,业务数据放在固定的子字段里。比如查询物流,永远返回{status, current_location, estimated_delivery, raw},即使查询失败也返回{status:"exception", message:"运单号不存在"}。这样模型读到status=exception就知道该向用户解释错误,而不是继续瞎猜。
3. 技能库的工程化落地:注册、发现与调度
有了技能定义模板,下一步就是把技能放进一个可持续运转的系统里。这一章节是整个 agent-skills 工程实现的核心,也是最容易出幺蛾子的地方。
3.1 技能注册中心:为什么不能手动维护
项目初期我是手动管理技能列表的——写一个skills.py,把所有技能函数和描述塞进一个列表。技能只有十几个的时候没问题,等技能开始变多,问题就来了:描述和函数之间靠人眼对齐,特别容易漏;改了函数签名忘了更新描述,模型还在按旧的文档猜;技能之间有重复描述,模型经常选错。
所以我用了一个很简单的注册装饰器模式,把技能定义和执行函数绑定在一起。大概长这样:
SKILL_REGISTRY = {} def register_skill(skill_meta): def decorator(func): skill_meta["handler"] = func SKILL_REGISTRY[skill_meta["name"]] = skill_meta return func return decorator @register_skill({ "name": "query_logistics", "description": "查询电商订单的物流轨迹。当用户询问订单物流状态、快递到哪了……", "parameters": {...}, "returns": {...}, "examples": [...] }) def query_logistics(tracking_id: str, courier: str = None): # 实际执行逻辑 return {"status": "in_transit", "current_location": "杭州转运中心", ...}这样技能描述和函数永远在一个文件里,改的人不会漏,注册中心自动生成。更重要的是,模型看到的技能列表是从SKILL_REGISTRY动态生成的,加技能不用改任何其他文件,减技能直接注释掉装饰器就能从可见列表里消失。
3.2 技能调度器:模型提出“意图”,系统落到“执行”
技能调度是 agent-skills 里最像“操作系统内核”的部分。它的职责有三块:第一,把用户请求和可用的技能列表交给模型,让模型选择要调用的技能;第二,对模型输出的技能选择做校验和参数补全;第三,执行技能并决定结果是直接返回给用户还是继续交给模型做下一步推理。
我的调度器核心逻辑简化后是这样的:
def dispatch(user_query, available_messages): # 1. 模型根据技能列表决定调用哪个技能 decision = llm.choose_skill( user_query=user_query, skills=[s for s in SKILL_REGISTRY.values()], # 这里只给模型看元数据,不暴露真实函数实现 ) # 2. 校验模型输出 if decision.name not in SKILL_REGISTRY: return "技能不存在,请重新选择" skill = SKILL_REGISTRY[decision.name] # 3. 参数校验与默认值补全 params = validate_and_complete(decision.arguments, skill["parameters"]) # 4. 执行 try: result = skill["handler"](**params) except Exception as e: result = {"status": "error", "message": str(e)} # 5. 把结果交给模型,让模型决定下一步 return llm.compose_response(user_query, result)这里有个很容易踩的坑:技能列表不可全量塞给模型。当技能超过 50 个,把这些技能的完整描述全放进上下文,Token 占用大,模型选择准确率也开始下降。我的做法是先做一轮粗筛选:用 embedding 把用户请求和技能的 description 做相似度排序,只把 Top 10 个候选技能给模型做精细选择。这一步看似多绕了弯,实测把技能选择准确率从 78% 拉到了 93%。
3.3 技能编排:单体技能与组合技能的分工
在实际业务里,用户的需求往往不是单个技能能解决的。比如“用户要退货”这个流程,背后可能是验证订单+生成退货单+通知仓库三个技能的组合。agent-skills 把这种组合也建模成一种技能——组合技能。组合技能自身的 handler 是一个编排逻辑,它内部会再次调用调度器,把子技能串起来。
组合技能和单体技能的区别在于:单体技能是原子的,执行完即返回;组合技能里有状态流转,可能有分支、有回退、有超时。我的经验是,组合技能的数量要克制,不要把每个小流程都做成组合技能,否则调度层会变成一锅粥。一个合理的判断标准是:当同样的组合逻辑被复用了三次以上,再把它固化成组合技能。
4. 从零构建一个带技能库的 Agent:代码级实战拆解
前面讲了不少设计原则,这一节我们真刀真枪地跑一个最小可用的实现。我自己维护的 agent-skills 项目就是从这个最小框架长出来的,你跟着做一遍,就能在自己的代码里复现整套体系。
4.1 最小可运行框架:技能加载与模型选择
先搭骨架。我用 Python 写一个类AgentSkillSystem,初始化时加载注册中心,然后提供dispatch方法。下面是核心代码:
import json from typing import Any, Callable, Dict, List, Literal import openai class AgentSkillSystem: def __init__(self, llm_client, embed_client=None): self.registry: Dict[str, dict] = {} self.llm = llm_client self.embed = embed_client # 可选,用于技能粗筛选 def register(self, meta: dict, handler: Callable): meta["handler"] = handler self.registry[meta["name"]] = meta def rank_skills(self, query: str, top_k: int = 10) -> List[dict]: """粗筛选:没有 embedding 客户端时直接全量返回""" if not self.embed: return list(self.registry.values())[:top_k] query_vec = self.embed.embed(query) scored = sorted( self.registry.values(), key=lambda s: -cosine_similarity(query_vec, self.embed.embed(s["description"])) ) return scored[:top_k] def dispatch(self, user_query: str) -> str: # 1. 粗筛选候选技能 candidates = self.rank_skills(user_query) # 2. 构造模型选择 prompt skill_prompt = [ {"name": s["name"], "description": s["description"], "parameters": s["parameters"]} for s in candidates ] decision = self.llm.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": "你是技能调度器。根据用户请求从候选技能中选择一个并给出参数。" }, { "role": "user", "content": f"候选技能:\n{json.dumps(skill_prompt, ensure_ascii=False)}\n用户请求:{user_query}" } ], response_format={"type": "json_object"} ) parsed = json.loads(decision.choices[0].message.content) skill_name = parsed.get("skill") arguments = parsed.get("arguments", {}) if skill_name not in self.registry: return "没有匹配到可用技能" skill = self.registry[skill_name] # 这里可以做参数校验和默认值填充,先省略精简展示 try: result = skill["handler"](**arguments) return json.dumps(result, ensure_ascii=False) except Exception as e: return f"技能执行失败: {e}"这里刻意把模型选择技能的过程放进了 Prompt,而不是直接用 Function Calling,原因是当我需要控制候选技能列表、做统一调度、或者切换不同模型厂商时,这套方法更通用。如果你用 OpenAI 的 Function Calling 也能做,但绑定较紧,切开源模型时很被动。
4.2 技能执行器:参数补全与错误兜底
实际项目里,模型输出的参数往往是不完整的。比如用户说“帮我查一下快递”,没说单号——模型可能就不调用技能了,或者直接报错。这时候执行器需要做两件事:参数补全和错误兜底。
参数补全的思路是:模型给出部分参数后,执行器先检查required字段是否齐全,缺了的字段要么向用户追问,要么从对话上下文里推断。我倾向于让模型先自己判断是否能从上下文补全,如果不行再返回一个“缺少参数”的信号,由主流程触发追问。
错误兜底更关键。技能调用异常、外部接口超时、数据解析失败,都是高概率事件。我要求技能 handler 务必返回结构化的{"status":"error","message":"..."},而不是抛出异常让调度器裸奔。这样模型拿到 error 状态就知道该怎么说、怎么处理,而不是一头雾水地编造一个错误原因。
4.3 实测效果:同一套技能在不同业务场景下的表现
框架搭好之后,我拿它跑了两个差异很大的业务。一个是电商客服场景,另一个是内部项目管理的助手场景。电商客服用了十几个技能:查物流、退换货、开票、优惠券查询等;项目管理助手用了另外十几个技能:建任务、查进度、分配负责人、生成周报。
同一个调度器、同一套注册机制,两边跑起来都很顺。有一个数据特别明显:在只有函数列表、没有技能体系的旧版本里,用户的非标准表述(比如“我那个单子到底发没发?”)触发正确技能的概率大概只有 65%;换成 agent-skills 体系后,正确率到了 91% 以上。那 9% 的失败,基本都是用户表述过于隐晦、参数确实缺失导致的,模型选择本身出错的概率小了很多。
5. 藏在细节里的坑:技能演进中的常见问题与排错链路
哪怕体系设计得再合理,真用起来还是会有各种意想不到的问题。这一节我把踩过的最有代表性的几个坑写出来,希望能帮你省几周的调试时间。
5.1 技能描述与实测行为不一致:排查的关键链路
第一个坑典型的表现是:模型明明选择了正确的技能,但执行结果就是不对。params 传得不对、执行逻辑有 bug、返回格式乱了,原因往往深藏在“描述层”和“执行层”的错位里。
我第一次遇到这个问题是在一个query_stock技能上。技能描述写的是“查询库存数量”,函数逻辑也确实返回了库存数。但模型调用后返回的结果在对话里总显示“没有库存”,排查了很久才发现,函数返回的字段叫available_stock,而模型在读取结果时习惯性找stock字段,没找到就当成了零。
这件事给我们的教训是:技能描述、参数 schema、返回 schema 里的字段命名必须做到自解释,并且要在 examples 里明确展示返回结构。文档里多写两行示例,比上线后猜模型为什么看错字段要省事一百倍。排查这类问题的链路通常是:先看调度日志里模型选择的技能名,再看传入参数,再看返回结果,三步都正常的话,问题基本都出在字段名对齐上。
5.2 技能冲突:多个技能描述重叠时的优先级设计
第二个坑是技能描述重叠。当你的技能库里同时有查询订单和查询物流,用户问“我的订单到哪了”,模型就容易犹豫。这俩技能描述的触发条件有交集,模型一会儿选前者、一会儿选后者,表现忽好忽坏。
处理这类冲突,我的经验不是去调模型,而是从源头缩小重叠。先分析两个技能的真实区别是什么,把区分度写进描述里。查询订单回答的是“订单本身有什么商品、多少钱、什么状态”,查询物流回答的是“货发出来没有、走到哪了、什么时候到”。然后在每个技能描述的最后加一句“不要用于查询xxx”,把边界画清楚。
如果两个技能的重叠是无法消除的,那就需要配置技能优先级。agent-skills 的注册中心里可以给技能加priority字段,调度器发现模型对两个候选技能犹豫时,直接按优先级落地。优先级怎么定?一个简单规则:越是业务上“收口”的技能,优先级越高。比如查询物流比查询订单更具体,在同义词场景下优先。
5.3 技能版本管理:一次 schema 变更引发的连锁事故
第三个坑来自技能版本演化。有一次我把create_task技能的参数从assignee_email改成了assignee_id,调用方从内部系统换成了用户 ID 体系,然后漏改了另一个依赖它的组合技能。结果用户明明在界面上选了人,任务却建不出来,报错信息指向参数缺失。
这事的根源是技能之间有依赖关系,而我当时没有做版本提醒。现在我在 agent-skills 里增加了一个简单的依赖声明:每个技能可以声明自己依赖哪些技能,注册变更时系统自动检查下游技能是否需要同步更新。改动参数的技能会被标记为breaking_change,所有依赖它的技能必须在变更单里关联记录。
这个机制不复杂,但非常重要,尤其是团队的 Agent 技能库不止一个人维护的时候。技能一旦被复用,它就不再是你一个人的私有函数,而是团队共享的接口,必须有契约意识。
6. 从技能库到技能生态:Agent 之间如何共享能力
单体 Agent 的技能体系做好之后,我自然而然想到一个问题:这套技能能不能跨 Agent、跨团队复用?答案是能,但需要先解决标准化的问题。
6.1 技能标准化的三个方向
目前我在 agent-skills 里实践的标准包括三层。第一层是技能描述规范:写触发条件但写边界、固定五个字段、强制 examples。第二层是参数类型规范:能用 enum 用 enum,字段名统一 camelCase 或 snake_case,不要混用。第三层是执行协议规范:返回结构必须包含status字段,错误信息必须可读,所有技能必须支持超时机制。
这三层规范看起来是文档工作,但实际影响非常大。我见过一些开源项目里技能定义五花八门,有的用字符串参数,有的直接传对象,有的返回结果没有状态码,导致 Agent 根本没法做后续判断。标准化的意义在于:当技能描述变成一种“语言”,不同 Agent 之间就能理解彼此的技能。
现在把 agent-skills 做成一个共享包之后,我在团队里做了个实验:运营组维护的业务技能,可以直接被客服组的 Agent 调用,因为两边遵循同一套注册规范,Agent 的调度逻辑不需要开发介入。
6.2 从个人技能库到团队级技能市场
再往前走一步,就是技能市场。想象一下,一个组织里已经沉淀了成百上千个技能:查物流、查库存、算薪资、生成合同、分析评论……如果每个新项目都从零开始造一套技能,那是巨大浪费。但如果组织内有一个技能市场,按领域、按权限共享,新 Agent 启动的时候只需要拉取相关技能,然后用自然语言描述一名协作目标,系统就能自动选择合适的技能组合。
这个话题已经超出代码层面了,涉及到组织层面的知识资产管理。但技术底座其实已经有了,就是那套统一的技能描述、注册和调度协议。我自己的体会是,agent-skills 这条路走到后面,真正值钱的不是代码,而是你沉淀下来的那批“会干活”的技能资产——模型会过时,框架会迭代,但一套经过业务锤炼的技能库,是可以长期复用的。
最后再分享一个小技巧:技能库里尽量保留“最小技能”的思维,每个技能只做一件事,做熟练了再考虑组合。你可能会觉得组合技能省事,但复杂组合技能的调试成本是指数级上升的。宁可让调度器多编排几步基础技能,也不要把一个技能做成“瑞士军刀”。这是我维护 agent-skills 一年多下来,最想对你说的经验。