FORM NOT VOID, MIND NO CORE

Chapter 3: Building the Backend Foundation for AI Services

2026.08.10

In the previous chapters, we cultivated our Python inner strength and mastered the tools of data science. Through the Kaggle hands-on project, we learned how to start from raw data, go through a series of meticulous processing and analysis, and finally train a decent machine learning model, saving it as a file (e.g., in .pkl or .pth format).

However, an isolated model file, no matter how powerful internally, has limited value. It is like a magnificent sword forged with great skill, locked in its scabbard, unable to display its edge. To truly unlock the value of an AI model, we must deploy it as an online service that can be called by other applications, websites, mobile clients, and even IoT devices, empowering countless scenarios. This process — from a model file to an online service — is the "last mile" of AI engineering.

Crossing this "last mile" means that the role of an AI engineer must evolve from an "alchemist" to an "architect." We must care not only about model accuracy, but also about service availability, latency, throughput, and scalability. A model that runs perfectly in a Jupyter Notebook is a commercial failure if its API service crashes under 100 concurrent requests.

In this chapter, we will focus on building the backend foundation for AI services. We will learn:

Flask: A lightweight, flexible Python web framework. Like an exquisitely crafted "scabbard," it allows us to quickly package our AI model into a Web API and serve its capabilities externally. We will learn how to define routes and handle HTTP requests and responses.

RESTful API Design: APIs are the language of communication between services. We will learn the principles of RESTful design, an elegant and powerful API design style widely adopted in the industry, which makes our service interfaces clear, intuitive, and easy to collaborate on.

Celery: Inference for AI models, especially complex deep learning ones, can be very time-consuming. Having users wait for a long time after making a request creates a poor experience. Celery is a powerful distributed task queue that acts as a "buffer pool" and "accelerator," allowing us to process time-consuming AI computations as asynchronous tasks in the background, enabling the API service to respond instantly and greatly improving user experience and system throughput.

Finally, through a single ongoing hands-on project — building an asynchronous image classification API service — we will integrate all the knowledge points. We will write code ourselves, transforming a pretrained image classification model from a local file into a fully functional, high-concurrency online AI service, step by step.

Mastering the content of this chapter will give you the core engineering capability to productize algorithm models. This is the key differentiator between an "algorithm researcher" and an "AI application engineer." Now, let us begin building a solid and efficient "home" for our AI model.

3.1 Flask Basics: Quickly Building a Lightweight API Service

In the world of Python web frameworks, Django and Flask are the two brightest stars. Django is a "batteries-included" framework with its own ORM, admin interface, and many other components, suitable for building complex, large-scale web applications. Flask, on the other hand, is a "microframework." Its core is minimal, retaining only the most basic functionalities of web development: routing and request/response handling. This minimalist design philosophy gives Flask exceptional flexibility and extensibility, making it an ideal choice for building API services, especially AI model APIs.

3.1.1 "Hello, AI World!": Your First Flask Application

Installing Flask is very simple:

pip install Flask

Now, let us create a web service with minimal code. Create a new file app.py:

# app.py
from flask import Flask

# 1. Create a Flask application instance
# __name__ is a Python predefined variable that points to the name of the current module.
# Flask uses it to determine the application's root directory, so it can find templates and static files.
app = Flask(__name__)

# 2. Define a route and a view function
# @app.route('/') is a decorator that tells Flask that when a user visits the root URL ('/'),
# the following home function should be called.
@app.route('/')
def home():
    # 3. The content returned by the view function is what the user sees in the browser.
    return "Hello, AI World!"

# 4. Start the web server
# This code ensures the server only starts when this script is run directly.
# If this file is imported by another file, the server will not start.
if __name__ == '__main__':
    # app.run() starts a built-in development web server.
    # debug=True enables debug mode: the server auto-restarts on code changes,
    # and detailed error messages are shown when errors occur. NEVER enable this in production!
    app.run(debug=True)

