☰
Agent Skills实战:构建稳定可控的AI Agent技能库
2026/9/26 8:41:12 网站建设 项目流程

最近挺多人问我,agent-skills 到底是个什么东西,是又一个新造出来的概念,还是真能把 AI Agent 从“勉强能跑”拉到“稳定可交付”。我在生产环境里维护自己这套技能库已经大半年了,可以负责任地说,它是目前我见过让 Agent 行为变得可控的最实在的手段之一。简单讲,agent-skills 就是把 Agent 需要反复完成的每一类高频任务,打包成一份包含触发条件、调用参数、执行步骤、输出校验的标准技能模板。它解决的核心问题很直接:模型太自由、行为太随机、结果没法验收。这篇文章我会从设计思路讲到代码实现,再讲排查经验,适合正在做 Agent 应用、想降低 prompt 维护成本、提升任务成功率的开发者。

1. Agent Skills 核心概念与设计心态

1.1 skills 和 tools 的关系:为什么不是所有能力都叫 skills

很多人第一次接触 agent 开发,会先学会 function calling,也就是给模型注册一堆函数。比如send_email(to, subject, body)、search_web(query)、create_calendar_event(title, time)。这些是 tools,是原子操作,模型可以直接调用,也能拿到返回值。问题在于,真实业务里很少能靠单个原子操作搞定一个任务。

举个实际例子,“把今天分散在三个文档里的会议记录、项目进度、临时想法整理成日报并归档”,如果拆成 tools,你会得到 read_file、write_file、extract_keywords、generate_summary 这一堆零散函数。模型每一次都要自己决定先调哪个、再调哪个、中间出了错怎么处理,结果就是调用链又长又容易断,漏步骤、格式错乱、文件路径写错,各种情况都发生过。

skills 就是来解决这层问题的。一个 skill 不是一个函数,而是一整套“工作方案”,内部可以包含多个小步骤,也可以调用多个工具或脚本。它对外暴露的是一个清晰目标:用户给一份输入,技能返回一份结构化的结果。所以我的理解里,tools 是扳手,skills 是“换轮胎的标准工序卡”。工序卡里可以写明什么时候用扳手、什么时候用千斤顶、按什么顺序操作、做完怎么检查。这套工序卡才是让一个新手稳定完成工作的关键。

agent-skills 项目要做的事情,就是把这类工序卡系统化地沉淀下来,让团队里任何一个 Agent 都能加载、复用、评估和迭代。它的价值不在某一句话术,而在分层设计:底层是原子工具,中间是标准化技能,上层才是 Agent 的规划和决策。

1.2 像写员工手册一样设计技能

我第一次设计 skill 模板时,特别喜欢堆细节,想把所有可能情况都写进描述里。后来发现效果并不好,模型要么因为描述太长而忽略,要么被过多的边界条件绕晕。后来我换了个思路:就当自己是在给一个刚入职的实习生写岗位手册。

给实习生写手册,你会怎么写?先告诉他这份工作什么时候需要做,具体目标和验收标准是什么;再告诉他第一步做什么、第二步做什么,每个步骤注意什么;然后给几个已经做好的样例,让他照着样子模仿;最后告诉他哪些情况不要做、出了异常找谁。这个结构,基本就是一份合格 skill 的骨架。

我把骨架提炼成三个原则:

  • 自包含:技能不依赖 Agent 的私密记忆或上下文状态。需要的数据全部通过输入参数传入,或者从一个明确的输入目录读取。这样任何 Agent、任何时间加载它,行为都是一致的。
  • 收敛:只做一件事,输入输出边界清晰。比如“整理日报”这个技能,就只负责把一堆零散文件转成一份日报 markdown,不负责发送邮件、不负责提醒日程。想扩展就再建一个技能。
  • 可观测:执行过程中写日志,返回结构化结果。进程退出码、错误码、日志路径都要明确,这样上层 Agent 才能知道“技能到底执行成功了没有、卡在哪一步”。

还有一个反直觉的经验:skill 描述里一定要写清楚“什么时候不要用”。我最初觉得写“不适用场景”会降低技能的曝光度,实际恰恰相反。模型在多个技能里做选择时,排除法比匹配法更可靠。你告诉它“这个技能不适合处理多语言文档”,它遇到外语文件就会主动换别的路径,而不是硬把一个中文场景的技能套上去。

