☰
Agent技能库实战:让AI代理从工具调用走向自动化操作
2026/10/8 21:16:17 网站建设 项目流程

真·干活的项目:把AI代理从“嘴强王者”变成“能力者”,全靠一套Agent Skills技能库

之前我一直在搞AI代理(Agent)应用,最头疼的问题就是:模型对话能力再强,只要一落到具体业务场景里,比如让它查个订单、算个运费、调一下库存,就立刻露怯。模型只是会“说”,根本不会“做”。后来我意识到,问题不在模型本身,而在于——你压根没给代理配上一套真正能干的“手和脚”。

这个项目叫agent-skills,说白了就是一套专门为AI代理设计的技能注册与调度系统。它做的事情非常聚焦:把外部能力(API、数据库查询、计算逻辑、规则引擎)封装成一个个结构化的“技能”,让代理在需要的时候能自动、准确地调用它们,而不是靠模型瞎猜或者硬编码一堆if-else。如果你正在开发AI客服、自动化运维助手、内部知识库问答机器人,或者任何需要让大模型真正操作业务系统的项目,这套设计思路绝对是值得直接抄作业的参考样板。

我是在处理一个电商售后场景的原型时启动这个项目的。当时把一堆功能逻辑硬塞进Prompt里,结果一长对话就崩溃:模型偶尔自己“脑补”出一个订单号去查询,或者该调接口的时候不动手,直接把一段假数据当成结果返回给我。后来我按agent-skills这套思路重构了整个调用链路,一下子清爽太多了。这篇就把完整的拆解、实现路径和踩坑记录拿出来聊聊。

1. 项目思路剖析:agent-skills到底在解决什么问题

1.1 从“会聊天”到“能办事”的关键跳跃

先说清楚,为什么光有大模型还不够。一个典型的AI代理应用,核心链路一定是:用户输入 → 模型理解意图 → 生成行动方案 → 调用外部工具 → 汇总结果回复。在这个链路里,前面两步模型干得特别好,但一旦进入“调用外部工具”,问题就来了。

传统做法是把工具函数一股脑塞进代码里,然后靠提示词让模型自己去选。最开始我这么干的时候,几十个函数全挂在一个tools数组里,结果模型经常选错工具。尤其是两个函数的功能描述比较接近时,比如query_order_detail(查订单详情)和search_order_list(搜索订单列表),模型分不清什么时候该用哪个,经常返回一个结构完全不对的调用请求。

agent-skills的核心思想,是把每一个能力封装成带独立命名空间、元信息描述、参数Schema校验的“技能”。它不追求模型直接调用函数,而是让模型学会“选技能、填参数”,然后由技能注册中心去完成实际执行。等于在模型和业务代码之间加了一层标准化网关。

这一层的价值非常直观:模型只负责“做决定”,不负责“做执行”。执行交给可靠的程序代码,决定通过结构化的技能元信息来约束和引导。

1.2 “工具”太多太乱的时候,你需要“技能”层面的抽象

很多团队是从“给模型加个函数调用”起步的,但很快会发现函数数量上来以后,管理就成了灾难。一个函数三五个人改过,签名变了,描述过期了,参数含义不清晰,模型自然就懵了。

agent-skills比单纯“工具函数”多出来的,是完整生命周期管理:

  • 每个技能有明确的名称、描述、版本号、所属领域;
  • 参数声明严格遵守JSON Schema规范,可以自动化校验;
  • 技能可以被动态启用/停用,不影响其他技能;
  • 技能之间可以被编排成组合流程,而不是孤立的一对一调用。

我实际体验中,最大的直观感受是排查问题变得极快。以前模型调错函数,得翻代码日志反复对。现在技能层提供了标准的入参、出参、耗时和状态记录,整个调用链一目了然,看一眼日志就知道模型选了什么技能、填了什么参数、结果哪里不对。

1.3 为什么“选技能”比“写死逻辑”更适合LLM应用

