02-让AI拥有记忆-对话上下文的两种实现

让AI拥有记忆——对话上下文的两种实现

这是《AI开发解密》系列的第二篇。我们将探索如何让AI"记住"之前的对话,从暴力拼接历史到智能向量检索,逐步构建真正有记忆力的AI助手。

引言

本文完整代码在 AIMemoryConsole2 项目中,克隆仓库后配置好API Key直接运行即可体验效果。
项目地址:https://github.com/MapleWithoutWords/AIStudyDemos

在上一篇文章中,我们实现了基础的AI对话。但你可能已经发现一个问题:每次对话都是独立的,AI完全不知道之前聊过什么

这不是bug,这是大模型的本质——它是无状态的。每次调用都是一次独立的请求,模型并不会"记住"你之前说过什么。

那ChatGPT等产品是怎么做到"有记忆"的?答案很简单:每次请求都把历史对话带上

本文将介绍两种记忆模式,从简单到智能,带你理解AI记忆的核心原理。

大模型的"失忆症"

先看一个场景:

你: 我叫张三
AI: 你好张三!

你: 我叫什么名字?
AI: 抱歉,我不知道你的名字,请告诉我。

这是因为第二次请求时,AI只收到了"我叫什么名字?"这一条消息,之前的对话已经"丢失"了。

解决方案很直接:把历史对话一起发给模型。但"怎么发"就有讲究了。

方式一:简单记忆——全量拼接历史

最直接的思路:把所有历史消息都塞进每次请求的messages里。

List<ChatMessage> chatMessages =
[
    new ChatMessage(ChatRole.System, "你是一个有用的AI助手,请用中文回答用户的问题。")
];

while (true)
{
    Console.Write("\n你: ");
    var userInput = Console.ReadLine();
    
    // 将用户消息加入历史
    chatMessages.Add(new ChatMessage(ChatRole.User, userInput));

    StringBuilder sb = new StringBuilder();
    await foreach (var update in client.GetStreamingResponseAsync(chatMessages))
    {
        foreach (var item in update.Contents)
        {
            if (item is TextContent text)
            {
                Console.Write(text.Text);
                sb.Append(text.Text);
            }
        }
    }
}

注意这里的关键:chatMessages是一个持续累积的列表,每次对话的User消息和Assistant回复都会被追加进去,下一次请求时全部发送给模型。

效果验证

你: 我叫张三
AI: 你好张三!

你: 我叫什么名字?
AI: 你叫张三呀!

AI"记住"了!但这种方式有明显的局限性:

简单记忆的问题

问题 说明
Token消耗爆炸 对话越久,每次请求携带的token越多,费用越高
超出上下文窗口 模型有最大token限制(如8K、32K),超过就会报错
信息噪音 无关的历史对话会干扰模型的回答质量

想象一下,如果你们聊了100轮,每次请求都要带上所有历史——这就是为什么我们需要更聪明的方案。

方式二:向量记忆——智能检索相关上下文

核心思想:不是所有历史都相关,只把与当前问题最相关的历史发给模型

那怎么判断"相关"呢?答案是:把语义变成向量,用数学计算相似度

Embedding:把文字变成向量

Embedding模型的作用是将一段文本转换为一个高维浮点数组(向量)。语义相近的文本,其向量在空间中的距离也越近。

IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator =
    new OpenAI.Embeddings.EmbeddingClient(
        embeddingModel, 
        new ApiKeyCredential(apiKey), 
        new OpenAI.OpenAIClientOptions { Endpoint = new Uri(baseUrl) })
    .AsIEmbeddingGenerator();

我们使用embedding-3模型,它会生成2048维的浮点向量。

余弦相似度:衡量语义距离

有了向量,我们需要一种方法来衡量两个向量的"相似程度"。余弦相似度是最常用的方法,值域为[-1, 1],值越接近1表示语义越相似。

.NET提供了高性能的TensorPrimitives.CosineSimilarity方法:

float similarity = TensorPrimitives.CosineSimilarity(
    new ReadOnlySpan<float>(record.Embedding),
    querySpan);

这里使用ReadOnlySpan<float>可以获得接近零拷贝的高性能计算。

向量记忆的完整流程

用户输入
  ↓
Embedding模型 → 生成查询向量
  ↓
计算查询向量与所有历史记录的余弦相似度
  ↓
过滤:相似度 >= 阈值(如0.3)
  ↓
排序:按相似度降序,取TopK条(如6条)
  ↓
构建消息:System提示 + 筛选后的相关历史 + 当前用户消息
  ↓
