第一章:从零构建终端 AI 助手 —— Octo 项目实战

LLM 智能体开发系列 · 第一章

本章以一个完整的终端 AI 聊天助手为例,带你从零搭建一个可运行的 LLM 应用。我们不造轮子,而是把正确的轮子装到正确的位置。


1. 我们要做什么?

在大模型时代,最简单的智能体形态就是一个对话助手------接收用户输入,调用 LLM API,返回生成结果。但"能跑"和"工程化"之间隔着一道鸿沟:

  • 配置散落在代码各处,换个模型要改好几个文件
  • API 调用没有超时和错误处理,网络一抖就崩
  • 日志全靠 fmt.Println,出问题无从排查
  • 没有结构,所有代码堆在 main.go 里

Octo 就是为了解决这些问题而生的。它是一个用 Go 编写的终端 AI 助手,麻雀虽小,五脏俱全:

能力 实现方式
配置管理 YAML 配置 + 结构体校验
LLM 对接 OpenAI 兼容 API + 流式输出
终端界面 Bubbletea TUI 框架
日志系统 slog + 按级别分文件
应用编排 setup 层统一初始化

2. 技术选型:为什么是这些?

在动手写代码之前,每个技术决策都值得被审视。选型不是追新,而是在当下约束下找到最合适的解。

2.1 开发语言:Go vs Python

维度 Go Python
部署形态 单二进制,零依赖 需要 runtime + venv + 依赖安装
启动速度 毫秒级 秒级(import 链长)
内存占用 低,适合常驻进程 高,尤其是多包场景
LLM 生态 成熟中(Eino、langchaingo) 极其丰富(LangChain、LlamaIndex...)
并发模型 goroutine 天然适合流式处理 asyncio 回调地狱 / trio
TUI 能力 Bubbletea 生态完善 rich / textual 可用但受限
学习曲线 简单直接 简单但动态类型易出错

选择 Go 的理由:

Octo 是一个终端 CLI 工具。Go 编译出的单二进制文件意味着用户下载即用,不需要装 Python、配虚拟环境、处理依赖冲突。对于 LLM 应用来说,Go 的 goroutine 天然适合处理流式响应------每个 stream.Recv() 都是一个轻量级的异步操作,不需要 asyncio 的心智负担。

结论:如果你的 LLM 应用是服务端/Web,Python 生态更丰富;如果是 CLI/边缘/嵌入式场景,Go 的部署优势是决定性的。


2.2 LLM 框架:Eino vs 其他方案

Go 生态中对接 LLM 有三条路:

方案 优点 缺点 适用场景
原始 HTTP 完全可控,零依赖 需自己实现流式解析、重试、协议 极简场景,只需一个 API
go-openai 官方 SDK 风格,API 稳定 仅限 OpenAI 协议,扩展性弱 只用 OpenAI/GPT
langchaingo LangChain Go 版,Chain 概念 社区活跃度低,文档不足 已有 Python LangChain 经验
Eino (CloudWeGo) 统一 ChatModel 接口 + Tool Calling + 流式 文档偏少,示例不够丰富 需要多模型切换 + 工具调用

选择 Eino 的理由:

graph TD A[你的代码] -->|统一接口| B[ChatModel] B --> C[OpenAI] B --> D[DeepSeek] B --> E[通义千问] B --> F[Claude] B --> G[Tool Calling] B --> H[流式输出]

Eino 的核心价值是接口统一 。ToolCallingChatModel 接口同时支持对话和工具调用,底层通过 eino-ext 适配不同 API 提供商。当前版本只用了 Stream() 方法,但接口本身已经为 Tool Use 预留了完整能力------第二章引入工具调用时,不需要换框架,只需在接口上扩展。

此外,Eino 由字节跳动 CloudWeGo 团队维护,底层依赖的 meguminnnnnnnnn/go-openai 在流式解析上做了大量边界处理,比自己手写 HTTP 调用可靠得多。

结论:原始 HTTP 适合学习,go-openai 适合只用 OpenAI,Eino 适合需要多模型 + 工具调用的生产级智能体。


2.3 TUI 框架:Bubbletea vs 其他方案

