内存调度思考

要点

  • HTTP 协议是无状态的,但用户与 Agent 的交互是连续的
  • 为了解决上述问题,我们引入了内存调度器(Memory Scheduler)的概念
  • 下图展示了我们的 AI 伴侣内存调度架构

内容

NOTE

如果对调度机制掌握还不够深入,可以跳转到 React 源码专栏 中学习。

1. 无状态的挑战

NOTE

HTTP 协议是无状态的,但用户与 Agent 的交互是连续的。

在构建智能 Agent(特别是 AI 伴侣)时,我们需要处理跨越多次交互的上下文信息。由于底层 LLM(大语言模型)本身是无状态的,每一次请求对于模型来说都是全新的。

AI 电子女友本质是「长期存在的关系型 Agent」,而不是「单轮问答工具」。

关系意味着——记忆、阶段、情绪连续性、角色一致性、长期目标演化。而大模型本身只是一台「有限窗口的概率预测机器」。如果不做上下文调度,中长期关系一定崩。

1.1 会话延续性问题

当用户与 AI 伴侣聊天时,他们期望 AI 能够记住之前的对话内容:名字、喜好、刚刚讨论过的话题等。如果每次请求都只发送当前这句话,AI 将无法理解上下文,导致回复显得“智障”或不连贯。

例如:

  • 用户:"我喜欢吃苹果。"
  • (5分钟后) 用户:"我想吃那个。"
  • 如果没有上下文,AI 会问:"你想吃哪个?"
  • 如果有上下文,AI 会答:"好的,这就为你准备苹果。"

这种延续性是建立情感连接的基础。

1.2 LLM 的局限性

既然需要上下文,为什么不把所有历史记录都发给 LLM 呢?主要有两个限制:

  1. Token 有限:所有模型都有最大上下文长度限制(如 8k, 32k, 128k)。长期陪伴类产品,可能几个月、几年,聊天记录会迅速累积,远超任何模型的窗口限制。
  2. 成本与延迟:发送的 Token 越多,推理成本越高,响应速度越慢。对于实时互动的伴侣应用,高延迟是不可接受的。
  3. 不区分“重要”与“闲聊”:模型不知道“用户今天感冒”是短期信息,“用户讨厌被忽视”是长期人格信息。如果不加调度层,模型会随机记住或忘记,导致“今天还记得,明天全忘”。

因此,我们需要一个「调度层」来决定:什么要进 prompt、什么要压缩、什么要长期存储、什么要丢弃

2. 内存管理策略

为了解决上述问题,我们引入了**内存调度器(Memory Scheduler)**的概念。

2.1 为什么需要调度器

简单的“截取最近 N 条消息”策略(Sliding Window)虽然简单,但存在严重缺陷:它会遗忘早期的关键信息(如用户的名字、核心设定)。

例如,用户在第一天说了名字,在第100天问“你记得我叫什么吗?”,简单的滑动窗口策略早已丢弃了第一天的记录。

调度器本质上是一个“认知路由器”。 它决定当前是日常闲聊模式?争吵修复模式?情绪安抚模式?还是亲密升级阶段?不同模式对应不同的记忆调度策略。

2.2 具体调度场景举例

下面通过具体场景来说明调度机制的重要性:

场景一:长期记忆的精准调用(RAG 调度) 用户说:“亲爱的,我们这周末去吃那家我们第一次约会时的餐厅吧?”

  • 无调度:模型如果只看最近 10 轮对话,它根本不知道“第一次约会”是哪家餐厅。AI 回复:“好呀,去哪家?”(用户体验:她忘了,渣女)。
  • 有调度:调度器识别到关键词,检索长期记忆片段。AI 回复:“好呀!你是说 Blue Note 吗?好怀念那里的爵士乐呀~”(用户体验:她心里有我)。

