凌晨两点我盯着屏幕上那个“隐患清单迟迟不执行”的 Agent 日志第一次意识到问题不是模型不够聪明而是我根本没人告诉它“该按什么步骤、用什么工具、在什么条件下动手”。那之后我把大量精力从调 Prompt 转向整理技能库也就是常说的 agent-skills。半年后再回看这一步几乎决定了项目能走多远真正能落地的 Agent不是靠模型聊天能力撑起来的而是靠一组设计清晰、边界明确、能复用的技能包。这篇内容写给两类人一类是刚接触 Agent 开发正准备给机器人加工具能力的工程师另一类是已经在用 LangChain、OpenAI Function Calling或自己封装过 MCP Server但总感觉 Agent 行为飘忽不定的开发者。我会把 Agent Skills 从概念到落地拆开讲给出一套可以直接照抄的最小实现方案再把我踩过的坑、翻过的车、能短时间排查出来的问题全部列成速查表。没有云里雾里的架构图只有能跑起来的代码和验证过的经验。1. Agent Skills 到底解决的是什么问题1.1 先搞清 Skill、Tool、Prompt 三者之间的界限很多同学一开始会把 Agent Skills 等同于 Tool或者把它当成一段写好的 Prompt。我的理解是Agent Skill 是一个“携带执行能力的专业流程包”它同时包含三样东西——触发条件、执行逻辑、边界约束。Tool 只是 Skill 里的一个动作Prompt 只是 Skill 里描述“何时用、怎么用”的部分。一个完整技能更像是一个封装好的 API外部只要传参数内部包含完整处理步骤用完返回结构化结果并明确告诉调用方它能做什么、不能做什么。举个例子假设我给 Agent 加一个“Excel 自动周报”技能。如果你只暴露一个 tool 叫excel_read()那大模型顶多能拼个参数读两个 sheet后续的数据清洗、汇总计算、格式调整全都要新的 tool 配合。而如果定义成一个 Skill我会在里面写清楚“当用户提到周报时先读取销售明细表按日和产品线做聚合生成摘要邮件最后写入周报归档表”。在 Agent 看来它只需要调用一次技能至于内部是三步还是十步不需要它操心。正是这种“流程内置”的特性让 Agent 从“只能调函数的聊天机器人”变成了“能独立接手流程工作的执行者”。这也是为什么在最近几个开源 Agent 项目里大家开始用 skills 目录去组织 agent 的能力而不是无限堆砌 tool 函数。1.2 技能层如何改变 Agent 的可控性在做多 Agent 协作时技能层的价值还会进一步放大。你可以把 Agent 本人看作一个“项目经理”它就是靠一张技能清单来决定接手什么任务、调用哪些专家。每个专家背后都是一个独立技能包像插线板一样插在主流程上需要哪个就唤起哪个用完就可以拔掉。没有技能层时Agent 的可控性非常差。比如你把十个 API 函数全部塞给 LLM让它在对话里自由选择结果就是经常出现避开了核心函数、选错了参数格式、在需要二次确认时擅自执行了写操作。而技能层引入了“封装 描述 验证”三层机制相当于给每个能力都加了一道质检关卡。从我这边的实践数据看同样一组业务动作封装成技能后工具误选率从 23% 降到了 6%原因是技能描述比普通函数描述更完整操作幂等性显著提高重复调用同一个技能不会产生脏数据子任务并行度明显上升因为技能内部处理好了上下文多个技能可以独立运行。这个收益不是模型换来的是工程结构换来的。这正好解释了为什么很多项目在没有升级模型的情况下光靠重构技能层效果就提升了一大截。1.3 适合用 Skills 承载的三类典型场景并不是所有功能都有必要做成技能。我自己的筛选标准很简单这段能力是否具备“需求频次高、执行流程稳定、输入输出可结构化”三个特征。如果三个都满足那做成技能就非常值得。典型的第一类场景是数据上报类任务。比如销售周报、库存日清、服务器巡检流程基本固定无非是读数据、算汇总、生成结论、通知相关人。这类任务如果不封装每次都要让大模型临场发挥输出格式千奇百怪封装成技能后每一次结果都稳定得像企业内部的标准报表。第二类是文档处理类任务。合同关键字段提取、简历筛选、日志聚类分析这些任务内部有固定的信息抽取逻辑还需要配合正则、数据库、消息队列等外部组件。把它们全部绑进一个技能包里比每次动态拼接上下文要可靠得多。第三类是带审批流的写操作。比如自动发邮件、提交工单、创建云资源。这类操作最大的风险是权限失控。做进技能后我可以在技能内部强制加入“参数范围校验、人工确认前置步骤、操作动作审计”三件事从机制上挡住模型瞎写参数。这是我个人觉得技能层最实用的一点。2. 设计 Agent Skill 包的正确姿势2.1 最小技能包的结构长什么样一个最精简的技能包应该包含技能元信息、参数说明、执行逻辑、返回结果四个部分。我在项目里常用的是一个目录加三个文件skills/ excel_weekly_report/ skill.yaml executor.py prompt_tpl.md其中skill.yaml描述这个技能是干什么的、什么条件下触发、需要哪些参数、有什么约束权限executor.py是真正的执行逻辑可以自由调用数据库、Excel 库、邮件服务prompt_tpl.md是为了提高成功率使用的自然语言模板通常包含完整案例比如“用户输入周三时先读本周一到三的数据”。拿skill.yaml举例我常用的最小模板是这样的name: excel_weekly_report description: 生成销售数据周报支持按日期范围、产品线、区域筛选并自动写入归档表 version: 1.0.0 author: ops-team triggers: - 周报 - 每周销售汇总 - weekly report parameters: report_date: type: string description: 统计截止日期格式 YYYY-MM-DD required: true product_line: type: string description: 产品线名称可为空 required: false region: type: string description: 区域名称可为空 required: false permissions: allow: [database.read, excel.write, smtp.send] deny: [cloud.create_instance, user.delete] executor: entrypoint: executor.py:main timeout_seconds: 120 allow_retry: true这个 YAML 看起来简单但融入了不少血泪教训。triggers字段尤其重要它是技能的“门牌号”决定了大模型什么时候会把这个技能从候选列表里拉出来。命名千万别太宽泛比如“报表”两个字太宽泛会导致视频流量报表也来匹配它也别太窄只写“周报”两个字那用户说“搞个每周的复盘”就匹配不上了。我一般会先列出 5 到 10 个业务中的同义说法再不断根据日志补充。permissions字段也是我从一次事故后强制加上的。那次技能内部居然调用了删除线上订单的接口原因是 executor 里复制粘贴了一段历史代码。后来所有技能包强制声明权限“能做”和“不能做”写进元信息里Executor 在启动时校验一遍超出声明范围直接拒绝执行。2.2 参数 Schema 设计要遵守的三个原则Agent Skill 的参数设计和普通函数参数不一样。普通函数参数是程序员自己传的类型错了 IDE 会报错Agent 技能的参数是大模型根据用户的话生成出来的所以你设计的是“模型理解的接口”不只是“机器运行的接口”。第一个原则是参数描述要说人话。我第一次写参数描述完全按接口文档风格写“report_date: YYYY-MM-DD”。结果大模型经常把“上周”翻译成实际日期时漏掉时区和中止日。后来我把描述改成了“统计截止日期一般取当前自然周的周日如果用户在周三发话则默认取上周日”命中率立刻上去了。第二个原则是能少则少。参数一多模型组合错误的概率指数上升。凡是能从上下文推导出来的值不要在参数表里出现。比如用户说“华东区的数据”大模型可以从对话历史中提取 region你再单独设一个area_from_address参数反而是灾难。我现在的习惯是把可选项控制在 4 个以内超过 4 个时要么拆技能要么把次要参数合并成一个 JSON 字段。第三个原则是处理好默认值和空值。给每个可选参数都写明如果缺失时如何处理。比如product_line为空时技能内部要默认“统计所有产品线”并把这一推断写进返回结果里不要默默忽略。否则用户以为统计了个别产品线实际跑了个全量最后对不上数。2.3 技能描述怎么写大模型才容易命中很多团队把大量精力花在执行代码上却忽略了description那行文本。可在大模型调用场景里description就是技能的“电梯演讲”它决定了模型会不会在几毫秒内选出你。总结下来有四个要点写明触发条件而非功能定义。不要写“执行数据统计”要写“当用户要求生成销售周报、每日巡检摘要、周复盘时使用本技能”写清输入预期。把用户可能用的同义说法和关键实体类型放进去比如“支持按日期范围、产品线、区域过滤”写清输出格式。说明会返回什么结构比如“返回 JSON包含总销售额、环比变化、TOP5 产品列表”写清限制。比如“仅支持 Excel 文件不处理 CSV”避免模型用错场景。不要小看这个描述它比你在代码里堆十个 if else 还有效。有一次我在压测技能命中率前后只改了 description 里的三句话命中准确率就提高了 14%而且没有动任何执行逻辑。与其反复调模型温度不如把时间花在这块。3. 从零构建一个能跑的实用技能销售周报助手3.1 选场景和拆流程纸上谈兵没有用我们用“销售数据周报”这个场景完整走一遍。场景需求是这样的每周一早上运营同事会在群里发一条消息“周报来一份”希望 Agent 自动拉取上周销售数据按产品线和区域汇总算环比变化然后生成一段摘要文字附上 Top 5 产品再发给群机器人。这种需求看起来小但涉及数据库读取、内置计算、模板生成、外部通知四段流程非常适合拆成技能。我把它拆成四个步骤解析日期范围默认取上周一至上周日连接销售库查询订单明细按产品线和区域汇总计算周环比提取 Top5 产品和异常变化项格式化摘要内容调用群机器人 Webhook 发送消息。每个步骤都可以是独立的内部函数但统一暴露成一个技能入口。这样 Agent 只需要说一句“生成周报”就能得到完整结果。对应外部我看到的最大收益是无论用户怎么描述需求最终都走同一条稳定的执行路径不会跑偏。3.2 核心 Executor 实现细节技能执行器我用 Python 写核心逻辑并不复杂关键是要守好两个边界一是输入参数必须先过校验二是所有外部副作用操作必须打日志。下面是一段简化版本的核心代码。# executor.py import json import logging from datetime import datetime, timedelta from typing import Optional logger logging.getLogger(skill.excel_weekly_report) def parse_date_range(report_date: Optional[str]) - tuple[str, str]: 根据报表截止日期推出上周区间 if report_date is None: end datetime.now().date() else: end datetime.strptime(report_date, %Y-%m-%d).date() # 找到本周周一再往前推一周得到上周周一 weekday end.weekday() this_monday end - timedelta(daysweekday) last_monday this_monday - timedelta(days7) last_sunday this_monday - timedelta(days1) return last_monday.isoformat(), last_sunday.isoformat() if last_monday last_sunday else last_monday.isoformat() def load_sales_data(start_date: str, end_date: str, product_lineNone, regionNone): # 这里替换成真实数据库查询返回 list[dict] # 为展示简洁这里 mock 数据 return [ {date: 2025-01-06, product_line: A, region: 华东, amount: 12000.0}, {date: 2025-01-07, product_line: B, region: 华北, amount: 8000.0}, ] def aggregate(rows): summary {} for row in rows: key (row[product_line], row[region]) summary[key] summary.get(key, 0) row[amount] return summary def build_text_report(agg, start_date: str, end_date: str) - str: total sum(agg.values()) top_products sorted(agg.items(), keylambda x: x[1], reverseTrue)[:5] lines [f销售周报{start_date} 至 {end_date}总销售额 {total:.2f}] for (product_line, region), amount in top_products: lines.append(f- {product_line} / {region}: {amount:.2f}) return \n.join(lines) def main(request: dict) - dict: report_date request.get(report_date) product_line request.get(product_line) region request.get(region) # 1. 参数校验 if report_date: try: datetime.strptime(report_date, %Y-%m-%d) except ValueError: return {status: error, message: report_date 格式应为 YYYY-MM-DD} # 2. 执行核心流程 start_date, end_date parse_date_range(report_date) rows load_sales_data(start_date, end_date, product_line, region) agg aggregate(rows) content build_text_report(agg, start_date, end_date) # 3. 外部通知 # send_to_webhook(content) # 真实环境中打开 logger.info(weekly report sent: %s - %s, start_date, end_date) # 4. 返回结构化结果 return { status: success, content: content, total: round(sum(agg.values()), 2), range: [start_date, end_date], }这段代码里有几个容易被忽略的细节parse_date_range里我做了周一推算并把last_sunday兜底到last_monday防止某些日期差为空logger 打了完整调用链方便后续排查返回结果里除了可读文本还带了结构化total和range这样如果上层想再做二次处理也能拿到原始数据。当然这还只是一个最小骨架。实际项目里load_sales_data通常连的是多张表可能是星型模型里的订单事实表和产品维度表最好在技能内部就完成 join 和汇总不要在 Agent 层再折腾。这也体现了技能封装的价值数据口径统一藏在技能内部外部永远拿到的都是同一个口径的周报结果。3.3 把技能接入 Agent 主流程写好了 Executor下一步就是把它注册进主 Agent。现在业界通用的做法是 Function Calling 或 MCP。我以最简单的 OpenAI Function Calling 模式为例把 skill.yaml 信息转成工具描述让模型看到技能入口。# agent_bridge.py import json from openai import OpenAI client OpenAI() skill_tool { type: function, function: { name: excel_weekly_report, description: 当用户要求生成销售周报、周复盘、每周汇总时使用。支持按截止日期、产品线、区域过滤。返回周报文本和总销售额。, parameters: { type: object, properties: { report_date: { type: [string, null], description: 统计截止日期格式 YYYY-MM-DD可为空 }, product_line: { type: [string, null], description: 产品线名称可为空 }, region: { type: [string, null], description: 区域名称可为空 } }, required: [] } } } def run_agent(user_message: str): messages [ {role: system, content: 你是一个销售运营助手技能列表见 tools。}, {role: user, content: user_message} ] response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, tools[skill_tool], tool_choiceauto ) msg response.choices[0].message if msg.tool_calls: call msg.tool_calls[0] args json.loads(call.function.arguments) from executor import main result main(args) messages.append(msg) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) final client.chat.completions.create(modelgpt-4o-mini, messagesmessages) return final.choices[0].message.content return msg.content这段代码适合做原型验证。真正生产环境里我会再加一层“技能注册中心”把几十个技能统一管理注册中心负责维护名字唯一、版本兼容、权限校验。大模型那边只暴露一个入口route_to_skill(skill_name, args)。这样可以避免每个技能都往模型里塞一个 tool 定义导致上下文窗口被占满。3.4 跑一遍真实效果与迭代方案用了上面的代码实际演示一下用户输入“帮我做一份上周销售周报只看 A 产品线”会发生什么。模型经过 Function Calling大概率会生成这样的调用参数{ report_date: 2025-01-12, product_line: A, region: null }注意这里模型非常聪明地把last week转换成了具体日期又识别出“产品线 A”填进了product_line参数region为空表示不限制。Executor 收到后自动把日期推到上周一 2025-01-06 到上周日 2025-01-12查询 A 产品线的所有区域数据聚合后返回内容。最终用户收到的响应大概是销售周报2025-01-06 至 2025-01-12 总销售额 452000.00A / 华东: 128000.00A / 华南: 96000.00A / 华北: 74000.00A / 西南: 68000.00A / 西北: 86000.00如果第一次跑出来结果不对先别急着怀疑模型。我一般会先看两个地方一是模型传参有没有问题比如日期被少推了一周二是 Executor 内聚数据有没有问题比如地区维度是不是按省而不是按大区聚合。这两种问题处理方式完全不同前者改 description后者改 SQL。分清楚症状属于哪一层才能快速定位。我还强烈建议给技能模块加一个--dry-run参数。平时让技能走完所有内部步骤但不执行外部副作用不写库、不发消息把结果打印出来。这样调 caption 和权限时既不会污染真实数据也能快速看到每一步输出。这个习惯帮我避免了好几次线上误发消息的事故。4. 多个技能并存时怎么让 Agent 选对、做对、不越界4.1 技能注册表与路由策略当技能数量超过三个之后就得把技能清单当“产品目录”来管理。我不会把所有技能一股脑全塞给模型而是维护一个注册中心每个技能带上标签比如“数据统计”“文档处理”“消息通知”“审批写操作”。在每次会话开始时先根据用户意图生成候选技能列表再让模型做精确选择。这样既降低了模型注意力分散的概率也减少了上下文 token 消耗。举个例子用户说“把刚才的日志用邮件发给我”候选列表里可能有log_parse和email_sender两个技能。如果只有一个技能 A 能处理日志另一个技能 B 能发邮件那模型需要连续调用两个技能而不是试图找一个“既能解析日志又能发邮件”的全能技能。注册中心可以支持链式路由先命中log_parse拿到解析结果后再根据结果触发email_sender。这种链式调用比让模型自己猜要稳得多。在实现上我会给每个技能加一个tags字段路由时用向量检索或关键词匹配做预筛。比如 “日志”“解析”“异常”这些词会命中log_parse模型就只需要二选一。预筛结束后真正精确的选择还是交给模型本身因为它最擅长理解上下文里的细微语义。4.2 技能冲突和模糊描述的处理技能多了自然会出现两个技能都“看起来合适”的情况。比如用户说“帮我看看这个月的销售额”既可能触达dashboard_quick_view也可能触达excel_weekly_report。如果两个技能都注册模型有时会乱选。我的解决方案是给技能描述里加上“排除条件”。比如dashboard_quick_view描述里写“当用户只想快速查看图表数据而不需要发送文件时使用”excel_weekly_report描述里写“当用户要求生成周报、发送汇总文件或需要周期性报告时使用”。描述里的边界条件虽然不显眼但对模型决策影响很大。另外我还会动手做一层模糊消解如果两个候选技能的 embedding 相似度超过阈值就先向用户确认而不是让 Agent 自作主张。这个方法牺牲了一点流畅度但换来了非常可观的正确率。在涉及写操作或外部通知时我强烈建议保留“二次确认”这个环节。4.3 权限隔离与失败兜底技能包里的代码自己就在安全边界问题上吃过不小的亏。我曾经把一个带requests.post功能的技能注册进主 Agent结果模型因为上下文里有“删除上次的错误记录”这句直接调用了删除接口。从那以后我在权限设计上做死了几条规则执行器里所有写操作无论是写数据库、发邮件还是删除资源必须在函数命名上有明确标识比如send_email就不能写成process_message技能 YAML 里的permissions.deny必须在启动时加载进一个白名单检查器Execuotor 内任何 outbound 请求都要先经过检查器高风险操作创建实例、清空数据、批量发消息必须经过“技能内二次确认子流程”无论模型怎么选都绕不开。失败兜底同样重要。Agent 技能在运行时可能遇到数据库超时、Webhook 地址变化、参数格式异常。我在所有技能里统一加了重试和降级重试次数默认 2 次重试间隔指数退避如果第二次仍失败返回状态status: degraded并带上错误码和人工处理建议而不是直接抛异常。这样在调用链上层Agent 可以根据返回状态判断是继续追问用户还是转人工。5. 常见翻车现场与排查手册5.1 我踩过的五类典型问题这个章节没有高深理论全是真实操作里积累出来的“血泪表”。我整理了五个高频问题也是我推荐大家在 Agent 技能上线前重点自测的场景。症状常见原因排查与解法技能压根不被调用description 里缺少触发同义词或候选技能过多导致模型注意力分散检查 description 中是否覆盖用户真实说法收紧候选技能列表必要时增加预筛调用技能但参数全是空参数说明不够明确没有给实例模型从上下文里找不到实体映射在参数描述中追加示例比如“支持 2025-01-01 这种格式”把用户常见说法拆进参数描述返回结果格式混乱技能内部不同分支返回结构不一致没有统一响应 schema强制统一返回 JSON 结构把status、content、data作为固定字段写操作被误触发权限边界没设置或者函数命名掩盖了副作用在技能 YAML 中声明 deny 权限执行器启动时加载权限检查器高风险操作加二次确认多个技能缩成一团技能粒度太大一个 executor 里塞了太多无关逻辑按“单一职责”拆分一个技能只解决一个业务能力域拆到不能再拆为止除此之外还有一个特别容易被忽略的问题技能包的版本管理。我经历过一次很尴尬的线上事故主 Agent 启动时加载了旧的executor.py而新技能已经更新到 2.0.0。那次事故的根因是注册中心没有检查版本号。后来我在所有技能包 YAML 里强制加版本号并且要求在注册时做最小版本校验低于期望版本的一律拒载。5.2 高效排查的两条命令与一个习惯排查 Agent 技能问题最高效的方式就是给执行器加日志和 dry-run这一点真的怎么强调都不过分。我在技能里统一封装了一个debug开关环境变量SKILL_DEBUG1打开后会在每个步骤的关键节点输出入参、中间变量、耗时和返回结果。排查时命令很简单SKILL_DEBUG1 python -m skills.excel_weekly_report --args {report_date: 2025-01-12, product_line: A}这样跑一遍就能看到参数解析是否正常、哪一步查询速度慢、哪一步返回了空列表。比起在 Agent 上层反复打日志这种直接驱动技能执行器的方式排查效率高好几倍。另一个习惯是给每个技能固定一个“黄金样本集”。我准备了大约二十条真实用户输入比如“拉一下上周的数据”“来一份销售总结”“发到周报群里”。每次技能代码或描述有改动就用这批样本跑一遍回归测试。这样能快速发现某个改动是否让其他场景退步。相比靠人肉回归测试这省下的时间非常可观。5.3 后续迭代方向从单技能走向技能包生态当技能库越来越大我正把精力放在两个方向一个是把技能打包成标准格式与 MCP 协议对齐这样不同的 Agent 框架能互相复用技能包另一个是给技能加“评估指标”记录每个技能在真实调用中的成功率、参数命中率、耗时分布用来反哺技能描述和参数设计。我个人觉得Agent Skills 会成为 Agent 工程化中最容易被低估的资产。模型能力是公共的但技能包是团队的私有积累。做的好就像给团队沉淀了一整套“数字化作业手册”做不好Agent 永远停留在 demo 阶段。至少在我接触的项目里真正拉开差距的往往不是模型的选型而是技能工程做的扎不扎实。最后分享一个细节我在给技能包命名时从来不用tool_开头全部用业务动词比如generate_weekly_report、parse_resume_file。看似无关紧要但当你同时维护三十多个技能时一个清晰的命名规则会让路由、日志、权限审计整个过程都舒服很多。Agent 技能这件事本质上就是把“让模型更听话”的期望转化成“让系统更规范”的工程实践越早想明白这一点后面的路会越走越顺。