When I first wired up Gemini to grade resumes, the output was garbage.
Not because Gemini is bad. Because I was prompting it like it was a chatbot.
I've since shipped the app to Google Play, and the analysis is now consistent enough that users trust it. Here are the 5 prompt engineering tricks that got me from "sometimes right" to "reliable enough to build a business on."
1. Never ask for a score. Ask for the rubric first, THEN the score.
Bad prompt:
"Rate this resume from 0 to 100."
Result: 60. 65. 70. Always in that range. Meaningless. Same resume → different score every call.
Good prompt:
"Grade this resume against the following rubric. For each criterion, output an integer 0-25 based on the criterion definition. Sum the four criteria to compute the final score."
Contact Info (0-25): 0 = missing/broken; 12 = present but formatted awkwardly for ATS parsing; 25 = clean 3-line header.
Work Experience (0-25): 0 = missing dates or roles; 12 = generic bullets, no metrics; 25 = every bullet has numbers and specific outcomes.
(etc.)
Result: Deterministic scores. Same resume → same score every time. Because you gave the model a math problem, not a vibe check.
The insight: LLMs are terrible at making a subjective judgment call and returning a single number. They're great at applying a structured rubric to a subjective input. Never skip the rubric.
2. Force JSON output with a schema, not just a request.
Bad prompt:
"Return your response as JSON."
Result: Sometimes valid JSON. Sometimes markdown-wrapped JSON. Sometimes JSON with an apologetic explanation before it. Sometimes valid JSON with an extra "reasoning" field you didn't ask for.
Good prompt:
"Return your response as valid JSON matching exactly this schema, with no surrounding text, no markdown fences, no explanation:"
{ "overallScore": number, "sectionScores": { "contactInfo": number, "workExperience": number, "skills": number, "education": number }, "improvements": [ { "id": string, "title": string, "category": "content" | "format" | "ats" | "keywords", "priority": number, "before": string, "after": string, "explanation": string } ] }
Result: Reliable JSON. Wrap the response in JSON.parse() in a try/catch and validate with Zod — you'll fail on <1% of calls now instead of 15%.
Bonus: Gemini specifically has a responseSchema parameter — pass the schema as a first-class API argument and it enforces even more reliably than prompting alone.
3. Give it examples, not just definitions.
Bad prompt:
"For each improvement, provide a 'before' bullet and an 'after' bullet."
Result: The "after" bullet is often barely different from the "before." Or the "after" is wildly better but you can't tell what specific change was made.
Good prompt:
"For each improvement, provide a 'before' bullet and an 'after' bullet. Follow these examples for the transformation quality expected:"
Example 1:
Before: "Responsible for managing team of engineers"
After: "Led team of 6 engineers; shipped 4 major features and reduced deploy time by 40% over 2 quarters"
Change explanation: Replaced vague responsibility framing with specific team size + measurable outcomes.Example 2:
Before: "Worked on backend infrastructure"
After: "Rebuilt payments backend in Go; reduced p95 latency from 320ms to 45ms and cut infrastructure cost by $60K/year"
Change explanation: Named specific technology + before/after metrics + business impact.
Result: The model actually understands what "quality transformation" means. Its outputs match the pattern.
The insight: LLMs learn from patterns in the prompt more than from instructions. One good example beats three paragraphs of guidelines.
4. Use "chain-of-thought" for reasoning, then discard it.
For the ATS keyword-matching feature specifically:
Bad prompt:
"Which keywords from the job description are missing from the resume?"
Result: Sometimes correct. Often misses obvious ones or fabricates keywords that aren't in the JD.
Good prompt:
"Follow these steps in order:
- Extract every noun phrase and technical term from the JOB DESCRIPTION. Output them as a list under 'jdKeywords'.
- Extract every noun phrase and technical term from the RESUME. Output them as a list under 'resumeKeywords'.
- Compare the two lists. For each JD keyword, mark whether it appears in the resume (fuzzy match — 'PostgreSQL' matches 'Postgres').
- Output the final result as JSON with 'present' and 'missing' arrays.
Show your work for steps 1-3 in fields called '_reasoning'. In production I'll strip these before displaying to the user, but the model needs to think through them."
Result: Much more accurate. The model is forced to actually enumerate before comparing.
Then in your parser: delete the _reasoning fields before returning to the client. You paid for the tokens, the model needed them to think — but the user doesn't need to see them.
5. Add a "fail loudly" instruction.
Bad approach:
Trust the model to always return good output.
Result: Sometimes the input is a corrupted PDF, a blank resume, or a joke resume ("Sole responsibility: dominating in Call of Duty"). The model gamely tries to grade it and returns nonsense.
Good approach:
"If the input does not appear to be a real resume (e.g., blank, corrupted, or clearly a non-resume document), do NOT attempt to grade it. Instead return exactly:"
{ "error": "not_a_resume", "reason": "<one-sentence explanation>" }
Result: The model gracefully rejects garbage input and you can show the user a helpful error message instead of a fake 43/100 score.
The reliability compound effect
Individually, each of these tricks buys you 5-15% reliability improvement. Together they turned my analysis from "cool demo" to "shippable product."
If you're building anything with LLMs where output structure matters — pricing engine, grading tool, classifier, extractor — apply these. They're not Gemini-specific. Same tricks work with GPT-4, Claude, Llama.
Where I applied this
My app is Crux — a free Android AI resume checker on Google Play. Everything above is what makes the actual scoring feel reliable to users.
If you want to see it in action, it's here: https://play.google.com/store/apps/details?id=com.crux.ai.resume
If you're building something similar and want more depth on any of these — happy to write follow-ups. Drop questions in the comments.
Follow for more posts like this as I keep shipping.
Top comments (0)