核心哲学
"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 证明了什么
#!/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))
常见疑问
因为 bash 是"元工具",它本身就可以调用所有其他命令。
cat不是 Python 函数,而是系统命令- bash 工具通过
shell=True调用系统 shell - Shell 可以访问所有已安装的命令
Claude 知道用 cat 是因为:
- 预训练知识:Claude 见过大量 bash 命令的使用
- 工具描述:
"Read: cat/head/tail"提示了常用命令 - 系统提示词:明确列出了可用的命令
本质相同,都是进程隔离 = 上下文隔离,但 v3 更加结构化:
| 特性 | v0 | v3 |
|---|---|---|
| 代理类型 | 无 | explore/code/plan |
| 工具过滤 | 无 | 白名单控制 |
| 进度显示 | 普通 stdout | 实时进度更新 |
核心哲学
"The Model IS the Agent"
模型是决策者,代码只提供工具并运行循环
从聊天机器人到自主代理
传统助手:
用户 -> 模型 -> 文本回复
Agent 系统:
用户 -> 模型 -> [工具 -> 结果]* -> 回复
^_________|
星号 (*) 很重要!模型反复调用工具,直到它决定任务完成。这将聊天机器人转变为自主代理。
四个核心工具
为什么要明确定义这些工具?
| 维度 | v0 (只有 bash) | v1 (专用工具) |
|---|---|---|
| 错误处理 | 需要解析 stderr | 结构化错误信息 |
| 安全性 | 可能访问任意文件 | safe_path 保护 |
| 意图表达 | 多种命令选择 | 明确的工具语义 |
| 编辑精确性 | sed/awk 容易出错 | 精确字符串替换 |
# =============================================================================
# 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}"
常见疑问
这是理论可能性和工程实用性的权衡:
- 更好的错误处理:bash 返回混乱的 stderr,专用工具返回结构化错误
- 安全性控制:
safe_path()确保模型无法访问工作区外的文件 - 更清晰的意图:
read_file("file.py", limit=100)比head -n 100 file.py更明确 - 精确的编辑:sed/awk 语法复杂,容易出错;专用的 edit_file 使用精确字符串匹配
模型通过 stop_reason 来控制:
stop_reason == "tool_use":模型想调用工具,继续循环stop_reason != "tool_use":模型认为任务完成,返回结果
这就是"模型即代理"的核心:模型决定什么时候停止,代码只是执行循环。
核心哲学
"Make Plans Visible"
让计划可见,让模型保持聚焦
问题:上下文消退
v1 对于简单任务工作良好。但让它"重构认证、添加测试、更新文档",会发生什么?
v1:"我先做 A,再做 B,然后 C"(不可见)
10 次工具调用后:"等等,我在干什么?"
计划只存在于模型的"脑中",容易失去方向。
解决方案:TodoWrite 工具
v2: [ ] 重构认证模块 [>] 添加单元测试 <- 当前在这 [ ] 更新文档 (1/3 已完成)
现在你和模型都能看到计划。
关键约束(护栏,不是限制)
深刻洞察
"结构既约束又赋能"
Todo 的约束(最大条目、只能一个进行中)赋能了可见的计划和追踪的进度。
这个模式在 Agent 设计中随处可见:
max_tokens约束 -> 赋能可管理的响应- 工具 Schema 约束 -> 赋能结构化调用
- Todo 约束 -> 赋能复杂任务完成
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. "
常见疑问
不是每个任务都需要 Todo。经验法则:如果你会写清单,就用 Todo。
| 适合 | 原因 |
|---|---|
| 多步骤工作 | 5+ 步需要追踪 |
| 长对话 | 20+ 次工具调用 |
| 复杂重构 | 多个文件 |
| 教学目的 | 可见的"思考过程" |
这是一个强制聚焦的设计:
- 防止模型同时处理多个任务导致混乱
- 确保每次只专注于一件事
- 让进度更加清晰可见
这不是 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/"
[现在用干净的上下文重构]
子代理有独立的上下文,父代理只收到简洁的摘要。
代理类型注册表
典型流程
# 代理类型注册表
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)"
常见疑问
为了防止无限递归。如果子代理可以调用 Task,它可能会不断生成新的子代理,导致无限循环。
主代理
└─ 子代理 A
└─ 子代理 B
└─ 子代理 C
└─ ... (无限)
在生产系统中,可以设置最大递归深度来允许有限的嵌套。
这是最小权限原则:
- explore 的目的是搜索和分析,不需要修改文件
- 限制权限可以防止意外的副作用
- 如果需要修改,应该用 code 类型的代理
这也让代码审查更容易:你可以确信 explore 代理不会修改任何东西。
核心哲学
"Knowledge Externalization"
知识外化:从训练到编辑的范式转变
工具 vs 技能
| 概念 | 是什么 | 例子 |
|---|---|---|
| Tool | 模型能做什么 | bash, read_file, write_file |
| Skill | 模型知道怎么做 | PDF 处理、MCP 构建 |
工具是能力,技能是知识。
渐进式披露(三层结构)
缓存保持机制(关键!)
把 Skill 内容放入 system prompt
-> 缓存失效,20-50x 成本增加
Skill 内容作为 tool_result 追加
-> 前缀不变,缓存命中
知识外化的意义
过去(知识内化)
- 知识锁在模型参数里
- 修改需要训练(LoRA、微调)
- 成本:$10K-$1M+
- 周期:数周
现在(知识外化)
- 知识存在 SKILL.md 文件中
- 修改就是编辑文本
- 成本:免费
- 周期:即时生效
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()
```
"""
常见疑问
它们解决不同的问题,不是简单的替代关系:
| 方法 | 本质 | 最佳用途 | 成本 |
|---|---|---|---|
| 微调 | 知识"长"在参数里 | 改变行为模式 | $10K+ |
| RAG | 知识"存"在库里 | 海量文档知识 | 中等 |
| Skills | 知识"写"在提示词里 | 流程化技能 | 免费 |
Skills 的生态位:小规模、流程化、需要精确执行、快速迭代的专业技能。
这是成本优化的关键:
- System prompt 变化:前缀改变 -> 缓存失效 -> 全部重新计算
- Tool result 追加:前缀不变 -> 缓存命中 -> 只计算新内容
缓存命中可以节省 20-50x 的计算成本。这就是为什么生产系统都采用"只追加"的策略。