让 AI 助手有自己的作息
问一句答一句,这叫工具。
知道什么时候该主动开口、什么时候闭嘴,才像助手。
这篇记的是给本地 AI 助手加上「作息」时想清楚的三件事:人格怎么落地、定时怎么不漂、主动消息怎么不变成骚扰。
一、人设不是一段提示词
最开始的写法是把人格写进系统提示词:「你叫某某,性格如何,说话怎样」。
问题很快出现:
- 提示词一长,人格部分被后续规则淹没,回答逐渐变回默认腔调
- 想换一个人格,要改一个很大的文件,容易碰坏别的规则
- 同一份人格没法复用到别的场景
改成「预设」:一个目录,里面放配置和人格文本,加载时拼进系统提示词的开头。
好处是三个:
可切换。 不同预设对应不同的工具集和说话方式,切过去就是另一个人。
可复制。 人格成为独立文件,能版本管理、能给别人。
不互相污染。 人格在最前面,后面的规则是「怎么做」,两件事分开写,改一个不会动另一个。
一个反直觉的地方:人格写得越具体,越容易变成表演。「你是一个冷静克制的人」比「你的口头禅是」有效得多——后者会让人格变成台词。
二、定时不是 sleep
第一版用固定间隔:每 30 分钟跑一次。
结果是漂移。每次任务的执行时间不同,间隔从上一次结束开始算,一小时能积累出几分钟的偏差。跑一天下来,原本对齐整点的任务变成对齐「某个奇怪的时间」。
改成 cron 表达式:*/30 * * * * 表示每个整点和半点。触发点固定在墙上时间,和上一次跑多久无关。
代价是可能重叠:上一次还没跑完,下一次的时间到了。所以要显式处理——正在跑就跳过这一轮,而不是排队。
错过怎么办 是另一个必须决定的事。
进程重启、机器休眠、任务被卡住,都会错过触发点。有两种策略:
- 补上:把错过的每一次都执行一遍
- 只补最近一次:跳到当前时间最近的那个触发点
第一种在长时间停机后会制造一场雪崩——几十个任务同时开始。所以选第二种。
改了 cron 还是漂
第一版我写的是 OnUnitActiveSec=30min,以为它等价于「每 30 分钟」。
不是。它是距上一次激活 30 分钟——上一次跑完多久,会原样叠加进间隔。
这台机器上的采样任务是一分钟一次,用这种写法实测 24 小时只采到 1238 次,比理论值 1440 少 14%。单看一两次完全看不出来,跑一天才发现数字对不上。
# 错的:周期 = 间隔 + 执行耗时
OnUnitActiveSec=60s
# 对的:挂在墙上时间,和跑多久无关
OnCalendar=*:0/1
OnBootSec=45s
AccuracySec=5s
AccuracySec 也值得留意:默认 1 分钟,意思是「这一分钟内什么时候触发都行」。对采样任务这会把时间戳全部打散,要显式收紧。
这个偏差最麻烦的地方不是漏采样,是它会让所有依赖「次数」的判断失真。比如「连续三次失败才告警」,实际要等更久才触发。
三、主动消息的两个坑
有了定时和人格,下一步是「它能主动找你」。
技术上不难:一个推送通道,往某个对话发一条消息。
难的是判断什么时候该发。
该发:任务完成、需要人做决定、发现异常。
不该发:进度更新(人不需要知道每一步)、重复提醒(上次已经说过)、自己也不确定要不要说的。
最后一条最重要。「这条消息发出去,对方会因此改变什么吗?」如果答案是不会,就别发。
推送是打扰。每一次打扰都在消耗对方对「这个助手说话值得看」的信任。信任消耗完,所有消息都变成噪音,包括真正重要的那些。
什么时候不打扰
有了主动消息,第二个问题不是「发什么」,而是「什么时候不发」。三条开关值得先想清楚:
- 静默时段:凌晨不推送,攒到早上合并成一条
- 同一条只发一次:状态没翻转就不重复,翻转了才发
- 对方正在说话时先别说:你正在问它问题,手机同时弹一条「任务完成」,等于同一个人跟你说了两遍
第三条最容易被忽略。它不需要技术实现,只需要一个「当前是否在对话中」的判断。
四、边界
两件事不做:
不替人做决定。 可以给出建议、列出选项、说明后果,但把「选哪个」留给人。助手越界最典型的表现就是「我已经帮你做了」。
不在不确定时假装确定。 「我不确定」比一个编造的很确定的答案有用得多。
小结
给人格一个独立的位置,给时间一个固定的锚点,给消息一个「值不值得发」的判据。
这三件事都不难,难的是想清楚为什么要这么做——否则做出来的只是一个会定时打扰你的脚本。