还在手搓 Agent ?SKILL.md 正在让一切变得不同
Agent 跑偏的时候,日志一般长这样:
tool_call=python.run
reason="need to check api"
args={"cmd":"curl https://prod.xxx.com/order/create"}
看到 prod 我就不太想往下看了。
这种 Agent 不是不会干活,是你把它养成了一个“全靠临场发挥”的东西。prompt 里塞一堆规则,工具列表挂一屏,最后再补一句“请谨慎操作”。这玩意儿在线上排障里,我一般是不信的。
现在 SKILL.md 出来以后,这个写法开始变味了。
以前我们手搓 Agent,大概是这样:
SYSTEM_PROMPT = """
你是一个后端排障助手。
遇到接口问题先看日志,再看 trace,再看 SQL。
不要直接操作生产环境。
生成报告要包含原因、证据、修复建议。
"""TOOLS = ["read_log", "query_trace", "run_sql", "write_report"]
这段代码看着没问题,其实问题很大。
规则在 prompt 里,工具在代码里,排查顺序在脑子里。过两周再来一个“接口压测分析 Agent”,你大概率复制一份 prompt,改几个词。再过两个月,项目里全是这种半成品 Agent。
SKILL.md 换了个思路。
它不是让你再造一个 Agent,而是把“怎么做一件事”沉到一个目录里。一个 Skill 通常就是一个文件夹,里面放 SKILL.md,再按需要放脚本、模板、参考文档。公开的 Agent Skills 规范里也是这么描述的:核心就是一个带 SKILL.md 的文件夹,里面可以带 metadata、instructions、scripts、references 这些东西。
我更关心的是这点:它把能力从“Agent 代码”里拆出来了。
比如接口冒烟检查,不要写死在 Agent 主逻辑里,直接这样放:
skills/
api-smoke-check/
SKILL.md
scripts/
smoke_check.py
SKILL.md 可以写得很短,别写成论文:
---
name: api-smoke-check
description: 当用户要求检查测试环境接口、回归核心链路、确认发布后接口是否正常时使用
---# API Smoke Check
只检查非生产环境,除非用户明确给出 allow_prod=true。
执行顺序:
1. 读取 endpoints.yml
2. 对每个接口发起请求
3. 记录 status、耗时、响应里的 biz_code
4. 失败接口必须保留请求参数和响应摘要
5. 最后生成一份 Markdown 报告
异常判断:
- HTTP 状态码不是 2xx,算失败
- biz_code 不等于 0,算失败
- 单接口耗时超过 1500ms,标记为 slow
这东西比一大坨 prompt 靠谱。
因为它有边界,有触发条件,有执行顺序,还有不能碰生产的限制。Agent 不是每次从头猜,而是任务命中这个 Skill 后,再加载里面的详细规则。Codex 的文档里也提到,它会先在上下文中保留可用 skills 的初始列表,决定使用某个 skill 后才加载完整 SKILL.md 指令。
脚本也别写成通用框架,现场够用就行:
# skills/api-smoke-check/scripts/smoke_check.py
import json
import time
from pathlib import Pathimport requests
import yaml
defcall_one(base_url, item):
url = base_url.rstrip("/") + item["path"]
started = time.time()
try:
resp = requests.request(
method=item.get("method", "GET"),
url=url,
json=item.get("body"),
timeout=item.get("timeout", 3),
)
cost_ms = int((time.time() - started) * 1000)
body = {}
try:
body = resp.json()
except Exception:
body = {"raw": resp.text[:300]}
biz_code = body.get("biz_code")
ok = 200 <= resp.status_code < 300and biz_code in (0, None)
return {
"name": item["name"],
"url": url,
"ok": ok,
"status": resp.status_code,
"biz_code": biz_code,
"cost_ms": cost_ms,
"slow": cost_ms > 1500,
"sample": body,
}
except Exception as e:
return {
"name": item["name"],
"url": url,
"ok": False,
"error": repr(e),
}
defmain():
cfg = yaml.safe_load(Path("endpoints.yml").read_text(encoding="utf-8"))
env = cfg["env"]
if env == "prod"andnot cfg.get("allow_prod"):
raise SystemExit("refuse to run on prod without allow_prod=true")
rows = [call_one(cfg["base_url"], item) for item in cfg["endpoints"]]
failed = [r for r in rows ifnot r["ok"]]
slow = [r for r in rows if r.get("slow")]
print(json.dumps({
"env": env,
"total": len(rows),
"failed": len(failed),
"slow": len(slow),
"items": rows,
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
这段代码没有什么高级东西,但它把排障习惯写死了:不碰生产、保留失败样本、单独标 slow、输出结构化结果。
这就是我觉得 SKILL.md 真正有用的地方。
不是“让 Agent 更聪明”,而是让 Agent 少自由发挥。
以前一个 Agent 要会查日志、写 SQL、跑接口、生成报告、改代码,最后就变成一个巨型提示词怪物。现在可以拆成几个 Skill:
slow-sql-review/
log-trace-lookup/
api-smoke-check/
release-note-writer/
python-batch-cleaner/
每个 Skill 只管一件事。
查慢 SQL 的 Skill 里就写:先要 EXPLAIN,再看扫描行数,再看索引,再看回表,不要一上来建议加缓存。
处理日志的 Skill 里就写:先按 trace_id 聚合,再按 ERROR 过滤,再找调用链断点,不要拿单行异常直接下结论。
写 Python 清洗脚本的 Skill 里就写:先备份原文件,输出 dry-run 统计,再真正落盘。
这些东西放在代码里也能做,但改起来麻烦。放在 prompt 里也能做,但容易越写越乱。放在 SKILL.md 里,至少能 review,能进 Git,能被团队复用。
当然,别把 SKILL.md 当银弹。
它也是指令,而且是能影响 Agent 行为的指令。第三方 Skill 不能随便装。近期也有研究专门提到,SKILL.md 这类自然语言元数据和指令会影响技能的发现、选择和加载,存在语义层面的供应链风险。
所以我现在看 Skill,第一眼不是看它功能多炫。
我先看三件事:
1. 有没有偷偷要求读取敏感文件
2. 有没有执行 shell / 网络请求
3. 有没有把结果发到外部地址
这跟看一个陌生 npm 包、pip 包差不多。你不能因为它是 Markdown,就觉得它安全。
SKILL.md 这波变化,最值得关注的不是格式,而是分工变了。
Agent 主体以后不该塞太多业务细节。它只负责理解任务、选择 Skill、调用工具、收口结果。
真正值钱的东西,是你们团队沉淀下来的那些判断顺序:
接口超时先不看业务代码,先看 trace。
SQL 慢别急着加索引,先看执行计划是不是走错。
批处理脚本别直接写库,先 dry-run。
线上操作没有回滚方案,就先别动。
这些以前都在老程序员脑子里,现在可以一点点写进 SKILL.md。
手搓 Agent 不是不能写。
只是再把所有能力揉进一个 prompt 里,后面维护起来,大概率会像一锅没分层的老系统。能跑,但没人敢改。