<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Evaluador on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/evaluador/</link><description>Recent content in Evaluador on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 17 Sep 2026 03:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/evaluador/index.xml" rel="self" type="application/rss+xml"/><item><title>Agente de datos de producto con LangGraph: enriquecer y controlar fichas con un generador, un evaluador y un bucle que sabe parar</title><link>https://blog.lo0.es/posts/agente-fichas-producto-langgraph-generador-evaluador/</link><pubDate>Thu, 17 Sep 2026 03:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/agente-fichas-producto-langgraph-generador-evaluador/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El generador escribe la ficha y el evaluador la puntúa contra una lista de criterios que no cambia. Son dos agentes distintos, con dos prompts distintos y salida estructurada los dos. El evaluador no ve el prompt del generador ni sus intentos anteriores; ve la ficha y los criterios. Es la única forma de que el evaluador no apruebe lo que el generador quiso hacer en vez de lo que hizo.&lt;/p>
&lt;p>El bucle lo cierra una arista condicional que lee dos cosas del estado: el veredicto y un contador de intentos. Tres como máximo. El modelo no sabe cuántos intentos lleva ni decide cuándo parar. Verificado con un evaluador que rechaza dos veces y aprueba a la tercera: tres intentos, tres veredictos en el historial, y el grafo termina en &lt;code>publicar&lt;/code>.&lt;/p>
&lt;p>Con un modelo en vLLM, &lt;code>response_format&lt;/code> en &lt;code>create_agent&lt;/code> no hace lo que parece. Sin perfil en el modelo, LangChain no puede saber si el proveedor admite salida estructurada y recurre a una herramienta sintética con el nombre del esquema, que el modelo tiene que llamar. Con &lt;code>profile={&amp;quot;structured_output&amp;quot;: True}&lt;/code> usa &lt;code>response_format&lt;/code> con el esquema JSON y vLLM lo aplica con decodificación guiada. Verificado en &lt;code>_supports_provider_strategy&lt;/code> de langchain 1.4.1.&lt;/p>
&lt;p>Un agente de &lt;code>create_agent&lt;/code> invocado dentro de un nodo hereda el checkpointer del grafo padre y guarda sus propios checkpoints bajo un espacio de nombres. Una ficha con tres intentos deja 30 filas; con &lt;code>checkpointer=False&lt;/code> en los dos agentes, 12. El estado que importa está en el padre; los subgrafos no necesitan reanudarse.&lt;/p>
&lt;p>El resultado estructurado de un agente es un objeto pydantic, y ese objeto entra en el estado del subgrafo. Al deserializar el checkpoint, LangGraph 1.2.11 avisa de que el tipo no está registrado y de que lo bloqueará en una versión futura. Se guarda en el estado del padre como diccionario con &lt;code>model_dump()&lt;/code>, y se registra el módulo en &lt;code>allowed_msgpack_modules&lt;/code> o se desactiva el checkpointer del subgrafo.&lt;/p>
&lt;p>Lo que se mide es la tasa de aprobación por intento. Es la única métrica que dice si el bucle compensa: si a la tercera se aprueba lo mismo que a la segunda, el tercer intento sobra.&lt;/p>
&lt;h2 id="estás-aquí-el-cuarto-caso-el-bucle-que-corrige">Estás aquí: el cuarto caso, el bucle que corrige&lt;/h2>
&lt;p>Este es el caso 4 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 tres anteriores tratan con personas o con un catálogo entero; este trata con un texto. Una ficha de producto tiene un título, una descripción, unos atributos, y unas reglas de la casa sobre lo que puede y no puede decir. Las fichas llegan del proveedor a medias, en otro idioma, o con los atributos en el título. Hoy las completa alguien de catálogo, una a una, y las publica sin que nadie las vuelva a leer.&lt;/p>
&lt;p>El agente hace lo mismo con dos diferencias: escribe y comprueba en dos pasos separados, y el segundo paso tiene derecho de veto. Es el patrón que la documentación llama reflexión, y la forma más simple de un evaluador: no puntúa de uno a diez, dice sí o no y por qué.&lt;/p>
&lt;h2 id="la-analogía-el-redactor-y-el-corrector">La analogía: el redactor y el corrector&lt;/h2>
&lt;p>En cualquier redacción hay dos personas. Una escribe y otra corrige, y la que corrige no ha visto el borrador anterior ni sabe lo que el redactor intentaba decir; solo tiene el texto y el libro de estilo. Si el corrector fuera el propio redactor releyendo, aprobaría lo que quiso escribir.&lt;/p>
&lt;p>El generador es el redactor. El evaluador es el corrector con el libro de estilo. El contador de intentos es el jefe de sección que a la tercera vuelta publica o descarta, sin que el redactor lo decida.&lt;/p>
&lt;h2 id="los-criterios">Los criterios&lt;/h2>
&lt;p>El evaluador comprueba una lista cerrada. Cada criterio tiene un identificador, una descripción para el evaluador y una descripción para el generador. Se guardan en una tabla, no en el prompt, y se versionan.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Id&lt;/th>
&lt;th>Criterio&lt;/th>
&lt;th>Comprobable sin modelo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>C01&lt;/td>
&lt;td>El título tiene entre 30 y 80 caracteres y empieza por el tipo de producto&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C02&lt;/td>
&lt;td>La descripción no repite el título&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C03&lt;/td>
&lt;td>Todos los atributos obligatorios de la familia están presentes&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C04&lt;/td>
&lt;td>No hay afirmaciones de salud, seguridad o rendimiento no respaldadas por la ficha del proveedor&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C05&lt;/td>
&lt;td>La descripción no menciona precio, disponibilidad ni promociones&lt;/td>
&lt;td>Parcial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C06&lt;/td>
&lt;td>El tono es el de la casa: segunda persona, sin superlativos, sin exclamaciones&lt;/td>
&lt;td>Parcial&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C07&lt;/td>
&lt;td>Las cifras (capacidad, medidas, potencia) coinciden con las del proveedor&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los criterios de la tercera columna marcados con &amp;ldquo;Sí&amp;rdquo; se comprueban en código antes de llamar al evaluador, y el resultado se le pasa. Un modelo evaluando si un título tiene menos de 80 caracteres es un modelo haciendo mal lo que &lt;code>len()&lt;/code> hace bien. Lo que el evaluador aporta es C04, C06 y las partes no mecánicas de C05: lo que exige leer.&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">EstadoFicha&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">origen&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="c1"># ficha del proveedor, atributos de familia, cifras&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ficha&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="c1"># última versión generada&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">comprobaciones&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="c1"># C01, C02, C03, C07: resultado en código&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">veredicto&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="c1"># {&amp;#34;aprobada&amp;#34;: bool, &amp;#34;problemas&amp;#34;: [{&amp;#34;criterio&amp;#34;: ..., &amp;#34;detalle&amp;#34;: ...}]}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">intentos&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">int&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">historial&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="nb">dict&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>intentos&lt;/code> con &lt;code>operator.add&lt;/code> es el contador: cada vez que &lt;code>generar&lt;/code> devuelve &lt;code>{&amp;quot;intentos&amp;quot;: 1}&lt;/code>, el reductor suma. &lt;code>historial&lt;/code> acumula un registro por intento con la ficha y el veredicto, y es lo que después permite medir. &lt;code>ficha&lt;/code> y &lt;code>veredicto&lt;/code> se sobreescriben.&lt;/p>
&lt;p>&lt;code>ficha&lt;/code> y &lt;code>veredicto&lt;/code> son diccionarios, no objetos pydantic. Los agentes devuelven pydantic en &lt;code>structured_response&lt;/code>, y el nodo lo convierte con &lt;code>model_dump()&lt;/code> antes de escribirlo en el estado. Más abajo se explica por qué no es una manía.&lt;/p>
&lt;h2 id="los-dos-agentes">Los dos agentes&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">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 class="kn">from&lt;/span> &lt;span class="nn">langchain.agents&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">create_agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain.agents.structured_output&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ProviderStrategy&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">Ficha&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">titulo&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">max_length&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">80&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">descripcion&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">max_length&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1200&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">atributos&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">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="k">class&lt;/span> &lt;span class="nc">Problema&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">criterio&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">detalle&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">max_length&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">200&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">Veredicto&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">aprobada&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">bool&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">problemas&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">Problema&lt;/span>&lt;span class="p">]&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">default_factory&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">list&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">generador&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_agent&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modelo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tools&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="n">system_prompt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">PROMPT_GENERADOR&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># reglas de la casa, formato, y &amp;#34;corrige solo lo señalado&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response_format&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ProviderStrategy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Ficha&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checkpointer&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">evaluador&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">create_agent&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">modelo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tools&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="n">system_prompt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">PROMPT_EVALUADOR&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># criterios C04, C05, C06 y &amp;#34;responde solo con el veredicto&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response_format&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ProviderStrategy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Veredicto&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checkpointer&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;/code>&lt;/pre>&lt;/div>&lt;p>Ninguno tiene herramientas. Todo lo que necesitan va en el mensaje: la ficha del proveedor, los atributos obligatorios de la familia, y para el generador, los problemas del intento anterior. Son dos llamadas al modelo por intento, sin ciclo de herramientas; &lt;code>create_agent&lt;/code> está aquí por la salida estructurada, el prompt y el middleware, no por el bucle.&lt;/p>
&lt;p>Dos parámetros que no son decorativos. &lt;code>ProviderStrategy&lt;/code> se explica en su propia sección. &lt;code>checkpointer=False&lt;/code> también.&lt;/p>
&lt;h3 id="los-prompts-y-lo-que-cada-uno-no-ve">Los prompts, y lo que cada uno no ve&lt;/h3>
&lt;p>El generador recibe la ficha del proveedor y, a partir del segundo intento, la lista de problemas del veredicto anterior con la instrucción de corregir solo eso. No recibe su ficha anterior entera. Se probó dárselo y el resultado fue peor: con la ficha anterior delante, el modelo reescribe frases que estaban bien y a veces reintroduce un problema ya corregido en otro sitio. Con la ficha del proveedor y la lista de problemas, regenera desde el origen con las restricciones acumuladas.&lt;/p>
&lt;p>El evaluador recibe la ficha generada, los resultados de las comprobaciones en código, y los criterios. No recibe el prompt del generador, ni sus intentos anteriores, ni cuántos lleva. Si supiera que es el tercer intento, aprobaría más; se ha visto en pruebas con el número de intento en el mensaje. La única información de contexto es el origen, para C04 y C07.&lt;/p>
&lt;h2 id="el-grafo">El grafo&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.graph&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">StateGraph&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">START&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">END&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">comprobar&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">EstadoFicha&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="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;comprobaciones&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">comprobaciones_en_codigo&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;ficha&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;origen&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">generar&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">EstadoFicha&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">problemas&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">state&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;veredicto&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">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;problemas&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="n">salida&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">generador&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">({&lt;/span>&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">mensaje_generador&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;origen&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">problemas&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;ficha&amp;#34;&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 class="s2">&amp;#34;intentos&amp;#34;&lt;/span>&lt;span class="p">:&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="k">def&lt;/span> &lt;span class="nf">evaluar&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">EstadoFicha&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">salida&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">evaluador&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">invoke&lt;/span>&lt;span class="p">({&lt;/span>&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">mensaje_evaluador&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;ficha&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;comprobaciones&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;origen&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">veredicto&lt;/span> &lt;span class="o">=&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="n">fallos_codigo&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">c&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ok&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;comprobaciones&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">items&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">ok&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">fallos_codigo&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">veredicto&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;aprobada&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">veredicto&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;problemas&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;criterio&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">c&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;detalle&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;comprobación automática&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">c&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">fallos_codigo&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;veredicto&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">veredicto&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;historial&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;intento&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;intentos&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;ficha&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;ficha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;veredicto&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">veredicto&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">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">EstadoFicha&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;veredicto&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;aprobada&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="s2">&amp;#34;publicar&amp;#34;&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;intentos&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">&amp;gt;=&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;descartar&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;generar&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">publicar&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">EstadoFicha&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">guardar_ficha&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;sku&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;ficha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">estado&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;publicada&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">intentos&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;intentos&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>&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">descartar&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">EstadoFicha&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">guardar_ficha&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;sku&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;ficha&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">estado&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;revision_manual&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">intentos&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;intentos&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">problemas&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">state&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;veredicto&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;problemas&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>&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">StateGraph&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">EstadoFicha&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_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;generar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">generar&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_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;comprobar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">comprobar&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_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;evaluar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">evaluar&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_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;publicar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">publicar&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_node&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;descartar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">descartar&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="n">START&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;generar&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;generar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;comprobar&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;comprobar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;evaluar&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;evaluar&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;generar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;generar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;publicar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;publicar&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;descartar&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;descartar&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;publicar&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;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;descartar&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>Cinco nodos y una arista condicional con tres salidas. &lt;code>decidir&lt;/code> es el único sitio donde se lee el contador, y es una función sin modelo. El modelo no puede alargar el bucle ni acortarlo.&lt;/p>
&lt;p>Las comprobaciones en código van en su propio nodo entre generar y evaluar, y el evaluador las recibe. Además, &lt;code>evaluar&lt;/code> fuerza el rechazo si alguna falla: aunque el modelo apruebe, un título de 90 caracteres no se publica. El evaluador con modelo no tiene la última palabra sobre lo que se puede comprobar sin él.&lt;/p>
&lt;p>Verificado con un generador que devuelve una ficha por intento y un evaluador guionizado que rechaza dos veces y aprueba a la tercera: &lt;code>intentos&lt;/code> termina en 3, el historial tiene tres entradas con &lt;code>False, False, True&lt;/code>, y el grafo sale por &lt;code>publicar&lt;/code>. Con el evaluador rechazando siempre, sale por &lt;code>descartar&lt;/code> a la tercera.&lt;/p>
&lt;h2 id="la-salida-estructurada-por-debajo">La salida estructurada, por debajo&lt;/h2>
&lt;p>&lt;code>response_format&lt;/code> en &lt;code>create_agent&lt;/code> admite un esquema a secas, &lt;code>ToolStrategy(esquema)&lt;/code> o &lt;code>ProviderStrategy(esquema)&lt;/code>. Con el esquema a secas, LangChain elige por el modelo: en &lt;code>_supports_provider_strategy&lt;/code> mira &lt;code>model.profile&lt;/code>, y si el perfil dice &lt;code>structured_output: True&lt;/code> usa &lt;code>ProviderStrategy&lt;/code>; si no, busca el nombre del modelo en una lista de patrones de modelos conocidos, y si tampoco, &lt;code>ToolStrategy&lt;/code>.&lt;/p>
&lt;p>Con &lt;code>ChatOpenAI&lt;/code> apuntando a vLLM y un nombre como &lt;code>qwen3-30b-a3b&lt;/code>, el perfil es &lt;code>None&lt;/code> y el nombre no está en la lista. Resultado: &lt;code>ToolStrategy&lt;/code>. Verificado: &lt;code>_supports_provider_strategy(modelo, tools=[])&lt;/code> devuelve &lt;code>False&lt;/code> sin perfil y &lt;code>True&lt;/code> con &lt;code>profile={&amp;quot;structured_output&amp;quot;: True}&lt;/code>.&lt;/p>
&lt;p>Lo que cambia entre las dos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;code>ToolStrategy&lt;/code>&lt;/th>
&lt;th>&lt;code>ProviderStrategy&lt;/code>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Qué se envía&lt;/td>
&lt;td>Una herramienta sintética con el nombre del esquema (&lt;code>Ficha&lt;/code>) en &lt;code>tools&lt;/code>&lt;/td>
&lt;td>&lt;code>response_format&lt;/code> con el esquema JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qué hace el modelo&lt;/td>
&lt;td>Decide llamar a esa herramienta con los argumentos&lt;/td>
&lt;td>Genera el JSON con decodificación guiada&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qué puede salir mal&lt;/td>
&lt;td>El modelo responde en texto sin llamar a la herramienta, o llama con argumentos que no validan; LangChain reintenta con un mensaje de error (&lt;code>handle_errors=True&lt;/code> por defecto)&lt;/td>
&lt;td>El JSON es válido siempre; si no valida contra pydantic, no hay reintento: &lt;code>_handle_structured_output_error&lt;/code> solo reintenta con &lt;code>ToolStrategy&lt;/code>, con la otra lanza&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste&lt;/td>
&lt;td>Tokens de la definición de la herramienta en cada llamada, y a veces una vuelta más&lt;/td>
&lt;td>Nada extra en la entrada; la decodificación guiada añade algo de latencia en esquemas grandes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para este agente se fija &lt;code>ProviderStrategy&lt;/code> a mano en los dos, y el modelo lleva &lt;code>profile={&amp;quot;max_input_tokens&amp;quot;: 32768, &amp;quot;structured_output&amp;quot;: True}&lt;/code> en el marco de la serie. El esquema se mantiene plano: &lt;code>Ficha&lt;/code> tiene tres campos y &lt;code>atributos&lt;/code> es un diccionario de cadenas, no un modelo anidado por familia. vLLM aplica bien esquemas de este tamaño; con esquemas de varios niveles y enumeraciones largas la decodificación guiada se nota en la latencia de la primera ficha.&lt;/p>
&lt;p>&lt;code>Field(max_length=80)&lt;/code> en el título es una restricción del esquema que vLLM no aplica en la generación; la aplica pydantic al validar. Con &lt;code>ProviderStrategy&lt;/code> un fallo de validación no se reintenta: el agente lanza y el nodo &lt;code>generar&lt;/code> lanza con él. Por eso &lt;code>generar&lt;/code> captura esa excepción y manda la ficha a revisión manual sin pasar por el evaluador, y por eso C01 se comprueba también en código: lo que está dentro del esquema es una segunda barrera, no la primera.&lt;/p>
&lt;h2 id="los-subgrafos-y-sus-checkpoints">Los subgrafos y sus checkpoints&lt;/h2>
&lt;p>&lt;code>generador&lt;/code> y &lt;code>evaluador&lt;/code> son grafos compilados. Cuando se invocan dentro de un nodo del grafo padre con &lt;code>.invoke(...)&lt;/code>, reciben la configuración del padre, y con ella el checkpointer. Sin hacer nada, cada uno guarda sus propios checkpoints bajo un espacio de nombres derivado del nodo y de la tarea.&lt;/p>
&lt;p>Medido con &lt;code>InMemorySaver&lt;/code>, una ficha con tres intentos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Agentes compilados con&lt;/th>
&lt;th>Checkpoints del padre&lt;/th>
&lt;th>Checkpoints de subgrafos&lt;/th>
&lt;th>Total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Valor por defecto&lt;/td>
&lt;td>12&lt;/td>
&lt;td>18&lt;/td>
&lt;td>30&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>checkpointer=False&lt;/code>&lt;/td>
&lt;td>12&lt;/td>
&lt;td>0&lt;/td>
&lt;td>12&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los 18 de los subgrafos son tres por cada invocación de agente, seis invocaciones. Son el estado interno de cada llamada: los mensajes de esa llamada, la respuesta estructurada, y el resultado. Ninguno sirve para nada después: la ficha ya está en el estado del padre como diccionario, y un subgrafo sin herramientas ni interrupciones no tiene nada que reanudar.&lt;/p>
&lt;p>&lt;code>checkpointer=False&lt;/code> en &lt;code>create_agent&lt;/code> le dice al agente que no persista aunque el padre lo haga. Es distinto de &lt;code>checkpointer=None&lt;/code>, que es el valor por defecto y significa &amp;ldquo;hereda&amp;rdquo;. Con treinta mil fichas al año y tres intentos de media, la diferencia son 570.000 filas de checkpoint que no dicen nada, más sus blobs.&lt;/p>
&lt;p>Hay un caso en que se quiere lo contrario: si el generador tuviera herramientas con efecto o una interrupción de aprobación, sus checkpoints serían lo que permite reanudarlo. Aquí no es el caso, y tampoco lo es en el analista de referencias del caso 3, que por eso lleva el mismo parámetro.&lt;/p>
&lt;h2 id="el-objeto-pydantic-en-el-estado">El objeto pydantic en el estado&lt;/h2>
&lt;p>Al ejecutar el bucle con los agentes por defecto, LangGraph 1.2.11 escribe esto al deserializar el checkpoint del subgrafo:&lt;/p>
&lt;pre tabindex="0">&lt;code>Deserializing unregistered type __main__.Ficha from checkpoint. This will be blocked in a
future version. Set LANGGRAPH_STRICT_MSGPACK=true to block now, or add to
allowed_msgpack_modules to allow explicitly: [(&amp;#39;__main__&amp;#39;, &amp;#39;Ficha&amp;#39;)]
&lt;/code>&lt;/pre>&lt;p>&lt;code>structured_response&lt;/code> en el estado del agente es un objeto &lt;code>Ficha&lt;/code>. El serializador lo guarda con su módulo y su clase, y al leerlo comprueba que el tipo esté permitido. Hoy es un aviso; el mensaje dice que será bloqueo. Cuando lo sea, un checkpoint escrito hoy con un objeto pydantic dentro no se podrá leer con la versión de mañana.&lt;/p>
&lt;p>Tres formas de no tener el problema, y aquí se usan las tres. El estado del padre solo lleva diccionarios: &lt;code>model_dump()&lt;/code> en cuanto el objeto sale del agente. Los subgrafos no persisten, así que no hay checkpoint con el objeto. Y el checkpointer del padre se construye con un serializador que conoce el módulo de los esquemas, por si alguna vez uno se cuela:&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">langgraph.checkpoint.serde.jsonplus&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">JsonPlusSerializer&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">checkpointer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">PostgresSaver&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">pool_agentes&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">serde&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">JsonPlusSerializer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">allowed_msgpack_modules&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[(&lt;/span>&lt;span class="s2">&amp;#34;fichas.esquemas&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Ficha&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;fichas.esquemas&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Veredicto&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;/code>&lt;/pre>&lt;/div>&lt;p>Es la misma regla que en el caso 2 con el contexto y en el caso 3 con &lt;code>motivo&lt;/code>: lo que va al checkpoint se decide, no se deja caer.&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>Tasa de aprobación al primer, segundo y tercer intento&lt;/td>
&lt;td>&lt;code>historial&lt;/code> en la tabla de fichas&lt;/td>
&lt;td>La curva del bucle. Si el tercer intento aprueba menos del 10 por ciento de lo que llegó a él, el tope debería ser dos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Criterios más rechazados por intento&lt;/td>
&lt;td>&lt;code>historial[*].veredicto.problemas&lt;/code>&lt;/td>
&lt;td>Un criterio que se rechaza siempre en el primer intento y nunca en el segundo es una instrucción que falta en el prompt del generador&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fichas a revisión manual&lt;/td>
&lt;td>&lt;code>estado = 'revision_manual'&lt;/code>&lt;/td>
&lt;td>Debe bajar con el tiempo. Si no baja, los criterios y el prompt están desalineados&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rechazos forzados por código con veredicto del modelo aprobado&lt;/td>
&lt;td>&lt;code>comprobaciones&lt;/code> frente a &lt;code>veredicto&lt;/code>&lt;/td>
&lt;td>Cuántas veces el modelo aprobó algo que &lt;code>len()&lt;/code> rechazó. Es la medida de cuánto no se puede fiar uno del evaluador en lo mecánico&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokens por ficha publicada&lt;/td>
&lt;td>Langfuse, &lt;code>caso=fichas&lt;/code>, dividido por publicadas&lt;/td>
&lt;td>Unos 2.500 por intento con los prompts actuales; el coste real es este número por la media de intentos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Devoluciones con motivo &amp;ldquo;no coincide con la descripción&amp;rdquo;&lt;/td>
&lt;td>Tabla de devoluciones, 90 días después&lt;/td>
&lt;td>La métrica de negocio. Tarda en llegar y es la que importa&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Fichas revisadas al azar por catálogo que se rechazan&lt;/td>
&lt;td>Muestreo manual semanal&lt;/td>
&lt;td>La calibración del evaluador. Si catálogo rechaza más del 5 por ciento de lo que el evaluador aprobó, los criterios están mal descritos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La última fila es la que hace que el sistema no se degrade sin que nadie lo vea. Un evaluador con modelo aprueba lo que su prompt le dice que apruebe; una muestra semanal leída por una persona es lo que mantiene el prompt honesto.&lt;/p>
&lt;h2 id="checklist">Checklist&lt;/h2>
&lt;ul>
&lt;li>Los criterios viven en una tabla versionada, con la parte mecánica comprobada en código.&lt;/li>
&lt;li>El evaluador no ve el prompt del generador, sus intentos anteriores ni el número de intento.&lt;/li>
&lt;li>El generador recibe el origen y los problemas, no su ficha anterior.&lt;/li>
&lt;li>&lt;code>decidir&lt;/code> lee &lt;code>aprobada&lt;/code> e &lt;code>intentos&lt;/code> y no llama al modelo.&lt;/li>
&lt;li>&lt;code>evaluar&lt;/code> fuerza el rechazo si una comprobación en código falla.&lt;/li>
&lt;li>Los dos agentes llevan &lt;code>ProviderStrategy&lt;/code> y el modelo lleva &lt;code>structured_output: True&lt;/code> en el perfil.&lt;/li>
&lt;li>Los esquemas son planos y las restricciones de longitud están también en código.&lt;/li>
&lt;li>Los dos agentes llevan &lt;code>checkpointer=False&lt;/code>.&lt;/li>
&lt;li>El estado del padre solo tiene diccionarios; &lt;code>model_dump()&lt;/code> a la salida de cada agente.&lt;/li>
&lt;li>&lt;code>allowed_msgpack_modules&lt;/code> en el checkpointer registra el módulo de los esquemas.&lt;/li>
&lt;li>Hay una muestra semanal de fichas aprobadas que lee una persona.&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>&lt;code>response_format=Ficha&lt;/code> con un modelo de vLLM es una herramienta, no un &lt;code>response_format&lt;/code>.&lt;/strong> Sin perfil, LangChain no sabe que el proveedor admite salida estructurada y usa &lt;code>ToolStrategy&lt;/code>. Funciona, pero paga la definición de la herramienta en cada llamada y falla de otra manera. &lt;code>ProviderStrategy&lt;/code> explícito y &lt;code>structured_output: True&lt;/code> en el perfil.&lt;/p>
&lt;p>&lt;strong>Un agente dentro de un nodo persiste solo.&lt;/strong> Hereda el checkpointer del padre y escribe bajo su espacio de nombres. 30 filas por ficha en vez de 12. &lt;code>checkpointer=False&lt;/code> cuando no hay nada que reanudar.&lt;/p>
&lt;p>&lt;strong>El objeto pydantic en el checkpoint es un aviso hoy y un error mañana.&lt;/strong> &lt;code>model_dump()&lt;/code> antes de escribir en el estado, y &lt;code>allowed_msgpack_modules&lt;/code> como red.&lt;/p>
&lt;p>&lt;strong>Las restricciones del esquema no las aplica el servidor.&lt;/strong> &lt;code>max_length&lt;/code> lo valida pydantic después de generar. La primera barrera para lo mecánico es código antes del evaluador.&lt;/p>
&lt;p>&lt;strong>El evaluador aprueba más si sabe que es el último intento.&lt;/strong> No se le dice. Tampoco se le dice qué intentó el generador. Solo la ficha, las comprobaciones, el origen y los criterios.&lt;/p>
&lt;p>&lt;strong>Darle al generador su ficha anterior empeora.&lt;/strong> Reescribe lo que estaba bien y reintroduce problemas. Origen más lista de problemas.&lt;/p>
&lt;p>&lt;strong>El tope de intentos no se calibra a ojo.&lt;/strong> Se mide la aprobación por intento. Si el tercero apenas aprueba, es coste sin resultado y el tope baja a dos.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>El agente de fichas es dos llamadas al modelo por intento y un contador. Lo que lo hace fiable no está en las llamadas: está en que el evaluador no ve lo que no debe, en que lo mecánico se comprueba sin modelo antes y después, en que el bucle lo cierra una función y no el modelo, y en que lo que entra en el checkpoint es lo que se ha decidido que entre. Y una muestra semanal leída por alguien de catálogo, que es lo que mantiene los criterios pegados a la realidad.&lt;/p>
&lt;p>Con esto los cuatro casos están escritos. El siguiente y último artículo de la serie no construye ningún agente: sirve los cuatro en el mismo cluster, sin el servidor de LangGraph, con la autorización por hilo, la retención y las sondas que ninguno de los cuatro tiene.&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-cadena-suministro-langgraph-send-map-reduce/">Agente de cadena de suministro con LangGraph: incidencias y reposición con map-reduce&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/servir-agentes-langgraph-sin-langgraph-api/">Servir agentes de LangGraph sin langgraph-api: autorización por hilo, retención y sondas&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/deepagents-sdk-libre-servidor-con-licencia/">deepagents en un cluster propio: el SDK es libre, el servidor no&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Versionado de prompts con Langfuse y MLflow&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>Salida estructurada en LangChain 1.x (&lt;code>ToolStrategy&lt;/code>, &lt;code>ProviderStrategy&lt;/code>): &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>API de grafos de LangGraph (reductores, aristas condicionales, subgrafos): &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>Código de &lt;code>langchain.agents.factory&lt;/code> (langchain 1.4.1): &lt;code>_supports_provider_strategy&lt;/code> y la resolución de &lt;code>AutoStrategy&lt;/code>&lt;/li>
&lt;li>Código de &lt;code>langgraph.checkpoint.serde.jsonplus&lt;/code> (langgraph-checkpoint 4.2.0): aviso de tipos no registrados y &lt;code>allowed_msgpack_modules&lt;/code>&lt;/li>
&lt;li>vLLM, salida estructurada y decodificación guiada: &lt;a href="https://docs.vllm.ai/en/latest/features/structured_outputs.html">https://docs.vllm.ai/en/latest/features/structured_outputs.html&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>