ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型Agent技能库:从Prompt到可复用技能包的工程实践

大模型Agent技能库:从Prompt到可复用技能包的工程实践 最近好几个团队朋友都在聊同一个话题大模型 Agent 跑起来不难三行代码就能让模型调工具可真要多步任务稳定执行、跨项目复用几乎每个人都在重复造轮子。我自己的做法是把这类可复用的能力沉淀成一套结构化的技能包也就是标题里的agent-skills——给 Agent 用的技能库。这个项目断断续续折腾了两个多月中间推翻过两版设计现在算是跑通了从“写技能”到“被 Agent 自动调用”的完整链路。这篇就把它拆开讲清楚。先说清楚这东西适合谁看。如果你正在做基于大模型的 Agent 应用比如客服机器人、自动化运维助手、代码生成工作流或者你发现同一个提示词、同一段工具代码在各个项目里来回复制粘贴那你大概率能从这套思路里省下不少事。不需要多深的算法基础只要写过 Python、用过 OpenAI 或类似接口就能跟着落地。1. 先搞清楚agent-skills 到底解决什么问题1.1 从一次失败的多步任务说起最开始我把一个“资料整理助手”做成了单体 Prompts效果还挺好系统提示词里塞了十几条规则让它能从网页里提取信息、按模板生成摘要、再格式化成 Markdown。可一旦任务变成“先查 A 地天气再安排 B 地行程”模型就开始乱来步骤一多指令就冲突甚至出现工具调用参数串台的离谱错误。后来我把同一个任务拆给专门的子 Agent 各管一段问题缓解了但新麻烦来了子 Agent 的配置、提示词、回调逻辑全部散落在不同文件里换一个项目就要重新调一遍。你复制过去的不是代码是半年前某个凌晨调通的玄学参数。1.2 agent-skills 的本质把一次性的灵感沉淀成可复用的能力agent-skills的核心思路其实非常朴素把 Agent 某项可独立完成的能力——不管它是纯文本处理、调用外部 API、操作结构化数据还是组合多个 API 的流程——封装成一个带描述、带输入输出契约、带验证逻辑的独立模块。每个技能就像工具箱里的一把专用扳手Agent 在对话中自己决定什么时候用哪一把。关键区别在于它不是又一套 Prompt 模板而是“描述 逻辑 校验”三合一的完整包。描述告诉模型这个技能是干什么的、适合什么场景逻辑是真实可执行的代码或 API 调用校验保证输出符合预期错了能给出修复信号而不是让模型硬编。1.3 和 Prompt、Workflow、Plugin 到底有什么区别这个坑我一开始也踩过。很多人看到“技能”两个字就开始往 Prompt 工程上靠但实际上它们解决的是不同层面的问题。用表格对比最直观形态解决的问题粒度是否包含代码逻辑复用方式Prompt / Few-shot控制模型行为风格单次对话级否复制粘贴Workflow串联多个固定步骤任务流程级通常是配置化的导入流程文件Plugin / Tool让模型能调用外部能力函数级是注册到运行时Agent Skill让模型能自主调用一项完整能力能力模块级是装进技能库拿做饭类比Prompt 是菜谱上的“盐少许”Workflow 是整套宴席的上菜顺序Plugin 是燃气灶开关Agent Skill 则是“做番茄炒蛋”这一道菜的完整做法——告诉你该不该做、需要什么材料、火候怎么控制、失败怎么补救。这也是为什么技能化之后Agent 面对复杂任务时忽然“靠谱”了许多它不再靠提示词碰撞出答案而是调用一个已经被验证过的工序。2. 整体设计与目录规划怎么组织一套技能库2.1 技能的分类维度技能拆到什么粒度合适是我试错最多的部分。拆太细比如把“读 CSV”和“读 JSON”分成两个技能模型选型时容易犹豫拆太粗比如做一个“数据处理”大技能内部逻辑又长又难维护。我最终采用的分类维度是“按能力域 按复杂度”两轴切分。能力域分成文本处理、数据抽取、API 集成、格式转换、流程编排这五类复杂度则分成单步技能一个函数搞定和多步技能内部要编排多个 API。单步技能追求“小而专”描述越精确越好多步技能讲究“接口简单内部复杂”对外只暴露一个入口。这套分类直接影响后面技能路由的准确率。模型做技能选型时主要靠的是描述文本和参数签名分类清晰、命名统一选型精度能明显提高。实测在同一批测试集上分类调整后技能命中率从 71% 提到 89%效果非常直接。2.2 目录结构与命名规范技能库的目录结构我改过三版最终采用如下方案兼顾了人工阅读和自动加载agent-skills/ ├── manifests/ # 技能描述清单供 Agent 运行时加载 │ ├── web-search.yaml │ ├── csv-summarizer.yaml │ └── meeting-notes.yaml ├── skills/ # 技能实际代码 │ ├── web-search/ │ │ ├── __init__.py │ │ ├── skill.py │ │ └── requirements.txt │ ├── csv-summarizer/ │ └── meeting-notes/ ├── examples/ # 每个技能的示例输入输出 │ ├── web-search.in.json │ └── web-search.out.json └── tests/ # 技能的自动化测试命名规范我建议统一用[动作]-[对象]的形式动词放前面。比如search-web比web-search-tool好summarize-document比document-processor好。原因很简单模型在理解自然语言指令时动词是最强的意图信号把动词放在名称开头能让技能路由的匹配分数更集中。2.3 技能元信息让 Agent 自己找到合适的技能每个技能配套一个 YAML 格式的描述文件这是整个技能库的逻辑中枢。一个合格的描述文件至少要包含name: summarize-document description: 对长文档进行结构化摘要支持按章节提取要点、生成一页纸简报。 适用于报告、论文、会议纪要等文本。 when_to_use: 当用户请求“总结”“摘要”“提炼要点”且输入是超长文本时。 input: document: string, 待处理的文档全文或路径 max_length: integer, 摘要最大长度默认 500 output: summary: string, 结构化摘要 key_points: array, 要点列表 tags: [text-processing, summarize]when_to_use字段是我加了之后效果提升最明显的一个它相当于是给模型的一个“使用提示”让选型从猜变成了匹配。描述里写“适用于报告、论文”比写“对输入进行摘要处理”要好用得多因为后者几乎所有文本处理技能都能套上。3. 从零实现一个 Agent Skill完整实操3.1 技能描述文件的写法别把描述文件写成给开发人员看的接口文档它是给模型读的。这意味着要用“场景化语言”而不是字段说明书。我一般遵循三条原则。第一描述里必须包含触发条件也就是when_to_use告诉模型什么情况下该用、什么情况下不该用。比如网页搜索技能我会明确写“当用户询问实时信息、最新新闻、特定网站内容时使用当用户只需要常识推理时不要使用”。这一句能让模型少做大量无效调用。第二字段命名要语义化。输入参数的 key 不要用arg1、data尽量用document、query、max_results这种一看就懂的名字。模型能不能把用户的问题正确映射到参数上靠的就是这个。第三描述里写清楚输出结构。模型是概率生成你不告诉它要什么结构它就自由发挥你给了key_points: array它会老老实实把要点装进数组。实测结构化描述能把输出格式错误率降低六成。3.2 核心执行逻辑一个技能的最小可运行实现技能本体就是一个普通 Python 类统一继承一个基类接口。为了保持框架无关性我不让它依赖任何特定的 Agent 框架只暴露run方法。下面是一个文档摘要技能的骨架# skills/summarize-document/skill.py from dataclasses import dataclass, field from typing import Any, Dict dataclass class SkillInput: document: str max_length: int 500 dataclass class SkillOutput: summary: str key_points: list field(default_factorylist) class SummarizeDocumentSkill: 给 Agent 用的长文档摘要技能。 name summarize-document version 0.1.0 def __init__(self, llm_client: Any): self.llm llm_client async def run(self, data: Dict[str, Any]) - Dict[str, Any]: inp SkillInput(**data) # 1. 分段加载避免超长输入截断 chunks self._split_document(inp.document, max_chars2000) # 2. 对每段做粗粒度摘要 partial_summaries [] for chunk in chunks: partial await self.llm.complete( f请用三句话概括以下内容\n{chunk} ) partial_summaries.append(partial) # 3. 合并粗摘要生成整体结构化摘要 merged \n.join(partial_summaries) final await self.llm.complete( f基于以下分段摘要输出一份不超过 {inp.max_length} 字的整体摘要 f并列出3-5个核心要点。\n{merged} ) return SkillOutput( summaryfinal, key_pointsself._extract_points(final), ).__dict__这段代码本身不复杂但它体现了一个重要设计技能的内部实现是“块状”的每块只做一件事模型只在两个节点介入——粗摘要和最终整理。这样做的好处是即便其中一段的内容超长也不会一次性把上下文打爆坏处是多次调用模型增加延迟所以我在粗摘要阶段用的模型是便宜的那档最终合成才用强模型。3.3 参数选择与验证不是跑通就行技能写出来必须验证而且是自动化验证。我每写一个技能都会配套一个 Python 的unittest测试但重点不是测试内部逻辑而是测试“输入边界”和“输出契约”。# tests/test_summarize_document.py import pytest from skills.summarize_document.skill import SummarizeDocumentSkill class FakeLLM: async def complete(self, prompt: str) - str: return 这是模拟摘要用于测试流程是否跑通。 pytest.mark.asyncio async def test_summarize_structure(): skill SummarizeDocumentSkill(llm_clientFakeLLM()) result await skill.run({ document: 一段足够长的测试文本。, max_length: 100, }) assert isinstance(result[summary], str) assert isinstance(result[key_points], list) assert len(result[key_points]) 1 pytest.mark.asyncio async def test_empty_document_raises(): skill SummarizeDocumentSkill(llm_clientFakeLLM()) with pytest.raises(ValueError): await skill.run({document: })边界测试特别重要。Agent 调用技能的输入是由模型生成的模型有时候会把字段拼错、传空值、传了意料之外的类型。如果你不在技能内部做防御性校验一个脏参数就能让整条技能链崩掉。我在每个技能入口统一加了一套入参校验逻辑凡是必填字段缺失或类型不符直接返回一个可读的错误信息而不是抛 500。实际跑下来的经验是技能开发的时间分配大概是40% 写核心逻辑30% 写描述文件和元信息30% 写测试和修边界。千万别跳过最后两步否则等技能多了之后维护成本会直线上升。4. 让 Agent 在对话中自动调用技能4.1 技能路由与选型策略技能库有了下一步是让 Agent 在对话里自己决定调用哪个技能。这一步的工程质量直接决定使用者体感它比技能本身还重要。我试过三种方案。最朴素的是嵌入拼接把所有技能的描述文件一次性拼进系统提示词让模型在每轮对话前先输出一个“技能选择”字段。优点是零额外架构缺点是一旦技能超过十几个提示词膨胀、选型准确率骤降而且浪费 token。第二种是基于相似度召回把用户当前请求向量化跟每个技能的描述向量算余弦相似度取 Top-3 再让模型从中选择。优点是可扩展几百个技能也能扛。缺点是需要引入向量检索组件增加了部署复杂度。第三种我现在用的是结合函数调用的工具注册机制把技能转换成 Function Calling 格式{ type: function, function: { name: summarize-document, description: 对长文档进行结构化摘要适用于报告、论文、会议纪要。, parameters: { type: object, properties: { document: {type: string, description: 待处理的文档全文}, max_length: {type: integer, description: 摘要最大长度} }, required: [document] } } }模型输出的 tool call 参数天然是 JSON我只要做一层解析把参数传给skill.run()然后把返回值以 tool 消息回传给模型Agent 就能基于技能输出继续对话。这套方案的选型准确率最高因为模型经过函数调用特化训练对 function schema 的遵循能力比对长文本描述的遵循能力强得多。4.2 上下文构建与注入技能执行完之后返回结果要不要全部塞回对话上下文容易被忽略但影响很大。早期我把摘要技能的全文都回传下一轮对话里模型就开始引用中间过程的噪声。后来的做法是技能自身在返回前就做好“为对话准备”的格式化只保留最终结论和必要证据。一个可复用的模式是给技能输出定义两种视图full和conversation。full用于日志和调试包含完整中间量conversation是专门写给模型下轮对话看的压缩文本。在技能基类里我加了一个to_conversation_text()方法默认实现是json.dumps但每个技能可以覆写它输出更符合自然语言的描述。上下文注入的另一个细节把技能返回的“结构信息”和“事实信息”分开。结构信息是比如“摘要包含 3 个要点”这种元描述事实信息是真正的摘要内容。模型创作后续回复时优先依赖事实信息结构信息只是辅助。5. 我踩过的坑与排查技巧实录5.1 常见问题速查表现象可能原因处理办法模型总是选错技能描述文件里没写清触发条件补when_to_use用正反两个例子区分边界技能参数频繁传错参数名语义不清改为人读的字段名必填字段标注required技能内部报错Agent 直接放弃错误信息不可读模型无法修复统一错误返回格式附上可执行的修复建议长文本摘要丢失尾部内容模型输入超长被截断技能内部分段处理先分段后合并多技能共存时接口冲突不同技能对同一含义用了不同参数名建立技能间共享的字典命名约定统一基类技能调用超时内部多次串行调 LLM粗摘要阶段换更快模型或改成并发分段第一条值得展开说。我最早给一个天气技能写的描述是“获取指定城市当前天气”模型在用户问“明天要不要带伞”时死活不调用反倒是问“北京现在多少度”才能触发。加了when_to_use: 当用户询问天气、降雨概率、穿衣建议、出行准备时使用之后几乎不再漏选。描述文件里的每一句话都要站在“模型会在什么表达下搜索这个技能”的角度来写而不是站在“程序员怎么理解这个技能”的角度。5.2 调试 Agent 技能的独家方法技能逻辑出错是最容易定位的因为它是确定性的。最难定位的是“技能被调用了但模型没用结果”——这种问题藏得深因为日志里看起来一切正常。我现在的做法是给每个技能加执行链路追踪。每次调用都会输出一个 trace 对象包含技能选型时的匹配分、入参 JSON、技能内部每个子步骤的耗时、返回给模型之前格式化后的消息文本、模型下一轮基于该消息生成的回复。把这些链路存下来之后凡是模型行为不符合预期第一件事就是看“格式化后的消息文本”是不是已经被截断或者噪声污染了。另外还有一个细节技能的requirements.txt依赖要尽量精简。不同技能之间的依赖一旦冲突整个库的加载都会失败。我踩过一次 scikit-learn 版本冲突导致服务起不来的坑后来立了规矩技能只允许依赖轻量工具库重模型、重框架一律从技能外部注入不做技能内建依赖。调试时还有一个好用的技巧给每个技能写一个--selftest的 CLI 入口。进入命令行之后直接运行一次技能传入构造的样例数据立刻能看到输出结构是否符合预期。这套方式在技能数量超过二十个之后特别受用重构公共基类时可以一键全量回归不用靠脑子记哪些技能功能正常。5.3 关于技能库扩展的经验一个技能库做到后面真正难的已经不是写技能了而是让技能库保持良好的可演进性。我现在的做法是每个月做一次“技能健康度检查”看哪些技能长期没有被 Agent 选中哪些技能经常报错。超过一个季度没被调用的技能我会先退休掉不删除移到archived/目录避免它在技能列表中占着位置干扰模型的选型判定。技能版本管理也要重视。技能描述文件里有version字段运行时加载时会做校验但真正有用的是通配符匹配Agent 配置里写summarize-document:0.1.0框架会自动选最新兼容版本。这样某个技能升级后有问题可以快速在总配置里锁定到旧版本而不必回滚整个仓库。最后再分享一个对稳定性提升很大的小习惯所有技能上线前都先把它“反向逼问”一遍——假如我是用户我会用哪些说法、哪些方言、哪些不完整的表达来描述这个需求把这些说法全部写进测试用例模拟一遍端到端的调用模型漏配、错配参数的场景就提前消化在了开发阶段。Agent 应用跑得稳不稳定很多时候不取决于模型多聪明而取决于边界情况被覆盖得多充分。这个习惯我用了大半年技能库的线上容错率明显比之前几版方案高出一截。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进