ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent技能化实战:让大模型从“乱编”到可靠执行

AI Agent技能化实战:让大模型从“乱编”到可靠执行 说实话这个项目最初并不是我刻意设计的。事情得从我维护的一个知识库问答机器人说起模型聊得头头是道什么技术问题都能给你说出个一二三但一旦让它真正去查数据库、拉报表、调接口就开始胡编乱造。不是它不聪明而是它根本不知道自己能调用哪些能力、怎么调用。后来我把高频操作全部封装成一个个技能让模型在技能列表里做选择、填参数、执行、取结果整个系统的可靠性肉眼可见地提升。这个项目对外就叫 agent-skills核心是一套围绕智能体技能设计、注册、编排和运维的完整方案。这篇文章我会把整个思路和实操过程拆开讲清楚。不管你是正在做 AI Agent 应用的开发者还是刚接触大模型应用想找一条稳妥落地路径的工程师应该都能从中拿到可以直接参考的东西包括技能描述怎么写、参数 Schema 怎么定、运行时怎么设计、多技能打架怎么处理。1. 为什么要做技能化先承认大模型是个嘴强王者1.1 一个典型的失控现场先讲一个我在项目早期遇到的场景。当时我的知识库机器人接入了大模型用户问帮我查一下上周所有订单的总金额这属于一个再简单不过的数据库查询任务。第一版实现是这样的把数据库表结构塞进系统提示词让模型直接写 SQL 并执行。结果模型确实生成了 SQL而且语法完全正确但表名记错了把order_detail写成了order_detail_log有时候又把SUM(amount)写成了COUNT(amount)。更要命的是当查询结果为空时模型会在回答里煞有介事地编一个总计 38000 元出来——它压根没意识到要检查执行结果是否为空。这个问题的本质是什么大模型的知识截止日期、推理能力和胡说八道倾向都是原生的它确实知道很多概念但它没有稳定的行为边界更没有可靠的外部世界感知能力。你让它自由发挥它就是在赌概率。而现实业务不允许赌概率。1.2 技能的本质把模型知道变成模型会做Agent Skills 化的核心思路就是把让模型自己想办法变成让模型在我们给定的工具箱里做选择。一个技能本质上是一个预置好的、可复用的能力单元它有固定的名称、明确的描述、定义好的参数结构、标准化的输出格式以及一段真正由代码执行的逻辑。这个思路听起来简单但做起来有讲究。最关键的心理模型转变是你要把技能当产品设计而不是当函数写。函数只是给程序员用的技能是给模型用的。模型不是一个会读注释的工程师它是一个拿着说明书挑选工具的学徒——说明书如果不清晰它就会选错工具、填错参数、理解错输出。我当时定的原则是任何需要外部副作用读库、写文件、调接口、发消息的操作一律不做自由发挥全部封装成技能任何有确定性流程的操作查天气、翻译、格式化、计算也尽量封装成技能。留给模型的自由空间只有选择哪个技能和怎么填参数这两件事。1.3 什么任务适合被封装成技能不是所有东西都适合做成技能。封装技能是有成本的每增加一个技能模型在做选择时就要多看一段描述判断负担也会增加。我总结了三个适合技能化的特征高频一个操作如果一个月用不到一次先别急着封装让模型自由发挥或者手动处理就行。高频操作才值得你把它的流程固定下来。有明确的成功标准查询订单金额成功与否数据库能告诉你发一封邮件成功与否接口返回值能告诉你。这种可验证的任务才适合技能化因为你能在运行时判断技能是否真正生效。流程确定性强一个技能应该对应一条稳定、可复用的执行路径。如果你发现每次执行都要根据用户的话做大量分支判断那说明这个技能其实是个子流程应该继续拆或者干脆留给编排层处理。反过来那些开放性极强的任务——写一篇文章、分析一个复杂问题的利弊——就不太适合做成技能因为它们没有标准输出结构硬做只会得到一个又笨重又难用的伪技能。2. Skill 格式设计让模型看得懂、调得对、用得稳2.1 元信息是给模型看的说明书一个技能在模型面前表现为一段文本描述和一串参数定义。这个描述的好坏直接决定了模型在关键时刻会不会选错技能。我见过很多人写技能描述极其敷衍比如查询订单四个字就完了结果模型面对帮我看看这个月总共花了多少钱这种稍微绕一点的请求就不知道去调用它。我的经验是技能描述要回答四个问题这个技能是干什么的、什么时候适合用、什么时候绝对不要用、输入参数分别是什么意思。不要嫌长模型读文本的成本很低但理解错误的成本很高。以下是我后来固定下来的一套技能的完整定义示例{ name: query_sales_report, description: 查询销售汇总报表。当用户询问订单金额、销售总额、销量、客单价、同比环比等经营数据时使用。仅用于汇总统计不返回明细数据如需查询具体订单明细请使用 query_order_detail。, parameters: { type: object, properties: { start_date: { type: string, format: date, description: 统计起始日期格式YYYY-MM-DD默认本月1号 }, end_date: { type: string, format: date, description: 统计结束日期格式YYYY-MM-DD默认当天 }, dimension: { type: string, enum: [day, week, month], description: 汇总维度默认month } }, required: [start_date, end_date] } }注意这里面几个细节描述里写了仅用于汇总统计不返回明细数据这句话是在主动给模型划边界避免它拿这类技能去查明细。描述里还写了如需查询具体订单明细请使用 query_order_detail这是在给模型做技能分流告诉它同类问题还有另一个选项。参数描述里带了默认本月1号、格式YYYY-MM-DD这是给模型提供填充参数的依据。你永远不要指望模型天然知道你的业务里起始日期该怎么传。2.2 参数 Schema 的克制原则参数 Schema 是另一个容易踩坑的地方。你会发现给模型传参数和给函数传参数完全不是一回事。程序员调用函数时能保证参数类型正确但模型填参数时是根据用户说的话去推断的它经常填错、填漏、填出明显不合逻辑的值。所以参数设计一定要遵循克制原则。能不加的参数就不加必要参数最少化枚举值能列就列。比如上面的dimension字段我直接给它限定成了[day, week, month]三个值模型就不会脑补出year或者daily这种拼写。再举一个真实教训。我早期有个技能是发送消息的参数里有一个priority字段我当时写的是type: string没给枚举。结果模型在一次调用里填了urgent_very_much——它确实理解了意图但参数值完全不合法。后来我把这个字段改成枚举[low, normal, high]坏参数的问题彻底消失了。另外一个很多人忽略的点给每个参数加示例值。例如参数描述里可以写成举例2024-05-01模型在选择和填充参数时会更稳定。对大模型来说一个具体例子胜过十句抽象描述。2.3 输出规范与失败语义技能执行完的输出也要有标准结构。否则模型拿到一段乱糟糟的结果再把它转述给用户时很容易转着转着就添油加醋了。我通常要求每个技能输出两种东西一个是计算机可读的结果通常是 JSON另一个是可选的注意事项文本。JSON 结构要扁平、字段名要见文知义。比如{ success: true, summary: 5月销售总额为1,234,567元环比增长12.3%, records: [] }特别重要的是success字段和失败语义。技能执行失败时输出里要带上清晰的错误原因但不允许模型自己脑补结果。我在提示词里给模型明确规则当技能的 success 为 false 时直接向用户说明失败原因不要继续编造答案。这条规则救了我很多次。你想想如果技能查询报错模型拿着一个error对象仍然可能硬着头皮说您的订单总额是五万元——因为它有极强的给用户一个答案的冲动。你必须在系统层面对这种冲动做约束。3. 技能注册、发现与调用一个轻量级运行时3.1 注册表与技能清单有了技能定义接下来需要一个运行时来管理它们。我这个项目里实现了一个非常轻量的技能注册表本质上就是一个内存字典外加动态扫描逻辑。每个技能在注册表里对应一个条目包含它的定义、执行函数、以及安全级别等信息class SkillRegistry: def __init__(self): self._skills {} def register(self, skill_def: dict, executor: Callable): self._skills[skill_def[name]] { definition: skill_def, executor: executor } def list_skills(self) - list[dict]: return [s[definition] for s in self._skills.values()] def get_executor(self, skill_name: str) - Callable: return self._skills[skill_name][executor]模型在每一轮对话中看到的技能清单就来自list_skills()。这个清单会以文本形式注入到系统提示词里或者以结构化工具列表的形式传给模型接口。关键是模型每次决策时能看到全部技能这个清单就是它的工作台。随着技能数量增长清单会越来越长。这个时候要考虑分层展示核心高频技能始终出现在最前面低频技能折叠在后面。实测下来技能顺序对模型的选择有肉眼可见的影响——排在前面的技能被选中的概率显著更高尤其是当两个技能描述相似时。所以我把常用技能排在前面不常用的放在后面并在这两者之间用一批特殊说明分隔。3.2 技能定义的动态加载与上下文压缩技能定义本身也是有 token 成本的。一个技能平均消耗 200-400 token 的描述如果有 50 个技能那就是 1 万到 2 万 token——这还不算参数定义。所以不仅要考虑注册还要考虑怎么在每一轮请求里裁剪技能列表。我采用的是静态注册 动态选择策略。系统启动时把所有技能加载进注册表但每次构建请求上下文时不把全部技能塞给模型而是先用一个快速匹配层做粗筛。粗筛逻辑很简单把用户最近的输入拿去跟技能描述做关键词重叠度匹配匹配度超过阈值的技能进入候选列表最多保留 15 个再连同你可能还需要的技能一起交给模型。这个策略帮我省了大量上下文空间也让模型做决策时更专注。不过这里有个微妙点粗筛逻辑如果用得太激进会漏掉用户没直接说但确实需要的技能。比如用户说帮我看看销售额涨没涨关键词涨跟query_sales_report的描述同比环比其实没有直接重叠但语义上是匹配的。所以我额外加了一条规则系统内置一组兜底技能比如通用查询、通用搜索这些在任何情况下都会出现在候选列表里。3.3 一次完整调用的循环整个技能调用的运行时循环我用伪代码描述一下1. 接收用户输入 2. 粗筛候选技能列表 3. 把候选技能定义 用户输入 对话历史 组装成提示词请求模型 4. 模型返回结果 - 若选择调用技能解析出技能名和参数 - 若选择直接回复跳过技能调用 5. 根据技能名从注册表取执行函数执行 6. 将技能输出结构化结果回填给模型请求模型基于结果生成最终回复 7. 返回最终回复给用户第 5 步到第 6 步之间的衔接是很多人的盲区。我见过不少实现技能执行完以后直接把原始 JSON 丢给用户用户看到{success: true, summary: ...}一头雾水。正确做法是让模型基于技能结果进行解释而不是直接展示技能结果。我通常在第 6 步的提示词里加入类似这样的说明以下是工具执行后的结果请你用通俗的语言向用户转达关键信息。如果结果为失败请直接说明失败原因不要编造数据。这一步的模型调用通常比第一步短但因为又生成了一轮文本整体延迟会相应增加。实测中我遇到过用户嫌慢的情况后来做了个优化对于success为 true 且输出里自带summary字段的技能直接展示 summary跳过第二步模型调用把延迟降下来。这个优化在我项目里节省了大约 40% 的响应时间。4. 编排与路由多技能协同的实战取舍4.1 串行编排与条件分支当你只有三五个技能时让模型自由选择就够了。但技能数量超过十个以后你很快会遇到一个场景一个用户请求需要多个技能按顺序执行。比如帮我看一下上海和北京这两个城市的天气哪个更适合出差——这需要先查上海天气再查北京天气然后比较。这就是编排问题。我第一版实现里没有任何编排能力模型一次性只能调用一个技能。结果遇到这种请求模型要么只查一个城市要么自己编造另一个城市的天气。后来我加了一个非常基础的编排支持允许模型在一次回复中发起多个技能调用。这个改动看起来简单但实现时要注意执行依赖。两个技能之间没有依赖关系的可以并行执行有依赖关系的后一个要用前一个的输出作为参数必须串行。我一开始偷懒全部并行执行结果发现先创建订单再发送确认邮件这种依赖链在并行执行下彻底崩坏。最终我采用的方案是分两个阶段先做无依赖的并行批次然后收集结果再做有依赖的串行步骤。这个方案实现简单对模型的要求也低绝大多数实际场景已经够用了。4.2 技能描述互相干扰一个隐蔽的坑技能多了以后最隐蔽的问题不是单个技能定义不够好而是技能与技能之间的描述互相干扰。典型案例我有两个技能一个叫get_stock_price查股价一个叫get_market_news查行情资讯。在get_stock_price的描述里我写了一句话当用户想了解市场动态时也可使用结果模型面对一个只是想要行情资讯的请求乱选了get_stock_price。这个问题的根源是描述里的松弛语句会被模型过度泛化。你在技能 A 的描述里提了场景 X而技能 B 才是真正覆盖场景 X 的模型就很容易被带偏。我的处理方式是什么在技能描述里加负面约束。每个技能描述都明确说明该技能不适用于哪些场景并且同类技能之间要互相可见、互相牵引。就像前面提到的query_sales_report里写了如需查询具体订单明细请使用 query_order_detail这样模型看到这句话就会知道它不仅选对了技能还明确知道了边界在哪。4.3 降级策略技能失败时怎么办技能调用不是百分百成功的。数据库可能连不上、第三方接口可能超时、参数可能让执行函数抛出异常。我在前面提到过失败语义但编排层还需要一个降级策略。我的做法是给技能分级不同等级对应不同的失败处理方式。一等技能比如查询类失败后直接向用户说明不做重试不编造替代数据。二等技能比如发送类失败后允许自动重试一次。三等技能比如生成类允许模型在失败后改用通用能力继续因为这类技能本身没有硬性成功标准。这个分级策略让我避免了两个极端一是无情地失败二是不计代价地反复重试。特别是当某个技能连续失败三次时我会让模型停止尝试并把失败信息记录到日志里方便后续人工排查。5. 让 Agent 技能从能用到稳定用测试与迭代5.1 回归测试集是底线我见过太多项目Agent 原型跑通以后就急着一路狂奔加功能等某天某个场景突然坏了才想起要回头做测试。这里我想强调一个观点Agent 技能体系的测试成本远远高于传统函数测试因为你不只是在测代码逻辑还在测模型的选择行为。所以从项目一开始我就建立了一个回归测试集里面全是真实用户问过的、带标准答案的问题。比如上周的销售额是多少 → 期望调用query_sales_report且 start_date/end_date 参数正确帮我看看 WQ210 这个订单的明细 → 期望调用query_order_detail且订单号参数正确今天天气怎么样 → 在有天气技能时不可调用订单相关技能测试集跑起来以后我会关注两个指标技能选择准确率和参数填充正确率。不夸张地说每次修改技能描述这两个指标都会发生波动。有时候你以为是在优化一个描述结果把模型选技能的行为带偏了而回归测试集就是抓这种回归的唯一手段。我建议把测试集做成半自动的用脚本构造输入用模型对输出做标注助手判断技能选择是否符合预期。人工只需要抽查那些模型标注为不通过的用例。这样迭代速度能快很多。5.2 从日志里发现技能设计缺陷日志是另一面镜子。我把每一轮对话的关键信息都记录下来包括用户输入、候选技能列表、模型最终选择的技能、填充的参数、技能执行结果、用户最终反馈如有。这个日志有两个巨大价值。第一你可以在日志里找到模型选错了但用户没抱怨的情况——用户可能通过追问或者换说法绕过去了但你这个技能体系其实已经失败了一轮。第二你可以找到参数填错的模式比如某个参数频繁填错说明描述写得不清楚。我举一个实际例子。我的send_mail技能里有个recipient参数描述写的是收件人邮箱地址。实测中发现模型经常把用户的姓名当成邮箱地址传进去比如传张三而不是zhangsanexample.com。日志里这个错误出现了四次之后我在参数描述里加了一句必须是完整的电子邮件地址形如 namedomain.com如果不是邮箱地址请先向用户确认。从那以后这个错误基本绝迹。这种迭代很朴素没有花哨的技术但非常有效。我的建议是每周固定抽半天时间翻一遍这一周的 Agent 日志每次至少要能找出一个问题和一个优化点。坚持下去你的技能体系就会越来越稳。5.3 迭代闭环的实操例子再分享一个具体的迭代闭环。我的知识库机器人早期有个技能叫search_knowledge_base用来检索内部文档返回相关片段。最初版本的描述是根据关键词搜索内部知识库文档返回相关片段参数只有一个keyword。后来日志暴露了两个问题用户问怎么申请报销时模型填的 keyword 是申请报销四个字。但知识库的文档标题都叫费用报销管理办法、差旅报销流程关键词匹配几乎找不到东西。用户问我需要报销差旅费请问需要什么材料时模型把整句话当作 keyword反而匹配不到准确片段。针对这两个问题我做了两处修改参数从单个keyword改为keywords数组允许模型拆出多个关键词提高召回率。描述里加了一段搜索建议请将用户的问题拆解为 2-3 个核心关键词优先使用文档类标题中的专业术语而不是口语化的表达。改完后同一批测试问题的检索质量明显提升。这个过程大约花了一个小时但效果立竿见影。我想说的是Agent 技能永远没有写完就完事的时候它是一个持续打磨的过程。技能描述要改、参数要调、测试集要扩这是很正常的状态。6. 踩坑记录技能化过程中最容易被忽视的四个问题6.1 技能粒度要么太粗要么太碎技能粒度是我反复调整最多的地方。早期我习惯把查客户信息做成一个技能参数一堆执行函数里又分了好几个分支查客户基本信息走一个分支查客户订单走另一个分支查客户欠款走第三个分支。结果模型经常选错分支填了一堆用不上的参数。后来我把这个粗技能拆成三个get_customer_basic、get_customer_orders、get_customer_balance每个技能执行函数都极其简单参数极少模型选择准确率直线上升。但也不要拆得太碎。我有一次甚至想把查订单总额和查订单数量拆成两个技能后来发现它们描述过于相似模型经常混淆反而增加了选择难度。最终我把它们合并成一个query_sales_report用参数区分统计口径。合适的粒度大概就是一个技能对应一个用户意图而不是对应每个函数。6.2 描述里的伪精确比模糊更危险写技能描述时人很容易犯一个错误堆叠大量精确但模型无法有效利用的专业术语。比如我早期写过一个技能描述里面全是ESG 指标PMI 指数环比口径同比口径这类词汇看起来精确无比实际模型根本不知道这些词对应的具体场景边界。更危险的是有些描述里写了一些具体数字或者规则比如查询超过 90 天的未支付订单模型可能会把这个90 天当作硬性条件去筛选哪怕用户根本没有提到时间。这就是伪精确的副作用。我的原则是描述里只写模型需要用来做决策的信息——什么时候用、什么时候不用、参数要注意什么至于执行逻辑内部的复杂规则全部放到代码层处理不要暴露给模型。很多规则根本不需要模型知道模型只知道这个技能能查未支付订单就够了。6.3 技能的环境依赖没写清楚这个坑更像工程问题。技能执行函数通常会依赖外部环境——数据库连接、Redis、第三方 API Key、文件路径、权限配置。项目跑在开发环境没问题部署到预发环境就崩往往就是环境依赖没有管理好。我早期部署时遇到过一个特别尴尬的问题send_mail技能在开发环境能正常发信因为配置里指向的是一个本地 SMTP 服务但部署到服务器后服务没起来技能直接报错。我的日志里只记录了发送失败没有记录具体原因导致排查了很久。后来我做了两件事一是每个技能定义里增加一个dependencies字段写明它依赖哪些外部资源二是技能执行函数统一包一层异常捕获把失败原因完整记录到日志里。现在任何技能出现环境问题我都能在五分钟内定位到是数据库挂了、Redis 没连上还是 API Key 过期。6.4 技能之间互相争抢调用权最后一个坑来自热情过头的技能。有些技能描述里为了展示能力我会写也可以处理 XXX 场景结果这个技能把本来属于另一个技能的用户请求抢走了。最典型的是search_web——我把它的描述写得很宽泛可以查找各类公开信息。结果用户问我们公司上个季度的销售额是多少模型居然去搜索网页而不是用内部的query_sales_report技能。这个行为非常隐蔽因为模型给出的答案看起来还挺合理的但实际上完全不是用户想要的内部数据。根因在于技能描述的势力范围没有划清楚。每个技能都应该有清晰的主场以及明确的非主场声明。search_web的职责应该是查找公开的互联网信息并明确如果用户询问内部数据或私有系统数据请勿使用本技能。这种主动划界的描述能有效防止技能之间互相争抢。最后说点实际的从整个 agent-skills 项目做完到现在我最深的体会是给智能体做技能体系本质上是在做一套人机协作的接口规范。你设计的不是代码是大模型与真实世界之间稳定交互的桥梁。所有踩过的坑几乎都指向同一个教训——永远不要高估模型的理解能力也永远不要低估描述文本的影响力。如果你打算在自己的项目里引入这套思路我的建议是先做减法从两三个最核心的技能开始跑通整个调用循环把日志和回归测试集搭好再逐渐扩大技能数量。技能不是越多越好而是越精准越好。最后再分享一个小技巧技能描述的每一版改动都保留一个版本记录。模型行为有时候会因为一句话的变化产生连锁反应有了版本记录你才能快速回滚到上一版明明还可以的状态。这些小细节往往才是决定一个 Agent 项目能否稳定跑下去的关键。
返回列表