In this guide
Every team using coding agents has seen it: a clean, confident change that calls userService.getActiveSubscriptions(), a method that does not exist. Or an import from @acme/utils/dates when the real path is @acme/dates. Or a library option that was removed two major versions ago. The code reads well, which is exactly why it is dangerous. It looks like something a teammate would write.
Hallucinated APIs are not random. They follow from how language models generate code and from what context the agent did or did not have when it wrote the line. That means they can be reduced systematically. This post explains why agents invent functions, then covers the grounding techniques that work in practice: symbol lookup, type checking, tests, version pinning, and workflow rules that make invented APIs fail fast instead of reaching review.
Key takeaways
- Agents invent APIs when they write from pattern memory instead of from your actual code. The fix is to make looking up real symbols the easy, default path.
- Give agents a way to resolve symbols and signatures precisely, ideally through a code graph or language-server-backed tool, not just text search.
- Make the compiler and tests part of every loop. A typecheck after each edit catches most invented members and imports immediately.
- Pin and expose dependency versions so the agent writes against the library you actually have.
- Require file and line anchors for claims about existing code. An agent that has to cite a definition is less likely to invent one.
Why agents hallucinate APIs
A language model writes code by predicting what plausibly comes next. When it has seen your actual code in context, "plausible" is anchored to reality. When it has not, plausible means "what code like this usually looks like", drawn from patterns in its training data. A user service usually has a method to get subscriptions. It is usually called something like getActiveSubscriptions. So that is what gets written.
Four conditions make this more likely:
- The definition was never in context. The agent saw a call site but not the class, so it guessed the rest of the interface.
- Similar names exist. Your repo has
getSubscriptionsandlistActiveSubscriptionsin different modules. The model blends them. - Library drift. The agent remembers a popular library's API as of some version, but you are on a different one.
- Long sessions. Context from earlier in the session was summarized or dropped, and details went with it.
None of these are solved by asking the model to "be careful". They are solved by changing what the model has in front of it when it writes, and by checking what it wrote before anyone else sees it.
Ground with real symbols
The single most effective step is making it easy for the agent to look up the real definition before using something.
Text search is not enough
Grep finds strings. Asking it for getActiveSubscriptions returns nothing, which an agent may read as "I need to create it" or may ignore. Asking for Subscription returns hundreds of hits across types, tests, and comments. Neither tells the agent what methods UserService actually has.
Structural lookup
Tools that understand code structure answer the right questions directly:
- Find symbol: where is
UserServicedefined, and what members does it have? - Search by description: which function returns a user's active subscriptions? This catches the case where the real name is
listActiveSubscriptions. - Trace calls: how do other parts of the code get subscriptions today? Existing usage is the best template.
These can come from a language server, an IDE integration, or an MCP server that exposes a code graph. The details of why graph-based lookup is more reliable than chunked embeddings for this are in code knowledge graph vs embeddings.
Make lookup the rule
Add it to your instruction file so the agent does it without being asked:
## Using existing code
- Before calling any function, method, or type from this repo, look up its
definition (find the symbol) and use its real signature.
- Before writing a new helper, search for an existing one by description.
- Prefer copying the pattern of an existing call site over writing from scratch.
- If you cannot find a definition, say so. Do not assume it exists.
The last line matters. Giving the agent explicit permission to say "I could not find this" removes some of the pressure to produce something plausible. More on writing these files in writing AGENTS.md and CLAUDE.md.
Let the type checker do its job
In typed languages, most hallucinated APIs fail compilation. The problem is when the agent writes many edits before compiling, then faces a pile of errors and "fixes" them by adding the missing methods.
Typecheck after every edit
## Verification loop
- After each file edit, run the typecheck for the affected package:
`pnpm --filter <pkg> typecheck` or `go build ./...` or `cargo check -p <crate>`.
- If a type error says a member or import does not exist, look up the real
symbol. Do not add the missing member unless the task asks for it.
That second rule closes a specific loophole. An agent that invents getActiveSubscriptions, sees "Property does not exist", and then adds a stub method has turned a hallucination into a design change.
Strengthen the types
Looser types give hallucinations more room. A few settings help:
- TypeScript:
strict: true, avoidanyin core modules, and enablenoUncheckedIndexedAccessif your codebase can tolerate it. - Python: run a type checker like mypy or pyright in CI on the packages agents touch most, and add type hints to their public functions.
- Dynamic call patterns such as string-keyed registries and
getattrdeserve typed wrappers, because no tool can check them otherwise.
Dynamic languages
In Python, Ruby, or JavaScript without types, an invented method fails only at runtime. Here tests and lookup carry more weight. Running the specific test file after each change and importing the module in a quick smoke test catches invented imports early.
python -c "import acme.billing.invoices" # catches bad imports immediately
pytest tests/billing/test_invoices.py -q
Tests as a grounding signal
Tests do two things against hallucinations. They catch invented behavior at runtime, and existing tests show the agent how real code is called.
- Point the agent at tests first. "Read
tests/billing/test_invoices.pyto see howInvoiceBuilderis used" grounds the agent in real usage. - Require a failing test before a fix. If the agent writes a reproduction first, it has to call real code to make it fail.
- Never let the agent weaken a test to pass. That is the runtime version of adding a stub method.
Pin and expose library versions
Library hallucinations are a different flavor: the method existed once, or exists in a neighboring library. Defenses:
- Lockfiles committed and respected. The agent should read installed versions from
package-lock.json,pnpm-lock.yaml,poetry.lock,go.sum, orCargo.lock, not guess. - State major versions in the instruction file. "We use React Router v6 data APIs; do not use v5 patterns" is cheap and effective.
- Read the installed source or types. For TypeScript, the
.d.tsfiles innode_modulesare the truth. For Python, the installed package source. Tell the agent to check there when unsure. - Use documentation tools carefully. If you give the agent a docs lookup tool, make sure it returns docs for your installed version.
## Dependencies
- Versions are in pnpm-lock.yaml. Check the installed version before using a library API.
- For library signatures, read the type definitions in node_modules/<pkg>.
- Major versions: React 19, Next.js 15 App Router, Zod 3, Drizzle ORM.
Require citations for claims
When an agent describes existing code, ask for file and line references. "The invoice total is computed in InvoiceBuilder.total()" is unverifiable. "packages/billing/src/invoice.ts:142 InvoiceBuilder.total()" can be checked in two seconds and is harder to invent, because the agent has to have actually found it.
For every existing function or type you mention in your plan, include its
definition as path:line. Mark anything you could not locate as UNVERIFIED.
Review plans with this in mind. An UNVERIFIED tag on a function the plan depends on is a stop sign.
Workflow rules that catch the rest
Even with good grounding, some invented APIs will slip through. Put a few cheap gates in the path:
| Gate | Catches | Cost |
|---|---|---|
| Typecheck after each edit | Invented members, imports, wrong signatures | Seconds |
| Targeted tests after each step | Invented behavior, wrong semantics | Seconds to minutes |
| Import smoke test (dynamic langs) | Invented modules and paths | Seconds |
| CI on every agent PR | Anything the agent did not run | Minutes |
| Reviewer checks new symbols | New helpers that duplicate existing ones | Minutes |
The reviewer gate is worth expanding. When reviewing an agent PR, look specifically for new functions or methods that were not requested. Each one is either a legitimate addition or a hallucination that was "made real" to satisfy the compiler. Our playbook for reviewing AI agent pull requests covers this and other checks.
A before and after
Without grounding, a task like "show active subscriptions on the account page" often goes: the agent opens the account page component, sees a userService import, writes userService.getActiveSubscriptions(user.id), adds a loading state, and finishes. Typecheck was not run. Review catches it, or worse, a runtime error does.
With grounding:
- The agent searches by description for "active subscriptions for user" and finds
listActiveSubscriptions(userId)inpackages/billing/src/subscriptions.ts:58. - It traces existing callers and sees the admin page uses it through
billingClient. - It writes the call using the real signature and existing pattern.
- Typecheck passes. The component test passes.
- The plan and summary cite the definition with its path and line.
The model is the same. What changed is that the real definition was in front of it before it wrote the call.
Where OpenVisio fits
OpenVisio exposes a locally built code graph to agents over MCP, including find_symbol (by name, pattern, or natural-language description), search_code, and trace_calls, each returning path:line anchors. That makes the "look up the real symbol first" rule cheap enough that agents actually follow it. For a human check, Ask answers questions like "where do we fetch a user's subscriptions" with the relevant code beside the explanation.
Agents will keep writing plausible code. Your job is to make sure plausible and real are the same thing: put the actual definitions in reach, run the compiler early, and do not let a missing method get quietly invented into existence.

