AI Engineer · Full-Stack Developer · Lima, Peru

I ship LLMs to production without breaking anything.

I'm Rodrigo Latorre. Five years building software, the last few on systems where a model makes decisions and the code answers for them. The hard part was never calling the model — it's getting its response into a database without corrupting it, without hallucinating, and without costing a fortune.

5+
years shipping to productionCV — Jan 2021 to date
4
systems I can show youCV — featured projects
15,000
lines of TypeScript across two SaaS products7,600 (MediaBriefs) + 7,400 (Sin Anestesia)
7
distinct AI touchpoints in one systemSin Anestesia §3

02 — Principles

Six rules I learned putting models into production.

Using a model is easy. The hard part is getting its output into a database without corrupting it, without lying, and without getting expensive. These are the decisions holding up my pipelines — all of them from code running right now.

02.1

The model judges, the code calculates.

The LLM never produces a number that matters.

Claude groups headlines by topic — semantic judgement no keyword algorithm does well — and that's where its job ends. Every metric is computed by code comparing against the previous state of the database. If a topic is flagged urgent, I can explain exactly why; a score the model assigned would be neither reproducible nor defensible to an editor.

MediaBriefs

The model judges, the code calculates.text
mentions     ← how many articles landed in the cluster
mediaCount   ← how many distinct outlets cover it
velocity     ← current mentions / previous run's mentions
score        ← mentions×2 + mediaCount×3 + velocity×4
Deterministic, auditable rules. The model decides none of them.
02.2

Forced tool-use, zero JSON parsing.

The model can't reply in prose even if it wants to.

All model access goes through a single method that declares a tool with its input_schema and forces tool_choice. The only possible output is a call to that tool with that shape. There is no free-form JSON parsing anywhere in the system, and the schema does double duty: it documents each field for the model and guarantees the shape for Mongoose.

MediaBriefs · Sin Anestesia

Forced tool-use, zero JSON parsing.ts
// One client for six use cases.
await client.messages.create({
  model,
  tools: [{ name, input_schema }],
  tool_choice: { type: 'tool', name },   // no prose output possible
})
Format constraints live in the schema, not in the prompt.
02.3

Reference by index, never rewrite.

Eliminates an entire class of hallucination and cuts output cost.

Clustering sends 200 numbered headlines and asks for indices back, not titles. The model can't return a subtly different headline or an invented URL — it can only point at which of the ones I gave it belong together. It saves output tokens, which are the expensive ones, and the code validates trivially with a range check.

MediaBriefs

Reference by index, never rewrite.text
[0] Finance minister announces budget cuts
[1] Congress approves motion of censure
...
→ { label: "Budget crisis", articleIndices: [0, 14, 37] }
Instead of rewriting 200 headlines, it returns ~200 integers.
02.4

AI never takes the system down.

Every call has a defined failure behaviour.

If clustering fails, the previous topics are kept: I'd rather show two-hour-old data than an empty screen. If diarization is unavailable, the transcript still ships with a flag that travels to the frontend — the app knows when it's showing a degraded result and says so, instead of pretending there was only one speaker.

MediaBriefs

AI never takes the system down.text
Clustering    → returns null, keeps the previous clusters
NER           → returns [], cluster exists without people
Briefing      → falls back to the representative article's description
Diarization   → everything as "Speaker 1" + diarizationEnabled: false
Four distinct policies. None propagates the error.
02.5

A human confirms before the AI writes.

The AI proposes, the person decides.

Column mapping on contact import is reviewed before confirming; WhatsApp messages are approved before sending; clips are approved before publishing. And where a mistake has real consequences — suggesting a guest who doesn't exist — the name the model returns is resolved against the database: if it isn't there, it's discarded. The model never creates contacts.

Sin Anestesia

02.6

Measure, don’t assume.

The right metric is the business one, not the paper's.

I cut 20% of a prompt after verifying with count_tokens that the scaffolding cost ~7,000 tokens per episode. And before self-hosting Whisper I built a benchmark that measures proper-noun coverage rather than WER: if the local model writes "Bolarte" instead of "Boluarte", the highlight gets picked worse even when global WER looks fine. Decision thresholds were set in advance, before seeing any results.

Sin Anestesia

Measure, don’t assume.text
≥ 95%   proper-noun coverage → migrate
85–95%                        → try the larger model
< 85%                         → stay on the API

Second filter: < 2× realtime (competes with ffmpeg renders)
Transcription was 72% of AI spend. The risk was never cost — it was quality.

03 — Work

Four systems, told through their decisions.

Of everything I have built over these years, these four are the ones I can show: they are deployed, used by someone other than me, and their owners let me talk about them. Not loose screenshots — the problem that existed, how it is built, and the decision that holds each one up.

MediaBriefs

In production

Editorial intelligence SaaS for journalists

A CRM for shows, guests and interviews with two AI layers on top: a radar that turns the day’s news into actionable briefings, and interview transcription split by speaker.