这里要解释一个底层原因:大模型的指令遵循能力是有边界概率的。你把一个任务写死在Prompt里,模型在简单场景下表现不错;但一旦任务边界模糊、输入多样化,写死逻辑就崩了。

举个例子,用户说“帮我看看我那个包裹到哪了”,模型需要自己判断:这涉及到“查询物流信息”这个技能,但它还需要从用户消息中抽取订单号、判断查询来源是哪个平台。如果你把“查询物流”的逻辑和“抽取订单号”的逻辑混在一起,模型很难稳定执行。

agent-skills的做法是:把“抽取订单号”也定义为一个技能,把“查询物流”定义为另一个技能,然后在技能描述里明确各自职责和依赖关系。模型可以通过一次“技能链调用”逐步完成:先用extract_order_number技能从用户原文中抽取订单号,再把结果传给query_logistics技能。每一步的输入输出都有清晰约束,可靠性高得多。

这就是技能抽象的核心价值:让每个动作都足够单一、足够可靠,模型只需要做选择题和填表题,不需要做自由发挥的综合题。

2. 整体架构与技能分类设计

2.1 技能注册中心:一切能力皆可声明

我先给出整体架构中最关键的一个角色:技能注册中心。它的职责是维护一份所有可用技能的清单,并提供给模型进行工具选择。

我用Python实现,核心是一个带有装饰器的注册表:

# skill_registry.py from typing import Callable, Dict, Any, Optional, List from pydantic import BaseModel, Field, create_model import inspect class Skill: def __init__(self, name: str, description: str, parameters_schema: Dict[str, Any], handler: Callable): self.name = name self.description = description self.parameters_schema = parameters_schema self.handler = handler self.enabled = True def to_openai_format(self) -> Dict[str, Any]: """转换为 OpenAI function calling 所需的结构""" return { "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters_schema } } class SkillRegistry: _skills: Dict[str, Skill] = {} @classmethod def register(cls, name: str, description: str, parameters_schema: Dict[str, Any]): def decorator(func: Callable): skill = Skill(name=name, description=description, parameters_schema=parameters_schema, handler=func) cls._skills[name] = skill return func return decorator @classmethod def get_all_skills(cls) -> List[Dict[str, Any]]: return [skill.to_openai_format() for skill in cls._skills.values() if skill.enabled] @classmethod def execute(cls, name: str, arguments: Dict[str, Any]) -> Any: skill = cls._skills.get(name) if not skill: raise KeyError(f"技能 {name} 不存在或未注册") if not skill.enabled: raise RuntimeError(f"技能 {name} 已被停用") return skill.handler(**arguments)

这个注册中心的实现本身并不复杂,但设计上是经过取舍的。用装饰器注册的好处是技能定义与业务实现完全内聚,新增一个技能只需要写一个函数加一行装饰器,不需要改任何集中配置文件。一旦技能数量突破五十个,这种声明式管理的优势会非常明显。

to_openai_format()这个方法很关键,它保证了技能注册表可以直接无缝对接到各类模型的function calling接口,不需要再单独维护一套映射逻辑。

2.2 技能分类:按职责粒度划分

技能不是随便堆的,我通常把技能分成三个层次:

原子技能:单个动作,不依赖其他技能,直接执行一个API调用或数据库查询。例如query_order_status、send_email、calculate_shipping_fee。

组合技能:编排多个原子技能的流程逻辑。例如handle_order_refund这个组合技能内部要先调verify_identity、再调query_order_detail、再调calculate_refund_amount、最后执行execute_refund。

兜底技能:当所有技能都不匹配用户意图时,触发一个结构化的话术回复或转人工逻辑。兜底技能的存在至关重要,它能避免模型强行匹配一个不相关技能的情况。

这里的关键是按照“业务能力”来划分,而不是按照“代码模块”来划分。这两个是有本质区别的。比如query_user_balance和query_user_points,从代码角度看可能都是读同一个用户表,但它们面对的是完全不同的业务意图,必须拆成两个技能。反过来,get_order_by_id和get_order_by_tracking_number虽然API不同,但业务意图都是“查订单详情”,建议合并成一个技能,通过不同参数来区分。

