每年都有一波新的技术热点,今年轮到了Agent。我最近在一个内部项目里做了一套“技能系统”,项目代号就叫agent-skills,本质上是给大模型驱动的智能体装一套“可插拔工具箱”。这篇文章就把这套系统的设计思路、实现细节和踩过的坑完整记录下来,给正在搞Agent应用、尤其是不满足于“只会聊天”的开发者一个参考。
先说结论:Agent真正落地,瓶颈不在模型,而在“边界”。模型再强,它也不知道你公司内部系统有哪些接口、你的数据库长什么样、你的业务流程分几步。你当然可以把这些信息全部塞进Prompt里,但很快会发现上下文塞爆、行为失控、维护成本疯涨。agent-skills的解法,是把模型不具备的能力“外挂”成一个一个标准化技能,让Agent在需要时按需调用。这套思路可以类比给人类员工配工具:你不要求一个人记住所有事情,但你要给他一套好用的工具和明确的使用手册。
适合哪类人来参考?正在设计客服机器人、自动化工单系统、数据分析助手、知识库问答这些场景的开发者,或者对Agent架构感兴趣、准备入坑的朋友,这篇文章都能给你一个足够落地的起点。
1. 先拆清楚:agent-skills到底在解决什么问题
1.1 Agent“看起来很智能,跑起来很拉胯”的根源
我见过太多Agent Demo的演示效果:问它“帮我把上个月的销售数据做个分析”,它一口气输出十几行流程,看着有板有眼。结果一放到真实业务环境里就露馅——要么调接口时参数格式不对,要么拿不到数据就开始胡编,要么一个简单任务反复横跳无法结束。
问题出在哪?三个字:不确定性。
大模型本质上是一个概率预测模型,它天然擅长的是“生成”,而不是“精确执行”。你让它“生成一段话”很容易,但让它“按这个接口格式精确调用并拿到正确数据”就很难稳定。Agent框架做的事情就是给它配手和脚,让生成路径变得可约束。但如果每一次功能调用都是现场写代码让模型自由发挥,那等于每次都让一个实习生在没有说明书的情况下操作一台机器,出错是大概率事件。
agent-skills的核心思路,是把“模型自由发挥”改为“模型选择题”。系统内预先定义好一套标准技能清单,每个技能有明确的名称、用途、参数格式、返回结构。Agent运行时,先由意图识别环节从技能清单里选出最合适的一个或多个技能,再由系统用确定性代码去执行,而不是让模型直接生成执行代码。这一步就把不确定变成确定,把大模型的失误率压到了可接受范围。
1.2 技能的三个层次:工具、流程、决策
做技能拆解的时候,我一开始把所有能想到的功能都堆在一起,导致技能列表混乱。后来参考了人类组织分工的逻辑,把技能分成三层,整个系统才理顺。
第一层是工具技能,解决“能不能做”的问题。比如发送HTTP请求、读写文件、查数据库、发邮件、发即时消息、调用某个内部API。这一层是全部技能的地基,粒度最细,复用性最高。
第二层是流程技能,解决“怎么做”的问题。流程技能是对工具技能的编排和封装,它面向一个相对完整的业务动作,比如“创建工单并通知负责人”“导出月度报表并发给指定邮箱”。流程技能的好处是,用户侧不需要关心内部调了几个接口、顺序是什么,Agent使用者只需要调用一个技能入口。
第三层是决策技能,解决“该不该做”的问题。这就是模型层做的事情,它本身不是一个可执行函数,而是一个判断逻辑。我在系统里把这类技能实现为“路由策略”,比如用户请求涉及删除操作时,先走一个权限校验技能,没通过就不调用底层工具。把决策也当作技能注册,好处是整个链路统一,审计起来一目了然。
三层分清楚之后,每次新需求进来,先判断这个需求落在哪一层。如果只是新接一个外部接口,那就是工具技能;如果需要对多个接口进行编排,那就是流程技能;如果是规则调整,那就是决策技能。维护成本立刻降下来了。
2. 技能系统设计的四个关键决策
2.1 决策一:技能描述比技能实现更重要
这是我踩坑最深的地方。最开始做技能注册表,我把大部分精力花在写执行逻辑上,技能的描述就随手写一句话,比如“查询订单”。结果模型在意图识别时频繁选错,经常用户明明问的是“退款进度”,它却调用了“查询订单”,返回结果驴唇不对马嘴。
后来我才想明白一个道理:对大模型来说,它看到的不是你的代码,而是你的技能描述。描述写得不够准确、缺少边界条件,它就无法理解这个技能到底该在什么场景下用。这就像公司内部工具库,如果工具上只贴着“扳手”两个字,员工拿错工具的概率就会很高。
我后来定了一套技能描述写作规范,每条技能的描述必须包含四要素:技能能干什么、适合在什么场景下触发、需要用户提供什么信息、不可以用于什么情况。比如“查询订单”这个技能,描述就写成:“根据订单编号查询订单当前状态,适合用户询问‘我的订单到哪了’‘发货没有’等场景,需要用户提供订单号或手机号;不适用于查询退款进度,退款进度请使用查询退款技能。”
这个改动带来的效果立竿见影,意图识别的准确率从勉强及格直接拉到了九成以上,而且技能的误触发率大幅下降。如果你做Agent系统,请把技能描述当作第一优先级来写。
2.2 决策二:标准化的技能声明结构与参数映射
技能和Agent之间的通信协议需要统一,否则每接一个技能就要写一套适配代码,后期维护会非常痛苦。我参考了接口文档的通用定义方式,把每个技能抽象成一个标准结构体,包括几个部分:技能ID、技能名称、描述信息、参数定义、执行入口、权限级别、返回结构。
参数定义是重头戏。每个技能都要声明它的参数列表,每个参数要说明类型、是否必填、取值范围、默认值、示例值。这里的关键是“示例值”,它不是为了给开发者看的,而是为了让模型在参数解析时有一个参照。我试过多个方案,在参数Schema里加上示例值之后,模型在抽取参数时明显更准确。
参数映射还有一个容易被忽略的点:用户的原话里往往不会直接给你规范字段名。比如用户说“我是上周买的那批货”,你的技能参数里根本没有“上周”,只有“购买时间”。解决这个问题不能靠模型硬推理,我在参数定义层增加了一个“别名映射”机制,把常见口语化表达映射到标准参数上,比如“上周”“前天”“最近”映射为具体时间段表达式。这样模型在做参数填充时压力小很多,准确率也更稳定。
2.3 决策三:注册中心与统一调用抽象
技能系统要做到可扩展,核心是有一个独立的注册中心。所有技能在启动时统一注册到注册中心里,注册中心负责三件事:维护技能清单、提供技能查询接口、管理技能启停状态。
我实现的时候没有搞复杂的微服务,注册中心就是一个独立模块,内部用字典存储技能元数据。技能启动的时候往注册中心注册自己的信息,Agent运行时要调用什么能力,先去注册中心查,查到了再走统一执行网关。执行网关就是整个系统里唯一允许调用技能代码的地方。
统一调用抽象的价值在于:所有的技能调用都要过同一层,这一层可以做统一的日志记录、超时控制、失败重试、流量统计和权限校验。审计的时候不用翻遍每个技能的代码,只要看执行网关上的日志,就知道系统在什么时间、调了哪个技能、花了多久、结果是什么。
而且这层抽象让技能的重试策略变得很方便。技能执行失败时,网关可以按配置决定重试几次、退避多久。我不建议在单个技能代码内部写死重试逻辑,否则每个技能的重试策略都不一样,出了问题很难排查。
2.4 决策四:权限隔离与最小授权
Agent系统一旦接上真实业务操作,安全问题就必须认真对待。我给每个技能挂了权限级别,共分为四档:只读、执行、写入、管理。只读技能比如查询数据,执行技能比如触发一个流程,写入技能比如修改数据库记录,管理技能比如删除数据、改配置。
Agent在运行时,当前会话会有一个“会话身份”,每一次技能调用都要经过权限校验,如果技能要求的权限级别高于当前会话身份,直接拒绝并返回提示。这个设计模仿的是人类组织里的授权逻辑,一个实习生不能删库,一个普通用户不能改别人的订单。
实际操作中,我还把所有高风险操作加了一道二次确认:技能定义里增加一个“人工确认”标志位。当标识开启时,执行网关会先生成一个确认请求,推送给前端或消息端,等用户点了确认再真正执行。这个设计在自动化任务里看似多了一步,但能挡住一大批误操作。
3. 实操:从零搭建一套可复用的技能注册与调度系统
3.1 技能注册表与装饰器封装
讲完设计,直接上一套能在本地跑起来的最小实现。我这里用Python写一个演示版本,没有依赖任何重型框架,只要环境里有Python 3.9以上就可以跑。
第一步是定义技能注册表,一个全局的字典结构,存所有技能元数据,提供注册和查询接口。
SKILL_REGISTRY = {} def register_skill(skill_id: str, name: str, description: str, parameters: list, permission: str = "read", confirm: bool = False): def decorator(func): SKILL_REGISTRY[skill_id] = { "id": skill_id, "name": name, "description": description, "parameters": parameters, "handler": func, "permission": permission, "requires_confirmation": confirm, } return func return decorator def get_skill(skill_id: str): skill = SKILL_REGISTRY.get(skill_id) if not skill: raise KeyError(f"技能不存在: {skill_id}") return skill def list_skills(): return [{"id": s["id"], "name": s["name"], "description": s["description"]} for s in SKILL_REGISTRY.values()]这段代码的作用是提供一个统一入口,让后续每次新增技能都只需要做一件事:写一个函数并用装饰器注册它的元信息。不需要在别的地方再加代码,这个设计可以保证技能模块之间是解耦的。
3.2 三个具体技能的注册示例
来看几个真实技能的定义。第一个是订单查询技能,属于只读级别。
import datetime import json @register_skill( skill_id="order_query", name="查询订单状态", description="根据订单编号或手机号查询订单当前状态,适合用户询问订单进展、是否发货、物流到哪等场景。" "需要用户提供订单编号或下单手机号。不适用于退款进度查询。", parameters=[ { "key": "order_id", "type": "string", "required": False, "desc": "订单编号,类似ORD2025010001", "examples": ["ORD2025010001", "ORD2025120999"] }, { "key": "phone", "type": "string", "required": False, "desc": "下单手机号后四位", "examples": ["1234", "8888"] } ], permission="read" ) def query_order(order_id: str = None, phone: str = None): if not order_id and not phone: return {"error": "order_id 和 phone 至少提供一个"} # 实际项目中这里替换为数据库查询或调用订单系统的API mock_orders = { "ORD2025010001": {"status": "已发货", "logistics": "运输中,预计两天后送达", "updated_at": "2025-01-12 10:00"}, } for oid, data in mock_orders.items(): if order_id and oid == order_id: return data if phone and data.get("phone_tail", "") == phone: return data return {"error": "未找到匹配订单"}第二个是发送通知技能,权限级别是执行,且操作后要留痕。
@register_skill( skill_id="message_send", name="发送即时消息通知", description="向指定用户发送站内信或即时消息通知,适合需要主动通知用户的场景。" "需要用户提供接收人标识和消息内容。消息内容长度限制在200字以内。", parameters=[ { "key": "user_id", "type": "string", "required": True, "desc": "接收人用户ID", "examples": ["U10001", "U10002"] }, { "key": "content", "type": "string", "required": True, "desc": "消息内容", "examples": ["您的订单已完成退款"] } ], permission="execute" ) def send_message(user_id: str, content: str): if len(content) > 200: return {"error": "消息内容超长,请压缩后重新发送"} # 对接实际的消息发送通道 print(f"[发送通知] 发给 {user_id}: {content}") return {"success": True, "message_id": "MSG" + datetime.datetime.now().strftime("%H%M%S")}第三个是导出报表技能,因为涉及生成文件,所以标记为需要人工确认。
@register_skill( skill_id="report_export", name="导出月度报表", description="生成指定月份的销售或运营报表,生成后通过消息发送给用户。" "适合用户提出要月度数据、导出Excel、生成报表等场景。" "需要用户提供报表月份。", parameters=[ { "key": "month", "type": "string", "required": True, "desc": "报表月份,格式YYYY-MM", "examples": ["2025-01", "2024-12"] }, { "key": "report_type", "type": "string", "required": False, "desc": "报表类型,sales代表销售,operation代表运营", "examples": ["sales", "operation"] } ], permission="write", confirm=True ) def export_report(month: str, report_type: str = "sales"): print(f"[报表导出] 月份={month}, 类型={report_type}") return {"success": True, "file": f"{month}_{report_type}_report.xlsx", "size": "2.3MB"}这三个技能展示了不同权限级别、不同复杂度、不同安全要求的写法。后面新增技能时,照着这个格式复制一份再改关键逻辑就可以了。
3.3 执行网关与意图路由
技能定义好之后,需要有调度逻辑。这一层我实现了两个核心模块:意图路由器和执行网关。
意图路由器的作用是把用户输入匹配到技能ID。在小规模技能库里,我的做法很简单,用模型判断太慢且不稳定,我是先用关键词规则做初筛,把候选技能缩小到两三个,再交给一个小模型做最终选择。这种方式兼顾速度和准确率。
def intent_route(user_input: str): # 第一阶段:关键词初筛 candidates = [] for skill_id, skill in SKILL_REGISTRY.items(): keywords = skill.get("keywords", []) if not keywords: candidates.append(skill) continue for kw in keywords: if kw in user_input: candidates.append(skill) break if len(candidates) == 1: return candidates[0]["id"] # 第二阶段:交给模型判断(这里用规则示例替代模型调用) if "订单" in user_input or "发货" in user_input or "物流" in user_input: return "order_query" if "通知" in user_input or "提醒" in user_input: return "message_send" if "报表" in user_input or "excel" in user_input or "导出" in user_input: return "report_export" return None实际项目中,第二阶段用大模型的函数调用能力来识别会更稳,但为了让你能在本地无依赖地跑通整个链路,上面的示例先用规则替代。生产级实现里,我会把候选技能的描述拼成一串,配合用户输入一起发给模型,让它返回一个技能ID和参数JSON。
执行网关负责调用技能并处理统一的事务逻辑:查询技能信息、检查参数、权限校验、确认拦截、执行、记录日志。
def execute_skill(skill_id: str, parameters: dict, session_permission: str = "read"): skill = get_skill(skill_id) if not has_permission(session_permission, skill["permission"]): return {"error": "权限不足,当前会话不允许调用该技能"} if skill["requires_confirmation"]: return {"error": "该操作需要人工确认,请调用确认接口", "confirm_required": True} # 参数完整性校验 missing = [] for param in skill["parameters"]: if param["required"] and parameters.get(param["key"]) is None: missing.append(param["key"]) if missing: return {"error": f"缺少必填参数: {', '.join(missing)}"} print(f"[执行] 技能={skill['name']}, 参数={json.dumps(parameters, ensure_ascii=False)}") try: result = skill["handler"](**parameters) return {"success": True, "result": result} except Exception as e: return {"error": f"技能执行失败: {str(e)}"}这里用一个简单的权限对照函数来完成权限管理。
PERMISSION_LEVEL = {"read": 1, "execute": 2, "write": 3, "manage": 4} def has_permission(session_permission: str, required_permission: str) -> bool: return PERMISSION_LEVEL.get(session_permission, 0) >= PERMISSION_LEVEL.get(required_permission, 99)3.4 完整调用链演示与结果
上面各个模块串起来,跑一个完整链路。假设用户说“帮我查一下订单ORD2025010001,然后发个通知给U10001说订单已发货”。系统先做意图路由,匹配到两个技能:订单查询、发送通知。
# 模拟完整调用链 user_input = "帮我查一下订单ORD2025010001,然后发个通知给U10001说订单已发货" session_role = "普通用户" # 技能清单给到模型或规则引擎 skills_info = list_skills() print("可用技能清单:") for s in skills_info: print(f" - {s['id']}: {s['name']}") # 第一步:意图路由,先匹配order_query route_1 = intent_route("查询订单 ORD2025010001") order_res = execute_skill(route_1, {"order_id": "ORD2025010001"}, session_role) print("订单查询结果:", order_res) # 第二步:根据查询结果触发消息通知 if order_res.get("success"): msg_res = execute_skill("message_send", {"user_id": "U10001", "content": "您的订单已发货,快递运输中"}, session_role) print("消息通知结果:", msg_res)运行这段代码,控制台会依次输出技能清单、订单查询结果、通知发送结果。整个链路是确定性的,每一步都有日志。你在这个基础上可以继续扩展:把技能清单交给大模型选择,把执行结果格式化后再交给大模型生成自然语言回复。这就是一个完整、可扩展的Agent技能系统雏形。
4. 常见问题与排查实录
4.1 技能描述写不好,模型总是选错技能
这是最大的坑,发生的频率也最高。现象是用户输入明明在问A技能,系统却调了B技能。排查思路:先看技能描述是否包含足够的触发场景词,再看描述里有没有明确写“不适用于什么场景”。
我给所有技能描述增加“不适用于”后缀之后,误选率明显下降。比如订单查询技能明确写上“不适用于退款进度查询”,模型在遇到退款提问时就会主动排除这个技能,转而寻找退款相关的技能。写技能描述的时候,克制一点,别把所有能力都堆在一个技能里,专一的技能更容易被准确选中。
另外建议在测试阶段对每个技能准备5到10条典型用户输入,做成回归测试集。每次新增技能或修改描述后,跑一遍回归测试,看有没有技能冲突。这个习惯能帮你尽早发现问题,而不是等到线上用户来投诉。
4.2 参数解析不准,模型给的参数格式对不上
用户说“帮我查上周的订单”,技能参数里只定义了order_id,模型没法把“上周”填进去。两个解决方向:一是把参数定义得更加语义化,给模型更充分的字段选择空间,比如增加一个“时间范围”参数;二是对常用口语化表达做预处理,在意图路由后、技能执行前,插入一层“参数标准化”逻辑,把“上周”“上个月”“昨天”这类表述转成具体的日期范围。
我踩过的另一个参数坑是类型问题。技能定义里参数类型是int,模型返回了string类型,执行时直接报错。解决方案是在执行网关里做一层严格类型转换和校验,而不是把转换逻辑散落在各个技能函数里。比如定义一个参数转换器,读取参数定义里的type字段,把string转int、把字符串转Boolean,转不了就返回明确的错误提示。
4.3 技能太多后,模型选择困难
技能数量从几个涨到几十个后,意图路由的准确率会明显下降。这就像给员工一本几百页的工具手册,他翻都要翻半天。解决思路有两个:分组路由和二级路由。
分组路由是把技能按业务域分组,比如订单域、用户域、财务域,第一步先识别用户请求属于哪个域,第二步在域内部做具体技能选择。二级路由则是先在粗粒度层面做一次判断,再做细粒度选择。我在生产环境里就是先按分组过滤,把候选技能数量控制在十个以内,模型的选择准确率就能保持在高位。
另一个思路是给技能增加“标签”字段,在注册中心维护时顺手打几个标签。路由阶段优先用标签缩小候选范围,再做最终选择。这个方案实现成本最低,效果也不错。
4.4 并发调用同一技能导致数据异常
有一次线上反馈,用户连续点了两次确认,结果同一个报表被导出了两次。排查发现是确认接口没有做幂等控制。解决方法是给技能执行增加“请求去重”:用户在发起技能调用时会生成一个request_id,执行网关维护一个最近处理过的request_id列表,如果短时间内收到相同的request_id,就直接返回第一次的执行结果,不再重复执行。
执行网关里还可以加一层并发锁。对于写类技能,同一技能同一参数在同一时间只允许一个请求在执行,后续请求排队等待。这个是基础保护,有了它很多奇怪的数据问题都能挡住。
4.5 技能执行报错,但用户看到的是模型胡编的“成功”
这是非常隐蔽的问题。模型在执行技能时,即使技能返回了error,模型也可能会“顺着编”一段看起来合理的成功回复。你问它“报表导出成功了吗”,它会非常自信地说“成功导出并发送,文件大小2.3MB”。
解决这个问题必须靠强约束。在给模型的系统提示词里明确写一条规则:只有技能调用返回success为true时可以确认成功;其他任何情况下,都要把原始错误信息完整转述给用户。同时执行网关里在技能返回error时,不把错误详情直接拼进大模型的上下文,而是用统一的错误模板替换,降低模型自由发挥的空间。
4.6 技能内部依赖的第三方接口不稳定
技能系统本身再稳,底层接口不稳定一切白搭。我实践的方案是给执行网关加两级保护:超时控制和降级策略。每个技能定义里可以配置超时时间,比如查询类技能设3秒超时,写类技能设5秒超时。超时后网关直接返回错误,不让技能函数无限等下去。
降级策略更实用。如果一个技能依赖多个数据源,比如报表技能依赖订单库和用户库,某个数据源挂了,系统可以自动降级成只输出部分数据,并在返回结果里带上“数据不完整”的标记。这样用户至少能得到一部分可用信息,而不是被彻底拒绝。
5. 一些具体的落地建议
如果你准备在自己的项目里引入这套技能体系,我建议从改造一个现有Agent开始,而不是推倒重来。先把你当前Agent里被频繁调用、逻辑最稳定的那几个功能抽出来,定义成技能,跑通链路后,再逐步把其他能力迁移进来。一次不要铺太开。
技能系统的元数据维护,一定要把它当一等公民来对待,而不是“顺手写个字符串”。我建了一个简单的技能维护页面,只包含技能名称、用途描述、参数列表和负责人四个字段,每次变更都有记录。技能库就像公司流程制度,版本混乱了,执行层面必然出乱子。
最后说说模型选择。技能调度层其实是可以独立于模型厂商的,只要模型具备基本的意图识别和参数抽取能力,就能配合这套系统跑起来。我个人建议流程技能尽量用确定代码实现,不要让模型参和流程编排的细节。大模型的强项是理解和交互,不是精确执行。把这两者的分工想清楚,你的Agent系统会越跑越顺。