当前位置: 首页 > news >正文

通过引入大模型来处理图片文件

通过引入大模型来处理图片文件

功能需要,通过编写java代码,引入大模型,对图片文件就系识别,证明图片是否合规,这里只是把功能实现了

这里用的是rouyi的框架来写,核心代码如下:

controller层:

package com.ruoyi.web.controller.ai;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.langchain4j.LangChain4jUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.multipart.MultipartFile;import java.util.Map;/*** AI 聊天控制器* 提供简单的 AI 问答功能** @author sjl*/
@Api(tags = "AI聊天接口")
@RestController
@RequestMapping("/datalabel/ai")
public class AiChatController extends BaseController
{@Autowiredprivate LangChain4jUtils langChain4jUtils;/*** 图片分析接口(使用视觉模型)** @param file 上传的图片文件* @param question 用户问题(可选,默认为"请分析这张图片中的数据合规性")* @return AI 分析结果*/@ApiOperation("图片合规性分析")@PostMapping("/analyzeImage")public AjaxResult analyzeImage(@ApiParam("图片文件") @RequestParam("file") MultipartFile file,@ApiParam("用户问题(可选)") @RequestParam(value = "question", required = false, defaultValue = "请分析这张图片中的数据合规性") String question){java.io.File tempFile = null;try{// 检查视觉模型是否已配置if (!langChain4jUtils.isVisionModelConfigured()){return AjaxResult.error("视觉模型未配置,请检查 application.yml 中的 vision-model-name 配置");}// 验证文件if (file.isEmpty()){return AjaxResult.error("图片文件不能为空");}// 验证文件类型String contentType = file.getContentType();if (contentType == null || !contentType.startsWith("image/")){return AjaxResult.error("只支持图片文件(jpg、png、gif等)");}// 1. 创建临时文件String originalFilename = file.getOriginalFilename();String suffix = originalFilename != null ? originalFilename.substring(originalFilename.lastIndexOf(".")) : ".jpg";tempFile = java.io.File.createTempFile("upload_", suffix);// 2. 将 MultipartFile 写入临时文件
            file.transferTo(tempFile);// 3. 调用视觉模型分析图片(传入文件绝对路径)String result = langChain4jUtils.analyzeImage(question, tempFile.getAbsolutePath());return AjaxResult.success("图片分析成功", result);}catch (Exception e){logger.error("图片分析失败", e);return AjaxResult.error("图片分析失败:" + e.getMessage());}finally{// 4. 删除临时文件if (tempFile != null && tempFile.exists()){tempFile.delete();}}}}

 

 

LangChain4jUtils类:
package com.ruoyi.common.utils.langchain4j;import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.service.AiServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;import java.util.Iterator;/*** LangChain4j 工具类* 提供便捷的 AI 服务调用方法** @author ruoyi*/
@Component
public class LangChain4jUtils
{@Autowired(required = false)@Qualifier("textChatModel")private ChatModel chatModel;@Autowired(required = false)@Qualifier("visionChatModel")private ChatModel visionChatModel;@Autowired(required = false)private EmbeddingModel embeddingModel;protected final Logger logger = LoggerFactory.getLogger(this.getClass());private final ObjectMapper mapper = new ObjectMapper();/*** 分析图片内容(GPU 环境下使用 )*/public String analyzeImage(String userQuestion, String imagePath){if (visionChatModel == null){return "视觉模型未配置";}// 构建专业的合规审查系统提示词//    要求模型://    - 识别图片中的文字、表格、图表//    - 依据三法一条例进行合规性审查//    - 只输出 JSON,禁止英文,必须使用中文//    - 如果无文字,则在结论中写明//    - 输出格式为 {"is_compliant":"合规/不合规","conclusion":"详细分析"}// 专业的合规审查提示词String systemPrompt = "你是一名资深的数据安全合规专家。请仔细识别图片中的所有文字、表格及图表信息。\n\n" +"依据《中华人民共和国网络安全法》、《数据安全法》、《个人信息保护法》及《关键信息基础设施安全保护条例》,对图片内容进行严格的合规性审查。\n\n" +"审查重点:\n" +"1. 是否存在违规收集个人信息?\n" +"2. 数据出境表述是否合规?\n" +"3. 是否有未授权的数据共享或交易?\n\n" +"输出要求(严格遵循):\n" +"1. 仅输出一个标准的 JSON 对象,不要包含 Markdown 格式或其他任何额外文字。\n" +"2. **所有文字必须使用中文,禁止出现任何英文单词、字母或标点。**\n" +"3. 如果图片中不包含任何文字信息,则在结论中明确说明“图片中未识别到文字”。\n" +"JSON 格式:\n" +"{\"is_compliant\": \"合规/不合规\", \"conclusion\": \"详细的法律依据和违规点分析\"}";try {//根据图片路径生成 file:// URIjava.io.File file = new java.io.File(imagePath);String fileUri = file.toURI().toString();//  调用视觉模型进行多模态对话//  参数:系统消息(合规专家角色)、用户消息(图片 + 用户问题)AiMessage aiMessage = visionChatModel.chat(SystemMessage.systemMessage(systemPrompt),UserMessage.from(dev.langchain4j.data.message.ImageContent.from(fileUri), // 图片内容dev.langchain4j.data.message.TextContent.from(userQuestion)  //用户问题
                    )).aiMessage();// 获取模型返回的原始文本(预期是一个 JSON 字符串)String raw = aiMessage.text();logger.info("原始返回结果: {}", raw);// 目标:仅删除 JSON 各字段值中的英文字母,保留键名(is_compliant、conclusion)和中文字符try {// 使用 Jackson 将原始 JSON 字符串解析为树模型JsonNode rootNode = mapper.readTree(raw);//递归遍历 JSON 树,清理所有文本节点中的英文字母
                cleanJsonValues(rootNode);//将清理后的树重新序列化为字符串String cleaned = mapper.writeValueAsString(rootNode);logger.info("清洗后结果: {}", cleaned);return cleaned;} catch (Exception e) {logger.warn("JSON 解析失败,返回原始结果: {}", raw);return raw;}} catch (Exception e) {logger.error("图片分析异常", e);return "图片分析失败: " + e.getMessage();}}// 辅助方法:递归清理所有文本节点的英文private void cleanJsonValues(JsonNode node) {// 处理 JSON 对象:遍历所有字段,对每个字段的值递归清理if (node.isObject()) {// 获取所有字段名的迭代器Iterator<String> fieldNames = node.fieldNames();while (fieldNames.hasNext()) {String fieldName = fieldNames.next(); // 当前字段名(如 "conclusion")JsonNode child = node.get(fieldName); // 当前字段对应的值节点if (child.isTextual()) {// 如果值是文本类型,则去掉所有英文字母(保留中文、数字、标点等)String cleanedText = child.asText().replaceAll("[a-zA-Z]+", "");// 将清理后的文本写回原节点
                    ((com.fasterxml.jackson.databind.node.ObjectNode) node).put(fieldName, cleanedText);} else {// 如果值不是文本(例如嵌套对象、数组),则递归处理
                    cleanJsonValues(child);}}} else if (node.isArray()) {for (JsonNode item : node) {cleanJsonValues(item);}} // 其他类型(数字、布尔)不做处理
    }}

 

 

