让 AI Agent 记住你是谁、你喜欢什么、你的项目用了什么技术栈 —— 实现跨会话的持久化记忆,使得每次对话都比上一次更懂你。

首先,探讨为何 Agent 需要记忆
大多数传统 AI 应用运行在“金鱼模式”下 —— 每次对话都从零开始。你告诉它“我喜欢用 Python”,下次它可能还会问你喜欢用什么语言。你纠正过一次“别用 var,用 const”,下次生成的代码里可能依然会出现 var。
这并不能算是一个真正的智能体,更像是一个缺乏海马体的对话机器人。
一个真正的 Agent 应当具备以下能力:
- 记住用户偏好 —— 编程风格、语言习惯、工具选择。
- 积累项目知识 —— 技术栈、文件结构、架构模式。
- 从交互中自主学习 —— 无需用户主动叮嘱“记住这个”,Agent 能够自行判断哪些信息值得被长期记忆。
这就是许多人在体验了先进的 智能体 框架后,感受到其高度智能背后的关键原理之一。我们在 so-claude-code 项目中实现了这套完整的记忆系统,其设计参考了优秀开源项目的理念,并经过实践验证。本文记录了该系统从第一版 JSONL 平面存储到第二版 Markdown 结构化文件系统的演进过程,并重点剖析一个核心设计:LLM 驱动的自动学习(Auto-Learn)。
回顾第一版:JSONL 平面记忆
数据结构
初始设计非常简洁。一条记忆就是一个 C 语言结构体:
typedef struct {
char *id; // 如 “mem_3_1710892800”
char *content; // 如 “用户偏好 Python 3.11”
char *category; // “preference” / “fact” / “pattern”
long timestamp; // Unix 时间戳
int hit_count; // 被检索次数
} memory_entry_t;
typedef struct {
memory_entry_t *items; // 动态数组
int count, cap;
char *filepath; // “data_dir/memory.jsonl”
} memory_store_t;
存储格式采用 JSONL(JSON Lines)—— 每行一条独立、完整的 JSON 记录:
{"id":"mem_1_1710892800","content":"用户偏好中文回复","category":"preference","timestamp":1710892800,"hit_count":3}
{"id":"mem_2_1710893000","content":"项目使用 Flask + SQLAlchemy","category":"fact","timestamp":1710893000,"hit_count":1}
双层加载
记忆被分为两个层级进行加载:
int memory_init(memory_store_t *store, const char *data_dir) {
// 1. 加载全局记忆 —— 跨项目共享
char *global_path = platform_expand_path(“~/.c_claude/memory.jsonl”);
load_from_file(store, global_path);
// 2. 加载本地记忆 —— 项目专属
store->filepath = platform_path_join(data_dir, “memory.jsonl”);
load_from_file(store, store->filepath);
}
全局记忆(~/.c_claude/memory.jsonl)存放跨项目通用的用户偏好,例如“喜欢简洁回复”、“习惯 vim 键位”。本地记忆({data_dir}/memory.jsonl)则存放项目特定的上下文,例如“这个项目用 React 18”、“API 入口在 src/routes/”。
注入到 Agent 循环
每次 Agent 开始推理前,记忆都会被注入到系统提示词中:
void memory_build_context(memory_store_t *store, strbuf_t *out, int max_entries) {
strbuf_append_str(out, “\n## Long-term Memory\n”
“The following are remembered facts and preferences:\n”);
int start = store->count > max_entries ? store->count - max_entries : 0;
for (int i = start; i < store->count; i++)
strbuf_appendf(out, “- [%s] %s\n”,
store->items[i].category,
store->items[i].content);
}
注意 start 变量的计算逻辑 —— 当记忆条目过多时,仅取最近的 N 条。这是一个简单却有效的策略:新产生的记忆通常比旧记忆更具相关性。
去重
用户重复表达十次“我喜欢 Python”,我们没必要存储十条完全相同的记录。去重逻辑如下:
int memory_has_similar(memory_store_t *store, const char *content) {
char norm_new[512];
normalize_str(content, norm_new, sizeof(norm_new)); // 转为小写并去除多余空白
for (int i = 0; i < store->count; i++) {
char norm_existing[512];
normalize_str(store->items[i].content, norm_existing, sizeof(norm_existing));
// 完全匹配
if (strcmp(norm_new, norm_existing) == 0) return 1;
// 子串匹配 + 长度比 > 60%(防止短字符串误匹配)
if (strstr(norm_existing, norm_new) || strstr(norm_new, norm_existing)) {
size_t shorter = min(new_len, exist_len);
size_t longer = max(new_len, exist_len);
if (shorter * 100 / longer > 60) return 1;
}
}
return 0;
}
60% 的阈值是经验值。设定过低会导致漏掉近似重复项,过高则会误杀有实质区别的记忆。例如,“用户喜欢 Python” 和 “用户喜欢 Python 3.11” 的长度比超过 72%,会被判定为相似;而 “用户喜欢 Python” 和 “项目使用 Python 做数据分析” 的长度比仅 47%,则不会被判定为重复。
第一版暴露的问题
实际使用一段时间后,几个问题逐渐显现:
- 结构过于扁平。所有记忆混杂在同一个文件中 —— 用户偏好、项目事实、行为模式,全部平铺在一个列表里。Agent 难以区分“这个人是谁”和“这个项目用了什么”。
- 人格与记忆割裂。人格(personality)配置存储在
config.json 中,而记忆存储在 memory.jsonl 中,两套系统独立运作,缺乏统一的上下文模型。
- 对人类不友好,难以手动编辑。JSONL 格式对人类阅读和编辑极不友好。若想手动修改一条记忆,需要打开文件,找到对应行 JSON,精确编辑字段后保存,过程繁琐。
- Agent 自身无法修改文件结构。没有提供让 Agent 直接编辑记忆文件结构的工具。它只能通过 API 添加或删除条目,无法进行重组或结构化调整。
演进至第二版:Markdown 上下文文件系统
核心思路
我们决定将“扁平的记忆存储”升级为“结构化的上下文文件系统”。核心变化是:
从一个 JSONL 文件,转变为一组具有明确语义角色的 Markdown 文件。
data_dir/context/
├── SOUL.md # Agent 的人格定义 —— 名字、语气、专长、价值观
├── USER.md # 用户画像 —— 偏好、习惯、技术背景
├── MEMORY.md # 项目知识 —— 技术栈、架构、模式
└── BOOTSTRAP.md # 首次启动引导(临时文件)
为何选择 Markdown?
- 人类可读可编辑。用户可以直接打开 SOUL.md,修改 Agent 的名字和语气。
- Agent 也能编辑。通过
context_update 工具,Agent 可以追加内容、替换段落或全量重写文件。
- 语义分层清晰。SOUL 定义 Agent 的身份,USER 定义用户的特征,MEMORY 定义项目的上下文。三个维度正交,互不干扰。
- 支持 Frontmatter 扩展。YAML frontmatter 可用于存储触发器(triggers)等元数据,正文(body)存储核心内容。一个文件承载两层信息。
SOUL.md —— Agent 的人格
---
triggers:
- type: session_start
action: “Greet the user by name and ask about today’s task”
cooldown: 300
- type: task_failed
action: “Analyze the failure and suggest alternative approaches”
cooldown: 60
---
# Soul
**Name:** 小助手
**Identity:** A focused coding assistant for Android/C projects
**Tone:** 专业但友善,适度幽默
**Expertise:** C, Android NDK, JNI, Kotlin
**Language:** 中文优先,技术术语保持英文
## Goals
- [P10] 帮助用户高效完成编程任务
- [P8] 主动发现代码中的潜在问题
- [P5] 在适当时候传授设计模式和最佳实践
Frontmatter 中的 triggers 是人格系统最有趣的部分。它定义了 Agent 在特定事件发生时应采取的自主行为:
const char *context_check_triggers(context_store_t *store, trigger_type_t event_type) {
time_t now = time(NULL);
for (int i = 0; i < store->trigger_count; i++) {
soul_trigger_t *st = &store->triggers[i];
if (st->type != event_type) continue;
// 冷却时间内不重复触发
if (st->cooldown_sec > 0 && st->last_fired > 0) {
if (difftime(now, st->last_fired) < st->cooldown_sec)
continue;
}
st->last_fired = now;
return st->action_prompt;
}
return NULL;
}
触发器设有冷却时间(cooldown),防止 Agent 在每次会话开始时都说同一句开场白。触发器的状态(如上次触发时间)会持久化在 soul_state.json 中,确保跨应用重启后依然有效。
上下文注入:每轮动态重建
Agent 每次进行推理前,系统提示词都会被重新组装:
void context_build_prompt(context_store_t *store, strbuf_t *out) {
// SOUL 优先级最高 —— 定义 Agent 的基本行为
context_file_t *soul = context_find_file(store, “SOUL.md”);
if (soul && soul->body_len > 0) {
strbuf_append_str(out, “\n\n## Soul\n”);
strbuf_append_str(out, soul->body);
}
// USER 次之 —— 让 Agent 了解用户
context_file_t *user = context_find_file(store, “USER.md”);
if (user && user->body_len > 0) {
strbuf_append_str(out, “\n\n## User Profile\n”);
strbuf_append_str(out, user->body);
}
// MEMORY 最后 —— 项目级知识
context_file_t *mem = context_find_file(store, “MEMORY.md”);
if (mem && mem->body_len > 0) {
strbuf_append_str(out, “\n\n## Long-term Memory\n”);
strbuf_append_str(out, mem->body);
}
// 首次启动时注入引导
if (store->bootstrapping) {
// 注入 BOOTSTRAP.md,引导 Agent 自我配置
}
}
为何选择每轮动态重建,而不是仅在初始化时构建一次?
因为记忆是动态变化的。Agent 在对话过程中可能通过 context_update 工具修改了 SOUL.md 或 MEMORY.md,也可能通过 auto-learn 机制添加了新知识。每轮重建确保了 Agent 始终基于最新的上下文进行决策。
context_update 工具 —— Agent 自主编辑记忆
这是整个系统最关键的设计之一:Agent 不仅能被动地读取记忆,还能主动地写入和修改记忆。
char *tool_context_update(const char *arguments, void *userdata) {
// 参数:file, action, content, section(可选)
// file: “SOUL.md” | “USER.md” | “MEMORY.md”(白名单)
// action: “append” | “replace_section” | “replace_all”
// content: 要写入的内容
// section: 要替换的段落标题(如 “## Goals”)
}
工具支持三种操作:
| 操作 |
场景 |
append |
新增一条记忆到文件末尾。 |
replace_section |
替换某个 Markdown 段落(按 ## 标题匹配)。 |
replace_all |
全量替换文件内容(但会保留原有的 frontmatter)。 |
实施的安全措施包括:
- 文件名白名单 —— 只允许操作 SOUL.md、USER.md、MEMORY.md。
- 路径穿越检查 —— 拒绝包含
.. 或 / 的文件名,防止目录遍历攻击。
- Frontmatter 保护 —— 执行
replace_all 操作时,自动保留文件原有的 frontmatter。
replace_section 的实现涉及一个小型的 Markdown 段落解析器:
// 找到 “## Goals” 段落的起止位置
const char *sec_start = find_section(body, “## Goals”, &sec_end);
// 拼接新内容:前半段 + 新段落 + 后半段
strbuf_append(new_body, body, sec_start - body);
strbuf_append_str(new_body, “## Goals\n”);
strbuf_append_str(new_body, new_content);
strbuf_append_str(new_body, sec_end);
修改文件后,会立即调用 context_reload_file() 刷新内存中的缓存。这保证了在同一轮对话中,Agent 修改记忆后,下一轮推理能立刻生效。
首次启动引导(Bootstrap)
一个全新的 Agent 初始状态是一无所知的 —— 没有人格,没有记忆。如何解决?
if (!soul || soul->body_len == 0) {
store->bootstrapping = 1;
// 创建 BOOTSTRAP.md
}

Bootstrap 模板会引导 Agent 完成自我初始化:
# First Launch
This is your first launch and you have no personality set yet. Please:
1. Introduce yourself, ask what name they'd like to call you
2. Ask about their main use case (coding, writing, research, etc.)
3. Ask about their preferred communication style
4. Use the context_update tool to create SOUL.md based on answers
5. Use the context_update tool to create USER.md to record preferences
Once personality setup is complete, this bootstrap phase ends.
Agent 会主动与用户对话,了解需求,然后自己调用 context_update 工具来创建 SOUL.md 和 USER.md。一旦 SOUL.md 被成功创建,bootstrap 模式便会自动关闭。
这意味着 Agent 的人格是通过与用户的对话“生长”出来的,而非由开发者硬编码预设。这对于 C/C++ 这类需要精细控制资源的项目来说,是一种既灵活又高效的初始化方式。
Auto-Learn:LLM 驱动的自动记忆
经过深思熟虑,我认为这才是整个记忆系统的灵魂所在。
之前遇到的问题
依赖手动记忆(用户明确说“记住 XXX”)覆盖率极低。大量有价值的上下文信息散落在对话中,用户不可能一一叮嘱 Agent 去记住。
我们的解决方案
在每次 Agent 执行工具调用后,系统会发起一个轻量级 LLM 调用,用于分析本轮交互,并自动提取值得长期记忆的事实。
int context_auto_learn(context_store_t *store, void *config,
const char *user_input, const char *tool_summary) {
// 1. 构造提取 prompt
// 2. 用 fast_model 调用 LLM
// 3. 解析返回的 [USER] / [MEMORY] 前缀行
// 4. 去重检查
// 5. 追加到目标文件
}
其中,提取提示词(prompt)的设计是关键:
Analyze this agent interaction and extract ONLY genuinely useful long-term facts.
Focus on:
- User preferences (coding style, tool choices, language preferences)
- Project-specific facts (tech stack, file structure patterns)
- Workflow patterns (how user likes to work, review preferences)
Rules:
- Return ONLY new, non-obvious facts worth remembering across sessions
- Each fact on its own line, prefixed with target file:
[USER] for user preferences and habits
[MEMORY] for project facts and patterns
- Maximum 3 facts per interaction. Return NONE if nothing worth learning.
- Be concise: each fact should be one short sentence.
我们设定了几个关键约束:
- 每次最多提取 3 条 —— 防止记忆文件过快膨胀。
- 内容长度过滤(5-200 字符)—— 过短缺乏信息量,过长则可能不是单纯的“事实”。
- 明确的目标文件前缀 —— 以
[USER] 开头的行写入 USER.md,以 [MEMORY] 开头的行写入 MEMORY.md。
- 允许返回 NONE —— 如果本次交互没有值得记忆的内容,LLM 可以直接返回 NONE,避免生成垃圾信息。
- 使用轻量模型(fast_model) —— 自动学习是后台任务,使用轻量级模型可以节省主模型的 Token 消耗,提升效率。
触发时机
Auto-learn 并非在每轮对话中都触发,而是只在 Agent 执行了工具调用 时触发:
// agent.c 中的 ReAct 循环
if (had_tool_calls && agent->context && agent->config->auto_learn) {
context_auto_learn(agent->context, agent->config,
user_input, tool_summary.data);
}
为何选择这个时机?因为纯粹的对话可能包含大量闲聊,而工具调用(如读取文件、修改代码、搜索内容)往往意味着用户正在执行实际任务。这些操作中蕴含的上下文信息密度最高,也最值得被记忆。
最后防线:去重与写入
自动学习提取出的内容在写入前必须经过去重检查:
context_file_t *target = context_find_file(store, target_filename);
if (target && context_has_similar(target, text)) {
continue; // 跳过相似内容
}
// 追加到文件
FILE *f = fopen(fpath, “a”);
fprintf(f, “- %s\n”, text);
fclose(f);
// 立即刷新内存缓存
context_reload_file(store, target_filename);
写入文件后立即调用 reload,确保在下一轮 Agent 推理时,新学到的知识就能被包含在上下文中。
平滑的数据迁移
从 JSONL 切换到 Markdown 系统,必须保证已有数据不丢失。我们实现了自动迁移逻辑:
int context_init(context_store_t *store, const char *data_dir) {
// ...
// 迁移 memory.jsonl → MEMORY.md
migrate_memory_jsonl(store, local_mem_path);
migrate_memory_jsonl(store, global_mem_path);
// 迁移 config.json 中的人格设置 → SOUL.md
migrate_personality(store, config_path);
// ...
}
迁移逻辑是幂等的(如果目标 Markdown 文件已存在则跳过迁移),并且会保留原始文件(重命名为 .bak 后缀),以便随时回滚。
系统数据流全景
将所有这些组件串联起来,完整的系统数据流如下所示:
用户发送消息
↓
cclaude_send() → agent_turn()
↓
重建系统提示词
├── 基础 system_prompt
├── context_build_prompt()
│ ├── SOUL.md body → “## Soul\n...”
│ ├── USER.md body → “## User Profile\n...”
│ └── MEMORY.md body → “## Long-term Memory\n...”
└── workspace context → “## Working Directory\n...”
↓
LLM 推理(携带完整上下文)
↓
工具调用?
├── 是 → 执行工具 → 收集 tool_summary
│ ↓
│ auto_learn 触发
│ ↓
│ 轻量 LLM 分析交互
│ ↓
│ 提取 [USER] / [MEMORY] 事实
│ ↓
│ 去重 → 追加到目标文件 → reload
│ ↓
│ 下一轮推理自动包含新知识
│
└── 否 → 直接返回结果
此外,Agent 也可能在推理过程中主动调用 context_update 工具:
Agent 推理:“用户似乎喜欢简洁的代码风格,我应该记住这个”
↓
tool_call: context_update({
“file”: “USER.md”,
“action”: “append”,
“content”: “- Prefers concise, minimal code style”
})
↓
文件写入 → reload → 当前对话的后续回复立即生效
这种将记忆与 系统设计 深度集成的思路,是构建高效、个性化 Agent 的核心。
实践中的经验与总结
回顾这套记忆系统的演进历程:
第一版(JSONL) 解决了“能记住”的基本问题 —— 通过简单的键值对存储实现了跨会话的持久化。
第二版(Markdown 上下文文件系统) 则系统地解决了“记什么、怎么记、谁来记”的深层问题:
- SOUL.md 定义 Agent 的身份(人格与触发器)。
- USER.md 定义用户的特征(偏好与习惯)。
- MEMORY.md 定义项目的上下文(知识与模式)。
- Auto-Learn 使 Agent 能够自动从交互中提取有价值的知识。
- context_update 工具 赋予 Agent 主动编辑自身记忆的能力。
- Bootstrap 机制 让一个空白的 Agent 能够优雅地完成自我初始化。
整套系统完全使用纯 C 语言实现,除了用于解析 JSON 的 cJSON 库外,实现了零外部依赖。编译后生成的动态链接库(.so)文件大小仅在 200KB 左右,非常适合移动端或资源受限的本地环境部署。
AI Agent 的核心竞争力并非其基础的推理能力 —— 那是底层大语言模型(LLM)所赋予的。Agent 真正的核心竞争力在于上下文管理能力:在正确的时间,将正确的信息,注入到正确的位置。而本文所阐述的记忆系统,正是构建这种强大上下文管理能力的基石。希望这次在 云栈社区 分享的从零到一的实现与演进思考,能为各位开发者构建自己的智能体提供有益的参考。