news 2026/10/14 8:36:33

LLM Agent技能库设计:从提示词到可复用能力的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM Agent技能库设计:从提示词到可复用能力的工程实践

1. 为什么Agent需要一套独立的技能库

1.1 从一次四处打补丁的开发说起

年初我在某个内部项目里负责给多个LLM Agent叠加功能,做着做着就发现不对劲。三个不同的Agent都在处理同一种“从网页中提取结构化字段”的需求,但每个Agent的提示词里都塞了一长串互相矛盾的指令,参数格式也完全不一致。A Agent用JSON返回,B Agent用自然语言拼接,C Agent干脆直接报错。每次加新功能,我都要同时改动三四处代码,测试用例还总是漏掉其中一两个。最气人的是,提示词越长,模型输出的稳定性越差,很多规则在长上下文里被模型选择性遗忘。

后来我实在忍不了,把所有通用能力从Agent的提示词里剥离出来,单独做成一个叫agent-skills的技能库项目。思路很简单:每个Agent不再重复写功能细节,而是声明自己需要哪些技能,技能库负责技能的注册、加载、调用和评估。这个项目后来在我们内部演变成了一个事实上的标准,你也可以把它理解成给Agent准备的“工具箱”和“使用说明书”的合集。

1.2 技能和提示词的根本区别

很多人觉得技能不就是一段提示词模板吗?我在做agent-skills之前也这么想,但实际跑下来发现区别很大。

提示词是写给模型看的,它没有固定的执行边界,模型可以自由发挥。而技能应该是一个“可执行的独立单元”,它至少要包含:元信息(名字、描述、版本)、参数定义(要什么输入)、执行逻辑(怎么处理)、测试用例(怎么验证)。这四样缺一不可。提示词只是技能内部执行策略的一部分,真正的技能应该像调工具一样被调用,输入合法参数,返回结构化结果,出错时还能抛出让调用方理解的异常。

举一个生活化的例子:你把“计算快递运费”直接写进客服系统的提示词里,模型会根据自己的理解乱算;但如果你把“计算快递运费”做成一个技能,定义清楚起止地、重量、体积、折扣参数,然后让模型去调用这个技能,结果就可复现多了。agent-skills的核心主张就是这样:可复用的能力不应该埋在提示词里,而应该抽出来单独管理。

2. agent-skills的技能四件套:从定义到验收的每个环节

2.1 元信息:让Agent和人都能一眼看懂

技能元信息是整个技能库的索引,也是后面自动路由的基础。我在项目里给每个技能定义了一个稳定的头部结构,类似这样:

name: fetch_web_fields description: 从网页HTML或Markdown中提取指定字段,返回结构化JSON。 version: 1.2.0 author: internal-platform-team tags: [web, extraction, scraping]

这里有个容易被忽略的点:description必须写清楚这个技能“能做什么”和“不能做什么”,最好带上一到两个典型使用场景。比如我见过有人写“处理网页数据”,这种描述太模糊,Agent根本无法判断该不该调用它。后来我们规定description至少包含“输入来源、输出格式、典型场景、边界限制”四个要素,比如:

description: > 从网页HTML或Markdown中提取字段列表指定的内容。 输入要求:原始的网页文本或干净的HTML。 输出:JSON对象,key为字段名,value为提取结果。 典型场景:商品价格抓取、新闻标题提取。 不能处理:需要登录态的页面、动态渲染的JS内容。

这样的描述,模型在few-shot情况下能用,关键词检索也能命中,后续接embedding语义检索效果也好得多。

2.2 参数定义:先定契约再写代码

我的第二个习惯是:一个技能在写执行逻辑之前,先把参数契约用JSON Schema定义好。agent-skills里每个技能都有一个schema.json,描述它允许哪些参数、必填哪些、值的类型和范围。

{ "type": "object", "properties": { "url": { "type": "string", "format": "uri", "description": "目标网页地址" }, "fields": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "需要提取的字段名称列表" }, "timeout_sec": { "type": "integer", "default": 10, "minimum": 1, "maximum": 60 } }, "required": ["url", "fields"] }

