ARTICLE DETAIL

资讯详情

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

从零构建Agent技能体系:定义、注册、调度与踩坑实录

从零构建Agent技能体系:定义、注册、调度与踩坑实录 写Agent技能体系之前先说一个我踩过的大坑。之前做智能客服Agent模型对话能力没问题但一到帮我查一下订单就废了——要么不知道调哪个接口要么把参数传得乱七八糟。折腾了一个多月最后发现核心症结不在模型而在技能这件事上压根没设计好。这也就是我今天想聊的agent-skills如何给AI智能体建立一套清晰、可复用、能让模型真正用起来的技能体系。Agent圈子现在都在说大模型是大脑技能是手脚但真正动手做的时候会发现手脚比大脑难搞得多。技能的定义、注册、调度、容错每一步都有坑。这篇文章我会把我从零搭建技能体系的全过程拆开来讲包括设计思路、代码实现、调试技巧和踩坑记录适合已经在做Agent开发、但被技能管理折磨得头疼的人也适合刚入行想系统了解Agent技能怎么落地的朋友。1. Agent技能体系的整体设计思路1.1 为什么Agent必须技能化很多人一开始做Agent直接就是把API文档塞给模型让它自己调。我最初也这么干过结果惨不忍睹。一个订单查询接口文档里写了十几个参数模型要么漏传必填项要么把门店ID当成用户ID传进去。问题的本质在于模型不是软件工程师它是个阅读理解能力很强的文科生——你给它一篇长文档它能总结个大概但你指望它精准执行每一步参数校验基本不现实。技能化的思路就是把让模型理解一篇复杂文档转化为让模型从一串清晰选项里做选择。一个技能对应一个明确的意图和一套严谨的参数结构模型需要做的只是选对技能、填对参数而不是读懂整个系统的业务逻辑。这种模式经过实践验证在准确率上的提升是数量级的。还有个更深层的原因业务是持续变化的。今天你的Agent会查订单明天要加一个退款技能后天要把两个技能串起来完成退货退款全流程。没有技能化的话每一次改动都是重写Prompt改一次崩三次。有了技能体系新增能力就是在技能列表里加一项完全不影响已有技能的稳定性。技能化不只是为了模型好用更是为了业务的可演进性。1.2 技能与工具、插件的边界要划清楚这几个概念容易混我先用大白话理清工具Tool最底层的能力原子比如调用HTTP接口读写文件执行Python代码。它不懂业务只负责执行。技能Skill面向业务场景封装的能力单元比如查询订单状态计算运费生成退款单。一个技能内部可能组合多个工具。插件Plugin更上层的集成形态把一组相关技能打包发布方便复用和分发。比如电商插件包含查订单、退换货、开发票好几个技能。我用一个生活类比把这些关系说明白工具是螺丝刀技能是换电灯泡这个动作插件是一整套工具箱。螺丝刀谁都会用但换电灯泡需要知道先断电、再拆灯罩、最后换灯芯——这一步一步的流程约束正是技能要做的事。所以设计技能时我给自己定了条红线技能层绝不直接写底层实现细节只服务业务意图。这样模型侧的理解成本最低业务侧也便于统计和运营。1.3 技能体系的分层架构我最终落地的技能体系分了三层层级职责示例技能注册中心维护技能清单、描述、参数Schema技能名query_order描述按订单号查询订单状态技能执行引擎解析模型输出、校验参数、调用实现函数、统一返回参数校验失败时自动补充缺失值技能实现层真实业务逻辑调用订单服务、组装结果、格式化输出这三层各司其职带来的直接好处是注册中心决定模型能看到什么执行引擎决定模型调用得顺不顺实现层决定业务准不准。每一层都能独立测试、独立升级。我在项目里最爽的一次体验是只改了注册中心的描述文案把查询订单改为查询订单支持模糊匹配订单号模型立刻学会了用新的查询方式——完全不用动任何业务代码。2. 技能定义与拆解方法2.1 技能拆解的三个核心原则把业务能力拆成技能比想象中难。拆粗了一个技能里塞了几十个参数模型照样懵拆细了技能数量爆炸模型选择困难。我总结了三个原则可以说是踩了无数坑换来的原则一按用户意图拆不按系统功能拆。比如系统里订单服务有查询、修改、取消三个接口从系统角度是一个模块但从用户角度是三个完全不同的意图。用户说我不想要了时模型应该去调取消订单而不是先想查一下订单再改。所以技能边界一定顺着用户意图画。原则二技能粒度以一次完整的用户请求为准。一个技能要能独立完成用户的一句话诉求。比如帮我查一下昨天买的那个包裹到哪了——这个请求虽然涉及查订单、查物流两个动作但我封装了一个query_delivery_status技能内部把两步串好了。模型只需要传订单号不用理解中间过程。原则三参数宁少勿多。我见过最离谱的技能Schema有二十多个参数结果模型输出时直接放弃治疗只填了前三个。每个技能的参数尽量控制在五个以内超过五个就要反思是不是拆得不够细。必填参数最好只有一两个其余全部可选并给出默认值。2.2 技能名称与描述的写作方法技能名称和描述是模型做选择的主要依据这部分写得烂后面全白搭。我总结了一个三段式描述法第一段这个技能做什么一句话说清。第二段在什么场景下使用给出典型用户说法。第三段使用限制和注意事项。用查询订单技能举例query_order按订单号或用户ID查询订单状态。 适用场景用户询问订单进度、物流节点、签收情况比如我的订单到哪了。 注意仅用于查询不处理退款、修改地址等变更操作。这个写法看起来简单但效果拔群。模型本质上是在做文本匹配你的描述越贴近用户真实说法匹配准确率越高。我做过对比实验把描述从订单状态查询接口改成上面这种写法后技能选择准确率从78%提到了93%。2.3 参数Schema设计的细节规范参数Schema是技能里最容易出问题的部分。我见过太多人直接用OpenAPI格式把后端接口参数原封不动搬过来结果模型根本填不对。这里有三条实践经验第一参数名要带业务语义。不要叫param1、field_a这种要叫order_id、user_name。模型对语义化命名天生友好参数名一眼看懂输出的正确率就高。第二在描述里写清楚参数格式和示例值。比如order_id的描述写订单位如JD20240115001支持完整的订单编号不支持模糊查询比只写订单编号强十倍。模型会把描述里的示例值当成参照模板输出格式稳定很多。第三枚举值要给足别名。如果订单状态有pending、paid、shipped这些取值最好在枚举里标注每个值的中文含义和常见口语说法比如pending配待付款/未支付/还没付钱。模型在做选择时遇到语义模糊的情况这个细节能救命。3. 技能注册与调用的核心实现3.1 技能注册中心的构建注册中心听起来玄乎本质就是一个结构化清单我建议直接用JSON文件维护再配合一份Python数据类做校验。上面这种配置化方式的好处是新增技能不需要改代码加一坨JSON再实现对应的处理函数就完了。技能注册中心的数据结构我建议这样设计dataclass class SkillDefinition: name: str # 技能名唯一标识 description: str # 技能描述喂给模型的关键信息 parameters: dict # 参数SchemaJSON Schema格式 handler: str # 执行函数标识注册到执行引擎 enabled: bool True # 是否对模型可见 version: str 1.0.0 # 技能版本支持灰度有一个小细节值得注意enabled字段。我在实际运营中发现有时候某个技能出问题比如线上接口抖动你不想让模型再选它但又不想删配置。这时候把enabled置为False技能就从模型可见列表中摘掉了等故障恢复再打开。这比改代码重新发布靠谱多了。3.2 执行引擎的调度逻辑技能执行引擎是整个体系中技术含量最高的一块它要完成四件事接收模型输出、解析技能调用、校验参数、调度执行函数。核心流程我按下面这四步来设计接收模型返回的结构化指令。现在主流做法是Function Calling模型会返回一个结构化的JSON包含要调用的技能名和参数后面代码示例会详细演示。技能名映射。通过注册中心把模型返回的技能名映射到实际的执行函数注意这里要做一层防护防止模型输出不存在的技能名。参数校验与补全。按Schema逐项做类型和格式校验。校验失败时不要直接报错返回而是尝试补全有默认值的填默认值能类型强转的做转型实在不行才进入下一步。执行与响应组装。调用真实的业务函数拿到结果后要加工成模型友好的格式再返回纯代码层面的返回结果模型未必能转化为用户能听懂的答案。这里有个关键点执行引擎和处理函数之间一定要解耦。我把处理函数全部注册到一个HandlerRegistry里技能定义只保存一个字符串标识执行时动态查找。这样技能实现可以独立测试而且能热替换。3.3 一个能跑通的代码骨架下面这套代码我用的是OpenAI的Function Calling规范配合FastAPI做服务封装是目前最常见的组合。核心步骤我都标了注释可以直接照搬改造。import json from typing import Any, Callable from pydantic import BaseModel, Field class SkillRegistry: 技能注册中心与执行引擎的整合实现 def __init__(self): self._skills: dict[str, dict] {} self._handlers: dict[str, Callable] {} def register_skill(self, name: str, description: str, parameters: dict, version: str 1.0.0): 注册技能定义使其对模型可见 self._skills[name] { type: function, function: { name: name, description: description, parameters: parameters, }, version: version, } def register_handler(self, name: str, handler: Callable): 注册技能对应的执行函数 self._handlers[name] handler def get_openai_tools(self) - list[dict]: 供模型端使用的工具列表 return list(self._skills.values()) def execute(self, name: str, arguments: str) - Any: 解析并执行技能调用 if name not in self._handlers: raise ValueError(f技能 [{name}] 未注册执行函数) args json.loads(arguments) if isinstance(arguments, str) else arguments return self._handlers[name](**args) # 实际业务处理函数 def query_order(order_id: str , user_id: str ) - dict: 查询订单的落地实现这里放真实业务逻辑 if not order_id and not user_id: return {error: order_id 和 user_id 至少提供一个} # 模拟查库结果 return {order_id: order_id, status: 已发货, express_no: SF1234567890} registry SkillRegistry() # 技能1查询订单 registry.register_skill( namequery_order, description按订单号或用户ID查询订单状态。适用于询问订单进度、物流节点等场景。, parameters{ type: object, properties: { order_id: { type: string, description: 完整的订单编号如JD20240115001, }, user_id: { type: string, description: 用户ID订单号不存在时使用, }, }, required: [], }, ) registry.register_handler(query_order, query_order) # 模型侧的调用示意真实场景用OpenAI SDK发出请求即可 tools registry.get_openai_tools() # response openai_client.chat.completions.create( # modelgpt-4o, # messagesmessages, # toolstools, # )注意required字段我建议留空。模型特别怕必填项一旦标记为必填它在拿不准时容易编造一个看似合理的值。留空之后配合后端校验逻辑做兜底效果通常更好。4. 实操从零构建一个可用技能库4.1 项目结构规划与依赖准备动手之前先规划项目结构我这边推荐一个清晰的目录布局agent-skills-demo/ ├── skills/ │ ├── __init__.py │ ├── registry.py # 注册中心与执行引擎 │ ├── order_skills.py # 订单类技能实现 │ └── user_skills.py # 用户类技能实现 ├── config/ │ └── skills.json # 技能清单配置 ├── server.py # FastAPI服务入口 ├── requirements.txt # 依赖清单 └── tests/ └── test_skills.py # 技能单元测试依赖方面我用的是Python 3.11 pydantic2.0fastapiopenai这些库都是目前生态最成熟的。需要注意版本兼容性pydantic v2和v1在数据类API上差别较大建议统一用v2。关于配置和代码的关系我一开始把技能定义全写在代码里结果每次调整描述都要重新发布。后来改成从skills.json加载才实现了配置热更新。但建议技能执行函数还是放在代码里因为业务逻辑天生需要测试和版本管理不适合放配置。4.2 技能实现的关键代码步骤接下来是完整搭建流程每一步我都写了实际代码和解释。以订单类技能集为例我提供一个包含两个技能的最小实现你可以照着扩展。第一步先定义技能处理函数的统一返回结构这一步容易忽略但对模型理解执行结果非常关键def success(data: Any) - dict: return {code: 0, msg: ok, data: data} def failure(error_msg: str) - dict: return {code: -1, msg: error_msg, data: None}统一返回结构的好处是后续接入监控体系时只需要检查code字段就能快速判断技能执行是否成功。第二步实现订单相关技能。除了查询订单我加一个取消订单技能用来演示技能之间的差异# skills/order_skills.py def cancel_order(order_id: str, reason: str 用户主动取消) - dict: 取消订单技能。注意校验订单状态是否允许取消。 # 模拟真实业务先查订单状态已发货的不能取消 order query_order(order_idorder_id) if order.get(status) 已发货: return failure(订单已发货无法取消请走售后流程) return success({order_id: order_id, status: 已取消, reason: reason})第三步注册这两个技能并把执行函数挂到注册中心。这一步我建议在独立的模块里完成保持入口干净# skills/registry.py from skills.order_skills import query_order, cancel_order def build_default_registry() - SkillRegistry: reg SkillRegistry() reg.register_skill( namequery_order, description按订单号查询订单状态。用户问到哪了发货没时使用。, parameters{...}, # 同上文 ) reg.register_handler(query_order, query_order) reg.register_skill( namecancel_order, description取消未发货的订单。用户说不想要了取消订单时使用。, parameters{ type: object, properties: { order_id: {type: string, description: 订单编号}, reason: {type: string, description: 取消原因可选}, }, required: [], }, ) reg.register_handler(cancel_order, cancel_order) return reg第四步封装FastAPI接口让技能库可以通过HTTP调用这一层主要为上层服务接入# server.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from skills.registry import build_default_registry app FastAPI() registry build_default_registry() class SkillRequest(BaseModel): name: str arguments: dict app.post(/execute) async def execute_skill(req: SkillRequest): try: result registry.execute(req.name, req.arguments) except ValueError as e: raise HTTPException(status_code404, detailstr(e)) return result app.get(/skills) async def list_skills(): return registry.get_openai_tools()到这一步你已经有了一个可以通过HTTP调用、可以对模型暴露技能列表的完整技能库骨架。还剩一层很关键但容易被忽略真实模型侧的调用逻辑见下一节。4.3 模型侧调用的完整流程技能库建好了还要让模型真正用起来。核心逻辑是把用户会话历史连同技能清单发给模型模型判断是否需要调用技能需要的话返回结构化指令你的服务再执行并回填结果。这里给一个完整的调用代码import openai def run_agent(user_message: str) - str: client openai.Client(api_keyyour_api_key) messages [{role: user, content: user_message}] # 第一轮告诉模型有哪些技能可用 resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolsregistry.get_openai_tools(), ) msg resp.choices[0].message # 如果模型决定调用技能 if msg.tool_calls: for tool_call in msg.tool_calls: skill_name tool_call.function.name skill_args tool_call.function.arguments result registry.execute(skill_name, skill_args) # 把执行结果回传给模型让模型组织最终回答 messages.append(msg) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) final_resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolsregistry.get_openai_tools(), ) return final_resp.choices[0].message.content return msg.content or 抱歉我没有找到相关能力。这段代码我已经在生产环境跑过稳定性不错。有几个细节要强调tool_call_id不能错它是模型关联工具调用结果的关键字段我曾因为漏填这个字段导致对话上下文错乱。执行结果要给模型做转换比如把查到的订单列表转成简洁的中文摘要再返回比直接把JSON丢给模型效果好模型省去了自己理解数据的开销。一轮执行完毕不代表流程结束如果要实现查订单后再计算运费这类多技能串联需要循环处理直到模型不再返回tool_calls。4.4 数字链路验证与测试方法技能库建完必须测试不能靠肉眼判断。我在这块吃过亏有一次改了参数结构没跑回归上线后模型报错率飙升。现在我的做法是写一套针对技能注册和执行的单元测试覆盖核心链路# tests/test_skills.py def test_query_order_success(): reg build_default_registry() result reg.execute(query_order, {order_id: JD20240115001}) assert result[code] 0 assert result[data][order_id] JD20240115001 def test_query_order_missing_params(): reg build_default_registry() result reg.execute(query_order, {}) assert result[code] -1 # 校验逻辑应该拦下空参数 def test_cancel_shipped_order(): reg build_default_registry() result reg.execute(cancel_order, {order_id: JD20240115002}) assert result[msg] 订单已发货无法取消测试框架用pytest跑一遍确保所有技能的正确性。另外五级测试法是我的习惯第一级单测验证逻辑第二级Mock模型输出验证调用链路第三级用真实模型做回归第四级灰度放量第五级全量上线。每一步都能拦住一类问题。5. 常见问题与排查技巧实录5.1 模型总选错技能怎么办这是群里被问得最多的问题明明定义了查天气和查空气质量两个技能用户问今天适合跑步吗模型愣是调了查天气。排查思路按顺序走第一步检查技能描述的适用场景是否覆盖了这类问法。描述里只有查天气没有跑步出门运动这些词模型匹配不到是正常的。把用户高频问法直接写进描述是成本最低的优化。第二步看技能数量是否太多。当技能超过十五个后模型的选择准确率明显下降这是上下文注意力分散导致的。我遇到这种情况会把相关技能合并比如物流跟踪并到订单查询里。第三步输出日志里查模型原始返回。如果模型返回了技能名但是理解偏差说明描述歧义大需要把边界说死比如明确本技能只处理实时天气不包含空气质量指数和穿衣建议。一定不要上来就改代码逻辑先改描述重新跑一轮测试大部分问题都出在描述上。5.2 参数校验失败的类型化排查模型返回的参数偶尔不合法这是Function Calling架构下的常态。我给参数校验分了三个层级排查起来很高效层级检查项常见问题类型层参数是否为预期类型整数传成字符串、数组传成逗号分隔文本格式层是否符合业务格式订单号位数不对、日期格式不标准语义层是否在合法取值范围内状态值拼写错、枚举超出范围排查时先看日志里arguments原始值长什么样大部分情况一眼就能定位。模型把order_id传成123这种明显少一位的问题出在描述里没写清楚格式要求。把订单编号为17位数字字母组合如JD20240115001写进参数描述类似问题能少八成。5.3 技能版本管理与灰度发布经验技能不是写完就一成不变的。我做了一个轻量版本管理方案每个技能定义带version字段注册中心对同一技能名保留最近三个版本。灰度时特意保留新旧两版线上按用户比例分流。比如新版的查询订单技能解析规则更严格我先让5%流量走新版观察一两天模型报错率没异常再逐步放量。实现逻辑其实不复杂注册中心在推送技能列表时根据请求里的用户标识选择返回prod或beta版本。技能执行函数入口加一层路由根据版本号分发到不同的实现函数。灰度期间务必要盯两个指标技能调用成功率、模型侧拒绝率。模型侧拒绝率代表模型决定不调用技能直接回复如果这个值异常升高说明新版技能描述让模型困惑应该立即回滚。5.4 多技能串联时上下文污染问题最隐蔽的一类问题出现在连续调用多个技能时。比如用户说帮我查订单20240115001的物流顺便把收货地址改了模型先查了订单拿到了订单数据包括一个address字段然后调用改地址技能时把这个字段原封不动传了进去。这是典型的上下文污染前一技能的输出污染了后一技能的参数输入。我的解法有两个层次。第一层在技能描述里明确标注禁止参数复用本技能只接受用户明确要求修改的新地址严禁使用订单查询结果中的地址字段。第二层在执行引擎加一层参数清洗对某些敏感参数只保留本轮用户输入中出现的值不接受上一轮技能返回的数据。这属于带业务性质的过滤规则对订单类、支付类技能尤其重要。6. 我的实操经验与后续扩展建议先说一个让我印象深刻的转折点。技能体系刚搭完时我特别兴奋一口气注册了二十多个技能觉得业务覆盖已经很完整了。结果模型在技能选择上频繁出错准确率还不如以前Prompt硬编码的时候。后来我下狠心做了减法把技能缩到8个核心技能配合精细的场景描述准确率才重新上来。技能体系的爆发力不在于数量多而在于每个技能都被定义得足够清晰。另外一个小技巧是务必给每个技能接上日志和统计。我在注册中心做了埋点每次模型选择技能、执行技能、成功率、耗时都记录下来。之前排查一个用户问发票流程但Agent答非所问的线上问题查了记录才发现模型把发票查询匹配到了订单查询技能上一句话描述差异就修复了。没有日志这种问题排查起来就像大海捞针。如果你准备把这个体系往生产环境推有几个扩展点可以优先考虑接入更丰富的工具类型比如文件读写、数据库查询、网页抓取技能内部组合工具的灵活性会大幅提升Agent能处理的任务类型会宽很多。加入技能自动编排当前每次调用还是一个技能对应一个动作真正复杂业务需要多个技能按序组合。可以尝试引入简单的流程定义引擎把技能按DAG方式编排让Agent按路径执行而不是自己发散。做技能效果的AB实验同一技能定义不同版本的描述在真实流量中对比选择准确率和任务成功率用数据驱动Desc迭代。最后分享一个我在项目复盘时反复确认的判断Agent能力的天花板很大程度由技能体系的质量决定。模型本身的能力在快速提升但技能层的工程化水平——定义是否精准、执行是否稳定、迭代是否敏捷——才是决定一个Agent能否从Demo走向生产的关键。这个过程没有捷径就是在一次次踩坑、改描述、加校验、看日志的循环里把体系打磨得越来越顺手。希望这篇记录能让你少走一些弯路。
返回列表