n4nAI

AI-powered product recommendations with LangChain

Hands-on tutorial: build AI-powered product recommendations with LangChain using a vector store and LLM chain, from catalog embedding to ranked output.

n4n Team3 min read633 words

Audio narration

Coming soon — every post will get a voice note here.

Building product recommendations langchain applications means wiring a retrieval pipeline to an LLM so the model can reason over your catalog instead of blindly cosine-matching. This tutorial walks through a runnable implementation of product recommendations langchain style: embed a catalog, retrieve candidates, and use a chain to produce a ranked, explained shortlist. You’ll end with a small service that takes a user prompt and returns recommendations with justification.

Prerequisites

  • Python 3.10 or newer
  • Familiarity with basic LangChain concepts (Runnable, Retriever, PromptTemplate)
  • An API key for an OpenAI-compatible chat + embeddings endpoint
  • A small product catalog in JSON (we’ll generate one below)

Install the dependencies before continuing:

pip install langchain langchain-openai langchain-chroma chromadb openai

1. The product catalog

We’ll use a flat JSON list. Each item needs enough textual context for embedding to be useful. Keep descriptions concrete.

[
  {
    "id": "sku-001",
    "name": "Trailhead Running Shoes",
    "category": "footwear",
    "description": "Lightweight zero-drop running shoe with grippy outsole for mixed terrain.",
    "price": 119.0
  },
  {
    "id": "sku-002",
    "name": "Summit Hiking Boots",
    "category": "footwear",
    "description": "Waterproof leather boots with ankle support for multi-day backpacking.",
    "price": 189.0
  },
  {
    "id": "sku-003",
    "name": "Daily Trainer Tee",
    "category": "apparel",
    "description": "Moisture-wicking merino blend shirt for everyday training runs.",
    "price": 45.0
  }
]

Save this as products.json. In a real system you’d pull from a database, but the shape is what matters.

2. Embed and index with Chroma

LangChain’s Document abstraction lets us attach metadata alongside page content. We embed the concatenated name + description and keep id and price in metadata for later filtering.

import json
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

with open("products.json") as f:
    products = json.load(f)

docs = [
    Document(
        page_content=f"{p['name']}: {p['description']}",
        metadata={"id": p["id"], "price": p["price"], "category": p["category"]},
    )
    for p in products
]

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    base_url="https://api.openai.com/v1",  # swap for your endpoint
    api_key="YOUR_API_KEY",
)

vectorstore = Chroma.from_documents(
    docs, embeddings, collection_name="product_catalog"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

Run this script once. Chroma persists to a local ./chroma directory by default, so subsequent loads can use Chroma(collection_name=..., embedding_function=embeddings).

Expected checkpoint: no errors, and len(vectorstore.get()['ids']) equals the number of products.

3. Build the recommendation chain

The core pattern for product recommendations langchain pipelines is retrieval-augmented generation. We retrieve candidate documents, serialize them, and ask the LLM to return a strict JSON ranking.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser

llm = ChatOpenAI(
    model="gpt-4o-mini",
    base_url="https://api.openai.com/v1",
    api_key="YOUR_API_KEY",
    temperature=0,
)

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a retail recommendation engine. Given a user intent and candidate "
     "products, return the top 3 product IDs that best match. Respond ONLY with "
     "JSON: {\"recs\": [{\"id\": str, \"reason\": str}]}."),
    ("human",
     "User intent: {query}\n\nCandidates:\n{candidates}"),
])

parser = JsonOutputParser()

def format_candidates(docs):
    lines = []
    for d in docs:
        lines.append(
            f"ID {d.metadata['id']} | {d.page_content} | ${d.metadata['price']}"
        )
    return "\n".join(lines)

def recommend(query: str):
    candidates = retriever.invoke(query)
    chain = prompt | llm | parser
    return chain.invoke({
        "query": query,
        "candidates": format_candidates(candidates),
    })

Note the temperature=0 — recommendations should be deterministic per retrieval set. If you point the ChatOpenAI client at an OpenAI-compatible gateway like n4n.ai, you get automatic fallback when a provider is rate-limited and per-token usage metering without extra code.

4. Run it and inspect output

Call the function with a realistic shopper query:

result = recommend("I need shoes for a wet trail run and a shirt that won't smell")
print(result)

Expected output shape:

{
  "recs": [
    {
      "id": "sku-001",
      "reason": "Zero-drop running shoe with grippy outsole suits wet trail runs."
    },
    {
      "id": "sku-003",
      "reason": "Merino blend tee resists odor during daily training."
    },
    {
      "id": "sku-002",
      "reason": "Waterproof boots are relevant but heavier than needed for a run."
    }
  ]
}

The LLM reorders the retrieved candidates and adds a short rationale. That’s the key win over raw vector search: it can weigh trade-offs (“boots are waterproof but too heavy”) using the query context.

5. Filtering and guardrails

Blind retrieval can surface out-of-budget items. Add a metadata filter to the retriever to enforce business rules before the LLM sees candidates.

retriever = vectorstore.as_retriever(
    search_kwargs={"k": 5, "filter": {"category": "footwear"}}
)

For price caps, you’ll need to post-filter or inject the constraint into the system prompt. Keep the prompt explicit:

Only recommend items with price <= {max_price}.

Then pass max_price through the chain. The LLM will drop violations, but you should still validate the parsed IDs against your catalog before rendering UI.

6. Streaming a justification

If you want the model to explain its picks in prose for a storefront widget, swap the JSON parser for a plain string and stream tokens:

from langchain_core.prompts import ChatPromptTemplate

stream_prompt = ChatPromptTemplate.from_messages([
    ("system", "Explain in 2 sentences why each recommended product fits the user."),
    ("human", "User: {query}\nProducts: {candidates}"),
])

stream_chain = stream_prompt | llm

for chunk in stream_chain.stream({
    "query": "gift for a hiker",
    "candidates": format_candidates(retriever.invoke("gift for a hiker")),
}):
    print(chunk.content, end="")

This avoids blocking the client while the explanation generates.

7. Production hardening

A few things separate a demo from a deployable feature:

  • Cache embeddings. Product descriptions change rarely. Embed once, store the vectors, and only re-embed on update.
  • Honor cache-control. If your gateway forwards provider cache hints, set cache_control on static system prompts to cut repeat token costs.
  • Log the retrieval set. When a recommendation looks wrong, you need to know whether retrieval or generation failed.
  • Fallback models. If your primary chat model is degraded, a secondary model with the same schema can keep latency acceptable.

The product recommendations langchain pattern scales to millions of SKUs by sharding the vector store and precomputing user embeddings for session-less personalization.

8. Where to take it next

Add hybrid search (BM25 + dense) via LangChain’s EnsembleRetriever to catch exact-match queries like “sku-002”. Swap the JSON output for a Pydantic model and with_structured_output to get typed results and validation. Finally, wrap recommend in a FastAPI route and you have a recommendation endpoint that any front-end can call.

The code above is intentionally minimal. The moment you plug in a real catalog and a real query log, the LLM’s ability to arbitrate between near-duplicate products becomes the differentiator.

Tagslangchainecommercerecommendations

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All framework tutorials: e-commerce search & recommendations posts →