🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

情绪记忆库 reaxlll/emotional-memory

DeepSeek Harness
reaxlll1 天前有提交已提供中文文档扫描:未发现敏感能力

把带时间戳的情绪记录算成当前状态,并支持按原话检索上千条经历

29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/11/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add reaxlll/emotional-memory
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · skill
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

情感状态与记忆检索:把带时间戳的情绪注解算成可用的状态,把上千条经历组织成能翻的树。零依赖。

README

由 DeepSeek 最新模型翻译生成
emotional-memory 给 LLM 智能体两样东西:一个能算的情绪状态,一个能翻的记忆库。 零依赖,Node 18+,MIT。 先说不是什么。 它不会让你的智能体更像人。 一个只会说「好的,我这就为您处理」的助手,接上这个库之后还是那么说话。没有人格的模型不会因为算了情绪就长出性格,也不会因为有了记忆库就学会心疼谁。 这个库管的是两件很具体的事: 一、情绪状态可以算。 给一堆带时间戳、带情绪标注的记录,算出「此刻是什么状态」,输出一行话,直接塞进系统提示词末尾。 二、记忆查得到。 上千条经历里问一句「防火墙规则生效了吗」,翻回当时那条原话,而不是返回十条语义相似度 0.83 的别的东西。 怎么说话、什么语气、要不要黏人——那是你的事,这个库不碰。 安装 npm install emotional-memory 零依赖。Node 18+。ESM。
展开完整文档(README 剩余 1 节)
emotional-memory

给 LLM 智能体两样东西:一个能算的情绪状态,一个能翻的记忆库。
零依赖,Node 18+,MIT。

先说不是什么。

它不会让你的智能体更像人。

一个只会说「好的,我这就为您处理」的助手,接上这个库之后还是那么说话。没有人格的模型不会因为算了情绪就长出性格,也不会因为有了记忆库就学会心疼谁。

这个库管的是两件很具体的事:

一、情绪状态可以算。 给一堆带时间戳、带情绪标注的记录,算出「此刻是什么状态」,输出一行话,直接塞进系统提示词末尾。

二、记忆查得到。 上千条经历里问一句「防火墙规则生效了吗」,翻回当时那条原话,而不是返回十条语义相似度 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 次大部分是阈值边界抖动——那是噪音,不是情绪。

所以它做三件事

1. 唤醒不读原值,靠代偿撑开:动量、极值、半小时内连发,三样取最大
2. 基线不是平均,是 48 小时半衰的指数加权,加等效先验,免得没数据时退回 0
3. 阈值不拍脑袋,取数据自己的分位数(不足 20 条退回固定边界)

二、三个量

基线

$$V_T = \frac,\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 \cdot e^{-h/6h},\ -0.4,\ 0.4\right)$$

两个防坑:乘 $e^{-h/6h}$(17 小时前那次暴跌不该还被说成「现在坐不住」)、clamp ±0.4(单条极值不该让系统进入持续亢奋的假象)。

唤醒代偿

$$a^} = \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^}$ 三档)+ 来路(跳了几级)+ 余温(还热 / 正在凉 / 快没了),三者查表。

同一格的两种说法:

此刻: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
预览分享卡片长什么样 ↓reaxlll/emotional-memory
分享 · DeepSeek Harness Hub
reaxlll/emotional-memory

把带时间戳的情绪记录算成当前状态,并支持按原话检索上千条经历

回到顶部