☰
Agent技能封装实战:从混乱工具调用到可复用技能库
2026/10/8 16:55:41 网站建设 项目流程

1. “agent-skills”到底在解决什么问题

我在做 Agent 项目的第三个月,决定把所有的能力模块全部重写成一套统一的 agent-skills 体系。起因很直接:同一个智能体,换了个业务场景之后,原来的提示词和工具调用逻辑全乱套了。当时最大的感受是,模型本身并不弱,真正拖后腿的是我给它的“手和脚”——那些能力边界模糊、互相耦合、难以单独验证的函数与提示词片段。

这里说的 agent-skills,不是某个框架的专有名词,而是我习惯用的一套工程化组织方式:把 Agent 能做的事拆成一个个可声明、可测试、可复用的技能模块,每个技能都有清晰的描述、输入输出协议、实现逻辑和验证方式。如果你正在开发 AI 助手、自动化流程、智能客服这类产品,或者你发现自己的 Agent 到了“demo 能跑、上线就崩”的阶段,那么这篇文章里的思路和踩坑记录应该能帮上忙。

1.1 一次事故让我决定重写技能层

事情发生在给一个客户做智能客服机器人时。当时为了快速上线,我把所有工具调用逻辑都堆在一个大文件里,意图判断靠大段 if-else,业务规则散落在各处。最初功能很少,跑起来还算顺畅。后来客户要求增加节假日话术,我在某个工具函数里改了一行返回值格式,结果导致另一个不相关的意图分支开始错误触发,线上对话连续出现答非所问。

排查了很久才发现,问题根源不是模型,而是我把“技能”和“业务规则”焊死在了同一段代码里。节假日话术本质上是一个独立的领域能力,它应该有自己独立的入口、参数和返回结构,可以被单独替换和测试。但在当时的架构里,它和周边十几个功能共用同一个状态机,牵一发而动全身。

这次事故之后,我把思路调整为“技能优先”:凡是 Agent 需要对外执行的动作,先抽象成技能,再通过技能模块去组合业务规则。这样每个能力像抽屉一样独立存在,模型按需抽取,工程侧也能针对单一技能做回归测试。听起来像常识,但真正动手做之前,我确实低估了这件事的价值。

1.2 技能模块解决的三个真实痛点

第一个痛点是模型的可理解性。如果你把一堆业务逻辑直接写进系统提示词,模型需要从长文本里自己找调用条件,稍微复杂一点就容易漏。技能模块则把每个能力压缩成一段“能力说明书”,模型只需要在候选列表里做匹配,理解和选择的成本都低很多。

第二个痛点是工程的可测试性。传统工具函数可以单测,但 Agent 的工具调用链路很难单测,因为你不知道模型会在什么上下文里触发它。技能模块通过标准化输入输出和独立执行逻辑,让测试变成一个纯粹的函数验证过程。先测技能本身能不能跑,再测模型能不能选对技能,问题边界变得非常清楚。

第三个痛点是能力的可复用性。同一个“查询订单”技能,可以用在客服机器人、工单助手、企业微信机器人里,只要描述和协议不变,换场景就是换 Agent 壳。之前我所有的能力都长在某个项目里,换个项目就要复制粘贴改一堆东西,现在沉淀成 agent-skills 库之后,新项目的冷启动速度明显快了很多。

这三件事听起来不大,却直接影响 Agent 从“能用”到“好用”的关键一步。

1.3 什么内容才配叫 skill

不是所有函数都值得做成技能。我现在的判断标准有三个:一是输入输出边界是否清晰,二是是否有可预期的副作用,三是能否独立验证。比如“查询订单状态”“计算运费”“发送提醒消息”这类操作,边界明确、结果可检验,天然适合做成技能。

相反,有些任务过于开放,比如“写一篇爆款文章”,就不适合直接塞成一个技能。这类任务需要进一步拆解为“生成标题候选”“搭建文章大纲”“生成正文段落”等更小的子技能,否则描述写不清楚,模型也不知道从哪下手。过早把大而泛的能力固化成技能,只会让注册表变得臃肿,还会增加模型选错技能的几率。

