news 2026/10/7 21:04:47

Agent技能体系实战:从提示词到稳定可复用的智能体技能库

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能体系实战:从提示词到稳定可复用的智能体技能库

我做了大半年Agent相关项目,有个很深的体会:绝大多数“看起来不够聪明”的智能体,问题压根不在模型本身,而在它没有一个像样的技能体系。模型明明能力不差,上下文也给足了,但行为就是飘忽不定——今天按A路径走,明天偏要自己开发个B方案,换个措辞结果完全变样。我一开始也怀疑是模型不行,后来把几十段对话日志翻出来逐条看,才确认问题出在哪:它根本不知道“自己能做什么”,所有操作都靠提示词现场发挥,自然时灵时不灵。

后来我开始认真搞agent-skills,也就是给智能体搭一套可复用的技能库,把底层的操作能力一个个定义清楚、注册好,让模型像查工具手册一样去检索和调用。改完之后,同样一批任务,成功率和行为确定性都明显上了一个台阶。这篇文章我就把整个设计思路、实操步骤和踩过的坑完整记录下来,适合正在做Agent应用、想解决智能体行为不稳定问题的开发者做参考。

1. 为什么智能体需要“技能”体系,而不是一份长长的提示词

1.1 纯提示词方案的局限:每一次都在“重新发明轮子”

先看最常见的做法:写一份几千字的系统提示词,把各种规则、流程、工具用法都堆进去,然后让模型自己“看着办”。这种做法我当时也用得很爽,因为入手特别快——把约束写得尽量细,模型基本就能照做。

但问题随着任务复杂度上升会越来越明显。第一是不稳定,模型本质上是概率模型,每次请求都像重新开一局游戏,同样的输入、同样的提示词,它可能给你走出完全不同的路线。第二是不可控,一旦中间步骤出错,你很难判断是规则没写清,还是模型理解偏了,因为“整段提示词+整段上下文”是搅在一起的黑盒。

第三是成本,我自己算过一笔账,一个串了5个工具调用的任务,如果全靠提示词让模型现场琢磨怎么用、参数怎么填,每次都要携带大段工具说明和范例,Token消耗比用技能体系高出一截,慢倒是次要的,关键是费钱。

类比你就能想明白:让一个新员工每次处理客户投诉,都从头想一遍“先查订单、再翻售后规则、再决定退款还是换货”,效率一定很低,而且每个人想的路径不一样。可如果公司给他一本操作手册,每个动作都明确“什么情况用、怎么用、参数是什么”,他照着执行,结果就稳定得多。技能体系对智能体来说,就是那本操作手册。

1.2 技能体系到底改变了什么

所谓技能,不是一个概念,而是四个可落地的东西凑在一起:一个能表达“什么时候用”的命名和描述,一个定义“要什么参数”的输入Schema,一段真正执行的函数或脚本,以及成功后返回给模型的结果。模型在这个过程中只负责“调度”,它判断当前任务该调用哪个技能,然后把参数填好,真正的执行由确定性的代码完成。

这样做带来的第一个好处是行为可控。技能是预先定义好的动作,模型的选择空间被收窄了,它不能自由发挥,只能从我给的清单里挑。第二个好处是可测试。每个技能可以单独跑,可以写单元测试,出错了能定位到具体环节。第三个好处是可复用,同一个“查订单”技能,客服Agent能用,售后Agent也能用,不用每处都重新写一遍提示词。第四个好处是可观测,日志能清楚记录某一步调用了哪个技能、传了什么参数、返回了什么结果,排查问题不再靠猜。

我当时改完第一批技能后最直观的感受是:以前让人头疼的“模型自作主张”变少了,它更像一个知道分寸的执行者,而不是一个漫无边际的写作机器。

2. 技能库的设计:任务拆解与技能粒度

2.1 把业务目标拆成能被调度的原子动作

搭建技能库的第一步,永远不是写代码,而是拆任务。我习惯把业务目标从粗到细拆成三层:宏观目标、任务序列、原子技能。

拿一个客服智能体举例。宏观目标是“处理用户退款请求”,任务序列大概是:先识别用户身份,再查订单状态,然后判断是否符合退款条件,最后执行退款或转人工。到了原子技能这一层,就会出现“查询订单信息”“查询退款策略”“提交退款申请”“转人工交接”这四个可以被调度的动作。

