ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent技能系统工程化:从Skills设计到稳定落地

AI Agent技能系统工程化:从Skills设计到稳定落地

1. Skills 的本质与工程化设计

1.1 从“对话智能”到“执行智能”

最近大半年,AI Agent 这个词几乎被聊烂了。从最早只会“你问我答”的对话助手,到能自己规划步骤、调用工具、执行脚本的智能体,变化非常明显。但接触的团队越多,越发现一个共性问题:很多人做了一个 Agent 的 Demo,能回答几个问题,能调一两个 API,就认为“跑通了”。真正扔到生产环境,问题立刻冒出来——技能文件散落各处,模型频繁挑错工具,脚本改了没有记录,出了故障不知道回滚到哪个版本。

说白了,Agent 光有脑子不够,还得有手。这双手,就是技能系统(Skills)。而让这些技能稳定、可控、可维护地运转,就需要一套工程工作流。

一个没有 Skills 的 Agent 只能基于预训练知识做“文本推演”,本质上和搜索引擎加文本生成的差别不大。一旦接入技能,Agent 才真正具备了“读取系统状态、操作文件、调用接口、执行计算”的能力。举个例子:让 AI 生成一份周报,没有技能时它只能给你一段模板文字;有了技能,它可以主动运行收集命令、读取 git 提交记录、扫描项目目录文件,再把这些真实数据组织成结构化报告。这就是“执行智能”和“对话智能”的分水岭。

有人会问:为什么不直接在 Prompt 里让 AI 写 Python 代码来完成这些事?答案很简单——LLM 当场生成的代码不稳定,很容易出现依赖缺失、路径写错、语法出错等问题。与其让模型临场发挥,不如把固定操作固化成经过测试的技能脚本。开发者的价值,就是把 AI 不可控的“临场发挥”变成可控的“标准动作”。

1.2 技能工作流的三条设计原则

做技能系统之前,先立三条原则。

第一条,技能要薄,逻辑要厚。模型负责理解意图、选择技能、汇总结果,脚本负责精确执行。不要把复杂的业务逻辑塞进自然语言描述里,那是模型最不擅长的工作方式。数据清洗、文件处理、指标计算这类逻辑全部下沉到脚本,模型只保留一个简单的调用入口。

第二条,一切可复现。技能目录结构必须是固定的,每个技能都有明确的名称、描述、参数、测试用例和变更记录。任何人接手一套技能库,只要按文档操作,就能在本地跑起来。最忌讳的是“一个人写了一套技能,走了之后没人能维护”。

第三条,失败要可控。技能执行失败不能卡死整个 Agent 流程,要能识别失败原因、给出明确错误信息、必要时自动重试或回退。一个技能如果失败时只能吐出一段莫名其妙的长英文报错,那这种技能就不合格。

这三条原则决定了下文所有技术方案的选择方向。

1.3 技能边界:单体技能与复合技能

实际项目里,技能最好分两类看待。

单体技能只做一件独立的事,比如“查询磁盘剩余空间”“批量重命名某个目录下的文件”“读取某个接口的数据并存为 JSON”。这类技能的特点是职责单一、参数少、输出稳定,非常适合作为技能库的第一批成员。

复合技能则要编排多个步骤,比如“生成项目周报”这一件事,背后往往要读取 git 提交记录、统计代码变更量、读取项目配置文件、生成 Markdown 文档。如果把这些逻辑全塞进一个技能脚本里,脚本会变得又长又笨,模型选择一个技能时很难判断它到底适不适用手头的需求,参数传递也容易出错。

我比较推荐的做法是“小技能 + 上层编排”。把收集提交记录、统计变更量、渲染模板分别做成三个独立的小技能,再让 Agent 的任务层或工作流引擎按顺序调用它们。这样每个技能都可以单独测试、单独复用,失败时也能精确定位到具体步骤。刚开始搭技能库时,一定要抵挡住做“万能技能”的诱惑。

2. 技能目录与配置规范

2.1 标准目录结构

技能系统想工程化,第一步是统一目录结构。目前主流 AI Agent 框架大多采用“一个技能一个目录”的组织方式,目录内放说明文件、脚本、资源和测试。

一个我常用的标准布局如下:

skills/ └── weekly-report/ ├── SKILL.md ├── scripts/ │ ├── collect_metrics.py │ └── render_report.py ├── resources/ │ └── report_template.md └── tests/ ├── test_collect_metrics.py └── fixtures/ └── sample_git_log.txt

