Projects / Built · Case study

Measuring an LLM insight pipeline instead of trusting it

The AI engine behind a customer-intelligence platform turns CRM notes, call transcripts and emails into a few prioritized insights. I owned it, and the work that matters is what I measured, what I undid, and what I traced to root cause.

Context
B2B customer-intelligence startup
My role
Owned the AI engine: pipeline, evaluation, reliability
Team
Started at seven people, later four; about 4–6 months
Focus
Evaluation and cost control in production
80–90%Of already-covered data re-processed on every run, found and removed
>90%Judge agreement with hand labels, on the two checks built (of seven)
59 sFull-table scans removed by an index on large organizations
7 → 4Model calls per weekly digest run

The system in under a minute

The problem

Early warning signs in a B2B account, such as a deal going cold or a support pattern repeating, are scattered across CRM notes, call transcripts and emails. Nobody reads all of it, so the signals surface late or not at all. The platform reads that data continuously and turns it into a small number of prioritized insights people can trust.

What value it generates

A person gets an early warning plus a next step, without reading every note and transcript. Every insight arrives with its severity, the evidence it was built from, and its history, so it can be checked rather than taken on trust. These are illustrative examples modeled on the pipeline’s documented output, with invented evidence. They are not customer data.

Critical insight

Competitive pressure from a named rival

Customers are weighing a switch because of the rival’s reporting and unlimited users. It came up in emails and calls across several accounts, so it reads as a pattern, not one unhappy customer.

  • Email“We’re evaluating them mainly for the reporting and unlimited seats.”
  • Call“If they match your dashboards, we’d move at renewal.”
  • Email“Finance asked why we pay per seat at all.”

5 sources · 2 systems · renewal at risk

Key insight

Onboarding complexity is delaying adoption

New users struggle with setup and adoption is slipping. The pipeline recognises it as the same theme as earlier runs, so it extends an existing thread instead of being reported as new.

  1. Run 1New
  2. Run 2Linked · 0.87
  3. Run 3Linked · 0.82

Match reason kept: “same underlying onboarding complexity theme”

Signal

Buyers want quantified ROI before committing

Pricing pushback in enterprise deals is tied to perceived ROI rather than headline cost. It spans pricing, renewal and expansion conversations, which points to a positioning gap.

  • Call“Can you put the payback period in writing?”
  • Email“Need a clearer ROI case before the Scale tier gets approved.”

Next step: tighten enterprise ROI messaging

How each insight stays traceable is covered under Trusting the output.

How the pipeline works

Pipeline: sync and clean seven sources, then cluster behind an evidence gate. A cluster whose sources an earlier insight already covers is dropped there and never reaches generation. The remaining clusters go on to generate themed insights, deduplicate with a Jaccard prefilter and an LLM merge, score significance and assign tags, then match lineage to prior runs. Every call is traced and a judge samples live output.Droppedalready coveredall sources covered by an earlier insight:dropped, no model call spentSync & clean7 sourcesClusterevidence gateGeneratethemed insightsDedupeJaccard + LLMScorerate + tagLineagelink past runsEvery model call traced · prompts versioneda judge samples live output · alerts on quality regressions
Seven sources: four CRMs and three call tools. The dashed branch drops clusters an earlier insight already covers, before any model call. Dedupe runs a cheap 60% Jaccard prefilter, so the LLM only sees likely duplicates.

Finding the waste

My own investigation found the pipeline reprocessing 80–90% of already-covered data on every run. That wasted model spend and resurfaced the same insight as if it were new. I built a check that runs before generation: if an earlier insight with an overlapping window already covers a cluster’s sources, regeneration is skipped.

The same cost instinct cut the weekly digest from seven model calls to four by merging two prompts, while I built its four sections: critical signals, emerging themes, executive summary and focus areas.

Trusting the output

There is no fixed correct answer to unit-test an insight against, and reading every insight by hand does not scale. So I built a judge, and validated the judge before trusting it. Each check was tuned on a labeled development set of 25–30 observations, re-checked on a held-out set of 10–15, then sampled continuously in production with alerts on regressions around new prompt releases. Both built checks agreed with hand labels above 90%. The sets are small, and I say so.

