惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Y
Y Combinator Blog
博客园 - Franky
D
Docker
B
Blog RSS Feed
M
MIT News - Artificial intelligence
雷峰网
雷峰网
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
S
SegmentFault 最新的问题
GbyAI
GbyAI
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
B
Blog
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
Vercel News
Vercel News
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News

阁子

LEANN 的重算式向量索引 · 阁子 lamarck 按真实调用进化全部 skill · 阁子 FrontierAgent 的上下文压缩与重复抑制 · 阁子 magpie 本地语义搜索 · 阁子 OpenViking 的上下文组织与检索 · 阁子 munder-difflin 的多 agent 协调实现 · 阁子 ai-memory:换个 CLI 接着干的 agent 记忆层 · 阁子 TimesFM:预测之前的脏活都在模型外面 · 阁子 DarwinX:模型不动,只进化外壳 · 阁子 Final2x:一个刻意做薄的超分外壳 · 阁子 Cordis:把卸载做完备的插件框架 · 阁子 DeepSeek Harness:一切皆插件的开源 agent 运行时 · 阁子 Agency Agents:一座 AI 角色库的分发工程 · 阁子 Harvey LAB:法律 Agent 基准的架构与评测方法 · 阁子 DeepTutor:开源的 AI 私人家教工作台 · 阁子 Semantica:面向可审计 AI 的图原生基础设施 · 阁子 PromptCache:LLM 语义缓存网关的架构与实现 · 阁子 Prime Agent,只给模型一个工具的长跑 agent · 阁子 Skill Shelf: 给 agent 的 skill 仓库,兼做服务的配置中心 · 阁子 Cloudflare Computer,装在 Durable Object 里的文件系统 · 阁子 QM,给全公司用的 agent 平台 · 阁子 PersonaLive: 把肖像扩散模型压到能直播 · 阁子 Neon: 把本地视频点亮成系统虚拟摄像头 · 阁子 LiteReality-Agent笔记: 从一次扫描到可交互的房间 · 阁子 蝉与梦 · 阁子 小工具(三) 小工具(三) · 阁子 相机小述 相机小述 · 阁子 四元数与旋转矩阵
LiteReality-Agent笔记: 从一次扫描到可交互的房间
dfine · 2026-08-10 · via 阁子

好久没写点正经的东西了,上一篇还是折腾容器的事。这两天读了一个叫LiteReality-Agent的项目,是用手机扫一遍房间,然后端到端生成一个可编辑、可渲染、门窗抽屉都能动的三维场景。
本来只想看看它怎么调模型的,结果被它的工程组织方式吸引了,索性做个笔记。主要记录它的流水线结构、agent的关进笼子的方式,以及几处让我后背发凉的坑。


RGBD扫描与重建结果

先说问题在哪

手机扫房间在2026年已经不难了,Apple的RoomPlan走一圈就能给出一串RGB帧、深度、ARKit相机位姿,以及一个room.usdz,里面有墙面、门窗洞口和一组粗糙的物体包围盒。
难的是这堆东西离「能用的三维场景」还差得远。

RoomPlan给的是物体包围盒加类别标签,做图形要的是有真实几何和材质的网格;
给的是不可编辑的usdz,要的是能改、能重编译、能导出的场景源;
给的是静态几何,要的是门会转、抽屉会拉、洗碗机门会翻下来;
给的是「大概像个房间」,要的是「就是这间屋子」。

最后一条最容易被忽略。多数生成式场景重建的验收标准是「看起来像个合理的房间」,而不是「和我扫的那间对得上」。这个项目整条设计都在咬后者。

顺手数了一下规模: src/下183个python文件,28219行;tests/下41个test_*.py;100个提交;要求python >=3.10,<3.13;Apache-2.0;版本号还是0.0.1,Development Status写的Alpha。
一个两万八千行的alpha项目,架构文档能和代码逐条对得上,这事本身就不多见。

一条命令

