恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
分层设计的记忆系统:用 SQLite 给 Agent Skills 搭一套可复现的配置骨架
首页
资讯中心
/
分层设计的记忆系统:用 SQLite 给 Agent Skills 搭一套可复现的配置骨架
分层设计的记忆系统:用 SQLite 给 Agent Skills 搭一套可复现的配置骨架
发布时间:2026/9/29 20:25:00
1. 为什么 Agent Skills 需要一套分层记忆系统如果你正在做 Agent Skills 相关的开发大概率遇到过这几个场景同一个 Skill 被反复调用每次都要重新告诉它项目用 Python 还是 TypeScript上一轮会话里刚确认过的接口约定下一轮就忘得干干净净想给 Skill 加一条“记住用户偏好”的能力结果发现只能把全部历史对话塞进上下文token 烧得飞快还容易超限。这些问题的根源在于大多数 Agent 框架的记忆是“平铺”的所有信息不分轻重缓急地堆在一起。而人类处理信息的方式是分层的——短期工作记忆、长期经验、技能肌肉记忆各司其职。分层设计的记忆系统就是把这套逻辑搬到 Agent Skills 上高频、关键的信息放在最靠近推理层的位置低频、海量的历史数据沉到可检索的存储层可复用的解决路径固化成技能文件。这篇文章要落地的方案是用 SQLite 作为 L3 会话记忆的存储底座配合 L1 核心记忆文件、L2 用户画像文件、L4 技能目录搭出一套可复现、可调试的配置骨架。选 SQLite 的理由很直接单文件、零运维、自带 FTS5 全文索引、Python/Node 都有成熟驱动本地开发和服务器部署的体验完全一致。整套骨架的目标不是“功能最多”而是“每一步都能验证、每一层都能单独调试”。适合谁跟做正在给 Agent Skills 加持久记忆的开发者、被跨会话上下文丢失困扰的工程同学、想用最小依赖搭出可复现记忆层的独立开发者。下面从表结构开始一路写到 config.toml 骨架和验证请求。2. TaoToken 前置把模型接入这层先固定下来分层记忆系统本身不依赖特定模型但 Agent Skills 要跑起来总得有个稳定的模型入口。我的做法是把模型接入这层单独抽出来用 TaoToken 统一管理这样记忆层的调试不会被模型切换打断。TaoToken 在这里扮演的角色是“模型网关”你拿到一个 API Key就能在 Agent 的配置里统一指向它后续换模型、调参数、看用量都在一个地方完成。对记忆系统来说这意味着 L1/L2 的注入逻辑、L3 的检索触发、L4 的技能调用都可以在模型不变的前提下反复调试。具体操作路径先到官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建项目在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 生成一个 Key复制保存如果只是想先验证模型通不通可以直接用模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 发一条消息测试接入细节和参数说明看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基础地址是 https://taotoken.net/api这个地址不加 UTM 参数直接用于代码里的 base_url。拿到 Key 之后把它写进环境变量后面 config.toml 里只引用变量名不硬编码。注意Key 只存在本地环境变量或密钥管理工具里不要提交到 Git 仓库。config.toml 里写${TAOTOKEN_API_KEY}这种占位形式即可。如果你后续要做长期编码类 Agent 或需要跑大量 Skill 调用可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它在用量和成本上更适合持续性的开发场景。Claude Code 相关的接入参考 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。3. SQLite 表结构与 config.toml 骨架3.1 四层记忆的职责划分在写代码之前先把四层的边界定清楚不然后面表结构会越加越乱。层级存储位置容量约束加载方式典型内容L1 核心记忆MEMORY.md≤800 tokens会话启动时冻结为快照注入系统提示词当前任务关键上下文、错误堆栈、变量状态L2 用户画像USER.md≤500 tokens会话启动时注入技术栈偏好、沟通风格、常用命令L3 会话记忆SQLite FTS5无硬上限按需检索不主动加载全量历史对话、检索结果L4 技能系统skills/ 目录按需任务匹配时加载SKILL.md 定义的可复用解决路径这个划分的核心思想是越靠近推理层的信息越精炼、越稳定越远离推理层的信息越全量、越按需。L1 和 L2 是“每次都要带上的”所以必须小L3 是“需要时才查的”所以可以大L4 是“匹配到才用的”所以按目录组织。3.2 SQLite 表结构L3 会话记忆用 SQLite 存核心是两张表一张存原始消息一张存 FTS5 全文索引。FTS5 是 SQLite 内置的全文检索模块建好索引后查询是毫秒级的。-- 开启外键约束 PRAGMA foreign_keys ON; -- 会话表记录每个会话的元信息 CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, skill_name TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime(%s,now)), updated_at INTEGER NOT NULL DEFAULT (strftime(%s,now)), summary TEXT ); -- 消息表存全量对话 CREATE TABLE IF NOT EXISTS messages ( message_id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL CHECK (role IN (user,assistant,system,tool)), content TEXT NOT NULL, token_count INTEGER DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (strftime(%s,now)), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ); -- FTS5 虚拟表对 content 建全文索引 CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( content, contentmessages, content_rowidmessage_id, tokenizeunicode61 ); -- 触发器保持 FTS 索引与主表同步 CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(rowid, content) VALUES (new.message_id, new.content); END; CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES(delete, old.message_id, old.content); END; CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES(delete, old.message_id, old.content); INSERT INTO messages_fts(rowid, content) VALUES (new.message_id, new.content); END; -- 常用查询索引 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at); CREATE INDEX IF NOT EXISTS idx_sessions_skill ON sessions(skill_name);把这段 SQL 存成schema.sql用一条命令初始化sqlite3 memory.db schema.sql验证表建好了sqlite3 memory.db .tables # 预期输出messages messages_fts sessions3.3 config.toml 骨架配置文件的作用是把“记忆层参数”和“模型接入参数”分开这样调试记忆逻辑时不会误改模型配置。# config.toml —— Agent Skills 分层记忆配置骨架 [model] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model_name claude-sonnet-4-20250514 max_tokens 4096 temperature 0.3 [memory.l1_core] path ./memory/MEMORY.md max_tokens 800 freeze_on_session_start true inject_into_system_prompt true [memory.l2_profile] path ./memory/USER.md max_tokens 500 auto_update true update_trigger session_end [memory.l3_session] db_path ./memory/memory.db fts_table messages_fts search_limit 10 search_mode bm25 auto_load false search_tool_name session_search [memory.l4_skills] skills_dir ./skills skill_file SKILL.md auto_extract true min_reuse_count 2 [gateway] enabled false platforms []几个关键参数说明memory.l3_session.auto_load false是分层设计的核心开关。它保证 L3 不会在会话启动时被全量加载只有 Agent 主动调用session_search工具时才去查 SQLite。这一条如果设成 true分层就退化成平铺了。memory.l1_core.freeze_on_session_start true表示会话开始时把 MEMORY.md 内容冻结成快照。冻结的好处是即使会话中途 MEMORY.md 被其他进程修改当前会话看到的仍是启动时的那份避免上下文漂移。memory.l4_skills.auto_extract true控制是否自动把复杂任务的解决路径提炼成 SKILL.md。调试阶段建议先设 false手动写几个 Skill 跑通流程后再打开。3.4 目录结构配置里引用的路径对应到实际目录agent-skills/ ├── config.toml ├── schema.sql ├── memory/ │ ├── MEMORY.md │ ├── USER.md │ └── memory.db ├── skills/ │ ├── code-debug/ │ │ └── SKILL.md │ └── api-call/ │ └── SKILL.md └── src/ ├── memory.py └── agent.pyMEMORY.md 和 USER.md 初始内容可以很简单但格式要固定方便后续解析!-- MEMORY.md -- ## 当前任务 - 正在调试 Agent Skills 的记忆层 - 关键约束L3 不主动加载 ## 关键上下文 - 数据库路径./memory/memory.db - FTS 表名messages_fts!-- USER.md -- ## 技术栈偏好 - 主力语言Python - 数据库SQLite ## 沟通风格 - 简洁直接给命令4. 验证请求从写入到检索跑通一遍配置搭好之后必须用一条完整的链路验证写入消息 → FTS 索引同步 → 检索命中 → 注入上下文。下面用 Python 写一个最小验证脚本。4.1 写入与检索# src/memory.py import sqlite3 import os from pathlib import Path DB_PATH Path(./memory/memory.db) def get_conn(): conn sqlite3.connect(DB_PATH) conn.execute(PRAGMA foreign_keys ON) return conn def create_session(session_id: str, skill_name: str): with get_conn() as conn: conn.execute( INSERT OR IGNORE INTO sessions(session_id, skill_name) VALUES (?, ?), (session_id, skill_name), ) def add_message(session_id: str, role: str, content: str): with get_conn() as conn: conn.execute( INSERT INTO messages(session_id, role, content) VALUES (?, ?, ?), (session_id, role, content), ) def search_messages(query: str, limit: int 10): with get_conn() as conn: rows conn.execute( SELECT m.session_id, m.role, m.content, bm25(messages_fts) AS score FROM messages_fts JOIN messages m ON m.message_id messages_fts.rowid WHERE messages_fts MATCH ? ORDER BY score LIMIT ? , (query, limit), ).fetchall() return rows if __name__ __main__: create_session(sess-001, code-debug) add_message(sess-001, user, SQLite FTS5 的 bm25 排序怎么用) add_message(sess-001, assistant, 在 MATCH 查询里用 bm25(表名) 作为排序字段即可) add_message(sess-001, user, 那 unicode61 分词器支持中文吗) results search_messages(FTS5) for r in results: print(r)运行cd agent-skills python src/memory.py预期输出类似(sess-001, user, SQLite FTS5 的 bm25 排序怎么用, -1.2e-06) (sess-001, assistant, 在 MATCH 查询里用 bm25(表名) 作为排序字段即可, -1.1e-06)bm25 返回的是负值越小越相关排序时升序即可。这一步跑通说明 L3 的写入和检索链路是通的。4.2 验证 L1/L2 注入L1 和 L2 的验证更简单读文件、数 token、拼进系统提示词# src/agent.py import os import tiktoken from pathlib import Path def load_l1(path: str, max_tokens: int 800) - str: content Path(path).read_text(encodingutf-8) enc tiktoken.get_encoding(cl100k_base) tokens enc.encode(content) if len(tokens) max_tokens: content enc.decode(tokens[:max_tokens]) return content def load_l2(path: str, max_tokens: int 500) - str: return load_l1(path, max_tokens) def build_system_prompt(l1: str, l2: str) - str: return f你是一个带分层记忆的 Agent。 ## 核心记忆L1 {l1} ## 用户画像L2 {l2} 需要历史信息时调用 session_search 工具查询。 if __name__ __main__: l1 load_l1(./memory/MEMORY.md) l2 load_l2(./memory/USER.md) prompt build_system_prompt(l1, l2) print(prompt) print(---) print(fL1 tokens: {len(tiktoken.get_encoding(cl100k_base).encode(l1))}) print(fL2 tokens: {len(tiktoken.get_encoding(cl100k_base).encode(l2))})运行后检查 L1 是否在 800 token 以内、L2 是否在 500 以内。超了就截断这是硬约束不能妥协。4.3 用模型对话验证整体链路把上面两步串起来发一条真实请求。这里用 TaoToken 的模型对话入口做快速验证打开 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把build_system_prompt的输出贴进系统提示词然后问一句“我之前问过 FTS5 的什么问题”。如果模型能通过session_search检索到sess-001里的内容并回答出来说明四层记忆的读取链路是通的。这一步的意义在于它验证的不是单个函数而是“L1/L2 注入 L3 按需检索 模型调用”的完整闭环。任何一层出问题这里都会暴露。5. 本篇常见错排查5.1 FTS5 表建了但检索不到最常见的原因是触发器没建或者建了但顺序不对。FTS5 的外部内容表contentmessages不会自动同步必须靠触发器。检查方法sqlite3 memory.db SELECT name FROM sqlite_master WHERE typetrigger;如果输出为空说明触发器没建成功。重新执行 schema.sql 里的触发器部分。另一个可能是插入数据时用了INSERT OR REPLACE这会触发 DELETE INSERT如果 DELETE 触发器缺失FTS 索引就会残留旧数据。5.2 bm25 排序结果为空MATCH查询对语法敏感。如果查询词里有特殊字符比如-、:需要加引号SELECT * FROM messages_fts WHERE messages_fts MATCH FTS5;另外unicode61分词器对中文是按字切分的搜“记忆系统”会拆成“记”“忆”“系”“统”四个字分别匹配。如果要做中文词组检索得换trigram分词器或者在应用层做查询改写。5.3 L1 注入后模型仍然“失忆”先确认freeze_on_session_start是否真的生效。如果每次请求都重新读 MEMORY.md而文件在会话中途被改过模型看到的内容就会变。冻结的实现方式是在会话开始时把内容读进内存后续请求复用这份内存副本不再读文件。另一个坑是系统提示词的位置。有些模型对系统提示词里的长文本注意力会衰减L1 内容如果放在最前面后面又跟了大段工具定义模型可能“看不到”。把 L1 放在系统提示词的末尾紧挨着用户消息命中率会高很多。5.4 config.toml 路径解析错误TOML 里的相对路径是相对于“运行命令时的工作目录”不是相对于 config.toml 所在目录。如果你在agent-skills/下运行python src/agent.py那./memory/MEMORY.md解析为agent-skills/memory/MEMORY.md没问题。但如果你在项目根目录运行路径就错了。稳妥做法是在代码里统一用Path(__file__).parent.parent推导项目根目录再拼配置里的相对路径。5.5 API Key 读取失败api_key_env TAOTOKEN_API_KEY表示从环境变量读。检查echo $TAOTOKEN_API_KEY如果为空说明没导出。临时导出用export TAOTOKEN_API_KEY你的key持久化写进~/.bashrc或~/.zshrc。注意不要在 config.toml 里直接写 Key 明文也不要把含 Key 的配置文件提交到版本控制。6. 继续往下走把记忆层接进你的 Agent Skills到这里一套可复现的分层记忆骨架已经跑通了SQLite 表结构建好、FTS5 索引同步、config.toml 参数分离、L1/L2 注入验证、L3 检索验证、常见错排查。接下来你可以按自己的场景做三件事。第一把session_search注册成 Agent 的工具。在工具定义里描述清楚它的用途和参数模型才会在需要时主动调用。工具描述写“检索历史会话中的相关消息用于回忆之前讨论过的内容”比写“搜索数据库”有效得多。第二给 L4 技能目录加第一个 SKILL.md。格式参考 agentskills.io 的规范至少包含名称、描述、输入输出、执行步骤。先手动写一个跑通“任务匹配 → 加载 Skill → 执行”的流程再打开auto_extract。第三把模型接入这层固定到 TaoToken。API Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 管理接入参数看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 长期编码场景可以了解 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。这样记忆层的调试和模型层的切换互不干扰出问题时能快速定位是哪一层的事。分层记忆系统的价值不在于“存得多”而在于“该记的记牢该忘的忘掉该查的查得到”。SQLite 在这里的角色是那个“该查的查得到”的底座FTS5 让检索足够快触发器让索引足够稳。剩下的就是根据你的 Skill 场景去调 L1 的容量、L2 的更新时机、L4 的提炼阈值。这些参数没有标准答案跑起来、看日志、改配置比一次设计到位更实际。