【万字长文】从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践

作者:mCell日期:2026/3/25

同步更新至个人站点:从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践

相关链接:

从零构建 Mini Claude Code:stack.mcell.top/blog/2026/a…

本次 Mini OpenCode 仓库地址:github.com/minorcell/m…

Memo Code:memo.mcell.top/

前阵子,我写过一篇 从零构建 Mini Claude Code 的 Agent 开发入门教程。

那次基本是顺着 AI SDK 往下做的。不得不说,TypeScript 这套生态在 Agent 开发这件事上,确实已经非常成熟了。

很多原本你以为要自己接的东西,比如 llm provider 协议、message schema、tool calling、loop、streaming,这些基础设施基本都已经有人替你铺好了。

所以整体开发体验会非常顺。你不太需要先去想“这套系统最底层应该怎么搭”,很多时候先把产品做出来就行。

后来我在看 opencode 早期实现的时候,注意到一个挺有意思的细节:

它早期居然是 Go 写的

这件事一下子把我勾住了。

不是因为我突然想讨论“Go 和 TS 到底谁更适合 Agent”,而是这个点刚好落在我自己的日常技术语境里。

因为我平时本来就在 TS 和 Go 两边来回切,所以这对我来说,不是一个“换门语言试试看”的问题,而是一个很自然的延伸:

既然前面我已经沿着 TS 那条路做过一版 mini claude code,现在又刚好看到 opencode 早期和 Go 有过这层关联,那不如顺着自己熟悉的另一条技术路径,再做一个 mini-opencode 看看。

于是,后面就有了这个小实验。

但这次真正有意思的地方,还不只是“换了门语言”。

更准确地说,是这次我把它当成了一次很完整的产品设计 + 架构设计实践

这段时间我一直在外面长期出差,在和一群大学生协作(当然更多是观察)。

一开始,大家通常都是从产品问题进入的:要做什么功能、支持什么能力、界面怎么组织、用户怎么用起来更顺。这个入口很自然,因为产品首先总是以功能和交互的形式被感知到。

但一旦真正开始往下做,问题很快就会往更底层走。你会发现,同样一个功能能不能做稳、后面还能不能继续长,往往取决于另外一些东西:

  • 模块怎么拆
  • 接口怎么定
  • 状态怎么流动
  • 哪一层应该知道什么
  • 哪一层不应该知道什么
  • 边界应该放在哪里

也就是说,产品问题并不会消失,而是会继续往下传导,最后落到架构设计上。

而 mini-opencode 这次,恰好就是一个非常适合拿来做这件事的载体。

因为 Agent 这种系统表面上看是在“调模型”,但你真正做进去以后,会很快发现它其实非常适合拿来练架构基本功。

这个东西应该怎么长

现在回头看,这次 mini-opencode 和之前那篇 mini claude code 最大的差别,不只是使用的编程语言。

而是入口变了

上一次,我们更像是在一个非常成熟的应用层生态里往前走。很多问题你不用太早面对,因为框架已经替你做了选择。你沿着那条路,自然会先关注产品形态、交互、工具设计这些更靠上的问题。

但这一次换到 Go 以后,很多东西一下子变得更裸露了。

不是说做不了,恰恰相反,是很多结构性问题必须尽早想清楚:

  • provider 怎么统一
  • session 放哪
  • loop 怎么闭环
  • tool call 怎么表达
  • 工具执行后的结果怎么回流
  • UI 怎么知道系统当前在干什么
  • 安全边界到底应该落在哪一层

你会发现,这时候讨论已经不再是“这个功能要不要加”。

而是一个更基础的问题:

这个系统应该怎么长,后面才不会乱?

这其实就是典型的架构题。

所以这次我一开始就没有从某个功能点切进去,而是先把项目拆成了 6 个模块:

  • cmd
  • config
  • provider
  • core
  • tools
  • tui

这个拆法表面上看很普通,但我后来越来越觉得,一个好的架构拆分本来就不应该追求“聪明”,而应该追求两件事:

  • 归属清楚
  • 演进自然

config 负责配置加载、默认值、系统提示词、工作区解析;provider 负责统一模型接口和多厂商适配;core 负责 agent loop、session、turn 执行;tools 负责工具注册、工具执行和运行时拦截;tui 负责终端交互;cmd 负责把这些模块装配起来。

这套结构最直接的好处,不是什么“优雅”,而是你一旦开始写代码,立刻知道:

  • 新能力应该长在哪
  • 变化应该落在哪层
  • 某个问题出现时应该先去看哪块

