修复 spintax 模板

修复就是定位缺陷 → 套用该类缺陷唯一的标准配方 → 复查。绝不自由重写。它还遵守本系列教过的顺序:先结构,再变量,最后同义替换——属于变量层的问题,在同义替换层修不好。

为什么模板坏在渲染时,而不是源码里

validate() 评判的是模板。一个模板可以毫无瑕疵,却照样输出坏文本,因为缺陷藏在各个选择的组合里:相邻两个槽位恰好选到同一个词;一个槽位里的名词遇上另一个槽位里的关系代词,两者性别不一致;一个空分支留下一对光秃秃的括号。这些在源码里都看不见,读源码的人也看不出来——它们只出现在部分渲染结果里。

所以修复从渲染出的变体开始,而不是从模板开始。n8n 节点的 Lint 操作正是为此而生:它评判渲染结果,从不评判模板;在一个真实文本池上,第一轮 200 次渲染只有 2% 干净,修好它指出的槽位之后是 100%。那个池子里最常见的发现,就是一个槽位的名词撞上另一个槽位的代词——сюжет, в которой 出现在 18% 的文章里,才有人注意到。

实际结论:只读模板,缺陷一定会上线。渲染几十个变体,跑一遍 lint,修它指出的地方。

黄金法则:按配方修,不凭感觉修

缺陷有类别,每个类别只有一种标准修法。自由修改——无论是赶时间的人,还是被告知"修一下"的模型——都会在修语法的同时弄坏语法。让模型修 дополнение, который,它会欣然换上一个不同性别的同义词,交回 расширение, который:被问到的那个代词依旧是错的,还凭空多了一个分支。换成给它一个具名配方——把受支配的词绑进每个分支——它只做这一个变换,然后停下。

法则的另一半是顺序。语法指南为写作立过规矩:"排列、变量和句子形状没锁定之前,不要加 {a|b}。"换到修复上就是:结构 → 变量 → 同义替换。一个名词在三个格里被重复引起的缺陷,是变量层的问题;在它周围打同义替换的补丁,修好一次渲染,下一次照样坏。

两种修复,按这个顺序

  1. 语法修复——依据 validate() 的诊断。每条诊断带行、列和代码;模型看到的是带行号的模板,被要求尽量少改,其余部分逐字节保持原样。@spintax/authoring-prompt 负责构建这个提示词(buildRepairPrompt()),n8n 节点把它暴露为 Build Repair Prompt,Spintax Studio 的 Fix 按钮跑的也是同一轮。这一步报错就先做它——解析不了的模板没有渲染结果可 lint。
  2. 渲染 lint 修复——依据渲染变体上的发现。一条发现带代码、出问题的片段和一句说明,但没有模板里的位置。本指南讲的就是这一步:把代码对应到配方,找到产生该片段的分支,套用配方,重新渲染,重新 lint。

缺陷 → 配方目录

下面每一类都写明由谁发现——lint 按代码发现,或你读样本发现——并给出唯一的配方,附"之前"和"之后"。"之前"的渲染是引擎的真实输出;lint 消息按 n8n 节点 lint 的原样引用。每个配方背后的语法只在语法安全指南里讲一次;链接直达对应段落。

冠词或限定词(英语)

发现者:你,在样本里——机器查不出来。症状:a encrypted connection配方:把限定词绑进分支。

a {secure|encrypted} connection
{a secure|an encrypted} connection

讲解见冠词与限定词的绑定

主语与谓语、性与数

发现者:你,在样本里。症状:the team respond;俄语里是 сайт удобнакоманда доступны配方:把主语和谓语绑在一个分支里。两个独立的槽位永远无法对齐;一个同时装着两半的槽位永远对齐。

{the team|all members} {responds|respond}
{the team responds|all members respond}
{сайт|платформа} {удобен|удобна}
{сайт удобен|платформа удобна}

讲解见英语的主谓一致性与数EN

受支配的词:关系代词(俄语)

发现者:lint,代码 agreement.relative。它从 который 的每个形式向前最多回看两个词,按词尾猜名词的性,词尾有歧义时(путьтень)宁可沉默也不瞎猜。症状,lint 原文如下:

"дополнение … который" — neuter noun with a masculine relative pronoun

配方 A:把受支配的词绑进每个分支,用该分支需要的形式。

{дополнение|плагин}, который ускоряет сайт
{дополнение, которое|плагин, который} ускоряет сайт

40 次渲染里,lint 标出了第一个模板的 18 次,第二个模板零次。

配方 B:把名词提升成同一性别的 #def,当它反复出现时。挑同一性别的同义词,只抽一次,之后每次提及天然一致:

#def %Ext% = {дополнение|расширение}
%Ext%, которое ускоряет сайт

