How to Stop AI Coding Agents From Hallucinating APIs

Agents invent functions when they write from pattern memory instead of your code. Grounding with real symbols, types, and tests fixes most of it.

By The OpenVisio Team · · 11 min read

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:

  1. The definition was never in context. The agent saw a call site but not the class, so it guessed the rest of the interface.
  2. Similar names exist. Your repo has getSubscriptions and listActiveSubscriptions in different modules. The model blends them.
  3. Library drift. The agent remembers a popular library's API as of some version, but you are on a different one.
  4. 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 UserService defined, 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, avoid any in core modules, and enable noUncheckedIndexedAccess if 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 getattr deserve 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.py to see how InvoiceBuilder is 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:

  1. Lockfiles committed and respected. The agent should read installed versions from package-lock.json, pnpm-lock.yaml, poetry.lock, go.sum, or Cargo.lock, not guess.
  2. State major versions in the instruction file. "We use React Router v6 data APIs; do not use v5 patterns" is cheap and effective.
  3. Read the installed source or types. For TypeScript, the .d.ts files in node_modules are the truth. For Python, the installed package source. Tell the agent to check there when unsure.
  4. 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:

GateCatchesCost
Typecheck after each editInvented members, imports, wrong signaturesSeconds
Targeted tests after each stepInvented behavior, wrong semanticsSeconds to minutes
Import smoke test (dynamic langs)Invented modules and pathsSeconds
CI on every agent PRAnything the agent did not runMinutes
Reviewer checks new symbolsNew helpers that duplicate existing onesMinutes

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:

  1. The agent searches by description for "active subscriptions for user" and finds listActiveSubscriptions(userId) in packages/billing/src/subscriptions.ts:58.
  2. It traces existing callers and sees the admin page uses it through billingClient.
  3. It writes the call using the real signature and existing pattern.
  4. Typecheck passes. The component test passes.
  5. 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.

AI hallucinationAI coding agentstype checkinggroundingcode quality

See it on your own repo

Paste a GitHub URL and get the day-one walkthrough — free, in your browser.

Index a repo

Keep reading