{"slug": "the-four-layers-of-ai-engineering-from-prompts-to-loops", "title": "The Four Layers of AI Engineering: From Prompts to Loops", "summary": "A developer outlined a four-layer framework for AI engineering — prompt, context, harness, and loop — using a code-review agent as a running example. The writeup argues that prompt engineering is only the innermost layer, and that supplying persistent project context such as Claude Code's CLAUDE.md files lets models judge code against team conventions rather than generic standards.", "body_md": "\"Prompt engineering\" has been a buzzword for a few years now. But if you look at what actually happens in practice, the prompt is only the innermost piece of the picture. I find it useful to think of AI engineering as four layers: prompt, context, harness, and loop. You start with the prompt at the center, and as you move outward, you deal with the information the model sees, then the code around the model, then the structure that keeps it running.\n\nTo be clear, this isn't an official industry taxonomy. The boundaries between layers overlap in places. But as a practical frame for deciding what to improve next in an AI system, it works well.\n\nDefinitions alone would be too abstract, so let's use the same scenario across all four layers: **a system where an AI reviews the code every time a PR is opened.** Watching the same goal change shape at each layer makes the role of each one clear.\n\nThe innermost layer. This is what you type directly into the chat box:\n\n**Example.** In the code review scenario, this is the stage where you paste a diff into a chat window and ask for a review.\n\nA bad prompt:\n\n```\nReview this code.\n```\n\nA better prompt:\n\n```\nReview this diff.\n- Check for potential bugs, then security issues, then performance, in that order.\n- Skip style nitpicks. The linter handles those.\n- If there's nothing wrong, just say \"no issues\". Don't invent problems to have something to say.\n- For each finding, include the line number and a suggested fix.\n```\n\nSame model, same diff, but the focus and consistency of the output change quite a bit, because you've spelled out what to look at, what to ignore, and what shape the answer should take. The \"don't invent problems\" line matters more than it looks. Without it, the model sometimes flags trivial style issues or manufactures findings just to produce something.\n\nThis is also the layer where most people stay when they first use AI. The problem is that no matter how much you polish the prompt, if the model doesn't know your project, the output stays generic. Even with the prompt above, the model has no idea what error handling convention your team uses or where this function gets called from.\n\nEverything you provide beyond the direct request, so the model has something to base its judgment on. This is the second layer, wrapped around the prompt:\n\n**Example.** In the code review scenario, context engineering means laying out the background the model needs instead of just throwing a diff at it.\n\n```\n[System instructions]\nYou are a senior reviewer on our team. Review against the conventions below.\n\n[Reference: team conventions]\n- Errors are returned as Result types, not thrown as exceptions.\n- All DB access goes through the repository layer.\n- External API calls must specify a timeout.\n\n[Reference: related code]\n- The relevant parts of the 3 files that call this function\n\n[Good examples: 2 past review comments]\n- The tone and depth of reviews our team actually writes\n\n[Prompt]\nReview this diff. (same as before)\n```\n\nNow the model judges the code against \"our team's standards\" rather than \"generally good code.\" If someone throws an exception in a place where the team convention calls for a Result type, the model can flag that violation. That's a finding you'd rarely get without context.\n\nCLAUDE.md in Claude Code is the classic example of this layer. Put your project structure, coding conventions, and build commands in there once, and you don't have to explain them in every prompt. You can also split rules by file type or directory with `.claude/rules/`, and provide documents like a DESIGN.md that captures design intent as context when it's needed. These files are persistent context: write them once, and they keep getting used. An implementation plan you write for a specific task, on the other hand, is closer to one-off context. If you keep the plan and its progress updated as you go, it helps the model keep track of the overall direction and where it currently is. Retrieving relevant documents with RAG and feeding them to the model is another typical form of context engineering. MCP is related to context in that it brings outside information to the model, but the part where you connect tools and manage calls belongs to the next layer, the harness. It's one of the clearest places where the boundaries blur.\n\nSo if a task depends on project-specific rules or related code, polishing the prompt only gets you so far. The model has never seen your codebase, and if you don't give it the information it needs, it has no choice but to fill the gaps with inference.\n\nThe harness is the code around the model call. Note that it wraps the call itself, not the model's output: assemble the prompt and context, call the model, validate what comes back, retry on failure. It's the software structure around that one cycle.\n\nIf the first two layers are about designing the input you hand to the model (not just what to instruct, but which documents to pull in and what to leave out), the harness is the code that processes that input reliably in a real system. It's traditional software engineering: validation, retries, exception handling. A familiar analogy is an external API client. It's a lot like putting timeouts, retries, and response validation around a remote service. The difference is that model calls add a new failure class on top of network errors and 5xx responses: semantic failures, where the output is malformed or plausibly wrong.\n\nModel output is probabilistic, so you get format errors and missing pieces. You ask for JSON and it comes back wrapped in a markdown code block, or with a field missing. The harness validates and retries on these failures, and it also manages tool calls and feeds their results back in.\n\n**Example.** To run the code review in GitHub Actions instead of a chat window, you need code around the model call. What follows is pseudocode for illustration.\n\n```\n# Pseudocode for illustration. In practice, use a Pydantic model or JSON Schema.\nREVIEW_SCHEMA = {\n    \"comments\": [{\"file\": str, \"line\": int, \"severity\": str, \"message\": str}]\n}\n\ndef review_pr(diff: str, context: str, feedback: list | None = None) -> list[Comment]:\n    retry_feedback = None\n    for attempt in range(3):\n        raw = call_model(\n            context=context,\n            diff=diff,                      # never mutate the original diff\n            feedback=feedback,              # failure records passed in by the outer loop\n            retry_feedback=retry_feedback,  # retry reasons travel on a separate channel\n            output_schema=REVIEW_SCHEMA,\n        )\n        try:\n            result = validate(raw, REVIEW_SCHEMA)\n        except ValidationError as e:\n            retry_feedback = f\"Schema error in previous response: {e}\"\n            continue\n        # cross-check that each file and line number belongs to an actual changed hunk\n        invalid = find_invalid_comments(result.comments, diff)\n        if invalid:\n            retry_feedback = \"Some comments don't match actual diff lines.\"\n            continue\n        return result.comments\n    raise ReviewFailed(\"Failed after 3 retries\")\n```\n\nThe harness does three things here. It requests structured output and validates it against a schema, it retries with the failure reason fed back in, and it cross-checks locations the model may have made up (files or lines that aren't in any changed hunk). That last one matters most. Prompts alone can't fully prevent bad output, so anything you can verify in code, verify in code. Retrying or failing on invalid comments, rather than silently filtering them out, is also a deliberate choice. If you just drop the made-up comments, a response where every comment is invalid becomes an empty list, which reads as \"no issues found.\"\n\nThis validation and recovery logic is what turns a model call into a reliable software component. Things that work fine in a demo fail intermittently in production, on parsing or validation, and catching those failures is the harness's job. A production harness also includes logging, cost tracking, and collecting failure cases. If you don't record failures, you don't know what to improve.\n\nBy this post's classification, Claude Code itself can be seen as a harness around the model. It provides the execution structure: not just model calls, but tool execution, file editing, and permission checks. If you use Claude Code, you're already using a harness every day.\n\nThe difference between context and harness shows up clearly if you compare CLAUDE.md with hooks. Write \"run the linter before committing\" in CLAUDE.md and it's context. It nudges the model to comply, but nothing guarantees it happens. Configure a `PreToolUse` command hook that fires before `git commit` runs, and now the commit can be blocked in code when the lint fails. Same goal, different enforcement depending on which layer it lives in. Things you can leave to the model's judgment go in context; things that must be guaranteed go in the harness.\n\nBut if retries are the harness's job, what's different about the next layer, the loop? The unit of retry. The `for attempt in range(3)` in the code above retries the same model call until it gets output in the right shape. The goal is one valid response. A loop, on the other hand, repeats the entire review-fix-test cycle until it reaches a target state. The harness builds reliable parts; the loop assembles those parts and drives them toward a goal.\n\nThe outermost layer. This is where the system starts running on its own:\n\nEven a system with a solid harness stops after one run, which means a human has to keep issuing the next instruction. A loop sets a goal and stop conditions, like \"until all tests pass\" or \"until lint errors hit zero,\" and lets the system drive itself.\n\n**Example.** Let's push the review system one step further: instead of just pointing out problems, it fixes them too, repeating review, fix, and re-verify on its own. Pseudocode again.\n\n``` python\nMAX_ITERATIONS = 5\n\ndef auto_fix_pr(pr):\n    feedback = []                                   # loop state that persists across iterations\n\n    # check the baseline first: tests that were already broken go to a human\n    baseline = run_tests(pr)\n    if not baseline.passed:\n        return pr.request_human_review(\"Tests were failing before auto-fix started\")\n\n    for i in range(MAX_ITERATIONS):\n        comments = review_pr(pr.diff, pr.context, feedback=feedback)  # reusing layer 3\n\n        if not comments:\n            result = run_tests(pr)\n            if result.passed:\n                # mark the AI review as passed; final approval stays with a human\n                return pr.request_human_approval(\"Passed AI review and tests\")\n            # no comments but tests fail: skip fixing, go to the next iteration\n            feedback.append({\"type\": \"test_failure\", \"log\": result.failure_log})\n            continue\n\n        pr, fix_commit = apply_fixes(pr, comments)  # keep the fix commit ID\n\n        result = run_tests(pr)                      # self-check\n        if result.passed:\n            feedback = []                           # clear resolved failure records\n        else:\n            pr = revert_commit(pr, fix_commit)      # revert exactly the commit the AI made\n            feedback.append({                       # carry the failure log to the next iteration\n                \"type\": \"test_failure\",\n                \"log\": result.failure_log,\n            })\n\n    return pr.request_human_review(\"Unresolved after 5 iterations\")  # stop condition\n```\n\nEverything is in there: a goal (no comments plus passing tests), self-checks (running tests), adjustment (revert on failure and carry the log forward in `feedback`), and stop conditions (5 iterations max, then escalate to a human). The fact that `auto_fix_pr` calls `review_pr` from layer 3 directly is the relationship from the previous section: the loop assembles the parts the harness built.\n\nFailure logs go into `feedback` outside the loop body, not into a local variable of the current iteration. That's what lets the next model call actually see the earlier failures. And once a fix passes the tests, the accumulated failure records get cleared. There's no reason for the next review to keep referring to errors that were already resolved.\n\nFix commits are only created when there are comments, and the revert targets the exact commit ID that `apply_fixes` returned. Reverting \"the last commit\" can delete the wrong commit if a human pushes while the loop is running.\n\nChecking the baseline tests before entering the loop follows the same logic. To treat a test failure as the result of an AI fix, you first have to know the PR's tests were passing before the auto-fix started. If they were already failing, reverting the last fix commit won't solve anything.\n\nIt also matters that meeting the conditions doesn't auto-approve the PR. The AI finding no comments is not a guarantee the code is fine, so final merge approval stays with a human. Permission control spans both the harness and the loop, but it becomes especially important at the loop stage, once the system starts acting repeatedly. In real operations, it's safer to auto-fix only low-risk categories like formatting rather than applying every comment automatically.\n\nClaude Code fixing code, running tests, reading the failures, and fixing again is a good example of a loop.\n\nThe heart of it is the [stop conditions](https://dev.to/claude-code-agent-loops/). Without a max iteration count, a cost ceiling, and a success criterion, a loop either runs away or spins in place. Take `MAX_ITERATIONS` and the human escalation out of the example above, and you have a system that burns tokens forever on a PR whose tests can never be fixed.\n\nWithout a loop, a human has to check the result and issue the next instruction on every iteration. Iteration with self-checks is a core ingredient of agent systems. But an actual agent combines this with tool use, state management, and permission control, so a loop by itself is not the whole of an agent.\n\n| Layer | What it does | In the code review scenario | Without it | \n|---|---|---|---|\n| 1. Prompt | The request you type | \"What to look at, what to ignore\" | Nothing starts | \n| 2. Context | Background the model sees | Team conventions, related code, past reviews | It guesses | \n| 3. Harness | Validation, retries, tool wiring | Schema validation, line-number cross-checks | It wobbles | \n| 4. Loop | Goals, stop conditions, iteration | Review-fix-test cycle, human keeps final approval | It stalls | \n\nA great prompt without context guesses. Great context without a harness wobbles. A harness without a loop stalls. And for repetitive work without a loop, you become the bottleneck.\n\nIf all you've been doing is polishing prompts, you've been working on just the innermost layer. When the results still disappoint, the next improvement may be further out.\n\n*Originally published at [jamongx.com](https://jamongx.com/four-layers-of-ai-engineering/).*", "url": "https://wpnews.pro/news/the-four-layers-of-ai-engineering-from-prompts-to-loops", "canonical_source": "https://dev.to/jamongx/the-four-layers-of-ai-engineering-from-prompts-to-loops-2997", "published_at": "2026-09-21 14:12:17+00:00", "updated_at": "2026-09-21 14:32:38.445624+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "large-language-models", "ai-products"], "entities": ["Claude Code"], "alternates": {"html": "https://wpnews.pro/news/the-four-layers-of-ai-engineering-from-prompts-to-loops", "markdown": "https://wpnews.pro/news/the-four-layers-of-ai-engineering-from-prompts-to-loops.md", "text": "https://wpnews.pro/news/the-four-layers-of-ai-engineering-from-prompts-to-loops.txt", "jsonld": "https://wpnews.pro/news/the-four-layers-of-ai-engineering-from-prompts-to-loops.jsonld"}}