news 2026/9/25 13:11:13

Agent Skills实战:从System Prompt膨胀到技能化编排

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent Skills实战:从System Prompt膨胀到技能化编排

在AI Agent落地这件事上,我踩过最大的坑,就是“什么都想直接塞进System Prompt里”。早期做一个内部知识库问答Agent,功能越加越多,提示词从500字膨胀到3000字,最后模型开始答非所问,排错排到怀疑人生。后来接触了agent-skills这一套把能力模块化的思路,才意识到问题不在于模型不够强,而在于我没有给Agent一套结构化的“职业技能体系”。这篇博文就围绕agent-skills聊聊我对Agent技能化的理解、架构拆解、从零落地的完整过程,以及那些文档里不会写、但实战一定会遇到的坑。

1. 项目定位:agent-skills究竟在解决什么问题

1.1 从“单体智能体”到“技能编排”的必然演进

早期做Agent的方式特别粗暴:把所有工具函数、规则、知识片段一股脑写进系统提示词,再给模型挂上几个Python函数当Tools。这种“单体智能体”模式在Demo阶段跑得很欢,一旦进入生产环境,问题立刻暴露——加一个新需求,要么改提示词导致其他功能劣化,要么在工具列表里再塞一个函数,模型选工具时经常选错,定位问题无比困难。本质问题在于,我们还停留在线性指令时代,没有给智能体建立“按需调用能力”的结构。

agent-skills代表的是另一个方向:把Agent能做的事拆成细粒度、可独立维护、可复用的“技能单元”。每个技能像人身上的肌肉记忆一样,只负责一件明确的事。有的技能负责解析文档,有的技能负责提取日程,有的技能负责把结构化数据渲染成自然语言。Agent需要什么就加载什么,不需要的完全隔离。它的核心价值是一句话——从“教模型怎么做”变成“给模型一套能干活的工具箱”。

1.2 技能化设计与传统Function Calling的差异

很多人看到skill会问:这不就是Function Calling吗?还真不是。Function Calling是模型在推理时临时决定调用某个函数的机制,属于“调用层”能力;而agent-skills更像是“组织层”能力。它关注的是技能的声明、加载、调度、版本、依赖,甚至技能与技能之间如何协作。

举个例子,一份PDF采购合同,Agent要完成“提取关键条款并生成摘要”这个任务。普通人会写两个函数:一个是解析PDF,一个是生成摘要。但技能化的思路是:先将“读取PDF并转成结构化文本”做成一个底层的文档解析技能,再将“摘要生成”做成一个依赖解析技能的语义层技能,最后再通过一个“任务编排技能”负责流程控制。中间任何一层要替换算法或升级模型,都不影响其他层。这就是技能编排的组合优势——低耦合、高内聚、可独立迭代。

1.3 这个思路适合谁、不适合谁

如果你正在做这些场景,agent-skills的思路会非常有帮助:

  • 办公自动化Agent:需要处理文档、表格、邮件、日历,技能天然可拆分。
  • 垂直领域问答系统:在通用大模型之上叠加专有领域的处理流程。
  • 数据分析助手:涉及数据库查询、代码生成、图表描述等多步骤任务。
  • 客服工单系统:需要识别意图、查库、组织话术、跟进流转。

但如果是纯闲聊型助手或者单轮简单问答,硬套技能框架完全是过度设计,加了一层复杂度反而影响响应速度。工具永远是服务于场景的,别为了架构而架构。

2. 技术拆解:技能的表示、注册与调度机制

2.1 一个技能的标准结构长什么样

在agent-skills体系里,一个技能通常不只包含一段函数代码,它由三部分组成:描述文件(manifest)、执行逻辑(implementation)和校验用例(test cases)。

manifest是技能的“身份证”,一般是YAML或JSON格式。里面最重要的字段包括:

  • name:技能唯一标识,比如document_parser,全局不能重名。
  • description:给大模型看的语义描述,要写清楚“这个技能在什么场景下做什么事”,因为模型不会看实现代码,它完全靠这段描述决定是否调用该技能。描述写模糊了,技能再强也是摆设。
  • parameters:入参定义,包括类型、是否必填、格式约束。我强烈建议这一步用类似JSON Schema的规范,而不是自己随手定义几个字段,便于后续校验和自动生成调用代码。
  • returns:返回值结构定义,同样需要明确的schema。
  • dependencies:依赖的其他技能或外部服务,例如 “上传文件前先调用file_downloader”。依赖声明可以避免运行时才发现技能链路断裂。

