基于 Spring Boot +Vue+ 通义千问的通用 AI 图像识别引擎设计与实现

基于 Spring Boot +Vue+ 通义千问的通用 AI 图像识别引擎设计与实现

作者:HUI4412

发布时间:2026-02-28

技术栈:Spring Boot | RestTemplate | 通义千问 VL | 多模态 AI


📖 前言

数字化转型卷到飞起的当下,每个开发者都在琢磨:咋用 AI 把打工人从重复劳动里解放出来?最近我就整了个实用活儿 ------上传专利证书图片,AI 自动扒关键信息填表单,主打一个 "解放双手,告别手敲"。

虽说大部分代码都是 AI 搭把手写的,但整个架构设计和工程化踩坑的过程,属实让我涨了不少经验。今天就把这个通用识别引擎的实现思路掰开揉碎讲讲,希望能给各位工友来点灵感~


🎯 项目背景

业务需求

搞科技创新管理系统的都懂,专利、著作权这些信息录入简直是 "职场酷刑":

  • ❌ 手动录入慢到抠脚,还动不动输错
  • ❌ 字段多到记不住,漏填一个就得返工
  • ❌ 重复干活磨心态,纯纯浪费打工人时间

解决方案

咱直接搬出通义千问的多模态视觉模型(qwen3-vl-plus),主打一个 "机器代劳":

  1. ✅ 用户咔咔上传证书图片
  2. ✅ AI 火眼金睛识别关键字段
  3. ✅ 乖乖返回结构化数据
  4. ✅ 前端自动填表单,主打一个丝滑

🏗️ 系统架构设计

整体架构图

plaintext 复制代码
┌─────────────────────────────────────────────────────────┐
│                      前端层                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐ │
│  │ 图片上传    │ →  │ 结果展示    │ →  │ 表单绑定    │ │
│  └─────────────┘    └─────────────┘    └─────────────┘ │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│                      后端层                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐ │
│  │ Controller  │ →  │ Service     │ →  │ 识别引擎    │ │
│  └─────────────┘    └─────────────┘    └─────────────┘ │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│                   AI 服务层                               │
│  ┌─────────────────────────────────────────────────┐   │
│  │  通义千问 DashScope API (qwen3-vl-plus)         │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

模块结构

plaintext 复制代码
com.ls.qwen/
├── config/
│   └── QwenRestTemplateConfig.java    # HTTP 客户端配置
└── VisionRecognitionEngine.java        # 核心识别引擎

设计理念:

  • 配置与业务分家:HTTP 客户端单独配置,不跟业务代码搅和
  • 通用 + 定制两手抓:引擎搞通用的,业务层只需要改改提示词就行

💻 后端核心实现

1. 专用 HTTP 客户端配置

AI 识别这活儿耗时长,得单独配超时时间,不然容易 "半路夭折":

java 复制代码
@Slf4j
@Configuration
public class QwenRestTemplateConfig {
    
    @Bean("qwenRestTemplate")
    public RestTemplate qwenRestTemplate() {
        log.info("初始化 qwenRestTemplate Bean...");
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(10000); // 连接超时 10 秒,够它喘口气了
        factory.setReadTimeout(30000);    // 读取超时 30 秒,给AI留足思考时间
        log.info("qwenRestTemplate 初始化完成");
        return new RestTemplate(factory);
    }
}

为啥要单独整个 RestTemplate?

  1. AI 请求又传图片又推理,耗时比普通接口久多了
  2. 用系统默认配置容易超时,白忙活一场
  3. 不同业务的 HTTP 客户端隔离开,出问题也好定位

2. 通用视觉识别引擎

这玩意儿就是整个模块的 "主心骨",抽象得明明白白,啥场景都能凑合用。

2.1 类结构
java 复制代码
@Slf4j
@Component
public class VisionRecognitionEngine {
    
    // 配置注入,不用硬编码,改配置文件就行
    @Value("${dashscope.api-key}")
    private String apiKey;
    
    @Value("${dashscope.url}")
    private String apiUrl;
    
    @Value("${dashscope.model}")
    private String model;
    
