标签:#AI Agent #Agent Memory #记忆架构 #RAG #代码图谱
数据口径:综合自 2026 年年中公开报道、开源项目与官方文档
2026 年,“Agent 会失忆"成了做多智能体系统最头疼的事之一。对话一结束,上一轮的结论就没了;换个会话,Agent 又得重新问一遍。GitHub 热榜上 OpenViking(火山引擎"AI Agent 上下文数据库”,2026 年初开源、半年获 2 万+ star)、腾讯云 Agent-Memory(开源 80 天 star 破 1.5 万、多次登顶 GitHub Trending 日榜)、Mem0 持续霸榜。说白了,这不是个别团队的问题,是行业里谁都绕不开的坑。
上一篇《多智能体协作编排实战》里,我们用认知科学的视角把记忆分成了三层:工作记忆、情节记忆、语义记忆。这篇换个更工程化的角度——按"存什么"来分四层:会话记忆、Skill、知识库、代码图谱。前者回答"记忆在大脑里怎么分层",后者回答"在代码里每一层该存哪、怎么读写"。
读完你会得到:四层各自的技术选型、能直接跑的读写代码、一个统一检索入口的完整实现,以及我踩过的坑。上手后,你的 Agent 至少能做到"换了会话还记得上次聊到哪"。
适合读者:写过 Agent、开始觉得"记忆问题绕不开"的开发者;想把记忆从"一把梭向量库"升级成分层架构的工程师。


太长不看版

  • 四层记忆,各有各的存法:会话记忆存"聊天上下文"(Redis/滑动窗口)、Skill 存"怎么做事"(模板/少样本)、知识库存"业务事实"(向量库+RAG)、代码图谱存"代码语义"(AST+符号索引+向量)
  • 不是四层都要上:绝大多数业务场景,会话记忆 + 知识库两层就够;Skill 和代码图谱是 Agent 编程助手、复杂工作流这类场景才需要
  • 记忆要分层,检索要统一:写代码时最忌每层自己写一套查询,用同一个 MemoryService 入口按场景路由
  • 短期靠滑动窗口+摘要,长期靠向量+图谱:两条路不能互相替代,工程上要搭配使用
  • 热点项目值得关注:OpenViking(AI Agent 上下文数据库)、TencentDB-Agent-Memory(长上下文压缩)、Mem0(通用记忆层)、Letta/MemGPT(记忆型 Agent 框架)
  • 一句话选型:Redis 管会话、Chroma 管知识库、tree-sitter 管代码、JSON/向量管 Skill——四样东西拼起来,就是一套够用的 Agent Memory

目录


一、引言:为什么 2026 年都在谈 Agent 记忆

先看一个很常见的翻车现场。

你给团队搭了一个"销售周报 Agent"。第一天它表现得不错:记住了客户 A 的预算、上一轮谈到的折扣、下周要跟进的节点。第二天你开个新会话,问"上周说的那个折扣方案后来怎么定的",它一脸茫然:什么折扣?什么客户?

不是模型变笨了,是记忆没存下来。大模型本身是无状态的,聊完就忘。想让它"记得",要么每次把历史塞进上下文(贵、还有窗口上限),要么把信息落到记忆系统里(这就要分层设计了)。