判断一个能力能不能沉淀为技能,我的经验是先在对话里手工测试三次,如果你每次都需要补充新规则才能让它稳,那就说明这个技能还没有收敛,继续拆。

2. 重新拆解 agent-skills:一个技能单元长什么样

我落地的 agent-skills 体系里,一个完整的技能单元由四部分组成:技能描述、输入输出协议、实现逻辑、自检测试。很多人只重视实现逻辑,把技能当成普通函数来写,结果模型根本不调用,或者调用了却传错参数。其实在大模型应用里,最关键的往往是那几行“给模型看的描述”。

2.1 技能描述:给模型看的产品说明书

技能描述的目标是让模型在候选列表里一眼认出“这个能力该不该由我来触发”。我写描述的格式基本固定:第一句说明能力边界,第二句说明执行前提,第三句给出常见参数示例。最后还会加一句“什么时候不要用”,用来减少误触发。

举个例子,一个查询天气的技能,我会写成这样:

name: get_weather description: | 查询指定城市当前天气和未来三天预报。 当用户明确提到天气、气温、降水、风力等意图时使用。 参数 city 需要是中文城市名,尽量从用户原句中提取。 如果用户只是在闲聊天气感受,不要调用本技能。

这样的描述看起来很短,但信息密度很高。模型在做技能选择时,本质上是在做语义匹配,它不需要看到你的 Python 类型注解,它需要的是“触发条件”和“参数来源”。我见过很多团队把函数文档直接复制成技能描述,里面全是技术术语,模型当然容易选错。

2.2 输入输出协议:一切可以序列化

技能和普通函数的另一个区别是,它面向的调用方不是程序员,而是模型。模型生成的是结构化文本,所以技能输入输出必须是可序列化的,最好用 JSON Schema 明确约束。

我在定义协议时,会为每个技能建立一个输入输出结构,例如:

from typing import TypedDict, Optional class GetWeatherInput(TypedDict): city: str date: Optional[str] # 缺省则为今天 class GetWeatherOutput(TypedDict): status: str # "ok" 或 "error" data: Optional[dict] # 天气详情 message: str # 给模型看的简短说明

输出里一定要带上message。因为模型需要根据返回值决定下一步动作,如果技能只返回一个裸 dict,模型很容易不知道发生了什么。加上一句“查询成功,北京今天晴,最高温度 30 度”这样自然的描述,模型就能直接理解并转述给用户。

协议设计要尽量扁平,避免深层嵌套。模型擅长生成平面结构,复杂的嵌套对象容易出现字段缺失或类型错误。宁可多几个顶层字段,也不要搞三层以上的对象。

2.3 注册与发现:把代码变为数据

最初的版本里,我是用一个巨大的 if-else 去分发工具调用,后来换成注册表机制。注册表的核心思路是:把技能名、技能描述、输入结构和实现函数登记到一个全局字典里,模型只需要看这个字典的“目录页”,就能了解整个 Agent 的能力范围。

我通常用 Python 装饰器来做这件事,代码会清清爽爽:

SKILL_REGISTRY: dict[str, Skill] = {} def skill(func): name = func.__name__ SKILL_REGISTRY[name] = Skill( name=name, description=func.__doc__.strip(), fn=func, ) return func @skill def get_weather(city: str, date: str = ""): """查询指定城市当前天气和未来三天预报。""" ...

当模型需要调用时,Agent 会把SKILL_REGISTRY里所有技能名和描述拼成一个“技能菜单”,让模型从中选择。这个过程中,代码变成了数据,新增技能不再需要改分发逻辑,只需要写一个函数并加上装饰器。

这里有个容易被忽略的点:注册顺序会影响模型的选择概率。如果两个技能描述相似,通常排在前面的更容易被选中。所以我会把高频技能放在前面,低频技能放在后面,而不是按字母序排列。

3. 实操:从零搭建一套可落地的 skill 库

理论说了一堆,接下来展示一套我实际在用的技能库结构和完整示例。沿着这个模板,你可以很快把现有代码整理成属于自己的 agent-skills 库。

3.1 目录结构与命名规范