支持的入口只有一个CLI。

1

uv run litereality run /path/to/capture

没有扫描数据也能试,官方放了示例房间。

1

2

git clone https://github.com/LiteReality/example-scans.git

uv run litereality run example-scans/<scan>

跑完之后直接在浏览器里走进去。

1

uv run litereality view run/<scan>

产物是通用格式,room_preview/Room.glb材质已烘焙、动画clip完整,Blender/Unity/Unreal/Web都能吃;room_preview/Room.blend是同一个房间的Blender场景;room/是可编辑的源,整个房间在这里定义。

五个stage,两个phase

架构文档第一句就把全貌摊开了。

1

2

cli.py → PipelineRunner → scene_init → realism_authoring

ingest → reconstruct → seed author → publish

五个公开stage声明在pipeline/stages.py里,十几行看完。

1

2

3

4

5

6

7

STAGES = (

Stage("ingest", ingest.run, is_complete=ingest.complete),

Stage("reconstruct", reconstruct.run, ("ingest",), is_complete=reconstruct.complete),

Stage("seed", seed.run, ("reconstruct",), is_complete=seed.complete),

Stage("author", author.run, ("seed",), is_complete=author.complete),

Stage("publish", publish.run, ("author",), is_complete=publish.complete),

)

每个stage带两样东西: 前置依赖元组,和一个is_complete(context)谓词。这两个撑起了整个可恢复语义,后面会看到。

确定性与agentic的分界

这是整个项目最重要的一条线。
scene_init确定性的——读扫描、检测、生成物体、拼成种子房间,同样输入出同样东西,没有模型在里面拍脑袋做布局决策。realism_authoring是agentic的——agent拿着种子房间,对着原始扫描的照片看,然后改代码,直到房间和照片对上。
两半可以各自单独跑。

1

2

uv run litereality run /path/to/capture --through seed

uv run litereality stage author run/my-room --force --polish --live

--polish在authoring之后追加三个可选pass: 物体精修、材质、模型驱动的质量检查。--live开一个浏览器视图,边跑边看房间被搭起来,旁边是agent的完整trace。
把这条线画明,调试时永远知道该骂谁: 布局歪了是seed的问题,材质不对是agent的问题。很多把LLM塞进流水线的项目坏在这条线是糊的。

PipelineRunner

pipeline/runner.py只有154行,但把可恢复流水线这件事做对了。状态落在run/<scene>/.litereality/pipeline.json,原子写。

1

2

3

4

5

def _write_state(self, context: RunContext, state: dict[str, Any]) -> None:

context.state_path.parent.mkdir(parents=True, exist_ok=True)

tmp = context.state_path.with_suffix(".tmp")

tmp.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")

tmp.replace(context.state_path)

先写.tmpreplace,中途断电不会留下半个JSON。基本功,但很多人不做。

force会级联失效下游

1

2

3

4

forced_indexes = [i for i, stage in enumerate(self.stages) if stage.name in force]

invalidate_from = min(forced_indexes) if forced_indexes else len(self.stages)

for stage in self.stages[invalidate_from:]:

prior.pop(stage.name, None)

强制重跑reconstruct,那么seedauthorpublish的完成记录全部作废。这是对的,上游变了下游的「已完成」就是谎话。见过太多流水线在这里偷懒,结果拿旧的authoring结果配新几何,产出一个谁也解释不了的场景。

磁盘状态可以反过来认领内存状态

1

2

3

4

5

6

for prerequisite in stage.prerequisites:

dependency = self.by_name[prerequisite]

if (prerequisite not in completed

and dependency.is_complete is not None

and dependency.is_complete(context)):

reused = StageResult(prerequisite, StageStatus.REUSED)

直接跑litereality stage author,runner发现seed没在状态文件里,但磁盘上Room.py确实躺着,那就承认它,标成REUSED。这让状态文件丢了不再是灾难,磁盘产物本身就是事实来源。

