前言

在当前生成式人工智能蓬勃发展的技术浪潮中,构建高效、安全且响应迅速的AI应用后端已成为开发者的核心诉求。Rust语言凭借其内存安全、零成本抽象以及惊人的执行效率,正逐渐成为构建高性能Web服务的首选语言之一。结合蓝耘(Lanyun)提供的DeepSeek大模型MaaS(Model as a Service)服务,开发者能够以极低的延迟和极高的吞吐量处理复杂的自然语言处理任务。

本文将深入剖析如何从零开始,利用Rust语言的Axum框架,配合Tokio异步运行时,构建一个能够流式对接DeepSeek V3大模型的全栈Web应用。我们将详细拆解每一个开发步骤,从底层环境构建到核心业务逻辑实现,再到前端流式渲染,进行全方位的技术解读。

第一阶段:底层开发环境的构建与验证

构建Rust应用的第一步是确保操作系统具备完整的编译工具链。Rust编译器在处理某些依赖包(Crate)时,尤其是涉及到系统底层交互或加密库(如OpenSSL、Ring等)时,往往需要调用系统的C语言链接器。

1.1 系统基础构建工具的安装

在基于Debian或Ubuntu的Linux环境中,build-essential软件包是不可或缺的。它包含了GCC编译器、Make工具以及Glibc开发库头文件。curl则用于后续下载安装脚本。

通过终端执行更新软件源并安装构建工具的指令,能够确保后续的Rust安装过程不会因缺少链接库而中断。

sudo apt update 
sudo apt install curl build-essential

执行上述命令后,系统包管理器将解析依赖树并完成下载安装。下图展示了终端在执行安装命令后的状态,可以看到系统成功读取了软件包列表,并准备进行环境的配置。这是Rust编译环境能够顺利运行的基石。

image.png

1.2 Rust工具链的部署

Rust官方提供了rustup作为版本管理和安装工具,这是管理Rust生态最推荐的方式。它不仅安装编译器rustc,还会安装包管理器cargo以及文档工具rustdoc

通过执行官方的Shell脚本,可以启动自动化的安装流程:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

该脚本会自动检测当前操作系统架构,下载对应的预编译二进制文件。它将安装最新的Stable(稳定)版本,这是生产环境开发的首选。

安装脚本执行完毕后,终端会输出安装成功的摘要信息。如下图所示,系统提示Rust已成功安装,并且相关二进制文件已被放置在用户的.cargo/bin目录下。该目录是Rust生态工具的核心存放位置,确保其被加入系统环境变量PATH中至关重要。

image.png

1.3 环境变量的加载与验证

虽然安装脚本通常会自动修改Shell的配置文件(如.bashrc.zshrc),但在当前终端会话中,变更不会立即生效。为了立即使用cargo命令,需要手动加载环境配置:

. "$HOME/.cargo/env"

随后,通过检查编译器和包管理器的版本,可以验证环境是否配置正确:

rustc --version
cargo --version

下图展示了版本验证的输出结果。可以看到当前安装的是Rust 1.84.0版本。能够正确打印出版本号,标志着Rust开发环境已经准备就绪,可以开始后续的项目构建。

image.png

为了确保每次登录终端时Rust环境都能自动加载,将加载命令追加到.bashrc文件中是一个稳健的做法。

echo '. "$HOME/.cargo/env"' >> ~/.bashrc

这一操作确保了环境变量的持久化,如下图所示,通过追加重定向操作符,配置被写入了用户的Shell启动脚本中。

image.png

第二阶段:蓝耘MaaS服务凭证配置

在开始编写代码之前,获取大模型服务的访问权限是必要环节。蓝耘平台提供了标准化的API接口,兼容OpenAI规范,极大降低了接入难度。

首先需要在蓝耘广场控制台生成API Key。这个密钥是应用与大模型服务进行安全通信的凭证,必须妥善保管。如下图所示,控制台界面提供了直观的密钥管理功能,新生成的API Key将用于后续的环境变量配置。

https://console.lanyun.net/#/register?promoterCode=0131

image.png

接下来是选择合适的模型。本项目选用/maas/deepseek-ai/DeepSeek-V3.2。DeepSeek V3模型在自然语言理解和生成代码方面表现优异,且通过蓝耘的MaaS架构,能够提供极低的推理延迟。下图展示了模型选择界面,明确了调用的模型标识符。

image.png

第三阶段:Rust项目架构与依赖管理

Rust的项目结构以清晰著称。本项目将采用标准的Cargo项目结构,并进行细分模块化设计,以实现高内聚低耦合的代码组织。

3.1 目录结构初始化

在服务器端,首先创建项目根目录及必要的子目录。合理的目录结构有助于代码的维护和扩展。

cd ~
mkdir -p rust-deepseek-web
cd rust-deepseek-web
mkdir -p src/api src/services src/models static/css static/js

