[{"data":1,"prerenderedAt":2033},["ShallowReactive",2],{"home-latest":3},[4,353,591,954,1416,1921],{"id":5,"title":6,"body":7,"date":344,"description":345,"draft":346,"extension":347,"meta":348,"navigation":64,"path":349,"seo":350,"stem":351,"__hash__":352},"blog\u002Fblog\u002Freal-screenshots-headless-capture.md","Real screenshots, not stock slop — headless source-page capture",{"type":8,"value":9,"toc":339},"minimark",[10,14,22,27,30,241,245,248,291,294,298,301,320,323,335],[11,12,13],"p",{},"Nothing says \"low-effort content\" like generic stock B-roll laid over a tech story. Glowing\nblue circuit boards, a stock-footage hacker in a hoodie. The moment it appears, the viewer\nknows nobody read the source.",[11,15,16,17,21],{},"The fix is nearly free: ",[18,19,20],"strong",{},"show the actual thing."," The GitHub repo. The docs page. The\nrelease notes. The benchmark table from the paper. A headless browser makes that cheap enough\nto do every single time.",[23,24,26],"h2",{"id":25},"the-capture","The capture",[11,28,29],{},"Playwright drives a real Chromium, navigates to the source, and screenshots it:",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-python shiki shiki-themes github-dark github-dark","from playwright.sync_api import sync_playwright\n\ndef capture(url: str, out: str, width=1280, height=1600):\n    with sync_playwright() as p:\n        browser = p.chromium.launch()\n        page = browser.new_page(\n            viewport={\"width\": width, \"height\": height},\n            device_scale_factor=2,  # retina-crisp — non-negotiable for video\n        )\n        page.goto(url, wait_until=\"networkidle\")\n        page.screenshot(path=out, full_page=False)\n        browser.close()\n","python","",[38,39,40,59,66,108,123,134,145,170,188,194,211,235],"code",{"__ignoreMap":36},[41,42,45,49,53,56],"span",{"class":43,"line":44},"line",1,[41,46,48],{"class":47},"sOPea","from",[41,50,52],{"class":51},"suv1-"," playwright.sync_api ",[41,54,55],{"class":47},"import",[41,57,58],{"class":51}," sync_playwright\n",[41,60,62],{"class":43,"line":61},2,[41,63,65],{"emptyLinePlaceholder":64},true,"\n",[41,67,69,72,76,79,83,86,88,91,94,97,100,102,105],{"class":43,"line":68},3,[41,70,71],{"class":47},"def",[41,73,75],{"class":74},"sFR8T"," capture",[41,77,78],{"class":51},"(url: ",[41,80,82],{"class":81},"s8ozJ","str",[41,84,85],{"class":51},", out: ",[41,87,82],{"class":81},[41,89,90],{"class":51},", width",[41,92,93],{"class":47},"=",[41,95,96],{"class":81},"1280",[41,98,99],{"class":51},", height",[41,101,93],{"class":47},[41,103,104],{"class":81},"1600",[41,106,107],{"class":51},"):\n",[41,109,111,114,117,120],{"class":43,"line":110},4,[41,112,113],{"class":47},"    with",[41,115,116],{"class":51}," sync_playwright() ",[41,118,119],{"class":47},"as",[41,121,122],{"class":51}," p:\n",[41,124,126,129,131],{"class":43,"line":125},5,[41,127,128],{"class":51},"        browser ",[41,130,93],{"class":47},[41,132,133],{"class":51}," p.chromium.launch()\n",[41,135,137,140,142],{"class":43,"line":136},6,[41,138,139],{"class":51},"        page ",[41,141,93],{"class":47},[41,143,144],{"class":51}," browser.new_page(\n",[41,146,148,152,154,157,161,164,167],{"class":43,"line":147},7,[41,149,151],{"class":150},"s-3mD","            viewport",[41,153,93],{"class":47},[41,155,156],{"class":51},"{",[41,158,160],{"class":159},"s4wv1","\"width\"",[41,162,163],{"class":51},": width, ",[41,165,166],{"class":159},"\"height\"",[41,168,169],{"class":51},": height},\n",[41,171,173,176,178,181,184],{"class":43,"line":172},8,[41,174,175],{"class":150},"            device_scale_factor",[41,177,93],{"class":47},[41,179,180],{"class":81},"2",[41,182,183],{"class":51},",  ",[41,185,187],{"class":186},"sJ8bj","# retina-crisp — non-negotiable for video\n",[41,189,191],{"class":43,"line":190},9,[41,192,193],{"class":51},"        )\n",[41,195,197,200,203,205,208],{"class":43,"line":196},10,[41,198,199],{"class":51},"        page.goto(url, ",[41,201,202],{"class":150},"wait_until",[41,204,93],{"class":47},[41,206,207],{"class":159},"\"networkidle\"",[41,209,210],{"class":51},")\n",[41,212,214,217,220,222,225,228,230,233],{"class":43,"line":213},11,[41,215,216],{"class":51},"        page.screenshot(",[41,218,219],{"class":150},"path",[41,221,93],{"class":47},[41,223,224],{"class":51},"out, ",[41,226,227],{"class":150},"full_page",[41,229,93],{"class":47},[41,231,232],{"class":81},"False",[41,234,210],{"class":51},[41,236,238],{"class":43,"line":237},12,[41,239,240],{"class":51},"        browser.close()\n",[23,242,244],{"id":243},"the-touches-that-separate-it-from-a-scrape","The touches that separate it from a scrape",[11,246,247],{},"A raw screenshot still looks scraped. Four things make it look intentional:",[249,250,251,261,271,281],"ul",{},[252,253,254,260],"li",{},[18,255,256,259],{},[38,257,258],{},"device_scale_factor=2","."," Capture at 2x and your screenshots are crisp when a video\nscales them up. A blurry screenshot is worse than no screenshot.",[252,262,263,266,267,270],{},[18,264,265],{},"Wait for the real content."," ",[38,268,269],{},"wait_until=\"networkidle\""," — or better, wait for a specific\nselector — so you don't capture a half-loaded skeleton.",[252,272,273,276,277,280],{},[18,274,275],{},"Kill the cookie banners."," A GDPR overlay in your shot screams \"I scraped this.\" Inject\nCSS to hide ",[38,278,279],{},"[id*=\"cookie\"], [class*=\"consent\"]",", or click the dismiss button before you\nshoot.",[252,282,283,286,287,290],{},[18,284,285],{},"Crop to what matters."," Don't screenshot the whole page and zoom in post. Grab the\nelement: ",[38,288,289],{},"page.locator(\"table.benchmarks\").screenshot(path=out)",". Frame the diff, the\ntable, the one paragraph you're talking about.",[11,292,293],{},"Then, in the video, a slow sub-pixel zoom across the screenshot (a Ken Burns move) reads as\ndeliberate instead of a static slide.",[23,295,297],{"id":296},"be-a-good-citizen","Be a good citizen",[11,299,300],{},"Headless capture is scraping, so behave like it:",[249,302,303,310,317],{},[252,304,305,306,309],{},"Set a real ",[38,307,308],{},"user_agent"," and don't hammer — you're grabbing one image, not crawling a site.",[252,311,312,313,316],{},"Respect ",[38,314,315],{},"robots.txt"," and terms of service. Public docs and repos are fair game; gated or\nauth-walled content is not.",[252,318,319],{},"Cache captures. The same release-notes page doesn't need re-shooting every run.",[321,322],"hr",{},[11,324,325,326,330,331,334],{},"The difference between content that looks ",[327,328,329],"em",{},"researched"," and content that looks ",[327,332,333],{},"generated"," is\noften a single question: did you show the actual source? Headless capture makes the honest\nanswer — yes — cheap enough that there's no excuse not to.",[336,337,338],"style",{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":61,"depth":61,"links":340},[341,342,343],{"id":25,"depth":61,"text":26},{"id":243,"depth":61,"text":244},{"id":296,"depth":61,"text":297},"2026-06-17","Stock footage screams content farm. Capturing the actual source — the repo, the docs, the release notes — with a headless browser makes the output look like it came from someone who read the thing.",false,"md",{},"\u002Fblog\u002Freal-screenshots-headless-capture",{"title":6,"description":345},"blog\u002Freal-screenshots-headless-capture","MPd3R8hK_7a__ZqBgR1BzWUmBuCzvieLkfDDo4iP4t8",{"id":354,"title":355,"body":356,"date":344,"description":585,"draft":346,"extension":347,"meta":586,"navigation":64,"path":587,"seo":588,"stem":589,"__hash__":590},"blog\u002Fblog\u002Fyour-own-voice-with-forced-alignment.md","Record your own voice — forced alignment instead of paying for TTS",{"type":8,"value":357,"toc":580},[358,361,368,374,378,384,501,504,508,515,538,541,545,565,567,574,577],[11,359,360],{},"The quickest way for a viewer to clock \"this is AI content\" is the voice. Even good\ntext-to-speech has a tell — a flatness, a wrong stress on the third word of every clause.\nRecording your own narration fixes it instantly.",[11,362,363,364,367],{},"But it costs you the thing that made TTS attractive in an automated pipeline: TTS hands you\nexact word timings for free. Record yourself and you've got a ",[38,365,366],{},".wav"," and no idea when each\nword lands — which you need for captions, cut timing, and trimming dead air.",[11,369,370,373],{},[18,371,372],{},"Forced alignment"," gives that back. Feed it an audio file and the transcript (you already\nhave the script you read), and it returns word-level timestamps.",[23,375,377],{"id":376},"the-setup","The setup",[11,379,380,383],{},[38,381,382],{},"faster-whisper"," will transcribe with word timings out of the box, and for short-form that's\nusually close enough:",[31,385,387],{"className":33,"code":386,"language":35,"meta":36,"style":36},"from faster_whisper import WhisperModel\n\nmodel = WhisperModel(\"base\", compute_type=\"int8\")  # int8 = runs fine on CPU\nsegments, _ = model.transcribe(\"narration.wav\", word_timestamps=True)\n\nwords = [(w.word, w.start, w.end) for seg in segments for w in seg.words]\n# [('The', 0.00, 0.18), ('seen', 0.18, 0.42), ('store', 0.42, 0.71), ...]\n",[38,388,389,401,405,435,460,464,496],{"__ignoreMap":36},[41,390,391,393,396,398],{"class":43,"line":44},[41,392,48],{"class":47},[41,394,395],{"class":51}," faster_whisper ",[41,397,55],{"class":47},[41,399,400],{"class":51}," WhisperModel\n",[41,402,403],{"class":43,"line":61},[41,404,65],{"emptyLinePlaceholder":64},[41,406,407,410,412,415,418,421,424,426,429,432],{"class":43,"line":68},[41,408,409],{"class":51},"model ",[41,411,93],{"class":47},[41,413,414],{"class":51}," WhisperModel(",[41,416,417],{"class":159},"\"base\"",[41,419,420],{"class":51},", ",[41,422,423],{"class":150},"compute_type",[41,425,93],{"class":47},[41,427,428],{"class":159},"\"int8\"",[41,430,431],{"class":51},")  ",[41,433,434],{"class":186},"# int8 = runs fine on CPU\n",[41,436,437,440,442,445,448,450,453,455,458],{"class":43,"line":110},[41,438,439],{"class":51},"segments, _ ",[41,441,93],{"class":47},[41,443,444],{"class":51}," model.transcribe(",[41,446,447],{"class":159},"\"narration.wav\"",[41,449,420],{"class":51},[41,451,452],{"class":150},"word_timestamps",[41,454,93],{"class":47},[41,456,457],{"class":81},"True",[41,459,210],{"class":51},[41,461,462],{"class":43,"line":125},[41,463,65],{"emptyLinePlaceholder":64},[41,465,466,469,471,474,477,480,483,486,488,491,493],{"class":43,"line":136},[41,467,468],{"class":51},"words ",[41,470,93],{"class":47},[41,472,473],{"class":51}," [(w.word, w.start, w.end) ",[41,475,476],{"class":47},"for",[41,478,479],{"class":51}," seg ",[41,481,482],{"class":47},"in",[41,484,485],{"class":51}," segments ",[41,487,476],{"class":47},[41,489,490],{"class":51}," w ",[41,492,482],{"class":47},[41,494,495],{"class":51}," seg.words]\n",[41,497,498],{"class":43,"line":147},[41,499,500],{"class":186},"# [('The', 0.00, 0.18), ('seen', 0.18, 0.42), ('store', 0.42, 0.71), ...]\n",[11,502,503],{},"Now you can place captions to the frame, time a visual cut to the end of a phrase, or trim\nthe silence between takes — automatically, from your real voice.",[23,505,507],{"id":506},"transcription-vs-true-alignment","Transcription vs. true alignment",[11,509,510,511,514],{},"Worth being precise, because it bites people: Whisper ",[327,512,513],{},"transcribes"," — it decides what was\nsaid and when. It can mishear, and its text won't exactly match your script.",[249,516,517,524],{},[252,518,519,520,523],{},"For ",[18,521,522],{},"captions and rough timing",", Whisper's word timestamps are close enough. Ship it.",[252,525,519,526,529,530,533,534,537],{},[18,527,528],{},"true forced alignment to a known script"," — you have the exact text and want every\nword of ",[327,531,532],{},"that text"," pinned to the audio — use a dedicated aligner (",[38,535,536],{},"aeneas",", or a wav2vec2\nforced-alignment model). It aligns to your words instead of guessing them.",[11,539,540],{},"Most short-form work lives in the first bucket. Reach for the second only when an exact-text\ncaption has to be perfect.",[23,542,544],{"id":543},"the-gotchas","The gotchas",[249,546,547,553,559],{},[252,548,549,552],{},[18,550,551],{},"Numbers and symbols"," get spoken differently than written (\"2026\" → \"twenty twenty-six\").\nIf you're matching against a script, normalize both sides first.",[252,554,555,558],{},[18,556,557],{},"Retakes and filler"," wreck alignment. One clean take per line, minimal \"um,\" beats one\nlong messy take you fix in post.",[252,560,561,564],{},[18,562,563],{},"Room noise"," drags accuracy down. A cheap mic in a quiet room beats a good mic in a live\none.",[321,566],{},[11,568,569,570,573],{},"The payoff is the whole point: your real voice ",[327,571,572],{},"and"," the automated timing. You don't have to\ntrade the human element for the pipeline.",[11,575,576],{},"And that's the transferable lesson — don't accept the synthetic default just because it's\neasier to wire up. There's often an alignment trick that lets you keep the part a human\nshould do and automate everything around it.",[336,578,579],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":61,"depth":61,"links":581},[582,583,584],{"id":376,"depth":61,"text":377},{"id":506,"depth":61,"text":507},{"id":543,"depth":61,"text":544},"Synthetic narration is the fastest tell that something is AI slop. Narrate in your own voice and still automate the timing — forced alignment maps your audio to the script for frame-accurate captions, free.",{},"\u002Fblog\u002Fyour-own-voice-with-forced-alignment",{"title":355,"description":585},"blog\u002Fyour-own-voice-with-forced-alignment","5oldqgydfjaGsqzKW32ckk86SN5tqwtb3c7gO4RjGU8",{"id":592,"title":593,"body":594,"date":947,"description":948,"draft":346,"extension":347,"meta":949,"navigation":64,"path":950,"seo":951,"stem":952,"__hash__":953},"blog\u002Fblog\u002Fwhat-production-means-for-an-llm-pipeline.md","What \"production\" actually means for an LLM pipeline",{"type":8,"value":595,"toc":939},[596,599,602,606,613,616,620,623,831,842,846,855,859,862,907,910,914,917,921,924,926,933,936],[11,597,598],{},"The gap between a working demo and a system you can leave running is not the model. The\nmodel is the easy part — it works in the demo. The gap is all the plumbing the demo got to\nskip because you were standing right there when it ran.",[11,600,601],{},"Here's the field guide to the boring parts, ordered by reliability bought per line of code.",[23,603,605],{"id":604},"_1-validate-the-models-output-never-trust-free-text","1. Validate the model's output — never trust free text",[11,607,608,609,612],{},"The model ",[327,610,611],{},"will"," return garbage eventually: a missing field, prose wrapped around your JSON,\nan empty string. If your pipeline assumes well-formed output, it dies on the first bad one —\nat 3am, unattended.",[11,614,615],{},"Parse, validate against a schema, and retry on failure. Better, force structured output with\na typed tool so malformed responses can't happen in the first place. The rule: the boundary\nbetween the model and the rest of your code is untrusted input. Treat it like any other.",[23,617,619],{"id":618},"_2-retries-with-backoff","2. Retries with backoff",[11,621,622],{},"Model APIs rate-limit and blip. A single 429 should not kill a run.",[31,624,626],{"className":33,"code":625,"language":35,"meta":36,"style":36},"import time, random\n\nRETRYABLE = {408, 409, 429, 500, 502, 503, 529}\n\ndef with_retry(fn, *, attempts=5, base=1.0):\n    for n in range(attempts):\n        try:\n            return fn()\n        except APIStatusError as e:\n            if e.status_code not in RETRYABLE or n == attempts - 1:\n                raise\n            time.sleep(base * 2 ** n + random.random())  # exponential backoff + jitter\n",[38,627,628,635,639,686,690,721,737,745,753,766,802,807],{"__ignoreMap":36},[41,629,630,632],{"class":43,"line":44},[41,631,55],{"class":47},[41,633,634],{"class":51}," time, random\n",[41,636,637],{"class":43,"line":61},[41,638,65],{"emptyLinePlaceholder":64},[41,640,641,644,647,650,653,655,658,660,663,665,668,670,673,675,678,680,683],{"class":43,"line":68},[41,642,643],{"class":81},"RETRYABLE",[41,645,646],{"class":47}," =",[41,648,649],{"class":51}," {",[41,651,652],{"class":81},"408",[41,654,420],{"class":51},[41,656,657],{"class":81},"409",[41,659,420],{"class":51},[41,661,662],{"class":81},"429",[41,664,420],{"class":51},[41,666,667],{"class":81},"500",[41,669,420],{"class":51},[41,671,672],{"class":81},"502",[41,674,420],{"class":51},[41,676,677],{"class":81},"503",[41,679,420],{"class":51},[41,681,682],{"class":81},"529",[41,684,685],{"class":51},"}\n",[41,687,688],{"class":43,"line":110},[41,689,65],{"emptyLinePlaceholder":64},[41,691,692,694,697,700,703,706,708,711,714,716,719],{"class":43,"line":125},[41,693,71],{"class":47},[41,695,696],{"class":74}," with_retry",[41,698,699],{"class":51},"(fn, ",[41,701,702],{"class":47},"*",[41,704,705],{"class":51},", attempts",[41,707,93],{"class":47},[41,709,710],{"class":81},"5",[41,712,713],{"class":51},", base",[41,715,93],{"class":47},[41,717,718],{"class":81},"1.0",[41,720,107],{"class":51},[41,722,723,726,729,731,734],{"class":43,"line":136},[41,724,725],{"class":47},"    for",[41,727,728],{"class":51}," n ",[41,730,482],{"class":47},[41,732,733],{"class":81}," range",[41,735,736],{"class":51},"(attempts):\n",[41,738,739,742],{"class":43,"line":147},[41,740,741],{"class":47},"        try",[41,743,744],{"class":51},":\n",[41,746,747,750],{"class":43,"line":172},[41,748,749],{"class":47},"            return",[41,751,752],{"class":51}," fn()\n",[41,754,755,758,761,763],{"class":43,"line":190},[41,756,757],{"class":47},"        except",[41,759,760],{"class":51}," APIStatusError ",[41,762,119],{"class":47},[41,764,765],{"class":51}," e:\n",[41,767,768,771,774,777,780,783,786,788,791,794,797,800],{"class":43,"line":196},[41,769,770],{"class":47},"            if",[41,772,773],{"class":51}," e.status_code ",[41,775,776],{"class":47},"not",[41,778,779],{"class":47}," in",[41,781,782],{"class":81}," RETRYABLE",[41,784,785],{"class":47}," or",[41,787,728],{"class":51},[41,789,790],{"class":47},"==",[41,792,793],{"class":51}," attempts ",[41,795,796],{"class":47},"-",[41,798,799],{"class":81}," 1",[41,801,744],{"class":51},[41,803,804],{"class":43,"line":213},[41,805,806],{"class":47},"                raise\n",[41,808,809,812,814,817,820,822,825,828],{"class":43,"line":237},[41,810,811],{"class":51},"            time.sleep(base ",[41,813,702],{"class":47},[41,815,816],{"class":81}," 2",[41,818,819],{"class":47}," **",[41,821,728],{"class":51},[41,823,824],{"class":47},"+",[41,826,827],{"class":51}," random.random())  ",[41,829,830],{"class":186},"# exponential backoff + jitter\n",[11,832,833,834,837,838,841],{},"Distinguish ",[18,835,836],{},"retryable"," (rate limits, overloads, timeouts) from ",[18,839,840],{},"fatal"," (a 400 — your\nrequest is malformed and will be malformed every time). Retrying a fatal error just burns\ntime and money.",[23,843,845],{"id":844},"_3-idempotency-assume-every-run-can-die-halfway","3. Idempotency — assume every run can die halfway",[11,847,848,849,854],{},"A scheduled job that crashes gets retried, which means every step runs twice sometimes.\nDesign so repeating a step is safe: guard irreversible actions with a\n",[850,851,853],"a",{"href":852},"\u002Fblog\u002Fthe-seen-store","seen-store",", use idempotency keys, write to temp files and rename.\n\"Exactly once\" is a distributed-systems fantasy; \"at least once, safely\" is achievable and\nenough.",[23,856,858],{"id":857},"_4-cost-tracking","4. Cost tracking",[11,860,861],{},"If you're not logging tokens and dollars per run, you find out what your pipeline costs from\nthe bill. It's a counter:",[31,863,865],{"className":33,"code":864,"language":35,"meta":36,"style":36},"usage = resp.usage  # input_tokens, output_tokens\nrun_cost += usage.input_tokens * IN_RATE + usage.output_tokens * OUT_RATE\n",[38,866,867,880],{"__ignoreMap":36},[41,868,869,872,874,877],{"class":43,"line":44},[41,870,871],{"class":51},"usage ",[41,873,93],{"class":47},[41,875,876],{"class":51}," resp.usage  ",[41,878,879],{"class":186},"# input_tokens, output_tokens\n",[41,881,882,885,888,891,893,896,899,902,904],{"class":43,"line":61},[41,883,884],{"class":51},"run_cost ",[41,886,887],{"class":47},"+=",[41,889,890],{"class":51}," usage.input_tokens ",[41,892,702],{"class":47},[41,894,895],{"class":81}," IN_RATE",[41,897,898],{"class":47}," +",[41,900,901],{"class":51}," usage.output_tokens ",[41,903,702],{"class":47},[41,905,906],{"class":81}," OUT_RATE\n",[11,908,909],{},"Log it per run with the run id. The day a prompt change quietly triples your token use, this\nis how you catch it in hours instead of at the end of the month.",[23,911,913],{"id":912},"_5-a-kill-switch","5. A kill switch",[11,915,916],{},"A runaway loop — a retry storm, a pagination bug, a model that keeps asking for \"one more\nstep\" — can spend real money fast. Put a ceiling on it: a per-run budget cap, a max-iteration\ncount, a circuit breaker that halts when the error rate spikes. Cheap insurance against the\n3am surprise.",[23,918,920],{"id":919},"_6-observability-you-can-grep","6. Observability you can grep",[11,922,923],{},"When it breaks, you get logs and nothing else. Make them worth having: structured lines with\na run id, the inputs, the decision, the output. \"Something failed\" is useless; \"run a3f scored\nitem X at 2, below threshold, skipped\" tells you whether the bug is the model or your code.",[321,925],{},[11,927,928,929,932],{},"The mindset under all six: ",[18,930,931],{},"assume every external call fails, every model output is suspect,\nand every run can be interrupted and resumed."," Build for that and you can actually walk away\nfrom the thing.",[11,934,935],{},"\"Production\" isn't a deploy target. It's just the set of failures you've already handled — and\nthe demo has handled none of them. Start with output validation and retries; they buy the most\nreliability per line. The rest you add the first time each one bites you.",[336,937,938],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":61,"depth":61,"links":940},[941,942,943,944,945,946],{"id":604,"depth":61,"text":605},{"id":618,"depth":61,"text":619},{"id":844,"depth":61,"text":845},{"id":857,"depth":61,"text":858},{"id":912,"depth":61,"text":913},{"id":919,"depth":61,"text":920},"2026-06-16","The demo works on your laptop. Production is everything the demo skipped — retries, idempotency, output validation, cost tracking, and what to do when the model returns garbage at 3am.",{},"\u002Fblog\u002Fwhat-production-means-for-an-llm-pipeline",{"title":593,"description":948},"blog\u002Fwhat-production-means-for-an-llm-pipeline","bfTzu0x32Bg73Mal_viiEg0sAZzv1ssi3EkMO9dPfdo",{"id":955,"title":956,"body":957,"date":1410,"description":1411,"draft":346,"extension":347,"meta":1412,"navigation":64,"path":852,"seo":1413,"stem":1414,"__hash__":1415},"blog\u002Fblog\u002Fthe-seen-store.md","The seen-store — give your agent a memory so it stops repeating itself",{"type":8,"value":958,"toc":1404},[959,962,969,973,979,1290,1293,1297,1311,1343,1346,1350,1353,1374,1385,1389,1392,1394,1401],[11,960,961],{},"The moment you put an agent on a schedule, the question stops being \"can it do the task\"\nand becomes \"will it do the task it already did.\" A pipeline that runs every morning and\ncan't remember yesterday will re-post the same story, re-render the same video, re-email\nthe same person — confidently, on schedule, forever.",[11,963,964,965,968],{},"The fix is the cheapest reliability primitive in an autonomous system, and almost everyone\nadds it ",[327,966,967],{},"after"," the agent embarrasses them. Add it first.",[23,970,972],{"id":971},"the-seen-store","The seen-store",[11,974,975,976],{},"A seen-store is a persistent set of keys you've already acted on. The rule is two lines of\nEnglish: ",[18,977,978],{},"check before you act, record after.",[31,980,982],{"className":33,"code":981,"language":35,"meta":36,"style":36},"import json, hashlib, pathlib\n\nSEEN = pathlib.Path(\"seen.json\")\n\ndef _load() -> set[str]:\n    return set(json.loads(SEEN.read_text())) if SEEN.exists() else set()\n\ndef _save(keys: set[str]) -> None:\n    SEEN.write_text(json.dumps(sorted(keys)))\n\ndef key_for(item: dict) -> str:\n    # Stable across runs: canonical URL beats title — titles get edited.\n    basis = item.get(\"url\") or item[\"title\"]\n    return hashlib.sha256(basis.strip().lower().encode()).hexdigest()[:16]\n\ndef unseen(items: list[dict]) -> list[dict]:\n    seen = _load()\n    return [i for i in items if key_for(i) not in seen]\n\ndef mark(items: list[dict]) -> None:\n    seen = _load()\n    seen.update(key_for(i) for i in items)\n    _save(seen)\n",[38,983,984,991,995,1010,1014,1029,1062,1066,1086,1100,1104,1124,1129,1158,1171,1176,1196,1207,1237,1242,1260,1269,1284],{"__ignoreMap":36},[41,985,986,988],{"class":43,"line":44},[41,987,55],{"class":47},[41,989,990],{"class":51}," json, hashlib, pathlib\n",[41,992,993],{"class":43,"line":61},[41,994,65],{"emptyLinePlaceholder":64},[41,996,997,1000,1002,1005,1008],{"class":43,"line":68},[41,998,999],{"class":81},"SEEN",[41,1001,646],{"class":47},[41,1003,1004],{"class":51}," pathlib.Path(",[41,1006,1007],{"class":159},"\"seen.json\"",[41,1009,210],{"class":51},[41,1011,1012],{"class":43,"line":110},[41,1013,65],{"emptyLinePlaceholder":64},[41,1015,1016,1018,1021,1024,1026],{"class":43,"line":125},[41,1017,71],{"class":47},[41,1019,1020],{"class":74}," _load",[41,1022,1023],{"class":51},"() -> set[",[41,1025,82],{"class":81},[41,1027,1028],{"class":51},"]:\n",[41,1030,1031,1034,1037,1040,1042,1045,1048,1051,1054,1057,1059],{"class":43,"line":136},[41,1032,1033],{"class":47},"    return",[41,1035,1036],{"class":81}," set",[41,1038,1039],{"class":51},"(json.loads(",[41,1041,999],{"class":81},[41,1043,1044],{"class":51},".read_text())) ",[41,1046,1047],{"class":47},"if",[41,1049,1050],{"class":81}," SEEN",[41,1052,1053],{"class":51},".exists() ",[41,1055,1056],{"class":47},"else",[41,1058,1036],{"class":81},[41,1060,1061],{"class":51},"()\n",[41,1063,1064],{"class":43,"line":147},[41,1065,65],{"emptyLinePlaceholder":64},[41,1067,1068,1070,1073,1076,1078,1081,1084],{"class":43,"line":172},[41,1069,71],{"class":47},[41,1071,1072],{"class":74}," _save",[41,1074,1075],{"class":51},"(keys: set[",[41,1077,82],{"class":81},[41,1079,1080],{"class":51},"]) -> ",[41,1082,1083],{"class":81},"None",[41,1085,744],{"class":51},[41,1087,1088,1091,1094,1097],{"class":43,"line":190},[41,1089,1090],{"class":81},"    SEEN",[41,1092,1093],{"class":51},".write_text(json.dumps(",[41,1095,1096],{"class":81},"sorted",[41,1098,1099],{"class":51},"(keys)))\n",[41,1101,1102],{"class":43,"line":196},[41,1103,65],{"emptyLinePlaceholder":64},[41,1105,1106,1108,1111,1114,1117,1120,1122],{"class":43,"line":213},[41,1107,71],{"class":47},[41,1109,1110],{"class":74}," key_for",[41,1112,1113],{"class":51},"(item: ",[41,1115,1116],{"class":81},"dict",[41,1118,1119],{"class":51},") -> ",[41,1121,82],{"class":81},[41,1123,744],{"class":51},[41,1125,1126],{"class":43,"line":237},[41,1127,1128],{"class":186},"    # Stable across runs: canonical URL beats title — titles get edited.\n",[41,1130,1132,1135,1137,1140,1143,1146,1149,1152,1155],{"class":43,"line":1131},13,[41,1133,1134],{"class":51},"    basis ",[41,1136,93],{"class":47},[41,1138,1139],{"class":51}," item.get(",[41,1141,1142],{"class":159},"\"url\"",[41,1144,1145],{"class":51},") ",[41,1147,1148],{"class":47},"or",[41,1150,1151],{"class":51}," item[",[41,1153,1154],{"class":159},"\"title\"",[41,1156,1157],{"class":51},"]\n",[41,1159,1161,1163,1166,1169],{"class":43,"line":1160},14,[41,1162,1033],{"class":47},[41,1164,1165],{"class":51}," hashlib.sha256(basis.strip().lower().encode()).hexdigest()[:",[41,1167,1168],{"class":81},"16",[41,1170,1157],{"class":51},[41,1172,1174],{"class":43,"line":1173},15,[41,1175,65],{"emptyLinePlaceholder":64},[41,1177,1179,1181,1184,1187,1189,1192,1194],{"class":43,"line":1178},16,[41,1180,71],{"class":47},[41,1182,1183],{"class":74}," unseen",[41,1185,1186],{"class":51},"(items: list[",[41,1188,1116],{"class":81},[41,1190,1191],{"class":51},"]) -> list[",[41,1193,1116],{"class":81},[41,1195,1028],{"class":51},[41,1197,1199,1202,1204],{"class":43,"line":1198},17,[41,1200,1201],{"class":51},"    seen ",[41,1203,93],{"class":47},[41,1205,1206],{"class":51}," _load()\n",[41,1208,1210,1212,1215,1217,1220,1222,1225,1227,1230,1232,1234],{"class":43,"line":1209},18,[41,1211,1033],{"class":47},[41,1213,1214],{"class":51}," [i ",[41,1216,476],{"class":47},[41,1218,1219],{"class":51}," i ",[41,1221,482],{"class":47},[41,1223,1224],{"class":51}," items ",[41,1226,1047],{"class":47},[41,1228,1229],{"class":51}," key_for(i) ",[41,1231,776],{"class":47},[41,1233,779],{"class":47},[41,1235,1236],{"class":51}," seen]\n",[41,1238,1240],{"class":43,"line":1239},19,[41,1241,65],{"emptyLinePlaceholder":64},[41,1243,1245,1247,1250,1252,1254,1256,1258],{"class":43,"line":1244},20,[41,1246,71],{"class":47},[41,1248,1249],{"class":74}," mark",[41,1251,1186],{"class":51},[41,1253,1116],{"class":81},[41,1255,1080],{"class":51},[41,1257,1083],{"class":81},[41,1259,744],{"class":51},[41,1261,1263,1265,1267],{"class":43,"line":1262},21,[41,1264,1201],{"class":51},[41,1266,93],{"class":47},[41,1268,1206],{"class":51},[41,1270,1272,1275,1277,1279,1281],{"class":43,"line":1271},22,[41,1273,1274],{"class":51},"    seen.update(key_for(i) ",[41,1276,476],{"class":47},[41,1278,1219],{"class":51},[41,1280,482],{"class":47},[41,1282,1283],{"class":51}," items)\n",[41,1285,1287],{"class":43,"line":1286},23,[41,1288,1289],{"class":51},"    _save(seen)\n",[11,1291,1292],{},"That's the whole idea. A JSON file is fine until it isn't; swap it for SQLite or Redis when\nyou outgrow it and the interface stays the same.",[23,1294,1296],{"id":1295},"the-key-is-the-actual-hard-part","The key is the actual hard part",[11,1298,1299,1302,1303,1306,1307,1310],{},[38,1300,1301],{},"unseen()"," is trivial. ",[38,1304,1305],{},"key_for()"," is where the bodies are buried. The key has to be\n",[18,1308,1309],{},"stable"," — the same logical item must hash to the same key on every run.",[249,1312,1313,1319,1337],{},[252,1314,1315,1318],{},[18,1316,1317],{},"Don't key on the title."," Titles get re-edited; a one-character change and the item\nlooks brand new.",[252,1320,1321,1324,1325,1328,1329,1332,1333,1336],{},[18,1322,1323],{},"Canonicalize URLs"," before hashing — strip ",[38,1326,1327],{},"utm_"," params, trailing slashes, fragments.\n",[38,1330,1331],{},"example.com\u002Fpost"," and ",[38,1334,1335],{},"example.com\u002Fpost?utm_source=rss"," are the same story.",[252,1338,1339,1342],{},[18,1340,1341],{},"Content-hash when there's no stable id"," — for near-duplicates (the same story from two\noutlets), hash a normalized chunk of the body, not the headline.",[11,1344,1345],{},"A bad key gives you one of two failures: too loose and you re-do work, too tight and you\nsilently drop new items. Spend your time here.",[23,1347,1349],{"id":1348},"record-before-or-after","Record before or after?",[11,1351,1352],{},"This is the question that decides whether your seen-store is an optimization or a\ncorrectness guarantee.",[249,1354,1355,1365],{},[252,1356,1357,1360,1361,1364],{},[18,1358,1359],{},"Record after success"," and a crash ",[327,1362,1363],{},"between acting and recording"," gives you a duplicate\nnext run.",[252,1366,1367,1360,1370,1373],{},[18,1368,1369],{},"Record before acting",[327,1371,1372],{},"between recording and acting"," drops the item\nforever.",[11,1375,1376,1377,1380,1381,1384],{},"For reversible work (rendering a file you'll overwrite anyway), record-after and accept the\noccasional repeat. For ",[18,1378,1379],{},"irreversible side effects — sending an email, posting a video,\ncharging a card"," — the seen-store ",[327,1382,1383],{},"is"," your correctness boundary: guard the irreversible\ncall, and lean toward at-least-once with a downstream dedupe rather than at-most-once that\ncan silently drop. A duplicate is embarrassing; a dropped payment is a bug report.",[23,1386,1388],{"id":1387},"keep-it-bounded","Keep it bounded",[11,1390,1391],{},"A seen-store that only grows is a slow leak. Cap it: a TTL (drop keys older than N days), or\na max size with FIFO eviction. For most content pipelines, \"anything older than 30 days\nwon't resurface anyway\" is a fine pruning rule.",[321,1393],{},[11,1395,1396,1397,1400],{},"Build the seen-store before the agent does something twice in public. It's twenty lines, it's\nthe difference between \"runs unattended\" and \"runs unattended until it doesn't,\" and it's the\nfoundation everything else in ",[850,1398,1399],{"href":950},"a production pipeline","\nstands on.",[336,1402,1403],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":61,"depth":61,"links":1405},[1406,1407,1408,1409],{"id":971,"depth":61,"text":972},{"id":1295,"depth":61,"text":1296},{"id":1348,"depth":61,"text":1349},{"id":1387,"depth":61,"text":1388},"2026-06-15","An autonomous pipeline that can't remember what it already did will, eventually, do it again. The fix is twenty lines, not a database migration.",{},{"title":956,"description":1411},"blog\u002Fthe-seen-store","1YZwEh3PUOQrn7YUBdIf9TZRJHzheoUQALEPb-AcxeU",{"id":1417,"title":1418,"body":1419,"date":1914,"description":1915,"draft":346,"extension":347,"meta":1916,"navigation":64,"path":1917,"seo":1918,"stem":1919,"__hash__":1920},"blog\u002Fblog\u002Fuse-claude-as-a-scorer.md","Use Claude as a scorer, not just a generator",{"type":8,"value":1420,"toc":1906},[1421,1428,1438,1442,1445,1459,1463,1466,1762,1765,1769,1776,1806,1810,1813,1849,1853,1864,1887,1890,1894,1900,1903],[11,1422,1423,1424,1427],{},"Everyone's first instinct with an LLM is to make it ",[327,1425,1426],{},"write"," something. Generate the\npost, generate the summary, generate the reply. That's the flashy half. The half that\nactually keeps an automated pipeline from shipping garbage is the boring one:",[11,1429,1430,1433,1434,1437],{},[18,1431,1432],{},"Use the model as a scorer."," A function from an item to a number. ",[38,1435,1436],{},"f(item) → score",".\nThen sort, filter, and only spend your expensive generation step on the things that\nearned it.",[23,1439,1441],{"id":1440},"why-scoring-beats-rules","Why scoring beats rules",[11,1443,1444],{},"You already know how to filter with code. Keyword lists, regexes, recency windows,\nsource allowlists. They're fast and free and you should still use them as a first pass.",[11,1446,1447,1448,1451,1452,1454,1455,1458],{},"But the judgments that matter in a content pipeline aren't keyword-shaped. ",[327,1449,1450],{},"Is this\nstory actually newsworthy to a working engineer, or is it recycled hype with a new\nheadline?"," No regex answers that. A model can — that's exactly the fuzzy, context-heavy\ncall LLMs are good at. The trick is to stop asking it to ",[327,1453,1426],{}," about the story and\nstart asking it to ",[327,1456,1457],{},"judge"," the story.",[23,1460,1462],{"id":1461},"the-pattern","The pattern",[11,1464,1465],{},"Score every candidate, keep the top N. Here's the whole thing in Python with the\nAnthropic SDK:",[31,1467,1469],{"className":33,"code":1468,"language":35,"meta":36,"style":36},"import anthropic, json\n\nclient = anthropic.Anthropic()\n\nRUBRIC = \"\"\"You score stories for an audience of working AI engineers, 1-5:\n5 = they'd stop scrolling and read it today\n4 = solid, relevant, shippable\n3 = mildly interesting, not urgent\n2 = recycled or thin\n1 = noise \u002F pure hype\n\nReturn ONLY JSON: {\"score\": \u003Cint 1-5>, \"reason\": \"\u003Cone line>\"}\"\"\"\n\ndef score(item: dict) -> dict:\n    msg = client.messages.create(\n        model=\"claude-haiku-4-5\",   # small, fast, cheap — use the current Haiku id\n        max_tokens=100,\n        temperature=0,              # scoring is not a place for creativity\n        system=RUBRIC,\n        messages=[{\"role\": \"user\", \"content\": f\"{item['title']}\\n\\n{item['summary']}\"}],\n    )\n    return json.loads(msg.content[0].text)\n\nranked = sorted(candidates, key=lambda i: score(i)[\"score\"], reverse=True)\ntop = ranked[:5]\n",[38,1470,1471,1478,1482,1492,1496,1506,1511,1516,1521,1526,1531,1535,1540,1544,1561,1571,1587,1600,1616,1627,1688,1693,1705,1709,1747],{"__ignoreMap":36},[41,1472,1473,1475],{"class":43,"line":44},[41,1474,55],{"class":47},[41,1476,1477],{"class":51}," anthropic, json\n",[41,1479,1480],{"class":43,"line":61},[41,1481,65],{"emptyLinePlaceholder":64},[41,1483,1484,1487,1489],{"class":43,"line":68},[41,1485,1486],{"class":51},"client ",[41,1488,93],{"class":47},[41,1490,1491],{"class":51}," anthropic.Anthropic()\n",[41,1493,1494],{"class":43,"line":110},[41,1495,65],{"emptyLinePlaceholder":64},[41,1497,1498,1501,1503],{"class":43,"line":125},[41,1499,1500],{"class":81},"RUBRIC",[41,1502,646],{"class":47},[41,1504,1505],{"class":159}," \"\"\"You score stories for an audience of working AI engineers, 1-5:\n",[41,1507,1508],{"class":43,"line":136},[41,1509,1510],{"class":159},"5 = they'd stop scrolling and read it today\n",[41,1512,1513],{"class":43,"line":147},[41,1514,1515],{"class":159},"4 = solid, relevant, shippable\n",[41,1517,1518],{"class":43,"line":172},[41,1519,1520],{"class":159},"3 = mildly interesting, not urgent\n",[41,1522,1523],{"class":43,"line":190},[41,1524,1525],{"class":159},"2 = recycled or thin\n",[41,1527,1528],{"class":43,"line":196},[41,1529,1530],{"class":159},"1 = noise \u002F pure hype\n",[41,1532,1533],{"class":43,"line":213},[41,1534,65],{"emptyLinePlaceholder":64},[41,1536,1537],{"class":43,"line":237},[41,1538,1539],{"class":159},"Return ONLY JSON: {\"score\": \u003Cint 1-5>, \"reason\": \"\u003Cone line>\"}\"\"\"\n",[41,1541,1542],{"class":43,"line":1131},[41,1543,65],{"emptyLinePlaceholder":64},[41,1545,1546,1548,1551,1553,1555,1557,1559],{"class":43,"line":1160},[41,1547,71],{"class":47},[41,1549,1550],{"class":74}," score",[41,1552,1113],{"class":51},[41,1554,1116],{"class":81},[41,1556,1119],{"class":51},[41,1558,1116],{"class":81},[41,1560,744],{"class":51},[41,1562,1563,1566,1568],{"class":43,"line":1173},[41,1564,1565],{"class":51},"    msg ",[41,1567,93],{"class":47},[41,1569,1570],{"class":51}," client.messages.create(\n",[41,1572,1573,1576,1578,1581,1584],{"class":43,"line":1178},[41,1574,1575],{"class":150},"        model",[41,1577,93],{"class":47},[41,1579,1580],{"class":159},"\"claude-haiku-4-5\"",[41,1582,1583],{"class":51},",   ",[41,1585,1586],{"class":186},"# small, fast, cheap — use the current Haiku id\n",[41,1588,1589,1592,1594,1597],{"class":43,"line":1198},[41,1590,1591],{"class":150},"        max_tokens",[41,1593,93],{"class":47},[41,1595,1596],{"class":81},"100",[41,1598,1599],{"class":51},",\n",[41,1601,1602,1605,1607,1610,1613],{"class":43,"line":1209},[41,1603,1604],{"class":150},"        temperature",[41,1606,93],{"class":47},[41,1608,1609],{"class":81},"0",[41,1611,1612],{"class":51},",              ",[41,1614,1615],{"class":186},"# scoring is not a place for creativity\n",[41,1617,1618,1621,1623,1625],{"class":43,"line":1239},[41,1619,1620],{"class":150},"        system",[41,1622,93],{"class":47},[41,1624,1500],{"class":81},[41,1626,1599],{"class":51},[41,1628,1629,1632,1634,1637,1640,1643,1646,1648,1651,1653,1656,1659,1661,1664,1667,1670,1673,1675,1678,1680,1683,1685],{"class":43,"line":1244},[41,1630,1631],{"class":150},"        messages",[41,1633,93],{"class":47},[41,1635,1636],{"class":51},"[{",[41,1638,1639],{"class":159},"\"role\"",[41,1641,1642],{"class":51},": ",[41,1644,1645],{"class":159},"\"user\"",[41,1647,420],{"class":51},[41,1649,1650],{"class":159},"\"content\"",[41,1652,1642],{"class":51},[41,1654,1655],{"class":47},"f",[41,1657,1658],{"class":159},"\"",[41,1660,156],{"class":81},[41,1662,1663],{"class":51},"item[",[41,1665,1666],{"class":159},"'title'",[41,1668,1669],{"class":51},"]",[41,1671,1672],{"class":81},"}\\n\\n{",[41,1674,1663],{"class":51},[41,1676,1677],{"class":159},"'summary'",[41,1679,1669],{"class":51},[41,1681,1682],{"class":81},"}",[41,1684,1658],{"class":159},[41,1686,1687],{"class":51},"}],\n",[41,1689,1690],{"class":43,"line":1262},[41,1691,1692],{"class":51},"    )\n",[41,1694,1695,1697,1700,1702],{"class":43,"line":1271},[41,1696,1033],{"class":47},[41,1698,1699],{"class":51}," json.loads(msg.content[",[41,1701,1609],{"class":81},[41,1703,1704],{"class":51},"].text)\n",[41,1706,1707],{"class":43,"line":1286},[41,1708,65],{"emptyLinePlaceholder":64},[41,1710,1712,1715,1717,1720,1723,1726,1729,1732,1735,1738,1741,1743,1745],{"class":43,"line":1711},24,[41,1713,1714],{"class":51},"ranked ",[41,1716,93],{"class":47},[41,1718,1719],{"class":81}," sorted",[41,1721,1722],{"class":51},"(candidates, ",[41,1724,1725],{"class":150},"key",[41,1727,1728],{"class":47},"=lambda",[41,1730,1731],{"class":51}," i: score(i)[",[41,1733,1734],{"class":159},"\"score\"",[41,1736,1737],{"class":51},"], ",[41,1739,1740],{"class":150},"reverse",[41,1742,93],{"class":47},[41,1744,457],{"class":81},[41,1746,210],{"class":51},[41,1748,1750,1753,1755,1758,1760],{"class":43,"line":1749},25,[41,1751,1752],{"class":51},"top ",[41,1754,93],{"class":47},[41,1756,1757],{"class":51}," ranked[:",[41,1759,710],{"class":81},[41,1761,1157],{"class":51},[11,1763,1764],{},"That's it. The model is now a ranking function, and the rest of your pipeline only ever\nsees the best five things instead of the firehose.",[23,1766,1768],{"id":1767},"making-it-cheap","Making it cheap",[11,1770,1771,1772,1775],{},"Scoring runs on ",[327,1773,1774],{},"every"," candidate, so cost adds up fast if you're careless.",[249,1777,1778,1784,1794,1800],{},[252,1779,1780,1783],{},[18,1781,1782],{},"Use a small model."," Scoring is a Haiku job, not an Opus job. You're asking for a\nnumber, not an essay.",[252,1785,1786,1793],{},[18,1787,1788,1789,1792],{},"Cap ",[38,1790,1791],{},"max_tokens"," hard."," A score and a one-line reason fit in ~100 tokens. Don't pay\nfor a paragraph you'll throw away.",[252,1795,1796,1799],{},[18,1797,1798],{},"Score in parallel."," These calls are independent — fan them out, don't loop.",[252,1801,1802,1805],{},[18,1803,1804],{},"Pre-filter with code first."," Don't spend a model call ranking something a recency\nwindow already killed.",[23,1807,1809],{"id":1808},"making-it-stable","Making it stable",[11,1811,1812],{},"A scorer that returns 3 today and 5 tomorrow for the same input is worse than useless.",[249,1814,1815,1823,1833,1839],{},[252,1816,1817,1822],{},[18,1818,1819,259],{},[38,1820,1821],{},"temperature=0"," You want the same input to land in the same bucket every time.",[252,1824,1825,1828,1829,1832],{},[18,1826,1827],{},"Anchor the scale in the prompt."," \"Score 1-5\" alone invites drift. Spell out what\neach number ",[327,1830,1831],{},"means",", like the rubric above. The anchors are what make the scores\ncomparable across runs.",[252,1834,1835,1838],{},[18,1836,1837],{},"Don't ask for 1-100."," That's false precision — the model can't reliably tell a 73\nfrom a 76, and now neither can you. A tight 1-5 with explicit anchors is honest about\nthe resolution you actually have.",[252,1840,1841,1844,1845,1848],{},[18,1842,1843],{},"Force structured output."," Parsing free text as JSON works until the day it doesn't.\nIf you want it bulletproof, define a tool with a typed schema and let the model fill it\nin, instead of hoping ",[38,1846,1847],{},"json.loads"," succeeds.",[23,1850,1852],{"id":1851},"making-it-honest-the-anti-slop-part","Making it honest (the anti-slop part)",[11,1854,1855,1856,1859,1860,1863],{},"Here's the failure mode that bites people: the model is ",[327,1857,1858],{},"confident",", the JSON is\n",[327,1861,1862],{},"clean",", and you start trusting the scores without ever checking them. Confidence is not\ncorrectness.",[249,1865,1866,1875,1881],{},[252,1867,1868,1874],{},[18,1869,1870,1871,259],{},"Always require a ",[38,1872,1873],{},"reason"," One line, per item. It costs almost nothing and it's the\nonly way you'll catch the rubric being misread.",[252,1876,1877,1880],{},[18,1878,1879],{},"Log every score with its reason."," When the pipeline picks something dumb, you want\nthe receipt, not a mystery.",[252,1882,1883,1886],{},[18,1884,1885],{},"Spot-check the boundary."," Read the things that scored a 3. That's where the model's\njudgment is fuzziest and where your rubric needs sharpening.",[11,1888,1889],{},"If you can't explain why item A beat item B, your rubric is the problem — not the model.",[23,1891,1893],{"id":1892},"this-is-the-agent-loop-skeleton","This is the agent-loop skeleton",[11,1895,1896,1897,1899],{},"Score-then-act is most of what \"agentic\" actually means in production. Generation is the\npart that demos well; scoring and filtering is the part that makes the output not embarrass\nyou. And it's completely transferable — the same ",[38,1898,1436],{}," shows up as re-ranking\nretrieved chunks in RAG, triaging a flood of PRs, prioritizing leads, picking which alert\nactually pages a human.",[11,1901,1902],{},"Reach for the model as a judge before you reach for it as a writer. Your pipeline gets\ncheaper, more predictable, and a lot less sloppy.",[336,1904,1905],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":36,"searchDepth":61,"depth":61,"links":1907},[1908,1909,1910,1911,1912,1913],{"id":1440,"depth":61,"text":1441},{"id":1461,"depth":61,"text":1462},{"id":1767,"depth":61,"text":1768},{"id":1808,"depth":61,"text":1809},{"id":1851,"depth":61,"text":1852},{"id":1892,"depth":61,"text":1893},"2026-06-14","The most underrated way to put an LLM in a pipeline is as a ranking function, not a writer. How to do it cheaply, stably, and without slop.",{},"\u002Fblog\u002Fuse-claude-as-a-scorer",{"title":1418,"description":1915},"blog\u002Fuse-claude-as-a-scorer","qWGEhVVLKQf_iwhmwRIniiGyzxQuUCX3Xfk0jHLIF_o",{"id":1922,"title":1923,"body":1924,"date":2026,"description":2027,"draft":346,"extension":347,"meta":2028,"navigation":64,"path":2029,"seo":2030,"stem":2031,"__hash__":2032},"blog\u002Fblog\u002Fhello-agent-brief.md","Hello, Agent Brief",{"type":8,"value":1925,"toc":2020},[1926,1933,1937,1960,1964,1978,1982,1985,1996,1999,2002,2006],[11,1927,1928,1929,1932],{},"This is ",[18,1930,1931],{},"Agent Brief"," — a content + product hub for working engineers\nbuilding with Claude, MCP, and the open AI stack.",[23,1934,1936],{"id":1935},"what-this-site-is","What this site is",[249,1938,1939,1948,1954],{},[252,1940,1941,1944,1945,259],{},[18,1942,1943],{},"A free newsletter."," Tuesday brief + Friday deep-dive. Engineer-voice,\nanti-slop. Subscribe at ",[850,1946,1947],{"href":1947},"\u002Fsubscribe",[252,1949,1950,1953],{},[18,1951,1952],{},"A growing library of long-form posts"," (you're reading one) — SEO-indexed,\nno signup wall.",[252,1955,1956,1959],{},[18,1957,1958],{},"A home for shippable products"," — starting with the Claude Code\nStarter Kit, which packages the production patterns we've spent months\ntrial-and-erroring our way into.",[23,1961,1963],{"id":1962},"what-this-site-is-not","What this site is not",[249,1965,1966,1969,1972,1975],{},[252,1967,1968],{},"Not a \"this represents a significant breakthrough in AI\" newsletter.",[252,1970,1971],{},"Not an analyst report.",[252,1973,1974],{},"Not a \"10 ways AI will change your life\" listicle.",[252,1976,1977],{},"Not face-of-the-creator personal brand vehicle. This is engineer-to-engineer\nsignal.",[23,1979,1981],{"id":1980},"why-now","Why now",[11,1983,1984],{},"The AI engineering space is full of:",[249,1986,1987,1990,1993],{},[252,1988,1989],{},"Hype-cycle thinkpieces that don't ship code",[252,1991,1992],{},"Tutorial sites that demo \"hello world\" then leave you stranded at production",[252,1994,1995],{},"Newsletters that recycle Hacker News with extra adjectives",[11,1997,1998],{},"If you're shipping real systems with Claude, MCP, and agents — you need\nsignal. You need patterns that survive contact with production. You need\nopinions that come with code attached.",[11,2000,2001],{},"That's what we're building here.",[23,2003,2005],{"id":2004},"what-to-do-next","What to do next",[249,2007,2008,2014,2017],{},[252,2009,2010,2013],{},[850,2011,2012],{"href":1947},"Subscribe to the newsletter"," — free, twice weekly",[252,2015,2016],{},"Watch for the Starter Kit launch (subscribers get founder pricing)",[252,2018,2019],{},"Forward this to one engineer who'd want it",{"title":36,"searchDepth":61,"depth":61,"links":2021},[2022,2023,2024,2025],{"id":1935,"depth":61,"text":1936},{"id":1962,"depth":61,"text":1963},{"id":1980,"depth":61,"text":1981},{"id":2004,"depth":61,"text":2005},"2026-06-08","What this site is and what it isn't.",{},"\u002Fblog\u002Fhello-agent-brief",{"title":1923,"description":2027},"blog\u002Fhello-agent-brief","EBJ7pgHBuObKeB3wMBqDCRxlqOwrQgOq2ZeG49Dotrk",1781756059864]