1. 为什么需要记忆 (Memory)?
在 AI 应用开发中,我们必须面对一个残酷的真相:大语言模型(LLM)本身是一个"金鱼脑",它没有任何记忆功能。
大模型的底层 API(如 OpenAI 的接口)本质上是**无状态(Stateless)**的。每一次 invoke 调用对它来说都是一次完全独立的、全新的运算。它绝对记不住上一秒你们聊了什么。
比如:
- 你问:"你好,我叫小明。"
- AI 答:"很高兴认识你,小明。"
- 你紧接着问:"我叫什么名字?"
- AI 答:"我不知道你的名字,除非你告诉我。"
为了让 AI 看起来像是一个有记忆、能连贯交流的"人",我们就必须引入**记忆(Memory)**机制。
记忆的核心本质
所谓的"记忆",并不是大模型自己主动回想起来的,而是 Agent 框架(或客户端)在后台默默地把历史聊天记录存了下来。等用户下一次提问时,框架会把这些历史记录打包,连同新问题一起再次"喂"给大模型。大模型是看着你给的历史档案,才"假装"它记得你。
因此,我们需要系统的记忆管理机制来解决以下痛点:
- 告别手动拼接 :如果不借助框架,开发者每次都需要手动维护并拼接长长的
messages数组,代码极其恶心。 - 防爆防超载(Token 管理):历史对话如果无限累加,很快就会撑爆大模型的上下文窗口,不仅报错还极度费钱。
- 跨会话持久化:普通的数组存在内存里,一旦重启或换了会话就清零了。我们需要机制让历史信息在不同场景下都能被利用。
1.1 核心概念辨析:短期记忆 vs 长期记忆 (行业共识与框架落地)
您可能会问:"短期记忆和长期记忆,到底是谁规定的?如果我不用 LangChain,这两个词又是什么意思?"
首先要明确一个大前提:这绝对不是 LangChain 发明的词汇! 它们最早来源于认知心理学 对人类大脑的划分(如 Atkinson-Shiffrin 记忆模型)。在进入大模型时代后,这套概念成为了整个 AI Agent 领域的底层行业共识,被大量顶级学术论文(如斯坦福的 Generative Agents 虚拟小镇)和顶级开源项目(如 AutoGPT)广泛采用。
1. 抛开框架,整个 AI 行业的通用定义是什么?
不管你用不用任何框架,只要你写代码调大模型,记忆的本质就是:
- 短期记忆(行业通用) = 上下文窗口 (Context Window)
- 大白话 :它就是大模型的"工作台",也就是你当前传给大模型的那个
messages数组(聊天框里的上下文)。 - 局限:受限于大模型的脑容量(Token 上限,如 8K、128K)。塞多了不仅极度烧钱,还会因为"注意力涣散"导致大模型变笨,一旦断开连接或者超载,数据就作废了。
- 大白话 :它就是大模型的"工作台",也就是你当前传给大模型的那个
- 长期记忆(行业通用) = 外挂数据库 (External Database)
- 大白话:它就是大模型的"硬盘"。通常是向量数据库(Vector DB)、图数据库或关系型数据库。
- 优势 :容量无限。大模型不需要时刻把这些信息塞在脑子里,只有当用户问到特定问题时,系统才去硬盘里检索 (Retrieve) 出相关记忆,然后临时抄写到"工作台(短期记忆)"上给大模型看。
2. 回到 LangChain / LangGraph,它是怎么在代码里落地的?
在了解了行业共识后,我们会发现一个极易踩坑的误区 :很多人误以为"既然短期记忆是工作台,那它一定只存在内存(RAM)里;长期记忆存硬盘,所以存在数据库里的才叫长期记忆"。这是完全错误的!
为了防止服务器宕机导致用户正在聊天的上下文丢失,在真正的企业级应用里,无论是长期还是短期记忆,统统都要持久化存进数据库里!
因此,在 LangChain (LangGraph) 的工程架构中,区分这二者的唯一标准是"作用域(Scope)":
- 短期记忆 (Short-term Memory) = 跟随
thread_id生灭的流水账- 官方定义:基于单个会话级别(Thread-Scoped)的记忆。
- 本质 :它记录的是某一次**特定聊天(由
thread_id唯一标识)**中的原汁原味的对话流水账。只要用户点击了"新建对话"(换了一个新的thread_id),哪怕它存在 Postgres 数据库里,它对这个新窗口也是"不可见/失忆"的。
- 长期记忆 (Long-term Memory) = 跨越
thread_id的全局画像- 官方定义:跨会话级别(Cross-Thread / Global)的记忆。
- 本质 :它是系统从海量流水账中提炼出的核心事实、用户偏好(如:该用户是 Python 程序员、对花生过敏)。无论用户明天新建了多少个聊天窗口,只要系统认出这个用户,就能跨越时空检索到这些信息。
一句话总结(终极心法) : 短期记忆解决的是"如何接上当前这局话";长期记忆解决的是"如何认识眼前这个人"。在工程上,跟 thread_id 绑死的叫短期记忆,跨越 thread_id 共享的叫长期记忆。
2. 短期记忆的实现:Checkpointer (持久化器)
在 LangChain (LangGraph) 中,解决"金鱼脑"问题并实现短期记忆的核心机制叫做 Checkpointer(状态保存器/持久化器)。
在刚才的 01 测试中,我们手动拼接 messages 数组非常麻烦。而引入 Checkpointer 后,框架会在后台自动帮我们做这件事。每次对话结束,它会把当前所有的聊天记录(State)存进数据库;每次新对话开始,它会根据你提供的 thread_id,自动从数据库里把历史记录捞出来,拼在你的新问题前面,再发给大模型。
2.1 核心概念:thread_id (会话隔离)
记忆是绝对不能串台的!张三和李四同时在跟 AI 聊天,AI 必须能分清谁是谁。 thread_id 就像是微信里的聊天窗口 ,或者单机游戏里的存档槽位 。 只要你调用时传入相同的 thread_id,AI 就能顺着之前的记忆继续聊;如果你传入一个新的 thread_id,对 AI 来说就是一个全新的、干干净净的"失忆"状态。
2.2 实战对比:从"失忆"到"拥有记忆"
下面展示了如何通过短短几行代码,让 Agent 拥有记忆能力:
❌ 错误示范:没有记忆(不挂载 Checkpointer)
python
from langchain.agents import create_agent
from langchain.messages import HumanMessage
# 裸奔的 Agent,没有记忆模块
agent = create_agent(model=model, tools=[])
# 第一轮
agent.invoke({"messages": [HumanMessage("我叫张三")]})
# 第二轮
response = agent.invoke({"messages": [HumanMessage("我叫什么名字?")]})
# 大模型回答:我还不知道你的名字。(彻底失忆)
✅ 正确示范:拥有记忆(挂载 InMemorySaver)
python
from langchain.agents import create_agent
from langchain.messages import HumanMessage
# 引入内存级存储器
from langgraph.checkpoint.memory import InMemorySaver
# 1. 实例化一个记忆存储器(注意:生产环境通常会换成 PostgresSaver 等存入真实数据库)
checkpointer = InMemorySaver()
# 2. 将存储器挂载到 Agent 上,赋予其存储能力
agent = create_agent(
model=model,
tools=[],
checkpointer=checkpointer
)
# 3. 必须定义 config,明确指定当前是在哪个"存档槽位 (thread_id)"里聊天
config = {"configurable": {"thread_id": "1"}}
# 第一轮
agent.invoke(
{"messages": [HumanMessage("我叫张三")]},
config=config # 4. 每次发请求,必须把槽位号带上
)
# 第二轮
response = agent.invoke(
{"messages": [HumanMessage("我叫什么名字?")]},
config=config # 带着同样的槽位号去读档
)
# 大模型回答:你叫张三。(完美记住!)
✅ 企业级示范:存入真实数据库(挂载 PostgresSaver)
在真正的生产环境中,服务器随时可能重启,存在内存 (InMemorySaver) 里的记忆非常不靠谱。此时我们只需要把它替换为 PostgresSaver(或其他数据库 Saver),业务代码几乎一字不改,就能瞬间实现工业级的、跨物理机的状态持久化:
python
from langchain.agents import create_agent
from langchain.messages import HumanMessage
# 引入 Postgres 数据库级别的存储器
from langgraph.checkpoint.postgres import PostgresSaver
# 你的真实数据库连接地址
DB_URL = "postgresql://user:password@localhost:5432/langchain_db?sslmode=disable"
# 1. 连接真实数据库并实例化存储器
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 第一次运行时可以调用 setup() 自动在数据库里建表
checkpointer.setup()
# 2. 将数据库存储器挂载到 Agent 上
agent = create_agent(
model=model,
tools=[],
checkpointer=checkpointer
)
# 3. 指定 thread_id
config = {"configurable": {"thread_id": "user_zhangsan_session_01"}}
# 第 1 轮对话
response1 = agent.invoke(
{"messages": [HumanMessage("你好,我是亮哥")]},
config=config
)
# 【高能预警】假设此时服务器突然断电重启,或者你的请求被分配到了另一台物理服务器...
# 第 2 轮:哪怕服务器换了,只要 thread_id 没变,Agent 瞬间去数据库里读档,依然能完美接话!
response2 = agent.invoke(
{"messages": [HumanMessage("你知道我是谁吗?")]},
config=config
)
# 大模型回答:你刚才自我介绍说你是亮哥。(数据库读档成功!)
💡 深度扩展:为什么要用 PostgreSQL,能用 MySQL 吗?
在看了上面的数据库持久化代码后,很多有经验的开发者会有两个常见的疑问:
疑问一:为什么偏偏是 PostgreSQL?我能用 MySQL 来做短期/长期记忆吗?
- 结论 :在 AI 和大模型开发领域,PostgreSQL 是绝对的王者,不建议使用 MySQL。
- 原因 1(处理短期记忆) :AI 的聊天记录、Token 消耗明细、工具调用等,全都是极其复杂的嵌套 JSON 结构。PostgreSQL 对 JSON(尤其是
JSONB格式)的原生支持、解析速度和深度检索能力,远远甩开 MySQL 几条街。 - 原因 2(处理长期记忆) :长期记忆往往需要将文本转为向量存储。PostgreSQL 只要装上一个
pgvector插件,就能瞬间变身为性能极其强悍的向量数据库 (Vector DB),完美支持 HNSW 等高级算法。这意味着你用一个 PG 就能同时搞定传统数据和 AI 向量数据。而 MySQL 在这方面不仅功能不成熟,更致命的是,几乎所有的 AI 框架(包括 LangChain)第一默认支持的都是 PG 的生态。
疑问二:用了 PostgresSaver,我还需要自己写 SQL 语句去建表、插数据吗?
- 结论 :完全不需要,一句 SQL 都不用写!
- 残酷的现实 :如果不借助 LangChain,你要自己设计表结构、处理各种
HumanMessage/AIMessage的恶心序列化/反序列化,并且在复杂 Agent 场景下还要自己处理并发带来的数据覆写问题,几百行代码是少不了的。 - 框架的魔法 :用了
PostgresSaver,所有的脏活累活框架全包了 。代码里的checkpointer.setup()会自动在数据库里建好所需的所有表。而后续的存取,你只需要传一个thread_id,框架就会自动在底层执行复杂的SELECT、INSERT甚至乐观锁校验。在你写代码的视角里,你甚至感受不到底层数据库的存在,这就是好框架的威力。
2.3 🕵️♂️ 幕后揭秘:查看记忆存档
如果你想看看 Checkpointer 到底在后台存了什么,可以使用 get_state 方法。它会把你和 AI 聊过的每一句话,甚至每一次 Token 的消耗明细,全都完好无损地记录在案:
python
# 偷窥后台到底在这个 thread_id 槽位里存了什么
state = agent.get_state(config)
# 打印所有历史消息
for msg in state.values["messages"]:
print(msg.content)
# 你会看到一个完整的对话链:
# 我叫张三
# 你好,张三!很高兴认识你。有什么我可以帮你的吗?
# 我叫什么?
# 你叫张三。
2.4 🧠 底层原理:LangChain 的短期记忆是怎么存的?
很多同学会好奇,不管是用 InMemorySaver 还是 PostgresSaver,LangChain 底层到底是怎么把这些消息存进去的?
它的底层依赖于 LangGraph 的 Checkpointer(状态快照机制),运行原理可以概括为以下四步:
- State Snapshot(拍快照) :在 Agent 执行的每一个关键节点(比如大模型刚生成完一条回复,或者某个 Tool 工具刚执行完),框架都会把当前整个 Agent 的内部状态(包括庞大的
messages数组,以及各类元数据)"咔嚓"拍一张完整的快照 (Snapshot)。 - Serialization(序列化打包) :内存里的复杂 Python 对象是没法直接塞进关系型数据库的。框架底层会自动把这个庞大的快照对象,压缩、序列化成一段特定的二进制数据流或 JSON 字符串(通常使用
msgpack、pickle或json协议)。 - Storage(入库存储) :
- 对于
InMemorySaver:它仅仅是把打包好的数据,塞进一个全局的 Python 字典里,键 (Key) 就是你的thread_id。 - 对于
PostgresSaver:它会把这段二进制数据通过一条底层的INSERT语句,存进 Postgres 数据库的checkpoint_blobs表中,主键同样是你的thread_id加上当前快照的唯一 ID (checkpoint_id)。
- 对于
- Retrieval(读档恢复) :当你第二天再次带着
thread_id="1"呼叫 Agent 时,Checkpointer 会去字典或数据库里,查出这个thread_id下面最新的一条快照记录。然后将其反序列化(解压缩)还原成原本的 Python 对象,原封不动地喂给大模型。大模型"一睁眼",就会以为你们的对话根本没有中断过。
一句话通俗总结 : LangChain 的持久化器,本质上就是一套**"游戏自动存档 (Auto-Save) 系统"**。每聊完一句,系统就在后台偷偷帮你存盘;下次只要你选对"存档槽位 (thread_id)",游戏就能满血复活、继续推进。
🕵️ 源码佐证 (Proof from Source Code)
口说无凭,如果您翻开 LangGraph 官方仓库中 langgraph/checkpoint/postgres/__init__.py 的源码,您会发现底层负责"存档"的 put() 方法的核心逻辑就是这么写的(已精简核心逻辑):
python
# langgraph 源码截取:PostgresSaver.put() 核心逻辑
def put(self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, ...) -> RunnableConfig:
# 1. 获取存档槽位 (thread_id) 和 快照ID (checkpoint_id)
thread_id = config["configurable"]["thread_id"]
checkpoint_id = checkpoint["id"]
# 2. 调用序列化器 (self.serde),把庞大的状态对象压缩打成二进制流
serialized_checkpoint = self.serde.dumps(checkpoint)
serialized_metadata = self.serde.dumps(metadata)
# 3. 执行底层的 SQL 语句,真正存入 Postgres 数据库
with self.conn.cursor() as cur:
cur.execute(
"""
INSERT INTO checkpoints (thread_id, checkpoint_id, parent_checkpoint_id, checkpoint, metadata)
VALUES (%s, %s, %s, %s, %s)
""",
(
thread_id,
checkpoint_id,
config["configurable"].get("checkpoint_id"),
serialized_checkpoint, # 注意:这里存进数据库的已经是纯粹的 byte 字节流了!
serialized_metadata,
),
)
return {"configurable": {"thread_id": thread_id, "checkpoint_id": checkpoint_id}}
可以看到,源码完美印证了我们前面讲的:找槽位 -> 序列化 (serde.dumps) -> 拼写 SQL 存入 checkpoints 表 的全过程。
3. 记忆治理策略 (Memory Governance)
💡 核心洞察:为什么必须要"治理"?(来自您的架构思考)
正如您刚才在总结中所说,通过前面的持久化机制,我们虽然完美解决了**"消息不丢失"**的问题,但这也引出了一个新麻烦: 由于消息不丢失,聊得越久,消息体会变得非常大。而大模型最关键的资源和瓶颈就是 Token。如果放任不管,历史记录就会多达上千条,这会带来致命后果:
- Token 撑爆:超出了大模型一次能处理的极限,直接报错死机。
- 极度费钱:每次都要带着前几千句话当上下文,烧钱速度惊人。
- 模型变笨:上下文越长,大模型的注意力越分散。
因此,我们必须对这日益庞大的消息进行治理(减肥)。LangChain 提供了三种经典的策略,但正如您所分析的,前两种(裁剪/删除)其实都不太好,因为**"它们有可能会把重要的历史东西给丢掉"**,只有第三种(摘要)才是最优雅的解法。
3.1 策略一:消息裁剪 (Message Trimming)
-
做法 :在发送给大模型之前 (通常用
@before_model中间件),硬性规定一个长度上限。比如"永远只给大模型看最开始的系统设定(System Prompt)和最近的 4 句话,中间的全扔掉"。 -
代码实现 :
pythonfrom langchain.agents.middleware import before_model from langchain.messages import RemoveMessage from langgraph.graph.message import REMOVE_ALL_MESSAGES @before_model def trim_messages(state: AgentState, runtime: Runtime): messages = state["messages"] if len(messages) <= 3: return None first_message = messages[0] # 保留第一句(通常是系统人设) recent_messages = messages[-3:] # 保留最近的3句 new_messages = [first_message] + recent_messages return { "messages": [ RemoveMessage(id=REMOVE_ALL_MESSAGES), # 杀手锏:清空所有历史 *new_messages # 重新塞入裁剪后的消息 ] } -
优点:实现极其简单粗暴,效果立竿见影。
-
缺点 :就像您说的,这种方式不太好,可能会把早期重要的上下文给直接丢掉。
3.2 策略二:消息删除 (Message Deletion)
-
做法 :在大模型回复之后 (通常用
@after_model中间件),定期去清理老旧的消息。比如发现总数超过 5 条,就把最老的那几条单独标记删除。 -
代码实现 :
pythonfrom langchain.agents.middleware import after_model from langchain.messages import RemoveMessage @after_model def delete_old_messages(state: AgentState, runtime: Runtime): messages = state["messages"] if len(messages) > 5: to_delete = len(messages) - 5 # 精准狙击:只删除最早的那几条 return {"messages": [RemoveMessage(id=m.id) for m in messages[:to_delete]]} return None -
缺点:同样存在**"丢掉重要信息"**的致命风险,只适合极轻量级的闲聊。
3.3 策略三:滚动摘要 (Summarization) ------ 最强王者方案
-
做法 :正是您刚才想到的最优解:既然大模型最在乎 Token,且我们又怕丢消息,我们就可以通过一个(便宜的)小模型,把之前的聊天记录提取、摘要一下,然后把精简后的摘要塞回历史记录里。
-
代码实现 :LangChain 为此提供了开箱即用的中间件!
pythonfrom langchain.agents.middleware import SummarizationMiddleware # 初始化一个便宜的小模型专门用来做摘要(省钱) model_in = init_chat_model("gpt-4o-mini", ...) agent = create_agent( model=model_out, # 负责主聊天的大模型 checkpointer=InMemorySaver(), middleware=[ SummarizationMiddleware( model=model_in, # 让便宜的小模型干总结的脏活 trigger=[("tokens", 1000)], # 触发条件:一旦超过 1000 Tokens 就开始减肥 keep=("messages", 2), # 减肥后,保留最近的 2 句原话不动 summary_prompt="对历史消息摘要,消息列表如下\n{messages}" ) ] ) -
底层神仙操作 :
- 当您疯狂发了几万字的废话后,触发
tokens > 1000的条件。 SummarizationMiddleware在后台偷偷召唤gpt-4o-mini,把废话浓缩成:"用户叫张三,是个话痨工程师。"- 系统悄悄把那几万字废话从
messages数组里删掉,把这句精简的摘要作为历史注入进去。
- 当您疯狂发了几万字的废话后,触发
-
优点:完美平衡了"不遗忘历史"和"节省 Token 成本"两大矛盾,是企业级应用的最佳实践!
4. 长期记忆 (Long-term Memory)
💡 核心洞察:长期记忆与短期记忆的本质区别(来自您的架构思考)
您的这个总结可以说是极其深刻!它直接道出了这两种记忆模式在业务设计哲学上的根本差异:
- 短期记忆(无脑全量记录) :它就像是一支"录音笔"。在一个会话(
thread_id)内,不管你们聊了多少毫无意义的废话,它都会原封不动、事无巨细地全部存下来。它的目的是保证"当前这局聊天"的连贯性。 - 长期记忆(有价值的通用信息) :它就像是一个"知识库"或"用户画像"。它存的都是**"高度浓缩、比较通用、且重要的信息"(比如用户的名字、爱好、身份)。长期记忆 绝对不是把所有聊天记录照单全收,而是需要我们(或让 AI 使用 Tool)去主动挑选、提炼**觉得有价值的信息存起来,以便在未来随时取用。
在 LangChain (LangGraph) 中,短期记忆使用的是 Saver (持久化器),而长期记忆使用的是 Store (存储器)。两者的 API 完全不同。
4.1 基础 API:增删改查 (put / get / search)
长期记忆的本质是一个具有层级结构(Namespace)的键值对(Key-Value)数据库。
(1) 基本的存入与读取
不论是用开发测试版的 InMemoryStore,还是生产级的 PostgresStore,API 都是一模一样的:
python
from langgraph.store.memory import InMemoryStore
# 生产环境中替换为:from langgraph.store.postgres import PostgresStore
store = InMemoryStore()
# 如果是 PostgresStore,需要 setup() 建表
# store = PostgresStore.from_conn_string(DB_URL); store.setup()
# 1. 定义命名空间 (Namespace) 和 唯一键 (Key)
namespace = ("users",)
user_id = "user-1"
user_info = {"name": "小明", "age": 18}
# 2. 存入数据 (Put)
store.put(namespace, user_id, user_info)
# 3. 读取数据 (Get)
item = store.get(namespace, user_id)
print(item.value) # 输出: {'name': '小明', 'age': 18}
💡 核心解惑:为什么存取数据需要 namespace 和 key 两个参数?
如果您之前只用过 MySQL,可能会疑惑为什么这里不用一条 SQL 语句,而是传了俩参数。
namespace(命名空间) :您可以把它完全等同于电脑里的**"多级文件夹路径"。在代码中它规定必须是一个 元组 (Tuple)**,比如("users", "Alice", "memories")。在存入底层数据库时,框架会自动把它拼成类似users/Alice/memories/这样的层级结构。key(在这里是user_id):您可以把它等同于文件夹里的**"文件名"**。- 为什么要这么设计? :有了这套"路径+文件名"的层级设计,我们不仅能精准存取某个文件,还能实现极速的**"按目录搜索"**。比如
store.search(("users", "Alice"))就能一键查出 Alice 名下的所有记忆,而不需要遍历整个数据库。这就是元组设计的精妙之处。
(2) 强大的搜索功能 (Search)
如果您忘记了具体的 user_id,你可以使用极其强大的 search 方法来进行过滤:
-
前缀搜索 :搜索某个文件夹下所有的档案
pythonitems = store.search(("users",)) # 找出 users 命名空间下的所有人 -
精确条件过滤 (Filter) :找出所有喜欢跑步的人
pythonitems = store.search(("users",), filter={"sports": "跑步"})
4.2 终极杀器:语义搜索 (Semantic Search)
长期记忆真正碾压普通数据库的地方在于,它原生支持基于向量模型(Embeddings)的语义检索。
比如,用户档案里记录了他喜欢"计算机组成原理"和"数字电路与模拟电路"。如果我们在普通数据库里搜索"数电模电",是绝对搜不到的(因为字面不匹配)。但借助语义搜索,AI 可以根据"意思相近"把档案找出来。
要实现这个功能,我们需要把原本普通的 Store 升级为向量数据库:
python
from langchain.embeddings import init_embeddings
# 1. 初始化一个专用的嵌入模型(打标机)
# 它的作用是不聊天,专门把文字翻译成一长串数组(向量)
embedding_model = init_embeddings(model="openai:text-embedding-3-large", ...)
# 2. 配置数据库的"建索引"规则 (核心!)
index_config = {
"embed": embedding_model, # 告诉数据库存数据时用哪个模型算向量
"dims": 3072, # 向量的维度(必须严格由您配置的 Embedding 模型决定,模型输出多长这里就得填多大,比如 OpenAI large 是 3072)
"fields": ["$"] # 极其重要:指定对 JSON 的哪部分做向量化。["$"] 代表把存入的整个 JSON 所有字段拼起来算向量;如果写 ["bio"],就只根据 bio 字段做语义搜索。
}
# 3. 给数据库"开光"
# 传了 index_config 之后,每次 store.put() 存数据,底层都会自动调 OpenAI 算好向量并存起来。
store = InMemoryStore(index=index_config)
# 4. 此时再进行搜索,就可以使用 query 参数进行"模糊语义搜索"了!
# 哪怕你搜的是"数电模电"这种缩写,它也能通过计算向量的余弦相似度(score)把档案找出来!
items = store.search(("users", ), query="数电模电")
print(items[0].score) # 比如 0.22(相似度得分,分数越高越相关)
长期记忆机制让大模型拥有了真正的"过目不忘"与"联想"能力。
4.3 实战:让 Agent 拥有长期记忆 (ToolRuntime 注入)
有了 Store 这个数据库之后,我们怎么让大模型去用它呢? 答案是:给大模型配两把"工具 (Tools)",一把用来存,一把用来取。
大模型本身不能直接写数据库,但我们可以把 Store 注入到工具中,让大模型自主决定什么时候调用。
(1) 定义带状态的 AgentState(为了给工具传参)
!NOTE 💡 核心洞察:为什么要扩充 AgentState?(来自您的架构思考) 除了日常的"聊天内容"之外,我们在实际业务中还需要额外传递一些核心信息 (比如用户的唯一ID、租户ID等)到底层数据库里。 为了达到这个目的,我们就需要继承并扩充默认的
AgentState,把想传的属性加进去。然后在每次invoke触发时,顺便把这些关键属性一同传过去。 这样在底层工具执行时,就能拿到这些"聊天之外的额外属性",顺利完成数据的存取,供未来随时使用!
具体原因解析: 默认的 AgentState 只包含一个 messages(聊天记录)字段。但是我们的工具去读写数据库时,必须知道当前在给哪个 user_id 干活。我们总不能指望用户在聊天里自己说"我的系统ID是user-1"吧? 所以,我们通过继承 AgentState,硬塞进去一个 user_id 字段。这样在触发大模型时,就可以把后端的 user_id 悄悄传进去,最终让底层的工具通过 runtime.state["user_id"] 顺利拿到。
!TIP 🌟 企业项目中的最佳实践 (Best Practice) 在真实的 SaaS 或企业级应用开发中,这个
CustomState通常会扮演**"全局请求上下文 (Request Context)"**的角色。除了user_id,往往还会塞入以下业务数据:
tenant_id(租户ID):如果是 ToB 的多租户系统,用于数据隔离,防止串号。session_id(业务会话):比如关联当前的某个"工单ID"或"购物车ID"。auth_token或user_profile:后端网关直接从鉴权 Token 提取好的基础身份信息。真实的调用链路往往是这样的 : 前端用户发消息 -> 后端接口拦截请求,解析 JWT 获取
user_id-> 后端代码执行agent.invoke({"messages": [...], "user_id": "解析出的ID"})-> Agent 带着这个 ID 去干活。
python
from langchain.agents import AgentState
from typing import NotRequired
class CustomState(AgentState):
user_id : NotRequired[str] # 扩展出一个 user_id 字段
# tenant_id: NotRequired[str] # 真实项目通常还会带上企业/租户标识
(2) 编写读写工具 (黑魔法:注入 ToolRuntime)
大模型在调用工具时,只会传业务逻辑参数(比如 name="小花"),它根本不知道系统底层数据库(Store)的存在。那我们的工具去哪找数据库呢? 这就用到了 ToolRuntime(工具运行时) 。只要在函数的参数里明确写上 runtime: ToolRuntime,LangGraph 框架在执行这个工具的瞬间,就会像变魔术一样,自动把当前的全局数据库对象 (runtime.store) 和 当前的状态 (runtime.state) 塞进这个参数里,供我们随意差遣:
!NOTE 💡 核心洞察:ToolRuntime 到底是什么?(来自您的架构类比) 您的类比简直绝了!
ToolRuntime完全就等同于前端 JS Canvas 或者 iOS 绘图引擎里的Context(上下文对象) 。 就像写前端画图时,系统会自动给您派发一个ctx,您不需要手动传参,就能通过ctx拿到画笔颜色、画布上下文等环境信息。 在这里,ToolRuntime就是 LangGraph 提供给底层工具的**"运行上下文"**。有了它,工具就能毫不费力地拿到数据库连接、当前会话 ID 等一切"后勤资源",从而放开手脚去干活。
python
from langgraph.prebuilt import ToolRuntime
from langchain_core.tools import tool
# 工具 1:存记忆
@tool(parse_docstring=True)
def save_user_info(name: str, runtime: ToolRuntime) -> str:
"""将客户信息保存在长期记忆中"""
namespace = ("users",)
key = runtime.state["user_id"] # 从 state 中拿到当前的 user_id
runtime.store.put(namespace, key, {"name": name}) # 写库
return "saved"
# 工具 2:取记忆
@tool(parse_docstring=True)
def get_user_info(runtime: ToolRuntime) -> str:
"""从长期记忆中读取客户的信息"""
namespace = ("users",)
key = runtime.state["user_id"]
item = runtime.store.get(namespace, key) # 读库
return str(item.value) if item else "unknown"
(3) 创建并测试跨会话 Agent
只要我们在 create_agent 时,把 store 和工具都塞进去,并写好系统提示词,大模型就能全自动管理它的记忆库了。 而且最牛的是,即使每次聊天都不传 thread_id(意味着每次都是全新失忆的局),大模型也能通过工具从全局数据库里把记忆捞回来!
python
agent = create_agent(
model=model,
tools=[save_user_info, get_user_info],
store=store, # 挂载全局长期数据库
state_schema=CustomState, # 挂载自定义状态
system_prompt="用户提及个人信息时,可以使用工具保存。如果询问个人信息,尝试读取。"
)
# 场景 1:在这个全新的局里告诉它我是小花
agent.invoke({
"messages": ["你好,很高兴认识你,我是小花"],
"user_id": "user-1"
})
# AI的内部动作:分析出要存数据 -> 调用 save_user_info("小花") -> 落库
# 场景 2:换一个完全没有上下文的全新局,甚至不传 thread_id
agent.invoke({
"messages": ["我是谁"],
"user_id": "user-1"
})
# AI的内部动作:发现回答不上来 -> 调用 get_user_info() -> 从库里查出 {"name": "小花"} -> 回答:"你是小花。"
通过这种**"把数据库封装成 Tool 交给大模型自己调度"的设计,AI 终于从一个"记性只能靠上下文硬撑"的聊天机器人,进化成了一个拥有真正"长线外接大脑"**的智能体!