这两年做大模型智能体我最大的一个体会就是模型的能力上限由参数决定但能力的边界却由工程决定。你可以用提示词把Agent调教得头头是道可一旦涉及真实业务——查库存、算报价、改配置、跑批处理它就像个只会纸上谈兵的实习生。而那个让智能体真正“动手干活”的模块我习惯叫它 agent-skills。这玩意儿不是什么新框架也不是某个商业平台的专属功能它本质是一套把模型之外的可执行能力封装成标准模块的工程规范。今天我会从设计思路、目录结构、实现细节到蹚坑实录把一个技能模块从零到一的全过程掰开揉碎讲清楚给正在做Agent落地的开发者一份可以直接抄作业的参考。做技能模块最忌一上来就写代码。你得先想明白一个问题既然模型本身已经能对话、能推理、能写东西为什么还要在它外面挂一层“技能”这个问题想不透后面全是烂尾工程。1. 技能化设计的底层逻辑与核心收益1.1 为什么需要把能力拆成独立技能先把场景摆出来。你有个智能体要让它做三件事读邮件、提炼待办、写周报。最粗暴的方案是把所有指令和背景塞进系统提示词里让模型在一个超长上下文里完成所有工作。第一版可能还能跑等需求一多提示词撑到几千字后你会遇到三个经典症状模型的注意力被稀释明明该调接口的时候它在编数据上下文窗口被打满连基本的对话记忆都保不住你改一处逻辑整条prompt的逻辑链就崩了。隐藏的问题是能力无法复用。你在这个项目里写好的“读邮件并分类”逻辑换个项目就得重写一遍。代码拷贝容易但里面附带的提示词策略、接口连接配置、错误处理分支全都耦合在一起迁移成本高得惊人。agent-skills的思路就是把这层能力从“提示词里的描述”变成“独立的可执行模块”——每个技能都有自己的说明文档、代码实现和配置项模型根据任务描述动态加载它用完就释放互不干扰。从操作系统的视角看这件事特别好理解。你往电脑上装个打印机驱动系统不会在启动时把驱动全读进内存而是在你点“打印”那一刻才把对应模块拉起来。Agent技能也是这么个逻辑一个装满技能的目录就是系统的驱动库模型是CPU调度器是内核任务到达时它挑出最匹配的技能来执行。1.2 技能模块与工具调用的边界在哪儿有经验的读者会问这不就是function calling吗工具Tool和技能Skill之间有重叠但站的角度不一样。函数调用是“接口契约”它定义你给模型暴露了哪些函数、参数怎么填、结果怎么回。技能则是完整的能力包它不仅包含接口实现还包含模型在什么场景下应该选择它、输入数据前需要做哪些预处理、执行期间遇到特定错误怎么办。对比维度函数/工具调用技能模块核心内容接口描述、参数Schema接口 使用说明 上下文样例触发方式模型显式发起调用调度器按意图匹配后加载生命周期随对话上下文存在按需加载执行结束即释放可复用范围单项目内共享跨项目、跨Agent复用附带信息函数签名和描述示例、约束、权限配置、版本信息我在工程里对团队定的规矩很简单一个计算、一次查询、一步操作做成工具一段需要多步骤配合、有状态流转和条件分支的逻辑封装成技能。比如“算两个日期差多少天”是个工具“根据接单时间自动计算交付日期并校验节假日顺延”就是个技能因为后者包含业务规则你的模型不该自己拍脑袋想这些规则它该做的只是选对技能。2. 技能模块的目录组织与元数据设计2.1 一个技能包应该包含哪些东西我推荐一个技能包至少包含五个部分这在我参与的多个项目里已经收敛成稳定结构skill-name/ ├── skill.toml # 元数据定义 ├── main.py # 技能实现入口 ├── schema.json # 入参出参的JSON Schema ├── README.md # 技能使用说明与示例 └── assets/ # 静态资源、参考数据可选很多新手写的技能包只有main.py这是个坑。缺失元数据文件调度器就无法知道这个技能是干嘛的、适合什么场景、有哪些参数模型只能靠猜。缺失schema.json入参校验全靠运行时debug线上环境出一堆四不像的错误。至于README它最大的作用不是给人看而是被上游的模型语义索引模块转成向量用于意图召回——你写得好不好直接决定模型能不能找对技能。为了让这套结构真正生效需要约定一个加载约定技能根目录下的 skill.toml 采用UTF-8编码依赖清单声明在系统或技能级运行环境由统一容器提供。只要遵守这些约束任何新技能加入系统时不需要修改一行调度代码。2.2 元数据字段设计尤其是描述和样例行skill.toml里最关键的不是版本号而是description和examples。先说description它是模型判断“什么时候该用这个技能”的依据。写过提示词的人都知道给模型的指令不能含糊技能描述同理。举个例子你写“本技能用于处理销售数据”模型无法确定它能不能在月度分析场景里用改成“本技能适用于月度销售明细的清洗、指标计算与趋势汇总输入一张包含日期、渠道、销售额的表格输出按渠道和月份聚合的汇总表”匹配准确率能上一个台阶。examples字段则更像是给模型的“考卷参考答案”。我会在每个技能的元数据里放两到三个完整的输入输出示例用于帮助模型理解调用边界。曾经有个内部测试场景模型在40%的情况下会把“统计接口响应时间P99”误判为“统计接口错误率”。在技能描述里加了正例和反例之后误判率直接降到5%以下这就是示例的价值。还有一个容易被忽略的字段是permissions。它声明技能执行的最低权限比如只读、写临时目录、可访问特定域名。权限设计的目标是把执行环境对模型幻觉的影响降到最低——模型可以选技能但不能靠技能给自己提权。2.3 技能描述与意图匹配的写法技巧写技能描述有一条我总结的心法给模型讲“你正在处理什么”而不是“技能做了什么”。举例错误示范本模块包括数据读取、异常值处理、缺失值填充、统计输出等功能。正确示范当你拿到一份包含空值或异常样本的历史订单数据集需要先清洗再按日汇总订单量、销售额最后输出趋势表时请使用该技能。任务中若包含“清洗数据”“统计汇总”“趋势分析”等动词优先匹配本技能。两者差别在于前者是功能导向后者是场景导向。模型对齐的不是功能而是用户当下的意图。这也解释了我为什么始终强调examples的重要性——在技能数量超过20个之后模型几乎完全依赖正反样例去做匹配决策描述文本只是辅助。3. 核心实现注册、加载与执行链路3.1 技能注册器的实现思路技能模块的入口是一个注册表所有技能在系统启动或热加载时登记进场。工程上我比较喜欢用装饰器方式它写起来最干净也便于框架复用。下面是你的第一段可以直接抄走的代码块from typing import Callable, Dict, List class SkillRegistry: def __init__(self): self._skills {} def register(self, name: str, entry: Callable, description: str ): if name in self._skills: raise ValueError(f技能重复注册: {name}) self._skills[name] { entry: entry, description: description, } def get(self, name: str): skill self._skills.get(name) if not skill: raise KeyError(f技能不存在: {name}) return skill[entry] def list_skills(self) - List[str]: return list(self._skills.keys()) registry SkillRegistry() def skill(name: str, description: str ): def decorator(func: Callable) - Callable: registry.register(name, func, description) return func return decorator用起来长这样skill(order_daily_summary, 按日聚合订单数据并输出汇总表) def order_daily_summary(payload: dict) - dict: ...注册器的核心价值是保持系统各模块之间的解耦。技能之间不互相引用它们通过注册表被调度器统一选择。引入一个新技能对已有代码的影响为零。3.2 加载机制动态导入是优雅的关键市面上的框架一聊到加载技能就喜欢搞插件扫描机制复杂一点还上个抽象基类。可我实测下来最稳的还是按命名规范动态导入。你准备好模块名和类名后直接用Python内置的importlib就够了import importlib import pathlib from typing import Dict, Any SKILL_DIR pathlib.Path(./skills) def load_skill_module(skill_name: str) - Dict[str, Any]: module_path SKILL_DIR / skill_name if not module_path.exists(): raise FileNotFoundError(f技能目录不存在: {skill_name}) if not (module_path / skill.toml).exists(): raise FileNotFoundError(f技能缺少 skill.toml: {skill_name}) spec importlib.util.spec_from_file_location( skill_name, module_path / main.py, ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return { module: module, metadata: parse_skill_metadata(module_path / skill.toml), }这套方案有几个天生优势不需要维护集中式的插件注册表技能目录结构即是配置文件的映射删除或替换技能时学不到旧代码做到真正的可插拔加载速度远快于执行子进程式隔离方案适合技能被高频调用的场景。我的实际经验是优先保证单技能加载时间在50毫秒以下。如果你的业务场景里要做跨语言技能调度建议把技能封装成独立的命令行进程再通过Socket通信返回结构化结果。图形界面应用不是问题因为Agent不是强交互路径上的短延迟服务。3.3 执行会话与上下文传递技能执行不能像裸函数调用那样只传参数它需要一个独立的执行上下文承载prompt前缀、临时目录路径、日志处理器和运行时长限制。这个上下文对象要反映“一次技能调用的完整生命周期”。一个经验是技能之间的上下文必须隔离。A技能写入的环境变量绝不能影响B技能一个技能执行中产生的临时文件结束后应该被清理。清理逻辑要靠上下文管理器保证哪怕技能内部抛了异常也要触发清理。我在生产环境里见过太多因为临时文件没有清理导致磁盘爆掉的案例。3.4 安全边界技能是代码代码就一定有破坏力。模型运行时若直接拿到shell权限那你就是把整个系统的钥匙交给了概率采样器。我有些习惯供你参考每个技能运行在受限用户态进程里不能触碰系统全局配置技能能访问的文件目录被限定在它自己的沙箱目录内网络访问用白名单域名列表默认禁外联单次技能执行最长不超过60秒超时自动terminate技能返回的文本大小限制在100KB以内防止内存被撑爆安全设计的核心原则是即使技能是一段恶意代码它也做不了超出最小权重的坏事。往好的方向推测大部分技能是没问题的但你要为最坏的情况兜底。4. 从零写一个可用技能实战演示4.1 需求场景与整体规划假设现在要做一个“会议纪要一键归档”技能。需求背景是业务团队的每日站会、周会、评审会纪要散落在各个群聊里我们需要把新产生的会议内容自动归档进结构化知识库。这个技能要接收一段会议记录文本提取主题、参会人、待办事项、结论输出JSON式摘要。先定方案不采用让模型实时抽取的在线方案。因为每次调模型成本高、延迟大且面对长文本不稳定。改为模型离线预解析后在技能内部用一个组合流程做结构化校验和归档。整体执行分成四步输入预处理、字段抽取、格式校验、写回存储。4.2 写元数据这是模型的“使用手册”先创建技能目录和skill.tomlname meeting_minutes_archive version 1.0.0 description 将会议记录文本转换为结构化归档信息。适用于站会、周会、评审会 等场景输入为原始会议纪要文本输出为包含主题、参会人、待办、 结论的JSON对象。 author some_developer entrypoint main.py permissions [read:meeting_notes, write:knowledge_base] [examples] positive [ 会议记录今天讨论订单接口超时问题参会人张川、李倩、王雷结论下周三前修复待办李倩出排查报告。, 会议纪要版本发布会复盘参会人全员结论发版流程要加自动化检查待办王雷整理检查清单。, ] negative [ 帮我把订单数据按周汇总。, 查询库存不足的SKU列表。, ]注意我把反面案例也写进去了。实践结论是存在正确反例的技能匹配准确率平均高出15到20个百分点这在多技能系统里差异非常明显。4.3 实现技能主逻辑main.py逻辑不复杂。我用正则加规则做粗提取再用模型接口做兜底这样既控制成本又保证复杂文本不漏关键字段import json import re def parse_participants(text: str) - list: # 按常见分隔符切分 names re.findall(r([\u4e00-\u9fa5]{2,3})(?:[、,\s]|$), text) return list(dict.fromkeys(names)) def run(payload: dict) - dict: raw_text payload.get(text, ) if not raw_text.strip(): raise ValueError(输入文本不能为空) # 粗提取主题通常取首个“”或“讨论”前的内容 topic_match re.search(r(?:主题|议题)[:]\s*(.)$, raw_text, re.MULTILINE) topic topic_match.group(1).strip() if topic_match else raw_text.split()[0] # 待办事项提取 todos re.findall(r(?:待办|TODO)[:]\s*([^\n]), raw_text) result { topic: topic, participants: parse_participants(raw_text), todos: [t.strip() for t in todos], conclusion: , status: parsed, } # 这里省略真实的模型兜底调用用空串占位 # result[conclusion] llm_extract(raw_text)[conclusion] return {ok: True, data: result}真实场景里你需要接入模型兜底这个代码结构已经给了你扩展点。把规则抽取当作主路径模型兜底当作补充能让命中率稳定在90%以上。4.4 schema定义与工具链接入schema.json按标准JSON Schema定义用来做入参校验千万别省{ $schema: http://json-schema.org/draft-2020-12/schema, type: object, required: [text], properties: { text: { type: string, minLength: 1, description: 原始会议记录文本 }, source: { type: string, enum: [group_chat, email, other], default: group_chat, description: 会议记录来源 } } }配好schema之后不管哪个框架来调用能传什么参数、参数长什么样都由这份文件说了算不会因为框架差异导致参数漏配或错配。这也是让技能模块“一次编写多处复用”的基础。4.5 挂载技能并完成一次完整调用现在把技能目录放到统一技能目录下运行一段测试脚本import json from skill_loader import load_skill_module skill load_skill_module(meeting_minutes_archive) result skill[module].run( payload{text: 主题订单超时问题复盘参会人张川、李倩、王雷。结论下周三完成修复。待办李倩出排查报告。} ) print(json.dumps(result, ensure_asciiFalse, indent2))输出长这样{ ok: true, data: { topic: 订单超时问题复盘, participants: [张川, 李倩, 王雷], todos: [李倩出排查报告], conclusion: , status: parsed } }这一步走通说明你的技能从编写到加载执行的全链路已经通了。剩下的是在调度层做意图匹配让模型在对话中自动感知“这段会议纪要该归档了”并主动拉起这个技能。5. 技能编排与状态管理的进阶玩法5.1 多技能协同链式调用与条件分支技能封装好了之后真正产生价值的是组合。一个复杂业务往往是多个技能按特定顺序配合的结果。比如“每日经营播报”这个功能先调用数据提取技能拉取销售表再将表传给指标计算技能最后让报告生成技能把指标转成自然语言。这就是链式调用。链式调用需要编排层。我的实现里用了一个极简的step定义playbook [ {skill: sales_fetch, input: {date: 2025-01-14}}, {skill: metric_calc, input: {source: step_0, metrics: [revenue, orders]}}, {skill: report_narrate, input: {source: step_1}}, ]执行器按顺序执行前一步的输出作为后一步的输入。每步结果都缓存到内存里供后续步骤引用。这种模式让玩法变得异常灵活你可以把playbook存成JSON配置不同的场景挂不同的playbook新玩法上线时不再需要改代码。5.2 条件分支与并行执行链式调用毕竟线性的真实场景还经常遇到“如果数据量超过阈值就走向量库否则走缓存”这种分支逻辑。我的做法是在执行器里加一个continue字段允许每个步骤声明“满足何种条件才继续”或“走到哪一步结束”。并行执行适合处理互不依赖的独立技能。比如用户提交一份文档你希望同时完成敏感词检测、摘要抽取、关键词索引。这三个任务可以起三个线程并发跑总耗时取决于最慢的那个技能而不再是三者之和。实测中这种并行优化能把端到端延迟压缩60%。5.3 技能内部的状态缓存与会话记忆内部状态管理是工程中容易被忽视的环节。单个技能执行是无状态的但业务往往需要技能产出的中间结果能在多次会话间复用。一个常用手段是技能级缓存目录技能执行结束后把计算结果以JSON格式落盘下次执行时先查缓存命中则免跑。最典型的应用是“模型摘要缓存”。会议纪要归档时同一份会议稿被重复处理很多次但后续提问只需要读取归档结果可以直接缓存到内存或独立的Redis实例。每次调用前先检查缓存键它的复杂度远低于重新抽取一遍带来的收益却是几个数量级的响应速度提升。做缓存有个致命注意点保证缓存键设计正确。我一般用输入文本内容哈希加参数版本的组合作为缓存键代码层面严格隔离环境变量避免脏数据互相污染。还有一点就是缓存结果必须跟技能版本绑定技能升级后要自动失效旧缓存。5.4 会话上下文如何跨技能传递跨技能传递的数据结构是执行上下文的延续。这里有个容易翻车的设计把全量对话历史都塞给每个技能。技能需要的是最小上下文塞一堆无关信息只会让模型越来越糊涂还会让解析变慢。我建议每次只传“面向当前任务的最小子集”并通过统一的数据格式在技能间传递。你可以在上下文对象上挂一个轻量的KV存储让前序技能能写入状态后续技能能读取依赖。总之状态管理的核心是把“能并行的坚决不串行能缓存的不重复算能断链的不死等”落实到每一层设计里去。6. 常见问题与排查技巧实录6.1 技能加载失败的三种姿势第一类问题技能目录不存在或路径配错。明明技能就放在当前目录但加载器找的是另一个相对路径。排查时先打印路径解析值写死成绝对路径最省事。第二种是技能文件名不符合约定比如加载器要求main.py但技能结构里是run.py。第三类最隐蔽TOML文件里带BOM头标准解析器直接报错。我建议从用户的配置模板里继承同一个TOML模板每次改动收敛到一个已知的可用的格式。6.2 模型“執拗地”不调用某个技能这大概是所有开发者都头疼的事。方案分两步。第一步检查技能描述和示例是否足够清晰是否出现模糊用词或者泛化写法。第二步检查是否有另一个技能与它“竞争上岗”两个技能描述有重叠时模型随机选其中一个你的目标技能经常被挤下去。解决方式是明确划分边界在冲突描述中写明“不适合什么场景”签名式的排除法通常能解决问题。6.3 技能升级引发的兼容性灾难技能从v1.0升到v2.0接口变了但旧调用方还在传旧参数。如果你只覆盖了新逻辑不报错那么它会静默返回错误结构这种bug极难排查。我建议在技能入口处做两层守护第一层用schema.json做入参校验第二层在工程层面启动自检对比实际参数机制与元数据声明一致后才进入逻辑流程。6.4 常见问题速查表症状可能原因处理手段技能未触发意图匹配描述模糊补正例和反例缩小描述范围调用报“技能不存在”注册时名称与目录不一致统一从skill.toml读取name字段入参经常缺字段schema未定义必填项严格定义required和时间参数做校验执行超时技能内死循环或外部依赖阻塞全局设置超时并日志输出耗时分布结果格式混乱输出结构不统一在入口强制JSON Schema校验出参缓存命中次级下降版本升级没同步缓存键版本号打入缓存键前缀上下文太大每次往返都传全量对话改成只传任务相关的最小字段集6.5 一些值得分享的调试技巧技能调试最好能热加载。我在本地开发时写了一个文件系统监听器技能代码一旦有变化就自动重新加载模块省去反复重启Agent进程的时间。配置改动同理监听skill.toml变化就能让新描述立即参与意图匹配对调试描述效果非常有帮助。调试期可以在注册表层面对每次技能调用做一次日志记录输入摘要、选用原因、耗时、输出结果。把这些日志汇聚起来你基本就拥有了技能调优的数据底座。根据这个底座你可以区分出哪些技能是高频命中但效果差哪些技能是低频但业务价值高就知道优化精力该往哪里投了。7. 更进一步技能的版本化与资产沉淀技能和代码一样需要版本管理、测试、发布流程。我在项目里会把每个技能包直接放进独立的Git仓库基于语义化版本打tag发布时生成锁文件。这样做的直接好处是当业务需要回退到上一版技能时一条指令就能做到不用手动改代码。测试这块我强烈建议给技能写一份“黄金输出”样例集。每个技能配上三到五组典型输入与期望输出的配对每次改动都跑一遍回归测试。技能是给模型用的接口模型的行为有随机性有了黄金样例集至少能保证上游逻辑不变的情况下输出是稳定的。我把这套东西叫“技能回归基线”实测效果屡试不爽。资产沉淀还包含技能之间的依赖管理。当技能B依赖技能A的时候我们不建议直接在代码里调工具而应该让编排层去调用保证技能B的输入和输出始终是结构化的JSON进一步做到灵活替换和编排。8. 最后说点实际心法做了这么多Agent技能模块之后我自己的技术审美已经有了一个倾向把技能做薄把描述做厚。技能内部代码越薄越好薄到核心逻辑一眼看完厚的是元数据文件和ROS文档——模型靠这个理解技能运维靠这个追溯行为。一个技能如果代码超过300行还塞了一堆复杂分支我的第一反应不是优化它而是拆成多个技能然后把流程编排交给调度层去组合。另一个心法是先让技能在自然语言下可用再去追求自动调度。手动测试时把技能调用直接写在对话里命令得到预期结果后再去调描述和示例等命中率稳定了再把它挂到编排里去做链式组合。顺序反了你会发现在错误链路上修修补补永远找不到真正的问题源头。最后再分享一个小技巧给技能起名字时用业务口径里的高频动词加业务名词比如order_summary、meeting_archive、risk_scan而不是用技术名词如data_process_001。模型对中文和高频业务动词的语义对齐程度远高于抽象技术名这一点在多技能并存时能让误召回率降低不少。agent-skills这条路没有终点技能目录会像业务沉淀一样越来越厚但只要你守住了元数据规范、目录组织、权限边界这三条底线整个体系就会越来越顺。踩过几次坑之后你基本就掌握这套设计剩下的都是体力和迭代。