核心哲学

💫
"Bash is All You Need"

终极简化:一个工具,一个循环,完整的 Agent 能力

核心洞察

Unix 哲学:一切皆文件,一切皆可管道。Bash 是这个世界的入口:

你需要Bash 命令
读文件cat, head, grep
写文件echo '...' > file
搜索find, grep, rg
执行python, npm, make
子代理python v0_bash_agent.py "task"

关键洞察:通过 bash 调用自身就实现了子代理,不需要 Task 工具,不需要 Agent Registry,只需要递归。

子代理工作原理

主代理
  |-- bash: python v0_bash_agent.py "分析架构"
       |-- 子代理(独立进程,全新历史)
            |-- bash: find . -name "*.py"
            |-- bash: cat src/main.py
            |-- 通过 stdout 返回摘要

进程隔离 = 上下文隔离

  • 子进程有自己的 history=[]
  • 父进程捕获 stdout 作为工具结果
  • 递归调用实现无限嵌套

v0 证明了什么

1 一个工具足够 — Bash 是通往一切的入口
2 递归 = 层级 — 自我调用实现子代理
3 进程 = 隔离 — 操作系统提供上下文分离
4 提示词 = 约束 — 指令塑造行为
v0_bash_agent.py
#!/usr/bin/env python
from anthropic import Anthropic
import subprocess, sys, os

client = Anthropic(api_key="your-key", base_url="...")

# 唯一的工具:Bash
TOOL = [{
    "name": "bash",
    "description": """执行 shell 命令。模式:
- 读取: cat/grep/find/ls
- 写入: echo '...' > file
- 子代理: python v0_bash_agent.py 'task description'""",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"]
    }
}]

SYSTEM = f"CLI agent at {os.getcwd()}. Use bash. Spawn subagent for complex tasks."

def chat(prompt, history=[]):
    history.append({"role": "user", "content": prompt})
    while True:
        # 1. 调用模型
        r = client.messages.create(
            model="...", system=SYSTEM,
            messages=history, tools=TOOL, max_tokens=8000
        )
        history.append({"role": "assistant", "content": r.content})

        # 2. 如果不再调用工具,返回
        if r.stop_reason != "tool_use":
            return "".join(b.text for b in r.content if hasattr(b, "text"))

        # 3. 执行工具调用
        results = []
        for b in r.content:
            if b.type == "tool_use":
                out = subprocess.run(
                    b.input["command"], shell=True,
                    capture_output=True, text=True, timeout=300
                )
                results.append({
                    "type": "tool_result",
                    "tool_use_id": b.id,
                    "content": out.stdout + out.stderr
                })
        history.append({"role": "user", "content": results})

if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(chat(sys.argv[1]))  # 子代理模式
    else:
        h = []
        while (q := input(">> ")) not in ("q", ""):
            print(chat(q, h))

常见疑问

Q 为什么只定义了 bash 工具,模型还能使用 cat 命令?

因为 bash 是"元工具",它本身就可以调用所有其他命令。

  • cat 不是 Python 函数,而是系统命令
  • bash 工具通过 shell=True 调用系统 shell
  • Shell 可以访问所有已安装的命令

Claude 知道用 cat 是因为:

  1. 预训练知识:Claude 见过大量 bash 命令的使用
  2. 工具描述"Read: cat/head/tail" 提示了常用命令
  3. 系统提示词:明确列出了可用的命令
Q v0 的子代理和 v3 的 Task 工具有什么区别?

本质相同,都是进程隔离 = 上下文隔离,但 v3 更加结构化:

特性v0v3
代理类型explore/code/plan
工具过滤白名单控制
进度显示普通 stdout实时进度更新

核心哲学

🤖
"The Model IS the Agent"

模型是决策者,代码只提供工具并运行循环

从聊天机器人到自主代理

传统助手:

用户 -> 模型 -> 文本回复

Agent 系统:

用户 -> 模型 -> [工具 -> 结果]* -> 回复
                     ^_________|

星号 (*) 很重要!模型反复调用工具,直到它决定任务完成。这将聊天机器人转变为自主代理。

四个核心工具

bash
运行任何命令
npm install, git status
read_file
读取文件内容
查看 src/index.ts
write_file
创建/覆盖文件
创建 README.md
edit_file
精确修改
替换一个函数

为什么要明确定义这些工具?

