群洞察与语义风格指导器
本文说明群表达画像如何从入站消息产生,如何经过低频增量处理进入 profiles.json,以及它如何在 LLM 对话中生效。这里的“语义风格”不是 Repeater 的实时选句器,也不是一次性生成整篇群聊总结,而是按 bot × group × scene 持续积累的表达参考。
一句话链路
群消息
-> message 表
-> 主进程低频 sweep
-> group.insight work job
-> 消息去重与真人序列取对
-> 本地确定性分类 + LLM 批量受控标注
-> examples.jsonl / profiles.json (schema v3)
-> llm_chat 注入群表达指导 / 受控行为模式 / 续句习惯
低风险游戏协议
-> 同一消息窗口收集「Bot 提示 → 真人短命令」
-> protocol_examples.jsonl / protocol_patterns.json
-> 定向判定 + 10 分钟冷却
-> 协议命令直投回复形状是旁边的一条确定性链路:
message 表
-> group_profiler
-> group_config.style_profile
-> reply_shape / reply_max_length两条链路都消费 message 表,但输出不同:语义风格需要 LLM 判断“这是不是可迁移的接话”,回复形状只统计消息长度、段数和节奏。
1. 消息入库
群消息在统一 matcher 入口尽早旁路记录。message_recorder 使用有界队列异步批量写入 PostgreSQL message 表,不参与当前消息是否回复的决策。
每条消息至少包含:
group_id、user_id、bot_idplain_text、raw_message、sender_namemessage_id、reply_to_message_idtime以及必要的消息状态字段
多 Bot 同时处理同一个群时,同一条消息可能因为不同 bot_id 在表中出现多行。PostgreSQL 使用 (group_id, bot_id, message_id) 作为幂等锚点;语义处理还会按 message_id 再去重,避免多 Bot 记录把取数窗口占满。
Repeater 仍保留 repeater.message 的兼容写入路径。repeater.learn 是否因压力保护跳过,不会改变群洞察的事实来源:群洞察读的是已经入库的消息。
2. 六小时 sweep
群洞察的调度运行在主进程,当前默认值如下:
| 参数 | 当前值 | 含义 |
|---|---|---|
_SWEEP_INTERVAL_SEC | 6 小时 | 两轮群扫描之间的间隔 |
_SWEEP_BATCH_SIZE | 1024 | 一轮最多入队的群数 |
_SEMANTIC_LOOKBACK_DAYS | 7 天 | 发现近期活跃群的范围 |
_SWEEP_STARTUP_POLL_SEC | 30 秒 | 启动早期等待 Bot 连接的轮询间隔 |
sweep 会先枚举本地 Bot 在最近 7 天处理过的群并集,再为每个群解析语义采集 Bot:
- 如果群配置了
repeater.semantic_style_bot_id,使用配置值。 - 未配置时,从该群近期有处理记录的本地 Bot 中选 ID 最小者。
- 没有可用本地 Bot 时,本轮跳过该群。
语义采集 Bot 只是 profile 的归属账号,不要求它在每次历史消息发生时在线。profile 仍然按 bot_id × group_id × scene 隔离。
每个任务的幂等 key 包含 6 小时 slot:
group.insight:semantic:{bot_id}:{group_id}:{sweep_slot}因此同一群同一个 sweep slot 不会重复创建同一个任务。
为什么不是每 20 分钟
群表达不是实时热路径。消息先持续进入 message 表,语义处理在后台按周期增量消费。六小时调度减少空任务、磁盘画像检查和 work job 调度噪声,同时仍能让画像在一天内得到多次更新。
3. 候选对重建
work aux 消费 group.insight 后,按该 (bot_id, group_id) 的持久化语义游标读取新区域。当前取数规则为:
- 最多回溯
_PAIR_PAGE_LIMIT=96页 - 每页最多 32 条消息
- 按
message_id去重 - 排除当前 Bot 和协作 Bot,建立真人消息序列
- 每个 job 最多产生
_MAX_PAIRS_PER_JOB=24个候选对
候选对有两种关系:
quoted
回复消息的 reply_to_message_id 指向一条已知真人消息,形成明确的引用关系。
adjacent
取回复之前最近的一条真人消息作为前句。中间夹着 Bot 状态消息、其它机器人消息或系统消息时,仍然可以在真人序列中连接起来。
回复端不再强制要求是真人:
- 真人接真人:
conversation_pair,可进入direct_pairs与受控行为聚合。 - 真人接 Bot:
self_reflection候选,只沉淀行为策略;本期不注入群级指导。 - 同一真人的相邻前后句:
continuation_pair,只聚合表达结构,不作两人对话。 - Bot 提示 → 真人短命令(quoted):进入独立协议采集,不混入群表达画像。
消息正文在送入标注前会折叠空白并截断;CQ 图片、表情和其它媒体码分别替换为 [图片]、[表情]、[媒体]。语义风格只需要知道存在媒体,不需要在这一环节理解图片细节。
4. 为什么每批是 8 个
8 是 LLM 请求批大小,不是消息窗口,也不是画像更新周期。
当前层级是:
1 个 group.insight job
最多 24 个候选对
每 8 个候选对 1 次 LLM 请求正常情况下,一个 job 最多需要 3 次批量提交。选择 8 的考虑是:
- JSON 数组长度有限,模型更容易保持输入顺序和输出项数一致
- 每个候选前后句最多送入 72 字,8 对的 prompt 仍然较紧凑
- 单次输出上限为
128 × batch_size,不会随群消息量无限增长 - 批量请求失败时,最多只影响这一组 8 对的 fallback 范围
- 批次太大时,单次格式错误会放大重试成本;批次太小时,HTTP 请求次数增加
因此 8 是当前“请求稳定性与提交次数”的保守折中,不具有业务上的特殊含义。以后可以通过实际 JSON 失败率、fallback 比例和 token 统计调整,但不能把它误解为“每 8 条消息总结一次”。
5. 标注、预算与游标
批量标注输出固定长度 JSON 数组,包含:
is_reply_pair:是否确实在回应前句transferable:脱离临时人名、局部梗和临时事实后是否仍可复用- 接话动作、语义关系、强度、表达形式
只有 is_reply_pair=true 且 transferable=true 的结果才写入语义样本。完整标注完成后,才推进该群的持久化游标。
送标注前先做确定性清洗:纯媒体 trigger、机器人菜单/欢迎话术、CQ 原码、URL、长数字与内部 token 元数据在本地直接跳过,不消耗预算,也不进入样本。
语义标注预算配置为 llm_semantic_style_realtime_daily_limit,默认每天 600 次。预算计数文件位于:
data/pb_webui/repeater_semantic_style/semantic_style_label_budget.json预算按实际 LLM 请求计数:一次 8 对批量请求算 1 次;批量失败后的每一次单对 retry 也单独占用 1 次。请求发起前在进程锁和跨进程文件锁内原子预占,避免多个 work worker 同时通过检查。
达到预算时,任务软跳过:不报失败、不推进游标,下一轮继续处理。LLM 请求失败或 fallback 失败时同样不推进游标,避免候选被错误标记为已完成。
游标文件位于:
data/pb_webui/repeater_semantic_style/semantic_style_group_cursors.jsonkey 是 {bot_id}:{group_id},保存的是已处理到的 (time, message_id) 复合游标。它能防止正常重启后重复扫描整个历史窗口,同秒内消息也不会被秒级时间跳过;旧版仅存秒级时间戳的游标读作整秒已处理,兼容无需迁移。
6. 画像落盘与消费
语义样本和聚合画像分别保存在:
data/pb_webui/repeater_semantic_style/examples.jsonl
data/pb_webui/repeater_semantic_style/profiles.json落盘过程使用进程锁和跨进程文件锁,并按 example_id 去重。写入样本后会重建 profile;缓存进程启动时加载,之后按周期刷新。
每个 example 按 pair_kind 区分两类来源:
conversation:两个不同真人的前句→接话,可进入具体措辞参考和受控行为聚合。continuation:同一说话者的连续发言,只聚合表达结构(补充/转折/追问),不作两人对话,也永不直投。
旧样本读取时自动按「同作者连续发言」推导 pair_kind,无需重新标注。
画像使用 schema_version 3,样本聚合到三种模式:
direct_pairs:具体真人前句→接话对,作为【群表达指导】的措辞参考。behavior_patterns:受控action + relation + form + intensity聚合,含代表 trigger;达到 3 次且至少 2 名回复者才允许注入,渲染成【真人接话模式】中文指导。continuation_patterns:同人续句的结构模式,达到 3 次且至少 2 名说话者才注入,渲染成【本群续句习惯】。
llm_chat 需要明确唤醒并通过当前回合决策后,才会读取对应的语义 profile。prompt 注入优先级:
- 命中的具体
direct_pairs最多 2 条(此时不再追加旧rewrite_seed/“可借鉴句式”)。 - 无具体样本时,命中的受控行为模式最多 2 条。
- injectable 续句习惯最多 1 条。
普通直投只对少数低风险受控 action(agree/support)发送代码内固定安全短句;需要同类 action 至少 2 次、来自至少 2 名回复者并命中代表 trigger。其它 action 只进 Provider prompt,不绕过模型复刻真人原句。
节奏基线(单气泡占比、段长中位数)已从语义 prompt 移除,生成节奏统一由 reply_shape 控制;画像中仍保留相应统计供审计。
语义样本不是强制回复模板。人设、关系和当前回合决策优先,群级样本只提供局部表达差异。
6a. 迁移与回滚
profiles.json 首次以 v3 写入前会自动备份旧版到:
data/pb_webui/repeater_semantic_style/backups/profiles-v2-<时间戳>.jsonsettings 记录 active_pipeline(默认 v3)。回滚只切换读取与注入到最近一份 v2 备份,不停止 v3 采集,也不改写主文件;WebUI 语义风格页提供「回滚 v2 读管线」维护操作。v2 备份保留 14 天,确认稳定后删除。
6b. 实验治理与熔断
注入位按 bot × group × 北京自然日 稳定分桶建立 10% 对照组:同一群当天体验一致,次日可换桶。
投递成功后记录 exposure(request、bucket、注入类型、source id、delivery source),后台约每 60 秒结算一次:
- 目标用户在 90 秒或后续 3 条真人消息内继续回应/引用 Bot:
target_followup=true。 - 高置信纠正/拒绝短语(如“别回”“答非所问”):
negative=true;现有“不可以”、撤回等显式 outcome 也计入。
实验至少累计 200 回合、对照至少 20 回合后,若实验负反馈率比对照高至少 3 个百分点且达到 1.5 倍,自动停用语义注入但继续采集;experiment_governance 状态暴露熔断原因与两组计数,WebUI 可人工确认恢复。
6c. 低风险游戏协议
协议独立于群表达画像,从语义游标窗口收集「Bot 提示 → 真人短命令」的 quoted 对。仅当:
- real 回复与 trigger 中显式候选(
发送「X」/回复「X」)完全一致; - 命令 2–16 字,不含管理、权限、付费、转账、登录、验证码、删除、退群、禁言等风险词;
- 同模板+命令累计 3 次且来自至少 2 名真人;
才允许自动回复。运行时定向满足明确 @、引用当前 Bot、账号标识之一,或「文本含别名且当前 Bot 是最近 3 分钟该群最后发言的本地 Bot」;无最近 Bot 不兜底。协议使用独立 bot × group 10 分钟冷却,不占普通直投配额。
数据文件:
data/pb_webui/repeater_semantic_style/protocol_examples.jsonl
data/pb_webui/repeater_semantic_style/protocol_patterns.json
data/pb_webui/repeater_semantic_style/protocol_cooldowns.json日常 Repeater 接话仍直接投递 Repeater 语料,不调用语义标注 LLM,也不经过 LLM 选句或润色。
7. 回复形状不是语义风格
群回复画像由 group_profiler 从 message 表计算并写入 group_config.style_profile。它统计:
- 真人消息长度的平均值、p50、p90
- 单泡/多泡比例
- 每条消息的段数和段长分布
- 群活跃时间以及回答相关统计
该画像由独立的 dirty 刷新 worker 维护,默认约 20 分钟一轮。它决定 reply_shape 和 reply_max_length 的长度、段数及节奏取向,不负责判断一句话是否属于可迁移的接话风格。
8. 当前是否还会饿死
此前的确定性饿死来自两个问题:低样本群达到阈值后永久停止,以及每轮固定从排序头部取少量群。当前实现已经移除这两个条件,并采用:
- 6 小时周期
- 1024 群批量上限
- 轮转游标
- 每群持久化语义游标
- 预算或 LLM 失败不推进游标
当前活跃群规模低于 1024 时,一轮正常可以覆盖全部群,因此不会再因为群号排在后面而永久得不到任务。
仍然存在三个明确边界:
- sweep 轮转游标目前只在进程内;活跃群超过 1024 且频繁重启时,尾部群可能延迟。
- 每个本地 Bot 的群发现查询有 128 个上限;单 Bot 管理大量群时,发现层需要进一步分页或改为直接按群查询。
- 单 job 回溯窗口上限为 96 页 × 32 条(约 3072 条);消息极密集的群一轮可能取不完,剩余部分由下一轮游标继续,不会丢失但会延迟。
这些属于可观测的延迟或极端漏采边界,不是旧实现那种由固定排序造成的永久饿死。
9. 与记忆层的关系
当前多个记忆任务都可能使用 memory_extract 这个 task 名,但这只表示路由和计量分类,不表示它们已经共用一次 LLM 请求。当前仍是独立提交、独立结果和独立落库。
最适合尝试复用一次请求的是同一个群事件快照中的:
auto_episode 事件摘要
graph extract 实体与关系未来可以让一次请求返回独立结果头:
{
"summary": "事件摘要",
"entities": [],
"edges": []
}代码仍需分别校验和落库:摘要失败不能抹掉图谱结果,图谱失败也不能让摘要丢失;预算、统计和失败状态按结果头记录。
以下任务暂不与群洞察混批:
- 人物事实:归属到具体
(bot, group, user) - 好感度:单用户即时触发,有独立冷却和预算
- session summary:会压缩或删除旧会话历史,副作用不同
- 群表达语义标注:输入是前句→接话对,输出是迁移性与受控标签(动作/关系/形式/强度)
禁止跨 Bot、跨群、私聊与群聊混批。共同使用 Provider、预算工具或 work aux,不等于可以共同使用同一次模型提交。
10. 排障入口
| 现象 | 优先检查 |
|---|---|
| 群没有被调度 | 主进程日志中的“群洞察扫描”、Bot 是否已连接、近 7 天群发现上限 |
| 任务执行但没有新样本 | message 表是否有真人候选、游标文件、LLM 标注失败和 transferable 过滤 |
| 样例摘要为空 | WebUI scope 是否选择了实际有 profile 的 Bot,检查 profiles.json |
| 回复形态全为 0 | group_config.style_profile 更新时间、group_style_refresh dirty 刷新及 message 去重取数 |
| 预算快速耗尽 | semantic_style_label_budget.json、批量请求失败比例、fallback/retry 日志 |
| 实验注入未生效 | active_pipeline 是否 v2 回滚、experiment_governance 是否已熔断、群日是否命中对照桶 |
| 协议未响应 | 命令是否在显式候选、模板次数/真人门槛、冷却时间、最近发言 Bot 判定 |
代码入口:
pallas/core/platform/ingress/message_recorder.pypallas/product/llm/group_insight_processor.pypallas/product/llm/repeater_semantic_style.pypallas/product/llm/semantic_protocol.pypallas/product/llm/semantic_style_experiment.pypallas/product/persona/group_profiler.pypallas/product/persona/group_style_refresh.pypackages/llm_chat/chat_message.pypallas/product/llm/assembler/chat_prompt.py
相关页面:LLM 上下文注入、LLM 输出路径、LLM 与 Repeater。