第一章:从零构建终端 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.modellog.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. 历史对话传递 :调用方负责维护 historyChatStream 只管发送

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.logerror.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...

相关推荐
Token炼金师1 小时前
梯度的呼吸:激活、归一化、反传与残流 —— 深度网络的四道命门
人工智能·深度学习·llm
贰先生1 小时前
Xiuno BBS 审计之问题02:后台用户搜索可能造成严重性能 DoS
后端
Gopher_HBo1 小时前
docker-run-调用链全图解
后端
贰先生1 小时前
Xiuno BBS 审计之问题09:登录回跳 referer 未严格校验,存在开放重定向风险
后端
JarvanStack1 小时前
【无标题】
后端
贰先生1 小时前
Xiuno BBS 审计之问题10:用户密码不符合现代密码存储规范
后端
冬奇Lab1 小时前
MCP Server 入门开发 + 协议调试 + 生产级部署
人工智能·llm·mcp
IT_陈寒2 小时前
SpringBoot自动配置的坑,我的Bean怎么不生效?
前端·人工智能·后端
CoderYanger2 小时前
视频切割脚本(Python版)
linux·开发语言·windows·后端·python·程序人生·职场和发展