Skip to content

Spring AI + Ollama 本地大模型集成:从聊天到嵌入向量的完整实践

作者: 必码 | bima.cc


一、AI集成架构概述

1.1 Spring AI框架介绍

Spring AI 是 Spring 生态系统中面向人工智能应用开发的全新框架项目,其设计理念与 Spring Data、Spring Security 等经典框架一脉相承——通过抽象层屏蔽底层实现差异,让开发者能够以统一的方式对接多种大语言模型(LLM)提供商。

在传统的大模型集成开发中,开发者面临的痛点是显而易见的:不同厂商的 API 接口格式各异,Ollama 使用 /api/generate 端点返回 response 字段,OpenAI 使用 /v1/chat/completions 端点返回 choices[0].message.content 结构,而各种兼容 OpenAI 的第三方服务(如 DeepSeek、通义千问的 API 服务)虽然在接口格式上向 OpenAI 靠拢,但在细节上仍有差异。这种碎片化导致每接入一个新模型,都需要重写大量的适配代码。

Spring AI 的核心价值在于提供了一套统一的抽象接口:

  • ChatClient / ChatModel:统一的聊天模型抽象,屏蔽不同提供商的 API 差异
  • EmbeddingModel:统一的嵌入向量模型抽象
  • VectorStore:统一的向量存储抽象(支持 ChromaDB、Pinecone、Redis Vector 等)
  • Prompt Template:统一的提示词模板管理

然而,在实际项目落地中,我们发现 Spring AI 的默认实现并不总是能满足生产环境的全部需求。特别是在以下场景中:

  1. 流式响应的精细化控制:Spring AI 默认的流式实现可能无法满足自定义 SSE 事件格式的需求
  2. 多协议适配:当需要同时支持 Ollama 原生协议和 OpenAI 兼容协议时,需要更灵活的适配层
  3. 用户级模型配置:生产环境中通常需要支持每个用户配置自己的模型参数,这超出了 Spring AI 默认配置的范围

因此,在 smart-scaffold-springboot 项目中,我们采用了一种"借鉴 Spring AI 设计理念,但自主实现核心逻辑"的策略。通过自研的 ChatClientFactoryEmbeddingConfigModelService 等组件,构建了一套既灵活又可控的 AI 集成架构。

1.2 本地大模型 vs 云端API

在架构选型阶段,我们需要在本地大模型和云端 API 之间做出权衡。这两种方案各有优劣,适用于不同的业务场景。

本地大模型(以 Ollama 为代表)的优势:

维度说明
数据隐私所有数据在本地处理,不离开企业网络边界,满足金融、医疗等行业的合规要求
成本可控一次性硬件投入后,推理成本几乎为零,适合高频调用场景
无网络依赖不受网络波动影响,延迟稳定且可预测
模型自主可以自由选择和微调开源模型,不受厂商锁定
无限调用不受 API 调用次数和频率限制

云端 API(以 OpenAI 为代表)的优势:

维度说明
模型能力GPT-4o、Claude 等顶级模型的能力仍然领先开源模型
零运维无需 GPU 服务器、无需模型部署和维护
弹性扩展自动处理并发,无需考虑服务器容量
快速迭代新模型发布后可以立即使用

我们的架构决策:同时支持两种模式,由用户自主选择。

在 smart-scaffold-springboot 项目中,我们设计了三种 AI 提供商类型:

OLLAMA            -- 本地 Ollama 部署,适合对数据隐私要求高的场景
OPENAI            -- OpenAI 官方 API,适合需要高精度模型能力的场景
COMPATIBLE_OPENAI -- 兼容 OpenAI 接口的第三方服务,如 DeepSeek、通义千问等

这种设计让系统具备了极大的灵活性:开发环境可以使用免费的本地 Ollama 模型进行调试,生产环境可以切换到 OpenAI 获取更强的推理能力,或者使用兼容 OpenAI 接口的国产大模型服务。

1.3 项目中的AI模块设计

smart-scaffold-springboot 项目采用经典的分层架构,AI 模块的设计遵循这一架构原则,清晰地划分了各层的职责。

smart-scaffold-springboot/
├── smart-scaffold-common/          # 公共模块
│   └── po/
│       └── ModelPO.java            # 模型配置值对象
├── smart-scaffold-dao/             # 数据访问层
│   ├── entity/db1/
│   │   └── UserModel.java          # 用户模型实体(继承ModelPO)
│   ├── dto/db1/
│   │   ├── UserModelDTO.java       # 用户模型传输对象
│   │   └── UserModelQueryDTO.java  # 用户模型查询对象
│   └── mapper/db1/
│       └── UserModelMapper.java    # MyBatis Mapper
├── smart-scaffold-service/         # 业务逻辑层
│   └── ai/
│       ├── ChatClientFactory.java  # 聊天客户端工厂(核心)
│       ├── EmbeddingConfig.java    # 嵌入向量配置
│       ├── ModelService.java       # 模型配置服务
│       └── writing/
│           ├── WritingPromptService.java  # 提示词管理
│           └── WritingStyleService.java   # 写作风格管理
└── smart-scaffold-web/             # Web控制层
    └── controller/ai/
        └── AIController.java       # AI功能控制器

核心设计原则:

  1. 单一职责:每个类只负责一个明确的功能领域。ChatClientFactory 负责构建和发送聊天请求,ModelService 负责模型配置的获取和管理,EmbeddingConfig 专门处理嵌入向量相关的逻辑。

  2. 依赖倒置ChatClientFactory 不直接依赖具体的模型配置来源,而是通过 ModelService 接口获取配置。这意味着配置可以从数据库、配置文件或任何其他来源加载,而不需要修改 ChatClientFactory 的代码。

  3. 开闭原则:通过 apiType 字段区分不同的 AI 提供商,新增提供商类型时只需要扩展 getApiPath()buildRequestBody() 方法中的分支逻辑,而不需要修改已有的代码路径。

  4. 值对象继承UserModel 实体继承自 ModelPOModelPO 封装了所有 AI 提供商共有的配置字段(apiType、apiKey、baseUrl、modelName 等),UserModel 在此基础上增加了用户关联、时间戳等数据库层面的字段。这种设计避免了字段重复定义,同时保持了清晰的继承关系。


二、ChatClientFactory 多模型聊天客户端工厂

2.1 工厂模式设计思路

ChatClientFactory 是整个 AI 集成模块的核心组件,它采用了工厂模式的设计思想,负责根据用户的配置创建合适的聊天客户端并发送请求。

在传统的工厂模式中,通常会有一个接口和多个实现类。但在我们的场景中,由于不同 AI 提供商之间的差异主要体现在 API 路径和请求体格式上,而 HTTP 通信的底层逻辑是相同的,因此我们采用了更轻量的"方法级工厂"设计——通过一个类中的不同方法路径来适配不同的提供商。

java
@Slf4j
@Component
@RequiredArgsConstructor
public class ChatClientFactory {

    private final ModelService modelService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    // 同步聊天方法
    public String chat(Long userId, String message, Boolean isCustom) { ... }

    // 流式聊天方法
    public Flux<String> chatStream(Long userId, String message, Boolean isCustom) { ... }

    // 根据API类型返回不同的API路径
    private String getApiPath(String apiType, boolean stream) { ... }

    // 根据API类型构建不同的请求体
    private Map<String, Object> buildRequestBody(ModelPO config, String model,
                                                 String message, boolean stream) { ... }
}

设计亮点:

  1. 统一入口:无论使用哪种 AI 提供商,调用方只需要调用 chat()chatStream() 方法,传入 userId、message 和 isCustom 参数即可。提供商的切换对调用方完全透明。

  2. 配置驱动:所有的模型配置信息(API 类型、基础 URL、模型名称、温度参数等)都封装在 ModelPO 对象中,由 ModelService 统一管理。ChatClientFactory 本身不持有任何配置状态。

  3. 同步/异步双模式:提供 chat() 同步方法和 chatStream() 异步流式方法,满足不同场景的需求。简单问答场景使用同步方法,长文本生成场景使用流式方法。

2.2 支持三种AI提供商

ChatClientFactory 通过 ModelPO 中的 apiType 字段来区分不同的 AI 提供商,并在 getApiPath()buildRequestBody() 方法中实现了差异化处理。

API 路径映射:

java
private String getApiPath(String apiType, boolean stream) {
    if ("OLLAMA".equals(apiType)) {
        // Ollama使用原生API端点
        return "/api/generate";
    } else {
        // OPENAI和COMPATIBLE_OPENAI使用OpenAI兼容接口
        return "/v1/chat/completions";
    }
}

这里有一个关键的设计决策:Ollama 实际上也提供了 OpenAI 兼容的 /v1/chat/completions 端点,但我们选择了使用其原生的 /api/generate 端点。原因是原生端点的响应格式更简洁(直接返回 response 字段),且在某些版本中性能更优。

请求体格式差异:

