我给自己的 LLM 框架重写了记忆系统,然后三天吃出两个真 Bug

我给自己的 Go LLM Agent 框架写了套"生产级记忆系统",三天狗粮吃出两个真 Bug

从一个尴尬的 Bug 说起

我维护一个 Go 实现的多协议 LLM Agent 框架 tt-agent(OpenAI/Anthropic/Gemini 协议适配 + ReAct 循环 + 工作流编排)。某天回头看代码,发现一个 embarrassing 的问题:

默认的会话记忆是纯内存的。

进程一重启,所有用户的对话历史全没了。多实例部署?两个 Pod 各记各的,用户上一句在 A 实例、下一句到 B 实例,上下文当场失忆。

这是很多 LLM 应用的通病:Demo 阶段 demo 得飞起,上生产第一件事就是记忆这块补丁摞补丁。于是我决定把记忆系统当成一等公民重做一遍------这篇文章讲的就是这套系统的设计决策、真实性能数据,以及我自己吃狗粮吃出来的两个真 Bug。

先看架构:分层,但每层只为一个失败模式负责

scss 复制代码
┌────────────────────────────────┐

│ core.Memory 接口 │

│ Add / Recent / Clear / Split │

└───────────────┬────────────────┘

│

┌─────────────────────┼─────────────────────┐

│ │ │

┌────────▼────────┐ ┌─────────▼─────────┐ ┌───────▼────────┐

│ memorystore.Store│ │ snapshot.Store │ │ ltm.Memory │

│ 预算/截断/摘要语义 │ │ 快照 + 安全回滚 │ │ 长期记忆(向量) │

└────────┬────────┘ └─────────┬─────────┘ └───────┬────────┘

│ │ │

└──────────┬──────────┴──────────┬──────────┘

│ Driver 契约 │

(只存取字节,不理解任何业务语义)

│

┌───────────────┼───────────────┐

│ │ │

┌────▼────┐ ┌──────▼──────┐ ┌─────▼─────┐

│ Redis │ │ SQLite │ │ PostgreSQL │

└─────────┘ └─────────────┘ └───────────┘

三个关键设计决策:

决策一:驱动只存取字节,语义全部上收。 预算截断、系统消息保留、tool_calls 配对这些容易跑偏的逻辑,全部由 memorystore.Store 统一实现,四种后端构造上就不可能行为不一致。怎么保证的?一套共享契约测试跑在全部后端上------Redis 的测试挂了,不用看代码就知道是驱动层的锅。

决策二:一个 Redis 驱动喂两个视图。 会话记忆和快照(时间旅行)包同一个驱动,共享存储,跨进程锁自动生效:

go 复制代码
driver := redis.New(client, redis.Options{Prefix: "myapp:", SessionTTL: 24 * time.Hour})

mem := memorystore.New(driver, memorystore.Options{Observer: bus})

snaps := snapshot.New(driver, snapshot.Options{MaxPerSession: 20})

决策三:加密挂在编解码层,不在驱动层。 换一个 EncryptedCodec,四个后端全部自动获得 AES-256-GCM 静态加密,密钥永远不进驱动代码。实测开销 ~0.5µs/条------基本免费。

时间旅行:回滚一个"说错话"的 Agent

LLM 应用有个独特需求:模型某轮抽风说了不该说的,你希望把会话回滚到那之前,就像 git revert。

scss 复制代码
快照 ──► 对话继续 ──► 模型说错话 ──► RollbackSafe(快照)

│

┌──────────────┤

▼ ▼

恢复到快照 自动留存"安全快照"

(坏轮次也保着,后悔还能再回滚)

跨进程怎么防两个 Pod 同时回滚互相踩?三种共享后端各有方案:

| 后端 | 锁实现 | 崩溃安全 |

|---|---|---|

| Redis | SET NX PX + token 校验释放 | 租约 TTL 自动回收 |

| PostgreSQL | 事务级咨询锁 | 连接断开锁自动消散 |

| SQLite | 锁表 + 租约 | 过期租约下一获取者回收 |

每一种都有测试真刀真枪验证过------Postgres 那个是测试里直接 pg_terminate_backend 杀后端进程验证锁回收的。

长期记忆:不改进模型,只改这一轮的上下文

会话记忆管"这次对话",长期记忆管"一直记得的事"(用户是素食者、对花生过敏、养了只猫叫煤球)。

go 复制代码
vecStore, _ := pgvector.New(pool, pgvector.Options{Dim: 768, Schema: "agent_ltm"})

mem := ltm.New(vecStore, ltm.Options{

Embedder: embeddings.NewOpenAICompatible(ollamaURL, "ollama", "nomic-embed-text"),

Extractor: extractor.New(chatLLM, extractor.Options{Lenient: true}),

})

mem.Remember(ctx, "user-42", "用户对花生严重过敏", nil) // 显式写入

mem.Learn(ctx, "user-42", conversation) // LLM 从对话里抽事实

facts, _ := mem.Recall(ctx, "user-42", "推荐午餐", 5) // 向量召回

prompt := ltm.Prompt(facts) // 渲染成提示词注入

三个生产级细节:

  1. 向量失败自动降级关键词------远程 embedding 服务挂了,事实不丢,召回照常(质量下降但不中断)

  2. 命名空间硬隔离 ------user-42 的召回构造上不可能 带出 user-43 的任何事实,有专门的跨命名空间泄露测试

  3. 事件绝不携带消息内容------观测系统只发计数和 ID,webhook 配错 URL 也不会把对话泄出去

