MAF Agent 初学者笔记:消息历史、持久化恢复与自定义历史提供者
这份笔记是面向 刚开始学习 Agent 的同学 整理的入门版学习资料
一、这一部分到底在学什么?
当我们开始做 Agent 时,很快就会遇到一个关键问题:
Agent 为什么能记住前面说过的话?
以及进一步的问题:
这些对话历史到底存在哪里?
程序一重启,历史还在吗?
多台服务器部署时,历史怎么共享?
如果默认存储不够用,能不能自己实现一套?
所以这几节课,本质上是在回答 3 个核心问题:
消息历史怎么管理?
对话怎么持久化与恢复?
如何自定义历史存储方案?
你可以把它理解为:
前面我们在学“怎么聊天”,现在开始学“聊天记录怎么管”。
二、整体知识地图
这一部分内容可以按下面的顺序理解:
1. 第一层:消息历史管理
核心角色是:
ChatHistoryProviderInMemoryChatHistoryProvider
它解决的是:
Agent 在一次次对话中,如何拿到历史消息、如何保存新消息。
2. 第二层:对话持久化与恢复
核心 API 是:
await agent.SerializeSessionAsync(session)await agent.DeserializeSessionAsync(jsonElement)
它解决的是:
如何把一个 Session 保存下来,并在之后恢复出来继续使用。
3. 第三层:自定义历史提供者
核心思路是:
继承
ChatHistoryProvider重写:
ProvideChatHistoryAsync(...)StoreChatHistoryAsync(...)
它解决的是:
当默认的内存方案不够用时,我们如何接入文件、Redis、数据库等外部存储。
三、先建立最重要的认知:消息历史到底存在哪里?
答案是:
消息历史由 ChatHistoryProvider 管理
在 MAF 中,Agent 的对话历史不是你手动存进去的,而是由一个专门的组件负责管理:
ChatHistoryProvider
它的职责非常清晰:
在对话开始前,提供历史消息
在对话结束后,保存本轮新消息
默认情况下,用的是 InMemoryChatHistoryProvider
也就是说,默认实现是:
InMemoryChatHistoryProvider
它的特点是:
历史记录放在内存里
速度很快
不需要额外配置
但程序一重启,数据就没了
消息状态实际存放位置
课程里提到一个很关键的点:
消息状态存储在
AgentSession.StateBag中。
你可以把 AgentSession 想成“一次会话容器”,而 StateBag 就像这个容器里的“状态抽屉”。
默认历史提供者会把消息列表放进这个抽屉里。
四、ChatHistoryProvider 是什么?为什么它这么重要?
1. 它是一个抽象基类
ChatHistoryProvider 不是某一种具体存储,而是一种统一规范。
也就是说,它先定义一套标准流程:
什么时候读取历史
什么时候保存历史
外部怎么访问它
这样后面不管你用的是:
内存
文件
Redis
SQL 数据库
Cosmos DB
都可以遵守同一套接口规范。
2. 它的核心方法
InvokingAsync(...)
作用:
在 Agent 开始调用前,提供历史消息。
ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context)
简单理解:
用户发了一条新消息
Agent 要组织上下文去调用模型
这时先从历史提供者里把过去的对话取出来
InvokedAsync(...)
作用:
在 Agent 调用结束后,保存本轮产生的新消息。
ValueTask InvokedAsync(InvokedContext context)
简单理解:
用户问了问题
Agent 回答了内容
这一问一答要写入历史中
GetService<TService>()
作用:
获取强类型服务。
这个方法很多初学者一开始不太在意,但很实用。
比如你可以这样拿到历史提供者:
ChatHistoryProvider? historyProvider = agent.GetService<ChatHistoryProvider>();
3. 为什么设计成抽象类?
因为官方知道,真实项目里的历史存储需求差异很大。
比如:
Demo 阶段:内存就够了
单机程序:文件存储可能够用
企业系统:通常要数据库
分布式部署:往往要 Redis + DB
所以它不把方案写死,而是给你留出扩展能力。
这就是抽象类设计的意义:
统一入口,允许多种实现。
五、默认实现:InMemoryChatHistoryProvider
这是初学者最先接触到的实现。
1. 它是什么?
InMemoryChatHistoryProvider 是 MAF 默认的消息历史提供者。
当你调用:
var agent = chatClient.AsAIAgent(...);
通常框架就会自动帮你创建一个默认的 InMemoryChatHistoryProvider。
也就是说:
你之所以不写任何历史管理代码也能多轮对话,是因为框架已经在背后替你做了。
2. 它的核心特性
状态存在内存里
消息历史保存在 AgentSession.StateBag 中。
性能高
因为没有磁盘 I/O、没有数据库访问,所以读写非常快。
支持裁剪
它可以配合 ChatReducer 使用,对过长历史进行裁剪,防止上下文无限膨胀。
不持久化
最大缺点也很明显:
程序一重启,历史就没了。
3. 内部状态结构
课程中给了一个内部 State 类:
public sealed class State
{
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = [];
}
这说明一件事:
默认内存历史提供者,本质上就是维护了一个
List<ChatMessage>。
也就是一串消息列表。
六、如何拿到 ChatHistoryProvider?
在实际开发里,我们常常需要手动查看历史消息。
这时候就可以从 Agent 上获取:
ChatHistoryProvider? historyProvider = agent.GetService<ChatHistoryProvider>();
一个完整的思路如下:
AgentSession session = await agent.CreateSessionAsync();
ChatHistoryProvider? historyProvider = agent.GetService<ChatHistoryProvider>();
你通常会拿它来做什么?
1. 查看当前历史提供者的类型
判断当前是不是默认的内存实现。
2. 读取消息历史
看看当前 Session 已经存了哪些消息。
3. 做调试与分析
比如:
消息总数
用户消息数
Assistant 消息数
历史增长速度
七、访问消息历史的两种方式
这部分非常重要,初学者经常会混淆。
方式一:通用方式 InvokingAsync(...)
var messages = await historyProvider.InvokingAsync(new InvokingContext(agent, session, []));
这是最通用的方式。
它的特点:
面向所有
ChatHistoryProvider实现不依赖具体类型
更符合抽象设计思维
会经过框架统一处理逻辑
适合什么时候用?
当你写的是通用代码,或者你不想和某个具体实现绑定时,用它最稳妥。
方式二:直接访问 InMemoryChatHistoryProvider
如果你已经明确知道当前用的是默认内存实现,就可以强转:
if (historyProvider is InMemoryChatHistoryProvider inMemoryProvider)
{
var messageList = inMemoryProvider.GetMessages(session);
}
它的特点:
更直接
更方便调试
可以直接拿到底层消息列表
但要注意:
这种方式只适用于内存实现,不具备通用性。
初学者该怎么选?
老师式建议:
写学习代码、调试代码时:可以用
GetMessages(session),更直观写正式代码、可扩展代码时:优先用
InvokingAsync(...)
一句话总结:
GetMessages更方便,InvokingAsync更规范。
八、InMemoryChatHistoryProviderOptions 怎么理解?
内存历史提供者可以通过配置项来自定义行为。
重点理解下面几项就够了。
1. ChatReducer
作用:
控制消息历史如何裁剪。
如果对话轮数很多,历史会越来越长,最终可能导致模型上下文超限。
这时候就需要 Reducer。
例如:
ChatReducer = new TokenCountingChatReducer(maxTokens: 4000)
意思是:
按 Token 数量限制历史长度
超过范围就进行裁剪
2. ReducerTriggerEvent
作用:
指定什么时候触发裁剪。
两种常见时机:
BeforeMessagesRetrieval
在获取消息前裁剪。
这通常是默认方式。
好处是:
存储里保留完整消息
真正提供给模型前再裁剪
AfterMessageAdded
在消息写入后立刻裁剪。
好处是:
存储本身就始终保持精简
不会无限增长
3. StateKey
作用:
指定在
StateBag中的存储键名。
适用场景:
同一个 Session 中可能有多个不同状态对象
或者你使用多个历史提供者实例
需要避免键名冲突
4. StateInitializer
作用:
自定义新 Session 的初始状态。
例如你希望每次新会话都默认插入一条系统消息:
StateInitializer = session => new InMemoryChatHistoryProvider.State
{
Messages = new List<ChatMessage>
{
new ChatMessage(ChatRole.System, "你是一位专业的助手。")
}
}
这非常适合做“预设上下文”。
九、内存存储的优点与局限
这部分一定要形成判断能力,因为面试和实际项目都常问。
优点
1. 性能极高
内存读写没有磁盘和网络开销。
2. 实现简单
不需要 Redis、不需要数据库、不需要额外配置。
3. 即开即用
默认就能工作,对初学者特别友好。
4. 调试方便
可以直接查看消息列表。
局限
1. 非持久化
程序一关闭,历史直接丢失。
2. 占内存
对话越长,占用越大。
3. 单机限制
只能在当前进程里使用,不能跨服务器共享。
4. 没有天然过期管理
如果不裁剪,历史会一直堆积。
适用场景
适合
学习和练习
Demo
原型验证
单机小项目
短对话场景
不适合
企业生产环境
需要持久化的业务
多实例部署
长对话和大规模用户
十、对话持久化与恢复:为什么这是企业级必学能力?
当你从“本地练习”走向“真实业务”时,你会发现:
仅靠内存保存历史,根本不够。
因为业务中常常会出现:
用户关闭浏览器又回来
会话隔了几天继续
多台服务器负载均衡
人工客服接管 AI 会话
合规审计要保留记录
这时就必须学会:
把 Session 序列化保存,再恢复出来继续用
十一、两个最重要的持久化 API
1. 序列化 Session
JsonElement serialized = await agent.SerializeSessionAsync(session);
作用:
把当前 Session 转成可以保存的 JSON 数据。
返回类型是:
JsonElement
2. 反序列化 Session
AgentSession resumed = await agent.DeserializeSessionAsync(serialized);
作用:
从 JSON 数据恢复出一个完整的 Session。
3. 初学者一定要记住的转换链路
真实存储时,通常不会直接存 JsonElement,而是会转成字符串:
JsonElement -> string -> 存储
存储 -> string -> JsonElement -> AgentSession
例如:
string jsonString = JsonSerializer.Serialize(serializedThread);
await File.WriteAllTextAsync(path, jsonString);
string loaded = await File.ReadAllTextAsync(path);
JsonElement json = JsonSerializer.Deserialize<JsonElement>(loaded);
AgentSession resumed = await agent.DeserializeSessionAsync(json);
十二、持久化与恢复的完整流程
这里我用老师讲课的方式,把流程拆成 5 步。
第一步:创建 Agent 和 Session
var agent = chatClient.AsAIAgent(...);
var session = await agent.CreateSessionAsync();
这一阶段只是先开启一段会话。
第二步:进行几轮对话
await agent.RunAsync("你好,我想退货", session);
await agent.RunAsync("订单显示已签收,但我没收到", session);
经过几轮对话后,Session 中已经有了完整上下文。
第三步:序列化保存
JsonElement serialized = await agent.SerializeSessionAsync(session);
string json = JsonSerializer.Serialize(serialized);
然后把 json 存到:
文件
数据库
Redis
对象存储
都可以。
第四步:稍后重新读取
string loadedJson = await File.ReadAllTextAsync(path);
JsonElement loadedElement = JsonSerializer.Deserialize<JsonElement>(loadedJson);
第五步:恢复 Session 并继续对话
AgentSession resumed = await agent.DeserializeSessionAsync(loadedElement);
var response = await agent.RunAsync("继续处理这个问题", resumed);
这样 Agent 就不是“重新开始聊天”,而是在原上下文基础上继续。
十三、持久化最大的业务价值是什么?
课程中用了“智能客服转人工”的案例,这个案例特别典型。
核心价值:避免用户重复描述问题
这其实是业务中最真实的痛点。
用户最烦的事情之一就是:
明明刚刚已经把问题说清楚了,转人工后还要再说一遍。
如果我们把 Session 保存下来,那么人工客服或者后续系统就能直接恢复完整上下文:
用户说了什么
订单号是什么
问题发展到哪一步
AI 已经做了哪些处理
这会带来几个非常实际的收益:
1. 提升用户体验
用户不用重复讲述。
2. 提升处理效率
客服直接进入问题处理阶段。
3. 支持跨会话连续体验
今天没处理完,明天继续。
4. 支持无状态服务架构
任何实例都可以恢复会话,不依赖某台机器的内存。
十四、无状态服务为什么特别依赖持久化?
现代云原生系统常常是无状态服务。
意思是:
当前服务实例不长期保存用户会话数据
请求可能被路由到任意服务器
这意味着:
你不能指望“某个用户一直连到同一台服务器”。
所以消息历史必须放到共享存储里,例如:
Redis
SQL Server
Cosmos DB
其他数据库
这样无论用户下一次请求落到哪台机器,都能恢复同一个 Session。
十五、持久化时的注意事项
这部分非常像老师会在黑板上专门提醒的“考试重点”。
1. 序列化 JSON 不包含 API Key
这意味着不会把你的模型密钥直接存进去。
2. 但可能包含用户敏感信息
比如:
姓名
电话
地址
订单号
医疗信息
财务数据
所以生产环境中建议:
加密存储
做权限控制
设置过期清理机制
3. 恢复时要使用相同配置的 Agent
如果恢复前后 Agent 配置差异太大,可能会出现行为不一致。
4. 反序列化要做好异常处理
恢复失败时,不要让系统崩掉,最好降级为新建 Session。
例如:
try
{
var json = LoadFromStorage(sessionId);
session = await agent.DeserializeSessionAsync(json);
}
catch
{
session = await agent.CreateSessionAsync();
}
十六、最佳实践:每次对话后立即保存
这个习惯非常重要。
推荐做法:
var response = await agent.RunAsync(message, session);
var serialized = await agent.SerializeSessionAsync(session);
SaveToStorage(sessionId, serialized);
return response.Text;
为什么推荐这样做?
因为如果你只在“最后结束时”才保存,那么中途服务异常、进程崩溃、网络中断,就可能丢掉最近几轮消息。
所以老师会建议你:
每完成一轮对话,就顺手保存一次状态。
十七、如果默认内存存储不够用,怎么办?
这就进入第三部分内容了:
自定义 ChatHistoryProvider
当你发现默认的 InMemoryChatHistoryProvider 有局限时,就可以自己实现一个新的历史提供者。
比如:
文件存储版
Redis 版
数据库版
Redis + 数据库混合版
十八、自定义历史提供者的核心思路
你只需要记住一句话:
自定义历史提供者,本质上就是“自己决定历史从哪读、往哪写”。
而要做到这一点,核心就是继承:
public class CustomChatHistoryProvider : ChatHistoryProvider
然后重点重写两个方法:
1. ProvideChatHistoryAsync(...)
负责:
从存储中读取历史消息。
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(
InvokingContext context,
CancellationToken cancellationToken = default)
你可以理解为:
对话要开始了
你去文件/数据库/Redis 中把历史消息取出来
然后返回给 Agent
2. StoreChatHistoryAsync(...)
负责:
把本轮新消息写入存储。
protected override async ValueTask StoreChatHistoryAsync(
InvokedContext context,
CancellationToken cancellationToken = default)
你可以理解为:
一轮对话刚结束
用户消息和 Agent 回复都产生了
你把这些新增消息保存起来
十九、ProvideChatHistoryAsync 和 InvokingAsync 有什么区别?
这是一个容易混淆的点。
InvokingAsync
这是公开方法,由 Agent 调用。
它负责更完整的流程,比如消息合并、来源标记等。
ProvideChatHistoryAsync
这是受保护方法,更适合自定义实现。
你只需要返回“历史消息本身”。
所以课程里也强调了一个推荐思路:
优先重写
ProvideChatHistoryAsync,通常更简单。
同理,存储也优先重写 StoreChatHistoryAsync。
二十、示例:FileChatHistoryProvider
课程中给了一个很实用的自定义实现:
public class FileChatHistoryProvider : ChatHistoryProvider
它的目标很明确:
把对话历史持久化到 JSON 文件中。
这个实现有什么特点?
1. 文件持久化
聊天记录会写到本地文件,而不是只存在内存中。
2. JSON 格式
方便查看、调试、序列化。
3. 支持 IChatReducer
可以和消息裁剪结合使用。
4. 使用 SemaphoreSlim
用于保证进程内线程安全。
这类实现特别适合什么场景?
学习自定义存储原理
单机小项目
本地测试
不想接数据库的轻量应用
但它不太适合什么场景?
高并发生产环境
多实例部署
跨进程一致性要求很高的场景
因为文件 I/O 天然会有性能和并发限制。
二十一、文件版历史提供者的核心工作流程
读取历史时
在 ProvideChatHistoryAsync(...) 中:
从 JSON 文件读取消息
如有需要,先做 Reducer 裁剪
返回消息列表
存储历史时
在 StoreChatHistoryAsync(...) 中:
先加载已有消息
追加本轮请求消息和响应消息
如有需要,做 Reducer 裁剪
再写回文件
这和内存版的逻辑本质一样,只不过“容器”从内存列表换成了文件。
二十二、Reducer 在历史管理中到底起什么作用?
随着对话越来越长,消息历史会膨胀。
这会带来两类问题:
上下文太长,模型成本升高
超出模型最大 Token 限制
所以 Reducer 的目标就是:
控制历史长度,让上下文保持可用、可控。
两种常见触发时机
1. BeforeMessagesRetrieval
在取消息之前裁剪。
流程:
从存储加载所有历史
调用
ChatReducer.ReduceAsync(...)返回裁剪后的消息
优点:
存储可以保留完整原始历史
模型拿到的是精简历史
2. AfterMessageAdded
在新增消息后立刻裁剪。
流程:
加入本轮消息
立刻调用 Reducer
保存裁剪后的结果
优点:
存储本身不会无限膨胀
该怎么选?
老师式建议:
想保留完整记录做审计/回溯:用
BeforeMessagesRetrieval想控制存储体积和长期增长:用
AfterMessageAdded
二十三、如何看待“文件存储 + Reducer”这个组合?
这是一个很好的练手模型。
因为它同时让你理解了两件事:
历史消息可以持久化到外部存储
持久化后仍然可以做自动裁剪
这说明:
存储方案和消息裁剪不是冲突关系,而是可以组合使用的。
二十四、进程重启后如何恢复?
课程还演示了一个很关键的场景:
即使进程重启了,只要文件还在,历史就能重新加载。
这里你要建立一个思维:
InMemoryChatHistoryProvider:进程一重启就没了FileChatHistoryProvider:文件还在,就能恢复数据库/Redis:服务重启也能恢复,且更适合生产
这其实就是“持久化能力层级”的差异。
二十五、生产环境到底推荐什么方案?
这部分要形成工程判断,而不是只记概念。
1. 原型阶段
推荐:
InMemoryChatHistoryProvider
原因:
最简单
上手快
调试方便
2. 单机部署
可以考虑:
FileChatHistoryProvider
原因:
有基本持久化
不需要外部中间件
适合轻量场景
3. 分布式缓存场景
可以考虑:
RedisChatHistoryProvider
原因:
多实例共享
读取速度快
适合高并发读取
4. 企业级正式方案
课程中更推荐:
数据库 + Redis 混合方案
也就是:
数据库存长期、可靠、可审计的记录
Redis 存热点会话,提升访问速度
这是比较成熟的企业级架构思路。
二十六、为什么生产环境推荐“数据库 + Redis”?
因为两者优势互补。
数据库的优点
持久可靠
可审计
查询能力强
适合长期保存
Redis 的优点
读取快
适合会话热点数据
适合高并发
所以组合起来通常会更合理:
写入时:数据库持久化 + 更新 Redis 缓存
读取时:优先从 Redis 读,没命中再查数据库
这就是典型的企业级混合存储思路。
二十七、线程安全与容错也要考虑
很多初学者写自定义 Provider 时,只想着“能跑起来”,但生产代码要考虑更多。
1. 线程安全
课程里用 SemaphoreSlim 做进程内锁,这是很重要的思路。
它可以避免多个线程同时写文件导致冲突。
但要注意:
SemaphoreSlim只能保证进程内安全,不能解决跨进程并发问题。
如果是多进程、多机器,就要靠:
文件锁
数据库锁
分布式锁
更可靠的存储系统
2. 容错与降级
课程里还提到一个很好的思路:
如果文件存储失败,可以降级到内存存储。
这种“兜底策略”在生产环境中很有价值。
例如:
文件写入失败
磁盘权限问题
I/O 异常
这时至少不要直接把消息全丢了。
二十八、这三节课之间的关系,一定要串起来
很多同学学完一节懂了,但三节一连起来就糊了。
这里我帮你串成一条完整主线。
第一节:默认历史管理
你学的是:
ChatHistoryProviderInMemoryChatHistoryProviderInvokingAsyncGetMessages历史默认存在
Session.StateBag
这节课的核心是:
先理解“历史是怎么被管理的”。
第二节:持久化与恢复
你学的是:
SerializeSessionAsyncDeserializeSessionAsync
这节课的核心是:
把一个 Session 保存下来,并在之后恢复继续。
第三节:自定义存储实现
你学的是:
重写
ProvideChatHistoryAsync重写
StoreChatHistoryAsync实现
FileChatHistoryProvider结合 Reducer
设计生产级存储方案
这节课的核心是:
当默认方案不够用时,如何自己接管历史的读写。
二十九、初学者最容易混淆的几个点
1. Session 和 ChatHistoryProvider 的关系
Session是会话容器ChatHistoryProvider是历史管理器
不要把两者当成一回事。
2. SerializeSessionAsync 和 自定义 Provider 的关系
它们不是互斥的。
SerializeSessionAsync是对整个 Session 做序列化自定义 Provider 是控制历史消息怎么存取
两者可以配合使用。
3. InvokingAsync 和 ProvideChatHistoryAsync
InvokingAsync更偏框架调用入口ProvideChatHistoryAsync更适合你自己扩展实现
4. “持久化”不等于“历史提供者一定是数据库”
不是的。
持久化有很多层次:
Session 序列化到文件
消息历史写文件
消息历史写 Redis
消息历史写数据库
本质都是持久化,只是方式不同。
三十、给初学者的学习建议
第一阶段:先吃透默认方案
你至少要真正搞懂下面这些:
AgentSessionChatHistoryProviderInMemoryChatHistoryProvideragent.GetService<ChatHistoryProvider>()InvokingAsync()GetMessages(session)
如果这部分没搞懂,后面的持久化和自定义扩展会很虚。
第二阶段:必须亲手做一次序列化恢复
建议你自己写一个最小 Demo:
创建 Agent
对话 2~3 轮
SerializeSessionAsync保存成 JSON 文件
重新读取
DeserializeSessionAsync继续对话
只要这一步你亲手打通过,对“会话恢复”的理解会立刻变深。
第三阶段:尝试自己实现一个简单 Provider
哪怕只是最简版文件存储,也很值得做。
因为一旦你自己写过:
从哪加载历史
什么时候追加新消息
怎么做裁剪
怎么保证线程安全
你对整个 Agent 会话体系的理解,就不是“会用”,而是“懂原理”。
三十一、这部分内容的最终记忆框架
如果你只想带走一个超浓缩版记忆框架,请记住下面这张脑图式总结。
1. 历史管理是谁负责?
ChatHistoryProvider
2. 默认实现是什么?
InMemoryChatHistoryProvider
3. 默认历史存在哪里?
AgentSession.StateBag
4. 读取历史的通用方式是什么?
await historyProvider.InvokingAsync(...)
5. 内存实现可直接怎么拿消息?
inMemoryProvider.GetMessages(session)
6. 如何保存整个 Session?
await agent.SerializeSessionAsync(session)
7. 如何恢复整个 Session?
await agent.DeserializeSessionAsync(jsonElement)
8. 自定义历史存储重点重写哪两个方法?
ProvideChatHistoryAsync(...)
StoreChatHistoryAsync(...)
9. Reducer 用来干什么?
控制历史长度,避免上下文无限增长。
10. 生产环境推荐什么?
数据库 + Redis 混合方案。
三十二、老师式总结
学到这里,你要真正建立起这样一个认识:
Agent 不是“会回复”就够了,真正进入项目后,最关键的是“它怎么记住、怎么保存、怎么恢复、怎么扩展”。
所以这部分内容虽然不像“工具调用”“函数调用”那样看起来炫,但它非常基础,也非常工程化。
如果把 Agent 比作一个真正能工作的数字员工,那么:
Prompt决定它怎么说话Tool决定它会做什么事历史管理与持久化 决定它有没有“连续工作的能力”
而这,恰恰是一个 Agent 从“演示品”走向“生产系统”的关键一步。
三十三、适合复习时看的精简版结论
核心结论 1
ChatHistoryProvider 是消息历史管理的统一抽象。
核心结论 2
默认使用 InMemoryChatHistoryProvider,消息保存在 AgentSession.StateBag 中。
核心结论 3
内存方案快,但不持久化,适合学习、Demo、短对话。
核心结论 4
跨会话恢复靠:
SerializeSessionAsyncDeserializeSessionAsync
核心结论 5
自定义存储时,重点重写:
ProvideChatHistoryAsyncStoreChatHistoryAsync
核心结论 6
Reducer 是历史裁剪器,用来控制上下文长度。
核心结论 7
生产环境一般不推荐纯内存,推荐:
数据库
Redis
或数据库 + Redis 混合方案