☰
AI Agent技能系统实战:从Prompt膨胀到可编排的Skill单元
2026/10/21 4:54:21 网站建设 项目流程

Agent的能力边界,很大程度上不取决于模型本身,而取决于你给它准备了多少把趁手的“工具”。这半年我一直在做AI Agent相关的应用,体会特别深:同样一个模型,有人调出来像个只会聊天的玩具,有人调出来能完成一整套跨系统任务,差别往往就在技能(skills)系统的设计上。

“agent-skills”这套方案,本质上是把传统塞在System Prompt里、靠模型自由发挥的能力项,抽出来变成一个个独立、可注册、可测试、可编排的技能单元。它解决的问题很具体:上下文爆炸、复用困难、行为失控。把技能当成一等公民来管理,每个技能都有独立的描述文件、参数协议、执行入口和测试用例,模型通过能力检索来发现它们,系统通过沙箱来执行它们。

这篇文章适合三类人:正在做Agent应用、被提示词越写越长但效果越来越不稳的开发者和产品经理;对Agent自主行为可控性有严格要求的技术负责人;以及想搞懂技能系统设计逻辑,准备自己从零搭一套的进阶学习者。我会从设计思路、技能格式、注册发现、编排调度、安全沙箱、测试监控几个层面,把这套体系拆开讲清楚,全程用我一个业余时间维护的内部项目agent-skills做例子。

1. 先看清“agent-skills”到底在解决什么问题

1.1 从一段Prompt到一套技能系统

最早做Agent的时候,我和大多数人一样,习惯把能力直接写进提示词。

比如做一个能查天气、写周报、订日程的助手,就在System Prompt里写“你可以调用天气查询接口,参数是城市名,返回JSON;你可以调用日程接口……”,然后把几个函数的说明、参数表、示例一股脑塞进去。拿一个模型试,前面几次效果还行;换一个模型,或者任务一多,就开始出问题——模型有时候记不住后面那个接口,有时候把参数拼错,有时候在不需要写周报的时候硬写一篇。

我记忆里印象最深的一次,是往Prompt里塞了二十多个工具说明之后,模型在对话中频繁提前输出“好的,我现在帮您调用”,但实际并没有任何参数被填充,纯粹是因为上下文太长,模型已经抓不住重点了。

后来我把能力项逐个抽出来,存成独立文件,每个文件就是一个技能。模型不需要“记住”这些技能,它只需要在需要的时候检索,找到合适的技能再调用。这就是agent-skills最初的出发点:把能力从提示词里搬到文件系统里,交给一套独立机制去管理。

1.2 没有技能系统时的三个坑

这里我把过去踩过的坑总结成三条,每条都很痛:

第一是上下文膨胀。工具说明放在System Prompt里,每轮对话都要跟着走。工具从几个涨到几十个之后,光工具说明就吃掉几千token,实际留给推理和记忆的空间被挤占,模型表现断崖式下降。技能系统把这些说明放到外部索引里,只有模型明确检索时才进入上下文,省下来的token非常可观。

第二是复用极差。老项目里写好的工具说明,新项目里根本没法直接搬,因为参数格式、命名习惯、触发条件全混在一段提示词里,复制粘贴过去还是一团乱麻。技能文件就不一样,一个技能的描述、参数、执行逻辑、测试用例都在自己的目录里,换项目直接拷目录就行。

第三是行为不可控。纯靠提示词约束模型,本质上是“祈使”,模型想做就做。技能系统可以通过路由规则和权限模型把某些技能设为强制触发或禁止触发,行为边界从“希望”变成“规定”。后来我管这个叫“从软约束到硬约束”。

1.3 这套方案适合谁,不适合谁

agent-skills的设计思路,最适合的是任务步骤相对清晰、可拆分成多个独立能力单元的场景,比如文档处理、数据抓取、报告生成、流程自动化这一挂。

反过来,如果你只是做一个单轮问答机器人,比如“根据资料回答问题”,那技能系统是过度设计,一个RAG管道就够了。再比如你的Agent主要是自由对话、情绪陪伴,强行塞技能系统反而显得生硬。技能系统的本质是给Agent提供“操作世界的接口”,如果它不需要操作世界,就不需要这套东西。

