挑时候、挑话题
想起一个人、也决定要找他了,还剩两件事:现在是不是好时候,找他说什么。这两件都在本地算,不花钱;话题只从真实的来源里挑,挑不出来就不找。
现在是不是好时候
这几种情况直接不找,过一会儿再看:
- 它自己睡着了、快睡了、刚醒、闹情绪躲着:20 分钟后再看;
- 它手上有事腾不出手(上班、训练、在路上……):25 分钟后再看;
- 和他正聊着:15 分钟后再看。正聊着就直接说,不用特地「找」;
- 他十分钟内刚说过话:等到满十分钟;
- 它半小时内刚找过他:等到满半小时。
过了这几关,再看「别扭不别扭」,几样加在一起:
- 三个小时内找过他:越近越犹豫;
- 这个钟点他通常不在:它学过他平时在哪些钟点出现,学得越可靠,这一项越算数;
- 这个念头放得越久,越不新鲜。
加起来太别扭,就不找:念头还能放半个小时以上的,半小时后再看;快过期的,就让它过去。
它怎么知道他什么时候在
他在哪个钟点说过话,它就给这个钟点记一笔(同一个小时只记一次);它主动找他,他回没回、隔多久回的,也记下来。按人分开学,私聊和群里也分开:一个人私聊秒回、在群里三天不吭声,是常有的事。
样本少的时候不当真:回过三次,学来的数只占三成左右;八次占六成;二十次九成多。三条消息就断定「他从来不回」,比不学还糟。
找他说什么
话题从这几处来,每一个都要指得出出处:
- 上次没聊完的那件事;
- 你们一起经历过的一件事(一条共同的记忆);
- 它自己刚做完的事;
- 答应过他的事(2.21);
- 它这会儿的心情;
- 发呆时又想起来的话题;
- 你们之间刚发生的事(2.14);
- 最近注意到的关于他的事。
每个话题按「和他有多相关、有多新、有多大分量」打分,翻太久远的旧事、无缘无故跟人说自己心情,会扣一点「尴尬」。由头和话题对得上的,加分:因为刚忙完想起他,先想到的多半是刚忙完的那件事。
分最高的那个够格,才把它交给模型,让模型把话说自然;一个都不够格,这次就不找了,记下一句「想找他,但一时不知道说什么」。
拿不准的时候,问一个更贵的模型
这一步默认关着。打开以后,本地算出来「要说」(主动找人、群里插一句)、又不太有把握的那一部分,会多问一次模型:最近几条消息说了什么,现在说这句合不合适。「不太有把握」是本地算的:内部几位专家意见分歧大;对方刚说了很重的事,或者一个有时间性的话题放旧了——这两种本地读不懂,一出现就问。
它只能往保守的方向改:放行、等等、不说,或者在已有的话题里换一个。它不能把「不说」改成「说」,也不能编一个新话题。超时了、回的东西看不懂,就当它说「先别说」。
冲着它来的消息永远不问这一步:别人直接跟它说话,它不会多花一次调用去想「要不要理他」。
一天最多问 40 次;先在影子模式里跑,只记它的意见、不照做,看过了再放开。
对应的代码
- 时机:
src/aeria/cognition/opportunity.py的judge_opportunity(摩擦 0.6 以上不说、0.2 以下正合适;软冷却最多加 0.35、他通常不在最多加 0.45 × 置信度、念头的陈旧度最多加 0.25);学他的习惯:ReplyHabit(置信度1 - exp(-回复次数 / 8);observe_presence按钟点记他出现;时段按 UTC 的钟点存)。 - 话题:
src/aeria/cognition/topic_candidates.py(八种TopicSource;rank_topics的分数 = 相关 × 0.32 + 新鲜 × 0.24 + 分量 × 0.22 + 由头加成 − 尴尬 × 0.30;pick_topic的门槛 0.25;没有证据编号的候选不要)。拼原料:src/aeria/bot.py的_pick_topic_for。 - 语义复核:
src/aeria/cognition/semantic_deliberation.py(apply_verdict只降不升、should_deliberate决定问不问);配置[semantic_deliberation](enabled= false、shadow_mode= true、veto_enabled= false、daily_budget= 40、timeout_ms= 6000)。 - 测试:
tests/test_topic_and_opportunity.py、tests/test_semantic_deliberation.py。
没做到的
- 它不知道他说过「我这会儿在忙」:没有这个信号,只靠学来的钟点。
- 学来的钟点一直往上累加,不会淡忘:他换了作息、换了时区,旧的记录要很久才被新的盖过去。
- 语义复核只看得到最近几条消息(默认八条),更早的语境它不知道。