过去半年,我一直在折腾LLM Agent。从最开始的定制化Agent——每个场景单独写一套Prompt、单独接几个工具、单独调参数——到后来转向通用Agent + Skills 的架构,算是一次彻底的重构。今天这篇东西,就是这次重构的完整复盘,包括为什么做、怎么做、以及中途掉进去的坑。
很多朋友一上来就问:怎么让Agent实现某个具体业务?比如查订单、写周报、订会议室。你可能会给它接上对应API,把业务规则写进Prompt,最多再加点护栏。这套做法很快见效,但用一段时间就会发现问题:需求一变,整个流程就崩,像是给每个场景专门养了个“临时工”,干完活就再也送不走了。
后来我换了个思路,把Agent本身抽成一个“通用大脑”,把具体业务能力封装成一个个独立的Skills,由Agent在运行时自由调度。这个转变带来的不只是代码结构的改变,而是对LLM Agent本质的理解发生了一点变化。下面详细说。
1. 重新审视定制化 Agent:它到底在解决什么问题?
1.1 定制化 Agent 的典型形态:什么算“定制化”
我最早做的Agent,基本长这样:给一个客服场景写一套Prompt,把话术、业务规则、常见问题全塞进去,然后再接五六个固定工具,每个工具负责一个具体动作,比如查订单、改地址、提交工单。有的场景为了可控,还会用状态机把对话流程钉死,用户说什么话、Agent进入哪个分支,全都提前规划好。
这种形态的优点非常明显:上线快、行为可控、老板看得懂。因为所有路径都在设计者的脑子里预演过,哪怕模型偶尔抽风,状态机也能兜住。很多团队选择定制化,不是因为它高端,而是因为它“确定性”强。做业务系统的人最怕不可控,定制化Agent恰好迎合了这种心理。
但问题也随之而来。一旦业务方提出“新增一个退款场景”“改一下投诉语气”“把工单和知识库打通”,你就要重新捋一遍状态机,调整Prompt里某些措辞,甚至重新设计工具参数。哪怕只是一个小改动,都可能影响之前调好的其他分支。时间一长,Agent就越长越胖,最后变成一团谁都不敢动的毛线球。
1.2 定制化的成本与天花板:为什么越做越累
说几个我实际踩过的点。第一是维护成本爆炸。每新增一个场景,往往不是新增一个分支那么简单,而是要重新考虑上下文管理、工具冲突、Prompt长度。曾经我有过一个Agent,Prompt写了快三千字,里面塞了十几个业务接口的描述,模型经常“记错”接口的参数,尤其当用户在一个对话里同时涉及两三个场景时,那种割裂感特别明显。
第二是无法沉淀能力。你在这个客服Agent里调过的“查订单”逻辑,换到另一个办公场景的Agent里基本用不上,因为代码、Prompt、数据格式全都耦合在一起。相当于同样的能力反复造轮子,每次还造得不太一样。团队根本没有一条“能力复用”的路径,所有东西都散落在各个Agent的黑盒里。
第三是扩展性天花板很低。如果你要做一个“全能助手”,不可能为每一个新场景定制一个新Agent。否则以后用户说“帮我写代码”,你用写代码Agent,说“帮我点外卖”,你用点外卖Agent,Agent之间互相不知道对方的存在,也没有一个统一的入口来调度它们。这就不叫“助手”,叫“App文件夹”。
下表大概总结了我当时感受到的差异:
| 维度 | 定制化Agent | 通用Agent + Skills |
|---|---|---|
| 上线速度 | 单场景很快 | 初期偏慢,之后逐步加快 |
| 行为可控性 | 高,流程写死 | 中,依赖调度器与Skill设计 |
| 需求变更成本 | 高,容易影响全局 | 低,新增/调整Skill即可 |
| 能力复用 | 基本没有 | 天然复用,所有场景共享 |
| 复杂任务处理 | 很难动态组合 | 可以拆解后动态编排 |
所以到后来,我几乎是被逼着换方向。不是定制化方案完全不能用,而是当你的目标从“做一个客服机器人”变成“做一个能应对多种业务的通用Agent”,再用定制化那一套去堆,一定会撞墙。
2. 通用 Agent + Skills 的设计思路
2.1 核心思路:把能力拆成可插拔的最小单元
通用Agent + Skills 的核心,是让Agent本身只负责“思考”和“组织”,把具体能力全部外包给Skill。每个Skill是一个能被Agent识别、调用、组合的功能模块,里面有清晰的描述、输入输出格式、调用逻辑,甚至还有使用示例。Agent拿到用户请求后,先理解意图,再在Skill列表里找匹配项,找到就调用,找不到就组合多个Skill完成。
打个比方。定制化Agent像是一个外包团队,每个项目单独招一批人,人跟着项目走。通用Agent则像一家公司,有固定的管理层(大脑)和一套标准化的部门(Skills),新业务来了,管理层把任务分给现有部门,必要时再成立一个新部门(新增Skill)。部门之间独立运作,管理层负责协调。
这个设计最大的好处是“单一职责”。每个Skill只做一件事,并且把它做到可描述、可测试、可替换。比如“查天气”是一个Skill,“查日历”是另一个Skill,它们互不干扰。业务方如果说“帮我看看明天适不适合开会”,Agent会先判断需要天气和日历两个信息,然后分别调用两个Skill,再把结果综合起来回答。
2.2 Skills 不只是工具:它比工具多了一层“使用说明”
很多人刚开始会把Skill理解成“工具列表”,其实不对。工具只是一个可执行函数,但Skill通常还包含以下几个关键部分:
- 技能描述:告诉Agent这个Skill是干什么的、适合什么场景、不适合什么场景。
- 输入参数:定义调用时需要提供哪些字段,每个字段的含义、类型、约束。
- 执行逻辑:可以是一段代码、一个API调用、一次本地脚本执行,甚至可以是一个子Agent。
- 上下文约束:说明这个Skill依赖什么信息,比如需要当前用户ID、需要上一步查到的订单号。
- 示例触发:给1到2个典型的用户请求示例,帮助模型在意图识别时少走弯路。
多出来的这些信息,才是Skill真正能被“通用Agent”使用的关键。模型不是人类,它不知道“查订单”背后的接口长什么样,我们需要把Skill描述得足够清晰,让它能在跑腿的时候自己判断该用哪个工具、传什么参数。如果只给一个裸函数,Agent大概率会在五花八门的新说法面前手足无措。
我最初犯过的错,就是把Skill写成只有“名称 + API地址 + 参数”。结果Agent经常把参数传错,或者该用它的时候没选它。后来我把使用示例、边界条件、参数常见取值全写进去,情况立刻好转。本质上,Skill是为模型准备的“产品说明书”,而不是为程序员准备的函数签名。
2.3 从“写死流程”到“动态编排”:复杂任务不再靠状态机
定制化Agent处理复杂任务靠的是状态机:定义好节点、边、条件分支,让用户沿着预设路径走。通用Agent处理复杂任务靠的是“动态编排”:Agent先把用户的目标拆成子任务,然后挨个选择合适的Skill去执行,最后汇总结果。这背后的能力来自大模型本身的推理能力,比如思维链、任务规划。
举个例子,用户说“帮我查一下这周的天气,然后根据天气推荐两条周边游路线”。定制化Agent要预先写一个“天气→旅游推荐”的状态流转。通用Agent则会自己先拆成“查天气”和“找旅游路线”两个步骤,分别匹配到对应Skill,再整合输出。如果用户中间改口说“再查一下高铁票”,它也只会当成新增一个子任务,而不是把整个流程推翻。
动态编排带来的风险是“不可预期”。模型可能在两个Skill之间产生错误的依赖,也可能在条件不满足时强行调用。所以需要调度器在中间加一层“约束”和“校验”。调度器不负责具体业务,但它负责:确认Skill可调用、确认参数完整、捕获执行异常、在Skill失败时触发兜底逻辑。这比状态机轻很多,但又保留了必要的控制力。
3. 实操:从定制化迁移到通用 Agent 的关键步骤
3.1 第一步:定义一份可落地的 Skill 规范
迁移的第一步不是写Agent,而是先定一套Skill规范。没有规范,后面所有Skill都会长成不同的样子,Agent根本没法统一调度。我最后用的Skill描述结构大致是这样的:
{ "id": "skill_order_query", "name": "查询订单状态", "description": "根据订单号或用户ID查询订单当前状态,适合在用户询问物流、待发货、退款进度时使用。", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户提供的订单号,如果缺失可以询问用户" } }, "required": ["order_id"] }, "execution": { "type": "api", "url": "/api/v1/order/status", "method": "GET" }, "examples": [ "帮我看看订单 20240815 到哪了", "最近买的那本书发货了吗" ], "fallback": "如果查询不到订单,返回订单不存在提示,并询问是否核对订单号。" }这个结构很简单,但每个字段都不能少。尤其“description”和“examples”,它们是模型判断是否选用这个Skill的核心依据。你可以把description理解成“搜索关键词”,写得太宽泛,模型容易乱选;写得太窄,该用的时候又不选。我后来用了一个经验:每个Skill的description都要包含“功能 + 适合场景 + 不适合场景”,比如“查询订单状态,适合用户询问物流、退款进度,不适合修改订单信息”。
3.2 第二步:实现一个轻量调度器
有了Skill规范,接下来需要实现一个调度器。这个调度器不一定复杂,但至少要做四件事:
第一,维护Skill注册表。启动时加载所有Skill元信息,把它们转成一个大列表,作为上下文的一部分喂给模型,让模型知道“我有哪些工具可用”。第二,解析模型输出的调用意图。模型通常会输出类似“我想调用query_order,参数order_id=xxx”的结构,调度器负责解析这个结构并校验参数完整性。第三,执行Skill并返回结果。调用真实API或脚本,把结果拼接成一段文本,塞回模型的上下文。第四,容错与兜底。如果Skill执行失败,要返回给模型一个错误信息,让它重新规划或向用户解释。
我实现调度器时用了最简单的循环:模型决策 → 执行Skill → 观察结果 → 再决策。这个循环不需要复杂的框架,只用一个while循环控制最大轮数即可。这里有一个很关键的细节:必须控制上下文长度。每次执行完Skill,都要把结果压缩、摘要,而不是无限追加日志。否则几轮之后上下文就爆了,模型开始胡言乱语。
3.3 第三步:把原有定制逻辑沉淀为 Skill
迁移过程中最核心的工作,是把原来散落在各个定制Agent里的逻辑,一个个“翻译”成Skill。这不是简单地把代码搬过来,而是要做三件事:
一是拆分。原来一个状态机节点里可能同时包含“校验用户身份”“查订单”“判断是否退货”等多步操作,现在要拆成独立的Skill,每个Skill只做一个动作。二是补说明书。给每个功能模块写清楚描述、参数和示例。三是加测试用例。每个Skill都配几条典型输入,用于日后回归测试。
我当时从一个“客服助手”开始迁移,先把“查订单”“改地址”“申请退款”三个动作拆成三个Skill,再写了一个非常简单的调度器,让用户用自然语言直接提问。效果比想象中好,因为原来状态机处理不了的“边角话术”,通用Agent反而能靠模型理解能力兜住。但也会有新问题,比如模型偶尔会把“改地址”和“查订单”搞混,这就需要靠Skill描述和示例来纠正。
4. Skills 设计与调优的工程细节
4.1 Skill 的粒度选择:太粗不行,太细也不行
Skill的粒度,是整个架构里最难拿捏的事情。如果粒度太粗,比如一个“handle_user_request”的Skill,什么都往里塞,那跟定制化Agent没什么区别,调度器形同虚设。如果粒度太细,比如把“去除字符串空格”“格式化日期”都单独做成Skill,Agent会陷入选择困难,一个简单任务要连续调用七八个Skill,反而降低效率和准确率。
我后来总结了一个判断标准:一个Skill应该是“用户可感知的一个完整动作”。什么叫用户可感知?就是用户提出一个请求,这个请求能对应到某个明确结果。比如“查询天气”“创建日程”“发送邮件”,这些是完整动作。而“解析JSON”“获取当前时间戳”是技术动作,不应暴露给Agent,应直接放在调度器内部或写进另一个Skill的执行逻辑里。
还有一个经验:如果某个Skill的description里需要写太多例外和边界,那它很可能需要拆细了。因为描述越复杂,模型越难选对。宁可拆成两个精准的小Skill,也不要留一个又长又含混的大Skill。但也要注意,Skill数量一旦超过几十个,模型的选择准确率就会下降,所以必要时要有“分组”或“层级”能力,比如先选大类,再选具体Skill。
4.2 Skill 的触发与选择:靠语义还是靠规则
通用Agent怎么知道该用哪个Skill?常见有两种路线:一种是靠模型语义匹配,另一种是靠规则或分类器预筛。我实际体验下来,纯语义匹配在小规模场景下很爽,但Skill一多,模型会把相似功能搞混。比如有一个“查订单”和一个“查物流”,两者描述高度相似,模型经常选错。这时候规则就很重要了。
一个比较稳妥的做法是“混合触发”:先用一个轻量分类模型或关键词规则把候选Skill缩小到Top3,再把这3个Skill的详细描述交给大模型做最终选择。这样既保留了大模型的理解灵活性,又通过规则排除了大部分不相关的Skill,准确率能提升不少。尤其在处理长尾、模糊请求时,预筛机制能显著减少误调用。
另外,Skill之间还可以定义“依赖关系”。比如计算物流时效这个Skill,依赖“查物流”返回的当前状态。调度器在执行时可以通过显式的前置条件来约束,而不只是靠模型自己推理。这些依赖关系尽量写在Skill的元数据里,调度器在调用前检查一下,不满足就先补前置条件,而不是让模型盲目跳步骤。
4.3 评估 Skill 质量:命中率、转写准确率与用户满意度
不要等到上线了才发现某个Skill稀烂。我给每个Skill都建了一份简单的“质检表”,主要看三个指标:
- 命中率:在测试集里,当前Skill应当被选中的次数中,Agent实际选了多少次。
- 参数完整率:调用Skill时,模型输出的参数是否完整、类型是否正确。
- 任务完成率:Skill执行完毕后,最终回答是否解决了用户问题。
这三个指标分别对应“选对没”“传对没”“干好没”。任何一个低,都需要重新调整Skill的描述、示例或执行逻辑。尤其是“任务完成率”,不能只看模型说“已完成”,要人工抽样看用户意图是否真正达成。
我通常的做法是:准备一个包含50到100条真实用户请求的评测集,每一条都标注了“期望Skill链”。然后跑一轮Agent,统计上述指标,找失败案例,最小化复现,再修改Skill描述或调度逻辑。这套流程其实不复杂,但能避免很多线上事故。很多团队之所以觉得通用Agent效果差,就是因为没做刻意评估,让模型放飞自我。
5. 避坑清单与我的实操心得
5.1 常见误区:从“过度定制”到“过度抽象”
我见过不少团队在迁移到通用Agent后,又走入另一个极端:把所有逻辑都抽象成Skill,哪怕只是一个简单的“问候语”也要做Skill。这种过度抽象带来的问题有三个:第一,Agent在简单任务上变得绕弯子,明明可以直接回答,非要走一遍Skill调用。第二,Skill数量爆炸,选择准确率下降。第三,维护成本反而上升,因为每个Skill都要维护文档和测试。
我的建议是:保留一个“不用Skill也能回答”的默认通道。对于纯表达、闲聊、常识类问题,Agent直接生成回复,不需要调用任何Skill。只有当任务真的涉及外部数据或可执行动作时,才进入Skill调度。否则你的通用Agent会成为一个“所有问题都要走流程”的低效机器。
另一个常见误区是“教程式设计Skill”。有人喜欢在description里写“如果用户说A,就调用本Skill;如果用户说B,就不要调用”,结果把模型整懵了。description应该描述“能力”,而不是描述“规则”。模型需要理解的是“这个Skill能帮我做什么”,而不是“这个Skill在什么时候被触发”。触发逻辑应尽量交给模型推理,写得太死反而适得其反。
5.2 问题排查技巧:日志、回放与最小化复现
通用Agent最让人头疼的地方是“偶然性”。同一句话,可能这次选对了Skill,下次选错了。所以排查问题不能靠感觉,必须把决策过程记录下来。我在调度器里加了一行日志,专门记录每一轮决策的输入和输出:当前上下文摘要、候选Skill列表、模型选择结果、选择理由(如果有)。这样的话,任何一个用户反馈,我都能精确定位到是哪一步选错了。
如果发现某个Skill总是被漏选,我一般会把它挑出来单独测试:构造一组包含各种表达方式的测试句子,逐一问模型“这个请求应该选哪个Skill”,看它的推理过程。很多情况下,模型不是因为能力不够,而是因为Skill描述里没提到关键的同义说法。这时往examples里补几个真实例句,效果立竿见影。
还有一个小技巧:可以在Agent启动时把所有Skill的id和名称打印出来,人工检查一下是否有命名过于相似的情况。比如“query_order”和“check_order_status”,听起来就是绕口令,模型区分起来很难。这种时候哪怕不改逻辑,只改名字,也能提升准确率。
5.3 我的最终建议:渐进迁移,不要推翻重来
如果你现在还在用定制化Agent,想往通用Agent + Skills迁移,我建议不要搞“大爆炸”式的重写。最好是先把最痛的一个场景抽出来,做成一个Skill,再搭一个极简调度器,把这个场景融入通用Agent里跑通。跑通之后,再把其他场景一个个搬过来。
我从客服场景起步,只写了三个Skill,就感受到明显的变化。后来逐步新增了日程、邮件、周报等Skill,积累到十几个时,才开始碰到选择准确率问题,于是又引入了预筛机制。整个过程是渐进的,每次只改一个变量,出问题也容易定位。
最后再分享一个小技巧:Skill的维护要像维护代码一样,纳入版本管理。每次修改description或执行逻辑,都要记录改动点,并且跑一遍回归测试。因为模型对Skill描述极其敏感,也许你只是删了一个“不”字,Agent的行为就可能截然不同。保持对Skill的敬畏,才能让通用Agent真正稳定下来。