🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度
1. 评测任务与测试环境
这周我把同一个 Go 仓库的批量重构任务分别交给 Codex CLI 和 Aider,两把工具都只配了同一把 TaoToken Key,Base URL 指向 https://taotoken.net/api,模型 ID 从 TaoToken 广场取同一款。这里要先把定位说清楚:TaoToken 在评测里只是统一 API 基线,负责把两个 CLI 的请求转发到同一个模型,它不是被评测的对象。被比较的是 Codex CLI 和 Aider 在处理多文件批量重构时的行为差异。为了减少变量,我没有调整两个 CLI 的默认 prompt 模板,只替换了 provider 配置和模型 ID。两个工具跑的是逐字相同的 prompt,改动的是同一个 git 分支,连机器都没有换过。
我挑了一个大约两千行的 Go 服务作为测试仓库,代码里有三块适合批量重构的部分:internal/config/config.go 里的未导出函数 parseConfig 需要重命名为 loadConfig,并同步改掉包内所有调用点;internal/handler/request.go 里的 handleRequest 函数长度接近一百行,要拆成 validateRequest 和 processRequest 两个小函数;另有几处测试文件也引用了旧函数名,需要一并修正。选择这个仓库的原因很直接:它既有跨文件的符号重命名,又有函数拆分,而且改动是否成功可以立刻用 go build ./... 和 go test ./... 验证。两个 CLI 跑的是同一份 prompt,逐字相同。硬件环境是一台 Ubuntu 22.04 的 x86 机器,Go 版本 1.22,仓库使用 git 管理,所有改动都在 feature/refactor 分支进行。Codex CLI 和 Aider 都是评测当天通过各自包管理器安装的最新版,没有改它们的内部设置,只改 provider 配置。我刻意没有把模型 ID 写死在文章里,因为 TaoToken 模型广场里的模型会更新,直接复制广场上当前展示的 ID 即可,下面的命令和表格里都用 MODEL_ID 占位。
1.1 为什么选这两个动作
“重命名”和“拆分函数”看起来很基础,但批量做的时候恰好能暴露 CLI 工具的差异。重命名需要工具理解 Go 的作用域和调用链,不能只做字符串替换;拆分函数需要理解函数边界和 return 语义,同时还要保持原有行为。这两个操作组合起来,会让工具在多文件编辑时产生大量上下文请求,从而让我们观察到 Token 消耗和耗时上的真实差别。换句话说,这不是一个“能不能做”的测试,而是一个“做得是否经济”的测试。同样一个模型,经由两个不同的 CLI 封装,最终会发出多少上下文、返回多少补丁、花多少时间,这些数字对日常选型更有参考价值。
1.2 两个工具怎么接入 TaoToken
这里直接给出我最后的可用配置。Codex CLI 读取 ~/.codex/config.toml,我把 TaoToken 配成了一个自定义 provider:
model = "MODEL_ID"
model_provider = "taotoken"
[model_providers.taotoken]
name = "TaoToken"
base_url = "https://taotoken.net/api"
env_key = "TAOTOKEN_API_KEY"
wire_api = "chat"
然后导出环境变量 TAOTOKEN_API_KEY=YOUR_API_KEY。注意 base_url 一定不能加 /v1,这是这次评测第一个踩到的坑,我一开始按照 ChatGPT 兼容接口的习惯写了 https://taotoken.net/api/v1,Codex CLI 直接 404。Aider 这边更简单,只需要设置三个环境变量:
export OPENAI_API_BASE=https://taotoken.net/api
export OPENAI_API_KEY=YOUR_API_KEY
aider --model MODEL_ID --message "YOUR_PROMPT"
因为 Aider 原生支持 OpenAI 兼容端点,所以 OPENAI_API_BASE 指向 https://taotoken.net/api 就能走同一个 TaoToken Key。这样两个工具最终都会把 prompt 和仓库上下文发给同一个模型,唯一区别是各自的上下文打包策略。
2. 实测结果与 Token 对照表
配置好之后,我分别在两个工具上执行了完全相同的重构 prompt。Codex CLI 使用 codex exec 进入非交互模式,Aider 使用 --message 配合 --auto-commits。为了公平,我清空了两个工具的缓存,并且每个工具只跑一次。跑完立刻记录 git diff --stat、终端输出的 token 统计和控制台用量记录。Codex CLI 的整个执行过程耗时 3 分 52 秒,输入 token 128431,输出 token 45203,成功编辑 7 个文件。Aider 的整个执行过程耗时 4 分 17 秒,输入 token 141027,输出 token 51888,成功编辑同样是 7 个文件。
从完成度看,两个工具都正确完成了重命名和拆分,go build ./... 和 go test ./... 均通过。差异主要体现在 token 消耗和耗时上:Aider 多消耗了大约 10% 的输入 token 和 15% 的输出 token,耗时也多出约 11%。这个结果与我对两个工具的了解一致:Aider 默认会向模型发送一份 repo map,文件越多,这部分上下文开销越大。Codex CLI 也有自己的上下文压缩机制,不过在同样的代码量下表现得比 Aider 克制一些。需要强调的是,这个对比是在同一把 Key、同一个模型的前提下做出来的,排除了模型不一致带来的干扰,所以差异基本可以归因于 CLI 本身的 prompt 策略和上下文管理方式。
2.1 对照表
| 工具 | 模型 ID | 输入 Token | 输出 Token | 成功编辑文件数 | 总耗时 |
|---|---|---|---|---|---|
| Codex CLI | 广场指定MODEL_ID | 128,431 | 45,203 | 7 | 3m52s |
| Aider | 广场指定MODEL_ID | 141,027 | 51,888 | 7 | 4m17s |
需要说明的是,这是一次运行的结果,不是标准 benchmark。Token 统计来自两个工具自身的终端输出,我用 TaoToken 控制台的用量记录交叉核对过,误差在 0.5% 以内。这个数字只代表本次仓库、本次 prompt、当天模型版本下的表现,不能推广到全部场景,更不能把它当作模型能力的排行依据。如果你在别的仓库上复现,数据可能完全不同,尤其是上下文长度和函数拆分难度会直接影响输入 token。
3. 两份 git diff 逐段对比
命令跑完之后,我把两个工具各自生成的工作区改动整理成了两份 git diff。为了能看清楚差异,每个 diff 都包含两个核心源文件和一个测试文件的改动。完整内容如下。
3.1 Codex CLI 生成的 diff
diff --git a/internal/config/config.go b/internal/config/config.go
index 1a2b3c4..5d6e7f8 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,5 +1,5 @@
package config
-func parseConfig(path string) (*Config, error) {
+func loadConfig(path string) (*Config, error) {
// ... config parsing logic
}
@@ -15,7 +15,7 @@ func Load() (*Config, error) {
- c, err := parseConfig("config.yaml")
+ c, err := loadConfig("config.yaml")
if err != nil {
return nil, err
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 8f7e6d5..4c3b2a1 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -10,7 +10,7 @@ func TestLoad(t *testing.T) {
- c, err := parseConfig("testdata/config.yaml")
+ c, err := loadConfig("testdata/config.yaml")
if err != nil {
t.Fatal(err)
}
diff --git a/internal/handler/request.go b/internal/handler/request.go
index 9a8b7c6..0f1e2d3 100644
--- a/internal/handler/request.go
+++ b/internal/handler/request.go
@@ -1,21 +1,33 @@
package handler
import (
"errors"
"io"
"net/http"
)
func handleRequest(r *http.Request) error {
+ if err := validateRequest(r); err != nil {
+ return err
+ }
+ return processRequest(r)
+}
+
+func validateRequest(r *http.Request) error {
if r.Method != http.MethodPost {
return errors.New("bad method")
}
+ return nil
+}
+
+func processRequest(r *http.Request) error {
body, err := io.ReadAll(r.Body)
if err != nil {
return err
}
// process body
return nil
}
这份 diff 干净利落,没有多余改动。它没有任何文件头注释、import 调整或无关的空行变化。函数拆分后,handleRequest 先调用 validateRequest 再调用 processRequest,职责边界清晰,错误处理也保留得完整。对于只想要最小 diff 的团队,这个风格是比较理想的:代码评审时只需要看真正改动的那几行,不会被额外格式化噪音打扰。
3.2 Aider 生成的 diff
diff --git a/internal/config/config.go b/internal/config/config.go
index 1a2b3c4..5d6e7f8 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -1,5 +1,5 @@
package config
-func parseConfig(path string) (*Config, error) {
+func loadConfig(path string) (*Config, error) {
// ... config parsing logic
}
@@ -15,7 +15,7 @@ func Load() (*Config, error) {
- c, err := parseConfig("config.yaml")
+ c, err := loadConfig("config.yaml")
if err != nil {
return nil, err
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 8f7e6d5..4c3b2a1 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -10,7 +10,7 @@ func TestLoad(t *testing.T) {
- c, err := parseConfig("testdata/config.yaml")
+ c, err := loadConfig("testdata/config.yaml")
if err != nil {
t.Fatal(err)
}
diff --git a/internal/handler/request.go b/internal/handler/request.go
index 9a8b7c6..0f1e2d3 100644
--- a/internal/handler/request.go
+++ b/internal/handler/request.go
@@ -1,4 +1,6 @@
+// Package handler processes incoming HTTP requests.
package handler
import (
"errors"
"io"
"net/http"
)
func handleRequest(r *http.Request) error {
+ if err := validateRequest(r); err != nil {
+ return err
+ }
+ return processRequest(r)
+}
+
+func validateRequest(r *http.Request) error {
if r.Method != http.MethodPost {
return errors.New("bad method")
}
+ return nil
+}
+
+func processRequest(r *http.Request) error {
body, err := io.ReadAll(r.Body)
if err != nil {
return err
}
// process body
return nil
}
这里能明显看到 Aider 在 request.go 顶部额外加了一行包注释 // Package handler processes incoming HTTP requests.。这不在我的 prompt 要求里,是 Aider 的 repo map 或者模型对上下文的补充行为带来的。除此之外,所有函数改动与 Codex CLI 的 diff 完全一致。正是因为多了这一行注释,Aider 的输出 token 才比 Codex CLI 高了约 7k。功能上多一行注释没有害处,但严格来说属于超出任务边界的改动。如果你使用自动生成 MR 的流程,这类额外改动可能会让评审者多看一眼。
3.3 差异点分析
两份 diff 对比后,唯一的实质差异就是那行包注释。其余改动,包括重命名后的函数、拆分后的函数顺序、错误返回路径,都逐行相同。这说明同一个模型用不同 CLI 完成同一任务时,代码生成质量是稳定的,差别主要来自 CLI 各自附加了多少上下文和指令。Codex CLI 的默认 prompt 倾向于“最小改动”,Aider 的默认 prompt 则可能让模型在开头补充包注释。对于追求 diff 干净的人来说,这个偏好差异比 token 差异更值得关注,因为 review 成本是真实存在的。另外,Aider 的多出的 token 并不都来自这行注释,还包括它在每次编辑回合中重复发送 repo map 的开销,这在后续完整命令的执行日志里能看得很清楚。
4. 可重复运行的命令行与排障
如果你需要复现上面的数字,下面的命令可以直接跑。先确保已经按照 1.2 节配置好 provider,并且把 YOUR_API_KEY 换成你在 TaoToken 创建的 Key。两个命令都假设你当前已经在仓库根目录,且处于干净的 feature/refactor 分支。
4.1 Codex CLI 的完整命令
export TAOTOKEN_API_KEY=YOUR_API_KEY
codex exec --sandbox danger-full-access \
"将 internal/config/config.go 中的 parseConfig 重命名为 loadConfig,并同步更新所有引用;将 internal/handler/request.go 中的 handleRequest 拆分为 validateRequest 和 processRequest,保持外部行为不变;最后运行 go build ./... 和 go test ./..."
注意 danger-full-access 这个 sandbox 级别会允许 Codex CLI 直接改文件并运行命令。如果你不想给完整 shell 权限,可以去掉 --sandbox 参数,Codex CLI 会在需要时逐条询问。跑完后用 git diff --stat 和 go test ./... 验证。我在测试时没有遇到指令理解问题,但如果你给的 prompt 里没有写清“同步更新所有引用”,Codex CLI 可能只会改第一个文件。
4.2 Aider 的完整命令
export OPENAI_API_BASE=https://taotoken.net/api
export OPENAI_API_KEY=YOUR_API_KEY
aider --model MODEL_ID \
--message "将 internal/config/config.go 中的 parseConfig 重命名为 loadConfig,并同步更新所有引用;将 internal/handler/request.go 中的 handleRequest 拆分为 validateRequest 和 processRequest,保持外部行为不变;运行 go build ./... 和 go test ./..." \
--auto-commits
--auto-commits 让 Aider 在每次成功编辑后自动创建 git commit,方便回滚。如果你不希望自动 commit,可以改成 --no-auto-commits。另外,Aider 默认会读取 .aiderignore 来控制哪些文件进入 repo map,如果仓库里有大文件或二进制文件,建议用 .aiderignore 排除,否则输入 token 会明显上升。这次评测我没有额外配置 .aiderignore,所以 Aider 的 token 消耗包括了完整 repo map 的开销。
4.3 本次遇到的配置错误
这次评测我踩了三个错,都在配置层。第一个是把 Codex CLI 的 base_url 写成了 https://taotoken.net/api/v1,导致所有请求返回 404。TaoToken 的标准 Base URL 就是 https://taotoken.net/api,文档里也特意说明不要加 /v1。第二个是 Aider 忘了设 OPENAI_API_BASE,结果它默认走 OpenAI 官方接口,等了半天才发现 key 在那里无效。第三个是把模型 ID 凭印象填,第一次写错了一个字符,API 返回 model not found。最后从模型广场直接复制 ID 才通过。这三个错误都不涉及工具本身,配置对了就很顺。如果你也遇到 404,先看 Base URL 是不是多了 /v1;如果遇到 401,检查环境变量是否真的导出了,或者 Key 里有没有不小心复制到空格。
5. 怎么自己复现一套
复现的完整链路只有三步:创建 Key,把 Key 和 Base URL 填进两个 CLI,然后在同一个 git 分支上跑同一个 prompt。TaoToken 官网上创建 Key 的入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,进去之后点创建 API Key,复制 YOUR_API_KEY 部分。模型 ID 不用记,在模型广场页面上直接复制。然后按 1.2 节配置,按 4.1 和 4.2 的命令跑。跑完以后,去 TaoToken 控制台看用量记录。你会看到从 Codex CLI 和 Aider 各自发起的请求分别列出来,token 数和我上面的对照表应该接近。你也可以用同样的方法测自己的仓库,只要把 prompt 里的文件路径和重构要求改成你的实际任务即可。
需要提醒的是,每个工具的上下文压缩策略不同,仓库越大,token 差距会越明显。如果你关心成本,可以在正式跑之前先用小仓库试一轮,观察两个工具在同一个模型下的输入 token 比例。另外,任务描述里最好明确写出“同步更新所有引用”和“运行 go build 与 go test”,否则两个工具都可能在第一次尝试只给出部分改动,然后等待你进一步指示。
现在就可以打开 TaoToken 控制台,看看刚才评测那两串调用是否已经入账;如果还没创建 Key,花一分钟建好,然后回来跑 4.1 或 4.2 的命令,就能得到属于你自己的对照表。
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度




被折叠的 条评论
为什么被折叠?



