做AI Agent开发这段时间,我最大的感触就是:模型能力决定Agent的下限,技能库决定Agent的上限。让大模型“说”不难,难的是让它“做”——去做检索、调接口、改文件、跑报表。而这一切的前提,就是得有一套设计良好的agent-skills。很多人把技能库当成简单的“工具列表”,随便写几个函数丢给模型就完事,结果Agent老是选错工具、传错参数、答非所问。今天我不聊大而全的框架,也不贴一堆官方文档,就围绕agent-skills这个话题,把技能库应该怎么设计、怎么写、怎么测、踩过的坑有哪些,从头到尾捋一遍。这篇内容适合正在做智能体应用、AI自动化流程、垂直领域Agent的开发者,也适合刚入门但想少走弯路的朋友。
1. Agent Skills是什么:先搞懂技能库到底解决什么问题
1.1 技能、工具与插件:先厘清三个高频混淆概念
我经常在技术群里看到有人把Tool、Plugin、Skill混着说,其实它们不是一个层面的东西。Function Calling是模型输出结构化调用指令的一种协议,它本身不落地任何能力;Tool指的是一个可执行的函数或接口,比如“搜索代码”、“发HTTP请求”,粒度一般比较小而直接;Plugin更偏产品形态,通常是一整套扩展能力的打包,比如IDE插件、浏览器插件。
Skill则是更强调整体能力的东西,它不是孤立的函数,而是“函数+使用说明+参数契约+适用场景+依赖关系”的组合。你可以把Tool想象成一把螺丝刀,而Skill是完整的使用训练包,里面写着“什么场景用哪个型号的螺丝刀、握哪里、往哪个方向拧、拧到什么力度”。模型拿到Skill之后,才能正确地判断什么时候调用、传参传什么、拿到结果怎么用。
所以Agent Skills本质上是一套“可被模型理解和调用的能力封装”。Agent主程序只负责理解意图、拆解任务、组织回答,真正干活的都是技能库里一个个Skill。把这一层想清楚了,后面很多设计决策就顺了。
1.2 为什么要有技能库,而不是把逻辑写死在代码里
有人可能会问:我直接在代码里写if-else,按关键词匹配去调用函数不行吗?行,但只能用在规则极其固定的场景。一旦你的Agent要面对开放式的用户请求,硬编码匹配基本会裂开。因为这个问题的核心不是“能不能调用”,而是“模型怎么知道该调哪个”。
技能库解决的核心问题有三个。第一,可发现。技能的定义、描述、参数格式集中在注册表里,模型在规划阶段能“看到”所有可用能力,才能做选择。第二,可复用。同一个技能可以被多个Agent、多个任务场景共用,不需要每个Agent重新实现一遍。第三,可演进。新增技能只需要往库里注册,不需要改Agent主逻辑,这对持续迭代非常重要。
我用一个类比:技能库之于Agent,就像手机应用商店之于手机。手机本身只提供运行环境、基础服务和分发机制,你要新功能就去装应用,而不是把手机底层系统重写一遍。Agent也是一样,主循环就是那个操作系统,技能库就是应用商店。
1.3 什么项目适合用Agent Skills,什么场景先别用
不是所有项目都适合引入技能库。我在做项目评估时一般这么判断:如果任务链路是100%确定的,比如每天定时跑脚本、同步数据、生成固定报表,那直接写定时任务就够了,塞一个Agent反而增加复杂度和延迟。但如果你的场景里存在“意图不固定、流程需要动态编排、模型要根据用户需求决定调用什么能力”的情况,那技能库就是必需品。
比较典型适合的场景包括:企业内部知识助手(查文档、查工单、发审批)、研发辅助Agent(搜代码、查日志、调监控接口)、个人效率助理(管日历、写邮件、整理会议纪要)、垂直行业的操作台(查库存、下单、算报价)。这类项目共同点是对接的工具多、用户需求表达模糊、需要模型做决策。
我见过最实用的落地方式,其实是小步快跑:先给Agent挂三五个技能跑通场景,用户在真实使用中会持续暴露新诉求,再逐步往技能库里加。那种一上来就想建“万能技能库”的项目,往往死在设计阶段——因为你也说不清Agent到底需要什么能力。
2. 技能库的顶层设计:四个原则决定Agent的上限
2.1 技能边界怎么划:单一职责在Agent世界同样成立
很多早期的技能库里,会出现这种技能:“用户信息处理”,里面既有查用户资料、又有改密码、还有拉订单记录。这种“全能技能”是灾难,因为模型面对一个模糊描述时,根本不知道它到底能干什么、该不该调。我在设计技能边界时,严格遵循几个信号来判断要不要拆分:参数开始超过5个、描述里出现了“如果用户想……则……否则……”的分支逻辑、一个技能要对接多个外部系统、执行结果包含好几种明显不同类型的数据。只要命中其中一条,我就会考虑拆。
拆分的粒度不是越细越好,核心判断标准是“模型能否在1-2句话内看懂这个技能什么时候用”。比如“查天气”和“查空气质量”可以合成“查城市环境信息”,因为用户通常会连着问;但“查用户资料”和“修改用户资料”必须拆开,因为一个是只读操作,一个是写操作,权限和风险完全不一样。命名也尽量用“动词+业务对象”的结构,比如search_code、create_ticket、send_mail,让技能名本身就能干一件事。
还有一点要注意:不要让两个技能在功能描述上大面积重叠。Agent执行任务时对技能的取舍未必像人那么理性,两个高度相似的描述会让它随机挑一个,结果时好时坏。这种情况要么把功能合并,要么在描述里明确各自的适用边界,比如“查询最近24小时错误率”和“查询近7天趋势”,必须写明时间范围和返回粒度。
2.2 技能描述不是写给人看的,是写给模型看的
这是整个技能库里最容易被低估的部分。很多开发者写技能描述,随手来一句“查询用户信息”,然后抱怨模型笨、老选错技能。实际上模型对技能的“理解”几乎完全来自描述文本,描述写得敷衍,模型就只能靠猜。
我的技能描述模板一般包含四块:功能定义、适用场景、边界声明、调用示例。功能定义说清楚“这个技能做什么、返回什么”;适用场景写“当用户提到哪些需求时优先使用”;边界声明写“什么情况下不要用”;调用示例给一个具体的传参格式,比如query_user_info({"user_id": 123})。
举个例子,我实际项目里的一个技能描述是这样写的:
根据用户ID查询用户的基本资料,包括姓名、邮箱、手机号、注册时间和账号状态。当用户询问“我是谁”“我的账号信息”“个人资料”时优先使用。注意:只能查询已登录授权的用户,禁止用本技能查询其他任意用户信息。示例:query_user_info({"user_id": 123})
这比“获取用户数据”不知道高到哪里去了。模型拿到这段描述后,不但知道什么时候该调,还知道不能瞎调。边界声明尤其重要,它能挡掉很多模型自作主张的调用。比如一个“生成日报”的技能,描述里明确“只负责日报生成,不负责发送邮件”,模型就不会顺手把发送也塞进去。
2.3 参数契约:JSON Schema为什么是技能库的地基
模型调用技能时最常出的问题,就是参数传错。要么把“用户ID”传成了“用户名”,要么该传数字的时候传了字符串,要么漏掉必填项。解决这个问题,不能靠模型自律,要靠参数契约去约束。
我在每个技能里都会配一个输入schema,用JSON Schema的标准格式描述字段、类型、是否必填、取值范围,还会给每个字段写一个辅助模型的说明。比如:
{ "type": "object", "properties": { "keyword": { "type": "string", "description": "要搜索的关键字,支持模糊匹配,不要传空字符串" }, "max_results": { "type": "integer", "description": "返回结果的最大条数,取值范围1-50,默认10" } }, "required": ["keyword"] }这套schema有两点实际价值。第一,模型生成调用参数时,会在明确的约束里做推断,出错率明显下降;第二,Agent主循环可以在真正执行前做一次参数校验,把不合法的调用直接拦下来,避免把错误请求送到下游系统。
如果你用的是OpenAI、Claude这类带Function Calling或工具调用能力的模型,schema基本是原生支持的。如果是自己搭的Agent框架,就把schema存进技能注册表,在调用前做一层校验。这一步在早期可以省,但技能数量超过10个之后必须补上,否则线上故障率会指数级上升。
2.4 注册中心与安全边界:技能多了以后必须考虑的事
技能库到后期会越来越大,如果每个技能只是散落在代码里,你会面临几个麻烦:不知道有哪些技能、不知道谁在调它、改了一个接口影响了一片。所以技能注册中心不应该等到技能多了再建,而是在设计技能库的第一天就搭一个最简版本。
注册中心本质是一个集中存储技能元数据的表,记录技能名、描述、输入schema、对应函数、版本号、权限等级。最简单的时候可以是一个Python字典,稍微复杂一点用配置文件或数据库。我在项目中常用的是一个全局注册表,用装饰器把函数直接注册进去,方便且直观。
安全边界是另一件不能拖的事。技能必须分权限等级:只读技能、普通写技能、危险操作技能。只读技能可以放心交模型自由调用;写操作建议记录审计日志;删除、支付、发送消息这类高危操作,必须在Agent流程里加一道人工确认。此外,密钥和鉴权凭据绝不能存放在模型可以读取的提示词或技能描述里,技能执行时的外部API认证统一走服务端配置。
3. 从零到一:搭建一个可用的Agent技能库全流程
3.1 场景定义:做一个研发辅助型Agent技能库
纸上谈兵没意思,我拿一个实际做过的项目来拆解。目标是一个研发辅助Agent,面向开发团队日常使用。初期规划四个技能:在工程目录里搜代码、读取指定文件内容、调用内部分析API查服务错误率、将查询结果整理成当天的工作日报。
这四个技能覆盖了“查代码、读文件、查监控、出报告”的典型研发链路,同时包含了本地工具技能、云端API技能、数据加工技能三类形态,很适合用来演示技能库的完整搭建流程。整个搭建过程我分成四步:定义技能清单、实现本地技能、封装外部API、接进Agent主循环。
我先在纸上把每个技能的名字、用途、参数列出来。这个动作看起来简单,实际上非常重要,它能逼你把思路理清楚。比如“查服务错误率”这个技能,我一开始想做得很复杂,支持按服务名、时间范围、错误码筛选,结果参数列了七个,最后砍到三个:服务名、起始时间、结束时间。砍完以后,模型调用成功率和结果准确率都上来了。
3.2 本地工具技能:搜索代码与读取文件实现
本地工具技能是最容易上手的一类,不需要外部依赖,直接操作文件系统或命令行就行。以“搜索代码”技能为例,我用Python实现了一个轻量版本:
def search_code(keyword: str, path: str = ".", max_results: int = 10) -> list[dict]: """在工程目录中搜索包含指定关键字的源代码文件。 Args: keyword: 要搜索的关键字,支持子串匹配。 path: 搜索的起始目录,默认当前目录。 max_results: 返回结果的最大条数,默认10。 """ import os results = [] code_exts = {".py", ".js", ".ts", ".go", ".java", ".c", ".cpp"} for root, dirs, files in os.walk(path): dirs[:] = [d for d in dirs if d not in {".git", "node_modules", "venv"}] for f in files: if os.path.splitext(f)[1] not in code_exts: continue fp = os.path.join(root, f) try: with open(fp, "r", encoding="utf-8", errors="ignore") as fh: for line_no, line in enumerate(fh, 1): if keyword in line: results.append({ "file": fp, "line": line_no, "content": line.strip() }) if len(results) >= max_results: return results except Exception: continue return results这类技能的注意点其实在代码之外。过滤目录、限制数量、用errors="ignore"容错,这些细节能避免模型调用时把结果集撑爆。实现完函数后,我用register装饰器把它注册进技能表,同时把JSON Schema挂上。很多框架在只做Function Calling时不需要注册中心,直接传函数定义给模型就行;但一旦技能多起来,注册中心对调试和维护的价值就体现出来了。
3.3 云端API技能:把外部服务封装成标准技能
研发辅助Agent最常用的外部能力,是把内部分析平台的服务错误率查询接口接进来。这类技能的难点不在请求逻辑,而在超时、认证、异常处理。我的封装思路是:内部保持真实API调用逻辑,对外暴露的却是稳定统一的技能接口。
@register( name="query_error_rate", description="查询指定服务在时间范围内的错误率。当用户询问服务是否正常、错误率多少、接口失败情况时使用。" "注意:只支持查询,不支持修改任何配置。" "示例:query_error_rate({\"service\": \"order-api\", \"start\": \"2025-01-01T00:00:00\", " "\"end\": \"2025-01-01T23:59:59\"})", input_schema={ "type": "object", "properties": { "service": {"type": "string", "description": "服务名称,来自服务列表"}, "start": {"type": "string", "description": "起始时间,ISO8601格式"}, "end": {"type": "string", "description": "结束时间,ISO8601格式"} }, "required": ["service", "start", "end"] } ) def query_error_rate(service: str, start: str, end: str) -> dict: response = requests.get( f"{MONITOR_BASE}/v1/error-rate", params={"service": service, "start": start, "end": end}, headers=build_auth_headers(), timeout=10 ) response.raise_for_status() data = response.json() return { "service": service, "error_rate": data["error_rate"], "total_requests": data["total_requests"], "time_range": [start, end] }这里我把密钥管理放在build_auth_headers函数内部,通过环境变量或配置中心读取,技能函数本身不接触密钥,模型拿到的描述里也完全没有认证信息。超时设了10秒,防止慢接口把Agent整体流程卡死。异常处理我通常会在上层包一层try/except,把HTTP错误转成模型能读懂的文本,比如“查询失败:服务不存在”。
3.4 Agent主循环:选择、执行、反馈与校准
技能本身写完了,还需要把它们接进Agent的运行逻辑。我常用的主循环分四步:解析用户请求、让模型在技能列表里做规划、逐个执行技能并校验结果、把执行结果交给模型生成最终回答。
def agent_loop(user_query: str): skills_desc = build_skills_prompt(SKILL_REGISTRY) plan = llm_plan(user_query, skills_desc) observations = [] for step in plan["steps"]: skill = SKILL_REGISTRY.get(step["skill"]) if skill is None: observations.append({"error": f"技能 {step['skill']} 不存在"}) continue validated_args = validate_args(step["arguments"], skill["input_schema"]) if validated_args["is_valid"] is False: observations.append({"error": validated_args["message"]}) continue try: result = skill["function"](**validated_args["data"]) observations.append(result) except Exception as e: observations.append({"error": str(e)}) return llm_summarize(user_query, observations)规划阶段我做了一个很关键的动作:把技能描述用统一的格式拼进提示词,而不是直接把代码丢给模型看。这样模型能像“读菜单”一样浏览技能,做出选择。执行阶段的核心是“校验优先”,宁可让模型回头补充参数,也不能带着错参数硬调。反馈阶段则把执行结果原样交给模型总结,不做过多的前置处理,让模型根据用户原话去组织回答。
这套主循环不复杂,但稳定。复杂框架里可能加记忆、多轮上下文、工具链编排,核心思路依然不变:技能是独立的能力单元,Agent负责决策和表达,两者职责分离。职责一旦混在一起,调试的时候你会非常痛苦。
3.5 技能库测试:上线前必须过的三道关
技能库的测试和普通接口测试很不一样,因为调用者是模型,输入不确定性更高。我上线前会过三道关:单技能测试、场景联调、回归冒烟。
单技能测试针对技能本身,确认函数逻辑正确、参数校验生效、异常能兜住。这个阶段我把每个技能当普通函数测,不引入模型,跑的是确定性的输入。场景联调则用真实用户原话做测试,比如“帮我查下order-api今天下午错误率高不高”,观察模型能否正确选择技能并填充参数。最容易在这关暴露的,是描述写得不清晰导致选错技能。回归冒烟在每次修改技能描述或新增技能后跑一遍,确认老场景没有被破坏。我维护了一份测试场景集,不到20条用户原话,但覆盖了核心链路,跑一遍也就三分钟,收益却很大。
这套流程跑完,技能库的基础版本就算立住了。接下来遇到的大部分问题,不再是“会不会写代码”,而是“模型为什么没按预期选技能、传参数”。
4. 踩坑实录:技能调用失败的常见问题与排查方法
4.1 Agent选错技能:八成是描述写得不行
我调过最多的线上问题,就是Agent在多个技能之间选错。比如用户说“帮我把这份报告发给项目经理”,模型没有调发邮件技能,反而调了生成报告技能,然后告诉用户“报告已生成,发件功能未实现”。这种问题表面看是模型笨,根因往往是技能描述之间打架。
我有一次在库里同时挂了“get_user_profile”和“get_user_orders”,描述分别写着“查询用户资料”和“查询用户订单”。用户问“帮我看看这个用户的订单”,模型居然调了get_user_profile。排查后发现,前者描述里写了“当用户询问用户相关信息时使用”,范围太大,把订单类的请求也囊括进去了。修正方式是收紧边界描述,把“用户相关信息”改成“用户个人资料、账号信息、联系方式”,同时在get_user_orders的描述里增加“当用户提到订单、购买记录、消费记录时优先使用”,问题立刻消失。
排查这类问题有个很有效的方法:把模型的完整决策轨迹打出来,看它到底看到了哪些技能描述、为什么会选中那个技能。大多数情况下,问题都出在描述文本的歧义、范围重叠和负面样例缺失上。描述里加一句“什么情况下不要用”,比加十句“什么时候要用”更有用。
4.2 参数幻觉与类型错乱:模型“编”参数怎么办
模型在调用技能时编造参数,是个很头疼的坑。典型场景是:用户问“查一下张三的账号余额”,模型在用户上下文里根本没有张三的用户ID,却自动编了一个“user_id: 9999”传进去,然后返回空结果。模型不会承认自己编了参数,它只会一本正经地告诉你“未查询到该用户信息”。
参数幻觉要分两层解决。第一层靠schema约束,必填字段、枚举值、格式校验能挡掉一部分低级的错误。第二层要靠流程设计,当技能需要一个在上下文中不存在的实体ID时,Agent应该先调用一个“搜索用户”技能把ID找出来,再调用查询技能;或者当参数无法确认时,直接反问用户。我给query类技能加了一条规则:如果参数来源不明,输出“需要用户提供XX信息”,而不是硬着头皮猜。
类型错乱的坑则多半出在数字和日期上。模型把"2025-01-01"转成时间戳时差8小时、把字符串"100"当数字传、把枚举值的大小写写错,都有可能出现。schema里写清楚格式和取值范围,校验层做类型转换并兜底报错,不要指望模型永远不犯错。还有一个经验:给日期类参数统一规定一种格式,比如ISO8601,模型对单一格式的遵循度明显更高。
4.3 超时、并发与锁:技能执行阶段的工程坑
技能本身写得再好,跑了真实流量还是会遇到执行层的工程问题。最常见的是超时。模型执行一个技能,如果遇到外部接口响应慢,整个Agent流程会卡住,用户那边看到的就是“转圈圈没反应”。我一开始也没注意给技能统一加超时,后来一个报表技能直接把Agent流程拖垮,才学乖了。处理方式很简单:所有外部IO统一设超时,文件操作限制扫描深度,长任务改成先提交再轮询的模式。
并发问题更容易被忽略。多个用户同时让Agent调用同一个文件操作技能,或者两个技能同时写同一个临时文件,就会遇到竞争条件。写文件类技能建议加锁或写到独立临时目录,避免互相覆盖。还有一个经常踩的坑是技能不幂等,比如“创建工单”技能被模型重复调用两次,产生了两个工单。设计写操作技能时,我会在描述里写明“本操作会创建一条新记录,重复调用会产生多条”,同时建议在代码里做重复请求检测,将幂等键作为可选参数。
4.4 技能冲突与版本管理:库大了之后的隐患
随着技能库越扩越大,技能之间的命名冲突、版本漂移、接口变更会逐渐浮出水面。我遇到过一次事故:另一个同事新增了同名技能,覆盖了注册表里的旧函数,导致查询订单的技能实际执行了查询用户信息的逻辑,整个Agent行为直接错乱。注册中心里加版本号、启动时做重名校验、变更后强制跑回归测试,这三件事能极大减少这类事故。
版本漂移的典型场景是:底层API接口升级了,但技能描述和schema没更新,模型按照旧逻辑传参,部分参数已经失效。我的处理习惯是:技能函数体、schema、描述放在同一个版本单元里,任何一块改动都触发该技能整体升级。这样虽然看起来“重”,但追踪问题非常方便。技能库到后期,元数据和描述的管理成本会超过函数实现本身,这一块越早规范越好。
最后补一份排查速查表,遇到问题可以先按这张表定位:
| 现象 | 最常见原因 | 快速排查步骤 |
|---|---|---|
| 模型选错技能 | 技能描述范围重叠或边界不清 | 打印决策轨迹,检查多个技能描述是否互相覆盖 |
| 参数缺失或类型错误 | schema约束不足或上下文缺实体ID | 补全JSON Schema,确认模型是否需要先调用实体查询技能 |
| 技能执行超时 | 外部接口慢或没有统一超时 | 检查技能代码是否有timeout,长任务改为异步轮询 |
| 返回结果为空但无报错 | 参数被模型篡改或下游接口行为变化 | 用固定参数直接调函数,确认是技能问题还是模型问题 |
| 新增技能后老场景失效 | 同名覆盖或描述互相干扰 | 查注册中心是否有同名技能,跑一遍回归冒烟用例集 |
我个人的习惯是,在技能函数入口打一条结构化日志,记录模型传入的参数、校验结果、执行耗时。排查问题时,这条日志比什么调试器都好用。你可以记录参数级别的调用细节,但要注意别把敏感数据写进日志,脱敏这件事从第一天就要做。
Agent技能库不是一次性的交付物,它会随着业务和场景持续生长。我见过不少团队一开始雄心勃勃想建一个覆盖所有业务的大库,结果被复杂度拖垮;反而是那些先做透两三个核心技能,再围绕真实需求一点点加技能的项目,最终跑得又快又稳。你自己上手的时候,不妨也从最小闭环开始,把一个技能做扎实,把一个场景跑通顺,技能树自然会慢慢长起来。