2. 技能库的整体结构设计

2.1 目录与文件规范:每个技能都是一个独立工程

agent-skills 不是写在单个 prompt 里的东西,而是一个完整的目录工程。一个技能在仓库里应该有独立目录,目录内部文件职责清楚。我的常用结构长这样:

agent-skills/ ├── README.md ├── skills/ │ ├── doc-summary/ │ │ ├── skill.md │ │ ├── schema.json │ │ ├── scripts/ │ │ │ ├── run.py │ │ │ └── requirements.txt │ │ ├── assets/ │ │ │ └── template.md │ │ ├── tests/ │ │ │ ├── test_doc_summary.py │ │ │ └── fixtures/ │ │ │ ├── input1.md │ │ │ └── expected1.md │ │ └── examples/ │ │ └── call_example.json │ └── ...

每个文件承担一个职责:

  • skill.md是给模型看的说明书,用自然语言写清楚技能的定位、使用步骤、注意事项和示例。
  • schema.json是给参数定义的 JSON Schema,模型根据它生成结构化的调用参数。
  • scripts/存放技能执行代码,默认入口统一叫run.py,便于加载器识别。
  • assets/放模板、字典、参考文件等静态资源。
  • tests/放自动化测试和测试夹具,保证技能在改动后仍然能跑出预期结果。
  • examples/放一个或几个标准调用样例,方便人工检查和调试。

之所以把每个技能都做成一个独立小工程,是为了让“技能”具备工程素养。很多 Agent 项目失败,不是因为模型能力不行,而是因为技能本身没有版本、没有测试、没有错误处理,出了问题只能靠猜。目录规范是第一步,它逼你把每个技能的上下文边界固定下来。

2.2 元数据设计:让模型更容易“选对”技能

模型在一个技能库里做选择,靠的是元数据的质量,而不只是代码质量。skill.md里的描述就是技能在模型眼中的“简历”。我们看一个示例:

--- name: doc-summary description: 把多份零散文档或笔记整理成一份结构化日报。适用于工作报告、会议纪要、每日反思等场景。不适用于需要翻译、需要情感分析或多语言内容处理的任务。 version: 2.3.0 --- 当用户希望把多个文件中的关键信息汇总成一份 markdown 文档时,使用 doc-summary 技能。 执行步骤: 1. 从参数 input_paths 中读取所有源文件路径。 2. 逐个读取文件内容,提取标题、正文预览。 3. 按统一模板生成日报,写入 output_path。 4. 输出 JSON 结果,包含输出路径和条目数量。 示例: 输入:input_paths: ["./notes/a.md", "./notes/b.md"], output_path: "./report.md" 输出:{"status": "ok", "output_path": "./report.md", "count": 2}

这里最容易被忽略的是 description 字段。我早期写得很简陋,例如“汇总文档”,结果模型在“是否调用”上经常犹豫。后来改成“把多份零散文档或笔记整理成一份结构化日报”,并明确写“不适用于翻译、情感分析”,调用准确率明显提升。

schema.json同样重要。模型要通过它生成合法参数,如果字段说明含糊,就会出错。我常用的 schema 结构:

{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "input_paths": { "type": "array", "items": { "type": "string" }, "description": "要汇总的源文件路径列表,必须是绝对路径或相对当前工作目录的路径", "minItems": 1 }, "output_path": { "type": "string", "description": "输出 markdown 文件的保存路径", "pattern": "\\.md$" }, "style": { "type": "string", "enum": ["brief", "detailed"], "default": "brief", "description": "brief 只保留每条摘要前 120 字,detailed 保留完整内容" } }, "required": ["input_paths", "output_path"] }

设计时我会注几个细节:枚举值要给口头说明,避免模型生成风格不符的值;必填字段要明确;带 pattern 的字段要说明规则。参数校验这一层,等于给模型套了一个“安全带”,越早暴露错误,后面就越容易排查。

3. 从零实现一个高复用技能:文档汇总助手

3.1 需求痛点与方案选择

我最初做 doc-summary 这个技能,是因为每天都在处理一堆零散笔记。上午开了个项目会议,下午写了一堆技术调研,晚上还有几条临时想法,散落在不同目录。以前的方式是把所有内容复制到对话框里,让模型直接生成日报,结果经常出现三种情况:内容太长被截断、重要时间信息丢失、输出格式每天都不一样。靠 prompt 反复强调也不是办法,因为模型每次都从零推理,一致性很难保证。

