Best Model for Legacy Code: Comprehension Beats Cleverness
Legacy work is mostly reading, not writing. Why the model that scores highest on code generation is often the wrong one for a twenty-year-old codebase.
Legacy code inverts the usual model requirement. On a greenfield project you want a model that writes well. On a system that has been in production for fifteen years, ninety percent of the work is understanding what the code already does and why, and only the last ten percent is changing it.
That difference matters because generation quality and comprehension quality are not the same capability, and the benchmarks everyone quotes measure the first one.
The defining constraint is missing intent
Legacy code carries decisions whose reasons are gone. A guard clause that looks redundant is there because of an incident in 2014. A field is written but never read because a downstream system that no longer exists used to consume it. A function is four hundred lines because splitting it broke something once.
A model cannot recover that context from the code. What it can do — and what separates a good model from a bad one here — is notice that something is unexplained and say so, rather than inventing a rationalisation.
This is a calibration property, not a capability one. The failure you care about is a model that reads a strange-looking branch, decides it is dead code, and confidently removes it. The behaviour you want is a model that flags it as unexplained and asks. Why LLMs hallucinate covers where that confident-invention behaviour comes from.
Old languages and frameworks are thin in training data
Model quality is not uniform across languages. There is vastly more modern TypeScript and Python in any training corpus than there is COBOL, VB6, Perl, ColdFusion, or a Java codebase written against a 2008 framework.
The practical consequence is that performance degrades in a specific way: the model still produces syntactically plausible output, but it reaches for idioms from a more popular language and gets the ecosystem details wrong. Library names are close but not right. Deprecated call signatures appear. Framework lifecycle assumptions come from something else entirely.
Test this directly before committing. Give each candidate three real files from your oldest subsystem and ask it to explain what they do. You are not scoring the prose — you are looking for whether it correctly identifies the framework, its version-specific behaviour, and the conventions in use. Models diverge sharply on this and the benchmark tables will not tell you.
No tests means no safety net, which changes everything
Modern code review with an agent assumes a test suite catches regressions. Legacy systems frequently have no meaningful tests, or have tests that have been failing so long that nobody looks.
Without that net, an agent that makes a plausible change is dangerous rather than helpful. There is nothing to catch a subtle behavioural difference, and the deployment path is often slow enough that discovering it takes days.
The productive sequence is to use the model to build the net first. Ask it to write characterisation tests: tests that assert current behaviour, whatever that behaviour is, including the odd parts. This is a genuinely good use of a model because it is mechanical, high-volume, and verifiable — the test either passes against the unmodified system or it does not. Generating tests with LLMs covers how to keep those tests useful rather than tautological.
Only once you have characterisation coverage should you let a model touch the implementation. This ordering is the single biggest determinant of whether legacy modernisation with AI goes well or badly.
Long context earns its keep here
Legacy code resists selective reading. Logic is spread across files with no clear boundaries, globals are mutated in distant places, and the call graph is genuinely tangled. Search-and-read-selectively — the right strategy in a well-structured modern repository — misses things.
This is one of the few tasks where filling a large window is defensible. Loading an entire subsystem, twenty thousand lines of it, and asking for a dependency map produces better results than twenty targeted greps, because the model needs to see the connections rather than the pieces.
Kimi K3, GLM-5.2, both DeepSeek V4 variants and MiniMax M3 all ship a one-million-token window, so the constraint is cost rather than capacity. Be aware that attention thins in the middle of very long prompts — the lost in the middle problem is real, so put the file you actually care about near the end.
Read cheap, change carefully
The economics fall out naturally from the read-heavy shape of the work. You are going to consume an enormous number of input tokens mapping the system, and produce very few output tokens changing it.
That argues for a two-tier setup. Use DeepSeek V4 Flash at roughly $0.14 per million input tokens for the survey work — summarising modules, tracing call chains, cataloguing where a global is written. At that price you can read an entire legacy subsystem repeatedly without thinking about it.
Then escalate to a stronger model for the actual modification, where you need judgement about what is safe to change. GLM-5.2 at around $1.40 in and $4.40 out is the reasonable middle; Kimi K3 at $3 in and $15 out is worth it when the change is genuinely risky. Input versus output token pricing explains why the read-heavy shape makes this split so favourable.
What to pick
For mapping and comprehension across a large old codebase, DeepSeek V4 Flash on a long context is the workhorse — cheap enough to read everything, capable enough to summarise accurately. For the risky changes, GLM-5.2 with a high reasoning effort level, escalating to Kimi K3 when the subsystem is one you cannot afford to break.
Avoid quantised local models for legacy work specifically. Quantisation tends to preserve fluency while eroding precise multi-step reasoning, which is exactly the capability you are relying on when tracing a data flow through unfamiliar code.
The decision rule: write characterisation tests before you let a model change anything, keep a human reading every diff that touches a subsystem with no coverage, and treat any confident claim about dead code as a hypothesis to verify rather than a finding. Choosing a model for refactoring covers the modern-codebase version of the same problem.
Common questions
Which model handles COBOL or other rare legacy languages best?
Test rather than assume. Training corpora are thin for old languages, and models degrade by producing plausible output with wrong ecosystem details. Give each candidate three real files and check whether it identifies the framework and version correctly.
Is it safe to let an agent refactor code with no tests?
No. Use the model to write characterisation tests that pin current behaviour first, verify they pass against the unmodified system, and only then allow implementation changes.
Should I load the whole subsystem into context or search selectively?
For legacy code, load it. Logic is spread across unclear boundaries, so selective search misses connections. Put the file you care about most near the end of the prompt, since attention thins in the middle.