SKILL.md 是这个技能的“说明书”,模型靠它理解什么时候该用、参数怎么传。scripts 目录放可执行脚本,resources 放模板和静态配置,tests 放测试用例和模拟数据。

这个结构看似简单,但统一之后收益立竿见影:新人接手时不用猜,自动化工具可以按固定路径扫描技能,版本对比也可以精确到文件级。目录命名必须直观,比如 report-generator、disk-cleaner,不要用 dev_tool_01 这种毫无语义的命名。

2.2 SKILL.md 里该写什么

SKILL.md 是整个技能系统里最关键的配置文件,模型对技能的理解几乎完全来自这份文档。写不好,脚本再牛逼也白搭。

我一般按下面几个板块组织:

  • 技能名称:必须简洁且唯一,通常是“动词-名词”形式,比如 generate-weekly-report。
  • 功能描述:用两到三句话说明这个技能做什么,以及它适合哪些场景。
  • 参数定义:每个参数的类型、必填性、默认值、含义。
  • 执行命令:模型调用技能时应该执行的命令,包含参数模板。
  • 示例:一个完整的调用示例,这是模型参考价值最高的部分。
  • 边界与限制:什么情况下不要用这个技能,能显著减少误用。

实际效果参考一份 SKILL.md 的写法:

--- name: generate-weekly-report description: 生成团队周报。适用于用户要求统计本周工作内容、整理 git 提交记录、或生成项目进展摘要的场景。 parameters: - name: team_name type: string required: true description: 团队名称,用于报告标题 - name: since type: date required: false description: 统计起始日期,默认本周一 - name: output_dir type: string required: false description: 输出目录,默认当前目录 --- 运行以下命令生成周报: python3 scripts/collect_metrics.py --since "{{since}}" --repo "{{repo_path}}" python3 scripts/render_report.py --data metrics.json --template resources/report_template.md --output "{{output_dir}}/weekly-report.md"

注意一个细节:参数模板用{{since}}这种占位符,不要直接写具体日期。模型在调用时会按照上下文替你完成参数填充,你传的占位符越规范,模型就越知道你期望它做什么。

2.3 描述与参数:一次写好,模型按提示选择

技能描述看似只是几句话,实际是模型做“技能选择”的最重要依据。我的经验是:描述必须写成“用户潜在表达”的样子,而不是写成技术文档摘要。

比如一个错误示范是“提供文件处理能力”,这种描述太宽泛,模型根本分不清它和另一个“文件管理技能”有什么差别。更合适的写法是:“批量将 CSV 文件转换为 JSON 格式,适用于用户上传 CSV 后需要结构化数据的场景”,直接点出输入、操作、输出和应用场景。

参数定义同样需要精细。必填参数必须标required: true,带默认值的参数要写明默认值。类型最好明确,是 string、integer 还是 array,模型在生成调用时会按类型自动处理。

还有一个很容易被忽略的点:给每个技能加“不适用场景”或者“边界”说明。比如一个“查询代码仓库信息”的技能,明确写一句“如果用户只是要写一段示例代码,请勿调用本技能,直接回答即可”。看似多此一举,实测下来能避免大量误调用。

2.4 脚本与资源的组织方式

脚本是整个技能的执行核心,工程化程度直接影响稳定性。我的建议是脚本必须自包含,不要依赖执行时现场安装的包,所有第三方依赖写在单独的requirements.txt里,并在 README 中注明安装命令。

入口设计统一走命令行接口,参数用标准的 argparse 或类似方式解析,输入输出尽量保持简单:结果输出到 stdout,错误输出到 stderr,退出码为 0 表示成功、非 0 表示失败。复杂结果以 JSON 或 Markdown 格式输出,模型读取结构化结果比读取普通文本要准确得多。

资源文件只放静态内容,比如报告模板、配置文件、示例数据。不能把临时生成的文件放进 resources,否则会在版本控制里制造大量噪音。再说一个实操细节:脚本文件头要加#!/usr/bin/env python3和# -*- coding: utf-8 -*-,并且指定 Python 版本,避免不同环境下解释器不一致导致技能执行失败。

3. 实操:从零搭建一套可用的技能工作流

3.1 初始化技能仓库与版本管理

动手第一步,创建一个 monorepo 风格的技能仓库。这样做的好处是技能与技能之间可以通过相对路径引用公共库,版本管理也集中在同一个仓库里。

初始化时我会做这几件事:

mkdir agent-skills && cd agent-skills git init mkdir skills touch README.md

