ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Agent Skills:打通智能体稳定落地的关键架构层

Agent Skills:打通智能体稳定落地的关键架构层

1. 为什么要单独聊 agent-skills:智能体的能力瓶颈不在模型侧

我去年做了一整个季度的 Agent 项目,最大的感悟是:真正决定一个智能体能不能稳定落地的,往往不是模型本身,而是"skills"这一层。模型选得再好,Prompt 写得再花,如果没有一套设计合理的 agent-skills 体系,最终交付的东西就像一辆发动机很强但变速箱乱跳的车——账面参数漂亮,一上路就露馅。

所谓 agent-skills,你可以理解成智能体可以调用的、可复用的原子能力集合。它不等同于一个 API 封装,不等同于一段普通的 Python 函数,也不等同于教科书里说的"工具调用"。Skills 包含的是一整套交互协议、参数约束、上下文返回和错误处理机制。它是连接大模型和真实业务操作的中间层。

我见过很多团队一上来就堆模型能力,GPT 级别的模型说换就换,但技能层粗糙得要命:要么是开发随手写的松散函数,要么是从同事的代码仓库里拷过来改两下就上线的脚本。结果智能体在 demo 里表现得无所不能,一上线碰到真实用户输入就频繁选错工具、参数传错、超时重试几乎不设防。问题的根源,几乎都集中在 agent-skills 这个环节。

这篇文章我想系统梳理一遍我在 agent-skills 设计、落地、排障和迭代过程中的完整方法论,包含具体的注册表结构、路由策略、踩坑链路和评估指标。适合正在做 Agent 应用、或者准备从原型走向生产环境的工程师;也适合团队里负责 AI 平台架构、想搞清楚 skills 层该怎么设计的技术负责人。如果你只是听说过这个词、想了解技能体系到底在解决什么问题,这篇文章同样能给你一个清晰的全局视角。

我不打算讲那种"用了某个平台、点了几个配置就万事大吉"的沙盘操作。我讲的是你自己动手搭一套可以跑在生产环境的 skills 体系时,会面临的真实选择。

1.1 从一场线上故障说起:模型会了,Agent 还是不会

先说一个真实事故。我们的智能体上线以后,其中一个高频诉求是"帮我查一下明天上海到北京的高铁"。当时我们只给它挂了一个简单的query_train函数,参数是出发城市、到达城市、日期。结果连续几天有用户反馈说"智能体答非所问",日志一拉,发现模型虽然完整理解了用户意图,但调用的却是另一个叫query_flight的技能,返回了一堆航班信息。

排查到最后,原因出在技能描述上。query_flight的描述里写了一句"支持城际出行查询",而query_train的描述则是"查询列车时刻表"。对模型来说,"城际出行"这个泛化词比"列车"更贴近用户的"出行"意图,于是选错了工具。你怪模型理解能力差?其实不怪。是我们的技能描述写得像给人看的说明文档,而不是给模型看的决策依据。

这种问题在模型能力越来越强的今天不仅没有消失,反而更隐蔽了。因为大模型太擅长"合理化"自己的选择——它即使拿到的工具不太匹配,也会尝试着把结果往用户的问题上圆。等你从用户反馈里发现问题时,已经不知道错了多少轮了。

1.2 API 调用、插件、工具和技能,到底差在哪

很多人会把 agent-skills 等同于"给模型增加函数调用"。这是最常见的一个误解。普通 API 调用强调的是输入输出的契约,参数类型、返回结构定好了,调用方按规矩来就行。但技能(skill)多了一层"智能适配"的职责:模型需要理解在什么场景下选它、参数怎么从自然语言抽取、出错以后怎么恢复。它不是被动的接口,而是主动参与决策的执行单元。

再和插件(plugin)概念做一下区分。早期插件体系往往是"装上了就有"的静态配置,技能则是运行时被动态编排和调度的。同样一个技能,在这次对话里可能排在优先级第一位,换个用户、换个上下文,可能就不该被选中。这种动态性,是插件模式不擅长的。

