20 · 生产级 Agent 架构:记忆、压缩与武器库¶
先讲给朋友听(如果你只有一分钟)¶
第 14 篇的 Agent 是个"失忆的打工人":每轮对话都把全部历史重发一遍,干三个小时就撑爆上下文。生产级 Agent 架构要解决的就是这件事:给它记忆、给它档案柜、教它忘掉废话,再配上一整套工具和护栏。 这些模块你全都见过——只是换了名字。
你的 Aha Moment¶
先看一组 2026 年的实测数字(Mem0 官方基准,LoCoMo 数据集):
| 方案 | 每次查询消耗 token |
|---|---|
| 全量上下文(把所有历史都塞进去) | 26,000+ |
| Mem0 式外部记忆(检索相关事实) | ~7,000 |
同样的效果,四分之一的成本。 在每天万级交互的生产系统里,这不是优化,是生死线。
但更值得你 Aha 的是另一件事:这两个数字背后,是两个完全不同的设计哲学。
- 上下文压缩(compression):历史太长了?摘要一下、截断一下、把工具输出删掉。它不记住任何东西,它只是忘得更慢。 单会话内有效,跨会话归零,失败模式是"静默的有损退化"——你根本不知道丢了什么。
- 外部记忆(memory):在信息消失之前把事实抽出来,存到上下文窗口外面,用的时候按相关性取回来。存的是原子事实,跨会话有效,成本是恒定的。
工业界 2025-2026 的共识是三层架构,把两者都用上:
Tier 1 工作记忆(in-context) 当前会话原样保留,无损,最贵
Tier 2 会话压缩(compression) 会话变长 → 锚定式增量摘要,省钱但有损
Tier 3 长期记忆(external) 跨会话原子事实,检索式取回,成本恒定
Aha 落点:这三层就是你熟悉的存储金字塔——Tier 1 是内存,Tier 2 是换出到磁盘的页面缓存,Tier 3 是数据库。连淘汰策略都眼熟:记忆的"过期"用 0.3~1.5 倍的新近度衰减因子做软重排(不硬删,只是排得靠后),活脱脱一个 LRU 的软评分版。
不讲术语的版本:生产级 Agent 的六个模块¶
画一张架构图,你就能看出它跟你做过的系统多么同构:
┌──────────────┐
用户请求 ───────▶│ 接入/网关层 │ 多租户、限流、鉴权、审计
└──────┬───────┘
▼
┌─────────────────────────────────────┐
│ 编排层(LangGraph) │ 状态机 / 工作流 / 人审节点
│ ┌─────────┐ ┌────────┐ ┌───────┐ │
│ │ 短期记忆 │ │ 长期 │ │ 压缩 │ │ ◀── Tier1/2/3
│ │ (buffer)│ │ 记忆库 │ │ 引擎 │ │
│ └─────────┘ └────────┘ └───────┘ │
└───┬──────────────┬──────────────┬───┘
▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌──────────┐
│ 知识检索 │ │ 工具/MCP │ │ 代码沙箱 │
│ ES+向量 │ │ 副作用闸门 │ │ 隔离执行 │
└──────────┘ └────────────┘ └──────────┘
└──────────────┴──────────────┘
▼
┌──────────────┐
│ 观测/评测/计费 │ 全链路 trace + 评测集 + token 账
└──────────────┘
模块一 · 接入/网关层。 多租户隔离(A 公司的问题不能看到 B 公司的记忆)、按用户限流(防刷 token)、全量审计(谁在什么时候问了什么)。这就是你的 API Gateway,加了一套 AI 特有的计费维度。
模块二 · 编排层。 LangGraph 的图。生产上和 Demo 的区别在两点:checkpointer 必须用持久化实现(断电恢复,对应 Camunda 的流程实例表);副作用工具必须过人审(17 篇的 interrupt())。
模块三 · 记忆三层。
- Tier 1 短期记忆:消息缓冲区,原样保留最近 N 轮
- Tier 2 压缩:锚定式增量摘要(anchored summarization)——不是每次都重新总结全文,而是在既有摘要上追加增量。行业里有个狠案例:某团队分析 36000 条工程会话消息,发现"锚定式"在准确率上优于完全重建,也优于 Anthropic 和 OpenAI 的摘要方案
- Tier 3 长期记忆:每轮对话后,用一个便宜模型抽取原子事实存库("用户是 3 年工龄的产品经理""他偏好表格而非长文"),按 user_id / agent_id / org_id 三维度做作用域隔离。注意 Mem0 的工程细节:抽取是 ADD-only(只加不覆盖),历史不会被静默改写;过期靠软衰减而非硬删除
模块四 · 知识检索。 就是 16 篇那套混合检索。企业场景多一个维度:知识库(wiki)的接入——飞书/Confluence/Notion 文档。工程上关注两件事:增量同步(文档更新了,索引要跟得上,ETL 的活)和权限过滤(检索时按用户权限裁剪文档集,别把财务制度推给实习生)。
模块五 · 工具与 MCP。 2026 年的生产标准是 MCP(Model Context Protocol):工具不再在每个 Agent 里各写一遍,而是做成标准 server,任何 Agent 都能挂。你机器上的 oMLX 就原生支持 --mcp-config。副作用工具(写库、发信、下单)走白名单 + 人工闸门;代码执行必须进沙箱(容器/微虚拟机,禁网、限时、限内存),否则模型的"我来帮你跑一下"就是你的线上事故。
模块六 · 观测与评测。 16 篇的评测集、17 篇的 token 账,在这里汇总成看板。生产级 Agent 的运维三件套:每次调用的 trace(Prompt/工具/延迟/token)、任务完成率(不是"感觉还行")、成本看板(按租户/功能分摊)。
Java 老司机对照室¶
| Agent 架构模块 | 你做过的系统 | 一句话翻译 |
|---|---|---|
| 三层记忆 | 内存 / 页面缓存 / 数据库 | 存储金字塔原样搬 |
| 锚定式增量摘要 | 增量 ETL / CDC | 别每次全量重算 |
| ADD-only 记忆 | 事件溯源(append-only log) | 只追加不改写,历史可追 |
| 新近度软衰减 | LRU 变体 | 软评分,不硬删 |
| 记忆作用域隔离 | 多租户数据隔离 | 按 user/agent/org 三维切 |
| wiki 增量同步 | Binlog → ES 同步 | ETL 老活 |
| 检索权限过滤 | 行级权限(RLS) | 检索时裁剪候选集 |
| MCP 工具 | 微服务注册中心 + SDK | 工具标准化,随处挂载 |
| 沙箱执行 | 容器隔离 / SecurityManager | 模型生成的代码 = 不可信输入 |
| 人审节点 | 审批流 UserTask | 17 篇的 interrupt() |
| 成本看板 | 多租户计费 | 按维度归因 |
最后一行值得念叨:记忆的"作用域"是生产事故高发区——把用户 A 的记忆喂给用户 B,不只是 bug,是合规事故。Mem0 官方专门列了五个作用域维度,你设计时至少要有 user / agent / org 三个。
动手实验室¶
实验 1:三层记忆的最小实现(零依赖,跑得动)¶
from collections import deque
import re
class MemoryAgent:
"""三层记忆:Tier1 原文缓冲 / Tier2 摘要压缩 / Tier3 事实库"""
def __init__(self, buffer_size=4, budget=12):
self.tier1 = deque(maxlen=buffer_size) # 工作记忆:最近 N 条原文
self.tier2_summary = "" # 会话摘要(增量追加)
self.tier3: dict[str, str] = {} # 长期记忆:key -> 事实
self.budget = budget # 上下文预算(token 数)
def observe(self, role: str, content: str):
self.tier1.append({"role": role, "content": content})
def extract_fact(self, text: str) -> str:
"""Tier3:抽取原子事实。真实实现是调一次便宜小模型,
这里用正则演示'什么样的事实值得抽'——右侧是可复用的抽取模式"""
patterns = {
"工龄": r"工龄([一二三四五六七八九十\d]+年)",
"城市": r"(北京|上海|深圳|广州|杭州)",
"角色": r"(后端|前端|产品经理|测试|运维)",
}
found = []
for key, pat in patterns.items():
m = re.search(pat, text)
if m and key not in self.tier3: # ADD-only:先到先得,不覆盖
self.tier3[key] = m.group(1)
found.append(f"{key}={m.group(1)}")
return ",".join(found)
def build_context(self) -> str:
parts = []
if self.tier2_summary:
parts.append(f"[会话摘要] {self.tier2_summary}")
if self.tier3:
facts = ";".join(f"{k}:{v}" for k, v in self.tier3.items())
parts.append(f"[长期记忆] {facts}")
for m in self.tier1:
parts.append(f"[{m['role']}] {m['content']}")
ctx = "\n".join(parts)
# Tier2 触发:超出预算就把最老的一段压成摘要
if len(ctx) > self.budget and self.tier1:
oldest = self.tier1.popleft()
delta = f"{oldest['role']}说过:{oldest['content'][:15]}…"
self.tier2_summary = (self.tier2_summary + " " + delta).strip() # 锚定式追加
return ctx
a = MemoryAgent(buffer_size=3, budget=120)
for i, msg in enumerate([
("user", "我入职三年了,工龄三年,在北京做后端"),
("assistant", "您好!三年经验正是黄金期呢~"),
("user", "帮我查下年假"),
("assistant", "年假是5天哦"),
("user", "那我病假呢"),
("assistant", "病假需要医院证明"),
]):
a.observe(*msg)
fact = a.extract_fact(msg[1])
print(f"第{i+1}条 → 上下文 {len(a.build_context())} 字|抽取到事实: {fact}")
print("\n最终上下文(注意摘要和记忆是怎么挤进来的):")
print(a.build_context())
跑这个,你能亲眼看到:缓冲区的旧消息被压成一行摘要(Tier2),"工龄=三年"被抽进事实库(Tier3),而 context 长度始终没炸。这就是生产 Agent 每天跑几万次的那个循环。
实验 2:上下文压缩 vs 外部记忆的成本曲线¶
def simulate(turns=20, mode="compress"):
"""模拟 20 轮对话的上下文体积增长"""
per_turn = 60 # 每轮问答约 60 token
if mode == "compress": # 只压缩,不建外部记忆
ctx = 0; summary_cap = 200
for _ in range(turns):
ctx = min(ctx + per_turn, summary_cap)
return ctx
if mode == "memory": # 外部记忆:恒定开销
return 400 + 7 # 固定检索窗口
if mode == "full": # 全量历史
return turns * per_turn
print(f"{'方案':<14}{'20轮后上下文':>12}")
for m in ("full", "compress", "memory"):
print(f"{m:<14}{simulate(mode=m):>10} token")
输出会显示:全量线性增长、压缩触顶、外部记忆恒定。把这三条曲线画给你老板看,比任何架构图都有说服力——它同时解释了钱花在哪和体验差在哪。
实验 3:MCP 工具注册的开放格式¶
# 一个 MCP server 的声明长这样(简化版):工具自描述,随处可挂
MCP_SERVER = {
"name": "hr-knowledge",
"version": "1.0.0",
"tools": [
{"name": "search_policy", "description": "检索公司制度",
"inputSchema": {"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"]}},
{"name": "submit_leave", "description": "提交请假申请(副作用,需人审)",
"inputSchema": {"type": "object",
"properties": {"days": {"type": "integer"},
"type": {"type": "string"}},
"required": ["days", "type"]}},
],
}
# 你的 Agent 只要认这个格式,就能挂上任何 MCP server
def mount(server, allowed: list[str]):
"""只挂载白名单内的工具——副作用工具默认不挂"""
return [t for t in server["tools"] if t["name"] in allowed]
print("默认挂载:", [t["name"] for t in mount(MCP_SERVER, allowed=["search_policy"])])
print("申请后挂载:", [t["name"] for t in mount(MCP_SERVER, allowed=["search_policy", "submit_leave"])])
注意默认白名单里没有 submit_leave。 这个函数就是"Agent 的权限模型"——和你的系统里"按钮级权限"是完全一样的设计。
费曼复述卡¶
- 用"存储金字塔"向上司解释三层记忆:工作记忆 / 会话压缩 / 长期记忆各解决什么、各花多少钱。
- 上下文压缩和外部记忆的区别是什么?为什么说压缩"只是忘得更慢"?
- 生产级 Agent 比你 14 篇的 Demo 多了哪六个模块?其中哪三个是你的 Java 老本行直接平移的?
三题过不了,把实验 1 的 extract_fact 换成真调一次小模型——你会发现 Tier3 的真实成本在于"抽取得准不准"。
今日一句话¶
生产级 Agent = 网关 + 编排 + 三层记忆 + 混合检索 + MCP 工具 + 观测六件套——除模型外,每个模块你都在 Java 里造过。
下一篇:21 · 评测:别问三个问题就说"还行"(返回 目录)