Ontology Shape vs Model Size: What a Graph's Design Costs an AI Agent
How three lossless shapes of one insurance knowledge graph changed accuracy, tool calls, tokens and cost for small and large models on Azure Cosmos DB.
This article is published in English.
When an agent underperforms, the fix most teams reach for is a bigger model. It works, too, at roughly fourteen times the price of fixing the graph instead. That figure comes from an experiment where the same facts were modeled three different ways and a single agent on Microsoft Foundry got four traversal tools and no other route to the answer. What was measured is the cost of the shape of knowledge, per question.
Three layouts of one fact base are drawn above, 2,435 facts in all. Every panel is a genuine subgraph taken from the experiment data, showing how each ontology holds the neighborhood of the very same three insurance claims. In the flat layout the structure is tucked inside properties, so hardly anything links up. The normalized layout makes each fact a vertex of its own: nothing is missing, yet nothing carries a name. The shaped layout types every entity and names every edge. Agent, model and database stay identical across the three, and shape is the only thing that moves.
First, a word on the data. Everything in it is invented: people, companies, places and the insurer come out of made-up word lists with a fixed seed, so none of it points at anything real. The measurements come from live runs in Sweden Central. Reasoning happened on a dedicated Microsoft Foundry deployment, the knowledge lived in an Azure Cosmos DB for Apache Gremlin account, and each tool call was a Gremlin query hitting a live graph. Altogether that came to 480 agent episodes, 4.3 million tokens and $8.09. A first pass over the same grid, where the tools read the generator files rather than the database, cost $7.74; below it is called the cross-check run. All reported figures come from the Cosmos campaigns, and the cross-check is brought in only when two runs together show something one run could not.
The layer nobody upgrades
Teams that ship agents tend to repeat a little ritual. The agent fails on questions it should handle. Somebody suggests a bigger model. The invoice doubles, the failures move to other questions, and the retro concludes that agents are not ready yet.
Hardly anyone puts the actual material on the table: the shape of the knowledge the agent was given. Forget the facts for a moment. Ask whether the adjuster is a node or a string buried in a JSON blob, whether the edge reads assessed_by or merely related, and whether getting from a claim to a policyholder takes one hop or five.
Previous measurement work covered a system that makes decisions and a tool that locks those decisions in. Elsewhere, a three-part public series assembled a persistent knowledge layer, ran agents on it over its own synthetic insurance data, and made the case for treating knowledge as a designed artifact instead of a retrieval bucket. What that case lacked was a measurement, and this experiment supplies one. It drops a layer to the substrate the agents reason over and poses a question that seldom gets numbers: with the model, tools, prompts and facts frozen, and only the ontology shape altered, how do accuracy, effort and cost respond?
That first answer invites an objection, so the entire grid was rerun on a much larger model to see whether money can simply outrun a poorly shaped graph. It can. The price is the interesting bit, along with what it says about the shape worth building. As before, every claim needs a receipt.
Three shapes of one truth
The test domain is an insurance book of business with policyholders, policies, coverages, claims, adjusters, repair providers, payments and regions. A seeded script produced all of it once, yielding 2,435 canonical facts across 507 entities, every one fictional.
Three ontologies come out of that one fact base. Calling them three datasets would be wrong; they are three shapes of the same data, under a single governing rule: each variant must restate the facts with no gain and no loss. To enforce it, a script walks every built graph, reconstructs the facts, and asserts the result equals the source exactly. A variant that knew one fact more or less than another would turn the race into a test of data coverage. The repository CI runs this proof on every push to the main branch and every pull request, together with a check that the committed data matches byte for byte what the seed regenerates, and the experiment is void unless both pass. A fairness caveat applies: the flat and shaped builders were written without reference to the fact set, which makes their checks substantive, while the normalized graph is encoded straight from the facts and so is lossless closer to by construction than by proof.
Flat is what many teams end up shipping first, and it uses just two heavy node types. Each policyholder node stores its policies and their coverages as a JSON blob in one property. Each claim node embeds its adjuster, provider and payments in the same fashion. A single generic edge type, related, ties holders to claims, which is the whole schema. All the information exists, buried.
Normalized fails in the opposite direction and is no strawman, since a textbook would applaud it. It reifies everything. Entities become bare Resource nodes carrying only their type, each attribute becomes an Attribute node reached over a generic has edge, and each relationship becomes a Relation node with subject and object edges. It looks immaculate on paper, and learning anything takes four hops.
Shaped is designed around the consumer that will really use it. Node types sit at a deliberate granularity, and every edge has a direction, a type and a name that means something, such as filed_against, assessed_by or settles. Policies point to policyholders with held_by, and the same name is reused for one deliberate extra: ninety derived held_by edges running from a claim directly to its policyholder, which squeeze the claim-policy-holder path into a single hop. Reusing the name is intentional, because the relationship reads the same from both ends and the agent does not need a second vocabulary. The ninety edges carry a derived flag, contribute no knowledge, and are left out of the losslessness proof since they are arithmetic over facts that already exist.
Figure 1 sketches a single relationship, a claim and its assessing adjuster, in all three shapes. Flat tucks the adjuster into a JSON property on the claim and links almost nothing. Normalized gives the relationship a node of its own, two edges, and a separate attribute node for every fact. Shaped simply names the edge and stops, and its panel also illustrates why the lone derived shortcut exists. A reconstruction script confirms all three hold exactly the same 2,435 facts.
Node counts preview the argument. One body of truth takes 130 nodes when flat, 507 when shaped and 2,942 when normalized, and each additional node is somewhere the agent may need to go.
A concrete entity shows the difference better than a description. Take the policy the results will make famous, stored as each variant really keeps it:
Figure 2 pulls policy P-7009 straight from the committed ontology files with the figure render script, the same files that were loaded into Cosmos, so it shows exactly what the agent walked. Flat keeps the policy as a piece of string inside a policyholder property: it is there, but it is not a vertex and cannot be addressed. Normalized stores it as a bare Resource vertex whose attributes each sit in a separate vertex two hops off. Shaped holds a typed vertex with its properties attached and named edges leaving it. The premium is the same in all three cases, yet it sits at three different distances from the question.
Each shape sits in its own graph container inside Azure Cosmos DB for Apache Gremlin, and that is what the agent traverses; every tool call below turns into Gremlin traversals on a live graph. The generator emits the fact base, a loader writes it into Cosmos, and the load audits itself: vertex and edge counts have to equal the source, otherwise a mismatch is reported instead of a half-built graph being found later.
One database, one tool layer and identical indexing defaults are what make the designs comparable, and they make the designs visible too. A property graph hides nothing. Ask it what it knows and the reply is the design itself.
Table 1 counts the same 2,435 facts under each schema, taken from the graph files. Flat has more vertices than edges and five vertices that nothing reaches. Normalized is uniform and wordless. Shaped is the best connected and the one whose labels use the vocabulary of the domain.
Return to the flat column. Edges are outnumbered by vertices, and five nodes connect to nothing; they are policyholders without claims, and under this design nothing else in the world leads to them. That is a graph database storing something that barely counts as a graph. The structure still exists, but it has been melted into 8.2 properties per vertex, so it behaves as text and not as topology. The results hinge on that design choice, and the graph store states it as a fact, not an opinion.
The normalized column mirrors it. This is the most uniform of the three graphs: roughly one edge per vertex, nothing isolated, 1.6 properties per node, because reification converts every fact into the same three building blocks, repeated 2,942 times. Structure abounds and vocabulary is absent, with three vertex labels and three edge labels covering a whole insurance business.
At 1.36 edges per vertex the shaped graph is the best connected, with eight vertex labels and nine edge types. The contrast shows up instantly when each graph is asked which kinds of thing it contains:
g.V().groupCount().by(label)
- Flat holds only claim (90) and policyholder (40).
- Normalized reports Resource (507), Attribute (1834) and Relation (601).
- Shaped lists eight domain labels: 216 coverage, 90 claim, 75 policy, 59 payment, 40 policyholder, 12 provider, 10 adjuster and 5 region.
A second query counts the policies by label:
g.V().hasLabel('policy').count()
Table 2 puts one query next to three answers. Both zeros are failures of different kinds. In the flat graph there is simply no policy to find. In the normalized graph a policy exists but will not announce itself. Flat lost the entity; normalized kept the entity and lost the vocabulary, so a policy can only be reached by walking toward it, never by naming it. Nothing in this stems from how the data happened to be stored: three schemas, one database, one query.
The same imbalance appears when pricing a realistic question. Which city does the policyholder live in, for the policy behind claim C-31020? Every graph answers Larkfield, because every graph holds the same facts. The walk to get there is what varies. On the shaped graph the derived edge shortens it:
# shaped - the derived shortcut collapses the path
g.V().has('number','C-31020').out('held_by').values('city')
The flat and normalized graphs need this instead:
# flat - one hop, but only because claims and holders are the only two things
g.V().has('number','C-31020').in('related').values('city')# normalized - the same answer, five steps and two unwrappings later
g.V().has('value','C-31020').in('has')
.in('subject').has('kind','filed_against').out('object')
.in('subject').has('kind','held_by').out('object')
.out('has').has('attr','city').values('value')
Flat manages it in one hop only because a claim and a holder happen to be the sole two kinds of thing in that schema. Normalized needs five steps and two unwrappings. An agent never composes these queries itself; it finds the path one tool call at a time, which is the point of the experiment. Still, the queries reveal what is being discovered and how much of it there is.
Figure 3 shows that identical query run on all three graphs inside the Cosmos DB Data Explorer. Whether a graph offers two labels, three meta-labels or the domain in its own words, the schemas do not hold different data; they hold one dataset in different forms.
Figure 4 selects claim C-31020 in each graph and lets the Data Explorer draw its neighbors. In the flat graph only one vertex is reachable, a policyholder, over an edge labeled just related. Adjuster AJ-09 and policy PL-043 are visible in the properties panel, but as text, so the agent has no way to walk to them. In the normalized graph eight vertices turn up, six attributes and two relations, none named in domain language. In the shaped graph three do, along filed_against, assessed_by and held_by. One claim, one database, three distances.
The instrument: four tools and a receipt
The agent is kept minimal on purpose, because it is not the subject under test. It is one plain function-calling loop running against a Microsoft Foundry model deployment, wired the way the Foundry docs describe tool use over the Chat Completions API. A fixed system prompt, a ceiling of sixteen model turns per question (after which the episode is dropped) and four tools complete it, with signatures identical across the three ontologies.
Figure 5 shows the complete compute footprint: a single Microsoft Foundry resource holding two model deployments, agent-small running gpt-5.4-nano and agent-large running gpt-5.5, both Global Standard. Names and versions are taken from the infrastructure template in the repository, so no hidden fleet sits behind the figures.
The four tools are:
find_nodessearches node properties and ids by substring, capped at twenty matchesget_nodereads one node's type and propertiestraversereturns up to thirty neighbors of a node, filterable by edge type and by directiondescribe_edgessummarizes which edges leave or enter a node and how many of each
Under the hood each tool turns into Gremlin on Cosmos. get_node and find_nodes take one traversal apiece, a point read and a scan, while traverse and describe_edges take three, because they first confirm the node exists and then walk outgoing and incoming edges one after the other. The agent writes none of this. It requests a node or a neighbor and receives a compact JSON reply, the usual shape of a production tool surface over a graph store, which also keeps the query language away from the model.
Walking the graph is the sole route to an answer. The agent never views the schema or writes Gremlin, and it has instructions to answer with the literal NOT_MODELED whenever the graph lacks what a question needs. The caps matter: a badly shaped graph pays for its fan-out through additional tool calls and not through one gigantic response, much as real tool budgets behave. The sixteen-turn limit counts conversation turns and not tool calls, and one turn can ask for several tools together, so the busiest episode managed sixty calls within its sixteen turns.
Three practical lessons from running against a genuine graph database each forced a rebuild. Cosmos refuses multi-statement scripts, so loading means one traversal per request, and throughput comes from running many in parallel instead of batching. A hard sixty-second request timeout means g.V().drop() cannot wipe a few thousand vertices: it dies partway and leaves a half-dropped graph, so clearing happens in slices. The serverless tier also has a throughput ceiling that turns a burst of forty simultaneous writes into timeouts, which is why the loader and the agent tool layer both retry with backoff. None of this is exotic, but it separates a graph store on a slide from one under load.
Grading without a judge
Evaluation of agents tends to loosen at the grading step, so this one got the most attention, and no LLM judge appears anywhere. Forty questions form four bands of ten: single-fact lookups, short traversals of two or three hops, questions that need a long path or an aggregation over a subgraph, and questions that cannot be answered. A script generates them from the fact base and derives each correct answer from the raw facts. Marking is mechanical. A number has to equal the computed value, a string has to contain it, and an unanswerable question counts as right only when the agent says outright that it cannot answer. No ontology marks itself, no model judges another model, and correctness comes from comparison instead of opinion.
Since the agent can act only through its four tools, each answer comes with the full list of calls that produced it. In the earlier router measurements the model field was the receipt; here the path is. A correct answer can be traced step by step, and a failure shows exactly where the shape lost the agent.
Figure 6 charts the setup. One seeded fact base feeds three lossless shapes, which become three graph containers in Azure Cosmos DB for Apache Gremlin, and a separate branch feeds the oracle that derives each correct answer from the same facts. The agent reaches the graphs through the four tools only, one Gremlin query per call. Tools, loop and questions stay fixed while shape varies, and each run produces receipts: tool calls, tokens, latency and an oracle verdict.
The unanswerable band merits a sentence of its own. Its ten questions ask for things the fact base never models: phone numbers, email addresses, weather, vehicle identification numbers. The right behavior is to reply that the graph cannot say. An agent that makes up an edge instead of admitting the graph has none exhibits the most dangerous failure in this family of systems, and hardly anyone measures it.
What the traversal cost
The main campaign hit the live Cosmos DB graphs, with reasoning on a dedicated Microsoft Foundry deployment in Sweden Central. It spanned three ontologies, forty questions and three full passes with a single small model held constant, which came to 360 agent episodes, 3.1 million tokens and seventy cents. Each tool call was a Gremlin query. A second campaign, described later, reruns the grid on a bigger model for another 120 episodes and $7.39. A repository script computes every number below from the recorded receipts, and none was typed by hand.
Table 3 summarizes the measured campaign: small model, three passes, 360 episodes, a Gremlin query behind every tool call. Green flags the best outcome in each row. Resource rows are left uncolored on purpose, since flat is lowest on all four, and as later sections show, part of that thrift pays for answers it never delivered.
Start by discounting the first row, the least informative number in the table. Shaped answered 84.2% of questions correctly, flat 78.3% and normalized 59.2%. Yet across three otherwise identical passes the accuracy of the variants drifted by 0.075, 0.150 and 0.075 respectively, so the six-point gap separating shaped from flat sits inside its own noise, and with three passes nothing here tells those two apart on overall accuracy. The twenty-five-point lead over normalized clears the noise easily.
That sounds weaker than it is. The overall figure is vague because it averages bands that disagree sharply, and the disagreement is the finding:
Table 4 explains the problem with the aggregate. The shapes do not differ slightly everywhere; they split sharply band by band, and every band asks something different of the graph.
Look first at single-fact lookups. Shaped scores 0.967 and flat 0.533, a forty-three-point spread, set against run-to-run noise of 0.100 for one and 0.200 for the other, so the signal is real. For two- and three-hop traversals both reach 0.967. For the hardest band, noise of 0.300 and 0.400 leaves no room to name a winner.
The two shapes are therefore not better and worse. They are broken in different places, and blending them into one number conceals the useful part. When something must be found, flat fails and keeps failing. When the agent already has a foothold and only needs to walk, flat does as well as the designed graph.
Normalized mirrors it, as its bands show: 0.867 on lookups, ahead of flat since every entity is a real vertex, then 0.467 and 0.133 as walks grow longer. It finds anything and finishes nothing.
How each shape fails
The shapes differ in how they fail as well as where. Each tackled thirty answerable questions three times, ninety episodes in all, and grouping the misses by failure type exposes the mechanism.
Table 5 groups the misses over those ninety answerable episodes. Normalized runs out of its turn budget; flat stops early and with confidence on questions the database could have answered.
Twenty-two of normalized's ninety answerable episodes, about a quarter, ended with the budget spent and the agent mid-walk. Flat seldom did. It stopped early and sure of itself, refusing eighteen questions with answers sitting in the database. One shape wears itself out and the other quits.
The flat failure is the sharper one, and the traces reveal a mechanism nobody expected. Here are two real episodes. Both ask for the premium on policy P-7009, and they differ only in which schema sits behind the tools:
flat shaped
find_nodes "P-7009" -> 1 hit: find_nodes "P-7009" type=policy
policyholder:PH-05 "Ivo Renner" -> 1 hit: policy:PL-009 "P-7009"
find_nodes "7009" -> 1 hit get_node policy:PL-009
find_nodes "P-7009" type=policy -> 0 -> premium 750
find_nodes "P-7009" type=policyholder -> 1 hit
ANSWER: NOT_MODELED ANSWER: 750
On the shaped graph two calls suffice. Ask for the policy by number, receive a policy vertex, read the premium from it, finished.
The flat graph tells a different story. The first search returned a vertex and so did the second. The third asked for type=policy and found nothing, since that label does not exist in this schema. The fourth asked for type=policyholder and got a hit. By type, then, the agent had found the exact vertex that held the answer, and still declared the data missing.
The premium was inside that vertex, in a property, one get_node call away. Over the ten failing premium episodes the unfiltered search delivered the containing vertex fifteen times, and get_node was run on it zero times. The vertex carried the label "Ivo Renner", a person. In the remaining failing premium episodes the label belonged to one of two other policyholders, never the policy, and a person name offers no hint that the requested policy lives inside.
That is the particular hazard of burying an entity, and a graph database pins it down. The data is reachable but unrecognizable. It is indexed, returned by the query and present in the agent context, yet it answers to no name the question used.
Normalized fails in the open, and expensively. Reading one attribute means a trip through an Attribute node, and following one relationship means a detour via a Relation node. The tokens column shows the bill: 13,701 per question against 7,176 for shaped, and 12.15 tool calls against 7.24. Twenty-five of its 120 episodes hit the sixteen-turn limit, against three for shaped and two for flat, and the hardest band scored 0.133. The graph lacked nothing. The agent simply ran out of breath before finishing the sentence.
The cheapest column is genuinely the cheapest, though that is no compliment. Flat runs at $0.109 per hundred questions against $0.165 for shaped, and divided by accuracy it still wins, $0.14 per hundred correct answers against $0.20. On this model, flat is truly cheaper to run. What no price buys is the eighteen answers it refused. Quitting quickly is inexpensive, and a small bill under a low ceiling is no saving, which a cost column alone never reveals.
The refusal paradox
Best and worst swap places on the unanswerable band once the answerable questions are consulted. Flat refused all thirty unanswerable episodes correctly, the only perfect score in the table. Shaped managed 0.933 and normalized 0.900, and their misses were episodes that ran out of turns, not inventions.
Viewed alone, that row crowns flat. Next to the false-refusal count the order reverses: the shape with the perfect refusal score declined eighteen questions it could have answered, whereas the shape with the worst refusal score declined thirteen. Flat is not better at spotting absence. It leans toward announcing it, and the lean that wins on unanswerable questions loses on answerable ones. A refusal metric without an answerability metric beside it invites shipping the blindest graph that can be built.
A finding against the design
One result runs against the shaped design the experiment favors, and the second campaign sharpened it. Only a single derived shortcut exists in shaped: held_by, linking a claim straight to its policyholder. On the claim-to-holder-city question it scored 0.83 where normalized got 0.33, yet flat reached 0.92 without any shortcut, since a claim and a holder are the only two things in that schema. A result that the shape without the edge beats cannot be credited to the edge.
Worse, the missing shortcut cost where it was expected to. Counting distinct coverage kinds across a holder policies scored 0.17 on the supposedly well-designed graph and 0.67 on flat, which keeps them in one readable property. Totaling payments over a holder claims tied at 0.33. For aggregation across a subgraph, the design being argued for loses, and loses to the design called broken. Denormalization is no compromise against good design but part of it, and the shaped graph applied its own principle too sparingly exactly where the workload needed it.
Buying your way out with a bigger model
The complete grid then ran a second time on a far larger model, and the answer to the main question came from it behaving unexpectedly. Going in, the assumption was that the small model on a well-shaped graph would beat the big model on a badly shaped one. It did not. Only answerable questions enter this comparison, so refusals of unanswerable ones cannot flatter any shape. Each small-model row has ninety episodes across three passes, and each large-model row has thirty from a single pass.
Table 6 restricts itself to answerable questions, so correct refusals cannot make a refusal-prone shape look good.
The large model reads buried properties without trouble. It answered all thirty answerable flat-graph questions, including those about entities the schema had turned into text, and the eighteen false refusals that hobbled the small model disappeared. The honest headline is not the one anyone wanted.
A poorly shaped ontology can be paid off. The rate is about fourteen times the cost per question: the large model on the flat graph costs $1.90 per hundred questions, the small model on the shaped graph thirteen cents. Keep the flat graph, upgrade only the model under it, and the bill is twenty times what the same small model cost on it.
That outcome is more useful than the predicted one for what it implies. With a frontier model and answerable questions, shape stopped being a correctness problem and turned into a cost problem: flat and shaped each answered all thirty, and only the bill separated them. With a small model, shape was correctness. A well-designed ontology therefore buys less quality than it buys the ability to run a cheap model. A flat graph ties the knowledge layer to the model budget, working only while payment continues for a model big enough to read through it, on every question, indefinitely.
Over all four bands the ordering is tidier than the answerable-only view hints. With the large model, shaped hit a perfect 1.000 in every band, the unanswerable one included. Flat reached 0.950 and normalized 0.825. Money fixes correctness only most of the way, and the bill stays untouched.
What the cross-check taught about caution
The cross-check run (same grid, same questions, tools reading files instead of Cosmos) produced the most alarming result of the project, and it came from the shaped graph. Asked what the weather was on the day of an incident, which the ontology deliberately does not model, the agent said "hail damage". It had found the cause property of the claim and presented it as weather. That was logged at the time as the favored design committing the failure called most dangerous, with the caution that it was one episode from one pass, an observation and not a measurement, no basis for a law.
Rerunning on real infrastructure was the right move. In the measured campaign the shaped graph correctly refused all ten of the large model unanswerable questions, and the confabulation turned up on the flat graph, which answered "hail" to the same question. The model, the question and the trap were identical; only the shape that got caught differed.
The finding is therefore not that rich graphs invite confabulation. A cause property reading "hail damage" will tempt any model asked about weather, wherever it is stored, and which shape gets caught changes between runs. That is a smaller claim than the one nearly made, and it is what the evidence supports. It also means the hazard does not depend on shape: if a graph carries a field that reads like the answer to something it does not answer, some model somewhere will hand it over.
The normalized shape on both models
Normalized lost overall with both models. With the small one it ranked last, and with the large one it still trailed at 0.867 on answerable questions while costing $9.31 per hundred, close to five times the flat graph on the same model, and it ran out of turns seven times. Reification fares no better with a bigger model because its trouble was never comprehension. It was distance: fifteen tool calls per question against 8.3 for shaped, from a model that never misread a thing.
Normalized did not lose everywhere, and that exception teaches the most. On single-fact lookups normalized scored 0.867 against 0.533 for flat, and on the two questions that broke flat it was clearly the better shape: 0.83 against 0.17 for a policy premium, 0.83 against 0.33 for a provider specialty. Reification is wildly expensive but hides nothing. Every entity is a vertex, so every entity can be found.
That sums up the argument: each failing shape kept half of the problem and discarded the other. Flat kept distance and lost addressability. Normalized kept addressability and lost distance. Each competes on the half it kept and is hopeless on the other.
Shaped is not a compromise between the two. It scores 0.967 on the band that beats flat and 0.967 on the band that beats normalized, so it declines the trade. That is where its advantage lives, and one blended accuracy number hides it entirely.
Latency against a real database
Latency is one measurement the cross-check could not support. With files, per-call times swung wildly between identical passes, the same variant could run at 1.17 seconds a call on one pass and 4.21 on another, so nothing was reported while service variance outweighed the ontology. Against Cosmos the picture is orderly, for a mildly deflating reason.
Table 7 shows per-question time separating cleanly between shapes and holding steady within them, yet identical per tool call across all three, which makes the table the tool-call count restated in seconds.
Within any one variant the widest swing is seven seconds, against a gap of nearly fifty seconds from flat to normalized. A normalized question takes two and a half times as long as a flat one. The last column deflates the result: per call, all three shapes cost about the same, 6.2 to 7.4 seconds, whatever the schema. This is therefore no independent finding. The database contributes consistency, since a Cosmos round trip is predictable where a local dictionary was not, and that is what makes the number reportable at all.
One practical consequence follows. With per-call latency roughly constant, ontology shape sets response time as directly as it sets the token bill, and the figure can be estimated before building by counting hops.
Principles, each with its receipt
Writing on ontology design usually offers principles as matters of taste. Most of those below arrive with the number that earned them, and any that lack one say so.
Anything that can be asked about must be a vertex. If one principle gets carved above the door, make it this. The flat graph holds each policy premium and provider specialty, indexed, in the same database as everything else, and the small model barely retrieved them. Premiums came back right 17% of the time and specialties 33%, where shaped managed 100% and 83%. The full band agrees, 0.533 to 0.967. Hiding an entity inside another entity property does not make it hard to find. It makes a fair question about it come back under another name, which the agent reads as evidence that the thing is absent. The crossover adds a condition, since the frontier model dug them all out. Burial is no absolute barrier, just a tax payable in model tier.
Granularity is a budget, and reification burns through it quickly. Each node added for schema purity charges rent on every visit. Next to shaped, normalized needed 3.7 times the tool calls for single-fact lookups, 2.3 times for short traversals and four times the tokens in the long-path band, and it hit the turn limit in twenty-five of 120 episodes, against three. A model priced twenty-five times higher per input token raised it from 0.489 to 0.867 on answerable questions. That is a real recovery, but it was bought at $9.31 per hundred, close to five times what the same model cost on flat. Distance is never cheap, whatever the budget.
Entry points are architecture, and they lean hard on the model. Each question begins with some name or number, and what the first tool call returns decides whether the agent gets to work or starts to guess. For the small model that call came back empty in 10% of shaped episodes, 26% of flat ones and 53% of normalized ones, where a search lands on an Attribute vertex and not on the entity. For the frontier model it was never empty, whatever the shape. So a wasted opening move is not only a graph property. It is what happens when a weaker model meets an entry point it did not expect. Build the doors for the model that will run in production, not the one used in testing.
Denormalize deliberately, and admit it when the receipt disagrees. One derived shortcut, held_by, went in expecting to pay off on the claim-to-holder path. It scored 0.83 there against 0.33 for normalized, but flat scored 0.92 on the same question with no shortcut, so credit is not available, and the clean test (shaped with the edge versus without) was not run. The shortcut that was missing teaches more. Counting coverage kinds across a holder policies scored 0.17 on the well-designed graph and 0.67 on flat, which keeps them in one readable property. For aggregation across a subgraph, the argued-for design lost to the one labeled broken. Choosing shortcuts is the design, the workload shows which are needed, and here the wrong one was chosen and the data said so.
Bound the fan-out. Search capped at twenty results and traversal at thirty meant an over-connected node cost visible, countable extra calls and not one huge response. No uncapped arm ran, so the savings are unknown. What is known is that without caps, cost hides in response size where no call-count metric looks.
Make absence legible, and score it honestly. Treat the unanswerable band as a design requirement and not a test trick, but read it beside the answerable band. Flat scored a perfect 1.000 on refusals while declining eighteen questions it could have answered. Shaped refused least, and its two missed unanswerable episodes ended in giving up rather than inventing something. A good ontology lets "we do not model this" be both reachable and true, and an evaluation needs both halves to separate a graph aware of its limits from one that cannot locate its own contents.
Names are the API. The agent reads edge names as a developer reads a function signature, and this experiment lumps naming and structure into one variable called design, deliberately. No number backs this principle by itself. Separating the two is the obvious next ablation, and saying so beats claiming more than was measured.
What this isn't
This is not a benchmark of graph databases. All three shapes share one Cosmos DB account, one tool layer and the same indexing defaults, so the store is a constant and not a variable, and nothing compares Cosmos with any other graph database or tunes it. Neither does it claim that traversal alone is how production agents should retrieve everything, since fuzzy questions without a clean entry point need search; the results show where plain traversal earns its place and where it would not. It makes no argument against normalization in transactional systems, because the normalized shape is wrong here for this consumer, an agent paying per hop, and not wrong in general. And it is no vendor comparison: Microsoft Foundry is the constant, not the variable, and the method would carry over to any platform with function calling.
Would a database in the loop change the outcome? That calls for an answer, not an assurance. Ahead of the measured campaigns, the same four tools were implemented twice, over Cosmos and over the files the generator emits, and the recorded tool calls were replayed against both: 3,056 distinct calls, every unique tool-and-argument pairing the campaigns produced. Both implementations returned identical answers in all cases. The database still has a role. It adds latency and request units, and Cosmos runs needed roughly 4% more agent steps than the file-backed ones, with nearly all of the extra on the normalized graph. But the answers the agent saw came from ontology shape and not from the engine below it, which is all the comparison needs. The replay lives in the repository as a script.
Limits of the instrument
Several caveats carry over from the router work. Forty synthetic questions in one synthetic domain add up to a small instrument. Contrasts are sharp since the shapes are archetypes, and a production ontology falls in between, which is why the endpoints are what got measured. The small-model campaign got three full passes and its noise floor is quoted throughout. The large-model grid got one, so only its saturated outcomes, thirty correct out of thirty on two shapes, are easy to trust. A gap of two questions inside that grid counts as an anecdote, and nothing here is argued from one. Just one small and one large model were compared too, so the curve between them stays unmeasured.
Auditing the code ahead of release exposed two flaws in the instrument. One: when the requested edge type is absent from the graph, traverse returns an empty neighbor list, which is indistinguishable from a node that really has no neighbors. The agent made 112 such calls across the campaigns, and nine of the twenty-two false refusals on the normalized graph contain one; the analysis script counts both. The headline gap survives, since shaped made the most such calls yet refused the fewest questions. Still, a tool replying "nothing here" for "no such edge type" is a confound, and the honest alternative is an error naming the available types. Two: the losslessness proof compares reconstructed facts, so it cannot see edges that hold no fact of their own, meaning the connective edges in flat and the derived shortcuts in shaped. A second check now validates those edges against the facts they restate, and it catches deletion and mis-pointing for every one. The campaigns predate that check, so the guarantee was absent when they ran.
The shape is the system prompt
Plenty of effort goes into the words placed before a model and almost none into the shape of the knowledge behind it. For a tool-using agent the ontology is a prompt: the moves the world allows, written in node types and edge names and read one tool call at a time. Design it for the reader that actually exists.
The supporting material sits in the experiment code repository: fact base, three ontologies, losslessness proof, questions, oracle, agent and every campaign receipt. Anyone who models the same truth a fourth way and beats these shapes on the questions that defeated them would add something real to the comparison.
For background on the pieces used, the relevant reference documentation covers Azure Cosmos DB for Apache Gremlin, function calling with Microsoft Foundry models and the Azure Retail Prices REST API, which supplied the per-token prices behind the cost figures.