pipelinetext
6 Peruvian RSS feeds (El Comercio, Gestión, BBC Mundo…)
   └─ NewsArticle ────────────────► ~200 articles from the last 24 h
      └─ Claude Haiku 4.5 ────────► groups into 8–15 topics
         └─ CODE ─────────────────► mentions, mediaCount, velocity, score
            └─ TopicDefinition + Snapshot ──► topic history over time
               └─ Claude Haiku 4.5 ────────► NER: people, role, organisation
                  └─ PersonProfile ────────► canonical identity + aliases
                     └─ match against Guests ──► "this guest of yours fits"
                        └─ Claude Haiku 4.5 ──► briefing · 3 questions · risk
The full editorial pipeline. The transcription pipeline runs in parallel.
7,600
lines of TypeScript in the backend
25
NestJS domain modules
61
endpoints in the OpenAPI contract
88
versioned schemas
32
frontend screens
~200
articles processed every 2 h
  • NestJS
  • TypeScript
  • Node.js
  • MongoDB
  • Redis + Bull
  • Session auth with rotation
Read the case study

Sin Anestesia

In production

Content automation for a daily news show

From a YouTube link to vertical Shorts published with burned-in subtitles and per-network copy. And from an RSS feed to "call this person today", with the WhatsApp message already drafted.

pipelinetext
YouTube link
   └─ yt-dlp ──────────► source video (mp4 ≤1080p)
      └─ ffmpeg ───────► mono audio 16 kHz 16 kbps
         └─ Whisper ───► transcript with word-level timestamps
            └─ Claude Sonnet 5 ──► N clippable moments (start, end, hook)
               └─ ffmpeg ────────► cut + 9:16 blurred bg + burned subtitles
                  └─ Claude Sonnet 5 ──► titles, hashtags, per-network caption
                     └─ YouTube / TikTok / Meta ──► published
Every stage is idempotent: if Bull retries, it never pays for transcription twice.
7,400
lines of TypeScript (94 files)
14
frontend screens
7
distinct AI uses in the system
−20%
prompt trimmed, measured with count_tokens
4
containers in production
72%
of AI spend was transcription
  • NestJS
  • TypeScript
  • MongoDB
  • Redis + Bull
  • ffmpeg (libass)
  • yt-dlp
Read the case study

RO Processor

Delivered

Transaction Register · Peru FIU / SBS

A Python pipeline that turns a currency exchange’s monthly PDF of invoices into the 26-column regulatory report the Peruvian Financial Intelligence Unit requires, automatically resolving against the tax authority the fields the invoice does not carry.

pipelinetext
Monthly PDF (48–81 pages)
   └─ pdfplumber ─────► text extraction, page by page
      └─ valid invoice? ──no──► skipped, reason logged
         └─ tolerant parser ──► 41 structural shapes
            └─ ≥ USD 5,000? ──no──► discarded, amount logged
               └─ InvoiceRecord
                  └─ headless lookup ──► legal representative
                     └─ idempotent merge + backfill
                        └─ sort by date + renumber
                           └─ FIU Excel · 26 columns
Nothing is dropped in silence: every omission leaves its reason on the record.
386
PDF pages processed
41
structural shapes of the operation line
148
rows generated across 6 months
33
lookups needed for 148 rows
87
invoices above the USD 5,000 threshold
~1,500
lines of code
  • Python 3
  • pdfplumber
  • openpyxl
  • Selenium + headless Chrome
  • Tkinter
  • ThreadPoolExecutor
Read the case study

Screening platform

Delivered

Sanctions and PEP screening for regulated entities

A multi-tenant platform that collapses sanctions and politically-exposed-person screening into a single query, and persists the evidence of every search for the regulator.

5
international sanctions lists cross-checked
+1
Peruvian PEP master (electoral + comptroller data)
1
query replaces the source-by-source search
API
to embed screening in third-party onboarding
  • Next.js
  • NestJS
  • MongoDB
  • Docker
  • Multi-tenant
  • Public API
Read the case study

04 — Process

I build with agents. I don’t generate with AI.

Worth saying plainly: working this way speeds up nothing on its own. An agent with no context boundaries and no contract to verify it produces plausible code you end up rewriting entirely, which costs more than writing it by hand. What follows is the architecture you have to build before working with agents stops being an experiment — and building it takes time.

The OpenAPI contract as the boundary between agents

The system is three repositories, not two. Each agent has a bounded domain and is forbidden from reading the other side. That solves the real problem of working with agents on a large system: context is finite, and sharing all of it degrades the result.

mediabriefs-backend    ← one agent works here
mediabriefs-openapi    ← the boundary: openapi.json (61 paths, 88 schemas)
mediabriefs-frontend   ← another agent works here

The boundary verifies itself

The frontend does not hand-write types: it generates them from openapi.json with openapi-typescript and consumes the API with openapi-fetch. If the backend changes a payload without updating the contract, the frontend stops compiling. Coordination does not depend on anyone remembering anything — the compiler enforces it.

