news 2026/10/28 4:03:04

从工具到技能:Agent稳定执行多步任务的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从工具到技能:Agent稳定执行多步任务的实战指南

很多人在做Agent开发时,都会遇到一个尴尬的阶段:演示Demo跑得很顺,一问一答都像模像样;可一旦把任务跨度拉大,比如让它连着处理“查天气→订会议室→写会议纪要→发邮件”,模型就开始在各个工具之间手忙脚乱,甚至干脆漏掉某一步。我遇到过最离谱的一次,客服Agent在用户问“明天会下雨吗”时,居然一本正经地编了一个晴天预报,因为它根本没有把“天气查询”作为一个可见、可控的技能挂载上去。

后来我把项目的重心从“堆工具”转向“立技能”,起名就叫agent-skills。它不是一套花哨的框架,也不是某个特定平台的插件,而是一种组织Agent能力的方式:把模型可以执行的每一个动作,从“一个函数”升级成“一套完整的操作契约”。这篇博文会完整拆解我在这个项目里的设计思路、注册表结构、路由策略,以及我和团队在实测中踩过的坑,给正在做Agent落地、尤其是想让Agent稳定执行多步任务的开发者一个可以直接抄作业的参考。

1. agent-skills解决了什么问题:一个被名字耽误的关键组件

1.1 从“工具越多越聪明”的误区说起

刚开始做Agent的时候,我和很多人一样,以为只要往系统里塞的API越多,模型就越“能干”。结果恰恰相反。工具列表一旦超过20个,模型的选择就开始飘忽不定:同样一句“帮我查一下上个月的报销单”,它今天调对接口,明天就选错函数;更麻烦的是,每个工具的参数格式不一致,有的要时间戳,有的要“YYYY-MM-DD”字符串,模型捏造参数的情况成了家常便饭。

问题出在哪里?出在工具只是“能力的最小切片”,它只告诉模型“你能做这件事”,却没有告诉模型“在什么条件下做、按什么顺序做、做坏了怎么收场”。而技能(skill)恰恰补上了这一层:它是一个带触发条件、执行步骤、参数约束、失败回退的完整行为单元。用生活里的例子来说,工具是一台电钻,技能是“在混凝土墙上打一个直径8毫米的孔”的标准操作流程——后者才是一个真正能交付的结果。

1.2 agent-skills的定位:不是工具层,而是行为层

我在设计agent-skills时,给自己定了一个原则:它在架构上位于LLM和工具之间,但它既不属于模型层,也不完全属于工具层。它更像是给Agent装了一套“职业手册”,告诉模型在特定场景下该如何调用自己的身体。

具体来说,一个技能条目需要包含三类信息。第一是触发契约,什么类型的用户意图应该激活它,什么情况下必须拒绝激活;第二是执行协议,调用哪些工具、按什么顺序、参数如何映射,每一步的校验规则是什么;第三是退路方案,主路径失败后是降级、重试,还是把控制权交还给用户。

这样的设计带来的直接好处是,模型不再需要在“20个平铺的函数”里做选择题,而是在“4到5个命中的技能候选项”里做判断题。选择范围变小,准确率自然上来。我自己的项目里,意图路由准确率从早期的78%左右提升到了94%上下,代价只是多写了一层注册表。

1.3 它不是一个新语言,也不是又一个Agent框架

有人看到agent-skills这个名字,会以为它是什么新的Agent DSL或者运行时框架。不是。它就是一个结构化的能力组织方式,你可以把它用LangChain实现,用LlamaIndex实现,也可以直接手写在Flask服务里。关键不在于用了什么库,而在于你承认了“技能”这个抽象层次的存在,并愿意为它建立一套管理规范。

在后面的章节里,我会给出我在项目中实际使用的注册表结构、路由评分算法和编排策略,你可以直接参考这些设计去搭建自己的版本,不需要依赖任何我这里的代码运行时。

2. 把“技能”拆成可管理的单元:三条核心设计原则

2.1 原则一:技能描述必须写成“触发契约”

这是我在实际使用中体会最深的一点。很多人写技能描述,喜欢写成功能说明:“本技能用于查询天气信息。”这种写法对模型来说几乎等于没用。模型是一个基于概率的文本预测器,它需要的是“当用户表现出何种意图时,该技能响应”,而不是“该技能是什么”。

