open-source
Fine Tuning Vs Rag For Code Assistants Design: practical systems guide
Technical field guide on fine tuning vs rag for code assistants design tradeoffs for teams building dependable AI coding workflows.
# Fine Tuning Vs RAG For Code Assistants Design: practical systems guide
You're building a code assistant and need to decide: should you fine-tune a foundation model on your codebase, or use retrieval-augmented generation (RAG) to pull in relevant context at query time? The honest answer is neither approach alone will carry you to production—most serious systems end up combining both, but the sequencing and resource allocation matter enormously.
The core tension is compile-time knowledge versus runtime flexibility. Fine-tuning bakes patterns into weights; RAG fetches them on demand. Each has failure modes that only show up under load with real engineers using real codebases.
Why fine-tuning alone fails in production
Fine-tuning a model on your repository teaches it your coding style, naming conventions, and common patterns. It works beautifully for autocompletion when you're writing code that looks like code you've written before. The model "knows" that in your team's React components, you always wrap API calls in a `useQuery` hook with specific error boundaries.
But the moment your codebase changes—a refactor, a dependency upgrade, a new architectural pattern—the fine-tuned weights are stale. You're now serving suggestions based on code that no longer exists. One team we spoke with fine-tuned on their Python backend every week and still found the model suggesting deprecated ORM methods that had been removed three weeks prior.
The retraining cadence becomes a product problem. Daily fine-tuning runs are expensive and slow; weekly runs leave your assistant perpetually behind. You end up in a loop where engineers stop trusting the suggestions because they're subtly wrong—not syntax errors the linter catches, but architectural mismatches that waste time in review.
Fine-tuning also struggles with cross-repository reasoning. If your assistant needs to understand how a shared library is used across five services, you either fine-tune on all five (expensive, potentially leaking context across boundaries) or accept that it will hallucinate usage patterns.
Where RAG breaks down
RAG solves the staleness problem: you index your codebase, and at inference time you retrieve the 10-20 most relevant chunks to stuff into context. When an engineer asks "how do we handle rate limiting?", you pull the actual rate-limiter implementation and recent usage examples.
The failure mode is retrieval precision. Code isn't prose—semantic similarity between docstrings doesn't mean two functions are related in practice. You'll retrieve a utility function because it has similar variable names to the one the engineer actually needs. Or you'll miss a critical dependency because the connection is implicit (a function is passed as a callback three layers deep).
Chunk size is the other knife edge. Too small (single functions) and you lose context about how pieces fit together. Too large (entire files) and you waste tokens on irrelevant code, pushing out the actually useful bits. We've found that a mix of chunk sizes—function-level for common utilities, file-level for configs, module-level summaries for architecture—works better than any single strategy, but now your retrieval pipeline has three indices to keep in sync.
Latency matters more than you'd expect. A 200ms retrieval delay feels instant in a chat interface but is unacceptable for inline autocomplete. You need a tiered system: fast local index for autocomplete, slower but more comprehensive vector search for explicit queries.
The hybrid pattern that actually works
Treat fine-tuning as your base layer for style and common patterns, but gate all outputs through compile/lint/test. Then use RAG to pull in fresh context for anything that touches files modified in the last week, or involves cross-file dependencies your static analysis can trace.
Here's a concrete setup: fine-tune a smaller model (7B-13B parameters) on your codebase's commit history from 3+ months ago. This captures stable patterns. At inference time, your agent:
1. **Plans** the edit based on the fine-tuned model's suggestion and a RAG-retrieved summary of recent changes to related files
2. **Constrains** the edit to a small diff (Goatfied targets single-function or single-class changes)
3. **Validates** against compile, lint, and unit tests—this catches style drift and stale patterns before they reach the engineer
The fine-tuned model gives you fast, stylistically consistent completions. RAG injects fresh context only when needed (recent refactors, new dependencies). The validation layer rejects anything that would break.
We've found this cuts the "looks right but doesn't compile" failure mode by roughly 80% compared to fine-tuning alone. The compile gate is non-negotiable—without it, you're just shipping faster hallucinations.
When to bias toward fine-tuning
Fine-tuning wins when:
- Your codebase has stable, opinionated patterns (think: a monorepo with strict lint rules and code generators)
- You have the infra to retrain weekly or on-demand after major refactors
- Latency is critical and you can't afford RAG's retrieval overhead for every keystroke
For autocomplete specifically, a well-tuned model feels magical because it "just knows" your patterns. RAG struggles here—you can't wait 200ms to retrieve context before showing a suggestion.
When to bias toward RAG
RAG wins when:
- Your codebase changes rapidly, or spans many repositories you can't easily train on together
- You need explainability—RAG lets you show the engineer exactly which files informed the suggestion
- You're starting out and don't have the ML infra to retrain models regularly
RAG also composes better with the agent loop. When Goatfied's planner decides to edit a file, it retrieves that file's recent git history, current tests, and dependent imports as part of the planning context. The model doesn't need to have "seen" those files during training—it just needs to reason about them in the moment.
Implementation gotchas
**Embedding models matter as much as LLMs.** We've seen teams spend weeks tuning their LLM and using off-the-shelf sentence transformers for retrieval, then wonder why retrieved chunks are useless. Code-specific embeddings (trained on code search tasks) make a measurable difference.
**Cache aggressively.** The same files get retrieved repeatedly. A 10-minute TTL cache on embeddings and retrieved chunks cuts costs by 60-70% without hurting freshness.
**Index build times compound.** If your CI takes 5 minutes and your embedding index rebuild takes another 10, you've just added 15 minutes to every PR where an engineer wants an AI suggestion. Build incrementally: only re-embed changed files.
Related reading
- [Why we open-sourced Goatfied (and what we kept proprietary)](/blog/why-we-open-sourced-goatfied-and-what-we-kept-proprietary)
- [Open-Source playbook #3: practical Goatfied tactics for shipping PRs](/blog/goatfied-open-source-playbook-3)