the backend breaks the contract
   └─► the frontend stops compiling
        └─► nobody has to remember anything

Durable context, with the reasoning behind each decision

CLAUDE.md with stack and conventions, ARCHITECTURE.md with a table of decisions and their reasons, API_RULES.md with the response envelope and pagination. Documenting the why is exactly what an agent cannot infer from the code, and what stops it "fixing" something intentional.

CLAUDE.md         stack · structure · conventions · commands
ARCHITECTURE.md   every decision WITH ITS REASON + phased roadmap
API_RULES.md      envelope · errors · pagination · soft delete
The why is what an agent cannot infer by reading the code.

Custom skills per domain

Capabilities packaged and versioned with the repo instead of re-explained in every conversation: a NestJS expert for the backend, and on the frontend a full design chain that goes from a vague UI idea to React components validated by AST.

.claude/skills/
   backend   → nestjs-expert
   frontend  → enhance-prompt → stitch-design → design-md
                  → react-components → stitch-loop

Repository hygiene

Conventional scoped commits, with the contract versioned alongside the code that implements it. The history lets you reconstruct what was done and why, which is exactly the difference between a project built with AI and one generated by it.

feat(billing):   …
fix(payments):   …
feat(contract):  align billing paths and schemas with implemented API

05 — About

A product engineer who specialised in AI.

Rodrigo Latorre Quispe

I have spent five years at the same consultancy in Lima. I joined building React components and ended up designing the AI pipelines behind its products. That full path — front end, back end, infrastructure, model — is what lets me decide where to put an LLM and, more importantly, where not to.

My judgement comes down to one line: the model judges and the code calculates. An LLM is extraordinary at semantic judgement and terrible at producing a number someone will later have to defend. Almost every problem I have seen in AI systems comes from confusing those two jobs.

I care about AI depth, production robustness and the quality of what I hand over. What interests me is software that keeps working when something fails: if the model does not respond, the system degrades and says so rather than pretending. That is what makes an AI product genuinely usable instead of a demo.

Languages

Spanish — NativeEnglish — Advanced (C1)

06 — Experience

From building components to designing AI pipelines.

Five years at one company, which is its own signal: I joined as a front-end developer and worked through the whole business to leading its AI engineering.

Jan 2021 — presentLima, Peru

Primesoft Consulting SRL

AI Engineer & Full-Stack Developer

I started as a front-end developer working in React and grew into full-stack and AI engineering roles over my time at the company.

  • Built and shipped multiple AI-powered applications to production — chatbots, automated transcription and dashboards — integrating the Claude, ChatGPT and Gemini APIs.
  • Built end-to-end full-stack applications with Next.js and NestJS, containerised with Docker and backed by MongoDB, from development through production.
  • Accelerated the development cycle with AI: refactoring legacy applications, refining tests and preparing environments, alongside deploying services on AWS.
  • Ran AI-optimised marketing campaigns to grow traffic and sales to landing pages, with a coherent design system across ads, landings and products.
  • React
  • Next.js
  • NestJS
  • Python
  • MongoDB
  • Docker
  • AWS
  • Claude
  • GPT
  • Gemini

07 — Stack

The tools I choose, and what for.

No proficiency bars: nobody knows what "React 92%" means. What follows is what I use in systems that are running.

AI / GenAI
  • Claude (Sonnet 5, Haiku 4.5)
  • GPT
  • Gemini
  • Llama
  • AWS Bedrock / Titan
  • Tool-use and structured outputs
  • Whisper
  • pyannote (diarization)
  • Prompt engineering
  • AWS Rekognition (liveness, compare faces)

A roster per task: quality where the output is the product, speed where it is extraction.

Backend
  • Python
  • FastAPI
  • Node.js
  • NestJS
  • Express
  • Microservices
  • Redis / Bull
  • MongoDB
  • PostgreSQL

NestJS for domain modules with DI: each capability is isolated and replaceable.

Frontend & Mobile
  • React
  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind v4
  • Swift
  • SwiftUI
  • UIKit
  • Combine
  • Flutter
Cloud & DevOps
  • AWS (EC2, S3, CloudFront, Lightsail)
  • Docker
  • GitFlow
  • Scrum
Contracts & quality
  • OpenAPI 3
  • openapi-typescript
  • openapi-fetch
  • Jest
  • Supertest

The contract as a boundary enforced by the compiler, not by anyone's memory.

Design
  • Design systems and branding
  • Figma
  • Sketch

08 — Collaboration

The problems I know how to solve.

Four situations I have already had to untangle, on an in-house team or on a scoped project. If yours looks like one of them, we already have somewhere to start.

09 — Contact

Tell me what you are building.

Whether it is an idea you have not started or a pipeline that breaks every week: write to me, I will tell you what I would do and whether I am the right person. No commitment, no sales pitch.

 

I reply to every message personally. No bots, no assistants.