Run this file in the terminal:

python app.py

You will see output similar to this:

 * Serving Flask app 'app'
 * Debug mode: on
 * Running on http://127.0.0.1:5000

Now, open your browser and visit http://127.0.0.1:5000. You will see "Hello, AI World!" displayed on the page. Congratulations, you have successfully built and run your first web service!

3.1.2 Routes, Requests, and Responses: The Core Interaction of a Web Service

The essence of a web service is receiving requests from clients (such as browsers or mobile apps), processing them, and returning a response.

Dynamic Routes

We can make part of the URL variable to handle more complex requests.

# ... (continuing from above) ...

# <username> is a variable. Flask captures the value of this part of the URL and passes it as an argument to the view function.
@app.route('/user/<username>')
def show_user_profile(username):
    return f"User: {username}"

# We can also specify the variable type, e.g., <int:post_id>
@app.route('/post/<int:post_id>')
def show_post(post_id):
    return f"Post ID: {post_id}, Type: {type(post_id)}"

Now visit http://127.0.0.1:5000/user/Alice, and you will see "User: Alice". Visit http://127.0.0.1:5000/post/123, and you will see "Post ID: 123, Type: <class 'int'>".

HTTP Methods

Web communication primarily uses different HTTP methods to express the intent of an operation. For API services, the most commonly used are:

GET: Retrieve a resource.

POST: Create or submit a resource (usually with a data body).

PUT: Update a resource.

DELETE: Delete a resource.

By default, Flask routes only respond to GET requests. We can specify other methods using the methods parameter.

from flask import request

@app.route('/predict', methods=['GET', 'POST'])
def predict():
    if request.method == 'POST':
        # Handle POST requests, typically receiving data and performing model inference
        return "Received a POST request. Ready to predict!"
    else:
        # Handle GET requests, can return an informational page
        return "This is the prediction endpoint. Please use POST to submit data."

The Request Object

Flask encapsulates all the information sent by the client in the global request object. We can extract:

request.method: The HTTP method ('GET', 'POST', etc.).

request.args: URL query parameters (e.g., query in /search?query=flask).

request.form: Form data from a POST request.

request.json: If the request's Content-Type is application/json, this provides the parsed JSON data directly. This is the most common data exchange format for modern APIs.

request.files: Uploaded files.

Response

A view function can return not only strings, but also more complex responses. The most common is to return data in JSON format, which requires the jsonify function.

from flask import jsonify

@app.route('/api/model/info')
def model_info():
    info = {
        "model_name": "ImageClassifier_ResNet50",
        "version": "1.0",
        "input_type": "image/jpeg",
        "output_type": "json"
    }
    # jsonify converts a Python dictionary into a JSON-formatted response,
    # and sets the correct Content-Type header (application/json).
    return jsonify(info)

Visit http://127.0.0.1:5000/api/model/info, and you will see a formatted JSON response.

3.1.3 Integrating an AI Model into Flask

