WPF + Semantic Kernel 实现流式输出
打字机效果完整代码
一、什么是流式输出
普通AI调用:
等待 → 等待 → 等待 → 一次性返回全部结果
流式输出:
开始返回 → 逐字/逐词输出 → 像打字机一样
用户体验差距非常明显。
这也是为什么 ChatGPT 用打字机效果,
而不是等全部生成完再显示。
二、Semantic Kernel 流式输出 API
核心方法是:
GetStreamingChatMessageContentsAsync
和普通调用的对比:
// 普通调用(一次性返回)
var result = await chatService
.GetChatMessageContentAsync(history);
string content = result.Content;
// 流式调用(逐块返回)
await foreach (var chunk in chatService
.GetStreamingChatMessageContentsAsync(history))
{
string piece = chunk.Content; // 每次一小块
}
三、在服务层封装流式方法
使用 IAsyncEnumerable 作为返回类型,
配合 yield return 逐块返回内容:
using System.Runtime.CompilerServices;
public async IAsyncEnumerable SummarizeStreamAsync(
string content,
[EnumeratorCancellation] CancellationToken ct = default)
{
var history = new ChatHistory();
history.AddUserMessage($“请总结以下内容:\n{content}”);
await foreach (var chunk in _chatService! .GetStreamingChatMessageContentsAsync( history, cancellationToken: ct)) { if (!string.IsNullOrEmpty(chunk.Content)) yield return chunk.Content; }}
注意事项:
- 必须加 [EnumeratorCancellation] 特性
才能正确处理取消操作 - 过滤空 chunk,避免UI无意义刷新
- CancellationToken 要透传,
支持用户取消
四、在 WPF 里实时更新 UI
关键点:
流式输出在后台线程产生数据,
需要用 Progress 或者直接更新
绑定属性来刷新UI。
因为 ObservableCollection 和
INotifyPropertyChanged 的绑定属性
在 WPF 里可以跨线程更新
(只要是通过属性setter更新的):
// 在处理循环里
var sb = new StringBuilder();
await foreach (var chunk in
_aiService.SummarizeStreamAsync(content, ct))
{
sb.Append(chunk);
// 直接更新绑定属性,WPF自动刷新UI item.Result = sb.ToString();}
item.Status = ProcessStatus.Completed;
五、完整测试代码(控制台版)
先用控制台验证流式输出效果:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: “deepseek-ai/DeepSeek-V3”,
apiKey: “你的硅基流动Key”,
endpoint: new Uri(“https://api.siliconflow.cn/v1”)
);
var kernel = builder.Build();
var chatService =
kernel.GetRequiredService();
var history = new ChatHistory();
history.AddUserMessage(“用200字介绍一下人工智能的发展历史”);
Console.WriteLine(“流式输出开始:”);
await foreach (var chunk in chatService
.GetStreamingChatMessageContentsAsync(history))
{
if (!string.IsNullOrEmpty(chunk.Content))
Console.Write(chunk.Content); // 不换行,逐字追加
}
Console.WriteLine(“\n完成”);
六、踩过的坑
坑1:忘记加 [EnumeratorCancellation]
IAsyncEnumerable 方法里如果有
CancellationToken 参数,
必须加 [EnumeratorCancellation] 特性,
否则取消操作不会正确传递。
// ❌ 错误
public async IAsyncEnumerable StreamAsync(
CancellationToken ct = default)
// ✅ 正确
public async IAsyncEnumerable StreamAsync(
[EnumeratorCancellation] CancellationToken ct = default)
坑2:chunk.Content 可能为 null 或空
流式返回的每个 chunk
Content 属性可能是 null 或空字符串,
必须过滤:
// ❌ 不过滤,可能空字符串刷新UI
yield return chunk.Content;
// ✅ 过滤空值
if (!string.IsNullOrEmpty(chunk.Content))
yield return chunk.Content;
坑3:StringBuilder 要在循环外声明
// ❌ 每次chunk都新建,结果只有最后一块
await foreach (var chunk in …)
{
var sb = new StringBuilder(); // 错误位置
sb.Append(chunk.Content);
item.Result = sb.ToString();
}
// ✅ 循环外声明,累积所有内容
var sb = new StringBuilder(); // 正确位置
await foreach (var chunk in …)
{
sb.Append(chunk.Content);
item.Result = sb.ToString();
}
七、效果对比
普通调用:
处理10秒钟 → 结果突然全部出现
用户:不知道有没有在运行,很焦虑
流式输出:
开始后立刻看到文字蹦出来
用户:直观感受到AI在工作,体验好很多
八、完整项目
这是我做的「文省事」AI文档批量处理工具
里用到的核心技术。
工具功能:
批量生成文档摘要
批量提取关键信息
支持PDF/Word/TXT
结果导出Excel
感兴趣的可以咸鱼搜索「文省事」。
如果本文对你有帮助,点个赞🙏
后续持续更新C# + AI实战内容。