前言
书接上回,关于MAF框架的探索,上次只聊了几个基本的Agent创建,本篇将深入探讨MAF框架的三个核心进阶特性:可观测性集成、聊天记录存储与持久化,以及为智能体赋予记忆能力。
我这里的案例代码都是跟着微软的官方文档,将智能体的角色案例改造成了一个"汽车大师",包含了一些自己的理解,可能存在偏差和错误,推荐大家优先查阅官方文档。
由于MAF框架,以及其相关的生态包,目前都是Preview状态(截止到2026.1.5),所以这里只是探索,目前上生产的话还是要慎重。
可观测性
这一趴,对应的文档地址是:learn.microsoft.com/en-us/agent...
实际上,微软的文档是有一个机翻的中文版的,但机翻的效果我个人感觉有点拉,不如直接看原版,然后用翻译软件或者AI助手翻译,效果更好,当然能无障碍阅读原版是最好了。
这一节,主要是展示MAF框架如何方便的开启可观测性。核心的目标是利用OpenTelemetry标准来自动记录和导出智能体育用户之间的交互数据。
整个接入流程可以概括为
- 安装必要的Nuget包
- 启用OpenTelemtry(TracerProvider)
- 配置代理
- 查看输出结果
好了,更具体的内容大家可以参考文档,我这里直接给出我的测试案例
csharp
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using OpenAI;
using OpenTelemetry;
using OpenTelemetry.Trace;
using System;
using System.ClientModel;
namespace AgentFrameworkQuickStart
{
public class ObservabilityAgent
{
public readonly ModelProvider modelProvider;
public ObservabilityAgent(ModelProvider modelProvider)
{
this.modelProvider = modelProvider;
}
public async Task ObservabilityDemo()
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource("agent-telemetry-source")
.AddConsoleExporter()
.Build();
var agent = new OpenAIClient(
new ApiKeyCredential(modelProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(modelProvider.Endpoint) })
.GetChatClient(modelProvider.ModelId)
.CreateAIAgent(instructions: "你是个资深汽车大师,了解很多汽车知识,包括配置,价格,驾驶体验的等等,回复内容尽可能简短高效,突出优缺点,给出综合购买建议,避免长篇大论", name: "汽车大师")
.AsBuilder()
.UseOpenTelemetry(sourceName: "agent-telemetry-source")
.Build();
await foreach (var update in agent.RunStreamingAsync("介绍一下新款宝马X3 25L这款车"))
{
Console.Write(update);
}
}
}
}
然后可以在看一下运行效果