LangChain4jConfig工具类

package com.ruoyi.framework.config;import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.ollama.OllamaChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.time.Duration;/*** LangChain4j 配置类* 配置 Ollama 本地大模型服务* 由于 Spring Boot 2.x 不支持 langchain4j-spring-boot-starter,* 因此需要手动配置 Bean** @author ruoyi*/
@Configuration
public class LangChain4jConfig
{@Value("${langchain4j.ollama.base-url:http://localhost:11434}")private String ollamaBaseUrl;@Value("${langchain4j.ollama.model-name:qwen3:0.6b}")private String ollamaModelName;@Value("${langchain4j.ollama.vision-model-name:qwen2.5vl:7b}")private String ollamaVisionModelName;@Value("${langchain4j.ollama.temperature:0.7}")private Double temperature;@Value("${langchain4j.ollama.timeout:60}")private Integer timeout;/*** 配置 Ollama Chat Model* 使用 qwen3:0.6B 模型*/@Bean("textChatModel")public ChatModel chatLanguageModel(){return OllamaChatModel.builder().baseUrl(ollamaBaseUrl).modelName(ollamaModelName).temperature(temperature).timeout(Duration.ofSeconds(timeout)).logRequests(true).logResponses(true).responseFormat(ResponseFormat.JSON).build();}/*** 配置视觉 Chat Model(用于图片识别)*/@Bean("visionChatModel")public ChatModel visionChatLanguageModel(){return OllamaChatModel.builder().baseUrl(ollamaBaseUrl).modelName(ollamaVisionModelName).temperature(0.0).timeout(Duration.ofSeconds(timeout)).logRequests(true).logResponses(true).responseFormat(ResponseFormat.JSON).topK(1)                         // 新增:只选最高概率 token.seed(42)                        // 新增:固定随机种子
                .build();}
}

 

 