2026 年,这问题有多热,看 GitHub 上的项目就懂了:

  • OpenViking(火山引擎,GitHub 2026 年初开源):主打"AI Agent 上下文数据库",核心思路是用文件系统范式(viking:// 协议 + L0/L1/L2 分层加载)统一管理记忆、资源与技能,而不是无脑堆 token,上线半年 GitHub 获 2 万+ star
  • TencentDB-Agent-Memory(腾讯云,2026-05 开源):主打长上下文压缩与记忆优化,官方实测数据:超长 Session 任务 Token 消耗最高节省 61%、通过率相对提升 52%;PersonaMem 评测集上总准确率从 47.85% 提升到 76.10%
  • Mem0:通用记忆层,把"新增-检索-更新"封装成一套 API
  • Letta / MemGPT:更激进,直接把记忆当操作系统内存来管,让 Agent 自己决定把什么写入"主存"、把什么换出

方向这么多,但落到代码上其实就一个问题:到底把哪些东西记下来、记在哪、怎么取回来。四层架构要解决的,就是这个问题。

如果你还没读过上一篇《多智能体协作编排实战:任务拆解+工具调用+记忆模块设计指南》,建议先扫一遍第三节的"三层记忆"——那里讲的是认知视角(Working/Episodic/Semantic),这篇的工程视角是它的延续,不会重复。


二、从三层到四层:把"认知分类"翻译成"工程组件"

上一篇的三层记忆分类来自认知科学:工作记忆、情节记忆、语义记忆。这个分类用来"理解问题"很好,但写代码时你会发现一个问题——分类口径和存储介质对不上

比如"情节记忆"(谁在什么时候做了什么),落到工程上既可能存进向量库(检索"上次聊过什么"),也可能存进 Redis(取"最近几轮");"语义记忆"更是能装下事实、流程、代码三种完全不同的东西。所以工程上更顺手的分法,是按**“存什么内容”**来切:

新四层(工程视角) 对应旧三层(认知视角) 存什么 典型介质
L1 会话记忆 Working Memory(工作记忆) 当前/近期对话上下文 Redis、滑动窗口、摘要
L2 Skill 记忆 Episodic 升华(经验沉淀) 怎么做事的流程/模板/少样本 JSON、向量库、提示词模板
L3 知识库记忆 Semantic Memory(语义记忆) 业务事实、文档、FAQ 向量库 + RAG
L4 代码图谱记忆 Semantic(代码语义) 代码结构、符号、调用关系 AST + 符号索引 + 向量

一句话概括四层的关系:

会话记忆管"短期" → Skill 管"怎么干" → 知识库管"事实" → 代码图谱管"代码"
     L1                 L2                L3                 L4
  (高频、易变)→(中频、可复用)→(低频、稳定)→(低频、高度结构化)

**为什么要把"代码图谱"单独拎出来当一层?**因为代码和自然语言是两种结构:函数签名、调用关系、类的继承,这些信息用自然语言向量检索(RAG)效果很差——你问"谁调用了 charge()",语义检索给你返回一段注释而不是调用点。代码图谱用 AST + 符号索引回答这类问题,向量检索只做兜底。OpenViking 的上下文优化、Cursor 的代码库索引,这些 2026 年热门的 AI 编程能力,底层靠的也是这套索引。

提醒:别一上来四层全上。先想清楚你的 Agent 要不要做代码理解——不做,就砍掉 L4;没有沉淀技能的需求,就砍掉 L2。分层是手段,不是 KPI。


三、L1 会话记忆:短期记忆的工程化

L1 管的是"最近聊了什么"。它要解决两个问题:放不下的问题(上下文窗口有限)和接不上的问题(跨会话找回)。

3.1 三种实现方式,按预算选

方式 做法 优点 缺点 适合场景
滑动窗口 只保留最近 N 轮,旧的全丢 实现简单、零成本 超出窗口的旧信息直接消失 简单客服、演示
摘要压缩 超窗口后,把前面的对话摘要成一段话再继续 兼顾成本与连续性 摘要会丢细节,压缩有损耗 大多数生产场景
Redis + 摘要 原始记录落 Redis 可查,上下文里只放摘要+最近轮次 可回溯、可审计 多一个存储组件 需要"翻旧账"的业务

3.2 滑动窗口 + 摘要的参考实现

# session_memory.py —— L1 会话记忆:Redis 存储 + 摘要压缩
import json
import redis

MAX_ROUNDS = 10      # 最近 N 轮完整保留
SUMMARY_AFTER = 20   # 超过 N 轮后触发摘要


class SessionMemory:
    """基于 Redis 的会话记忆:原始记录全量落库,上下文只放摘要+最近轮次"""

    def __init__(self, redis_url="redis://localhost:6379/0"):
        self.r = redis.Redis.from_url(redis_url, decode_responses=True)

    def _key(self, session_id: str) -> str:
        return f"session:{session_id}"

    def add(self, session_id: str, role: str, text: str):
        """追加一条消息到会话记录"""
        msg = {"role": role, "text": text}
        self.r.rpush(self._key(session_id), json.dumps(msg, ensure_ascii=False))

    def get_recent(self, session_id: str, k: int = MAX_ROUNDS) -> list[dict]:
        """取最近 k 条原始消息"""
        raw = self.r.lrange(self._key(session_id), -k * 2, -1)
        return [json.loads(m) for m in raw]

    def summarize(self, session_id: str, llm) -> str:
        """把窗口外的历史压成一段摘要(调用大模型)"""
        history = [json.loads(m) for m in self.r.lrange(self._key(session_id), 0, -1)]
        if len(history) <= SUMMARY_AFTER:
            return ""
        to_summarize = history[:-SUMMARY_AFTER]
        joined = "\n".join(f"{m['role']}: {m['text']}" for m in to_summarize)
        return llm(f"把以下对话压缩成 100 字以内的背景摘要,保留关键事实:\n{joined}")

关键点:摘要和原始记录是两条线。摘要进上下文(省钱、省窗口),原始记录留 Redis(可回溯)。很多团队只做摘要不落库,后面想"翻旧账"就抓瞎。

动手前先想清楚:你的"会话"边界是什么?按 session_id 隔离是最低要求;多用户场景还要在 key 里带 user_id,否则 A 用户会读到 B 用户的聊天记录(这是隐私事故,不是技术问题)。


四、L2 Skill 记忆:把"经验"沉淀成"技能"

L2 管的不是"聊过什么",而是"这活怎么干"。典型场景:Agent 第一次写周报,要现场发挥;同一个 Agent 用过一次之后,团队希望它"下次直接按上次的路子来"。

4.1 Skill 长什么样

一个 Skill 本质上是一份结构化模板,包含触发条件、执行步骤、示例输入输出:

{
  "skill_id": "weekly-report",
  "name": "销售周报生成",
  "trigger": "用户提到 周报/weekly report",
  "steps": [
    "1. 拉取本周销售数据(订单、回款、新客户)",
    "2. 对比上周,计算环比变化",
    "3. 输出 3 段式:本周进展 / 风险点 / 下周计划"
  ],
  "few_shot": "用户说'帮我写周报' → 输出符合团队模板的周报",
  "usage_count": 12,
  "tags": ["report", "sales"]
}

4.2 沉淀与检索

  • 写入:Agent 完成任务后,把"这次怎么干的"拆成 steps 存下来;也可以人工 review 后入库,保证质量
  • 检索:理想情况是按触发词(关键词)+ 语义(向量相似度)双路召回——下面代码先演示向量这一路,关键词过滤留给你补;召回后让 LLM 判断"这个 Skill 是否适用于当前任务"
  • 更新usage_count 计数,用得多的 Skill 优先级高;被 LLM 判断为不适用的 Skill 降权
# skill_memory.py —— L2 Skill 记忆:向量检索 + 关键词触发
import chromadb


class SkillMemory:
    """Skill 记忆:存的是'怎么做一件事'的模板"""

    def __init__(self, persist_dir="./skill_db"):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.col = self.client.get_or_create_collection(
            "skills", metadata={"hnsw:space": "cosine"}
        )

    def save_skill(self, skill: dict):
        skill_id = skill["skill_id"]
        # documents 里放"可检索的文本":触发词 + 步骤,向量化后用于语义召回
        self.col.upsert(
            ids=[skill_id],
            documents=[f"{skill['name']} {skill['trigger']} {' '.join(skill['steps'])}"],
            metadatas=[{"name": skill["name"], "usage_count": skill.get("usage_count", 0)}],
        )

    def retrieve(self, query: str, top_k: int = 3) -> list[dict]:
        """向量召回:按相似度取 top_k(生产环境可再加关键词前置过滤 + usage_count 加权)"""
        hits = self.col.query(query_texts=[query], n_results=top_k)
        return [
            {"skill_id": id_, "score": score}
            for id_, score in zip(hits["ids"][0], hits["distances"][0])
        ]

容易翻车的地方:Skill 被 LLM 当"提示词模板"用一次就完了,没人维护。Skill 得定期看 usage_count——长期不用的删掉,用得多但老翻车的,人工改改步骤。


五、L3 知识库记忆:业务事实的长期存储

L3 是四层里大家最熟的:文档、FAQ、业务数据,切块后进向量库,检索时做 RAG。直接复用上一篇讲过的向量库方案即可,这里只补两个经常被忽略的工程细节

5.1 基础实现(ChromaDB)

# knowledge_memory.py —— L3 知识库记忆:文档切块 + 向量检索
import chromadb


class KnowledgeMemory:
    """知识库记忆:存的是'业务事实',查询时按语义召回"""

    def __init__(self, persist_dir="./kb_db"):
        self.client = chromadb.PersistentClient(path=persist_dir)
        self.col = self.client.get_or_create_collection(
            "knowledge", metadata={"hnsw:space": "cosine"}
        )

    def add_document(self, doc_id: str, chunks: list[str], metadata: dict):
        """doc_id: 文档唯一标识;chunks: 已经切好的文本块"""
        self.col.upsert(
            ids=[f"{doc_id}-{i}" for i in range(len(chunks))],
            documents=chunks,
            metadatas=[{**metadata, "chunk_index": i} for i in range(len(chunks))],
        )

    def search(self, query: str, top_k: int = 5, where: dict | None = None):
        """where 参数支持按来源/部门过滤,比如 {"dept": "sales"}"""
        return self.col.query(
            query_texts=[query], n_results=top_k, where=where
        )

5.2 两个常被忽略的细节

细节一:切块别拍脑袋。 固定 512 字符切块在"规则文档"上还行,遇到表格、代码块、多级标题就碎得没法看。工程上建议:先按文档结构(标题/段落)切,再对超长块二次切割。没有万能 chunk 策略,用你自己的真实文档压测。

细节二:向量库不是唯一入口。 精确匹配(“编号 KX-2026-001 的合同条款是什么”)用向量召回很容易跑偏,应该先走结构化查询(数据库/词典),查不到再走向量。生产里的 RAG,基本都是关键词 + 向量 + 规则混着来,单一向量检索只能算入门。


六、L4 代码图谱记忆:让 Agent 真正懂你的代码库

L4 是四层里最"新"的一层,也是 Agent 编程助手(Cursor、Copilot、OpenClaw 这类)最卷的地方:Agent 得知道你的代码里有哪些函数、谁调谁、定义在哪

6.1 三层索引结构

索引 用什么建 回答的问题 查询方式
符号索引 tree-sitter 解析 AST,提取函数/类/变量 charge() 定义在哪” 精确匹配(SQLite)
调用关系图 从 AST 提取 caller/callee 边 “谁调用了 charge() 图遍历
代码语义索引 代码块向量化 “处理退款的逻辑在哪个文件” 向量相似度

6.2 用 tree-sitter 建符号索引

# code_graph.py —— L4 代码图谱:tree-sitter 提取符号 + SQLite 存索引
# 依赖:pip install tree-sitter tree-sitter-python
import sqlite3
import tree_sitter_python as tspython
from tree_sitter import Language, Parser

PY_LANGUAGE = Language(tspython.language())
parser = Parser(PY_LANGUAGE)

# 函数/类定义节点类型(Python:function_definition / class_definition)
DEF_TYPES = {"function_definition", "class_definition"}
NAME_FIELD = {"function_definition": "name", "class_definition": "name"}


def extract_symbols(source: str) -> list[dict]:
    """解析一份源码,返回其中的符号(名称、类型、行号)"""
    tree = parser.parse(source.encode("utf-8"))
    symbols = []

    def walk(node):
        if node.type in DEF_TYPES:
            name_node = node.child_by_field_name(NAME_FIELD[node.type])
            if name_node:
                symbols.append({
                    "name": name_node.text.decode("utf-8"),
                    "type": node.type,
                    "line": node.start_point[0] + 1,
                })
        for child in node.children:
            walk(child)

    walk(tree.root_node)
    return symbols


def extract_calls(source: str) -> list[tuple[str, int]]:
    """解析一份源码,返回其中的函数调用点:[(被调用名, 行号)]"""
    tree = parser.parse(source.encode("utf-8"))
    calls = []

    def walk(node):
        if node.type == "call":
            fn = node.child_by_field_name("function")
            if fn:
                # a.b() 取最后的 b 作为被调用名(按名字匹配的粗略近似,不做类型解析)
                name = fn.text.decode("utf-8").split(".")[-1]
                calls.append((name, node.start_point[0] + 1))
        for child in node.children:
            walk(child)

    walk(tree.root_node)
    return calls


class CodeGraphMemory:
    """代码图谱记忆:符号索引 + 调用关系落 SQLite,语义索引用向量库"""

    def __init__(self, db_path="./code_index.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS symbols(
                name TEXT, type TEXT, file TEXT, line INT,
                PRIMARY KEY(name, file)
            )
        """)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS calls(
                callee TEXT, file TEXT, line INT
            )
        """)

    def index_file(self, file_path: str, source: str):
        # 1) 符号表:函数/类定义(INSERT OR REPLACE,按文件重复索引不产生脏数据)
        symbols = extract_symbols(source)
        rows = [(s["name"], s["type"], file_path, s["line"]) for s in symbols]
        self.conn.executemany(
            "INSERT OR REPLACE INTO symbols VALUES (?,?,?,?)", rows
        )
        # 2) 调用表:先清掉该文件的旧记录,再写入新提取的调用点
        self.conn.execute("DELETE FROM calls WHERE file = ?", (file_path,))
        calls = extract_calls(source)
        self.conn.executemany(
            "INSERT INTO calls VALUES (?,?,?)",
            [(name, file_path, line) for name, line in calls],
        )
        self.conn.commit()

    def find_symbol(self, name: str) -> list[tuple]:
        """精确回答:这个函数/类在哪定义"""
        return self.conn.execute(
            "SELECT name, type, file, line FROM symbols WHERE name = ?", (name,)
        ).fetchall()

    def find_callers(self, callee: str) -> list[tuple]:
        """回答:哪些地方调用了这个函数(按名字匹配,未做类型解析,同名会一并返回)"""
        return self.conn.execute(
            "SELECT file, line FROM calls WHERE callee = ?", (callee,)
        ).fetchall()

