mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
776 lines
28 KiB
Markdown
776 lines
28 KiB
Markdown
# ST-BME
|
||
|
||
> 面向 [SillyTavern](https://github.com/SillyTavern/SillyTavern) 的图谱记忆扩展。它把聊天过程转成结构化记忆图,再在生成前按场景召回并注入,服务于长对话、角色扮演、持续剧情和世界观管理。
|
||
|
||
## 项目概览
|
||
|
||
ST-BME 的全称是 **ST-Bionic-Memory-Ecology**。它不是一个独立的后端服务,也不是一个通用数据库,而是一个运行在 SillyTavern 第三方扩展体系内的前端记忆层。
|
||
|
||
它主要做两件事:
|
||
|
||
1. **写入**:从聊天内容中抽取结构化记忆,存入当前聊天对应的知识图谱。
|
||
2. **读取**:在下一次生成前,从图谱中找出与当前输入最相关的记忆,并把它们整理成适合模型理解的上下文片段。
|
||
|
||
项目目标不是替代大模型的原生上下文能力,而是为长期互动补上一层可累计、可更新、可压缩、可检索的外部记忆系统。
|
||
|
||
## 这个项目解决什么问题
|
||
|
||
在长期 RP 或持续陪伴式对话里,模型通常会遇到几类典型问题:
|
||
|
||
- 早期剧情、角色状态、地点细节很快被上下文窗口挤掉。
|
||
- 模型会记得“有这回事”,但不容易稳定地记住“谁在什么时候做了什么、现在又变成什么状态”。
|
||
- 角色状态和地点状态会随剧情变化,旧信息与新信息容易混杂。
|
||
- 世界规则、主线目标、前情提要常常需要持续注入,但又不能把所有历史全文塞回 prompt。
|
||
|
||
ST-BME 的思路是把“聊天历史”变成“图谱化记忆”:
|
||
|
||
- 事件、角色、地点、规则、主线、概要、反思都以节点表示。
|
||
- 节点之间通过关系边连接,形成可扩散的结构。
|
||
- 重要、常驻的信息直接进入 Core 注入。
|
||
- 与当前用户输入强相关的状态和补充记忆再走召回注入。
|
||
|
||
## 项目边界
|
||
|
||
当前实现很明确地有以下边界:
|
||
|
||
- **单聊天作用域**:每个聊天维护一份独立图谱,图状态挂在当前聊天 `chat_metadata` 下。
|
||
- **无独立数据库**:没有额外服务端存储层,所有持久化都依赖 SillyTavern 的聊天元数据保存机制。
|
||
- **LLM 与 Embedding 分离**:
|
||
- 结构化提取、精确召回、压缩、进化、概要、反思都通过 ST 内部的 `sendOpenAIRequest('quiet', ...)` 调用聊天模型。
|
||
- 向量检索依赖单独配置的 OpenAI 兼容 Embedding API。
|
||
- **图谱是工程化记忆,不是事实真相库**:它依赖 LLM 的结构化输出质量,因此仍然存在抽取偏差、更新遗漏、关系误判等风险。
|
||
|
||
## 运行依赖
|
||
|
||
| 依赖 | 是否必需 | 作用 |
|
||
| --- | --- | --- |
|
||
| SillyTavern 第三方扩展系统 | 必需 | 提供事件钩子、设置存储、聊天上下文、Prompt 注入接口 |
|
||
| 当前可用的聊天模型 | 必需 | 用于提取、精确召回、压缩、进化、概要、反思等所有 LLM 子任务 |
|
||
| OpenAI 兼容 Embedding API | 向量检索相关功能必需 | 用于节点 embedding、向量预筛、Mem0 风格近邻对照、记忆进化近邻搜索 |
|
||
| 当前聊天元数据 | 必需 | 存储图谱状态、最后处理楼层、最后召回结果 |
|
||
|
||
## 系统总览
|
||
|
||
```text
|
||
聊天消息
|
||
├─ assistant 回复完成后
|
||
│ └─ ST-BME 提取未处理片段
|
||
│ ├─ LLM 生成 create/update/delete 操作
|
||
│ ├─ 执行图谱写入
|
||
│ ├─ 生成缺失 embedding
|
||
│ ├─ 可选执行进化 / 概要 / 反思 / 遗忘 / 压缩
|
||
│ └─ 保存回 chat_metadata
|
||
│
|
||
└─ 下次生成前
|
||
└─ ST-BME 检索当前图谱
|
||
├─ 可见性过滤
|
||
├─ 向量预筛
|
||
├─ 图扩散
|
||
├─ 混合评分
|
||
├─ 可选 LLM 精确召回
|
||
├─ 场景重构
|
||
└─ 格式化为注入文本并送入 prompt
|
||
```
|
||
|
||
## 与 SillyTavern 的集成方式
|
||
|
||
ST-BME 的主入口在 [index.js](./index.js)。它不是轮询式工作的,而是绑定在 SillyTavern 的事件生命周期上:
|
||
|
||
| ST 事件 | 对应逻辑 | 作用 |
|
||
| --- | --- | --- |
|
||
| `CHAT_CHANGED` | `onChatChanged()` | 切换聊天时重新加载该聊天的图谱 |
|
||
| `GENERATION_AFTER_COMMANDS` | `runExtraction()` | assistant 回复完成后,处理尚未提取的内容 |
|
||
| `GENERATE_BEFORE_COMBINE_PROMPTS` | `runRecall()` | 下一轮生成前召回记忆并注入 |
|
||
| `MESSAGE_RECEIVED` | `onMessageReceived()` | 新消息到达时保存当前图状态 |
|
||
|
||
这意味着 ST-BME 的运行时机非常清楚:
|
||
|
||
- **写入发生在回复之后**,记录刚刚发生了什么。
|
||
- **读取发生在下一次生成之前**,决定接下来模型应该看见哪些记忆。
|
||
|
||
## 数据存储与持久化
|
||
|
||
图谱键名固定为 `st_bme_graph`,存储在当前聊天的 `chat_metadata` 中。
|
||
|
||
图状态的核心结构如下:
|
||
|
||
| 字段 | 含义 |
|
||
| --- | --- |
|
||
| `version` | 图数据版本号,当前实现为 v3 |
|
||
| `lastProcessedSeq` | 已处理到的聊天楼层索引 |
|
||
| `nodes` | 全部节点,包括活跃和归档节点 |
|
||
| `edges` | 全部关系边,包括失效边和历史边 |
|
||
| `lastRecallResult` | 最近一次召回选中的节点 ID 列表 |
|
||
|
||
图数据由 [graph.js](./graph.js) 管理,支持:
|
||
|
||
- 空图创建
|
||
- 节点/边增删改查
|
||
- 时序链表维护
|
||
- 时序边失效处理
|
||
- 版本迁移与兼容反序列化
|
||
- 导入导出
|
||
|
||
### 节点公共字段
|
||
|
||
所有节点都会带有一套统一元数据:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `id` | UUID |
|
||
| `type` | 节点类型 |
|
||
| `level` | 压缩层级,原始节点为 0 |
|
||
| `parentId` / `childIds` | 压缩层级父子关系 |
|
||
| `seq` | 该节点对应的主楼层索引 |
|
||
| `seqRange` | 节点覆盖的楼层范围 |
|
||
| `archived` | 是否归档 |
|
||
| `fields` | 业务字段主体 |
|
||
| `embedding` | 向量表示 |
|
||
| `importance` | 重要性,范围 0-10 |
|
||
| `accessCount` | 被召回/注入的访问次数 |
|
||
| `lastAccessTime` | 最近被访问时间 |
|
||
| `createdTime` | 节点创建时间 |
|
||
| `prevId` / `nextId` | 同类型节点的时间链表 |
|
||
| `clusters` | 额外标签/聚类信息 |
|
||
|
||
### 边公共字段
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `id` | UUID |
|
||
| `fromId` / `toId` | 边起点和终点 |
|
||
| `relation` | 关系类型 |
|
||
| `strength` | 边强度,范围 0-1 |
|
||
| `edgeType` | 边类型标记,`255` 表示抑制边 |
|
||
| `createdTime` | 创建时间 |
|
||
| `validAt` | 生效时间 |
|
||
| `invalidAt` | 失效时间 |
|
||
| `expiredAt` | 系统标记过期时间 |
|
||
|
||
其中 `contradicts` 关系会被映射成抑制边,后续在扩散阶段会传递负能量。
|
||
|
||
## 默认 Schema
|
||
|
||
默认 Schema 定义在 [schema.js](./schema.js)。它不仅定义了字段,还定义了注入策略、更新策略和压缩策略。
|
||
|
||
| 类型 | 作用 | `alwaysInject` | `latestOnly` | 压缩 | 说明 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| `event` | 事件、动作、阶段推进 | 是 | 否 | 分层压缩 | 当前实现里属于 Core 常驻注入 |
|
||
| `character` | 角色状态快照 | 否 | 是 | 不压缩 | 同名 `create` 会转成 `update` |
|
||
| `location` | 地点状态快照 | 否 | 是 | 不压缩 | 同名 `create` 会转成 `update` |
|
||
| `rule` | 规则、约束、世界设定 | 是 | 否 | 不压缩 | 常驻注入 |
|
||
| `thread` | 主线或任务线 | 是 | 否 | 分层压缩 | 常驻注入 |
|
||
| `synopsis` | 全局前情提要 | 是 | 是 | 不压缩 | 只保留最新一条概要 |
|
||
| `reflection` | 高层反思与长期提示 | 否 | 否 | 分层压缩 | 通过召回进入上下文 |
|
||
|
||
### 关系类型
|
||
|
||
默认关系类型包括:
|
||
|
||
- `related`
|
||
- `involved_in`
|
||
- `occurred_at`
|
||
- `advances`
|
||
- `updates`
|
||
- `contradicts`
|
||
- `evolves`
|
||
- `temporal_update`
|
||
|
||
## 写入链路详解
|
||
|
||
写入逻辑主要集中在 [index.js](./index.js) 和 [extractor.js](./extractor.js)。
|
||
|
||
### 1. 提取触发条件
|
||
|
||
ST-BME 只统计 **assistant 消息** 来决定何时提取。
|
||
|
||
默认策略:
|
||
|
||
- `extractEvery = 1` 时,每 1 条 assistant 回复提取一次。
|
||
- `lastProcessedSeq` 记录的是聊天数组索引,因此它用的是“楼层”语义,而不是消息 ID。
|
||
|
||
如果开启 `enableSmartTrigger`,则会在普通频率判断外再做一次轻量触发评分。评分来源包括:
|
||
|
||
- 命中默认关键词
|
||
- 命中自定义正则
|
||
- 用户与助手多轮往返
|
||
- 感叹号/问号等情绪波动
|
||
- 疑似新实体/新地点
|
||
|
||
当评分达到阈值时,即使未到 `extractEvery`,也会直接处理所有待处理 assistant 楼层。
|
||
|
||
### 2. 提取上下文打包
|
||
|
||
真正送去提取的不是单条消息,而是一段上下文窗口。
|
||
|
||
当前实现会:
|
||
|
||
- 找到本批 assistant 楼层的起止索引 `startIdx` / `endIdx`
|
||
- 从 `startIdx - extractContextTurns * 2` 开始回溯
|
||
- 取到 `endIdx` 为止的非系统消息
|
||
- 用 `#楼层 [role]: content` 的形式拼成对话文本
|
||
|
||
这样做的目的,是让 LLM 在提取时看到足够的上下文因果关系,而不是孤立处理单条回复。
|
||
|
||
### 3. LLM 结构化提取
|
||
|
||
提取调用在 [llm.js](./llm.js) 中统一封装,要求模型返回严格 JSON,核心产物是:
|
||
|
||
```json
|
||
{
|
||
"thought": "对这一批对话的理解",
|
||
"operations": [
|
||
{
|
||
"action": "create",
|
||
"type": "event",
|
||
"fields": {
|
||
"summary": "......"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
当前默认提示词会约束模型:
|
||
|
||
- 支持的节点类型必须来自当前 Schema
|
||
- 每批对话最多创建 1 个事件节点
|
||
- 角色/地点优先更新已有同名节点,而不是无脑新建
|
||
- 关系类型必须来自允许列表
|
||
- `importance` 落在 1-10
|
||
- `summary` 应该是抽象摘要,而非原文复制
|
||
|
||
### 4. Mem0 风格精确对照
|
||
|
||
如果开启 `enablePreciseConflict` 且配置了 Embedding API,则在正式执行操作前,会先对所有 `create` 操作做近邻对照:
|
||
|
||
1. 对新事实文本生成 embedding。
|
||
2. 在已有活跃节点中搜最相似近邻。
|
||
3. 若最高相似度超过阈值,则调用 LLM 判断:
|
||
- `add`
|
||
- `update`
|
||
- `skip`
|
||
|
||
这一步的目标是降低重复节点、弱冲突节点和应该被视作“状态更新”的伪新增节点。
|
||
|
||
### 5. 操作执行语义
|
||
|
||
#### `create`
|
||
|
||
- 正常创建新节点。
|
||
- 若类型是 `latestOnly` 且存在同名旧节点,则自动转为更新旧节点。
|
||
- 同批次创建的节点支持通过 `ref` / `targetRef` 建立链接。
|
||
|
||
#### `update`
|
||
|
||
更新的处理比普通字段覆盖更复杂,当前实现还会补出一层“可追踪更新语义”:
|
||
|
||
- 合并字段更新。
|
||
- 刷新 `seq` 与 `seqRange`。
|
||
- 清空 embedding,等待后续重建。
|
||
- 失效掉旧的 `updates` / `temporal_update` 关系边。
|
||
- 若存在 `sourceNodeId`,则补建新的 `temporal_update` 边。
|
||
- 根据字段差异自动生成一条新的 `event` 节点,摘要形如 `field: before -> after`。
|
||
- 再用 `updates` 边把这个事件挂回被更新节点。
|
||
|
||
这让“状态变化”不只是覆盖写入,还能留下被检索的更新痕迹。
|
||
|
||
#### `delete`
|
||
|
||
当前是**软删除**,不会真的移除节点,只是把节点标记为 `archived = true`。
|
||
|
||
### 6. Embedding 补齐
|
||
|
||
提取结束后,系统会为所有缺少 embedding 的活跃节点批量生成向量。拼接文本时优先使用:
|
||
|
||
- `summary`
|
||
- `name`
|
||
- `title`
|
||
- `traits`
|
||
- `state`
|
||
- `constraint`
|
||
|
||
如果这些字段都没有,就退化为节点类型名。
|
||
|
||
### 7. 提取后增强流程
|
||
|
||
在一次成功提取后,当前实现还可能继续执行以下步骤:
|
||
|
||
#### 记忆进化 `enableEvolution`
|
||
|
||
新节点写入后,[evolution.js](./evolution.js) 会:
|
||
|
||
1. 为新节点找近邻旧节点。
|
||
2. 调用 LLM 判断新信息是否改变了对旧记忆的理解。
|
||
3. 若需要:
|
||
- 给新旧节点补链接
|
||
- 回溯更新旧节点的 `state` / `summary` / `core_note`
|
||
- 更新旧节点的 `clusters`
|
||
- 记录 `_evolutionHistory`
|
||
|
||
#### 全局概要 `enableSynopsis`
|
||
|
||
每 `synopsisEveryN` 次提取后,会基于:
|
||
|
||
- 事件时间线
|
||
- 角色状态
|
||
- 主线状态
|
||
|
||
生成或更新一个 `synopsis` 节点,用于充当前情提要锚点。
|
||
|
||
#### 反思条目 `enableReflection`
|
||
|
||
每 `reflectEveryN` 次提取后,会基于:
|
||
|
||
- 最近事件
|
||
- 近期角色状态
|
||
- 当前主线
|
||
- 已知矛盾边
|
||
|
||
生成 `reflection` 节点,并用 `evolves` 边连接到最近事件。
|
||
|
||
#### 主动遗忘 `enableSleepCycle`
|
||
|
||
[compressor.js](./compressor.js) 中的 `sleepCycle()` 会按保留价值归档低价值节点。当前保留价值大致由以下因素决定:
|
||
|
||
- 重要性
|
||
- 新近性
|
||
- 访问频率
|
||
|
||
规则、概要、主线和高重要性节点默认不会被遗忘。
|
||
|
||
#### 层级压缩
|
||
|
||
对支持分层压缩的类型,系统会:
|
||
|
||
- 从最低层级开始扫描
|
||
- 只压缩超过阈值且不属于“最近保留叶子”的旧节点
|
||
- 以 `fanIn` 为批次调用 LLM 总结
|
||
- 新建更高层级节点
|
||
- 把被压缩子节点归档,并建立 `parentId` / `childIds`
|
||
|
||
当前默认支持分层压缩的类型是:
|
||
|
||
- `event`
|
||
- `thread`
|
||
- `reflection`
|
||
|
||
## 读取链路详解
|
||
|
||
读取逻辑主要集中在 [retriever.js](./retriever.js) 和 [injector.js](./injector.js)。
|
||
|
||
### 1. 活跃节点筛选
|
||
|
||
读取开始前会先获取当前图中的活跃节点,并过滤掉:
|
||
|
||
- `archived = true` 的节点
|
||
- `seqRange` 不完整的异常节点
|
||
|
||
如果启用了 `enableVisibility`,还会根据节点 `fields.visibility` 进行认知边界过滤:
|
||
|
||
- 支持数组形式
|
||
- 支持逗号分隔字符串
|
||
- 支持 `*` 通配
|
||
- 当前视角默认取 `context.name2`
|
||
|
||
### 2. 自适应检索策略
|
||
|
||
检索策略会根据活跃节点规模自动调整:
|
||
|
||
| 活跃节点数 | 检索策略 |
|
||
| --- | --- |
|
||
| `< 20` | 不做向量预筛,所有节点参与评分,可选直接走 LLM 精确召回 |
|
||
| `20 - 200` | 向量预筛 + 图扩散 + 混合评分,默认不走 LLM 精确召回 |
|
||
| `> 200` | 向量预筛 + 图扩散 + 混合评分 + LLM 精确召回 |
|
||
|
||
这套阈值定义在 [retriever.js](./retriever.js) 的 `STRATEGY_THRESHOLDS` 中。
|
||
|
||
### 3. 向量预筛
|
||
|
||
如果图规模达到阈值且配置了 Embedding API,会:
|
||
|
||
1. 对当前用户输入生成 query embedding。
|
||
2. 对已有节点 embedding 做暴力余弦相似度检索。
|
||
3. 取 Top-K 作为候选。
|
||
|
||
当前实现明确采用暴力搜索,而不是 HNSW/ANN,因为它假设 ST 使用场景通常是中小图规模。
|
||
|
||
### 4. 实体锚点与交叉检索
|
||
|
||
系统会额外从用户输入中做一层简单实体锚定:
|
||
|
||
- 如果消息里直接出现了某个节点的 `name` 或 `title`
|
||
- 就把它视为一个高能量种子
|
||
|
||
如果开启 `enableCrossRecall`,则会进一步:
|
||
|
||
- 沿着这些实体节点的有效边展开
|
||
- 找到相邻的 `event` 节点
|
||
- 把它们也作为附加扩散种子
|
||
|
||
这一步更偏向“场景联想”,而不是单纯语义相似。
|
||
|
||
### 5. 图扩散
|
||
|
||
[diffusion.js](./diffusion.js) 实现了一个轻量版 PEDSA 扩散引擎:
|
||
|
||
- 从种子节点出发传播能量
|
||
- 每步乘衰减因子
|
||
- 只保留 Top-K 活跃节点
|
||
- 抑制边会传递负能量
|
||
- 能量值会被钳位到固定区间
|
||
|
||
当前默认配置:
|
||
|
||
- 最多 2 步扩散
|
||
- 衰减系数 0.6
|
||
- 每步最多保留 100 个活跃节点
|
||
|
||
### 6. 混合评分
|
||
|
||
混合评分公式定义在 [dynamics.js](./dynamics.js):
|
||
|
||
```text
|
||
FinalScore = (GraphScore * alpha + VectorScore * beta + ImportanceNorm * gamma) * TimeDecay
|
||
```
|
||
|
||
默认权重为:
|
||
|
||
- `graphWeight = 0.6`
|
||
- `vectorWeight = 0.3`
|
||
- `importanceWeight = 0.1`
|
||
|
||
时间衰减采用对数衰减,而不是快速指数衰减,目的是让久远但重要的记忆不要掉得太快。
|
||
|
||
### 7. LLM 精确召回
|
||
|
||
在小图或大图场景下,如果开启 `recallEnableLLM`,系统会:
|
||
|
||
1. 先把候选节点按混合得分排好。
|
||
2. 取前 30 个以内节点作为候选池。
|
||
3. 把最近对话、用户最新输入、候选节点字段摘要一起喂给 LLM。
|
||
4. 让 LLM 输出最终选中的节点 ID 列表。
|
||
|
||
如果 LLM 召回失败,则回退到纯评分排序结果。
|
||
|
||
### 8. 场景重构
|
||
|
||
在得到初始召回节点后,系统不会立刻结束,而是还会做一次“场景补全”:
|
||
|
||
- 若命中的是 `event`,会补入与该事件直接相关的角色、地点、主线、反思节点,以及时间上最邻近的事件。
|
||
- 若命中的是 `character` / `location`,会先找其关联事件,再围绕这些事件继续补场景。
|
||
|
||
这一步的目标,是避免只召回一个孤立节点,尽量把一个能被模型理解的局部情境一起带回来。
|
||
|
||
### 9. 概率触发回忆
|
||
|
||
如果开启 `enableProbRecall`,系统还会从未选中的高重要性节点里抽少量候选,并按概率追加进结果。这更像是“偶发闪回”,用于给长期剧情增加一点远程记忆回流。
|
||
|
||
### 10. 访问强化
|
||
|
||
被最终选中的节点会执行访问强化:
|
||
|
||
- `accessCount + 1`
|
||
- `importance + 0.1`
|
||
- 更新时间 `lastAccessTime`
|
||
|
||
这使得经常被召回、反复证明有用的节点,后续更容易继续存活和命中。
|
||
|
||
## 注入策略
|
||
|
||
注入文本由 [injector.js](./injector.js) 生成,格式是 Markdown 表格,主要分为两部分:
|
||
|
||
### 1. Core 常驻注入
|
||
|
||
凡是 Schema 中 `alwaysInject = true` 的类型,都会直接进入 Core:
|
||
|
||
- `event`
|
||
- `rule`
|
||
- `thread`
|
||
- `synopsis`
|
||
|
||
这意味着当前默认设计并不是“所有东西都走检索”,而是:
|
||
|
||
- **叙事主干**直接常驻
|
||
- **状态与补充记忆**按需召回
|
||
|
||
这是当前实现最值得注意的一个架构选择。
|
||
|
||
### 2. Recalled 召回注入
|
||
|
||
非 `alwaysInject` 且被选中的节点会进入召回区,并按桶组织:
|
||
|
||
- 当前状态记忆
|
||
- 情景事件记忆
|
||
- 反思与长期锚点
|
||
- 规则与约束
|
||
- 其他关联记忆
|
||
|
||
在默认 Schema 下,召回区最常见的其实是:
|
||
|
||
- `character`
|
||
- `location`
|
||
- `reflection`
|
||
|
||
因为事件、规则、主线、概要默认都属于 Core。
|
||
|
||
### 3. Token 估算
|
||
|
||
注入完成后,系统会做一个粗略 token 估算,便于观察注入体积。当前估算规则大致是:
|
||
|
||
- 2 个中文字符约等于 1 token
|
||
- 4 个英文字符约等于 1 token
|
||
|
||
## 一个完整运行示例
|
||
|
||
下面用一个简化示例说明从聊天到图谱、再到召回的大致闭环:
|
||
|
||
1. 用户说:“我们先去钟楼看看,之前失踪案很可能和那里有关。”
|
||
2. 助手回复了一段剧情,描述角色艾琳进入钟楼,发现地下暗门。
|
||
3. 这轮回复结束后,提取器可能产出:
|
||
- 一个 `event`:艾琳在钟楼发现地下入口
|
||
- 一个 `location`:钟楼,状态为存在隐藏入口
|
||
- 一个 `thread`:失踪案调查,状态推进
|
||
4. 如果图中本来就有“钟楼”地点节点,则该地点不会重复创建,而会变成更新。
|
||
5. 新节点生成后,系统补 embedding,并可能触发:
|
||
- 记忆进化:修正旧事件对钟楼的理解
|
||
- 全局概要:更新前情提要
|
||
6. 下一轮用户问:“地下入口会不会和之前失踪的人有关?”
|
||
7. 召回阶段会:
|
||
- 命中“地下入口”“失踪”等语义相关节点
|
||
- 把钟楼、相关事件、最近主线等一起拉回
|
||
- 再用注入表格告诉模型当前关键情境
|
||
8. 模型在生成时,就不只是看当前一句话,而是能同时看到:
|
||
- 最近核心事件
|
||
- 当前地点/角色状态
|
||
- 当前主线和概要
|
||
|
||
## 功能清单与成熟度
|
||
|
||
### 已实现主链路
|
||
|
||
| 功能 | 当前状态 | 说明 |
|
||
| --- | --- | --- |
|
||
| 聊天级图谱持久化 | 已实现 | 图谱跟随当前聊天保存与切换 |
|
||
| LLM 结构化提取 | 已实现 | 支持 `create/update/delete` |
|
||
| 节点 embedding 生成 | 已实现 | 依赖外部 Embedding API |
|
||
| 向量预筛 | 已实现 | 余弦相似度暴力检索 |
|
||
| 图扩散排序 | 已实现 | PEDSA 风格轻量扩散 |
|
||
| 混合评分 | 已实现 | 图分、向量分、重要性、时间衰减 |
|
||
| LLM 精确召回 | 已实现 | 小图/大图场景触发 |
|
||
| 场景重构 | 已实现 | 围绕事件和实体补上下文 |
|
||
| 层级压缩 | 已实现 | 事件/主线/反思支持 |
|
||
| 记忆进化 | 已实现 | 基于近邻与 LLM 回溯更新 |
|
||
| 全局概要 | 已实现 | 周期生成 `synopsis` |
|
||
| 反思条目 | 已实现 | 周期生成 `reflection` |
|
||
| 主动遗忘 | 已实现 | 按保留价值归档 |
|
||
| 导入/导出 | 已实现 | 导出时去掉 embedding |
|
||
|
||
### 实验性能力
|
||
|
||
| 功能 | 当前状态 | 备注 |
|
||
| --- | --- | --- |
|
||
| 精确对照(Mem0 风格) | 实验性 | 对不同剧情密度的收益仍需更多验证 |
|
||
| 认知边界过滤 | 实验性 | 依赖节点 `visibility` 字段质量 |
|
||
| 交叉检索 | 实验性 | 更像场景增强,不一定总是增益 |
|
||
| 概率触发回忆 | 实验性 | 可能提升“闪回感”,也可能增加噪声 |
|
||
| 反思节点召回策略 | 实验性 | 当前以结构就绪为主,策略仍可细化 |
|
||
|
||
### 已有实现但未完全打通的预留项
|
||
|
||
下面这些字段或配置已经出现在代码中,但当前还不应在 README 中当作完整能力宣传:
|
||
|
||
| 项 | 当前情况 |
|
||
| --- | --- |
|
||
| `nodeTypeSchema` | 设置层支持,但当前没有现成 UI 做 Schema 编辑 |
|
||
| `extractPrompt` | 设置层支持,但当前没有现成 UI 暴露自定义提取提示词 |
|
||
| `injectPosition` / `injectRole` | 默认设置存在,但实际注入调用当前只使用 `injectDepth` |
|
||
| `evoConsolidateEvery` | 设置项存在,但当前没有真正的“进化后整理”执行逻辑 |
|
||
| `forceUpdate` | Schema 元数据存在,但当前运行期没有用它强制产出节点 |
|
||
|
||
## 配置说明
|
||
|
||
设置面板定义在 [settings.html](./settings.html),逻辑绑定在 [index.js](./index.js)。
|
||
|
||
### 基础与召回配置
|
||
|
||
| 配置项 | 默认值 | 作用 |
|
||
| --- | --- | --- |
|
||
| `enabled` | `false` | 总开关 |
|
||
| `extractEvery` | `1` | 每 N 条 assistant 回复提取一次 |
|
||
| `extractContextTurns` | `2` | 提取时往前带多少轮上下文 |
|
||
| `recallEnabled` | `true` | 是否启用生成前记忆注入 |
|
||
| `recallTopK` | `15` | 评分后的候选上限 |
|
||
| `recallMaxNodes` | `8` | LLM 精确召回最多选多少节点 |
|
||
| `recallEnableLLM` | `true` | 是否启用 LLM 精确召回 |
|
||
| `injectDepth` | `4` | 注入深度 |
|
||
|
||
### 混合评分权重
|
||
|
||
| 配置项 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `graphWeight` | `0.6` | 图扩散得分权重 |
|
||
| `vectorWeight` | `0.3` | 向量相似度权重 |
|
||
| `importanceWeight` | `0.1` | 节点重要性权重 |
|
||
|
||
### v2 增强功能配置
|
||
|
||
| 配置项 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `enableEvolution` | `true` | 开启记忆进化 |
|
||
| `evoNeighborCount` | `5` | 进化近邻搜索数量 |
|
||
| `enablePreciseConflict` | `true` | 开启精确对照 |
|
||
| `conflictThreshold` | `0.85` | 触发精确对照的相似度阈值 |
|
||
| `enableSynopsis` | `true` | 开启全局概要 |
|
||
| `synopsisEveryN` | `5` | 每 N 次提取更新概要 |
|
||
| `enableVisibility` | `false` | 开启认知边界过滤 |
|
||
| `enableCrossRecall` | `false` | 开启交叉检索 |
|
||
| `enableSmartTrigger` | `false` | 开启轻量触发提取 |
|
||
| `triggerPatterns` | `""` | 自定义关键词或正则 |
|
||
| `smartTriggerThreshold` | `2` | 智能触发阈值 |
|
||
| `enableSleepCycle` | `false` | 开启主动遗忘 |
|
||
| `forgetThreshold` | `0.5` | 节点保留价值阈值 |
|
||
| `sleepEveryN` | `10` | 每 N 次提取执行一次遗忘 |
|
||
| `enableProbRecall` | `false` | 开启概率回忆 |
|
||
| `probRecallChance` | `0.15` | 概率回忆触发概率 |
|
||
| `enableReflection` | `false` | 开启反思条目 |
|
||
| `reflectEveryN` | `10` | 每 N 次提取生成反思 |
|
||
|
||
### Embedding 配置
|
||
|
||
| 配置项 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `embeddingApiUrl` | `""` | OpenAI 兼容 API 基地址 |
|
||
| `embeddingApiKey` | `""` | API Key |
|
||
| `embeddingModel` | `text-embedding-3-small` | embedding 模型名 |
|
||
|
||
## 推荐使用方式
|
||
|
||
### 起步建议
|
||
|
||
如果你是第一次用这个扩展,建议先用最保守的组合:
|
||
|
||
- 开启 `enabled`
|
||
- 保持 `extractEvery = 1`
|
||
- 开启 `recallEnabled`
|
||
- 开启 `recallEnableLLM`
|
||
- 开启 `enableEvolution`
|
||
- 开启 `enableSynopsis`
|
||
- 暂时关闭 `enableVisibility`、`enableCrossRecall`、`enableProbRecall`
|
||
- `enableReflection` 可以先关闭,等剧情稳定后再打开
|
||
|
||
### 成本敏感场景
|
||
|
||
如果更在意 API 成本,可以尝试:
|
||
|
||
- `extractEvery = 2` 或 `3`
|
||
- 关闭 `recallEnableLLM`
|
||
- 提高 `synopsisEveryN`
|
||
- 关闭 `enableReflection`
|
||
- 仅保留 Embedding 相关能力
|
||
|
||
### 长剧情 / 高连续性场景
|
||
|
||
如果是重剧情、重状态变化的 RP:
|
||
|
||
- 保留 `enableEvolution`
|
||
- 保留 `enableSynopsis`
|
||
- 在确认节点 `visibility` 字段可控后,再测试 `enableVisibility`
|
||
- 对多地点、多人物切换频繁的剧情,可逐步开启 `enableCrossRecall`
|
||
|
||
## 操作面板
|
||
|
||
当前 UI 已经提供以下手动操作:
|
||
|
||
| 按钮 | 作用 |
|
||
| --- | --- |
|
||
| 查看图谱 | 显示活跃/归档节点数、边数、类型分布、最后处理楼层 |
|
||
| 查看注入 | 直接查看最近一次生成前的注入文本 |
|
||
| 重建图谱 | 清空当前聊天图谱,下次生成重新抽取 |
|
||
| 手动压缩 | 对当前图谱执行压缩 |
|
||
| 导出 | 导出图谱 JSON,不包含 embedding |
|
||
| 导入 | 导入图谱 JSON,导入后会清空所有 embedding |
|
||
| 测试连接 | 测试 Embedding API 是否可用 |
|
||
|
||
## 目录与模块职责
|
||
|
||
| 文件 | 作用 |
|
||
| --- | --- |
|
||
| [manifest.json](./manifest.json) | 扩展清单 |
|
||
| [index.js](./index.js) | 扩展入口、事件绑定、设置管理、总流程调度 |
|
||
| [settings.html](./settings.html) | 设置面板 UI |
|
||
| [style.css](./style.css) | 扩展样式 |
|
||
| [graph.js](./graph.js) | 图数据结构、时序边、序列化、导入导出 |
|
||
| [schema.js](./schema.js) | 默认 Schema 与关系类型定义 |
|
||
| [extractor.js](./extractor.js) | 写入路径、精确对照、概要、反思 |
|
||
| [retriever.js](./retriever.js) | 读取路径、图扩散、混合评分、精确召回 |
|
||
| [injector.js](./injector.js) | 注入文本组织与格式化 |
|
||
| [embedding.js](./embedding.js) | Embedding API 调用、向量相似度检索 |
|
||
| [llm.js](./llm.js) | LLM 请求与 JSON 解析封装 |
|
||
| [diffusion.js](./diffusion.js) | PEDSA 风格扩散引擎 |
|
||
| [dynamics.js](./dynamics.js) | 时间衰减、访问强化、混合评分 |
|
||
| [compressor.js](./compressor.js) | 层级压缩与主动遗忘 |
|
||
| [evolution.js](./evolution.js) | 记忆进化引擎 |
|
||
| [tests/](./tests) | 当前已有的轻量本地测试 |
|
||
|
||
## 测试与验证
|
||
|
||
当前仓库内已有的测试比较轻量,主要覆盖部分核心逻辑:
|
||
|
||
```bash
|
||
node tests/smart-trigger.mjs
|
||
node tests/graph-retrieval.mjs
|
||
node tests/injector-format.mjs
|
||
```
|
||
|
||
它们分别验证:
|
||
|
||
- 智能触发评分逻辑
|
||
- 时序边过滤与图扩散基础行为
|
||
- 注入文本格式化流程
|
||
|
||
当前**尚未**覆盖的重点包括:
|
||
|
||
- 真实 LLM 提取质量
|
||
- 真实 Embedding API 行为
|
||
- 完整的 ST 生命周期集成
|
||
- 大图规模下的性能与稳定性
|
||
- 导入导出后的重建与回归
|
||
|
||
## 已知限制
|
||
|
||
截至当前代码实现,建议明确接受以下限制:
|
||
|
||
1. 这是一个**聊天内图谱**,不是跨聊天统一记忆库。
|
||
2. 导入图谱后,所有节点 embedding 会被清空;当前没有单独的“全量重建 embedding”按钮,向量能力需要后续写入或额外处理来逐步恢复。
|
||
3. LLM 子任务很多,结构化输出质量会直接影响图谱质量。
|
||
4. 当前没有内建图谱可视化界面,调试主要依赖统计信息、日志和注入文本。
|
||
5. 默认 `event` / `rule` / `thread` / `synopsis` 都是 Core 常驻注入,项目当前更偏向“主干常驻 + 状态召回”,而不是纯检索式记忆架构。
|
||
6. 实验性功能已经接入主流程,但仍缺少更系统的 benchmark 和回归验证。
|
||
|
||
## 设计来源与参考
|
||
|
||
ST-BME 不是这些项目的直接移植,而是结合 SillyTavern 扩展场景做的工程化整合。当前设计大致受以下项目启发:
|
||
|
||
| 参考项目 | 启发点 |
|
||
| --- | --- |
|
||
| `A-MEM` | 记忆进化、基于近邻的回溯修正 |
|
||
| `EM-LLM` | 惊奇度触发、段落边界与提取时机 |
|
||
| `Graphiti` | 时序边、关系有效性和图建模思路 |
|
||
| `Mem0` | 新旧记忆对照、增量更新决策 |
|
||
| `RoleRAG` | 认知边界过滤 |
|
||
| `AriGraph` | 沿图边展开的交叉检索 |
|
||
| `MemoRAG` | 全局概要作为长期锚点 |
|
||
| `SleepGate` | 主动遗忘与保留价值评估 |
|
||
| `Reflexion` | 反思条目方向 |
|
||
| `PeroCore` | 图扩散、记忆动力学、向量检索策略 |
|
||
|
||
## 当前版本
|
||
|
||
- 扩展版本:`0.1.0`
|
||
- 清单文件:[`manifest.json`](./manifest.json)
|
||
|
||
## 许可证
|
||
|
||
本项目采用 AFPL License,详见 [LICENSE](./LICENSE)。
|