资讯动态

Agent技能管理实战:从提示词堆砌到可复用技能库

发布时间:2026/10/13 16:09:41 来源:尧图企业网站定制
agent-skills 这个名字看起来像一个开源仓库的命名其实是我在一连做了四个智能体应用之后被逼出来的一套技能管理方案。最开始我也以为把系统提示词写得足够长模型就能完成所有事情结果上下文越堆越长指令之间开始互相矛盾同一个功能换个说法就触发不了。后来我把所有能力拆成一个个独立、可复用的 skill也就是 Agent 技能整个调用链路才变得可控制、可 debug、可回归。这篇文章就围绕 agent-skills 这套思路讲讲技能到底是什么、技能文件怎么写、执行链路怎么搭以及我在实际操作中踩过的一组坑。如果你正准备给大模型应用增加工具调用、想让智能体稳定完成多步任务或者已经被“提示词写了一大堆但模型就是不按套路执行”折磨过那这套东西大概率能帮到你。它不是某个特定框架的插件而是一套通用方法论把能力抽象成技能把技能描述成模型能看懂的“名片”然后由一个轻量执行器负责校验、调度和兜底。1. 项目整体思路先别急着写工具先把技能抽象出来我见过很多项目的第一步是直接撸代码写各种 API 调用函数然后一股脑塞给模型。这种做法的优点是启动快缺点也很明显没过多久你就会被工具数量淹没。一个智能体如果有二十个函数每个函数的描述都写得很随意模型在决策时就会陷入“多选一”的混乱经常把 A 工具的参数传给 B 工具。1.1 为什么要做技能化拆分可以把每个 Agent 技能想象成工具箱里的一把专用扳手。普通提示词是“你是一个会修水管的人你需要用几种方法把漏水点处理掉”而技能是“这是一把 16 号扳手用于拧开 16 号螺帽如果螺帽生锈请先喷除锈剂”。模型最擅长的不是记住冗长规则而是在清晰的选项面前做选择。把能力拆成技能化模块最大的收益是隔离复杂度。每个技能有自己独立的描述、输入输出约定、示例、边界条件和测试用例。改一个技能不会影响其他技能调试时也能精准定位是哪个环节出了问题。相比之下把所有指令都揉进系统提示词里一旦出问题你根本不知道是语气不对、顺序不对还是描述歧义。1.2 技能与普通提示词的本质差异拿“让 Agent 查天气再给穿衣建议”这个需求来举例。普通提示词可能写“如果用户问天气就去调用天气接口然后结合温度给出建议”。听起来没问题但模型可能会在用户说“今天冷不冷”时完全不知道要查天气。技能化之后你会有一个名为“查询城市天气”的技能它的描述里明确写了“当用户询问温度、体感、降水、风速等当前气象信息或问‘冷不冷’‘要不要带伞’时使用”。维度普通提示词Agent 技能粒度多任务混在一起单一职责一个技能只做一件事触发方式依赖模型从长文本中推理有显式 description 和触发条件参数靠模型猜或从对话里找有明确 input_format 和校验规则复用性换一个项目基本作废复制到新项目稍改配置即可测试难以单独验证可以对每个技能做独立回归版本管理无技能目录本身就是版本单元这张表基本说明了 agent-skills 的底层逻辑把“如何做”和“何时做”分开。技能实现负责“如何做”技能描述负责“何时做”。模型只读描述执行器只跑实现两者通过标准化接口衔接。1.3 设计取舍声明式技能描述加轻量执行器我没选择把技能逻辑直接写死在主程序里而是用了“声明式配置 执行器”的结构。每个技能文件夹里放一份人类可读、模型也可读的说明文件再加一份实现代码。执行器启动时扫描技能目录读取所有说明文件拼接成一份工具清单交给模型。这么做的好处是扩展新技能时不需要改主程序只需要新增一个文件夹。某个技能出了问题可以直接把文件夹摘出来单独调试。坏处是会增加一点启动时的读取和校验开销但对绝大多数场景来说这点开销完全可以忽略。更重要的是它让“技能”从一个抽象概念落地成了文件系统里看得见摸得着的实体团队协作时每个人负责自己的技能目录互不干扰。2. 技能文件的编写规范一张让模型少犯错的“名片”技能文件是整个体系里最容易被低估的部分。很多人写函数注释非常详细却忽略了一个事实模型在做工具选择时基本只读你给的简短描述而不是完整读一遍函数源码。所以描述写得不好代码写得再漂亮也没用。2.1 元信息字段解析我用的技能说明文件长这样为了便于解析直接用 Markdown 加固定段落# 技能名query_city_weather ## description 当用户询问某个城市当前或未来几天的天气情况包括温度、体感、降水概率、风力、空气质量或问“冷不冷”“要不要带伞”时使用。 当用户只是闲聊天气好坏不涉及具体城市和具体时间时不要使用本技能。 ## when_to_use - 用户提供了城市名并希望了解该城市天气。 - 用户询问“某地今天多少度”“明天会下雨吗”等具体气象问题。 ## input_format 输入必须为 JSON 对象 { city: 城市中文名或行政区划名, date: YYYY-MM-DD默认为今天 } ## output_format 返回 JSON 对象包含字段 - temperature: 摄氏度数值 - condition: 晴天/多云/雨天等 - wind_force: 风力等级 - advice: 一句由模型后续生成的穿衣或出行建议不在本技能内生成 ## examples 输入: {city: 杭州, date: 2026-05-20} 输出: {temperature: 26, condition: 多云, wind_force: 3} ## constraints - 如果城市无法从用户表达中确定必须明确询问不得猜测。 - 如果日期缺失默认使用当前日期并在返回结果里标注使用了默认值。第一行的# 技能名是注册索引用的。description 标题严格写成小写是为了让解析器好识别。description 里最关键的是最后一句“什么时候不要用”这是很多同学容易漏掉的部分。模型在选择工具时经常被相似问题误导给出一个负面声明能显著降低错误调用率。2.2 示例与边界条件只给对示例是不够的技能文件里的 examples 不是摆设。模型并不会像人一样去读完整规格说明书它更擅长从示例里“领悟”模式。但我见过很多技能文件只给了一个理想输入输出对结果模型遇到边界情况时就自由发挥。我现在的习惯是至少给两个示例一个标准情况一个边界情况。标准情况教它正确调用边界情况教它何时不能调用。比如上面的天气技能可以再加一个负面示例## negative_examples 输入: {city: null, date: null} 处理方式: 不要调用先向用户确认城市。在给模型做 few-shot 时负面示例比在 description 里写一百字“如果城市为空请询问用户”更有效。因为模型看过具体反例后面对相似输入会产生更强的约束记忆。这个经验是我在做了几轮盲测后得出的结论数据也从侧面验证了这点加上负面示例后天气技能的空参数猜测率下降了接近一半。2.3 技能库的目录与命名规范技能文件放哪儿、叫什么名字直接决定了后续自动化扫描的复杂度。我推荐的目录结构skills/ ├── query_city_weather/ │ ├── SKILL.md │ ├── main.py │ └── tests/ │ └── test_cases.json ├── calculate_expression/ │ ├── SKILL.md │ ├── main.py │ └── tests/ │ └── test_cases.json └── send_email_draft/ ├── SKILL.md ├── main.py └── tests/ └── test_cases.json命名统一用“动词_目标对象”的结构比如 query_city_weather而不是天气接口、weather_api 这种模糊名称。动词让模型知道这个技能能做什么目标对象让模型知道它针对什么。SKILL.md 放在每个技能目录根下执行器扫描时只认这个文件名实现代码统一叫 main.py测试用例用 JSON 文件离线管理。每个技能尽量保持单一职责。如果一个技能文件里既有查天气又有计算穿衣指数建议拆成两个一个负责数据获取一个负责推理生成。这样后续可以做独立的单元测试也能让模型按需调用不会出现为了拿温度而把整个文案生成流程都跑一遍的情况。3. 从零落地一个 Agent Skills 技能库说完了设计直接进入实操。我会用一个最常见的“城市天气查询与穿衣建议”场景带你从头走一遍技能库的建设流程。这里不依赖任何商业框架只用一个基础的 Agent 运行时加两个 Python 文件重点是让你看清楚每个环节在做什么。3.1 需求拆解与技能划分首先不要急着写工具。把用户需求拆成三层意图识别、数据获取、内容生成。意图识别由模型完成它需要根据用户的话判断该调用哪个技能。数据获取适合做成技能比如查询城市天气它应当是确定性的、有输入输出的。内容生成同样适合做成技能但它更多依赖模型本身不适合做成硬编码接口。所以我拆出了两个核心技能query_city_weather负责根据城市名和日期返回结构化天气数据。generate_weather_advice负责接收天气数据生成穿衣、出行建议。前者是典型的工具型技能后者是典型的模型型技能。拆开的好处是如果以后要做一个“雨天自动推送提醒”的新应用可以只复用 query_city_weather而不用被迫带上整套文案生成逻辑。3.2 技能注册表与调度器技能注册表的核心功能是扫描目录、解析 SKILL.md、汇总成模型能读的工具清单。以下是一个简化实现from pathlib import Path import re def parse_skill(path: Path) - dict: text path.read_text(encodingutf-8) title_match re.search(r^# 技能名(.)$, text, re.MULTILINE) desc_match re.search(r^## description\s*\n(.?)\n^## , text, re.MULTILINE | re.DOTALL) return { name: title_match.group(1).strip(), description: desc_match.group(1).strip(), } def build_agent_manifest(skills_dir: str) - str: lines [] for skill_file in Path(skills_dir).glob(*/SKILL.md): skill parse_skill(skill_file) lines.append(f- 技能名: {skill[name]}) lines.append(f 说明: {skill[description]}) return \n.join(lines)这段代码做了三件事遍历技能目录、提取技能名和描述、拼装成标准清单。拼装出来的清单会放入系统提示词或是喂给模型的工具选择模块。每次新增技能只需要在目录里加一个文件夹主流程一行都不用改。如果技能数量超过十个我会把清单拆成两级。先给模型一份所有技能的“一句话摘要”模型初步判断可能需要哪个技能后再把对应技能的完整 description 注入上下文。这个技巧能有效避免技能描述把所有上下文都吃光。3.3 技能实现的工程细节技能实现的基本要求是只做一件事、返回结构化结果、失败时返回明确错误码不能让异常把整个对话打断。我用一个简单装饰器来统一处理注册和超时import json import timeout_decorator def skill_register(func): # 注册到全局技能表 SKILLS[func.__name__] func return func skill_register timeout_decorator.timeout(5) def query_city_weather(params: dict) - dict: # 参数校验 city params.get(city) date params.get(date, ) if not city: return {error: missing_city} # 这里假设通过某个 HTTP API 获取天气 raw fetch_weather(city, date) if raw is None: return {error: upstream_failed} return { city: city, temperature: raw[temp], condition: raw[cond], wind_force: raw[wind], }装饰器把技能注册到一个全局表执行器会在模型决定调用后从表里取出对应函数。超时装饰器是个保险丝防止上游接口卡死导致整个智能体响应卡死。另一个重要细节是输出格式。我统一要求所有工具型技能返回 JSON而不是返回一段自然语言。因为自然语言会被模型当成“答案”直接展示给用户而 JSON 会让模型知道这只是“中间数据”还需要后续处理。这个约定不写进代码里而是写进 SKILL.md 的 output_format 字段让模型在生成调用结果时也能保持清醒。3.4 回环测试与描述调优技能写完后不要急着接进业务系统。我的习惯是先做一轮离线回环测试准备若干条用户表述让模型判断应该调用哪个技能然后对比实际调用结果和期望结果。举个例子测试问题可以是用户表述期望技能模型实际选择“杭州明天多少度”query_city_weatherquery_city_weather“今天要不要带伞”query_city_weathergenerate_weather_advice“帮我写一段明天出行的提醒”generate_weather_advicequery_city_weather我实际跑下来最常见的错误就是模型把“要不要带伞”这种隐含天气信息的问题直接丢给建议生成技能而没先去查天气数据。解决方法是在 query_city_weather 的 description 里增加触发词“要不要带伞、冷不冷、会不会下雨”同时在 generate_weather_advice 的 description 里写清楚“该技能只接收已经获取的天气数据如果对话中还没有任何天气信息必须先调用查询天气技能。”经过两轮调优这类问题的命中率能从六成提升到九成以上。4. 踩坑实录技能调用中的常见问题与排查方法再完备的设计也躲不过实际问题。这一节把我真实踩过的坑整理成速查表并且会告诉你每个问题最关键的排查顺序。问题现象常见原因排查顺序解法建议技能死活不触发description 与用户表达语义距离太大先看模型实际收到的工具清单增加同义词触发词和负面示例技能调用了但参数明显是编的输入格式描述不清晰模型在猜测检查 input_format 和 examples明确缺失参数时“必须询问”并在执行器加校验多个技能都可用模型随机选择技能边界重叠描述互相抢任务检查各技能 description 首句给技能增加优先级说明或拆掉重叠描述上下文被技能描述塞满技能数量太多全量注入过长统计注入字符数使用两级加载先摘要后详情技能升级后老任务失败新描述覆盖了旧场景回归测试集是否更新维护 versions 目录跑测试集再切换模型在技能失败后反复重试同一个错误异常返回不允许模型转向检查失败时模型看到的反馈让技能失败时返回“可替代方案”或“建议询问用户”4.1 技能死活不触发这是最让人抓狂的问题。你明明看到工具清单里已经有那个技能模型却像瞎了一样绕开它自己编一段答案出来。大多数情况下问题不在模型而在描述里缺少“用户可能说的人话”。比如你的技能叫 query_stock_price描述里写“查询股票价格”用户可能说的是“帮我看看某只股票最近怎么样”。这里的“最近怎么样”并不等同于“价格”模型自然无法高置信度地调用。我的做法是去翻历史对话里真实出现的用户表达把高频说法提炼成触发词写进 description。比如加上“最近走势、现价多少、涨了还是跌了、收盘价”等。4.2 参数幻觉与错误填充模型在缺少参数时不会主动说“我不知道”而是倾向于自己编一个看起来合理的值。这是大模型的通病。比如城市名没提取出来它可能填一个全国最著名的城市然后返回一个错误天气。解决这个问题需要三层防线。第一层是技能描述里写明“如果参数不确定必须向用户确认”。第二层是在输入格式里明确标注哪些字段是必填、哪些可默认。第三层是执行器入口做严格校验发现 city 为空就直接返回错误提示而不是带着空值继续处理。我个人的经验是前两层解决的是“模型态度问题”第三层解决的是“底线问题”。即使模型态度不好底线也能兜住。否则一旦脏数据进入下游错误会被放大排查时更头痛。4.3 上下文膨胀与技能数量控制当技能数量超过十五个时每个技能的描述都有一两百字全量注入系统提示词会让上下文变得很臃肿。模型处理无关信息的能力有限过多技能描述反而会冲刷真正重要的任务指令。我的应对策略是把技能拆成两个层级。第一层是索引摘要每个技能只留一行话比如“查天气根据城市和日期返回天气结构化数据常用触发词包括温度、下雨、带伞”。第二层是详细描述只有当模型根据摘要初筛认为相关时才在下一轮请求中注入该技能的完整内容。这种方法牺牲了一点响应速度但换来了更高的决策准确率。4.4 版本冲突与回归测试技能不是静态文件它会随着业务需求迭代。我犯过的错误是直接修改某个技能描述后上线结果老用户反馈“之前能完成的任务现在不行了”。后来我定了一个规范每个技能目录下保留一份 test_cases.json里面存着历史关键用例每次改描述或实现都要先跑一遍测试集全部通过才允许切换。[ { input: 上海明天冷不冷, expected_skill: query_city_weather, expected_params: {city: 上海} }, { input: 我在北京需要提醒我周五带伞, expected_skill: query_city_weather } ]这套测试集其实并不需要复杂的测试框架一个脚本读 JSON 后调用模型做离线判断即可。它最大的价值是防止“改一处毁一路”。4.5 安全边界与执行器约束给模型提供工具接口时安全边界必须提前划好。我见过的反面案例是直接把删除数据库、执行任意 shell 命令等能力暴露给模型结果在一次测试中模型因为理解偏差执行了高风险操作差点酿成大祸。我的安全底线是技能实现里只做白名单操作。任何带有不可逆后果的能力都必须在代码层写死限制而不是指望模型自觉。例如只允许查询操作不允许更新和删除只允许读白名单目录不允许扫盘所有外部请求必须走统一代理并做超时控制。技能描述里可以写“该技能只读”但代码里也要真正只读。5. 进阶从单技能调用到技能编排与多智能体共享当你把十个、二十个技能都建好之后会发现真正的难点不是单个技能而是技能之间的协作。一个复杂任务往往需要多个技能按顺序或分支执行这就是所谓“技能编排”。5.1 技能编排的三种基础模式我目前用到的编排模式有三种都不复杂但很实用。第一种是串行管道前一个技能的输出直接作为后一个技能的输入。比如先查天气再把天气数据交给建议生成技能。这种模式要求每个技能的输出格式高度规范我在前面反复强调返回 JSON就是为了这里的顺畅衔接。第二种是条件分支根据模型对用户意图的判断决定走哪条技能路径。比如用户说“帮我定一个明天出差的提醒”系统需要先判断是否需要查询天气再判断是否需要写提醒内容。条件分支通常写在主控制器的路由规则里而不是写死在技能内部。第三种是并行聚合多个技能同时调用最后汇总结果。比如用户问“对比北京和上海的天气”可以同时跑两个 query_city_weather 实例再把结果送给建议生成技能。并行调用能大幅缩短响应时间但要注意控制频率避免滥用外部接口。5.2 多智能体共享技能库技能库的另一个好处是天然适合多智能体复用。一个团队可能有客服智能体、内容助手、数据分析助手等如果每个智能体都从零写一套工具调用维护成本会直线上升。我现在是把技能库放到一个统一目录不同智能体通过配置文件选择自己需要加载哪几个技能。比如客服智能体只加载订单查询、物流查询、退款规则三个技能数据分析助手只加载数据库查询和报表生成两个技能。这样既能保持各自上下文精简又能让技能本身被多业务复用。这种模式对权限管理提出了更高要求。如果同一个技能被不同智能体使用实现本身必须是通用的不能写死某个业务逻辑。另外不同智能体的安全等级可能不同我建议在技能注册表里增加一个“允许使用角色”字段防止低权限智能体调用了高风险技能。5.3 从个人实践出发的几点体会如果你现在正打算搭建自己的 agent-skills 体系我建议不要一上来就做一个全自动的总控调度器。先把三到五个最核心的技能做扎实每一份 SKILL.md 都认真打磨跑通一次完整的“意图识别到技能调用到结果返回”链路。调度器在总控逻辑完全可以后面再慢慢加。遇到问题时先别急着调整模型参数或换更大的模型。多数技能调用失败根源都在技能描述与用户真实表达的匹配上。把问题用户表述记下来分析它和现有描述之间的语义距离然后针对性补触发词或负面示例这比反复调温度参数有效得多。另外技能库的维护是一项长期工作。我每隔一段时间就会整理一次历史失败案例把它们变成新的边界测试用例。这套做法的好处是随着测试集越来越厚技能库的鲁棒性会越来越强新需求接入时也有据可依。agent-skills 真正解决的不是“让模型更聪明”而是“把不确定的模型行为变成确定的工程行为”。技能描述是写给模型看的产品说明书技能实现是写给系统看的可靠服务二者的边界清晰了智能体应用才不会越做越乱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