说明:tree-sitter 的精确 API 随版本(0.21 → 0.23+)有差异,Parser 的构造方式、Query 的写法可能不一样,以你装的版本为准。上面这段我刻意用了最基础的节点遍历,不依赖 Query 语法,迁移成本最低。

为什么代码图谱不能只用向量? 向量检索擅长"语义相近",但"谁调用了 charge()“这种问题是结构查询,语义相似度给不了确定答案。AST 符号表给的是确定性结果,向量只负责兜底(“这段逻辑大概在哪”)。这俩配起来,才勉强算得上"代码图谱记忆”。


七、统一检索层:四层记忆怎么配合

四层各自建好之后,最容易犯的错是各写各的查询逻辑——对话里一会儿直接查 Redis、一会儿直接查 Chroma,代码越来越散。正确做法是收口到一个 MemoryService,按"记忆类型"路由。

# memory_service.py —— 统一检索入口:按记忆类型路由到四层
from session_memory import SessionMemory
from skill_memory import SkillMemory
from knowledge_memory import KnowledgeMemory
from code_graph import CodeGraphMemory


class MemoryService:
    """统一记忆入口:write 负责分层写入,query 负责按场景路由检索"""

    def __init__(self):
        self.session = SessionMemory()          # L1 会话记忆
        self.skills = SkillMemory()             # L2 Skill 记忆
        self.kb = KnowledgeMemory()             # L3 知识库记忆
        self.code = CodeGraphMemory()           # L4 代码图谱记忆

    # ── 写入侧:按内容性质分到对应层 ──
    def write(self, memory_type: str, **kwargs):
        if memory_type == "session":
            self.session.add(kwargs["session_id"], kwargs["role"], kwargs["text"])
        elif memory_type == "skill":
            self.skills.save_skill(kwargs["skill"])
        elif memory_type == "knowledge":
            self.kb.add_document(kwargs["doc_id"], kwargs["chunks"], kwargs.get("metadata", {}))
        elif memory_type == "code":
            self.code.index_file(kwargs["file_path"], kwargs["source"])

    # ── 检索侧:组装一次"记忆增强"的上下文 ──
    def build_context(self, user_input: str, session_id: str,
                      with_code: bool = False) -> str:
        parts = []

        # 1) 会话记忆:最近几轮(低成本,先进上下文)
        recent = self.session.get_recent(session_id)
        if recent:
            parts.append("【最近对话】\n" + "\n".join(
                f"{m['role']}: {m['text']}" for m in recent
            ))

        # 2) Skill 记忆:命中技能模板就给 LLM 参考
        skills = self.skills.retrieve(user_input)
        if skills:
            parts.append("【可用技能】\n" + "、".join(s["skill_id"] for s in skills))

        # 3) 知识库记忆:语义召回业务事实
        kb_hits = self.kb.search(user_input, top_k=3)
        if kb_hits["documents"][0]:
            parts.append("【业务知识】\n" + "\n".join(kb_hits["documents"][0]))

        # 4) 代码图谱记忆:按需启用(成本高,默认关)
        if with_code:
            code_hits = self.code.find_symbol(user_input)
            if code_hits:
                parts.append("【代码位置】\n" + "\n".join(
                    f"{f}{ln}行:{n}" for n, _, f, ln in code_hits
                ))

        return "\n\n".join(parts)

