An AI construction inspection is only worth sending when each defect carries a clause reference and a named supervisor who verified it
The first AI construction inspection report we sent to a contractor came back with three words: says who, exactly. The finding was correct. The photo showed it, the description was accurate, and nothing in that email connected it to a line in the project specification or to any standard. He was right to push back, and that reply changed the architecture more than any model upgrade did.
### What one AI construction inspection produces
One inspection run produces three artifacts: a verified defect list, a report in the company's own format, and one email per responsible contractor containing only his trade's findings. Everything else in the system exists to make those three defensible when somebody disputes them.
The chain is short. Photo and inspector note go in, specialist agents analyse in parallel, retrieval attaches supporting references, a person confirms or rejects, a model writes the report into a company template, notifications go out per trade.
Orchestration is LangGraph over FastAPI with Dramatiq workers, so an inspection is a checkpointed background job and not a long HTTP request.
Nothing in that list is exotic. What makes it work is that no stage produces output the next stage cannot check.
A specialist agent is a model call scoped to one trade, with its own prompt, its own slice of the retrieval corpus, and its own output schema. Moonraker Inspector splits by trade, so structural, piping and drainage, electrical, tiling and finishes, and safety and compliance, because that split maps onto who receives the email at the end.
Responsibility gets assigned at the moment of analysis instead of being guessed later. A piping finding was produced by the piping agent, so it goes to the plumbing subcontractor. There is no classification step downstream, and no place for that step to be wrong.
Narrow scope also raises what gets seen. One prompt asked to find every defect in a site photo reports the visually loudest two or three and stops, with no error and no low confidence to warn you, which is quiet failure and the dangerous kind in a product. An agent looking only for water and drainage will examine staining around a shower tray that the general prompt walked past because it had already found the cracked tile.
The cost is honest and close to linear. The photo is re-sent to every agent, so input tokens scale with the number of trades in scope, and latency is whatever the slowest agent takes. Prompt caching helps on the rule block, not on the image, and the image is the expensive half.
The corpus holds the documents for that specific project, not general construction knowledge. Three kinds of thing: the specification and drawings for the project, the applicable building regulations, and the standards and normatives the project was designed against, so Eurocodes and the MKS EN adoptions we work with here in North Macedonia, EN 806 for water installations, HD 60364 for electrical work.
Grounding does exactly one job here: attach a citable reference to a finding so the person receiving it can verify it himself. A tile flatness complaint carrying a tolerance from the project's own finishing specification is a different object than the same complaint without it. First one gets fixed. Second one gets the reply I opened with.
Storage is pgvector inside the same PostgreSQL the product already runs on, with hybrid search, because clause numbers and standard codes are exact strings that vector similarity handles badly. Somebody searching EN 806-2 wants that document, not nine chunks that sit near it in embedding space.
Every chunk carries project id, document revision, trade and effective date, and the filter runs before scoring:
-- revision filter belongs in the WHERE, not in a post-filter after top-k.
-- found this out when retrieval kept returning revision B of a spec
-- that revision C had already superseded, and nothing looked broken
SELECT chunk_id, content, source_ref
FROM doc_chunks
WHERE project_id = $1
AND trade = $2
AND is_current_revision
AND effective_date <= $3
ORDER BY embedding <=> $4
LIMIT 8;
Superseded revisions are the failure mode that worries me most in this corpus. A model quoting an obsolete tolerance with full confidence produces a finding nobody catches by reading it, because it looks exactly like a correct one. Old revisions stay in the table for audit and never enter retrieval.
The verification gate is a hard stop where a supervisor confirms, edits or rejects each grounded finding before any report is generated or any email is sent. In LangGraph that is interrupt, and as of August 2026 it needs a real checkpointer, otherwise the state you want to resume into is gone when the worker process ends.
# langgraph 0.6.x + langgraph-checkpoint-postgres
# first version used MemorySaver, worked perfectly in dev,
# then lost every pending inspection on the first worker restart
from langgraph.types import interrupt, Command
from langgraph.checkpoint.postgres import PostgresSaver
def verification_gate(state: InspectionState) -> dict:
decisions = interrupt({
"findings": [f.model_dump() for f in state["findings"]],
"action": "confirm_reject_or_edit",
})
kept = [f for f, d in zip(state["findings"], decisions) if d["keep"]]
return {"verified": kept, "rejections": [d for d in decisions if not d["keep"]]}
with PostgresSaver.from_conn_string(DSN) as saver:
app = graph.compile(checkpointer=saver)
# resumed hours later from the web app, same thread_id
app.invoke(Command(resume=decisions), config={"configurable": {"thread_id": tid}})
Rejections are stored, never discarded. A supervisor rejecting the same category of finding every week is the cheapest eval signal in the product, and it arrives labelled, for free, from somebody who was standing in the room. We have changed two trade prompts on that evidence alone.
The gate is also the legal position. Somebody put his name on that defect list, so when a contractor disputes a finding the answer is a named supervisor plus a clause, and the argument finishes quickly.
Agents also confabulate when their domain is empty. Point the electrical agent at a photo of floor tiling and it will report a socket that is not in the frame, plausible enough to pass schema validation. An explicit no-findings path in the prompt, a required region reference on every finding, and a closed category enum validated through Pydantic with structured outputs reduce it. They do not remove it. The verification gate is what removes it.
Each company defines its own report format, and the model fills declared slots inside that format instead of writing a document from scratch. Construction companies already have handover formats their clients accept, sometimes formats a client requires contractually, and a well-written report in the wrong shape is worth nothing to them.
Structure stays deterministic. Section order, numbering, defect table columns, severity vocabulary and header block belong to the template. The model writes prose only, so per-defect description, trade summary, overall condition paragraph, and every generated block is validated against the slots the template declared. A missing slot fails the render instead of shipping a report with a quietly empty section.
This is the least interesting engineering in the product and one of the two things buyers ask about on the first call. Other one is who is liable for a wrong finding.
Each contractor receives one email per inspection containing the full findings for his trade: photo, description, severity, location, and the specification or standard reference behind each one. No dashboard login, no "3 new issues, click to view". The subcontractor tiling a bathroom is not going to create account in our product to read a complaint about his work.
Sending is a state machine with its own table, since email is the part of this pipeline that touches the outside world and cannot be retried carelessly. Each notification records which findings it covered and which revision of them, so a corrected finding goes out as a new record referencing the old one and the contractor can see what changed.
One mistake worth naming, since I made it. First version was fire and forget. Contractors replied to those emails with photos of repaired work, and the replies landed in a mailbox nobody was reading, which is rude thing to do to a person doing his job. Replies now thread back onto the finding, and closing that loop turned out to be the feature supervisors asked for next. I did not see it coming.
StageWhat it addsWhat it costsAgent per tradeHigher recall, responsibility assigned during analysisPhoto re-sent per agent, input tokens scale with trade countRetrieval over project documentsClause reference on each finding, disputes end fasterIngestion per project, revision hygiene foreverHuman verification gateNamed accountability, labelled rejection dataSupervisor time on every inspection, pipeline is not autonomousCompany report templateOutput in a format the client already acceptsTemplate onboarding per company, slot validation to maintainPer-trade emailReaches people who will never log inDeliverability, threading, reply handling
Can AI replace a construction site inspector?
Not in this product, and I am not trying to. Agents find and ground candidate defects, and a supervisor confirms every one before it reaches a contractor. What gets removed is documentation work: writing findings up, matching them to specification clauses, formatting the report and routing it to the right subcontractor.
How does RAG help with construction defect detection?
It supplies the reference that makes a finding actionable, retrieving from the project's own specification and drawings plus the regulations and standards the project was designed against. Retrieval must filter on document revision before scoring, otherwise a superseded tolerance gets quoted confidently and looks completely normal in the finished report.
Why use multiple AI agents for photo inspection instead of one prompt?
One prompt over a site photo reports the visually loudest defects and stops, with no error and no low confidence to signal what it skipped. Splitting per trade gives each domain its own scope, rules and retrieval slice, and it assigns responsibility during analysis instead of guessing later who should receive the finding.
How do you keep an AI inspection report in a company's own format?
Keep structure out of the model. The template owns section order, numbering, columns and severity vocabulary, the model fills declared prose slots, and a schema validates that every declared slot exists before the document renders.
Whether the verification gate can ever be sampled instead of complete is the open question. Confirming every finding is right at this stage and it also puts a ceiling on how many inspections one supervisor processes in a day. Sampled verification lifts that ceiling and means some unverified finding eventually reaches a contractor, and I do not yet know how to price that risk in way I would defend to a client paying in EUR for exactly this reliability.
The other thing I keep circling is retrieval at clause boundaries. Normative documents split badly, since a requirement and its exception often live in different clauses, and chunking that keeps them together for one standard breaks another. I have tried three chunking strategies already and results were different than I expected each time, which usually means the strategy is not the variable that matters.