环境变量的兼容桥被关在编排边界

一部分从旧代码移植来的stage还在读全局环境变量。作者没有逐个去改,而是在唯一一处用patch.dict把环境注进去,stage一返回立刻还原调用方的进程环境。

1

2

3

4

5

# Several ported stage implementations still read canonical environment

# names. Keep that compatibility bridge at the orchestration boundary and

# restore the caller's process immediately after the stage returns.

with patch.dict(os.environ, context.environment, clear=True):

result = stage.run(context, stage_options)

这是我很欣赏的处理历史债的姿态: 把脏东西收敛到一个点,并在注释里写明它为什么在那里。既不假装它不存在,也不为了洁癖去做一次高风险大重构。

CLI层的状态输出也干净。

1

2

3

4

ingest completed 142.3s

reconstruct reused 0.0s

author skipped 0.0s

publish failed 12.1sfinal compile failed; see ...

房间是一段可读的程序

这是整个项目最聪明的一个决定: 房间的源不是JSON,不是usdz,是一个python程序。
格式契约如下。

1

2

3

4

5

6

7

Room/

├── Room.py 语义外壳 + 物体摆放程序

├── Room.md 编辑指南(给agent看的)

├── manifest.json 物体到RoomPlan的映射

└── Objects/

├── Procedural/<name>/ object.py, object.md, textures.json

└── Static/<name>/ 源GLB + 统一的object.py包装

编译产物单独放,随时可重新生成。

1

2

3

4

5

room_preview/

├── Room.glb

├── Room.blend

├── room_layout.json

└── Object/

Room.py里嵌的是可读的几何: 墙的端点、门窗洞口、地板天花板高度、物体包围盒。程序化物体保留可编辑的Blender构建代码和纹理配方,神经网络生成的静态资产保留源GLB。
为什么这个决定重要? 因为它让「agent编辑场景」退化成了「agent编辑代码」——一个LLM已经很擅长的问题。不需要发明场景编辑DSL,不需要给agent一堆move_object(id, x, y, z)工具,agent就用它最熟的Read/Edit/Write改python文件,然后重新编译看结果。
room_ops包拥有这个表示,以及所有不依赖流水线状态的操作: 读写manifest、编译Blender和GLB、渲染、导出、起可行走的viewer。公开API就两行。

1

2

3

4

from litereality_agent.room_ops import compile_room, export_scene

room = export_scene("office-elliott")

glb = compile_room(room)

还有一条纪律: import room_ops不会启动Blender,只有显式的compile/render操作才会。

agent的能力工具是个闭集

agent手里除了Read/Edit/Write/Glob,还有六个领域能力工具,agent/tools/default_registry.py是唯一真值来源。

fetch_material: 从Poly Haven抓真实PBR材质集(diffuse+rough+normal),可选重新着色
select_views: 为房间、某面墙或某个物体挑最合适的采集帧,绝不让agent猜帧号
render: 针对一个目标层出「渲染图 ‖ 照片」对照,自动取景并重新编译
grid: 在表面拼图上画公制标尺,读出装置的(u, z)米坐标而不是估
critic: VLM打分,对着目标给{pass, score, issues},是判官
check_collisions: 真网格穿插和包含检测,object↔object、穿墙、出房间、离地,还有机器人风格的铰接门窗检查,每条都附一个公制的挪动或缩放修正建议

关键设计在select_viewsgrid这两个: 凡是agent容易「合理地猜错」的量,都变成一次工具调用去读。帧号、米制坐标,这些东西LLM编起来毫无阻力,而且编出来的值看着完全合理。做成可查询的工具,比在prompt里写「请不要猜」有效得多。
critic是个VLM judge,输入图像和一句目标描述,输出结构化的通过与否加问题列表。有了它authoring才成为闭环,而不是一次开环生成。
另外compile不是注册工具,它是render内部用的共享代码。agent只能通过render顺带编译,不能单独调编译——少一个工具,少一条走错的路。

