☰
从提示词工程到 Agent 技能库:让大模型应用告别碰运气执行
2026/10/7 4:14:57 网站建设 项目流程

最近在整理 agent-skills 这个项目,说白了,就是给 Agent 搭一套“技能库”。做它的动力来自一个特别现实的痛点:现在大家对 Agent 的期待已经不是“能聊”,而是“能干活”。可真把 Agent 接进业务里就会发现,同样一个任务,模型今天做得好、明天做砸,换一个模型效果千差万别,换个场景之前配好的 Prompt 全得重来。agent-skills 想解决的问题,就是把 Agent 执行任务的方法沉淀成可复用的技能单元,让每一次执行都有章可依,而不是每次都在碰运气。

这篇分享不是讲概念,而是讲我怎么从零设计、落地、踩坑的全过程。适合三类人:正在给团队搭 Agent 应用的技术同学,研究提示词工程和工具调度的高级用户,以及想把自己工作流工具化的效率党。读完你可以照着思路搭一个最小可用的技能库,也能把你手上乱糟糟的 Prompt 整理成结构化技能。我尽量把每步的“为什么这么做”也讲清楚,这样你换到自己场景时才知道怎么调整。

1. 项目想清楚了什么,才开始做 agent-skills

1.1 Agent 的下一站不是“更会聊天”,而是“更会干活”

我看过很多 Agent 项目,Demo 阶段都挺惊艳,一上生产就露馅。原因在于模型本身并不“执行”任何操作,它只是生成文本、给出意图,真正落地靠的是工具调用。你把一堆工具函数抛给它,它确实能调,但每次都是临时发挥:先写一遍步骤,再试错,再修正,没有沉淀。同一个流程跑了十次,它可能用十种不同的方式绕到终点,甚至绕不到。

还有个更隐蔽的问题:你很难稳定复现一次好的执行。比如做竞品调研,上个月你精心调好的一套 Prompt,这个月换了模型版本,输出的结构就漂了;或者业务指标改了,你要在所有 Prompt 里逐个改,那真是改到怀疑人生。agent-skills 的核心思路,是把“做某件事的方法”固化成一份可加载、可版本管理、可组合的说明文件,让模型每次调用时只需要填充变量,而不是重新发明轮子。

拿我自己的一个场景举例。我每周要整理竞品动态,原来我会写一大段提示词:“你是一个市场分析师,请打开以下网站,提取价格、功能更新、融资动态,然后按表格输出……”每次都要写,每次模型输出格式还不一样。后来我把它变成competitor_watch技能,参数只有company_name和latest_urls,模型拿到技能说明后,按固定步骤执行,输出结构稳定,改业务需求时只改技能文件本身,其他什么都不用动。

1.2 skill 和工具、提示词到底有什么区别

这是做技能系统绕不开的问题。很多人觉得“技能不就是工具函数嘛”,或者“技能不就是把 Prompt 包一层吗”。如果只是包一层,根本没必要大动干戈。我区分它们的方式很简单,看三个维度:

维度普通 Prompt工具函数Agent Skill
表现形式自然语言说明可执行代码文档 + 代码 + 元信息
复用粒度按任务写整体提示按能力写单个函数按“完整任务域”封装
可组合性基本不可组合代码级组合声明式组合
维护成本高,分散在各处中,散落在业务代码低,集中在一个目录
调度方式人工选择模型直接调用模型按描述选择后再加载

工具函数解决的是“手”的问题,技能解决的是“怎么干活”的问题。举个例子,你有一个fetch_html(url)工具,模型能访问网页,但面对“帮我整理这三个竞品官网的导航结构,并对比首页主推卖点”这个任务,它依然不知道该按什么步骤做、先看什么后看什么、最终输出什么格式。技能就是把这套步骤、条件和结果约束写成一份“岗位说明书”,模型拿到说明书照章办事。

1.3 agent-skills 的边界:它不是什么

我在设计的时候也划了三条边界,防止把事情做复杂。

第一,技能库不做记忆和长期存储。Agent 的记忆是另一个系统的事,技能只关心“按给定参数执行”。虽然技能执行过程会产生中间结果,但那是缓存,不是记忆。

