Claude Code 演进之路

从零开始理解 AI Agent 的核心原理 从 50 行代码到完整的 Skills 系统

⌨️
v0
🛠️
v1
📋
v2
🔀
v3
v4

所有 Agent 的核心模式

while True:
    response = model(messages, tools)
    if response.stop_reason != "tool_use":
        return response.text
    results = execute(response.tool_calls)
    messages.append(results)
向下滚动探索

选择一个版本开始学习

每个版本都建立在前一个版本的基础上,逐步添加新的功能和概念。点击下方的版本按钮,查看详细的代码实现和学习笔记。

v0

Bash is All You Need

💡 核心哲学

"ONE tool (bash) + ONE loop = FULL agent capability"

代码行数 ~50
工具数量 1
核心新增 递归子agent
关键洞察 One tool is enough

核心特性

仅定义一个bash工具 通过bash调用自身实现子agent Process isolation = Context isolation 递归实现无限嵌套

代码实现

#!/usr/bin/env python
"""
v0_bash_agent.py - Mini Claude Code: Bash is All You Need (~50 lines core)

Core Philosophy: "Bash is All You Need"
======================================
This is the ULTIMATE simplification of a coding agent.
The answer: ONE tool (bash) + ONE loop = FULL agent capability.
"""

from anthropic import Anthropic
from dotenv import load_dotenv
import subprocess
import sys
import os

load_dotenv(override=True)

client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.getenv("MODEL_ID", "claude-sonnet-4-5-20250929")

# The ONE tool that does everything
TOOL = [{
    "name": "bash",
    "description": """Execute shell command. Common patterns:
- Read: cat/head/tail, grep/find/rg/ls, wc -l
- Write: echo 'content' > file, sed -i 's/old/new/g' file
- Subagent: python v0_bash_agent.py 'task description'""",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"]
    }
}]

SYSTEM = f"""You are a CLI agent at {os.getcwd()}. Solve problems using bash commands.
Rules:
- Prefer tools over prose. Act first, explain briefly after.
- Read files: cat, grep, find, rg, ls, head, tail
- Write files: echo '...' > file, sed -i, or cat << 'EOF' > file
- Subagent: For complex subtasks, spawn a subagent to keep context clean.
The subagent runs in isolation and returns only its final summary."""

def chat(prompt, history=None):
    """The complete agent loop in ONE function."""
    if history is None:
        history = []
    
    history.append({"role": "user", "content": prompt})
    
    while True:
        # 1. Call the model with tools
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=history,
            tools=TOOL,
            max_tokens=8000
        )
        
        # 2. Build assistant message content
        content = []
        for block in response.content:
            if hasattr(block, "text"):
                content.append({"type": "text", "text": block.text})
            elif block.type == "tool_use":
                content.append({
                    "type": "tool_use",
                    "id": block.id,
                    "name": block.name,
                    "input": block.input
                })
        history.append({"role": "assistant", "content": content})
        
        # 3. If model didn't call tools, we're done
        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if hasattr(b, "text"))
        
        # 4. Execute each tool call and collect results
        results = []
        for block in response.content:
            if block.type == "tool_use":
                cmd = block.input["command"]
                print(f"\033[33m$ {cmd}\033[0m")
                
                try:
                    out = subprocess.run(
                        cmd, shell=True, capture_output=True, 
                        text=True, timeout=300, cwd=os.getcwd()
                    )
                    output = out.stdout + out.stderr
                except subprocess.TimeoutExpired:
                    output = "(timeout after 300s)"
                
                print(output or "(empty)")
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output[:50000]
                })
        
        # 5. Append results and continue the loop
        history.append({"role": "user", "content": results})

if __name__ == "__main__":
    if len(sys.argv) > 1:
        # Subagent mode
        print(chat(sys.argv[1]))
    else:
        # Interactive REPL mode
        history = []
        while True:
            try:
                query = input("\033[36m>> \033[0m")
            except (EOFError, KeyboardInterrupt):
                break
            if query in ("q", "exit", ""):
                break
            print(chat(query, history))

