GPT-Astra 不是传统意义上的建模软件,而是一类“一次生成可探索科幻飞船”的生成式场景项目。核心链路很短:用户输入一段描述,例如“一艘用于深空科考的飞船,冷色调,包含舰桥、宿舍区、引擎室,走廊连接”,大语言模型在一次回复里输出一整份结构化的飞船场景描述;渲染端读取这份描述,把地板、墙体、门洞、控制台、全息屏幕、灯光和碰撞体全部构建出来。玩家进入场景后可以第一人称视角行走,在舰桥查看控制台,沿着走廊进入引擎室。
这里最值得拆开的技术点是“结构数据 + 确定性构建”的分离:模型负责布局语义,代码负责几何与交互。手工搭建一条科幻走廊可能要花几个小时,而这种方案把建模过程中可参数化的部分变成规则,模型只需要输出房间尺寸、位置、门洞方向和道具清单。下面用最小可运行项目把这条链路走通,技术栈使用 Node.js、Three.js 和 OpenAI Chat Completions 接口。跑完后你会得到一条完整的“文字描述 -> 场景 JSON -> 可探索 3D 飞船”流水线,并知道每一步出现问题时该查哪里。
1. 先把“生成可探索飞船”拆成三个核心问题
1.1 场景描述、几何构建、交互探索要互相解耦
一个可复现的生成式 3D 管线,不能把“写描述”和“画几何”混在一起。大模型擅长写文案、列清单、安排房间关系,但直接让它输出二进制的 glTF 或精细网格并不稳定。反过来,让代码凭空理解“科幻引擎室”是什么样,能力又不够。所以要把流程拆成三层:
- 场景描述层:定义飞船有哪些房间、房间的尺寸位置、门开在哪面墙、放哪些道具、用什么配色和光照。
- 几何构建层:用一段确定性的 Three.js 代码,把场景描述转换成真实可渲染的 Mesh。
- 交互探索层:给玩家第一人称相机、碰撞检测和基本移动规则,让玩家真的“走进”这艘船。
分层之后,每一层都可以单独验证。生成结果不对,去查 Prompt 和 JSON 校验;构建结果不对,去查 Builder 和坐标逻辑;交互手感不对,去查相机和碰撞体。GPT-Astra 这类项目能稳定落地,不是因为模型本身多聪明,而是因为模型只负责它擅长的部分,其余部分由确定代码兜底。
1.2 为什么“一次生成”的最终产物是 JSON,而不是模型文件
“一次生成”听起来像一句提示词直接产出可导入 Blender 的模型,实际工程里并不是这样。更稳的做法是让模型输出一份 JSON 场景描述。JSON 有几个明显优势:
- 可校验:用 JSON Schema 或手写校验函数,能把结构错误挡在渲染之前。
- 可版本化:场景结构变化时可以做 diff,甚至可以保留多个版本。
- 可回退:如果某一条生成结果不合格,可以把 JSON 回滚到上一份成功结果。
- 可读性好:房间、门洞、彩色值都肉眼可查,调试成本低。
在 GPT-Astra 的链路里,“一次生成”通常指:用户给一条完整需求后,模型一次性返回一份完整的布局方案,而不是多轮对话逐步追加房间。渲染端拿到 JSON 再构建场景,因此模型是否一次输出成功,主要取决于 Prompt 和响应格式约束。
1.3 “可探索”不是加一个相机,而是加一套规则
很多 3D 示例项目做成“转盘效果”:鼠标拖动视角绕模型旋转。那不算可探索。真正的可探索要求玩家位置在飞船内部,视角受房间约束,移动时会撞墙而不是穿模。
要达成这个效果,至少要有四件事:
- 房间高度:天花板不能低于玩家身高太多,否则视野压迫感强。
- 门洞宽度:门洞至少要比玩家半径大,否则走不过去。
- 碰撞集合:所有墙体、道具都登记为碰撞体。
- 相机控制:使用 Pointer Lock 或类似方案,让鼠标控制视角,键盘控制位移。
仅这一小节就能看出,GPT-Astra 的关键不是“像不像飞船”,而是“能不能走通”。这也是它和普通 AI 生图项目的本质区别。
2. 最小可运行项目:环境、目录与依赖
2.1 环境要求
在开始实现前,先确认环境满足下表要求。
| 项目 | 建议要求 | 说明 |
|---|---|---|
| Node.js | 18 及以上 | 使用 Express 和 dotenv 的基础要求 |
| npm | 9 及以上 | 安装依赖即可,版本不用刻意追新 |
| OpenAI API Key | 有可用额度 | 只放在服务端环境变量,不要写进前端代码 |
| 浏览器 | 支持 WebGL 和 Pointer Lock API | Chrome、Edge、Firefox 均可 |
如果当前环境没有 Node.js,先安装 LTS 版本。下面示例使用的 Three.js 版本以npm install后 lock 文件里的实际版本为准,代码按 r150 之后的 API 习惯编写,基本不用改动。
2.2 项目结构和初始化命令
这里用一个轻量的 Express 服务同时承担 API 和前端静态文件,减少额外配置。项目结构如下:
gpt-astra-demo/ ├─ .env ├─ package.json ├─ server/ │ ├─ index.js │ └─ generate.js └─ public/ ├─ index.html ├─ main.js └─ builder.js初始化命令:
mkdir gpt-astra-demo cd gpt-astra-demo npm init -y npm install express openai dotenv npm install three.env文件内容:
OPENAI_API_KEY=sk-你的密钥 PORT=3000注意:.env必须加入.gitignore,任何情况下都不能提交到仓库。
2.3 为什么用 Express 做前端静态服务
前端直接访问 OpenAI 接口最省事,但会把 API Key 暴露给浏览器。GPT-Astra 这类生成式项目应该让浏览器只请求自己的后端,由后端持有密钥并转发请求。
因此这里用 Express 做两件事:
- 提供
/api/generate接口,接收前端文字描述。 - 提供静态文件和 Three.js 构建产物。
浏览器页面里通过页面根路径访问main.js、builder.js,通过/vendor/访问node_modules/three下的模块文件。这种服务方式适合 Demo;如果是正式项目,前端建议用 Vite 打包,后端独立部署,本文最后会展开说明。
3. 场景 Schema:给大模型一张严格的“图纸模板”
3.1 字段设计的目标
如果让模型自由发挥,它会输出各种奇怪格式,渲染端很难统一处理。所以第一步是定义一份最小的场景 Schema,把允许出现的字段、单位、取值约束全部固定下来。
Schema 需要回答这几个问题:
- 飞船整体叫什么名字,用什么配色。
- 有哪些房间,每个房间的位置、尺寸、墙壁颜色、地板颜色。
- 每个房间有哪些门洞,门洞开在哪面墙,宽度多少。
- 每个房间有哪些道具,道具类型、位置、朝向。
字段越少越好,先保证能跑通,再逐步加纹理、动画和特效。
3.2 一个最小的飞船场景 JSON
下面是一份符合 GPT-Astra 最小 Schema 的示例输出:
{ "schemaVersion": "1.0", "shipName": "Astra-7", "palette": { "floor": "#1c2230", "wall": "#33415c", "accent": "#00d4ff" }, "rooms": [ { "id": "bridge", "position": [0, 0, 0], "size": [10, 3, 6], "floorColor": "#1c2230", "wallColor": "#33415c", "doors": [ { "wall": "south", "width": 2 } ], "props": [ { "type": "console", "position": [-2, 0, -1.8], "rotation": 0 }, { "type": "screen", "position": [-4.2, 1.1, 0], "rotation": 1.5708 } ] }, { "id": "corridor", "position": [0, 0, -5], "size": [4, 3, 4], "floorColor": "#181d29", "wallColor": "#2a3550", "doors": [ { "wall": "north", "width": 2 }, { "wall": "south", "width": 1.5 } ], "props": [] } ] }在这个示例里,bridge 的中心在[0, 0, 0],尺寸[10, 3, 6]表示宽、高、深分别为 10、3、6。corridor 的中心在[0, 0, -5],尺寸[4, 3, 4],它的 north 面对应 bridge 的 south 面,两扇门宽度一致,玩家才能从舰桥走进走廊。
3.3 坐标系和单位约定
所有生成结果都要遵循统一约定,否则构建端无法计算墙体位置。
- y 轴向上,房间高度取
size[1]。 position表示房间中心点坐标,不是墙角坐标。- 1 个单位等于 1 米。
- 方向约定:north 指向 -Z,south 指向 +Z,east 指向 +X,west 指向 -X。
这些约定要写进 Prompt,也要写进渲染代码。位置和方向一旦混用,门洞就会开到错误墙面,走廊出现断头路。
4. 让 LLM 稳定输出可构建的 JSON
4.1 System Prompt 设计:把约束写进角色和规则
模型不会自动理解你的坐标系,必须把规则写清楚。System Prompt 建议包含以下内容:
- 角色:你是一个科幻飞船场景设计师。
- 输出格式:只输出 JSON,不输出任何解释文本。
- Schema 定义:字段含义和单位。
- 坐标约定:north = -Z,position 是房间中心。
- 规则限制:每个房间至少有 1 个门洞,门洞宽度在 1.2 到 3 之间。
- 道具类型白名单:
console、screen、chair、light、container。
一个示意 Prompt 如下:
你是科幻飞船场景设计师。用户会描述飞船需求,你必须输出严格 JSON,禁止输出 JSON 之外的文字。 JSON 字段规则: - schemaVersion: 固定 "1.0" - shipName: 字符串 - palette: 包含 floor, wall, accent 三个颜色 - rooms: 房间数组,每项包含 id, position, size, floorColor, wallColor, doors, props - position 是房间中心,格式 [x, y, z],y 固定为 0 - size 是 [宽, 高, 深] - 坐标系:north 指向 -Z,south 指向 +Z,east 指向 +X,west 指向 -X - doors 的 wall 只能是 north/south/east/west,width 在 1.2 到 3 之间 - props 的 type 只能是 console/screen/chair/light/container - 所有房间必须通过门洞连通,不能出现孤立房间 - 房间之间不能重叠 用户描述:{用户输入}关键点是“禁止输出 JSON 之外的文字”。很多生成失败发生在模型多输出了一行注释或 Markdown 代码块标记,导致 JSON.parse 直接报错。
4.2 请求参数:温度、响应格式、超时
调用 Chat Completions 接口时,参数对稳定性的影响很大。
| 参数 | 建议值 | 说明 |
|---|---|---|
| model | gpt-4o-mini 或项目可用模型 | 成本低,足够完成结构生成 |
| temperature | 0.3 到 0.5 | 温度太低像模板,太高容易偏离约束 |
| response_format | { "type": "json_object" } | 强制模型输出合法 JSON 对象 |
| timeout | 30 秒以上 | 场景复杂时生成耗时较长 |
temperature不建议设为 0。即使设为 0,模型也不是绝对确定,尤其在 JSON 对象内部字段顺序上仍可能变化。response_format只能保证 JSON 语法合法,不能保证房间连通或门洞宽度合理,这些要靠 Prompt 和校验层兜底。
4.3 后端生成函数与失败兜底
server/generate.js的核心代码如下:
require("dotenv").config(); const { OpenAI } = require("openai"); const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, timeout: 30 * 1000, }); const systemPrompt = `你是科幻飞船场景设计师。...(完整 Prompt 见上文)...`; async function generateShip(description) { const response = await client.chat.completions.create({ model: "gpt-4o-mini", temperature: 0.4, response_format: { type: "json_object" }, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: description }, ], }); const content = response.choices[0].message.content ?? ""; const parsed = JSON.parse(content); if (!Array.isArray(parsed.rooms) || parsed.rooms.length === 0) { throw new Error("生成结果缺少 rooms 数组"); } return parsed; } module.exports = { generateShip };这里做了两层防护:第一层用response_format保证语法合法,第二层在解析后检查最基础的字段是否存在。更完整的项目还应该校验每个 room 的 id 是否重复、门洞墙名是否在白名单内、房间是否重叠。这些校验可以放在一个独立的validate.js文件里,避免把所有逻辑堆在生成函数中。
server/index.js提供一个/api/generate路由:
const express = require("express"); const path = require("path"); const { generateShip } = require("./generate"); const app = express(); app.use(express.json()); app.use(express.static("public")); app.use("/vendor", express.static(path.join(__dirname, "../node_modules/three/build"))); app.use("/vendor/addons", express.static(path.join(__dirname, "../node_modules/three/examples/jsm"))); app.post("/api/generate", async (req, res) => { const { description } = req.body; if (!description || description.trim().length < 4) { return res.status(400).json({ error: "description 太短" }); } try { const ship = await generateShip(description); res.json(ship); } catch (err) { console.error(err); res.status(500).json({ error: err.message }); } }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`GPT-Astra demo listening on http://localhost:${port}`); });注意:不要把
process.env.OPENAI_API_KEY打印到日志里。一旦日志泄露密钥,需要立刻在控制台吊销并重新生成。
5. Three.js 构建可探索飞船
5.1 渲染器、相机和 PointerLockControls
前端入口public/main.js负责初始化渲染器、场景、相机,并处理一次生成请求:
import * as THREE from "three"; import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; import { buildShip } from "./builder.js"; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 100); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); scene.add(new THREE.HemisphereLight(0xffffff, 0x33415c, 1.2)); const controls = new PointerLockControls(camera, document.body); document.getElementById("startBtn").addEventListener("click", () => { controls.lock(); }); function move() { const speed = 0.06; if (!controls.isLocked) return; if (keys.has("KeyW")) camera.position.z -= speed; if (keys.has("KeyS")) camera.position.z += speed; if (keys.has("KeyA")) camera.position.x -= speed; if (keys.has("KeyD")) camera.position.x += speed; } setInterval(move, 16); renderer.setAnimationLoop(() => renderer.render(scene, camera));这里的移动逻辑是最简写法,实际项目需要把移动向量根据相机朝向换算,并加入 delta 时间,否则不同帧率下手感不一致。重点是PointerLockControls让鼠标可以旋转视角,controls.isLocked保证只在玩家点击开始后才响应键盘。
5.2 把 JSON 房间变成几何体:墙体、地板、门洞
public/builder.js是整个构建层的核心。它读取 JSON 里的房间数据,生成地板、天花板、墙体和门洞两侧的短墙。
import * as THREE from "three"; function createBox(width, height, depth, color) { const mesh = new THREE.Mesh( new THREE.BoxGeometry(width, height, depth), new THREE.MeshStandardMaterial({ color }) ); mesh.castShadow = true; mesh.receiveShadow = true; return mesh; } function addWallWithDoor(room, side, door) { const h = room.size[1]; const group = new THREE.Group(); if (side === "north" || side === "south") { const wallWidth = room.size[0]; const segWidth = (wallWidth - door.width) / 2; if (segWidth < 0.1) return group; [-1, 1].forEach((sign) => { const x = room.position[0] + sign * (door.width / 2 + segWidth / 2); const wall = createBox(segWidth, h, 0.2, room.wallColor); wall.position.set(x, h / 2, side === "north" ? room.position[2] - room.size[2] / 2 : room.position[2] + room.size[2] / 2 ); group.add(wall); }); } else { const wallDepth = room.size[2]; const segWidth = (wallDepth - door.width) / 2; if (segWidth < 0.1) return group; [-1, 1].forEach((sign) => { const z = room.position[2] + sign * (door.width / 2 + segWidth / 2); const wall = createBox(0.2, h, segWidth, room.wallColor); wall.position.set( side === "east" ? room.position[0] + room.size[0] / 2 : room.position[0] - room.size[0] / 2, h / 2, z ); group.add(wall); }); } return group; } export function buildShip(ship) { const root = new THREE.Group(); for (const room of ship.rooms) { const [x, y, z] = room.position; const [w, h, d] = room.size; const floor = createBox(w, 0.2, d, room.floorColor); floor.position.set(x, 0, z); root.add(floor); const ceil = createBox(w, 0.2, d, room.wallColor); ceil.position.set(x, h, z); root.add(ceil); ["north", "south", "east", "west"].forEach((side) => { const hasDoor = room.doors.filter((d) => d.wall === side); if (hasDoor.length > 0) { hasDoor.forEach((door) => root.add(addWallWithDoor(room, side, door))); } else { const wall = buildSolidWall(room, side); root.add(wall); } }); (room.props || []).forEach((prop) => root.add(createProp(prop))); } return root; }createBox使用BoxGeometry创建墙体和地板。门洞处理的关键在于:有门的墙面不是一整面墙,而是分成左右两段短墙,中间空出门的宽度。这样玩家既能在视觉上看到门洞,又能在碰撞体上真正通过。
5.3 道具、灯光和碰撞体
道具生成逻辑要根据prop.type分发:
function createProp(prop) { const group = new THREE.Group(); switch (prop.type) { case "console": group.add(createBox(2, 0.9, 0.8, 0x222831)); group.add(createBox(1.8, 0.1, 0.6, 0x00d4ff)); break; case "screen": { const screen = createBox(3, 1.8, 0.15, 0x001f2f); group.add(screen); break; } case "chair": group.add(createBox(0.6, 0.6, 0.6, 0x303030)); break; case "light": group.add(createBox(0.4, 0.1, 1.2, 0xffe9a8)); break; default: break; } group.position.set(prop.position[0], prop.position[1], prop.position[2]); if (prop.rotation) group.rotation.y = prop.rotation; return group; }碰撞体是所有 Mesh 包围盒的集合:
export function collectColliders(root) { const boxes = []; root.traverse((obj) => { if (obj.isMesh) { const box = new THREE.Box3().setFromObject(obj); boxes.push(box); } }); return boxes; } export function collides(pos, radius, boxes) { for (const box of boxes) { const dx = Math.max(box.min.x - pos.x, 0, pos.x - box.max.x); const dz = Math.max(box.min.z - pos.z, 0, pos.z - box.max.z); if (dx * dx + dz * dz < radius * radius) return true; } return false; }在键盘移动逻辑里,计算目标位置后先调用collides,如果碰撞则取消该方向位移,这样玩家会被墙体挡住而不是穿过去。
注意:这是一个简化碰撞方案。房间数量多、道具复杂时,建议使用 Cannon-es 或 Rapier 物理引擎,或预烘焙导航网格,否则逐帧遍历所有 Mesh 包围盒会产生不必要的性能损耗。
6. 运行验证:从浏览器走一遍完整流程
6.1 启动服务并取回生成结果
在项目根目录执行:
node server/index.js输出:
GPT-Astra demo listening on http://localhost:3000浏览器打开http://localhost:3000,在文本框输入“一艘科考飞船,包含舰桥、宿舍区和引擎室,冷色调,有全息星图屏幕”,点击生成。在浏览器开发者工具 Network 面板可以看到一个 POST 请求/api/generate,响应体是 JSON 场景数据。
也可以直接用 curl 测试后端:
curl -X POST http://localhost:3000/api/generate \ -H "Content-Type: application/json" \ -d '{"description":"一艘小型科考飞船,舰桥连接走廊,走廊通向引擎室"}'正常返回的结果包含schemaVersion、shipName、palette、rooms四个顶层字段。
6.2 在 3D 场景里验证可探索性
生成成功后,页面会自动把 JSON 交给buildShip构建场景。点击“开始探索”锁定鼠标,使用 WASD 行走。
可以按下面的清单逐项验证:
| 检查项 | 预期结果 | 失败时的现象 |
|---|---|---|
| 房间结构 | 能看到地板、天花板、四面墙 | 只看到低多边形网格,缺墙或重叠 |
| 门洞 | 能从一个房间走入门洞进入下一个房间 | 门洞被墙堵住或宽度过窄 |
| 碰撞 | 走到墙边会被挡住 | 玩家穿模进入墙体内部 |
| 道具 | 舰桥有控制台、屏幕,引擎室有设备和灯 | 道具位置漂移到墙外 |
| 光照 | 室内整体清晰,不会全黑或过曝 | 场景太暗或强光刺眼 |
| 配色 | 接近用户要求的冷色调 | 颜色随机,与描述不符 |
6.3 生成 JSON 与渲染结果的对照
这一步最容易发现“模型生成得对,但渲染代码没读懂”的问题。把 Network 面板里的 JSON 复制下来,和 3D 场景逐条对照。例如 JSON 说 bridge 的position: [0, 0, 0],那么在场景里桥的中心应该落在原点附近;JSON 说 corridor 的 north 门宽 2,那么场景里走廊北侧应该有一个 2 米宽的门洞。
如果 JSON 正确但场景错误,问题在构建层;如果 JSON 本身房间重叠或门洞开错方向,问题在 Prompt 和校验层。两者不要混在一起排查。
7. 常见问题排查:分开查生成层、构建层和交互层
7.1 生成层:JSON 解析失败、房间重叠、门洞开错墙
JSON 解析失败是最常见的现象。可能原因包括模型输出了 Markdown 代码块、尾部多了一个逗号、字段值不是合法数字。检查方式是在生成函数里打印response.choices[0].message.content原始内容,用 JSON.parse 定位错误位置。解决方式是把响应格式约束和“只输出 JSON”写进 Prompt,并视情况重试一次。
房间重叠通常发生在模型没有做碰撞计算时。生成结果里两个房间的包围盒相交,渲染端会出现墙体穿插。建议在校验层计算每对房间的 AABB 重叠,一旦超过阈值就触发重试。门洞开错墙则说明坐标系约定没有被模型理解,需要在 Prompt 里把方向规则写在更显眼的位置,并在校验时检查相邻房间的门洞是否对齐。
7.2 构建层:墙错位、门洞被堵、道具加载失败
墙错位多是因为把position当成了墙角,而代码里按中心点计算。排查时先在 Builder 里打印房间中心和四墙位置,和 JSON 数值逐一对比。门洞被堵通常是门洞所在墙面同时生成了整墙和短墙,要去掉整墙分支,只保留有门洞的短墙。道具加载失败一般是prop.type不在白名单内,Builder 走到default分支直接跳过,校验层需要提前拦截未知类型。
7.3 交互层:穿模、卡死、性能差、灯光过暗
穿模有三个常见原因:碰撞体没有收集全、门洞两侧的短墙没加 Mesh 所以没有包围盒、移动步长过大导致角色在高速时跳过了薄墙。解决方案是每帧移动时把位移拆成多个小步长,并让碰撞判断使用预期位置而不是当前位置。
相机卡死可能是因为 PointerLock 没有获得锁定,也可能因为出生点落在碰撞盒内部。更好的做法是生成后把玩家放在第一个房间中心偏安全区域,并避开道具。性能差主要来自碰撞检测遍历所有 Mesh 包围盒,优化方向是离线把碰撞盒烘焙成静态数组,运行时只检查附近的几十个盒。灯光过暗通常是半球光强度不足,或者场景没有添加环境贴图,可以先用HemisphereLight提高系数。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| JSON 解析失败 | 模型输出了非 JSON 文本 | 打印原始 content | 强化 Prompt 并使用 response_format |
| 房间重叠 | 模型没做包围盒计算 | 校验 AABB 重叠量 | 校验失败自动重试 |
| 门洞方向错 | 坐标系约定没被遵守 | 对照相邻房间坐标 | Prompt 中强调方向映射 |
| 玩家穿模 | 碰撞体不完整或步长过大 | 打印碰撞盒列表 | 拆分移动步长并补齐碰撞盒 |
| 场景过暗 | 光照强度不足 | 调整 HemisphereLight 参数 | 增加到 1.2 到 2.0 之间 |
8. 从 Demo 到生产:最佳实践与扩展方向
8.1 落地前必改的安全与稳定性清单
把示例推到生产环境前,建议按下面的清单逐项检查:
- API Key 只存在于服务端环境变量,前端永远不接触。
- 为
/api/generate增加限流,避免单用户高频调用导致费用失控。 - 对生成结果做严格校验,至少检查字段类型、枚举值、房间连通性。
- 增加超时和重试,捕获超时后返回可读错误,而不是让请求一直悬挂。
- 记录生成日志,但日志中不要包含完整 Prompt 里的业务敏感信息,也不要包含 API Key。
- 增加缓存,用用户输入文本的哈希作为 key,重复需求直接返回缓存场景。
- 前端增加错误提示,失败时显示“生成失败,请重试”而不是白屏。
8.2 让生成更可控的工程策略
从示例升级到正式项目,最值得做的改造是引入 JSON Schema 校验工具,例如 zod 或 ajv。这样字段结构、枚举值、范围都能在代码层强制校验,避免模型偶然输出不合规数据。其次建议为场景描述增加版本号,未来扩展房间类型时,旧场景仍然能被旧 Builder 读取。
Prompt 也可以做成模板 + 约束片段管理。把坐标系、门洞规则、道具白名单拆成可组合片段,不同项目复用。每次修改 Prompt 后,最好准备一组固定测试用例,跑一遍回归,观察输出分布是否变化。
8.3 扩展方向
GPT-Astra 的最小闭环可以往多个方向扩展:
- 生成更大尺度:从单一飞船扩展到空间站,多个模块拼接,玩家通过气闸舱切换区域。
- 生成更细纹理:让模型输出墙面颜色、材质关键词,代码端映射到 PBR 材质。
- 加入物理引擎:使用 Cannon-es 或 Rapier,处理复杂碰撞、重力、可开门物体。
- 多人探索:把场景 JSON 作为同步数据源,浏览器端各自构建同一份场景,再叠加玩家坐标同步。
- 流式加载:大型场景按区域拆分,玩家接近时才构建该区域,降低首屏压力。
对新手来说,不要一上来就追求照片级画面。先把“文字生成 JSON、JSON 构建房间、玩家能走通门洞”这条主线练熟,再考虑材质、动画和多房间复杂度。GPT-Astra 这类项目真正的难点从来不是单个模型有多聪明,而是生成结果与确定性构建之间能否稳定衔接。把这条衔接整理成 Schema、Prompt、校验、渲染四层,后面的扩展都会顺畅很多。