前两集我们让 Agent 讲了笑话、还给它装上了函数工具。但那些例子里,每次调用都是"一问一答、互不相关"。你问第二句时,Agent 早把第一句忘了。
这一集要解决的就是多轮对话:让 Agent 记住你们前面聊过什么,第二句话能接着第一句话的意思往下走。比如先让它讲个海盗笑话,再补一句"给这笑话加表情、用海盗鹦鹉的口吻再讲一遍",它得知道"这笑话"指的是哪一个,对吧?
示例源码位于:
dotnet/samples/01-get-started/03_multi_turn/
一、这一集比前两集多了什么
前两集的调用都是单轮:RunAsync("一句话") 进去,结果出来,结束。第三集只多了一个东西,就是会话对象 AgentSession:
- 调用前先用
agent.CreateSessionAsync()开一个会话; - 之后每一次
RunAsync(...)/RunStreamingAsync(...)都把这个session一起传进去; - 框架自动把每轮问答存进会话,下一轮调用时连同历史一起发给模型。
环境、建客户端、造 Agent 的写法跟第一集完全一样,这里不再重复,没配好密钥的回第一集看环境变量那一节即可。
二、先搞懂:多轮对话靠什么"记性"
大模型本身没有记忆:你每发一次请求,对它来说都是全新的对话,除非你把历史消息一条条附上。
两种做法:
- 手工拼历史:自己维护一个消息列表,每轮把旧消息和新问题一起发。能行,但麻烦,还得管 token 长度。
- 交给
AgentSession(本集做法):MAF 把"历史消息的管理"封装进了会话对象。你只管反复调用同一个session,框架在背后替你拼接上下文。这才是 Agent 框架存在的意义之一。
这个"记忆"存在你本地程序的会话对象里,不在 DeepSeek 服务端。关掉程序,会话就没了;重开一个
CreateSessionAsync()得到的也是全新空白会话。所以"让 Agent 记住"指的是"在当前运行周期内连续对话",不是跨进程持久化。
三、核心代码逐行讲
// Copyright (c) Microsoft. All rights reserved.
// 本示例演示如何用 OpenAI 兼容接口(这里用 DeepSeek)创建一个 Agent,
// 并通过会话(AgentSession)实现多轮对话:上下文由会话对象自动保留。
using System.ClientModel;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Chat;
// DeepSeek 提供 OpenAI 兼容的接口。密钥通过环境变量提供;
// 默认模型为 "deepseek-flash"(官方当前推荐)。其他可选模型包括 "deepseek-v4-pro"。
// 注意:旧模型名如 "deepseek-chat" / "deepseek-reasoner" 已不在官方文档中,应避免使用。
var apiKey = Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY") ?? throw new InvalidOperationException("未设置环境变量 DEEPSEEK_API_KEY。");
var model = Environment.GetEnvironmentVariable("DEEPSEEK_MODEL") ?? "deepseek-flash";
// 将 OpenAI 客户端指向 DeepSeek 的接口地址,而不是 api.openai.com。
OpenAIClient openAIClient = new(
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri("https://api.deepseek.com") });
// 获取选定模型的聊天客户端,并将其包装为 MAF 的 Agent,设定人设与名字。
ChatClient chatClient = openAIClient.GetChatClient(model);
AIAgent agent = chatClient.AsAIAgent(instructions: "你是一个擅长讲笑话的助手。", name: "笑话大王");
// 多轮对话(非流式):创建一个会话对象,后面每次调用都把同一个 session 传进去,
// Agent 就会记住之前说过的话,无需我们手动拼接历史。
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("给我讲一个关于海盗的笑话。", session));
Console.WriteLine(await agent.RunAsync("给这个笑话加上一些表情符号,并用海盗鹦鹉的口吻再讲一遍。", session));
// 多轮对话(流式):同样创建一个新会话,逐句把更新打印出来,
// 上下文依旧由 session 自动保留。
session = await agent.CreateSessionAsync();
await foreach (var update in agent.RunStreamingAsync("给我讲一个关于海盗的笑话。", session))
{
Console.WriteLine(update);
}
await foreach (var update in agent.RunStreamingAsync("给这个笑话加上一些表情符号,并用海盗鹦鹉的口吻再讲一遍。", session))
{
Console.WriteLine(update);
}
几个值得停下来看的地方:
CreateSessionAsync()是关键:它返回一个空的AgentSession。多轮对话的"连续性"全靠反复传同一个session实例。如果你每次都新建一个会话,那就又退回单轮了。RunAsync(提示词, session)多了一个参数:前两集我们只传提示词;这一集把会话作为第二个参数带上,框架就会自动把这次问答并入会话历史。流式版RunStreamingAsync(提示词, session)同理。- 非流式和流式用的是同一套会话机制:上面两段代码各开了一个新会话(
CreateSessionAsync()得到的是全新空白会话),互不干扰;想让流式接着非流式的话题聊,就得共用同一个session变量。 name: "笑话大王"和instructions跟第一集一样,没变。多轮对话改变的只是"调用时多了会话参数",Agent 本身的定义不变。
四、运行时到底发生了什么
把非流式那两段拆开看,幕后其实是:
CreateSessionAsync()在本地建好一个空会话;- 第一轮
RunAsync("讲个海盗笑话", session):框架把"空历史 + 这句话"发给 DeepSeek,拿到笑话,并把这次问答写回session; - 第二轮
RunAsync("加表情、用鹦鹉口吻再讲", session):框架把"上一轮的问答 + 这句新指令"一起发给 DeepSeek; - 模型因为看到了前面的笑话,才知道"这笑话"指什么,于是产出带表情、鹦鹉口吻的版本。
所以"记住上下文"靠的不是模型,而是框架在每次请求时帮你把历史带上。
五、动手改造
跑通之后,试试这几件事,把多轮对话吃透:
- 加第三轮:在同一个
session里再问"把它翻译成英文",验证它仍记得前面那只鹦鹉讲的笑话。 - 验证"失忆":把第二次
RunAsync改用await agent.CreateSessionAsync()新建会话再问,你会发现它答不上来"这笑话"指什么,直观感受会话的作用。 - 串起第二集的工具:把
03的session机制套到02_add_tools的 Agent 上,让带工具的 Agent 也能多轮连续对话(提示词换成"阿姆斯特丹天气怎样?那北京呢?")。
六、小结
这一集你学会了用 AgentSession 做多轮对话:用 CreateSessionAsync() 开会话,每次调用把同一个 session 传给 RunAsync / RunStreamingAsync,框架自动保留上下文。记住三点:记忆在本地会话里、不在服务端;连续性靠复用同一 session;每次新建会话等于开新话题。
下一篇我们讲"记忆"(04_memory),看看如何让 Agent 跨运行、跨会话地长期记住信息。
示例源码
- 本示例:
dotnet/samples/01-get-started/03_multi_turn/Program.cs - 上一篇:
dotnet/samples/01-get-started/02_add_tools/使用说明书.md - 系列参考:同目录
01_hello_agent、02_add_tools、04_memory等
参考资料
- Microsoft Agent Framework 的 OpenAI 扩展(
Microsoft.Agents.AI.OpenAI) - DeepSeek 官方 API 文档(中文):https://api-docs.deepseek.com/zh-cn/
- 参考博文:《MAF快速入门(1)化繁为简的AGENT创建范式》,作者 EdisonZhou(cnblogs)