这对一个要持续迭代的系统来说,非常重要。

因为很多项目不是死在“做不出来”,而是死在“第二轮开始就不知道该怎么继续长,逐而陷入重构轮回甚至停止维护”。

好的模块拆分是为了让每一层只处理自己该处理的问题

这是我最近观察下来越来越确定的一件事情。

很多人一开始拆模块,容易变成“按文件类型拆”或者“按功能名拆”,最后得到的是一组名字看起来合理、但职责边界非常模糊的目录。短期能跑,长期一定会相互污染。

mini-opencode 这次我反而特别在意一件事:

每一层到底在回答什么问题?

比如 provider 这一层,回答的是:

  • 模型如何被统一接入?

core 回答的是:

  • 一轮 Agent 执行怎么闭环?

tools 回答的是:

  • 系统如何把“能做的事”暴露出来并真正执行?

tui 回答的是:

  • 用户如何感知这个系统正在干什么?

一旦这么想,模块边界就会非常清楚。

而且这种拆法还有一个很大的收益:它让你能同时照顾到产品视角工程视角

从产品视角,你知道系统最重要的组成部分是什么。 从工程视角,你知道这些组成部分应该如何解耦。

这也是为什么有时候我会觉得,产品设计和架构设计在很多时候并不是两件事。尤其是做 Agent 这种系统的时候,它们几乎就是同一件事的两个面向。

因为你怎么定义产品里的“能力单元”,最后几乎一定会反映在你的模块边界和接口设计上。

Provider 这层,是一个很典型的“接口先于实现”的架构练习

在这次 mini-opencode 里,provider 是我最早收的一层。

因为这层特别适合练一件很基本、但非常关键的架构能力:

不要一开始就把自己绑在某个实现上,而是先定义系统真正依赖什么。

所以这版里我没有让上层直接去感知 OpenAI、Anthropic、Gemini 这些具体差异,而是先定了一组很窄的统一接口:

  • Client
  • Request
  • Response
  • Message
  • ToolCall
  • ToolDefinition

这套接口有两个好处。

第一个好处,是把“系统真正依赖什么”说清楚了。

core 来说,它根本不关心底下是哪一家模型。它真正关心的只有这些:

  • 我有一组 messages
  • 我有一组 tools
  • 我发起一次 complete
  • 我拿回一个 response
  • response 里可能带 assistant message,也可能带 tool calls

就够了。

第二个好处,是实现可以换,但上层逻辑不用跟着漂。

当前这版 provider 模块分别适配了 OpenAI、Anthropic、Gemini,底下用的是共享 HTTP client,没有额外引入外部 LLM SDK。

这并不意味着“手写就更好”,但它很适合这个阶段。因为这里最重要的,不是把每家能力都吃满,而是先把系统和实现之间那条边界立住。

这其实也是我最近和大家最想强调的一点:

接口设计本身就是在决定系统的长期形状。

如果一开始就让 coretoolstui 直接依赖具体 provider 细节,那后面整个系统会很快长歪。你也许一开始觉得快,但第二轮扩展时就会明显感受到成本。

Core 这层最重要的,不是复杂,而是把执行主线收得足够直

很多时候一提到 Agent loop,大家很容易联想到复杂的编排、状态机、planner、memory 等等。

但这次做 mini-opencode 的过程中,我反而越来越确认一件事:

在一个 mini 版本里,最重要的不是把 loop 做复杂,而是把主线做直。

所以 core 这层现在干的事情非常集中:

  • 管理会话消息
  • 执行单轮 turn
  • 记录 usage 和事件
  • 把进度抛给 UI

当前 runTurn() 的主逻辑非常简单:

  1. 用户消息进入 Session.Messages
  2. max_steps 范围内循环
  3. 调用 provider
  4. 记录 assistant message、usage 和进度事件
  5. 没有 ToolCalls 就结束
  6. ToolCalls 就逐个执行
  7. 执行结果转成 tool message 回写
  8. 超过步数就报错

这套设计的关键,不在于它“经典”,而在于它把整个系统最核心的一条执行链完整地暴露出来了。

也有很多系统难维护的原因不是因为模块拆得不对,而是因为最关键的执行主线藏得太深。你表面上看有很多抽象,但一旦要排查问题,根本不知道状态是怎么流动的。

而现在这版里,turn 继续还是结束,本质上就是看本轮模型响应里有没有 ToolCalls;usage 在每步记录;progress event 在每步抛出;tool result 会显式回到 session。

这就是一个很典型的架构收益:

不是功能更多,而是系统行为更可解释。