java
private Map<String, Object> buildRequestBody(ModelPO config, String model,
                                             String message, boolean stream) {
    if ("OLLAMA".equals(config.getApiType())) {
        // Ollama API格式
        return Map.of(
            "model", model,
            "prompt", message,           // 注意:Ollama使用prompt字段
            "stream", stream,
            "options", Map.of(
                "temperature", config.getTemperature() != null
                    ? config.getTemperature().doubleValue() : 0.7,
                "max_tokens", config.getMaxTokens() != null
                    ? config.getMaxTokens() : 2000
            )
        );
    } else {
        // OpenAI兼容API格式
        return Map.of(
            "model", model,
            "messages", List.of(
                Map.of("role", "system", "content",
                       "你是一个智能助手,需要准确回答用户的问题。"),
                Map.of("role", "user", "content", message)
            ),
            "temperature", config.getTemperature() != null
                ? config.getTemperature().doubleValue() : 0.7,
            "max_tokens", config.getMaxTokens() != null
                ? config.getMaxTokens() : 2000,
            "stream", stream
        );
    }
}

两种格式的主要差异点:

特性Ollama 格式OpenAI 格式
消息字段prompt(纯文本)messages(角色数组)
系统提示词需要拼接到 prompt 中独立的 system role 消息
参数位置嵌套在 options 对象中与 model 同级
响应格式{"response": "..."}{"choices": [{"message": {"content": "..."}}]}

2.3 WebClient实现详解

ChatClientFactory 使用 Spring WebFlux 的 WebClient 作为 HTTP 客户端,而非传统的 RestTemplate。这是一个经过深思熟虑的技术选型。

为什么选择 WebClient 而非 RestTemplate?

  1. 原生支持响应式流WebClient 天然支持 FluxMono,可以无缝处理 SSE 流式响应。而 RestTemplate 是同步阻塞的,处理流式响应需要额外的复杂性。

  2. 非阻塞 I/O:在高并发场景下,WebClient 基于 Netty 的非阻塞 I/O 模型可以显著提升吞吐量,减少线程占用。

  3. Spring 官方推荐:自 Spring 5 以来,RestTemplate 已进入维护模式,Spring 官方推荐使用 WebClient 作为替代。

同步聊天方法的 WebClient 使用:

java
public String chat(Long userId, String message, Boolean isCustom) {
    ModelPO config = modelService.createModelService(userId, isCustom);
    String apiKey = config.getApiKey();
    String baseUrl = getBaseUrl(config);
    String model = config.getModelName();
    String apiPath = getApiPath(config.getApiType(), false);

    // 构建WebClient
    WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);

    // 添加认证头
    if (apiKey != null && !apiKey.isEmpty()) {
        webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION,
                                       "Bearer " + apiKey);
    }

    WebClient webClient = webClientBuilder.build();

    // 构建请求体
    Map<String, Object> requestBody = buildRequestBody(config, model, message, false);

    // 发送请求(同步阻塞)
    String responseBody = webClient.post()
        .uri(apiPath)
        .contentType(MediaType.APPLICATION_JSON)
        .body(BodyInserters.fromValue(requestBody))
        .retrieve()
        .bodyToMono(String.class)
        .block();  // 阻塞等待响应

    // 解析响应(支持多种格式)
    Map<String, Object> parsedResponseBody =
        objectMapper.readValue(responseBody, Map.class);

    // 1. 尝试OLLAMA格式
    if (parsedResponseBody.containsKey("response")) {
        return (String) parsedResponseBody.get("response");
    }
    // 2. 尝试OpenAI兼容格式
    if (parsedResponseBody.containsKey("choices")) {
        List<Map<String, Object>> choices =
            (List<Map<String, Object>>) parsedResponseBody.get("choices");
        Map<String, Object> messageMap =
            (Map<String, Object>) choices.get(0).get("message");
        return (String) messageMap.get("content");
    }
    // 3. 尝试message.content格式
    if (parsedResponseBody.containsKey("message")) {
        Map<String, Object> messageMap =
            (Map<String, Object>) parsedResponseBody.get("message");
        return (String) messageMap.get("content");
    }

    throw new RuntimeException("无法从响应中提取内容: " + responseBody);
}

响应解析的三层兜底策略:

代码中对响应格式的解析采用了三层兜底策略,确保最大程度的兼容性:

  1. 第一层:检查 response 字段(Ollama 原生格式)
  2. 第二层:检查 choices 字段(OpenAI 标准格式)
  3. 第三层:检查 message 字段(某些兼容服务的简化格式)

这种设计使得系统能够自动适配各种 API 响应格式,无需调用方关心底层使用的是哪种提供商。

2.4 多模型适配策略

在实际生产环境中,多模型适配不仅仅是 API 路径和请求体格式的差异,还涉及以下更深层次的适配需求:

1. 认证机制差异

java
// Ollama本地部署通常不需要API Key
if (apiKey != null && !apiKey.isEmpty()) {
    webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION,
                                   "Bearer " + apiKey);
}

Ollama 本地部署默认不需要认证,而 OpenAI 和兼容服务需要 API Key。代码中通过判断 apiKey 是否为空来决定是否添加认证头,实现了认证机制的自动适配。

2. Base URL 的灵活处理

java
private String getBaseUrl(ModelPO config) {
    if (hasValue(config.getBaseUrl())) {
        String baseUrl = config.getBaseUrl().trim();
        while (baseUrl.endsWith("/")) {
            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
        }
        return baseUrl;
    }
    return "https://api.openai.com";  // 默认使用OpenAI官方API
}

Base URL 的处理包含了两个细节:

  • 自动去除末尾的斜杠,避免拼接后出现双斜杠
  • 当用户未配置 Base URL 时,默认使用 OpenAI 官方地址

3. 默认值兜底

java
// 模型名称默认值
if (model == null) {
    model = "gpt-3.5-turbo";
}
// 温度参数默认值
config.getTemperature() != null ? config.getTemperature().doubleValue() : 0.7
// 最大Token数默认值
config.getMaxTokens() != null ? config.getMaxTokens() : 2000

每个参数都有合理的默认值,确保即使配置不完整,系统也能正常运行。

2.5 自定义模型切换机制

ChatClientFactorychat()chatStream() 方法都接受一个 isCustom 参数,这个参数决定了使用系统默认配置还是用户自定义配置:

java
public ModelPO createModelService(Long userId, Boolean isCustom) {
    if (isCustom) {
        return this.getUserModelById(userId);  // 用户自定义配置
    } else {
        return this.getSystemModel();           // 系统默认配置
    }
}

这种设计允许系统管理员设置全局默认模型,同时允许每个用户配置自己偏好的模型。例如:

  • 系统默认使用 Ollama + qwen2.5:7b 模型(免费、快速)
  • 用户 A 配置了自己的 OpenAI API Key,使用 GPT-4o(更强能力)
  • 用户 B 配置了 DeepSeek API,使用 deepseek-r1(性价比高)

三、Ollama本地模型集成

3.1 Ollama服务部署

Ollama 是一个开源的大语言模型运行时,它让在本地部署和运行大语言模型变得像使用 Docker 一样简单。在 smart-scaffold-springboot 项目中,Ollama 作为默认的 AI 提供商,承担了主要的模型推理任务。

安装方式:

bash
# Linux (curl安装)
curl -fsSL https://ollama.com/install.sh | sh

# macOS (Homebrew安装)
brew install ollama

# Docker部署
docker run -d -v ollama:/root/.ollama -p 11434:11434 ollama/ollama

systemd 服务管理(Linux):

bash
# 启动Ollama服务
sudo systemctl start ollama

# 设置开机自启
sudo systemctl enable ollama

# 查看服务状态
sudo systemctl status ollama

配置环境变量:

bash
# 设置模型存储路径
OLLAMA_MODELS=/data/ollama/models

# 设置监听地址(允许远程访问)
OLLAMA_HOST=0.0.0.0:11434

# 设置并行请求数
OLLAMA_NUM_PARALLEL=4

# 设置最大并发模型加载
OLLAMA_MAX_LOADED_MODELS=3

在项目的 application.properties 中,Ollama 的默认配置如下:

properties
# AI配置 - Ollama
bima.ai.default-api-type=OLLAMA
bima.ai.ollama.model-url=http://localhost:11434
bima.ai.ollama.model-name=qwen2.5:7b-instruct-q4_k_m
bima.ai.ollama.api-key=
bima.ai.ollama.temperature=0.7
bima.ai.ollama.max-tokens=4096
bima.ai.ollama.embedding-url=http://localhost:11434
bima.ai.ollama.embedding-name=nomic-embed-text

3.2 API调用方式

Ollama 提供了 RESTful API 接口,主要包含以下几个端点:

聊天生成接口 /api/generate

这是项目中最主要使用的 Ollama 端点,用于发送聊天请求并获取模型响应。

bash
# 基本请求
curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b-instruct-q4_k_m",
  "prompt": "什么是Spring AI?",
  "stream": false,
  "options": {
    "temperature": 0.7,
    "max_tokens": 2000
  }
}'

响应格式:

json
{
  "model": "qwen2.5:7b-instruct-q4_k_m",
  "response": "Spring AI 是 Spring 生态中面向 AI 应用开发的框架...",
  "done": true,
  "total_duration": 3567892000,
  "eval_count": 156,
  "eval_duration": 3456789000
}

流式生成请求:

bash
curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b-instruct-q4_k_m",
  "prompt": "写一首关于春天的诗",
  "stream": true
}'

流式响应会以 SSE 格式逐块返回:

