ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

从零搭建可复用的skills技能体系:智能体架构与工程实践

从零搭建可复用的skills技能体系:智能体架构与工程实践 多模态模型正从单独的技术能力走向完整的产品化组织方式。这篇围绕标题skills展开的项目复盘记录了我从零搭建一套可复用、可维护、可升级的 AI 功能体系的过程。核心关键词skills、智能体架构、提示词工程、功能编排、多模态交互。如果你也在做 AI 产品或者在自建 AI 工作流经常遇到功能像补丁一样越堆越乱、上下文越塞越杂、模型稍微升级就各种失灵那这篇内容就是冲着你来的。我踩过坑也把坑填了沉淀下来一套设计范式并且在真实项目里跑通了。这篇文章会把这套体系的思路、细节、实现过程、踩坑记录和排查方法全拆给你看适合正在做 AI 应用架构的开发者、AI 产品经理以及所有想让模型稳定输出复杂结果的工程实践者。1. 整体设计思路从“堆 Prompt”到“搭 Skill 体系”先说我之前的问题。早期做 AI 功能我习惯把所有要求写进一个巨大的系统提示词角色设定、背景数据、任务步骤、输出格式、限制条件、兜底策略全部塞在一起。结果很典型提示词超过几千字之后模型在中部内容的遵循度明显下降越靠后的规则越容易失效。更麻烦的是每次需求变化都要动整段提示词改一个点经常引起连锁反应别的功能莫名其妙地崩了。后来我接触到了skills这个概念核心变化就一句话不写巨无霸提示词而是把能力拆成可独立安装、独立调用的技能单元。从产品角度看一个 AI 功能就是一个技能它有名字、有说明、有执行步骤、有需要的资源和工具、有输入输出协议。模型自身只保留最基础的推理和对话能力具体领域的活儿通过挂载对应的技能来完成。我花了差不多三周时间把原来的单体提示词体系彻底推翻重做整个系统重构为一组 skill 模块。这一步走完立刻感受到几个实实在在的好处可组合性同一个技能可以被不同场景复用比如“数据清洗”技能既能用在报表生成也能用在知识库整理不需要重复写逻辑。可测试性每个技能独立验证输入明确、输出明确能单独写测试用例找出问题不再是抓瞎。可版本化技能文件就是普通文本和脚本可以放进 Git 管理改动和回滚都干净不像以前改提示词只能凭记忆。这个思路其实类比一下很好懂以前是让一个实习生背一本一百页的手册去做十件事结果是每件事都做得稀烂现在是把十个有明确分工的老员工放进团队每个员工只负责自己手里这件事各拿各的标准作业流程出了问题直接问责单一模块。这就是技能化的本质用结构化对抗不可控。在设计整套 skill 体系时我给自己定了三条原则这三条也是后续所有设计决策的根源第一职责单一切割。一个技能只解决一个问题宁可技能粒度小一点也不要做出一个什么都能干但是全都干不精的万能技能。第二显式输入输出协议。每个技能必须说清楚自己期望的输入格式和输出的数据结构没有协议的技能没法被编排。第三最少依赖优先。技能能不用额外脚本就不加脚本能少依赖第三方库就少依赖一个技能拉起来要能在任何环境里低成本跑起来。这套原则直接决定了后面的文件结构、接口设计还有调度逻辑。可以说整个项目从方法论层面就是围绕这三个核心约束长出来的。2. 核心细节解析技能单元的标准结构与工作机理2.1 SKILL.md技能的核心描述文件整个技能体系里最基础也最重要的文件是SKILL.md。它不是给模型吃的提示词那么简单的附属品而是整个技能的中枢控制文件。模型在执行任何技能前第一件事就是读取这个文件搞清楚这个技能到底是干嘛的、什么情况下用、操作步骤是什么、有什么约束要注意。我设计一个SKILL.md时一定包含六个部分缺一不可技能名称一句话说清技能用途比如“生成技术周报”“清洗用户反馈数据”。触发条件明确说明当用户输入遇到什么情况时应该调用本技能。这是编排器判断要不要调用技能的依据。执行步骤把任务拆成 5~10 步以内的操作序列每一步指令清晰、无歧义而且可以用脚本辅助的步骤尽量用脚本减少模型自由发挥的空间。输入要求说明需要的参数、字段以及字段的数据类型和格式示例。输出规范定义输出结构最好给出一个 JSON 示例或者 Markdown 模板。约束与边界列明技能不做什么、什么情况下该停止、什么数据不处理。写执行步骤的时候有一个重要细节不给模型自由发挥的机会。比如写“整理出本周重点”这种描述模型就会产生各种理解偏差但如果写成“从输入中提取本周所有状态为‘已完成’的任务按完成时间倒序排列输出前十条”模型的执行结果就稳定得多。我用一个很简单的例子说明 SKILL.md 的写法。我做过一个“会议纪要结构化”技能它的核心描述文件长这样--- name: meet-minutes-structurer description: 将一段原始会议记录转为结构化摘要输出议题、结论、待办 trigger: 用户提供会议记录并希望整理摘要或对话中出现“会议纪要”“会议记录整理” input: 原始会议文本支持纯文本或 Markdown output: json object包括 summary、items、actions 三个字段 --- ## 执行步骤 1. 将原始文本切分为句子级片段。 2. 使用规则脚本过滤无关话语寒暄、口头语、语气词。 3. 将剩余内容按议题聚类标记议题标题。 4. 每个议题提取结论句若无结论则标记 n/a。 5. 将所有行动项提取到 actions 字段标注负责人和截止时间。 6. 校验输出 JSON 完整性缺失字段用 null 补齐。 ## 约束 - 不输出主观建议。 - 不补全缺失信息。 - 议题数量超过 8 个时按时间顺序保留前 8 个。这种结构的 SKILL.md模型只需要做“执行者”不需要做“规划者”。它要做的事情被完全规定死了偏差自然被压缩到最小。2.2 辅助脚本与资源文件让规则代替概率光有文字说明还不够。模型毕竟是概率生成文字再精确也有随机性。为了进一步稳定输出我会把凡是能程序化处理的部分全部用脚本接管这就是技能目录里的scripts/和resources/。比如会议纪要技能里我写了一个很小但很关键的 Python 脚本作用就是把文本切句、过滤语气词、识别高频议题关键词并在最终输出前做 JSON schema 校验。import re import json def split_sentences(text): parts re.split(r(?[。!?]), text.strip()) return [p for p in parts if len(p) 1] def filter_noise(sentences): stop_words [嗯, 这个, 那个, 就是说, 对吧, 然后] result [] for s in sentences: if any(w in s for w in stop_words): continue result.append(s) return result def build_summary(sentences): # 简易聚类按句首关键词识别议题块 topics [] current None keywords [议题, 接下来, 关于, 重点讨论] for s in sentences: if any(k in s for k in keywords): if current: topics.append(current) current { topic: s.strip(:), content: [] } elif current: current[content].append(s.strip()) if current: topics.append(current) return topics if __name__ __main__: import sys text sys.stdin.read() sentences split_sentences(text) filtered filter_noise(sentences) topics build_summary(filtered) out { summary: ; .join(filtered[:5]), items: topics, actions: [] # 规则识别含“负责”“跟进”“完成”的句子 } for s in filtered: if any(a in s for a in [负责, 跟进, 完成, 截止]): out[actions].append({text: s, owner: unknown, deadline: unknown}) print(json.dumps(out, ensure_asciiFalse, indent2))脚本的价值不只是提效更关键的在于它给了 AI 一个“固定答案的地板”。模型负责理解脚本负责规则两边各干各擅长的事整体可靠性立刻上了一个台阶。脚本还有一类用途是资源查找。有些技能领域性很强比如医疗术语解析、特定行业法规查询这类技能会需要一个本地知识库或外部 API 查询脚本。技能触发时脚本检索出与当前问题最相关的知识片段把这些片段作为上下文提供给模型而不是让模型靠它训练数据里的模糊记忆。这种机制在处理强时效性信息时尤其有用。2.3 技能调度策略模型如何知道该用哪个一套技能体系说白了是一堆独立技能的集合但模型怎么从这么多技能里选出正确的那个我用的是“描述匹配 触发条件排序”的双通道机制。首先每个技能的描述文件里都有 trigger 字段这是给调度器看的。模型在进入任务前会阅读当前技能目录里的所有SKILL.md的 name 和 description 字段快速判断哪些技能与当前用户意图相关。这一步本质上是一次文本相似性匹配但注意模型不是在做数学计算而是做语义理解。因此 desc 写得越具体、越准确选错的概率就越低。这就是我为什么坚持每个技能只用一句话描述核心场景而不是写一段四平八稳的废话。其次调度器还维护了一个优先级表某些高频技能可以被配置为“默认考虑”状态比如用户对话中只要提到“整理”“总结”“汇总”就直接把整理类技能排在候选列表前列。这样能显著减少模型在几十个技能之间反复犹豫而产生的“什么技能都调一点、结果四不像”的情况。技能之间还会存在协同使用的场景。比如一个技能负责从原始资料中抽取结构化数据抽完后的结果要交给另一个技能做可视化分析。这种跨技能协作在架构上怎么处理我的做法很朴素在技能描述里显式写清楚“上游输出是什么格式、下游预期输入是什么格式”两个技能用 JSON 作为中间语言对接口。模型在编排时只要能读懂两个接口描述就能像拼乐高一样把它们拼起来。明确了调度策略后下一个要关注的问题就是技能的完整性校验。技能系统里最容易出现的问题是文件都放那儿了但内部结构不规范。模型读取时缺字段会直接跳过该技能造成隐蔽的失效。我开发期间就撞上过这种问题明明感觉某个技能配置无误调用时却毫无反应。后来给技能目录加了一个结构检查器每次保存配置时自动跑一遍字段完整性校验少了哪个字段、类型对不对、脚本有没有缺失一目了然。3. 实操全记录从零搭建一套多模态技能库理论部分讲得再多都不如把一套真实技能库的搭建过程完整走一遍。这一节我以一个具体项目为例当时我需要给一个内部工具做一个“多模态数据汇报”功能输入物是一组散乱的报销单据照片和 Excel 表格输出物是一份可以对外汇报的开支总结。整个过程我拆成四步环境与骨架准备、技能单元编写、技能加载与测试、整体联调。每一步你都可以照着操作。3.1 环境准备与技能库骨架我在一个已有的项目仓库里建立了skills/目录结构是这样的skills/ ├── expense-summarizer/ │ ├── SKILL.md │ ├── scripts/ │ │ ├── extract_receipt.py │ │ └── summarize_expense.py │ └── resources/ │ ├── category_map.json │ └── template_report.md ├── chart-builder/ │ ├── SKILL.md │ └── scripts/ │ └── build_chart.py └── registry.yamlregistry.yaml是技能注册表记录了每个技能的启用状态、版本号、入口脚本路径。当时想得很简单后续技能增多时所有技能信息集中到这里便于统一管理。实际跑下来这个文件非常有用尤其是排查“技能为什么不生效”的时候第一件事就是检查注册表里的条目。3.2 技能单元编写细节第一个写的是expense-summarizer。这个技能的目标是从各种原始资料中提取支出数据汇总成结构化结果。SKILL.md 我是这样写的name: expense-summarizer description: 从报销单据图片和表格中提取支出数据汇总为结构化开支表 trigger: 用户上传收据、发票、报销单、Excel开支记录并要求汇总、分析、制作报告 input: - 支持 jpg/png 图片内部通过 OCR 提取文字 - 支持 xlsx/csv 表格内部通过 pandas 读取 output: json_object: total: number categories: [{ name: string, amount: number }] top_expenses: [{ item: string, amount: number, date: string }] steps: - 调用 extract_receipt.py 提取图片文字 - 调用 summarize_expense.py 读取表格并合并数据 - 按 categories 聚合 - 生成最终 JSON constraints: - 金额单位统一为元 - 日期格式统一为 YYYY-MM-DD - 无数据的字段必须置为 null不得省略这里有几个细节值得单独拿出来说。第一我特别在 input 里写了“支持 jpg/png”“支持 xlsx/csv”这类细节原因是很多模型在判断文件类型时容易忽略 MIME 类型直接读字节流导致数据错位。把支持的格式显式列出来模型在调用时就不再需要猜测。第二constraints 里规定“无数据字段置为 null”这不是随便写的。JSON 输出中如果字段缺失下游程序跑起来会直接抛 KeyError脚本链路当场断掉。定好这个约束模型输出结构从一开始就符合代码的预期后面省了无数调试时间。脚本部分我给 OCR 和表格读取分别写了独立的工具函数。OCR 用了现成的开源库表格读取就是 pandas。这里最需要注意的点是要给脚本加上容错。比如 OCR 的图有些旋转了、模糊了、反光了直接识别就可能输出乱码。我在extract_receipt.py里做了图像预处理缓解一部分低质量输入的崩坏风险。import argparse import json from PIL import Image, ImageOps, ImageFilter def preprocess(image_path): img Image.open(image_path).convert(RGB) # 灰度化、增强对比度、适度降噪能显著提升OCR识别率 gray ImageOps.grayscale(img) gray gray.filter(ImageFilter.SHARPEN) contrast ImageOps.autocontrast(gray) return contrast def main(image_path): img preprocess(image_path) text ocr_engine.recognize(img) # 按所选OCR库的接口调用 return {raw: text} if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(image_path) args parser.parse_args() print(json.dumps(main(args.image_path), ensure_asciiFalse))注意我没有在脚本里写死某个 OCR 厂商的接口这是刻意为之。换不同 OCR 库时只有recognize这一行需要改动测试和替换成本被压缩到最低。3.3 技能加载机制把技能真正挂到模型上环境和技术栈不同加载方式差异很大。我这里用的是通用可控的编排方式技能的 SKILL.md 通过系统消息注入到对话上下文中脚本作为外部工具暴露给模型调用。具体来说就是启动时读取registry.yaml遍历注册的每个技能目录解析每个目录下的SKILL.md将技能描述和触发规则汇总成一份能力清单放进系统提示词模型的输出如果是特定本领的调用请求比如call_script: summarize_expense.py由编排器拦截并执行对应脚本把脚本的输出喂回对话。整个过程说得更形象一点模型像是一个总览全局的中层管理者它手里的能力清单就是一份团队成员简介当它判断某项任务需要某个成员动手时就发一个“工作请办单”出去脚本执行完后把成果汇报给它它再继续往下走流程。拦截机制需要一套清晰的中转协议。我之前用过很多不同格式最后锁定为 JSON。模型返回的 JSON 里包含tool_name和args两个字段编排器解析后调用本地函数再把结果以日志形式追加到对话上下文。这套协议简单、透明、容易调错。3.4 整体联调与试运行结果我把两个技能都挂载之后用一批真实报销数据做了联调。输入是一张餐厅发票照片、一张高铁票照片和一张 Excel 开支表。整体流程跑下来约 40 秒最终输出的 JSON 总金额、分类汇总、金额最高前三条全部正确并且输出格式直接符合下游模板要求。试着看一下我当时跑出的中间结果{ total: 3568.50, categories: [ { name: 餐饮, amount: 328.00 }, { name: 交通, amount: 2380.50 }, { name: 住宿, amount: 860.00 } ], top_expenses: [ { item: 高铁票, amount: 553.00, date: 2024-11-02 }, { item: 酒店住宿, amount: 860.00, date: 2024-11-03 }, { item: 高铁票, amount: 498.00, date: 2024-11-04 } ] }注意到这里没有报告编号、没有发票编号因为我在约束里压根没让模型输出这些字段。脚本从图像和表格里真的提取不出也无妨按协议它们被省掉了。表面看信息少了实际上减少了幻觉空间这正是显式协议带来的稳定性红利。4. 经验沉淀把技能体系做“稳”的五个关键策略这一节的内容比较杂但每一条都是从反复踩坑里提纯出来的。如果你照着我前面的设计搭建技能库在实战中你一定还会遇到这些看不见的细节。提前写在这里能让你少浪费几个周末。4.1 技能边界必须刻意窄化这是第一条也是最重要的一条。技能一旦定义的边界过宽就会出现“什么都想干、什么都干不精”的情况。我早期做过一个叫analyze的万能技能既能分析文本、又能分析数据、还能分析图片结果就是在每类输入上都不稳定经常把文本分析结果输出成图表结构。我后来把每个技能都改造成职责单一的窄技能。比如“发票识别”只做识别“支出汇总”只做汇总“趋势判断”只做趋势判断。窄化意味着模型每次要做的判断变少遵循率大幅提高。你可以粗暴地理解成“给模型的自由越少输出越稳。”4.2 规则交给代码理解交给模型从设计之初我就反复拿捏一个问题技能里的逻辑到底写在提示词里还是写在脚本里说白了文字规则是概率性的代码规则是确定性的。凡是涉及计数、排序、筛选、去重、校验这一类逻辑全都应该交给代码脚本凡是涉及理解意图、提取语义、判断相关性这一类任务才应该交给模型。比如我设计抽取动作时“过滤掉状态为已取消的订单”“按金额降序排列”“合并同一天的多笔支付记录”这些判断统统写进了 Python 脚本SKILL.md 中反而只写“过滤、排序、合并”这几个动词模型不用理解这些操作的具体运算逻辑它只需要触发脚本。这种分工方式让整体可靠性有了质的飞跃。4.3 输出协议要有兜底设计再稳定的模型也会有偶尔不守规矩的时候。最经典的坑是要求输出 JSON它给你一段 Markdown要求字段是amount它给了Amount要求空字段填null它直接不输出那个字段。这些都会导致下游代码中断。所以后来我在每个脚本的最后一步都加上了输出校验逻辑比如用 JSON Schema 检查字段类型、检查必填键是否存在、检查数字类型是否合法。不合规就自动重试一次把上次输出塞回去让模型重新生成再不行就返回固定错误结构。这套兜底设计把失败率从肉眼可见的尴尬降到了几乎可忽略。4.4 技能文档要写“反面约束”很多人在写技能描述时会只写“做什么”不写“不做什么”。我建议反过来想一下一个技能如果放任模型自行理解最容易在哪些地方走偏把这些地方显式写到“约束”里。比如一个客服技能约束里写“不得承诺赔偿金额”“不得编造退换货政策”一个分析技能约束里写“不做超出既有数据范围的推断”“不自造指标”一个总结技能约束里写“不加入原文没有的观点”。有了反面约束模型的边界感明显强了很多。那些看似理所当然的规则你不写它它就真的会不遵守。4.5 技能库一定要可以自动化回归测试技能会不断迭代模型也会时不时更换版本。每一次改动都可能让原本稳定的技能发生回归。在没有自动化测试的情况下你根本不知道这次改动到底改坏了什么。后来我建立了一套轻量级回归测试机制准备固定测试用例集每个用例包含一个模拟输入和一份预期输出样本。每次技能库更新后自动跑一遍用规则比对输出结构再辅助模型对结果打分全部通过才允许进入生产。说白了这套自动化测试是技能体系的照妖镜。模型行为没法穷举保证但至少常见路径和输出格式是随时被盯住的不会出现上线第二天用户反馈技能失效而你自己还浑然不知的情况。5. 实战问题速查与修复思路这一节整理了我这几次实操过程中遇到频率最高、最有代表性的一些问题以及我摸索出的修复策略。不一定每条都爱听但真的都是保命技巧。现象可能原因修复思路技能明明在列表里但模型根本不调用SKILL.md 的 trigger 字段写得太抽象模型没能把用户输入关联起来将触发条件改成“用户出现哪些词就调用”的显式写法用 5~10 个示例短语调用了技能但经常输出错乱执行步骤里出现“适当”“合理”这类模糊词或步骤数超过模型可使用的工作记忆上限删除模糊限定词步骤压缩到 7 步以内且尽量用脚本接手步骤输出 JSON 偶尔缺字段输出规范中未说明空值策略在约束里强制要求所有字段必有无法计算则填 null脚本调用失败缺少依赖/路径错误/参数类型不匹配技能目录带一个requirements.txt并控制脚本只从stdin读入、只向stdout输出路径问题归零多个技能同时激活时输出风格漂移技能描述之间存在语义重叠模型混淆选择精简技能描述明确“什么时候该用另一个技能什么时候不该用本技能”模型换版本后技能效果降级新模型对指令的遵循方式与旧版本不同回归测试跑一遍快速锁定受影响技能找出描述中可能产生歧义的细节并改写这里面我最想在单独强调一下的是“脚本只从 stdin 读、只向 stdout 出”这条。一开始我习惯把数据路径直接写进脚本参数结果模型在不同场景下给出的路径五花八门脚本动不动就找不着文件。改成统一从 stdin 读全文、执行结果输出到 stdout 后所有技能脚本的调用方式一致了模型不再需要猜测文件位置编排器也不需要解析复杂的参数结构。表面上看这只是一个接口设计的小改动实际把整个技能系统的调试复杂度降了一个维度。另外模型换版本导致技能降级这个坑远比想象中频繁。很多人以为提示词写得越明确模型版本更换影响就越小我实测下来恰恰相反新模型的分词方式和指令遵循粒度经常变化哪怕措辞完全相同的指令在旧版上遵循得好好的在新版上就可能把某一步忽略掉。唯一的解法就是回归测试别在每次发版时手动试几个用例就放行。还有一件事要提醒技能目录和代码一样需要版本管理。SKILL.md 就是代码脚本就是代码注册表也是代码。我见过不少人把技能文件往项目里一堆标注“已完成的配置”然后后续怎么改的、为什么改、改了什么完全没有记录出了问题根本没法追。把技能文件全部纳入 Git 管理每次调整都带注释这是最基本的工程素养。最后再分享一个我个人的体会这套技能化体系的本质是把“大模型对话”从自由发挥变成工程可控。你不可能让模型永远不犯错但你可以通过结构、协议、脚本、测试把错误发生的位置死死限制在最小范围内。我在实际使用中最大的感受是系统性设计带来的稳定性红利远超想象一次搭好后续每次迭代都是线性成本而不是指数级的心累。如果你现在还在被“这功能时好时坏”折磨我强烈建议你抽一个完整的下午把技能设计方法论走一遍然后老老实实做个回归测试你会回来感谢这套体系的。
RELATED READING

延伸阅读

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