Introduction – When AI Becomes a Recruiter
Imagine uploading a job description and a candidate’s resume, and an AI instantly evaluates the match, gives ATS scores, writes tailored feedback, and even generates interview questions.
No spreadsheets. No manual parsing. Just structured, explainable intelligence.
That’s what I set out to build an Agentic AI Resume Evaluator powered by LangGraph, Ollama, and FastAPI.
Project Repository: github.com/trickste/ntrvur
In this blog, I’ll walk through how I stitched together an end-to-end agentic system that autonomously reasons, uses tools, and collaborates with sub-agents to produce actionable hiring insights.
By the end, you’ll see how agentic architectures, local LLMs, and good old Python orchestration come together to make AI do work, not just talk.
What Is Agentic AI, Really?
Agentic AI means giving AI the ability to think, decide, and act toward a goal, much like a human agent.
Instead of “just predicting text,” the AI plans its steps:
“Extract candidate details → Compute ATS score → Evaluate → Review → Merge → Return final report.”
That requires:
Tools – for specific actions (e.g., ATS computation, text extraction).
Memory & State – to carry information between steps.
Reasoning Loop – to know when to invoke which tool.
That’s where frameworks like LangChain and its orchestration layer LangGraph shine, they let you turn an LLM into a multi-step problem solver.
The Tech Stack – Tools Behind the Magic
Before diving into code, let's introduce the key players in this adventure:
| Component | Purpose |
|---|---|
| LangGraph | Builds and executes the agent workflow as a directed graph. Each node performs a task (ATS, experience extraction, etc.). |
| LangChain (Community) | Provides integrations and structured tool definitions (like StructuredTool.from_function). |
| Ollama | Runs local LLMs (like llama3 or phi3) offline, via ollama.Client. |
| FastAPI | Wraps everything in a clean REST API (/api/evaluate) for users to upload JDs & resumes. |
| scikit-learn | Handles ATS scoring via TF-IDF cosine similarity. |
| PyMuPDF (fitz) | Extracts text from PDF resumes reliably. |
With this stack, I had the ingredients for an AI that could think (LLM via Ollama), remember/learn (via LangChain’s memory or retrieval components), act (use tools or retrieve info, potentially via MCP or LangChain tools), and be deployed (FastAPI serving a LangGraph-managed workflow). Now it was time to design the brain of the operation – the agent’s architecture.
Designing an Agentic Workflow (Architecture)
The system runs like a small team of AI specialists working in sync. Once a JD and resume are uploaded, FastAPI triggers a LangGraph workflow, a series of smart nodes that extract the candidate’s name, compute the ATS score, and infer years of experience. A local Ollama LLM then wraps these findings into structured JSON.
From there, the Evaluator Agent analyzes the match and drafts interview questions, while the Reviewer Agent critiques and improves them. A final Synthesizer merges both perspectives into one coherent report, a clean orchestration of reasoning, review, and refinement.
Each part operates independently but shares structured state through LangGraph.
Let’s unpack the pieces.
Step 1: Agentic Evaluation with LangGraph
LangGraph was the backbone of my orchestration.
I defined a StateGraph with typed state fields using Pydantic, and created sequential nodes that act like tools in a workflow.
# agentic_evaluator.py
class EvaluationState(BaseModel):
jd_text: str
resume_text: str
ATS_SCORE: float | None = None
YEARS_OF_EXPERIENCE: int | None = None
CANDIDATE_NAME: str | None = None
Nodes as Actions
Each node modifies and returns the updated state:
def ats_node(state: EvaluationState):
result = ats_tool(state.jd_text, state.resume_text)
print(f"[ATS] → {result}")
return state.model_copy(update=result)
def experience_node(state: EvaluationState):
result = experience_extractor_tool(state.jd_text)
print(f"[Experience] → {result}")
return state.model_copy(update=result)
Finally, a small LLM finalizer via Ollama neatly packages all extracted values as JSON:
def llm_finalize_node(state: EvaluationState):
llm = ChatOllama(model="llama3", temperature=0)
data = {
"ATS_SCORE": state.ATS_SCORE,
"YEARS_OF_EXPERIENCE": state.YEARS_OF_EXPERIENCE,
"CANDIDATE_NAME": state.CANDIDATE_NAME,
}
prompt = f"Given this data, return valid JSON:\n{json.dumps(data, indent=2)}"
response = llm.invoke(prompt)
return json.loads(response.content)
LangGraph made it effortless to visualize this reasoning loop as a flowchart.
Each node is deterministic, while the last one invokes reasoning, the true “thinking” step.
Step 2: Evaluator Agent – LLM as the Hiring Analyst
Once the structured MCP (Model Context Protocol)-like output was ready, I passed it into an Evaluator Agent, an Ollama-powered LLM acting as a hiring analyst.
It uses templated system and user prompts stored as Markdown:
app/prompts/evaluator_system.md
app/prompts/evaluator_user.md
The system prompt strictly enforces a JSON schema with ATS score, resume feedback, and 10–15 interview questions.
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
llm = OllamaLLM()
raw = llm.chat_json(messages)
This agent reads the JD and resume, scores the match, and auto-generates a structured evaluation.
Step 3: Reviewer Agent – The Senior Interviewer
Next, the Reviewer Agent steps in, another Ollama instance prompted with:
- The JD and resume text
- Candidate experience level
- The evaluator’s generated questions
Its role: review the quality of those questions, detect missing areas, and produce a refined list.
This agent embodies human-in-the-loop reasoning, automated quality control.
review_json = run_reviewer(
jd_text=jd_text,
resume_text=resume_text,
questions=questions,
years_of_experience=years_of_experience
)
The output includes:
{
"QUESTION_REVIEW": {
"summary": "...",
"missing_topics": ["CI/CD", "Cloud Costing"],
"improvement_suggestions": ["Add a behavioral question"]
},
"UPDATED_QUESTIONS": ["Explain blue/green deployments?", "How do you manage secrets in CI/CD?"]
}
Step 4: Synthesizer – The Final Merge
Finally, I wrote a merge_evaluator_and_reviewer function to combine both agents’ results:
def merge_evaluator_and_reviewer(evaluator_json, reviewer_json):
candidate_name = next(iter(evaluator_json.keys()))
data = evaluator_json[candidate_name]
updated = {
"payload": {
candidate_name: {
"ATS_SCORE": data["ATS_SCORE"],
"RESUME_FEEDBACK": data["RESUME_FEEDBACK"],
"FINAL_QUESTIONS": list(dict.fromkeys(
data["INTERVIEW_QUESTIONS"] + reviewer_json["UPDATED_QUESTIONS"]
)),
"QUESTIONS_REVIEW_SUMMARY": reviewer_json["QUESTION_REVIEW"]
}
}
}
return updated
The result is a rich, structured payload ready for dashboards or downstream workflows.
Step 5: FastAPI – Making It Real
To expose the system, I used FastAPI for a clean HTTP interface:
@router.post("/evaluate", response_model=EvaluateResponse)
async def evaluate(jd_file: UploadFile, resume_pdf: UploadFile):
jd_text = (await jd_file.read()).decode("utf-8")
resume_bytes = await resume_pdf.read()
resume_text = extract_text_from_pdf(resume_bytes)
mcp_output = run_agentic_evaluation(jd_text, resume_text)
eval_json = run_evaluator(jd_text, resume_text, mcp_output["CANDIDATE_NAME"])
review_json = run_reviewer(jd_text, resume_text, eval_json["INTERVIEW_QUESTIONS"], mcp_output["YEARS_OF_EXPERIENCE"])
return merge_evaluator_and_reviewer(eval_json, review_json)
The beauty? Upload a JD and a resume, and within seconds you get:
{
"payload": {
"John Doe": {
"ATS_SCORE": 84.6,
"RESUME_FEEDBACK": { "Python": "Strong", "Docker": "Moderate" },
"FINAL_QUESTIONS": [ "Explain container orchestration?", "Describe CI/CD best practices?" ]
}
}
}
What I Learned
1. Agentic AI is orchestration, not magic.
Breaking down tasks into nodes and state transitions makes the system explainable and debuggable. LangGraph turns “AI reasoning” into visual, modular code.
2. Local LLMs are surprisingly capable.
Running llama3 and phi3 locally through Ollama gave me full control no latency, no API keys. And switching models was one-line config.
3. Strict JSON prompts save hours.
Enforcing schema at the prompt level eliminated parsing headaches. (Still, I built coerce_json() to auto-clean malformed outputs.)
4. Agents need reviewers too.
Having a second LLM review the first’s work brought real reliability a taste of collaborative multi-agent systems.
5. FastAPI makes deployment feel native.
Integrating async routes and file uploads made serving AI models feel as easy as any microservice.
Conclusion – Agentic AI for Real-World Automation
This project taught me that Agentic AI isn’t just theory, it’s a practical blueprint for intelligent systems.
By combining LangGraph (structured reasoning), Ollama (local intelligence), and FastAPI (deployment glue), I built a hiring-assistant AI that’s fast, explainable, and private.
But more than that, it changed how I think about AI apps, not as “chatbots,” but as collaborative systems of agents, each with a role, a goal, and a reasoning process.
If you’re looking to dive deeper, here are the docs that shaped my journey:
- LangGraph Documentation
- Ollama – Run local LLMs easily
- LangChain Tools and Agents
- FastAPI – Lightning-fast web APIs
- Model Context Protocol – Emerging standard for LLM-tool integration
Repo Snapshot
app/
├── core/config.py
├── routers/evaluate.py
├── services/
│ ├── agentic_evaluator.py
│ ├── evaluator_agent.py
│ ├── reviewer_agent.py
│ ├── synthesizer_agent.py
│ └── ollama_client.py
├── prompts/
│ ├── evaluator_system.md
│ ├── evaluator_user.md
│ ├── reviewer_system.md
│ └── reviewer_user.md
└── utils/json_safety.py
Final Thought:
Agentic AI is not about creating a single genius model, it’s about creating a team of smart, cooperative components.
And once they start working together you realize, automation can actually think.
Happy building, fellow tinkerers!


Top comments (0)