{"model":"qwen2.5:7b-instruct-q4_k_m","response":"春","done":false}
{"model":"qwen2.5:7b-instruct-q4_k_m","response":"风","done":false}
{"model":"qwen2.5:7b-instruct-q4_k_m","response":"拂","done":false}
...
{"model":"qwen2.5:7b-instruct-q4_k_m","response":"","done":true}

嵌入向量接口 /api/embeddings

bash
curl http://localhost:11434/api/embeddings -d '{
  "model": "nomic-embed-text",
  "prompt": "Spring AI 是一个强大的框架"
}'

响应格式:

json
{
  "model": "nomic-embed-text",
  "embedding": [0.0234, -0.0156, 0.0789, ...]
}

模型管理接口:

bash
# 查看已安装的模型
curl http://localhost:11434/api/tags

# 查看模型信息
curl http://localhost:11434/api/show -d '{"name": "qwen2.5:7b-instruct-q4_k_m"}'

# 删除模型
curl -X DELETE http://localhost:11434/api/delete -d '{"name": "old-model"}'

3.3 模型管理

Ollama 支持丰富的开源大语言模型,以下是在项目中常用的模型及其特点:

推荐模型列表:

模型名称参数量量化方式特点适用场景
qwen2.5:7b-instruct-q4_k_m7BQ4_K_M中文能力强,推理速度快通用对话、中文场景
llama3:8b8BQ4_0Meta出品,英文能力强英文对话、代码生成
chatglm3:6b6BQ4_0清华出品,中英双语中英混合场景
deepseek-r1:7b7BQ4_K_M推理能力强数学推理、逻辑分析
nomic-embed-text--专用嵌入模型文本向量化

模型安装命令:

bash
# 安装Qwen2.5(项目默认模型)
ollama pull qwen2.5:7b-instruct-q4_k_m

# 安装Llama3
ollama pull llama3:8b

# 安装ChatGLM3
ollama pull chatglm3:6b

# 安装嵌入模型
ollama pull nomic-embed-text

# 查看已安装模型
ollama list

量化等级说明:

Ollama 支持多种量化等级,量化等级越低,模型体积越小、推理速度越快,但精度会有所损失:

  • Q4_0 / Q4_K_M:4-bit 量化,推荐用于 7B-13B 模型,性价比最高
  • Q5_K_M:5-bit 量化,精度和速度的良好平衡
  • Q8_0:8-bit 量化,精度接近原始模型,但体积较大
  • F16:半精度浮点,精度最高,需要更多显存

3.4 硬件要求与性能优化

最低硬件要求:

模型规模最低显存推荐显存最低内存推荐CPU
3B (Q4)4 GB8 GB8 GB4核
7B (Q4)6 GB12 GB16 GB8核
7B (Q8)10 GB16 GB16 GB8核
13B (Q4)10 GB16 GB32 GB8核
33B (Q4)20 GB24 GB64 GB16核

性能优化策略:

  1. 模型量化选择:对于大多数场景,Q4_K_M 量化是最佳选择。它在模型精度和推理速度之间取得了很好的平衡。只有在需要更高精度的场景(如专业代码生成、数学推理)中,才需要使用 Q8 或 F16。

  2. GPU 加速:确保安装了正确的 GPU 驱动和 CUDA 工具包:

bash
# 检查NVIDIA驱动
nvidia-smi

# 检查CUDA版本
nvcc --version
  1. 并发控制:通过环境变量控制并发请求数和最大加载模型数:
bash
OLLAMA_NUM_PARALLEL=4        # 同时处理4个请求
OLLAMA_MAX_LOADED_MODELS=2   # 最多同时加载2个模型到显存
  1. 模型预热:在系统启动时预先加载模型,避免首次请求的延迟:
bash
# 在systemd服务启动后执行预热
ollama run qwen2.5:7b-instruct-q4_k_m "你好" &
  1. 内存映射优化:将模型文件放在高速存储设备上(NVMe SSD),减少模型加载时间。

四、OpenAI API集成

4.1 官方API调用

OpenAI API 是目前生态最成熟、应用最广泛的大语言模型 API 服务。在 smart-scaffold-springboot 项目中,OpenAI 作为可选的 AI 提供商,为需要更强模型能力的场景提供支持。

项目中的 OpenAI 配置:

properties
# AI配置 - OpenAI
bima.ai.openai.base-url=https://api.openai.com
bima.ai.openai.model-name=gpt-4o
bima.ai.openai.embedding-model=text-embedding-3-small
bima.ai.openai.api-key=sk-your-api-key-here
bima.ai.openai.temperature=0.7
bima.ai.openai.max-tokens=4096

切换到 OpenAI 模式:

properties
# 修改默认API类型为OPENAI
bima.ai.default-api-type=OPENAI

OpenAI API 请求格式(由 ChatClientFactory 自动构建):

json
{
  "model": "gpt-4o",
  "messages": [
    {"role": "system", "content": "你是一个智能助手,需要准确回答用户的问题。"},
    {"role": "user", "content": "什么是Spring AI?"}
  ],
  "temperature": 0.7,
  "max_tokens": 4096,
  "stream": false
}

OpenAI API 响应格式(由 ChatClientFactory 自动解析):

json
{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "model": "gpt-4o",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Spring AI 是 Spring 生态中面向 AI 应用开发的框架..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 156,
    "total_tokens": 181
  }
}

4.2 兼容OpenAI接口

兼容 OpenAI 接口是指那些遵循 OpenAI API 格式但由第三方服务商提供的接口。这类服务在国内市场非常流行,包括 DeepSeek、通义千问、智谱 AI、月之暗面等。

项目中的兼容 OpenAI 配置:

properties
# AI配置 - 兼容OpenAI
bima.ai.compatible-openai.base-url=https://api.deepseek.com
bima.ai.compatible-openai.model-name=deepseek-chat
bima.ai.compatible-openai.embedding-model=
bima.ai.compatible-openai.api-key=sk-your-deepseek-key
bima.ai.compatible-openai.temperature=0.7
bima.ai.compatible-openai.max-tokens=4096

切换到兼容 OpenAI 模式:

properties
bima.ai.default-api-type=COMPATIBLE_OPENAI

兼容性设计要点:

由于兼容 OpenAI 接口在格式上与 OpenAI 官方 API 基本一致,ChatClientFactory 将它们归为同一类处理,都使用 /v1/chat/completions 端点和相同的请求/响应格式。唯一的区别是 baseUrl 不同:

提供商Base URL模型名称示例
OpenAI 官方https://api.openai.comgpt-4o
DeepSeekhttps://api.deepseek.comdeepseek-chat
通义千问https://dashscope.aliyuncs.com/compatible-modeqwen-turbo
智谱 AIhttps://open.bigmodel.cn/api/paasglm-4
月之暗面https://api.moonshot.cnmoonshot-v1-8k

这种设计使得用户只需要修改 base-urlmodel-name 两个配置项,就可以在不同提供商之间无缝切换。

4.3 API Key管理

API Key 是访问 AI 服务的凭证,其安全管理至关重要。在 smart-scaffold-springboot 项目中,API Key 的管理分为两个层面:

系统级 API Key(配置文件):

properties
# 系统级API Key,存储在application.properties中
bima.ai.openai.api-key=sk-proj-xxxxxxxxxxxx
bima.ai.compatible-openai.api-key=sk-xxxxxxxxxxxx

系统级 API Key 适用于所有用户共享一个 API 账号的场景。在开发环境或小型部署中,这种方式简单直接。

用户级 API Key(数据库加密存储):

在生产环境中,更推荐让每个用户配置自己的 API Key。用户级 API Key 存储在 user_model 表的 api_key 字段中,采用 AES-256 加密存储(详见第十章安全与性能)。

API Key 的传递流程:

用户配置 API Key
    ↓ (AES-256加密)
存储到 user_model.api_key
    ↓ (AES-256解密)
ModelService.getUserModelById()
    ↓ (明文,仅在内存中)
ChatClientFactory.chat() / chatStream()
    ↓ (添加到HTTP请求头)
WebClient → AI服务提供商

4.4 请求频率限制

不同的 AI 服务提供商有不同的请求频率限制(Rate Limit),在系统设计中需要考虑这些限制:

提供商免费层限制付费层限制限制维度
OpenAI20 RPM / 40K TPM5000 RPM / 2M TPMRPM + TPM
DeepSeek60 RPM300 RPMRPM
Ollama 本地无限制无限制仅受硬件限制

项目中的频率控制策略:

  1. 批量请求延迟:在批量聊天接口中,通过 Thread.sleep(100) 控制请求频率:
java
List<String> responses = messages.stream().map(message -> {
    try {
        Thread.sleep(100);  // 100ms延迟,避免请求过于频繁
        return chatClientFactory.chat(userId, message, false);
    } catch (Exception e) {
        return "错误: " + e.getMessage();
    }
}).toList();
  1. 异步处理:对于耗时较长的请求,提供异步接口:
