开发 AI coding 工具时,最让人头痛的问题往往不是模型能力不够,而是“上一次的上下文去哪了”。项目里改到一半的接口、用户刚说过的代码风格偏好、昨天刚排查出的编译异常,只要关掉终端,一切就像没发生过。本文将以 Llmem 这个本地持久化记忆方案为主线,拆解它在 AI coding 场景下的设计思路、不依赖 embedding 的检索方式,并给出完整的本地接入示例和工程建议。
1. 从“AI 对话失忆”说起:本地持久化记忆要解决什么问题
1.1 AI coding 的常见困境
先还原一个很常见的开发场景。
你正在用 AI coding 工具辅助开发一个 Spring Boot 服务,上午刚让工具生成了UserService接口的骨架,定义了findById、updateUser、deleteUser三个方法,并且明确了接口返回值为R<T>统一封装。下午继续工作时,工具对项目背景一无所知。
于是你只能反复做这几件事:
- 重新描述项目结构。
- 重新粘贴关键代码。
- 重新解释业务规则。
- 重新指出“不要用 Map 接收返回结果”。
这就是典型的“AI 对话失忆”问题。会话上下文只存在于一次对话内部,不会自动跨会话持久化。对于需要长期维护的中大型项目,这种重复沟通成本会变得非常高。
而 AI coding 工具要真正进入日常开发流程,一个很重要的能力就是:记住项目上下文、用户偏好和历史决策,并在后续会话中自动恢复。
1.2 Llmem 是什么
Llmem 是一个面向 AI coding 场景的本地持久化记忆方案,全称可以理解为 Local Memory。它解决的问题非常聚焦:让 AI coding agent 在多次会话之间保留可检索的记忆数据。
从项目标题 “Show HN: Llmem – Local persistent memory for AI coding, no embeddings” 可以看出,这个方案有两个核心特点:
- Local:记忆数据存储在本地,不依赖云端服务。
- No embeddings:不使用向量嵌入,不依赖向量数据库。
这两个特点组合起来意味着什么?意味着它不追求“语义模糊检索”,而是把记忆当作结构化数据来管理。你可以把它理解成一个给 AI 助手专用的“本地工作日志数据库”,每次开发会话结束后,把关键信息写入记忆;下次会话开始时,按照作用域和条件把相关记忆读取出来,再拼进 prompt 上下文。
1.3 适合哪些读者
如果你满足以下任一情况,这篇文章会比较有帮助:
- 正在开发自己的 AI coding agent 或 AI 编程助手。
- 觉得现有 AI 工具在长周期项目里“记不住事”。
- 需要一个不依赖云服务、不上传代码的本地记忆方案。
- 想了解不使用 embedding 的情况下,如何设计记忆系统。
接下来,我们先解释为什么 Llmem 可以选择放弃 embedding,再逐步拆解它的设计和实现。
2. 为什么本地优先?为什么不用 embedding?
2.1 embedding 方案的典型成本
embedding(向量嵌入)是当前 AI 知识库的常见做法。流程一般是:
- 将文本切块。
- 调用 embedding 模型生成向量。
- 存入向量数据库。
- 查询时把用户问题向量化。
- 用向量相似度召回最相关内容。
这套方案在开放域语义检索里非常有效,但在 AI coding 场景里,有几个现实问题。
第一个是成本问题。embedding 模型需要额外的计算资源。本地跑模型,对开发机内存和 CPU 有要求;调用云端 API,则涉及费用和数据出网。
第二个是配置复杂度。你需要维护 embedding 服务、向量数据库、索引参数、切块策略、相似度阈值等。对于个人开发者或中小团队的项目工具来说,这套基础设施并不轻量。
第三个是可控性问题。向量检索是“近似匹配”,结果不一定完全符合项目上下文。代码项目里的方法名、类名、配置项都是强结构化信息,比如UserService、application.yml、@Transactional,这些关键字段需要的不是模糊语义,而是准确命中。
2.2 结构化记忆的可行性
AI coding 的记忆,本质上和“知识库问答”不太一样。
知识库问答需要理解长文档,问题往往是开放式的:“这个产品的售后政策是什么?”而 AI coding 的记忆更加偏向工作日志和状态记录:
- 当前项目用到哪些技术栈。
- 代码风格是什么。
- 哪些模块已经完成。
- 哪些接口还在开发中。
- 用户最近做了哪些调整。
- 上次运行时报了什么错。
这些信息天然可以用结构化字段表达。例如:
{ "scope": "project:llmem", "type": "coding_preference", "content": "Java 后端统一使用 R<T> 返回,禁止直接返回 Map", "updated_at": "2026-08-11T09:30:00Z" }这种结构下,要召回一条记忆,不需要“语义理解”,只需要按scope过滤,按type分类,按updated_at排序就足够了。这也是 Llmem 不依赖 embedding 的底气所在。
2.3 Llmem 的定位与适用场景
Llmem 更适合对准确性和可解释性要求较高的本地开发场景。
它的定位可以总结为:
- 轻量级。不需要启动额外的向量检索服务。
- 确定性。记忆通过键、标签、作用域来定位,结果可预期。
- 隐私友好。数据留在本地,不进入第三方模型服务。
- 易接入。结构化数据格式统一,可以快速集成到 AI coding agent 的 prompt 组装流程中。
当然,这不意味着 embedding 方案没有优势。如果你的项目需要从大量非结构化文档里做语义检索,比如检索团队 Wiki、历史工单、设计文档,那向量检索仍然是最合适的方案。Llmem 的适用边界是“编程过程中的结构化记忆”,两者不是替代关系。
3. 环境准备与快速上手
3.1 运行环境要求
Llmem 的部署形态偏向本地工具,通常以命令行工具或库的形式集成。本文示例以常见环境为例,重点演示设计思路,具体版本需要根据你的项目实际情况调整。
建议环境:
- 操作系统:macOS / Linux / Windows(WSL2 均可)。
- 运行时:Node.js 18+ 或 Python 3.9+,取决于你拿到的发行版。
- 数据存储:本地文件或 SQLite。
- 调用方:任何支持命令行调用或 SDK 接入的 AI coding agent。
如果项目提供了 CLI 安装入口,常见安装命令看起来像这样:
npm install -g llmem # 或 pip install llmem注意:具体包名和安装方式以项目仓库 README 为准。我们这里不再猜测版本号,核心是掌握它的设计思想。
3.2 初始化本地存储
安装完成后,第一步是初始化记忆存储目录。通常需要指定一个数据库文件或数据目录。
llmem init --db ~/.llmem/agent.db执行完成后,会在~/.llmem/目录下生成一个数据库文件,用来存放所有记忆条目。
如果项目提供了配置文件,通常会包含类似这样的内容:
storage: type: sqlite path: ~/.llmem/agent.db default_scope: global max_results: 10 ttl_days: 180storage.type:存储类型。storage.path:存储文件位置。default_scope:默认作用域。max_results:单次召回的最大条数。ttl_days:记忆保留天数,超过时间的自动过期。
3.3 最小可运行示例
我们来演示一个最基础的写入和读取流程。
写入一条记忆:
llmem add \ --scope "project:llmem" \ --type "task_progress" \ --content "已完成 SQLite 存储层设计,下一步实现 API 层" \ --tag "llmem" --tag "storage"读取指定作用域的最新记忆:
llmem get \ --scope "project:llmem" \ --limit 5预期输出可能是一组按时间倒序排列的记忆条目,每条包含内容、类型、标签和更新时间。
这样一个最小闭环就建立了。AI coding agent 在每次会话开始时,可以调用llmem get --scope <当前项目>把历史记忆加载到上下文中。
4. 核心设计与配置拆解
4.1 记忆的存储结构
Llmem 在数据模型上通常包含几个核心字段:
| 字段 | 作用 | 示例 |
|---|---|---|
| id | 唯一标识 | 01J4X... |
| scope | 作用域 | project:llmem 或 user:default |
| type | 记忆类型 | task_progress / coding_preference / error_log |
| content | 记忆内容 | 纯文本或 Markdown |
| tags | 标签列表 | ["python", "storage"] |
| created_at | 创建时间 | 2026-08-11T09:20:00Z |
| updated_at | 更新时间 | 2026-08-11T09:30:00Z |
| ttl | 过期时间 | 可为空,表示长期有效 |
scope 是记忆隔离的关键。同一个数据库里可以存放多个项目的记忆,通过 scope 天然隔离,避免互相污染。
type 的作用是让后续检索更有针对性。比如:
task_progress:任务进度。coding_preference:代码风格偏好。error_log:报错记录。architecture_decision:架构决策。
这种分类本身就是在做“元数据管理”,是替代 embedding 检索的重要基础。
4.2 如何写入记忆
写入记忆的核心不是“存进去”,而是“知道该存什么、怎么分类”。
一个好的记忆条目需要满足三个条件:
- 内容本身反映可复用信息。
- 作用域足够清晰。
- 类型和标签能够支撑后续精确召回。
举例说明。
llmem add \ --scope "project:llmem" \ --type "coding_preference" \ --content "Python 代码使用 black 格式化,行宽 88,提交前必须运行 ruff 检查。" \ --tag "python" --tag "style"这条记忆可以被后续任何会话读取,从而避免用户重复说明代码风格。
写入代码内部调用时,逻辑大致如下:
from llmem import LocalMemory mem = LocalMemory(db_path="~/.llmem/agent.db") mem.add( scope="project:llmem", memory_type="coding_preference", content="Python 代码使用 black 格式化,行宽 88。", tags=["python", "style"], )这段代码展示了将记忆写入本地库的基本思路。实际 SDK 的类名和参数名请以项目版本为准。
4.3 如何检索与召回
检索是记忆系统的核心。Llmem 不靠向量相似度,而是靠字段过滤和排序。
常用检索维度:
scope:必须匹配,这是最基本的隔离维度。type:按记忆类型过滤。tags:按标签过滤,支持多标签组合。keyword:对 content 做关键词匹配。time_range:按时间范围过滤。limit:限制返回条数。
一个简单的查询示例:
llmem get \ --scope "project:llmem" \ --type "error_log" \ --limit 10这个查询返回该项目下最近的 10 条错误日志,用于快速恢复报错背景。
如果要做更精细的匹配,可以搜索关键词:
llmem search \ --scope "project:llmem" \ --keyword "ruff" \ --limit 5这个命令会返回 content 中包含 “ruff” 的记忆条目。对代码项目来说,关键词匹配往往比语义检索更直接,因为方法名、包名、报错信息都是强 token。
4.4 会话隔离与作用域
多项目场景下,作用域设计非常重要。
推荐的作用域命名规则:
- 全局偏好:
user:default - 项目维度:
project:<project-name> - 模块维度:
module:<project-name>:<module-name> - 用户维度:
user:<username>
AI coding agent 可以在每次对话开始时,加载多个作用域的记忆。例如:
- 先加载
user:default,获得用户通用偏好。 - 再加载
project:llmem,获得项目级上下文。 - 如果要处理某个模块,再加载
module:llmem:storage。
这种分层加载方式,和传统日志系统里的 logger 层级十分相似。它比把所有内容塞进一个大的 prompt 要更容易控制上下文长度。
5. 完整实战:给本地 AI coding 助手接入 Llmem
这一节我们做一个完整示例,模拟一个本地 AI coding 助手的记忆模块。为了便于展示,代码采用 Python 实现,并用 SQLite 作为存储层。你可以根据实际语言和 Llmem 版本来调整。
5.1 需求分析
我们要实现一个简单但完整的记忆服务,支持:
- 添加记忆。
- 按作用域查询记忆。
- 按类型过滤记忆。
- 关键词搜索记忆。
- 记忆过期清理。
5.2 项目结构
llmem-demo/ ├── db.py # 数据库连接与建表 ├── memory.py # 记忆服务核心逻辑 ├── agent.py # 模拟 AI coding agent 调用 └── demo.py # 演示入口5.3 数据库层代码
先创建数据库连接和初始表结构。
# 文件路径:llmem-demo/db.py import sqlite3 from pathlib import Path DB_PATH = Path.home() / ".llmem" / "demo.db" def get_connection(): """获取数据库连接,并自动创建数据目录""" DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): """初始化记忆表""" conn = get_connection() conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, scope TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT '[]', created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), ttl INTEGER DEFAULT NULL ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_scope ON memories(scope) """) conn.commit() conn.close()这里用tags TEXT存储 JSON 数组,是为了简化示例。生产环境可以考虑拆成关联表,或者使用支持数组类型的数据库。
5.4 记忆服务核心逻辑
接下来是记忆的增删改查。
# 文件路径:llmem-demo/memory.py import json from typing import List, Optional from db import get_connection, init_db class MemoryManager: """本地持久化记忆管理""" def __init__(self): init_db() def add( self, scope: str, memory_type: str, content: str, tags: Optional[List[str]] = None, ttl_days: Optional[int] = None, ) -> int: """新增一条记忆,返回记忆 ID""" tags_json = json.dumps(tags or []) conn = get_connection() cur = conn.execute( """ INSERT INTO memories (scope, type, content, tags, ttl) VALUES (?, ?, ?, ?, ?) """, (scope, memory_type, content, tags_json, ttl_days), ) conn.commit() memory_id = cur.lastrowid conn.close() return memory_id def get( self, scope: str, memory_type: Optional[str] = None, limit: int = 10, ) -> List[dict]: """按作用域查询记忆""" conn = get_connection() sql = "SELECT * FROM memories WHERE scope = ?" params = [scope] if memory_type: sql += " AND type = ?" params.append(memory_type) sql += " ORDER BY updated_at DESC LIMIT ?" params.append(limit) rows = conn.execute(sql, params).fetchall() conn.close() result = [] for row in rows: item = dict(row) item["tags"] = json.loads(item["tags"]) result.append(item) return result def search(self, scope: str, keyword: str, limit: int = 10) -> List[dict]: """关键词搜索记忆""" conn = get_connection() rows = conn.execute( """ SELECT * FROM memories WHERE scope = ? AND content LIKE ? ORDER BY updated_at DESC LIMIT ? """, (scope, f"%{keyword}%", limit), ).fetchall() conn.close() result = [] for row in rows: item = dict(row) item["tags"] = json.loads(item["tags"]) result.append(item) return result def cleanup(self): """清理过期记忆""" conn = get_connection() conn.execute( """ DELETE FROM memories WHERE ttl IS NOT NULL AND datetime('now', '+' || ttl || ' days') < datetime(created_at) """ ) conn.commit() conn.close()这里有几个设计点值得说明。
第一,ttl_days作为可选字段,不填就是永久记忆。这样既支持临时提醒,也支持长期项目经验沉淀。
第二,所有查询都强制要求scope参数。这避免了误把所有项目的记忆混在一起,是数据隔离的底线。
第三,search方法使用LIKE做关键词匹配。它虽然简单,但足够应对代码项目中的方法名、报错信息等精确匹配场景,这也是“不用 embedding”的核心思路。
5.5 模拟 AI coding agent 调用
现在模拟一个简单的 agent 会话恢复流程。
# 文件路径:llmem-demo/agent.py from memory import MemoryManager class CodingAgent: """模拟 AI coding agent,在对话开始时恢复记忆""" def __init__(self, project_name: str): self.project_name = project_name self.scope = f"project:{project_name}" self.mem = MemoryManager() self.context = [] def load_context(self): """加载全局偏好 + 项目进度""" global_pref = self.mem.get(scope="user:default", memory_type="coding_preference", limit=5) project_progress = self.mem.get(scope=self.scope, memory_type="task_progress", limit=10) error_logs = self.mem.get(scope=self.scope, memory_type="error_log", limit=5) self.context = global_pref + project_progress + error_logs return self.context def build_prompt(self, user_question: str) -> str: """将记忆拼接到 prompt 中""" memory_block = "\n".join( f"[{item['type']}] {item['content']}" for item in self.context ) prompt = f"""你是 {self.project_name} 项目的 AI 编程助手。 以下是项目历史记忆: {memory_block} 用户问题: {user_question} 请基于历史记忆和项目上下文回答。 """ return prompt # 文件路径:llmem-demo/demo.py from agent import CodingAgent from memory import MemoryManager # 1. 初始化记忆 mem = MemoryManager() # 写入全局代码风格偏好 mem.add( scope="user:default", memory_type="coding_preference", content="Python 使用 black 格式化,行宽 88,提交前运行 ruff。", tags=["python", "style"], ) # 写入项目进度 mem.add( scope="project:llmem-demo", memory_type="task_progress", content="已完成记忆服务的数据库层和核心查询逻辑,下一步补充 API 层。", tags=["progress"], ) # 写入一条错误记录 mem.add( scope="project:llmem-demo", memory_type="error_log", content="启动时出现 module not found: db,原因是 PYTHONPATH 未包含当前目录。", tags=["error", "python"], ) # 2. 模拟新的会话 agent = CodingAgent(project_name="llmem-demo") agent.load_context() prompt = agent.build_prompt("上次我们做到哪里了?") print(prompt)5.6 运行与验证
运行演示脚本:
cd llmem-demo python demo.py预期输出会展示一个包含历史记忆的 prompt。可以看到,agent 在没有任何额外说明的情况下,自动获得了代码风格、任务进度和错误记录。这正是本地持久化记忆的价值:让每次新会话都像上次对话的延续。
6. 常见问题与排查思路
6.1 记忆写入成功但查询不到
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
add返回成功,get查不到 | scope 不一致 | 检查写入和查询的 scope 是否完全一致 |
| 只有部分记忆被召回 | limit 设置过小 | 调大 limit 参数 |
| 查不到某条关键词 | content 大小写/格式不一致 | 确认关键词是否与 content 中的 token 完全一致 |
建议在写入时做一次get回读,并把 scope 打印到日志里,避免低级失误。
6.2 数据文件被占用
SQLite 在多进程同时写入时可能出现database is locked错误。
解决思路:
- 设置合理的
timeout。 - 写入操作使用短事务。
- 多个 agent 进程尽量写入不同的 scope,但同一个 SQLite 文件仍然需要串行写。
也可以在命令行工具层面增加“写入重试”机制。
6.3 记忆越来越多,prompt 过长
记忆系统最大的风险不是存不下,而是“攒得太多导致每次恢复时 prompt 爆掉”。
解决思路:
- 限制
limit。 - 对记忆做分层,高频偏好放长期,任务进度按周清理。
- 增加
ttl字段,让过期记忆自动清理。 - 必要时只加载最近 N 条,而不是全部加载。
6.4 多 agent 协同时的记忆混淆
近期 AI coding 领域强调多 agent 协同,多个子 agent 可能同时读写记忆。此时 scope 设计要更加严格:
- 每个子 agent 使用独立 scope。
- 共享的决策记录放到
project级别。 - 为每个 agent 写入操作带上 agent 标识。
例如:
scope: project:llmem-demo:planner scope: project:llmem-demo:coder scope: project:llmem-demo:reviewer这样既能共享项目级信息,又能避免子 agent 之间互相干扰。
7. 最佳实践与工程建议
7.1 记忆的粒度控制
不是所有内容都值得写入记忆。
建议只记录以下内容:
- 用户长期偏好,例如代码风格、提交规范。
- 项目关键决策,例如“不要使用 Map 作为返回类型”。
- 排错结论,例如“启动失败通常是缺少环境变量”。
- 当前里程碑状态,例如“已完成存储层,待开发 API 层”。
不建议记录:
- 临时性的代码片段,这些可以从历史 git 记录中找回。
- 大段日志,除非是重要报错摘要。
- 用户的日常闲聊。
7.2 prompt 组装顺序
AI coding agent 在恢复记忆时,prompt 组装顺序会影响效果。推荐顺序:
- system 指令。
- 用户全局偏好。
- 项目级上下文。
- 任务级进度。
- 当前用户输入。
越靠近当前任务的信息越应该放在后面,因为最新内容对模型注意力影响更直接。
7.3 数据安全与备份
本地记忆是隐私敏感数据。需要遵循最小权限原则:
- 记忆文件设置合适的文件权限,例如
chmod 600。 - 数据库文件不要提交到 git 仓库。
- 如果记忆同步到云端,必须加密传输。
- 定期备份数据库文件。
备份命令示例:
cp ~/.llmem/agent.db ~/.llmem/backups/agent-$(date +%Y%m%d).db7.4 与 embedding 方案的组合使用
Llmem 不依赖 embedding,不代表项目里不能同时存在 embedding 检索。
一个合理的架构是:
- 项目文档、技术方案等非结构化内容,走向量检索。
- 开发偏好、任务进度、报错记录等结构化内容,走 Llmem 这类方案。
最终在 agent 内部把两类结果合并后组装 prompt。这样既保证轻量,又不牺牲复杂的语义检索能力。
8. 总结与学习路线
本文围绕 Llmem 这个本地持久化记忆方案,梳理了它在 AI coding 场景下的核心价值:让 AI coding agent 在多次会话之间保存和恢复上下文,从而减少重复沟通。
关键点可以归纳为:
- 本地存储保证数据隐私和可控性。
- 不依赖 embedding,用作用域、标签、关键词完成精确召回。
- 结构化记忆更适合开发场景中的项目进度、代码偏好和报错记录。
- 通过 scope 隔离实现多项目、多 agent 的协同安全工作。
- 接入方式以 CLI、SDK、数据库文件为主,轻量且易集成。
如果你正在开发自己的 AI coding agent,下一步可以尝试从这几个方向继续深入:
- 将 Llmem 记忆加载逻辑接入到现有 prompt 工程流程中。
- 研究多 agent 场景下记忆共享和互斥的锁机制。
- 增加记忆的自动摘要功能,用 LLM 把零散记录压缩成结构化条目。
- 结合本地向量库,形成“结构化记忆 + 语义检索”的混合上下文方案。
本地持久化记忆是一个相对新兴的方向,但它解决的问题非常真实。每次开发中断后重新找回上下文的时间,累积起来是很可观的成本。如果你也遇到过类似的“AI 失忆”问题,不妨在本地工具链里加入这样一层轻量记忆。
如果本文对你有帮助,可以收藏备用。后续我还会继续更新 AI coding 工具链、本地上下文管理和多 agent 协同的实操笔记。