DEV Community

shashank ms
shashank ms

Posted on

Leveraging LLMs for Recommender Systems

I recently shipped a prototype recommender for a content app where the existing collaborative filter was cold-starting on new inventory. Swapping in an LLM as a ranking layer let us bootstrap personalized suggestions from day one using only item metadata and a short user history. In this tutorial we will build the same core pipeline: a small Python service that retrieves candidate movies and ranks them with an LLM call through Oxlo.ai.

What you'll need

Step 1: Set up the Oxlo.ai client

I create recommender.py and point the OpenAI SDK at Oxlo.ai. I am using llama-3.3-70b because it follows JSON formatting instructions tightly, and on Oxlo.ai there are no cold starts on popular models so the first request after deploy is just as fast as the hundredth.

from openai import OpenAI
import json

client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")

Step 2: Build the candidate catalog

In production this list would come from Postgres or a vector store, but a hardcoded catalog is enough to prove the concept. I added six movies with overlapping genres so the LLM has to actually discriminate rather than match keywords.

CATALOG = [
    {"id": "m1", "title": "Blade Runner 2049", "genres": ["sci-fi", "thriller"], "year": 2017, "synopsis": "A young blade runner's discovery of a long-buried secret leads him to track down former blade runner Rick Deckard."},
    {"id": "m2", "title": "The Grand Budapest Hotel", "genres": ["comedy", "drama"], "year": 2014, "synopsis": "A concierge teams up with one of his employees to prove his innocence after he is framed for murder."},
    {"id": "m3", "title": "Inception", "genres": ["sci-fi", "action"], "year": 2010, "synopsis": "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea."},
    {"id": "m4", "title": "Parasite", "genres": ["thriller", "drama"], "year": 2019, "synopsis": "Greed and class discrimination threaten the newly formed symbiotic relationship between the wealthy Park family and the destitute Kim clan."},
    {"id": "m5", "title": "Arrival", "genres": ["sci-fi", "drama"], "year": 2016, "synopsis": "A linguist works with the military to communicate with alien lifeforms after twelve mysterious spacecraft appear around the world."},
    {"id": "m6", "title": "The Dark Knight", "genres": ["action", "thriller"], "year": 2008, "synopsis": "Batman faces the Joker, a criminal mastermind who wants to plunge Gotham City into anarchy."},
]

Step 3: Write the recommender prompt

The prompt is the only scoring function we have, so I make it explicit. It defines the input schema, the output schema, and the ranking criteria. Keeping this in a constant makes A/B testing trivial.

SYSTEM_PROMPT = """You are a movie recommender engine. Your job is to select the best movies for a user from a provided candidate list.

You will receive:
1. A user profile containing liked movies and preferred genres.
2. A numbered list of candidate movies with metadata.

Instructions:
- Recommend exactly 3 movies from the candidate list.
- Order them from most to least relevant.
- Explain briefly why each matches the user's taste.
- Respond ONLY with a JSON object in this exact format:
{
  "recommendations": [
    {"rank": 1, "id": "movie_id", "title": "Movie Title", "reason": "Brief explanation"},
    {"rank": 2, "id": "movie_id", "title": "Movie Title", "reason": "Brief explanation"},
    {"rank": 3, "id": "movie_id", "title": "Movie Title", "reason": "Brief explanation"}
  ]
}

Do not include any text outside the JSON."""

Step 4: Retrieve candidates and build the message

Before burning an LLM call, I filter the catalog by genre overlap. This keeps the prompt short and the inference predictable. Because Oxlo.ai charges a flat rate per request rather than per token, trimming context still saves latency even though cost is fixed.

def retrieve_candidates(user_prefs, catalog=CATALOG, max_candidates=5):
    liked_genres = set(user_prefs.get("preferred_genres", []))
    candidates = []
    for movie in catalog:
        if liked_genres & set(movie["genres"]):
            candidates.append(movie)
    return candidates[:max_candidates]

def build_user_message(user_prefs, candidates):
    lines = [
        "User Profile:",
        f"Liked movies: {', '.join(user_prefs.get('liked_movies', []))}",
        f"Preferred genres: {', '.join(user_prefs.get('preferred_genres', []))}",
        "",
        "Candidate Movies:",
    ]
    for idx, movie in enumerate(candidates, 1):
        lines.append(
            f"{idx}. ID: {movie['id']} | Title: {movie['title']} | Year: {movie['year']} | "
            f"Genres: {', '.join(movie['genres'])} | Synopsis: {movie['synopsis']}"
        )
    return "\n".join(lines)

Step 5: Rank with the LLM

Now I wire the pieces together. I ask the model for JSON output using Oxlo.ai's JSON mode, then parse the result. If you want deeper reasoning, swap llama-3.3-70b for kimi-k2.6 or qwen-3-32b on the same endpoint.

def recommend(user_prefs):
    candidates = retrieve_candidates(user_prefs)
    if not candidates:
        return {"recommendations": []}

    user_message = build_user_message(user_prefs, candidates)

    response = client.chat.completions.create(
        model="llama-3.3-70b",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        response_format={"type": "json_object"},
    )

    raw = response.choices[0].message.content
    return json.loads(raw)

Run it

I add a small test block that simulates a user who likes sci-fi and thrillers, then execute the script.

if __name__ == "__main__":
    user_prefs = {
        "liked_movies": ["Inception", "The Matrix"],
        "preferred_genres": ["sci-fi", "thriller"]
    }

    result = recommend(user_prefs)
    print(json.dumps(result, indent=2))

Run the file:

python recommender.py

Example output:

{
  "recommendations": [
    {
      "rank": 1,
      "id": "m1",
      "title": "Blade Runner 2049",
      "reason": "Sequel to a seminal sci-fi film that blends dystopian world-building with noir thriller tension."
    },
    {
      "rank": 2,
      "id": "m3",
      "title": "Inception",
      "reason": "Already a stated favorite; confirms alignment with layered, high-concept science fiction."
    },
    {
      "rank": 3,
      "id": "m5",
      "title": "Arrival",
      "reason": "Slow-burn sci-fi that prioritizes intellect and communication over explosions, matching the user's taste for cerebral films."
    }
  ]
}

Next steps

That is the entire pipeline. Two concrete ways to push it further: replace the genre filter with an embedding retrieval step using Oxlo.ai's embeddings endpoint, or turn the ranker into a multi-turn conversation agent that asks clarifying questions before it recommends. You can find details on models and flat per-request pricing at https://oxlo.ai/pricing.

Top comments (0)