我总结了一个触发契约的模板,分为三段:适用场景(用户明确提出或隐含需要什么)、边界条件(哪些相似意图不该命中本技能)、参数来源(执行所需的参数从对话的哪个部分提取)。一个合格的天气技能描述,至少应该长成下面这样:

skill_id: weather_query name: 天气查询 description: > 当用户询问当前天气、未来天气预报、降雨概率、温度、风力等天气相关信息时触发。 包括隐含的出行决策需求,例如“明天适合跑步吗”“周末能去爬山吗”。 不包括:历史气候统计、空气质量指数查询(请使用 air_quality_query 技能)。 parameters: location: source: user_query type: string required: true date: source: user_query type: string required: false

这套写法的逻辑是:让“触发条件”在描述里占据超过一半的篇幅。模型内部实际上是在做“这段用户输入和技能描述是否语义相关”的匹配,描述里场景越多、边界越清楚,匹配就越稳定。

2.2 原则二:每个技能必须自带“验证钩子”

Agent执行技能时最怕两件事:参数不对、执行结果不符合预期。光靠模型“自我检查”是没用的,就像你不能让一个刚学开车的人一边开车一边评价自己的车技。正确的做法是把验证工作外置成一个个小的检查项,挂载在技能的入口和出口。

我管这叫验证钩子(validation hooks)。入口钩子检查参数——比如日期字段必须是“2025-06-01”这样的格式,如果模型给的是“明天”,钩子会尝试换算成具体日期;换算不了就返回参数错误,让模型重新从对话里提取。出口钩子检查结果——比如订单查询技能的结果必须包含订单号,如果没有,说明工具返回异常,需要触发重试或者降级。

def validate_parameters(skill, extracted_params): errors = [] for field, rule in skill.parameter_rules.items(): value = extracted_params.get(field) if rule.get("required") and not value: errors.append(f"{field} is required") if value and rule.get("format") == "date": if not parse_date(value): errors.append(f"{field} must be a valid date") return errors

这段代码看起来简单,但它解决了一个特别实际的问题:模型填参数天然是“随机的”。同一句“帮我查明天北京到上海的高铁”,它今天能把“明天”解析成正确的日期,明天可能就把“明天”直接当作字符串传给接口。有了入口验证,至少能保证进入业务系统之前,参数是合法的。

2.3 原则三:技能要有“降级链”,不能一条路走到黑

设计Agent技能的时候,最容易忽略的是失败路径。大多数demo只考虑了“技能成功执行”的情况,一旦接口超时、结果为空、权限不足,Agent就卡在原地,或者开始胡说八道。我给每个技能都配了一条降级链:主路径 → 补偿路径 → 兜底回答。

拿“查航班”来说,主路径是调用航班查询API;如果API返回超时,补偿路径是查询缓存里最近一次同样的结果并注明数据时间;再不行就兜底回答:“航班信息暂时不可用,您可以直接访问航空公司的官网查询”,而不是让模型编一个航班号出来。看起来是小事,但在真实的生产环境里,一条降级链能挡住大量用户投诉。

3. 注册表、路由和生命周期:agent-skills的核心架构

3.1 注册表:把技能变成一行行可查询的元数据

所有技能在系统里都不是散装的代码,而是集中登记在一张注册表里。我用SQLite作为存储,每一行代表一个技能,字段包括技能ID、名称、描述(触发契约全文)、参数规则(JSON格式)、启停状态、版本号、最后评审日期。注册表本身不存执行逻辑,它只存“元数据”,执行逻辑是单独注册的Python函数。

这种做法带来的一个直接好处是:你可以通过更新一行数据库记录来调整技能行为,而不需要发布新代码。比如某个技能的触发描述写得太宽泛,经常误命中,我直接改写描述文本,下一次加载时新的行为就会生效。运营人员不需要看懂代码,只需要会编辑数据库行,就能调整Agent的大部分行为边界。

skill_id TEXT PRIMARY KEY, name TEXT, description TEXT, parameter_rules TEXT, allow_auto_invoke INTEGER, enabled INTEGER, version INTEGER, owner TEXT, last_review_date TEXT

这张注册表的“灵魂”是description字段。我甚至可以说,这个项目80%的效果都取决于description写得好不好,而不仅仅是路由算法。因为模型理解的不是你的函数,而是你写给它的那段“触发契约”。

3.2 路由器:用相关性打分替代硬编码关键词