Tools 是这次最像“产品设计落到架构设计”的地方

一开始我以为 tools 只是“把能力接进去”。

后来发现不是。

真正写起来之后,tools 很快就变成了整个系统里最像“产品设计与架构设计交叉点”的一层。

因为从产品视角看,tool 就是系统能做什么。 从架构视角看,tool 是系统如何组织外部能力、如何执行、如何治理。

所以这一层我没有做成若干函数拼盘,而是明确拆成了三层:

  • Tool
  • Registry
  • Interceptor

这是这次我最满意的一个拆分之一。

先说 Tool

Tool 很朴素,它就是定义“这个能力是什么”和“这个能力怎么执行”。

这层保持简单非常重要。因为工具会越来越多,如果每个工具都夹带一堆框架性逻辑,系统很快会失控。

再说 Registry

Registry 负责注册工具、列出工具定义、按 name 分发执行。 这个抽象看起来很普通,但它解决了一个很关键的问题:

模型看到的能力集合,和系统内部真正可执行的能力集合,是同一个源头。

这意味着能力暴露和能力执行不会出现两套定义。

最后是 Interceptor

这一层是整个工具系统真正开始“像系统”的地方。

因为工具调用不只是一次函数调用,它还常常伴随着:

  • 参数校验
  • 安全拦截
  • 前后状态记录
  • 审计或扩展点
  • 后续治理能力

所以工具执行真正的顺序变成:

  1. 解析 ToolCall
  2. 执行 Before()
  3. 执行 tool
  4. 逆序执行 After()
  5. 返回结果

这套结构的收益非常直接:

  • 加新工具不需要改动主循环
  • 加安全规则不需要侵入每个工具
  • 后续做日志、埋点、审计、缓存,也有明确插点
  • tool system 不会因为能力增长而迅速散掉

这点设计的架构收益在于: 不是今天多做了什么,而是明天新增东西时,不会把昨天的结构弄坏。

工具选型这件事,本身也是产品设计

当前默认工具包括:

  • bash
  • read
  • write
  • edit
  • list
  • glob
  • grep
  • todo
  • webfetch

如果只是站在工程实现上,这看起来像一组功能列表。

但如果从产品设计角度看,它其实是一组非常明确的能力选择:

  • read / write / edit:文件操作闭环
  • list / glob / grep:定位和检索闭环
  • bash:环境执行能力
  • todo:任务状态外显
  • webfetch:把外部网页内容带回本地上下文

也就是说,这不是“多做几个工具”,而是在定义:

这个 mini-opencode 要能做哪些事?

而这次设计稿里,对 MVP 的边界其实是非常明确的:支持单轮/多步 Agent 对话、文件读写、shell 执行、todo 管理、网页抓取、多 provider,以及终端交互;不做持久会话、插件 marketplace、token 级 streaming、多会话管理等。

这其实就是一个很标准的产品设计动作:

  • 不是什么都做
  • 先把主闭环切出来
  • 保证每个模块都围绕主闭环服务

而一旦产品边界切得清楚,架构也会跟着变得更自然。

边界落在哪一层,是这次架构实践里另一个很重要的点

做 Agent 很容易碰到一个问题:很多人会把“约束”写在 prompt 里。

这当然不是不行,但一旦系统能:

  • 读写文件
  • 执行 shell
  • 操作工作区
  • 抓网页

那很多边界如果还只停留在 prompt 层,其实是非常虚的。

所以当前这版里,我比较在意的是让边界尽量落到运行时:

  • WorkspaceInterceptor 校验 bash.working_dir
  • ShellSafetyInterceptor 拦截危险 shell 片段
  • 文件工具内部走 SafeJoin()
  • 工具结果统一渲染成 JSON 再回写给模型

这里你会发现,架构设计和产品设计又一次连到一起了。

从产品角度,这定义了“这个系统允许用户怎么用”。 从工程角度,这定义了“边界到底由谁负责”。

这是一个很典型的设计判断题:

  • 是让每个工具各自想办法守边界?
  • 还是把共性边界提到系统层?

显然这次我选的是后者。因为对于一个会不断加工具的系统来说,共性边界最好尽量系统化。否则能力一多,安全性和一致性就会迅速下降。

TUI 这层,看起来是 UI,实际上也是架构的一部分

很多人会把 UI 看成最后再补的壳,包括我之前在做 Memo Code 的时候也是这么想。

但这次我越做越觉得,在 Code Agent 场景里,TUI 根本不只是个壳。

