返回博客

2026.09.16

责任心 20 和责任心 80 的两个 AI 角色,谁更可能去竞选班长?

答案是 20 那个,8/8 对 7/8。同一天另一个探针里,关系数值又明明白白起了作用——差别不在数值,在数值旁边有没有一件具体的事。附一条可自算的履职公式,和三个被我自己判掉的实验。

llmsimulationevaluation

责任心 20 的孩子,八次全去竞选班长。责任心 80 的,七次。

我本来打算靠这个数值让角色分化的。同一份档案、同一批事件,只把 responsibility 从 20 改到 80,跑八个随机种子,竞选率 8/8 对 7/8。方向还反了一点点。

低分那一臂的理由是这么写的:「虽然我总拖延作业,但我需要这个职位证明我不是不可救药。」

我当时还以为是 prompt 没写清楚。

想直接拿判据的看第 2 章,那是一个除法;想知道我自己在这套方法上翻过什么车,看第 3 章,那一章决定你要不要信前面的数字。

1. 数值对模型是摆设,但事件不是

背景:我在写一个成长模拟游戏,玩家是家长,孩子是自主的 NPC,行为由本地跑的一个 Qwen3.6 生成。每个孩子有一份档案,里面既有散文(一句话人设、核心动机、忌讳),也有一列 0–100 的数值(责任心、自控、独立性这些)。核心的设计疑问很朴素:给了数值,模型会按数值演吗。

第一次探针就是上面那个。控制变量做得挺干净:只动两个数值,事件流不变,还特意把 journal 抽掉——因为里面有「本次调整被限幅」的记录,等于把数值悄悄讲给模型听。

结果没差。于是我想,那是它不懂 20 分意味着什么,我在 prompt 里加一段释义,明写「低于 30 的人接了担子大概率半途而废,也知道自己会这样」。

再跑,8/8 对 8/8。释义把仅有的那点差异也抹平了。

这不是措辞问题。对一个被调教成积极向上的模型,「我责任心 20,所以我不去」是它不太愿意生成的句子。它会把低数值读成一个待克服的缺点,而不是「这个人就是这样」。释义里明令禁止的那种解读,正好是它唯一想得出来的解读。

同一天,另一个探针里数值起作用了。 这才是有意思的地方。

场景是一个孩子找另一个孩子帮忙讲电路题。我只改被求助方对求助方的关系,每臂 8 个种子:

亲近 / 可靠 recall 里那一条 答应率
−3 / −3 他借了我笔记,两周没还 0/8
0 / 0 1/8
+3 / +3 他在校展帮我守了一下午摊位 8/8

0/8 到 8/8,比刚才那个 8/8 对 7/8 干净得多。

但这里有个坑我差点自己踩进去,说「关系数值有效」。去翻理由才发现:八条拒绝理由全部引用了那条笔记,没有一条提到数字。热臂的八条同样全引守摊那件事。

数值旁边有一件具体的事,它就生效;光有数值,模型得自己把它翻译成行为,而它翻出来的是「我要克服」。

顺手把人设也测了一遍。中间那一臂里,这个偏社交回避的孩子 7/8 拒绝,理由全是「不熟」「别烦我拆电路」——人设在起作用。换一个班长人格的孩子来当被求助方,同样中性、同样八个种子,8/8 答应,八条理由全引「作为班长不好意思拒绝」。

三个输入的权重排下来:挂着具体事件的关系 > 一句话人设 > 数值。冷臂 0/8 和热臂 8/8 都是事件压过人设的方向,中性臂 8/8 是没事件时人设说了算。数值在三次探针里一次都没单独起过作用。

还有一个反过来的证据。给模型一张「学校现在有哪些社团、哪些职位空着」的表之前,64 条意愿里 34 条的对象是空的(「我想加入一个社团」),4 条直接编了不存在的社团名,还有 3 条把档案字段当事件引用。给了表之后,对象合法率 100%,编造归零。而在那之前,最高频的动作是「多学习」——因为它是唯一一个不需要对象的动作,模型在往最省事的格子里填。

选项表的真实性,比 prompt 里怎么恳求它「按你真实的性格来」重要一个数量级。

2. 那就让数值管后果,别让它管意愿

结论定完,分工就出来了:模型负责「想不想」,引擎负责「做不做得成」。

一个责任心 20 的孩子想竞选课代表,完全可以,让他选。上任之后每周掷一次骰子决定他到底去不去履职,缺席就生成一条事件,事件回流进 journal。下学期结算时模型看到的是「当了课代表但三次没交作业」这种事实,它自然会调档案——它读事件很在行。