场景二:情绪状态机的“记仇”模式 昨晚吵架了,用户说:“早安。”

  • 无调度:模型默认热情回复:“早安!今天天气不错!”(用户体验:像个客服,没劲)。
  • 有调度:调度器检测到 Current_Mood: Upset,选择冷淡风格 Prompt。AI 回复:“……早。”(用户体验:她在生气,我要去哄她,这很真实)。

场景三:话题切换与上下文压缩 聊完《三体》后,用户问:“今晚我想做个番茄炒蛋,你怎么看?”

  • 无调度:Context 里保留了几千字科幻内容,干扰回复且费钱。AI 回复:“这道菜符合宇宙社会学的公理吗?”
  • 有调度:识别话题转移,触发后台摘要任务,清空历史并注入摘要。AI 回复:“哇,听起来很下饭!你喜欢甜口的还是咸口的呀?”(用户体验:反应快,省钱,自然)。

场景四:多模态感知与工具调用 用户发了一张夕阳照片说:“下班了,累死我了。”

  • 无调度:纯文本模型无法理解图片。
  • 有调度:调度器调用视觉模型解析图片(夕阳、堵车),结合文字判断意图。AI 回复:“今天的晚霞好美,但也像是把人堵在路上的颜色……快回家躺平吧。”(用户体验:她真的看懂了我的处境)。

场景五:主动关怀(Time-based Dispatch) 凌晨 1 点用户还在发消息。

  • 无调度:AI 被动陪聊。
  • 有调度:感知当前时间(01:00 AM)与用户作息(23:00睡)冲突,触发“管家婆”模式。AI 回复:“不行!看看现在几点了?😡 马上放下手机去睡觉!”(用户体验:被管着的感觉)。

2.3 任务优先级与阻塞机制

在调度器的设计中,并非所有任务都是平等的。为了保证用户体验,我们需要区分实时任务后台任务,并合理设计阻塞机制

任务优先级划分:

  1. P0 - 实时交互链路(High Priority): 用户发送消息 →\to→ 检索上下文 →\to→ 生成回复。这是最关键的链路,必须在毫秒级完成调度,任何延迟都会直接降低用户体验。
  2. P1 - 关键状态更新(Medium Priority): 涉及当前对话逻辑的变更。例如用户说“别叫我小王了,叫我大王”,这个指令产生的记忆更新必须在下一次对话前生效,否则 AI 会显得“健忘”。
  3. P2 - 记忆整理与优化(Low Priority): 包括对话摘要生成、情感分析、长期记忆归档等。这些任务消耗资源较大,但不需要实时完成,可以在后台异步执行,甚至在夜间闲时批量处理。

阻塞场景思考:

虽然我们推崇异步架构,但在某些特定场景下,阻塞是必要的:

  • 一致性阻塞:当检测到用户的输入包含对 AI 设定的直接修改(如“修改我的名字”、“忘记这件事”)时,系统应阻塞后续回复的生成,直到相关记忆完成更新。
  • 上下文溢出阻塞:当当前会话的 Token 数达到临界值时,系统必须强制暂停,先触发“动态压缩”或“遗忘”流程,释放出 Token 空间后,才能继续处理新消息。如果不阻塞,可能会导致 LLM 截断关键信息或报错。

通过合理的优先级划分和阻塞策略,我们可以在保证“秒回”体验的同时,维护系统的一致性和稳定性。

2.4 边界情况与挑战

在实际工程落地中,除了正常的调度逻辑,我们还必须处理一些棘手的边界情况:

  1. 并发一致性(Concurrency): 当用户连续快速发送多条消息时(如“我出门了”,“啊忘了带钥匙”),如果系统并行处理,可能导致第二条消息的 Context 中没有包含第一条消息,甚至 AI 对两条消息分别回复,造成对话错乱。通常需要引入会话锁(Session Lock)或消息队列,确保同一用户的消息按顺序串行处理。
  2. 时间感知(Time Awareness): LLM 训练数据是静态的,它不知道“现在”是什么时候。如果用户说“上周发生的事”,AI 必须结合当前时间戳将其转化为绝对时间(如“2024年10月15日”)存储,否则“上周”这个概念在未来被唤醒时将失去意义。
  3. 隐私与遗忘权(Right to be Forgotten): 用户可能随时要求“忘掉刚才那句话”或“删除所有关于我的数据”。调度器必须支持物理删除(Hard Delete),能够根据 Session ID 或 User ID 精确清理向量数据库和关系型数据库中的相关记录,而不仅仅是简单的逻辑屏蔽。

