通过引入大模型来处理图片文件
功能需要,通过编写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 { @Autowired private 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:// URI java.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:7b、bakllava:7b、qwen2.5vl:7b等多模态模型。 - 任务:通过 Java(LangChain4j)调用多模态模型识别图片中的文字,依据「三法一条例」进行数据合规审查,输出 JSON 格式的合规结论(纯中文)。
二、第一阶段:模型崩溃(panic/NaN)
问题现象
qwen2.5vl:7b→panic: failed to sample token: logits sum to NaNllava:7b、bakllava: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:7b、bakllava: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 -e apt-get update && apt-get install -y cmake git cd /workspace/llama # 应用补丁(如果 patches 目录存在) if [ -d patches ]; then for patch in patches/*.patch; do echo "Applying $patch" git apply "$patch" 2>/dev/null || true done fi # 编译 mkdir -p build && cd build cmake .. \ -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=Release make -j$(nproc) ggml-cuda echo "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-vulkan restart: always container_name: ollama entrypoint: ["/usr/bin/ollama"] # 覆盖为原始 entrypoint command: ["serve"] # 启动 Ollama 服务 mem_limit: 64g shm_size: 16g # GPU 支持配置 deploy: resources: reservations: devices: - driver: nvidia device_ids: ["0"] capabilities: [gpu] # 解决 pthread_create failed 错误 security_opt: - seccomp:unconfined volumes: - ./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-8 ports: - "11434:11434" # 确保容器有足够的系统资源 ulimits: nproc: 65535 nofile: soft: 65535 hard: 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服务器运行新的容器后,效果也不理想

宿主机上确实没有 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_compliant、conclusion完整保留 - 所有英文词汇(如 “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 + 纯文本模型 | 突破硬件限制 |
浙公网安备 33010602011771号