Skip to content
Go back

AI Agent 对话历史存储模式:Microsoft Agent Framework 的架构选择

对话气泡流分叉为服务端云存储与本地客户端存储两条路径

构建 AI Agent 时,大家通常把精力放在模型选型、工具接入和 Prompt 工程上。但有一个更基础的架构问题常常被忽视:对话历史存在哪里?

这个选择直接决定了用户能不能续接之前的对话、能不能并行探索不同回答方向、能不能在隔天重新打开对话时让 Agent 还记得昨天说了什么。它还牵涉隐私合规、服务商依赖度,以及你愿意花多少工程成本来维护对话状态。

两种根本模式

服务端管理(Service-Managed)

AI 服务负责在自己的服务器上存储对话状态。Agent Framework 只在 AgentSession 里保存一个引用(比如 conversation_idthread_id),服务在处理每次请求时自动加载相关历史。

优势:

代价:

客户端管理(Client-Managed)

Agent Framework 在本地维护完整的对话历史(在 AgentSession 或关联的历史提供者中),每次请求时把相关消息一并发送。服务是无状态的——处理完请求就忘。

优势:

代价:

两种服务端存储形态

不是所有的服务端管理都一样,存储形态决定了你能构建什么样的用户体验。

线性(单线程)对话

最传统的聊天模型:消息形成一个有序序列,每条新消息追加到末尾,无法分叉或回退。

典型实现:Microsoft Foundry Prompt Agents、OpenAI Responses + Conversations API。

适合:客服机器人、简单问答流程、需要严格审计轨迹的场景。

局限:无法”回头”尝试不同回答,无法并行探索不同对话路径。

可分叉(Forking)对话

现代 Responses API 引入了更灵活的模型:每个响应都有唯一 ID,新请求可以引用任意一个历史响应作为续接点,从而实现对话树的分叉。

典型实现:Microsoft Foundry Responses 端点、Azure OpenAI Responses API、OpenAI Responses API。

适合:探索型和头脑风暴应用、A/B 测试不同回答策略、“撤销”和”重试”功能、树形结构对话 UI、多路径探索的 Agentic 工作流。

客户端管理的隐藏复杂度:压缩策略

服务端管理历史时,服务也顺带处理了压缩——把对话上下文保持在模型的 token 限制之内。你不用操心,但也无从控制。

客户端管理时,压缩成了你的责任。随着对话增长,必须采用明确的策略防止上下文窗口溢出并控制成本。常见方案:

Agent Framework 提供了所有这些模式的内置压缩策略,不需要从头实现。但你仍然需要选择、配置并维护适合自己场景的策略——这是服务端管理所没有的工作量。

Agent Framework 的抽象层

Microsoft Agent Framework 提供了一个统一编程模型,不管底层用哪种存储模式,应用代码都保持一致。

AgentSession:统一的对话容器

每次对话都用一个 AgentSession 表示。它负责:

// C# — 不管用哪个服务商,用法完全相同
AgentSession session = await agent.CreateSessionAsync();

var first = await agent.RunAsync("My name is Alice.", session);
var second = await agent.RunAsync("What is my name?", session);

// 底层细节由 session 处理:
// - 服务端管理:内部追踪 conversation_id
// - 客户端管理:在本地累积历史
# Python
session = agent.create_session()

first = await agent.run("My name is Alice.", session=session)
second = await agent.run("What is my name?", session=session)

ChatHistoryProvider:可插拔的存储后端

需要客户端管理存储时,历史提供者让你控制历史存在哪里、如何检索:

// C# — 内置内存提供者(最简单,默认选项)
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    ChatOptions = new() { Instructions = "You are a helpful assistant." },
    ChatHistoryProvider = new InMemoryChatHistoryProvider()
});

// 自定义数据库提供者(你来实现)
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    ChatOptions = new() { Instructions = "You are a helpful assistant." },
    ChatHistoryProvider = new DatabaseChatHistoryProvider(dbConnection)
});
# Python
from agent_framework import InMemoryHistoryProvider

agent = OpenAIChatCompletionClient().as_agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
    context_providers=[InMemoryHistoryProvider("memory", load_messages=True)],
)

关键设计原则:切换服务端管理和客户端管理时,应用代码不需要改变。

透明模式切换

比如你从 OpenAI Chat Completions(客户端管理)迁移到 Responses API(服务端管理+分叉),Agent 调用代码完全不变:

// C# — Chat Completions 和 Responses API 调用方式相同
var response = await agent.RunAsync("Hello!", session);

sessionprovider 在背后处理所有差异。这种解耦在实验不同服务商、迁移服务、构建服务商无关应用时很有价值。

Responses API 的三种配置模式

大多数 AI 服务的存储模式是固定的,但 Responses API(Microsoft Foundry、OpenAI、Azure OpenAI 均支持)是个例外——它通过 store 参数可配置。

模式一:服务端存储 + 分叉(默认)

最简配置,直接从 Responses 客户端创建 Agent。服务存储所有内容,支持通过响应 ID 分叉。

// C# — Responses API,store=true(默认)
AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetResponseClient("gpt-5.4-mini")
    .AsAIAgent(
        instructions: "You are a helpful assistant.",
        name: "ForkingAgent");

AgentSession session = await agent.CreateSessionAsync();
var response1 = await agent.RunAsync("What are three good vacation spots?", session);
// session 内部追踪响应 ID,可以从此处分叉出新的对话分支

模式二:客户端管理(store=false)

同样的 Responses 客户端,但禁用服务端存储。Agent Framework 在客户端管理历史,完全控制持久化和压缩。

// C# — Responses API,store=false
AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetResponseClient("gpt-5.4-mini")
    .AsIChatClientWithStoredOutputDisabled()
    .AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        ChatHistoryProvider = new InMemoryChatHistoryProvider()
    });

AgentSession session = await agent.CreateSessionAsync();
var response = await agent.RunAsync("Hello!", session);
// 历史存在 InMemoryChatHistoryProvider 里,不在服务端。
// 你来控制压缩。

模式三:线性对话(Conversations API)

基于 Responses 构建,提供线性线程模型。先在服务端创建一个对话,再把 session 与它绑定。

// C# — Responses API + Conversations(通过 Foundry)
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());

FoundryAgent agent = aiProjectClient
    .AsAIAgent("gpt-5.4-mini",
        instructions: "You are a helpful assistant.",
        name: "ConversationAgent");

// 一次调用创建服务端对话并绑定到 session
ChatClientAgentSession session = await agent.CreateConversationSessionAsync();

Console.WriteLine(await agent.RunAsync("What is the capital of France?", session));
Console.WriteLine(await agent.RunAsync("What about Germany?", session));
// 两条响应都属于同一个线性对话线程,由服务管理

如何选择

选择存储模式时,可以沿着这几个维度判断:

  1. 数据主权:对话数据是否必须留在自己的基础设施里?如果是,客户端管理是唯一选项。
  2. 用户体验:需要支持”重试”、“分叉”或并行探索吗?需要可分叉的服务端管理(Responses API)或客户端管理。
  3. 工程成本:愿意实现并维护压缩策略吗?如果否,服务端管理更省心。
  4. 服务商绑定:应用是否需要跨服务商可移植?客户端管理的状态不依赖任何服务商。

核心原则:根据实际需求选择(隐私、控制权、功能),而不是只图一开始方便。正确的存储模式会让你的应用在长期维护上更有竞争力。

参考


Tags


Previous

EF Core 10 数据库迁移的五种执行方式对比

Next

C# 正则表达式完全指南:从基础到现代 .NET API