2.3 技能描述:写清楚才能被正确调用

这是整个agent-skills体系里最容易被人忽略但影响最大的部分。技能描述写不好,模型再强也白搭。

我总结了一套技能描述的黄金写法,核心规则如下:

  • 描述必须以动作开头,明确“这个技能能做什么”;
  • 必须包含触发场景的关键词,让模型在意图匹配时能找到;
  • 必须说明参数之间是否有依赖关系、哪个是必填项、哪个是可选;
  • 必须说明输出格式的基础特征,避免模型误解返回结构。

我举个例子,一个原本写得很差的技能描述是:

查询订单信息。

这种描述模型根本不知道怎么触发,也不知道应该传什么参数。我重写之后是这样:

name: query_order_detail description: > 根据订单编号查询订单的详细信息,包括商品清单、支付状态、配送进度和售后状态。 当用户使用以下词汇表达时使用此技能:查订单、订单详情、我的订单、订单状态、 tracking、物流跟踪。不适用于搜索历史订单列表,那是另一个技能。 parameters: order_id: type: string description: 订单编号,格式为"SO-"开头的字符串,用户在消息中直接提供,如果没有则需先通过用户询问获取,切勿编造。 required: true

描述里面加了“不适用于”这几个字看着不起眼,但对模型来说是非常强力的负向约束,能显著减少技能误触发的概率。实测下来,把一组相似技能都这样写上“不适用场景”之后,模型工具选择的准确率能提升不少。

3. 从零构建技能库:核心环节实战

3.1 一个实战技能:从封装到上线的完整过程

我这里用一个真实的电商场景技能来走一遍完整流程:商品库存查询。这个技能在售后客服场景中特别常用,但也很容易写崩。

第一步,定义技能参数Schema。库存查询的关键参数有两个:商品SKU编号和查询维度(全局库存还是某个仓库)。另外,还需要一个可选的include_locked参数,用来控制是否包含锁定库存。参数定好了之后,模型才不会传乱七八糟的东西:

INVENTORY_CHECK_SCHEMA = { "type": "object", "properties": { "sku_id": { "type": "string", "description": "商品SKU编号,例如:SKU-8842" }, "warehouse": { "type": ["string", "null"], "description": "仓库编码,例如 WH-SH-01,如果省略则查询全渠道总库存", "default": None }, "include_locked": { "type": "boolean", "description": "是否统计锁定库存,默认False,即只返回可售库存", "default": False } }, "required": ["sku_id"] }

第二步,写具体执行逻辑。这里要特别注意:技能处理器里要做参数校验和容错,不能假设模型一定传了正确的参数进来。实际项目里我遇到过模型把订单号当SKU编号传进来,或者把仓库名写成中文,这类脏数据全靠处理器的守门逻辑拦截:

@SkillRegistry.register( name="check_inventory", description="根据SKU编号查询商品库存情况,当用户询问是否有货、库存多少、什么时候能发货时使用。", parameters_schema=INVENTORY_CHECK_SCHEMA ) def check_inventory(sku_id: str, warehouse: Optional[str] = None, include_locked: bool = False): # 参数兜底 if not sku_id: raise ValueError("sku_id不能为空") sku_id = sku_id.strip().upper() if not sku_id.startswith("SKU-"): raise ValueError(f"SKU编号格式错误: {sku_id}") # 调用真实的库存服务接口 # 这里以mock数据代替实际RPC调用 inventory_data = get_inventory_from_service(sku_id, warehouse, include_locked) result = { "sku_id": sku_id, "available": inventory_data["available"], "locked": inventory_data["locked"], "warehouse": warehouse or "ALL", "estimated_restock_days": inventory_data.get("restock_days", None) } return result

