
上个月我把手头几个Agent项目的公共能力全部抽出来统一整理成了一个叫agent-skills的技能库。所谓技能库就是把大模型从只会说话变成能干实事的那一层模型通过自然语言理解任务意图然后去调用预先定义好的技能函数完成搜索、计算、查库、改状态这类具体操作。做了一段时间后我发现Agent能力的好坏很大程度上取决于你给它配了多少高质量的技能以及这些技能被定义得有多清晰。这篇文章就把我做agent-skills过程中踩过的坑、总结出的方法、沉淀下来的规范一次性讲透。无论你是刚接触Agent开发的初学者还是已经在写多Agent系统的工程师只要想弄清楚技能到底怎么设计、怎么落地、怎么管理这篇文章都值得你花十分钟读完。1. 项目概述agent-skills到底在解决什么问题1.1 为什么Agent需要技能而不是提示词很多人刚开始做Agent时有个误区以为把提示词写长一点、写细一点模型就能自动完成复杂任务。确实LLM能根据提示词生成代码、写文案、做分析但你要让它真正操作外部系统——下单、查库存、调用内部API、读取数据库——光靠提示词是不够的。提示词本质上是文字指令它的天花板在于模型自身的知识边界和上下文长度。你可以在提示词里写请根据天气决定是否带伞但模型并不知道今天上海到底下不下雨因为它没有实时数据入口。技能则是把决策和执行分开模型负责决策——判断用户意图、选择合适技能、填入参数技能负责执行——真正去请求天气API、查询数据库、调用业务系统。这种分工让Agent不再是一个只会说的理论家而是一个能干事的小助手。我用一个生活化的类比提示词相当于你告诉一个实习生去把会议室订好但没给他公司系统账号、不知道订哪个时段、也不知道用什么工具。技能相当于你不仅告诉他目标还给了他一套订会议室操作手册上面写着账号、流程、备选方案。实习生只需要判断该用哪一页手册然后照做就行。1.2 agent-skills的总体设计思路整个agent-skills的设计逻辑可以概括成一句话把零散能力变成结构化、可复用、可组合的技能单元。所谓结构化是指每个技能都有统一的元信息名称、描述、参数Schema、执行函数、错误处理策略。这样无论是人类开发者还是LLM都能用同一套标准来理解技能。所谓可复用是指技能不绑定特定场景——写一个发送邮件的技能既可以用在客服机器人里也可以用在日报自动发送流程里。所谓可组合是指复杂任务可以拆成多个技能的串联、并联或分支执行。我把整个系统分成了三层技能层包含一个个独立的技能模块职责单一互不依赖。调度层负责让LLM理解用户请求并选择合适的技能相当于大脑。执行层真正运行技能函数、处理异常、返回结果相当于手脚。这三层的好处是隔离复杂度。技能层出问题改技能层不影响调度逻辑调度层效果差优化提示词和描述不用动技能代码。对团队协作也很友好算法工程师专注调度层后端工程师专注技能实现各干各的不用互相拖后腿。2. 技能拆解与定义规范2.1 技能命名的艺术与参数Schema设计技能命名这件事很多人觉得无所谓实际上它直接决定了LLM的调用准确率。我踩过的第一个坑就是把技能命名成抽象名词比如handleOrderprocessData。模型看到这种名字完全不知道这个技能是干什么的只能靠描述去猜猜错概率极高。后来我定了一条规矩技能名称必须采用动词业务对象的结构且动词要具体到不是一个万能动词。比如用fetchOrderStatus而不用getOrderInfogetinfo这类词太模糊用calculateShippingCost而不用calcPriceprice涵盖太广用summarizeMeetingMinutes而不用processDocument参数Schema方面原则是少而精类型明确尽量给默认值。每个技能参数不要超过5个因为参数越多LLM幻觉率越高。我举个例子。一个发送邮件的技能我刚开始设计了7个参数收件人、抄送、密送、主题、正文、附件列表、优先级。结果测试发现模型经常把抄送和密送搞混或者把优先级这个非必填参数脑补出奇怪的值。后来我精简到4个参数skill_schema { name: sendEmail, description: 向指定收件人发送一封文本邮件支持设置主题与优先级, parameters: { to: {type: string, required: True, description: 收件人邮箱地址}, subject: {type: string, required: True, description: 邮件主题}, body: {type: string, required: True, description: 邮件正文内容}, priority: {type: string, enum: [low, normal, high], default: normal, description: 邮件优先级} } }每个参数都要配上人话描述因为LLM不是按照代码逻辑读参数的它靠 natural language 理解。描述里最好带上可选范围比如收件人邮箱地址多个用逗号分隔这样模型才知道传什么。2.2 技能描述怎么写才能让LLM准确调用技能描述是整个定义规范里最被低估、但最重要的部分。LLM调错技能80%是因为技能描述写得含糊。我总结了一套模板每条技能描述必须包含三块信息触发场景什么时候该调用这个技能比如当用户询问订单配送进度时。输入输出说明参数含义、返回结果形态比如返回订单当前状态和物流轨迹列表。边界与禁忌什么情况不要调用比如仅适用于已付款订单退款订单请调用refundOrder。好的描述和烂的描述差距有多大我贴两个真实版本你一看就懂。烂版本查询订单状态。这个描述几乎没提供决策信息。用户说我的快递到哪了模型可能调用查询订单状态也可能调用查询物流轨迹因为它们听起来都沾边。好版本查询订单的当前处理状态。当用户询问订单进度发货了没物流到哪了时使用。参数orderId为用户订单编号。返回状态包含待支付、已支付、配送中、已完成。仅适用于交易系统内的电商订单。这个描述把适用场景、触发关键词、参数说明、返回格式一次性告诉模型。实测下来技能路由准确率能从65%提高到92%以上幅度非常明显。另外技能描述里不要写废话不要写这是一个非常实用的功能这种形容词LLM不会因此更倾向于调用它反而会稀释关键信息。3. 技能开发与落地的完整流程3.1 工具选型为什么是Python FastAPI 装饰器技能用什么语言、什么框架实现我在agent-skills项目里选了Python FastAPI 装饰器注册机制理由很实际。首先Python的生态最全无论是调数据库、发请求、做文件处理都有成熟的库。FastAPI能快速地把一个Python函数暴露成HTTP接口而且自带参数校验省去很多手工处理。但如果每个技能都单独写一个HTTP服务你会发现很快陷入服务爆炸——几十个技能就是几十个端口部署、监控都是灾难。所以我采用的方案是单进程服务 装饰器注册。把所有技能函数写在一个服务里启动时自动注册到技能注册表同时给调度层暴露统一的调用接口。# agent_skills/registry.py from fastapi import FastAPI from pydantic import BaseModel app FastAPI() SKILL_REGISTRY {} def register_skill(name, description, parameters_schema): def decorator(func): SKILL_REGISTRY[name] { name: name, description: description, parameters: parameters_schema, handler: func } return func return decorator class InvokeRequest(BaseModel): skill_name: str arguments: dict app.post(/invoke) def invoke_skill(req: InvokeRequest): skill SKILL_REGISTRY.get(req.skill_name) if not skill: return {error: fskill {req.skill_name} not found} try: result skill[handler](**req.arguments) return {success: True, result: result} except Exception as e: return {success: False, error: str(e)}每次新增一个技能只需要在函数上加上register_skill装饰器填好元信息剩下的注册逻辑完全不用管。这样耦合低新增技能时不需要改动调度层代码。3.2 从想法到可调用技能一个完整案例光说不练是假把式。我拿一个真实做过的技能工作日计算器来走一遍完整流程。背景是某业务系统需要计算从当前日期起n个工作日后是哪一天原本是后端硬编码后来要开放给Agent用于排期咨询。第一步拆需求。技能输入两个参数——起始日期、工作日天数。输出是一个结果日期。如果传入节假日列表则跳过节假日。需求看起来简单但有个细节如果不传节假日是否默认跳过周末我决定默认跳过周末节假日可选。第二步写技能函数。这里我用了workdays库配合自定义逻辑避免自己造轮子。from datetime import date, timedelta register_skill( namecalculateWorkingDays, description从指定日期起计算n个工作日后的日期。当用户询问某日期后n个工作日是什么时候时使用。, parameters_schema{ start_date: {type: string, required: True, description: 起始日期格式YYYY-MM-DD如2025-01-01}, days: {type: integer, required: True, description: 要计算的工作日天数可为正数或负数}, holidays: {type: array, items: {type: string}, required: False, description: 需要跳过的节假日日期列表格式YYYY-MM-DD} } ) def calculate_working_days(start_date: str, days: int, holidays: list None): current date.fromisoformat(start_date) holidays set(holidays or []) step 1 if days 0 else -1 remaining abs(days) while remaining 0: current timedelta(daysstep) # 跳过周末 if current.weekday() 5: continue # 跳过自定义节假日 if current.isoformat() in holidays: continue remaining - 1 return {result_date: current.isoformat(), working_days: days}第三步测试。自己先写几个用例周一算1个工作日应该是周二周五算1个工作日应该是下周一遇到节假日则顺延。这些用例不光要让技能函数通过还要在真实Agent场景里测试用不同的话术让LLM调用这个技能观察参数提取是否正确。这里我踩过一个很经典的坑days参数明明是整数但模型偶尔会传字符串3导致类型错误。后来我不仅依赖Pydantic的自动校验还在函数入口加了int(days)强制转换并在描述里写明days参数必须为整数。3.3 技能测试与评估指标技能测试不能只做函数能跑级别的单元测试还要做Agent层面的效果测试。单元测试验证的是参数传对了函数返回是否符合预期。Agent层测试验证的是给定一段用户问题模型是否能准确选对技能、填对参数。我设计了一套简单的评估流程每次技能改动后跑一遍路由准确率构造100条用户问题人工标注应该调用的技能名跑Agent后统计正确调用的比例。参数准确率对于正确调用的案例再检查参数是否有缺失或错误。比如用户说帮我算下从今天开始10个工作日后模型应该把start_date填成今天days填成10不能把days填成10个工作日。成功率技能函数实际执行成功的比例包含异常处理和降级逻辑。我自己习惯了用pytest写自动化用例同时跑上面三层指标。如果你的技能库很大可以给每个技能建一个独立测试目录不要让所有技能测试堆在一个文件里不然改一个技能要跑全部用例成本太高。4. 技能编排与管理实践4.1 复杂任务的技能组合策略单个技能能解决的问题有限真实业务里更多是组合拳。比如用户问帮我规划一下明天去杭州拜访客户查天气、预订高铁票、预约会议室这个请求至少涉及三个技能。如果让LLM一次性决定并执行很可能中途某一步出问题整个任务就断了。我用的组合策略主要有三种顺序链上个技能的输出作为下个技能的输入。比如先查天气再根据天气建议交通方式最后预订会议室。并行组多个技能互相独立同时执行。比如查天气和查高铁时刻表没有依赖关系可以一次性并行调用省时间。条件分支根据前一个技能的结果决定下一步。比如查天气返回明天有暴雨那就走建议改期流程而不是继续订票。为了实现这些组合调度层的返回结构需要设计成支持多步执行。我一版的做法是让LLM一次返回多个技能调用后来发现不可靠——一旦第一步就报错后面全是白费。第二版改成Plan-and-Execute模式LLM先生成一个技能执行计划plan然后按计划逐步执行每执行完一步将结果反馈给LLM再决定下一步。这种方式虽然多了一次LLM调用但稳定性和可诊断性都远好于一次性多技能并行调用。4.2 技能版本管理与热更新技能是有生命周期的需求一变技能逻辑就要改。我原来直接在技能函数内部改代码改几次之后发现新逻辑测试通过但线上还是老版本或者改坏了想回滚发现没有历史版本可回滚。后来我把技能版本管理做成了显式的每个技能注册时带上version字段部署时记录版本号到技能注册表。register_skill( namecalculateWorkingDays, version2.1.0, description..., ... ) def calculate_working_days(...): ...大版本x.0.0技能输入输出变化不兼容旧调用。小版本0.x.0新增可选参数或补充描述兼容旧调用。补丁版本0.0.x修复内部bug对外行为不变。线上部署时我做了个简单的灰度策略注册表里同时保留新老两个版本新版本先让10%的流量试跑观察成功率是否稳定再逐步扩大到全量。如果效果不好把流量切回老版本即可。技能热更新方面我用的是动态加载模块方式技能代码放在独立目录运行时通过 importlib 重新加载。这样不用重启主服务就能更新单个技能对线上Agent业务非常友好。但要注意热更新有潜在风险新代码异常不能被主服务吞掉。所以我每次热更新前都会先跑一遍该技能的单测用例再上灰度再全量。5. 常见问题与排查实录5.1 LLM选错技能怎么办这是我在agent-skills项目里遇到最多的问题。用户问我想取消订单模型总是调成查询订单状态因为两个技能描述里都有订单这个关键词。排查方法是先看调度层的日志确认模型实际看到了哪些技能以及它为什么选那个技能。然后对照技能描述找出混淆点。我总结了一个描述互斥法给容易混淆的技能的描述增加边界声明用不要来排除。比如cancelOrder的描述加一句仅用于用户明确表示要取消订单的场景。若用户仅询问订单进度请调用fetchOrderStatus。fetchOrderStatus的描述加一句仅用于查询状态不执行任何修改操作。若用户要求取消/修改订单请调用cancelOrder/updateOrder。这个方法听起来简单实际效果非常好。本质上是在描述层面做负向提示让模型在模糊匹配时能区分语义差异。5.2 技能执行超时与降级技能调用外部API最怕外部服务慢或者挂掉。我们有个查询物流信息的技能经常因为第三方物流接口响应超过5秒导致整个Agent对话卡顿。我的解决方案分三层超时控制在技能函数层给所有外部请求设置超时时间。Python requests库可以用timeout(connect, read)不要不设超时。推荐连接超时3秒读取超时5秒。降级策略超时后不要直接把错误抛给用户先尝试一个兜底逻辑。比如物流查询失败改为返回大致的物流公司官网查询链接并提示用户稍后再试。熔断机制如果一个技能连续失败超过阈值比如5分钟内失败20次就自动熔断调度层暂时不再路由到此技能5分钟后再恢复。这个机制避免了外部服务恶化时Agent反复用同一个技能失败白白浪费时间和Token。5.3 上下文污染与参数幻觉随着对话进行上下文越来越长技能调用的问题也随之出现模型开始脑补参数。最典型的现象是用户说帮我把会议改到明天模型调用更新会议技能时居然把meeting_id填了一个之前对话中从来没出现过的数字。这就是参数幻觉。排查后发现两个原因一是日志显示该会议ID其实是模型为了满足参数必填要求而随机生成的二是在历史记录里模型看到过类似结构的ID于是复制了。解决办法一是给参数描述补充说明来源写明从历史消息中提取若无法获取请反问用户二是在技能层加参数校验比如meeting_id必须是数字且存在才会执行这样可以拦截大部分幻觉参数。如果发现某个参数屡次被幻觉最彻底的办法是把该参数从技能入参去掉改为由技能函数内部查询获取上下文信息。另外一个上下文污染问题是技能返回结果太大塞进对话后挤占了其他信息。比如查询客户订单详情返回了上百条记录后续对话模型就被这些记录带偏。我建议技能返回前做结果裁剪只保留最关键字段细节数据放在响应中单独存起来供用户点详情时再查。6. 最后分享几个实战习惯做agent-skills这段时间我最大的体会是这个项目的难点从来不是写技能函数而是定义技能的元信息。一个技能写出来只要半小时但把它的描述、参数边界调到让LLM稳定命中可能要磨好几天。所以我现在每次新增技能都会给自己留足描述打磨的时间直接在开发排期里体现出来不再轻视这一步。另外我习惯每改一个技能描述就顺手把路由准确率测试跑一遍。因为描述改动的效果不直观可能你以为加了关键词是优化实际上反而让模型更容易混淆。用数据说话比凭感觉判断靠谱得多。最后说一个小技巧技能库的目录结构我会按照业务域来划分比如customer/、order/、calendar/、notification/每个目录里放同域技能的注册文件和测试用例。刚开始所有技能平铺在一起后来技能数量超过30个的时候平铺结构找起来、维护起来都很难受按域划分之后就清楚多了。这个经验对于准备把Agent技能做成长期资产的朋友应该会有参考价值。