这也是我一直提醒自己的:技能系统不是万能药,它解决的是“多能力复用与可靠执行”的问题,而不是“所有Agent都该有技能”的问题。

2. 整体架构与关键设计决策

2.1 以“技能即文件”为核心理念

整套系统我最核心的一个设计决策,是把“技能”定义为文件系统里的一组文件,而不是代码里的一个对象或数据库里的一条记录。

为什么这么定?因为文件系统天然具备几个对Agent技能非常重要的特性:可版本管理、可审查、可单独测试、可用普通文件工具编辑。任何开发者都可以打开技能目录看它到底做了什么,而不用进数据库翻记录;任何技能升级都可以走代码仓库的合并请求,带评审、带改动历史。

一个技能的完整文件结构大概长这样:

skills/ convert_doc/ SKILL.md run.py requirements.txt tests/ test_convert_basic.py assets/ sample.docx

SKILL.md是技能的描述文件,也是模型和系统理解这个技能的唯一入口。run.py是具体执行逻辑,可以是Python脚本、Shell命令,也可以是调用外部API的封装。tests目录用来跑回归,assets放测试需要用到的样本数据。

文件结构本身就在约束团队:写一个技能,必须同时写描述、写实现、写测试。这个约束看起来简单,实际收益很大——很多技能写完后半年没人动,等到再捡起来的时候,光靠SKILL.md里的描述就能知道它是干什么的、参数是什么、什么情况下用,比翻聊天记录和代码注释靠谱多了。

2.2 技能、工具、插件的边界到底怎么划

刚上手的时候,很容易把“技能(Skill)”“工具(Tool)”“插件(Plugin)”混为一谈。我自己的划分方式是这样:

  • 工具(Tool)是最小的原子能力,比如“发送一封邮件”“读取一个文件内容”“调用一次数据库查询”。它不包含业务编排逻辑,参数和返回格式非常固定。
  • 技能(Skill)是面向一个目标场景封装好的能力单元,可以组合多个工具,也可以包含自己的编排逻辑和校验规则。比如“整理会议纪要并发送给参会人”,内部会用到语音转写、摘要生成、邮件发送三个工具。
  • 插件(Plugin)在我这里指的是带配置界面的集成包,一般会把好几个技能和工具打包到一起,面向特定平台或软件,比如办公套件插件、浏览器插件。

我用一个简单的分层表格说明:

层级粒度是否包含编排逻辑典型形态
工具最小否一个函数、一个API封装
技能中等是SKILL.md + 执行脚本
插件较大是技能集合 + 配置界面

这个边界明确了之后,至少解决了一个老问题:大家讨论“要不要做技能”的时候,其实很多人说的是“要不要封装工具”。工具解决的是“能不能做”,技能解决的是“怎么做得好、做得稳”,两者需要分开管理,不能混在一起排优先级。

2.3 生命周期是谁在管

技能不是写出来就完事的,它有完整的生命周期。我的系统里分为六个阶段:注册、发现、评估、调用、更新、下线。

注册阶段,系统启动时扫描skills目录,读取每个SKILL.md,生成技能索引。发现阶段,模型根据用户请求和技能描述做语义匹配,把候选技能拉出来。评估阶段,系统按匹配分数和调用历史给候选技能排序,排除权限不足的和明确禁用的。调用阶段,进入沙箱执行,记录日志和结果。更新阶段,技能代码或描述文件发生变化,索引自动刷新或手动刷新。下线阶段,技能标为deprecated,索引中不再向模型展示,但历史日志保留。

以前没有生命周期管理的时候,出现过一次很尴尬的情况:某个技能已经停止维护三个月了,但描述文件还在,模型还在频繁调用,返回的结果全是过期数据,用户投诉了一轮我们才发现是技能还挂在索引里。后来我规定,任何技能超过60天没有调用记录,自动进入冻结状态,不再参与技能匹配。这个机制出来之后,误调用的情况几乎绝迹。

3. 手把手搭建技能仓库

3.1 技能目录结构怎么设计