Now, let us do a simple model integration. Suppose we have a trained iris classification model using Scikit-learn.

  1. Train and save the model (one-time operation)

    # scripts/train_iris_model.py
    from sklearn.datasets import load_iris
    from sklearn.linear_model import LogisticRegression
    import pickle
    
    iris = load_iris()
    X, y = iris.data, iris.target
    
    model = LogisticRegression(max_iter=200)
    model.fit(X, y)
    
    # Use pickle to serialize the trained model object to a file
    with open('iris_model.pkl', 'wb') as f:
        pickle.dump(model, f)
    
  2. Load and use the model in the Flask application

    # app.py
    from flask import Flask, request, jsonify
    import pickle
    import numpy as np
    
    app = Flask(__name__)
    
    # Load the model into memory once when the application starts
    # This avoids reloading it for every request, improving efficiency
    try:
        with open('iris_model.pkl', 'rb') as f:
            model = pickle.load(f)
        iris_target_names = ['setosa', 'versicolor', 'virginica']
        print("Model loaded successfully!")
    except FileNotFoundError:
        model = None
        print("Error: Model file 'iris_model.pkl' not found. Please run the training script first.")
    
    @app.route('/')
    def home():
        return "Iris classification API. Please POST to /predict endpoint."
    
    @app.route('/predict', methods=['POST'])
    def predict():
        if model is None:
            return jsonify({"error": "Model not loaded, service unavailable"}), 500
    
        # 1. Get and validate input data
        data = request.get_json()
        if not data or 'features' not in data:
            return jsonify({"error": "Request body must be a JSON with 'features' key"}), 400
    
        features = data['features']
        if not isinstance(features, list) or len(features) != 4:
            return jsonify({"error": "'features' must be a list of 4 numerical values"}), 400
    
        try:
            # 2. Data preprocessing
            # Convert the input list to a NumPy array and reshape it to the required (1, 4) shape
            input_data = np.array(features).reshape(1, -1)
    
            # 3. Model inference
            prediction_idx = model.predict(input_data)[0]
            prediction_name = iris_target_names[prediction_idx]
    
            probabilities = model.predict_proba(input_data)[0].tolist()
            confidence = dict(zip(iris_target_names, probabilities))
    
            # 4. Construct the response
            response = {
                "prediction": prediction_name,
                "class_index": int(prediction_idx),
                "confidence": confidence
            }
            return jsonify(response)
    
        except Exception as e:
            # Catch potential errors, e.g., input data cannot be converted to numerical values
            return jsonify({"error": f"An error occurred while processing the request: {str(e)}"}), 500
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    Now you can use curl or Postman to test this API:

    curl -X POST http://127.0.0.1:5000/predict \
    -H "Content-Type: application/json" \
    -d '{"features": [5.1, 3.5, 1.4, 0.2]}'
    

    You will receive a JSON response like this:

    {
    "class_index": 0,
    "confidence": {
        "setosa": 0.98...,
        "versicolor": 0.01...,
        "virginica": 0.0...
    },
    "prediction": "setosa"
    }
    

    This example, though simple, contains the complete flow for building an AI API: loading the model -> defining the API endpoint -> receiving and validating data -> preprocessing -> model inference -> formatting the response.

3.2 RESTful API Design Principles and Best Practices

We have built a working API, but to make it a "good" API, we need to follow certain design standards. REST (Representational State Transfer) is a software architectural style, not a strict protocol. APIs built based on REST are called RESTful APIs.

3.2.1 Core Principles

  1. Resources: The core of an API is "resources." Anything can be a resource, such as a user, an image, a model prediction result. Each resource is identified by a unique URI (Uniform Resource Identifier), typically a URL.

    • Bad practice: /getUser?id=123, /createPrediction
    • Good practice: /users/123, /predictions
  2. Use HTTP Methods to Express Operations: Operations on resources should be defined by HTTP methods.

    • GET /users/123: Get information about user with ID 123.
    • POST /users: Create a new user.
    • PUT /users/123: Update information for user with ID 123.
    • DELETE /users/123: Delete user with ID 123.

    URLs should use nouns, not verbs.

  3. Use HTTP Status Codes to Express Results: The API response should use standard HTTP status codes to inform the client of the operation's result.

    2xx (Success):

    • 200 OK: Request succeeded.
    • 201 Created: Resource created successfully.
    • 204 No Content: Operation succeeded but no content to return (e.g., DELETE).

    4xx (Client Error):

    • 400 Bad Request: Invalid request (e.g., JSON format error, missing parameters).
    • 401 Unauthorized: Authentication is required.
    • 403 Forbidden: Authentication succeeded but no permission to access.
    • 404 Not Found: Resource not found.

    5xx (Server Error):

    • 500 Internal Server Error: An unknown internal server error occurred.
    • 503 Service Unavailable: Service is temporarily unavailable.
  4. Stateless: The server should not save any state about the client session. Every request should contain all the necessary information for the server to process it independently. This greatly simplifies server design and makes horizontal scaling easy.

