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.
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)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
curlor 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
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
- Bad practice:
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.
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.
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+jsonin 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:
Client
POST /jobsinitiates a task; the request body contains the task's data.Server immediately validates the request, creates a task, and returns a
202 Acceptedstatus 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" }Client later polls
GET /jobs/{job_id}to check the task status.When the task completes, the
GETrequest returns200 OKwith 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:
- 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.
- 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.
- 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.
- 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:
- The Flask API receives a request and immediately sends an inference task (including image data) to Redis.
- 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.
- In the background, an idle Celery Worker fetches the task from Redis.
- The Worker executes the time-consuming model inference.
- The Worker stores the inference result in the Redis result backend.
- 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
Configuration file (
config.py)class Config: CELERY_BROKER_URL = 'redis://localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'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 appDefine 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 resultThe
@celery.taskdecorator converts a regular function into a Celery task.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)Startup scripts (
run.pyandcelery_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:
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.Check the status:
Copy the
task_idfrom the previous step, then access the status URL:curl http://127.0.0.1:5000/task-status/your-task-id-hereInitially, the status might be
PENDINGorSTARTED. After waiting 5 seconds, query again, and the status will change toSUCCESS, 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:
- Provide a
/predictendpoint that accepts uploaded image files. - The endpoint should respond immediately, returning a task ID.
- A background Celery Worker handles image preprocessing and model inference.
- 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)
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 eNote: You need to download the
imagenet_class_index.jsonfile from the internet and place it in the project root directory.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)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.