因为用户不只是想得到一个结果,他还想知道:

  • 系统当前是不是在工作
  • 走到第几步了
  • 在调用什么工具
  • todo 现在是什么状态
  • 当前上下文里有什么

所以 tui 模块不是简单把文本打印出来,而是明确承担一件事:

把运行时状态翻译成用户可感知的交互。

当前界面布局很直接:

  • 左边 Conversation
  • 右边 Context
  • 底部 Composer

同时会显示 token、step、状态文案和 todo;支持 @ 文件补全、queued prompt、草稿恢复、文件选择器,以及小屏时的布局堆叠。

这里最关键的其实不是界面多炫,而是 TUI 和 core 的事件模型是对齐的。core 会抛出 step started / completed、assistant message、tool started / finished 等 progress event,UI 再消费这些事件去刷新状态。

这也是一个非常标准的架构收益:

  • core 不需要知道 UI 怎么渲染
  • tui 不需要知道模型协议细节
  • 两边靠事件接口对齐

这类拆法的好处,在做产品时会特别明显。因为它意味着未来你要换成别的展示层,或者想补更完整的 trace viewer,并不需要把 loop 和 provider 重新打散。

这次做完之后,我对“架构实践”这件事反而更有体感了

如果只把这次 mini-opencode 看成“我又做了一个 Agent demo”,大可不必如此浪费。

因为它更有价值的地方在于:它几乎把一套小型系统里最典型的架构问题,比较完整地暴露出来了。

比如:

  • 模块到底按什么维度拆
  • 接口应该先于实现,还是边写边收
  • 主执行链要不要尽量直
  • 产品能力边界如何映射成工具集合
  • 系统级约束该不该提到运行时
  • UI 和运行时之间应该怎样对接

而 mini-opencode 这次很妙的一点在于,它不是一个抽象案例,而是一个可以真做、真跑、真迭代的具体系统。你每做一层,都会立刻感受到一个架构判断到底是不是靠谱。

这比只在白板上讲“应该高内聚低耦合”“应该接口隔离”“应该职责清晰”,有效得多。

因为真正的架构能力,从来都不是背原则,而是:

你能不能在一个具体系统里,做出那些让后续演进更顺的拆分和接口。

从这个角度看,这次 Go mini-opencode 对我来说,确实是一场很好的架构实践。

不是因为它多复杂。 而是因为它刚好足够完整:有主流程、有能力系统、有状态、有交互、有边界、有扩展点。

这样的系统最适合练基本功。

当然,这仍然是一个很初步的 mini-opencode

当前也还有一些边界非常明确:

  • glob 目前不支持把 ** 当递归匹配
  • grep 现在返回的是匹配行,context 参数还没展开成上下文块
  • Context 面板当前更聚焦状态和 todo,不单独展示完整 trace
  • provider 还是 request/response 式,没有做 token 级 streaming

但我反而觉得,这种边界清楚是好事。

因为一个架构实践里,最怕的不是“还没做完”,而是“系统边界已经糊了”。只要边界清楚,下一步该补什么、该在哪层补,都是清晰的。

这其实也是我最近越来越喜欢的一种做法:

先做一个结构清楚的 mini 版本。 功能可以少一点,但形状要正。

因为一旦形状正了,后面很多东西都是顺着长出来的。

(完)


【万字长文】从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践》 是转载文章,点击查看原文


相关推荐


Rust宏编程完全指南:用元编程解锁Rust的终极力量
土豆12502026/3/17

"宏就像是编译器的魔法棒,挥一挥,重复的代码就消失了。" —— 某位深夜 debug 的 Rustacean 目录 Why:为什么需要宏? What:宏是什么? How:如何使用宏? 声明宏 (macro_rules!) 派生宏 (Derive Macros) 属性宏 (Attribute Macros) 函数式宏 (Function-like Macros) 最佳实践 常见误区 总结 Why:为什么需要宏? 想象一下,你正在写一个 Web 框架,需要为 50 个不同的结构体实现相


【毕设】前后端(无模型训练)
2301_815389372026/3/8

后端 第一步,先建一个项目文件夹。 打开你电脑上任意一个地方,新建一个文件夹,就叫 ebike-detection,然后把你的 best.pt 复制进去。 第二步,安装Flask和相关依赖。 打开命令提示符(按 Win+R,输入 cmd,回车),然后把下面这行命令复制进去运行: pip install flask flask-cors ultralytics pillow 好,第三步,创建Flask后端文件。 在你的 ebike-detection


Node.js 安装与配置完全指南:从零开始搭建开发环境
张3蜂2026/2/28