退役的工具也留了记录

agent/tools/README.md里有个「retired tools」小节,写着旧的闭环驱动器(agent/harness/loop.py,一套11个原语加3个复合的闭集配裸API循环)已经删除,退役的原语被停放在legacy/下仅供参考,活跃路径没有任何东西import它们。
把「我们试过什么、为什么不用了、残骸在哪」写进文档,比只留下当前状态有价值得多。半年后有人问「为什么不做成闭集工具」,答案在仓库里。

harness和model是两个正交旋钮

agent/providers/base.py开头就点明的抽象,我认为是这个项目最有普适价值的一条。

harness agent循环: 文件工具、权限模型、MCP接线、hooks、事件流
model 循环里的那颗脑子(HARNESS_MODELLR_PROCEDURAL_MODEL…)

选择靠配置解析,优先级是LR_<ROLE>_PROVIDER > LR_AGENT_PROVIDER > claude。支持两个harness: claudeclaude_agent_sdkcodexcodex exec

harness不是功能等价的,而且这件事被显式建模了

每个harness声明一个supports集合,调用方分支判断而不是假设。

hooks: 能在工具调用真正执行前拒绝或引导它,用于step budget的优雅收尾
inproc_tools: 能力工具跑在本进程,不用stdio MCP子进程
cost: 会报告这次session花了多少钱
skills: 原生加载.claude/skills
restricted: 工具白名单真的被执行(Codex永远有shell权限)

源码注释里那句话写得很好。

Harnesses are NOT feature-equivalent, and pretending otherwise is how a step budget silently stops existing.

具体差异: Claude Code在进程内托管能力工具、能用PreToolUse hook引导session收尾、报告成本、遵守工具白名单、把文件读取暴露成可观测的Read调用。Codex跑在进程外,能力工具通过agent/tools/mcp_server.py走stdio MCP桥接,step budget退化成硬停,shell权限没法收回,不报成本。
然后是我最喜欢的部分——providers.describe()把这些差距打印到stage头部。

1

2

3

4

5

6

7

8

9

10

11

if spec.step_budget > 0:

native = "hooks" in harness.supports

parts.append(

f"step budget={spec.step_budget}"

+ (f" (wind-down at {max(1, spec.step_budget - max(0, spec.step_reserve))})"

if native

else f" (HARD STOP — {harness.name} has no pre-tool hook, no wind-down)")

)

...

if spec.file_tools and "restricted" not in harness.supports:

parts.append("WARNING: tool allowlist not enforced (shell access is always on)")

配套的注释是。

A missing capability must be visible in the log. …a run that quietly lost its graceful landing looks identical to one that never had it.

降级运行绝不能长得像正常运行。这条我准备直接搬到自己项目里。

step budget的优雅落地

SessionSpec里这两个字段设计得很细。

1

2

3

4

5

# Graceful landing: at `step_budget` tool calls the session ends; for the last `step_reserve`

# of them the capability tools are switched off so what remains goes to final edits.

# 0 disables. Honoured natively where "hooks" is supported, emulated (hard stop) otherwise.

step_budget: int = 0

step_reserve: int = 0

不是到点就砍,而是到点前留一段预算,把能力工具关掉,逼agent用剩下的步数把编辑落盘。做过长agent循环的人都懂这个痛: 硬停最恶劣的形态是agent刚渲染完、正准备改文件,被掐了,一轮全废。

事件流被归一化了

TextBlockToolUseBlockToolResultBlockAgentMessageSessionResult,字段名故意对齐claude_agent_sdk的block形状,这样ToolNarratorAgentTrace、拼图覆盖率检查、Room.py checkpointer这些消费者一套代码吃两个harness。
AgentMessage.raw保留harness自己的原始对象,所以raw trace sidecar是逐字保真的。normalise_blocks对不认识的block类型原样透传而不是丢掉。