一个经验是:manifest的description不要太长,但一定要把“边界”说清楚。例如file_downloader的描述不能只写“下载文件”,而要写“根据给定的URL下载文件到本地临时目录并返回路径,仅支持HTTP/HTTPS协议,不支持FTP”。模型有了明确边界,才不会在收到FTP链接时还硬调这个技能,调完报错还得迭代半天。

2.2 技能注册中心与动态加载

技能放到目录里是开发态,要变成Agent可用的资产,需要经过注册中心统一管理。注册中心本质上是一个内存映射表:技能名称到加载后的执行类实例之间的映射。启动时扫描指定目录,读取所有manifest,校验合法性后懒加载执行类。这里有两个关键设计:

懒加载而不是全量加载。一个大型系统的技能可能上百个,如果启动时全部加载,光是依赖初始化就会拖慢启动流程,更重要的是,大语言模型在每次推理时看到的工具数量是有限制的,工具列表太长,选路准确率会急剧下降。所以注册中心要支持按需加载,即:

  • 启动时只加载全局必需的基础技能。
  • 收到任务后,先做一次意图粗筛,把候选技能范围缩小到3-8个。
  • 再将范围内技能的manifest拼接到当次推理的上下文中。

版本挂载而不是覆盖升级。技能会迭代,同一天内可能更新多次。如果注册中心里一个技能名只对应一份代码,那么压测中一部分请求命中新版本、一部分命中旧版本,结果会非常不稳定。实践上我采用“name + version”双标识,在分发策略里显式指定什么时候切换流量。比如先在预发环境把新版本技能和旧版本技能同时注册,跑一个小时的影子流量对比,确认指标没问题再灰度切流量。

2.3 技能调度链路:模型怎么知道该用哪个技能

这是我把agent-skills应用到一个内部项目时最花心思的地方。最初我天真地以为把技能列表一股脑传给模型就行,结果技能超过15个之后,模型选工具的正确率明显下降。后来我调整了调度策略,分三层做决策:

  • 第一层:规则过滤。根据任务类型用硬规则过滤掉明显无关的技能。比如用户输入是语音转文字任务,就不用加载图像增强技能。这一步能大幅缩小候选集。
  • 第二层:语义检索。把用户意图用Embedding向量化,与技能描述的向量做相似度检索,选出Top-5相关技能。这层解决的是“描述近似但实际不同”的技能区分问题。
  • 第三层:模型决策。把前两层筛选出的候选技能列表交付给大模型,让它基于对用户问题的理解选择最合适的技能或技能组合。

这种三级调度的效果非常明显,一个原本有40个技能的项目,最终单次任务实际参与决策的技能只有5个左右,工具选择准确率从76%提升到94%以上。注意,这个数据是我们的封闭测集结果,不同业务会有浮动,但思路可以复用。

2.4 技能之间如何通信、如何共享状态

技能不是孤岛。很多时候技能之间需要传递数据。我见过最糟糕的设计是用全局变量共享数据,技能执行顺序稍微一变,取到的数据就错了。在agent-skills体系里,我推荐两种共享方式:

  • 显式上下文对象。每个技能接收一个context参数,维护一个类似KV存储的状态容器。技能A执行完把结果写入context.set("parsed_docs", docs),技能B声明依赖后通过context.get("parsed_docs")读取。数据流完全显式,问题链路好追踪。
  • 事件总线。适合异步场景,技能A发布一个事件,技能B订阅该事件并触发执行。这种方式解耦强,但排错难度高,小团队不建议一上来就用事件驱动。

我个人的建议是,初期先把90%的技能通信做成“显式上下文”,代码可读性强,新人接手也容易上手。事件总线等到真有必要时再引入不迟。

3. 实操落地:从零构建一个技能化Agent

3.1 环境准备与工程脚手架