第三步尤其重要:返回值要设计成结构化JSON,而且要“够用、不多给”。模型拿到返回结果后,还会用它组织回复话术。如果返回字段太少,比如只返回available,模型就不知道如何解释“为什么缺货”;如果返回字段太多,模型反而会抓不住重点。所以返回结构我一般会控制在三到六个关键字段,并给每个字段起直观的英文命名。

3.2 给模型接上技能:从提示词工程到Function Calling

技能注册好之后,接下来是让模型能够“看到”这份技能清单。这里存在两种主流做法,我实际都用过:

做法一:纯提示词+JSON输出解析。把技能清单的JSON格式塞进系统提示词里,要求模型输出一个固定格式的JSON调用请求。这个做法的优点是兼容所有模型,不需要额外接口能力;缺点是输出稳定性差,模型可能偶尔输出额外的解释文本或者格式错乱。

做法二:使用模型的Function Calling接口。这是目前推荐的方案。GPT系列、Claude等主流模型都原生支持工具调用功能。做法是:把技能注册中心的get_all_skills()返回结果直接传给API的tools参数,然后由模型输出结构化的调用指令:

def chat_with_agent(user_message: str): messages = [{"role": "user", "content": user_message}] # 第一次调用:让模型决定是否调用技能 response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=SkillRegistry.get_all_skills(), tool_choice="auto" ) tool_calls = response.choices[0].message.tool_calls if not tool_calls: # 模型直接回复,没有调用技能 return response.choices[0].message.content # 执行技能并拼接结果 result_messages = [] for tool_call in tool_calls: func_name = tool_call.function.name func_args = json.loads(tool_call.function.arguments) try: result = SkillRegistry.execute(func_name, func_args) result_messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) except Exception as e: result_messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": f"技能执行失败: {str(e)}" }) # 第二次调用:把执行结果交回给模型组织最终回复 messages.extend(result_messages) final_response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=SkillRegistry.get_all_skills() ) return final_response.choices[0].message.content

这个模式的精髓在于“两次调用”:第一轮让模型决定要不要技能、要哪些技能;第二轮把技能真实执行结果交还给模型,让它组织成人话回复。如果技能执行出错,同样可以走第二轮流程,让模型基于错误信息生成安抚话术或转人工判断,体验会自然很多。

3.3 技能编排:如何实现多技能组合调用

单一技能场景相对简单,真正体现agent-skills价值的是多技能编排。售后场景中,用户一句“我要退了这个订单里那个不合适的尺码”,就同时涉及身份验证、订单查询、商品信息读取、售后受理等多个技能。

我有两种编排方式可以分享:

顺序编排:模型在第一轮调用时输出多个tool_calls,它们之间没有依赖关系,可以同时或者按顺序执行。比如同时查订单信息和查用户等级,互不干扰。

链式编排:后一个技能需要前一个技能的执行结果作为输入。这种情况模型第一轮只会输出一个调用,执行完拿到结果、拼接进消息历史之后,再到第二轮继续决策。我代码里的循环结构就是干这个用的:

while True: response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=SkillRegistry.get_all_skills(), tool_choice="auto" ) tool_calls = response.choices[0].message.tool_calls if not tool_calls: break for tool_call in tool_calls: # 逐个执行技能 result = SkillRegistry.execute(...) messages.append({"role": "tool", "content": json.dumps(result)})

这里需要注意一个关键设置:loop的最大次数要设上限(通常三到五次)。不设上限的话,遇到一个循环依赖或者模型抽风,请求就永远停不下来了,既烧钱又拖慢响应。

4. 踩坑记录与排查实录:技能库落地最容易翻车的地方

这部分是平时最容易忽略、但线上问题几乎全部集中在这里的内容。

4.1 技能描述与用户真实表达之间的“语言鸿沟”

我遇到过最典型的案例:技能描述里写的是“查询订单”,但用户实际说的是“我那个东西怎么还没到”“能帮我催一下快递吗”。模型其实知道要用技能,但就是匹配不上,因为描述里根本没有“快递”“到货”“催单”这类词。