如果你准备从零搭一个类似agent-skills的仓库,第一步是把目录规范定下来,这个比写代码更重要。

我的技能根目录是skills/,里面按领域分子目录,领域下面才是具体技能。比如:

skills/ office/ convert_doc/ extract_report/ schedule_meeting/ data/ fetch_webpage/ clean_table/ save_to_db/ infra/ check_service_status/ restart_service/

为什么按领域分?因为技能数量上了五十个之后,平铺在一个目录里,无论是人工审查还是调试都很难受。按领域分组之后,同一个领域的技能可以共用一套基础依赖和配置,权限策略也可以按领域批量设置,省很多事。

每个技能目录内,我要求至少包含SKILL.md和一段可执行逻辑。可执行逻辑可以是脚本,也可以是一段调用远程接口的配置。为了方便其他协作者审查,我还强制要求写一个tests/目录,哪怕只有一个冒烟测试也行。没有测试的技能不能合并进主线分支,这个规则坚持下来,线上事故少了一大半。

3.2 技能描述文件怎么写才不会被模型误用

SKILL.md是整个技能系统最关键的接口。模型不读你的代码,它只读SKILL.md。所以这个文件写得好不好,直接决定技能被调用得准不准。

我用的字段和一份真实示例放在下面,你可以直接抄:

--- name: convert_doc description: 把常见的办公文档(docx、pdf、pptx)转换成干净的markdown文本。适合需要把文档内容输入给大模型做分析、检索、摘要的场景。如果输入文件本身就是markdown,不建议用本技能。 version: 1.2.0 author: internal-agent-team when_to_use: - 用户上传docx/pdf文档并要求提取文字 - 需要将办公文档统一成markdown格式做后续处理 - 文档内容需要去页眉页脚、去重复换行 when_not_to_use: - 输入已经是markdown文本 - 用户只是想查看文档内容,不需要转换格式 parameters: - name: file_path type: string required: true description: 待转换文档的绝对路径或相对于工作目录的路径 - name: engine type: enum default: default enum: [default, conservative, aggressive] description: 转换策略,conservative保留更多格式,aggressive更激进地清理样式 dependencies: - pandoc - python:3.10+ security: filesystem: read: true write: false network: false exec_command: true --- # convert_doc 将办公文档转换为markdown的工具类技能。内部调用pandoc完成格式转换,并在此基础上做清洗。 ## 输入说明 file_path指向要转换的文件,必须是用户明确提供的路径。engine参数控制清洗强度。 ## 输出说明 返回一个JSON对象,包含status和content两个字段。status为ok时content是转换后的markdown内容;status为error时content是错误信息文本。 ## 失败处理 如果pandoc没有安装,返回错误码1001,提示安装pandoc。如果输入文件不存在,返回错误码1002,提示检查路径。

这里我特别想提醒几个点:

description要写清楚“适合什么场景”和“不适合什么场景”。很多初版技能只写了description“把docx转成markdown”,结果模型在不该用的时候也用,该用的时候又犹豫。后来加了when_to_use和when_not_to_use两个字段,匹配精度提升非常明显。

参数描述要写上“用户明确提供”这类约束词。模型是会被上下文影响的,如果参数说明里没有强调来源,它有时候会编一个路径出来。加了约束之后,模型会更倾向于向用户索要路径,而不是自己瞎猜。

不要把SKILL.md写成代码说明书。它是给模型和人生成匹配信号的,不是给机器执行的API文档。术语要准确、场景要具体、边界要清晰。

3.3 注册与发现机制:匹配不是只有一种方式

技能文件写好了,接下来要解决一个问题:系统怎么知道有哪些技能可以用?

我的实现里,有两种触发路径。第一种是启动时扫描,系统启动后遍历skills/目录,把每个SKILL.md里的name、description、when_to_use、parameters等字段解析出来,构建成一个索引,供后续检索使用。这个方式简单可靠,适合技能数量稳定的场景。

第二种是动态注册,运行中通过API把新技能加入索引。适合技能由外部系统动态下发、或者技能市场需要实时上新的场景。动态注册有个坑:条目多了之后容易重名、版本混乱,因此我要求动态注册的技能必须带namespace前缀,比如company_a.report_fetcher,避免和内置技能冲突。

