法律文书分析系统接入 A-MEM 长程记忆
项目实训 | Vue 3 + FastAPI | A-Mem / Agentic Memory | DeepSeek | ChromaDB | SQLite
一、为什么要做长程记忆
在做法律文书分析系统时,我遇到的第一个明显问题是:聊天助手没有长期记忆。
用户可能在前几轮已经说过案件事实,例如购买时间、付款金额、故障过程、客服沟通记录、证据缺口等。但换一个对话,或者过几轮之后,模型本身并不会稳定记住这些内容。直接把历史对话全部塞进 prompt 虽然简单,但很快会遇到两个问题:
- 上下文越来越长,成本和延迟都会上升。
- 原始对话里有大量重复、追问和口语化表达,信息密度不高。
所以我想做的不是“保存聊天记录”,而是“从聊天中抽取有用知识,并在之后需要时召回”。对法律场景来说,这些知识通常包括案件事实、证据材料、时间线、用户偏好、诉求、法律依据和行动建议。
二、选型:A-Mem 给了一个合适的起点
我参考的是 A-Mem: Agentic Memory for LLM Agents。官方摘要里对它的定位是:一个能够动态组织记忆的 agentic memory system。它借鉴 Zettelkasten 的组织原则,把记忆做成带有上下文、关键词、标签和连接关系的 note 网络,并且在新记忆加入时触发连接和演化。
这里我需要更准确地说明:我做的不是完整复现实验论文,也不是证明 A-Mem 在法律任务上达到论文指标。我做的是把 A-Mem 的工程代码接入到自己的法律文书分析系统里,并围绕法律问答做了一些适配。
我保留和利用的核心思想有三点:
- 每条记忆不是一段原始聊天,而是一个结构化 note。
- 新 note 会和已有 note 建立语义连接。
- 记忆不是只增不改,新的内容可能影响旧记忆的上下文和标签。
三、系统接入方式
项目整体是前后端分离:
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,所以需要做一些适配。
主要问题有两个:
- 上游会使用
response_format=json_schema,但 DeepSeek 对这个参数的支持并不完全一致。 - 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
我没有只依赖向量库。当前设计里:
- A-Mem / ChromaDB 负责语义检索和 note 网络。
- SQLite 的
amem_notes表保存结构化备份。 - 启动或初始化时,可以从 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
),
})
这样模型更容易把召回内容视为已知事实,而不是普通聊天文本。
八、第一版效果和不足
接入后,系统确实能做到一些原来做不到的事:
- 用户跨轮提到的证据、诉求、偏好可以被保存。
- 新问题会召回历史 note,回答更连续。
- 前端可以把 note 和语义连接画成图谱,便于调试。
但第一版也有明显不足:
- 记忆越多,简单语义检索容易混入不该召回的内容。
- 所有记忆如果只放在一个大池子里,用户画像、法律规则、案件事实会互相干扰。
- 图谱只能展示“哪些 note 有连接”,不能解释“为什么这次检索走到了这条记忆”。
- 前端 demo 如果靠手写数据,容易和真实系统脱节。
这些问题后来促成了第二阶段优化:给记忆加层级、给检索加计划、给图谱加 HNSW 风格导航和真实检索路径。
九、总结
这次接入让我最大的感受是:论文里的 memory system 到工程落地之间,有很多论文不会展开的细节。
例如:
- LLM API 的兼容问题。
- JSON 输出不稳定。
- 向量库和项目原有检索系统的冲突。
- 原始聊天是否值得存。
- 记忆如何持久化、如何调试、如何展示。
所以我现在更愿意把这一阶段定义为:把 A-Mem 思路接入法律助手,并完成一版可运行的长程记忆链路。它不是终点,只是让系统从“没有记忆”走到了“可以保存和召回结构化知识”。
下一篇我会继续写:当记忆数量变多后,我如何把它从普通图谱改造成分层记忆和 HNSW 风格的可解释导航图。
参考
- A-Mem: Agentic Memory for LLM Agents, NeurIPS 2025 Main Conference Track
- OpenReview: A-Mem: Agentic Memory for LLM Agents
更多推荐
所有评论(0)