java
@PostMapping(value = "/chat/async", consumes = MediaType.TEXT_PLAIN_VALUE)
public BaseResult<?> chatAsync(@RequestParam("userId") Long userId,
                                @RequestBody String message) {
    CompletableFuture.supplyAsync(() -> {
        return chatClientFactory.chat(userId, message, false);
    }).thenAccept(response -> {
        // 回调处理:保存到数据库或通知用户
    });
    return BaseResult.success(Map.of("taskId", "async-" + System.currentTimeMillis()));
}
  1. 前端限流:通过前端 EventSource 的连接管理,避免用户重复发起请求。

五、SSE流式聊天实现

5.1 WebFlux + WebClient基础

SSE(Server-Sent Events)是一种基于 HTTP 的服务器推送技术,它允许服务器通过一个持久的 HTTP 连接向客户端持续发送数据。在 AI 聊天场景中,SSE 是实现"打字机效果"的标准方案。

项目依赖配置:

xml
<!-- Spring WebFlux依赖,用于支持SSE流式响应 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

值得注意的是,项目的主框架是 Spring MVC(基于 Servlet),而非纯 WebFlux 应用。通过引入 spring-boot-starter-webflux 依赖,我们可以在 Spring MVC 应用中使用 WebClient 进行响应式 HTTP 调用,同时使用 FluxServerSentEvent 来实现 SSE 流式响应。

WebClient 的流式请求配置:

java
// 构建支持SSE的WebClient
WebClient.Builder webClientBuilder = WebClient.builder().baseUrl(baseUrl);

// 添加认证头
if (apiKey != null && !apiKey.isEmpty()) {
    webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION,
                                   "Bearer " + apiKey);
}

// 添加SSE相关的HTTP头
webClientBuilder.defaultHeader("X-Accel-Buffering", "no");
webClientBuilder.defaultHeader("Cache-Control", "no-cache");
webClientBuilder.defaultHeader("Connection", "keep-alive");

WebClient webClient = webClientBuilder.build();

这里添加的三个 HTTP 头至关重要:

  • X-Accel-Buffering: no:告诉 Nginx 等反向代理不要缓冲响应,确保数据实时传输到客户端
  • Cache-Control: no-cache:禁止缓存,确保每次请求都获取最新数据
  • Connection: keep-alive:保持长连接,避免频繁建立和断开连接的开销

5.2 Flux流式响应处理

ChatClientFactory.chatStream() 方法返回 Flux<String>,这是一个响应式流,它会持续产生数据直到流结束。

流式聊天核心实现:

java
public Flux<String> chatStream(Long userId, String message, Boolean isCustom) {
    ModelPO config = modelService.createModelService(userId, isCustom);
    String apiKey = config.getApiKey();
    String baseUrl = getBaseUrl(config);
    String model = config.getModelName();
    String apiPath = getApiPath(config.getApiType(), true);

    WebClient webClient = buildWebClient(baseUrl, apiKey);
    Map<String, Object> requestBody = buildRequestBody(config, model, message, true);

    return webClient.post()
        .uri(apiPath)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.TEXT_EVENT_STREAM)  // 声明接受SSE流
        .body(BodyInserters.fromValue(requestBody))
        .retrieve()
        .bodyToFlux(String.class)  // 将响应体映射为Flux<String>
        .map(line -> {
            if (!line.trim().isEmpty()) {
                // 提取JSON数据,移除data:前缀
                String jsonStr = line;
                while (jsonStr.startsWith("data:")) {
                    jsonStr = jsonStr.substring("data:".length()).trim();
                }
                return jsonStr;
            }
            return null;
        })
        .filter(line -> line != null)
        .filter(line -> {
            // 过滤掉空响应
            try {
                ObjectMapper mapper = new ObjectMapper();
                Map<String, Object> jsonMap = mapper.readValue(line, Map.class);
                if (jsonMap.containsKey("response")) {
                    String response = (String) jsonMap.get("response");
                    return response != null && !response.isEmpty();
                }
                return true;
            } catch (Exception e) {
                return true;
            }
        });
}

Flux 操作链解析:

  1. bodyToFlux(String.class):将 HTTP 响应体映射为 Flux<String>,每一行响应数据都会触发一次 onNext 事件
  2. .map():对每一行数据进行转换,移除 SSE 协议中的 data: 前缀
  3. .filter(line -> line != null):过滤掉空行
  4. .filter(line -> {...}):过滤掉 Ollama 返回的空响应块(response 字段为空的 JSON)

5.3 SSE事件格式与双协议适配

在 Controller 层,AIController.chatStream() 方法将 Flux<String> 转换为 Flux<ServerSentEvent<String>>,并在此过程中实现了 Ollama 和 OpenAI 双协议的响应解析。

双协议适配核心代码:

java
@PostMapping(value = "/chat/stream",
             produces = MediaType.TEXT_EVENT_STREAM_VALUE,
             consumes = MediaType.APPLICATION_JSON_VALUE)
public Flux<ServerSentEvent<String>> chatStream(
        HttpServletRequest request,
        @RequestBody Map<String, Object> requestBody) {

    Long userId = extractUserId(request, requestBody);
    String message = requestBody.get("message").toString();

    return chatClientFactory.chatStream(userId, message, false)
        .map(content -> {
            String chunk = "";
            try {
                ObjectMapper mapper = new ObjectMapper();
                String cleanContent = content;
                while (cleanContent.startsWith("data:")) {
                    cleanContent = cleanContent.substring("data:".length()).trim();
                }
                Map<String, Object> json = mapper.readValue(cleanContent, Map.class);

                // 尝试解析为OLLAMA格式
                if (json.containsKey("response")) {
                    chunk = (String) json.get("response");
                }
                // 尝试解析为OpenAI格式
                else if (json.containsKey("choices")) {
                    List<Map<String, Object>> choices =
                        (List<Map<String, Object>>) json.get("choices");
                    if (!choices.isEmpty()) {
                        Map<String, Object> choice = choices.get(0);
                        if (choice.containsKey("delta")) {
                            Map<String, Object> delta =
                                (Map<String, Object>) choice.get("delta");
                            if (delta.containsKey("content")) {
                                chunk = (String) delta.get("content");
                            }
                        }
                    }
                }
            } catch (Exception e) {
                chunk = content;  // 解析失败,直接使用原始内容
            }

            // 构建SSE事件
            return ServerSentEvent.<String>builder()
                .event("message")
                .data("{\"type\":\"chunk\",\"content\":\""
                      + escapeJson(chunk) + "\"}")
                .build();
        })
        .filter(event -> {
            // 过滤掉空内容的事件
            String data = event.data();
            try {
                Map<String, Object> json =
                    new ObjectMapper().readValue(data, Map.class);
                if (json.containsKey("content")) {
                    String content = (String) json.get("content");
                    return content != null && !content.isEmpty();
                }
                return true;
            } catch (Exception e) {
                return true;
            }
        })
        // 流结束时发送done事件
        .concatWith(Mono.just(
            ServerSentEvent.<String>builder()
                .event("done")
                .data("{\"type\":\"done\"}")
                .build()
        ))
        // 错误处理
        .onErrorResume(e -> {
            String errorJson = "{\"type\":\"error\",\"error\":\""
                + e.getMessage().replace("\"", "\\\"") + "\"}";
            return Mono.just(
                ServerSentEvent.<String>builder()
                    .event("error")
                    .data(errorJson)
                    .build()
            );
        });
}

Ollama vs OpenAI 流式响应格式对比:

Ollama 的流式响应格式:

data: {"model":"qwen2.5:7b","response":"Spring","done":false}
data: {"model":"qwen2.5:7b","response":" AI","done":false}
data: {"model":"qwen2.5:7b","response":" 是","done":false}
data: {"model":"qwen2.5:7b","response":"","done":true}

OpenAI 的流式响应格式:

data: {"id":"chatcmpl-abc","choices":[{"index":0,"delta":{"content":"Spring"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","choices":[{"index":0,"delta":{"content":" AI"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","choices":[{"index":0,"delta":{"content":" 是"},"finish_reason":null}]}
data: [DONE]

关键差异:

特性OllamaOpenAI
内容字段responsechoices[0].delta.content
结束标记done: truedata: [DONE]
模型信息每个chunk都包含仅在首个chunk包含

统一后的 SSE 输出格式:

json
// 数据chunk
event: message
data: {"type":"chunk","content":"Spring"}

event: message
data: {"type":"chunk","content":" AI"}

// 流结束
event: done
data: {"type":"done"}

// 错误(如果发生)
event: error
data: {"type":"error","error":"连接超时"}

通过这种统一格式,前端只需要一套代码就能处理来自不同 AI 提供商的流式响应。

5.4 前端EventSource API对接

前端使用浏览器原生的 EventSource API 来接收 SSE 流式响应。以下是一个完整的前端对接示例:

javascript
// 创建SSE连接
function startStreamChat(userId, message) {
    const eventSource = new EventSource(
        '/ai/chat/stream',
        {
            // 注意:EventSource只支持GET请求
            // 对于POST请求,需要使用fetch + ReadableStream
        }
    );
}

由于标准的 EventSource API 只支持 GET 请求,而我们的聊天接口使用 POST 方法,因此需要使用 fetch API 配合 ReadableStream 来实现:

javascript
async function streamChat(userId, message) {
    const response = await fetch('/ai/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, message })
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop();  // 保留不完整的行

        for (const line of lines) {
            if (line.startsWith('event:')) {
                const eventType = line.substring(6).trim();
                // 处理不同的事件类型
                handleEventType(eventType);
            } else if (line.startsWith('data:')) {
                const data = line.substring(5).trim();
                if (data) {
                    handleStreamData(JSON.parse(data));
                }
            }
        }
    }
}

function handleStreamData(data) {
    switch (data.type) {
        case 'chunk':
            // 追加内容到显示区域
            appendToChatArea(data.content);
            break;
        case 'done':
            // 流结束
            onStreamComplete();
            break;
        case 'error':
            // 错误处理
            onStreamError(data.error);
            break;
    }
}

5.5 流式异常处理和连接管理

流式聊天的异常处理比普通请求更加复杂,因为连接可能在中途断开,需要在多个层面进行处理。

后端异常处理:

java
// 在Flux链中添加错误恢复
.onErrorResume(e -> {
    log.error("AI流式聊天失败: {}", e.getMessage(), e);
    String errorJson = "{\"type\":\"error\",\"error\":\""
        + e.getMessage().replace("\"", "\\\"") + "\"}";
    return Mono.just(
        ServerSentEvent.<String>builder()
            .event("error")
            .data(errorJson)
            .build()
    );
})

前端连接管理:

javascript
class StreamChatManager {
    constructor() {
        this.abortController = null;
    }

    async start(userId, message, onChunk, onDone, onError) {
        // 如果有正在进行的请求,先取消
        this.stop();

        this.abortController = new AbortController();

        try {
            const response = await fetch('/ai/chat/stream', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ userId, message }),
                signal: this.abortController.signal
            });

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            // ... 流式读取逻辑
        } catch (error) {
            if (error.name === 'AbortError') {
                console.log('请求已取消');
            } else {
                onError(error.message);
            }
        }
    }

    stop() {
        if (this.abortController) {
            this.abortController.abort();
            this.abortController = null;
        }
    }
}

连接超时处理:

java
// 在WebClient中配置超时
import reactor.netty.http.client.HttpClient;
import io.netty.handler.timeout.TimeoutHandler;

HttpClient httpClient = HttpClient.create()
    .responseTimeout(Duration.ofSeconds(120))  // 响应超时120秒
    .doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(60))  // 读取超时60秒
        .addHandlerLast(new WriteTimeoutHandler(30))  // 写入超时30秒
    );

WebClient webClient = WebClient.builder()
    .baseUrl(baseUrl)
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

六、嵌入向量(Embedding)

6.1 EmbeddingConfig配置详解

EmbeddingConfig 是项目中负责文本向量化的核心配置类,它使用 Apache HttpClient 5(而非 WebClient)来调用嵌入向量 API。

java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class EmbeddingConfig {

    private final ModelService userApiConfigService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostConstruct
    public void init() {
        try {
            ModelPO systemConfig = userApiConfigService.createEmbeddinService();
            log.info("系统Embedding模型配置已加载: apiType={}, model={}",
                     systemConfig.getApiType(), systemConfig.getEmbeddingModel());
        } catch (Exception e) {
            log.warn("系统Embedding模型配置未找到,请在数据库中创建配置记录");
        }
    }
}

为什么嵌入向量使用 Apache HttpClient 而非 WebClient?

这是一个有意为之的设计选择。嵌入向量请求通常是同步的、短生命周期的 HTTP 调用,不需要流式响应。Apache HttpClient 在同步场景下更直观,且与 Spring MVC 的同步编程模型更契合。

嵌入向量请求构建:

java
public List<List<Double>> embed(Long userId, List<String> texts) {
    ModelPO config = userApiConfigService.createEmbeddinService();
    String apiKey = config.getApiKey();
    String baseUrl = getBaseUrl(config);
    String model = getOrDefault(config.getEmbeddingModel(), "text-embedding-3-large");

    try (CloseableHttpClient client = HttpClients.createDefault()) {
        HttpPost post = new HttpPost(baseUrl + "/v1/embeddings");
        if (!apiKey.isEmpty()) {
            post.setHeader("Authorization", "Bearer " + apiKey);
        }
        post.setHeader("Content-Type", "application/json");

        // 构建请求体
        Map<String, Object> requestBody = Map.of(
            "model", model,
            "input", texts  // 支持批量文本
        );

        String json = objectMapper.writeValueAsString(requestBody);
        post.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));

        // 发送请求并解析响应
        try (CloseableHttpResponse response = client.execute(post)) {
            Map<String, Object> responseBody = objectMapper.readValue(
                response.getEntity().getContent(), Map.class);

            List<Map<String, Object>> data =
                (List<Map<String, Object>>) responseBody.get("data");

            return data.stream()
                .map(item -> (List<Double>) item.get("embedding"))
                .toList();
        }
    }
}

请求与响应格式:

请求:

json
{
  "model": "nomic-embed-text",
  "input": ["Spring AI 是一个强大的框架", "Ollama 让本地部署变得简单"]
}

响应:

json
{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "embedding": [0.0234, -0.0156, 0.0789, ...],
      "index": 0
    },
    {
      "object": "embedding",
      "embedding": [0.0456, -0.0321, 0.0654, ...],
      "index": 1
    }
  ],
  "model": "nomic-embed-text",
  "usage": {
    "prompt_tokens": 28,
    "total_tokens": 28
  }
}

6.2 文本向量化流程

文本向量化的完整流程如下:

原始文本

文本预处理(分句、清洗)

调用 EmbeddingConfig.embed()

构建HTTP请求(model + input)

发送到嵌入模型API(Ollama / OpenAI)

获取向量响应(List<List<Double>>)

向量后处理(归一化、降维等,可选)

存储到向量数据库

单个文本嵌入接口:

java
// Controller层
@PostMapping(value = "/embed/single", consumes = MediaType.TEXT_PLAIN_VALUE)
public BaseResult<?> embedSingle(@RequestParam("userId") Long userId,
                                  @RequestBody String text) {
    List<Double> embedding = embeddingModelProvider.embed(userId, List.of(text)).get(0);
    return BaseResult.success(embedding);
}

批量文本嵌入接口:

java
// Controller层
@PostMapping("/embed")
public BaseResult<?> embed(@RequestParam("userId") Long userId,
                            @RequestBody List<String> texts) {
    List<List<Double>> embeddings = embeddingModelProvider.embed(userId, texts);
    return BaseResult.success(embeddings);
}

6.3 向量存储与ChromaDB

嵌入向量生成后,需要存储到向量数据库中以支持高效的相似度搜索。ChromaDB 是一个轻量级的开源向量数据库,非常适合与 Ollama 本地部署搭配使用。

ChromaDB 部署:

bash
# Docker部署
docker run -d -p 8000:8000 chromadb/chroma

# 带持久化存储的部署
docker run -d -p 8000:8000 \
  -v /data/chroma:/chroma/chroma \
  chromadb/chroma

向量存储流程(伪代码):

java
// 1. 生成嵌入向量
List<Double> embedding = embeddingConfig.embed(userId, text);

// 2. 存储到ChromaDB
public void storeDocument(String collectionName, String docId,
                          String text, List<Double> embedding) {
    // 使用ChromaDB的HTTP API
    // POST /api/v1/collections/{collection_name}/add
    Map<String, Object> request = Map.of(
        "ids", List.of(docId),
        "embeddings", List.of(embedding),
        "documents", List.of(text)
    );
    // 发送HTTP请求...
}

ChromaDB 常用操作:

bash
# 创建集合
curl -X POST http://localhost:8000/api/v1/collections \
  -H "Content-Type: application/json" \
  -d '{"name": "documents", "get_or_create": true}'

# 添加文档
curl -X POST http://localhost:8000/api/v1/collections/documents/add \
  -H "Content-Type: application/json" \
  -d '{
    "ids": ["doc1", "doc2"],
    "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]],
    "documents": ["文档1内容", "文档2内容"]
  }'

# 相似度查询
curl -X POST http://localhost:8000/api/v1/collections/documents/query \
  -H "Content-Type: application/json" \
  -d '{
    "query_embeddings": [[0.1, 0.2, ...]],
    "n_results": 5
  }'

6.4 语义搜索应用

嵌入向量最典型的应用场景是语义搜索。与传统的关键词搜索不同,语义搜索能够理解查询意图,返回语义上最相关的内容。

语义搜索流程:

用户输入查询

将查询文本向量化

在向量数据库中进行相似度搜索

返回最相似的文档列表

(可选)将搜索结果作为上下文,调用LLM生成最终回答

RAG(检索增强生成)集成示例:

java
public String ragSearch(Long userId, String question) {
    // 1. 将问题向量化
    List<Double> questionEmbedding = embeddingConfig.embed(userId, question);

    // 2. 在向量数据库中搜索相关文档
    List<String> relevantDocs = chromaService.search(
        "knowledge_base", questionEmbedding, 5);

    // 3. 构建增强提示词
    String context = String.join("\n\n", relevantDocs);
    String enhancedPrompt = "基于以下参考资料回答问题:\n\n"
        + "参考资料:\n" + context + "\n\n"
        + "问题:" + question;

    // 4. 调用LLM生成回答
    return chatClientFactory.chat(userId, enhancedPrompt, false);
}

七、用户级模型配置管理

7.1 user_model表设计