Unknown blocks are kept rather than dropped: consumers ignore what they do not recognise, but the raw trace should never lose something because this mapping had not heard of it.

还有一条带血的注释,在ToolResultBlock上。

Carries tool_use_id, never the tool’s name — see tool_narration.py for what reading a name off this block cost us.

「从这个block上读工具名,让我们付了什么代价」。这种注释比任何设计文档都值钱。

复杂度路由: TRELLIS还是procedural

物体重建不是一条路走到黑,classify_complexity.py给每个物体做路由。

1

2

3

4

object_init产出干净参考图

→ classify_complexity路由

├── 椅子 / 沙发 / 抽象几何 → TRELLIS(神经生成,静态GLB)

└── 规则盒体 / 家电几何 → procedural(Blender原语 + PBR)

这个划分很务实。TRELLIS擅长有机形状,但它给不了你正确的关节;而桌子、储物柜、洗碗机、电视、水槽这类东西几何规则,但运动方式必须对。
procedural路线的核心洞见写在models/object_generation/README.md里。

RoomPlan only detects a fixed set of object categories, so we hand-author a detailed spec per category — geometry, materials, and exactly how each part moves.

因为RoomPlan的类别集是封闭有限的,所以「每个类别手写一份详细规格」是可完成的工作量,不是无底洞。category_specs.py是16KB纯领域知识,定义了每类物体的几何、材质,以及每个部件具体怎么动。生成时把类别规格、该物体真实的RoomPlan尺寸、干净参考图一起注入agent prompt。

the dishwasher door drops to horizontal, the drawer pulls out along +Y, the cabinet door swings on its outer vertical edge

洗碗机门翻到水平、抽屉沿+Y拉出、柜门绕外侧竖边旋转。这些不是模型猜的,是规格里写死的。
产物里每个运动部件带glTF node extras,有articulation_type(revolute或prismatic)、articulation_axislimit_minlimit_max,仿真器可以直接读。这就是「可交互」三个字的落点,不是「能点击」,是物理仿真意义上的关节。
顺带,这条路线的驱动是一个Claude Code skill(image-to-articulated-glb),装在articulated-glb-agent/.claude/skills/下,再被python launcher批量调用。用skill封装「图到铰接GLB」这个能力,这个组合挺有启发性。

最后一道闸门不含模型

agent会犯错,所以收尾是纯几何的。pipeline/room_qc/checks.py的docstring直接列清了要报什么。

below_floor / above_ceiling: 物体穿出地板或天花板
floating / sunk: 落地家具没落在地上
outside_room: 物体中心跑到房间轮廓外
wall_clash: 家具插进墙板
object_clash: 两件家具互相穿插
fixture_over_opening: 墙面装置压在门窗洞口上

关键在下一句: 正确的重叠不报。台下式水槽本来就在台面里、嵌入式烤箱本来就在橱柜里、椅子本来就塞在桌子下,这些在EXPECTED_CONTAINMENTPASSTHROUGH里白名单化。

Everything is pure arithmetic over AABBs, so it’s fast, exact, and needs no LLM.

一个agentic项目最后用纯AABB算术把关,这个层次感是对的: 能用算术判定的事情不要交给模型。报出来的问题由room_qc/fix.py挪动家具解决。
而且盒体运算只有一份,住在每轮agent调用都会用的那个工具里(agent/tools/check_collisions/source/geometry.py),checks.py只拥有「报告」这件事,因为「房间算不算过关」是流水线的决定。同一套几何判定,agent自查和流水线终检共用,不可能出现两套标准。

两个静默失效的教训

第一个是python-fcl不能做可选依赖。pyproject.toml里有一段异常长的注释。

QC true-mesh collision. NOT optional: publish runs room_qc.correct on every default run and only records a warning if it exits non-zero, so a missing FCL turned the deterministic clash gate into a silent no-op.