看再多的架构图都不如真正动手跑一遍。我先用一个最小工程说明怎么搭,技术栈是我个人实践的方案,不一定对所有人都最优,但足够清爽:Python 3.11 + FastAPI + pydantic v2。

第一步是初始化工程目录,我通常这样组织:

agent-skills-demo/ ├── skills/ │ ├── __init__.py │ ├── calendar/ │ │ ├── skill.yaml │ │ ├── __init__.py │ │ └── impl.py │ └── document_parser/ │ ├── skill.yaml │ ├── __init__.py │ └── impl.py ├── registry.py ├── dispatcher.py ├── main.py └── pyproject.toml

先把依赖管理做清爽,用uv创建一个虚拟环境,核心依赖只有三样:fastapi、pydantic、pyyaml。真正跑大模型调用的时候再按需引入openai或对应的SDK。我见过太多人一开始就把一堆依赖丢进项目,最后依赖冲突看得脑子疼,工程上先做减法总没错。

3.2 定义第一个技能:以“会议记录整理”为例

一个具体例子胜过千言万语。假设我们要做一个技能:meeting_minutes,职能是把一段会议发言转写文本整理成结构化的会议纪要。

先在skills/meeting_minutes/skill.yaml里写清楚技能描述:

name: meeting_minutes version: 0.1.0 description: > 把会议录音转写后的原始文本整理为结构化会议纪要, 包含参会人、议题、结论、待办事项四个部分。 适合输入为纯文本的会议记录,不适合直接处理音视频文件。 parameters: type: object properties: raw_text: type: string description: 会议转写文本,UTF-8编码 attendees: type: array items: type: string description: 已知参会人列表,可为空 required: - raw_text returns: type: object properties: summary: type: string description: 会议整体摘要 action_items: type: array items: type: string description: 待办事项列表 dependencies: []

注意几点:description字段里我专门加了“适合纯文本”“不适合音视频”这两个边界信息,这个习惯在后续减少很多误调用;parameters里的required必须只列真正必要字段,让模型拼参数的负担尽可能小。

然后是执行逻辑impl.py,核心函数长这样:

from pydantic import BaseModel, Field class MeetingMinutesInput(BaseModel): raw_text: str = Field(..., min_length=10, max_length=50000) attendees: list[str] = Field(default_factory=list) class MeetingMinutesOutput(BaseModel): summary: str action_items: list[str] def execute(params: dict, context: dict) -> dict: data = MeetingMinutesInput(**params) llm_client = context["llm_client"] prompt = build_prompt(data) response = llm_client.chat_completion( messages=[{"role": "user", "content": prompt}], temperature=0.2, max_tokens=1024, ) result = parse_response(response) output = MeetingMinutesOutput(**result) return output.model_dump()

这里有个需要补充说明的点:我在execute里用了context["llm_client"],说明LLM客户端不是技能自己new的,而是由框架通过context注入。这样技能本身不关心上游用的是哪家大模型,以后换模型或做多模型路由都方便。实际项目中不要在每个技能里都各自初始化一个OpenAI客户端,连接池和认证统一管理会比散落各处的客户端稳定得多。

3.3 注册中心与调度器的代码骨架

技能写好了,要进注册中心。注册中心的核心逻辑是扫描目录、解析manifest、建立映射。

import yaml from pathlib import Path from importlib import import_module class SkillRegistry: def __init__(self, skills_dir: str): self.skills_dir = Path(skills_dir) self._skills = {} def scan(self): for manifest_path in self.skills_dir.rglob("skill.yaml"): with open(manifest_path, encoding="utf-8") as f: meta = yaml.safe_load(f) module_path = manifest_path.parent.name impl = import_module(f"skills.{module_path}.impl") self._skills[meta["name"]] = { "meta": meta, "execute": impl.execute, } def get(self, name: str): if name not in self._skills: raise KeyError(f"skill {name} not registered") return self._skills[name] def search_by_keywords(self, query: str, top_k: int = 5) -> list[str]: # 这里可以是向量检索,也可以先用关键词粗过滤 scored = [] for name, skill in self._skills.items(): desc = skill["meta"]["description"] score = len(set(query) & set(desc)) scored.append((score, name)) scored.sort(reverse=True) return [name for _, name in scored[:top_k]]

