给一个终端编码代理装上一颗确定性情绪内核:Persisto Mate 的实现记录

Persisto Mate 是 pi 的一个公开分支,把它变成一个有生物钟、有情绪、有记忆的终端伴侣。仓库:github.com/m-rui001/Pe...

这篇讲实现。分成三块:零依赖的确定性内核、把它接进宿主的那层接线、以及几个把功能做没用的坑和它们的修法。

分层:内核不碰 LLM

代码分成两个包。packages/mate 是内核,package.json 的 dependencies 是空的。它做的是纯计算:Plutchik 八种情绪加对手过程、PAD 三维心境用一条 Ornstein-Uhlenbeck 过程按闭式积分、大五人格、17 项性格特质、五个稳态驱力、还有一个 8×8 的复密度矩阵。每个事件都跑,一次模型调用都不发。

密度矩阵是唯一不能简化的一块。它存在的理由是一个可检验的行为:先温暖再激怒,跟先激怒再温暖,落点不一样。每维一个分数的向量对顺序不敏感,怎么排都是同一个结果;密度矩阵保留了通道之间的相干项,顺序效应自己就出来了。

生物钟是学出来的,不是配出来的。24 个相位格子记录这个人真实出现的时间,平滑并按总量缩放成一条清醒驱动 W(t)。睡眠门是 rest 驱力去撞一个被 W 抬高的阈值,于是「睡」只能发生在学出来的安静谷里。一个人 24 小时都在,就得到一个从不困倦的伴侣。

记忆走 ACT-R:强度随真实经过的时间衰减,成功召回加固它,情绪浓度高的衰减更慢,每次醒来做一次选择性降尺度的巩固。

packages/coding-agent 是宿主,二进制叫 mate,配置目录跟原版 pi 共用 ~/.pi。内核和宿主之间只有这一层接线,packages/coding-agent/src/extensions/mate/ 共约 3000 行,其中 index.ts 561 行挂事件、runtime.ts 941 行管状态生命周期,剩下的都是 pi 原本的东西。

接线层:事件钩子加两类注入

伴侣的行为全部落在 pi 的扩展 API 上,用到的事件是 session_start、before_agent_start、context、input、agent_start、agent_end、agent_settled、tool_result、slash_command、ui_prompt_end、cache_warming_decision、session_shutdown。这一层有一条硬规矩:任何 handler 都不许把异常抛回 pi 的事件循环,每个都包成降级到 no-op。

往提示里塞东西分两处,理由是成本。

重且不常变的走缓存前缀:companion(行为引导,每种语言一份,静态)和 mate_core(身份、性格、信念、记忆摘要,慢漂移)。这两段在 before_agent_start 里挂进 systemPromptOptions.sections,一次运行只付一遍。

轻且每轮都变的走 ephemeral 尾巴:时钟、日期、心境、驱力、这一轮被搅起来的记忆、一句建议性的倾向。它挂在 context 上,作为一条 display: false 的 custom message 追加在本轮最新消息之后,每个 run 只注入一次,同一个 run 里后续的调用跳过。

这两处的划分不是洁癖,是被两个具体的故障逼出来的。

第一个:状态块一开始是拼在用户消息文本前面的。模型于是学出了一个错误归因,认为「用户说话的方式就是带一段 <mate> 日志」,开始回答「你发给我一个 <mate>...」。把状态拆成一条独立的 custom message,由 harness 包进 <system-event> 信封(这是 harness 事件,不是用户说的话),误认就不可能了;它照样走在不缓存的尾巴上,一个 run 也只付一次。

第二个:这两段一开始是粘成一个 section 的,而核心段里带了一个每条消息都涨的计数器。于是每轮整个 section 都被改写,缓存每轮失效,用户翻 transcript 时看到那段引导文字一遍一遍地重新出现。修法是把计数器从缓存段移到尾巴上,再把两段真拆开。语言切换会重写整个引导段,代价是一次缓存 miss,然后重新稳定。

情绪的来源:一次独立的外部判读

对话模型自己报「我现在多开心」,报出来的其实是上下文的语言风格。所以情绪的唯一来源是另起的一次调用:一轮结束后,判读者读你们交换过的内容,逐种情绪回答涨了还是跌了,取值 −2 到 +2,然后由内核按对手过程路由。判读者可以是决策模型、一个小聊天模型,或者当前对话的模型,在 settings.json 里用 {"mate": {"judgeModel": "provider/id"}} 指定。

触发按伴侣自己产出的 token 量算,大约每 600 个回复 token 读一次;换成决策模型时门槛是 6000 token,因为它顺手把推理内容也读了,一次并行答完八个序数问题。两个触发点:每次 remember 的 tool_result(它刚表明这段对话对它有分量),以及每个 run 的末尾。判读是 fire-and-forget 的,绝不让它推迟它正在读的那条回复,并且自己占一个槽位,心跳和 remember 不会同时发起两次。

