乐于分享
好东西不私藏

零依赖打造彩色终端 AI 聊天工具——基于 .NET 10 的 ChatAgentCLI

零依赖打造彩色终端 AI 聊天工具——基于 .NET 10 的 ChatAgentCLI

零依赖打造彩色终端 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 技术栈

项目
选择
语言
C# 13(.NET 10.0)
依赖
零 NuGet 包
,仅用 BCL
通信协议
SSE(Server-Sent Events)
API 格式
OpenAI 兼容格式(DeepSeek)
目标模型
DeepSeek Chat
目标平台
Windows x64

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 - 11);// 清除整行并重绘,按 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

  1. 前往 DeepSeek API Keys 页面
  2. 登录后点击 "Create New Key" 创建新的 API Key
  3. 复制生成的 Key(格式为 sk- 开头),妥善保管

5.2 API 文档与 Base URL

DeepSeek API 文档详见 DeepSeek API Docs,核心信息:

项目
Base URL
https://api.deepseek.com
接口路径
/chat/completions
认证方式
Bearer Token
流式参数
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 框架,为什么不直接用?

这个项目本质上是一个技术验证——用最基础的工具能做出什么效果。零依赖意味着:

  1. 编译产物极小:没有一堆 DLL 跟着跑
  2. 没有依赖冲突:不需要担心包版本兼容性
  3. 可控性高:每一行代码都在自己手里,出 bug 能快速定位
  4. 学习价值:手写 SSE 解析和 Markdown 状态机的过程,比调用一个 await client.ChatStreamAsync() 学到的东西多得多

当然,如果要投入生产,可能需要引入更成熟的终端 UI 库(如 Spectre.Console)来处理跨平台、表格、进度条等更复杂的需求。但作为一个轻量级工具,当前方案已经足够。


unsetunset七、不足与改进方向unsetunset

当前版本虽然能用,但还有一些可以优化的地方:

方向
说明
多轮对话
目前每次请求只发送当前问题,没有维护对话历史
API Key 管理
Key 硬编码在代码中,应改为环境变量或配置文件读取
跨平台支持
当前针对 Windows x64,可适配 Linux/macOS 终端
Markdown 解析健壮性
跨 chunk 的语法标记(如 ** 被拆到两个 chunk)可能无法正确识别
错误处理
网络异常、API 限流等情况需要更完善的降级策略
多模型切换
可支持配置不同模型(DeepSeek、Claude、GPT 等)

unsetunset八、总结unsetunset

ChatAgentCLI 用不到 250 行代码、零外部依赖,实现了一个功能完整的终端 AI 聊天工具:

  • SSE 流式解析:逐行读取、实时响应
  • Markdown 着色:状态机逐字符渲染,彩色输出
  • 加载动画:多线程 spinner + 中文提示短语轮播
  • 终端交互:手动按键处理 + 缓冲区管理 + CJK 字符宽度适配

它证明了:很多时候,我们不需要引入一堆 NuGet 包,标准库就能把事情办好。


开源地址:https://github.com/yanjinhuagood/ChatAgentCLI