一个技能只做一件事,这件事要小到可以稳定执行、清晰验证。要是把一个“复杂报表分析”技能直接塞进去,模型调度起来没问题,但这个技能内部逻辑太黑盒,一旦出错很难排查。我建议的粒度标准是:能独立测试,输入输出明确,边界清晰。哪怕“生成Markdown表格”这种听起来很小的能力,也值得做成一个独立技能,因为它会被很多上游任务复用。

2.2 技能命名与描述:决定Agent能不能“看见”你的技能

这里有个大多数人会忽略的关键点:模型选技能,主要不是靠函数名,而是靠描述文本。你把技能定义好,模型在推理时会读一遍技能列表,根据描述判断“这个技能适不适合当前场景”。所以描述写得好不好,直接影响技能被选中的概率和正确率。

我总结的优质技能描述公式是:这个技能做什么 + 适合在什么场景用 + 不适合在什么场景用。第三点特别重要,很多人漏掉。举个实际例子,我原来有个技能描述是这样写的:

  • 原生描述:根据订单号查询订单详情。

这个描述够明确了吧,结果测试的时候,用户问“帮我查一下退款到哪一步了”,模型居然也去调用了这个技能,因为描述里没说明它只查订单基础信息、不包含退款进度。后来我改成:

  • 优化后:根据订单号查询订单基础详情,包括商品、金额、下单时间。适合用户需要确认订单内容时使用。如果用户询问退款进度、物流状态,请使用对应的查询技能,不要调用本技能。

改完之后,错调用率明显下降。所以描述不是越短越好,也不是越长越好,而是要精准覆盖边界条件。

2.3 一个可落地的技能清单样式

当技能数量超过五六个之后,靠脑子记就不现实了,我习惯把它们整理成一个独立目录,每个技能一个文件,内容用结构化格式描述。

skills/ ├── catalog.yaml ├── order/ │ ├── query_order.yaml │ └── query_refund_progress.yaml ├── logistics/ │ └── query_express_info.yaml └── notify/ └── create_ticket.yaml

单个技能文件里,我会保留这几项:技能名、一句话描述、使用边界、参数定义、执行入口、返回说明。

name: query_order namespace: order description: 根据订单号查询订单基础详情,包括商品、金额、下单时间,适合用户需要确认订单内容时使用。 when_not_to_use: 用户询问退款进度、物流状态时,不要调用本技能。 parameters: order_id: type: string required: true description: 订单编号,一般在用户消息中提取,格式为ORD开头。 returns: - order_status - items - total_amount entry: functions/order_api.query_order

这种做法最大的好处是,技能库可以脱离Agent代码独立演进。新加一个技能就加一个文件,改一个描述就改一个字段,谁负责维护、做了什么改动,都一目了然。

3. 从零搭建agent-skills:实操过程与核心步骤

3.1 盘点现有能力,整理技能候选

正式动手前,先把手上已有的API、函数、脚本全部盘一遍。我当时做的是一个内部工单助手,所以我列出的技能候选包括:查工单信息、查用户注册信息、查常见问题库、创建工单、给工单追加备注、发邮件通知——这就是第一批技能池。

从盘点清单变成正式技能,有个筛选标准:这个动作是不是高频复用的,输入输出是否稳定,能不能被确定性代码执行。有些动作明明很小,比如“从工单列表里筛选未处理项”,但它会在很多场景里反复出现,就值得封装。而有些动作,比如“根据上下文判断用户情绪”,这种带有主观判断的事,现阶段不适合做成技能,更适合让模型本身处理。

3.2 编写技能Schema与参数约束

接下来是给每个技能写参数定义。理论上这步很像写API文档,但区别在于,这个Schema的读者是模型,不是人。模型不知道该不该用某个参数时会查描述,填错类型时会看类型定义。

我给一个实际用过的参数Schema做示例。

{ "name": "query_refund_progress", "description": "查询售后单的退款当前进度,适合用户询问退款到哪一步时调用", "parameters": { "type": "object", "properties": { "after_sale_id": { "type": "string", "description": "售后单号,格式为AS开头,可多选,以逗号分隔", "minLength": 3, "maxLength": 30 }, "include_timeline": { "type": "boolean", "description": "是否返回时间线明细,默认false,只在用户明确询问进度详情时设置true" } }, "required": ["after_sale_id"] } }