缺了FCL,那道确定性碰撞闸门就变成空操作,而且只留一条warning。作者的判断是预编译wheel覆盖所有支持的平台,代价是几MB和零编译,比发一个悄悄什么都不干的闸门便宜得多。
第二个是不烘焙材质,发布出去的是另一个房间。publish/__init__.py里的注释。

a procedural wall/floor/ceiling material (two_tone_mat, carpet_mat, ceiling_tile_mat) has no glTF representation, so it exports with neither a texture nor a baseColorFactor and renders WHITE. Flat-RGB and fetched-image materials survive either way, which is why this stayed invisible until a room used procedural ones.

程序化材质在glTF里没有表示,导出后既没纹理也没baseColorFactor,渲染成纯白。而平色和贴图材质两种路径都活得下来,所以这个bug一直隐身,直到某个房间真的用了程序化材质。修法是publish时显式调api.bake_room(),让agent看到的每张渲染图和最终产物走同一条烘焙路径。
这两条读的时候都有点后背发凉。验证通道自己失效是最难发现的一类bug,因为所有指示灯都是绿的。

配置

LiteRealitySettingspydantic-settings,加载顺序是进程环境 > .env > models.env > 类型化默认值。models.env是仓库自带的模型默认值,.env是你机器上的密钥和路径,每行用${VAR:-default}所以shell里已有的值一定赢。
一次性在组合边界解析完,然后apply_environment()setdefault铺到环境里,不覆盖调用方的shell。

两个校验细节值得看。未知harness必须在这里炸,而不是在付费session深处。

1

2

3

4

# An unknown harness name must fail HERE, at the composition boundary, rather than deep

# inside a paid session: `LR_AGENT_PROVIDER=codexx` would otherwise fall through to the

# claude default and silently run the wrong agent.

self.agent_provider = self._checked_provider(self.agent_provider) or "claude"

LR_AGENT_PROVIDER=codexx多打一个x,会静默fall through到claude默认值,跑错agent还花你的钱。
半个token pair不能算「已配置」。

1

2

3

4

5

6

7

def modal_credentials(self) -> tuple[str, str] | None:

"""...Half a token pair cannot authenticate, so it must not read as configured —

otherwise the runtime selects Modal and fails at the call instead of falling back

or saying what is missing."""

if self.modal_token_id and self.modal_token_secret:

return (...)

return None

只填了MODAL_TOKEN_ID没填secret,如果算作配置了Modal,运行时就会选择Modal然后在真正调用时失败,而不是回退或者告诉你缺什么。
还有一个挺可爱的兼容处理: models.env被python-dotenv读,而dotenv不展开用作默认值的$VAR,所以${VAR:-$OTHER}会原样到达变成字面量$other。代码把以$开头当作未设置。

1

2

if not chosen or chosen.startswith("$"):

return None

models.env

这个文件写得像文档,一个地方选完所有模型。

LR_AGENT_PROVIDER: 哪个agent harness,默认claude
HARNESS_MODEL: 写和改Room.py的编辑者,也驱动classify和VLM reader,默认claude-opus-5,这是主要旋钮
HARNESS_CRITIC_MODEL: VLM critic,默认跟随HARNESS_MODEL
LR_CHAIR_JUDGE_MODEL: 椅子类型判官(框架、扶手、软包、底座属性)
LR_PROCEDURAL_MODEL: 铰接GLB的程序化agent
LR_COMPLETENESS_MODEL: 完整性闸门,参考图对渲染图判「有没有缺件」
LR_OPENAI_IMAGE_MODEL: 参考图生成,默认gpt-image-2
LR_DINO_MODEL / LR_DINO_EMBED_MODEL: GroundingDINO检测与DINOv2分组嵌入

LR_COMPLETENESS_MODEL的注释很诚实。

Runs once per build attempt per object and sits on the critical path. …Lowering it is tempting but two-sided: too lenient ships objects missing parts, too strict costs a full agent rebuild.

