WPF + Semantic Kernel 实战:AI文档批量处理工具开发记录

一、背景

做了快9年C# / WPF开发,最近开始探索AI应用方向。

第一个项目选了「AI文档批量处理工具」,
取名叫文省事,主要功能是:

  • 批量生成文档摘要
  • 批量提取关键信息
  • 结果导出Excel / TXT

本文记录完整开发过程和踩过的坑。


二、技术选型

模块 选择 原因
UI框架 WPF .NET 8 擅长,开发效率高
AI框架 Semantic Kernel 微软官方,.NET原生支持
AI模型 DeepSeek / 硅基流动 国内可用,价格低
PDF读取 Aspose.PDF 文字提取稳定
Word读取 DocumentFormat.OpenXml 官方库,免费
Excel导出 ClosedXML API简洁,免费
架构 MVVM 代码解耦

为什么选 Semantic Kernel

Semantic Kernel 是微软专门为.NET开发者
做的AI框架,相比直接调用HttpClient有几个优势:

  • 原生支持C#,API设计符合.NET习惯
  • 自动管理对话上下文(ChatHistory)
  • 支持多种模型无缝切换
  • 内置流式输出支持

三、项目结构

AiDocTool/
├── Models/
│ └── DocumentItem.cs // 文档数据模型
├── Services/
│ ├── DocumentReader.cs // 文档读取(TXT/PDF/DOCX)
│ ├── AiDocumentService.cs // AI处理核心
│ └── BatchProcessor.cs // 批量处理+导出
├── ViewModels/
│ └── MainViewModel.cs // MVVM数据层
├── Converters.cs // 值转换器
├── MainWindow.xaml // 主界面
├── MainWindow.xaml.cs // 交互逻辑
├── ApiKeyDialog.xaml // API Key设置弹窗
└── ApiKeyDialog.xaml.cs


四、核心实现

4.1 接入 DeepSeek(兼容OpenAI格式)

DeepSeek 的API完全兼容 OpenAI 格式,
用 Semantic Kernel 接入只需要两行配置:

builder.AddOpenAIChatCompletion(
modelId: “deepseek-chat”,
apiKey: “你的APIKey”,
endpoint: new Uri(“https://api.deepseek.com”)
);

同样,硅基流动也是兼容 OpenAI 格式:

builder.AddOpenAIChatCompletion(
modelId: “deepseek-ai/DeepSeek-V3”,
apiKey: “你的APIKey”,
endpoint: new Uri(“https://api.siliconflow.cn/v1”)
);

这意味着只需要改两个参数就能切换AI服务商。

4.2 文档摘要的Prompt设计

Prompt设计对结果质量影响很大。

我最终用的版本:

var prompt = $“”"
请对以下文档内容进行总结,要求:
1. {lengthDesc}
2. 语言简洁,重点突出
3. 直接输出摘要内容,
不需要"摘要:"等前缀

文档内容:
{truncated}
""";

几个关键点:

  • 明确说「直接输出,不要前缀」
    否则AI会加「以下是摘要:」之类的废话
  • 长度描述要具体
    「200字」比「适中」效果好很多
  • 文档内容要截断
    避免超出token限制

4.3 PDF读取

使用 Aspose.PDF 提取文字:

using var doc = new Document(filePath);
var absorber = new TextAbsorber();
doc.Pages.Accept(absorber);
return absorber.Text;

注意:Aspose.PDF免费版对PDF页数有限制
但文字提取功能不受影响,对我们的场景够用。

4.4 批量处理的节流控制

批量处理时需要在每次请求之间加延迟
避免频繁调用被API限流:

// 避免频繁请求
if (i < list.Count - 1)
await Task.Delay(600, ct);

600ms是测试后的经验值
太短容易限流,太长影响处理速度。

4.5 WPF事件在初始化阶段提前触发

这是我踩的一个经典WPF坑。

RadioButton的Checked事件
会在InitializeComponent()过程中就触发,
这时候其他控件可能还没初始化完,
导致NullReferenceException。

解决方案:用IsLoaded判断拦截:

private void SummaryLength_Changed(
object sender, RoutedEventArgs e)
{
if (!IsLoaded || _vm == null ||
sender is not RadioButton rb)
return;

if (rb == RadioShort)
    _vm.SummaryLength = SummaryLength.Short;
else if (rb == RadioDetailed)
    _vm.SummaryLength = SummaryLength.Detailed;
else
    _vm.SummaryLength = SummaryLength.Medium;

}

IsLoaded为true说明窗口已完全加载,
可以安全地访问所有控件。


五、踩过的坑

坑1:HTTP 402 余额不足

调用DeepSeek返回402错误。

原因:

  • 新注册的免费额度有有效期
  • 过期或用完后直接返回402
  • 错误信息不够直观

解决:

  • 充值,或者换用硅基流动
  • 硅基流动新用户有14元免费额度

坑2:PDF扫描版无法提取文字

扫描版PDF本质上是图片,
Aspose.PDF提取出来是空字符串。

目前处理方式:

  • 检测提取结果是否为空
  • 是空则提示用户「暂不支持扫描版PDF」
  • 后续考虑接入OCR解决

坑3:文档内容超出token限制

大文档直接传给AI会超出限制报错。

解决:截断到8000字符:

private static string TruncateContent(
string content, int maxLength)
{
if (content.Length <= maxLength)
return content;
return content[…maxLength] +
“\n\n[文档内容较长,已截取前段处理]”;
}

更好的方案是分块处理,
后续版本会优化。

坑4:API Key存储

第一版直接让用户输入Key,
保存到AppData目录的配置文件。

var configPath = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData),
“AiDocTool”, “config.txt”);

注意不要把Key写死在代码里
更不要提交到GitHub。


六、打包发布

dotnet publish -c Release -r win-x64
–self-contained true
-p:PublishSingleFile=false

–self-contained true 让用户不需要
单独安装.NET运行时,开箱即用。


七、后续计划

  • 支持超长文档分块处理
  • 支持扫描版PDF(OCR)
  • 支持更多格式(Excel/PPT)
  • 流式输出(打字机效果)
  • 本地模型支持(Ollama)

工具已发布,咸鱼搜索「文省事」可以找到。

如果本文对你有帮助,点个赞 🙏
后续会持续更新C# + AI实战内容。


源码

后续整理后开源到GitHub,届时更新链接。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