ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills实战指南:从Prompt和工具函数到稳定技能包的设计与落地

Agent Skills实战指南:从Prompt和工具函数到稳定技能包的设计与落地 最近半年我一直在折腾一件事把Agent里那些经常重复、又总不能稳定复现的能力从Prompt和工具函数里捞出来整理成一个独立的技能包。这个动作在社区里有个名字叫agent-skills但我发现很多人对这个词的理解其实不太一致。有人以为是写更多提示词有人以为是封装一堆API调用还有人觉得是某种新框架。我自己是从工程实践里摸过来的。最初只是给一个内部知识库助手做增强后来发现真正难的不是“让模型跑通”而是“让同一种能力在不同场景下稳定复现”。比如会议纪要整理同样的输入格式今天输出是Markdown明天可能就变HTML让模型自己发挥它能把无关内容也总结进来。试过调Prompt、加Few-shot、甚至微调效果都不持久。最后逼着我换了一种思路把“会议纪要整理”这件事做成一个有边界、有参数、有输入输出规范、有校验逻辑的技能包让Agent像工具箱里抽专用扳手一样去调用它。这一换问题一下子清爽了。这篇文章我尽量讲实操。你会看到我为什么放弃堆Prompt和堆工具函数的写法也会看到一个技能从设计到落地的完整过程包括目录结构、路由策略、示例样本设计以及我在测试中拿到的真实对比数据。适合正在做Agent应用、维护了一堆Prompt已经感觉吃力的开发者。如果你只是想了解概念前两节也能帮你建立一套清晰的判断框架。1. Agent Skills到底是什么别急着写代码先把概念掰开揉碎1.1 从一次重复劳动说起我先描述一个大概率你也遇到过的场景。你开发了一个Agent最开始只有三四个功能查天气、订会议室、写周报。这时候很轻松每个功能就是一个函数Prompt里写清楚什么时候调谁就行。但业务量上来之后需求变成了把会议录音转成待办、把周报按部门风格改写、从长文档里抽取结构化字段、给不同角色生成日报摘要。于是你的代码里开始多出一堆“看起来差不多但又不完全一样”的函数。我当时就是这么被拖垮的。内部助手的工具函数一度超过60个每个函数都有使用说明但Agent经常在“该用A函数时选了B函数”因为函数的描述写得太像了。Prompt也越来越长为了告诉模型每个函数在什么场景下用我不得不把大量业务背景塞进System Prompt里。结果模型越来越“话痨”经常在不需要工具时也硬调甚至自己编造参数。后来我把几个高频能力单独摘出来做成技能包才慢慢明白一个道理Agent Skills并不是什么全新架构它只是在Prompt、工具函数之上增加了一层“用说明书 边界约束 示例样本 校验逻辑”的封装。这个封装让Agent不需要猜只需要选择让开发者不需要把每个细节都塞进上下文只需要维护一份技能定义。1.2 分清Prompt、Tool和Skill很多文章把这三个概念混在一起讲导致不少人以为Skill就是结构化Prompt或者Skill就是函数装饰器。我在项目里给团队画过一张很朴素的表现在放在这里应该能帮你快速建立分辨框架。维度PromptTool工具函数Skill技能本质自然语言指令可执行的函数/API可复用、可路由、可验证的能力单元载体文本代码结构化定义 代码 示例 校验解决的核心问题告诉模型“怎么说”提供模型“能做什么”的入口明确模型“什么时候做、按什么标准做、做错了怎么兜底”可测试性弱改动后容易影响全局可单测但无法约束模型调用姿势强可对路由、调用、输出做独立验证可组合性差容易互相污染中等依赖开发者手动编排强多个技能可串成流程维护成本随数量上升暴涨中低边界清晰后各改各的一句话总结我自己的理解Prompt是给模型看的说明书草稿Tool是给模型用的手Skill是包含了“什么时候伸手、怎么伸手、伸完手要检查什么”的完整动作包。Skill可以包含Tool也可以完全不依赖Tool它甚至可以是一个纯推理流程比如“把一段非结构化内容整理成公司规范的四级文档结构”。1.3 技能包的标准长相我估计你更关心的是一个Skill文件里到底装了什么。我目前维护的技能库每个技能都包含这样几个部分能力描述一段短文本说明这个技能擅长处理什么、不擅长处理什么主要用于路由匹配。输入参数定义用JSON Schema描述参数名、类型、必填性、取值约束。输出定义明确返回结构通常也是JSON方便下游程序直接消费。策略说明给模型看的推理指令包括处理步骤、口径标准、不允许做的事情。示例样本至少一组“输入 - 中间思考 - 输出”的完整示例用来约束模型的推理路径。校验规则用代码实现的检查逻辑比如字段是否齐全、日期格式是否合法、有没有出现幻觉字段。有了这六样东西一个技能才算完整。前五样决定模型能不能正确调用最后一样决定调用后能不能被信任。我在项目里最看重的其实是最后一样因为模型输出最大的问题不是“写不出来”而是“写得很流畅但全是编的”。后面的章节我会拿一个具体例子把这些组件逐个拆开。2. 为什么我放弃“万能Prompt”和“堆工具函数”三个结构性问题2.1 上下文越长模型越抓不住重点我见过很多人给Agent写Prompt越写越长恨不得把公司所有业务规则都灌进去。体感上你会觉得信息都给全了模型总该会了吧事实是模型确实“看”到了但注意力是有限的。当System Prompt超过一定长度模型对后面补充规则的遵循度会明显下降尤其是与中间部分互相矛盾的规则。我在日志里观察到过一个典型案例。Prompt里写着“处理报销单时如果金额超过5000元必须转人工审核如果低于5000元可直接通过”同时后面又写了“所有报销单都必须经过财务助理确认”。模型在处理6200元的报销单时一会儿选择直接通过一会儿又转人工完全没有稳定逻辑。问题不在于模型笨而在于规则之间没有优先级、没有边界模型只能在概率上“猜”。如果把“报销审批”做成一个技能这些规则就应该被整理成明确的步骤先判断金额区间再走对应分支同时把“财务助理确认”定义为另一个技能而不是全局规则。Context里只需要留一行字“如需审批报销单请调用报销审批技能。”模型不需要理解全部规则它只需要知道什么时候调用技能。2.2 工具函数没有“使用说明书”Agent经常瞎调只堆工具函数的问题是另一个极端。你的代码会被拆得很干净但模型侧感知不到这种干净。模型看到的只是工具名和描述。当你有50个工具每个描述都差不多模型根本分辨不出它们的边界。更麻烦的是工具函数本身没有人会告诉你“什么参数不应该出现”。我记得一个失败案例我们的助手有一个“查询员工信息”的工具输入参数是员工姓名。另一个工具叫“查询最近会议记录”输入参数是时间范围。某次用户问“小张上周开过哪些会”模型居然先调了员工查询工具把返回的部门ID当作参数传给会议查询工具。代码层面两个函数都没有问题但模型缺少一层“把用户意图翻译成正确调用序列”的约束能力。Skill其实是在这中间垫了一张“使用说明书”。它不只是描述函数还带上了边界声明、示例、调用前置条件、错误输出规则。模型看到的不再是孤零零的函数而是一套完整的使用方案。哪怕模型仍然会犯路径上的错误校验规则也能在输出阶段把它拉回来。2.3 技能化带来的结构性改变我并不认为Prompt和Tool应该被丢掉它们仍然是底层语言。技能化的价值在于它给系统增加了一层结构让三个原本被忽略的问题变得可解。第一是可路由。技能定义里清晰的描述可以让一个负责任的路由模块来判断当前任务该调用哪个技能而不是把几十个工具全部塞给模型。第二是可测试。每个技能可以独立构造测试集改动一个技能不影响其他技能。第三是可组合。一个技能可以调用另一个技能比如“生成周报”技能内部会调用“拉取本周提交记录”技能和“格式化Markdown”技能但它们之间的匹配关系由开发者控制而不是全凭模型临场发挥。这种结构性的改变本质上是在给Agent做“职能拆分”。过去一个Agent要同时扮演调度员、执行者、质量检查员现在我们把这三个角色拆到了系统层面模型只负责执行它最擅长的那部分推理任务。3. 手把手设计一个技能以“会议纪要One-Pager”为例3.1 第一步明确边界和场景定义技能最忌讳一上来就写Prompt。我建议先写一句话场景声明。比如这个技能用于把一段会议记录或会议录音转写文本整理成一页纸的会议纪要。输入是原始文本和会议基本信息输出是包含会议主题、结论、待办事项、风险项四部分的JSON。它不负责发送邮件不负责自动创建任务不负责总结历史会议。写清楚边界有两个好处。第一路由匹配时你有明确依据第二写Prompt时你不会把所有相关能力都揉进来。很多技能做不到位就是因为边界没收住把“会议纪要整理”写成了“会议全流程助手”导致模型认知混乱。3.2 第二步设计输入与输出我每次设计输入参数都会问自己一个问题这个技能最少需要哪几个字段就能完成高质量输出少一个字段结果就会失真多一个字段就会让调用方和模型都无所适从。以会议纪要技能为例我的输入定义长这样{ meeting_topic: 会议主题字符串必填不超过100字, start_time: 会议开始时间ISO8601格式必填, end_time: 会议结束时间ISO8601格式必填, raw_transcript: 会议记录原文或转写文本字符串必填, participants: 参会人列表数组类型选填每个元素是人名, focus_areas: 重点关注领域选填例如[结论, 待办, 风险] }这里有两个容易被忽略的设计细节。一是时间格式一定要用ISO8601不要留自然语言空间否则下游解析全是坑。二是focus_areas看起来多余但实测中很有用允许调用方按场景收窄注意力比如老板关注的会议纪要只需要“结论和待办”这时模型不会花篇幅去写风险。输出定义同样用JSON Schema约束{ summary: 一段100字以内的会议摘要面向未参会读者, conclusions: [会议达成的明确结论每条不超过50字], action_items: [ { owner: 负责人姓名, task: 待办内容, due_date: 截止日期ISO8601格式或null } ], risks: [需要关注的风险点没有则返回空数组] }我坚持所有技能永远返回结构化JSON而不是自由文本。理由很简单Agent的下游动作需要解析结果。你让模型输出一段漂亮的Markdown接下来如果要自动创建任务还是得用正则去抽字段抽不出来的概率会让你崩溃。JSON虽然在人类阅读上不如Markdown但对系统来说是最稳定的接口。3.3 第三步写指令与示例输入输出定好之后才开始写给模型看的策略说明。这一步的核心不是“命令”而是“给出口径”。我会把步骤拆成三段式先读全文抓事实再按字段归类最后检查冲突。举个例子我的策略说明里有一段是这样写的请按以下步骤处理会议记录先通读全文区分“讨论过程”和“已经明确达成的结论”。讨论过程中的观点不要写入conclusions只有出现“我们决定”“就这样定”“没问题”等明确结论信号时才能写入。提取action_items时必须找到明确的责任人和动作不能从一般描述中臆测。如果原文提到“小王下周二前更新方案”owner是“小王”task是“更新方案”due_date是下周二对应的具体日期。如果同一件事在原文中出现多次保留最完整、最具体的那一次表达。真正让模型表现稳定的其实不是这些规则而是示例。我给每个技能至少准备两组样本一组偏普通一组偏反例。普通样本展示标准路径反例样本展示“看起来像结论但其实是讨论”的情况。示例里的中间思考不是越长越好能说清楚“为什么这么判断”就够了。3.4 第四步加校验和兜底校验规则是很多人忽略、但我认为最值钱的部分。模型输出的JSON经常会出现几种问题日期格式不合法、action_items里owner为空、conclusions和summary里出现原文没有的人名。这些问题单靠Prompt几乎堵不住但只要在代码里加几行校验就能把风险控制住。我一般会用一段独立的函数做输出校验逻辑不复杂但覆盖面要全def validate_meeting_summary(data): errors [] if len(data.get(summary, )) 200: errors.append(summary超过200字请压缩) if not isinstance(data.get(conclusions), list) or len(data[conclusions]) 1: errors.append(缺少conclusions字段或为空) for item in data.get(action_items, []): if not item.get(owner): errors.append(存在没有负责人的action_item) if item.get(due_date): # 这里可以继续校验ISO8601格式 pass if errors: # 可以返回错误提醒也可以触发一次“修复重试” raise SkillValidationError(errors)遇到校验失败怎么办我的办法是给模型一次修复机会把错误信息拼回去让模型重新产出通常第二次就能过了。如果第二次仍然失败就把这条样本收入人工复核队列并记录成测试用例。后面你维护技能时这些用例就是最宝贵的回归资产。3.5 我常用的设计检查清单经过这段时间折腾我总结出一份检查清单每次新建技能都会逐条过一遍这个技能是否只做一件事如果还能拆继续拆。输入参数能不能再少一个少掉会损失什么输出结构是否稳定下游是否能直接消费示例样本是否覆盖了普通场景和反例场景有没有校验规则检查输出中的事实性字段技能描述里是否写了“不擅长什么”是否有一组测试样本可以快速回归这份清单看着简单但能挡住绝大多数后期返工。4. 工程化落地让Agent在运行时找到对的技能4.1 技能仓库的目录结构设计好单个技能后更大的问题是怎么组织技能库。如果还是靠if-else硬编码调用那技能化就没有意义了。我的做法是把技能当作项目里的独立模块每个技能占一个目录目录内有定义文件、示例文件和校验代码。一个典型结构长这样skills/ ├── registry.json ├── meeting_minutes/ │ ├── skill.yaml │ ├── examples/ │ │ ├── normal_1.json │ │ ├── normal_2.json │ │ └── edge_case_1.json │ ├── validator.py │ └── README.md ├── weekly_report/ │ ├── skill.yaml │ ├── examples/ │ │ └── normal_1.json │ ├── validator.py │ └── README.md └── ...registry.json是整个技能库的索引文件记录每个技能的名称、路径、版本号、支持的语言类型、路由关键词。加载技能时先读注册表再按需导入目录而不是扫描整个目录这样能减少不必要的IO和依赖冲突。4.2 注册表与加载流程技能定义文件我用的是YAML因为它写描述时比JSON省很多引号。一个简化版如下name: meeting_minutes version: 2.1.0 description: 将会议记录或转写文本整理成结构化One-Pager纪要 适合内部项目会、周会、复盘会。不负责创建任务或发送邮件。 input_schema: type: object properties: meeting_topic: type: string max_length: 100 raw_transcript: type: string start_time: type: string format: date-time output_schema: type: object properties: summary: type: string conclusions: type: array items: type: string action_items: type: array items: type: object properties: owner: type: string task: type: string due_date: type: [string, null] routing_keywords: - 会议纪要 - 会议总结 - 周会记录 - meeting minutes allow_parallel: falserouting_keywords看起来只是普通列表但它在实际路由中非常有用。模型或规则路由可以先用关键词粗筛减少候选技能范围再做语义匹配两条腿走路比单纯靠向量匹配稳定得多。加载逻辑我写得很轻量。启动时读注册表构建技能ID - 技能对象的映射运行时按需加载。延迟加载的好处是技能多了之后不会撑爆内存也避免了某个技能里引用了不存在的库导致整个Agent启动失败。4.3 路由策略规则优先还是向量匹配这是我在群里被问得最多的一个问题。我的答案永远是能用规则解决的问题不要一开始就上向量。优先规则。把用户输入和routing_keywords做一次大小写不敏感的子串匹配匹配到的技能直接进入候选。如果匹配到多个再用一个迷你模型或规则做意图判别。如果规则匹配不到才走向量检索用技能描述和用户问题算相似度。我还会把“不擅长什么”也写进路由逻辑。比如某次用户问“帮我预订下周一的会议室”它可能会匹配到会议纪要技能里的“会议”两个字这时候技能边界里的“不负责创建会议或发送邮件”就派上用场了。我在加载技能时会把这个字段解析成负向过滤器只要用户输入命中负向关键词这个技能就会被直接排除。路由决策最好不要做得“非黑即白”。我见过不少方案非要选出一个技能但现实是用户问题经常横跨多个技能。遇到这种情况我倾向于返回一个技能列表让Agent主循环按列表顺序尝试而不是只给一个答案。这个策略让整体成功率提升了大概15%。4.4 与Agent主循环的接入点技能化之后Agent的主循环会变得非常薄。大致是这个流程先做意图分类判断是否命中技能命中的话走“技能选择 - 参数抽取 - 技能调用 - 输出校验”没命中才回退到通用对话或通用工具。参数抽取这个环节值得单独说。很多技能输入参数不止一个模型必须从用户原话里抽出来。我会把这个动作也交给模型但会提供一个“参数抽取模板”。模板里写明每个参数的来源方式比如“meeting_topic从用户问题里抽取抽不到就用‘未命名会议’”“start_time如果没提返回null并让用户补齐”。抽取完成后再用JSON Schema校验参数不合格就不调用技能直接进入澄清对话。这个设计避免了模型硬拿残缺参数去调用技能把大量错误挡在了执行之前。等于是给技能增加了一道“门禁”。4.5 上线前的沙箱验证技能在离线环境里跑得顺不代表线上也顺。我见过太多次模型在测试数据上表现完美一上真实流量就翻车因为真实对话里充满了省略主语、指代不清、口语化表达。我的做法是把每个技能都接到一个“影子模式”里跑一周。影子模式的意思是真实流量照常走但每一条请求都会把结果复制一份给Skill系统系统用它自己的路由和生成逻辑跑一遍输出只记录不返回。一周后对比两组结果确认技能的准确率和召回率都达标再正式切流。影子模式听起来多花一倍资源但它帮我提前发现了大量问题。有一次我甚至发现技能在50%以上的真实请求里都输出了虚假的owner字段原因是示例样本全部是正式场景忽略了口语里的“他说他会做”这种表达。如果没有影子模式这个问题可能要等到用户投诉才会暴露。5. 真实项目数据用与不用Skill的差别5.1 我是怎么对比的光讲概念容易空我拿一个自己项目里的真实场景来说明。我们的内部知识库助手有一个高频能力“把一段项目会议记录整理成结构化周报素材”。在技能化之前这个能力是靠一个超长Prompt加三个工具函数实现的技能化之后我把它拆成了一个Skill。对比周期是两周前一周用旧方案后一周切到技能方案。两组流量用的都是同一个底层模型唯一变量就是系统设计和Prompt组织方式。评测维度包括字段完整率、日期格式合法率、结论条目准确率、单次请求平均Token消耗、以及我维护代码的变更量。5.2 准确率从“靠运气”到“基本稳定”指标旧方案Prompt工具函数Skill方案必填字段完整率86.3%97.8%日期格式合法率79.4%98.6%结论条目准确率71.8%91.2%action_items负责人缺失率22.5%6.7%最让我惊讶的是结论条目准确率的提升。旧方案经常把讨论中的提议当成最终结论因为模型没有明确的“结论信号”标准。技能方案里示例加上规则之后模型学会了识别“我们决定”“先这样定”这类表达误判率大幅下降。这个提升我并不认为全部来自模型能力改变更多是技能方案把“对错标准”前置到了系统设计里。模型不需要靠猜去对齐预期它只需要照着标准执行。5.3 Token消耗省的不只是钱Token消耗这块结果比我预想的还要明显。旧方案为了稳定必须把大量规则、工具说明、历史示例全部塞进上下文技能方案把这部分移到了技能定义里上下文里只留精简的调用说明和抽取结果。指标旧方案Skill方案单次任务平均输入Token42001700单次任务平均输出Token1200950总Token消耗降幅-约49%Token省下来不仅是成本问题响应延迟也跟着降了。我在日志里看到旧方案单次任务平均要6.8秒技能方案降到了4.5秒左右。对用户来说体感就是“快了半拍”。再加上输出结构稳定后续解析代码也不需要写一大串兼容逻辑。5.4 维护效率改一处还是改十处旧方案时期每次业务规则变化我都要同步改Prompt、改工具说明、改后处理脚本三个地方只要漏改一处线上就会出诡异问题。技能化之后规则变化通常只影响技能定义文件或校验代码工具说明和Prompt不需要动。有一次我们把日期格式从“2024-01-01”改成“2024/01/01”旧方案要改6个文件技能方案只改了2个地方技能的示例样本和校验正则。而且因为每个技能有独立版本改坏了可以快速回滚不会牵连其他功能。维护效率是最难用数字衡量的但对我来说它可能比Token消耗更重要。6. 构建Agent Skills时我反复踩的坑和方法论6.1 技能粒度不是越小越好技能化最诱人的地方在于“拆”但拆得过度同样会出事。我最早把“写周报”拆成了“拉数据”“总结本周”“格式化输出”三个技能以为可以复用。结果Agent经常不知道该按什么顺序调用光是编排就得写一堆胶水代码效果还不如一个整合技能。我现在的基本原则是如果两个技能几乎总是被一起调用且调用顺序固定那就合并成一个技能。可复用性不是看“步骤有多小”而是看“在多少场景里能独立产生价值”。一个没人单独调用的原子技能再小也是死重量。6.2 示例不是越多越好要防止“示例污染”早期我给技能拼命加示例觉得示例越多模型表现越稳。但加到第20个之后我发现效果反而下降了。原因是多个示例之间风格不一致模型开始过度模仿某一条示例的格式甚至把示例里的具体人名、项目名代入真实输出。后来我控制每个技能最多放5到8个示例覆盖三类场景就够了标准场景、边界场景、反例场景。反例示例尤其重要它告诉模型“什么情况下不要输出结论”但反例不需要多两个足矣。示例之间必须保持格式一致性否则就是给模型制造混乱。6.3 别把技能做成“隐藏后门”权限边界技能化之后Agent的执行能力会大幅增强。一个“整理会议纪要”的技能如果内部调用了文件系统访问或者把结果写入某个数据库你必须确保这些动作都在技能文档里被明确声明。我在项目里出过一次事故一个内部演示技能从只读的“查询数据”功能被误改成了带写操作的版本结果测试阶段差点覆盖生产库。那之后我定了一条硬规矩技能定义文件里必须声明权限级别加载器在运行时强制校验没有声明写权限的技能一律以只读模式运行。权限边界这事不是危言耸听。当技能数量超过50个你根本无法靠人肉记住每个技能能做什么。让权限声明成为技能元数据的一部分并且由统一模块校验是必须的工程态度。6.4 没有回归测试技能就是定时炸弹最后一段时间我深刻体会到技能库最值钱的资产不是代码而是测试集。每次调整技能定义或示例都必须跑一遍回归测试否则你不知道这次改动有没有悄悄破坏另一个技能的表现。我的回归测试很简单每个技能目录下的examples/文件夹就是天然测试用例。加载器会读取这些JSON文件把输入交给技能执行然后断言输出满足Schema定义、必填字段不为空、日期格式合法。这些用例数量不大但能挡住绝大多数低级回归。6.5 我的技能验收清单现在每次新建或修改技能我都要求至少满足以下条件才算完成技能描述里有明确的边界声明包括“不负责什么”。输入参数不超过5个每个参数都有默认值或兜底逻辑。输出格式是稳定的JSON并且被完整写进Schema。示例样本至少包含一组反例。有独立校验代码至少检查必填字段和格式。路由关键词覆盖了中英文常见说法负向词也写清楚了。跑过至少5条回归用例没有破坏其他技能。这套清单不复杂但它帮我避免了很多线上翻车。Agent Skills这条路真正难的不是技术而是“把一个模糊能力固化成稳定接口”的耐心和纪律。现在我的技能库越用越顺手每次新增需求时先想能不能复用已有技能再决定是否新增整个系统的复杂度被控制在了可维护的范围里。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进