在企业级AI大模型项目中,系统上线后最先暴露的问题往往不是模型回答得不够聪明,而是模型突然拒绝继续回答。日志里最常见的是context overflow: this conversation is too large for the model,或者是类似this model's maximum context length is 1048576 tokens的 400 错误。这个矛盾的根源在于:Agent 每一次推理只能看到 Context 窗口内的内容,而模型上下文窗口再大也有上限。想让 Agent 真正记住用户、记住项目背景、记住跨会话的关键决定,不能只靠把历史消息全部塞进提示词,必须把长记忆、文本自动摘要、MCP 工具接入和 Context 上下文工程当成一整条链路来设计。本文以语音智能体为入口,围绕 DeepAgent + MCP 这条主线,说明企业级 Agent 长记忆的原理、实现、验证与排错。
1. 长记忆先要回答一个问题:Agent 到底能“看到”什么
1.1 Context 窗口是一次推理的可见边界
通俗地说,Context 窗口就是大模型一次处理文字输入的范围。所谓长记忆,本质上是让 Agent 在跨会话、跨任务时仍然能拿到与当前问题相关的信息,并把它们放入这一次推理的 Context 窗口里。
技术定义上,Context 窗口是模型一次可接受的最大 token 数量。token 是模型处理文本的最小单位,中文场景下一个字可能对应 1 到 2 个甚至更多 token,英文则通常一个单词对应 1 到 3 个 token。不同模型的上下文窗口从几万 token 到百万 token 不等,但再大也有上限。
这里要先纠正一个常见误区:很多人以为“窗口越大,长记忆问题就自动解决了”。窗口变大确实能容纳更多历史,但它同时带来三个代价:
- 每次请求的 token 成本上升。
- 推理延迟变长。
- 当长文本里的无关信息过多,模型对关键信息的注意力会被稀释,回答质量反而下降。
所以在设计长记忆系统时,不能把“把历史全塞进去”当默认方案。先用一个简单的估算函数建立 token 直觉:
def estimate_tokens(text: str) -> int: # 仅用于工程估算,生产环境应使用模型对应的 tokenizer cjk_chars = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff') other_chars = len(text) - cjk_chars return int(cjk_chars * 1.6 + other_chars / 4)这个函数对中文为主的文本偏保守,对英文偏宽松。它解决的不是精确计数问题,而是在写代码时随时判断“当前拼出来的 Context 是否已经逼近预算”。真正精准的统计,应该使用 tiktoken 这类与模型配套的 tokenizer,或者直接读取模型服务返回的 usage 字段。
1.2 context overflow 不是偶发错误,而是放大后的必然结果
在实际开发中,凡是长会话、工具调用密集、代码内容返回较多的 Agent 项目,几乎都会遇到上下文超限问题。下面是几类典型报错:
| 报错或日志信息 | 出现场景 | 根因 |
|---|---|---|
| this model's maximum context length is 1048576 tokens | 单次请求输入和输出总长度超过模型上限 | 输入 prompt 过大或 max_tokens 设置过大 |
| context overflow: this conversation is too large for the model | 多轮对话或工具调用历史持续累积 | 历史记录未压缩、未裁剪 |
| Codex ran out of room in the model's context window | 编码 Agent 在一次任务里读取了过多代码文件 | 工具返回的代码内容没有按需截断 |
| context overflow and auto-compaction is disabled | 本地或私有化部署模型不支持自动压缩 | 未配置摘要策略或自动压缩被关闭 |
这些报错有一个共同原因:把全量对话历史 + 全量工具返回 + 检索结果直接拼进下一次请求。对话轮次一旦增多,工具返回的 JSON 一旦变大,Context 占用就会快速增长。更隐蔽的情况是,Context 没有超限,但已经接近上限,模型虽然返回结果,却把真正关键的信息“忘”了。
把这个过程反过来看,就得到长记忆系统必须处理的三个问题:
- 哪些历史必须保留原文?通常是最近几轮对话。
- 哪些历史可以压缩保存?通常是较早但仍有价值的会话。
- 哪些历史可以外部化存储,按需检索?通常是跨会话的偏好、事实、决策。
1.3 三层记忆模型:工作记忆、长期记忆、摘要记忆
我在企业级项目里落地记忆系统时,习惯把记忆拆成三层。这个结构不是为了概念好看,而是为了控制 Context 成本:
| 记忆层 | 存储载体 | 保留内容 | 进入 Context 的方式 |
|---|---|---|---|
| 工作记忆 | 当前会话的消息数组 | 最近若干轮用户输入和助手输出 | 原文截取,优先级最高 |
| 摘要记忆 | 文本文件、数据库或对象存储 | 历史会话压缩后的结构化摘要 | 全文带入,控制长度 |
| 长期记忆 | 向量库或关系数据库 | 跨会话的用户偏好、事实、决策 | 检索后带入,按相关性过滤 |
工作记忆解决连续性,摘要记忆解决历史压缩,长期记忆解决跨会话回溯。三层配合起来,才能在一次请求里兼顾“知道用户刚才说了什么”和“记得用户三个月前定的规则”。
注意:不要把模型上下文窗口当成硬性容量来用。接近上限时虽然不报错,但回答仍会退化,应该主动设置 Context 预算,并给模型输出预留空间。
2. DeepAgent 与 MCP:长记忆链路的两块关键拼图
2.1 DeepAgent 在长记忆链路中扮演什么角色
这里把 DeepAgent 作为一个参考架构名来理解。它可以是你正在使用的智能体框架,也可以是自研的 Agent 调度服务。在企业级长记忆链路中,它至少要做四件事:
- 接收上游输入。这里的上游可以是语音识别模块,也可以直接是 Web 页面或接口。
- 调用记忆读写能力。把用户说过的话、助手做过的决定写入存储,在当前问题到来时检索相关记忆。
- 组装 Context。把摘要记忆、长期记忆、最近对话、工具返回结果按预算组装成一个完整的 prompt。
- 调用大模型生成回复,并把回复结果交给下游模块处理,例如 TTS 语音合成。
理解这层职责很重要。很多项目把 Agent 的“记忆”简单做成一个列表,然后把列表拼进 prompt,这只能算临时调试。企业级架构里,记忆应被建模为独立组件,DeepAgent 只负责编排,不负责持久化细节。
DeepAgent 还负责一个容易忽略的工作:判断什么时候写入记忆,什么时候不写。如果每一轮对话都写入长期记忆,检索噪声会迅速上升。合理做法是,只把用户明确表达的偏好、关键决定、未完成事项、重要数据写入长期记忆,普通寒暄和临时问答可以只保留在会话摘要里。
2.2 MCP:把记忆存储变成模型可调用的标准工具
MCP 是 Model Context Protocol 的缩写,中文通常叫模型上下文协议。它不负责生成文本,而是为 AI 大模型提供标准化的外部工具和上下文来源。Agent 在规划阶段列出可用工具,在执行阶段调用工具,工具执行结果再作为上下文回到模型。
对于长记忆系统,MCP 带来的最大变化是:记忆存储不再是某个 Agent 内部的私有实现,而是可以复用的外部服务。一个团队可以同时维护多个 MCP 服务端,分别对接向量库、权限库、业务数据库,不同智能体通过统一协议调用它们。
MCP 的通信基础是 JSON-RPC 2.0。最常见的几个方法是:
| 方法 | 作用 |
|---|---|
| tools/list | 列出当前可用的工具 |
| tools/call | 调用指定工具并返回结果 |
| resources/read | 读取外部资源内容 |
| prompts/get | 获取预置提示词模板 |
下面是一个记忆检索工具的调用请求示例,用于说明协议层的数据结构:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "memory_search", "arguments": { "user_id": "u_10001", "query": "用户最近提到的时间偏好", "top_k": 5 } } }对应的结果可以是这样:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "[{\"content\":\"用户习惯在晚上9点后处理报表\",\"score\":0.87,\"created_at\":\"2025-01-12T10:00:00Z\"}]" } ] } }在企业级项目中,MCP 服务端还需要考虑鉴权、超时、健康检查和日志。协议本身不解决这些问题,需要开发者在服务端实现。
2.3 MCP 在真实项目中的典型接入场景
从搜索材料里可以看到,MCP 的生态已经覆盖多个领域。整理成表格会更容易理解它已经不只是“玩具协议”:
| 场景 | 示例 | 解决的问题 |
|---|---|---|
| 设计稿取上下文 | 蓝湖 MCP、MasterGo MCP、Figma 插件 | 让 Agent 读取设计稿中的尺寸、颜色、标注 |
| 浏览器自动化 | Playwright MCP | 让 Agent 操作浏览器完成页面验证和自动化测试 |
| 游戏与 3D | Blender MCP、Unity MCP、Cocos Creator MCP | 让 Agent 在编辑器里执行场景操作 |
| 数据库读写 | MySQL/PostgreSQL MCP | 让 Agent 通过标准工具执行受控查询 |
| 记忆与向量检索 | 向量数据库 MCP | 让 Agent 检索长期记忆和知识库 |
这里还要区分一个热门概念:computer use 和 MCP 的区别。computer use 强调的是模型直接操作屏幕、浏览器、鼠标键盘,模拟人的操作路径。MCP 强调的是以标准化接口读写外部系统和上下文。两者可以配合,比如用 MCP 拿到需要操作的页面结构,再用 computer use 去执行点击,但它们的定位不同,不能混为一谈。
3. 长记忆实战:从语音智能体到 Agent 全链路
3.1 链路设计:语音智能体和文本 Agent 的记忆链路是一致的
语音智能体与纯文本 Agent 的差别只在两端:入口多一个 ASR 语音识别,出口多一个 TTS 语音合成。中间的记忆、检索、Context 组装链路完全一致。完整链路可以这样看:
音频输入 -> ASR(语音转文字) -> 用户文本 -> 记忆检索(向量库 / MCP) -> Context 组装(长期记忆 + 会话摘要 + 最近对话 + 工具结果) -> LLM 生成回复 -> 回复文本 -> 异步任务:写入记忆 + 更新会话摘要 -> TTS(语音合成) -> 音频输出这里的重点是异步任务。用户不应该等待记忆写入完成才收到回复,记忆写入和摘要压缩必须从请求链路中剥离出去,否则一次请求会因为向量库写入和 LLM 摘要调用而显著变慢。
3.2 环境准备与依赖
为了让示例能直接跑起来,我选择 Python 技术栈。这是一个学习环境组合,生产环境需要替换为分布式组件。
| 组件 | 用途 | 说明 |
|---|---|---|
| Python 3.10+ | 运行环境 | 低版本可能不支持部分异步语法 |
| FastAPI + uvicorn | 暴露 Agent HTTP 接口 | 处理语音上传和文本请求 |
| openai | 调用大模型 | 通过 base_url 兼容任意 OpenAI 协议的服务 |
| chromadb | 本地向量存储 | 学习环境持久化到目录,生产可替换 |
| tiktoken | token 统计 | 用于 Context 预算控制 |
| openai-whisper | 语音识别 | 本地运行,学习环境用 small 模型即可 |
安装命令:
pip install fastapi uvicorn openai chromadb tiktoken openai-whisper如果使用 whisper,还需要在系统里安装 ffmpeg。学习阶段可以先不用 whisper,用文本接口模拟 ASR,把语音链路和记忆链路分开调试。
3.3 构造最小可运行项目
先准备一个简单的目录结构:
voice_agent/ ├── main.py ├── memory_store/ # chromadb 持久化目录 ├── summaries/ # 会话摘要文本目录 └── requirements.txt核心代码从初始化开始。这里把所有配置写在一个文件里是为了演示,生产环境必须把 API Key、模型名称、向量库路径都放进环境变量或配置中心。
# main.py import uuid from datetime import datetime from fastapi import FastAPI, UploadFile, Form from openai import OpenAI import chromadb import whisper app = FastAPI() # 学习环境用本地文件型向量库,生产环境建议切换为独立向量数据库 client = chromadb.PersistentClient(path="./memory_store") collection = client.get_or_create_collection("agent_long_memory") # LLM 客户端,base_url 指向你的模型服务 llm = OpenAI( base_url="https://your-llm-endpoint/v1", api_key="your-api-key", ) # 语音识别模型,学习环境用 small 即可 asr_model = whisper.load_model("small")这里有两个关键点。其一是 chromadb 的PersistentClient,它会将向量数据持久化到本地目录,服务重启后记忆不丢失。其二是 LLM 客户端的base_url,它让你不必绑定某一家模型服务,只要对方兼容 OpenAI 协议,就可以替换。
3.4 记忆写入与检索代码
记忆写入需要同时保存用户侧内容和助手侧内容,并打上用户 ID、记忆类型和时间戳。这个元数据是后续过滤的基础。
def save_memory(user_id: str, content: str, memory_type: str = "fact"): collection.upsert( ids=[f"{user_id}-{uuid.uuid4()}"], documents=[content], metadatas=[{ "user_id": user_id, "memory_type": memory_type, "created_at": datetime.utcnow().isoformat(), }], )检索时先用where按用户隔离,再取与当前问题最相似的前 N 条记忆。这里的query_texts参数由向量库负责编码,所以不需要手动调用 embedding 接口。
def search_memory(user_id: str, query: str, top_k: int = 5): result = collection.query( query_texts=[query], where={"user_id": user_id}, n_results=top_k, ) docs = result["documents"][0] scores = result["distances"][0] return [ {"text": text, "score": score} for text, score in zip(docs, scores) ]需要特别说明:chromadb 默认返回的是距离值,距离越小表示越相似。不同向量库对 score 的语义不同,有的返回 cosine 相似度,值越大越相似。在写阈值过滤前,先确认当前函数返回的是什么。
3.5 文本自动摘要:控制 Context 增长的核心手段
文本自动摘要不是把对话“变短”,而是把高价值信息重新组织成结构化内容。适合记忆场景的摘要模板至少应该包含五个字段:
- 用户偏好
- 关键决定
- 未完成事项
- 重要时间点、数值、编号
- 最近一次任务状态
先实现一个基础摘要函数:
def compress_conversation(messages, model="your-model"): text = "\n".join(f"{m['role']}: {m['content']}" for m in messages) prompt = f""" 请把以下多轮对话转换为结构化摘要,只保留长期有价值的信息: 1. 用户偏好 2. 关键决定 3. 未完成事项 4. 重要时间点、数值、编号 5. 最近一次任务状态 原对话: {text} """.strip() resp = llm.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0.2, ) return resp.choices[0].message.content当历史会话很长时,直接丢给大模型做摘要仍然可能超限,所以需要把会话分批摘要,再合并分段摘要。这个过程类似 map-reduce:
def split_messages(messages, chunk_size=20): return [messages[i:i + chunk_size] for i in range(0, len(messages), chunk_size)] def hierarchical_summarise(messages, chunk_size=20): chunks = split_messages(messages, chunk_size) partials = [compress_conversation(chunk) for chunk in chunks] if len(partials) <= 1: return partials[0] combined = "\n\n---\n\n".join(partials) return compress_conversation([ {"role": "user", "content": f"合并以下分段摘要,保持结构化字段:\n{combined}"} ])分层摘要解决了两个问题:第一,避免单次摘要输入超过模型上限;第二,让每一段摘要都能聚焦当前分块里的关键信息,避免长文本信息稀释。代价是会多消耗一次模型调用,所以生产环境建议把摘要任务丢到后台队列,而不是放在用户请求链路里。
3.6 Context 上下文工程:把记忆装进有限窗口
Context 工程的核心不是拼接字符串,而是分配预算。我通常按这样的优先级组装:
- 历史会话摘要:提供兜底背景。
- 检索出的长期记忆:提供与当前问题相关的事实。
- 最近对话原文:保留当前会话连续性。
- 工具返回结果:按需截断。
- 用户当前问题:必须完整保留。
下面是一个简化的组装函数:
def build_prompt(user_question, memory_text, session_summary=None, recent_messages=None): # 给模型输出留 1/4 的窗口,输入 prompt 最多占 3/4 budget = int(MAX_CONTEXT_TOKENS * 0.75) parts = [] if session_summary: block = f"[历史会话摘要]\n{session_summary}" tokens = estimate_tokens(block) if tokens <= budget: parts.append(block) budget -= tokens if memory_text: block = f"[长期记忆]\n{memory_text}" tokens = estimate_tokens(block) if tokens <= budget: parts.append(block) budget -= tokens if recent_messages: recent_text = "\n".join(recent_messages[-6:]) parts.append(f"[最近对话]\n{recent_text}") return "\n\n".join(parts) + f"\n\n[用户当前问题]\n{user_question}"这里要注意,最近对话不参与预算裁剪并不是因为它不重要,而是因为它的长度通常可控。真正容易撑爆 Context 的是记忆检索结果和工具返回结果,所以这两部分必须设置严格的 token 预算。
3.7 语音接口和运行验证
语音接口把前面的模块串起来:
@app.post("/voice-agent") async def voice_agent(audio: UploadFile, user_id: str = Form(...)): # 1. 保存临时音频文件 tmp_audio = f"/tmp/{uuid.uuid4()}.wav" with open(tmp_audio, "wb") as f: f.write(await audio.read()) # 2. ASR 语音转文字 user_text = asr_model.transcribe(tmp_audio)["text"].strip() if not user_text: return {"error": "ASR没有识别到有效文本", "asr_text": ""} # 3. 检索长期记忆 memory_items = search_memory(user_id, user_text) memory_text = "\n".join( [f"- {item['text']} (相关度 {item['score']:.2f})" for item in memory_items] ) # 4. 组装 Context prompt = build_prompt(user_text, memory_text) # 5. 调用大模型 resp = llm.chat.completions.create( model="your-model", messages=[ {"role": "system", "content": "你是企业级智能助手,请基于用户记忆和当前问题回答。"}, {"role": "user", "content": prompt}, ], temperature=0.3, ) reply = resp.choices[0].message.content # 6. 异步写入记忆,生产环境应放入消息队列 save_memory(user_id, f"用户提问:{user_text}") save_memory(user_id, f"助手回答:{reply}", memory_type="assistant") # TTS 环节省略,真实项目在这里调用语音合成服务 return {"asr_text": user_text, "reply": reply}启动服务:
uvicorn voice_agent:app --host 0.0.0.0 --port 8000用 curl 上传音频:
curl -X POST "http://127.0.0.1:8000/voice-agent" \ -F "user_id=u_10001" \ -F "audio=@demo.wav"预期返回:
{ "asr_text": "帮我查一下上周会议上提到的价格策略", "reply": "根据你之前的记录,上周会议的核心决定是……" }如果暂时没有音频文件,可以先写一个只接收文本的调试接口,用{"user_id": "u_10001", "text": "你好"}验证记忆链路,避免语音模块和记忆模块混在一起排查。
4. 记忆写入与检索:不是“存下来”就行,还要“用得上”
4.1 哪些内容值得写入长期记忆
记忆系统的精度取决于写入策略。写入太多垃圾,检索噪声就会大;写入太少,需要时又查不到。实际项目里,我至少会写入以下五类内容:
- 用户明确表达偏好:例如“我习惯每晚 9 点看报表”。
- 关键决策:例如“价格策略确定为主打性价比”。
- 未完成事项:例如“下周还要跟进供应商报价”。
- 重要数据:例如合同编号、项目周期、预算数字。
- 重复出现的任务模式:例如“用户每周一都会导出上周订单”。
不建议写入的是所有原始对话。对话轮次越多,向量检索越容易返回一堆相关但无用的内容。原始对话的价值应该保留在会话摘要和工作记忆里,而不是长期记忆里。
4.2 检索质量决定 Context 质量
检索只是第一步,真正的挑战是从一堆候选结果里挑出对当前问题最有用的记忆。我用过比较有效的策略包括:
- 按用户隔离:所有检索都必须带
user_id。 - 相似度阈值过滤:明显不相关的候选直接丢弃。
- 时间衰减:距离当前时间越远的记忆,分数越低。
- 最近摘要兜底:如果长期记忆检索结果很少,就把最近会话摘要完整拼入 Context。
时间衰减在代码里可以是这样的思路:
def filter_memory_items(items, threshold, oldest_days=30, now=None): filtered = [] for item in items: score = item["score"] # 这里 score 的语义取决于向量库,落地前先确认是距离还是相似度 if score > threshold: continue filtered.append(item) return filtered[:5]关键点在于,阈值不是拍脑袋定死的。不同的业务数据分布、不同的向量库、不同的 embedding 模型,分数范围完全不一样。上线前要抽样看一批检索结果,确定当前场景的合理阈值。
4.3 记忆链路关键参数
把长记忆链路里最常见的参数整理成一张速查表,方便复盘:
| 参数 | 含义 | 调大影响 | 调小影响 | 推荐起点 |
|---|---|---|---|---|
| top_k | 检索记忆条数 | 上下文更丰富,但噪声增加 | 更聚焦,但可能漏关键信息 | 3 到 10 |
| score_threshold | 相关度阈值 | 漏召回更多 | 噪声更多 | 先日志观察,再设定 |
| chunk_size | 摘要分块对话轮数 | 单次摘要更全面,但可能超过限制 | 摘要更碎片化 | 10 到 20 轮 |
| max_context_tokens | 输入 prompt 预算 | 模型可参考信息多,但输出空间变小 | 模型更“短视” | 模型上限的 75% |
| memory_type | 记忆类型标签 | 便于按类型过滤和统计 | 无 | fact / decision / todo |
注意:Context 组装必须给模型输出预留空间,不要用满上限。输入占 75%,输出留 25%,是比较稳妥的初始设置。
5. 常见问题排查
5.1 context overflow 类报错
先说现象。模型服务返回 400、context overflow、maximum context length exceeded 等错误,或者 Agent 在长时间运行后突然无法继续。
排查顺序:
- 在请求入口打印 prompt 各模块的 token 统计,判断是哪一段撑爆了窗口。
- 检查
recent_messages是否无限增长,是否只截取了最近 N 轮。 - 检查工具返回结果是否原样进入 Context,JSON 返回值是否过长。
- 检查
max_tokens设置是否过大,导致输入和输出之和超过上限。 - 检查摘要任务是否正常执行,旧的会话摘要是否被正确替换。
如果发现是工具返回内容过大,解决方式是截断和结构化抽取,而不是把整个返回结果拼进 Context。例如数据库查询结果,只保留前 20 行,或只对结果做统计后再让模型回答。
5.2 MCP 服务端连接失败
现象:Agent 日志里出现 timeout、connection refused,或者模型报“没有可用工具”。
排查路径:
- 先单独启动 MCP 服务端,确认它能独立工作。
- 用 MCP 客户端手动调用
tools/list,确认工具列表能正常返回。 - 检查配置里的 command、args、env 是否正确,Python 解释器路径是否对。
- 检查网络服务型 MCP 的地址和鉴权 token。
- 在服务端入口加日志,确认请求是否到达。
MCP 的启动信息很容易被忽略。stdio 型服务端打印出来的调试信息会混进协议流,导致客户端解析失败。排查时先看客户端日志是否出现非 JSON-RPC 格式的输出。
5.3 摘要丢失关键信息
现象:Agent 在回答跨会话问题时明显缺失了某个数字、时间点或决定。
原因通常是摘要 prompt 没有给出结构化约束,模型按自己的理解自由压缩。也可能是摘要的max_tokens设置太小,长摘要被截断。
解决方式:
- 摘要必须按固定模板输出,不能只写“以下是对话总结”。
- 摘要任务单独设置足够大的输出上限。
- 追加一层校验:把摘要和原文同时记录到日志,抽样对比。
- 对数字、编号、时间点做二次校验,必要时在摘要里单独维护一个“关键数据列表”。
5.4 语音链路识别为空或错字多
现象:asr_text为空,或者识别结果与原音频相差较大。
排查顺序:
- 先确认音频文件能正常打开,格式是否为 wav 或 whisper 支持的格式。
- 检查 ffmpeg 是否安装。
- 看 whisper 模型是否完整加载,small 模型在 CPU 上也可以跑,但速度较慢。
- 先用同一段音频转成文本,排除记忆链路问题。
- 确认上传接口是否使用
UploadFile,而不是用 JSON base64 传音频。
这里有一个工程建议:语音识别和记忆链路分开验证。语音识别用固定文本接口模拟,记忆链路用文本输入测试,两边各自稳定后再合起来。
6. 从 Demo 到企业级落地的几处关键改造
6.1 存储与写入链路
本地 chromadb 适合验证,但企业级落地时至少要考虑以下替换:
- 向量库从本地持久化切换为独立向量数据库,例如支持远端部署的向量库,或使用 PostgreSQL 的 pgvector。
- 记忆写入从同步调用改为异步任务,通过消息队列处理后写入。
- 摘要压缩从请求链路中剥离,改为定时任务或事件触发。
写入链路的异步化尤其重要。一次摘要压缩调用大模型可能需要数秒到数十秒,如果放在用户请求链路里,整个语音智能体都会变慢。正确做法是回复用户后,再把本轮对话写入待压缩队列。
6.2 权限、安全与可观测性
长期记忆里存的是用户真实信息,一旦权限控制不到位,就是数据事故。落地时至少要覆盖:
- 用户维度隔离,所有检索强制携带
user_id,服务端再校验一次。 - 多租户场景要在 user_id 前加上 tenant_id,不能只靠前端传参。
- 记忆内容写入前做敏感信息脱敏,例如手机号、身份证号、银行卡号。
- 提供记忆删除和遗忘接口,满足用户删除请求。
- 日志记录记忆写入和检索的关键路径,但不记录完整记忆原文,避免日志泄露。
可观测性方面,建议至少统计三个指标:
- 记忆写入延迟和写入条数。
- 记忆检索命中率,即实际被模型采纳的记忆占检索出记忆的比例。
- Context 平均 token 消耗,用于成本评估和预算调整。
6.3 成本与效果控制
长记忆会在两个地方放大成本:一是向量检索本身,二是摘要压缩调用大模型的消耗。控制成本需要明确什么时候做摘要、摘要做到什么粒度。
一个可用策略是分层压缩:
- 最近 N 轮对话保留原文。
- 会话累计超过 N 轮后,触发一次摘要。
- 旧摘要再次累积后,执行多级摘要合并。
- 只有用户偏好、关键决定等高价值内容写入长期记忆。
这样既能控制成本,也能避免摘要无限膨胀。搜索材料里多次出现“上下文自动压缩被禁用”这类报错,背后的教训是:不能依赖模型运行时的自动压缩,应该由应用层主动设计压缩策略。
6.4 上线前检查清单
对于记忆类 Agent 项目,我每次上线前都会过一遍这张检查清单:
- 记忆是否按用户隔离,多租户是否额外校验。
- 是否存在敏感信息,脱敏和删除接口是否就绪。
- Context 预算是否设置,输入和输出比例是否合理。
- 摘要任务是否异步化,队列堆积是否有监控。
- 模型和向量库的 token 统计是否接入日志。
- 是否有记忆写入失败的补偿机制。
- 是否有删除记忆的运维入口。
- 是否有压测数据,长会话运行 1 小时和 24 小时后 Context 是否稳定。
- 是否配置了降级方案,例如记忆服务不可用时,只回退到当前会话。
- 是否有记忆检索命中率指标和人工抽样评估机制。
7. 扩展方向
7.1 从向量记忆走向知识图谱记忆
向量记忆擅长找“相似”,但很难回答“用户 A 和用户 B 之间的关系是什么”。当业务需要关系推理时,知识图谱可以补充结构化记忆。实际落地通常是向量库和知识图谱并存:先向量召回候选记忆,再在图谱中补充关系路径。
7.2 多模态记忆与主动遗忘
语音智能体的记忆不只是文本。用户语气、情绪、图片、屏幕截图都可能成为记忆的一部分。多模态记忆的难点在于统一 embedding 空间和权限控制。另一个容易被忽略的方向是主动遗忘:记忆不是永久保存越全越好,时间衰减、删除请求、定期清理都应该成为记忆系统的基础能力。
企业级长记忆系统的核心判断是:它从来不是一个模型、一个数据库或一个框架能解决的问题,而是一条从语音识别、文本摘要、向量存储、MCP 工具到 Context 组装的完整链路。先把 Context 预算管住,把记忆写入和检索的边界划清楚,再逐步叠加更多能力,这个顺序比堆砌模型参数和技术名词更重要。