Lesson 1 of 9 in Phase 3 · Embeddings & RAG

Embeddings

🔍 Phase 3 · Embeddings & RAGBeginner~23 min read~14 min listen
Prerequisites:Tokenization
← PreviousAdversarial Prompting: Jailbreaks, Injections & Defense StrategiesFrom: 💬 Phase 2 · Prompting & Structured OutputNext →Embedding Models: Representations, Similarity & Fine-tuning

The 30-Second Pitch

Embeddings are dense, low-dimensional vector representations of discrete, high-dimensional data—like words, sentences, or images. They solve the problem of making complex, unstructured data computationally tractable for machine learning models by capturing semantic meaning and relationships in a continuous vector space. A team would pick embeddings over simpler encodings (like one-hot vectors) because they enable models to understand similarity (e.g., "king" is to "queen" as "man" is to "woman"), reduce dimensionality for efficiency, and serve as a powerful, transferable foundation for downstream AI tasks like search, recommendation, and classification.

Key Takeaway: An embedding turns "what does this mean?" into "where does this sit on the map?" — meaning becomes a coordinate you can do math on.

Mental Model

The mental model for embeddings is a learned map where distance means meaning. Discrete things (words, sentences, images) are placed as points in a continuous space such that semantically similar things land near each other. Everything downstream — search, RAG retrieval, clustering, classification — is just a geometry operation (nearest-neighbor, dot product, cluster) on that map. The model's only job is to make "near" mean "similar enough for your task".

Push the map one step further and the whole production stack falls out of it. Think of a surveyed city:

ConceptOn the map
Embedding modelThe surveyor who assigns every building its coordinates
An embeddingA pin on the map
Training objectiveThe surveying method — what "near" was calibrated to mean
Vector databaseThe map index that answers "what's near this pin?" fast
ChunkingWhat counts as one address — a block, a building, or a doorknob
Normalization + metricThe projection and units — mix miles with kilometers and the numbers still look valid, but mean nothing
ANN searchAsking a well-connected local instead of measuring the distance to every address
Model upgradeA new surveyor redraws the map — every old pin is now on the wrong map

Three consequences follow. First, an embedding is only as good as the objective it was trained for: a map surveyed for driving distance is wrong for hiking. A model tuned for semantic similarity is not automatically good at code or multilingual retrieval — that is the embedding models selection problem. Second, the map is useless without an index that can answer "what's near this point?" at scale, which is the vector databases problem. Third, the map is drawn by one surveyor: coordinates from model v1 and model v2 are different maps. Upgrading your embedding model means re-surveying — re-embedding — the entire corpus. Budget for that on the day you pick the model, not the day you upgrade it.

Warning: Swap the embedding model and every existing pin sits on the wrong map — query vectors and index vectors from different model versions are never comparable.

How It Actually Works

The core mental model is a mapping function: f(item) -> vector. This function is learned, typically by a neural network, such that the geometric relationships (distance, direction) in the resulting vector space reflect the semantic relationships of the original items.

Key Internals & Data Flow:

  1. Input: Raw, discrete tokens (words, product IDs, user IDs) or chunks of data (sentences, image patches).
  2. Lookup Layer (Embedding Layer): This is the surveyor's ledger — a massive matrix E of size [vocab_size, embedding_dim]. Each row i is the embedding vector (the recorded coordinates) for token i. The input token's integer ID acts as an index to retrieve its corresponding row from this matrix.
    # Pseudocode for the forward pass
    token_id = 42  # The integer representing the word "cat"
    embedding_vector = embedding_matrix[token_id]  # e.g., shape: [300]
    
  3. Training Objective: The values in the E matrix are not predetermined; they are learned. The training objective is the surveying method — it decides what "near" gets calibrated to mean. Classic objectives include:
    • Word2Vec's Skip-gram: Predict context words given a target word. This pushes words that appear in similar contexts to have similar embeddings.
    • Contrastive Learning (e.g., for sentence embeddings): Train on pairs (anchor, positive, negative). The loss function (e.g., Triplet Loss) minimizes the distance between the anchor and positive embedding while maximizing the distance between the anchor and negative embedding.
  4. Output Vector Space: After training, the vectors exhibit emergent properties.
    • Distance: Cosine similarity or Euclidean distance between two vectors measures their semantic relatedness.
    • Analogies: Vector arithmetic works: embedding("king") - embedding("man") + embedding("woman") ≈ embedding("queen").