CheckAsksStatus
GroundednessIs the insight backed by the source data?Built, validated
Data sufficiencyWas there enough evidence to generate it at all?Built, validated
QualityIs it well written?Specified
ActionabilityDoes it tell the team what to do?Specified
CategorizationIs it correctly tagged?Specified
RelevanceIs it relevant to that account?Specified
CalibrationIs the urgency neither over- nor under-stated?Specified

Citations

Every insight keeps its sources

Each insight stores the IDs of the source texts it was built from. The groundedness judge sees exactly those snippets, so a claim is checked against its own evidence and nothing else.

Lineage

Themes are followed across runs

New insights are matched to the previous run’s, and linked only above a 0.6 confidence. The score and the model’s written reason are stored with every link, so a trend can be questioned.

Audit trail

Every model call is replayable

Each call is traced with the prompt version that produced it, so a bad output can be tied to a specific prompt change and rolled back.

I applied the same standard to my own work. I shipped a rule-based signal-strength score, measured it in production, and found it over-inflated significance in specific patterns: single-account insights, and narrative framing without evidence. I replaced it with the model’s own judgment rather than keep two competing systems.

Production incidents, traced to root cause

  • Failures across 10+ organizations

    Uploads starved after the response returned

    Timeouts and SSL failures traced to the hosting platform throttling CPU to near zero once the HTTP response was sent, starving background uploads. Tuning timeouts could not fix it; the fix was at the deployment level. Switching serialization to orjson also cut peak memory in the upload path from about 3.8× to 2× the payload, for payloads up to 66 MB.

  • Connection pool exhausted

    A retry storm caused by a status code

    A webhook handler returned 400 for permanently unprocessable messages, so the queue retried them forever and drained the database connection pool. In the same investigation I fixed two N+1 query patterns and a misread pool metric, and found that worker threads equaled pool size, leaving no headroom under load.

  • 59-second scans

    An index, and the two traps behind it

    An unindexed multi-column query forced full scans of about a minute for large organizations. I added a composite index through a live backfill, then hardened the fix against two Postgres traps: autovacuum conflicting with concurrent index builds, and invalid indexes silently blocking retries.

Peak memory in the upload path, as a multiple of payload size

  • Before (standard JSON)3.8×
  • After (orjson)2×

Engineering judgment calls

DecisionWhyOutcome
Reverted incremental sync for a CRM (Monday.com)Updates could be filtered by exact date, but items only accept relative presets. A mixed sync risked silent gaps, because two downstream tables hard-delete rows missing from a payload.Back to a full sync. Other providers got per-provider watermarks in place of fixed 180-day pulls.
Plain index over trigram indexThe filter was an exact match on a field with few distinct values, which a B-tree index handles directly.Trigram kept only for free-text search.
Throttle before backoffRetrying after a 429 only reacts once a request is already wasted.Limit of 2 requests per second, with backoff as the fallback.
Rule-based score to model judgmentProduction data showed the rule-based score over-rated certain patterns.Deleted it, rather than run two scoring systems.

I also reviewed the AI engine against SOLID principles (it scored 5 out of 10, with file-level citations), wrote a five-phase refactor plan, and executed the first phases: one shared background-task runner in place of three copies, pooled database connections, and one shared layer for events, logging and errors.

Investigated, not built

Before committing to a planned feature that would group insights by theme, I ran a short experiment to see whether it was feasible, with no production writes. The verdict: grouping by theme looks feasible, but not fully automatic. The feature itself is not built.

The experiment turned each insight into an embedding, a list of numbers where similar text gets similar numbers, then measured how well insights separate into groups.

  • Same-topic pairs0.744
  • Different-topic pairs0.667

Insights on the same topic are more alike than insights on different topics, but the ranges overlap. That is enough to group insights, not to separate them cleanly.

  • Synthetic data0.401
  • Real organization A0.556
  • Real organization B0.548

0.30: below this a clustering is considered weak

QuestionFinding
Can similarity deduplicate?Treating pairs above 0.93 similarity as duplicates wrongly merged 0 of 435 synthetic pairs and 2 of 1,225 real pairs.
Which methods lost?t-SNE (gives different results each run and only works in 2–3 dimensions) and k-means (needs the number of groups chosen up front, forces every insight into a group, and assumes round-shaped groups). UMAP with HDBSCAN fit instead.
Can the model step out of the loop?No. The model must still make the final call where meaning can flip, such as risk rising versus risk falling.