src/api用于存放HTTP路由处理逻辑,src/services用于封装业务逻辑(如调用第三方API),src/models用于定义数据结构,static则用于存放前端静态资源。下图展示了创建完成后的目录层级,这种结构为后续的代码填充奠定了良好的基础。

image.png

3.2 核心依赖配置 (Cargo.toml)

Cargo.toml是Rust项目的核心配置文件。本项目引入了一系列高性能的异步生态库:

  • axum (0.7): 当前Rust生态中最流行的Web框架之一,基于Tokio构建,提供符合人体工程学的API设计。
  • tokio (full): 强大的异步运行时,负责任务调度、I/O操作。
  • reqwest: 全功能的HTTP客户端,支持异步调用和流式响应处理,用于与DeepSeek API通信。
  • serde & serde_json: 序列化与反序列化框架,处理JSON数据的核心工具。
  • tracing: 结构化日志记录系统,用于生产环境的监控与调试。
  • dotenv: 用于加载.env文件中的环境变量,实现配置与代码分离。
  • futures & async-stream: 处理异步流(Stream)的关键库,是实现打字机效果(流式输出)的基础。

通过编写Cargo.toml文件,声明了项目所需的所有外部依赖及其特性标志。


cat > Cargo.toml << 'EOF'

[package]

name = "rust-deepseek-web"

version = "0.1.0"

edition = "2021"

  

[dependencies]

axum = "0.7"

tokio = { version = "1", features = ["full"] }

tower = "0.4"

tower-http = { version = "0.5", features = ["fs", "cors", "trace"] }

reqwest = { version = "0.11", features = ["json", "stream"] }

serde = { version = "1.0", features = ["derive"] }

serde_json = "1.0"

dotenv = "0.15"

tracing = "0.1"

tracing-subscriber = { version = "0.3", features = ["env-filter"] }

anyhow = "1.0"

thiserror = "1.0"

futures = "0.3"

tokio-stream = "0.1"

async-stream = "0.3"

  

[profile.release]

opt-level = 3

lto = true

codegen-units = 1

strip = true

EOF

3.3 环境变量配置 (.env)

为了避免将敏感信息(如API Key)硬编码在源码中,使用.env文件进行管理是行业最佳实践。该文件包含了API密钥、API地址、模型名称以及服务监听的端口配置。

DEEPSEEK_API_KEY=sk-your-api-key-here
DEEPSEEK_API_URL=https://maas-api.lanyun.net/v1/
DEEPSEEK_MODEL=/maas/deepseek-ai/DeepSeek-V3.2
SERVER_HOST=0.0.0.0
SERVER_PORT=3000
RUST_LOG=info

下图展示了配置文件的内容预览。正确配置该文件是服务能够成功鉴权并路由到正确模型的关键。

image.png

第四阶段:后端核心业务逻辑实现

后端开发是本项目的重头戏,涉及到配置加载、数据模型定义、外部服务通信以及API接口的实现。

4.1 配置管理模块 (src/config.rs)

src/config.rs模块负责将环境变量加载到强类型的Rust结构体中。这种做法利用了Rust的类型系统,在应用启动阶段就能捕获配置错误(如缺少Key或端口格式错误),防止应用在运行时因配置问题崩溃。

代码中定义了Config结构体,并实现了from_env构造方法。该方法使用dotenv加载环境文件,并通过std::env::var读取变量。对于可选配置(如HOST),提供了默认值回退机制;对于必须配置(如API Key),则会返回错误,确保安全性。


cat > src/config.rs << 'EOF'

use std::env;

  

#[derive(Debug, Clone)]

pub struct Config {

    pub deepseek_api_key: String,

    pub deepseek_api_url: String,

    pub deepseek_model: String,

    pub server_host: String,

    pub server_port: u16,

}

  

impl Config {

    pub fn from_env() -> Result<Self, String> {

        dotenv::dotenv().ok();

  

        let deepseek_api_key = env::var("DEEPSEEK_API_KEY")

            .map_err(|_| "DEEPSEEK_API_KEY 未设置".to_string())?;

  

        let deepseek_api_url = env::var("DEEPSEEK_API_URL")

            .unwrap_or_else(|_| "https://api.deepseek.com/v1".to_string());

  

        let deepseek_model = env::var("DEEPSEEK_MODEL")

            .unwrap_or_else(|_| "deepseek-chat".to_string());

  

        let server_host = env::var("SERVER_HOST")

            .unwrap_or_else(|_| "127.0.0.1".to_string());

  

        let server_port = env::var("SERVER_PORT")

            .unwrap_or_else(|_| "3000".to_string())

            .parse::<u16>()

            .map_err(|_| "SERVER_PORT 必须是有效的端口号".to_string())?;

  

        Ok(Config {

            deepseek_api_key,

            deepseek_api_url,

            deepseek_model,

            server_host,

            server_port,

        })

    }

  