存储聊天记录
官方文档在可观测性之后,详细介绍了持久化对话和第三方外部存储。为了聚焦核心概念,本文将直接展示如何实现一个自定义的聊天记录存储,该方法也涵盖了持久化的核心思想,大家可以参考官网查看完整的教程。
默认情况下,ChatClientAgent的聊天记录存储在AgentThread对象中,也就是内存当中,为了实现对话的持久化、跨会话恢复或大规模历史管理,开发者需要提供自定义存储实现。
本文的案例就是基于官方教程,创建了一个自定义存储类,继承抽象类 ChatMessageStore并分别实现一个存储(AddMessagesAsync)和检索(GetMessagesAsync)的关键方法。这里要注意的是,在检索方法里要考虑Token的限制,不过我们这里是技术验证阶段,这个也可以先跳过,降低一些心智负担。
代码的大体逻辑是
- 提供一个InMemoryVectorStore示例类VectorChatMessageStore
- 在第一次添加消息时生成一个唯一的 ThreadDbKey
- 定义了一个内部类ChatHistoryItem来保存消息文本、时间戳和序列化后的消息体。
- Serialize 方法只返回ThreadDbKey这样下次加载线程时,就能根据这个 Key 找回历史。
来看下代码
csharp
using AgentFrameworkQuickStart.Models;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI;
using Spectre.Console;
using System.ClientModel;
using System.Text.Json;
namespace AgentFrameworkQuickStart;
public class InMemoryChatHistoryAgent
{
private readonly ModelProvider _modelProvider;
private readonly string _threadStatePath;
private readonly VectorStore _vectorStore = new InMemoryVectorStore();
public InMemoryChatHistoryAgent(ModelProvider modelProvider, string threadStateFileName = "thread_state.json")
{
_modelProvider = modelProvider ?? throw new ArgumentNullException(nameof(modelProvider));
_threadStatePath = Path.Combine(Directory.GetCurrentDirectory(), threadStateFileName);
}
public async Task RunInteractiveChatAsync()
{
// 创建带自定义消息存储的 Agent
var agent = new OpenAIClient(
new ApiKeyCredential(_modelProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(_modelProvider.Endpoint) })
.GetChatClient(_modelProvider.ModelId)
.CreateAIAgent(
new ChatClientAgentOptions
{
Name = "记忆大师",
Description = "你是一个有长期记忆的助手,能记住之前的对话。",
ChatMessageStoreFactory = ctx => new VectorChatMessageStore(_vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions)
});
// 尝试恢复线程
AgentThread thread;
if (File.Exists(_threadStatePath))
{
Console.WriteLine("检测到已保存的对话状态,正在恢复...");
string json = await File.ReadAllTextAsync(_threadStatePath);
var element = JsonSerializer.Deserialize<JsonElement>(json, JsonSerializerOptions.Web);
thread = agent.DeserializeThread(element, JsonSerializerOptions.Web);
Console.WriteLine("对话已恢复!");
}
else
{
Console.WriteLine("开始新对话(使用 InMemory 向量存储记录历史)...");
thread = agent.GetNewThread();
}
while (true)
{
Console.Write("\n💬 你: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) continue;
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
var state = thread.Serialize(JsonSerializerOptions.Web).GetRawText();
await File.WriteAllTextAsync(_threadStatePath, state);
Console.WriteLine("线程状态已保存,再见!");
break;
}
if (input.Equals("clear", StringComparison.OrdinalIgnoreCase))
{
if (File.Exists(_threadStatePath))
File.Delete(_threadStatePath);
thread = agent.GetNewThread();
Console.WriteLine("已开启全新对话(旧历史不可见)");
continue;
}
try
{
var response = await agent.RunAsync(input, thread);
Console.WriteLine($"\n助手: {response}");
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
continue;
}
var updatedState = thread.Serialize(JsonSerializerOptions.Web).GetRawText();
await File.WriteAllTextAsync(_threadStatePath, updatedState);
}
}
//这个基本和文档的案例一致👇,我引入了AnsiConsole美化输出
private sealed class VectorChatMessageStore : ChatMessageStore
{
private readonly VectorStore _vectorStore;
public string? ThreadDbKey { get; private set; }
public VectorChatMessageStore(
VectorStore vectorStore,
JsonElement serializedStoreState,
JsonSerializerOptions? jsonSerializerOptions = null)
{
_vectorStore = vectorStore ?? throw new ArgumentNullException(nameof(vectorStore));
if (serializedStoreState.ValueKind == JsonValueKind.String)
ThreadDbKey = serializedStoreState.Deserialize<string>(jsonSerializerOptions);
}
public override async Task AddMessagesAsync(
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken = default)
{
ThreadDbKey ??= Guid.NewGuid().ToString("N");
AnsiConsole.MarkupLine($"[cyan]【Add】 ThreadKey: {ThreadDbKey}, 消息数: {messages.Count()}[/]");
var collection = _vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
await collection.UpsertAsync(
messages.Select(msg => new ChatHistoryItem
{
Key = $"{ThreadDbKey}_{msg.MessageId}",
ThreadId = ThreadDbKey,
Timestamp = DateTimeOffset.UtcNow,
SerializedMessage = JsonSerializer.Serialize(msg, SourceGenerationContext.Default.ChatMessage),
MessageText = msg.Text ?? ""
}),
cancellationToken);
}
public override async Task<IEnumerable<ChatMessage>> GetMessagesAsync(
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(ThreadDbKey))
return [];
AnsiConsole.MarkupLine($"[yellow]【Get】 从 ThreadKey: {ThreadDbKey} 读取消息[/]");
var collection = _vectorStore.GetCollection<string, ChatHistoryItem>("ChatHistory");
await collection.EnsureCollectionExistsAsync(cancellationToken);
// 获取该线程的所有消息(按时间倒序取最新 10 条)
var records = collection.GetAsync(
filter: x => x.ThreadId == ThreadDbKey,
top: 10,
options: new() { OrderBy = x => x.Descending(y => y.Timestamp) },
cancellationToken);
var messages = new List<ChatMessage>();
await foreach (var record in records)
{
messages.Add(JsonSerializer.Deserialize<ChatMessage>(
record.SerializedMessage!,
SourceGenerationContext.Default.ChatMessage)!);
}
messages.Reverse();
return messages;
}
public override JsonElement Serialize(JsonSerializerOptions? options = null)
=> JsonSerializer.SerializeToElement(ThreadDbKey, options);
private sealed class ChatHistoryItem
{
[VectorStoreKey] public string? Key { get; set; }
[VectorStoreData] public string? ThreadId { get; set; }
[VectorStoreData] public DateTimeOffset? Timestamp { get; set; }
[VectorStoreData] public string? SerializedMessage { get; set; }
[VectorStoreData] public string? MessageText { get; set; }
}
}
}