第二,技能不负责复杂路由。到底执行哪个技能,由调度层决定,技能库只提供“描述”,不提供“判断”。我的注册中心里每个技能都只带描述和参数定义,调度策略放在外部。

第三,技能库不强制任何大模型厂商。它只是定义了技能文件的规范和一个加载器。模型能用 GPT 的方式接,也能用开源模型接,只要把技能内容渲染进上下文就行。这让我可以随时换模型做 A/B 测试。

边界划清楚之后,实现起来就简单了。整个项目核心只有三个字:定义、注册、执行。

2. 技能如何设计与拆分,才是真正的核心难点

2.1 技能的三层模型:元信息、执行体、反馈接口

我设计技能时参考了函数接口和命令行工具各自的优点,把每个技能拆成三层。

元信息是给调度模型看的,等同函数签名。包括:技能名称、一句话描述、触发场景、输入参数、输出格式、版本号。这一层最重要,因为大模型是靠描述来“认领”任务的,描述写得差,技能再多也用不上。

执行体是给执行模型看的,包括一段结构化说明和一个可选脚本目录。结构化说明描述完成任务的步骤、约束、注意事项、输出模板;脚本目录放那些不能靠纯语言完成的操作,比如访问网页、解析 PDF、调数据库。

反馈接口定义异常处理方式和输出校验规则。比如提取网页信息时页面为空怎么办,结果字段缺失怎么办。没有这层,技能在执行链里一旦出错,会直接传染给下游。

这三层我一般拆成三个文件:SKILL.md放元信息和执行说明,scripts/放可执行脚本,rules.json放校验规则。为什么不把全写进一个文件?因为调度时只需要元信息和描述,不需要把几百行的执行细节全塞给模型,负担太重。按层拆开之后,调度器可以只读头部,执行时才加载正文。

2.2 技能描述怎么写,模型才认账

这是最容易被低估的部分。技能描述的受众不是人,是调度模型。模型靠“语义相似度”来匹配任务和技能,所以描述里必须包含三样东西:任务的典型触发方式、输入的约束条件、它明确不做的事。

我踩过的反面典型是这样的技能描述:

处理网页。

这等于没写。模型根本不知道什么时候该用、输入是什么、输出是什么。我后来统一改成动词开头的结构化描述:

抓取一组 URL 的正文内容并生成结构化摘要,适用于需要从多个网页中提炼要点的场景。输入必须为公开可访问的 HTTP 链接数组;不处理需要登录的页面,不处理视频和音频内容。

对比一下,后面这个描述信息量大了很多。模型遇到“帮我把这三篇文章总结一下”时,召回这个技能的概率会高很多,因为它能匹配上“多个网页”“提炼要点”这些语义。

我还加了一个“反例”字段,专门写这个技能不处理什么。主要原因是模型会过度泛化:web_research技能火了之后,模型连“给我讲一下量子计算”这种不需要联网的任务都想调度它。加上反例之后,误触发率明显下降。

2.3 原子技能与复合技能的拆法:螺丝钉和发动机

把技能拆到多细,这是个分寸问题。拆太细,调度次数多、上下文开销大;拆太粗,一个技能背后裹着一大串逻辑,改一处就要动全部。我的标准就一条:能不能单独测试?能单独测试且结果可验证的,就是原子技能;需要编排多个原子技能的,就是复合技能。

用智能体做竞品调研举例。最粗的一层是“做一份竞品调研报告”,它不是技能,是任务。往下拆:搜索候选公司列表、抓取竞品官网、提取产品更新、生成对比表。其中“抓取竞品官网”可以继续拆成“获取 robots 检查”“访问首页”“过滤正文”“保存 Markdown”,这时候每个环节都能独立测,就算原子技能。而“生成对比表”依赖前面多步结果,自然就成了复合技能。

复合技能的执行体我不会写具体操作,而是写编排列表,声明依赖哪些原子技能和它们之间的数据流向。相当于一张配方,调度器照着配方依次调用下层技能。这样做的最大好处是:底层的抓取逻辑改了,上层“竞品调研”完全不受影响。

3. 落地实现:从目录结构到一套能跑的技能库

