DEV Community

Nishant prakash
Nishant prakash

Posted on

Teaching AI to Take Initiative – Building a Self-Thinking App with LangGraph and Ollama

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.

Agent?Really?

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.”
Enter fullscreen mode Exit fullscreen mode

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.

Flow Architecture

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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?"]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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?" ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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)