ARTICLE DETAIL

资讯详情

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

Agent技能体系实战:从工具调用到稳定落地的完整指南

Agent技能体系实战:从工具调用到稳定落地的完整指南 最近接手了一个智能体项目的复盘团队把基座模型从开源换到商业API提示词工程也迭代了好几版用户问帮我查一下上周的订单为什么还没发货系统还是经常答非所问——要么直接说我没有这个权限要么把订单号都搞错。我翻了一周的调用日志发现80%的失败发生在同一个环节模型不知道该调用哪个工具或者工具的参数压根传不对。这件事让我彻底想明白一个问题Agent能不能落地很多时候不取决于模型多聪明而是取决于你把能力暴露给模型的方式。这也是我写下这篇关于agent-skills实战总结的起因。agent-skills这个名字听起来像某个框架的插件库但它本质上描述的是一整套把Agent能力系统化的方法论把可复用的操作封装成带元数据、带参数协议、带调度策略的技能然后让模型像查API文档一样去发现、选择和调用。我在这轮项目里把这套体系从零搭了一遍走了不少弯路也沉淀了一套可以直接抄作业的代码结构。这篇文章会把设计思路、核心模块、代码实现和生产环境里的坑一次性讲清楚适合正在做Agent应用的工程师也适合想理解Agent内部协作机制的技术负责人。1. 为什么Agent项目会死在技能这一步先聊聊我看到的普遍现状。很多人搭Agent的第一版就是往系统提示词里塞一个可用工具列表长这样你有以下工具get_order_status()、update_address()、create_return()当用户需要查询订单时调用get_order_status需要修改地址时调用update_address。这种做法的本质是——把函数签名硬编码在提示词里让模型靠上下文理解来碰运气式调用。小demo完全够用一旦业务复杂起来问题就成指数级增长。1.1 把工具硬塞进提示词的三无困境第一是无边界。一个月后工具列表从5个涨到50个提示词被塞到几千token模型开始混淆相似工具。比如get_order_info和get_order_status两个函数连开发团队自己都要看注释才分得清模型更分不清。第二是无协议。每个函数期望什么参数、参数格式是什么、异常了怎么反馈全都靠自然语言在提示词里描述模型一旦传错参数整个调用链就断了。第三是无治理。工具的版本更新、灰度下线、权限控制都没有统一入口前端Agent的代码和后端API的改动永远在打架。我印象最深的一个故障运营同学要求把赠品发放的逻辑从下单环节挪到售后环节开发改了API但没同步改提示词里的工具描述。结果模型连续三天在用户问赠品什么时候到时调用下单接口再造出好几个错误订单。这就是没有技能层管理的典型后果——工具和Agent之间是裸连接的任何一个环节变了其他环节全崩。1.2 技能层到底解决什么重新定义Agent能力的基本粒度后来我换了个思路不再把工具当作散装函数暴露给模型而是把能力包装成技能。技能和工具的区别在于技能携带完整的自我描述和调用协议并且有独立的生命周期。一个技能至少包含以下结构元数据技能名称、功能摘要、适用场景、不适用场景这是给模型看的说明书参数协议输入参数的JSON Schema字段类型、必填性、取值枚举这是给模型遵守的约束执行器真正干活的业务代码可以是本地函数也可以是远程API封装策略集合超时时间、重试次数、权限范围、降级方案这么一包装Agent的运行逻辑就清晰了。模型不再需要在提示词里记住50个函数怎么写而是通过语义匹配从技能注册中心里检索出当前任务最相关的3-5个技能再按技能的参数协议生成标准调用。这个设计和人脑的记忆机制很相似。你不需要记住每个同事的工位号和全部职责只需要知道找财务能报销、找行政能领电脑具体流程由对方告诉你。技能层就是Agent的人事通讯录。2. 技能体系的四个基础模块从定义到调度我在项目里把技能体系拆成四个模块每个模块各管一段互不越界。这四个模块分别是技能定义层、技能注册中心、技能发现机制、技能调度器。2.1 技能元数据一份让模型看得懂的技能说明书技能元数据是整个体系的基石。我早期犯过一个错误把技能描述写得特别文学比如当用户流露出不满情绪时调用安抚话术技能。结果模型对着用户说我理解您的沮丧但在真正需要调用退换货技能时犹豫不决。后来我把技能描述规范化成四段式功能概述、触发条件、不触发条件、参数说明。拿订单查询技能举例它的元数据大概长这样name: get_order_status_v2description: 根据订单号查询订单当前状态包括待付款、待发货、运输中、已签收、售后中。适用于用户询问我的订单到哪了发货了吗什么时候到等场景。不适用于查询历史已完成订单的详情。parameters: order_idstring必填订单号格式为14位数字示例20250101123456customer_phonestring选填用户手机号后四位用于二次校验这样一份元数据模型不需要猜测照着协议调用就行。描述里不能有歧义词不能有让模型自由发挥的空间。2.2 注册、发现、调度技能层的工作流技能的生命周期可以概括为四个阶段注册新技能上线时向注册中心提交元数据和执行器注册中心分配技能ID并验证参数Schema的合法性发现用户请求进来后Agent把请求文本通过语义模型向量化在注册中心的技能索引里做相似度检索召回候选技能编排调度器结合上下文和召回结果让LLM做最终选择决定调用哪个技能、传什么参数也可以是规则引擎直接绑定技能执行调度器调用执行器把结果返回给LLMLLM生成面向用户的最终回复这里的核心变化是——模型不再记住所有工具而是每次动态检索相关技能。技能多了也不怕索引在向量数据库里检索是亚秒级的。我目前管理着120多个技能提示词里的工具列表长度从3000token降到了200token模型准确率反而提升了。2.3 技能不是工具列表生命周期管理传统工具列表是静态的技能体系则要求每个技能有版本、状态、负责人、依赖关系。我在注册中心里给每条技能记录维护了以下状态机草稿、审核中、已上线、灰度中、已下线、已废弃。灰度是特别重要的一环。我试过直接上线一个新技能结果它和旧技能在边界场景上打架导致一小部分用户被错误引导。后来所有技能改动都走灰度先让5%的流量用新技能对比调用成功率和用户满意度没问题再逐步放大。这个机制救了我好几次尤其在上线涉及财务计算的技能时。3. 从零搭建技能注册中心一个可落地的实现方案理论讲完直接上代码。我用的技术栈是Python FastAPI Redis 向量数据库但注册中心的核心逻辑不绑定具体框架你完全可以用Node.js、Go重写。核心就三件事定义数据模型、实现注册与查询接口、接入技能发现。3.1 先定接口执行器与元数据分离技能执行器我抽象成一个统一接口业务代码只需要实现它。这样做的好处是注册中心完全不关心技能内部怎么实现它只负责元数据和调度。from typing import Any, Dict from pydantic import BaseModel, Field class SkillRequest(BaseModel): skill_name: str parameters: Dict[str, Any] class SkillResponse(BaseModel): success: bool data: Any None error: str | None None latency_ms: int 0 class BaseSkill(ABC): name: str description: str parameters_schema: Dict[str, Any] abstractmethod async def execute(self, params: Dict[str, Any], context: Dict[str, Any]) - SkillResponse: ...以订单查询技能为例实现类大概长这样class GetOrderStatusSkill(BaseSkill): name get_order_status_v2 description ( 根据订单号查询订单当前状态包括待付款、待发货、运输中、已签收、售后中。 适用于用户询问我的订单到哪了发货了吗什么时候到等场景。 不适用于查询历史已完成订单的详情。 ) parameters_schema { type: object, properties: { order_id: { type: string, description: 订单号14位数字示例20250101123456, pattern: ^\\d{14}$ }, customer_phone: { type: string, description: 用户手机号后四位用于二次校验, pattern: ^\\d{4}$ } }, required: [order_id] } async def execute(self, params, context): order_id params[order_id] # 实际业务逻辑调订单服务API查数据库或者走缓存 result await fetch_order_status(order_id) return SkillResponse(successTrue, dataresult)这里有一个值得注意的设计细节参数Schema必须用JSON Schema标准而不是自由文本。因为LLM对JSON Schema的遵循能力远高于对自然语言规则的遵循能力。你写order_id是必填的14位数字模型可能偶尔出错但你在Schema里用pattern字段声明格式模型按格式生成参数的准确率会高很多。3.2 注册表实现把技能变成可检索的资产技能注册表的核心是一个存储技能元数据的索引。我用Redis做热数据缓存用向量数据库做语义索引元数据的唯一来源存在PostgreSQL里。class SkillRegistry: def __init__(self, redis_client, vector_store, pg_pool): self.redis redis_client self.vector_store vector_store self.pg pg_pool async def register_skill(self, skill: BaseSkill, owner: str): # 1. 校验技能元数据完整性 validate_metadata(skill.name, skill.description, skill.parameters_schema) # 2. 存入PostgreSQL生成技能ID和版本号 skill_id await self.pg.execute( INSERT INTO skills (name, description, parameters_schema, owner, status) VALUES ($1, $2, $3, $4, draft) RETURNING id, skill.name, skill.description, json.dumps(skill.parameters_schema), owner ) # 3. 生成技能描述的向量表示写入向量库 embedding await embed_text(f{skill.name}: {skill.description}) await self.vector_store.upsert( collectionagent_skills, idskill_id, vectorembedding, payload{name: skill.name, status: draft} ) # 4. 写入Redis热缓存 await self.redis.hset(fskill:{skill_id}, mapping{ name: skill.name, description: skill.description, params_schema: json.dumps(skill.parameters_schema), status: draft }) return skill_id async def search_skills(self, query: str, top_k: int 5) - List[str]: # 语义检索把用户请求转为向量在技能索引中找最相似的技能 query_embedding await embed_text(query) hits await self.vector_store.search( collectionagent_skills, vectorquery_embedding, top_ktop_k, filter{status: {$in: [active, gray]}} ) return [hit.id for hit in hits] async def get_skill_metadata(self, skill_id: str) - Dict: # 先查Redismiss再查PG写回缓存 cached await self.redis.hgetall(fskill:{skill_id}) if cached: return cached row await self.pg.fetchrow(SELECT * FROM skills WHERE id $1, skill_id) if row: await self._refresh_cache(row) return dict(row) if row else None这套注册表实现下来我最大的感受是技能入库的那一刻就应该把描述是否清晰当作代码评审一样对待。一个模糊的技能描述入库时毫无感觉等到线上检索召回错技能排查成本是当时的十倍。3.3 从docstring生成技能描述减少人的主观性技能描述写多了以后我发现一个规律开发者的描述水平不稳定有的人写得极清楚有的人写出来全是套话。为了保证入库质量我写了一个从docstring自动生成描述的小工具。def skill_description_from_docstring(func) - str: doc inspect.getdoc(func) if not doc: raise ValueError(f{func.__name__} 缺少docstring无法生成技能描述) sections {} current overview for line in doc.splitlines(): line line.strip() if line.startswith(场景:): current scenario sections.setdefault(current, []) sections[current].append(line.replace(场景:, ).strip()) elif line.startswith(参数:): current params sections.setdefault(current, []) elif line.startswith(返回:): current response sections.setdefault(current, []) elif line: sections.setdefault(current, []).append(line) description f{sections.get(overview, [])[0]}。 if sections.get(scenario): description 适用于 、.join(sections[scenario]) 等场景。 return description开发者在写业务函数时只需规范docstring格式入库时自动生成技能描述再人工审核一遍即可。这样既减少了工作量也保证了描述风格的统一。4. 技能编排的运行时逻辑选择、组合与降级有了技能注册中心下一步是解决运行时的问题用户请求进来技能这么多谁来选、怎么选、选错怎么办。4.1 单技能调度与多技能组合最简单的情况是单技能调度语义检索召回一个最相关的技能LLM生成参数并调用然后返回结果。这种情况适合用户意图非常明确的场景比如查订单状态。但真实业务中大量需求是多技能组合。比如用户说我上周买的连衣裙要退货但我的收货地址也改了帮我一起处理。这里涉及两个技能查订单找到订单号和发起退货执行退货流程可能还涉及更新收货地址。如果只召回一个技能另一个需求就被忽略了。我的做法是引入技能链机制调度器先做一个意图拆解把用户请求拆成多个子任务每个子任务匹配一个技能技能之间按依赖关系串联。class SkillChainPlanner: def __init__(self, registry: SkillRegistry): self.registry registry async def plan(self, user_query: str, context: Dict) - List[SkillCall]: # 第一步用LLM做意图拆解输出结构化子任务列表 subtasks await llm_extract_subtasks( user_query, available_schemasself._get_compact_schemas() ) # 第二步为每个子任务检索技能 skill_calls [] for subtask in subtasks: skill_id await self.registry.search_skill_for_subtask(subtask) if not skill_id: # 找不到技能时记录miss走降级逻辑 await record_skill_miss(subtask) continue skill_calls.append(SkillCall(skill_idskill_id, paramssubtask.args)) # 第三步检查技能依赖做拓扑排序 return topological_sort(skill_calls)这里有个关键的眼力活儿并不是所有子任务都需要技能。有些子任务LLM自己就能回答比如用户问退货需要几天这是常识或政策知识不需要调技能调了反而慢。我加了一个规则子任务先过一个是否需要外部数据或操作的分类器只有确认需要才进入技能检索流程。这一步把平均调用次数从3.2次降到1.8次总延迟降了40%。4.2 技能链的失败处理与降级技能链一旦某个环节失败整个流程怎么走我踩过一个大坑退货链路里查订单成功、发起退货失败用户的订单状态没有变化但系统给用户回复退货申请已提交。这是灾难性的。后来我确定了一套失败处理规范前置依赖失败比如订单号没查到后续所有依赖订单号的技能直接跳过回复用户暂时没查到您的订单请核对订单号技能本身失败区分可重试和不可重试错误。超时、网络抖动属于可重试设置2次重试间隔递增1s、3s参数校验失败、业务规则不允许属于不可重试直接返回错误原因给LLM部分成功更新地址成功、发起退货失败必须如实告知用户地址已更新退货申请处理中遇到了问题请稍后重试不能让LLM脑补成功降级方案我统一走语义兜底当技能调用多次失败LLM直接基于已有上下文给出安全回复绝不编造技能执行结果。4.3 可观测性技能调用的全链路监控技能体系上线后监控是必须同步做的。我维护了一张技能调用分析表每个H2都要回答三个问题技能被召回了多少次、被实际调用了多少次、调用成功了多少次。召回和调用之间的差值就是召回错但最终没用的技能这个指标可以反向优化技能描述。我遇到过有技能被召回了2000次调用量却是0最后发现它的描述里有订单两个字导致所有订单相关请求都把它召回但LLM最终都选了别的技能。改描述后召回量降了一半整体精度提升明显。我的活跃技能监控表格大致长这样指标定义我关注的阈值召回次数语义检索命中该技能的次数低于100次/天需检查是否集成错误调用次数实际被LLM选中并执行的次数与召回次数的比值应超过30%成功率技能执行无异常的比例低于95%触发告警平均耗时技能执行平均延迟超过2秒需考虑异步化参数错误率参数不符合Schema导致校验失败的比例高于5%需检查Schema是否过严5. 我在生产环境里踩过的几个技能设计坑这一节是全文最有血泪感的部分。技能体系从理论搭建到稳定运行我至少踩了五个坑每一个都浪费了少则几天多则两周的时间。全部列出来给后面的朋友当路标。5.1 粒度失控一个技能变成小应用我第一个技能上线时塞了太多职责。那是处理售后全流程技能——能查订单、审核退款资格、生成退货标签、通知仓库甚至处理用户情绪安抚话术。结果就是LLM面对一个长着售后大名的技能根本不知道该从哪个步骤开始用参数列表也极其混乱。后来我按最小可理解动作来切分技能粒度。判断标准很简单一个技能在业务描述里能不能用一句话说完。查询订单状态可以处理售后全流程不行。切完之后技能数量从30个涨到90个但每个技能的调用成功率反而高了不少。5.2 描述里写作文让模型不知道怎么选有个技能是推荐相似商品开发同学在描述里写了三大段推荐算法的原理甚至包含协同过滤、向量召回这些技术词。结果模型开始喜欢这个技能——因为它看起来无所不能在任何购物相关请求里都想调用它。技能描述是写给LLM看的不是写给领导汇报的。最好控制在50字以内关键是讲清楚这个技能是干什么的、什么时候用、什么时候不用。我后来给团队定了一条硬规定描述超过80字必须打回重写。5.3 参数Schema的两难太松和太紧都不行参数Schema太松模型会传错太紧模型会频繁校验失败。最典型的是订单号字段我一开始严格规定死14位数字结果线上有老订单是12位老编码导致一堆老用户查询失败。调整思路是分层校验必填字段必须严格格式校验改为宽松识别确认回显。模型传order_id123456不足14位不再直接报错而是补0后查询如果查不到再告知用户。这样既保持了执行安全又提升了鲁棒性。5.4 从不声明不适用场景大部分开发者写技能描述只写适用场景不写不适用。我上面提过退货技能描述里加入了不适用于查询历史已完成订单详情这种负向描述的价值远超正向描述。原理很简单LLM的检索和选择本质上是概率匹配正负边界越清晰模型在边界情况下的表现越稳定。就像你给新同事指路光说财务在那栋楼不够还得说不是旁边那栋行政楼。5.5 技能冲突仲裁两个技能都能处理同一个请求最后这个坑特别隐蔽。当技能库超过100个必然出现语义重叠。比如修改收货地址和变更偏好设置都能处理地址变更。用户说把地址改了召回结果里两个技能都在LLM随机选了一个有时候行为不一致。我的解决方案是加一层显式的冲突仲裁在技能元数据里增加优先级字段和互斥声明。当召回结果里有互斥技能时调度器按优先级强制排序而不是把选择权完全交给LLM。这类规则不用多覆盖最常出问题的几个业务点就够了。6. 技能体系的下一步从人工设计走向半自动沉淀技能体系稳定跑了两个月之后我开始思考一个更省力的问题能不能让系统自己发现需要新技能现在我维护技能的流程还是偏人工运营提需求、产品定义、开发实现、审核入库。这个链路太长等技能上线用户的痛点可能已经换了。我目前在做的一个试验是把技能缺失变成可观测信号——当用户请求无法匹配到合适的技能、且LLM长期用抱歉我暂时无法处理来兜底时把这些对话流量聚合起来做成潜在技能需求报告推给产品团队评估。另外一个方向是把一些低频但逻辑固定的技能链做成模板技能。比如退款处理流程其实是固定的几个步骤我可以把它固化成一个退款处理v3技能内部自动调三个子技能对外只暴露一个入口。这样LLM的外部选择空间变小内部执行的稳定性反而变高了。说回个人体会——技能体系建设最耗时间的不是写代码而是统一语言让产品、开发、LLM三方对一个技能到底是什么达成默契。产品想的是业务动作开发想的是函数接口LLM需要的是带边界的语义描述。这三者能对得上技能体系的杠杆效应才会真正显现。我当时把这个项目起名叫agent-skills就是想说清楚Agent的能力不是靠模型单点突破而是靠一层组织良好的技能资产来放大。谁先把这层资产管好谁就能让Agent从玩具走向生产力。
返回列表