工具(tool)的概念就更宽泛了。我在实践里倾向于把工具理解为具体实现(比如一个 HTTP 客户端、一个数据库查询函数),而技能是工具加上模型的调用协议。一个技能内部可能包含多个工具,也可以有自己的一套前置处理和后续加工逻辑。你可以把它看作"模型可理解、可调度、可校验的执行单元"。把概念分层以后,很多讨论混乱就消除了。

1.3 技能层做不好会付出什么代价

技能层设计得烂,代价不是一次调用失败那么简单。我总结了三层:

第一层是用户体验崩坏。用户问的是高铁,你返回航班,一次两次可以解释是模型抽风,十次八次用户就流失了。

第二层是排障成本爆炸。没有统一的技能调用日志,没有参数快照,出错了你根本不知道是模型决策错了、参数抽取错了、还是底层 API 错了。我见过团队花了两天时间查一个问题,最后发现是技能内部有一个参数名拼写错误,而且这个错误只是在某个分支里存在。这种问题光靠看代码很难发现,必须靠技能层的可观测性设计提前兜住。

第三层是迭代失控。没有版本管理,技能一改,前面所有依赖它的场景全部受影响;没有灰度机制,你只能全量上、全量挂。技能从十几个涨到几十个以后,这种失控会指数级放大。

所以 agent-skills 不是"写完函数注册一下就行"的体力活,它是 Agent 系统里最值得投入精力的架构层。接下来我从最底层开始拆,一个技能到底该长什么样。

2. 一个技能的本质:从"能跑通"到"稳定复用"

在动手写代码之前,我先给技能的构造做一个定义。一个生产级技能需要具备四个核心要素:模型可读的描述、明确约束的参数 Schema、可追踪的调用上下文、可观测的返回结构。四个要素缺一个,这个技能就只能停留在"能跑通"的阶段,达不到"稳定复用"。

很多开发者的第一反应是:四个要素里描述和 Schema 我已经写过了,还差什么?差的是后两个。调用上下文决定了模型在什么条件下应该想起这个技能,返回结构则决定了技能执行完以后框架能不能判断它是否真的成功了。这两个点直接关系到多技能编排时的可靠程度。

2.1 技能描述是写给模型读的,不是写给文档用的

这是 agent-skills 设计里最反直觉的一点:技能描述不是给人看的接口文档,而是影响模型决策的上下文。

我见过最典型的错误是拿 Javadoc 风格来写技能描述,什么"This function is used to query train information"——这种描述信息量极低。模型面对几十个技能时,它不是在"读文档",而是在做快速的语义匹配。描述里必须有触发场景、典型用户表达、边界说明。

一个好描述至少包含三块内容:

  • 触发条件:什么情况下该用我这个技能
  • 典型输入:用户会怎么说(真实的话术样例)
  • 边界声明:什么东西我不管、不要选我

举个例子。同样是查询列车,我之前认为合格的描述是这样:

查询国内列车时刻与余票信息。当用户需要了解高铁、动车、普速列车的出发到达时间、历时、票价、余票时使用。主要触发词包括:高铁、火车、动车、列车、车次、余票、几点开。如果用户询问的是航班信息,不要使用本技能。

这块描述包含了触发条件和边界声明。模型在技能选择时,看到这段文本,能很清晰地算出"这个技能和当前问题的相关性"。与其说是在写描述,不如说是在给模型写一份"决策说明书"。后期我拿这个描述做测试,选错率从之前的 14% 直接降到了 3% 左右。

2.2 参数 Schema:宁可少给,不要模糊

参数 Schema 是另一个重灾区。我最初设计技能时喜欢把参数定义得非常宽松,觉得这样模型发挥空间大。后来发现大错特错。宽松的 Schema 意味着模型有更大的自由度去猜测、去自己造值,这恰恰是参数错误的温床。