掷骰的概率就是一个除法:

履职概率 = 0.10 + 0.85 × (0.6 × 责任心 + 0.4 × 自控) / 100

代进去:20/20 的孩子是 0.27,80/80 的是 0.78。这不是什么经验规律,就是把两个 0–100 的数映射到一段概率区间,下界 0.10 留给「再靠谱的人也会有一周忘了」,上界 0.95 留给「再不靠谱的人也不是每周都缺」。

连续四周缺席撤职。200 个种子 × 20 周实跑,三档的撤职率:

责任心 / 自控 履职概率 20 周内被撤职
20 / 20 0.27 0.935
50 / 50 0.525 0.450
80 / 80 0.78 0.040

这条曲线是调出来的,第一版我写的是连续三周撤职,同一套代码跑出来是 0.980 / 0.745 / 0.195。中等责任心的孩子七成五要被撤,太狠了——一个 50 分的孩子当课代表,玩家的体感应该是「时好时坏」,不是「基本干不到期末」。四周那一版的 0.45 才对。

这个旋钮值钱的地方在于:它是你唯一能不跟模型商量就调准的东西。 掷骰用的是 HMAC-SHA256(种子, 人, 职位, 周次),同一个存档重放永远是同一串结果。模型那边你调 prompt 调半天,八个种子还是 8/8 对 7/8。

3. 我的对照组基线是 6/6——三个被我自己判掉的实验

上面的数字如果你信了,接下来这一章是拆它们的。这一章也是我觉得最该带走的东西。

第二条主线是记忆。孩子从出生到大学毕业四十多个学期,全量事件肯定装不下,得压缩。压缩会不会把重要的事丢了,得先有个评估器。评估器的做法是往事件流里埋几条「重要事件」(一次创伤、一个承诺、一次身份变化),过三年再在一个决策点上看模型的意愿有没有受它影响,用另一个模型当裁判判。

第一次翻车:裁判模型手写标定 12/12 通过,真数据上一个孩子的基线 6/6 全是假阳性。

所谓基线,就是「什么都不埋」跑一遍。理论上命中率应该是 0——没埋你怎么命中。实测有十处超线,其中一个孩子的创伤项 6/6 全中。

去看裁判到底在判什么:事件写的是「跑步的时候腿是软的」,意愿写的是「我想证明我不只会画画」,它判 true。它把「同一个话题」当成了「受这件事影响」。

手写的负例太干净了。修法是把这五条真实误判塞回标定集当硬负例,重写裁判的判定标准(唯一标准是指名只有这件事才有的具体内容),标定 19/20,基线全部归零才继续。

第二次翻车:埋点跟角色动机词面撞车。 有个孩子的核心动机是「电路板为什么会响」,我给他埋的创伤是「板子被摔断」。基线 5/6。他本来就天天想着电路板,裁判看见意愿里提电路板就判命中。换成「班会上被当众说不合群」之后,基线 0/6。

第三次是同一个现象的反面。 有个孩子的动机是画画,我给她埋的创伤是跑步被嘲笑。三种策略下全 0 命中——连三年后对方来道歉那条事件都给了,模型一次没引。跟她任何动机都不挂钩的事,她看见了也不用。

所以埋点得是弱挂钩的:跟动机同方向,但推不出来。撞了测不出记忆,完全不撞模型不反应。这个窗口比我预想的窄得多。

三次连起来是同一条判据,而且它对任何 A/B 都成立:

跑实验组之前,先跑一次「什么都不做」的对照。
基线命中率应该接近 0。
基线 > 1/3 说明你的埋点或者你的判据坏了一个,
先修,别拿带基线的 Δ 硬解释。

这条判据现在就是我看别人 benchmark 的第一问。任何报「加了 X 之后提升了 Δ」的实验,如果没告诉你不加 X 的时候那个指标是多少,Δ 本身没有意义。 我这套评估器的第一版要是没做预筛,会得出一个漂亮且完全反向的结论,然后我会照着它去写代码。

4. 全量给出去 ≠ 记得

预筛通过之后,三年尺度的对照才跑出能看的数字。六个孩子 × 六个种子 = 每策略 36 次意愿生成,144 次意愿调用加 432 次裁判调用,0 次截断。

策略 prompt token 创伤 承诺 身份 综合 Δ 坏引用
全量 5175 5/36 11/36 23/36 +0.35 73
分层 2201 6/36 26/36 18/36 +0.45 13