然后在根目录放一份pyproject.toml,声明公共依赖和 Python 版本。每个技能目录内单独保留requirements.txt,避免所有技能共享一个依赖清单导致升级时的相互影响。

版本管理我使用 git tag。技能达到可发布状态时打一个 tag,比如skills-weekly-report-v1.0.0,线上服务加载时指定版本范围,需要回滚时直接切换到上一个 tag。这个习惯在多人协作时非常关键——你把描述改了一个字,可能就改变模型对技能的判断,所以任何变更都必须有版本记录。

3.2 实现第一个实用技能:数据整理

用一个实际场景演示技能开发全过程:让 Agent 能够在收到 CSV 文件后输出一份清洗后的 JSON 文件和摘要。

第一步,建立技能目录和脚本。我选择实现一个名为csv-cleaner的技能:

mkdir -p skills/csv-cleaner/{scripts,resources,tests}

第二步,编写核心脚本clean_csv.py。脚本逻辑很简单:读取 CSV,去除重复行,填充缺失字段默认值,输出 JSON。

#!/usr/bin/env python3 import argparse import csv import json import sys from pathlib import Path def main(): parser = argparse.ArgumentParser(description="Clean a CSV file and output JSON.") parser.add_argument("--input", required=True, help="Input CSV file path") parser.add_argument("--output", required=True, help="Output JSON file path") parser.add_argument("--fill-missing", default="unknown", help="Value for missing fields") args = parser.parse_args() input_path = Path(args.input) if not input_path.exists(): print(f"ERROR: input file {input_path} not found", file=sys.stderr) sys.exit(1) rows = [] with input_path.open(encoding="utf-8") as f: reader = csv.DictReader(f) for line in reader: cleaned = {k: (v if v else args.fill_missing) for k, v in line.items()} rows.append(cleaned) deduplicated = [dict(t) for t in {tuple(r.items()) for r in rows}] with open(args.output, "w", encoding="utf-8") as f: json.dump({"records": deduplicated, "total": len(deduplicated)}, f, ensure_ascii=False, indent=2) print(json.dumps({"output": str(args.output), "total": len(deduplicated)}, ensure_ascii=False)) if __name__ == "__main__": main()

第三步,编写 SKILL.md,把技能描述、参数和调用示例写清楚。这一步是整个技能能否被模型正确调用的关键,描述里要明确输入是“CSV 文件路径”,输出是“JSON 文件路径和记录总数”,同时给出一个完整调用示例。

第四步,本地测试。准备好一个包含重复项的sample.csv,运行脚本检查输出 JSON 是否符合预期。这一步千万别省,脚本一旦上线,模型会以你从未想到的方式调它。

3.3 技能加载:手动与自动的取舍

技能做好之后,需要考虑怎么让 Agent 加载它。主流的加载方式有三种。

手动加载是固定的:你显式配置一个技能列表,Agent 每次运行都携带这份列表。优点是行为可预测,模型只会在这几个技能里做选择,不会乱调用;缺点是不够灵活,技能数量一多,全量携带会消耗大量上下文窗口。

自动加载则相反:Agent 根据用户的当前请求,从一个大的技能库中动态挑选可能相关的技能。优点是灵活,缺点是选择有时不靠谱,很容易出现“选错技能”的情况,尤其在技能描述写得模糊的时候。

混合加载是我现在用得最多的方案:核心技能常驻,长尾技能动态加载。常驻技能数量控制在 5 个以内,通常是最高频使用的;其他技能放在索引里,由模型按需检索。这个方案兼顾了稳定性和灵活性,生产环境建议优先复用。

3.4 把复杂逻辑下沉到脚本,保持技能“薄”

刚开始做技能系统的人,最容易犯的一个错误是让 Agent 在描述里编写内联代码,或者试图用自然语言描述所有逻辑步骤。比如让模型“先检查每一行有没有空值,如果为空就填 unknown,再去重,再转 JSON”,这不是技能系统,这是靠运气做工程。

正确姿势是:把这类确定性逻辑全部写进脚本,SKILL.md 只描述“你要处理什么输入,会得到什么输出,怎么调用”。这样模型不需要理解 CSV 去重的细节,只负责判断“用户要清理 CSV,我应该选 csv-cleaner 技能”。

优势非常明显。第一,脚本是经过测试的,不会像模型现场写代码那样出现语法错误和逻辑漏洞。第二,执行结果稳定,输入相同输出一定相同,便于排查问题。第三,token 消耗大大降低,模型不再需要在上下文中生成一大段代码。第四,脚本可以独立优化,比如性能问题、边界问题都只改脚本体,不动 SKILL.md 和模型行为。