以查询高铁这个技能为例。最初的版本里,日期字段我用了一个通用的date: string,没有做格式约束。模型有时传2025-06-18,有时传明天,有时甚至传6月18日。你让后端怎么统一处理?后来痛定思痛,我把日期参数的类型改成了YYYY-MM-DD格式,并在描述里明确写了一句"日期必须为公历日期,格式化为 YYYY-MM-DD,若用户未指定则默认为今天"。改了以后,参数解析错误率直线下降。

同时要注意别走另一个极端:把参数限定得过死,导致真实场景无法表达。有的技能查询需要支持模糊条件,比如"找一个价格便宜点的酒店",你非要模型必须传星级字段,它只能瞎猜。参数 Schema 的原则是:必填字段严格约束;选填字段给清楚默认行为;拿不准的业务参数宁可不定义,让技能内部去处理模糊逻辑,也不要推给模型硬抽。

2.3 可观测性:每次技能调用都要能回溯

第三个要素往往最被忽视。技能是给模型用的,但模型的选择可能出错、参数可能抽错、底层 API 可能超时、返回结果可能不符合预期。你凭什么知道是哪一环出了问题?靠的是调用日志里只记录"调用成功/失败"这种粗粒度信息,远远不够。

我现在的做法是给每个技能调用打一份标准化的日志:包含请求 ID、用户原始输入、模型抽取出的完整参数、实际执行耗时、返回结果摘要、错误堆栈(如有)。这份日志统一收集到单独的数据表里,后续做评估和复盘时拉出来看。没有这套日志,你根本谈不上"持续优化技能",因为你连错在哪都不知道。

我见过不止一个团队,技能出了故障只能靠用户截图、靠工程师肉眼翻聊天记录来还原现场。那种排障效率低到令人绝望。技能层一旦有了标准化的日志,大多数问题都能在十分钟内定位到具体环节。

2.4 好技能的四个评判标准

我把这些年的经验压缩成一个验收表,每次新写一个技能都拿它过一遍:

维度合格标准不合格信号
描述质量包含触发场景、用户话术例句、边界声明只有函数功能说明;多个技能描述风格混乱
参数约束必填字段格式明确;可选字段默认行为清晰模型经常传错格式;字段语义模糊
可观测性调用日志含原始输入、参数快照、耗时、错误只能看到"成功/失败"两个状态
返回结构有统一的状态字段;错误时返回可读原因靠抛异常裸奔;返回格式随业务随意定

这张表我贴在项目白板上,每次有人提议"先写个函数,回头再补文档"的时候,就指着它问一句:你打算哪一步补?大部分人都答不上来。因为补这项工作,放到后面就永远不会做了。

3. 实战:搭一套可扩展的 agent-skills 技能库

讲过原理之后,进入实操环节。我用的技术栈以 Python 为主,但这一整套设计思路和语言无关。核心就三件事:定内容、定注册方式、定目录结构。下面按顺序展开。

3.1 第一步,先盘点能力,划分技能粒度

很多人第一步就翻车,因为他们把技能的粒度定错了。一个技能到底应该多细?这没有绝对标准,但有一个判断原则:一个技能应当对应一个完整的、可独立完成的任务意图,而不是一次具体的 API 调用。

举个反例。比如要做一个"订阅新闻"功能,你拆出的技能如果是get_news_list、subscribe_channel、unsubscribe_channel三个独立的技能——这在纯函数设计上没错,但作为技能的粒度就太碎了。用户在对话里通常说的是"帮我订阅科技频道的新闻",模型需要自己组合三个技能才能完成这件事。组合链路越长,出错的概率越大。

更好的拆分是:将subscribe_channel作为技能,内部在调用具体的接口之前,先用参数抽取完成频道识别和订阅偏好确认。至于用户想先看看有什么频道,那可以单独放一个get_news_list技能。这样两个技能各管一类完整的用户意图,职责清晰,模型选起来也不纠结。

