<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Betty Wu</title>
    <description>The latest articles on DEV Community by Betty Wu (@jiamanbettywu).</description>
    <link>https://dev.to/jiamanbettywu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024419%2Fcdf5ed0f-ebec-4012-a290-5a61020f6bb2.jpg</url>
      <title>DEV Community: Betty Wu</title>
      <link>https://dev.to/jiamanbettywu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jiamanbettywu"/>
    <language>en</language>
    <item>
      <title>One Tap, Four Meanings: What a Thumbs-Down Actually Teaches</title>
      <dc:creator>Betty Wu</dc:creator>
      <pubDate>Fri, 24 Jul 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/jiamanbettywu/one-tap-four-meanings-what-a-thumbs-down-actually-teaches-3lin</link>
      <guid>https://dev.to/jiamanbettywu/one-tap-four-meanings-what-a-thumbs-down-actually-teaches-3lin</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 3 of a series on building Wardrobe AI. Part 1 set up the guiding principle; Part 2 walked the pipeline that decides which clothes the model sees and promised a deeper look at the one stage it waved past, the feedback signal. This is that look. All the code in this post is public, in &lt;a href="https://github.com/JiamanBettyWu/mise" rel="noopener noreferrer"&gt;github.com/JiamanBettyWu/mise&lt;/a&gt;; snippets link to their source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  I wanted it to know my taste
&lt;/h2&gt;

&lt;p&gt;The recommender from Part 2 was good at &lt;em&gt;variety&lt;/em&gt; and &lt;em&gt;weather&lt;/em&gt;, but it didn't know &lt;strong&gt;me&lt;/strong&gt;. It didn't know I reach for neutral colors, that I think most outfits are improved by exactly one interesting piece, or that I quietly hate a particular jacket I keep meaning to donate. I wanted the app to learn my aesthetic over time, to get more &lt;em&gt;me&lt;/em&gt; the longer I used it.&lt;/p&gt;

