1. 为什么要自己搭一个 Nano Claude Code
Claude Code 这类编码智能体,表面看是“聊天框里让它改代码”,底层其实就三件事:一个不断循环的 agent loop、一组被 JSON Schema 约束的工具、以及一套在上下文快满时把历史压掉的压缩机制。你如果只是天天用它写业务,很难感知到这三件事怎么咬合;一旦想改行为、加工具、调压缩阈值,就会卡在“不知道从哪下手”。
Nano Claude Code 的价值就在这:它把 Claude Code 的核心机制拆成从简到繁的最小示例,每一章都能直接跑、直接改、直接打日志。我按它的目录一路调下来,最大的收获不是“会写 agent”,而是能亲眼看到一轮完整循环里 messages 数组是怎么被追加、tool_result 是怎么回填、压缩是在第几轮被触发的。这种可观测性,比读十篇概念文章都管用。
这篇面向的是想读懂 CC 底层原理的开发者:你最好写过 Python、用过命令行,对 LLM 的 messages 结构有基本概念。全文会给出可复制的目录结构、关键模块配置、运行验证步骤,并说明怎么通过 TaoToken 统一 Key/API 通道把模型接进来,在本地跑通并做对照实验。读完你应该能独立观察一轮 agent 循环的输入输出,并定位到上下文压缩的触发点。
先说清楚 Nano Claude Code 是什么:它是一个教学性质的最小实现,不是生产级框架。它复刻的是 Claude Code 的机制骨架——agent loop、工具调用、todo 计划、子 agent、skill 加载、上下文压缩。适合谁?适合想从“会用”走到“会改”的人。不适合谁?不适合想直接拿它上生产的人,它的工具沙箱、错误处理都做了简化,安全性要你自己补。
我试过把它的每一章都跑一遍再对照日志读代码,发现最容易劝退新手的不是 agent loop,而是环境准备和模型接入这两步。所以下面先把接入通道讲清楚,再进配置和验证。
2. TaoToken 前置:统一 Key 与 API 通道怎么准备
Nano Claude Code 默认走 Anthropic 风格的 messages 接口,所以你需要一个能提供该接口的通道。TaoToken 在这里的作用是统一 Key 和 API 通道:你不用为每个实验单独配一套凭证,一个 Key 就能覆盖模型对话、编码计划等场景,切换模型时只改 Model ID 即可。
先注册并拿到 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,完成账号注册后进入控制台。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在里面可以创建和管理 API Key。创建完记得立刻复制保存,Key 一般只完整显示一次。
拿到 Key 之后,去 API Keys 页面确认它的状态和额度:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。这一步别跳过,我踩过的坑就是 Key 建好了但没注意额度,跑了几轮循环就报 401,排查半天以为是代码问题。
接口基地址用 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 Base URL 填进配置。模型对话的入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,你可以在那里先手动发一条消息,确认 Key 和通道是通的,再去跑代码。这个“先手动验证再写代码”的顺序能省掉大量排障时间。
如果你后面要做长期编码或 Agent 实验,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到参数不确定时以文档为准。
这里要强调一点:TaoToken 是合规的 API 通道,不是让你绕过任何限制的工具。你只需要把它当成一个标准的模型服务入口,配置方式和接任何官方 SDK 一样。环境变量建议这样组织,避免 Key 硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="claude-sonnet-4-20250514"把这三件套(Base URL、Key、Model ID)固定下来,后面所有章节的代码都从环境变量读,切换模型时只改TAOTOKEN_MODEL。这也是我建议的对照实验基础:同一份代码,换 Model ID 就能对比不同模型在 agent loop 里的行为差异。
3. 可复制配置:目录结构与关键模块
这一节给你可以直接抄的目录结构和配置片段。Nano Claude Code 的组织方式是“每章一个可运行脚本 + 共享的工具模块”,我按这个思路整理成下面这棵树:
nano-claude-code/ ├── .env # 本地环境变量(不要提交) ├── requirements.txt ├── config.py # 读取环境变量,集中管理 ├── client.py # 封装 messages 调用 ├── tools/ │ ├── __init__.py │ ├── bash.py # bash 工具 + 沙箱约束 │ ├── file_ops.py # read_file / edit_file │ ├── todo.py # todo 工具 + TodoManager │ └── task.py # 子 agent 委托 ├── skills/ │ └── SKILL.md # YAML frontmatter + 正文 ├── chapters/ │ ├── 01_agent_loop.py │ ├── 02_tools.py │ ├── 03_todo.py │ ├── 04_subagent.py │ ├── 05_skills.py │ └── 06_compact.py └── transcripts/ # 压缩时落盘的完整历史config.py负责把三件套读进来,这是所有章节的公共入口:
import os API_KEY = os.environ["TAOTOKEN_API_KEY"] BASE_URL = os.environ["TAOTOKEN_BASE_URL"] MODEL = os.environ.get("TAOTOKEN_MODEL", "claude-sonnet-4-20250514") # 压缩相关阈值 KEEP_RECENT = 3 # micro_compact 保留最近几条 tool_result TOKEN_THRESHOLD = 50000 # auto_compact 触发阈值 MAX_TODO_ITEMS = 20client.py把调用封装起来,注意 Base URL 的用法:
from anthropic import Anthropic from config import API_KEY, BASE_URL, MODEL client = Anthropic(api_key=API_KEY, base_url=BASE_URL) def call_model(messages, tools=None, system=None, max_tokens=2000): kwargs = {"model": MODEL, "messages": messages, "max_tokens": max_tokens} if tools: kwargs["tools"] = tools if system: kwargs["system"] = system return client.messages.create(**kwargs)工具定义的关键是input_schema,它用 JSON Schema 子集约束模型输入。以 bash 和 edit_file 为例:
BASH_TOOL = { "name": "bash", "description": "执行一条 shell 命令并返回输出", "input_schema": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, } EDIT_FILE_TOOL = { "name": "edit_file", "description": "把文件中的 old_text 替换为 new_text", "input_schema": { "type": "object", "properties": { "path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}, }, "required": ["path", "old_text", "new_text"], }, }模型如果少传字段、类型不对,或者传了 schema 以外的数据,都会在执行前被拦截。这消除了一整类错误:模型无法传递格式错误的输入,因为 API 会在执行前校验 schema。这也让模型意图变得明确——当它用特定字符串调用edit_file时,不存在“想改哪里”的解析歧义。
如果你用 Claude Code 的 settings 文件来管理本地配置,可以这样写,路径和字段名保持一致:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }注意这里的三件套是 Base URL、Key、Model ID,缺一不可。很多人只填了 Key 就以为能跑,结果报local proxy failed,其实是 Base URL 没配对。
4. 验证请求:跑通一轮完整 agent 循环
配置就绪后,先跑最原始的 agent loop,确认通道和循环都正常。chapters/01_agent_loop.py的核心逻辑是:一个循环 + 一个 bash 工具,判断模型返回里有没有tool_use,有就继续循环,没有就退出。
from client import call_model from tools.bash import run_bash, BASH_TOOL SYSTEM = "你是一个编码助手,可以用 bash 工具执行命令。" def agent_loop(messages): while True: resp = call_model(messages, tools=[BASH_TOOL], system=SYSTEM) messages.append({"role": "assistant", "content": resp.content}) tool_uses = [b for b in resp.content if b.type == "tool_use"] if not tool_uses: print("循环结束,最终回复:", resp.content[-1].text) return messages for tu in tool_uses: print(f"[tool_use] {tu.name} -> {tu.input}") result = run_bash(tu.input["command"]) print(f"[tool_result] {result[:200]}") messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": tu.id, "content": result, }], }) if __name__ == "__main__": msgs = [{"role": "user", "content": "列出当前目录下的文件"}] agent_loop(msgs)运行:
python chapters/01_agent_loop.py预期你会看到类似这样的输出,第一轮模型返回tool_use,第二轮返回tool_result后模型给出最终文本:
[tool_use] bash -> {'command': 'ls -la'} [tool_result] total 24 drwxr-xr-x 5 user staff 160 ... 循环结束,最终回复:当前目录下有 config.py、client.py、tools 等文件...这里有个关键观察点:第二次循环时,messages 里会出现类型为tool_result的 content。这个结构在后面的压缩章节会反复出现,因为 micro_compact 替换的就是它。你可以在循环里加一行print(len(messages)),看着数组一轮轮变长,就能直观理解“上下文是怎么被填满的”。
接着跑 todo 章节,验证计划驱动。TodoManager是 todo 工具背后的状态机,做三层事:校验(最多 20 条、text 不能为空、status 必须在枚举里、同一时刻最多 1 条 in_progress)、归一化(清洗后覆盖写入self.items)、可视化(把状态映射成[ ]/[>]/[x]并追加(done/total)统计)。
class TodoManager: def __init__(self): self.items = [] def update(self, items): assert len(items) <= 20, "最多 20 条" in_progress = 0 for it in items: assert it["text"], "text 不能为空" assert it["status"] in ("pending", "in_progress", "completed") if it["status"] == "in_progress": in_progress += 1 assert in_progress <= 1, "同一时刻最多 1 条 in_progress" self.items = items def render(self): mark = {"pending": "[ ]", "in_progress": "[>]", "completed": "[x]"} done = sum(1 for it in self.items if it["status"] == "completed") lines = [f"{mark[it['status']]} {it['text']}" for it in self.items] return "\n".join(lines) + f"\n({done}/{len(self.items)})"在 agent_loop 里加一个“催更机制”:如果连续若干轮没有调用 todo,就自动往下一轮结果里插入Update your todos.,强制模型回到计划驱动。调试时我把rounds_since_todo改成>=1就触发,这样能快速看到效果。
rounds_since_todo = 0 # 循环内 if used_todo: rounds_since_todo = 0 else: rounds_since_todo += 1 if rounds_since_todo >= 1: messages.append({"role": "user", "content": "Update your todos."})跑通后你会看到模型先提交一份 items 数组,然后每轮更新状态。todo 清单有三个好处:用户能在执行前看到 agent 打算做什么;开发者能通过检查计划状态调试行为;agent 自身能在后续轮次引用计划,即使早期上下文已经滚出窗口。
5. 本篇常见错排查
这一节对照真实报错来。第一个高频错误是 401,通常出现在 Key 没读到或额度不足:
anthropic.AuthenticationError: Error code: 401 - {'error': {'message': 'invalid api key'}}排查顺序:先确认echo $TAOTOKEN_API_KEY有值,再确认 Key 没被复制时带空格,最后去 API Keys 页面看额度。我遇到过一次是.env没被加载,代码读的是空字符串。
第二个是local proxy failed,这个多半是 Base URL 配错。检查TAOTOKEN_BASE_URL是不是https://taotoken.net/api,注意不要多加/v1或结尾斜杠。如果你用 settings 文件,确认字段名是ANTHROPIC_BASE_URL,写错成别的名字不会生效。
第三个是reading 'choices'类报错,这通常说明你用了 OpenAI 风格的响应解析,但通道返回的是 Anthropic 风格。Nano Claude Code 走的是 messages 接口,响应里是content数组而不是choices。检查你的解析代码:
# 错误:按 OpenAI 解析 # text = resp.choices[0].message.content # 正确:按 Anthropic messages 解析 text = resp.content[-1].text第四个是 OAuth 相关报错,如果你之前配过 Claude Code 的登录态,可能会和 API Key 冲突。解决办法是清掉本地 OAuth 缓存,只用 Key 认证。这类报错信息里一般带oauth字样,看到就检查是不是混用了两种认证方式。
第五个是压缩没触发。如果你跑长任务发现上下文一直涨,检查TOKEN_THRESHOLD是不是设太高,以及estimate_tokens有没有被正确调用。micro_compact 是每轮都跑的,auto_compact 才看阈值。可以在agent_loop里打印estimate_tokens(messages),观察它什么时候越过 50000。
第六个是子 agent 递归套娃。CHILD_TOOLS故意不包含task,如果你不小心把task也传给了子 agent,会出现无限递归。检查run_subagent里用的是CHILD_TOOLS而不是PARENT_TOOLS。
排障时记住三件套的检查顺序:Base URL、Key、Model ID。任何一个不对都会报错,而且报错信息不一定直指根因。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,参数不确定时以它为准。
6. 继续深入:压缩机制与后续实验
压缩是 Nano Claude Code 里最值得反复读的一章。问题很直接:上下文窗口有限,读一个 1000 行的文件就吃掉约 4000 token,读 30 个文件、跑 20 条命令,轻松突破 100k token。不压缩,智能体根本没法在大项目里干活。
三层压缩,激进程度递增。第一层 micro_compact 每轮静默执行,把超过 3 轮的旧 tool_result 替换成占位符:
def micro_compact(messages): tool_results = [] for i, msg in enumerate(messages): if msg["role"] == "user" and isinstance(msg.get("content"), list): for j, part in enumerate(msg["content"]): if isinstance(part, dict) and part.get("type") == "tool_result": tool_results.append((i, j, part)) if len(tool_results) <= KEEP_RECENT: return messages for _, _, part in tool_results[:-KEEP_RECENT]: if len(part.get("content", "")) > 100: part["content"] = f"[Previous: used {part.get('tool_name', 'tool')}]" return messages第二层 auto_compact 在 token 超过阈值时触发,先把完整对话落盘到transcripts/,再让 LLM 做摘要,用一条[Compressed]消息替换全部历史:
def auto_compact(messages): path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl" with open(path, "w") as f: for msg in messages: f.write(json.dumps(msg, default=str) + "\n") resp = call_model( [{"role": "user", "content": "Summarize this conversation for continuity..." + json.dumps(messages, default=str)[:80000]}], max_tokens=2000, ) return [ {"role": "user", "content": f"[Compressed]\n\n{resp.content[0].text}"}, {"role": "assistant", "content": "Understood. Continuing."}, ]第三层是 manual compact,模型主动调用compact工具触发同样的摘要机制。循环里三层整合:
def agent_loop(messages): while True: micro_compact(messages) # Layer 1 if estimate_tokens(messages) > TOKEN_THRESHOLD: messages[:] = auto_compact(messages) # Layer 2 resp = call_model(messages, tools=TOOLS) # ... 工具执行 ... if manual_compact: messages[:] = auto_compact(messages) # Layer 3关键认知是:完整历史通过 transcript 保存在磁盘上,信息没有真正丢失,只是移出了活跃上下文。你可以打开transcripts/里的 jsonl 文件,对照压缩前后的 messages,看哪些 tool_result 被替换、摘要保留了哪些信息。这就是“观察压缩触发点”的具体做法。
后续实验建议:把TOKEN_THRESHOLD调小到 5000,跑一个多步任务,观察 auto_compact 在第几轮触发;再对比不同 Model ID 在同样任务下的压缩频率。长期做编码或 Agent 实验的话,Coding Plan 能提供更稳定的额度:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。想先手动验证模型行为,去模型对话页发几条消息最快:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
最后留一个我调试时的小技巧:在agent_loop每轮开头打印len(messages)和estimate_tokens(messages),再在 micro_compact 和 auto_compact 里各加一行日志。这样一轮长任务跑下来,你能拿到一张完整的“上下文增长与压缩”时间线,比任何架构图都直观。