匹配阶段,我把“语义检索”和“规则路由”分开。语义检索是利用描述文本做embedding或关键词匹配,适合开放场景。规则路由则是写死的条件,比如“用户输入路径以.pdf结尾时,强制转给convert_doc”,适合特定场景的硬性绑定。

两条路径各有用处:语义检索灵活,但偶尔会召回过宽;规则路由稳定,但不灵活。我的方案是先用规则路由过滤一遍,再用语义检索排序,两层叠加的效果比单用任何一层都要好。

3.4 参数传递与结果约定的硬规范

技能执行时,系统通过统一接口传入参数,技能通过统一接口返回结果,这个接口协议必须高度标准化,否则编排阶段会非常痛苦。

我定的参数传入格式如下:

{ "skill": "convert_doc", "args": { "file_path": "/data/uploads/report.docx", "engine": "default" }, "context": { "user_id": "u_1001", "workspace_dir": "/data/workspace/u_1001", "request_id": "req_abc123" } }

args就是SKILL.md里声明的参数,context是系统层面的公共上下文,包括用户身份、工作目录、请求ID。技能实现里只能读args和context里自己声明需要的内容,不能读别的。

返回格式统一成下面这样:

{ "status": "ok", "exit_code": 0, "output": { "content": "# 转换后的markdown...", "line_count": 128 }, "error": null }

失败时返回:

{ "status": "error", "exit_code": 1002, "output": null, "error": { "message": "file not found: /data/uploads/report.docx" } }

exit_code很有用,它让“技能如何被编排”这件事变得极其简单。比如编排器看到exit_code是0就继续下一个技能,非0就跳到错误处理分支。技能里不需要感知下一步做什么,它只需要把自己的结果和退出码交给编排器就好了。

这套约定看起来简单,但统一了以后,我后面写编排、监控、告警都省了大量解析工作。建议你也把接口协议当成第一优先级的规范来定。

4. 技能编排:从单个技能到工作流

4.1 会用单个技能不算什么,能串起来才是真本事

单个技能能解决简单问题,但真实业务大多是复合的。比如“把用户上传的会议录音转成结构化会议纪要并发送邮件”这个任务,至少涉及三段能力:语音转写、文本结构化整理、邮件发送。如果一次只让模型调用一个技能,它做不了这件事;如果在一段提示词里让模型直接完成三步,又回到老问题——复杂任务靠模型自由发挥,稳定性很差。

我的做法是提供技能编排层,让用户或模型可以声明“按顺序执行这些技能,并把上一个技能的输出作为下一个技能的输入”。

最简单的编排形态是线性管道:

transcribe_audio -> generate_summary -> send_email

每个技能的上游输出和下游输入由我们预先定义好的数据契约连接。比如transcribe_audio的输出字段是transcript,generate_summary输入字段是text,那就在编排定义里显式声明“transcript -> text”这个映射。不要依赖技能自己去猜,猜来猜去早晚出错。

4.2 条件分支、循环和错误转移

顺序执行是骨架,但业务流程不全是线性的。我在实践中遇到最多的是三种控制结构:

条件分支:根据上一个技能的结果决定走哪条路径。比如“判断文档里是否含敏感词,含就走人工审批,不含就直接发送”。这个判断我建议尽量放在一个轻量技能里,或者用编排器内置的规则引擎做,而不是让模型去决策,模型的判断在关键路径上不可靠。

循环:比如“批量转换文件夹里的所有docx文件”,可以先获取文件列表,再循环调用convert_doc。循环一定要限制最大次数,我统一设置max_iterations=10,防止因为文件系统异常或者技能本身有bug导致死循环拖垮整个任务。

错误转移:业务里必须定义“这个技能挂了之后谁顶上”。我的习惯是给每个技能指定一个fallback技能,比如convert_doc挂了自动降级到convert_doc_legacy,两个都挂了再进入人工处理队列。错误处理不设计好的编排,上线就是事故。

4.3 技能间的数据流转与上下文管理