Architecture Context: In a modern full-stack AI system, you rarely train embeddings from scratch. You use a pre-trained model (like OpenAI's text-embedding-ada-002, Cohere's Embed, or open-source Sentence Transformers). Your application has two halves that must stay symmetric — survey the city once at index time, then ask for directions the same way at query time:

Tip: You will almost never train the surveyor. Your job is picking one, calling it identically on both sides of the search, and indexing what it returns.

Patterns You Should Know

1. Semantic Search with a Vector Database

This is the most common production pattern. You convert your corpus (documents, products, user profiles) into embeddings and store them in a dedicated vector database (e.g., Pinecone, Weaviate, pgvector). At query time, you embed the user's search query and perform a nearest-neighbor search.

javascript
// Node.js example using OpenAI's API and Pinecone
import { OpenAIEmbeddings } from '@langchain/openai';
import { Pinecone } from '@pinecone-database/pinecone';

const embedder = new OpenAIEmbeddings({ model: 'text-embedding-ada-002' });
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pinecone.Index('product-catalog');

// Indexing: Store product embeddings
async function indexProduct(product) {
  const vector = await embedder.embedQuery(product.description);
  await index.upsert([{
    id: product.id,
    values: vector,
    metadata: { title: product.title, category: product.category }
  }]);
}

// Querying: Semantic search
async function semanticSearch(userQuery, topK = 5) {
  const queryVector = await embedder.embedQuery(userQuery);
  const results = await index.query({
    vector: queryVector,
    topK,
    includeMetadata: true,
  });
  return results.matches; // Returns products semantically closest to the query
}

2. Embedding Caching for Cost & Latency Optimization

Calling external embedding APIs (OpenAI, Azure OpenAI) for every request is expensive and slow. A critical production pattern is to cache embeddings for immutable or slowly-changing data — save the coordinates in your contacts instead of paying the surveyor to measure the same building twice.

javascript
// Pattern: Cache embeddings in Redis or your primary database
import { Redis } from 'ioredis';
const redis = new Redis();

async function getOrCreateEmbedding(text, model = 'ada-002') {
  const cacheKey = `embed:${model}:${hash(text)}`; // Use a stable hash (e.g., SHA-256)

  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Cache miss: call API
  const embedding = await openai.embeddings.create({
    model: `text-embedding-${model}`,
    input: text,
  });
  const vector = embedding.data[0].embedding;

  // Store with a TTL (or forever if data is static)
  await redis.set(cacheKey, JSON.stringify(vector), 'EX', 86400); // 24h TTL
  return vector;
}

Tip: Cache key = model name + content hash. The same sentence has different coordinates on different maps — a cache that ignores the model serves pins from the wrong map.

3. Hybrid Search (Combining Vector + Keyword)

Pure vector search can sometimes miss on exact keyword matches or be influenced by irrelevant semantic noise. Hybrid search combines the strengths of both: the map for meaning, the street signs for exact names.

javascript
// Conceptual flow for a hybrid search service
async function hybridSearch(query, vectorWeight = 0.7, keywordWeight = 0.3) {
  // 1. Parallel Fetches
  const [vectorResults, keywordResults] = await Promise.all([
    vectorIndex.query(query), // Returns [{id, score: cosine_sim}]
    elasticSearch.query(query), // Returns [{id, score: BM25}]
  ]);

  // 2. Normalize scores (e.g., Min-Max scaling per result set)
  const normalizedVectorScores = normalizeScores(vectorResults, 'score');
  const normalizedKeywordScores = normalizeScores(keywordResults, '_score');

  // 3. Fuse scores using Reciprocal Rank Fusion (RRF) or weighted sum
  const fusedScores = new Map();
  for (const res of vectorResults) {
    const fused = (vectorWeight * normalizedVectorScores.get(res.id)) + (keywordWeight * (normalizedKeywordScores.get(res.id) || 0));
    fusedScores.set(res.id, { ...res, fusedScore: fused, type: 'hybrid' });
  }
  // ... similar for keywordResults not already in map

  // 4. Sort by fused score and return
  return Array.from(fusedScores.values())
    .sort((a, b) => b.fusedScore - a.fusedScore)
    .slice(0, 10);
}

