---
title: "How to Ask an AI So It Gets It Right and Stops Making Things Up"
url: https://phatdev.net/en/blog/prompt-engineering-for-developers
lang: en
updated: 2026-10-21
alternate:
  vi: https://phatdev.net/vi/blog/prompt-engineering-for-developers
markdown: https://phatdev.net/en/blog/prompt-engineering-for-developers.md
---

# How to Ask an AI So It Gets It Right and Stops Making Things Up

> The same AI gives very different results depending on how you ask. A five-part way to write the request, three layers against invention, and a working code check.

Categories: AI | Published: 2026-10-19 | Level: Intermediate | 6 min read

Tags: prompt-engineering, llm, code-review, testing, hallucination

You ask an AI to look over some code. It sends back three paragraphs of vague praise and criticism, plus a bug on line 48 of a file that has 30 lines. You try again with the same question and this time it says the code is fine. Same AI, two people asking differently, and the quality can be completely different.

This is fixable, and no magic phrase is needed. This post covers how I write requests for code review, test writing and documentation, plus one simple way to catch the AI when it makes things up.

## Write the request like a task for a new hire

The text you type to the AI is called a prompt. Imagine handing work to someone who joined your company today. They're smart, but they know nothing about your project. If they'd be confused reading your request, the AI will be too. Anthropic's own guide says nearly the same thing: show your prompt to a colleague with little context, and if they can't follow it, neither can the model.

Before you polish the wording, get two things in place. One is a measurable idea of what "good" means. The other is a few fixed test cases so you can compare before and after. Without them you're only guessing.

![Four stacked layers representing role, rules, examples and request, pouring down a funnel into one structured output block](https://phatdev.net/uploads/blog/prompt-engineering-for-developers/prompt-anatomy-1920.webp)

## What a good request contains

I usually write in five parts.

First, the role and situation, with the reason. "You review changes to a Node.js API. A machine will read the result, so return JSON only" works far better than "don't write anything else". Second, rules that say what to do instead of only what to avoid. Third, examples. Examples are the surest way to fix the format, but pick ones close to real life and include a "no problems found" case, or the AI will always find exactly one problem.

Fourth, the shape of the answer: give a sample JSON skeleton. If you call through an API, use the structured-output feature to force the shape. Last, the data to process, wrapped in its own tag such as `<diff>` or `<code>`, separate from your instructions. That also stops the AI from mistaking a sentence inside the data for a command.

## How to stop the AI making things up

Today's AIs invent less than before, but they still do, especially when you ask about something they can't see. I use three layers.

The first layer allows "I'm not sure". Add a separate slot for doubts and a rule: if unsure, put it there. The second layer demands an exact quote: every finding must come with one line copied word for word from the data. The third layer is the one you can trust most: your own code checks the answer. Is the quoted line really in the data? Is the severity a valid value? Can the answer be parsed? The first two layers reduce invention, the third catches what slips through.

## For those who write code

Here is the code-review template I use, shortened, with its checking function. Plain Python is enough.

```python
REVIEW_PROMPT = """\
<role>You review pull requests for a Node.js REST API.</role>

<rules>
- Report only problems that are visible in the diff below. If you are not sure, put it in "questions".
- Every finding must quote ONE line copied exactly from the diff in "quote".
- severity is one of: "bug", "security", "style". Ignore formatting.
- If there is nothing to report, return {"findings": [], "questions": []}.
</rules>

<output_format>
Return ONLY JSON, no prose:
{"findings": [{"severity": "bug|security|style", "quote": "...", "why": "...", "fix": "..."}],
 "questions": ["..."]}
</output_format>

<diff>
{diff}
</diff>
"""

def validate(raw: str, diff: str) -> dict:
    data = json.loads(raw)                                   # not JSON -> exception
    for i, f in enumerate(data["findings"]):
        if f.get("severity") not in {"bug", "security", "style"}:
            raise BadAnswer(f"finding {i}: bad severity")
        if not f.get("quote") or f["quote"] not in diff:     # anti-hallucination
            raise BadAnswer(f"finding {i}: quote is not in the diff")
    return data

def review(diff, ask_model, retries=1):
    prompt = REVIEW_PROMPT.replace("{diff}", diff)           # replace, not format(): the diff has braces
    for _ in range(retries + 1):
        raw = ask_model(prompt)
        try:
            return validate(raw, diff)
        except (BadAnswer, ValueError) as err:
            prompt += f"\n<previous_answer>{raw}</previous_answer>\n<problem>{err}. Fix it, return only JSON.</problem>\n"
    raise BadAnswer("model never produced a valid answer")
```

I re-ran it on 21 Oct 2026 with a fake model (a function returning fixed strings) across five situations. A good answer passed straight away. An answer with an invented quote was rejected, the error went back to the model, and the second try was accepted. An answer with chatter before the JSON was rejected and then fixed. A permanently bad answer raised an error instead of returning junk. And a diff containing curly braces didn't break the template. All five passed. To be clear, this only tests the checking logic, not how good a real AI is.

Two small details are easy to get wrong. Use `.replace` instead of `str.format`, because your diff contains `{` and `}`. And when retrying, hand the AI the specific error rather than just saying "try again".

![A block travelling along a lit path between two guard rails, faint cloud shapes beside it blocked and unable to enter](https://phatdev.net/uploads/blog/prompt-engineering-for-developers/guardrails-1920.webp)

For writing tests I add two rules to the request: "write a general solution, don't hard-code values to fit this one implementation" and "if the function looks wrong or a requirement is unclear, put it under `<concerns>` instead of writing a test that locks in the bug". Without these, the AI tends to write tests that pass rather than tests that check. For documentation, the most useful rule is "anything you can't tell from the code gets a TODO: confirm, not a guess". I haven't run those two templates against a real AI.

## Common mistakes

Some people add a new rule to the prompt every time they spot a bug. A few weeks later the prompt is a page long and two rules fight each other. Reread it from time to time and merge.

Another mistake is forbidding without showing the way. "Don't use markdown" is weaker than "write in flowing paragraphs". And the most time-wasting habit is tuning the prompt by feel. Keep a few fixed test cases, rerun them after each change, and compare.

## What to do next

Pick the task you most often hand to an AI. Rewrite the request using the five parts above, add one code check, and compare it with the old way on the same few cases. If the new version isn't better you'll know right away, which is the whole point of measuring.

When one AI isn't enough and several must work together, these rules still apply to each small AI; how to coordinate them is in [Handing a Big Job to Several AIs Without Breaking Things](https://phatdev.net/en/blog/agentic-workflow-orchestrator). The part about calling a real API, including structured output, is in [Using the Claude API in Your Product: Tool Calls, Live Text and Lower Bills](https://phatdev.net/en/blog/claude-api-tool-use-streaming-caching).

## References

- [Anthropic: Prompt engineering overview](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview)
- [Anthropic: Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices)
- [Python docs: json](https://docs.python.org/3/library/json.html)

## Related posts

- [Using the Claude API in Your Product: Tool Calls, Live Text and Lower Bills](https://phatdev.net/en/blog/claude-api-tool-use-streaming-caching.md)
- [Handing a Big Job to Several AIs Without Breaking Things](https://phatdev.net/en/blog/agentic-workflow-orchestrator.md)
- [Common Problems When Using AI to Write Code, and How to Avoid Them](https://phatdev.net/en/blog/ai-coding-common-problems.md)
