ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从提示词工程到 Agent 技能库:让大模型应用告别碰运气执行

从提示词工程到 Agent 技能库:让大模型应用告别碰运气执行 最近在整理 agent-skills 这个项目说白了就是给 Agent 搭一套“技能库”。做它的动力来自一个特别现实的痛点现在大家对 Agent 的期待已经不是“能聊”而是“能干活”。可真把 Agent 接进业务里就会发现同样一个任务模型今天做得好、明天做砸换一个模型效果千差万别换个场景之前配好的 Prompt 全得重来。agent-skills 想解决的问题就是把 Agent 执行任务的方法沉淀成可复用的技能单元让每一次执行都有章可依而不是每次都在碰运气。这篇分享不是讲概念而是讲我怎么从零设计、落地、踩坑的全过程。适合三类人正在给团队搭 Agent 应用的技术同学研究提示词工程和工具调度的高级用户以及想把自己工作流工具化的效率党。读完你可以照着思路搭一个最小可用的技能库也能把你手上乱糟糟的 Prompt 整理成结构化技能。我尽量把每步的“为什么这么做”也讲清楚这样你换到自己场景时才知道怎么调整。1. 项目想清楚了什么才开始做 agent-skills1.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(encodingutf-8) meta parse_front_matter(content) registry[meta[name]] SkillMeta( namemeta[name], descriptionmeta[description], versionmeta[version], paramsmeta.get(params, {}), triggermeta.get(trigger, []), negativemeta.get(negative, []), pathstr(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, encodingutf-8), ensure_asciiFalse, indent2)为什么不用现成的 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(encodingutf-8) body content.split(---, 2)[2] user_prompt f请按照以下技能说明执行任务参数\n{json.dumps(params, ensure_asciiFalse)}\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字段放两条真实调用记录这对调试和新人培训都特别有用比任何说明文档都好使。
返回列表