所以我决定把它做成一个 skill。设计思路是“能本地规则化的步骤坚决用代码,只有语义理解部分才让模型参与”。如果整条链路都用模型做主,仍然会有随机性;如果全部用规则,又处理不了不同文档之间的语义差异。于是我把任务拆成三层:

  1. 本地规则层:读取文件、清理格式、提取基础信息,纯脚本完成。
  2. 模型决策层:由 Agent 决定要不要调用这个技能、参数怎么填、要不要继续做后续处理。
  3. 模板输出层:统一生成 markdown 结构,保证格式稳定。

这里要注意,skills 不一定非要调用大模型。很多场景下,纯脚本反而是最可靠的。你说到底,模型在技能里的角色是“选择入口”和“解释结果”,而不是每一步都替代代码。这样设计,测试成本大大降低。

3.2 核心脚本实现:脚本要能独立运行

技能的核心脚本放在doc-summary/scripts/run.py。我要求每个技能脚本能单独在命令行里跑通,不依赖 Agent 框架。这样调试时可以直接python run.py --input-paths a.md b.md --output-path report.md,快速验证脚本本身有没有问题。

一个可运行的版本:

#!/usr/bin/env python3 import argparse import json import pathlib import re import sys from datetime import datetime def load_params_from_args(): parser = argparse.ArgumentParser(description="doc-summary skill runner") parser.add_argument("--input-paths", nargs="+", required=True, help="要汇总的源文件路径列表") parser.add_argument("--output-path", required=True, help="输出 markdown 文件路径") parser.add_argument("--style", choices=["brief", "detailed"], default="brief", help="汇总格式,brief 只保留摘要,detailed 保留完整内容") args = parser.parse_args() return { "input_paths": args.input_paths, "output_path": args.output_path, "style": args.style, } def clean_text(text: str) -> str: """清理文档,去掉空行和首尾空白,保证模板输出不乱。""" lines = [line.strip() for line in text.splitlines() if line.strip()] return "\n".join(lines) def extract_title(text: str) -> str: """提取标题:优先取 markdown 第一个 # 标题,否则取第一行。""" m = re.search(r"^#\s+(.+)$", text, re.MULTILINE) if m: return m.group(1).strip() first_line = text.splitlines()[0].strip() return first_line[:20] def build_entry(path, text, style): title = extract_title(text) body = clean_text(text) if style == "brief": body = body[:120] + ("..." if len(body) > 120 else "") return { "title": title, "path": str(path), "preview": body, } def main(): params = load_params_from_args() entries = [] for p in params["input_paths"]: fp = pathlib.Path(p) if not fp.exists(): print(json.dumps({ "status": "error", "error": "file_not_found", "path": p, }, ensure_ascii=False)) sys.exit(1) text = fp.read_text(encoding="utf-8") entries.append(build_entry(fp, text, params["style"])) output_lines = [f"# Daily Report {datetime.now():%Y-%m-%d}", ""] for i, e in enumerate(entries, 1): output_lines.append(f"## {i}. {e['title']}") output_lines.append(f"- Source: `{e['path']}`") output_lines.append("") output_lines.append(e["preview"]) output_lines.append("") out_path = pathlib.Path(params["output_path"]) out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text("\n".join(output_lines), encoding="utf-8") print(json.dumps({ "status": "ok", "output_path": str(out_path), "count": len(entries), }, ensure_ascii=False)) if __name__ == "__main__": main()

这段脚本有几个刻意的设计:

  • 输出只用 JSON,不上屏打印无关日志。这样上层 Agent 拿到 stdout 就能直接解析,不会被日志干扰。
  • 文件不存在的错误要单独返回一个file_not_found状态码,而不是直接抛异常。模型看到这个错误码,才能自己决定要修正路径还是向用户询问。
  • 所有路径参数都保留原样,由调用方保证位置正确,脚本只负责校验文件是否存在。这样脚本和文件系统环境的耦合最小。

如果技能需要更复杂的语义理解,可以在build_entry或后续步骤里接入模型,但建议把模型调用包装成一个独立函数,并且设置超时。否则一个技能因为网络调用挂掉,整个 Agent 任务都会卡住。

3.3 把技能注册进 Agent 主循环