降配是双向风险: 太松就发出缺零件的物体,太严就白烧一次完整重建。把这种权衡写在配置项旁边,而不是留给后人试错。
另外注释里明确写了claude-fable-5太贵且并不比claude-opus-5强,所以默认不用。这种基于实测的取舍记录,比任何benchmark表都实用。

运行时隔离

TRELLIS和GroundingDINO需要GPU,项目的默认选择是托管在Modal上。

1

2

3

4

uv sync --frozen --extra modal --group dev

cp .env.example .env

uv run litereality setup

SANITY_DEEP=1 uv run python sanity.py

理由说得很直白: 本机不跑重活,一台没独显的Apple Silicon Mac就够,而且检测能并行铺开到多个容器,不用在一张卡后面排队。有自己的Linux GPU就走deploy/local-gpu.md
绑定逻辑在models/registry.py,短得可以全文引用。

1

2

3

4

5

6

7

8

9

10

11

12

def gen3d_from_settings(settings=None):

settings = settings or load_settings()

if settings.modal_configured():

from litereality_agent.models.trellis.modal import ModalTrellisService

return ModalTrellisService(...)

if settings.trellis_python:

from litereality_agent.models.trellis.service import LocalTrellisService

return LocalTrellisService(python=str(settings.trellis_python))

raise RuntimeError(

"TRELLIS is not configured: set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET for hosted "

"execution (the default), or TRELLIS_PYTHON for an explicit local GPU runtime."

)

分层很清楚: model包拥有一条推理路径,runtime拥有「在哪执行」。deploy/modal/里是托管模型的包装,应用代码永远不import它。重依赖也隔离在自己的环境里(--extra detect--extra gen3d),不污染轻量的agent循环环境。
顺带,CLI和单元测试不会启动DINO、TRELLIS、Blender,也不发起付费调用。这条保证让「随手跑一下测试」成为可能。

工程纪律

这部分其实是我读完最想聊的。

依赖方向被测试执法

1

2

3

cli.py → pipeline → agent → room_ops

↘ models → runtimes

↘ room_ops

箭头从调用方指向依赖。models、room_ops、可复用agent永远不import pipeline。这条规则不是写在文档里靠自觉,是tests/test_architecture.py在跑的。
架构文档还专门写了不存在什么。

There are no top-level services, adapters, or shared packages. There is also no nested pipeline/stages package.

声明不存在的东西,是防止架构熵增的好办法。下一个人想加shared/之前,会先在文档里撞到这句话。

测试默认离线且快

1

2

3

4

5

6

addopts = "-m 'not blender and not scan and not live' --strict-markers"

markers = [

"blender: needs a Blender install ($LITEREALITY_BLENDER) — run with `-m blender`",

"scan: needs real scan data under scans_uploaded/ — run with `-m scan`",

"live: makes paid model calls — run with `-m live`",

]

注释是「The default run must stay offline and fast, so it is worth running before every commit.」三个marker把「需要Blender」「需要真实扫描」「要花钱」分层剥离,默认套件保持离线,所以它值得在每次提交前跑。加上--strict-markers,打错marker名字会直接失败而不是静默失效——又一个防静默的例子。
安全的本地验证只有三条命令。

1

2

3

uv run ruff check src tests sanity.py scripts

uv run pytest -q

uv build

根目录还有个24440字节的sanity.py,配SANITY_DEEP=1做深度检查。对一个依赖Blender加三个模型加一个云运行时加两个agent CLI的项目来说,把「你的环境到底行不行」做成一个可执行的自检,是省下无数issue的投资。

唯一副本,测试钉死

agent/tools/README.md里说明了哪些原语是共享而非某个工具独有的,以及为什么。

config.py: harness路径和旋钮,四个工具加pipeline的evidence.py都要
scan.py: scan_from_roomconfig_fortests/test_scan_inference.py钉死只存在一份
overlay.py: 墙面投影,composeselect_views都要
image_selection/: 表面几何加正视比较
stitch_wall_image/: 矫正后的墙面拼图,四处使用