框架 架构模型 组件生态 社区活跃度 适合场景
Bubbletea Elm (MVU) bubbles + lipgloss 极高(Charm 全家桶) 复杂交互式 TUI
tview 直接式(构建后运行) 丰富(表格、树、模态框) 中等 管理后台、仪表盘
termui Widget 堆叠 基础 低 简单状态展示
自绘 ANSI 无框架 无 无 极简输出

选择 Bubbletea 的理由:

Octo 的 TUI 需要处理异步流式消息------用户按下 Enter 后,界面需要实时更新 LLM 的流式输出,同时还能响应 Ctrl+C 退出。这种"状态驱动 + 异步更新"的需求,恰好是 Elm 架构的强项:

graph LR A[KeyPressMsg<br/>'enter'] --> B[Update] B --> C[Model 状态更新] C --> D[View 重新渲染] D --> E[终端输出] E -->|streamMsg 到达| B E -->|KeyPressMsg| B

Bubbletea 的 tea.Cmd 机制天然支持异步操作------startStream() 返回一个 tea.Cmd,框架会在 goroutine 中执行并把结果作为 tea.Msg 送回 Update。这避免了手写 goroutine 管理的复杂性。

Charm 生态的 bubbles 提供现成的输入框、列表等组件,lipgloss 提供声明式样式------Octo 的 ASCII Logo 渐变色就是用 ANSI 转义码实现的,配合 lipgloss 可以更优雅地管理。

结论:如果你需要构建有状态、有交互的终端界面,Bubbletea 是 Go 生态的不二之选。简单展示用 termui 就够了。


2.4 配置格式:YAML vs 其他

格式 可读性 Go 生态支持 支持注释 支持复杂嵌套 文件大小
YAML 极高 yaml.v3 成熟 ✅ ✅ 中等
TOML 高 toml.v2 可用 ✅ 一般 小
JSON 中等 标准库 ❌ ✅ 大
ENV 低 os.Getenv ❌ ❌ 小

选择 YAML 的理由:

Octo 的配置需要支持嵌套结构(llm.model、log.level),需要注释(用户需要知道每个字段的含义),需要高可读性(用户直接编辑)。YAML 在这三项上都是最优解。

yaml 复制代码
# 这个配置文件就是 Octo 的"说明书"
llm:
  model: 'deepseek-v4-flash'  # 支持任何 OpenAI 兼容 API
  base_url: 'https://api.deepseek.com'
  api_key: 'sk-xxx'
  timeout: 300  # 秒

log:
  level: 1       # 1=Debug, 2=Info, 3=Warn, 4=Error
  format: text   # text 或 json
  log_dir: .logs

结论:YAML 是配置文件的事实标准。TOML 适合简单场景,JSON 不适合手写配置。


2.5 日志库:slog (stdlib) vs 其他方案

库 来源 性能 结构化日志 自定义 Handler 零依赖
slog Go 标准库 高 ✅ ✅ ✅
zap Uber 极高 ✅ 一般 ❌
zerolog RS 极高 ✅ 一般 ❌
logrus Sirupsen 中 ✅ 一般 ❌

选择 slog 的理由:

Octo 是一个 CLI 工具,不是高并发微服务。日志量级不需要 zap/zerolog 的极致性能优化。而 slog 作为 Go 1.21+ 的标准库,有几个关键优势:

  1. 零外部依赖 ------不引入额外的 go.mod 条目
  2. Handler 接口开放 ------Octo 的 LevelFileHandler 就是基于 slog.Handler 接口自定义的
  3. MultiHandler 支持------一行代码实现控制台 + 文件双输出
  4. 与 Go 生态一致 ------其他库的 slog.Logger 可以直接注入使用
go 复制代码
// slog 的 Handler 接口只有 4 个方法,实现成本极低
type Handler interface {
    Enabled(context.Context, Level) bool
    Handle(context.Context, Record) error
    WithAttrs([]Attr) Handler
    WithGroup(string) Handler
}

结论:新项目优先用 slog。只有在性能是瓶颈时才考虑 zap/zerolog。Octo 选择 slog 是因为"够用且无依赖"。


2.6 选型决策图

