De texto plano a objetos validados: elegir un analizador de salida para LangChain
Compare StrOutputParser, JsonOutputParser, StructuredOutputParser y PydanticOutputParser en las cadenas de LangChain, y averigüe cuánta estructura realmente garantiza cada uno.
Si desea conocer una visión más amplia de cómo los analizadores coexisten con LCEL, los ejecutables y la memoria, consulte de texto bruto a pipelines en LangChain. Aquí el enfoque es más específico: qué promete realmente cada analizador y dónde termina esa promesa.
Por qué el texto bruto del modelo no es suficiente
Si le pregunta a un modelo sobre el Sistema Solar, es posible que obtenga algo similar a lo siguiente. Se lee bien, pero un programa no puede extraer un hecho específico de él sin adivinar dónde termina una oración y comienza la siguiente.
The Solar System consists of the Sun and the objects that orbit it.
It contains eight planets along with moons, asteroids, and comets.
El código que consume esta respuesta preferiría recibir valores con nombres a los que pueda acceder directamente, por ejemplo, un objeto con una clave por cada hecho:
{
"fact_1": "The Solar System contains eight planets.",
"fact_2": "The Sun is at the center of the Solar System.",
"fact_3": "The Solar System also contains moons, asteroids, and comets."
}
Un analizador de salida es el componente que conecta estas dos formas. Recibe todo lo que genera el modelo y devuelve un valor que su aplicación puede utilizar sin necesidad de realizar manipulaciones adicionales en las cadenas de texto. Los analizadores difieren en el grado de procesamiento: algunos solo desempaquetan el texto, otros analizan JSON, y los más estrictos validan el resultado contra un esquema.
Raw LLM Response
↓
Output Parser
↓
Parsed Output
Tenga presente esta imagen de tres etapas. Cada ejemplo que se presenta a continuación es una variante de ella, con más componentes añadidos al bloque intermedio a medida que aumentan los requisitos.
Cuatro analizadores, cuatro niveles de estructura
Los cuatro analizadores que se tratan aquí forman una escalera, donde cada peldaño añade un nivel adicional de control sobre el resultado:
StrOutputParserconvierte el mensaje del modelo en una cadena de texto simple en Python.JsonOutputParseranaliza la respuesta y la convierte en un valor compatible con JSON, como un diccionario o una lista.
StructuredOutputParser le permite declarar los campos con nombre que el modelo debe devolver.PydanticOutputParser describe la estructura esperada mediante un modelo Pydantic y valida la salida en función de ella.Independientemente del que elija, la ruta de los datos es la misma:
LLM Response
↓
Output Parser
↓
Parsed Output
Lo que cambia es lo que se obtiene al final y hasta qué punto se puede confiar en ello.
Ejemplo en ejecución: informe y luego resumen
El primer proyecto es un pipeline de dos pasos. Un tema, “Sistema Solar”, se envía a un modelo que genera un informe extenso. Ese informe luego se pasa a una segunda instrucción que solicita un resumen de cinco líneas. El detalle clave es la transferencia: la salida de la primera llamada al modelo se convierte en la entrada de la siguiente instrucción.
Solar System
↓
LLM
↓
Detailed Report
↓
LLM
↓
5-Line Summary
Sin un analizador, el primer paso devuelve un objeto de mensaje y no el texto del informe, por lo que habría que desempaquetarlo antes de crear el segundo mensaje. Un analizador intermedio realiza ese proceso de desempaquetado como parte de la cadena, lo que permite que los dos pasos se combinen de manera ordenada.
StrOutputParser: cuando lo único que necesitas es texto
StrOutputParser es el analizador más sencillo que ofrece LangChain. Los modelos de chat devuelven un AIMessage; este analizador extrae su contenido y te proporciona una cadena de texto normal. Es la herramienta adecuada cuando el siguiente consumidor es un humano, otro mensaje o cualquier cosa que solo requiera texto narrativo, y no necesites JSON ni un esquema.
Integrarlo en el flujo de generación de informes
En el proyecto report-then-summary, los pasos son:
- El primer mensaje solicita al modelo un informe detallado sobre el tema.
Después de cada llamada al modelo hay un StrOutputParser, por lo que cada transferencia consiste en una cadena de texto simple. La secuencia completa de componentes es:
Solar System
↓
Prompt 1
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Prompt 2
↓
LLM
↓
StrOutputParser
↓
5-Line Summary
La versión de OpenAI
Aquí está la cadena completa que utiliza ChatOpenAI. Léanla de arriba abajo una vez; luego analizaremos las partes importantes.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
load_dotenv()
model = ChatOpenAI()
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
parser = StrOutputParser()
chain = template1 | model | parser | template2 | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
El modelo se crea con configuraciones predeterminadas. La función load_dotenv() anterior extrae la clave de API de un archivo local .env, por lo que no hay credenciales en el script.
model = ChatOpenAI()
La primera plantilla acepta una sola variable, topic, y solicita un informe detallado sobre ella.
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
El segundo plantilla espera una variable llamada text, que recibirá el informe, y solicita un resumen de cinco líneas del mismo.
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
Hay un pequeño error que vale la pena corregir si copias esto: la cadena de plantilla contiene /n, que es una barra literal seguida de la letra n, y no un salto de línea. Usa \n si deseas que el informe comience en una nueva línea. Los modelos suelen funcionar de ambas maneras, pero el mensaje que crees que estás enviando debe ser el mismo que realmente envías.
A continuación está el analizador. Se puede reutilizar una única instancia en varios puntos de la cadena, ya que no mantiene estado entre llamadas.
parser = StrOutputParser()
La línea que une todo es la definición de la cadena:
chain = template1 | model | parser | template2 | model | parser
El operador de tubería es una composición en LCEL (LangChain Expression Language): la salida de cada componente se convierte en la entrada del siguiente. Disposición verticalmente, el orden de ejecución es:
template1
↓
model
↓
parser
↓
template2
↓
model
↓
parser
Observe qué le ofrece el primer analizador. Después de la primera llamada al modelo, el analizador devuelve el informe como una cadena de texto, y esa cadena es la que llena {text} en el segundo plantilla. El segundo analizador realiza la misma tarea para la respuesta final, por lo que el resultado de la cadena es el resumen en sí y no un objeto de mensaje.
Comienza la cadena pasando un diccionario cuyas claves coinciden con las variables de entrada del primer plantilla:
result = chain.invoke({'topic': 'Solar System'})
Luego muestra el resultado:
print(result)
Qué devuelve la cadena
El valor que se muestra al final es el resumen de cinco líneas obtenido a partir del informe generado. Dado que el último componente es un StrOutputParser, el resultado es una cadena regular de Python str:
print(result)
Una ejecución típica produce algo similar a lo siguiente:
1. The Solar System consists of the Sun and all objects that orbit it.
2. It includes eight planets, along with dwarf planets, moons, asteroids, and comets.
3. The four inner planets are rocky, while the outer planets are mostly gas or ice giants.
4. The Sun contains most of the Solar System's mass and provides the energy that drives many processes.
5. The Solar System is located in the Milky Way galaxy.
Considérelo como una ilustración de la estructura, no como una respuesta fija. La redacción variará entre ejecuciones y entre modelos.
Mismo flujo de trabajo con un modelo de Hugging Face
El proceso de informe seguido de resumen también puede aplicarse a un modelo público. Esta variante utiliza HuggingFaceEndpoint dirigido a google/gemma-2-2b-it y lo envuelve en ChatHuggingFace para que se comporte como un modelo de chat:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
# 1st prompt -> detailed report
template1 = PromptTemplate(
template='Write a detailed report on {topic}',
input_variables=['topic']
)
# 2nd prompt -> summary
template2 = PromptTemplate(
template='Write a 5 line summary on the following text. /n {text}',
input_variables=['text']
)
prompt1 = template1.invoke({'topic': 'Solar System'})
result = model.invoke(prompt1)
prompt2 = template2.invoke({'text': result.content})
result1 = model.invoke(prompt2)
print(result1.content)
Existe una diferencia importante aquí. Esta versión nunca utiliza StrOutputParser ni crea una cadena de tuberías. Formatea cada prompt manualmente con .invoke(), llama al modelo y lee .content del mensaje devuelto antes de pasarlo adelante. Eso funciona, pero es exactamente el proceso manual de desempaquetado que existe para eliminar.
Junto a ellos, los dos enfoques se ven así. Con OpenAI y un parser:
OpenAI
Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
String
Con Hugging Face y acceso manual:
Hugging Face
Prompt
↓
ChatHuggingFace
↓
result.content
↓
String
Ambos terminan en una cadena de texto. El script de OpenAI muestra al analizador realizando esa tarea dentro de una cadena, mientras que el script de Hugging Face muestra la misma lógica de aplicación con un proveedor diferente y sin analizador. Nada impide escribir template1 | model | parser | template2 | model | parser también con el modelo de Hugging Face; al analizador no le importa qué proveedor generó el mensaje.
La conclusión de este nivel es: utilice StrOutputParser siempre que la aplicación solo necesite la respuesta en formato de texto.
JsonOutputParser: JSON sin contrato
JsonOutputParser representa el siguiente paso. Pide al modelo que genere JSON y convierte la respuesta en datos de Python, lo cual es útil cuando su código necesita extraer valores por clave en lugar de leer texto narrativo.
Lo que no hace es imponer una forma específica. Sin un esquema, le indica al modelo que responda en JSON, pero no dice nada sobre qué claves deben aparecer ni qué tipo de valores deben tener. Dos ejecuciones con el mismo prompt pueden devolver legítimamente objetos de formas diferentes, y su código debe estar preparado para eso.
Cómo encajan las piezas
Este proyecto pide al modelo cinco hechos sobre el Sistema Solar. Los pasos son:
- Crear un
JsonOutputParser. - Pedirle instrucciones de formato con
get_format_instructions(). - Incluir esas instrucciones en el prompt.
- Enviar el prompt al modelo.
- Dejar que el analizador convierta la respuesta en un valor de Python.
Solar System
↓
PromptTemplate
↓
Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON Object
La versión de OpenAI
La cadena completa es corta:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
parser = JsonOutputParser()
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
El modelo está configurado con gpt-4.1-mini y una temperatura de 0, lo que mantiene la salida tan reproducible como permite el modelo. Es el componente que escribirá los cinco hechos.
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
El analizador se crea sin argumentos, y esa es precisamente la razón por la cual no tiene un esquema que aplicar:
parser = JsonOutputParser()
Las instrucciones de formato realizan el trabajo real
Antes de que ejecute el modelo, el analizador puede describir el formato que espera. Esa descripción proviene de una sola llamada a método:
parser.get_format_instructions()
Devuelve un bloque de texto que indica al modelo que responda en formato JSON. No se trata de código que se ejecute sobre el modelo; es texto de prompt. Se inserta en la plantilla a través de partial_variables, lo cual llena una variable de la plantilla una sola vez, cuando se define la plantilla, y no en cada llamada:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
La plantilla resultante contiene dos marcadores de posición:
template = PromptTemplate(
template="Give me 5 facts about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic}se proporciona en el momento de la llamada y indica sobre qué información se desea obtener datos.{format_instruction}ya está prellenado con las indicaciones de formato del analizador.
Por lo tanto, cuando se invoca la cadena con esta entrada, el modelo recibe tanto el tema como la instrucción para responder en formato JSON:
{"topic": "Solar System"}
Montaje y ejecución de la cadena
La propia cadena consta únicamente de tres etapas:
chain = template | model | parser
En orden de ejecución:
PromptTemplate
↓
ChatOpenAI
↓
JsonOutputParser
↓
Parsed JSON
La plantilla genera el prompt final, ChatOpenAI lo responde y JsonOutputParser analiza la respuesta para convertirla en datos en Python. El analizador también es tolerante con algunas costumbres comunes de los modelos, como el hecho de que el JSON esté envuelto en un marco de código Markdown, el cual se elimina antes del análisis.
Invóquelo con el tema:
result = chain.invoke({"topic": "Solar System"})
Y imprima lo que devuelve:
print(result)
Lo que se devuelve
El resultado contiene cinco hechos en formato JSON. El script solo imprime el valor, por lo que no hay una salida canónica que citar; una respuesta plausible sería esta:
{
"facts": [
"The Solar System is centered around the Sun.",
"There are eight recognized planets in the Solar System.",
"The four inner planets are rocky planets.",
"The outer planets include gas giants and ice giants.",
"The Solar System is located in the Milky Way galaxy."
]
}
Esa estructura, con una única clave facts que contiene una lista, es una de las varias que el modelo podría elegir. Otra ejecución podría devolver fact_1 hasta fact_5, o simplemente una lista. Si el código posterior accede a result["facts"], dejará de funcionar en el día en que el modelo elija un formato diferente.
Rastreando el flujo
En comparación con StrOutputParser, la novedad es que el analizador participa dos veces: una vez antes de la llamada al modelo, al proporcionar instrucciones, y otra vez después, al analizar los datos.
Prompt
↓
JSON Format Instructions
↓
LLM
↓
JsonOutputParser
↓
JSON
Las instrucciones son generadas por el propio analizador:
parser.get_format_instructions()
Llegan al prompt a través de la variable parcial:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
El modelo responde teniendo en cuenta esas instrucciones, y el analizador convierte la respuesta en un valor de Python. De principio a fin:
Solar System
↓
PromptTemplate
↓
JSON Format Instructions
↓
ChatOpenAI
↓
JsonOutputParser
↓
JSON Object
La diferencia con el nivel anterior cabe en dos líneas. StrOutputParser genera:
StrOutputParser
↓
Plain String
Mientras que JsonOutputParser genera:
JsonOutputParser
↓
JSON-compatible Structured Data
Tenga en cuenta que aún no existe un esquema fijo. Se obtiene JSON, pero los campos y la estructura anidada dependen del modelo. Como nota adicional, las versiones actuales de JsonOutputParser también aceptan un argumento opcional pydantic_object que agrega un esquema a las instrucciones de formato, pero en la forma mostrada aquí, sin argumentos, solo exige JSON válido.
Variante de Hugging Face
El flujo de trabajo en JSON se conecta directamente al modelo Gemma. La configuración del modelo cambia; el analizador, las instrucciones de formato y la cadena no lo hacen:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
parser = JsonOutputParser()
template = PromptTemplate(
template='Give me 5 facts about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
La pipeline es idéntica excepto por el bloque del modelo:
PromptTemplate
↓
Hugging Face Model
↓
JsonOutputParser
↓
JSON Object
Este es el beneficio práctico de colocar la解析 en un componente separado: cambiar los proveedores no afecta a la lógica de parsing. Sin embargo, hay que tener en cuenta que los modelos abiertos pequeños siguen las instrucciones de formato con menos fiabilidad que los modelos alojados más grandes. Si la respuesta contiene texto adicional alrededor del JSON o una coma al final, el analizador genera un OutputParserException; por lo tanto, el código de producción debe capturarlo y volver a intentarlo o recurrir a una solución alternativa.
StructuredOutputParser: nombrar los campos que se esperan
StructuredOutputParser extrae JSON según una lista de campos que se definen por adelantado. Mientras que JsonOutputParser simplemente indica “respuesta en JSON”, este analizador especifica “respuesta en JSON con estas claves”.
Los campos se declaran mediante ResponseSchema. Cada uno cuenta con un name y un description; la descripción indica al modelo qué contenido debe incluirse en ese campo. Esto permite un control mucho mayor sobre la estructura de la respuesta.
El proyecto de tres hechos
Este proyecto solicita tres hechos sobre el Sistema Solar, con un campo por hecho:
fact_1alberga el primer hecho sobre el tema.fact_2alberga el segundo.fact_3alberga el tercero.
El analizador convierte esas declaraciones en instrucciones y luego verifica la respuesta contra ellas:
Solar System
↓
PromptTemplate
↓
Predefined Field Schema
↓
LLM
↓
StructuredOutputParser
↓
Structured JSON
La versión de OpenAI
Aquí está el script completo:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define Model
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
result = chain.invoke({"topic": "Solar System"})
print(result)
El modelo sigue teniendo la misma configuración gpt-4.1-mini que antes:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
La verdadera diferencia comienza con la lista de esquemas:
schema = [
ResponseSchema(name="fact_1", description="Fact 1 about the topic"),
ResponseSchema(name="fact_2", description="Fact 2 about the topic"),
ResponseSchema(name="fact_3", description="Fact 3 about the topic"),
]
Cada ResponseSchema aporta dos elementos:
namese convierte en la clave del diccionario resultante.descriptionindica al modelo qué debe contener esa clave.
Tres esquemas generan tres claves obligatorias: fact_1, fact_2 y fact_3.
No se instancia este analizador directamente. Un método de clase lo crea a partir de la lista de esquemas:
parser = StructuredOutputParser.from_response_schemas(schema)
Al igual que con el analizador JSON, las instrucciones de formato provienen del propio analizador:
parser.get_format_instructions()
Esta vez las instrucciones son más detalladas. Incluyen un esqueleto en JSON que enumera cada nombre de campo con su descripción, y piden al modelo que envuelva la respuesta en un bloque JSON delimitado. Se conectan de la misma manera:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
El plantilla de prompt cuenta con los dos marcadores de posición habituales:
template = PromptTemplate(
template="Give 3 fact about {topic} \n {format_instruction}",
input_variables=["topic"],
partial_variables={
"format_instruction": parser.get_format_instructions()
},
)
{topic} se rellena en el momento de la llamada, mientras que {format_instruction} contiene la lista de campos generada a partir de sus esquemas. Al invocar con esta entrada, ambas se envían al modelo:
{"topic": "Solar System"}
Ejecución de la cadena
La cadena tiene las mismas tres etapas que antes:
chain = template | model | parser
Con el analizador en la última posición:
PromptTemplate
↓
ChatOpenAI
↓
StructuredOutputParser
↓
Structured JSON
El plantilla construye el prompt, el modelo responde, y StructuredOutputParser extrae los campos declarados de la respuesta.
result = chain.invoke({"topic": "Solar System"})
print(result)
Así se ve la salida
El resultado es un diccionario que contiene tres datos bajo las claves que definió. El script lo imprime:
print(result)
Y un ejemplo representativo del resultado es:
{
"fact_1": "The Solar System is centered around the Sun.",
"fact_2": "There are eight recognized planets in the Solar System.",
"fact_3": "The Solar System is located in the Milky Way galaxy."
}
Los datos específicos variarán. Lo que no debe cambiar es el conjunto de claves:
fact_1
fact_2
fact_3
Esa es la ventaja frente al análisis simple de JSON: su aplicación decide los nombres de los campos, no el modelo. Si el modelo omite una de las claves declaradas, el analizador genera un error en lugar de devolver silenciosamente una estructura diferente, lo cual es mucho más fácil de manejar que un KeyError tres funciones después.
Rastreando el flujo
La ruta completa, desde el tema hasta el resultado con claves:
Solar System
↓
PromptTemplate
↓
ResponseSchema
↓
Format Instructions
↓
ChatOpenAI
↓
StructuredOutputParser
↓
{
fact_1: ...,
fact_2: ...,
fact_3: ...
}
Comienza con las definiciones de los campos:
ResponseSchema(
name="fact_1",
description="Fact 1 about the topic"
)
El analizador convierte esos elementos en instrucciones de formato, las instrucciones se incluyen en la solicitud, el modelo responde y el analizador extrae los campos declarados. En comparación con la etapa anterior:
JsonOutputParser
↓
JSON output
↓
Structure can vary
StructuredOutputParser
↓
Predefined fields
↓
More controlled structure
En resumen, JsonOutputParser se encarga de obtener JSON en general, mientras que StructuredOutputParser busca obtener JSON con las claves que se hayan solicitado.
Existe un límite que cabe mencionar claramente. ResponseSchema cuenta con un atributo type cuyo valor por defecto es string, pero este solo modifica la redacción de las instrucciones. El analizador verifica que las claves estén presentes; no valida los tipos ni rangos de los valores. Si se necesita que age sea un número entero superior a cierto umbral, este analizador no lo garantizará.
También verifique la ruta de importación en relación con la versión de LangChain que está utilizando. El ejemplo importa desde langchain.output_parsers, y en las versiones más recientes este analizador heredado ha sido eliminado de los paquetes principales, por lo que la importación podría necesitar cambios.
Variante de Hugging Face
La versión Gemma reutiliza los mismos tres esquemas y la misma estructura del analizador:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
load_dotenv()
# Define the model
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
schema = [
ResponseSchema(name='fact_1', description='Fact 1 about the topic'),
ResponseSchema(name='fact_2', description='Fact 2 about the topic'),
ResponseSchema(name='fact_3', description='Fact 3 about the topic'),
]
parser = StructuredOutputParser.from_response_schemas(schema)
template = PromptTemplate(
template='Give 3 fact about {topic} \n {format_instruction}',
input_variables=['topic'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
result = chain.invoke({'topic': 'Solar System'})
print(result)
Y el mismo pipeline:
PromptTemplate
↓
ChatHuggingFace
↓
StructuredOutputParser
↓
Structured JSON
Solo difiere el proveedor del modelo. El esquema y el analizador permanecen iguales.
PydanticOutputParser: estructura más validación
PydanticOutputParser es el más estricto de los cuatro. Describe la respuesta esperada mediante un modelo Pydantic, por lo que la definición de la salida también es la definición de lo que se considera válido.
Eso va más allá del análisis simple. Los campos tienen tipos reales de Python, y Field() permite establecer restricciones, por ejemplo que un número entero debe superar un valor mínimo. Cuando la respuesta del modelo no cumple con esas restricciones, se genera una excepción en lugar de datos inválidos.
Por qué vale la pena el esfuerzo adicional
- Aplicación de esquemas: la respuesta debe ajustarse a una estructura bien definida.
- Seguridad de tipos: los campos utilizan tipos de Python como
str,intyfloat, y los valores se convierten o rechazan en consecuencia. - Validación: Pydantic verifica cada restricción que se declare.
- Integración en cadenas: se integra con prompts, modelos y cadenas LCEL de la misma manera que los otros analizadores.
El proyecto de personajes ficticios
Este ejemplo pide al modelo que invente una persona de un lugar determinado, en este caso “India”, con tres campos:
name, el nombre de la persona.age, la edad de la persona.city, la ciudad en la que vive.
El campo age también tiene una restricción: debe ser mayor que 18.
Input
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
LLM
↓
PydanticOutputParser
↓
Validated Pydantic Object
La versión de OpenAI
El script completo:
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
chain = template | model | parser
final_result = chain.invoke({"place": "Indian"})
print(final_result)
Sigue el mismo esquema que antes: se define un modelo Person, se pasa a PydanticOutputParser y el analizador se conecta a un prompt y a un modelo.
La configuración del modelo no ha cambiado:
model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
El elemento central es la clase Pydantic:
class Person(BaseModel):
name: str = Field(description="Name of the person")
age: int = Field(gt=18, description="Age of the person")
city: str = Field(description="Name of the city of the person")
Este es el contrato para la respuesta:
namedebe ser una cadena de texto.
age debe ser un número entero estrictamente mayor que 18.city debe ser una cadena de texto.Field() incluye tanto una descripción legible por humanos, que aparecerá en la solicitud, como restricciones, que se verifican después del análisis. El campo restringido por sí solo:
age: int = Field(gt=18, description="Age of the person")
gt=18 significa “mayor que 18”, por lo que una edad exacta de 18 no pasa la validación. Si se quiere decir “18 o más”, use ge=18 en su lugar.
El analizador se crea pasando la clase, no una instancia:
parser = PydanticOutputParser(pydantic_object=Person)
Eso indica qué modelo utilizar tanto para generar instrucciones como para validar la respuesta.
Las instrucciones de formato provienen del mismo método que antes:
parser.get_format_instructions()
Para este analizador, contienen un JSON Schema generado a partir del modelo Pydantic, que incluye descripciones de campos y la restricción exclusiveMinimum para age. Se insertan a través de la variable parcial como de costumbre:
partial_variables={
"format_instruction": parser.get_format_instructions()
}
El template de la solicitud:
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=["place"],
partial_variables={"format_instruction": parser.get_format_instructions()},
)
{place} controla qué tipo de persona debe crear el modelo. Al usar esta entrada, se solicitan el nombre, la edad y la ciudad de una persona india ficticia:
{"place": "Indian"}
Ejecución de la cadena
La cadena mantiene su estructura habitual de tres etapas:
chain = template | model | parser
Esta vez, la etapa final devuelve una instancia del modelo:
PromptTemplate
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Pydantic Object
El template construye la solicitud, el modelo responde y PydanticOutputParser analiza el JSON y lo valida para convertirlo en un Person.
final_result = chain.invoke({"place": "Indian"})
print(final_result)
Así se ve la salida
El resultado es un objeto Person, no un diccionario. Al imprimirlo se muestra la representación por defecto de Pydantic:
name='Rahul Sharma' age=28 city='Mumbai'
Los valores variarán de una ejecución a otra. Las garantías no lo harán:
name → string
age → integer (> 18)
city → string
Aquí es donde las diferencias son más evidentes. JsonOutputParser solo solicitó JSON, StructuredOutputParser nombró los campos, y PydanticOutputParser representa todo el contrato como una clase real. Puedes acceder a final_result.age con el completado automático del editor y estar seguro de que se trata de un int mayor a 18, ya que cualquier otro valor habría generado un error de validación antes de llegar a tu código.
Rastreando el flujo
De la entrada al objeto validado:
"Indian"
↓
PromptTemplate
↓
Pydantic Model
↓
Format Instructions
↓
ChatOpenAI
↓
PydanticOutputParser
↓
Person Object
Comienza con la estructura, mostrada aquí sin las descripciones y restricciones para mayor legibilidad:
class Person(BaseModel):
name: str
age: int
city: str
La clase se pasa al analizador:
PydanticOutputParser(pydantic_object=Person)
El analizador genera instrucciones a partir del modelo; estas instrucciones se incluyen en la solicitud, el modelo responde y el analizador procesa esa respuesta para convertirla en un objeto Person, aplicando además validación con Pydantic. Conceptualmente:
Pydantic Model
↓
Defines Structure + Types + Constraints
↓
LLM Response
↓
PydanticOutputParser
↓
Validated Pydantic Object
Se obtiene un objeto de Python cuyos datos están garantizados a seguir sus reglas, lo cual es más de lo que puede ofrecer cualquier diccionario JSON.
Una consecuencia práctica: un fallo en la validación se manifiesta como una OutputParserException en la cadena de procesamiento. Hay que decidir qué hacer en ese caso. Las opciones comunes son intentar la llamada nuevamente, devolver el error al modelo mediante OutputFixingParser de LangChain, o registrar el error y devolver un valor por defecto seguro.
Variante de Hugging Face
La versión Gemma define el mismo modelo Person y lo pasa al mismo analizador:
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
load_dotenv()
llm = HuggingFaceEndpoint(
repo_id="google/gemma-2-2b-it",
task="text-generation"
)
model = ChatHuggingFace(llm=llm)
class Person(BaseModel):
name: str = Field(description='Name of the person')
age: int = Field(gt=18, description='Age of the person')
city: str = Field(description='Name of the city the person belongs to')
parser = PydanticOutputParser(pydantic_object=Person)
template = PromptTemplate(
template='Generate the name, age and city of a fictional {place} person \n {format_instruction}',
input_variables=['place'],
partial_variables={
'format_instruction': parser.get_format_instructions()
}
)
chain = template | model | parser
final_result = chain.invoke({'place': 'Indian'})
print(final_result)
La secuencia de procesamiento no cambia:
PromptTemplate
↓
ChatHuggingFace
↓
PydanticOutputParser
↓
Pydantic Object
Solo cambia el proveedor; el modelo Pydantic y el analizador se comparten. Los modelos más pequeños tienen mayor probabilidad de incumplir las restricciones o agregar texto no deseado, y ese es precisamente el caso en el que la validación resulta útil: la mala respuesta se detecta en la entrada en lugar de filtrarse en los datos.
Elegir el analizador adecuado
La decisión depende de cuánta estructura y cuánta validación realmente necesita el destinatario de la respuesta. A continuación se resume cada opción.
StrOutputParser
Úsalo cuando la respuesta del modelo es simplemente texto.
- Más adecuado para: informes, explicaciones, resúmenes, respuestas de chat.
- Devuelve: una cadena de texto.
- Análisis JSON: no.
JsonOutputParser
Úselo cuando necesite JSON pero pueda tolerar o manejar una estructura variable.
- Más adecuado para: salida estructurada exploratoria, cargas útiles flexibles.
- Devuelve: un diccionario o una lista.
- Análisis de JSON: sí.
- Estructura: no (en la forma sin argumentos utilizada aquí).
- Validación: solo verifica que sea JSON válido.
StructuredOutputParser
Úselo cuando su código espera claves específicas, como de fact_1 a fact_3.
- Más adecuado para: registros simples con nombres de campos conocidos.
- Devuelve: un diccionario con las claves declaradas.
- Análisis de JSON: sí.
- Estructura: sí, con nombres de campos y descripciones.
- Validación: solo verifica la presencia de las claves, sin comprobaciones de tipo.
PydanticOutputParser
Úselo cuando la salida alimenta directamente la lógica de la aplicación y debe ser correcta.
- Mejor para: datos que almacena, con los que calcula o que pasa a APIs.
- Devuelve: una instancia de su modelo Pydantic.
- Análisis JSON: sí.
- Estructura: sí, tipos y restricciones completas.
- Validación: sí.
Un modelo mental rápido
StrOutputParser: el texto es suficiente.JsonOutputParser: cualquier JSON válido sirve.StructuredOutputParser: el JSON debe contener estas claves.PydanticOutputParser: se verifican estas claves, estos tipos y todas las restricciones.
Elija el analizador más simple que ofrezca las garantías de las que depende. Cada nivel adicional añade tokens de instrucción y nuevas formas en las que se puede rechazar una respuesta, por lo que un nivel de rigor mayor debe ser una elección deliberada.
Otra opción debe considerarse. Los cuatro analizadores funcionan describiendo un formato en la instrucción y luego analizando el texto. Muchos modelos de chat también admiten salida estructurada nativa o llamadas a herramientas, las cuales LangChain expone mediante with_structured_output() en el modelo. Cuando tu proveedor lo soporta, este enfoque suele ser más fiable para datos con esquema definido, mientras que los analizadores basados en instrucciones siguen siendo útiles para proveedores y modelos que no lo cuentan.
Puntos clave
- Los analizadores de salida convierten el mensaje del modelo en un valor que tu código puede utilizar, y se integran en cadenas LCEL con el operador de tubería.
StrOutputParserelimina el envoltorio del mensaje para que el texto de un modelo pueda alimentar la siguiente instrucción.JsonOutputParseranaliza JSON pero no corrige su estructura a menos que se le proporcione un esquema.
StructuredOutputParser corrige los nombres de las claves a través de ResponseSchema, pero no verifica los tipos de valor.PydanticOutputParser combina el análisis con verificaciones y restricciones de tipo, devolviendo un objeto real.Una vez que las respuestas llegan en un formato fiable, el siguiente paso lógico es combinar varios prompts, modelos y analizadores en flujos de trabajo más amplios, que incluyen cadenas secuenciales, paralelas y condicionales construidas con RunnableParallel y RunnableBranch. Para conocer más al respecto, consulte componer pipelines LangChain con LCEL.
Lecturas relacionadas
- De texto bruto a pipelines: analizadores, LCEL, runnables y memoria en LangChain — Cómo LangChain convierte el texto bruto del modelo en datos estructurados, combina pasos mediante LCEL e la interfaz Runnable, y gestiona la memoria en las conversaciones actualmente.