| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- #!/usr/bin/env python3
- """
- Agent 脚手架脚本 - 按最佳实践创建新的 Agent 项目。
- 用法:
- python init_agent.py <agent-name> [--level 0-4] [--path <output-dir>]
- 示例:
- python init_agent.py my-agent # Level 1(4 个工具)
- python init_agent.py my-agent --level 0 # 最小版(仅 bash)
- python init_agent.py my-agent --level 2 # 带 TodoWrite
- python init_agent.py my-agent --path ./bots # 自定义输出目录
- """
- import argparse
- import sys
- from pathlib import Path
- # 各等级的 Agent 模板
- TEMPLATES = {
- 0: '''#!/usr/bin/env python3
- """
- Level 0 Agent - Bash 就够了(约 50 行)
- 核心洞察:一个工具(bash)就能做很多事。
- 通过自递归启动子 Agent:python {name}.py "subtask"
- """
- from anthropic import Anthropic
- from dotenv import load_dotenv
- import subprocess
- import os
- load_dotenv()
- client = Anthropic(
- api_key=os.getenv("ANTHROPIC_API_KEY"),
- base_url=os.getenv("ANTHROPIC_BASE_URL")
- )
- MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
- SYSTEM = """你是一个编码 Agent。所有事情都通过 bash 完成:
- - 读取:cat, grep, find, ls
- - 写入:echo 'content' > file
- - 子 Agent:python {name}.py "subtask"
- """
- TOOL = [{{
- "name": "bash",
- "description": "执行 shell 命令。",
- "input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}
- }}]
- def run(prompt, history=[]):
- history.append({{"role": "user", "content": prompt}})
- while True:
- r = client.messages.create(model=MODEL, system=SYSTEM, messages=history, tools=TOOL, max_tokens=8000)
- history.append({{"role": "assistant", "content": r.content}})
- if r.stop_reason != "tool_use":
- return "".join(b.text for b in r.content if hasattr(b, "text"))
- results = []
- for b in r.content:
- if b.type == "tool_use":
- print(f"> {{b.input['command']}}")
- try:
- out = subprocess.run(b.input["command"], shell=True, capture_output=True, text=True, timeout=60)
- output = (out.stdout + out.stderr).strip() or "(无输出)"
- except Exception as e:
- output = f"错误:{{e}}"
- results.append({{"type": "tool_result", "tool_use_id": b.id, "content": output[:50000]}})
- history.append({{"role": "user", "content": results}})
- if __name__ == "__main__":
- h = []
- print("{name} - Level 0 Agent\\n输入 q 退出。\\n")
- while (q := input(">> ").strip()) not in ("q", "quit", ""):
- print(run(q, h), "\\n")
- ''',
- 1: '''#!/usr/bin/env python3
- """
- Level 1 Agent - 模型即 Agent(约 200 行)
- 核心洞察:4 个工具覆盖 90% 的编码任务。
- 模型本身就是 Agent,代码只负责运行循环。
- """
- from anthropic import Anthropic
- from dotenv import load_dotenv
- from pathlib import Path
- import subprocess
- import os
- load_dotenv()
- client = Anthropic(
- 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()
- SYSTEM = f"""你是位于 {{WORKDIR}} 的编码 Agent。
- 规则:
- - 优先使用工具,而不是只写解释;直接行动。
- - 不要编造文件路径。不确定时先使用 ls/find。
- - 保持最小改动,不要过度设计。
- - 完成后总结修改内容。"""
- 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"}}}}, "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"]}}}},
- ]
- def safe_path(p: str) -> Path:
- """防止路径逃逸出工作区。"""
- path = (WORKDIR / p).resolve()
- if not path.is_relative_to(WORKDIR):
- raise ValueError(f"路径逃逸出工作区:{{p}}")
- return path
- def execute(name: str, args: dict) -> str:
- """执行工具并返回结果。"""
- if name == "bash":
- dangerous = ["rm -rf /", "sudo", "shutdown", "> /dev/"]
- if any(d in args["command"] for d in dangerous):
- return "错误:已阻止危险命令"
- try:
- r = subprocess.run(args["command"], shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=60)
- return (r.stdout + r.stderr).strip()[:50000] or "(无输出)"
- except subprocess.TimeoutExpired:
- return "错误:执行超时(60 秒)"
- except Exception as e:
- return f"错误:{{e}}"
- if name == "read_file":
- try:
- return safe_path(args["path"]).read_text()[:50000]
- except Exception as e:
- return f"错误:{{e}}"
- if name == "write_file":
- try:
- p = safe_path(args["path"])
- p.parent.mkdir(parents=True, exist_ok=True)
- p.write_text(args["content"])
- return f"已写入 {{len(args['content'])}} 字节到 {{args['path']}}"
- except Exception as e:
- return f"错误:{{e}}"
- if name == "edit_file":
- try:
- p = safe_path(args["path"])
- content = p.read_text()
- if args["old_text"] not in content:
- return f"错误:在 {{args['path']}} 中未找到目标文本"
- p.write_text(content.replace(args["old_text"], args["new_text"], 1))
- return f"已编辑 {{args['path']}}"
- except Exception as e:
- return f"错误:{{e}}"
- return f"未知工具:{{name}}"
- def agent(prompt: str, history: list = None) -> str:
- """运行 Agent 循环。"""
- if history is None:
- history = []
- history.append({{"role": "user", "content": prompt}})
- while True:
- response = client.messages.create(
- model=MODEL, system=SYSTEM, messages=history, tools=TOOLS, max_tokens=8000
- )
- history.append({{"role": "assistant", "content": response.content}})
- if response.stop_reason != "tool_use":
- return "".join(b.text for b in response.content if hasattr(b, "text"))
- results = []
- for block in response.content:
- if block.type == "tool_use":
- print(f"> {{block.name}}: {{str(block.input)[:100]}}")
- output = execute(block.name, block.input)
- print(f" {{output[:100]}}...")
- results.append({{"type": "tool_result", "tool_use_id": block.id, "content": output}})
- history.append({{"role": "user", "content": results}})
- if __name__ == "__main__":
- print(f"{name} - Level 1 Agent,工作目录:{{WORKDIR}}")
- print("输入 q 退出。\\n")
- h = []
- while True:
- try:
- query = input(">> ").strip()
- except (EOFError, KeyboardInterrupt):
- break
- if query in ("q", "quit", "exit", ""):
- break
- print(agent(query, h), "\\n")
- ''',
- }
- ENV_TEMPLATE = '''# API 配置
- ANTHROPIC_API_KEY=sk-xxx
- ANTHROPIC_BASE_URL=https://api.anthropic.com
- MODEL_NAME=claude-sonnet-4-20250514
- '''
- def create_agent(name: str, level: int, output_dir: Path):
- """创建新的 Agent 项目。"""
- # 校验等级
- if level not in TEMPLATES and level not in (2, 3, 4):
- print(f"错误:脚手架暂未实现 Level {level}。")
- print("可用等级:0(最小版)、1(4 个工具)")
- print("Level 2-4 请从 mini-claude-code 仓库复制。")
- sys.exit(1)
- # 创建输出目录
- agent_dir = output_dir / name
- agent_dir.mkdir(parents=True, exist_ok=True)
- # 写入 Agent 文件
- agent_file = agent_dir / f"{name}.py"
- template = TEMPLATES.get(level, TEMPLATES[1])
- agent_file.write_text(template.format(name=name))
- print(f"已创建:{agent_file}")
- # 写入 .env.example
- env_file = agent_dir / ".env.example"
- env_file.write_text(ENV_TEMPLATE)
- print(f"已创建:{env_file}")
- # 写入 .gitignore
- gitignore = agent_dir / ".gitignore"
- gitignore.write_text(".env\n__pycache__/\n*.pyc\n")
- print(f"已创建:{gitignore}")
- print(f"\nAgent '{name}' 已创建于 {agent_dir}")
- print(f"\n下一步:")
- print(f" 1. cd {agent_dir}")
- print(f" 2. cp .env.example .env")
- print(f" 3. 编辑 .env,填入你的 API key")
- print(f" 4. pip install anthropic python-dotenv")
- print(f" 5. python {name}.py")
- def main():
- parser = argparse.ArgumentParser(
- description="搭建一个新的 AI 编码 Agent 项目",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
- 等级:
- 0 最小版(约 50 行) - 单一 bash 工具,通过自递归实现子 Agent
- 1 基础版(约 200 行)- 4 个核心工具:bash、read、write、edit
- 2 Todo(约 300 行) - 增加 TodoWrite,用于结构化规划
- 3 子 Agent(约 450) - 增加 Task 工具,用于上下文隔离
- 4 Skills(约 550) - 增加 Skill 工具,用于领域能力
- """
- )
- parser.add_argument("name", help="要创建的 Agent 名称")
- parser.add_argument("--level", type=int, default=1, choices=[0, 1, 2, 3, 4],
- help="复杂度等级(默认:1)")
- parser.add_argument("--path", type=Path, default=Path.cwd(),
- help="输出目录(默认:当前目录)")
- args = parser.parse_args()
- create_agent(args.name, args.level, args.path)
- if __name__ == "__main__":
- main()
|