Back to resources
// technical · agents

From a prompt to agentic AI

My first LangGraph agent, rebuilding the AI in Klickbee — and why a good prompt stops being enough the moment the task gets real.

TechnicalJul 6, 2026~8 min · intermediate
LangGraphLLMZodPostgresLangfuseGitLab API
01

At the start: a prompt, a JSON object

The first version of Klickbee's AI came down to one simple idea: a prompt, a response. The user writes a brief, we drop it into a big prompt, and the model returns a structured object — a roadmap with its milestones and tasks. Technically: a single generateObject call, a Zod schema to guarantee the shape.

And honestly, it works. To turn “redesign the marketing site” into a plausible roadmap, a good prompt is enough. Fast to write, easy to understand, cheap.

i

The problem: this model hits a ceiling the moment the task gets real.

02

The limits of the one-shot

Actually using the product, four flaws keep coming back. None of them is fixed with “a better prompt” — they are architectural.

01
It invents instead of investigating

Rebuilding a GitLab repo's roadmap from a plain prompt yields a believable fiction, not what actually happened. It has no access to commits, merge requests, issues.

02
It has no memory

Every exchange starts from scratch. The user explains a constraint, and two messages later the AI asks for it again. It turns into an interrogation.

03
It never checks itself

A one-shot returns its first answer, good or bad. No stepping back, no self-critique.

04
It fails silently

One mistyped field from the model and the whole turn dies with a “Failed to parse” that means nothing to the user.

03

The shift to agentic

“Agent” is not a buzzword for “a slightly smarter prompt.” The real shift is going from generating an answer to carrying out a task. In Klickbee, every important AI became a graph (LangGraph) with explicit steps.

// the graph (langgraph)
gather
context
clarify
ask ?
draft
decide
review
self-review
apply
apply
review ↺ draft — loop if not enoughclarify → human-in-the-loop
Postgres — durable stateLangfuse — traced
gather context — via tools: read memory, read real GitLab activity, list existing projects;
decide — either ask a question, or produce concrete operations;
self-review — the model critiques its own plan and loops if it falls short;
ask the human when it is genuinely blocking (human-in-the-loop, with interrupt and resume);
apply.

The whole thing is durable — state persists in Postgres, a run survives a crash or a later resume — and traced: every step, every LLM call is visible in Langfuse to debug and score.

a prompt

answers.

one input, one output
no access to reality
no memory between turns
an agent

gathers, decides, verifies, collaborates, and resumes.

grounds itself on real data via tools
reviews and corrects itself before showing
remembers, and resumes where it left off
04

Four concrete cases

Roadmap generation

The brief now runs through gather → clarify → draft → review → validate → apply. The draft node is itself agentic: a loop where the model calls its tools to ground itself before writing. If a key detail is missing — deadline, scope — it asks a question instead of guessing. Then it reviews and corrects itself before showing anything.

Reconstruction from GitLab

Here grounding changes everything. The agent reads the real commits, merge requests and issues, autonomously, and reconstructs a roadmap faithful to what was actually shipped — instead of an invented story.

i

Telling detail: the GitLab API returns no commit total on some endpoints; we had to paginate to stop showing “100 commits” forever. Agentic work forces you to face the real thing.

Adjustment chat

This is where the lesson was sharpest. The first version interrogated the user, one question after another, forgetting previous answers. We flipped it to act-first: act by default with explicit assumptions, ask only on a real blocker, and remember the whole conversation. Like a good colleague, not a form.

Team generation

The newest one: you describe “who works on what,” the agent labels the roles, composes the teams, and — because it reads the existing roster — reuses the people and teams already there instead of creating duplicates. It asks a question when two people step on each other.

05

The upside — and the cost

What agentic buys you, concretely:

Grounding over fluff: the AI works on real data, not guesses.
Memory: decisions and preferences survive across exchanges.
Self-verification: the model reviews itself; on the dev side, we even add an adversarial review of the changes.
Collaboration: it asks when it must, acts otherwise — no more interrogation or confident hallucination.
Robustness and observability: tolerant structured outputs, progress streaming, durability, full traceability.
!

Let's be honest about the bill: more complexity (a graph, nodes, tools), more tokens (several calls instead of one), more infra (a database for state, tracing). A one-line generateObject becomes a subsystem.

a prompt is enough
rephrase a text
classify, categorize
extract a field from a document
the agent wins
investigate the real world
hold over time (memory, resume)
verify itself and collaborate with a human
06

The mental shift

The real click isn't technical, it's a change of posture. You stop asking “what prompt will give me the right answer?” and start asking “what steps would a competent person follow to do this work?” — then you give the AI the tools, the memory and the checkpoints to follow those steps.

In Klickbee, that's what took the AI from a demo that impresses to a tool you can actually work with.

07

What makes a graph “agentic”

There's an ambiguity I took a while to clear up myself: a LangGraph graph is not automatically an agent. Anthropic draws the sharpest line I know — a workflow orchestrates LLMs and tools along predefined code paths, whereas an agent lets the model direct its own process and decide which tools to call. In Klickbee, the overall graph stays largely a workflow (gather → clarify → draft → review → apply, fixed edges), but the draft node is genuinely agentic: a loop where the model chooses to call this tool, loop again, or stop.

i

The rule of thumb: stay in workflow mode as long as the path is known in advance, and only open an agentic zone where the “what next” decision genuinely depends on what the model just discovered. Every autonomous turn costs latency, tokens, and lets an early mistake propagate.

Durable state, concretely

The durability I mentioned isn't a vague “we save to a database.” LangGraph models it with two precise notions. A checkpointer persists graph state at every superstep, tied to a thread_id: that's what lets a run survive a crash and resume exactly where it stood. And a store serves the durable memory that spans threads — the preferences and decisions that must survive from one conversation to the next. The adjustment chat's memory is the store; resuming after an interrupt is the checkpointer.

checkpointerone thread's state, snapshot at each step — resume, time-travel, crash tolerance
storelong-lived memory across threads — preferences, decisions, roster
thread_idthe key that ties a run to its checkpoint history

The interrupt is not a try/catch

Human-in-the-loop had the subtlest trap in store for me. When a node calls interrupt() with a JSON-serializable value, the graph freezes its state and waits indefinitely; you resume later, possibly in a different HTTP request, with Command(resume=…) and the same thread_id — the human answer becomes the return value of interrupt. The trap: on resume, the node restarts from the beginning, not from the interrupt line. Any code that ran before is replayed. The lesson: put side effects (writes, non-idempotent API calls) after the interrupt point, not before.

draft-node.tscopy
// Le nœud agentique : boucle outillée + auto-relecture avant de rendre
async function draftNode(state: GraphState) {
// effets de lecture seule : OK avant un éventuel interrupt
const grounding = await gatherTools.run(state);
let draft = await model.generateObject({
schema: RoadmapSchema, // Zod : la forme est garantie ou l'appel échoue
prompt: buildDraftPrompt(state, grounding),
});
// boucle évaluateur-optimiseur : le modèle critique son propre plan
for (let i = 0; i < MAX_REVIEWS; i++) {
const review = await model.generateObject({
schema: ReviewSchema, // { ok: boolean; issues: string[] }
prompt: `Critique ce plan, cite ce qui manque :\n\${JSON.stringify(draft)}`,
});
if (review.object.ok) break;
draft = await model.generateObject({
schema: RoadmapSchema,
prompt: buildRevisePrompt(draft, review.object.issues),
});
}
// blocage réel : on demande à l'humain. Les écritures viennent APRÈS.
if (needsHuman(draft)) {
const answer = interrupt({ question: whatIsMissing(draft) });
return applyHumanAnswer(draft, answer);
}
return { draft: draft.object };
}
08

The self-review loop, in detail

“It reviews itself” deserves to be taken apart, because it's a named, studied pattern, not a homemade trick. Anthropic calls it evaluator-optimizer: one LLM produces, a second evaluates and returns feedback, you loop. It only shines under two conditions — clear evaluation criteria, and measurable gain per iteration. Roadmap generation meets both: “does every task have a parent milestone? are there duplicates? is the brief's scope covered?” are checkable questions.

The most ambitious version of this pattern is called Reflexion. LangChain describes it well: instead of purely internal self-critique, the actor grounds its criticism in external data — it fetches evidence before judging itself. The Reflexion guide breaks it into three roles: an actor that produces, an evaluator that scores the trajectory, and a self-reflection step that turns that score into a verbal instruction for the next round. That's exactly what my draft node does when it re-reads the real GitLab commits before validating a plan.

internal self-critique

the model judges itself on what it “thinks.”

cheap, one model, two prompts
can reinforce its own mistake
good for shape, style, coherence
grounded reflection (Reflexion)

the model fetches evidence before judging itself.

more tokens, tool calls
fixes facts, not just form
essential the moment reality is involved
!

A loop with no guardrail either spins forever or blows up the bill. Three protections: an iteration cap (MAX_REVIEWS), a clear stop condition (“review.ok === true”), and a validated structured output every round. On my side the Zod schema pulls double duty: generation fails rather than returning an invalid object, turning a silent “Failed to parse” into an error you can catch and retry.

09

Sources & further reading

01Anthropic — Building Effective AgentsThe reference on workflow vs agent and on the evaluator-optimizer pattern. Read it before adding any autonomy.02LangGraph — Persistence (checkpointers & threads)How state becomes durable: per-thread checkpointer, cross-thread store, resume after crash.03LangGraph — Interrupts (human-in-the-loop)interrupt() and Command(resume=…), plus the trap of the node replaying from the start on resume.04LangChain — Reflection AgentsGeneration/critique loop and the Reflexion implementation as a LangGraph graph.05Prompt Engineering Guide — ReflexionThe three roles — actor, evaluator, self-reflection — explained plainly.06AI SDK — Generating Structured DatagenerateObject + Zod schema: validated outputs, and the error thrown when the object is invalid.

The real question isn't “prompt or agent?” but “what does the task demand?” Rephrase, classify, extract: a prompt is enough. Carry out a task that must investigate, hold over time, verify itself and collaborate: the agent wins, by a lot.