<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Data Science Collective - Medium]]></title>
        <description><![CDATA[Advice, insights, and ideas from the Medium data science community - Medium]]></description>
        <link>https://medium.com/data-science-collective?source=rss----8993e01dcfd3---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Data Science Collective - Medium</title>
            <link>https://medium.com/data-science-collective?source=rss----8993e01dcfd3---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 11:05:16 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/data-science-collective" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Why DART wins your Optuna slice plot and loses the benchmark]]></title>
            <link>https://medium.com/data-science-collective/why-dart-wins-your-optuna-slice-plot-and-loses-the-benchmark-b797a3129821?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/b797a3129821</guid>
            <category><![CDATA[gradient-boosting]]></category>
            <category><![CDATA[hyperparameter-tuning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[lightgbm]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Ilia Ekhlakov]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 19:40:35 GMT</pubDate>
            <atom:updated>2026-11-02T08:02:02.566Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Three things an Optuna slice plot hid about DART, checked on 131 small datasets with LightGBM and XGBoos</strong></p><h3>A closed issue against my slice plots</h3><p><em>For readers who tune LightGBM or XGBoost with Optuna and read plot_slice to decide what goes into the search space.</em></p><p>I came across <a href="https://github.com/catboost/catboost/issues/1006">CatBoost issue #1006</a>. In September 2019 a user asked the CatBoost team to add DART, the boosting mode that randomly drops already built trees during training. XGBoost and LightGBM had it, so the request looked natural, though the author doubted that DART leads to better models in general. The maintainers closed it as “won’t fix”. Their reply: “We don’t see the profit from it.” They asked for an example where DART improves quality and the same gain cannot be reached by tuning other parameters.</p><p>This surprised me. In my LightGBM work I often check optuna.visualization.plot_slice after a search. When boosting_type is in the search space, DART trials often had a longer lower tail than the gbdt and goss ones, with more trials at low log loss.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4s1U4SPYjG5Uk_BuGBGpqw.png" /><figcaption>Validation log loss of every trial on Bank Marketing, learning rate on [0.1, 1], seed 1 of 3 (the median case). Left: one shared study, where TPE gave DART 42 trials and gbdt 8. Right: one study per mode, 20 trials each. The 10 goss trials and trials above 0.27 are not drawn.</figcaption></figure><p>Who is right? The CatBoost team gave a clear test, and I took it literally. DART counts as a win only if it beats a second, independent round of tuning of the plain booster. That answers whether DART helps. A second question stayed open: why did my plots say otherwise, and what else can such a plot hide?</p><p>On 131 small datasets the short answer is no. Against pooled re-runs of the plain booster, LightGBM’s DART scores 0.0033 lower in mean PR AUC (area under the precision-recall curve) and XGBoost’s 0.0020 lower, so a mean gain of 0.001 is excluded for both. The median shortfall is 0.0002 to 0.0003, so on most datasets DART and plain boosting are level and a few datasets carry the loss. My slice plots were not wrong either. In a learning-rate range that starts at 0.1, DART’s built-in step decay gives it room plain LightGBM lacks, and in almost every study DART won exactly when the sampler had given it the most trials.</p><p>The second answer matters beyond DART. A slice plot shows the trials the sampler chose, so it cannot show what each setting can do. Here it hid three things, and I now read the plot as a hint to test, not as evidence.</p><h3>What DART does to a boosted ensemble</h3><p>Gradient boosting builds trees one after another, each fixing the errors the earlier trees left. The DART paper notes a side effect: trees added late tend to change the prediction of only a few rows, so the first trees dominate. DART, introduced in 2015 by Rashmi Korlakai Vinayak and Ran Gilad-Bachrach, borrows a trick from neural networks. Before each new tree, it switches off a random part of the existing trees, as dropout does with neurons, and fits the new tree to what the remaining trees still get wrong. The new tree must cover for the missing ones. Then the dropped trees return, and the weights are rescaled so the sum of all trees does not overshoot.</p><p>In LightGBM the main controls are:</p><ul><li>drop_rate (default 0.1): the fraction of existing trees dropped in a round.</li><li>max_drop (default 50): the cap on dropped trees per round.</li><li>skip_drop (default 0.5): the probability that a round skips dropout altogether.</li><li>drop_seed (default 4): the seed that picks which trees to drop.</li></ul><p>XGBoost has rate_drop, skip_drop, one_drop (always drop at least one tree), sample_type (uniform or weighted choice of trees) and normalize_type (tree or forest rescaling). Two details matter later: the weight of the new tree shrinks as more trees are dropped, and the choice of dropped trees depends on a seed.</p><h3>Why a benchmark of small datasets</h3><p>I ran the check on <a href="https://github.com/firefly1248/SmallDataBenchmarks">SmallDataBenchmarks</a>, my fork of the <a href="https://www.data-cowboys.com/blog/which-models-are-best-for-small-datasets">Data Cowboys benchmark</a> of classifiers on small tabular datasets. Three reasons.</p><p>First, I know it well, and its controls, bugs and noise levels are documented. Second, it is cheap to rerun: a whole booster runs in hours on one laptop, so a methodological mistake can be rerun instead of explained away.</p><p>Third, DART is a regularization method, and small data needs regularization most. If dropping trees helps anywhere, it should help on datasets with a few hundred or a few thousand rows.</p><p>XGBoost also implements DART, so I added it as a second booster. That broke my second reason, because XGBoost’s DART search took 101.8 hours against 2.4 hours for plain XGBoost.</p><h3>DART against a second round of tuning, decided before the run</h3><p>I added DART to both boosters and changed nothing except its own parameters:</p><ul><li>lgbm_dart is LightGBM with boosting_type=&quot;dart&quot; and max_drop=-1. The default caps a round at 50 dropped trees, which would flatten drop_rate on larger ensembles.</li><li>xgboost_dart is XGBoost with booster=&quot;dart&quot;, uniform sampling and tree normalization.</li><li>Both search their library’s usual space plus a drop rate, log-uniform on [0.01, 0.5], and skip_drop, uniform on [0, 0.5]. With this range, dropout runs in at least half of the rounds.</li></ul><p>Everything else follows the benchmark’s protocol: nested cross-validation with 4 outer and 4 inner folds, 50 trials per outer fold from Optuna’s TPE sampler (Tree-structured Parzen Estimator), at most 500 trees, a learning rate between 0.001 and 0.3, and selection on PR AUC.</p><p>The control decides what counts as a win. A plain booster tuned once is a single draw from a noisy search. So DART faced two controls, the first run of each plain booster and a second run with another sampler seed (the seed control). For LightGBM I added a third run and one with twice the trial budget.</p><p>Before the run I fixed the analysis as one family of four tests, DART against each control in each library, with Holm correction (a standard fix for testing several hypotheses at once). “DART beats tuning” needed a significant gain against both controls, plus a larger effect than the seed alone produces. After seeing the results I added bootstrap intervals against the pooled controls, with a margin of 0.001 chosen after the fact. Those were not planned, and I mark which is which.</p><h3>Neither booster gains, and a gain of 0.001 is excluded</h3><p>Each DART lost to each of its controls. Mean PR AUC over the datasets each pair shares:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T-G7Wt-VUR4ww6aqR1CBSw.png" /><figcaption>Mean PR AUC of each DART and each control over shared datasets, with Holm correction over the four planned tests.</figcaption></figure><p>LightGBM has 131 datasets, XGBoost 129: its DART search on the two 100-class plant-species datasets was projected past my 12-hour limit and skipped.</p><p>Only the comparisons with the first runs are significant, but that does not mean DART ties the seed control. The gap to it is as large, -0.0035 for LightGBM. Most datasets sit near zero and a few carry the loss, so a rank test has little to work with.</p><p>Against the mean of the controls (the pooled analysis, added after these results), LightGBM’s DART is 0.0033 behind, with a 90% bootstrap interval of [-0.0068, -0.0008]. XGBoost’s DART is 0.0020 behind, [-0.0032, -0.0008]. The upper bounds sit below zero, so a mean gain of 0.001 is excluded at one-sided 95% for both libraries. The medians are -0.0003 and -0.0002, so on a typical dataset you will not notice the difference.</p><p>Three checks could have rescued DART. None did:</p><ul><li><strong>DART’s own seed.</strong> A second LightGBM DART search moves the mean by +0.0017, all of it from one dataset. The two seeds together are still 0.0024 behind, [-0.0043, -0.0008].</li><li><strong>A higher learning-rate cap.</strong> DART picked a rate above 0.2 in 21% of folds, plain LightGBM in 7.6%. With the cap raised from 0.3 to 1.0 on 31 datasets, DART scores 0.0076 lower than with the old cap.</li><li><strong>Other outer splits.</strong> On 30 datasets under five outer splits, the mean gap moves between -0.0012 and +0.0018 and averages +0.0001. The sign changes with the split, so on a small set of datasets DART and plain LightGBM are within split noise.</li></ul><p>Small data was my third reason, so I checked it separately. On the smallest third of the datasets (50 to about 540 rows) LightGBM’s DART is 0.0018 behind the pooled controls, XGBoost’s 0.0024. The seed noise is also largest there: a re-tune alone moves a small dataset’s score with a standard deviation of 0.017 for LightGBM and 0.014 for XGBoost, against 0.003 on the largest third. Where regularization should matter most, the search noise is larger than any gain DART could bring.</p><h3>Back to the slice plots: a hidden step decay and a sampler that follows the winner</h3><p>If DART does not win on the benchmark, why did it keep winning my slice plots? My work searches differed in three ways: hundreds of thousands of rows, 50 to 300 trees with no early stopping, and a learning rate searched on [0.1, 1]. The benchmark searched it on [0.001, 0.3]. I checked the difference in a separate notebook, on Bank Marketing (45,211 rows), six more OpenML datasets and synthetic data with label noise.</p><p>DART has a built-in learning-rate decay. LightGBM adds each new DART tree with weight learning_rate / (1 + k), where k is the number of trees dropped in that round, and shrinks a dropped tree by k / (k + 1) each time. The trained model shows it: at learning rate 0.5 and 300 trees, the tree weights of a DART model sum to 41.3, against 149.5 for plain LightGBM. DART takes 28% of the step it was given.</p><p>So DART needs a higher learning rate to reach its best. In sweeps with 300 trees and everything else fixed, DART’s best learning rate was higher than plain LightGBM’s on all seven real datasets, by a median factor of 3.1 (the weights predict 3.6). Plain LightGBM’s best rate was below 0.1 on all seven. DART’s best log loss was lower on only one of them. The decay moves where the best learning rate sits, not how good the best model is.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f4iuHamIT5c1lKb7_JYWsQ.png" /><figcaption>Best learning rate in a sweep with 300 trees and all other parameters fixed. The gray band is the range [0.1, 1] of my work searches.</figcaption></figure><p>A range that starts at 0.1 cuts plain LightGBM off from its best step, and DART is not cut off. Over 36 Optuna studies on Bank Marketing and the synthetic data, DART’s best trial beat plain LightGBM’s in 14 of 18 studies on [0.1, 1] and in 2 of 18 on [0.001, 1]. On synthetic data, DART’s lead on the narrow range grows with label noise in the sweeps, because noise pushes plain LightGBM’s best step further down.</p><p>The sampler does the rest. With boosting_type as one categorical parameter, TPE sends most trials to the mode that looks best. In one Bank Marketing study on [0.1, 1], DART got 43 of 60 trials, plain LightGBM 8. Over all 36 studies:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c_sXNv6oToK61utUwccHoQ.png" /><figcaption><em>36 Optuna studies on Bank Marketing and synthetic data: the mode that got the most trials won 35 times.</em></figcaption></figure><p>More trials in one mode give that mode a longer lower tail on the slice plot. Equal counts do not remove it: when I draw only as many DART trials as plain LightGBM has, DART keeps its lead in all 14 of the 18 studies on [0.1, 1] where it had it. The count is not the whole story, and the leader’s trials also sit closer to the region the sampler has already found. But allocation follows the outcome, so this table cannot say which causes which. The control is to give each mode its own study, so the sampler cannot move trials between them. On Bank Marketing, over three seeds, DART’s lead on [0.1, 1] then shrinks to 0.0010 in test log loss, averaged over three seeds with a standard deviation of 0.0019. DART wins in two of the three seeds. Three seeds are too few to say more than that the lead is small.</p><p>One of my three predictions failed, and I keep it in. I expected DART’s lead on [0.1, 1] to grow with noise in the studies as it does in the sweeps. In 1 of 3 seeds it did not, because plain LightGBM got too few trials near its own optimum. The search can also shorten plain LightGBM’s total step by choosing fewer trees, which the sweeps do not allow.</p><p>Three things a slice plot hid here, none visible in the plot itself:</p><ul><li><strong>It shows what the sampler chose, not what each setting can do.</strong> Trial counts follow the results, and the leader’s trials are better placed, so the categories are compared on unequal and unequally tuned samples.</li><li><strong>One parameter name can mean two scales.</strong> In my runs a DART model took about 28% of the step that plain LightGBM takes at the same learning rate, so a single learning-rate axis carries two different units.</li><li><strong>The search range decides who is in the picture.</strong> On [0.1, 1] plain LightGBM never reaches the rates where it does best, and the plot shows no sign that anything is missing.</li></ul><p>Both sides were right. My slice plots showed a real effect of the search space and the sampler. The CatBoost team asked for a gain that tuning other parameters cannot reach, and here the gain disappears as soon as plain LightGBM can tune its learning rate down to 0.001.</p><p>The benchmark also turned up three smaller costs of DART, and they matter before you ship it.</p><h3>One fold where the drop pattern decided everything</h3><p>Half of LightGBM DART’s loss comes from a single fold. On the fourth outer fold of pendigits, a 10-class handwriting dataset, the final DART model predicts class 8 for 91% of the rows. It scores 0.101 PR AUC, chance level for ten classes. Its other folds, and plain LightGBM on every fold, score 0.999. Without pendigits the gap to the first run halves, from -0.0033 to -0.0016.</p><p>My first guess was overfitting, and it was wrong: the refit model collapses on its own training rows too, while the same parameters score above 0.998 on all four inner folds.</p><p>The cause is the drop pattern. With these parameters (learning rate 0.3, drop_rate 0.033, skip_drop 0.44) the model survives with drop_seed 1 and collapses again with drop_seed 2 and 3. Of seven patterns I tried on the full training rows, three collapse, which overstates the rate because I found the first one by its collapse. LightGBM draws the pattern from drop_seed and the iteration number only, not from the data. So the inner folds replayed the same pattern on three quarters of the rows and survived. Cross-validation could not catch it.</p><p>This is rare. Out of 524 outer folds it is the only chance-level one, it disappears with the second DART seed, and XGBoost’s DART has none. But plain boosting does not fail this way. A configuration that passes cross-validation can still break at refit. If you ship LightGBM DART, score the final model on its training rows before you trust it.</p><h3>DART costs calibration when you select on PR AUC</h3><p>PR AUC only scores the ranking of predictions, so I also measured calibration on the 108 datasets where it can be estimated. ECE (expected calibration error) is the average gap between predicted confidence and observed accuracy.</p><p>Under PR AUC selection LightGBM’s DART raises ECE from 0.067 to 0.106. The rise is heavy-tailed: the median increase is only 0.013. XGBoost’s DART pays less, ECE from 0.063 to 0.074.</p><p>My work searches selected on log loss, not PR AUC, so I ran the LightGBM DART search once more with log loss as the objective, same space and budget. Then DART calibrates close to plain LightGBM selected the same way: ECE 0.042 against 0.040, and PR AUC does not change (0.8277 against 0.8278). The Brier score (the mean squared error of predicted probabilities) is still slightly worse, 0.227 against 0.223. The log-loss search also chose less dropout and a higher learning rate, so part of the fix may be that it selects less DART. Why DART calibrates worse under PR AUC selection, I did not find out.</p><h3>XGBoost’s DART costs at least 43 times the search time</h3><p>LightGBM’s DART search took 9.27 hours against 3.95 hours for its seed control, 2.35 times as long. XGBoost’s DART took 101.8 hours on 129 datasets against 2.36 hours, 43 times as long. That is a lower bound, since the two skipped datasets were projected to be the slowest. These are recorded times of whole searches on a shared machine, with parallel outer folds and pauses removed.</p><p>The ratio grows with the number of classes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WHsiD8aI4cYz3TTKPtAGXA.png" /><figcaption><em>DART’s search time over its seed control’s, per dataset, median and quartiles.</em></figcaption></figure><p>The source code explains the shape. Plain boosting adds each new tree to a running prediction. XGBoost’s DART (Dart::PredictBatchImpl in src/gbm/gbtree.cc, version 3.2.0) rebuilds the prediction every round from every tree that was not dropped, so the cost grows with the trees and with the classes. LightGBM&#39;s DART (src/boosting/dart.hpp, version 4.6.0) only subtracts and re-adds the trees it drops. I did not profile the individual terms, so this is consistent with the numbers, not a measured breakdown.</p><h3>What I do with DART now</h3><p>The list below covers LightGBM only, since I don’t use XGBoost in practice.</p><ul><li><strong>I no longer put DART in a search space by default.</strong> It did not beat a second round of tuning on 131 small datasets, and it costs calibration and time.</li><li><strong>When DART wins a slice plot, I check the learning-rate range first.</strong> If the range starts at 0.1 or higher, I widen it to 0.001 on a log scale or run one study per boosting mode with the same range and budget, and I compare refit test scores, not the best trial. I cannot equalize the modes with early stopping, because LightGBM has no early stopping for DART. If DART’s lead survives that, it is worth a closer look.</li><li><strong>If I ship LightGBM DART, I score the final model on its training rows.</strong> A configuration that passed cross-validation can still collapse at refit.</li></ul><p>This test has limits. It used 50 trials per fold, at most 500 trees, datasets of at most 10,000 rows, selection on PR AUC (log loss only for LightGBM’s DART), one sampler seed for XGBoost’s DART, and five outer splits on only 30 of the 131 datasets.</p><p>The slice-plot notebook shows a mechanism on seven real datasets and one synthetic generator, but not how often it decides a real search. The searches in my own work and research projects mostly use far more rows than the benchmark does. DART may still help on data I did not test, and the CatBoost team’s question remains open in the only useful form: “Show me a case where tuning cannot match it”.</p><h3>Resources</h3><ul><li><a href="https://github.com/firefly1248/SmallDataBenchmarks">SmallDataBenchmarks</a>: code, results and the full DART write-up in <a href="https://github.com/firefly1248/SmallDataBenchmarks/blob/master/Findings_notes.md#dart-buys-nothing-over-tuned-lightgbm-or-xgboost">Findings_notes.md</a> (commit a781b23)</li><li><a href="https://github.com/firefly1248/demo-notebooks/blob/main/notebooks/lightgbm/lightgbm_dart_slice_plot.ipynb">lightgbm_dart_slice_plot.ipynb</a>: the slice-plot experiments, with cached results (commit f2041a8)</li><li><a href="https://github.com/catboost/catboost/issues/1006">CatBoost issue #1006</a>: the 2019 request to add DART and the maintainers’ answer</li><li><a href="https://github.com/microsoft/LightGBM/blob/v4.6.0/src/boosting/dart.hpp">LightGBM src/boosting/dart.hpp, v4.6.0</a>: the learning_rate / (1 + k) step and the weight normalization</li><li><a href="https://github.com/dmlc/xgboost/blob/v3.2.0/src/gbm/gbtree.cc">XGBoost src/gbm/gbtree.cc, v3.2.0</a>: Dart::PredictBatchImpl, the per-round rebuild of the prediction</li><li><a href="https://lightgbm.readthedocs.io/en/stable/Parameters.html">LightGBM parameters</a>: the DART parameters and their defaults</li><li><a href="https://xgboost.readthedocs.io/en/stable/parameter.html">XGBoost parameters</a>: the Dart booster parameters</li><li><a href="https://optuna.readthedocs.io/en/stable/reference/visualization/generated/optuna.visualization.plot_slice.html">Optuna plot_slice</a></li><li><a href="https://lightgbm.readthedocs.io/en/stable/pythonapi/lightgbm.early_stopping.html">LightGBM early_stopping callback</a>: no early stopping with boosting_type=&quot;dart&quot;</li><li><a href="https://proceedings.mlr.press/v38/korlakaivinayak15.html">DART: Dropouts meet Multiple Additive Regression Trees</a>, Rashmi Korlakai Vinayak and Ran Gilad-Bachrach, AISTATS 2015, PMLR 38:489–497: the original DART paper</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b797a3129821" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/why-dart-wins-your-optuna-slice-plot-and-loses-the-benchmark-b797a3129821">Why DART wins your Optuna slice plot and loses the benchmark</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[NVIDIA RTX Spark vs. Apple M-Series: Which is Better for Local AI Development?]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/nvidia-rtx-spark-vs-apple-m-series-which-is-better-for-local-ai-development-6036b868b44a?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/1024/1*hhiGRSdzKXyFkVOGd212Mw.png" width="1024"></a></p><p class="medium-feed-snippet">A simple breakdown of NVIDIA&#x2019;s new Arm superchip, unified memory math, local LLM capabilities, and how it compares against Apple&#x2019;s&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/nvidia-rtx-spark-vs-apple-m-series-which-is-better-for-local-ai-development-6036b868b44a?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/nvidia-rtx-spark-vs-apple-m-series-which-is-better-for-local-ai-development-6036b868b44a?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/6036b868b44a</guid>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Satyam Sahu]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 19:39:58 GMT</pubDate>
            <atom:updated>2026-11-04T18:54:04.338Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[What Is the Best GPU to Rent for LLMs in 2026? (RTX 5090 to H100)]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/what-is-the-best-gpu-to-rent-for-llms-in-2026-rtx-5090-to-h100-b72f7bdb3e10?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/2600/1*YR-MWDeKRZNDzKHe12rV_A.png" width="2752"></a></p><p class="medium-feed-snippet">RTX 5090s rent for under $1 an hour while H100s cost nearly 50% more than in December. Which card fits which open model, RunPod vs Vast.ai&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/what-is-the-best-gpu-to-rent-for-llms-in-2026-rtx-5090-to-h100-b72f7bdb3e10?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/what-is-the-best-gpu-to-rent-for-llms-in-2026-rtx-5090-to-h100-b72f7bdb3e10?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/b72f7bdb3e10</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Anubhav]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:51:05 GMT</pubDate>
            <atom:updated>2026-11-02T09:42:02.150Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[I Tried to Replace Claude With Jev, and Kept Both]]></title>
            <link>https://medium.com/data-science-collective/i-tried-to-replace-claude-with-jev-and-kept-both-f2333bbd2342?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/f2333bbd2342</guid>
            <category><![CDATA[ai-engineering]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Pawel Plaszczak]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:51:02 GMT</pubDate>
            <atom:updated>2026-10-29T19:19:30.119Z</atom:updated>
            <content:encoded><![CDATA[<h4>System One Decision Model + semantic embeddings: better content discovery, less LLM tokens</h4><p>I build tools for writers. Every day, my agentic pipeline scans a few thousand Substack stories and decides which ones deserve attention. Claude, used as the final LLM-as-judge, is costly: up to $3 per batch of 70 stories.</p><p>This is why I looked at Jev: a decision model that cannot produce text, but can make decisions, cheaply: $0.042 per million tokens. Could Jev replace Claude? I tried… and failed. But then I saw another possibility: Jev could make Claude’s job easier. Rather than replacing the expensive model, I used Jev to reduce its workload. After a week, I saw a 54% reduction in costs, and significantly better ranking of content.</p><p>All that is already in production. Read below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lRYBCaWds7SdqV76Gxp20A.png" /><figcaption>My pipeline, before and after learning to use Jev</figcaption></figure><h3>What my pipeline does</h3><p>I’ve been working on analytical tools for authors who publish on Substack and Medium. While Medium promotes posts automatically, relying mostly on its discovery algorithm, Substack is more relationship-driven: writers are expected, and encouraged, to engage with each other. So my tools help authors find other, kindred authors to engage with. You will find some details on the mechanics of my scripts in my older stories: <a href="https://medium.com/@pp_85623/who-wrote-similar-52110fc58950">“Who wrote similar?”</a> and <a href="https://medium.com/data-and-beyond/evaluating-retrieval-without-ground-truth-sentence-transformer-llm-as-judge-and-data-leak-fc3177ce6d6b">“Evaluating Retrieval Without Ground Truth”</a>. Since then, the toolset has grown into a number of agentic pipelines working daily over a large database of blogs.</p><p>The idea is simple. Take the author’s recent story, search all of Substack for stories with an intriguing similarity to it, and propose them to the author, who may then decide to engage with the other authors. One form of engagement is mutual recommendation, beneficial to both if the audiences overlap. Alternatively, commenting under the post also increases the author’s visibility, especially if his or her writings are related.<br> So the script crawls the blogosphere and identifies candidate stories, ranked by cosine similarity and a few other factors.</p><p>But cosine similarity is a “shallow” metric. It captures shared words and themes, but not well the deep, underlying ideas. Here is an example. One of the blogs the pipeline serves is Nomadic Mind, a travel blog. Its series “<a href="https://nomadicmind.substack.com/p/where-europe-still-dances-the-other?r=31fxoh&amp;utm_campaign=post-expanded-share&amp;utm_medium=web">Where Europe Still Dances</a>” explores the experience of the traditional French dance balls. On the other end of the blogosphere, another author wrote “Why Making Music Together Feels So Good” — featuring a Chilean teacher performing Depeche Mode for his class. Both stories are kindred, similar in spirit. Their underlying theme is the joy of participating in music together. But the cosine ranker missed that deeper connection and ranked the similarity low. Well, after all, France is so far from Chile…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k1EueOijC6E6JtM6a9srlQ.jpeg" /><figcaption>from Nomadic Mind’s “Where Europe Still Dances” (by permission)</figcaption></figure><h3>My architecture so far</h3><p>This is why the cosine ranker, a bit dumb, is only a preliminary ranking. My pipeline, until now, was:</p><ol><li><strong>scoring</strong>: cosine similarity of embeddings</li><li><strong>vetting</strong>: a cheap model, Gemini 2.5 Flash or Claude Haiku, filters out unwanted topics</li><li><strong>triage</strong>: Claude, expensive, acting as an LLM-as-judge, making the final decision, often overturning the initial judgement.</li></ol><p>On the test run, 492 articles were scored. The vetting filtered out most of them, and the remaining 61 went to Claude. Claude triaged them, rejected most and eventually selected 18 fair and 9 highly promising candidates for an engagement. The pipeline works well.</p><p>Except it costs $3.02, most of which are Claude tokens.</p><p>My pipeline sits on Databricks, ready to be parallelized as multitenant Spark jobs, run for many clients at once, simultaneously, and regularly. There is nothing blocking it technically, except the prohibitive cost of tokens. Too high — by two orders of magnitude.</p><p>This is when decision models caught my attention.</p><h3>Jev, a model that cannot write</h3><p>While an LLM produces text, a decision model produces, you guessed it, a decision. Conceptually, it’s like a good old classifier: “Look at this picture. Is it a human? Is it a bicycle?” But it’s better: modern decision models are built on transformer architectures, so they accept text as input. Still, the output is not text.</p><p><a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev">Jev</a> comes from TypeSafe AI, a San Francisco lab founded by Diogo Almeida, a former OpenAI researcher. On 15 September 2026 it was released and marketed as “System One Model”, after System 1, the fast thinking in Daniel Kahneman’s famous book <em>Thinking, Fast and Slow</em>. All of a sudden everyone was talking about it. Vercel added Jev to its AI Gateway on the second day, and <a href="https://www.infoq.com/news/2026/10/typesafe-ai-jev-released/">reported</a> that nearly 13% of its paid teams tried it within 24 hours. Welcome to the world where things happen faster than we’ve been used to…</p><p>Also OpenRouter added Jev to its collection. OpenRouter is a public gateway that provides a uniform interface to many models. So I used Jev through OpenRouter.</p><p>How does Jev work in practice? It is simple. In a JSON document, you ask Jev a question, with some context (text). The questions can be of three types:</p><ul><li><strong>Choice:</strong> “Is this email intended for sales, invoicing or complaints?”</li><li><strong>Score:</strong> “Rate this student between 0 and 100”</li><li><strong>Noul:</strong> “Provide probability that a statement is true.”</li></ul><p>Here below is an example request I was sending, shortened. In the first question (choice), I am asking to classify the story. In the second (noul), I am asking if the story is politically biased.</p><pre>{<br>  &quot;model&quot;: &quot;typesafe/jev-1.14&quot;,<br>  &quot;state&quot;: &quot;Title: ...\nSubtitle: ...\nBlog: ...\n\n(the first 3,000 characters of the article)&quot;,<br>  &quot;questions&quot;: {<br>    &quot;topic&quot;: {<br>      &quot;type&quot;: &quot;choice&quot;,<br>      &quot;instructions&quot;: &quot;What is the main topic of the post?&quot;,<br>      &quot;criteria&quot;: {<br>        &quot;travel_places_cultures_abroad&quot;: &quot;Travel, specific places, other cultures, or living abroad&quot;,<br>        &quot;partly&quot;: &quot;Touches on travel or places but mainly about something else&quot;,<br>        &quot;other&quot;: &quot;Not about travel, places, cultures or living abroad&quot;<br>      }<br>    },<br>    &quot;political&quot;: {<br>      &quot;type&quot;: &quot;noul&quot;,<br>      &quot;instructions&quot;: &quot;The post takes one political side as unquestioned fact, uses derogatory labels for political opponents, or is written for one political camp. A post that discusses politics, policy or political events analytically does not count.&quot;<br>    }<br>  }<br>}</pre><p>And here is the answer:</p><pre>{<br>  &quot;topic&quot;: { &quot;choice&quot;: &quot;partly&quot;,<br>             &quot;probabilities&quot;: { &quot;travel_places_cultures_abroad&quot;: 0.02, &quot;partly&quot;: 0.90, &quot;other&quot;: 0.08 } },<br>  &quot;political&quot;: { &quot;noul&quot;: 0.92 },<br>  &quot;usage&quot;: { &quot;input_tokens&quot;: 1249, &quot;cost&quot;: 0.000052 }<br>}</pre><p>This approach has tremendous advantages. First, it is fast and cheap: the call above cost five thousandths of a cent. Secondly, it radically simplifies the architecture. Many guardrails become unnecessary: since text is not part of the output, it cannot be toxic, harmful or adversarial. Also, prompt hacking has a smaller effect. An attacker can, at most, influence or bias the output categories.</p><p>TypeSafe even claims that Jev “can’t hallucinate”. I don’t know about that. Jev’s architecture has not been published. As Armin Ronacher put it, the design “delegates the hallucination problem a little bit to the user”.</p><p>I realized quickly Jev could solve two problems at once: reduce cost and simplify architecture. Could it also improve the results?</p><h3>Jev judged bias better than the LLM</h3><p>Now, I need to explain why I need the vetting phase.</p><p>In the process of identifying kindred stories, I learned that embedding similarity paints only part of the picture. Especially in the American blogosphere, there are two particularly nasty types of texts that pass unnoticed through the embedding sieve: politically biased and religiously biased ones. They use similar wording and similar themes, yet they are far from “kindred”.</p><p>Take France. Nomadic Mind has a story called “<a href="https://nomadicmind.substack.com/p/paris-propriety-and-urban-rebellion?r=31fxoh&amp;utm_campaign=post-expanded-share&amp;utm_medium=web">Paris: Propriety and Urban Rebellion</a>”, in which its author boards a train to a clandestine anarchist meeting on a Parisian suburb. Its tone is playful, tongue-in-cheek. The embeddings ranker did not get the humor and rated the story closest to “What’s Really Going on in France?”, written by Nick Griffin, the former leader of the British National Party, blaming the recent riots on the far left. One story is sarcastic, the other is political, but they were rated alike for similar vocabulary. The similarity ranking was initially driven by 384-dimensional embeddings of MiniLM over Chroma. Increasing the dimensionality to 768 (replacing MiniLM with all-mpnet-base-v2) did not help either. Possibly, scaling further to deeper models with 3K dimensions would change something, but that is outside my options.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*W1TT146NsIMw53EI.jpeg" /><figcaption>From Nomadic Mind’s Paris story (by permission)</figcaption></figure><p>Religion works the same way. Nomadic Mind has a story from West Africa, where its author listens to people who believe in the devil of the forest. The story landed close to various obscure conspiracy theories explaining how the magical operations of freemasonry hidden lodges lead to control of the Global Elite. Both stories speak of the devils, but the authors are very unlikely to want to engage mutually: the first author looks at the phenomenon as folklore, the other one honestly believes in the paranormal and takes religious stance.</p><p>This is where the vetting comes in. In the second phase of the pipeline, I used a cheap model, Gemini or Haiku to filter out various unwanted features of the candidate stories. Among them, importantly, I wanted the politically and religiously biased texts out.</p><p>Could the vetting model be replaced by Jev?</p><p>I sent the same 492 articles to Jev asking him to do the same job. To my disappointment, it flagged only 16 articles as political, and 5 as religious (as opposed to Gemini, which filtered out over 100 for bias). I thought Jev was wrong, so I reviewed what was happening, and it turned out that the initial sieve had been wrong! Gemini (or Claude Haiku) classified stories as “political” when they just spoke about politics. Jev was sharper for the price: it distinguished political topic from political bias. It wasn’t perfect, but it was better: when I manually ranked stories for political bias, it agreed with me 21 out of 24 times.</p><p>All in all, the effect was bizarre: even though Jev was generally more effective, we ended up filtering fewer stories, not more. Not good! Our goal was to filter early as much as possible, so that the LLM phase would be cheaper. So I looked at the more interesting part: could Jev replace the LLM in the third phase (triage)?</p><h3>Jev for triage</h3><p>Triage (phase 3) was the most expensive phase. Cutting the cost of this phase would have the highest effect.</p><p>I first tried to bluntly replace the LLM-as-Judge by Jev. This failed. Claude understood the context and nuance better. On the sample of 52 stories a human ranked top (most aligned with target), Claude agreed with 40, while Jev agreed with 17. But this gave me an idea. Seventeen, which comes almost free, is already something. Could I use Jev to better rank stories, so Claude could just read the top of the list and burn fewer tokens?</p><p>Here, I used Jev’s Noul-type query. The goal was to understand whether a candidate article had an interesting angle relative to the author’s story. So I asked Jev a simple question: “Would the author of story A be interested in commenting on article B?” A Noul yes/no answer carried probability, which I later interpreted as score. It could be more natural to use Jev’s native Score-type query, but somehow I was unsuccessful in it. The requests were blocked, probably at OpenRouter’s API level. Whatever. Probability of True was good enough.</p><p>Jev’s scores were promising, but not great. It ranked somewhat better than embeddings, but not well enough to replace Claude. Initially, the numbers looked poor, confirmed by this visualization. Both rankers correlated poorly with each other, and also with human judgement. If they did, the blue dots on the diagram below should sit in the top right corner, where they are not.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xQyxZcqKhpBq5q1IRlEpbg.png" /><figcaption>How Jev and Embeddings ranked the stories which were best (by an impartial human ranker)</figcaption></figure><h3>Two rankers, two kinds of kinship</h3><p>But a seasoned data scientist learns to distrust numbers. I dived deeper and saw something intriguing: the two rankers ranked different kinds of articles high.</p><p>The embeddings found kinship in vocabulary, but this included location and subject. “The women banished as witches in West Africa” scored 0.80 against <a href="https://nomadicmind.substack.com/p/how-the-witch-saved-aishas-baby-47a">“How the Witch Saved Aisha’s Baby”</a>, Nomadic Mind’s story also set in West Africa. Jev instead found kinship in ideas. He correctly decided that an Irish music session in a Dublin pub was related to a sagra (village festival) in Puglia, southern Italy.</p><p>Once I understood this, the solution was straightforward.</p><p>I tweaked some things, and hacked an algorithm that blended both rankers (score fusion) — copying the idea of machine learning models ensemble: “can a set of weak classifiers create, as an ensemble, a powerful classifier?” Yes it can:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*murK7ryb4nQ-pmS7upUEdg.png" /><figcaption>Blended ranker is better than both Jev and Embeddings alone</figcaption></figure><h3>The new architecture</h3><p>So we have built the final architecture:</p><ol><li><strong>Discovery.</strong> The script finds candidate stories, as before</li><li><strong>Vetting.</strong> Jev filters out biased posts and other unwanted categories</li><li><strong>Ranker.</strong> The embeddings and Jev, blended, tell Claude what to read first</li><li><strong>LLM-as-judge, only on the top of the list.</strong> Claude is still there, but he reads only very few articles, so he is much cheaper.</li></ol><p>Performance? Twice as good as before. At the top of the list, Claude used to find 11 of the 52 best stories. Now he finds 24.</p><p>Cost? $3.02 dropped to $1.39. Claude no longer loads a list of 60 to 90 articles into its context, but reads only the very best candidates and stops. This is still too expensive, but it defines an avenue for further tuning and cost saving. I have a number of ideas how. Hint: Jev’s part of today’s $1.39 cost is $0.02 (yes, two cents)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fU070qZPkq6yTwsj6IIGtg.png" /><figcaption>total cost of a run: before, after, and Jev alone</figcaption></figure><p>Architecture? It is not only simplified (less worry on LLM guardrails), but also it became ready to scale over Databricks, enabling multitenancy over massively parallel runs that scale not only technically, but also financially.</p><h3>Are decision models a game changer?</h3><p>Yes. For bounded questions, I believe they are. And, as this project shows, many problems can be redefined as such.</p><p>Jev could not replace Claude as the judge. But on bias, it was better than the LLM, and as a ranker it let Claude read much less. Per article, it was 130 to 180 times cheaper. The cheap layer already costs almost nothing: what remains is Claude’s reading. Jev writes no text, so fewer things can go wrong and output processing is simpler, although its answers can still be wrong. This fits a pattern that is becoming common in AI engineering: a cascade, or model routing, where cheap “System One” models make the fast decisions, and an expensive LLM is called only sparingly.</p><p>Jev is not alone. Since it launched, several alternatives appeared. For instance, Remigiusz Kinas published <a href="https://huggingface.co/Remek/basal-1.0-1.5B">Basal</a>, a small model (1.5 billion parameters), fine-tuned from the Polish model Bielik, and free under the Apache 2.0 license. In its author’s benchmark, it beats Jev. Then came Cloudflare’s Clef, OpenAI’s Decisions API, and Fastino’s GLiNER2.5-Decide. I tested only Jev, not because I found it superior, but because I wanted to test the concept: decision model instead of LLM-as-judge.</p><p>Decision models such as these, together with sentence-transformers and a vector database such as my Chroma, are the base for the cheap layer. Then, the LLM-as-judge reads only what is worth reading.</p><p>Thank you for reading. Do share your take.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*l-YAN2CE0LTOgWYA.jpeg" /><figcaption>Taking a break from tuning Jev, I walked the autumn woods near my home and took this picture. Isn’t autumn pretty?</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2333bbd2342" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/i-tried-to-replace-claude-with-jev-and-kept-both-f2333bbd2342">I Tried to Replace Claude With Jev, and Kept Both</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Multi-Agentic framework scored 100% on Accuracy Task Completion — It Was Not the Most Important…]]></title>
            <link>https://medium.com/data-science-collective/my-multi-agentic-framework-scored-100-on-accuracy-task-completion-it-was-not-the-most-important-c67214f9310b?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/c67214f9310b</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[business]]></category>
            <dc:creator><![CDATA[Rolando L Bouloy Fascitelli]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:50:36 GMT</pubDate>
            <atom:updated>2026-10-31T00:49:41.783Z</atom:updated>
            <content:encoded><![CDATA[<h3>My Multi-Agentic framework scored 100% on Accuracy Task Completion — It Was Not the Most Important Number</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YIG3XPTOkqfOlmKdielIlA.png" /><figcaption>AI generated image</figcaption></figure><p><em>How a deterministic shell, a small LangGraph agent core, and two separate “judges” exposed what a perfect answer-key score could not.</em></p><p>In this article, I’ll share the approach I took during the project organized by Great Learning in collaboration with the University of Texas at Austin and the McCombs School of Business to solve a very common supply chain problem: <strong>the last-mile logistics delivery</strong>.</p><p>A practical case study in multi-agentic-AI orchestration, progressive and parallel task execution in ambiguous contexts, LLM as a judge, and rigorous evaluation methodology.</p><h3>1. The Problem</h3><p>In last-mile logistics, roughly <strong>10%</strong> of all shipments encounter delivery exceptions — failed attempts, address mismatches, damaged packages, refused deliveries, and weather delays. Each failed delivery costs in direct reattempt expenses, while the downstream impact is far steeper: <em>consumers stop shopping with a retailer after just 2–3 failed deliveries, and supply chain disruptions cost companies millions per year.</em></p><p>Despite these costs, most organizations still handle exception triage and resolution manually. Operations staff read through status logs and messy driver notes, cross-reference customer profiles and internal playbooks, decide on a resolution action, and draft customer notifications — all under time pressure. Teams rely on support tickets, tribal knowledge, and rigid rule engines that cannot handle the nuanced, multi-factor judgment calls that real exceptions demand. A VIP customer with a perishable package and a broken gate code on a second attempt requires a fundamentally different resolution than a standard customer’s first failed attempt on a non-perishable item — but manual processes treat them with the same slow, inconsistent workflow.</p><p>Key <strong>KPIs</strong> affected include:</p><blockquote>- Exception resolution time (constrained by manual triage throughput)<br>- Escalation accuracy (missed or unnecessary supervisor escalations)<br>- Customer communication quality and personalization<br>- Cost per exception (reattempt costs, spoilage write-offs, unnecessary truck rolls)<br>- Customer retention and lifetime value</blockquote><p>If unaddressed, logistics providers face rising exception-handling costs, inconsistent service quality, preventable customer churn, and loss of competitive ground to players who are already investing in AI-powered exception detection and response automation.</p><p>By applying an AI-powered multi-agent system that ingests delivery logs, cross-references customer profiles and locker availability, retrieves resolution policy from an operational playbook, and generates validated decisions with personalized customer communications, <strong>logistics providers</strong> can automate the full exception-handling pipeline from detection through resolution, with built-in quality validation and supervisor escalation where policy requires it.</p><h3>2. Technology Stack</h3><p>The project was developed in VS Code as a modular Python package (src/), using OpenAI APIs, <a href="https://github.com/RoloMagnus/ai-powered-shipment-disruption-router">GitHub</a> for version control, LangGraph for agent orchestration, LangChain for LLM integration, Pydantic for structured output contracts, <a href="https://arxiv.org/html/2410.09871v1">PyPDF</a>Loader for PDF ingestion, SQLite for data storage, and LangSmith for tracing agent behavior and monitoring drift. This architecture was a deliberate choice: it provided a cohesive foundation for development and helped keep agent interactions grounded, observable, and controlled.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/785/1*wonJWesm3E5xZA-Z3s-zhw.png" /><figcaption>Breakdown of the tech-stack architecture used in the project.</figcaption></figure><p>Splitting <em>generation </em>and <em>review </em>across two models is a design choice, not a measured optimum. There was no cost or model-ablation experiment.</p><p>The two models are created once and reused:</p><pre>from langchain_openai import ChatOpenAI<br> <br># Cheaper, faster: Resolution and Communication agents<br>gen_llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;, temperature=0)<br> <br># Stronger reasoning: both Critic nodes and the judges<br>eval_llm = ChatOpenAI(model=&quot;gpt-4o&quot;, temperature=0)</pre><h3>3. Deterministic Logic vs. Probabilistic Reasoning: Knowing When to Use Each</h3><h4>3.1 A Mixed Challenge: delivery exceptions need judgement and deterministic rules</h4><p>A delivery exception is any event that needs a decision: a failed attempt, an address that cannot be found, a damaged parcel, a refused delivery, a weather delay.</p><p>For each one, an operator has to decide:</p><blockquote>- <strong>Is it really an exception?</strong> Routine scans and successful deliveries are noise.<br>-<strong>What is the resolution?</strong> One of RESCHEDULE, REROUTE_TO_LOCKER, REPLACE, or RETURN_TO_SENDER.<br>-<strong>Does a supervisor need to be involved?</strong><br>-<strong>What should the customer be told, and in what tone?</strong></blockquote><p>The inputs were three types of data:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CekcpDoFLJNAUJHOxHk6IA.png" /><figcaption>Main sources of data</figcaption></figure><p>A separate ground-truth file holds the reference answer for each shipment: whether it is an exception, the expected resolution, the expected tone, and whether it should escalate. <strong>It is an evaluation reference only and is never shown to any agent.</strong></p><p>This prototype evaluated only 10 shipments that are small but deliberately varied:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M07lM8ApT_-CV8cqgb3lqg.png" /><figcaption>Ten shipments tested</figcaption></figure><h4>3.2 The Approach: use an LLM only where there is no single right answer</h4><p>The design principle is stated in the notebook’s plan: <strong>be deterministic where there is one right answer, and use retrieval and LLMs where judgement over policy text is needed.</strong></p><p>That produces four layers:</p><blockquote>1. <strong>A rule-based preprocessor.</strong> Deduplication, consolidation, prompt-injection scanning, and a noise filter. No LLM.</blockquote><blockquote>2. <strong>Deterministic tools.</strong> Locker eligibility and an escalation rule engine. No LLM.</blockquote><blockquote>3. <strong>A small multi-agent core.</strong> A Resolution agent and a Communication agent, each reviewed by a stronger Critic.</blockquote><blockquote>4. <strong>An evaluation layer kept separate from the agents.</strong> Answer-key checks, a coherence judge, and a playbook-adherence judge.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eUJ1fM8JKyHrdusDWQF96w.png" /><figcaption><em>Figure 1. The implemented LangGraph topology. Every worker returns control to a rule-based orchestrator.</em></figcaption></figure><h4>3.3 The Deterministic Layer: where correctness is cheap</h4><p>If a decision has a single, explicitly defined outcome, it belongs in code — not in an LLM prompt. This principle was fundamental to the robustness of the pipeline. The key lies in establishing a clear distinction between qualitative inference and deterministic operations.</p><p>Interpreting ambiguous situations, extracting meaning from unstructured information, and reasoning through conflicting evidence are sophisticated tasks that may justify the flexibility — and computational expense — of an LLM.</p><p>Conversely, arithmetic calculations, threshold evaluations, conditional logic, score-based outputs, and rule-based validations are better handled by conventional code: faster, cheaper, more predictable, and easier to audit.</p><p>The architectural mistake is treating every decision as a reasoning problem when many are simply execution problems. Too many organizations blur this distinction, introducing unnecessary complexity, latency, and cost into their agentic workflows — and then struggle to understand why those systems fail to deliver consistent, scalable performance.</p><p>And yes, now you know why your evaluation score keeps changing every time you run the same pipeline.</p><p>Back to our case, the locker eligibility is arithmetic, not opinion. For illustration purposes, this simple logical function can solve the headache.</p><pre>@tool<br>def check_locker_availability(zip_code: str, package_size: str) -&gt; list[dict]:<br>    &quot;&quot;&quot;Find compatible lockers in the same zip code, with eligibility reasoning.<br>    (abridged)&quot;&quot;&quot;<br>    size_hierarchy = {&quot;SMALL&quot;: 1, &quot;MEDIUM&quot;: 2, &quot;LARGE&quot;: 3}<br>    pkg_level = size_hierarchy.get(package_size, 0)<br>    # ... query lockers in this zip code, then for each one:<br>    for row in rows:<br>        locker = dict(row)<br>        locker_max = size_hierarchy.get(locker[&quot;max_package_size&quot;], 0)<br>        if locker_max &lt; pkg_level:<br>            locker[&quot;eligible&quot;], locker[&quot;reason&quot;] = False, &quot;Locker max size too small&quot;<br>        elif locker[&quot;capacity_status&quot;] == &quot;FULL&quot;:<br>            locker[&quot;eligible&quot;], locker[&quot;reason&quot;] = False, &quot;Locker is FULL&quot;<br>        elif locker[&quot;capacity_status&quot;] == &quot;LIMITED&quot; and package_size != &quot;SMALL&quot;:<br>            locker[&quot;eligible&quot;], locker[&quot;reason&quot;] = False, &quot;Locker is LIMITED - SMALL only&quot;<br>        else:<br>            locker[&quot;eligible&quot;], locker[&quot;reason&quot;] = True, &quot;Compatible&quot;</pre><p>Following the same line of thought, escalation is a rule engine, split into AUTOMATIC and DISCRETIONARY</p><pre>@tool<br>def check_escalation_rules(customer_tier, exceptions_last_90d, attempt_number,<br>                           package_type, status_code, status_description) -&gt; dict:<br>    &quot;&quot;&quot;Deterministic escalation rule engine (abridged).&quot;&quot;&quot;<br>    triggers = []<br>    if attempt_number &gt;= 3:<br>        triggers.append(&quot;AUTOMATIC: 3rd failed delivery attempt&quot;)<br>    if customer_tier == &quot;VIP&quot; and exceptions_last_90d &gt;= 3:<br>        triggers.append(&quot;AUTOMATIC: VIP customer with 3+ exceptions in 90d&quot;)<br>    if status_code == &quot;DAMAGED&quot; and package_type == &quot;PERISHABLE&quot;:<br>        triggers.append(&quot;AUTOMATIC: Damaged perishable package&quot;)<br>    # Weather delay over 4 hours for a perishable: parsed from free text like &quot;5hr&quot;<br>    # Discretionary: a Standard customer with more than 5 exceptions in 90 days<br>    if customer_tier == &quot;STANDARD&quot; and exceptions_last_90d &gt; 5:<br>        triggers.append(&quot;DISCRETIONARY: Standard customer with &gt;5 exceptions in 90d&quot;)</pre><p>The engine also derives a <strong>triage priority</strong> (AUTOMATIC → <strong>P2-HIGH</strong>, discretionary only → <strong>P3-ELEVATED</strong>, none → <strong>P4-ROUTINE</strong>, with a perishable moved up one level to <strong>P1-CRITICAL</strong>). Priority only orders the queue; it never changes the resolution or the escalation decision.</p><p>Run over the ten shipments with no LLM calls, it produced this queue:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7gjmZIXLsq5IjbB5dCT2yg.png" /><figcaption>Deterministic and Logical Triage Priority Output — With no LLMs calls.</figcaption></figure><p><strong>Guardrails run BEFORE any LLM call.</strong> The preprocessor scans driver notes (and later the retrieved playbook chunks) for prompt-injection keywords, and filters routine status codes with no anomaly words as noise:</p><pre>def check_noise_override(consolidated: dict) -&gt; bool:<br>    &quot;&quot;&quot;Flag routine status codes with no anomaly indicators.&quot;&quot;&quot;<br>    routine_codes = {&quot;DELIVERED&quot;, &quot;IN_TRANSIT&quot;, &quot;OUT_FOR_DELIVERY&quot;, &quot;SCANNED&quot;}<br>    if consolidated[&quot;status_code&quot;] not in routine_codes:<br>        return False<br>    anomaly_indicators = [&quot;damage&quot;, &quot;wrong&quot;, &quot;suspicious&quot;, &quot;overdue&quot;, &quot;missing&quot;,<br>                          &quot;lost&quot;, &quot;stolen&quot;, &quot;leak&quot;, &quot;delay&quot;, &quot;late&quot;, &quot;fraud&quot;]  # abridged<br>    desc = consolidated[&quot;status_description&quot;].lower()<br>    return not any(word in desc for word in anomaly_indicators)</pre><p>Both noise shipments (SHP-001, SHP-010) closed in about zero seconds with no agent calls. The injection guardrail never blocked a shipment in this dataset (all ten showed Guardrail: CLEAR), so its blocking path was not exercised by these cases.</p><h4>3.4 The Multi-Agent Core: typed outputs and a rule-based orchestrator</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6unthGrPlNWW-Qd-ekQd0A.png" /><figcaption>Image 1 — The Multi-Agentic Workflow Architecture</figcaption></figure><h4>3.4.1 Schema-enforced decisions</h4><p>The Resolution agent must return a typed object. A model validator rejects internally inconsistent answers:</p><pre>class ResolutionOutput(BaseModel):<br>    is_exception: Literal[&quot;YES&quot;, &quot;NO&quot;]<br>    resolution: Literal[&quot;RESCHEDULE&quot;, &quot;REROUTE_TO_LOCKER&quot;, &quot;REPLACE&quot;,<br>                        &quot;RETURN_TO_SENDER&quot;, &quot;N/A&quot;]<br>    rationale: str<br> <br>    @model_validator(mode=&quot;after&quot;)<br>    def validate_consistency(self):<br>        if self.is_exception == &quot;YES&quot; and self.resolution == &quot;N/A&quot;:<br>            raise ValueError(&quot;resolution cannot be N/A when is_exception is YES&quot;)<br>        if self.is_exception == &quot;NO&quot; and self.resolution != &quot;N/A&quot;:<br>            raise ValueError(&quot;resolution must be N/A when is_exception is NO&quot;)<br>        return self</pre><h4>3.4.2 The orchestrator is not an LLM</h4><p>A LangGraph conditional edge reads next_agent from state, and every worker returns to the orchestrator:</p><pre>workflow = StateGraph(UnifiedAgentState)<br>workflow.add_node(&quot;preprocessor&quot;, preprocessor_node)<br>workflow.add_node(&quot;orchestrator&quot;, orchestrator_node)<br>workflow.add_node(&quot;resolution_agent&quot;, resolution_agent_node)<br>workflow.add_node(&quot;critic_resolution&quot;, critic_resolution_node)<br>workflow.add_node(&quot;communication_agent&quot;, communication_agent_node)<br>workflow.add_node(&quot;critic_communication&quot;, critic_communication_node)<br>workflow.add_node(&quot;finalize&quot;, finalize_node)<br> <br>workflow.add_edge(&quot;preprocessor&quot;, &quot;orchestrator&quot;)<br>ROUTES = [&quot;resolution_agent&quot;, &quot;critic_resolution&quot;, &quot;communication_agent&quot;,<br>          &quot;critic_communication&quot;, &quot;finalize&quot;]<br>workflow.add_conditional_edges(&quot;orchestrator&quot;, lambda s: s[&quot;next_agent&quot;],<br>                               {r: r for r in ROUTES})<br>for worker in [&quot;resolution_agent&quot;, &quot;critic_resolution&quot;,<br>               &quot;communication_agent&quot;, &quot;critic_communication&quot;]:<br>    workflow.add_edge(worker, &quot;orchestrator&quot;)<br>workflow.add_edge(&quot;finalize&quot;, END)</pre><p>Its routing order is fixed and documented: guardrail, noise, resolution, critic, capped revision loop, <strong>enforce AUTOMATIC escalation triggers</strong>, communication, communication critic, finalize. Because the rule engine is authoritative, an LLM cannot talk its way out of a mandatory escalation.</p><h4>3.4.3 The Critic reports playbook alignment on every review</h4><p>The Resolution Critic returns a typed verdict that includes how the decision compares with the retrieved playbook text:</p><pre>class CriticResolutionOutput(BaseModel):<br>    decision: Literal[&quot;ACCEPT&quot;, &quot;ESCALATE&quot;, &quot;REVISE&quot;]<br>    playbook_alignment: Literal[&quot;ALIGNED&quot;, &quot;ALIGNED_WITH_NOTE&quot;,<br>                                &quot;CONFLICT&quot;, &quot;NO_RULE_FOUND&quot;]<br>    playbook_rule_cited: str   # &quot;[Page N] ...&quot; or empty<br>    alignment_note: str<br>    rationale: str</pre><p>The Critic is told it may only ask for one of the four valid resolution values, and that a perishable must never be handled as routine.</p><h3>4. Evaluation methodology</h3><p>Every shipment is scored on <strong>five metrics</strong>, plus a sixth added in version 3. We will explain the 3 main iterations performed to fine-tune the model later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KIHDhcN4f-Y3mdspekRXJA.png" /><figcaption>All 6 metrics used to scored the model performance.</figcaption></figure><p>The new check (not in the original plan) is the interesting one. Its verdicts are ALIGNED, ALIGNED_WITH_NOTE, CONFLICT, and NO_RULE_FOUND.</p><pre>def compute_playbook_adherence(pred: dict) -&gt; dict:<br>    &quot;&quot;&quot;Check the final decision against the retrieved playbook text.<br>    The ground truth is never passed in.&quot;&quot;&quot;<br>    audit = {<br>        &quot;consolidated_event&quot;: pred.get(&quot;consolidated_event&quot;, {}),<br>        &quot;customer_tier&quot;: pred.get(&quot;customer_profile&quot;, {}).get(&quot;tier&quot;),<br>        &quot;locker_availability&quot;: pred.get(&quot;locker_availability&quot;, []),<br>        &quot;escalation_signals&quot;: pred.get(&quot;escalation_signals&quot;, {}),<br>        &quot;retrieved_playbook_sections&quot;: [<br>            f&quot;[Page {c[&#39;page&#39;]}] {c[&#39;content&#39;]}&quot; for c in pred.get(&quot;playbook_context&quot;, [])<br>        ],<br>        &quot;final_resolution&quot;: pred[&quot;resolution_output&quot;].get(&quot;resolution&quot;),<br>        &quot;final_rationale&quot;: pred[&quot;resolution_output&quot;].get(&quot;rationale&quot;),<br>        &quot;escalated&quot;: pred.get(&quot;escalated&quot;),<br>    }<br>    # eval_llm returns {&quot;verdict&quot;, &quot;rule&quot;, &quot;justification&quot;}</pre><p>Aggregates then report two new rows: the <strong>adherence rate</strong> (aligned, with or without a note) and the count of shipments that <strong>pass the answer key but conflict with the playbook</strong>.</p><h3>5. Three iterations, and what each one taught</h3><p>No weights changed. Three versions of the notebook changed schemas, prompts, and how results were scored. The headline metrics from each executed run:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zUt34JnQ9VvQaxeatTsJTw.png" /><figcaption>A comparative table showing all 3 iterations and their results.</figcaption></figure><h4>Iteration 1: The agent invented a fifth action</h4><p>In v1 the output schema accepted a fifth resolution, HOLD_AT_DEPOT, because the playbook’s third-attempt and address-not-found paths describe holding a parcel at the depot. The agent used it for <strong>SHP-003</strong> and <strong>SHP-005</strong>. Against the original answer key, both failed: task completion was 8/10.</p><p>The v1 notebook also scored against a second, <em>playbook-reconciled</em> answer key, in which those two shipments were relabeled HOLD_AT_DEPOT. Against that key v1 scored 10/10. That is a flattering number, and it is the pattern this article warns about: <strong>when you reconcile the answer key to the system’s behaviour, the system always passes.</strong></p><h4>Iteration 2: Tighten the contract, keep the original labels</h4><p>v2 made two changes that are visible in the code diff:</p><p>· <strong>HOLD_AT_DEPOT was removed from the resolution schema</strong>, leaving the four valid actions plus N/A.</p><p>· <strong>The reconciled-label column was removed</strong>, so scoring returned to the original answer key. The system prompts for the Resolution agent, Communication agent, and both Critics were rewritten as new cells.</p><p>Task completion rose to 10/10 against the original labels. But escalation accuracy <strong>fell to 88% (7/8)</strong>, because of one shipment.</p><p><strong>What happened on SHP-003 (address not found):</strong> the Resolution agent proposed RESCHEDULE, the right answer. The Critic, reading a playbook that says to <em>hold at the depot and verify the address</em>, sent it back to revise, twice. On the retry the model returned N/A for an exception, which the Pydantic validator rejected as invalid. After three failed attempts the pipeline hit its safe fallback: default to RESCHEDULE with <strong>forced escalation</strong>. The final resolution was correct, but the escalation was wrong, the run took <strong>18.91 s</strong>, and it used two revisions.</p><p>That is a good failure. The guardrail worked as designed, the audit trail explained everything, and a human would have been asked to look at a case the answer key says needs no escalation. It was a prompt-contract problem, not a model problem.</p><h4>Iteration 3: Reconcile in the prompts, and make conflicts visible</h4><p>v3 changed prompts and the evaluation layer only. Architecture, tools, data, and labels were unchanged. There were two kinds of change.</p><p><strong>A. Reconcile the playbook and the schema in the prompts.</strong> The Resolution agent and the Critic are told that “<strong><em>hold at the depot</em></strong>” and “<strong><em>contact the customer</em></strong>” are the <em>first part of</em> RESCHEDULE, not a separate action, and that the Critic may only request one of the four valid values:</p><pre>- ADDRESS_ISSUE (building or street not found, missing unit): RESCHEDULE.<br>  The playbook&#39;s hold-at-depot and contact-the-customer steps are the first part<br>  of this resolution, not a different one: say in the rationale that the package<br>  waits at the depot until the customer verifies or corrects the address, then<br>  is redelivered.<br> <br>The resolution field accepts only RESCHEDULE, REROUTE_TO_LOCKER, REPLACE,<br>RETURN_TO_SENDER or N/A. Playbook wording such as &quot;hold at the depot&quot; describes<br>an interim step: write it in the rationale.</pre><p>The retry logic was also changed so a failed attempt is told <em>what</em> was invalid, instead of repeating an identical prompt:</p><pre># abridged: last_error starts as &quot;&quot; before the loop<br>for attempt in range(max_retries):<br>    # After a validation failure, tell the model what was wrong<br>    retry_note = (<br>        f&quot;\n\nYOUR PREVIOUS OUTPUT WAS INVALID ({last_error}). Return exactly one of &quot;<br>        f&quot;RESCHEDULE, REROUTE_TO_LOCKER, REPLACE or RETURN_TO_SENDER for an exception, &quot;<br>        f&quot;and put any interim step in the rationale.&quot;<br>        if last_error else &quot;&quot;<br>    )<br>    try:<br>        result = structured_llm.invoke([<br>            SystemMessage(content=system_prompt + retry_note),<br>            HumanMessage(content=user_content),<br>        ])<br>        break<br>    except Exception as e:<br>        last_error = str(e)[:150]</pre><p><strong>B. Make conflicts visible.</strong> The Critic’s playbook_alignment and the new playbook-adherence judge were added, and the aggregate table gained the two new rows.</p><p><strong>Results.</strong> SHP-003 was accepted on the first review, not escalated, and took <strong>7.89 s instead of 18.91 s</strong>. Escalation accuracy recovered to 8/8, mean latency fell from 7.76 s to 7.19 s, and <strong>zero revisions</strong> were needed across all ten shipments. The labels, the data, and the answer key were unchanged, so this is a prompt effect.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*koBY0za5oHcXOO1g_LBsxg.png" /><figcaption>Figure 2. Version 3 recorded results. Blue bars are checked against the answer key. The orange bar is checked against the playbook text alone.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zCNWD4V4OYT-gtgm5NXTjQ.png" /><figcaption><em>Figure 3. The SHP-003 correction. Escalation accuracy 88% → 100%; mean latency 7.76 s → 7.19 s; max 18.91 s → 14.82 s.</em></figcaption></figure><h3>6. The AHA moment — What the evaluation can see now: the answer key versus the playbook.</h3><p>Here is the finding that shaped the rest of this article.</p><p>Every answer-key metric is green for <strong>SHP-002</strong> and <strong>SHP-005</strong>. But the playbook judge, which never sees the answer key, flagged both as <strong>CONFLICT.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UMC3_IjI6PsPYovpndo2kQ.png" /><figcaption><em>Figure 4. Eight exception shipments. All answer-key columns pass. The two playbook checks do not always agree.</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/787/1*e858v1OmeplqHM5Hs3tw6Q.png" /><figcaption>Driver notes vs Policy — Judgement discrepancies and how to handle them gracefully.</figcaption></figure><p>The rollup: <strong>playbook adherence 62% (5 of 8 exception shipments aligned, with or without a note)</strong>, and <strong>2 shipments pass the answer key but conflict with the playbook (SHP-002 and SHP-005)</strong>.</p><p>Two checks that agree on SHP-005 and SHP-003 but disagree on SHP-002 and SHP-008 are not two verdicts. They are a <strong>signal for a human reviewer</strong>. Neither the Critic nor the Judge is ground truth, and the judge sees only retrieved chunks, so NO_RULE_FOUND can mean retrieval missed a clause rather than that no rule exists.</p><h3>7. Lessons Learned</h3><p>Anthropic Research recently highlighted why agent evaluations are essential — not merely as instruments for measuring quality and performance, but also as powerful mechanisms for discovery, continuous improvement, and challenging our own operational assumptions.</p><p>In their article, <a href="https://www.anthropic.com/research/demystifying-evals-for-ai-agents"><strong>Demystifying evals for AI agents</strong></a>, the researchers illustrate this point with a particularly interesting example:</p><blockquote>“Agent evaluations are even more complex. Agents use tools across many turns, modifying state in the environment and adapting as they go — which means mistakes can propagate and compound. Frontier models can also find creative solutions that surpass the limits of static evals. For instance, Opus 4.5 solved a <a href="https://github.com/sierra-research/tau2-bench">τ2-bench</a> problem about booking a flight by <a href="https://www.anthropic.com/news/claude-opus-4-5">discovering </a>a loophole in the policy. It ‘failed’ the evaluation as written, but actually came up with a better solution for the user.”</blockquote><p><strong>Sound familiar? That’s precisely the kind of problem our case study uncovered.</strong></p><p>In our shipment exception workflow, the agents produced resolutions that initially failed against the predefined ground-truth labels. Yet, when we examined their decisions against the actual exception resolution playbook, we discovered something more interesting: <strong>the agents had correctly prioritized the policy’s immediate operational requirements over the expected outcomes encoded in the evaluation dataset.</strong></p><p>The issue wasn’t necessarily that the agents were wrong — or even that the policy itself was flawed. Rather, the evaluation exposed a disconnect between <strong>what the ground truth expected, what the policy required, and when a particular action was permissible</strong>.</p><p>And that’s where evaluations become far more valuable than a simple <strong>PASS </strong>or <strong>FAIL</strong>.</p><p>These discrepancies provide an opportunity to refine ground-truth definitions, clarify policy prerequisites, strengthen enforcement mechanisms, and improve operational coverage for those pesky, persistent edge cases that rarely reveal themselves under ideal conditions.</p><p>There’s an interesting parallel with the legal world. Legislators cannot anticipate every factual scenario, interpretation, or unintended consequence that may emerge when a law is applied in practice. Legal frameworks therefore evolve through judicial interpretation, amendments, and the resolution of previously unforeseen cases.</p><p>Corporate policies are no different. They are written under assumptions about how the real world operates, and those assumptions are not always complete.</p><p><strong>The difference today is that well-designed agentic systems, combined with rigorous evaluations, can help organizations uncover those hidden inconsistencies systematically — before they propagate into </strong><a href="https://aws.amazon.com/blogs/machine-learning/evaluating-ai-agents-real-world-lessons-from-building-agentic-systems-at-amazon/"><strong>operational failures</strong></a><strong>.</strong></p><p>The real value of an agentic evaluation framework, therefore, isn’t just determining whether an AI agent made the right decision. It’s also helping us discover whether <strong>we defined the right decision correctly in the first place.</strong></p><h3>8. What worked, and what to treat with caution.</h3><h4><strong>What worked</strong></h4><blockquote>· <strong>The deterministic layer carried most of the safety.</strong> Deduplication, the noise guardrail, locker eligibility, and the escalation rule engine closed both noise cases and <strong>forced five of the six correct escalations without relying on an LLM</strong>. The sixth, SHP-008, rests on a discretionary trigger, so the Critic had to choose to escalate. It did.</blockquote><blockquote>· <strong>Perishables were never held or sent to a locker.</strong> The triage queue put SHP-007 and SHP-009 first.</blockquote><blockquote>· <strong>No cell raised an error, and the injection guardrail never blocked a shipment.</strong></blockquote><blockquote>· <strong>Structured outputs plus bounded retries turned a model error into a controlled fallback</strong> (SHP-003 in v2), with a full audit trail rather than a silent wrong answer.</blockquote><h4><strong>What to treat with caution</strong></h4><blockquote>· <strong>Ten shipments, one run per version.</strong> Version comparisons are between single runs.</blockquote><blockquote>· <strong>LLM judges grade LLM output.</strong> The Critics, the coherence judge, and the playbook judge are all LLMs. The coherence judge gave 5/5 to every shipment in v2 and v3, so it is not discriminating much and cannot guide improvement on its own.</blockquote><blockquote>· <strong>Tool-call accuracy is a log check.</strong> It confirms the required tools and agents ran, not that their output was good.</blockquote><blockquote>· <strong>The judge sees only retrieved chunks,</strong> so NO_RULE_FOUND is ambiguous.</blockquote><blockquote>· <strong>The Critic’s full rationale is not printed.</strong> Only its alignment fields are shown.</blockquote><blockquote>· <strong>The answer key may itself be debatable.</strong> SHP-005 shows that the labelled “<strong><em>correct</em></strong>” action and a literal reading of the playbook can disagree. That is a question for the policy owner, not for a prompt.</blockquote><h3>9. LangSmith: traceability today, drift detection as the next step</h3><p>Seven node functions (<strong>preprocessor_node, orchestrator_node, resolution_agent_node, critic_resolution_node, communication_agent_node, critic_communication_node, and finalize_node</strong>) are decorated with @traceable, and tracing is configured from the project’s settings file. Each run therefore yields a trace of which node ran, in what order, with what inputs and outputs. That is what made the SHP-003 failure easy to diagnose: <strong>the retries, the invalid N/A, and the forced escalation are all visible as steps rather than inferred.</strong></p><pre>from langsmith import traceable<br> <br>@traceable(name=&quot;critic_resolution_node&quot;)<br>def critic_resolution_node(state: UnifiedAgentState) -&gt; UnifiedAgentState:<br>    &quot;&quot;&quot;Critic Agent: validates the resolution decision against the playbook.&quot;&quot;&quot;<br>    result = eval_llm.with_structured_output(CriticResolutionOutput).invoke([...])<br>    state[&quot;critic_resolution_output&quot;] = result.model_dump()<br>    state[&quot;trajectory_log&quot;].append(<br>        f&quot;critic_resolution: decision={result.decision}, &quot;<br>        f&quot;playbook_alignment={result.playbook_alignment}&quot;<br>    )<br>    return state</pre><p>Tracing is the foundation. It is <strong>not</strong> drift detection by itself. The notebook does not implement versioned cohorts, alerts, or a regression gate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kq06OfwHhgdnpsntCETAkg.png" /><figcaption><em>Figure 5. A proposed extension, not something already deployed.</em></figcaption></figure><p>To turn traces into drift traceability, I would add:</p><ol><li><strong>Version metadata on every run:</strong> code revision, prompt version, playbook-PDF hash, model and embedding versions, dataset version. LangSmith supports run metadata and tags for this.</li></ol><p><strong>2. Signals tied to this system’s failure modes:</strong></p><p>· <strong>Playbook drift:</strong> a changed PDF changes the retrieved text, so re-baseline the labels and the judge.</p><p>· <strong>Retrieval drift:</strong> a rising share of NO_RULE_FOUND verdicts.</p><p>· <strong>Critic-judge disagreement rate,</strong> the measurable version of the SHP-002/SHP-008 gap.</p><p>· <strong>Retry and forced-escalation rate,</strong> which would have flagged the SHP-003 problem in v2.</p><p>· <strong>Escalation-rate shift</strong> and <strong>latency percentiles.</strong></p><p><strong>3. A frozen regression set,</strong> with repeated runs per version and human review of every CONFLICT.</p><p><strong>4. Governance:</strong> redact customer names and unnecessary personal data from traces, control access, and set a retention period. Logging every prompt is not automatically an acceptable audit policy.</p><p>An illustrative sketch of the metadata (not implemented in the notebook):</p><pre>import hashlib, os<br>from pathlib import Path<br> <br>playbook_hash = hashlib.sha256(<br>    Path(&quot;exception_resolution_playbook.pdf&quot;).read_bytes()<br>).hexdigest()<br> <br>run_config = {<br>    &quot;tags&quot;: [&quot;delivery-exceptions&quot;, &quot;regression&quot;],<br>    &quot;metadata&quot;: {<br>        &quot;revision_id&quot;: os.environ[&quot;GIT_SHA&quot;],     # supplied by your build<br>        &quot;prompt_version&quot;: &quot;v3&quot;,<br>        &quot;playbook_sha256&quot;: playbook_hash,<br>        &quot;dataset_version&quot;: &quot;capstone-10&quot;,<br>    },<br>}</pre><h3>10. Recommendations</h3><p>These are recommendations. None has been run.</p><p>1. <strong>Ask the policy owner to resolve SHP-005.</strong> Should a third attempt with a full locker reroute, hold, or escalate? Change the answer key or the playbook, not the prompt.</p><p>2. <strong>Grow the evaluation set</strong> well beyond ten shipments, including more third-attempt, locker-constrained, and ambiguous-playbook cases.</p><p>3. <strong>Repeat each case several times</strong> per version, and report spread, not a single run.</p><p>4. <strong>Make the judges more discriminating.</strong> A coherence score that is always 5 cannot guide improvement. Add human-reviewed samples.</p><p>5. <strong>Give the playbook judge full-page context</strong> for a fair NO_RULE_FOUND, or separately score retrieval recall.</p><p>6. <strong>Add a deterministic playbook-rule check</strong> for the cases where the playbook states a literal rule, so conflicts are caught without an LLM.</p><p>7. <strong>Implement the observability loop above</strong> before any production use.</p><h3>11. Conclusion: measure what the answer key cannot</h3><p>Every number below comes from the saved outputs of the executed notebooks (v1, v2, v3), not from a new experiment. Temperature is 0, but LLM behaviour can still vary between runs, and the critics and judges are themselves LLMs. <strong>No model weights were fine-tuned.</strong> All “<strong><em>tuning</em></strong>” described is prompt, schema, and evaluation-layer engineering. Comparisons between versions are between single runs: evidence, not a guarantee.</p><p>The system’s final scorecard is excellent on the metrics it was built to optimise:</p><blockquote><strong>100% task completion,</strong></blockquote><blockquote><strong>100% escalation accuracy,</strong></blockquote><blockquote><strong>100% tool-call accuracy,</strong></blockquote><blockquote><strong>5.00 / 5 coherence,</strong></blockquote><blockquote><strong>7.19 s mean latency.</strong></blockquote><p>The most useful line in the report is <strong>62% playbook adherence with 2 answer-key passes that conflict with the playbook</strong>, because it is the only line that could tell us something we did not already believe.</p><p>Three lessons carry over to any agentic system:</p><blockquote>1. <strong>Put rules in code and judgement in models.</strong> The deterministic layer, not an LLM, carried most of the safety.</blockquote><blockquote><strong>2. A reconciled answer key will always flatter you.</strong> Keep the original labels, and add an independent check that does not see them.</blockquote><blockquote>3. <strong>Make disagreement visible.</strong> When two checks differ, that is a review queue, not a verdict.</blockquote><h3>References</h3><p>· <a href="https://docs.langchain.com/langsmith/observability">LangSmith observability</a></p><p>· <a href="https://docs.langchain.com/langsmith/evaluation">LangSmith evaluation</a></p><p>· <a href="https://docs.langchain.com/langsmith/add-metadata-tags">Trace metadata and tags</a></p><h3>Let’s Connect</h3><p><a href="https://www.linkedin.com/in/rolando-bouloy-fascitelli/"><strong>LinkedIn</strong></a></p><p><a href="https://github.com/RoloMagnus/ai-powered-shipment-disruption-router"><strong>GitHub</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c67214f9310b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/my-multi-agentic-framework-scored-100-on-accuracy-task-completion-it-was-not-the-most-important-c67214f9310b">My Multi-Agentic framework scored 100% on Accuracy Task Completion — It Was Not the Most Important…</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Forecasting Agent Platform, Part 3: Turning Agentic Workflows into MCP Tools and Skills]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/building-a-forecasting-agent-platform-part-3-turning-agentic-workflows-into-mcp-tools-and-skills-7044991e147a?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/1250/1*3oNHl22cVRYAc9rEV1-9vQ.png" width="1250"></a></p><p class="medium-feed-snippet">Running Annual Forecasting and Planning in Claude Code</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/building-a-forecasting-agent-platform-part-3-turning-agentic-workflows-into-mcp-tools-and-skills-7044991e147a?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/building-a-forecasting-agent-platform-part-3-turning-agentic-workflows-into-mcp-tools-and-skills-7044991e147a?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/7044991e147a</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[time-series-forecasting]]></category>
            <category><![CDATA[ai-native-data-science]]></category>
            <category><![CDATA[agentic-workflow]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Sophia Chen, PhD]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:50:27 GMT</pubDate>
            <atom:updated>2026-11-02T05:35:47.655Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The LLMs That Were Talked Into Anxiety And Back]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/the-llms-that-were-talked-into-anxiety-and-back-22ea2ab51d25?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/1365/1*HoTEP4ZztyFmO_HJ55sjDw.png" width="1365"></a></p><p class="medium-feed-snippet">A short text could talk LLMs into seven negative emotions, and mostly out of them; then what does the way we talk to them do to the&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/the-llms-that-were-talked-into-anxiety-and-back-22ea2ab51d25?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/the-llms-that-were-talked-into-anxiety-and-back-22ea2ab51d25?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/22ea2ab51d25</guid>
            <category><![CDATA[mindfulness]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[psychology]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Giovanni Pagano]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:50:11 GMT</pubDate>
            <atom:updated>2026-11-01T13:31:19.464Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Quantizing an Analytics Agent Without Losing the Decision]]></title>
            <link>https://medium.com/data-science-collective/quantizing-an-analytics-agent-without-losing-the-decision-by-sagar-ganapaneni-a5579a429fed?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/a5579a429fed</guid>
            <category><![CDATA[model-compression]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai-engineering]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Sagar Ganapaneni]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:49:57 GMT</pubDate>
            <atom:updated>2026-11-05T06:09:18.599Z</atom:updated>
            <content:encoded><![CDATA[<p><em>A follow-up to “Fitting a Stronger Model Into Less Memory”: what happens when INT4 rounding doesn’t just add noise, but flips which answer an agent picks.</em></p><p>In <a href="https://medium.com/data-science-collective/fitting-a-stronger-model-into-less-memory-a-quantization-walkthrough-by-sagar-d4530c368aa5">Fitting a Stronger Model Into Less Memory</a>, two quantized weights flipped a toy business from retail to repair, and nothing about the arithmetic was wrong. Rounding did exactly what rounding does. That example was narrow on purpose: small enough to check by hand, so the flip couldn’t be waved away as a measurement artifact.</p><p>Here, the same question gets a sharper stand-in. Instead of a business classifier, a two-choice scorer meant to resemble a decision an analytics agent actually makes, such as picking which denominator a retention query should use. The mechanics are the same scale, rounding, and clipping as last time. What changes is what’s riding on the answer.</p><p>My first instinct when learning quantization was to keep reducing the scale. If the values are closer together, the rounding error should get smaller. I was thinking of a scale of 0.01, but with only four codes that leaves us with 0, 0.01, 0.02, and 0.03. We have better spacing and almost no range. A value like 0.5 no longer fits.</p><p>That was the part I needed to work through. The number of bits limits the choices we can store. The scale decides how far apart those choices sit, and values beyond the range have to be clipped. Making the scale smaller does not give us more choices.</p><p>For a model, the next question is what those rounding errors do to the calculation. With an analytics agent, I’m interested in whether they change a choice that reaches the query or the final answer. The model can still produce readable text after choosing a different metric definition.</p><p>A small calculation makes this easier to inspect. The example below is a constructed two-output scorer, evaluated in NumPy, rather than a measurement from an LLM. It has two choices, A and B, with these weights and one shared input. Multiplying each row by the input gives A a score of 0.662 and B a score of 0.639: A wins by 0.023.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/701/1*tTFkRKsvIfNbbXD8hj1-6w.png" /><figcaption>setup-two-choices</figcaption></figure><p>Now quantize the weights using four-bit integer codes from −7 to 7, leaving −8 unused. Use zero as the zero-point, round-to-nearest-even, and one scale for the whole matrix. The largest absolute weight is 2.8, so the scale is 2.8 / 7 = 0.4. Divide each weight by 0.4, round it to a code, then multiply by 0.4 to reconstruct it. A now scores 0.720 and B scores 0.760. The choice has switched to B. Nothing was clipped.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*vEQE8tVBf6UIe3Ae-_m_Lw.png" /><figcaption>decision-flip-bars</figcaption></figure><p>The 2.8 is worth looking at here. Its input is zero, so it contributes nothing to this particular answer, but it still sets the scale for the other weights. Those smaller weights lose the differences that previously gave A the higher score.</p><p>We can give the first three weights in each row their own scale and put the final weight in a separate group. Doing that restores A on this input and reduces the average squared weight error by about 30 times. These tiny groups illustrate the calculation; production groups also have to account for kernel and storage overhead.</p><p>That fixes the input we started with. Changing the third input from 0.1 to a variable t shows what happens elsewhere. Keep the other inputs fixed at [1, 0.2, t, 0]. The original score difference, A minus B, is 0.028 − 0.05t. It changes sign at t = 0.56.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*tLXt-Da-I_RY5-5XVI_qtA.png" /><figcaption>margin-panels</figcaption></figure><p>Global INT4 chooses B for every positive t. Split INT4 keeps choosing A across the plotted range. INT8 moves the boundary to about 0.3333. So the grouping that fixed our first input still fails elsewhere, despite its smaller weight error. These eight constructed weights show how a decision can change; they cannot estimate an actual agent’s failure rate.</p><p>For a fixed input x, a weight change ΔW changes the output by Δy = ΔW x. The input determines how much the rounding matters. In a full model, earlier layers can also change the inputs to later ones.</p><p>For paid-account retention, accounts that became paid after the opening snapshot should not enter the opening cohort. Choosing the wrong denominator can still produce executable SQL and a plausible percentage. Quantization could change that choice, but this numerical example does not establish that it happened in a real agent.</p><p>I’d test the same checkpoint at BF16, W8A16, and W4A16, keeping the instructions, tools, data, activation precision, and attention-cache precision fixed. W4A16 means four-bit weights and sixteen-bit activations. Replay identical prefixes and tool results to locate differences, then let each version complete the task with the same retry budget.</p><p>Grade against reviewed answers and executed queries; BF16 can be wrong too. Include user corrections and missing evidence. Keep those tests separate from calibration data, which should reflect the schemas and tool responses the agent handles.</p><p>Then measure memory and end-to-end latency. Scales and the attention cache add memory beyond the weights. Kernel performance, tool waits, and extra retries determine whether the smaller model actually saves the user time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/701/1*0EHJsKv3y4TDO6atwdi1zQ.png" /><figcaption>release checklist</figcaption></figure><p>For this agent, I’d keep the retention question in the release test. I want to see the right denominator survive the change in precision, alongside a measured improvement in memory or latency.</p><h3>Notes</h3><p><em>A follow-up to: </em><a href="https://medium.com/data-science-collective/fitting-a-stronger-model-into-less-memory-a-quantization-walkthrough-by-sagar-d4530c368aa5"><em>Fitting a Stronger Model Into Less Memory: A Quantization Walkthrough</em></a><em>.</em></p><p><em>The scorer, weights, and input sweep here are constructed for illustration, independently re-derived and verified: the score computations, the per-group rescaling, the ~30× error reduction, and the INT8 crossing point at t ≈ 0.333 all check out exactly against hand and NumPy calculation.</em></p><p><em>W4A16 / W8A16 notation and the BF16/INT8/INT4 release-testing approach follow common LLM-serving practice (four- or eight-bit weights, sixteen-bit activations).</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a5579a429fed" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/quantizing-an-analytics-agent-without-losing-the-decision-by-sagar-ganapaneni-a5579a429fed">Quantizing an Analytics Agent Without Losing the Decision</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Data Engineering Roadmap I Followed: The Complete Beginner to Expert Guide]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/the-data-engineering-roadmap-i-followed-the-complete-beginner-to-expert-guide-a962db909a2f?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/1536/1*8Mpyo2ZPPRCAAf4l6buOsQ.png" width="1536"></a></p><p class="medium-feed-snippet">Building expertise in any domain requires more than learning random tools it requires following a structured roadmap from fundamentals to&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/the-data-engineering-roadmap-i-followed-the-complete-beginner-to-expert-guide-a962db909a2f?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/the-data-engineering-roadmap-i-followed-the-complete-beginner-to-expert-guide-a962db909a2f?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/a962db909a2f</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Muaaz]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:49:55 GMT</pubDate>
            <atom:updated>2026-11-04T13:16:11.544Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Sam Altman’s Math on Your RAM Is Right. His Conclusion Isn’t.]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/sam-altmans-math-on-your-ram-is-right-his-conclusion-isn-t-70e7cf196634?source=rss----8993e01dcfd3---4"><img src="https://cdn-images-1.medium.com/max/1494/1*DMQ1maL7N5JJsyPYUeQeZA.png" width="1494"></a></p><p class="medium-feed-snippet">Renting a 284B model beats running it on free hardware by 3x.</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/sam-altmans-math-on-your-ram-is-right-his-conclusion-isn-t-70e7cf196634?source=rss----8993e01dcfd3---4">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/sam-altmans-math-on-your-ram-is-right-his-conclusion-isn-t-70e7cf196634?source=rss----8993e01dcfd3---4</link>
            <guid isPermaLink="false">https://medium.com/p/70e7cf196634</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Samvardhan Singh]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 15:49:44 GMT</pubDate>
            <atom:updated>2026-11-03T16:08:42.386Z</atom:updated>
        </item>
    </channel>
</rss>