Agent技能库实战:从工具封装到稳定可复用的工程化设计 做了快两年大模型Agent开发有一个越来越强烈的感受很多团队卡住的不是模型能力而是不知道怎么把模型能力变成稳定、可复用、可迭代的工程资产。最早接触agent-skills这个概念时我以为是给模型写几段“行为说明”而已真上手把业务场景拆成一套技能库之后才发现从命名、描述、参数Schema到错误处理、版本管理每一步都有大量坑。这篇文章会把我在项目里从0到1搭建Agent技能库的完整过程、设计思路和实测排障链路摊开写清楚希望能给正在做或准备做Agent开发的同行提供一份可以直接参考的实战手册。1. 先搞明白技能和工具、提示词到底有什么区别1.1 我在项目里遇到的实际痛感最开始我们团队做的是电商售后客服Agent需求很明确用户问订单、查物流、申请退款、改地址、催发货。第一版方案很朴素直接把后端十几条API接口改写成OpenAI function calling里的function再在系统提示词里写了一大段“什么情况调用什么函数”的规则。结果一上线就翻车模型经常把订单号和物流单号搞混用户说要“取消订单”它却调用了“退款”接口更离谱的一次是模型自己编了一个不存在的参数值传进函数。后来我才意识到问题出在我把“工具”和“技能”混为一谈了。后端API是最原子的操作但模型面对的不应该是这些原子操作而应该是“一个能直接完成用户目标的动作”。于是我开始把多个API的调用流程、异常分支、数据格式转换全部封装成技能模块再把这些技能以类似tool的形式暴露给模型调用成功率才明显上来。1.2 三者的边界Tool是手Skill是动作组合Prompt是行动指南想理解agent-skills的价值必须先分清这三个概念。**Tool工具**是最小的可执行单元。比如“查询订单表”、“调用支付退款接口”、“发送短信”它只做一件事不关心用户到底要什么。它就像工具箱里的螺丝刀单独存在不能独立解决“修好一个电风扇”这样的目标。**Skill技能**是围绕一个业务目标组织的完整动作序列。它可能包含多个工具的调用、数据校验、状态判断、错误处理。比如“处理用户退款”这个技能内部要先查订单状态、判断是否超出售后时限、计算可退金额、调用退款接口、记录退款单号、给用户发送通知甚至还要处理退款失败后的重试。它就像“修好电风扇”这件事的方法论拆开外壳、检查电容、更换损坏零件、重新装配。模型只需要说“我要用处理用户退款这个技能”剩下的事情由技能内部自己完成。**Prompt提示词**则是描述行动规则的语言指令。它可以是技能的一部分比如我们可以在技能描述里用一段话告诉模型什么时候该调用它但Prompt本身不执行任何操作它只是引导模型做决策。实际工程里很多人喜欢把大量逻辑写进系统提示词结果提示词越来越长模型注意力被稀释。正确的做法是把能固化的判断和操作全部装进技能里让提示词尽量短只保留必要的身份信息和通用约束。这三者的关系可以这样理解Tool是手Skill是熟练工的动作组合Prompt是贴在墙上的操作说明。熟练工看说明但干活靠的是手和脑的组合如果你只给模型一堆Tool和一个很长的说明书它还是会经常抓错工具、不知道下一步该干什么。2. 技能库前期设计把任务拆成技能的关键逻辑2.1 从业务场景反推技能清单设计技能库最容易犯的错误是直接按系统已有的API划分技能。比如公司有订单接口、库存接口、支付接口就做成“查订单”技能、“查库存”技能、“发起支付”技能。这表面上是技能实际上还是工具思维。正确的做法是从用户意图出发按“用户希望结果发生什么变化”来划分。拿售后场景举例用户可能产生的核心结果有获取订单信息、修改收货信息、取消未发货订单、申请退款、查询物流进度、催促发货。这六个结果就对应六个技能。技能内部是调三个接口还是五个接口都由技能自己消化。我在新项目里一般会先收集200到300条真实历史对话逐条标记用户最终想要的结果然后按结果聚类。如果一类结果里涉及的内部操作明显分成若干个独立阶段比如退款流程要先验证资格再执行扣款我会拆成“校验退款资格”和“执行退款”两个技能吗通常不会除非校验和执行会被不同场景单独调用。我的经验是**一个技能的完整执行流程最好控制在5步内超过5步就考虑是做成编排好的Workflow而不是Skill。**因为技能越复杂描述就越难写准模型也越难判断自己是否该调用它。2.2 技能命名的信息密度与描述的黄金公式技能的名字和描述很大程度上决定了模型能不能选中它。命名有一个很实用的原则动词 业务对象 场景限定。比如“create_order”这种命名信息量太少模型对“下单”和“支付”可能混淆。“给用户创建待支付订单”就好很多。更稳妥的是在描述里把所有用户可能说出的话都覆盖到。我给技能描述定了一个黄金公式技能名称简短但唯一的动词短语 描述适用条件什么时候用 行为说明会做什么 排除条件什么时候千万别用 参数每个字段必须有类型、说明、示例、允许的枚举值 输出固定格式的结构化数据举一个真实例子。最开始我们的“取消订单”技能描述写的是取消用户的订单。结果用户说“我不想要了能退吗”模型分不清应该用“退款”还是“取消”经常随机选。后来我把描述改成当用户明确表示要取消一笔尚未发货的订单时使用。取消操作会终止订单并释放库存。如果订单已经发货请不要使用本技能应引导用户申请退款。当用户提及“退款”、“退钱”、“我不买了但货已经发出”等情况时请优先使用退款技能。这样一改误选率明显下降。核心技巧就是把和它容易混淆的技能做显式区隔把这句区隔写进描述里而不是指望模型自己领悟。2.3 技能间依赖与冲突规则技能通常不是孤立的。一个流程里可能要先技能A再技能B。我在设计时就要求每个技能在返回值里带上它完成后的关键状态标识。比如“取消订单”技能会返回订单状态为“cancelled”并返回可用的售后单号这些字段会放到对话上下文里方便后续“退款”技能直接识别而不用让模型从零开始推断。技能之间也可能出现“都匹配”的情况比如“查询订单”和“查询物流”在用户同时问“我的东西到哪儿了”时都像。处理办法是在技能描述里给出精确边界比如“查询订单”侧重订单金额、商品明细、订单状态“查询物流”侧重物流节点、位置、预计送达时间。同时可以在系统层面设置优先级权重但我的建议是尽量不依赖优先级而是靠描述把重叠面降到最低。3. 技能落地的工程结构从函数到技能描述的一步步实现3.1 一个标准技能模块的代码骨架技能不只是一个函数它应该是一个自带描述、参数schema、执行逻辑、错误处理的最小工程单元。我用Python写一套统一基类每个技能都继承它这样注册、校验、调用全流程都能被框架统一管理。下面是一个极简但完整的技能基类示例# skills/base.py from typing import Any, Dict class BaseSkill: name: str description: str parameters: Dict[str, Any] {} def get_schema(self) - Dict[str, Any]: 返回可以传给大模型 tools 参数的 JSON Schema return { type: function, function: { name: self.name, description: self.description, parameters: { type: object, properties: self.parameters, required: [k for k, v in self.parameters.items() if v.get(required)], }, }, } def execute(self, **kwargs) - Dict[str, Any]: raise NotImplementedError具体技能实现# skills/refund_order.py from skills.base import BaseSkill class RefundOrderSkill(BaseSkill): name refund_order description ( 当用户明确要求退款、退钱、退回货款或者订单已发货但用户不想要时使用。 该技能会校验订单状态计算可退金额执行退款并生成退款单号。 如果订单尚未发货且用户要求取消请调用 cancel_order 技能。 ) parameters { order_id: { type: string, description: 订单号格式如 ORD-2025-0001, example: ORD-2025-0012, }, refund_reason: { type: string, description: 用户输入的退款原因可简略概括, example: 收到商品与描述不符, }, auto_approve: { type: boolean, description: 是否自动通过退款申请true表示走自动退款false表示需要人工审核, }, } def execute(self, order_id: str, refund_reason: str , auto_approve: bool True) - Dict[str, Any]: # 这里是业务实现比如调用退款API、写日志、发通知 result { refund_id: fRF-{order_id}, status: approved if auto_approve else pending_review, amount: self._cal_refund_amount(order_id), } return result def _cal_refund_amount(self, order_id: str) - float: # 简化逻辑正常场景需要查询订单实付金额 return 199.00注册中心只需要把所有技能实例放进一个列表skills [RefundOrderSkill(), CancelOrderSkill(), QueryLogisticsSkill()] tools [s.get_schema() for s in skills]然后把tools传给模型API模型返回的tool_call里的function.name可以直接用来查字典并执行skill_map {s.name: s for s in skills} call response.tool_calls[0] skill skill_map[call.function.name] args json.loads(call.function.arguments) result skill.execute(**args)3.2 参数Schema设计中的坑类型、枚举和必填参数Schema是大模型与我们业务系统之间的“接口契约”这里出问题的概率极高。我列几个踩过的坑。第一类型必须严格。有些模型会默认把字符串数字传成数字类型比如订单号“012345”可能被解析成12345前面两位0丢失。我踩过一次数据匹配失败的坑后才长教训订单号字段设置成type: string并在description里写“请保留原始字符串不要转成数字”。第二能用枚举就不要开放自由文本。比如退款原因如果定义成type: string模型会自由发挥出各种奇怪的表述后续统计分析很痛苦。我发现把原因归类成几个固定枚举值会好很多refund_reason: { type: string, enum: [商品破损, 描述不符, 尺码不合适, 多拍/拍错, 不想要了, 其他], description: 用户选择的退款原因分类 }模型输出任意文本时我在技能内部做一层映射把“东西坏了”“质量太差”等模糊说法映射到“商品破损”。但前提是这部分映射逻辑要在技能代码里写清楚不要指望模型自己完成归一化。第三必填字段要克制。实战经验是模型在参数不完整时要么强行编造要么重复问你。解决办法是除了绝对必须的字段外其余都设为可选并让描述里注明“如果不确定可以留空由系统自动判断”。比如自动审核标志不一定需要用户明说我可以让模型不传技能内部默认走自动审核。第四避免要求模型输出嵌套结构。我最初把收货地址设计成{province: ..., city: ..., detail: ...}这样一个嵌套对象结果十次有八次模型漏掉某个子字段。后来改成直接在参数里展平为province、city、detail三个字段再由技能内部组装成地址字符串成功率直接拉满。3.3 技能内部错误处理与重试机制技能执行过程中一定会遇到外部依赖失败的情况。这时候不能直接把Exception抛给上层循环让模型再调一次同一个技能很容易造成重复扣款之类的严重问题。我的做法是技能内部捕获所有异常并返回统一的错误结构def execute(self, **kwargs): try: # 业务逻辑 return {success: True, data: {refund_id: ...}} except OrderNotFoundError: return {success: False, error: order_not_found, message: 订单不存在请核实订单号} except RefundApiError as e: return {success: False, error: refund_api_error, message: f退款接口异常: {e}}返回错误后模型会在下一轮对话里看到这个结果从而决定是修改参数重试还是换一个技能或者直接向用户解释失败原因。这里有一点非常重要只有幂等操作才能自动重试。比如查库存、查物流可以放心重试但退款、扣款、修改订单这种非幂等操作必须在技能内部生成一个幂等键每次调用使用同样的order_id和request_id并在API层保证相同幂等键不会重复执行。4. 实测验证四类常见翻车现场与排查链路4.1 模型选错技能描述关键词陷阱排查思路永远是从日志开始。我要求每个Agent的调用链路都要记录当前模型选中的function.name、传入的原始参数、技能执行结果。一旦发现用户说“我要退掉这个订单”系统却调用了cancel_order就把这段记录拉出来。这类问题的根因基本在于描述的“关键词重叠”。cancel_order的描述写了“用于取消订单”refund_order的描述写了“用于退款”两者在用户口语里界限模糊。解决分两步第一步重写描述加入明确的负向排除。第二步构造一批易混淆的测试语句离线跑一遍确认模型选型全部正确后再上线。我还发现一个细节技能的排序会影响模型选择。在OpenAI的tools参数中靠前的函数往往有被优先选择的倾向。我把调用频率最高的技能往前排频率低的往后排误选率也降低了一些。4.2 技能输出格式不稳定强制结构化返回早期我让技能返回一段人类可读的文本比如“退款成功退款金额199元退款编号RF-12345”。模型拿到这段文字后继续给用户生成回答文本本身没问题但如果后续还要接着调用其他技能模型就需要从这段文字里解析出退款编号这一步很容易出错。现在所有技能统一返回JSON并且我会在技能描述里写明返回结构比如返回结果为一个JSON对象包含refund_id字符串、amount数字、status字符串枚举approved/pending_review/rejected。同时在技能执行函数内部用json.dumps(result)强制序列化绝不允许返回自由文本。这样上下文里的技能输出永远是结构化数据模型读取时几乎没有歧义。4.3 多技能协作时的上下文污染有一次用户先说“帮我查一下订单ORD-12”系统调用query_order技能返回里有个status: shipped。接着用户说“那申请退款”模型调用refund_order技能时居然把上一轮返回里的status: shipped当成参数传给了refund_reason。参数字段完全对不上API直接报错。这种上下文污染在多轮对话里很常见。我做了两件事第一在技能执行结果前面加一个显式前缀比如[SKILL_RESULT] query_order: {status: shipped}让模型清晰地意识到这是前面的技能结果而不是当前对话的自然语句。第二在技能描述里写明“执行本技能所需的参数必须从用户当前这句话中提取不要从历史技能执行结果里读取如果缺少请反问用户”。更重要的是我在参数提取层加了一道校验模型传出的参数必须满足所有必填字段的类型要求否则直接返回错误并提示模型补充。不要让它带着残缺的参数进入执行阶段。4.4 技能运行超时与并发冲突技能调用外部API如果长时间无响应模型侧会一直等待甚至超过大模型接口本身的超时时间造成整个会话失败。我给每个技能的execute包了一层带超时的执行器默认5秒超过就返回“技能执行超时请稍后重试”。查询类技能可以把超时放宽到10秒写操作类技能统一5秒。并发是另一个容易被忽略的坑。同一个用户开了两个会话窗口同时操作同一笔订单可能触发重复退款。我在订单状态表里加了版本号字段每次执行退款前先检查当前版本执行时带条件更新UPDATE order SET statusrefunded, versionversion1 WHERE order_id? AND version?受影响行数为0就说明被其他会话改变了技能返回“订单状态已更新请刷新后重试”。5. 把技能库当成产品来迭代版本管理、评测与经验沉淀5.1 技能变更如何不做坏旧场景技能一旦上线就会被很多Agent流程依赖改接口、改描述都可能引发连锁反应。我从业务系统的版本管理思路里借鉴了一套轻量策略每个技能模块维护一个语义化版本号比如v1.2.0。技能描述和参数Schema的变更要记录在CHANGELOG里。变更原则是新增参数必须用optional字段不要删除已有字段不要改变已有字段的语义。如果实在要破坏性变更就创建一个新技能名让旧技能保留一段时间再下线避免老会话里已经生成的历史tool_call在新代码里找不到对应技能。5.2 一套轻量级的技能评测集技能改没改坏不能只靠感觉。我维护了一个评测集里面放80条真实历史用户问题每条标注了期望调用的技能名、期望的参数组合。每次修改技能描述或参数Schema就用大模型模拟Agent完整跑一遍评测集然后对比实际选出的技能名和参数。统计三个指标技能选择准确率、参数解析正确率、任务成功率。举一个简化的统计表用户问题期望技能模型实际选择参数是否准确整体结果取消我刚下的订单订单号是ORD-001cancel_ordercancel_order是通过东西收到了但是不想要能退吗refund_orderrefund_order是通过我昨天买的手机查一下什么时候发query_logisticsquery_order否失败线上测完后再挑出失败的样本分析是因为描述不清晰还是参数说明不够迭代技能描述再跑一轮评测。这套循环让我的技能库在两个月内从91%的准确率升到了98.6%。5.3 对新手做技能库的几个复盘建议第一不要试图做一个包罗万象的大技能一个技能只解决一类用户目标宁可多拆几个技能也不要让一个技能承担太多职责。第二技能描述是代码的一部分它和业务代码一样需要测试和review。第三每次线上翻车都是打磨描述的机会把错误样本沉淀下来形成你自己的反例清单。第四控制技能总数当技能超过20个时模型的选择准确率会明显下降这时应该思考是不是粒度过细了可以合并部分同域技能。我还想分享一个一直在用的检查方法每次新增或修改技能我都会拿十个真实场景跑一遍看模型是不是能准确选中并正确完成。这个习惯帮我避免了很多线上问题也让技能库慢慢成为团队真正意义上的AI能力中台。