📁 第一步:项目结构概览

在开始之前,先了解一下我们整个系统的构成,它由三个项目组成,就像三条生产线协同工作:

flowchart TD
    A[前端项目<br>vue-plant-demo] --> B[后端项目<br>spring-boot-plant-demo]
    B --> C[AI中台项目<br>flask-plant-ai]
    
    C --> D[视觉识别引擎<br>模拟YOLOv11]
    C --> E[认知推理引擎<br>本地DeepSeek (Ollama)]
    
    B --> F[(MySQL数据库<br>存储记录)]

现在,我们逐一攻克这三个项目。

⚙️ 第二步:AI中台 (Flask-Python) - 系统的“智能大脑”

这是最核心的部分,负责“看”图和“想”问题。我们把它建在一个独立的文件夹 ​​flask-plant-ai​​ 里。

1. 环境准备

确保你已经安装并启动了Ollama,并拉取了DeepSeek模型。在命令行执行:

ollama pull deepseek-r1:7b
ollama serve

服务默认运行在 ​​http://localhost:11434​​ 。

2. 安装依赖

在 ​​flask-plant-ai​​ 目录下,创建 ​​requirements.txt​​:

Flask==2.3.3
flask-cors==4.0.0
requests==2.31.0

然后打开终端,执行 ​​pip install -r requirements.txt​​ 安装。

3. 核心代码 ​​app.py​

创建 ​​app.py​​,这是AI中台的入口。为了让大家能快速跑通Demo,这里的视觉识别部分先用一个模拟函数代替,它代表了你未来会用YOLOv11训练出的真实模型。

# flask-plant-ai/app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import requests
import json
import random

app = Flask(__name__)
CORS(app) # 允许跨域请求,方便前端直接调用

# --- 模拟视觉识别引擎 (未来可替换为真实的YOLOv11模型) ---
# 模拟一个能识别几种植物叶片的“AI眼睛”
PLANT_CLASSES = {
    "0": {"name": "健康的番茄叶", "latin": "Solanum lycopersicum"},
    "1": {"name": "番茄早疫病", "latin": "Alternaria solani"},
    "2": {"name": "健康的玉米叶", "latin": "Zea mays"},
    "3": {"name": "玉米大斑病", "latin": "Exserohilum turcicum"},
    "4": {"name": "玫瑰叶", "latin": "Rosa rugosa"}
}

def mock_vision_model(image_bytes):
    """
    模拟视觉模型:根据图片内容(这里随机)返回识别结果。
    在实际项目中,这里会加载你的YOLOv11模型,对image_bytes进行预测。
    """
    # 模拟识别,随机返回一个类别和较高的置信度
    class_id = str(random.randint(0, len(PLANT_CLASSES)-1))
    confidence = round(random.uniform(0.85, 0.99), 4)
    result = {
        "class_id": class_id,
        "class_name": PLANT_CLASSES[class_id]["name"],
        "latin_name": PLANT_CLASSES[class_id]["latin"],
        "confidence": confidence
    }
    print(f"[视觉模型] 识别结果: {result}")
    return result
# --- 模拟视觉识别引擎结束 ---

@app.route('/api/analyze-plant', methods=['POST'])
def analyze_plant():
    """
    核心接口:接收图片,返回识别结果 + DeepSeek的趣味解读
    """
    if 'image' not in request.files:
        return jsonify({"error": "请上传图片"}), 400

    image_file = request.files['image']
    image_bytes = image_file.read()

    # 1. 调用视觉模型进行识别
    vision_result = mock_vision_model(image_bytes)

    # 2. 准备发给DeepSeek的提示词,让它基于识别结果进行“思考”
    prompt = f"""你是一个充满热情的植物学家。用户上传了一张植物叶片的照片,我们的视觉模型识别它为“{vision_result['class_name']}”(学名:{vision_result['latin_name']}),置信度为{vision_result['confidence']}。
请用一段100字左右、生动有趣的话,向用户介绍这片叶子,可以包括它的特点、有趣的小知识,或者如果它是病害,给出一些友好的防治小建议。语气要亲切、自然。"""

    # 3. 调用本地DeepSeek模型 (Ollama API)
    try:
        ollama_response = requests.post(
            "http://localhost:11434/api/generate",
            json={"model": "deepseek-r1:7b", "prompt": prompt, "stream": False}
        )
        ollama_result = ollama_response.json()
        deepseek_comment = ollama_result.get("response", "哎呀,我被问住了,让我再学一学。")
        print(f"[DeepSeek] 生成解读: {deepseek_comment}")
    except Exception as e:
        print(f"[DeepSeek] 调用失败: {e}")
        deepseek_comment = "(AI大脑暂时离线,但这片叶子真的很特别!)"

    # 4. 合并结果并返回
    final_result = {
        "vision": vision_result,
        "comment": deepseek_comment
    }
    return jsonify(final_result)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)
☕ 第三步:后端 (SpringBoot-Java) - 系统的“业务中台”

在另一个文件夹(如 ​​spring-boot-plant-demo​​)中创建SpringBoot项目。它负责接收前端请求,并转发给AI中台。

1. ​​pom.xml​​ 关键依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- 用于调用Python服务的HTTP客户端 -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <version>5.2.1</version>
    </dependency>
</dependencies>
2. 核心Controller ​​PlantController.java​

创建一个控制器,提供一个接口供前端调用。这个接口会调用Python AI中台的接口。

package com.example.demobackend.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/plants")
@CrossOrigin(origins = "http://localhost:5173") // 允许Vue前端访问
public class PlantController {

    @Autowired
    private RestTemplate restTemplate;

    // Python AI中台的地址
    private final String pythonAiUrl = "http://localhost:5001/api/analyze-plant";

    @PostMapping("/identify")
    public ResponseEntity<?> identifyPlant(@RequestParam("image") MultipartFile imageFile) {
        try {
            // 1. 准备要发送给Python服务的数据 (multipart/form-data)
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.MULTIPART_FORM_DATA);

            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            // 将Spring的MultipartFile转换为字节资源
            body.add("image", new MultipartFileResource(imageFile.getOriginalFilename(), imageFile.getSize(), imageFile.getBytes()));

            HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);

            // 2. 调用Python AI中台
            ResponseEntity<Map> response = restTemplate.postForEntity(pythonAiUrl, requestEntity, Map.class);

            // 3. 将Python服务返回的结果直接转发给前端
            return ResponseEntity.ok(response.getBody());

        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", "图片处理失败"));
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(Map.of("error", "AI服务暂时不可用,请稍后再试。"));
        }
    }

    // 一个辅助类,用于将MultipartFile包装成RestTemplate能识别的资源
    static class MultipartFileResource extends org.springframework.core.io.ByteArrayResource {
        private final String filename;
        private final long size;

        public MultipartFileResource(String filename, long size, byte[] content) {
            super(content);
            this.filename = filename;
            this.size = size;
        }

        @Override
        public String getFilename() {
            return this.filename;
        }

        @Override
        public long contentLength() {
            return this.size;
        }
    }
}
3. 配置 ​​RestTemplate​​ (在 ​​DemoBackendApplication.java​​ 或配置类中)
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}
🖼️ 第四步:前端 (Vue3) - 系统的“颜值担当”

在第三个文件夹 ​​vue-plant-demo​​ 里,用Vue3搭建用户界面。

1. 创建Vue3项目并安装依赖
npm create vue@latest vue-plant-demo
cd vue-plant-demo
npm install axios element-plus
2. 核心页面代码 ​​src/views/HomeView.vue​

这个页面包含上传组件,并展示从后端返回的识别结果和DeepSeek的趣味解读。上传组件可以参考 ​​vue3-dropzone​​ 的实现。

<template>
  <div class="home">
    <h1>🌿 植物叶片智能识别系统 Demo</h1>
    <p>上传一张叶子照片,让我告诉你它的故事~</p>

    <div class="upload-area">
      <label for="file-upload" class="custom-file-upload">
        📤 点击上传图片
      </label>
      <input id="file-upload" type="file" accept="image/*" @change="handleFileUpload" />
    </div>

    <!-- 加载状态 -->
    <div v-if="loading" class="loading">
      <p>🔍 正在观察叶子... (视觉模型识别中)</p>
      <p>🧠 正在询问DeepSeek... (AI生成解读中)</p>
    </div>

    <!-- 识别结果显示区 -->
    <div v-if="result" class="result-card">
      <h2>✨ 识别结果</h2>
      <div class="vision-result">
        <p><strong>🌱 植物名称:</strong> {{ result.vision.class_name }}</p>
        <p><strong>🔬 学名:</strong> <i>{{ result.vision.latin_name }}</i></p>
        <p><strong>📊 置信度:</strong> {{ (result.vision.confidence * 100).toFixed(2) }}%</p>
      </div>
      <div class="deepseek-comment">
        <h3>📖 DeepSeek 的植物笔记</h3>
        <p>{{ result.comment }}</p>
      </div>
    </div>

    <!-- 错误提示 -->
    <div v-if="error" class="error">
      <p>❌ {{ error }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import axios from 'axios'

const loading = ref(false)
const result = ref(null)
const error = ref(null)

const handleFileUpload = async (event) => {
  const file = event.target.files[0]
  if (!file) return

  // 重置状态
  loading.value = true
  result.value = null
  error.value = null

  // 准备表单数据
  const formData = new FormData()
  formData.append('image', file)

  try {
    // 调用SpringBoot后端接口
    const response = await axios.post('http://localhost:8080/api/plants/identify', formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    })
    result.value = response.data
  } catch (err) {
    console.error('识别失败:', err)
    error.value = err.response?.data?.error || '识别服务异常,请稍后再试'
  } finally {
    loading.value = false
    // 清空input,以便再次上传同一文件
    event.target.value = ''
  }
}
</script>

<style scoped>
.home {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  text-align: center;
}
.upload-area {
  margin: 30px 0;
}
.custom-file-upload {
  border: 2px dashed #4caf50;
  display: inline-block;
  padding: 15px 30px;
  cursor: pointer;
  background-color: #f9f9f9;
  border-radius: 8px;
  transition: background-color 0.3s;
}
.custom-file-upload:hover {
  background-color: #e8f5e9;
}
input[type="file"] {
  display: none;
}
.loading p {
  margin: 10px 0;
  color: #666;
}
.result-card {
  text-align: left;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 20px;
  margin-top: 20px;
  background-color: #fafafa;
}
.vision-result {
  background-color: #e3f2fd;
  padding: 15px;
  border-radius: 8px;
  margin-bottom: 20px;
}
.deepseek-comment {
  background-color: #fff3e0;
  padding: 15px;
  border-radius: 8px;
  font-style: italic;
}
.error {
  color: #d32f2f;
  background-color: #ffcdd2;
  padding: 10px;
  border-radius: 4px;
  margin-top: 20px;
}
</style>
3. 配置 ​​main.js​
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
🚀 第五步:启动与演示

现在,三条生产线都准备好了,我们来启动它们:

  1. 启动AI中台 (Python)
  • 打开终端,进入 ​​flask-plant-ai​​ 目录。
  • 执行 ​​python app.py​​。
  • 看到 ​​Running on http://0.0.0.0:5001​​ 即成功。
  1. 启动后端 (SpringBoot)
  • 用IDE打开 ​​spring-boot-plant-demo​​ 项目。
  • 运行 ​​DemoBackendApplication.java​​ 的 ​​main​​ 方法。
  • 看到 ​​Started DemoBackendApplication​​ 即成功。
  1. 启动前端 (Vue3)
  • 打开新终端,进入 ​​vue-plant-demo​​ 目录。
  • 执行 ​​npm run dev​​。
  • 看到 ​​VITE v... ready in ... ms​​ 和本地访问地址(通常是 ​​http://localhost:5173​​)。
  1. 打开浏览器,体验奇迹
  • 访问 ​​http://localhost:5173​​。
  • 点击上传区域,选择一张植物叶片的图片(随便一张都行,因为我们的视觉模型目前是模拟的,会随机返回结果)。
  • 稍等片刻,你就会看到:
  • 视觉模型给出的“识别结果”(虽然是随机的,但流程是真实的)。
  • DeepSeek 基于这个结果,生成的一段生动有趣的“植物笔记”。

这个Demo清晰地展示了数据是如何在Vue3、SpringBoot和Python AI中台之间流动的,以及“视觉识别”和“大语言模型”是如何分工协作的。

当你成功跑通这个Demo,看到来自本地DeepSeek生成的“植物笔记”时,那种成就感一定很棒。如果在任何一个环节遇到问题,比如端口冲突、依赖安装失败,随时把错误信息发给我,我们一起看看怎么解决。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