&lt;p&gt;There are really only two ways to get taste into the system. The first is to just &lt;strong&gt;tell it&lt;/strong&gt;: type "I prefer neutral basics" into a settings box and have the model treat that as a rule. Easy, and I built it (more on why it's a special case below). The second is to have it &lt;strong&gt;learn from my reactions&lt;/strong&gt;: let me thumb outfits up or down and infer my taste from the pattern. That second path is where&lt;br&gt;
all the interesting problems live, because &lt;em&gt;recording&lt;/em&gt; a thumbs-down is trivial and &lt;em&gt;learning the right lesson from it&lt;/em&gt; is not.&lt;/p&gt;

&lt;p&gt;Here's the trap. I thumb down today's outfit. What did I just say? Maybe the jacket is ugly. Maybe the jacket is fine but clashed with those pants. Maybe the whole thing was lovely and it's just freezing and I wanted something warmer. Maybe it was perfect for a coffee run but I have a meeting. One tap, at least four completely different meanings, and if the system guesses wrong it doesn't just&lt;br&gt;
miss once. It bakes the wrong lesson into every future morning. This post is about making that one tap teach the &lt;em&gt;right&lt;/em&gt; thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33wblcfn6wasqxixzl0y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F33wblcfn6wasqxixzl0y.png" alt="A recommended outfit in the app with 👍 and 👎 buttons in its corner, above the outfit's reasoning and item photos." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhk4w0ez6sfa1cng07qj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhk4w0ez6sfa1cng07qj.jpg" alt="The bottom of the daily email: the outfit's item photos above two tappable buttons, " width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Three doors a preference can walk through
&lt;/h2&gt;

&lt;p&gt;Taste enters this system through three distinct doors, and (this is the whole point of the post) they are deliberately &lt;em&gt;not&lt;/em&gt; treated the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit preferences I write&lt;/strong&gt; ("prefer neutral colors"). A &lt;strong&gt;hard constraint&lt;/strong&gt;, honored in every outfit, no exceptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-item feedback&lt;/strong&gt; (thumbs on individual outfits). A &lt;strong&gt;soft, self-correcting nudge&lt;/strong&gt;, expressed as the sampling-weight multiplier from &lt;a href="https://dev.to/writing/wardrobe-ai-recommendation-pipeline/"&gt;Part 2&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inferred preferences:&lt;/strong&gt; patterns a weekly "reflection" job distills from my whole feedback history ("you seem to avoid layering in summer"). Also &lt;strong&gt;soft&lt;/strong&gt;, but fundamentally riskier. The &lt;em&gt;machinery&lt;/em&gt; of that weekly job is its own story (Part 5); what matters now is how its output is treated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Doors 2 and 3 are both "learning from feedback," but they fail in opposite ways. Hold that thought; first, the math behind door 2.&lt;/p&gt;
&lt;h2&gt;
  
  
  The math: what a thumbs-down does to the odds
&lt;/h2&gt;

&lt;p&gt;Recall from Part 2 that every item carries a &lt;strong&gt;feedback multiplier&lt;/strong&gt; that tips its sampling odds up or down. Here's how that number is actually computed, and why the obvious version is a trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The naive version.&lt;/strong&gt; Track likes and dislikes per item; the item's score is its like rate, &lt;code&gt;ups / (ups + downs)&lt;/code&gt;. Clean, but brittle. Thumb down a shirt exactly once and its score is &lt;code&gt;0/1 = 0&lt;/code&gt;: a single bad day damns it forever. Thumb one up once and it's &lt;code&gt;1/1 = 100%&lt;/code&gt;: a perfect item on one data point. With the handful of&lt;br&gt;
verdicts you actually accumulate day to day, raw rates lurch around wildly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: smoothing.&lt;/strong&gt; Instead of trusting the raw count, start every item from a neutral assumption and let evidence move it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ups&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ups&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;downs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# 0 verdicts -&amp;gt; 0.5 (neutral)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;sup&gt;→ full context: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Those &lt;code&gt;+1&lt;/code&gt; and &lt;code&gt;+2&lt;/code&gt; are the whole trick. With no data, the score is &lt;code&gt;1/2&lt;/code&gt;, a shrug, "no opinion yet." One thumbs-down takes a fresh item to &lt;code&gt;1/3 ≈ 0.33&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;: &lt;em&gt;disliked, but not condemned on a sample of one.&lt;/em&gt; As real verdicts pile up, those constants matter less and the score converges on the true rate. This is a &lt;strong&gt;Beta-Bernoulli&lt;/strong&gt; estimate, the textbook way to estimate a yes/no rate (here, "will I like it?") when samples are scarce, with the &lt;code&gt;+1/+2&lt;/code&gt; acting as a prior belief of 50/50. The everyday version: a restaurant with one 1-star review isn't &lt;em&gt;really&lt;/em&gt; a zero-star restaurant. You assume roughly average until enough reviews&lt;br&gt;
prove otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spreading the blame.&lt;/strong&gt; A thumbs-down lands on an &lt;em&gt;outfit&lt;/em&gt;, but&lt;br&gt;
the multiplier lives on &lt;em&gt;items&lt;/em&gt;. If I dislike a four-piece outfit, which piece was&lt;br&gt;
the problem? Honest answer: I don't know, and neither does the system. So it&lt;br&gt;
refuses to over-blame: one verdict distributes &lt;strong&gt;one unit&lt;/strong&gt; of credit or blame across the items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;credit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# a 4-item outfit -&amp;gt; 0.25 each
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single tap is one vote, not four. It nudges everything in the outfit a little,&lt;br&gt;
rather than slamming each piece as if it were individually condemned. Over many&lt;br&gt;
outfits, the genuinely bad item shows up in more dislikes and accumulates the&lt;br&gt;
blame; the innocent bystanders that happened to be in one bad outfit recover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bounding it.&lt;/strong&gt; Finally, that smoothed score (between 0 and 1) is stretched onto&lt;br&gt;
a deliberately narrow multiplier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FLOOR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CEILING&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;FLOOR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;   &lt;span class="c1"&gt;# [0,1] -&amp;gt; [0.6, 1.4]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;sup&gt;→ full context: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The ceiling (1.4) keeps a beloved item from dominating every outfit. The floor&lt;br&gt;
(0.6) is the one I want to call out, because it's the &lt;strong&gt;exploration vs.&lt;br&gt;
exploitation&lt;/strong&gt; dial from Parts 1 and 2 made concrete. Exploitation says "show me&lt;br&gt;
what I've liked." Exploration says "keep showing me other things anyway." That&lt;br&gt;
floor of 0.6 means even an item I've thumbed down repeatedly never drops to zero&lt;br&gt;
probability; it just gets quieter. The system never &lt;em&gt;stops&lt;/em&gt; exploring, even on the&lt;br&gt;
stuff it thinks I dislike, because "I disliked it once in July" is not proof I&lt;br&gt;
should never see it in October. The feedback multiplier exploits; the floor&lt;br&gt;
guarantees exploration; both are tunable constants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disambiguating the tap
&lt;/h2&gt;

&lt;p&gt;The math above quietly assumes the dislike was about the items. But remember the&lt;br&gt;
four meanings of a thumbs-down. So a dislike can optionally carry an&lt;br&gt;
&lt;strong&gt;attribution&lt;/strong&gt;: was it the &lt;em&gt;items&lt;/em&gt;, the &lt;em&gt;combination&lt;/em&gt;, the &lt;em&gt;weather&lt;/em&gt;, or the&lt;br&gt;
&lt;em&gt;occasion&lt;/em&gt;? That tag changes who gets blamed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsu0x44aeidz7bcgwyb2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsu0x44aeidz7bcgwyb2.png" alt="One thumbs-down, four meanings. " width="799" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3oikk288gqi5dco89a9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3oikk288gqi5dco89a9.png" alt="After a thumbs-down: a " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specific items&lt;/strong&gt;: blame lands only on the pieces I named. High-confidence signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The combination&lt;/strong&gt;: the items are individually fine; it was the pairing. The
items are &lt;strong&gt;exonerated&lt;/strong&gt; (zero blame in the multiplier), and instead that exact
&lt;em&gt;set&lt;/em&gt; goes on the deterministic block-list from Part 2's post-generation filter.
(This is where that "known-bad combination" tag came from: I promised you in
Part 2, and here it is.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The weather / the occasion&lt;/strong&gt;: not the outfit's fault at all. The items are
exonerated; a weather complaint is dropped from learning entirely (it's feedback
on the forecast, not the wardrobe).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attribution is what stops a single grumpy "too cold today" from quietly&lt;br&gt;
penalizing four perfectly good items for the rest of the year. It routes each&lt;br&gt;
verdict to the part of the system that can actually act on it: items to the&lt;br&gt;
multiplier, bad pairings to the block-list, weather to the bin.&lt;/p&gt;

&lt;p&gt;Recent verdicts also go into the prompt as plain context: disliked outfits&lt;br&gt;
as an "avoid recombining similar assemblies" list, liked ones explicitly captioned&lt;br&gt;
&lt;em&gt;style direction only, do not recreate these.&lt;/em&gt; That caption is deliberate. Recency&lt;br&gt;
weighting just pushed those exact items down to keep things fresh, and I don't want&lt;br&gt;
the model clawing them back by hunting near-duplicates of something I liked last&lt;br&gt;
week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson: a loop that self-corrects vs. a bias that doesn't
&lt;/h2&gt;

&lt;p&gt;Now the payoff, and the reason doors 2 and 3 exist as separate doors.&lt;/p&gt;

&lt;p&gt;The per-item feedback loop (door 2) is &lt;strong&gt;safe to be aggressive&lt;/strong&gt; because it&lt;br&gt;
&lt;em&gt;self-corrects&lt;/em&gt;. Suppose it over-penalizes a shirt after one unlucky dislike. The&lt;br&gt;
floor keeps the shirt in rotation; it resurfaces; if I like it next time, its&lt;br&gt;
score climbs right back. Wrong guesses get pulled back toward the truth by the&lt;br&gt;
next data point. It behaves like a &lt;strong&gt;thermostat&lt;/strong&gt;: it can overshoot, but the&lt;br&gt;
feedback that caused the overshoot is the same feedback that corrects it.&lt;/p&gt;

&lt;p&gt;An inferred preference (door 3) is a different animal. When the weekly reflection&lt;br&gt;
job writes "you avoid bright colors," that sentence goes into the prompt and&lt;br&gt;
shapes &lt;em&gt;every&lt;/em&gt; outfit from then on. If it's right, wonderful. If it's &lt;strong&gt;wrong&lt;/strong&gt;&lt;br&gt;
(maybe I downvoted three bright things for unrelated reasons), nothing in the loop&lt;br&gt;
fixes it. A wrong inference is a &lt;strong&gt;systematic bias with no floor&lt;/strong&gt;: it quietly tilts everything, and the only thing that ever corrects it is &lt;em&gt;me noticing and deleting it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That asymmetry dictates the design. Because a wrong inference can't self-correct,&lt;br&gt;
it has to be &lt;strong&gt;legible and overridable&lt;/strong&gt;: every inferred preference is short,&lt;br&gt;
specific, cites the exact feedback that produced it, and shows up in an editable&lt;br&gt;
list I can prune in one click. And because it's &lt;em&gt;soft&lt;/em&gt;, a hard preference I wrote&lt;br&gt;
myself always wins when the two conflict. The model is told to lean on learned preferences but never to sacrifice weather, coherence, or an explicit rule. So a wrong inference &lt;em&gt;degrades gracefully&lt;/em&gt;: it nudges a few outfits until I catch it, rather than quietly overruling everything.&lt;/p&gt;

&lt;p&gt;This is the same organizing principle from Part 1, one level deeper. Part 1 split&lt;br&gt;
&lt;strong&gt;physics (hard) from preference (soft)&lt;/strong&gt;. Here, &lt;em&gt;within&lt;/em&gt; the soft world, there's&lt;br&gt;
a second split: a signal that self-corrects can be trusted to be assertive; a&lt;br&gt;
signal that can't must be kept legible, weak, and easy to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;Door 3, the weekly "reflection" job that reads my whole feedback history and&lt;br&gt;
distills durable preferences, has been lurking in the background this whole post.&lt;br&gt;
It turns out to be a small AI system in its own right, with a surprisingly fussy&lt;br&gt;
set of rules to keep it from doing exactly the kind of unrecoverable damage I just&lt;br&gt;
described. But it's also the second of two &lt;em&gt;agentic&lt;/em&gt; pieces in this project, so&lt;br&gt;
before I get to it I want to introduce the agentic system with a friendlier&lt;br&gt;
example: a trip packing planner that can decide its own next move. That's Part 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go deeper
&lt;/h2&gt;

&lt;p&gt;Everything in this post is real, running code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The whole multiplier, smoothing + blame-splitting + attribution routing, is
one pure function: &lt;code&gt;_feedback_multipliers&lt;/code&gt; in
&lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The hard-vs-soft split in the prompt (user preferences as constraints, learned
ones as nudges): &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/claude.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/claude.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The design doc where these rules were actually worked out, trade-offs and
rejected alternatives included: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/docs/feedback-loop-design.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/feedback-loop-design.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>The Same Outfit, Four Days Straight: Fixing What the Prompt Can't</title>
      <dc:creator>Betty Wu</dc:creator>
      <pubDate>Tue, 14 Jul 2026 16:02:00 +0000</pubDate>
      <link>https://dev.to/jiamanbettywu/the-same-outfit-four-days-straight-fixing-what-the-prompt-cant-151p</link>
      <guid>https://dev.to/jiamanbettywu/the-same-outfit-four-days-straight-fixing-what-the-prompt-cant-151p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 2 of a series on building &lt;strong&gt;Wardrobe AI&lt;/strong&gt;. &lt;a href="https://dev.to/writing/wardrobe-ai-overview/"&gt;Part 1&lt;/a&gt; covered what the&lt;br&gt;
app does and the principle behind it: &lt;em&gt;stochastic weights for preferences,&lt;br&gt;
deterministic logic for physics.&lt;/em&gt; This post is where that principle earns its&lt;br&gt;
keep. The feedback math gets a breadth-level mention here and a full treatment&lt;br&gt;
in Part 3. All the code in this post is public, in&lt;br&gt;
&lt;a href="https://github.com/JiamanBettyWu/mise" rel="noopener noreferrer"&gt;github.com/JiamanBettyWu/mise&lt;/a&gt;; snippets link to their source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug that started it: four days, one outfit
&lt;/h2&gt;

&lt;p&gt;The first version of the recommender was about as simple as it sounds: pull all my available clothes from the database, hand the whole list to the model, ask for an outfit. It worked. It also recommended me &lt;strong&gt;the same outfit four days in a row.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a special kind of disappointing, because it's not &lt;em&gt;wrong&lt;/em&gt;. The outfit was fine all four days. The trouble is that a stylist who suggests the identical thing every morning isn't really doing the job. The whole value, as I framed it in Part 1, is the &lt;em&gt;warm start&lt;/em&gt;: a fresh push to react to. A stuck recommendation is a cold start wearing a trenchcoat.&lt;/p&gt;

&lt;p&gt;Why did it happen? Large language models have a well-known tendency to &lt;strong&gt;anchor&lt;/strong&gt; on whatever comes first in their input. My database returned items in a stable order, so the model kept seeing the same clothes at the top of the list and kept reaching for them. The fix people reach for first is "tell the model to be more varied," and I did eventually do a version of that, but prompting alone is a soft nudge against a strong bias. The bigger lever turned out to be upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key idea: the model never sees the whole closet
&lt;/h2&gt;

&lt;p&gt;Here's the realization that reorganized the whole feature: &lt;strong&gt;the quality of a&lt;br&gt;
recommendation is decided mostly &lt;em&gt;before&lt;/em&gt; the AI is ever called.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of dumping my entire wardrobe on the model, I built a pipeline that curates a smaller &lt;strong&gt;candidate pool&lt;/strong&gt; first, a deliberately chosen, shuffled subset of clothes, and only &lt;em&gt;that&lt;/em&gt; goes to the model. Think of the model as the host at a party greeting guests, and the pipeline as the bouncer with the guest list. By the time the host says hello, the bouncer has already decided who's in the room. Most of the interesting decisions live with the bouncer.&lt;/p&gt;

&lt;p&gt;The pipeline is almost entirely &lt;strong&gt;deterministic&lt;/strong&gt; plumbing wrapped around two&lt;br&gt;
small bits of &lt;strong&gt;randomness&lt;/strong&gt;. Here's the whole thing end to end:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fryxcjwnf6v72iio19w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fryxcjwnf6v72iio19w.png" alt="The recommendation pipeline. The available wardrobe passes through an extremes gate (deterministic). Two signals, recency decay and a Beta-Bernoulli feedback like-rate, combine into a sampling weight that drives a weighted random sample (stochastic). Category floors refine it into the candidate pool the model sees; the model, steered by the prompt, proposes candidates; a post-generation combo filter drops known-bad and repeat sets into the final outfits. Teal = deterministic/physics, amber = stochastic/preference." width="800" height="996"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me walk it stage by stage. I'll go deep on everything except the feedback math, which has enough subtlety to deserve its own post (Part 3); here it's just "a number that tips the odds."&lt;/p&gt;
&lt;h2&gt;
  
  
  Stage 1: The extremes gate (physics, and it runs first)
&lt;/h2&gt;

&lt;p&gt;Before anything gets weighted or sampled, a deterministic filter drops items that are &lt;em&gt;absurd&lt;/em&gt; for today's weather, like a heavy parka when it's 30°C (86°F). This is the "physics" side of the principle: there's nothing fuzzy about "it's too hot for a parka," so it's a hard rule, not a probability.&lt;/p&gt;

&lt;p&gt;It runs &lt;em&gt;first&lt;/em&gt;, and the ordering matters. If absurd items were still in the pool during sampling, they'd take up slots that should go to wearable clothes. A parka sitting in the candidate pool on a hot day is, at best, an item the model ignores and, at worst, one it accidentally picks. Gate first, then the model only ever reasons about clothes that make sense for today.&lt;/p&gt;

&lt;p&gt;Note what the gate does &lt;em&gt;not&lt;/em&gt; do: it only removes the absurd extremes, not the merely-suboptimal. Fine-grained "is this warm &lt;em&gt;enough&lt;/em&gt; for a cool morning" judgment is handled later, by the model itself. That two-tier split (hard gate for the absurd, model judgment for the nuanced) comes back when we get to the prompt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Stage 2: Recency decay (so yesterday's clothes step back)
&lt;/h2&gt;

&lt;p&gt;This is the direct fix for the four-days bug. Every item gets a &lt;strong&gt;recency score&lt;/strong&gt;: the more recently (and more often) it's been recommended, the higher its score, with the influence fading a little each day. Concretely it's exponential decay, where each day in the past counts for less:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DAILY_DECAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;           &lt;span class="c1"&gt;# per-day multiplier; ~4-day half-life
# an item worn d days ago contributes DAILY_DECAY ** d to its recency score
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;sup&gt;→ full context: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;"Half-life" here means that after about four days, a wear counts for half as much; after eight, a quarter. The analogy is a music shuffle that's smart about repeats: a song you just heard drops &lt;strong&gt;down&lt;/strong&gt; the queue, but it doesn't fall &lt;strong&gt;off&lt;/strong&gt; the playlist. Nothing is ever banned for being recent; it's just made less likely for a while, so the rotation breathes.&lt;/p&gt;

&lt;p&gt;One subtlety I got wrong the first time and had to fix: recency pressure only makes sense when you have &lt;em&gt;substitutes&lt;/em&gt;. I have exactly one pair of dressy shoes. Penalizing them for "you wore them yesterday" just means tomorrow's elevated outfit has no shoes at all. So items in a &lt;strong&gt;small category&lt;/strong&gt; (at or below a threshold count of available pieces) are exempt from recency entirely. Rotation is for when there's something to rotate &lt;em&gt;to&lt;/em&gt;. I learned this the way most of these rules got learned: it did something dumb first. (I call it the sandals incident, and it shows up again below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: The feedback nudge (breadth only; full story in Part 3)
&lt;/h2&gt;

&lt;p&gt;Alongside recency, each item carries a &lt;strong&gt;feedback multiplier&lt;/strong&gt; derived from my thumbs-up / thumbs-down history: liked items get their odds gently boosted, disliked ones gently reduced. Crucially, it's a &lt;em&gt;bounded&lt;/em&gt; nudge (it never drives an item's probability to zero or to certainty), which keeps a little exploration alive even for things I've thumbed down.&lt;/p&gt;

&lt;p&gt;That's all you need for the pipeline view: it's a number that tips the dice. &lt;em&gt;How&lt;/em&gt; that number is computed (it involves a smoothed Bayesian estimate and blame attribution) is the subject of Part 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: Weighted sampling (where the randomness happens)
&lt;/h2&gt;

&lt;p&gt;Now the two signals combine into a single &lt;strong&gt;sampling weight&lt;/strong&gt; per item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recency_factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;feedback_multiplier&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I draw a random subset, roughly 70% of the pool, where an item's weight is its chance of being picked. Higher weight (not worn lately, generally liked) means more likely to make the cut; lower weight means less likely, never impossible.&lt;/p&gt;

&lt;p&gt;The neat part is &lt;em&gt;how&lt;/em&gt; you sample without replacement when every item has a different weight. There's an elegant one-liner for it (the Efraimidis–Spirakis algorithm): give each item a random key and sort by it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;     &lt;span class="c1"&gt;# bigger weight -&amp;gt; key closer to 0 -&amp;gt; ranks higher
# sort all items by key (descending); the top k are a correct weighted sample of size k
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;sup&gt;→ full context: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This uses only the standard library: no dependencies, no model call, fully deterministic to debug given a fixed random seed. This is the "stochastic" half of the system, and it's deliberately small and contained.&lt;/p&gt;

&lt;p&gt;This is also where Part 1's &lt;strong&gt;exploration vs. exploitation&lt;/strong&gt; trade-off stops being an abstraction and becomes literal code. The feedback multiplier is the &lt;em&gt;exploitation&lt;/em&gt; lever: it leans on what's already worked, pulling proven items up. The randomness in the draw is the &lt;em&gt;exploration&lt;/em&gt; lever: it keeps putting less-favored and not-recently-worn items in front of me, which is exactly how I stumble onto combinations I'd never have chosen myself. There's a subtler reason for exploration, too. The feedback multiplier can only learn about items it actually &lt;em&gt;shows&lt;/em&gt; me; a piece that never gets sampled generates no verdicts. So rotation isn't only about variety, it's what &lt;em&gt;feeds&lt;/em&gt; the taste-learning. Kill the randomness and you'd quietly starve the very thing that's supposed to get smarter over time.&lt;/p&gt;

&lt;p&gt;The nice part is that the balance between the two isn't hard-coded into the logic; it lives in a handful of &lt;strong&gt;tunable constants&lt;/strong&gt;: how hard a thumbs-up boosts an item, what fraction of the wardrobe gets sampled (~70%), how fast recency decays (&lt;code&gt;DAILY_DECAY = 0.85&lt;/code&gt;). Nudge them one way and the app plays favorites; nudge them the other and it turns adventurous. &lt;em&gt;How far&lt;/em&gt; to push each dial, and why the feedback nudge keeps a hard floor so it never stops exploring entirely, is a Part 3 question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: Category floors (don't evict all the shoes)
&lt;/h2&gt;

&lt;p&gt;Random sampling has a failure mode: by chance, a draw can wipe out a whole category. Pull 70% at random and you might end up with zero shoes in the pool, and then there's no possible complete outfit, no matter how clever the model is.&lt;/p&gt;

&lt;p&gt;So after sampling, &lt;strong&gt;category floors&lt;/strong&gt; top the pool back up: guarantee a minimum number of tops, bottoms, shoes, and outerwear. If a category fell below its floor, the next-best items (the ones just outside the random cut) get promoted back in until the floor is met. It's cheap insurance (having an item in the &lt;em&gt;pool&lt;/em&gt; costs nothing; the model can always ignore it), and it's the final piece of the sandals incident: not just "don't rotate away the only shoes," but "never let the dice remove an entire category."&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 6: The candidate pool meets the model
&lt;/h2&gt;

&lt;p&gt;After the floors, what's left is the &lt;strong&gt;candidate pool&lt;/strong&gt;, the curated, shuffled rack the model finally gets to see. &lt;em&gt;Now&lt;/em&gt; the model gets called. One detail matters for what comes next: it doesn't return a single outfit per mode, it returns &lt;strong&gt;three candidates&lt;/strong&gt;, ordered best-first. Hold onto that, because it's&lt;br&gt;
what makes the last filter possible. First, how the prompt steers the call itself.&lt;/p&gt;

&lt;p&gt;One line of logging makes the whole pipeline debuggable. Every run prints the pool it built, which answers the question I ask most often when an outfit surprises me: &lt;em&gt;was that item sampled out, or did the model just ignore it?&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO wardrobe.recommend: candidate pool (52 of 73 after gate + sampling): 
'90s Vintage Maxi Denim Skirt front slit, 
Aritzia Wilfred black suit vest, 
Black cushioned recovery flip flop,
Black long-sleeve merino crew tee,
Black ribbed boat-neck tee,
Blue satin strapless ruched tube top,
Cream collarless ultra light down jacket, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real run: 73 items available, 21 gated or sampled out today, 52 in the room when the model starts picking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: steering the model with the prompt
&lt;/h2&gt;

&lt;p&gt;Everything above curates &lt;em&gt;which&lt;/em&gt; clothes the model considers. The system prompt handles &lt;em&gt;how&lt;/em&gt; it assembles them. The two are complementary levers, and I tuned the prompt the same way I tuned the pipeline: by watching it fail and writing a rule.&lt;/p&gt;

&lt;p&gt;Three passes are worth sharing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warmth, the nuanced tier.&lt;/strong&gt; Remember the extremes gate only removes the &lt;em&gt;absurd&lt;/em&gt;. Everyday "is this actually warm enough for a 9°C (48°F) morning" is a judgment call, so I gave it to the model: every item has a &lt;code&gt;warmth&lt;/code&gt; rating from 1 to 5, and the prompt asks it to reason about the outfit's &lt;em&gt;combined, layered&lt;/em&gt; warmth against today's high and low (light pieces for heat; for cold, either a high-warmth piece or several lighter layers). Hard gate for the absurd, model judgment for the nuance: the same physics-versus-context split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Omit a slot rather than force a bad one.&lt;/strong&gt; My favorite failure. The model kept pairing &lt;strong&gt;sports sandals with an elegant dress&lt;/strong&gt;, repeatedly. The logic was understandable: an outfit needs shoes, and after sampling the sandals were sometimes the only footwear left, so it dutifully jammed them in. The fix was to make "complete" optional:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;An outfit missing shoes beats an outfit with off-mode shoes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The prompt now tells the model that if nothing fits a slot for the occasion, it should &lt;strong&gt;leave that slot empty&lt;/strong&gt; and note the gap in its reasoning, rather than forcing something off-key. It's a small example of treating the prompt as &lt;em&gt;policy&lt;/em&gt;: an incomplete-but-coherent outfit is more useful than a complete-but-absurd one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the full set.&lt;/strong&gt; Finally, the prompt closes the loop on the original bug. It tells the model explicitly that &lt;em&gt;the inventory order is randomized and implies no preference, so explore the whole set rather than defaulting to the first few items.&lt;/em&gt; On its own this would be a weak nudge against the anchoring bias. But it's not on its own: the sampler already shuffles the pool every time, so the prompt and the pipeline push the same direction from two sides. The randomized order removes the stable thing the model was anchoring to; the instruction tells it not to anchor on whatever it gets handed instead.&lt;/p&gt;

&lt;p&gt;An honest epilogue to the four-days bug, though: these two forces killed the &lt;em&gt;worst&lt;/em&gt; of the repetition, but months later I caught a sneakier version of the same disease. The model had developed a fondness for alternating between two similar favorites. The fix was a third force: the prompt now also receives a short list of what it recommended in the past week, with an instruction to prefer pieces that aren't on it. How I &lt;em&gt;found&lt;/em&gt; that pattern, proved the fix actually worked with numbers instead of vibes, and then verified it in production is a story about building an eval loop, and it gets its own post later in the series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 7: The post-generation filter (known-bad and exact repeats)
&lt;/h2&gt;

&lt;p&gt;This is the one stage that runs &lt;em&gt;after&lt;/em&gt; the model, on its output rather than its input, which is exactly why the model returns three candidates instead of one. A final deterministic filter walks the candidates and takes the first one that survives two hard checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Known-bad combinations.&lt;/strong&gt; If I've thumbed down an outfit &lt;em&gt;and&lt;/em&gt; flagged that the problem was the &lt;em&gt;combination&lt;/em&gt; specifically (not the weather, not the occasion), that exact set of items is rejected permanently. This isn't a soft preference to whisper to the model; it's a recorded fact, so it's enforced in code. (Where that "it was the combination" tag comes from is a Part 3 story.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact recent repeats.&lt;/strong&gt; Any exact outfit recommended in the last 7 days is also rejected, so the same assembly can't reappear within the week. This is set-level dedup; item-level rotation is recency's job back in Stage 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the three-candidates choice pays off: if the best candidate is rejected, the filter falls through to the next acceptable one instead of coming back empty-handed, and only if &lt;em&gt;every&lt;/em&gt; candidate is a known-bad combo does it skip the mode. The prompt asks for variety; this filter enforces it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5ya2gxfx778n66oh85a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5ya2gxfx778n66oh85a.png" alt="A generated recommendation in the Today's Outfit page: outfit options with reasoning paragraphs and item photo cards, under a weather summary strip." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stepping back
&lt;/h2&gt;

&lt;p&gt;The shape of this whole feature is the Part 1 principle made concrete. The parts that should be &lt;em&gt;reliable&lt;/em&gt; (don't suggest a parka in July, never empty a category, never repeat a known-bad combo) are deterministic code I can step through and debug. The parts that should be &lt;em&gt;lively&lt;/em&gt; are a small, bounded dose of randomness. The model sits at the end, doing the hard part of assembling a coherent outfit, but only over a set the pipeline has already made safe and varied.&lt;/p&gt;

&lt;p&gt;The one stage I kept waving past, that feedback multiplier, turns out to be the most subtle of all. Recording a thumbs-down is easy; &lt;em&gt;learning from it correctly&lt;/em&gt; is surprisingly hard, and getting it wrong quietly poisons everything downstream.&lt;/p&gt;

&lt;p&gt;That's Part 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go deeper
&lt;/h2&gt;

&lt;p&gt;Everything in this post is real, running code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sampler (recency decay, weighted sampling, category floors):
&lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/outfit_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/outfit_history.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The extremes gate: &lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/weather_gate.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/weather_gate.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The system prompt with the warmth, slot-omission, and explore rules:
&lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/backend/services/claude.py" rel="noopener noreferrer"&gt;&lt;code&gt;backend/services/claude.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The full algorithm reference, with formulas and worked examples:
&lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/docs/recommendation-algorithm.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/recommendation-algorithm.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llms</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Outsourced Getting Dressed to an AI (and Accidentally Built Three of Them)</title>
      <dc:creator>Betty Wu</dc:creator>
      <pubDate>Fri, 10 Jul 2026 21:17:52 +0000</pubDate>
      <link>https://dev.to/jiamanbettywu/i-outsourced-getting-dressed-to-an-ai-and-accidentally-built-three-of-them-b5h</link>
      <guid>https://dev.to/jiamanbettywu/i-outsourced-getting-dressed-to-an-ai-and-accidentally-built-three-of-them-b5h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is Part 1 of a series on building &lt;strong&gt;Wardrobe AI&lt;/strong&gt;, a personal app that&lt;br&gt;
recommends what to wear. This post is the map: what it does and how it's put together. Later posts go deep on the recommendation math, the feedback loop, and the two agents running underneath. Code samples are kept light here; the details get their own posts.&lt;/p&gt;

&lt;p&gt;Everything in this series is a running app with public code and history:&lt;br&gt;
&lt;a href="https://github.com/JiamanBettyWu/mise" rel="noopener noreferrer"&gt;github.com/JiamanBettyWu/mise&lt;/a&gt; (the repo is named &lt;code&gt;mise&lt;/code&gt;, as in &lt;em&gt;mise en place&lt;/em&gt;, but I'll call it Wardrobe AI&lt;br&gt;
throughout). Later posts link the specific files and pull requests as they come up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  It started in front of my closet
&lt;/h2&gt;

&lt;p&gt;A few months ago I was standing in front of my closet before work, running late, staring at a rack that was visibly &lt;em&gt;full&lt;/em&gt; of clothes, and thinking: &lt;em&gt;"I have nothing to wear."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Which was obviously false. I had plenty to wear. That was the actual problem — the rack was overwhelming. Too many options, no obvious answer. What I was experiencing wasn't a shortage of clothes. It was decision fatigue, the small daily tax of having to assemble a reasonable outfit from scratch every single morning before my brain is fully online.&lt;/p&gt;

&lt;p&gt;So I had a second thought, the one that actually mattered: &lt;em&gt;I should just outsource this decision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the entire origin of the project. Not "wouldn't it be cool to use AI for fashion," but something more like "I have a recurring, low-stakes decision I'd happily hand to a machine, and I have enough context (my actual clothes, the weather, my calendar) to make a decent suggestion possible." &lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;The app is a personal wardrobe catalog with an AI stylist bolted on. The setup cost is one-time: I photograph each piece of clothing, and a vision model tags it automatically with type, color, formality, season, fabric, even a rough warmth rating. I review the tags, fix anything it got wrong, optionally add a note, and save. After that, the catalog just exists.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkqzvq3dqcg4qeqg47dl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwkqzvq3dqcg4qeqg47dl.png" alt="A clothing item's detail view in the catalog: a striped sweater vest photo above its AI-generated tags — vest, blue/brown/white stripe, smart-casual, spring, cotton, warmth 2/5 — plus a written description." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, the day-to-day value shows up in two channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A push channel: the morning email.&lt;/strong&gt; Every morning I get an email with an&lt;br&gt;
outfit suggestion (sometimes a few, depending on what my calendar looks like that day), chosen for the actual weather and reasoned out in a sentence or two. I don't have to open anything; it just arrives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohyaf4t0tf7y6pzf4imp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohyaf4t0tf7y6pzf4imp.jpg" alt="The morning email on a phone: " width="800" height="1372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pull channel: on-demand generation in the app.&lt;/strong&gt; The email is great for the default "what do I wear to the office today" case, but some days are not the default when I have a special event. So the app's Today's Outfit page has a notes box (&lt;em&gt;"Describe theoccasion, then click Generate"&lt;/em&gt;) where I can ask for something specific: dinner out, an interview, cold rain, a day on my feet. It generates suggestions for that context, and if the first batch doesn't land, I hit Regenerate. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1m9v1pmen3psayv1p85d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1m9v1pmen3psayv1p85d.png" alt="The Today's Outfit page after typing " width="800" height="944"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are two more capabilities I'll only mention here and unpack later: a &lt;strong&gt;trip packing planner&lt;/strong&gt; (give it a destination and dates, get a weather-appropriate packing list, and if you're missing something for the trip it'll even find things to buy), and a &lt;strong&gt;style learner&lt;/strong&gt; that quietly studies which suggestions I liked and distills durable preferences over time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlai2ky5mg9y06fnf5wp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnlai2ky5mg9y06fnf5wp.png" alt="The Profile's " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What success actually looks like: a warm start, not a verdict
&lt;/h2&gt;

&lt;p&gt;It's worth being clear up front about what this app is &lt;em&gt;for&lt;/em&gt;, because it's easy to assume the goal is an AI that dictates your outfit while you obey. That's not it. It isn't meant to replace my judgment or have the final say on what I wear. It's meant to do two narrower, more useful things.&lt;/p&gt;

&lt;p&gt;First, it gives me a &lt;strong&gt;warm start&lt;/strong&gt;. Cold start is the "I have nothing to wear" paralysis — a blank slate and no idea where to begin. A warm start is being handed a plausible draft to react to. It's the difference between a writer facing an empty page and a writer editing a rough draft, and the second is so much easier. That alone removes most of the daily decision fatigue,&lt;br&gt;
which was the whole point.&lt;/p&gt;

&lt;p&gt;Second, it nudges me toward outfits I wouldn't have assembled on my own. Left to myself I reach for the same handful of safe combinations; a system with a little randomness in it occasionally pairs things I'd never have tried, and sometimes that's the best part. This is the classic &lt;strong&gt;exploration vs. exploitation&lt;/strong&gt; trade-off, balancing the reliable favorite against the worthwhile gamble, and it's a big enough idea that it gets real treatment in Part 2.&lt;/p&gt;

&lt;p&gt;Both goals share a freeing consequence: the app doesn't have to be &lt;em&gt;right&lt;/em&gt;, only &lt;em&gt;useful&lt;/em&gt;. That tolerance is what lets me lean on randomness and learning instead of chasing an impossible standard of correctness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: this isn't one AI, it's three
&lt;/h2&gt;

&lt;p&gt;When I started, I assumed "the AI" would be a single thing: feed it my clothes and the weather, get back an outfit. What I ended up with is three different AI systems, because the three jobs turned out to have different shapes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The daily recommender.&lt;/strong&gt; The thing behind the morning email and the on-demand button. It's actually the &lt;em&gt;least&lt;/em&gt; fancy of the three under the hood: plain, straight-line Python that prepares the right inputs and makes one call to the model. No agent framework, no graph. (Most of its cleverness is in &lt;em&gt;how&lt;/em&gt; it chooses which clothes to even show the model, which is Part 2.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The trip planner.&lt;/strong&gt; Packing is a bigger problem than "what do I wear today," because it can spawn follow-up work: if you're missing something the trip needs, the system should go &lt;em&gt;find&lt;/em&gt; it. That "do a step, then maybe do a different step depending on what you found" shape is what people mean by an &lt;strong&gt;agent&lt;/strong&gt;: a program that decides its own next move at runtime. I built this one on &lt;strong&gt;LangGraph&lt;/strong&gt;, a library for wiring those steps into a graph. (Part 4.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The style learner.&lt;/strong&gt; Once a week, in the background, a second LangGraph reads my whole history of thumbs-up/thumbs-down feedback and tries to distill it into durable preferences ("leans toward neutral basics," that sort of thing). Same tool as the trip planner, completely different job, and, it turns out, completely different risks.(Part 5.)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only remember one thing from this post, make it this: &lt;strong&gt;the three systems share a single design principle&lt;/strong&gt;, and that principle is the spine of everything that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one principle: stochastic for preferences, deterministic for physics
&lt;/h2&gt;

&lt;p&gt;Early on I kept running into the same fork in the road: &lt;em&gt;should this piece of behavior be a hard rule, or a soft nudge?&lt;/em&gt; After enough of those decisions I noticed I was answering them all the same way, and the rule generalized:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use stochastic weights for preferences, and deterministic logic for physics.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me unpack the two words. &lt;strong&gt;Stochastic&lt;/strong&gt; just means "involving randomness": behavior that's probabilistic rather than fixed. &lt;strong&gt;Deterministic&lt;/strong&gt; means the opposite, the same input giving the same output every time, no dice rolls.&lt;/p&gt;

&lt;p&gt;The insight is that the app's inputs split cleanly into two kinds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Preferences&lt;/strong&gt; are soft, fuzzy, and accumulate over time. &lt;em&gt;"She probably doesn't love this shirt."&lt;/em&gt; Getting one of these wrong on any given day is harmless; remember, it's a warm start. So preferences belong in the &lt;strong&gt;stochastic&lt;/strong&gt; parts of the system: things like randomized, weighted sampling, where the wrongness washes out over time and a little randomness is actually a feature (it's what keeps suggestions varied).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Physics&lt;/strong&gt; are hard constraints about the real world. &lt;em&gt;"It is 4°C andraining."&lt;/em&gt; There's nothing fuzzy about that. Recommending shorts isn't a charming bit of variety; it's just broken. So physics belongs in the &lt;strong&gt;deterministic&lt;/strong&gt; parts: explicit filters and gates that run the same way every time and that I can debug when they misfire.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason this matters, and the reason I made it the organizing principle, is that mixing the two up creates the worst kind of bug. Encode a hard physical constraint as a soft probabilistic nudge and you get failures that are &lt;em&gt;unreproducible&lt;/em&gt;: it recommends a parka on a hot day one time in twenty and you can never quite catch it. Encode a soft preference as a rigid rule and the app becomes brittle and samey, suggesting the same "safe" outfit forever. Keeping the two in separate homes is what keeps the system both lively and trustworthy.&lt;/p&gt;

&lt;p&gt;You'll see this principle cash out concretely in every remaining post: in how the recommender randomizes which clothes it considers but hard-filters weather-absurd ones (Part 2), in why thumbs-feedback adjusts probabilities rather than banning items outright (Part 3), and in how the two agents separate "decide what to do" from "do it reliably" (Parts 4 and 5).&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on the stack (because cheap was a constraint)
&lt;/h2&gt;

&lt;p&gt;This is a personal app for exactly one user (me), and I wanted it to cost roughly nothing to run. That shaped a lot of pragmatic choices worth a quick mention: a Python/FastAPI backend, a React frontend, Supabase for the database and photo storage, and Anthropic's Claude models for the AI parts. The one choice I'm quietly proud of is the morning email: instead of paying for a hosted scheduler, the daily job runs on a free &lt;strong&gt;GitHub Actions&lt;/strong&gt; cron, the same automation most people only use for running tests. All in, the whole thing costs me somewhere around &lt;strong&gt;one to two dollars a month&lt;/strong&gt;, almost entirely model usage. That number comes from a dashboard, not a guess: the app meters its own model calls in a small ledger table and shows me the running cost. The constraint of "free" turned out to be a good forcing function for simple architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this series is going
&lt;/h2&gt;

&lt;p&gt;That's the map. Five core posts, each opening on the actual moment or annoyance that drove the work, then the engineering decision it led to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This post:&lt;/strong&gt; the overview and the physics-vs-preference principle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The recommendation engine:&lt;/strong&gt; how it avoids suggesting the same outfit four days in a row (a thing it absolutely did, at first) without ever putting me in a parka in July.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closing the feedback loop:&lt;/strong&gt; why a thumbs-down is trivial to &lt;em&gt;record&lt;/em&gt; and
surprisingly hard to &lt;em&gt;learn from&lt;/em&gt; correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Going agentic with LangGraph:&lt;/strong&gt; the trip planner, and what "agent" actually buys you over a plain function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangGraph again, for learning:&lt;/strong&gt; the weekly style learner, the same tool doing the opposite job, and why a system that quietly changes its own behavior needs very different guardrails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And it won't stop at five: the app is alive, so the series will follow it. Whenever a new piece of the system is worth writing about, it becomes a Part 6, a Part 7, and so on. (The next candidate is already queued: how I built an eval loop to &lt;em&gt;measure&lt;/em&gt; whether the recommender was actually getting better, instead of just feeling like it was.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to poke around?
&lt;/h2&gt;

&lt;p&gt;The code is public at &lt;a href="https://github.com/JiamanBettyWu/mise" rel="noopener noreferrer"&gt;github.com/JiamanBettyWu/mise&lt;/a&gt;. If you enjoy seeing how a project actually gets built (as opposed to how it looks once it's tidied up for a blog post), the repo carries two artifacts I kept while building: a session-by-session journal of what shipped and why (&lt;a href="https://github.com/JiamanBettyWu/mise/blob/9ea8099daa31d76d9d0cc453442216928118402d/SESSIONS.md" rel="noopener noreferrer"&gt;&lt;code&gt;SESSIONS.md&lt;/code&gt;&lt;/a&gt;, and an unpolished, chronological log of every gotcha that bit me (&lt;a href="https://github.com/JiamanBettyWu/mise/blob/main/LEARNINGS.md" rel="noopener noreferrer"&gt;&lt;code&gt;LEARNINGS.md&lt;/code&gt;&lt;/a&gt;. Between them, they're the honest version of this series. Each later post links the specific files and pull requests it describes.&lt;/p&gt;

&lt;p&gt;If the idea of handing a small daily decision to a slightly-unreliable machine sounds interesting, the next post is where it gets concrete. See you there.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