mindmap root((技术选型)) 语言: Go 单二进制部署 goroutine 流式处理 TUI 生态成熟 LLM: Eino 统一 ChatModel 接口 多模型切换 Tool Calling 预留 TUI: Bubbletea Elm 架构 异步消息驱动 Charm 全家桶 配置: YAML 可读性最佳 支持注释 嵌套结构自然 日志: slog 标准库零依赖 Handler 可扩展 MultiHandler 聚合

3. 架构总览

先看全景图,再逐层拆解:

graph TB subgraph 应用层 main[main.go<br/>入口] setup[setup/<br/>初始化编排] end subgraph 功能层 config[config/<br/>配置加载] llm[llm/<br/>模型对接] tui[tui/<br/>终端界面] end subgraph 基础层 structs[structs/<br/>数据定义] log[setup/init_log.go<br/>日志系统] end subgraph 外部依赖 eino[CloudWeGo Eino<br/>LLM 框架] bubbletea[Charm Bubbletea<br/>TUI 框架] yaml[YAML v3<br/>配置解析] end main --> setup setup --> config setup --> llm setup --> tui setup --> log config --> structs llm --> structs tui --> structs tui --> llm llm --> eino tui --> bubbletea config --> yaml

启动流程是一条清晰的初始化链:

sequenceDiagram participant M as main.go participant S as setup participant C as config participant L as llm participant T as tui M->>S: Init(ctx) S->>C: LoadConfig(ctx) C-->>S: OctoConfig S->>S: InitLog(&LogConfig) S->>L: InitModel(ctx, LLMConfig) L-->>S: ChatModel ready S->>T: Init(ctx, OctoConfig) T-->>S: TUI running S-->>M: (user exits)

4. 项目结构

bash 复制代码
octo/
├── main.go                  # 入口,仅调用 setup.Init
├── go.mod                   # Go 1.26 + 依赖声明
├── .octo/
│   └── config.yaml          # 运行时配置(LLM + 日志)
├── config/
│   └── config.go            # 配置加载与校验
├── structs/
│   └── config.go            # 配置结构体定义
├── llm/
│   ├── model.go             # ChatModel 初始化
│   ├── chat.go              # 流式对话接口
│   └── system_prompt.go     # 系统提示词
├── tui/
│   └── tui.go               # Bubbletea 终端界面
└── setup/
   ├── setup.go             # 应用初始化编排
   └── init_log.go          # 分级文件日志系统

设计原则 :main.go 只有 12 行,不包含任何业务逻辑。所有初始化由 setup 层编排,各模块职责单一、边界清晰。


5. 配置系统:让项目能"换"

5.1 数据结构

配置的骨架定义在 structs/config.go:

go 复制代码
// structs/config.go

type LLMConfig struct {
    Model   string `yaml:"model"`    // 模型名称,如 deepseek-v4-flash
    BaseURL string `yaml:"base_url"` // API 地址
    APIKey  string `yaml:"api_key"`  // 密钥
    Timeout int    `yaml:"timeout"`  // 超时时间(秒)
}

type OctoConfig struct {
    LLMConfig LLMConfig `yaml:"llm"`
    LogConfig LogConfig `yaml:"log"`
}

type LogConfig struct {
    Level     int    `yaml:"level"`      // 日志级别 1-4
    Format    string `yaml:"format"`     // text 或 json
    LogDir    string `yaml:"log_dir"`    // 日志目录
    AddSource bool   `yaml:"add_source"` // 是否添加源码位置
    Console   bool   `yaml:"console"`    // 是否输出到控制台
}

5.2 加载与校验

config/config.go 负责从 .octo/config.yaml 读取并校验:

go 复制代码
// config/config.go

var configPath = ".octo/config.yaml"
var OctoConfig *structs.OctoConfig

func LoadConfig(_ context.Context) error {
    getwd, err := os.Getwd()
    if err != nil {
        return err
    }
    configFilePath := filepath.Join(getwd, configPath)
    file, err := os.ReadFile(configFilePath)
    if err != nil {
        return errors.WithStack(err)
    }

    OctoConfig = &structs.OctoConfig{}
    if err = yaml.Unmarshal(file, OctoConfig); err != nil {
        return errors.WithStack(err)
    }

    return validateConfig(OctoConfig)
}

校验逻辑确保关键字段不为空:

go 复制代码
func validateConfig(cfg *structs.OctoConfig) error {
    if cfg.LLMConfig.Model == "" {
        return errors.New("llm model is required")
    }
    if cfg.LLMConfig.BaseURL == "" {
        return errors.New("llm base_url is required")
    }
    if cfg.LLMConfig.APIKey == "" {
        return errors.New("llm api_key is required")
    }
    if cfg.LLMConfig.Timeout <= 0 {
        return errors.New("llm timeout must be positive")
    }
    // ... 日志配置校验
    return nil
}

5.3 配置文件

用户只需编辑 .octo/config.yaml:

yaml 复制代码
llm:
  model: 'deepseek-v4-flash'
  base_url: 'https://api.deepseek.com'
  api_key: 'sk-xxx'
  timeout: 300

log:
  level: 1
  format: text
  log_dir: .logs
  add_source: true
  console: false

为什么要这样设计?

把配置从代码中剥离,意味着换模型只改 YAML,不碰 Go 代码。这在智能体开发中至关重要------你很快会发现,同一个 agent 可能需要在 GPT-4o、Claude、DeepSeek 之间来回切换做测试。


6. LLM 集成:让模型"说话"

6.1 为什么选 Eino?

LLM 的 API 调用看似简单(一个 HTTP POST),但生产级实现需要处理:流式响应解析、重试机制、token 计数、Tool Calling 协议等。

我们选用 CloudWeGo Eino 框架,它提供了统一的 ChatModel 接口:

graph LR A[你的代码] -->|调用| B[Eino ChatModel] B -->|HTTP| C[OpenAI 兼容 API] C -->|流式响应| B B -->|StreamReader| A

Eino 的好处是:同一套代码,换个 BaseURL 就能对接 DeepSeek、通义千问、Moonshot 等任何 OpenAI 兼容的 API。

6.2 模型初始化

go 复制代码
// llm/model.go

var tcm model.ToolCallingChatModel

func InitModel(ctx context.Context, llmConfig structs.LLMConfig) error {
    config := &openai.ChatModelConfig{
        APIKey:  llmConfig.APIKey,
        Timeout: time.Second * time.Duration(llmConfig.Timeout),
        BaseURL: llmConfig.BaseURL,
        Model:   llmConfig.Model,
    }
    var err error
    tcm, err = openai.NewChatModel(ctx, config)
    return err
}

ToolCallingChatModel 接口同时支持普通对话和工具调用------虽然当前版本只用到了对话能力,但接口选型已经为未来的 Tool Use 留好了扩展口。

6.3 流式对话

流式输出是终端 AI 助手的体验核心------用户不想等 5 秒看到一大段文字,而是想看到文字像打字一样逐字出现。

go 复制代码
// llm/chat.go

func ChatStream(ctx context.Context, history []*schema.Message) (
    *schema.StreamReader[*schema.Message], error,
) {
    messages := make([]*schema.Message, 0, len(history)+1)
    messages = append(messages, schema.SystemMessage(octoPrompt))
    messages = append(messages, history...)

    stream, err := tcm.Stream(ctx, messages)
    return stream, errors.WithStack(err)
}

关键设计:

  1. System Prompt 注入 :每轮对话都在 history 前面拼接 system prompt,确保模型角色一致
  2. 返回 StreamReader:不直接消费流,而是把流的控制权交给上层(TUI),实现解耦
  3. 历史对话传递 :调用方负责维护 history,ChatStream 只管发送

6.4 系统提示词

系统提示词定义了 Octo 的"人设":

go 复制代码
// llm/system_prompt.go

var octoPrompt = `
你是一个名为 Octo 的个人 AI 助手,运行在命令行终端中。

## 核心定位
- 你是用户的私人助手,帮助解决日常工作、学习和生活中的各种问题
- 你擅长编程开发、代码分析、技术问题解答、写作润色、翻译、数据分析等
- 你可以处理多轮对话,记住上下文并提供连贯的帮助

## 沟通风格
- 使用中文与用户交流(除非用户使用其他语言)
- 回答简洁明了,重点突出,避免冗长
- ...
`

注意:当前版本的 system prompt 是硬编码的。在后续版本中,我们会支持从 YAML 或 Markdown 文件加载,并允许用户自定义。