为智能体添加记忆
本节对应文档的地址:learn.microsoft.com/en-us/agent...
这一章关于概念性的内容我这里不过多介绍了,这里简单总结一下,我觉得这一篇是MAF的基础文档介绍中最为压轴的一篇,通过AIContextProvider可以让Agent具备"学习"和"个性化"的能力,构建出复杂交互系统。如文档所示
AIContextProvider是一个抽象类,您可以从中继承,并且可以与AgentThread关联以用于ChatClientAgent。 该功能允许:
- 在代理调用基础推理服务之前和之后运行自定义逻辑。
- 在调用基础推理服务之前,向代理提供其他上下文。
- 检查代理提供和生成的所有消息。
AIContextProvider本质上是一个拦截器或者说中间件,像是在Agent从"接受输入(InvokingAsync )"到"调用模型(InvokedAsync)"这个执行流程上切了2刀,这个过程可以参考以下流程图。

更多概念性内容大家还是参考文档吧,我这里就不再赘述了。咱们看案例吧
csharp
using AgentFrameworkQuickStart.Models;
using AgentFrameworkQuickStart.Tools;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI;
using Spectre.Console;
using System.ClientModel;
using System.Text;
using System.Text.Json;
namespace AgentFrameworkQuickStart
{
public class CarMasterMemory : AIContextProvider
{
private readonly IChatClient _innerClient;
public CarPreference Preference { get; private set; }
public CarMasterMemory(IChatClient client, CarPreference? pref = null)
{
_innerClient = client;
Preference = pref ?? new CarPreference();
}
public CarMasterMemory(IChatClient client, JsonElement serializedState, JsonSerializerOptions? options = null)
{
_innerClient = client;
Preference = serializedState.ValueKind == JsonValueKind.Object
? serializedState.Deserialize<CarPreference>(options) ?? new CarPreference()
: new CarPreference();
}
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken ct = default)
{
var sb = new StringBuilder("\n[后台画像已加载]");
if (Preference.BudgetMax > 0) sb.Append($" | 预算上限:{Preference.BudgetMax}万");
if (Preference.EnergyType != "未指定") sb.Append($" | 能源偏好:{Preference.EnergyType}");
if (Preference.MustHaves.Any()) sb.Append($" | 关键需求:{string.Join("、", Preference.MustHaves)}");
return new ValueTask<AIContext>(new AIContext { Instructions = sb.ToString() });
}
public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken ct = default)
{
if (context.RequestMessages.Any(m => m.Role == ChatRole.User))
{
try
{
var lastUserMessage = context.RequestMessages.LastOrDefault(m => m.Role == ChatRole.User)?.Text;
if (string.IsNullOrEmpty(lastUserMessage)) return;
var analysisOptions = new ChatOptions
{
ResponseFormat = ChatResponseFormat.Json,
Instructions = """
你是一个数据提取器。请分析用户的输入,提取购车意向。
返回 JSON 格式如下:
{
"BudgetMax": 数字 (如果是30万请写30, 必须是万为单位的数字),
"EnergyType": "字符串 (如: 纯电/燃油/混动)",
"MustHaves": ["需求点1", "需求点2"] (如果没有提到任何具体配置或功能需求,请返回空数组 [])
}
注意:如果是配置需求(如: 智驾、全景天窗、大空间),请放入 MustHaves。
"""
};
var extraction = await _innerClient.GetResponseAsync<CarPreference>(
context.RequestMessages.TakeLast(2), // 只看最近一两轮
analysisOptions);
if (extraction.Result != null)
{
var newInfo = extraction.Result;
if (newInfo.BudgetMax > 5000) newInfo.BudgetMax /= 10000;
if (newInfo.BudgetMax > 0) this.Preference.BudgetMax = newInfo.BudgetMax;
if (!string.IsNullOrEmpty(newInfo.EnergyType) && newInfo.EnergyType != "未指定" && newInfo.EnergyType != "null")
{
this.Preference.EnergyType = newInfo.EnergyType;
}
if (newInfo.MustHaves != null && newInfo.MustHaves.Any())
{
var validNewItems = newInfo.MustHaves
.Where(s => !string.IsNullOrWhiteSpace(s) && s != "无" && s != "null");
var updatedList = this.Preference.MustHaves.Union(validNewItems, StringComparer.OrdinalIgnoreCase).ToList();
this.Preference.MustHaves = updatedList;
}
}
}
catch (Exception ex)
{
// 调试用
// Console.WriteLine($"[DEBUG] 提取失败: {ex.Message}");
}
}
}
public override JsonElement Serialize(JsonSerializerOptions? options = null)
=> JsonSerializer.SerializeToElement(Preference, options);
}
public class CarMasterAgent : BaseAgent
{
private readonly VectorStore _vectorStore = new InMemoryVectorStore();
public CarMasterAgent(ModelProvider modelProvider) : base(modelProvider) { }
public async Task RunMasterAsync()
{
var client = new OpenAIClient(
new ApiKeyCredential(modelProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(modelProvider.Endpoint) });
var chatClient = client.GetChatClient(modelProvider.ModelId);
var agent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Name = "汽车大师",
Description = "你是一个毒舌但专业的汽车大师。你会根据后台画像(预算、需求)给出精准建议。",
// 1. 对话记录存入向量数据库(你写的逻辑)
ChatMessageStoreFactory = ctx => new VectorChatMessageStore(_vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions),
// 2. 画像提炼存入上下文提供者(我优化的逻辑)
AIContextProviderFactory = ctx => new CarMasterMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
});
var thread = agent.GetNewThread();
while (true)
{
var input = AnsiConsole.Ask<string>("[white]你:[/]");
if (input == "exit") break;
var response = await agent.RunAsync(input, thread);
AnsiConsole.MarkupLine($"\n[cyan]大师: {response}[/]");
var mem = thread.GetService<CarMasterMemory>()?.Preference;
AnsiConsole.MarkupLine($"[grey]>>> 系统画像更新 | 预算: {mem?.BudgetMax}w | 能源: {mem?.EnergyType} | 需求数: {mem?.MustHaves.Count}[/]");
}
}
public async Task RunMasterStreamAsync()
{
var client = new OpenAIClient(
new ApiKeyCredential(modelProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(modelProvider.Endpoint) });
var chatClient = client.GetChatClient(modelProvider.ModelId);
var agent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Name = "汽车大师",
Description = "你是一个既毒舌又专业的汽车大师。你会根据后台画像(预算、需求)给出精准建议。重点突出优缺点,有没有排面,不要长篇大论,扯一些没用的。",
// 对话记录存入向量数据库
ChatMessageStoreFactory = ctx => new VectorChatMessageStore(_vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions),
// 画像提炼存入上下文提供者
AIContextProviderFactory = ctx => new CarMasterMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
});
var thread = agent.GetNewThread();
AnsiConsole.MarkupLine("[bold green]--- 汽车大师已上线 (流式模式) ---[/]");
while (true)
{
var input = AnsiConsole.Ask<string>("\n[white]你:[/]");
if (input == "exit") break;
AnsiConsole.Markup("[cyan]大师:[/] ");
await foreach (var chunk in agent.RunStreamingAsync(input, thread))
{
// 直接输出片段,不换行
Console.Write(chunk);
}
Console.WriteLine(); // 结束后手动换行
await Task.Delay(500);
var mem = thread.GetService<CarMasterMemory>()?.Preference;
var panel = new Panel($"""
[yellow]预算限制:[/] {mem?.BudgetMax} 万
[yellow]能源偏好:[/] {mem?.EnergyType}
[yellow]核心需求:[/] {(mem?.MustHaves.Any() == true ? string.Join("、", mem.MustHaves) : "尚不明确")}
""")
{
Header = new PanelHeader("🚗 [bold]当前画像记录[/]"),
Border = BoxBorder.Rounded
};
AnsiConsole.Write(panel);
}
}
public async Task RunMasterWithToolsAsync()
{
var chatClient = new OpenAIClient(
new ApiKeyCredential(modelProvider.ApiKey),
new OpenAIClientOptions { Endpoint = new Uri(modelProvider.Endpoint) })
.GetChatClient(modelProvider.ModelId);
var agent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
Name = "汽车大师",
Description = "一个从业20年的专业汽车顾问,擅长结合用户画像进行精准推荐。",
// 将推理相关的配置放入 ChatOptions
ChatOptions = new ChatOptions
{
Instructions = "你是一个专业的汽车推荐助手。请优先参考后台画像。如果用户询问具体推荐,请调用 SearchCars 工具。",
Tools = [AIFunctionFactory.Create(new CarTool().SearchCars)]
},
AIContextProviderFactory = ctx => new CarMasterMemory(
chatClient.AsIChatClient(),
ctx.SerializedState,
ctx.JsonSerializerOptions),
ChatMessageStoreFactory = ctx => new VectorChatMessageStore(
_vectorStore,
ctx.SerializedState,
ctx.JsonSerializerOptions)
});
var thread = agent.GetNewThread();
while (true)
{
var input = AnsiConsole.Ask<string>("\n[white]你:[/]");
if (input == "exit") break;
// 使用流式输出
AnsiConsole.Markup("[cyan]大师:[/] ");
await foreach (var chunk in agent.RunStreamingAsync(input, thread))
{
Console.Write(chunk);
}
Console.WriteLine();
var mem = thread.GetService<CarMasterMemory>()?.Preference;
AnsiConsole.Write(new Panel($"预算: {mem?.BudgetMax}w | 能源: {mem?.EnergyType}").Border(BoxBorder.Rounded));
}
}
}
}
简单说明一下,这个案例,前半部分的代码是核心,重载了InvokingAsync和InvokedAsync两个方法,分别在调用模型前,和调用模型后做一些业务相关的操作,比如这个案例是根据用户输入,提炼一个用户画像,有多少预算,倾向买什么车等。
后半部分,定义了3个智能体,其中RunMasterAsync和RunMasterStreamAsync实际只有一个输出方式的区别,而RunMasterWithToolsAsync则包含了一个工具的调用,智能体会在合适的时机调用工具执行操作。
- 不使用工具

- 使用工具(动图后半部分执行工具)

结语
好了,受篇幅限制,就写到这里吧,下一篇再来聊聊工作流的部分。
附一条小插曲,截止到笔者发文,刚刚看到C#获得了2025年年度编程语言,多年来,C#经历了根本性的变革。从语言设计的角度来看,C#常常率先采纳主流语言中的新趋势。与此同时,它成功完成了两次重大范式转变:从仅限Windows到跨平台,以及从微软专有到开源。C#始终能在恰当的时机实现与时俱进的演进。
