C# + WPF 做了一个 AI 文档批量处理工具,我是怎么从 0 到发布的
一、背景
做了快 9 年 C# / WPF / WinForm 开发,最近开始认真尝试 AI 应用开发方向。
与其一直想"学 AI 以后能做什么",不如先做一个真正能跑起来的小工具。
所以我给自己定的思路很简单:
- 先做一个体量不大的工具
- 先解决一个明确的问题
- 先把东西发布出去
- 再根据真实反馈慢慢迭代
于是我做了这个工具:文省事。
它的定位很直接:批量读取文档,用 AI 生成摘要或提取关键信息。
目前支持:
- TXT
- DOCX
输出结果支持:
- Excel
- TXT
这篇文章不只讲技术实现,也记录一下从选题、开发到发布的完整过程。
二、为什么做这个工具
我一开始没有想做特别大的 AI 产品,也没有打算一上来就做 Agent、知识库或者复杂工作流。
因为对我来说,现阶段更重要的是:先把一个真正可用的小工具做出来。
选择"文档处理"这个方向,主要有几个原因:
1. 需求比较真实
很多场景都会遇到:
- 文档太多,根本没时间一个个看
- 需要快速知道每份文档讲了什么
- 想批量提取关键信息,比如价格、日期、联系人、结论
2. 技术实现难度适中
这个方向刚好能把我现有的能力用上:
- WPF 做界面
- C# 处理文件
- Semantic Kernel 接 AI
- Excel 导出结果
3. 容易快速做出 MVP
不需要复杂的用户系统,也不需要部署服务端,
先做成桌面工具就可以验证。
三、技术选型
| 模块 | 选择 | 原因 |
|---|---|---|
| UI 框架 | WPF .NET 8 | 自己最熟悉,开发效率高 |
| AI 接入 | Semantic Kernel | 微软官方,.NET 生态友好 |
| 模型平台 | 硅基流动 / DeepSeek | 国内可用,成本低 |
| PDF 读取 | Aspose.PDF | 提取文字比较方便 |
| Word 读取 | DocumentFormat.OpenXml | 官方库,免费 |
| Excel 导出 | ClosedXML | API 简洁,够用 |
| 架构 | 简化版 MVVM | 项目不大,控制复杂度 |
为什么没用 Python
因为我现在做的是AI 应用开发,不是训练模型。
对我来说,现阶段重点是:
- 能不能把 AI 能力接起来
- 能不能做成一个真实可用的工具
- 能不能跑通从开发到发布的完整过程
这些事情用 C# 完全可以做。
四、项目结构
项目结构比较简单:
AiDocTool/ ├── Models/ │ └── DocumentItem.cs ├── Services/ │ ├── DocumentReader.cs │ ├── AiDocumentService.cs │ └── BatchProcessor.cs ├── ViewModels/ │ └── MainViewModel.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── ApiKeyDialog.xaml └── ApiKeyDialog.xaml.cs主要分成三层:
DocumentReader
负责读取不同格式的文档内容:txt、pdf、docx
AiDocumentService
负责和 AI 模型通信:摘要、提取信息、流式输出
BatchProcessor
负责把"文件读取"和"AI处理"串起来,
并处理批量执行、导出结果这些事情。
五、核心功能设计
1. 文档统一读取入口
不同文件格式底层处理方式不同,
但对上层来说,只有一个统一入口:
publicstaticasyncTask<string>ReadAsync(stringfilePath){varext=Path.GetExtension(filePath).ToLower();returnextswitch{".txt"=>awaitReadTxtAsync(filePath),".pdf"=>awaitReadPdfAsync(filePath),".docx"=>awaitReadDocxAsync(filePath),_=>thrownewNotSupportedException($"不支持的格式:{ext}")};}这样上层不用关心具体格式,只要拿到文本内容就行。
2. AI 摘要和提取
AI 层主要做两件事:
SummarizeAsync:生成摘要ExtractAsync:提取指定信息
本质上就是 Prompt + 模型调用。
摘要 Prompt 核心代码:
varprompt=$""" 请对以下文档内容进行总结,要求:1.{lengthDesc}2.语言简洁,重点突出3.直接输出摘要内容,不需要任何前缀 文档内容:{truncated}""";varhistory=newChatHistory();history.AddUserMessage(prompt);varresult=await_chatService!.GetChatMessageContentAsync(history,cancellationToken:ct);returnresult.Content?.Trim()??"";提取信息 Prompt 核心代码:
varprompt=$""" 请从以下文档中提取:{extractTarget}要求:1.只提取文档中实际存在的信息2.找不到时明确说"未找到相关信息"3.结果格式清晰,每条信息单独一行4.直接输出结果,不需要额外解释 文档内容:{truncated}""";Prompt 设计几个关键点:
- 明确说「直接输出,不需要前缀」,否则 AI 会加「以下是摘要:」之类的废话
- 长度描述要具体,「200字」比「适中」效果好很多
- 文档内容要截断,避免超出 token 限制
3. 流式输出
v1.1 新增了流式输出,
就是像 ChatGPT 那样,结果一个字一个字蹦出来,
而不是等全部生成完再一次性显示。
核心实现:
publicasyncIAsyncEnumerable<string>SummarizeStreamAsync(stringcontent,SummaryLengthlength=SummaryLength.Medium,[EnumeratorCancellation]CancellationTokenct=default){EnsureInitialized();vartruncated=TruncateContent(content,8000);varhistory=newChatHistory();history.AddUserMessage(prompt);awaitforeach(varchunkin_chatService!.GetStreamingChatMessageContentsAsync(history,cancellationToken:ct)){if(!string.IsNullOrEmpty(chunk.Content))yieldreturnchunk.Content;}}批量处理时实时更新 UI:
varsb=newStringBuilder();awaitforeach(varchunkin_aiService.SummarizeStreamAsync(content,summaryLength,ct)){sb.Append(chunk);// 直接更新绑定属性,WPF 自动刷新 UIitem.Result=sb.ToString();}用户看到的效果是:
点击开始后,右侧结果框立刻开始有内容出现,
一个字一个字地显示,而不是一直等待。
4. 批量处理与导出
批量处理器负责把所有步骤串起来:
for(inti=0;i<list.Count;i++){if(ct.IsCancellationRequested)break;varitem=list[i];try{// 读取文档内容varcontent=awaitDocumentReader.ReadAsync(item.FilePath);// AI 处理(流式)varsb=newStringBuilder();awaitforeach(varchunkin_aiService.SummarizeStreamAsync(content,summaryLength,ct)){sb.Append(chunk);item.Result=sb.ToString();// 实时更新 UI}item.Status=ProcessStatus.Completed;progress.Report(newBatchProgressReport(i,true,""));}catch(OperationCanceledException){throw;}catch(Exceptionex){item.ErrorMessage=ex.Message;item.Status=ProcessStatus.Failed;progress.Report(newBatchProgressReport(i,false,ex.Message));}// 避免频繁请求被限流if(i<list.Count-1)awaitTask.Delay(600,ct);}导出 Excel 用 ClosedXML 实现,
每一行对应一个文件,包含文件名、摘要内容、处理状态。
六、踩过的坑
坑1:DeepSeek 返回 HTTP 402
现象:
调用 DeepSeek API 返回:
HTTP 402: invalid_request_error原因:
- 免费额度有有效期,过期直接返回 402
- 错误信息不够直观,容易误判为代码问题
解决:
方案 A:DeepSeek 充值,10 元够用很久
方案 B:换用硅基流动(推荐测试阶段使用)
- 新用户送 14 元免费额度
- API 格式完全兼容 OpenAI
- 只需要改两个参数:
// DeepSeekbuilder.AddOpenAIChatCompletion(modelId:"deepseek-chat",apiKey:"你的Key",endpoint:newUri("https://api.deepseek.com"));// 硅基流动builder.AddOpenAIChatCompletion(modelId:"deepseek-ai/DeepSeek-V3",apiKey:"你的Key",endpoint:newUri("https://api.siliconflow.cn/v1"));坑2:PDF 扫描版读取内容为空
现象:
用 Aspose.PDF 提取某些 PDF 文字,返回空字符串。
原因:
扫描版 PDF 本质是图片,没有真正的文字层,
文字提取库无法处理。
解决:
提取后判断是否为空,明确提示用户:
vartext=absorber.Text?.Trim();if(string.IsNullOrEmpty(text))thrownewException("PDF 内容为空或为扫描版图片,暂不支持扫描版 PDF");坑3:WPF 事件初始化阶段触发空引用
现象:
程序启动时直接崩溃,报NullReferenceException。
原因:RadioButton.Checked事件会在InitializeComponent()过程中就触发,
这时候其他控件可能还没有初始化完成,直接访问就会报空引用。
解决:
加IsLoaded判断,拦截初始化阶段的事件触发:
privatevoidSummaryLength_Changed(objectsender,RoutedEventArgse){// IsLoaded 为 true 才说明窗口已完全加载if(!IsLoaded||_vm==null||senderisnotRadioButtonrb)return;if(rb==RadioShort)_vm.SummaryLength=SummaryLength.Short;elseif(rb==RadioDetailed)_vm.SummaryLength=SummaryLength.Detailed;else_vm.SummaryLength=SummaryLength.Medium;}坑4:文档内容超出 Token 限制
现象:
大文档处理时 API 直接报错。
原因:
模型有 token 上限,文档内容过长会超出限制。
解决:
处理前截断内容,超长部分提示用户:
privatestaticstringTruncateContent(stringcontent,intmaxLength=8000){if(content.Length<=maxLength)returncontent;returncontent[..maxLength]+"\n\n[文档内容较长,已截取前段进行处理]";}后续版本计划做分段处理,逐段调用 AI 再合并结果。
坑5:API Key 不能写死在代码里
错误做法:
privateconststringApiKey="sk-xxxx";// 危险!正确做法:
让用户自己输入,保存到本地 AppData 目录:
varconfigPath=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"AiDocTool","config.txt");// 保存File.WriteAllText(configPath,apiKey);// 读取if(File.Exists(configPath))ApiKey=File.ReadAllText(configPath).Trim();同时把配置文件加入.gitignore,
避免提交到 GitHub 时泄露密钥。
七、从"开发完"到"发布出去",是完全不同的阶段
做完这个工具之后,我最大的感受不是技术层面的,而是:
“做出来"和"发出去”,是两件完全不同的事情。
开发阶段关注的是:
- 功能有没有跑通
- 代码逻辑对不对
- 界面有没有问题
发布之后开始关注的是:
- 标题别人看得懂吗
- 封面图够不够吸引人
- 用户为什么没有点进来
- 为什么曝光有了但浏览很低
- 描述里的卖点够不够清晰
这部分反而让我感觉,比写代码更像是在学产品和运营。
而且我发现一个很有意思的现象:
技术文章的数据,比工具销售的数据更容易起来。
写文章、讲技术、记录过程,反而更容易被别人找到、认可。
这也是为什么我决定继续坚持写技术文章,
把每一个功能点、每一个踩过的坑都记录下来。
八、目前发布后的真实情况
工具目前已经发布,说说真实的数据情况。
咸鱼:
- 曝光:225
- 浏览:11
- 成交:0
曝光到浏览的转化率偏低,
说明标题或者封面图不够吸引人,
用户看到了但没有点进去的欲望。
这周已经调整了标题,下周看数据变化。
CSDN 技术文章:
- 第一篇:阅读 198,点赞 4,收藏 3
- 第二篇:阅读 190,点赞 9,收藏 4
这个数据比我预期的要好,
说明技术内容是有价值的,方向继续就行。
整体感受:
第一周没有成交很正常,
但数据告诉我问题在哪里,这比什么都重要。
知道问题在标题和封面,那就改标题和封面。
知道技术文章数据不错,那就继续写技术文章。
九、接下来怎么迭代
工具层面:
- 流式输出(v1.1,已在做)
- 支持超长文档分段处理
- 支持扫描版 PDF(接 OCR)
- 支持更多格式(Excel、PPT)
- 处理前后文件大小对比显示
运营层面:
- 继续优化咸鱼标题和封面
- 调整小红书内容风格,从"工具介绍"改成"真实记录"
- 每周持续发技术文章,积累长尾流量
- 等有用户咨询后,主动问反馈
内容层面:
- 每周一篇技术文章(CSDN / 知乎 / 掘金)
- 每周三篇小红书(工具类 / 生活类 / 复盘类)
- 技术文章和功能迭代同步,做完就写
十、写在最后
这个工具不一定会很快带来多少收入,
但对我来说有一个很重要的意义:
我开始把"会开发"这件事,往"能做出可发布的产品"这件事上走了。
以前做项目,交付给甲方就结束了。
现在做工具,要想的是:
- 用户是谁
- 他们有什么问题
- 我的东西能不能真正帮到他们
- 他们会不会愿意为这个付钱
这些问题,比任何技术框架的学习都让我成长更快。
如果你也是 C# / .NET 开发者,
也在考虑做 AI 方向的副业,
我的建议只有一句话:
先做一个小而完整的东西,把它发出去。
不用等完全准备好,不用一上来就做很大,
先跑通一次完整的流程,比想很多都重要。
工具已发布,咸鱼搜索「文省事」可以找到。
如果本文对你有帮助,点个赞 🙏
后续会持续更新 C# + AI 实战内容,欢迎关注。
