目录先从一个问题出发三大 API 如何管理对话历史Chat Completions APIOpenAI / Azure / DeepSeek / QwenResponses APIOpenAI / Azure OpenAIMessages APIAnthropic Claude国内主流模型的协议兼容情况两种根本模式从 API 差异中提炼服务端管理Service-Managed客户端管理Client-Managed服务端管理的两种子形态线性对话Linear分叉对话ForkingMAF 的统一抽象AgentSession ChatHistoryProviderAgentSession通用状态容器AgentSessionStateBag线程安全的类型化字典会话序列化跨进程/重启恢复ChatHistoryProvider可插拔的存储后端ProviderSessionState连接两者的桥梁调用生命周期三阶段精密协作⚠️ 关键前提默认 InMemory Provider 的按需创建完整调用时序图阶段一——从 StateBag 读取历史并标记来源消息来源标记防重复的闭环机制InMemoryChatHistoryProvider内置实现的源码剖析构造过程读取实现ProvideChatHistoryAsync存储实现StoreChatHistoryAsyncChatReducer 的两个触发时机实战40 行代码实现 SQLite 持久化 Provider会话恢复只需两行背后的三种设计模式模式一状态外部化Externalized State模式二模板方法Template Method模式三关注点分离Separation of Concerns如何选择存储策略总结先从一个问题出发多轮对话需要上下文模型才能理解你之前说的指的是什么。那么问题来了这些上下文历史消息由谁来维护、存在哪里答案因 API 而异这是很多开发者第一次接触多 Provider 场景时感到困惑的根源。三大 API 如何管理对话历史目前主流的三大 AI API 在历史管理上走了完全不同的路。Chat Completions APIOpenAI / Azure / DeepSeek / Qwen谁管历史你客户端每次请求必须把完整的历史消息全部发给服务端服务本身是无状态的处理完即忘。// 第三轮对话的请求 payload必须携带前两轮的全部内容 { model:gpt-4o, messages:[ {role:system,content:你是技术助手}, {role:user,content:我叫圣杰},// 第一轮 {role:assistant,content:你好圣杰}, {role:user,content:推荐一本技术书},// 第二轮 {role:assistant,content:推荐《CLR via C#》}, {role:user,content:这本书适合初学者吗}// 第三轮新消息 ] }特征请求 payload 随对话轮次线性增长数据完全在你手里行业通用DeepSeek、Qwen 等大多数模型都兼容。Responses APIOpenAI / Azure OpenAI谁管历史服务端每次请求只需发送新消息 上一次响应的 ID服务端自动维护完整会话树。// 第三轮对话的请求 payload只有新消息 { model:gpt-4o, input:这本书适合初学者吗, previous_response_id:resp_r2_xyz// 指向上一次响应 }响应中会包含新的id下一轮用它继续{ id:resp_r3_abc,// ← 下一轮用这个 output:[ ... ] }特征请求 payload 固定极小天然支持分叉同一个response_id可以派生出多条对话分支但数据存在 OpenAI/Azure 服务器受隐私合规约束。Messages APIAnthropic Claude谁管历史你客户端与 Chat Completions API 类似每次必须携带完整历史。但格式不同system是独立的顶级字段content是内容块数组支持文本、图片、工具结果混合。{ model:claude-opus-4-5, system:你是技术助手, max_tokens:1024, messages:[ {role:user,content:我叫圣杰}, {role:assistant,content:你好圣杰}, {role:user,content:这本书适合初学者吗} ] }特征与 Chat Completions 同属客户端管理但max_tokens是必填项工具结果回传格式使用role: user 内容块而非role: tool。国内主流模型的协议兼容情况在选择 API 协议之前有必要先搞清楚你用的模型支持哪些协议。下表基于各厂商截至 2026 年 4 月的公开文档整理后续能力可能变化请以官方最新文档为准模型 / 厂商Chat Completions APIResponses APIMessages APIDeepSeek深度求索✅ 官方支持端点api.deepseek.com❌✅ 官方支持端点api.deepseek.com/anthropicQwen / 通义千问阿里云百炼✅ 官方支持端点dashscope.aliyuncs.com/compatible-mode/v1✅ 官方支持端点/compatible-mode/v1/responses支持previous_response_id❌Kimi月之暗面✅ 官方支持端点api.moonshot.cn/v1/chat/completions❌❌智谱 AIGLM 系列✅ 官方支持端点open.bigmodel.cn/api/paas/v4/chat/completions❌✅MiniMax✅ 官方支持端点api.minimaxi.com/v1❌✅ 官方支持推荐端点api.minimaxi.com/anthropic小米 MiMo✅ 官方支持端点api.xiaomimimo.com/v1/chat/completions❌✅ 官方支持端点api.xiaomimimo.com/anthropic/v1/messages(Azure)OpenAI GPT✅ 原生✅ 原生协议发起者❌Anthropic Claude❌❌✅ 原生协议发起者结论一目了然Chat Completions API 是覆盖面最广的事实标准Responses API 目前只有 OpenAI/Azure 和 Qwen 支持Qwen 是国内首家跟进的Messages API 是 Anthropic 私有协议DeepSeek 提供了兼容端点。⚠️ 如果你的应用需要在国内模型和 OpenAI 之间切换不要把 Responses API 的服务端历史管理作为核心依赖——Kimi、GLM 等均不支持。两种根本模式从 API 差异中提炼现在答案就清楚了。三大 API 本质上代表了两种管理模式API谁管历史模式Chat CompletionsOpenAI/Azure/DeepSeek/Qwen客户端客户端管理Responses APIOpenAI/Azure服务端服务端管理Messages APIAnthropic Claude客户端客户端管理服务端管理Service-ManagedAI 服务自己保存对话状态。Agent 只持有一个服务端会话引用如conversation_id、thread_id或response_id每次请求时服务自动拼接上下文。适用场景• 对话数据隐私要求不高可接受数据托管于提供商• 希望由服务端自动处理历史拼接与压缩• 追求极简客户端实现不想管理历史• 对话轮次多、历史很长不想每次传全量 payload是否支持分叉取决于具体服务端模型有些是线性会话conversation/thread有些才是基于response_id的分叉模型。代价• ❌ 数据存在提供商服务器隐私风险• ❌ 失去对压缩策略的控制权• ❌ 换提供商需要迁移会话状态客户端管理Client-ManagedMAF 本地维护完整对话历史每次请求都把历史消息一起发给模型。服务是无状态的处理完即忘。适用场景• 数据隐私/合规要求高必须自主控制对话数据• 需要接入多个模型提供商或可能随时切换• 需要自定义压缩策略摘要、截断、滑动窗口• 需要将对话持久化到自有数据库Redis/PostgreSQL/Blob代价• ❌ 请求 payload 随对话增长而增大• ❌ 必须自己处理上下文窗口溢出问题• ❌ 生产环境需要额外的持久化基础设施服务端管理的两种子形态服务端管理并不是铁板一块还有线性 vs 分叉两种子模型线性对话Linear消息形成有序序列只能往后追加不能分叉。用户: 推荐三个旅游目的地 Assistant: 东京、巴黎、纽约 用户: 介绍第一个 Assistant: 东京是...适用场景客服机器人、简单 QA、需要严格审计追踪。代表实现Microsoft Foundry Prompt Agent、OpenAI Conversations API分叉对话Forking每个响应都有唯一response_id新请求可以从任意历史响应点继续天然支持撤回重试和并行探索。适用场景头脑风暴工具、A/B 响应测试、带重试功能的对话 UI。代表实现Microsoft Foundry Responses、Azure OpenAI Responses API、OpenAI Responses APIMAF 的统一抽象AgentSession ChatHistoryProvider无论底层是哪种模式你的应用代码保持不变// 无论 Chat Completions (客户端管理) 还是 Responses API (服务端管理) // 代码完全相同 AgentSession session await agent.CreateSessionAsync(); var r1 await agent.RunAsync(我叫圣杰, session); var r2 await agent.RunAsync(我叫什么名字, session);这种透明性来自两个核心抽象。AgentSession通用状态容器AgentSession是一个抽象基类其核心只有一个属性StateBag。public abstract class AgentSession { // 核心通用的键值状态容器 public AgentSessionStateBag StateBag {get;protectedset;}new(); // 服务发现模式 public virtual object? GetService(Type serviceType,object? serviceKey null); }AgentSessionStateBag线程安全的类型化字典StateBag底层是ConcurrentDictionary提供完整的线程安全 API方法功能线程安全SetValueT(key, value)存储任意类型对象✅GetValueT(key)类型安全地读取对象✅TryGetValueT(key, out value)安全尝试读取✅TryRemoveValue(key)删除键值对✅Serialize()/Deserialize()JSON 序列化/反序列化✅关键设计StateBag不仅用于存储消息历史它是一个通用容器任何 Provider 都可以在其中存储自己的状态互不干扰AgentSession.StateBagConcurrentDictionary ├── InMemoryChatHistoryProvider → { Messages: [...] } ├── TextSearchProvider → { SearchState: ... } └── PersonalizationProvider → { UserProfile: ... }会话序列化跨进程/重启恢复StateBag使用 JSON 序列化意味着整个会话状态包括完整消息历史可以被持久化和恢复// 序列化保存会话快照含全部历史 JsonElement snapshot await agent.SerializeSessionAsync(session); // 重启后恢复任意时刻从快照还原 AgentSession restored await agent.DeserializeSessionAsync(snapshot);ChatHistoryProvider可插拔的存储后端对于客户端管理场景ChatHistoryProvider控制历史存在哪里、怎么读写// 内置内存 Provider开发/原型阶段 AIAgent agent chatClient.AsAIAgent(newChatClientAgentOptions { ChatHistoryProvider newInMemoryChatHistoryProvider() }); // 自定义数据库 Provider生产环境 AIAgent agent chatClient.AsAIAgent(newChatClientAgentOptions { ChatHistoryProvider newSqliteChatHistoryProvider(db) });源码有一段关键注释道出了设计意图Since aChatHistoryProvideris used with many different sessions, it should not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associatedAgentSession.StateBag.一个 Provider 实例被多个 Session 共享。如果 Provider 自身保存消息不同用户的数据就会混在一起。MAF 的设计强制要求•AgentSession.StateBag 每个会话独立的数据保险箱 •ChatHistoryProvider 操作保险箱的钥匙 知道怎么读写但自己不存数据这带来了重要的可测试性提升单元测试只需 mockAgentSession完全不需要考虑 Provider 的并发状态。ProviderSessionState连接两者的桥梁ChatHistoryProvider不直接操作StateBag而是通过中介类ProviderSessionStateTState来简化状态管理。它的核心方法是GetOrInitializeState()public TState GetOrInitializeState(AgentSession? session) { // 1️⃣ 尝试从 StateBag 中读取已有状态 if(session?.StateBag.TryGetValueTState(this.StateKey,outvar state)is true state isnotnull) { return state; } // 2️⃣ 没有找到 → 调用初始化器创建新状态 state this._stateInitializer(session); // 3️⃣ 保存回 StateBag if(session isnotnull) { session.StateBag.SetValue(this.StateKey, state); } return state; }为什么需要这个中介直接操作 StateBag通过 ProviderSessionState每次手动写TryGetValue一行代码GetOrInitializeState()键名散落各处StateKey集中管理初始化逻辑重复初始化器统一配置序列化选项不一致统一的JsonSerializerOptions调用生命周期三阶段精密协作当你调用agent.RunAsync(你好, session)时底层发生了以下精密交互⚠️ 关键前提默认 InMemory Provider 的按需创建首先有一个非常重要的细节如果你没有显式配置ChatHistoryProvider框架才可能在首次RunAsync后按需创建默认的InMemoryChatHistoryProvider。如果你在构建 Agent 时已经传入自定义或内置 Provider它会从一开始就可用。// 未显式配置 Provider 时Agent 刚创建后调用 → null var p1 agent.GetServiceChatHistoryProvider();// null ❌ // 首次 RunAsync 完成后调用 → 默认 InMemoryChatHistoryProvider await agent.RunAsync(你好, session); var p2 agent.GetServiceChatHistoryProvider();// 非 null ✅原因在未显式配置 Provider 的前提下Agent 需要先探测底层 AI 服务是否自行管理历史。若服务端已管理就不需要本地 Provider确认不支持后才会按需创建默认的InMemoryChatHistoryProvider。源码中的触发点private void UpdateSessionConversationId(ChatClientAgentSession session, string? responseConversationId) { if(!string.IsNullOrWhiteSpace(responseConversationId)) { // 服务端管理历史 → 记录 ID不需要本地 Provider session.ConversationId responseConversationId; } else { // 服务端不管理历史 → 延迟创建 InMemoryChatHistoryProvider this.ChatHistoryProvider ??newInMemoryChatHistoryProvider(); } } 如果你想显式控制如使用自定义 Provider在构建 Agent 时直接传入即可不必依赖默认 Provider 的按需创建行为。完整调用时序图阶段一——从 StateBag 读取历史并标记来源return historicalMessages .Select(m m.WithAgentRequestMessageSource( AgentRequestMessageSourceType.ChatHistory,// 标记为历史消息 this.GetType().FullName!)) .Concat(context.RequestMessages);// 历史在前新消息在后阶段二——把合并后的完整消息列表发给 LLM。阶段三InvokedAsync——过滤来源只存储真正的新消息// 如果调用失败跳过存储 if(context.InvokeException is not null)returndefault; // 过滤掉来源为 ChatHistory 的消息防止重复存储 var filteredMessages this._storeInputMessageFilter(context.RequestMessages); returnthis.StoreChatHistoryAsync(filteredMessages,...);消息来源标记防重复的闭环机制这是最精妙的设计。消息在系统中可能来自多个来源来源类型枚举值说明外部输入External用户直接传入的消息消息历史ChatHistoryChatHistoryProvider 提供的历史消息上下文提供者AIContextProviderAIContextProvider 注入的上下文标记与过滤形成完整闭环默认过滤器的实现极为简洁private static IEnumerableChatMessage DefaultExcludeChatHistoryFilter( IEnumerableChatMessage messages) messages.Where(m m.GetAgentRequestMessageSourceType() ! AgentRequestMessageSourceType.ChatHistory);结论历史消息被送去模型用于上下文存储时自动排除永远不会被重复写入。开发者完全无需手动处理这个问题。InMemoryChatHistoryProvider内置实现的源码剖析了解默认实现如何利用这套机制对于自定义 Provider 很有帮助。构造过程public sealed class InMemoryChatHistoryProvider:ChatHistoryProvider { // 桥梁通过 ProviderSessionState 操作 StateBag private readonly ProviderSessionStateState _sessionState; public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options null) :base(options?.ProvideOutputMessageFilter, options?.StorageInputMessageFilter) { this._sessionState newProviderSessionStateState( options?.StateInitializer ??(_ newState()),// 默认空消息列表 options?.StateKey ??this.GetType().Name,// 默认类名作为 Key options?.JsonSerializerOptions); this.ChatReducer options?.ChatReducer; this.ReducerTriggerEvent options?.ReducerTriggerEvent ?? ChatReducerTriggerEvent.BeforeMessagesRetrieval; } }读取实现ProvideChatHistoryAsyncprotected override async ValueTaskIEnumerableChatMessage ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken) { // 1️⃣ 通过桥梁从 Session.StateBag 获取状态 var state this._sessionState.GetOrInitializeState(context.Session); // 2️⃣ 如果配置了 ChatReducer 且触发时机为读取前默认 if(this.ReducerTriggerEvent is BeforeMessagesRetrieval this.ChatReducer isnotnull) { state.Messages (awaitthis.ChatReducer.ReduceAsync( state.Messages, cancellationToken)).ToList(); } // 3️⃣ 返回消息列表基类负责标记来源和合并 return state.Messages; }存储实现StoreChatHistoryAsyncprotected override async ValueTask StoreChatHistoryAsync( InvokedContext context,CancellationToken cancellationToken) { var state this._sessionState.GetOrInitializeState(context.Session); // 合并请求消息和响应消息追加到列表 // 注意此时的 RequestMessages 已被基类过滤不含 ChatHistory 来源的消息 var allNewMessages context.RequestMessages.Concat(context.ResponseMessages ??[]); state.Messages.AddRange(allNewMessages); // 如果配置了 ChatReducer 且触发时机为添加后 if(this.ReducerTriggerEvent isAfterMessageAdded this.ChatReducer isnotnull) { state.Messages (awaitthis.ChatReducer.ReduceAsync( state.Messages, cancellationToken)).ToList(); } }ChatReducer 的两个触发时机ChatReducer用于控制上下文窗口截断、摘要、滑动窗口等支持两个触发点实战40 行代码实现 SQLite 持久化 Provider继承ChatHistoryProvider只需实现两个方法public class SqliteChatHistoryProvider:ChatHistoryProvider { private readonly ChatDatabase _db; private readonly ProviderSessionStateState _sessionState; public SqliteChatHistoryProvider(ChatDatabase db):base() { _db db; // ProviderSessionState 是桥梁首次访问时触发 LoadFromDatabase // 后续访问直接读 StateBag 内存缓存兼顾持久化与性能 _sessionState newProviderSessionStateState( stateInitializer: session LoadFromDatabase(session), stateKey:nameof(SqliteChatHistoryProvider)); } // 读取从 StateBag 缓存或首次从 SQLite获取历史 // 基类负责标记来源 与新消息合并 protected override ValueTaskIEnumerableChatMessage ProvideChatHistoryAsync( InvokingContext context,CancellationToken ct) { var state _sessionState.GetOrInitializeState(context.Session); return new ValueTaskIEnumerableChatMessage(state.Messages); } // 存储写入 SQLite 同步更新 StateBag 缓存 // 基类已过滤 ChatHistory 来源只有真正的新消息会到达这里 protected override ValueTaskStore ChatHistoryAsync( InvokedContext context,CancellationToken ct) { var state _sessionState.GetOrInitializeState(context.Session); var newMessages context.RequestMessages .Concat(context.ResponseMessages ??[]).ToList(); // 写入 SQLite使用 AIJsonUtilities.DefaultOptions 完整序列化 // 可保留 Function Calling、Tool Results 等复杂消息结构 SaveToDatabase(context.Session, newMessages); // 同步内存缓存 state.Messages.AddRange(newMessages); return default; } // stateInitializer 回调首次访问时从数据库加载 private State LoadFromDatabase(AgentSession? session){/* ... */} }关键设计决策•ProviderSessionStateTState封装了 StateBag 的首次初始化 后续缓存逻辑一行代码搞定•AIJsonUtilities.DefaultOptions序列化ChatMessage完整保留 Function Calling、Tool Results 等复杂内容•无需处理防重复框架的标记-过滤闭环已自动处理会话恢复只需两行// 重启后恢复已有会话把 sessionId 写入 StateBag // Provider 的 stateInitializer 会在首次访问时自动从数据库加载 var session await agent.CreateSessionAsync(); session.StateBag.SetValue(SqliteSessionId, existingSessionId);背后的三种设计模式理解 MAF 为什么这样设计有助于举一反三地扩展它。模式一状态外部化Externalized StateChatHistoryProvider将状态外部化到AgentSession.StateBag这是经典的无状态服务设计传统有状态设计MAF 无状态设计Provider 内部持有DictionarysessionId, messagesProvider 从session.StateBag读写状态Provider 需要清理过期 SessionSession 被 GC 时状态自然回收Provider 无法简单序列化Session 完整序列化包含所有状态多实例部署需同步 Provider 状态Session 独立传递天然支持分布式模式二模板方法Template MethodChatHistoryProvider使用经典的模板方法模式——基类定义流程骨架子类实现具体步骤三层可重写粒度让你按需介入自定义存储后端只需实现最内层两个方法自定义过滤逻辑重写中间层最外层提供固定的安全校验始终不可绕过。模式三关注点分离Separation of ConcernsChatHistoryProvider和AIContextProvider遵循完全相同的架构——它们都通过ProviderSessionStateTState管理各自独立的 StateBag 状态都使用相同的Invoking/Invoked生命周期钩子都为消息添加不同的来源标记ChatHistoryvsAIContextProvider。这意味着你学会扩展一个就学会了扩展所有。如何选择存储策略需求推荐方案快速原型不在意数据持久化InMemoryChatHistoryProvider可显式配置未显式配置时框架可按需回退创建默认实例数据隐私要求高需完全自控客户端管理 自定义 ProviderRedis/数据库需要撤回或并行探索分支Responses API服务端分叉模型简单聊天机器人追求最低复杂度Foundry Prompt Agent服务端线性模型需要自定义压缩策略摘要/截断客户端管理 ChatReducer配置生产环境需跨重启保持会话客户端管理 数据库 Provider利用 StateBag 序列化恢复一个实用原则从InMemoryChatHistoryProvider起步当遇到以下信号时再升级1. 用户要求上次聊过什么 → 换持久化 Provider2. 对话越来越长、成本飙升 → 配置ChatReducer3. 合规审查要求数据留在本地 → 切换到客户端管理模式总结MAF 的会话历史存储架构有几个核心设计值得记住1.两种模式一套 APIAgentSession统一封装了服务端和客户端管理的差异切换模式不改应用代码2.Provider 无状态ChatHistoryProvider只是操作逻辑数据永远属于AgentSession.StateBag3.StateBag 是通用容器不只存消息历史所有 ProviderChatHistory、TextSearch、Personalization……共享同一个 ConcurrentDictionary互不干扰4.ProviderSessionState 是桥梁封装了首次初始化 后续缓存逻辑一行代码搞定 StateBag 的读写5.默认 Provider 可按需创建未显式传入ChatHistoryProvider时Agent 会先探测服务端是否托管历史再决定是否回退创建默认 InMemory Provider6.标记-过滤闭环框架自动防止历史消息被重复存储开发者零负担7.模板方法三层重写自定义后端只需实现两个方法需要更深度定制时可分层介入基类始终保障校验安全8.两个方法扩展自定义任何存储后端只需实现ProvideChatHistoryAsync和StoreChatHistoryAsync这套设计让你能从最简单的InMemoryChatHistoryProvider起步在不改一行业务代码的前提下演进到生产级的分布式持久化方案。参考资料• Chat History Storage Patterns in Microsoft Agent Framework — Wes Steyn, Microsoft• Conversations Memory 官方文档引入地址
.NET+AI | Agent | MAF 会话历史存储全解析,从三大主流 AI API 讲起,不仅 Know What,而且讲清 Why and How
目录先从一个问题出发三大 API 如何管理对话历史Chat Completions APIOpenAI / Azure / DeepSeek / QwenResponses APIOpenAI / Azure OpenAIMessages APIAnthropic Claude国内主流模型的协议兼容情况两种根本模式从 API 差异中提炼服务端管理Service-Managed客户端管理Client-Managed服务端管理的两种子形态线性对话Linear分叉对话ForkingMAF 的统一抽象AgentSession ChatHistoryProviderAgentSession通用状态容器AgentSessionStateBag线程安全的类型化字典会话序列化跨进程/重启恢复ChatHistoryProvider可插拔的存储后端ProviderSessionState连接两者的桥梁调用生命周期三阶段精密协作⚠️ 关键前提默认 InMemory Provider 的按需创建完整调用时序图阶段一——从 StateBag 读取历史并标记来源消息来源标记防重复的闭环机制InMemoryChatHistoryProvider内置实现的源码剖析构造过程读取实现ProvideChatHistoryAsync存储实现StoreChatHistoryAsyncChatReducer 的两个触发时机实战40 行代码实现 SQLite 持久化 Provider会话恢复只需两行背后的三种设计模式模式一状态外部化Externalized State模式二模板方法Template Method模式三关注点分离Separation of Concerns如何选择存储策略总结先从一个问题出发多轮对话需要上下文模型才能理解你之前说的指的是什么。那么问题来了这些上下文历史消息由谁来维护、存在哪里答案因 API 而异这是很多开发者第一次接触多 Provider 场景时感到困惑的根源。三大 API 如何管理对话历史目前主流的三大 AI API 在历史管理上走了完全不同的路。Chat Completions APIOpenAI / Azure / DeepSeek / Qwen谁管历史你客户端每次请求必须把完整的历史消息全部发给服务端服务本身是无状态的处理完即忘。// 第三轮对话的请求 payload必须携带前两轮的全部内容 { model:gpt-4o, messages:[ {role:system,content:你是技术助手}, {role:user,content:我叫圣杰},// 第一轮 {role:assistant,content:你好圣杰}, {role:user,content:推荐一本技术书},// 第二轮 {role:assistant,content:推荐《CLR via C#》}, {role:user,content:这本书适合初学者吗}// 第三轮新消息 ] }特征请求 payload 随对话轮次线性增长数据完全在你手里行业通用DeepSeek、Qwen 等大多数模型都兼容。Responses APIOpenAI / Azure OpenAI谁管历史服务端每次请求只需发送新消息 上一次响应的 ID服务端自动维护完整会话树。// 第三轮对话的请求 payload只有新消息 { model:gpt-4o, input:这本书适合初学者吗, previous_response_id:resp_r2_xyz// 指向上一次响应 }响应中会包含新的id下一轮用它继续{ id:resp_r3_abc,// ← 下一轮用这个 output:[ ... ] }特征请求 payload 固定极小天然支持分叉同一个response_id可以派生出多条对话分支但数据存在 OpenAI/Azure 服务器受隐私合规约束。Messages APIAnthropic Claude谁管历史你客户端与 Chat Completions API 类似每次必须携带完整历史。但格式不同system是独立的顶级字段content是内容块数组支持文本、图片、工具结果混合。{ model:claude-opus-4-5, system:你是技术助手, max_tokens:1024, messages:[ {role:user,content:我叫圣杰}, {role:assistant,content:你好圣杰}, {role:user,content:这本书适合初学者吗} ] }特征与 Chat Completions 同属客户端管理但max_tokens是必填项工具结果回传格式使用role: user 内容块而非role: tool。国内主流模型的协议兼容情况在选择 API 协议之前有必要先搞清楚你用的模型支持哪些协议。下表基于各厂商截至 2026 年 4 月的公开文档整理后续能力可能变化请以官方最新文档为准模型 / 厂商Chat Completions APIResponses APIMessages APIDeepSeek深度求索✅ 官方支持端点api.deepseek.com❌✅ 官方支持端点api.deepseek.com/anthropicQwen / 通义千问阿里云百炼✅ 官方支持端点dashscope.aliyuncs.com/compatible-mode/v1✅ 官方支持端点/compatible-mode/v1/responses支持previous_response_id❌Kimi月之暗面✅ 官方支持端点api.moonshot.cn/v1/chat/completions❌❌智谱 AIGLM 系列✅ 官方支持端点open.bigmodel.cn/api/paas/v4/chat/completions❌✅MiniMax✅ 官方支持端点api.minimaxi.com/v1❌✅ 官方支持推荐端点api.minimaxi.com/anthropic小米 MiMo✅ 官方支持端点api.xiaomimimo.com/v1/chat/completions❌✅ 官方支持端点api.xiaomimimo.com/anthropic/v1/messages(Azure)OpenAI GPT✅ 原生✅ 原生协议发起者❌Anthropic Claude❌❌✅ 原生协议发起者结论一目了然Chat Completions API 是覆盖面最广的事实标准Responses API 目前只有 OpenAI/Azure 和 Qwen 支持Qwen 是国内首家跟进的Messages API 是 Anthropic 私有协议DeepSeek 提供了兼容端点。⚠️ 如果你的应用需要在国内模型和 OpenAI 之间切换不要把 Responses API 的服务端历史管理作为核心依赖——Kimi、GLM 等均不支持。两种根本模式从 API 差异中提炼现在答案就清楚了。三大 API 本质上代表了两种管理模式API谁管历史模式Chat CompletionsOpenAI/Azure/DeepSeek/Qwen客户端客户端管理Responses APIOpenAI/Azure服务端服务端管理Messages APIAnthropic Claude客户端客户端管理服务端管理Service-ManagedAI 服务自己保存对话状态。Agent 只持有一个服务端会话引用如conversation_id、thread_id或response_id每次请求时服务自动拼接上下文。适用场景• 对话数据隐私要求不高可接受数据托管于提供商• 希望由服务端自动处理历史拼接与压缩• 追求极简客户端实现不想管理历史• 对话轮次多、历史很长不想每次传全量 payload是否支持分叉取决于具体服务端模型有些是线性会话conversation/thread有些才是基于response_id的分叉模型。代价• ❌ 数据存在提供商服务器隐私风险• ❌ 失去对压缩策略的控制权• ❌ 换提供商需要迁移会话状态客户端管理Client-ManagedMAF 本地维护完整对话历史每次请求都把历史消息一起发给模型。服务是无状态的处理完即忘。适用场景• 数据隐私/合规要求高必须自主控制对话数据• 需要接入多个模型提供商或可能随时切换• 需要自定义压缩策略摘要、截断、滑动窗口• 需要将对话持久化到自有数据库Redis/PostgreSQL/Blob代价• ❌ 请求 payload 随对话增长而增大• ❌ 必须自己处理上下文窗口溢出问题• ❌ 生产环境需要额外的持久化基础设施服务端管理的两种子形态服务端管理并不是铁板一块还有线性 vs 分叉两种子模型线性对话Linear消息形成有序序列只能往后追加不能分叉。用户: 推荐三个旅游目的地 Assistant: 东京、巴黎、纽约 用户: 介绍第一个 Assistant: 东京是...适用场景客服机器人、简单 QA、需要严格审计追踪。代表实现Microsoft Foundry Prompt Agent、OpenAI Conversations API分叉对话Forking每个响应都有唯一response_id新请求可以从任意历史响应点继续天然支持撤回重试和并行探索。适用场景头脑风暴工具、A/B 响应测试、带重试功能的对话 UI。代表实现Microsoft Foundry Responses、Azure OpenAI Responses API、OpenAI Responses APIMAF 的统一抽象AgentSession ChatHistoryProvider无论底层是哪种模式你的应用代码保持不变// 无论 Chat Completions (客户端管理) 还是 Responses API (服务端管理) // 代码完全相同 AgentSession session await agent.CreateSessionAsync(); var r1 await agent.RunAsync(我叫圣杰, session); var r2 await agent.RunAsync(我叫什么名字, session);这种透明性来自两个核心抽象。AgentSession通用状态容器AgentSession是一个抽象基类其核心只有一个属性StateBag。public abstract class AgentSession { // 核心通用的键值状态容器 public AgentSessionStateBag StateBag {get;protectedset;}new(); // 服务发现模式 public virtual object? GetService(Type serviceType,object? serviceKey null); }AgentSessionStateBag线程安全的类型化字典StateBag底层是ConcurrentDictionary提供完整的线程安全 API方法功能线程安全SetValueT(key, value)存储任意类型对象✅GetValueT(key)类型安全地读取对象✅TryGetValueT(key, out value)安全尝试读取✅TryRemoveValue(key)删除键值对✅Serialize()/Deserialize()JSON 序列化/反序列化✅关键设计StateBag不仅用于存储消息历史它是一个通用容器任何 Provider 都可以在其中存储自己的状态互不干扰AgentSession.StateBagConcurrentDictionary ├── InMemoryChatHistoryProvider → { Messages: [...] } ├── TextSearchProvider → { SearchState: ... } └── PersonalizationProvider → { UserProfile: ... }会话序列化跨进程/重启恢复StateBag使用 JSON 序列化意味着整个会话状态包括完整消息历史可以被持久化和恢复// 序列化保存会话快照含全部历史 JsonElement snapshot await agent.SerializeSessionAsync(session); // 重启后恢复任意时刻从快照还原 AgentSession restored await agent.DeserializeSessionAsync(snapshot);ChatHistoryProvider可插拔的存储后端对于客户端管理场景ChatHistoryProvider控制历史存在哪里、怎么读写// 内置内存 Provider开发/原型阶段 AIAgent agent chatClient.AsAIAgent(newChatClientAgentOptions { ChatHistoryProvider newInMemoryChatHistoryProvider() }); // 自定义数据库 Provider生产环境 AIAgent agent chatClient.AsAIAgent(newChatClientAgentOptions { ChatHistoryProvider newSqliteChatHistoryProvider(db) });源码有一段关键注释道出了设计意图Since aChatHistoryProvideris used with many different sessions, it should not store any session-specific information within its own instance fields. Instead, any session-specific state should be stored in the associatedAgentSession.StateBag.一个 Provider 实例被多个 Session 共享。如果 Provider 自身保存消息不同用户的数据就会混在一起。MAF 的设计强制要求•AgentSession.StateBag 每个会话独立的数据保险箱 •ChatHistoryProvider 操作保险箱的钥匙 知道怎么读写但自己不存数据这带来了重要的可测试性提升单元测试只需 mockAgentSession完全不需要考虑 Provider 的并发状态。ProviderSessionState连接两者的桥梁ChatHistoryProvider不直接操作StateBag而是通过中介类ProviderSessionStateTState来简化状态管理。它的核心方法是GetOrInitializeState()public TState GetOrInitializeState(AgentSession? session) { // 1️⃣ 尝试从 StateBag 中读取已有状态 if(session?.StateBag.TryGetValueTState(this.StateKey,outvar state)is true state isnotnull) { return state; } // 2️⃣ 没有找到 → 调用初始化器创建新状态 state this._stateInitializer(session); // 3️⃣ 保存回 StateBag if(session isnotnull) { session.StateBag.SetValue(this.StateKey, state); } return state; }为什么需要这个中介直接操作 StateBag通过 ProviderSessionState每次手动写TryGetValue一行代码GetOrInitializeState()键名散落各处StateKey集中管理初始化逻辑重复初始化器统一配置序列化选项不一致统一的JsonSerializerOptions调用生命周期三阶段精密协作当你调用agent.RunAsync(你好, session)时底层发生了以下精密交互⚠️ 关键前提默认 InMemory Provider 的按需创建首先有一个非常重要的细节如果你没有显式配置ChatHistoryProvider框架才可能在首次RunAsync后按需创建默认的InMemoryChatHistoryProvider。如果你在构建 Agent 时已经传入自定义或内置 Provider它会从一开始就可用。// 未显式配置 Provider 时Agent 刚创建后调用 → null var p1 agent.GetServiceChatHistoryProvider();// null ❌ // 首次 RunAsync 完成后调用 → 默认 InMemoryChatHistoryProvider await agent.RunAsync(你好, session); var p2 agent.GetServiceChatHistoryProvider();// 非 null ✅原因在未显式配置 Provider 的前提下Agent 需要先探测底层 AI 服务是否自行管理历史。若服务端已管理就不需要本地 Provider确认不支持后才会按需创建默认的InMemoryChatHistoryProvider。源码中的触发点private void UpdateSessionConversationId(ChatClientAgentSession session, string? responseConversationId) { if(!string.IsNullOrWhiteSpace(responseConversationId)) { // 服务端管理历史 → 记录 ID不需要本地 Provider session.ConversationId responseConversationId; } else { // 服务端不管理历史 → 延迟创建 InMemoryChatHistoryProvider this.ChatHistoryProvider ??newInMemoryChatHistoryProvider(); } } 如果你想显式控制如使用自定义 Provider在构建 Agent 时直接传入即可不必依赖默认 Provider 的按需创建行为。完整调用时序图阶段一——从 StateBag 读取历史并标记来源return historicalMessages .Select(m m.WithAgentRequestMessageSource( AgentRequestMessageSourceType.ChatHistory,// 标记为历史消息 this.GetType().FullName!)) .Concat(context.RequestMessages);// 历史在前新消息在后阶段二——把合并后的完整消息列表发给 LLM。阶段三InvokedAsync——过滤来源只存储真正的新消息// 如果调用失败跳过存储 if(context.InvokeException is not null)returndefault; // 过滤掉来源为 ChatHistory 的消息防止重复存储 var filteredMessages this._storeInputMessageFilter(context.RequestMessages); returnthis.StoreChatHistoryAsync(filteredMessages,...);消息来源标记防重复的闭环机制这是最精妙的设计。消息在系统中可能来自多个来源来源类型枚举值说明外部输入External用户直接传入的消息消息历史ChatHistoryChatHistoryProvider 提供的历史消息上下文提供者AIContextProviderAIContextProvider 注入的上下文标记与过滤形成完整闭环默认过滤器的实现极为简洁private static IEnumerableChatMessage DefaultExcludeChatHistoryFilter( IEnumerableChatMessage messages) messages.Where(m m.GetAgentRequestMessageSourceType() ! AgentRequestMessageSourceType.ChatHistory);结论历史消息被送去模型用于上下文存储时自动排除永远不会被重复写入。开发者完全无需手动处理这个问题。InMemoryChatHistoryProvider内置实现的源码剖析了解默认实现如何利用这套机制对于自定义 Provider 很有帮助。构造过程public sealed class InMemoryChatHistoryProvider:ChatHistoryProvider { // 桥梁通过 ProviderSessionState 操作 StateBag private readonly ProviderSessionStateState _sessionState; public InMemoryChatHistoryProvider(InMemoryChatHistoryProviderOptions? options null) :base(options?.ProvideOutputMessageFilter, options?.StorageInputMessageFilter) { this._sessionState newProviderSessionStateState( options?.StateInitializer ??(_ newState()),// 默认空消息列表 options?.StateKey ??this.GetType().Name,// 默认类名作为 Key options?.JsonSerializerOptions); this.ChatReducer options?.ChatReducer; this.ReducerTriggerEvent options?.ReducerTriggerEvent ?? ChatReducerTriggerEvent.BeforeMessagesRetrieval; } }读取实现ProvideChatHistoryAsyncprotected override async ValueTaskIEnumerableChatMessage ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken) { // 1️⃣ 通过桥梁从 Session.StateBag 获取状态 var state this._sessionState.GetOrInitializeState(context.Session); // 2️⃣ 如果配置了 ChatReducer 且触发时机为读取前默认 if(this.ReducerTriggerEvent is BeforeMessagesRetrieval this.ChatReducer isnotnull) { state.Messages (awaitthis.ChatReducer.ReduceAsync( state.Messages, cancellationToken)).ToList(); } // 3️⃣ 返回消息列表基类负责标记来源和合并 return state.Messages; }存储实现StoreChatHistoryAsyncprotected override async ValueTask StoreChatHistoryAsync( InvokedContext context,CancellationToken cancellationToken) { var state this._sessionState.GetOrInitializeState(context.Session); // 合并请求消息和响应消息追加到列表 // 注意此时的 RequestMessages 已被基类过滤不含 ChatHistory 来源的消息 var allNewMessages context.RequestMessages.Concat(context.ResponseMessages ??[]); state.Messages.AddRange(allNewMessages); // 如果配置了 ChatReducer 且触发时机为添加后 if(this.ReducerTriggerEvent isAfterMessageAdded this.ChatReducer isnotnull) { state.Messages (awaitthis.ChatReducer.ReduceAsync( state.Messages, cancellationToken)).ToList(); } }ChatReducer 的两个触发时机ChatReducer用于控制上下文窗口截断、摘要、滑动窗口等支持两个触发点实战40 行代码实现 SQLite 持久化 Provider继承ChatHistoryProvider只需实现两个方法public class SqliteChatHistoryProvider:ChatHistoryProvider { private readonly ChatDatabase _db; private readonly ProviderSessionStateState _sessionState; public SqliteChatHistoryProvider(ChatDatabase db):base() { _db db; // ProviderSessionState 是桥梁首次访问时触发 LoadFromDatabase // 后续访问直接读 StateBag 内存缓存兼顾持久化与性能 _sessionState newProviderSessionStateState( stateInitializer: session LoadFromDatabase(session), stateKey:nameof(SqliteChatHistoryProvider)); } // 读取从 StateBag 缓存或首次从 SQLite获取历史 // 基类负责标记来源 与新消息合并 protected override ValueTaskIEnumerableChatMessage ProvideChatHistoryAsync( InvokingContext context,CancellationToken ct) { var state _sessionState.GetOrInitializeState(context.Session); return new ValueTaskIEnumerableChatMessage(state.Messages); } // 存储写入 SQLite 同步更新 StateBag 缓存 // 基类已过滤 ChatHistory 来源只有真正的新消息会到达这里 protected override ValueTaskStore ChatHistoryAsync( InvokedContext context,CancellationToken ct) { var state _sessionState.GetOrInitializeState(context.Session); var newMessages context.RequestMessages .Concat(context.ResponseMessages ??[]).ToList(); // 写入 SQLite使用 AIJsonUtilities.DefaultOptions 完整序列化 // 可保留 Function Calling、Tool Results 等复杂消息结构 SaveToDatabase(context.Session, newMessages); // 同步内存缓存 state.Messages.AddRange(newMessages); return default; } // stateInitializer 回调首次访问时从数据库加载 private State LoadFromDatabase(AgentSession? session){/* ... */} }关键设计决策•ProviderSessionStateTState封装了 StateBag 的首次初始化 后续缓存逻辑一行代码搞定•AIJsonUtilities.DefaultOptions序列化ChatMessage完整保留 Function Calling、Tool Results 等复杂内容•无需处理防重复框架的标记-过滤闭环已自动处理会话恢复只需两行// 重启后恢复已有会话把 sessionId 写入 StateBag // Provider 的 stateInitializer 会在首次访问时自动从数据库加载 var session await agent.CreateSessionAsync(); session.StateBag.SetValue(SqliteSessionId, existingSessionId);背后的三种设计模式理解 MAF 为什么这样设计有助于举一反三地扩展它。模式一状态外部化Externalized StateChatHistoryProvider将状态外部化到AgentSession.StateBag这是经典的无状态服务设计传统有状态设计MAF 无状态设计Provider 内部持有DictionarysessionId, messagesProvider 从session.StateBag读写状态Provider 需要清理过期 SessionSession 被 GC 时状态自然回收Provider 无法简单序列化Session 完整序列化包含所有状态多实例部署需同步 Provider 状态Session 独立传递天然支持分布式模式二模板方法Template MethodChatHistoryProvider使用经典的模板方法模式——基类定义流程骨架子类实现具体步骤三层可重写粒度让你按需介入自定义存储后端只需实现最内层两个方法自定义过滤逻辑重写中间层最外层提供固定的安全校验始终不可绕过。模式三关注点分离Separation of ConcernsChatHistoryProvider和AIContextProvider遵循完全相同的架构——它们都通过ProviderSessionStateTState管理各自独立的 StateBag 状态都使用相同的Invoking/Invoked生命周期钩子都为消息添加不同的来源标记ChatHistoryvsAIContextProvider。这意味着你学会扩展一个就学会了扩展所有。如何选择存储策略需求推荐方案快速原型不在意数据持久化InMemoryChatHistoryProvider可显式配置未显式配置时框架可按需回退创建默认实例数据隐私要求高需完全自控客户端管理 自定义 ProviderRedis/数据库需要撤回或并行探索分支Responses API服务端分叉模型简单聊天机器人追求最低复杂度Foundry Prompt Agent服务端线性模型需要自定义压缩策略摘要/截断客户端管理 ChatReducer配置生产环境需跨重启保持会话客户端管理 数据库 Provider利用 StateBag 序列化恢复一个实用原则从InMemoryChatHistoryProvider起步当遇到以下信号时再升级1. 用户要求上次聊过什么 → 换持久化 Provider2. 对话越来越长、成本飙升 → 配置ChatReducer3. 合规审查要求数据留在本地 → 切换到客户端管理模式总结MAF 的会话历史存储架构有几个核心设计值得记住1.两种模式一套 APIAgentSession统一封装了服务端和客户端管理的差异切换模式不改应用代码2.Provider 无状态ChatHistoryProvider只是操作逻辑数据永远属于AgentSession.StateBag3.StateBag 是通用容器不只存消息历史所有 ProviderChatHistory、TextSearch、Personalization……共享同一个 ConcurrentDictionary互不干扰4.ProviderSessionState 是桥梁封装了首次初始化 后续缓存逻辑一行代码搞定 StateBag 的读写5.默认 Provider 可按需创建未显式传入ChatHistoryProvider时Agent 会先探测服务端是否托管历史再决定是否回退创建默认 InMemory Provider6.标记-过滤闭环框架自动防止历史消息被重复存储开发者零负担7.模板方法三层重写自定义后端只需实现两个方法需要更深度定制时可分层介入基类始终保障校验安全8.两个方法扩展自定义任何存储后端只需实现ProvideChatHistoryAsync和StoreChatHistoryAsync这套设计让你能从最简单的InMemoryChatHistoryProvider起步在不改一行业务代码的前提下演进到生产级的分布式持久化方案。参考资料• Chat History Storage Patterns in Microsoft Agent Framework — Wes Steyn, Microsoft• Conversations Memory 官方文档引入地址