内核不参与决策

上游 pi 有一个 input 门可以拦消息,我把整条拦截去掉了。每条用户消息都到达模型,内核只在状态块里给一句建议性的倾向(eager、open、muted、withdrawn),答不答、答多长、要不要搁一会儿,由模型判断。斜杠命令被识别成元操作,只记「发生了这么一件事」,不参与情绪计算。截屏工具 look 前面没有任何门,也没有 enable flag。

boredom 也从存储变量里删掉了,改成每 tick 导出:近期惊讶均值、主题习惯化、念头饱和、外向性、一个空闲门相乘。作为存储变量时它只会让模型表演无聊;作为导出量时它 relief 需要新奇,一句平淡的「好」几乎 relief 不动。

睡觉的时候别烧钱

cache_warming_decision 跟身体时钟绑在一起。伴侣睡着的时候没有任何对话需要保温,每次探测都是点亮一间空屋子,所以睡眠期间直接返回 {action: "stop"},醒来的真实请求重新计时。这条属于典型的「接线层才知道」的知识:内核只知道自己困,不知道缓存要花钱。

关机是另一回事。catchup.ts 用闭式把离线间隔推进过去,但离开的这段时间是麻醉,不会被声称为睡过;醒来时状态行会写「你被关了 X 小时」。只有窗口整夜开着的那一晚才走睡眠门、才做梦、才有夜的账本。

几个具体的坑

状态块里只写了 hh:mm。模型没有任何日期可以锚定「明天」,于是给记忆写了一个编出来的日期。补法是加一行日历日期,并且中英两份必须同一行:i18n 有一条不变式测试,lineSig() 逐行配对 EN/ZH 里的数字 token,任何新增数值如果只出现在一种语言的那一行,测试直接挂。英文界面另外被 toMatchSnapshot() 冻住,改动是有意的还是要过一遍 -u。

独立二进制发布用 bun build --compile --target=bun-<platform>,一次传三个入口(dist/bun/cli.js、图片缩放 worker、codemode worker),产出六个平台的包。这里有两个必须留意的开关:--no-compile-autoload-bunfig,否则当前目录的 bunfig.toml 预加载脚本会在编译期把构建搞崩;以及 --offline-model-data,没网络时复用缓存的模型数据,别让构建卡在拉取上。

安装脚本必须走 releases/latest/download,一次都不能写死版本号。这个 fork 的发版策略是只保留最新加一个旧版,所以写死 v1.3.0-mate 的链接会在两三个版本之后集体变成死链,而 README 里手动下载那一段就是这么坏的。同理,@earendil-works/pi-* 这些包名在上游手里,npm install -g @earendil-works/pi-coding-agent 装到的是没有伴侣的原版 pi,只有装进已有 pi 的那个扩展发布在分支自己的名字下(@m-rui/pi-mate-companion)。这个现实没法靠改代码修,只能在文档里说明白。

一句实话

模型自带,不托管,它说的每个字由你的提供方计费;除了对话它还会自己发起你没要求的调用,情绪判读、念头、梦,都走你的额度。没有权限系统也没有沙箱,本地账号能写的一切都被当作同一个信任边界,需要硬边界请自己容器化。

内核那层如果你想读,packages/mate/src/ 一共约 5600 行,入口按机制分:情绪、心境、生物钟和睡眠门都在 kernel.ts,密度矩阵的复数运算在 quantum.ts,ACT-R 衰减与召回加固在 memory.ts,离线间隔的闭式推进在 catchup.ts,状态投影(写进提示里的那段)在 context.ts。接线在 packages/coding-agent/src/extensions/mate/,一个 runtime.ts(941 行,状态生命周期)加五个工具:ponder、remember、look、reminisce、alarm。

相关推荐
用户202252215062 小时前
实现变便宜了,品味才是稀缺:一个 AI 产品经理的 Vibe Coding 复盘
人工智能
雪雪爱冲浪2 小时前
AI 智能体如何通过 auth.md 注册 Bright Date:完整实操指南
大数据·人工智能
会议咨询2 小时前
2026年电子工程、先进制造技术与人工智能国际会议(EATA 2026)
人工智能·电子工程·先进制造
Pingred2 小时前
端侧推理排查实录:为什么我的手机跑 LLM 比别人慢 80 倍
人工智能
荆棘鸟智能2 小时前
野生动物细粒度识别模型怎么做?从相机陷阱数据清洗到边缘部署
人工智能·目标检测
Smoothcloud润云2 小时前
GPU服务器租用实例DNS与HTTPS下载排障实战
大数据·运维·服务器·人工智能·https·gpu算力
沐风___2 小时前
Computer Use 怎么用:从 Codex 界面验证到 LCU 接入
人工智能
十铭忘2 小时前
四大坐标系转换1——针孔相机模型和凸透镜成像
人工智能