1. agent-skills到底是个什么东西,为什么圈内人都在聊
最近后台收到不少读者来问 agent-skills 相关的问题,大多是同一个困惑:我的 Agent 已经能正常对话了,也能接上大模型 API,可一旦让它“真正干点活”——查个文件、调个接口、改个配置——它就只会说“我无法直接操作”,或者给你返回一段正确的废话。
这其实就是 Agent 开发里最典型的一道坎:模型很聪明,但手脚是废的。agent-skills 解决的就是这件事,它本质上是给 Agent 配备的“可复用行动单元”,让模型从“会说话”进化到“会做事”。我理解的 agent-skills 不是一个固定的开源库名,而是一整套关于技能(Skill)的定义、组织、注册和调用规范。你可以把它理解成 Agent 的“工具箱”,每个技能就是箱子里一把趁手的工具,模型自己会去挑哪把合适。
这篇文章我不会跟你念文档,而是把我实际搭技能库、调试技能调度、踩坑排查的过程完整写出来。适合正在做 Agent 应用、研究 Function Calling、或者准备把自动化能力接入业务系统的开发者参考。讲原理,但更偏实操,所有代码和配置你都能直接抄。
1.1 技能的本质:从“输出文字”到“执行动作”
先说一个最容易被忽略的事实:大模型本身就是个“纯文本进出”的系统。你给它一段 prompt,它返回一段文本,这就是全部。它不会真的去读你磁盘上的文件,不会真的去调用支付接口,也不会真的帮你把服务器重启了——它只是“生成了”一段看起来像会做这些事的文字。
那 Agent 是怎么“动手”的?靠的是外部执行器。模型根据当前对话场景,从预定义的技能列表里挑一个,然后按技能定义里写好的参数格式生成一次调用请求,框架收到请求后执行真正的代码逻辑,再把执行结果以文本形式回填给模型,让模型基于真实结果继续回答。整个过程模型没有直接操作任何东西,它只是在“发指令”,真正干活的,是技能背后的那段函数。
我打个比方。新来的实习生很聪明,能看懂资料也能写报告,但刚入职没开通公司的 OA、ERP 权限,也没人教他报销流程长什么样。你让他“去把上个月的订单数据导出来”,他只能干瞪眼。skills 就是给这个实习生配的“岗位权限+操作手册”:告诉他“导出订单时走这个接口”,还告诉他“调用时参数怎么填”,他才能真正把事情办了。所以技能的本质,就是给模型这个“聪明但无手”的实习生装上能干活的手。
1.2 为什么不能把逻辑全塞进 System Prompt
有人可能想,那我直接在系统提示词里把操作步骤写得清清楚楚,让模型照着文本里的伪代码“假装执行”,然后再由外部程序去解析这段文本不行吗?说实话,这条路早期确实有人这么干过,比如让模型输出固定格式的 JSON,再由调度程序解析执行。但它有天花板,而且越往后越难受。
最明显的问题是上下文窗口。你塞一份完整的操作手册进 System Prompt,每个会话都要重复计费,占掉的 token 你都要付钱。技能多了以后 prompt 会变得非常臃肿,模型反而开始“忘事”,你让它执行 A 操作时,它注意力可能已经被后面的 C 操作描述分散了。其次,文本约定没有类型校验,模型输出 JSON 时字段名差一个字母,外部解析就直接报错,或者更糟,解析成功了但执行了错误逻辑。
对比一下就能看明白:
| 对比维度 | 全部塞进 System Prompt | 独立的技能定义与注册 |
|---|---|---|
| 上下文占用 | 每次会话都全量占用 | 仅在调用时按需注入 |
| 复用性 | 换个 Agent 要复制粘贴 | 技能独立注册,多 Agent 复用 |
| 参数可靠性 | 靠模型自觉,易出错 | 有结构化 Schema 约束 |
| 调试成本 | 改一处要重新验证全流程 | 单技能单测,定位快 |
| 扩展方式 | Prompt 越改越长 | 加一个技能包就是一个能力 |
所以正规的 agent-skills 项目几乎都会走“结构化技能定义+动态注册”的模式。模型只会在某个技能可能相关时看到技能的名字、描述和参数说明,而不是把所有技能和全部操作细节都塞进上下文。
1.3 技能、工具调用与 MCP 的关系
聊 agent-skills 绕不开三个概念:Function Calling、Tool Use、MCP。很多人问我它们是不是一回事,其实不是,但对 Agent 技能体系来说,它们是层层递进的关系。
Function Calling 是模型侧的一种能力,指模型能输出一次结构化的“函数调用意向”,比如“调用 search_files,参数是 path=/home/user,pattern=*.log”。Tool Use 是应用侧的实现模式,你提供一组工具,模型在需要时选择工具。MCP(Model Context Protocol)则是把这些能力统一成一种标准化协议,让技能的定义、发现、调用和传输有统一的格式。agent-skills 更像是在这些基础之上的一层工程化封装:你把某项能力做成一个包,包含描述、参数、执行体和返回格式,然后按一定规则挂载到 Agent 上。
理解这层关系很重要,因为网上一堆项目名都带 skills,有的其实就是 MCP server 的配置集合,有的则是 Function Calling 工具的函数列表。你在参考的时候先搞清楚它是哪一层,才不会抄错方向。我下面讲的技能设计方法,无论底层用 Function Calling 还是走 MCP,都能直接套用。
2. 技能库的整体设计:先想清楚再动手
很多入门教程上来就让你写函数、注册工具,但我实际做了几个项目后发现,最费时间的根本不是写函数本身,而是技能库的顶层设计。技能定义得太粗,模型看不懂;定义得太细,维护成本爆炸。这节我把自己的设计思路完整拆开。
2.1 技能的最小单元长什么样
一个技能在落地时,至少要包含五个要素:名称、描述、参数定义、执行体、返回约定。缺哪一个都会在运行期出幺蛾子。
{ "name": "search_local_files", "description": "根据关键字和路径搜索本地文件,支持按文件类型过滤,适合用户要求查找或定位文件时使用。", "parameters": { "type": "object", "properties": { "root_path": { "type": "string", "description": "搜索的起始目录,默认当前工作目录" }, "keyword": { "type": "string", "description": "文件名中要匹配的关键字,支持模糊匹配" }, "file_type": { "type": "string", "enum": ["all", "doc", "image", "code", "log"], "description": "要筛选的文件大类,默认 all" }, "max_results": { "type": "integer", "description": "最多返回结果数,默认 20,最大 100" } }, "required": ["keyword"] } }这五要素里最容易翻车的是描述和参数说明。原因后面细说,这里先记住一个原则:技能的定义不是写给人看的,是写给模型看的。模型的“阅读理解”能力决定了它只能依据字面含义去匹配技能,所以描述必须具体,参数说明必须把边界条件写透。
执行体就是实际跑逻辑的那段代码,可以用 Python、Node.js 或任何语言实现,关键在于它必须是“纯函数式”的:给定参数返回结果,不依赖外部状态,不隐藏副作用。这样技能才能被安全地并发调用、重试和单元测试。
返回约定则决定模型能不能读懂执行结果。我强烈建议所有技能都返回结构化 JSON,并且包含两个字段:success(布尔值)和summary(一段面向模型的话术摘要)。很多人只返回数据,导致模型拿到一堆原始 JSON 不知道怎么向用户解释,这就是后面“驴唇不对马嘴”问题的根源。
2.2 技能描述是给模型看的“说明书”
技能描述这个东西,在本地调试时可以糊弄,但一上真实环境,模型调不调用你的技能,基本就靠这段描述。写得太抽象,模型根本不知道该在什么场景用它。
举个例子,第一个版本我写过:“用于文件搜索的工具。”结果是什么?模型在用户说“帮我找一下项目里的配置文件”时,依然回复“抱歉,我无法直接访问你的文件系统”。它压根没意识到这个技能能派上用场。
后来我把描述改成了这样:“当用户要求查找、定位、搜索本地文件或目录时使用此技能。可指定起始路径、文件名关键字、文件类型过滤条件。典型场景包括:帮我找一下某个配置文件、根据关键字搜索日志文件、统计某个目录下有哪些图片文件。” 同样是这个技能,模型几乎每次都能正确触发。
这里面的逻辑并不玄学。模型只在对话上下文中看到技能的名称和描述,看不到技能背后的代码。技能描述就是它做“选哪个工具”这个决策的唯一依据。你描述里写了“搜索”,它在遇到“找一下”这种口语时,是没法自动把“找”和“search”对齐的,除非你把触发场景、同义表达、边界情况都写清楚。
2.3 技能分层:基础技能、组合技能、编排技能
技能一旦多起来,几十个技能平铺在列表里,模型的选择准确率会明显下降。这不是模型笨,而是选择空间太大,干扰太多。我后来采用三层结构解决了这个问题。
基础技能是最底层的原子操作,比如“读文件”“写文件”“执行 shell 命令”“发 HTTP 请求”。这类技能尽量做到细粒度、职责单一,每个技能只做一件事。组合技能是在基础技能之上封装出来的业务动作,比如“根据关键字搜索日志并统计错误次数”,它内部会调用读文件、搜索、正则匹配等多个基础技能。编排技能则更上层,通常本身不直接执行操作,而是负责决定“先调哪个组合技能、再调哪个基础技能”的流程。
| 层级 | 职责 | 示例 | 特点 |
|---|---|---|---|
| 基础技能 | 原子操作 | read_file、search_files、http_request | 无状态、可复用、可单测 |
| 组合技能 | 业务动作 | search_logs_and_count_errors | 编排基础技能,有明确业务含义 |
| 编排技能 | 流程决策 | analyze_project_and_generate_report | 面向复杂任务,内部多步 |
为什么要分层?因为模型不擅长一次处理太长的工具链。你给它一个“分析项目结构并生成报告”的编排技能,描述写得再清楚,它也很难立刻理解内部步骤。但如果你提供的是清晰的组合技能,模型只需要决策“现在调用 analyze_project_report”,剩下的内部调度交给代码逻辑,成功率会高很多。
另一个好处是维护方便。底层接口变了,只改对应基础技能,上层组合技能和编排技能不用动。复用性也好,换个行业场景,组合技能在几个 Agent 之间能共享。
3. 从零手写一个技能:完整实操记录
这节我会从头到尾走一遍自己做技能的流程,场景选最典型的“技能调度落地”,顺便把容易出问题的地方全部标出来。建议你开着编辑器跟我一起写,比干看印象深刻。
3.1 场景设计与输入输出定义
我选择实现的技能是“按关键字搜索本地日志并统计错误类型分布”。这个技能很典型,既有文件搜索,又有文本解析,还要返回统计结果,能覆盖技能开发的大部分要点。
先想清楚需求输入输出。用户诉求可能是“看看这周 error 日志里有没有数据库连接相关的错误”,模型需要知道去哪里找日志、按什么关键字筛、返回什么统计信息。所以我设计的参数包括:log_dir(日志目录,选填,默认 ./logs)、days(只看最近几天的文件,默认 7)、keyword(筛选用关键字,选填,不填则统计所有级别)、top_n(返回数量最多前几种错误类型,默认 5)。
输出上,我要求执行体返回一个结构化的 JSON,包含 success、total_files(扫描文件数)、total_matches(匹配行数)、top_errors(按类型聚合的结果)、summary(给模型读的一句话结论)。设计这一步想清楚的好处是,后面写函数和调试的时候目标非常明确。
参数默认值要格外用心。比如log_dir我给了默认当前目录下的 logs 文件夹,days给默认值 7,这样用户只是含糊地说“查一下日志错误”,模型也能直接调用,不需要反复追问用户细节。凡是能给默认值的参数,一定给默认值。
3.2 执行体代码实现与边界处理
执行体我用 Python 实现。读日志、按关键字过滤、按错误类型正则提取,逻辑不复杂,但边界情况特别多。第一步是文件遍历,要处理目录不存在、权限不足、文件编码不是 UTF-8 这三种情况;第二步才是关键字过滤和类型统计。
import os import re import json from collections import Counter from datetime import datetime, timedelta def scan_logs(log_dir: str = "./logs", days: int = 7, keyword: str = "", top_n: int = 5): # 基础校验 if not os.path.isdir(log_dir): return { "success": False, "message": f"日志目录不存在: {log_dir}", "summary": "用户提供的日志目录不存在,无法执行搜索。" } cutoff = datetime.now() - timedelta(days=days) total_files = 0 total_matches = 0 error_counter = Counter() for root, _, files in os.walk(log_dir): for fname in files: fpath = os.path.join(root, fname) if not fname.endswith((".log", ".txt")): continue # 跳过超出时间范围的文件 mtime = datetime.fromtimestamp(os.path.getmtime(fpath)) if mtime < cutoff: continue total_files += 1 try: with open(fpath, "r", encoding="utf-8", errors="ignore") as f: for line in f: if keyword and keyword not in line: continue total_matches += 1 # 错误类型通常是 [ERROR] xxx match = re.search(r"\[(ERROR|WARN|INFO|DEBUG)\]\s*(.+)", line) if match: error_counter[match.group(1)] += 1 except PermissionError: continue top = error_counter.most_common(top_n) if error_counter else [] summary = f"扫描了 {total_files} 个文件,匹配到 {total_matches} 条日志。" if top: summary += " 主要日志级别分布:" + ", ".join( f"{k} {v} 条" for k, v in top ) else: summary += " 未发现符合条件的日志级别。" return { "success": True, "total_files": total_files, "total_matches": total_matches, "level_distribution": top, "summary": summary, }这段代码实际跑通了,但有三个经验值得单独说。
第一,打开文件一定要加errors="ignore"。日志文件混入 GBK 或 Latin-1 编码是常态,不加这个参数,一个非法字符就能让整个技能崩溃。
第二,权限问题要安静跳过,而不是直接返回错误。你搜一个目录时有几个文件没权限,不应该影响整体结果,执行体里把PermissionError吞掉继续往下走,最后的 summary 里体现扫描的文件数即可。
第三,返回的 summary 是给模型看的“人话”,必须是一段自然语言。模型拿到这段文字,才知道怎么向用户解释结果。你只返回一个 Counter 对象,模型看着那串数字,很容易开始胡说。
3.3 定义技能元数据:用模型的视角写参数
执行体完成之后,接着要写技能元数据。这是很多新手最容易忽略的一步,但我可以说,90% 的技能调用失败都发生在这一层。
参数 JSON Schema 里,description字段比type还重要。模型推断参数值时靠的就是这段描述。比如days参数,如果你只写“天数”,模型可能填 30、7、365 都能对,但它不知道你期望的是近几天。我写的是“只统计最近多少天内的日志文件,默认 7 天,用户没明确说时间范围时就传 7”。
enum字段能帮大忙。如果你限定file_type只能是 all、doc、image、code、log 五种,模型就只能在里面选,降低乱传参的概率。同理,max_results设置合理的最大值,防止模型填一个 100000 把执行体拖垮。
技能描述同样要以“模型视角”来写。不要写“此工具用于日志扫描与错误聚合统计”,而要写“当用户要求分析日志、查找错误原因、统计不同级别日志数量时使用此技能。典型问题:日志里有没有数据库报错、最近一周 ERROR 主要出现在哪里”。我自己的经验是,描述里带上“典型问题”比任何抽象概括都好用。
3.4 把技能挂到 Agent 上:注册与联调测试
技能写好后要挂到 Agent 上。这一步不同框架写法不同,但核心动作一致:把技能的函数定义(名称、描述、参数 Schema)注入到模型的工具列表里,然后把执行体代码挂到框架的工具调度器上。
# 伪代码,不同框架 API 可能不同 agent.register_tool( name="analyze_logs", description="分析日志文件并统计各级别日志数量与错误分布", parameters=log_schema, handler=scan_logs )注册完成后,我习惯做一轮“五连问测试”:分别用直接指令、模糊指令、带具体参数的指令、超出技能边界的指令、完全不相关的指令去调 Agent,观察它是否正确触发技能、是否正确传参、是否在技能不适用时果断不调用。
我当时就踩了一个典型坑。用“帮我看看日志”这种模糊指令测试时,模型触发了技能,但log_dir传了一个不存在的路径。后来我在参数描述里加了“默认使用工作目录下的 logs 文件夹,只有用户明确指定其他路径时才传值”,同时把log_dir的默认值写死到函数签名里,这才彻底解决。
这轮联调非常值得认真做,因为在真实场景里用户不会每次都按标准格式说话。模糊表达能不能正确映射到参数默认值,直接决定技能可用性。
4. 技能运行中的常见问题与排查实录
技能上线跑起来以后,真正的挑战才开始。模型调用技能的成功率不会永远 100%,这里把我实际遇到过的几类高频问题按优先级整理出来,并附上排查方法和最终解法。
4.1 模型死活不调用技能:先查描述,再查参数
问题表现:用户问“帮我找一下昨天的日志”,Agent 回答“我无法直接访问你的日志文件”,完全没有触发日志分析技能。
排查思路分三步。第一步,确认技能是否真的注册成功。很多框架注册工具时是异步的,注册完立即测试可能还没生效,我遇到过不止一次。第二步,检查技能描述是否具体。如果描述还是“用于日志分析”这种抽象写法,立即改成“当用户要求分析日志、查找错误、统计日志数量时使用此技能”,并在描述里明确列出触发词汇。第三步,检查参数 Schema 是否过于严格。required里塞了五个必填参数,模型看到传参成本高,可能就直接放弃调用了。
有一个很实用的调试技巧:把技能列表打印出来,用你的大模型 API 手动发一条测试消息,在返回里看模型有没有给出 function_call 意向。如果没有,说明模型看完了技能定义也没找到匹配项,问题基本锁定在描述上;如果有调用意向但参数不对,问题出在参数 Schema。
4.2 技能确实执行了,但 Agent 回答得驴唇不对马嘴
问题表现:技能正确执行,返回了{"success": true, "total_matches": 23},但 Agent 跟用户说“已找到 23 个错误”,完全忘了total_matches匹配的是含关键字的日志行,不一定都是错误。
这是最典型的“返回结构设计缺陷”。Agent 没有读代码的能力,它只能读返回的 JSON 字段名和值。字段名是total_matches,它自然理解为“匹配总数”,至于匹配的到底是错误日志还是普通日志,靠猜。
解法也很直接:返回结构里必须有summary字段,把所有关键信息翻译成一句模型可以直接引用的话。比如“扫描了 12 个日志文件,匹配到 23 条包含关键字‘timeout’的日志行,其中 ERROR 级别 5 条”。模型看到这句话,回答基本不会跑偏。
| 问题现象 | 可能的根因 | 排查/解法 |
|---|---|---|
| 模型不调用技能 | 描述太抽象/触发词缺失 | 描述里加典型问题和触发场景 |
| 调用但参数乱传 | parameters 说明模糊 | 每个参数写清楚含义、默认值、可选项 |
| 返回结果被误读 | 字段含义不直观 | 加 summary 自然语言摘要 |
| 执行后上下文爆炸 | 返回体过大 | 截断、分页、只返回摘要 |
| 偶尔调用出错 | 异常没有被捕获 | 执行体顶层加 try-except 并返回错误信息 |
4.3 上下文污染与技能输出爆炸
技能返回的数据量过大,是一个隐蔽但危害极大的问题。我试过让技能返回文件全文,结果 5000 行的日志一下子塞进上下文,后续对话质量立刻下降,连带着模型开始遗忘前面用户的指令。
解决思路是按需返回。搜索类的技能默认只返回前 20 条结果,并在 summary 里提示“匹配到 X 条记录,已显示前 20 条”。文件读取类的技能按行数截断,一般限制在 300 行以内,需要更多再让 Agent 二次调用获取下一段。统计类的技能直出聚合结果,不返回明细。
这里有个测试技巧:每开发完一个技能,强制用最大参数调用一次,看看返回体占多少 token。如果超过 2000 token,就要考虑是不是该截断或改成摘要模式。很多技能的“性能问题”其实是输出太大撑爆上下文,不是模型本身跑得慢。
4.4 重试、超时与幂等:越早想越好
单机 Demo 可以忽略这类问题,但只要技能涉及外部服务,比如发请求、写数据库、调第三方 API,就必须考虑重试和幂等。我第一次写一个“自动发消息”的技能时没想幂等,结果模型超时后自动重试了一次,消息被发了两次,现场相当尴尬。
给非查询类技能设计参数时,一定要加一个request_id参数,执行体内部对该 ID 去重。同样,超时时间要单独设置,不能依赖模型层默认超时,执行体如果在拉起子进程或请求外部服务,应设置自己的超时上限,超时后返回整段逻辑提前结束。
还有一个和模型层配合的经验:在技能描述里标注“该操作不可重试”或“该操作是幂等的,可安全重试”。模型在生成调用请求时如果看到不可重试的标注,会倾向于一次成功,降低自动重试概率。这套机制在纯 Function Calling 场景不明显,但接 MCP 后 effect 语义会越来越重要。
5. 个人的几条经验和收尾建议
写到这,我发现踩过的坑基本都集中在同一个根源上:我们总把 Agent 当成一个传统的“代码程序”,觉得它应该精确理解每个参数,而实际上它是个“读说明书做决策”的系统。你要做的不是把逻辑写得更严谨,而是把技能的“说明书”写得更好懂、边界更清晰、返回更易读。
一个很实用的习惯:每个技能建一个 examples 目录,放 3 到 5 个测试输入和期望输出。不只是单测用,更大价值是调试时快速回看“这个技能当初设计成什么样、模型的什么误解让我改了参数描述”。几次下来,你会发现自己对“如何给模型写描述”的判断力会明显提升。
最后分享一个看起来很小但收益极高的小技巧:技能返回的 summary 里,开头永远用“扫描了”“搜索了”“统计了”这类动作动词,而不要用“结果如下”“成功返回”这类套话。模型从 summary 里提取用户能听懂的结论时,动作动词能让它更快组织口语化回复。这个小改动,是我在做了七八个技能之后才总结出来的,实测对回答质量的提升比调模型参数还明显。