项目实训 | Vue 3 + FastAPI | A-Mem / Agentic Memory | DeepSeek | ChromaDB | SQLite

一、为什么要做长程记忆

在做法律文书分析系统时,我遇到的第一个明显问题是:聊天助手没有长期记忆。

用户可能在前几轮已经说过案件事实,例如购买时间、付款金额、故障过程、客服沟通记录、证据缺口等。但换一个对话,或者过几轮之后,模型本身并不会稳定记住这些内容。直接把历史对话全部塞进 prompt 虽然简单,但很快会遇到两个问题:

  1. 上下文越来越长,成本和延迟都会上升。
  2. 原始对话里有大量重复、追问和口语化表达,信息密度不高。

所以我想做的不是“保存聊天记录”,而是“从聊天中抽取有用知识,并在之后需要时召回”。对法律场景来说,这些知识通常包括案件事实、证据材料、时间线、用户偏好、诉求、法律依据和行动建议。

二、选型:A-Mem 给了一个合适的起点

我参考的是 A-Mem: Agentic Memory for LLM Agents。官方摘要里对它的定位是:一个能够动态组织记忆的 agentic memory system。它借鉴 Zettelkasten 的组织原则,把记忆做成带有上下文、关键词、标签和连接关系的 note 网络,并且在新记忆加入时触发连接和演化。

这里我需要更准确地说明:我做的不是完整复现实验论文,也不是证明 A-Mem 在法律任务上达到论文指标。我做的是把 A-Mem 的工程代码接入到自己的法律文书分析系统里,并围绕法律问答做了一些适配。

我保留和利用的核心思想有三点:

  1. 每条记忆不是一段原始聊天,而是一个结构化 note。
  2. 新 note 会和已有 note 建立语义连接。
  3. 记忆不是只增不改,新的内容可能影响旧记忆的上下文和标签。

三、系统接入方式

项目整体是前后端分离:

Vue 3 前端
  ├─ AssistantView:用户和 AI 对话
  └─ MMMemoryView:查看长期记忆图谱

FastAPI 后端
  ├─ /api/assistant/memory-chat:带长程记忆的聊天接口
  ├─ /api/mmem/graph:记忆图谱接口
  ├─ legal_memory.py:A-Mem 接入、DeepSeek 适配、知识抽取
  └─ migration.py:SQLite 表结构维护

聊天主流程大致是:

用户问题
  ↓
分层记忆检索
  ↓
把召回的 note 作为 system context 注入
  ↓
DeepSeek 流式回答
  ↓
对话结束后后台抽取结构化知识
  ↓
把知识点写入 A-Mem 和 SQLite

注意最后一步是后台异步做的。用户不需要等知识抽取完成才能看到回答,这样不会明显拖慢聊天体验。

四、DeepSeek 兼容问题

A-Mem 上游默认按 OpenAI 的接口方式调用模型。我这里使用的是 DeepSeek 的 OpenAI-compatible API,所以需要做一些适配。

主要问题有两个:

  1. 上游会使用 response_format=json_schema,但 DeepSeek 对这个参数的支持并不完全一致。
  2. DeepSeek 的 JSON 模式要求提示词中明确出现 JSON 相关约束。

我的处理方式是 monkey patch 上游的 LLM controller:

def patched_get_completion(self, prompt, response_format, temperature=0.7):
    schema_hint = ""
    fmt = response_format
    if isinstance(fmt, dict) and fmt.get("type") == "json_schema":
        schema = fmt.get("json_schema", {}).get("schema", {})
        schema_hint = (
            "\n\n仅返回一个合法 JSON 对象,匹配下面 schema (不要 markdown 代码块,不要任何解释):\n"
            + json.dumps(schema, ensure_ascii=False)
        )
        fmt = {"type": "json_object"}
    try:
        r = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": "You must respond with a JSON object."},
                {"role": "user", "content": prompt + schema_hint},
            ],
            response_format=fmt,
            temperature=temperature,
            max_tokens=1000,
        )
        return r.choices[0].message.content
    except Exception:
        # response_format 不被支持时降级为普通请求
        r = self.client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": "You must respond with a JSON object."},
                {"role": "user", "content": prompt + schema_hint},
            ],
            temperature=temperature,
            max_tokens=1000,
        )
        return r.choices[0].message.content

核心思路:把 json_schema 的约束塞进 prompt,外层改用 DeepSeek 支持的 json_object 模式。如果连 json_object 也失败(某些模型版本),再降级为不带 response_format 的普通请求。

上游还有一个问题:写死了 collection_name='memories'__init__ 里调 client.reset() 会清空所有数据。我用 monkey-patch 把 collection 名改成 legal_amem_<session_id> 来隔离:

def _patch_chroma_collection_per_session():
    original_init = AgenticMemorySystem.__init__
    def patched_init(self_inner, *args, **kwargs):
        original_init(self_inner, *args, **kwargs)
        # 用独立 collection 防止跨会话污染
        sid = kwargs.get("session_id") or GLOBAL_SESSION
        coll_name = f"legal_amem_{sid}"[:63]
        self_inner.retriever.collection = self_inner.retriever.client.get_or_create_collection(
            name=coll_name,
            metadata={"hnsw:space": "cosine"},
        )
    AgenticMemorySystem.__init__ = patched_init

这些都不优雅,但在实训项目里足够直接:不改上游源码,把兼容层放在本项目服务里。

五、为什么不直接存原始聊天