参数定义的过程,其实是把模型的“自由发挥空间”压缩到可控范围。有了这个schema,模型在选择调用技能时,就能按照格式生成参数。我们训练Agent时还发现,参数定义越细,模型第一次调用成功的概率越高,因为模型不需要去猜参数名和单位。如果技能自己内部还要做单位转换,那也要在schema里明确说明默认单位。

2.3 执行逻辑:把任务拆成可测试的小函数

我习惯把每个技能的执行逻辑分成两个部分:预处理器和执行器。预处理器负责把LLM传来的杂乱参数规整成标准格式,执行器只负责做真正的计算。

比如一个“调用内部汇率接口”的技能,预处理器可能要去掉货币符号、统一币种代码;执行器只负责请求接口、解析响应、返回固定结构。预处理器里我会写很多防御逻辑,因为LLM生成的参数总有不按常理出牌的时候。有一次模型把超时时间传成了字符串"10s",预处理器没处理好,整个技能报错,Agent也坏了。从那以后,所有数值参数都要经过类型校验和白名单校验。

执行器本身也要遵守单一职责。不搞一个大函数塞满全部逻辑,而是拆成几个内部私有方法。且每个私有方法最好都能单独测试,因为技能库越往后越庞大,不拆分根本没法维护。

2.4 测试用例:没有验证的技能等于定时炸弹

agent-skills下的每个技能都要求至少带三个测试用例:正常输入、边界输入、异常输入。正常输入验证主流程,边界输入验证极端情况,异常输入验证错误处理。

我举个例子。技能“计算折扣价”的正常输入是原价100、折扣0.8,期望输出80;边界输入是原价0、折扣1,期望输出0;异常输入是折扣大于1或小于0,期望抛参数错误。这些测试用例除了用于开发联调,更重要是接入CI/CD做回归测试。每次有新的技能版本,都要跑一遍所有依赖方的测试,防止改坏别的Agent。

测试用例还有个作用——作为Agent的few-shot示例。我经常从测试用例里挑一两个典型输入输出拼进技能描述里,帮助模型快速理解怎么调用。这比单独写一大段调用说明文档有效得多。

3. 搭建agent-skills的目录结构与加载机制

3.1 目录即知识库:技能如何组织才能不失控

技能多了以后,最头疼的就是找不到东西。我在agent-skills里采用了按领域分目录、按技能名建包的方式:

agent-skills/ ├── registry.json ├── skills/ │ ├── web/ │ │ ├── fetch_web_fields/ │ │ │ ├── METADATA.yaml │ │ │ ├── schema.json │ │ │ ├── execute.py │ │ │ └── tests/ │ │ └── html_to_markdown/ │ ├── math/ │ │ ├── calculate_discount/ │ │ └── currency_convert/ │ └── nlp/ │ ├── summarize_text/ │ └── extract_keywords/

registry.json是一个总的索引文件,记录每个技能的名称、路径、版本、支持的语言运行时。加载器启动时读这个索引,然后按需导入对应技能模块。这样技能库启动很快,没有“加载所有技能”的浪费。

3.2 技能注册:两种方式我都踩过

技能注册我试过两种方案。第一种是自动扫描目录,通过METADATA.yaml里的name字段识别技能。好处是新增技能不用改配置,坏处是一旦有格式错误,整个扫描可能失败,排错很痛苦。第二种是显式在registry.json里登记,新增技能必须手动加一行。好处是可控,坏处是人容易忘。

最后我选择了一个折中:扫描目录生成候选技能列表,但在启动时和registry.json做diff,不一致就给出warning。如果是正式环境,diff结果会写入日志,让负责人确认。这种“半自动注册”在团队协作里最省心。

3.3 动态调用与上下文注入:怎么让Agent觉得技能是“原生能力”

加载机制搞定以后,核心问题变成:Agent怎么调用技能?尤其是多个Agent共享同一套技能库时,不能让Agent记住所有技能的细节。