3.1 目录结构:让技能像商品一样被检索

技能库规划好之后,落地就是组织文件。一个技能就是一份商品,有说明、有附件、有规格。我用这样的目录结构组织:

agent-skills/ ├── skills/ │ ├── competitor-watch/ │ │ ├── SKILL.md │ │ ├── scripts/ │ │ │ └── check_robots.py │ │ └── templates/ │ │ └── report.md │ ├── web-research/ │ │ ├── SKILL.md │ │ └── scripts/ │ │ └── extract_content.py │ ├── meeting-notes/ │ │ ├── SKILL.md │ │ └── templates/ │ │ └── notes.md │ └──>--- name: web_research description: 抓取一组公开 URL 的正文内容并生成结构化摘要,适合多网页信息归纳 trigger: - 给出一批链接要求总结要点 - 需要从多个网页提取同一主题信息 params: urls: type: array description: 公开可访问的 HTTP/HTTPS 链接数组 required: true focus: type: string description: 摘要侧重方向,如价格、功能、动态 required: false output: format: markdown checks: - 每个 URL 至少输出一个要点 - 标注信息来源 version: 1.2.0 negative: - 不需要登录凭证的页面才可用 - 不处理本地文件路径 --- # 网页信息提炼 你的任务是对用户提供的 URL 列表逐页提取正文,并按统一模板输出。 ## 执行步骤 1. 对每个 URL,调用 fetch_page 工具获取正文。 2. 删除导航、广告、页脚等无关文本。 3. 按 focus 参数提取重点信息。 4. 汇总为 Markdown 卡片,每张卡片包含来源、核心要点、原文链接。 ## 约束 - 如果某个 URL 无法访问,在结果中标记为【失败】并继续处理其他链接。 - 不要编造页面中不存在的信息。 - 所有内容必须来自实际抓取结果。 ## 模板 ### 来源:{url} - 核心要点:...

这个文件的写法有几个刻意的地方。trigger不是给执行模型看的,是给调度器做初筛用的;negative字段是我踩坑后加的,能显著降低误调度;output.checks是给反馈接口用的,模型输出之后可以用简单的规则校验,不满足就重试一次。

前面说元信息和执行体可以拆开,这里我统一放在一个文件里,用 front matter 切分。原因是单文件便于复制、迁移,对新手也更友好。解析时,我读 front matter 得到元信息,剩下的 markdown 正文作为执行体。如果以后技能复杂到几百行,再拆不迟,现阶段别过度设计。

3.3 技能管理器:一个扫描目录的注册中心

有了技能文件,下一步是写加载器。我的manager.py核心逻辑很简单:遍历skills/目录,解析每个SKILL.md的 front matter,把技能注册进字典,并生成registry.json缓存。核心代码如下:

