v4: 技能机制
核心洞察:技能是知识包,不是工具。
知识外化:从训练到编辑
技能体现了一种深刻的范式转变:知识外化。
传统方法:内化在参数中的知识
传统的 AI 系统将所有知识存储在模型参数中。你无法访问、修改或重用它。
想要模型学习新技能?你需要:
- 收集大量训练数据
- 设置分布式训练集群
- 执行复杂的参数微调(LoRA、全量微调等)
- 部署新的模型版本
这就像你的大脑突然失去记忆,但你没有笔记可以恢复。知识被锁定在神经网络的权重矩阵中,对用户完全不透明。
新范式:外化为文档的知识
代码执行范式改变了一切。
┌──────────────────────────────────────────────────────────────────────┐
│ 知识存储层级 │
│ │
│ 模型参数 → 上下文窗口 → 文件系统 → 技能库 │
│ (内化的) (运行时) (持久的) (结构化的) │
│ │
│ ←────── 需要训练 ──────→ ←─── 自然语言编辑 ────→ │
│ 需要集群、数据、专业知识 任何人都可以修改 │
└──────────────────────────────────────────────────────────────────────┘
关键突破:
- 之前:修改模型行为 = 修改参数 = 需要训练 = GPU 集群 + 训练数据 + ML 专业知识
- 现在:修改模型行为 = 编辑 SKILL.md = 编辑文本文件 = 任何人都可以做到
这就像给基础模型附加一个可热插拔的 LoRA 适配器,但不需要任何参数训练。
为什么这很重要
- 民主化:无需 ML 专业知识即可自定义模型行为
- 透明度:知识以人类可读的 Markdown 存储,可审计和理解
- 可重用性:编写一次技能,可在任何兼容的代理上使用
- 版本控制:Git 管理知识变更,支持协作和回滚
- 在线学习:模型在更大的上下文窗口中"学习",无需离线训练
传统微调是离线学习:收集数据 → 训练 → 部署 → 使用。 技能实现了在线学习:在运行时按需加载知识,立即生效。
知识层级对比
| 层级 | 修改方式 | 生效时间 | 持久性 | 成本 |
|---|---|---|---|---|
| 模型参数 | 训练/微调 | 几小时到几天 | 永久 | $10K-$1M+ |
| 上下文窗口 | API 调用 | 立即 | 每次会话 | ~$0.01/调用 |
| 文件系统 | 编辑文件 | 下次加载 | 永久 | 免费 |
| 技能库 | 编辑 SKILL.md | 下次触发 | 永久 | 免费 |
技能达到了最佳平衡点:持久存储 + 按需加载 + 人类可编辑。
实际示例
假设你希望 Claude 学习你公司特定的编码标准:
传统方式:
1. 收集公司代码库作为训练数据
2. 准备微脚本和基础设施
3. 运行 LoRA 微调(需要 GPU)
4. 部署自定义模型
5. 成本:$1000+,时间:数周
技能方式:
# skills/company-standards/SKILL.md
---
name: company-standards
description: 公司编码标准和最佳实践
---
## 命名规范
- 函数使用小写加下划线
- 类使用 PascalCase
...
成本:$0,时间:5 分钟
这就是知识外化的力量:将曾经需要训练才能编码的知识变成任何人都可以编辑的文档。
问题所在
v3 为我们提供了用于任务分解的子代理。但还有一个更深层次的问题:模型如何知道如何处理特定领域的任务?
- 处理 PDF?它需要知道
pdftotextvsPyMuPDF - 构建 MCP 服务器?它需要协议规范和最佳实践
- 代码审查?它需要系统的检查清单
这些知识不是工具——它是专业知识。技能通过让模型按需加载领域知识来解决这个问题。
核心概念
1. 工具 vs 技能
| 概念 | 是什么 | 示例 |
|---|---|---|
| 工具 | 模型能做什么 | bash, read_file, write_file |
| 技能 | 模型知道如何做 | PDF 处理, MCP 构建 |
工具是能力。技能是知识。
2. 渐进式披露
第 1 层:元数据(始终加载) ~100 令牌/技能
└─ 名称 + 描述
第 2 层:SKILL.md 正文(触发时) ~2000 令牌
└─ 详细说明
第 3 层:资源(按需) 无限制
└─ scripts/, references/, assets/
这保持上下文精简,同时允许任意深度的知识。
3. SKILL.md 标准
skills/
├── pdf/
│ └── SKILL.md # 必需
├── mcp-builder/
│ ├── SKILL.md
│ └── references/ # 可选
└── code-review/
├── SKILL.md
└── scripts/ # 可选
SKILL.md 格式:YAML 前言 + Markdown 正文
---
name: pdf
description: 处理 PDF 文件。在读取、创建或合并 PDF 时使用。
---
# PDF 处理技能
## 读取 PDF
使用 pdftotext 快速提取:
```bash
pdftotext input.pdf -
...
## 实现(新增约 100 行)
### SkillLoader 类
```python
class SkillLoader:
def __init__(self, skills_dir: Path):
self.skills = {}
self.load_skills()
def parse_skill_md(self, path: Path) -> dict:
"""解析 YAML 前言 + Markdown 正文。"""
content = path.read_text()
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
# 返回 {name, description, body, path, dir}
def get_descriptions(self) -> str:
"""生成系统提示的元数据。"""
return "\n".join(f"- {name}: {skill['description']}"
for name, skill in self.skills.items())
def get_skill_content(self, name: str) -> str:
"""获取完整内容用于上下文注入。"""
return f"# Skill: {name}\n\n{skill['body']}"
Skill 工具
SKILL_TOOL = {
"name": "Skill",
"description": "加载技能以获得任务的专业知识。",
"input_schema": {
"properties": {"skill": {"type": "string"}},
"required": ["skill"]
}
}
消息注入(保留缓存)
关键洞察:技能内容进入 tool_result(用户消息的一部分),而不是系统提示:
def run_skill(skill_name: str) -> str:
content = SKILLS.get_skill_content(skill_name)
# 完整内容作为 tool_result 返回
# 成为对话历史(用户消息)的一部分
return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>
遵循上面技能中的说明完成用户的任务。"""
def agent_loop(messages: list) -> list:
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM, # 从不更改 - 缓存被保留!
messages=messages,
tools=ALL_TOOLS,
)
# 技能内容作为 tool_result 进入消息...
关键洞察:
- 技能内容追加到末尾作为新消息
- 之前的所有内容(系统提示 + 所有先前消息)都被缓存并重用
- 只有新追加的技能内容需要计算 — 整个前缀命中缓存
与生产系统的对比
| 机制 | Claude Code / Kode | v4 |
|---|---|---|
| 格式 | SKILL.md(YAML + MD) | 相同 |
| 加载 | Container API | SkillLoader 类 |
| 触发 | 自动 + Skill 工具 | 仅 Skill 工具 |
| 注入 | newMessages(用户消息) | tool_result(用户消息) |
| 缓存 | 追加到末尾,整个前缀被缓存 | 追加到末尾,整个前缀被缓存 |
| 版本控制 | Skill Versions API | 省略 |
| 权限 | allowed-tools 字段 | 省略 |
关键相似之处:两者都将技能内容注入对话历史(而不是系统提示),保留提示缓存。
为什么这很重要:缓存经济学
忽略缓存的代价
许多使用 LangGraph、LangChain、AutoGen 的开发者习惯性地:
- 将动态状态注入系统提示
- 编辑和压缩消息历史
- 使用滑动窗口截断对话
这些操作会使缓存失效,使成本暴涨 7-50 倍。
一个典型的 50 轮 SWE 任务:
- 缓存破坏:$14.06(每轮修改系统提示)
- 缓存优化:$1.85(仅追加方式)
- 节省:86.9%
对于每天处理 100 个任务的应用程序,这意味着 $45,000+ 的年度节省。
自回归模型和 KV 缓存
LLM 是自回归的:生成每个令牌都需要关注所有先前的令牌。为了避免冗余计算,提供商实现了 KV 缓存:
请求 1:[System, User1, Asst1, User2]
←────── 全部计算 ─────→
请求 2:[System, User1, Asst1, User2, Asst2, User3]
←────── 缓存命中 ─────→ ←─ 新内容 ─→
(0.1x 价格) (正常价格)
缓存命中需要精确的前缀匹配。修改系统提示或历史记录会使整个前缀缓存失效。
常见反模式
| 反模式 | 影响 | 成本乘数 |
|---|---|---|
| 动态系统提示 | 100% 缓存未命中 | 20-50 倍 |
| 消息压缩 | 从替换点开始失效 | 5-15 倍 |
| 滑动窗口 | 100% 缓存未命中 | 30-50 倍 |
| 消息编辑 | 从编辑点开始失效 | 10-30 倍 |
| 多代理全网状 | 上下文爆炸 | 3-4 倍(vs 单代理) |
提供商差异
| 提供商 | 自动缓存 | 折扣 | 配置 |
|---|---|---|---|
| Claude | ✗ | 90% | 需要 cache_control |
| GPT-5.2 | ✓ | 90% | 无需配置 |
| Kimi K2 | ✓ | 90% | 无需配置 |
| GLM-4.7 | ✓ | 82% | 无需配置 |
| MiniMax M2.1 | ✗ | 90% | 需要 cache_control |
| Gemini 3 | ✓(隐式) | 90% | 无需配置 |
重要:Claude 和 MiniMax 需要显式的 cache_control 配置 — 否则不会有缓存命中。
推荐:仅追加方式
# 错误:编辑历史
messages[2]["content"] = "已编辑" # 缓存失效!
# 正确:仅追加
messages.append(new_msg) # 前缀不变,缓存命中
# 错误:动态系统提示
system = f"状态:{state}" # 每次都变!
# 正确:固定系统,状态在消息中
SYSTEM = "你是一个助手。" # 从不更改
messages.append({"role": "user", "content": f"状态:{state}"})
上下文长度支持
现代模型支持大上下文窗口:
- Claude Sonnet 4.5 / Opus 4.5:200K
- GPT-5.2:256K+
- Gemini 3 Flash/Pro:1M-2M
200K 令牌 ≈ 15 万字 ≈ 一本 500 页的书。对于大多数 Agent 任务,现有上下文窗口已足够。
将上下文视为仅追加日志,而非可编辑文档。
深入探讨
关于缓存经济学的全面覆盖:
- 常见反模式:LangGraph/LangChain 中的 5 个破坏缓存的错误
- 详细计算:50 轮 SWE 任务的逐轮成本分析
- 提供商策略:各提供商的缓存机制和定价对比
- 代理编排:令牌消耗差异(多代理 ~3-4 倍 vs 单代理)
- 最佳实践:如何检测和修复破坏缓存的问题
参见:上下文缓存经济学:Agent 开发者的成本优化指南(中文)
哲学:实践中的知识外化
知识作为一等公民
回到开始时讨论的知识外化范式。传统观点:AI 代理是"工具调用者"——模型决定使用哪个工具,代码执行它。
但这漏掉了一个关键维度:模型如何知道该做什么?
技能是知识外化的完整实践:
之前(知识内化):
- 知识锁定在模型参数中
- 修改需要训练(LoRA、全量微调)
- 用户无法访问或理解
- 成本:$10K-$1M+,时间线:数周
现在(知识外化):
- 知识存储在 SKILL.md 文件中
- 修改只是编辑文本
- 人类可读、可审计
- 成本:免费,时间线:即时
技能承认领域知识本身就是一种需要明确管理的资源。
- 分离元数据和内容:描述是索引,正文是内容
- 按需加载:上下文窗口是宝贵的认知资源
- 标准化格式:编写一次,在任何兼容代理中使用
- 注入,不返回:技能改变认知,而不只是提供数据
- 在线学习:在更大的上下文窗口中即时学习,无需离线训练
知识外化的本质是将隐性知识变成显性文档:
- 开发者用自然语言"教"模型新技能
- Git 管理和共享知识
- 版本控制、审计、回滚
这是从"训练 AI"到"教育 AI"的范式转变。
系列总结
| 版本 | 主题 | 新增代码行 | 关键洞察 |
|---|---|---|---|
| v1 | 模型即代理 | ~200 | 模型占 80%,代码只是循环 |
| v2 | 结构化规划 | ~100 | Todo 使计划可见 |
| v3 | 分而治之 | ~150 | 子代理隔离上下文 |
| v4 | 领域专家 | ~100 | 技能注入专业知识 |
学习笔记
按此顺序阅读源代码
打开 v4_skills_agent.py,先阅读以下部分:
SKILLS_DIRSkillLoaderSKILLS = SkillLoader(SKILLS_DIR)SYSTEMSKILL_TOOLrun_skillexecute_toolagent_loop
v4 是 v3 加上一个新想法:
工具让模型行动
技能让模型知道如何在领域中行动
技能目录是知识库
代码指向:
SKILLS_DIR = WORKDIR / "skills"
每个技能都是一个包含必需的 SKILL.md 的文件夹:
skills/
pdf/
SKILL.md
mcp-builder/
SKILL.md
code-review/
SKILL.md
这是从隐藏知识到可编辑知识的核心转变。团队可以通过编辑 Git 中的 Markdown 文件来更改代理的行为。
SkillLoader 分离元数据和完整内容
SkillLoader 不会将每个技能都转储到提示中。它首先索引廉价的元数据:
def get_descriptions(self) -> str:
return "\n".join(
f"- {name}: {skill['description']}"
for name, skill in self.skills.items()
)
该元数据被添加到系统提示中:
**可用技能**(当任务匹配时使用 Skill 工具调用):
{SKILLS.get_descriptions()}
模型可以看到存在哪些技能,但昂贵的正文在需要之前不会进入上下文。
加载技能是一次工具调用
Skill 工具的模式是:
SKILL_TOOL = {
"name": "Skill",
"description": "加载技能以获得任务的专业知识。",
...
}
当模型调用它时,调度程序将其路由到:
if name == "Skill":
return run_skill(args["skill"])
这意味着技能适合与其他每个行动相同的循环:
模型选择 Skill
主机加载 SKILL.md
主机返回技能内容作为 tool_result
模型在新知识下继续
runskill 是关键机制
v4 的核心是:
def run_skill(skill_name: str) -> str:
content = SKILLS.get_skill_content(skill_name)
return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>
遵循上面技能中的说明完成用户的任务。"""
这与从搜索工具返回数据不同。技能改变了模型处理其余任务的方式。
可以把它想象成暂时在对话中添加一个专门的操作手册。
为什么技能内容进入 toolresult
源代码明确说明了这种设计:
为什么是 tool_result 而不是系统提示?
- 系统提示更改会使缓存失效
- 工具结果追加到末尾
这是一个微妙但重要的生产教训。保持系统提示稳定。将新信息追加到消息中。
这保留了提示缓存:
稳定前缀 -> 缓存命中
新技能内容 -> 只处理新后缀
技能加载有三层
代码实现了渐进式披露:
| 层级 | 源代码位置 | 模型看到什么 |
|---|---|---|
| 元数据 | get_descriptions() | 名称 + 描述 |
| 正文 | get_skill_content() | 完整的 SKILL.md 正文 |
| 资源 | scripts/, references/, assets/ 提示 | 要检查或运行的额外文件 |
这让代理知道存在哪些技能,而无需预先支付加载每个手册的上下文成本。
v4 如何结合早期版本
v4 仍然包含早期机制:
v1: bash/read/write/edit 工具
v2: TodoWrite
v3: Task 子代理
v4: Skill 加载
在 ALL_TOOLS 中,最终代理获得:
ALL_TOOLS = BASE_TOOLS + [TASK_TOOL, SKILL_TOOL]
所以 v4 不是不同的架构。它是相同的循环,只是有更精心设计的上下文源。
工具 vs 技能:实际测试
问这个问题:
这是帮助模型做某事,还是知道如何做某事?
如果它执行一个动作,它可能是一个工具:
bash
read_file
write_file
edit_file
Task
如果它教授一种方法、检查清单、惯例或领域工作流程,它可能是一个技能:
code-review
mcp-builder
pdf
agent-builder
常见失败模式
在构建你自己的技能时,避免:
- 描述太模糊。模型将不知道何时加载技能。
- 巨大的始终加载的提示。这违背了渐进式披露的目的。
- 把秘密放在技能中。技能是文件;将它们视为仓库内容。
- 混合工具和技能。技能可以提到脚本,但执行仍然通过工具发生。
- 每次都更改系统提示。更喜欢仅追加的消息注入以实现缓存友好性。
学习检查
阅读代码后,确保你能回答:
- v4 在哪里发现可用的技能?
- 哪个方法只读取技能元数据?
- 哪个方法加载完整的技能正文?
- 为什么
run_skill返回 XML 风格的标签? - 为什么技能内容作为
tool_result返回而不是插入到系统提示中? - 技能如何与待办事项和子代理结合?
完整源代码
#!/usr/bin/env python3
"""
v4_skills_agent.py - Mini Claude Code:技能机制(约 550 行)
核心理念:"知识外化"
========================================
v3 为我们提供了用于任务分解的子代理。但还有一个更深层次的问题:
模型如何知道如何处理特定领域的任务?
- 处理 PDF?它需要知道 pdftotext vs PyMuPDF
- 构建 MCP 服务器?它需要协议规范和最佳实践
- 代码审查?它需要系统的检查清单
这些知识不是工具 - 它是专业知识。技能通过让
模型按需加载领域知识来解决这个问题。
范式转变:知识外化
------------------------------------
传统 AI:知识锁定在模型参数中
- 教授新技能:收集数据 -> 训练 -> 部署
- 成本:$10K-$1M+,时间线:数周
- 需要 ML 专业知识、GPU 集群
技能:知识存储在可编辑的文件中
- 教授新技能:编写 SKILL.md 文件
- 成本:免费,时间线:分钟
- 任何人都可以做到
这就像附加一个可热插拔的 LoRA 适配器,无需任何训练!
工具 vs 技能:
---------------
| 概念 | 是什么 | 示例 |
|-----------|-------------------------|---------------------------- |
| **工具** | 模型能做什么 | bash, read_file, write |
| **技能** | 模型知道如何做 | PDF 处理, MCP 开发 |
工具是能力。技能是知识。
渐进式披露:
------------------
第 1 层:元数据(始终加载) ~100 令牌/技能
仅名称 + 描述
第 2 层:SKILL.md 正文(触发时) ~2000 令牌
详细说明
第 3 层:资源(按需) 无限制
scripts/, references/, assets/
这保持上下文精简,同时允许任意深度。
SKILL.md 标准:
-----------------
skills/
|-- pdf/
| |-- SKILL.md # 必需:YAML 前言 + Markdown 正文
|-- mcp-builder/
| |-- SKILL.md
| |-- references/ # 可选:文档、规范
|-- code-review/
|-- SKILL.md
|-- scripts/ # 可选:辅助脚本
保留缓存的注入:
--------------------------
关键洞察:技能内容进入 tool_result(用户消息),
而不是系统提示。这保留了提示缓存!
错误:每次编辑系统提示(缓存失效,20-50 倍成本增加)
正确:将技能追加为工具结果(前缀不变,缓存命中)
这就是生产 Claude Code 的工作方式 - 这也是它具有成本效益的原因。
使用方法:
python v4_skills_agent.py
"""
import os
import re
import subprocess
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
try:
from anthropic import Anthropic
except ImportError:
sys.exit("请安装:pip install anthropic python-dotenv")
# =============================================================================
# 配置
# =============================================================================
API_KEY = os.getenv("ANTHROPIC_API_KEY")
BASE_URL = os.getenv("ANTHROPIC_BASE_URL")
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
WORKDIR = Path.cwd()
SKILLS_DIR = WORKDIR / "skills"
client = Anthropic(api_key=API_KEY, base_url=BASE_URL) if BASE_URL else Anthropic(api_key=API_KEY)
# =============================================================================
# SkillLoader - v4 中的核心新增
# =============================================================================
class SkillLoader:
"""
从 SKILL.md 文件加载和管理技能。
技能是一个包含以下内容的文件夹:
- SKILL.md(必需):YAML 前言 + markdown 说明
- scripts/(可选):模型可以运行的辅助脚本
- references/(可选):额外文档
- assets/(可选):模板、输出文件
SKILL.md 格式:
----------------
---
name: pdf
description: 处理 PDF 文件。在读取、创建或合并 PDF 时使用。
---
# PDF 处理技能
## 读取 PDF
使用 pdftotext 快速提取:
```bash
pdftotext input.pdf -
```
...
YAML 前言提供元数据(名称、描述)。
Markdown 正文提供详细说明。
"""
def __init__(self, skills_dir: Path):
self.skills_dir = skills_dir
self.skills = {}
self.load_skills()
def parse_skill_md(self, path: Path) -> dict:
"""
解析 SKILL.md 文件为元数据和正文。
返回包含以下内容的 dict:name, description, body, path, dir
如果文件格式不匹配则返回 None。
"""
content = path.read_text()
# 匹配 --- 标记之间的 YAML 前言
match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
if not match:
return None
frontmatter, body = match.groups()
# 解析类似 YAML 的前言(简单键:值)
metadata = {}
for line in frontmatter.strip().split("\n"):
if ":" in line:
key, value = line.split(":", 1)
metadata[key.strip()] = value.strip().strip("\"'")
# 要求名称和描述
if "name" not in metadata or "description" not in metadata:
return None
return {
"name": metadata["name"],
"description": metadata["description"],
"body": body.strip(),
"path": path,
"dir": path.parent,
}
def load_skills(self):
"""
扫描技能目录并加载所有有效的 SKILL.md 文件。
启动时仅加载元数据 - 正文按需加载。
这保持初始上下文精简。
"""
if not self.skills_dir.exists():
return
for skill_dir in self.skills_dir.iterdir():
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
skill = self.parse_skill_md(skill_md)
if skill:
self.skills[skill["name"]] = skill
def get_descriptions(self) -> str:
"""
生成系统提示的技能描述。
这是第 1 层 - 仅名称和描述,每个技能 ~100 令牌。
完整内容(第 2 层)仅在调用 Skill 工具时加载。
"""
if not self.skills:
return "(无可用技能)"
return "\n".join(
f"- {name}: {skill['description']}"
for name, skill in self.skills.items()
)
def get_skill_content(self, name: str) -> str:
"""
获取完整技能内容用于注入。
这是第 2 层 - 完整的 SKILL.md 正文,加上任何可用的
资源(第 3 层提示)。
如果未找到技能则返回 None。
"""
if name not in self.skills:
return None
skill = self.skills[name]
content = f"# Skill: {skill['name']}\n\n{skill['body']}"
# 列出可用资源(第 3 层提示)
resources = []
for folder, label in [
("scripts", "脚本"),
("references", "参考文献"),
("assets", "资产")
]:
folder_path = skill["dir"] / folder
if folder_path.exists():
files = list(folder_path.glob("*"))
if files:
resources.append(f"{label}:{', '.join(f.name for f in files)}")
if resources:
content += f"\n\n**{skill['dir']} 中的可用资源:**\n"
content += "\n".join(f"- {r}" for r in resources)
return content
def list_skills(self) -> list:
"""返回可用技能名称列表。"""
return list(self.skills.keys())
# 全局技能加载器实例
SKILLS = SkillLoader(SKILLS_DIR)
# =============================================================================
# 代理类型注册表(来自 v3)
# =============================================================================
AGENT_TYPES = {
"explore": {
"description": "用于探索代码、查找文件、搜索的只读代理",
"tools": ["bash", "read_file"],
"prompt": "你是一个探索代理。搜索和分析,但永远不要修改文件。返回简洁的摘要。",
},
"code": {
"description": "用于实现功能和修复错误的完整代理",
"tools": "*",
"prompt": "你是一个编码代理。高效地实现所请求的更改。",
},
"plan": {
"description": "用于设计实施策略的规划代理",
"tools": ["bash", "read_file"],
"prompt": "你是一个规划代理。分析代码库并输出编号的实施计划。不要做任何更改。",
},
}
def get_agent_descriptions() -> str:
"""为系统提示生成代理类型描述。"""
return "\n".join(
f"- {name}: {cfg['description']}"
for name, cfg in AGENT_TYPES.items()
)
# =============================================================================
# TodoManager(来自 v2)
# =============================================================================
class TodoManager:
"""带约束的任务列表管理器。详见 v2。"""
def __init__(self):
self.items = []
def update(self, items: list) -> str:
validated = []
in_progress = 0
for i, item in enumerate(items):
content = str(item.get("content", "")).strip()
status = str(item.get("status", "pending")).lower()
active = str(item.get("activeForm", "")).strip()
if not content or not active:
raise ValueError(f"项目 {i}:需要 content 和 activeForm")
if status not in ("pending", "in_progress", "completed"):
raise ValueError(f"项目 {i}:无效状态")
if status == "in_progress":
in_progress += 1
validated.append({
"content": content,
"status": status,
"activeForm": active
})
if in_progress > 1:
raise ValueError("只能有一个任务处于 in_progress 状态")
self.items = validated[:20]
return self.render()
def render(self) -> str:
if not self.items:
return "无待办事项。"
lines = []
for t in self.items:
mark = "[x]" if t["status"] == "completed" else \
"[>]" if t["status"] == "in_progress" else "[ ]"
lines.append(f"{mark} {t['content']}")
done = sum(1 for t in self.items if t["status"] == "completed")
return "\n".join(lines) + f"\n({done}/{len(self.items)} 已完成)"
TODO = TodoManager()
# =============================================================================
# 系统提示 - v4 更新
# =============================================================================
SYSTEM = f"""你是位于 {WORKDIR} 的编码代理。
循环:规划 -> 使用工具执行 -> 报告。
**可用技能**(当任务匹配时立即使用 Skill 工具):
{SKILLS.get_descriptions()}
**可用代理**(对于聚焦的子任务调用 Task 工具):
{get_agent_descriptions()}
规则:
- 当用户任务匹配技能描述时立即使用 Skill 工具
- 对于需要聚焦探索或实施的子任务使用 Task 工具
- 使用 TodoWrite 跟踪多步骤工作
- 优先使用工具而非文字说明。行动,不要只是解释。
- 完成后,总结所做的更改。"""
# =============================================================================
# 工具定义
# =============================================================================
BASE_TOOLS = [
{
"name": "bash",
"description": "运行 shell 命令。",
"input_schema": {
"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"],
},
},
{
"name": "read_file",
"description": "读取文件内容。",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"limit": {"type": "integer"}
},
"required": ["path"],
},
},
{
"name": "write_file",
"description": "写入文件。",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"],
},
},
{
"name": "edit_file",
"description": "替换文件中的文本。",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"old_text": {"type": "string"},
"new_text": {"type": "string"},
},
"required": ["path", "old_text", "new_text"],
},
},
{
"name": "TodoWrite",
"description": "更新任务列表。",
"input_schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"]
},
"activeForm": {"type": "string"},
},
"required": ["content", "status", "activeForm"],
},
}
},
"required": ["items"],
},
},
]
# Task 工具(来自 v3)
TASK_TOOL = {
"name": "Task",
"description": f"""生成子代理处理聚焦的子任务。
代理类型:
{get_agent_descriptions()}""",
"input_schema": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "简短任务描述(3-5 个词)"
},
"prompt": {
"type": "string",
"description": "给子代理的详细指令"
},
"agent_type": {
"type": "string",
"enum": list(AGENT_TYPES.keys())
},
},
"required": ["description", "prompt", "agent_type"],
},
}
# v4 新增:Skill 工具
SKILL_TOOL = {
"name": "Skill",
"description": f"""加载技能以获得任务的专业知识。
可用技能:
{SKILLS.get_descriptions()}
使用时机:
- 当用户任务匹配技能描述时立即使用
- 在尝试特定领域工作之前(PDF、MCP 等)
技能内容将被注入到对话中,为你提供
详细说明和资源访问权限。""",
"input_schema": {
"type": "object",
"properties": {
"skill": {
"type": "string",
"description": "要加载的技能名称"
}
},
"required": ["skill"],
},
}
ALL_TOOLS = BASE_TOOLS + [TASK_TOOL, SKILL_TOOL]
def get_tools_for_agent(agent_type: str) -> list:
"""根据代理类型过滤工具。"""
allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
if allowed == "*":
return BASE_TOOLS
return [t for t in BASE_TOOLS if t["name"] in allowed]
# =============================================================================
# 工具实现
# =============================================================================
def safe_path(p: str) -> Path:
"""确保路径保持在工作区内。"""
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"路径超出工作区:{p}")
return path
def run_bash(cmd: str) -> str:
"""执行 shell 命令。"""
if any(d in cmd for d in ["rm -rf /", "sudo", "shutdown"]):
return "错误:危险命令"
try:
r = subprocess.run(
cmd, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=60
)
return ((r.stdout + r.stderr).strip() or "(无输出)")[:50000]
except Exception as e:
return f"错误:{e}"
def run_read(path: str, limit: int = None) -> str:
"""读取文件内容。"""
try:
lines = safe_path(path).read_text().splitlines()
if limit:
lines = lines[:limit]
return "\n".join(lines)[:50000]
except Exception as e:
return f"错误:{e}"
def run_write(path: str, content: str) -> str:
"""写入文件内容。"""
try:
fp = safe_path(path)
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content)
return f"已向 {path} 写入 {len(content)} 字节"
except Exception as e:
return f"错误:{e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
"""替换文件中的精确文本。"""
try:
fp = safe_path(path)
text = fp.read_text()
if old_text not in text:
return f"错误:在 {path} 中未找到文本"
fp.write_text(text.replace(old_text, new_text, 1))
return f"已编辑 {path}"
except Exception as e:
return f"错误:{e}"
def run_todo(items: list) -> str:
"""更新待办事项列表。"""
try:
return TODO.update(items)
except Exception as e:
return f"错误:{e}"
def run_skill(skill_name: str) -> str:
"""
加载技能并将其注入对话。
这是关键机制:
1. 获取技能内容(SKILL.md 正文 + 资源提示)
2. 将其包装在 <skill-loaded> 标签中返回
3. 模型将此作为 tool_result(用户消息)接收
4. 模型现在"知道"如何执行任务
为什么是 tool_result 而不是系统提示?
- 系统提示更改会使缓存失效(成本增加 20-50 倍)
- 工具结果追加到末尾(前缀不变,缓存命中)
这就是生产系统保持成本效益的方式。
"""
content = SKILLS.get_skill_content(skill_name)
if content is None:
available = ", ".join(SKILLS.list_skills()) or "无"
return f"错误:未知技能 '{skill_name}'。可用:{available}"
# 包装在标签中,以便模型知道这是技能内容
return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>
遵循上面技能中的说明完成用户的任务。"""
def run_task(description: str, prompt: str, agent_type: str) -> str:
"""执行子代理任务(来自 v3)。详见 v3。"""
if agent_type not in AGENT_TYPES:
return f"错误:未知代理类型 '{agent_type}'"
config = AGENT_TYPES[agent_type]
sub_system = f"""你是位于 {WORKDIR} 的 {agent_type} 子代理。
{config["prompt"]}
完成任务并返回清晰、简洁的摘要。"""
sub_tools = get_tools_for_agent(agent_type)
sub_messages = [{"role": "user", "content": prompt}]
print(f" [{agent_type}] {description}")
start = time.time()
tool_count = 0
while True:
response = client.messages.create(
model=MODEL,
system=sub_system,
messages=sub_messages,
tools=sub_tools,
max_tokens=8000,
)
if response.stop_reason != "tool_use":
break
tool_calls = [b for b in response.content if b.type == "tool_use"]
results = []
for tc in tool_calls:
tool_count += 1
output = execute_tool(tc.name, tc.input)
results.append({
"type": "tool_result",
"tool_use_id": tc.id,
"content": output
})
elapsed = time.time() - start
sys.stdout.write(
f"\r [{agent_type}] {description} ... {tool_count} 个工具, {elapsed:.1f}秒"
)
sys.stdout.flush()
sub_messages.append({"role": "assistant", "content": response.content})
sub_messages.append({"role": "user", "content": results})
elapsed = time.time() - start
sys.stdout.write(
f"\r [{agent_type}] {description} - 完成({tool_count} 个工具,{elapsed:.1f}秒)\n"
)
for block in response.content:
if hasattr(block, "text"):
return block.text
return "(子代理未返回文本)"
def execute_tool(name: str, args: dict) -> str:
"""将工具调用分派到实现。"""
if name == "bash":
return run_bash(args["command"])
if name == "read_file":
return run_read(args["path"], args.get("limit"))
if name == "write_file":
return run_write(args["path"], args["content"])
if name == "edit_file":
return run_edit(args["path"], args["old_text"], args["new_text"])
if name == "TodoWrite":
return run_todo(args["items"])
if name == "Task":
return run_task(args["description"], args["prompt"], args["agent_type"])
if name == "Skill":
return run_skill(args["skill"])
return f"未知工具:{name}"
# =============================================================================
# 主代理循环
# =============================================================================
def agent_loop(messages: list) -> list:
"""
支持技能的主代理循环。
与 v3 模式相同,但现在有 Skill 工具。
当模型加载技能时,它会接收领域知识。
"""
while True:
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=ALL_TOOLS,
max_tokens=8000,
)
tool_calls = []
for block in response.content:
if hasattr(block, "text"):
print(block.text)
if block.type == "tool_use":
tool_calls.append(block)
if response.stop_reason != "tool_use":
messages.append({"role": "assistant", "content": response.content})
return messages
results = []
for tc in tool_calls:
# 为不同工具类型提供特殊显示
if tc.name == "Task":
print(f"\n> Task: {tc.input.get('description', '子任务')}")
elif tc.name == "Skill":
print(f"\n> 正在加载技能:{tc.input.get('skill', '?')}")
else:
print(f"\n> {tc.name}")
output = execute_tool(tc.name, tc.input)
# Skill 工具显示摘要,而非完整内容
if tc.name == "Skill":
print(f" 已加载技能({len(output)} 字符)")
elif tc.name != "Task":
preview = output[:200] + "..." if len(output) > 200 else output
print(f" {preview}")
results.append({
"type": "tool_result",
"tool_use_id": tc.id,
"content": output
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": results})
# =============================================================================
# 主 REPL
# =============================================================================
def main():
print(f"Mini Claude Code v4(带技能)- {WORKDIR}")
print(f"技能:{', '.join(SKILLS.list_skills()) or '无'}")
print(f"代理类型:{', '.join(AGENT_TYPES.keys())}")
print("输入 'exit' 退出。\n")
history = []
while True:
try:
user_input = input("你:").strip()
except (EOFError, KeyboardInterrupt):
break
if not user_input or user_input.lower() in ("exit", "quit", "q"):
break
history.append({"role": "user", "content": user_input})
try:
agent_loop(history)
except Exception as e:
print(f"错误:{e}")
print()
if __name__ == "__main__":
main()
工具让模型行动。技能让模型知道如何行动。