技术社区里隔一段时间就会冒出一个类似的问题:How do you correct spatial reasoning of LLMs?翻译过来就是,怎么修正大语言模型的空间推理能力。每次讨论到后面都会分成两派:一派觉得空间推理属于逻辑推理的一部分,只要把 Prompt 写得更好,模型自然能学会;另一派直接拿一个正方体转九十度的问题去测,发现模型回答看似合理,实际方向完全反过来,于是得出结论说大语言模型根本没有空间感。
这两派其实都站在同一个误区上:他们默认空间推理是模型内部可以“修正”的某种能力,差别只是修正手段不同。但如果你真的把这类问题拆开来看,会发现“修正”这个词从一开始就用错了。你没法通过提示词或微调,给一个纯文本模型装上真正连续的空间感受器。你能做的,是重新设计问题的表示方式,把空间推理从“让模型猜几何”变成“让模型走流程”。这篇文章的主线就是这个判断。
1. 为什么 LLM 会犯错:它不是“不聪明”,而是没有空间画布
1.1 语言模型没有连续几何表征
先看一个直观例子。你问一个模型:“桌上有一个正方体,把它顺时针转 90 度,原来朝东的面现在朝哪里?”模型可以给你一个语法通顺、听起来很有把握的回答。但你紧接着问:“再转 90 度呢?”它很可能给出一个与上一问互相矛盾的答案。
这个现象不是偶然。LLM 的训练目标是预测下一个 token,它学到的是词与词之间的统计关联,不是空间中的几何变换关系。人类做空间推理时,脑子里有一个可以旋转、比较、遮挡的“画面”,或者说一个连续的空间模型。模型没有这个东西,它只有一串离散 token。
所以当你问它几何题时,它并不是真的在脑海里转动那个正方体,而是在大量语料里找到“顺时针 90 度”“朝东”“朝南”这类文本模式,再按概率组合出一个答案。只要组合出来的答案看起来合理,它就会表现为“流利但不可靠”。
1.2 它真正做的是语言层面的拟真,而不是空间计算
我之所以强调这一点,是因为它能解释一个非常反直觉的现象:模型在小范围、常见布局里表现得不错,比如“书本在杯子左边”这种简单关系。很多人因此觉得模型有空间感。其实不是。这种判断能在语料里找到大量相似表达,模型记住的是语言模式,不是空间事实。
一旦进入不常见的组合——多层嵌套的包含关系、三维旋转、镜子反射、从不同视角问同一个场景——语言模式就不够用了。模型开始自创拓扑关系:两个物体可以同时在左边,一个物体可以在旋转后仍然保持原方向。这些错误在数学上很离谱,但在文本层面几乎无法被察觉。
这也是为什么单纯靠“多问几次”或“换一个更大的模型”不能根治问题。更大的模型只会把语言模式拟合得更精细,但底层仍然没有一个连续的几何本体可依赖。
1.3 这个问题在生产环境里会被放大
如果你只是拿模型聊聊天,空间错误顶多是个段子素材。但在真实工程里,空间推理错误会直接变成交付事故。典型场景包括:
- 把自然语言描述转成 CAD 图纸或布局参数;
- 生成前端页面里元素之间的位置关系;
- 解释某张图片里的遮挡关系;
- 生成室内导航或物品摆放指令;
- 输出几何题目的解题过程。
这些场景有一个共同点:你不仅需要模型给出“一个答案”,还需要答案经过校验、可复现、能对接下游系统。当模型内部没有稳定空间表征时,这个缺陷就会从“偶尔答错”变成“结果不可维护”。
2. 先分清四类空间任务,再决定要不要“修”
2.1 四层任务划分
不是所有空间问题都得用同一种方式修。我习惯把空间推理任务分成四层:
第一层,空间描述。把一段自然语言里的位置关系解释清楚,属于“阅读 + 转述”。比如“解释这句话里 A 和 B 的左右关系”。这类任务模型通常能应付,因为它本质上还是语言理解。
第二层,空间关系判断。给定多个物体的位置,回答谁在谁的左边、谁离谁更近。这类任务需要模型在心里维持一个坐标表。模型能完成一部分,但参照系一旦复杂,就开始出错。
第三层,空间变换。旋转、镜像、平移、缩放。这是纯文本模型最薄弱的环节,因为它要求的不只是理解“关系”,而是执行“计算”。
第四层,空间规划与生成。设计一条路径、摆一桌物品、排一栋房子的功能分区。这类任务往往包含约束条件,结果必须满足“不重叠”“可达”“符合朝向”等硬性规则。
2.2 四类任务需要的修正程度完全不同
对应地,修正策略也分四级:
- 第一、二层,通过 Prompt 里增加结构化要求就能显著改善。比如“先把所有物体列成表格,再判断关系”。
- 第三层,不建议靠模型内部计算。最可靠的方式是让模型输出一个可执行的几何变换程序或结构化数据,由外部工具完成计算。
- 第四层,本质上是一个约束满足问题。前面两层的修正方法都可以用,但最终需要接一个确定性求解器或校验器,否则模型生成的布局经常“看起来合理,实际重叠”。
把任务分清楚,是空间推理修正的第一步。很多人拿着第三层的问题,却用第一层的方法去修,方向自然是错的。
2.3 一张判断表
| 任务类型 | 典型问题 | 模型可靠度 | 建议修正手段 |
|---|---|---|---|
| 空间描述 | 解释一段文字中的位置关系 | 较高 | 加少量 Prompt 提示即可 |
| 关系判断 | 谁在谁的左边;谁离得更近 | 中等 | 结构化列表 + 明确参照系 |
| 空间变换 | 旋转、镜像、平移、缩放 | 低 | 外接几何计算 / 生成代码执行 |
| 空间规划 | 布局、路径、排盘 | 低 | 约束求解器 + 规则校验器 |
判断标准很朴素:如果这个任务可以用笔在纸上画出来、算出来,就不要指望模型在 token 上“脑补”。能外部化计算的就外部化。
3. 修正空间推理的三个可落地方向
3.1 方向一:把自由文本翻译成结构化坐标
第一个方向不改变模型的结构,只改变问题的输入输出格式。核心思想是:不要问模型“感觉上谁在左边”,而是先逼它把场景转成一个坐标系。
一个常见做法是在 Prompt 中加入“建模指令”:
- 确定参照系:俯视图还是侧视图?观察者面朝哪个方向?
- 给每个物体分配坐标和朝向。
- 所有判断基于坐标,不要基于直觉。
这样做有两个作用。第一,它把隐含的语言歧义显式化。“左边”到底是观察者的左边,还是物体自己的左边,一旦落成坐标,歧义立刻暴露。第二,它让模型的输出可以被程序解析。即使答案错了,你也能知道错在哪一步。
实际调 Prompt 时,我会让模型先输出一个 JSON 格式的场景描述,字段固定为物体名、x、y、朝向。只要 JSON 结构稳定,下游解析就稳定。
3.2 方向二:把几何计算交给外部工具
如果说方向一是让模型“把题抄清楚”,方向二就是让模型“把计算外包”。空间变换的难点在于计算,不在于表达。LLM 擅长把自然语言变成计划,但不擅长执行数值计算。
所以一个更稳的架构是:模型只负责把空间问题翻译成工具调用,由外部几何引擎或代码解释器完成变换,最后再把工具结果回写成自然语言。
现在不少 agent 框架都支持工具调用(比如 function calling、MCP 这类接口方式),这正好可以接一个“几何服务”。输入是“三个物体坐标 + 旋转角度”,输出是变换后的坐标。这个服务可以用非常简单的 Python 函数实现,但它比模型在 token 上做旋转稳定得多。
3.3 方向三:用推理链和反向校验强制检查
第三方向是针对“不得不让模型直接输出答案”的场景。这个时候你可以要求模型把思考过程外显,并做反向验证。
一个比较实用的 Prompt 结构是:
- 列出场景中的所有物体;
- 明确参照系;
- 给出初始布局;
- 逐步完成变换;
- 反向变换检查结果是否回到原位;
- 最后给出结论。
反向验证尤其有用。比如你让模型把物体顺时针旋转 90 度,得到结果后,再让它逆时针旋转 90 度。如果回不到初始位置,答案一定有问题。
这个方法不能保证百分之百正确,但它能把模型的错误从“隐蔽错误”变成“可定位错误”。一旦错误可定位,你就可以在工程里加上重试、投票、人工复核等策略。这就是为什么要强调“流程修正”而非“能力修正”。
4. 实操案例:把一个旋转问题从“瞎猜”改成“可计算”
4.1 一个容易翻车的旋转问题
假设有这样的场景:桌面俯视图里三个物体,坐标分别记为 A(1,0)、B(2,1)、C(0,2)。问题:整体逆时针旋转 90 度后,谁在桌面的最左边?
这里的 A、B、C 可以是一本书、一个杯子、一盏台灯,也可以是 UI 组件、货架上的商品或巡检路径点。关键不是物体名,而是坐标。
直接问模型,常见回答是“B”或者“C”,但推理过程经常不稳定。如果追问理由,模型会给出听起来合理的解释,但要么旋转方向写反,要么坐标变换算错。这种错误很难通过“多问几次”解决,因为每次可能错得不一样。
4.2 第一步:确定参照系和初始布局
修正的第一步是给模型一个坐标系统。我习惯用俯视图下的二维坐标:x 轴向右,y 轴向上,逆时针为正方向。初始布局可以写成:
{ "coordinate_system": "top-down, x right, y up, positive rotation counterclockwise", "A": {"x": 1, "y": 0}, "B": {"x": 2, "y": 1}, "C": {"x": 0, "y": 2} }这里补充一句:实际使用中,“最左边”到底对应 x 最小还是屏幕坐标里的 x 最小,必须提前约定。数学坐标系 y 向上,屏幕坐标系 y 向下,如果混用,旋转方向会整个反过来。
4.3 第二步:用代码完成变换
如果单纯靠模型描述“逆时针转 90 度”,边界仍然模糊。更可靠的方式是让模型生成一段变换代码,然后由解释器执行。
下面是一个常见的二维旋转写法,不是唯一方案,但足够演示:
import numpy as np objects = {"A": [1, 0], "B": [2, 1], "C": [0, 2]} theta = np.radians(90) R = np.array([ [np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)] ]) rotated = {name: np.array(pos) @ R.T for name, pos in objects.items()} for name, pos in rotated.items(): print(name, [round(v, 2) for v in pos])执行后你会得到:
- A(1,0) 变成 (0,1)
- B(2,1) 变成 (-1,2)
- C(0,2) 变成 (-2,0)
在这个约定里,x 最小的物体就是最左边的物体。最终结果是 C 在最左边。
注意一点:旋转中心不同,结果完全不同。上面这个例子默认绕原点旋转。如果题目没有明确旋转中心,应该在建模阶段就向用户确认,而不是让模型替用户预设。
4.4 第三步:回写答案并做反向校验
拿到变换后的坐标后,不要急着把结果交给用户。先让模型用自然语言把推理过程讲清楚:参照系是什么、每个物体初始坐标是什么、旋转矩阵是什么、旋转