配置文件需要把相应的大模型配置上:

# LangChain4j 配置 - Ollama
langchain4j:ollama:# Ollama 服务地址(默认本地 11434 端口)base-url: http://172.16.120.3:11434# 模型名称(使用 qwen3:0.6B 模型)model-name: qwen3:0.6b# 视觉模型名称(用于图片识别)vision-model-name: llava:7b# 温度参数(0.0-2.0,控制输出的随机性)temperature: 0.1# 超时时间(秒)timeout: 12000

 

 

这次任务,主要是在服务器搭建适配的大模型上比较多问题,代码方面其实问题不大:

一、背景与初始配置

  • 硬件:离线服务器,6 张 NVIDIA Tesla P100(Pascal 架构,计算能力 6.0),驱动版本 575.57.08,CUDA 12.9。
  • 软件:Docker 运行 Ollama 0.14.0,部署了 llava:7bbakllava:7bqwen2.5vl:7b 等多模态模型。
  • 任务:通过 Java(LangChain4j)调用多模态模型识别图片中的文字,依据「三法一条例」进行数据合规审查,输出 JSON 格式的合规结论(纯中文)。

二、第一阶段:模型崩溃(panic/NaN)

问题现象

  • qwen2.5vl:7b → panic: failed to sample token: logits sum to NaN
  • llava:7bbakllava:7b → model runner has unexpectedly stopped(后期日志显示 EOF,无 panic)

尝试与结果

操作结果
关闭 Flash Attention,限制 num_parallel=1 失败
禁用 mmap 失败
增加 shm_size 和 mem_limit 失败
恢复 CUDA_VISIBLE_DEVICES=0 失败
使用纯文本模型 qwen3:0.6b ✅ 成功(证明 Ollama + CUDA 基础可用)
 

结论:问题不在于 Ollama 配置,而是 Pascal 架构的 CUDA kernel 对多模态模型的支持存在系统性 bug。Qwen2.5VL、llava、bakllava 均崩溃,唯独纯文本模型一切正常。

 

三、第二阶段:降低 Ollama 版本尝试

操作

  • 降级到 0.9.2、0.7.0
  • 模型切换为 llava:7bbakllava:7b

结果

  • 仍然崩溃(qwen2.5vl 在 0.9.2 上也是 NaN panic)
  • 0.9.2 甚至无法加载多模态模型(子模块不完整)

结论:降低版本解决不了问题,因为底层 llama.cpp 对 Pascal 的兼容缺陷在所有版本中都存在。

 

四、第三阶段:自编译针对 sm_60 的 CUDA 库

这个阶段需要编译,因为我们通常使用的服务器都是比较老旧的系统,这个时候,我们可以通过docker容器进行编译,这样就能避免老系统的各种依赖环境的困扰了

