<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Retrieval on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/retrieval/</link><description>Recent content in Retrieval on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 04 Jun 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/retrieval/index.xml" rel="self" type="application/rss+xml"/><item><title>Function calling y tool-augmented retrieval: el detective que sabe qué archivo pedir</title><link>https://blog.lo0.es/posts/function-calling-tool-augmented-retrieval/</link><pubDate>Thu, 04 Jun 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/function-calling-tool-augmented-retrieval/</guid><description>&lt;blockquote>
&lt;p>Este post profundiza en el mecanismo de razonamiento agentivo que extiende el RAG descrito en &lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">RAG con reranker e hybrid retrieval&lt;/a>. El retriever que se invoca cuando el LLM elige &lt;code>vector_search&lt;/code> es exactamente el pipeline de ese artículo. El JSON Schema que define cada tool call es &lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">structured output&lt;/a> aplicado a la interfaz herramienta. Y las requests del agente pasan por el &lt;a href="https://blog.lo0.es/posts/router-inferencia-llm-gateway-l7/">gateway L7 de inferencia&lt;/a> antes de llegar al modelo.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un RAG naive consulta siempre la misma fuente. Function calling le da al LLM la capacidad de decidir qué herramienta invocar — vector store, SQL, web search — en función de lo que la query realmente necesita. El patrón ReAct encadena esas invocaciones en un bucle razonado hasta obtener suficiente evidencia. Un pipeline de 3 iteraciones con Llama-3.1-70B en hardware on-premise tarda ≈ 1,1 s frente a los ≈ 300 ms del RAG de un solo paso; la ganancia no es en velocidad sino en queries que el RAG naive simplemente no puede responder. La métrica de eval crítica es &lt;strong>tool selection accuracy&lt;/strong>: el porcentaje de turns en que el modelo elige el tool correcto, medida sobre un eval set sintético.&lt;/p>
&lt;h2 id="la-analogía-el-detective-que-sabe-qué-archivo-pedir">La analogía: el detective que sabe qué archivo pedir&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="El detective y sus fuentes de evidencia">
&lt;style>
.db{fill:#f8f8f8;stroke:#444;stroke-width:1.4}
.dh{fill:#7aafff;stroke:#444;stroke-width:1.4}
.ds{fill:#ffd76b;stroke:#444;stroke-width:1.4}
.dg{fill:#b2e8b2;stroke:#444;stroke-width:1.4}
.dr{fill:#ffb3b3;stroke:#444;stroke-width:1.4}
.dl{font:600 13px sans-serif;fill:#222}
.dm{font:400 11px sans-serif;fill:#555}
.da{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mda)}
.dq{stroke:#666;stroke-width:1.5;fill:none;stroke-dasharray:5 3;marker-end:url(#mda)}
&lt;/style>
&lt;defs>&lt;marker id="mda" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;!-- Detective (LLM) en el centro -->
&lt;rect x="290" y="130" width="200" height="80" rx="8" class="dh"/>
&lt;text x="390" y="158" text-anchor="middle" class="dl">Detective (LLM)&lt;/text>
&lt;text x="390" y="176" text-anchor="middle" class="dm">razona qué evidencia&lt;/text>
&lt;text x="390" y="192" text-anchor="middle" class="dm">necesita y la solicita&lt;/text>
&lt;!-- Caso / Query -->
&lt;rect x="310" y="20" width="160" height="50" rx="8" class="db"/>
&lt;text x="390" y="42" text-anchor="middle" class="dl">Caso (Query)&lt;/text>
&lt;text x="390" y="60" text-anchor="middle" class="dm">"¿cuántos contratos UE &amp;gt; 100k€?"&lt;/text>
&lt;path class="da" d="M390,70 L390,128"/>
&lt;!-- Vector store -->
&lt;rect x="20" y="240" width="160" height="70" rx="8" class="ds"/>
&lt;text x="100" y="263" text-anchor="middle" class="dl">Archivo testimonios&lt;/text>
&lt;text x="100" y="281" text-anchor="middle" class="dm">vector_search&lt;/text>
&lt;text x="100" y="297" text-anchor="middle" class="dm">Qdrant · 5-50 ms&lt;/text>
&lt;!-- SQL -->
&lt;rect x="310" y="240" width="160" height="70" rx="8" class="dg"/>
&lt;text x="390" y="263" text-anchor="middle" class="dl">Registro contable&lt;/text>
&lt;text x="390" y="281" text-anchor="middle" class="dm">sql_query&lt;/text>
&lt;text x="390" y="297" text-anchor="middle" class="dm">PostgreSQL · 10-200 ms&lt;/text>
&lt;!-- Web search -->
&lt;rect x="600" y="240" width="160" height="70" rx="8" class="dr"/>
&lt;text x="680" y="263" text-anchor="middle" class="dl">Hemeroteca&lt;/text>
&lt;text x="680" y="281" text-anchor="middle" class="dm">web_search&lt;/text>
&lt;text x="680" y="297" text-anchor="middle" class="dm">pública · 200-2000 ms&lt;/text>
&lt;!-- Flechas del detective a fuentes -->
&lt;path class="da" d="M310,190 L180,238"/>
&lt;path class="da" d="M390,210 L390,238"/>
&lt;path class="da" d="M470,190 L600,238"/>
&lt;!-- Flechas de vuelta (observaciones) -->
&lt;path class="dq" d="M140,240 Q200,220 300,195"/>
&lt;path class="dq" d="M390,240 L390,212"/>
&lt;path class="dq" d="M640,240 Q560,220 480,195"/>
&lt;!-- Respuesta final -->
&lt;rect x="310" y="20" width="160" height="50" rx="8" class="db"/>
&lt;text x="390" y="42" text-anchor="middle" class="dl">Caso (Query)&lt;/text>
&lt;text x="390" y="60" text-anchor="middle" class="dm">"¿cuántos contratos UE &amp;gt; 100k€?"&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Un detective de novela no va al mismo archivador independientemente del caso que le llegue. Cuando recibe un caso, razona primero: ¿qué tipo de evidencia necesito? Si hay testigos, pide los testimonios (vector search sobre documentos no estructurados). Si hay transacciones financieras, pide los registros contables al banco (SQL sobre la base de datos estructurada). Si el sospechoso tiene actividad reciente que la empresa no puede tener indexada, va a la hemeroteca (web search). No consulta las tres fuentes de golpe en cada caso: elige la que la evidencia requiere, recibe el resultado, razona de nuevo si necesita más, y sólo cuando tiene suficiente evidencia redacta el informe.&lt;/p>
&lt;p>Un detective malo siempre va al mismo archivador. Un RAG naive es ese detective malo: vectoriza la query, va al vector store, y devuelve lo que encuentra aunque la pregunta fuera &amp;ldquo;¿cuántos contratos?&amp;rdquo; — algo que ningún chunk de PDF puede responder mejor que un &lt;code>COUNT(*)&lt;/code> en SQL.&lt;/p>
&lt;p>&lt;strong>Function calling es darle al LLM la capacidad de razonar sobre qué fuente pedir, y de invocarla de forma estructurada.&lt;/strong> La analogía tiene tres aristas que conviene retener:&lt;/p>
&lt;ol>
&lt;li>El detective no improvisa el archivo que pide: hay un catálogo de fuentes disponibles con descripción de qué contiene cada una. La descripción del tool en el system prompt cumple esa función.&lt;/li>
&lt;li>El detective puede pedir varias evidencias a la vez si son independientes (parallel tool calling).&lt;/li>
&lt;li>El detective sabe cuándo parar: si tras N rondas no llega a conclusión, declara que no tiene suficiente evidencia. El agente tiene un límite de iteraciones por la misma razón.&lt;/li>
&lt;/ol>
&lt;h2 id="qué-es-function-calling-la-anatomía-de-una-tool-call">Qué es function calling: la anatomía de una tool call&lt;/h2>
&lt;p>Function calling — también llamado tool use — es un mecanismo por el que el LLM, en vez de generar texto libre como respuesta, genera un objeto JSON estructurado que representa una invocación de herramienta. El sistema intercepta ese JSON, ejecuta la herramienta real, y devuelve el resultado como un mensaje de rol &lt;code>tool&lt;/code> en la conversación.&lt;/p>
&lt;h3 id="definición-de-tools-en-el-system-prompt">Definición de tools en el system prompt&lt;/h3>
&lt;p>Cada tool se define mediante un JSON Schema que especifica nombre, descripción y parámetros. Este JSON Schema es exactamente el mismo mecanismo descrito en &lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">structured output&lt;/a>, aplicado aquí a la interfaz herramienta:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vector_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Search internal company documents about policies, contracts and procedures. Use when the query requires unstructured text, document context or semantic similarity.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Natural language search query&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;top_k&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;integer&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Query the SQL database for structured metrics, counts, aggregations and financial data. Use when the query requires exact numbers, filters, sums or joins over structured records.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Parameterized SQL query with $1, $2 placeholders&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;array&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;items&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Parameter values for the placeholders&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;web_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Search public web for real-time information, recent news or current prices. Use only when data is public and not covered by internal sources.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="el-ciclo-de-una-tool-call">El ciclo de una tool call&lt;/h3>
&lt;p>Cuando el LLM decide invocar una tool, el mensaje que genera en lugar de texto libre tiene esta estructura (formato OpenAI-compatible, el mismo que soporta vLLM):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool_calls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;query\&amp;#34;: \&amp;#34;SELECT COUNT(*), SUM(amount) FROM contracts WHERE amount &amp;gt; $1 AND year = $2 AND provider_region = $3\&amp;#34;, \&amp;#34;params\&amp;#34;: [100000, 2025, \&amp;#34;EU\&amp;#34;]}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El sistema ejecuta la tool y devuelve:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;tool_call_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;count\&amp;#34;: 47, \&amp;#34;total\&amp;#34;: 8300000}&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El LLM recibe ese mensaje como continuación de la conversación y decide si necesita más información o puede generar la respuesta final.&lt;/p>
&lt;h3 id="soporte-en-modelos-oss">Soporte en modelos OSS&lt;/h3>
&lt;p>En 2026, el soporte de function calling nativo (no emulado vía system prompt) está disponible en:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 / 3.3+&lt;/strong>: formato de tool call nativo, soportado en vLLM con &lt;code>--enable-auto-tool-choice --tool-call-parser llama3_json&lt;/code>&lt;/li>
&lt;li>&lt;strong>Qwen 2.5+&lt;/strong>: soporte nativo con &lt;code>--tool-call-parser hermes&lt;/code>&lt;/li>
&lt;li>&lt;strong>Mistral NeMo / Mistral 7B Instruct v0.3+&lt;/strong>: formato nativo con &lt;code>--tool-call-parser mistral&lt;/code>&lt;/li>
&lt;li>&lt;strong>Qwen3 (2025+)&lt;/strong>: soporte nativo extendido con parallel tool calling&lt;/li>
&lt;/ul>
&lt;p>Sin &lt;code>--enable-auto-tool-choice&lt;/code>, vLLM puede forzar el formato de tool call vía guided decoding (JSON Schema), pero la capacidad de &lt;em>decidir cuándo invocar&lt;/em> una tool vs generar texto libre requiere entrenamiento específico del modelo.&lt;/p>
&lt;h2 id="el-patrón-react-reasoning--acting">El patrón ReAct: Reasoning + Acting&lt;/h2>
&lt;p>ReAct (Yao et al., 2022) es el bucle que convierte un LLM con tools en un agente con razonamiento. La idea central es que el modelo intercala &lt;strong>pensamientos&lt;/strong> (razonamiento explícito sobre qué hacer) con &lt;strong>acciones&lt;/strong> (invocación de tools) y &lt;strong>observaciones&lt;/strong> (resultados recibidos). Este bucle se repite hasta que el modelo estima tener suficiente información para responder, o hasta que se alcanza el límite de iteraciones.&lt;/p>
&lt;pre tabindex="0">&lt;code>Thought → Action → Observation → Thought → Action → Observation → ... → Response
&lt;/code>&lt;/pre>&lt;h3 id="trace-react-completo-una-query-de-producción-real">Trace ReAct completo: una query de producción real&lt;/h3>
&lt;p>Query: &lt;em>&amp;quot;¿Cuántos contratos de más de 100k€ firmamos en 2025 con proveedores de la UE, y cuáles son las condiciones especiales que se repiten?&amp;quot;&lt;/em>&lt;/p>
&lt;p>Esta query requiere dos fuentes distintas: los números exactos viven en SQL, y el contexto de condiciones especiales vive en los PDFs de contratos indexados en el vector store.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Iteración 1&lt;/strong>&lt;/p>
&lt;p>&lt;em>Thought:&lt;/em> La query pide datos agregados (conteo y suma) con filtros exactos sobre campos estructurados (importe, año, región). Esto es una consulta SQL, no una búsqueda semántica.&lt;/p>
&lt;p>&lt;em>Action:&lt;/em> &lt;code>sql_query&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">num_contratos&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SUM&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">total_eur&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">contracts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">year&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">provider_region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>params: &lt;code>[100000, 2025, &amp;quot;EU&amp;quot;]&lt;/code>&lt;/p>
&lt;p>&lt;em>Observation:&lt;/em> &lt;code>{&amp;quot;num_contratos&amp;quot;: 47, &amp;quot;total_eur&amp;quot;: 8300000}&lt;/code>&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Iteración 2&lt;/strong>&lt;/p>
&lt;p>&lt;em>Thought:&lt;/em> Tengo los números. Ahora necesito las condiciones especiales que se repiten en estos contratos. Ese tipo de información está en el texto libre de los PDFs, no en la tabla estructurada. Voy al vector store.&lt;/p>
&lt;p>&lt;em>Action:&lt;/em> &lt;code>vector_search&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;contratos proveedores UE 2025 importe superior 100k condiciones especiales cláusulas recurrentes&amp;#34;
&lt;/code>&lt;/pre>&lt;p>top_k: 5&lt;/p>
&lt;p>&lt;em>Observation:&lt;/em>&lt;/p>
&lt;pre tabindex="0">&lt;code>[chunk_1] Contrato Proveedor A — cláusula 8.3: penalización por retraso del 2% mensual sobre el importe facturado...
[chunk_2] Contrato Proveedor C — cláusula 7.1: revisión de precios vinculada al IPC de la zona euro con tope del 4% anual...
[chunk_3] Contrato Proveedor B — cláusula 8.3: penalización por retraso del 2% mensual...
&lt;/code>&lt;/pre>&lt;hr>
&lt;p>&lt;em>Thought:&lt;/em> Tengo datos suficientes de ambas fuentes. La cláusula 8.3 de penalización por retraso aparece en al menos dos contratos de los recuperados. Puedo sintetizar la respuesta.&lt;/p>
&lt;p>&lt;em>Response:&lt;/em> &amp;ldquo;En 2025 firmasteis &lt;strong>47 contratos&lt;/strong> con proveedores de la UE por importe superior a 100.000 €, con un total acumulado de &lt;strong>8,3 M€&lt;/strong>. La condición especial que aparece con más frecuencia en los contratos de mayor valor es la cláusula de penalización por retraso (8.3 en los modelos de contrato de Proveedor A y B): 2% mensual sobre importe facturado. También se repite la revisión de precios vinculada al IPC de la zona euro con tope del 4% anual (cláusula 7.1).&amp;rdquo;&lt;/p>
&lt;hr>
&lt;p>Este trace tiene &lt;strong>2 iteraciones&lt;/strong>. Una query más compleja (comparar con el año anterior, cruzar con estado de pago) añadiría iteraciones adicionales. La práctica habitual es fijar un límite de &lt;strong>5–10 iteraciones&lt;/strong> máximo para evitar bucles.&lt;/p>
&lt;h2 id="las-tres-fuentes-y-cuándo-usar-cada-una">Las tres fuentes y cuándo usar cada una&lt;/h2>
&lt;p>La elección de fuente no es arbitraria ni queda sólo en manos del LLM: la arquitectura define qué tools existen y cómo se describen. La tabla siguiente resume los criterios de selección:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Característica&lt;/th>
&lt;th>Vector store (Qdrant)&lt;/th>
&lt;th>SQL / estructurado (PostgreSQL)&lt;/th>
&lt;th>Web search&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Tipo de dato&lt;/strong>&lt;/td>
&lt;td>Texto libre, documentos, PDFs&lt;/td>
&lt;td>Tablas con esquema fijo&lt;/td>
&lt;td>Páginas públicas, noticias&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Estructura&lt;/strong>&lt;/td>
&lt;td>No estructurado&lt;/td>
&lt;td>Altamente estructurado&lt;/td>
&lt;td>Semi-estructurado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Temporalidad&lt;/strong>&lt;/td>
&lt;td>Índice estático (actualización periódica)&lt;/td>
&lt;td>Tiempo real (transaccional)&lt;/td>
&lt;td>Tiempo real (crawl)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Latencia típica&lt;/strong>&lt;/td>
&lt;td>5–50 ms&lt;/td>
&lt;td>10–200 ms&lt;/td>
&lt;td>200–2.000 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Privacidad&lt;/strong>&lt;/td>
&lt;td>Datos internos, soberanía total&lt;/td>
&lt;td>Datos internos, soberanía total&lt;/td>
&lt;td>Solo datos públicos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Query natural&lt;/strong>&lt;/td>
&lt;td>Sí (lenguaje natural → embedding)&lt;/td>
&lt;td>No (SQL parametrizado)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Agregaciones exactas&lt;/strong>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Sí (&lt;code>COUNT&lt;/code>, &lt;code>SUM&lt;/code>, &lt;code>GROUP BY&lt;/code>)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo usar&lt;/strong>&lt;/td>
&lt;td>Contexto documental, semántica, PDFs&lt;/td>
&lt;td>Métricas, conteos, filtros exactos, joins&lt;/td>
&lt;td>Datos que no existen internamente y son públicos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla práctica más importante: si la pregunta contiene palabras como &amp;ldquo;cuántos&amp;rdquo;, &amp;ldquo;suma&amp;rdquo;, &amp;ldquo;total&amp;rdquo;, &amp;ldquo;más de X&amp;rdquo;, &amp;ldquo;en el año Y&amp;rdquo; y los datos están en una tabla estructurada, la respuesta correcta es &lt;code>sql_query&lt;/code>. Si la pregunta pide contexto, explicaciones, cláusulas, procedimientos o ejemplos de documentos, la respuesta es &lt;code>vector_search&lt;/code>. Si pide el precio actual de algo o noticias recientes sobre un tercero, &lt;code>web_search&lt;/code> — pero sólo si no hay soberanía de datos implicada.&lt;/p>
&lt;h2 id="tool-routing-cómo-el-llm-elige-el-tool-correcto">Tool routing: cómo el LLM elige el tool correcto&lt;/h2>
&lt;p>La descripción de cada tool en el system prompt es &lt;strong>el factor más crítico&lt;/strong> para la precisión del routing. Un LLM con buenas capacidades de function calling puede elegir mal si las descripciones son ambiguas o se solapan.&lt;/p>
&lt;h3 id="descripciones-que-funcionan-vs-las-que-no">Descripciones que funcionan vs las que no&lt;/h3>
&lt;p>&lt;strong>Descripción débil&lt;/strong> (lleva al LLM a usar el tool equivocado):&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;search_docs&amp;#34; — Busca información en las fuentes disponibles.
&amp;#34;query_data&amp;#34; — Obtiene datos del sistema.
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Descripción fuerte&lt;/strong> (delimita con precisión cuándo usar cada uno):&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;vector_search&amp;#34; — Search internal company documents about policies, contracts and procedures.
Use when the query requires unstructured text, document context or semantic
similarity. NOT for counts, sums or exact filters.
&amp;#34;sql_query&amp;#34; — Query the SQL database for structured metrics, counts, aggregations and
financial data. Use when the query requires exact numbers, filters, sums or
joins over structured records. NOT for finding document context.
&lt;/code>&lt;/pre>&lt;p>La diferencia está en dos elementos: (1) ejemplos de casos de uso positivos, y (2) exclusiones explícitas con &lt;code>NOT for&lt;/code>. Ambos reducen el solapamiento semántico entre tools y mejoran la tool selection accuracy.&lt;/p>
&lt;h3 id="parallel-tool-calling">Parallel tool calling&lt;/h3>
&lt;p>Cuando dos tools son independientes entre sí — es decir, el resultado de una no afecta a la query de la otra — el LLM puede invocarlas simultáneamente en el mismo turno:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool_calls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_02&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vector_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El sistema ejecuta ambas en paralelo y devuelve ambas observaciones antes del siguiente turno del LLM. Esto reduce la latencia total cuando las queries son independientes: en vez de 2 iteraciones secuenciales (2 × latencia_tool), se paga 1 × max(latencia_sql, latencia_vector). Para el ejemplo del detective: si necesita tanto los registros contables como los testimonios para responder, puede pedirlos a la vez.&lt;/p>
&lt;h3 id="tool-selection-accuracy-la-métrica-de-eval">Tool selection accuracy: la métrica de eval&lt;/h3>
&lt;p>La &lt;strong>tool selection accuracy&lt;/strong> es el porcentaje de turns en que el LLM elige el tool correcto dado un conjunto de queries evaluadas:&lt;/p>
&lt;p>[
\text{TSA} = \frac{\text{turns con tool correcto elegido}}{\text{total turns con tool call esperada}}
]&lt;/p>
&lt;p>Se mide sobre un eval set sintético construido con triples &lt;code>(query, tool_esperado, args_esperados)&lt;/code>. Un ejemplo de eval set mínimo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Query&lt;/th>
&lt;th>Tool esperado&lt;/th>
&lt;th>Indicador de fallo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;¿Cuántos pedidos en marzo?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>sql_query&lt;/code>&lt;/td>
&lt;td>LLM usa &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Qué dice la política de devoluciones?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>vector_search&lt;/code>&lt;/td>
&lt;td>LLM usa &lt;code>sql_query&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Cuál es el precio del cobre hoy?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>web_search&lt;/code>&lt;/td>
&lt;td>LLM usa &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Suma los contratos del Q3&amp;rdquo;&lt;/td>
&lt;td>&lt;code>sql_query&lt;/code>&lt;/td>
&lt;td>LLM usa &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Una TSA &amp;lt; 0,85 en un agente de producción es señal de que las descripciones de tools necesitan revisión antes que el modelo. Para más detalle sobre cómo construir estos evals, ver &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">evals LLM&lt;/a>.&lt;/p>
&lt;h2 id="manejo-de-errores-en-tool-calls">Manejo de errores en tool calls&lt;/h2>
&lt;h3 id="sql-injection-via-prompt">SQL injection via prompt&lt;/h3>
&lt;p>El riesgo más serio del tool-augmented retrieval es que el LLM genere SQL malicioso — bien porque un usuario lo indujo via prompt injection, bien porque el modelo alucinó una query destructiva. Este vector de ataque se cubre en detalle en &lt;a href="https://blog.lo0.es/posts/llm-guard-fundamentos/">LLM Guard&lt;/a>, pero las reglas mínimas del lado del tool son:&lt;/p>
&lt;p>&lt;strong>Regla 1: Queries parametrizadas siempre, nunca interpolación directa.&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NUNCA esto:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cursor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;SELECT * FROM contracts WHERE provider = &amp;#39;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">llm_output&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#39;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Siempre esto:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cursor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;SELECT * FROM contracts WHERE provider = $1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">llm_output&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Regla 2: Usuario de BD con permisos mínimos.&lt;/strong> El usuario con el que el agente ejecuta SQL debe tener &lt;code>SELECT&lt;/code> sobre las tablas necesarias y nada más. Ningún &lt;code>DROP&lt;/code>, &lt;code>INSERT&lt;/code>, &lt;code>UPDATE&lt;/code> ni &lt;code>DELETE&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Regla 3: Allowlist de tablas.&lt;/strong> El sistema valida que la query generada por el LLM sólo referencia tablas en una allowlist antes de ejecutarla.&lt;/p>
&lt;p>&lt;strong>Regla 4: Timeout por query.&lt;/strong> Queries que bloquean demasiado tiempo — potencialmente inducidas para hacer DoS a la BD — se cancelan con timeout configurado.&lt;/p>
&lt;h3 id="rate-limits-timeouts-y-errores-de-tool">Rate limits, timeouts y errores de tool&lt;/h3>
&lt;p>Cuando una tool falla, el error se devuelve al LLM como observación:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;tool_call_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;error\&amp;#34;: \&amp;#34;timeout after 5s\&amp;#34;, \&amp;#34;tool\&amp;#34;: \&amp;#34;web_search\&amp;#34;}&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El system prompt debe instruir al LLM sobre qué hacer en este caso:&lt;/p>
&lt;pre tabindex="0">&lt;code>If a tool returns an error or is unavailable, acknowledge the limitation in your response.
Do not retry more than once. If web_search is unavailable, state that real-time data
is not accessible at this moment and answer with available internal sources only.
&lt;/code>&lt;/pre>&lt;p>Esto evita que el agente entre en bucles de reintentos y gestiona la degradación graceful: si &lt;code>web_search&lt;/code> no está disponible, responde con lo que tiene en las fuentes internas.&lt;/p>
&lt;h2 id="diagrama-del-bucle-react-con-las-tres-fuentes">Diagrama del bucle ReAct con las tres fuentes&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 480" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Bucle ReAct con tres fuentes de datos">
&lt;style>
.rb{fill:#f8f8f8;stroke:#444;stroke-width:1.4}
.rh{fill:#7aafff;stroke:#444;stroke-width:1.4}
.ry{fill:#ffd76b;stroke:#444;stroke-width:1.4}
.rg{fill:#b2e8b2;stroke:#444;stroke-width:1.4}
.rr{fill:#ffb3b3;stroke:#444;stroke-width:1.4}
.rp{fill:#e0c8ff;stroke:#444;stroke-width:1.4}
.rl{font:600 13px sans-serif;fill:#222}
.rs{font:400 11px sans-serif;fill:#555}
.ri{font:italic 11px sans-serif;fill:#555}
.ra{stroke:#555;stroke-width:1.5;fill:none;marker-end:url(#mra)}
.rloop{stroke:#999;stroke-width:1.2;fill:none;stroke-dasharray:5 3;marker-end:url(#mra)}
&lt;/style>
&lt;defs>&lt;marker id="mra" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#555"/>&lt;/marker>&lt;/defs>
&lt;!-- Query entrada -->
&lt;rect x="310" y="10" width="200" height="50" rx="8" class="rb"/>
&lt;text x="410" y="32" text-anchor="middle" class="rl">Query de usuario&lt;/text>
&lt;text x="410" y="50" text-anchor="middle" class="rs">mensaje de rol user&lt;/text>
&lt;!-- LLM: Thought -->
&lt;rect x="290" y="90" width="240" height="60" rx="8" class="rh"/>
&lt;text x="410" y="114" text-anchor="middle" class="rl">LLM — Thought&lt;/text>
&lt;text x="410" y="132" text-anchor="middle" class="rs">razona qué necesita&lt;/text>
&lt;text x="410" y="146" text-anchor="middle" class="rs">y qué tool invocar&lt;/text>
&lt;!-- Action -->
&lt;rect x="290" y="185" width="240" height="50" rx="8" class="ry"/>
&lt;text x="410" y="207" text-anchor="middle" class="rl">Action&lt;/text>
&lt;text x="410" y="223" text-anchor="middle" class="rs">genera tool_call JSON&lt;/text>
&lt;!-- Router de tool -->
&lt;rect x="290" y="265" width="240" height="50" rx="8" class="rp"/>
&lt;text x="410" y="287" text-anchor="middle" class="rl">Tool router (sistema)&lt;/text>
&lt;text x="410" y="303" text-anchor="middle" class="rs">despacha la llamada&lt;/text>
&lt;!-- Tres fuentes -->
&lt;rect x="30" y="355" width="160" height="70" rx="8" class="ry"/>
&lt;text x="110" y="378" text-anchor="middle" class="rl">vector_search&lt;/text>
&lt;text x="110" y="396" text-anchor="middle" class="rs">Qdrant&lt;/text>
&lt;text x="110" y="412" text-anchor="middle" class="rs">5–50 ms&lt;/text>
&lt;rect x="330" y="355" width="160" height="70" rx="8" class="rg"/>
&lt;text x="410" y="378" text-anchor="middle" class="rl">sql_query&lt;/text>
&lt;text x="410" y="396" text-anchor="middle" class="rs">PostgreSQL&lt;/text>
&lt;text x="410" y="412" text-anchor="middle" class="rs">10–200 ms&lt;/text>
&lt;rect x="630" y="355" width="160" height="70" rx="8" class="rr"/>
&lt;text x="710" y="378" text-anchor="middle" class="rl">web_search&lt;/text>
&lt;text x="710" y="396" text-anchor="middle" class="rs">API externa&lt;/text>
&lt;text x="710" y="412" text-anchor="middle" class="rs">200–2.000 ms&lt;/text>
&lt;!-- Observación -->
&lt;rect x="290" y="355" width="0" height="0" rx="8"/>
&lt;!-- Flechas principales -->
&lt;path class="ra" d="M410,60 L410,88"/>
&lt;path class="ra" d="M410,150 L410,183"/>
&lt;path class="ra" d="M410,235 L410,263"/>
&lt;path class="ra" d="M370,315 L190,353"/>
&lt;path class="ra" d="M410,315 L410,353"/>
&lt;path class="ra" d="M450,315 L630,353"/>
&lt;!-- Flechas de observación de vuelta al LLM (líneas punteadas) -->
&lt;path class="rloop" d="M110,355 Q60,260 280,145"/>
&lt;path class="rloop" d="M410,355 Q510,310 530,150"/>
&lt;path class="rloop" d="M710,355 Q780,260 540,145"/>
&lt;!-- Etiquetas de observación -->
&lt;text x="60" y="255" class="ri">Observation&lt;/text>
&lt;text x="548" y="255" class="ri">Observation&lt;/text>
&lt;text x="726" y="255" class="ri">Observation&lt;/text>
&lt;!-- Respuesta final -->
&lt;rect x="600" y="90" width="190" height="60" rx="8" class="rg"/>
&lt;text x="695" y="114" text-anchor="middle" class="rl">Response&lt;/text>
&lt;text x="695" y="132" text-anchor="middle" class="rs">cuando el LLM tiene&lt;/text>
&lt;text x="695" y="148" text-anchor="middle" class="rs">suficiente evidencia&lt;/text>
&lt;!-- Flecha a respuesta -->
&lt;path class="ra" d="M530,120 L598,120"/>
&lt;!-- Límite iteraciones -->
&lt;rect x="0" y="185" width="240" height="50" rx="8" class="rb"/>
&lt;text x="120" y="207" text-anchor="middle" class="rl">Límite de iteraciones&lt;/text>
&lt;text x="120" y="223" text-anchor="middle" class="rs">máx. 5–10 turns&lt;/text>
&lt;path class="rloop" d="M290,210 L242,210"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="matemáticas-de-latencia-del-pipeline-react">Matemáticas de latencia del pipeline ReAct&lt;/h2>
&lt;p>Cada iteración del bucle ReAct tiene tres componentes de latencia:&lt;/p>
&lt;p>[
T_{\text{iter}} = \text{TTFT}&lt;em>{\text{LLM}} + T&lt;/em>{\text{tool}} + \Delta_{\text{context}}
]&lt;/p>
&lt;p>donde:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT_LLM&lt;/strong>: tiempo hasta el primer token del LLM (dominado por el prefill del contexto acumulado)&lt;/li>
&lt;li>&lt;strong>T_tool&lt;/strong>: tiempo de ejecución de la tool&lt;/li>
&lt;li>&lt;strong>Δ_context&lt;/strong>: overhead de context window creciente (cada iteración añade el output anterior al contexto)&lt;/li>
&lt;/ul>
&lt;h3 id="valores-de-referencia-llama-31-70b-en-4h100-sxm-320-gb-nvlink">Valores de referencia: Llama-3.1-70B en 4×H100 SXM (320 GB, NVLink)&lt;/h3>
&lt;p>Con Llama-3.1-70B en FP8 en un nodo con 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s), los valores típicos en producción son:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Componente&lt;/th>
&lt;th>Valor&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT con contexto &amp;lt; 4k tokens&lt;/td>
&lt;td>≈ 150 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT con contexto 8k tokens&lt;/td>
&lt;td>≈ 220 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sql_query&lt;/code> (query simple, índice)&lt;/td>
&lt;td>≈ 50 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vector_search&lt;/code> (top-5, Qdrant en RAM)&lt;/td>
&lt;td>≈ 20 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>web_search&lt;/code> (API externa)&lt;/td>
&lt;td>≈ 600 ms&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="latencia-total-según-número-de-iteraciones">Latencia total según número de iteraciones&lt;/h3>
&lt;p>&lt;strong>Pipeline de 1 iteración&lt;/strong> (query simple, una sola tool):&lt;/p>
&lt;p>[
T_1 = 150 + 50 = 200 \text{ ms} + \text{síntesis final} \approx 200 + 300 = 500 \text{ ms}
]&lt;/p>
&lt;p>&lt;strong>Pipeline de 2 iteraciones&lt;/strong> (SQL + vector_search secuenciales):&lt;/p>
&lt;p>[
T_2 = (150 + 50) + (180 + 20) + 400 = 800 \text{ ms}
]&lt;/p>
&lt;p>El contexto en la segunda iteración ya incluye el resultado de la primera, por lo que el TTFT sube ligeramente a ≈ 180 ms.&lt;/p>
&lt;p>&lt;strong>Pipeline de 3 iteraciones&lt;/strong> (el caso más común en queries complejas):&lt;/p>
&lt;p>[
T_3 = (150 + 50) + (180 + 20) + (200 + 50) + 450 \approx 1.100 \text{ ms}
]&lt;/p>
&lt;p>&lt;strong>Parallel tool calling&lt;/strong> (SQL + vector_search en paralelo, 1 sola iteración):&lt;/p>
&lt;p>[
T_{\text{parallel}} = 150 + \max(50, 20) + 400 = 600 \text{ ms}
]&lt;/p>
&lt;p>Cuando las dos queries son independientes, el parallel tool calling recorta la latencia de ≈ 800 ms a ≈ 600 ms: un 25% de mejora para el caso de 2 iteraciones secuenciales.&lt;/p>
&lt;h3 id="comparación-con-rag-naive">Comparación con RAG naive&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuración&lt;/th>
&lt;th>Latencia&lt;/th>
&lt;th>Queries que puede responder&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>RAG naive (1 retriever, 1 paso)&lt;/td>
&lt;td>≈ 300 ms&lt;/td>
&lt;td>Queries de contexto documental&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 1 iteración (SQL)&lt;/td>
&lt;td>≈ 500 ms&lt;/td>
&lt;td>Queries de agregación estructurada&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 2 iteraciones (SQL + vector)&lt;/td>
&lt;td>≈ 800 ms&lt;/td>
&lt;td>Queries híbridas numérico + contexto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 3 iteraciones&lt;/td>
&lt;td>≈ 1.100 ms&lt;/td>
&lt;td>Queries complejas multi-fuente&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct con web_search&lt;/td>
&lt;td>≈ 1.500 ms&lt;/td>
&lt;td>Queries que requieren datos en tiempo real&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La conclusión práctica: ReAct multi-hop es &lt;strong>3–5× más lento que un RAG naive de un solo paso&lt;/strong>. La ganancia no está en la velocidad sino en la &lt;strong>cobertura de queries&lt;/strong>: el RAG naive no puede responder &amp;ldquo;¿cuántos contratos?&amp;rdquo; porque esa respuesta no está en ningún chunk de texto. Para aplicaciones con SLO de latencia estricto (&amp;lt; 500 ms), hay que diseñar si el caso de uso realmente necesita ReAct o si un RAG bien configurado con hybrid retrieval cubre el 90% de las queries.&lt;/p>
&lt;h2 id="hardware-on-premise-para-agentes-react">Hardware on-premise para agentes ReAct&lt;/h2>
&lt;p>Un agente ReAct con Llama-3.1-70B en producción tiene requisitos distintos a un RAG naive porque el contexto crece con cada iteración y el throughput de prefill es más crítico.&lt;/p>
&lt;p>&lt;strong>Configuración recomendada: 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Llama-3.1-70B en FP8: cabe en 2×H100 (70B params × 1 byte/param ≈ 70 GB + KV cache). Con 4×H100 se puede servir en tensor parallelism TP=4, reduciendo el TTFT por prefill en ≈ 2×.&lt;/li>
&lt;li>Instancia de Qdrant: se puede colocar en el mismo nodo (si la colección cabe en RAM) o en nodo dedicado. Para colecciones &amp;lt; 50M vectores de 768 dims: ≈ 150 GB, cabe en RAM de un servidor dual-socket.&lt;/li>
&lt;li>PostgreSQL: nodo separado o instancia gestionada. El agente no añade carga inusual al SQL — las queries son simples y acotadas por timeout.&lt;/li>
&lt;li>vLLM con &lt;code>--enable-auto-tool-choice --tool-call-parser llama3_json --max-model-len 16384&lt;/code>: el contexto de 16k tokens cubre con holgura los 5–10 turns de un pipeline ReAct.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Configuración mínima viable: 2×H100 SXM (160 GB)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Llama-3.1-70B en FP8 en TP=2. TTFT ≈ 250–300 ms para contextos de 4k tokens (aumento del 60–100% sobre TP=4).&lt;/li>
&lt;li>Sirve para workloads internos con &amp;lt; 20 requests concurrentes.&lt;/li>
&lt;li>No recomendable para SLO &amp;lt; 1 s con más de 5 usuarios concurrentes y contexto largo.&lt;/li>
&lt;/ul>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;p>&lt;strong>Agentic retrieval loops con planificación.&lt;/strong> ReAct es el patrón más simple de agente. Cuando una query requiere descomposición en sub-tareas con dependencias, se necesitan frameworks de orquestación como LangGraph (grafos de estados), smolagents (Hugging Face, agentes con código Python como actions) o llama-index Agents (pipeline de planning + retrieval). Estos introducen un paso de planificación previo al bucle de ejecución.&lt;/p>
&lt;p>&lt;strong>MCP (Model Context Protocol).&lt;/strong> El estándar emergente de Anthropic — con implementaciones OSS — para definir tools de forma portable entre frameworks y hosts. En lugar de definir el JSON Schema de cada tool por separado en cada aplicación, MCP centraliza esas definiciones en un servidor MCP que cualquier cliente compatible puede descubrir e invocar. La adopción en 2025–2026 es rápida entre frameworks OSS (LangChain, smolagents, OpenWebUI).&lt;/p>
&lt;p>&lt;strong>Tool caching.&lt;/strong> Si el mismo tool call (mismos argumentos, misma tool) se va a invocar múltiples veces dentro del mismo contexto o en contextos muy similares, se puede cachear el resultado. El mecanismo es análogo al semantic cache descrito para RAG: antes de ejecutar el tool, se compara el hash de los argumentos (o su embedding para matching semántico) contra una caché con TTL. Especialmente valioso para &lt;code>sql_query&lt;/code> con queries frecuentes y datos que cambian poco.&lt;/p>
&lt;p>&lt;strong>Multi-agent.&lt;/strong> Cuando un agente orquestador delega sub-tareas a agentes especializados — uno para SQL, otro para recuperación de documentos, otro para generación de código — se entra en el territorio de los sistemas multi-agente. Cada sub-agente puede tener su propio set de tools y su propio LLM (posiblemente más pequeño y especializado). La coordinación entre agentes introduce complejidad de trazado y observabilidad adicional.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/">RAG con reranker e hybrid retrieval&lt;/a> — el retriever que se invoca cuando el LLM elige &lt;code>vector_search&lt;/code> es exactamente el pipeline descrito allí: dense + sparse + reranker cruzado&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">Structured output: fundamentos&lt;/a> — el JSON Schema que define el contrato de cada tool call es exactamente structured output aplicado a la interfaz herramienta&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/router-inferencia-llm-gateway-l7/">Router de inferencia y LLM gateway L7&lt;/a> — el gateway L7 que recibe las requests del agente ReAct y enruta al LLM correcto; también aplica rate limiting por usuario y tenant&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/llm-guard-fundamentos/">LLM Guard: fundamentos&lt;/a> — SQL injection via prompt es un vector de ataque real en tool-augmented retrieval; LLM Guard cubre la detección de prompt injection antes de que el request llegue al LLM&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de seis etapas&lt;/a> — tool-augmented retrieval vive en la intersección de las etapas Deploy y Observe del pipeline: se despliega como parte del sistema de inferencia y se observa vía tracing de cada turn del agente&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals LLM: la capa después del tracing&lt;/a> — tool selection accuracy es la métrica de eval crítica para un agente ReAct; el golden dataset de eval debe incluir triples (query, tool esperado, args esperados)&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Yao, S. et al. (2022). &lt;em>ReAct: Synergizing Reasoning and Acting in Language Models&lt;/em>. arXiv:2210.03629. &lt;a href="https://arxiv.org/abs/2210.03629">https://arxiv.org/abs/2210.03629&lt;/a>&lt;/li>
&lt;li>vLLM documentation. &lt;em>Tool calling&lt;/em>. &lt;a href="https://docs.vllm.ai/en/stable/features/tool_calling.html">https://docs.vllm.ai/en/stable/features/tool_calling.html&lt;/a>&lt;/li>
&lt;li>Qdrant documentation. &lt;em>Search&lt;/em>. &lt;a href="https://qdrant.tech/documentation/concepts/search/">https://qdrant.tech/documentation/concepts/search/&lt;/a>&lt;/li>
&lt;li>OpenAI. &lt;em>Function calling&lt;/em>. &lt;a href="https://platform.openai.com/docs/guides/function-calling">https://platform.openai.com/docs/guides/function-calling&lt;/a>&lt;/li>
&lt;li>Meta AI. &lt;em>Llama 3.1 Model Card&lt;/em>. &lt;a href="https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md">https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md&lt;/a>&lt;/li>
&lt;li>Qwen Team (Alibaba). &lt;em>Qwen2.5 Technical Report&lt;/em>. arXiv:2412.15115. &lt;a href="https://arxiv.org/abs/2412.15115">https://arxiv.org/abs/2412.15115&lt;/a>&lt;/li>
&lt;li>Anthropic. &lt;em>Model Context Protocol&lt;/em>. &lt;a href="https://modelcontextprotocol.io">https://modelcontextprotocol.io&lt;/a>&lt;/li>
&lt;li>OWASP. &lt;em>LLM Top 10 for Large Language Model Applications&lt;/em>. LLM01: Prompt Injection. &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">https://owasp.org/www-project-top-10-for-large-language-model-applications/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Evaluar un RAG sin engañarse: RAGAS, el golden dataset y las cuatro métricas que importan</title><link>https://blog.lo0.es/posts/rag-eval-ragas-golden-dataset/</link><pubDate>Thu, 04 Jun 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/rag-eval-ragas-golden-dataset/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un pipeline RAG falla en modos que la satisfacción del usuario no distingue: el LLM puede alucinar incluso con buenos chunks, o el retrieval puede ignorar documentos clave aunque el LLM sintetice bien lo que recibe. RAGAS descompone la evaluación en cuatro métricas ortogonales —faithfulness, answer relevance, context precision y context recall— cada una apuntando a un sub-componente diferente. El golden dataset es el calibrador de referencia; sin él las métricas no tienen ancla. El stack completo corre 100 % on-premise con vLLM como judge y Langfuse para trazabilidad.&lt;/p>
&lt;hr>
&lt;h2 id="la-analogía-maestra-el-inspector-de-calidad-de-una-fábrica-de-muebles">La analogía maestra: el inspector de calidad de una fábrica de muebles&lt;/h2>
&lt;p>Imagina que fabricas sillas. Podrías preguntar a los clientes &amp;ldquo;¿es cómoda?&amp;rdquo; y punto. Pero esa pregunta no te dice qué arreglar cuando la respuesta es &amp;ldquo;no&amp;rdquo;. El inspector de calidad no pregunta eso: mide el tablero con dureza Shore, comprueba que cada pata tenga exactamente 45 cm, verifica que el manual de montaje incluya los doce tornillos del BOM y detecta si un tablero de densidad baja pasó el filtro de entrada.&lt;/p>
&lt;p>RAGAS es ese inspector aplicado a RAG:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Faithfulness&lt;/strong> → ¿el tablero tiene la dureza especificada? El LLM solo puede usar el material (chunks) que el retrieval le entrega.&lt;/li>
&lt;li>&lt;strong>Context Precision&lt;/strong> → ¿la pata tiene la longitud exacta? De los K chunks recuperados, ¿cuántos son realmente útiles o son relleno que confunde al ensamblador?&lt;/li>
&lt;li>&lt;strong>Context Recall&lt;/strong> → ¿el manual incluye todos los tornillos? De todos los hechos que debería contener la respuesta correcta, ¿cuántos aparecen en los chunks recuperados?&lt;/li>
&lt;li>&lt;strong>Noise Sensitivity&lt;/strong> → ¿si el operario usa un tablero de densidad media baja, se nota en el producto final? Si introduces chunks irrelevantes, ¿el LLM empieza a alucinar?&lt;/li>
&lt;/ul>
&lt;p>Sin medir cada dimensión por separado, el diagnóstico es opaco: &amp;ldquo;el RAG no funciona bien&amp;rdquo; no te dice si reparar el embedder, el reranker, el prompt o el corpus.&lt;/p>
&lt;hr>
&lt;h2 id="el-problema-de-evaluar-rag">El problema de evaluar RAG&lt;/h2>
&lt;p>La clasificación tiene una virtud incómoda: si predices 87 de 100 etiquetas correctamente, accuracy = 0,87. No hay ambigüedad. RAG no tiene esa gracia.&lt;/p>
&lt;p>Un sistema RAG puede fallar en al menos tres modos independientes:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Retrieval correcto, LLM alucina&lt;/strong>: los chunks contienen la respuesta correcta, pero el LLM genera afirmaciones que no están en esos chunks. Faithfulness baja; context recall alta.&lt;/li>
&lt;li>&lt;strong>LLM correcto, retrieval falla&lt;/strong>: el retrieval devuelve chunks irrelevantes (baja context precision) o incompletos (bajo context recall). Si el LLM tiene suficiente conocimiento paramétrico, puede parecer que responde bien, pero en realidad está ignorando el contexto — lo cual es una bomba de tiempo cuando el conocimiento paramétrico queda obsoleto.&lt;/li>
&lt;li>&lt;strong>Retrieval y LLM correctos, respuesta no responde la pregunta&lt;/strong>: la respuesta es fiel al contexto y los chunks son relevantes, pero la pregunta era otra. Answer relevance baja.&lt;/li>
&lt;/ol>
&lt;p>Cada modo requiere una métrica diferente y una acción correctiva diferente. Usar una métrica única (BLEU, ROUGE, satisfacción de usuario) mezcla las señales y hace imposible priorizar el trabajo de mejora.&lt;/p>
&lt;hr>
&lt;h2 id="las-cuatro-métricas-ragas">Las cuatro métricas RAGAS&lt;/h2>
&lt;h3 id="1-faithfulness--fidelidad-al-contexto">1. Faithfulness — fidelidad al contexto&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> ¿cuántas afirmaciones de la respuesta generada están soportadas por los chunks recuperados?&lt;/p>
&lt;p>&lt;strong>Cálculo:&lt;/strong>&lt;/p>
&lt;p>$$\text{Faithfulness} = \frac{|\text{claims soportados por el contexto}|}{|\text{total claims en la respuesta}|}$$&lt;/p>
&lt;p>El proceso usa un LLM-as-judge (ver https://blog.lo0.es/posts/llm-as-judge-fundamentos/): primero se extraen las afirmaciones atómicas de la respuesta (&amp;ldquo;el modelo fue lanzado en 2023&amp;rdquo;, &amp;ldquo;admite contextos de 128k tokens&amp;rdquo;, &amp;hellip;), luego el judge clasifica cada claim como &lt;em>supported&lt;/em> o &lt;em>not supported&lt;/em> por los chunks.&lt;/p>
&lt;p>&lt;strong>Ejemplo:&lt;/strong> La respuesta generada tiene 5 claims. El judge determina que 4 están en los chunks y 1 es una extrapolación sin respaldo.&lt;/p>
&lt;p>$$\text{Faithfulness} = \frac{4}{5} = 0{,}80$$&lt;/p>
&lt;p>&lt;strong>Señal de alarma:&lt;/strong> faithfulness &amp;lt; 0,85 indica que el LLM está generando contenido que va más allá del contexto — es decir, está alucinando con respaldo superficial.&lt;/p>
&lt;h3 id="2-answer-relevance--relevancia-de-la-respuesta">2. Answer Relevance — relevancia de la respuesta&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> ¿la respuesta realmente responde a la pregunta formulada?&lt;/p>
&lt;p>&lt;strong>Intuición:&lt;/strong> Una respuesta que responde bien a la pregunta &amp;ldquo;implica&amp;rdquo; esa pregunta. Si generas N preguntas hipotéticas a partir de la respuesta y mides su similitud semántica con la pregunta original, obtienes una señal de relevancia.&lt;/p>
&lt;p>&lt;strong>Cálculo:&lt;/strong>&lt;/p>
&lt;p>$$\text{AnswerRelevance} = \frac{1}{N} \sum_{i=1}^{N} \cos(\vec{q}&lt;em>{\text{original}}, \vec{q}&lt;/em>{i}^{\text{generada}})$$&lt;/p>
&lt;p>donde $\vec{q}$ son embeddings de las preguntas.&lt;/p>
&lt;p>&lt;strong>Ejemplo:&lt;/strong> Para la pregunta &amp;ldquo;¿Qué versiones de Python soporta FastAPI?&amp;rdquo; y una respuesta sobre frameworks web en general, las preguntas hipotéticas generadas versarán sobre &amp;ldquo;¿cuáles son los mejores frameworks web?&amp;rdquo; — coseno bajo con la pregunta original → answer relevance baja.&lt;/p>
&lt;h3 id="3-context-precision--precisión-del-retrieval">3. Context Precision — precisión del retrieval&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> de los K chunks recuperados, ¿qué proporción son realmente relevantes?&lt;/p>
&lt;p>&lt;strong>Cálculo&lt;/strong> (versión weighted):&lt;/p>
&lt;p>$$\text{ContextPrecision@K} = \frac{\sum_{k=1}^{K} \text{Precision@}k \cdot \mathbb{1}[\text{chunk}_k \text{ es relevante}]}{|\text{chunks relevantes en top-K}|}$$&lt;/p>
&lt;p>La forma más directa: el judge LLM clasifica cada chunk como relevante o no para responder la pregunta. La precisión es la fracción relevante.&lt;/p>
&lt;p>&lt;strong>Ejemplo:&lt;/strong> Se recuperan 5 chunks. El judge considera que 3 son relevantes y 2 son ruido.&lt;/p>
&lt;p>$$\text{ContextPrecision} = \frac{3}{5} = 0{,}60$$&lt;/p>
&lt;p>&lt;strong>Señal de alarma:&lt;/strong> precision &amp;lt; 0,6 indica que el retrieval está contaminando el contexto con información que puede contradecir o diluir la respuesta correcta.&lt;/p>
&lt;h3 id="4-context-recall--recall-del-retrieval">4. Context Recall — recall del retrieval&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> de todos los hechos necesarios para construir la respuesta correcta (ground-truth), ¿qué proporción están cubiertos por los chunks recuperados?&lt;/p>
&lt;p>&lt;strong>Cálculo:&lt;/strong>&lt;/p>
&lt;p>$$\text{ContextRecall} = \frac{|\text{claims del ground-truth atribuibles a algún chunk}|}{|\text{total claims en ground-truth}|}$$&lt;/p>
&lt;p>Esta métrica &lt;strong>requiere ground-truth&lt;/strong>, es decir, necesitas el golden dataset.&lt;/p>
&lt;p>&lt;strong>Ejemplo:&lt;/strong> El ground-truth tiene 6 afirmaciones. Los chunks recuperados cubren 5 de ellas; la sexta está en un documento que el retrieval no encontró.&lt;/p>
&lt;p>$$\text{ContextRecall} = \frac{5}{6} \approx 0{,}83$$&lt;/p>
&lt;h3 id="5-noise-sensitivity--sensibilidad-al-ruido">5. Noise Sensitivity — sensibilidad al ruido&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> si introduces chunks irrelevantes en el contexto, ¿cómo cae la faithfulness?&lt;/p>
&lt;p>Se calcula como la diferencia de faithfulness entre la condición limpia y la condición con ruido controlado:&lt;/p>
&lt;p>$$\Delta\text{Faithfulness} = \text{Faithfulness}&lt;em>{\text{limpio}} - \text{Faithfulness}&lt;/em>{\text{ruidoso}}$$&lt;/p>
&lt;p>Un $\Delta &amp;gt; 0{,}15$ indica que el prompt o el LLM son frágiles ante contextos contaminados — problema que context precision baja convierte en rutinario.&lt;/p>
&lt;hr>
&lt;h2 id="ejemplo-numérico-completo">Ejemplo numérico completo&lt;/h2>
&lt;h3 id="escenario">Escenario&lt;/h3>
&lt;p>&lt;strong>Pregunta:&lt;/strong> &amp;ldquo;¿Qué garantías de latencia ofrece Redis para operaciones de lectura en memoria con un dataset de 10 GB?&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Respuesta generada por el RAG:&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Redis garantiza latencias sub-milisegundo para operaciones GET en datasets que caben en RAM. Con 10 GB de dataset y clientes en la misma red local, P99 está típicamente por debajo de 1 ms. Redis también soporta clustering horizontal para escalar más allá de la RAM de un nodo. Además, Redis incluye soporte nativo para JSON desde la versión 7.2.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Ground-truth (respuesta de referencia):&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Redis opera completamente en memoria, lo que garantiza latencias sub-milisegundo para GET. En redes locales con datasets de 10 GB en RAM, el P99 se sitúa por debajo de 1 ms. El clustering permite escalar más allá de la RAM de un único nodo.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Chunks recuperados (5 chunks, fragmentos resumidos):&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Contenido resumido&lt;/th>
&lt;th>Relevante&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>C1&lt;/td>
&lt;td>&amp;ldquo;Redis opera en memoria; GET tiene latencias &amp;lt; 1 ms en LAN&amp;rdquo;&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C2&lt;/td>
&lt;td>&amp;ldquo;Redis Cluster permite sharding para escalar la RAM total&amp;rdquo;&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C3&lt;/td>
&lt;td>&amp;ldquo;Redis Sentinel gestiona alta disponibilidad mediante failover automático&amp;rdquo;&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C4&lt;/td>
&lt;td>&amp;ldquo;Benchmarks de Redis: P50 = 0,3 ms, P99 = 0,9 ms en 10 GB dataset&amp;rdquo;&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C5&lt;/td>
&lt;td>&amp;ldquo;Redis Stack añade módulos: RedisJSON, RediSearch, RedisTimeSeries&amp;rdquo;&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="cálculo-paso-a-paso">Cálculo paso a paso&lt;/h3>
&lt;p>&lt;strong>Faithfulness:&lt;/strong>&lt;/p>
&lt;p>Claims en la respuesta generada:&lt;/p>
&lt;ol>
&lt;li>&amp;ldquo;Redis garantiza latencias sub-milisegundo para GET en datasets en RAM&amp;rdquo; → &lt;strong>soportado&lt;/strong> por C1, C4&lt;/li>
&lt;li>&amp;ldquo;Con 10 GB en LAN, P99 &amp;lt; 1 ms&amp;rdquo; → &lt;strong>soportado&lt;/strong> por C4&lt;/li>
&lt;li>&amp;ldquo;Redis soporta clustering horizontal para escalar RAM&amp;rdquo; → &lt;strong>soportado&lt;/strong> por C2&lt;/li>
&lt;li>&amp;ldquo;Redis incluye soporte nativo para JSON desde la versión 7.2&amp;rdquo; → &lt;strong>NO soportado&lt;/strong> por ningún chunk (C5 menciona RedisJSON como módulo de Redis Stack, no como nativo de Redis core)&lt;/li>
&lt;/ol>
&lt;p>$$\text{Faithfulness} = \frac{3}{4} = 0{,}75$$&lt;/p>
&lt;p>El claim 4 es una extrapolación que mezcla información de C5 de forma imprecisa — alucinación parcial.&lt;/p>
&lt;p>&lt;strong>Context Precision:&lt;/strong>&lt;/p>
&lt;p>Chunks relevantes: C1, C2, C4 (3 de 5).&lt;/p>
&lt;p>$$\text{ContextPrecision} = \frac{3}{5} = 0{,}60$$&lt;/p>
&lt;p>C3 y C5 son ruido. C5 en particular contribuyó a la alucinación parcial sobre JSON.&lt;/p>
&lt;p>&lt;strong>Context Recall:&lt;/strong>&lt;/p>
&lt;p>Claims del ground-truth:&lt;/p>
&lt;ol>
&lt;li>&amp;ldquo;Redis opera en memoria, GET &amp;lt; 1 ms&amp;rdquo; → atribuible a C1 ✓&lt;/li>
&lt;li>&amp;ldquo;P99 &amp;lt; 1 ms en LAN con 10 GB&amp;rdquo; → atribuible a C4 ✓&lt;/li>
&lt;li>&amp;ldquo;Clustering escala más allá de la RAM de un nodo&amp;rdquo; → atribuible a C2 ✓&lt;/li>
&lt;/ol>
&lt;p>$$\text{ContextRecall} = \frac{3}{3} = 1{,}00$$&lt;/p>
&lt;p>El retrieval encontró todos los chunks necesarios para el ground-truth. El problema no es recall sino precision (C3, C5 contaminaron el contexto).&lt;/p>
&lt;p>&lt;strong>Answer Relevance:&lt;/strong>&lt;/p>
&lt;p>El judge genera 3 preguntas hipotéticas a partir de la respuesta:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;¿Qué latencias ofrece Redis para lecturas en memoria?&amp;rdquo; — cos = 0,91&lt;/li>
&lt;li>&amp;ldquo;¿Cómo escala Redis horizontalmente?&amp;rdquo; — cos = 0,74&lt;/li>
&lt;li>&amp;ldquo;¿Qué módulos JSON incluye Redis?&amp;rdquo; — cos = 0,52 (deriva de la alucinación)&lt;/li>
&lt;/ul>
&lt;p>$$\text{AnswerRelevance} = \frac{0{,}91 + 0{,}74 + 0{,}52}{3} = 0{,}72$$&lt;/p>
&lt;p>La derivación hacia JSON redujo la relevancia. Una respuesta más ajustada habría obtenido ~0,90.&lt;/p>
&lt;h3 id="resumen-del-ejemplo">Resumen del ejemplo&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Valor&lt;/th>
&lt;th>Diagnóstico&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Faithfulness&lt;/td>
&lt;td>0,75&lt;/td>
&lt;td>LLM extrapoló más allá del contexto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Precision&lt;/td>
&lt;td>0,60&lt;/td>
&lt;td>Retrieval devolvió 2 chunks irrelevantes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Recall&lt;/td>
&lt;td>1,00&lt;/td>
&lt;td>Retrieval capturó todo lo necesario&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Answer Relevance&lt;/td>
&lt;td>0,72&lt;/td>
&lt;td>Respuesta desvía el tema&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Acción correctiva principal:&lt;/strong> mejorar el reranker para filtrar C3 y C5 antes de que lleguen al LLM. El problema de faithfulness y relevance es consecuencia directa de la baja precision, no del LLM en sí.&lt;/p>
&lt;hr>
&lt;h2 id="construcción-del-golden-dataset">Construcción del golden dataset&lt;/h2>
&lt;h3 id="qué-es-y-por-qué-importa">Qué es y por qué importa&lt;/h3>
&lt;p>El golden dataset es un conjunto de tuplas &lt;code>(pregunta, chunks relevantes, respuesta correcta)&lt;/code> que actúa como calibrador de referencia. Sin él, context recall no se puede calcular (no hay ground-truth) y las demás métricas carecen de ancla interpretativa: ¿0,75 de faithfulness es bueno o malo para este corpus y este dominio?&lt;/p>
&lt;p>Un golden dataset bien construido permite:&lt;/p>
&lt;ul>
&lt;li>Comparar versiones del pipeline (embedder v1 vs v2, chunk size 512 vs 1024)&lt;/li>
&lt;li>Detectar regresiones en CI antes de desplegar&lt;/li>
&lt;li>Estratificar el análisis por tipo de pregunta&lt;/li>
&lt;/ul>
&lt;h3 id="pipeline-de-construcción-asistida-por-llm">Pipeline de construcción asistida por LLM&lt;/h3>
&lt;p>La construcción manual pura es cara. El patrón estándar en 2026 es asistencia LLM con revisión humana de muestra:&lt;/p>
&lt;p>&lt;strong>Paso 1 — Selección de chunks semilla.&lt;/strong> Del corpus total, seleccionar chunks representativos mediante muestreo estratificado (por sección, fecha, tipo de documento). Para un corpus técnico de 10.000 chunks, 500-1.000 semillas es un punto de partida razonable.&lt;/p>
&lt;p>&lt;strong>Paso 2 — Generación de preguntas.&lt;/strong> Un LLM potente (Llama-3.1-70B o similar) genera 2-3 preguntas por chunk semilla usando un prompt del tipo:&lt;/p>
&lt;pre tabindex="0">&lt;code>Dado el siguiente fragmento de documentación, genera preguntas específicas
que solo puedan responderse correctamente usando ESTE fragmento y no
conocimiento general. Las preguntas deben ser las que haría un ingeniero
buscando información operativa.
Fragmento: {chunk}
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Paso 3 — Generación de respuestas de referencia.&lt;/strong> El mismo LLM, con acceso al chunk semilla (y a chunks adyacentes si la pregunta lo requiere), genera la respuesta de referencia.&lt;/p>
&lt;p>&lt;strong>Paso 4 — Revisión humana de muestra.&lt;/strong> Revisar manualmente el 10-20 % del dataset generado. Los criterios de rechazo más comunes: preguntas triviales que cualquier LLM responde sin el corpus, respuestas que el LLM rellenó con conocimiento paramétrico en lugar de los chunks, y preguntas mal formuladas o ambiguas.&lt;/p>
&lt;h3 id="tamaño-mínimo">Tamaño mínimo&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Caso de uso&lt;/th>
&lt;th>Pares mínimos&lt;/th>
&lt;th>Notas&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Prototipo / validación inicial&lt;/td>
&lt;td>50-100&lt;/td>
&lt;td>Suficiente para detectar problemas gruesos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Corpus técnico en producción&lt;/td>
&lt;td>200-500&lt;/td>
&lt;td>Permite estratificación básica&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Producción robusta con estratificación completa&lt;/td>
&lt;td>500-1.000+&lt;/td>
&lt;td>Necesario para detectar regresiones sutiles&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="estratificación-del-dataset">Estratificación del dataset&lt;/h3>
&lt;p>Un golden dataset plano mide el promedio pero oculta los casos extremos. La estratificación mínima recomendada incluye tres tipos de preguntas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fáciles (single-hop):&lt;/strong> Un único chunk contiene toda la información necesaria. El baseline que cualquier RAG decente debe superar.&lt;/li>
&lt;li>&lt;strong>Difíciles (multi-hop):&lt;/strong> La respuesta correcta requiere combinar información de 2-4 chunks diferentes. Aquí se detectan los límites del reranker y del prompt de síntesis.&lt;/li>
&lt;li>&lt;strong>Adversariales:&lt;/strong> La pregunta tiene una premisa falsa, o el corpus no contiene la respuesta. El RAG correcto debe responder &amp;ldquo;no tengo información suficiente&amp;rdquo; — un RAG frágil alucina con confianza. Este tipo de pregunta mide directamente el riesgo de alucinación de alto impacto.&lt;/li>
&lt;/ul>
&lt;h3 id="la-trampa-de-goodhart">La trampa de Goodhart&lt;/h3>
&lt;blockquote>
&lt;p>&amp;ldquo;Cuando una medida se convierte en objetivo, deja de ser una buena medida.&amp;rdquo; — Charles Goodhart&lt;/p>
&lt;/blockquote>
&lt;p>Si optimizas el embedder o el reranker usando el golden dataset como función de pérdida, el dataset se corrompe como métrica: el sistema aprende a rendir bien en esas preguntas específicas sin mejorar en el dominio general.&lt;/p>
&lt;p>La solución es la misma que en ML supervisado: separar &lt;strong>dev set&lt;/strong> (para optimización e iteración) de &lt;strong>test set&lt;/strong> (para evaluación final, congelado y auditado). El test set nunca debe usarse para tomar decisiones de diseño; solo para reportar el estado del sistema en releases.&lt;/p>
&lt;hr>
&lt;h2 id="correlación-con-satisfacción-real">Correlación con satisfacción real&lt;/h2>
&lt;p>Los estudios de campo publicados por los equipos de Databricks (2024) y los análisis de adopción de RAGAS (2025) apuntan a umbrales operativos interpretables:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Rango de métrica&lt;/th>
&lt;th>Síntoma observable&lt;/th>
&lt;th>Acción correctiva&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Faithfulness &amp;lt; 0,75&lt;/td>
&lt;td>Usuarios reportan &amp;ldquo;respuestas inventadas&amp;rdquo; con frecuencia&lt;/td>
&lt;td>Revisar el prompt del LLM; aumentar instrucciones de cita; reducir temperatura&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Faithfulness 0,75-0,85&lt;/td>
&lt;td>Alucinaciones ocasionales en topics periféricos&lt;/td>
&lt;td>Mejorar context precision para eliminar chunks contaminantes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Faithfulness ≥ 0,85&lt;/td>
&lt;td>Correlaciona con NPS positivo en estudios de campo&lt;/td>
&lt;td>Mantener; monitorear deriva&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Precision &amp;lt; 0,60&lt;/td>
&lt;td>LLM incluye información contradictoria; respuestas inconsistentes&lt;/td>
&lt;td>Ajustar el reranker; reducir K; revisar umbrales de similitud&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Recall &amp;lt; 0,70&lt;/td>
&lt;td>Preguntas multi-hop fallidas; información clave ausente&lt;/td>
&lt;td>Revisar el chunking strategy; añadir chunks de mayor tamaño; enriquecer metadatos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Answer Relevance &amp;lt; 0,70&lt;/td>
&lt;td>Respuestas &amp;ldquo;correctas pero que no responden&amp;rdquo;&lt;/td>
&lt;td>Revisar el prompt de síntesis; añadir instrucción explícita de adherencia a la pregunta&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La context precision baja es especialmente perniciosa: chunks irrelevantes no son neutrales. Aumentan la probabilidad de que el LLM use información incorrecta como si fuera relevante, degradando faithfulness de forma encadenada. Es la transmisión por la que un problema de retrieval se convierte en un problema de LLM.&lt;/p>
&lt;hr>
&lt;h2 id="diagrama-el-bucle-de-evaluación-continua">Diagrama: el bucle de evaluación continua&lt;/h2>
&lt;figure>
&lt;svg viewBox="0 0 800 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="rag-eval-title rag-eval-desc" style="width:100%;max-width:800px;font-family:system-ui,sans-serif;">
&lt;title id="rag-eval-title">Bucle de evaluación RAG con RAGAS&lt;/title>
&lt;desc id="rag-eval-desc">Diagrama circular que muestra el flujo desde el corpus hasta la acción correctiva pasando por retrieval, LLM, respuesta, RAGAS judge y métricas con alertas.&lt;/desc>
&lt;!-- Fondo -->
&lt;rect width="800" height="420" fill="#0f1117" rx="12"/>
&lt;!-- Nodo: Corpus -->
&lt;rect x="30" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="85" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Corpus&lt;/text>
&lt;text x="85" y="207" text-anchor="middle" fill="#718096" font-size="10">documentos&lt;/text>
&lt;!-- Flecha Corpus → Retrieval -->
&lt;line x1="140" y1="195" x2="175" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Nodo: Retrieval -->
&lt;rect x="175" y="170" width="120" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="235" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Retrieval&lt;/text>
&lt;text x="235" y="207" text-anchor="middle" fill="#718096" font-size="10">top-K chunks&lt;/text>
&lt;!-- Flecha Retrieval → LLM -->
&lt;line x1="295" y1="195" x2="330" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Nodo: LLM -->
&lt;rect x="330" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="385" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">LLM&lt;/text>
&lt;text x="385" y="207" text-anchor="middle" fill="#718096" font-size="10">síntesis&lt;/text>
&lt;!-- Flecha LLM → Respuesta -->
&lt;line x1="440" y1="195" x2="475" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Nodo: Respuesta -->
&lt;rect x="475" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="530" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Respuesta&lt;/text>
&lt;text x="530" y="207" text-anchor="middle" fill="#718096" font-size="10">generada&lt;/text>
&lt;!-- Flecha Respuesta → RAGAS -->
&lt;line x1="585" y1="195" x2="620" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Nodo: RAGAS Judge -->
&lt;rect x="620" y="170" width="130" height="50" rx="8" fill="#1a2640" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="685" y="191" text-anchor="middle" fill="#93c5fd" font-size="12" font-weight="600">RAGAS Judge&lt;/text>
&lt;text x="685" y="207" text-anchor="middle" fill="#6b9fd4" font-size="10">LLM-as-judge&lt;/text>
&lt;!-- Flecha RAGAS → Métricas (bajando) -->
&lt;line x1="685" y1="220" x2="685" y2="285" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
&lt;!-- Nodo: Métricas -->
&lt;rect x="615" y="285" width="140" height="60" rx="8" fill="#1a2a1a" stroke="#22c55e" stroke-width="1.5"/>
&lt;text x="685" y="305" text-anchor="middle" fill="#86efac" font-size="12" font-weight="600">Métricas&lt;/text>
&lt;text x="685" y="320" text-anchor="middle" fill="#4ade80" font-size="9">faithfulness · precision&lt;/text>
&lt;text x="685" y="333" text-anchor="middle" fill="#4ade80" font-size="9">recall · relevance&lt;/text>
&lt;!-- Flecha Métricas → Alerta -->
&lt;line x1="615" y1="315" x2="510" y2="315" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-green)"/>
&lt;!-- Nodo: Alerta Grafana -->
&lt;rect x="390" y="285" width="120" height="60" rx="8" fill="#2a1a1a" stroke="#f97316" stroke-width="1.5"/>
&lt;text x="450" y="305" text-anchor="middle" fill="#fdba74" font-size="12" font-weight="600">Alerta&lt;/text>
&lt;text x="450" y="320" text-anchor="middle" fill="#fb923c" font-size="9">Prometheus&lt;/text>
&lt;text x="450" y="333" text-anchor="middle" fill="#fb923c" font-size="9">Grafana&lt;/text>
&lt;!-- Flecha Alerta → Acción -->
&lt;line x1="390" y1="315" x2="285" y2="315" stroke="#f97316" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
&lt;!-- Nodo: Acción -->
&lt;rect x="155" y="285" width="130" height="60" rx="8" fill="#1a1a2a" stroke="#a855f7" stroke-width="1.5"/>
&lt;text x="220" y="305" text-anchor="middle" fill="#d8b4fe" font-size="12" font-weight="600">Acción correctiva&lt;/text>
&lt;text x="220" y="320" text-anchor="middle" fill="#c084fc" font-size="9">retrieval / chunking&lt;/text>
&lt;text x="220" y="333" text-anchor="middle" fill="#c084fc" font-size="9">prompt / fine-tuning&lt;/text>
&lt;!-- Flecha Acción → Corpus (cerrando el loop) -->
&lt;line x1="155" y1="315" x2="85" y2="315" stroke="#a855f7" stroke-width="1.5"/>
&lt;line x1="85" y1="315" x2="85" y2="222" stroke="#a855f7" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
&lt;!-- Golden Dataset (entrada lateral) -->
&lt;rect x="600" y="350" width="155" height="40" rx="6" fill="#1a2633" stroke="#64748b" stroke-width="1" stroke-dasharray="5,3"/>
&lt;text x="677" y="366" text-anchor="middle" fill="#94a3b8" font-size="10" font-weight="600">Golden Dataset&lt;/text>
&lt;text x="677" y="380" text-anchor="middle" fill="#64748b" font-size="9">ground-truth para recall&lt;/text>
&lt;!-- Flecha Golden Dataset → Métricas -->
&lt;line x1="677" y1="350" x2="685" y2="347" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-gray)"/>
&lt;!-- Langfuse label -->
&lt;rect x="460" y="130" width="90" height="28" rx="5" fill="#1a2633" stroke="#64748b" stroke-width="1" stroke-dasharray="4,2"/>
&lt;text x="505" y="148" text-anchor="middle" fill="#94a3b8" font-size="9">Langfuse tracing&lt;/text>
&lt;!-- Línea Langfuse arriba -->
&lt;line x1="505" y1="158" x2="505" y2="170" stroke="#64748b" stroke-width="1" stroke-dasharray="3,2"/>
&lt;!-- Definición de marcadores -->
&lt;defs>
&lt;marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#4a5568"/>
&lt;/marker>
&lt;marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#3b82f6"/>
&lt;/marker>
&lt;marker id="arrow-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#22c55e"/>
&lt;/marker>
&lt;marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#f97316"/>
&lt;/marker>
&lt;marker id="arrow-purple" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#a855f7"/>
&lt;/marker>
&lt;marker id="arrow-gray" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#64748b"/>
&lt;/marker>
&lt;/defs>
&lt;/svg>
&lt;figcaption style="text-align:center;font-size:0.85em;color:#718096;margin-top:0.5em">El bucle de evaluación continua: corpus → retrieval → LLM → RAGAS judge → métricas → alerta → acción correctiva → corpus.&lt;/figcaption>
&lt;/figure>
&lt;hr>
&lt;h2 id="stack-oss-2026-para-ejecutar-ragas-on-premise">Stack OSS 2026 para ejecutar RAGAS on-premise&lt;/h2>
&lt;h3 id="ragas-apache-20">ragas (Apache 2.0)&lt;/h3>
&lt;p>La librería &lt;code>ragas&lt;/code> soporta evaluación asíncrona y múltiples backends de LLM. La integración con vLLM como judge elimina la necesidad de enviar datos a APIs externas — crítico en entornos con datos sensibles.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">ragas&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">evaluate&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">ragas.metrics&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">faithfulness&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">answer_relevancy&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_precision&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_recall&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain_openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ChatOpenAI&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">OpenAIEmbeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Judge LLM apuntando a vLLM on-premise&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">judge_llm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ChatOpenAI&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;meta-llama/Llama-3.1-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://vllm-service:8000/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-local&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># vLLM ignora el valor pero requiere el campo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">embeddings&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpenAIEmbeddings&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;BAAI/bge-m3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://embedding-service:8001/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-local&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">evaluate&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dataset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">golden_dataset&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># HuggingFace Dataset con columnas estándar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">metrics&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">faithfulness&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">answer_relevancy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_precision&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_recall&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">llm&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">judge_llm&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">embeddings&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">embeddings&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El dataset esperado por RAGAS tiene cuatro columnas: &lt;code>question&lt;/code>, &lt;code>answer&lt;/code>, &lt;code>contexts&lt;/code> (lista de strings), &lt;code>ground_truth&lt;/code>.&lt;/p>
&lt;h3 id="langfuse-para-trazabilidad-de-evals">Langfuse para trazabilidad de evals&lt;/h3>
&lt;p>Cada evaluación RAGAS se registra en Langfuse como un &lt;em>dataset experiment&lt;/em>, vinculando los scores a los spans de producción (ver https://blog.lo0.es/posts/tracing-llm-otel-genai/). Esto permite correlacionar una caída de faithfulness con el request específico que la provocó — sin esta vinculación, las métricas son números sin contexto accionable.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langfuse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">lf&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Langfuse&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Crear o recuperar el dataset en Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lf&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_or_create_dataset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rag-golden-v3&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Registrar scores del experiment&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">idx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_pandas&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iterrows&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lf&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ragas-faithfulness&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;faithfulness&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">trace_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;trace_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="c1"># vinculado al span de producción&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="prometheus--grafana-para-alertas-operativas">Prometheus + Grafana para alertas operativas&lt;/h3>
&lt;p>Las métricas RAGAS se exponen como gauges de Prometheus. Un dashboard de Grafana con umbrales configura alertas cuando faithfulness cae sostenidamente por debajo de 0,80:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># regla de alerta Prometheus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RAGFaithfulnessLow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">avg_over_time(rag_faithfulness_score[30m]) &amp;lt; 0.80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;RAG faithfulness por debajo de umbral ({{ $value | humanize }})&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Revisar context precision y reranker. Posible deriva del corpus.&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="corriendo-ragas-contra-vllm-on-premise--consideraciones-prácticas">Corriendo RAGAS contra vLLM on-premise — consideraciones prácticas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Tamaño del judge:&lt;/strong> Llama-3.1-70B como judge produce resultados comparables a GPT-4 en faithfulness y context evaluation, según los benchmarks de RAGAS 0.2 (2025). Modelos más pequeños (8B-13B) degradan la calidad del judge en preguntas multi-hop.&lt;/li>
&lt;li>&lt;strong>Throughput:&lt;/strong> En hardware on-premise con 4×H100 SXM (320 GB, NVLink), un run de 200 evaluaciones con Llama-3.1-70B tarda aproximadamente 8-12 minutos con batch_size=8 y vLLM en modo continuous batching.&lt;/li>
&lt;li>&lt;strong>Coste por evaluación:&lt;/strong> Sin API externa, el coste marginal es electricidad + amortización de GPU. Con 4×H100 a ~3 kW sostenidos, un run de 200 evaluaciones cuesta &amp;lt; 0,10 € en energía a tarifa industrial típica.&lt;/li>
&lt;li>&lt;strong>Frecuencia recomendada:&lt;/strong> eval offline semanal sobre el golden dataset completo + eval online muestreada (5-10 % de requests de producción) con un subconjunto de métricas que no requieren ground-truth (faithfulness, answer relevance).&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Alternativas a RAGAS:&lt;/strong> TruLens (evaluación con feedbacks modulares), DeepEval (aserciones programáticas, integración con pytest), ARES (framework de Stanford con trained classifiers en lugar de LLM-as-judge), y el framework de evals de OpenAI. Cada uno tiene trade-offs distintos en coste de judge, fiabilidad y facilidad de integración.&lt;/li>
&lt;li>&lt;strong>Continuous eval en producción:&lt;/strong> muestrear automáticamente requests reales, anonimizarlos, ejecutar un subconjunto de métricas sin ground-truth y usar el resultado para detectar deriva del sistema antes de que los usuarios lo reportan. Requiere un pipeline de datos separado del pipeline de inferencia.&lt;/li>
&lt;li>&lt;strong>Eval multilingüe:&lt;/strong> RAGAS con un judge en español o catalán sobre corpus no inglés tiene sesgos documentados cuando el judge es un modelo fundamentalmente entrenado en inglés. Los embeddings de similitud semántica para answer relevance son especialmente sensibles al idioma del corpus vs. idioma del judge.&lt;/li>
&lt;li>&lt;strong>A/B testing de configuraciones RAG:&lt;/strong> usar las métricas RAGAS como criterio de éxito en experimentos controlados — chunk size 512 vs. 1024, BM25 puro vs. hybrid, reranker cross-encoder vs. biencoder — con significancia estadística calculada sobre el golden dataset.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/posts/llm-as-judge-fundamentos/ — el patrón de juez LLM que RAGAS usa para medir faithfulness claim a claim&lt;/li>
&lt;li>https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/ — el marco general de evals donde RAGAS es la especialización RAG&lt;/li>
&lt;li>https://blog.lo0.es/posts/rag-reranker-hybrid-retrieval-fundamentos/ — la capa de retrieval cuya context precision y recall miden estas métricas&lt;/li>
&lt;li>https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/ — la calidad del corpus que context recall refleja&lt;/li>
&lt;li>https://blog.lo0.es/posts/tracing-llm-otel-genai/ — los spans de producción donde Langfuse anota los scores RAGAS&lt;/li>
&lt;li>https://blog.lo0.es/posts/data-versioning-dvc-lakefs/ — el golden dataset es un artefacto data que necesita versioning igual que el corpus&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ol>
&lt;li>Es Shahul, et al. &lt;em>RAGAS: Automated Evaluation of Retrieval Augmented Generation&lt;/em>. arXiv:2309.15217 (2023). &lt;a href="https://arxiv.org/abs/2309.15217">https://arxiv.org/abs/2309.15217&lt;/a>&lt;/li>
&lt;li>RAGAS Documentation v0.2. &lt;em>Metrics Reference&lt;/em>. &lt;a href="https://docs.ragas.io/en/stable/concepts/metrics/">https://docs.ragas.io/en/stable/concepts/metrics/&lt;/a> (consultado junio 2026)&lt;/li>
&lt;li>Langfuse. &lt;em>Dataset Experiments&lt;/em>. &lt;a href="https://langfuse.com/docs/datasets/overview">https://langfuse.com/docs/datasets/overview&lt;/a> (consultado junio 2026)&lt;/li>
&lt;li>Databricks. &lt;em>LLM Quality Evaluation: From Lab to Production&lt;/em>. Databricks Engineering Blog (2024).&lt;/li>
&lt;li>Saad-Falcon, J. et al. &lt;em>ARES: An Automated Evaluation Framework for Retrieval-Augmented Generation Systems&lt;/em>. arXiv:2311.09476 (2023).&lt;/li>
&lt;li>Goodhart, C.A.E. &lt;em>Problems of Monetary Management: The U.K. Experience&lt;/em>. Papers in Monetary Economics. Reserve Bank of Australia (1975). Formulación moderna de la ley que lleva su nombre.&lt;/li>
&lt;li>vLLM Project. &lt;em>OpenAI-Compatible Server&lt;/em>. &lt;a href="https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html">https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html&lt;/a> (consultado junio 2026)&lt;/li>
&lt;/ol></description></item></channel></rss>