💬 学习过程中的疑问

bash是"元工具"(Meta Tool)。v0只定义了一个bash工具,但bash本身就是可以调用所有其他命令的工具。

工作原理:

  • 工具定义中描述了bash可以执行shell命令
  • 通过shell=True调用系统shell
  • Shell可以访问所有已安装的命令(cat, grep, find, python等)

类比理解:

  • ❌ 传统方式:定义read_file、search_file、execute_python等多个工具
  • ✅ v0方式:只定义一个bash工具,让Claude学会用cat、grep、python等命令

这就是标题"Bash is All You Need"的含义——bash本身就是通往所有功能的大门。

v1

Model as Agent

💡 核心哲学

"The model IS the agent"

代码行数 ~200
工具数量 4
核心新增 核心循环
关键洞察 Model as Agent

核心特性

4个核心工具: bash, read_file, write_file, edit_file 完整agent_loop实现 安全机制: safe_path, 危险命令拦截 更好的错误处理和输出控制

代码实现

#!/usr/bin/env python3
"""
v1_basic_agent.py - Mini Claude Code: Model as Agent (~200 lines)

Core Philosophy: "The Model IS the Agent"
=========================================
The secret of Claude Code, Cursor Agent, Codex CLI? There is no secret.

Strip away the CLI polish, progress bars, permission systems. What remains
is surprisingly simple: a LOOP that lets the model call tools until done.

Traditional Assistant:
    User -> Model -> Text Response

Agent System:
    User -> Model -> [Tool -> Result]* -> Response
                          ^________|

The asterisk (*) matters! The model calls tools REPEATEDLY until it decides
the task is complete. This transforms a chatbot into an autonomous agent.
"""

import os
import subprocess
import sys
from pathlib import Path
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)

WORKDIR = Path.cwd()
MODEL = os.getenv("MODEL_ID", "claude-sonnet-4-5-20250929")
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))

SYSTEM = f"""You are a coding agent at {WORKDIR}.
Loop: think briefly -> use tools -> report results.
Rules:
- Prefer tools over prose. Act, don't just explain.
- Never invent file paths. Use bash ls/find first if unsure.
- Make minimal changes. Don't over-engineer.
- After finishing, summarize what changed."""

# The Four Essential Tools
TOOLS = [
    {
        "name": "bash",
        "description": "Run a shell command. Use for: ls, find, grep, git, npm, python, etc.",
        "input_schema": {
            "type": "object",
            "properties": {"command": {"type": "string"}},
            "required": ["command"],
        },
    },
    {
        "name": "read_file",
        "description": "Read file contents. Returns UTF-8 text.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "limit": {"type": "integer"}
            },
            "required": ["path"],
        },
    },
    {
        "name": "write_file",
        "description": "Write content to a file. Creates parent directories if needed.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"}
            },
            "required": ["path", "content"],
        },
    },
    {
        "name": "edit_file",
        "description": "Replace exact text in a file. Use for surgical edits.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "old_text": {"type": "string"},
                "new_text": {"type": "string"},
            },
            "required": ["path", "old_text", "new_text"],
        },
    },
]

def safe_path(p: str) -> Path:
    """Ensure path stays within workspace (security measure)."""
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_bash(command: str) -> str:
    """Execute shell command with safety checks."""
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    
    try:
        result = subprocess.run(
            command, shell=True, cwd=WORKDIR,
            capture_output=True, text=True, timeout=60
        )
        output = (result.stdout + result.stderr).strip()
        return output[:50000] if output else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Command timed out (60s)"
    except Exception as e:
        return f"Error: {e}"

def run_read(path: str, limit: int = None) -> str:
    """Read file contents with optional line limit."""
    try:
        text = safe_path(path).read_text()
        lines = text.splitlines()
        if limit and limit < len(lines):
            lines = lines[:limit]
            lines.append(f"... ({len(text.splitlines()) - limit} more lines)")
        return "\n".join(lines)[:50000]
    except Exception as e:
        return f"Error: {e}"

