ByteNoteByteNote
手写迷你 Claude Code(一):项目搭建与 REPL
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

手写迷你 Claude Code(一):项目搭建与 REPL

API中转
¥120

REPL 循环流程:读取输入,判断是否斜杠命令,本地处理或交给 Agent,打印结果后循环

这是「手写迷你 Claude Code」系列的第一篇。整个系列只依赖 Go 标准库,不引入任何第三方依赖,目标是从零写出一个能对话、能读文件、能执行命令的命令行 Agent。本篇先解决最基础的问题:搭好项目,写一个能和用户持续交互的终端 REPL。对应代码:main.go、render.go。

这一篇你得到什么

读完你能:

  • 用 Go 搭建一个终端交互程序
  • 实现 Read-Eval-Print Loop(REPL)
  • 处理斜杠命令(/exit /help /clear)
  • 做彩色终端输出

这是 Agent 的「壳」。Agent 的「大脑」,也就是循环、模型调用和工具,从下一篇才开始写。但没有这个壳,Agent 无法和人交互。

第一步:初始化项目

bash
mkdir mini-claude-code && cd mini-claude-code
go mod init mini-claude-code

go.mod 只有一行 module mini-claude-code。整个系列不用任何第三方依赖,只用 Go 标准库,这是「纯手写」的精髓:每一行代码都自己写过,出了问题知道去哪里查。

第二步:最简 REPL

REPL 就是「读一行输入,处理,打印,然后循环」,Python 和 Node 的交互式命令行都是这个形态。最简版长这样:

go
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:

go
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

第三步:斜杠命令

Claude Code 有 /help、/exit 这类命令,我们也加一套:

go
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 转义码给终端加颜色:

go
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 位。

终端会话效果与 ANSI 颜色对照

第五步:组装进 main.go

最终的 main.go 按顺序做六件事:

  1. 读工作目录
  2. 初始化模型客户端(后续篇详讲)
  3. 注册工具(后续篇详讲)
  4. 创建 Agent(后续篇详讲)
  5. 打印横幅
  6. 进入 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 的「大脑」。

相关文章

分享: