我在一个业务群里看到过这样的三条消息,时间戳原样抄下来:
05:01 天刚亮,打个照面。
10:00 上午好,开工了,随时招呼一声。
10:00:55 本次执行已结束,但有消息是否送达尚未确认,请查看运行记录。
(群里没有任何活人发言)
# generated by hugo AI
发消息的是我们自己的数字员工。三条里没有一条错——问候很客气,状态提示很负责,措辞也没毛病。可我在屏幕这头看着,只觉得尴尬。
尴尬在哪,我想了几天才想清楚:05:01 那一刻,群里没有一个活人在。它不是在跟谁打招呼,它是在跟一个空房间打招呼。 而不到一分钟之后那条运行状态提示,等于它转过身,把后台日志贴到了业务群的公告栏上。
我在 数字员工的活人感,不是设计出来的,是长出来的 里写过:活人感不是皮肤,是行为,是「可验证的一致性」。那篇给了六条行为特征——记忆、主动性、边界、一致性、成长、节奏,又给了六个把它做出来的动作。但这三条消息让我发现那篇漏了一整维:特征和动作全是加法,没有一条讲什么时候该做减法。
这篇补上那一维。我的判断是:数字员工的活人感,一半来自它知道什么时候不说话。
一、活人感与现实感,是两件不同的事
先把两个词分开,因为它们的药方完全不同。
活人感 说的是「它像个同事」——有节制、看得懂场合、说话有分寸。它的反面是客服腔和机器人腔。
现实感 说的是「它说的每件事都指向现实」——带得出一条数据、一个待办、一个进展。它的反面是客套话。
那三条消息同时丢了这两样。05:01 和 10:00 两条丢的是活人感(没人说话的时候自己开口,不像同事,像定时器);「随时招呼一声」丢的是现实感(这句话没法接——你回什么?回「好的」?然后呢?)。而 10:00:55 那条同时丢了两个:既是运维噪音(不该在这个群),又是零信息量(没人知道该做什么)。
活人感是「该不该说」,现实感是「说的有没有东西」。前者靠克制,后者靠内容。
这个区分很重要,因为大多数团队只补第二个(改 prompt、加人设、调语气),而第一个是 结构问题——不是措辞能修的,是触发机制错了。你把「天刚亮打个照面」改成任何更自然的话,只要它还是 05:01 由定时器发到一个空群里,尴尬一分不减。
二、开口前的四道闸
我把该问的问题收成四道闸。任何一条过不去,就不发。
┌──────────────────────────────────────────────┐
消息 → │ 闸 1 触发源:谁让我开口的? │
│ 事件/被@/有产出 → 过 纯时间点 → 拦 │
├──────────────────────────────────────────────┤
│ 闸 2 信息量:这句话里有几件具体的事? │
│ ≥1 件(数据/待办/进展) → 过 0 件 → 拦 │
├──────────────────────────────────────────────┤
│ 闸 3 场子:最近 N 条都是我自己发的吗? │
│ 有活人在说 → 过 全是自己 → 拦 │
├──────────────────────────────────────────────┤
│ 闸 4 渠道:这是给人看的,还是给运维看的? │
│ 业务结论 → 群 运行状态 → 监控/私聊 │
└──────────────────────────────────────────────┘
↓
过四道闸才发
# generated by hugo AI
拿这几句话过一遍,每处都死得很清楚:
| 消息 | 闸 1 | 闸 2 | 闸 3 | 闸 4 |
|---|---|---|---|---|
| 05:01「天刚亮打个照面」 | ✗ 纯时间点 | ✗ 零件事 | ✗ 全是自己 | ✓ |
| 10:00「上午好,开工了」 | ✗ 纯时间点 | ✗ 零件事 | ✗ 全是自己 | ✓ |
| 「随时招呼一声」(10:00 那条的后半句) | — | ✗ 没法接的话 | — | ✓ |
| 10:00:55「送达尚未确认」 | ✓ 执行结束触发 | ✗ 无行动项 | ✗ 全是自己 | ✗ 运维状态 |
四道闸里,闸 1 是根,其余三条是它的并发症。因为一旦触发源是「时间到了」而不是「有事发生了」,它必然凑不出信息量(没事发生,哪来的数据)、必然撞上闸 3(没人因为定时器而说话,所以最近几条一定是它自己)、必然要拿客套话填空——定时问候不是措辞问题,是它在结构上无话可说,只能编话说。
三、最强反方:不刷脸,谁会记得它?
这个方案有一个很硬的反方,我得正面回答:数字员工是要被用起来的。它不主动出现,没人记得它存在,没人给它派活,最后变成一个装了没多久就没人用的功能。定时问候至少是一种运营手段——曝光量摆在那儿。
这个担心不是没道理,尤其是刚上线那阵子。但我认为它算错了一笔账:负曝光比没曝光更糟,而且它是有利息的。
在群里刷客套话,活人的反应只有三种:沉默、静音、把它移出群。沉默是最常见的——你没法接「随时招呼一声」,于是没人回,于是它下一条还是没人回。连续几次之后,群成员对这个机器人的印象就从「有个数字员工」变成「那个总在群里刷早安的机器人」。而一旦有人点了静音,它真正有用的产出也一起被静音了——这才是最贵的部分:你为了几次无效曝光,把未来所有有效触达一起卖掉了。
更麻烦的是这笔账是复利的。曝光的收益是一次性的(看到一次,多一次印象),而厌烦是累积的(每刷一次,加一分「这玩意很烦」)。用一次性收益换累积成本,是亏的。
那存在感靠什么?靠 有用的产出被看见。我们的另一个 harness 里根本没有定时问候任务,发言全部由入站消息或事件触发;兜底提示只在真的失败时发一句人话,任务统计默认只走单聊、不进群。它从不刷脸,但没人觉得它不在岗——因为它每次开口都带着一件具体的事。存在感不是发言次数给的,是发言内容给的。
(这里有个边界要说清:冷启动期确实需要让人知道它存在。但正确的做法是 上岗仪式一次讲清——它叫什么、负责什么、怎么召唤它、什么情况下它会主动说话——而不是每天提醒一次。一次说清楚是介绍,天天说是骚扰。)
四、四道闸怎么落到工程上
四道闸不是靠 prompt 里写「请克制」能实现的——模型不会知道自己上一条是什么时候发的,也不知道群里有没有活人说话。这四条全是 运行时状态判断,必须在 harness 里做。
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
class TriggerKind(Enum):
"""谁让我开口的。"""
EVENT = "event" # 告警/数据变化/任务完成 —— 有现实发生
MENTION = "mention" # 被 @ 或被直接问 —— 有人在等
SCHEDULE = "schedule" # 纯时间点 —— 什么都没发生
@dataclass
class SpeechGate:
"""开口前的四道闸。任何一道 False,这条消息就不该发出去。"""
recent_window: int = 10 # 闸 3:回看最近多少条
max_self_streak: int = 2 # 闸 3:自己连续发言上限
def gate1_trigger(self, kind: TriggerKind) -> bool:
"""闸 1 触发源:纯时间点触发一律拦。"""
return kind is not TriggerKind.SCHEDULE
def gate2_substance(self, message: str, facts: list[str]) -> bool:
"""闸 2 信息量:必须带至少一件具体的事(数据/待办/进展)。"""
return len(facts) >= 1 and len(message.strip()) > 0
def gate3_room(self, history: list[tuple[str, datetime]]) -> bool:
"""闸 3 场子:最近 N 条全是我自己发的,就闭嘴。"""
recent = history[-self.recent_window:]
streak = 0
for speaker, _ in reversed(recent):
if speaker == "self":
streak += 1
else:
break
return streak < self.max_self_streak
def gate4_channel(self, is_ops_status: bool, target: str) -> bool:
"""闸 4 渠道:运行状态不进业务群,走监控或私聊负责人。"""
if is_ops_status:
return target in {"monitor", "dm_owner"}
return True
def should_speak(self, kind, message, facts, history, is_ops_status, target) -> bool:
return all([
self.gate1_trigger(kind),
self.gate2_substance(message, facts),
self.gate3_room(history),
self.gate4_channel(is_ops_status, target),
])
# generated by hugo AI
几个实现上的细节,是「想过才知道」的:
- 闸 3 的
history必须是真实的群消息序列,不是自己的发送日志。 很多机器人只记「我发过什么」,不记「别人发过什么」,于是永远不知道自己在空房间里说了多久。这条依赖 IM 侧能读到群历史——拿不到群历史的场景,退一步用自己的连续发言计数,效果差一些但聊胜于无。 - 闸 2 的
facts要由任务侧结构化传入,不要让模型自己判断「我这句话有没有信息量」。 让模型自评信息量,它会觉得自己每句话都挺有信息量。正确的做法是:任务完成时产出一个结构化结果(指标、待办、进展),有结果才允许触发发言——没有 facts 就没有发言权,这在代码层面就是一行len(facts) >= 1。 - 闸 4 是渠道分流,不是内容过滤。 运行状态本身有价值,只是它的读者不是业务群——应该进监控渠道,或者私聊负责人。所以实现上不是「丢掉」,是「改道」。
- 四道闸要有旁路,且旁路必须显式。 告警、被 @、明确的任务产出,应该直接过闸(
TriggerKind.EVENT/MENTION天然满足闸 1)。危险的实现是把闸门做成可配置的开关然后默认关掉——那样过一阵子就没人记得它存在。
顺便说一个对照。我在 谁停掉了数字员工的早读? 里写过,我的数字员工有个「今日早读」任务:早上 8 点整由定时投递,读公众号后台、读 GA、digest 小红书、更新 token 消耗、读 aibase——五步,预算一小时。那也是定时器触发的,但它过得了闸 2:跑完有实打实的产出,值得开口。同样是 cron,一个触发的是活儿,一个触发的是寒暄。
定时器解决的是「别忘了做」,不是「该不该说」。 这两件事被混在一起,就产生了 05:01 的那句「天刚亮打个照面」——它把「别忘了打招呼」当成了「该打招呼」。
五、把活人感的定义补完整
回到开头那篇旧文。我在那里写:活人感 = 可验证的一致性。 现在我得给它加一条:
活人感 = 可验证的一致性 + 有节制的发言权。
一致性说的是「它是不是同一个『人』」,节制说的是「它是不是一个懂场合的人」。真同事让你舒服,一半是因为他记得你说过什么,另一半是因为 他不废话——他知道什么事该在群里说、什么事该私聊、什么事压根不用播报。
这一半恰恰是当下最被忽略的。整个行业的注意力都在给数字员工加东西:加形象、加音色、加人设、加记忆、加工具、加主动性。加到最后,它在群里一天说上十几二十句话,每句都客气,每句都没法接。而「加主动性」这条尤其危险——没有节制的主动性,比没有主动性更糟:前者让人想把它静音,后者只是让人忘了它。
至于去模板化用语(不用「打个照面」「随时招呼」这种固定客套),我认为它是四道闸的 结果而不是前提。闸 2 一旦生效——每句话必须带一件具体的事——客套话自然就没了,因为客套话的定义就是「不带事的话」。反过来,如果只改措辞不设闸,你会得到一批措辞更自然、但依然没人想回的定时消息。
所以整套规范其实只有一句:先问该不该说,再问怎么说。 大多数团队直接从第二个问题开始,于是做出了一个说话很好听、但没人愿意它说话的同事。
最后留一个我自己也没想清楚的问题:四道闸拦住的是「无话可说时的发言」,但还有一种更难的情况——它有话可说,可那些话此刻不值得打断别人。 比如它半夜跑完了一个分析,结论很重要,但没人在等;比如它发现了一个小异常,够不上告警,但攒三天会变成大事。
前者考验的是它对「紧急度」的判断,后者考验的是它对「累积效应」的判断。这两个都超出了四道闸的能力——四道闸只管「该不该现在说」,不管「该攒到什么时候说」。
你在实际落地里遇到过数字员工「话太多」或者「话太少」的问题吗?特别是那个更难的第二类——它是怎么决定什么时候开口的?欢迎留言聊聊。