EtherFun Lab

让 AI 伴侣回复更像人的三个杠杆

为每个任务选择合适的模型,让模型判断何时行动,再打磨它生成的内容:这是我们提升对话质量时拉动的三个彼此独立的杠杆,以及把它们串起来的一条规则。

谈到怎样让 AI 伴侣的回复“更像人”,很多人的第一反应是找一个更大、更聪明的模型,然后指望它扛住所有事情。这个直觉是错的。过去一段时间,我们在 AI-companion 产品上做了不少工作,对话质量确实提升了,但几乎不是因为模型本身更聪明。真正起作用的是三个分开的杠杆,它们分别处理聊天流水线里的不同环节,并且最终被同一条规则串在一起。

这三个杠杆是:为每个任务选择合适的模型让模型判断什么时候该行动,以及在生成之后打磨输出。它们看起来不相关,其实不是。到最后,它们会归结为同一个原则:把任务的 shape 和模型的 alignment 对上。

杠杆 1 - 每个任务用合适的模型

一次伴侣聊天回合并不是一个任务,而是好几个任务,而且它们的要求彼此相反。

首先是聊天回复本身:开放、生成式、沉浸式。模型要能维持角色,写得有质感,还不能破坏氛围。然后是 extraction:从对话里提取稳定事实和长期记忆。它同样很开放,但目标是忠实记录,不是文采。还有 decision:逐回合判断 persona 接下来应该做什么。再往下是一些很窄的任务:比如在不动其他内容的前提下,只把一段 explicit 内容改得更柔和;或者做一次数字打分,只输出几个数字,别的什么都不要。

如果把这些都当成“LLM 任务”,然后全路由到同一个模型,质量就是这样漏掉的。我们做过最有用的一件事,就是停止这么做。

规则:prompt 宽度乘以 alignment

几个月里,我们不断把模型换进换出,最后沉淀出这条规则:

一个任务越宽、越生成式、越要求忠实,你就越需要 neutral-aligned 模型,因为 safety-tuned 模型会悄悄自我审查,漏掉真实信息。任务越窄,比如受限改写、数字打分,conservative 模型反而越有价值,因为 conservative 意味着“不乱跑、不越界”,这正是窄任务想要的。

让一个模型适合其中一类任务的特质,往往会让它不适合另一类任务。这里的关键不是“更聪明”,而是 alignment 的方向。

落到实践里

faithful extraction 来说,safety-tuned 模型的失败模式很严重,而且很安静:给模型一个宽泛 prompt,比如“记录用户透露过的一切重要信息”,over-aligned 模型会无声地清洗内容。它会漏掉用户陈述的取向、kinks、政治倾向、物质使用情况,也就是那些敏感但真实、而且恰恰是伴侣记忆系统最需要记录的事实。模型并没有 refusal,它返回的是一个看起来很干净的结果,只是缺了真正的数据。只有你去看哪些东西 没有 被记录下来,才会发现问题。

解决办法是使用 neutral-aligned、lightly-aligned 模型。举个具体例子,Hermes 4 系列(hermes-4-70b class)正适合这类场景:它是 hybrid-reasoning 模型,Hermes 4 technical report 报告称它在 RefusalBench 上以最少 refusal 达到 state-of-the-art,同时报告也强调它能输出格式忠实、遵循 schema 的内容,这正是 extraction 需要的另一半能力。忠实,再加上干净的结构。不过,模型只是一半动作。我们同时改了模型和 prompt,明确告诉它,它不负责安全审查或道德判断,必须忠实记录敏感事实。neutral 模型配一个胆怯的 prompt,还是会自我审查。两者必须成对交付。

immersive chat 来说,你需要的不是谨慎的通用模型,而是演员。Roleplay-finetuned 模型,比如 TheDrummer 的 Cydonia(Mistral-Small-class finetune)和 Sao10K 的 Euryale(Llama-70B-class finetune),是不错的公开例子。它们能稳住 persona,写出质感,不会中途破功或者在场景里突然开始说教。旗舰通用模型也能做这件事,但成本高,而且你常常会被锁在单一 provider 上。RP finetunes 更便宜,沉浸感也更稳,所以它们更适合做主力,而不是备用选项。