真实性能数据(Intel Ultra 9 185H 实测)

| 操作 | Redis | SQLite | PostgreSQL |

|---|---|---|---|

| 追加一条消息 | 122µs | 458µs | 1,584µs |

| 读取最近 1000 条 | 2,908µs | 2,159µs | 3,857µs |

| 修剪 1000 条 | 1,957µs | 606µs | 1,049µs |

| 长期记忆 | 100 条事实 | 1,000 条 | 5,000 条 |

|---|---|---|---|

| 关键词召回 | 71µs | 989µs | 6,322µs |

| 向量召回(pgvector) | --- | 1,184µs | 8,382µs |

(Redis 数字基于 miniredis 无网络往返;真 Redis 会多一个 RTT)

并发语义是测试背书的:16 并发连接 × 10 条消息打到同一会话,160 条零丢失;双路同时安全回滚,会话锁串行化,最终状态精确一致。

狗粮时刻:三天吃出两个真 Bug

我专门写了一个消费者项目(Gin 架构的完整 API 服务)来吃自己的狗粮,全链路打真实基础设施(真 Redis、真 pgvector、真 DeepSeek/智谱 GLM 双厂商)。结果第一天就咬出两个框架级 Bug:

Bug 1:SystemPrompt 静默丢失。 配置一路从 HTTP handler → agent loop 传到底,最后组装请求时忘了注入。所有设了系统提示词的用户,实际一个都没生效。为什么之前没发现?单测都只断言"传参正确",没人看真正发出去的请求。

Bug 2:抽取器硬编码模型名。 事实抽取模块发请求时写死 Model: "extractor"。宽松的服务商忽略未知模型名,DeepSeek 严格校验直接 400:The supported API model names are deepseek-flash, deepseek-v4-pro, but you passed extractor。宽松环境下所有测试全绿------第一次真实调用当场爆炸。

两个 Bug 的共同点:只有真实消费者 + 真实厂商 + 端到端断言才抓得出来。这也是我给所有做框架的人的建议:你的 dogfood 项目应该比你想象的更早开始。

后来问自己"支持 Redis Cluster 吗",查代码发现键没加 hash tag,Lua 修剪脚本在分片集群必炸 CROSSSLOT------第三个坑。已修(v1.3.0),但这件事说明:不问不查,用户会替你发现。

质量凭据

不是"我觉得能上生产",是这些:

javascript 复制代码
270+ 测试函数、27 个基准,-race 全绿

跨后端契约套件:四后端行为一致性是构造保证

故障路径测试:启动降级 / 运行中断网 / 同端口重启自愈

CI:push/PR 全量测试(-race + 真 pgvector 服务容器)

JSON wire 契约金样本:改一个 tag,CI 当场红给你看

双厂商实测:DeepSeek 15/15 + GLM 15/15 端到端

版本纪律:semver、Release 说明、破坏性变更显著标注

快速开始

bash 复制代码
go get github.com/Lookfukc/tt-agent@v1.3.0
go 复制代码
llm, _ := adapters.NewLLM(cfg) // 任意 OpenAI 兼容厂商

mem := driver.Memory(memorystore.Options{})

loop := agent.NewLoop(llm, tools.NewRegistry(), mem, agent.Config{

Model: "deepseek-chat",

SystemPrompt: "你是一个简洁的助手",

})

msg, usage, _ := loop.Run(ctx, sessionID, "你好")

会话记忆换后端只改一行(Redis ↔ SQLite ↔ PostgreSQL),业务语义零变化。双语 README(2600+ 行)有完整的生产装配指南、参数速查表和三个实战坑预警。

诚实的时间表

还没做好的:Anthropic embedding 适配器未对线上端点实测(地区限制,README 如实标注);零外部用户------所有"生产级"的凭证来自我自己的消费者项目和测试,还没有陌生人的生产环境替我锤过它。

项目在这里:github.com/Lookfukc/tt... 。如果你在做 Go 的 LLM 应用,欢迎来提 issue------被真实需求锤打是框架变好的唯一捷径。

  • 掘金:《我给自己的 LLM 框架重写了记忆系统,然后三天吃出两个真 Bug》
相关推荐
hpoenixf1 小时前
Agent 持续进化的秘诀:让你的 Agent 从大哥大进化成智能手机
agent
fellow992 小时前
DSH Desktop:把 DeepSeek Harness 变成真正的桌面应用
electron·agent·ai编程·deepseek
hpoenixf3 小时前
从“研究日志”到自然对话:我们如何让 AI Agent 的回答真正流畅起来
agent
运维开发王义杰3 小时前
让 AI 写文档、建 Skill,算知识沉淀吗?
agent
程序猿DD4 小时前
不知道自己的软件好不好用?让Agent扮演“新用户”来试试!
agent
hpoenixf5 小时前
Agent 已经生成答案,为什么还需要一个独立的发布组件?
agent
guslegend5 小时前
工程地基、Provider 与 ReAct 循环:Agent 核心能力的设计、实现与验收
人工智能·agent·react·架构设计·spring ai
10年前端老司机5 小时前
一次业务迭代:聊聊知识库多版本可追溯审核流程怎么做
人工智能·后端·agent