调度器则负责把用户请求路由到技能执行:

class Dispatcher: def __init__(self, registry: SkillRegistry): self.registry = registry async def dispatch(self, user_input: str, context: dict): candidates = self.registry.search_by_keywords(user_input, top_k=5) if not candidates: raise ValueError("no suitable skill found") # 在实际项目中,这里可以先让LLM从candidates里选出最合适的技能 # 再构建参数,这里为了演示直接取第一个 skill_name = candidates[0] skill = self.registry.get(skill_name) params = {"raw_text": user_input, "attendees": []} return skill["execute"](params, context)

当然,生产环境不可能这么简单,你需要在这里集成语义检索、LLM意图识别、参数抽取和多轮上下文管理。但脚手架的意义在于先把链路打通。链路通了,后面都是一点一点迭代的事。

3.4 性能与成本优化:每轮调用到底花多少Token

技能化架构能控制成本,但前提是你会算Token账。我以meeting_minutes这个技能为例,粗算一次调用的消耗:

  • 技能manifest拼接后约300 token。
  • 用户原始文本按每字约1.5 token计算,一份30分钟会议的转写文本大概3000字,折合4500 token。
  • 指令模板和输出约束说明约500 token。
  • 模型的输出,会议纪要一般800-1200 token。

单次技能调用总Token在6000-6500左右。注意这里只算了LLM推理的对话上下文,还没算你上一次意图识别和技能检索的开销。如果使用三级调度,每次任务还需要额外花一轮200-400 token的“模型选技能”调用,很多项目在评估成本的时候漏掉这笔账,结果月底账单出来了吓一跳。

所以建议做好两个缓存:

  • 技能选路缓存:相同意图的请求在短时间内直接复用上一次的选路结果,不需要每次都让模型重新选技能。业务场景变化不快的话,准确率下降幅度完全可以接受。
  • 中间结果缓存:比如某份文档解析结果当天没变,技能A解析后可以把结构化结果缓存到Redis,技能B再次调用时直接读缓存,省掉重复解析的开销。

我在实际项目中把这两个缓存加上之后,整体Token成本下降了约40%,而且响应时延也在提升。这不是玄学,是实打实的账单数字。

4. 常见问题与排查技巧实录

4.1 技能调用冲突:两个技能都觉得自己该干活

这是技能多了之后最容易出现的问题。比如项目里同时有meeting_minutes和meeting_notes_extractor,功能高度重叠,模型对同一个输入时而选A时而选B,输出风格不一致,下游处理就会出问题。

排查思路是先找重叠:把每个技能的description和测试样例导出,算一遍相似度矩阵。两条技能描述向量相似度超过0.85,就要警惕功能重叠。解决办法有三个方向:

  • 合并技能,把两个能力收编成一个更通用的技能,同时把边界写在description里。
  • 职责细分,一个偏“整理结论与待办”,另一个偏“抽取关键时间点”,让模型按需选择。
  • 加规则兜底,针对输入中带“时间”“排期”等关键词的请求,强制路由到后者。

另外每次技能变更后要跑一遍意图分流的回归集,防止改了一个技能的description导致其他任务被错误路由过去。这个坑我踩过不止一次,T+1回测不可省。

4.2 技能执行超时:重试机制反而让问题更严重

技能执行超时的原因通常不是大模型太慢,而是上游服务抖动,或者技能内部在等待某个同步接口响应。这时候如果不加控制的简单重试,会导致多个请求同时阻塞,服务吞吐量下降甚至雪崩。

我的做法是把超时与熔断分开设置:

  • 单次技能执行:设置硬超时,比如30秒,超时直接返回错误结果,并把这次执行标记为失败。
  • 技能级熔断器:1分钟内失败次数超过阈值(比如5次),熔断该技能10秒,熔断期间直接返回兜底文案,不给上游继续施加压力。