def run_write(path: str, content: str) -> str:
    """Write content to file, creating parent directories if needed."""
    try:
        fp = safe_path(path)
        fp.parent.mkdir(parents=True, exist_ok=True)
        fp.write_text(content)
        return f"Wrote {len(content)} bytes to {path}"
    except Exception as e:
        return f"Error: {e}"

def run_edit(path: str, old_text: str, new_text: str) -> str:
    """Replace exact text in a file (surgical edit)."""
    try:
        fp = safe_path(path)
        content = fp.read_text()
        if old_text not in content:
            return f"Error: Text not found in {path}"
        new_content = content.replace(old_text, new_text, 1)
        fp.write_text(new_content)
        return f"Edited {path}"
    except Exception as e:
        return f"Error: {e}"

def execute_tool(name: str, args: dict) -> str:
    """Dispatch tool call to the appropriate implementation."""
    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"])
    return f"Unknown tool: {name}"

def agent_loop(messages: list) -> list:
    """
    The complete agent in one function.
    
    This is the pattern that ALL coding agents share:
        while True:
            response = model(messages, tools)
            if no tool calls: return
            execute tools, append results, continue
    """
    while True:
        # Step 1: Call the model
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=TOOLS,
            max_tokens=8000,
        )
        
        # Step 2: Collect any tool calls and print text output
        tool_calls = []
        for block in response.content:
            if hasattr(block, "text"):
                print(block.text)
            if block.type == "tool_use":
                tool_calls.append(block)
        
        # Step 3: If no tool calls, task is complete
        if response.stop_reason != "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            return messages
        
        # Step 4: Execute each tool and collect results
        results = []
        for tc in tool_calls:
            print(f"\n> {tc.name}: {tc.input}")
            output = execute_tool(tc.name, tc.input)
            preview = output[:200] + "..." if len(output) > 200 else output
            print(f"  {preview}")
            
            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output,
            })
        
        # Step 5: Append to conversation and continue
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": results})

def main():
    print(f"Mini Claude Code v1 - {WORKDIR}")
    print("Type 'exit' to quit.\n")
    
    history = []
    while True:
        try:
            user_input = input("You: ").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"Error: {e}")
        
        print()

if __name__ == "__main__":
    main()

💬 学习过程中的疑问

这涉及到理论上的可能性工程上的实用性之间的权衡。

v0的哲学:极简主义证明

  • "证明概念"——bash理论上可以做一切
  • 但实践中容易出错

v1的哲学:生产可用性

  • 更好的错误处理
  • 安全性控制(safe_path、危险命令拦截)
  • 更清晰的意图表达
  • 更好的输出控制
  • 编辑文件的精确性
维度v0 (bash only)v1 (4 tools)
理论能力100%100%
实际可用性60%95%
安全性
错误率

v0的价值:证明"一个工具就够了"的理论可行性
v1的价值:提供"生产环境可用"的实际系统

1. 安全机制 🔒

  • 路径安全检查:safe_path()确保不越界
  • bash命令黑名单:拦截rm -rf /等危险命令
  • 超时保护:60秒限制

2. 错误处理 🛡️

  • 每个工具都有专门的错误处理
  • 友好的错误信息
  • 特定的错误检查(如edit_file检查文本是否存在)

3. 输出格式化 📊

  • 工具执行时的可视化显示
  • 智能截断(显示"... (N more lines)")
  • 结果预览

4. System Prompt改进 📝

  • "Never invent file paths"——防止幻觉
  • "Make minimal changes"——指导原则
  • "After finishing, summarize"——要求总结

5. 代码组织 🏗️

  • 清晰的模块化
  • 每个工具独立函数
  • 易于扩展和维护
v2

Structured Planning

💡 核心哲学

"Make Plans Visible"

代码行数 ~300
工具数量 5
核心新增 TodoManager
关键洞察 Constraints enable complexity

核心特性