多个技能之间共享数据,是最容易出bug的地方。我早期踩过一个典型坑:技能A往共享上下文里写了一个字段叫data,技能B也写了同名data,后者把前者的数据覆盖了,结果后续技能拿到的是错的数据,排查半天。

后来我定了三条规则:

第一,共享上下文统一是JSON对象,每个技能只能写自己命名空间下的字段。命名空间建议用技能名加下划线前缀,比如convert_doc.output,generate_summary.summary。系统级字段保留给编排器用,技能严禁占用。

第二,技能的输出默认不自动合并进上下文,除非编排器显式声明。这样可以避免技能之间误传数据,也方便排查“这个值到底是谁写的”。

第三,上下文里大对象要按引用存放。有些技能的输出可能是几十MB的文本或图片,直接塞进上下文会导致内存和序列化开销爆炸。我通常会把大输出写入临时文件或对象存储,上下文中只存文件路径和元信息。

这几条规则听着简单,但把多技能编排的调试成本从“灾难级”降到了“可接受级”。现在排查问题只需要顺着编排定义和数据契约找,而不是在代码里瞎翻。

5. 权限、沙箱与安全红线

5.1 默认拒绝的权限模型

Agent的技能在自动执行时,权限控制绝对不能靠自觉。这里的核心思路是“默认拒绝”:一个技能如果没有显式声明需要某个权限,那它就一定没有这个权限。

每个技能在SKILL.md的security字段里声明自己需要的能力,我的系统里支持四类基础权限:

  • filesystem.read:读取指定目录或文件
  • filesystem.write:写入指定目录或文件
  • network:发起网络请求
  • exec_command:在沙箱内执行系统命令

技能只允许在声明过的权限范围内活动。比如convert_doc声明了filesystem.read和exec_command,没有声明network,那它即使尝试访问外网也会被拦截。过去全靠提示词约束模型不要乱调接口,现在的效果是一个硬编码的边界。

权限还有一个粒度参数,比如filesystem.write可以限定为write: ["/data/workspace/{user_id}/"],技能只能在该目录下写文件。这个粗粒度到细粒度的转变,是我们安全设计里最关键的一步。

5.2 沙箱执行环境怎么选

沙箱负责真正执行技能代码。我试过三种方案,各有利弊,简单说下自己的使用感受:

子进程方案。用Python的multiprocessing或subprocess启动技能脚本,配合操作系统级的资源限制来隔离。部署最简单,但隔离性弱,技能代码能拿到进程的能力。适合个人项目或完全可信的私有技能。

容器方案。每个技能调用都在独立容器里运行,用镜像模板预装依赖。隔离性明显好一个台阶,启动延迟在几百毫秒到几秒之间,适合团队项目。缺点是镜像管理有成本,每个技能都可能需要单独的依赖环境。

独立虚拟机方案。隔离性最强、启动最慢、资源开销最大,一般不用。我带过的一个项目里,只有处理外部不可信上传文件并且要求极高隔离性的技能才走这条路径。

从性价比角度看,团队项目首选容器方案。给每个技能打一个带依赖的基础镜像,调用时临时起一个容器实例,用完销毁。跑完的临时文件全部回收,不会残留到下一个任务里污染环境。

5.3 敏感操作的兜底审批

有些操作即使权限模型放行了,也不能让Agent自己说了算,比如发送对外邮件、删除生产数据、转账,这类必须有兜底审批。

我的实现思路是给技能打上needs_human_approval标记。编排器在碰触这类技能时,不直接执行,而是把任务挂起并进入审批队列,等待人工确认或拒绝。

审批动作会影响整个编排流程:通过,则继续执行;拒绝,则走拒绝分支,终止后续操作;超时,则默认拒绝并通知任务发起人。把超时默认设为拒绝而不是通过,这个细节非常重要,它保证了系统在“没人盯”的时候更安全而不是更激进。

还有一个经验是审批消息要带足够上下文。只给审批人看“技能send_email申请发送邮件”是不够的,要把收件人、主题、正文摘要、触发来源全部带上,否则审批人只能瞎猜,要么全拒要么全放。我们后来直接把技能的关键输出渲染成审批卡片,审批效率和准确率提升了好几个量级。

