ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent实战:agent-skills从零搭建到工程化落地的完整指南

AI Agent实战:agent-skills从零搭建到工程化落地的完整指南 先交代一个背景我这两年一直在做 AI Agent 方向的应用开发。团队最早做出来的“智能助手”本质上就是一个套了 Prompt 的大模型聊天框用户觉得新鲜但用两天就腻了因为光会说话解决不了实际问题。后来我们把方向调整到 agent-skills 这套体系上局面才真正打开。所谓 agent-skills你可以直接理解成“给 Agent 装上能干活的手脚”。它解决的正是大模型“脑子聪明但四肢瘫软”的尴尬——模型再强不会查库存、不会提工单、不会读报表就永远只是个陪聊。这篇就把我们团队从零搭建 agent-skills 的完整过程、踩过的坑、优化思路全部倒出来项目无论大小只要你在做 Agent这套方法论基本都能直接套。1. 先搞清楚 agent-skills 到底是什么1.1 从“聪明但手无缚鸡之力”的大模型说起大模型本身的能力边界很清晰它能理解、推理、生成自然语言但它做不到“对外部世界产生物理影响”。让它描述一下 ERP 系统的接口逻辑它可以讲得头头是道让它自己去调一下接口把订单状态改掉它立刻歇菜。模型没有手所有的语言输出都需要一个“执行层”去落地。agent-skills 就是那个执行层的标准载体。它把一个个真实业务动作——查数据库、调 API、处理文件、发通知——封装成模型可以感知、可以调用、可以安全运行的独立模块。每个 skill 自带说明文档、参数协议、执行代码和运行环境模型读了说明就知道这个技能能干什么然后把用户模糊的自然语言需求翻译成结构化的调用参数真正去执行。生活里打个比方新来的实习生脑子聪明、理解力强但他不会用公司内部的 OA 系统你得先给他一份《OA 系统操作手册》再让他在沙箱环境里练几次。agent-skills 就是给大模型准备的“岗位操作手册上岗培训”手册内容和执行工具打包在一起随取随用。1.2 Skills、Function Calling、Plugin到底有什么区别很多人一开始会把 agent-skills 和 function calling 混为一谈。我自己也经历过这个阶段踩了阵子坑才把边界理清楚。Function Calling 是模型层的能力它允许你在请求里声明一批函数的名称和参数结构模型在生成回复时会额外输出一个结构化的“函数调用指令”。但它本身只是一个协议函数具体怎么实现、运行环境怎么隔离、依赖怎么管理都不归它管需要应用层自己兜底。它解决的问题是“让模型输出调用意图”而不是“把技能完整地交付出去”。Plugin 更偏向应用层整合通常是某个产品平台定义的一套扩展规范插件跟着平台走迁移性一般。agent-skills 的定位则要更“重”一些它不只是一个调用协议而是一整套工程化封装。一个 skill 里除了给模型看的“使用说明”还包括实际执行代码、依赖清单、参数校验规则、生命周期管理逻辑。Skills 之间相互独立可以单独开发、单独测试、单独上线也能在不同 Agent 项目之间复用。说得直白点function calling 是“让模型知道有个电话可以打”而 agent-skills 把“电话装好、线路调通、通话录音留档”全部一并交付了。选型和取舍上我个人的建议是如果只是三五个轻量工具用 function calling 足够一旦技能数量上到几十个或者涉及多步骤、多依赖的复杂任务就别硬扛直接上 skills 体系管理成本会低非常多。1.3 一个标准 Skill 到底长什么样我们的 skill 仓库里每个技能目录基本都长这样skills/ ├── order_query/ # 工单查询技能 │ ├── SKILL.md # 技能说明书给模型看的 │ ├── schema.json # 入参协议定义 │ ├── main.py # 核心执行逻辑 │ ├── requirements.txt # 依赖清单 │ ├── tests/ # 用例测试 │ └── examples/ # 示例调用SKILL.md 是给大模型看的核心文档里面的描写质量直接决定模型能不能精准地调用这个技能。schema.json 是参数校验的依据确保模型传进来的参数不越界、没缺项。main.py 是真刀真枪干活的脚本所有外部副作用都收敛在这里。这样一套打散的结构其实和做微服务是一个思路只不过服务的“调用方”从人换成了大模型。一个技能是否能可靠运行通常取决于这四个部分配合得怎么样缺一个都会在后面埋雷。2. 方案选型与架构设计为什么 Skills 要这样搭2.1 决定模型成败的 SKILL.md描述质量是第一生产力和很多人想的不一样SKILL.md 不是写给人看的文档它是一份“面向模型的接口说明”。模型的调用决策依据主要来自技能名称和描述文本写得含糊模型就不敢用、不会用。我们最早写描述特别随意比如“获取订单信息的工具。”结果模型经常在用户问“帮我看看我上周买的蓝牙耳机发货没”的时候完全不去调这个工具因为它根本不认为这条描述和“发货查询”有什么关系。后来我们重写成了“当用户询问订单状态、物流进度、发货时间、商品到货情况时使用本工具查询真实订单数据。输入需要订单号或用户手机号如果用户没有提供任何可识别信息先向他询问订单号。”效果立刻不一样调用准确率从六成直接拉到九成以上。写好 SKILL.md 有几个实操要点触发条件写全模型要能“认出”什么场景该用它越具体越好不要怕啰嗦。输入描述写透参数缺失时该怎么引导用户补充你要在文档里交代清楚否则模型会自作主张瞎编。边界和禁忌写死哪些情况不归它管哪类数据不能碰必须在文档里红线圈出来。示例调用必须有模型对“该传什么参数”的理解能力有限给一两个真实例子效果立竿见影。记住模型是不带行业常识的“高智商外星人”你和它沟通的唯一渠道就是文本描述。描述的质量决定了它做事的天花板。2.2 参数 Schema把“模糊的自然语言”翻译成“精确的结构化输入”有了 SKILL.md 之后模型还要面对一个难题把用户的自然语言转换成符合要求的参数。这一层靠 schema.json 约束。我推荐直接用标准 JSON Schema因为它被大模型训练语料覆盖得最好模型理解成本最低。以常见的工单查询为例最简 schema 长这样{ type: object, properties: { order_id: { type: string, description: 订单号形如 alipay-20240601-xxxx }, phone: { type: string, description: 用户下单手机号用于未登录状态下查询 } }, oneOf: [ { required: [order_id] }, { required: [phone] } ] }这里两个设计细节值得说说。一是用oneOf限定“必须且只能提供一种查询凭据”避免模型同时传了个残缺的订单号和手机号后端根本没法查。二是每个字段里都补了具体格式示例模型在生成参数时更不容易跑偏。实战中我们还发现一个规律模型对“类型”的把握非常严格但对“格式”经常松懈。比如你声明某字段是string模型会老老实实传字符串但如果你不告诉它日期格式是YYYY-MM-DD它可能给你输出“2024年6月1日”这种人类友好、机器灾难的玩意儿。所以凡是格式敏感的字段必须在 description 里写死格式。2.3 运行环境隔离Skill 不应该和 Agent 进程“同生共死”早期我们图省事让 skill 直接在 Agent 主进程里跑 Python 代码。结果有一次某个技能内部的第三方库和主程序依赖冲突把整个服务搞崩了线上 Agent 直接宕机十分钟。那次事故之后我们彻底定了规矩skill 必须在独立的子进程或容器中执行。倒不是说每个 skill 都得上 Docker。现在我们用的是“进程级隔离超时熔断”的方案每个 skill 调用时由调度器拉起一个新进程通过 stdin/stdout 做 JSON-RPC 通信外部依赖全部打包到独立的 virtualenv 里。开销不算大但换来的是“怎么造都炸不到主服务”的安心感。隔离设计里最容易被忽略的是超时控制。模型端一个响应通常几秒到十几秒就结束了但 skill 执行时可能遇到外部接口慢、代码死循环、依赖卡住等问题。没有超时熔断一个失控的 skill 就会变成一场事故。我们现在统一设置了从 10 秒到 120 秒不等的超时阈值根据技能耗时特性单独配置超时直接干死进程并给模型返回“技能执行超时”的提示让模型有机会换成备用方案或者如实向用户解释。2.4 状态管理把每次调用都当成“第一次见面”Skill 设计里最隐蔽的一个坑是状态共享。如果一个技能可以把“上一轮查询结果”存在内存里下一轮接着用表面看是省事了实际制造了一堆难以排查的诡异问题。比如两个用户同时调用了技能A 用户的查询结果可能被 B 用户读到再比如 Agent 重启后技能内部状态丢了但模型不知道继续按“之前已经查到过”的逻辑走给出的答复就是错的。我们的原则很简单技能必须无状态。所有需要跨轮保存的数据要么由 Agent 主流程显式存到外部存储里要么就放弃记忆。技能只负责“收到参数→执行→返回结果”其他的事情不要越俎代庖。这个原则一开始可能觉得束缚但它能让技能的复用性、并发安全性直接上一个台阶。3. 实操从零开发一个“工单查询 Skill”并接入 Agent3.1 定需求与边界先搞清楚技能该干什么拿一个最常见的业务场景练手给 Agent 做一个工单查询技能。需求其实很清晰——用户问“我的耳机订单到哪了”Agent 能调用技能查询真实物流状态并整理成自然语言回答。边界也得一开始就切好。这个技能只负责查不负责改单、不负责退换货、不负责催发货。凡是修改类操作全部在技能里直接拒绝。边界画清楚模型就不会把“帮我把订单退了”也错误地路由到查询技能上。3.2 定义 SKILL.md 和 schema.json把“说明书”写到位这部分是整个技能的灵魄直接决定模型后续能不能准确调用。我们现成的模版是这样# 工单查询技能 ## 功能描述 查询用户在平台上的订单信息、物流状态、预计送达时间。 ## 触发条件 - 用户询问“订单到哪了 / 发货没 / 物流信息 / 快递进度” - 用户询问“我买了什么 / 我的订单列表” - 用户对某笔订单有疑问但明确提到“查询订单” ## 不处理的事项 - 不处理订单修改、退款、退货、投诉 - 不处理未登录场景下的隐私信息查询 ## 输入要求 必须提供以下至少一种凭据 - order_id订单号形如 ORD-20240601-0001 - phone下单时填写的手机号 如果用户两个凭据都没有提供先向用户询问订单号或手机号。 ## 输出要求 返回订单状态、商品名称、物流轨迹摘要、预计送达时间。对应 schema.json 就是前面那段代码这里不再重复。3.3 实现执行逻辑查询 API 并把数据整理成模型友好的格式技能核心是 Python 脚本整体逻辑不复杂解析输入参数 → 调用后端查询 API → 清洗整理成结构化 JSON → 输出。要注意的是输出给模型的结果建议同时带“人读”和“机读”双层结构方便模型直接引用。import json import sys import httpx def main(raw_input: str) - str: params json.loads(raw_input) order_id params.get(order_id) phone params.get(phone) if not order_id and not phone: return json.dump({error: 缺少order_id或phone}, ensure_asciiFalse) # 真实项目里用签名、鉴权等逻辑这里省略 resp httpx.post( https://api.yourapp.com/order/query, json{ order_id: order_id, phone: phone, }, timeout5, ) resp.raise_for_status() data resp.json()[data] # 整理成模型友好的摘要结构 result { order_status: data[status_name], tracking: data[tracking_events], eta: data.get(estimated_delivery), } return json.dump(result, ensure_asciiFalse) if __name__ __main__: raw_input sys.stdin.read() print(main(raw_input))这里有个很容易被新手忽略的坑空值处理。如果订单查不到接口返回的是 null而不是错误——脚本里若不显式处理模型很容易把“null”当成“系统异常”给用户回复一堆莫名其妙的故障描述。所以我们会在返回结构里专门留一个message字段查询无结果时明确写“未查询到该订单请核对凭据是否正确”模型拿到的文本越自洽它的回复就越靠谱。3.4 接入 Agent 框架把技能挂到调度器上技能本身写成独立脚本只是第一步接入 Agent 主流程才真正产生价值。现在的做法是把 skill 声明成一个可调用工具注册进调度器模型根据用户意图自行选择调用哪个技能。接入方式不取决于某个框架只要是支持工具调用的都能用核心只是注册时提供一个技能清单给模型。我的习惯是把 SKILL.md 的内容和 schema 合并成一个 manifest注册时直接传给框架。skills_manifest [ { name: order_query, description: open(skills/order_query/SKILL.md).read(), input_schema: json.load(open(skills/order_query/schema.json)), executor: subprocess, command: python skills/order_query/main.py, } ] def route_skill(name: str, params: dict) - str: skill next(s for s in skills_manifest if s[name] name) if skill[executor] subprocess: proc subprocess.run( [skill[command]], inputjson.dumps(params), capture_outputTrue, textTrue, timeout30, ) return proc.stdout.strip()整个调度器就这么点逻辑真正核心的决策还是模型在做。我们要做的就是把每个技能的说明书写得足够好用让模型的“路由决定”尽可能精准。3.5 测试与调优别着急上线先在九宫格里练一轮技能写完最忌直接梭哈上生产。我们内部有一套固定的测试方法用一组典型 query 把技能“逼到墙角”。测试用例大概分四类正常查询“帮我查下订单 ORD-20240601-0001”、缺参查询“我的货到哪了”、边界查询“查个不存在的订单”、违规请求“帮我取消订单”。模型面对这几类 query 时技能是否被正确触发、参数是否完整解析、输出是否正确引导用户补信息、越权请求是否被拒绝全流程都要过一遍。我们会专门记录“模型没有调用技能”的 case分析是 SKILL.md 触发条件写得不够全还是用户用语太口语化。实际测试中“查不到订单”这个场景最容易翻车模型看到空結果常常会脑补出一堆物流轨迹。后来在 SKILL.md 里专门加了一条“没有查询结果时如实告知用户凭据可能错误不要虚构信息”情况才好转。4. 实战中踩过的坑与排查记录4.1 模型就是不去调用 Skill症状很典型用户问“我的快递到哪了”模型没走 skill直接回答“您好您的快递正在运输中请耐心等待”连查都没查纯靠猜。一开始我们以为是 Prompt 没加“你必须依赖工具”后来才发现根子在技能触发条件写得太窄。排查思路很简单打开调试日志看模型到底有没有“看见”这个技能。很多框架都会把工具清单塞给模型如果模型压根没提到这个工具那就是描述和触发条件有问题。把 SKILL.md 的触发场景扩到用户最口语化的表达方式比如“我的手机什么时候到”“东西发出来了吗”命中率立刻上去。另外一个常被忽略的因素是技能排列顺序。同一个 Agent 挂了几个技能时模型对排在前面的技能有天然偏向。把高频技能往前排能显著减少“看着低级技能却挑高级技能”的诡异情况。4.2 参数传得驴唇不对马嘴模型虽然聪明但参数抽取这件事上经常翻车。我们遇到过模型把用户输入的“我在淘宝买的手机”直接当成order_id传进来也遇到过用户给了手机号结果模型宁可在order_id里猜一串数字也不肯用更有把握的phone参数。排查下来两个原因最常见一是schema.json里字段描述太弱模型不知道哪个才是可靠凭据二是oneOf约束表达不够明确。我们后来把 schema 的 description 写得更“霸道”一点明确标注“order_id 格式为 ORD-开头”并在 SKILL.md 里强调“优先使用 order_id如果没有 order_id优先使用 phone”。模型是文本动物你得用文本把规则钉死它才不会自由发挥。4.3 技能一执行就超时/报错技能跑通后另一大坑是生产环境执行报错。最常见的原因有三个依赖没装全、外部 API 网络不通、数据编码错乱比如中文变乱码或 JSON 解析失败。排查手段没有捷径先把技能的执行日志完整打到单独文件里再做一次“裸跑”把命令直接拿出来在服务器上执行能看到真实报错。我们遇到最多的是依赖版本冲突虚拟环境里装好了换台机器却跑不起来后来统一用固定版本号锁定 requirements.txt问题大幅减少。还有一个容易忽略的细节技能脚本的输出必须“干净”。曾经有个技能里混了一句print(开始查询...)结果模型拿到的执行结果前面多了调试文本JSON 解析直接失败整个调用白费。以后所有调试打印一律走 stderrstdout 只留正式输出。4.4 并发一上来技能就乱套单请求测得好好的并发一上各种鬼故事都来了。最典型的是共享变量串数据。前文说过技能必须无状态但代码实现时一偷懒就容易把“最近一次查询结果”存在模块级变量里两个用户同时调用后到的覆盖先到的返回数据完全错乱。解决方式就是坚决不共享所有数据用完即丢需要持久化的交给外部存储。另一个并发问题是对外部 API 频控处理不当上个限就挨个超时。我们后来在技能脚本里加上了简单的重试机制遇到 429 或 5xx 就退避重试直到超时上限。还有一点子进程化调用在高并发下需要控制并发数。我们压测时发现单机同时拉起 50 个子进程时CPU 直接跑满。后来调度器加了信号量限制最多同时跑 12 个技能进程剩下的排队等待。4.5 升级一个 Skill老用例全部挂了技能也会迭代。改一个字段名、调整一下返回结构都可能让旧测试用例失效。最头疼的是模型和技能之间没有强契约技能端改了输出模型端可能还在按旧结构解读。我们现在给每个 skill 加了版本字段并在 SKILL.md 里记录变更历史。A/B 发布时新版本的技能先跑灰度流量观察模型回答准确率确认无误再全量切过去。同时保留一个“回滚按钮”一旦线上回答问题质量下降立刻切回上一个稳定版本再排查问题。这套机制救了我们好几次。5. 一些值得沉淀的优化经验5.1 给技能加“示例调用”比任何注释都管用测试时我们发现一个规律模型对“这个技能到底该怎么用”的理解很大程度依赖 SKILL.md 里的示例。只写参数说明模型经常抽象过度补了完整示例后它的调用准确率肉眼可见地提升。示例不要只写标准情况最好覆盖一两个边界。以查询订单为例我们会写一个“没有任何识别凭据时的对话示例”引导模型主动索要信息。模型看完示例遇到类似场景时会自动照做。5.2 让技能自己“报错”别让模型瞎编模型最怕的不是技能报错而是技能没报错但返回了误导信息。我们要在脚本里把各种异常情况都转成明确的文本提示绝不静默失败。比如外部接口超时返回值里明确写“查询服务超时请稍后重试”比如参数格式不对明确写“order_id 格式应为 ORD-开头”。模型拿到这类“坦诚”的消息至少不会编造错误结论而是会告诉用户系统出了什么状况。在客服场景里“我不确定但帮你查一下”比“我确信但答案是编的”可靠一万倍。5.3 技能粒度宁拆勿揉技能粒度怎么定是团队吵得最多的问题。有人喜欢做一个“全能操作技能”一个技能里既查订单又改订单再发消息结果好了模型每次选择技能都很纠结参数也经常混用一个失误全盘皆输。我们后来的原则是“一个技能只做一件事并且做到极致”。查询归查询修改归修改通知归通知。技能之间可以编排组合但单个技能内部保持单一职责。虽然技能数量会膨胀但每个技能都很简单模型的选择准确率更高调试维护也更容易。写在最后的一点心得agent-skills 这套东西别看概念不算新真正落地时考验的全是细节。模型不懂人情世故它对你的技能体系的“信任”完全建立在文档质量和工程可靠性上。描述写得糙它就瞎搞环境隔离不到位它就拖垮全局输出不干净它就答非所问。每一处细节过了关你才会得到一个真正“能干活”的智能体。我的建议是先不要贪多求全挑一个你业务里最高频、最刚需的场景做成第一个 skill用真实用户对话把它打磨到“模型不失误、执行不超时、报错不瞎编”的程度。跑通这一个闭环后你自然会对物品的边界、参数的坑、模型的脾性有直觉后面的技能生产速度会快得多。这套工程化思维才是 agent-skills 真正值钱的地方。
返回列表