这里我把所有可能用到的参数都放到properties里,但required只保留一个。为什么?因为模型会根据描述判断要不要传include_timeline,而不是每个字段都非得给值。参数直接决定了后面代码能不能跑通。我第一次实践时吃过亏,没写minLength和maxLength,模型自行脑补了一个超长ID传进来,接口直接报错,返给模型一个异常结果,它又一本正经地编了一个答案告诉用户处理失败了,看着好笑,其实很致命。

3.3 注册技能并接入Agent循环

技能定义好之后,就要让模型能够在实际推理中调用到。现在很多框架都有封装好的工具注册机制,我当时为了看得透彻,直接用OpenAI风格的Function Calling写了一遍最小可执行循环。核心结构是这样:先把技能声明成列表传给模型,模型返回一个tool_call,代码执行技能后把结果作为新消息回填给模型,模型再继续推理。如果模型不需要调用技能,就直接输出最终答案。

import json import openai def query_order_details(order_id: str): # 实际对接订单服务的逻辑,这里用模拟数据 return {"order_id": order_id, "status": "delivered", "items": ["蓝牙耳机"], "total_amount": 299.0} skills = [ { "type": "function", "function": { "name": "query_order_details", "description": "根据订单号查询订单基础详情,适合确认订单内容时使用", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,ORD开头" } }, "required": ["order_id"] } } } ] def call_model(messages): return openai.ChatCompletion.create( model="gpt-4o", messages=messages, tools=skills ) def run_agent(user_message): messages = [{"role": "user", "content": user_message}] while True: resp = call_model(messages) msg = resp.choices[0].message if msg.get("tool_calls"): messages.append(msg) for tc in msg["tool_calls"]: args = json.loads(tc["function"]["arguments"]) result = query_order_details(args["order_id"]) messages.append({ "role": "tool", "tool_call_id": tc["id"], "content": json.dumps(result) }) else: return msg["content"] print(run_agent("帮我查一下订单ORD20250118买了啥"))

这个循环就是所有技能调用的地基。模型不是真正执行了函数,它只是告诉系统“我选了这个技能、我给出这些参数”,真正干活的是query_order_details这个Python函数。所以技能的安全性、稳定性、权限控制都在这一层做,而不是指望模型不乱来。建议刚开始做Agent的同学,不要一上来就套重型框架,先把这个最小循环跑明白,后面再用框架心里就透亮得多。

3.4 小规模验证技能触发效果

技能接入后,最怕的是自以为写得很完善,实际模型根本不买账。我养成了一个习惯,每次改完技能库,先准备一组测试对话,大概5到10条,覆盖正向用例、边界用例和反面用例。比如测“查询订单”技能,我会准备这些输入:

  • 正向:帮我看看这个订单ORD20250118发货了吗(期望触发查询技能)。
  • 边界:查一下最近一单(期望触发查最近订单,而不是直接查订单,因为没给具体单号)。
  • 反面:我的退款啥时候到账(期望绝对不能触发查订单,应该触发退款进度查询)。

跑完测试后逐个检查两个指标:技能是否被调用,参数是否填对。如果模型选错技能,那大概率是描述有歧义;如果参数填错,大概率是Schema写得不够细。别急着骂模型,先回头看自己的定义。这个验证过程,每次改动都要做,因为模型推理是松散耦合的,改了一个描述,可能连带影响其他技能的选中率。

4. 技能编排进阶:让多个技能串联成完整流程

4.1 线性串联与依赖关系处理

单个技能只能处理单点动作,真实任务通常是一串技能按顺序协作。比如用户问“我这周五有空,帮我约个会议室”,至少需要两个技能:查我日历确认空闲时间,然后创建会议邀请。

这里有个关键设计问题:技能之间的依赖怎么表达。我推荐的做法是,在技能描述里显式写明前置条件。比如创建会议邀请这个技能,描述可以写成“根据参会人、开始时间和结束时间创建日历会议,通常应先调用查询日历技能确认没有冲突再调用本技能”。模型看到这样的描述,就会自然形成“先查后建”的调度顺序,不需要额外在代码里写死。