Key Takeaway: Vector search finds "near the courthouse"; keyword search finds "Article 4, Clause B". Production search almost always needs both.

4. Generating Embeddings for Microservices in a Batch Pipeline

In a microservices architecture, you often have a dedicated service or pipeline job that generates and updates embeddings, decoupling it from real-time request flows.

yaml
# Kubernetes CronJob manifest for a nightly embedding update pipeline
apiVersion: batch/v1
kind: CronJob
metadata:
  name: embedding-pipeline
spec:
  schedule: "0 2 * * *" # Run at 2 AM daily
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: pipeline-worker
            image: your-ai-pipeline:latest
            env:
            - name: SOURCE_DB_URL
              valueFrom: { secretKeyRef: { name: db-secret, key: url } }
            - name: VECTOR_DB_API_KEY
              valueFrom: { secretKeyRef: { name: pinecone-secret, key: api-key } }
            command: ["node", "scripts/batchEmbeddings.js"]

The batch script (batchEmbeddings.js) would:

  1. Query the source SQL/NoSQL DB for new or updated items since the last run.
  2. Chunk large texts (e.g., documents) to fit the embedding model's context window.
  3. Call the embedding API in batched requests for efficiency.
  4. Upsert the new vectors into the vector database.
  5. Log metrics (items processed, API latency, cost) to CloudWatch/Azure Monitor.

5. Symmetric vs. Asymmetric Search — Match the Map to the Trip

Not every search compares like with like, and the difference decides which model you need. Symmetric search compares two things of the same kind and length — "find sentences like this sentence", deduplication, clustering. One plain similarity model embeds both sides, the same way one map answers "find a café like this café".

Asymmetric search is a tourist's five-word question that must land near a guidebook's full page: a short query has to match a long passage. The two distributions are different, so a plain similarity model often places them poorly relative to each other. Use a model trained on (question, passage) pairs — bi-encoders trained on datasets like MS MARCO, or e5-style models that expect query: / passage: prefixes. Omitting the prefix is the same class of bug as a pooling mismatch: nothing throws, scores look plausible, recall quietly drops. For the last mile of precision, re-rank the top-k with a cross-encoder.

Tip: Ask "do my queries look like my documents?" If a five-word question must find a 500-word passage, you need a map surveyed on question-to-place pairs — not a plain similarity model.

Generating embeddings with the OpenAI Python SDK

The patterns above use JavaScript. Here is the equivalent in Python, the more common ML runtime, using text-embedding-3-small — a good default for most RAG pipelines:

python
from openai import OpenAI
import numpy as np

client = OpenAI()  # reads OPENAI_API_KEY from env

def embed(texts: list[str], model: str = "text-embedding-3-small") -> list[list[float]]:
    """Batch-embed a list of strings. Returns one vector per input."""
    response = client.embeddings.create(input=texts, model=model)
    # response.data is a list ordered by index — sort to be safe
    return [item.embedding for item in sorted(response.data, key=lambda x: x.index)]

# Embed a knowledge base (batch call — one API round-trip)
docs = [
    "RAG combines retrieval with generation to ground LLM responses.",
    "HNSW builds a multi-layer navigable graph for fast ANN search.",
    "Embeddings map discrete tokens to a continuous vector space.",
]
doc_vectors = embed(docs)

# Embed a query and compute cosine similarity
query_vec = np.array(embed(["How does approximate nearest-neighbor search work?"])[0])
doc_vecs = np.array(doc_vectors)

# Vectors are already L2-normalized by the API, so cosine sim == dot product
similarities = doc_vecs @ query_vec
top_idx = int(np.argmax(similarities))
print(f"Most relevant: {docs[top_idx]}  (score={similarities[top_idx]:.4f})")

The vectors returned by text-embedding-3-small are 1536-dimensional floats in [-1, 1], already normalized. For higher accuracy at higher cost, substitute text-embedding-3-large (3072 dimensions). For a fully self-hosted, cost-free option, use sentence-transformers with all-MiniLM-L6-v2 (384 dimensions) — the tradeoff is lower quality on out-of-domain text. Think of dimensionality as map resolution: the 1:1000 survey captures every alleyway but is heavy to store and slow to search; the pocket map is often enough.

