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