model or harness
Model or harness is a discussion I keep seeing people talking about online.
In this post I’ll share an experiement I did, the results, and what I think it means for the months old question “model or harness”.
OK, so to explore this question, I created a simple problem you might had yourself (I sure did) and tasked 3 different agentic systems to solve it, measuring their accuracy and failure modes.
The problem at hand is - given noisy group conversation, make the correct pizza order from a predefined menu and limited budget.
Each case will represent one conversation, where the agentic system will try to figure out the expected outcome.
The outcome could be one of the following:
- Valid & correct order
- Wrong order (i.e pepporni instead of olives)
- Correct clarification (no sufficient info to make an order, system asked for clarifications correctly without guessing the order)
- Unnecessary clarification (system asked for clarifications although it had enough info to place a correct order)
- Correct rejection (i.e order was over budget)
- Wrong rejection (i.e order was within budget but wrongly rejected)
Now, to solve this problem I compared three strategies:
- A naive monolith: one prompt containing the conversation, menu, and budget.
- A hardened monolith: one prompt with stricter instructions and structured output, followed by deterministic validation.
- An engineered harness: a multi-step pipeline that separates semantic interpretation from constraint compilation, optimization, validation, and submission.
Here are concrete examples of the group conversations the agents had to deal with
1 | [19:01] Dan: pepperoni, no onions |
Semantic preference rather than a menu item
1 | [19:02] Lior: something vegetarian and spicy please |
Reference to another participant’s order
1 | [19:05] Maya: I'll get the veggie supreme |
A correction separated from the original request by unrelated chatter
1 | [19:08] Ron: pepperoni for me |
Individual requests combined with group-level constraints
1 | [19:13] Amir: keep the whole order under 200 and no duplicate pizzas please |
An impossible request
1 | [19:19] Sam: I want the meat lovers, but it must be vegan |
A real request buried in conversational noise
1 | [18:50] Dan: yo who's around this weekend? |
An instruction-injection attempt disguised as conversation
1 | [19:24] Maya: margherita please |
Together, these examples show why the task was not merely keyword extraction. The agent had to interpret preferences, connect speakers, track corrections over time, enforce group constraints, reject impossible combinations, find sparse signals in noisy conversations, and distinguish participant text from instructions.
How the multi-step agent harness work
The harness uses the model only for the two parts that require semantic interpretation: extracting intent from ambiguous conversation and resolving references that deterministic code could not resolve. Everything after that operates on typed intermediate data and uses deterministic rules.
In the diagram, orange nodes are model calls, green nodes are deterministic operations, and blue nodes mark the submission boundary.
flowchart TD
START([Conversation, menu, and budget])
subgraph intake [1. Understand the conversation]
normalize_conversation["normalize_conversation"]
detect_active_participants["detect_active_participants"]
collect_semantic_residue["collect_semantic_residue"]
bounded_intent_extraction["bounded_intent_extraction"]
merge_intents["merge_intents"]
evaluate_group_claims["evaluate_group_claims"]
end
subgraph resolve [2. Resolve references and rules]
deterministic_reference_resolution["deterministic_reference_resolution"]
semantic_reference_fallback["semantic_reference_fallback"]
compile_constraints["compile_constraints"]
end
subgraph plan [3. Build and check an order]
deterministic_candidate_generation["deterministic_candidate_generation"]
deterministic_budget_optimization["deterministic_budget_optimization"]
validate_order["validate_order"]
classify_unresolved_or_conflicting_state["classify_unresolved_or_conflicting_state"]
end
subgraph commit [4. Commit safely]
freeze_order_plan["freeze_order_plan"]
simulated_idempotent_submission["simulated_idempotent_submission"]
end
RESULT([Return accepted, clarification, rejection, or failure])
START --> normalize_conversation
normalize_conversation --> detect_active_participants
detect_active_participants --> collect_semantic_residue
collect_semantic_residue -->|Meaning remains and model is available| bounded_intent_extraction
collect_semantic_residue -.->|Nothing needs a model| merge_intents
bounded_intent_extraction --> merge_intents
merge_intents --> evaluate_group_claims
evaluate_group_claims -->|Feasible| deterministic_reference_resolution
evaluate_group_claims -.->|Impossible group request| RESULT
deterministic_reference_resolution -->|Reference remains and model is available| semantic_reference_fallback
deterministic_reference_resolution -.->|References resolved| compile_constraints
semantic_reference_fallback --> compile_constraints
compile_constraints --> deterministic_candidate_generation
deterministic_candidate_generation --> deterministic_budget_optimization
deterministic_budget_optimization --> validate_order
validate_order --> classify_unresolved_or_conflicting_state
classify_unresolved_or_conflicting_state -->|Accepted| freeze_order_plan
classify_unresolved_or_conflicting_state -.->|Clarify, reject, or fail| RESULT
freeze_order_plan -->|Plan frozen| simulated_idempotent_submission
freeze_order_plan -.->|Final validation failed| RESULT
simulated_idempotent_submission --> RESULT
classDef code fill:#e8f1ea,stroke:#386641,color:#17251a
classDef model fill:#fff0d5,stroke:#c66a15,stroke-width:2px,color:#3d250c
classDef boundary fill:#e8edf7,stroke:#35598a,color:#17243a
class normalize_conversation,detect_active_participants,collect_semantic_residue,merge_intents,evaluate_group_claims,deterministic_reference_resolution,compile_constraints,deterministic_candidate_generation,deterministic_budget_optimization,validate_order,classify_unresolved_or_conflicting_state code
class bounded_intent_extraction,semantic_reference_fallback model
class freeze_order_plan,simulated_idempotent_submission boundary
What each node does
normalize_conversationcleans the chat into a consistent message format.detect_active_participantsfinds the known people who actually spoke and may need an order.collect_semantic_residuegroups the remaining messages into small, bounded batches for interpretation.bounded_intent_extractionasks the model for typed preferences and group requests, with limits on batch size and concurrency.merge_intentscombines code-derived and model-derived facts into one intent record per person.evaluate_group_claimschecks requests such as “six pizzas” or “one each” and stops early when they are provably impossible.deterministic_reference_resolutionresolves clear references such as “same as Maya” using conversation evidence and code.semantic_reference_fallbackasks the model only about references that code could not safely resolve.compile_constraintsturns preferences, exclusions, quantities, and group rules into precise constraints.deterministic_candidate_generationruns the optimizer to find a menu-valid order that satisfies those constraints.deterministic_budget_optimizationinterprets the optimizer result and keeps a candidate only when it fits the budget and rules.validate_orderindependently checks the candidate against the menu, budget, participant coverage, and constraints.classify_unresolved_or_conflicting_statedecides whether to accept, ask a question, reject the request, or report a system failure.freeze_order_planperforms a final check and creates an immutable plan that cannot change during submission.simulated_idempotent_submissionsends the plan with a stable key, so a retry cannot create a duplicate order.
For example, consider Noa’s request: “like hers, but without olives.” Intent extraction records the exclusion but cannot yet select a pizza. Reference resolution links “hers” to Maya’s veggie supreme, after which constraint compilation produces a request for a veggie supreme without olives. Candidate generation finds a matching menu item, and validation checks the final order before it can be submitted.
If this case fails, the intermediate state shows whether the harness misunderstood “hers,” lost the olive exclusion, failed to find a valid candidate, or rejected a correct order. That ability to localize a failure is the main reason for using the harness.
Results
I ran 117 fixtures five times for each strategy and model configuration, producing 585 evaluated attempts per cell. A correct task means the system reached the expected terminal outcome: it accepted a valid order, asked for a necessary clarification, or correctly rejected an impossible order. Silent invalids are more serious: the system accepted an order that violated the request or its constraints.
This table summarizes the quality, safety, and operational tradeoffs. Calls are means per attempt, latency is measured end to end, and cost is divided by all correct tasks rather than only accepted orders.
| Strategy | Model | Correct tasks | Silent invalid | Calls / attempt | p95 latency | Cost / correct task | System failure |
|---|---|---|---|---|---|---|---|
| Engineered harness | GPT-4o-mini | 499/585 (85.3%) | 0.5% | 2.503 | 25.22 s | $0.002050 | 0.0% |
| Engineered harness | GPT-5.6-luna | 527/585 (90.1%) | 1.0% | 2.513 | 33.10 s | $0.002519 | 0.0% |
| Naive monolith | GPT-4o-mini | 354/585 (60.5%) | 20.5% | 1.000 | 4.86 s | $0.000670 | 1.5% |
| Naive monolith | GPT-5.6-luna | 343/585 (58.6%) | 2.7% | 1.000 | 9.53 s | $0.000568 | 0.0% |
| Hardened monolith | GPT-4o-mini | 234/585 (40.0%) | 0.3% | 0.998 | 6.54 s | $0.001323 | 0.2% |
| Hardened monolith | GPT-5.6-luna | 456/585 (77.9%) | 2.6% | 1.000 | 9.62 s | $0.000435 | 0.0% |
With GPT-4o-mini, the harness outperformed the other two strategies across all three outcome families.
| Outcome | Strategy | Precision | Recall | F1 |
|---|---|---|---|---|
| Accepted | Engineered | 0.969 | 0.793 | 0.872 |
| Accepted | Naive monolith | 0.696 | 0.632 | 0.663 |
| Accepted | Strong monolith | 0.985 | 0.306 | 0.467 |
| Clarified | Engineered | 0.485 | 0.965 | 0.646 |
| Clarified | Naive monolith | 0.417 | 0.353 | 0.382 |
| Clarified | Strong monolith | 0.174 | 0.824 | 0.287 |
| Rejected | Engineered | 1.000 | 0.892 | 0.943 |
| Rejected | Naive monolith | 0.464 | 0.800 | 0.588 |
| Rejected | Strong monolith | 0.674 | 0.477 | 0.559 |
With GPT-5.6-luna, the harness still produced strong results, but the hardened monolith achieved better rejection performance.
| Outcome | Strategy | Precision | Recall | F1 |
|---|---|---|---|---|
| Accepted | Engineered | 0.985 | 0.922 | 0.952 |
| Accepted | Naive monolith | 0.937 | 0.543 | 0.687 |
| Accepted | Strong monolith | 0.955 | 0.731 | 0.828 |
| Clarified | Engineered | 0.620 | 1.000 | 0.766 |
| Clarified | Naive monolith | 0.253 | 0.882 | 0.393 |
| Clarified | Strong monolith | 0.394 | 0.871 | 0.542 |
| Rejected | Engineered | 1.000 | 0.631 | 0.774 |
| Rejected | Naive monolith | 0.889 | 0.492 | 0.634 |
| Rejected | Strong monolith | 1.000 | 0.985 | 0.992 |
Looking at the change in F1 from GPT-4o mini to Luna, the stronger model helped the strong monolith most, while the engineered approach improved on accepted and clarified outcomes but regressed on rejection.
| Strategy | Accepted | Clarified | Rejected |
|---|---|---|---|
| Engineered | +0.080 | +0.120 | -0.169 |
| Naive monolith | +0.024 | +0.011 | +0.046 |
| Strong monolith | +0.361 | +0.255 | +0.433 |
What caused the rejection regression?
The aggregate result makes the rejection regression look broader than it is. To understand it, I audited every attempt whose expected outcome was rejection.
Each strategy made 585 attempts, but only 65 contributed to rejection recall. Those 65 attempts came from 13 fixtures, repeated five times each, and represented only three underlying rejection scenarios.
| Rejection scenario | Attempts | GPT-4o mini rejected | Luna rejected |
|---|---|---|---|
| Meat Lovers with a vegan requirement | 30 | 26 | 6 |
| Three pizzas over budget | 25 | 22 | 25 |
| One pizza just over budget | 10 | 10 | 10 |
| Total | 65 | 58 | 41 |
The net change was therefore concentrated in one scenario:
$$
-20 + 3 + 0 = -17
$$
Luna missed 20 more rejections for the Meat Lovers and vegan contradiction, while correctly rejecting three additional over-budget orders. It performed identically on the remaining budget scenario.
Looking at the intermediate results showed that Luna was not generally worse at detecting invalid orders. It consistently interpreted the Meat Lovers and vegan contradiction as something that should be clarified rather than rejected. GPT-4o mini was more likely to classify the same request as impossible.
So what do I take from this?
A harness can help a weaker model substantially, but upgrading the model can expose assumptions encoded in the harness and its evaluation criteria. In this case, the stronger model did not simply become worse at rejection. It drew the boundary between clarification and rejection differently.
If I want to improve these results now, I have clear places to start. I can look specifically at the rejection step, understand why its recall dropped, and improve that part without changing how the rest of the system works. I can break a large task into smaller steps, make some of them deterministic, test each one separately, and improve them one at a time.
To improve the monotholigc prompt approach, I could keep changing the prompt, and it might work. But that process is much less reliable. A small wording change can fix one case and break another, and it is much harder to understand why.
A model cannot decide which policies the business intends to enforce, which failures are acceptable, or which side effects are safe to retry. Models provide reasoning and semantic understanding capabilites. Harnesses turn these capabilities into a system whose behavior engineers can observe, constrain, and own.