    pub fn server_address(&self) -> String {

        format!("{}:{}", self.server_host, self.server_port)

    }

}

EOF

4.2 数据模型定义 (src/models)

src/models目录下,chat.rs定义了与DeepSeek API交互所需的所有JSON数据结构。

  • ChatRequest: 前端发送的请求体,包含用户消息、模式(聊天/代码/文档)以及历史上下文。
  • DeepSeekRequest: 发送给大模型API的请求体,严格遵循OpenAI API规范。其中stream: bool字段决定了是否开启流式传输。
  • DeepSeekResponse & StreamChunk: 分别对应非流式和流式响应的数据结构。特别需要注意的是StreamChunk,它用于解析SSE(Server-Sent Events)推送的增量数据片段。

使用#[derive(Serialize, Deserialize)]宏,配合serde库,能够自动处理JSON字符串与Rust结构体之间的转换,极大地简化了数据处理流程。


cat > src/models/mod.rs << 'EOF'

pub mod chat;

EOF

4.3 DeepSeek服务层封装 (src/services/deepseek.rs)

这是连接本地服务与远端大模型的桥梁。DeepSeekService结构体持有HTTP客户端和配置信息。

该服务实现了两个核心方法:

  1. chat: 处理常规的一次性响应请求。
  2. chat_stream: 处理流式响应请求。

chat_stream方法中,利用了reqwestbytes_stream()功能获取原始字节流。代码通过async-stream宏构建了一个生成器,逐块读取网络数据。由于SSE协议的数据格式是data: {json}\n\n,解析逻辑需要处理缓冲区,按行分割数据,剔除data: 前缀,并识别[DONE]结束标记。这种细粒度的流处理确保了前端能够实时接收到AI生成的每一个字符,而不是等待所有内容生成完毕。

// src/services/deepseek.rs

// DeepSeek API 客户端服务

  

use crate::config::Config;

use crate::models::chat::{DeepSeekRequest, DeepSeekResponse, Message, StreamChunk};

use anyhow::{Context, Result};

use futures::stream::Stream;

use reqwest::Client;

use std::pin::Pin;

  

/// DeepSeek 服务客户端

#[derive(Clone)]

pub struct DeepSeekService {

    client: Client,

    config: Config,

}

  

impl DeepSeekService {

    /// 创建新的 DeepSeek 服务实例

    pub fn new(config: Config) -> Self {

        let client = Client::new();

        Self { client, config }

    }

  

    /// 发送聊天请求(非流式)

    ///

    /// # 参数

    /// - `messages`: 消息历史列表

    ///

    /// # 返回

    /// - `Result<String>`: AI 回复内容

    pub async fn chat(&self, messages: Vec<Message>) -> Result<String> {

        let request = DeepSeekRequest {

            model: self.config.deepseek_model.clone(),

            messages,

            stream: false,

            temperature: Some(0.7),

            max_tokens: Some(2000),

        };

  

        let url = format!("{}/chat/completions", self.config.deepseek_api_url);

  

        let response = self

            .client

            .post(&url)

            .header("Authorization", format!("Bearer {}", self.config.deepseek_api_key))

            .header("Content-Type", "application/json")

            .json(&request)

            .send()

            .await

            .context("发送请求到 DeepSeek API 失败")?;

  

        if !response.status().is_success() {

            let status = response.status();

            let error_text = response.text().await.unwrap_or_default();

            anyhow::bail!("DeepSeek API 返回错误 {}: {}", status, error_text);

        }

  

        let deepseek_response: DeepSeekResponse = response

            .json()

            .await

            .context("解析 DeepSeek API 响应失败")?;

  

        let content = deepseek_response

            .choices

            .first()

            .and_then(|choice| Some(choice.message.content.clone()))

            .unwrap_or_else(|| "无响应内容".to_string());

  

        Ok(content)

    }

  

    /// 发送聊天请求(流式)

    ///

    /// # 参数

    /// - `messages`: 消息历史列表

    ///

    /// # 返回

    /// - `Result<impl Stream>`: 流式响应