排查方法很简单:打开线上对话日志,把模型实际触发的技能名和用户原始文本放在一起对比。你会发现,凡是频繁出现“模型未调用任何技能但用户明显有需求”的情况,大概率是技能描述缺少口语化同义词。

解决方式是我后来固定的一个习惯:每上线一个技能,先拉取过去三十天使用过的真实用户语料,提取高频词汇,回填进技能描述里。这个动作看起来简单,但对技能命中率的提升非常显著。例如,发货查询技能的描述里加上“包裹”“物流单号”“到哪了”“多久到”,整体准确率能直接上一个台阶。

4.2 模型“幻觉”参数:明明没有的订单号硬编一个出来

这个坑几乎每个做Agent的人都会踩。用户问“我上周的订单”,模型确实调用了查订单技能,但是把ORDER_ID填成了ORDER-12345678,这个单号完全是虚构的。技能层拿这个假单号去查询,自然什么都查不到。

这种情况的本质是:模型的参数抽取能力有问题,或者上下文中根本不存在订单号,模型只能靠猜。

彻底解决方案是在技能参数Schema中设置依赖描述:

{ "order_id": { "type": "string", "description": "订单编号,必须由用户直接提供或通过身份验证接口获取,禁止自行生成或猜测。如果缺少该参数,请先询问用户。" } }

描述里明确“禁止猜测”三个字,虽然不能保证100%避免幻觉,但实测能明显减少次数。更稳妥的做法是加一层执行前校验:如果参数值不符合业务规则(比如订单号格式不匹配,或者没有在已登录用户上下文里找到订单记录),直接让技能返回一个标准错误消息,让模型基于错误消息去询问用户。

4.3 技能之间的隐式依赖与循环调用

多技能组合场景下,一个比较隐蔽的问题是:技能A的执行逻辑内部又去调用了技能B,而技能B内部又回调A,形成了循环调用。例如:

技能A: 查询订单详情 技能B: 查询退款状态 查询订单详情的逻辑里,为了展示退款状态,又去调用了查询退款状态的接口。 查询退款状态的逻辑里,为了判断退款进度,又去调用了订单详情接口。

表面看两个技能都写得没毛病,但实际一跑,一对用户请求就能把服务拖僵死。

我处理这类问题的原则是:技能处理器内部不调用其他技能,只调用真实的业务API或数据服务。如果需要跨技能数据,应该在编排层组合,而不是在实现层相互调用。为此我还加了一个简单的依赖检查工具,扫描注册的所有技能处理器函数,凡是直接调用了SkillRegistry.execute内部逻辑的,都会在启动时被标记警告。

4.4 技能参数校验失败后的提示语模板

参数校验失败时,错误信息直接影响后续模型的回复质量。如果你直接抛出ValueError: sku_id为空,模型很可能把这个硬邦邦的异常文本直接转述给用户,非常不友好。

后来我给每个技能都定义了标准错误码和错误提示模板:

{ "error_code": "SKU_NOT_FOUND", "user_message": "没有找到编号为 {sku_id} 的商品,请您核对一下商品编码后重新发送。", "debug_message": "库存服务返回404,sku_id={sku_id}" }

技能执行异常时返回这个结构体(而不是抛出异常),模型拿到user_message字段,就能组织成一个自然、温和的回复。而debug_message则会同步记录在日志里,供研发人员排查。这样一鱼多吃,用户体验和工程排障都兼顾到了。

5. 技能库的日常维护:测试、观测与迭代

5.1 给每个技能配一套“罐头用例”做回归测试

技能是给模型用的,模型的调用充满了不确定性,所以技能库比普通代码更需要测试。我给自己定了一条规矩:每个技能上线时必须配备至少五个测试用例,覆盖正常调用、边界参数、缺失必填项、业务异常四种情况。

测试用例的形态是一一对应的“用户输入语句 → 期望触发的技能 → 期望参数值”。例如:

用例名称用户输入期望技能期望参数
正常查询帮我查一下订单SO-12345到哪里了query_order_detailorder_id=SO-12345
无参数查询我的订单情况怎么样query_order_listuser_id=当前用户
参数格式错误查订单123query_order_detail无(应触发追问)
非相关请求今天天气怎么样不触发任何订单技能-

