<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Plan-and-Execute on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/plan-and-execute/</link><description>Recent content in Plan-and-Execute on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 17 Sep 2026 02:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/plan-and-execute/index.xml" rel="self" type="application/rss+xml"/><item><title>Agente de cadena de suministro con LangGraph: incidencias con proveedores y reposición con plan-and-execute y map-reduce sobre miles de referencias</title><link>https://blog.lo0.es/posts/agente-cadena-suministro-langgraph-send-map-reduce/</link><pubDate>Thu, 17 Sep 2026 02:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/agente-cadena-suministro-langgraph-send-map-reduce/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El modelo no ve treinta mil referencias. Ve las que un filtro en SQL declara en riesgo: cobertura por debajo del plazo del proveedor o incidencia abierta. Con un catálogo de 30.000 y una tasa de riesgo normal, son entre quinientas y dos mil. El primer ahorro del agente no es del modelo; es del &lt;code>WHERE&lt;/code>.&lt;/p>
&lt;p>El abanico se hace con &lt;code>Send&lt;/code> desde una arista condicional, un nodo trabajador por lote de referencias, un reductor &lt;code>operator.add&lt;/code> en el estado, y un nodo de consolidación con &lt;code>defer=True&lt;/code>. Todo el abanico corre en un superstep. Verificado en un grafo de prueba con solo el abanico y la consolidación: 2.000 trabajadores dejan 4 checkpoints, no 2.000.&lt;/p>
&lt;p>Lo que sí crece con el abanico son las escrituras y el tamaño del checkpoint. 2.000 envíos de una referencia cada uno son 6.003 filas en &lt;code>checkpoint_writes&lt;/code> y un checkpoint de 287 KB con la lista de resultados entera. En lotes de 50, 123 filas. El tamaño del checkpoint no baja con los lotes, porque el estado consolidado es el mismo; baja acotando lo que cada trabajador devuelve.&lt;/p>
&lt;p>En modo síncrono el abanico corre en un pool de hilos con el valor por defecto de Python, cinco en un pod de una CPU. En modo asíncrono corre sin límite: 2.000 tareas en vuelo a la vez. &lt;code>max_concurrency&lt;/code> en la configuración lo acota en los dos modos, y hay que fijarlo al valor que el gateway y vLLM puedan absorber.&lt;/p>
&lt;p>Un trabajador que falla tumba el &lt;code>invoke&lt;/code>, pero no pierde a los demás: sus escrituras quedan como pendientes y la reanudación ejecuta solo el que falló. Con &lt;code>retry_policy&lt;/code> en el nodo, el fallo transitorio contra el gateway ni siquiera sale del grafo.&lt;/p>
&lt;p>La caché por nodo con &lt;code>cache_policy&lt;/code> hace que la segunda pasada del día sobre las mismas referencias no llame al modelo: 0 llamadas reales en 200 referencias ya analizadas, con la clave por defecto sobre la entrada del nodo. Es lo que permite relanzar el análisis a media mañana sin pagar la mañana entera otra vez.&lt;/p>
&lt;h2 id="estás-aquí-del-cliente-al-almacén">Estás aquí: del cliente al almacén&lt;/h2>
&lt;p>Este es el caso 3 de la &lt;a href="https://blog.lo0.es/posts/agentes-langgraph-retail-marco-serie/">serie de agentes con LangGraph para retail&lt;/a>. Los dos anteriores, el &lt;a href="https://blog.lo0.es/posts/agente-empleado-tienda-langgraph-rag-tools/">asistente de sala&lt;/a> y el &lt;a href="https://blog.lo0.es/posts/agente-cliente-devoluciones-langgraph-interrupt-hitl/">agente de cliente&lt;/a>, son conversaciones: una persona pregunta, el agente contesta, y el volumen lo marca la persona. Este no tiene persona delante. Lo lanza una tarea a las seis de la mañana, recorre el catálogo, y deja una propuesta de reposición para que compras la revise a las nueve.&lt;/p>
&lt;p>Eso cambia el problema. Ya no importa la latencia de una respuesta, importa el coste total de la pasada, cuánto tarda, cuánto carga el servidor de inferencia, y qué pasa cuando se cae a la mitad. Y como el modelo se llama cientos o miles de veces con el mismo prompt y datos distintos, cada token del prompt cuenta cientos o miles de veces.&lt;/p>
&lt;p>Es el primer caso de la serie que se escribe sobre &lt;code>StateGraph&lt;/code> y no sobre &lt;code>create_agent&lt;/code>. El agente del caso 1 vuelve a aparecer, como subgrafo, en el sitio donde hace falta razonar sobre una referencia con sus incidencias y su historial; el resto del grafo es determinista y no lleva modelo.&lt;/p>
&lt;h2 id="la-analogía-la-ronda-del-jefe-de-compras">La analogía: la ronda del jefe de compras&lt;/h2>
&lt;p>Un jefe de compras con treinta mil referencias no las repasa una a una cada mañana. Mira un listado que el sistema le saca ordenado por riesgo, decide por dónde empieza (los proveedores con incidencias, las familias que rotan más), reparte el listado entre dos compradores, y al final junta lo que le traen en un pedido por proveedor. Si a media mañana llega un aviso de un proveedor, vuelve a mirar solo las referencias de ese proveedor.&lt;/p>
&lt;p>El filtro es el listado del sistema. El planificador es el &amp;ldquo;por dónde empiezo&amp;rdquo;. El abanico son los compradores trabajando a la vez. La consolidación es el pedido por proveedor. Y la caché es no volver a mirar lo que ya se miró esta mañana.&lt;/p>
&lt;h2 id="el-filtro-determinista">El filtro determinista&lt;/h2>
&lt;p>La primera decisión de diseño es qué referencias llegan al modelo. La respuesta es un &lt;code>SELECT&lt;/code>, no un prompt:&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">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cobertura&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &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="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sku&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="k">sum&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">unidades&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">reservadas&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">unidades&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="k">sum&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">unidades&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">reservadas&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">nullif&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">media_diaria&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&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">dias_cobertura&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">stock&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&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">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ventas_media_30d&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sku&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="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">s&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sku&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">v&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">media_diaria&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="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="n">riesgo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &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="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sku&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plazo_dias&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">unidades&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dias_cobertura&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="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&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">incidencia_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tipo&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">incidencia_tipo&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">abierta_ts&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">productos&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">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="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">proveedores&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pr&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">proveedor_id&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="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cobertura&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">c&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sku&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="k">LEFT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">incidencias&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sku&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">sku&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">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cerrada_ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&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="k">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">dias_cobertura&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">pr&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">plazo_dias&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">5&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">OR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&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="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="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">riesgo&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dias_cobertura&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NULLS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FIRST&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con esto, la referencia entra en el análisis si su cobertura no llega a una vez y media el plazo del proveedor, o si tiene una incidencia abierta. El multiplicador 1,5 es un parámetro de compras, no del agente, y vive en una tabla de configuración. Sobre un catálogo generado de 30.000 referencias con una distribución de ventas realista, el filtro devuelve entre 500 y 2.000 filas según el día. Ese es el volumen que ve el modelo.&lt;/p>
&lt;p>Todo lo demás, las referencias con cobertura holgada y sin incidencias, no genera tokens. Un agente que le pregunte al modelo &amp;ldquo;¿hay que reponer esto?&amp;rdquo; para cada una de las 30.000 está pagando 28.000 respuestas que un &lt;code>WHERE&lt;/code> da gratis.&lt;/p>
&lt;h2 id="el-estado">El estado&lt;/h2>
&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">import&lt;/span> &lt;span class="nn">operator&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">typing&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Annotated&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">TypedDict&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="k">class&lt;/span> &lt;span class="nc">Referencia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TypedDict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sku&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plazo_dias&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">unidades&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dias_cobertura&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">float&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">incidencia&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&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="k">class&lt;/span> &lt;span class="nc">Analisis&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TypedDict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sku&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">riesgo&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># &amp;#34;alto&amp;#34; | &amp;#34;medio&amp;#34; | &amp;#34;bajo&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">accion&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># &amp;#34;pedir&amp;#34; | &amp;#34;esperar&amp;#34; | &amp;#34;sustituir_proveedor&amp;#34; | &amp;#34;reclamar&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">unidades&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">motivo&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># una frase, acotada por el prompt&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="k">class&lt;/span> &lt;span class="nc">EstadoCompras&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TypedDict&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fecha&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">candidatas&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Referencia&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plan&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">analisis&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Annotated&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Analisis&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">operator&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">propuesta&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pasada&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Dos decisiones aquí. &lt;code>analisis&lt;/code> es la única clave con reductor, y es la única en la que escriben los trabajadores. &lt;code>motivo&lt;/code> está limitado a una frase en el prompt y a 200 caracteres en la validación, porque ese campo multiplicado por dos mil es lo que hace crecer el checkpoint. Un análisis de 180 bytes por referencia son 360 KB por checkpoint con dos mil; uno de 1 KB con explicaciones largas, 2 MB, y ese checkpoint se escribe en cada superstep posterior al abanico.&lt;/p>
&lt;h2 id="el-planificador">El planificador&lt;/h2>
&lt;p>El planificador es la única llamada al modelo que ve el conjunto entero, y no ve las referencias: ve un resumen por proveedor. Con salida estructurada:&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">pydantic&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">BaseModel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Field&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="k">class&lt;/span> &lt;span class="nc">PasoPlan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BaseModel&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">prioridad&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Field&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ge&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">le&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enfoque&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Field&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;qué mirar primero en este proveedor, una frase&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="k">class&lt;/span> &lt;span class="nc">Plan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BaseModel&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pasos&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">PasoPlan&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">proveedores_a_reclamar&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&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="n">planificador&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">modelo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">with_structured_output&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Plan&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;json_schema&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="k">def&lt;/span> &lt;span class="nf">planificar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EstadoCompras&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resumen&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resumen_por_proveedor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;candidatas&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="c1"># SQL: n_refs, n_incidencias, plazo, sla&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plan&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">planificador&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&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="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;system&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">PROMPT_PLANIFICADOR&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="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">resumen&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&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="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;plan&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">plan&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">model_dump&lt;/span>&lt;span class="p">()}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>method=&amp;quot;json_schema&amp;quot;&lt;/code> envía &lt;code>response_format&lt;/code> con el esquema JSON al gateway, y vLLM lo aplica con decodificación guiada. Con un modelo de clase 30B es fiable para esquemas de este tamaño; con esquemas anidados profundos se ha visto que la decodificación guiada alarga la generación, y este esquema se mantiene plano a propósito.&lt;/p>
&lt;p>El plan no decide qué referencias se analizan; eso lo decidió el filtro. Decide en qué orden van los proveedores, qué enfoque lleva cada trabajador en el prompt, y qué proveedores tienen tantas incidencias que lo que toca es reclamar y no pedir. Es una llamada al modelo por pasada, con un prompt de un par de miles de tokens. Su coste es despreciable frente al abanico; su valor es que el prompt de cada trabajador lleva una línea de contexto del proveedor que el trabajador no tendría.&lt;/p>
&lt;h2 id="el-abanico">El abanico&lt;/h2>
&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">langgraph.types&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Send&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">LOTE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">50&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="k">def&lt;/span> &lt;span class="nf">repartir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EstadoCompras&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Send&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">plan&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">]:&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;plan&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;pasos&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">por_proveedor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Referencia&lt;/span>&lt;span class="p">]]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&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">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;candidatas&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">por_proveedor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setdefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">[])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">envios&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&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">proveedor_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">refs&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">por_proveedor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enfoque&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">plan&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;enfoque&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="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">refs&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">LOTE&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">envios&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Send&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;analizar_lote&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="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fecha&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="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">proveedor_id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;enfoque&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">enfoque&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;referencias&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">refs&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="n">i&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">LOTE&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="k">return&lt;/span> &lt;span class="n">envios&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>repartir&lt;/code> es la función de una arista condicional. Devuelve una lista de &lt;code>Send&lt;/code>, y cada uno lanza una instancia de &lt;code>analizar_lote&lt;/code> con su propio estado de entrada. Todas corren en el mismo superstep, y el grafo no avanza hasta que todas terminan.&lt;/p>
&lt;p>El lote de 50 no es por el modelo. Cada referencia se analiza en su propia llamada al modelo dentro del trabajador; el lote existe por las escrituras del checkpoint. Medido con 2.000 referencias y un resultado de unos 180 bytes por referencia:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Reparto&lt;/th>
&lt;th>Checkpoints&lt;/th>
&lt;th>Filas en &lt;code>checkpoint_writes&lt;/code>&lt;/th>
&lt;th>Bytes de escrituras&lt;/th>
&lt;th>Tamaño del checkpoint mayor&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Un &lt;code>Send&lt;/code> por referencia&lt;/td>
&lt;td>4&lt;/td>
&lt;td>6.003&lt;/td>
&lt;td>946 KB&lt;/td>
&lt;td>287 KB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Un &lt;code>Send&lt;/code> por lote de 50&lt;/td>
&lt;td>4&lt;/td>
&lt;td>123&lt;/td>
&lt;td>319 KB&lt;/td>
&lt;td>287 KB&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Medido con &lt;code>InMemorySaver&lt;/code> y el serializador &lt;code>JsonPlusSerializer&lt;/code>, que es el mismo que usa el checkpointer de Postgres. Las filas de escrituras son las tareas pendientes del superstep: tres por tarea de una referencia. En Postgres son filas de &lt;code>checkpoint_writes&lt;/code>, con su índice, y con treinta mil referencias serían noventa mil filas por pasada. El tamaño del checkpoint no cambia con el lote, porque es la lista de análisis consolidada, y se repite en cada checkpoint posterior al abanico.&lt;/p>
&lt;h3 id="el-trabajador">El trabajador&lt;/h3>
&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">langgraph.types&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">CachePolicy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">RetryPolicy&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="k">def&lt;/span> &lt;span class="nf">analizar_lote&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lote&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resultados&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&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">ref&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;referencias&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">historial&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">historial_incidencias&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ref&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">dias&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">90&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># SQL, sin modelo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">salida&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">analista&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;messages&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">prompt_referencia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ref&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">historial&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;enfoque&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="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ContextoCompras&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fecha&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">proveedor_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;proveedor_id&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">resultados&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">salida&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;structured_response&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">model_dump&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;analisis&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">resultados&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="n">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_node&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;analizar_lote&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">analizar_lote&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">cache_policy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CachePolicy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ttl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">12&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">retry_policy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">RetryPolicy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">max_attempts&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">initial_interval&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">1.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">backoff_factor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">2.0&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>&lt;code>analista&lt;/code> es un &lt;code>create_agent&lt;/code> con &lt;code>response_format=ProviderStrategy(Analisis)&lt;/code>, una herramienta de consulta de incidencias por proveedor, el mismo &lt;code>ToolErrorMiddleware&lt;/code> y límites del caso 1, y &lt;code>checkpointer=False&lt;/code>. Lo último importa: un agente invocado dentro de un nodo hereda el checkpointer del grafo padre y escribe sus propios checkpoints bajo un espacio de nombres, tres por invocación; con dos mil referencias son seis mil filas que no sirven para reanudar nada. Se mide en el caso 4. Devuelve &lt;code>structured_response&lt;/code> validado. El prompt por referencia lleva la referencia, su cobertura, su incidencia si la hay, el historial de 90 días en cinco líneas, y el enfoque del plan. Son unos 600 tokens de entrada y unos 120 de salida por referencia.&lt;/p>
&lt;p>Dos mil referencias son 1,2 millones de tokens de entrada y 240.000 de salida por pasada. Con el prefijo del prompt del sistema compartido, y el gateway configurado para que las peticiones del mismo grupo de modelos caigan en la misma réplica, la mayor parte de esos 600 tokens de entrada acierta en la caché de prefijo de vLLM; la parte variable son las líneas de la referencia y el historial. Esto se mide con &lt;code>vllm:prefix_cache_hits&lt;/code> frente a &lt;code>vllm:prefix_cache_queries&lt;/code> durante la pasada, y es la primera cifra que hay que mirar si el coste no cuadra.&lt;/p>
&lt;h3 id="la-concurrencia">La concurrencia&lt;/h3>
&lt;p>En modo síncrono, LangGraph ejecuta las tareas del superstep en un pool de hilos con el valor por defecto de Python: el número de CPU más cuatro, con un tope de 32. En un pod con una CPU son cinco tareas en vuelo, medido. &lt;code>max_concurrency&lt;/code> en la configuración lo cambia: con 32, se ven 32 en vuelo.&lt;/p>
&lt;p>En modo asíncrono, con &lt;code>ainvoke&lt;/code>, no hay pool y no hay límite: con 2.000 envíos se han medido 2.000 tareas en vuelo a la vez. Contra un gateway con un límite de peticiones por clave, eso es una tormenta de 429 en el primer segundo. &lt;code>max_concurrency&lt;/code> también aplica en este modo, y es obligatorio:&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="n">config&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;configurable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;thread_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;compras:&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">fecha&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&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="s2">&amp;#34;max_concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">24&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;caso&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;compras&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">fecha&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;callbacks&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">langfuse_handler&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="n">resultado&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">ainvoke&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">estado_inicial&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">durability&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sync&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El 24 sale del gateway: la clave virtual del agente de compras tiene &lt;code>max_parallel_requests&lt;/code> a 24, y por debajo vLLM con &lt;code>--max-num-seqs&lt;/code> a 64 reparte entre este agente y el tráfico de sala. Un &lt;code>max_concurrency&lt;/code> por encima del límite de la clave solo produce 429 y reintentos; por debajo, deja capacidad sin usar. Con 24 en vuelo y unos dos segundos por referencia en un modelo de clase 30B, dos mil referencias son unos tres minutos de abanico. Las cifras de tiempo por referencia son las que hay que medir en cada despliegue; la aritmética es la que vale.&lt;/p>
&lt;h3 id="el-fallo-a-medias">El fallo a medias&lt;/h3>
&lt;p>Con &lt;code>retry_policy&lt;/code> en el nodo, un error de conexión con el gateway se reintenta dentro del grafo, con retroceso exponencial, y el trabajador termina sin que el &lt;code>invoke&lt;/code> se entere. Verificado con un trabajador que falla la primera vez: el abanico de 20 acaba con 20 resultados.&lt;/p>
&lt;p>Sin &lt;code>retry_policy&lt;/code>, la excepción sale de &lt;code>invoke&lt;/code>. Lo que no se pierde es el trabajo de los demás: verificado con un abanico de 20 en el que uno falla, el estado tras el fallo tiene 19 resultados guardados como escrituras pendientes, &lt;code>next&lt;/code> apunta al nodo trabajador, y &lt;code>invoke(None, config)&lt;/code> sobre el mismo hilo ejecuta solo la tarea que faltaba. Diecinueve llamadas al modelo que no se repiten.&lt;/p>
&lt;p>Lo que sí se repite es un lote en vuelo cuando muere el pod: sus 50 referencias vuelven a analizarse. Aquí no hay dinero de por medio y el análisis es idempotente por naturaleza, así que la única consecuencia es el coste, y la caché lo cubre: si el lote ya había terminado alguna referencia, no, porque la caché es por nodo y el nodo no terminó. Es un argumento más para lotes pequeños.&lt;/p>
&lt;h2 id="la-consolidación">La consolidación&lt;/h2>
&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="k">def&lt;/span> &lt;span class="nf">consolidar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EstadoCompras&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">por_proveedor&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">list&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Analisis&lt;/span>&lt;span class="p">]]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&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">a&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;analisis&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">por_proveedor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setdefault&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="p">[])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">propuesta&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fecha&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="s2">&amp;#34;pedidos&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="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">pid&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;lineas&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;unidades&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;unidades&amp;#34;&lt;/span>&lt;span class="p">]}&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;accion&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;pedir&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="s2">&amp;#34;reclamaciones&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;accion&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;reclamar&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="s2">&amp;#34;n_alto&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">sum&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;riesgo&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;alto&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="k">for&lt;/span> &lt;span class="n">pid&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">items&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">sorted&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">por_proveedor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&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="s2">&amp;#34;sin_analizar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">faltan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;candidatas&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;analisis&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="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;propuesta&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">propuesta&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;pasada&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;pasada&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&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="n">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;consolidar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">consolidar&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">defer&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>defer=True&lt;/code> retrasa el nodo hasta que no quede ningún camino pendiente hacia él. Sin ese parámetro, &lt;code>consolidar&lt;/code> se ejecutaría en cuanto terminara el primer lote, con una lista de análisis parcial. Con él, se ejecuta una vez, después de todo el abanico, y ve la lista completa. Verificado con &lt;code>stream_mode=&amp;quot;updates&amp;quot;&lt;/code>: cinco lotes, cinco actualizaciones de &lt;code>analizar_lote&lt;/code> y una de &lt;code>consolidar&lt;/code> al final.&lt;/p>
&lt;p>El orden de &lt;code>analisis&lt;/code> es determinista. Los reductores se aplican al final del superstep en el orden de las tareas, que es el orden de los &lt;code>Send&lt;/code>; verificado que la lista consolidada sigue el orden del reparto aunque los trabajadores terminen en otro. No hay que ordenar después, y un análisis que dependa del orden de llegada no tiene forma de depender de él.&lt;/p>
&lt;p>&lt;code>sin_analizar&lt;/code> es la lista de candidatas que no tienen análisis. En una pasada limpia es vacía; si un lote fue cortado por los límites del agente analista o devolvió un error, sus referencias aparecen ahí, y el nodo siguiente decide si se relanza.&lt;/p>
&lt;h2 id="replanificar">Replanificar&lt;/h2>
&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="k">def&lt;/span> &lt;span class="nf">decidir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EstadoCompras&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;propuesta&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;sin_analizar&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;pasada&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;reintentar&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;entregar&amp;#34;&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="k">def&lt;/span> &lt;span class="nf">reintentar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">EstadoCompras&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pendientes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;propuesta&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;sin_analizar&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="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;candidatas&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">r&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;candidatas&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">pendientes&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="n">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_edge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">START&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;cargar&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_edge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;cargar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;planificar&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_conditional_edges&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;planificar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">repartir&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;analizar_lote&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_edge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;analizar_lote&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;consolidar&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_conditional_edges&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;consolidar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">decidir&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;reintentar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;reintentar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;entregar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;entregar&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_conditional_edges&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reintentar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">repartir&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;analizar_lote&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">grafo&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add_edge&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;entregar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">END&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Es el plan-and-execute en su forma mínima: planificar, ejecutar en abanico, consolidar, y volver a ejecutar solo lo que falta, con un contador. El modelo no decide si se repite; lo decide &lt;code>decidir&lt;/code> leyendo &lt;code>sin_analizar&lt;/code> y &lt;code>pasada&lt;/code>. Tres pasadas como máximo. Lo que quede sin analizar después de tres va en la propuesta marcado como tal, y compras lo mira a mano.&lt;/p>
&lt;p>Una segunda pasada no vuelve a llamar al planificador. El plan de la primera sigue en el estado y &lt;code>repartir&lt;/code> lo lee. Volver a planificar tendría sentido si el conjunto de candidatas cambiara, y en una pasada no cambia.&lt;/p>
&lt;h2 id="la-caché-por-nodo">La caché por nodo&lt;/h2>
&lt;p>&lt;code>cache_policy=CachePolicy(ttl=12 * 3600)&lt;/code> en &lt;code>analizar_lote&lt;/code> hace que el grafo, antes de ejecutar el nodo, calcule una clave a partir de la entrada del nodo y busque en la caché. Con la clave por defecto, la entrada del lote entera es la clave: fecha, proveedor, enfoque y las 50 referencias con sus cifras.&lt;/p>
&lt;p>Verificado: una pasada sobre 200 referencias hace 200 llamadas; una segunda pasada sobre las mismas 200, en otro hilo, hace 0 y termina en dos centésimas de segundo. En las actualizaciones del flujo, cada lote cacheado llega con &lt;code>__metadata__&lt;/code> y &lt;code>cached: True&lt;/code>.&lt;/p>
&lt;p>Para que la caché sirva en una tienda hay que decidir la clave. Con la clave por defecto, cualquier cambio en las unidades de una referencia invalida el lote entero, que es lo correcto si el stock cambió, y demasiado si cambió una unidad. La versión de producción usa una &lt;code>key_func&lt;/code> propia que redondea la cobertura a días enteros y quita del lote los campos que no afectan al análisis:&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="k">def&lt;/span> &lt;span class="nf">clave_lote&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">lote&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&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&lt;/span> &lt;span class="o">=&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="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sku&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;proveedor_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;dias_cobertura&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="mi">0&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="n">r&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;incidencia&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;id&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="k">for&lt;/span> &lt;span class="n">r&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;referencias&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="k">return&lt;/span> &lt;span class="n">hashlib&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;fecha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">lote&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;enfoque&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">base&lt;/span>&lt;span class="p">])&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">encode&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">hexdigest&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="n">cache_policy&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">CachePolicy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key_func&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">clave_lote&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ttl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">12&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La caché no es el checkpointer. Es un &lt;code>BaseCache&lt;/code> que se pasa en &lt;code>compile(cache=...)&lt;/code>, y en un cluster con varias réplicas del agente tiene que ser compartida. El paquete trae &lt;code>InMemoryCache&lt;/code> y &lt;code>RedisCache&lt;/code> (&lt;code>langgraph.cache.redis&lt;/code>, construido sobre un cliente de &lt;code>redis&lt;/code>); la interfaz son seis métodos (&lt;code>get&lt;/code>, &lt;code>set&lt;/code>, &lt;code>clear&lt;/code> y sus versiones asíncronas) si se quiere una sobre una tabla de Postgres. Con &lt;code>InMemoryCache&lt;/code>, cada pod tiene la suya y la segunda pasada solo ahorra si cae en el mismo pod.&lt;/p>
&lt;h2 id="lo-que-se-mide">Lo que se mide&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Dónde&lt;/th>
&lt;th>Qué dice&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Candidatas por pasada frente a catálogo&lt;/td>
&lt;td>&lt;code>SELECT&lt;/code> del filtro&lt;/td>
&lt;td>Cuánto trabajo ve el modelo. Si sube del 10 por ciento, el multiplicador del filtro es alto o hay un problema de stock real&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokens por referencia analizada&lt;/td>
&lt;td>Langfuse, generaciones bajo &lt;code>caso=compras&lt;/code> divididas por candidatas&lt;/td>
&lt;td>Unos 700 con el prompt actual. Si sube, el historial se ha alargado o el motivo se ha desbordado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Aciertos de caché de prefijo durante la pasada&lt;/td>
&lt;td>&lt;code>vllm:prefix_cache_hits&lt;/code> / &lt;code>vllm:prefix_cache_queries&lt;/code>&lt;/td>
&lt;td>Por encima del 70 por ciento con el prompt del sistema compartido y afinidad en el gateway. Por debajo, las peticiones se reparten entre réplicas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Aciertos de caché de nodo&lt;/td>
&lt;td>&lt;code>__metadata__.cached&lt;/code> en el flujo, contados&lt;/td>
&lt;td>Solo cuentan en pasadas repetidas. Cero en la primera del día es lo normal&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Duración del abanico frente a candidatas&lt;/td>
&lt;td>Traza del hilo, span de &lt;code>analizar_lote&lt;/code>&lt;/td>
&lt;td>Lineal con candidatas y &lt;code>max_concurrency&lt;/code>. Si no es lineal, el gateway está encolando&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Filas en &lt;code>checkpoint_writes&lt;/code> por pasada&lt;/td>
&lt;td>Postgres&lt;/td>
&lt;td>Candidatas dividido por lote, por tres. Si crece más, el lote se ha reducido o hay reintentos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sin_analizar&lt;/code> tras la tercera pasada&lt;/td>
&lt;td>&lt;code>propuesta&lt;/code>&lt;/td>
&lt;td>Debe ser cero casi siempre. Cada referencia ahí es una llamada al modelo que no terminó&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Líneas de pedido aceptadas por compras frente a propuestas&lt;/td>
&lt;td>Tabla de pedidos de compra&lt;/td>
&lt;td>La métrica de negocio. Un 70 por ciento de aceptación sin edición es una propuesta útil; un 30, un agente que hay que afinar&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La propuesta consolidada no se convierte en pedido sola. &lt;code>entregar&lt;/code> la escribe en &lt;code>propuestas_reposicion&lt;/code> y abre la aprobación de compras con el mismo patrón del caso 2: un hilo por propuesta, una interrupción con las líneas por proveedor, y un comprador que aprueba, edita cantidades o rechaza. Lo que cambia respecto al caso 2 es el tamaño del vale: son cientos de líneas, y la interfaz de aprobación es una tabla editable, no un botón.&lt;/p>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;ul>
&lt;li>El filtro de candidatas es SQL y su multiplicador vive en configuración.&lt;/li>
&lt;li>&lt;code>Analisis&lt;/code> acota &lt;code>motivo&lt;/code> a una frase y a 200 caracteres.&lt;/li>
&lt;li>El planificador ve un resumen por proveedor, no las referencias, y usa &lt;code>with_structured_output(..., method=&amp;quot;json_schema&amp;quot;)&lt;/code>.&lt;/li>
&lt;li>&lt;code>repartir&lt;/code> agrupa por proveedor y envía lotes de 50.&lt;/li>
&lt;li>&lt;code>analizar_lote&lt;/code> tiene &lt;code>cache_policy&lt;/code> con &lt;code>key_func&lt;/code> propia y &lt;code>retry_policy&lt;/code>.&lt;/li>
&lt;li>&lt;code>consolidar&lt;/code> lleva &lt;code>defer=True&lt;/code>.&lt;/li>
&lt;li>&lt;code>max_concurrency&lt;/code> está en la configuración y coincide con &lt;code>max_parallel_requests&lt;/code> de la clave del gateway.&lt;/li>
&lt;li>La caché se pasa en &lt;code>compile(cache=...)&lt;/code> y es compartida entre réplicas (&lt;code>RedisCache&lt;/code> o propia).&lt;/li>
&lt;li>Hay un contador de pasadas y un tope de tres.&lt;/li>
&lt;li>La propuesta pasa por aprobación de compras antes de ser pedido.&lt;/li>
&lt;li>Se ha medido &lt;code>vllm:prefix_cache_hits&lt;/code> durante una pasada completa.&lt;/li>
&lt;/ul>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>El abanico asíncrono no tiene límite.&lt;/strong> 2.000 en vuelo, medido. Sin &lt;code>max_concurrency&lt;/code> el primer efecto es una ráfaga de 429 del gateway y el segundo, los reintentos multiplicando la ráfaga.&lt;/p>
&lt;p>&lt;strong>El abanico síncrono tiene un límite que no es el suyo.&lt;/strong> Cinco en un pod de una CPU, por el pool de hilos de Python. Un agente que parece lento en producción y rápido en el portátil de ocho núcleos está pagando esta diferencia.&lt;/p>
&lt;p>&lt;strong>Los lotes reducen filas, no bytes de checkpoint.&lt;/strong> El checkpoint lleva la lista consolidada, y esa lista es la misma se envíe como se envíe. Lo que la reduce es acotar lo que cada análisis devuelve.&lt;/p>
&lt;p>&lt;strong>&lt;code>defer=True&lt;/code> no es opcional.&lt;/strong> Sin él, la consolidación corre con el primer lote que termina. El grafo no avisa; la propuesta sale con una fracción de las referencias.&lt;/p>
&lt;p>&lt;strong>La caché por defecto es por entrada exacta.&lt;/strong> Una unidad de stock de diferencia invalida el lote. &lt;code>key_func&lt;/code> propia, o la caché no acierta nunca fuera de las pruebas.&lt;/p>
&lt;p>&lt;strong>&lt;code>InMemoryCache&lt;/code> es por pod.&lt;/strong> Con tres réplicas, dos tercios de las repeticiones no aciertan. &lt;code>RedisCache&lt;/code> viene en el paquete; una sobre Postgres son seis métodos.&lt;/p>
&lt;p>&lt;strong>La re-ejecución tras un fallo es por tarea, no por superstep.&lt;/strong> Los que terminaron no se repiten. Pero el lote en vuelo cuando muere el pod sí, entero, y sus referencias ya analizadas dentro del lote también. Lotes pequeños.&lt;/p>
&lt;p>&lt;strong>El planificador no puede añadir referencias.&lt;/strong> Si el filtro no las sacó, el modelo no las ve. Un jefe de compras que eche en falta una referencia en la propuesta tiene que mirar el filtro, no el prompt.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>El agente de compras es determinista en todo menos en dos sitios: el plan por proveedor y el análisis por referencia. Todo lo demás, el filtro, el reparto, la consolidación, la decisión de repetir y el tope, es código que no llama al modelo. Es la proporción que hace que treinta mil referencias sean dos mil llamadas y no treinta mil, que una caída a media pasada cueste un lote y no la mañana, y que la segunda pasada del día cueste cero.&lt;/p>
&lt;p>Las primitivas que lo permiten son cuatro y son de &lt;code>StateGraph&lt;/code>: &lt;code>Send&lt;/code> desde una arista condicional, un reductor en el estado, &lt;code>defer=True&lt;/code> en la consolidación, y &lt;code>cache_policy&lt;/code> con clave propia. Y un parámetro de configuración, &lt;code>max_concurrency&lt;/code>, que no está en el grafo y sin el cual el grafo tumba el gateway.&lt;/p>
&lt;p>El siguiente caso vuelve a una escala pequeña, una ficha de producto cada vez, y cambia el problema: no es cuántas veces se llama al modelo, es cuántas veces se le deja corregirse antes de decir que no.&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/agentes-langgraph-retail-marco-serie/">Agentes con LangGraph para retail: el marco de la serie&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-fichas-producto-langgraph-generador-evaluador/">Agente de datos de producto con LangGraph: generador, evaluador y bucle acotado&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/servir-agentes-langgraph-sin-langgraph-api/">Servir agentes de LangGraph sin langgraph-api&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-empleado-tienda-langgraph-rag-tools/">Agente de empleado de tienda con LangGraph: procedimientos, stock y roturas&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/agente-cliente-devoluciones-langgraph-interrupt-hitl/">Agente de atención al cliente con LangGraph: el límite de aprobación humana&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/dimensionar-gateway-flota-agentes/">Dimensionar el gateway para una flota de agentes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/enrutado-prefijo-kv-cache-lo-que-litellm-no-hace/">Enrutado por prefijo y caché KV: lo que LiteLLM no hace&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/litellm-limites-presupuestos-claves-virtuales/">Límites, presupuestos y claves virtuales en LiteLLM&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>API de grafos de LangGraph (&lt;code>Send&lt;/code>, reductores, &lt;code>defer&lt;/code>, &lt;code>cache_policy&lt;/code>, &lt;code>retry_policy&lt;/code>): &lt;a href="https://docs.langchain.com/oss/python/langgraph/graph-api">https://docs.langchain.com/oss/python/langgraph/graph-api&lt;/a>&lt;/li>
&lt;li>Persistencia y durabilidad en LangGraph: &lt;a href="https://docs.langchain.com/oss/python/langgraph/persistence">https://docs.langchain.com/oss/python/langgraph/persistence&lt;/a>&lt;/li>
&lt;li>Salida estructurada en LangChain 1.x: &lt;a href="https://docs.langchain.com/oss/python/langchain/structured-output">https://docs.langchain.com/oss/python/langchain/structured-output&lt;/a>&lt;/li>
&lt;li>Código de &lt;code>langgraph.types&lt;/code> (langgraph 1.2.11): &lt;code>Send&lt;/code>, &lt;code>CachePolicy&lt;/code>, &lt;code>RetryPolicy&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.checkpoint.serde.jsonplus&lt;/code> (langgraph-checkpoint 4.2.0), usado para medir los tamaños&lt;/li>
&lt;li>Métricas de vLLM 0.29 (&lt;code>vllm:prefix_cache_queries&lt;/code>, &lt;code>vllm:prefix_cache_hits&lt;/code>): &lt;a href="https://docs.vllm.ai/en/latest/usage/metrics.html">https://docs.vllm.ai/en/latest/usage/metrics.html&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>