4. 工程化流程:测试、发布与监控

4.1 技能开发的标准流程

技能系统要长期维护,必须有一套标准开发流程。我在项目里规定,每个技能从需求到上线至少要经过七个环节。

  1. 需求描述:用几句话说明技能要解决什么问题,给出两个典型的用户输入示例。
  2. 编写 SKILL.md 草稿:重点是写清楚描述、参数和调用示例,先给业务方评审描述是否容易被理解。
  3. 实现脚本:严格遵守统一 CLI 约定,输出结构标准化。
  4. 编写测试:覆盖正常流程、边界条件、错误输入。
  5. 本地联调:用小样本对话验证模型能否正确选择并调用该技能。
  6. 评审与合并:至少一名同事评审技能描述和脚本实现。
  7. 打标签发布:按语义化版本打 tag,更新技能库索引。

这套流程看起来有点重,但一旦技能数量超过十个,没有流程约束几乎一定会乱。哪怕是一个人维护,也要按这个流程走,因为你会忘掉自己两个月前写的细节。

4.2 测试策略:单元测试、集成测试与回放验证

技能测试不是“脚本能跑就行”,至少要覆盖三层。

单元测试只针对脚本,用 pytest 直接调用脚本里的函数,验证数据处理逻辑。比如clean_csv.py对空值、乱序字段、非法字符的处理是否正确。这些测试跑得飞快,每次改代码都要全量跑一遍。

集成测试验证“模型调用脚本”这条链路。方式是用一个模拟的用户请求,让 Agent 选择技能并执行脚本,最后断言输出文件的格式和内容。这种测试适合放到 CI 里,每次提交代码后自动跑。

回放验证是我近期比较依赖的一种测试方式。把历史真实对话中的技能调用记录存下来,包括当时用户的请求、模型选择的技能、传入的参数。技能升级时,用同一批请求重新跑一遍,对比新旧输出,检查有没有行为漂移。这个方法能发现很多稀奇古怪的问题,比如你只改了一行描述,模型就从选技能 A 变成选技能 B 了。

沙箱验证也值得一提。所有需要连接外部系统或真实数据的技能,在测试阶段一律在隔离环境中执行,用模拟数据和 mock 接口替代真实依赖,保证测试不会因为外部故障而失败。

4.3 灰度发布与回滚策略

技能发布和代码发布一样,最忌讳一次性全量上线。我会先在受限环境里加载新版本技能,让少量内部会话使用,观察调用成功率和输出质量。

为什么需要灰度?因为技能的“逻辑正确”和“模型正确使用它”是两件完全不同的事。脚本写得再好,如果新描述让模型产生了误解,线上就会出现大量误调用或拒绝调用。灰度阶段重点观察的是:调用次数是否合理、选择是否准确、失败率是否异常。

回滚策略我建议做到“一键级别”:每个技能发布时保留上一版本的完整目录快照,线上加载器支持按 tag 切换版本。出问题时,只需要把加载器配置里的版本号改回去,然后重新加载技能索引。整个过程应该在五分钟内完成,而不是翻 git 历史重新部署。

4.4 调用日志与质量评估

技能上线之后,必须建立监控。裸奔的技能库,跟写了代码不提交 git 一个性质。

每次技能被调用,至少记录以下字段:技能名称、触发时间、调用耗时、退出码、错误信息(如果有)、传入参数摘要、关联的会话 ID、消耗的 token 数。这些日志统一写入结构化存储,方便后续按技能维度做聚合分析。

我这里有一个简单可用的质量评估指标:技能失败率 = 调用失败次数 / 总调用次数,超过 5% 需要立刻排查;技能召回率指的是“用户有真实需求时模型是否正确选择了该技能”,这个指标没有自动化手段时,可以抽样人工审核。

日志还有一个重要用途是优化技能描述。翻看日志时会发现模型在某些场景下选错了技能,比如用户说的是“处理一下这个 Excel”,模型却选中了 csv-cleaner 而不是 excel-processor。这类 case 积累到一定数量,就要反向修改 SKILL.md 的描述,把这类用户表达写进新技能的描述里,或者写进旧技能的“不适用场景”里。

5. 常见问题排查实录

5.1 技能老是选错或答非所问

这个问题的出现概率最高,根因往往不在模型,而在技能描述。模型不会“记住你心里的预期”,它只能根据 SKILL.md 的文本做判断。

