
做 AI Agent 项目也有一段时间了。最近这个项目代号叫agent-skills它不只是一个开源框架更像是我给自己定的一套方法论把智能体具备的能力拆成一个个可以单独维护、单独测试、按需组合的“技能”然后让模型像工人进工具间领工具一样去调用它们。这套做法的直接收益是我的提示词从几千字瘦身到几百字也把那种时而靠谱、时而离谱的输出变成了大部分时候稳定可复现的结果。这篇算是自己的完整复盘包含技能拆解规范、描述写法、编排策略、代码实例和调试经验。如果你正在用 LLM 做业务功能或者被超长提示词折磨得想摔键盘这篇应该对你有用。1. 重新认识 Agent Skills从单体提示词到技能系统1.1 单体提示词为什么撑不住最早我写 Agent 的思路很直接把角色设定、业务流程、示例、工具说明全塞进一段 system prompt。几十条业务规则一进去提示词轻松过万。结果遇到三个很现实的问题。第一个是上下文浪费。模型每次请求都要从头读完整个提示词那些低频规则白白吃掉了大量 token而真正重要的用户信息反而被挤到后面注意力被稀释。第二个是决策互相干扰。不同业务线的指令放在同一段文本里模型经常分不清当前该听谁的。有一次我把“客服语气”和“技术判断”写在同一个 prompt 里结果模型一边道歉一边给出专业建议说话风格四不像。第三个问题是改一处牵全身。改一条规则可能影响所有场景的表现回归测试成本非常高我当时的方案是每次改动前后跑几十条测试用例效率极低。拿生活类比一下这就好比一个公司里所有岗位都由同一个人兼任让他上午写代码、下午做客服、晚上算账这个人迟早精神分裂。把技能拆出来相当于设立独立部门各司其职各配各的流程和标准。Agent Skills 的核心逻辑就是让每个技能承担单一职责互不污染再通过调度让它们协作。1.2 Agent Skills 的最小构成单元描述、参数、执行我做过很多版本的技能封装最后沉淀下来的最小单元是三个部分组合体技能描述skill description告诉模型这个技能是干什么的、什么时候用、什么时候不用。这是模型判断“该不该调用”的唯一依据。参数结构parameter schema声明调用这个技能需要哪些参数用什么格式传入哪些必填、哪些可选。执行逻辑execution logic真正干活的那段代码可以是普通函数、API 调用、脚本甚至可以内嵌另一个 Agent。从数据结构上看大概是这个样子skill { name: check_todo_status, description: 仅当用户询问待办任务的完成状态时使用例如‘那个任务搞定了没’。如果用户想新增任务不要使用本技能。, parameters: { type: object, properties: { task_id: { type: string, description: 任务的唯一ID } }, required: [task_id] }, execute: check_todo_status }我最终会把这三部分打包成一个 Skill 类注册到技能管理器里后续的路由、测试、监控都围绕这个单元展开。这里有个容易忽略的细节不要把调用函数本身写得太复杂技能的输入输出要尽量设计成纯函数式也就是同样的参数进来返回结果确定且稳定。一旦某个技能内部依赖了大量全局状态后面做并发编排或者复用时就会非常痛苦。2. 技能定义与封装决定 Agent 智商的关键细节2.1 技能描述让模型“看得懂”的三种写法技能描述是模型决定“用不用”以及“用哪个”的关键依据它的重要性怎么强调都不为过。如果描述写得模糊模型就会频繁选错。我自己在实践里总结出一份及格的表现描述必须包含三个信息技能用处、使用场景、明确排除的场景。拿“搜索文档”这个技能举例。一种常见的写法是“搜索文档获取相关信息”这种写法的问题在于它太宽泛了。模型在任何一个需要信息的时刻都可能倾向于调用这个技能哪怕用户只是闲聊。更好的写法是根据关键词在知识库中检索匹配的文档片段。当用户提出问题、需要查阅系统资料时使用本技能。如果用户只是闲聊、或者内容属于常识问题不要使用本技能。这样写模型就有了明确的边界感。我还有一个检验写法是否合格的方法把几个技能的描述打印出来给一个完全不了解系统的同事看让他在三个候选里选正确答案。如果人都会选错模型不可能选对。另外我实测下来描述里加入“触发强度”词汇非常有效。比如“必须仅在用户明确要求……时使用”“除非用户提出需要否则不要执行”。这类词能显著降低误触发率。我见过一个数据比较夸张的例子某天气技能的描述原本是“查询天气”误触发率很高加了“仅当用户明确要求查询实时天气信息时使用”之后误触发率降了一半以上。2.2 参数设计能少则少能限就限参数 schema 设计得好不好直接影响模型生成参数的成功率。模型要做的事本质上是把自然语言映射到结构化参数上你让它填的字段越多、约束越少出错概率就越高。我印象最深的一次事故是一个技能定义了 12 个参数其中 6 个必填。上线第一天模型每次调用不是漏掉必填项就是把 enum 值写成自由文本几乎全线崩溃。后来我把参数精简到 4 个其余全部通过默认值或后端补齐准确率立刻上来了。整理我目前遵循的三条原则基本可以概括必填参数控制在 23 个以内。能通过默认值解决的就不要让模型生成。给枚举值加 enum。模型在 enum 约束下基本不会乱填因为它能看到可选项。参数 description 写明确别只写类型。比如“日期”要写成“任务截止日期格式 YYYY-MM-DD”而不是一个孤零零的 date。另外还要注意参数名的可读性。不要用抽象缩写比如s、t这种模型看到参数名title比看到t要容易理解得多。参数名和描述共同构成了模型对“这里该填什么”的认知这个认知直接影响参数的生成质量。2.3 返回结构让结果能被二次消费技能执行完返回的数据不能只是一段黑盒字符串。因为 Agent 拿到返回结果之后往往还要继续加工比如总结、翻译、或者作为另一个技能的输入。如果返回结构混乱后续环节就很难衔接。我会统一用这样的结构返回{ status: success, # 或 error data: {...}, # 结构化数据 message: 任务已完成 # 给人/模型看的一句话总结 }status 字段告诉模型这次调用是否成功data 里放真正要用的结构化数据message 是一句自然语言的摘要模型可以直接拿来组织回答。这样设计的好处是模型不需要深入解析 data 就能知道结果概要如果后续还要处理它也知道去哪找细节。这里有一个比较隐蔽的问题如果技能返回了超长文本一定要做截断或摘要否则上下文窗口很快会被塞满。我吃过亏有个技能把一个几千行的日志全文返回了紧接着的对话质量立刻崩坏因为模型可用的上下文空间被压缩到了极限。现在我都会在 execute 函数里做一道防线把返回内容统一压到预设长度超出的部分要么截断要么用摘要代替。3. 技能编排与调度让多个技能协同工作3.1 路由选择谁来决定下一步该调用哪个技能当系统里有十几个技能时模型怎么做路由决策就成了核心问题。我实践下来主要有三类方案各有各的适用场景。第一类是全量路由所有技能描述都传给模型让模型自己选。这种方式实现最简技能少于 20 个时非常好用因为描述总量可控模型能在一个完整列表里做全局判断。第二类是分组路由把技能按领域分组先用一个相对轻量的模型判断当前请求属于哪个领域再只把该组的技能描述传给主模型。这种方式能有效降低候选数量提升准确率。第三类是规则路由用关键词或正则直接决定调用哪个技能。这个只适合意图极其明确的场景比如看到“天气”两个字基本可以直接锁定天气技能不需要让模型参与决策。全量路由到了后期会有一个明显问题技能多了以后所有描述占用的 token 不少而且模型在几十个候选里选对的概率会明显下降。我目前的主力方案是分组路由第一层用分类模型做领域筛选第二层再让主模型在小组内做精细选择。类比来说全量路由就像把所有工具摊在桌上让工人自己挑桌上只有 5 件工具没问题50 件就乱套了分组路由就像先带工人进对房间再让他从墙上的几件工具里挑效率差一个量级。3.2 组合模式顺序、并行与条件分支单个技能有时候撑不起复杂任务这时候需要把多个技能串起来。常用的编排模式有三种顺序链、并行扇出、条件分支。顺序链是最常见也最容易实现的一种A 技能的输出作为 B 技能的输入典型场景是“查订单-生成发票”。并行扇出则是一个请求同时触发多个技能最后汇总结果统一回复比如同时查库存、查物流、查天气。这种模式要注意多个技能的返回是分多次调用拿到的如果没有做会话级别的结果聚合模型只会看到最后一个返回前面几个就白跑了。条件分支是根据前一个技能的结果决定下一步执行哪个流程比如先查用户登录状态已登录走 A 流程未登录走 B 流程。我过去犯过的错是把一个本来只需要 3 个技能的流程硬拆成 8 个结果编排逻辑比业务逻辑还复杂调试时极其痛苦。技能粒度不是越细越好拆到“独立可复用、且承担单一职责”这个程度就足够了过度拆分只是给自己找麻烦。3.3 上下文管理技能之间的“临时记忆”多技能协作里上下文管理是最容易被低估的环节。模型本身没有跨技能记忆它只能看到当前会话里的内容。如果你不在上下文里保留关键中间结果模型到了下一个技能就会彻底丢失前文信息。我现在维护一个“工作记忆区”内部有三层处理逻辑。第一层放全局上下文只保留用户原始意图、关键实体比如用户 ID 和订单号、当前任务状态这些是精华中的精华。第二层是技能输出摘要每次技能返回后我会提炼 data 里最重要的几个字段写回上下文而不是把完整返回结果塞进去。第三层是会话裁剪策略当上下文接近上限时优先裁剪技能原始返回保留用户对话和关键摘要。有一个很典型的调试例子某个多技能流程在第二个技能里拿到的订单号总是空值排查了半天最后发现是第一个技能的返回结果里有订单号但在写回上下文时被摘要逻辑截断了。后来我把摘要逻辑单独抽出来写成规则明确的函数问题才彻底消失。这类问题用传统接口测试几乎发现不了只能在编排层自查靠日志追溯。4. 实战记录从需求到完整技能系统的拆解过程4.1 需求拆解一个具体场景变成技能清单拿一个最近落地的例子来说。业务方要做一个内部的任务管理助手用户可以用自然语言让 Agent 创建任务、查询进度、标记完成、汇总当天事项。拿到这个需求第一件事不是写代码而是把需求拆成技能清单。我拆出来的初步清单是这样技能名用途触发场景create_task新建待办任务用户提出新增任务、安排工作list_tasks查询当日任务列表用户问“我今天的任务有哪些”update_task_status更新任务状态用户说“这个完成了”“推迟一下”daily_summary生成当天任务汇总用户要求汇总或日报拆技能的过程其实是在回答三个问题这个动作是否会被反复触发这个动作是否与别的动作输入输出不耦合这个动作能否单独测试只有当三个答案都是“是”时拆出来才划算。比如“发送日报邮件”这个动作虽然看起来像一个独立操作但它的前置依赖是先调 daily_summary再走通知渠道和汇总逻辑耦合较强所以我倾向于把它留在 daily_summary 的编排层处理而不是拆成独立技能。4.2 编码落地技能基类与注册中心拆完成清单接下来就是落地。我写了一个非常简单的 Skill 基类和注册中心代码核心部分不长class Skill: def __init__(self, name, description, parameters, execute_fn): self.name name self.description description self.parameters parameters self.execute_fn execute_fn def run(self, args): return self.execute_fn(**args) skills [ Skill( namecreate_task, description仅在用户要求新增待办任务时使用。参数 title 为任务标题due_date 为可选截止日期格式 YYYY-MM-DD。, parameters{ type: object, properties: { title: {type: string, description: 任务标题例如‘整理周报’}, due_date: {type: string, description: 截止日期YYYY-MM-DD可省略} }, required: [title] }, execute_fncreate_task ), # 其余技能类似 ] skill_registry {s.name: s for s in skills}然后给模型提供一段技能使用说明。这一步很关键不要把所有代码都贴给模型而是把 name、description、parameters 拼成一段简洁的说明文本让模型看得懂、能正确调用就行。调用时让它返回一个 JSON包含技能名和参数再由后端真正执行。模型侧看到的说明大概是这样的你可以使用以下技能来完成任务。当用户的请求匹配某个技能的描述时调用该技能。 技能列表 1. create_task: 仅在用户要求新增待办任务时使用。必填参数 title可选参数 due_date。 2. list_tasks: 当用户查询任务列表时使用无需参数。 调用时返回 JSON 格式{skill: 技能名, params: {...}}这样设计有几个好处。一是提示词长度可控二是模型的决策空间被收敛了三是在模型侧增加或者删除技能时只需要改说明文本的拼接逻辑后端函数可以保持独立。4.3 调优复盘一次失败调用是怎么救回来的刚上线那会儿模型经常把“查询列表”理解成“创建任务”或者反过来。我抓了一段日志来复盘发现了一个很有意思的记录{ user: 把我今天的任务都列出来, model_thought: 用户想知道任务列表应该调用查询类技能, tool_call: {skill: create_task, params: {title: 把今天的任务都列出来}} }问题就出在 create_task 的描述里没有排除“查询”这个意图而技能描述里又有“任务”这个关键词导致模型产生了混淆。当时的修复方案很粗暴但有效在 create_task 的描述末尾加了一句“仅当用户明确要求新增、创建、安排新任务时使用查询、列出、汇总等意图请调用 list_tasks 或 daily_summary”同时在 list_tasks 的描述里加上了“用户询问‘有哪些任务’‘列一下’时使用”。改完之后我重新跑了 50 条历史测试用例误调用数量从 7 次降到了 0 次。这次复盘让我彻底明白技能描述里写“不要做什么”和写“做什么”同样重要甚至更重要因为大模型对“禁止项”的遵循往往比对“功能项”的泛化理解更可靠。5. 常见问题与排查技巧实录5.1 高频踩坑点与对策速查表做 Agent Skills 这么久我沉淀了一张高频问题速查表新手可以直接按表排查现象可能原因解决方向该调用 A 技能却调用了 B两个技能描述触发词重叠或描述过泛给各自描述加场景限定和排除句参数凭空捏造或漏填必填参数太多或参数描述不明确精简参数加默认值加 enum同一请求反复调用多次模型没看到上次调用结果上下文被截断在上下文保留最近一次技能输出摘要长文本返回后对话崩坏技能返回内容未截断在 execute 函数里统一截断或摘要多技能流程中关键字段丢失中间结果未写回上下文抽独立摘要逻辑写回工作记忆区模型“想到”了却没执行技能列表不在模型可见上下文内检查上下文裁剪逻辑优先保留技能列表5.2 一次排查全流程为什么“想了”却没“执行”有一次我在调试一个电商场景的 Agent流程是查订单之后应该自动发送提醒但从结果看提醒从来没有被触发过。排查过程我走了一条比较典型的路径。第一步打开完整对话日志先看模型在第一次技能返回之后的思考内容。结果显示模型的思路里明确写着“应该发送提醒”完全正确。第二步我看模型生成的 tool_call发现里面根本没有发送提醒这个技能的调用。那就说明问题不在意图识别而在执行链路。第三步检查上下文管理代码发现技能返回结果写回上下文时覆盖了技能列表的一部分内容模型在后续请求里已经看不到完整的技能清单了所以它想调也调不出来。第四步我把技能列表设为会话级只读数据不允许技能返回内容覆盖它问题当场解决。这类问题非常隐蔽因为模型思考了但没执行从产品表现看就像随机失效。我的判断方法是日志里如果 think 有正确意图但 tool_call 没有对应技能优先怀疑技能列表是否还在模型上下文里如果 tool_call 有技能但参数不对再去查参数定义和路由逻辑两条路径完全不同查错了方向很浪费时间。另外一个实用的习惯给每个技能调用都打上 trace_id把模型思考和最后执行的 tool_call 配对保存。定期统计各技能的调用率和误调用率误调用率高的技能描述优先优化。我每个月都会拉一次调用分布看看哪些技能实际用的很少哪些技能频繁被误用这个数据驱动的优化方式比凭感觉改提示词要靠谱得多。回到开头说的方法论。我现在的习惯是每次搭建新的 Agent 项目都会先把技能清单列出来然后用“描述三要素”逐条审核再落到基类里跑测试。整套流程走下来给我最大的体感是一个字稳。单体提示词那种“碰运气”的失控感没有了取而代之的是可以把每个技能单独拎出来优化和测试的掌控感。最后分享一个实际操作中的小技巧技能上线后别急着丢进生产先拿 20 条典型用户请求跑一遍离线评测重点看误调用和参数错误这两项指标只要这两个数值下来了线上表现一般不会差到哪里去。