1. 前置结论:我从零搭建了一套技能体系,先沉淀踩坑认知
两年前我第一次尝试给聊天机器人叠能力的时候,以为"会做某件事"就是往提示词里塞一段描述,让大模型自由发挥。结果上线第一周就被现实教育了:同样一句"帮我查物流",用户换个说法就解析失败;昨天还能准确调用的接口,今天上下文多了几轮闲聊,模型就开始一本正经编造数据。后来我意识到,真正缺的不是更长的提示词,而是一层结构化的能力封装——这就是 agent-skills 这个概念落到工程上要解决的事情。
我先给一个明确的前置结论,给急着抄作业的朋友:代理技能不是"提示词模板"的升级版,它是一套可复用、可组合、可校验的能力单元。一个完整的技能至少包含五个要素——标识与版本号、触发条件、输入参数契约、内部执行逻辑、失败兜底策略。我在项目里验证过的路径是:先拆原子技能,再组复合技能,调度层完全代码化,模型只负责意图识别和参数抽取。下面所有经验都来自实际跑过的流程,不涉及任何没验证过的花把势。
顺便说清楚适合谁来读这篇文章。如果你是正在把 LLM 应用从"聊天玩具"推向"能干活的生产工具"的开发者,这篇能帮你拆解怎么把模糊需求变成技能单元;如果你已经在用 LangChain、Semantic Kernel 或者自研框架做 Agent,这篇更多在讲技能库设计背后的取舍,而不是某个框架现成 API 的堆砌。
2. 为什么提示词塞不进"可靠执行":技能与提示词的本质边界
先把最容易混淆的一个概念掰开——技能和提示词到底差在哪。很多人觉得给模型写清楚"当用户询问运费时,请根据重量和目的地计算",这就是一个技能了。实际跑起来你会发现,模型收到"运费"两个字就开始自由发挥,它可能把体积也算进去,可能忘记你写了八遍的折扣规则,可能在费率表选择上自作聪明。提示词擅长的是"知道有这么回事",不擅长的是"每一步都按固定规则执行"。而技能的核心恰恰是后者:把确定的事情从模型的临场发挥里剥离出来。
我自己比较喜欢用一个比方:提示词像给实习生口头交代工作,你说了"注意安全"、"看清楚再填",但是不是真做到了全看对方状态;技能像给实习生一张带校验的操作单,每一步填什么、不满足什么条件就停、失败走哪个分支,全写死在流程里。大模型的强项是理解模糊意图、生成自然语言回复,弱项是严格遵守多步约束、执行精确计算、在长上下文里保持规则不漂移。技能体系就是扬长避短,让模型做调度员,让代码做执行者。
拆开看,提示词和技能在执行可靠性上有几个明显的差异维度。第一个是参数约束。提示词里你写"请根据用户输入提取城市",模型传回来一个"杭州"还是"Hangzhou"还是"浙江省杭州市",全看它心情;技能里有 JSON Schema 顶着,字段缺失、类型错误、格式不符,直接在校验层被拦下。第二个是执行路径。提示词方案里每个动作都是模型现场决策,今天心情好先查库存再查价格,明天可能就反过来;技能方案里你把查询顺序写死,模型进来只要输入、等结果。第三个是错误处理。提示词方案里,接口返回异常,模型可能顺着话头编一个"查到了,预计明天到"出来;技能方案里错误对象明确携带状态码和下一步动作,模型就算要安抚用户,也没有瞎编的空间。
这个边界想清楚之后,你自然会得出一个结论:能用技能封装的事情,尽量不要留给提示词临场发挥。那哪些事情适合封装成技能?我的筛选标准很简单——你确定要做什么、做法也基本确定的,就封装;用户意图本身模糊、需要多轮澄清判断的,才留给模型自由对话。前者比如查库存、算运费、创建工单、解析某个固定格式的文档;后者比如"帮我处理一下售后"这种需求,它本身不是一个动作,而是一串动作的编排。
实际做的时候还有一个反向注意点:不要把什么都封装成技能。有人会把"给用户发送欢迎语"这种纯模板输出也做成技能,结果技能数量爆炸,模型在工具列表里翻得眼花缭乱,调度准确率不升反降。我的经验是,纯文本生成、不需要外部依赖、规则固定到一句话就能说清的事情,留在提示词里就够了。技能只有在你需要它调用服务、操作数据、执行计算、承载状态的时候,才真正有价值。
3. 技能库设计:原子技能与复合技能的组合逻辑
打定主意要做技能体系之后,第一个绕不开的问题是技能库怎么组织。很多人上来就写一个 handle_refund 大函数,把查订单、算退款、发起退款全塞进去。结果用户只是问一句"我订单到哪了",你没法复用里面那段查订单的逻辑,只能新写一个查物流的技能,两段逻辑大量重复。等业务规则变了,你得同时改好几个地方,而且每改一次都可能引入不一致。
我后来用起来效果最好的组织方式,是先拆原子技能,再组复合技能。原子技能的粒度控制在"一个动作、一个输出",比如查订单 get_order_info、算退款金额 calc_refund_amount、发起退款 issue_refund、发通知 notify_user,每一个都是独立的、单一职责的、可以被其他场景复用的。复合技能则是"一个完整的用户意图",比如 handle_refund 就是把这几个原子技能按固定顺序编排起来。这样做的好处最直接体现在复用性上:get_order_info 能被查状态、查物流、售后处理好几个场景共用;calc_refund_amount 可以单独做单元测试;以后某个业务规则变了,只改一个技能,不影响其他调用链。
组合逻辑里有一个容易被低估的设计点:复合技能要显式声明子技能的调用顺序和终止条件,而不要让模型自由决定下一步。比如 handle_refund 的流程是"先查订单、再算金额、再发起退款",任何一个步骤失败就终止返回错误。这套顺序是业务规则,不需要模型临场推理。我在项目里倾向于把这类编排逻辑直接写死在代码层,不让模型参与决定,只在描述里留一个例外空间的说明——"如果订单状态已经是已退款,直接返回提示,不要重复发起退款"。代码层控制主流程,描述层处理少量例外,双管齐下最稳。
技能粒度怎么把握,这是团队讨论最多的问题。太粗了组合困难,太细则代理每做一个动作要跳多个环节,Token 开销和执行延迟双双上升。我给一个参考标准:原子技能的输入输出足够简单,人类能在一分钟内说清楚它"传什么、返回什么";复合技能对应一个完整业务诉求,用户一句话能描述清楚。比如"查订单"是原子技能,"处理退货"是复合技能。在真实项目里你可以先用这个标准拆,跑一段时间再看调用数据调整。
技能库里每个技能除了执行逻辑,还必须有完整的元数据。元数据不只是给文档用的,更是给代理的调度层和评估系统用的。我维护的元数据项主要包括四类:适用场景描述、限制条件、依赖关系、版本号。限制条件尤其是重中之重,比如 calc_refund_amount 要声明"仅适用于已完成支付且未超过退货期限的订单",get_order_info 要声明"只读不写"。调度层靠这些元数据判断当前输入是否匹配这个技能;评估系统靠这些元数据自动生成测试用例;版本号则在线上事故回滚时候救命,这个我后面细说。
输入输出 Schema 我建议直接用 JSON Schema,别自己发明格式。原因有三个:一是校验工具生态成熟,随手就能做运行时校验;二是主流模型的 function calling 接口基本都解析 JSON Schema;三是团队协作时 JSON Schema 的自描述性远好于注释。写 Schema 时有一条铁律:参数名要跟着业务语义走,别用缩写。模型是靠参数名猜字段语义的,你写 user_id 它知道填用户ID,写 uid 它就可能把订单号也填进来。这个坑我踩过,模型把 refund_id 填给 order_id 就是因为我两个字段名太像。
4. 技能注册与调度落地:从接口定义到运行时校验的完整管线
设计搞清楚了,接着聊落地实现。我先跑一个实际项目里的典型流程,把技能注册进代理,不管用哪个框架,本质都是三件事:定义接口、暴露给模型、运行时校验。下面是一个库存管理场景的简化例子,我用 Pydantic 写接口定义,因为可以直接生成 JSON Schema 给模型用。
from pydantic import BaseModel, Field class StockQuery(BaseModel): sku: str = Field(description="商品SKU编号,必填,例如 SKU-10086") warehouse: str | None = Field(default=None, description="仓库代码,可省略,例如 W-HZ-01") class StockLevel(BaseModel): sku: str warehouse: str available: int inbound: int = Field(description="在途入库数量,无则为0") class RestockOrder(BaseModel): sku: str quantity: int = Field(gt=0, le=1000, description="补货数量,1到1000之间") warehouse: str reason: str | None = Field(default=None, description="补货原因说明") async def get_stock_level(q: StockQuery) -> StockLevel: # 实际项目里这里查库存服务 return StockLevel(sku=q.sku, warehouse=q.warehouse or "W-HZ-01", available=42, inbound=17) async def create_restock_order(o: RestockOrder) -> dict: # 实际项目里这里写工单系统 return {"order_id": "RO-2025-0001", "sku": o.sku, "quantity": o.quantity, "status": "pending"}写这套定义的时候有几个细节,直接被线上事故教育过。字段 description 必须写业务边界,写"商品SKU编号,必填,例如 SKU-10086"和只写"SKU编号",给模型的信息量完全不同。枚举值直接写清楚,别让模型在头脑里翻答案,warehouse 如果不是开放输入就写死可选列表。数值字段带范围约束,quantity 限定 1 到 1000,能挡掉大量匪夷所思的幻觉参数。这些约束在开发阶段看来是"多写两行字",上了生产你会知道它们帮你挡了多少脏数据。
接口定义好之后,第二件事是暴露给模型。主流做法有两种:一种是把函数定义塞进工具列表,让模型走 function calling;另一种是把技能描述写进上下文,让模型输出结构化 JSON 再由解释器执行。我推荐前者,因为 function calling 自带输出约束,模型产生的工具参数会被强制对齐到函数签名,比自由输出 JSON 的幻觉空间小得多。框架通常会帮你把上面的 Pydantic 模型自动转换成工具定义,你真正要花心思的是写 tool description。
工具描述里有一个常被忽略的点:第一句话就写明"何时不该用"。模型做意图选择时,负向说明往往比正向说明更有效。比如 create_restock_order 的描述要写"仅当用户明确要求创建补货订单时使用。如果用户只是询问缺货情况,不要调用此技能,请使用 get_stock_level"。后期评估数据会告诉你,这一句话能把混淆场景的准确率拉高一大截。
第三件事是运行时校验。别信任模型一定传了合法参数,它经常会传错。技能入口必须加校验层,校验不过时返回可读的错误信息,而不是直接抛异常。校验通过之后,技能内部还要有防御逻辑:查不到数据、第三方接口超时、权限不足,都要返回结构化错误码。下面这段调度层的伪代码,说明我做校验与兜底的基本思路。
async def call_skill(model_decision): skill_name = model_decision["skill"] arguments = model_decision["parameters"] if skill_name not in skill_registry: return {"status": "error", "code": "SKILL_NOT_FOUND", "message": "未定义的技能"} skill = skill_registry[skill_name] try: validated = skill.input_schema.validate(arguments) except ValidationError as e: return {"status": "error", "code": "INVALID_PARAM", "detail": str(e)} try: result = await skill.execute(validated) return {"status": "ok", "result": result} except SkillExecutionError as e: return {"status": "error", "code": e.code, "message": e.message}调度层一定要和业务逻辑隔离。我项目里把调度层抽成独立模块,不管底层接 GPT、Claude 还是本地开源模型,调度逻辑都不改。这样每次模型厂商切换,代价只是工具生成的格式适配。这件事收益很大,调度层代码可以单独写单元测试,不依赖任何模型厂商的在线服务。
注册表设计还要提一点:别用魔法字符串硬编码。我用一个 registry 类,启动时扫描被 @skill 装饰器标记的函数,自动收集名字、描述、Schema 和 handler。装饰器把元数据写在实现旁边,技能和它的实现放一起,版本不会漂移。注册表还要记得维护 semantic_version 字段,调度层把技能版本写进每次调用的日志。线上出问题时没有版本号根本没法回滚,有了版本号才能做灰度流量对比。
5. 技能评估与边界约束:让代理知道什么不能做
技能库建起来了,但如果只靠"单技能测试通过"就敢上线,后面基本要吃大亏。技能本身执行正确,和代理在真实对话里调对了技能,是两码事。我遇到过单技能命中率接近满分、集成之后整段对话一塌糊涂的项目——用户问A,代理先去调了B;用户嘴上说"查库存",代理把补货单给创建了。技能评估不能只看单点命中,还要看调度准确率和边界敏感性。
测试集一定要覆盖三类场景,缺一不可。正向场景是用户输入明确指向某技能,代理必须调用它;混淆场景是用户输入表面像A技能、实际是B技能,代理不能被表面词汇带走;负向场景是用户输入与任何技能都不匹配,代理必须拒绝调用,回退到通用对话或追问澄清。负向场景最容易被忽略,但它恰恰最能体现技能系统的价值。我测试集里专门放了很多"看似能调用但实际不该调"的输入,比如"你们客服电话多少",它和售后技能沾边,但只该走通用问答,不该触发任何技能调用。
评估指标我分三层来看,每一层都有用。第一层是技能调用正确率,测试集里每个样本标注期望技能,跑完代理比对输出,算准确率和召回率。第二层是参数解析正确率,单独看传给技能的参数对不对,这个指标极其重要但经常被忽略。用户说"查一下杭州仓库的库存",代理调对了 get_stock_level,但参数里没有仓库字段,只有 SKU,结果就不对。第三层是交互回归表现,把一批真实对话回放给代理,看整段对话完成度,这能捕捉到多轮测试发现不了的意图漂移、调用顺序错乱、同一技能重复触发。
评估发现问题之后怎么改?我总结了一个规律:模型调度出错,十有八九是技能描述有歧义。描述不要堆功能点,要写清楚"做什么、不做什么、什么时候用、什么时候不用"。描述的质量和代码质量一样重要,它就是投喂给模型的接口文档。有个例子我记得很清楚,一个叫 get_latest_news 的技能,描述只写"获取最新资讯",用户在对话里问"你们公司最近有什么活动吗",代理就调了它,返回一堆驴唇不对马嘴的新闻。后来改成"获取站内公告和行业新闻资讯。仅当用户明确使用新闻、资讯、公告、最新消息等词时调用。涉及公司活动、优惠促销等话题,不要调用此技能,应使用 get_promotion_info",混淆场景准确率从 71% 升到了 94%。
边界约束这块还有一个权限维度的坑。技能是能力载体,但并不是所有技能都该对普通用户开放。创建补货单、发起退款这种写操作,和查库存这种读操作,在权限模型里必须严格分开。调度层收到写操作意图时,要检查会话身份,必要时让用户做一次明确的二次确认。这道防线放在技能层,比放在模型提示词里可靠得多。模型提示词随时会被系统升级覆盖,技能层的代码逻辑不会。
6. 生产环境里踩过的坑:五个案例与对应补救方案
最后这部分讲实战中踩过的坑。技能系统上线之后,问题不会出现在单元测试里,而是出现在真实流量的角落里。我按踩坑频率排序,把印象最深的五个事故和补救方案写出来,每一条都是拿线上数据换的。
第一个坑是上下文污染导致技能误触发。代理在长对话里,前面的内容会影响后面的工具调用判断。最典型的是用户先聊退货,后又聊别的,模型在话题早已切换之后仍坚持调用售后技能。排查下来有两个原因:一是技能描述的适用场景写得太宽,让模型认为旧话题仍相关;二是上下文窗口太长,早期对话的残留信号压过了当前意图。补救办法有两个,一是做意图快照——每次工具调用后重新编码最近一条用户消息、重新做意图判断,不让历史对话直接决定技能选择;二是给技能加运行时限制,比如"一次会话内最多触发次数"和"话题相关性阈值"。这两个限制把误触发率直接降了三分之一。
第二个坑是技能参数里的幻觉值。模型在参数解析上会凭空想象超出预期。有个案例让我印象很深:用户问"帮我查一下上周五的销售数据",代理调了 get_sales_report,参数 sales_date 传的是"2025-04-01",但当天是 4 月 18 日,"上周五"应该是 4 月 11 日。模型没有做日期计算,而是从上下文随便找了个像日期的值硬塞进去。这种问题靠校验层挡不住,因为格式和类型都合法。我的补救办法是,凡涉及日期、金额、数量这类需要计算的参数,不让模型直接填,而是让技能内部用解析器处理。定义技能参数时用 date_expr 这种表达式字段,技能内部调 date parser 解析,把计算责任从模型身上移走,准确性立刻上了一个台阶。
第三个坑是技能描述被系统提示词"覆盖"。模型能力升级或系统提示词模板调整后,技能描述里的优先级可能被静默稀释。比如你写了"查库存优先用 get_stock_level",但系统提示词里有一句"用户投诉时优先安抚情绪并推荐查询库存",模型就可能绕过技能层直接生成一段建议性回复。这种问题很阴险,它不会让系统突然崩掉,而是效果缓慢下滑,你很难定位到是哪次改动导致的。我的处理办法是把"技能优先"写进调度层硬代码:用户请求与技能触发条件匹配时,强制要求调用技能,并限制模型的自由文本输出。不要让"可调用也可自由发挥"两种选项同时存在,二选一才可控。
第四个坑是工具返回结果太长,挤爆上下文。技能执行后的原始结果往往包含大量无关字段,比如查库存返回了内部服务报文、带分页信息的完整列表。模型拿到这些数据后,Token 大头都耗在无关信息上,后续对话质量急剧下降。补救办法很粗暴但有效:所有技能输出都定义一个"给模型看的精简视图",只保留回答用户问题所必需的核心字段。查订单的模型视图只包含订单号、状态、商品名称、发货地址,其余一律丢弃或折叠。有了这层精简,推理速度变快,答案也更准,因为模型的注意力不会被无关信息带偏。
第五个坑是复合技能的错误处理没有传递进度。复合技能调了三个原子技能,中间某个失败,代理收到错误后不知道前面执行到哪一步,就可能编造成功消息回复用户。补救办法是所有技能错误都以结构化错误码返回,错误信息包含"当前进度"和"下一步可执行动作"。比如"退款创建失败,原因是支付系统超时,当前订单尚未扣款,请重试或联系人工处理"。有了明确的进度信息,模型即便要生成安抚话术,也不会凭空造一个"已退款"出来。
除了这五个高频坑,我再补一个通用建议:给所有技能调用加链路追踪。每条调用,无论成功失败,都要记录时间戳、参数快照、返回摘要、调度模型版本、技能版本。上生产之后这是一切排查的起点。我用最简单的结构化日志方案,trace_id 贯穿每次调用,配合查询面板按 trace_id 检索,成本不高,但线上事故复盘时能省掉几个小时手工翻日志。
说回团队协作层面,技能库不是一次性交付物,它需要持续运营。我现在的习惯是每周一次"技能体检",看每个技能的调用次数、失败率、平均耗时、参数校验失败率。调用量极低的技能,反思是不是描述引导不够;失败率高的,定位是内部逻辑问题还是参数解析问题;参数校验失败率高的,基本都能追查到是描述有歧义。把技能当成产品来运营,而不是当成代码来维护,这才是 agent-skills 能持续产生价值的关键。技能库建完不是终点,你要跟着真实交互不停调优——粒度、边界、描述、组合方式都在变。这个迭代节奏,才是这个方向真正考验人的地方。