TodoManager: 结构化任务管理 约束: 最多20项,仅一项in_progress activeForm: 实时显示当前动作 NAG_REMINDER: 提醒使用Todo

代码实现

# v2: TodoManager - The core addition

class TodoManager:
    """
    Manages a structured task list with enforced constraints.
    
    Key Design Decisions:
    1. Max 20 items: Prevents endless lists
    2. One in_progress: Forces focus
    3. Required fields: content, status, activeForm
    """
    
    def __init__(self):
        self.items = []
    
    def update(self, items: list) -> str:
        """Validate and update the todo list."""
        validated = []
        in_progress_count = 0
        
        for i, item in enumerate(items):
            content = str(item.get("content", "")).strip()
            status = str(item.get("status", "pending")).lower()
            active_form = str(item.get("activeForm", "")).strip()
            
            # Validation checks
            if not content:
                raise ValueError(f"Item {i}: content required")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError(f"Item {i}: invalid status")
            if not active_form:
                raise ValueError(f"Item {i}: activeForm required")
            
            if status == "in_progress":
                in_progress_count += 1
            
            validated.append({
                "content": content,
                "status": status,
                "activeForm": active_form
            })
        
        # Enforce constraints
        if len(validated) > 20:
            raise ValueError("Max 20 todos allowed")
        if in_progress_count > 1:
            raise ValueError("Only one task can be in_progress")
        
        self.items = validated
        return self.render()
    
    def render(self) -> str:
        """Render the todo list as human-readable text."""
        if not self.items:
            return "No todos."
        
        lines = []
        for item in self.items:
            if item["status"] == "completed":
                lines.append(f"[x] {item['content']}")
            elif item["status"] == "in_progress":
                lines.append(f"[>] {item['content']} <- {item['activeForm']}")
            else:
                lines.append(f"[ ] {item['content']}")
        
        completed = sum(1 for t in self.items if t["status"] == "completed")
        lines.append(f"\n({completed}/{len(self.items)} completed)")
        
        return "\n".join(lines)


# NEW in v2: TodoWrite Tool
{
    "name": "TodoWrite",
    "description": "Update the task list. Use to plan and track progress.",
    "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"],
    },
}


# Agent Loop with todo tracking
rounds_without_todo = 0

def agent_loop(messages: list) -> list:
    global rounds_without_todo
    
    while True:
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=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 = []
        used_todo = False
        
        for tc in tool_calls:
            output = execute_tool(tc.name, tc.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output,
            })
            if tc.name == "TodoWrite":
                used_todo = True
        
        # Update counter
        if used_todo:
            rounds_without_todo = 0
        else:
            rounds_without_todo += 1
        
        messages.append({"role": "assistant", "content": response.content})
        
        # Inject reminder if model hasn't used todos
        if rounds_without_todo > 10:
            results.insert(0, {"type": "text", "text": NAG_REMINDER})
        
        messages.append({"role": "user", "content": results})

💬 学习过程中的疑问

问题:Context Fade(上下文消退)

在v1中,计划只存在于模型的"头脑"中:

"我会先做A,再做B,最后做C"(看不见)
10个工具调用后:"等等,我在做什么?"

解决方案:TodoWrite工具

v2添加了一个新工具,让计划可见:

[ ] 重构认证模块
[>] 添加单元测试  <- 正在做
[ ] 更新文档

关键约束(guardrails):

规则原因
最多20项防止无限任务列表
仅一项in_progress强制聚焦
必填字段确保结构化输出

深层洞察:

"Structure constrains AND enables."

约束(最多20项、仅一项进行中)ENABLE(可见计划、可追踪进度)。

好的约束不是限制,而是脚手架。

v3

Divide and Conquer

💡 核心哲学

"Divide and Conquer with Context Isolation"

代码行数 ~450
工具数量 6
核心新增 Task tool
关键洞察 Clean context = better results

核心特性

Agent Type Registry: explore, code, plan Task工具: 创建子agent 上下文隔离: 独立的message history 工具过滤: 根据agent类型限制权限

代码实现