目录 引言 第一部分:Node.js 简介与版本选择 1.1 什么是 Node.js? 1.2 Node.js 版本介绍 第二部分:Node.js 安装方式详解 2.1 方式一:官方安装包(最简单,适合初学者) Windows/macOS 安装步骤: 2.2 方式二:包管理器安装(适合 Linux 用户) Ubuntu/Debian 系统安装步骤: CentOS/RHEL 系统安装步骤: macOS 使用 Homebrew 安装: 2.3 方式三:使用 NVM 安装(最推


ThreadForge v1.1.0 发布:让 Java 并发更接近 Go 的开发体验
一只叫煤球的猫2026/2/20

正好春节放假,自驾去了陕西、河南、安徽,一路上走走停停。 白天基本在路上,晚上在酒店或者服务区休息时,抽一些时间继续打磨 ThreadForge。 一点点补了个 v1.1.0 出来。 仍然保持 ThreadForge 的目标: 让 Java 能写出更简单、更可推理、更可观测的并发代码。 这次版本,重点补齐了并发开发里几个还算常见的能力。 v1.1.0 核心更新 Retry Policy(失败重试) 支持 scope 级默认重试,也支持任务级覆盖,不再到处手写 while/try-catch


git pull拉取的时候碰到报错:error: 您对下列文件的本地修改将被合并操作覆盖 请在合并前提交或贮藏您的修改。
skywalk81632026/2/11

git pull拉取的时候碰到报错: error: 您对下列文件的本地修改将被合并操作覆盖:         data/processed/acnes_related_data.csv         data/processed/activity_data.csv         data/processed/hemolytic_data.csv         data/raw/active_peptides.csv         data/raw/hemolytic.csv      


自己搭邮件服务器有多难?我用 Mailu 跑通了整套企业邮箱
GetcharZp2026/2/3

从“为什么要自建邮箱”讲起,拆解 Mailu 的架构、优缺点,以及普通人也能照着做的安装实战。 Github:github.com/Mailu/Mailu 官网:mailu.io/ 这几年,越来越多团队开始重新审视一件事:邮箱,到底要不要掌握在自己手里? 用第三方企业邮箱当然省事,但账号封禁、功能限制、隐私不可控的问题,一旦遇到,几乎没有回旋余地。于是,自建邮件服务器这件事,又被不少技术团队重新捡了起来。 而在一堆方案里,Mailu 是被频繁提到的一个名字。 Mailu 是什么?一句话先讲明


VS code 类产物中 win11 终端字体内容和颜色 加粗不匹配问题
小兵张健2026/1/24

我尝试了各种方式,换字体,改配置,结果还是乱的,有人知道怎么搞吗?跪求,Mac好像天然就没问题,急急急大佬们


Django 踩坑记:OceanBase 4012 Timeout 两条红线,语句超时 vs 事务超时一次讲透
哈里谢顿2026/1/16

一、4012 是谁抛的? Django 本身没有 4012 错误码,它是 OceanBase 的“杀手”信号: 当前 SQL 或 当前事务累计执行时间 ≥ 系统阈值,直接返回 4012。 二、两条红线长啥样? 变量名默认阈值计时对象触发后果ob_query_timeout10 000 000 µs = 10 s单条 SQL 执行时长这条语句被杀,事务可继续ob_trx_timeout100 000 000 µs = 100 s事务 begin→


HarmonyOS一杯冰美式的时间 -- @Env
猫猫头啊2026/1/8

一、前言 该系列依旧会带着大家,了解,开阔一些不怎么热门的API,也可能是偷偷被更新的API,也可以是好玩的,藏在官方文档的边边角角~当然也会有一些API,之前是我们辛辛苦苦的手撸代码,现在有一个API能帮我们快速实现的,希望大家能找宝藏。 如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏 二、@Env的诞生背景 OK,步入正题把,在多设备开发的场景中,我们经常需要根据不同的设备环境(比如窗口大小、横竖屏等)来调整UI布局。以前我


Nop入门:使用Excel模板生成包含图表的报表
canonical_entropy2025/12/30

讲解视频:Nop入门:使用NopReport导出Excel图表_哔哩哔哩_bilibili Nop平台内置了一个非常精简的中国式报表引擎NopReport,它可以实现商业报表引擎如FineReport和润乾报表的核心功能。NopReport很巧妙的利用Excel单元格的批注机制来存放扩展信息,可以直接使用Excel模板来作为报表模板,这样大大简化了报表制作过程,并且可以复用客户已有的各种业务表格。 在实现层面,NopReport没有使用Apache POI库,而是选择了直接使用流式接口解析Off

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客