plan.md:按用户口径重写 —— 粗/细渲染分工、抽象模型(参考 mineflayer)、机位降为用例

This commit is contained in:
JianFeeeee
2026-09-26 10:32:22 +08:00
parent c5560765a3
commit 7c7297ebad

263
plan.md
View File

@ -28,6 +28,11 @@
## 1. 用户设想的场景(2026-09-26,本计划的新方向)
★ **这是抽象模型的一个用例,不是全部**(用户 2026-09-26:
「机位这个只是一个具体应用场景,而不是只有这一个场景」)。
它用来验证 §1b 那套「状态 + 事件 + 操作」是否立得住 ——
若拍照能跑通,agent 的日常感知/交互也就能跑通。
> 我设想的场景是玩家可以在聊天框给 agent 发消息,然后比如让 agent 给他**拍照**,
> 如果是他和某个景物的**合照**,agent 可以先通过 **ass 粗渲染**,找好角度
> 再去**渲染实图**。
@ -38,48 +43,127 @@
① 玩家在聊天框发消息 → agent 通过统一接口收到
② agent 要"给玩家拍合照" → 需要:玩家位置/朝向 + 景物位置 + 相机可摆放范围
③ 选角度 → 用 **ass 粗渲染**(低成本、快)试若干机位,
挑一个好的,再用**真渲染**出图
挑一个好的,再用**细渲染**出图
```
### ★ ass 粗渲染是什么(这里是关键设计点)
★ 注意:这只是粗渲染的**一个用例**。它的本职是 **agent 的常驻感知层**
(日常游玩),见 §4。
「ass」= **ASCII / 低保真近似**(用户 2026-09-26 提出)。
它要能回答「这个角度拍出来大概是什么样」,但**不启动 MC**:
### ★ 粗渲染与细渲染的分工(用户 2026-09-26 明确,关键)
- **不是**把真渲染结果降采样 —— 那还是要跑 MC,没省。
- **是**用一个**纯数据的近似模型**:从方块数据直接推
「视线方向上有哪些方块、遮挡关系、大致明暗」,
在文本/低分辨率网格上画出来。
> **粗渲染是 agent 日常游玩用的**;**细渲染是出图和 agent 判断精细状态用的**;
> **细渲染是单次调用的而非常驻的**。
**为什么它值得做**(三个具体收益):
| | 粗渲染(ass) | 细渲染 |
|---|---|---|
| 用途 | **agent 日常游玩** | **出图 + 判断精细状态** |
| 生命周期 | **常驻**(随 agent 会话) | **单次调用**(起 → 渲染 → 销毁) |
| 频率 | 高(agent 每一步都可能问) | 低(要图 / 需要精细判断时) |
| 延迟预算 | 毫秒级 | 20-40s 可接受 |
| 依赖 | 纯 Python(**无 JVM**) | JVM + 完整 MC 栈 + 加载器/光影 |
| 数据源 | 轻量世界快照 | 完整渲染管线 |
1. **试错便宜**:一次真渲染要起 JVM(~20-40s),粗渲染是毫秒级
⇒ agent 可以试几十个机位,只把最好的那一个送进真渲染。
2. **给 agent 可读的中间结果**:ASCII 图能被 LLM 直接「看」和理解,
而 PNG 需要多模态输入。agent 可以在文本空间里推理"该往哪挪"。
3. **失败模式友好**:机位选错时,粗渲染已经暴露(比如"镜头埋在地里"、
"景物被树挡住"),不用浪费一次完整渲染。
★ **这条分工决定了架构**:
- 粗渲染**不是**细渲染的降级,而是 agent 的**常驻感知层** ——
它要回答 agent 平时的所有感知问题(周围有什么、挡不挡视线、
前面走得通吗、那边是什么),这些都必须廉价。
- 细渲染**按需起、用完走**(JVM 启停成本不敏感)⇒ 慢一点没关系。
- ⇒ 所以**「粗渲染↔细渲染一致性」不是刚需**:两者服务于不同问题,
不要求粗渲染预测细渲染的像素。真正要测的是
**各自对各自用途的正确性**(见 4.5)。
### mineflayer 的参考价值(用户 2026-09-26 点名)
---
> 记得参考 mineflayer 的实现,我觉得它对于**行为触发条件**与**展示方式**
> 以及 **ass 粗略绘图**的形式很优秀。
## 1b. 抽象模型:参考 mineflayer 的「状态 + 事件 + 操作」三层
mineflayer 相关能力(本机已有:`/home/newqqagent/mcbridge/node/node_modules/mineflayer`):
> 至于事件更新通报与操作抽象,我觉得可以参考 mineflayer,
> 使用它的抽象模型调用我们的后端。(用户 2026-09-26)
| mineflayer 的做法 | 对我们的启发 |
|---|---|
| **事件驱动**:`bot.on('chat')` / `'entitySpawn'` / `'physicsTick'` | 行为触发条件:聊天消息 → 意图识别 → 动作(拍照)|
| **`bot.chat()` / `bot.entity.position`** | agent 拿到玩家位置/朝向;回复走聊天 |
| **`prismarine-viewer`**:三种视角(first_person / third_person / fixed)| ★ 相机摆放的**现成语义**:合照 = third_person + 朝向景物 |
| **`.saveScreenshot()`** | 截图接口的形态 |
| **blockAt / findBlock / raycast** | ★ **粗渲染的核心原语**:从方块数据做视线求交,算遮挡 |
| **minecraft-data**:权威方块/物品数据 | 服务端交互的注册表来源之一 |
★ **注意**:机位/拍照只是这个抽象的**一个用例**,不是目的。
下面这套模型要覆盖 agent 的日常:感知、移动、交互、观察、问答。
**⚠️ 但要注意 mineflayer 与我们的根本差别**:
mineflayer 是**真连服务端**、有完整世界状态;
我们是**离屏渲染 + 自喂数据**。所以它的**行为触发条件**能直接借鉴,
而**世界访问**要走我们自己的数据通路(存档 / 服务端注入)。
### 1b.1 mineflayer 的实际形态(已读源码核实)
```
① 状态(挂在 bot 上的对象,agent 直接读)
world 方块/生物群系/光照 entities {id: Entity}
players {username: Player} time / game / controlState
inventory / experience / tablist / teamMap / spawnPoint / settings
② 事件(bot.emit(name, ...),细分层用 ':')
消息类 message / messagestr / chat / chat:<pattern> / actionBar
世界类 blockUpdate / chunkColumnLoad / weatherUpdate
实体类 entitySpawn / entityMoved / playerJoined / playerLeft
玩家类 moved / heldItemChanged / lookingAt / physicsTick
流程类 login / spawn / health / death / kicked / error
③ 操作(bot.<verb>(...))
查询 blockAt / findBlocks / raycast / nearestEntity
移动 move / look / setControlState / simpleClick
交互 dig / placeBlock / activateBlock / attackEntity / equip
社交 chat / whisper / tell
观察 saveScreenshot
容器 openContainer / clickWindow / closeWindow / …
```
**★ 三个值得直接照搬的设计**:
1. **状态是「对象」而不是「方法调用」** —— agent 读 `bot.entities` /
`bot.world` 就拿到快照,不必每次查询。
⇒ 我们的粗渲染(常驻)就该是这个角色。
2. **事件分「粗粒度 + 细分层」**(`message` 与 `chat:<pattern>` 并存)
—— agent 可以只订阅粗粒度(省 token),需要时再挂细分。
⇒ 避免「每个方块变化都推给 agent」把上下文淹掉。
3. **操作是动词,参数是结构化对象**
—— `bot.findBlocks({ matching, maxDistance, count })`
⇒ 不是「一条命令 + 位置参数」,而是「意图 + 可选约束」。
### 1b.2 映射到我们的后端
```
mineflayer 概念 我们的实现 备注
─────────────────────────────────────────────────────────────────
bot.world mcfold 的 World(已有 block_at) ★ 粗渲染的底座
bot.entities 待加:EntityStore 从 chunk/实体数据来
bot.on(...) 事件总线(待加) 粗/细分层同 mineflayer
bot.findBlocks({...}) rough.find_blocks(matching=…, …) 纯 Python,毫秒级
bot.raycast rough.cast_ray(…) DDA
bot.move / bot.look 待定:见 1b.4(我们不做移动)
bot.chat 走 agentmail / 服务端聊天 已有通路
bot.saveScreenshot WorldRenderer.screenshot() 细渲染(单次调用)
```
★ **最重要的一条**:`bot` 那些方法最终落到 `bot._client`(协议层),
而我们落到 **jar 端**(用户定调:「jar 端接受操作意图,输出包」)。
所以**抽象形状照抄 mineflayer,落地换到 jar 端** —— 这正是用户那句话的含义。
### 1b.3 我们的三层(与 mineflayer 同形)
```
状态层 常驻、无 JVM、毫秒级
World / EntityStore / 玩家与相机状态 / 时间天气
★ 这层就是「粗渲染」
操作层 意图式动词
find_blocks / cast_ray / look / say / place / dig …
多数落粗渲染(快);需精细视觉的转细渲染(慢,单次)
事件层 粗粒度 + 细分层
moved / block_changed / entity_appeared / message / chat:<pattern>
★ 分层是为省 token,不是为性能
```
### 1b.4 一个必须先定的边界
⚠️ 我们**不做移动/战斗**(用户:范围是「渲染 + 与服务端 mod 物品交互」)。
所以 mineflayer 的 `bot.move` / `dig` / `attackEntity` **不照抄** ——
它们要么不需要,要么该由「服务端交互」那条线以别的方式提供。
⇒ 待定:`bot.move` 这类「改变世界状态」的操作,在我们的定位里
究竟**不做**、还是**转交服务端**(agent 在真实世界里动,我们在旁边看/记)。
这决定 `操作层` 的边界。
---
@ -224,9 +308,9 @@ protodef 描述**不随版本变** ⇒ `ProtocolVersion` 只影响少数结构
已有 `WorldRenderer` / `Instance` / `create_instance`,缺一个把
「渲染」+「服务端交互」串起来的门面。
### ⬜ G4. ass 粗渲染(§1,新方向)
### ⬜ G4. 粗渲染(ass):agent 的常驻感知层(§1、§4)
见 §4 设计。
见 §4 设计(**常驻**,与细渲染的**单次调用**互补)。
### ⬜ G5. 聊天框 → agent → 拍照 的行为链(§1,新方向)
@ -234,57 +318,86 @@ protodef 描述**不随版本变** ⇒ `ProtocolVersion` 只影响少数结构
---
## 4. ass 粗渲染设计(⬜ 未开始)
## 4. 粗渲染(ass)设计(⬜ 未开始)
### 4.1 定位
### 4.1 定位:agent 的**常驻感知层**
按 §1 的分工,粗渲染服务于**日常游玩**,所以它不是一个"拍照辅助",
而是一个**常驻的、无 JVM 的世界感知接口**。
```
真渲染: JVM + 完整 MC 渲染栈 + (可选)Iris 光影 ~20-40s ~1MB PNG
粗渲染: 纯 Python,从方块数据做视线近似 <100ms 文本网格
细渲染: JVM + 完整 MC 栈 + Iris 单次调用(起→渲→销) ~20-40s
粗渲染: 纯 Python + 世界快照 常驻 <100ms
```
**两者不是精度不同,是用途不同**:
- 粗渲染用来**搜索机位**(试几十个)
- 真渲染用来**出最终图**(只跑选中的那个)
### 4.2 它要回答的问题(agent 游玩时会问的)
### 4.2 核心原语(借鉴 mineflayer / prismarine 的做法)
| 问法 | 需要的能力 |
|---|---|
| 「我面前是什么?」 | 视线求交(DDA)|
| 「这堵墙后面有没有通路?」 | 射线 + 邻域查询 |
| 「我站的地方是空的吗?」 | `block_at`(已有)|
| 「那边那座山是什么?」 | 远距离扫描 / 俯瞰索引 |
| 「周围一圈都有什么?」 | 局部切面(ASCII 最有用的场景)|
| 「从这里能不能看到它?」 | 可见性测试(遮挡判定)|
**其中「ASCII 网格」只是其中一种展示形态** —— 它的价值在于
让 LLM 能直接读,而 PNG 需要多模态。
### 4.3 核心原语(借鉴 mineflayer / prismarine 的做法)
mineflayer 的 `raycast` / `blockAt` 是纯数据的视线求交 —— 我们不连服务端,
但**同样的计算**可以跑在存档数据上:
但**同样的计算**可以跑在存档/注入的世界快照上。
```python
# 输入:世界的方块数据(已能拿到,见 render.py 的 SaveChunk)
# 输出:ASCII 网格 + 结构化线索
# 基础已有(mcfold/world.py):
# World.block_at(x,y,z) -> BlockHit(name, state_id, loaded, source)
# ★ 它已能区分 unloaded / unknown-state ⇒ mod 方块不会被当成空气
for 每条视线(从相机出发,过每个网格像素):
沿射线步进(DDA,与 MC 的方块网格对齐)
记录第一个命中的非空气方块
→ 得到 depth / block / 明暗(按法线与光方向)
# 待加(mcfold/rough.py):
def cast_ray(world, origin, direction, max_dist) -> RayHit
"""沿方向步进(DDA,与 MC 方块网格对齐),返回首个非空气方块"""
def visibility(world, eye, target) -> Visibility
"""两点之间是否通视(被几个方块挡住)"""
def slice_view(world, eye, yaw, pitch, w, h) -> AsciiGrid
"""把视野切成 w×h 的 ASCII 网格(agent 直接读)"""
def scene_summary(world, center, radius) -> dict
"""结构化线索:方块分布 / 开阔度 / 遮挡率(LLM 读数字比读 ASCII 准)"""
```
### 4.3 输出形态(三件,服务于"让 agent 能推理")
### 4.4 「ass」的三种展示形态(服务于不同用途)
```
① ASCII 网格 相机看到的大致形状("镜头埋地里"一眼可见)
② 结构化线索 视野内方块分布 / 主景物的屏幕占比 / 遮挡率
③ 建议 "往左挪 3 格可让景物居中"(可选,规则驱动)
① ASCII 网格 视野切面 —— 人看 + LLM 直接读
② 结构化线索 开阔度/遮挡率/最近景物/可通行方向 —— LLM 推理用(比①准)
③ 可选:俯视 ASCII 局部地图(w×h 格,每格一个地标字符)—— 定位用
```
**②比①更重要** —— LLM 读数字比读 ASCII 准;
ASCII 主要是给人看和做快速否决。
### 4.5 验证口径(★ 改成"各自对各自用途",不是互相预测)
### 4.4 与真渲染的一致性(关键风险)
```
粗渲染的判据(不需要细渲染参与):
· block_at 已区分 unloaded / unknown-state ⇒ 不能把未加载当空气
· cast_ray 的结果与人工推算一致(可构造的简单场景:单方块、空走廊、斜坡)
· 局部切面的 ASCII 与直接读方块数据一致
⚠️ 粗渲染的结论必须**能预测**真渲染。要测:
- 同一机位,粗渲染说"看得见主景物" ⇒ 真渲染确实看得见
- 建立**一致性判据**(不是"看起来差不多",而是可判的:
「主景物的屏幕包围盒重叠率 > 阈值」)
细渲染的判据(不需要粗渲染参与):
· 现有:注入地形 → distinctColors / chunksRendered 达阈
· 现有:Sodium tracker 通知后 chunkReady > 0
### 4.5 实现落点
★ **不再要求**「粗渲染能预测细渲染」—— 两者服务不同问题
(用户口径:粗=日常游玩,细=出图与精细判断)
唯一的交集是「拍照选机位」这一个用例,那里是**用法**上的配合,
不是**正确性**上的依赖。
```
`mcfold/rough.py`(新模块)+ `tests/test_rough.py`
- 输入:`WorldRenderer` 的方块数据(或直接读存档)
- 不依赖 JVM ⇒ 可在默认测试套件里跑(秒级)
### 4.6 实现落点
`mcfold/rough.py`(新模块,纯 Python、**不依赖 JVM**)
`tests/test_rough.py`(默认套件里跑,秒级)
---
@ -301,9 +414,9 @@ ASCII 主要是给人看和做快速否决。
↓
③ 需要什么 玩家位置/朝向 + 景物位置 + 可用机位
↓
④ 规划 粗渲染试 N 个机位 → 挑最好的
④ 规划 粗渲染(常驻)试 N 个机位 → 挑最好的
↓
⑤ 出图 真渲染(Iris + 光影)
⑤ 出图 细渲染(单次调用:起 JVM → Iris+光影 → 销毁)
↓
⑥ 送达 回聊天 / 发邮件(走现有 agentmail 通路)
```
@ -332,7 +445,7 @@ shot = capture_photo(
subject_scale=20, # 景物半径
with_player=True, # 是否要合照
player_pos=(x, y, z), # 合照时的"玩家"位置
samples=20, # 粗渲染试几个机位
samples=20, # 粗渲染试几个机位(常驻,毫秒级)
)
shot.png → /path/out.png
shot.rough → ASCII + 线索(给 agent 看它为什么选了这个角度)
@ -345,14 +458,20 @@ shot.rough → ASCII + 线索(给 agent 看它为什么选了这个角度)
| # | 内容 | 依赖 | 状态 |
|---|---|---|---|
| M1 | 修 G1(build.sh 编译 CP 加 Fabric 库),1.21.1 构建跑通 | — | 🔄 |
| M2 | `rough.py` 粗渲染(纯 Python,DDA 视线) | — | ⬜ |
| M3 | 粗渲染 ↔ 真渲染一致性判据 | M2 | ⬜ |
| M4 | `capture_photo()` 机位搜索 + 统一接口门面 | M2, M3 | ⬜ |
| M2 | **抽象模型骨架**:状态/事件/操作三层(照 mineflayer 形状) | — | ⬜ |
| M2a | └ `rough.py` 常驻粗渲染(World 视图 + DDA 视线 + ASCII/线索) | M2 | ⬜ |
| M2b | └ 事件总线(粗粒度 + 细分层) | M2 | ⬜ |
| M3 | 粗渲染自身判据(构造场景;**不要求预测细渲染**) | M2a | ⬜ |
| M4 | `capture_photo()`(**作为第一个用例**:粗搜机位 → 细出图) | M2a, M2b, M3 | ⬜ |
| M5 | 服务端交互(configuration / registry_data / 物品表) | M1 | ⬜ |
| M6 | 聊天框 → agent → 拍照 全链 | M1, M4, M5 | ⬜ |
| M6 | 聊天框 → agent → 拍照 全链(§5) | M1, M4, M5 | ⬜ |
**建议先做 M1 + M2**:M1 解阻断,M2 是用户新设想里最有独立价值的一块
(纯 Python、不依赖 JVM、可秒级验证)。
**建议先做 M1 + M2**:M1 解阻断;M2 是**主线**(抽象模型骨架),
拍照只是它的第一个用例。M2 做完后 agent 就有常驻感知能力了,
而这正是"日常游玩"的地基。
★ M3 特意**不叫**"一致性判据":按用户口径粗/细服务于不同问题,
不要求粗渲染预测细渲染的像素。M3 只验粗渲染**自己**对日常感知问题的正确性
(构造简单场景,人工可推算)。
---