我写了一个 DeepSeek 桌面 AI 编程助手,纯 WPF,双击即用
我用 WPF 写了个 DeepSeek 桌面 AI 编程助手——从重构到发布全记录
一句话:一个 63MB 单文件的 Windows AI 编程助手,双击即用,深度榨干 DeepSeek V4 Pro 的 KV Cache。
目录
一、为什么要从 TypeScript 切到 WPF?
📜 第一版的教训
第一版用 TypeScript + Node.js 写的 CLI。理想很丰满:
Ink + React 全屏 TUI → 漂亮的终端界面 → 丝滑的 AI 对话
现实很骨感:
| 问题 | 详情 |
|---|---|
| 🐛 Windows 终端渲染 | Ink 在 Windows Terminal 下布局错位,中文 IME 跟 readline 冲突 |
| 🐛 打包分发 | pkg 已死、sea 不稳定,没法让用户"双击即用" |
| 🐛 代码渲染 | 终端里做语法高亮和 diff 预览是天方夜谭 |
修修补补了两天,最后搞了个 Ink 渲染输出 + readline 处理输入 的混合架构——能跑,但丑。
✅ 为什么选 WPF + WebView2?
WebView2 是 Windows 上的 Electron,但它不需要打包浏览器。
Windws 10/11 自带 WebView2 Runtime,你可以用 marked.js + highlight.js + KaTeX 做流式 Markdown 渲染,效果碾压任何终端方案:
- 🎨 代码语法高亮(30+ 语言)
- 📐 LaTeX 数学公式
- 📊 Diff 绿红对比
- 🚀 流式实时渲染
最终技术选型:
┌──────────────────────────────────┐
│ WPF (.NET 10) │ ← 原生 Windows,GPU 加速
│ ┌────────────────────────────┐ │
│ │ WebView2 渲染引擎 │ │ ← marked.js + highlight.js + KaTeX
│ │ 流式 Markdown 实时渲染 │ │ 全部离线,不依赖外网
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ EventBus 事件总线 │ │ ← 22 个事件类型解耦 UI ↔ 业务
│ │ ServiceLocator DI │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 15 个内置工具 + MCP 扩展 │ │ ← 文件/Git/Shell/Web 抓取
│ │ 12 个 Slash 命令 │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
二、DeepSeek V4 Pro 适配——KV Cache 才是大招
⚡ 这是本文的核心。理解了这一节,你一个月能省几百块 API 费用。
2.1 两个关键数字
deepseek-v4-pro |
deepseek-v4-flash |
|
|---|---|---|
| 上下文窗口 | 1M tokens | 1M tokens |
| 最大输出 | 384K | 384K |
| 缓存命中(输入) | $0.003625 / 1M | $0.0028 / 1M |
| 缓存未命中(输入) | $0.435 / 1M | $0.14 / 1M |
| 缓存命中折扣 | 🔥 120 倍 | 50 倍 |
一个 900K prompt 的费用对比:
| 缓存未命中 | 缓存命中 | |
|---|---|---|
| V4 Pro | $0.39 / 次 | $0.003 / 次 |
| V4 Flash | $0.13 / 次 | $0.0025 / 次 |
💡 命中的话,聊一整天可能就几分钱。
2.2 怎么才能命中?
DeepSeek 的 KV Cache 用前缀匹配。缓存单元在"请求边界"产生——每条 system 消息末尾、每条 user 消息末尾、每次模型输出末尾。
看一个例子:
请求 1:┌─────────────────────────────┐
│ system: "你是助手" │ ← 缓存锚点
│ user: "北京是哪个首都?" │ ← 此处产生缓存前缀单元
└─────────────────────────────┘
请求 2:┌─────────────────────────────┐
│ system: "你是助手" │ ← ✅ 前缀匹配!
│ user: "北京是哪个首都?" │
│ assistant: "北京是中国的首都" │
│ user: "上海呢?" │
└─────────────────────────────┘
→ system + user1 命中缓存,只有 assistant + user2 需要计算
核心规律:
+ ✅ system 消息是缓存锚点 —— 绝对不能乱改
+ ✅ 多轮对话天然利于缓存 —— 每次追加,前缀不变
- ❌ 在消息列表中间插入/修改 → 后面的全废
2.3 我们最初的做法——好心办坏事 ❌
对话太长时需要压缩。最初的 SmartCompress 策略是:AI 生成摘要,插入一条 system 消息:
旧:[system, user1, assistant1, user2, assistant2, user3, assistant3]
↓ 超 900K → SmartCompress
新:[system, 📛 system-summary, user2, assistant2, user3, assistant3]
↑ 插入了新 system 消息!
↑ 前缀变了 → 下次请求 100% cache miss!
🔴 每压缩一次,缓存全废。费用暴涨 120 倍。
2.4 缓存友好改造 ✅
核心原则:永远不动 system 消息,摘要作为 user 消息注入。
// ❌ 旧方案:插入 system 消息 → 破坏缓存前缀
result.Add(ChatMessage.CreateSystem(
$"## 历史对话摘要\n\n{summary}"));
// ✅ 新方案:作为 user 消息注入 → system 前缀不变 → 缓存持续命中
result.Add(ChatMessage.CreateUser(
$"<conversation_history_summary>\n" +
$"以下是对之前 {count} 轮对话的摘要:\n\n{summary}\n" +
$"</conversation_history_summary>\n\n" +
$"请基于以上摘要和接下来的对话继续工作。"));
改造前后对比:
❌ 旧:[system, system-summary, round3, round4]
→ 前缀变化,cache miss
✅ 新:[system, user-summary, round3, round4]
→ system 前缀不变,cache hit!费用 ×0.01
↑ 摘要作为 user 消息
2.5 三级上下文策略
有了缓存友好基础,再加分层管理,渐进式而非一刀切:
| 层级 | Token 范围 | 策略 | 动作 |
|---|---|---|---|
| 🟢 正常 | 0 - 500K | — | 不做处理,让缓存自然累积 |
| 🟡 轻度 | 500K - 800K | TruncateToolResults |
截断 >5KB 的工具结果 |
| 🟠 中度 | 800K - 950K | SmartCompress |
AI 摘要旧轮次(缓存友好) |
| 🔴 重度 | 950K+ | SlidingWindow |
暴力裁剪最早轮次兜底 |
// App.xaml.cs 中的策略注册
var orchestrator = new ContextStrategyOrchestrator(options)
.AddStrategy(new TruncateToolResultsStrategy()) // 500K+
.AddStrategy(new SmartCompressStrategy()) // 800K+
.AddStrategy(new SlidingWindowStrategy()) // 950K+
.AddStrategy(new FileInjectionStrategy()); // 始终
三、其他技术优化
3.1 Token 估算——中英文分离
📐 官方文档:1 英文字符 ≈ 0.3 token,1 中文字符 ≈ 0.6 token。
之前一刀切 text.Length / 2.5,中英文混合场景误差 ±30%。
// ✅ 按字符类型分段估算
public static int EstimateTokenCountSync(string text)
{
int asciiChars = 0, cjkChars = 0;
foreach (var c in text)
{
if (IsCJK(c)) // 汉字/平假名/片假名/韩文
cjkChars++;
else
asciiChars++;
}
return (int)Math.Ceiling(asciiChars * 0.3 + cjkChars * 0.6);
}
3.2 上下文裁剪性能——O(n²) → O(n)
原始的 TrimContextIfNeeded 每删除一轮就重新估算全部消息:
// ❌ O(n²):每次循环 O(n),循环 n 次
while (rounds.Count > minRounds)
{
tokenCount = EstimateAll(messages); // ← 重复计算!
if (tokenCount <= max) break;
RemoveOldestRound();
}
改为预计算每轮 token,删除时减法:
// ✅ O(n):预计算一次,后续 O(1) 减法
int[] roundTokens = rounds.Select(r => r.Sum(m => Estimate(m.Content))).ToArray();
int currentTotal = roundTokens.Sum();
while (currentTotal > max && rounds.Count > minRounds)
{
currentTotal -= roundTokens[0]; // ← O(1)!
RemoveOldestRound();
}
100+ 轮对话裁剪速度提升 10-100 倍。
3.3 WebView2 渲染节流
⚡ SSE 流式输出每几毫秒一个 chunk。如果不节流,每秒 100+ 次 full re-render。
❌ 优化前:chunk→render chunk→render chunk→render...
每个 chunk 触发:marked.js + highlight.js + KaTeX + innerHTML
长文本直接卡死
✅ 优化后:chunk chunk chunk... → 50ms攒一批 → render
每秒 ~20 次渲染,始终流畅
C# 侧(50ms Stopwatch 节流):
private void AppendStreamText(string text)
{
_aiStreamBuffer.Append(text);
// 50ms 内跳过渲染,只攒文本
if (_renderThrottle.ElapsedMilliseconds < 50 && !_iterationFirstContent)
return;
_renderThrottle.Restart();
_ = _chatRenderer.UpdateAiContent(_aiStreamBuffer.ToString());
}
JS 侧(requestAnimationFrame 防抖):
let renderPending = false;
function updateAiContent(text) {
if (renderPending) return; // ← 跳过重复请求
renderPending = true;
requestAnimationFrame(() => {
renderPending = false;
aiBlock.innerHTML = marked.parse(text); // 每帧最多一次 DOM 更新
});
}
3.4 HttpClient 连接池
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5), // 连接复用
EnableMultipleHttp2Connections = true, // HTTP/2 多路复用
KeepAlivePingDelay = TimeSpan.FromSeconds(30), // 心跳保活
MaxConnectionsPerServer = 4 // 并发上限
};
连续 API 调用复用已有 TCP 连接,省去每次 TLS 握手,延迟 ↓30%。
四、项目结构一览
DeepSeekCode/
├── App.xaml.cs # 入口,DI 注册
├── MainWindow.xaml # 主界面(侧边栏 + WebView2 + 输入栏)
├── MainWindow.Conversation.cs # 流式对话核心 + 工具管线
│
├── Models/ # 数据模型(AppConfig/ChatMessage/ToolDefinition...)
│
├── Services/
│ ├── DeepSeekClient.cs # API 客户端(SSE + Thinking + FIM)
│ ├── ConversationManager.cs # 对话管理 + 上下文裁剪 O(n)
│ ├── ContextStrategy.cs # 三级上下文策略(缓存友好)
│ ├── EventBus.cs # 22 个事件类型解耦
│ ├── PlanModeService.cs # Plan 模式(5 阶段工作流)
│ ├── SubagentRunner.cs # 子代理引擎(并行 + Flash 模型)
│ └── ...
│
├── Tools/ # 15 个内置工具
│ ├── FileTools.cs # read/edit/write/glob/grep
│ ├── GitTools.cs # diff/log/commit
│ ├── ShellTool.cs # pwsh 执行 + 危险命令拦截
│ ├── TaskTool.cs # 子代理分派
│ └── ...
│
├── Commands/ # Slash 命令系统(12 内置 + 自定义)
├── MCP/ # JSON-RPC 2.0 over stdio
├── UI/ # ChatRenderer + StatusViewModel
└── Resources/js/ # 离线 JS 库(marked/highlight/KaTeX)
15 个内置工具:
| 工具 | 功能 |
|---|---|
read_file / edit_file / write_file |
文件读写编辑 |
glob / grep |
文件搜索 |
shell |
PowerShell 执行 |
webfetch |
网页抓取(正文提取) |
read_skill |
按需加载技能 |
git_diff / git_log / git_commit |
Git 集成 |
todo_write |
任务列表管理 |
task |
子代理分派(explore/general 双模式) |
enter_plan_mode / exit_plan_mode |
Plan 模式切换 |
12 个 Slash 命令:/help /clear /model /save /load /config /compact /settings /workspace /skills /mcp /plan
五、快速开始
📥 下载(推荐)
| 平台 | 链接 |
|---|---|
| GitHub | Releases · yuyu-s2c/DeepSeekCode |
| Gitee | Releases · yu9929/deep-seek-code |
下载 DeepSeekCode.exe(~63MB),双击运行。系统要求:Windows 10/11(自带 WebView2 Runtime)。
🔨 从源码构建
# 需要 .NET 10 SDK
git clone https://github.com/yuyu-s2c/DeepSeekCode.git
cd DeepSeekCode
dotnet build
# 打包为单文件 exe(无需运行时)
dotnet publish -p:PublishSingleFile=true -c Release -o ./publish
🚀 开始使用
- 点击齿轮图标 → 填入 DeepSeek API Key
- 选择模型:
deepseek-v4-pro(强力推理)或deepseek-v4-flash(快速响应) - 输入问题,开始对话
六、写在最后
💡 适配好 API 的特性,远比堆功能重要。
DeepSeek V4 Pro 的 KV Cache 如果不理解、不利用,同样的代码量,输入费用可能是别人的百倍。反过来,理解了原理之后改几行代码就能大幅优化——这就是"懂底层"和"只会调 API"的区别。
另一个感悟:桌面应用不等于复杂。 WPF + WebView2 的架构让 Windows 原生能力和 Web 渲染生态完美互补,比纯 Electron 方案轻量得多。一个 63MB 的单文件 exe,双击即用,不需要装任何东西——这种体验是 Web 应用永远给不了的。
项目完全开源,MIT 协议。欢迎 ⭐ star,欢迎提 issue 和 pr,一起把 Windows 上的 AI 编程体验做得更好。
个人开发爱好项目,市面上已有许多优秀产品,这个小工具只是多一种选择。有问题还请大佬们轻喷 🙏
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)