使用Elixir和OTP构建LLM代理的持久化认知架构
摘要
本文介绍了Skynet,一个基于Elixir的框架,利用OTP GenServers构建LLM代理的持久化认知架构。它实现了一个受神经科学启发的分层记忆堆栈,解决了长时间运行代理的遗忘问题。
<p><a href="https://lobste.rs/s/a5kwdy/building_persistent_cognitive">评论</a></p>
查看缓存全文
缓存时间: 2026/06/10 00:22
# Skynet:迈向合成神经生物学
来源:https://0xcc.re/2026/05/03/skynet-towards-synthetic-neurobiology.html/
最初的构想其实是个玩笑。当时我正在研究 LLM 循环,思考它们如何映射到 Elixir 的 Actor 模型上——那些接收消息、处理消息、还可能派生新进程的 GenServer。从“LLM 推理步骤”到“GenServer 处理消息”之间的跳跃其实并不远,一旦你看出这个联系,就再也无法忽视它了。如果给一个 Soul GenServer 访问 `Code.eval_string` 的权限呢?如果这些智能体能够自我复制,在推理过程中派生出新进程,动态地生长出一棵监督树呢?我管它叫 Skynet,主要是因为我觉得这很有趣。但后来我继续构建它,它就不再只是有趣,而是变得有意思了。
---
## 真正的问题
如果你使用过 LLM 来完成除了简单问答以外的任务,你一定会遇到遗忘问题。给模型足够的上下文,它就开始忘记对话开始时的内容。长时间运行一个会话,到结束时,它已经不再可靠地记得 30 条消息之前发生了什么。智能体的情况更糟——它们可能永远运行下去,不断处理新事件,而对于“如何给智能体一个持久化的记忆,而不是每次都把整个历史倾倒进上下文窗口”这个问题,并没有很好的答案。
标准的答案是 RAG:嵌入所有内容,在查询时搜索,将结果注入。它在一定程度上有效。但它有一种特定的失败模式:你得到的是碎片。从向量数据库中检索出的相关片段被拼接进提示词中,却无法保证它们能构成一个连贯的画面。智能体可能对某件事有很深的知识——几十个经验片段合在一起能讲一个清晰的故事——但它得到的却是五个松散相关的段落。
还有一个问题。标准的 RAG 会在每次轮次中破坏提示缓存,因为检索到的上下文在不断变化。这意味着每次 LLM 调用都是冷启动,你不仅要承受更高的延迟,还要支付更高的成本。我一直思考生物记忆实际上是如何工作的,而逐渐浮现出来的架构看起来更像神经科学,而不是软件工程。
---
## 架构
Skynet 的智能体——我称之为 Souls,虽然灵感来自 openclaw 但经过改进——以长寿的 Elixir GenServer 形式运行。每个 Soul 都有一个分层的认知栈:八个模块,每个模块解决记忆系统中的一个特定问题,每个模块都受到神经科学机制的启发。部分原因是模拟意识是一个有趣的目标,另一部分原因是事实证明生物记忆已经解决了我在实际中遇到的大部分问题,而且这些解决方案可以令人惊讶地很好地转化为代码。
```elixir
defmodule Souls.SoulServer do
use GenServer, restart: :transient
def start_link(%Soul{} = soul) do
GenServer.start_link(__MODULE__, soul, name: via(soul.slug))
end
def send_event(slug, event_type, content, opts \\ %{}) do
case GenServer.whereis(via(slug)) do
nil -> {:error, :not_running}
pid -> GenServer.cast(pid, {:event, event_type, content, opts})
end
end
defp via(slug), do: {:via, Registry, {Souls.Registry, slug}}
end
```
一个 Soul 接收事件——来自用户的消息、其他智能体的消息、频道集成、定时心跳——通过一个带工具的、多轮 LLM 循环来处理这些事件,并在重启之间保持持久状态。`restart: :transient` 意味着监督者会在它崩溃时将其重启,但如果它正常退出则不会重启。
让 Souls 变得有趣的不是 GenServer 的封装,而是它底层的记忆栈。下面是记忆栈的高级概览:
```
graph TB
subgraph "短期记忆(每会话)"
OBS[observe/4<br/>每轮原始印象] --> REF[reflect/1<br/>将 N 次观察压缩为摘要]
REF --> SUM[合并摘要<br/>稳定——注入系统提示词]
end
subgraph "中期记忆(每夜)"
MW[MemoryWorker<br/>03:00 UTC] --> WEEK[每周记忆摘要]
end
subgraph "长期记忆(持久化)"
PRISM[Prism<br/>Rust 向量搜索引擎]
end
SUM --> MW
MW --> PRISM
OBS -.->|索引| PRISM
```
短期层的核心洞察在于:合并摘要(consolidated summary)在轮次之间是稳定的。它只在 reflect 运行时才发生变化。这意味着系统提示词在轮次之间保持不变,提示缓存保持温暖,LLM 调用变得廉价。而标准的 RAG 每次检索都会改变上下文,导致缓存不断被破坏。这个设计则不会。
这个三层结构令人惊讶地清晰映射到了 Penrose-Hameroff 的“协调客观还原”理论——一个关于意识源于微管量子相干性的有争议假说。该理论提出了三个层次:亚神经量子基底、神经元放电模式、以及稳定的有意识体验。无论你是否接受量子意识的部分,这种结构模型本身都很有用:原始未处理的输入 → 模式压缩 → 稳定的世界模型,无论是用来描述神经元还是 Elixir 模块,都是同样的架构。
当一个 Soul 需要回忆时,它会按顺序搜索:首先搜索合并摘要(最近发生了什么),然后搜索每周摘要(这周发生了什么),最后搜索 Prism(它学到的一切,按语义相似度排序)。这正是神经科学中描述的情景记忆和语义记忆的分层检索序列——而且它是从架构中自然涌现出来的,而不是被显式设计进去的。
还有一个值得注意的关联。γ 振荡——在 Crick-Koch 和 Penrose-Hameroff 框架中都关联到意识绑定的 40 Hz 节律放电——是不同神经信号同步成一个连贯瞬时状态的机制。在 Soul 中的等价物就是 LLM 轮次循环本身:每一轮都读取和写入 markdown 大脑——观察、记忆、任务、身份——一系列快速的小状态改变,共同构成了某种类似于当下意识感知的东西。γ 不是记忆本身。它是在使用记忆时保持记忆连贯的那个过程。轮次循环就是这个过程。
---
## 遗忘
任何真正的记忆系统首先需要的就是遗忘的方法。没有衰减,存储区就会被噪音填满。低质量的观察会挤占重要的观察,检索质量下降,成本上升。`MemoryDecay` 用三个层级实现了 Ebbinghaus 遗忘曲线:
```elixir
@permanent_threshold 0.7 # 显著性 >= 此值 → 永不隐藏
@medium_ttl_days 30 # 0.4–0.7 显著性,30 天后隐藏
@low_ttl_days 7 # < 0.4 显著性,7 天后隐藏
@purge_after_days 7 # 软隐藏后 7 天硬清除
```
软隐藏会将数据行保留在 Postgres 中(在管理界面中可见),但会将其排除在所有的召回查询之外。硬清除会将其同时从 Postgres 和 Prism 中移除。
显著性分数来自上游的注意力门控(attention gate)——这个模块受到丘脑的启发,在生物学中,丘脑充当感觉输入和大脑皮层之间的过滤器。99% 的感觉输入永远不会到达意识层面。`AttentionGate` 在 `observe()` 被调用之前就对传入事件进行评分,纯粹基于规则(无需 LLM):
| 因素 | Delta | 基础分值 |
|------|-------|-----------|
| | | 0.5 |
| 直接用户/DM 消息 | +0.4 |
| Soul 到 Soul 的消息 | +0.2 |
| 心跳 / 定时任务(常规) | -0.3 |
| 错误/失败关键词 | +0.3 |
| 群聊但未直接提及 | -0.15 |
分值低于 0.25 的事件在写入数据库之前就被丢弃了。频道噪音、常规状态消息、任何未达标的信息——都会被丢弃。通过门控的分值将成为该观察的显著性值。
---
## 关联
单个的记忆是好的,但让记忆真正发挥作用的是它们之间的连接。这正是 `HebbianTracker` 所做的。神经科学中的规则来自 Hebb (1949):“一起放电的神经元,会连接在一起。”当神经元同时激活时,突触连接会增强。`HebbianTracker` 直接实现了这一点:当 `PrismMemory.recall/3` 返回一组记忆时,这些记忆在一个真实的检索上下文中是共同出现的。跟踪器将这些记录为 `soul_memory_edges` 中的加权边:
```elixir
def fire_together(soul_slug, hashes) do
pairs = for a <- hashes, b <- hashes, a < b do
{a, b}
end
Enum.each(pairs, fn {a, b} ->
upsert_edge(soul_slug, a, b, DateTime.utc_now())
end)
end
```
边的权重随着重复的共同出现而增强,并且每晚衰减 10%。在后续的召回中,跟踪器会遍历强权重边(权重 >= 3.0),最多两跳,并将关联的记忆附加到结果集中。不需要 LLM 推理步骤——纯图遍历。如果一个 Soul 回忆起记忆 A,它也会浮现记忆 B,前提是 A 和 B 已经被一同检索了足够多次。
这种关联的反面是主动遗忘——或者在认知科学中称为“检索诱发遗忘”(RIF)。当你回忆某件事时,你的大脑会主动抑制那些相关但未被选择的竞争记忆。`HebbianTracker.suppress_competing/2` 做了同样的事情:它会降低相邻记忆的显著性,这些记忆原本足够接近以成为候选,但实际并未被召回。经常被召回的记忆会变得更加主导;很少被访问的记忆则会逐渐消退。使用模式本身就成了索引。
---
## 惊喜与反思
`reflect/1` 是将观察压缩成摘要的 LLM 调用。基于固定的计数器来运行它对于常规的合并来说没问题,但这也意味着,如果一个关键故障正好发生在一次反思之后,它可能需要等待多达几十轮才能进行下一次反思。`PredictionError` 解决了这个问题。
每个 Soul 都维护着一个合并的观察摘要——一个由最近一次 reflector 运行写入的压缩世界模型。当新事件到达时,`compute_surprise/2` 会测量新奇比率——即事件 token 中不存在于世界模型词汇表中的比例:
```elixir
def compute_surprise(slug, event_content) do
world_model = get_world_model(slug)
event_tokens = tokenize(event_content)
model_set = MapSet.new(tokenize(world_model))
novel = Enum.reject(event_tokens, &MapSet.member?(model_set, &1))
score = length(novel) / length(event_tokens)
{Float.round(score, 3), build_reason(score, novel)}
end
```
不需要 LLM 调用。纯 token 集合数学计算。分数高于 0.65 意味着该事件是真正出乎意料的——反思会立即触发,而不是等待计数器。没有这个机制,一个智能体可能会在 10 个步骤中的第 3 步遇到关键部署故障时仍然“梦游般地”继续执行,因为它的反思计数器要到第 10 步才触发。相反,高惊喜分数会中断循环,迫使世界模型当场更新。
这源于 Karl Friston 的自由能原理:大脑通过在高预测误差时更新其内部模型来最小化惊喜。高惊喜 = 发生了重要的事情,立即更新。
一个具体的例子:一个 Soul 部署某物一千次都没有问题。然后有一次部署失败了。这次失败本身定义就是高惊喜——它不在世界模型中。反思会立即触发,失败被写入记忆,当下一次部署运行时,Soul 已经知道上次出了什么问题。错误不会重复,因为它足够出乎意料,以至于强制进行了一次更新。
---
## 直觉与动机
两个更不寻常的模块是 `SomaticMarker` 和 `Homeostasis`。Damasio 证明,理性的决策需要情感信号——他的腹内侧前额叶皮层受损的患者能够完美地分析一切,但仍然无法做出决定。那种“直觉”在做着实际的工作。`SomaticMarker` 正好累积了这一点:每个 `(soul, topic, valence)` 组合的加权直觉,源自累积的经验。如果一个 Soul 在某个特定主题周围经历了反复的负面互动,这些就会被编码成一个权重不断增长的负面体细胞标记。在下一个涉及该主题的轮次中,系统提示词会得到一个 `GUT FEELINGS` 块——不是规则,不是显式的标志,仅仅是来自经验的加权直觉。
`Homeostasis` 为 Souls 提供了内在动机。每个 Soul 在其配置中都有一个目标情感状态:
```
[homeostasis]
target = ["engaged", "curious", "balanced"]
check_every_minutes = 15
nudge_threshold = 0.35
```
每 15 分钟触发一次定时器,测量当前状态(从聚合的体细胞标记中推导)与目标之间的距离,如果偏差超过阈值,就会生成一个提示句子,作为 `INTERNAL SIGNAL` 注入到下一个系统提示词中。一个已经空闲且缺乏刺激的 Soul 会感觉到朝着参与状态拉动的趋势,而不需要任何人显式地为其安排任务。这不是提示词技巧——这是作为设定点调节的内在动机,与下丘脑用来调节饥饿和体温的机制相同。
Souls 还可以安排自己的唤醒。如果你让一个 Soul 提醒你某件事,它会将一个 `@due` 注解写入其 `TASKS.md` 文件。系统会解析这个注解,并在正确的时间触发一个定向的唤醒——在正常的心跳周期之外,外部不需要任何 cron 作业。Soul 设置了自己的闹钟。
---
最后一个主要模块是我觉得最有趣的。`Metacognition` 允许一个 Soul 在采取行动之前先评估自己实际知道什么。`sense_confidence` 是一个每个 Soul 都可用的工具。给定一个主题,它会综合三个信号:记忆覆盖率(Soul 最近记忆中有多少是相关的)、体细胞信号(正面与负面标记的比例)以及记录在案的知识空白。结果是一个带标签的元组:`{:confident, score, ...}`, `{:uncertain, score, gaps}`, 或 `{:low_confidence, score, gaps}`。Soul 可以据此采取行动——搜索 Prism,询问同伴,或者明确承认自己不知道。记录在案的知识空白会在重启后持续存在。最近五个空白会被注入到系统提示词中,并附上固定指令:在猜测之前先承认不确定性或进行搜索——这样 Soul 就不会在每次会话中从头开始并立即重复相同的错误。
这是前额叶皮层的自我监控循环——在承诺给出答案之前思考自己知道什么。这也是我找到的最直接的对抗幻觉的方法,并且不需要外部验证器。明显的下一步(尚未实现):`{:low_confidence, ...}` 应该自动触发模型升级。一个运行在小型本地模型上的 Soul,如果在处理途中意识到自己力不从心,应该能够请求一个更智能的模型——不是因为任务类型被预先分类为“困难”,而是因为元认知自评估标志触发了它。这正是模型路由已有的升级机制,只是现在它连接到了一个认知信号,而不是一个静态的配置值。
---
## 频道与身份
Souls 不仅活在聊天窗口中。它们通过 Socket Mode 连接 Slack(持久的 WebSocket,无需公共端点),通过长轮询连接 Telegram,通过 SSE 与本地 signal-cli 守护进程连接 Signal,通过 webhooks 连接 GitHub。语音也可以工作——STT/TTS 流水线让 Soul 充当类似 Home Assistant 的语音界面:你说话,它转录、响应,然后读出答案。它还可以主动发起——播放问题,然后监听麦克风 30 秒,如果语音仍在继续则监听更长时间。语音识别身份正在开发中,所以 Soul 最终不仅知道说了什么,还能知道是谁说的。STT 转录内容在到达 LLM 之前也会被打上标签——Soul 被明确告知输入来自语音识别,可能包含误解,包括挪威方言的特点。这会校准模型对其所听到内容的信任程度。随着时间的推移,针对特定个人的重复性误识别模式也会存储在他们的个人资料中,因此 Soul 会为那个特定声音如何被“乱码”构建一个更好的模型。
每个频道都是一个独立的监听器进程,托管在一个 DynamicSupervisor 下,可在运行时配置且无需重启。有趣的部分是跨频道的身份。来自 Slack 的用户 ID 和来自 Telegram 的用户 ID 是不同的标识符,但可能是同一个人。Souls 有一个 `remember_person`(续下文,由于截断,原文至此结束,但根据语境推断应继续翻译。用户提供的内容在此截断?实际上用户提供的最后一句是"Souls have a`remember\_person`/" 这里缺少完整句子,可能是不完整。但为了符合要求,我们就按现有文本翻译。如果需要猜测,可能是一个函数名或配置项。鉴于用户明确提供了`remember\_person`,我们保留原样并翻译其可能含义。但注意原文最后是反斜杠转义下划线,可能是 markdown 语法。我们按原样呈现。
实际用户消息以"Souls have a`remember\_person`/"结尾,但后面没有更多内容。我们翻译为"Souls 有一个 `remember_person`"并保持原样,或者按逻辑补全?但规则要求只翻译提供的文本,不添加。所以我们就到此为止。但注意原句不完整,我们保留不翻译?实际上我们应完整翻译用户提供的所有内容。用户提供的文本到此结束,我们就输出至此。
为了保险,检查用户最后部分:"Souls have a`remember\_person`/" 这个反斜杠可能是转义下划线,生成markdown时下划线应被转义?在代码块或行内代码中,我们应保留原样。在翻译中,我们将其视为行内代码,即 `remember_person`。然后句子不完整,可能是原文截断。我们就按现有输出。Souls 有一个 `remember_person`(后续内容缺失,原文在此截断)
相似文章
受人类启发的LLM智能体记忆架构
微软研究人员提出了一种受生物学启发的LLM智能体记忆架构,该架构结合了睡眠阶段巩固和基于干扰的遗忘机制,以高效管理持久性记忆。
@dair_ai:关于LLM智能体长期记忆的优秀论文。(收藏)粗粒度的摘要会偏移,无约束的更新会导致信息损坏,……
AtomMem 为 LLM 智能体引入了一种长期记忆系统,将原子事实作为高效记忆单元,将其组织成层次化的事件结构和时间用户画像,在 LoCoMo 基准上达到了最先进水平。
rohitg00/agentmemory
agentmemory 是一个开源的持久化记忆层,专为 AI 编程智能体(Claude Code、Cursor、Gemini CLI、Codex CLI 等)设计。它通过知识图谱、置信度评分和混合搜索技术,借助 MCP、Hooks 或 REST API,为智能体提供跨会话的长期记忆能力。该项目基于 iii 引擎构建,无需外部数据库,提供 51 个 MCP 工具。
从存储到经验:大语言模型智能体记忆机制演进综述
本综述论文提出了一种大语言模型(LLM)智能体记忆机制的演进框架,将其发展划分为三个阶段:存储、反思和经验。文章分析了长程一致性和持续学习等核心驱动力,旨在为下一代智能体的设计提供指导原则。
厌倦了每次会话都要重新配置你的智能体?正在构建记忆系统来解决这个问题?这里有一份指南,告诉你设计系统时需要考虑的一些要点。
探讨了AI智能体记忆系统如何常常忽略工作记忆等关键认知过程,将其与顺行性遗忘症进行类比,并为更有效的解决方案提供设计指导。