维度v0 (只有 bash)v1 (专用工具)
错误处理需要解析 stderr结构化错误信息
安全性可能访问任意文件safe_path 保护
意图表达多种命令选择明确的工具语义
编辑精确性sed/awk 容易出错精确字符串替换
v1_basic_agent.py (核心部分)
# =============================================================================
# Agent 循环 - 这是一切的核心
# =============================================================================
def agent_loop(messages: list) -> list:
    """
    完整的 Agent 在一个函数里。

    所有编程 Agent 共享的模式:
        while True:
            response = model(messages, tools)
            if no tool calls: return
            execute tools, append results, continue
    """
    while True:
        # 1. 调用模型
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=TOOLS,
            max_tokens=8000,
        )

        # 2. 打印文本输出
        tool_calls = []
        for block in response.content:
            if hasattr(block, "text"):
                print(block.text)
            if block.type == "tool_use":
                tool_calls.append(block)

        # 3. 如果没有工具调用,任务完成
        if response.stop_reason != "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            return messages

        # 4. 执行工具,收集结果
        results = []
        for tc in tool_calls:
            print(f"\n> {tc.name}: {tc.input}")
            output = execute_tool(tc.name, tc.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": tc.id,
                "content": output,
            })

        # 5. 追加到对话,继续循环
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": results})


# 工具实现示例:安全的文件读取
def safe_path(p: str) -> Path:
    """确保路径在工作区内(安全措施)"""
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_read(path: str, limit: int = None) -> str:
    """读取文件内容,可选行数限制"""
    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_edit(path: str, old_text: str, new_text: str) -> str:
    """精确替换文件中的文本"""
    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}"

常见疑问

Q 既然 bash 可以做所有事,为什么还要定义专用工具?

这是理论可能性工程实用性的权衡:

  • 更好的错误处理:bash 返回混乱的 stderr,专用工具返回结构化错误
  • 安全性控制safe_path() 确保模型无法访问工作区外的文件
  • 更清晰的意图read_file("file.py", limit=100)head -n 100 file.py 更明确
  • 精确的编辑:sed/awk 语法复杂,容易出错;专用的 edit_file 使用精确字符串匹配
Q 模型怎么知道什么时候停止调用工具?

模型通过 stop_reason 来控制:

  • stop_reason == "tool_use":模型想调用工具,继续循环
  • stop_reason != "tool_use":模型认为任务完成,返回结果

这就是"模型即代理"的核心:模型决定什么时候停止,代码只是执行循环。

核心哲学

📋
"Make Plans Visible"

让计划可见,让模型保持聚焦

问题:上下文消退

v1 对于简单任务工作良好。但让它"重构认证、添加测试、更新文档",会发生什么?

v1:"我先做 A,再做 B,然后 C"(不可见)
    10 次工具调用后:"等等,我在干什么?"

计划只存在于模型的"脑中",容易失去方向。

解决方案:TodoWrite 工具

v2:
  [ ] 重构认证模块
  [>] 添加单元测试         <- 当前在这
  [ ] 更新文档

  (1/3 已完成)

现在你和模型都能看到计划

关键约束(护栏,不是限制)

最多 20 条
防止无限任务列表
只能一个进行中
强制聚焦于当前任务
必填字段
确保结构化输出

深刻洞察

"结构既约束又赋能"

Todo 的约束(最大条目、只能一个进行中)赋能了可见的计划和追踪的进度。

这个模式在 Agent 设计中随处可见:

  • max_tokens 约束 -> 赋能可管理的响应
  • 工具 Schema 约束 -> 赋能结构化调用
  • Todo 约束 -> 赋能复杂任务完成
v2_todo_agent.py (核心部分)
class TodoManager:
    """带约束的任务列表管理器"""

    def __init__(self):
        self.items = []

    def update(self, items: list) -> str:
        """验证并更新任务列表"""
        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()

            # 验证检查
            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 '{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
            })

        # 强制约束
        if len(validated) > 20:
            raise ValueError("Max 20 todos allowed")
        if in_progress_count > 1:
            raise ValueError("Only one task can be in_progress at a time")

        self.items = validated
        return self.render()

    def render(self) -> str:
        """渲染为人类可读的文本"""
        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)


# TodoWrite 工具定义
TODO_TOOL = {
    "name": "TodoWrite",
    "description": "Update the task list. Use to plan and track progress.",
    "input_schema": {
        "type": "object",
        "properties": {
            "items": {
                "type": "array",
                "description": "Complete list of tasks (replaces existing)",
                "items": {
                    "type": "object",
                    "properties": {
                        "content": {"type": "string", "description": "Task description"},
                        "status": {
                            "type": "string",
                            "enum": ["pending", "in_progress", "completed"]
                        },
                        "activeForm": {
                            "type": "string",
                            "description": "Present tense action, e.g. 'Reading files'"
                        },
                    },
                    "required": ["content", "status", "activeForm"],
                },
            }
        },
        "required": ["items"],
    },
}


