法律文书分析系统接入 A-MEM 长程记忆
项目实训 | Vue3 + FastAPI | NeurIPS 2025 A-MEM 复现与工程落地
一、背景与动机
在法律文书智能分析系统的开发过程中,我们发现了一个核心痛点:AI助手没有"记忆"。
用户在第一轮对话里详细描述了案件事实——“我是原告张三,2024年3月15日购买了8999元的笔记本电脑,屏幕花屏,联系客服三次被推诿”——但切换到新对话后,AI 完全不记得这些信息,用户需要反复重述。
传统方案是把历史对话塞进 prompt 上下文,但这有两个致命问题:
- 上下文窗口有限:几十轮对话就爆 token 了
- 信息密度低:大量寒暄和重复内容浪费 token
我们需要的是一种长程记忆机制:自动从对话中提取关键知识,持久化存储,在需要时精准召回。
二、技术选型:A-MEM(NeurIPS 2025)
经过调研,我们选择了A-MEM(Agentic Memory for LLM Agents),这是 NeurIPS 2025 的一篇 Spotlight 论文,提出了模仿人类 Atkinson-Shiffrin 记忆模型的三步记忆算法:
2.1 核心算法流程
用户输入 │ ▼ ┌──────────────────────────────┐ │ Step 1: Note Construction │ LLM 抽取 keywords/context/tags │ (笔记构建) │ 构建结构化 MemoryNote 对象 └──────────────┬───────────────┘ │ ▼ ┌──────────────────────────────┐ │ Step 2: Link Generation │ ChromaDB embedding 检索 │ (关联生成) │ 召回 top-K 语义邻居,建立关联边 └──────────────┬───────────────┘ │ ▼ ┌──────────────────────────────┐ │ Step 3: Memory Evolution │ LLM 判断 should_evolve │ (记忆演化) │ strengthen(强化连接) │ │ update_neighbor(更新邻居标签) │ │ evo_cnt > threshold → consolidate └──────────────────────────────┘关键创新点:记忆不是静态存储,而是会演化。新记忆加入后,LLM 会评估其与已有记忆的关系,决定是否强化连接或更新邻居的元数据。这模拟了人脑的记忆巩固过程。
2.2 技术栈
| 组件 | 选型 | 说明 |
|---|---|---|
| LLM | DeepSeek-Chat | 知识抽取 + 演化决策 |
| Embedding | all-MiniLM-L6-v2 | 语义检索 (sentence-transformers) |
| 向量数据库 | ChromaDB | A-MEM 原生支持 |
| 持久化 | SQLite | 二次备份,重启可恢复 |
| 后端 | FastAPI | 异步 API |
| 前端 | Vue 3 + ECharts | 记忆图谱可视化 |
三、工程实现
3.1 架构设计
前端 (Vue3) 后端 (FastAPI) ┌─────────────┐ ┌──────────────────┐ │ AssistantView│ ──memory-chat──→ │ /api/assistant/ │ │ 聊天页面 │ │ memory-chat │ └─────────────┘ │ │ │ │ ▼ │ ┌─────────────┐ │ ┌──────────────┐ │ │ MMMemoryView │ ──graph────────→ │ │legal_memory.py│ │ │ 记忆图谱 │ │ │ A-MEM 集成层 │ │ └─────────────┘ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌───────────┐ │ │ │ A-MEM 上游 │ │ │ │ (NeurIPS) │ │ │ └───────────┘ │ └──────────────────┘3.2 DeepSeek 适配(Monkey-Patch)
A-MEM 上游代码写死了 OpenAI 的 API 格式,尤其是response_format=json_schema,但 DeepSeek 不支持这个参数。解决方案是monkey-patch:
defpatched_get_completion(self,prompt,response_format,temperature=0.7):# 把 json_schema 内嵌进 prompt,外层改用 json_objectifisinstance(fmt,dict)andfmt.get("type")=="json_schema":schema=fmt["json_schema"]["schema"]schema_hint="\n\n仅返回合法JSON,匹配schema:\n"+json.dumps(schema)fmt={"type":"json_object"}# 调用 DeepSeek APIr=self.client.chat.completions.create(model=self.model,messages=[...],response_format=fmt,)踩坑记录:DeepSeek 的json_object模式要求 system prompt 必须包含 “JSON” 关键词,否则会报错。
3.3 知识提取引擎(核心改进)
这是我们对 A-MEM 的主要工程改进。
原始 A-MEM 的add_note()直接存储输入文本。如果我们把用户的聊天消息原样存入,记忆库就会充满 “三次客服都说啥了” 这样的废话。
改进方案:在对话完成后,用 LLM 从用户问题 + AI回复中提取结构化知识点,每个知识点独立存入 A-MEM。
_KNOWLEDGE_EXTRACT_PROMPT="""从下面的对话中提取有价值的结构化知识点。 规则: 1. 只提取事实性知识,不要存对话本身 2. 每条知识点必须独立可理解 3. 关注:案件事实、法律依据、关键证据、时间线 返回JSON: {"knowledge": [{"content": "...", "category": "事实|证据|法条|诉求"}]}"""asyncdefextract_and_store_knowledge(user_msg,ai_resp):# 调用 DeepSeek 提取知识resp=awaithttpx.AsyncClient().post(f"{base_url}/chat/completions",json={"messages":[...],"response_format":{"type":"json_object"}},)items=resp.json()["choices"][0]["message"]["content"]# 每条知识独立存入 A-MEMforiteminitems["knowledge"]:ms.add_note(content=item["content"])效果对比:
| 存原始消息 | 知识提取后 | |
|---|---|---|
| 存入内容 | “三次客服都说啥了” | “客服第一次说等检测,第二次说人为损坏,第三次不接电话” |
| 检索质量 | 噪声多,相关性低 | 精准匹配,信息密度高 |
| Token 开销 | 存储冗余 | 精炼,节省 70%+ |
3.4 全局记忆设计
最初我们按session_id隔离记忆(每个对话框独立记忆),但用户反馈这不符合使用习惯——“我换个对话框,记忆就没了”。
改为全局记忆:所有对话共享同一个 ChromaDB collectionlegal_amem_global,SQLite 表amem_notes作为持久化备份。启动时从 SQLite 回放全部笔记到 ChromaDB。
GLOBAL_SESSION="__global__"defget_legal_memory():# 全局单例,跨会话共享ms=AgenticMemorySystem(collection_name="legal_amem_global")_replay_all_notes(ms)# 启动时从 SQLite 恢复returnms3.5 记忆图谱可视化
使用 ECharts 的graph类型渲染记忆图谱:
- 节点:每个 MemoryNote,按类别着色(事实/证据/法条/诉求)
- 边:语义关联(ChromaDB 检索建立)+ 演化关联(LLM 决策建立)
- 交互:点击节点展开详情(keywords、tags、evolution_history)
- 布局:力导向布局,repulsion=280, gravity=0.15
节点 label 只展示分类名,hover tooltip 显示摘要和标签,避免信息过载。
四、踩坑与解决
4.1 ChromaDB 实例冲突
问题:项目原有的文档向量检索和 A-MEM 各自创建 ChromaDB 客户端,导致An instance of Chroma already exists for ephemeral with different settings。
解决:将文档向量检索改为PersistentClient,A-MEM 保持 ephemeral 模式,两者不冲突。
4.2 Pydantic BaseSettings 优先级
问题:.env文件里配了正确的 API key,但后端一直报 “Invalid API key”。
原因:PydanticBaseSettings优先读取系统环境变量而非.env。Windows 系统上存在一个旧的OPENAI_API_KEY环境变量覆盖了.env的值。
解决:启动命令中显式设置环境变量,确保优先级最高。
4.3 PowerShell 中文编码
问题:PowerShell 执行含中文的python -c "..."命令时,字符被编码损坏导致 SyntaxError。
解决:将 Python 代码写入.py文件再执行,设置PYTHONIOENCODING=utf-8。
4.4 记忆注入方式
问题:最初将记忆上下文追加在用户消息末尾,AI 经常忽略这部分内容。
解决:改为以独立的system消息注入,明确告知 AI “这些是已知事实”:
messages.insert(1,{"role":"system","content":"以下是与用户案件相关的长程记忆知识点,视为已知信息:\n"+mem_lines})五、项目结构
backend/ ├── app/ │ ├── api/ │ │ ├── assistant.py # 聊天端点 (memory-chat + 知识提取) │ │ └── mmem.py # 记忆管理 API (/notes, /search, /graph) │ └── services/ │ └── mmem/ │ ├── legal_memory.py # A-MEM 集成(patch + 知识引擎) │ └── migration.py # SQLite schema frontend/ ├── src/ │ ├── views/ │ │ ├── AssistantView.vue # 聊天 (含长程记忆模式) │ │ └── MMMemoryView.vue # 记忆图谱页面 │ └── api/ │ └── mmem.ts # 记忆 API 调用六、总结
本次实训完成了 A-MEM 长程记忆系统从论文到工程的完整落地:
- 算法复现:完整保留了 A-MEM 的三步流程(Note Construction → Link Generation → Memory Evolution)
- 工程适配:Monkey-patch 解决 DeepSeek 兼容、ChromaDB 冲突、全局记忆等问题
- 创新改进:知识提取引擎,从对话中自动萃取结构化知识而非存储原始消息
- 可视化:ECharts 图谱直观展示记忆网络的节点和关联
核心收获:论文算法到工程落地之间有巨大的 gap——API 兼容、并发冲突、数据质量、用户体验——这些在论文里一笔带过的"implementation details",往往是实际开发中最耗时的部分。
