Environment-specific configuration: dev vs staging vs prod
Why this matters
In development you want fast, cheap models and verbose logging; in production you need real models, error handling, and cost controls. Hardcoding these differences kills your ability to deploy safely: environment config makes your chain adapt automatically.
Explanation
What it is: Environment-specific configuration means your LangChain application reads settings (API keys, model names, temperature, timeout values, logging level) from environment variables or a config object that changes based on where the code runs. How it works: You create a configuration class or dictionary that reads from os.environ and defaults to safe values. Your chain initialization code branches on the environment (dev/staging/prod) and sets up the LLM, retriever, and chain differently. For example: in dev you use gpt-4o-mini with temperature 0.7 and streaming enabled; in prod you use gpt-4-turbo with temperature 0, streaming disabled, and exponential backoff retry logic. When to use it: Always, once you have more than one person or machine running your code, or once you need to test before going live.
Code
import os
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
class LangChainConfig(BaseModel):
"""Environment-aware configuration for LangChain chains."""
environment: Literal["dev", "staging", "prod"] = Field(
default_factory=lambda: os.getenv("ENVIRONMENT", "dev")
)
openai_api_key: str = Field(default_factory=lambda: os.getenv("OPENAI_API_KEY", ""))
model_name: str = Field(default_factory=lambda: os.getenv("MODEL_NAME", ""))
temperature: float = Field(default_factory=lambda: float(os.getenv("TEMPERATURE", "0.7")))
max_retries: int = Field(default_factory=lambda: int(os.getenv("MAX_RETRIES", "2")))
timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("TIMEOUT_SECONDS", "30")))
enable_streaming: bool = Field(default_factory=lambda: os.getenv("ENABLE_STREAMING", "true").lower() == "true")
verbose: bool = Field(default_factory=lambda: os.getenv("VERBOSE", "false").lower() == "true")
def set_defaults_for_environment(self) -> None:
"""Override settings based on environment."""
if self.environment == "dev":
if not self.model_name:
self.model_name = "gpt-4o-mini"
if self.temperature == 0.7:
self.temperature = 0.7
self.max_retries = 1
self.timeout_seconds = 60
self.enable_streaming = True
self.verbose = True
elif self.environment == "staging":
if not self.model_name:
self.model_name = "gpt-4-turbo"
self.temperature = 0.3
self.max_retries = 3
self.timeout_seconds = 45
self.enable_streaming = False
self.verbose = True
elif self.environment == "prod":
if not self.model_name:
self.model_name = "gpt-4-turbo"
self.temperature = 0.0
self.max_retries = 5
self.timeout_seconds = 30
self.enable_streaming = False
self.verbose = False
def create_chain(config: LangChainConfig):
"""Create a LangChain chain with environment-specific settings."""
config.set_defaults_for_environment()
if config.verbose:
print(f"[{config.environment.upper()}] Initializing chain with {config.model_name}")
llm = ChatOpenAI(
api_key=config.openai_api_key,
model=config.model_name,
temperature=config.temperature,
max_retries=config.max_retries,
timeout=config.timeout_seconds,
)
prompt = ChatPromptTemplate.from_template(
"You are a helpful assistant. Answer this concisely: {question}"
)
chain = prompt | llm | StrOutputParser()
return chain, config
if __name__ == "__main__":
os.environ["ENVIRONMENT"] = "dev"
os.environ["OPENAI_API_KEY"] = "sk-test-key"
config = LangChainConfig()
chain, config = create_chain(config)
print(f"Environment: {config.environment}")
print(f"Model: {config.model_name}")
print(f"Temperature: {config.temperature}")
print(f"Max retries: {config.max_retries}")
print(f"Streaming enabled: {config.enable_streaming}")
print(f"Verbose: {config.verbose}")
print(f"Timeout: {config.timeout_seconds}s")
print()
print("=== Switching to PROD ===")
os.environ["ENVIRONMENT"] = "prod"
config_prod = LangChainConfig()
chain_prod, config_prod = create_chain(config_prod)
print(f"Environment: {config_prod.environment}")
print(f"Model: {config_prod.model_name}")
print(f"Temperature: {config_prod.temperature}")
print(f"Max retries: {config_prod.max_retries}")
print(f"Streaming enabled: {config_prod.enable_streaming}")
print(f"Verbose: {config_prod.verbose}")
print(f"Timeout: {config_prod.timeout_seconds}s") [DEV] Initializing chain with gpt-4o-mini Environment: dev Model: gpt-4o-mini Temperature: 0.7 Max retries: 1 Streaming enabled: True Verbose: True Timeout: 60s === Switching to PROD === [PROD] Initializing chain with gpt-4-turbo Environment: prod Model: gpt-4-turbo Temperature: 0.0 Max retries: 5 Streaming enabled: False Verbose: False Timeout: 30s
What just happened?
The code defined a Pydantic configuration class that reads from environment variables with sensible defaults, then created a factory function that applies environment-specific overrides (dev gets cheaper model and more debugging, prod gets better model and stricter timeouts). When ENVIRONMENT changed from 'dev' to 'prod', the same code path produced different configurations automatically: no if-statements in the factory function itself, just pure data-driven defaults.
Common gotcha
The most common mistake is setting environment variables after importing your config. If you do os.environ["ENVIRONMENT"] = "prod" after config = LangChainConfig(), the config object was already created with the old value. Environment variables must be set before config instantiation, or you must pass them as constructor arguments. Also: developers often hardcode API keys in config classes instead of reading from os.environ: this means the code is only 'environment-aware' on paper, not in practice.
Error recovery
ValueError: invalid literal for int()KeyError: OPENAI_API_KEYAttributeError: Config object has no attribute 'model_name'Experienced dev note
The real production gotcha: environment variables get read once at application startup. If you change an environment variable while your app is running (common in container orchestration, Kubernetes pod restarts, etc.), your app won't see the change unless you reload the config or restart. Some teams use a config reloader that polls the environment every N seconds, or integrate with a secrets manager (AWS Secrets Manager, Vault) that pushes updates. Also: never log your config object in prod: it exposes API keys. Use a custom __repr__ that masks sensitive fields, or just log the environment name and model.
Check your understanding
You have a chain running in production with temperature=0.0. A new requirement asks for more creative responses, so you change TEMPERATURE=0.5 in the environment and restart the container. Your chain reads the new environment variable, but it's still not using temperature 0.5: the config object already had temperature hardcoded to 0.0 before the env var changed. Why did this happen, and what one-line change would fix it?
Show answer hint
The answer requires understanding that <code>set_defaults_for_environment()</code> checks the environment name and overrides the temperature value. In production mode, it explicitly sets <code>self.temperature = 0.0</code>, overwriting the environment variable. The fix is to only override the temperature in <code>set_defaults_for_environment()</code> if it wasn't explicitly set via environment variable: or to remove the hardcoded override and trust the environment variable entirely for prod.
from langchain.chat_models import ChatOpenAI pattern. Always use from langchain_openai import ChatOpenAI. Pydantic v2 is required (BaseModel behavior changed; if you're on Pydantic v1, Field will behave differently with default_factory).