3.2.2 Best Practices for AI API Design

Versioning: APIs should have versions. When your model or interface undergoes incompatible changes, you can upgrade the version to avoid breaking existing client integrations.

  • URL versioning: https://api.example.com/v1/predict
  • Header versioning: Specify Accept: application/vnd.example.v1+json in the HTTP header.

Clear Request/Response Format: Use JSON as the primary data exchange format. The JSON structure for requests and responses should be clear, consistent, and documented.

Request body:

{
    "model_version": "v1.2",
    "data": {
    "image_url": "http://...",
    // or "image_base64": "..."
    },
    "parameters": {
    "top_k": 5
    }
}

Success response body:

{
    "request_id": "xyz-123",
    "prediction": [
    {"label": "cat", "score": 0.95},
    {"label": "dog", "score": 0.04}
    ]
}

Error response body:

{
    "error": {
    "code": "INVALID_INPUT",
    "message": "Input image format not supported."
    }
}

Asynchronous Processing Pattern: For time-consuming AI tasks (such as video analysis, large model generation), synchronous waiting is not feasible. An asynchronous pattern should be adopted:

  1. Client POST /jobs initiates a task; the request body contains the task's data.

  2. Server immediately validates the request, creates a task, and returns a 202 Accepted status code. The response body includes a task ID and a URL to check the status.

    {
      "job_id": "job-abc-456",
      "status": "pending",
      "status_url": "/jobs/job-abc-456"
    }
    
  3. Client later polls GET /jobs/{job_id} to check the task status.

  4. When the task completes, the GET request returns 200 OK with the final result.

We will learn how to implement this powerful asynchronous pattern using Celery in the next section.

3.3 Celery: Handling Time-Consuming AI Computations with Asynchronous Tasks

3.3.1 Why Do We Need a Task Queue?

Back to our Flask API. When a POST /predict request comes in, the predict function performs model inference. If this inference takes 5 seconds, the HTTP connection is held for 5 seconds, and the client is kept waiting the entire time. Worse, Flask's development server is single-threaded, meaning it cannot handle any other requests during those 5 seconds.

In production, we use multi-process/multi-threaded WSGI servers like Gunicorn, but this does not fundamentally solve the problem. If 10 requests come in simultaneously, each requiring 5 seconds, then all 10 server workers are occupied, and the 11th request must wait in line. This results in extremely low system throughput and a very poor user experience.

Celery introduces a Task Queue architecture to solve this problem. Its core components include:

  1. Producer: Our Flask application. It does not execute time-consuming tasks directly but sends a task description (e.g., "Please classify this image") to a message queue.
  2. Message Broker: A message queue, such as Redis or RabbitMQ. It acts like a "to-do list" for tasks, responsible for receiving and storing tasks sent by the producer.
  3. Consumer (Worker): One or more independent Celery processes. They continuously fetch tasks from the Broker and execute them in the background. Workers are completely decoupled from the Flask application and can be deployed on different machines.
  4. Result Backend: A database for storing task execution results, such as Redis or a database. This allows the Flask application to query the task's status and results later.

Workflow:

  1. The Flask API receives a request and immediately sends an inference task (including image data) to Redis.
  2. The Flask API immediately returns a "task received" response to the client, including a task ID. The entire HTTP request-response cycle may take only tens of milliseconds.
  3. In the background, an idle Celery Worker fetches the task from Redis.
  4. The Worker executes the time-consuming model inference.
  5. The Worker stores the inference result in the Redis result backend.
  6. The client uses the task ID to query the task status via another API endpoint and eventually retrieves the result.

This architecture brings enormous benefits:

  • High Responsiveness: The API responds almost instantly.
  • High Throughput: The API can handle a large number of requests quickly because its main job is simply to throw tasks into the queue.
  • Scalability: If tasks pile up, we simply add more Celery Worker processes or machines, without modifying the Flask application.
  • Decoupling and Robustness: Even if a Worker crashes due to a failed task, the main web application is unaffected.

3.3.2 Integrating Celery with Flask

Install the required libraries:

pip install celery redis

You also need to install and run a Redis server. Using Docker is the simplest way:

docker run -d -p 6379:6379 --name my-redis redis

Project Structure Adjustment:

To better organize the code, we will create a Flask project using the application factory pattern.

async_image_classifier/
├── app/
│   ├── __init__.py       # Application factory
│   ├── tasks.py          # Celery task definitions
│   └── routes.py         # Flask route definitions
├── celery_worker.py      # Script to start the Celery worker
├── config.py             # Configuration file
└── run.py                # Script to start the Flask application
  1. Configuration file (config.py)

    class Config:
        CELERY_BROKER_URL = 'redis://localhost:6379/0'
        CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
    
  2. Create the Celery instance and Flask application factory (app/__init__.py)

    from flask import Flask
    from celery import Celery
    from config import Config
    
    # Create the Celery instance, but not yet configured
    celery = Celery(__name__, broker=Config.CELERY_BROKER_URL)
    
    def create_app():
        app = Flask(__name__)
        app.config.from_object(Config)
    
        # Update the Flask config to the Celery instance
        celery.conf.update(app.config)
    
        # Register routes
        from . import routes
        app.register_blueprint(routes.bp)
    
        return app
    
  3. Define Celery tasks (app/tasks.py)

    from . import celery
    import time
    
    # A simulated time-consuming AI task
    @celery.task
    def long_running_ai_task(data):
        """Simulate an AI computation that takes 5 seconds"""
        print(f"Starting task, received data: {data}")
        time.sleep(5)
        result = {"input": data, "output": "This is the prediction result."}
        print("Task completed.")
        return result
    

    The @celery.task decorator converts a regular function into a Celery task.

  4. Define Flask routes (app/routes.py)

    from flask import Blueprint, request, jsonify, url_for
    from .tasks import long_running_ai_task
    
    bp = Blueprint('main', __name__)
    
    @bp.route('/start-task', methods=['POST'])
    def start_task():
        data = request.get_json()
        if not data:
            return jsonify({"error": "No data provided"}), 400
    
        # Call the task asynchronously
        # .delay() is a shortcut for .apply_async()
        task = long_running_ai_task.delay(data)
    
        # Return immediately, providing a URL to check the status
        return jsonify({
            "message": "Task started",
            "task_id": task.id,
            "status_url": url_for('main.task_status', task_id=task.id, _external=True)
        }), 202
    
    @bp.route('/task-status/<task_id>')
    def task_status(task_id):
        task = long_running_ai_task.AsyncResult(task_id)
    
        response = {
            "task_id": task_id,
            "status": task.state
        }
    
        if task.state == 'PENDING':
            response['info'] = 'Task is waiting to be executed.'
        elif task.state == 'SUCCESS':
            response['result'] = task.result
        elif task.state != 'FAILURE':
            # Task is in progress
            response['info'] = task.info or 'No progress info'
        else:
            # Task failed
            response['info'] = str(task.info) # Exception information
    
        return jsonify(response)
    
  5. Startup scripts (run.py and celery_worker.py)

    # run.py (Start Flask)
    from app import create_app
    
    app = create_app()
    
    if __name__ == '__main__':
        app.run(debug=True)
    
    # celery_worker.py (Start Celery)
    from app import create_app, celery
    
    app = create_app()
    app.app_context().push()
    

Running:

You need to open two terminal windows.

Terminal 1 (Start Celery Worker):

# -A specifies the location of the Celery application instance
# -l info sets the log level
celery -A celery_worker.celery worker -l info

