All posts
· Asif

From Prompt to Graph: A Field Guide to How We Talk to AI

Engineering
From Prompt to Graph: A Field Guide to How We Talk to AI

Introduction: Four Layers, One Question

Every wave of AI engineering has answered the same question a little differently: how do we get a language model to reliably do what we want?

First we obsessed over the wording of a single instruction. Then we realized the instruction was never the bottleneck — what surrounded it was. Then we started asking the model to act, not just answer, which meant repeating that cycle in a loop. And now, as tasks span multiple specialized agents working together, the industry is talking about graphs: networks of nodes that pass a shared, structured state — made of typed "fields" — along their edges.

None of these layers replaced the one before it. They stacked.

1. Prompt Engineering: Choosing the Right Words

Prompt engineering is the original discipline: crafting the single instruction that goes into the model so it produces the output you want. It's about wording, examples, structure, and constraints — role framing, few-shot examples, chain-of-thought nudges, explicit output formats.

You are a senior copy editor. Rewrite the paragraph below for clarity
and brevity. Keep the meaning identical. Return only the revised text,
no commentary.

Paragraph:
"""
{input_text}
"""

This works well for single-turn, self-contained tasks. It starts to strain the moment a task needs outside information, memory of earlier steps, or the ability to take actions — because no amount of clever wording can hand the model facts it was never given.

2. Context Engineering: Curating What the Model Sees

Context engineering is the discipline of designing the system around the prompt: deciding what information, tools, and history the model sees, in what format, at what moment. The distinction is simple but important — prompt engineering asks "how do I phrase this instruction," context engineering asks "what should be in the model's window at all, and what should be left out."

A widely used way to think about it, popularized in the LangChain ecosystem, breaks the job into four moves:

  • Write — author the instructions and scratchpad the agent will use.
  • Select — retrieve only the facts relevant to this step (RAG, memory lookups, file search).
  • Compress — summarize or prune so the window doesn't collapse under its own weight.
  • Isolate — split unrelated work into separate context windows so one task's noise doesn't contaminate another's.
def assemble_context(user_query, memory_store, tool_registry):
    system_prompt = load_system_instructions()
    relevant_docs = retrieve(user_query, top_k=5)       # select
    relevant_docs = compress(relevant_docs, max_tokens=2000)  # compress
    recent_history = memory_store.get_recent(user_query)      # select
    tools = tool_registry.describe_available()

    return {
        "system": system_prompt,
        "context": relevant_docs,
        "history": recent_history,
        "tools": tools,
    }

This is also where long-term memory enters the picture — vector databases, knowledge graphs, or structured logs that let an agent carry information across sessions instead of starting cold every time.

3. Agentic Loops: From One-Shot to Iterative

A single well-built context still only gets you one turn. Most real tasks need the model to act, see what happened, and decide what to do next — repeatedly. That's an agentic loop: think, act, observe, repeat, until some stopping condition is met.

def agent_loop(task, context, tools, max_steps=10):
    state = {"task": task, "context": context, "history": []}

    for step in range(max_steps):
        response = call_model(state)

        if response.is_final_answer:
            return response.content

        tool_result = execute_tool(response.tool_call, tools)
        state["history"].append((response.tool_call, tool_result))

    return "Max steps reached without a final answer."

A loop's "harness" — the code wrapped around the model call — has to manage the prompt, the context window, the token budget, and retry logic when a tool call fails. That's usually simple enough to fit in a script. The trouble starts when one agent, one loop, has to own too much: research, writing, validation, and hand-off, all at once. Quality drops, context gets noisy, and failures are hard to isolate.

4. Graphs with Fields: Coordinating Many Agents Through Shared State

The current layer being built on top of loops is graph engineering: instead of one agent looping alone, you wire multiple specialized agents, tools, validators, and even human checkpoints into an explicit graph. Nodes do the work. Edges route control from one node to the next, sometimes conditionally. And a shared state object — with typed fields — flows along those edges, so every node can read what came before and write what it produced.

The "fields" are the point: instead of passing around a loose blob of text, the graph defines a schema — a fixed set of named, typed fields (a draft, a list of citations, a validation flag, a retry count) that every node contracts against. That's what makes a multi-agent system debuggable instead of a game of telephone.

from typing import TypedDict
from langgraph.graph import StateGraph, END

class ResearchState(TypedDict):
    query: str
    draft: str
    citations: list[str]
    is_verified: bool
    retries: int

def researcher(state: ResearchState) -> ResearchState:
    state["draft"] = research_and_draft(state["query"])
    state["citations"] = extract_citations(state["draft"])
    return state

def verifier(state: ResearchState) -> ResearchState:
    state["is_verified"] = check_citations(state["citations"])
    return state

def route_after_verify(state: ResearchState) -> str:
    if state["is_verified"]:
        return "publish"
    if state["retries"] >= 2:
        return END
    state["retries"] += 1
    return "researcher"

graph = StateGraph(ResearchState)
graph.add_node("researcher", researcher)
graph.add_node("verifier", verifier)
graph.add_node("publish", lambda s: s)

graph.set_entry_point("researcher")
graph.add_edge("researcher", "verifier")
graph.add_conditional_edges("verifier", route_after_verify)

app = graph.compile()

Frameworks like LangGraph, CrewAI, and AutoGen — along with cross-vendor efforts like Google's A2A protocol for agent-to-agent coordination — have made this pattern a practical default for regulated or high-stakes workflows in 2026, precisely because the explicit state schema makes it possible to trace which node changed what, and when.

The honest caveat: a graph harness is closer to a small distributed system than a bash script. It buys you inter-agent routing, node-level failure isolation, and observability — at the cost of real complexity. Most tasks still don't need it. A single well-context-engineered loop remains the right tool for anything that one focused agent can finish end to end.

Putting It Together: A Stack, Not a Ladder

It helps to picture these four layers as concentric, not sequential:

  • Prompt engineering still happens inside every node of every graph — someone still has to word the instruction well.
  • Context engineering decides what each node sees before it acts.
  • Loops are what happen inside a node when a single agent needs several steps to finish its piece.
  • Graphs with fields coordinate multiple loops, giving them a shared, typed state to hand off instead of a wall of raw text.

None of them retired the layer beneath it. They just moved the hard problem one level up — from "what do I say" to "what does it see" to "how does it act" to "how do many of them act together, without losing track of what's true."

Asif

Asif

Developer who cares about fast, accessible, well-designed software.