class CircuitBreaker: def __init__(self, fail_threshold=5, cooldown=10): self.fail_threshold = fail_threshold self.cooldown = cooldown self.fail_count = 0 self.opened_at = None def call(self, fn, *args, **kwargs): if self.opened_at and time.time() - self.opened_at > self.cooldown: self.opened_at = None self.fail_count = 0 if self.opened_at: raise RuntimeError("circuit opened") try: result = fn(*args, **kwargs) self.fail_count = 0 return result except Exception: self.fail_count += 1 if self.fail_count >= self.fail_threshold: self.opened_at = time.time() raise

这段代码很朴素,但内部的稳定性比起裸重试好了不止一个量级。

4.3 大模型上下文窗口溢出

很多人在Agent技能链条拉长之后,会把每一轮的对话历史、每个技能的输出都一股脑塞进上下文,最后发现自己陷入“窗口溢出—截断—丢失重要信息—技能执行错误”的恶性循环。Skill场景下上下文管理要遵循一个原则——只保留当前任务最小集。

具体操作上我会做三件事:

  • 技能执行完毕只把结构化结果放回上下文,原始长文本归档到外部存储。
  • 对话历史做滑动窗口裁剪,只保留最近2轮的用户意图和最终结果。
  • 如果技能A的输出要喂给技能B,只传递与B的入参schema匹配的字段,而不是把A的完整输出对象整个传过去。

这样即使单条回复内容再大,上下文中的有效信息始终维持在可控区间。

4.4 技能回归测试:表面跑通不等于真的没问题

技能开发最容易忽视的是测试,但这类系统一旦上线,问题往往是多技能交互时才会暴露的。我建议每个技能维护一组测试样例,分为三类:

类型样例特征期望结果
正向用例典型正常输入输出符合schema
边界用例极长输入、空字段、非法格式优雅报错或安全兜底
负向用例明显超出技能范围的输入不调用该技能或返回明确拒绝信息

每次技能升级时,把这组测试样例跑一遍,同时额外跑一遍“技能分流正确率”测试——针对50条带意图标签的请求,检查最终路由选择是否符合预期。只要这轮测试通过率不下降,就可以放心走灰度。

还有个小技巧:把测试中所有技能的真实执行输入输出录成快照,下次升级时对比快照差异。模型升级、提示词调整导致的行为漂移,在快照对比下一目了然,调试效率能提升很多。

一些实操心得

讲真,把技能架构引入项目之后,前期开发速度反而变慢了。因为你需要花时间设计manifest、写用例、搭注册中心和调度器。但好处也扎扎实实体现在后面:新增一个技能不需要改动老链路,排查问题的时候按技能边界切分调试,模块复杂度被限制在局部。

如果你是第一次尝试,我强烈建议别一上来就设计几十个技能,先挑3-5个你业务里最高频的动作做技能化,比如文档解析、日程提取、信息检索,跑通一两个真实的端到端任务。等这个链路稳定了,再逐步把其他能力往这个框架里搬。技能不是越多越好,一个能被正确调用的技能,远比十个躺在目录里没人用的技能有价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 13:06:48

AI音乐生成提示词模板库:12种风格四维框架与实战技巧

音乐生成工具用了一年多,从最早拿Suno瞎试,到后来帮朋友做短视频配乐、给播客做片头,踩过的坑基本能写一本小册子。最深的感受是:AI音乐生成的门槛不在工具,在提示词。同一段旋律动机,提示词写"轻快的…

作者头像 李华
网站建设 2026/9/25 13:01:52

Atlas 300V 24G是运算加速卡吗?CANN环境搭建与YOLO模型部署实战

这阵子正好接了个边缘服务器项目,调试对象是Atlas 300V 24G这张卡。身边好几个人第一次见到这块卡,第一句话都是:“这玩意儿是运算加速卡吗?看着怎么不像显卡?”还有人直接把热搜词扔过来:“atlas 300v 24g…

作者头像 李华
网站建设 2026/9/25 13:01:46

零JavaScript写插件:desktop-cc-gui Tier-0声明式插件实战教程

零JavaScript写插件:desktop-cc-gui Tier-0声明式插件实战教程 【免费下载链接】desktop-cc-gui Multi-engine AI coding desktop client (Tauri). Claude Code, Codex, Gemini, OpenCode, DeepSeek Harness and more in one GUI. 项目地址: https://gitcode.com/…

作者头像 李华