Agent技能库实战:让AI代理从工具调用走向自动化操作
真·干活的项目把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(namename, descriptiondescription, parameters_schemaparameters_schema, handlerfunc) 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( namecheck_inventory, description根据SKU编号查询商品库存情况当用户询问是否有货、库存多少、什么时候能发货时使用。, parameters_schemaINVENTORY_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(fSKU编号格式错误: {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( modelgpt-4o, messagesmessages, toolsSkillRegistry.get_all_skills(), tool_choiceauto ) 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_asciiFalse) }) 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( modelgpt-4o, messagesmessages, toolsSkillRegistry.get_all_skills() ) return final_response.choices[0].message.content这个模式的精髓在于“两次调用”第一轮让模型决定要不要技能、要哪些技能第二轮把技能真实执行结果交还给模型让它组织成人话回复。如果技能执行出错同样可以走第二轮流程让模型基于错误信息生成安抚话术或转人工判断体验会自然很多。3.3 技能编排如何实现多技能组合调用单一技能场景相对简单真正体现agent-skills价值的是多技能编排。售后场景中用户一句“我要退了这个订单里那个不合适的尺码”就同时涉及身份验证、订单查询、商品信息读取、售后受理等多个技能。我有两种编排方式可以分享顺序编排模型在第一轮调用时输出多个tool_calls它们之间没有依赖关系可以同时或者按顺序执行。比如同时查订单信息和查用户等级互不干扰。链式编排后一个技能需要前一个技能的执行结果作为输入。这种情况模型第一轮只会输出一个调用执行完拿到结果、拼接进消息历史之后再到第二轮继续决策。我代码里的循环结构就是干这个用的while True: response client.chat.completions.create( modelgpt-4o, messagesmessages, toolsSkillRegistry.get_all_skills(), tool_choiceauto ) 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: 库存服务返回404sku_id{sku_id} }技能执行异常时返回这个结构体而不是抛出异常模型拿到user_message字段就能组织成一个自然、温和的回复。而debug_message则会同步记录在日志里供研发人员排查。这样一鱼多吃用户体验和工程排障都兼顾到了。5. 技能库的日常维护测试、观测与迭代5.1 给每个技能配一套“罐头用例”做回归测试技能是给模型用的模型的调用充满了不确定性所以技能库比普通代码更需要测试。我给自己定了一条规矩每个技能上线时必须配备至少五个测试用例覆盖正常调用、边界参数、缺失必填项、业务异常四种情况。测试用例的形态是一一对应的“用户输入语句 → 期望触发的技能 → 期望参数值”。例如用例名称用户输入期望技能期望参数正常查询帮我查一下订单SO-12345到哪里了query_order_detailorder_idSO-12345无参数查询我的订单情况怎么样query_order_listuser_id当前用户参数格式错误查订单123query_order_detail无应触发追问非相关请求今天天气怎么样不触发任何订单技能-这些用例不仅是测试脚本实际也是后续微调Prompt的数据基础。每轮技能改动后跑一遍回归能非常快速发现模型行为是否有退化。5.2 技能调用日志从对话中复盘每一处“失手”我强烈建议从项目第一天起就记录完整技能调用日志。字段不需要太复杂但下面这五项必须有时间戳、会话ID、用户输入原文、技能名称、入参JSON、出参JSON、执行耗时、是否成功。注意如果技能执行失败必须同时记录异常堆栈或业务错误码否则排查等于抓瞎。有了日志我做的最有价值的动作是每天跑一次“技能失手分析”筛选出模型调用了技能、但用户随后明确表达不满例如“不对”“不是这个”“我是说”的会话逐条查看是什么原因导致模型选错了技能或填错了参数。这个过程非常痛苦但非常值得往往能发现描述上的模糊点、参数Schema设计不合理等平时注意不到的问题。5.3 技能上线、停用与版本迭代机制技能库发展到后期一定会有技能被新技能替代或者业务下线导致某个技能废弃。agent-skills里我实现了enabled开关同时还有一个简单的版本控制字段。版本控制的策略很简单给每个技能增加一个version属性注册时记录执行时默认使用最新版本如果模型在参数解析时出现历史技能的缓存引用则自动映射到最新版本并记录一条告警日志。技能废弃时不是直接删除代码而是先置为enabledFalse保留定义和实现在日志中观察一段时间没有异常调用后再清理。这样做的原因是已经被上下文中保存的历史消息引用的工具调用某些模型还会尝试用旧ID访问如果直接删除技能可能会在第二轮回调时报错。6. 一些关键配置与技巧沉淀到这里主体架构已经全拆完了。最后我把自己几个月来沉淀下来的几条硬经验直接列在这里希望能帮你少走一些弯路。第一技能数量控制在20到40个左右时模型的选择准确率最高。少于20个意味着有些技能粒度过粗模型绕弯路多于40个模型的选择困惑度明显上升。如果真的需要超过40个技能优先考虑分组路由先让模型选择技能分类再在分类内选择具体技能。第二技能返回的JSON结构必须稳定。上线后尽量不要更改字段名或嵌套层级否则已经习惯旧结构的模型在相同场景下可能会输出错误引用。必须改时先在描述里同步更新示例再用几轮真实对话做回归验证。第三不要吝啬在技能描述里写“边界约束”。那些写着“不要用于XX场景”“仅当XX时才使用”的约束虽然让描述显得啰嗦但它们的价值在模型误触发率上有着直接影响。这是投入产出比极高的优化项。第四为技能执行设置超时与重试机制。我当时用的是两秒超时失败后最多重试一次仍失败则直接返回标准错误结构。这个设置让整体掉线率大大下降——外部API不稳定就是技术的常态与其让用户干等不如快速给一个可解释的答复。第五关于日志脱敏。技能库场景里一定会接触到用户手机号、订单号等敏感数据日志记录前必须做脱敏处理至少把中间几位打码。千万别贪图排查便利把明文数据全量入库一旦日志库泄露就是安全事故这个底线不要碰。我在实际项目的感受是agent-skills这套东西最爽的一点在于你每新增一个业务能力不再需要改动对话主流程的代码只需注册一个新技能写清楚描述和参数结构就能立刻被代理使用。整个系统的扩展方式从“改代码”变成了“加配置”这才是它真正的长期价值。如果你也正在做Agent类应用建议从小规模开始先把三个最核心的业务动作封装成技能跑通全链路再慢慢扩充这个方法比我一开始贪多贪全要舒服得多。