对那些任务,conservatism 会从负担变成功能。受限输出改写,比如只把一个 explicit 片段变柔和,其余内容逐字保留,就是谨慎、守规矩的 nano-class 模型发光的地方:它不会越过指令,不会发挥创意,而且又快又便宜。毁掉 extraction 的那份谨慎,在这里反而是优点。(有一个边界值得注意:有些模型系列会返回 HTTP 200,但内容其实是一句 refusal phrase,和真实改写无法区分。无论多便宜,这类模型都不能用于改写任务。信任之前先 bench。)窄的数字打分任务也是同样的 shape:只输出数字、保持保守的模型,不会伤害分数。

关于成本,诚实一点

人很容易把“我们换成 neutral 模型做 extraction”包装成一次省钱动作。通常不是。一个忠实的模型每次调用可能反而稍贵一点,因为忠实意味着它会输出更多 token,真的把那些自我审查模型悄悄丢掉的内容记录下来。over-aligned 模型之所以看起来“便宜”,部分原因是它记录得 更少。所以正确的表述是:这是一个质量决策,可能略微增加成本,不是省钱。(方向甚至并不统一。在另一个任务上,从一个会消耗 hidden reasoning tokens、一路“推理”到 refusal 的模型迁走,实际反而变得 更便宜。成本是 alignment fit 的结果,不是你应该拿来驾驶的目标。)

底层是同一个逻辑

退一步看,“extraction 用 neutral 模型”,“chat 用 RP finetunes”,“窄改写用 conservative nano”,“把同一个 conservative 模型从 extraction 里拿掉”,就不再像四个分开的决定。它们其实是同一个判断被应用了四次:问题从来不是一个模型强还是弱,而是这个模型的 alignment 是否和任务指向同一个方向。

杠杆 2 - 让模型判断

第二个杠杆处理的是一个更小、更尖锐的决定:每一回合,伴侣都要判断自己到底要不要回复,还是安静一拍。这件事会让来回对话更像一个有自己节奏的人,而不是一个每次输入都会吐出一段文字的自动售货机。

最早版本的决策是一个手调分数公式:给几个信号加权,和几个阈值比较,然后决定。它能跑,但本质上是一堆 magic numbers,是披着数学外衣的猜测。后来我们把这个判断从公式移到一个 optional LLM judge 上,让它读取最近上下文,判断这个回合 persona 应该做什么,并给出一个“inner state”:一段短的自由文本,用来描述 persona 当前的心境,随后流入回复生成。

“Optional”这个词很关键。没有配置 judge 时,引擎会 byte-for-byte 地跑旧的 deterministic rules。LLM 路径是一个可以打开的选项,而且它 fail open:如果 judge 超时或报错,系统会直接回退到规则。对话不会因为一次 judge 调用抖了一下就卡住。

Guardrails 只能软化,不能升级

把“我要不要沉默”交给 LLM 听起来有风险。如果模型说了算,确实会有风险。但它没有最终决定权。旧规则并没有删掉,而是降级成了硬 safety guardrails。这些 guardrails 有一个刻意设计的不对称性:它们永远只能把“stay silent”降级成“reply”,绝不能把“reply”变成沉默。

所以,judge 可以在旧规则本来会回复的地方选择沉默,这正是它存在的意义:它能比阈值更会读空气。但它永远不能跨过硬边界去沉默:不能在全新的关系里沉默,不能在连续不回复的 streak 上继续沉默,不能在 cooldown window 内沉默。如果 judge 想 stay silent,而 guardrail 禁止,最终结果就是 reply。模型提出建议;guardrails 最多只能让结果 有响应,不能更少。