    pub async fn chat_stream(

        &self,

        messages: Vec<Message>,

    ) -> Result<Pin<Box<dyn Stream<Item = Result<String>> + Send>>> {

        let request = DeepSeekRequest {

            model: self.config.deepseek_model.clone(),

            messages,

            stream: true,

            temperature: Some(0.7),

            max_tokens: Some(2000),

        };

  

        let url = format!("{}/chat/completions", self.config.deepseek_api_url);

  

        let response = self

            .client

            .post(&url)

            .header("Authorization", format!("Bearer {}", self.config.deepseek_api_key))

            .header("Content-Type", "application/json")

            .json(&request)

            .send()

            .await

            .context("发送流式请求到 DeepSeek API 失败")?;

  

        if !response.status().is_success() {

            let status = response.status();

            let error_text = response.text().await.unwrap_or_default();

            anyhow::bail!("DeepSeek API 返回错误 {}: {}", status, error_text);

        }

  

        // 创建流式响应处理器

        let stream = async_stream::stream! {

            let mut bytes_stream = response.bytes_stream();

            let mut buffer = String::new();

  

            use futures::StreamExt;

  

            while let Some(chunk_result) = bytes_stream.next().await {

                match chunk_result {

                    Ok(chunk) => {

                        let chunk_str = String::from_utf8_lossy(&chunk);

                        buffer.push_str(&chunk_str);

  

                        // 处理 SSE 格式的数据

                        while let Some(line_end) = buffer.find('\n') {

                            let line = buffer[..line_end].trim().to_string();

                            buffer = buffer[line_end + 1..].to_string();

  

                            if line.starts_with("data: ") {

                                let data = &line[6..];

  

                                // 检查是否是结束标记

                                if data == "[DONE]" {

                                    break;

                                }

  

                                // 解析 JSON

                                if let Ok(stream_chunk) = serde_json::from_str::<StreamChunk>(data) {

                                    if let Some(choice) = stream_chunk.choices.first() {

                                        if let Some(content) = &choice.delta.content {

                                            yield Ok(content.clone());

                                        }

                                    }

                                }

                            }

                        }

                    }

                    Err(e) => {

                        yield Err(anyhow::anyhow!("读取流数据失败: {}", e));

                        break;

                    }

                }

            }

        };

  

        Ok(Box::pin(stream))

    }

}

4.4 API路由处理 (src/api)

src/api模块负责将HTTP请求映射到具体的业务逻辑。

  • chat.rs: 包含了核心的chat_stream_handler。该处理器接收前端的JSON请求,调用DeepSeekService获取流对象,然后将其转换为Axum支持的Sse(Server-Sent Events)响应格式。这里使用了keep_alive机制,防止长连接中断。同时,根据请求模式(Coding/Document),会在消息历史前注入不同的System Prompt(系统提示词),以调整AI的回答风格。
// src/api/chat.rs

// 聊天 API 路由处理器

  

use crate::models::chat::{ChatRequest, ChatResponse, Message};

use crate::services::deepseek::DeepSeekService;

use axum::{

    extract::State,

    http::StatusCode,

    response::{sse::Event, IntoResponse, Response, Sse},

    Json,

};

use futures::stream::Stream;

use std::convert::Infallible;

use std::sync::Arc;

  

/// 应用状态

#[derive(Clone)]

pub struct AppState {

    pub deepseek_service: DeepSeekService,

}

  

/// 聊天接口(非流式)

///

/// POST /api/chat

/// 接收用户消息,返回 AI 回复

pub async fn chat_handler(

    State(state): State<Arc<AppState>>,

    Json(request): Json<ChatRequest>,

) -> Result<Json<ChatResponse>, (StatusCode, String)> {

    tracing::info!("收到聊天请求: {}", request.message);

  

    // 构建消息列表

    let mut messages = request.history.clone();

  

    // 根据模式添加系统提示词

    let system_prompt = get_system_prompt(&request.mode);

    if !system_prompt.is_empty() {

        messages.insert(

            0,

            Message {

                role: "system".to_string(),

                content: system_prompt,

            },

        );

    }

  

    // 添加用户消息

    messages.push(Message {

        role: "user".to_string(),

        content: request.message.clone(),

    });

  

    // 调用 DeepSeek API

    match state.deepseek_service.chat(messages).await {

        Ok(response) => {

            tracing::info!("AI 回复成功");

            Ok(Json(ChatResponse {

                message: response,

                status: "success".to_string(),

            }))

        }

        Err(e) => {

            tracing::error!("AI 回复失败: {}", e);

            Err((

                StatusCode::INTERNAL_SERVER_ERROR,

                format!("AI 回复失败: {}", e),

            ))

        }

    }

}

  

/// 聊天接口(流式)

///

/// POST /api/chat/stream

/// 接收用户消息,返回流式 AI 回复

pub async fn chat_stream_handler(

    State(state): State<Arc<AppState>>,

    Json(request): Json<ChatRequest>,

) -> Response {

    tracing::info!("收到流式聊天请求: {}", request.message);

  

    // 构建消息列表

    let mut messages = request.history.clone();

  

    // 根据模式添加系统提示词

    let system_prompt = get_system_prompt(&request.mode);

    if !system_prompt.is_empty() {

        messages.insert(

            0,

            Message {

                role: "system".to_string(),

                content: system_prompt,

            },

        );

    }

  

    // 添加用户消息

    messages.push(Message {

        role: "user".to_string(),

        content: request.message.clone(),

    });

  

    // 调用 DeepSeek API 流式接口

    match state.deepseek_service.chat_stream(messages).await {

        Ok(stream) => {

            let sse_stream = create_sse_stream(stream);

            Sse::new(sse_stream).into_response()

        }

        Err(e) => {

            tracing::error!("流式 AI 回复失败: {}", e);

            (

                StatusCode::INTERNAL_SERVER_ERROR,

                format!("流式 AI 回复失败: {}", e),

            )

                .into_response()

        }

    }

}

  

