
前一阵子我在重构手头的智能体项目时agent-skills这个词频繁出现在我面前。一开始我以为它只是又一组工程命名真正动手把技能模块拆开重写之后我才意识到它背后藏着一个被大多数人忽略的核心命题Agent到底靠什么稳定完成复杂任务——是模型本身的聪明还是一套能被复用、被验证、被组合的“技能系统”我的答案是后者。单次对话里靠提示词让大模型临场发挥短期看没问题一旦任务链路变长、工具变多、调用场景变复杂模型的“临场发挥”就开始暴露不稳定、难追溯、不可复用的问题。于是我把项目里所有工具调用和任务步骤全部重新抽象成可注册、可检索、可编排的技能组件这几个月跑下来效果比预期好不少。这篇文章不是学术论文是我在实际项目中把agent-skills落地、踩坑、调优的全过程记录。1. agent-skills要解决的真实问题单次提示词不够用先说一个我在实际项目里遇到的典型场景。最早我做的智能体是“一段系统提示词 一堆 function calling 定义”每个工具函数在调用时把参数塞给大模型让模型决定调哪个、参数怎么填。表面看没有任何问题demo 跑得飞快。但一旦进入真实业务三件让人头疼的事就来了。第一上下文被工具定义塞满。十几个工具的 JSON Schema 加起来有几千 token这些 token 是每轮请求都固定占用的。模型输入窗口越大留给历史对话和业务数据的空间就越小。等到任务需要连续阅读多轮文档、多轮搜索结果时上下文已经拥挤得不行。更麻烦的是每新增一个工具所有历史会话的 token 成本都跟着涨模型还会因为工具描述之间互相干扰而出现“选择困难”。第二技能的复用和沉淀几乎为零。同一个“总结当日项目进展”的能力在周报场景要用在晨会场景要用在跨团队同步场景还要用。但我当时的实现方式是把这段逻辑分别复制进不同的提示词分支里每次改动都得同步改好几个地方。更别说“先查日历再安排会议”“先读文档再写纪要”这种固定套路完全依赖模型每次碰运气似的自己走对流程。第三失败不可追踪、结果不可复验。模型调了工具参数对不对、返回结果有没有真正满足业务条件这些判断都散落在对话逻辑里。一旦出问题翻日志只能看到“模型返回了函数调用”却看不到当时模型认为的上下文是什么、为什么会选这个参数。排查效率低到让人怀疑人生。agent-skills的解法在我看来很直接把“能力”从对话里抽出来变成独立、有名字、有输入输出契约、有验证规则、可组合的模块。模型不再需要“看懂”所有工具的细节它只需要根据自己的任务目标从技能注册表里“选中”一个技能把参数交给技能去执行。这个转变最核心的收益不是 token 变少了而是责任边界变清晰了——模型负责决策“做什么”技能模块负责执行并保证“做到什么标准”。决策和执行解耦之后我才能在上面叠加缓存、审计、回退、人工介入这一整套工程保障。1.1 一次重构后的效果对比我把重构前后同一个任务的调用链路放在一起做过一次对比。重构前智能体要完成“读取今天的所有项目邮件提取任务项按项目分组生成待办清单”这个动作大致流程是模型从系统提示词里知道有“邮件查询”工具和“文本分析”能力模型自己决定顺序先调邮件接口拉取列表然后逐封读取内容模型在对话上下文里临时生成摘要和分类结果用户如果追加一句“去掉已完成事项”模型又要重新理解一遍历史消息再补一次输出。重构之后我把“读取今日邮件并提取任务项”整体设计成一个技能命名为extract_tasks_from_emails。它内部封装了邮件 API 调用、正文清洗、关键词过滤、任务项结构化输出这几个步骤。模型在决策层只需要看到一行技能描述“提取今日邮件中的任务项并按项目分组”然后传入日期即可。至于这个技能内部怎么调用邮件接口、怎么清洗文本对决策模型完全不可见。输出结果由技能模块自己保证格式返回的是一个符合{ project: string, tasks: string[], priority: number }[]契约的 JSON。业务侧不需要再让模型“看一遍结果再总结一遍”下游直接消费结构化数据就行。整个调用链路的 token 消耗大概压缩了四成最关键的是结果稳定性好了一截——因为核心逻辑不再依赖模型每次随机应变。所以我的第一个结论是agent-skills不是一个组件而是一套设计原则——把临时性的“提示工程”沉淀成永久性的“能力资产”。这个原则贯穿了后面所有设计和实现。2. 技能的定义与结构设计把“会做什么”变成可验证的契约技能系统能不能用定义准不准是最要命的一环。定义得太粗模型选不准定义得太细技能数量爆炸检索和维护成本都失控。我最终把技能的“定义”拆成两个层面元信息层和执行层。元信息层是给决策模型和检索系统看的执行层是给技能引擎跑的。绝大部分人只关注执行层怎么写代码忽略了元信息层的设计结果技能做出来像个黑盒模型知道有这个技能但完全不知道什么情况下该用它。2.1 技能元信息我建议至少包含这些字段以我项目里的一个真实技能为例。这是“调取指定客户的历史订单并计算累计消费”的技能YAML 形式的元信息如下name: get_customer_lifetime_value version: 2.1.0 description: 查询指定客户的历史订单数据并按时间范围聚合计算累计消费金额。 适合在“评估客户价值”“生成客户画像”“筛选高价值用户”等场景使用。 输入客户ID和可选的时间范围输出聚合后的金额与订单列表。 input_schema: type: object required: - customer_id properties: customer_id: type: string description: 客户唯一标识CRM系统中的ID start_date: type: string format: date description: 可选开始日期默认不限制 end_date: type: string format: date description: 可选结束日期默认不限制 output_schema: type: object required: - total_amount - order_count - orders properties: total_amount: type: number description: 累计消费金额单位元 order_count: type: integer description: 订单总数 orders: type: array items: type: object description: 订单简要信息 preconditions: - customer_id 必须存在于 CRM 中 - 如果调用方未提供 end_date默认按当前时间为止 side_effects: - 只读操作不修改任何业务数据 - 会调用 CRM 订单查询接口注意接口限流 verification: - 返回的 total_amount 必须 0 - orders 数组为空时 total_amount 必须为 0 models: [gpt-4o, claude-3.5-sonnet]这里我觉得最容易被忽略的是description字段。它不是写给人类看的注释而是写给我们选型用的检索器以及决策模型看的“路标”。它需要回答三个问题这个技能解决什么场景、何时不该用、输入参数里最容易填错的点是什么。在早期版本里我把 description 写得特别短比如“查询客户订单”。结果模型经常在“只需要查单笔订单金额”的场景去调用这个聚合技能传错了参数导致下游计算逻辑全部出错。后来我把 description 扩写成“查询历史订单并计算累计消费”并且明确补充了“如果需要查询单笔订单详情请调用 get_order_detail 技能”误用率立刻降下来。2.2 技能粒度太大太小都是坑技能粒度是我踩过最深的坑之一。刚开始重构时我倾向于把流程拆得极细一个“发送告警通知”的功能都被我拆成了“查询告警规则”“匹配告警条件”“加载通知模板”“调用通知渠道”四个技能。结果模型在决策链路里频繁在这些技能之间跳来跳去每一步都要做一次路由判断。跳转次数越多累计误差越大。有一次模型在执行时先是正确选择了“匹配告警条件”然后在下一次路由时把一个本应传给“加载通知模板”的参数传给了“查询告警规则”任务直接断链。粒度设计的平衡点在哪儿我在反复尝试后的经验是一个技能应该是一个人类能直接描述清楚的、有明确业务结果的原子动作。判断标准就一条——如果你需要写两句话才能向同事说明白这个技能是干嘛的大概率粒度不合适的。比如“发送告警通知”就不是“查询规则”“加载模板”“调用渠道”的集合它本身就够原子。内部可以做模板渲染、渠道熔断、重试但这些是实现细节决策模型不需要感知。反过来“生成周报并发送给团队并同步到知识库”这个动作太大它应该是由“生成周报”“获取团队成员列表”“发送消息”“写入知识库”四个技能编排出来的任务而不是一个孤独的大技能。2.3 执行层技能不是“函数”是带契约的模块执行层的设计也藏着一个关键思路。我不建议把一个技能直接实现成一个裸函数——裸函数只是“能跑”但不带任何自我校验和日志能力。我最终为技能执行定义了统一接口大致如下class BaseSkill: name: str version: str def validate_input(self, params: dict) - dict: 校验输入参数返回清洗后的参数 raise NotImplementedError def run(self, params: dict, context: SkillContext) - SkillResult: 执行技能核心逻辑 raise NotImplementedError def validate_output(self, result: SkillResult) - SkillResult: 校验输出结果确保符合 output_schema 与 verification 规则 raise NotImplementedError def rollback(self, context: SkillContext) - None: 失败时执行回滚默认空实现 raise NotImplementedError这个接口的核心意图是把“执行”和“校验”剥离开。run()里只关心业务逻辑不要让执行代码既算结果又判断结果对不对validate_output()负责对照契约做最终把关。业务逻辑和校验规则分离之后技能的可测试性大大增加。我可以对同一个技能传入正常参数、边界参数、非法参数分别验证三者的表现而不用去 mock 一大堆内部状态。# 简化示例单个技能的实现骨架 class GetCustomerLifetimeValueSkill(BaseSkill): name get_customer_lifetime_value version 2.1.0 def run(self, params, context): customer_id params[customer_id] start_date params.get(start_date) end_date params.get(end_date, datetime.utcnow().isoformat()) raw_orders context.tools.fetch_orders(customer_id, start_date, end_date) total sum(o[amount] for o in raw_orders) return { total_amount: total, order_count: len(raw_orders), orders: [{id: o[id], amount: o[amount]} for o in raw_orders], }这里我特意不把业务数据源写死在技能内部而是通过context.tools注入。这样技能和具体的数据存储解耦测试时我可以很容易把fetch_orders替换成 mock 方法跑一套预置订单数据来验证聚合逻辑。3. 注册表与路由检索让Agent在海量技能里快速选对技能定义好了下一步就是让 Agent 能“找到”对的技能。我见过很多项目把技能堆在一个目录里靠决策模型读一遍所有技能描述来硬选。技能一多这种方法准不准完全看运气。3.1 技能注册表元信息加索引我做了一个轻量级的技能注册表本质上就是一个技能元信息的集合。注册表负责技能的上架、下架、版本管理和检索索引更新。修改技能元信息、调整技能版本都必须经过注册表不能绕过它直接改文件。class SkillRegistry: def __init__(self): self._skills {} self._embedder None def register(self, skill: BaseSkill) - None: if skill.name in self._skills: raise DuplicateSkillError(skill.name) self._skills[skill.name] skill llm_skill skill_metadata(skill) if not self._embedder: self._embedder Embedder() self._index self._embedder.index(llm_skill)注册表里每一份技能元信息都会被解析成两部分结构化字段name、version、input_schema、preconditions 等和语义向量description 的 embedding。结构字段用于精确检索语义向量用于模糊匹配。两者最后在路由阶段做一次融合打分。实际运行时路由流程是这样的拿到用户当前的任务描述提取关键词与意图先用结构化字段做一次粗筛比如任务里出现“客户”“订单”就锁定向get_customer_lifetime_value这类技能的输入标签同时计算任务文本与技能 description 的语义相似度综合粗筛结果与语义相似度按分数排序取 Top-K 作为候选技能集候选技能集连同各自的关键元信息一起送进决策模型由模型最终决定调用哪个技能。这个流程最大的价值是让决策模型从“从几十个技能里大海捞针”变成“从两三个高度相关的技能里挑一个”。候选越少模型选准的可能越大。实测下来技能数量从十几个涨到六十几个之后路由准确率没有明显下降这在过去靠模型硬读全部描述时是做不到的。3.2 路由打分要注意的坑打分这件事听起来简单实际操作里有个很容易犯的错——只计算任务文本与技能描述之间的相似度。真实场景中用户的任务描述往往高度模糊比如“帮我看看这个客户最近的情况”这个任务既可能是要查订单也可能是要查聊天记录还可能涉及工单。此时如果只做语义匹配候选技能之间分数差距会非常小模型仍然容易混淆。我的补充策略是让技能描述里加上“不适用场景”的负面示例作为负样本参与检索和下游提示。例如get_customer_lifetime_value的描述里明确写“仅用于聚合统计不适用于查询单笔订单详情如需单笔详情请用 get_order_detail”。语义检索时可以把它单独抽取出来作为过滤条件。这样即使任务描述模糊只要包含“单笔”“一笔订单”这类信号该技能的分值都会被压低模型自然偏向正确选项。场景信号命中技能降序未加负面描述时的表现“查这个客户总共花了多少钱”get_customer_lifetime_value 优先级远高于 get_order_detail两个技能分数接近随机性高“看看去年这一笔退了多少钱”get_order_detail 优先级远高于 get_customer_lifetime_valuelifetime 技能经常被误选导致报错这个表基本就是我的真实踩坑记录。负面描述是技能路由里性价比最高的优化手段没有之一。3.3 检索之外路由缓存与固定路径另一个值得说的是“路由缓存”。对同一类任务模型的选择结果其实是可以沉淀的。用户在项目里高频执行的“生成每日项目进展报告”这条链路第一次跑通后后续出现的相似任务可以直接命中原有的技能组合路径省掉整套检索 模型判断的过程。我在注册表外面又加了一层路径缓存记录“任务指纹 → 技能调用序列”。任务指纹的特征包括模块ID、参数结构、用户意图分类三个维度。命中缓存时直接执行已经验证过的技能组合不再次调用决策模型。这不仅节省了 token还让高频任务的响应时间稳定下来不再波动。这个机制也隐含一个原则Agent 不是每次都要重新思考同样的问题。系统设计者应该主动为高频路径创造快速通道而不是要求模型反复做同样的判断。毕竟“思考”的成本很高而且容易出偏差。4. 技能的嵌套、编排与上下文传递从单个能力到复杂工作流单个技能能解决的问题始终有限。真实任务往往需要多个技能配合比如“根据销售数据生成季度复盘报告并推送给团队成员”至少涉及查询数据、生成可视化、撰写文案、发送消息四个能力。技能编排就是把这些能力串成一条可控的执行链。4.1 两种编排模型链式与图式我先后试过两种编排方式。第一种是链式编排技能 A 的输出直接作为技能 B 的输入一条线走下去。这种模型的好处是简单、容易理解、容易 debug。坏处是不够灵活一旦中间出现分支比如根据数据结果决定是否发送告警写起来就很别扭。第二种是图式编排技能之间通过共享上下文交互。每个技能从上下文里读取自己需要的输入处理后把结果写回上下文再由引擎根据预设的流程决定下一步走哪个分支。业务流程复杂、存在条件跳转时图式的表达力强很多。以“销售数据复盘报告”这个场景为例我的编排配置长这样workflow: quarterly_sales_report steps: - id: fetch_sales skill: query_sales_data input_mapping: quarter: workflow.quarter department: workflow.department - id: generate_chart skill: create_chart_from_data input_mapping: data_ref: steps.fetch_sales.output branch: if: steps.fetch_sales.output.total_revenue 100000 then: skip - id: draft_report skill: generate_report_text input_mapping: chart_ref: steps.generate_chart.output data_ref: steps.fetch_sales.output - id: send_report skill: send_team_message input_mapping: content_ref: steps.draft_report.output channel: #sales-review配置驱动的编排好处是流程透明。我看一眼配置就知道每一步调用了什么技能、输入从哪来、条件分支往哪走。相比硬编码在 Python 里配置的可维护性高非常多。业务人员不需要动代码也能调整流程。4.2 上下文传递的边界控制说到图式编排最核心的调优点就是上下文如何传递。早期版本我图省事让所有技能共享同一个大 context 字典技能 A 写进去的内容技能 B 能直接读。结果就出了问题——技能 A 写了一个date字段技能 B 也写了一个date字段覆盖得无声无息最后下游拿到的日期完全不对。排查了两个小时才发现是两个技能的字段名撞了。后来我引入“命名空间”机制。每个技能写入上下文的键都必须带技能名前缀例如get_customer_lifetime_value.output.total_amount。读取时只能读自己声明依赖的字段。同时引擎在技能执行前会把上下文里允许该技能读写的字段声明出来执行完成后校验代码是否越权写入。这样虽然写起来啰嗦了一点但基本杜绝了字段污染的问题。如果你现在的技能系统规模还没大到需要完整上下文框架也至少请记住一条给每个技能的中间产物一个独立的命名空间不要共享裸字段名。4.3 编排中途失败的恢复策略编排越长中途失败的概率就越大。技能链一旦在某一步报错前面已经执行完成的技能结果怎么处理是全部作废重来还是在失败点重试或是从断点继续我的处理方式是给每个技能声明side_effects元信息。如果技能的副作用只是内存里的数据计算那失败后无脑重试即可。如果技能带外部副作用比如已经发了邮件、已经写了一条数据那必须实现回滚逻辑。side_effects字段在元信息里早就定义了这里才真正派上用场。实测中面对外部 API 偶发超时、网络抖动这类错误最简单的有效策略是带退避的重试而不是立刻失败。我通常配置最多三次重试第一次失败后等 500 毫秒第二次等 2 秒第三次等 5 秒。超过三次才真正判定失败并触发整条链路的回滚。不要小看这个笨办法它把我在真实业务里的编排失败率降了大概七成。5. 我在实战中踩过的坑探测失效、描述污染和技能漂移到了这一节我把项目落地过程中最值得讲的三类坑摊开说。这些都是文档里不会写、网上也很少有人系统总结的经验。5.1 坑一“会调工具”不等于“技能真的可用”有一段时间我把技能在单元测试里跑通作为“可用”的唯一标准。后来上线第一天就被打了个措手不及技能查询接口用的 API Key 因为是测试环境的在线上环境里没有权限一连串调用全部 401。技能“存在”和技能“在当前环境可用”是两码事。完整技能体系里光有注册、检索、执行还不够还必须有持续的可探测性验证。我做了一个周期性的技能探针任务每隔几分钟从注册表里随机抽取一批技能用模拟参数发起一次执行校验返回结果是否符合output_schema。探针任务把所有技能标识成三个状态正常、异常、降级。异常技能会在路由阶段被剔除降级技能会在候选集里被压到最低优先级。这个探针看起来简单但价值极大。它把技能可用性从“碰到问题时被动发现”变成了“在产生影响前主动拦截”。上线探针之后我经历过某一数据源认证过期导致二十多个技能集体异常的事故。探针在用户真正调用前三分钟就发现了异常技能自动从注册表隐藏前端用户完全没感知到故障。5.2 坑二技能描述被 LLM 输出污染这个坑我印象很深。技能描述最开始是我手工写的风格基本统一。后来为了加快迭代速度我尝试用大模型批量生成技能描述再从其中挑合适的收入注册表。模型生成的描述细节丰富、看起来逻辑严密但用的时候问题不断。问题出在描述内容不干净。模型会把“上个版本是这么实现”这类语料写进描述还时不时冒出一句“注意如果数据为空请提醒用户确认 CRM 系统是否配置了正确的客户 ID”。此类信息残渣进入 embedding 和路由检索之后产生了两个后果语义向量聚焦到无关信息上检索排名出现偏差决策模型读到描述里的特殊情况频繁选择该技能却没有处理对应的数据条件反而增加了挫败的调用路径。最后我的处理方式是花大力气清洗描述并把描述分成标准字段让模型逐个填充而不是整段自由生成。我对每个技能的描述字段固定为四段技能用途、适用场景、不适用场景、使用限制。模型只能按这个模板补全不允许自由发挥。这四段内容保证每种信息各归其位检索和提示都能提取到结构化信息。描述生成可以用 LLM但必须有强约束否则是在给系统埋雷。5.3 坑三技能漂移——老技能悄悄变了味儿项目迭代半年后我开始收到一类反馈“以前这个技能挺好用的最近怎么老出错”查来查去发现某个人改了技能内部的某个参数阈值另一个人为了兼容新业务改了输出字段的命名还有一次是技能的底层 API 换了新版本返回结构变了但技能代码没跟上。这类问题我统称为“技能漂移”——技能定义、实现、行为在持续不受控地变化。漂移直接切断了“技能名称”与“技能行为”之间的稳定性而稳定性恰恰是技能体系的基础。应对漂移的办法是版本锁定和回归测试缺一不可。每个技能在注册表里都有版本号。部署时锁定版本更新必须走升级流程不能直接改正在用的技能文件。同时为关键技能建立“行为指纹”——固定输入对应的固定输出。每次技能升级CI 自动跑一遍行为指纹回归输出差异超过阈值就阻止合并。skill: get_customer_lifetime_value version: 2.1.0 fingerprint: input: [{customer_id: C001, start_date: 2024-01-01}] expected_output_hash: a94f8c...这套机制加上前面的可用性探针让我终于不再担心“某个技能在没人注意的时候悄悄变了一个样子”。技能的每一次变化都有记录、有校验、有回退依据系统整体重新进入可控状态。最后再分享一个实际的体会。agent-skills表面上是一堆文件、接口、注册表和路由逻辑但它的核心其实是把“让模型临场发挥”转变成“让系统稳定发挥”。每次你在某个 Agent 任务里发现同样的错误反复出现问一下自己这个问题是应该靠提示词再去“叮嘱”模型注意还是应该把这个步骤抽成一个带输入输出契约、带校验、带版本、带可用性探测的技能只要做过一次改造你就能感觉得到两者的差别。技能的抽象听起来是个工程名词但放在智能体这个语境里它就是把不可控的智能变成可控的基础设施。这也是我现在对agent-skills最直接的理解。