同样 40 次全干净。配方 B 附带的条件:它只在名词始终处于同一个格时成立。单独声明的 %ExtGen% 不会和 %Ext% 相关——两个定义就是两次抽取——所以需要多个格的名词要走格族阶梯EN:变格方式相同的同义词、词尾写在定义之外,或者一个 #def 词干、每个格形式都引用它。

介词 + 格(俄语、德语)

发现者:你,在样本里——lint 不评判槽位内部的格。症状:после первом заказеfür dem Team配方:介词连同它支配的形式一起进入分支。

{после|при} первом заказе
{после первого заказа|при первом заказе}
{mit|für} dem Team
{mit dem Team|für das Team}

同一个名词在多处被支配时,另一条路是格形式变量——%OrderGen%%OrderLoc%——按族声明。讲解见介词 + 受支配形式EN

重复的词根

发现者:lint,代码 repeat.word。它扫描六个词的窗口,所以能抓到相距最多五个词的重复;它比较的是词干,所以 вопрос / вопросов算同一个词。六是实测出来的:窗口为九时 82% 的命中是正常的文本衔接,为六时每次命中都是真的。症状,原样打印:

"сразу" and "сразу" repeat 4 words apart

配方:词根只留在一个槽位里。要么枚举带着它,要么固定文本带着它,绝不两边都有;重复的是一个实体时,把它提升成 #def,只提一次。

{Сразу|Немедленно} после установки сайт сразу ускоряется
{Сразу|Немедленно} после установки сайт начинает отвечать быстрее

修之前 40 次渲染标出 18 次,修之后零次。英语的形状一样——重复词冲突展示了 need to acquire… can acquire

lint 看不见的接缝。排列会让任意两个块成为邻居,于是标题和紧随其后的导语可能隔着块边界共用一个词根。lint 刻意不标记"标题—正文"的重复("正常的文本衔接"就住在那里),所以接缝靠源文本的纪律——在边界处把词汇分开——或者靠对拼装完成的整篇模板再跑一遍 lint。

语域与槽位同质性

发现者:你,在样本里。症状:正式分支挨着口语分支,或者一个叙事连接词在段落中途改变语域(Therefore / So yeah)。配方:一个槽位一种语域;把连接词钉死。删掉不属于这里的分支,而不是重写整个槽位。规则见同义替换规则

留在分支里的标点

发现者:lint,代码 punctuation.empty-pair症状:empty brackets or quotes——一个可选分支把自己的外壳留在了外面。配方:把标点挪进需要它的那个分支。

Works in any browser ({fast|})
Works in any browser{ (fast)|}

修之前 40 次渲染 18 次干净,修之后 40 次全干净。

同一家族还有三个代码——punctuation.double-spacepunctuation.space-beforepunctuation.duplicated——从引擎这边你很少会见到它们:引擎的后处理在渲染时就把双空格和逗号前的空格规整掉了。它们是给在引擎之外拼装的文本、或关掉后处理来渲染的宿主准备的安全网。配方相同:标点进入它所属的分支。

变量:最强的修法

