从零写一个 AI 编程助手:MiniCode 的设计笔记
做 MiniCode 这个项目的起因很简单:我想真正搞懂 Claude Code 是怎么工作的。
读源码没有,代码不开源。看教程,全是 Python + LangChain 的 Hello World,看完还是不知道一个真实的编程助手内部长什么样。所以就自己写了一个——MiniCode,用 Go,3000+ 行,能用。
这篇文章不是文档,是记录几个做的时候觉得有意思的设计决策。
工具系统:Agent 能做什么取决于你给它什么
最开始我以为 AI Agent 的核心是模型有多强。做完之后发现,工具设计才是最重要的部分。
MiniCode 有六个工具:glob(文件搜索)、view(文件查看)、grep(内容搜索)、bash(命令执行)、write(文件写入)、edit(代码编辑)。
这六个工具对应的是一个程序员操作文件系统的基本动作,没有更多了。但 Agent 能用这六个工具做相当复杂的事:它可以先 glob 找相关文件,grep 搜索关键词,view 看上下文,然后 edit 精准修改。
工具系统的接口长这样:
1 | func GlobTool(ctx context.Context, input GlobInput, call fantasy.ToolCall) (fantasy.ToolResponse, error) { |
每个工具就是一个普通函数。Fantasy SDK 负责把函数签名转换成 JSON Schema 告诉模型,处理模型返回的调用请求,把结果塞回对话历史。
这里有个细节值得说:edit 工具用的是字符串精确匹配,不是行号。
1 | count := strings.Count(originalContent, input.OldString) |
为什么不用行号?因为 LLM 数行号不可靠。模型看到的文件内容里有行号前缀,它可能数错,或者在多轮对话后行号已经变了。字符串匹配虽然要求模型复制一段原文,但更稳定。Claude Code 也是这个思路。
权限系统:用 channel 阻塞代替轮询
这是整个项目里我最满意的一个设计。
问题是这样的:Agent 在执行 bash 命令之前需要用户确认。但 Agent 跑在一个 goroutine 里,TUI 在主线程,怎么让 Agent “等待”用户按键?
最直觉的做法是加个回调函数,或者让 Agent 定期轮询一个 flag。但这样代码会变得很丑。
Go 的 channel 天然适合这个场景:
1 | // Agent goroutine 里 |
Agent goroutine 发一个权限请求消息给 TUI,然后阻塞在 channel 上。TUI 显示确认对话框,用户按键,往 channel 里写一个响应,Agent goroutine 恢复执行。
整个流程线性、清晰,没有回调嵌套,没有状态机,没有轮询。代码读起来就像同步代码。
还加了持久授权(按 a),下次遇到相同操作直接放行:
1 | case Persistent: |
流式输出:打字机效果背后
流式输出看起来像是个 UI 特性,但实现起来涉及并发。
agent.Stream() 跑在独立 goroutine 里,通过回调把数据推给 TUI:
1 | result, err := m.agent.Stream(ctx, fantasy.AgentStreamCall{ |
m.program.Send() 是 Bubble Tea 的线程安全接口,可以从任意 goroutine 向主事件循环发消息。TUI 主循环收到 streamTextMsg 后追加文本、刷新渲染,就是打字机效果。
取消也很干净。每次开始流式请求都创建一个 context.WithCancel,Esc 键触发 cancelFunc():
1 | if msg.Type == tea.KeyEsc && m.streaming { |
agent.Stream() 里的网络请求会感知到 context 被取消,返回 context.Canceled 错误。TUI 收到 streamDoneMsg 时检查是不是取消,是的话回滚历史:
1 | if errors.Is(msg.err, context.Canceled) { |
TUI:Elm 架构让状态管理不至于失控
Bubble Tea 强制你用 Elm 架构:
1 | Model(状态) + Update(消息 → 新状态) + View(状态 → 字符串) |
Update 是纯函数,所有副作用都用 tea.Cmd 表达(本质上是一个返回消息的函数)。流式请求、文件读取、权限确认,都是 Cmd。
1 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { |
好处是状态转移完全可追踪,不会出现”这个状态是怎么变成这样的”这种困惑。坏处是消息类型会越来越多,Update 函数会变长。现在 MiniCode 的 Update 大概有 300 行,已经开始有点难找了。
关于 Go 的选择
为什么用 Go 不用 Python?
主要是因为 Go 的并发模型让上面这些设计实现起来更自然。goroutine + channel 处理”Agent 线程和 UI 线程通信”这类问题,代码非常直接。Python 做同样的事需要 asyncio 或者 threading,会复杂一些。
另外 Go 单二进制部署,不用管依赖环境,用户 go run . 就能跑起来。
当然 Python 生态在 AI 这块确实更成熟。如果要做复杂的上下文压缩、长期记忆这些,Python 的库会方便很多。MiniCode 目前的上下文管理非常简单——就是把历史全部追加,对话够长了就会超出 token 限制。这是后面要解决的问题。
项目还在更新,计划把 30 天的教程写完。目前到 Day 7(配置系统),后面会覆盖 Agent 循环、上下文压缩、子 Agent 这些。感兴趣的话可以看 GitHub。
- 标题: 从零写一个 AI 编程助手:MiniCode 的设计笔记
- 作者: Infy AI
- 创建于 : 2026-03-31 10:00:00
- 更新于 : 2026-03-31 08:58:27
- 链接: https://www.rasior.com/2026/03/31/20260331_minicode-design-notes/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。