6. Learned CPTs¶
The synthesised tree says an aggregator delivers a wrong answer when both inputs are wrong. That is a modelling assumption, and in a real multi-agent system it is usually wrong in an interesting direction: a judge repairs some upstream errors and introduces others, so the true table is neither AND nor OR.
When per-node outcomes have been logged, stop assuming and estimate.
Fit a gate¶
learn_gate needs failure indicators, so invert the correctness columns once,
explicitly:
from hiphopsllm import load_outcomes
from hiphopsllm.bayes.learn import learn_gate
outcomes = load_outcomes()
calibration = outcomes[outcomes["split"] == "calibration"].copy()
for column in ("react_agent", "cot_agent", "aggregator"):
calibration[column] = 1 - calibration[column] # correct → failed
learned, distance = learn_gate(calibration, "aggregator", ["react_agent", "cot_agent"])
distance
{'and': 0.1225, 'or': 0.4947, 'nearest': 'and'}
distance is the mean absolute difference between the fitted P(Fail | parents)
column and each deterministic table. The gate is much closer to AND than to OR —
the redundancy is real — but 0.12 away from it, and that gap is where the
interesting behaviour lives.
import pandas as pd
pd.DataFrame(learned.cpt.rows())
react_agent cot_agent P(Fail)
0 OK OK 0.181818
1 OK Fail 0.200000
2 Fail OK 0.055556
3 Fail Fail 0.947368
Read the four rows against the AND gate the tree assumed (0, 0, 0, 1):
Row 0 — both drafts right, the judge fails 18% of the time. The AND gate says
zero. This is BE-aggregator-OWN and BE-aggregator-SELECT in the synthesised
tree, and the data says together they account for a fifth of all failures. A
model without them is optimistic by exactly that much.
Rows 1 and 2 are not symmetric (0.20 vs 0.056). The judge recovers from a bad
cot_agent draft far more often than from a bad react_agent one. No
deterministic gate can express that, and no amount of tuning the basic-event
probabilities will produce it.
Row 3 — both wrong, the judge is right 5% of the time. It occasionally produces a correct answer neither agent gave.
How much does the assumption cost?¶
This is the motivating result of the HIP-MAS study, reproduced under known ground truth: with a reviewer repairing 55% of upstream errors, the deterministic AND-series gate mispredicted held-out failure by +0.386, while the learned-CPT model was within 0.005.
You can see the mechanism directly here:
import numpy as np
fitted = learned.cpt.table[..., 1]
p_react, p_cot = 0.212, 0.153 # measured component failure rates
and_gate = p_react * p_cot # 0.032
learned_p = sum(
fitted[(a, b)] * (p_react if a else 1 - p_react) * (p_cot if b else 1 - p_cot)
for a in (0, 1) for b in (0, 1)
)
print(f"AND gate: {and_gate:.4f} learned: {learned_p:.4f}")
The AND gate predicts about 3% because it can only fail when both inputs do. The learned table predicts far more, because row 0 says the judge fails on its own.
Fit a whole network¶
from hiphopsllm import BayesianNetwork
from hiphopsllm.bayes.learn import fit_cpts
cpts, fits = fit_cpts(
outcomes[outcomes["split"] == "calibration"],
{"react_agent": [], "cot_agent": [], "aggregator": ["react_agent", "cot_agent"]},
outcomes_are_failures=False, # the columns hold 1 = correct
)
network = BayesianNetwork(cpts=cpts, name="learned")
network.p_fail("aggregator")
network.p_fail("aggregator", evidence={"react_agent": "Fail", "cot_agent": "Fail"})
The structure must be given parents-first, and outcomes_are_failures states the
polarity of the columns once, here, rather than silently everywhere.
Two guards¶
Smoothing is not cosmetic. At pilot sample sizes several parent
configurations — “both agents wrong and they agree” — are observed a handful of
times or not at all. An unsmoothed maximum-likelihood estimate would put a hard
0 or 1 in the table and make the network claim a certainty it has not earned.
alpha=1 (Laplace) is the default; the raw counts come back with the table so a
report can say how much of it was measured:
learned.n_observations # 160
learned.total_rows # 4
learned.prior_dominated_rows # 0 — every configuration was observed
learned.coverage # 1.0
print(learned.summary())
aggregator: 160 observations over 4 rows, 0 prior-dominated
(coverage 100%, Dirichlet alpha=1.0)
A coverage below 1.0 means some rows of your CPT are the prior, not the data. That is fine, as long as the report says so.
A CPT is never fitted on a test split. If the frame carries rows marked
test, held_out or eval, learn_cpt raises:
CPTLearningError: learn_cpt('aggregator') was given rows marked ['test'] in
column 'split'. Fit CPTs on the calibration split only; a table fitted on the
evaluation set makes every downstream number optimistic and untestable.
Override only when you mean it: check_split=False.
When to use which¶
Use the synthesised tree |
Use a learned CPT |
|
|---|---|---|
You have |
an architecture, and per-component failure rates at most |
per-item outcomes for every node |
It gives you |
cut sets, SPOFs, an FMEA, structural insight |
an accurate predictive model of one gate |
It cannot |
say what a component actually does, only what its archetype does |
tell you why, or generalise to an architecture you have not run |
They are complements. Use the tree to find where redundancy is missing; use a learned table where you have data and the archetype is visibly wrong.