但要注意,不要把流程编排全押在模型自觉上。对于那种步骤完全固定、顺序不能乱的任务,宁可写一段确定性流程代码,把技能依次调用,模型只负责在每步填充参数。两者没有高下之分,区别只在“需要灵活性”还是“需要确定性”。我现在的取舍标准是:任务越线性、越容不得错,就越倾向用代码固定流程;任务越开放、越需要变通,就越交给模型自行编排。

4.2 中间结果保存与上下文管理

多技能串联带来的另一个问题,是中间结果怎么传递。最省事的做法是把上一步技能返回的结果拼回对话上下文,让模型看到,然后它继续调度下一步。这个做法在步骤少的时候没啥问题,但一旦技能返回的是很长的大表单,几千字符直接塞进上下文,几次调用之后上下文就臃肿了,模型越往后越抓不住重点。

我后来学到的做法是:大结果不要直接返回全文,而是先把内容写入临时文件或者向量库,技能只返回一个引用ID和摘要。上一个技能说“结果已保存在store_09ea62,共120条记录”,下一个技能需要具体字段时,再用专门的读取技能去取。这就好比你在公司做交接,不会把整份文档贴到消息里,而是给一个文档链接,谁需要谁再点进去看。这样上下文能被控制在稳定范围,模型读到的都是精炼信息,决策质量也更高。

4.3 用日志判断编排质量

技能编排是否合理,不能靠感觉,要看每一步的调用日志。我把每一步的推理结果都打出来,格式大概是:用户意图、模型选择了哪个技能、传了什么参数、技能返回了什么、模型基于返回结果又做了什么决策。不要嫌这一步麻烦,它在排查问题的时候是救命稻草。

之前有个案例让我印象很深:模型在同一个问题上反复调用同一个查询技能,一共查了5遍,每次参数都一样。从外部看像是个循环Bug,但看日志发现,是那个技能返回结果里缺少一个字段,模型觉得信息不够,就会再去查一遍,形成死循环。后来我在技能返回结构里补了这个关键字段,问题立刻消失。你只有把调用链路日志化,才能捕捉到这些藏在细节里的坑。

5. 常见问题与排查技巧实录

5.1 模型总是选错技能

这是遇到概率最高的问题。表面现象是:用户问“退款到哪了”,模型调了“查询订单详情”,返回了订单状态,然后一本正经告诉用户退款正在处理。根子基本都出在描述边界没有写明。

解决套路其实很固定:在when_not_to_use这种负面描述上下功夫。你可以在描述里直接列出不要调用的场景,模型对这种显式指令的遵循率,比泛泛的正面描述高不少。我还见过一个更极端的办法,给相似技能分别加上“关键词提示”,比如退款进度技能描述里写“常见问法包括:商家退了吗、退款到哪一步了”,实测效果也不错。这里有个小技巧,你可以在每个技能描述的最后写一句“如果你不确定该用哪个技能,请先询问用户,而不要自己猜测”,能有效减少模型的错配行为。

5.2 参数“幻觉”:模型生成了不存在的参数

有时候模型会自己编参数,比如给一个只接受order_id的技能,额外带上order_status、user_name之类的字段。原因往往是Schema里字段名和业务的习惯叫法对不上,或者模型看不懂字段的描述。

我的排查清单是这样:先确认参数名是不是业务术语,再检查每个参数有没有说清格式和示例,再看required是不是只留了真正必须的字段。另外有两个非常实用的约束:给字符串参数加enum,给数字参数加minimum和maximum。用enum限死取值范围,可以极大降低模型编造非法值的概率。比如“退款原因”枚举成“不想要了”“商品质量问题”“物流太慢”三项,模型就只能在里面挑。

5.3 技能冲突与命名空间隔离

技能库大了以后,会出现一个真实存在却容易被忽略的问题:两个技能描述上高度相似,模型分不清。比如“查工单详情”和“查工单操作记录”,听起来都有“查工单”,但用途其实不同。如果描述里的区分信息不足,模型就会随机蒙一个。

我的经验是,给技能做命名空间隔离,不只靠名称,更要在描述里刻意强调差异化场景。同时,同类技能超过三四个,就该引入“先分流、后细查”的机制:先用一个分类技能判断用户属于哪一类需求,再由模型决定调用子技能。这个做法有点像给书建目录,一级目录内容少了,二级目录才不会被看漏。

