Agent-Skills工程化实践:从单体Prompt到可复用技能模块的架构演进
1. 从“agent-skills”这个标题说起它到底在解决什么问题第一次看到“agent-skills”这个标题我脑子里蹦出来的不是某个具体框架而是一类很实际的需求怎么让一个智能体agent真正具备可复用、可组合、可评估的能力模块。过去两年里我参与过几个自动化流程项目从最简单的“定时抓数据发通知”到稍微复杂一点的“多轮对话工具调用结果校验”踩过的坑几乎都指向同一个根因——agent 的能力边界模糊写的时候靠堆 prompt跑起来靠运气出了问题靠猜。“agent-skills”这个标题本质上是在给 agent 的能力做工程化拆解。它不是一个新概念但它是把“agent 能干什么”从玄学变成可管理资产的关键一步。你可以把它理解成以前我们写一个 agent是把所有逻辑塞进一个巨大的系统提示词里像把整个工具箱倒进一个麻袋而 skills 的思路是把每项能力单独封装成一个小模块有明确的输入输出、触发条件、失败处理和评估标准用的时候按需挂载。这套东西适合谁如果你正在做以下任何一件事它都值得你花时间你在维护一个多轮对话机器人发现 prompt 越写越长改一处崩三处你在做自动化任务编排需要 agent 调用外部工具但工具一多就乱你在评估 agent 效果发现“它有时候行有时候不行”却定位不到问题你想把某个 agent 的能力迁移到另一个场景结果发现耦合太深搬不动。我见过太多项目死在“能跑 demo 但上不了线”这个阶段核心原因就是能力没有模块化。agent-skills 这个方向解决的就是从“能跑”到“可维护、可扩展、可度量”之间的那道鸿沟。接下来我会从设计思路、核心细节、实操落地、问题排查四个层面把这件事拆开讲透。2. 整体设计思路为什么要把 agent 能力拆成 skills2.1 从“单体 prompt”到“技能模块”的演进逻辑早期做 agent最直接的做法是写一个超长系统提示词把角色设定、任务描述、工具说明、输出格式、边界条件全塞进去。我试过最夸张的一次系统提示词写到四千多字结果模型开始“选择性遗忘”——前面说的规则后面不遵守工具调用格式偶尔跑偏多轮对话到第五轮之后基本失控。这不是模型不行而是信息密度和注意力分配的问题。人类团队做事也一样你不可能让一个人同时记住所有流程、所有工具、所有异常处理然后指望他每次都做对。合理的做法是分工——每个人有自己的职责说明书需要谁就找谁。Skills 的设计思路完全一致。每个 skill 是一个独立的能力单元包含触发条件什么情况下该用这个 skill输入契约需要哪些参数格式是什么执行逻辑具体怎么做调什么工具走什么流程输出契约返回什么结构成功和失败分别长什么样评估标准怎么判断这次执行是合格的。这样做的好处是agent 的“大脑”只需要做一件事判断当前该调用哪个 skill。至于 skill 内部怎么实现大脑不需要关心。这就把复杂度从“全局”降到了“局部”改一个 skill 不会影响其他 skill加一个新 skill 也不需要动主流程。2.2 技能拆分的粒度怎么定三个实用原则拆分粒度是这件事里最容易翻车的地方。拆得太粗等于没拆拆得太细调用链长得像迷宫。我总结下来有三个原则比较实用原则一一个 skill 只做一件“可命名”的事。如果你没法用一句动宾短语说清楚它干什么比如“查询订单状态”“生成周报摘要”“校验地址格式”那说明它可能该继续拆或者它根本不是一个 skill 而是一个流程。原则二skill 之间尽量不共享状态。每个 skill 拿到输入、产出输出中间状态自己管理。如果两个 skill 需要频繁读写同一份上下文那它们大概率应该合并或者抽出一个共享的数据层。原则三按“失败模式”拆分。如果两个能力失败的原因完全不同处理方式也完全不同那就该拆开。比如“调用外部接口”和“解析返回结果”前者失败要重试后者失败要兜底混在一起就没法分别处理。我一般会先用一个表格把候选 skill 列出来标注触发条件、输入、输出、失败模式然后看哪些可以合并、哪些必须拆开。这个表格本身就是很好的设计文档。2.3 技能注册与发现机制让 agent 知道“自己会什么”拆完 skill 之后下一个问题是agent 怎么知道有哪些 skill 可用最笨的办法是把所有 skill 描述硬编码进系统提示词但这样又回到了“信息过载”的老路。比较稳妥的做法是技能注册表 动态注入。注册表里存每个 skill 的元信息名称、描述、触发关键词、输入 schema、输出 schema。agent 在每一轮推理时先根据当前上下文从注册表里检索出最相关的几个 skill只把这几个的描述注入提示词。这样既控制了提示词长度又保证了 agent 能发现合适的能力。检索方式可以很简单比如关键词匹配加向量相似度也可以更复杂比如用一个轻量分类模型判断意图。我实测下来对于 skill 数量在 20 个以内的场景关键词加描述匹配就够用了超过 50 个才需要考虑更精细的检索策略。注意技能注册表本身也需要版本管理。每次 skill 的输入输出契约变更都要同步更新注册表否则 agent 会按旧契约调用导致莫名其妙的失败。3. 核心细节解析一个 skill 到底该包含什么3.1 输入输出契约比你想的更重要很多人写 skill 的时候输入输出定义得很随意觉得“反正都是我自己用”。但一旦 skill 数量超过五个或者需要多人协作契约不清晰的问题就会集中爆发。输入契约要明确几件事参数名、类型、是否必填、默认值、取值范围。比如一个“查询天气”的 skill输入应该是{city: string, date: string(YYYY-MM-DD), unit: celsius|fahrenheit}而不是“传个城市和日期就行”。后者看起来灵活实际上调用方每次都要猜格式出错率极高。输出契约同样重要。我习惯把输出分成三个部分status成功、失败、部分成功data成功时的结构化数据error失败时的错误码和可读信息。这样调用方可以统一处理不需要为每个 skill 写一套解析逻辑。下面是一个我常用的输出模板{ status: success, data: { result: ..., metadata: {} }, error: null }失败时{ status: failed, data: null, error: { code: TIMEOUT, message: 外部接口响应超时, retryable: true } }retryable这个字段很关键它告诉 agent 这个失败能不能重试。没有它agent 要么盲目重试导致雪崩要么该重试的时候直接放弃。3.2 触发条件设计什么时候该用这个 skill触发条件是 skill 的“入口开关”。设计得不好会出现两种典型问题该用的时候没触发或者不该用的时候乱触发。我的做法是给每个 skill 定义三类触发信号显式关键词用户输入里出现特定词汇比如“查一下”“帮我算”“生成”上下文状态当前对话处于某个阶段比如“已经收集完信息等待执行”前置条件某些数据已经就绪比如“用户已提供地址”。这三类信号可以组合使用但不要过度依赖单一信号。只靠关键词容易误触发只靠上下文容易漏触发。我一般会设置一个简单的打分机制每命中一个信号加一分超过阈值才触发。阈值可以根据实际效果调整初期宁可保守一点避免乱触发。实操心得触发条件一定要写进 skill 的元信息里并且和描述放在一起。这样检索的时候描述和触发条件会一起被匹配到提高召回准确率。3.3 执行逻辑与工具调用skill 内部的“小流程”一个 skill 内部可能包含多个步骤比如“查询订单”可能要先校验用户身份再调订单接口再格式化结果。这些步骤不需要暴露给 agent 主流程但需要在 skill 内部管理好。我通常会把 skill 内部逻辑写成一个小型状态机或者流水线每个步骤有明确的输入输出和失败处理。这样做的好处是skill 内部可以独立测试不需要每次都跑整个 agent。工具调用是 skill 执行中最容易出问题的环节。我的经验是所有外部调用都要设超时默认 5 到 10 秒根据接口特性调整所有外部调用都要有重试策略但重试次数不要超过 3 次且要加退避所有外部调用的返回都要校验不能假设对方一定返回预期格式所有外部调用的失败都要有兜底要么返回降级结果要么明确告诉 agent 失败原因。这些规则看起来琐碎但每一条都是踩坑换来的。我见过因为没设超时导致整个 agent 卡死的也见过因为没校验返回格式导致后续步骤全部报错的。3.4 评估标准怎么判断一个 skill 干得好不好没有评估标准的 skill等于没有质量保障。评估标准不需要很复杂但必须可量化、可复现。我一般从四个维度评估维度说明示例指标准确性输出是否符合预期正确率、F1 值稳定性多次执行结果是否一致方差、失败率效率执行耗时和资源消耗平均耗时、token 消耗健壮性异常输入下的表现异常捕获率、降级成功率评估数据来源可以是人工标注的小样本也可以是线上真实调用的采样。关键是每次 skill 变更后都要重新跑一遍评估否则你永远不知道这次改动是优化还是劣化。4. 实操过程从零搭建一个可用的 agent-skills 体系4.1 环境准备与基础结构假设你现在要从零开始我建议先搭一个最小可用的结构不要一上来就追求大而全。基础目录可以这样组织agent-skills/ ├── skills/ │ ├── skill_a/ │ │ ├── manifest.json # 元信息名称、描述、触发条件、输入输出 schema │ │ ├── handler.py # 执行逻辑 │ │ └── tests/ # 单元测试和评估用例 │ └── skill_b/ ├── registry/ │ └── index.json # 技能注册表 ├── core/ │ ├── loader.py # 加载 skill │ ├── router.py # 检索和路由 │ └── executor.py # 执行和错误处理 └── config/ └── settings.yaml # 超时、重试等全局配置manifest.json是每个 skill 的核心我一般会包含这些字段{ name: query_order_status, description: 根据订单号查询当前订单状态, triggers: { keywords: [订单, 查单, 物流], context: [user_authenticated], preconditions: [order_id_provided] }, input_schema: { order_id: {type: string, required: true} }, output_schema: { status: {type: string}, order_status: {type: string}, updated_at: {type: string} }, timeout_seconds: 8, retry: {max_attempts: 2, backoff: 1.5} }这个文件既是文档也是运行时配置。加载器读它来注册 skill路由器读它来匹配触发条件执行器读它来设置超时和重试。4.2 技能加载与注册的代码实现加载器的逻辑很简单遍历skills/目录读取每个manifest.json校验 schema然后写入注册表。下面是一个简化版的 Python 实现import json import os from pathlib import Path def load_skills(skills_dir: str) - dict: registry {} for skill_path in Path(skills_dir).iterdir(): if not skill_path.is_dir(): continue manifest_file skill_path / manifest.json if not manifest_file.exists(): continue with open(manifest_file, r, encodingutf-8) as f: manifest json.load(f) validate_manifest(manifest) registry[manifest[name]] { manifest: manifest, path: str(skill_path), handler: load_handler(skill_path) } return registry def validate_manifest(manifest: dict): required [name, description, input_schema, output_schema] for key in required: if key not in manifest: raise ValueError(fmanifest 缺少必填字段: {key}) if not manifest[name].replace(_, ).isalnum(): raise ValueError(fskill 名称只能包含字母、数字和下划线: {manifest[name]})load_handler负责动态导入执行逻辑可以用importlib实现。这里的关键是加载时校验不要等到运行时才发现 manifest 有问题。注册表可以持久化成 JSON也可以放在内存里。我一般会同时做内存里存一份用于快速检索磁盘上存一份用于审计和版本对比。4.3 路由与执行agent 怎么找到并调用 skill路由的核心是匹配。给定当前上下文用户输入、对话历史、已有数据从注册表里找出最合适的 skill。我常用的匹配逻辑分三步关键词过滤扫描用户输入命中触发关键词的 skill 进入候选集上下文校验检查候选 skill 的上下文条件和前置条件是否满足打分排序根据命中信号数量、描述相似度、历史成功率综合打分取最高分。def route_skill(user_input: str, context: dict, registry: dict) - list: candidates [] for name, skill in registry.items(): manifest skill[manifest] score 0 triggers manifest.get(triggers, {}) for kw in triggers.get(keywords, []): if kw in user_input: score 2 for ctx in triggers.get(context, []): if context.get(ctx): score 1 for pre in triggers.get(preconditions, []): if context.get(pre): score 1 if score 0: candidates.append((name, score)) candidates.sort(keylambda x: x[1], reverseTrue) return [name for name, _ in candidates[:3]]执行器负责调用 skill 的 handler处理超时、重试和异常。这里有一个细节超时控制要用独立的机制不能依赖 handler 自己实现。我一般用线程池或者异步任务加超时确保即使 handler 内部卡住也能被强制中断。import concurrent.futures def execute_skill(skill: dict, params: dict, timeout: int) - dict: handler skill[handler] with concurrent.futures.ThreadPoolExecutor(max_workers1) as executor: future executor.submit(handler, params) try: result future.result(timeouttimeout) return {status: success, data: result, error: None} except concurrent.futures.TimeoutError: return { status: failed, data: None, error: {code: TIMEOUT, message: 执行超时, retryable: True} } except Exception as e: return { status: failed, data: None, error: {code: EXECUTION_ERROR, message: str(e), retryable: False} }4.4 一个完整 skill 的示例从定义到测试假设我们要做一个“生成周报摘要”的 skill。它的职责是输入一周的工作记录输出一段结构化摘要。manifest.json{ name: generate_weekly_summary, description: 根据一周的工作记录生成结构化周报摘要, triggers: { keywords: [周报, 总结, 摘要], context: [weekly_records_available], preconditions: [] }, input_schema: { records: {type: array, required: true}, style: {type: string, required: false, default: concise} }, output_schema: { summary: {type: string}, highlights: {type: array}, blockers: {type: array} }, timeout_seconds: 15, retry: {max_attempts: 1, backoff: 1.0} }handler.pydef handle(params: dict) - dict: records params[records] style params.get(style, concise) if not records: raise ValueError(records 不能为空) highlights [r for r in records if r.get(priority) high] blockers [r for r in records if r.get(status) blocked] summary build_summary(records, style) return { summary: summary, highlights: highlights, blockers: blockers } def build_summary(records, style): total len(records) done len([r for r in records if r.get(status) done]) if style concise: return f本周共 {total} 项任务完成 {done} 项。 return f本周共处理 {total} 项任务其中已完成 {done} 项进行中 {total - done} 项。测试用例要覆盖正常输入、空输入、异常输入三种情况。我一般会写一个简单的测试脚本每次 skill 变更后跑一遍def test_generate_weekly_summary(): skill load_skill(generate_weekly_summary) # 正常输入 result execute_skill(skill, {records: [ {title: 任务A, status: done, priority: high}, {title: 任务B, status: blocked, priority: low} ]}, timeout15) assert result[status] success assert summary in result[data] # 空输入 result execute_skill(skill, {records: []}, timeout15) assert result[status] failed assert result[error][code] EXECUTION_ERROR这套结构跑通之后加新 skill 就是复制目录、改 manifest、写 handler、加测试流程非常固定。5. 常见问题与排查技巧实录5.1 技能不触发或误触发怎么办这是最常见的问题表现是 agent 该调用某个 skill 的时候不调用或者不该调用的时候乱调用。排查思路分三步第一步检查触发条件是否写得太窄或太宽。如果关键词只有一两个很容易漏如果关键词太泛比如“帮我”几乎每句话都能命中。我一般会先用一批真实输入跑一遍统计每个 skill 的触发次数明显偏高或偏低的都要调整。第二步检查上下文条件是否满足。很多时候不是关键词没命中而是上下文条件没满足。比如 skill 要求user_authenticated但实际流程里这个标记没被设置。这种问题要靠日志定位我一般会在路由阶段打印每个候选 skill 的打分明细。第三步检查检索范围。如果 skill 数量多但每次只检索 top 3可能把正确的 skill 挤掉了。这时候要么扩大检索范围要么优化打分逻辑让正确 skill 的分数更高。下面是一个排查速查表现象可能原因排查方法解决方向完全不触发关键词未命中打印输入和关键词匹配结果补充关键词或改用模糊匹配偶尔触发上下文条件不稳定检查上下文标记设置时机调整条件或增加兜底频繁误触发关键词太泛统计触发频率收窄关键词或提高阈值触发但执行失败输入契约不匹配校验实际参数和 schema修正调用方或放宽 schema5.2 执行超时和外部依赖不稳定外部依赖不稳定是 agent 体系里最头疼的问题之一。我的处理原则是永远假设外部依赖会失败。具体做法包括每个外部调用设独立超时不要用全局超时重试要加退避避免瞬间打爆对方重试次数不要超过 3 次超过就降级降级方案要提前设计好不能临时想。我遇到过一个案例某个 skill 调用外部接口接口平均响应 2 秒但偶尔会卡到 30 秒。最初没设超时导致 agent 整体卡死。后来加了 8 秒超时和 2 次重试失败率从 15% 降到了 2% 以下。注意超时时间不是越短越好。设得太短正常请求也会被误杀设得太长失败请求会拖慢整体。我一般会先统计接口的 P95 响应时间然后在此基础上加 50% 作为超时阈值。5.3 技能之间互相干扰怎么排查技能之间互相干扰通常是因为共享了状态或者执行顺序有问题。比如 skill A 修改了某个全局变量skill B 依赖这个变量但 B 执行时 A 还没跑完。排查这类问题我一般会做两件事第一给每个 skill 的执行加上唯一 trace id所有日志都带上这个 id方便追踪调用链。这样能清楚看到哪个 skill 在什么时候改了什么东西。第二尽量消除共享状态。如果两个 skill 必须共享数据就把数据放在显式的上下文对象里由主流程统一管理skill 只读不写。写入操作要么由主流程做要么通过明确的事件机制通知。我见过最隐蔽的干扰是“隐式缓存”skill A 调用外部接口后把结果缓存在内存里skill B 也调同一个接口拿到了 A 的缓存但缓存已经过期。这种问题只能靠严格的缓存 key 设计和过期策略来避免。5.4 评估结果波动大怎么定位评估结果波动大说明 skill 的行为不稳定。可能的原因有输入数据分布变化比如线上输入和测试集差异大外部依赖返回结果不稳定模型本身有随机性比如温度参数设得太高评估指标本身不够鲁棒比如用单次结果而不是多次平均。我的做法是先固定随机种子再固定输入集然后多次运行取平均。如果这样波动还是大就逐个排查外部依赖和模型参数。很多时候把温度从 0.7 降到 0.2波动就会明显减小。另外评估集要定期更新。我一般每个月会从线上采样一批新数据补充到评估集里确保评估结果能反映真实情况。6. 一些实操心得和后续扩展方向这套 agent-skills 体系我前后迭代了三个版本最大的体会是不要一开始就追求完美设计。第一版能跑通就行哪怕 skill 拆得不够合理、触发条件写得粗糙都没关系。关键是先把“注册-路由-执行-评估”这个闭环跑起来然后在实际使用中逐步调整。另一个心得是skill 的文档和测试比代码本身更重要。代码可以重构但如果没有文档和测试重构就是灾难。我现在的习惯是每加一个 skill先写 manifest 和测试用例再写 handler。这样能强迫自己想清楚输入输出和边界条件。后续如果要扩展我觉得有几个方向值得尝试技能组合把多个 skill 编排成一个复合 skill对外暴露单一入口自动评估用另一个 agent 来评估 skill 的输出质量减少人工标注版本灰度skill 更新时先在小流量上验证确认没问题再全量跨 agent 复用把 skill 注册表做成独立服务多个 agent 共享同一套能力。最后分享一个小技巧如果你不确定某个能力该不该拆成独立 skill就先把它写成独立函数观察它在多少个地方被调用、被谁调用、失败模式是否一致。如果调用点多、失败模式统一就值得拆如果只有一处用、失败模式还很杂就先放着等需求明确了再拆。这个判断方法我用了很多次基本没出过错。