    // 依赖注入,省心又好测
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();
}
2.2 核心处理流程(四步走,不绕弯)
plaintext 复制代码
上传文件 → 校验(防乱传) → Base64 编码(AI认这格式) → 调用 API → 解析响应 → 返回结果
2.3 统一接口设计
java 复制代码
/**
 * 通用识别方法(所有业务复用,主打一个偷懒)
 * @param file 上传图片
 * @param prompt 业务层传入的提示词
 * @param requiredFields 业务字段列表
 * @return 识别结果 Map
 */
public Map<String, String> recognize(
    MultipartFile file, 
    String prompt, 
    List<String> requiredFields
) throws Exception {
    validateFile(file);
    String base64Image = Base64.getEncoder().encodeToString(file.getBytes());
    String apiResponse = callDashScopeApi(base64Image, prompt);
    return parseAndNormalize(apiResponse, requiredFields);
}

设计精髓:

  • ✅ 变与不变拎得清:流程固定死,提示词和字段按需改
  • ✅ 业务层零重复:不管是识别专利、著作权还是标准,都用这一套逻辑,不用重复造轮子
2.4 文件校验机制

先把 "妖魔鬼怪" 挡在门外,省得后面出幺蛾子:

java 复制代码
private void validateFile(MultipartFile file) {
    if (file == null || file.isEmpty()) {
        throw new IllegalArgumentException("图片不能为空!传个空文件逗我呢?");
    }
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new IllegalArgumentException("图片大小不能超过 10MB!太大了AI处理不过来");
    }
    String contentType = file.getContentType();
    if (contentType == null || !contentType.startsWith("image/")) {
        throw new IllegalArgumentException("仅支持图片文件!别传些奇奇怪怪的玩意儿");
    }
}

校验三要素(一个都不能少):

  1. ✅ 非空校验:别传空文件浪费算力
  2. ✅ 大小限制(10MB):AI 处理大文件容易卡
  3. ✅ 类型校验(image/*):只认图片,其他一概不收
2.5 调用大模型 API(OpenAI 兼容格式)
java 复制代码
private String callDashScopeApi(String base64Image, String prompt) throws Exception {
    // 构建多模态内容列表,图片+文字都安排上
    List<Map<String, Object>> contentList = new ArrayList<>();
    
    // 添加图片(用image_url格式,AI才认)
    Map<String, Object> imageItem = new HashMap<>();
    imageItem.put("type", "image_url");
    Map<String, Object> imageUrl = new HashMap<>();
    imageUrl.put("url", "data:image/jpeg;base64," + base64Image);
    imageItem.put("image_url", imageUrl);
    contentList.add(imageItem);
    
    // 添加文本提示词,告诉AI该干啥
    Map<String, Object> textItem = new HashMap<>();
    textItem.put("type", "text");
    textItem.put("text", prompt);
    contentList.add(textItem);
    
    // 构建消息对象
    Map<String, Object> message = new HashMap<>();
    message.put("role", "user");
    message.put("content", contentList);
    
    // 构建请求体
    Map<String, Object> requestBody = new HashMap<>();
    requestBody.put("model", model);
    requestBody.put("messages", Arrays.asList(message));
    
    // 设置请求头,API Key得带好
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", "Bearer " + apiKey);
    headers.setContentType(MediaType.APPLICATION_JSON);
    
    HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
    
    // 发送请求,坐等AI回复
    ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, request, String.class);
    return response.getBody();
}

OpenAI 兼容格式的好处(谁用谁知道):

  • ✅ 业界通用款,换模型供应商也不用大改代码
  • ✅ 文档多到看不完,出问题容易查
  • ✅ 天生支持多模态,图片 + 文字一把抓
2.6 智能解析响应(三层降级,稳如老狗)

这部分是整个引擎最 "秀" 的地方,就算 AI 返回的格式乱七八糟,咱也能给它捋顺了。

java 复制代码
private Map<String, String> parseAndNormalize(String apiResponse, List<String> fields) {
    try {
        JsonNode root = objectMapper.readTree(apiResponse);
        
        // 第一层:提取原始内容(多路径兼容,不怕AI返回来的格式变样)
        String rawContent = extractContent(root);
        
        // 第二层:尝试解析为 JSON(优先选,最规整)
        String cleanJson = rawContent
                .replace("```json", "")
                .replace("```JSON", "")
                .replace("```", "")
                .trim();
        
        if (cleanJson.startsWith("{") || cleanJson.startsWith("[")) {
            try {
                // 修复不完整的 JSON(AI偶尔会忘加{},咱手动补)
                if (!cleanJson.startsWith("{")) {
                    cleanJson = "{" + cleanJson;
                }
                if (!cleanJson.endsWith("}")) {
                    cleanJson = cleanJson + "}";
                }
                
                JsonNode jsonNode = objectMapper.readTree(cleanJson);
                Map<String, String> result = new LinkedHashMap<>();
                
                for (String field : fields) {
                    if (jsonNode.has(field)) {
                        result.put(field, jsonNode.get(field).asText("").trim());
                    } else {
                        result.put(field, "");
                    }
                }
                return result;
            } catch (Exception jsonEx) {
                log.warn("JSON 解析失败,换文本解析试试:{}", jsonEx.getMessage());
            }
        }
        
        // 第三层:文本正则提取(JSON不行就硬扒,不能认输)
        return parseFromText(rawContent, fields);
        
    } catch (Exception e) {
        log.error("解析翻车了:", e);
        // 安全兜底:就算全失败,也返回空字段,系统绝不崩
        return fallback(fields);
    }
}

三层降级策略(层层兜底,永不翻车):

plantext 复制代码
┌─────────────────────────────────────┐
│  第一层:JSON 解析(优先)            │
│  - 扒掉markdown格式的```json外套      │
│  - 补全AI漏写的{},救不完整的JSON     │
│  - 精准提取目标字段                  │
└──────────────┬──────────────────────┘
               │ 失败 ↓
┌──────────────▼──────────────────────┐
│  第二层:文本正则提取                │
│  - 按"字段名:值"格式硬扒            │
│  - 不管AI咋排版,总能抠出关键信息    │
└──────────────┬──────────────────────┘
               │ 失败 ↓
┌──────────────▼──────────────────────┐
│  第三层:安全兜底                    │
│  - 返回空字段Map,不影响前端使用     │
│  - 记好错误日志,方便后续排查        │
│  - 就算全错,系统也能正常跑          │
└─────────────────────────────────────┘

文本解析实现(硬扒也要扒出来):

java 复制代码
private Map<String, String> parseFromText(String text, List<String> fields) {
    Map<String, String> result = new LinkedHashMap<>();
    
    for (String field : fields) {
        // 正则匹配"字段名:值",不管有没有空格都能匹配
        String pattern = field + "\\s*[::]\\s*([^\\n,;,;]+)";
        java.util.regex.Matcher m = java.util.regex.Pattern.compile(pattern).matcher(text);
        
        if (m.find()) {
            result.put(field, m.group(1).trim());
        } else {
            result.put(field, "");
        }
    }
    
    return result;
}

3. 配置文件

yaml 复制代码
# application.yml
dashscope:
  api-key: "sk-your-api-key-here"  # 换成你自己的API Key,别直接用我的
  model: "qwen3-vl-plus"           # 模型版本,想换就换
  url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"

配置设计原则(主打一个灵活):

  • ✅ 敏感信息配置化:API Key 放配置文件,别写死在代码里
  • ✅ 模型可切换:想换 qwen3-vl-base 或者其他模型,改个配置就行
  • ✅ 环境隔离:开发、测试、生产环境用不同配置,不串味儿

4. 业务层实现示例

4.1 专利识别场景
java 复制代码
@Service
public class PatentDetailServiceImpl implements PatentDetailService {
    
    @Autowired
    private VisionRecognitionEngine visionRecognitionEngine;
    
    // 要识别的字段列表,想加想减直接改这儿
    private static final List<String> PATENT_FIELDS = Arrays.asList(
        "专利名称", "专利号", "授权时间", "专利权人", "发明人", 
        "专利类型", "是否转化"
    );
    
    @Override
    public Map<String, String> recognizePatentImage(MultipartFile file) throws Exception {
        log.info("开始识别专利图片啦,文件名:{}", file.getOriginalFilename());
        
        String prompt = buildPatentPrompt();
        log.info("提示词整好了,喊识别引擎干活咯");
        
        // 一行代码搞定识别,主打一个省心
        Map<String, String> result = visionRecognitionEngine.recognize(file, prompt, PATENT_FIELDS);
        
        log.info("识别完事儿,结果长这样:{}", result);
        return result;
    }
    
    private String buildPatentPrompt() {
        return "麻烦你精准识别专利证书上的信息,只提以下字段:" + 
               String.join("、", PATENT_FIELDS) + "。\n\n" +
               "要求:\n" +
               "1. 所有字段都用字符串返回\n" +
               "2. 日期格式必须是YYYY-MM-DD,别瞎写\n" +
               "3. 多个名字用逗号分隔\n" +
               "4. 必须返回纯纯的JSON,别带多余的废话\n" +
               "5. 没找到的字段就返回空字符串";
    }
}

5. Controller 层实现

java 复制代码
@RestController
@RequestMapping("/patent")
public class PatentDetailController {
    
    @Autowired
    private PatentDetailService patentDetailService;
    
    /**
     * 专利图片识别接口(前端调这个就行)
     */
    @PostMapping("/ai/recognize")
    public LSResult recognizePatentImage(@RequestParam("file") MultipartFile file) {
        try {
            Map<String, String> result = patentDetailService.recognizePatentImage(file);
            return LSResult.success("识别成功!不用手动填啦", result);
        } catch (Exception e) {
            log.error("识别翻车了:", e);
            return LSResult.error("识别失败咯:" + e.getMessage());
        }
    }
}

