
最近在重构手头的一个Agent项目时我把原先散落在Prompt模板和工具函数里的各种能力统一收拢成了一个起名很朴素的模块——agent-skills。这个名字背后是一个很直接的想法既然Agent本质上是“大模型工具记忆”的组装体那它对外表现出来的每一项本领都应该被当作一个可管理、可评估、可迭代的“技能”来对待而不是一段一段飘在提示词里的玄学。这篇文章就来聊聊我在agent-skills上的完整落地过程为什么要做技能库、技能怎么拆才能既通用又不含糊、注册和调用的工程链路怎么设计、以及最重要的——如何让这套体系在真实业务里越用越顺手。内容适合正在做Agent开发、或者准备把原型Agent推向生产环境的朋友看完可以直接抄走一版能跑的架构思路。1. 为什么Agent需要一套“技能库”而不是更多提示词1.1 从一次翻车现场说起先说个让我下决心重构的翻车案例。当时我在做一个办公助手类Agent需求是让它帮忙处理邮件、整理会议纪要、查排期。第一版很简单一段系统提示词加上五六个Python函数就把核心链路跑通了。Demo演示给同事看效果不错。问题出在场景扩展到20多个以后。今天加一个“查天气”功能明天加一个“生成周报”功能所有能力都塞进同一个system prompt里。提示词越来越长模型开始顾此失彼它会在不该调用工具的时候强行调用也会在用户问“明天下午三点会议室还有没有空”这种很直接的问题时兜一大圈先去查了日历API、又去翻了联系人列表最后才反应过来要查楼层平面图。更麻烦的是排错。某个技能出问题时你根本说不清问题到底出在提示词描述不清晰、工具参数传错了、还是模型上下文被其他技能描述挤占。整个系统变成一个黑盒改一句Prompt可能让A场景变好、B场景直接崩掉。这个局面的本质是我把“能力”和“提示词”绑定在一起能力越多提示词越臃肿模型的决策空间被不相关信息污染得越严重。我需要的不是继续往Prompt里堆描述而是把这些能力拆出来变成一份份独立的、可被模型按需发现的技能清单。1.2 提示词堆叠与硬编码调度的三大死穴把Agent的所有能力都揉在提示词里至少有三个绕不开的问题第一上下文窗口被无效信息持续占用。模型每轮推理都要把完整的技能清单读一遍哪怕这轮只需要其中两个技能。窗口被占满之后要么强行截断历史要么牺牲长对话能力两样都伤体验。第二能力边界模糊。人话描述再怎么精炼模型还是可能误解“这个技能到底在什么条件下该用”。比如你写了一句“当用户提到会议室时调用会议室查询技能”用户说“帮我找个小会议室”模型可能纠结到底该不该触发因为“会议室”这个词出现了但意图其实是“预订”不是“查询”。第三灰度迭代基本不可能。改了某个技能描述等于改了整份Prompt影响面是不可控的。你无法精确回答“这次改动到底影响了哪些对话”只能靠回归测试碰运气。硬编码调度则是另一个极端。把技能调用逻辑写成if-else或者规则引擎虽然可控性上来了但代价是Agent失去了灵活性——用户换一种说法规则没覆盖到能力就暴毙了。而且每加一个新技能都要写一套匹配规则维护成本线型上涨场景一多就变成天坑。1.3 技能库到底管的是什么agent-skills想解决的问题就是在“提示词堆叠”和“硬编码调度”之间找一个中间态技能是独立描述、独立注册的但技能的选择和执行交给模型配合一个轻量级的路由层来兜底。具体来说它管四样东西技能的元信息这个技能叫什么、干什么、什么时候该用、参数结构是什么样的技能的执行体真正干活的代码或API调用逻辑技能的上下文声明哪些信息必须提前拿到哪些信息可以现场推断技能的评估结果它过去接了多少次调用、成功率多少、平均耗时多少。这套思想其实跟微服务很像——把单体拆成服务服务之间有契约、有注册中心、有调用链追踪。Agent的“技能”就是微服务里的“服务”只不过消费方从别的程序变成了大模型。2. 技能拆解把任务域切成一棵可复用的技能树2.1 技能的标准五要素在agent-skills里每个技能都要按统一的Schema注册。我的做法是五要素缺一不可要素作用设计要点技能名称唯一标识用动词名词结构比如schedule_meeting技能描述给模型看的“招聘广告”说清楚干什么、什么时候用、什么时候不用参数Schema调用契约用JSON Schema定义入参越精简越好执行逻辑真正干活的部分一段函数、一个API封装或一条工作流触发条件路由兜底规则可选用于在模型选错时做强制修正技能名称和参数Schema是给代码看的技能描述是给模型看的这两者经常被混为一谈后面我会专门讲这个坑。2.2 一个实战案例消息助手怎么拆成6个原子技能拿一个很常见的“消息助手”需求举例。这个Agent要做的事是理解用户用自然语言描述的消息意图调用通讯工具发消息、拉群、查未读数汇报发送结果。一开始我把它做成一个巨大的process_message_request函数参数里有五六个可选字段。结果模型经常漏填参数要么没传接收人要么消息内容带了Markdown符号去发到纯文本IM。拆完以后变成6个原子技能resolve_recipient从“给产品组的张三发消息”里解析出通讯录中的具体账号get_group_info根据群名关键词查群ID和成员列表send_text_message发送纯文本消息send_rich_message发送带格式或附件的消息check_unread_count查询某个会话的未读数summarize_send_result把发送成功、失败、部分失败的结果整理成一段话回复用户。拆完之后模型在大部分场景下会自动按顺序调用先resolve_recipient再send_text_message最后summarize_send_result。每个技能的入参都只有两到三个字段漏传和错传的比例大幅下降。这个案例说明一个道理技能拆得越细单次调用的认知负担越小模型越不容易犯错。但拆得过细也有问题后面会讲。2.3 技能的粒度定多细才合适我自己的经验是用两个标准衡量粒度一是入参数量不超过5个二是技能描述能在一句话内说清楚。如果某个技能的描述需要三句话还说不明白说明它承担了不止一个职责继续拆。反过来如果几个技能总是被模型连续调用而且调用顺序几乎固定比如resolve_recipient后面永远跟着send_text_message那就可以考虑合并成一个复合技能send_message_to_recipient减少模型做多余决策的次数。这里有个平衡原子技能拆得太碎模型可能为了完成一个简单的任务频繁发起多次调用每一次调用都有延迟成本和失败风险粒度太粗又会重蹈参数爆炸的覆辙。我的建议是先拆碎再根据实际调用日志合并用数据驱动而非拍脑袋。第一版agent-skills里我就犯过这个错——上来就拆了60个技能很多技能一个月都用不到两次白白增加了模型每次读取技能列表的干扰。3. 技能注册与调用链路的工程落地3.1 注册中心与技能Schema定义技能注册中心在agent-skills里承担的角色相当于一个“技能的菜单”。模型每次决策前系统会把菜单里的一部分技能描述连同它们的参数Schema塞进上下文让模型“知道这里有这些能力可以用”。菜单不能全量塞。60个技能全塞进去跟上一种所有能力写进Prompt没有本质区别。所以注册中心要支持技能分组和按需加载。我的做法是给每个技能打标签比如“通讯”“日历”“数据查询”模型先按用户意图锁定一个标签组再从这个组里选具体技能。技能Schema我用JSON Schema格式存储它的好处是既能做运行时校验又能直接转成模型的Function Calling参数结构。以Python环境为例定义如下{ name: send_text_message, description: 向指定的联系人发送一条纯文本消息。适用于用户明确要求发送文字信息且接收人已解析成功的场景。, parameters: { type: object, properties: { recipient_id: { type: string, description: 接收人在通讯录中的唯一标识需先通过 resolve_recipient 获得 }, content: { type: string, description: 要发送的纯文本内容长度不超过2000字 } }, required: [recipient_id, content] } }这里有两个细节值得提。第一description里必须写明“需先通过 resolve_recipient 获得”这等于告诉模型技能之间的依赖关系避免它拿一个原始人名直接塞进来。第二参数要用object而不是裸字段因为大部分Function Calling协议都要求结构化对象后续做扩展也方便。3.2 技能选择器的两条路径规则路由与嵌入检索模型模型不一定每次都能选对技能所以agent-skills里还有一个技能选择器作为兜底。它跑在模型决策之前做两件事第一件事是粗过滤。根据用户当前消息用关键词规则或者一个轻量级意图分类模型快速排掉明显无关的技能组。比如用户消息里出现了“开会”“会议室”但没有任何与“发消息”相关的词就把通讯组过滤掉只留给模型会议相关技能。这一步能把候选技能数量从几十个压到五六个模型的选择准确率会高很多。第二件事是预检索。当技能数量超过一定规模之后靠标签分组还是太粗糙我直接用Embedding把每个技能的描述向量化用户消息也向量化算余弦相似度召回Top-K个技能。这个方案在技术选型上很常见实际效果也稳定。但要注意不能把嵌入召回当作唯一选择机制。模型Function Calling在“候选技能少且描述清晰”时表现得已经很好嵌入检索只是缩小候选范围最终拍板权还是交给模型。这样设计的目的只有一个减少模型的决策负担而不是取代模型的判断。附上一个典型的调用流程图逻辑用户消息进入 → 技能选择器先做粗过滤/嵌入召回 → 候选技能列表(连同Schema)拼接到上下文 → 模型决策调用哪个技能 → 执行体运行 → 结果返回给模型 → 模型生成最终回复这条链路里每个环节都有功能够独立测试和替换。想换召回模型只改选择器想升级某个技能的参数Schema只动那一个技能的注册内容。3.3 执行引擎的工作流与超时保护技能的执行体五花八门有纯函数、有HTTP调用、有数据库查询甚至有需要多步协调的工作流。agent-skills里我把它们统一包装成一个执行接口输入是已经解析好的结构化参数输出是一个标准结果对象执行结果对象 - status: success / failed / timeout - data: 实际返回的数据 - error: 错误信息如果失败 - latency_ms: 耗时执行引擎最重要的任务是超时保护。Agent场景里模型调用技能是同步等待的技能如果长时间不返回用户的对话体验会直接崩掉。我的经验是默认超时设为5秒复杂技能可以单独配置为15秒但对大多数原子技能来说5秒已经是上限了。超时后怎么处理也很关键。我见过不少项目在超时后直接抛异常让模型一脸懵地告诉用户“出错了”。更好的做法是把超时当作一种结果返回给模型让模型基于已有信息尝试备选方案。比如send_text_message超时了模型可以告诉用户“发送没有确认成功我帮你再试一次或者你先检查一下网络” —— 这比冷冰冰的“服务异常”舒服得多。另外执行引擎要支持技能重试。对于幂等的技能比如查询类超时后自动重试一次没问题但对于有副作用的技能比如“发送消息”重试可能造成重复发送。所以幂等性评估在注册技能时就要做并在技能元信息里标注是否允许自动重试。3.4 观测性每个技能的调用都要留痕没有观测性的Agent系统等于开盲盒。我在agent-skills里强制要求每一次技能调用都必须记录一条完整的日志包含这样几个字段用户消息原文脱敏后候选技能列表和最终选中的技能入参和出参调用耗时、是否超时、是否重试模型最终生成的回复。这些日志的价值远不止排查故障。它们是后续技能评估和迭代的原材料我在下一节会展开讲。如果没有这些留痕技能改得好不好就只能靠感觉这是生产环境绝对不能接受的。4. 评估与迭代让技能库越用越聪明4.1 离线评估用历史对话做技能召回率测试技能库建好之后最怕一件事模型压根不知道该在什么时候调用某个技能。要防住这个问题就得做召回率测试。我从线上日志里抽出一批历史对话每条记录都标注了“当时期望调用的技能”是什么。然后我做一个离线脚本把用户消息喂给技能选择器看它能不能在Top-K里召回正确的技能。召回率低于90%的技能要么描述写得不够清晰要么候选过滤太激进要么就是技能本身和其他技能语义太接近需要合并或重新切分。这个评估方法类似搜索系统的召回率评估。技能描述就是索引里的文档用户消息就是查询目标是把正确的技能排到候选列表里。我会在代码里加一个简单的评估脚本定期跑一遍用召回率数字监控技能库的整体健康度。# 伪代码示意技能召回率离线评估 def evaluate_recall(test_cases, skill_selector, top_k5): hit 0 for case in test_cases: candidates skill_selector.retrieve(case.user_message, top_ktop_k) if case.expected_skill in candidates: hit 1 return hit / len(test_cases)这个数字不求一上来就是100%但每次调整技能描述后它的变化趋势能告诉你改动方向对不对。4.2 在线观察从日志里找出技能调用的异常模式离线评估看的是“能不能想起这个技能”在线观察看的是“用起来顺不顺”。我在日志里特别关注三个指标第一个是调用成功率。低于95%的技能必须立即查原因大概率是参数Schema设计不合理或者执行体本身有Bug。第二个是平均调用耗时。超过3秒的技能要警惕它们会拖累整条对话的响应速度。第三个是调用后的纠错频率——这是我最看重的指标。如果某次调用之后模型紧接着又调用了另一个功能或重复提交说明上一个技能的结果大概率没让模型满意。还有一种更隐蔽的异常模型绕过了正确的技能用更笨的方式完成了任务。比如我明明提供了check_unread_count但日志显示模型一直先调用get_group_info再自己推断未读数。这说明技能描述里没有说清楚“什么时候该用它”或者它和其他技能的边界模糊。遇到这种情况我会调整描述明确写出“当用户直接询问未读消息数量时应首先考虑此技能而非查询群信息”。4.3 技能版本与灰度发布改技能不能靠手感技能库是活的。今天调一个描述明天改一个参数如果每次改完都全量上线风险太高。我自己的项目里后来加了简单的版本机制每个技能都带version字段修改技能时新增一个版本旧版本保留。线上运行时可以做到按用户比例灰度——比如新版本先放10%的流量观察调用成功率没有下降再逐步放大到50%、100%。灰度期间的对比指标主要看三个调用成功率、平均耗时、最终回复中用户正向反馈的比例。如果新版本在10%灰度期这三项都不输旧版我才会全量推。这套机制初期可以做得很简单不需要引入复杂的配置中心一个数据库表加一个配置文件就够用了。但“可以简单”和“不做是两回事”——版本机制能让每一次技能改动都可回溯、可回滚这是技能库能不能持续演进的地基。5. 踩坑记录agent-skills里最容易翻车的三个细节5.1 参数Schema设计得太严Agent直接罢工我在做日历技能时一开始给create_event定义了8个必填参数包括event_type、location、reminder_minutes、attendee_list等等。上线后发现一个头疼的问题模型经常在用户没有明确说全信息的时候直接编造一个默认值比如顺手把地点填成“线上会议”。这是因为模型为了满足必填参数约束强行“补全”信息。更糟糕的是这些补全的信息用户根本没确认过Agent就在错误的信息上执行了操作。解决方式参数 Schema 遵循“最少必要”原则。必填参数只保留真正不能缺的比如事件的开始时间其余全部设为可选并在描述里明确“如果用户未提供请主动询问不要擅自假设”。这样模型在信息不足时会更倾向于追问而不是自作主张。5.2 技能复用与冗余的平衡相似技能越来越多技能库建到一定规模后会出现一个尴尬的现象为了适配不同的说法你可能会建好几个功能几乎一致的技能。比如cancel_meeting和delete_calendar_event本质上干的是同一件事只是触发场景不同。技能冗余会直接破坏召回准确率——模型在候选列表里看到两个语义高度相似的技能很容易选错而且选错后排查难度特别大。我后来定了一条规矩新建技能前必须搜索现有技能库如果发现语义相似度超过某个阈值优先扩展现有技能而不是另起炉灶。扩展的方式也很简单在已有技能的description里增加一句适用范围比如在delete_calendar_event的描述里补充“包括用户说‘把会议取消’、‘把日程删掉’等场景”。这样技能还是同一个覆盖场景变宽了模型的选择负担没有增加。5.3 权限和沙箱技能执行边界不清会出事这是我最想强调的一点而且是教训换来的。当Agent技能开始调用真实的API、写入真实的数据时权限模型必须跟技能一起设计。比如消息助手能发消息那它能不能在未经确认的情况下自动发送日历技能能建日程那它能不能删掉别人的日程我在早期版本里放过这个错——为了演示方便所有技能的执行体都跑在一个高权限服务账户下。结果有一次测试中模型理解错了意图把一个“帮我看看明天会议安排”的请求执行成了“发送一封邮件通知参会人会议取消”。虽然是在测试环境也足以让我惊出一身冷汗。现在的做法是给每个技能挂一份权限声明它能访问哪些资源、能执行哪些操作、哪些操作需要用户二次确认。执行引擎在调度前会做一次权限校验对需要确认的操作先把结果透传给模型让模型向用户提问而不是闷头执行。这个设计在技术上不难但必须在技能库成型之前就定下来否则后面补会很痛苦——因为补权限意味着要重新梳理每一个技能的执行边界比一开始就考虑要费事得多。5.4 别把技能库做成“提示词仓库”最后再说一个方向上的坑。有人会把agent-skills理解成“把一段段优化好的Prompt存起来按需取用”。这个理解是错的。Prompt和技能的本质区别在于Prompt是“说给模型听的”技能是“让Agent去做的”。技能必须有可执行体、有输入输出契约、有权限边界、有观测日志。哪怕某个技能当前实现仅仅是“让模型输出一段话”它也应该被包装成有Schema、有权限声明、有评估记录的完整技能而不是一段裸的提示词文本。只有把技能当成一类“一等公民”的对象来管理你才能享受到复用、灰度、评估这些工程能力。否则你只是换了个地方堆提示词系统该脆弱的还是脆弱。写在最后如果你也在做Agent开发我的建议是别等到场景多到失控了再做技能化改造。从第一个能力上线开始就用agent-skills这套思路把它拆出来、注册好、挂上日志。前期会慢一点但到了20个技能以上的阶段你会庆幸当初没把所有东西堆在提示词里。回看整个落地过程我最大的体会是Agent技能的工程化本质上是把“模型的灵性”和“系统的确定性”结合到一起。技能库管住边界和契约模型在边界内自由发挥。这两者配合好了Agent才真正从一个demo玩具变成可交付的产品。