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模型调用费用 |
向量记忆的局限
虽然向量记忆解决了上下文膨胀的问题,但它还有一个致命缺陷:记忆存在内存中,程序重启就丢失了。
如果你想实现跨会话的持久化记忆——比如用户今天关闭程序,明天再来还能记得之前的对话——那就需要把向量存到数据库中。
这正是下一篇文章要解决的问题。
小结
这篇文章我们学习了:
- 大模型的无状态本质:每次请求都是独立的,"记忆"需要靠我们自己维护
- 简单记忆模式:全量拼接历史,简单但有token爆炸风险
- 向量记忆模式:Embedding向量化 + 余弦相似度检索,智能筛选相关上下文
IEmbeddingGenerator和TensorPrimitives.CosineSimilarity的使用- TopK + 阈值的检索策略
下一篇,我们将把向量记忆持久化到Redis,实现跨会话的长期记忆。
完整代码见项目:AIMemoryConsole2/Program.cs

浙公网安备 33010602011771号