我在agent-skills里实现了一个简单的技能代理网关,暴露统一的接口:

async def invoke_skill(skill_name: str, params: dict, timeout: float = 10.0): skill = loader.load(skill_name) validated = skill.schema.validate_and_normalize(params) if hasattr(skill, "preprocess"): validated = skill.preprocess(validated) result = await skill.execute(validated) return skill.postprocess(result)

Agent这边,只需要在系统提示词里放一份“技能清单”,每项技能包含名称和一句话描述。真正要执行时,模型通过function calling机制选一个技能名,填入参数,然后网关负责真正调用。这个设计让Agent和技能库之间彻底解耦,新增技能完全不用改Agent的提示词——只要把技能清单更新一下就行。

上下文注入方面,我建议不要把整个技能文档塞给模型。你可以把“运行时环境信息”(比如用户所在时区、业务线ID)放在一个全局上下文中,技能执行时自动从上下文里取。比如汇率转换技能需要知道基准币种,全局上下文里有默认值。这样既减少了参数传递的复杂度,也让跨技能共享数据变得容易。

4. 让Agent自动选择技能:路由策略与冲突处理

4.1 从关键词匹配到语义路由:小步快跑的迭代过程

技能库只有个位数技能时,靠函数调用里的description就能让模型选对。等技能数量超过三十个,单靠description就经常选错。尤其当两个技能描述相似时,模型容易混淆。

我先尝试了基于关键词的检索路由。给每个技能挂几个关键词标签,然后把Agent的意图做简单的文本匹配。比如用户问“把这段文字摘要成三句话”,命中“摘要”“文本概括”等词,就路由到summarize_text技能。这个方法上限很低,用户换个说法就失配了。

后来我换成了embedding语义向量。把所有技能的description离线编码成向量,存到向量数据库里,实时把Agent的当前任务描述也编码成向量,计算余弦相似度,取top5作为候选技能,再交给LLM最终选择。这招很稳,即使用户表达跟技能描述字面完全不同,也能找对。

4.2 冲突消解:两个技能都想接同一个任务怎么办

没有冲突管理的路由一定会出事故。比如“查询订单状态”和“查询物流轨迹”这两个技能,在语义上高度接近,很多用户的表述两者都沾边。我的处理策略是给每个技能增加一个权重和适用范围字段,当候选技能相似度差距小于0.05时,优先使用有更高先验权重的技能,并且会向Agent返回一个备选列表让模型判断。

更重要的是,在技能本身写清楚边界。物流轨迹技能的description里明确写“仅处理快递运输节点信息,不含订单支付状态”;订单状态技能里也写“不管物流详情”。冲突自然减少了很多。还是要靠清晰的契约去消除模糊。

4.3 回退与人工兜底:不能让Agent卡死在一个错误选择上

路由不可能100%正确。agent-skills里我设计了一个回退机制:当某个技能执行失败连续两次时,网关会自动重新路由到候选技能列表中的下一个。如果所有技能都失败,就会把原始请求转交给一个兜底策略,可以是人工客服队列,也可以是默认回复模板。

回退机制的关键是记录失败原因。每个技能的异常都会转成标准化的错误码,比如PARAM_INVALID、EXECUTION_TIMEOUT、REMOTE_SERVICE_UNAVAILABLE。有了标准错误码,路由层才能正确决定是重新尝试、换技能还是转人工。我见过太多人忽略这层设计,最后Agent一报错就露出满屏堆栈,用户体验极差。

5. 技能的评估与持续迭代:不是写完就完了

5.1 构建基准测试集:把高发场景固化成测试样本

技能库做了一半时,我发现最缺的不是代码,而是评估标准。没有评估标准,你根本说不清“更新了技能后它是不是变聪明了”。我借鉴传统软件测试的思路,构建了一个基准集,里面包含三类样本:

  • 真实历史请求:从生产日志里脱敏抽取的成功/失败请求
  • 人工构造的对抗样本:故意用模糊、口语化、带错别字的表达
  • 边界组合样本:参数越界、缺失必填项、超长文本

