Agent技能封装实战:从混乱工具调用到可复用技能库 1. “agent-skills”到底在解决什么问题我在做 Agent 项目的第三个月决定把所有的能力模块全部重写成一套统一的 agent-skills 体系。起因很直接同一个智能体换了个业务场景之后原来的提示词和工具调用逻辑全乱套了。当时最大的感受是模型本身并不弱真正拖后腿的是我给它的“手和脚”——那些能力边界模糊、互相耦合、难以单独验证的函数与提示词片段。这里说的 agent-skills不是某个框架的专有名词而是我习惯用的一套工程化组织方式把 Agent 能做的事拆成一个个可声明、可测试、可复用的技能模块每个技能都有清晰的描述、输入输出协议、实现逻辑和验证方式。如果你正在开发 AI 助手、自动化流程、智能客服这类产品或者你发现自己的 Agent 到了“demo 能跑、上线就崩”的阶段那么这篇文章里的思路和踩坑记录应该能帮上忙。1.1 一次事故让我决定重写技能层事情发生在给一个客户做智能客服机器人时。当时为了快速上线我把所有工具调用逻辑都堆在一个大文件里意图判断靠大段 if-else业务规则散落在各处。最初功能很少跑起来还算顺畅。后来客户要求增加节假日话术我在某个工具函数里改了一行返回值格式结果导致另一个不相关的意图分支开始错误触发线上对话连续出现答非所问。排查了很久才发现问题根源不是模型而是我把“技能”和“业务规则”焊死在了同一段代码里。节假日话术本质上是一个独立的领域能力它应该有自己独立的入口、参数和返回结构可以被单独替换和测试。但在当时的架构里它和周边十几个功能共用同一个状态机牵一发而动全身。这次事故之后我把思路调整为“技能优先”凡是 Agent 需要对外执行的动作先抽象成技能再通过技能模块去组合业务规则。这样每个能力像抽屉一样独立存在模型按需抽取工程侧也能针对单一技能做回归测试。听起来像常识但真正动手做之前我确实低估了这件事的价值。1.2 技能模块解决的三个真实痛点第一个痛点是模型的可理解性。如果你把一堆业务逻辑直接写进系统提示词模型需要从长文本里自己找调用条件稍微复杂一点就容易漏。技能模块则把每个能力压缩成一段“能力说明书”模型只需要在候选列表里做匹配理解和选择的成本都低很多。第二个痛点是工程的可测试性。传统工具函数可以单测但 Agent 的工具调用链路很难单测因为你不知道模型会在什么上下文里触发它。技能模块通过标准化输入输出和独立执行逻辑让测试变成一个纯粹的函数验证过程。先测技能本身能不能跑再测模型能不能选对技能问题边界变得非常清楚。第三个痛点是能力的可复用性。同一个“查询订单”技能可以用在客服机器人、工单助手、企业微信机器人里只要描述和协议不变换场景就是换 Agent 壳。之前我所有的能力都长在某个项目里换个项目就要复制粘贴改一堆东西现在沉淀成 agent-skills 库之后新项目的冷启动速度明显快了很多。这三件事听起来不大却直接影响 Agent 从“能用”到“好用”的关键一步。1.3 什么内容才配叫 skill不是所有函数都值得做成技能。我现在的判断标准有三个一是输入输出边界是否清晰二是是否有可预期的副作用三是能否独立验证。比如“查询订单状态”“计算运费”“发送提醒消息”这类操作边界明确、结果可检验天然适合做成技能。相反有些任务过于开放比如“写一篇爆款文章”就不适合直接塞成一个技能。这类任务需要进一步拆解为“生成标题候选”“搭建文章大纲”“生成正文段落”等更小的子技能否则描述写不清楚模型也不知道从哪下手。过早把大而泛的能力固化成技能只会让注册表变得臃肿还会增加模型选错技能的几率。判断一个能力能不能沉淀为技能我的经验是先在对话里手工测试三次如果你每次都需要补充新规则才能让它稳那就说明这个技能还没有收敛继续拆。2. 重新拆解 agent-skills一个技能单元长什么样我落地的 agent-skills 体系里一个完整的技能单元由四部分组成技能描述、输入输出协议、实现逻辑、自检测试。很多人只重视实现逻辑把技能当成普通函数来写结果模型根本不调用或者调用了却传错参数。其实在大模型应用里最关键的往往是那几行“给模型看的描述”。2.1 技能描述给模型看的产品说明书技能描述的目标是让模型在候选列表里一眼认出“这个能力该不该由我来触发”。我写描述的格式基本固定第一句说明能力边界第二句说明执行前提第三句给出常见参数示例。最后还会加一句“什么时候不要用”用来减少误触发。举个例子一个查询天气的技能我会写成这样name: get_weather description: | 查询指定城市当前天气和未来三天预报。 当用户明确提到天气、气温、降水、风力等意图时使用。 参数 city 需要是中文城市名尽量从用户原句中提取。 如果用户只是在闲聊天气感受不要调用本技能。这样的描述看起来很短但信息密度很高。模型在做技能选择时本质上是在做语义匹配它不需要看到你的 Python 类型注解它需要的是“触发条件”和“参数来源”。我见过很多团队把函数文档直接复制成技能描述里面全是技术术语模型当然容易选错。2.2 输入输出协议一切可以序列化技能和普通函数的另一个区别是它面向的调用方不是程序员而是模型。模型生成的是结构化文本所以技能输入输出必须是可序列化的最好用 JSON Schema 明确约束。我在定义协议时会为每个技能建立一个输入输出结构例如from typing import TypedDict, Optional class GetWeatherInput(TypedDict): city: str date: Optional[str] # 缺省则为今天 class GetWeatherOutput(TypedDict): status: str # ok 或 error data: Optional[dict] # 天气详情 message: str # 给模型看的简短说明输出里一定要带上message。因为模型需要根据返回值决定下一步动作如果技能只返回一个裸 dict模型很容易不知道发生了什么。加上一句“查询成功北京今天晴最高温度 30 度”这样自然的描述模型就能直接理解并转述给用户。协议设计要尽量扁平避免深层嵌套。模型擅长生成平面结构复杂的嵌套对象容易出现字段缺失或类型错误。宁可多几个顶层字段也不要搞三层以上的对象。2.3 注册与发现把代码变为数据最初的版本里我是用一个巨大的 if-else 去分发工具调用后来换成注册表机制。注册表的核心思路是把技能名、技能描述、输入结构和实现函数登记到一个全局字典里模型只需要看这个字典的“目录页”就能了解整个 Agent 的能力范围。我通常用 Python 装饰器来做这件事代码会清清爽爽SKILL_REGISTRY: dict[str, Skill] {} def skill(func): name func.__name__ SKILL_REGISTRY[name] Skill( namename, descriptionfunc.__doc__.strip(), fnfunc, ) return func skill def get_weather(city: str, date: str ): 查询指定城市当前天气和未来三天预报。 ...当模型需要调用时Agent 会把SKILL_REGISTRY里所有技能名和描述拼成一个“技能菜单”让模型从中选择。这个过程中代码变成了数据新增技能不再需要改分发逻辑只需要写一个函数并加上装饰器。这里有个容易被忽略的点注册顺序会影响模型的选择概率。如果两个技能描述相似通常排在前面的更容易被选中。所以我会把高频技能放在前面低频技能放在后面而不是按字母序排列。3. 实操从零搭建一套可落地的 skill 库理论说了一堆接下来展示一套我实际在用的技能库结构和完整示例。沿着这个模板你可以很快把现有代码整理成属于自己的 agent-skills 库。3.1 目录结构与命名规范项目根目录下我会专门建一个skills文件夹每个技能独占一个子目录子目录里放描述文件、实现文件和测试文件。skills/ ├── get_weather/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py ├── get_exchange_rate/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py └── send_email/ ├── skill.yaml ├── impl.py └── test_impl.py技能命名的规范我用的是“动词 目标对象”比如get_weather、send_email、calculate_delivery_fee。尽量避免使用process_data、handle_request这类模糊名字因为模型在技能匹配时对名称很敏感动词越具体召唤成功率越高。skill.yaml存放模型的可见元信息包括技能名、描述、参数示例和授权级别等。impl.py是纯实现逻辑不掺杂任何 Agent 上下文。test_impl.py是单元测试直接调用函数验证结果。3.2 一个最小案例汇率查询技能我们做一个最简单的汇率查询技能。先写skill.yamlname: get_exchange_rate description: | 查询实时汇率支持常见货币之间换算。 当用户提到汇率、换汇、外汇、某货币兑某货币时使用。 参数 base 表示基础货币代码quote 表示目标货币代码。 如果用户未指定目标货币默认使用 CNY。 返回换算比例和参考金额。 version: 1.0.0然后写impl.pyfrom typing import TypedDict, Optional class ExchangeRateInput(TypedDict): base: str # 基础货币如 USD quote: str # 目标货币如 CNY amount: Optional[float] # 金额缺省则返回汇率 class ExchangeRateOutput(TypedDict): status: str rate: Optional[float] converted: Optional[float] message: str def get_exchange_rate(input_data: ExchangeRateInput) - ExchangeRateOutput: base input_data[base].upper() quote input_data.get(quote, CNY).upper() try: rate fetch_rate_from_db(base, quote) except Exception as e: return { status: error, rate: None, converted: None, message: f查询失败{e}请检查货币代码是否输入正确, } amount input_data.get(amount) converted amount * rate if amount is not None else None if amount is not None: message f{amount} {base} 约等于 {converted:.2f} {quote}当前汇率为 {rate} else: message f当前 {base}/{quote} 汇率为 {rate} return { status: ok, rate: rate, converted: converted, message: message, }接入 Agent 时只需要把get_exchange_rate注册进SKILL_REGISTRY然后把技能菜单交给模型。整个过程不需要改业务代码。我第一次重构时最惊讶的就是原来加一个新能力可以这么快。3.3 技能自检与 dry_run实际运行中模型经常传错参数比如把“人民币”直接当货币代码传进来或者把日期传成“明天”。为了减少这类问题我在每个技能里加了一个轻量自检逻辑当参数缺省或明显异常时返回一个“提示型错误”告诉模型应该怎么补参数。我还会给关键技能增加dry_run模式。这个模式只做校验和演练不真正产生副作用。比如发送邮件技能在dry_run下只会打印“将向某某发送主题为某某的邮件”不会真的发出去。这样能让模型在正式生成动作前先自检一遍大幅减少误操作。dry_run的实现也简单就是给输入增加一个test_mode字段处理函数在开头判断一下。别小看这个字段它是我做技能灰度时最依赖的安全阀。3.4 版本管理与灰度技能不是写一次就不动了。业务规则一改技能实现就要跟着改。但模型的行为需要保持一致所以我给每个技能加了version字段注册表里记录当前请求使用的版本号。升级时先记录旧版本行为方便回滚。灰度策略我做得比较朴素注册表里同时保留新旧两个版本通过一个开关分配流量。比如get_exchange_rate从 v1 升到 v2先让 10% 的请求走到 v2观察调用成功率和用户反馈再逐步放量。这个方法不花哨但确实能帮我避免“一次性全量上线然后被模型的新错误行为淹没”的情况。版本管理最需要注意的坑是不要只改描述不改协议。如果 v2 改了参数结构一定要在skill.yaml里同步更新否则模型按旧描述生成新参数技能直接报错。4. 真正让 skills 好用的几个关键细节如果说前面是骨架这一节就是血肉。我自己在把 agent-skills 打磨到能上生产环境的过程中积累了几个非常实际的经验。4.1 描述里要明确“什么时候不要用”给技能写描述时大家很容易只写“什么时候用”却忘了写“什么时候不要用”。在真实对话里模型经常过度调用技能。比如用户只是抱怨“今天天气太糟了”并不想查天气但如果你的天气技能描述里全是“天气”“气温”等词模型可能就触发查询。我现在会在描述末尾固定加一句如果用户只是在表达主观感受不要调用本技能。这句“负向提示”对降低误触发非常有效。同样地如果一个技能只能由管理员使用就在描述里写清楚“普通用户询问权限相关问题时不要调用转交由权限判断逻辑处理”。负向描述不用太长一两句话点中常见混淆场景就够了。写得太多反而会让模型困惑。4.2 错误信息是给模型看的纠错信号技能里抛异常很容易但模型拿到的只是一个异常字符串时往往不知道下一步该做什么。我把错误信息改成了结构化格式包含状态、原因和建议{ status: error, reason: invalid_currency_code, suggestion: 请将货币参数改为国际标准代码例如 USD、CNY再重试 }这样模型看到suggestion后会自然地对用户说“请提供标准货币代码”甚至主动修正参数后重试。我实测过结构化错误让技能调用失败后的恢复成功率提高了不少。记住技能返回的错误也是模型的一次“输入”你的错误信息写得越像给同事看的消息模型就越容易接着干活。4.3 控制返回体量与敏感信息Agent 的上下文窗口是有限的。如果技能返回一大段完整订单明细、几十条搜索结果模型还没开始推理上下文就已经被撑爆了。我的原则是技能返回给模型的内容只保留“决策所需的最小信息量”详细信息写入外部存储需要时再按消息 ID 拉取。举个例子查询订单列表时不要在返回值里塞完整的商品详情和物流轨迹只返回订单号、状态、金额、时间这几个字段就够了。如果用户追问详情再通过另一个“查询订单详情”技能去取。这种拆分不仅省 token还让每个技能的链路更短、更容易排查。敏感信息方面技能输出里绝不能带明文密码、完整身份证号、银行卡号等。我在输出层做了一层脱敏比如只返回尾号四位。防的不只是模型还有日志系统和下游服务。任何时候审计日志里都不该出现用户敏感字段。4.4 幂等性与并发安全多个技能被并行调用时很容易出现重复副作用。最典型的是“支付”或“发消息”这类操作模型判断失误重试两次用户就收到两条消息。所以我在技能设计里强制要求凡是有副作用的技能必须支持幂等。幂等的做法很简单给每次调用生成一个request_id服务端记录这个 id 是否已经处理过。如果重复提交同一个request_id直接返回上一次的结果不再次执行副作用。同时技能内部尽量保持无状态不要依赖全局变量避免并发时数据互相污染。这个设计在单机 demo 里看不出来一旦技能被多个 Agent 实例共享或者被用户手动触发和模型触发同时调用幂等就是保命符。5. 常见问题与踩坑实录再正确的理论落到实战里都会有一堆意想不到的问题。这里记录几个我反复遇到的坑以及对应的排查方法。5.1 模型不调用技能时先别急着调 prompt模型完全无视技能菜单是最常见的问题。很多人第一反应是加长系统提示词结果越加越乱。我现在的排查顺序是先看技能描述是否出现在模型上下文中再看描述里的关键词是否和用户表达有明显匹配最后才考虑调整 prompt。如果用户说“帮我查下美元兑人民币”技能描述里却没有“美元”“人民币”这些具体词模型就很难触发。解决方法是把常见说法作为示例写进描述里比如支持 USD/CNY、EUR/CNY 等常见货币对。这不是让模型死记硬背而是给它更容易匹配的锚点。另一个容易被忽略的原因是技能菜单太长。当候选技能超过十几个时模型可能遗漏靠后的技能。我会把高频技能排在前面并且为同一类能力做一个“分组描述”减少候选数量。5.2 技能明明存在却选错了技能选错技能比不调用更隐蔽。我遇到过两个技能描述高度相似一个是“查询订单”另一个是“查询售后单”模型总是把售后单查询请求派给订单查询。后来我在两个描述里分别加入了“如果不确定是哪个先问用户是否有售后纠纷”效果立竿见影。还有一个技巧是给每个技能写一个“反例”字段比如not_to_use: 当用户提到退货、换货、维修时请选择 query_after_sale。这种显式的互斥指引比单纯加形容词有用得多。要彻底排查我会维护一个技能评测集每个评测样本包含“用户话术”和“期望技能名”。每次改动描述后跑一遍看准确率和召回率变化。没有评测集你根本不知道哪次描述改动是变好还是变坏。5.3 技能返回内容撑爆上下文早期我做一个搜索类技能直接把前几十条搜索结果全部返回模型还没来得及总结上下文就已经爆了。后来我改用“分页摘要”策略技能只返回前 5 条结果的核心标题和摘要如果用户要更多再通过参数page翻页。这样既控制了 token又让模型每一步只聚焦一小批信息。还要注意返回值里的“冗余信息”。有些技能实现者图省事把整个数据库行原样返回里面全是创建时间、更新时间、内部 ID。这些字段对模型决策没有帮助只会稀释注意力。我在输出层做白名单字段明确哪些可以出站。5.4 问题排查速查表症状可能原因处理方式模型从不调用某技能描述关键词不匹配、技能排序太后补充用户常见说法调整注册顺序调用技能但参数频繁错误输入协议定义过宽缺少示例在描述中增加参数格式示例多个技能经常混淆描述相似度高缺少互斥说明加反例字段明确触发边界技能返回内容太长未做摘要和字段白名单只返回决策所需最小字段升级后行为变化大描述或协议未同步更新版本号分离灰度放量重复执行副作用操作技能非幂等增加 request_id 去重这张表是我每次上线前都会过一遍的基础检查清单能帮我快速定位八层以上的问题。6. 落地 agent-skills 一年后的个人体会6.1 技能库需要“新陈代谢”技能库不能只增不减。我在维护了大半年后发现很多早期技能已经没人调用但还在注册表里占着位置每次模型选择时都会浪费注意力。后来我加了一个“调用日志”统计每个月清理一次近 30 天调用次数为零的技能。不是直接删而是先标记为deprecated下线再删代码。这个过程让我意识到agent-skills 不是一个静态的目录它更像代码库本身需要持续的 review 和重构。给技能写描述时我心里会有个标准如果一个新同事不看实现代码只看skill.yaml能完全理解这个技能的能力边界那才算合格。达不到标准的描述一律重写。6.2 下一步可以从评测与观测入手如果你已经搭好了一套技能库我建议下一步把重心放在“可观测性”上。每次技能调用都记录下选技结果、参数、耗时、返回状态然后定期统计调准率。我后来用这些数据做过一次很有效的优化发现某个技能虽然经常被选中但执行成功率只有六成原因是它的输入协议和描述之间存在两张皮模型按描述生成参数函数却按更严格的协议校验。改掉这个不一致后整体成功率立刻回升。最后再分享一个小技巧新增技能之前先问自己三个问题——这个能力可以被一句话说明吗它的输入输出可以被结构化吗它值得被独立测试和复用吗三个问题都回答“是”再做。少建一个模糊技能比多写一个完美技能更重要。这套思路陪我走过几个项目也希望帮你少踩一些坑。