注册表有了以后,下一步是决定“当用户说了一句话,系统该激活哪个技能”。最朴素的做法是关键词匹配,比如用户说“下雨”就触发天气技能。但这种方式太脆了,用户换个说法“带伞”就匹配不上。我采用的方案是基于向量相似度和关键词命中加权的混合评分。

每条用户输入都会先做同一套处理:分词、向量化,然后用它与每个已启用技能的描述向量做余弦相似度计算。与此同时,如果输入文本包含技能关键词表中的词(比如“气温”“风”),就给对应的技能加一个关键词加权分。最后综合得分超过阈值(我设为0.72)的技能入选 candidate pool,送入决策模块。

def route(user_input, skills, threshold=0.72): input_vec = embed(user_input) scored = [] for skill in skills: if not skill.enabled: continue cosine = dot(input_vec, embed(skill.description)) keyword_bonus = sum([1 for kw in skill.keywords if kw in user_input]) * 0.05 scored.append((cosine + keyword_bonus, skill)) candidates = [s for score, s in scored if score >= threshold] candidates.sort(key=lambda x: -x[0]) return candidates

这种混合路由的稳健性在于:当描述向量相似度本身已经很高时,关键词加权只是锦上添花;当向量相似度因为文本太短而偏低时,关键词又能起到补偿作用。实际测试下来,长句意图的命中率提升最明显,因为向量相似度在长语义上表现更可靠。

3.3 生命周期:不用的技能不等于删掉,而是“禁用”

技能并不是一成不变的。一个业务线可能这个季度需要“新冠疫苗接种查询”,下个季度就不再需要了。我的习惯是坚决不删技能,而是给它打enabled = 0。原因很简单,技能的描述文本里沉淀着你对一类业务意图的理解,删了就真的没了,哪天业务回来你还得重新写。

版本字段也很重要。每当我调整技能的描述、参数规则或降级链,我都会把version加一,并在last_review_date上登记当天日期。这样做的好处是:如果新的描述导致路由效果下降,我可以快速回滚到上一个版本,而不是在git历史里翻找。注册表就是Agent的“记忆”,管理好一张注册表,比管理好Agent的代码更能稳定线上行为。

4. 技能编排中的边界问题:自动执行还是手动确认

4.1 技能编排的危险之处:Agent太“主动”也不好

当多个技能命中同一个用户请求时,就涉及编排问题了。比如用户说“帮我查一下明天北京天气怎么样,如果下雨的话就提醒我带伞”。这句话实际上需要命中两个技能:天气查询、发送提醒。如果Agent自动执行提醒,等于替用户做了一个“需要提醒”的假设,万一用户下一个小时计划变了,提醒就变成骚扰。

我的原则是,把技能分成两类:稳定执行的技能和需要确认的技能。查天气、算日期、查运费这类低风险动作,允许Agent自动执行;而发消息、下单、改配置这类有外部副作用的动作,必须走一次用户确认。在注册表里用allow_auto_invoke字段来控制。

4.2 子Agent与技能编排:让规划者只做选择题

还有一种编排模式,更适合复杂任务:用一个“规划Agent”负责任务拆解,然后再把每一步交付给子Agent去执行。但这里有一个常见陷阱——规划Agent一旦被允许“自由生成步骤”,就容易幻觉出不存在的技能名称。我试过让规划Agent输出“调用技能:send_reminder_v2”,结果系统里根本没有这个技能。

解决办法是限制规划Agent的候选集:它只能从注册表里enabled的、并且已在白名单中的技能ID里做选择。换句话说,规划Agent不是“自由写作”,而是“做选择题”。它拿到的技能池在每次会话开始时就从注册表加载并生成了候选清单,超出清单外的一律拒绝。

4.3 技能链与循环检测

多技能编排还会遇到一个很头疼的问题:递归调用。技能A的结果触发技能B,技能B的结果又触发技能A,如果没有循环检测,整个会话就死循环了。我在agent-skills里给每次会话维护了一个“技能调用栈”,当一个技能试图二次进入调用栈时,系统会检查它的上下文是否和上次一样——一样则判定为循环,强制终止并把控制权还给用户。

这个检查看似笨拙,但在真实场景里非常有效。有一段时间我们的数据分析技能经常循环,root cause是分析技能生成了一个新的后续问题,又把自己给触发了。加了调用栈检测之后,这类问题基本根除。我还设了一个硬性上限:单次会话最多执行12个技能调用,超过上限强制结束并输出摘要,避免用户等太久。