这个基准集每个季度做一次更新,补充新出现的请求模式。在agent-skills里,每个技能目录下的tests子目录就是基准集的一部分,运行pytest即可一键回归。某个技能改动后,必须通过全部相关测试才能合并到主线。

5.2 性能指标:不只盯着准确率

准确率当然重要,但评估Agent技能不能只看这个。我重点关注三个指标:

指标含义常用阈值
首次调用成功率Agent第一次调用技能就成功拿到合法结果的比例≥ 90%
平均调用延迟从Agent发出调用到收到结果的时间≤ 2s
无效参数率参数校验未通过的比例≤ 5%

第一次调用成功率这个指标特别能反映技能描述写得清不清楚。如果模型经常漏传必填参数,说明技能参数设计太复杂或描述不够准确。无效参数率高,就要重新审视schema里的字段命名——我曾经把字段名从product_quantity改成qty,无效参数率立刻涨了10个百分点,后来又改回来了,这给我一个教训:字段名保持一致性和可猜测性比省几个字符重要得多。

5.3 版本管理:技能更新不能影响正在运行的Agent

技能的版本管理我用了语义化版本号,主版本号变更代表不兼容的更新。在registry.json里,每个技能记录当前版本和兼容版本范围。Agent调用时会声明它接受的版本范围,比如fetch_web_fields>=1.0.0,<2.0.0。这样技能库升级时,老Agent不会立刻被破坏。

同时,我坚持给每个技能更新写CHANGELOG文件。大到一个执行逻辑重构,小到一个description措辞优化,都记录下来。因为Agent的决策非常依赖描述文本,某个词语变化可能影响路由效果,所以必须能追踪到哪次改动导致哪个指标发生了变化。

6. 做agent-skills这一路踩过最深的几个坑

6.1 技能参数设计得过于“聪明”,反被聪明所误

第一版技能库里,我把“计算快递运费”的参数设计得特别灵活,允许传package_info对象,里面可以嵌套重量、体积、件数、是否加急等等。本意是让Agent能自由表达,结果模型根本不会正确嵌套,经常漏掉关键字段。后来我把参数拍扁成扁平结构:weight_kg、length_cm、width_cm、height_cm、urgent。奇怪的是,模型的表现立即提升了。

这让我意识到:给LLM使用的参数schema,越简单越好。不要用深层嵌套对象,不要用复杂的条件必填逻辑。每多一层嵌套,多一个条件组合,模型出错的概率就指数上升。宁可多定义几个并列参数,也不要用对象套数组套对象。

6.2 技能执行结果没有结构化,连锁反应很严重

初期有些技能直接返回模型的原生文本,比如“计算成功,结果为100”。下游解析就靠字符串匹配,一换说法就断。后来我统一要求所有技能返回JSON,结构如下:

{ "status": "success", "data": { ... }, "meta": { "skill_name": "calculate_discount", "version": "1.2.0", "latency_ms": 34 } }

这一步是最值得做的工程投入。有了统一的返回结构,路由层可以读取meta做监控,调用方可以安全解析data。更重要的是,Agent拿到结构化返回后,能直接把结果转化为用户友好的回复,比自己二次解析可靠得多。

6.3 错误信息写得太隐晦,Agent看不懂也没法转述

技能调用失败时,错误信息不能只给工程师看,Agent也需要读懂。早期我直接抛异常“IndexError: list index out of range”,Agent拿到后只能干瞪眼。后来我强制每个错误都包成可读的英文短语,比如:

SkillError: Field "price" not found in the provided HTML. Check if the page structure has changed or if the field name is correct.

原因写得具体,还带建议。Agent拿到这个信息,能自己决定是换参数重试还是换个技能,也能给用户一句合理的解释。这一步对体验的提升超出预期,因为用户看到的是“没有找到价格字段,可能是页面结构变了”而不是“系统报错”。

6.4 权限和审计:技能库越大,越需要管控