项目根目录下,我会专门建一个skills文件夹,每个技能独占一个子目录,子目录里放描述文件、实现文件和测试文件。

skills/ ├── get_weather/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py ├── get_exchange_rate/ │ ├── skill.yaml │ ├── impl.py │ └── test_impl.py └── send_email/ ├── skill.yaml ├── impl.py └── test_impl.py

技能命名的规范我用的是“动词 + 目标对象”,比如get_weather、send_email、calculate_delivery_fee。尽量避免使用process_data、handle_request这类模糊名字,因为模型在技能匹配时对名称很敏感,动词越具体,召唤成功率越高。

skill.yaml存放模型的可见元信息,包括技能名、描述、参数示例和授权级别等。impl.py是纯实现逻辑,不掺杂任何 Agent 上下文。test_impl.py是单元测试,直接调用函数验证结果。

3.2 一个最小案例:汇率查询技能

我们做一个最简单的汇率查询技能。先写skill.yaml:

name: get_exchange_rate description: | 查询实时汇率,支持常见货币之间换算。 当用户提到汇率、换汇、外汇、某货币兑某货币时使用。 参数 base 表示基础货币代码,quote 表示目标货币代码。 如果用户未指定目标货币,默认使用 CNY。 返回换算比例和参考金额。 version: 1.0.0

然后写impl.py:

from typing import TypedDict, Optional class ExchangeRateInput(TypedDict): base: str # 基础货币,如 USD quote: str # 目标货币,如 CNY amount: Optional[float] # 金额,缺省则返回汇率 class ExchangeRateOutput(TypedDict): status: str rate: Optional[float] converted: Optional[float] message: str def get_exchange_rate(input_data: ExchangeRateInput) -> ExchangeRateOutput: base = input_data["base"].upper() quote = input_data.get("quote", "CNY").upper() try: rate = fetch_rate_from_db(base, quote) except Exception as e: return { "status": "error", "rate": None, "converted": None, "message": f"查询失败:{e},请检查货币代码是否输入正确", } amount = input_data.get("amount") converted = amount * rate if amount is not None else None if amount is not None: message = f"{amount} {base} 约等于 {converted:.2f} {quote},当前汇率为 {rate}" else: message = f"当前 {base}/{quote} 汇率为 {rate}" return { "status": "ok", "rate": rate, "converted": converted, "message": message, }

接入 Agent 时,只需要把get_exchange_rate注册进SKILL_REGISTRY,然后把技能菜单交给模型。整个过程不需要改业务代码。我第一次重构时最惊讶的就是:原来加一个新能力可以这么快。

3.3 技能自检与 dry_run

实际运行中,模型经常传错参数,比如把“人民币”直接当货币代码传进来,或者把日期传成“明天”。为了减少这类问题,我在每个技能里加了一个轻量自检逻辑,当参数缺省或明显异常时,返回一个“提示型错误”,告诉模型应该怎么补参数。

我还会给关键技能增加dry_run模式。这个模式只做校验和演练,不真正产生副作用。比如发送邮件技能,在dry_run下只会打印“将向某某发送主题为某某的邮件”,不会真的发出去。这样能让模型在正式生成动作前先自检一遍,大幅减少误操作。

dry_run的实现也简单,就是给输入增加一个test_mode字段,处理函数在开头判断一下。别小看这个字段,它是我做技能灰度时最依赖的安全阀。

3.4 版本管理与灰度

技能不是写一次就不动了。业务规则一改,技能实现就要跟着改。但模型的行为需要保持一致,所以我给每个技能加了version字段,注册表里记录当前请求使用的版本号。升级时先记录旧版本行为,方便回滚。

灰度策略我做得比较朴素:注册表里同时保留新旧两个版本,通过一个开关分配流量。比如get_exchange_rate从 v1 升到 v2,先让 10% 的请求走到 v2,观察调用成功率和用户反馈,再逐步放量。这个方法不花哨,但确实能帮我避免“一次性全量上线,然后被模型的新错误行为淹没”的情况。

