Bringing Generative AI to Production
Moving generative AI products from proof-of-concept to production is proving harder than many expected. The difficulties often stem from treating these systems as extensions of traditional transactional or analytical software. GenAI introduces a distinct set of problems: hallucination, unbounded data access, and non-determinism.
Across client engagements, we've observed recurring patterns for addressing these issues. These are early days for the technology, and none of these patterns are gold standards. Understanding when to use a particular approach is often more valuable than knowing how it works.
| Direct Prompting | Send prompts directly from the user to a Foundation LLM |
| Embeddings | Transform large data blocks into numeric vectors so that embeddings near each other represent related concepts |
| Evals | Evaluate the responses of an LLM in the context of a specific task |
| Fine Tuning | Carry out additional training to a pre-trained LLM to enhance its knowledge base for a particular context |
| Guardrails | Use separate LLM calls to avoid dangerous input to the LLM or to sanitize its results |
| Hybrid Retriever | Combine searches using embeddings with other search techniques |
| Query Rewriting | Use an LLM to create several alternative formulations of a query and search with all the alternatives |
| Reranker | Rank a set of retrieved document fragments according to their usefulness and send the best of them to the LLM. |
| Retrieval Augmented Generation (RAG) | Retrieve relevant document fragments and include these when prompting the LLM |
Direct Prompting and Its Limits
The most basic LLM pattern is connecting an off-the-shelf foundation model directly to the user. The user types prompts and receives responses with no intermediate steps — the experience LLM vendors themselves offer.
This works in many contexts and initially triggered widespread excitement. But direct prompting has significant limitations. The model's knowledge is frozen at its training cutoff and cannot incorporate events or proprietary data that occurred after. Even within its training data, the model lacks the operational context needed to prioritize the most relevant knowledge for a given situation.
Behavioral concerns compound these knowledge constraints. Malicious prompts may trick the model into divulging confidential information or producing misleading responses. LLMs project confidence even when their knowledge is weak, freely fabricating plausible but incorrect answers. This is amusing in a toy demo but a serious liability when the model acts as an organization's public-facing interface.
For our clients, direct prompting alone almost never suffices in production. It requires supporting measures to handle these shortcomings. The first step is systematically evaluating how good the model's results actually are. Traditional software engineering has taught us the value of rigorous testing; for GenAI, this translates into a methodical approach for assessing response quality and verifying that enhancements genuinely improve performance.
Evals: The Testing Layer for GenAI
Evals are the cornerstone of production GenAI quality control. They provide a systematic way to ensure non-deterministic systems operate within sensible boundaries.
Evaluation approaches fall into two broad categories: scoring and judging. Scoring applies deterministic metrics to responses — measuring factual accuracy, relevance, or similarity to expected outputs. Judging uses another LLM to assess response quality against defined criteria, a technique that scales well but introduces its own non-determinism.
A robust eval setup typically involves a curated dataset of prompts with expected behavior, run through an evaluation pipeline on every significant change to the system. This becomes the regression suite for the LLM layer, catching degradations that prompt tweaks or model updates can silently introduce.
Evals and Benchmarks
General-purpose LLM benchmarks measure raw model capability. Evals, in contrast, measure whether a model performs well within your specific system — your data, your prompts, your success criteria. Treating public benchmarks as a substitute for custom evals is a common production mistake. Benchmarks tell you how the model performs in general; evals tell you how it performs for your users.
Enhancing the Base Model
Even with solid evals in place, the underlying LLM lacks the information your product needs. Two enhancement patterns address this:
- Retrieval Augmented Generation (RAG) gives the model access to external data at query time, grounding its responses in content outside the static training set.
- Fine-tuning modifies the model's weights using task-specific training data, changing the model itself rather than its inputs.
RAG is the more common starting point and generally the preferred approach. It avoids the cost and complexity of retraining while providing a mechanism for keeping responses current. Basic RAG, however, has limitations that several supporting patterns address.
The Basic RAG Pattern and Its Variations
The core RAG pipeline has a well-known structure: at query time, relevant documents are retrieved from a knowledge base, inserted into the prompt as context, and the LLM generates its answer grounded in that supplied material. This addresses the training-cutoff problem by giving the model current, domain-specific content to work with.
Hybrid Retrieval
The naive RAG implementation typically relies on a single retrieval mechanism, usually embeddings-based vector search. Vector search alone struggles with with exact keyword matches, acronyms, and queries where lexical precision matters. A hybrid retriever combines multiple retrieval strategies — such as text-based and vector-based search — merging results from each into the context. The different recall characteristics compensate for each other's blind spots.
Query Rewriting
Users frame queries conversationally, often referencing prior turns or using ambiguous phrasing. Retrieval systems expect well-formed search queries. Query rewriting asks an LLM to transform the user's raw request into an optimized retrieval form before executing the search. A follow-up question like "And its pricing?" becomes a self-contained query that includes the full context of the conversation.
Reranking
Retrievers return documents they predict to be relevant; relevance as measured by the retriever frequently differs from what actually helps the LLM produce a good answer. A reranker — either an LLM or a dedicated cross-encoder model — sits after initial retrieval. It re-examines the candidate documents against the query and reorders them, allowing downstream steps to focus on the most valuable content. This catches cases where the top retrievals miss crucial context buried slightly lower in the results.
Guardrails
LLM outputs can be misleading, abusive, or dangerous. Guardrails control what the model is allowed to do with and to the user — validating outputs, restricting topics, preventing the leakage of restricted information, and blocking abusive requests.
Effective implementations layer multiple guardrail types:
- LLM-based guardrails ask a second model to check the primary model's output for compliance with stated constraints.
- Embeddings-based guardrails compare incoming and outgoing content against known-dangerous or restricted inputs via vector similarity.
- Rule-based guardrails apply deterministic checks — regex, list matching, content filters — that require no model inference and operate with predictable latency.
Each approach accepts tradeoffs. LLM-based guardrails handle nuance but add inference cost and latency. Rule-based guardrails are instant and reliable but brittle. Embeddings-based approaches sit between, catching semantic analogies that exact rules miss.
A Realistic RAG Stack
Putting the individual patterns together produces the RAG pipeline that survives production contact with real users. A typical deployment combines:
- A hybrid retriever spanning both keyword and semantic search
- Query rewriting to handle ambiguous conversational requests
- A reranker to sharpen retrieval precision before context assembly
- Guardrails at both boundaries — filtering inputs and moderating outputs
- A continuous eval suite that measures each component's contribution
Each added component increases system complexity and latency. The eval suite is what justifies their inclusion — a component earns its place by measurably improving response quality against the defined success criteria.
Fine-tuning When RAG Is Not Enough
RAG won't always suffice. Fine-tuning modifies the model's weights on task-specific data and becomes worthwhile when:
- The model needs to adopt a particular style, tone, or domain vocabulary that prompting cannot reliably elicit.
- The required knowledge is stable and confined, making retrieval overkill.
- Latency or cost constraints make round-trip retrieval unattractive.
- Prompt-based attempts at steering behavior repeatedly fail evals.
Fine-tuning also risks embedding outdated knowledge into the weights — the same static-data problem that prompted RAG in the first place. It pairs well with RAG but rarely fully replaces it when the knowledge base keeps changing.
Ongoing Work
These patterns capture what our colleagues have observed across production engagements. The field moves quickly, and tools continue to arrive. Some potentially valuable approaches are either untested in our engagements or too fresh to yield discernible patterns yet. As further experience accumulates, these patterns will inevitably get revised and extended.
Evaluating LLM Behavior
Traditional software is tested against expected outputs; we feed in carefully chosen inputs and verify the system responds deterministically. LLM-based systems break that contract — the same prompt can produce different results on repeated requests. That doesn't mean behavior is beyond scrutiny, but it does change how we approach verification.
In GenAI we talk about evals rather than tests. Instead of checking a single response, evals assess model behavior across a range of scenarios. The goal is to confirm that the system handles all anticipated situations and that outputs meet a desired standard.
Scoring and Judging
Evals rely on a scorer — a component or function that converts generated outputs into numerical scores. Those scores reflect metrics such as relevance, coherence, factuality, or semantic similarity between the model output and an expected answer. Typical inputs to the scorer include the model input and output, the expected output, retrieval context from a RAG pipeline, and the metrics to evaluate.
The scorer produces a performance score, a ranking of results, and additional feedback, which can take the form of:
- Self-evaluation: The LLM assesses its own responses. Some models do this better than others, but the technique is risky: if the model's internal assessment is flawed, it may reinforce errors or biases in subsequent iterations, producing outputs that look more confident or refined than they actually are. We strongly recommend exploring other strategies.
- LLM as a judge: A separate model — either a more capable LLM or a specialized Small Language Model (SLM) — scores the output. Using a different model addresses the core weakness of self-evaluation, since the chance of both models sharing the same errors or biases is low. This has become the standard choice for automating the evaluation process.
- Human evaluation: Often called "vibe checking," this informal technique verifies that responses match the desired tone, style, and intent. Humans write prompts manually and review the output. It is hard to scale but remains the most reliable way to catch qualitative elements that automated methods miss.
In our experience, combining LLM-as-a-judge with human evaluation delivers the fullest picture of how your LLM performs on the aspects that matter for your product. The pairing leverages automated judgment for coverage and human insight for nuance.
Evaluation in Practice
The following example uses DeepEval to test response relevancy for a nutrition application:
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric
def test_answer_relevancy():
answer_relevancy_metric = AnswerRelevancyMetric(threshold=0.5)
test_case = LLMTestCase(
input="What is the recommended daily protein intake for adults?",
actual_output="The recommended daily protein intake for adults is 0.8 grams per kilogram of body weight.",
retrieval_context=["""Protein is an essential macronutrient that plays crucial roles in building and
repairing tissues.Good sources include lean meats, fish, eggs, and legumes. The recommended
daily allowance (RDA) for protein is 0.8 grams per kilogram of body weight for adults.
Athletes and active individuals may need more, ranging from 1.2 to 2.0
grams per kilogram of body weight."""]
)
assert_test(test_case, [answer_relevancy_metric])
This test embeds the LLM response and measures its relevance score directly. Teams can also add integration tests that generate live LLM outputs and measure them across a number of pre-defined metrics.
Running Evals in the Pipeline
Like tests, evals run as part of the build pipeline. Unlike tests, they don't produce a simple pass/fail result. Evals require defined thresholds and checks designed to catch performance decline. In practice it is often most useful to treat evals like performance testing rather than unit testing.
Evals are not just pre-deployment activities. A live GenAI system can drift in performance while serving production traffic, so the system needs regular evaluation after deployment to flag any drop in scores.
Evaluation applies at every level. Guardrails and Query Rewriting contain logically distinct LLMs, so they can and should be evaluated individually as well as within the context of the full request flow.
Evals vs. Benchmarking
Benchmarking establishes a baseline for comparing the output of LLMs on a well-defined set of tasks. The goal is to minimize variability: standardized datasets, defined tasks, and established metrics consistently track model performance over time. When a new model version ships, benchmarks allow teams to compare metrics and make an informed decision about upgrading.
Benchmarking is largely the domain of LLM creators assessing overall model quality. As a GenAI product owner, you can use public benchmarks to gauge general model capability, but targeted evals are required to determine whether a model suits your specific problem. Evals operate without industry-standard datasets — you build your own tailored to your use case.
When Evals Are Necessary
Assessing the accuracy and value of any software system is important; letting users make bad decisions based on system behavior is the failure mode to avoid. Eval methodology is still early in its evolution — the mechanisms for scoring and judging remain unsettled. Despite that, evals are crucial whenever an LLM powers a system that operates beyond constrained scenarios where users will apply appropriate skepticism to the output.
Evals give us a way to examine the broad behavior of a generative AI system, but they don't tell us how to structure that behavior in the first place. The next step is understanding a foundational requirement for generative and other AI systems: how they handle, and reason over, the vast datasets they were trained on — the groundwork for every decision they make.
Embeddings: Making High-Dimensional Data Usable
Modern AI applications often need to reason about large, unstructured inputs — photos, documents, or multi-page reports. A single image captured at 1280 by 960 resolution contains approximately 3.6 million pixel values (1280 × 960 × 3 for RGB). Analyzing patterns directly in such a high-dimensional space is impractical, even for the most capable models.
An embedding is a lossy compression of that data into a numeric vector with several hundred elements. The transformation is designed so that similar inputs — images, text, or other content — map to vectors that are close to one another in this hyper-dimensional space. This property enables efficient similarity comparisons at scale.
A Practical Example: Image Similarity
Deep learning models produce more effective image embeddings than hand-crafted approaches. Using the clip-ViT-L-14 model (a CLIP variant), we can generate embeddings for images with a few lines of code.
# python
from sentence_transformers import SentenceTransformer, util
from PIL import Image
import numpy as np
model = SentenceTransformer('clip-ViT-L-14')
apple_embeddings = model.encode(Image.open('images/Apple/Apple_1.jpeg'))
print(len(apple_embeddings)) # Dimension of embeddings 768
print(np.round(apple_embeddings, decimals=2))
Running this prints the length of the embedding vector followed by the vector itself:
768
[ 0.3 0.25 0.83 0.33 -0.05 0.39 -0.67 0.13 0.39 0.5 # and so on...
768 numbers represent a far more manageable dataset than the original 3.6 million pixels. To test the hypothesis that similar images cluster together in this vector space, we need a distance metric. Common options include cosine similarity and Euclidean distance. For a nutrition app scenario, cosine similarity is a natural fit.
Cosine similarity values range from -1 to 1, and the score between two embeddings is computed as:
| cosine value | vectors | result |
|---|---|---|
| 1 | perfectly aligned | images are highly similar |
| -1 | perfectly anti-aligned | images are highly dissimilar |
| 0 | orthogonal | images are unrelated |
def cosine_similarity(embedding1, embedding2): embedding1 = embedding1 / np.linalg.norm(embedding1) embedding2 = embedding2 / np.linalg.norm(embedding2) cosine_sim = np.dot(embedding1, embedding2) return cosine_sim
Comparing a single apple image against three apple variations and a burger yields clear results:
| image | cosine_similarity | remarks |
|---|---|---|
| apple 1 | 1.0 | same picture, so perfect match |
| apple 2 | 0.9229323 | similar, so close match |
| apple 3 | 0.8406111 | close, but a bit further away |
| burger | 0.58842075 | quite far away |
In practice, real-world variations are abundant — cut apples, apples on a plate, green apples, or top-down views. A good embedding model encodes these meaningful variations such that semantically similar images remain in close proximity.
Visualizing clusters confirms this behavior. Though models operate comfortably in hundreds of dimensions, visualization requires a reduction to two or three dimensions. Techniques like T-SNE or UMAP serve this purpose:
from sklearn.manifold import TSNE tsne = TSNE(random_state = 0, metric = 'cosine',perplexity=2,n_components = 3) embeddings_3d = tsne.fit_transform(array_of_embeddings)
Using a fruit classification dataset, the three-dimensional projection shows that the embeddings model does a solid job of clustering similar images together.
Text works the same way. Whether it's a chunk of text or a multi-page document, an embedding model converts the content into a comparable vector space. Critically, these models understand context rather than raw token counts — "Mary had a little lamb" means one thing to a storyteller and another to a restaurateur. Models such as text-embedding-3-large and all-MiniLM-L6-v2 capture complex semantic relationships between words and phrases.
Embeddings Inside an LLM
LLMs are specialized neural networks built on the Transformer architecture. Conceptually, they divide into an input layer, multiple hidden layers, and an output layer. A significant portion of the input layer consists of embeddings for the model's vocabulary — sometimes called internal, parametric, or static embeddings.
In the context of a nutrition app that analyzes meal photos, the pipeline works as follows: the tokenizer converts the user's prompt text and images to embeddings at the input layer; the hidden attention layers extract relevant features, analyzing the input from nutritional and health perspectives; the final hidden state then predicts the output.
When to Use Embeddings
Embeddings capture meaning in a way that supports semantic similarity comparisons across text, images, and other data. Unlike surface-level keyword or pattern matching, they encode deeper contextual relationships. Generating embeddings requires running specialized AI models — typically smaller and more efficient than full LLMs — and similarity comparisons are cheap afterward, relying on vector operations like cosine similarity.
That said, embeddings are not the right tool for structured or relational data where exact matching or traditional query syntax is appropriate. Exact-match lookups, numerical comparisons, and relational queries belong in SQL and conventional databases, not vector stores.
Retrieval Augmented Generation (RAG)
A common metaphor for an LLM is a well-read junior researcher: articulate and broadly knowledgeable but patchy on specifics, prone to inventing plausible answers when uncertain. With RAG, we hand that researcher a dossier of the most relevant documents and instruct them to consult it before responding.
RAG is an effective way to give an LLM specialized, current, or private knowledge. However, it raises classic information retrieval (IR) challenges: how do we find the right documents to include?
The standard approach is to build an index over documents using embeddings, then search that index for the user's query. The process works in two phases:
- Build the index: Divide documents into chunks, generate embeddings for each chunk, and store the chunks and their embeddings in a vector database.
- Handle requests: Create an embedding for the user's query, run an ANN similarity search on the vector store, combine the top results with the original prompt using a RAG prompt template, and send the full input to the LLM.
RAG Prompt Template
Once documents are retrieved, they must be combined with the user's prompt in a structured way. A typical template includes the user query, the retrieved context, and explicit instructions that tell the model to ground its response in the supplied material.
A template might look like:
User prompt: {{user_query}}
Relevant context: {{retrieved_text}}
Instructions:
- 1. Use the provided context to deliver a comprehensive, accurate answer to the user query.
- 2. If the context is sufficient, focus on precise and relevant information.
- 3. If the context is insufficient, say so and suggest possible sources or next steps for obtaining more information.
- 4. Avoid introducing unsupported information or speculation.
When to Use RAG
RAG Is More Than Retrieval Plus Generation
The basic RAG recipe—embed a corpus, retrieve relevant chunks, and stuff them into an LLM prompt—works well for unstructured prose. In practice, however, moving from a proof-of-concept to a production system reveals several gaps. A recent engagement for a multinational life sciences company illustrates the point. Researchers needed to query 17,000 reports compiled over two decades, each containing thousands of pages of mixed text and tables. What used to take days or weeks of manual PDF sifting now takes minutes with a multi-hop chatbot. Getting to that point required tackling four recurring limitations.
| Limitation | Mitigating Pattern | |
|---|---|---|
| Inefficient retrieval | When you're just starting with retrieval systems, it's a shock to realize that relying solely on document chunk embeddings in a vector store won’t lead to efficient retrieval. The common assumption is that chunk embeddings alone will work, but in reality it is useful but not very effective on its own. When we create a single embedding vector for a document chunk, we compress multiple paragraphs into one dense vector. While dense embeddings are good at finding similar paragraphs, they inevitably lose some semantic detail. No amount of fine-tuning can completely bridge this gap.2 | Hybrid Retriever |
| Minimalistic user query | Not all users are able to clearly articulate their intent in a well-formed natural language query. Often, queries are short and ambiguous, lacking the specificity needed to retrieve the most relevant documents. Without clear keywords or context, the retriever may pull in a broad range of information, including irrelevant content, which leads to less accurate and more generalized results. | Query Rewriting |
| Context bloat | The Lost in the Middle paper reveals that LLMs currently struggle to effectively leverage information within lengthy input contexts. Performance is generally strongest when relevant details are positioned at the beginning or end of the context. However, it drops considerably when models must retrieve critical information from the middle of long inputs. This limitation persists even in models specifically designed for large context. | Reranker |
| Gullibility | We characterized LLMs earlier as like a junior researcher: articulate, well-read, but not well-informed on specifics. There's another adjective we should apply: gullible. Our AI researchers are easily convinced to say things better left silent, revealing secrets, or making things up in order to appear more knowledgeable than they are. | Guardrails |
Hybrid Retrieval: Combine Vectors with Classical Search
Vector embeddings are powerful for semantic similarity, but they are not the only tool. Mature techniques such as TF/IDF and BM25 match exact terms efficiently and cheaply. Combining these keyword hits with vector search results yields a better set of candidates than either method alone. The trade-off is a larger candidate pool, which a reranker (covered below) can trim.
Hybrid retrieval forces changes to the indexing pipeline. In the life-sciences project, chunking experiments settled on 1000-character chunks with 100 characters of overlap. The team used OpenAI’s text-embedding-3-large model and stored vectors in AWS OpenSearch. A simple JSON document illustrates the additional work:
{
“Title”: “title of the research”,
“Description”: “chunks of the document approx 1000 bytes”
}
For keyword search, indexing title or description as text is sufficient. Vector search on description, however, requires an explicit field to store the embedding:
{
“Title”: “title of the research”,
“Description”: “chunks of the document approx 1000 bytes”,
“Description_Vec”: [1.23, 1.924, ...] // embeddings vector created via embedding model
}
With this schema, queries can target the text index on title and description as well as the description_vec field.
When to Use Hybrid Retrieval
Embeddings fit naturally with LLMs, but they are not always the best representation. In work on legacy code modernization, a Neo4J graph of the Abstract Syntax Tree—annotated with documentation fragments placed via embeddings—proved more effective for representing module dependencies and call relationships than vector search alone.
The lesson: a vector database is just one form of knowledge base. When structure exists in the data—relationships, hierarchies, or call graphs—tease it out and use it to support retrieval. Multiple indexing strategies often beat a single approach.
Query Rewriting: Search with Alternatives
Rephrasing a question often yields different results from an LLM. A query rewriter exploits this by asking an LLM to generate several alternative phrasings, then sends each to the retriever and combines the results—usually passing them through a reranker before building the prompt.
In the life-sciences chatbot, a user might ask whether specific clinical findings appeared in a study. The rewriter generates variations that replace technical terms with synonyms and restructure the sentence:
- Can you provide details on the clinical symptoms reported in research XYZ-1234, including any occurrences of goosebumps, lack of coordination, semi-closed eyelids, or diarrhea?
- In the results of experiment XYZ-1234, were there any recorded observations of hair standing on end, unsteady movement, eyes not fully open, or watery stools?
- What were the clinical observations noted in trial XYZ-1234, particularly regarding the presence of hair bristling, impaired balance, partially shut eyes, or soft bowel movements?
For diverse datasets, 3–5 variations usually work best; simpler datasets may need only up to 3. Tune this with evals to track progress.
When to Use Query Rewriting
This pattern is valuable for complex queries spanning multiple subtopics or domain-specific vocabulary. It costs extra LLM calls for rewriting and additional retrieval calls—in this engagement, five variations with GPT-4o—so the latency and resource overhead must be weighed against retrieval quality.
Reranker: Sort Candidates by Usefulness
Fast retrieval produces broad but noisy results. A reranker applies a more expensive model—typically a cross-encoder such as bge-reranker-large—to score the relevance of the query against each candidate. Running this over the entire vector store is impractical, but it is worthwhile for the few hundred candidates returned by the initial search. The top results go into the prompt, keeping it lean and preventing low-quality context from confusing the LLM.
When to Use a Reranker
Reranking improves answer accuracy whenever the candidate set is too large for the prompt or contains distracting results. It adds a model interaction, increasing latency and cost, so it is less suited to high-traffic applications. A reranker can also encode user preferences: in the life-sciences chatbot, users can specify preferred or avoided conditions, and those preferences factor into the ranking so the final response aligns with their constraints.
Shielding the Model: Guardrails in Practice
Conversational interfaces remove the constraints that traditional form-based UIs impose on user input. Anyone can type anything into a prompt, including attempts at prompt injection like "ignore previous instructions." Even without malicious intent, an LLM might respond with confidential or inaccurate information. Guardrails sit between the user and the model to catch these problems: an input guardrail inspects the user's query for signs of malicious or poorly worded prompts before they reach the conversational LLM, and an output guardrail scans the model's response for content that shouldn't be there.
Guardrails are typically implemented with dedicated platforms, often using their own LLM trained specifically for the task via instruction tuning. This training approach, which uses datasets of instruction-output pairs, helps bridge the gap between an LLM's next-word prediction objective and the goal of following user instructions. For instance, you can self-host a Llama Guard model with NeMo to enforce guardrails while using OpenAI's LLM for the core generative work.
LLM-Based Guardrails
To keep a nutrition app focused strictly on that topic, you could use the self_check_input rail from the NeMo Guardrails framework. The framework wraps the user's prompt inside a template that asks a separate model to classify the input. The template lays out a list of conditions that require blocking the request: harmful data, impersonation requests, attempts to override rules, requests for explicit or abusive content, asks for sensitive personal information, prompts to execute code, attempts to extract system prompts, or garbled language. The model answers "Yes" or "No" as to whether the input should be blocked.
Embeddings and Rule-Based Guardrails
Not all guardrails require an LLM call. Embeddings-based guardrails analyze the semantic similarity of user inputs to enforce topic constraints or ethical guidelines without relying on rigid keyword matching. Semantic Router is one tool that can safely direct user queries to the LLM or reject off-topic requests. For protecting sensitive data, rule-based approaches like Microsoft's Presidio can filter personally identifiable information from the knowledge base before it ever reaches the model.
Weighing the Need for Guardrails
The appropriate level of guardrailing depends on how much you trust your users. Public-facing systems are open doors to anyone with an inclination for mischief, so guardrails are essential. A restricted user base—say, a small group of employees—needs less protection from deliberate abuse, especially if prompts are logged and there are consequences for bad behavior. However, even trusted users need proactive protection from model-generated issues like inappropriate content, misinformation, and unintended biases.
Guardrails aren't free. The extra LLM calls add cost and latency, and setting up and monitoring them takes effort. The decision comes down to weighing those costs against the risk of an incident that guardrails could prevent.
When Prompting and RAG Aren't Enough: Fine-Tuning
RAG covers most generative AI scenarios by supplying specific knowledge to a general-purpose model. But sometimes the required domain knowledge is too broad to fit within a set of retrieved documents. In those cases, you may need to fine-tune the model itself.
Fine-tuning takes a pre-trained model and refines it with additional training on a dataset specific to the task at hand. The model processes each training example, generates a prediction, and measures it against the known correct output. A loss function quantifies the error, and backpropagation adjusts the model's weights to minimize it. Hyper-parameters—learning rate, batch size, number of epochs, optimizer, and weight decay—significantly influence the process, and tuning them is crucial for balancing generalization and stability.
| Full fine-tuning | Full fine-tuning involves taking a pre-trained LLM and training it further on a smaller dataset. This helps the model become better at specific tasks while keeping its original pretrained knowledge. During full fine-tuning, every part of the model is affected, including the input embedding layers, attention mechanisms, and output layers. |
| Selective layer fine-tuning | In the Less is More paper, the authors observe that not all layers in LLM are created equal. As different layers across the network contribute variably to the overall performance, you can achieve drastic improvements in performance by selectively fine tuning the input, attention or output layers. |
| Parameter-Efficient Fine-Tuning (PEFT) | PEFT adds and trains new parameters while keeping the original LLM parameters frozen. It uses techniques like Low-Rank Adaptation (LoRA) or Prompt Tuning to create trainable delta parameters that modify the model's behavior without changing its original base parameters. |
The Aalap project, part of the Opennyai engagement, offers a concrete example. The team fine-tuned a Mistral 7B model on instruction data for legal tasks in the Indian judicial system, choosing LoRA due to a strict budget and limited training data. The fine-tuned model outperformed GPT-3.5-turbo on 31% of the test data. The training itself took about 88 hours, but the project stretched over four months: nearly half the effort went into understanding legal document structures and curating training data.
Choosing the Right Technique
Fine-tuning demands significant skills, compute resources, expense, and time, so it should be a last resort. Start with different prompting techniques—and keep prompt evals in the build pipeline to track progress as models improve. If prompting alone doesn't get you there, move to RAG. In most products, eval metrics are satisfactory once RAG is properly implemented.
Only when eval metrics remain unsatisfactory after optimizing RAG should you consider fine-tuning. In the Aalap case, the model needed to operate in the style of the Indian legal system—a deeper re-alignment of its working methods than could be achieved by augmenting prompts with document fragments.
If fine-tuning is your competitive edge, prioritize curating high-quality data for your domain. Identify gaps in the data and explore methods, including synthetic data generation, to bridge them.
Where the Patterns Lead
The patterns covered here—prompting, evals, RAG, guardrails, and fine-tuning—fit together in a realistic RAG system. Most generative AI work can be handled with RAG alone, but the additional techniques exist for cases where context windows or retrieved documents aren't sufficient. These are early days for GenAI product patterns, and this article will be extended as more insights emerge.