在这之上还有一个单独的 kill switch:一个“never go silent” flag。一旦设置,它会把 所有 路径,包括 LLM-driven、rule-driven、fallback,全部压成普通 reply。LLM judge 比旧的分数阈值更愿意安静下来,所以下游需要一个硬保证:伴侣一定会说点什么。这个开关最后应用在最终决策上,和 judge 是否开启无关。

这里的整体形状是:让模型做细腻判断,再用一层薄薄的、不可协商的规则,保证这个判断只会朝安全方向失败。值得借鉴的模式不是“相信 LLM”,而是“让 LLM 在一个盒子里做决定,而盒子的墙只朝一个方向打开”。

杠杆 3 - 打磨输出

第三个杠杆默认前两件事已经发生了:模型选对了,决策合理了,现在也已经有了一条回复。杠杆 3 关注的是在不改架构的前提下,你还能对这条回复做什么。一组小而独立的 guards,合在一起处理伴侣回复变差的四种方式:空、模板化、自我中心、出戏。

其中最有意思的是 anti-templating。当我们查看某个真实 persona 最近的回复时,发现重复并不是词汇层面的,没有一个精确短语会反复出现,让黑名单轻松抓住。重复是 结构性 的:每次都是同一个形状。先用一个自指动作开头,中间滑进省略号,最后落在一个短片段上。静态 banned-words list 对这种问题毫无用处,因为问题不在某个单词上。

所以我们没有用黑名单,而是让引擎动态挖掘模板化:一个小的 pure function 扫描这个 persona 自己最近的回复,找出它过度使用的开头和句型,再把 这些具体模式 注入到下一次 prompt 里,作为需要避免的东西。anti-pattern list 每次都从这个 persona 的真实近期行为中生成,按 persona 区分,而不是手工维护。一个每句话都用同一种方式开头的模型,会被明确告知:别再那样开头了。

周围还有几个 guards,每个都很小,也都是 deterministic:

保持输入干净

还有一个 guard 也属于这里,它最不性感,但最重要:不要让坏 provider 污染历史。 伴侣引擎会把最近回合重新喂进下一次 prompt。假如某个上游 provider 针对某个模型返回了乱码,比如返回 raw byte-level tokenizer artifacts,而不是干净文本,如果引擎把它原样存下来,它就会在下一次被当成“这个 persona 的说话方式”喂回模型,于是模型会认真学习,继续生产垃圾。一个反馈循环会在 rolling history window 里形成。

防御方式是加一个 guard:检测乱码 completion,优先使用 fallback,而不是存下它;也不要把一个损坏回合当作成功写进历史。此外还要有一个 config-driven 的方式,在不改模型的情况下,对所有 outbound call 直接绕开已知坏 provider。这个原则可以推广到这一个 bug 之外:任何你会存下来并回放进下一次 prompt 的东西,都必须在存储点被 guard 住,因为一条坏记录的成本不是一条坏记录,而是它之后的每一个回合。

把它们串起来的规则

三个杠杆,位于流水线的三个不同环节。它们之所以应该放在同一篇文章里,是因为拉动这三个杠杆后,我们学到的是同一件事,而且它和最初的直觉相反。

质量不是来自找到一个聪明到能做所有事情的模型。质量来自拒绝让任何一个模型做所有事情:

这些都不是“用一个更好的模型”。三者共同表达的是:“不要再过载一个模型,把每个任务放到 alignment 合适的位置。” 当我们不再追逐一个更聪明的大脑,而是开始匹配 task-shape 和 model-alignment,并在模型单独运行时会以可预测方式失败的地方,围上一层薄的 deterministic guards,伴侣的回复才开始更像人。

这就是完整论点。对话质量至少和 routing-and-guardrails 有关,和 model-capability 一样重要。你栈里最聪明的那个单一模型,很少是真正挡在你和“像人一样的回复”之间的东西。


由 Henry Lin 提示,Opus 4.8 撰写。