从零用Python构建你自己的Claude Code:250行实现智能体循环与工具调用
本文目录
- 一个AI编程助手,到底由哪几块组成?
- 动手前要准备什么环境?
- V1:20行能跑起来的最小聊天循环长什么样?
- V2:怎么让它像打字机一样逐字蹦出来?
- V3:怎么让终端输出像样地渲染Markdown?
- V4:让它真正会"动手"的工具系统怎么搭?
- 第一步:用Anthropic格式定义工具
- 第二步:写工具执行函数
- 第三步:搭智能体循环
- 这段循环里,Anthropic的消息协议到底怎么转?
- 这个手搓版和真正的Claude Code差在哪?
- 这副骨架上还能长出哪些肉?
- 常见问题解答
- 为什么用Anthropic SDK而不是OpenAI接口来构建?
- Anthropic和OpenAI的工具调用格式,最关键的区别是什么?
- 模型不会自己执行命令,那危险操作怎么防?
- 这250行真能干活吗,还是只是玩具?
- 智能体循环为什么要用while而不是一次调用?
- 把模型换成Opus或Haiku要改什么?
- 权威参考资料
一句话结论:所谓AI编程助手,去掉外壳后只剩三个核心概念——智能体循环(Agentic Loop)、工具调用(Tool Use)和消息协议。本文用大约250行Python,调用Claude官方的Anthropic SDK,从一个20行的聊天循环一步步迭代到能读写文件、执行命令、搜索代码的完整工具系统。读完你会发现,Claude Code、Cursor这些工具的"魔法"内核其实只有一个
while循环那么简单,而真正的门道全在消息协议的几个字段里。
市面上讲"250行手写一个Claude Code"的教程不少,但绝大多数有个让人哭笑不得的硬伤:它们用OpenAI的接口去构建一个叫"Claude Code"的东西。这就像教人造特斯拉,结果装了台燃油发动机。既然要复刻Claude Code的架构,最地道、也最能学到真东西的做法,当然是用Claude自己的引擎——Anthropic官方SDK。保哥这篇就把代码全部换成真正的Claude接口重写一遍,顺带把两家API协议的关键差异讲清楚,这恰恰是理解工具调用最值钱的部分。
别担心,这不是什么高深的工程。把它拆开看,一个AI编程智能体的工作方式朴素得很:你说"帮我写个hello world",它就创建文件、写入代码、运行、把结果报告给你。这一整套自主行为,背后就是下面要讲的三块积木。
一个AI编程助手,到底由哪几块组成?
先建立全局认知。普通聊天机器人和AI编程智能体的根本区别,在于后者能"动手"——它不只是回答你,还能在你的环境里读文件、跑命令、改代码。支撑这种能力的,是三个层层递进的概念。
第一块,智能体循环(Agentic Loop)。这是灵魂。它的流程是:你发消息→模型思考、决定要不要用工具→执行工具、把结果发回去→模型基于结果再思考→可能继续用工具,也可能直接回复→如此往复,直到任务完成。注意,这是一个循环,不是一问一答。模型可以读完一个文件后决定再读另一个,跑完测试发现报错后自己去改——多轮自主推理,全靠这个循环撑起来。
第二块,工具调用(Tool Use)。这里有个最容易被误解的关键点:模型永远不会自己执行工具。它只负责决定"调用哪个工具、传什么参数",真正的执行发生在你的Python代码里。模型说"我要调用write_file,路径是hello.py,内容是这段",你的代码收到这个意图后,才真的去写那个文件。这个"决策与执行分离"的设计,既是安全的根基,也是你能完全掌控它行为的原因。
第三块,消息协议。这是把前两块粘起来的胶水——一个精心组织的消息数组,记录着谁说了什么、调用了什么工具、工具返回了什么。理解了消息协议的字段结构,你就理解了整个机器的运转。后面会专门拆它,因为这正是Anthropic和OpenAI两家差异最大、也最值得学的地方。
动手前要准备什么环境?
前置条件很轻:Python 3.10以上(建议3.12+)、一个Anthropic API密钥、一个终端。
初始化项目:
mkdir magiccode && cd magiccode
python3 -m venv venv
source venv/bin/activate # Windows用 venv\Scripts\activate
pip install anthropic rich两个依赖:anthropic是Claude官方Python SDK,原生支持工具调用;rich负责终端里的Markdown渲染、语法高亮和面板美化。配置密钥:
export ANTHROPIC_API_KEY="sk-ant-你的密钥"SDK会自动读取这个环境变量,代码里一行anthropic.Anthropic()就完成初始化。如果你还没装真正的Claude Code、想先体验官方版本再来手搓,可以参考Claude Code安装配置完全指南。
V1:20行能跑起来的最小聊天循环长什么样?
从最小可用版本起步。这一版只有基础聊天,没有流式、没有工具:
#!/usr/bin/env python3
"""MagicCode v1 — 20行的终端AI助手。"""
import anthropic
client = anthropic.Anthropic()
SYSTEM = "You are MagicCode, a terminal AI coding assistant. Be concise and helpful."
messages = []
print("MagicCode v1 — 输入 exit 退出")
while True:
user_input = input("\nYou > ")
if user_input.strip().lower() in ("exit", "quit"):
break
messages.append({"role": "user", "content": user_input})
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM,
messages=messages,
)
reply = resp.content[0].text
messages.append({"role": "assistant", "content": reply})
print(f"\nMagicCode: {reply}")这里就藏着第一个和OpenAI写法的关键差异,新手最容易栽:Claude的系统提示是一个独立的顶层参数system=,不是塞进消息数组里的一条role: "system"消息。Anthropic的messages数组里只有user和assistant两种角色,系统指令单拎出来。如果你照搬OpenAI那套把system塞进messages,直接就报错。
另一个细节:响应的内容在resp.content里,它是一个内容块(content block)列表,不是一个字符串。纯文本回复时取resp.content[0].text。这个"内容是块列表"的设计先记住,到了工具调用那一节它就是主角。
V2:怎么让它像打字机一样逐字蹦出来?
一次性等完整回复,体验很憋。流式输出让文字逐字显示,观感立刻不一样。Anthropic SDK提供了专门的流式上下文管理器:
#!/usr/bin/env python3
"""MagicCode v2 — 流式输出。"""
import anthropic
client = anthropic.Anthropic()
SYSTEM = "You are MagicCode, a terminal AI coding assistant. Be concise."
messages = []
print("MagicCode v2(流式)— 输入 exit 退出")
while True:
user_input = input("\nYou > ")
if user_input.strip().lower() in ("exit", "quit"):
break
messages.append({"role": "user", "content": user_input})
print("\nMagicCode: ", end="", flush=True)
full_reply = ""
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
full_reply += text
print()
messages.append({"role": "assistant", "content": full_reply})关键改动:用client.messages.stream(...)配合with上下文,遍历stream.text_stream拿到一段段文本增量,实时打印;flush=True强制立即输出、不被缓冲卡住。这比OpenAI那种手动遍历chunk、层层取delta.content的写法清爽不少——SDK帮你把文本增量直接抽好了。
V3:怎么让终端输出像样地渲染Markdown?
AI的回复满是Markdown:代码块、列表、加粗。直接打印一堆星号和井号很难看。rich能把它渲染成带语法高亮的漂亮面板,配合流式实时刷新:
#!/usr/bin/env python3
"""MagicCode v3 — Rich渲染 + 实时流式。"""
import anthropic
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.live import Live
client = anthropic.Anthropic()
console = Console()
SYSTEM = "You are MagicCode. Format responses in Markdown."
messages = []
console.print(Panel("[bold cyan]MagicCode v3[/] — 输入 exit 退出", border_style="cyan"))
while True:
user_input = console.input("\n[bold green]You >[/] ")
if user_input.strip().lower() in ("exit", "quit"):
break
messages.append({"role": "user", "content": user_input})
full_reply = ""
with client.messages.stream(
model="claude-sonnet-4-6", max_tokens=2048,
system=SYSTEM, messages=messages,
) as stream:
with Live(console=console, refresh_per_second=8) as live:
for text in stream.text_stream:
full_reply += text
live.update(Panel(Markdown(full_reply),
title="MagicCode", border_style="blue"))
messages.append({"role": "assistant", "content": full_reply})核心是rich.Live不断重渲染面板,Markdown组件把累积的文本实时格式化。到这一步,它看起来已经很像个正经工具了——但它还只会"说",不会"做"。下一版才是真正的分水岭。
V4:让它真正会"动手"的工具系统怎么搭?
这是核心版本,把前面三版的聊天能力升级成能读写文件、执行命令、搜索代码的智能体。分三步:定义工具、写执行函数、搭智能体循环。
第一步:用Anthropic格式定义工具
这里是和OpenAI差异最大的地方,必须看清楚。OpenAI的工具定义是{"type": "function", "function": {...}}这种双层嵌套;Anthropic的格式是扁平的,直接name + description + input_schema三个字段。照搬OpenAI的嵌套结构,Claude会直接拒绝。用一个小helper统一生成:
def tool(name, desc, props, required):
return {
"name": name,
"description": desc,
"input_schema": {
"type": "object",
"properties": props,
"required": required,
},
}
TOOLS = [
tool("read_file", "Read file contents. Returns text with line numbers.",
{"path": {"type": "string", "description": "File path"}}, ["path"]),
tool("write_file", "Write content to a file. Creates directories if needed.",
{"path": {"type": "string", "description": "File path"},
"content": {"type": "string", "description": "Complete file content"}},
["path", "content"]),
tool("edit_file", "Replace old_text with new_text in a file (first match).",
{"path": {"type": "string", "description": "File path"},
"old_text": {"type": "string", "description": "Text to find"},
"new_text": {"type": "string", "description": "Replacement"}},
["path", "old_text", "new_text"]),
tool("run_command", "Execute a shell command with 30s timeout.",
{"command": {"type": "string", "description": "Shell command"}}, ["command"]),
tool("list_files", "List directory structure (max 3 levels, ignores .git etc.).",
{"path": {"type": "string", "description": "Directory path"}}, []),
tool("search_code", "Search a pattern across files in a directory.",
{"pattern": {"type": "string", "description": "Search pattern"},
"path": {"type": "string", "description": "Search directory"}}, ["pattern"]),
]这个description不是写给人看的,是写给模型看的——它越清楚,Claude越知道该在什么时候调用这个工具。这是工具调用里一门容易被忽视的手艺。Anthropic官方工具调用文档把每个字段的作用和强制调用的tool_choice选项都讲得很细,值得对着读一遍。
第二步:写工具执行函数
模型只决策,执行全在这个函数里。注意所有工具都返回字符串(协议要求),并且run_command带了一道危险命令黑名单——这就是"执行权在你手里"带来的安全可控:
import os, glob, subprocess
IGNORED = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}
def execute_tool(name, params):
try:
if name == "read_file":
with open(params["path"], encoding="utf-8", errors="replace") as f:
lines = f.read().split("\n")
return "\n".join(f"{i+1:4d} | {ln}" for i, ln in enumerate(lines))
elif name == "write_file":
path = params["path"]
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(params["content"])
return f"Written to {path} ({len(params['content'])} chars)"
elif name == "edit_file":
with open(params["path"], encoding="utf-8") as f:
content = f.read()
if params["old_text"] not in content:
return "Target text not found"
content = content.replace(params["old_text"], params["new_text"], 1)
with open(params["path"], "w", encoding="utf-8") as f:
f.write(content)
return f"Edited {params['path']}"
elif name == "run_command":
cmd = params["command"]
if any(d in cmd for d in ["rm -rf /", "mkfs", "dd if=", "> /dev/sd"]):
return "Refused: dangerous command"
r = subprocess.run(cmd, shell=True, capture_output=True,
text=True, timeout=30)
return (r.stdout + ("\n--- stderr ---\n" + r.stderr if r.stderr else "")).strip() or "(no output)"
elif name == "list_files":
out = []
def walk(d, prefix="", depth=0):
if depth >= 3:
return
for e in sorted(os.listdir(d)):
if e in IGNORED or e.startswith("."):
continue
full = os.path.join(d, e)
if os.path.isdir(full):
out.append(f"{prefix}{e}/")
walk(full, prefix + " ", depth + 1)
else:
out.append(f"{prefix}{e}")
walk(params.get("path", "."))
return "\n".join(out[:200]) or "Empty"
elif name == "search_code":
hits = []
base = params.get("path", ".")
for fp in glob.glob(os.path.join(base, "**", "*"), recursive=True):
if any(d in fp for d in IGNORED) or not os.path.isfile(fp):
continue
try:
with open(fp, encoding="utf-8", errors="replace") as f:
for i, ln in enumerate(f, 1):
if params["pattern"].lower() in ln.lower():
hits.append(f"{fp}:{i}: {ln.rstrip()}")
if len(hits) >= 50:
break
except OSError:
continue
return "\n".join(hits) or "No matches"
except Exception as e:
return f"{type(e).__name__}: {e}"第三步:搭智能体循环
重头戏来了。这个循环就是Claude Code的内核,逻辑和OpenAI版同构,但消息协议的字段名和结构完全是Anthropic的一套:
import json
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
MODEL = os.getenv("MAGIC_MODEL", "claude-sonnet-4-6")
SYSTEM_PROMPT = """You are MagicCode, a terminal AI coding assistant.
Tools: read_file, write_file, edit_file, run_command, list_files, search_code.
Principles:
1. Always read a file before modifying it.
2. Break complex tasks into steps; verify each step.
3. Never run destructive commands.
4. Respond in Markdown."""
class MagicCode:
def __init__(self):
self.console = Console()
self.messages = []
def chat(self, user_input):
self.messages.append({"role": "user", "content": user_input})
tool_count = 0
while True:
resp = client.messages.create(
model=MODEL, max_tokens=4096,
system=SYSTEM_PROMPT, tools=TOOLS,
messages=self.messages,
)
# 把assistant完整响应(含工具调用)原样存回历史
self.messages.append({"role": "assistant", "content": resp.content})
# 显示其中的文本块
for block in resp.content:
if block.type == "text":
self.console.print(Panel(Markdown(block.text), title="MagicCode"))
# 没有工具调用 → 任务完成,跳出
if resp.stop_reason != "tool_use":
break
# 逐个执行工具,结果用tool_result块回传
results = []
for block in resp.content:
if block.type == "tool_use":
tool_count += 1
self.console.print(f" [yellow]工具[{tool_count}] {block.name}[/] [dim]{block.input}[/]")
output = execute_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
self.messages.append({"role": "user", "content": results})
if tool_count > 20:
self.console.print("[red]工具调用上限(20)[/]")
break
def run(self):
self.console.print("[bold cyan]MagicCode[/] — 你的终端AI编程助手 (exit退出)")
while True:
try:
ui = self.console.input("[bold green]You >[/] ").strip()
if ui.lower() in ("exit", "quit"):
break
if not ui:
continue
self.chat(ui)
except KeyboardInterrupt:
break
if __name__ == "__main__":
MagicCode().run()这段循环里,Anthropic的消息协议到底怎么转?
代码能跑只是第一步,真正要装进脑子的是消息协议怎么流转。这是Claude和OpenAI差异的集中地,也是这篇相比那些"OpenAI冒充Claude"教程最值钱的部分。把一次"帮我写hello world"展开,messages数组长这样:
[
# 1. 用户提问
{"role": "user", "content": "帮我写个hello world"},
# 2. assistant的响应:content是块列表,可同时含文本和工具调用
{"role": "assistant", "content": [
{"type": "text", "text": "好,我来创建这个文件。"},
{"type": "tool_use", "id": "toolu_01abc",
"name": "write_file",
"input": {"path": "hello.py", "content": "print('hello world')"}}
]},
# 3. 工具结果:用role=user,content里放tool_result块
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01abc",
"content": "Written to hello.py (20 chars)"}
]},
# 4. Claude基于结果继续……
]四个魔鬼细节,记牢了你就真懂了:
- 系统提示在
system=参数里,不在messages里。这是Anthropic和OpenAI最显眼的结构差异。 - assistant的
content是块列表,一条回复里可以同时有text块和多个tool_use块。所以存回历史时要把整个resp.content原样塞回去,不能只取文本。 - 工具结果用
role: "user"回传(不是什么独立的tool角色),内容是tool_result块,靠tool_use_id和当初那个tool_use的id精确配对。少一个id或对不上,整轮就乱套。 - 循环的退出信号是
stop_reason:等于"tool_use"说明Claude还想调工具,继续循环;不等于(通常是"end_turn")说明它说完了,跳出。
把这四点和那个while循环对照看,你会有种通透感:所谓智能体,就是"调模型→看它要不要用工具→用了就执行并把结果塞回去→再调模型"这么转圈,转到它不再要工具为止。Claude Code、Cursor、各路Agent框架,内核都是这个。Anthropic官方的工具调用智能体教程有一份完整的端到端走查,想再夯实一遍可以跟着做。
这个手搓版和真正的Claude Code差在哪?
别飘,250行复刻的是架构骨架,不是全部肌肉。摆张对照表心里有数:
| 能力 | MagicCode(手搓) | Claude Code(官方) |
|---|---|---|
| 读、写、编辑文件 | 有 | 有,且基于精细Diff |
| 执行命令、搜索代码 | 有 | 有 |
| 列目录 | 有 | 有 |
| MCP集成(连外部工具) | 无 | 有 |
| 多文件Diff、笔记本编辑 | 无 | 有 |
| 权限系统、计划模式、子代理 | 无 | 有 |
| 大致覆盖度 | 约八成核心架构 | 百分百 |
差距主要在工程化的深度和外围生态。比如官方版能通过MCP协议连数据库、连GitHub、连各种外部服务,这套机制怎么接,可以看MCP配置指南。但骨架你已经亲手搭出来了,剩下的都是在这副骨架上长肉。
这副骨架上还能长出哪些肉?
给几个高性价比的扩展方向,每个都是真实工具里有的功能,照着加能让你的MagicCode迅速变强。
权限确认。只读类工具(读文件、列目录、搜索)直接放行;写文件、执行命令这类有副作用的,先弹一句问你(y/n),确认了再执行。一道关,安全感天差地别。
加载项目上下文。启动时自动读取项目根目录的CLAUDE.md、AGENTS.md、README.md,拼进系统提示,让你的助手一开口就懂这个项目的规矩。这正是官方Claude Code记忆机制的简化版,背后的设计思路在CLAUDE.md记忆术指南里讲得很透。
对话持久化。把messages数组用JSON存盘,下次启动恢复,跨会话记忆就有了。
Token用量追踪。每次调用后从resp.usage读input_tokens和output_tokens累加,退出时打印本次会话花了多少,成本心里有数。
模型切换。把MODEL做成环境变量,硬任务切Opus、日常用Sonnet、批量活换Haiku——这正是按"纠正税"选模型的实践,详见Claude Code最佳实践。
常见问题解答
为什么用Anthropic SDK而不是OpenAI接口来构建?
因为要复刻的是Claude Code,用Claude自己的引擎才地道,也才能学到真正的协议差异。Anthropic的消息协议在系统提示位置、内容块结构、工具定义格式、工具结果回传方式上都和OpenAI不同,这些差异恰恰是工具调用最核心的知识点。用OpenAI构建一个叫Claude Code的东西,逻辑上就拧着。
Anthropic和OpenAI的工具调用格式,最关键的区别是什么?
四点:系统提示在Anthropic是独立的system参数、不进messages;工具定义是扁平的name/description/input_schema、没有OpenAI那层function嵌套;工具结果用role为user的tool_result块回传、靠tool_use_id配对;循环退出看stop_reason是否等于tool_use。记住这四点,两家代码就能互相翻译。
模型不会自己执行命令,那危险操作怎么防?
模型永远只决定调用什么工具、传什么参数,真正执行在你的execute_tool函数里。所以安全完全可控:你可以在run_command里设危险命令黑名单、给写操作加权限确认、限制可访问的目录。这种决策与执行分离,正是智能体安全设计的根基。
这250行真能干活吗,还是只是玩具?
能干真活,但定位是学习骨架。它读写文件、跑命令、搜代码、多轮自主推理都没问题,覆盖了约八成核心架构。缺的是MCP集成、精细Diff、权限系统、计划模式这些工程化外围。把它当成理解所有AI编程工具底层的最佳教具,而不是生产工具。
智能体循环为什么要用while而不是一次调用?
因为一个任务往往需要多轮工具调用。比如改bug,Claude要先读文件、再搜相关代码、改完跑测试、看报错再改——每一步的下一步都取决于上一步的结果。while循环让它能基于工具返回继续推理,直到stop_reason不再是tool_use才停。这正是它从聊天机器人进化成智能体的关键。
把模型换成Opus或Haiku要改什么?
只改MODEL那一个值即可,比如claude-opus-4-8或claude-haiku-4-5,其余代码不动——这是把模型做成环境变量的好处。复杂、易错、不可逆的任务上Opus,日常开发用Sonnet平衡速度和智能,批量简单活换Haiku省成本,按纠正税的高低来选。
权威参考资料
FAQPage + Article AI 引用友好版
市面教程大多用OpenAI接口去构建一个叫Claude Code的东西,逻辑就拧着。这篇改用Claude官方Anthropic SDK从头手搓终端编程助手,拆解tool_use与tool_result块、stop_reason循环、system参数等协议细节及与OpenAI格式的关键差异,附可运行代码。
- Claude Code
- AI编程
- Python
- Anthropic SDK
- Agent开发
- AI编程与工具链
title: 从零用Python构建你自己的Claude Code:250行实现智能体循环与工具调用 author: 张文保 (Paul Zhang) — PatPat SEO 经理 url: https://zhangwenbao.com/build-magic-code.html published: 2026-02-24 modified: 2026-06-03 source-type: First-hand expert commentary language: zh-CN license: CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
本文标题:《从零用Python构建你自己的Claude Code:250行实现智能体循环与工具调用》
本文链接:https://zhangwenbao.com/build-magic-code.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0