import json from dataclasses import dataclass, asdict from pathlib import Path import re SKILL_ROOT = Path("skills") @dataclass class SkillMeta: name: str description: str version: str params: dict trigger: list negative: list path: str def parse_front_matter(text: str) -> dict: m = re.match(r"^---\s*\n(.*?)\n---", text, re.S) if not m: raise ValueError("SKILL.md 缺少 front matter") return json.loads(m.group(1)) def scan_skills(root: Path = SKILL_ROOT) -> dict[str, SkillMeta]: registry = {} for skill_dir in root.iterdir(): if not skill_dir.is_dir(): continue skill_file = skill_dir / "SKILL.md" if not skill_file.exists(): continue content = skill_file.read_text(encoding="utf-8") meta = parse_front_matter(content) registry[meta["name"]] = SkillMeta( name=meta["name"], description=meta["description"], version=meta["version"], params=meta.get("params", {}), trigger=meta.get("trigger", []), negative=meta.get("negative", []), path=str(skill_dir), ) return registry if __name__ == "__main__": reg = scan_skills() json.dump({k: asdict(v) for k, v in reg.items()}, open("registry.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)

为什么不用现成的 MCP 或插件加载器?因为我没有把所有工具都做成远程服务的需求,本地目录解析足够了,而且依赖少,换 Python 版本也不受影响。对多数项目和内部分享场景来说,一个文件 60 行解决加载问题,比引一个框架再学一遍配置划算得多。

registry.json生成出来之后,我日常都不直接读SKILL.md,而是读这个缓存。调度器只需要知道有哪些技能、描述是什么、参数长什么样。真正要执行某个技能时,才按path找到技能目录,读取正文和脚本。

3.4 把技能接到 Agent 上:一次完整的调度与执行

注册中心就绪,剩下来把技能灌注到 Agent 的上下文里。我的做法是分两步:先把所有技能的“描述列表”注入系统提示词;等模型选定了某个技能,再把它的正文注入第二轮对话。这样避免了每轮都塞满所有技能详情。整个过程大概是:

import json def build_system_prompt(registry: dict) -> str: intro = "你可以调用以下技能完成任务。选择技能时需要严格匹配用户意图和技能描述。\n\n" skill_lines = [] for name, meta in registry.items(): params_desc = ",".join( f"{k}({'必填' if v.get('required') else '可选'})" for k, v in meta["params"].items() ) skill_lines.append( f"## {name}\n描述:{meta['description']}\n参数:{params_desc}\n" ) return intro + "\n".join(skill_lines) def run_skill(skill_path: str, params: dict, llm_generate) -> str: content = (Path(skill_path) / "SKILL.md").read_text(encoding="utf-8") body = content.split("---", 2)[2] user_prompt = f"请按照以下技能说明执行任务,参数:\n{json.dumps(params, ensure_ascii=False)}\n\n{body}" return llm_generate(user_prompt)

我用llm_generate抽象了模型调用,这样换模型不用改业务代码。实际跑起来之后,我发现一个值得注意的点:用户参数必须用 JSON 单独传一遍,而不是混在技能正文里。因为技能正文是固定的模板,里面不该有具体值;具体值单独渲染,既方便日志留档,也避免了模板被用户输入污染。

这一步做好,Agent 就能稳定复现一套调研流程了。整个调用链路变成:用户任务 -> 调度模型看技能列表 -> 选中web_research-> 渲染技能正文 -> 模型执行时调用fetch_page工具 -> 按模板输出摘要。链路稳定之后,我的日志里再也没出现过“第二步忘了提取来源”这种低级问题。

4. 技能调优与组合逻辑:从“能跑”到“好用”

4.1 复合技能怎么组合:用配方而不是这段子

技能多了,一定会遇到组合问题。像“竞品周报”这种任务,单独的web_research和data_tidy都不够,它需要先把信息抓回来,再清洗成表格,再套模板。我一开始傻乎乎地在技能正文里写“先做 A 再做 B 再做 C”,结果模型顺序经常漂,A 执行了两次,C 给漏了。

后来我改成声明式配方:复合技能的正文不写操作步骤,只声明依赖哪些原子技能和它们之间的数据契约。

name: weekly_competitor_report depends_on: - web_research - data_tidy pipeline: - step: collect skill: web_research params: urls: "{company_urls}" - step: clean skill: data_tidy params: input: "{collect.output}" - step: render skill: report_builder params: input: "{clean.output}"

每个step的params支持引用上一步的输出,用{step.output}传值。这种配方比自然语言描述可靠得多,因为它是可校验的,缺了依赖就能立刻发现。而且每个步骤的日志天然分隔开,排查时一眼看到是哪一步出了问题。

4.2 怎么评估一个技能“好用”

技能不是写完就算完,要有量化指标。我给每个技能设了三个指标:触发准确率、执行成功率、输出稳定率。怎么测呢?我会收集过去两周的调度日志,统计三件事:技能被正确调用的次数占该调用的比例、技能内部步骤没有因为异常中断的比例、相同输入下输出结构一致的比例。

触发准确率低于 80%,说明描述写得有歧义,我会回去改description和negative。执行成功率低,要看是工具挂了还是技能正文步骤有 bug。输出稳定率低,多半是模板约束不够,我会把output.checks写得更死,比如“必须包含四个字段”。

这个评估体系最大的价值不是指标本身,而是逼着我去记录每一次调用。没有日志就没有度量,没有度量就不知道技能改得好不好。我甚至在SKILL.md里加了一个expected_cases字段,放两条经典的输入输出用例,每次改完技能先跑一遍用例,过了才敢发版。

4.3 技能要不要有状态

技能描述看起来像无状态函数,但在真实 Agent 场景里,它是需要有中间状态的。一个调研流程要跑三四个技能,中间结果如果全部丢在模型上下文里,成本高且容易被截断。我的做法是引入一个轻量的工作区。

agent-skills/ └── workspace/ ├── collect_output.json ├── clean_output.csv └── final_report.md

每个技能的输入输出都可以落盘到工作区,下游技能直接按文件名读取。相当于给一组技能提供了传参的通道,不依赖超长上下文。当然这带来了新问题:工作区文件会越来越多。我的清理策略是按任务 ID 分组,任务结束后保留一天,之后自动清理。这套机制没有做得很重,但足以支撑绝大多数调研类场景。

5. 我在实操中踩过的坑,一个个都填平了

5.1 技能描述冲突导致调度“串戏”

这是我最先遇到的问题。技能库建到七个左右的时候,突然发现模型经常用错技能。查日志发现,data_tidy的描述写的是“整理表格数据”,而另一个plan_analyze描述里也写着“对表格进行分析”,两个技能的语义边界重叠,模型拿不准就随机选,结果经常把清洗工具当成分析工具来用。

排查方法并不复杂,我把每条调度日志里模型实际调用技能的名字和输入参数的相似度打出来,一眼就能看到冲突。解决方式也简单:给描述加上“分工声明”。data_tidy的描述里明确写“只负责格式清洗,不做指标计算”,plan_analyze则强调“接收已完成清洗的数据”。从那以后我把所有技能描述都检查了一遍,专门找近义词和重叠场景。

5.2 技能列表把上下文塞爆了

技能一多,问题立刻变味。注册表里 30 个技能,每个描述按 100 token 算,光技能列表就占 3000 token,挤占了下游任务的推理空间。尤其我的完整技能正文里还带步骤、约束、模板,全部塞进去根本不行。

解决方法是分层加载:始终加载的是“轻量描述列表”,每个技能只保留一句话描述和参数名;等模型选定了候选技能,再把三四个候选的完整正文加载进来比较。等于把一次全局匹配变成了两轮粗排加精排。实测 token 占用降了约一半,而且调度准确率没有明显下降。

如果你技能库继续涨到几百个,粗排也不能扫全部描述,就需要给描述做向量索引,拿用户输入去召回最相关的十个技能再给模型。我们目前还没到那一步,但设计上留了接口,随时能替换。

5.3 技能里的脚本接口漂移

技能文件有版本号,但它的依赖没有。我吃过一次亏:web_research的脚本调用了一个外部接口,接口升级后字段变了,脚本没更新,技能直接崩了。更隐蔽的是它崩溃后模型会自动“编造”结果,导致下游收到了假数据。

现在我对所有技能脚本做两层防护。第一层是 golden 用例回归,每次技能文件改动或外部依赖升级,先跑/scripts/test.sh,里面有固定的输入输出对。第二层是输出校验,rules.json里写明关键字段必须有值,没有就标记失败,不让错误数据往下游传。这招虽然土,但救了我很多次。

5.4 常见问题速查表

现象可能原因解决方案
同一个任务有时调 A 技能有时调 B描述语义重叠补negative字段,明确分工边界
模型忽略了技能里的某个步骤步骤太长,模型注意力分散拆原子技能,或把约束写在模板强校验
技能执行了但输出格式不一致缺少输出模板校验加output.checks,失败重试
上下文开销过大所有技能正文全量注入分层加载,先粗排再精排
技能失败后模型编造结果没有失败反馈机制加规则校验,失败立即标记,不让错误数据外流
技能库改完旧任务不兼容参数契约破坏版本号 + golden 用例回归

我在实际维护中最大的体会是:技能库像代码库,更注重增量迭代而非一次成型。不要试图第一天就做出完美体系,先跑通两个高频技能,把描述、校验、日志的闭环建起来,剩下的技能自然会在这个框架里长出来。另外一个小技巧:每个技能都留一个examples字段,放两条真实调用记录,这对调试和新人培训都特别有用,比任何说明文档都好使。

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

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

立即咨询