
市面上讲大模型 Agent 的文章已经很多了但大多数都停留在“跑通一个Demo”的层面。今天我想认真聊聊一个更落地、更工程化的话题agent-skills。我把它理解为“Agent 的技能化开发与复用”。直白点说就是把原本写死在代码里的工具调用、Prompt模板、上下文处理逻辑全部按技能单元的方式拆分、注册、编排和管理让 Agent 真正具备可扩展、可维护、可复用的“手和脚”。这篇内容是我在过去半年里把几个业务系统从“单线程工具调用”重构为“技能化架构”之后的经验总结。适合两类人看一类是准备把 LLM 从聊天机器人推向业务工具的开发者和架构师另一类是已经在用 LangChain、Semantic Kernel 或自研 Agent 框架但对技能如何划分、上下文怎么管理、冲突怎么解决还不太清楚的实践者。我会尽量把每一步的选型逻辑、卡点原因、实测效果都摊开讲而不是只贴一堆代码了事。1. 为什么我不再把所有功能写死在 Agent 的代码里先说一个很常见的场景。早期做 Agent 的时候我习惯把外部系统的所有能力都封装成一个大函数比如handle_user_request(order_id, user_id, action_type)里面塞满了订单查询、物流跟踪、退款处理的分支逻辑。刚开始确实能用但随着业务扩展问题开始爆发式出现。首先是维护成本。每加一个新的业务操作都要改动整个函数体测试范围被拉得很大。其次是上下文污染。一次用户对话可能涉及订单、优惠券、客服记录多个实体如果所有工具都在同一个函数里扫荡式执行大模型拿到的上下文会极其混乱经常出现“该查库存的时候去调了物流接口”这种离谱行为。最后是复用性差。换一个行业、换一套业务几乎等于重写。我把问题说直白一点Agent 不是只要“能调用工具”就行它需要的是结构化地理解“什么东西在什么条件下可以用、怎么用、边界是什么”。而 agent-skills 解决的核心问题就是把“能力”从“逻辑”里抽出来形成独立的、可插拔的“技能单元”。这和我开发传统业务系统时把“公共服务”抽象成接口是同样的思路只不过在 Agent 世界里技能单元还要额外承担一部分语义描述工作——因为大模型不像传统程序那样靠函数签名判断逻辑它得靠自然语言描述来理解你这个功能是干什么的、什么时候该触发、参数怎么填。1.1 技能化之前我踩过的三个具体坑第一个坑是参数盲区。有一次做智能客服为了让 Agent 能查询订单物流直接把一个函数传给了大模型函数里面有十几个参数其中一半是内部标识。结果大模型经常凭空编造一个tenant_id传进去服务端直接报错。后来排查才发现大模型根本不知道某些参数应该从上下文里找还是从用户对话里提取更不知道哪些是必填、哪些有默认值。技能化之后每个参数都要声明来源用户输入/上下文推导/固定值、校验规则和默认值问题立刻就清晰了。第二个坑是触发错乱。当时我做了几个工具接口一个是“查订单”一个是“查优惠券”原本以为名字写清楚就好了。实际运行的时候用户说“我那张满100减20的券为什么用不了”模型居然去调了订单查询接口。原因是两个接口都返回了包含“金额”和“状态”字段的结构化数据大模型在模糊意图下选择了印象里更“通用”的那个。这就是典型的技能边界描述不到位——技能的触发条件、适用范围、不适用场景都必须显式写清楚。第三个坑是上下文膨胀。一开始我喜欢把所有工具返回结果不加筛选地塞进后续对话结果一轮工具调用后上下文里堆了几千 token 的 JSON。模型反而抓不到重点回答质量严重下降。技能化之后每个技能都要定义“哪些字段进入长期上下文、哪些字段仅用于当前决策、哪些直接丢弃”。这个取舍过程本质上是从“给模型更多信息”转向“给模型更精准的信息”。这三个坑给我最大的教训是Agent 的能力上限很大程度上取决于技能定义的精准程度而不只是模型本身的聪明程度。“技能描述写得好不好”比“模型选得大不大”更影响实际效果。2. 从零搭建一套 Agent 技能体系的具体步骤把技能化的思路落地成代码并不复杂但有一个逐步完善的过程。我建议的第一步是先从业务流程中提炼出“动作清单”而不是急着写代码。拿我做过的一个“智能售后助手”来说业务动作无非就是查订单、查物流、查退款进度、提交退款申请、修改收货地址、转人工。这每一个动作就是一个候选技能。第二步是给每个技能建模包括技能名称、功能描述、触发条件、输入参数、输出规范、错误处理、使用边界。第三步是注册到技能注册表第四步是在对话中通过调度逻辑/模型决策选择合适的技能。2.1 配置版技能定义描述怎么写得准写技能描述是一门体力活但有套路可循。下面是我后来稳定使用的配置结构用 YAML 写核心字段给出来skill_name: query_order description: 查询用户订单的详细信息包括商品名称、下单时间、订单金额、当前状态 trigger_conditions: - 用户询问订单状态、物流状态 - 用户提到“我买的XX怎么样了” - 用户要求查看历史购买记录 not_trigger_when: - 用户尚未提供订单号且历史上下文无法推断唯一订单 - 用户询问的是优惠券使用规则 parameters: order_id: type: string required: false source: from_user_or_context description: 订单号支持完整订单号或后四位 user_id: type: string required: true source: from_context description: 当前登录用户ID从会话上下文中获取 output: format: json keep_in_history: [order_id, order_status, logistics_company] discard: [internal_remark, created_by] error_handling: - order_not_found: 提示用户核对订单号建议提供下单时间范围 - network_error: 返回“系统繁忙”并标记重试一次这里我想强调几个易错点。description和trigger_conditions最大的区别前者描述的是“这个技能做什么”后者描述的是“什么情况下激活它”。很多教程把它们混为一谈实际跑起来就会发现模型经常在错误的时机调用技能。not_trigger_when这个字段是我强烈建议加的。它相当于给模型画了一条负向边界能显著降低误调用率。我在一次对比测试中发现加了负向条件之后错误调用率下降了约45%。为什么因为大模型在意图识别时常常会因为“字段看起来相关”而误判负向条件直接把容易混的场景点明了。keep_in_history和discard则是从上下文卫生角度做的精细控制。在技能返回结果较大的时候比如订单列表有几十条模型并不需要记住每一条只需要记住关键状态和订单号用于后续连续对话。其他字段如果塞进历史不仅浪费 token还会干扰下一步决策。2.2 功能注册与调度让 Agent 知道有什么技能可用技能定义写好之后要进入一个“调度解析器”。这个调度器可以理解为一个路由器它把用户的当前请求 近期对话历史 候选技能列表一起交给大模型让模型输出一个结构化的调用指令。我的实现思路是在系统提示词里加载一份“技能目录”目录里是每个技能的简介和触发条件摘要而不是全部参数的完整定义。完整定义只在技能被选中后才加载到上下文里。这么设计是为了节省 token同时减少干扰。你想象一下如果一次对话要挂 20 个技能每个技能带 300 字参数描述那光技能说明就占了 6000 token用户的请求反而被淹没了。这里可以借鉴 LangChain 里工具列表的思路但一定要做一层轻量摘要skills_catalog [ {name: query_order, summary: 查询订单状态、物流信息支持按订单号/时间范围检索}, {name: submit_refund, summary: 提交退款申请需确认订单状态为已收货或商家同意}, {name: modify_address, summary: 修改未发货订单的收货地址发货后不可修改}, ]调度层执行完技能之后结果要经过“后处理”一部分写入记忆一部分直接返回给用户。这个环节千万别省否则技能调用完之后模型依然不知道结果是好是坏。2.3 技能状态管理不光是“调用一次”那么轻松如果你的技能只是简单查询状态管理可以不那么讲究。但只要是涉及流程性操作比如退款申请就必须给技能加上状态机待执行 - 执行中 - 成功 / 失败 / 需要补充信息。每个技能的调用不是一次性动作而是一个可回溯的过程。我遇到过最典型的一个问题是“重复执行”。用户在对话里问“我的退款申请怎么样了”模型误判为用户要“再次提交退款申请”结果同一个订单被提交了两次。这个问题的根因不是模型笨而是技能定义里没有区分“查询状态”和“提交申请”。所以后来我在定义技能时增加了一个idempotent字段标记操作是否幂等并且把“查询退款状态”和“提交退款申请”拆成了两个独立技能。状态管理这块我还做了个很轻量的方案用一个内存字典维护每个会话的技能执行状态在重试或追问时直接读状态而不是让模型重新猜测。代码大概是这样session_skills_state {} def execute_skill(session_id, skill_name, params): state session_skills_state.get(session_id, {}) if state.get(skill_name) executing: return {status: blocked, message: 技能执行中请勿重复触发} session_skills_state[session_id][skill_name] executing try: result run_skill(skill_name, params) session_skills_state[session_id][skill_name] success return result except Exception as e: session_skills_state[session_id][skill_name] failed raise e这个方案虽然简单但在真实对话场景里极大减少了“模型自作主张反复触发同一技能”带来的脏数据。3. 技能编排一个请求触发多个技能时先后顺序和并行策略怎么定单技能的调用问题解决之后下一个坎就是多技能的编排。用户的真实请求往往不是单一动作举几个例子“把我最近未发货的订单都查出来然后帮我改成新地址”——这里涉及query_order、modify_address且必须按顺序执行。“查一下商品到哪了顺便看下这个订单能不能退”——涉及query_logistics、query_refund_policy其实可以并行查。编排方案目前我深入用过三种基于固定流程、基于模型动态规划、基于混合编排。它们各有利弊。3.1 固定流程适合业务稳定、步骤明确的场景固定流程指的是用代码显式定义“先做 A 再做 B 再做 C”。例如“改地址”的流程是先查订单确认状态 - 校验是否已发货 - 如果未发货则调用修改接口。这个流程固定容错要求高不适合让模型自由发挥。实现方式可以是简单的有向无环图也可以用状态机来控制。我评价固定流程的核心指标是“确定性”。当业务逻辑涉及真金白银比如退款、改价、扣库存时我绝对不敢让模型来自由规划。这个度必须把好生成式的能力用于“理解用户意图、抽取参数”但动作的编排顺序必须是工程师定义的。3.2 模型动态规划适合探索型、长链路场景反过来如果业务逻辑比较轻或者用户问题模式千变万化用模型来做“规划者”更合适。做法是把所有技能描述都给模型让它输出一段规划 JSON比如{ steps: [ {skill: query_order, params: {order_id: null}, depends_on: []}, {skill: query_refund_policy, params: {order_id: 来自上一步}, depends_on: [query_order]} ] }模型自己决定依赖关系。这种方案在“探索型”任务上表现很好比如用户根本不了解自己的订单状态模型会自动先查订单、再根据订单状态决定下一步是查退款规则还是查物流。它的缺点也很突出模型规划不稳定依赖模型能力和 prompt 质量。同样的输入跑十次可能有九种不同计划。我一般只在“非关键操作”链路上用纯模型规划并且在规划结果出来后加一层“规则校验”比如检查是否有缺失参数、是否引用了不存在的技能名称。校验失败就退回固定流程或让用户重新表述。3.3 混合编排目前我用的最优解实际项目中我用的最多的是混合编排用代码解构稳定的骨架用模型填充分支点。打个比方固定流程像地铁主干线模型动态规划像出站后的共享单车你既不能只靠地铁到不了所有地方也不能只靠单车在高峰期堵死。举一个具体例子售后助手处理“我要退货”时我会先按固定流程走查询订单 - 校验是否符合退货条件 - 询问退货原因 - 生成退货单。这里每一步都固定。但在“询问退货原因”这个环节我不会给用户一堆按钮而是让模型根据对话上下文动态生成追问话术并把用户的自由文本解析成结构化原因标签。这就是“流程固定、表达自由”。混合编排的另一个好处是便于监控。固定流程结构清晰出了问题能快速定位到具体环节模型规划的环节单独打 log方便单独优化 prompt 或微调。4. 实测对比技能化改造前后的效果差异理论说了很多最终还是得看数据。这是我接手的一个售后对话场景在同一个模型GPT-4o级别下对比“无技能化的裸工具调用”和“技能化调度”在 500 条真实对话样本上的表现指标裸工具调用技能化调度工具误调用率18.6%6.2%参数缺失/报错率12.4%3.1%用户问题一次解决率72.3%84.7%平均单轮工具调用次数3.82.2上下文平均消耗 token21401560这里要说明工具误调用率指的是模型调用了错误的工具或者在不该调用的时候调用参数缺失/报错率指的是调用后因为参数不合法而失败的比例。这两项下降是技能描述和参数声明的直接功劳。平均单轮工具调用次数从 3.8 降到 2.2是因为技能清单和负向条件让模型更早确定目标技能不需要“试错式”调用。上下文 token 下降则是keep_in_history和discard策略生效的结果。另外一个意料之外的收获是调试效率大幅提升。以前在裸工具方案下出了问题要先看模型调用了哪个工具再看工具的参数怎么填的最后看返回结果是什么排查链路特别长。技能化之后每个技能都有独立的日志模板和执行记录能直接看到一次调用的完整入参出参定位问题通常只需要一分钟。5. 技能化落地的常见误区和我踩到过的新坑最后一部分我专门把这段时间踩过且网上很少提到的坑列出来当作给同行的提示。5.1 技能划分的粒度不是越细越好刚开始我把“查询订单”拆成了“查询订单基本信息”“查询订单物流”“查询订单商品明细”三个技能心想这样职责更单一。实际却很惨模型经常不知道用户问“我的东西到哪了”该用哪个技能因为“物流”“明细”“基本信息”这些概念在用户表达里高度重叠。后来我把三个合并成query_order通过参数detail_type区分。调用成功率反而升高了。经验是技能的划分要以“用户的自然意图”为粒度而不是以“后端接口”为粒度。接口粒度通常偏细多个接口经常服务于同一个用户意图硬拆开只会增加模型的选择难度。5.2 技能描述里的“负面信息”比正面信息更值钱我发现在技能描述里写清楚“这个技能不做什么”远比反复强调“做什么”更有用。比如submit_refund这个技能如果只写“提交退款申请”模型很容易在订单还在运输途中就给用户提交退款。而一旦写上“仅当订单状态为已收货或已入库时可提交在途订单需先确认签收”模型立刻就会先查询订单状态再决定是否调用。这个现象背后的逻辑在于大模型的意图判断本质上是在做概率匹配。正面描述能提高正确调用的概率但无法消除“边缘情况”的误判。负面描述实际上是在给概率分布打补丁把禁区标记出来从而压缩错误空间。5.3 技能注册表太长了模型会“看不过来”当技能数量超过 20 个以后我发现模型对技能列表末位的技能几乎处于“失明”状态。这不是模型指令遵循能力不行而是注意力被前面的描述稀释了。解决办法有两个方向一是做技能分组把相关技能放在一个组内调度时先选组再选具体技能二是做技能召回先根据用户请求的语义做一次嵌入检索只把召回的相关技能放在候选列表里。我现在用的方案是两者结合第一层是一个轻量语义检索器从全量技能库里召回 top 5 候选第二层把召回结果和前几个最常用的技能合并组成动态技能列表。实测下来即使技能库扩展到 50 个调用准确率依然能维持在高位。5.4 多轮对话状态下技能状态和用户状态要分开最后这一个坑比较隐蔽但特别重要。技能是有状态的用户也是有状态的两者千万别混在一个变量里管。举个例子技能状态是“退款申请已提交”但用户状态可能是“正在犹豫要不要退”。如果你直接根据技能状态判断用户下一步意图模型很可能机械地追问“您的退款已提交请等待处理”而忽略了用户其实想撤销退款。我在实践中把用户状态单独建模比如pending_intent当前用户的潜在意图、awaiting_input正在等待用户补充信息、needs_confirmation需要确认后才能执行。调度层会同时参考技能状态和用户状态再做最终决策。这个改造最初是为了解决“用户想撤销退款但模型一直引导他走完退款流程”的问题。改完后这类生硬对话基本绝迹了。6. 技能化架构下一步可以怎么扩展目前这套 agent-skills 的体系已经在我的两个项目里跑稳定了但它还有几个明显可以继续延展的方向。第一个方向是技能的自动评估与自更新。我现在新建一个技能之后主要靠人工标注测试集来验证描述是否合理。如果能做成自动的“技能体检”定期用历史对话日志回放一遍检测哪些技能出现了误调用、哪些技能从未被触发就能持续优化技能描述。这个方向已经在规划中。第二个方向是跨 Agent 的技能共享与组合。不同 Agent 之间如果有大量重叠技能比如订单查询在两个 Agent 里都需要那么技能定义应该可以像插件一样跨项目复用。我在内部把技能定义文件抽成了一个独立包用 Git 子模块的方式挂到不同项目里效果挺好。第三个方向是让技能支持多模态输入输出。目前我的技能还是以纯文本输入为主但在客服场景里用户经常发截图。如果能把“理解截图内容”和“查询订单状态”组合成一个技能让用户直接发一张订单截图就能得到物流信息那体验会是质的提升。回到我最初的观点Agent 的能力提升不能只靠“换个更强的模型”技能体系的工程化反而是更稳定也更可控的杠杆。如果你正在做 Agent 相关项目我强烈建议从“技能定义”这个层面开始打磨它的复利效应比反复调 prompt 要长远得多。总的来说agent-skills 这条路本质上是在为 Agent 打造一套“表达能力之外的执行体系”。把动作拆解成技能把技能描述成语义把调度做成流程把状态管理纳入架构——这四步走完你的 Agent 就不再是只能“聊聊天”的玩具而是真的能接手业务逻辑的“数字员工”。