边缘 AI 集成 · 异步流通道 · 真正打字机

基于 llama.cpp 与 Drogon SSE 的流式本地 AI 助手

本实践文档详细记录了在本地 WSL 环境下编译大模型推理引擎、打通 MySQL 历史记录会话表,并利用服务端事件发送(SSE)实现前后端全链路无缝流式输出的硬核操作 [cite: 2, 5, 24, 55]。

1. 编译本地 llama.cpp 算力引擎

为了在不需要昂贵显卡的开发机环境下跑通 LLM 对话,我们利用现代 CPU 编译加速套件进行源码级物理重构 [cite: 9, 20]。首先安装基础系统支撑依赖 [cite: 7, 8]:

sudo apt update && sudo apt install -y git cmake build-essential curl wget python3 python3-pip unzip ca-certificates

获取并编译离线推理底座 [cite: 12, 15]:

cd ~/workspace
mkdir echo-ai && cd echo-ai
curl -L --http1.1 -o llama.cpp.zip https://codeload.github.com/ggml-org/llama.cpp/zip/refs/heads/master
unzip llama.cpp.zip && mv llama.cpp-master llama.cpp
cd llama.cpp
cmake -B build
cmake --build build -j2 --target llama-server llama-cli

2. 模型下发与本地推理服务唤醒

考虑到普通宿主机 WSL 的承载极限,我们选用量化小型边缘模型(千问 Qwen2.5-1.5B 4位量化版)进行本地常驻 [cite: 19, 20]:

mkdir -p ./models/qwen2.5-1.5b
wget -c -O ./models/qwen2.5-1.5b/qwen2.5-1.5b-instruct-q4_k_m.gguf https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf

启动独立 AI 推理微服务进程,监听本地 18080 端口 [cite: 22, 23]:

./build/bin/llama-server \
  -m ./models/qwen2.5-1.5b/qwen2.5-1.5b-instruct-q4_k_m.gguf \
  --host 127.0.0.1 --port 18080 -c 2048

3. 多轮对话持久化表结构设计

为了让 AI 助手具备历史记忆和多会话管理能力,必须打通 MySQL 存储阵列 [cite: 24, 27]。创建会话主表与消息明细表 [cite: 25, 26]:

CREATE TABLE IF NOT EXISTS ai_conversations (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    title VARCHAR(100) NOT NULL DEFAULT 'AI Chat',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS ai_messages (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    conversation_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    role VARCHAR(20) NOT NULL, -- user / assistant
    content TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

4. Drogon 后端异步流转发拦截

如果使用传统的同步等待,模型生成长文本时会导致前端长达数秒的一片死寂,体验极差 [cite: 54]。我们引入现代 **Drogon 异步流式回执(SSE)** 机制进行全量破局 [cite: 55, 56]:

// 初始化连接 llama.cpp 的内网 HTTP 客户端
g_aiClient = drogon::HttpClient::newHttpClient("http://127.0.0.1:18080");

// 注册真正的异步 SSE 传输通道
app().registerHandler("/api/ai/chat/stream", [](const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback){
    // 1. 拦截鉴权并提取上下文历史记录 [cite: 33, 63]
    // 2. 构造同步流式分块响应
    auto streamResp = HttpResponse::newAsyncStreamResponse([](ResponseStreamPtr stream){
        // 内部通过 g_aiClient 发送 chunk 阻断请求
        // 逐行转发 llama.cpp stream 的文本增量语料
        stream->send("data: {\"content\":\"文本片段\"}\n\n");
        // 当大模型吐出 [DONE] 标记时,干净断开
        stream->close();
    });
    callback(streamResp);
});

5. 前端原生 ReadableStream 承接(真正的打字机)

在前端,摒弃落后的整体渲染方案,利用高级 Fetch API 捕获底层的流式读取器 [cite: 59, 61]:

const response = await fetch('/api/ai/chat/stream', { method: 'POST', body: JSON.stringify({ ... }) });
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");

while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    
    const chunkText = decoder.decode(value);
    // 解析 SSE 协议标志 "data: {...}" 并实时追加
    state.aiMessages[lastIndex].content += parseSseChunk(chunkText);
    renderAiChat(); // 瞬间驱动局部 DOM 刷新,带来行云流水般的顺畅体验! [cite: 52]
}
验收红线:启动应用后,左下角显示 WSL 互通,进入 AI 交互面板发送消息,可以看到本地边缘大模型不仅能在后台完美根据历史多轮语料完成连续对话,前端聊天面板更是能逐字展示流畅、毫无阻断的真·打字机特效 [cite: 52, 63, 74]。
← 返回我的主页