当然反过来也要警惕。粒度太粗同样有问题。一个技能里面如果塞了十几个分支逻辑,内部状态一多,出问题的概率也会暴增。我个人的经验是:一个技能内部的主流程最好不超过三五个决策点,业务再复杂就拆成子技能,用编排逻辑去组合。

3.2 技能注册表:让调度器知道"有什么可用"

技能的粒度定好后,要有一个统一的注册表。注册表的核心作用,是让调度器在每一轮对话中知道"当前有哪些技能可用、它们的描述是什么、参数长什么样"。没有注册表,你就要在前端写一大堆硬编码的 if-else,技能一多就变成屎山。

注册表我用 JSON 存储,追求的是"任何人都能照着格式改"的朴素目标。结构大致如下:

{ "skill_id": "query_train_schedule", "name": "查询列车时刻", "description": "查询国内列车时刻与余票信息。当用户需要了解高铁、动车、普速列车的出发到达时间、历时、票价、余票时使用。主要触发词包括:高铁、火车、动车、列车、车次、余票、几点开。如果用户询问的是航班信息,不要使用本技能。", "version": "1.3.0", "enabled": true, "tags": ["travel", "transportation"], "parameters": { "type": "object", "required": ["departure", "arrival", "date"], "properties": { "departure": { "type": "string", "description": "出发城市,如北京、上海" }, "arrival": { "type": "string", "description": "到达城市,如广州、杭州" }, "date": { "type": "string", "description": "出发日期,格式必须为 YYYY-MM-DD,未指定时默认今天" } } }, "handler": "skills.travel.query_train" }

这个注册表里我额外加了tags字段和version字段。tags用于快速分组和筛选,version配合后面的评估灰度机制使用。handler指向实际的函数实现,框架加载时直接动态导入。

3.3 一个最小可用的技能注册示例

光有注册表还不够,还要有一个加载器。我写的最简版本是这个样子:

import importlib import json import glob class SkillRegistry: def __init__(self, skill_dir: str): self.skill_dir = skill_dir self._skills = {} def load_all(self): for manifest_path in glob.glob(f"{self.skill_dir}/**/skill.json", recursive=True): with open(manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f) if not manifest.get("enabled", True): continue handler_path = manifest["handler"] module_path, func_name = handler_path.rsplit(".", 1) module = importlib.import_module(module_path) handler = getattr(module, func_name) self._skills[manifest["skill_id"]] = { "manifest": manifest, "handler": handler, } def list_skills(self): return [v["manifest"] for v in self._skills.values()] def execute(self, skill_id: str, **params): skill = self._skills.get(skill_id) if not skill: raise KeyError(f"skill not found: {skill_id}") return skill["handler"](**params)

这只是一个最薄的地基,生产环境里你还要加依赖注入、权限检查、限流之类的逻辑。但有了这个骨架,技能就是"往目录里丢一个 skill.json + 一个 Python 模块"就能上线的东西了。新成员加入团队以后,不需要理解整个 Agent 的调度细节,也可以独立开发一个技能并且注册进去。这就是注册表模式最大的价值:把技能开发的复杂度隔离在框架之外。

3.4 技能仓库的组织方式:目录、命名与版本

技能一多,仓库组织就显得格外重要。我采用的目录结构如下:

skills/ ├── travel/ │ ├── query_train/ │ │ ├── skill.json │ │ └── handler.py │ └── query_flight/ │ ├── skill.json │ └── handler.py ├── news/ │ ├── get_news_list/ │ │ ├── skill.json │ │ └── handler.py │ └── subscribe_channel/ │ ├── skill.json │ └── handler.py └── common/ ├── get_weather/ └── calc_expression/

目录按业务域分组,每个技能独立成文件夹。这样当某个域的技能质量出现问题,可以快速定位到对应的子目录去做集中 review,而不会和其他域的东西搅在一起。