发送给大模型 → 流式输出回答
  ↓
将本轮对话(用户消息+AI回复)的向量和文本存入记忆

核心实现:选择相关历史

record ChatMessageRecord(ChatMessage Message, float[] Embedding);

List<ChatMessageRecord> SelectRelevantMessages(
    List<ChatMessageRecord> allMessages, 
    float[] queryVector, 
    int topK, 
    float threshold)
{
    if (allMessages.Count == 0) return [];

    ReadOnlySpan<float> querySpan = queryVector;

    // 计算每条历史记录与当前查询的余弦相似度
    var scored = new List<(ChatMessageRecord Record, float Score)>();
    foreach (var record in allMessages)
    {
        float similarity = TensorPrimitives.CosineSimilarity(
            new ReadOnlySpan<float>(record.Embedding),
            querySpan);
        if (similarity >= threshold)
        {
            scored.Add((record, similarity));
        }
    }

    // 按相似度降序排列,取TopK
    return scored
        .OrderByDescending(x => x.Score)
        .Take(topK)
        .Select(x => x.Record)
        .ToList();
}

完整的向量记忆对话

// 存储所有对话记录及其对应的向量
List<ChatMessageRecord> allMessages = [];

const int topK = 6;             // 最多选取6条最相关的历史对话
const float threshold = 0.3f;   // 余弦相似度阈值

// 1. 为当前用户输入生成向量
var userEmbedding = await embeddingGenerator.GenerateAsync(userInput);
float[] userVector = userEmbedding.Vector.ToArray();

// 2. 计算与所有历史对话的余弦相似度,选出最相关的
var relevantMessages = SelectRelevantMessages(allMessages, userVector, topK, threshold);

// 3. 构建本次发送给AI的消息列表
List<ChatMessage> chatMessages =
[
    new ChatMessage(ChatRole.System,
        "你是一个有用的AI助手,请用中文回答用户的问题。" +
        "下面是与用户当前问题相关的历史对话记录,请参考这些上下文来回答。")
];

foreach (var record in relevantMessages)
{
    chatMessages.Add(record.Message);
}
chatMessages.Add(new ChatMessage(ChatRole.User, userInput));

// 4. 调用AI(流式输出)...

// 5. 将本轮对话存入历史记录(包含向量)
allMessages.Add(new ChatMessageRecord(
    new ChatMessage(ChatRole.User, userInput),
    userEmbedding.Vector.ToArray()));

if (sb.Length > 0)
{
    var aiText = sb.ToString();
    var aiEmbedding = await embeddingGenerator.GenerateAsync(aiText);
    allMessages.Add(new ChatMessageRecord(
        new ChatMessage(ChatRole.Assistant, aiText),
        aiEmbedding.Vector.ToArray()));
}

注意第5步:AI的回复也要生成向量并存入记忆。因为下一轮对话中,用户的问题可能与AI之前的回复内容相关。

两种模式对比

维度 简单记忆 向量记忆
实现复杂度 ⭐ 极低 ⭐⭐⭐ 中等
Token消耗 随对话轮数线性增长 稳定,每次最多TopK条
记忆质量 全量保留,但有噪音 精准检索,语义相关
上下文窗口 容易超限 几乎不会超限
适用场景 简短对话(<10轮) 长期对话、主题切换频繁
额外成本 Embedding模型调用费用

向量记忆的局限

虽然向量记忆解决了上下文膨胀的问题,但它还有一个致命缺陷:记忆存在内存中,程序重启就丢失了

如果你想实现跨会话的持久化记忆——比如用户今天关闭程序,明天再来还能记得之前的对话——那就需要把向量存到数据库中。

这正是下一篇文章要解决的问题。

小结

这篇文章我们学习了:

  1. 大模型的无状态本质:每次请求都是独立的,"记忆"需要靠我们自己维护
  2. 简单记忆模式:全量拼接历史,简单但有token爆炸风险
  3. 向量记忆模式:Embedding向量化 + 余弦相似度检索,智能筛选相关上下文
  4. IEmbeddingGeneratorTensorPrimitives.CosineSimilarity 的使用
  5. TopK + 阈值的检索策略

下一篇,我们将把向量记忆持久化到Redis,实现跨会话的长期记忆。


完整代码见项目:AIMemoryConsole2/Program.cs

项目地址:https://github.com/MapleWithoutWords/AIStudyDemos

posted @ 2026-06-27 17:28  隔壁老黎  阅读(4)  评论(0)    收藏  举报