# v3: Agent Type Registry - The core of subagent mechanism

AGENT_TYPES = {
    "explore": {
        "description": "Read-only agent for exploring code, finding files, searching",
        "tools": ["bash", "read_file"],  # No write access
        "prompt": "You are an exploration agent. Search and analyze, but never modify files. Return a concise summary.",
    },
    "code": {
        "description": "Full agent for implementing features and fixing bugs",
        "tools": "*",  # All tools
        "prompt": "You are a coding agent. Implement the requested changes efficiently.",
    },
    "plan": {
        "description": "Planning agent for designing implementation strategies",
        "tools": ["bash", "read_file"],  # Read-only
        "prompt": "You are a planning agent. Analyze the codebase and output a numbered implementation plan. Do NOT make changes.",
    },
}

# NEW in v3: Task Tool
TASK_TOOL = {
    "name": "Task",
    "description": """Spawn a subagent for a focused subtask.

Subagents run in ISOLATED context - they don't see parent's history.
Use this to keep the main conversation clean.

Agent types:
- explore: Read-only exploration
- code: Full implementation
- plan: Design strategies
""",
    "input_schema": {
        "type": "object",
        "properties": {
            "description": {
                "type": "string",
                "description": "Short task name (3-5 words)"
            },
            "prompt": {
                "type": "string",
                "description": "Detailed instructions for the subagent"
            },
            "agent_type": {
                "type": "string",
                "enum": list(AGENT_TYPES.keys()),
            },
        },
        "required": ["description", "prompt", "agent_type"],
    },
}


def run_task(description: str, prompt: str, agent_type: str) -> str:
    """
    Execute a subagent task with isolated context.
    
    This is the core of the subagent mechanism:
    1. Create isolated message history (KEY: no parent context!)
    2. Use agent-specific system prompt
    3. Filter available tools based on agent type
    4. Run the same agent loop
    5. Return ONLY the final text
    """
    if agent_type not in AGENT_TYPES:
        return f"Error: Unknown agent type '{agent_type}'"
    
    config = AGENT_TYPES[agent_type]
    
    # Agent-specific system prompt
    sub_system = f"""You are a {agent_type} subagent at {WORKDIR}.
{config["prompt"]}
Complete the task and return a clear, concise summary."""
    
    # Filtered tools for this agent type
    sub_tools = get_tools_for_agent(agent_type)
    
    # ISOLATED message history - this is the key!
    sub_messages = [{"role": "user", "content": prompt}]
    
    # Progress tracking
    print(f"  [{agent_type}] {description}")
    start = time.time()
    tool_count = 0
    
    # Run the agent loop (silently)
    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
            })
            
            # Update progress line
            elapsed = time.time() - start
            sys.stdout.write(
                f"\r  [{agent_type}] {description} ... {tool_count} tools, {elapsed:.1f}s"
            )
            sys.stdout.flush()
        
        sub_messages.append({"role": "assistant", "content": response.content})
        sub_messages.append({"role": "user", "content": results})
    
    # Final progress update
    elapsed = time.time() - start
    sys.stdout.write(
        f"\r  [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)\n"
    )
    
    # Extract and return only the final text
    for block in response.content:
        if hasattr(block, "text"):
            return block.text
    
    return "(subagent returned no text)"


def get_tools_for_agent(agent_type: str) -> list:
    """Filter tools based on agent type."""
    allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
    
    if allowed == "*":
        return BASE_TOOLS  # All base tools, but NOT Task
    
    return [t for t in BASE_TOOLS if t["name"] in allowed]

💬 学习过程中的疑问

问题:Context Pollution(上下文污染)

单Agent历史记录:

[探索中...] cat file1.py -> 500行
[探索中...] cat file2.py -> 300行
... 15个文件后 ...
[现在重构...] "等等,file1里有什么来着?"

模型的上下文被探索细节填满,留给实际任务的空间很少。

解决方案:子agent + 上下文隔离