版本管理方面,除了注册表里的version字段,我还会给每个技能目录维护一个CHANGELOG.md,哪怕只是改一句描述也要记进去。原因很简单:技能描述的变化会直接影响模型的决策概率,这种变化最隐蔽也最致命。一旦改了描述,线上行为就可能悄悄变化。你没记日志,下次排查的时候等于两眼一抹黑。

4. 技能编排:单技能能跑,多技能怎么打配合

技能库搭好只是第一步。真实用户的问题往往是复合型的,一个需求可能需要多个技能协同完成。技能编排,就是决定"谁来调用、按什么顺序调用、参数如何流转"的那一层。没有编排策略,技能库就是一堆散装零件;有了编排,才能拼成完整的解决方案。

4.1 路由策略:谁来决定调哪个技能

先讨论最核心的问题:谁来决定每个请求应该调哪个技能?我试过三种方案,各有适用场景。

第一种是纯提示词路由。把所有技能的描述拼进系统提示词,让模型直接输出要调用的技能与参数。优点是实现简单、灵活度高,适合技能数量不多(比如二三十个以内)的场景。缺点是技能一多,上下文爆炸,模型的选择准确率也会下降。

第二种是语义检索路由。先把所有技能描述做向量化,用户请求进来时做一次相似度检索,挑出最相关的几个技能再交给模型做最终决策。这种方案适合技能数量很大(上百个)的场景,能够把决策规模控制在合理范围内。缺点是依赖向量检索质量,而且要额外维护一套 embedding 流程。

第三种是规则路由。针对特定场景,用代码明确指定调用路径。例如"当用户当前处于某个多轮流程的第二步时,直接调用步骤对应的技能,不走模型选择"。这种方案最稳定,但不够灵活。我的实践是把它作为保底策略,用在那些绝对不能出错的强流程里。

实际项目中,我建议第一版先用纯提示词路由,因为实现最快,能帮你快速验证业务价值。等到技能数量超过三四十个、选错率明显上升的时候,再引入语义检索做预筛。这套升级路径最平滑。

4.2 串行与并行:什么时候必须排队

多个技能协同有两种基本方式:串行和并行。

串行的场景很常见。比如用户说"帮我查一下上海到北京的高铁,顺便订一张明天的票"。这个需求里,查车次要先执行,拿到车次结果、确定班次以后才能下单订票。如果两个技能并行执行,订票技能拿不到车次号,就会出问题。

并行的场景也有。用户说"帮我安排明天上午开会,下午去机场,顺便看看机场到酒店怎么走"。这里查会议室、查航班、查路线三个动作之间没有严格依赖,理论上可以并行执行,节省整体响应时间。并行会带来一个麻烦:框架要处理多路调用拼装的上下文,还要处理好部分失败的问题——三个并行请求里有一个挂了,其他两个的结果是否继续采用?

我的建议是:初期先不要追求花哨的并行编排。先把串行链路跑稳定,通过日志观察哪些技能的耗时确实可以重叠,再逐批改成并行。不必一开始就把架构搞得特别复杂。

4.3 技能之间的参数透传与冲突处理

多技能协同还有一个容易忽略的细节:参数怎么在技能之间传递。最常见的是直接透传,比如查询完车次后的trip_id,原样交给订票技能。这类透传要设计清晰的数据结构,避免两个技能各自用不同的字段名,一个叫trip_id、一个叫train_id,最后拼接的时候还得做映射。

另一个常见问题是参数冲突。用户说"帮我订明天下午去杭州的票,但是上午我还想去一趟绍兴"。这里"去杭州"和"去绍兴"是两个不同的出行意图,如果简单地把所有城市参数都塞给同一个订票技能,就会产生混乱。解决的思路是:在技能交互协议里明确"一个技能调用实例只处理一个意图",当用户的一个句子里包含多个意图时,框架先把意图拆开,再分别调技能。这就是我说的"技能层前置处理"的典型例子。