user_model 表是用户级模型配置管理的核心数据结构,它存储在 smart_scaffold_1 数据库中。

完整的表结构:

sql
CREATE TABLE `user_model` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '模型ID,自增主键',
  `time_create` datetime NOT NULL COMMENT '创建时间',
  `time_update` datetime NOT NULL COMMENT '更新时间',
  `admin_id` varchar(20) DEFAULT NULL COMMENT '操作人ID',
  `admin_name` varchar(20) DEFAULT NULL COMMENT '操作人',
  `user_id` bigint NOT NULL COMMENT '用户ID (0表示系统配置)',
  `api_type` varchar(50) NOT NULL COMMENT 'AI提供商类型: OPENAI, OLLAMA等',
  `config_name` varchar(100) NOT NULL COMMENT '配置名称',
  `api_key` varchar(500) DEFAULT NULL COMMENT 'API密钥 (AES-256加密存储)',
  `base_url` varchar(500) DEFAULT NULL COMMENT 'API基础URL',
  `model_name` varchar(100) DEFAULT NULL COMMENT '模型名称',
  `temperature` decimal(3,2) DEFAULT '0.70' COMMENT '温度参数 (0.0-2.0)',
  `max_tokens` int DEFAULT '4096' COMMENT '最大Token数',
  `embedding_model` varchar(100) DEFAULT NULL COMMENT '嵌入模型名称',
  `is_default` tinyint(1) DEFAULT '0' COMMENT '是否为默认配置',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态: 1-禁用, 0-启用',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='用户大模型配置表';

字段设计说明:

字段类型说明设计考量
user_idbigint用户ID0 表示系统配置,大于 0 表示用户配置
api_typevarchar(50)AI提供商类型支持 OPENAI、OLLAMA、COMPATIBLE_OPENAI
config_namevarchar(100)配置名称用户自定义,如"我的GPT4配置"
api_keyvarchar(500)API密钥AES-256 加密存储,字段长度 500 足够容纳加密后的数据
base_urlvarchar(500)API基础URL支持自定义端点地址
model_namevarchar(100)模型名称如 gpt-4o、qwen2.5:7b-instruct-q4_k_m
temperaturedecimal(3,2)温度参数0.0-2.0,精度到小数点后两位
max_tokensint最大Token数控制响应长度
embedding_modelvarchar(100)嵌入模型名称如 nomic-embed-text、text-embedding-3-small
is_defaulttinyint(1)是否默认每个用户只能有一个默认配置
statustinyint(1)状态1-禁用,0-启用(注意这里的逻辑:false 表示启用)

示例数据:

sql
INSERT INTO `user_model` VALUES (
    1, '2026-03-21 11:25:51', '2026-03-21 11:25:51',
    '1', '必码', 1,
    'OLLAMA', 'Ollama配置', 'bima.cc',
    'http://192.168.1.99:11434', 'qwen2.5:7b-instruct-q4_k_m',
    0.70, 4096, '',
    1, 0, NULL
);

7.2 ModelService CRUD管理

ModelService 是模型配置的核心服务类,它负责从不同来源获取模型配置。

系统默认配置加载:

java
@Value("${bima.ai.default-api-type:OLLAMA}")
private String defaultApiType;

private ModelPO getSystemModel() {
    ModelPO modelPO = new ModelPO();
    switch (defaultApiType) {
        case "OPENAI":
            modelPO.setApiType("OPENAI");
            modelPO.setConfigName("OpenAI配置");
            modelPO.setApiKey(openaiApiKey);
            modelPO.setBaseUrl(openaiBaseUrl);
            modelPO.setModelName(openaiModelName);
            modelPO.setTemperature(new BigDecimal(openaiTemperature));
            modelPO.setMaxTokens(openaiMaxTokens);
            break;
        case "COMPATIBLE_OPENAI":
            modelPO.setApiType("COMPATIBLE_OPENAI");
            modelPO.setConfigName("兼容OpenAI配置");
            modelPO.setApiKey(compatibleOpenaiApiKey);
            modelPO.setBaseUrl(compatibleOpenaiBaseUrl);
            modelPO.setModelName(compatibleOpenaiModelName);
            modelPO.setTemperature(new BigDecimal(compatibleOpenaiTemperature));
            modelPO.setMaxTokens(compatibleOpenaiMaxTokens);
            break;
        case "OLLAMA":
        default:
            modelPO.setApiType("OLLAMA");
            modelPO.setConfigName("Ollama配置");
            modelPO.setApiKey(ollamaApiKey);
            modelPO.setBaseUrl(ollamaModelUrl);
            modelPO.setModelName(ollamaModelName);
            modelPO.setTemperature(new BigDecimal(ollamaTemperature));
            modelPO.setMaxTokens(ollamaMaxTokens);
            break;
    }
    modelPO.setEmbeddingModel(ollamaEmbeddingName);
    return modelPO;
}

用户自定义配置加载:

java
private ModelPO getUserModelById(Long userId) {
    ModelPO modelPO = new ModelPO();
    if (userId == null) {
        return modelPO;
    }

    UserModelQueryDTO queryDTO = new UserModelQueryDTO();
    queryDTO.setUserId(userId);
    queryDTO.setIsDefault(true);   // 查询用户的默认配置
    queryDTO.setStatus(false);      // 查询启用状态的配置

    List<UserModelDTO> userModelDTOs = userModelService.selectBy(queryDTO);
    if (userModelDTOs != null && !userModelDTOs.isEmpty()) {
        BeanUtils.copyProperties(userModelDTOs.get(0), modelPO);
    }
    modelPO.setEmbeddingModel(ollamaEmbeddingName);
    return modelPO;
}

CRUD 接口(通过 MybatisUserModelController 提供):

接口方法路径说明
列表查询(分页)POST/mybatis-usermodel/list/page分页查询用户模型配置
列表查询POST/mybatis-usermodel/list不分页查询
新增POST/mybatis-usermodel/add新增模型配置
编辑PUT/mybatis-usermodel/{id}/edit更新模型配置
详情GET/mybatis-usermodel/{id}/detail查看配置详情
删除DELETE/mybatis-usermodel/{id}/delete删除配置

7.3 动态切换模型

动态切换模型是系统灵活性的重要体现。用户可以在运行时切换不同的模型配置,无需重启服务。

切换流程:

用户在管理界面修改模型配置

调用 PUT /mybatis-usermodel/{id}/edit

MybatisUserModelService.save() 更新数据库

下次聊天请求时

ChatClientFactory.chat() → ModelService.createModelService()

根据 isCustom 参数决定使用系统配置还是用户配置

使用新的模型配置发送请求

配置优先级:

  1. 用户自定义配置(isCustom=true):最高优先级,使用数据库中用户的默认配置
  2. 系统默认配置(isCustom=false):使用 application.properties 中配置的默认提供商

这种设计允许系统管理员设置全局默认模型,同时允许每个用户根据自身需求配置不同的模型。例如,普通用户使用免费的 Ollama 本地模型,而高级用户可以使用自己的 OpenAI API Key 访问 GPT-4o。


八、写作辅助系统

8.1 WritingPromptService提示词管理

WritingPromptService 是一个提示词模板管理服务,它定义了多种预设的提示词场景,为 AI 写作辅助提供标准化的提示词模板。

预设提示词模板:

项目中内置了五种提示词模板,每种模板针对不同的使用场景进行了优化:

1. AI去味提示词(AI_DENOISING):

这是最具有创意性的提示词模板,用于将 AI 生成的文本改写得更像人类作家的手笔。它通过四个维度的指令来消除"AI味":

修改要求:
1. 去除AI痕迹:
   - 删除过于工整的排比句
   - 减少重复的修辞手法
   - 去掉刻意的对称结构
   - 避免机械式的总结陈词

2. 增加人性化:
   - 使用更口语化的表达
   - 添加不完美的细节
   - 保留适度的随意性
   - 增加真实的情感波动

3. 优化叙事:
   - 让节奏更自然不做作
   - 用简单词汇替换华丽辞藻
   - 保持叙述的松弛感
   - 让对话更生活化

4. 保持原意:
   - 不改变核心情节
   - 保留关键信息点
   - 维持角色性格
   - 确保逻辑连贯

2. 智能对话提示词(SMART_CHAT):

你是一个智能对话助手,能够理解用户的问题并提供准确、有用的回答。
回答要求:
1. 直接回答用户的问题,不要有任何引言或开场白
2. 回答要准确、简洁、有条理
3. 如果需要,可以提供具体的例子或步骤
4. 保持专业、友好的语气
5. 不要使用任何特殊符号或格式标记

3. 文本摘要提示词(TEXT_SUMMARIZATION):

你是一个专业的文本摘要工具,能够从长文本中提取核心信息并生成简洁的摘要。
摘要要求:
1. 提取文本的核心内容和关键信息
2. 保持原文的主要观点和重要细节
3. 语言简洁明了,避免冗余
4. 不要添加任何个人观点或评论
5. 摘要长度控制在原文的10-20%左右

4. 代码生成提示词(CODE_GENERATION):

你是一位专业的程序员,能够根据用户需求生成高质量的代码。
代码要求:
1. 代码要符合最佳实践和编码规范
2. 要有适当的注释和文档
3. 要考虑边界情况和错误处理
4. 代码结构清晰,易于理解和维护
5. 直接输出代码,不要有任何解释或说明

