ARTICLE DETAIL

资讯详情

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

Agent技能体系实战:从零搭建LLM应用的可复用技能层

Agent技能体系实战:从零搭建LLM应用的可复用技能层 做LLM应用开发的朋友应该都撞上过同一堵墙模型聊天能力满分但真正让它“干点活”——查个日程、调个接口、发条消息、整理份文档——它就开始花式摆烂。不是模型智商不够而是你漏了一层东西技能层。agent-skills这个词最近在圈子里被反复提起我一开始以为又是什么新框架在炒作真正在项目里把它落地之后才反应过来它根本不玄乎。它指的是一套完整的方法论和工程实现把模型的能力输出包装成可注册、可编排、可执行、可复用的原子技能单元。今天这篇文章我不打算讲任何纸上谈兵的概念。就把我自己在项目里从零搭建一套agent技能体系的全过程——设计思路、数据结构、路由策略、执行引擎、踩坑实录——原原本本端出来。无论你是在做对话机器人、智能助手还是想把agent接入内部业务系统这篇东西应该都能帮你省下几个月的摸索时间。1. 整体设计与思路拆解为什么是“技能”而不是“工具”1.1 技能与工具的本质区别很多人一上来就搞混两个概念工具和技能。我早期也犯过这个错总觉得我在写的是工具层后来重构时才意识到工具和技能是两个维度完全不同的东西。工具是单一的、无状态的、缺乏上下文的。比如一个send_email函数它就是个工具——你给我参数我给你执行。但技能是什么技能是“在什么样的场景下、基于什么业务逻辑、以什么顺序调用哪些工具、拿到结果后如何处理”这一整套行为。打一个直白的比方扳手和螺丝刀是工具但“把柜子的合页拧紧”是技能。技能需要你判断该用哪个工具、用多大力、拧几圈、拧完之后怎么验证。模型也一样你给一个agent十个工具函数它未必知道什么时候该用哪个更不知道用完怎么组织结果。但如果你给它十个技能定义它就知道“凡是用户表达了这个意图就调用这个技能按这种格式组织输出”。这也是agent-skills背后最重要的设计哲学把决策逻辑从模型身上部分转移到技能体系本身。模型不应该是拿着一堆工具瞎猜怎么组合的实习生而应该是一个照着操作手册打螺丝的老师傅。技能就是这个操作手册。1.2 技能体系的三大模块我落地的时候把整个技能体系拆成了三个模块分工清楚就不会乱。技能注册中心负责管理所有技能的元数据包括技能名、描述、参数Schema、版本号、分组、权限级别相当于一个技能“仓库”或“货架”。意图路由层拿到用户输入之后先在注册中心里筛一遍候选技能再把候选技能列表塞进模型上下文让模型做选择并输出结构化参数。这一层就是“挑哪个技能合适”。技能执行引擎真正调用业务函数执行完毕之后把结果做格式化、摘要化再回填给模型做后续分析。这一层就是“干活并把活干漂亮的结果告诉老板”。这套结构在工程上最大的好处是各层可以独立演进。注册中心可以随时加技能路由层可以单独调提示词和策略执行引擎可以加缓存、加超时控制互不干扰。你不需要为了加一个技能去改路由代码也不需要为了调执行逻辑去动注册中心的数据结构。1.3 我为什么坚持“契约优先”的设计顺序我见过太多agent项目是怎么死的先写一堆工具函数然后写提示词让模型去选选完之后发现参数对不上、报错满天飞于是再回头改工具。这种开发方式本质上是让模型去适应烂接口等于倒着画图纸。我做agent-skills的核心理念是契约先行。写代码之前先把每个技能的输入输出Schema定义成一份完整的契约。参考OpenAPI那套思路但比OpenAPI更强调对模型友好的描述。每一份技能契约要包含技能名、触发条件、禁止调用条件、参数说明类型、枚举、必填项、返回值格式、调用成功与失败的标志。等契约全部定完再去写路由提示词最后才去写业务实现函数。这个顺序帮我规避了绝大多数典型的agent失败模式。因为模型不挑接口只要你契约清楚它就能对齐但只要契约模糊它一定会给你夹带私货。后面你会看到无数个看似是“模型笨”的问题根因全在契约写得不清楚。2. 核心细节解析与实操要点技能定义契约怎么设计才不翻车2.1 一份靠谱的技能定义长什么样技能定义是整个agent-skills的命根子。你的技能定义写得烂后面再怎么调提示词都是亡羊补牢。下面直接放一份我实际用过的技能定义模板以日程查询技能为例{ name: calendar_query, version: 1.2.0, group: daily, description: 查询用户日历中的日程安排和会议。适用于用户询问某天有什么安排、几点有会、什么时候有空、某个会议具体时间等场景。不适用于创建或修改日程这类需求请使用calendar_create或calendar_update。, parameters: { type: object, properties: { date: { type: string, format: date, description: 查询的日期格式YYYY-MM-DD必填。如果用户说今天或明天你需要换算成具体日期。 }, keywords: { type: string, description: 可选。过滤日程的关键词比如会议主题或参会人名称。 } }, required: [date] }, returns: { type: array, items: { type: object, properties: { start_time: { type: string }, end_time: { type: string }, title: { type: string } } }, description: 按时间升序排列的日程列表。如果没有日程返回空数组。 } }这份定义看起来平平无奇但有几个细节是踩过坑才写进去的。第一description字段不是给产品经理看的是给模型看的。很多人的技能描述写得像功能文档“该技能用于查询日历”然后模型在用户说“帮我看看明天下午三点以后能不能约个会”的时候死活不肯调它因为描述里没有告诉模型“查空闲时间”也属于这个技能的管辖范围。我把“什么时候有空”这种意图直接写进描述里之后命中率一下子就从六成提到了九成以上。第二description里必须写明“不适用什么”。这是我在agent-skills调试里学到的最贵的一课。一个技能定义如果只说能做什么模型就会把边界模糊的请求都塞给它。加了“不适用于创建或修改日程”之后误调用的概率直接腰斩。负例描述比正例描述还重要因为模型本质上是靠语义相似度来匹配的你不划清楚边界它就会把你不想它干的事也强匹配过来。第三parameter描述要写“模型换算规则”。比如date参数我明确要求模型把“今天”“明天”“这周五”换算成具体日期。你不写这句话模型可能真的给你传“明天”两个字过来你的后端校验直接报错。记住一个原则在技能定义里永远要假设模型是个听话但理解力有上限的实习生你交代得越具体它出错的概率越低。2.2 技能粒度怎么切拆得多细才算合适技能粒度是个反直觉的问题。我见过两种极端一种是所有操作塞进一个“万能工具函数”里参数里加一个action字段去区分另一种是把一个简单场景拆成五个细碎技能每个技能只做一件事。这两种都是坑。万能函数的写法听起来省事实际上完全违背了技能设计的初衷。你等于把路由逻辑塞给了模型自己判断action枚举且模型在选择参数的时候经常张冠李戴。比如actionquery带上了user_idactioncreate却漏了user_id因为参数Schema混在一起模型分不清哪些参数属于哪个action校验逻辑也没法针对不同action精细化处理。拆开之后每个技能的参数Schema独立校验模型输出错的概率就小多了。但拆太细连带的问题也麻烦。技能越多注入给模型看的技能清单就越长token消耗越大选择时出现混淆的概率也在上升。比如“发邮件给同事”和“给同事发消息”这两个技能语义边界本来就模糊你非要拆成两个独立技能模型就得花额外精力去分辨。我实践的粒度判断标准有一条看两个技能能否用一个清晰的“用户意图边界”切分。如果用户说一句话你能立刻判断出该走哪个技能那就可以拆如果你自己都得想一下“这句话到底算哪个”那就一定会发生误路由。每个技能要有独立的价值闭环就是说它的返回值要能独立支撑用户的一次请求不需要依赖另一个技能的副作用。2.3 技能分组加载与上下文token控制这是agent-skills里极易被忽视的工程问题技能清单是塞进模型的system prompt里的你注册了一百个技能总不可能一百个全塞进去。一次对话要用的技能顶多十个再多模型就开始犯迷糊而且上下文里光技能说明就吃掉一大截token预算用户的多轮对话历史反被挤掉这是典型的本末倒置。我的做法是给技能分组标签按场景懒加载。日常类技能组包含日历、备忘、提醒、天气通讯类技能组包含邮件、IM消息文件类技能组包含搜索、整理、改名、压缩。系统启动时只加载基础技能组等到识别出用户意图偏向某方向时再把对应技能组的描述补进上下文。注意分组不是简单的分类它应该有行业业务在里边。比如在电商客服场景里订单查询、退款申请、物流跟踪这几个技能必须放同一组因为用户经常在对话里交叉触发这些需求拆在不同组会导致技能加载转换的延迟。在个人助理场景里日程和提醒也是必然绑定的。还有一个小技巧是给每个技能加一个cost字段代表这个技能调用一次消耗的成本。路由打分的时候如果两个技能的匹配度接近我会优先选cost低的那个。这个字段还可以用来在对话状态下拉比如用户反复在几个技能之间来回横跳时把高频技能的匹配权重往上调。这个我后面再细说。3. 实操过程与核心环节实现从零搭一个可用的技能系统3.1 案例设定与技能清单规划空谈误事我直接用一个完整案例来演示实现。场景是一个个人事务助理agent需要具备日程管理、天气查询、备忘记录、邮件发送、附件整理几个能力。我不想用现成的LangChain或CrewAI框架包办一切因为那会掩盖掉技能体系设计的关键细节。自己搭一遍你才真正知道框架在背后帮你做了什么、没帮你做什么。第一步是整理技能清单。所谓的agent-skills在这个案例中至少需要以下条目技能名分组一句话职责calendar_querydaily查询日程和空闲时间calendar_createdaily新建日程calendar_updatedaily修改/取消日程weather_querydaily按城市/日期查天气memo_createdaily创建备忘memo_querydaily查看备忘email_sendcommunication发送邮件email_draftcommunication生成邮件草稿file_findfile按关键词找文件file_organizefile按规则整理目录这个清单规模不大但已经覆盖了技能系统的所有核心挑战有读类技能、有写类技能、有需要多参数组合的技能、有需要权限级别区分的技能。规划阶段一定要做的事是给每个技能写一个“一句话职责”。等你写到技能定义的时候那句“一句话职责”就是description的骨架。3.2 技能注册中心的代码实现技能注册中心不需要引入任何重量级框架我用Python的dataclass加一个全局注册表就够用关键是数据结构要稳定。from dataclasses import dataclass, field from typing import Any, Callable, Optional import json dataclass class SkillSpec: name: str version: str group: str description: str parameters: dict returns: dict cost: int 1 permissions: list field(default_factorylist) handler: Optional[Callable] None def to_prompt_block(self) - str: 把技能定义压缩成适合放进system prompt的文本 return f### {self.name} Description: {self.description} Parameters: {json.dumps(self.parameters, ensure_asciiFalse)} class SkillRegistry: def __init__(self): self._skills {} def register(self, spec: SkillSpec): if spec.name in self._skills: raise ValueError(fduplicate skill: {spec.name}) self._skills[spec.name] spec def get(self, name: str) - SkillSpec: return self._skills.get(name) def list_by_group(self, group: str) - list: return [s for s in self._skills.values() if s.group group] def list_by_ids(self, ids: list) - list: return [self._skills[i] for i in ids if i in self._skills] REGISTRY SkillRegistry() # 注册一个技能 REGISTRY.register(SkillSpec( namecalendar_query, version1.2.0, groupdaily, description查询用户日历中的日程安排...不适用于创建或修改日程..., parameters{ type: object, properties: { date: { type: string, format: date, description: 查询日期YYYY-MM-DD... }, keywords: {type: string, description: 可选过滤关键词} }, required: [date] }, returns{type: array, items: {type: object}}, cost1, permissions[user], handlercalendar_query_impl # 业务函数 ))注册中心的代码本身不复杂真正的门道在于to_prompt_block()这个方法。我专门做了压缩处理只保留技能名、Description和Parameters不把returns放进去。为什么因为returns对模型选型没有帮助它只会让prompt更长。模型做路由决策时只需要知道“这个技能是干什么的、需要什么参数”至于返回什么那是调用之后的事模型不需要提前知道序列化格式。3.3 意图路由的核心二级筛选加结构化输出路由层是整个技能系统的中枢神经。我最初尝试过直接让模型从全部技能清单里选技能一多就翻车。后来改成二级筛选准确率明显上了一个台阶。第一级是关键词粗筛把用户消息里的关键词和技能分组做匹配比如出现“邮件”“发送”“发给”就优先加载communication组出现“明天”“几点”“会议”就优先加载daily组。这一步不需要模型纯规则匹配瞬间完成。粗筛的token成本几乎是零但它能把上百个技能瞬间压缩到二十个以内。第二级才是模型精挑。从粗筛结果中取出候选技能定义拼进提示词让模型输出最终选择、参数JSON、补充说明。这个阶段我用的提示词模板是固定的每次迭代调优都改这份模板而不是改技能定义你是技能调度器。根据用户意图从候选技能中选择最合适的一个。 规则 1. 只输出JSON不要任何解释性文字。 2. JSON格式{skill: 技能名, arguments: {参数对象}} 3. 如果用户意图与所有候选技能都不匹配输出{skill: none} 4. 严格按技能定义中的parameters结构输出参数缺失必填项时给出空值并标记。 候选技能列表 {{candidate_skills}} 用户消息{{user_message}} 请输出JSON这里有几个必须坚持的细节。第一输出的JSON格式要严格限定。我踩过的最大的一个坑是让模型“自由发挥”模型经常会自作多情地加上一段解释“我已经理解了用户的需求看起来用户想查询日程所以调用了calendar_query……”然后JSON夹在解释中间解析器扑街。强制它只输出JSON之后问题彻底消失。第二候选技能列表的排列顺序非常重要。模型在输出选择时受位置偏差影响极大它更倾向于选排在最前面的那个。所以我把技能列表按“预估命中概率”排序第一级粗筛命中关键词权重越高、近两轮对话中命中频率越高的技能排越前。这里没法保证永远最优但实测下来比随机排序的准确率高出一截。第三别忘了兜底输出。加{skill: none}这个分支是为了防止模型强凑一个技能出来。没有这个兜底的时候模型会在技能完全不匹配的情况下硬选一个最接近的然后执行引擎跑出一堆废结果。有了这个兜底你可以在路由层直接拒绝调用回复“抱歉我还不具备这个能力”用户体验好得多。3.4 技能执行引擎参数校验、超时控制与结果回填路由层输出技能名和参数之后进入执行引擎。执行引擎的第一个工作是参数校验。模型生成的参数十次有六次会有小毛病——日期格式不对、缺了必填项、传了不存在的枚举值。我在执行引擎里用jsonschema做严格校验校验失败直接返回错误信息不进入业务函数。import jsonschema from jsonschema import Draft7Validator def execute_skill(spec: SkillSpec, arguments: dict): # 参数校验 try: jsonschema.validate(arguments, spec.parameters) except jsonschema.ValidationError as e: return { status: error, error_type: invalid_arguments, message: f参数校验失败: {e.message} } # 超时控制 import time if spec.handler is None: return {status: error, error_type: no_handler} try: # 用一个简单包装做超时控制生产环境建议用 asyncio.wait_for result spec.handler(**arguments) return {status: ok, result: result} except Exception as e: return {status: error, error_type: handler_error, message: str(e)}参数校验失败时返回给模型的信息要包含具体字段名但不能暴露整个底层异常堆栈。模型会根据错误信息自我修正然后重新调用这一招在实际测试中特别好用模型第一次参数给错了你告诉它“date字段格式应为YYYY-MM-DD你传的是明天”它第二次调用时就能自己改成正确的日期格式不需要你额外纠错。执行成功之后的结果回填也有讲究。技能函数返回的原始结果经常是很长的结构化数据不可能全塞回上下文。我写了一个摘要器把原始结果压缩成模型后续推理够用的“精华版”回填。以calendar_query为例原始结果可能是一周内所有日程的完整列表回填给模型的时候只保留主要字段和时间段并在末尾加一句统计信息“共5个日程最早的是周一9点的季度评审会最晚的是周五17点的1v1沟通。”模型拿这个摘要就能自然回答用户各种追问不需要每次都再调一次技能。3.5 新技能接入全流程从注册到灰度上线当你设计好了基础框架后续加技能就是一个纯流水线流程。我的标准步骤是这样的写技能定义契约JSON先不写实现函数。把定义塞进一个离线测试脚本里用20条该技能的目标语料和20条易混淆语料跑一遍路由看命中率和误判率。通过路由测试后再写业务实现函数接入执行引擎。在测试环境跑几轮真实对话观察执行结果回填是否适合模型继续回答。灰度发布先把技能标记为beta状态只在指定用户群里生效跑一段时间看日志里的调用成功率和回退率再放量全量。这套流程里最容易被跳过的就是第二步。很多人觉得“不就是加个技能嘛”直接写完一注册就上生产了。结果上线之后发现模型一遇到带歧义的表达就误调用这个技能轻则答非所问重则执行了用户根本没要求的操作比如把“删除”误解成“归档”数据安全问题就大了。技能系统的核心价值就是让agent的行为可以预测、可测试、可灰度如果你的技能上线过程一点都不严谨那跟裸奔没什么区别。4. 常见问题与排查技巧实录那些必须踩过才知道的坑4.1 模型总是选错技能怎么办这是agent-skills里最让人崩溃的问题。症状是用户明明在问天气模型却去调日历查询用户说“把文件发给小王”模型去调邮件发送但漏了附件参数。排查方法在绝大多数情况下都指向同一个根因技能描述写得太“功能化”了没有写到触发条件和语义边界。我的修法是用三句话重构任何一条技能描述把“这个技能是干什么的”改成“当用户想要什么结果时你才用这个技能”、把“有什么参数”改成“参数在什么情况下取什么值”、再加一句“绝对不要用这个技能来处理什么”。改完之后第一轮测试精准命中率基本能从60%提到85%以上。还有一个容易被忽略的点技能名本身也会参与相似度匹配。calendar_query和calendar_create这种两个词高度重叠的技能名在向量空间里容易混淆。我在命名上刻意把动词差异前置化比如改成query_schedule和add_schedule字面差异更大模型误选的概率就会变小。4.2 参数幻觉和参数错传怎么防模型经常生成根本不存在的参数。比如你的weather_query只定义了city和date两个参数模型偏偏多传一个unitsmetric。严格Schema校验是第一道闸但治标不治本模型输出被校验拦截后会重试反复几次就成死循环了。更好的办法是在路由提示词里就加一句约束“只允许输出技能定义中显式声明的参数禁止添加任何未定义字段。”并且把这句话放在提示词的开头附近。实测下来放在开头比放在结尾管用得多模型对prompt前部的指令遵从度远高于中部和尾部。另一种参数错传的情况是参数名写错。比如技能定义了start_date模型记成了start_time。对付这个我会在description里显式写出参数标准写法“注意参数名是start_date表示开始日期与end_date配套使用。”模型看到这句话之后错传的次数直线下降。4.3 技能返回结果太长把上下文撑爆了技能执行函数返回一个五百行的销售报表执行引擎不管三七二十一全塞进上下文下一轮对话的模型输入就膨胀到接近上限。这个问题在我早期项目里频繁出现每次都是用户问一个问题之后后续对话质量急剧下降因为上下文里堆满了无用数据。解决办法我已经在上文提到过执行层必须做摘要。摘要规则按技能类型定制。查询类技能保留Top结果和统计值写入类技能只返回“已成功创建日程时间周四1400主题产品评审”不需要把整个日程对象原样回填。回填格式统一用“动作关键对象结果状态”三段式模型读起来效率最高。4.4 多个技能都匹配优先级怎么排当用户说“帮我看看邮件然后把这个附件存到网盘”时这本质上是一次对话里连续触发两个技能。如果你的路由层只支持单技能返回模型就会纠结半天选哪个。我后来把路由层的输出格式升级成支持技能序列数组模型可以按顺序输出多个技能及其参数执行引擎依次执行前一个技能的结果摘要作为下一条技能的隐含参考。这样做的代价是路由层的决策复杂度上升模型偶尔会把不该串行的技能也串起来。我的应对是给技能增加一个context_aware标记只有显式声明允许在连续技能调用中使用的技能才会被模型考虑串行。像email_send这种副作用明显的技能默认只允许单独调用除非模型在前置技能里已经拿到了完整的收发人信息和主题内容。4.5 离线调试技能路由的必备工具最后分享一个我自己搭的离线调试工具简单但极其有效。我把所有技能定义、历史用户问题、正确路由结果组成一个测试集每次改动技能描述、调整提示词模板后就在测试集上全量跑一遍。跑的方式是离线模拟对每条测试样本把候选技能列表和用户问题组装成prompt让模型输出路由决策然后和正确路由结果对比算出命中率和误判率自动生成一份差异报告。这份报告会直接告诉你三件事哪些技能的描述导致模型在哪些样本上选错、哪些样本同时命中了多个技能导致路由不稳定、哪些技能定义在候选列表里被完全跳过。有了这份报告调优就变成了数据驱动的活而不是靠手感瞎试。我强烈建议任何做agent的人不管项目多小都给自己搭一个这样的小工具它带来的效率提升是数量级的。自己在实际项目里摸爬滚打一圈我最深的感受是做agent技能体系本质上不是在写代码而是在写操作说明书。模型的可塑性很强你给它的说明书逻辑清晰、边界明确、例子充分它就能给你一个可预期的行为你给它的说明书含糊其辞、功能堆砌、边界模糊它就会还你一个失控现场。所以最后再强调一遍我的核心工作流程先定契约再写描述最后写实现。这个顺序我是在反复返工了不知道多少次之后才咬牙定下来的规矩。拿走直接用能帮你省下大量我没必要重复踩的坑。
返回列表