# 系统提醒(软约束)
INITIAL_REMINDER = "Use TodoWrite for multi-step tasks."
NAG_REMINDER = "10+ turns without todo update. Please update todos."

常见疑问

Q 什么时候应该使用 Todo?

不是每个任务都需要 Todo。经验法则:如果你会写清单,就用 Todo

适合原因
多步骤工作5+ 步需要追踪
长对话20+ 次工具调用
复杂重构多个文件
教学目的可见的"思考过程"
Q 为什么限制"只能一个进行中"?

这是一个强制聚焦的设计:

  • 防止模型同时处理多个任务导致混乱
  • 确保每次只专注于一件事
  • 让进度更加清晰可见

这不是 bug,是 feature!好的约束是脚手架,不是限制。

核心哲学

👥
"Divide and Conquer with Context Isolation"

分而治之,上下文隔离

问题:上下文污染

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

探索过程把大量内容倒进历史记录,重构时上下文已经被污染。

解决方案:子代理

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

子代理有独立的上下文,父代理只收到简洁的摘要

代理类型注册表

explore
只读,用于搜索和分析
工具: bash, read_file
code
完整代理,用于实现
工具: 全部
plan
规划和分析
工具: bash, read_file

典型流程

1
Task(explore): "找到所有认证相关文件"
-> 返回: "认证在 src/auth/login.py..."
2
Task(plan): "设计 JWT 迁移方案"
-> 返回: "1. 添加 jwt 库 2. 创建 token 工具..."
3
Task(code): "实现 JWT tokens"
-> 返回: "创建了 jwt_utils.py,更新了 login.py"
4
总结向用户汇报更改
v3_subagent.py (核心部分)
# 代理类型注册表
AGENT_TYPES = {
    "explore": {
        "description": "Read-only agent for exploring code",
        "tools": ["bash", "read_file"],  # 不能写
        "prompt": "Search and analyze. Return concise summary."
    },
    "code": {
        "description": "Full agent for implementing features",
        "tools": "*",  # 所有工具
        "prompt": "Implement the requested changes efficiently."
    },
    "plan": {
        "description": "Planning agent for design",
        "tools": ["bash", "read_file"],  # 只读
        "prompt": "Output a numbered plan. Do NOT make changes."
    }
}


def run_task(description: str, prompt: str, agent_type: str) -> str:
    """
    执行子代理任务,带隔离的上下文。

    这是子代理机制的核心:
    1. 创建隔离的消息历史(关键:没有父上下文!)
    2. 使用代理特定的系统提示词
    3. 根据代理类型过滤可用工具
    4. 运行同样的查询循环
    5. 只返回最终文本(不是中间细节)
    """
    if agent_type not in AGENT_TYPES:
        return f"Error: Unknown agent type '{agent_type}'"

    config = AGENT_TYPES[agent_type]

    # 代理特定的系统提示词
    sub_system = f"""You are a {agent_type} subagent at {WORKDIR}.
{config["prompt"]}
Complete the task and return a clear, concise summary."""

    # 过滤后的工具
    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

    # 运行同样的 Agent 循环(静默执行)
    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
            })

        sub_messages.append({"role": "assistant", "content": response.content})
        sub_messages.append({"role": "user", "content": results})

    # 只返回最终文本 - 父代理看到简洁的摘要
    elapsed = time.time() - start
    print(f"  [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)")

    for block in response.content:
        if hasattr(block, "text"):
            return block.text

    return "(subagent returned no text)"

常见疑问

Q 为什么子代理不能调用 Task 工具?

为了防止无限递归。如果子代理可以调用 Task,它可能会不断生成新的子代理,导致无限循环。

主代理
  └─ 子代理 A
       └─ 子代理 B
            └─ 子代理 C
                 └─ ... (无限)

在生产系统中,可以设置最大递归深度来允许有限的嵌套。

Q explore 代理为什么没有 write_file 权限?

这是最小权限原则

  • explore 的目的是搜索和分析,不需要修改文件
  • 限制权限可以防止意外的副作用
  • 如果需要修改,应该用 code 类型的代理

这也让代码审查更容易:你可以确信 explore 代理不会修改任何东西。