克隆 Ollama v0.14.0
git clone --branch v0.14.0 --depth 1 https://github.com/ollama/ollama.git
cd ollama

 

获取 llama.cpp 的精确 Commit

Ollama v0.14.0 的 llama/ 子模块(当时是目录而非子模块)对应 llama.cpp 的某一次提交。我们通过 Ollama 仓库中的 .gitmodules 和 Git 读取锁定 hash。

若当前目录下已有 llama/ 子目录(Ollama 自带占位),但为了编译干净,我们不使用它。改用从 upstream 直接克隆。精确 commit 可以通过以下命令从 Ollama 仓库获取:

# 方法1(如果子模块初始化过)
git submodule status llama
# 方法2(直接读取索引)
git ls-tree HEAD llama

 

 

Docker 编译环境

docker pull nvidia/cuda:12.4.1-devel-ubuntu22.04

 

docker run --rm -v /opt/softwares/ollama/ollama:/workspace \nvidia/cuda:12.4.1-devel-ubuntu22.04 \bash -c '
    set -eapt-get update && apt-get install -y cmake gitcd /workspace/llama# 应用补丁(如果 patches 目录存在)if [ -d patches ]; thenfor patch in patches/*.patch; doecho "Applying $patch"git apply "$patch" 2>/dev/null || truedonefi# 编译mkdir -p build && cd buildcmake .. \-DGGML_CUDA=ON \-DCMAKE_CUDA_ARCHITECTURES="60" \-DGGML_CUDA_FA_TYPES=OFF \-DBUILD_SHARED_LIBS=ON \-DGGML_BUILD_TESTS=OFF \-DGGML_BUILD_EXAMPLES=OFF \-DCMAKE_BUILD_TYPE=Releasemake -j$(nproc) ggml-cudaecho "Build completed!"'

 

 

编译完成后,把/opt/softwares/ollama/ollama/llama/build/bin目录下的libggml-cuda.so文件传输到GPU服务器上

libggml-cuda.so文件在具体哪个目录在编译完成后的日志最后能看到

# 备份原始文件
docker cp ollama:/usr/lib/ollama/cuda_v12/libggml-cuda.so /tmp/libggml-cuda.so.bak
# 替换为新文件(注意:这里要使用从联网机器传过来的真实文件路径)
docker cp /path/to/libggml-cuda.so ollama:/usr/lib/ollama/cuda_v12/libggml-cuda.so
# 设置权限
docker exec -it ollama chmod 644 /usr/lib/ollama/cuda_v12/libggml-cuda.so
# 重启 Ollama
docker-compose restart

 

 

我在GPU服务器上面的docker-compose文件如下

services:ollama:image: ollama/ollama:0.14.0-vulkanrestart: alwayscontainer_name: ollamaentrypoint: ["/usr/bin/ollama"]          # 覆盖为原始 entrypointcommand: ["serve"]                       # 启动 Ollama 服务mem_limit: 64gshm_size: 16g# GPU 支持配置deploy:resources:reservations:devices:- driver: nvidiadevice_ids: ["0"]capabilities: [gpu]# 解决 pthread_create failed 错误security_opt:- seccomp:unconfinedvolumes:- ./data:/root/.ollama  # 模型数据持久化environment:- OLLAMA_HOST=0.0.0.0- OLLAMA_KEEP_ALIVE=5m- OLLAMA_MAX_LOADED_MODELS=1- OLLAMA_VULKAN=1          # 启用Vulkan- OLLAMA_FLASH_ATTENTION=false- OLLAMA_NUM_PARALLEL=1- OLLAMA_MMAP=1- LANG=C.UTF-8- LC_ALL=C.UTF-8ports:- "11434:11434"# 确保容器有足够的系统资源ulimits:nproc: 65535nofile:soft: 65535hard: 65535

 

 

经过这个阶段,调用我sping boot的接口,得出的结论