版本管理最需要注意的坑是:不要只改描述不改协议。如果 v2 改了参数结构,一定要在skill.yaml里同步更新,否则模型按旧描述生成新参数,技能直接报错。

4. 真正让 skills 好用的几个关键细节

如果说前面是骨架,这一节就是血肉。我自己在把 agent-skills 打磨到能上生产环境的过程中,积累了几个非常实际的经验。

4.1 描述里要明确“什么时候不要用”

给技能写描述时,大家很容易只写“什么时候用”,却忘了写“什么时候不要用”。在真实对话里,模型经常过度调用技能。比如用户只是抱怨“今天天气太糟了”,并不想查天气,但如果你的天气技能描述里全是“天气”“气温”等词,模型可能就触发查询。

我现在会在描述末尾固定加一句如果用户只是在表达主观感受,不要调用本技能。这句“负向提示”对降低误触发非常有效。同样地,如果一个技能只能由管理员使用,就在描述里写清楚“普通用户询问权限相关问题时,不要调用,转交由权限判断逻辑处理”。

负向描述不用太长,一两句话点中常见混淆场景就够了。写得太多反而会让模型困惑。

4.2 错误信息是给模型看的纠错信号

技能里抛异常很容易,但模型拿到的只是一个异常字符串时,往往不知道下一步该做什么。我把错误信息改成了结构化格式,包含状态、原因和建议:

{ "status": "error", "reason": "invalid_currency_code", "suggestion": "请将货币参数改为国际标准代码,例如 USD、CNY,再重试" }

这样模型看到suggestion后,会自然地对用户说“请提供标准货币代码”,甚至主动修正参数后重试。我实测过,结构化错误让技能调用失败后的恢复成功率提高了不少。

记住:技能返回的错误也是模型的一次“输入”,你的错误信息写得越像给同事看的消息,模型就越容易接着干活。

4.3 控制返回体量与敏感信息

Agent 的上下文窗口是有限的。如果技能返回一大段完整订单明细、几十条搜索结果,模型还没开始推理,上下文就已经被撑爆了。我的原则是:技能返回给模型的内容只保留“决策所需的最小信息量”,详细信息写入外部存储,需要时再按消息 ID 拉取。

举个例子,查询订单列表时,不要在返回值里塞完整的商品详情和物流轨迹,只返回订单号、状态、金额、时间这几个字段就够了。如果用户追问详情,再通过另一个“查询订单详情”技能去取。这种拆分不仅省 token,还让每个技能的链路更短、更容易排查。

敏感信息方面,技能输出里绝不能带明文密码、完整身份证号、银行卡号等。我在输出层做了一层脱敏,比如只返回尾号四位。防的不只是模型,还有日志系统和下游服务。任何时候审计日志里都不该出现用户敏感字段。

4.4 幂等性与并发安全

多个技能被并行调用时,很容易出现重复副作用。最典型的是“支付”或“发消息”这类操作:模型判断失误重试两次,用户就收到两条消息。所以我在技能设计里强制要求:凡是有副作用的技能,必须支持幂等。

幂等的做法很简单,给每次调用生成一个request_id,服务端记录这个 id 是否已经处理过。如果重复提交同一个request_id,直接返回上一次的结果,不再次执行副作用。同时,技能内部尽量保持无状态,不要依赖全局变量,避免并发时数据互相污染。

这个设计在单机 demo 里看不出来,一旦技能被多个 Agent 实例共享,或者被用户手动触发和模型触发同时调用,幂等就是保命符。

5. 常见问题与踩坑实录

再正确的理论,落到实战里都会有一堆意想不到的问题。这里记录几个我反复遇到的坑,以及对应的排查方法。

5.1 模型不调用技能时,先别急着调 prompt

模型完全无视技能菜单,是最常见的问题。很多人第一反应是加长系统提示词,结果越加越乱。我现在的排查顺序是:先看技能描述是否出现在模型上下文中,再看描述里的关键词是否和用户表达有明显匹配,最后才考虑调整 prompt。

