Appearance
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 的默认实现并不总是能满足生产环境的全部需求。特别是在以下场景中:
- 流式响应的精细化控制:Spring AI 默认的流式实现可能无法满足自定义 SSE 事件格式的需求
- 多协议适配:当需要同时支持 Ollama 原生协议和 OpenAI 兼容协议时,需要更灵活的适配层
- 用户级模型配置:生产环境中通常需要支持每个用户配置自己的模型参数,这超出了 Spring AI 默认配置的范围
因此,在 smart-scaffold-springboot 项目中,我们采用了一种"借鉴 Spring AI 设计理念,但自主实现核心逻辑"的策略。通过自研的 ChatClientFactory、EmbeddingConfig、ModelService 等组件,构建了一套既灵活又可控的 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功能控制器核心设计原则:
单一职责:每个类只负责一个明确的功能领域。
ChatClientFactory负责构建和发送聊天请求,ModelService负责模型配置的获取和管理,EmbeddingConfig专门处理嵌入向量相关的逻辑。依赖倒置:
ChatClientFactory不直接依赖具体的模型配置来源,而是通过ModelService接口获取配置。这意味着配置可以从数据库、配置文件或任何其他来源加载,而不需要修改ChatClientFactory的代码。开闭原则:通过
apiType字段区分不同的 AI 提供商,新增提供商类型时只需要扩展getApiPath()和buildRequestBody()方法中的分支逻辑,而不需要修改已有的代码路径。值对象继承:
UserModel实体继承自ModelPO,ModelPO封装了所有 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) { ... }
}设计亮点:
统一入口:无论使用哪种 AI 提供商,调用方只需要调用
chat()或chatStream()方法,传入 userId、message 和 isCustom 参数即可。提供商的切换对调用方完全透明。配置驱动:所有的模型配置信息(API 类型、基础 URL、模型名称、温度参数等)都封装在
ModelPO对象中,由ModelService统一管理。ChatClientFactory本身不持有任何配置状态。同步/异步双模式:提供
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?
原生支持响应式流:
WebClient天然支持Flux和Mono,可以无缝处理 SSE 流式响应。而RestTemplate是同步阻塞的,处理流式响应需要额外的复杂性。非阻塞 I/O:在高并发场景下,
WebClient基于 Netty 的非阻塞 I/O 模型可以显著提升吞吐量,减少线程占用。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);
}响应解析的三层兜底策略:
代码中对响应格式的解析采用了三层兜底策略,确保最大程度的兼容性:
- 第一层:检查
response字段(Ollama 原生格式) - 第二层:检查
choices字段(OpenAI 标准格式) - 第三层:检查
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 自定义模型切换机制
ChatClientFactory 的 chat() 和 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/ollamasystemd 服务管理(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-text3.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_m | 7B | Q4_K_M | 中文能力强,推理速度快 | 通用对话、中文场景 |
| llama3:8b | 8B | Q4_0 | Meta出品,英文能力强 | 英文对话、代码生成 |
| chatglm3:6b | 6B | Q4_0 | 清华出品,中英双语 | 中英混合场景 |
| deepseek-r1:7b | 7B | Q4_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 GB | 8 GB | 8 GB | 4核 |
| 7B (Q4) | 6 GB | 12 GB | 16 GB | 8核 |
| 7B (Q8) | 10 GB | 16 GB | 16 GB | 8核 |
| 13B (Q4) | 10 GB | 16 GB | 32 GB | 8核 |
| 33B (Q4) | 20 GB | 24 GB | 64 GB | 16核 |
性能优化策略:
模型量化选择:对于大多数场景,Q4_K_M 量化是最佳选择。它在模型精度和推理速度之间取得了很好的平衡。只有在需要更高精度的场景(如专业代码生成、数学推理)中,才需要使用 Q8 或 F16。
GPU 加速:确保安装了正确的 GPU 驱动和 CUDA 工具包:
bash
# 检查NVIDIA驱动
nvidia-smi
# 检查CUDA版本
nvcc --version- 并发控制:通过环境变量控制并发请求数和最大加载模型数:
bash
OLLAMA_NUM_PARALLEL=4 # 同时处理4个请求
OLLAMA_MAX_LOADED_MODELS=2 # 最多同时加载2个模型到显存- 模型预热:在系统启动时预先加载模型,避免首次请求的延迟:
bash
# 在systemd服务启动后执行预热
ollama run qwen2.5:7b-instruct-q4_k_m "你好" &- 内存映射优化:将模型文件放在高速存储设备上(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=OPENAIOpenAI 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.com | gpt-4o |
| DeepSeek | https://api.deepseek.com | deepseek-chat |
| 通义千问 | https://dashscope.aliyuncs.com/compatible-mode | qwen-turbo |
| 智谱 AI | https://open.bigmodel.cn/api/paas | glm-4 |
| 月之暗面 | https://api.moonshot.cn | moonshot-v1-8k |
这种设计使得用户只需要修改 base-url 和 model-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),在系统设计中需要考虑这些限制:
| 提供商 | 免费层限制 | 付费层限制 | 限制维度 |
|---|---|---|---|
| OpenAI | 20 RPM / 40K TPM | 5000 RPM / 2M TPM | RPM + TPM |
| DeepSeek | 60 RPM | 300 RPM | RPM |
| Ollama 本地 | 无限制 | 无限制 | 仅受硬件限制 |
项目中的频率控制策略:
- 批量请求延迟:在批量聊天接口中,通过
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();- 异步处理:对于耗时较长的请求,提供异步接口:
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()));
}- 前端限流:通过前端 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 调用,同时使用 Flux 和 ServerSentEvent 来实现 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 操作链解析:
bodyToFlux(String.class):将 HTTP 响应体映射为Flux<String>,每一行响应数据都会触发一次 onNext 事件.map():对每一行数据进行转换,移除 SSE 协议中的data:前缀.filter(line -> line != null):过滤掉空行.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]关键差异:
| 特性 | Ollama | OpenAI |
|---|---|---|
| 内容字段 | response | choices[0].delta.content |
| 结束标记 | done: true | data: [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_id | bigint | 用户ID | 0 表示系统配置,大于 0 表示用户配置 |
api_type | varchar(50) | AI提供商类型 | 支持 OPENAI、OLLAMA、COMPATIBLE_OPENAI |
config_name | varchar(100) | 配置名称 | 用户自定义,如"我的GPT4配置" |
api_key | varchar(500) | API密钥 | AES-256 加密存储,字段长度 500 足够容纳加密后的数据 |
base_url | varchar(500) | API基础URL | 支持自定义端点地址 |
model_name | varchar(100) | 模型名称 | 如 gpt-4o、qwen2.5:7b-instruct-q4_k_m |
temperature | decimal(3,2) | 温度参数 | 0.0-2.0,精度到小数点后两位 |
max_tokens | int | 最大Token数 | 控制响应长度 |
embedding_model | varchar(100) | 嵌入模型名称 | 如 nomic-embed-text、text-embedding-3-small |
is_default | tinyint(1) | 是否默认 | 每个用户只能有一个默认配置 |
status | tinyint(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 参数决定使用系统配置还是用户配置
↓
使用新的模型配置发送请求配置优先级:
- 用户自定义配置(isCustom=true):最高优先级,使用数据库中用户的默认配置
- 系统默认配置(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/stream | SSE 流式响应 |
| 批量聊天 | 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/health | AI 服务健康检查 |
| 写作风格列表 | 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/generate | SSE 流式写作生成 |
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加密实现要点:
- 密钥管理:AES-256 密钥应存储在安全的密钥管理服务中(如 HashiCorp Vault),而非硬编码在配置文件中
- 初始化向量(IV):每次加密使用随机生成的 IV,确保相同明文加密后的密文不同
- 密钥轮换:定期轮换加密密钥,并对已有数据进行重新加密
传输安全:
java
// API Key仅在内存中以明文存在
private String apiKey = config.getApiKey(); // 从数据库解密后
// 通过HTTPS传输到AI服务提供商
webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION,
"Bearer " + apiKey);API Key 的生命周期中,明文仅在以下两个短暂时刻存在:
- 从数据库读取并解密后,在 JVM 内存中
- 通过 HTTPS 发送到 AI 服务提供商时,在网络传输中
10.2 请求超时配置
合理的超时配置对于系统稳定性至关重要。不同的 AI 提供商和模型,其响应时间差异很大。
推荐的超时配置:
| 场景 | 连接超时 | 读取超时 | 总超时 |
|---|---|---|---|
| Ollama 本地(7B 模型) | 5s | 60s | 120s |
| Ollama 本地(33B 模型) | 5s | 120s | 300s |
| OpenAI API | 10s | 120s | 180s |
| 兼容 OpenAI | 10s | 120s | 180s |
| 嵌入向量 | 5s | 30s | 60s |
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 计数方式:
- API 响应中的 usage 字段:OpenAI API 的响应中包含
usage字段,提供了精确的 Token 计数 - 估算公式:对于中文,大约 1 个 Token 对应 1-2 个汉字;对于英文,大约 1 个 Token 对应 0.75 个单词
- tiktoken 库:OpenAI 提供的 Token 计数库,可以精确计算不同模型的 Token 数量
max_tokens 参数的作用:
java
// 在请求体中设置max_tokens
"max_tokens", config.getMaxTokens() != null ? config.getMaxTokens() : 2000max_tokens 参数限制的是模型生成的 Token 数量,不包括输入的 Token 数量。合理的设置可以:
- 控制成本:避免模型生成过长的响应,浪费 Token
- 控制延迟:较短的响应意味着更快的生成速度
- 防止失控:防止模型陷入无限循环或生成大量无意义的内容
项目中的默认配置:
properties
# Ollama默认max_tokens
bima.ai.ollama.max-tokens=4096
# OpenAI默认max_tokens
bima.ai.openai.max-tokens=40964096 是一个合理的默认值,对于大多数对话和写作场景来说足够。对于需要更长输出的场景(如长文写作),可以适当增大这个值。
十一、总结与展望
架构总结
本文详细解析了 smart-scaffold-springboot 项目中 AI 集成模块的完整架构设计和实现细节。核心要点如下:
1. 多提供商统一抽象
通过 ChatClientFactory 工厂类,实现了 Ollama、OpenAI、兼容 OpenAI 三种 AI 提供商的统一抽象。调用方无需关心底层使用的是哪种提供商,只需调用 chat() 或 chatStream() 方法即可。
2. 配置驱动的灵活架构
通过 ModelService 和 user_model 表,实现了系统级和用户级的双层配置管理。系统管理员可以设置全局默认模型,每个用户也可以配置自己偏好的模型和参数。
3. SSE 流式响应的完整实现
基于 WebFlux + WebClient,实现了完整的 SSE 流式聊天功能,包括双协议响应解析(Ollama 格式和 OpenAI 格式)、前端 EventSource 对接、异常处理和连接管理。
4. 嵌入向量与语义搜索
通过 EmbeddingConfig 实现了文本向量化,为 RAG(检索增强生成)等高级应用场景奠定了基础。
5. 写作辅助系统
通过 WritingPromptService 和 WritingStyleService,构建了一套完整的 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。