What Interviewers Actually Ask

Q: Explain how a model like Word2Vec or BERT creates a meaningful embedding for a word. A: Word2Vec uses a shallow neural network with a simple objective: predict context words (Skip-gram) or a target word from context (CBOW). By training on a massive corpus, the model adjusts the weights in its embedding layer so that words with similar linguistic contexts end up with similar weight vectors. BERT, being a transformer, creates contextual embeddings. The same word will have different embeddings based on its surrounding sentence, because BERT uses self-attention to process the entire input sequence simultaneously, allowing the representation of "bank" in "river bank" to differ from "bank" in "bank deposit."

Q: When would you choose a sparse embedding (like TF-IDF or BM25) over a dense embedding? A: I'd choose sparse embeddings when interpretability and exact keyword matching are critical, and I have a limited, domain-specific dataset. Sparse embeddings are also cheaper to compute and don't require a pre-trained model. They work well for classic search where users rely on specific terms (e.g., legal document search for "Article 4, Clause B"). Dense embeddings are superior for capturing semantic similarity, handling synonyms/rephrasing, and working with pre-trained knowledge from large corpora.

Q: You've deployed a semantic search feature, and users complain that results for specific technical acronyms are poor. What do you check? A: First, I'd verify the embedding model's training data. General-purpose models (trained on Wikipedia, Common Crawl) may not have seen niche acronyms. The solution might be to use a domain-specific model or fine-tune one on our corpus. Second, I'd check if hybrid search is implemented; adding a keyword-matching component would boost exact acronym matches. Third, I'd inspect the search logs to see if the acronym is being chunked incorrectly or if its embedding is being averaged with irrelevant surrounding text.

Q: How do you handle embedding very long documents that exceed the model's context window (e.g., 8192 tokens)? A: The standard pattern is chunking. I'd split the document into semantically coherent chunks (using sentences, paragraphs, or sliding windows with overlap) and embed each chunk separately. For retrieval, I embed the query and find the most relevant chunks. For generating a document-level representation, I might average the chunk embeddings or use a more sophisticated method like clustering the chunk vectors and taking the centroid. It's crucial that the chunking strategy preserves meaning—splitting mid-sentence is usually bad.

Q: In a microservices architecture, which service should own the responsibility of generating and storing embeddings? A: Embedding generation should be owned by a dedicated AI/ML Pipeline Service or a Feature Store, not the core business logic services. This separates concerns: product services manage canonical data, while the pipeline service listens for change events (via a message queue like Kafka) or runs batch jobs to compute embeddings and write them to a vector DB. This keeps the latency and complexity of model inference out of the critical user request path.

Q: How would you monitor the performance and cost of an embeddings-based feature in production? A: I'd track four key areas: 1) Quality: Compute and log metrics like Mean Reciprocal Rank (MRR) or NDCG on sampled queries using human-labeled relevance data. 2) Latency: P95/P99 latency for the embedding API call and the vector DB query. 3) Cost: Meter and alert on the number of embedding API tokens consumed per day, as this is the primary cost driver. 4) Operational Health: Monitor vector DB connection pools, memory usage, and index build times. All this would go into dashboards (Grafana) and an observability platform (Datadog).

Q: Explain the difference between symmetric and asymmetric semantic search. When would you use each? A: (You met this in Pattern 5.) In symmetric search, the query and the documents are of the same nature and length (e.g., finding similar sentences, deduplication). You can use the same model to embed both. In asymmetric search, the query is short and the documents are long (e.g., a question vs. answer paragraphs, a search query vs. product descriptions). Here, you often need a model trained specifically for this task (like a bi-encoder trained on (question, passage) pairs) or a cross-encoder for re-ranking, as a standard embedding model may not align the two different distributions well in the vector space.

Q: How do embeddings fit into a modern Retrieval-Augmented Generation (RAG) system? A: Embeddings are the retrieval engine of RAG. When a user query comes in, it's converted into an embedding. This embedding is used to perform a nearest-neighbor search over a vector database containing pre-computed embeddings of your knowledge base (chunked documents). The top-k most semantically relevant document chunks are retrieved and then injected as context into the prompt for a Large Language Model (LLM). The LLM then generates an answer grounded in that retrieved context. The quality of the embeddings directly determines the relevance of the retrieved context, which is the biggest lever for improving RAG accuracy.