脚本能跑还不够,得让 Agent 在合适的时候决定调用它。我的主循环加载逻辑非常简单:扫描skills/下的每个目录,读取skill.md和schema.json,组合成一个候选技能表,让模型决策。

import json import pathlib import subprocess def load_skill(skill_dir: pathlib.Path): return { "dir": skill_dir, "doc": (skill_dir / "skill.md").read_text(encoding="utf-8"), "schema": json.loads((skill_dir / "schema.json").read_text(encoding="utf-8")), "script": skill_dir / "scripts" / "run.py", } def load_all_skills(base_dir: str) -> list: root = pathlib.Path(base_dir) skills = [] for p in root.iterdir(): if (p / "skill.md").exists() and (p / "schema.json").exists(): skills.append(load_skill(p)) return skills def decide_and_run(agent, user_message: str, skills: list) -> dict: skill_descriptions = [] for s in skills: freeze = { "name": s["schema"].get("name"), "description": s["doc"].split("---")[2].strip() if s["doc"].startswith("---") else s["doc"], } skill_descriptions.append(freeze) prompt = ( "当前可用技能如下:\n" + json.dumps(skill_descriptions, ensure_ascii=False) + "\n用户需求:" + user_message + "\n" + "请返回 JSON:{\"skill\": \"技能名\", \"arguments\": {...}},只返回 JSON,不要多余解释。" ) # agent.complete 是对你实际使用的模型封装的抽象 decision = json.loads(agent.complete(prompt)) target = None for s in skills: if s["schema"]["name"] == decision["skill"]: target = s break if target is None: return {"status": "error", "error": "skill_not_found"} args = decision["arguments"] cmd = [ "python", str(target["script"]), "--output-path", args["output_path"], "--style", args.get("style", "brief"), "--input-paths", *args["input_paths"], ] try: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60) except subprocess.TimeoutExpired: return {"status": "error", "error": "timeout"} if proc.returncode != 0: return {"status": "error", "error": "skill_exec_failed", "stderr": proc.stderr[-500:]} return json.loads(proc.stdout)

这里的关键不是代码本身,而是“模型决策 + 脚本执行”这个闭环。模型只负责从技能列表里选一个,并生成参数;脚本负责把结果稳定做出来;返回结果用 JSON 结构化。整个过程可追踪、可回放。

实际项目里,我会额外加一层参数校验,比如用jsonschema在脚本执行前验证decision["arguments"],提前发现模型生成的非法参数。校验失败时,把失败原因返回给模型,让它修正后再试一次。这个“试错回环”能让技能调用的成功率从 70% 提到 95% 以上。

4. 调优与排查:Agent Skills 落地中的坑

4.1 技能不生效怎么办:问题排查速查表

我在实际维护 agent-skills 时踩过不少坑,很多问题不是模型笨,而是技能本身写得有问题。我把常见问题整理成一张表,方便遇到情况时对号入座。

症状可能原因排查方向解决思路
模型始终不调用技能技能 description 太宽泛,或和用户需求匹配度低检查模型决策日志,看它选择了什么工具重写 description,明确“适用于什么场景”,加一个真实示例
模型调用了错误的技能技能之间边界不清,存在大量重叠描述对比两个技能的 description,找出冲突点增加各自的 when_not_to_use,区分使用场景
参数生成错误schema 中字段说明含糊,或类型没有约束查看模型生成的 arguments 是否符合 schema给每个字段写更具体的描述,增加枚举值和默认值
脚本执行失败脚本依赖了不存在文件、环境变量或第三方包手工运行脚本,复现失败在技能目录 requirements.txt 里声明依赖,脚本加载时检查环境
输出解析不了脚本启用了交互模式,或者 print 了多余内容看脚本 stdout 是否严格为 JSON关闭交互,日志写入文件,stdout 只保留结构化结果
技能热更新不生效加载器缓存了旧 skill.md查看加载器是否每次重新读文件用文件哈希做缓存失效,或开发模式下禁用缓存
多个技能互相干扰同一个操作被拆到多个技能里,模型难以区分检查是否有技能重复合并重叠技能,或建立技能索引并压缩描述
调用超时技能执行内部调用了网络服务或模型查看脚本耗时给脚本设置超时,把长时间任务改成异步并返回任务 ID

