
写一个通用型的“技能包”让原本只会聊天的大模型变成能干活、能查数、能操作外部系统的数字员工。这个方向业内叫Agent Skills核心思路是把复杂任务拆成一个个可命名、可描述、可调用的最小操作单元模型根据任务描述按需加载并执行。如果你正在做AI应用落地或者刚接触智能体开发就会发现目前最大的困境不是模型不够聪明而是模型的聪明没办法落地。模型能理解你的意图但你指望它自己调用数据库查询、处理Excel表格、发送HTTP请求基本是做梦。我做了几个真实业务项目后越来越确认一件事做Agent不是写提示词不是调模型参数而是做一套技能系统。这篇就按照我自己动手从零搭一套Agent技能系统的过程来写包含设计思路、核心数据结构、加载策略、以及实际跑项目时踩过的坑。1. 从需求到架构技能体系到底在解决什么先说我做这个技能系统的背景。某次接了个企业内部数据问答的活要求员工通过自然语言问销售数据、库存状况系统自动从数据库捞数并生成分析结果。一开始我用的是最朴素的做法——把数据库表结构、业务规则全塞进System Prompt让模型自己生成SQL去查。结果也猜得到提示词写到6000字模型一遇到复杂查询就开始胡说八道生成的SQL要么字段写错要么连表逻辑完全不对。对一次可以不可能每次都对。后来我换了个思路模型不需要自己会写SQL它只需要知道“有个技能叫销售数据查询传给它一个日期范围和区域它就能返回结果”。具体怎么连数据库、怎么SQL拼条件、怎么处理空值全封装在技能内部模型只负责解析用户意图然后把参数提取出来、调用对应技能。这就是技能系统的价值——你不需要逼模型什么都懂你只需要让它当一个聪明的“调度员”。模型的理解能力负责听懂人话技能系统负责把听懂的意图变成可靠的执行动作。插一段对比方便理解两种模式的差异维度裸调模型提示词硬编码技能化方案技能系统业务逻辑封装全堆在Prompt里封装在独立技能模块中模型幻觉影响直接生成错误SQL或参数只做意图识别执行由代码控制新增能力改Prompt风险大加一个技能文件零侵入可调试性靠对话日志猜技能调用可单测、可追踪多场景复用基本不可复用技能可跨Agent复用做过一次你就明白技能化这个方向对Agent工程化来说不是可选项是必选项。2. 技能系统设计先把主流程走通下面说说我设计的这套技能系统主流程分四步核心原则是“越简单越不容易出错”。技能清单加载用户意图路由参数提取与执行结果反馈与错误恢复其中第一步最不起眼但影响最大。一开始我图省事把所有技能的描述全塞进System Prompt结果上下文很快被塞满模型选择技能的准确率直线下降——这就像你给一个实习生发了300页的产品手册他反而找不到第5页那条关键规定。改法是用技能索引机制。每个技能在系统启动时注册为一个轻量描述条目包含编号、功能和关键词然后一次性注入上下文。模型通过编号引用技能而不是靠记忆冗长的完整描述。技能数量控制在25个以内时这种方案效果很稳。第二步意图路由我采用的是“让模型做主选规则兜底”的双轨结构。模型先根据用户输入选择最合适的技能编号如果置信度不足或编号非法就走规则匹配把用户输入切割成关键词块按关键词命中率决定走哪个技能。双轨的好处是既保留了大模型的语义理解弹性又用规则保底不至于在模型走神时全线崩盘。第三步参数提取是实操里最容易翻车的环节。技能定义里必须写明每个参数的中文别名和抽取规则比如“日期”参数要同时支持“最近一周”“7月1日到7月5日”这类自然语言表达。如果只给模型一个空的JSON参数结构它经常会漏抽、错抽。让模型以填空的方式去理解上下文效果会好得多。第四步错误恢复同样重要。技能执行不可能每次都成功数据库超时、接口返回异常、参数内容非法都需要有对应的失败分支。我的做法是错误信息会经过“翻译层”转成用户能理解的自然语言再回传给模型而不是把原始报错堆给用户看。3. 技能描述的数据结构这是最核心的设计如果你问我技能系统里哪一块投资回报率最高我会毫不犹豫说是技能描述的Schema设计。模型能不能准确调用技能七成靠技能描述写得好不好。记不太清楚有多少次就是因为技能描述写得含糊模型把该走A技能的请求路由到了B技能。先看我这边的技能描述Schema核心结构{ skills: [ { id: skills.orders.stats, name: 销售订单统计查询, description: 按时间范围、区域、品类汇总销售订单金额、订单量、客单价仅用于销售数据分析场景, trigger_words: [销售, 订单, 营收, 销售额, 业绩], parameters: [ { name: start_date, description: 统计开始日期格式YYYY-MM-DD支持相对日期描述, required: true, alias: [开始日期, 起始日期, 从] }, { name: end_date, description: 统计结束日期格式YYYY-MM-DD支持相对日期描述, required: true, alias: [结束日期, 截止日期, 到] }, { name: region, description: 区域过滤支持多个区域逗号分隔, required: false, alias: [区域, 地区, 城市] } ], output: 返回JSON包含total_amount、total_orders、customer_unit_price字段 } ] }字段设计有几个讲究。description要写清楚这个技能的边界避免“啥都能干”的错觉。比如“销售订单统计查询”就必须点明“仅用于销售数据分析场景”这能显著减少模型把不相干请求硬塞给技能的情况。trigger_words是给规则兜底用的不参与模型推理但对双轨路由来说必不可少。alias这个字段相当有用模型抽参数时看到“从6月到7月”能正确映射到start_date和end_date。另外会在每个技能上标注max_retry和timeout字段——这两个在后面做并发控制和防故障扩散时非常关键一开始不设计后面就得返工。这里补充一个容易忽视的点技能描述里的description不要写“这个技能可以帮你完成销售订单统计”。这句话对模型来说全是废话。要说“销售订单统计查询按条件汇总订单金额与订单量”干净利落一针见血。4. 技能执行器的实现把调用过程工程化说了设计这里放一段执行器的核心代码。这个执行器承接模型层和技能层负责调度、超时控制、错误捕获与日志记录。import asyncio import json import logging import time from typing import Any, Callable, Dict logger logging.getLogger(agent_skills) class SkillExecutor: def __init__(self): self._registry: Dict[str, Callable[..., Any]] {} self._timeout_map: Dict[str, float] {} def register(self, skill_id: str, handler: Callable, timeout: float 10.0): self._registry[skill_id] handler self._timeout_map[skill_id] timeout logger.info(fskill registered: {skill_id}, timeout{timeout}s) async def execute(self, skill_id: str, params: Dict[str, Any]) - Dict[str, Any]: if skill_id not in self._registry: return { status: error, error: fskill {skill_id} not found } handler self._registry[skill_id] timeout self._timeout_map.get(skill_id, 10.0) try: start time.time() result await asyncio.wait_for( handler(**params), timeouttimeout ) elapsed time.time() - start logger.info(fskill {skill_id} ok, elapsed{elapsed:.2f}s) return {status: success, result: result} except asyncio.TimeoutError: logger.error(fskill {skill_id} timeout after {timeout}s) return { status: timeout, error: f技能执行超时{timeout}s请缩小数据范围后重试 } except Exception as e: logger.exception(fskill {skill_id} failed: {str(e)}) return { status: error, error: f技能执行失败{str(e)} }关键在于用asyncio.wait_for强制超时控制。之前没有这层保护技能内部如果连了个慢数据库一个请求能把整个Agent卡死好几分钟用户早就流失了。加超时后慢查询能快速失败并反馈给模型让模型自行调整查询条件或换技能。注册机制这块我采用装饰器式注册读起来更清爽executor SkillExecutor() executor.register(skills.orders.stats, timeout8) async def order_stats(start_date: str, end_date: str, region: str ): # 内部实现拼SQL、查库、聚合计算 ...以这种形式注册技能技能函数的参数天然成为参数抽取的约束来源。函数定义的参数名、默认值、类型注解可以直接用于生成技能描述里的parameters结构不必两边手动维护从根上减少“函数签名和Schema不一致”的问题。5. 技能选择策略模型为主规则兜底实际的技能路由我用的是两层结构。第一层让模型自己做意图分类并在候选技能列表中选最佳匹配第二层是规则引擎兜底防止模型抽风或者没选出来。这样双层保险效果比只靠模型稳得多。模型选择那层我构建的System Prompt长这样你是技能调度器。根据用户的问题从下方技能列表中选择最匹配的一个只返回技能ID。 技能列表 - Id: skills.orders.stats, 名称: 销售订单统计查询, 用法: 按时间/区域/品类汇总订单金额与订单量 - Id: skills.inventory.alert, 名称: 库存预警查询, 用法: 查询SKU库存低于安全水位的情况 - Id: skills.customer.portrait, 名称: 客户画像分析, 用法: 分析与某客户关联的交易行为偏好 规则 1. 如果用户询问销售、营收、订单金额优先选 skills.orders.stats。 2. 如果用户询问缺货、补货、库存预警优先选 skills.inventory.alert。 3. 如果问题不匹配任何技能返回 no_skill。 4. 只返回技能ID不要返回任何解释。规则引擎那层则很简单用trigger_words做关键词打分命中分数最高的技能胜出。两者优先级上模型选在前规则兜底在后模型出结果但规则判定风险高就信规则的。模型选错的典型案例是用户问“对比这两个区域哪个卖得好”。模型一眼看到“对比”可能直接跳到一个叫“数据对比工具”的技能完全忽略了用户讨论的对象是销售订单。叠了规则层之后关键词“销售”“区域”“订单”命中销售统计技能就把这个偏离拽回来了。实际操作中我在路由日志里打印过一批数据发现模型选错技能的情况里有将近一半是把“泛指查询”和“特定业务技能”搞混。这个问题的解法就是在技能描述里把适用范围写窄,越窄越不容易误触。6. 技能的参数抽取与格式化技能选对了参数抽错了照样白搭。我用一个二次抽取策略来解决模型先抽原始参数JSON再写一个校验函数做格式归一。比如把“最近一周”这种相对时间翻译成具体的start_date和end_date。内核对日期表达式的翻译思路是这样的from datetime import datetime, timedelta def normalize_date(text: str) - str: 将自然语言日期转为YYYY-MM-DD格式 text text.strip() if text.endswith(天) and 最近 in text or text.endswith(天) and 过去 in text: n int(text.replace(最近, ).replace(过去, ).replace(天, )) return (datetime.now() - timedelta(daysn)).strftime(%Y-%m-%d) if text.endswith(周) and (最近 in text or 过去 in text): n int(text.replace(最近, ).replace(过去, ).replace(周, )) return (datetime.now() - timedelta(weeksn)).strftime(%Y-%m-%d) # 其他复杂表达直接交给模型解析后校验 return text这个翻译逻辑不追求全够用就成。真正复杂的时间语义还是靠模型处理规则只负责那些稳定可枚举的模式。参数抽取出错场景里最典型的是“多值参数被抽成字符串”。比如region字段支持多区域用户说“华东和华南”模型抽参数时可能把值抽成华东和华南导致技能执行时报区域不存在。我后来在Schema里加了parameter_type: array的标识并在抽取后将字符串按分隔符拆分成数组再配合枚举值校验才把这个问题的发生率降下来。参数校验函数我放在技能内部每个技能自己管自己。这样比统一校验器灵活也符合单一职责原则。7. 技能编排让多个技能协同完成复杂任务单独一个技能只能解决单一问题真实业务里动不动就要两个以上技能串起来跑。比如“这个月华东地区销售下降帮我查一下是不是库存出了问题顺便看看这个区域重点客户的近期采购行为”。这个请求涉及三个技能销售统计、库存预警、客户画像。正确的流程是先查销售数据确认“下降”程度再看库存判断“缺货”是否是原因最后落到客户行为上。技能编排这块我的方案分两条路走。线性串联方式靠模型的ReAct式推理在上下文中不断追加技能执行结果让模型决定下一步调用什么技能。代码层面对模型返回做循环解析识别出skill_call指令循环执行直到模型给出完整结论。用户请求 - 模型选择技能A - 执行A - 结果回填上下文 - 模型再选技能B - 执行B - ... - 模型汇总最终结果这种方式的优点是灵活适合场景不固定、技能组合路径多的业务缺点是Token消耗大、延迟逐轮累积。对于交互时延敏感的场景要谨慎用超过三轮技能串联用户就会明显觉得慢。预设工作流方式把固定流程硬编码比如“销售数据异常分析”固定三步查销售汇总→查库存水位→查客户动向。流程执行器直接按顺序调用中间不加模型推理延迟低且可控。两种方式前一种适合探索性、组合多变的业务后一种适合业务路径已经跑熟、固定下来的高频场景。我现在的倾向是能预设就预设探索性的场景才让模型动态编排。8. 关键注意事项与踩坑总结说了这么多思路下面这部分是最值钱的。这些坑都是实际跑出来的不发出来可惜。坑一技能数量贪多上下文塞爆。一开始我注册了40多个技能全量注入Prompt结果模型选技能的准确率肉眼可见地下降。后来做了技能分组按需注入根据用户画像、当前对话意图只动态注入相关分组的技能描述。比如做数据分析的会话就只注入数据查询类技能做客服的会话注入工单查询类技能。技能全量放索引区分组放在详细区。这个优化让技能选择准确率从81%升到93%左右。坑二同一技能并发请求打爆后端。用户涌进来时如果20个人同时触发销售统计数据库连接池直接爆掉。解决思路是给执行器加信号量并发控制限制同一技能最大并行数超出部分排队等待并调短超时时间快速失败。坑三技能错误信息直接裸露给用户。有一次技能抛了个数据库字段冲突的原始异常模型把这段异常原样复述给用户。这既不专业也容易暴露内部结构。正确做法是技能内部捕获异常后统一转成业务语义错误比如“当前数据范围过大请缩小时间跨度重试”并让模型基于这个业务错误组织话术。坑四模型自己“发明”技能。有次模型没匹配到技能竟然自己在返回里编了一个skills.orders.delete然后假装执行成功。这其实是对模型指令约束不足导致的。解法是在系统提示里写明“仅可使用列表中的技能禁止自创技能ID”收到非法技能ID时要直接短路处理一律返回no_skill。坑五技能描述互相包含路由冲突。比如有个“订单查询”技能还有个“订单退款查询”技能。用户问“查一下退款进度”模型经常被“订单”这个词带偏到前者。后来我重新梳理了技能边界描述把相似技能改成层级关系在description里显式提示“如需查询退款请使用skills.orders.refund.status”冲突就明显减少了。9. 常见问题速查与排查技巧实录平时维护这套技能系统最常被问的几类问题我整理成一张速查表现象可能原因排查思路解决方案模型选错技能技能描述边界模糊、触发词重叠看路由日志里候选技能排序收敛技能描述增加Skills边界对比参数漏抽或抽错Schema缺少别名和格式约束打印原始抽取JSON比对用户原句补alias、补枚举校验、加二次解析技能执行超时后端查询慢、并发堆积查看技能执行日志耗时分布加超时控制加并行信号量优化SQL错误信息被原样暴露异常未分类捕获看错误信息是否含SQL或堆栈统一异常翻译层敏感信息脱敏模型自创技能IDSystem Prompt约束不足查看模型输出是否在技能清单之外强化ID白名单校验非法ID短路全局上下文被撑爆每轮都注入全部技能描述查看Token消耗趋势技能分组、按需注入、索引与详情分离排查时我习惯先把路由日志完整打出来包括用户原话、候选技能排序、最终选择的技能ID、抽取出的参数JSON、技能返回的状态码。这套日志是不需要猜的“铁证”。日志结构长这样{ user_query: 这个月华东区销售情况怎么样, candidate_skills: [skills.orders.stats, skills.orders.detail, skills.customer.portrait], selected_skill: skills.orders.stats, confidence: 0.87, extracted_params: {start_date: 2025-11-01, end_date: 2025-11-30, region: 华东}, execute_status: success, elapsed_ms: 423 }日志里confidence字段有什么用我设了个阈值低于0.75时会把这次请求标成“低置信度”后续可以拿着这些样本去迭代技能描述比漫无目的地改Prompt高效得多。10. 技能系统的通用扩展方向做到这一步很多朋友会问那这个技能系统是不是就只能用在内部数据问答当然不是。这套骨架可以把任意外部能力包成技能比如发邮件、创建工单、导报表、查天气、调推荐算法接口。每个技能是独立的、可测试的模块像搭乐高一样往Agent上拼。按我个人的实操体会后续值得投入的方向有这么几个。第一是技能自动生成。现在注册技能还得手写描述、手写Schema、手写参数校验函数比较繁琐。我在尝试让模型阅读一段工具代码后自动生成技能描述再经过人审入库。这能大幅降低扩展新技能的边际成本。第二是技能推荐。根据用户的历史对话和常用技能给新会话推荐默认技能集。这一步能把技能选中准确率再往上拔因为注入的候选列表已经从40个缩小到58个模型选择压力大幅下降。第三是技能执行反馈闭环。把技能执行成功率和用户反馈数据拉通定期淘汰低质量技能、修正歧义描述。这属于纯工程化管理但长期做下来积累的收益相当可观。最终回到那个核心认知Agent的上限取决于技能边界而不是模型本身。模型负责理解和表达技能负责稳定和可靠这两者结合才能真正把大模型从“聊天玩具”变成“业务工具”。如果你正在做Agent应用希望这篇对你构建自己的技能体系有那么一点参考价值。