10.记忆 (Memory)

1. 为什么需要记忆 (Memory)?

在 AI 应用开发中,我们必须面对一个残酷的真相:大语言模型(LLM)本身是一个"金鱼脑",它没有任何记忆功能。

大模型的底层 API(如 OpenAI 的接口)本质上是**无状态(Stateless)**的。每一次 invoke 调用对它来说都是一次完全独立的、全新的运算。它绝对记不住上一秒你们聊了什么。

比如:

  • 你问:"你好,我叫小明。"
  • AI 答:"很高兴认识你,小明。"
  • 你紧接着问:"我叫什么名字?"
  • AI 答:"我不知道你的名字,除非你告诉我。"

为了让 AI 看起来像是一个有记忆、能连贯交流的"人",我们就必须引入**记忆(Memory)**机制。

记忆的核心本质

所谓的"记忆",并不是大模型自己主动回想起来的,而是 Agent 框架(或客户端)在后台默默地把历史聊天记录存了下来。等用户下一次提问时,框架会把这些历史记录打包,连同新问题一起再次"喂"给大模型。大模型是看着你给的历史档案,才"假装"它记得你。

因此,我们需要系统的记忆管理机制来解决以下痛点:

  1. 告别手动拼接 :如果不借助框架,开发者每次都需要手动维护并拼接长长的 messages 数组,代码极其恶心。
  2. 防爆防超载(Token 管理):历史对话如果无限累加,很快就会撑爆大模型的上下文窗口,不仅报错还极度费钱。
  3. 跨会话持久化:普通的数组存在内存里,一旦重启或换了会话就清零了。我们需要机制让历史信息在不同场景下都能被利用。

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,框架就会自动在底层执行复杂的 SELECTINSERT 甚至乐观锁校验。在你写代码的视角里,你甚至感受不到底层数据库的存在,这就是好框架的威力。

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(状态快照机制),运行原理可以概括为以下四步:

  1. State Snapshot(拍快照) :在 Agent 执行的每一个关键节点(比如大模型刚生成完一条回复,或者某个 Tool 工具刚执行完),框架都会把当前整个 Agent 的内部状态(包括庞大的 messages 数组,以及各类元数据)"咔嚓"拍一张完整的快照 (Snapshot)。
  2. Serialization(序列化打包) :内存里的复杂 Python 对象是没法直接塞进关系型数据库的。框架底层会自动把这个庞大的快照对象,压缩、序列化成一段特定的二进制数据流或 JSON 字符串(通常使用 msgpackpicklejson 协议)。
  3. Storage(入库存储)
    • 对于 InMemorySaver :它仅仅是把打包好的数据,塞进一个全局的 Python 字典里,键 (Key) 就是你的 thread_id
    • 对于 PostgresSaver :它会把这段二进制数据通过一条底层的 INSERT 语句,存进 Postgres 数据库的 checkpoint_blobs 表中,主键同样是你的 thread_id 加上当前快照的唯一 ID (checkpoint_id)。
  4. 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。如果放任不管,历史记录就会多达上千条,这会带来致命后果:

  1. Token 撑爆:超出了大模型一次能处理的极限,直接报错死机。
  2. 极度费钱:每次都要带着前几千句话当上下文,烧钱速度惊人。
  3. 模型变笨:上下文越长,大模型的注意力越分散。

因此,我们必须对这日益庞大的消息进行治理(减肥)。LangChain 提供了三种经典的策略,但正如您所分析的,前两种(裁剪/删除)其实都不太好,因为**"它们有可能会把重要的历史东西给丢掉"**,只有第三种(摘要)才是最优雅的解法。

3.1 策略一:消息裁剪 (Message Trimming)

  • 做法 :在发送给大模型之前 (通常用 @before_model 中间件),硬性规定一个长度上限。比如"永远只给大模型看最开始的系统设定(System Prompt)和最近的 4 句话,中间的全扔掉"。

  • 代码实现

    python 复制代码
    from 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 条,就把最老的那几条单独标记删除。

  • 代码实现

    python 复制代码
    from 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 为此提供了开箱即用的中间件!

    python 复制代码
    from 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}"
            )
        ]
    )
  • 底层神仙操作

    1. 当您疯狂发了几万字的废话后,触发 tokens > 1000 的条件。
    2. SummarizationMiddleware 在后台偷偷召唤 gpt-4o-mini,把废话浓缩成:"用户叫张三,是个话痨工程师。"
    3. 系统悄悄把那几万字废话从 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}

💡 核心解惑:为什么存取数据需要 namespacekey 两个参数?

如果您之前只用过 MySQL,可能会疑惑为什么这里不用一条 SQL 语句,而是传了俩参数。

  • namespace(命名空间) :您可以把它完全等同于电脑里的**"多级文件夹路径"。在代码中它规定必须是一个 元组 (Tuple)**,比如 ("users", "Alice", "memories")。在存入底层数据库时,框架会自动把它拼成类似 users/Alice/memories/ 这样的层级结构。
  • key(在这里是 user_id:您可以把它等同于文件夹里的**"文件名"**。
  • 为什么要这么设计? :有了这套"路径+文件名"的层级设计,我们不仅能精准存取某个文件,还能实现极速的**"按目录搜索"**。比如 store.search(("users", "Alice")) 就能一键查出 Alice 名下的所有记忆,而不需要遍历整个数据库。这就是元组设计的精妙之处。

(2) 强大的搜索功能 (Search)

如果您忘记了具体的 user_id,你可以使用极其强大的 search 方法来进行过滤:

  • 前缀搜索 :搜索某个文件夹下所有的档案

    python 复制代码
    items = store.search(("users",)) # 找出 users 命名空间下的所有人
  • 精确条件过滤 (Filter) :找出所有喜欢跑步的人

    python 复制代码
    items = 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,往往还会塞入以下业务数据:

  1. tenant_id (租户ID):如果是 ToB 的多租户系统,用于数据隔离,防止串号。
  2. session_id (业务会话):比如关联当前的某个"工单ID"或"购物车ID"。
  3. auth_tokenuser_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 终于从一个"记性只能靠上下文硬撑"的聊天机器人,进化成了一个拥有真正"长线外接大脑"**的智能体!

相关推荐
Raistwen1 小时前
Agent架构师从0出发(第1篇):Python环境与依赖管理的N个致命陷阱 —— 一个Java架构师的AI入门血泪史
agent
冻感糕人~2 小时前
大模型学习指南:收藏这份AI Agent四层工程地图(小白程序员必备)
java·大数据·人工智能·学习·大模型·agent·大模型学习
深念Y5 小时前
AI编程Agent工具定义对比分析
agent·ai编程·开源项目·工具·tool·hermes·ccsiwtch
tkevinjd13 小时前
MiniCode 项目详解6:原项目控制系统的10个缺陷(已修复)
python·llm·agent
smartfish_liu16 小时前
分享: 如何利用workbuddy来构建批量自动化任务.
llm·agent
oil欧哟18 小时前
我做了一个 Vibe Coding 术语学习站:VibeHub
前端·ai·agent·独立开发·vibe coding
敬叫唤18 小时前
基于历史记忆&场景融入Bugfix流程
agent·loop·skill
思考着亮19 小时前
9.中间件 (Middleware)
agent