🎨 前端实现

就说表单自动填这事儿,其他的咱不啰嗦

我的思路贼简单:拿到后端返回的 JSON,直接往表单变量上绑,主打一个 "懒人福音",代码如下

"由于当时写好了中文json,后面也懒得修改了,就这样吧"

前端实现要点

javascript 复制代码
handleUploadSuccess(response, file, fileList) {
      
      this.fileList = fileList 
      // 标记识别成功,给用户点反馈
      this.recognitionSuccess = true;
      if (response && response.data) {
        this.recognitionResult = response.data;
      }
       // ========== 重点来咯 ==========

      if (this.recognitionResult) {
        this.form = {
          ...this.form,
          // 基础字段直接映射,不用手动敲
          patentName: this.recognitionResult['专利名称'] || '',
          patentNumber: this.recognitionResult['专利号'] || '',
          patentType: this.recognitionResult['专利类型'] || '',
          applicationDate: this.recognitionResult['申请日期'] || '',
          noticeDate: this.recognitionResult['授权公告日'] || '',
          publicationNumber: this.recognitionResult['授权公告号'] || '',
          patentSource: this.recognitionResult['专利来源'] || '',
          // 多选组件字段也安排上
          patentee: this.recognitionResult['专利权人'] || '',
          inventor: this.recognitionResult['发明人'] || '',
          implementationUnit: this.recognitionResult['实施单位'] || '',
          // 状态类字段别落下
          isConversion: this.recognitionResult['是否转化'] || '',
          patentApplyStatus: this.recognitionResult['状态'] || '',
          isJointApply: this.recognitionResult['是否联合申请'] || '',
          jointApplicant: this.recognitionResult['联合专利权人'] || '',

        };

🎯 完整数据流

plaintext 复制代码
前端上传图片(点一下按钮的事儿)
    ↓
后端 Controller 接招 (/patent/ai/recognize)
    ↓
Service 喊识别引擎干活 (recognizePatentImage)
    ↓
VisionRecognitionEngine.recognize() 一顿操作
    ↓
返回 Map<String, String> 格式的识别结果
    ↓
LSResult.success("识别成功", result) 告诉前端成了
    ↓
前端拿到JSON数据
    ↓
自动绑表单字段,用户直接提交就行

后端返回示例(长这样):

json 复制代码
{
    "code": 200,
    "msg": "识别成功!不用手动填啦",
    "data": {
        "专利名称": "一种高效节能装置",
        "专利号": "ZL202310123456.7",
        "授权时间": "2023-05-15",
        "专利权人": "某某科技有限公司",
        "发明人": "张三,李四",
        "专利类型": "1",
        "是否转化": "1"
    }
}

🏆 设计模式应用

1. 策略模式(Strategy Pattern)

换个提示词和字段列表,就能识别不同东西,主打一个 "一招鲜吃遍天":

java 复制代码
// 专利识别策略
recognize(file, patentPrompt, patentFields);

// 著作权识别策略(换汤不换药)
recognize(file, softwarePrompt, softwareFields);
  1. 模板方法模式(Template Method)

流程固定死,只改参数就行,不用重复写代码:

java 复制代码
public Map<String, String> recognize(...) {
    validateFile(file);           // 步骤 1:先校验,别瞎传
    encodeToBase64(file);         // 步骤 2:转Base64,AI认这个
    callApi(base64, prompt);      // 步骤 3:调API,让AI干活
    parseResponse(response);      // 步骤 4:解析结果,返给前端
    return result;
}

3. 责任链模式(Chain of Responsibility)

解析的时候层层降级,总有一款能行:

plaintext 复制代码
JSON 解析 → 文本解析 → 安全兜底

💡 技术亮点总结

1. 高度抽象的通用性

  • ✅ 业务层只需要关心提示词 和字段列表,其他不用管
  • ✅ 识别逻辑完全解耦,想识别啥就改改提示词,贼灵活
  • ✅ 新增识别场景零代码重复,主打一个偷懒不重复造轮子

2. 健壮的容错机制

plaintext 复制代码
多层降级:JSON → 文本 → 空字段兜底

就算 AI 返回的格式乱七八糟,咱的系统也能稳如老狗,绝不崩溃。

3. OpenAI 格式兼容

  • ✅ 用业界通用格式,后续想换 OpenAI、智谱这些模型,改个配置就行
  • ✅ 不用大改代码,省不少事儿

4. 依赖注入设计

  • ✅ 专用 RestTemplate 配置,不跟其他 HTTP 请求抢资源
  • ✅ 方便做单元测试,出问题也好定位

5. 完善的日志记录

java 复制代码
// INFO:记清楚业务流程,方便排查
log.info("开始专利图片识别,文件名:{}", file.getOriginalFilename());

// DEBUG:调试的时候能看详细信息
log.debug("API 原始响应:{}", apiResponse);

// WARN:降级处理也记下来,知道哪儿出问题了
log.warn("JSON 解析失败,尝试其他方式解析:{}", jsonEx.getMessage());

// ERROR:异常堆栈记全,方便定位bug
log.error("DashScope 调用异常", e);

📝 总结

这个 qwen 模块代码量不多,但设计得是真 "香",主打一个 "小而美、简而精"。靠着 AI 辅助写代码,再加上人工把控架构设计,实现了:

✅ 通用性:一套代码支持多场景,想识别啥就改改提示词

✅ 健壮性:多层降级兜底,就算 AI 抽风也不崩

✅ 可维护性:配置清晰,日志完善,出问题一眼就能看出来

✅ 可扩展性:新增场景零代码重复,偷懒也能偷得有水平

这事儿也告诉咱:现代程序员得驾驭 AI,而不是被 AI 牵着鼻子走------AI 帮咱写代码省时间,咱来把控架构和逻辑,分工明确效率才高。

希望这篇文章能给你带来点启发!要是有啥问题或者更好的想法,欢迎在评论区唠唠~


🔗 参考资料


版权声明:本文为原创技术文章,转载请注明出处。

相关推荐
siuser小伟的AI实验室6 小时前
主目录还有改动,怎么让 Codex 另开任务?一次 worktree 小练习
ai编程
siuser小伟的AI实验室6 小时前
Codex 说完成了,怎么验收?先核对范围、测试、用户路径和上线
ai编程
blackappleD6 小时前
MCP 是什么?按 2026-07-28 新版规范讲清架构、调用流程和配置
ai编程
zw_onemaker_ai6 小时前
让 Agent 自己收钱:72 小时搭出 x402 Agent 经济体(附八个真实踩过的坑)
人工智能·ai编程
程语AI6 小时前
RAG 混合检索实战:向量 + BM25 怎么用 RRF 融合
ai编程
程序员大狐狸6 小时前
Ultra X7 358H 笔记本电脑,本地三个大模型编程能力、速度、上下文全面对比与选型报告
llm·ai编程
IllegalCreed6 小时前
让文档不烂——Agent 时代的文档生命周期(四)
ai编程
qinglongcn6 小时前
Agent 可靠性的数学——为什么编程是第一个跑通的赛道
agent·ai编程
IllegalCreed7 小时前
Agent 不是失忆,是找不到入口——大型项目文档分层设计(一)
ai编程
IllegalCreed7 小时前
让每份文档都有唯一的位置——文档寻址设计(三)
ai编程