ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Agent技能体系设计与实践:从散装函数到可复用能力

Agent技能体系设计与实践:从散装函数到可复用能力 “agent-skills”这个词我第一次被它击中是在一个开源的Agent项目文档里。当时项目方把几十个工具函数一股脑塞进一个工具文件夹命名混乱到连作者本人都要翻半天才能找到某个功能我就意识到这件事不是“把函数换个目录”这么简单。真正把agent-skills当一回事去设计是在我自己做一个多轮对话Agent产品、被一堆裸函数和硬编码逻辑折磨到崩溃之后。所谓技能体系本质上是一套让AI Agent把“会做的事情”模块化、标准化、可复用、可评估的能力组织方式它不绑定某个具体框架而是一种实践方法直接解决的问题是Agent的能力边界在哪里、什么时候该调用什么能力、能力出错了如何快速定位。这篇内容适合三类人正在给Agent疯狂追加工具函数、但发现越来越难维护的开发者在做企业级Agent产品、需要多人协作管理一大堆能力模块的团队以及研究提示词工程和工具调用的算法同学。我会从为什么需要技能体系、到技能目录怎么设计、再到一个技能从零到能上线运行的完整路径最后讲维护和排查的坑尽量把这条链路讲透。1. 为什么Agent需要一套“技能体系”而不是一堆工具函数1.1 裸工具函数到底哪里让人崩溃先还原一下我在项目里看到的典型“裸工具”状态。你定义了一个get_weather(city)另一个同事写了个query_weather_by_city(city_name)还有一个遗留系统里躺着fetchWeather(cityId)。三个函数做的事情几乎一样但函数名、参数名、返回结构全不一样。大模型在规划时看到的是工具列表里的函数名和描述它根本不知道这三个函数是同一个能力更悲剧的是它可能会随机选一个。这带来的连锁反应很现实工具调用日志里出现大量“无效规划”模型今天选A明天选B行为不可复现出现bug时你得挨个排查三个函数的返回字段差异想做一个统一的缓存策略或者权限控制都没地方下手。说句不好听的这种状态下的工具集根本谈不上“能力”只是一堆散装函数。类比一下裸工具函数就像把一个五金工具箱里的螺丝刀全部混在一起没有标注规格也没有按用途分类。你需要一把十字螺丝刀的时候得把整箱倒出来翻。而技能体系做的事情就是给每个能力一个明确的“规格标签”并且把相近能力做归一化处理。1.2 技能的本质工具是“能做什么”技能是“承诺怎么做”我理解中工具函数只回答了“我能做什么”但技能要回答一组完整的问题这个能力在什么场景下该被使用、输入输出长什么样、失败时怎么处理、边界是什么。它更像一份“能力契约”而不是一段孤立代码。拿天气能力举例一个工具函数版本的描述可能是get(city) - dict一个技能版本的描述会变成名称current_weather_snapshot适用场景用户询问当前天气、出行建议、穿衣建议等场景输入城市名中文或拼音均可可选时间点输出结构化天气数据 一句给用户看的自然语言摘要失败模式城市不存在时返回明确错误码不抛出未捕获异常这带来的价值是大模型在做工具选择时不再是“猜”而是根据技能的“适用场景”字段做一次精准匹配。调用方也不再需要关心技能内部怎么实现只要按照契约传参和解析结果。技能内部是翻第三方API、查数据库、还是读本地缓存对外部完全透明。1.3 技能粒度怎么拆才合理技能粒度是这套体系里最容易翻车的设计点。拆太粗一个技能干太多事比如“处理订单”这种技能内部可能是分页、统计、修改状态、退款杂在一起模型调用时不知道具体触发哪个分支结果完全不可控拆太细技能数量爆炸一个场景要串五六个技能模型规划链路变长Token开销和出错率一起涨。我在实践中总结了一套“可再分性 复用频率”的衡量标准体感比拍脑袋定规则靠谱得多粒度判定标准示例使用频率预期原子技能单个业务行为不可再拆分查库存、查订单状态、发验证码高频组合技能固定顺序的多个原子技能串联下单前检查库存锁定库存中频工作流技能含有条件分支的完整业务流程售后退款全流程低频但复杂判断一个技能是否该独立出来我建议问三个问题这个行为会在多少个场景里被复用这个行为是否只有一个清晰的业务意图它的输入输出是否可以独立于其他技能定义三个问题答案都是“是”就值得拆成一个独立技能有一个“否”再往上一层考虑。2. 技能目录怎么设计从命名、结构到文档规范2.1 一个能跑起来的技能目录长什么样先给一个我目前比较满意的目录结构这个结构经历了三轮迭代现在基本稳定skills/ current_weather/ SKILL.md metadata.yaml main.py tests/ test_main.py test_snapshots.py examples/ case_01.json query_order/ SKILL.md metadata.yaml main.py tests/ test_main.py examples/ case_01.json每个技能一个独立目录目录名就是技能名里面必须有SKILL.md、metadata.yaml和主实现文件。SKILL.md是给大模型看的“操作手册”metadata.yaml是给框架看的“注册信息”主实现文件是给运行环境看的“执行代码”。这样三个角色各自看各自该看的东西互不干扰。有人会问为什么不直接用一个大tools.py把所有技能塞进去我的体会是当技能数量超过30个后单文件模式的协作冲突会急剧上升两个人同时改同一个文件合并代码都能耗掉半天。独立目录带来的隔离性短期看是多了一些文件长期看在多人协作时收益非常明显。2.2 SKILL.md 里的核心字段少一个都容易翻车SKILL.md是整个技能体系里最容易被低估的文件我见过太多人把它当成一个“简介”来写结果模型根本不知道怎么用这个技能。一个合格的SKILL.md至少要包含以下字段字段作用填写建议name技能唯一标识与目录名一致禁止重名description一句话说明能力写明“做什么”不要写“怎么做”when_to_use触发场景描述列出明确的业务场景越具体越好when_not_to_use负向排除防止模型在错误场景误调用input_schema输入参数定义字段类型、是否必填、枚举值范围output_schema返回结构定义顶层字段、嵌套结构、空值策略examples调用示例2-3个不同场景下的输入输出样例error_handling错误处理说明每种错误码的含义、调用方该怎么做description和when_to_use是最影响模型决策的两个字段。我见过一个写得极差的描述“此技能用于查询订单相关信息。”这个描述几乎没有任何区分度因为订单相关的技能可能有十个。后来改成“此技能用于查询订单当前状态、物流信息、支付状态适用于用户咨询‘我的订单到哪了’‘发货了没’‘什么时候到货’等场景。”效果立竿见影误调用率下降了一半以上。2.3 技能命名与版本管理这两件小事别忽略技能命名看似小事实际影响着模型的理解和团队协作效率。我推荐的命名规则是“动词 名词”结构比如query_order、send_email、convert_pdf。名字里不要带版本号不要带环境名不要用拼音缩写。convert_pdf_v2_final这种名字在第一版上线那天就注定是灾难。版本管理上每个技能目录内部维护自己的版本号用语义化版本规则主版本号.次版本号.修订号。主版本号在输入输出结构不兼容时升级次版本号在功能扩展但保持兼容时升级修订号只改内部实现和bug修复。metadata.yaml里记录版本号、作者、最后修改时间、依赖项这些信息在定位问题时能省大量时间。3. 从零写一个可复用的Agent技能以“仓库库存快照”为例3.1 场景设定与输入输出定义下面我用一个具体的技能“仓库库存快照”完整走一遍从设计到落地的过程这是我在一个电商客服Agent里实际做过的技能。业务背景是客服Agent被问到“这个商品还有货吗”“现在下单什么时候能发”需要在回复前先查库存。原先是直接读数据库后来发现库存表在高峰期有大量读写而客服Agent查询频率高直接查库经常超时另外返回一整张表的几十个字段模型反而不知道该怎么组织回复。于是我决定做成一个“快照式”技能底层维护一个定期刷新的库存快照缓存对外只暴露精简结果。输入参数定为商品SKU列表输出为每个SKU的库存状态、剩余数量、预估发货时长。定义输入输出时我优先考虑三个原则参数尽量少、类型尽量明确、输出带一个面向用户的话术摘要。3.2 实现代码从数据模型到主函数用pydantic定义输入输出结构是我目前体感最稳的方案类型校验、错误提示、序列化都省心from typing import List, Optional from pydantic import BaseModel, Field class InventoryInput(BaseModel): sku_list: List[str] Field( description需要查询库存的SKU列表最多支持20个, min_length1, max_length20 ) check_time: Optional[str] Field( defaultNone, description查询时间点格式YYYY-MM-DD HH:MM:SS默认当前时间 ) class SKUInventory(BaseModel): sku: str stock_status: str Field(descriptionin_stock/low_stock/out_of_stock) remaining: int Field(description剩余可售数量) estimated_delivery_days: int Field(description预估发货天数) class InventoryOutput(BaseModel): items: List[SKUInventory] summary: str Field(description给用户看的一句库存摘要) generated_at: str Field(description快照生成时间)主函数的核心逻辑是先从缓存读快照再标记已过期或缺失的SKU异步回源数据库刷新一次def run(sku_list: List[str]) - InventoryOutput: cache_hits [] miss_list [] for sku in sku_list: item snapshot_cache.get(sku) if item and not item.is_stale(): cache_hits.append(item) else: miss_list.append(sku) if miss_list: fresh_items query_and_rebuild(miss_list) cache_hits.extend(fresh_items) items [SKUInventory( skux.sku, stock_statusclassify_status(x.remaining), remainingx.remaining, estimated_delivery_daysestimate_delivery(x.remaining) ) for x in cache_hits] summary build_summary(items) return InventoryOutput(itemsitems, summarysummary, generated_atnow())这里有几个在真实业务里踩过的细节classify_status不能只看“有没有货”要结合安全库存水位判断estimate_delivery要考虑仓配区域build_summary要生成自然语言摘要而不是只给结构化数据否则模型每次都要自己“翻译”一遍数据既费Token又容易出错。3.3 技能注册与Agent调用链路实现完主函数后需要把技能注册到框架里。我用一个简单的注册表来管理避免各技能之间互相感知from typing import Dict, Callable skill_registry: Dict[str, Dict] {} def register_skill(name: str, description: str, when_to_use: str, handler: Callable, input_model, output_model): skill_registry[name] { name: name, description: description, when_to_use: when_to_use, handler: handler, input_model: input_model, output_model: output_model } register_skill( nameinventory_snapshot, description查询商品库存状态、剩余数量和预估发货时长, when_to_use用户询问是否有货、是否能发货、预计发货时间等场景, handlerrun, input_modelInventoryInput, output_modelInventoryOutput )在Agent运行时框架根据LLM生成的“技能名参数”去注册表里找handler执行后再把结构化结果拼回对话上下文。这里有个关键点技能的执行结果要“翻译”成适合放回上下文的格式我的做法是把summary字段直接拼进去同时附上结构化items供模型按需取用。4. 技能的测试、评估与迭代4.1 单元测试是底线不是可选项技能一旦被模型调用它的输出质量直接影响用户体验因此单元测试必须覆盖核心逻辑。对每个技能我要求至少写三组测试正常输入、边界输入、异常输入。正常输入用典型业务场景比如库存技能查询5个SKU边界输入要覆盖空列表、单个SKU、超过20个SKU异常输入要覆盖不存在的SKU、数据库超时、缓存为空等。断言不只验证返回结构还要验证summary里的关键信息是否与结构化数据一致比如remaining0时摘要里不能出现“现货”字样。测试框架用pytest再配合快照测试保护输出结构。快照测试的作用是当某次改动无意中改了返回字段名或类型时测试会直接失败逼着你审视这次改动是否合理。4.2 场景回放比十组手写测试更能暴露问题单元测试能保护“代码逻辑”但保护不了“模型调用技能的决策”。我后来建立了一套场景回放机制思路是把真实对话历史里“Agent成功调用技能”的样本收集起来形成回归集每次技能改动后自动跑一遍。具体做法从生产日志里抽200条包含技能调用的完整对话把用户原始提问、模型规划时的工具列表、最终选择的技能、传入参数、技能返回结果、用户反馈都存成JSON。回归脚本会重放这200条记录输出“技能选择一致性”和“参数生成有效性”两个指标。如果某次改动导致80%的用例选择了不同的技能那八成是SKILL.md里的描述被改坏了。场景回放还有个额外好处它帮你积累了一批高质量的“调用示例”这些示例可以反过来补充到SKILL.md的examples字段里形成正向循环。4.3 技能调优从“能跑”到“好用”的三板斧技能做完能跑只是第一步真正好用的技能要经过几轮调优。我的经验集中在三个方向。第一调描述。如果日志里模型经常在“该用技能A时偏偏选了技能B”先检查两个技能的when_to_use是否重叠过多再把高频场景词写进描述里。我见过最有效的一次调整是往描述里加了三个用户原话的例子误选率直接从30%降到8%。第二容错输入。模型生成的参数经常不按套路来日期可能是“明天”而不是具体日期城市名可能是“帝都”而不是“北京”。技能内部做一层宽松表达式解析把常见口语别名映射到标准值能极大减少“技能会写但不会用”的尴尬。第三控Token。SKILL.md会随系统提示一起发给模型它越长每次调用消耗的Token就越多。一个技能文档超过800字时我开始警觉优先精简描述性废话把关键触发词前置到开头200字内。实测下来在不影响调用准确率的前提下能省掉约20%的Token开销。5. 多人协作维护一套技能库的坑5.1 接口评审是维护技能库的“安全带”当技能数量多起来尤其是团队超过三个人之后接口评审就变得非常重要。一个常见事故是客服系统的人改库存技能时为了让管理后台也能用给InventoryOutput增加了一个warehouse_detail字段结果客服Agent的上下文被塞进一堆用不到的详情数据Token消耗飙升模型开始犯糊涂。我后来规定凡涉及输入输出结构变动、新增技能、删除技能、修改触发场景必须走一次接口评审。评审不看实现代码只看三样东西SKILL.md的when_to_use、输入输出schema、旧版本兼容方案。这个过程其实很快但能挡掉90%的隐性事故。5.2 文件管理、私有包、中心平台怎么选多人协作还牵涉到一个工具选型问题技能库到底存在哪、怎么分发。最简单的是“git仓库 文件目录 PR评审”适合10人以内的小团队每个技能一个目录串行评审合并。缺点是技能达到上百个之后检索和依赖管理会比较痛苦。中等规模团队我推荐“私有包管理”比如用内网的pip源或npm源发布每个技能为独立包调用方按版本安装可以在服务间复用技能但打包发布流程会增加一定维护成本。大型组织会倾向于建设中心化的技能管理平台统一存储、统一权限、可视化调试但搭建成本高团队规模不够大时容易变成负担。我的建议是团队人数在5人以下先别急着造平台git仓库 目录约定完全够用等技能量级上来了再迁移不要一上来就追求“平台化”。5.3 几个真实踩过的协作坑说几个我在实际协作中踩过的坑都是血泪教训。第一个坑是“技能目录名改动引发连环失败”。有个技能原来叫query_stock后来我觉得不够语义化改成了inventory_snapshot结果没同步更新历史会话里的技能调用记录导致场景回放时大量用例失败。现在所有技能改名都必须走评审并且要批量同步更新历史样本里的名字。第二个坑是“共享数据结构被悄悄修改”。两个技能都用OrderInfo结构有人觉得加个字段很安全结果另一个技能的测试全部挂了。后来我把公共schema抽出来单独管理并规定任何人改公共结构必须全量跑一遍所有技能的测试集。第三个坑是“文档和实现脱节”。有同事改了技能内部逻辑但忘了更新SKILL.md模型按照旧描述传参数接口不兼容直接报错。现在我在CI里加了一个检查如果SKILL.md和metadata.yaml的最后修改时间戳相差超过某段时间就触发提醒逼着作者同步更新文档。6. 常见故障与排查技巧6.1 技能在Agent运行时总是不被调用这是最让人头疼的问题之一技能明明存在测试也能跑通但模型就是不用它。排查思路首先看描述和触发场景把技能名和when_to_use打印在日志里对比模型实际生成了什么容易发现问题。更多时候问题出在“技能描述与其他技能太相似”模型在两个相似技能之间纠结最终选了错误的那一个。解决办法是在描述里增加负向排除比如when_not_to_use: 本技能不适用于售后改单场景改单请使用order_modify_skill。另一个常见原因是技能列表太长模型上下文窗口被塞满有些技能压根没被“看到”。这时需要做技能分片或摘要机制而不是一味堆技能。6.2 参数解析报错与模型“乱传参”模型传参不按input_schema来是技能上线初期的高频问题。常见表现是日期传了“后天”而不是具体日期枚举值传了“快点发货”而不是express/normal数组类型传了逗号分隔字符串。我的应对策略不是让模型变得严谨而是让技能变得宽容在runner层统一加一层参数清洗这层根据字段注释做类型转换和别名映射。比如日期字段支持natural_language_date()解析把“后天”转成具体日期枚举字段做模糊匹配容忍同义词。经过这层清洗后参数解析报错率下降非常明显。6.3 技能返回结果太复杂Agent反而看不懂有些技能返回的数据结构设计得极其“完备”嵌套四五层、字段几十个模型拿到后一脸懵不知道该用哪个字段组织回答最后给用户一段含糊其辞的话。解决办法是设计“双层输出”顶层是给模型直接使用的摘要和核心判断底层是给需要深度处理场景的详细数据。比如库存技能的summary是一句“该商品上海仓现货充足预计明天可发货”底层是各仓库存明细。模型大多数场景只用顶层摘要复杂度高的场景再取底层数据。这个设计从根本上减少了模型“读不懂输出”的问题。排查这类问题时我建议开起技能调用日志的“输出长度统计”如果某个技能的平均输出Token显著高于其他技能就优先考虑精简它的返回结构。技能体系不是一次搭建就一劳永逸的东西它更像是一个需要持续打理的花园。每次看到模型准确命中一个技能、顺利生成用户满意的回复时你会觉得前面那些设计、评审、测试的功夫都值了。如果你正在被一堆散装工具函数折磨不妨先从一个小技能开始尝试把SKILL.md写详细把输入输出结构钉死再慢慢铺开这套实践的收益会随着技能数量的增长越来越明显。
返回列表