核心哲学

📚
"Knowledge Externalization"

知识外化:从训练到编辑的范式转变

工具 vs 技能

概念是什么例子
Tool模型能什么bash, read_file, write_file
Skill模型知道怎么做PDF 处理、MCP 构建

工具是能力,技能是知识。

渐进式披露(三层结构)

Layer 1: 元数据
~100 tokens/skill
name + description(始终加载)
Layer 2: SKILL.md 主体
~2000 tokens
详细指南(触发时加载)
Layer 3: 资源文件
无限制
scripts/, references/, assets/(按需使用)

缓存保持机制(关键!)

❌ 错误做法

把 Skill 内容放入 system prompt

-> 缓存失效,20-50x 成本增加

✅ 正确做法

Skill 内容作为 tool_result 追加

-> 前缀不变,缓存命中

知识外化的意义

过去(知识内化)

  • 知识锁在模型参数里
  • 修改需要训练(LoRA、微调)
  • 成本:$10K-$1M+
  • 周期:数周

现在(知识外化)

  • 知识存在 SKILL.md 文件中
  • 修改就是编辑文本
  • 成本:免费
  • 周期:即时生效
v4_skills_agent.py (核心部分)
class SkillLoader:
    """加载和管理 SKILL.md 文件"""

    def __init__(self, skills_dir: Path):
        self.skills_dir = skills_dir
        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)
        if not match:
            return None

        frontmatter, body = match.groups()
        metadata = {}
        for line in frontmatter.strip().split("\n"):
            if ":" in line:
                key, value = line.split(":", 1)
                metadata[key.strip()] = value.strip().strip("\"'")

        return {
            "name": metadata["name"],
            "description": metadata["description"],
            "body": body.strip(),
            "path": path,
            "dir": path.parent,
        }

    def get_descriptions(self) -> str:
        """Layer 1: 只返回元数据(放入 system prompt)"""
        return "\n".join(
            f"- {name}: {skill['description']}"
            for name, skill in self.skills.items()
        )

    def get_skill_content(self, name: str) -> str:
        """Layer 2: 返回完整内容(触发时注入)"""
        skill = self.skills[name]
        content = f"# Skill: {skill['name']}\n\n{skill['body']}"

        # Layer 3: 列出可用资源
        resources = []
        for folder in ["scripts", "references", "assets"]:
            folder_path = skill["dir"] / folder
            if folder_path.exists():
                files = list(folder_path.glob("*"))
                if files:
                    resources.append(f"{folder}: {', '.join(f.name for f in files)}")

        if resources:
            content += f"\n\n**Available resources:**\n" + "\n".join(f"- {r}" for r in resources)

        return content


def run_skill(skill_name: str) -> str:
    """
    加载 Skill 并注入到对话中。

    关键机制:
    1. 获取 skill 内容(SKILL.md 主体 + 资源提示)
    2. 包装在  标签中返回
    3. 模型作为 tool_result 接收(user message)
    4. 模型现在"知道"如何完成任务

    为什么用 tool_result 而不是 system prompt?
    - System prompt 变化会使缓存失效(20-50x 成本增加)
    - Tool results 追加到末尾(前缀不变,缓存命中)
    """
    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}"

    # 包装在标签中,让模型知道这是 skill 内容
    return f"""
{content}


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


# SKILL.md 示例
"""
---
name: pdf
description: Process PDF files. Use when reading, creating, or merging PDFs.
---

# PDF Processing Skill

## Reading PDFs

Use pdftotext for quick extraction:
```bash
pdftotext input.pdf -
```

For complex layouts, use PyMuPDF:
```python
import fitz
doc = fitz.open("input.pdf")
for page in doc:
    text = page.get_text()
```
"""

常见疑问

Q Skills 和微调/RAG 有什么区别?

它们解决不同的问题,不是简单的替代关系:

方法本质最佳用途成本
微调知识"长"在参数里改变行为模式$10K+
RAG知识"存"在库里海量文档知识中等
Skills知识"写"在提示词里流程化技能免费

Skills 的生态位:小规模、流程化、需要精确执行、快速迭代的专业技能。

Q 为什么 Skill 内容要放在 tool_result 而不是 system prompt?

这是成本优化的关键:

  • System prompt 变化:前缀改变 -> 缓存失效 -> 全部重新计算
  • Tool result 追加:前缀不变 -> 缓存命中 -> 只计算新内容

缓存命中可以节省 20-50x 的计算成本。这就是为什么生产系统都采用"只追加"的策略。