7. 终端界面:让用户"看见"

7.1 为什么用 Bubbletea?

Go 生态中 TUI 框架不少,但 Bubbletea 是最成熟的一个。它基于 Elm 架构(Model-Update-View),把 UI 状态管理变得像写 Redux 一样清晰:

graph LR A[用户输入<br/>KeyPressMsg] --> B[Update<br/>处理消息] B --> C[Model<br/>更新状态] C --> D[View<br/>渲染界面] D --> E[终端输出] E -->|等待下一条消息| A

7.2 核心数据结构

go 复制代码
// tui/tui.go

type OctoModel struct {
    ctx      context.Context
    config   *structs.OctoConfig
    input    textinput.Model          // 输入框组件
    history  []*schema.Message        // 对话历史
    response string                   // 当前流式响应
    stream   *schema.StreamReader[*schema.Message]  // 当前流
}

7.3 消息处理(Update)

Update 方法是整个 TUI 的大脑,处理两类消息:

go 复制代码
func (m *OctoModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+c":
            // 退出前关闭流
            if m.stream != nil {
                m.stream.Close()
            }
            return m, tea.Quit

        case "enter":
            value := m.input.Value()
            if value == "" {
                return m, nil
            }
            m.clear()
            m.history = append(m.history, schema.UserMessage(value))
            return m, m.startStream()  // 触发异步流式请求
        }

    case streamMsg:
        // 流式数据到达
        if msg.err != nil {
            m.response = "错误: " + msg.err.Error()
            return m, nil
        }
        if msg.isDone {
            // 流结束,将完整回复加入历史
            if m.response != "" {
                m.history = append(m.history,
                    schema.AssistantMessage(m.response, nil))
            }
            return m, nil
        }
        // 累加内容并继续读取
        m.response += msg.content
        return m, m.readStream(m.stream)
    }
    // ...
}

流式读取的核心是一个递归的 tea.Cmd 链:

go 复制代码
func (m *OctoModel) startStream() tea.Cmd {
    return func() tea.Msg {
        stream, err := llm.ChatStream(m.ctx, m.history)
        if err != nil {
            return streamMsg{err: err}
        }
        return streamMsg{stream: stream}
    }
}

func (m *OctoModel) readStream(stream *schema.StreamReader[*schema.Message]) tea.Cmd {
    return func() tea.Msg {
        if stream == nil {
            return streamMsg{isDone: true}
        }
        msg, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                return streamMsg{isDone: true}
            }
            return streamMsg{err: err}
        }
        return streamMsg{content: msg.Content}
    }
}
sequenceDiagram participant User as 用户 participant TUI as Bubbletea participant LLM as LLM API User->>TUI: 输入消息 + Enter TUI->>TUI: history 追加 UserMessage TUI->>LLM: ChatStream(history) LLM-->>TUI: StreamReader loop 流式接收 TUI->>LLM: stream.Recv() LLM-->>TUI: chunk(部分内容) TUI->>TUI: response += chunk TUI->>User: View() 实时渲染 end LLM-->>TUI: io.EOF TUI->>TUI: history 追加 AssistantMessage TUI->>User: 最终渲染

7.4 界面渲染(View)

go 复制代码
func (m *OctoModel) View() tea.View {
    var sb strings.Builder

    // ASCII Logo
    sb.WriteString(logo)

    // 模型信息
    sb.WriteString("\nModel ")
    sb.WriteString(m.config.LLMConfig.Model)
    sb.WriteString("\n\n")

    // 历史消息
    for _, msg := range m.history {
        switch msg.Role {
        case schema.User:
            sb.WriteString("You: ")
            sb.WriteString(msg.Content)
            sb.WriteString("\n\n")
        case schema.Assistant:
            sb.WriteString("Assistant: ")
            sb.WriteString(msg.Content)
            sb.WriteString("\n\n")
        }
    }

    // 当前流式响应(正在生成中)
    if m.response != "" && m.stream != nil {
        sb.WriteString("Assistant: ")
        sb.WriteString(m.response)
        sb.WriteString("\n\n")
    }

    // 输入框 + 提示
    sb.WriteString(m.input.View())
    sb.WriteString("\n")
    sb.WriteString("(Ctrl+C 退出)\n")

    return tea.NewView(sb.String())
}