这张表是排查问题的起点,不是终点。真正有价值的,是把每次故障都记录下来,反向补充到技能的skill.md里去。比如某次模型总是把“output_path”填成.txt,我就在 schema 的 description 里加了一句“output_path 必须以 .md 结尾”,之后这个问题就不再出现。

4.2 我的调优路径:从 60% 到 95% 成功率

如果你刚开始做 agent-skills,想快速提升稳定度,我建议按下面这套路径走。

第一步,先建一个黄金测试集。收集 5 到 10 条真实用户请求,覆盖这个技能的典型场景和边界场景,比如“文件不存在”“空文件”“超长文档”“多个文件合并”等。把这组请求固化成tests/下的 markdown 或 JSON 文件。

第二步,用脚本批量跑这些用例,记录三个指标:技能调用率、参数合法率、执行成功率。调用率是指模型是否在应该使用技能时选择了它;参数合法率是指模型生成的参数有没有通过 schema 校验;执行成功率指脚本本身是否成功返回结构化结果。

第三步,针对最低的指标专项优化。如果调用率低,改 description,把它放在决策 prompt 的更前面,或者增加一个触发关键词示例。如果参数合法率低,升级 schema,把模糊字段改成枚举,或者给字段加minLength、pattern等约束。如果执行成功率低,那多半是脚本逻辑问题,断点调试脚本就行,和模型没关系。

第四步,技能版本管理。每次修改后,skill.md里的 version 字段要递增。我在仓库里用 git tag 对应技能版本,例如doc-summary@2.3.0。技能变更后,跑一遍整个测试集,再决定是否合并到主分支。这听起来很重,但一旦技能数量超过十来个,没有版本约束一定会乱。

第五步,让模型给出决策原因。我在决策 prompt 里要求模型在返回 JSON 时附带reason字段,比如“因为用户提到要整理多份会议纪要,所以选择 doc-summary”。这样即使出错,也能从日志里看出模型当时的判断逻辑。这个字段不需要给用户看,只用于内部审计和调优。

我自己维护的这套技能库,从最开始每个技能 60% 上下的小时成功率,经过两轮迭代,普遍稳定在 95% 左右。剩余 5% 大多发生在用户需求和技能描述差异极大的突发场景,这类问题靠堆技能数量解决不了,反而应该回到产品设计层面去想,是不是该做一个新的技能,或者调整任务边界。

4.3 给新手的一点避坑建议

最后分享几个新手容易忽略的细节。

第一,技能不要一开始就做得很大很全。我见过有人想做一个“全能办公助手”技能,里面塞了文档处理、邮件发送、日程管理、图表生成。结果模型经常选了它却只执行一部分,因为技能内部分支太多,描述根本覆盖不过来。宁可拆成五六个小技能,每个只做一件明确的事。

第二,脚本的 stdout 要克制。任何print("开始处理...")之类的调试输出,都可能在线上成为解析炸弹。我统一规定:正常执行只输出一个 JSON 对误码,日志写入专用日志文件。

第三,测试夹具很重要。没有测试夹具,你很难判断改动技能是变好了还是变坏了。我习惯在每个tests/fixtures下放一组固定的输入文件和期望输出文件。每次升级技能,把夹具跑一遍,对比 diff,比任何 review 都有效。

第四,不要完全信任模型返回的参数。即便模型已经把技能选对了,参数也可能填错。我在加载器里用jsonschema.validate做一层校验,不合法的参数不执行脚本,直接返回给模型让它改。这一步至少能挡掉一半的无谓报错。

结尾:把技能库变成团队的共同资产

做 agent-skills 越久,我越觉得它的价值不仅仅在于“让 Agent 更聪明”,而是让团队的协作方式发生了变化。技能库不再是一两个人的 prompt 草稿,而是大家共同维护的标准操作手册。每个技能都有版本、有测试、有说明,新同事看一眼就能复用,出问题也能快速定位。

我个人在实际运行中最受益的一个做法,是在每个技能目录里放一个TEST_CASES.md,里面手工记录几组“当时为什么会这样设计”的输入输出。这些记录比任何架构文档都真实,因为它是实际踩坑沉淀下来的。以后你再往里加新技能,或者想重构旧技能,一翻这份记录就知道哪些行为是不能动的底线。如果你正打算给自己的 Agent 项目引入技能体系,不要急着追求大而全,挑两三个最频繁、最痛的任务先做起来,让技能先跑通,再慢慢沉淀成一套属于你们自己的 agent-skills。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询