<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Llama on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/llama/</link><description>Recent content in Llama 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/llama/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></channel></rss>