5. 实测中的坑与战术应对:三场典型事故复盘

5.1 事故一:技能命中率突然从93%暴跌到82%,原因是一句描述改动

某个周末,我改了“发票查询”技能的描述,加了几个业务术语,结果周一线上命中率掉了一大截。排查过程很典型:先看路由日志,发现“发票查询”的命中次数减少的同时,“报销进度”技能的命中次数增加了——模型把“发票”和“报销”两个概念混在一起了。

根因是我在报销技能的描述里,新加了一句“可能包含发票信息”的说明。模型在做语义匹配时,这句说明让报销技能的向量中心向“发票”方向偏了一点,导致原本应该命中发票查询的请求被分流过去。这件事给我的教训是:技能的边界描述里,千万不要出现“可能包含某某内容”这种模棱两可的话。边界就是边界,宁可说得窄一点,也不要制造重叠。

5.2 事故二:模型给“日期”参数填了“明天”,入口校验拦下但不会换算

这是一个很尴尬的场景。我教模型“明天”要换算成具体日期,结果模型有时候换算正确,有时候直接就把“明天”当参数传出去。入口钩子倒是很尽责,每次都拦下来,但它只返回一个“invalid date”,模型也不知道该怎么修正,就卡死在重试循环里。

后来我在参数规则里增加了parser配置——日期参数先经过解析器处理,解析成功就自动替换成标准格式,解析失败才走重试。这个改动把原本要反复重试的对话,压缩成了单轮解决。我的总结是:别指望模型自己学会格式转换,在验证钩子里做掉它。

5.3 事故三:技能雪崩——一次查询触发了七个子技能

有一次用户问“帮我看看上海到杭州的高铁票还有没有”,结果Agent按顺序执行了车票查询、天气查询、酒店推荐三个无关技能。原因是指定的组合技能“出行规划”的描述太宽泛,把“目的地”和“出行时间”之外的所有相关信息都当成了必须项。

修复方案是给组合技能加了“必要输入条件”校验:只有用户明确提供了两个以上关键参数(出发地、目的地、日期)时,组合技能才被激活;否则只触发单程的车票查询。这个改动直观地削减了Agent“过度服务”的倾向,也让每一轮会话的执行路径更加可预测。经过这次事故,我强制要求每个技能在描述里都写清楚“缺少哪些参数时,本技能不得激活”。

6. 当前技能库的结构与后续扩展思路

6.1 我的技能库现在长什么样

到目前为止,我的agent-skills项目里共有三类技能:原子技能(直接映射到单个API)、组合技能(串联多个原子技能的固定流程)、策略技能(不直接调用工具,而是改变下一步的计划方向)。三类加在一起,在注册表里大约有四十多条记录,其中启用的约二十条,始终维持一个“少而精”的规模。

原子技能:query_weather / query_air_quality / query_calendar 组合技能:travel_planner / meeting_minutes_pipeline 策略技能:clarify_ambiguity / step_by_step_breakdown

我刻意控制了技能数量,因为每一个新技能都会增加路由的噪声。如果一个技能描述写得不好,它不只是“浪费一个位置”,还会拉低其他技能的命中率。所以每新增一个技能,我都会做一个简单的A/B测试:在同样的测试集上,对比加技能前后、其他技能命中率的变化,如果全局命中率下降,就重写描述或者直接不启用。

6.2 技能库的治理:两周一次的重审机制

技能不是写完就一劳永逸的。我每两周会做一次“技能重审”,方法很原始:拉出所有技能的命中率、误触发次数、重试率,逐条过一遍。命中率特别低的不一定是要删除的,也有可能只是描述里的触发场景跟当前业务用语脱节了——更新一下同义词就好了。

重试率高的技能则一定有问题,要么是参数规则太严,要么是降级链设计缺失。我会在重审时重新审视每个技能在上一周期的会话日志,把那些“用户问了很多次但始终没触发正确技能”的对话,当作新场景补进描述,或者干脆拆分出一个新技能。这个“观察→调整→回测”的循环,是整个项目里最花时间但也最值得的一步。

6.3 技术栈与集成建议

如果你准备在自己的项目里实现一套技能治理机制,完全不依赖任何第三方库也可以。我的这套东西只用了:一个SQLite存注册表、一个embedding模型算相似度、一个任务队列调度技能执行。模型层你可以继续用自己习惯的LLM API,技能执行函数也可以用普通的Python实现,整个体系非常轻量。

