用 Go 写 AI 命令行工具的实践

沈知远
沈知远
发布时间:2026-08-25 | 201人浏览过
原创

前言

AI 正在快速进入命令行场景,很多开发者最先接触到的并不是网页,而是能直接提问、立即看到结果的 CLI 工具。用 Go 来做这件事,既能利用标准库快速搭起输入循环和网络请求,也方便把程序编译成单文件分发。下面就按交互对话、SSE 流式响应和终端显示优化三步展开,看看一个可用的 AI CLI 应该抓住哪些关键实现点。

AI 工具正在快速进入命令行场景,很多开发者最先接触到的并不是网页,而是能直接在终端里提问、查看结果、继续追问的 CLI。用 Go 来做这类工具有很现实的优势:标准库够用、并发处理顺手,最终还能编译成单文件,便于分发和内部使用。下面就按“先跑通交互、再接入流式返回、最后优化终端输出体验”的顺序,拆开看一个 Go 版 AI 命令行工具该怎么搭起来。

交互式命令行为什么适合 AI 工具

不少 AI 应用已经提供了 CLI 形态,比如 Claude Code、CodeBuddy CLI。它的价值不只是在“能用命令行”,而是在开发和调试场景里更直接:启动快、资源占用少,也更容易嵌进现有工作流。

如果使用 Go 来写,这个方向会更顺手一些。原因也很明确:一方面,标准输入、HTTP 请求、并发控制这些基础能力都能直接用标准库处理;另一方面,编译后的产物通常就是一个单文件,分发和部署都比较省事。

从实现上看,一个最基本的 AI CLI,本质上就是一个持续循环:

  • 读取用户输入;
  • 把输入发送给 AI 服务;
  • 把返回结果输出到终端;
  • 继续等待下一轮输入。

先把这条主线跑通,后面的流式输出和显示优化才有意义。

先实现最小可用的对话循环

交互式对话的核心并不复杂,关键是保持一个稳定的读写循环。最简单的做法就是从标准输入读取内容,然后把回复打印出来:

用纸张卡片和箭头展示 Go AI 命令行工具的基础交互循环:输入、调用 AI、返回结果、继续下一轮。
AI CLI 的最小交互骨架先把输入循环跑通,CLI 工具的骨架就搭起来了。
func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Print("You: ")
        if !scanner.Scan() {
            break
        }
        input := scanner.Text()
        response := callAI(input)
        fmt.Printf("AI: %sn", response)
    }
}

这段代码的作用很直接:`bufio.NewScanner(os.Stdin)` 负责不断读取用户输入;`for` 循环保证程序不会只执行一轮;当 `scanner.Scan()` 结束时,程序退出;中间的 `callAI(input)` 则代表真正调用模型接口的部分。

对于入门实现来说,这种写法已经足够说明问题。它把 CLI 对话工具最重要的交互框架固定了下来,后续无论你接的是 OpenAI 兼容接口,还是内部封装的模型服务,整体结构都不会有太大变化。

流式输出怎么接:用 SSE 逐段接收模型结果

真正决定 AI CLI 体验的,往往不是“能不能返回答案”,而是“答案是不是实时出现”。如果必须等模型完整生成后再一次性输出,用户会明显感觉到停顿;而流式输出能把等待时间拆散,让响应显得更及时。

以专栏卡片结构展示 SSE 流式输出的处理过程,包括 data 行、DONE 结束标记、JSON 解析和内容打印。
SSE 流响应的处理路径流式输出的关键不是一次性拿到答案,而是持续接住模型吐出的内容片段。

为什么流式返回几乎是标配

很多大模型 API 都支持 SSE(Server-Sent Events)流式输出。对于命令行工具来说,这种方式非常合适:服务端不断推送生成片段,客户端一边接收、一边显示,不需要等整段文本全部完成。

以便签和纸卡对比展示终端直接打印与逐字输出的阅读差异,突出节奏控制和延迟设置。
逐字输出为何更适合终端终端体验是否自然,往往取决于输出节奏,而不只是有没有流式返回。

Go 里如何读取 SSE 响应

Go 处理这类响应并不复杂,关键点是把返回体当成持续到来的文本流来扫描:

func streamResponse(prompt string) {
    resp, _ := http.Post("https://api.openai.com/v1/chat/completions", "application/json", body)
    defer resp.Body.Close()
    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if strings.HasPrefix(line, "data: [DONE]") {
            break
        }
        if strings.HasPrefix(line, "data: ") {
            data := strings.TrimPrefix(line, "data: ")
            var chunk StreamChunk
            json.Unmarshal([]byte(data), &chunk)
            fmt.Print(chunk.Choices[0].Delta.Content)
        }
    }
}

这里可以关注三个事实:

  • SSE 的每一行通常以 `data: ` 开头;
  • 当收到 `data: [DONE]` 时,说明这轮生成结束;
  • 解析出 `chunk.Choices[0].Delta.Content` 后,直接打印就能形成连续输出。

也就是说,所谓“流式输出”,本质上并不是一次拿到完整答案,而是把模型不断吐出的片段按顺序接住,再原样送到终端里。

终端里怎么显示,体验差别会很明显

把流式结果直接 `fmt.Print` 出来,确实已经能工作,但终端体验未必理想。尤其是当 AI 回复很长时,内容会快速铺满屏幕,用户虽然看到了“实时输出”,却不一定更容易读。

逐字显示是一种更自然的做法

一个常见优化是加入逐字显示效果,用很小的延迟把文本一个字符一个字符打出来:

func typeWriter(text string, delay time.Duration) {
    for _, char := range text {
        fmt.Printf("%c", char)
        time.Sleep(delay)
    }
    fmt.Println()
}

在流式处理中,可以这样接起来:

for scanner.Scan() {
    data := parseStreamLine(scanner.Text())
    typeWriter(data.Content, 10*time.Millisecond)
}

这种方式的重点不是“模拟真人”,而是改善阅读节奏。用户能持续看到新内容出现,又不会一下子被整段长文本砸满终端窗口。

速度控制比效果本身更重要

逐字输出虽然更自然,但参数如果没调好,体验反而会变差。延迟太小,终端看起来像在高速刷屏;延迟太大,又会把本来已经生成出来的内容硬生生拖慢。

文中的示例使用了 `10*time.Millisecond`,这类数值更适合作为起点,而不是固定标准。是否合适,最终要看两件事:一是模型真实返回的速度,二是你的工具主要用于短问答还是长文本生成。命令行工具的体验优化,很多时候就在这种小细节上拉开差距。

适合用 Go 来做,但重点仍然是交互细节

把这几个部分合在一起看,一个可用的 Go 版 AI 命令行工具并不需要很重的工程结构:先有稳定的输入循环,再接上 SSE 流式响应,最后根据终端阅读习惯调整输出方式,就已经能形成一个体验不错的基础版本。

Go 在这里的优势很明确。并发模型和标准库让网络请求、流式读取、终端输出这些环节都能用比较简洁的方式完成;编译成单文件后,也适合做开发辅助工具或内部快速分发。不过,真正决定这个工具是否好用的,通常不是“功能有没有”,而是交互是否顺手、输出是否足够自然、等待过程是否让人愿意继续用下去。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表