排查方向按顺序来:先确认是不是多个技能描述重叠了。把技能列表中所有 SKILL.md 的描述并排列出来,看是否存在“谁都像谁”的情况。比如“整理数据”“处理数据”“转换数据”三个描述放在一起,模型能分清才怪。

修复方式:第一,描述里加入典型用户提问的原文,比如“当用户说‘帮我把这个 CSV 清理一下’时使用本技能”;第二,给容易混淆的技能之间增加排除性描述,比如在 csv-cleaner 里写“本技能不处理 Excel 文件,需要处理 xlsx 请选择 excel-processor”;第三,简化参数名,不要用内部叫法,直接用用户能理解的业务术语。

改完描述之后,一定要做回放测试,拿历史对话跑一遍,确认选择准确率确实提升,而不是靠感觉。

5.2 脚本执行失败:路径、权限和时间耗尽

脚本失败常见的有三类,先说路径问题。Agent 执行脚本时的工作目录不一定是技能所在目录,你在本地写相对路径能跑,线上很可能报“文件找不到”。解决办法是脚本内部用入口文件定位自身目录:

from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent

然后用BASE_DIR拼接所有资源和输出路径,绝不依赖进程当前工作目录。

权限问题多发生在读取系统状态或写入特定目录时。技能脚本要提前检查目标目录是否存在、是否可写,不行就输出明确错误并退出,不要抛一个 Python traceback 让模型试图去猜。

超时问题更隐蔽。模型在平台上调用技能通常有超时限制,长时间执行的脚本可能直接被平台杀掉。解决办法是脚本内部设置处理超时、分批处理大数据集,并且卡住时把执行状态及时写入文件,这样即使被杀,也能在下次运行时从断点恢复。

5.3 技能更新不生效

明明改好了 SKILL.md,实际测试时发现模型还在用旧版本,让人非常抓狂。

这类问题先看有没有缓存。很多 Agent 框架会把技能描述在会话启动时固定下来,运行中不会重新读取文件改动。修改技能后,需要重新加载索引、重启会话,或者等待缓存失效。

再看版本号有没有变化。如果 git tag 没更新,线上加载器可能仍指向旧版本。我的习惯是任何技能变更都同步升级小版本号,哪怕只是改了一个字的描述,也至少 bump patch 版本。这样排查时能明确分辨“我改的东西有没有被加载到”。

实在不行就在技能索引里加一个“最后更新时间”字段,日志里也能准确定位当前会话到底加载的是哪一版技能。这个字段很小,但在多人协作时能省掉一大半沟通成本。

5.4 并发调用时的状态污染

技能系统一旦跑真实业务,并发调用就躲不开。多个会话同时调用同一个技能,如果脚本用了固定临时文件或固定输出路径,就会出现文件互相覆盖、数据错乱的问题。

解决办法是彻底隔离会话状态。脚本内临时文件一律用tempfile.mkdtemp()创建,输出文件名加随机后缀或会话 ID。比如:

import tempfile import uuid tmp_dir = tempfile.mkdtemp(prefix="skill-clean-") session_id = uuid.uuid4().hex output_path = Path(args.output_dir) / f"result_{session_id}.json"

不要让技能脚本修改任何全局共享目录,也不要依赖内存中的全局变量。幂等性也是一个重要指标:同一个技能在相同输入下多次执行,结果应该完全一致。如果有随机因素,要显式固定随机种子并有日志解释。

6. 写在最后:我的几点体会

做技能系统这几个月,我最大的体会是:AI Agent 的工程化,核心不是把模型调得多聪明,而是把外部世界变得足够可靠。技能的脚本层,就是 Agent 和真实系统之间的“减震器”。你不可能指望模型每次都能写出正确的文件处理代码,但你可以写一个经过测试的脚本让它调用几百次。

我建议所有刚开始搭技能库的朋友,克制住一次做十几个技能的冲动。先挑三个最高频、最底层的操作做成技能,把它们做到描述清晰、脚本健壮、测试完备。跑两三个星期之后,再用回放数据去优化描述、调整参数,慢慢地你的技能库会从“能用”变成“好用”。

最后分享一个我认为最有价值的小技巧:每个技能在发布时,保存一条“实测用例对话”。这条对话记录了三样东西——用户原话、模型正确选择的技能、模型传入的参数。下个版本的维护者打开仓库,一眼就能看到这个技能的预期效果,比读一万字文档都有用。这个习惯我一直在用,也是我能把几十个技能维护下来不翻车的重要原因。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进