场景解决方案2026年7月28日

LaTeX 引用变成 ??、目录页码对不上?因为只编译了一遍

PDF 里的 ?? 和错位的目录页码不是语法错误,而是 LaTeX 的多趟机制没跑完。本文讲清 .aux/.toc 的工作方式、什么情况要编译几遍、BibTeX 为什么要四步,以及怎么从日志判断已经收敛。

PDF 里的 ?? 和对不上的目录页码,几乎都不是你的语法写错了,而是编译次数不够。LaTeX 需要靠上一趟运行留下的辅助文件才能算出正确的编号,只跑一遍必然缺一半信息。

第一趟留下辅助文件,第二趟才能把 ?? 换成正确编号

为什么 \ref 输出的是 ??

因为第一趟编译时,那个标签还不存在。LaTeX 从头到尾单趟顺序处理文档,当它读到第 3 页的 \ref{fig:result} 时,第 12 页的 \label{fig:result} 还没被读到,它无从知道那张图最终是第几张、落在第几页。于是它先输出一个 ?? 占位,并把这一趟收集到的所有标签信息写进一个同名的 .aux 辅助文件。

第二趟编译时,.aux 已经躺在磁盘上了。LaTeX 会在开头先把它读进来,于是遇到 \ref 就能直接查表,填上正确的编号。这套「先记账、下次再查账」的机制,是理解 LaTeX 所有多趟问题的钥匙。

同一套机制还管着这些东西:\pageref 的页码、\cite 的文献编号、\tableofcontents 的目录(存在 .toc)、图表目录(.lof / .lot)、以及 hyperref 生成的 PDF 书签(.out)。它们的共同点是——都要等整篇文档走完才知道结果,但又要在文档中间就印出来。

为什么目录页码经常差一页

因为目录本身也占版面。假设正文原本 30 页,第二趟编译插入了一页目录,后面所有内容就都往后挪了一页,而目录里印的还是第一趟算出来的旧页码,于是整体差 1。第三趟才会用新页码重排目录。

极端一点的情况是震荡:某个章节标题正好卡在页面边界,多一行目录条目就把它挤到下一页,页码变了;下一趟目录条目又缩回去,它又回到上一页。这种文档可能编译多少遍都不收敛,需要人工微调(比如给那一处加一点弹性间距)。好在这种情况不常见。

到底要编译几遍

看文档里用了什么。下面这张表是常见组合的最少趟数:

文档里有什么 最少趟数 原因
只有正文,无引用无目录 1 遍 没有需要跨页回填的信息
\ref / \pageref 交叉引用 2 遍 第一遍写 .aux,第二遍读 .aux
\tableofcontents 2–3 遍 目录本身改变页码,可能需要再收敛一次
\cite + BibTeX 4 步(见下节) 中间要插一趟 BibTeX
有 hyperref 书签 +1 遍 .out 变化会触发 rerunfilecheck 警告

注意这是最少趟数。多跑一趟不会有副作用(结果已收敛时输出完全相同),少跑一趟就会留下 ??。所以传统做法是无脑跑三遍,虽然浪费但不出错。

有参考文献时为什么是四步

因为 BibTeX 是一个独立程序,得插在两趟 LaTeX 中间。完整顺序是 xelatex → bibtex → xelatex → xelatex

  1. 第一趟 xelatex:把正文里 \cite{knuth1984} 引用到的条目名记进 .aux,此时还没有任何文献列表;
  2. bibtex:读 .aux 拿到条目名,去 .bib 里查,按你指定的 \bibliographystyle 排好版,输出一个 .bbl 文件;
  3. 第二趟 xelatex:把 .bbl 的内容插进文末,参考文献列表出现了,同时把「哪条文献是第几号」写进 .aux——但正文里的 \cite 在这一趟读到的还是上一版 .aux,所以仍然显示 [?]
  4. 第三趟 xelatex:现在 .aux 里有编号了,正文的 [?] 才变成 [1]

很多人卡在第三步,看到文献列表已经出来了就以为完事,结果正文里全是 [?]。再跑一遍就好。

用 biblatex 的话命令不同:中间那步要换成 biber 而不是 bibtex,趟数逻辑一样。

怎么从日志判断已经收敛

搜日志里的 rerun 提示。LaTeX 和相关宏包在发现「这一趟的结果和上一趟对不上」时,会主动打印明确的警告,这几条是最常见的原文:

textLaTeX Warning: There were undefined references.
LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
Package rerunfilecheck Warning: File `main.out' has changed.
(rerunfilecheck)                Rerun to get outlines right
Package hyperref Warning: Rerun to get /PageLabels entry.

只要还看得到这些,就说明还得再跑一趟。反过来,收敛的标志是它们全部消失,rerunfilecheck 会转而打印一条 Info:

textPackage rerunfilecheck Info: File `main.out' has not changed.
(rerunfilecheck)             Checksum: 53F1C61ABCC06CF9FD3B6F1940CE0C3D;330.

这条校验和就是 rerunfilecheck 的判断依据——它记下上一趟 .out 文件的 MD5,这一趟算完再比一次,一样就说明书签已经稳定。目录能不能停,判断方法也类似:比较这一趟写出的 .toc 和上一趟是否逐字节相同。

本地写作时不必手动数趟数,latexmk -xelatex main.tex 会自动循环编译直到日志里再没有 rerun 信号(它内部也有次数上限,防止遇到震荡文档时死循环)。VS Code 的 LaTeX Workshop 默认 recipe 走的就是 latexmk。

浏览器里编译同理:在线 LaTeX 编辑器 的高级模式跑的是 WebAssembly 版 XeLaTeX,它会在每趟结束后扫描日志信号、并比对 .toc / .lof / .lot 有没有变化,需要几趟就跑几趟,不需要你手动点第二次编译。

什么时候该删掉 .aux 重来

改了标签名或换了文档类之后。.aux 是缓存,缓存就会过期:如果你把 \label{fig:old} 改成了 \label{fig:new},旧 .aux 里那条 fig:old 还在,某些情况下会掩盖真正的错误,让你以为引用还在正常工作。

同样,换文档类(比如从 article 换成期刊模板)之后,旧 .aux 里的计数器格式可能对不上新模板,编译会报一些看起来毫无道理的错。遇到这种「明明代码没问题却报怪错」的情况,先把 .aux.toc.out.bbl 这些中间文件全删掉再编译一遍,是性价比最高的排查动作。

小结

?? 不是错误,是 LaTeX 在说「这个编号我还不知道,等我跑完这一趟」。记住三件事就够了:交叉引用和目录至少两遍,带参考文献是四步,判断收敛看日志里还有没有 Rerun to get。本地用 latexmk 自动化,遇到怪错先清中间文件——这几乎能解决所有「编译出来不对劲」的问题。

本文用到的工具

常见问题

因为 LaTeX 是单趟顺序处理文档的。读到 \ref{fig:1} 时,它还没走到后面的 \label{fig:1},只能先写个占位符 ??,同时把标签和页码记进 .aux 辅助文件。下一趟编译才能从 .aux 里读到正确的编号。目录、图表目录、参考文献编号都依赖同一套机制。