ARTICLE DETAIL

资讯详情

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

Agent技能系统实战:从Prompt困境到可插拔技能编排

Agent技能系统实战:从Prompt困境到可插拔技能编排 1. 我理解中的agent-skillsAgent不是聊天的是干活的先说一个我踩过的坑。之前在做某个客服类Agent项目一开始图省事把所有能力和指令全部堆进system prompt里前前后后写了上千行的规则什么查物流、退换货、开发票、算优惠全塞进去。结果模型越跑越笨经常把查物流和退换货搞混对用户说我帮您查一下发票进度实际调用的是另一个完全不相关的接口。后来我痛下决心把整套逻辑从prompt里抽出来重新整理成技能单元每个能力独立定义、独立注册、按需触发。改完之后效果立竿见影误触发率降了一个量级prompt长度也砍掉了一半多。这就是agent-skills要解决的问题让Agent从会聊天变成会干活。聊天只需要一张嘴干活必须有一双手。技能就是Agent的手每一个技能对应一个具体的操作能力比如查天气、写代码、订机票、算账它是介于意图和动作之间的那一层抽象。所有的技能聚合起来就是技能库。对开发者来说agent-skills不是一个具体软件而是一种架构思路——把Agent的能力拆碎变成可插拔、可复用、可控的模块再交给模型去调度。这篇文章不是讲某个现成产品的使用教程而是从我自己搭建、调试、上线一套Agent技能系统的经验出发拆解整个设计的来龙去脉。不管你是做ChatGPT类应用的外层封装、企业内部Agent平台还是研究多智能体协作这套思路应该都能给你一些参考。我会重点讲三件事为什么要把技能从提示词里拆出来技能定义怎么写才不会被模型瞎调以及调用链路上那些文档里不会写清楚的坑。2. 技能抽象的本质把能力从上下文里解放出来2.1 单体提示词的失效临界点先认真聊一下为什么技能仓库这个设计会成为Agent工程里的核心话题。很多人觉得反正模型这么聪明把所有指令写清楚不就行了吗这个想法在功能少于5个的Demo阶段确实能跑通但规模一上来就崩。原因在于大语言模型的注意力机制。模型处理一个超长prompt的时候虽然技术上能看到全部文本但它对每个位置的关注权重并不是均匀的。实测下来当指令数量超过一定阈值模型会开始遗忘早期的指令尤其当这些指令之间还存在语义相似性时混乱几乎是必然的。把十个技能描述全部写进prompt模型就像一个人面对十本操作手册临时翻书去找查物流在哪个章节翻得越久越糊涂。更关键的是prompt里的指令是静态的。你写上去的内容无论这次对话需不需要模型都必须处理一遍。这不仅浪费token更浪费模型的决策带宽。就好比一个工具箱你得把所有工具都摆在桌面上干活的时候反而不知道怎么选了。技能抽象的切入点就是把能力定义和上下文拆开。能力回归到索引里去上下文只保留与当前任务相关的东西。模型不需要每轮都看完所有技能说明它只需要在需要的时候通过一个轻量级的能力目录去检索并唤起目标技能。这既减少了token开销也大幅降低了误触发率。2.2 技能和函数有什么本质区别很多技术文章喜欢把agent-skills等同于函数调用这个表述其实不够准确。函数是编程观点下的产物注重的是输入输出类型、异常处理、返回值。技能是语义观点下的产物更注重的是什么场景下该用、怎么做决策、需要依赖什么前置条件。我做一个区分维度普通函数Agent技能触发方式代码里显式调用由模型根据用户意图自动决策核心描述输入输出类型触发场景与边界条件失败处理抛出异常返回可被模型理解的状态信息依赖关系编译期确定运行时动态解析可观测性日志记录需要记录决策依据与执行轨迹这个区别决定了写技能的思路完全不同。写函数你只要保证调用方懂参数就行但写技能你得假设调用方是一个概率模型它无法百分之百理解你的意图所以你必须在描述里把什么时候用什么时候别用都说清楚。2.3 三种技能组织形态的取舍我自己经历过的技能组织方式大概有三种演进阶段。第一阶段是平铺式。所有技能放在一个文件里靠名字区分。优点是简单缺点是技能一多就乱。后面我管理到30多个技能的时候每次触发逻辑都开始互相污染比如查天气和查气温这种语义高度重叠的技能模型根本分不清。第二阶段是分组式。按领域分组比如电商类内容类系统工具类。这种在技能数量上百时很有效因为模型可以先定位到组再在组内检索定位难度降了一个等级。但我发现分组也不能解决所有问题因为很多技能是跨域的比如发送邮件确认通知这个技能既属于邮件又属于客户服务硬归档会丢上下文。第三阶段也是我现在在用的方式语义索引分组兜底。每个技能除了有人工定义的分组外还额外生成一组语义向量索引检索时先在向量库里做相似度召回召回结果再通过分组过滤掉不相关的候选。本质上是用embedding去做预筛人工分组做精排两者配合。这套方案的实际效果是技能数量突破100个之后误召回率依然能控制在5%以内。如果你的项目刚起步不需要一上来就上向量检索。先去平铺式技能超过20个再切分组超过80个再考虑语义索引。过早引入复杂度就是给自己挖坑。3. 实操第一步一份能让模型准确理解的技能定义3.1 技能描述的核心是边界不是功能这是我觉得整个agent-skills设计中最容易被低估的点。技能描述的目标不是让模型知道这个技能能做到什么而是让它知道在什么情况下应该选它、什么情况下不应该选它。我之前犯过一个特别典型的错误。给一个技能写描述查询用户的会员等级。相信我这五个字放到真实场景里就是一坨浆糊。模型根本分不清用户的这句我开了会员怎么没折扣到底该触发查会员等级还是算折扣金额于是它会两个都触发或者随机触发一个。后来我把描述改成了长句当用户主动询问自己的会员等级、积分数量、会员有效期或暗示需要了解当前会员权益时调用此技能。若用户只是抱怨没有优惠应先检查优惠计算技能而非此技能。注意最后那句否定说明这非常关键。模型通过对比正面例子和反面例子来理解边界比单纯给一堆同义词要强得多。所以技能描述的基本公式是一句话说清技能职责具体列出至少三个触发场景的例子至少写一个不触发的反例如果存在容易混淆的邻近技能明确说出区分点。3.2 技能结构和参数别让模型猜除了描述技能的结构定义同样重要。每个技能我维护的主要字段如下字段作用必备程度name技能唯一标识建议用snake_case必备description触发条件边界条件必备parametersJSON Schema定义的参数必备required_params必填参数列表建议dependencies依赖的其他技能或数据源复杂技能必填timeout超时时间建议success_criteria判定执行成功的标准进阶推荐参数定义这块我反复吃过亏。最早我给参数写得极简比如city这个参数直接写城市名。结果模型在用户说我在上海的时候传了上海用户说这边下雨没的时候也传了上海——这倒没错但遇到用户说帮我看看明天的时候模型直接缺了城市参数宁可瞎猜一个也不肯反问用户。我后来在参数描述里加了补充说明city指的是用户当前所在城市或用户明确指定的城市。如果用户未提及任何城市信息不要自行推断将该参数留空并主动询问用户。关键就是加上了不要自行推断这句约束。你会发现大模型在面对不确定信息时有一种强烈的补全冲动你必须用显式规则把它压住。3.3 技能的原子性与复用性拆多细才算合适技能粒度这个问题我直到现在都觉得没有标准答案但有一些经验法则。理论上说技能拆得越细复用度越高但调度负担也越重。一个技能如果只有打印日志这种粒度那它就没有存在的意义。反过来如果一个技能内部包含了做A、做B、做C、再看情况做D那它其实是一个流程不应该叫技能。我自己的判断标准是**如果一个技能可以被至少两个不同的上游场景复用且内部只做一件事那它就是原子技能。**比如查订单状态是原子技能处理售后就不算处理售后是查订单状态判断售后政策创建工单三个技能按照特定策略组合出来的流程。有了原子技能的概念之后上层才能做编排。我在实际项目里维护了一个技能依赖图每个技能节点记录它依赖了哪些更底层的技能。这样当处理售后被触发时系统可以自动把它拆解为子任务的调用链而不是在处理售后这个技能的JSON里硬编码所有的操作步骤。3.4 不同技能间的状态传递容易忽略的隐性上下文还有一个容易踩的坑就是状态传递。技能不是每次都被独立调用的技能之间经常需要共享上下文。比如查订单状态之后用户紧跟着问那能发货到北京吗这个北京是靠上下文推断的而不是在第二次调用里显式传参。我现在会在技能执行上下文里维护一个轻量级的共享状态池。每个技能执行完后会把关键输出字段自动提交到状态池下一个技能被触发时可以先从状态池里读值补全自己的参数。比如发货地址这个字段如果当前技能参数没填系统会先到状态池里查查到就直接补进去查不到再问用户。这套设计的关键在于状态池的写入要有严格的schema约束不能什么值都往里塞。否则积累到后面状态池就变成了一个谁都能写但谁都不知道里面有什么的黑洞。我现在用的是白名单字段机制只有预先声明可以被共享的字段才会进入状态池其他字段一律归技能私有。4. 从注册到调度技能系统落地的核心实现4.1 技能注册一切能力入库的第一关技能注册是整个技能库的入口。它的作用是把一个技能从代码定义变成可被检索、可被调用、可被观测的系统资源。我实现的最核心部分是注册器使用Python实现核心逻辑大概长这样class SkillRegistry: def __init__(self): self._skills {} self._index {} # 技能名 - 技能定义 self._embeddings {} # 技能名 - 语义向量 def register(self, skill: BaseSkill): if skill.name in self._skills: raise ValueError(f技能重名: {skill.name}) self._skills[skill.name] skill self._index[skill.name] skill.get_openai_schema() # 预生成语义索引 self._embeddings[skill.name] embed_text( f{skill.name}: {skill.description} ) logger.info(f技能注册成功: {skill.name}) def get_skill(self, name: str) - BaseSkill: return self._skills.get(name) def list_schemas(self) - list: return list(self._index.values())注册的时候我强调两点。第一重名坚决拒绝。技能重名引发的后果不是报错这么简单它会让模型在调度阶段直接晕掉因为它不知道两个同名技能到底该选哪个。第二注册时顺手把语义向量算好存起来不要在运行时算否则每次请求的延迟都会高得没法看。4.2 模型调用技能的两种路径function calling和自然语言解析实现技能调度的具体路径主要看你的底座模型能力。我的经验是主流方案大致分成两种如果模型原生支持function calling优先走function calling如果模型不支持就得靠提示词加JSON解析。两条路各有利弊。function calling路径把注册好的技能Schema列表传给模型模型在回复中返回一个结构化的是否调用函数参数的对象。优点是很稳因为模型经过了特定训练知道什么情况下返回什么格式。缺点是你能传给模型的函数Schema数量是有限的通常是几十个的量级。技能一旦超过这个阈值就不能全量传了必须做预筛。自然语言解析路径把所有技能描述写进提示词让模型自主输出一个指令格式系统再正则或结构化解码。优点是理论上技能数量不受限缺点是稳定性差模型经常输出格式残缺的JSON。我在线上项目里的实际方案是两者结合的核心思路是分阶段先用一层轻量级意图分类模型做技能粗召回把候选压缩到10个以内再把这10个技能的定义完整传给大模型让它做function calling路由。我把这个叫作粗召回精路由的双层结构。4.3 技能编排引擎单技能到多技能的跨越当你的Agent开始需要把多个技能组合起来完成任务时编排引擎就得上场了。我说的编排不只是简单的先调用A再调用B顺序执行而是需要支持条件分支、并行执行、依赖解析和回滚补偿。以我之前做的订单处理Agent为例用户提出这样一个需求帮我把上个月的手机订单取消掉。这个需求拆解后需要的技能链是这样的解析需求技能识别出取消订单意图、时间范围、品类限定词查订单技能按用户ID时间范围品类过滤出订单列表筛选匹配技能在上述列表中匹配手机订单检查取消策略技能根据订单状态判断是否允许取消执行取消技能如果可取消则执行操作通知技能如果不可取消生成解释信息告诉用户。这不是一个固定的DAG因为执行路径取决于中间结果——订单可能不存在可能已发货可能取消被拒。我用的是一个基于规则流程的可视化编排器每个节点代表一个技能调用节点之间的连线表示依赖关系连线上的condition是进入下一节点的判断条件。编排放到代码里就变成了声明式结构。你可以简单维护一个步骤序列但认真建议把编排逻辑从代码里独立出来变成一个配置文件或者可视化画布。这样后续调流程的人不需要动代码改配置就能上线。flow SkillFlow(cancel_order_flow) flow.add_step(parse_intent, skillintent_parser) flow.add_step(fetch_orders, skillorder_fetcher, deps[parse_intent], params{start_time: {parse_intent.start}, end_time: {parse_intent.end}}) flow.add_step(filter_target, skillfilter_brand, deps[fetch_orders]) flow.add_step(check_policy, skillcancellation_policy, deps[filter_target], conditionlambda ctx: ctx[filter_target].has_match) flow.add_step(do_cancel, skillcancel_order, deps[check_policy], conditionlambda ctx: ctx[check_policy].allowed)这段代码的意思是每一步技能的输出可以通过占位符方式传给下一个技能。这就是技能之间状态传递在代码层的落地方式。4.4 技能召回评分保证正确的技能被选中的数学方案技能召回评分机制是整个系统的隐形核心它决定了模型面对多个相似技能时能不能选对。我的召回评分方案是线性的权重需要在自己的数据上反复调def score_candidate(skill, user_input, current_context): semantic_sim cosine_similarity(skill.embedding, embed_text(user_input)) context_bonus 0.3 if skill.name in current_context.active_skills else 0 confidence 0.5 if skill.discriminative_patterns else 0 fatal_penalty -10.0 if skill.is_exclusive_conflict(user_input) else 0 return (0.6 * semantic_sim 0.2 * context_bonus 0.1 * confidence fatal_penalty)这个评分公式里有几个关键设计。上下文加权如果当前对话的上文已经激活过某一个技能领域那该领域下的其他技能得分就需要得到加成。比如用户前面问的是订单退款那这次它说帮我看看进度就该优先触发退款进度查询而不是物流进度查询。排他性惩罚这是很实用的防混淆手段。有些技能和目标技能之间存在明显语义冲突比如取消订单和修改订单当用户说我想把地址改一下时取消订单就应该得到重罚。我通过在技能定义里增加exclusive_conflict字段来标识那些绝对不能同时出现的技能配对。这套评分不是绝对的它只能帮你做预筛和兜底。真正做最终决策的还是你把候选技能放进模型上下文后让模型自己选。但有了合理的预筛你在模型里看到的候选列表质量会完全不同。4.5 技能的可观测性没有追踪就无从优化最后技能系统的可观测性设计绝对不能省。技能在跑但你不知道它为什么选了这个技能那出了问题就只能靠猜。我在每个技能的调用链路上埋点记录的数据包括触发意图输入、命中技能名、召回评分、模型最终决策理由、参数填充内容、执行耗时、成功/失败状态、失败原因明细、token消耗。每一轮调用都生成一个trace_id整条调用链全部串起来。这样任何一个线上问题拿到trace_id就能完整复现。具体日志结构大概长这样{ trace_id: 66f3a2b8e19c4f0ea1d1e2a3b4c5d6e7, user_input: 帮我查一下订单, recall_candidates: [ {skill: query_order, score: 0.82}, {skill: query_logistics, score: 0.76} ], model_selected_skill: query_order, filled_params: {order_id: null, user_id: U123}, param_missing: [order_id], execution_result: failed_missing_param }有了这样的日志我后面做评测集和回归测试时才能拿到真实的数据。没有可观测性技能系统就永远停留在能用和碰运气之间。5. 调试路上的那些坑实测问题清单5.1 高频问题速查表整理一份我实际遇到而且大概率别人也会踩的坑常见问题根因解决思路模型频繁触发错误技能技能描述语义边界不清有歧义词在描述中增加反例和易混淆区分说明同一技能被重复触发多次技能结果未写入上下文模型不知道已完成执行完成后标记该技能状态为done参数总是缺关键字段参数描述未说明缺省处理规则显式写缺失时主动询问用户不要猜两个技能互相冲突未配置排他性约束在技能定义中维护exclusive_conflict列表编排流程执行到一半卡住上游技能失败后无降级处理为每个节点配置失败策略重试/跳过/终止技能调用结果模型不理解返回结果缺少自然语言摘要技能返回时增加human_readable字段5.2 排查方法论别从头看要从决策出发技能系统的排查逻辑和传统软件不同。传统后端排查是先看报错——哪个接口返回了500然后追堆栈。技能系统的问题往往没有一个明确的报错整个系统正常地完成了调用但调用了错误的技能或者填充了错误的参数。我现在排查问题的起点不是日志而是决策轨迹。先找你这条链路里模型的每一次选择选了哪个技能、填了什么参数、置信度是多少。然后顺着选择去翻上下文确认它做这个选择的时候看到了哪些信息。80%的问题在这一步就能定位。如果模型确实选对了只是执行报错那问题就回到传统的服务排查轨道上了看技能内部的服务调用、超时、异常分支即可。5.3 评测集搭建与回归测试技能系统的核心是模型决策而模型的决策质量会随着模型版本、提示词调优不断变化。所以评测集是刚需必须有。我搭的评测集结构分三层基础触发评测、边界条件评测、组合编排评测。基础触发评测就是单轮对话直接命中目标技能的场景我在里面主要放的是相同意图但不同表达的句子。比如同一个查询订单技能评测集里至少有30种不同的用户说法从我的订单到哪了到那个快递走得怎么样了再到我昨天买的东西什么时候能到全都覆盖。单技能评测目标很简单触发正确率必须在95%以上。边界条件评测考察的是不该触发的时候不能触发。比如用户在开玩笑说你再不发货我就取消订单这里用户并未最终确认取消意图系统此时就不应该触发取消订单技能最多触发询问是否确认取消。这种例子是评测集里最值钱的部分。组合编排评测是考察多技能串联的场景比如帮我查一下我前天买的那个黑色手机壳的物流顺便看看订单里还有没有其他待发货的东西。这种输入至少触发了三个技能查订单、查物流、查待发货列表。评测目标是看编排结果是否完整、顺序是否合理、参数是否正确传递。我每次迭代技能描述或底层模型的prompt策略后都会把评测集完整跑一遍。早晨跑完再决定要不要上线。这个流程守住了很多次回归事故。5.4 性能与成本调优的实战数值技能系统上线之后性能和成本往往比功能质量更早成为问题。拿我之前线上Agent的运行数据来举例调用一次技能系统在大模型层大约消耗800到1500个输入token。随着技能Schema越加越多token消耗还会持续上涨。三个维度让我把成本降了下来。召回压缩最开始我一次性把所有技能Schema全部传给模型40多个技能有时一次输入token要3000多。后来加上粗召回预筛每次只传5到8个候选技能Schema单次输入token降到1200左右成本直降60%。缓存命中对于很多决策型技能用户意图和参数组合其实高度重复。我在技能调度层加了一层结果缓存命中缓存的请求直接复用上次的中间决策不走模型推理。实测线上命中率能达到25%左右。小模型预筛粗召回那个环节不一定非要用大模型我现在用的是一个轻量级的分类模型单次判断耗时不到30毫秒成本可以忽略。如果你刚开始做其实用一个关键词表加几个手工规则也能撑住前几十个技能。5.5 模型升级的连带风险必须盯着做的回归最后聊一个很多人会栽的坑模型供应商一升级你整个技能系统可能就失智了。我有一次遇到的情况是底层模型从某个版本突然升级结果所有技能的召回率从93%直接掉到75%。一条代码没改纯粹是因为新版本模型的指令遵循风格变了它对技能描述的敏感度、对触发边界的理解方式全变了。所以我要强烈建议模型版本升级必须作为一种重大变更来对待不仅要跑评测集也要抽看线上日志里的决策轨迹确认模型的新理解方式和旧版本一致再灰度放量。6. 关于这套思路的两个延伸思考6.1 技能里的Prompt是不是可以省掉有的人会觉得技能描述本质上还是Prompt只不过改了个结构。这个说法有道理但差了一层。技能描述和普通Prompt的核心差异在于技能是一个带状态、带调度、带返回契约的执行单元。它不只是给模型看的文字它同时是注册表里的条目、编排器里的节点、观测系统里的埋点对象。它是在跟工程系统产生交互而不是只跟模型对话。所以单看一段技能描述你确实可以说它就是个Prompt但把它放回整个agent-skills的架构里它就是整个系统架构中的一等公民承载了交互协议、状态转换和业务边界这些工程语义。这层区别决定了你是在写好一点的提示词还是搭一个能上规模的技能系统。6.2 不同场景下Agent技能设计的差异Agent技能的设计不是放之四海而皆准的。我做了客服Agent和创作辅助Agent两个项目它们对技能系统的要求差异很明显。客服类的技能强调确定性和稳定性一个操作失败的代价很高所以技能参数要极其严格、编排要尽可能走规则分支、容错要丰富。而创作辅助类的技能更强调灵活性和发散性用户输入高度不可预测技能之间的组合方式千变万化所以技能的定义可以更宽泛召回评分可以更偏向语义相似度而不是硬规则。如果你要做的Agent是工具调用密集型的比如办公助手重点应该放在技能编排和参数精确填充上如果你做的是知识问答型Agent重点应该放在技能召回准确率和场景边界上。方向走对了后面才不会返工。7. 最后说点我的个人体会这套agent-skills架构从我最早的单体prompt走过来前前后后迭代了将近半年。我最大的感受是Agent工程的复杂度不在单个模型的聪明程度而是在能力管理这件事上。技能本身就是一种管理手段它逼着你去把每个能力说清楚——什么时候用、什么时候不用、参数是什么、依赖什么、失败了怎么办。另外很多人在设计技能系统时容易陷入一个误区就是花大量时间处理长尾场景。我的建议是先把主干跑通、评测集做扎实、观测体系建好长尾问题等数据说话哪里报错改哪里。技能系统的好玩之处就在于它是一个可以持续生长的系统你给技能库增加一个技能、调一段描述、优化一条编排路径整个Agent的表现都会跟着变。如果你也在搭建类似的技能系统建议从最小闭环开始定义5个明确不重叠的技能跑通注册、召回、调用、日志的全链路再往里面加复杂度。这个基础打稳了后面技能数量翻倍增长的时候你才知道哪些设计撑得住哪些设计会被压塌。
返回列表