/// 创建 SSE 流

fn create_sse_stream(

    stream: impl Stream<Item = anyhow::Result<String>> + Send + 'static,

) -> impl Stream<Item = Result<Event, Infallible>> {

    use futures::StreamExt;

  

    stream.map(|result| match result {

        Ok(content) => Ok(Event::default().data(content)),

        Err(e) => {

            tracing::error!("流式数据错误: {}", e);

            Ok(Event::default().data(format!("[ERROR]: {}", e)))

        }

    })

}

  

/// 根据模式获取系统提示词

fn get_system_prompt(mode: &str) -> String {

    match mode {

        "code" => {

            "你是一个专业的编程助手。请提供清晰、准确的代码示例和解释。\

            使用 Markdown 格式输出代码块。"

                .to_string()

        }

        "document" => {

            "你是一个文档分析专家。请仔细分析文档内容,\

            提供准确的摘要和见解。"

                .to_string()

        }

        _ => String::new(), // 默认聊天模式不需要特殊提示词

    }

}
  • health.rs: 提供了一个简单的健康检查端点,用于监控服务存活状态。
// src/api/health.rs

// 健康检查 API

  

use axum::{http::StatusCode, Json};

use serde::Serialize;

  

#[derive(Serialize)]

pub struct HealthResponse {

    pub status: String,

    pub message: String,

}

  

/// 健康检查接口

///

/// GET /api/health

pub async fn health_handler() -> (StatusCode, Json<HealthResponse>) {

    (

        StatusCode::OK,

        Json(HealthResponse {

            status: "ok".to_string(),

            message: "服务运行正常".to_string(),

        }),

    )

}

4.5 主程序入口 (src/main.rs)

main.rs是将所有模块串联起来的中枢。

  1. 日志初始化: 配置tracing_subscriber,将日志输出到控制台。
  2. 状态共享: 将DeepSeekService实例封装在Arc(原子引用计数)中,传递给Axum的路由状态。这使得多线程异步任务可以安全地共享同一个服务实例。
  3. CORS配置: 设置跨域资源共享策略,允许前端跨域调用(虽然本项目是同源部署,但保留CORS配置增加了灵活性)。
  4. 路由注册: 将/api/chat等路径映射到对应的Handler,并使用NestService挂载static目录,提供静态文件服务。
  5. 服务启动: 绑定TCP端口并启动Axum服务。
// src/main.rs

// 主程序入口

  

mod api;

mod config;

mod models;

mod services;

  

use crate::api::chat::{chat_handler, chat_stream_handler, AppState};

use crate::api::health::health_handler;

use crate::config::Config;

use crate::services::deepseek::DeepSeekService;

use axum::{

    routing::{get, post},

    Router,

};

use std::sync::Arc;

use tower_http::cors::{Any, CorsLayer};

use tower_http::services::ServeDir;

use tower_http::trace::TraceLayer;

  

#[tokio::main]

async fn main() {

    // 初始化日志

    tracing_subscriber::fmt()

        .with_target(false)

        .compact()

        .init();

  

    // 加载配置

    let config = match Config::from_env() {

        Ok(cfg) => cfg,

        Err(e) => {

            eprintln!("❌ 配置加载失败: {}", e);

            eprintln!("请确保 .env 文件存在并包含必要的配置项");

            std::process::exit(1);

        }

    };

  

    tracing::info!("✅ 配置加载成功");

  

    // 创建 DeepSeek 服务

    let deepseek_service = DeepSeekService::new(config.clone());

  

    // 创建应用状态

    let app_state = Arc::new(AppState { deepseek_service });

  

    // 配置 CORS

    let cors = CorsLayer::new()

        .allow_origin(Any)

        .allow_methods(Any)

        .allow_headers(Any);

  

    // 构建路由

    let app = Router::new()

        // API 路由

        .route("/api/health", get(health_handler))

        .route("/api/chat", post(chat_handler))

        .route("/api/chat/stream", post(chat_stream_handler))

        // 静态文件服务

        .nest_service("/", ServeDir::new("static"))

        // 添加状态

        .with_state(app_state)

        // 添加中间件

        .layer(cors)

        .layer(TraceLayer::new_for_http());

  

    // 启动服务器

    let addr = config.server_address();

    let listener = tokio::net::TcpListener::bind(&addr)

        .await

        .expect("无法绑定地址");

  

    tracing::info!("🚀 服务器启动成功!");

    tracing::info!("📡 监听地址: http://{}", addr);

    tracing::info!("💡 访问 http://{} 开始使用", addr);

  

    axum::serve(listener, app)

        .await

        .expect("服务器运行失败");

}

