零依赖打造彩色终端 AI 聊天工具——基于 .NET 10 的 ChatAgentCLI
不用任何 NuGet 包,仅靠 .NET BCL 手写 SSE 流式解析 + Markdown 语法高亮渲染,不到 250 行代码实现一个带加载动画的彩色终端 AI 聊天客户端。
unsetunset一、起因unsetunset
在日常开发中,与 AI 模型对话几乎是刚需。Web 界面固然方便,但频繁切换窗口、复制粘贴代码片段的操作并不高效。如果能在终端里直接和 AI 对话,并且回复还能实时流式输出、带代码高亮,那开发体验会有很大提升。
市面上已有不少类似的 CLI 工具,但它们大多依赖大量第三方库。于是我想:能不能只用 .NET 内置库,从零手写一个?
这就是 ChatAgentCLI 的由来。
unsetunset二、效果预览unsetunset
启动时显示 ASCII Logo,输入问题后出现中文加载动画("思考中 |" → "整理思路 /" → "组织语言 -" → "生成回复 "),随后 AI 回复以流式方式逐字符输出,并根据 Markdown 语法实时着色:
代码块( ```):绿色文字 + 黑色背景加粗( text):黄色文字斜体( text):青色文字行内代码(`text`):白色文字 + 灰色背景标题( #):品红色文字
2.1 自定义 ASCII Logo
启动时的 ASCII 文字 Logo 可以通过 TAAG 在线生成。该工具提供了丰富的字体选项(Graffiti、Big、Slant 等),在文本框中输入你想要的文字,选择喜欢的字体后直接复制生成的 ASCII 字符画,替换到代码中对应的 Logo 字符串即可。
unsetunset三、技术架构unsetunset
整个项目极其精简,只有三个文件,零 NuGet 依赖:
ChatAgentCLI/├── ChatAgentCLI.csproj # .NET 10.0 项目文件├── Program.cs # 入口 + REPL 交互循环└── DeepSeekClient.cs # HTTP 客户端 + SSE 解析 + Markdown 着色渲染3.1 技术栈
| 零 NuGet 包 | |
unsetunset四、核心实现拆解unsetunset
4.1 流式 SSE 解析
DeepSeek API 返回的是 OpenAI 兼容的 SSE 格式流式响应,每行以 data: 开头。我们不使用任何 SSE 客户端库,而是直接逐行读取:
usingvar stream = await response.Content.ReadAsStreamAsync();usingvar reader = new StreamReader(stream, Encoding.UTF8);while ((line = await reader.ReadLineAsync()) != null){if (string.IsNullOrEmpty(line)) continue;if (!line.StartsWith("data:")) continue;var data = line.Substring(5).Trim();if (data == "[DONE]") break;usingvar doc = JsonDocument.Parse(data);var root = doc.RootElement;if (root.TryGetProperty("choices", outvar choices) && choices.GetArrayLength() > 0) {var choice = choices[0];if (choice.TryGetProperty("delta", outvar delta) && delta.TryGetProperty("content", outvar text)) {var chunk = text.GetString();// 渲染 chunk 到终端... } }}当收到新的 content 字段时,立即渲染到终端,用户看到的就是逐字跳出的流式效果。
4.2 逐字符 Markdown 着色
这是项目最有意思的部分。收到的文本是 Markdown 格式,但在终端中我们需要把它渲染成彩色文字。实现方式是一个简单的状态机,逐字符扫描:
bool inCodeBlock = false;bool bold = false;bool italic = false;for (int i = 0; i < chunk.Length; i++){// 代码块 ```if (chunk.Substring(i, 3) == "```") { inCodeBlock = !inCodeBlock; Console.ResetColor();if (inCodeBlock) { i += 2; Console.ForegroundColor = ConsoleColor.Green; Console.BackgroundColor = ConsoleColor.Black; }continue; }if (inCodeBlock) { Console.ForegroundColor = ConsoleColor.Green; Console.Write(chunk[i]);continue; }// 加粗 **text**if (chunk[i] == '*' && chunk[i + 1] == '*') { bold = !bold; i++; Console.ForegroundColor = bold ? ConsoleColor.Yellow : Console.ForegroundColor;if (!bold) Console.ResetColor();continue; }// 斜体 *text*、行内代码 `text`、标题 # 等...if (chunk[i] == '\n') { Console.ResetColor(); bold = false; italic = false; } Console.Write(chunk[i]);}Console.Out.Flush();每个 chunk 到来时即时渲染并 Flush,用户在终端看到的就是"逐字跳出"的效果,和 Web 端的流式输出体验一致。
4.3 加载动画
在请求发出后、第一个 chunk 到达前,终端需要给用户一个"正在处理"的反馈。这里实现了一个简单的加载动画:
privatevoidLoadingLoop(CancellationToken ct){var spinner = new[] { '|', '/', '-', '\\' };var phrases = new[] { "思考中", "整理思路", "组织语言", "生成回复" };var sw = Stopwatch.StartNew();int i = 0;while (!ct.IsCancellationRequested) {int pi = ((int)(sw.ElapsedMilliseconds / 3000)) % phrases.Length; Console.SetCursorPosition(0, _loadingRow); Console.Write($" {phrases[pi]}{spinner[i % spinner.Length]} "); i++; Thread.Sleep(150); }}动画在后台线程运行,每 150ms 刷新一次。当收到第一个内容 chunk 时,通过 CancellationTokenSource.Cancel() 停止动画并清除该行。
4.4 终端输入处理
终端输入没有直接用 Console.ReadLine(),而是用 Console.ReadKey(intercept: true) 手动处理每个按键,这样可以:
自定义退格行为 在输入区域下方绘制分隔线( ─字符)动态调整 Console.BufferHeight防止光标越界正确处理中文字符的显示宽度(CJK 字符占 2 列)
while (true){var key = Console.ReadKey(intercept: true);if (key.Key == ConsoleKey.Enter) break;if (key.Key == ConsoleKey.Backspace) {if (inputSb.Length > 0) { inputSb.Remove(inputSb.Length - 1, 1);// 清除整行并重绘,按 Unicode 类别计算显示宽度 Console.SetCursorPosition(0, inputRow); Console.Write(newstring(' ', 60)); Console.SetCursorPosition(1, inputRow); Console.Write(inputSb.ToString());int displayWidth = 1;foreach (char c in inputSb.ToString()) displayWidth += char.GetUnicodeCategory(c) == System.Globalization.UnicodeCategory.OtherLetter ? 2 : 1; Console.SetCursorPosition(displayWidth, inputRow); }continue; }if (!char.IsControl(key.KeyChar)) { inputSb.Append(key.KeyChar); Console.Write(key.KeyChar); }}unsetunset五、API 对接细节unsetunset
5.1 获取 API Key
前往 DeepSeek API Keys 页面 登录后点击 "Create New Key" 创建新的 API Key 复制生成的 Key(格式为 sk-开头),妥善保管
5.2 API 文档与 Base URL
DeepSeek API 文档详见 DeepSeek API Docs,核心信息:
https://api.deepseek.com | |
/chat/completions | |
stream: true |
5.3 请求体构建
使用 DeepSeek 的 OpenAI 兼容格式构建请求:
_http.DefaultRequestHeaders.Authorization =new AuthenticationHeaderValue("Bearer", apiKey);var requestBody = new{ model = "deepseek-chat", max_tokens = 4096, messages = new[] { new { role = "user", content = question } }, stream = true, stream_options = new { include_usage = false }};var json = JsonSerializer.Serialize(requestBody);usingvar content = new StringContent(json, Encoding.UTF8, "application/json");var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/chat/completions");request.Content = content;使用 C# 匿名类型 + JsonSerializer.Serialize 构建 JSON,无需引入 Newtonsoft.Json 等第三方库。.NET 内置的 System.Text.Json 完全够用。
5.4 在代码中使用
// Program.csvar chatClient = new DeepSeekClient( apiKey: "sk-your-key-here", baseUrl: "https://api.deepseek.com");await chatClient.AskStreamingWithLoadingAsync(userInput);unsetunset六、为什么零依赖?unsetunset
你可能会问:有现成的 SSE 库、Markdown 渲染库、终端 UI 框架,为什么不直接用?
这个项目本质上是一个技术验证——用最基础的工具能做出什么效果。零依赖意味着:
编译产物极小:没有一堆 DLL 跟着跑 没有依赖冲突:不需要担心包版本兼容性 可控性高:每一行代码都在自己手里,出 bug 能快速定位 学习价值:手写 SSE 解析和 Markdown 状态机的过程,比调用一个 await client.ChatStreamAsync()学到的东西多得多
当然,如果要投入生产,可能需要引入更成熟的终端 UI 库(如 Spectre.Console)来处理跨平台、表格、进度条等更复杂的需求。但作为一个轻量级工具,当前方案已经足够。
unsetunset七、不足与改进方向unsetunset
当前版本虽然能用,但还有一些可以优化的地方:
** 被拆到两个 chunk)可能无法正确识别 | |
unsetunset八、总结unsetunset
ChatAgentCLI 用不到 250 行代码、零外部依赖,实现了一个功能完整的终端 AI 聊天工具:
SSE 流式解析:逐行读取、实时响应 Markdown 着色:状态机逐字符渲染,彩色输出 加载动画:多线程 spinner + 中文提示短语轮播 终端交互:手动按键处理 + 缓冲区管理 + CJK 字符宽度适配
它证明了:很多时候,我们不需要引入一堆 NuGet 包,标准库就能把事情办好。
开源地址:https://github.com/yanjinhuagood/ChatAgentCLI
作者:小码编匠

.NET 8+MAUI跨平台 IoT 移动端,实时监控水温、转速与光强
WPF 表格终于能筛选了!支持嵌套对象、百万级数据、开箱即用
C# 工业机器视觉平台,实现 OpenCV 与 深度学习算法的可视化编排
填补.NET 生态空白:面向工业视觉的高性能 3D 点云/网格处理库
WPF + MVVM架构的开源高效工业级电池管理系统(BMS)
.NET + Surging 微服务引擎,快速搭建多协议物联网平台
Avalonia 跨平台聊天客户端实战:基于 Prism 的 MVVM 架构实现
.NET 9+ Avalonia + Prism 高性能、支持 AOT 跨平台桌面应用
一个真正好用的 .NET 开源短链系统:支持生成 + 实时监控
WPF 工业视觉检测系统:双工位(面阵 + 线扫)独立运行架构
.NET 10 + CQRS + MediatR 一个跨平台文档管理系统
谁说 WinForm 不能高颜值?看这个 Ant Design 无边框收银系统
WinForm + SQL Server 企业物资管理系统(库存、出入库、审批全搞定)
基于 WinForm、Halcon、OpenCV的多功能图像处理与机器视觉框架
WPF + Halcon + YOLO 工业视觉检测的全能上位机
零依赖!WinForm 车牌识别系统开发全流程(算法实现+模块拆解)
WPF/WinForm 也能用 ECharts?快来试试这个开源项目
WinForm 也能玩转工业物联网?这个轻量级 SCADA 数据采集网关做到了
觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力




夜雨聆风