这个前置处理我通常会放在调度器里做,而不是让技能内部去猜。因为技能内部一旦承担了过多意图解析逻辑,它的职责边界就模糊了,后面也就越来越难维护。

4.4 编排失败的兜底机制

编排流程再完善,也会遇到失败。失败处理我遵循一个原则:技能执行失败不该成为对话的终点,而是应该回到模型层做降级处理。

兜底机制分三层做好。第一层是技能内部的重试,针对网络抖动等瞬时错误。第二层是编排器的降级策略,比如主技能失败后,是否尝试备选技能,或者略过该技能继续后续流程。第三层是模型的回复策略,当所有技能都失败时,模型能否坦诚地告诉用户"暂时没法完成",而不是编造一个结果。

这一条我特别强调。见过太多智能体在技能失败后,生成一段看似顺滑、实则完全虚构的回复。这种"幻觉兜底"非常危险,尤其在涉及金额、时间、日程的场景里,会直接导致用户信任崩塌。我的框架里有一个强制规则:任何技能调用失败,模型的系统提示词里都会注入"最近一次技能调用失败,请基于失败原因如实回复"。这个规则表面上限制了模型的自由发挥,实际上保护了整个系统的可信度。

5. 踩坑清单:我在 agent-skills 上翻过车的地方

原理讲了,实操也讲了,但真正让你成长的永远是那些踩过的坑。这一节我列四个真实翻车现场,附带完整的排查过程和修复方案。这些问题你大概率也会遇到,希望你能拿着我的经验少走弯路。

5.1 坑一:技能描述太泛,调度器选错工具

这个坑在开头提过,这里展开完整版。当时我们有query_train和query_flight两个技能。query_flight的描述是"查询航班信息,支持城际出行查询"。用户问的是火车,模型选中了航班技能。

完整排查链路是这样的:

  1. 先看技能调用日志,发现选错发生在模型决策层,参数抽取是正常的。
  2. 单独用同样的问题去测试query_train,发现它能正确处理,说明技能本身没问题,是"选择"这个环节出了偏差。
  3. 对比两个技能的描述,发现query_flight里的"城际出行"这个泛化关键词,和用户的"出行"口语表达产生了语义重叠。
  4. 修改query_flight描述,去掉"城际出行"这类泛化词,明确写"本技能仅用于民航航班查询,不包含铁路列车"。并把query_train的描述补上"用户询问火车/高铁/动车时必须使用本技能"。
  5. 重新跑一批历史真实请求做回归,选错率降到了 3% 以下。

这个坑的教训是:技能描述里的每一个词都可能影响模型决策。写描述时,别追求辞藻丰富,反而要刻意排除那些会和别的技能冲突的泛化表达。你可以把它理解为给技能"划清地盘"。

5.2 坑二:Schema 定义过死,真实场景参数对不上

这是反向踩坑。有一个订酒店技能,我把star_level(星级)设成了必填字段。心想用户订酒店总归会说"五星级"或"四星级"吧。结果真实用户很少按这个格式说话,他们说的是"找个差不多的就行""环境好一点的,不要太吵"这种模糊表达。

模型为了满足必填约束,只能强行猜一个星级,猜得又不准。订出来的酒店用户不满意,体验分暴跌。

修复方案分两步。第一步,把star_level从必填改成选填,并增加一个preference_note字段,专门用来承接这些模糊的偏好描述。第二步,在技能内部通过语义规则把偏好文本映射到实际筛选逻辑。比如"环境好一点"对应"安静""评分高","不要太吵"对应排除临街房型。这样就把模型的"硬猜测"变成了"软传递",准确率反而更高。

这个坑给我的通识是:参数 Schema 不是越严格越好,而是要贴近真实用户表达的规律。必填参数应该只留给那些业务上绝对必需的字段,其余尽量给模型留出表达空间,让技能内部去消化模糊性。