8. 日志系统:让问题"可查"

8.1 设计思路

智能体应用的日志量很大------每次 API 调用、每轮对话、每个错误都需要记录。如果全部写到一个文件,排查问题时就像大海捞针。

Octo 的方案是按级别分文件:

graph TD A[slog 系统调用] --> B{日志级别} B -->|Level 1| C[debug.log] B -->|Level 2| D[info.log] B -->|Level 3| E[warn.log] B -->|Level 4| F[error.log] E -->|同时写入| F

warn 级别的日志会同时写入 warn.log 和 error.log,因为警告往往和错误相关。

8.2 实现

核心是一个自定义的 slog.Handler:

go 复制代码
// setup/init_log.go

type LevelFileHandler struct {
    handlers map[slog.Level]*slog.TextHandler
    files    map[slog.Level]*os.File
    minLevel slog.Level
}

初始化时为每个级别创建独立的文件 handler:

go 复制代码
func NewLevelFileHandler(dir string, opts *slog.HandlerOptions) (*LevelFileHandler, error) {
    os.MkdirAll(dir, 0755)

    levels := map[slog.Level]string{
        slog.LevelDebug: "debug.log",
        slog.LevelInfo:  "info.log",
        slog.LevelWarn:  "warn.log",
        slog.LevelError: "error.log",
    }

    handlers := make(map[slog.Level]*slog.TextHandler)
    files := make(map[slog.Level]*os.File)
    for level, filename := range levels {
        f, _ := os.OpenFile(
            filepath.Join(dir, filename),
            os.O_CREATE|os.O_WRONLY|os.O_APPEND,
            0644,
        )
        handlers[level] = slog.NewTextHandler(f, opts)
        files[level] = f
    }
    // ...
}

日志路由逻辑:

go 复制代码
func (h *LevelFileHandler) Handle(_ context.Context, record slog.Record) error {
    // 写入对应级别文件
    if handler, ok := h.handlers[record.Level]; ok {
        handler.Handle(context.Background(), record)
    }
    // warn 同时写入 error 文件
    if record.Level == slog.LevelWarn {
        if handler, ok := h.handlers[slog.LevelError]; ok {
            handler.Handle(context.Background(), record)
        }
    }
    return nil
}

8.3 多输出支持

日志同时支持控制台和文件双输出,通过 slog.NewMultiHandler 组合:

go 复制代码
func InitLog(cfg *structs.LogConfig) error {
    var handlers []slog.Handler

    if cfg.Console {
        // 控制台输出
        handlers = append(handlers, slog.NewTextHandler(os.Stdout, opts))
    }
    if cfg.LogDir != "" {
        // 分级文件输出
        fileHandler, _ := NewLevelFileHandler(cfg.LogDir, opts)
        handlers = append(handlers, fileHandler)
    }

    slog.SetDefault(slog.New(slog.NewMultiHandler(handlers...)))
    return nil
}

9. 应用编排:把一切串起来

9.1 入口

main.go 极度简洁------只有 12 行:

go 复制代码
func main() {
    ctx := context.Background()
    setup.Init(ctx)
}

9.2 编排层

setup.go 按依赖顺序依次初始化:

go 复制代码
func initInner(ctx context.Context) error {
    // 1. 加载配置(后续模块都依赖它)
    if err := config.LoadConfig(ctx); err != nil {
        return err
    }
    // 2. 初始化日志(后续模块需要日志能力)
    if err := InitLog(&config.OctoConfig.LogConfig); err != nil {
        return err
    }
    // 3. 初始化 LLM(TUI 依赖它做对话)
    if err := llm.InitModel(ctx, config.OctoConfig.LLMConfig); err != nil {
        return err
    }
    // 4. 启动 TUI(阻塞运行,直到用户退出)
    if err := tui.Init(ctx, config.OctoConfig); err != nil {
        return err
    }
    return nil
}

初始化顺序不是随意的------它遵循依赖链:

graph LR C[1. Config<br/>配置] --> L[2. Log<br/>日志] L --> M[3. Model<br/>LLM] M --> T[4. TUI<br/>界面]