How It Connects to This Role's Stack

  • Node.js: This is your runtime for building lightweight, efficient services that interact with embedding APIs (using the OpenAI SDK, @langchain/community) and vector databases. You'll build REST/gRPC endpoints for search and retrieval.
  • AWS/Azure: You'll leverage managed services to offload complexity. On AWS, you might use Bedrock for access to embedding models, OpenSearch (with k-NN plugin) as a vector store, and Lambda for serverless embedding generation. On Azure, you'd use Azure OpenAI Service for embeddings, Azure AI Search for vector/hybrid search, and Azure Functions for pipeline logic. Both clouds provide monitoring (CloudWatch, Azure Monitor) for the critical metrics mentioned.
  • CI/CD: Your embedding pipeline code and the services that consume vectors are part of the same codebase and CI/CD flow. You need to version your embedding models in the pipeline (e.g., text-embedding-ada-002 vs -3) just like any other dependency, as a change can alter your entire vector space and require a re-indexing step in deployment. A model bump is a map redraw: the deploy that ships it must include the re-index step.
  • Microservices: The "Embedding Service" or "Vector Search Service" is a prime candidate to be its own microservice. It exposes a clean API (POST /embed, GET /search) and encapsulates all logic related to model choice, chunking, caching, and vector DB interactions. This follows the Single Responsibility Principle.
  • Kubernetes: You'll deploy your embedding pipeline as a CronJob and your vector search service as a Deployment with horizontal pod autoscaling based on QPS. You'll manage secrets for API keys via Secrets, and configs for model parameters via ConfigMaps. The vector database client in your service needs to be configured for connection pooling suitable for the Kubernetes environment.

Runtime Internals

The map metaphor hides the mechanics that decide whether retrieval actually works in production.

Pooling: token vectors → one vector

A transformer emits one vector per token; an embedding is a single vector, so a pooling step (mean-pool, CLS, or last-token) collapses them. On the map: a campus has many buildings, but it gets one pin — either its geographic center (mean-pool) or its main entrance (CLS). Both are defensible; what kills you is dropping corpus pins by one rule and query pins by the other.

Concretely: index your corpus with sentence-transformers/all-MiniLM-L6-v2 (mean-pooling is baked into its pipeline), then embed queries by calling the raw HuggingFace model and taking the [CLS] token. Both produce 384-dim vectors, nothing throws, cosine scores still look plausible (0.3–0.6) — but recall@10 collapses, because every query pin was placed by a different rule than the corpus pins.

Warning: Pooling mismatch throws no error. Dimensions match, scores look plausible, and recall is quietly destroyed — the worst kind of production bug.

Normalization and the metric

Cosine similarity is just a dot product on L2-normalized vectors. If you normalize at index time but not query time (or mix metrics), similarity scores become meaningless — it's measuring in miles on a map drawn in kilometers. The rule: pick one metric, normalize consistently on both sides.

Key Takeaway: Pick one metric, normalize on both sides, forever. Cosine similarity is just a dot product on unit-length vectors.

ANN index: trading recall for speed

Exact nearest-neighbor is O(N); production uses ANN (HNSW/IVF) which trades a little recall for huge speed — asking a well-connected local instead of measuring the distance to every address. The local occasionally misses the true closest café, but answers instantly. The runtime knobs (HNSW ef, IVF nprobe) are a recall/latency dial you must tune against real query load: ef=16 might give ~0.85 recall at 1 ms, ef=200 ~0.99 at 10 ms (numbers illustrative — measure on your own load).

Tip: HNSW ef and IVF nprobe are one dial: left for speed, right for recall. Tune it against your real queries, not the library default.

Re-embedding and drift

Embeddings are tied to a model version. Upgrading the model means re-embedding the entire corpus — a new surveyor redraws the map, and query and index vectors from different model versions are not comparable. This is the operational cost the chunking strategies decision compounds, since changing chunking also forces a full re-embed. The safe upgrade is a parallel map, not an in-place redraw:

