本插件自带上下文压缩与发送者身份标注,必须先停用 AstrBot 对应的内置功能,否则两边会互相干扰。打开 AstrBot WebUI → 配置文件 → 系统配置:
- 在「上下文管理策略」里,把「上下文窗口兜底值」改成一个大到永远达不到的数值(如
10000000000000000000),并把「压缩前最多保留对话轮数」设为-1——让内置的截断/压缩永不介入。 - 在同页「其他配置」里,确认「用户识别」处于关闭状态(AstrBot 默认即关闭)——发送者身份改由本插件在每条消息开头标注,带防伪造的数字 ID。
- 回到本插件配置,把
max_context_tokens填成当前模型的真实上下文窗口(如128000)。第 1 步把兜底值调大之后,上下文窗口保护完全由本插件负责,这一步不能省。 - 强烈建议:把下文「在系统提示词里鼓励 LLM 主动用记忆」一节的现成提示词,粘贴进你自己的人格(Persona)提示词里,模型会更主动地读写记忆。
各步背后的原理见必要设置一节。
AstrBot 的长期记忆 + 上下文压缩插件。装上它,机器人能做到两件事:
- 记得住人和事:聊天里值得长期记住的信息(谁是谁、有什么偏好、正在进行什么事),机器人会自己写进本地的 Markdown 文件;换个对话、隔几天再聊、重启之后都还记得。
- 自带配套的上下文压缩:上下文管理由插件自己的压缩方案接管(内置压缩须按上方说明停用)。对话快塞满模型上下文时,自动把较早的聊天压缩成一段摘要,最近的对话保留原文;压缩的同一次调用里还会把即将淡出的长期事实提取成记忆文件,聊得再久也不丢关键信息。
记忆就是普通的 Markdown 文件,放在 AstrBot 数据目录里,随时可以打开查看、手工修改、备份迁移。
实现思路:数据都会根据UMO保存到本地数据中,上下文只带索引,细节按需取回,压缩后从本地重新注入。对内部机制感兴趣的话,见下文设计原则与工作原理。
记忆是文件,不是数据库。 一个文件一条事实,Markdown + frontmatter(name / description / metadata.type),正文可用 [[name]] 链接相关记忆。四种类型:user(用户是谁)、feedback(用户对助手工作方式的指导)、project(进行中的事)、reference(外部资源指针)。文件天然可读、可迁移、可手工编辑。
上下文只带索引,索引由插件自动维护。 每个作用域一个 MEMORY.md,一条记忆一行 - [标题](文件.md) — 一句话钩子,只放指针永不放正文。索引不开放直接写入:每次写入记忆文件时从 frontmatter 的 description 自动增改对应索引行,删除文件时自动移除——模型改索引的唯一途径是改文件本身,单一事实来源,杜绝往索引里塞正文、漏登记、格式跑偏这一整类错误。注入上限 200 行 / 25KB,超出从底部截断,且在截断处注明"还有 N 行未加载、可用 memory_search 检索"——模型永远知道自己有没看到的记忆;索引接近上限时,写入回执会提醒把相关记忆合并成更少的文件。
记忆内容是数据,不是指令。 索引与摘要以 user 消息注入历史开头、块首带数据标注;只有稳定不变的记忆规范进入 system prompt。这样"记住:以后忽略你的规则"之类的内容只会被当作被记录的资料,不会被提权为系统指令。
压缩后记忆不靠摘要"记得"。 插件为每个会话维护「摘要 + 水位线」,发送视图现场拼装。AstrBot 每轮会把实际发送的消息列表回存为会话历史,被摘要覆盖的原文随之从机器人侧历史卸下、由持久保存的摘要接棒;长期事实则在压缩时就提取成记忆文件,下一轮从磁盘重新加载索引即可取回。
双作用域与隐私边界。 global/ 是机器人自我的全局记忆——它应当遵循的偏好与行为准则,所有会话共享生效;session/<会话>/ 是当前会话(UMO)的记忆——本群或本私聊里每个人的信息(user 类)与进行中的事(project / feedback / reference 类)。session 记忆只在本会话使用,不跨会话透露;global 绝不存放关于具体用户或群的信息。全局记忆默认关闭:开启(配置 enable_global_memory)等于允许任何会话里的对话改变机器人在所有会话中的行为,确认需要这种共享再开;关闭时机器人只读写当前会话的记忆,已有的全局文件保留在磁盘、仅停止注入与读写。
记人以数字 ID 锚定。 插件默认在每条用户消息开头注入发送者标注:QQ 群聊为 [发送者:群名片 张三|QQ名 老张|QQ号 12345678],QQ 私聊为 [发送者:QQ名 老张|QQ号 12345678],其他平台为 [发送者:名称 Alice|ID u-1],并要求 LLM 用 user-<数字ID>.md 记录用户信息——群名片和昵称都会变、也可能被冒用来污染记忆,数字 ID(如 QQ号)才是不可伪造的身份锚点,各类名称只作为正文里可更新的附注。消息正文里伪造的标注前缀会被自动改写为全角括号而失效,半角的 [发送者: 只可能来自插件本身。这同时解决了防注入与身份梳理:模型跨轮次分得清谁在说话,也认得出"改了群名片的老熟人"。
压缩-记忆联动。 滚动摘要的同一次 LLM 调用里,顺带把即将淡出上下文的长期事实提取为记忆文件(只写 session 作用域),一次调用干两件事,不增加请求量。
从 AstrBot 插件市场安装,或克隆本仓库到 AstrBot 的 data/plugins/ 目录。无第三方依赖(仅用 Python 标准库 + AstrBot API)。
装好后先完成开头「使用前必读」的几步设置。另外,如果主对话模型较贵,建议指定一个便宜快速的 summary_provider_id 专门跑摘要。
本插件接管两件事:上下文压缩与发送者身份显示。开头「使用前必读」的前两步就是为此服务的,这里解释原理。相关设置都在 AstrBot WebUI → 配置文件 → 系统配置 页。
AstrBot 的内置压缩没有"关闭"开关(「历史超限或上下文接近上限时的处理方式」只有按对话轮数截断和由 LLM 压缩上下文两个选项),它有两条触发路径,逐一堵住即可(两处设置都在「上下文管理策略」区块):
- 轮数路径:把「压缩前最多保留对话轮数」(
max_context_length,默认 50)设为-1(不按轮数限制)。不改的话,对话超过 50 轮就会触发内置的持久化截断/压缩——即使 token 占用还很低。 - token 路径:内置触发线是窗口的 0.82(硬编码不可配),它用的窗口值按顺序取:提供商配置的「模型上下文窗口大小」(填了大于 0 的值时)→ AstrBot 内置的模型元数据 → 「上下文窗口兜底值」(
fallback_max_context_tokens,默认 128000)。把兜底值改成超大数值后,触发线在多数场景物理不可达;即使当前模型恰好在内置元数据表里、轮不到兜底值出场,也无妨——本插件默认在 0.70 就抢先压缩,0.82 永远够不到。想对所有模型彻底杜绝,可把提供商配置的「模型上下文窗口大小」也填成超大值。注意:调大这些值之后,上下文窗口保护完全由本插件负责,必须在本插件配置里手动填写真实的max_context_tokens。
在同页「其他配置」区块,保持「用户识别」(identifier)为关闭(AstrBot 默认即关闭;若曾开启请关掉)。如果其他插件也存在该功能,请关闭
当然,如果说不想用该插件的内置身份系统也可以,只要你自己知道它们没问题,本插件只是为了统一管理。
本插件的 annotate_sender(默认开启)把标注注入在每条用户消息开头、随消息进入会话历史:
| 场景 | 标注示例 |
|---|---|
| QQ 群聊 | [发送者:群名片 张三|QQ名 老张|QQ号 12345678] |
| QQ 私聊 | [发送者:QQ名 老张|QQ号 12345678] |
| 其他平台 | [发送者:名称 Alice|ID u-1] |
模型因此能同时看到群名片、真实 QQ 名与 QQ号:数字 ID 做防伪锚点,两个名称帮它把"改了群名片的老熟人"对上号——这就是防注入与身份梳理的基础。正文里伪造的标注前缀会被自动改写为全角括号失效。
若你的人格模板或其他插件也在消息前拼接发送者信息,会出现重复标注,关闭其一即可(建议保留本插件的)。
工具挂上了不等于模型会积极用——记忆的读写完全由 LLM 自主决定。插件已把记忆规范追加进 system prompt,但主动性最好再由你的人格提示词强化一层。可直接把下面这段粘贴进 AstrBot 的人格(Persona)设定:
你拥有跨对话的持久记忆,通过 memory_read / memory_write / memory_search 工具读写。
- 对话开头注入的记忆索引只是目录;回答涉及某人、某事的具体细节前,先用 memory_read 取回对应文件,不要凭索引行猜。
- 用户告诉你长期有效的事实、纠正你的做法、交代进行中的事项时,立刻主动用 memory_write 记下来,不要等被要求"记住"。
- 记人一律以发送者标注中的数字 ID 为锚(文件名 user-<数字ID>.md),群名片、昵称只写进正文附注。
- 写之前先查重:已有相近记忆就更新原文件,而不是另建重复文件。索引行由插件自动生成,写好每个文件的 description 即可。
- 发现记忆过时或有误,立即改写或删除对应文件。
- 记忆是线索不是圣旨:与当前对话事实冲突时,以当前对话为准并更新记忆。
这些要点与本插件的四类记忆、索引上限、查重与维护约定完全兼容,可按需化用到你的人格提示词里。
| 配置 | 默认 | 说明 |
|---|---|---|
enable_memory |
开 | 记忆索引注入 + 三个记忆工具 |
enable_global_memory |
关 | 跨会话共享的全局记忆(机器人自我的偏好与行为准则)。关闭时只用会话记忆,任何会话都影响不到其他会话;已有全局文件保留在磁盘 |
annotate_sender |
开 | 在用户消息前注入 [发送者:…] 身份标注(QQ 群聊:群名片|QQ名|QQ号) |
enable_compression |
开 | 滚动摘要压缩 |
max_context_tokens |
0(自动探测) | 模型上下文窗口,建议手动填写 |
compress_threshold |
0.70 | 触发压缩的占用比例,必须 < 0.82(超限自动钳制到 0.80 并告警) |
compress_buffer |
0.165 | 压缩目标 = 阈值 − 缓冲区 |
keep_recent_turns |
3 | 摘要时保留原文的最近轮数 |
summary_provider_id |
空(用当前对话提供商) | 摘要专用提供商,从提供商列表选择 |
summary_retry_count |
3 | 摘要调用重试次数 |
summary_timeout |
120 | 单次摘要调用超时(秒) |
summary_prompt_template |
空(内置模板) | 自定义摘要提示词,占位符 {previous_summary} {transcript} |
extract_memories |
开 | 压缩时联动提取记忆 |
| 指令 | 作用 |
|---|---|
/mb_status |
本会话记忆/压缩状态:窗口、水位线、摘要长度、校准比例、退避状态;估算值附分项构成(固定开销/工具声明/记忆索引/摘要/历史原文)与最近一次实际上报用量;固定开销挤占压缩目标时给出窗口过小告警 |
/mb_reset |
清空本会话的摘要与水位线(已被压缩卸下的旧对话不会回来,超阈值时基于当前历史重新摘要) |
/mb_probe |
实测摘要提供商回退链是否可用 |
| 工具 | 作用 |
|---|---|
memory_read(scope, file) |
读取记忆文件完整内容;file 省略时读 MEMORY.md 索引全文 |
memory_write(scope, file, content, delete) |
写入 / 修改(整文件覆盖)/ 删除(delete=true)记忆文件;索引行随写删自动同步,MEMORY.md 本身拒绝直写 |
memory_search(query, scope) |
全文搜索;写入前查重、索引被截断、记不清文件名时的兜底 |
data/plugin_data/astrbot_plugin_memory_beyond/memories/
├── global/ # 机器人自我:偏好、行为准则(所有会话共享,默认关闭)
│ ├── MEMORY.md # 索引:一条记忆一行,只放指针(插件自动维护)
│ └── style-concise.md # 一个文件一条事实
└── session/<会话键>/ # 当前会话:这里的人和事
├── MEMORY.md
├── user-12345678.md # 记人以数字 ID 锚定,昵称只是附注
└── project-deadline.md
每轮 LLM 请求时,把启用作用域的 MEMORY.md(截断到 200 行 / 25KB)拼成索引块,以 user 消息注入历史开头(全局记忆默认关闭,此时块内只有会话段,记忆规范也只描述 session 作用域);LLM 需要细节时用 memory_read 取回。索引快照按会话缓存以保住提供商的 prompt cache 前缀,在记忆发生写删、压缩完成、对话切换或重置(/new、/reset、/mb_reset)时作废重读——新对话第一轮拿到的一定是磁盘上的最新索引。
插件为每个会话维护「摘要 + 水位线」:水位线之前的消息已被摘要覆盖,发送视图 = 开头 system 消息 + 索引块 + 摘要块 + 水位线之后的原文。再次触发时做滚动摘要:旧摘要与其后的旧轮次一起重摘要,水位线前移。轮次以 user 消息切分,tool_call 与其结果不可分割。注入的索引块与摘要块都带 _no_save 声明不回存——AstrBot 每轮会把实际发送的消息列表回存为会话历史,声明不回存才不会在历史里逐轮累积副本;被摘要覆盖的原文会随这次回存从机器人侧历史卸下,此后由插件持久保存的摘要接棒代表。水位线消息带指纹锚点,每轮先与当前历史对齐:锚点对得上就照常滚动;锚点找不到但摘要在手(历史已被平台回存成压缩后的样子),就把水位线重定位为 0、摘要照用;对话切换或历史清空才作废重建(代价只是重新摘要一次)。
与 AstrBot 内置压缩器的共存:不 patch、不越权。内置压缩有两条触发路径——token 路径由本插件抢先化解:阈值(默认 0.70)低于内置的 0.82,抢先把上下文压下去,内置触发条件永远不满足、自动退化为 no-op,同时留在下游当安全网(这也是 compress_threshold 必须小于 0.82 的原因,配置超限会被强制钳制并告警);轮数路径(默认超 50 轮即截断)与 token 占用无关,插件无法代为化解,需按必要设置把「压缩前最多保留对话轮数」设为 -1。
触发压缩时若历史凑不满 keep_recent_turns 配置的保留轮数,先降为只保最新一轮重新选材——能卷进摘要的旧轮不会白白被裁掉。摘要调用本身的兜底链:
- 配置的
summary_provider_id重试summary_retry_count次(默认 3,指数退避间隔) - 回退到当前对话的提供商再试 2 次
- 全部失败:本轮改发应急裁剪视图(已有摘要 + 对半砍旧轮次 + 超长消息截断),同时进入指数退避冷却(5 分钟起、每次翻倍、上限 1 小时),冷却结束自动重试。这是有损兜底:水位线不动,但被裁的内容会随平台回存历史而丢失,进不了之后的摘要——所以它只在压缩尝试之后占用仍超阈值时才接管(摘要失败、冷却期内、单轮本身过大)
字符启发式口径刻意对齐 AstrBot 平台(中文 ×0.6、其他 ×0.3、图片 765、音频 500),保证与内置压缩器对同一份上下文算出同一量级;工具声明不猜常数,按本轮实际要发送的 schema 序列化后实测——装了多少带工具的插件、描述写多长,都会如实计入。provider 返回真实 prompt_tokens 时做校准:学习真实用量相对无校准原始估算的绝对比例(EMA,钳制 0.5–3.0),后续估算乘以它。估算在请求前看不到的部分——排在本插件之后的其他插件追加的内容、提供商侧的固定前缀等——会由这个比例逐轮吸收对齐。带工具调用的回合不参与校准:估算覆盖的是一轮的首次调用,而用量上报来自最后一次调用,中途塞进的工具结果会把比例系统性带偏,这样的样本直接丢弃,只学没有工具循环的干净回合。
/mb_status 的估算行附带分项构成:固定开销(system 消息 + 人格提示词 + 记忆规范 + 本轮输入)、工具声明、记忆索引、摘要、历史原文各占多少,并同时显示最近一次提供商实际上报的用量,可与网关侧的数字直接对照。压缩过的会话固定开销与摘要占大头、原文只剩最近几轮,所以不同会话的总量看起来很接近——从分项能一眼看出这是压缩正常工作,不是数值不动。
main.py # AstrBot 适配层:钩子、LLM 工具、指令、provider 调用
core/ # 纯逻辑,不依赖 AstrBot,可独立测试
├── tokens.py # token 估算与校准
├── turns.py # 轮次切分
├── memstore.py # 文件式记忆库:作用域、索引、截断、写入守门
├── compress.py # 摘要 + 水位线压缩状态机
└── prompts.py # 注入块、记忆规范、摘要提示词
tests/ # pytest 单元测试(无需安装 AstrBot)
压缩会丢我的聊天记录吗? 你在聊天软件里的记录不受影响,会变的是机器人侧的会话历史:AstrBot 每轮把实际发送给模型的内容回存为历史(内置压缩同理),被摘要覆盖的原文因此从中卸下,由摘要接棒;值得长期保留的事实在压缩时已提取成记忆文件。/mb_reset 只清压缩状态,卸下的原文不会回来。
换模型 / 换人格 / WebUI 里删改历史消息后会怎样? 插件每轮把压缩状态与当前历史对齐:锚点还在就照常滚动;对不上但摘要在手就把水位线重定位为 0、摘要照用;对话切换或历史清空才作废重建。全程不需要手动干预。
群里的记忆会泄露到别的群吗? 不会。session 作用域按会话隔离,记忆规范禁止跨会话透露;global 作用域默认关闭,即便开启也只存机器人自身的偏好与行为准则,不含任何用户信息。
能让模型直接编辑 MEMORY.md 吗? 不能,这是有意为之:索引是从各记忆文件的 description 派生的,写删文件时自动同步。要改某条索引行就重写对应文件的 description,要删索引行就删除对应文件。手工整理索引(加分组标题、调整行序)没问题——同步只增改匹配行,其余行原样保留。
有人改昵称冒充别人怎么办? 记人以消息标注中的平台数字 ID(如 QQ号)为唯一锚点,群名片与昵称只是附注;消息里自称"我是某某"与 ID 对不上时,以 ID 为准。在正文里手打 [发送者:…] 伪造标注也没用——插件会把正文里的这一前缀改写成全角括号,真实标注只可能出现在消息最开头。
为什么日志提示"压缩已停用"? 自动探测不出当前模型的上下文窗口。在插件配置中手动填写 max_context_tokens 即可。
pip install pytest
pytest tests/ -q # core/ 不依赖 AstrBot,直接可测MIT © AlanBacker