Agent 记忆管理

模型本身是无状态的。

这一次调用和下一次调用之间,模型不会自动记住你之前说过什么。它看起来"有记忆",通常是因为 Agent 在每次调用模型前,把历史消息重新组织成上下文发给了模型。

图里的关键点是:Agent 一直在收集用户消息、模型回复、工具结果,然后把这些消息组成队列。模型每次回答时,看到的是这次传进去的完整上下文,而不是它自己真的保存了记忆。

先记住一句话:Agent 的 Memory 本质上是消息管理,不是模型真的拥有记忆。

为什么需要 Memory

如果你连续问两句话:

txt 复制代码
用户:我叫张三。
用户:我叫什么?

第二次调用模型时,如果你只发送"我叫什么?",模型并不知道"张三"这个信息。

要让模型回答出来,就必须把前面的消息一起发过去:

txt 复制代码
HumanMessage("我叫张三")
AIMessage("你好张三")
HumanMessage("我叫什么?")

所以 Memory 要解决两个问题:

  • 历史消息存在哪里。
  • 每次调用模型时,应该带上哪些历史消息。

最简单的消息历史

最基础的 Memory 就是一个列表。

python 复制代码
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage


messages = [
    SystemMessage(content="你是一个记得用户偏好的学习助手。"),
]


def chat(input_text: str) -> str:
    # 1. 把本轮用户输入追加到消息列表
    messages.append(HumanMessage(content=input_text))

    # 2. 把完整 messages 发给模型
    response = model.invoke(messages)

    # 3. 把模型回复也存下来,供下一轮使用
    messages.append(response)

    return str(response.content)


chat("我叫张三,喜欢 Python。")
chat("我喜欢什么语言?")

这就是最小可用的短期记忆。

优点是简单,缺点也明显:

  • 进程结束后就丢失。
  • 消息越来越多,迟早超过模型上下文窗口。
  • 所有历史都塞给模型,成本会越来越高。

持久化消息历史

如果希望重启程序后还能继续对话,就需要把消息保存到外部存储。

最简单的方式是保存到 JSON 文件。

python 复制代码
import json
from pathlib import Path
from typing import Literal, TypedDict

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage


history_file = Path("chat_history.json")


class StoredMessage(TypedDict):
    role: Literal["human", "ai"]
    content: str


def load_history() -> list[StoredMessage]:
    try:
        # 1. 从文件读取历史消息
        return json.loads(history_file.read_text(encoding="utf-8"))
    except FileNotFoundError:
        # 2. 文件不存在时,从空历史开始
        return []


