1. 从一把梭到技能化Agent 开发为什么需要拆件我最早做 AI Agent 的时候思路特别朴素把所有能力塞进一个超长系统提示词里再挂上几个 function让模型自己挑。项目刚开始还行demo 跑得飞起但一旦功能变多问题就出来了——提示词越攒越长改一个功能像拆炸弹新增能力要小心翼翼生怕影响其他任务的输出格式。更麻烦的是团队里另一个同事想复用其中一段能力只能把提示词复制一份再改改完两个版本分叉最后谁也说不清当前线上逻辑到底是谁写的。后面我接触到 agent-skills 这类做法才算想明白一件事Agent 的能力不该以一段提示词或者一个函数的形式散落存在而应该被拆成一个个可描述、可注册、可复用、可单独测试的独立技能单元。每个技能回答三个问题这个能力是干什么的它需要什么输入它怎么执行这个概念说白了就是把让 Agent 会做某事这件事产品化。你不再跟模型说你是一个全能助手你会用搜索、会算数、会查库而是给模型提供一个技能清单清单上每一项都有清晰的说明书。模型根据用户的请求自动选择最合适的技能来调用。这样做的好处我在实际项目里体会很深。首先是可控性每个技能的输入输出是明确的模型不需要猜测这个工具到底能返回什么幻觉和误调用的概率明显下降。其次是可复用性同一个技能可以出现在不同 Agent 里比如一个数据库查询技能既能给客服 Agent 用也能给数据分析 Agent 用。再就是可维护性改技能只需要改技能内部实现不用动整个系统提示词回归测试也只需要验证这一个技能。当然技能化不是银弹。如果你的 Agent 只是做一个非常简单的任务比如给定一段文本做关键词提取那强行拆技能反而绕远路。技能化适合的是那些能力边界清晰、复用频率高、逻辑相对稳定的场景。判断标准也很简单如果你发现同一个能力在多个对话里被反复使用并且每次调用方式大同小异那就值得把它抽成一个技能。2. 一个 Skill 的标准长相输入契约、执行体与元信息很多人以为技能就是一段代码或者一个 prompt这是理解上的最大误区。一个真正能放进 Agent 主流程的技能至少包含三个部分元信息metadata、输入契约input schema、执行体implementation。三者缺一不可。元信息是给模型看的说明书通常包括技能名称和技能描述。名字要短描述要精。描述这东西直接决定模型会不会在合适的时候调用这个技能。描述写得太泛模型什么任务都想试一下写得太窄模型该用的时候又视而不见。我常用的写法是当用户需要【做什么】时使用此技能。该技能会【怎么执行】并返回【什么结果】。少用形容词多写触发条件和边界。输入契约是技能的接口定义也就是这个技能接受哪些参数、每个参数是什么类型。这一层极其重要因为模型天生擅长从用户的话里抽取参数也天生容易抽错。把参数 schema 定得越严格模型就越容易按规矩办事。比如一个查天气的技能输入就应该是城市名称、日期不要允许什么模糊地点或者大概时间否则后端处理的时候全是坑。执行体则是真正干活的代码或流程。它接收参数执行逻辑返回结构化结果。这里要强调一点执行体返回给模型的内容也应该尽量结构化。返回一段散文让模型自己理解和返回一个 JSON 让模型直接用前者的出错率高得惊人。2.1 三种常见的技能描述格式对比在实际落地中不同框架对技能的描述格式有差异但底层逻辑高度相似。我整理了一个对比表方便你判断自己该用哪种格式风格适合场景典型代表Function Calling 风格用 JSON Schema 描述函数参数以代码执行为主的技能比如查库、调用 APIOpenAI、各类兼容框架自然语言技能卡用结构化文字描述触发条件和步骤以提示词模板为主的技能比如写邮件、做总结Claude Skills、自研框架混合式注册表参数走 Schema执行走代码同时附自然语言说明复杂技能既需要模型理解何时用也需要精确传参企业内部自建技能中台无论用哪种格式本质都是把模型的判断和程序的执行中间的那层协议打通。协议越清晰Agent 越稳定。2.2 一个最小技能定义示例拿一个极其简单的获取当前时间技能举例。用 JSON 描述大概是这样的{ skill_name: get_current_time, description: 当用户询问当前日期、时间或星期几时使用此技能。无需额外参数直接调用即可。, input_schema: { type: object, properties: { timezone: { type: string, description: IANA 时区名称例如 Asia/Shanghai默认为 UTC } }, required: [] } }执行体里你需要实现一个函数接收 timezone 参数返回格式化后的时间字符串。模型看到这份定义就知道什么情况下该调用它、调用时怎么填参数、参数填错了系统会怎么处理。这个看似简单的结构其实解决了一个大问题模型和代码之间的翻译成本被压缩到了最低。用户说现在几点模型不用去翻系统提示词里关于时间的描述只需要匹配技能名和描述填入参数然后等结果。3. 手写一个可落地的技能从需求拆解到代码实现光讲概念没用我们直接上手写一个技能。我选一个实际工作中经常遇到的场景让 Agent 具备查询数据库并生成周报的能力。这个技能适合作为范例因为它的链路足够长——涉及参数抽取、数据库操作、结果格式化、LLM 润色四层逻辑能把这个技能跑通你对技能化的理解就到位了。3.1 需求拆解生成周报听起来很宽泛直接让 Agent 做它一定会懵。拆开来看这里面其实有两个子能力的叠加查数据的能力和写报告的能力。我的建议是拆成两个技能第一个技能query_sales_data负责从数据库里取原始数据输入是时间范围和产品线第二个技能generate_weekly_report负责把一段结构化数据转成周报文本输入是数据 JSON 和报告风格。这两个技能可以单独测试也可以组合使用。组合的编排由 Agent 主流程完成技能本身不需要关心搭档是谁。这就是模块化的好处。3.2 实现第一个技能query_sales_data先定义输入契约{ skill_name: query_sales_data, description: 查询指定产品线在指定时间范围内的销售汇总数据返回按日聚合的订单量和销售额。当用户需要了解销售表现、生成销售周报或分析销售趋势时使用此技能。, input_schema: { type: object, properties: { product_line: { type: string, enum: [hardware, software, service], description: 产品线名称硬件为 hardware软件为 software服务为 service }, start_date: { type: string, description: 开始日期格式 YYYY-MM-DD }, end_date: { type: string, description: 结束日期格式 YYYY-MM-DD含当天 } }, required: [product_line, start_date, end_date] } }参数定这么细是有原因的。我见过很多技能设计者为了灵活把参数全部设为可选结果模型每次调用都少传参数后端只能返回错误。宁可让参数多一点也得把必填项标清楚。另外用 enum 限制产品线的取值可以直接避免模型把硬件翻译成hardware还是Hardware这种低级错误。3.3 执行体实现执行体我用 Python 写一个简版虽然实际项目里肯定要接 ORM 或连接池但核心逻辑是相通的import json from datetime import datetime, timedelta def query_sales_data(product_line: str, start_date: str, end_date: str) - dict: # 参数校验虽然 schema 层已经做了一层但执行体必须再做一次 allowed_lines {hardware, software, service} if product_line not in allowed_lines: return {error: fproduct_line must be one of {allowed_lines}} try: start datetime.strptime(start_date, %Y-%m-%d) end datetime.strptime(end_date, %Y-%m-%d) except ValueError: return {error: date format should be YYYY-MM-DD} if end start: return {error: end_date must be greater than or equal to start_date} # 这里替换成真实的数据库查询逻辑 records _load_from_database(product_line, start, end) # 返回结构化数据方便上层直接消费 return { product_line: product_line, start_date: start_date, end_date: end_date, daily_data: records, summary: { total_orders: sum(r[orders] for r in records), total_revenue: round(sum(r[revenue] for r in records), 2) } }执行体里做两次校验不是冗余而是防御性编程的基本功。第一层校验是模型传参时遵守 schema 的预期校验第二层是应对模型传了非法值的兜底校验。模型偶尔会传一个无法解析的日期格式如果你不拦最后报错的是数据库连接层排查起来费劲得多。3.4 实现第二个技能generate_weekly_report这个技能不查库只做文本生成。它的输入是一段结构化的销售数据 JSON加上报告语言风格{ skill_name: generate_weekly_report, description: 根据销售数据生成中文周报文本。输入必须是通过 query_sales_data 返回的 JSON 结构输出为一篇包含总体概览和趋势分析的周报。, input_schema: { type: object, properties: { sales_data: { type: object, description: query_sales_data 技能返回的完整 JSON 数据 }, tone: { type: string, enum: [flat, positive, negative], description: 报告语气flat 为客观陈述positive 为积极解读negative 为风险预警 } }, required: [sales_data, tone] } }这个技能的执行体本质是一条提示词模板。但我强烈不建议把提示词当成普通字符串直接拼进系统提示词里而是应该通过一个函数来组装def generate_weekly_report(sales_data: dict, tone: str) - str: prompt f 你是业务分析助理。请根据以下结构化销售数据生成一份周报。 数据 {json.dumps(sales_data, ensure_asciiFalse, indent2)} 要求 - 使用中文分三段总体概览、产品线分析、趋势判断 - 语气{tone} - 结论处给出至少一条可执行的建议 response call_llm(prompt) return response把数据 JSON 直接嵌入提示词优点是简单直接模型不需要额外记忆。缺点是数据量大的时候会超过上下文窗口所以实际项目中我会额外做一个数据摘要步骤把原始数据先压缩成每日聚合值再喂给提示词。这一步放在技能内部实现对外部调用方无感。3.5 双技能组合的调用路径当用户说帮我看看硬件产品线这周的销售情况写个简短汇报时Agent 主流程做的事情是解析意图识别出需要查数据和写报告两个技能调用query_sales_data传入product_linehardware、start_date本周一、end_date今天拿到返回值后把里面的daily_data和summary传给generate_weekly_report把生成的报告文本返回给用户。这个过程中两个技能互不感知对方存在只通过标准输入输出协作。这种设计最大的价值是你可以随意替换某个技能的内部实现只要输入输出格式不变整个系统照常运转。比如把数据库从 MySQL 换成 ClickHouse只需要改_load_from_database一个函数其他全部不动。4. 注册、调度与回退技能进 Agent 主流程的完整链路技能写好了不等于 Agent 就能用了。要把技能真正接入主流程需要一套注册、调度、回退的机制。这块是最容易被忽视的部分——很多人写完技能就完事了结果线上模型根本不调用或者调用了但报错没人管。4.1 技能注册表注册表的作用是让 Agent 在每次请求时知道自己有哪些技能可用。实现方式不复杂核心是维护一个技能清单并在每次对话开始时把它注入上下文或者作为工具定义传入模型。我建议注册表里除了技能本身再维护三个字段status启用/停用、version版本号、owner负责人。这三个字段平时看着不起眼等技能数量超过 20 个的时候你会感谢自己当初留了这几个字段。没有状态管理你没法做灰度没有版本管理你回滚都不知道回哪一版没有负责人出 bug 找不到人。SKILL_REGISTRY { query_sales_data: { status: enabled, version: 1.2.0, owner: data-team }, generate_weekly_report: { status: enabled, version: 0.9.1, owner: ai-team } }4.2 意图路由的两种策略技能接入主流程后下一个问题是模型怎么知道该用哪个技能业界主流做法有两种——让 LLM 自己选和先用规则分流再让 LLM 选。让 LLM 自己选是最省事的方案。把所有技能的描述塞给模型让它从用户消息中判断该调用哪个。这种方式适合技能数量少个位数、技能边界清晰的场景。缺点也很明显技能一旦超过 15 到 20 个模型的选择准确率会明显下降A 技能描述里带了个搜索B 技能描述里也带了个搜索模型就开始纠结了。规则分流适合技能数量多的场景。我经历过一个项目内部技能库有 30 多个技能直接全量注入模型效果惨不忍睹。后来我们加了一道轻量级意图分类器先用一个小的 embedding 模型把用户请求和技能描述都向量化算相似度取 Top 5 技能再把这 5 个技能的描述交给 LLM 做精确路由。这一层粗排 精排的思路让路由准确率从不到 80% 提到了 95% 以上而且因为少传入大量无关技能描述Token 消耗也降了不少。4.3 调用失败后的回退机制技能调用一定会失败这是你需要提前接受的事实。失败的原因五花八门模型传了无法解析的参数、数据库连接超时、第三方 API 限流、执行体抛了个没预料到的异常……如果没有回退机制用户看到的就是一句冷冰冰的系统错误。我在工程里会为每个技能配置回退策略按顺序执行第一步自动重试一次。很多失败是瞬时的比如网络抖动重试就能解决第二步如果重试还是失败把错误信息返回给模型让模型尝试用另一种方式调用。比如用户要的数据在数据库里没有模型可以改个参数再查一次第三步仍然失败则降级到兜底回复模板告诉用户当前数据源暂时不可用请稍后再试同时把完整错误记录到日志和告警系统。回退机制听起来简单但最容易被忽略的是错误信息要传回给模型这一步。很多实现里执行体报错后直接在服务端被吞掉了模型完全不知道发生了什么只能瞎编结果。正确做法是把异常信息格式化成一段结构化错误描述塞到模型可见的上下文中让它有据可依地决定下一步动作。5. 迭代与避坑我在实际项目中踩过的技能化深坑做技能化一年多踩过的坑比写过的代码还多。挑几个印象最深的讲每一个都是线上事故换来的教训。5.1 技能描述写得太文青模型就会开始自发创作有次我写一个查员工加班时长的技能描述写的是此技能用于查看团队成员的工作投入情况帮助管理者了解大家的工作状态。上线后模型在用户问今天有几个人请了事假的时候把这个技能调出来了返回了一堆加班数据用户一脸懵。后来我把描述改成当用户需要查询员工在指定日期范围内的工作时长记录或加班统计时使用此技能。请传入员工 ID 或部门编号。此技能不包含请假数据不包含考勤异常。加了两个不包含模型立刻老实了。技能描述里的正反面示例都很重要——你要明确告诉模型这不是用来干什么的才能有效减少误调用。5.2 技能粒度别拆到原子级也别做成全家桶技能粒度这件事没有绝对标准但我有一个经验法则一个技能应当对应一个用户可感知的完整意图。发送邮件是合理粒度打开邮件编辑器偏细处理所有邮件相关的事情偏粗。我见过最惨烈的例子是把查天气拆成了获取经纬度、调用天气 API、格式化天气信息三个技能。表面上很优雅实际上模型根本不知道用户说今天上海热不热时该依次调用哪三个技能链路一长就断路。反过来也有同事把查用户、查订单、查物流合并成一个综合查询技能结果模型经常搞不清该传哪些参数返回的数据要么多余要么缺失。这两种极端都必须避免。5.3 技能之间的隐式依赖是最大的坑当技能 A 的输出是技能 B 的输入时两个技能之间就产生了隐式依赖。这种依赖在单机开发时不会暴露一旦技能上了生产环境A 返回的数据格式稍微变一点B 就炸了。我后来养成了一个习惯给技能的输出单独做一层适配层。也就是说B 技能不直接消费 A 技能的原始返回值而是先把 A 的返回值过一遍适配函数转换成 B 内部约定的数据结构。这样哪怕 A 的输出从数组变成了对象只要适配层改几个字段映射就能顶住而不是让 B 内部所有逻辑跟着返工。5.4 测试技能需要一整套仿真用户单测技能本身不难难的是模拟模型会怎样调用技能。模型比你想象的更会钻空子也更会犯低级错误。比如明明 enum 里只有三个值它非要传一个不在枚举里的字符串明明描述写了仅限中文它用日文回你。我的经验是每个技能都要准备三类测试用例标准用例用户意图清晰、参数完整、模糊用例用户表达含糊、需要模型推断部分参数、对抗用例用户意图与技能描述部分相关但本质不相关。特别是对抗用例一定要覆盖全。只有技能在面对看似该用但实际不该用的请求时按兵不动这个技能才算真正可靠。5.5 版本管理不要只盯着代码技能出了新版本代码仓库确实有记录但线上的 Agent 还在用旧版本这种问题经常发生。我的习惯是给每个技能设一个独立的版本号通过注册表维护与代码仓库的 Git 版本独立开。每次修改技能描述或执行体版本号加一同时在变更日志里写清楚改了什么、为什么改、影响了哪些用户话术。这样做的好处是线上出了问题你能立刻回答出当前这个技能版本是谁改的、改动点在哪。最后说点实在的技能化这条路走到现在给我最大的感受是Agent 开发早期拼的是模型的聪明程度后期拼的却是工程化的规整程度。模型再聪明遇到一团乱麻的提示词和接口设计照样在原地打转。而把能力拆成一个个边界清晰的技能看似多写了不少结构代码实则让整个系统从一个不可解释的黑盒变成了一个可以逐段排查、逐段优化的积木系统。如果让我给一个最具体的建议那就是别追求一次把所有东西都写成技能而是从你调用频率最高的那个功能开始拆一个、测一个、稳定一个再往下推。拆到第三个第四个的时候你会明显感觉后面的速度越来越快因为套路你已经摸透了。
