序言
本篇内容并非展示,仅仅只是作为已经结束的项目的复盘。
我当时想做个东西辅助听力练习,它死了,下面是从里面捡出来的东西。
这里可以先说个有关的结论:使用 NotebookLM 这样成熟的产品,足以支撑“听力练习”这个目标。至于反问:“为什么我不直接用 NotebookLM”,这个问题留到最后回答。
本文涉及到一些 vibe coding 和 LLM 开发的相关经验,如果能为你所用,那再好不过。
技术细节和去掉项目特征后的通用经验放在附录。
一、一个朴素的想法
大约在 26 年 3-4 月份,我尝试了 Fish Audio 当时的最新 TTS 模型 S2,模型惊人的流畅度和零样本能力让我感到震惊。实际上,其听感和真人的差异已经很小。
我同时也在考虑语言学习相关的问题,加上对 LLM 能力的评估,自然地产生一个想法:在语言学习的听力侧,如果能够用自己喜欢的音色搭配自己感兴趣的材料,就能创造一个绝佳的场景:在收听自己喜欢的内容的同时,提升听力水平。
然而,那时我还没明白,自己面对的是一道由不成熟的想法本身造成的鸿沟。
随后我发现,有 AI 的指导,尝试部署本地 TTS 模型等工作其实都不算困难。可以这样说,看起来像是魔法的那部分:TTS 和音色克隆——解决得如此轻松,而看起来最简单的部分才是瓶颈,就是内容本身。
二、问题被一次次重述
我尝试按时间梳理。从当时的做法,到不断暴露出来的新问题。
1. 长度解决了,内容变平庸
首先遇到的问题是输出截断,Web 端通常都被限制了一次对话的输出长度。几乎不能做到一次生成 20k token 以上的长文。
然后我尝试使用深度研究功能,其能够加载一些预设提示词,同时有很长的单次输出额度。于是最开始的试验是:将一份高质量的人类长视频原文转化成风格化的播客文本。
产生的问题也很简单,输出的东西质量太低。
- 输出倾向摘要,或是整篇表达,情绪沉稳,典型的 LLM 式的垃圾。
- 既要压制模型的先验幻觉,又要试图让其生成额外的内容,这两个要求本身是冲突的。
我此时开始觉察到:音色既然如此廉价,那我究竟想要的是什么?
音色并不能带来全部,文稿背后的思考模式和文面本身的表达习惯,都是我想要的正文质量的一部分。
准确来说,是“像是目标 host 自己写出的”一部分。即一个真正的 host,会考虑选什么,按什么顺序讲,措辞/音色只是浮在文章最表面的那层。
2. 上下文流水线与白熊
这个时候,我产生了简单的上下文控制的想法:一次输入的上下文中,哪些块是固定的,哪些块是“动态”的。
最后实际流水线的层级如下:
| 层级 | 类型 |
|---|---|
| 【任务描述】 | 固定 |
| 【host 描述】 | 固定 |
| 【全局大纲】 | 固定 |
| 【目标材料】 | 动态:随流水线替换 |
| 【上一段末尾】 | 动态:随流水线替换 |
全局大纲负责“身处全稿的哪处”的信息,和部分全局叙事节奏。
目标材料则尝试给出所需的内容——什么是所需内容,不妨从常见的问题模式反过来想:
- 模型写到了这段不应该出现的,后文的情节;
- 重复了前文已经出现过的某些事实或内容?
做法就是:不要给其能写出这些内容的材料,同时用指令让其专注在编辑局部内容上。
似乎是废话,但我展开来解释。任何“禁令”因其自身的结构是有代价的,在写下前需要考量。即不要尝试这样的指令:“不要输出后续的 xxx 情节”,因为这本身也是信息,虽然是负向的。如果模型听从了,无事发生,流水线继续运转。但如果模型犯错,没有理解好呢?
指令起效时收益小,因为本身是对上下文空间的一种占用,而且在我的观察中,负向指令还存在反向激活被禁止的内容的可能。
所以相比之下,不妨直接把后续材料移出上下文,让模型无从写起。即能用信息缺席解决的,就不要用信息存在去禁止。
这看起来像是不言自明的,但其实是基于思考上下文控制的某种第一性原理。类比接近的话,就是“不要提到白熊”。
但是也存在做法后半句的边界:未来的内容可以靠不提供来解决;过去已生成的内容不行,模型避不开它看不到的重复,所以需要最小的、定向的提示。
总的来说,核心在于控制可见性,下禁令只是某种特定的工具。
如果这层继续抽象出来,即是单次任务指令的编写成功与否,实际上不依赖指令本身的复杂性、说明性——而在于是否给到足以完成此任务的信息,当然也要考虑模型能力,更详尽的讨论,附录 C 可能有帮助。
3. 信息上限
上面那条流水线跑通之后,继续展现出的问题是——它只是在换一套表达习惯。
原因有两个:
- 原稿是视频稿,想要转化成播客,就需要补上大量描述性的细节,而流水线里没有承载这些东西的结构。
- 没有新信息进入,系统能做到的上限就是“改写”。
于是我重新从这样一个问题出发:人类编辑是怎么写长稿的?他会先想清楚材料之间是什么关系、从哪讲起;写到一半发现缺东西,会回头去查;某个信息只在需要的时候才拿出来。
带着这个问题,我搜寻了长文生成领域的相关工作和资料。大部分方向都不符合。
最后我认为真正比较有用的是三个样本,算是各自回答了一个问题:
- 材料怎么组织:GraphRAG。材料被拆成节点和关系之后,“讲什么、从哪个粒度讲”变成了一个可以操作的问题。
- 写作过程怎么规划:WriteHERE(Beyond Outlining)。规划和写作交错进行,超越“先定死大纲再填空”的思路。
- 信息什么时候可见:酒馆(SillyTavern)的世界书。提到了才出现,没提到就当不存在,和第二节第 2 小节的“控制可见性”是相通的。
这三个样本的具体做法,见附录 A。
但绕了一圈,最根本的问题还在:播客稿多出来的那些细节,究竟从哪来?要么来自模型自己,有幻觉风险;要么来自更多的外部资料,而这会引出另一个问题,即多来源的去重与置信度分级,而下文实际系统会将这层排除在架构之外,目的是避免维护又一套复杂的数据流水线,也是考虑重复造轮子的代价。
4. 图驱动
这个想法一度令我很兴奋。
把人物、事件、背景、话题都变成节点,节点本身有固定标签和任意外挂标签,节点之间连上有向关系。因为最终的音频是线性的,讲述本身就等价于在图上走一条路径。
通过这样的建模,host 的个性有了一个实在的定义:路径的特征就是 host 的个性。
- 只挑主线上的重要节点、快速串起来,对应一个擅长把控主线、节奏快的 host。
- 愿意停在细节节点上、追求覆盖全面,对应另一种 host。
- 偏向选择含特定标签的节点,例如一个关注剧情主线,另一个更偏好周边轶闻。
指令也是一样,“讲个梗概”和“讲详细点”,本质上都只是在约束路径的形状。
当然,最后实现中它塌缩成了一个简单得多的东西:节点的有序选择。边、权重、回访刷新只是一次思考。最后的实现效果只是“按某种策略挑出一串节点,然后逐个写”。
在“路径特征就是 host 的个性”这个想法里,有一部分路径特征是可以事先规划的,比如挑哪些节点、按什么顺序;另一部分则适合边走边决定。前者系统实现了,而后者需要的是一个完全不同的架构。更多讨论见附录 B。
三、然后我搭了一个系统
嘛,既然开头已经说了,这是死掉的项目。那么就算是为在残骸中寻找些有价值的想法或者经验的目的来说,还是简要介绍一下 Tracecast 吧。
一句话概括它做的事:把本地的原始材料喂进去,吐出一份长篇播客稿。链条如下:
材料切片 → 候选节点 → 选路径 → 节点编辑 → 分块渲染 → 审计 → 拼接
前两步对应第二节第 4 小节的图:材料被切成最小的事实单元,再聚成候选节点。“选路径”是 host 个性进入的地方。节点编辑在单个节点内部整理材料。分块渲染是第二节第 2 小节那条上下文流水线的后代,每一块只看得见自己该看见的东西。审计和拼接负责收尾。
看起来每一步都有来由,或对应着前文的某些思考。问题也恰出在这里。
1. 错误一:先验架构的代价
上面这条链里的每个阶段,其实都是一个假设:图结构能让内容更好,不同的路径能被听出差别,节点内编辑比直接喂原材料强……
这些假设一个都没有被单独验证过。实现是先把整条链搭起来,再端到端地看最终产物好不好。最终产物不好的时候,判断是哪一环出了问题就是一个几乎不可能的任务。更糟的是,判断本身成本很高,因为每一环背后都有架构、测试和文档。工程投入越多,代价越高。
唯一做过消融的是图的边。因为层与层之间是解耦的,所以我可以在节点选择层隔离着做:拿掉边,看选择结果有没有变化。结果是没有可观测的影响,于是我决定去掉这个机制,也是前文提到的“路径”退化成了“有序选择”。
面对复杂的黑盒,这种局部消融几乎是唯一可行的办法,但它有自己的前提:局部指标得对。数据量够不够、设计是否完备、“选择结果没变”究竟意味着“边没用”还是“边没用好”,这些都没法完全确认。最后选择删掉它,算是在证据不足时做的一次取舍。
那么,如果更早、更粗糙地去验证呢?比如拿一份材料手工建图,挑两条风格不同的路径各渲染一遍,直接去听。这看起来测的正是最终在意的东西,但它同样有如下问题:除非一条极其有趣、一条特别无聊,细微的差别靠耳朵很难稳定地区分,听一两次的感受也撑不起结论。端到端的观察并没有绕开指标问题,只是把指标换成了更模糊的“感觉”。
也就是动手之前先回答一个问题:如果这个阶段有效,会在哪里、用什么看到差别?局部消融需要可信的局部指标,端到端试听需要能区分的判据,两者都要求先有一个可检验的假设。答不出这个问题,这个阶段的价值就是不可证伪的,它不该先被搭建,至少不该建得过重。
现在看,一般个人开发者选择演进式的开发方式是更合理的。
2. 错误二:被 agent 的实现带偏
播客稿需要细节,这是第二节第 3 小节的结论。细节不能来自模型自己,否则就是幻觉,所以需要来源。既然细节必须来自来源,就得保证正文里的每个事实都能追溯回某个材料单元。要保证可追溯,就要有稳定的 ID、lineage、每次调用的记录、校验报告……
审计成了首要的东西。到最后,系统规范里最硬的一条是“只有原始材料能授权正文事实”,host、风格样本、指令统统不算。这条规则本身没错。但一个以“想听”为起点的项目,最严格的约束却是关于事实来源的,这就说明重心已经偏了。
本意是想保全内容的可读性、正确性,现在发现其实有更合适的处理层级,即一个合适的后校验层。见下一小节的相关结论。
我想要的是一份我会主动去听的播客稿,但实际得到的是一个可靠、不会编造、但很无聊的东西。
为什么会这样?我觉得这和用 agent 写代码这件事本身有关。agent 擅长推进能被校验的东西,schema、测试、lineage 都能一步步收敛,而“有趣”没法校验,于是在开发过程中被逐渐推向边缘。
3. agent 会放大每一处没想清楚的地方
再看这样一个实例。
在最开始的仓库中,规范中已经定义好了必要字段。然后 agent 在默认实现下,开始让流水线中被调用的模型原样搬运 16 位 hash。成百上千个 JSON 在里面流转——接受这样的输入,再原样输出出去,如果犯错了还会被字段校验拦下来。
模型有能力意识到这样做不合理,因为你指出问题后,它会修改,甚至意识到这样的实现很愚蠢:让模型复制随机串既容易出错又浪费。但模型的先验里没有好的既有实现,而架构者又不去规范如何做,并采取先验式的开发方式,只会是一场灾难。
实际做法:正式产物里只保存稳定的全局 ID,每次调用前,把 ID 投影成 n1、n2 这样的短别名,别名只在这一次调用里有效;模型输出别名,再由 parser 展开回 ID 并校验。模型只负责“选哪个、为什么”,ID 的生成、映射和一致性全部交还给程序。
这和前文白熊一节其实机制类似:生成时给局部上下文,是为了不让模型越界;而 agent 改代码时只看得见眼前的模块,于是常常做出局部合理、全局有害的修改。区别只在于任务是否真的是局部的,而判断这一点本身就需要全局视野,需要架构者的那部分判断。
应对是把计划和实现分开,或者交给一个没有参与实现的 agent 来审查,在决策和执行之间留出一个空间用于审视,其实也都是常见的经验。
还有与之相关的结论是:
能后加的放后面。 审计、校验、专名统一、各种报告,本质上都是在一个已经值得听的东西上加保险。它们什么时候加都行,晚一点加不会损失什么。
必须设计进生成里的放前面。 文章本身质量,有趣与否不能在生成之后补救,它取决于选什么、怎么排、在哪里停下来,这些在生成那一刻就得存在。如果最初的版本里没有它,后面堆再多的层级也长不出来。
四、结局
项目已经结束。就目前的产出来说,我不太愿意听。而对一个为自己做的东西来说,口味即是验收标准——这便宣判了项目的死刑。
其实仔细分析的话,我是会被“有人在其中”的内容吸引。
举个实例,要讲解“拳皇97”,一份文稿可以把游戏背景故事,按照播客节目的目标时长,精确分配情绪密度,事件节点,进而抓住听众的注意力;或者讲解硬核的格斗技巧或者策略,那么怎样讲解得直观、好理解本身就已经是难点了。
但只要一件简单的事,讲述者当年和朋友们在机厅如何切磋高下,如何观察形形色色的人们,就是这样个人化的旅程与体验——也已经足够吸引我,我不想要拔高什么,单纯觉得,这同样很有意思,也很必要,不是吗?
这样的经历本身就是材料,几乎只存在于讲述者的生活里。系统的审计性在这里和想要的东西发生冲突:要么有真实的经历作为来源,要么编造,而编造却是审计要防范的。
其实考虑了“比如作者自己口述的片段、访谈稿,作为原始材料进入系统”,但这涉及到的是去重等数据处理问题,即排除在架构外的 intake 层。
不过我也无力去做更多了,也许哪天类人 agent 做得足够好时,我会接受,或者以另一种姿态,即承认自己没有物理经验的方式,或许也会很有趣。
五、回到开头:为什么不直接用 NotebookLM
这是一个基线问题。对单纯的英语练习来说,增量接近零;对主动收听来说,需要的增量是人,两者都无法直接给出。
如果重来,我或许会做更便宜的验证:用现成产品处理自己喜欢的材料,听两周,记下各类感受,和客观的行为情况,例如是否主动打开、听完率、在哪里跳过、想重听的段落等。
主动收听无法保证,因为它取决于人和内容的匹配,只能靠闭环去逼近。靠的是不断的观测更新,而不是直接根据你的特定选择建立一个先验模型,类似的就是现代的内容推荐算法,它的思路是看行为。
当然也留下些东西:一批与项目无关的设计经验;也让我认识到数据驱动、研究方法、科学训练的重要性;还有工程管理。
收尾
真正的问题是两个目标混在了一起:十分钟的练习材料,和会主动听的精品长播客。后者的门槛其实是由口味决定的。
如果要落到具体行为上:
- 先找现成的基线;
- 先做最小验证,再搭架构;
- 动手前先找判据。
最后想提的就是,原生英语世界里已经有大量“有人在其中”的内容,瓶颈在于找到既感兴趣又听得懂的那部分。这是选择问题,不是生成问题。
附录
附录 A:三个参考样本
GraphRAG。 微软这篇工作的起点是一个观察:普通 RAG 在面向整个语料的全局问题上会失效,比如“这批数据的主题是什么”,因为这本质上是面向查询的摘要任务。它的做法是先用 LLM 从文档里抽出实体知识图谱,再为每一组关系紧密的实体预先生成社区摘要,而且摘要沿社区层级自底向上生成,高层递归地吸收低层。提问时,每个社区摘要各自产出部分回答,最后汇总成一个答案。它给我的启发不在“检索”,而在于材料一旦拆成节点和关系,“讲什么、从哪个粒度讲”就变得可操作了。(https://arxiv.org/abs/2404.16130)
Beyond Outlining(WriteHERE)。 它的批评点几乎就是我那条流水线的毛病:现有方法依赖预定义的工作流和僵硬的思维模式,写之前先生成大纲,导致写作过程缺乏适应性。它的方案是通过递归的任务分解,把检索、推理、写作三类基本任务动态组合起来,规划和执行交错进行。这和“人类编辑者”的直觉很接近:写到一半发现缺材料会回头去查,查到新东西会改后面的安排。(https://arxiv.org/abs/2503.08275)
酒馆(SillyTavern)的世界书。 World Info / Lorebook 是一种朴素但好用的上下文控制:每个条目挂着一组触发关键词,被激活时正文才插入 prompt;关键词、标题这些不在正文里的信息不会进入上下文,所以每个条目都应该是一段完整、自足的描述。它和“控制可见性”是同一件事,只不过由对话本身驱动。(https://docs.sillytavern.app/usage/worldinfo/)
附录 B:中心问题与显式状态
以一个领域讲解类的项目为例,节点是论文、问题、方法这类复杂材料。一种很直观的讲法是先选定一个中心问题,最终的路径会呈现这样的特征:反复回到这个中心问题,并且每次回来,它都和上次不太一样了。“我们刚才看了这篇论文,现在再回头看最初那个问题,它变成了什么样?”
这种结构看似复杂,其实仍然是路径特征能描述的东西,只是多了两个条件:允许路径回到同一个节点;回访时,这个节点的内容要被更新。
第二个条件意味着,中心问题不再是一份静态材料,而是一份会变化的状态。仔细拆开,host 对它做的其实是三件事:
- 每讲完一个节点,判断它是否改变了我们对中心问题的理解,改变了就记下来。
- 大部分时候只是把这份理解揣着,不说出口。
- 在合适的时机回到它,把当前的状态讲出来。
“揣着”和“说出口”是分开的,这一点很重要。如果每讲完一个节点都回头总结一遍,听感会非常机械;真正好的讲解者,大部分时间是带着问题在讲,只在少数节点停下来回望。如果中心不止一个,就是同时揣着几份这样的状态。
这在写作上并不新鲜,议论文里不断被修正的中心论点、叙事里反复出现的母题,都是同一回事。新的地方在于,放进生成流水线之后,它暴露出一个工程问题:这份状态由谁来保管?
两套状态
模型的每一次调用本身是无状态的。第二节第 2 小节流水线里传入的“上一段末尾”,已经是一份最小的状态,但它只管相邻两段的衔接,管不了全局。中心问题需要的是一份全局的、显式维护的状态。
最直接的设想是一张状态卡:当前对问题的表述、已经确立的部分、仍然悬而未决的部分、被推翻或修正过的部分。每写完一个节点更新一次,写下一个节点时把卡片放进上下文。
但回头看,这张卡只说对了一半,因为它把两种性质不同的状态混在了一起。
第一种是节点自身的状态。卡上的四个字段描述的都是“这个问题现在是什么样”,它们其实属于中心问题节点本身。节点不是一份静态材料,讲到别处时临时挂载进来的信息会改写它,让它推进到新的位置。这和第二节第 2 小节的可见性控制、世界书“提到了才出现”是同一个机制,只是方向反过来:不是把信息拿给模型看,而是把信息写回节点。节点能承载的语义几乎没有上限,这本身也是风险,只增不减的话,几轮之后它就会膨胀成第二份文稿。所以更新应该是改写而不是追加:保留当前版本,被推翻的部分压缩成一行。
第二种是讲述者自身的状态:现在处在哪个阶段,下一步是继续往下讲、回头看、去找材料,还是收尾。它不描述内容,只描述行为。这部分才是状态卡真正该承担的东西,而且是必要的。
前者回答“是什么”,后者回答“做什么”。“揣着”和“说出口”之所以能分开,正是因为它们落在两套状态上:“揣着”是对节点状态的一次写入,“说出口”是讲述者的一次决策。如果只有一张混合的卡,“有了更新”和“该说出来”就很容易被绑在一起,结果正是上面说的那种机械感。
两套状态之间还需要一个接口:讲述者要能从节点状态里看出,哪些悬而未决的部分已经“成熟”,值得回去讲了。
关于为什么没有跑起来
这个设想最终没有落地,但回头看,跑不起来几乎是必然的。我的实现是一条预先排好的流水线:先选出节点,定好顺序,再逐个写,本质上是一个 DAG。而中心问题要求的东西恰恰相反。回访本身不是障碍,如果规则是固定的(比如每三个节点回看一次),环完全可以预先展开。真正的障碍在于:什么时候该回去、刚写完的这个节点有没有改变中心问题,都要看刚刚生成出来的内容才能判断。控制流必须在运行时决定,这天然对应的是 agent,而不是工作流。
附录 C:一份实际文档
# Prompt Construction
- 状态:规范模板(跨项目通用)
- 读者:贡献者、维护者
- 事实来源:`<prompt 定义模块路径>`、`<control prompt 模块路径>`、`<共享 prompt component 模块路径>`、
各阶段 projection/parser 代码、`<prompt variant parity 测试路径>`、`<control prompt 测试路径>` 和
对应阶段行为测试
- 使用说明:本文档不绑定具体项目。落地到某个仓库时,把所有 `<...>` 占位符替换为该仓库的真实路径、
模块名与阶段名;示例中的抽象角色(如"入口阶段""下游阶段")替换为项目实际的服务/阶段名称。
本文约束运行时 system/user/repair prompt、共享 prompt component、代码生成的 model-facing 固定
文案、DSL 输出协议和调用拆分。阶段语义以 `<各阶段模块文档路径>` 为准,跨模块 artifact ownership
以 `<跨模块契约文档路径>` 为准。研究、实验和历史记录不自动成为本规范的依据。
## 先确定责任层
修改 prompt 前先判断问题属于哪一层:
| 需要解决的问题 | 责任层 |
|---|---|
| 结合上下文作语义判断或目标语言表达 | 模型与 prompt |
| 模型缺少完成判断所需的证据、分组或顺序 | projection / 调用拆分 |
| stable ID、alias 展开、覆盖、枚举、hash、schema、hard gate | 程序 |
| 输入本身不能支持目标结论 | 任务定义或 operator 输入 |
| transport、usage、raw payload、attempt ledger | 调用可观测性 / Observer 层 |
不要用附加指令掩盖信息缺失,也不要让模型自报程序可以确定性验证的条件。LLM 可以输出 DSL 或
plain text;parser/compiler 负责校验、stable ID 和 artifact rebuild。
## Prompt 必须自包含
模型没有仓库上下文。每个 model-facing 专名必须满足至少一项:
- 在本次 prompt 中定义;
- 是必须逐字使用的协议 literal;
- 是输入数据的一部分,并被明确标为数据;
- 属于完成任务所需的通用概念,删除会造成语义损失。
内部模块名不能替代任务说明。prompt 必须说明:输入是什么、模型拥有什么判断权、输出怎样被下游
消费、哪些内容是事实/控制/审计信息,以及错误会造成什么不可逆影响。
一个概念使用一个名称。call-local alias、stable global ID、schema 字段和 artifact 名称不得混称。
材料、指令、示例和输出协议必须用明确 heading、围栏或 block marker 分隔;输入中的命令式文本应
明确标为数据,不能改变调用协议。
## System 与 User 的分工
消息角色表达权限和稳定性:
System 放置:
- 跨调用稳定的任务、判断目标、阶段位置和下游影响;
- 事实来源、证据权限及不可改变的阶段责任;
- 稳定的语义取舍原则;
- 输入 payload 只作为数据的高优先级边界。
User 放置:
- 本次参数、target language、alias 范围和动态 allowed set;
- 当前输入结构、字段含义与 payload;
- 本次精确输出 shape、cardinality 和最小合法示例;
- 简短输出前检查。
换一批输入仍原样成立的规则倾向 system;随 payload、允许集合或输出 shape 变化的内容倾向 user。
不要为形式完整制造空标题。
## Projection 与模板约束
阶段配置中的 `system_prompt`、`user_prompt` 和可选 `repair_prompt` 指向 prompt 资产。路径可以包含
`{locale}` marker;loader 将它展开为项目支持的语言标签(如 `zh`、`en`)。没有 marker 的既有路径仍
按原字节加载。每个 Markdown prompt 第一行必须是:
```text
<!-- version: <label> -->
```
loader 去掉该行并返回 version label;阶段代码把它写入调用记录(如 `<CallRecord>.prompt_version`
字段)。同时使用 system/user 或共享 component 时,阶段代码必须把所有会影响 request 的版本组合进
记录。
动态字段使用 `{{name}}`。模板替换逻辑要求模板 placeholder 集合与代码生成的 block keys 精确相等,
并只扫描原模板一次;payload 内类似 placeholder 的字面不得二次展开。共享 JSON component 由对应
loader 校验 key 和 shape,不允许阶段代码绕开 loader 复制协议。
正式 artifact 只持久化 stable global ID。投影可以生成短 alias,模型输出 alias 后由 parser 展开;
alias 只在一次调用或明确的单次 contract projection 内有效。
## 输出协议与恢复
输出协议只保留模型必须知道的内容:字段语义、精确 shape、允许值、cardinality 和一个不引入额外
策略的最小示例。输出前检查最多覆盖:
1. 最重要且程序无法判断的语义边界;
2. 必要覆盖/唯一性/allowed-set;
3. 输出纯度与格式。
程序负责机械校验。坏输出的处置属于阶段模块:局部丢弃、明确 repair、fallback 或 hard fail 必须与
模块文档和行为测试一致。调用可观测性层只处理 transport retry,不承担业务 repair。新增 repair
调用或改变调用次数属于阶段行为变更,不能只改 prompt 文案。
## 控制 Prompt 语言
所有运行时 prompt、共享 component 和代码生成的 model-facing 固定文案只提供语义等价的多语言
variant(项目需明确支持哪几种语言,例如 `zh/en`)。各语言 variant 必须保持相同的:
- 文件集合与 placeholder 集合;
- DSL literal、allowed set、shape 和 cardinality;
- 事实边界、阶段责任和恢复语义;
- component key 与结构。
一次端到端流程只解析一次控制 Prompt decision,并由该流程内的所有派生调用继承该 decision;
派生调用不从任何下游 `output_language` 字段或下游文本反推控制语言。resolver 按输入来源的非空
字符数加权 language tag,再结合脚本比例(如 CJK/Latin),记录来源分类(如 `zh/en/mixed/other/
unknown`)。声明语言与脚本比例均达到项目设定阈值(如 0.80)的判定为对应语言,命中该阈值的语言
使用其专属 variant;其余分类落到项目设定的默认 variant(如 `en`)。来源分类与实际使用的 variant
是不同字段,不应混淆。
流程中如果存在若干"入口阶段"(独立解析自己的输入语料、单独判定一次 decision)与"下游阶段"
(没有独立可判定的语言信号、因此记录 `unknown` 分类并使用默认 variant,其后续多次调用继承该
decision)之分,应在项目落地时用实际阶段名替换这两个抽象角色,并写明每类阶段各自解析几次、
继承规则是什么。
控制语言只改变模型阅读指令的语言,与目标输出语言正交。所有语言 variant 都必须直接处理任意或
混合源语言,不增加中间翻译。decision 的 source language、confidence、distribution、policy version
和实际使用的 variant 写入对应审计记录(manifest);实际 prompt 资产版本写入每条调用记录。
`<prompt variant parity 测试路径>` 校验各语言 variant 的文件集合、placeholder、version family、
已登记 DSL literal 及 JSON component shape;`<control prompt 测试路径>` 校验分类阈值、默认
variant fallback、旧命名兼容(如历史上使用过的分类名)和 locale path 展开。阶段行为测试仍负责
验证 projection、parser、调用次数和失败语义;parity 测试不能替代行为测试。
## 修改方法
1. 明确要改变的模型行为和可观察成功标准。
2. 用代码、raw request 或代表性失败确认问题属于 prompt、projection、parser、调用拆分还是模型。
3. 做最小修改;新规则覆盖旧句时删除旧句。
4. 同步修改各语言 variant、version、component、projection/parser 和模块文档中实际受影响的部分。
5. 运行原失败例、正常例、相邻反例、variant parity 和对应阶段测试。
不能从一次局部失败直接创建永久规则。负向规则只用于封闭真实的证据禁区、不可逆损失或相邻职责
混淆;能说明正确替代行为时,应同时写出替代行为。
## Review Checklist
提交 prompt、component、projection 文案、DSL 协议或调用拆分变更前逐项确认:
- [ ] 输入足以支持要求模型输出的结论;
- [ ] model-facing 专名均已定义、是协议 literal、是明确标记的数据,或已删除;
- [ ] 阶段位置、输入证据、事实边界和下游影响清楚;
- [ ] system/user 按稳定权限与本次实例分工;
- [ ] 同一概念没有混用多个名称;
- [ ] 负向规则排除真实错误,并尽量提供正向替代行为;
- [ ] 程序可验证的机械不变量仍由 parser/schema/compiler 承担;
- [ ] 输出协议和末尾检查没有淹没语义任务;
- [ ] 没有为一次局部失败加入未经正常例和相邻反例验证的硬指令;
- [ ] 各语言 variant 的文件、placeholder、DSL、cardinality、事实边界、阶段责任与恢复语义等价;
- [ ] 非目标语言分类只作来源审计,实际 variant 为项目设定的默认语言,且不引入中间翻译;
- [ ] 共享 component 与代码生成的固定文案使用同一控制语言 variant;
- [ ] 一次端到端流程只解析一次、派生调用继承,且没有从下游语言字段反推控制语言;
- [ ] control Prompt decision 和实际 prompt version 进入各自审计记录;
- [ ] prompt version、模块文档、projection/parser 测试、parity 测试与代表性行为测试已同步。