Terminal 2 (Start Flask application):

python run.py

Now you can test it with curl as before:

  1. Initiate a task:

    curl -X POST http://127.0.0.1:5000/start-task -H "Content-Type: application/json" -d '{"image_id": 123}'
    

    You will immediately receive a response containing a task_id.

  2. Check the status:

    Copy the task_id from the previous step, then access the status URL:

    curl http://127.0.0.1:5000/task-status/your-task-id-here
    

    Initially, the status might be PENDING or STARTED. After waiting 5 seconds, query again, and the status will change to SUCCESS, showing the task's result.

We have now successfully built a complete asynchronous task processing system!

3.4 Hands-On Project: Building an Asynchronous Image Classification API Service

Now, we will integrate all the knowledge points in this chapter to complete our final project. We will use a pretrained deep learning model (e.g., ResNet50), deploy it as a fully functional, asynchronous image classification service using Flask and Celery.

Project Objectives:

  1. Provide a /predict endpoint that accepts uploaded image files.
  2. The endpoint should respond immediately, returning a task ID.
  3. A background Celery Worker handles image preprocessing and model inference.
  4. Provide a /results/<task_id> endpoint for querying classification results.

Tech Stack:

  • Flask
  • Celery + Redis
  • PyTorch + TorchVision (for the model and image processing)
  • Pillow (for image I/O)

Install additional dependencies:

pip install torch torchvision Pillow

