从提示词堆砌到技能层:Agent技能库设计实战

发布时间:2026/10/13 7:58:52
从提示词堆砌到技能层:Agent技能库设计实战 最近在整理手头的Agent项目时我突然意识到一个重复踩了很多次的坑几乎每个项目里都在写一遍类似的“能力接线”代码。调用外部接口、查数据库、算指标、发通知……这些功能换个项目就得重新接一遍而且全堆在提示词里提示词越来越长模型越来越容易犯傻。所以我抽时间做了一个技能库性质的小项目名字就叫agent-skills。它做的事情很简单把智能体要用到的每个能力封装成一份带契约的“技能”统一注册、统一调度、统一执行让Agent只负责说“我要干什么”不负责操心“具体怎么干”。这个方案特别适合那些需要反复迭代、要接一堆工具、还要让不同Agent复用能力的个人开发者或小团队。下面这些内容不是产品文档是我从0到1搭建、又在实际任务里反复翻车后总结出来的实操经验。如果你也在折腾Agent希望这篇能帮你少走一些弯路。1. 为什么我单独搞了一个“技能层”从提示词堆砌到可复用技能的转变1.1 之前Agent项目里最痛的三个问题先说最痛的三个问题。第一个是提示词膨胀。早期我做一个内部客服知识库Agent时把所有小工具都描述在提示词里查订单要怎么写、退换货要怎么写、算运费要怎么写……提示词写到三千多字以后模型开始出现幻觉经常把“查询订单”的接口当成“退款”接口来调。因为描述信息太多模型自己也分不清哪句话对应哪个动作了。更麻烦的是每次新增一个能力提示词又要继续膨胀系统整体稳定性肉眼可见地下降。第二个问题是逻辑与工具混杂。当一段业务规则要改的时候我既要改提示词里的描述又要改后面处理数据的函数两边必须同步改漏一个就出事故。有一次我改了运费计算规则只更新了代码函数忘了更新提示词里的描述结果模型还在按旧规则给用户报运费线上被投诉了好几次。第三个问题是扩展性极差。新接一个技能我得重新梳理整段提示词还要测试旧技能有没有被“挤掉”每次上线都像拆炸弹。项目之间也没法复用A项目里写好的查天气逻辑到了B项目要再复制一遍代码再改一遍提示词所有苦工从头吃一遍。这段经历让我意识到Agent真正缺的不是“更多的工具”而是一层能把工具整齐管起来的东西。我把这层东西叫技能层。1.2 技能层的定位大脑、手和筋腱的分工后来我把架构调整成“大脑-技能层-工具”三层。大脑就是LLM负责理解任务、拆解步骤、决定调用哪个技能技能层是中间目录登记了所有能力并且定义好调用契约工具则是技能层背后的具体实现可能是HTTP接口、Python函数也可能是SQL查询。我习惯把这层关系比喻成神经和肌肉的联动机制。大脑不用去想每块肌肉要怎么收缩它只需要说“我要拿起杯子”技能层负责把这个意图翻译成精确的肌肉动作序列。放到Agent里就是模型不用知道“查订单”背后调的是哪个接口、返回字段长什么样它只需要知道“有一个技能叫get_order填一个订单号就能拿到结果”。这层抽取最关键的价值是把“模型能理解的描述”和“机器能执行的代码”解耦开。描述归描述代码归代码中间只通过一份契约交互。这样模型升级了不用改工具工具换了实现也不用动提示词。1.3 两种方案对比全塞提示词 vs 独立技能层我整理了一张对比表给团队和身边做Agent的朋友看基本一看就明白差别对比维度全塞进提示词独立技能层新增能力改提示词还要担心破坏其他能力写一个skill文件注册即用调试问题只能反复改提示词调对话看技能日志问题隔离在单个技能跨项目复用基本不可复用每项目重写一遍技能目录直接引用换配置即可权限控制极难做所有能力对模型可见metadata里声明权限标签失败回退模型只能瞎编或重试超时、重试、降级都能收口在技能层可测试性只能做端到端对话测试每个技能可以单独加单元测试当然不是说所有东西都要技能化。如果某个能力极其简单、稳定、无副作用比如“把用户输入的文字转成大写”直接写在提示词里就好。技能化的重点对象是那些有副作用、需要外部资源、带有专业业务逻辑的能力比如发邮件、扣库存、生成报表、调用第三方接口。2. 一份能被智能体真正读懂并执行的技能契约skill五要素拆解2.1 描述为什么是命根子技能层最核心的资产不是代码而是那份技能契约。代码写得再漂亮如果模型看不懂这个技能什么时候该用一切都白搭。我最早犯的错就是把技能描述写得像函数注释比如“获取天气数据”。后来发现模型非常容易被这种“底层数据操作感”的描述误导它会觉得这不适合直接回答用户于是绕去找其他看起来更通用的技能。后来我把描述改成了“当用户想了解某地当前天气、气温、降水情况时调用本技能城市名必填查询新闻、百科信息请勿使用本技能”命中率立刻上来了。所以我对description的要求是写清楚用途、适用场景、触发条件以及最重要的“不要用它来做什么”。后半句特别管用能帮模型排除掉大量误选。2.2 五要素与一个最小例子一份skill文件里目前我固定放五个字段name、description、input_schema、execute、metadata。字段作用关键注意点name唯一标识小写英文下划线不要带空格和点号description模型选择依据用途适用场景不适用场景input_schema参数契约用JSON Schema注明必填项execute执行体入口Python函数、HTTP端点、Shell脚本均可metadata运行属性超时时间、版本、标签、权限范围一个最小例子长这样{ name: send_email, description: 发送一封邮件给指定收件人。适合用户要求通知、汇报、发送报告等场景。收件人必须提供合法邮箱地址。不要用它来处理即时聊天消息。, input_schema: { type: object, properties: { to: { type: string, description: 收件人邮箱地址, example: userexample.com }, subject: { type: string, description: 邮件主题 }, body: { type: string, description: 邮件正文 }, cc: { type: array, items: { type: string }, description: 抄送邮箱列表非必填 } }, required: [to, subject, body] }, execute: { type: python_function, entry: skills.mail.send }, metadata: { version: 1.2.0, timeout: 30, tags: [mail, notify], permissions: [internal-user] } }特别注意input_schema里我加了example字段。这个字段模型非常买账它在生成参数时会参照你给的示例格式极大地减少格式错误。2.3 参数设计让模型不乱传参的几招技能契约里参数设计是最容易翻车的地方。模型生成参数时经常会出现该传字符串的传了对象、必填字段漏传、日期格式不对。我总结了几招给参数设置枚举值而不是开放文本。比如“查询周期”这个参数与其让模型自由填一个字符串不如限定[today, week, month]模型犯错空间直接小一半。必填字段控制在3个以内。超过5个必填模型漏填的概率会显著上升。能设默认值的都设默认值把“必填”留给真正不可缺的字段。对自由文本参数给格式示例。日期写2025-06-01金额写数字两位小数给一次样例胜过十句说明。服务端必须做强校验。不要只靠模型自觉execute函数里第一件事就是校验参数非法参数直接返回明确错误让模型看到错误信息后自行修正。我自己早期的教训是某次技能参数里有个query字段模型传了一个JSON对象而不是字符串我后端没做校验直接把对象拼进SQL当场报错。后来所有技能执行前都过一遍JSON Schema校验器这个问题基本绝迹。3. 技能注册与路由决策怎么让Agent在几十个技能里选对那一个3.1 目录结构按领域分组自动扫描技能一多管理就成了问题。我现在用一套很朴素的目录结构agent-skills/ registry/ index.yaml basic/ send_mail/ skill.json execute.py search_docs/ skill.json execute.py workflow/ generate_report/ skill.json execute.py runner/ loader.py router.py executor.py加载时自动扫描目录下所有skill.json构建{name: skill}映射同时把description做一次关键词倒排索引。为什么要按目录分组因为后续要做权限隔离和命名空间管理basic和workflow分组正好对应技能分级后面详细说。启动时全量扫描会带来一个问题技能多了以后加载会变慢。我的做法是加一个registry/index.yaml缓存文件记录每个技能目录的相对路径和文件哈希只有文件变更时才重新加载对应技能。这样上百个技能也能秒级启动。3.2 两层路由召回 决策当技能数量超过20个以后把所有技能的description全部塞给模型做选择准确率会明显下降。模型不是不会选而是“选择面太宽”的时候会出现随机漂移跟人从三十个选项里挑一个容易眼花是一个道理。我采用的是两层路由召回阶段基于用户任务文本用关键词倒排索引或者向量相似度把所有技能缩小到5个以内的候选集。决策阶段把候选技能的name和description连同用户任务描述一起交给LLM由模型做最终选择。这个流程相当于先让图书管理员按分类找到书架再让读者在5本书里挑效率高很多。向量召回我是用了一个轻量级的文本编码模型来做的但如果你不想引入太多依赖纯关键词倒排配合同义词扩展也能对付大多数场景。核心原则是召回宁可多不可漏决策不能太宽。3.3 冲突消解与幂等控制技能一多还会遇到“两个技能看起来都能干这事”的冲突。比如我之前同时存在create_ticket创建工单和create_order创建订单两个description都写了“创建一个新的XX记录”模型经常串场。我做了三件事解决冲突给每个技能加priority字段业务上更常用或更精准的技能优先级更高并列时优先选高优先级。命名上避免同义动词。比如统一用create_表示“创建类操作”但后面的对象名要足够差异化并且description开头就声明“这是一个XX领域专用技能不处理其他领域请求”。必要时加alias字段把用户可能的同义说法映射到正确技能而不是为了不同叫法重复新增技能。幂等控制也很重要尤其是发邮件、扣库存、创建订单这类有副作用的操作。我用request_id skill_name params_hash做去重键同一会话内相同请求只执行一次避免模型因为网络原因重复调用造成重复扣款或重复发信。这个看似不起眼的细节救过我好几次。4. 最小可用实现技能加载器、执行器与主循环怎么串起来4.1 模块划分技能层整体代码不大我现在分成了五个模块loader负责扫描目录、读取skill清单、动态导入execute函数。validator负责校验输入参数基于JSON Schema做强制检查。router负责召回候选技能并把候选列表交给LLM做最终决策。executor负责真正执行技能统一处理超时、异常、重试。tracer负责记录每一次调用的完整链路日志。这里的核心设计点是executor产生的执行结果必须标准化。我统一返回{ok: true/false, result: ..., error: ...}结构这样Agent拿到结果后不需要猜直接看ok字段判断是否成功失败时把error信息放回上下文模型可以基于错误信息自我修正。4.2 加载器与执行器关键代码加载器的核心逻辑很简单动态导入execute函数import importlib import json from pathlib import Path def load_skill(skill_dir: Path): manifest json.loads((skill_dir / skill.json).read_text(encodingutf-8)) module_path, func_name manifest[execute][entry].rsplit(., 1) module importlib.import_module(module_path) func getattr(module, func_name) manifest[func] func return manifest def scan_skills(root: Path): skills {} for manifest_path in root.glob(*/skill.json): skill load_skill(manifest_path.parent) skills[skill[name]] skill return skills执行器统一包装超时和异常from concurrent.futures import ThreadPoolExecutor, TimeoutError def run_skill(skill, params, request_id): timeout skill[metadata].get(timeout, 30) with ThreadPoolExecutor(max_workers1) as executor: future executor.submit(skill[func], **params) try: result future.result(timeouttimeout) return {ok: True, result: result} except TimeoutError: return {ok: False, error: timeout} except Exception as exc: return {ok: False, error: str(exc)}ThreadPoolExecutor包一层时间限制是处理外部接口卡死最省事的方案。单技能最长执行时间可以按业务场景配置普通查询类我一般给15秒带外部文件生成的任务给60秒。4.3 接入Agent主循环技能层和Agent主循环的交互是每一步决策的核心。简化后的伪代码如下# 1. 用当前用户任务文本做技能召回 candidates router.recall(user_task) # 2. 把候选技能描述注入给LLM做最终决策 decision llm.select_skill(candidates, user_task) # 3. 按选中技能的schema提取参数 params llm.extract_params(decision.skill, user_task) # 4. 执行技能并标准化回填 result executor.run(decision.skill, params, request_id) conversation conversation.append_result(result)这里有个容易被忽略的细节extract_params这一步不是让模型自由地传入任意JSON而是我们决定好schema后把schema原样发给模型让模型按字段填充。之前我偷懒直接从用户原话里正则抽参数结果各种奇怪格式冒出来后来统一改成“模型读schema生成参数”准确率高了一个量级。4.4 日志和可观测性没有日志的技能层等于没有眼睛。我以request_id为主键把每一次技能调用记录成这样agent_request_id | skill_name | action | params_hash | status | duration_ms | model_token_usedaction字段标记这次调用是recall、select还是execute。这样一旦用户反馈“机器人答错了”我能从会话日志快速还原它当时召回了哪些技能、模型选了哪个、参数是什么、执行结果如何、耗时多少。这比让用户复述“它当时说了一句什么”靠谱一百倍。5. 实测翻车记录技能被无视、被误调、超时与排查链路5.1 技能被无视description写得太“内向”前面提过一次我第一个技能description写的是“获取天气数据”结果模型总在用户问天气时绕去调更通用的搜索技能。翻日志才发现模型认为“获取天气数据”是一个底层数据操作接口不适合直接面向用户回复。后来我在description里补上了完整的对话触发语境“当用户想了解某地当前天气、气温、降水情况时调用本技能城市名必填查询新闻、百科信息请勿使用本技能。”补完之后命中率从不到一半提升到了九成以上。这个教训让我养成了一个习惯description里必须写“什么时候用”和“什么时候一定不要用”只写功能说明是不够的。5.2 技能被误调兄弟技能描述太像导致串场还有一次线上事故是create_ticket和create_order串了。用户说“帮我建个投诉工单”模型调了create_order直接给用户生成了一个订单记录场面非常尴尬。问题根源是两个技能都写了“创建一个新的XX”语义上高度重叠。我的修复方案是给每个技能加明确的领域限定比如“本技能仅用于客户服务领域的工单创建不处理商品购买、订单生成等场景”。在路由决策前对候选技能做语义差异强化把两个相似技能的description同时喂给模型并在系统提示里注明“注意区分XX和YY的差异”。这个方法实测有效几乎所有相似技能串场问题都可以靠这个处理。5.3 超时与任务悬挂技能执行超时是最隐蔽的坑。某个内部统计接口偶尔会卡60秒用户问一句话等一分钟体验极差。更严重的是如果模型在等待技能返回时没有收到结果某些情况下会自己编一个结果继续对话这就变成数据污染了。我的解决方案是三步所有技能执行套timeout统一由executor收口。超时后返回结构化错误给Agent让Agent明确告诉用户“数据源暂时不可用”而不是报一个成功但内容是编造的结果。追加调用次数限制。同一轮任务里单个技能最多调用3次超过就停止防止模型因为失败反复重试形成死循环。5.4 标准排查链路一次问题定位的完整过程技能相关的线上问题我基本按下面这张表排查十分钟内能定位七八成问题问题现象第一步看什么再往下查什么技能没被调用会话trace里的候选列表看description与任务文本的匹配打分技能被误调路由决策日志看候选技能之间description的重叠度技能执行报错skill执行日志的error字段看参数快照与schema校验结果技能超时链路追踪里的duration_ms看外部接口返回耗时分布记得有一次用户说“机器人不回话了”日志显示模型连续三次调同一个外部接口每次都超时最后模型放弃一句话没回用户。排了很久才发现是外部接口的鉴权token过期了返回值永远是一个缓慢的报错。从那以后我把“外部依赖健康检查”也放进了技能层的启动流程里所有技能依赖的接口都要在启动时做一次连通性探测探测失败直接标记技能为不可用状态不进候选列表。这样模型永远不会选到一个必然失败的技能。6. 把技能库从“能跑”做到“好用”分级、反馈与隔离6.1 技能分级基础、组合、工作流技能数量超过三十个以后我开始做分级。目前分三级基础技能原子操作比如发邮件、查数据、算运费一个函数搞定。组合技能编排多个基础技能的流程比如generate_report内部会依次调用get_data、format_table、send_mail但它对外仍然是一个技能。工作流技能带有状态和人工确认环节比如“订单异常处理”这种需要多轮确认、可能中途改判的复杂流程。组合技能的价值在于Agent不需要一次性把四五个步骤想清楚它只需要选择generate_report这个组合技能执行器内部把子流程跑完即可。这也降低了模型做长链路规划时的失误率。组合技能本身也注册成skill只是它的execute不是一个普通函数而是一个子调度器内部走同样的路由和执行逻辑。6.2 反馈闭环从误用中自动修描述技能库光有静态登记是不够的它还需要能从误用中学习。我的做法是维护一份corrections.json记录每一次“模型选错技能”的案例包括用户原始描述、模型选的技能、应该选的技能。每天跑一次脚本自动分析纠错案例把“千万不要用于XX”的补充说明追加到对应技能的description里。举个实际例子用户说“帮我订明天早上的闹钟”模型误选了create_calendar_event建日历日程。纠错日志记录后脚本在create_calendar_event的description末尾自动加了一句“本技能不处理闹钟设置请求闹钟设置请调用create_alarm”。一个月跑下来这类误选率能下降一半以上。这个思路成本极低但对长期维护价值巨大。6.3 多Agent共享与权限隔离最后聊一下多Agent场景。不同Agent需要的技能集合完全不同比如客户服务Agent需要查订单、提交工单内部运营Agent需要看报表、发周报。所有技能放在同一个目录里直接共享会带来越权风险。我现在的做法是每个技能在metadata.permissions里声明可见范围每个Agent实例有一个角色标签路由召回阶段就过滤掉该角色无权调用的技能而不是到执行阶段才拦截。这样面向外部用户的Agent永远看不到后台管理类技能也不大可能被提示词注入诱导去调用权限之外的接口。这套权限机制也给技能复用加了安全边界。同一个技能库可以由多个项目共享但每个项目看到的技能清单是它自己那个子集。我在内部两个模拟项目上试过一个客服对话项目一个运营日报项目共用同一套技能层但技能可见范围完全隔离互不干扰。这正是技能层从“能跑”到“好用”最关键的一步。最后分享一个小习惯我会在每个技能目录里放一个CHANGELOG.md技能新增、描述修改、参数变更都记录一句。因为技能库最坑的不是写第一个技能而是三个月后你改了一个技能的description另一个Agent的决策明显变了如果没有变更记录你根本不知道是哪里引起的。把agent-skills当成所有Agent项目的地基之后我的工作方式变成了先列业务要哪些能力再写一遍技能契约然后才调模型。步骤看起来多了一步但后续维护省了十倍的心。希望这套经验对正在折腾Agent的你也有用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询