最近在折腾 AI 编程工具时,一直被一个问题吊着胃口:不同智能体客户端接上不同模型,实际写一个完整小项目时差距到底有多大?光看跑分和宣传页没有意义,于是我干脆拿经典生存玩法“饥荒Like”复刻当成了试金石,让 DeepSeek V4 Pro 相关模型 + Zcode、Codex 两个工具分别从空目录开始生成一个可玩的小游戏原型。今天这篇就把完整测试流程、核心配置、可复现代码和排错清单一起整理出来,文章略长,但对准备引入 AI 辅助开发的同学会有参考价值。
在正式开始之前先说明一点:我选择“饥荒小游戏复刻”并不是要把 Don't Starve 照搬,而是做一个功能类似的学习版 Demo,不涉及原作名称、美术素材和关卡设计。整个项目采用原生 HTML + Canvas + JavaScript,方便代码生成、方便浏览器直接运行,也更适合用来暴露编程助手在任务拆解、状态管理和多轮修改上的真实水平。
1. 为什么拿“饥荒小游戏”当 AI 编程工具的试金石
1.1 先对齐三个关键词
在展开实测细节前,先把这次涉及的几个概念说清楚,避免后面看配置时发懵。
DeepSeek V4 Pro:在最近一段时间的社区讨论里,这个名字出现得很频繁。需要提醒的是,不同模型服务商提供的 model 标识可能不一致,并不是所有平台都会把deepseek-v4-pro当作可直接调用的模型名。真实使用时,而是要以你当前 API 服务商的控制台列表为准。本文提到的“DeepSeek V4 Pro”更像是一个测试目标名称,实际测评中如果模型名不对,会直接报there is an issue with the selected model deepseek v4 pro,这个我在后面排错章节会单独讲。
Zcode:一个围绕智能编码场景设计的客户端工具,网上讨论常会把它和智谱生态放在一起说。它的核心卖点之一是支持长上下文,不少文章会提到“1 亿 token”“3 亿 token”等会话能力。实际体验时可以把它理解为一款类似 Codex 的编程智能体前端:在界面上描述任务,它会读取本地代码文件,自动增删改代码,并在终端或面板中展示执行过程。
Codex:OpenAI 推出的编程智能体工具,官方提供 CLI 和桌面端形态。它不仅能回答代码问题,还能在本地项目里完成“规划 -> 修改代码 -> 运行命令 -> 反馈结果”的闭环。默认使用 OpenAI 模型,但也允许通过配置文件接入第三方模型端点。
1.2 为什么选择“饥荒Like”而不是待办清单
现在很多 AI 编程工具演示案例都是“生成一个 ToDo List”或者“写一个计算器”。这类项目流程太短,只要模型背过类似代码,生成质量看起来都会不错,根本测不出编程智能体的差异。
“饥荒Like”生存小游戏却能暴露不少真实问题:
- 涉及玩家移动、资源采集、饱食度衰减、战斗、怪物 AI、昼夜循环等多个系统;
- 各系统之间状态相互影响,饥饿值低了要掉血,白天黑夜要改变视野,怪物会主动追击;
- Canvas 游戏需要在
requestAnimationFrame单循环里维护增量时间,如果模型不理解dt,写出来的游戏要么速度失控,要么卡顿; - 需要跨多个代码块维护全局变量,很容易出现“上下文一长就忘记前面设定”的问题;
- 资源类型、掉落物、攻击判定都强依赖坐标距离计算,非常考验代码结构。
所以这个项目的复刻过程能很好回答:一个编程工具的真正短板,到底是模型太笨,还是智能体没把代码改对?我用同一个需求文档,分别在 Zcode 中接入 DeepSeek 系列模型,以及在 Codex 中使用默认配置,从零搭建同一款游戏,再横向观察修复 Bug 的多轮表现。
2. 环境准备:Codex 与 Zcode 的安装
2.1 环境要求
如果只是让 AI 生成一段代码,哪个工具都差不多。但要做“饥荒小游戏复刻”这种需要代理读写项目文件的任务,建议先把本地环境整理干净。本次测试使用的是一台 Windows 11 x64 电脑,Node.js 版本为 20+,Python 环境为 3.11。如果你使用的是 macOS 或 Linux,命令差异不大,只是全局安装路径和 shell 配置会不同。
需要准备的工具列表如下:
- Node.js 20 或更高版本,用于本地预览 HTTP 服务;
- Git,用于记录 AI 每次修改前后的代码变化;
- 一个模型服务商的 API Key,比如 DeepSeek API Key;
- Codex CLI 或 Codex 桌面端;
- Zcode 客户端;
- 一个空的测试项目目录,避免让工具去读无关历史代码。
需要说明的是,各工具版本迭代较快,安装包的下载方式也可能调整,建议打开对应官方文档获取最新地址。本文章节的重点是“为什么要这样配”和“报错以后怎么查”,而不是锁死某个具体版本号。
2.2 安装 Codex CLI
Codex CLI 的安装依赖 npm 或 Homebrew。以 npm 方式为例,在终端执行:
npm install -g @openai/codex安装完成后,执行codex --version验证是否安装成功。如果终端提示codex 不是内部或外部命令,需要检查 npm 全局安装路径是否在 PATH 环境变量中。
在 Windows 下,如果依赖 Rust 工具链构建失败,可以优先使用官方提供的桌面版安装包。之后在项目根目录执行:
codex即可进入交互式编程智能体界面。
2.3 安装并启动 Zcode
Zcode 的安装方式比 Codex 更偏向图形界面操作。你可以从相关官网下载安装包,安装完成后启动客户端,使用账号登录或配置 API Key。在部分版本中,Zcode 也提供命令行入口,具体以你下载版本的 README 为准。
安装 Zcode 成功后,建议先做下面几件事:
- 进入设置,找到“模型供应商”或“Provider”配置;
- 确认是否已经预置了 DeepSeek API 模板;
- 准备一个专门用于本项目的空目录,不要直接让智能体读取整个磁盘文件。
从实际体验看,Zcode 对新手更友好,比较适合在图形界面里观察“它到底改动了哪些文件”;Codex 则更接近终端工作流,适合长期写脚本和命令行工具的开发者。两者并不冲突,后面我会给出结合使用的建议。
2.4 准备好模型 API Key
无论使用哪个工具,只要是接入第三方模型,都要准备 API Key。以 DeepSeek 平台为例,注册后进入控制台,创建 API Key,然后记录两个信息:
base_url,也就是模型接口地址;model名称,如deepseek-chat、deepseek-reasoner,或你所在平台提供的其他标识。
真实项目里千万不要把 API Key 硬编码进前端代码,也不要提交到 Git 仓库。我建议在本地创建.env文件管理敏感信息,并确保.gitignore正确忽略该文件。生成 API Key 时也要设置额度上限,防止测试过程中因为死循环导致账单异常。
3. 核心配置:让 Codex 和 Zcode 接入 DeepSeek 模型
3.1 Codex 接入自定义模型 Provider
Codex 默认连接 OpenAI 模型,但它支持通过config.toml添加自定义模型服务商。这很像给前端配置 axios 的baseURL,只不过这里配置的是一个完整的编码智能体。
先找到 Codex 配置文件所在目录,在常见系统里通常为~/.codex/config.toml。下面是一个最简配置示例:
model = "deepseek-v4-pro" [model_providers.deepseek] name = "DeepSeek" base_url = "https://api.deepseek.com/v1" env_key = "DEEPSEEK_API_KEY" wire_api = "chat"配置项说明:
model:指定默认使用的模型名,必须和 API 服务商提供的名称完全一致;[model_providers.deepseek]:定义一个名为deepseek的模型供应商;base_url:供应商接口地址,缺少/v1或填错都会导致鉴权失败;env_key:告诉 Codex 从哪个环境变量读取 API Key;wire_api:接口协议风格,这里填chat表示走 Chat Completions 格式。
配置完成后,打开终端执行:
export DEEPSEEK_API_KEY="你的Key" codex --provider deepseek如果模型名不存在或服务商尚未支持,Codex 会返回类似下面的错误:
There is an issue with the selected model deepseek-v4-pro此时不要急着认为“模型不行”,应该先到 API 服务商的控制台确认实际模型名,并把config.toml中的字符串改成正确的值。
3.2 Zcode 添加 DeepSeek 模型
Zcode 的模型配置通常在图形界面里完成。进入模型或 Provider 设置,选择“自定义模型”或“添加供应商”,填写参数:
Provider 名称:DeepSeek Base URL:https://api.deepseek.com/v1 API Key:你的 Key 模型 ID:deepseek-v4-pro 或服务商列表中实际存在的模型名保存后建议新建会话测试一次。
如果遇到“会话提问的时候好像没有上下文”的情况,大概率不是模型变笨了,而是:
- 会话里没有把项目目录正确添加到工作区,导致智能体根本没读取到代码;
- 开启了每次提问独立请求的模式,上下文没有累积;
- 上下文总长度超过了当前所配模型的能力上限,旧内容被自动丢弃。
因此在 Zcode 中开始项目前,需要先确认工作区目录已加载。如果工具支持“自动包含最近文件”或“补充本地项目上下文”的相关选项,建议打开。长上下文是本类工具的一大卖点,但如果项目里包含node_modules或构建缓存,不是所有内容都适合塞进上下文中,需要设置忽略规则。
3.3 为什么配置一直不生效
很多人在配置自定义模型时会有一种感觉:明明填对了,工具还是用默认模型回答。原因通常有三个:
- 缓存未清理:部分客户端会缓存 provider 列表,修改后需要重启应用,甚至删除本地配置缓存;
- 模型名和端点不匹配:服务商不一定使用公共标准名,比如控制台里显示的是
DeepSeek V4 Pro,但 API model 字段却是另一个内部代号; - 没有切换会话:在旧会话中修改模型配置,会话仍然沿用旧的模型参数,所以最好新建会话验证。
遇到这类问题的最快排查路径是:先在本地用 curl 或 Postman 直接调用 API,确认这个模型名和密钥确实可用;再用该信息去配置智能体工具。这样能快速把问题定位到“API 服务端”还是“客户端配置”。
4. 饥荒Like 小游戏复刻:需求拆解与代码演示
4.1 先把游戏需求压缩成可执行清单
这次实测里最容易翻车的地方就是最初的需求描述。如果直接让 AI 做“复刻饥荒”,它可能会生成一段美丽但无法运行的伪代码。经过几轮踩坑后,我把需求打磨成了下面这个版本,并同步给了 Zcode 和 Codex。
复刻目标:
- 玩家角色在地图上自由移动,视角为俯视角;
- 地图上随机生成树木、浆果丛和怪物;
- 玩家靠近树木后,按空格砍树,获得木头;
- 玩家靠近浆果丛后,按空格采集,饥饿值恢复;
- 玩家饥饿值会随时间下降,饥饿值归零后生命值开始减少;
- 按 F 键攻击附近怪物,击败怪物后获得食物;
- 怪物会主动靠近并攻击玩家;
- 游戏进入夜间时视野范围缩小;
- 玩家生命值归零时,游戏结束。
为了让 AI 不陷入“自由发挥”状态,我还给它补充了一段约束:使用原生 JavaScript、Canvas、requestAnimationFrame、单 HTML 文件,不引入外部图片资源,所有图形使用 Canvas 基础图形绘制。
4.2 技术选型:为什么用 HTML + Canvas
很多读者可能会问:既然是复刻饥荒,为什么不用 Unity 或 Phaser?原因在于这次测试的核心目的是评估 AI 编程工具,而不是追求成品商业品质。HTML + Canvas 的方案具备几个不可替代的优势:
- 环境简单,浏览器直接打开即可运行;
- 生成物可视化反馈快,AI 改完代码后马上能刷新验证;
- 不需要安装大型游戏引擎,能让模型把注意力集中在逻辑而不是引擎 API 上;
- 一旦生成结果出现问题,可以很快在浏览器 DevTools 中定位到具体函数。
在实际业务中,如果你的项目同样需要快速验证某个玩法原型,这套技术组合也非常适合。
4.3 给 AI 的提示词怎么写
同一个模型,在“给我做饥荒”和下面这种结构化提示词下产生的项目质量差别巨大。我在测试中使用的核心提示词如下:
你是一名资深 HTML5 小游戏开发工程师。请使用原生 JavaScript + Canvas 实现一个“饥荒Like”生存小游戏 Demo: 1. 玩家角色使用 WASD 移动; 2. 地图随机生成树木、浆果丛和怪物; 3. 玩家靠近树木时按空格砍树,获得木头; 4. 玩家靠近浆果丛时按空格采集,恢复饥饿值; 5. 饥饿值随时间下降,饥饿值归零后生命值持续减少; 6. 按 F 攻击附近怪物,击败怪物获得食物; 7. 怪物会自动追击玩家,碰到玩家后造成伤害并进入攻击冷却; 8. 游戏具备白天黑夜循环,夜晚缩小视野; 9. 单 HTML 文件即可,无需外部资源,代码保留中文注释; 10. 请在画面左上角显示生命值、饥饿值、木头数量、食物数量。这段提示词的价值在于把“游戏系统”拆分成了“输入事件”和“系统规则”。项目一旦复杂,AI 编程工具很难自己补齐所有细节,所以我们要尽量把模糊的交互判定写成可验收的行为描述。
4.4 核心代码结果展示
下面这段代码是让 AI 完成需求后的一个整理版本。用文本编辑器保存为ds-survival-demo.html,浏览器打开即可运行。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>饥荒Like 生存 Demo</title> <style> * { margin: 0; padding: 0; } body { min-height: 100vh; background: #0e2612; display: flex; align-items: center; justify-content: center; } canvas { background: #315c2e; border: 2px solid #18331a; display: block; } p { color: #d8f0d8; font-family: 'Courier New', monospace; margin-top: 10px; text-align: center; } </style> </head> <body> <div> <canvas id="game" width="960" height="640"></canvas> <p>WASD移动 | 空格砍树/采集 | F攻击 | 活下去</p> </div> <script> const cv = document.getElementById("game"); const ctx = cv.getContext("2d"); const W = cv.width; const H = cv.height; const keys = {}; addEventListener("keydown", e => { keys[e.code] = true; e.preventDefault(); }); addEventListener("keyup", e => { keys[e.code] = false; }); const rand = (min, max) => Math.random() * (max - min) + min; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y); const player = { x: W / 2, y: H / 2, r: 16, speed: 170, hp: 100, hunger: 100, wood: 0, food: 0, attackCd: 0, actionCd: 0 }; let trees = []; let bushes = []; let monsters = []; let gameTime = 0; let gameOver = false; function createWorld() { trees = []; bushes = []; monsters = []; for (let i = 0; i < 7; i++) { trees.push({ x: rand(60, W - 60), y: rand(60, H - 60), hp: 3 }); } for (let i = 0; i < 12; i++) { bushes.push({ x: rand(60, W - 60), y: rand(60, H - 60), food: Math.floor(rand(2, 5)) }); } } function spawnMonster() { monsters.push({ x: rand(60, W - 60), y: rand(60, H - 60), hp: 4, speed: rand(50, 80), hitCd: 0 }); } function resetGame() { player.x = W / 2; player.y = H / 2; player.hp = 100; player.hunger = 100; player.wood = 0; player.food = 0; player.attackCd = 0; player.actionCd = 0; gameTime = 0; gameOver = false; createWorld(); for (let i = 0; i < 2; i++) { spawnMonster(); } } resetGame(); setInterval(() => { if (!gameOver && monsters.length < 3) { spawnMonster(); } }, 8000); function update(dt) { if (gameOver) return; let dx = 0; let dy = 0; if (keys["KeyW"] || keys["ArrowUp"]) dy -= 1; if (keys["KeyS"] || keys["ArrowDown"]) dy += 1; if (keys["KeyA"] || keys["ArrowLeft"]) dx -= 1; if (keys["KeyD"] || keys["ArrowRight"]) dx += 1; if (dx !== 0 || dy !== 0) { const len = Math.hypot(dx, dy); player.x += (dx / len) * player.speed * dt; player.y += (dy / len) * player.speed * dt; } player.x = clamp(player.x, player.r, W - player.r); player.y = clamp(player.y, player.r, H - player.r); player.actionCd -= dt; player.attackCd -= dt; player.hunger = Math.max(0, player.hunger - dt * 1.5); if (player.hunger <= 0) { player.hp = Math.max(0, player.hp - dt * 2); } // 空格砍树或采集 if (keys["Space"] && player.actionCd <= 0) { player.actionCd = 0.45; let minD = 70; let target = null; let targetType = ""; for (const tree of trees) { const d = distance(player, tree); if (d < minD) { minD = d; target = tree; targetType = "tree"; } } for (const bush of bushes) { const d = distance(player, bush); if (bush.food > 0 && d < minD) { minD = d; target = bush; targetType = "bush"; } } if (target) { if (targetType === "tree") { target.hp -= 1; if (target.hp <= 0) { player.wood += 1; trees = trees.filter(t => t !== target); } } else if (targetType === "bush") { target.food -= 1; player.hunger = Math.min(100, player.hunger + 12); player.food += 1; } } } // F 攻击 if (keys["KeyF"] && player.attackCd <= 0) { player.attackCd = 0.35; let nearest = null; let nearestD = 80; for (const monster of monsters) { const d = distance(player, monster); if (d < nearestD) { nearestD = d; nearest = monster; } } if (nearest) { nearest.hp -= 2; if (nearest.hp <= 0) { player.food += 2; monsters = monsters.filter(m => m !== nearest); } } } // 怪物追击与攻击 for (const monster of monsters) { const d = distance(player, monster); if (d > 1) { const step = monster.speed * dt; monster.x += (player.x - monster.x) / d * step; monster.y += (player.y - monster.y) / d * step; } monster.hitCd -= dt; if (d < 34 && monster.hitCd <= 0) { player.hp = Math.max(0, player.hp - 6); monster.hitCd = 1.2; } } // 浆果缓慢刷新 for (const bush of bushes) { if (bush.food < 5 && Math.random() < dt * 0.05) { bush.food += 1; } } if (player.hp <= 0) { gameOver = true; } gameTime += dt; } function drawPlayer() { ctx.save(); ctx.beginPath(); ctx.arc(player.x, player.y, player.r, 0, Math.PI * 2); ctx.fillStyle = "#f1c27d"; ctx.fill(); ctx.strokeStyle = "#7a4b1a"; ctx.lineWidth = 2; ctx.stroke(); ctx.beginPath(); ctx.arc(player.x - 5, player.y - 3, 3, 0, Math.PI * 2); ctx.fillStyle = "#222"; ctx.fill(); ctx.beginPath(); ctx.arc(player.x + 5, player.y - 3, 3, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawTree(tree) { ctx.fillStyle = "#5d3a1a"; ctx.fillRect(tree.x - 6, tree.y - 4, 12, 28); ctx.fillStyle = tree.hp > 1 ? "#2d7a3a" : "#b3d65f"; ctx.beginPath(); ctx.arc(tree.x, tree.y + 4, 24, 0, Math.PI * 2); ctx.fill(); } function drawBush(bush) { ctx.fillStyle = "#3e7f3e"; ctx.beginPath(); ctx.ellipse(bush.x, bush.y, 18, 12, 0, 0, Math.PI * 2); ctx.fill(); if (bush.food > 0) { ctx.fillStyle = "#e63946"; for (let i = 0; i < Math.min(bush.food, 4); i++) { ctx.beginPath(); ctx.arc(bush.x - 8 + i * 6, bush.y - 4, 4, 0, Math.PI * 2); ctx.fill(); } } } function drawMonster(monster) { ctx.fillStyle = "#3d2b1f"; ctx.beginPath(); ctx.arc(monster.x, monster.y, 20, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#c1121f"; ctx.beginPath(); ctx.arc(monster.x - 6, monster.y - 4, 5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(monster.x + 6, monster.y - 4, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#222"; ctx.beginPath(); ctx.arc(monster.x - 6, monster.y - 4, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(monster.x + 6, monster.y - 4, 2, 0, Math.PI * 2); ctx.fill(); } function drawHUD() { ctx.fillStyle = "rgba(0,0,0,0.5)"; ctx.fillRect(8, 8, 280, 90); ctx.font = "14px 'Courier New', monospace"; ctx.fillStyle = "#fff"; ctx.fillText("生命: " + Math.max(0, Math.floor(player.hp)), 20, 34); ctx.fillText("饥饿: " + Math.max(0, Math.floor(player.hunger)), 20, 58); ctx.fillText("木头: " + player.wood + " 食物: " + player.food, 20, 82); } function render() { ctx.clearRect(0, 0, W, H); // 简单地面网格 ctx.strokeStyle = "rgba(200,230,200,0.25)"; ctx.lineWidth = 1; for (let i = 0; i < W; i += 40) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, H); ctx.stroke(); } for (let i = 0; i < H; i += 40) { ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(W, i); ctx.stroke(); } for (const bush of bushes) drawBush(bush); for (const tree of trees) drawTree(tree); for (const monster of monsters) drawMonster(monster); drawPlayer(); drawHUD(); // 昼夜循环:每 40 秒一个周期,后 30% 时间进入夜晚 const cycle = gameTime % 40 / 40; const isNight = cycle > 0.7; if (isNight && !gameOver) { const nightProgress = (cycle - 0.7) / 0.3; const gradient = ctx.createRadialGradient( player.x, player.y, 40, player.x, player.y, 150 + nightProgress * 100 ); gradient.addColorStop(0, "rgba(2, 6, 20, 0)"); gradient.addColorStop(1, "rgba(2, 6, 20, 0.88)"); ctx.fillStyle = gradient; ctx.fillRect(0, 0, W, H); } if (gameOver) { ctx.fillStyle = "rgba(0,0,0,0.72)"; ctx.fillRect(0, 0, W, H); ctx.fillStyle = "#e63946"; ctx.font = "36px 'Courier New', monospace"; ctx.textAlign = "center"; ctx.fillText("游戏结束", W / 2, H / 2 - 10); ctx.fillStyle = "#d8f0d8"; ctx.font = "16px 'Courier New', monospace"; ctx.fillText("刷新页面重新开始", W / 2, H / 2 + 30); ctx.textAlign = "left"; } } let lastTime = performance.now(); function frame(now) { let dt = (now - lastTime) / 1000; dt = Math.min(dt, 0.05); lastTime = now; update(dt); render(); requestAnimationFrame(frame); } requestAnimationFrame(frame); </script> </body> </html>代码结构并不复杂。update(dt)中先处理玩家移动和状态衰减,再处理空格交互、F 攻击和怪物逻辑;render()中绘制地面、资源、敌人、HUD 和夜晚遮罩;整体采用固定的 requestAnimationFrame 循环,因此受刷新率影响较小。
这段代码的关键在于,生成工具必须理解“攻击冷却”和“采集冷却”是两类不同的冷却,否则会出现按住空格后饥饿值瞬间回满的问题。这也是评估模型逻辑是否细致的一个小考点。
4.5 运行验证
将上面的代码保存为ds-survival-demo.html,然后用浏览器直接打开。更推荐在项目目录下启动一个本地静态服务,以避免后续扩展模块时出现文件协议限制:
npx serve .运行后应满足以下默认状态:
- 玩家出生在画面中央,可使用 WASD 移动;
- 画面中存在绿色树木、浆果丛和深色怪物;
- 按住空格,在树旁可砍树并获得木头,在浆果丛旁可采集食物;
- 饥饿值随时间降低,降到 0 后生命值开始下降;
- 点击 F 可攻击怪物,怪物靠近会造成伤害;
- 运行一段时间后画面进入夜晚视野缩小状态;
- 生命值归零后显示“游戏结束”。
5. 实测记录:Codex vs Zcode + DeepSeek
5.1 测试方法与评分规则
我在同一台机器上,使用相同版本的需求提示词,分别完成两次测试。
第一次在 Zcode 中接入 DeepSeek 系列模型,第二次使用 Codex 官方默认模型配置。评分维度设为:
- 首次生成成功率:一次生成后页面能否正常运行,不出现脚本报错;
- 需求覆盖率:是否完成砍树、采集、饥饿、怪物、昼夜循环等核心需求;
- 多轮修改能力:当我提出“攻击频率太高”“饥饿消耗太快”“夜晚不够暗”等调整需求时,工具能否准确定位代码位置并修改,而不是把整个文件重写;
- 上下文一致性:经过多轮修改后,是否还记得前面的功能,不出现“修好战斗,却忘记饥饿系统”的情况;
- 使用流畅度:安装、配置、切换模型、查看 diff 等操作过程是否顺畅。
5.2 对比结果
下表是个人测试结果,不代表所有项目场景结论:
| 对比维度 | Zcode + DeepSeek 系列 | Codex 默认模型 |
|---|---|---|
| 首次生成成功率 | 较高,能一次给出可运行页面 | 较高,但依赖提示词是否清晰 |
| 需求覆盖率 | 核心功能基本覆盖 | 对复杂系统理解较好 |
| 多轮修改能力 | 长上下文在连续改需求时表现稳定 | 修改精度高,但在超长会话中仍需主动收敛 |
| 上下文一致性 | 只要工作区路径正确,遗忘较少 | 能跟踪文件内容,但建议每轮提供“当前故障现象” |
| 安装门槛 | 图形界面配置相对直观 | CLI 风格偏工程师习惯,需要理解 config.toml |
| 扩展自定义模型 | 支持自定义 Provider,配置路径清晰 | 需要手动改配置,报错后要自行排查 Provider |
需要特别说明的是:这次测试的对比主体并不是严苛的“谁打几分”,而是不同工具和模型组合的适用场景。如果你只需要会读代码、补单元测试,两者都够用;如果你要在没有图形界面的服务器上持续执行长时间重构任务,Codex 的命令行交互可能更省事;如果你希望反复修改同一个功能需求,Zcode 的长上下文会减少“前面代码被忘掉”的频率。
5.3 两种工具暴露出的不同问题
Codex 侧的问题
Codex 在首次生成阶段对代码结构和变量命名比较讲究,但有时候会“过度设计”。比如面对“复刻饥荒小游戏”这种需求,它可能会生成一个包含engine.js、entities.js、scenes.js的多目录工程。这对大项目是优点,但对只想快速验证玩法的 Demo 却是浪费。
另外,Codex 接入自定义模型时可能遇到的报错更多集中在协议兼容层面:
cc switch local proxy failed while handling codex endpoint /responses. provider ...这类报错通常不是模型能力问题,而是本地代理工具或第三方 Provider 并不支持 Codex 所使用的/responses端点。如果你是通过本地代理服务切换模型端点,需要核对 Provider 的接口规范是chat还是responses格式。
Zcode + DeepSeek 侧的问题
Zcode 图形界面让配置过程更平滑,但 AI 在生成逻辑时偶尔会把“数据结构”和“渲染代码”耦合在一起,导致后期扩展困难。比如做完战斗系统后,想让怪物有不同的移动速度,它可能把所有怪物类型写死在 update 函数里,而不是抽取成一个对象结构。
同时,如果一次需求描述得太复杂,DeepSeek 模型也会出现“看起来逻辑完整,但漏掉了攻击冷却初始值”之类的小问题。这不是模型不会写代码,而是用户提示词里没有明确规定状态边界。针对这种问题,模型自己的修复思路往往是给每个变量强行加一个“防御性初始化”,这时候开发者的作用反而更加重要。
5.4 回到标题:夯爆了还是拉完了
从结果来看,两者都没有到“夯爆了”或者“拉完了”的极端状态。
在“饥荒小游戏复刻”这个任务里,无论 Zcode 接 DeepSeek 还是 Codex 默认模型,只要能正确理解需求列表,都能生成一个可运行的原型。真正的差异在三轮以上修改后开始显现:Zcode 的长上下文让我在连续改动“采集冷却”“怪物速度”时少了很多解释成本;Codex 对文件级修改的感知很稳,但自定义模型接入时的报错需要开发者对协议有基础认知。
如果把问题放大到真实工程,最理性的观点是:编程工具优化的是“写代码的组织效率”,不能完全替代“判断需求是否合理”的思考。拿这次复