
1. 为什么Agent时代技能成了新的编程单位1.1 从一句话需求到一套可复用能力我遇到的实际问题先说个我自己的经历。去年我在做一个内部数据整理Agent初始想法很简单用户丢进来一堆发票和报销单Agent负责提取关键字段、校验金额、汇总成表。第一版我走的是最常规的路子——把所有提取规则、字段清单、校验逻辑全部塞进Prompt里。效果怎么说呢能用但很不让人安心模型有时候把发票号码和发票代码搞混有时候多提取一行备注当成金额最头疼的是每次规则微调都要重新调试那一段越来越长的Prompt而且同样一段Prompt在不同对话上下文里表现完全不一样。同一张发票这个对话里提取对了换个对话开头再问一次结果可能就少了一个字段。后来我换了一种组织方式把发票信息提取整个流程——字段定义、提取规则、校验脚本、异常处理、输出模板——打包成一个独立的技能skillAgent在对话中只看到一个简短的能力描述真正执行时才会把这个技能的内部逻辑加载进来。效果对比非常明显字段级的准确率从大概90%出头稳定到了99%以上而且不再受对话上下文波动的影响。同一张发票一百次测试的结果几乎完全一致。这个变化让我想明白了一件事在Agent的应用里技能的英文名skills之所以变成一个公认的架构概念是因为它本质上是把告诉模型怎么做升级成了给模型一套确定性的执行方案。Prompt是慢性的、概率性的指导技能是确定性的、可测试的能力封装。1.2 技能和普通Prompt、Function Calling的本质区别很多刚开始接触Agent开发的朋友会把三个概念混在一起Prompt、Function Calling、Skill。我在早期的项目里也经历过这个混乱阶段这里用最直白的方式捋清楚。Prompt是一段文本作用是给模型补充知识和约束。它适合表达你是一个严谨的财务助理输出必须包含以下字段这类柔性规则。但Prompt不包含可执行的逻辑模型怎么理解、怎么组织仍存在大量自由发挥空间。Function Calling是一个接口让模型在对话中决定调用某个预定义函数。它适合查天气获取用户信息这种单一动作。但函数本身不负责多步流程——你让模型提取发票信息并校验金额一致性那就不是一个函数调用能覆盖的你需要串联好几个函数还得处理中间状态的传递。Skill则是一个完整的、可复用的复杂能力单元。它里面可以同时包含指令文本、脚本代码、参考样例、输出模板、边界说明。模型在对话中通过技能描述判断这个任务我有对应技能然后按技能内部定义的分步流程执行。生活化一点说Prompt是给实习生的口头交代Function是工位上的一个按钮Skill是一整本岗位SOP手册——什么时候该做、第一步做什么、遇到异常怎么处理、交付物长什么样全在里面。我自己对这三者的选择原则很简单单次、无状态的动作用Function知识约束和风格控制用Prompt凡是需要多步骤、有内部逻辑、要在多个项目里复用的就封装成Skill。1.3 什么样的任务值得封装成技能不是所有东西都该做成技能。技能有维护成本技能多了之后还有管理成本。我在跑过一段时间的技能库之后总结出五个判断标准判断维度具体要求反例高频复用每周至少用几次而不是偶尔一次给文章起标题每次创意不同确定性输入输出可以被明确定义和验证聊天陪伴没有统一输出格式多步骤需要多个操作串联不是一次调用翻译一句话单次生成可测试有明确的输入样例和预期输出性格分析结果无法客观校验边界清晰触发场景明确不容易误用通用知识问答几乎任何对话都能沾边拿第二点举个例子我当时要做一个生成周报的技能。周报有固定的章节结构有量化指标有格式要求输入是本周的任务流水输出是可提交的文档——完全符合上面的标准。但如果你想把回答用户任何闲聊问题也做成技能那基本没有意义因为触发边界太模糊模型会乱调。所以我的建议是如果你现在突然有灵感想写一个技能先拿上面这个表过一遍至少满足前三项再动手。不然技能库会变成一个塞满听起来有用但实际没人调用的杂物间。2. 拆解一份技能文件从元信息到执行逻辑2.1 技能的标准目录结构与每个文件的责任目前主流的Agent开发框架对技能的组织方式大同小异基本都是一个目录装一个技能目录内由一个描述文件加若干附属资源构成。我习惯用的结构是这样的skills/ csv-cleaner/ SKILL.md scripts/ main.py validate.py assets/ sample.csv cleaned_sample.csv requirements.txt每个文件干一件事SKILL.md是技能的门面里面写清楚这个技能是干什么的、什么时候该用、什么时候不该用、执行步骤是什么。模型在对话中首先要读的就是这个文件。scripts/放实际执行脚本。技能里的脚本不一定全都会被调用模型会根据SKILL.md的指引按需选用。assets/放样例数据。比如csv-cleaner里放一份脏数据的样例和对应的清洗结果模型可以通过对比样例来理解预期输出格式。requirements.txt声明这个技能需要的Python依赖在技能被安装或首次执行时自动装上。这里有一个很重要的设计原则技能目录是自包含的。不要让你的技能依赖另一个技能的脚本也不要假设执行环境里已经装了某个特定的库。这不光是复用性问题也是稳定性问题——一旦技能之间产生了隐式依赖你升级一个技能就可能弄挂另一个。2.2 SKILL.md的核心字段怎么写才能让模型准确理解SKILL.md的写法是技能好不好用的分水岭。同一个功能的技能描述写得潦草和写得严谨在Agent实际调用时的表现差距非常大。我整理了一下自己反复打磨出来的字段结构下面这个例子是一个CSV自动清洗技能--- name: csv-cleaner description: 当用户提供CSV文件且文件中存在缺失值、重复行、格式不一致等脏数据问题时使用。适用于数据预处理阶段不接受无文件输入的纯问答请求。 when_to_use: 用户提到清洗CSV、处理脏数据、填充缺失值、去除重复行 when_not_to_use: 用户只是询问CSV格式的定义或教学问题不涉及具体文件处理 --- # CSV 数据清洗技能 ## 输入 - 一个CSV文件路径或直接粘贴的CSV内容 - 可选的清洗规则参数缺失值填充策略、去重字段 ## 执行步骤 1. 读取CSV并解析表头 2. 检查缺失值数值列用中位数填充分类列用众数填充 3. 检查重复行基于全字段匹配进行去重 4. 检查格式一致性日期列统一为YYYY-MM-DD金额列统一保留两位小数 5. 输出清洗后的CSV并生成一份清洗报告 ## 输出格式 - 清洗后的CSV文件 - 清洗报告包含发现的问题类型、处理数量、处理前后的行数对比 ## 注意事项 - 如果CSV超过10万行改用分块处理 - 如果某列缺失值比例超过80%直接删除该列并说明原因 - 清洗不可逆输出前必须保留原始文件备份重点在于两处。第一是description和when_not_to_use这两段决定了模型在什么情况下会调用这个技能。写得越具体、越有边界感模型就越不容易在无关场景乱调用。第二是执行步骤它给模型提供了一张操作地图——模型在执行时不一定要逐行照搬但有了这张地图它的行为就不会跑偏得太远。2.3 技能的加载机制不是整个技能都塞进上下文很多人第一次接触技能时以为技能也就是把一段长Prompt塞进系统消息里。事实上不是这样。一个设计良好的技能框架在对话层面暴露给模型的只有元信息——也就是name、description、when_to_use这些字段可能总共几百token。只有当模型判断这个任务需要某个技能之后技能内部的完整内容——步骤说明、脚本代码、样例数据——才会被加载进来。这个设计直接决定了技能库能建多大。如果你的全部技能都常驻上下文10个技能可能就占用了一万到两万tokenAgent一开局就背着一个沉重的包袱响应速度、推理质量都会明显下降。而按需加载的方案下技能库可以做到几十个技能每个Agent单次对话实际加载的内容却只有需要的两三个。这个机制也解释了一个我早期踩过的坑技能描述里不要写太长的细节。我一开始把SKILL.md写得跟论文一样非常详尽结果模型在对话阶段就要消化大量冗余信息反而更容易做出错误的调用判断。后来我把SKILL.md精简成一句话概述触发词边界说明把详细步骤留在技能内部调用准确率立刻上去了。3. 从零实现一个真正可用的技能含完整代码3.1 选一个场景把凌乱的会议纪要变成结构化任务清单空谈理论没有意义我们动手写一个。我选择一个非常常见、又很好地体现技能价值的场景把一段混乱的自然语言会议记录转换为结构化的任务清单。这个任务看起来简单其实完美命中技能的核心优势它高频——几乎每个团队每周都在做。它有确定性——输出要求稳定总任务数、负责人、截止日期、优先级、状态。它多步骤——需要先解析会议文本再提取行动项再分类和排序最后输出表格。它可测试——给定同样的输入预期输出应该是等价的。模型用自然语言能力提取关键信息脚本用确定性的逻辑做格式化和规则校验两边分工避免模型在责任截止日期判断这种环节上自由发挥。3.2 主体代码实现import json import re from datetime import datetime def parse_meeting_notes(raw_text: str) - dict: 从原始会议文本中解析结构化信息。 # 分阶段处理 # 1. 模型负责语义提取输出中间JSON # 2. 本脚本负责格式校验和规则约束 intermediate llm_extract_tasks( raw_text, output_schema{ tasks: [ { action: 任务描述, owner: 负责人, due_date: YYYY-MM-DD格式或null, priority: high/medium/low, status: todo/in_progress/done } ] } ) return normalize_tasks(intermediate) def normalize_tasks(intermediate: dict) - dict: 规则校验与规范化日期标准化、优先级默认值、重复任务合并。 tasks intermediate.get(tasks, []) normalized [] for task in tasks: # 日期宽松解析支持下周三、6月底等模糊表达 due task.get(due_date) if isinstance(due, str) and due ! null: due parse_fuzzy_date(due) priority task.get(priority, medium) if priority not in (high, medium, low): priority medium normalized.append({ action: task[action].strip(), owner: task.get(owner, 待指派), due_date: due, priority: priority, status: task.get(status, todo) }) # 按优先级排序high - medium - low order {high: 0, medium: 1, low: 2} normalized.sort(keylambda x: order.get(x[priority], 1)) return { task_count: len(normalized), tasks: normalized, generated_at: datetime.now().isoformat() } def llm_extract_tasks(text: str, output_schema: dict) - dict: 调用底层模型完成语义提取替换为实际模型API调用。 # 实际项目中这一函数会发起模型请求 # 并确保返回结果符合output_schema的结构 # 这里给出一个示意实现 return {tasks: []} def parse_fuzzy_date(expression: str) - str: 把模糊日期表达转换为标准日期。 # 实际项目里可以用 dateparser 这类库 # 这里做最小的示例处理 patterns { r明天: 1, r后天: 2, r下周: 7, } for pattern, offset in patterns.items(): if expression.startswith(pattern): target datetime.now() timedelta(daysoffset) return target.strftime(%Y-%m-%d) return expression这里有一个关键点值得展开脚本和模型的边界怎么切。在这个例子中模型负责理解——从自然语言中找出谁是负责人、哪个日期是截止时间。脚本负责规范化——把模糊日期转成标准格式、把任意字符串的优先级映射到三个固定档位、按优先级排序。这样的分工让整体输出的确定性最大化又保留了模型在语义理解上的优势。3.3 技能描述怎么写模型才肯在正确的时机调用技能代码写完之后另一件同样重要的工作是写SKILL.md里的描述字段。我见过太多人代码写得很漂亮却在描述上翻车。先看一个典型的反例description: 用于整理会议信息帮助用户提取任务清单。这个描述有三个问题。第一整理会议信息太宽泛——用户可能只是问上次会议是什么时候开的模型也可能调用这个技能。第二缺少触发条件——模型不知道什么情况下应该优先想到这个技能。第三缺少反例——模型不知道什么情况下不该调用。我最后打磨出来的版本是这样的--- name: meeting-notes-to-tasks description: 当用户粘贴一段会议记录或讨论文字并要求整理任务、提取行动项、分配负责人、列待办事项时使用。输出结构化任务清单。 when_not_to_use: 用户仅询问会议时间或参会人员等事实性问题或要求写一份会议纪要摘要该项目由另一个技能负责。 --- # 会议纪要转任务清单 ## 执行步骤 1. 通读全文找出所有包含行动指向的句子 2. 对每个行动项提取负责人、截止日期、优先级 3. 将提取结果交给 normalize_tasks 脚本进行规范化 4. 输出最终任务清单表格 ## 输出格式 - 任务表格包含编号、任务内容、负责人、截止日期、优先级、状态 - 总任务数统计对比一下就能看出区别。把该用和不该用都写明白模型才有足够的依据做判断。我当时记录过一个数据把描述从宽泛版本改成边界清晰版本之后这个技能在测试集上的调用准确率真需要的时候调用、不需要时不调用从61%直接提升到了87%。这是纯文字调整带来的收益代码一行没改。3.4 技能自测别上线了才发现边界不对技能开发完我会做一组自测。下面是其中一些典型的测试用例和预期输出你可以拿来当作自测参考测试输入类型预期行为一遍完整的会议记录包含3个任务输出3条结构化任务按优先级排序会议记录中有一句这个先放着不做了不把这句话提取为任务用户直接提问如何写周报不调用该技能进行普通回答会议记录中只有一个模糊任务尽快处理仍输出1条任务due_date为null优先级为medium第四种情况尤其值得注意。它测试的是技能的兜底能力——即使输入信息不全技能也应该正常输出只是某些字段为空而不是抛异常或者重复询问用户。我一开始的设计里没有这个兜底结果遇到模糊信息时脚本会直接报错模型也不知道怎么处理。后来加上默认值逻辑整个技能才算真正稳定。4. 多技能协同当Agent手上有10个技能时4.1 一次任务如何触发多个技能以会议场景为例单个技能用起来简单技能一多真正考验的是编排能力。我以一个完整的会后日常工作流为例用户输入了一段两小时的会议记录希望Agent最终产出三样东西任务清单、周报素材、给未参会同事的摘要邮件。这个需求会依次触发三个技能meeting-notes-to-tasks提取行动项生成任务清单。weekly-report-builder把任务清单转成周报格式补充进度说明。email-summarizer从会议记录中提炼一段简短的邮件摘要。关键问题来了这三个技能的执行顺序由谁决定实践经验是不要把顺序写死在任何一个技能内部。技能之间应该是松耦合的——weekly-report-builder不应该负责调用meeting-notes-to-tasks否则你以后换了提取技能就得改周报技能。正确的做法是把编排决策交给Agent的调度层。每个技能只声明自己的输入输出格式Agent根据用户需求判断这个任务需要先拿到结构化任务清单才能生成周报然后按顺序调用。技能之间通过统一的数据格式连接比如都输出标准JSON。4.2 技能间的数据传递统一JSON结构是必需品如果每个技能的输出结构都信马由缰比如一个用due_date另一个用deadline那下游技能就要写各种兼容逻辑时间一长根本维护不动。我在技能库里定了一条规矩所有技能的输出必须遵循统一的JSON Schema涉及任务的一律用tasks数组涉及日期的字段一律是YYYY-MM-DD字符串。这个约定让技能之间可以直接用管道式串联就像Unix命令的输出能接到另一个命令的输入前提是大家都遵守相同的文本流规则。在meeting-notes-to-tasks的输出格式里就体现这个约定{ task_count: 3, tasks: [ {action: ..., owner: ..., due_date: 2025-07-10, priority: high, status: todo} ], generated_at: 2025-07-03T10:23:00 }下游的weekly-report-builder拿到这个JSON后不需要做任何字段映射直接读取tasks数组就能使用。4.3 上下文窗口的预算管理技能库越大越要克制技能一多上下文管理就成了必修课。我曾经遇到过一个问题某个智能体加载了8个技能之后单次响应的token开销涨了三倍延迟从2秒变成7秒而且中间推理质量下降因为很多上下文被技能文本占用了。解决这个问题我的经验是把上下文占用分成两级常驻层只放所有技能的元信息每条控制在50-150 token。这部分承载模型知道有哪些能力可用的功能。按需加载层技能内部详细内容、脚本、样例数据只在调用时加载。按这个策略即使技能库里有20个技能常驻层的token开销也能控制在3000以内对大多数模型来说都是可接受的。我维护技能库时还有一个习惯每次新增技能前先看一眼它命中触发词的概率——如果一个技能的description在常驻层里占了大量篇幅但真实调用率很低就坚决精简描述把空间让给调用频率高的技能。上下文预算项预算上限说明全部技能的元信息3000 token超过就要精简description单次对话实际加载的技能内容4000 token超过说明步骤/脚本可能过重系统指令与角色设定800 token保持精简不做知识堆砌留给对话历史和模型输出的空间视模型而定建议至少保留一半窗口5. 我踩过的坑技能在生产环境里的真实问题5.1 乱调用描述写得太宽泛的代价这是我在技能开发早期踩得最狠的一个坑。做一个会议总结技能时我把description写成了帮助用户整理会议信息。结果上线后用户问上周的会议纪要在哪里可以找到模型也触发了这个技能——它没有查文件系统也没有给出答案反而执行了一通完全没有输入的总结流程输出了一堆空话。排查过程是这样的我先把技能的调用日志捞出来发现触发场景里有一大批根本不是需要总结的请求。再回到SKILL.md里看description里确实没有任何输入什么、什么时候不该用的约束。问题根源清楚了模型只能依据你给的信息做判断你没有告诉它不要用在查询类场景它就全靠猜。修复方式也直接把边界说明写清楚并且明确加一条when_not_to_use。修复后这类误调用基本消失。这件事让我养成一个习惯——每写一个技能描述我都会先问自己一句如果变成问答场景模型会误调用吗5.2 技能内容过载想覆盖太多场景反而啥都干不好另一个反模式是往一个技能里塞太多以防万一的逻辑。我有个技能原本职责是判断一个文件是不是包含敏感信息后来陆续给它加了如果是PDF怎么办如果是扫描件怎么办如果是图片里的文字怎么办——这堆分支把主流程淹没了模型在这个技能里的决策准确率不升反降。我对这个问题的反思是技能不是代码库不需要覆盖所有边界。处理边界情况是模型推理能力的职责不需要都写死在技能内部。核心路径做到快速、稳定异常情况让模型自己判断是否需要其他技能或求助用户这才是常态。拆分也是解决方案之一。如果发现一个技能确实承载了太多不相关场景就把它拆成两个独立技能。比如上面的例子后来我拆成detect-pii-in-text处理文本和extract-text-from-files处理文件前者只管纯文本扫描后者负责从各种文件里抽文本然后再把抽出来的文本交给前者。一拆开两个技能的描述都能写得更精准调用率也上去了。5.3 权限边界技能能执行代码也意味着能闯祸技能不仅仅是文本指令它往往还带着能执行真实操作的脚本。这是它强大的原因也是风险所在。我出过一次事故有个技能内部写了段清理临时文件的逻辑用来在每次任务结束后删除中间产物。脚本本身没问题但问题是那个技能被配置在了一个处理用户上传文件的智能体里。某次测试用户上传的文件名碰巧包含了一个容易匹配爆破的路径脚本误删了缓存目录里的其他文件。数据没有永久损失但那段时间整个智能体现在某些任务上异常。从那以后我给所有技能定了几条安全基线技能里的脚本默认只读任何写操作改文件、删文件、发请求必须在SKILL.md里显式声明。危险操作需要二次确认技能内部发现要删除或覆盖文件时不允许静默执行必须把待操作清单输出给用户确认。执行环境沙箱化技能脚本跑在隔离环境中不直接接触宿主的系统和网络。这些规则看起来多花了一点功夫但能在你技能库规模上来之后避免很多半夜被叫起来处理的线上事故。5.4 版本混乱改了一个技能连累了一排Agent技能是复用单元也就意味着它是单点。我维护过一个技能被三个不同的Agent引用。某次我觉得它的某个执行步骤不够好直接改了结果其中一个Agent的表现反而变差——因为它原本承接的上游输出格式和这个技能的新逻辑不匹配。教训就是技能必须带版本号Agent的配置里必须写清楚锁定哪个版本。我把技能目录里的SKILL.md加上version字段同时要求每个Agent在配置里声明依赖的技能版本范围。更新流程也改成先在新Agent里灰度确认无回归再批量升级其他Agent的应用。版本状态影响范围处理策略control-1新增未发布只有测试Agent可以随便改control-2已发布部分Agent只允许向后兼容的修复control-3大规模使用多数生产Agent任何改动都必须先灰度验证版本问题不新鲜但在技能这种描述即接口的东西上尤其容易被忽视因为你不一定能直观地看到技能的接口边界。它不像一个函数有明确的签名一个技能的行为模式是模型阅读文本后自由发挥出来的所以任何微调都可能产生意外影响。6. 从写技能到设计技能进阶前的最后一公里6.1 量化技能成本决定一个技能该拆还是该合技能不是免费的。虽然按需加载能控制上下文但每个被调用的技能都会带来真实的时间成本和token成本。我在评估技能设计时会算一笔简单的账单次技能调用成本 技能加载token数 × 模型输入单价 模型输出token数 × 模型输出单价举个例子某个技能加载需要3000 token输入单价0.003美元/千token处理一次任务模型输出大约2000 token输出单价0.015美元/千token。那单次成本就是3000/1000 × 0.003 2000/1000 × 0.015 0.009 0.03 0.039美元。如果这个技能一天被调用500次日成本接近20美元。这个成本视角会直接影响技能设计决策。如果一个技能经常只用到其中一小部分逻辑说明拆分可能更划算如果多个技能经常被同时触发而且共享大量前置步骤合并成一个更大的技能可能减少重复加载开销。我一般用这个思路做季度复盘看每个技能的真实调用次数和平均消耗token然后决定这一季要拆哪个、合哪个、精简哪个。6.2 技能如何与记忆、工作流、多模型协同技能不是孤立存在的。一个成熟的Agent系统里技能要跟记忆系统配合技能负责做事的方式记忆负责具体项目的历史信息。比如meeting-notes-to-tasks技能知道如何把会议内容变成任务但它不知道这个团队习惯用哪个项目代号此时记忆层提供这个背景知识。跟工作流引擎配合时技能充当动手术的那个角色工作流负责什么时候做手术的编排。技能内部不要做定时触发、不要做完一件事之后自己决定做下一件事这些都应该交给外层的工作流控制。多模型协同也是技能设计里一个值得琢磨的方向。并非所有技能都需要最强的模型来执行。文本分类、格式校验这类确定性较强的步骤可以让轻量模型完成真正需要深层语义理解的任务才留给强模型。我试过在一个技能内部配置不同的模型后端——提取阶段用快模型校验和复杂推理阶段用强模型成本直接降了三分之一质量没有明显变化。6.3 可复用技能包的设计原则一份检查清单做了大半年技能开发之后我沉淀了一份设计检查清单每次新写技能或者重构技能都会过一遍职责是否单一如果一句话说不清楚这个技能干什么就拆。描述是否自洽只写经常触发的场景不需要穷举所有可能。是否自带测试样例技能目录里有样例数据和预期输出才能保障回归不出问题。输出是否符合团队统一Schema不统一的话下游联动迟早会出问题。依赖是否声明清楚包括Python依赖、外部工具、网络权限。危险操作是否有显式确认涉及删除、覆盖、请求发送的必须有确认环节。是否标注了版本号没有版本号就无法做灰度发布和回滚。按照这份清单技能的编写过程就从一个写代码的活变成了设计接口的活。你做的不再是给模型添加一段指令而是为你的Agent团队里增加一个能力边界清晰、可独立测试、能被多个场景共享的成员。我自己现在的体会是真正玩明白skills之后Agent项目会从一个什么都能聊、但不一定靠谱的聊天框变成一支各司其职、步骤可验证、能力可扩展的小团队。技能就是给团队里每个成员写的岗位说明书。如果你准备开始搭建自己的技能库不用想太复杂从日常重复率最高的那个任务开始写第一个就好。写完你会明显感觉到Agent的行为从凭感觉变成了按规程这个转变带来的安全感是任何Prompt技巧都给不了的。