Project Structure: (Similar to Section 3.3)

  1. Define Celery tasks (app/tasks.py)

    This time it is a real AI task.

    # app/tasks.py
    from . import celery
    from PIL import Image
    import torch
    import torchvision.transforms as transforms
    import torchvision.models as models
    import json
    import io
    import base64
    
    # --- Model Loading ---
    # Load the model once when the worker starts to avoid repeated loading
    # Use a pretrained ResNet50 model
    model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
    model.eval() # Set to evaluation mode
    
    # Load ImageNet class labels
    try:
        with open('imagenet_class_index.json') as f:
            class_index = json.load(f)
        imagenet_labels = {int(k): v[1] for k, v in class_index.items()}
        print("ImageNet labels loaded.")
    except FileNotFoundError:
        print("Warning: imagenet_class_index.json not found. Predictions will be class indices.")
        imagenet_labels = None
    
    # --- Image Preprocessing ---
    # Define the same preprocessing pipeline as used during ResNet50 training
    preprocess = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])
    
    @celery.task(bind=True)
    def classify_image_task(self, image_b64_string: str):
        """
        Celery task: decode the image, preprocess, and run model inference.
        bind=True allows the task function to access self, enabling status updates.
        """
        try:
            self.update_state(state='PROGRESS', meta={'status': 'Decoding image...'})
            # Decode the Base64 string into an image
            image_bytes = base64.b64decode(image_b64_string)
            image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
    
            self.update_state(state='PROGRESS', meta={'status': 'Preprocessing image...'})
            # Preprocess the image
            input_tensor = preprocess(image)
            input_batch = input_tensor.unsqueeze(0) # Create a mini-batch
    
            self.update_state(state='PROGRESS', meta={'status': 'Running model inference...'})
            # Model inference
            with torch.no_grad(): # Disable gradient calculation for faster inference
                output = model(input_batch)
    
            # Get Top 5 predictions
            probabilities = torch.nn.functional.softmax(output[0], dim=0)
            top5_prob, top5_catid = torch.topk(probabilities, 5)
    
            self.update_state(state='PROGRESS', meta={'status': 'Formatting results...'})
            results = []
            for i in range(top5_prob.size(0)):
                prob = top5_prob[i].item()
                cat_id = top5_catid[i].item()
                label = imagenet_labels.get(cat_id, "Unknown") if imagenet_labels else "Unknown"
                results.append({"label": label, "probability": f"{prob:.4f}"})
    
            return {'status': 'Completed', 'predictions': results}
    
        except Exception as e:
            self.update_state(state='FAILURE', meta={'exc_type': type(e).__name__, 'exc_message': str(e)})
            # In Celery, it is better to raise the exception rather than return an error message
            raise e
    

    Note: You need to download the imagenet_class_index.json file from the internet and place it in the project root directory.

  2. Define Flask routes (app/routes.py)

    # app/routes.py
    from flask import Blueprint, request, jsonify, url_for
    from .tasks import classify_image_task
    import base64
    
    bp = Blueprint('main', __name__)
    
    @bp.route('/predict', methods=['POST'])
    def predict():
        if 'image' not in request.files:
            return jsonify({"error": "No image file provided in the request"}), 400
    
        file = request.files['image']
    
        # Check file type (optional but recommended)
        if file.filename == '' or not file.filename.lower().endswith(('.png', '.jpg', '.jpeg')):
            return jsonify({"error": "Invalid file type. Please upload a PNG, JPG, or JPEG image."}), 400
    
        try:
            # Read the file content and encode it as a Base64 string
            # This is a common method for safely transmitting binary data through JSON
            image_bytes = file.read()
            image_b64_string = base64.b64encode(image_bytes).decode('utf-8')
    
            # Initiate the asynchronous task
            task = classify_image_task.delay(image_b64_string)
    
            return jsonify({
                "message": "Image classification task started.",
                "task_id": task.id,
                "status_url": url_for('main.get_result', task_id=task.id, _external=True)
            }), 202
    
        except Exception as e:
            return jsonify({"error": f"Failed to start task: {str(e)}"}), 500
    
    @bp.route('/results/<task_id>')
    def get_result(task_id):
        task = classify_image_task.AsyncResult(task_id)
    
        response = {
            "task_id": task_id,
            "status": task.state
        }
    
        if task.state == 'SUCCESS':
            response['result'] = task.result
        elif task.state == 'FAILURE':
            response['error'] = str(task.info)
        elif task.state == 'PROGRESS':
            response['progress'] = task.info
    
        return jsonify(response)
    
  3. Run and Test

    Start the Celery Worker and Flask application (same as Section 3.3). Then test by uploading an image with curl:

    # Replace 'path/to/your/image.jpg' with your image path
    curl -X POST http://127.0.0.1:5000/predict -F "image=@path/to/your/image.jpg"
    

    You will receive a task ID. Use this ID to query the results URL, and you will eventually see JSON output similar to this:

    {
    "result": {
        "predictions": [
        { "label": "golden_retriever", "probability": "0.9213" },
        { "label": "Labrador_retriever", "probability": "0.0345" },
        // ...
        ],
        "status": "Completed"
    },
    "status": "SUCCESS",
    "task_id": "..."
    }
    

Chapter Summary

In this chapter, we achieved the critical leap from a local model file to a fully functional, robust, and scalable online AI service.

We first learned to use Flask, the lightweight framework, to quickly wrap a "Web coat" around our AI model, allowing it to communicate with the outside world via the HTTP protocol. Then, we delved into the principles of RESTful API design, learning how to design clear, standardized, and easy-to-collaborate API interfaces — a hallmark of professional software engineering.

Most importantly, we introduced Celery and the task queue architecture, fundamentally solving the API performance bottleneck caused by time-consuming AI inference. By making compute-intensive tasks asynchronous, the service we built not only provides users with an "instant response" experience but also gains the ability to horizontally scale under high concurrency.

The final hands-on project brought all these technical points together. You now not only know how to train a model but also how to deploy it as an industrial-grade AI service that can generate real business value. This is a crucial step forward in your career as an AI engineer.

In the following chapters, we will continue to explore other areas of AI engineering, such as containerizing our application with Docker and exploring more advanced AI application paradigms. But always remember, the backend engineering foundation learned in this chapter will be the solid bedrock upon which you will build any complex AI system in the future.