ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent技能拆分实战:从混乱提示词到可复用技能库

AI Agent技能拆分实战:从混乱提示词到可复用技能库 上个月帮客户调一个销售分析Agent遇到一个特别典型的故障用户问“上周华东大区的销售额是多少”Agent没有去查数据反而洋洋洒洒输出了一篇《如何提升销售效率》的演讲稿。翻日志才发现团队把所有业务逻辑全塞在了一个将近两万字的system prompt里模型在浩如烟海的上下文里彻底迷失了方向。后来我们花了三天时间把二十多项业务能力拆成了九个独立技能同一个问题再问一遍稳稳返回了正确的数字。这篇文章就想把我在这类项目里的完整经验讲清楚为什么要把Agent的能力拆成技能技能的接口契约应该怎么定怎么手写一个能落地的技能多个技能之间如何编排协同以及我踩过的那些用真金白银换回来的坑。内容主要面向正在做AI Agent应用、想让大模型真正接上业务能力的朋友也适合想把零散脚本沉淀成可复用技能库的开发者。1. 从一次翻车说起为什么什么都能干的Agent反而什么都干不好1.1 那次让我决定重构的对话事故先还原一下当时的现象。整个系统是一个企业内部知识库加数据查询的Agent用户会问销售数据、库存水位、客户投诉情况也会问一些制度流程问题。最初的实现方式很粗暴把所有工具函数一股脑列在提示词里每个函数写一段长描述再附上各种业务规则、口径说明、示例对话期望模型自己“悟”出该调用哪个函数。结果就是文章开头那幕。模型在那个场景里选择了“发挥”而不是“查询”。为什么因为提示词里信息熵太高了——销售分析、制度问答、演讲稿生成、邮件起草几十种能力混在一起每个能力都有一大段说明模型在做工具选择时相当于在做一道超多分类任务。任务类别越多、每个类别描述越长分类准确率掉得越快。这跟人一样给你一本三千页的操作手册让你现场回答“现在该翻哪一页”你也容易懵。那次之后我把架构整体改成了技能化模式。所谓技能就是一个具备标准输入输出契约、可独立注册、可被模型按需调用的功能单元。每个技能只做一件事描述写得像“API文档的精确摘要”而不是“散文”。改造完成后同一个Agent在两百多次测试里的工具选择准确率从不到七成提升到了九成五以上。1.2 Agent技能和普通函数到底有什么区别很多同学会问这不就是函数调用吗我直接把函数扔给模型不也一样这里有一个核心差异普通函数是给程序员调用的调用者是确定的Agent技能是给模型“选择”的调用者是一个概率系统。这意味着你的技能设计首先要服务的是“模型的判断力”而不是“函数的接口优雅度”。具体来说有四点明显区别普通函数靠函数名和注释文档说明用途Agent技能靠description字段帮模型做意图匹配description写得好不好直接决定模型会不会选错。普通函数的参数是程序员按文档传的Agent技能的参数是大模型从对话上下文里抽取的所以参数名、参数类型、枚举值都必须极其明确最好让模型“没法猜错”。普通函数的异常处理是给上层代码看的Agent技能的异常会回到对话里模型需要基于错误信息决定下一步行动所以错误码和错误消息要能“被模型阅读并理解”。普通函数可以任意嵌套调用Agent技能的输出常常会成为另一个技能或模型的输入所以返回格式必须结构化、稳定不能依赖一段自然语言让人去解析。简单记一句话函数是写给代码看的技能是写给模型看的。设计技能时你的用户不仅是使用这个Agent的人也是那个帮你路由函数调用的模型。1.3 技能化之后解决了我最头疼的三个问题第一是模型选错工具的频率显著降低。以前一个工具列表里放四十多个函数描述模型经常张冠李戴。拆成技能后常用技能不到十个描述精确模型选择就像从十张卡片里挑一张难度完全不是一个量级。第二是调试效率完全不一样了。以前出问题要在巨大的prompt里排查是业务规则写错了还是上下文污染了现在每个技能可以单独测试、单独打日志哪个环节出问题一目了然。技能跑挂了把那段技能的输入输出拉出来看就行。第三是复用性上了一个台阶。以前新接一个业务场景要重新设计一大段prompt现在是从技能库里挑两三个技能做组合再补一小段流程说明就完成了。说句实话技能化以后我的交付速度至少快了一倍。2. 技能的最小骨架先把接口契约定明白2.1 一张技能描述卡片应该包含什么我给团队定的标准是每个技能对应一张“描述卡片”用于注册到技能库供模型感知。卡片就五个部分多了不要少了不行技能名称必须有语义且唯一比如query_sales_summary、generate_comparison_chart不要用func1、tool_001这种。名称本身是模型做初步筛选的最强信号好的名称相当于给模型划了重点。description的要求最苛刻两到三句话第一句直接说明“什么情况下调用本技能”第二句说明“本技能不处理什么”明确排除边界第三句在必要时给出一个用户问法的示例。比如这样写当用户询问销售额、订单量、客单价、销售环比同比等数据口径时调用。不处理利润、成本、库存相关查询。例如“上月华东区销售额多少”。这种写法能把许多边缘情况直接挡在外面。input_schema是模型抽取参数的地图。每个字段都要有类型、是否必填、取值范围、示例值。比如日期字段就明确写成YYYY-MM-DD格式千万别写“一个日期”这种模糊描述。字段数量控制在5个以内超过5个就说明这个技能粒度太粗得拆。返回结构应该固定为JSON包含status、data、error_code、error_message四个顶层字段。业务数据全放data里错误统一放error_code。后面章节还会展开讲为什么不能返回自然语言。sample_call是一个参数填好的调用示例比如{start_date: 2025-01-01, end_date: 2025-01-31, region: 华东}。这个示例不仅帮助模型理解参数格式也是我写自动化测试的重要基线。2.2 输入输出的Schema设计思路Schema设计是整个技能体系里最容易被低估的环节。很多团队一开始觉得这不过就是定义几个参数结果上线之后模型频频抽错参数——把region传成“华东大区”把日期传成“上个月”Schema里明明写了格式模型就是视而不见。我的实践经验是Schema要让模型“没机会犯错”而不是“有机会做对”。怎么做有几个小技巧非常管用字段名直接使用业务通用词汇不要用内部缩写。用户说“华东”你的字段就叫region用户说“上个月”你千万别设计一个叫last_30d的字段。枚举值显式列全。如果区域只有华东、华南、华北、西南就把这四个写在enum里模型在有限选项里选准确率远高于让它自由填写。日期类参数不要只给格式还可以明确写出“用户说‘上周’时请换算为具体的起止日期后再传入”。在description里加一句“如果你不确定参数值请向用户发起追问”这能在很大程度上避免模型拿空值硬调用。返回结构同样有讲究。data字段内部尽量使用扁平结构避免多层嵌套。模型读取一条技能结果时嵌套越深越容易出现理解偏差。我之前设计过一个返回结果里套了三层数组模型在后续总结时经常漏掉内层信息。改成扁平结构以后这个问题基本消失。2.3 技能注册与加载机制技能写完之后要有一个统一的加载机制把它们暴露给大模型。这个机制我建议自建一个轻量注册表而不是直接堆在系统提示词里。注册表的核心是三个动作登记、索引、加载。登记阶段读取技能目录下每个技能的描述卡片把名称、描述、Schema汇总成一份“技能清单”索引阶段按业务域给技能打标签比如销售域、库存域、制度域加载阶段根据对话上下文动态挑选相关技能只把候选技能的描述注入到提示词里。动态加载这一步特别关键。假设技能库积累到五十个技能如果全部描述都塞进提示词你就又回到了“信息过载”的老路上。动态挑选的规则并不复杂可以用一个轻量embedding模型对用户问题做向量化和技能描述做相似度排序取Top K注入。K一般取5到8既保证覆盖面又不至于淹没模型。注册表的具体实现可以先极简起步一个目录每个技能一个文件夹里面放skill.yaml描述卡片和实现文件。用一个Python脚本扫描目录生成索引交给Agent运行时加载。等到技能数量超过三十个再考虑引入数据库或向量库也不迟。3. 手写一个真实技能从任务拆解到可交付3.1 技能的目标设定与拆解原则接下来用一个销售数据查询技能当例子把这套方法从头到尾走一遍。先定目标用户用一句自然语言查询销售汇总数据技能返回结构化结果模型再基于结果组织回答。这个技能要处理几个关键分支分支一是基础查询比如“华东区上月销售额”要能做时间区间过滤和区域过滤分支二是聚合口径切换用户说“按月看趋势”返回就要带月份维度分支三是数据为空比如查了一个没有数据的时间段技能必须返回明确的空结果标识而不是一串让模型自行脑补的字符串。拆解原则是一个技能只覆盖一个业务动作再加两个紧邻的边缘动作。这里的主动作是“查询销售汇总数据”边缘动作是“切换聚合时间粒度”和“处理空结果”。如果用户还要做销售额和去年同期的对比那属于另一个技能compare_sales_period的职责不要揉进来。3.2 代码实现从参数校验到结果格式化技能的Python实现我推荐用类的方式组织每个技能继承一个BaseSkill基类基类负责公共逻辑。下面这段代码是一个简化但完整的示例class BaseSkill: name version 1.0.0 description input_schema {} async def validate(self, params: dict) - dict: # 参数校验的统一入口 errors [] for field, spec in self.input_schema.items(): if spec.get(required) and field not in params: errors.append(fmissing required field: {field}) if field in params and enum in spec: if params[field] not in spec[enum]: errors.append(finvalid value for {field}: {params[field]}) if errors: raise SkillInputError(; .join(errors)) return params async def run(self, params: dict) - dict: raise NotImplementedError async def execute(self, params: dict) - dict: try: await self.validate(params) result await self.run(params) return {status: success, data: result, error_code: , error_message: } except SkillBusinessError as e: return {status: failed, data: None, error_code: BIZ_ERROR, error_message: str(e)} except SkillDatabaseError as e: return {status: failed, data: None, error_code: DB_ERROR, error_message: str(e)} except Exception as e: return {status: failed, data: None, error_code: UNKNOWN_ERROR, error_message: str(e)}业务技能这样写register_skill class SalesQuerySkill(BaseSkill): name query_sales_summary version 1.2.0 description ( 当用户询问销售额、订单量、客单价、销售数据时调用。 不处理利润、成本、库存相关查询。 例如上月华东区销售额多少。 ) input_schema { start_date: {type: string, required: True, format: YYYY-MM-DD}, end_date: {type: string, required: True, format: YYYY-MM-DD}, region: {type: string, enum: [华东, 华南, 华北, 西南, 全国]}, product_line: {type: string}, aggregate: {type: string, enum: [total, daily, weekly, monthly], default: total} } async def run(self, params: dict) - dict: start params[start_date] end params[end_date] region params.get(region, 全国) product params.get(product_line, ) aggregate params.get(aggregate, total) sql build_sales_query(start, end, region, product, aggregate) try: df await query_dws(sql) except DatabaseTimeout: raise SkillDatabaseError(销售数据查询超时请缩小时间范围后重试) if df.empty: raise SkillBusinessError(该条件下没有销售数据请确认查询条件) summary sales_summary_from_df(df, aggregate) return { query_condition: { start_date: start, end_date: end, region: region, product_line: product, aggregate: aggregate }, summary: summary }这段代码里需要注意两个细节。第一业务错误和数据库错误分开捕获这样当Agent收到BIZ_ERROR和DB_ERROR时可以采取不同的策略——空数据就如实告诉用户数据库超时则可以建议用户缩小范围重试。第二返回里包含了query_condition把这次查询实际使用的条件回显给模型。这个字段能有效防止模型在总结时把条件说错比如用户问“华东”技能实际按“全国”查了模型照实说有误时query_condition就是澄清依据。3.3 如何测试一个技能的行为边界技能测试跟普通单元测试很不一样。普通测试关注“正确输入下输出是否正确”技能测试更要关注“模型可能给出的各种畸形输入下技能会不会优雅失败”。我常用的技能测试清单包含以下分支参数完全正确时返回是否完整、稳定缺少必填参数时是否能返回明确的缺失提示枚举值传错时比如region传了个“北方”是否有清晰报错日期范围过大且数据库超时错误类型是否是DB_ERROR查询结果为空是否准确返回空结果业务错误同一个技能并发调用时是否存在共享状态污染。这些测试用Pytest把BaseSkill的示例Schema和真实技能实现一起跑就行。每次技能版本更新先把这些测试跑绿了再上线。这条纪律我在项目里是死命令很多线上事故其实就是因为某个技能改了一行SQL没回归测试导致的。3.4 把技能交付给Agent动态加载与调用链路技能注册完成后Agent运行时的调用链路是这样的用户提问进入会话先由意图路由层计算问题与所有技能描述的相关度选Top K技能注入提示词。模型在推理时看到的是精简过的技能说明决定调用哪个技能并填好参数。参数到达技能执行层经过校验、查询、格式化后返回结构化结果。结果回到模型模型基于data内容生成自然语言答复。这条链路里有一个特别值得强调的点技能执行层只负责返回数据永远不要返回“成品文案”。我见过很多团队让技能内部直接拼好“华东区1月销售额为100万元环比增长5%”这种句子看似省事实则麻烦——一旦用户追问“那环比增长的原因是什么”模型无法从这句话里拆出结构化数据去做进一步分析。技能永远只返回{amount: 1000000, growth_rate: 0.05}这类原始信息把表述的工作交给模型。4. 多个技能协同编排层的设计误区与正确姿势4.1 第一个误区让一个技能干所有事技能体系变大之后下一个问题自然出现多个技能怎么配合。最常见的错误做法是贪图省事把一个复杂任务的所有步骤封装进一个技能。比如“生成销售周报”这个技能内部集成了查询数据、计算环比、生成图表、排版推送四个步骤。听起来很合理但真正上线后就发现问题用户只是想“看看本周销售数据”也被迫走了整个周报流程响应慢、费用高、还容易在某一步失败时整体崩溃。正确的做法恰恰相反把周报拆成四个独立技能——查询数据、计算同期对比、生成图表、组装文本。每个技能可以被单独召唤也可以在编排层组合。用户问“本周数据”只触发第一个技能用户说“生成周报”编排层按顺序触发四个技能。拆分的标准很简单任何一步的输出如果可能被其他场景复用就值得独立成技能。数据查询结果可以复用于指标卡、周报、异常分析图表生成可以复用于周报、汇报PPT、对外战报。独立之后每种场景都是在不同位置复用同一批底层能力而不是重复造轮子。4.2 技能间的数据传递与上下文保持技能协同的另一个关键技术点是数据传递。每个技能是无状态的执行完把结果返回就结束了。编排层要负责把上一个技能的结果塞给下一个技能作为输入的一部分。这里有一个我研究很久的细节用一个结构化的workspace在技能之间传递数据而不是靠对话自然语言。什么是workspace可以理解为一个轻量的数据暂存区。编排层维护一个字典每个技能执行后把关键产出放进去下一个技能按需读取。比如卖报场景第一步生成销售汇总写入workspace[sales]第二步读取sales算环比第三步读取sales和compare画图。技能只需声明自己需要读取哪些键不必关心数据是从哪来的。这个模式做出来的体系技能之间的耦合度非常低换数据源或者调整流程都是改编排层技能本体不用动。但也要注意一个分寸不要把整个数据库都放进workspace。每个技能只写入自己真正产出且后续可能被引用的内容否则堆料过多又会重现“信息过载”的老问题。4.3 冲突处理与回退策略多个技能的调用顺序不是总按预想的来因为决定顺序的是模型而模型偶尔会做出奇怪的决策。我遇到过一种典型情况模型已经调用了图表生成技能接着又回头调了数据查询技能还试图把新查询结果“追加”到已经生成的图里。面对这种乱序调用编排层最稳妥的做法是一致性校验为每个技能声明前置依赖。generate_chart依赖query_sales_summary和compare_sales_period的产出如果编排层发现这两个前驱没有执行记录就拒绝调用图表技能并引导模型先执行依赖项。回退策略同样重要。当某个技能连续失败两次时编排层应该切换到一条预设的兜底路径而不是让模型反复重试同一个必败动作。比如图表技能依赖的数据查询超时兜底路径可以是调用一个轻量版的query_sales_summary_from_cache用缓存数据出图。如果缓存也没有就直接告诉用户“图表能力暂不可用”而不是让模型绕来绕去浪费时间和token。5. 落地过程中的六个坑每一个都是真金白银换来的5.1 描述写得像散文模型根本选不中技能第一次大规模上线时我给一个技能写的description是“这个功能是用于获取用户不同维度的销售信息帮助运营团队快速了解最新的业务动态支持按日期、区域、品类灵活筛选同时也可以对数据进行简单的统计汇总……”看起来没什么问题是吧但模型在用户问“上个月华东卖了多少”时选中的竟然是另一个技能。排查发现问题就出在描述太“功能化”而不是“触发化”。模型做技能选择时不是在看“你这个功能有什么能力”而是在看“用户这个问题跟你哪个技能对得上”。后来我把描述全部改成“当……时调用。不处理……。例如……”的句式效果立竿见影。核心原则是description写触发条件不写功能清单写边界不写能力罗列。团队的技能描述模板到现在还是这个标准任何人都不能写成功能说明书。5.2 技能返回的数据格式不稳定另一个很深的坑技能返回数据格式频繁变动。最初设计返回时我图省事把一些额外信息直接追加成一段人性化字符串放在data里。结果下游做环比计算的技能拿到这段字符串后发现有中文、有数字、有单位解析正则写了几十条还是不达预期。这次的教训非常深刻技能返回格式的不稳定程度决定了下游技能的调试成本。从那以后我定了三条硬规矩所有数值型字段只放数字不加单位所有时间字段统一ISO 8601格式data字段下只放结构化JSON禁止出现任何人类可读的长文本。展示文案永远交给模型生成技能层不做“presentation”。这条规矩之后再也没有破过。5.3 超时与幂等技能调用失败的连锁反应Agent调用技能和普通程序调用接口有一个显著区别普通接口超时了调用方可以快速重试而Agent技能超时会直接影响一次对话的成败。模型等待技能返回的耐心窗口是有限的超时一次模型可能在后续回答里胡编一个结果来填补空白这个风险比超时本身严重得多。我的应对方案是三管齐下给每个技能设置合理的超时阈值比如数据类技能30秒、轻量计算技能5秒超时后立刻返回带error_codeTIMEOUT的失败响应并附一句可操作建议关键技能实现幂等支持——同一个查询请求重复执行返回相同结果这样编排层才能放心重试。幂等的实现有时比想象中麻烦比如涉及“插入”操作的任务就需要生成请求ID做去重但这一步必须做。5.4 技能更新后缓存未失效Agent还在按老规矩调用技能体系稳定运行了一段时间后我修改了某个技能的Schema把字段province改成了region。技能本身和测试都跑通了但线上Agent时不时还在用province这个旧参数调用导致大量校验错误。问题出在动态加载的缓存上。当时技能描述还是从缓存里读取的Schema更新后缓存却还保留着旧的字段名。修了这个Bug之后我完善了技能发布流程凡是Schema变更必须触发描述卡片缓存重建同时保留一个版本的兼容映射让旧参数能自动映射到新字段上。Agent系统的发布流程和传统后端一样需要严肃对待一个字段的改动就可能引起连锁故障。5.5 并发场景下的状态污染技能设计成无状态是有原因的但实际操作里还是容易踩坑。有一段时间我的技能里用了模块级的临时目录来存中间产物结果两个用户同时触发图表生成任务时各自的中间文件互相覆盖生成出来的图完全错乱。排查之后把临时目录改成了按task_id创建的独立目录任务结束自动清理。这个教训让我全面审查了所有技能代码把所有共享的可变状态全部改成了调用级隔离。给Agent做技能跟写单机脚本不一样线上同时会有多个用户在多个会话里调用同一个技能任何共享变量、全局配置、公共缓存都可能成为并发事故的温床。5.6 测试覆盖率陷阱只测成功路径等于没测技能测试最后一个坑也是最隐蔽的初始测试全在覆盖“正确参数正确返回”的成功路径所有失败场景模拟都是后补的。结果上线后碰到的第一个真实问题就是用户输入了超出日期范围的查询技能抛了一个Python原生异常没有任何可读错误信息模型面对异常完全不知道该怎么处理。那之后我在每个技能的测试计划里强制加入失败分支矩阵参数缺失、类型错误、枚举越界、依赖服务超时、数据为空、权限不足。每一个分支都必须返回标准化的错误结构。技能测试要做到“失败永不出原始异常”无论内部发生了什么丢给模型的一定是结构清晰、可读、可行动的提示。6. 技能库的持续运营写一次、用三年6.1 技能分级从一次性脚本到公司级能力技能库沉淀到一定规模后管理就成了核心问题。我把所有技能分成三个级别L1是个人实验技能只在本机使用不注册到生产环境。L2是团队共享技能通过注册表对特定团队开放需要经过代码评审和测试。L3是公司级技能作为标准能力对外统一暴露必须满足严格的质量要求——描述规范、Schema稳定、超时可控、幂等实现、测试全绿。这个分级制度最大的价值不是技术上的而是责任边界清晰。个人实验技能随便折腾团队技能出了问题有明确的负责人公司级技能基本只允许向后兼容的小改动。有了分级技能库才不会在迭代过程中失控。6.2 命名规范与目录结构技能库的目录结构我用的是按业务域划分的方式skills/ ├── sales/ │ ├── query_sales_summary/ │ │ ├── skill.yaml │ │ ├── main.py │ │ └── tests/ │ └── compare_sales_period/ ├── inventory/ ├── customer_service/ └── common/ ├── generate_chart/ └── date_utils/命名上的几个约定技能名一律小写加下划线动词开头如query_*、generate_*、notify_*表达“能力”的词放动词后面不要放最前面。这样技能清单在模型眼里是“按动作索引”的更利于意图匹配。还有一个容易被忽略的点每个技能目录必须自带示例调用和示例返回各一份。这两份示例不仅是给测试用的基线也是新同事学习技能用法的入口。很多团队技能文档写得天花乱坠但找不到一个“真实长什么样”的输入输出样例这种技能到了别人手里基本就是废的。6.3 技能的度量与持续改进技能库上线后不能躺平不管我每季度会做一轮全量复盘。复盘需要关注的指标不是调用次数而是模型“选错”的比例——被注入提示词但最终没有被调用的技能比例是多少调用之后返回错误的比例是多少这两个数字高说明技能描述和用户真实意图之间存在系统性偏差。改进方式也很直接挑出错率最高的三个技能重新读一遍它们的description回看实际对话里模型是拿什么理由跳过它们或搞错参数的然后针对性地改写描述或调整Schema。技能体系是一个持续迭代的活物不是写完就完了。按我现在带团队的流程整个技能库从搭建到稳定大约需要一个月时间。第一周定契约和注册机制第二周写核心技能加测试第三周编排协同第四周灰度上线并做第一轮复盘。过了这个阶段技能库带来的效率优势会非常明显——新场景接进来就是装配组合的事很值得投入。最后再分享一个个人体会Agent技能体系最大的门槛不是技术而是克制。别贪大求全把每个技能的边界划清楚比让它“什么都会”重要得多。一个五十个技能但个个边界清晰的库远好过一个五十个技能但描述含糊互相打架的库。
返回列表