从去年开始,我一直在折腾AI Agent相关的项目,前前后后做了好几个原型,踩过的坑能装满一卡车。说实话,大多数Agent项目半途而废的原因,不是模型不够聪明,也不是Prompt写得不好,而是压根没想清楚"Agent会什么、不会什么"这个问题。最近我把手头的模拟项目X重构了一版,核心就是把原本散落在各处的工具调用逻辑,统一收拢成一个结构化的技能系统——也就是我内部的agent-skills方案。
这个方案解决的核心痛点很直接:当Agent需要完成的任务复杂度上来之后,你不可能把几十个工具说明一股脑塞进System Prompt里,也不该在代码里写一大堆if-else判断该调哪个函数。技能系统的本质,是把"能力"这件事从业务逻辑里抽离出来,让Agent具备更清晰的自我认知——它知道自己会什么,也知道什么情况下该用什么。这篇文章不聊理论概念,就聊聊我在实际设计、实现和调试这套技能系统时积累的实操经验,包括整体架构怎么搭、技能怎么定义更合理、编排层需要解决哪些问题,以及哪些地方特别容易踩坑。适合正在做Agent落地、对工具调度和多步骤任务编排刚需的朋友,当然,如果你刚开始接触Agent开发,这篇同样值得一读,至少能帮你避开我在最初几版草稿里走过的弯路。
1. 技能系统的整体设计思路:从工具列表到能力地图
1.1 先捋清楚一个前提:工具、技能、任务这三层到底啥关系
很多人在刚开始做Agent工具接入的时候,脑子里只有"工具"这一个概念:查天气是一个工具,发邮件是一个工具,解析PDF也是一个工具。然后呢?然后就没有然后了——当Agent涉及的任务变成"帮我调研一下行业趋势,整理成报告,再发到团队群里",单个工具就不够用了,你需要的是组合拳。
我第一次做这类系统的时候,就把工具和技能混为一谈了。当时的架构里只有工具注册表,Agent用ReAct模式去调用工具,每个工具都是一个函数加上一段描述。问题出在哪呢?当一个任务需要五六个工具协作完成时,Agent在每一步都需要重新推理该调哪个工具,步子迈得又碎又多。后来在重构模拟项目X时,我把架构改成了三层结构:
- 工具层(Tool):最原子的执行单元,比如"发送HTTP请求""读本地文件""执行SQL查询"。工具不关心业务语义,只关心"我能干嘛"。
- 技能层(Skill):工具的编排组合,面向具体场景,比如"搜索并整理资料""生成周报""查询并汇总订单数据"。技能层已经具备业务语义,知道完成这个场景目标需要哪几步、每步调什么工具、失败怎么回退。
- 任务层(Task):用户发过来的一个完整目标。任务由LLM理解之后,分解为一系列技能调用。
把技能从工具里掰开之后,Agent的推理负责变轻了。它不再需要关心"查询订单数据要用哪个API、分页参数是什么、返回结果怎么清洗",这些已经被技能内部消化掉了。Agent只需要说"我要调用订单汇总技能",然后等待技能层返回结果。这个思路其实很像真实团队的分工:老板只需要说"把这事儿办了",至于怎么办、分几步、用什么资源,是执行者的事情。
1.2 为什么技能化能明显改善Agent的稳定性?
我在对比测试里发现,同样一个多步骤任务,用裸工具版和技能版跑同一组测试用例,成功率差距大概在20到30个百分点。技能版更高的原因其实不难理解。
第一个因素是上下文节约。System Prompt里塞的工具越多,模型对每个工具的注意力就越分散,选错工具的概率就越高。技能化之后,Prompt里只放技能清单及其概括性描述,每个技能再在内部管理自己的子工具。这意味着模型在每一步决策时,面对的不是三十个平权的工具,而是五六个高层次的技能。就好比你给下属安排工作,一次交代三件事,比一次交代三十件事靠谱得多。
第二个因素是容错能力的封装。工具层的失败是高频的,比如某个外部接口突然超时、返回格式不符合预期、涉及权限校验失败。如果没有技能层做兜底,Agent很容易在某个失败节点上原地打转,反复试同一个错误路径。而技能层可以在内部实现重试、降级和替代路径,实在不行再把一个结构化的错误信息抛给Agent。我在技能层里加了重试策略和超时熔断之后,整体的端到端成功率又涨了差不多10个点。
第三个因素是可观测性。技能层天然成一个执行单元,在执行过程中可以埋点、打日志、记录耗时和关键中间态。出了问题,直接定位到是哪个技能卡了壳,不需要在工具调用的流水账里翻半天。说实话,Agent调试本来就够头疼了,有一个方便的观测入口,省了非常多精力。
1.3 设计这套系统时,我给自己定的几条硬约束
在动手写代码之前,我先把设计约束列了个清单,免得写着写着失控。
第一条,技能必须无状态。技能内部不持有跨请求的记忆,同一个技能在一百次调用里行为应该是可预测的。如果有需要跨步骤保存的数据,一律放到任务上下文里。这条约束让我后面做并发和重试时省了不少心。
第二条,技能的输入输出必须结构化。不要让技能接收"一句话描述"这种自由文本,至少是一个JSON Schema定义的参数对象。模型尽量从任务上下文里取出结构化参数传给技能,技能的返回值也尽量用JSON而不是自由文本。这样做的理由很直接:结构化数据方便后续技能编排层做路由和判断,也方便人眼排查。
第三条,技能之间禁止相互调用。技能只能被编排层调用,不能A技能内部调B技能。这个约束一开始看着有点死板,但实践下来非常值得。如果允许技能间互相调用,很快就会出现调用链过深、循环依赖和隐式耦合的问题,最终谁也说不清一次任务到底执行了什么。通过编排层来组合技能,调用关系永远是清晰的、有向的、可审计的。
这三条约束贯穿了整个重构过程,后面所有步骤都是在它们的前提下一路推进的。
2. 技能定义与注册:怎么让模型和代码都看懂你的技能
2.1 技能清单文件的Schema设计
技能系统的第一步,不是写代码,而是设计一个技能清单文件。我用的是纯JSON格式的清单,目的很简单:人可以读,模型也可以读,同时还能让Builder在加载时做静态校验。下面是我在模拟项目X中实际使用过的Schema结构,你可以直接参考或者在此基础上裁剪。
{ "name": "weekly_report_generator", "description": "根据用户提供的关键数据和目标方向,生成结构化的周报文本", "version": "2.1.0", "author": "core-team", "tags": ["report", "weekly", "office"], "input_schema": { "type": "object", "properties": { "start_date": { "type": "string", "description": "统计起始日期,YYYY-MM-DD格式" }, "end_date": { "type": "string", "description": "统计结束日期,YYYY-MM-DD格式" }, "focus_areas": { "type": "array", "items": { "type": "string" }, "description": "重点关注方向,比如研发进度、市场动态" } }, "required": ["start_date", "end_date"] }, "steps": [ { "run": "extract_metrics_from_database", "args": { "date_range": "{start_date}~{end_date}" }, "output": "metrics_data" }, { "run": "retrieve_previous_report", "args": { "target_week": "{end_date}" }, "output": "prev_report" }, { "run": "synthesize_report", "args": { "metrics": "{metrics_data}", "reference": "{prev_report}", "focus": "{focus_areas}" }, "output": "final_report" } ], "timeout_seconds": 30, "retry_policy": { "max_attempts": 3, "backoff_base_seconds": 1.5 } }这套Schema里几个关键字段我说一下设计思路。
input_schema是给模型看的。Agent在决定要不要调这个技能时,会拿当前的任务上下文和这个Schema对比,确认参数是否齐活。所以我要求每个字段都要写清楚description,尤其是格式要求,比如日期格式、单位、边界条件。我还遇到过模型把时间传成"下周"的尴尬情况,后来在description里明确"必须是YYYY-MM-DD格式"就再没犯过。
steps是技能的执行蓝图。它定义了这个技能内部要依次调用哪些子操作,并把前一个步骤的输出作为后一个步骤的输入。每个步骤都可以从技能入口参数或者前面步骤的输出中取数据,用的是花括号模板语法。这套设计本质上就是一个小型的、领域特定的工作流,学起来几乎没有成本。
retry_policy是技能层容错的一部分。当技能内的某一步失败时,按策略退避重试,避免无脑重试加重外部系统压力。参数怎么定我后面在实操章节细讲。
2.2 技能的Registration机制:构建期做静态校验
有了技能清单文件,还只是"描述"。真正的关键在Registration(注册)阶段,也就是把描述文件转换为可供运行时调用的技能实例。我抽了一个独立的Builder组件来处理这件事,它做了三件事。
第一件事是解析和校验。加载JSON文件,用JSON Schema校验器检查字段完整性。有没有缺description、steps是否为空、参数引用是否对得上。这一步能挡掉大部分低级的配置错误。我最初没有做校验,结果上线之后发现有个技能文件里steps引用了不存在的输出变量,运行时才爆出来,排查了半天。自那以后,任何技能文件变更必须过本地校验,跑不过直接拒绝合并。
第二件事是绑定执行器。技能里的每个step需要对应一个实际的可调用函数。Builder内部维护了一个注册表,从step名字映射到具体的处理函数。如果在数据库里查不到对应的函数,Builder会在启动阶段就报错。这是我非常喜欢的一个特性:注册阶段的问题早发现,好过运行时才暴露。
第三件事是构建技能清单索引。Builder把所有已注册的技能汇总成一份索引,生成模型可读的列表。这份列表后续会被注入到System Prompt中,或者存储在Vector Store里供检索。索引的格式也经过精心设计,每个技能压缩成一行摘要,包含名字、标签、一句话描述、核心参数说明。这样模型做技能选择时,输入到attention里的东西非常干净。
Builder还支持按需动态注册技能。比如说有一个技能是实验性的,不需要参与线上正常的调用流程,那可以用单独的注册入口加载,由一个标记位控制是否对其可见。
2.3 技能选择的策略:是硬路由,还是让模型自己挑?
技能都注册好了,Agent拿到一个任务目标之后,该选哪个技能呢?我在实践中试过三种方案,每种都有明确的适用场景。
第一种,硬编码路由。就是代码里写死,什么任务关键词匹配哪个技能。这种方案最稳,但也是最死的。一旦有新技能加入,或者任务的表述改了,就需要改代码重新发布。它适合那种场景极其固定、业务极稳定的情况,比如后台系统里"给你订单号,返回订单详情"这种铁板一块的场景。
第二种,全量推理选择。把所有技能描述一股脑塞给模型,让模型从中挑出需要调用的技能。这种方案是零成本起步,但正如前面说的,技能数量一多,选择准确率就会下降。我实测下来,超过15个技能之后,准确率的滑坡就比较明显了。所以它更适合小规模的Agent原型验证。
第三种,检索辅助选择。把技能描述向量化放入向量库,任务来了先用语义检索捞Top-K个候选技能,再让模型在这K个里做精排。这套方案混合了确定性和灵活性,是我最后采用的方案。它的额外成本是维护一个向量索引,但对技能数量超过20个的系统来说,这个成本非常值得。我在实际项目中,K取值是5到7,效果比较稳定。如果K太小,容易漏掉冷门但正确的技能;K太大,又回到了全量推理的注意力度量问题。
三种选择策略没有绝对优劣,关键看你的技能规模和场景稳定性。我给的建议是:如果你现在技能只有六七个,老老实实用第二种;等数量上了两位数,再花时间把第三种落地。
3. 实操过程:技能编排层与多技能协作的落地实现
3.1 编排层:串行、并行还是动态规划?
技能编排层面是我整个重构里花时间最多的地方。它要回答的问题很简单:一个复杂的任务如果有多个技能参与执行,该以什么顺序、什么方式来组织它们。
第一种是串行编排,适合步骤依赖明确的场景。比如"先取数,后生成报告",顺序天然固定。我在Schema上用chain来描述,编排引擎按顺序依次调用技能,前一个技能的输出缓存到上下文,供后一个技能的参数填充。这是最多出现的情况,也是最容易实现正确的路径。
第二种是并行编排,适合步骤之间互不依赖的场景。比如"同时去查竞品动态和内部销售数据",两边没先后关系。并行能显著缩短整体耗时,但我需要额外处理的是资源控制和超时。我在技能层给每个技能设了timeout_seconds,并行执行时还会考虑全局的超时预算,防止一个卡住的技能拖死整个任务。这里要重点说一个容易被忽略的问题:多个技能并发往同一个外部系统打请求时,可能触发对方的限流。所以我在并行编排模块里增加了并发上限的配置项,默认是3,实测对接大多数内部服务时比较相安无事。
第三种是动态规划编排,就是让LLM来决定技能的组合顺序。这种方案最灵活,但同时也最不稳定。我在模拟项目X里做了一个折中——只允许LLM在特定的有限集合内做动态编排,而且限制编排深度不超过三层。超过三层就让Agent向用户澄清需求,而不是自己在黑暗中瞎闯。
实现的时候,编排引擎的核心是一个简单的状态机。每个技能调用节点有pending/running/succeeded/failed四种状态,引擎负责推进、记录日志、处理错误。上层的记忆模块负责维护一个执行轨迹列表,每一步的输入输出摘要都会写入,在后续的Prompt构造时拼装进去。
3.2 上下文管理和Prompt拼装:别让记忆无限膨胀
技能多了,执行步骤就多了,每一步产生的输出如果不节制,上下文的Tokens消耗会爆炸。我在实际项目里跑了一段时间后发现,Prompt里塞的东西越多,模型不但更慢,任务执行的准确率还会下降。所以我制定了几条上下文管理的铁律。
第一条,只保留摘要。每个技能返回的完整结果写入内存存储,但拼装给模型的只保留摘要。比如数据统计结果,摘要就是"共47条记录,总金额827123.56元,平均单笔17601.55元"。模型决策通常只需要这些概要信息,细节如果需要再按需取用。
第二条,窗口裁剪。当上下文字符数超过预设阈值时,把最久远的步骤摘要优先压缩。压缩有两种策略,一是简单截断,二是让模型把旧步骤重写成一个极简的总结句。第二种效果好,但要多花一次LLM调用,所以我的默认策略是先用截断,实在需要完整历史时再触发重写。
第三条,关键状态单独维护。任务的目标、用户约束和中间产物清单这几个状态,绝对不能被窗口裁剪掉。它们单独放在上下文的固定区域里,锚定不变。这样即使早期细节被压缩掉了,Agent依然知道任务的方向和关键限制条件。
在Prompt拼装层面,我准备的模板分为几段:系统角色、技能说明、上下文摘要、执行轨迹、用户请求。整个Prompt像是一个不断更新的状态卡,Agent在每一轮决策时看到的就是这份状态卡加上可用的技能列表。实验下来,这种结构化Prompt比一股脑倒历史记录的效果好了不少。
3.3 参数填充细节:模板插值与数据校验
编排引擎调用技能前要做的最后一件事,是参数填充。我踩过一个很真实的坑:技能清单里的input_schema定义得很完整,但实际传参时有些字段是从上下文里抽取的,抽取结果不稳定。比如用户说"上周的数据",模型理解为某个日期范围,但不同轮次的推理结果可能有出入。
后来我增加了三层校验。第一层是格式化校验,按照schema里的type和format规则检查。第二层是业务规则校验,这是技能的专属逻辑,比如"结束日期不能早于开始日期""查询数量上限1000条"。第三层是合理性校验,把参数值放在任务上下文里看是否合理。举例来说,任务明确说只关心华东区域,而技能参数里出现华北区域,就需要报给编排层复核。这套校验流程走下来,很多不合理的技能调用发生在真正执行之前就被拦下了。
模板插值也有个细节。技能的steps参数里用花括号引用变量,最常遇到的坑是花括号和JSON本身的语法冲突,导致解析异常。我把模板引擎换成了更可读的命名变量方式,用{{param_name}}这样的格式,并且转义了JSON中可能出现的花括号位置,才把这个坑填平。
4. 常见问题与排查技巧实录
4.1 技能命中率低:是描述的问题,还是检索的问题?
我遇到过的最普遍的问题,就是Agent在遇到该用某技能的场景时,没有选到那个技能。最开始的排查思路是从检索工具入手,实际上大部分时候问题出在技能描述上。
技能描述有两种典型错误。一种是太抽象,太像技术文档。例如"execute_order_fetching_logic",对于人类开发者来说没问题,但模型实际上得做一层推断才能理解它跟"查订单"之间的关系。更好的做法是直接写业务场景化的描述:"根据订单号获取最新订单状态和物流进度"。另一种典型错误是描述里堆砌了过多细节,把内部实现都写进去了。模型看到冗长的说明反而迷失了重点。
一个好的技能描述,应该回答三个问题:这个技能在什么场景下用、它需要什么关键参数、它的输出大概长什么样。描述控制在三到五句话之间,效果比较好。我按这个标准重写了全部技能描述之后,检索Top-K命中率提升非常明显,从最初的不到70%升到了90%上下。
还有一个冷门的因素,技能标签的设计。我在技能清单里引入了tags字段,检索时把标签词和描述文本一起向量化。标签词的作用更像是给技能画几个高亮的关键词,比如"日报""周报""报表"这类,模型在做关键词匹配时更容易把用户口语和技能关联起来。
4.2 执行超时与重试风暴
排除了选择问题,实际操作里面临的另一大类问题是执行超时与重试风暴。
我在一个对接外部数据接口的技能中,第一次上线就遭遇了超时。那个接口正常情况下100毫秒返回,结果某天因为对方系统抽风,响应时间飙到了3秒以上。技能配置的超时是2秒,所以连续几轮调用全部超时。更糟糕的是,重试策略没有上限,Agent发现一次超时后又触发下一轮调用,形成了重试风暴,把外部系统压得更喘不过气。
这个教训让我把重试策略改成了两层。第一层是技能内部重试,最多3次,每次退避时间按指数增长:1.5秒、3秒、6秒。第二层是编排层兜底,如果技能重试三次仍然失败,编排层不再自动重试这个技能,而是把错误信息返回给Agent,让Agent决定是换技能还是向用户澄清。这个策略上线之后,类似的问题没有再威胁到整体任务。
重试策略的退避时间不是拍脑袋拍的,需要考虑技能上游接口的恢复特性和整个任务的全局耗时预算。如果上游接口是那种秒级自动恢复的,退避时间可以短一些;如果可能涉及到批处理修复,就得按分钟级别来退避。经验之谈:重试退避基础值不要小于1秒,小于1秒的重试几乎没有意义,只会在系统还没恢复前就再来一次。
4.3 技能编排顺序错乱:缺乏校验的代价
还有一个高频问题,出现在技能编排的合理性上。部分任务虽然技能组合正确,但执行顺序错乱,比如先生成报告后拉数据,最后报告里大量占位符。这种问题光靠LLM是很那盯住的,得在编排引擎里做一把额外保险。
我的做法是为部分关键技能声明前置依赖条件。例如"生成报告"技能的前置条件是"指标查询结果已存在"。编排引擎在执行每一个技能前先检查前置依赖,如果依赖不满足,就回退到上游可能满足依赖的技能节点,或者请求Agent补充数据获取步骤。有了这个机制之后,乱序问题基本是绝迹了。
我再补充一个小技巧:在技能编排引擎里保留一份人工审核后的"正确路径样例"。比如某个技能A实际执行时统计样本里90%都跟着技能B,那就把这组关联记下来。当Agent规划的路径明显偏离该模式时,给运行日志打一个warn级别标记。标记不等于禁止,只是提醒后续排查时多留意一下。
4.4 难以定位的细节问题:日志和追踪如何设计更省心
Agent系统的排障难,很多时候难在细节在多次LLM推理之间被稀释了。我的做法是建立一套结构化的日志体系。日志记录的核心,不是记录"发生了什么",而是记录"在哪个决策环节、哪些可用信息下,做了什么决定"。
每个技能执行开始和结束都打一条日志,字段包括技能名、参数摘要、耗时、返回状态码。每次LLM推理也打一条日志,字段包括注入的Prompt版本、模型响应、推理耗时。最关键的是把推理之间传递上下文的关键动作记录下来——哪一步填充了哪些参数、从哪个字段取值、是否做了格式转换。这些细节在问题出现时往往就是唯一能快速定位的线索。
日志落地的存储,建议至少存够一个月的量。我在线上为了避免日志量过大,精简了消息体,但它最终成了我排查问题的第一入口。很多时候,不用看代码,光看日志链条就能推断出是在哪一环出了问题。譬如某个技能突然被大量调用,日志里可能显示是因为某些任务都触发了同一个错误分支。再比如某个参数反复被更换,日志里能看到是Prompt模型换版本导致的。
5. 性能与稳定性:技能系统上生产前的最后几道坎
5.1 缓存策略
技能系统上线前,另一个容易被忽视的问题,是技能结果的重用。同一个技能,在多个任务里可能被反复调用,比如"查询用户基本信息"这种高频技能。如果每次都从零跑一遍全链路,响应时间就上来了。
我在技能层做了两层缓存。第一层是内存缓存,适合命中率高、数据一致性要求低的场景,比如"本周周报模板内容"。第二层是分布式缓存,适合跨服务共享热数据,比如"查询产品资料"。缓存失效策略要非常谨慎——Agent系统最怕的就是用了旧数据产生误导,所以我对任何写了缓存的地方都定了过期时间,尤其在对外部数据源接口做缓存时,TTL设得非常短,一般不超过5分钟。
5.2 速率限制与资源隔离
一个Agent服务在线上跑的时候,并不只是服务一个用户。多个任务并发,每个任务又包含多个技能调用,这时候如果不做速率限制,很容易把下游数据库和API打满。
我在编排层实现了令牌桶限流,按技能维度区分配额。高优先级的技能配额给足,低优先级的先让让位。同时,我还给不同客户的任务设置了资源池隔离。最简单的方式是给每个任务划分一个最大并发技能调用数,超出之后排入队列等待。这个参数调优需要结合具体场景,我一开始给的是8路并发,后来测试发现6路更稳,因为下游的线程池和连接数经不住8路同时猛踩。
5.3 技能异常的处理等级
技能异常不都是一样的,有的可以重试,有的可以直接降级,有的必须抛给用户决策。我按严重程度把异常分成几级。
- L1:瞬时的网络抖动、数据库连接失败。策略:重试。
- L2:数据格式变了、字段不存在。策略:降级,尽量从周边数据推算。
- L3:权限不足、资源根本找不到。策略:直接终止当前技能,返回明确错误码。
- L4:技能内部逻辑复杂不可恢复,需要走人工排查。策略:记录全链路日志,告警通知。
分级处理一开始很繁琐,但走到生产环境之后收益极大。它让Agent在异常面前不放羊,也能保障用户收到的错误信息仍然明确有效。
6. 写在最后的经验和扩展建议
一套技能系统,做了三个版本迭代以后,我最大的体会是:不要把Agent的能力设计当成一个纯技术问题,它更像是给一个实习生编写工作手册——手册里既要写清楚岗位职责、操作步骤和边界约束,又要留出足够的临场发挥空间。技术人容易犯的毛病是过度设计,觉得技能越多越好、编排越复杂越酷,但到线上以后,真正在跑且跑得稳定的技能,往往只有三分之一。精简不等于单薄,把少数几个高频、高价值技能打磨到极致,比铺开一堆半成品技能的效果要好得多。
从扩展性的角度说,这个技能系统的下一步可以做的事情其实不少。比如引入技能版本之间的A/B测试机制,让新旧版本的技能在同量级流量的条件下对比效果;比如把技能执行里产生的中间结果沉淀为一个知识缓存库,让后续任务能复用之前任务的有效产出;再比如建设一个技能质量仪表盘,持续观测每个技能的调用量、成功率、平均耗时、变更影响。这些方向都是顺着技能系统自然延伸出来的,如果后续有时间,我大概会先在质量仪表盘上投入——毕竟可观测永远是改进的前提。
分享一个小工具上的心得吧。技能清单文件里的description字段,是整套系统里性价比最高的待优化项。只需要花一个下午把所有技能描述重写一遍,用业务语言替代技术语言,你的技能命准率可能就有明显提升。这比调整向量检索参数、重训Embedding模型都来得快、来得多。有时候最朴素的修改,反而是占用时间最少却见效最直接的一个动作。