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 往往会组合使用最近消息、摘要和检索。
相关推荐
ShallWeL8 分钟前
【机器学习】(16)—— 数值数据
人工智能·python·算法·机器学习·数据分析
gr95ZS6E615 分钟前
基础入门java安全(一)--CC1基础分析
开发语言·python
极度的坦诚就是无坚不摧18 分钟前
数据分析DAY1-Python基础
python·数据分析
智码看视界23 分钟前
Day19 Spring Boot启动优化:启动时间从8秒降到2秒
java·spring boot·后端·启动优化
hongyucai33 分钟前
详解rlinf强化学习四步曲
人工智能·python·算法·架构
c_lb728833 分钟前
2026年下半年AI量化工具,阶段不同重点也不同
人工智能·python
IT_陈寒37 分钟前
React中useEffect的依赖项把我坑惨了
前端·人工智能·后端
Xpower 171 小时前
详细解释 Codex 最近一次功能更新,以及 GPT-5.6 Sol、Terra、Luna 的能力、价格、使用方式和适用场景。
人工智能·python·gpt·学习
SQL-First布道者1 小时前
Spring JDBC Ultra 十边型战士
java·spring boot·后端·mysql·spring·mybatis
Dovis(誓平步青云)1 小时前
《精讲Spring Boot后端跑另一个端口:如何解决暴露服务器问题》
服务器·人工智能·spring boot·后端·生成对抗网络