
字节笔记本
2026年10月6日 · 约 7 分钟读完
手写迷你 Claude Code(一):项目搭建与 REPL

这是「手写迷你 Claude Code」系列的第一篇。整个系列只依赖 Go 标准库,不引入任何第三方依赖,目标是从零写出一个能对话、能读文件、能执行命令的命令行 Agent。本篇先解决最基础的问题:搭好项目,写一个能和用户持续交互的终端 REPL。对应代码:main.go、render.go。
这一篇你得到什么
读完你能:
- 用 Go 搭建一个终端交互程序
- 实现 Read-Eval-Print Loop(REPL)
- 处理斜杠命令(/exit /help /clear)
- 做彩色终端输出
这是 Agent 的「壳」。Agent 的「大脑」,也就是循环、模型调用和工具,从下一篇才开始写。但没有这个壳,Agent 无法和人交互。
第一步:初始化项目
mkdir mini-claude-code && cd mini-claude-code
go mod init mini-claude-codego.mod 只有一行 module mini-claude-code。整个系列不用任何第三方依赖,只用 Go 标准库,这是「纯手写」的精髓:每一行代码都自己写过,出了问题知道去哪里查。
第二步:最简 REPL
REPL 就是「读一行输入,处理,打印,然后循环」,Python 和 Node 的交互式命令行都是这个形态。最简版长这样:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "/exit" {
fmt.Println("再见!")
return
}
fmt.Println("你说:", input)
}
}go run main.go 跑起来,能输入、能回显、能 /exit 退出,这就是一个 REPL 的骨架。
几个细节
为什么用 bufio.Scanner 而不是 fmt.Scanln? 因为 fmt.Scanln 遇到空格就停,读不了完整句子。Scanner 按行读,一句话不管多长、中间有几个空格,都能完整拿到。
scanner.Buffer(...) 要调大。 Scanner 默认缓冲 64KB,长输入(比如粘贴一大段代码)会被截断报错,代码里把它设成了 1MB:
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)第三步:斜杠命令
Claude Code 有 /help、/exit 这类命令,我们也加一套:
if strings.HasPrefix(input, "/") {
switch input {
case "/exit", "/quit", "/q":
os.Exit(0)
case "/help":
fmt.Println("命令: /exit /help /clear /tools")
case "/clear":
history = []Message{} // 清空对话历史
case "/tools":
fmt.Println("read_file edit_file run_command")
}
continue
}要点:斜杠命令在 Agent 循环之前处理,它们是本地命令,不会发给模型。/clear 比较特殊,它要修改 history(清空对话记忆),所以在 main 里单独处理,不放进 switch。
第四步:彩色输出
为了让输出像 Claude Code,用 ANSI 转义码给终端加颜色:
const (
colorGreen = "\033[32m"
colorBlue = "\033[34m"
colorGray = "\033[90m"
colorReset = "\033[0m"
)
func printPrompt() {
fmt.Printf("%s> %s", "\033[1m\033[32m", "\033[0m") // 绿色粗体 >
}\033[32m 是「开始绿色」,\033[0m 是「重置」,不同数字对应不同颜色,render.go 里对它们做了统一封装。不同元素用不同颜色:
- 提示符
>:绿色 - AI 输出:蓝色
- 工具调用:青色
- 工具结果:灰色
- 错误:红色
还有一个容易忽略的细节:当输出被重定向到文件或管道时,转义码会变成乱码。所以渲染层先检测标准输出是不是终端,不是终端就把颜色码替换成空字符串,判断方式是用 os.Stdout.Stat() 拿到文件模式后检查 ModeCharDevice 位。

第五步:组装进 main.go
最终的 main.go 按顺序做六件事:
- 读工作目录
- 初始化模型客户端(后续篇详讲)
- 注册工具(后续篇详讲)
- 创建 Agent(后续篇详讲)
- 打印横幅
- 进入 REPL 循环
main.go 是「装配」,把各个组件按顺序组装起来,每个组件的具体实现放在本系列后面的章节。本篇先把装配顺序定下来,后面的章节往里面填组件就行。
关键设计:history 放在 main 里
注意对话历史 history []Message 定义在 main 里,不在 Agent 里。为什么?
因为多个 Agent 可能共享历史(虽然 MVP 阶段只有一个 Agent),而且 /clear 命令要清空历史:历史放在 main 里,命令就能直接改它。
Agent 则是「无状态」的:每次 agent.Run(history, input) 把历史传进去,Agent 用完就还。这和 agents-go 库的思路一致,Session 同样由调用方在外部传入和保管。
这一篇总结
你有了:
- Go 项目骨架
- REPL 循环(读输入、处理斜杠命令、回显)
- 彩色输出
history对话历史的数据结构
但 Agent 还不会「想」,输入什么就回显什么。下一篇把 REPL 接上大模型:从最简单的「调一次模型拿回答」,写到「模型说要调工具,执行,回填结果,再调」的完整 Agent 循环,那是 Agent 的「大脑」。