调用方式就很简单了——Agent 每次回复前,先 build_context() 把四层记忆拼进 prompt:

context = memory_service.build_context(
    user_input="上周那个折扣方案后来怎么定的?",
    session_id="user-42",
    with_code=False,   # 编程场景开 True
)
# → 把 context 拼进 system prompt / 检索结果

写入侧的节奏,也建议统一在这里定:会话记忆实时写;知识库、代码图谱在任务完成后异步批量写(别拖慢主链路);Skill 等 Agent 完成一次复杂任务、人工确认后再入库。

架构上再补一句:写这层服务时,把"记忆层"和"LLM 调用层"解耦。记忆层只负责存取,不负责判断"要不要记、怎么记"——判断逻辑放编排层(LangGraph 节点 / Agent 主循环)里。这样记忆层可以独立测试、独立换存储,不会被 prompt 逻辑拖累。


八、避坑指南

表现 根因 对策
四层一锅炖进一个向量库 检索结果乱七八糟,业务事实和代码混在一起 没按内容类型隔离 每层独立 collection/表,检索时按类型路由
只做摘要不落库 想"翻旧账"时找不到原始记录 摘要丢了细节 原始记录留 Redis/S3,摘要只进上下文
代码图谱用纯向量检索 "谁调用了 X"答不准 结构问题被当语义问题 符号索引答结构题,向量只兜底
Skill 只存不维护 旧技能带偏新任务 没有质量反馈闭环 统计 usage_count,定期清理降权
记忆层耦合 LLM 逻辑 换个 prompt 要改存储代码 分层不彻底 记忆层只做存取,判断逻辑放编排层
所有会话共用记忆 A 用户读到 B 用户的记录 缺少隔离维度 key 里带 user_id + session_id,检索加过滤
一次写入全量历史 上下文爆掉、成本飞涨 没有压缩策略 滑动窗口 + 摘要 + 按需检索三管齐下
四层全上求"先进" 维护成本翻倍,收益趋近于零 过度设计 先两层(会话+知识库),验证有需求再补

九、总结

  1. 四层记忆不是新理论,是把"记什么"分清楚了:会话记忆(最近聊的)、Skill(怎么干的)、知识库(业务事实)、代码图谱(代码结构)——各管一段,互不抢活
  2. 短期靠滑动窗口+摘要,长期靠向量+图谱:两条技术路线解决的是两类问题,缺一不可
  3. 分层写、统一查:写入按内容性质分层落库,检索统一走 MemoryService 路由,代码才不会散成一地
  4. 先砍到两层再扩:大多数业务场景会话记忆 + 知识库就够;Skill 和代码图谱按需加,别为了"完整"硬上
  5. 2026 年的风向:OpenViking(AI Agent 上下文数据库)、TencentDB-Agent-Memory(上下文压缩)、Mem0、Letta 都在做"记忆基础设施",说明这层越来越值得投入——但基础设施是别人的,分层架构得自己搭,这篇文章讲的就是后者

参考资料

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