结果

  • ❌ 模型不再崩溃(logits 正常,推理完成)
  • ❌ 输出中英文混杂(如 “wearing dragon costumes” 出现在中文结论中)
  • 模型能运行,但语言模型输出不稳定,倾向于使用训练集中的英文

结论:编译修复了崩溃,但无法解决 Pascal 架构上多模态模型固有的数值精度问题(输出语言混杂)。

 

五、第四阶段:尝试 Vulkan 后端

绕过 CUDA kernel 缺陷,使用 Vulkan 后端,可能获得更稳定的输出。

操作

  • 在容器内安装 libvulkan1 mesa-vulkan-drivers
  • 将容器提交为新镜像(ollama/ollama:0.14.0-vulkan
  • 设置 OLLAMA_VULKAN=1,启动新容器

我这里是通过把离线服务器的容器打包成镜像,传输到虚拟机去下载的

# 加载镜像
docker load -i ollama-base.tar
# 启动一个临时容器,安装 Vulkan 库
docker run --rm -it --name ollama-temp --entrypoint bash ollama-base
# 进入容器后执行:
apt-get update
apt-get install -y libvulkan1 mesa-vulkan-drivers
# 安装完成后,不要退出,打开另一个终端
# 在另一个终端中,将当前运行的容器提交为新镜像
docker commit ollama-temp ollama-vulkan-ready
# 然后退出临时容器
exit

 

 

重新在GPU服务器运行新的容器后,效果也不理想

image

 

宿主机上确实没有 nvidia_icd.json,只有 libnvidia-glvkspirv.so,这说明系统的 NVIDIA 驱动未正确安装 Vulkan ICD 组件。因此,Vulkan 后端在容器内无法与 Tesla P100 通信,Ollama 的 Vulkan 支持会初始化失败。

结论: 在当前的机器上,无论 CUDA 还是 Vulkan,都无法让多模态模型稳定输出纯净中文。这是 Pascal 架构(P100)的硬件限制,而非配置不当。

 

六、最终判断:Pascal 架构的硬件限制

经过所有尝试,得出结论:

  • Pascal 架构(P100)的多模态 CUDA kernel 存在缺陷,无法通过软件配置或自编译解决。
  • 所有多模态模型在 P100 上都会出现输出不稳定、中英文混杂的情况
  • 继续折腾 CUDA/Vulkan 不会有本质改进

 

七、最终解决方案:后处理清洗模型输出

不再花费精力去改变模型行为,而是对模型输出的 JSON 结果进行清洗,只保留中文字符,删除所有英文字母。

private void cleanJsonValues(JsonNode node) {if (node.isObject()) {Iterator<String> fieldNames = node.fieldNames();while (fieldNames.hasNext()) {String fieldName = fieldNames.next();JsonNode child = node.get(fieldName);if (child.isTextual()) {// 仅删除文本值中的英文字母,保留键名String cleanedText = child.asText().replaceAll("[a-zA-Z]+", "");((ObjectNode) node).put(fieldName, cleanedText);} else {cleanJsonValues(child);}}} else if (node.isArray()) {for (JsonNode item : node) {cleanJsonValues(item);}}
}

效果

  • 键名 is_compliantconclusion 完整保留
  • 所有英文词汇(如 “wearing dragon costumes”)被删除
  • 中文分析结论保持完整
  • JSON 结构完整无损

当前运行状态

  • 模型使用 CPU 推理(视觉编码器 + 语言模型均在 CPU)
  • 速度较慢(单张图片约 10-20 秒),但对于合规审查任务可接受
  • 输出为纯净中文 JSON

 

八、技术栈总结

组件最终决策原因
Ollama 版本 0.14.0(CUDA + CPU 混合) 0.9.2 等版本同样不行
多模态模型 llava:7b(Q4_K_M) 能工作,输出结构完整
后端 CPU 推理(CUDA 编译后仍不稳定) Pascal 硬件限制
后处理 Jackson 解析 JSON,删除英文 成本最低,立即可用
未来提升方向 更换 GPU(V100 或更高)或采用 OCR + 纯文本模型 突破硬件限制