如果用户说“帮我查下美元兑人民币”,技能描述里却没有“美元”“人民币”这些具体词,模型就很难触发。解决方法是把常见说法作为示例写进描述里,比如支持 USD/CNY、EUR/CNY 等常见货币对。这不是让模型死记硬背,而是给它更容易匹配的锚点。

另一个容易被忽略的原因是技能菜单太长。当候选技能超过十几个时,模型可能遗漏靠后的技能。我会把高频技能排在前面,并且为同一类能力做一个“分组描述”,减少候选数量。

5.2 技能明明存在,却选错了技能

选错技能比不调用更隐蔽。我遇到过两个技能描述高度相似,一个是“查询订单”,另一个是“查询售后单”,模型总是把售后单查询请求派给订单查询。后来我在两个描述里分别加入了“如果不确定是哪个,先问用户是否有售后纠纷?”,效果立竿见影。

还有一个技巧是给每个技能写一个“反例”字段,比如not_to_use: 当用户提到退货、换货、维修时,请选择 query_after_sale。这种显式的互斥指引,比单纯加形容词有用得多。

要彻底排查,我会维护一个技能评测集,每个评测样本包含“用户话术”和“期望技能名”。每次改动描述后跑一遍,看准确率和召回率变化。没有评测集,你根本不知道哪次描述改动是变好还是变坏。

5.3 技能返回内容撑爆上下文

早期我做一个搜索类技能,直接把前几十条搜索结果全部返回,模型还没来得及总结,上下文就已经爆了。后来我改用“分页摘要”策略:技能只返回前 5 条结果的核心标题和摘要,如果用户要更多,再通过参数page翻页。这样既控制了 token,又让模型每一步只聚焦一小批信息。

还要注意返回值里的“冗余信息”。有些技能实现者图省事,把整个数据库行原样返回,里面全是创建时间、更新时间、内部 ID。这些字段对模型决策没有帮助,只会稀释注意力。我在输出层做白名单字段,明确哪些可以出站。

5.4 问题排查速查表

症状可能原因处理方式
模型从不调用某技能描述关键词不匹配、技能排序太后补充用户常见说法,调整注册顺序
调用技能但参数频繁错误输入协议定义过宽,缺少示例在描述中增加参数格式示例
多个技能经常混淆描述相似度高,缺少互斥说明加反例字段,明确触发边界
技能返回内容太长未做摘要和字段白名单只返回决策所需最小字段
升级后行为变化大描述或协议未同步更新版本号分离,灰度放量
重复执行副作用操作技能非幂等增加 request_id 去重

这张表是我每次上线前都会过一遍的基础检查清单,能帮我快速定位八层以上的问题。

6. 落地 agent-skills 一年后的个人体会

6.1 技能库需要“新陈代谢”

技能库不能只增不减。我在维护了大半年后,发现很多早期技能已经没人调用,但还在注册表里占着位置,每次模型选择时都会浪费注意力。后来我加了一个“调用日志”统计,每个月清理一次近 30 天调用次数为零的技能。不是直接删,而是先标记为deprecated下线,再删代码。

这个过程让我意识到,agent-skills 不是一个静态的目录,它更像代码库本身,需要持续的 review 和重构。给技能写描述时,我心里会有个标准:如果一个新同事不看实现代码,只看skill.yaml能完全理解这个技能的能力边界,那才算合格。达不到标准的描述,一律重写。

6.2 下一步可以从评测与观测入手

如果你已经搭好了一套技能库,我建议下一步把重心放在“可观测性”上。每次技能调用都记录下选技结果、参数、耗时、返回状态,然后定期统计调准率。我后来用这些数据做过一次很有效的优化:发现某个技能虽然经常被选中,但执行成功率只有六成,原因是它的输入协议和描述之间存在两张皮,模型按描述生成参数,函数却按更严格的协议校验。改掉这个不一致后,整体成功率立刻回升。

最后再分享一个小技巧:新增技能之前,先问自己三个问题——这个能力可以被一句话说明吗?它的输入输出可以被结构化吗?它值得被独立测试和复用吗?三个问题都回答“是”,再做。少建一个模糊技能,比多写一个完美技能更重要。这套思路陪我走过几个项目,也希望帮你少踩一些坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询