5.4 上下文被拖垮:技能定义过多的副作用

最后一个问题,很多人会忽略:技能定义多了,上下文窗口就被技能列表本身占满了。当技能数量超过10个,且每个描述都在200字以上,光技能列表就有好几千Token,这会压缩模型真正用于思考的空间,技能之间的相互干扰也会变严重。

我的建议是分层管理:第一层只给模型几个粗粒度技能分类,比如“订单类”“物流类”“售后类”;模型先选类别,第二轮再把该类别下的具体技能定义发给它。这样每一轮模型需要阅读的技能数量都很少,决策更准。如果你不想做得这么复杂,至少要把描述压缩到一个合理长度,我个人的上限是180字符,只留最核心的边界信息。

6. 技能库的版本管理与团队协作

6.1 让每一次技能变更可追溯

技能库本身是代码资产,应该像代码一样管理。我团队的技能定义全部放在Git仓库里,每个技能文件、每个描述变更都有提交记录。为什么强调这个?因为技能描述是“模型行为的关键变量”,有时候你改了一句描述,可能连带影响其他技能的选中概率。如果变更没有记录,出了问题就没有回滚依据。

我们的流程是:技能改动必须走合并请求,至少一个人review描述和Schema,再跑一遍第3.4节里的测试用例集,全部通过才允许合入。版本号上,所有Agent统一引一个固定版本的技能库,不做成依赖最新main分支的裸引用。这样才能保证线上每个版本的行为可追溯,不会出现“昨天还好好的,今天加了个技能就坏了”这种玄学问题。

6.2 技能维护的Owner机制

技能库没人负责是维护不起来的。我建议按业务域划分Owner,比如订单域一个Owner,物流域一个Owner,每块业务对应技能的新增和描述优化由对应的人负责。线上收集到的失败case,统一整理到一个issue列表里,每周挑出出现频次最高的几个,优先优化对应技能描述或Schema。

听起来好像有点“重”,但Agent项目到了后期,拼的其实就是这个技能库的维护质量。模型选型当天就能定下来,一套持续迭代的技能体系却需要两周、一个月长期打磨。把技能库当成一个产品来运营,是Agent走向稳定工程化的必经阶段。

6.3 把失败case变成新技能

技能库不是建完一次就固定了。实际上我经常在复盘线上对话时发现,某类问题模型总能回答,但答得特别费劲,每次都要绕很多弯路。这种情况往往意味着,那里应该出现一个技能,但还没有被封出来。

举个小例子,一开始我们的Agent遇到用户投诉时,会先查各种信息,然后自己拼回复末尾的致歉语,每次发挥都不一样。后来我单独做一个“生成标准致歉模板”技能,把话术版本固化在里面,模型只需在需要时调用,结果不仅回复风格统一了,上下文里还少了一大段临时思考的输出。这就是把失败case变成新技能的过程:每次看到模型“不必要地努力”,就想想这里是不是缺一个技能。技能库是活的,它会随着业务理解加深持续生长,而不是一次性交个差就完事。

做技能库这件事,我个人的体会很简单:它的难度不在于写代码,而在于对业务的理解和表达。能把这个技能清单写得让模型一看就懂,让队友一读就明白,是一件很见功力的活。如果你现在正在做Agent,我建议别急着追新模型,先花一天时间把手上的能力梳理成技能清单,把描述反复打磨几遍,整个系统的实际表现会给你惊喜。这个工作是慢功夫,但回报会在后面每一天的稳定运营里慢慢体现出来。

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

1DCNN滚动轴承故障诊断:端到端时序建模实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 20:58:25

ESP32-P4掌上无线电瑞士军刀:SDR/LoRa/收音机三合一实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 20:58:00

Linux内核PM QoS框架:功耗调控的动态仲裁中枢

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 20:57:58

SAP生产订单全流程:创建、下达、发料、报工、入库与冲销

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 20:56:29

Java学生成绩管理系统:基于Servlet+JSP+MySQL的完整项目实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 20:56:28

OpenLANE开源ASIC设计流程实战:从RTL到GDSII的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华