第五阶段:前端交互界面开发

前端部分旨在提供一个简洁、现代且响应迅速的聊天界面,核心在于处理流式数据和Markdown渲染。

5.1 页面结构 (index.html)

HTML使用了Tailwind CSS CDN版本进行快速样式开发。页面布局采用Flexbox,分为头部、消息展示区和底部输入区。

  • 消息容器: messagesContainer用于动态追加对话内容。
  • 模式选择: 下拉菜单允许用户选择不同的对话模式,这将影响后端使用的System Prompt。
  • 外部库引入: 引入了marked.js用于Markdown解析,highlight.js用于代码块语法高亮。

5.2 样式定制 (style.css)

除了Tailwind的实用类,style.css定义了深色模式下的细节样式,如自定义滚动条、消息淡入动画以及Markdown渲染后的排版样式(如代码块背景、列表缩进等)。这保证了AI生成的复杂内容(如代码、表格)在界面上显示得体。

/* static/css/style.css */

/* 自定义样式 */

  

/* 全局样式 */

* {

    margin: 0;

    padding: 0;

    box-sizing: border-box;

}

  

body {

    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',

        sans-serif;

    -webkit-font-smoothing: antialiased;

    -moz-osx-font-smoothing: grayscale;

}

  

/* 滚动条样式 */

::-webkit-scrollbar {

    width: 8px;

}

  

::-webkit-scrollbar-track {

    background: #1f2937;

}

  

::-webkit-scrollbar-thumb {

    background: #4b5563;

    border-radius: 4px;

}

  

::-webkit-scrollbar-thumb:hover {

    background: #6b7280;

}

  

/* 消息容器 */

#messagesContainer {

    scroll-behavior: smooth;

}

  

/* 消息样式 */

.message {

    animation: fadeIn 0.3s ease-in;

}

  

@keyframes fadeIn {

    from {

        opacity: 0;

        transform: translateY(10px);

    }

    to {

        opacity: 1;

        transform: translateY(0);

    }

}

  

/* Markdown 内容样式 */

.message-content {

    line-height: 1.6;

}

  

.message-content h1,

.message-content h2,

.message-content h3 {

    margin-top: 1em;

    margin-bottom: 0.5em;

    font-weight: 600;

}

  

.message-content h1 {

    font-size: 1.5em;

}

  

.message-content h2 {

    font-size: 1.3em;

}

  

.message-content h3 {

    font-size: 1.1em;

}

  

.message-content p {

    margin-bottom: 0.8em;

}

  

.message-content ul,

.message-content ol {

    margin-left: 1.5em;

    margin-bottom: 0.8em;

}

  

.message-content li {

    margin-bottom: 0.3em;

}

  

.message-content code {

    background-color: #374151;

    padding: 0.2em 0.4em;

    border-radius: 3px;

    font-size: 0.9em;

    font-family: 'Courier New', Courier, monospace;

}

  

.message-content pre {

    background-color: #1f2937;

    padding: 1em;

    border-radius: 6px;

    overflow-x: auto;

    margin-bottom: 1em;

}

  

.message-content pre code {

    background-color: transparent;

    padding: 0;

    font-size: 0.9em;

}

  

.message-content blockquote {

    border-left: 4px solid #3b82f6;

    padding-left: 1em;

    margin-left: 0;

    margin-bottom: 1em;

    color: #9ca3af;

}

  

.message-content a {

    color: #3b82f6;

    text-decoration: underline;

}

  

.message-content a:hover {

    color: #60a5fa;

}

  

.message-content table {

    width: 100%;

    border-collapse: collapse;

    margin-bottom: 1em;

}

  

.message-content th,

.message-content td {

    border: 1px solid #4b5563;

    padding: 0.5em;

    text-align: left;

}

  

.message-content th {

    background-color: #374151;

    font-weight: 600;

}

  

/* 输入框样式 */

textarea {

    font-family: inherit;

}

  

textarea:focus {

    outline: none;

}

  

/* 按钮动画 */

button {

    transition: all 0.2s ease;

}

  

button:active {

    transform: scale(0.98);

}

  

/* 加载动画 */

.loading {

    display: inline-block;

    width: 12px;

    height: 12px;

    border: 2px solid #3b82f6;

    border-radius: 50%;

    border-top-color: transparent;

    animation: spin 0.8s linear infinite;

}

  

@keyframes spin {

    to {

        transform: rotate(360deg);

    }

}

  

/* 响应式设计 */

@media (max-width: 768px) {

    .container {

        padding: 1rem;

    }

  

    header h1 {

        font-size: 1.5rem;

    }

  

    .flex-col {

        gap: 0.5rem;

    }

}

5.3 前端逻辑实现 (app.js)

