Open to offers

Rodrigo
Torres

AI Engineer and founder of Árkos

clients since 2020
45+
projects delivered
50+
people on the team
9
years of AI in production
2
  • SUNAT, payment gateways and ledgers
  • I lead nine people and stay in the code

I founded a nine-person consultancy. Six years shipping software, two with models in production.

BM25 + vector search // Reciprocal Rank Fusion // MCP servers // FastAPI // Step Functions // AWS Lambda // CDK // DynamoDB // walk-forward // PostgreSQL // electronic invoicing // pytest

Ask the page

A search written for this page: 39 passages, BM25 plus vectors fused with RRF. It is not PrecioVivo, which is the case study and runs over a price catalogue.

What that search actually scores

I wrote a gold set and the fusion lost one

The box above was described as hybrid and graded by nobody. Twenty-six questions, each with the passages that genuinely answer it, marked by reading the passages rather than by reading what the search returned. Three configurations differing in exactly one thing each.

Lexical only (BM25)

0.577

Recall@4 · [0.39, 0.74]

MRR
0.563
NDCG@4
0.489
Vector only

0.846

Recall@4 · [0.66, 0.94]

MRR
0.790
NDCG@4
0.745
Fused with RRFConfiguration

0.769

Recall@4 · [0.58, 0.89]

MRR
0.682
NDCG@4
0.634
Reading the run

The lexical half alone misses eleven of twenty-six, and they are the questions that share no word with their passage: whether he builds alone, whether he has broken production, what salary he is after. That is precisely what a vector index exists to answer.

Fusion recovers a question neither half finds on its own, which is the evidence that fusing is not decoration: two weak signals in different lists add up to a position neither reached alone.

It pays for that. The fusion's MRR sits below the vector half's: it puts the answer inside the top four more often and leaves it slightly lower within those four. With four passages going to the model that costs little; with one result on screen it would cost a great deal.

In English the fusion comes out below the vector half alone, 0.769 against 0.846, losing all three questions where they disagree. At n = 26 the sign test gives p = 0.25, which does not separate that from chance. Switching configuration per language on that evidence would be fitting the system to the gold set, so both run the same one.

The Wilson interval is the part that cannot be skipped. A recall of 0.923 over 26 cases is 0.923 [0.76, 0.98]: the system could be at 0.76 and this gold set would not know. Twenty-six cases are a starting point, not an answer.

A real defect came out of this. The two lists entered the fusion at different depths: the lexical one whole, the vector one cut at 12. BM25 keeps every passage scoring above zero, a median of 31 out of 39, so a passage ranked 30th still collected a vote that no comparable vector match could answer. Cutting both to the same depth took Spanish recall from 0.885 to 0.923. The correction is justified by the argument; the figure only says what it had been costing.

Sound familiar?

Hiring an AI engineer means hiring numbers nobody can verify.

Almost every AI portfolio claims results without saying how they were measured or where the system fails. I publish the measurement conditions next to every figure, and a whole section devoted to what breaks. If I cannot link it to its source, it does not appear.

How I work ↗
Measured, source unpublished: sources still to publish. Every figure waits on one before it counts as proven.sources still to publish. Every figure waits on one before it counts as proven.
37
How the system searches

Closeness is the measure

A vector index does not store words, it stores directions. Every fragment is a point, and answering a query means asking which points face the same way. Move the cursor closer and watch what each stroke does: that is what happens 9,125 times when you type a question.

The field answers the cursor. The index answers the query.

Measured, not estimatedAll the measurements ↗

Every figure with the conditions it was taken under

Measured, source unpublished: the evaluation run with jina-embeddings-v3What the vector half earns
0.862

Recall of the search isolated from the deterministic floor, with the real embedder. The toy embedder gave 0.759: that difference is the only thing proving the semantic half is not decoration

Unverified: the evaluation runGold set violations
2

With the real embedder. Asking for papaya, which is not in the catalogue, returns potato records: 0.6444 against 0.6656, two hundredths apart. No threshold separates them without killing the aggregate questions

Measured, source unpublished: tool definitionsMCP tools
8

MCP server in production for the RAG system

Measured, source unpublished: OpenAPI schemaFastAPI routes
13

The same surface, exposed over HTTP

Unverified: output of both suitesRAG tests
392 + 77

392 in pytest, 77 in vitest

Unverified: validation notebookGradient boosting
0.1711

Same walk-forward validation. Lost to AR(1)

Experience

Founder and technical lead at Árkos. I design the architecture and build, not only direct.

6years of software
2years of AI in production
50+projects delivered
45+clients
20+systems in production
9people on the team
Measured, source unpublished: client case with metricsSolutec

2+ years in production, 650 work orders a month, 15 users, 45% less time to close

Measured, source unpublished: client case with metricsAll White Vacations

4,000 bookings, 43% less overbooking

Measured, source unpublished: suite outputRestHUB

49 backend modules, 130 screens, 2,371 tests across 281 files, 5 locations, 500 daily transactions

Measured, source unpublished: repositorySignMed

MediaPipe, 35-frame sequences, CNN and LSTM in TensorFlow. Fixed privilege escalation, a committed default secret, and unbounded reads of uploaded files

Measured, source unpublished: client case with metricsFacturArkos

60% less time to issue

Measured, source unpublished: store listingsRapiditos

Flutter over a Spring Boot backend, shipped on App Store and Google Play

Measured, source unpublished: client case with metricsClínica Juan Pablo II

78% of appointments handled digitally

Measured, source unpublished: client case with metricsSolutec DHA

40% more monthly enquiries through the digital channel

Where it breaksThe four limits ↗

Limits carry the same weight as results.

Measured, source unpublished: the evaluation run with the real embedderRAG system

Papaya returns potato, and I did not fix it in retrieval

Where and why

Asking for a product that is not in the catalogue returns the closest one by name. Papaya scores 0.6444 and potato 0.6656: two hundredths apart. I tried the obvious threshold and it does not work, because any cut that separates them kills the aggregate questions, which score 0.5151. I solved it by warning the model from the lexical filter, which already knew no word matched the catalogue. The evaluation still reports both violations: I did not relax the gold set.

Measured, source unpublished: the state size calculationForecasting on AWS

It breaks at 510 products

Where and why

514 bytes per Map element against the 256 KB Step Functions state ceiling. Past that number the Map has to be paginated or the payload moved to S3.

Shall we talk?

I answer quickly, and never with a template.