任何一步失败,程序直接 panic 退出并打印错误。对于 CLI 工具来说,这是合理的------静默失败比崩溃更危险。


10. 依赖关系全景

graph TB subgraph 内部模块 main[main] setup[setup] config[config] structs[structs] llm[llm] tui[tui] end subgraph 外部依赖 eino[cloudwego/eino<br/>LLM 框架] einoOpenai[eino-ext/openai<br/>OpenAI 适配器] bubbletea[charmbracelet/bubbletea<br/>TUI 框架] bubbles[charmbracelet/bubbles<br/>UI 组件] yaml[gopkg.in/yaml.v3] errors[pkg/errors] end main --> setup setup --> config setup --> llm setup --> tui config --> structs llm --> structs tui --> structs tui --> llm llm --> eino llm --> einoOpenai tui --> bubbletea tui --> bubbles config --> yaml config --> errors llm --> errors

11. 当前版本的局限与思考

作为系列文章的第一章,Octo v0.1.0 刻意保持简单。但简单不等于简陋------每个设计决策都有其理由,也为后续迭代留下了空间:

当前状态 局限 未来方向
System Prompt 硬编码 无法自定义角色 支持从文件加载 + 动态切换
纯文本对话 无 Tool Calling 接入工具调用能力
单文件日志 无日志轮转 引入 lumberjack 做日志切割
无持久化 重启丢失对话 支持 SQLite/文件存储历史
无错误重试 网络抖动直接报错 指数退避重试机制
handler/ 为空 无消息处理管线 支持 middleware 链式处理
无版本管理 不知道运行的哪个版本 编译时注入版本号

12. 运行效果

启动 Octo 后,你会看到:

makefile 复制代码
  ██████╗   ██████╗ ████████╗  ██████╗
 ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔═══██╗
 ██║   ██║ ██║         ██║    ██║   ██║
 ██║   ██║ ██║         ██║    ██║   ██║
 ╚██████╔╝ ╚██████╗    ██║    ╚██████╔╝
  ╚═════╝   ╚═════╝    ╚═╝     ╚═════╝

Model deepseek-v4-flash

You: 你好,请介绍一下自己

Assistant: 你好!我是 Octo,一个运行在终端里的 AI 助手...

(Ctrl+C 退出)

13. 本章小结

通过 Octo v0.1.0,我们搭建了一个工程化的 LLM 终端助手骨架:

mindmap root((Octo v0.1.0)) 配置管理 YAML 解析 结构体校验 运行时热切换 LLM 集成 Eino 框架 OpenAI 兼容 流式输出 终端界面 Bubbletea Elm 架构 多轮对话 日志系统 slog 原生 分级分文件 多输出聚合 应用编排 依赖链初始化 单一入口 快速失败

核心收获:

  1. 配置与代码分离------换模型不改代码
  2. 流式输出------用户体验的关键
  3. 模块边界清晰------每个包做一件事
  4. 日志分级------出问题能查到
  5. 接口预留 ------ToolCallingChatModel 为 Tool Use 留好扩展口

项目源码:github.com/aethelgards...

相关推荐
虫无涯1 小时前
大模型联动 CodeQL + Coverity 完整落地方案
人工智能·python·大模型·llm·codeql·coverity
周杰伦fans1 小时前
C#对话摘要降低Token消耗
人工智能·后端·c#
做系统的大强1 小时前
脚本说 PASS、OS 读全零 —— 到底是谁在撒谎?
后端·操作系统
Yuzhiyuxia2 小时前
【RAG面试系列】工程落地
llm·rag
code2cat2 小时前
【随笔】MCP资源更新订阅:通知到达以后,Agent怎样刷新旧资料
java·后端·开发工具·ai agent·mcp
桃西西呀3 小时前
LangChain 之三:模型与消息抽象
人工智能·langchain·llm
桃西西呀3 小时前
LangChain 之二:组合原语深拆
人工智能·llm·ai编程
Harvil_3 小时前
任务跑一半,API 突然报“对话超长“断流:我给 Agent 修的紧急逃生通道
后端
用户EasyAdminBlazor3 小时前
EasyAdminBlazor SignalR 实时消息源码解析:从 NotificationHub 到站内消息
后端
程序猿乐锅4 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven