Agent技能体系设计:从万能提示词到可复用技能库
agent-skills 这个名字第一次见到的朋友多半会联想到游戏里的技能树。其实它是我最近一直在打磨的一套 Agent 工程化方案给大模型智能体配一套可复用、可组合、可测试的技能库让智能体不再靠一段万能提示词硬扛所有任务而是像成熟团队那样遇到什么问题就调用对应的标准工序。技能skill在这里是一等公民每个技能都封装了元数据、输入输出契约、前置后置条件和真正的实现逻辑可以被注册、检索、编排、测试和版本化管理。这篇文章会把 agent-skills 的设计思路、核心模块、最小可运行实现以及我在实践中踩过的坑全部拆开讲适合正在做 Agent 应用、工具集或自动化流程的开发者。如果你能看懂基本的 Python 代码跟着第四节的例子完整跑一遍基本就能理解整套体系的骨架。1. 项目逻辑拆解为什么 Agent 一定要有技能体系1.1 从万能提示词到分工协作的转变刚接触 Agent 开发时几乎所有人都会走同一条路把所有要求、所有工具用法、所有边界条件一股脑塞进 system prompt指望模型自己表现得像一个全知全能的老员工。这种方式在最简单的 demo 上确实能跑通我最早的几个测试项目就是这么干的当时还觉得大模型真神奇什么都会。但随着任务范围扩大问题就像夏天的剩菜一样迅速变质。第一结果不可复现同样的输入今天能给出正确答案明天可能因为提示词里某个措辞被微调输出就换了一套逻辑甚至直接跑偏。第二排查成本极高当结果不对时你根本分不清到底是模型理解错了、上下文里缺了关键信息、还是它的推理步骤本身就写错了。第三完全谈不上复用项目 A 里好不容易调好的能力搬到项目 B 只能靠复制粘贴改一处就要全局检查越到后面越不敢动。把能力拆分成技能本质上是把让模型自由发挥变成给模型提供标准工序。我常拿厨房打比方如果没有预制调料和标准配方每个厨师炒出来的鱼香肉丝味道都不一样今天咸明天甜全看手感。但如果后厨按工位划分配菜工只负责切配炉灶工只负责调味每道工序都有明确的输入输出标准和检查环节出品质量就能稳定下来。agent-skills 做的事就是给智能体建一套后厨工位体系每个工位是一个技能每个技能有标准操作流程、有验收标准、有边界条件。1.2 技能化解决的核心问题技能化落地之后至少四个层面的问题被有效缓解。首先是可测试性每个技能是独立单元可以单独喂测试样例出错了能立刻定位到具体是哪个技能的问题而不是在整段多轮对话里大海捞针。我见过不少人排查 Agent 问题靠反复问你再试一次那和抛硬币没什么区别。其次是上下文成本。不需要把全量说明塞进提示词系统只把相关技能的紧凑描述注入上下文token 消耗能下降一个数量级。以我自己早期的一个项目为例当时往 system prompt 里堆了十几个工具的长文档一次任务动辄烧掉七八千 token改成技能化之后降到一千以内响应速度也快了不少。然后是可编排性。复杂任务不再是一次性长推理而是拆成计划 - 选技能 - 执行 - 观察结果 - 继续计划的小循环每一步都有迹可循模型犯错也能在早期步骤就被拦截。最后是可演化性技能可以独立升级、灰度、回滚不会因为改了某个底层工具实现导致整个 Agent 的行为像多米诺骨牌一样全部漂移。这四个价值点叠加在一起让我确信技能体系是 Agent 从玩具走向工程化的必经之路而不是某个框架的专属卖点或者花哨概念。2. 核心细节解析一个技能的结构化定义2.1 技能的元数据 接口 实现三段式在agent-skills里我把一个技能定义成三段结构每一段承担完全不同的职责。元数据解决这个技能是什么、什么时候该用的识别问题接口解决调用方怎么跟它安全交互的契约问题实现解决具体如何完成动作的执行问题。三者缺一不可少了任何一段技能要么无法被正确发现要么无法被安全执行要么就只能算是一段死代码。元数据字段我一般长这样{ skill_id: file_search, name: 文件名检索, description: 在本地目录中按文件名进行搜索支持通配符返回匹配文件的完整路径和基本信息。只负责按名称查找不执行内容读取。适合定位资源路径、统计文件分布等场景不适合做全文内容检索。, version: 1.2.0, tags: [file, search, fs], status: active, author: platform, updated_at: 2025-01-20 }接口部分包含 input_schema、output_schema、preconditions 和 postconditions。我习惯用 JSON Schema 来表达输入输出原因很朴素大模型对 JSON 的理解天然稳定JSON Schema 又能直接用于运行时校验不需要引入额外的序列化逻辑。前置条件在这里不是形式主义的摆设它承担着防呆作用后面我会专门展开说。实现部分则根据技能类型有所不同。纯函数型技能可以直接写一个 Python 函数提示词型技能封装了一段带变量的 prompt交给模型推理混合型技能则是代码加模型调用的组合。不管属于哪种最终都暴露统一的执行入口上层根本不需要关心技能内部是代码还是提示词。这种封装方式让整个体系保持着接口稳定、实现自由的弹性后续替换实现方案也不会破坏调用方。2.2 前置条件与后置条件为什么不能省这里要展开说一下因为这是设计早期最容易偷懒的地方。前置条件的作用是防止技能在错误的状态下被调用。举个例子某个发送通知技能如果收件人列表为空你硬调用它模型完全有可能自己编一个假地址发出去这种事在真实系统里后果很严重。加了 precondition 之后执行器会在调用前做检查条件不满足就直接返回当前状态不满足调用要求并附上缺失的信息让规划层知道下一步该去补什么数据。后置条件则是每次执行完必须核验的承诺。拿文件压缩技能举例后置条件可以声明输出 zip 文件存在且大小大于 0。如果技能内部出现了静默失败比如目标目录没有写权限但函数没抛异常执行器能在第一时间发现并触发重试或者降级策略而不是懵懵懂懂地把错误结果交给下一个环节。我见过不少 Agent 项目的失败不是模型不行而是执行环节缺少这种做完检查的意识错误结果被当作正常结果继续往下传最终像滚雪球一样放大。从工程视角看这就是把 Agent 的黑盒能力变成白盒契约。有了契约你才能做监控、做断言、做回归测试。设计阶段多花半小时写条件和校验后期排错节省的时间是几十倍。2.3 描述信息是隐藏的命根子我在 agent-skills 踩过的第一个大坑就是对描述字段不重视。当时随手写了句处理文件结果检索阶段发现这个技能几乎什么文件类任务都会被召回真被调用后又因为意图不匹配产生一堆错误输出。描述信息本质上是你写给 LLM 的功能说明书它直接决定模型在繁杂的技能列表里能不能一眼选中正确的那一个。我后来总结出好的描述应该包含三个要素功能边界、典型场景、反例提示。反对例尤其关键比如本技能只负责按文件名搜索不执行内容读取适合批量定位文件不适合做全文关键词查询。实践下来在描述里写上一两句不适合做什么的情况误调用率能下降不少。另外要注意改了描述之后注册中心里的向量索引必须重建否则粗召回阶段拿到的还是旧语义新描述等于白改。3. 关键模块技能注册、检索与执行3.1 注册中心是技能的户籍系统没有注册中心的技能只是一堆散落的函数有了注册中心技能才变成可被感知、可被治理的对象。我用一个 SkillRegistry 管理所有技能核心就是把技能信息和检索用数据集中维护起来。关键字段包括技能唯一 id、用于向量化和关键词检索的描述文本、描述文本的 embedding 向量、版本和上下线状态、指向实现对象的引用、输入输出 schema。我习惯用一张表来表达它们之间的对应关系字段作用说明id技能唯一标识注册时校验不得重复description_text同时用于关键词匹配和向量化embedding描述文本的向量表示启动时生成version / status版本控制与上下线开关implementation_ref指向实际执行函数的引用input / output schema运行时校验的依据注册中心除了基本的注册、反注册、查询还会在启动时对每个技能做一次自检schema 是否合法、实现是否存在、依赖是否齐备、embedding 是否已经生成。自检不通过的技能直接标记为不可用避免运行到一半才发现调用链是断的。这一步花不了几毫秒但在分布式部署或者多实例场景下能避免很多为什么这台机器上能跑、那台上不能跑的诡异问题。3.2 两步走先粗召回再理性选择技能数量超过十几个之后把全部技能说明都塞给模型显然不现实上下文不够放放了模型也容易中间迷失被夹在一堆相似描述里找不到重点。我的做法是两阶段选择这个思路在很多信息检索系统里都能看到但放在 Agent 技能选择里尤其顺手。第一阶段是粗召回用关键词匹配加向量语义检索从注册中心里捞出与当前任务最相关的 Top-K 个技能。K 通常取 5 到 10太小容易漏掉真正合适的技能太大会让第二阶段的选择压力变大。这个阶段完全不需要 LLM 参与纯索引和向量计算毫秒级完成成本几乎可以忽略。第二阶段是把 Top-K 个技能的紧凑描述——只取名称、一句话描述、关键参数说明不放完整 schema——拼接进提示词让 LLM 规划器做最终选择同时要求它输出选择理由。这一步能有效纠正向量检索里语义相似但功能不匹配的偏差因为模型能结合任务整体意图做判断而不是单纯靠文本相似度。我试过把完整 schema 一起放进去效果没有显著提升token 倒是翻了几倍最后就保持紧凑描述了。两步走背后是对成本和效果的权衡。全量技能直接进提示词既费钱又容易被无关信息干扰只靠向量选技能在复杂任务面前又显得不够聪明。粗召回加模型裁决刚好在中间找到了一个可调的平衡点而且每一步都可以单独调参数出了问题也好定位。3.3 执行器统一入口和全链路日志执行器是技能真正跑起来的地方也是整个体系里我觉得最不能糊弄的模块。所有技能调用统一走 executor.execute(skill_id, input)内部依次完成调用前检查前置条件校验输入 schema加载技能实现执行校验输出 schema验证后置条件最后返回结果包。这个顺序我调过好几版把输出 schema 校验放在后置条件验证之前是有讲究的先确认数据结构没坏再确认业务承诺有没有达成避免后置条件函数收到一个格式都不对的结果报出让人看不懂的错误。执行器还会自动记录一整套链路日志包括谁在什么任务上下文里调用了哪个技能、输入是什么、输出是什么、耗时多少、校验是否通过、有没有走重试分支。这套日志是排错和优化最重要的依据。我见过不少 Agent 项目技能写得挺好但完全不记录调用链路出了问题只能靠感觉猜这在生产环境完全不可接受。有了日志你能回答上一个错误是哪里来的这种最基础的问题。4. 实操从零搭建一个最小可用的 agent-skills 体系4.1 目录结构与核心代码骨架直接上一个最小实现代码我都实际跑过你可以照着敲。整个工程就几个文件麻雀虽小五脏俱全。agent_skills/ ├── skill.py # 技能数据模型 ├── registry.py # 注册中心 ├── executor.py # 执行器 ├── retriever.py # 粗召回检索 └── skills/ ├── calculator.py # 四则运算技能 └── file_search.py # 文件名检索技能skill.py 里核心数据模型长这样from dataclasses import dataclass, field from typing import Any, Callable, Optional dataclass class Skill: skill_id: str name: str description: str version: str 1.0.0 tags: list field(default_factorylist) status: str active input_schema: Optional[dict] None output_schema: Optional[dict] None preconditions: Callable[[dict], tuple[bool, str]] lambda inputs: (True, ) postconditions: Callable[[Any], tuple[bool, str]] lambda result: (True, ) implementation: Optional[Callable] None注意这里 preconditions 和 postconditions 都用可调用对象这样既可以是固定的校验逻辑也可以是动态变化的状态检查灵活性大很多。有些技能的前置条件跟全局状态有关比如数据库连接池是否就绪你用 lambda 包一下就能实现。4.2 注册中心与检索模块registry.py 用字典加列表实现生产环境可以换成数据库或者外部缓存class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: Skill): if skill.skill_id in self._skills: raise ValueError(fduplicated skill id: {skill.skill_id}) if skill.status active: skill.embedding self._embed(skill.description) self._skills[skill.skill_id] skill def get(self, skill_id: str) - Optional[Skill]: return self._skills.get(skill_id) def _embed(self, text: str): # 实际项目里这里调用 embedding 服务 pass def retrieve_by_keyword(self, query: str, top_k: int 5): scored [] for skill in self._skills.values(): weight 0 if query in skill.name or query in skill.description: weight 1 if any(tag in query for tag in skill.tags): weight 1 scored.append((weight, skill)) scored.sort(keylambda x: x[0], reverseTrue) return [skill for w, skill in scored[:top_k] if w 0]embedding 部分我通常接本地的小模型离线跑把技能描述编码成向量查询时把任务描述也编码成向量再算余弦相似度。向量检索的代码很标准这里就不展开了思路和关键词检索是互补的关键词能精确匹配术语向量能抓住语义近义两者取并集再打分召回效果最稳。4.3 执行器实现executor.py 是核心中的核心我贴出完整调用链import time class Executor: def __init__(self, registry: SkillRegistry): self.registry registry def execute(self, skill_id: str, inputs: dict): skill self.registry.get(skill_id) if skill is None or skill.status ! active: return {ok: False, error: skill unavailable} if not self._validate(skill.input_schema, inputs): return {ok: False, error: input schema validation failed} pre_ok, pre_msg skill.preconditions(inputs) if not pre_ok: return {ok: False, error: fprecondition failed: {pre_msg}} start time.time() try: result skill.implementation(inputs) except Exception as e: return {ok: False, error: fexecution error: {str(e)}} duration time.time() - start if not self._validate(skill.output_schema, result): return {ok: False, error: output schema validation failed} post_ok, post_msg skill.postconditions(result) if not post_ok: return {ok: False, error: fpostcondition failed: {post_msg}} return {ok: True, result: result, duration_ms: round(duration * 1000, 2)}这里有个小设计值得说每个错误分支都返回明确的可读信息而不是抛异常。因为 Agent 的规划器是 LLM模型特别擅长从错误信息里理解下一步该怎么办。返回precondition failed: 缺少收件人地址远比抛出一段堆栈信息有用前者能让模型自己去补数据后者只会让它犯懵。如果你要做重试、做降级路由这种结构化错误信息也是最好的输入。4.4 一个完整的执行链路演示现在注册两个技能模拟一个任务把当前目录下所有 png 图片找出来再统计总大小。规划器经过粗召回和模型裁决会选择 file_search 和 calculator 两个技能但要串起来执行。设计上我会让 file_search 返回文件名和大小列表calculator 做加法汇总。def file_search_impl(inputs): import glob, os pattern inputs.get(pattern, *) results [] for path in glob.glob(pattern): results.append({ name: os.path.basename(path), size: os.path.getsize(path) }) return results def calculator_impl(inputs): return {sum: inputs[a] inputs[b]}调用执行器之后链路日志会留下这样的记录task统计图片总大小 returns[ {skill:file_search,ok:true,duration_ms:2.4}, {skill:calculator,ok:true,duration_ms:0.8} ]两条记录意味着任务被拆成了两个可观测的步骤哪一步慢、哪一步失败一眼就能看出来。在实际的长链路编排里你可以再加一层 plan 结构让规划器输出一个技能序列执行器按顺序逐步推进每步结果校验通过后才允许走下一步。这个模式不复杂但它让复杂任务变得可以拆解、可以审计、可以人工介入。5. 常见问题与排查技巧实录5.1 技能描述和实际能力对不上这是最高频的坑。描述里写搜索文件实现却只匹配了当前目录一层用户用了之后发现漏文件第一反应往往是模型没找对实际上技能本身就偷工减料了。排查方式很简单先看执行器日志里这个技能被调用时的输入输出再跟真实需求对照。修的时候不光要改代码还得改描述和 schema把边界条件写清楚同时补一组覆盖边界用例的测试。记住改了描述之后embedding 索引要重建否则粗召回阶段拿到的还是旧语义。5.2 上下文窗口被技能元数据吃满早期版本里我把每个技能的完整 JSON schema 都挂到系统提示词尾巴上技能一多一次任务的 token 直接爆掉模型的注意力也被稀释。后来把注入内容压缩成三条技能代号、一句描述、一段参数示例完整 schema 只存注册中心真正调用时才由执行器拉取。这样一次任务从几千 token 降到了几百 token效果基本不受影响响应速度反而快了。另外一个技巧是把技能按标签分组任务进来时先用标签过滤一遍再走粗召回等于加了一道前置漏斗。5.3 技能互相调用形成环有段时间某个技能实现里为了图省事内部又调用执行器去触发另一个技能结果 A 调 BB 又调 A直接死循环到超时。我当时在日志里看到一眼望不到头的调用链内心是崩溃的。后来在执行器里加了两层保护调用深度上限默认 10单次技能调用超时默认 30 秒。超时后执行器会返回技能执行超时请尝试拆分子任务或降级给规划器让它换一条路走。不过更根本的解法还是在设计上技能之间尽量不互相调用公共逻辑抽成基础工具由规划器统一编排从结构上避免环的出现。5.4 版本更新导致老任务失败技能升级后新版本可能改了输入 schema旧任务流还在按老签名传参校验直接挂掉。这种情况在持续迭代的项目里很常见不管理版本就等于埋雷。现在我给注册中心做了简单的版本管理技能注册时记录版本号任务链条创建时固定所用的技能版本执行器遇到 schema 校验失败时会拉取该技能当前启用的兼容版本做二次尝试。这跟依赖锁定的思路一致能显著减少昨天还能跑今天突然挂的问题。5.5 回归测试怎么做才有效我经验是用技能夹具给每个技能准备三到五组典型输入覆盖正常、边界、异常三种情况。每次注册新技能或更新旧技能时把这套夹具跑一遍校验输出 schema 和后置条件是否都通过。因为技能之间有依赖我还给夹具加了标注写明这个技能可以在哪些任务上下文中被调用防止将来有人改了描述导致召回错位。这个做法不复杂但实施之后技能上线导致的事故率下降得非常明显。还有个细节是夹具里的输入和期望输出要尽量贴近真实使用场景别整一堆理想的、不会有人真这么写的样例那测了等于没测。再补一个我自己觉得特别实用的经验agent-skills 这类体系真正的价值不在技能写得有多聪明而在边界划得有多清楚。最初我也雄心勃勃想一次性把二十个技能做好结果大多数都因为描述不清、条件不严被模型乱用最后反而推倒重来。后来老老实实从三四个业务痛点最明显的流程开始把每个技能的描述、schema、前置后置条件都打磨到能断言的程度再逐步扩展整个体系才真正稳定下来。如果你也要搭自己的技能系统我建议第一周只做一件事把你手上最常用、最不想重复踩坑的两三个流程固化成技能然后把至少一个错误调用样例写进测试夹具。先跑通这几个再谈规模这个节奏比一次性设计一个大而全的框架要靠谱得多。