ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI Agent技能层实战:从Prompt模板到可复用技能库的工程化之路

AI Agent技能层实战:从Prompt模板到可复用技能库的工程化之路 最近后台好几个朋友都在问同一个词agent-skills。有人以为它是一个开源项目有人把它理解成一类技能库的统称也有人直接问“我的 Agent 是不是缺了这层东西”。其实名字怎么叫不重要重要的是这个话题背后扎扎实实踩中了一个痛点AI Agent 能跑通 demo但一到真实业务就“只会聊天不会干活”就算会干活也沉淀不下来。我自己的体会是把 Agent 能力做厚的那个“技能层”比换更大参数的模型、堆更多的工具都更关键。一个再强的模型如果只能用一堆零散的 function call没有围绕真实任务把规则、工具、流程、异常处理打包成可复用的技能那它就像一个刚入行的实习生聪明但没有章法。agent-skills 想解决的问题就是把这个“章法”沉淀成目录、规范和执行体让 Agent 真正具备可维护、可组合、可复制的工作能力。这篇内容不会讲太多空泛概念主要分享我在技能库搭建、技能注册、技能组合和线上故障排查里的真实经验。适合正在做 Agent 应用、想把手头 prompt 工程往工程化方向推一步的开发者如果你刚接触这个概念也能从第二、三节的内容里把“技能到底是什么”彻底搞明白。1. 先搞清楚Agent 缺的不是模型是“技能层”1.1 从 prompt 模板到技能库的演变早期做 Agent大家普遍的做法是写一长串 prompt把任务步骤一步步写在系统提示词里。比如“你是客服助手第一步先判断用户意图第二步查询订单状态第三步根据状态套用话术”。这种方式在 demo 阶段没问题可一旦任务变多prompt 会膨胀到几千 token模型每轮都要重新理解全部规则改一句话就要重新调参线上出了错也只能靠肉眼翻日志。说白了prompt 模板把“做事步骤”写在文字里但它既不能被程序校验也不能被复用。后来有了 function calling 和 MCPAgent 可以调用外部工具了。这解决了“动手”的问题查天气、发邮件、算汇率、读数据库都能做成一个 API 暴露给模型。但工具是原子化的一个工具只做一件事不携带任何任务上下文。以“生成周报”为例你可能需要先查项目进度、再查工时、再调历史周报模板、最后汇总刻薄语言。这几个工具可以一个个被调用但“先干什么、后干什么、什么情况走什么分支”这件事依然没有被固化下来。agent-skills 这类设计本质上是在 prompt 和工具之间加了一层“技能”一个技能包含明确目标、适用场景、执行步骤、依赖的工具、参数协议、异常分支以及沉淀下来的经验教训。模型拿到技能描述不是学一段干巴巴的说明而是拿到一个“带使用说明书和内部构造的模块”。这才是从“会调函数”到“会做事”的分水岭。下面这张对比能帮你看清区别能力形态表示方式复用性可维护性典型问题Prompt 模板自然语言步骤低依赖复制粘贴差改动牵一发动全身规则多了模型容易忽略Function/Tool接口定义执行函数中可被多个 Agent 调中但缺少任务上下文工具碎片化没有流程Skill元数据流程工具策略高可组合可版本化高独立测试独立发布需要额外设计规范和运行时1.2 agent-skills 解决的核心问题如果你接手过一段时间的 Agent 项目一定会遇到三个问题知识分散、流程失传、质量不稳定。知识分散指的是每个人做 Agent 都在自己的 prompt 里塞规则同样的“退款校验逻辑”可能散落在五个文件里流程失传是指核心员工一走他脑海里那些“碰到 XX 情况要 XX 处理”的经验也跟着没了质量不稳定则表现为同一个任务上午成功下午失败只因为模型换了个采样参数。把这些沉淀成一个技能库之后逻辑就变了。每一项经验都是一个独立技能有版本、有测试、有调用入口。Agent 不再靠“临场发挥”完成任务而是从技能库里检索最合适的技能去执行。技能本身是代码和文档的结合体可以被 review、被评审、被回滚。我见过一个很典型的客服场景退款处理原来是一段 800 字的 prompt逻辑里混着规则、话术、工具调用格式。后来把它拆成“退款资格校验”“退款金额计算”“退款话术生成”三个技能每个技能都有独立的输入输出结构发现问题的速度从小时级降到分钟级。这就是技能层带来的工程收益。2. 技能定义长什么样从元数据到执行体的设计2.1 一份技能描述该包含什么我习惯把每个技能放在独立目录里整体结构大概是这样skills/ web-to-markdown/ SKILL.md run.py requirements.txt tests/ test_basic.json其中SKILL.md是技能的“门面”模型能不能正确调用它全靠这份描述写得好不好。常见字段包括--- name: web-to-markdown description: 当用户需要从指定 URL 抓取网页正文并转换为 Markdown 时使用。 version: 1.3.0 entrypoint: run.py input_schema: type: object required: [url] properties: url: type: string description: 需要抓取的公开网页地址 ---这里最容易被忽略的是description。它不是写给人看的文档标题而是写给模型看的“触发条件说明”。我见过太多的项目把描述写成“网页转 Markdown”结果模型遇到任何跟网页沾边的任务都调它遇到真正需要转 Markdown 的任务反而又因为描述太宽泛而犹豫。一份合格的技术描述应该包含三件事什么场景下用、输入是什么、有什么限制。比如这样当用户希望把某个公开网页的正文内容提取出来并转成 Markdown 格式时使用。输入为 URL输出为结构化 JSON包含标题和正文。仅支持静态页面不处理需要登录的页面。这样模型就能非常精准地匹配意图。2.2 技能依赖与运行时隔离技能是代码就会引入依赖。依赖一旦不隔离A 技能升级了 requests 库B 技能可能直接崩。所以我在技能目录里强制要求写明依赖文件和锁版本。Python 技能就用requirements.txt加requirements.lockNode 技能就配package-lock.json。如果有条件更推荐每个技能跑在独立容器里或者至少用独立虚拟环境。比依赖更隐蔽的是权限边界。技能一旦能被 Agent 调用就相当于给模型开了一个执行代码的口子。比如“网页转 Markdown”技能它其实需要网络访问权限不需要文件删除权限更不需要读取环境变量的权限。我会在技能元数据里声明 permissionspermissions: network: allow: [*] filesystem: read_workspace: true write_workspace: true shell: allow: []这个设计看起来有点重但线上事故往往就出在“顺手放权”上。我早期写过一个小技能内部用os.system执行命令参数来自模型生成的 JSON结果用户构造了一个恶意 URL把环境变量打印出来了。从那以后凡是能走专用库做的事绝对不让技能直接拼 shell 命令必须走 shell 的就白名单命令和参数格式。3. 手写一个技能并接入 Agent 的完整过程3.1 技能代码骨架直接用“网页转 Markdown”这个例子带你看一个技能执行体到底长什么样。这个技能在我的项目里跑得最多代码很短但足够说明接口约束。import json import sys from urllib.parse import urlparse from trafilatura import fetch_url, extract INPUT_SCHEMA { type: object, required: [url], properties: { url: {type: string} } } def run(params: dict) - dict: url params[url] parsed urlparse(url) if parsed.scheme not in (http, https): return {ok: False, error: unsupported_protocol} html fetch_url(url) if not html: return {ok: False, error: fetch_failed} markdown extract(html, output_formatmarkdown) if not markdown: return {ok: False, error: extract_empty} return {ok: True, content: markdown} if __name__ __main__: params json.loads(sys.stdin.read()) result run(params) print(json.dumps(result, ensure_asciiFalse))注意我看重的是输入输出都走 JSON。这个约定不是拍脑袋定的而是为了兼容不同语言的技能。无论技能内部是 Python、Node 还是 Go对外只需要保证“吃进 JSON吐出 JSON”主框架就不用关心每个技能的实现了。很多 Agent 项目死在“技能接口不统一”上最后只能靠胶水代码拼凑。3.2 注册与动态发现技能写好了接下来要让它被 Agent 发现。我的做法是在框架启动时扫描技能目录读取每个技能目录里的元数据文件然后把它转换成模型能理解的 tool schema。这样新增技能时完全不用改主程序扔进目录、重启服务或者触发动态加载就完事。async def register_skills(repo_path: str) - list[dict]: tools [] for skill_dir in Path(repo_path).iterdir(): if not skill_dir.is_dir(): continue meta parse_skill_meta(skill_dir / SKILL.md) tools.append({ type: function, function: { name: meta[name], description: meta[description], parameters: meta[input_schema], } }) return tools这里有几个容易踩的坑。第一技能名必须全局唯一否则后面的技能把前面的覆盖了排查起来像鬼打墙。第二description 一定不要拼接太多动态内容否则 token 消耗会非常夸张。第三如果技能数量超过几十个不能全部塞给模型要做检索召回这块我在第五节细讲。3.3 一次调用链路演示我们模拟一个真实用户请求“帮我把这篇网页内容转成 Markdown 存到本地”。完整的处理链路是这样的用户请求进入主 Agent主模型看到“转成 Markdown”这个意图会去匹配已注册的技能。匹配到web-to-markdown后模型按输入结构生成一个 JSON 参数比如{url: https://example.com/article}。框架拿到这个参数直接调run()脚本执行完把结果 JSON 回传。主模型看到返回结果后再负责跟用户对话“已经转换完成共生成 2356 字需要我保存到本地吗”这个链路里技能本身不负责“理解用户”也不负责“对话”它只负责把一件事做扎实。理解用户的职责在主 Agent执行的职责在技能这种分工一旦清晰系统的每一层都变得可优化。模型太笨就换模型技能出错就修技能互不干扰。4. 技能不只是单点能力组合与编排4.1 两种编排方式Agent 自主编排 vs 工作流硬编排单个技能是积木真正有价值的点在于组合。比如“生成项目周报”这样的任务至少需要三个技能的协作查询项目进度、查询团队的工时记录、根据模板生成报告。问题是这三个技能怎么串起来我见过两种极端思路。一种是全交给 Agent 自己编排模型自己决定先调哪个技能、再调哪个技能。这种方案灵活但缺点也很明显模型可能会漏掉关键步骤或者在上一步失败后硬着头皮继续走产出一份错误百出的结果。另一种是用工作流引擎硬编码流程第一步必须调 A第二步必须调 B失败就终止。这种方案稳但每新增一个场景就要写一段代码Agent 的“智能性”完全没发挥出来。我的建议是折中对于关键路径用 workflow 硬编排对于非关键路径允许 Agent 自主发挥。比如周报生成先查项目进度和工时这两个动作必须严格按顺序执行不能乱也不能跳但最后生成报告的措辞、详略、风格可以让模型自己决定。agent-skills 这种“技能目录”模式天然适合这种折中——每个技能保持独立外部用轻量级编排器把技能串起来。4.2 技能之间的数据契约多个技能协作时最让人头疼的是输出格式对不上。A 技能返回的是列表B 技能期望的是字典A 技能里的字段叫contentB 技能里叫text。这种字段错位在写 demo 时还能忍一旦进入生产环境就是事故温床。所以我给每个技能都定了输出 schema并且要求下游技能直接声明它接受哪些上游输出。以“生成项目周报”为例query_project_progress输出[{ project: agent-skills, status: dev, updated_at: 2025-03-20 }]query_worklog输出[{ user: 张三, hours: 6.5, date: 2025-03-19 }]generate_weekly_report输入{ progress: [...], worklog: [...] }这个设计是受到函数式编程启发每个技能都像纯函数输入输出可预期组合时才不会出岔子。实际落地时我会在技能的测试目录里放一批“契约测试用例”专门校验给定输入时输出结构是否符合预期。这样任何一个技能改了输出CI 第一时间就会报错而不是等到线上跑挂了才发现。4.3 用“元技能”控制编排流程除了普通技能我还会设计一类“元技能”——它不是直接处理业务而是用来调度其他技能。比如router技能负责判断用户请求该走哪个业务技能guardrail技能负责在技能输出返回给用户之前做一次合规检查planner技能负责把一个复杂目标拆成多个技能调用的序列。元技能的好处是把控制逻辑也变成可维护的资产。过去你在主 Agent 的 prompt 里写“如果用户想投诉先查订单再查客服记录”现在你可以做成一个customer_complaint元技能它内部声明了子技能清单和调用顺序。主模型只需要决定调用哪个元技能而不需要自己临场发挥一套流程决策负担小了很多效果也更可控。5. 保姆级踩坑记录技能化路上最常见的五个问题5.1 技能描述写得太抽象模型根本不会调用很多团队把技能当普通函数写描述一句话带过。比如“读取 Excel 文件”这看起来没问题但模型遇到“帮我看看这个表格里哪几个城市的销售额超过 100 万”时会把任务拆成“先读取 Excel再计算筛选”然后它去技能库里找发现只有“读取 Excel 文件”没有“Excel 数据筛选”于是只能返回一个“读取结果”让用户自己看。我后来把“读取 Excel”升级成“读取 Excel 文件并返回行记录列表支持按列名筛选、按数值条件过滤”模型就能直接用它完成筛选任务了。写描述时一个有效的方法写完自己先问一遍“如果我是模型看到这个描述知道它适合处理我这个问题吗”不确定就重写直到描述里能看到触发条件和边界。5.2 环境依赖不一致换个机器就挂技能本质是程序程序的第一死因就是环境依赖。我踩过一次很蠢的坑某个技能在本地跑得好好的部署到服务器后所有请求都失败查了半天才发现服务器环境缺少 CA 证书requests库发起 HTTPS 请求时直接报 SSL 错误。这种问题在单体应用里很容易发现但到了技能库这种“一个技能一个环境”的架构里问题会被放大。我的对策很简单每个技能必须有独立的依赖清单和固定版本核心技能要在干净的 CI 环境跑冒烟测试不能只在开发者自己的机器上测。对于跟外部网络打交道的技能我会在测试用例里加一条“检查系统信任证书是否存在”。这些看似琐碎的检查恰好是线上可靠性的基石。5.3 上下文膨胀技能说明太多当技能库超过几十个把所有技能的描述一次性塞给模型会带来两个问题一是 token 成本飙升二是模型反而“挑花了眼”。我有一次往主 Agent 里塞了 40 个技能描述结果模型开始频繁调用错误技能调用成功率不升反降。后来我把技能库改成两级索引第一级是技能分类列表第二级是每个分类下的技能明细。主模型先决定“该走技术类还是业务类”再在对应分类里检索。如果不想自己写分类器也可以用向量检索把每个技能的描述和输入输出结构 embedding 化用户请求进来后先做相似度检索取 top 5 技能注入给模型。实测下来检索召回的方式比全量灌输在准确率和成本上都有明显优势。5.4 权限与安全边界技能是最近的特权入口技能最大的隐患是它给了模型“动手”的能力而模型并不真正理解安全边界。用户可能在对话里诱导模型去执行一个危险操作而模型只是忠实调用了技能。比如一个“网页内容分析”技能内部有爬取功能如果它在爬取时把用户提供的 URL 直接拼进命令行攻击者就能通过构造 URL 执行任意命令。我的安全底线是命令行参数绝不直接拼接用户数据一律用白名单或专用库技能需要访问网络时尽量限定协议和域名需要访问文件系统时限定在指定工作目录内。另外高危技能必须在执行前加一道人工确认不要全自动放行。这不是小题大做一旦技能被恶意利用背锅的还是自己。5.5 没有评测体系技能越改越不靠谱技能是代码代码最怕没有回归测试。我见过团队反复调技能描述这次把 A 场景调好了下次某次改动又把 B 场景搞坏了但大家靠感觉找不到原因。真正的解法是建立一套最小评测集准备 20 到 50 条带标准答案的测试用例每次改动技能后都跑一遍看调用成功率、输出结构正确率、用户反馈等指标。我给技能库做了很简单的评测脚本每次变更触发 CI 跑一遍pytest tests/ --junitxmlreport.xml测试用例不止包含输入输出还包含“这种请求不应该调用该技能”的负样例。比如web-to-markdown的负样例是“帮我把这段剪贴板里的文字转成 Markdown”它没有 URL所以不应该命中。负样例能有效防止技能描述范围过大导致的误召回。6. 把 agent-skills 落地到自己的项目从 0 到 1 的路线6.1 先别追求多挑高 ROI 的三个技能开始如果你现在还没搭建技能库别急着把所有功能都技能化。我的经验是先挑三个“高频、窄边界、结果易校验”的场景。高频保证投入产出比合理窄边界保证模型容易理解触发条件结果易校验保证你能快速判断技能是否正常工作。比如“网页转 Markdown”“Excel 转 JSON”“订单状态查询”都是很好的开局选择。这三个技能跑顺之后你会自然积累出技能目录设计、描述撰写、测试模板、参数校验的一整套经验。有了这套经验再往业务深处扩展就会快很多。反之一上来就做“全能助理”这种大杂烩技能大概率会在调试模型调用的泥潭里挣扎一个月。6.2 团队协作技能评审像 code review技能不是一个人的玩具而是团队资产。我所在团队现在把技能库当成代码仓库来管新增或修改任何技能都要走 PR有模板、有评审、有测试。评审人是技能库维护者不是业务负责人——因为他能看出技能描述是否清晰、依赖是否越界、输出是否破坏兼容性。一个很容易忽略的细节是“僵尸技能”如果某个技能 30 天没被调用它的描述可能已经过时参数也可能失效。建议在技能元数据里记录 seen 时间和 last_used 时间定期清理。技术债不只是代码还有无人维护的“沉睡技能”。6.3 后续扩展版本化、私有技能库与社区共享技能做到一定规模后版本化是必然要求。我推荐使用语义化版本号技能 A 依赖技能 B 的 v1.2B 升级到 v2.0 时如果不破坏兼容要能在依赖声明里显式体现。再往下走你可以把技能打包成 OCI artifact推送到内部私有仓库让不同项目通过 registry 拉取。这样技能库就变成了整个组织的基础设施而不是某个项目里的一堆文件夹。我最近也在关注社区里共享技能库的做法。同一类问题比如“生成会议纪要”“抓取网页正文”“格式化 JSON”谁都可以做一个技能关键是谁的边界定义更好、谁的错误处理更细致。技能库的生态一旦形成Agent 的能力就不再取决于团队里某几个人的经验而取决于整个社区沉淀下来的方案。我个人在实际操作中的体会是技能化这条路没有终局它更像是对团队隐性知识的一次持续整理。每次把一段经验固化成技能就是把一次偶然的成功变成可复现的能力。如果你刚准备动手我的建议只有一条——不要迷信“全自动”先把一个技能从描述到执行体完整走一遍那种从“模型随机发挥”到“按套路办事”的转变你会上瘾的。
返回列表