ARTICLE DETAIL

资讯详情

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

Agent技能库设计与实现:从Prompt塞逻辑到标准化Skills调用的工程实践

Agent技能库设计与实现:从Prompt塞逻辑到标准化Skills调用的工程实践 做 Agent 开发的朋友应该都有这种体验模型本身能力再强不接工具就是“空有想法没手”一旦接了工具又开始头疼“什么时候调、调哪个、参数怎么填”。我最早做智能体的时候也是踩了一堆坑——把所有逻辑全塞进 Prompt 里模型倒是能“理解”但一旦任务复杂起来它就乱选、漏选、甚至自己编参数。后来我把思路从“让模型自己发挥”改成“给模型一张标准化的技能清单”也就是把能力抽成 agent-skills 的形式模型按清单去匹配和调用。这个转变之后调用准确率明显上了一个台阶而且整个系统变得特别好维护。这篇文章我把这套思路、定义方法、完整实现链路和踩坑记录都整理出来正在做 Agent、RAG 或自动化工作流的朋友可以直接参考。1. 为什么需要“技能库”而不是写死逻辑很多人一开始的做法是把工具描述一股脑写在 System Prompt 里告诉模型“你可以使用以下工具”然后靠模型自己去理解。前期工具少的时候还能跑通等到工具超过五六个或者任务开始分步依赖、条件判断的时候马上露馅模型要么漏调用要么把互斥的工具一起调用要么根本不知道该先调哪个。技能库的核心思路是把“能不能调”变成“该不该调”——每一个技能都是一个独立的模块模型的任务不是理解工具本身而是理解“当前场景匹配哪个技能”。1.1 直调模型和技能调用的本质差异直接让模型调用工具模型面对的是“一堆函数签名”它需要自己判断参数类型、边界、调用时机这其实是把工程上的判断压力全甩给了模型。而技能调用是给每个能力配上“使用说明书”说明书里写清楚触发条件、前置依赖、输出格式。两者最大的区别在于直调模型是基于“概率”在猜技能调用是基于“匹配”在做选择。拿一个最简单的场景来说用户问“北京今天多少度”直调方案是告诉模型有个get_weather(city)函数模型自己推断出你要传city北京。听着没问题但如果同时还有get_air_quality(city)、get_forecast(city)模型就开始纠结了——到底哪个才是用户真正想要的技能库的方案会在get_weather的描述里写清楚“仅当用户询问当前温度或天气状况时使用不用于空气质量或未来预测”模型一看这个描述匹配起来就不费劲了。1.2 agent-skills 的完整调用链路长什么样一套标准的技能调用链路应该有五个环节技能注册、技能发现、技能选择、技能执行、结果回填。技能注册是把所有可用的能力登记到一张清单里这个清单就是模型唯一的“菜单”技能发现是根据用户当前的问题先从清单里筛出候选技能这一步通常靠 embedding 召回或关键词匹配技能选择是模型在候选中做出最终决定并且输出结构化调用参数技能执行是后端真正跑这段逻辑结果回填是把执行结果交还给模型让它继续推理。这个链路里面最容易被忽略的是“技能发现”这一步。很多人直接让模型在全量技能里选技能少还行一旦技能上百个模型在选择时就会产生注意力分散选错率明显上升。我个人一开始也是直接全量塞给模型后来技能多了才发现前置一个召回步骤能过滤掉 80% 完全不相关的技能模型的选择压力小很多准确率自然就上来了。1.3 哪些场景收益最大不是说所有项目都要上技能库我实践下来下面这几类场景收益最大。工具数量超过五个五个以上工具同时暴露给模型时选择准确率会显著下降技能库的“先召回再选择”能有效缓解。任务存在先后依赖比如“先查订单状态再决定是否发起退款”这种流程不能靠模型一次调用搞定必须拆成多个技能分步执行。多轮对话中需要保持上下文技能执行结果要能“记住”并参与后续轮次的推理这时候技能库的标准化输出就很重要。团队协作场景同一个技能库可以被多个 Agent 复用写好一次到处调用。反过来如果项目只有一个工具或者流程是完全固定的直接调用函数比上技能库划算得多。技能库不是银弹它的核心价值是“在动态场景下提供结构化的选择能力”。2. 技能定义与描述成败的隐藏关键技能库的整个地基就是“技能定义”。定义写得好模型选得准定义写得烂后面全白搭。一个技能定义至少需要包含五部分名称、描述、参数、返回、权限。这五部分各有各的讲究尤其是描述这一块大部分人都没写到位。2.1 一个技能清单该有的字段技能清单有时候叫 skill manifest是整体技能的注册表我习惯用一个 YAML 或 JSON 文件来维护。每个技能的骨架大概长这样技能 ID 用于程序内部识别名称用人类可读的短句描述是给模型看的触发条件说明参数是 JSON Schema 格式的结构化定义返回值定义执行结果的格式约束。设计的时候有一个重要原则技能 ID 和名称要“见名知意”但描述要“见文知用”。什么意思ID 可以直接叫get_weather没问题但描述里一定要写清“什么时候用、什么时候不用、参数怎么从对话里提取”。我自己维护技能清单时还会加一个enabled开关。这个字段特别实用——线上突然发现某个技能有 bug或者要灰度测试新技能直接把开关关掉就行不必改代码、不必新发版。等测试好了再把开关打开对生产环境非常友好。2.2 技能描述是写给模型看的“说明书”描述写得好不好直接决定模型能不能正确选择技能。很多人写描述会写成“获取天气信息的工具”这其实是一种无效描述因为它只说了“是什么”没说“什么时候用”。有效的描述应该包含三部分内容触发条件、不触发条件、参数来源。举一个负面例子和正面例子的对比。负面写法是“该工具可以查询指定城市的天气信息参数为城市名称。”正面写法是“当用户询问当前或未来的天气状况、气温、降雨概率时使用。从对话中提取城市名称作为参数。当用户询问空气质量、历史天气时不要调用此工具。”这两种描述在模型面前效果差别非常明显。原因是模型不是靠“理解”工具而是靠“匹配”场景描述里把场景写全匹配的准确度就高。还有一种进阶写法就是在描述里加入“示例对话片段”。比如写清楚“用户说‘北京热不热’也算天气查询可以调用。”这种方式特别适合那些触发边界模糊的技能能显著降低模型误判率。2.3 技能粒度的选择太细和太粗都有问题技能粒度是我觉得整个设计里最需要拿捏的部分。粒度太细比如把“查天气”拆成“查温度”“查湿度”“查风力”三个技能模型反而会困惑用户说“今天冷吗”到底调哪个粒度太粗比如把所有信息查询类能力揉成一个大工具那模型就退化成了在读一本巨大的说明书和直接塞工具给模型没有区别。我的经验是按“用户意图边界”来切分技能而不是按“功能边界”。用户说“帮我安排明天的日程”这是一个完整意图哪怕内部需要调日历、设提醒、查时间冲突三个后端能力对外也应该是一个“日程安排”技能。这个技能内部可以编排多个函数调用但模型不需要知道这些细节它只需要知道“这个技能能安排日程”。另外技能之間尽量不要有功能重叠。我踩过的一个坑是两个技能都能查订单状态一个查普通订单一个查售后订单结果模型经常选错。后来把两个技能的描述彻底区分开在触发条件上加了明确边界“仅当……”问题才解决。重叠的边界必须要在描述里“划清领地”。3. 实战从零搭一个可用的 agent-skills 引擎概念讲再多不如直接动手。我这边用一个实际的例子带大家完整走一遍让 Agent 具备两个技能一个是“查天气”一个是“生成日程提醒”然后让模型根据用户的一句话自动选择技能并调用。3.1 准备阶段技能清单与工具定义首先定义技能清单。我会把它写成一个 JSON 文件因为 JSON 的结构化程度高模型读取时不容易误解。下面这份清单定义了weather_query和schedule_reminder两个技能注意看清我描述的写法——每个技能都写清楚了触发条件、不触发条件、参数来源、返回格式。{ skills: [ { id: weather_query, name: 查询天气, description: 当用户询问当前或未来某天的天气、温度、降雨概率时使用。需要从对话中提取城市名称可选日期默认为今天。当用户询问空气质量、历史天气、穿衣建议时不要调用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海 }, date: { type: string, description: 日期格式YYYY-MM-DD默认当天 } }, required: [city] } }, { id: schedule_reminder, name: 创建日程提醒, description: 当用户要求创建提醒、设置闹钟、安排日程时使用。需要提取时间和提醒内容。当用户只是询问日程列表不涉及新增提醒时不要调用。, parameters: { type: object, properties: { time: { type: string, description: 提醒时间格式YYYY-MM-DD HH:mm }, event: { type: string, description: 提醒内容 } }, required: [time, event] } } ] }这份清单会作为 System Prompt 的一部分传给模型。但注意实际传给模型的内容我会做一次精简只保留技能的id、name、description、parameters去掉工程上的冗余字段避免模型读太长的内容产生注意力偏移。3.2 核心链路让模型在“思考”和“调用”之间切换接下来是核心的运行时链路我用 Python 写一个简化版。这个流程分成三步第一步让模型判断当前用户的输入是否需要技能并输出一个结构化指令第二步解析指令执行对应技能第三步把执行结果回填给模型让它基于结果做最终回复。这里的关键设计是不要让模型“直接说话调工具”混在一起而是强制模型输出一个 JSON 动作指令。这样可以避免模型在回复文本里夹杂工具调用解析起来特别痛苦。下面这个函数展示了动作解析的过程import json import openai client openai.OpenAI() def parse_action(user_input, skills_prompt): response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: skills_prompt \n你需要输出JSON动作指令格式为{\action\: \技能ID或none\, \parameters\: {参数对象}}。如果不需调用任何技能action为none。}, {role: user, content: user_input} ], response_format{type: json_object} ) return json.loads(response.choices[0].message.content) def execute_skill(action, params): if action weather_query: # 实际工程中这里调用天气API return f{params[city]}今天晴25°C降水概率10% elif action schedule_reminder: return f已设置提醒{params[time]} {params[event]} return None注意两点第一我用了response_format强制模型输出 JSON 对象这比让模型“自由说话”再解析稳定得多第二动作指令里的parameters是模型根据技能描述里的 JSON Schema 生成的不是我们代码里定义好的所以我们执行前一定要做校验。实际执行的时候我会把这两个函数串起来再让模型基于工具结果生成用户能看懂的回复。整个链路就是“用户输入 - 动作解析 - 技能执行 - 结果回填 - 最终回复”。多轮对话时每一轮都重复这个链路并把前一轮的技能执行结果作为历史上下文传给模型。3.3 参数校验与失败回退机制模型生成参数这件事永远不能百分百信任。用户说“提醒我明天早上八点开会”模型可能会把时间格式写成明天早上8点而不是2025-01-15 08:00。如果后端直接拿这个字符串去存数据库肯定要出问题。我的做法是在技能执行前加一层参数清洗和校验。第一步检查必填参数是否齐全缺了就直接返回“参数缺失”的错误信息而不是硬着头皮执行第二步对时间、日期这类格式敏感的参数做解析能转标准格式就转转不了就返回给模型去追问用户第三步参数范围也要校验比如查天气的城市参数如果模型输出了“地球”那后端 API 肯定会报错这时候返回一个通俗的错误提示比堆栈信息友好得多。from datetime import datetime def clean_params(action, params): if action schedule_reminder: if time not in params: return None, 缺少提醒时间 try: # 尝试把各种常见说法归一为标准时间 params[time] normalize_datetime(params[time]) except ValueError: return None, 无法解析提醒时间请让用户补充具体时间 return params, None养成一个习惯把技能执行的结果尽量设计成“可以直接回填给模型”的字符串而且要简洁。像查天气接口原始返回可能是一大段 JSON里面有几十个字段模型看到那么长的内容反而容易迷失重点。我会在技能内部就把返回结果提炼成“北京今天晴25°C降水概率10%”这种一句话模型拿到的信息干净明确后续推理质量也会提升。4. 常见问题与排查技巧实录技能库搭建起来不难真正难的是跑起来之后的各种“幺蛾子”。我把自己在生产和实验环境中踩过的坑整理成了一份速查表这些问题的表现形态各不相同但根因往往都出在技能定义或参数处理上。4.1 模型不调用技能或乱调用技能这是最常见的问题表现形式有两种该调的时候不调或者不该调的时候瞎调。遇到这种情况我的排查顺序是先看技能描述里有没有写清楚“触发条件”和“不触发条件”再看是不是两个技能描述存在模糊的边界最后看是不是技能太多了模型注意力分散。如果你用的是 GPT 这类能力较强的模型并且技能数在十个以下不调用多半是描述问题。描述不要写“查询天气的工具”要写“当用户询问天气……时”。如果你用的是开源的小参数模型不调用还有一个常见原因模型输出格式不稳定没有严格遵循“输出 JSON 动作指令”的要求。这时候考虑换一个更大的模型或者在解析时做容错例如支持解析“带代码块包裹的 JSON”和“纯文本里的 JSON 片段”。还有一个容易忽略的点System Prompt 里的技能列表排位。模型对靠前的内容注意力更强所以高频技能要往前放。我实测过同一个技能放在第一位和第五位被选中率有明显差距。4.2 参数幻觉模型自己编造参数值模型在用户没有提供某个参数时经常会“脑补”一个值。比如用户说“帮我查天气”没提城市模型可能自己填一个city北京导致结果完全偏离用户预期。这个问题单靠描述很难根治因为模型有很强的“补全”倾向。我的方案有两层。第一层是在技能描述的参数说明里明确标记“该参数必须从用户对话中提取未明确提及时设为空不得自行猜测”。这句话能起到一定约束作用但不是完全可靠。第二层是在代码里做“信息缺失检测”如果必填参数没有被用户提供直接让模型反问用户而不是拿猜测值去执行。具体做法是在动作解析时同时要求模型输出“参数来源置信度”对于置信度低的参数就走追问流程。4.3 工具返回体过大或过于结构化拖垮推理质量天气 API 原文可能是这样的{city: {name: 北京, id: 101010100}, now: {temp: 25, feels_like: 26, humidity: 30}, daily: [{date: 2025-01-15, temp_max: 27, temp_min: 18}, ...]}如果直接把这么一大坨 JSON 丢给模型它虽然能看懂但会把注意力浪费在无关字段上而且模型回复时会忍不住引用那些原始字段导致解释冗长且不贴近用户。技能层一定要做“信息提炼”把模型需要的核心信息抽出来变成一句话或一个小表。这个操作我给一个很朴素的比喻技能库提供给模型的应该是“菜单”而不是“后厨”。模型不是数据管道它不需要看到所有原始数据。我建议设定一个硬性规范任何技能返回给模型的内容都要经过一个“提炼函数”确保模型拿到的是一段不超过 200 字、且直接面向用户问题的结论。如果技能逻辑复杂需要模型基于多步骤推理那可以把中间结果放在内部存储里模型每步只看到当前需要的信息。4.4 速查表最常见的六个问题与直接对策问题现象可能原因直接对策该调用的技能不调用描述中未写清触发条件在描述里增加“当用户……时使用”句式和负向触发条件两个相似技能选错技能边界重叠给每个技能划分“领地”描述中明确指出各自的排除场景参数被模型编造模型补全倾向描述中标记“参数必须来自用户”代码层做缺失检测并追问调用顺序不稳定缺乏流程编排把有依赖的调用拆成“技能链”用前一个技能结果驱动后一个技能工具返回体过大未做信息提炼增加提炼函数只把核心结论回填给模型模型输出的 JSON 解析失败格式不稳定使用强制 JSON 输出的接口或做带容错的解析器从实际经验来看日常 Agent 开发中遇到的绝大多数“模型不听话”的问题本质上都不是模型的问题而是我们提供的信息不够结构化。把技能库做好模型的表现通常会比你反复调 Prompt 要稳定得多。做技能库这件事值得在前期的定义上多花时间——我自己的体会是定义技能比写调用代码多花了三倍时间但后期的调试成本降了十倍。多花点时间把技能边界描述清楚绝对不亏。
返回列表