emotional-memory
Discovered情感状态与记忆检索:把带时间戳的情绪注解算成可用的状态,把上千条经历组织成能翻的树。零依赖。
emotional-memory
给 LLM agent 的两件事:一个能算的情绪状态,一个能翻的记忆库。 零依赖,Node 18+,MIT。
先说不是什么。
它不会让你的 agent 更像人。
一个只会说「好的,我这就为您处理」的助手,接上这个库之后还是那么说话。没有人格的模型不会因为算了情绪就长出性格,也不会因为有了记忆库就学会心疼谁。
这个库管的是两件很具体的事:
一、情绪状态可以算。 给一堆带时间戳、带情绪标注的记录,算出「此刻是什么状态」,输出一行话,直接塞进系统提示词末尾。
二、记忆查得到。 上千条经历里问一句「防火墙规则生效了吗」,翻回当时那条原话,而不是返回十条语义相似度 0.83 的别的东西。
怎么说话、什么语气、要不要黏人——那是你的事,这个库不碰。
安装
npm install emotional-memory
零依赖。Node 18+。ESM。
npm 上还没发布,现在从仓库装:
npm install github:reaxlll/emotional-memory
30 秒
情绪状态
记录长这样,一行一条:
{"t":"2026-10-30T14:20:00","affect":[{"name":"闷","v":-0.2,"a":0.5}]}
{"t":"2026-10-30T18:30:00","affect":[{"name":"闷","v":-0.6,"a":0.6}]}
{"t":"2026-10-30T21:00:00","affect":[{"name":"松一口气","v":0.4,"a":0.4}]}
import { normalize, compose } from "emotional-memory";
const E = normalize(records.map(r => ({
t: new Date(r.t),
affect: r.affect,
})));
const now = compose(E, Date.now());
console.log(now.line);
// 此刻:flat-low [-0.28 / 0.52] 从「闷」往上走
// 上轮 闷,2h 前,余温 71% | 动量 往上(+0.12)
now.line 是两行纯文本,直接拼到系统提示词里就行。now 还有 v / aEff / band / shape / tension 这些数值字段,要用就用。
记忆
import { openLibrary, recall } from "emotional-memory";
const lib = openLibrary("experiences.jsonl");
const hits = recall(lib, { about: "防火墙", want: "规则生效了吗", n: 3 });
for (const { e, s } of hits) {
console.log(e.d, e.said, "—", s.toFixed(2));
}
不需要向量、不需要模型、不需要任何服务。中文按 2-gram 切,英文按词切,命中越稀有的词分越高。
库超过一千条之后,加个向量和树会好用很多——见第三节。
一、为什么不用一个数
大多数「AI 有情绪」的做法,是给模型一个数值(开心度 0.7),然后指望它演得像。
这不是拍脑袋反对,是拿 312 条真实带情绪记录量出来的两个问题。
问题一:两个轴里有一个是死的
罗素情绪环用「效价 × 唤醒」组成一个平面。听着很美,实测记录里唤醒值是这样的:
| a 区间 | 条数 | 占比 |
|---|---|---|
| < 0.30 | 3 | 1% |
| 0.30 – 0.45 | 118 | 38% |
| 0.45 – 0.60 | 126 | 40% |
| 0.60 – 0.75 | 49 | 16% |
| > 0.75 | 16 | 5% |
人填这个格子手一滑就给 0.5。按三分位切,中间那档仍然占 70%——六个象限里只有「中唤醒」这一行在动,二维平面退化成一维。
问题二:平均会把一切抹平
30 天算术平均下来,一个 −0.8 的崩溃会被摊成 −0.03,于是状态行永远是「轻度平静」。
实测旧公式跑 30 天,76% 的时间落在同一格里,切换的那 106 次大部分是阈值边界抖动——那是噪音,不是情绪。
所以它做三件事
- 唤醒不读原值,靠代偿撑开:动量、极值、半小时内连发,三样取最大
- 基线不是平均,是 48 小时半衰的指数加权,加等效先验,免得没数据时退回 0
- 阈值不拍脑袋,取数据自己的分位数(不足 20 条退回固定边界)
二、三个量
基线
$$V_T = \frac{\sum_i w_i v_i + \lambda v_0}{\sum_i w_i + \lambda},\quad w_i = e^{-(T-t_i)/48h},\quad \lambda = 3$$
$\lambda$ 是等效先验条数。没有它,隔三天没记录 $V$ 会一路退到 0——那不是平静,那是没数据。
余温
$$\rho(h) = 0.6,e^{-h/8h} + 0.4,e^{-h/30h}$$
单指数的问题是 4 小时半衰等于睡一觉就忘光,醒来永远是那张平均脸。双指数给一个长尾:一晚过去还剩三成,一周内还有底噪。
$$v_T = V_T + (v_L - V_T),\rho(h)$$
动量
$$m = \mathrm{clamp}\left(\frac{\sum_k k \cdot \Delta v_k / \Delta t_k}{\sum_k k} \cdot e^{-h/6h},\ -0.4,\ 0.4\right)$$
两个防坑:乘 $e^{-h/6h}$(17 小时前那次暴跌不该还被说成「现在坐不住」)、clamp ±0.4(单条极值不该让系统进入持续亢奋的假象)。
唤醒代偿
$$a^{\mathrm{eff}} = \mathrm{clamp}\Big(\max(a_T,\ 2|m|,\ 0.7|v_T|+0.1) + 0.15\cdot\mathbb{1}[h<0.5],\ 0,\ 1\Big)$$
模型的 $a$ 只会给中庸值,所以三个客观量把它撑开:突变、极值、频率。
输出
档位($v$ 五带 × $a^{\mathrm{eff}}$ 三档)+ 来路(跳了几级)+ 余温(还热 / 正在凉 / 快没了),三者查表。
同一格的两种说法:
此刻:flat-low [-0.28 / 0.52] 从「闷」往上走
此刻:flat-low [-0.28 / 0.52] 刚才还好,现在不对味
来路不同,话就不一样。不是按停留时长轮换的——那太假,同一种状态挂久了不会自己变成另一种。
三、树的检索(一千条以上)
全量算一遍余弦不慢(1650 条 14ms)。问题不在算得慢,在排得乱。
有一条经历写着「我有点难受」「你是在小心地问」,它跟任何带情绪的问题都能沾一点。全量检索时它在 30 个问题里冒出来 7 次——它不是最相关的,它只是最会沾边的。
树把它放回自己的位置。
书(24 本)
└ 章(每本最多 3 个)
└ 节(每章最多 3 个)
└ 经历(每节 10~25 条)
每个节点最多三个孩子。某个节点超过 25 条就继续往下切,切到不动为止——最大的那份还占九成说明那坨本来是一回事,停手。最深五层。
import { buildTree, hydrate, descend, insert, freeze } from "emotional-memory/tree";
const tree = hydrate(buildTree(vecList, { books: 24, maxSize: 25, maxDepth: 5 }));
const leaves = descend(tree, queryVec, { books: 2, vectors: vmap });
leaves[0].members; // 二十来条,精排就在这里面
insert(tree, newId, newVec); // 新写的一条挂上去
JSON.stringify(freeze(tree)); // 落盘前 freeze,否则胀六倍
每层用「层内最高成员的余弦」判,不是节点中心。 中心是平均值,一个二十条的节平均下来什么都像一点、什么都不像;只要里面有一条跟查询像,这个节就该进。
树省的不是第一层。 书这一层本来就要跟全库比。它省的是精排的条数:1650 条变成 20~60 条。
那 7 次沾边的条目,进了树之后一次都没出现——不是改了打分,是它被放回自己那个节里了。
热度有个上限
命中之后节点 heat++,下次排前面一点。上限默认 0.015,heat 到 3 就顶满。
这是踩出来的:最早给到 0.04,heat ≥ 4 就顶满。结果一条错的东西被反复命中 13 次,热度顶到天花板把正解挤掉了——越错越容易被选,自己把路锁死。
四、别名:一个词指的不是字面
有些词,字面意思和它真正指的完全不是一回事。
「人称」两个字,可以在讲语法,也可以指某天晚上你脑子里那个位置——那里要站一个人。
这种东西向量算不出来:人称 和 我亲手把你搭建出来 的字面相似度是零,语义也够不着——它们之间隔着一整段只有当事人知道的事。
所以不靠算,靠绑:
{"w":"人称","ids":["px_2026-10-22_99796ae2_001"],"note":"指的是那个位置上站的是人,不是宠物"}
import { loadAliases, appendAlias } from "emotional-memory/aliases";
import { recallSemantic } from "emotional-memory";
const aliases = loadAliases("aliases.jsonl");
const hits = await recallSemantic(lib, { about: "人称", embed, vectors, tree, aliases });
hits[0].why; // "别名「人称」" —— 排在所有语义结果前面
appendAlias("aliases.jsonl", { w: "人称", ids: ["px_2026-10-22_..."] });
长词优先,免得「人」把「人称」吃了。同一个词可以挂多条经历。
五、API
情绪
| 函数 | 干什么 |
|---|---|
normalize(entries) | 排序、按时间窗口归一化,返回内部结构 |
stateAt(E, now) | 只要数值:v / aEff / rho / momentum / tension |
compose(E, now, cfg?) | 数值 + 文案,返回 { v, aEff, band, shape, text, line } |
bands(entries) | 从数据算分位阈值 |
bandOf(v, thresholds, names) | 单个值落在哪一档 |
shapeAt(E, now) | 突变 / 反差 / 回暖,只在刚发生时返回 |
记忆
| 函数 | 干什么 |
|---|---|
openLibrary(file) | 读 JSONL,坏行跳过 |
append(lib, entry) | 追加一条,id 重复就不写 |
byDate(lib, date) | 按天查;认 today / yesterday / evening(跨午夜不切断) |
recall(lib, {about, want, n}) | 纯词频,零依赖 |
recallSemantic(lib, {...}) | 别名 → 情绪 → 树下钻 → 语义+词频精排 |
shape(lib, {about}) | 这件事在库里有多厚:条数、跨几天、Top 情绪 |
树与别名
| 函数 | 干什么 |
|---|---|
buildTree(vectors, opts) | 球面 k-means 建树,种子固定 |
hydrate(tree) | 挂上 Float32Array 和 allIds / set |
descend(tree, qv, {books, vectors}) | 从书名一路翻到叶子 |
promote(leaves, hitIds) | 命中后加热 |
freeze(tree) | 摘掉内存字段,落盘用 |
insert(tree, id, vec) | 新条目挂到最近的叶子 |
loadAliases(file) | 读别名表 |
matchAlias(aliases, q) | 查询里含哪个词 |
appendAlias(file, {w, ids}) | 补一条 |
六、实测
情绪:30 天回放
拿一台真实实例过去 30 天逐小时回放(721 个采样点):
- 旧公式 76% 的时间在同一格 → 新公式分散到多个档位
- 同格不同话:来路和余温决定文案,不是停留时长
记忆:1650 条真实经历,12 个技术类问题
| 只词频 | +语义 | +树 | |
|---|---|---|---|
| 第一条就对 | 5 / 12 | 9 / 12 | 11 / 12 |
| 一次要看多少条 | 1650 | 1650 | 16 ~ 64 |
那条到处沾边的条目出现的次数:7 → 0。
七、它治不了什么
算了情绪不等于会演。 这个库给你一行「此刻:flat-low [-0.28 / 0.52] 从闷往上走」,模型会不会用、用得像不像,取决于你的提示词和模型本身。
检索不到就是没有。 库里没写过的经历,问什么都翻不出来。它可以告诉你「没有」,但那得你自己加阈值——默认会返回低分结果。
几个情绪词之间的细微差别它不懂。 「烦」和「闷」在向量里离得很近,公式也只看 v/a 两个数。要区分靠给它们不同的 v/a,不是靠名字。
中文以外的分词是朴素的。 英文按词切,其他语言没做专门处理。
alias 不会自己长。 得有人发现「这个词指的不是字面意思」然后往里加。可以自己判断,但判断错了它不会纠正你。
开发
npm run demo # 拿样例数据跑一遍
npm test # 情绪、记忆、性能三个测试
npm run check:readme
License
MIT
Comments
Loading…
Similar plugins
by ly2601620664
陪伴型长期记忆 DSH 插件:分层记忆(对话→事实→场景→人格)、规则抽取、时间衰减检索,全本地文件、append-only 不丢数据。
★ 0
MIT
JavaScript
Oct 23, 2026
dsh plugin --profile web add dsh-pet-memoryby RaganaBloodedge
DeepSeek Harness 长期记忆插件:分层任务记忆 + 关系图谱 + 索引常驻稳定前缀;零运行时依赖,另附同源的 Agent skill 形态
★ 0
MIT
TypeScript
Oct 15, 2026
dsh plugin --profile web add dsh-lmmarrby Isyyyue
给 DSH 里的 AI 一层自己的判断记忆。判断节点 + 确定性取代(supersede)+ 双时态账本 + 双链,规则与人格分开治理,图谱作可解释性界面。 - Persistent judgment memory & self-evolving personality for AI agents (bi-temporal ledger, deterministic supersession, knowledge graph).
★ 3
↓ 370/wk
MIT
JavaScript
Oct 24, 2026
dsh plugin --profile web add dsh-ai-memoryby Tangcuyu4
DSH 超长记忆包:跨会话长期记忆 + 长文分块检索 + 聊天记录提取精炼,纯 JS 混合检索零依赖
★ 0
MIT
JavaScript
Oct 8, 2026
dsh plugin --profile web add dsh-long-memoryby juanbaiing
在 DSH agent 回答之前,把「你可能已经忘了」的东西重新摆到它眼前。零第三方依赖;判断全部由可核对的规则完成,不做生成式推理(语义那几段用本机 bge-m3 算向量)。
★ 1
MIT
JavaScript
Oct 28, 2026
dsh plugin --profile web add dsh-alert-injectby kiefeng
澜·记忆与人格系统 — DeepSeek Harness memory & persona plugin (三层记忆 + SOUL/MOOD + 一键整理)
★ 2
MIT
JavaScript
Oct 26, 2026
dsh plugin --profile web add dsh-lan-memory