如果把用户每一句话原样存进记忆库,会出现很多低价值内容。例如:

用户:那我现在该咋办?
用户:他说要检测,这是不是拖时间?
用户:你再帮我总结一下。

这些句子单独看没有足够语义,后面检索时容易制造噪声。法律场景更需要的是可独立理解的知识点,例如:

笔记本电脑在购买后约两周出现大面积花屏。
用户已保存电子发票、付款截图、花屏照片和三次客服录音。
商家以可能存在人为损坏为由拒绝直接退换。
该用户偏好先给结论,再列证据清单,最后给协商、投诉、起诉路径。

所以我加了一层知识抽取。每轮对话结束后,用 LLM 从“用户问题 + AI 回复”里提取结构化知识点,再把这些知识点作为独立 note 写入 A-Mem。

实际使用的提示词和调用代码:

_KNOWLEDGE_EXTRACT_PROMPT = """你是一个知识提取引擎。从下面的对话交互中,提取出有价值的结构化知识点。

【用户消息】{user_msg}
【AI回复】{ai_resp}

提取规则:
1. 只提取事实性知识,不存"用户问了什么/AI回答了什么"
2. 每条知识点必须独立可理解
3. 重点关注: 用户偏好/案件事实/证据材料/时间线/金额/法律依据/诉求
4. 每条附 category: 事实|证据|法条|诉求|偏好|行动建议

返回 JSON: {{"knowledge": [{{"content": "...", "category": "..."}}]}}"""


async def extract_and_store_knowledge(user_msg, ai_resp, session_id=None):
    if len(user_msg.strip()) < 8 and len(ai_resp.strip()) < 30:
        return []
    prompt = _KNOWLEDGE_EXTRACT_PROMPT.format(user_msg=user_msg[:500], ai_resp=ai_resp[:1500])
    resp = client.chat.completions.create(
        model=settings.openai_model,
        messages=[{"role": "system", "content": "You must respond with a JSON object."},
                  {"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )
    items = json.loads(resp.choices[0].message.content).get("knowledge", [])
    ms = get_legal_memory()
    for item in items[:5]:
        nid = ms.add_note(content=item["content"])
        _persist_note_to_sqlite(nid, item, session_id)

这一步不是 A-Mem 原论文的实验结论,而是我针对法律问答做的工程补充。它的好处是记忆库更干净,坏处是依赖 LLM 抽取质量,偶尔会漏掉细节或抽象过度。

六、存储设计:A-Mem + SQLite

我没有只依赖向量库。当前设计里:

  1. A-Mem / ChromaDB 负责语义检索和 note 网络。
  2. SQLite 的 amem_notes 表保存结构化备份。
  3. 启动或初始化时,可以从 SQLite 回放 note,恢复到检索侧。

SQLite 里保存的字段包括:

note_id
session_id
memory_scope
owner_id
subject_id
source_type
source_id
importance
confidence
content
keywords_json
tags_json
context
category
links_json
evolution_history_json

这比“只存向量”更适合调试。出现检索异常时,我可以直接查某条 note 是怎么来的、属于哪个 session、来源是什么、是否被打上了正确标签。

七、记忆注入方式

第一版里我曾尝试把记忆拼在用户消息末尾,但效果不稳定。模型有时会把它当作附加说明,有时会忽略。

后来改成 system 消息注入:

messages.insert(1, {
    "role": "system",
    "content": (
        "以下是分层检索 agent 召回的长程记忆,请优先结合当前案件层,"
        "其次参考用户画像、跨案经验和通用法律层。\n\n"
        + mem_lines
    ),
})

这样模型更容易把召回内容视为已知事实,而不是普通聊天文本。

八、第一版效果和不足

接入后,系统确实能做到一些原来做不到的事:

  1. 用户跨轮提到的证据、诉求、偏好可以被保存。
  2. 新问题会召回历史 note,回答更连续。
  3. 前端可以把 note 和语义连接画成图谱,便于调试。

但第一版也有明显不足:

  1. 记忆越多,简单语义检索容易混入不该召回的内容。
  2. 所有记忆如果只放在一个大池子里,用户画像、法律规则、案件事实会互相干扰。
  3. 图谱只能展示“哪些 note 有连接”,不能解释“为什么这次检索走到了这条记忆”。
  4. 前端 demo 如果靠手写数据,容易和真实系统脱节。

这些问题后来促成了第二阶段优化:给记忆加层级、给检索加计划、给图谱加 HNSW 风格导航和真实检索路径。

九、总结

这次接入让我最大的感受是:论文里的 memory system 到工程落地之间,有很多论文不会展开的细节。

例如:

  1. LLM API 的兼容问题。
  2. JSON 输出不稳定。
  3. 向量库和项目原有检索系统的冲突。
  4. 原始聊天是否值得存。
  5. 记忆如何持久化、如何调试、如何展示。

所以我现在更愿意把这一阶段定义为:把 A-Mem 思路接入法律助手,并完成一版可运行的长程记忆链路。它不是终点,只是让系统从“没有记忆”走到了“可以保存和召回结构化知识”。

下一篇我会继续写:当记忆数量变多后,我如何把它从普通图谱改造成分层记忆和 HNSW 风格的可解释导航图。

参考

  • A-Mem: Agentic Memory for LLM Agents, NeurIPS 2025 Main Conference Track
  • OpenReview: A-Mem: Agentic Memory for LLM Agents
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