最后说说安全和权限。技能库里有些技能涉及内部数据或写操作,不能给所有Agent用。我在技能元信息里增加了一个access_level字段,取值可以是public、internal、restricted。网关在invoke_skill时校验调用方的权限token。这一步看起来简单,但没有它,等到技能库超过五十个技能时,你会完全失控。

审计日志也是必做的。每个技能调用都记录下Agent的ID、调用的技能名、参数摘要、返回状态和延迟。这些日志在排查问题时非常有价值,也直接反哺了基准测试集的构建——跑得越多,评估样本越丰富。

回到开头那个问题:Agent需要独立技能库吗?我现在可以坚定地回答,需要,而且非常需要。把能力和提示词分离后,Agent的稳定性明显提升,迭代速度也快了。后续我计划在agent-skills里加入技能依赖关系的声明,让一个技能能显式地调用另一个技能,形成技能组合。这条路还很长,但方向是对的。如果你也在做Agent应用,不妨从最小化的技能库开始,把公共能力沉淀下来,你一定会感谢这个决定。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:36:08

AMD Radeon云实例+ROCm跑通RoboTwin具身智能训练全流程

坦白说&#xff0c;一开始我以为在 AMD 的 Radeon GPU 云实例上跑通 RoboTwin 会是一趟特别折腾的旅程。毕竟具身智能训练这条链路&#xff0c;大家早就习惯了 NVIDIA 的 CUDA 生态&#xff0c;模型选型、第三方库、训练框架全都默认往 CUDA 靠&#xff1b;一换到 AMD 的 ROCm&…

作者头像 李华
网站建设 2026/10/14 8:35:53

C++列表初始化的双重路径解析与工程实践

1. 为什么列表初始化值得专门讨论&#xff1f; 第一次在C代码里看到 vector<int> arr{1,2,3} 这种写法时&#xff0c;我和许多初学者一样以为这只是花括号版本的构造函数调用。直到某天调试时遇到一个诡异问题&#xff1a; vector<string> strs{10, "hi&qu…

作者头像 李华
网站建设 2026/10/14 8:34:30

AnyPS5项目安全合规性说明与信息补充指引

我无法基于当前输入生成符合要求的博文。原因如下&#xff1a;项目标题 "AnyPS5" 缺乏必要上下文&#xff1a;该标题本身是一个模糊的命名组合&#xff08;"Any" "PS5"&#xff09;&#xff0c;未指明其所属领域&#xff08;是软件工具&#xf…

作者头像 李华
网站建设 2026/10/14 8:34:13

Agent Skills实战指南:如何为大模型打造可复用的技能体系

坦白讲&#xff0c;第一次看到“agent-skills”这个词&#xff0c;是在某次内部技术分享的草稿里。当时团队正在为一个大模型对话系统增加“让模型自己决定调哪些工具”的能力&#xff0c;试来试去发现最麻烦的不是模型不会调&#xff0c;而是我们不知道该把什么能力“交给”模…

作者头像 李华
网站建设 2026/10/14 8:33:31

AnyPS5:PS5相关开源工具与跨平台管理方案解析

项目标题是“AnyPS5”&#xff0c;但当前输入中未提供任何实质性内容&#xff1a;没有项目正文、没有关键词列表、没有摘要描述&#xff0c;网络搜索部分为空白&#xff0c;相关热搜词与最新网络热词也未给出有效信息。 作为一名从业十余年的资深博主&#xff0c;我必须坦率说…

作者头像 李华
网站建设 2026/10/14 8:33:20

C++面试高频数据结构与算法考点:链表、二叉树与排序题精讲

说句实话&#xff0c;数据结构与算法这块&#xff0c;几乎是C面试里最没法临时抱佛脚的部分。前三篇聊了语法、面向对象和内存&#xff0c;到了这一篇&#xff0c;考察方式完全不一样&#xff1a;不是问你“知不知道”&#xff0c;而是让你当场写、当场讲、当场证明。我见过不少…

作者头像 李华