5.3 坑三:技能内部重试与超时没有策略

这是最枯燥但最实用的一条。早期我们有一个查天气技能,内部调第三方天气 API。偶尔 API 返回超时,技能直接抛异常,模型只能回复"查询失败"。用户觉得智能体特别蠢,因为天气这种东西换个渠道随时能查到。

排查发现,第三方天气 API 的超时阈值是 3 秒,但天气服务在高峰期平均响应就要 2.8 秒。这意味着经常卡在超时边缘,成功率自然不高。我给技能内部加了两条策略:

  • 超时时间从 3 秒调整到 5 秒,并设置一次重试,重试间隔 500 毫秒。
  • 重试仍然失败时,触发备选数据源,而不是直接抛出异常。

改完之后,查天气技能的成功率从 82% 提到了 98%。这个改善没有任何模型层面的改动,纯粹是把工程基本功补齐了。很多 agent-skills 的问题,最后查下来根本不是 AI 的问题,而是普通后端工程的健壮性问题。别一上来就怪模型。

5.4 坑四:测试时用固定 Prompt,上线被真实对话击穿

这个坑非常有代表性。我们的技能开发完成后,测试阶段用的是预先写好的标准 Prompt,比如"查一下北京到上海的高铁"。每次都完美通过,于是信心满满地上线。

上线第二天就出问题。用户问的是"帮我看看我上周买的那趟车还在不在"。这句话里没有任何明确的城市名,模型需要结合多轮对话里的历史信息才能补全参数。我们的技能直接暴露了,参数缺失导致调用失败,又触发不了兜底逻辑,只能给用户一个非常僵硬的失败回复。

修复的核心思路不是去改技能逻辑,而是在技能之上加一层"会话上下文补全器"。在把参数交给技能之前,先用模型从对话历史里提取缺失的字段,补全后再调用。同时,测试集里加入一批真实用户的话术,尤其是那些省略了关键信息的模糊表达,做一轮针对性回归。

我现在的习惯是:每个技能发布前,至少准备十条真实场景的模糊表达,专门用来验证技能的鲁棒性。你写出来的技能不是要应付标准句式,而是要应付那些省略、指代、口语化表达。

5.5 排查链路:一次"技能未调用"的完整追踪

最后给一个通用的排查流程。假设用户问了一个问题,但 Agent 完全没有触发任何技能、直接自己回答了。这种情况比"调用出错"更隐蔽,因为系统看起来一切正常,只是结果可能不靠谱。

我排查时会按照下面这条链路一步步走:

  1. 拉取该请求的完整日志,先确认模型是否拿到了技能列表。有时候注册表加载失败,技能列表是空的,模型只能自由发挥。
  2. 确认模型拿到了列表,再看模型的能力配置里是否启用了技能调用。有些模型在参数里设置了disable_tool_use,请求到了也不会触发。
  3. 确认模型已经触发选择逻辑,再看用户的输入是否被 Prompt 里的其他指令干扰了。比如系统提示词里写了"你是一个知识问答助手",模型就会倾向直接回答而不是调技能。
  4. 如果一切正常但就是没调技能,用一个已知的、必须调用技能才能回答的问题压测,判断是不是描述的问题导致模型完全没有关联到该技能。

这个流程我走了不下十次,每次都能找到不同环节的问题。它不是一个高深的方法论,但能把"看似随机"的故障变成"可定位的环节问题"。排查 agent-skills 问题千万别凭感觉猜,一定要按数据链路一层一层摸过去。

6. 用数据说话:技能评估与持续迭代

技能体系不是搭完就完事的,它需要持续迭代。而在迭代之前,你得先有数据支撑。这一章讲评估和迭代,也是这个项目的收尾部分。

6.1 记录每一次技能调用的原始上下文

评估的前提是记录。前面我提过日志要包含原始输入、参数快照、耗时、错误信息。这里再强调一层:日志一定要保存原始上下文,包括用户完整的话术和模型决策时的提示词快照。

