Every developer using Claude, ChatGPT, or DeepSeek to write technical papers, documentation, or math proofs runs into the same wall: LLM-generated LaTeX is notoriously sloppy.
LLMs don't run compilers. They guess the next token. That means when you ask an AI for a quick equation breakdown, you often get a mix of raw LaTeX environments, broken delimiters, and unescaped characters that choke standard renderers like MathJax, KaTeX, or Pandoc.
If you’ve ever pasted AI output into Obsidian, Quarto, or a custom web app and watched the rendering break instantly, here is why it happens—and how to fix it cleanly.
*The 3 Big Reasons LLM Math Output Breaks
*
- Delimiter Mismatches ([ ... ] vs $$) LLMs love switching back and forth between LaTeX display style [ ... ], TeX double-dollars $$ ... $$, and inline brackets ( ... ). Many Markdown parsers (including GitHub-flavored Markdown and static site generators) expect clean, unified $ and $$ boundaries. **
- Hallucinated Packages & Macros** Ask an LLM for a multi-line matrix proof, and it might throw in \begin{align*} inside a block that KaTeX tries to parse as inline math, or use custom macros like \argmax without including \DeclareMathOperator.
3. Unescaped Markdown Special Characters
When an LLM outputs LaTeX commands containing underscores (_) or carets (^) inside a Markdown code block or text stream, the Markdown parser often intercepts them first—turning your subscripts into tags before LaTeX ever sees them.
Quick Python Fix: Cleaning LLM Math Delimiters
If you're ingesting LLM text via API, you can run a pre-processing pass to normalize math delimiters before sending them to your frontend parser:import re
def normalize_llm_math(text: str) -> str:
# Convert display brackets [ ... ] to standard $$ ... $$
text = re.sub(r'\[\s*', '\n$$\n', text)
text = re.sub(r'\s*\]', '\n$$\n', text)
# Convert inline brackets ( ... ) to standard $ ... $
text = re.sub(r'\(\s*', '$', text)
text = re.sub(r'\s*\)', '$', text)
Clean up empty block artifacts left by the LLM
text = re.sub(r'\$\$\s*\$\$', '', text)
Example
raw_ai_output = r"The variance is given by: [ \sigma^2 = \frac{1}{N} \sum_{i=1}^{N} (x_i - \mu)^2 ]"
print(normalize_llm_math(raw_ai_output))
Automated Parsing & Preflighting
Writing custom regex rules works fine for simple inline math, but once you start dealing with complex tables, full .tex documents, and mismatched BibTeX keys, manual scripts get messy fast.
To automate this whole cleanup and preflight process, I’ve been working on stemdoc.org.
It acts as a conversion and syntax validation layer specifically for STEM documents. It catches broken environments, resolves syntax mismatches, and converts raw LaTeX/LLM streams into clean Markdown, Typst, or Word formats without having to manually patch Pandoc ASTs.
How are you handling AI math rendering?
Are you running regex pipelines on your API responses, relying on frontend MathJax hacks, or doing something else? Let’s chat in the comments!
Top comments (3)
I totally understand the pain of dealing with LLM-generated LaTeX output, especially when it comes to delimiter mismatches and unescaped Markdown special characters. The
normalize_llm_mathfunction you provided is a great starting point for addressing these issues, and I appreciate the example use case. One potential improvement could be to add additional rules for handling hallucinated packages and macros, such as detecting and removing unused\DeclareMathOperatordeclarations. Have you considered integrating this function with popular Markdown parsers like GitHub-flavored Markdown or static site generators to provide a more seamless experience for users?Thanks for reading and for the great feedback!
You nailed a big limitation of pure regex here. Simple pattern matching works for basic delimiter normalization, but as soon as you hit macro hallucinations like unmapped \DeclareMathOperator or custom environments, regex starts breaking down fast.
To catch hallucinated packages cleanly without choking the renderer, you really need to step up from regex to actual AST (Abstract Syntax Tree) parsing—walking the syntax tree, inspecting the preamble, and either expanding missing macros or injecting the appropriate fallback definitions before it hits the parser.
As for SSG and GFM integration: that's definitely the ultimate goal! For JS/TS static site pipelines (like Next.js, Astro, or Hugo), hooking into the unified ecosystem (via custom remark / rehype plugins) makes this seamless. For Python/Django workflows, running it as a pre-processing middleware before sending text to Markdown-it or KaTeX works really well.
Handling that jump from basic regex cleaning to full AST preflighting and macro resolution is actually what drove me to start building stemdoc.org.
Are you working with a specific SSG stack (like Hugo or Next.js) or custom Markdown parser in your own pipeline right now? Would love to know how you're handling rendering on your end!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.