app.js承载了前端的核心交互逻辑。

  • 流式请求处理: sendStreamRequest函数使用Fetch API发起POST请求。关键在于获取response.body.getReader()。通过while循环不断读取reader.read()返回的二进制分片(Chunk)。
  • 增量解码与渲染: 使用TextDecoder将二进制数据转为文本。由于网络分片可能截断JSON字符,逻辑中包含了简单的缓冲处理。解析出的Markdown文本被实时追加到变量中,并反复调用marked.parse()刷新DOM。虽然全量重绘在长文本下可能有性能损耗,但在当前规模下能保证渲染的实时性和正确性(避免Markdown标记未闭合导致的渲染错误)。
  • 代码高亮: 每次渲染后,调用hljs.highlightElement对新生成的代码块进行语法着色。
  • 交互细节: 支持Ctrl+Enter发送、自动调整输入框高度、滚动到底部等人性化功能。
// static/js/app.js

// 前端应用逻辑

  

// 全局变量

let conversationHistory = [];

let isProcessing = false;

  

// DOM 元素

const messagesContainer = document.getElementById('messagesContainer');

const messageInput = document.getElementById('messageInput');

const sendBtn = document.getElementById('sendBtn');

const clearBtn = document.getElementById('clearBtn');

const modeSelect = document.getElementById('modeSelect');

const statusText = document.getElementById('statusText');

  

// 初始化

document.addEventListener('DOMContentLoaded', () => {

    // 配置 marked.js

    marked.setOptions({

        highlight: function(code, lang) {

            if (lang && hljs.getLanguage(lang)) {

                return hljs.highlight(code, { language: lang }).value;

            }

            return hljs.highlightAuto(code).value;

        },

        breaks: true,

        gfm: true

    });

  

    // 事件监听

    sendBtn.addEventListener('click', sendMessage);

    clearBtn.addEventListener('click', clearConversation);

  

    // 支持 Ctrl+Enter 发送

    messageInput.addEventListener('keydown', (e) => {

        if (e.ctrlKey && e.key === 'Enter') {

            sendMessage();

        }

    });

  

    // 自动调整输入框高度

    messageInput.addEventListener('input', () => {

        messageInput.style.height = 'auto';

        messageInput.style.height = messageInput.scrollHeight + 'px';

    });

});

  

// 发送消息

async function sendMessage() {

    const message = messageInput.value.trim();

  

    if (!message || isProcessing) {

        return;

    }

  

    // 禁用输入

    isProcessing = true;

    sendBtn.disabled = true;

    messageInput.disabled = true;

    updateStatus('发送中...');

  

    // 显示用户消息

    addMessage('user', message);

  

    // 清空输入框

    messageInput.value = '';

    messageInput.style.height = 'auto';

  

    // 获取当前模式

    const mode = modeSelect.value;

  

    try {

        // 调用流式 API

        await sendStreamRequest(message, mode);

    } catch (error) {

        console.error('发送失败:', error);

        addMessage('assistant', `❌ 错误: ${error.message}`);

        updateStatus('发送失败');

    } finally {

        // 恢复输入

        isProcessing = false;

        sendBtn.disabled = false;

        messageInput.disabled = false;

        messageInput.focus();

        updateStatus('就绪');

    }

}

  

// 发送流式请求

async function sendStreamRequest(message, mode) {

    updateStatus('AI 思考中...');

  

    // 创建 AI 消息容器

    const messageDiv = createMessageElement('assistant');

    const contentDiv = messageDiv.querySelector('.message-content');

  

    let fullResponse = '';

  

    try {

        const response = await fetch('/api/chat/stream', {

            method: 'POST',

            headers: {

                'Content-Type': 'application/json',

            },

            body: JSON.stringify({

                message: message,

                mode: mode,

                history: conversationHistory

            })

        });

  

        if (!response.ok) {

            throw new Error(`HTTP ${response.status}: ${response.statusText}`);

        }

  

        // 读取流式响应

        const reader = response.body.getReader();

        const decoder = new TextDecoder();

  

        while (true) {

            const { done, value } = await reader.read();

  

            if (done) {

                break;

            }

  

            // 解码数据

            const chunk = decoder.decode(value, { stream: true });

            const lines = chunk.split('\n');

  

            for (const line of lines) {

                if (line.startsWith('data: ')) {

                    const data = line.slice(6);

  

                    if (data === '[DONE]') {

                        continue;

                    }

  

                    // 累积响应内容

                    fullResponse += data;

  

                    // 实时渲染 Markdown

                    contentDiv.innerHTML = marked.parse(fullResponse);

  

                    // 高亮代码块

                    contentDiv.querySelectorAll('pre code').forEach((block) => {

                        hljs.highlightElement(block);

                    });

  

                    // 滚动到底部

                    scrollToBottom();

                }

            }

        }

  

        // 保存到对话历史

        conversationHistory.push(

            { role: 'user', content: message },

            { role: 'assistant', content: fullResponse }

        );

  

        updateStatus('完成');

  

    } catch (error) {

        contentDiv.innerHTML = `<p class="text-red-400">❌ 错误: ${error.message}</p>`;

        throw error;

    }

}

  

