技能终局Agent化:6个Harness编译器孵化独立Agent
先说结论(避免你划走)
三个月里,为 SKILL.md 打造了一个编译器,项目就叫 agenthatch。
核心任务只有一个:将一份用 Markdown 编写的 skill,编译成一个可 pip install、可独立运行、附带类型签名与状态机的 Python Agent。
它不是 prompt 封装、不是 RAG 外衣——是真正生成代码:pyproject.toml、agent.py、tools.py、runtime.toml,全套齐备,跑起来就是一个标准的 Python 包。
复制代码pip install agenthatch
agenthatch init
agenthatch skills add ./my-skill/SKILL.md
agenthatch hatch my-skill # 编译
agenthatch run my-skill # 运行
三个命令,markdown 秒变运行中的 agent。
下文会交代动机、设计思路与踩坑实录。绝非 README 搬运工,每个关键节点都附了源码路径,你大可自行验证。
一、起因:Claude Code 把我的 skill 当废纸处理
四月份,全公司推行 skill。每个团队都在写——测试、部署、代码审查,气氛热烈得像抢购。我也写了一个,把自己做 agent 开发的全部经验塞进去:每个 API 端点怎么调、MCP 怎么配、token 在哪类场景下不足、几个月沉淀下来的规则——一一写入 SKILL.md,作为工作成果提交。
然后眼睁睁看着 Claude Code 来执行它。
那些严格规定——"必须先检查环境变量"、"遇到 X 类型错误立即终止"——模型完全当耳旁风。它把 skill 当成一本参考手册,而非约束规范。每次执行,理解都偏离一点。有一次它差点让一个本应在测试环境拦截的问题溜进生产环境。
心血写的东西,在它手里只是一段可以被选择性忽略的散文。
你懂那种感觉吗?递交了一份自认为严谨的产出,但执行它的“大脑”根本不尊重它。
那天晚上,一个特别原始的想法冒出来:skill 不该被“解读”,它应该被“编译”。
二、范式缺陷在哪:SKILL.md 本质是散文
先把问题讲清楚,不跟你绕弯子。
一份 SKILL.md 文件,本质上是什么?是一段散文。人类写给人类看的散文。 然后你把它塞进 system prompt,让 LLM 在运行中自己去猜这段文字到底要求它做什么。
一两个 skill 还行。三个?凑合。五个以上?问题就开始了。
| 痛点 | 实际发生了什么 |
|---|---|
| 零隔离 | 所有 skill 挤在同一 context window 里。文件整理 skill 的逻辑会嫁接到 git 操作上,输出四不像。 |
| 参考书,不是操作手册 | Agent 把 SKILL.md 当作松散建议,而非契约。长 skill 它扫读,挑看起来相关的,忽略剩下的。 |
| Token 浪费 | 每个 SKILL.md 都驻留在 system prompt 里。5 个 skill 每个 3KB,开口前上下文已烧掉 15KB。长任务指数级恶化。 |
| 零校验 | 工具名拼错、参数缺失、指令歧义——agent 一个都抓不到,全留到 runtime 爆炸。等爆的时候对话已经 20 轮深了。 |
| 规模衰减 | 1-3 个 skill 还能用,10+ 就失控。没有依赖图,没有冲突检测,不知道谁覆盖谁。 |
这不是 Anthropic 的 bug。不是你 skill 写得差。这是架构层面的退化——一个 LLM 在同一段上下文里解读七份互相零隔离的散文,每次解读都不一致。
打个比方:让一个人同时看七本操作手册,每次问他问题都要从头翻这七本书再拼答案。这个人早晚疯掉。LLM 也一样。
核心问题不在格式。问题在于 SKILL.md 是 prompt engineering,不是 software engineering。你在让 LLM 在 runtime 解读人类散文,没有编译,没有类型检查,没有契约。
三、范式反转:skill 的终点是 agent 化
这是整篇文章最希望你记住的一句话,也是做这个项目的底层逻辑:
展开讲。
现在所有人都在写 skill,写给 Claude Code、写给 Codex CLI、写给 OpenClaw。但 skill 在这些工具里的角色是什么?是一段塞进 system prompt 的散文,被 LLM 在 runtime 反复解读。 skill 是 prompt 的一部分,永远依附于某个 host agent。
这个范式有问题。skill 不该是 prompt 的附属品,skill 应该是 agent 的源代码。
你想一想,Java 源码编译成字节码给 JVM 跑,TypeScript 编译成 JavaScript 给浏览器跑。编译这一步,在运行之前就把人类表达变成了机器能确定性执行的格式——编译时抓 typo,编译时做类型检查,编译时消灭歧义。
skill 缺的就是这一步。它没有编译。它把散文原封不动塞给 LLM,让 LLM 每次自己猜意思。
所以 skill 的终点不是“写得更好”,而是“被编译成 agent”。 skill 不该是 host agent 的 prompt 配件,skill 该是 agent 的孵化输入——你写 skill,编译器把它孵化成一个独立的、有自己运行时、有自己工具、有自己状态机的 agent。
这就是 agenthatch 做的事情。它不是 skill 的替代品(你的 skill 该怎么写还怎么写),它不是 Claude Code 的替代品(生成的 agent 完全独立运行)。它是中间那一步:把散文变成可执行代码。 javac 之于 .java,tsc 之于 .ts,agenthatch 之于 SKILL.md。
一旦你想通这一层,很多事就顺理成章:
- skill 不再死在 system prompt 里烧 token,编译后运行时只占 ~150 字节配置
- 每个 skill 编译成独立 agent,天然隔离,不再互相污染
- 编译时做 schema 校验,typo 和歧义在编译期就被抓,不留到 runtime
- 生成的 agent 能
pip install、能import、能独立跑,不依赖任何 host
四、三阶段管线:不画饼,源码在此
agenthatch 的核心是一条三阶段管线:
复制代码SKILL.md → Parse → 6-Harness LLM Pipeline → Code Generation → Runnable Agent
(输入) (Phase 1) (Phase 2: AI 推理) (Phase 3: Jinja2) (输出)
一个个讲,源码位置都贴出来,你可以去翻。
Phase 1:确定性解析,零 AI
Phase 1 不用 AI。直接把 SKILL.md 的 frontmatter、正文、目录文件拆出来。确定性操作,不存在 AI 随机性。
入口在 parser.py 的 assemble_context():
复制代码def assemble_context(skill_path: str | Path) -> ContextPack:
# Step 1: 解析路径 → 目录名
skill_dir = _resolve_skill_directory(Path(skill_path))
dir_name = skill_dir.name
# Step 2: 发现文件 → 生成清单(SHA-256 + 全文内容)
manifest = _discover_files(skill_dir)
# Step 3: 基于 YAML 的最佳解析 → frontmatter 字典 + body 原始文本
frontmatter, body, warnings = _best_effort_parse_yaml(skill_dir)
return ContextPack(frontmatter, body, manifest, dir_name, warnings, skill_dir)
关键设计:Phase 1 不做任何语义判断。文件是脚本、文档还是配置,Phase 1 不猜测——那是 Phase 2 的任务。Phase 1 只负责把字节读出来、计算 SHA-256、做 YAML 解析。
v0.8 版本加入了一个 Phase 1.5 ScriptAnalyzer,通过 AST 解析 Python 脚本、正则解析 shell 脚本,提取函数签名。这一步也是确定性的,喂给 Phase 2 的 Harness C 做精确接口推理。能用确定性解决的,绝不麻烦 LLM。
Phase 2:6 个 AI Harness 并行推理
这是整个项目的核心。六个专门化的 AI Harness 处理 skill,每个都有自己的 persona 和温度。
温度配置表在 engine.py 里写死了,直接贴:
复制代码HARNESS_CONFIG: dict[str, dict[str, Any]] = {
"A": {"thinking": True, "temperature": 0.1,
"reason": "身份提取是确定性的 — 低温保证一致性"},
"B": {"thinking": True, "temperature": 0.5,
"reason": "意图推理需要创造性以覆盖长尾触发词"},
"C": {"thinking": True, "temperature": 0.5,
"reason": "接口推理复杂 — 需要 SKILL.md + ScriptManifest"},
"D": {"thinking": True, "temperature": 0.3,
"reason": "基类检测需要精确 — 中等温度"},
"E": {"thinking": True, "temperature": 0.2,
"reason": "组装验证是结构化的 — 低温保证一致性"},
"F": {"thinking": True, "temperature": 0.3,
"reason": "MCP 配置提取需要精确匹配 — 中等温度"},
}
每个温度都不是随便定的,有 reason。身份提取是确定性的,温度压到 0.1;意图推理要覆盖长尾触发词,需要一点创造力,给 0.5;组装验证是结构化的,压到 0.2。
六个 Harness 各司其职:
| Harness | 职责 | 模型档位 | 温度 |
|---|---|---|---|
| A — Identity | 从 frontmatter 提取 name/version/description | small | 0.1 |
| B — Intent | 推理触发词和用户意图 | small | 0.5 |
| C — Interface | 设计工具签名、参数、返回类型 | large | 0.5 |
| D — Base | 检测运行时基类和指令结构 | large | 0.3 |
| E — Assembly | 交叉校验其他五个输出,产出 AHSSPEC | small | 0.2 |
| F — MCP | 检测并配置 MCP server 连接 | small | 0.3 |
为什么拆六个?因为我们试过一个超大 prompt 全搞定,输出像抽奖一样。 拆开之后每个只管一件事,质量提升了一大截。这跟编译器把前端拆成 lexer/parser/semantic 是一个道理——单一职责。
每个 Harness 维持一个 Analyze → Infer → Self-Validate → Correct 循环,最多两次内部重试。循环实现在 engine.py 的 AgentHarness.run():
复制代码def run(self, **inputs: object) -> HarnessOutput:
"""执行 Analyze → Infer → Self-Validate → Correct 循环."""
reasoning: list[str] = []
degradations: list[str] = []
retries = 0
system = self.build_system_prompt()
user = self.build_user_message(**inputs)
messages = [{"role": "system", "content": system},
{"role": "user", "content": user}]
reasoning.append(f"[{self.name}] analyze: 已接收输入")
# Step 1: 初始推理
result = self._infer(messages)
reasoning.append(f"[{self.name}] infer: 已收到输出,{len(str(result))} 字符")
# Step 2: 自校验 + 纠错循环
while retries <= self.max_internal_retries:
passed, reason = self.validate_output(result)
if passed:
reasoning.append(f"[{self.name}] self_validate: 通过")
break
reasoning.append(f"[{self.name}] self_validate: 未通过 — {reason}")
if retries >= self.max_internal_retries:
degradations.append(reason)
break
result = self.correct_on_failure(result, reason, **inputs)
retries += 1
confidence = self._estimate_confidence(result, degradations, retries)
return HarnessOutput(result, confidence, reasoning, ...)
每个 Harness 都有自己的 validate_output()。例如 Harness A 校验 identity.id 必须是 kebab-case:
复制代码def validate_output(self, result: dict[str, Any]) -> tuple[bool, str]:
identity = result.get("identity", {})
identity_id = identity.get("id", "")
if not identity_id:
return False, "identity.id 为空"
if not re.match(r"^[a-z0-9]+(-[a-z0-9]+)*$", identity_id):
return False, f"identity.id '{identity_id}' 不是 kebab-case 格式"
if not identity.get("display_name"):
return False, "identity.display_name 为空"
return True, ""
Harness B 校验 triggers 数量必须在 [5, 15],satisfies 在 [3, 8],summary 至少 20 字符。这些约束不是 LLM 自己说了算,是代码强制执行的。 LLM 输出不合规,立刻打回去重做。
E 最关键——它校验其他五个的输出,生成统一的 AHSSPEC(Agent Hatch Standard Specification)。E 还会计算一个 结构性置信度,不是 LLM 自评,是代码字段数字:
复制代码def _compute_structural_confidence(self, ahs_dict: dict[str, Any]) -> float:
checks = 0
passed = 0
id_ = ahs_dict.get("identity", {})
for f in ("id", "display_name", "version"):
checks += 1
if id_.get(f): passed += 1
iface = ahs_dict.get("interface", {})
for f in ("provides", "requires"):
checks += 1
if iface.get(f): passed += 1
# ... 继续检查 instructions、resources
score = round(passed / max(checks, 1), 2)
return score
LLM 自评的 confidence 不信任,信任代码数出来的结果。 这是一个很重要的设计决策。
还有一个细节值得一提:Orchestrator 在派发 Harness 之前,会先做一个 预飞分类,根据 skill 类型决定每个 Harness 用哪个模型档位。分类逻辑在 _classify():
复制代码def _classify(self, context: ContextPack) -> str:
_SCRIPT_SUFFIXES = {".py", ".sh", ".js", ".ts", ".rb", ".go", ".rs"}
has_scripts = any(Path(e.path).suffix.lower() in _SCRIPT_SUFFIXES
for e in context.file_manifest.entries)
body_lower = context.body.lower()
api_indicators = ["api", "oauth", "token", "http", "rest", "webhook"]
has_api = any(ind in body_lower for ind in api_indicators)
if has_scripts and has_api: return "integration"
if has_scripts: return "script_driven"
if len(entries) > 2: return "knowledge"
return "pure_instruction"
四种 skill 类型,对应四套模型档位组合。纯指令类 skill 的 D 直接跳过(不需要检测基类),省 token;集成类 skill 全部上 large 模型。不是所有 skill 都值得烧大模型,预飞分类帮你省钱。
Phase 3:Jinja2 把 spec 渲染成完整 Python 包
Phase 3 是代码生成。Jinja2 模板把 AHSSPEC 渲染成一个完整的 Python agent 包:
复制代码hatched-agent/
├── pyproject.toml # 可 pip-install 的包
├── runtime.toml # LLM provider、model、API keys
├── README.md # 生成的使用文档
├── agenthatch.yaml # AHSSPEC manifest
└── src/{package_name}/
├── __init__.py
├── agent.py # Agent 类(继承 AHCoreAgent)
├── tools.py # 带类型注解的工具实现
└── references.py # AI 提取的结构化数据
引擎在 generate/engine.py 的 GenerateEngine。模板映射写得很直白:
复制代码TEMPLATE_MAP: dict[str, str] = {
"pyproject.toml.j2": "pyproject.toml",
"agent.py.j2": "src/{package_name}/agent.py",
"tools.py.j2": "src/{package_name}/tools.py",
"references.py.j2": "src/{package_name}/references.py",
"runtime.toml.j2": "runtime.toml",
"README.md.j2": "README.md",
}
出来的东西能 pip install,能 import,能独立跑。这次是真 agent,不是套了层皮的 prompt。
Runtime:PlanLayer 六状态机
生成的 agent 跑起来不是裸的 ReAct 循环,它带一个 PlanLayer 状态机——六状态规划引擎。状态定义在 plan.py:
复制代码class AgentState(str, Enum):
STARTING = "starting" # 初始状态,等待计划
PLANNING = "planning" # 正在生成/更新计划
EXECUTING = "executing" # 正在执行计划步骤
VERIFYING = "verifying" # 正在校验结果
REPLANNING = "replanning" # 遇到阻塞,正在修订计划
DONE = "done" # 终态——所有步骤完成
具体运作流程:agent 启动时先通过一个虚拟的 plan 工具生成结构化计划,然后按步骤执行,每步有显式状态跟踪。遇到连续工具失败或步骤阻塞,触发 replanning。状态转换由循环控制,不是由 LLM 控制——这是关键,LLM 不可靠,状态机可靠。
计划渲染成文本注入 system prompt,agent 自己能看到进度:
复制代码## Plan: 给项目添加国际化支持
Step 1: 安装 next-intl
▶ Step 2: 创建语言包
Step 3: 配置 middleware
进度: 1/3 步骤完成
五、实战演示:从 SKILL.md 到运行中的 Agent
光说不练假把式。用一个真实的 skill 跑一遍完整流程。
Step 1:编写 SKILL.md
复制代码---
name: weather-advisor
description: 查询全球任意城市天气,支持多日预报和穿衣建议
version: 0.1.0
---# Weather Advisor Agent## 能力
- 查询指定城市的实时天气
- 查询未来 3 天天气预报
- 根据天气给出穿衣建议## 工具
- httpx 调用 OpenWeatherMap API
- rich 彩色格式化输出## 工作流程
1. 接收用户输入的城市名
2. 调用 OpenWeatherMap API 获取天气数据
3. 解析 JSON 响应,提取关键信息
4. 用 rich 格式化输出
5. 根据温度给出穿衣建议
就是这么个 markdown 文件。没有一行代码,没有工具签名,没有类型。 全是散文。
Step 2:编译
复制代码agenthatch skills add ./weather-advisor/SKILL.md
agenthatch hatch weather-advisor
复制代码weather-advisor-agent/
├── pyproject.toml
├── runtime.toml
├── README.md
├── agenthatch.yaml
└── src/weather_advisor/
├── __init__.py
├── agent.py # 继承 AHCoreAgent,带 PlanLayer
├── tools.py # get_weather(city: str) -> WeatherResponse
└── references.py
注意 tools.py——Harness C 推理出来的工具签名,是带类型注解的 Python 函数,不是散文描述。get_weather(city: str) -> WeatherResponse,参数类型、返回类型都有,LLM 不用猜。
Step 3:运行
复制代码agenthatch run weather-advisor
跑起来是一个正经的交互式 agent,带工具调用、上下文压缩、PlanLayer 驱动执行。它不依赖 Claude Code,不依赖 Codex,不依赖任何 host agent。它就是一个独立的 Python 程序。
六、SKILL.md vs agenthatch,一张表讲清楚
| SKILL.md(裸用) | agenthatch(编译后) | |
|---|---|---|
| 执行方式 | LLM 在 runtime 逐次解读 | 编译成独立 Python 包 |
| 隔离性 | 所有 skill 共享同一个 context | 每个 agent 拥有独立运行时、工具、配置 |
| 校验 | 无,typo 和歧义 runtime 才暴露 | 代码生成前 schema 校验 AHSSPEC |
| Token 成本 | 全文塞 system prompt,每轮都要消耗 | ~150 字节运行时配置 |
| 工具定义 | 散文描述,LLM 需要自己揣测调用方式 | 类型注解的 Python 函数 + JSON Schema |
| MCP | 每个 agent 需手动接线 | 自动检测、自动配置 |
| 确定性 | LLM 每次解读可能是不同的 | 同一 SKILL.md → 同一 AHSSPEC 结构(低温推理) |
| 多 skill 扩展 | 3-5 个就开始出现性能衰减 | 无上限,每个 agent 独立进程 |
| 调试 | 只能读 LLM 的 chain-of-thought 祈祷 | 标准 Python 调试、日志、测试 |
七、踩坑记录(这玩意儿不完美,实话实说)
不写踩坑就是耍流氓。以下把 agenthatch 目前存在的缺陷全部摊开:
坑 1:仅限 Python。 目前只支持 Python。JS/TS 版本在开发中,但尚未发布。如果你是 Node 生态用户,还需要再等待。
坑 2:需要 LLM API key。 Phase 2 的六个 Harness 需要调用 LLM(DeepSeek、OpenAI、Anthropic 均可),但必须提供 key。想完全离线?暂时做不到。
坑 3:单文件 skill 支持。 多文件目录开发中。现在一个 SKILL.md 带上几个脚本文件没有问题,但复杂的多文件 skill 目录还在打磨阶段。
坑 4:v0.9.x,非常早期。 bug 肯定有。我们自己在运行时遇到过 Harness E 偶尔 JSON 解析失败,然后 fallback 到 raw chat 重试。代码里已包含这个 fallback,但说明还不是 100% 稳定。
坑 5:Windows 未测试。 只在 macOS 和 Linux 上运行过,Windows 没有系统性地测试。路径处理可能存在坑点。
坑 6:Harness 调用是串行的,不是真正的并行。 README 中写的是"6 harnesses working in parallel",但实际查看 Orchestrator.run(),A→B→C→D→F→E 是顺序派发的(D 依赖 C,E 依赖前面所有)。后来意识到 README 这句描述有点夸大,正在修正文档。先坦白告知你。
八、适合谁?
- Claude Code / Codex CLI / OpenClaw 用户,skill 超过 3 个就开始感到别扭的——这是核心受众,你最能共鸣
- 智能体开发工程师,想把 skill 沉淀为可交付、可复用的 agent 制品,而不是每次都让 LLM 重新解读散文
- 团队 tech lead,想为团队建立一套 skill → agent 的标准化流水线,skill 当源码管理,agent 当制品管理
- 编译器/DSL 爱好者,想看看 LLM 如何被拆分成专业化流水线来充当编译前端
不适合谁?
- 只有一两个 skill 的个人玩家——杀鸡用牛刀,Claude Code 原生就够用
- 等待"完美工具"的人——这工具现在并不完美,v0.9.x,边飞边修
- 想"零代码搭 agent"的人——agenthatch 不是 no-code 平台,它是 compiler,你需要会写 skill 也需要能读 Python
九、谁在做这个项目
我之前为 pytest 提交过 8 个 PR(全部是 core 的真实 bug fix,不是文档 typo),为 agent-browser 提交过 1 个 PR。agenthatch 是第一个从零到一独立完成的项目,利用下班和周末一行行敲的。
从开源社区学到最重要的一句话:Ship beats perfect。 这个工具现在还不完美,但能跑,能帮到人。剩下的边飞边修。
如果你也在用 Claude Code/Codex CLI,且 skill 超过三个就开始觉得不对劲,不妨试一试。
最后再重复那个范式判断,因为它值得:
你写 skill,不是在写 prompt,而是在写 agent 的源代码。agenthatch 就是它的编译器。