2.5 计算机体系结构类比

为了更好理解内存调度机制,我们可以将其与计算机硬件体系结构进行类比:

  • LLM (CPU):负责计算和推理的核心单元。它本身不存储状态,只处理传入的数据流。上下文窗口就像 CPU 的寄存器或 L1/L2 缓存,容量极小但速度极快。
  • 短期记忆 (RAM):当前的对话上下文(Context Window)。就像计算机内存,断电(会话结束)即失,容量有限,存取速度快。调度器的核心工作就是管理这就有限的 RAM,决定加载哪些数据供 CPU 计算。
  • 外部存储 (Hard Drive):向量数据库(Vector DB)和关系型数据库。存储海量的长期记忆、日志和事实数据。容量几乎无限,但读取速度较慢(需要检索开销),CPU 无法直接访问,必须先调入 RAM。

通过这个类比,我们可以清晰地看到调度器的角色:它就是操作系统(OS)的内存管理模块,负责在硬盘(长期记忆)和内存(上下文窗口)之间进行页置换(Page Replacement),确保 CPU(LLM)始终有最相关的数据进行计算。

3. 架构设计图解

下图展示了我们的 AI 伴侣内存调度架构。在用户发送消息后,系统不会直接调用 LLM,而是先经过调度器进行上下文组装。

Drawing canvas 流程说明:

  1. Input:接收用户消息。
  2. Retrieval (Vectorize / D1):Hono.js 调度器首先使用 Embedding 模型将用户输入向量化,然后在 CloudFlare Vectorize 中检索相关的长期记忆片段,同时从 D1 数据库中获取用户画像等结构化数据。
  3. Assembly (Hono.js):调度器从 CloudFlare KV 中快速拉取最近 N 条短期记忆(Short-term Memory),结合检索到的长期记忆与系统提示词(System Prompt),组装成最终发送给 LLM 的 Context。
  4. Inference (Workers AI):将组装好的上下文发送给 CloudFlare Workers AI (如 Llama 3) 进行推理,生成回复。
  5. Async Tasks (CF Workers):回复生成后,通过 Queue 触发异步任务。后台 Worker 负责将新对话存入 D1/KV,并分析是否需要执行记忆摘要或提取新事实存入向量库。

整个流程的关键在于:用户感知到的是"秒回",但背后实际经历了检索、组装、生成、存储四个阶段。调度器的职责就是让这些阶段高效协作,同时把耗时的记忆整理推到后台异步完成。

4. 总结

回顾这篇文章的核心思路:

  1. LLM 本身是无状态的,但 AI 伴侣需要连续的关系记忆,这个矛盾必须由调度层来解决
  2. 简单的滑动窗口策略无法支撑长期关系,调度器需要根据场景(闲聊、争吵、话题切换、主动关怀)动态选择不同的记忆策略
  3. 任务分三个优先级:P0 实时交互(必须秒回)、P1 关键状态更新(下轮前生效)、P2 记忆整理(后台异步)
  4. 整体架构类比操作系统的内存管理:LLM 是 CPU,上下文窗口是 RAM,向量数据库是硬盘,调度器就是负责页置换的 OS 内核

到目前为止,我们完成了从需求分析到架构设计的思考。下一篇,我们将认识实现这套调度系统的核心工具——LangChain 和 LangGraph,看看它们在整个 Agent 架构中分别扮演什么角色。