An AI agent that discovers the news and drafts it
Fibre2Fashion's news desk spent every morning searching dozens of sources by hand, judging what mattered, confirming it, then writing it up. A two-phase agent now does the finding and the drafting, with an editor choosing what gets pursued — cutting 80–90% of that routine and running the same desk with fewer people on it.
On this page
Every morning, a news desk does the same thing: open Google, visit a few dozen competitor and source sites across a dozen countries, decide what's actually relevant, confirm each story against a primary source, and rewrite it in house style. It's hours of repetitive triage — and because it's manual, there's no memory across days and no consistent ranking of what mattered.
I built an agent that reproduces that routine for a textile/apparel newsroom — the full case study lives here. It now does the finding and the first draft, and the editor's job shifts from doing the triage to deciding on it: 80–90% of that morning routine is gone, and the desk runs on fewer people than it used to.
That number is the whole reason the rest of this post is about cost and trust rather than about prompting. The hard part wasn't calling an LLM. It was making the thing cheap, trustworthy, and controllable enough that an editor would actually hand it the morning.
Why two phases
The naive version is one big pipeline: search everything, fetch every page, summarize, rank, draft. That's expensive and slow — you pay full-text fetch and LLM cost on stories nobody wanted.
So I split it in two, with a human in the middle:
- Discovery (cheap). Expand the beat plus ~40 textile, trade and macro keyword topics into date-aware queries and harvest a curated source master of ~65 primary and ~75 secondary pages across 17 countries, one lane per run, preferring each source's RSS feed with Google News as the fallback for bot-blocked or feedless sites, then have an LLM score, de-duplicate, and rank the candidates. A full scan is roughly four LLM calls and a few thousand tokens on a small model.
- Pursuit (expensive, opt-in). The editor ticks the stories worth chasing. Only those get the full-text fetch, clustering into distinct candidates, primary-source confirmation, and one-click drafting.
That single boundary — let a human pick before you spend — is what keeps the economics sane. The model triages; the editor decides; the expensive work runs on a shortlist.
Getting the inputs without getting blocked
Discovery is only as good as what it can read. The harvest is RSS-first: if a source publishes a feed (configured or auto-discovered), prefer it over scraping. When a page is bot-walled, fall back to a search snippet so a blocked URL never silently drops a story.
For the pages that do need fetching, the extractor is layered — trafilatura, then JSON-LD, then the article container, then a paragraph sweep — and a bot-wall detector re-fetches through Playwright when the cheap path returns junk. Messy or hostile pages still yield clean copy more often than not.
Ranking and de-duplication
An LLM judges relevance as direct vs. indirect to the beat, then collapses the same story told by five different outlets into one item, and ranks what's left by newsworthiness, source authority, and recency. Cross-scan de-duplication (URL + content-hash + headline overlap) means a story that appeared yesterday doesn't resurface today. (Retrieval — picking the right source chunks rather than ranking whole stories — is a related but distinct problem; I dig into it in metadata-filtered RAG.)
Confirming a story without paying for it
A draft is only safe if the story is real. I confirm each one against a primary source — but without an LLM call. It's a targeted search, a preference for higher-authority source tiers, and a Jaccard title-overlap guard to reject loose matches. Cheap, deterministic, and good enough to catch the obvious fabrications and rumor-mill reposts.
Provider-agnostic by design
The LLM layer is provider-agnostic — OpenAI, Gemini, or Claude — all in JSON mode with backoff retries and per-job token accounting. That matters for cost control: high-volume discovery runs on a small, fast model, while drafting (where quality shows) can use a stronger one. Swapping providers is a config change, not a rewrite.
Production hardening is the actual work
The agent is the easy 60%. The last 40% is what makes it deployable:
- SSRF-guarded fetching — it follows URLs the internet hands it, so the fetcher refuses private/loopback ranges.
- robots.txt + rate limits on every outbound request.
- Per-job token and URL budgets, so a pathological run can't burn the month's spend.
- Retries with backoff, structured JSON logs, and Prometheus metrics.
- Durable across restarts — a job that was mid-flight resumes, not restarts.
- A watchdog that self-heals a wedged worker, and interrupted-job recovery so a restart never silently loses a scan.
Streaming the work, not just the result
Every step — each query, each source, each judgement — streams to the editor live over Server-Sent Events. That turned out to be more than polish: a visible activity log is what makes an agentic system trustable. When you can watch it reason and reject, you stop second-guessing it. One config-driven client even serves two hosts — a FastAPI SPA and a .NET admin portal — so a fix ships once.
What I'd tell my past self
- Put the human at the most expensive boundary. Don't ask for approval on trivia; ask right before the costly work.
- Determinism where you can afford it. Confirmation didn't need an LLM, and not using one made it cheaper and easier to reason about.
- Observability is a feature, not ops overhead. The live log is the difference between a demo and a tool people rely on.
The result: a manual morning scan became one click and a deduped shortlist of ~20–30 ready-to-draft candidates — at a fraction of the cost a fetch-everything pipeline would have run up.
What's next: closing the loop
The shortlist is ranked by an LLM relevance score against a beat description. That's a guess at what this desk cares about, written once, by me — and it stays a guess no matter how carefully I tune the prompt.
Meanwhile the desk answers the question properly every single morning. An editor opens the shortlist, picks a handful, and leaves the rest. That's the whole list labelled — positives and negatives — generated for free as a side effect of work someone was doing anyway. There's a second signal downstream, too: of the stories that get drafted, some are published and some quietly aren't.
None of it currently comes back into the ranking.
The next thing I want to build is an outcome tracker that closes that loop: persist each shortlist alongside what the editor did with it, then use the accumulated accept/reject history to re-rank — as features feeding the existing score, or eventually as a small learned ranker trained on the desk's own decisions.
Two things I want to be careful about when I do:
- This is a data problem, not a framework problem. No agent library makes a system learn; the signal has to be captured, stored and evaluated first. Reaching for a framework here would be solving the easy part.
- A feedback loop can eat itself. If the ranker only ever learns from stories that made the shortlist, it narrows toward its own past behaviour and stops surfacing the unfamiliar. Any version of this needs deliberate exploration held back from the learned score, plus a held-out set to measure against — otherwise "improving" and "converging on its own taste" look identical from the inside.
Which is the honest version of a self-improving agent, I think. Not a model that retrains itself, but a system whose users are labelling its training set without being asked — and an engineer paying attention to what that data is quietly selecting for.
Frequently asked questions
- What is a two-phase AI agent?
- It splits the work into a cheap discovery phase, where an LLM scores and ranks candidates, and an expensive pursuit phase that only runs on the items a human picks. You never pay full-text fetch and generation cost on stories nobody wanted.
- How do you keep an LLM agent's costs down?
- Put the human at the most expensive boundary, run high-volume triage on a small fast model and reserve a stronger model for drafting, cap per-job token and URL budgets, and use deterministic checks instead of LLM calls wherever they're good enough.
- Why stream agent steps over Server-Sent Events?
- A live activity log is what makes an agentic system trustable — when an editor can watch it search, judge, and reject in real time, they stop second-guessing it. SSE is a simple one-way stream that fits this perfectly.
- How do you stop an AI agent from drafting fake news?
- Each story is confirmed against a primary source before drafting — a targeted search with source-authority preference and a title-overlap guard — which catches obvious fabrications and rumor reposts without spending an extra LLM call.
One vector search across a mixed corpus returns confident nonsense, because semantic closeness is not topical relevance. Filtering on metadata first, then searching, then reranking fixed it. Here is what each stage actually earns.
ReadA payment can succeed and still go wrong: a dropped webhook, a replayed one, a signature nobody checked. Here is how I verify them, settle exactly once, and keep a ledger I can reconcile against months later.
Read