上面目录里一半的缺陷都有变量层的解药,而且胜过任何同义替换层的补丁,因为它直接消灭了缺陷赖以存在的那个组合。方法:

  1. 先提升,再同义替换。在全文反复出现的变格实体先进 #def;之后同义替换才在它周围的文本上跑。顺序反过来,行内同义词替换会悄悄改掉句子其余部分所一致的那个形式。在 wpps-ru 项目里,一篇文章仅凭这一步就从 200 次渲染 52 次干净到 200 次全干净#def %Ext% = {дополнение|расширение},两者都是中性,行内同义词再也碰不到它。
  2. 给形式命名。主格用 %PascalCase%,用到的每个格加后缀——%OrderGen%%OrderDat%%OrderLoc%——遵循变量指南的约定。
  3. 让一个族只来自一次抽取——这是引擎让各形式相关的唯一方式。变格方式相同的同义词、词尾写在定义之外(#def %e% = {магазин|сайт|проект}для %e%а);或者一个 #def 词干、每个格形式都引用它(#def %s% = {посетител|гост}#def %VisitorsGen% = %s%ей);或者整个短语放进一个枚举。两个独立的定义就是两次抽取,永远不相关——#set 不行,#def 也不行
  4. 凡是两次提及必须一致的,用 #def,不用 #set#set 每次引用都重抽,是你想要变化时的工具;#def 每次渲染只抽一次。

什么时候不要 spin

有些片段不是修,而是整个从枚举里拿出来。专有名词、品牌名、价格、URL 和法律措辞永远不进分支。带着一长串修饰成分的名词也一样——形容词、分词和关系从句都要与它一致——因为每个分支都得背着整条尾巴。在它们周围做变化,它们本身固定不动。

用 LLM 修复

给模型四样东西、拒绝第五样,它就修得好:模板、带片段的具体缺陷、要套用的具名配方、以及"只做这一件"的指令——不给任何改进什么的许可。请求从来不是"修一下",而是"对产生 Y 的片段套用配方 X"。下面这个提示词就是在真实材料上站住了的形状。

You are repairing a spintax template. Do NOT rewrite freely and do NOT improve the wording.

A lint pass over rendered variants found this defect:
  code:     <agreement.relative | repeat.word | punctuation.empty-pair | …>
  fragment: "<the offending rendered text>"
  message:  "<the lint message>"

Apply exactly ONE recipe from https://spintax.net/docs/repairing-spintax.md
  recipe: <its name, e.g. "bind the governed word into each branch">
to the branch or branches that produced the fragment — and to nothing else.

Rules:
- Keep meaning, structure, variables and every other branch byte-for-byte unchanged.
- Do not add words, clauses, prepositions or relative pronouns the template does not
  already have.
- If the recipe cannot be applied without changing meaning, leave the template as it is
  and reply: unfixable
- Return only the corrected template — no explanation, no quotes, no code fences.

同一个项目里的两个习惯让模型不越线。仅替换式标注:模型给成品文本加标记时,只能替换词和短语,不能引入源文本里没有的连词、从句、介词和关系代词——凭空发明的 который 和凭空发明的介词是大多数一致关系和格缺陷的来源,这条规则把它们清除了。以及对每个标注完的段落跑一遍 lint,因为仅替换式也会跑偏:有一段回来时成了 {Совпадает|Совпадает ли} — careful, no. {Совпадает ли|…}——英文的思考过程漏进了俄语分支,外加一个重复的词根。循环抓住了它;人不会。

再给模型两个指针:模型在 spintax 里会犯的九个错误,以及语法安全指南里的语法审计提示片段——它是这个提示词在写作侧的孪生。

自动化修复

对流水线来说,把三个长篇模板推到 200 次渲染全部 lint 干净的形状是这样,按段落:

draft = markup(concrete_text)            # substitution-only
repeat up to K:
  errors = validate(draft)
  if errors:        draft = repair(draft, errors); continue   # syntax: the model fixes the spans
  if any(lint(render(draft, seed_i)) for i in 1..N):
                    draft = markup(concrete_text); continue   # dirty: regenerate, do not free-fix
  return draft                                                # clean
fail loud

注意这里的不对称:语法错误由模型按精确的位置修;lint 发现要么把段落送回去重新生成,要么——在循环的完整版里——对该片段套用目录中的具名配方来修。循环从不让模型自由地修 lint 缺陷。每段 N = 8 次渲染、K = 3 次尝试之下,项目里的第二个模板从 200 次 95 次干净到 200 次全干净

在 n8n 里,同一个循环是四个节点:Validate 把条目路由到 Valid 或 Invalid,Build Repair Prompt 把 Invalid 输出变成给你的 LLM 节点的修复提示词(给循环设上限,两次足够),Render Many 抽样本,Lint 把样本路由到 Clean 或 Defective——只有抽到的每一份文档都干净,样本才算干净。同一套规则也能作为普通函数跑在任何文本上——项目自己的流水线跑的就是它自己的一份副本,节点的 lint 正是从那里移植来的。

接线之前要知道的三件事:

  • 溯源得你自己搭。lint 发现带的是渲染出的片段,不是模板里的位置。要套用配方,就得找到产生片段的分支——在模板里逐个分支搜片段的词——每个修复工具最后都得写这块管道。
  • 抽样是概率性的。每段八次渲染抓得住常见组合;罕见的那个要在量上才会浮现。对拼装完成的模板的最终关卡——200 次渲染、每次都 lint——作为安全网保留着,lint 的抽样操作就是为它造的(默认 50 次渲染,最多 500)。
  • 接缝仍归人管。按段落的循环看不到跨块边界的重复;如上所述,整篇模板的那一遍或源文本的纪律负责守住它们。

速查表

缺陷发现者配方讲解
冠词 / 限定词(EN)你,在样本里把限定词绑进分支限定词
主语与谓语、性与数你,在样本里把主语和谓语绑在一个分支里一致关系性与数EN
关系代词不一致(RU)lint agreement.relative把受支配的词绑进每个分支;或把名词提升成同一性别的 #def格族EN
介词 + 格(RU、DE)你,在样本里介词连同形式一起进入分支;或用格形式变量介词 + 格EN
动词支配、量词(RU)你,在样本里把动词和宾语 / 量词和名词绑在一个分支里支配量词EN
重复的词根lint repeat.word词根只留在一个槽位;实体提升成 #def,只提一次冲突
语域 / 槽位同质性你,在样本里一个槽位一种语域;连接词钉死规则
空括号对、多余空格、重复标点lint punctuation.empty-pairpunctuation.double-spacepunctuation.space-beforepunctuation.duplicated把标点挪进它所属的分支本页

记住一句话:定位,说出配方名,套用,复查。如果你看到的东西没有配方,那缺陷就在结构或变量里,它要在本指南之前的那几篇里修。


继续本系列