不过有几点我想提醒一下:

  1. 路由阈值需要根据业务场景调优。0.72是我这组数据上比较平衡的值,但如果你发现用户的表达方式更口语化,可能要调到0.65;如果业务场景更正式单一,0.80也更适用。别指望一套参数打天下。
  2. 技能描述和代码要分开管理。描述并不需要和代码同步发布,它更应该像一份可编辑的业务配置,让不懂代码的人也能参与调整。分开管理之后,调整路由策略的效率会高很多。
  3. 留给“未知意图”一条退路。任何技能都没命中时,我建议默认走一个“澄清意图”的策略技能,让模型反问用户,而不是强行选择相关性最高的那个。这个设计能救回大量边缘case。

6.4 后续还能扩展什么

我现在正在做的扩展方向有两个。第一个是跨领域技能复用——把注册表的schema标准化后,尝试在不同业务线的系统之间共享技能定义,这样在某一个行业项目里写好的技能积累,可以平移复用到另一个场景,省去重复调参的过程。第二个是让技能具备简单的自评估能力,即在技能执行完成后,让Agent基于出口钩子的结果,生成一段简短的“执行摘要”,包括做了什么、结果是否可信、哪些数据缺失。这份摘要既可以给用户看,也可以进日志,作为后续优化技能的语料。

我个人在这几轮迭代里的一个体会是:参数校验逻辑写起来不复杂、路由调一调也只花半天,最花功夫的其实是“观察会话日志、重写技能描述、验证效果”这个循环。它没有一锤定音的时刻,你永远在发现新的边界case,然后把它纳入某个技能的触发契约或边界条件里。这也是为什么我一直觉得,Agent能力的核心不是模型选择,而是你有多认真地管理“技能”这一层元数据——它决定了一个Agent到底是在“鲁莽地尝试”,还是在“稳定地执行”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 9:08:12

目标管理的核心价值与SMART原则实践

1. 目标管理对管理者的核心价值刚接手团队时,我曾在三个月内连续三次调整季度目标,每次团队会议上都能看到组员们困惑的眼神。直到有位资深工程师私下找我:"老大,我们到底该优先做哪个功能?现在所有人都在瞎忙。&…

作者头像 李华
网站建设 2026/10/27 20:39:26

知识工作浏览器插件集:轻量高亮摘录、剪藏与本地搜索实战

1. 项目整体设计与思路拆解1.1 知识工作的真实痛点先说个事儿。我平时的工作状态,大概就是“浏览器开了二十几个标签页,脑子里一团浆糊”。看到一篇有价值的测评文章,划几句重点,关掉;第二天发现想引用的时候&#xff…

作者头像 李华
网站建设 2026/10/27 22:17:30

159 tok/s!本地27B模型实测:AI编程离替代云端还有多远?

1. 159 tok/s 意味着什么:先别急着激动先说结论:这个数字放在半年前,我连想都不敢想。Qwen3.8-27B(实际为 Qwen2.5 最大规模代码系列的本地量化版本,下同)在本地跑出 159 tok/s,什么概念&#x…

作者头像 李华
网站建设 2026/10/22 19:50:12

LiteLLM统一大模型API网关实战:多模型接入与密钥管理

写 AI 应用最头疼的从来不是“模型不够聪明”,而是每个模型都有自己的接口脾气。最早就 OpenAI 一家的时候很清爽,一个 SDK 打通所有。但后来局势变了,Anthropic 杀进来了,各种国产大模型也排着队上线,再加上本地用 Ol…

作者头像 李华
网站建设 2026/10/27 23:50:34

Interposer技术解析:2.5D/3D封装的核心突破

1. Interposer技术概述:芯片封装的革命性突破在半导体行业持续追求更高性能、更低功耗的今天,传统封装技术已经逐渐接近物理极限。Interposer(硅中介层)作为2.5D/3D封装的核心组件,正在重新定义芯片性能的天花板。这种…

作者头像 李华
网站建设 2026/10/22 13:44:00

轻量实时事件分析框架REA:从事件接入到秒级推送的实战解析

REA这个代号,最早是我笔记本上一个文件夹的名字,后来慢慢长成了一个轻量实时事件分析框架。简单说,它解决的是这么一类问题:传感器在每秒不断上报数据,日志像流水一样往外冒,前端想看到的不是冷冰冰的离线报…

作者头像 李华