// FUNDAMENTOS · MÓDULO 02/12
LangChain v1 esencial para quants
El modelo mental Runnable, LCEL, salida estructurada con Pydantic y tool calling de bajo nivel.
El Módulo 1 estableció la regla que gobierna todo el curso: el LLM razona, la matemática decide, el humano veta. Este módulo convierte esa regla en código. Los primitivos que se presentan aquí —modelos inicializados de forma uniforme, cadenas LCEL composables, salidas estructuradas validadas con Pydantic y tool calling de bajo nivel— son el vocabulario mínimo sobre el que el Módulo 3 construirá agentes completos. La idea que conviene retener desde ahora es simple: en LangChain v1 todo componente, desde un prompt hasta un agente, implementa la misma interfaz Runnable, de modo que un agente no es más que un Runnable adicional que se puede invocar, ejecutar en lote y streamear como cualquier otro.
2.1 Arquitectura de paquetes v1 y el modelo mental Runnable
2.1.1 v1.0 (22-oct-2025): namespace reducido, langchain-classic como puente, promesa semver
LangChain 1.0 se anunció el 22 de octubre de 2025 como el primer major version del framework, con el compromiso explícito de no introducir breaking changes hasta la 2.0 (MDPI). El artefacto langchain==1.0.0 se había publicado en PyPI unos días antes, el 17 de octubre de 2025 (questdb.com). La release está designada como LTS: permanece en estado ACTIVE hasta la publicación de 2.0 y entra después en mantenimiento por al menos un año, mientras la serie 0.3 queda en mantenimiento hasta diciembre de 2026 (Github). La cadencia oficial es de minors cada uno o dos meses y patches semanales (Github).
A la fecha de referencia de este curso, las versiones verificadas contra la API JSON de PyPI son: langchain 1.3.14 (16-jul-2026), langchain-core 1.5.2 (28-jul-2026), langchain-openai 1.4.1 (23-jul-2026), langchain-anthropic 1.5.3 (28-jul-2026) y langchain-classic 1.0.8 (10-jun-2026) (questdb.com). Todos los paquetes v1 requieren Python 3.10 o superior (Github).
El cambio estructural de v1 es la reducción del namespace de langchain a bloques esenciales: langchain.agents (con create_agent), langchain.messages, langchain.tools, langchain.chat_models (con init_chat_model) y langchain.embeddings, en su mayoría re-exports de langchain-core (Github). El código legado —LLMChain, retrievers clásicos, la API de indexing, el hub y los re-exports de community— se trasladó al paquete langchain-classic, que se instala aparte y mantiene compatibilidad hacia atrás (MDPI). La migración de imports es mecánica: from langchain.chains import LLMChain pasa a from langchain_classic.chains import LLMChain (Github). En este curso langchain-classic se usa únicamente como referencia histórica; ningún ejemplo nuevo lo requiere.
Tabla 2.1 — Paquetes del ecosistema LangChain v1: rol, versión verificada y uso típico en el stack quant
| Paquete | Rol en v1 | Versión verificada (PyPI, jul-2026) | Uso típico en el stack quant |
|---|---|---|---|
langchain | Namespace esencial: agentes, modelos, herramientas, embeddings | 1.3.14 | Punto de entrada: init_chat_model, create_agent (M3) |
langchain-core | Abstracciones base: Runnable, mensajes, prompts, parsers, rate limiters | 1.5.2 | LCEL, with_structured_output, bind_tools, callbacks |
langchain-openai / langchain-anthropic | Partner packages por proveedor | 1.4.1 / 1.5.3 | Acceso a GPT y Claude con paridad de interfaz |
langchain-classic | Puente de compatibilidad con código legacy | 1.0.8 | Solo migración; prohibido en código nuevo del curso |
langchain-text-splitters | Chunking de documentos | serie 1.x | Preparación de filings y transcripts para RAG (M4) |
La tabla anterior resume la anatomía del ecosistema, pero tres observaciones prácticas importan más que la lista en sí. Primera: la separación entre langchain y langchain-core no es cosmética — las abstracciones que este módulo usa a diario (Runnable, ChatPromptTemplate, RunnableLambda, InMemoryRateLimiter) viven en langchain-core, mientras que langchain aporta las fachadas de conveniencia. Un requirements.txt institucional debe fijar ambos con pins estrictos, porque la cadencia de patches semanales hace que dos entornos instalados con una semana de diferencia no sean idénticos. Segunda: los partner packages versionan de forma independiente del core, lo que permite actualizar la integración de un proveedor sin tocar el resto del pipeline — una propiedad valiosa cuando un proveedor cambia su API a mitad de un ciclo de validación de modelos. Tercera: la existencia de langchain-classic como paquete separado convierte la deprecación en una decisión explícita de instalación; si un pipeline de research heredado aún importa LLMChain, la dependencia es visible en el lockfile y auditable, en lugar de estar escondida en un namespace monolítico. Para un risk manager, esa trazabilidad de dependencias es parte del inventario de modelos exigido por las prácticas de model risk management heredadas de SR 11-7, cuyos principios persisten aunque el marco haya sido rescindido.
2.1.2 init_chat_model: interfaz unificada, configuración en runtime, resiliencia integrada
init_chat_model es la puerta de entrada a los modelos en v1: una factoría que devuelve un BaseChatModel con la misma interfaz para más de ochenta proveedores, con sintaxis "provider:model" y kwargs que se pasan inline (alphanova.tech). El siguiente snippet inicializa un modelo primario con reintentos y un limitador de tasa explícito — dos controles que en un entorno de research masivo no son opcionales:
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
# Limitador thread-safe: máximo 2 requests/segundo, ráfaga de 10
rate_limiter = InMemoryRateLimiter(
requests_per_second=2.0, # presupuesto de llamadas al provider
check_every_n_seconds=0.1, # comprobación cada 100 ms
max_bucket_size=10, # tamaño máximo de ráfaga
)
model = init_chat_model(
"openai:gpt-5.5",
temperature=0, # clasificación financiera: determinismo
timeout=30,
max_tokens=1_000,
max_retries=6, # defecto; reintenta 429/5xx/red con backoff
rate_limiter=rate_limiter,
)
respuesta = model.invoke("Resume en una frase el riesgo principal de este titular: "
"ACME Corp recorta su guía de ingresos un 12%.")
print(respuesta.text) # en v1, .text es propiedad, no métodoLos chat models reintentan automáticamente con backoff exponencial hasta seis veces ante errores de red, límites de tasa (HTTP 429) y errores de servidor (5xx); los errores de cliente como 401 o 404 no se reintentan (alphanova.tech). La propiedad .text y el tipo de retorno AIMessage son parte de los breaking changes oficiales de v1 respecto a la serie 0.3 (Github).
El segundo patrón es el modelo configurable en runtime, directamente relevante para el routing de coste: un modelo ligero para el screening masivo de titulares y un modelo frontera para la redacción de la tesis, intercambiables sin recompilar la cadena (alphanova.tech):
from langchain.chat_models import init_chat_model
# Sin `model`: queda configurable en runtime vía config
router_model = init_chat_model(temperature=0, configurable_fields=("model", "model_provider"))
# Screening masivo: modelo barato
router_model.invoke(
"Clasifica el sentimiento de este titular: ACME eleva dividendo un 8%.",
config={"configurable": {"model": "gpt-5.4-mini"}},
)
# Tesis de inversión: modelo frontera, misma llamada, mismo código
router_model.invoke(
"Redacta la tesis de riesgo para ACME a 12 meses.",
config={"configurable": {"model": "claude-sonnet-4-6"}},
)Cuando una cadena contiene varios modelos, config_prefix evita colisiones de claves en el dict configurable (alphanova.tech). Finalmente, langchain-core 1.0 introdujo los standard content blocks: la propiedad .content_blocks de los mensajes expone con tipado uniforme texto, razonamiento, citas, tool calls (incluidos los server-side) y datos multimodales (imagen, audio, vídeo, PDF) con compatibilidad total hacia atrás (MDPI). Para el quant esto significa que un gráfico de velas adjunto como imagen, una transcripción de earnings call y el razonamiento del modelo se consumen con la misma interfaz independientemente del proveedor (PyQuant News).
2.2 LCEL: composición de pipelines financieros
2.2.1 El operador |: prompt | model | parser aplicado a un pipeline de análisis de noticias
LCEL (LangChain Expression Language) es el estilo de composición declarativa de langchain-core: los componentes se encadenan con el operador |, que construye una RunnableSequence donde la salida de cada eslabón es la entrada del siguiente (Source). La interfaz Runnable es el contrato universal del framework: cualquier componente —prompt, modelo, parser, función propia o grafo compilado— puede ser "invoked, batched, streamed, transformed and composed" (Source). Un dict literal dentro de una secuencia se coacciona automáticamente a RunnableParallel, que ejecuta las ramas concurrentemente sobre el mismo input y devuelve un dict de resultados (Source). La cadena compuesta hereda gratis ejecución síncrona, asíncrona, por lotes y streaming (Source).
El siguiente pipeline analiza una noticia corporativa y produce en paralelo dos vistas: una clasificación de sentimiento y una extracción de métricas mencionadas:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
prompt_sentimiento = ChatPromptTemplate.from_messages([
("system", "Eres un analista sell-side. Clasifica el sentimiento del titular "
"como Positive, Negative o Neutral. Responde solo con la etiqueta."),
("user", "Titular: {titular}"),
])
prompt_metricas = ChatPromptTemplate.from_messages([
("system", "Extrae en una línea cada cifra financiera mencionada en el titular, "
"con su unidad y escala. Si no hay ninguna, responde NO_DISPONIBLE."),
("user", "Titular: {titular}"),
])
# Rama paralela: mismo input, dos análisis concurrentes
pipeline_noticias = (
RunnablePassthrough()
| {
"sentimiento": prompt_sentimiento | model | StrOutputParser(),
"metricas": prompt_metricas | model | StrOutputParser(),
}
)
resultado = pipeline_noticias.invoke(
{"titular": "ACME Corp reporta ingresos de 1.234,56 millones USD (+9% interanual) "
"y eleva su guía de margen EBIT para 2027."}
)
# {'sentimiento': 'Positive', 'metricas': '1.234,56 millones USD (ingresos); +9% interanual'}Los cuatro primitivos de composición cubren el resto de necesidades estructurales. RunnablePassthrough transmite la entrada sin modificarla y, con .assign(), añade claves calculadas al dict que fluye por la cadena (NewYorkCityServers). RunnableLambda envuelve cualquier callable de Python como Runnable — la vía estándar para incrustar preprocesado determinista, como normalizar tickers o truncar texto (Source). RunnableParallel es el fan-out concurrente ya visto (Source). RunnableBranch implementa routing condicional como lista de pares condición→runnable más un default (Dividend Capture Pro), patrón útil para enrutar el análisis según el tipo de noticia (resultados, M&A, guidance) hacia prompts especializados. El siguiente diagrama resume el pipeline anterior:
2.2.2 invoke/ainvoke/batch/stream: streaming para dashboards, batch para screening masivo
La interfaz Runnable define una familia completa de métodos de ejecución; elegir el correcto es una decisión de ingeniería con impacto directo en latencia y coste (Source):
Tabla 2.2 — Métodos de ejecución de la interfaz Runnable: semántica, variante async y uso en trading cuantitativo
| Método | Semántica | Variante async | Uso en trading cuantitativo |
|---|---|---|---|
invoke(input, config) | Una entrada → una salida | ainvoke | Señal unitaria: clasificar el titular que acaba de llegar |
batch(inputs, config) | Lista de entradas → lista de salidas; paralelo con thread pool por defecto | abatch | Screening masivo: sentiment de 500 titulares pre-apertura |
batch_as_completed | Como batch, devuelve resultados según terminan | abatch_as_completed | Pipelines con latencias heterogéneas por proveedor |
stream(input) | Generador de chunks conforme se producen | astream | Dashboards de research: tokens en vivo de un informe |
astream_events | Stream de eventos intermedios de la cadena | nativo async | Trazabilidad paso a paso en depuración y auditoría |
map() | Aplica el runnable a cada elemento de una lista | — | Fan-out declarativo sobre una cartera |
La interpretación de esta tabla en contexto institucional pasa por tres puntos. Primero, batch no es un bucle for cosmético: por defecto ejecuta invoke() en paralelo sobre un thread pool executor, y admite max_concurrency vía config, de modo que el throughput de un screening de tickers queda acotado por el límite de tasa del proveedor y no por la latencia unitaria (Source). En términos formales, si cada llamada tarda segundos, el coste secuencial crece como , mientras que el lote con concurrencia escala como : para titulares y , la mejora es cercana a un orden de magnitud. Segundo, los métodos con prefijo a son asíncronos y, por defecto, ejecutan su contraparte síncrona en el thread pool de asyncio; las integraciones de chat models los sobreescriben con async nativo, lo que importa cuando el pipeline convive con un event loop de market data (Source). Tercero, el streaming no es un lujo de demo: en v1 los modos updates, messages y custom permiten emitir tokens, progreso por paso y datos arbitrarios hacia un dashboard de research, y desde la versión 1.3 la documentación recomienda el event streaming tipado como API preferida (Github). Si un componente no implementa streaming, la interfaz degrada con elegancia a invoke (Source).
El siguiente snippet muestra ambos patrones sobre el mismo clasificador de titulares:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("openai:gpt-5.4-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "Clasifica el sentimiento del titular: Positive, Negative o Neutral."),
("user", "{titular}"),
])
clasificador = prompt | model | StrOutputParser()
# 1) Screening masivo pre-apertura: batch paralelo sobre 200 tickers
titulares = [{"titular": f"Titular de mercado nº {i} sobre el ticker T{i:03d}"}
for i in range(200)]
etiquetas = clasificador.batch(
titulares,
config={"max_concurrency": 8, "tags": ["screening", "pre-market"],
"metadata": {"desk": "equity-research", "prompt_version": "v3"}},
)
# 2) Dashboard de research: streaming token a token de un informe largo
for chunk in clasificador.stream({"titular": "ACME eleva guidance y anuncia buyback."}):
print(chunk, end="", flush=True)El argumento config con tags y metadata no es decorativo: esas etiquetas viajan con cada ejecución y alimentan la trazabilidad en LangSmith, base de la observabilidad que el Módulo 10 desarrolla como defensa ante el vacío de gobernanza de la IA agéntica (Source).
max_concurrency=8), julio de 2026. La forma de las curvas es estructural; los valores absolutos dependen del proveedor y del modelo.La figura anterior cuantifica el punto con datos ilustrativos sintéticos: con una latencia media de 1,15 s por llamada, clasificar 200 titulares en secuencia costaría unos 230 s, frente a unos 31 s con batch y max_concurrency=8. Los números exactos dependen del proveedor y del modelo, pero la forma de las curvas —lineal frente a escalonada— es estructural, no empírica.
2.2.3 Robustez: with_retry con backoff, with_fallbacks entre providers, callbacks
Todo Runnable expone .with_retry(...), que devuelve un nuevo runnable que reintenta ante excepciones con política configurable, y .with_fallbacks([...]), que encadena alternativas de respaldo ante fallos (Source). Como los chat models ya reintentan internamente hasta seis veces los errores 429/5xx/red, with_retry se reserva para fallos de lógica de aplicación —por ejemplo, un parseo que no valida— mientras with_fallbacks resuelve la contingencia mayor: la caída completa de un proveedor (alphanova.tech). El patrón canónico institucional es primario OpenAI → respaldo Anthropic:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "Clasifica el sentimiento del titular: Positive, Negative o Neutral."),
("user", "{titular}"),
])
primario = init_chat_model("openai:gpt-5.5", temperature=0, timeout=30)
respaldo = init_chat_model("anthropic:claude-sonnet-4-6", temperature=0, timeout=30)
cadena_resiliente = (
prompt
| primario.with_fallbacks([respaldo]) # conmutación ante outage
| StrOutputParser().with_retry(stop_after_attempt=3) # lógica de aplicación
)
# Callbacks por llamada para observabilidad puntual
from langchain_core.tracers import ConsoleCallbackHandler
etiqueta = cadena_resiliente.invoke(
{"titular": "ACME suspende su dividendo trimestral citando preservación de liquidez."},
config={"callbacks": [ConsoleCallbackHandler()],
"tags": ["sentiment", "fallback-enabled"]},
)Tres detalles de producción completan el cuadro. Los fallbacks también están migrando a infraestructura: LangSmith ofrece un LLM Gateway (en beta privada a jul-2026) donde el orden de respaldo se define una vez y se aplica por código de error HTTP (429, 500, 502, 503, 504), sin tocar el código de cada pipeline (Quant Memo). Los callbacks de desarrollo (ConsoleCallbackHandler, set_debug(True)) son para depuración local; en producción la vía es el tracing centralizado (Source). Y la conmutación entre proveedores solo es segura si el contrato de salida es estable — razón de peso para la salida estructurada de la siguiente sección: un StrOutputParser que recibe texto libre de dos modelos distintos no ofrece ninguna garantía de paridad.
2.3 Salida estructurada y tool calling de bajo nivel
2.3.1 with_structured_output con Pydantic: el primer schema financiero, include_raw para auditoría
with_structured_output(schema) envuelve un chat model para que sus salidas casen con un esquema Pydantic, TypedDict o JSON schema, y devuelve instancias validadas en lugar de texto (wallstreetmojo.com). Internamente elige la estrategia según la capacidad del proveedor: si soporta tool calling o JSON schema nativo, el esquema viaja como definición de tool o response_format; si no, cae al patrón prompt+parser (Quant Memo). Un detalle de diseño con consecuencias: el nombre de la clase, el docstring y las descripciones de los campos se inyectan efectivamente en el prompt del modelo, así que diseñar el schema Pydantic es diseñar prompting (Quant Memo).
Este es el punto donde la regla del Módulo 1 se hace código. El clasificador de sentimiento institucional no devuelve una palabra suelta: devuelve un contrato tipado con score acotado y evidencia textual que un auditor puede verificar:
from typing import Literal, Optional
from pydantic import BaseModel, Field
from langchain.chat_models import init_chat_model
class SentimientoNoticia(BaseModel):
"""Clasificación de sentimiento de un titular financiero con evidencia auditable."""
etiqueta: Literal["positive", "negative", "neutral"] = Field(
description="Sentimiento del titular para el precio de la acción a 1-5 días")
score: float = Field(
ge=-1.0, le=1.0,
description="Intensidad del sentimiento normalizada en [-1, 1]; "
"0.0 solo si la etiqueta es neutral")
evidencia: str = Field(
description="Frase VERBATIM del titular que justifica la clasificación")
tickers_mencionados: list[str] = Field(
description="Tickers explícitos en el titular; lista vacía si no hay ninguno")
confianza: Optional[Literal["baja", "media", "alta"]] = Field(
default=None,
description="Auto-evaluación de confianza; null si el titular es ambiguo")
model = init_chat_model("openai:gpt-5.5", temperature=0)
# include_raw=True: salida dict con 'raw', 'parsed' y 'parsing_error'
extractor = model.with_structured_output(SentimientoNoticia, include_raw=True)
salida = extractor.invoke(
"Clasifica este titular: ACME Corp desploma un 18% tras revelarse "
"una investigación regulatoria sobre sus cuentas de 2025."
)
assert salida["parsing_error"] is None
analisis: SentimientoNoticia = salida["parsed"]
mensaje_crudo = salida["raw"] # AIMessage completo: trazabilidad total
print(analisis.etiqueta, analisis.score, analisis.evidencia)Con include_raw=True la salida es siempre un dict con claves raw (el AIMessage original, con usage_metadata y response_metadata), parsed (la instancia validada) y parsing_error (la excepción capturada, sin lanzarla) (wallstreetmojo.com). Para una mesa sujeta a requisitos de auditoría, persistir raw junto a parsed es la diferencia entre poder y no poder reconstruir qué dijo exactamente el modelo seis meses después. El schema también puede forzarse a modo estricto en OpenAI con method="json_schema", strict=True (manishgoelstocks.com), con la restricción documentada de que en modo estricto los campos no pueden llevar metadata de validación ni valores por defecto en algunos providers (wallstreetmojo.com). Existe además un bug abierto conocido (issue #38223, junio 2026): con la Responses API, streaming y esquemas Pydantic anidados, la ruta de streaming construye un JSON schema inválido donde la ruta no-streaming funciona — un recordatorio de que los edge cases de salida estructurada siguen activos (ryanoconnellfinance.com).
2.3.2 bind_tools y el bucle de tool calling manual: el LLM como router de herramientas deterministas
bind_tools es el método de bajo nivel de BaseChatModel para adjuntar herramientas —funciones, clases Pydantic, dicts o BaseTool— y devuelve un Runnable cuya salida es un AIMessage con la lista tool_calls (arXiv.org). El contrato es la segunda mitad de la regla del curso: el LLM propone, el código dispone. El modelo nunca ejecuta la herramienta; solo emite dicts {'name', 'args', 'id', 'type': 'tool_call'} que la aplicación valida y ejecuta (alphanova.tech). En un sistema de trading esto no es un matiz técnico: es la frontera entre el componente no determinista (razonamiento) y los componentes deterministas y auditables (cálculo, datos, ejecución):
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
@tool
def get_market_data(ticker: str, field: str) -> dict:
"""Devuelve un dato de mercado point-in-time (precio, volumen, beta)
para un ticker. Fuente determinista: nunca la inventa el LLM."""
datos = {"ACME": {"precio": 184.32, "beta": 1.21, "volumen_medio": 8_400_000}}
return {"ticker": ticker, "field": field, "valor": datos.get(ticker, {}).get(field)}
@tool
def calculator(expression: str) -> float:
"""Única vía autorizada para aritmética. El LLM no calcula en texto."""
import ast, operator as op
OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.USub: op.neg}
def ev(n):
if isinstance(n, ast.Constant): return n.value
if isinstance(n, ast.BinOp): return OPS[type(n.op)](ev(n.left), ev(n.right))
if isinstance(n, ast.UnaryOp): return OPS[type(n.op)](ev(n.operand))
raise ValueError("expresión no permitida")
return ev(ast.parse(expression, mode="eval").body)
tools = {t.name: t for t in [get_market_data, calculator]}
model = init_chat_model("openai:gpt-5.5", temperature=0).bind_tools(list(tools.values()))
# Bucle manual de tool calling: el LLM enruta, el código ejecuta
historial = [HumanMessage(
"Con el precio y la beta de ACME, calcula su coste de equity aproximado "
"si rf = 4% y la prima de mercado es 5% (CAPM: rf + beta * prima).")]
respuesta = model.invoke(historial)
historial.append(respuesta)
while respuesta.tool_calls: # ¿el modelo propone herramientas?
for call in respuesta.tool_calls:
resultado = tools[call["name"]].invoke(call["args"]) # ejecuta el CÓDIGO
historial.append(ToolMessage(content=str(resultado), tool_call_id=call["id"]))
respuesta = model.invoke(historial) # nueva ronda con resultados
historial.append(respuesta)
print(respuesta.text) # respuesta final: 4% + 1.21 * 5% = 10.05% (calculado por la tool)Tres observaciones cierran la sección. Primera, tool_choice="any" fuerza al modelo a llamar a una herramienta — patrón que la propia documentación usa para clasificación estructurada con guardrails (Github). Segunda, en streaming los tool calls llegan como tool_call_chunk con JSON parcial que hay que agregar antes de ejecutar nada; ejecutar argumentos a medio parsear es un fallo de seguridad operativa (Github). Tercera, este bucle manual de treinta líneas es, en esencia, lo que create_agent encapsula con estado, persistencia y middleware; el Módulo 3 lo formalizará, pero conviene llegar sabiendo que un agente no añade magia sino orquestación sobre este mismo contrato — y que el agente resultante sigue siendo un Runnable componible.
2.4 Cuándo NO usar LangChain
2.4.1 Críticas documentadas, comparación con llamadas directas y el patrón híbrido dominante
La crítica canónica es el post de Octomind (junio 2024), que tras doce meses en producción abandonó LangChain porque sus abstracciones de alto nivel encarecían entender y mantener el código; alcanzó 480 puntos en Hacker News y provocó respuesta directa del CEO de LangChain (lilys.ai). Su ejemplo icónico: traducir un texto es una llamada directa a la API, pero en LangChain clásico exigía cuatro abstracciones (prompt template, chat model, output parser, chain) (arXiv.org). Anthropic, en su guía de ingeniería de diciembre de 2024, formula la versión positiva del mismo consejo: las implementaciones con más éxito se construyen con "simple, composable patterns" en lugar de frameworks complejos, empezando con llamadas directas y añadiendo capas solo cuando demuestran mejorar resultados — aunque la guía matiza que los frameworks sirven para empezar rápido (arXiv.org). Críticas posteriores añaden costes ocultos por llamadas implícitas al LLM y stack traces de cinco o más capas de abstracción (arXiv.org).
El consenso de 2026 es matizado y práctico: para una sola llamada o un pipeline lineal trivial, el SDK directo del proveedor más Pydantic es más legible y sin riesgo de versionado; LangChain se justifica cuando hay orquestación multi-modelo, intercambio de proveedores con una línea, agentes con estado o necesidad de observabilidad integrada (arXiv.org). La propia trayectoria del framework —del namespace monolítico al paquete adelgazado de v1, con el legado exiliado a langchain-classic— es una respuesta directa a esas críticas, reconocida en el anuncio oficial (MDPI). En retrieval documental, el patrón dominante combina frameworks: LlamaIndex como capa de retrieval envuelta como herramienta dentro de la orquestación de LangChain (arXiv.org). Y la adopción es real: los cinco frameworks principales concentran más del 93% de las descargas del segmento, con LangChain en torno a 233M descargas mensuales (QASkills.sh), y el 57,3% de las organizaciones encuestadas en el State of Agent Engineering 2026 declara agentes en producción — 67% en empresas de más de 10.000 empleados — con telemetría independiente de Datadog mostrando la adopción de frameworks de agentes casi duplicada interanualmente (AlphaCreek).
La guía de decisión para el quant se resume así: SDK directo para lo trivial; LCEL corto para pipelines de análisis repetibles sobre muchos activos; salida estructurada y tool calling de langchain-core como contrato estable entre proveedores; agentes (Módulo 3) solo donde exista un bucle genuino herramienta-decisión. Sobredimensionar — meter un agente donde basta un batch — es el error de arquitectura más caro y más común.
Ejercicios
-
Verificación de versiones. Escribe un script que consulte la API JSON de PyPI (
https://pypi.org/pypi/langchain/json) y compruebe que las versiones instaladas delangchain,langchain-corey un partner package coinciden con los pins derequirements.txt. Debe fallar con exit code distinto de cero si hay deriva de versión. -
Modelo configurable. Construye con
init_chat_modelyconfigurable_fieldsun clasificador de titulares que enrute a un modelo ligero o frontera según el valor deconfig["configurable"]. Mide contime.perf_counterla latencia de diez invocaciones en cada configuración y tabula el coste relativo asumiendo precios de 0,40 USD y 5,00 USD por millón de tokens de entrada (ilustrativos). -
Pipeline LCEL paralelo. Extiende el pipeline de noticias de 2.2.1 con una tercera rama que extraiga los tickers mencionados y una
RunnableLambdaposterior que normalice el dict de salida a minúsculas y añada un campoas_ofcon la fecha de corte. Ejecuta el resultado con.batchsobre una lista de 20 titulares sintéticos ymax_concurrency=4. -
Robustez. Envuelve el pipeline del ejercicio 3 con
with_fallbackshacia un segundo proveedor ywith_retry(stop_after_attempt=3)en el parser. Simula el fallo del primario con unRunnableLambdaque lanceConnectionErrory verifica que la cadena conmuta al respaldo sin excepción. -
Clasificador auditable. Implementa el schema
SentimientoNoticiade 2.3.1 coninclude_raw=True, añade una verificación de dos pasos en código —la cadenaevidenciadebe aparecer literalmente en el titular de entrada— y persiste en un JSONL cada registro conparsed,raw.usage_metadatay un hash del prompt. Clasifica 30 titulares y reporta la fracción que supera la verificación. -
Router de herramientas. Implementa el bucle manual de
bind_toolsde 2.3.2 con tres herramientas (get_market_data,calculator,xbrl_lookupsimulada) y un límite de cinco rondas. Demuestra con un caso que el modelo encadena dos tool calls (lookup → cálculo) y que ninguna aritmética aparece en el texto libre del modelo.
Módulo 2 de 15 — LangChain para Trading Cuantitativo. Datos y versiones verificados a julio de 2026; precios y cifras de mercado sujetos a cambio. LangChain 1.3.14 / langchain-core 1.5.2.
// check
Comprueba lo aprendido
Autoevaluación con respuesta inmediata. No se guarda ninguna puntuación.
Ver respuesta
B. La sección 2.1.2 lo explicita: los chat models reintentan con backoff ante errores de red, límites de tasa (429) y errores de servidor (5xx); los errores de cliente como 401 o 404 no se reintentan. Un 401 indica credenciales inválidas: reintentar quemaría presupuesto sin arreglar nada, y la conmutación a respaldo exige with_fallbacks explícito.
Ver respuesta
B. Un dict literal dentro de una secuencia LCEL se coacciona a RunnableParallel: fan-out concurrente sobre el mismo input que devuelve un dict de resultados (sección 2.2.1). Las dependencias entre pasos se expresan con composición secuencial o RunnablePassthrough.assign(), nunca dentro del dict paralelo.
Ver respuesta
B. batch paraleliza con un thread pool, pero el throughput queda acotado por el presupuesto de llamadas del proveedor: a 2 req/s, 500 llamadas exigen al menos 250 s. La fórmula T_batch ≈ ⌈N/c⌉·t del módulo asume margen en ese presupuesto; la concurrencia útil es c* ≈ req/s × latencia ≈ 2,4.
Ver respuesta
B. Los chat models ya reintentan internamente los fallos transitorios (429/5xx/red), así que with_retry se reserva para la lógica de aplicación y with_fallbacks resuelve la contingencia mayor: el outage del proveedor. Y la conmutación solo es segura con un contrato de salida estable entre ambos, razón de peso para la salida estructurada.
Ver respuesta
B. Con include_raw=True la salida es un dict con raw (el AIMessage completo), parsed (la instancia validada) y parsing_error (2.3.1). Persistir raw junto a parsed es la diferencia entre poder y no poder reconstruir la respuesta exacta del modelo seis meses después ante un auditor.
Ver respuesta
B. La sección 2.3.1 lo advierte como detalle de diseño con consecuencias: el nombre de la clase, el docstring y las descripciones de los campos viajan al prompt que recibe el modelo. Escribir «Frase VERBATIM del titular» en la descripción de evidencia es una instrucción operativa, no un comentario.
Ver respuesta
C. La decodificación restringida garantiza la forma, no la verdad: un campo bien tipado puede contener un valor inventado (el fallo «confidently wrong»). La mitigación es de diseño de schema —null legítimo, evidencia verbatim obligatoria— más verificación de dos pasos en código (2.3.1). JSON mode, además, tiene peor cumplimiento documentado.
Ver respuesta
B. Si las entradas se pueden enumerar antes de ejecutar, el patrón es batch/map: throughput gobernado por max_concurrency y el limitador. Sobredimensionar —meter un agente donde basta un batch— es, en palabras del módulo, el error de arquitectura más caro y más común; los agentes (Módulo 3) se reservan para bucles genuinos herramienta-decisión.