一句话交代动机: 「a per-tool copy is a bug waiting to happen」。写个测试来保证某个函数全仓库只有一份,这个手法我以前没想到过,但确实是对付「复制一份改改」这种腐化的最直接办法。
另外有些包装是故意的: compile包着room_ops.compile_room(格式自己的编译器),fetch_material包着compile/fetch_textures(因为textures.json是Room格式契约的一部分)。文档里标了deliberately。把「这里看起来该合并但我们没合并」写清楚,省掉后人一次好心的错误重构。

抛开三维重建,能抄的几点

  • 把确定性的和agentic的显式分层,不让模型参与能用算术解决的决策,最后一道闸门用纯几何。
  • agent容易「合理地猜错」的量做成工具去读。帧号、米制坐标,LLM编这些毫无阻力且编得很合理。
  • 把场景或配置变成代码,让「编辑」退化成「改代码」,直接复用LLM最强的能力。
  • harness和model是两个正交旋钮,而且harness之间不是功能等价的,用supports集合显式建模差异。
  • 降级运行绝不能长得像正常运行,缺失的能力必须打进日志。
  • 长agent循环要留优雅落地的预算,到点前关掉能力工具。
  • 配置错误在组合边界炸掉,不要漏进付费session。
  • 警惕验证通道自己失效,这类问题只能靠「改输入看输出是否真的变」来抓。
  • 文档要写「不存在什么」和「我们试过什么」。
  • 把权衡写在配置项旁边,替后人省掉一轮试错。

上手清单

需要的东西: uv;Blender 5.x(测试于5.1,BLENDER_PATH指向安装目录而不是可执行文件);OpenAI API key用于参考图生成,通常每个场景不到一美元;一个已登录的agent CLI在PATH上,claude是默认,codex也支持;以及一个跑GPU的地方,Modal账号(推荐,免费额度够,Mac也能用)或者显存不小于24GB的Linux机器。平台上macOS的Apple Silicon和Linux都测过。

1

2

3

4

5

6

uv sync --frozen --extra modal --group dev

cp .env.example .env

uv run litereality setup

SANITY_DEEP=1 uv run python sanity.py

uv run litereality run /path/to/capture

uv run litereality view run/<scan>

扫描端是免费的LiteReality Scanner,走一圈就够。

局限

免得读起来像软文,说说不好的地方。
版本还是0.0.1,Development Status是Alpha,技术报告没出。整条推理链路挂在claudecodex的登录态上,不是纯API key就能起,这对复现和CI是个真实的摩擦点。成本也不透明,参考图生成有报价,但authoring那一大段跑在订阅制CLI上,注释里明说not metered,所以很难算清一个场景的真实开销,Codex harness干脆不报成本。
Codex路线是明确的二等公民,没有pre-tool hook、没有工具白名单、不报成本,文档诚实标出来了,但不用Claude的话体验是降级的。物体精修更是只支持Claude,因为render_object工具是围绕活跃session状态按物体动态构建的,stdio桥接没有registry条目可以重建。作者的处理是报明确的错然后失败,而不是在没有唯一自检工具的情况下硬跑,这个选择我赞成。

不过话说回来,这个项目真正的贡献我觉得不是「用LLM做三维重建」,而是演示了一套把agent关进笼子的工程范式。笼子的骨架是可恢复的确定性流水线、代码化的领域表示、闭集能力工具、不含模型的终检闸门,以及对降级路径的强制可见性。agent在笼子里干它最擅长的事——看图、改代码、迭代,笼子保证它改不出可验证边界之外的东西。
两万八千行代码、41个测试文件、100个提交,做到架构文档和代码逐条对得上,注释里还留着「这个bug让我们付了什么代价」,这种密度的工程自觉,2026年的agentic项目里不算常见。

仓库在这里,项目主页在这里

看完之后手有点痒,感觉自己那几个项目的日志该重写了。