6. 测试、监控与问题排查

6.1 离线怎么把技能测明白

技能上线前的测试,我一般分三层:

第一层是冒烟测试,验证技能能跑通。给一组最简单的合法参数,看技能有没有正常返回,返回格式是不是标准统一格式。

第二层是回归测试,跑一遍测试目录里的历史用例,确保新改动没有破坏旧行为。技能一旦被其他流程依赖,回归测试就变得特别重要。我把每个技能的历史典型输入输出固化成测试用例,在每次发布前自动执行。

第三层是干跑测试,只在日志里记录“将要做什么”,不真正执行副作用操作。这一步对权限范围大、操作有外部影响的技能特别有用,比如发送邮件的干跑测试会把拟发送的标题加载到日志,但不会真的发出去。

三层都过了,我才会把技能合并进主线。这个流程看起来有点重,但回头算一笔账:技能出问题导致线上数据被污染,恢复成本比写测试的成本高得多。宁可测试多花点时间,也不要上线后到处救火。

6.2 线上运行我要盯着哪些指标

技能系统上线之后,比“功能是否正常”更值得关注的,是“模型是否总在乱调技能”以及“技能是否总在失败”。我固定监控四类指标:

调用成功率,单个技能的成功率变化曲线,跌了就要查是上游依赖挂了还是参数变化了。

调用分布,哪些技能被频繁调用、哪些技能长期零调用。长期零调用的技能会被冻结,这个机制前面提过,能有效防止“死技能还在误导模型”。

平均耗时时长,特别是编排任务的p95耗时。技能A变慢10秒,可能整个编排链就慢了1分钟。耗时时长上来了,优先查网络IO和进程调度。

每次调用的token消耗,这个指标陷阱最深。技能描述写得越长,匹配阶段消耗就越大,所以description不是越长越好,要精炼;编排器在把技能输出合并进上下文时,也要控制输出体量,大输出走引用而不要走内容。

另外强烈建议在所有关键环节打日志:技能匹配结果、参数解析结果、执行结果、退出码、耗时。没有日志的Agent系统出了事故根本没法复盘,这个成本在项目早期就要想清楚。

6.3 踩坑速查表

按我过去这段时间的经验,把高频踩坑整理成一张速查表,每一条都来自真实项目现场,供你直接参考。

现象最常见原因处理办法
模型一直不调用某个技能技能description没有写清适用场景,或者when_not_to_use冲突重写description,突出“什么时候必须用”
技能总在不该被调用时出现语义检索范围过宽收紧description边界,加规则路由限制触发条件
参数校验失败率超高参数描述不够具体,模型只能猜在参数description里说明来源约束,必要时加示例值
技能返回的不是统一格式实现没有遵循exit_code协议加一个输出格式校验器,在进入编排前拦截
多个技能共享数据被覆盖上下文未做命名空间隔离严格按“技能名前缀+字段名”写入,禁止裸字段
并发任务下技能状态串了技能实现用了全局变量或静态实例技能内部状态清零,所有临时状态随调用实例销毁
技能执行权限过大权限声明粒度太粗,比如filesystem.write写全盘收敛权限范围,按工作目录或用户目录授权

这张表我会持续更新。很多时候,系统不稳定不是模型不行,而是技能系统的边界设计、描述质量和权限策略没跟上,把这些基建补齐,Agent的表现会肉眼可见地上一个台阶。

以我个人经验来说,技能系统的核心不是“让模型更强大”,而是“让模型更可靠”。把能力边界、触发条件、执行环境、权限范围都用工程手段固化下来,模型在约束里反而能发挥得更稳定。最后再分享一个我做这套系统时收获最大的细节:给每个技能的SKILL.md写一段“失败反馈”,告诉模型技能没跑成功时该做什么、不该做什么。加了失败反馈之后,很多原本会卡死在错误节点的任务,模型会自己尝试换参数或者向用户澄清,调整个性比硬性重试好得多。这套设计值得你多花点心思,回报率非常高。

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

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

立即咨询