修复 spintax 模板
修复就是定位缺陷 → 套用该类缺陷唯一的标准配方 → 复查。绝不自由重写。它还遵守本系列教过的顺序:先结构,再变量,最后同义替换——属于变量层的问题,在同义替换层修不好。
为什么模板坏在渲染时,而不是源码里
validate() 评判的是模板。一个模板可以毫无瑕疵,却照样输出坏文本,因为缺陷藏在各个选择的组合里:相邻两个槽位恰好选到同一个词;一个槽位里的名词遇上另一个槽位里的关系代词,两者性别不一致;一个空分支留下一对光秃秃的括号。这些在源码里都看不见,读源码的人也看不出来——它们只出现在部分渲染结果里。
所以修复从渲染出的变体开始,而不是从模板开始。n8n 节点的 Lint 操作正是为此而生:它评判渲染结果,从不评判模板;在一个真实文本池上,第一轮 200 次渲染只有 2% 干净,修好它指出的槽位之后是 100%。那个池子里最常见的发现,就是一个槽位的名词撞上另一个槽位的代词——сюжет, в которой 出现在 18% 的文章里,才有人注意到。
实际结论:只读模板,缺陷一定会上线。渲染几十个变体,跑一遍 lint,修它指出的地方。
黄金法则:按配方修,不凭感觉修
缺陷有类别,每个类别只有一种标准修法。自由修改——无论是赶时间的人,还是被告知"修一下"的模型——都会在修语法的同时弄坏语法。让模型修 дополнение, который,它会欣然换上一个不同性别的同义词,交回 расширение, который:被问到的那个代词依旧是错的,还凭空多了一个分支。换成给它一个具名配方——把受支配的词绑进每个分支——它只做这一个变换,然后停下。
法则的另一半是顺序。语法指南为写作立过规矩:"排列、变量和句子形状没锁定之前,不要加 {a|b}。"换到修复上就是:结构 → 变量 → 同义替换。一个名词在三个格里被重复引起的缺陷,是变量层的问题;在它周围打同义替换的补丁,修好一次渲染,下一次照样坏。
两种修复,按这个顺序
- 语法修复——依据
validate()的诊断。每条诊断带行、列和代码;模型看到的是带行号的模板,被要求尽量少改,其余部分逐字节保持原样。@spintax/authoring-prompt负责构建这个提示词(buildRepairPrompt()),n8n 节点把它暴露为 Build Repair Prompt,Spintax Studio 的 Fix 按钮跑的也是同一轮。这一步报错就先做它——解析不了的模板没有渲染结果可 lint。 - 渲染 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}
{сайт|платформа} {удобен|удобна}
{сайт удобен|платформа удобна}
受支配的词:关系代词(俄语)
发现者: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-space、punctuation.space-before 和 punctuation.duplicated——从引擎这边你很少会见到它们:引擎的后处理在渲染时就把双空格和逗号前的空格规整掉了。它们是给在引擎之外拼装的文本、或关掉后处理来渲染的宿主准备的安全网。配方相同:标点进入它所属的分支。
变量:最强的修法
上面目录里一半的缺陷都有变量层的解药,而且胜过任何同义替换层的补丁,因为它直接消灭了缺陷赖以存在的那个组合。方法:
- 先提升,再同义替换。在全文反复出现的变格实体先进
#def;之后同义替换才在它周围的文本上跑。顺序反过来,行内同义词替换会悄悄改掉句子其余部分所一致的那个形式。在 wpps-ru 项目里,一篇文章仅凭这一步就从 200 次渲染 52 次干净到 200 次全干净:#def %Ext% = {дополнение|расширение},两者都是中性,行内同义词再也碰不到它。 - 给形式命名。主格用
%PascalCase%,用到的每个格加后缀——%OrderGen%、%OrderDat%、%OrderLoc%——遵循变量指南的约定。 - 让一个族只来自一次抽取——这是引擎让各形式相关的唯一方式。变格方式相同的同义词、词尾写在定义之外(
#def %e% = {магазин|сайт|проект}→для %e%а);或者一个#def词干、每个格形式都引用它(#def %s% = {посетител|гост}、#def %VisitorsGen% = %s%ей);或者整个短语放进一个枚举。两个独立的定义就是两次抽取,永远不相关——#set不行,#def也不行。 - 凡是两次提及必须一致的,用
#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-pair、punctuation.double-space、punctuation.space-before、punctuation.duplicated | 把标点挪进它所属的分支 | 本页 |
记住一句话:定位,说出配方名,套用,复查。如果你看到的东西没有配方,那缺陷就在结构或变量里,它要在本指南之前的那几篇里修。