# How Top Engineers Use AI as a Devil’s Advocate — Ten Prompt Techniques That Actually Work
### Most people treat AI like a helpful intern who agrees with everything. These techniques flip that assumption entirely.
—
If you’ve ever asked an AI to review your code, plan a feature, or audit a configuration and received a response that felt reassuring but shallow, you’ve experienced the core problem this article addresses. The default behavior of most AI models is to be agreeable — to validate your framing, suggest gentle improvements, and give you a green light. That’s useful for brainstorming, but it’s dangerously insufficient for engineering work where blind spots cause outages, security breaches, and tech debt.
The ten techniques below all share a single underlying philosophy: **ask the model to argue against you, not with you.** Each one is presented with a practical example built around a recurring project — a task-tracker REST API — so you can see how these patterns compound as a project evolves from idea to production.
—
## 1. Pressure-Test the Idea Before Writing a Single Line
Before committing to an implementation, run the concept through a rigorous mental workout. Assign the AI a persona of a skeptical technical leader and forbid it from producing code entirely. Instead, have it surface the hidden risks, edge cases, and architectural tensions buried in the proposal.
Why does this work? When you ask a model to “help me plan,” it almost always echoes your assumptions back to you. When you ask it to challenge your assumptions, it has to generate real objections — and those objections are where the most valuable planning insights live.
**Example prompt:**
“`
Act as a battle-hardened chief architect reviewing a new feature proposal.
Do not write any code. For the idea of adding recurring tasks to a
task-tracker API — tasks that regenerate on schedules like daily, weekly,
or custom RRULE — list the five most important technical, data-model,
and UX concerns. For each concern, pose the specific questions you’d need
answered before approving this feature. After I respond, compile everything
into a requirements brief and implementation roadmap. Keep it pragmatic:
don’t over-engineer for a scale we don’t have, but don’t ignore timezone
complications or edge-case handling either.
“`
The back-and-forth this creates is far more valuable than a feature spec generated from a single prompt. You end up confronting questions you hadn’t considered — around data retention for recurring instances, what happens when a recurrence rule references a deleted calendar, and how to handle the first occurrence versus subsequent ones.
—
## 2. Audit Testability Before Writing Tests
A common mistake is asking an AI to “write tests” for existing code. What typically happens is the model generates tests for whatever is easiest to test — the happy paths, the simple functions — while leaving the genuinely risky untested logic completely untouched.
A better approach: have the AI first evaluate whether your code is even structured in a way that supports proper testing. Are dependencies injected or hardcoded? Is the scheduling logic tangled with the HTTP request handler? Only after it has assessed the testability landscape should it build and execute a testing plan.
**Example prompt:**
“`
I need help improving test coverage on this task-tracker API. Don’t
rush to write tests yet. First, examine the codebase and tell me which
endpoints and business logic paths are completely untested. Then assess
whether the existing code is actually written in a testable way — are
external service calls injected or baked in directly, is the recurrence
expansion logic isolated from the web layer, and can database interactions
be mocked cleanly? Based on what you find, build a prioritized testing
plan that addresses the highest-risk gaps first. Tell me what’s already
covered and what isn’t, then implement the missing tests.
“`
This distinction between “tested” and “well-tested” is one of the most consequential in engineering. A codebase can have 80% coverage on its trivial utility functions while the complex scheduling logic that actually breaks in production remains completely untested.
—
## 3. Split Your Reviews Into Structural and Sloppiness Passes
When asking an AI to review code, a single broad prompt like “review this commit” tends to produce a blended, average response — it catches some issues but misses others because it’s trying to do too many things at once.
A more effective pattern is to run two separate reviews in fresh conversations with no shared context:
– **Pass one** focuses on what’s structurally missing: unhandled edge cases, race conditions, concurrency problems, missing error boundaries.
– **Pass two** focuses on what’s sloppy leftover: dead code, stale comments, debug logging left behind, unresolved TODOs that were never addressed, and any other residue that accumulates during feature development.
The reason this works isn’t just about getting two passes — it’s about forcing the model into two distinct analytical modes. A single prompt that asks for everything produces a generalist scan. Two narrow, focused prompts produce specialist scans.
**Pass one prompt:**
“`
[Fresh conversation with no prior context]
Run through the test suite for this task-tracker project and identify
any missing test coverage. Pay special attention to edge cases — empty
recurrence rules, timezone boundary conditions at daylight saving time
transitions, and race conditions where two concurrent requests attempt to
update the same recurring task instance. Write the missing tests.
“`
**Pass two prompt:**
“`
[Same fresh conversation, no prior context]
Scan the most recent commit for anything that shouldn’t ship: unused
code, leftover debug statements, comments that describe behavior the code
no longer has, unresolved TODO markers, or any other form of accumulated
mess. For each item found, provide the file name and line number.
“`
Running these in separate conversations matters. When the model has context from a previous review pass, it tends to reference what it already flagged rather than independently re-examining the code with fresh eyes.
—
## 4. Cross-Reference Declarations Against Actual Usage
Any system with a configuration or permissions surface benefits from the same audit pattern: locate every declaration, cross-reference it against where it’s actually used, and flag the discrepancies. This applies to OAuth scopes, environment variable usage, API permission grants, feature flags, IAM role assignments, or even Android manifest permissions.
The key instruction is to **not make any edits until the analysis is complete and approved.** This prevents the AI from rushing to “fix” things without giving you a chance to validate the findings first.
**Example prompt (auditing API authentication scopes):**
“`
Perform a compliance audit on this task-tracker API’s authentication
scopes. Find every location where a required OAuth scope is declared —
route decorators, middleware configuration, API gateway rules — and build
a master list. Then cross-reference that list against the actual code paths
where each scope is enforced. Flag anything that appears in the master list
but is never enforced at runtime, and flag anything that’s enforced at
runtime but never declared anywhere. Output the findings as a markdown
report with file paths and line numbers for each item. Do not make any
code changes until I’ve reviewed and approved the plan.
“`
This pattern is especially valuable in large codebases where permissions and scopes accumulate over time, often through incremental additions by different team members who each added what they needed without checking what was already there.
—
## 5. Grade Your Own Code With a Real Rubric
Here’s a problem worth naming explicitly: when you ask a model for a code review, it almost always defaults to being polite. It compliments the naming, suggests a docstring, gives the code a pass. This isn’t a failure of the model — it’s the natural result of training models to be helpful and non-confrontational.
The fix is to assign a demanding persona and require a concrete letter grade. When the model has to give an actual failing grade and then explain exactly how the code would break in production, it shifts from a supportive role to an adversarial one — which is exactly what you want in a code review.
**Example prompt:**
“`
You are a strict principal engineer conducting a pre-production code
review. You have zero tolerance for code that only works on the happy
path. Grade my uncommitted changes with a letter grade from A to F for
production readiness. Do not award an A unless the code genuinely
demonstrates robustness across all three dimensions: efficiency, resilience,
and architecture. Specifically evaluate: whether there are redundant
database queries that should be cached, whether the scheduler has silent
failure points with no error boundaries, and whether the recurrence logic
is tightly coupled to the HTTP layer in a way that makes it impossible to
test or reuse independently. For every issue found, explain precisely how
it would manifest as a production failure, then provide the exact fix as
a git diff so the code can earn a better grade.
“`
The requirement that the model provide a git diff for each issue is crucial. Commentary without a fix path is easy to ignore. A diff that the developer can apply immediately transforms feedback from abstract to actionable.
—
## 6. Force the Model to Defend Its Own Recommendations
After the AI produces an implementation plan, ask it to stress-test its own reasoning before you accept it. Have it explicitly list what it’s trading away with its chosen approach — performance, cost, security, maintainability — compared to at least one alternative.
This counteracts a subtle but pervasive failure mode: a well-formatted, confident AI plan can feel like a settled decision when it’s really just one option among many. By forcing the model to articulate what it’s giving up, you keep the decision-making authority where it belongs — with the human.
**Example prompt:**
“`
The recurring-tasks implementation plan you just proposed has some clear
merits. But before I accept it, walk me through the trade-offs. What are
we sacrificing on performance, cost, security, and long-term maintainability
compared to at least one alternative approach you considered but didn’t
recommend? Be specific about what each alternative would look like and why
you chose this path over it. I want to make an informed decision, not just
accept your first suggestion as final.
“`
This technique is particularly effective for architectural decisions where the “right” answer depends heavily on context — team size, expected traffic patterns, existing infrastructure, and long-term maintenance capacity.
—
## 7. Build a Security Checklist From Real-World Evidence
Rather than asking the model for a generic “review this for security issues,” take a two-step approach: first have the model research current, real-world security pitfalls and subtle logic errors specific to the tech stack you’re using, drawing on developer forums, GitHub issue trackers, and recent technical writing. Then use those findings to build a targeted, manual review checklist for the highest-risk parts of your code.
This matters because AI-generated code has a documented tendency to look correct without being correct. It compiles. It passes basic tests. It survives a casual inspection. An evidence-based checklist that targets known failure modes in your specific stack catches issues that a generic review prompt would miss entirely.
**Example prompt:**
“`
Research current security vulnerabilities and subtle logic errors that are
commonly found in AI-generated FastAPI code. Focus on sources like developer
forums, GitHub issue trackers, and recent technical blog posts. Based on your
findings, build a manual review checklist specifically for auditing this task-
tracker project’s highest-risk areas: the scheduling and cron logic, webhook
signature verification, and how task ownership is validated on update requests.
Present the checklist as a structured audit document I can walk through
systematically.
“`
The value here isn’t just the checklist itself — it’s the process of directing the model’s research capability toward your actual stack and actual risk surface, rather than relying on generic security advice.
—
## 8. Match Prompt Specificity to the Stage of Work
Different stages of development require different levels of prompt precision. Early discovery benefits from vague, open-ended prompts that let the model explore broadly without constraining its thinking. Late-stage refinement demands precise, narrow prompts that catch specific classes of issues.
A practical workflow might look like this:
1. **Discovery:** Stay deliberately broad. Ask the model to explore possible approaches without prescribing constraints. Being too specific too early creates blind spots the model won’t think to question.
2. **Proof of concept:** Narrow just enough to confirm the core idea is buildable.
3. **Refinement:** Get precise about implementation details, pushing toward code you’d be satisfied to own.
4. **Final review:** Open a fresh conversation with no prior context and ask for a sharp, focused code review of the changes. Iterate until the findings become trivial.
The principle is simple: **loose early, precise late.** Matching prompt specificity to the actual phase of work consistently outperforms either being vague throughout or over-specifying from the very first interaction.
**Final-stage review prompt (used in a fresh conversation):**
“`
Code review the uncommitted changes to this task-tracker project. Identify
any unhandled corner cases and assess overall performance characteristics.
Summarize your findings concisely.
“`
After reviewing the findings, you can selectively address them:
“`
Implement fixes for findings 2 and 5 only. Leave findings 1 and 3 — I’ve
decided the added complexity isn’t worth the benefit given our current priorities.
“`
This selective engagement is itself a technique: not every finding deserves the same investment, and the ability to triage AI feedback is a skill that improves with practice.
—
## 9. Automate the Adversarial Review So Nobody Has to Remember
The most consistent code review is the one that happens automatically. Instead of relying on someone remembering to ask for an adversarial review, wire it into your CI pipeline so every pull request gets a structured, critical assessment with zero manual effort.
A practical approach: build a script that pulls the git diff from a PR, sends it through a rigorous grading rubric (like the one in technique 5), and posts the results as a comment on the PR. This removes human forgetfulness from the equation entirely.
**A working Python example:**
“`python
“””
adversarial_review.py
Runs a structured code review against the current git diff.
Designed to run in CI on every pull request so that review
happens automatically, not depending on someone remembering to ask.
“””
import os
import subprocess
import sys
import requests
REVIEW_PROMPT = “””You are a strict, principal-level code reviewer with zero
tolerance for fragile, happy-path-only code. Review the diff below and grade
it A through F for production readiness. Do not award an A unless the code is
genuinely robust. For each issue found, cover:
1. Efficiency: redundant calls, uncached lookups, wasteful queries.
2. Resilience: silent failure points, missing error handling, no fallback
behavior for external calls.
3. Architecture: tight coupling, unclear separation of concerns.
For every issue, explain concretely how it could fail in production, then
give the exact fix. Output as a markdown report with a letter grade at the top.
DIFF:
{diff}
“””
API_URL = “https://api.anthropic.com/v1/messages”
API_KEY = os.environ.get(“ANTHROPIC_API_KEY”)
MODEL = “claude-sonnet-4-6”
def get_diff():
result = subprocess.run(
[“git”, “diff”, “–staged”], capture_output=True, text=True
)
diff = result.stdout
if not diff.strip():
result = subprocess.run(
[“git”, “diff”], capture_output=True, text=True
)
diff = result.stdout
return diff
def review_diff(diff):
headers = {
“x-api-key”: API_KEY,
“content-type”: “application/json”,
“anthropic-version”: “2023-06-01”,
}
payload = {
“model”: MODEL,
“max_tokens”: 2000,
“messages”: [
{“role”: “user”, “content”: REVIEW_PROMPT.format(diff=diff)}
],
}
response = requests.post(API_URL, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
return “”.join(
block[“text”] for block in data[“content”] if block[“type”] == “text”
)
def main():
diff = get_diff()
if not diff.strip():
print(“No changes to review.”)
sys.exit(0)
report = review_diff(diff)
with open(“review_report.md”, “w”) as f:
f.write(report)
print(report)
if __name__ == “__main__”:
main()
“`
This script does three things:
– **Grabs the diff** from git, checking staged changes first and falling back to unstaged if nothing is staged yet. This makes it work both locally before a commit and in CI against a PR branch.
– **Sends it to the model** using the same adversarial grading rubric, pulling the plain text response back out.
– **Writes the report to a file** so a CI step can post it as a PR comment and exits cleanly with no API call if there’s nothing to review.
Running it in CI means every pull request gets reviewed with the same rigor regardless of whether any human remembered to ask. The script slots into a GitHub Actions step that triggers on every `pull_request` event.
—
## 10. Map Your Application as a Graph Instead of a Checklist
The most unconventional technique on this list: instead of asking the model for a generic list of test ideas — which almost always produces a flat, boilerplate checklist that ignores the specifics of your project — have it represent your application’s workflow as a directed acyclic graph and reason structurally about where failures can propagate.
The key concept here is **seams**, a term from Michael Feathers’ work on working effectively with legacy code. Seams are the boundaries between components where two pieces hand off responsibility — and they’re often the most under-tested parts of a system precisely because no single component “owns” what happens at the boundary.
**Example prompt:**
“`
Model the workflow of this task-tracker application as a directed acyclic
graph. Map out the path a request takes: authentication middleware, task-
ownership verification, recurrence expansion logic, database write, and
webhook dispatch. For each individual component, identify the highest-impact
tests. Then separately identify the seams — the boundaries between components
where one hands off to the next and neither is clearly responsible for
validating what crosses that boundary. Present everything as a prioritized
markdown table with columns for seam or component, risk description, and
suggested test.
“`
This approach produces fundamentally different results than “write me some tests for this project.” Instead of a flat list of test cases, you get a structural understanding of where your system is most vulnerable — the places where assumptions from one component collide with assumptions from another.
—
## Frequently Asked Questions
**Q: Do I need to use all ten techniques, or can I pick just a few?**
A: You can absolutely start with just one or two. The grading rubric from technique 5 is the fastest way to feel the difference between an agreeable AI response and a genuinely adversarial one. Once you see that difference, the other techniques start making intuitive sense as variations on the same core idea — forcing the model to work against your assumptions rather than alongside them.
**Q: Won’t adversarial prompts slow down my workflow?**
A: Initially, yes. Running multiple focused prompts takes more time than a single broad request. But the alternative — shipping code with undiscovered blind spots — almost always costs more time in the form of production incidents, hotfixes, and refactoring sessions. The workflow also speeds up as you internalize the patterns and start combining techniques into streamlined routines.
**Q: Is this only useful for code review, or does it apply to other tasks?**
A: These techniques apply broadly. The spec-building approach works for any design document. The compliance-check pattern applies to environment configuration, infrastructure-as-code templates, and API scope audits. The graph-based thinking technique works for database migration planning, deployment pipeline design, and anything with a workflow that has component boundaries.
**Q: What if the model still gives me a polite review even with a demanding persona?**
A: This is a common experience, especially with models that are heavily tuned to be helpful and non-confrontational. If you’re still getting green-lights on code that you know has problems, try adding an explicit failing condition — require a minimum number of issues to be found, or state that a grade of A is impossible unless at least three distinct categories of problem are identified. The more specific the failure condition, the harder the model has to work to meet it.
**Q: Can I use these techniques with open-source models, not just commercial APIs?**
A: Yes. The prompt patterns are model-agnostic — they work with any LLM interface that accepts system-level instructions and conversation history. The difference you’ll notice is in how consistently the model adheres to the adversarial framing. Some models require more explicit instruction to resist the agreeable default, but the underlying technique remains the same regardless of the model backend.
**Q: How do I avoid the model hallucinating findings that don’t exist in my code?**
A: Two strategies help here. First, always include the actual diff or code snippet in the prompt rather than relying on the model’s memory of a previous conversation. Second, ask the model to cite specific file names and line numbers for every finding it reports. When a model has to point to exact locations, it’s much harder for it to fabricate issues that don’t exist.
**Q: Is the automated CI approach from technique 9 difficult to set up?**
A: The Python script shown in technique 9 is a working starting point that requires minimal setup: Python 3.9+, an API key from your model provider, and a single environment variable. Integrating it into a CI pipeline as a GitHub Actions step is straightforward — the script can be invoked as a step in your workflow file, and its output can be posted as a PR comment using the GitHub API or a dedicated action. The main effort is in tuning the prompt to match your team’s standards and deciding which findings should block a merge versus which are advisory.
—
## Conclusion
The thread that connects all ten techniques is a single, powerful idea: **the most dangerous assumption in AI-assisted engineering is that the model’s default behavior is good enough.** Default responses are polite, they validate your framing, and they gloss over the edge cases where real failures live. Every technique above exists to break that default — to force the model out of its agreeable-assistant role and into an adversarial-second-opinion role.
If you take one thing away from these techniques, let it be this: the quality of your AI-assisted work depends less on the model you’re using and more on the structure of your prompts. A well-shaped prompt that demands specific, adversarial reasoning will outperform a generic prompt on any model. The techniques here are patterns, not one-time tricks — they compound as you internalize them and adapt them to your own projects and workflows.
Start with the grading rubric. Feel the difference between a model that’s being helpful and a model that’s actually working against your blind spots. Once you’ve felt that difference, the rest of the techniques become natural extensions of the same principle.
Thank you for reading



