Agent技能化实践:从零构建可复用、可演进的智能体应用
发布时间:2026/10/13 15:41:10 锦皓数字建站

1. 项目定位与核心价值1.1 agent-skills 到底解决什么问题这两年大模型应用领域最热闹的方向除了模型本身的能力提升就是怎么让模型真正动手干活。聊到 agent 落地大家最先想到的是 function calling、工具调用、ReAct 这类概念但真去搭一个能稳定完成复杂任务的 agent 时你会发现一个很尴尬的问题东西都跑通了但稍微换一个场景整套逻辑就又得重写。我见过太多项目把 agent 的能力全部塞进提示词里或者一股脑堆在代码的 if-else 里。刚开始 demo 效果很好一旦进入真实业务需求一变整个调用链就崩了。问题不在于模型不够聪明而在于我们把技能和流程混为一谈了。agent-skills 这个思路本质上就是要把模型会什么和怎么用彻底解耦。拿生活里的例子类比一下。一个人会开车、会做饭、会游泳这是技能今天出门上班、晚上回家做饭、周末去游泳这是流程。你不可能因为临时改了目的地就忘了怎么踩油门。agent-skills 想做的就是把踩油门这种原子能力沉淀成独立模块让上层的任务编排可以自由组合、随时替换而不是每次出行都把驾驶知识从头学一遍。放到实际项目里这套思路解决的是三个长期痛点第一个痛点是复用。同一个查天气能力你会在日程助手、旅行规划、客服机器人里反复用到。如果每次都在各自代码里写一遍维护成本直接爆炸。第二个痛点是稳定。把技能封装成独立单元后每个技能可以单独测试、单独优化出问题时定位范围非常明确。第三个痛点是扩展。新业务接入时不需要理解整个 agent 的复杂逻辑只要对接对应技能接口就行。所以 agent-skills 的核心不是某个具体的工具或框架而是一种组织 agent 能力的方式。它强调的是让技能本身成为可管理、可测试、可演进的一等公民。1.2 适合谁来学习和参考如果你正准备做 agent 应用或者已经做过几个 demo 但感觉不够工程化这个项目思路非常值得你参考。哪怕你只是用脚本调模型接口还没有完整的 agent 框架也能从里面学到一套能力拆分的通用方法。我自己的体会是这个思路对三类人最有价值第一类是刚入门 agent 开发的新手。很多人上来就学各种框架结果被抽象概念淹没。agent-skills 提供了一条很直观的路径先罗列出 agent 要完成的所有动作每个动作单独做成一个技能再考虑怎么组合。这个顺序天然符合人的思维习惯。第二类是在做中大型系统的工程师。系统一复杂最怕的就是各个模块之间耦合太深。把技能做成独立模块后每个人只需要维护自己负责的那部分团队协作的边界也清晰了。第三类是做 AI 产品落地的人。产品需求变化频繁今天想加个新功能明天想调整交互逻辑。技能化的结构让你能快速响应变化而不需要每次都动底层架构。这个项目不是那种看完就会的教程类项目它更像一套方法论加上配套的实践脚手架。你需要自己动手把一个技能跑起来再去改造成适合自己的形态。但只要跟着做一轮你回头再看其他 agent 项目时会突然觉得通透了。2. 整体设计与拆解思路2.1 为什么技能化比流程化更抗变化在拆解 agent-skills 的设计之前我想先聊一个很多人没想透的问题为什么把技能单独拎出来比直接写流程更抗变化我之前接手过一个客服问答 agent当时的实现方式很直接用户问题进来经过意图识别然后走对应的处理分支。看起来没问题但每次上线新业务流程开发就得在意图识别的逻辑里加分支测试要在回归用例里补场景有时候连历史行为都会被改掉。后来我把核心动作全部抽成独立技能——查订单、退换货、催发货——意图识别只负责决定调用哪个技能流程编排单独放在配置层。从那以后新业务接入基本没再动过核心代码。这里面的道理其实很简单流程是易变的技能是相对稳定的。用户今天的诉求可能是我要退款明天可能是我要改地址但查询订单信息这个动作本身没有变。把稳定部分和易变部分拆开就是把变化隔离在最小的范围内。agent-skills 的设计遵循了同样的原则。上层负责做什么和按什么顺序做下层技能只关心怎么把一个动作做好。两者通过统一的接口协议衔接各层可以在不影响对方的前提下独立演进。从团队协作的角度看这种拆分还有额外的好处。负责业务流程的人可以专注在编排逻辑上不用关心模型调用、参数解析、异常重试这些技术细节负责底层技能的工程师也可以专心把每个技能做到极致而不需要理解上层的业务语义。职责边界清晰了沟通成本自然就降下来了。2.2 核心模块拆解从宏观架构上agent-skills 大致可以分成三层每一层的职责都非常明确。第一层是技能注册中心。所有技能在这里登记包括技能的名称、描述、输入参数、输出格式、版本号、依赖信息等。这一层解决的有哪些技能可用的问题。你可以把它理解成一本技能目录上层模块通过查询这个目录来发现和选择合适的技能。第二层是技能执行引擎。当一个技能被选中后引擎负责加载对应实现、校验输入参数、调用底层模型或外部 API、处理异常情况、返回结构化结果。这一层解决的技能怎么跑起来的问题。它隔离了上层编排逻辑与底层的具体实现细节。第三层是技能定义协议。这是连接上面两层的桥梁也是整个设计里最容易被低估的部分。每个技能都必须按照统一的协议来描述自己的输入输出和行为约束上层编排才能用一致的方式调用所有技能。如果没有一个清晰的协议技能再丰富也只是一个个孤岛。这三层之间的关系有点像操作系统里的文件系统。注册中心是目录执行引擎是文件读写驱动定义协议是标准文件格式。目录帮你找到文件驱动帮你读写内容格式标准保证任何程序都能识别。你可以替换驱动、更换格式但目录和文件的组织方式始终稳定。2.3 与主流 agent 方案的对比我知道很多人会问这和现成的 agent 框架有什么区别我拿几个主流方案做个对比方便你判断哪种更适合自己的场景。如果对比 LangChain 这类全功能框架agent-skills 的定位更轻。LangChain 提供了大量预构建的工具和链式封装开箱即用但也正因为功能多上手成本和学习曲线都比较高。agent-skills 其实不排斥这类框架你可以把它理解成一种更克制的组织方式核心关注技能的封装和复用而不是把所有功能都塞进来。如果对比 function calling 原生的方案agent-skills 多了一层抽象。原生 function calling 的每个函数是独立的模型通过结构化参数来调用这已经很接近技能的概念了。但实际项目中你会发现函数间的依赖关系、错误处理、重试策略、版本管理这些问题原生方案没有给出答案。agent-skills 的价值恰恰在于把这些工程问题纳入了体系。如果对比 AutoGPT 这类自主 agent 方案agent-skills 更强调可控性和工程性。AutoGPT 让模型自己规划并执行多步任务灵活度高但可预测性差容易失控。agent-skills 的思路是技能本身可以是灵活的工具但执行路径和编排规则是显式的、可审阅的适合对稳定性有要求的业务场景。所以我的建议很直接想快速验证想法用现成框架确实省事想让 agent 真正成为生产系统的一部分技能化这个思路值得你提前布局。两者并不冲突完全可以在现成框架的基础上按照技能化的思想来组织代码。3. 技能定义与协议设计3.1 一个技能到底包含哪些要素这是整个项目里最需要花心思的部分。我在反复踩坑后总结了一套自认为比较完整的技能要素清单你可以直接拿去做参考。一个标准的技能定义至少需要包含这些字段name技能全局唯一标识用蛇形命名法例如 search_product、submit_order。好名字本身就是文档能让人一眼看出技能职责范围。description一句话说清楚这个技能做什么、在什么场景下使用。这段描述不仅是给人看的还是给上层模型看的。实际经验是这段描述写得越准确上层编排调错技能的概率就越低。args_schema输入参数的 JSON Schema 定义。包括每个参数的类型、是否必填、默认值、取值范围、参数间依赖关系。这一点无比重要模型生成参数时经常出现格式偏差严格校验能挡掉大部分低级错误。output_schema输出结果的结构定义。我建议所有技能统一返回 JSON 格式哪怕内部实现是文本处理也包一层结构化的壳。这样上层消费数据时非常舒服。version技能版本号。技能逻辑更新后版本号必须递增。上层编排可以指定使用某个版本范围避免灰度期间出现不确定行为。dependencies技能运行所需的外部依赖包括依赖的其他技能、环境变量、外部服务等。显式声明依赖部署时不会漏配置。timeout技能的最大执行时间。如果一个技能经常超时需要在超时策略上做专门设计比如降级或异步化。error_codes自定义错误码列表。每个错误码对应一种可预期的异常场景上层编排可以针对不同错误码做差异化处理。这些要素看起来多但每一条我都建议认真对待。尤其是指标象 Schema 和错误码它们是技能能稳定运行的基础保障。3.2 协议描述一个具体例子光讲理论比较抽象我拿一个真实的查实时天气技能来演示协议应该怎么写。这个例子非常简单但足够说明协议设计的要点。首先是技能描述部分{ name: query_weather, description: 根据城市名称查询当前天气情况包括温度、湿度、风力、空气质量。适用于任何需要天气信息的场景。, version: 1.2.0, args_schema: { type: object, properties: { city: { type: string, description: 城市中文名称如北京、上海, minLength: 2, maxLength: 20 }, unit: { type: string, enum: [celsius, fahrenheit], default: celsius, description: 温度单位 } }, required: [city] }, output_schema: { type: object, properties: { city: { type: string }, temperature: { type: number }, humidity: { type: number, minimum: 0, maximum: 100 }, wind_level: { type: string }, air_quality: { type: string }, update_time: { type: string, format: date-time } }, required: [city, temperature, update_time] }, dependencies: { apis: [weather_service], env_vars: [WEATHER_API_KEY] }, timeout: 10, error_codes: [CITY_NOT_FOUND, API_KEY_INVALID, SERVICE_UNAVAILABLE] }注意几个细节minLength和maxLength限定了城市名的长度范围避免模型传入空字符串或超长乱码unit用了enum限定取值输出里humidity用minimum和maximum做了数值范围校验。这些约束看起来繁琐但如果让模型自由发挥错误率会让你怀疑人生。在写技能描述时description 部分要特别注意语气。我试过用不同风格描述同一个技能比如查询天气和获取指定城市的实时气象数据可作为旅行规划、户外活动安排的依据后者的被调用准确率明显更高。原因很简单描述越具体越能帮模型区分什么时候该用它。3.3 协议设计里的几个关键取舍技能协议不是越长越好也不是字段越多越好。设计时需要在完整性和简洁性之间找到平衡。我实践下来有三个最关键的取舍。第一个取舍是是否需要统一的输出包装结构。我之前见过一种做法所有技能输出统一包装成{ code: 0, data: ..., message: ok }。这样上层解析逻辑确实统一了但代价是每个技能的实现都得多包一层。我后来改成允许技能直接返回业务数据结构只有异常时才返回带错误码的结构。这样代码更清爽上层处理也没增加多少复杂度。第二个取舍是参数校验放在哪一层。最保险的做法是注册中心、执行引擎各校验一次双重保险。但实际项目里这会导致大量重复代码。我的建议是执行引擎做严格校验因为这是技能执行的必经之路注册中心只做基础格式校验保证目录可读即可。第三个取舍是是否需要支持技能嵌套调用。有些场景下一个技能内部需要调用其他技能比如制定旅游计划技能内部调用查天气和查机票。支持嵌套会让表达力更强但也让依赖管理复杂起来。初期建议先禁止嵌套所有组合都在上层编排完成。等你的技能库稳定了再考虑放开嵌套能力。我在项目里经历过一次教训某段时间为了追求架构优雅把所有技能都写成可以互相嵌套的结果出了问题时根本说不清是哪个环节出的错。后来强制平铺所有组合逻辑上收问题立刻清晰了很多。这个教训让我明白架构上的克制往往比架构上的炫技重要得多。4. 技能执行引擎的实现要点4.1 执行引擎的核心工作流技能执行引擎是 agent-skills 的心脏它负责把协议描述变成一个真正跑起来的调用。一个完整的执行流程大概经过这几个阶段第一步是技能定位。根据上层传入的技能名称和版本要求在注册中心里查出对应的技能定义。如果找不到返回技能不存在错误如果版本不匹配返回版本冲突错误。这一步看起来简单但版本过滤器写错的人不在少数。第二步是参数校验。按照args_schema的定义逐一校验输入参数。类型不对的要尝试转换缺必填的要直接报错枚举值不在范围内的要给出可选列表。我之前见过一个场景模型把北京传成了北京市如果 Schema 里没有做归一化处理这个调用就会以参数错误失败。后来我在校验前加了一层轻量归一化不再是简单报错而是尝试据上下文修正。第三步是加载执行器。每个技能背后都有一个执行函数不同技能的加载方式可能不同。有的技能只是调用一个本地方法有的需要启动一个子进程有的需要发起 HTTP 请求到外部服务。执行引擎要统一封装这些差异化行为对上层暴露一致的接口。第四步是执行并收集结果。调用技能实现同步捕获结果或异步等待完成。这里需要注意超时控制技能执行超过预设的超时时间时要主动中断而不是无限等下去。我记得有一个外部服务经常在高峰期变慢某次线上事故就是因为超时设置失效导致整个 agent 主流程被拖死。第五步是输出规范化。把技能返回的原始结果按output_schema的结构整理成标准格式。这一步通常不需要太复杂但要做防御性处理比如字段缺失时给默认值、类型不符合时做转化。第六步是异常处理。如果执行过程中出现了自定义错误码范围内的异常组织成标准错误结构返回给上层。如果出现未预期异常则归类为通用错误同时记录详细日志供排查。这六个步骤合起来就构成了一个技能调用的完整生命周期。每一步都有很细致的工程考量但最核心的原则只有一个执行的细节不应该泄漏给上层。上层只需要知道技能被成功调用了结果是这个或者调用失败了错误码是那个而这背后的重试、超时、降级都应该封装在引擎内部。4.2 参数校验的实战细节在六步流程中参数校验是最容易被低估的一步。写起来容易但真正的挑战在于模型的输出和真实世界的输入一样总是充满各种差不多但不一样的情况。我整理了几个高发问题每个都踩过坑第一个是类型混淆。模型传参时有时候数字是字符串格式比如温度传成了25而不是25。如果用严格类型校验直接拒绝用户体验很差。我现在的策略是如果 Schema 声明了 number 类型先尝试Number(value)转换转换成功则继续失败才报错。第二个是枚举值漂移。Schema 定义了unit只能是celsius或fahrenheit但模型可能传C、Celsius、摄氏度。这时候不能直接拒绝最好做一个模糊匹配表把常见的别名归一化到标准值上。这个小改动让我的技能调用成功率提升了接近 30%。第三个是必须参数缺失。有些参数模型确实没生成与其直接失败不如看看有没有合理的默认值。但这里的取舍要谨慎对于安全敏感的参数比如金额、数量即使缺省也要强行要求对于展示类的参数比如显示单位、排序方式就可以给默认值。具体哪些能补、哪些不能补在技能定义里标明。第四个是参数之间的依赖关系。比如输入地址时城市参数可不填但如果给了城市则必须给省。这类逻辑在 JSON Schema 里表达很别扭我通常会在执行引擎里单独加一个自定义校验函数专门处理这类关联约束。我的经验是参数校验的目的不是把错误挡死而是把错误变得更可理解。没有校验体系时模型生成的错误参数会一路传到业务深处最终暴露成一个晦涩的第三方接口报错有了校验体系后同样的错误会变成一个清晰的参数校验失败信息上层能够据此自动修正或优雅降级。4.3 超时、重试与并发控制执行引擎里最容易出问题的其实是超时和重试。这两块看似简单但设计不当会引发连锁故障。关于超时我的建议是三层设计。第一层是技能协议里声明的 expected timeout这是正常情况下的期望值。第二层是执行引擎的 hard timeout在这个时间点必须强制终止。第三层是全局熔断时间防止多个技能并行时整体耗时失控。单技能和全局之间的关系需要用动态控制去协调不能简单设置一个固定值。关于重试有个容易被忽视的原则不是所有失败都适合重试。如果错误码是参数校验失败重试再多次结果都一样属于不可重试错误如果错误码是服务不可用或网络超时则值得重试且重试间隔需要逐步增大避免雪崩。比如第一次失败后等 1 秒重试第二次等 3 秒第三次等 9 秒最多重试三次。这种指数退避策略虽然朴素但实测非常有效。关于并发控制核心是避免资源争抢导致系统整体性能下降。当一个 agent 在一次任务中需要同时调用多个技能时如果某个技能依赖外部限流接口并发量一大就会触发限流。我的做法是在执行引擎内部维护每个外部依赖的并发计数器超限时让新请求排队等待而不是直接并发打过去。有一次线上事故给我的教训很深刻。某个技能调用第三方服务对方限流是每分钟 100 次但我们某个时段业务量突然增长并发直接打到 300 次结果外部服务把我们整个 IP 封了连带影响其他技能也全部失败。后来做了并发控制和排队再也没出现过类似的问题。4.4 技能实现的两种形态聊完引擎来说说技能本身长什么样。我在实际项目中主要使用两种实现形态一种是基于大模型的提示技能另一种是基于代码的传统技能。提示技能的核心是一个精心设计的提示词模板。它利用模型的语言理解能力把自然语言输入处理成结构化输出。比如从用户评论中提取情感倾向这个技能本质上就是定义一个带输出格式约束的提示词让模型去跑。这类技能的优势是开发快、适应性强对格式不固定的输入特别有用。劣势是响应慢、成本高、结果有一定随机性。代码技能则是传统的软件实现用代码完成解析、计算、数据读写等确定性操作。比如计算订单运费这个技能输入商品重量和目的地按规则计算运费完全不需要模型参与。这类技能的优势是速度快、结果确定、成本极低。劣势是开发量大对灵活输入的处理能力弱。实际项目里90% 的技能应该是代码技能只有那些确实需要语义理解的场景才用提示技能。很多初学者喜欢把一切都丢给大模型看起来能力很强但在稳定性、成本和延迟上都吃亏。经过一段时间实践你会发现好 agent 的底层大部分是朴实无华但稳定可靠的代码。5. 上层编排与技能的组合逻辑5.1 编排层应该如何依赖技能层上层编排是 agent-skills 架构里最贴近业务的一层。它的职责是理解用户需求拆解成一系列技能调用按合理的顺序执行并把结果汇总成最终答案。但具体怎么实现差别非常大。我在早期犯过一个错误把编排逻辑也做成了一个大模型对话。每次用户请求进来把所有技能描述和历史对话一股脑塞给模型让模型自己返回要调用哪个技能。这在 demo 阶段效果惊艳但上线后问题成堆——模型经常选错技能、漏传参数、忘记多步任务之间的状态维护。原因很简单技能选择本质上是一个高确定性决策交给每次输出都可能不同的模型稳定性自然没保障。后来我改成了一种混合策略。把技能选择这个动作拆分成两条路径对于用户请求里可以直接映射到固定技能的场景走规则路由只有真正需要模型参与的任务拆分和复杂决策才让模型介入。比如用户说查询北京的天气这完全可以靠关键词直接映射到 query_weather 技能不用大模型参与。这样设计的好处有三个。第一是稳定简单场景下的行为完全可以预期。第二是省钱规则路由不消耗 token只在复杂场景下才调用模型。第三是可调试规则覆盖的场景出问题时你可以在代码里精确定位而不需要去审问模型为什么要这么做。5.2 制定调度策略的实战思路当一次任务需要调用多个技能时顺序和依赖关系就变得关键。我在实际项目里常用的调度策略有四种每种都有明确的适用场景。第一种是串行执行。任务里的技能之间有严格的先后依赖后一个技能需要前一个技能的输出。比如先查库存再下订单下单技能必须拿到库存查询的结果才能执行。这种策略实现最简单但总耗时是所有技能耗时之和。第二种是并行执行。任务里的技能之间没有依赖关系可以同时进行。比如同时查机票和查酒店两者互不干扰。这种策略能显著降低总耗时但要注意并发控制和资源限制。第三种是条件分支。根据前序技能的结果决定后续执行哪个技能。比如库存充足时走下单流程库存不足时走补货提醒流程。这种策略需要编排层具备状态判断能力可以用规则实现也可以让模型参与决策。第四种是循环重试。当某个技能的返回结果不符合预期时重新调用或换一个技能。比如用户问周末适合去哪玩查完天气发现下雨可以再触发一个室内活动推荐技能。这种策略让 agent 具备了一定的自主修正能力但要防止死循环一定要设置最大迭代次数。我建议初期不要把这些策略搞得太抽象。最简单的方式就是写一个流程编排函数里面用 Python 的条件和循环把技能调度串起来。等流程多了、复杂度上来了再考虑引入工作流引擎或状态机。过早抽象化只会让你在调试时多绕弯路。5.3 上下文如何跨技能传递多技能协作最麻烦的问题之一是上下文怎么在技能之间传递。我见过不少项目上一个技能的结果好不容易存下来下一个技能又找不到了最后整个 agent 变成金鱼记忆。目前业界比较常用的方案是维护一个全局上下文对象。比如一个名为 context 的字典里面存放当前任务的关键信息用户意图、历史对话、各个技能的中间结果、已执行的步骤列表等。每个技能执行后把产出写入 context后续技能执行时从 context 里读取它需要的输入。但全局上下文有个隐患它把所有信息都堆在一起技能可能读取到不该读的数据导致难以排查的副作用。我的做法是给上下文分域管理。最外层是全局上下文存放所有任务共享的基础信息每一轮技能调用再创建一个局部上下文只包含该技能应该看到的信息。技能执行完把需要的产出提升到全局上下文把临时数据丢弃。用生活类比解释一下全局上下文就像办公室的公共文件夹大家都能看局部上下文就像自己的工位抽屉只有自己知道里面放什么。跨技能传递信息就是你把抽屉里的文件整理好放到公共文件夹里给别人取用。这个比喻虽然简单但记牢了就会少犯很多上下文污染的问题。5.4 状态管理与任务进度追踪如果 agent 执行的是一个耗时较长的多步任务状态管理就变得绕不开。用户点了一个帮我规划三亚五天四晚行程你可能需要依次调用查航班、查酒店、查景点、查天气四五个技能整个流程可能耗时几十秒甚至更多。这种情况下编排层必须做到两件事。第一是给用户实时反馈告诉用户当前执行到哪一步了第二是允许用户在中间取消或修改任务。如果这两点做不到用户会以为 agent 卡死了体验极差。我的做法是引入一个简单的任务状态机。每个任务实例维护一个状态字段取值为 pending、running、waiting_input、success、failed、cancelled。编排层每执行完一个技能就把任务推进到下一个状态同时把进度信息推送给前端。如果某个技能需要用户补充信息任务停在 waiting_input并不会占用执行资源。这个状态机看起来很简单但它让整个系统变得可观察、可控。用户能看到自己的任务走到哪了开发者也能通过状态分布来排查性能瓶颈。有一次线上反馈说某个任务总是失败我通过状态机日志一看原来是它在 etcd 状态更新的时候有一处重复提交修掉后问题立刻消失。如果当时没有状态管理这个问题可能要排查很久。6. 实操过程与完整案例演示6.1 从零搭建一个技能化 agent理论讲了不少接下来带你从零走一遍完整的搭建流程。我准备了一个极简但五脏俱全的示例做一个旅行小助手它能够根据用户的需求调用查天气和查景点两个技能组合成一个完整的行程建议。第一步是创建项目结构。按我的习惯目录组织长这样travel_agent/ ├── skills/ │ ├── __init__.py │ ├── query_weather.py │ └── query_attractions.py ├── engine/ │ ├── __init__.py │ ├── registry.py │ ├── validator.py │ └── executor.py ├── orchestrator/ │ ├── __init__.py │ └── travel_planner.py ├── schemas/ │ ├── weather.json │ └── attractions.json └── main.pyskills/目录存放每个技能的实现engine/存放执行引擎的公共逻辑orchestrator/存放上层编排逻辑schemas/存放技能协议定义。这种目录规划的目的是让每层代码的边界一眼可辨。就算项目大了也能很清楚地知道新代码应该加在哪个目录下。第二步是定义协议 Schema。先给查天气技能定义一份。这是刚刚已经展示过的样例直接拿来用即可。再给查景点技能定义一份输入是城市、兴趣标签可选输出是景点列表。第三步是实现技能代码。以query_weather.py为例def query_weather(city: str, unit: str celsius) - dict: # 这里省略真实天气 API 调用用模拟数据代替 data { city: city, temperature: 26 if unit celsius else 79, humidity: 40, wind_level: 3级, air_quality: 良, update_time: 2025-03-15T12:00:0008:00 } return data实际项目中这里会发起 HTTP 请求到天气服务商并且在异常时抛出自定义错误码。但代码结构是一样的接收参数返回结构化 dict错误时抛异常。第四步是注册技能。在注册中心里加载所有技能定义# engine/registry.py SKILL_REGISTRY {} def register_skill(skill_def): name skill_def[name] SKILL_REGISTRY[name] skill_def def get_skill(name, versionNone): skill SKILL_REGISTRY.get(name) if not skill: raise SkillNotFoundError(f技能 {name} 不存在) if version and skill[version] ! version: raise VersionMismatchError(f技能 {name} 版本不匹配) return skill第五步是实现执行引擎的入口。执行引擎是核心但代码量反而不大# engine/executor.py def execute_skill(name, args, versionNone): skill_def get_skill(name, version) validate_args(skill_def, args) # 参数校验 impl load_implementation(name) # 加载技能函数 result run_with_timeout(impl, args, skill_def.get(timeout, 10)) return normalize_output(skill_def, result)第六步是写编排逻辑。旅行助手的核心编排函数如下# orchestrator/travel_planner.py def plan_trip(city: str, days: int, interest: str None): context {city: city, days: days} try: weather execute_skill(query_weather, {city: city}) context[weather] weather except SkillError as e: context[weather] None context[warnings].append(f天气查询失败: {e.error_code}) try: attractions execute_skill( query_attractions, {city: city, interest: interest} ) context[attractions] attractions except SkillError as e: context[attractions] [] context[warnings].append(f景点查询失败: {e.error_code}) # 生成最终建议 suggestions build_suggestions(context) context[suggestions] suggestions return context这段代码体现了几个要点技能调用都放在 try-except 里单个技能失败不会拖垮整个任务执行顺序是串行的因为天气和景点查询互补干扰但又需要同时展示错误被转换成警告信息而不是直接让整个 agent 崩溃。第七步是写一个简单的命令行入口跑通整个流程。到这里你的第一个技能化 agent 就算落地了。6.2 技能注册中心的完整实现注册中心是整个架构里最容易被忽略、但实际价值非常高的组件。除了基础的登记和查询它还承担技能发现、版本管理和健康检查的职责。技能发现指的不是简单的字典查找而是支持条件查询。比如上层编排想知道所有能接收城市参数并返回天气信息的技能有哪些注册中心应该能按描述和参数 Schema 做模糊匹配。我用最朴素的方式实现了这个功能遍历所有技能定义用关键词匹配 description 和 args_schema 的字段名。虽然简单但已经能满足大多数场景。版本管理需要支持多个版本共存。技能迭代是常态但正在跑的编排流程可能还在用旧版本。我的做法是注册中心允许同一个技能注册多个版本查询时默认返回最新稳定版也可以按指定版本查询。新旧版本的切换靠编排层显式指定不会在运行中自动升级导致行为突变。健康检查则是对技能的基本可用性做监控。直接在技能进程内部通过探活机制对每个外部依赖做连通性检测。这样在上层调度之前就能提前知道哪些技能当前不可用避免无效调用浪费时间和成本。我见过一些团队把注册中心做得特别重甚至引入了完整的服务发现框架。其实对大多数应用场景来说一个字典加上几个查询函数就够了。过早地引入分布式服务治理只会让这个阶段的问题更加复杂。6.3 模型如何调用技能的完整链路说完编排侧再看模型侧。在很多 agent 架构里模型是技能的调度者之一。当模型需要调用技能时完整的链路通常是这样的第一步是组装工具列表。模型接口通常要求传入所有可选工具的 JSON Schema 描述。这个列表可以从注册中心动态生成避免手工维护一份和注册中心不一致的副本。我用自动化生成的方式从注册中心的技能定义转换成模型要求的结构格式。第二步是发起模型推理。模型根据用户请求和工具列表返回一个结构化调用请求里面的内容是需要调用哪个技能参数是什么。这一步要保持请求结构稳定模型才会越用越顺手。如果你频繁修改工具定义的结构模型的输出质量会明显下降。第三步是由执行引擎接管执行目标技能。注意这个执行不是模型在控制的而是由系统执行的。模型只负责决定不负责行动。第四步是返回执行结果给模型。把技能的结构化输出塞回对话上下文中让模型基于结果继续推理直到生成最终回复。整个链路的关键点是模型永远不直接触达真实的业务系统。它只发出调用指令真正的副作用都经由执行引擎完成。这样做的好处是即使模型被诱导输出恶意调用指令执行引擎也能通过参数校验、权限控制、异常拦截来保护系统安全。这条链路是 agent 工程化的重要底线。6.4 手工配置和动态注册的两种玩法技能注册有两种方式对应不同的部署场景。第一种是手工配置。所有技能定义预先写在代码里或者配置文件里服务启动时一次性加载。这种方式最适合代码库和技能集耦合比较紧密的小团队。优点是非常直观代码审查就能看到技能变更。缺点是不够灵活技能上线需要重新发布。第二种是动态注册。外部系统通过管理接口把技能定义实时注册到注册中心不用修改代码。我通常提供独立的管理接口支持新增、更新、下线技能。这种方案适合多团队协作或技能经常变化的大系统。但要注意权限控制不能让人随意注册技能否则注册中心会被垃圾定义污染。我的建议是初期先用手工配置。技能数量少开发和维护成本都低而且每加一个技能都要重新发布一次反而能倒逼团队认真审视技能定义。等到技能超过一定量级再引入动态注册机制并且配套严格的审核流程。我自己是从手工配置开始的中间一度觉得不够高大上想改成动态注册但后来想想几乎只有我一个人在维护动态注册带来的灵活性根本没有用武之地反而多了不少权限管理和一致性维护的负担。技术选型不是越先进越好适合当下的复杂度和团队规模才是最好的。7. 常见问题与排查技巧实录7.1 模型选错技能的四大原因在做技能调用的过程中最让人头疼的问题就是模型调用错了技能明明用户问的是天气它非要去查景点。我排查了无数次最终总结出四个主要原因。第一个原因是技能描述写得不够精确。描述里的业务术语含糊不清模型就容易混淆。比如query_weather和query_climate的描述都写得像查询天气模型自然会随机选一个。排查方法很简单把注册中心所有技能描述拉出来通读一遍看你是否能一眼分辨出每个技能的差异。如果连人都分辨不出模型更不可能。第二个原因是技能数量太多模型注意力分散。当一个调用请求里塞了几十个技能的描述模型的注意力会被分散选错概率会急剧上升。我的建议是不是所有技能都需要一次性暴露给模型。你可以根据用户意图做预筛把候选技能控制在五六个以内。减少模型的选择负担就是提高准确率。第三个原因是参数描述不清晰导致模型将错就错。有些技能名和参数名对不上比如一个函数叫 submit_refund参数却叫 reason_code模型容易误解成提交原因。定期检查技能参数命名与描述的一致性能减少很大一部分选错概率。第四个原因是上下文误导。模型在做技能选择时会受到用户历史消息的影响。比如用户之前聊过酒店现在突然问天气模型可能带着用户想住的酒店附近天气这种猜测去调用技能。比如额外加一个前置步骤让模型先明确当前用户查询的目标城市再进入技能选择能有效降低这类错误。7.2 技能执行超时的三类诱因技能超时是线上高频问题我把它归纳成三类诱因每一类的处理方式不太一样。第一类是外部服务响应慢。这是最常见的情况比如天气服务偶尔在整点访问量大时变慢。处理方式是超时重试并配合指数退避。如果连续重试仍失败就降级到备用技能或者返回缓存数据。第二类是技能内部逻辑复杂导致执行时间长。比如一个技能要处理一个特别大的列表代码占用了大量 CPU 时间。这属于技术债需要重构算法或拆分子技能。排查方法是用性能分析工具找出时间消耗在哪一段。第三类是执行引擎本身的问题比如线程池被占满、锁竞争严重、等待队列过长。这类问题通常在高峰期暴露特征是对所有技能都超时而不是个别技能超时。解决办法是检查引擎的并发配置看是否需要扩容或优化锁粒度。我在一次复盘里发现某段时间所有技能的超时率同步飙升第一反应以为是外部服务出问题了仔细一查才发现是引擎里某个共享连接池用完了。换掉那个连接池的默认配置后超时率立刻回到正常。我想说的是排查的时候别只盯着技能本身引擎底层的资源瓶颈也可能是真正元凶。7.3 常见问题速查表我把平时在技能化 agent 开发中最高频的问题整理成了一个速查表方便你直接对照排查。问题现象可能原因排查方法解决方案模型总是调用某个固定技能技能描述太泛模型被误导对比技能描述与用户典型请求精简描述加入使用场景限制参数缺失导致技能调用失败模型没有生成完整参数查看模型原始返回内容在提示词中强调参数必须完整必要时做参数补全技能返回结果格式混乱输出 Schema 定义不严查看技能实现返回的原始数据严格定义输出 Schema增加校验多个技能并发调用时互相阻塞引擎并发控制失效查看引擎线程池状态调整并发策略限制外部依赖的并发上限技能更新后行为变化线上无法定位版本管理不规范查看注册中心版本记录强制递增版本号编排层锁定版本范围技能调用链路偶发失败外部服务不稳定查看重试日志增加重试和熔断策略新部署的技能无法被发现注册中心未加载检查注册中心的加载流程统一注册入口确保启动时自动加载7.4 避坑指南日志、测试与版本管理最后分享三个在长期实践中才体会到价值的接地气经验。第一日志要结构化。技能调用产生的日志不能只是纯文本建议统一输出成 JSON 一行一行的结构。里面至少要包含技能名、版本、参数摘要、耗时、结果状态、错误码、链路 ID。有了结构化的日志你才能在出问题时快速聚合分析。我排查问题第一步永远是按链路 ID 检索所有相关日志能把问题范围缩小到某一个环节。第二测试要分层。第一层是单元测试针对每个技能的核心逻辑不依赖外部环境用 mock 数据跑。第二层是集成测试把技能运行时的真实依赖连接起来确保环境配置没有问题。第三层是契约测试重点是验证协议定义与实际调用之间的匹配度。很多问题不是代码错而是协议和实现不一致这种测试能接住那一类错误。第三版本控制要严肃。技能的版本号不只是标记它还是线上安全的一道保险。我见过太多次一个工程师改了技能逻辑后没有升版本结果灰度流量把新逻辑带到了全量用户出了事故才被发现。建议从一开始就养成习惯任何技能行为变化版本号必须递增编排层引用的版本范围必须收紧不要随便用 latest。再说一个我在日志里发现的有趣规律大部分低级错误其实都是在一开始没做好参数类型校验导致的。表面上看起来是模型笨实际是系统给了模型太多自由发挥的余地。当你把边界收紧、约束写清楚模型的笨会立刻少很多。这倒不是说模型变聪明了而是你的系统让模型犯错的空间变小了。8. 实战复盘与进阶建议8.1 一次完整项目落地的复盘最后拿我前段时间帮某公司做的一个模拟项目来复盘。这个项目的目标是把一个原先全靠规则匹配的售后客服系统升级成带 agent 能力的智能客服。项目初期最费时间的不是写代码而是梳理业务流程。售后场景很杂查物流、退款、换货、补差价、开发票……每一种背后都有不同的后台系统和操作规则。我们第一步做的事很简单和业务方连续开了几次梳理会把售后场景里所有可能涉及的操作列成清单逐个确认输入输出。这个阶段大约是两周时间。之后我们把清单里的每一项操作设计成了标准技能。比如 query_logistics、submit_refund、query_refund_status、exchange_product、send_invoice 等等统一定义 Schema统一注册。这里的关键体验是业务方看到的是清晰的技能清单技术方看到的是可实现的模块边界双方用同一份协议做协作语言减少了大量需求理解偏差。然后是执行引擎的搭建和编排逻辑的开发。因为技能已经封装好了编排层的代码其实很薄。核心就是把用户问题和技能选择映射起来然后串起调用链。我们仍然采用规则路由优先模型兜底的策略。简单的物流查询、退款进度这类高频场景直接走规则遇到语义模糊或者需要综合判断的才让模型介入。上线后的第一个月系统运行很稳定但我们也观察到一些细节问题。最典型的是用户喜欢在一个请求里带多个子任务比如我的快递到哪了如果要退的话怎么弄。这条意图如果只映射一个技能就把用户的第二个需求漏掉了。后来我们在编排层加了意图拆分模块先判断用户请求是否包含多个原子意图拆开后再分别走技能调用。这个改动让系统的满意度提升了一个台阶。另一个复盘教训是关于权限控制的。售后场景里退款操作涉及资金变动绝不能因为模型误判就自动执行。我们在退款技能外面加了一道人工确认闸口模型根据用户描述判断退款原因生成退款方案但真正执行前必须推送给客服人员审批。这个设计保住了系统底线也帮团队建立了长期信任。8.2 进阶方向一让技能具备离线评测能力当你积累了多个技能后你会发现一个更高频的需求如何知道技能改得好不好。靠手工体验基本不可行需要建立自动化的评测体系。我的做法是给每个技能准备一批评测用例覆盖正常场景、边界场景、异常场景。正常场景验证正确输出边界场景验证入参极端情况下不崩溃异常场景验证失败时错误码是否符合预期。技能改版时跑一遍评测集对比新旧版本在不同场景下的表现很快就能定位出改动带来的负面影响。更进一步可以把评测和模型对技能的调用准确率绑定。收集真实用户请求看模型是否正确选择了技能是否正确填充了参数。这些数据比任何主观评价都更有说服力。我甚至见过有团队把评测结果做成了一个计分板每次技能改动上线前必须达到一定分数否则不允许发布。这套离线评测能力和技能化架构是天然互补的。技能定义只要稳定下来评测用例也就稳定了。如果每次改的都是大段的流程逻辑评测用例可能跟着频繁变动投入产出比就没这么高了。8.3 进阶方向二技能间的依赖与共享治理技能数量超过几十个以后你迟早会遇到依赖治理问题。比如退换货技能依赖查订单技能而查订单技能又依赖用户鉴权技能。一旦底层技能接口变更上层技能就可能跟着挂。我的建议是建立依赖矩阵明文记录每个技能依赖哪些其他技能或外部服务。每次接口变更前先查依赖矩阵评估影响范围。必要时引入契约测试在构建阶段自动跑一遍所有依赖技能的关键场景确保这次变更不会破坏下游。另一个常见问题是共享技能的权限差异。比如查订单技能普通用户能查自己的订单客服人员能查任意订单管理员还能查跨部门的订单。同一个技能在不同上下文里的权限边界完全不同。我的做法是技能本身不感知权限权限控制放在编排层。技能只管查订单这个动作而你能查谁的订单由上层根据用户身份和角色决定再以参数或凭证的方式传给技能。这个解耦非常关键否则技能层就会变成权限判断的大杂烩维护成本不可控。8.4 进阶方向三跨项目复用技能库最后再说一个常常被忽略的点技能库是可以跨项目复用的。我在不同项目里都用过查天气发短信查订单这类通用技能把它们的协议定义和实现沉淀下来放到一个独立仓库里统一维护新项目接入时直接引依赖即可。但跨项目复用也有陷阱。最怕的是为了复用而强行把两个场景融合结果技能逻辑被各种配置项塞满越来越难维护。我的原则是通用技能只保留真正通用的能力项目专用逻辑通过参数或子技能扩展来实现。如果某个技能被塞进了十来个专有参数大概率是它已经承担了太多不属于它的职责该拆分了。对想要上手的读者我的建议很直白不要一上来就想着建一整套庞大的技能库。先挑一个你熟悉的小场景比如做一个查快递的小助手把查快递这一个技能走通定义协议、实现代码、注册、编排调用、测试上线。这一圈走完你对技能化的感受会比看任何教程都深刻。之后每遇到一个新需求都习惯性想想这是不是一个独立技能能不能复用已有技能。坚持半年你的 agent 项目会越来越有条理越来越抗变化。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。