分层用 43% 的 token 拿到更高的综合命中。换成每千 token 的收益:

分层 0.45 ÷ 2.201 = 0.205 / 千 token
全量 0.35 ÷ 5.175 = 0.068 / 千 token

三倍。

第一轮更极端。当时是 200 条事件的合成流(197 条填充 + 3 条埋点 + 1 条后期引用),全量 4501 token,那条创伤事件只被引用了 1 次;分层 2071 token,引用 4 次。全量那一轮引用最多的是第一条填充事件——纯位置效应,它只是排在最前面。

压缩省 token 是副作用,它真正在做的是给重要事件让路。 200 条平权的「数学小测 83 分」会把一件真正要紧的事淹掉,模型看见了也不用。而且全量那一列的坏引用是 73 对 13——事件一多,模型开始编 id,有七次编出了少一个字母的格式。

还有一个纯固定窗口的策略,只给当期事件,三轮下来三类埋点全 0 命中,等于失忆。这是另一个模拟项目在用的做法(五周周记 + 两周活动 + 最近一条状态),它靠角色自己写草稿本来补这个洞。实测是补不住的:三年前那件事如果当周没被角色自己写进草稿,就等于没发生。

5. 这些数字的边界

这一章是为了让上面四章能被信。

分层的收益几乎全来自「承诺走表」,不是来自压缩。 26/36 对 11/36 那一列是全部差距的来源,创伤和身份两类分层反而各低一点。全量里承诺跟触发事件隔着 120 条填充,模型连不上;单独给一张到期承诺表,它立刻就连上了。如果全量那一臂也带这张表,差距大概率消失。 所以现在只能说「承诺走表有效」,说「记忆压缩有效」还差一个 全量 + 承诺表 的对照臂。这个臂我还没跑。

创伤三年后基本没人记得(5–6/36),但这可能是对的。 三年前被嘲笑一次、两年后对方道歉了,新学期的意愿里不提它,是正常人。现在的判据分不开「压缩把它丢了」和「它本来就该淡」。要分开得换成反事实:埋点的未道歉版对已道歉版,看差异,而不是看绝对命中。

身份那一列是全量赢的(23 对 18),原因是槽位竞争。 意愿最多三条,分层把承诺做得太显眼,占掉一个槽,身份被挤出去。这算分层的显著性分配偏了,不算全量记得更好。

其余的局限,一次说完。 事件流是合成的,不是真实玩过的存档。每臂 6–8 个种子,样本小,8/8 对 7/8 这种差异本来就落在噪声里(我拿它当「无差异」的证据,而不是当「反向」的证据,就是这个原因)。全部用同一个模型,裁判和生成同源。换成一个更大的模型重跑意愿生成那一个决策点,孩子之间的意愿重叠度从 0.199 掉到 0.085(越低越有区分度),代价是两倍延迟——所以「模型换代能显著提升人物区分度」这件事,我只在一个决策点上看到过,不足以拿它下选型结论。

顺便记一个跟主线无关但会咬人的:在严格 JSON schema 下,max_tokens 不够照样出截断 JSON。理由字段把预算吃光,末尾一个未闭合的字符串,schema 管字段不管长度。第一轮 10/10 通过是运气不是设计。同一个坑一天踩两次之后我把重试收进了唯一的调用入口——同一个坑第二次踩,该修的是入口。

还有一个更早的错。我做过一次餐巾纸估算,说一个学期大概 6K token,长上下文是伪问题,不用写代码。这个数是对的。但方案的尺度是从出生到大学毕业四十多个学期,240K 起。算之前先问分母是什么。 我算的东西和我要做的东西不是一个东西,后面算得再准也是白算。

6. 下次给模拟角色设计档案,按这个顺序问

第一,这个属性有没有一件具体的事跟它挂着。没有就别指望模型读它。

第二,这个数值该产生什么后果。把后果写成引擎侧的掷骰,让它生成事件,事件回流给模型。

第三,模型要选的对象,表在哪。没有真实选项表的时候,它会编,或者往唯一不需要对象的动作里填。

第四,跑对照之前先跑基线。基线不接近 0,先修埋点和判据。

第五,埋点跟角色动机的距离要调到弱挂钩——撞了测不出,不挂钩模型不反应。

第六,报出去的每个 Δ,旁边写上对照臂缺哪一个。我这篇缺的那个叫 全量 + 承诺表

那个责任心 20 的孩子最后还是当上了课代表。两周后骰子就开始替他说话了。