Red Flags to Avoid

  • "We can just train our own embeddings from scratch." For almost all real-world applications, this is a massive waste of time and compute. Acknowledge the power of pre-trained models. You don't found a survey office; you hire one.
  • "Cosine similarity and Euclidean distance are interchangeable." They are not. For normalized embeddings (which is common), cosine similarity measures the angle and is invariant to magnitude, making it better for semantic similarity. Euclidean distance is sensitive to magnitude. Know which one your vector DB uses by default — it's the map's projection, not a detail.
  • "Embeddings are the model." No, embeddings are a representation or a layer of a model. The model (Word2Vec, BERT, etc.) is the system that learns or generates the embeddings. The pin is not the surveyor.
  • Not considering dimensionality. Blithely saying "we'll use 1536-dim embeddings" without considering the trade-offs: higher dimensions capture more nuance but increase storage cost, memory usage, and query latency. For some tasks, a 384-dim model might be sufficient and much faster. Pick the map resolution the trip needs.
  • Ignoring the cold-start/hydration problem. Talking about a recommendation system that uses user embeddings without explaining how you get an embedding for a new user who has no interaction history. You need a strategy, like using a default embedding or using side-information (demographics) to create an initial embedding — a provisional pin dropped from the neighborhood.
  • "We'll just embed the whole PDF." This shows a lack of understanding of context windows and chunking. Always think about the input limits of the model and the need to preserve semantic coherence in chunks. One pin for a whole city block means nobody finds suite 4B.
  • Treating the vector DB as a primary database. It's a specialized index for search. The canonical data should live elsewhere (SQL/NoSQL DB). The vector DB contains derived data (embeddings) and metadata, which can be rebuilt from the source if lost. The map is not the territory.

Warning: The vector DB is the map, not the territory. If you can't rebuild it from your source-of-truth DB, you've made a disposable index your canonical store.

The One-Page Recap: COMPASS

A compass belongs on a map. Seven rules cover everything in this lesson that breaks in production:

  1. C — Chunk before you pin. One address per coherent idea; never "embed the whole PDF."
  2. O — One metric, normalized both sides. Same projection at index time and query time, or the scores are noise.
  3. M — Map is derived data. Canonical data lives in the source DB; the vector index can always be rebuilt.
  4. P — Pooling must match. Mean vs CLS is not cosmetic — a mismatch throws no error and silently kills recall.
  5. A — ANN trades recall for speed. HNSW/IVF is a dial you tune against real load, not magic.
  6. S — Same surveyor on both sides. Query and index vectors must come from the same model version; upgrading means re-embedding everything.
  7. S — Sparse + dense beats either. The map for meaning, the phone book for exact terms.

And the full map legend, for review:

ConceptOn the map
Embedding modelThe surveyor who assigns coordinates
An embeddingA pin on the map
Training objectiveThe surveying method — what "near" was calibrated to mean
Vector databaseThe map index that answers "what's near this pin?"
PoolingA campus gets one pin — geographic center (mean) or main entrance (CLS)
Normalization + metricThe projection and units — mix them and numbers mean nothing
ANN (HNSW/IVF)A well-connected local instead of measuring every address
Re-embeddingA new surveyor redraws the map — every old pin is invalid
ChunkingWhat counts as one address — a block, a building, a doorknob
Hybrid searchMap + street signs — proximity for meaning, exact names for exact terms
Sparse vs densePhone book vs map
Symmetric vs asymmetricA café like this café vs a tourist's 5-word question matching a guidebook page
CachingSaving coordinates in your contacts instead of paying the surveyor twice
Cold startA brand-new building — drop a provisional pin from its neighborhood
Vector DB is derivedThe map is not the territory — lose the map, re-survey from the buildings
DimensionalityMap resolution — the full survey vs the pocket map

Key Takeaway: If you remember one sentence: embeddings are a learned map where distance means meaning — and everything that goes wrong in production is someone mixing maps.

References & further reading
Checkpoint — answer before revealing1 of 4
What is the core mental model for embeddings, and what does every downstream task reduce to?
Put this into practiceRecalling beats rereading — retrieval practice is the best-supported technique in the evidence base.

Flashcards for this topic

Explain it in your own words while the tutor is still here. This score becomes the baseline your closed-book check is measured against tomorrow.
← PreviousAdversarial Prompting: Jailbreaks, Injections & Defense StrategiesFrom: 💬 Phase 2 · Prompting & Structured OutputNext →Embedding Models: Representations, Similarity & Fine-tuning