August 31, 2026
Your Agent Is Not Confused, Your Ontology Is
I modeled the same facts three different ways, gave one agent on Microsoft Foundry four traversal tools and no other way to the answer, andβ¦

By Miodrag Cekikj
27 min read
I modeled the same facts three different ways, gave one agent on Microsoft Foundry four traversal tools and no other way to the answer, and measured what the shape of knowledge costs per question, because when an agent underperforms, the fix everyone reaches for is a bigger model, and it does work, at roughly fourteen times the price of fixing the graph instead.
Note on data and numbers. Every entity in this experiment is synthetic and fictional: the people, the companies, the places and the insurer itself are generated from invented word lists with a fixed seed, and none of them refers to any real person, company or place. The numbers in section IV come from live campaigns in Sweden Central: the reasoning on a dedicated Microsoft Foundry deployment, the knowledge in an Azure Cosmos DB for Apache Gremlin account, and every tool call a Gremlin query against a live graph β four hundred and eighty agent episodes, 4.3 million tokens, $8.09. The same grid had been run a first time with the tools reading the generator's files instead of the database, for $7.74. That earlier grid is referred to throughout as the cross-check run; every measurement reported here comes from the Cosmos campaigns, and the cross-check appears only where two runs say something a single run could not.
Contents
I. The layer nobody upgrades
II. Three shapes of one truth
III. The instrument β four tools and a receipt
IV. What the traversal cost
V. The principles, each with its receipt
VI. What this is not Coda β the shape is the system prompt
I. The layer nobody upgrades
There is a ritual that plays out in every team shipping agents right now. The agent misses questions it should get. Someone proposes the bigger model. The bill doubles, the misses move around, and the retro concludes that agents are not ready.
I have sat in that retro. What is almost never on the table is the thing the agent was actually reasoning over: the shape of the knowledge it was given. Not the facts, the shape. Whether the adjuster is a node or a string inside a JSON blob. Whether the edge is called assessed_by or related. Whether reaching a policyholder from a claim takes one hop or five.
My previous two articles measured a system that decides and then built the tool that pins what it decided [1][2]. In parallel, a three-part series on Towards Data Science built a persistent knowledge layer and put agents on top of it, on a synthetic insurance corpus of its own [3] β the architectural argument for treating knowledge as a designed artifact rather than a retrieval bucket. This article is the measurement that series never had: it goes one layer down, to the substrate agents reason over, and asks a question I have not seen anyone answer with numbers: hold the model, the tools, the prompts and the facts constant, change only the ontology's shape, and what happens to an agent's accuracy, to the work it must do, and to the bill?
Then, because the first answer raises an obvious objection, I ran the whole thing again on a far larger model to ask the follow-up: can you simply buy your way past a badly shaped graph? You can. The interesting part is the price, and what it says about which shape you should build.
The method is the same as before. No opinions without receipts.
II. Three shapes of one truth
The domain is an insurance book of business β policyholders, policies, coverages, claims, adjusters, repair providers, payments, regions. All of it synthetic, all of it fictional, generated once from a seeded script: 2,435 canonical facts about 507 entities, and not one of them real.
From that single fact base I built three ontologies. Not three datasets β three shapes of the same dataset, and this rule is the heart of the experiment: every variant must re-express the facts without gain or loss. A script walks each built graph, reconstructs the fact set from it, and asserts it equals the source exactly. If a variant knew one fact more or less than another, the race would be measuring data coverage, not design. That proof runs in the repository's CI on every push to the main branch and on every pull request, alongside a check that the committed data is byte-identical to what the seed regenerates, and the experiment is invalid unless both pass. One caveat in fairness: the flat and shaped builders are written independently of that fact set, so their checks are substantive, while the normalized graph is encoded from the fact set directly and is therefore lossless closer to by construction than by proof.
The flat one is what most teams actually ship first: two fat node types. A policyholder node carries its policies, and their coverages, as a JSON blob in a property. A claim node embeds its adjuster, its provider and its payments the same way. One generic edge type β related - connects holders to claims, and that is the entire schema. Nothing is missing; everything is buried.
The normalized one is the opposite failure, and it is not a strawman β it is what a textbook would praise. Full reification: entities are bare Resource nodes carrying nothing but their own type, every attribute is its own Attribute node reached by a generic has edge, every relationship is its own Relation node with subject and object edges. Academically immaculate. Four hops to learn anything.
The shaped one is designed for the consumer it will actually have: typed nodes at deliberate granularity, typed directional edges whose names say what they mean β filed_against, assessed_by, settles. A policy is held_by a policyholder, and the shaped graph reuses that same name for one deliberate addition: ninety derived held_by edges that jump from a claim straight to the policyholder, collapsing the claim-to-policy-to-holder path into a single hop. Reusing the name is the point - the relationship means the same thing from either end, so the agent does not have to learn a second vocabulary. Those ninety edges are flagged derived in the graph, they add no knowledge, and the losslessness proof excludes them from reconstruction precisely because they are arithmetic over facts already present, not information of their own.
The counts alone preview the argument. The same truth is 130 nodes in the flat graph, 507 in the shaped one, and 2,942 in the normalized one. Every extra node is a place an agent might have to visit.
And because the difference is easier to see than to describe, here is one entity β the policy the results section will make famous, as the raw data each variant actually stores:
All three shapes live in Azure Cosmos DB for Apache Gremlin [4], one graph container each, and that is what the agent traverses: every tool call in every measurement below resolves to Gremlin traversals against a live graph database. The generator writes the fact base, a loader materialises it into Cosmos, and the load verifies itself β vertex and edge counts must match the source exactly, or it reports a mismatch rather than leaving a half-built graph to be discovered later.
Putting the three designs into the same database, under the same tool layer, with the same indexing defaults, is what makes them comparable. It also makes them visible. A property graph has nowhere to hide: you can ask it what it knows, and the answer is the design.
Read the first column again. The flat ontology has fewer edges than vertices, and five of its nodes are connected to nothing at all β they are policyholders who happen to have no claims, and in this design nothing else in the world reaches them. It is a graph database holding something that is barely a graph. All the structure is still there, but it has been dissolved into those 8.2 properties per vertex, where it is text rather than topology. That is the design decision that section IV is about, and the graph store states it as a fact rather than an opinion.
The normalized column is the mirror image. It is the most uniform graph of the three β almost exactly one edge per vertex, nothing isolated, every node carrying 1.6 properties, because reification turns every fact into the same three shapes repeated 2,942 times. It has structure to spare and no vocabulary at all: three vertex labels and three edge labels for an entire insurance business.
The shaped graph is the most connected, at 1.36 edges per vertex, and the only one whose labels name the domain. Eight vertex labels, nine edge types. Ask each graph what kinds of thing it contains and the difference is immediate:
g.V().groupCount().by(label)g.V().groupCount().by(label)- flat:
{claim: 90, policyholder: 40} - normalized:
{Resource: 507, Attribute: 1834, Relation: 601} - shaped:
{claim: 90, policy: 75, coverage: 216, policyholder: 40, payment: 59, provider: 12, adjuster: 10, region: 5}
One line makes the point better than this section has:
g.V().hasLabel('policy').count()g.V().hasLabel('policy').count()
Those two zeros mean different things, and section IV is largely about that difference. The flat graph lost the entity. The normalized graph kept the entity and lost the vocabulary: you can reach a policy there, but only by walking to it, never by naming it. Nothing about that is an artifact of how I chose to store the data, it is three schemas in the same database, answering the same query.
The same asymmetry shows up in what it costs to answer a real question. Take one: which city does the policyholder live in, for the policy that claim C-31020 was filed against? All three graphs return Larkfield, because all three contain the same facts. What differs is the walk.
# shaped - the derived shortcut collapses the path
g.V().has('number','C-31020').out('held_by').values('city')
# 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')# shaped - the derived shortcut collapses the path
g.V().has('number','C-31020').out('held_by').values('city')
# 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')An agent does not write those queries β it discovers the path one tool call at a time, which is the whole experiment. But the queries show what it is discovering, and how much of it there is to discover.
III. The instrument β four tools and a receipt
The agent is deliberately minimal, because the agent is not the experiment. One plain function-calling loop against a Microsoft Foundry model deployment, built exactly the way the Foundry documentation describes tool use with the Chat Completions API [5]; a fixed system prompt; a budget of sixteen model turns per question, after which the episode is abandoned; and four tools, identical in signature across all three ontologies:
find_nodes- substring search over node properties and ids, capped at twenty matchesget_node- read one node's type and propertiestraverse- list a node's neighbors, optionally by edge type and direction, capped at thirtydescribe_edges- what can be walked from here, with counts
Each resolves to Gremlin against Cosmos. get_node and find_nodes are a single traversal each - a point read and a scan; traverse and describe_edges issue three apiece, since they check the node exists and then walk outgoing and incoming edges separately. The agent issues none of that itself - it asks for a node or a neighbor and gets a small JSON answer back, which is how a tool surface over a graph store usually looks in production, and which keeps the query language out of the model's hands.
Traversal is the only path to an answer. The agent never sees the schema, never writes Gremlin, and is instructed to refuse, with a literal NOT_MODELED - when the graph does not contain the fact a question needs.
The caps matter: a badly shaped graph pays its fan-out cost in extra tool calls rather than in one giant response, which is exactly how real tool budgets behave in production. Note that the sixteen-turn budget bounds conversation turns, not tool calls β a single turn may request several tools at once, so the busiest episode in these campaigns made sixty calls inside its sixteen turns.
Three things about running this against a real graph database are worth passing on, because each cost me a rebuild. Cosmos rejects multi-statement scripts, so bulk loading is one traversal per request and throughput comes from concurrency rather than batching. There is a hard sixty-second request timeout, which means g.V().drop() cannot clear a few thousand vertices - it times out half way and leaves a partially dropped graph, so clearing has to happen in slices. And serverless has a throughput ceiling that turns a burst of forty concurrent writes into timeouts, so both the loader and the agent's tool layer retry with backoff. None of that is exotic; all of it is the difference between a graph store in a diagram and one under load.
Grading is the part I care most about, because agent evaluation is where methodology usually goes soft. There is no LLM judge anywhere in this experiment. The forty questions, ten single-fact lookups, ten short traversals of two or three hops, ten that need either a long path or an aggregation across a subgraph, and ten that are deliberately unanswerable, are generated from the fact base by a script that computes every correct answer from the raw facts directly. Grading is then mechanical: numbers must match the computed value, strings must contain it, and an unanswerable question is only right if the agent says so explicitly. No ontology grades its own homework, no model judges another, and correctness is a comparison rather than an opinion.
And because the agent can only act through the four tools, every answer arrives with the complete list of calls that produced it. Readers of my router articles will recognise the pattern: the model field was the receipt there, the routing lock made the receipt executable, and here the path is the receipt
- when the agent answers, I can show you the exact walk that led to it, and when it fails, the trace shows where the shape lost it.
The unanswerable band deserves its sentence. Ten questions ask for things the fact base deliberately never models β phone numbers, email addresses, weather, vehicle identification numbers. The correct behavior is to come back and say so. An agent that invents an edge rather than admitting the graph has none is the most dangerous failure mode in this entire class of systems, and to my knowledge almost nobody instruments for it.
IV. What the traversal cost
The main campaign ran against the live graphs in Cosmos DB, with the reasoning on a dedicated Microsoft Foundry deployment in Sweden Central: three ontologies, forty questions, three full passes, one small model held constant β three hundred and sixty agent episodes, 3.1 million tokens, seventy cents. Every tool call was a Gremlin query. A second campaign later in this section repeats the grid on a larger model, for a further hundred and twenty episodes and $7.39. Every number below is computed from the recorded receipts by a script in the repository; none of it is typed in by hand.
Bold marks the best outcome in a row. The resource rows are deliberately unbolded: the flat graph is lowest on all four of them, and as the next sections show, part of that thrift is the cost of answers it never produced.
The first row is the least informative number in the table, and it is worth saying so before anyone quotes it. Shaped answered 84.2% of questions correctly, flat 78.3%, normalized 59.2%. But each variant's own accuracy moved by 0.075, 0.150 and 0.075 between three otherwise identical passes, which means the six-point gap between shaped and flat does not clear its own noise. On this campaign, at three passes, I cannot tell you those two shapes differ on aggregate accuracy. The twenty-five-point gap to normalized does clear it, and comfortably.
That looks like a weaker result than it is. The aggregate is uninformative because it averages together bands that disagree violently, and the disagreement is the finding:
On single-fact lookups the shaped graph beats the flat one 0.967 to 0.533 β a forty-three-point gap against noise floors of 0.100 and 0.200, so this one is signal with room to spare. On two-to-three-hop traversals the two are identical, 0.967 apiece. On the hardest band their spreads are 0.300 and 0.400 and I will not call a winner.
So the two shapes are not better and worse. They are differently broken, and averaging them into one number hides exactly the thing worth knowing. Where you have to find something, the flat graph fails and keeps failing. Where you already have a foothold and only need to walk, it is every bit as good as the designed one.
Normalized is the mirror image, and its bands say so just as clearly: 0.867 on lookups β better than flat, because every entity really is a vertex, and then 0.467 and 0.133 as the walks get longer. It can find anything and finish nothing.
They also fail in different ways, not just at different questions. Each shape faced thirty answerable questions three times over, so ninety episodes. Sorting the misses by how they failed is where the mechanism shows:
Normalized ran out of turns twenty-two times β a quarter of its answerable episodes ended with the agent still walking. Flat almost never did that; it stopped early and confidently, refusing eighteen questions whose answers were sitting in the database. One shape exhausts itself, the other gives up.
The flat failure is the sharper one, and the traces show a mechanism I did not anticipate. Here are two real episodes side by side, same question, the premium on policy P-7009, same model, same database, different schema:
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: 750flat 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: 750On the shaped graph it takes two calls. Ask for a policy by its number, get a policy vertex back, read the premium off it, done.
On the flat graph, look at what the agent actually achieved. Its very first search found a vertex. Its second found one. Its third asked for type=policy and got nothing, because that label does not exist in this schema. Its fourth asked for type=policyholder - and got a hit. The agent successfully located, by type, the exact vertex containing the answer, and then reported the data as missing.
The premium is inside that vertex, in a property, one get_node away. Across the ten failing premium episodes the unfiltered search handed the agent that containing vertex fifteen times, and get_node was called on it zero times. What the search returned was a vertex labelled "Ivo Renner" - a person's name. Across the other failing premium episodes it was one of two other policyholders, never the policy: nothing about somebody's name suggests that the policy you asked for is inside them.
That is the specific danger of burying an entity, and a graph database makes it precise. The data is not unreachable; it is unrecognizable. The vertex is indexed, returned by the query, and sitting in the agent's context. It just does not answer to the name of the thing that was asked for.
Normalized fails honestly and expensively instead. Every attribute read costs a round trip through an Attribute node, every relationship a detour through a Relation node, and the bill arrives in the token column: 13,701 tokens per question against the shaped graph's 7,176, and 12.15 tool calls against 7.24. Twenty-five of its hundred and twenty episodes hit the sixteen-turn budget and stopped, against three for shaped and two for flat, and on the hardest band it scored 0.133. Nothing was missing from that graph. The agent simply could not reach the end of a sentence before running out of breath.
The cheapest column really is the cheapest, and that is not the compliment it looks like. Flat costs $0.109 per hundred questions against the shaped graph's $0.165, and I want to be careful here rather than convenient: it stays cheaper even when you divide by accuracy, at $0.14 per hundred correct answers against $0.20. The flat schema is genuinely the cheaper one to operate on this model. What it does not sell you at any price is the eighteen answers it refused. Giving up quickly is inexpensive, and a low bill on a low ceiling is not a saving. That is the distinction a cost column alone will never show you.
The refusal paradox: the best and worst scores on the unanswerable band swap places the moment you check the answerable ones. Flat refused all thirty unanswerable episodes correctly β a perfect score, and the only perfect score in the table. Shaped scored 0.933, normalized 0.900, and in both cases the misses were episodes that ran out of turns rather than inventions.
Read that row alone and you would crown the flat schema. Now read it beside the false-refusal count, where the order flips: the shape with the perfect refusal score refused eighteen questions it could have answered, and the shape with the worst refusal score refused thirteen. Flat is not better at recognizing absence. It is biased toward declaring it, and the same bias that earns a perfect score on questions with no answer is what loses the questions that have one. A refusal metric without an answerability metric beside it is an invitation to ship the blindest graph you can build.
And one finding at my own expense, which this campaign made worse rather than better. The shaped ontology carries exactly one kind of derived shortcut, the claim-to-policyholder held_by edge. On the question that walks from a claim to its holder's city it scored 0.83 against normalized's 0.33 - but the flat graph scored 0.92 on the same question, with no shortcut at all, because in that schema a claim and a holder are the only two things there are. The shortcut cannot be credited for a result the shape without it beats.
Worse, the shortcut I failed to add cost me exactly where I predicted it would. Counting distinct coverage kinds across a holder's policies scored 0.17 on my supposedly well-designed graph against 0.67 on the flat one, which keeps them all in a single readable property. Totaling payments across a holder's claims tied at 0.33. On aggregation over a subgraph, the design I argued for is the one that loses, and it loses to the design I called broken.
Denormalization is not a compromise against good design. It is part of it, and I under-applied my own principle in the one place the workload needed it most.
Then I ran the whole grid again on a far larger model, and it answered the question I actually cared about β by refusing to behave the way I expected.
I had assumed the small model on the well-shaped graph would beat the big model on the badly shaped one. It does not. The comparison below uses only the answerable questions, so that refusals of unanswerable ones cannot flatter anybody β ninety episodes per small-model row across three passes, thirty per large-model row from its single pass:
The large model reads buried properties perfectly well. It answered every answerable question on the flat graph, thirty out of thirty, including the ones about entities that schema had dissolved into text β and the eighteen false refusals that crippled the small model vanished entirely. So the honest headline is not the one I wanted:
You can buy your way out of a badly shaped ontology. The exchange rate is about fourteen times the cost per question β that is what the large model on the flat graph costs against the small model on the shaped one, $1.90 per hundred questions against thirteen cents. If you keep the flat graph and simply upgrade the model underneath it, you pay twenty times what that same small model was costing you on it.
That is the result, and it is more useful than the one I predicted, because of what it implies. On answerable questions with a frontier model, shape stopped being a correctness problem and became a cost problem: flat and shaped both answered all thirty, and what separated them was the bill. With a small model, shape was correctness. Which means a well-designed ontology is not primarily a quality investment. It is what lets you run a cheap model. A flat graph silently couples your knowledge layer to your model budget: it works, as long as you keep paying for something big enough to read through it, on every question, forever.
Across all four bands the ordering is cleaner than the answerable-only view suggests. The shaped graph scored a perfect 1.000 with the large model β every band, including the unanswerable one. Flat scored 0.950 and normalized 0.825. Even where money solves the correctness problem, it does not quite solve all of it, and it never touches the bill.
And here the cross-check run taught me something about my own caution. In that run, the same grid, the same questions, the tools reading files instead of Cosmos, the shaped graph produced the single most alarming result in the whole project: asked what the weather was on the day of an incident, a fact the ontology deliberately does not model, it answered "hail damage", having found the claim's cause property and offered it as the weather. I wrote that up as the design I favour committing the failure I had called the most dangerous in this class of system. I also wrote that it was one episode from one pass, an observation rather than a measurement, and that I would not build a law on it.
Running it again on the real infrastructure was the right call. In the measured campaign the shaped graph refused all ten of the large model's unanswerable questions correctly, and the confabulation appeared on the flat graph instead, which answered "hail" to the same question. Same model, same question, same trap β a different shape caught by it.
So the finding is not that rich graphs invite confabulation. It is that a cause property containing the words "hail damage" will tempt a model asked about weather, wherever you store it, and which shape gets caught is not stable across runs. That is a smaller claim than the one I nearly made, and it is the one the evidence supports. It also means the hazard is real and shape-independent: if your graph holds a field that reads like an answer to a question it does not answer, some model somewhere will hand it over.
The normalized ontology lost overall on both models β worst of the three with the small one, and still trailing at 0.867 on the answerable questions with the large one, while costing $9.31 per hundred of them, nearly five times the flat graph on the same model, and still running out of turns seven times. Reification survives contact with a bigger model no better than it survives a small one, because its problem was never comprehension. It was distance: 15 tool calls per question against the shaped graph's 8.3, even with a model that never once misread what it was given.
But it did not lose on every axis, and the exception is the most instructive thing in the whole dataset. On single-fact lookups the normalized graph scored 0.867 against the flat graph's 0.533, and on the two questions that broke flat it was the better shape by a distance: 0.83 against 0.17 for a policy's premium, 0.83 against 0.33 for a provider's specialty. Reification is grotesquely expensive, but it never hides anything β every entity really is a vertex, so every entity can be found.
Which is the whole argument in one line: each failing shape kept one half of the problem and threw the other away. Flat kept distance and lost addressability. Normalized kept addressability and lost distance. Both are competitive on the half they kept and hopeless on the half they did not.
The shaped ontology is not a compromise between them. It scores 0.967 on the band that defeats flat and 0.967 on the band that defeats normalized β it refuses the trade. That is where its advantage actually lives, and averaging the bands into a single accuracy number hides it completely.
One measurement the cross-check run could not support, and this one can: latency. Reading files, per-call times swung wildly between identical passes β the same variant averaging 1.17 seconds per call in one pass and 4.21 in another, and I refused to report it, because service variance was larger than anything the ontology was doing.
Against Cosmos the picture is orderly, and for a slightly deflating reason.
Per-question time is now cleanly separated between shapes and stable within them: the widest swing inside a variant is seven seconds, against a gap of nearly fifty seconds between flat and normalized. A question against the normalized graph takes two and a half times as long as the same question against the flat one.
The deflating part is the last column. Per call, all three shapes cost essentially the same β between 6.2 and 7.4 seconds, whatever the schema. So this is not an independent finding; it is the tool-call count restated in seconds. What the database adds is consistency: a round trip to Cosmos is predictable in a way that a local dict was not, which is precisely what makes the number reportable at all.
That has a practical consequence worth keeping. If per-call latency is roughly constant, then the shape of your ontology sets your agent's response time as directly as it sets your token bill, and you can estimate it before you build: count the hops.
V. The principles, each with its receipt
Articles about ontology design usually present principles as taste. Most of these arrive with the number that earned them; where one does not, I say so rather than dress it up.
If it can be asked about, it must be a vertex. This is the one I would carve above the door. The flat graph holds every policy premium and every provider specialty, in the same database as the others, fully indexed β and the small model retrieved almost none of them: 0.17 and 0.33, against 1.00 and 0.83 on the shaped graph. The whole band shows it, 0.533 against 0.967. Burying an entity inside another entity's property does not make it hard to reach; it makes a reasonable question about it come back wearing somebody else's name, which the agent reads as proof of absence.
Note the conditional the crossover forces: the frontier model dug all of them out. Burial is not an absolute barrier. It is a tax payable in model tier.
Granularity is a budget, and reification spends it fastest. Every node that exists only for schema purity charges rent on every visit. The normalized graph paid 3.7 times the shaped graph's tool calls on single-fact lookups and 2.3 times on short traversals, four times the tokens on band L, and ran out of turns twenty-five times in a hundred and twenty episodes, against three for shaped. A model twenty-five times more expensive per input token lifted it from 0.489 to 0.867 on answerable questions, a real recovery, bought at $9.31 per hundred questions, nearly five times what that same model cost on the flat graph. Distance is the one property no budget makes cheap.
Entry points are architecture, and they are the most model-sensitive thing here. Questions arrive holding a name or a number, and the first tool call decides whether the agent starts working or starts guessing. With the small model that call came back empty in 10% of episodes on the shaped graph, 26% on the flat one and 53% on the normalized one, where search lands on an Attribute vertex rather than the thing itself. With the frontier model it came back empty in none of them, on any shape. So a wasted first move is not purely a property of the graph - it is what happens when a weaker model meets an entry point that does not match its expectations. Design the doors for the model you intend to run, not the one you are testing with.
Denormalize deliberately β and be honest when the receipt goes against you. I added one derived shortcut, held_by, expecting it to pay on the claim-to-holder path. It scored 0.83 there against normalized's 0.33 - and the flat graph scored 0.92 on the same question with no shortcut at all. I cannot credit my shortcut for a result that the shape without it beats, and the clean test, shaped with the edge against shaped without it, is one I did not run.
The shortcut I failed to add is the clearer lesson. Counting coverage kinds across a holder's policies scored 0.17 on my well-designed graph against 0.67 on the flat one, which keeps them all in a single readable property. On aggregation over a subgraph, the design I argued for loses to the design I called broken. Choosing shortcuts is the design, and the workload tells you which you need β I picked the wrong one and the data said so.
Bound the fan-out. Twenty-result search caps and thirty-result traversal caps meant an over-connected node cost visible, countable extra calls instead of one enormous response. I did not run an uncapped arm, so I cannot tell you what the caps saved β I can only tell you that without them, cost hides inside response size where no call-count metric will find it.
Make absence legible, and measure it honestly. The unanswerable band is a design requirement, not a test trick. But read it beside the answerable one: flat's perfect 1.000 refusal score sat next to eighteen refusals of questions it could have answered, while the shaped graph, which refused the fewest, gave up the two unanswerable episodes it missed rather than inventing anything. An ontology should make "we do not model this" reachable and true β and your evaluation needs both halves, or it cannot tell a graph that knows its limits from a graph that cannot find its own contents.
Names are the API. The agent reads edge names the way a developer reads a function signature, and this experiment bundles naming with structure into a single variable called design, deliberately. That one arrives without a number of its own. Separating them is the obvious next ablation, and I would rather say that than claim more than I measured.
VI. What this is not
It is not a benchmark of graph databases. All three shapes sit in the same Cosmos DB account, under the same tool layer, with the same indexing defaults, so the store is held constant rather than tested β nothing here says anything about Cosmos against any other graph database, and it is not tuned. It is not a claim that traversal-only is how production agents should retrieve everything; fuzzy questions with no clean entry point need search, and section IV shows exactly where pure traversal earns its keep and where it would not. It is not an argument against normalization in transactional systems β the normalized shape here is wrong for this consumer, an agent paying per hop, not wrong in general. And it is not a vendor comparison: Microsoft Foundry is the constant in this experiment, not the variable, and the same method would transfer to any platform with function calling.
One question deserves an answer rather than an assurance: does putting a database in the loop change the result? Before the measured campaigns I built the same four tools twice, once over Cosmos and once over the files the generator emits, and replayed the recorded tool calls against both β 3,056 distinct calls, every unique combination of tool and arguments the campaigns produced. The two implementations returned identical answers on every single one. That does not make the database irrelevant: it costs latency and request units, and across the campaigns the agent took about 4% more steps against Cosmos, almost all of it on the normalized graph. It does mean the answers the agent saw were a property of the ontology's shape rather than of the engine underneath it, which is the only thing the comparison in section IV needs to be true. The replay is a script in the repository, not a claim in a paragraph.
Several honesty points, inherited from the router work. Forty synthetic questions over one synthetic domain are a small instrument; the contrasts are sharp because the shapes are archetypes, and your production ontology sits somewhere between them, which is rather the point of measuring the endpoints. The small-model campaign ran three full passes and I have quoted its noise floor throughout, but the large-model grid ran once β its saturated results (thirty of thirty, twice) are hard to misread, yet any two-question difference inside it is an observation, not a measurement, and I have not built an argument on one. The whole experiment also scores a single small model against a single large one; the shape of the curve between them is unmeasured.
Two limitations in the instrument itself, which I found while auditing the code for release and would rather publish than quietly fix. First, traverse returns an empty neighbor list when asked for an edge type a graph does not have, which is indistinguishable from a node that genuinely has no such neighbors. Across the campaigns the agent made a hundred and twelve such calls, and nine of the normalized graph's twenty-two false refusals contain one; the analysis script counts both. It does not explain the headline gap - the shaped graph made the most of these calls and still refused the fewest questions - but a tool that reports "nothing here" when it means "no such edge type" is a confound, and the honest version would return an error naming the available types. Second, the losslessness proof compares reconstructed facts, so it is blind to edges that carry no fact of their own: the flat graph's connective edges and the shaped graph's derived shortcuts. I have since added a second check that validates those against the facts they re-express, and it catches deletion and mis-pointing of every one of them - but the campaigns ran before that check existed, and the guarantee it now provides is one I did not have at the time.
Coda β the shape is the system prompt
We spend enormous care on the words we put in front of a model and almost none on the shape of the knowledge we put behind it. But to a tool-using agent, the ontology is a prompt β it is the set of moves the world affords, written in node types and edge names, read one tool call at a time. Design it for the reader it actually has.
The fact base, the three ontologies, the losslessness proof, the questions, the oracle, the agent and every campaign receipt are in the repository at github.com/mcekikj/ontology-shape-race [7]. Model the same truth a fourth way, race it against mine, and if your shape beats these on the questions that defeated them, I genuinely want to see the walk that got you there.
All data in this experiment is synthetic and fictional; no real person, company or place appears in it, and all measurements ran against the author's own Azure subscription on a dedicated Microsoft Foundry deployment. Earlier articles in this thread: My Routing Table Is Now a Baseline and The Router Changed Its Mind, My Code Did Not.
References
[1] M. Cekikj β My Routing Table Is Now a Baseline: Measuring Microsoft Foundry's Model Router, Three Times, With Receipts. AI Advances. https://medium.com/ai-advances/my-routing-table-is-now-a-baseline-measuring-microsoft-foundrys-model-router-three-times-with-e7fcb28633d6
[2] M. Cekikj β The Router Changed Its Mind, My Code Did Not: A Lockfile for Microsoft Foundry's Model Router. https://medium.com/@cekikjmiodrag/the-router-changed-its-mind-my-code-did-not-a-lockfile-for-microsoft-foundrys-model-router-692a2973a41a
[3] M. Cekikj β Designing a Persistent Knowledge Layer That Refuses to Guess. Towards Data Science. Part I of a three-part series; parts II and III are cross-referenced from it. https://towardsdatascience.com/designing-a-persistent-knowledge-layer-that-refuses-to-guess/
[4] Microsoft Learn β Azure Cosmos DB for Apache Gremlin, introduction. https://learn.microsoft.com/en-us/azure/cosmos-db/gremlin/introduction
[5] Microsoft Learn β How to use function calling with Microsoft Foundry Models. https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/function-calling
[6] Microsoft Learn β Azure Retail Prices REST API overview. https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices
[7] GitHub β ontology-shape-race: the fact base, the three ontologies, the losslessness proof, the agent, the Cosmos loader and every campaign receipt. https://github.com/mcekikj/ontology-shape-race