为什么这么强调"原始"?因为后期你分析"为什么模型选错技能"时,如果没有原始输入,你完全无法还原模型当时的决策环境。光是"用户问的是高铁"这一句话,不足以支撑评估;你要看到上一个对话轮的上下文、系统提示词是否发生了变化、其他技能的描述是否也参与了竞争。

我在实践中会把每次技能调用的"决策上下文"存到一张独立的表里,字段如下:

字段说明
request_id全局请求 ID,串起所有日志
user_message用户完整原始输入
system_prompt当前系统提示词快照
skill_list模型当时的技能候选列表
chosen_skill最终选中的技能
correct_flag人工标注或规则判断是否选对
latency_ms技能执行总耗时
error_info错误信息(如有)

有了这张表,你的技能评估就不再依赖感觉,而是能直接量化。

6.2 最小可用指标:调用成功率、纠偏率、替代率

评估指标不用贪多,我长期只盯三个:

第一个是调用成功率。统计学上,技能被选中后是否成功执行、返回结果是否符合预期。这个指标反映技能的工程健壮性。

第二个是纠偏率。纠偏率指的是用户对技能执行结果不满意、要求重新处理的比例。这个指标能间接反映参数抽取和结果质量。如果某个技能的纠偏率特别高,说明它虽然能跑通,但结果和用户真实意图匹配度差。

第三个是替代率。替代率是用户请求本应命中技能 A,但模型最终选了技能 B 的比例。这个指标直接暴露描述混淆和路由质量问题。替代率高的技能,描述边界该重新打磨了。

这三个指标配合原始日志,基本能覆盖技能层大部分问题的诊断。

6.3 技能灰度:别一次推全量

技能迭代里我栽过一个跟头。那次改了一个技能描述,觉得只是优化措辞,风险很低,就直接全量发布了。结果线上选错率瞬间升高,我们花了半天才定位到是描述改动引起的。

从那以后我立了一条规矩:任何技能改动都要灰度。灰度机制不复杂,最简单的方式是给技能加一个revision_id,线上按比例把请求切换到新旧版本上,对比关键指标,稳定以后再全量。具体做法是:注册表里的技能可以同时存在多个 revision,调度器按预设比例路由流量。

对于没有条件做完整灰度环境的小团队,我建议至少做"影子对比":把新版本技能同时在一部分请求上做模拟调用,只输出结果但不实际影响用户,和旧版本做离线对比。这样成本不高,却能在正式发布前暴露很多问题。

6.4 版本管理与回滚:技能也要有 changelog

最后补一个容易被忽略的工程习惯:每个技能都要有清晰的版本历史和回滚方式。技能改动不像普通代码,它影响的是一整套模型决策逻辑,而且影响可能延后显现。没有版本历史,你是没办法安全回滚的。

我的做法是每个技能目录下维护CHANGELOG.md,每次改动哪怕只是一句话,也要写明"改了什么、为什么改、预期影响是什么"。回滚方式则直接依赖 git:每个技能独立文件夹,改动走 commit,上线走 tag。出了问题,git revert就完事了。

这也是为什么我坚持每个技能独立目录、独立版本号。如果所有技能混在一个大模块里,你连回滚一个技能的余地都没有,只能全体回退,连带把不想回退的改动也拖下水。

说到底,agent-skills 是一个需要认真对待的工程层。它不是模型的附属品,也不是能临时拼凑的螺丝钉。给技能的描述多花半小时打磨,给技能的日志多留一个字段,给技能的版本多写一行 changelog,这些看似琐碎的习惯,会在你业务规模变大之后,变成你最坚实的护城河。我自己就是这些方法论的直接受益者——从最初频繁翻车,到现在新技能基本两天内就能稳定上线,靠的全是这一套体系。如果你也在做 Agent 方向的开发,希望这篇总结能帮你绕开我踩过的那一圈坑。

返回列表