主Agent历史记录:
  [任务:探索代码库]
    -> 子agent探索20个文件(在自己的上下文中)
    -> 只返回:"认证在src/auth/,数据库在src/models/"
  [现在用干净上下文重构]

每个子agent拥有:

  1. 自己全新的message history
  2. 过滤后的工具(explore不能write)
  3. 专门的system prompt
  4. 只返回最终摘要给父agent

核心洞察:

Process isolation = Context isolation

通过生成子任务,我们获得:

  • 主agent的干净上下文
  • 并行探索可能
  • 自然任务分解
v4

Knowledge Externalization

💡 核心哲学

"Knowledge Externalization"

代码行数 ~550
工具数量 7
核心新增 Skill tool
关键洞察 Expertise without retraining

核心特性

SkillLoader: 加载和管理SKILL.md文件 三层渐进式加载: 元数据 → 完整内容 → 资源文件 Cache-Preserving: 通过tool_result注入 Tools vs Skills: 能力 vs 知识

代码实现

# v4: SkillLoader - The core addition

class SkillLoader:
    """
    Loads and manages skills from SKILL.md files.
    
    A skill is a FOLDER containing:
    - SKILL.md (required): YAML frontmatter + markdown instructions
    - scripts/ (optional): Helper scripts
    - references/ (optional): Additional documentation
    - assets/ (optional): Templates, files for output
    """
    
    def __init__(self, skills_dir: Path):
        self.skills_dir = skills_dir
        self.skills = {}
        self.load_skills()
    
    def parse_skill_md(self, path: Path) -> dict:
        """Parse a SKILL.md file into metadata and body."""
        content = path.read_text()
        
        # Match YAML frontmatter between --- markers
        match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", content, re.DOTALL)
        if not match:
            return None
        
        frontmatter, body = match.groups()
        
        # Parse YAML-like frontmatter
        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):
        """Scan skills directory and load all valid SKILL.md files."""
        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:
        """
        Generate skill descriptions for system prompt.
        This is Layer 1 - only name and description, ~100 tokens per skill.
        """
        if not self.skills:
            return "(no skills available)"
        
        return "\n".join(
            f"- {name}: {skill['description']}"
            for name, skill in self.skills.items()
        )
    
    def get_skill_content(self, name: str) -> str:
        """
        Get full skill content for injection.
        This is Layer 2 - the complete SKILL.md body.
        """
        if name not in self.skills:
            return None
        
        skill = self.skills[name]
        content = f"# Skill: {skill['name']}\n\n{skill['body']}"
        
        # List available resources (Layer 3 hints)
        resources = []
        for folder, label in [
            ("scripts", "Scripts"),
            ("references", "References"),
            ("assets", "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**Available resources:**\n"
            content += "\n".join(f"- {r}" for r in resources)
        
        return content


# NEW in v4: Skill Tool
SKILL_TOOL = {
    "name": "Skill",
    "description": """Load a skill to gain specialized knowledge for a task.

When to use:
- IMMEDIATELY when user task matches a skill description
- Before attempting domain-specific work (PDF, MCP, etc.)

The skill content will be injected into the conversation.""",
    "input_schema": {
        "type": "object",
        "properties": {
            "skill": {
                "type": "string",
                "description": "Name of the skill to load"
            }
        },
        "required": ["skill"],
    },
}


def run_skill(skill_name: str) -> str:
    """
    Load a skill and inject it into the conversation.
    
    Why tool_result instead of system prompt?
    - System prompt changes invalidate cache (20-50x cost increase)
    - Tool results append to end (prefix unchanged, cache hit)
    
    This is how production systems stay cost-efficient.
    """
    content = SKILLS.get_skill_content(skill_name)
    
    if content is None:
        available = ", ".join(SKILLS.list_skills()) or "none"
        return f"Error: Unknown skill '{skill_name}'. Available: {available}"
    
    return f"""<skill-loaded name="{skill_name}">
{content}
</skill-loaded>

Follow the instructions in the skill above to complete the user's task."""


# Progressive Disclosure:
# Layer 1: Metadata (always loaded)      ~100 tokens/skill
# Layer 2: SKILL.md body (on trigger)    ~2000 tokens
# Layer 3: Resources (as needed)         Unlimited

💬 学习过程中的疑问

核心区别:知识的"消化程度"

微调(Fine-tuning)  → 知识"长"在神经网络里
RAG                → 知识"存"在外部库,用时检索
Skills             → 知识"写"在提示词里,直接给AI看

详细对比:

维度微调RAGSkills
知识存储模型参数向量数据库Markdown文件
知识类型深层模式事实性知识流程化操作知识
访问方式自动激活语义检索精确加载
成本$10K-$1M+向量化+检索几乎为0
更新速度周/月分钟
准确性最高中等

适用场景:

  • 微调:改变模型的"底层行为"(如医疗诊断流程)
  • RAG:海量、动态更新的知识库(如企业内部文档)
  • Skills:流程化、需要精确执行的专业技能(如MCP开发、PDF处理)

关键洞察:Cache-Preserving Injection(缓存保持注入)

# ❌ 错误方式:修改system prompt
SYSTEM = f"""You are a coding agent...
{skill_content}  # ← 每次不同,缓存失效!
"""
# 结果:缓存失效,成本增加20-50倍

# ✅ 正确方式:通过tool_result追加
messages.append({
    "role": "user",
    "content": [{"type": "tool_result", "content": skill_content}]
})
# 结果:system prompt不变,缓存命中!

为什么这样有效?

Claude的提示词缓存基于前缀匹配:

  • System prompt是前缀
  • 如果前缀不变,缓存有效
  • Skill内容追加到messages末尾,不影响前缀

渐进式披露(Progressive Disclosure):

Layer 1: 元数据(始终加载)      ~100 tokens/skill
         ↓ 放入system prompt
Layer 2: SKILL.md正文(触发时)   ~2000 tokens
         ↓ 通过tool_result追加
Layer 3: 资源文件(按需使用)      无限制
         ↓ 可以包含代码、模板等

这保持了上下文精简,同时允许任意深度。

核心区别:能力 vs 知识

概念它是什么例子
Tool模型做什么bash, read_file, write_file
Skill模型知道如何PDF处理、MCP开发、代码审查

类比理解:

  • Tools = 工具箱里的工具(锤子、螺丝刀、扳手)——告诉你"有什么"
  • Skills = 使用说明书("如何正确锤钉子"、"什么时候用螺丝刀而不是锤子")——告诉你"怎么做"

实际例子:

# Tool告诉AI它能执行bash
bash_command = "pdftotext input.pdf -"

# Skill告诉AI什么时候用pdftotext,什么时候用PyMuPDF
"""
## Reading PDFs
Use pdftotext for quick extraction:
  pdftotext input.pdf -

Use PyMuPDF for complex layouts:
  python -c "import fitz; ..."
"""

Tools是能力,Skills是知识。两者结合,AI才能真正完成复杂任务。

版本对比

从代码行数到核心功能的演进一览

版本 代码行数 工具数量 核心新增 关键洞察
v0 ~50 1 递归子agent One tool is enough
v1 ~200 4 核心循环 Model as Agent
v2 ~300 5 TodoManager Constraints enable complexity
v3 ~450 6 Task tool Clean context = better results
v4 ~550 7 Skill tool Expertise without retraining

学习路径建议

1

理解核心循环

从 v0 开始,理解 "while True" 循环是所有 Agent 的基础模式。

2

掌握工具设计

学习 v1 的 4 个核心工具,理解如何给 AI 提供与真实世界交互的能力。

3

显式规划

v2 的 TodoManager 教你如何让 AI 的行为更可预测、更可控。

4

上下文隔离

v3 的子 agent 机制展示了如何通过任务分解保持上下文干净。

5

知识外化

v4 的 Skills 系统让你理解如何在不重新训练的情况下注入专业知识。

📚

动手实践

运行代码,修改参数,观察 AI 的行为变化。实践是最好的老师。