def save_history(history: list[StoredMessage]) -> None:
    # 3. 每轮对话后写回文件
    history_file.write_text(
        json.dumps(history, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

# 消息分类
def to_langchain_messages(history: list[StoredMessage]):
    result = []

    for message in history:
        if message["role"] == "human":
            result.append(HumanMessage(content=message["content"]))
        else:
            result.append(AIMessage(content=message["content"]))

    return result


def chat(input_text: str) -> str:
    # 加载历史消息
    history = load_history()
    # 添加本轮消息
    history.append({"role": "human", "content": input_text})
	
	# 组装消息
    messages = [
        SystemMessage(content="你是一个能基于历史对话回答问题的助手。"),
        *to_langchain_messages(history),
    ]

    response = model.invoke(messages)

    history.append({"role": "ai", "content": str(response.content)})
    save_history(history)

    return str(response.content)

文件只是一个例子。

真实项目里,Memory 可以存在:

  • 文件系统:适合本地工具和单机应用。
  • Redis:适合临时会话和分布式服务。
  • 数据库:适合长期保存和复杂查询。
  • 向量数据库:适合按语义找相关历史。

为什么不能一直追加

模型有上下文窗口限制。

如果每一轮都把全部历史消息发给模型,会遇到三个问题:

  • 超过上下文长度,模型调用失败。
  • token 成本越来越高。
  • 很早之前的无关信息会干扰当前回答。

所以 Memory 管理的重点不是"无限保存",而是"选择哪些历史应该进入本次上下文"。

常见策略有三种:

txt 复制代码
截断:只保留最近消息(数量、长度)
总结:把旧消息压缩成摘要
检索:根据当前问题找相关历史

按数量截断

最简单的策略是只保留最近 N 条消息。

python 复制代码
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage


history: list[BaseMessage] = []


def get_recent_messages(max_messages: int) -> list[BaseMessage]:
    # 保留最后 max_messages 条消息
    return history[-max_messages:]


def chat_with_count_limit(input_text: str) -> str:
    history.append(HumanMessage(content=input_text))

    messages = [
        SystemMessage(content="你是一个简洁的助手。"),

        # 只把最近 6 条消息发送给模型
        *get_recent_messages(6),
    ]

    response = model.invoke(messages)
    history.append(response)

    return str(response.content)

这种方式很好理解,但它只按消息条数处理,不关心每条消息有多长。

如果某条消息特别长,仍然可能撑爆上下文。

按长度截断

更稳一点的方式是按内容长度控制。

这里先用字符数演示思想;生产环境里可以换成模型对应的 token 计算方式。

python 复制代码
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage


def get_message_text(message: BaseMessage) -> str:
    # message.content 可能不是纯字符串,统一转成可计数文本
    return message.content if isinstance(message.content, str) else str(message.content)


# 从后往前保留最近消息(不超过 max_chars)
def trim_by_char_length(messages: list[BaseMessage], max_chars: int) -> list[BaseMessage]:
    result: list[BaseMessage] = []
    total_chars = 0

    # 从后往前保留最近消息
    for message in reversed(messages):
        length = len(get_message_text(message))

        if total_chars + length > max_chars:
            break

        result.insert(0, message)
        total_chars += length

    return result


def chat_with_length_limit(input_text: str) -> str:
    history.append(HumanMessage(content=input_text))

    messages = [
        SystemMessage(content="你是一个简洁的助手。"),
        *trim_by_char_length(history, 2000),
    ]

    response = model.invoke(messages)
    history.append(response)

    return str(response.content)

按长度截断比按条数更接近真实 token 控制。

但它仍然会丢掉旧信息,比如用户一开始说过的名字、偏好、项目背景。

总结旧消息

总结策略的思路是:旧消息不直接丢掉,而是先压缩成摘要。

python 复制代码
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, get_buffer_string


def summarize_messages(messages: list[BaseMessage]) -> str:
    if not messages:
        return ""

    # 1. 把消息列表转成适合总结的对话文本
    conversation_text = get_buffer_string(
        messages,
        human_prefix="用户",
        ai_prefix="助手",
    )

    # 2. 让模型总结旧对话里的稳定信息
    summary_response = model.invoke(
        [
            SystemMessage(
                content=f"""请总结以下对话,保留用户身份、偏好、任务背景和重要结论:

{conversation_text}

总结:"""
            )
        ]
    )

    return str(summary_response.content)


def compress_history(max_messages: int = 8, keep_recent: int = 3) -> None:
    if len(history) <= max_messages:
        return

    # 3. 最近几条消息保留原文
    recent_messages = history[-keep_recent:]

    # 4. 更早的消息压缩成摘要
    old_messages = history[:-keep_recent]
    summary = summarize_messages(old_messages)

    history.clear()

    # 5. 摘要也作为一条消息放回历史
    history.append(AIMessage(content=f"历史摘要:{summary}"))
    history.extend(recent_messages)

总结适合保存长期背景:

  • 用户叫什么。
  • 用户正在做什么项目。
  • 用户偏好的回答风格。
  • 前面已经达成的结论。

它的缺点是摘要可能丢细节,也可能总结错。

所以常见做法是:摘要保留长期信息,最近几轮保留原文。

检索向量数据库

有些历史消息不适合一直放在上下文里,但也不应该彻底丢掉。

这时可以把历史消息保存到可检索的存储里,当前问题来了之后,只取最相关的几条。

python 复制代码
from dataclasses import dataclass
from datetime import datetime
from uuid import uuid4

from langchain_core.messages import HumanMessage, SystemMessage


@dataclass
class MemoryRecord:
    id: str
    text: str
    created_at: str


memory_store: list[MemoryRecord] = []


def save_memory(user_input: str, ai_output: str) -> None:
    # 真实项目里可以在这里生成 embedding,并写入向量数据库
    memory_store.append(
        MemoryRecord(
            id=str(uuid4()),
            text=f"用户:{user_input}\n助手:{ai_output}",
            created_at=datetime.now().isoformat(),
        )
    )


def retrieve_memory(query: str, limit: int = 3) -> list[MemoryRecord]:
    # 这里用字符串包含演示检索思想;真实项目里应替换成向量检索(向量数据库 milvus 等)
    return [item for item in memory_store if query in item.text][:limit]


def chat_with_retrieval(input_text: str) -> str:
    # 1. 根据当前问题找相关历史
    related_memories = retrieve_memory(input_text)

    memory_text = "\n\n".join(
        f"历史 {index + 1}:\n{item.text}"
        for index, item in enumerate(related_memories)
    )

    # 2. 只把相关历史放进本次上下文
    messages = [
        SystemMessage(
            content=(
                "你是一个能利用相关历史回答问题的助手。"
                + (f"\n相关历史:\n{memory_text}" if memory_text else "")
            )
        ),
        HumanMessage(content=input_text),
    ]

    response = model.invoke(messages)

    # 3. 保存本轮对话,供未来检索
    save_memory(input_text, str(response.content))

    return str(response.content)

检索式记忆适合:

  • 历史很多,不可能全部放进上下文。
  • 当前问题只和一小部分历史相关。
  • 需要跨会话记住用户偏好、项目事实、长期背景。

真实实现通常会用 embedding + 向量数据库。

这部分和 RAG 思路接近,只是检索对象从"知识文档"换成了"历史对话"。

怎么选策略

可以按复杂度逐步升级:

txt 复制代码
小 demo:消息列表
临时会话:内存历史 + 最近 N 条
本地工具:文件持久化 + 截断
长期助理:总结 + 最近消息
大量历史:总结 + 向量检索

一般不要只用一种方式。

更常见的组合是:

  • 最近几轮保留原文。
  • 更早的对话压缩成摘要。
  • 长期事实写入可检索存储。

这样既能控制上下文长度,又不至于丢掉重要信息。

小结

这一篇要记住几个核心点:

  1. 模型本身无状态,Memory 是 Agent 管理 messages 的结果。
  2. 最简单的 Memory 是消息列表。
  3. 需要跨会话时,要把历史消息持久化到文件、Redis 或数据库。
  4. 上下文有限,所以不能无限追加历史。
  5. 截断适合控制长度,但会丢旧信息。
  6. 总结适合压缩旧对话,但可能丢细节。
  7. 检索式记忆适合大量历史和长期偏好。
  8. 实际 Agent 往往会组合使用最近消息、摘要和检索。
相关推荐
葫芦和十三8 小时前
图解 MongoDB 02|BSON:你以为存的是 JSON,其实是带类型的二进制
后端·mongodb·agent
葫芦和十三8 小时前
图解 MongoDB 01|文档数据库
后端·mongodb·agent
runnerdancer9 小时前
LLM是怎么处理messages数组的,提示词缓存又是什么
前端·agent
陈随易10 小时前
VSCode的Copilot扩展支持接入DeepSeek,Kimi了!
前端·后端·程序员
我不是外星人11 小时前
有了 Harness Engineering ,真的还需要研发工程师吗?
前端·后端·ai编程
冬奇Lab11 小时前
Agent 系列(23):Web Agent——让 Agent 真正浏览网页
人工智能·llm·agent
candyTong12 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
程序员小假13 小时前
Agent 推理太慢?从同步阻塞到异步事件驱动的架构演进指南
agent