Common LLM mistakes & repair
LLM 生成 Rho 时最常见的 10 个错误——每个含症状 / 原因 / 自动修复代码 / prompt 修复 hint。 把这些 patterns 集成到 server 端校验链——准确率从 ~95% 拉到 99%+。
Mistake 1: 用 JavaScript 语法(Math.X)
症状:
\```interact
slider angle 0 360 0 1
template stl: [{Math.sin(angle * Math.PI / 180):.2f}]
\```
原因:LLM 默认认 JS。
自动修复(regex):
function fixMathPrefix(text: string): string {
return text
.replace(/Math\.PI/g, 'pi')
.replace(/Math\.E/g, 'e')
.replace(/Math\.(\w+)/g, '$1') // Math.sin → sin / Math.sqrt → sqrt
.replace(/Number\.(\w+)/g, '$1');
}
Prompt 加固:
NEVER use Math.X or Number.X — Rho mini DSL has built-in functions: sin, cos, tan, sqrt, pow, abs, min, max, floor, ceil, log, exp, round.
NEVER use JavaScript syntax — use ternary `cond ? a : b` instead of if/else.
Mistake 2: 写 if/else 语句
症状:
computed cat = if (bmi < 18.5) "thin" else if (bmi < 24) "normal" else "over"
原因:mini DSL 没有 if/else 语句。
自动修复(regex 转 ternary):
function fixIfElse(text: string): string {
// 简单 case:if (cond) val1 else val2
return text.replace(
/if\s*\(([^)]+)\)\s+("[^"]*"|\d+\.?\d*)\s+else\s+("[^"]*"|\d+\.?\d*)/g,
'$1 ? $2 : $3'
);
// 嵌套 if/else 复杂——多次替换
}
Prompt 加固:
For conditional logic, use ternary `cond ? trueValue : falseValue`:
- Simple: `bmi < 18.5 ? "thin" : "normal"`
- Nested: `bmi < 18.5 ? "thin" : bmi < 24 ? "normal" : "over"`
NEVER use `if (...) ... else ...` syntax.
Mistake 3: 忘 namespace(多块期待共享)
症状:
\```interact
slider rate 0 0.15 0.05 0.01
\```
\```interact
template stl: [Rate] -> [{rate}] ← rate not declared in this block
\```
原因:LLM 不知 namespace 是必填。
自动修复(AST 检查):
function detectMissingNamespace(blocks: InteractBlock[]): Issue[] {
const issues: Issue[] = [];
const declared: Map<string, Set<string>> = new Map();
for (const block of blocks) {
const ns = block.namespace || '__default__';
if (!declared.has(ns)) declared.set(ns, new Set());
block.controls.forEach(c => declared.get(ns)!.add(c.name));
}
for (const block of blocks) {
const ns = block.namespace || '__default__';
const refs = extractTemplateRefs(block.template);
for (const ref of refs) {
if (!declared.get(ns)!.has(ref) && !block.controls.some(c => c.name === ref)) {
issues.push({
block,
message: `Template references "${ref}" but it's not declared in this block. Did you mean to add namespace?`
});
}
}
}
return issues;
}
Prompt 加固:
When multiple interact blocks should share variables, ALL of them must use the same `namespace`:
\```interact namespace finance
slider rate ...
\```
\```interact namespace finance
template stl: [{rate}]
\```
If blocks are intentionally independent, omit namespace.
Mistake 4: HTML 标签代替 Rho DSL
症状:
<details>
<summary>Click to expand</summary>
content
</details>
原因:LLM 知道 HTML,可能默认走 HTML。
自动修复(规则映射):
const HTML_TO_RHO: Array<{ pattern: RegExp; replacement: (match: RegExpMatchArray) => string }> = [
{
pattern: /<details>\s*<summary>(.+?)<\/summary>\s*([\s\S]+?)\s*<\/details>/g,
replacement: (m) => `\`\`\`modal trigger="${m[1]}"\n${m[2].trim()}\n\`\`\``
},
{
pattern: /<div class="grid">([\s\S]+?)<\/div>/g,
replacement: (m) => `\`\`\`layout grid cols=2\n${convertDivCellsToCards(m[1])}\n\`\`\``
}
];
Prompt 加固:
Don't use raw HTML. Use Rho DSL instead:
- <details> → ```modal trigger="..."
- <div class="grid"> → ```layout grid cols=N + :::card
- <summary> / accordions → ```modal or ```tabs
All output must be valid markdown + Rho DSL only.
Mistake 5: directive 没在 container 里
症状:
:::card accent=blue
content
:::
(独立出现,不在 layout 里)
原因:LLM 误把 directive 当 standalone DSL。
自动修复(包装 layout):
function wrapOrphanDirectives(markdown: string): string {
// 找独立 :::card / :::tab / :::step / :::event
// 上下文:前面没有 ```layout/tabs/stepper/timeline 开头
// 修复:包成对应 layout / tabs / stepper / timeline
// 实现略 — 需要 markdown AST 分析
}
Prompt 加固:
Directives MUST be inside their proper container:
- :::card → must be inside ```layout grid cols=N + ```
- :::tab → must be inside ```tabs + ```
- :::step → must be inside ```stepper + ```
- :::event → must be inside ```timeline + ```
Never write a :::card outside a ```layout fenced block.
Mistake 6: vega-lite spec 是无效 JSON
症状:
template vega-lite:
{
"mark": "line"
"encoding": { ... } ← 缺逗号
}
原因:LLM 写复杂 nested object 容易丢逗号 / 漏引号。
自动修复(JSON 修复 lib):
import { jsonrepair } from 'jsonrepair';
function fixVegaLiteJSON(spec: string): string {
try {
JSON.parse(spec);
return spec;
} catch {
try {
return jsonrepair(spec); // npm: jsonrepair
} catch {
throw new Error('Vega-Lite spec invalid even after repair');
}
}
}
Prompt 加固:
Vega-Lite spec must be strict JSON:
- All keys in double quotes
- Comma between every key:value pair
- No trailing commas
- Use https://vega.github.io/editor/ format as reference
Mistake 7: SVG 含 script / event handlers
症状:
template svg:
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" onclick="alert('hi')"/>
<script>console.log('rendered')</script>
</svg>
原因:LLM 学过 interactive SVG 的 web 套路。
自动修复(SVG sanitizer 库):
import DOMPurify from 'isomorphic-dompurify';
function sanitizeSVGTemplate(svg: string): string {
return DOMPurify.sanitize(svg, {
USE_PROFILES: { svg: true },
FORBID_TAGS: ['script', 'foreignObject'],
FORBID_ATTR: [/^on/] // 全部 on* 属性
});
}
@rho/md 内置 sanitizer 会自动处理——本 patch 是给 server 端早期校验加层防御。
Prompt 加固:
SVG templates: never use <script>, <foreignObject>, on* event attributes (onclick, onload, etc.), or javascript: URLs.
For interactivity, drive SVG attributes from sliders/timers via {expr} placeholders.
Mistake 8: format spec 错
症状:
template stl: [BMI] -> [{bmi:.1}] ← 缺 type 字符(应是 .1f)
template stl: [BMI] -> [{bmi:1f}] ← 缺 . 前缀
原因:LLM 不熟 Python f-string-style format spec。
自动修复(regex):
function fixFormatSpec(text: string): string {
return text
// {x:.1} → {x:.1f}
.replace(/\{(\w+):(\.\d+)\}/g, '{$1$2f}')
// {x:1f} → {x:.1f}
.replace(/\{(\w+):(\d+)f\}/g, '{$1:.$2f}');
}
Prompt 加固:
Format spec syntax (similar to Python f-string):
- {value:.2f} - 2 decimal places (always include the .)
- {value:.0f} - 0 decimals (integer)
- {value:%} - percentage (auto ×100)
NEVER use {value:.1} (missing type char) or {value:1f} (missing dot).
Mistake 9: computed 循环依赖
症状:
\```interact
computed a = b + 1
computed b = a + 1 ← circular
\```
原因:LLM 没意识到表达式之间的 dependency 必须无环。
自动修复(拓扑排序检测):
function detectCycle(computeds: Computed[]): boolean {
const graph: Map<string, Set<string>> = new Map();
for (const c of computeds) {
graph.set(c.name, new Set(extractIdentifiers(c.expression)));
}
// DFS 检环
const visiting = new Set<string>();
const visited = new Set<string>();
function dfs(node: string): boolean {
if (visiting.has(node)) return true;
if (visited.has(node)) return false;
visiting.add(node);
for (const dep of graph.get(node) || []) {
if (graph.has(dep) && dfs(dep)) return true;
}
visiting.delete(node);
visited.add(node);
return false;
}
for (const c of computeds) {
if (dfs(c.name)) return true;
}
return false;
}
Prompt 加固:
Computed values must form a DAG (no cycles):
- ✅ `computed a = sliderX + 1; computed b = a * 2` (b depends on a, OK)
- ❌ `computed a = b + 1; computed b = a + 1` (cycle)
For "previous-value" patterns, use button self-reference instead.
Mistake 10: 引用未声明变量
症状:
\```interact
slider x 0 10 5 1
template stl: [{y}] ← y 未声明
\```
原因:LLM 笔误 / 想象有变量但忘加 slider。
自动修复(symbol table check):
function detectUndeclaredVars(block: InteractBlock): Issue[] {
const declared = new Set<string>([
...block.controls.map(c => c.name),
...(block.computed || []).map(c => c.name),
'pi', 'e' // built-in constants
]);
const refs = extractTemplateRefs(block.template);
return refs
.filter(r => !declared.has(r))
.map(r => ({
block,
message: `Template references undeclared "${r}"`
}));
}
Prompt 加固:
Every variable referenced in `{...}` placeholders must first be declared via:
- slider name min max initial step
- input name "default"
- select name "opt1" "opt2"
- toggle name true|false
- button name "label" value
- computed name = expression
Or it must be a built-in constant: pi, e.
Bonus: 完整 server-side validation pipeline
async function validateAndRepair(llmOutput: string): Promise<{ markdown: string; warnings: string[] }> {
let md = llmOutput;
const warnings: string[] = [];
// 1. Auto-fix Math.X / Number.X
const fixed1 = fixMathPrefix(md);
if (fixed1 !== md) { warnings.push('Removed Math/Number prefixes'); md = fixed1; }
// 2. Auto-fix if/else → ternary
const fixed2 = fixIfElse(md);
if (fixed2 !== md) { warnings.push('Converted if/else to ternary'); md = fixed2; }
// 3. Auto-fix HTML → Rho DSL
const fixed3 = convertHTMLToRho(md);
if (fixed3 !== md) { warnings.push('Converted HTML tags to Rho DSL'); md = fixed3; }
// 4. Sanitize SVG
md = sanitizeAllSVGTemplates(md);
// 5. Repair vega-lite JSON
md = repairAllVegaSpecs(md);
// 6. Fix format specs
md = fixFormatSpec(md);
// 7. Final parse check
const ast = await parseRho(md);
const issues = [
...detectOrphanDirectives(ast),
...detectMissingNamespace(ast),
...detectCircularComputeds(ast),
...detectUndeclaredVars(ast),
];
if (issues.length > 0) {
// 不可自动修复 → 重新 prompt LLM 或返回错误
throw new RhoValidationError(issues);
}
return { markdown: md, warnings };
}
Re-prompt strategy
如果自动修复失败 / 校验仍出错:
async function generateWithRetry(userPrompt: string, maxRetries = 3) {
let lastError: string | null = null;
for (let i = 0; i < maxRetries; i++) {
const messages = [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userPrompt },
];
if (lastError) {
messages.push({
role: 'user',
content: `Previous attempt had error: ${lastError}\nPlease fix and try again.`
});
}
const llmOutput = await llm.generate({ messages });
try {
return await validateAndRepair(llmOutput);
} catch (e) {
lastError = String(e);
}
}
throw new Error('Max retries exceeded');
}
See also
- Prompt templates — 主动避免错误的 prompt
- Schema-aware generation — schema 减少错误源
- AINP 协议 — §21 单页速查表 — 全部围栏与 spec 行,一页看完(英文)
- AINP 协议 — §16 给 LLM 作者
- Writer's Guide common pitfalls — 写作者侧也常踩这些(人也会犯)