WPF + Semantic Kernel 实现流式输出

📅 2026/7/4 20:18:06 👁️ 阅读次数 📝 编程学习
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; }

}

注意事项:

  1. 必须加 [EnumeratorCancellation] 特性
    才能正确处理取消操作
  2. 过滤空 chunk,避免UI无意义刷新
  3. 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实战内容。