// 添加消息到界面

function addMessage(role, content) {

    const messageDiv = createMessageElement(role);

    const contentDiv = messageDiv.querySelector('.message-content');

  

    if (role === 'assistant') {

        // AI 消息使用 Markdown 渲染

        contentDiv.innerHTML = marked.parse(content);

  

        // 高亮代码块

        contentDiv.querySelectorAll('pre code').forEach((block) => {

            hljs.highlightElement(block);

        });

    } else {

        // 用户消息纯文本

        contentDiv.textContent = content;

    }

  

    scrollToBottom();

}

  

// 创建消息元素

function createMessageElement(role) {

    const messageDiv = document.createElement('div');

    messageDiv.className = `message ${role}-message`;

  

    const isUser = role === 'user';

    const avatar = isUser ? '👤' : 'AI';

    const bgColor = isUser ? 'bg-green-600' : 'bg-blue-600';

    const contentBg = isUser ? 'bg-gray-700' : 'bg-gray-700';

  

    messageDiv.innerHTML = `

        <div class="flex items-start gap-3">

            <div class="w-8 h-8 rounded-full ${bgColor} flex items-center justify-center flex-shrink-0">

                <span class="text-sm">${avatar}</span>

            </div>

            <div class="flex-1">

                <div class="${contentBg} rounded-lg p-4 message-content"></div>

            </div>

        </div>

    `;

  

    messagesContainer.appendChild(messageDiv);

    return messageDiv;

}

  

// 清空对话

function clearConversation() {

    if (confirm('确定要清空所有对话吗?')) {

        conversationHistory = [];

        messagesContainer.innerHTML = `

            <div class="message assistant-message">

                <div class="flex items-start gap-3">

                    <div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center flex-shrink-0">

                        <span class="text-sm">AI</span>

                    </div>

                    <div class="flex-1">

                        <div class="bg-gray-700 rounded-lg p-4">

                            <p>对话已清空。有什么可以帮助你的吗?</p>

                        </div>

                    </div>

                </div>

            </div>

        `;

        updateStatus('已清空');

    }

}

  

// 滚动到底部

function scrollToBottom() {

    messagesContainer.scrollTop = messagesContainer.scrollHeight;

}

  

// 更新状态文本

function updateStatus(text) {

    statusText.textContent = text;

}

第六阶段:最终部署与全链路测试

完成所有代码编写后,项目结构清晰,后端逻辑严密,前端交互完善。

下图展示了最终的项目文件目录树,清晰地反映了MVC(Model-View-Controller)式的架构思想。

image.png

6.1 启动服务

在终端根目录下运行cargo run。Cargo会自动编译所有依赖并启动应用。首次编译可能需要几分钟,之后的增量编译将非常快。

image.png

上图显示服务器已成功监听在0.0.0.0:3000端口,且配置加载无误。
项目结构如下:

.
├── Cargo.toml
├── src
│   ├── api
│   │   ├── chat.rs
│   │   ├── health.rs
│   │   └── mod.rs
│   ├── config.rs
│   ├── main.rs
│   ├── models
│   │   ├── chat.rs
│   │   └── mod.rs
│   └── services
│       ├── deepseek.rs
│       └── mod.rs
└── static
    ├── css
    │   └── style.css
    └── js
        └── app.js

6.2 接口验证

在打开浏览器之前,通过curl命令测试健康检查接口是一个好习惯,这能排除网络层面的基本问题。

curl http://localhost:3000/api/health

如下图所示,接口返回了JSON格式的ok状态,证明API服务已正常响应。

image.png

6.3 浏览器体验与监控

访问浏览器地址(如http://localhost:3000),即可看到深色主题的聊天界面。输入问题后,可以看到AI的回答以打字机的形式流畅涌现,代码块被正确高亮,Markdown格式渲染完美。

与此同时,在蓝耘的控制台能够实时查看到Token的消耗情况和API调用记录。这为开发者提供了透明的用量监控和成本管理能力。

image.png

总结

本文详细展示了如何利用Rust的高性能特性与蓝耘DeepSeek的强大模型能力,构建一个端到端的生成式AI应用。从Linux底层库的安装到Rust异步流处理,再到前端的SSE流式渲染,每一个环节都体现了现代Web开发的最佳实践。Rust的安全性保证了后端服务的稳健,Axum框架提供了极高的并发处理能力,而蓝耘MaaS服务则为应用注入了智能的核心。这种全栈架构不仅适用于个人助手项目,更为构建企业级AI应用提供了坚实的技术参考。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