这些用例不仅是测试脚本,实际也是后续微调Prompt的数据基础。每轮技能改动后跑一遍回归,能非常快速发现模型行为是否有退化。

5.2 技能调用日志:从对话中复盘每一处“失手”

我强烈建议从项目第一天起就记录完整技能调用日志。字段不需要太复杂,但下面这五项必须有:

时间戳、会话ID、用户输入原文、技能名称、入参JSON、出参JSON、执行耗时、是否成功。
注意:如果技能执行失败,必须同时记录异常堆栈或业务错误码,否则排查等于抓瞎。

有了日志,我做的最有价值的动作是每天跑一次“技能失手分析”:筛选出模型调用了技能、但用户随后明确表达不满(例如“不对”“不是这个”“我是说”)的会话,逐条查看是什么原因导致模型选错了技能或填错了参数。这个过程非常痛苦但非常值得,往往能发现描述上的模糊点、参数Schema设计不合理等平时注意不到的问题。

5.3 技能上线、停用与版本迭代机制

技能库发展到后期,一定会有技能被新技能替代,或者业务下线导致某个技能废弃。agent-skills里我实现了enabled开关,同时还有一个简单的版本控制字段。

版本控制的策略很简单:给每个技能增加一个version属性,注册时记录;执行时默认使用最新版本;如果模型在参数解析时出现历史技能的缓存引用,则自动映射到最新版本并记录一条告警日志。

技能废弃时,不是直接删除代码,而是先置为enabled=False,保留定义和实现,在日志中观察一段时间没有异常调用后再清理。这样做的原因是:已经被上下文中保存的历史消息引用的工具调用,某些模型还会尝试用旧ID访问,如果直接删除技能可能会在第二轮回调时报错。

6. 一些关键配置与技巧沉淀

到这里,主体架构已经全拆完了。最后我把自己几个月来沉淀下来的几条硬经验直接列在这里,希望能帮你少走一些弯路。

第一,技能数量控制在20到40个左右时,模型的选择准确率最高。少于20个,意味着有些技能粒度过粗,模型绕弯路;多于40个,模型的选择困惑度明显上升。如果真的需要超过40个技能,优先考虑分组路由:先让模型选择技能分类,再在分类内选择具体技能。

第二,技能返回的JSON结构必须稳定。上线后尽量不要更改字段名或嵌套层级,否则已经习惯旧结构的模型在相同场景下可能会输出错误引用。必须改时,先在描述里同步更新示例,再用几轮真实对话做回归验证。

第三,不要吝啬在技能描述里写“边界约束”。那些写着“不要用于XX场景”“仅当XX时才使用”的约束,虽然让描述显得啰嗦,但它们的价值在模型误触发率上有着直接影响。这是投入产出比极高的优化项。

第四,为技能执行设置超时与重试机制。我当时用的是两秒超时,失败后最多重试一次,仍失败则直接返回标准错误结构。这个设置让整体掉线率大大下降——外部API不稳定就是技术的常态,与其让用户干等,不如快速给一个可解释的答复。

第五,关于日志脱敏。技能库场景里一定会接触到用户手机号、订单号等敏感数据,日志记录前必须做脱敏处理,至少把中间几位打码。千万别贪图排查便利把明文数据全量入库,一旦日志库泄露就是安全事故,这个底线不要碰。

我在实际项目的感受是,agent-skills这套东西最爽的一点在于:你每新增一个业务能力,不再需要改动对话主流程的代码,只需注册一个新技能,写清楚描述和参数结构,就能立刻被代理使用。整个系统的扩展方式从“改代码”变成了“加配置”,这才是它真正的长期价值。如果你也正在做Agent类应用,建议从小规模开始,先把三个最核心的业务动作封装成技能跑通全链路,再慢慢扩充,这个方法比我一开始贪多贪全要舒服得多。

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

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

立即咨询