5. 数据分析提示词(DATA_ANALYSIS):

你是一位专业的数据分析师,能够分析数据并提供有价值的见解。
分析要求:
1. 对数据进行全面分析
2. 识别数据中的模式和趋势
3. 提供数据背后的洞察和建议
4. 分析要具体、深入,不要泛泛而谈
5. 保持专业、客观的态度

提示词场景配置:

每个模板都关联了一个 PromptScenario 对象,包含场景编码、名称、描述和示例参数:

java
@Data
@AllArgsConstructor
public static class PromptScenario {
    private String code;          // 场景编码
    private String name;          // 场景名称
    private String description;   // 场景描述
    private String templateName;  // 对应的模板名称
    private Map<String, String> parameters;  // 示例参数
}

8.2 WritingStyleService写作风格管理

WritingStyleService 是一个写作风格管理服务,它定义了六种精心设计的写作风格,每种风格都有详细的指令规则。

六种预设写作风格:

1. 自然沉浸(Natural & Immersive)

核心理念:祛除翻译腔,强调生活质感,像呼吸一样自然的叙事。

关键规则:

  • 严禁使用"一种...的感觉"、"随着..."、"与此同时"等连接词
  • 多用短句和"流水句",模拟人类视线的移动和思维的跳跃
  • 描写要聚焦在具体的、微小的生活细节(如杯子上的水渍、衣服的褶皱)
  • 不要写"他很生气",要写他"把烟头按灭在还没吃完的米饭里"

2. 古典雅致(Classical & Elegant)

核心理念:白话文与古典韵味的结合,强调留白与炼字。

关键规则:

  • 尽量使用双音节词或四字短语,但严禁堆砌辞藻
  • 注重句子的声调韵律,读起来要有金石之声或流水之韵
  • 少用现代的比喻,多用取自自然的比喻(如"如风过林")
  • 意在言外:不要把话说透,留三分余地

3. 冷硬现代(Modern & Hard-boiled)

核心理念:海明威式的冰山理论,节奏极快,零度情感。

关键规则:

  • 只写动作和对话,完全剔除心理描写和形容词堆砌
  • 句子要短、脆、硬,像手术刀一样切开场景
  • 删除所有废话,如果一个词删掉不影响理解,就删掉它
  • 多用名词和强动词,少用副词

4. 意识流(Stream of Consciousness)

核心理念:注重感官通感与内心独白,打破现实与幻想的边界。

关键规则:

  • 打通五感(如:听到了颜色的声音,闻到了悲伤的气味)
  • 绝对禁止直接出现"开心"、"痛苦"等抽象词汇
  • 必须寻找"客观对应物",将情绪投射到具体的景物上
  • 允许思维的非线性跳跃,模拟梦境或深层潜意识的逻辑

5. 白描速写(Sketch & Concise)

核心理念:只有骨架的叙事,强调绝对的精准和功能性。

关键规则:

  • 每一句话必须推动情节,或者揭示关键信息
  • 尽量使用简单的主谓宾结构,减少修饰语
  • 对话直接进入主题,去除寒暄和废话
  • 环境描写仅限于对情节有物理影响的物体

6. 感官特写(Sensory & Vivid)

核心理念:高分辨率的描写,强调材质、光影和微观细节。

关键规则:

  • 不要写大众化的细节(如蓝天白云),要写具有独特性的细节
  • 关注物体的质感:粗糙的、粘稠的、冰凉的、颗粒感的
  • 不要写静止的画面,要写光影的流变、灰尘的飞舞、肌肉的抽动
  • 禁止使用"映入眼帘"、"宛如画卷"等陈词滥调

风格应用机制:

java
public String applyStyleToPrompt(String basePrompt, WritingStyle style) {
    if (style == null) {
        return basePrompt;
    }
    return basePrompt + "\n\n" + style.getPromptContent()
        + "\n\n请直接输出章节正文内容,不要包含章节标题和其他说明文字。";
}

风格指令被追加到基础提示词的末尾,利用大语言模型"最后指令优先"的特性来确保风格指令被有效执行。

8.3 提示词工程实践

提示词工程(Prompt Engineering)是大模型应用开发中的核心技能。在 smart-scaffold-springboot 项目中,我们总结出了一套实用的提示词设计原则。

1. 角色设定原则

每个提示词都以明确的角色设定开头,这有助于模型理解其应该扮演的角色:

你是一位追求自然写作风格的编辑。
你是一个专业的文本摘要工具。
你是一位专业的程序员。
你是一位专业的数据分析师。

2. 结构化指令原则

使用编号列表来组织指令,使模型更容易理解和遵循:

修改要求:
1. 去除AI痕迹:
   - 删除过于工整的排比句
   - 减少重复的修辞手法
2. 增加人性化:
   - 使用更口语化的表达
   - 添加不完美的细节

3. 负面指令原则

除了告诉模型"应该做什么",更重要的是告诉模型"不应该做什么":

- 严禁使用"一种...的感觉"、"随着..."等连接词
- 绝对禁止直接出现"开心"、"痛苦"等抽象词汇
- 禁止使用"映入眼帘"、"宛如画卷"等陈词滥调

4. 示例驱动原则

在提示词中提供具体的示例,比抽象的描述更有效:

- 不要写"他很生气",要写他"把烟头按灭在还没吃完的米饭里"
- 不要写"他重重地关上门",写"他摔上了门"

5. 输出格式约束原则

在提示词末尾明确指定输出格式,避免模型添加不必要的解释:

请直接输出修改后的文本,无需解释。
请直接输出你的回答。
请直接输出代码。

8.4 AI写作流程设计

基于提示词模板和写作风格,项目设计了一套完整的 AI 写作流程。

流程架构:

用户选择写作场景(如:AI去味、文本摘要、代码生成)

选择写作风格(如:自然沉浸、古典雅致、冷硬现代)

输入内容参数(如:原始文本、问题、需求描述)

WritingPromptService 获取提示词模板

WritingStyleService 应用写作风格

组合最终提示词 = 基础提示词 + 风格指令

ChatClientFactory.chatStream() 流式生成内容

前端实时显示生成结果

API 调用示例:

bash
# 1. 获取可用的写作风格
GET /ai/prompt/styles

# 2. 获取可用的提示词场景
GET /ai/prompt/scenarios

# 3. 使用提示词模板生成内容
POST /ai/prompt/generate?userId=1&templateName=ai_denoising
Content-Type: application/json
{
  "original_text": "在一个阳光明媚的早晨..."
}

# 4. 应用写作风格
POST /ai/prompt/apply-style?userId=1&styleCode=natural
Content-Type: text/plain
你是一位追求自然写作风格的编辑...

# 5. 流式写作生成
POST /ai/writing/generate?userId=1
Content-Type: text/plain
(组合后的完整提示词)

九、Controller层设计

9.1 AIController接口总览

AIController 是 AI 功能的统一入口控制器,它将聊天、流式聊天、嵌入向量、写作辅助、提示词管理等功能整合在一个控制器中。

接口清单:

接口方法路径说明
同步聊天POST/ai/chat发送消息,等待完整响应
流式聊天POST/ai/chat/streamSSE 流式响应
批量聊天POST/ai/chat/batch批量发送消息
异步聊天POST/ai/chat/async异步处理聊天请求
批量嵌入POST/ai/embed批量文本向量化
单个嵌入POST/ai/embed/single单个文本向量化
系统配置GET/ai/config/system获取系统嵌入模型配置
模型名称GET/ai/model/name获取当前使用的模型类型
健康检查GET/ai/healthAI 服务健康检查
写作风格列表GET/ai/prompt/styles获取所有预设写作风格
写作风格详情GET/ai/prompt/styles/{styleCode}获取指定写作风格
提示词场景列表GET/ai/prompt/scenarios获取所有提示词场景
模板生成POST/ai/prompt/generate使用模板生成内容
应用风格POST/ai/prompt/apply-style应用写作风格到提示词
流式写作POST/ai/writing/generateSSE 流式写作生成

9.2 RESTful API设计规范

项目遵循 RESTful API 设计规范,以下是几个关键设计决策:

1. Content-Type 的灵活使用:

java
// 聊天接口接受纯文本
@PostMapping(value = "/chat", consumes = MediaType.TEXT_PLAIN_VALUE)

// 流式聊天接口接受JSON
@PostMapping(value = "/chat/stream",
             produces = MediaType.TEXT_EVENT_STREAM_VALUE,
             consumes = MediaType.APPLICATION_JSON_VALUE)

// 嵌入接口接受JSON数组
@PostMapping("/embed")

不同的接口根据其输入输出的特点选择不同的 Content-Type:

  • 简单的文本输入使用 TEXT_PLAIN
  • 结构化的请求体使用 APPLICATION_JSON
  • 流式响应使用 TEXT_EVENT_STREAM

2. 统一响应格式:

所有同步接口都使用 BaseResult<?> 作为返回类型,确保响应格式的一致性:

java
public class BaseResult<T> {
    private Integer code;    // 状态码
    private String message;  // 提示信息
    private T data;          // 数据内容
}

3. 用户身份传递:

java
// 优先从TokenRequestWrapper中获取userId
String userIdStr = request.getParameter(Constants.USER_ID_KEY);
Long userId;
if (userIdStr != null) {
    userId = Long.valueOf(userIdStr);
} else {
    // 如果没有,从requestBody中获取
    userId = Long.valueOf(requestBody.get("userId").toString());
}

用户身份通过两种方式传递:请求参数和请求体。优先从请求参数中获取(由 TokenRequestWrapper 统一设置),如果不存在则从请求体中获取。

9.3 错误处理机制

Controller 层错误处理:

java
@PostMapping(value = "/chat", consumes = MediaType.TEXT_PLAIN_VALUE)
public BaseResult<?> chat(@RequestParam("userId") Long userId,
                          @RequestBody String message) {
    try {
        String content = chatClientFactory.chat(userId, message, false);
        return BaseResult.success(content);
    } catch (Exception e) {
        log.error("AI聊天失败: {}", e.getMessage(), e);
        return BaseResult.fail("AI聊天失败: " + e.getMessage());
    }
}

流式接口的错误处理:

java
.onErrorResume(e -> {
    log.error("AI流式聊天失败: {}", e.getMessage(), e);
    String errorJson = "{\"type\":\"error\",\"error\":\""
        + e.getMessage().replace("\"", "\\\"") + "\"}";
    return Mono.just(
        ServerSentEvent.<String>builder()
            .event("error")
            .data(errorJson)
            .build()
    );
})

流式接口的错误处理与同步接口不同,它通过 SSE 事件将错误信息推送给客户端,而不是直接返回 HTTP 错误码。这是因为流式响应已经开始发送后,无法再修改 HTTP 状态码。

健康检查接口:

java
@GetMapping("/health")
public BaseResult<?> healthCheck() {
    return BaseResult.success(Map.of(
        "status", "healthy",
        "service", "ai-controller",
        "currentApiType", modelService.getCurrentApiType()
    ));
}

健康检查接口返回当前 AI 服务的状态信息,包括当前使用的 API 类型,便于运维监控。


十、安全与性能

10.1 API Key加密存储

API Key 是访问 AI 服务的凭证,一旦泄露可能造成严重的经济损失。在 smart-scaffold-springboot 项目中,API Key 的安全管理遵循以下原则:

数据库加密存储:

user_model 表的 api_key 字段采用 AES-256 加密算法存储。加密流程如下:

用户输入原始API Key: sk-proj-abc123...

AES-256加密(使用系统密钥)

密文: xK9mP2qR7vT...(Base64编码)

存储到 user_model.api_key

加密实现要点:

  1. 密钥管理:AES-256 密钥应存储在安全的密钥管理服务中(如 HashiCorp Vault),而非硬编码在配置文件中
  2. 初始化向量(IV):每次加密使用随机生成的 IV,确保相同明文加密后的密文不同
  3. 密钥轮换:定期轮换加密密钥,并对已有数据进行重新加密

传输安全:

java
// API Key仅在内存中以明文存在
private String apiKey = config.getApiKey();  // 从数据库解密后

// 通过HTTPS传输到AI服务提供商
webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION,
                               "Bearer " + apiKey);

API Key 的生命周期中,明文仅在以下两个短暂时刻存在:

  1. 从数据库读取并解密后,在 JVM 内存中
  2. 通过 HTTPS 发送到 AI 服务提供商时,在网络传输中

10.2 请求超时配置

合理的超时配置对于系统稳定性至关重要。不同的 AI 提供商和模型,其响应时间差异很大。

推荐的超时配置:

场景连接超时读取超时总超时
Ollama 本地(7B 模型)5s60s120s
Ollama 本地(33B 模型)5s120s300s
OpenAI API10s120s180s
兼容 OpenAI10s120s180s
嵌入向量5s30s60s

WebClient 超时配置示例:

java
import reactor.netty.http.client.HttpClient;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
    .responseTimeout(Duration.ofSeconds(120))
    .doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(60))
        .addHandlerLast(new WriteTimeoutHandler(30))
    );

WebClient webClient = WebClient.builder()
    .baseUrl(baseUrl)
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

10.3 并发控制

在高并发场景下,需要考虑以下并发控制策略:

1. Ollama 并发限制:

Ollama 默认同时处理一个请求(单模型加载到 GPU 时)。可以通过环境变量调整:

bash
OLLAMA_NUM_PARALLEL=4  # 同时处理4个请求

2. WebClient 连接池配置:

java
import reactor.netty.resources.ConnectionProvider;

ConnectionProvider provider = ConnectionProvider.builder("ai-connection-pool")
    .maxConnections(50)                    // 最大连接数
    .maxIdleTime(Duration.ofSeconds(30))   // 最大空闲时间
    .maxLifeTime(Duration.ofSeconds(300))  // 连接最大生命周期
    .pendingAcquireTimeout(Duration.ofSeconds(60))  // 获取连接超时
    .build();

WebClient webClient = WebClient.builder()
    .baseUrl(baseUrl)
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create(provider)))
    .build();

3. 信号量限流:

java
import reactor.core.publisher.Sinks;

// 使用Semaphore限制并发请求数
private final Semaphore semaphore = new Semaphore(10);

public String chatWithRateLimit(Long userId, String message, Boolean isCustom) {
    try {
        semaphore.acquire();
        return chatClientFactory.chat(userId, message, isCustom);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("请求被中断", e);
    } finally {
        semaphore.release();
    }
}

10.4 Token计数与限制

Token 计数是 AI 应用成本控制的重要手段。每次 API 调用都会消耗 Token,需要对其进行监控和限制。

Token 计数方式:

  1. API 响应中的 usage 字段:OpenAI API 的响应中包含 usage 字段,提供了精确的 Token 计数
  2. 估算公式:对于中文,大约 1 个 Token 对应 1-2 个汉字;对于英文,大约 1 个 Token 对应 0.75 个单词
  3. tiktoken 库:OpenAI 提供的 Token 计数库,可以精确计算不同模型的 Token 数量

max_tokens 参数的作用:

java
// 在请求体中设置max_tokens
"max_tokens", config.getMaxTokens() != null ? config.getMaxTokens() : 2000

max_tokens 参数限制的是模型生成的 Token 数量,不包括输入的 Token 数量。合理的设置可以:

  1. 控制成本:避免模型生成过长的响应,浪费 Token
  2. 控制延迟:较短的响应意味着更快的生成速度
  3. 防止失控:防止模型陷入无限循环或生成大量无意义的内容

项目中的默认配置:

properties
# Ollama默认max_tokens
bima.ai.ollama.max-tokens=4096

# OpenAI默认max_tokens
bima.ai.openai.max-tokens=4096

4096 是一个合理的默认值,对于大多数对话和写作场景来说足够。对于需要更长输出的场景(如长文写作),可以适当增大这个值。


十一、总结与展望

架构总结

本文详细解析了 smart-scaffold-springboot 项目中 AI 集成模块的完整架构设计和实现细节。核心要点如下:

1. 多提供商统一抽象

通过 ChatClientFactory 工厂类,实现了 Ollama、OpenAI、兼容 OpenAI 三种 AI 提供商的统一抽象。调用方无需关心底层使用的是哪种提供商,只需调用 chat()chatStream() 方法即可。

2. 配置驱动的灵活架构

通过 ModelServiceuser_model 表,实现了系统级和用户级的双层配置管理。系统管理员可以设置全局默认模型,每个用户也可以配置自己偏好的模型和参数。

3. SSE 流式响应的完整实现

基于 WebFlux + WebClient,实现了完整的 SSE 流式聊天功能,包括双协议响应解析(Ollama 格式和 OpenAI 格式)、前端 EventSource 对接、异常处理和连接管理。

4. 嵌入向量与语义搜索

通过 EmbeddingConfig 实现了文本向量化,为 RAG(检索增强生成)等高级应用场景奠定了基础。

5. 写作辅助系统

通过 WritingPromptServiceWritingStyleService,构建了一套完整的 AI 写作辅助系统,包含五种提示词模板和六种写作风格。

技术选型回顾

技术选型选择原因
HTTP 客户端(聊天)WebClient原生支持响应式流,适合 SSE 场景
HTTP 客户端(嵌入)Apache HttpClient 5同步场景更直观,与 Spring MVC 契合
流式协议SSE标准协议,浏览器原生支持
向量数据库ChromaDB轻量级,与 Ollama 本地部署搭配
加密算法AES-256行业标准,安全性高

未来展望

1. 多模态支持

当前系统仅支持文本输入输出。未来可以扩展支持图片、语音等多模态输入,利用 GPT-4V、Qwen-VL 等多模态模型的能力。

2. 对话上下文管理

当前的实现是无状态的(每次请求独立处理)。未来可以引入对话历史管理,支持多轮对话的上下文保持。

3. 智能路由

根据请求的复杂度自动选择合适的模型:简单问题使用本地小模型(快速、免费),复杂问题调用云端大模型(能力强、收费)。

4. 缓存层

对于高频的重复查询,引入语义缓存层。当新查询与缓存中的查询语义相似度超过阈值时,直接返回缓存结果。

5. 可观测性

集成 OpenTelemetry,对 AI 请求的延迟、Token 消耗、错误率等指标进行全面的监控和告警。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc