Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/essays/2026-W33-grafo-vs-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Por qué un grafo que no aporta nada hoy es la decisión correcta de hoy

Cuando construímos soluciones dentro de ML es bastante común que como científico de datos tendamos a crear procesos síncronos, lineas y continuos de manera que cada paso está completamente determinado y el proceso fluye desde el punto A hasta el punto B en una serie de sucesos predecibles y replicables.

Ahora bien, en la transición a AI engineer me he encontrado con la necesidad de pensar en sistemas que se retroalimenten, que tengan la capacidad de volver sobre sí mismos, "repensarse" y luego seleccionar otros caminos si es el caso, además de tener que manejar sistemas con muchas bifurcaciones y caminos posibles. Así pues, surge como herramienta langGraph y en particular la filosofía de grafos: Aplicado al desarrollo que tenemos, un grafo permite estructuralmente volver hacia atrás si es necesario y además bifurcarse, de manera que el sistema se retro-alimenta y se mueve en flujos que no podrían ser posibles con una cadena de funciones ya que como lo mencioné arriba, éstas son secuenciales.

Este hecho lleva a cuestionar por qué es necesario en este punto integrar LangGraph cuando mi pipeline actualmnete solo tiene 2 pasos "recuperar->generate", pues la realidad es que ahora mismo un grafo no aporta mucho valor ya que el sistema es secuencial, sin embargo, previendo que en próximas tareas quiero que haya retroalimentación y que además espero bifurcar decisiones entonces introducir un grafo en este momento me ayuda a que la construcción de la V2 y futuras versiones esté sobre esta filosofía. Sí podría continuar como lo tengo ahora pero más adelante tendría que hacer mayor refactorización para alcanzar las funcionalidades que espero que el sistema tenga.

Una ventaja adicional es que LangGraph al ser una librería de bajo nivel tiene implementaciones que permiten conocer el estado explícito del sistema, si mantuviera el pipeline entonces sí podría inspeccionar el estado pero tendría que implementar manualmente estrategias para conocer esos estados, lo cual en temas de observabilidad da una ventaja importante con langGraph.

Dicho esto, no es que no pueda continuar con un pipeline secuencia, sino que hacerlo ahora mismo es una decisión estratégica de cara a la evolución de funcionalidades que espero alcanzar con mi sistema, por lo que esta sobre-ingeniería tendrá sus beneficios en el futuro.
39 changes: 39 additions & 0 deletions docs/essays/prompts/2026-W33.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Tema propuesto — Semana 2026-W33

## Título sugerido
Por qué un grafo que no aporta nada hoy es la decisión correcta de hoy

## Por qué este tema
Esta semana arrancaste LangGraph y tu primera respuesta a "¿qué gana un grafo
sobre tu pipeline `retrieve → generate`?" se quedó corta: te centraste en el
estado explícito y dejaste afuera las dos razones de fondo (bifurcaciones
condicionales y ciclos), y además afirmaste algo técnicamente falso ("no puedo
conocer el estado intermedio en un pipeline lineal") — ambos corregidos en
`learnings.md` del 13/08 y ahora en el banco como LG-002. Es el primer ciclo
de ensayos del proyecto (0 ejecutados hasta hoy según el diagnóstico del
10/08), y este es el hueco más caro de dejar sin consolidar: T18 arranca el
lunes 17/08 y si la justificación no está firme, vas a construir el grafo sin
poder defender por qué, ni en el código ni en una entrevista.

## Preguntas guía para arrancar la escritura
- ¿Qué puede hacer un grafo que una cadena de funciones no puede hacer
*estructuralmente*, no solo "más cómodamente"?
- Si tu pipeline de dos pasos no tiene ramas ni ciclos hoy, ¿dónde vive
exactamente el beneficio de montar el grafo en T18 en vez de esperar a T19?
- ¿Por qué "no puedo inspeccionar el estado intermedio de un pipeline lineal"
es una afirmación falsa, y cuál es la diferencia real que sí importa?
- Si tuvieras que defender esta decisión ante alguien que dice "eso es
sobre-ingeniería para dos pasos", ¿qué le respondés?

## Longitud
400–500 palabras. Escribí sin abrir código ni `learnings.md`. Después de
escribir, compará contra la entrada del 13/08 y anotá qué no recordaste sin
mirar.

## Alternativas si este no resuena
1. **De Protocol a alias de función: cuándo un contrato pesado sobra** — sobre
`AnswerFn`/`RetrieveFn` (ADR-004) y el criterio de "un comportamiento →
función, varios comportamientos con estado → Protocol".
2. **Mockear la dependencia no es lo mismo que un test hermético** — sobre el
fix real de T14: `httpx` mockeado no evitó que `_download_pdf` escribiera
al `data/papers/` real del repo.
121 changes: 115 additions & 6 deletions docs/interview_prep/bank.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ proyecto. Meta: 80–100 preguntas al final de V5.
## Índice por tema

- [Clean Architecture (CA)](#clean-architecture) — 5 preguntas
- [Protocols (PR)](#protocols) — 3 preguntas
- [Protocols (PR)](#protocols) — 4 preguntas
- [RAG y retrieval (RG)](#rag-y-retrieval) — 4 preguntas
- [Async y concurrencia (AS)](#async-y-concurrencia) — 3 preguntas
- [Testing (TS)](#testing) — 0 preguntas
- [LangGraph y agentes (LG)](#langgraph-y-agentes) — 0 preguntas
- [LangGraph y agentes (LG)](#langgraph-y-agentes) — 3 preguntas
- [Observabilidad y evals (OB)](#observabilidad-y-evals) — 0 preguntas
- [Guardrails y seguridad (GR)](#guardrails-y-seguridad) — 0 preguntas
- [LLM providers y SDKs (LM)](#llm-providers-y-sdks) — 0 preguntas
Expand Down Expand Up @@ -189,9 +189,10 @@ su código.
"rigor" adicional de ABC (enforcement al instanciar) es innecesario si
usás mypy en CI — y a cambio pagás con acoplamiento por herencia.

**Ejemplo en el proyecto:** `application/services/rag_service.py` recibe
`llm: LLMProvider` y `store: VectorStore` — no sabe si son AnthropicLLM,
GeminiLLM, ChromaVectorStore o mocks; solo sabe qué métodos puede llamar.
**Ejemplo en el proyecto:** `application/services/ingestion_service.py`
recibe `store: VectorStore | None` — no sabe si es `ChromaVectorStore` o un
mock; solo sabe qué métodos puede llamar. (`rag_service.answer_query` ya no
recibe `store` directamente — inyecta `retrieve: RetrieveFn`, ver PR-004.)

---

Expand All @@ -217,6 +218,32 @@ mocks de LLMProvider usados en `tests/unit/application/`.

---

#### [PR-004] Nivel: intermedio
**Pregunta:** `rag_service.answer_query` recibe `retrieve: RetrieveFn`, un
alias `Callable[[str], Awaitable[list[Document]]]`, en vez de un Protocol.
¿Por qué no un Protocol acá, si ya se usa `VectorStore` y `LLMProvider` en
el resto del proyecto?

**Respuesta esperada:** Un Protocol tiene sentido cuando hay varios métodos
relacionados que comparten estado (`VectorStore` con `search` + `upsert`).
Acá la dependencia es un solo comportamiento anónimo — un parámetro, un
retorno — y envolver eso en una clase con un único método es ceremonia sin
beneficio. El alias de función sigue siendo estáticamente verificable
(mypy valida la firma) pero es más liviano. Regla: un comportamiento →
alias de función; varios comportamientos relacionados → Protocol o clase.

**Trampa común:** Pensar que "más formal siempre es mejor" y usar Protocol
por defecto. La complejidad debe ser proporcional al número de
comportamientos que la dependencia agrupa, no una preferencia estilística
fija.

**Ejemplo en el proyecto:**
`src/researchos/application/services/rag_service.py:8` —
`RetrieveFn = Callable[[str], Awaitable[list[Document]]]`; documentado en
ADR-004 de `docs/architecture.md`.

---

## RAG y retrieval

#### [RG-001] Nivel: básico
Expand Down Expand Up @@ -448,7 +475,89 @@ _(sin preguntas todavía)_

## LangGraph y agentes

_(sin preguntas todavía)_
#### [LG-001] Nivel: básico
**Pregunta:** ¿Cuál es la diferencia entre LangChain y LangGraph, y por qué
`create_agent` de LangChain se apoya en LangGraph por debajo?

**Respuesta esperada:** LangGraph es orquestación de bajo nivel — control
de flujo explícito, human-in-the-loop, ejecución duradera y persistencia
de estado. LangChain es construcción de agentes de alto nivel, con modelos
y herramientas ya integrados en abstracciones como `create_agent`. Desde
la migración de LangGraph 0.x a 1.x (que deprecó `create_react_agent`),
`create_agent` de LangChain usa LangGraph internamente para ese control de
flujo — LangChain no reemplaza a LangGraph, se apoya en él.

**Trampa común:** Tratarlos como alternativas competidoras ("¿uso
LangChain o LangGraph?") en vez de verlos como capas — LangGraph es la
base de orquestación, LangChain es la abstracción de más alto nivel
construida encima.

**Ejemplo en el proyecto:** `notebooks/201-jmmz-langraph-study.ipynb`;
versiones fijadas en `pyproject.toml`
(`langgraph>=1.2.11`, `langchain>=1.3.15`, `langchain-anthropic>=1.5.6`).

---

#### [LG-002] Nivel: intermedio
**Pregunta:** Tu pipeline de V1 es dos pasos lineales
(`retrieve → generate`). ¿Qué gana un grafo sobre eso, y por qué vale la
pena montarlo en T18 si hoy, aislado, no aporta ningún beneficio?

**Respuesta esperada:** El estado explícito es el mecanismo, no el fin —
permite leer el estado de un nodo no adyacente y persistir para retomar
tras un fallo — pero la razón de fondo es que el grafo habilita
bifurcaciones condicionales (T19: ir a una tool si la recuperación local
es pobre) y ciclos (T22: recuperar, evaluar, reescribir query, recuperar
de nuevo), algo que una cadena de funciones no puede hacer porque va en
una sola dirección. Para dos pasos sin ramas ni ciclos, un grafo es
sobrecosto puro: la justificación de T18 no está en T18 mismo, sino en que
T19 y T22 no se pueden construir sin el grafo ya montado — es
infraestructura que se paga por adelantado.

**Trampa común:** (a) Decir que un pipeline lineal "no permite conocer el
estado intermedio" — sí permite, con logging manual; la diferencia real es
que el grafo lo da por construcción, no por instrumentación. (b)
Justificar T18 por "mejor arquitectura" en vez de por secuenciación, sin
reconocer que hoy es sobrecosto sin beneficio inmediato.

**Ejemplo en el proyecto:**
`src/researchos/application/services/rag_service.py` (`answer_query`, el
pipeline lineal candidato a convertirse en el grafo); `ROADMAP.md` muestra
T19 y T22 dependiendo de T18.

---

#### [LG-003] Nivel: avanzado
**Pregunta:** En LangGraph, un nodo de clasificación puede enrutar con una
conditional edge (el nodo devuelve `dict`, un router aparte decide el
destino) o devolviendo `Command(goto=...)` (el nodo decide y enruta en un
solo retorno). Para tu T19 (decidir si ir a una tool de arXiv o responder
directo), ¿cuál usarías y por qué?

**Respuesta esperada:** Conditional edge. El criterio real no es cuál es
más simple, sino si el nodo *calcula* algo que sirve solo para decidir o si
esa información ya se necesita en el estado de todas formas. En T19 los
documentos recuperados van al estado igual — el nodo de generación los
necesita — así que no hay campo transitorio que `Command` evitaría
persistir. Con conditional edge el router queda como función pura,
testeable con estados fabricados sin ejecutar el nodo completo ni golpear
el LLM, y la estructura de ruteo queda declarada en `add_conditional_edges`,
visible en el builder.

**Trampa común:** Pensar que `Command` es "la forma moderna" y usarla por
defecto. `Command` es obligatorio en un caso real y puntual: enrutar desde
un subgrafo hacia el grafo padre (`Command(graph=Command.PARENT)`), porque
las edges no cruzan fronteras de subgrafo — eso es multi-agente (V6), no
T19. Fuera de ese caso, la pregunta correcta es si el nodo calcula algo
transitorio que solo sirve para decidir (ahí `Command` evita ensuciar el
esquema de estado) o si el nodo solo decide sobre información que ya está
en el estado (ahí conditional edge).

**Ejemplo en el proyecto:** Aplica directamente al criterio de aceptación
de T19 ([#7](https://github.com/johnma96/researchos/issues/7)): "test del
grafo verificando que la conditional edge enruta correctamente en ambos
casos" ya asume el patrón de router-como-función-pura que impone un
conditional edge, no `Command`.

## Observabilidad y evals

Expand Down
87 changes: 87 additions & 0 deletions docs/interview_prep/by_topic/langgraph_agentes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# LangGraph y agentes — banco de preguntas

3 preguntas. Fuente: `docs/interview_prep/bank.md`.

#### [LG-001] Nivel: básico
**Pregunta:** ¿Cuál es la diferencia entre LangChain y LangGraph, y por qué
`create_agent` de LangChain se apoya en LangGraph por debajo?

**Respuesta esperada:** LangGraph es orquestación de bajo nivel — control
de flujo explícito, human-in-the-loop, ejecución duradera y persistencia
de estado. LangChain es construcción de agentes de alto nivel, con modelos
y herramientas ya integrados en abstracciones como `create_agent`. Desde
la migración de LangGraph 0.x a 1.x (que deprecó `create_react_agent`),
`create_agent` de LangChain usa LangGraph internamente para ese control de
flujo — LangChain no reemplaza a LangGraph, se apoya en él.

**Trampa común:** Tratarlos como alternativas competidoras ("¿uso
LangChain o LangGraph?") en vez de verlos como capas — LangGraph es la
base de orquestación, LangChain es la abstracción de más alto nivel
construida encima.

**Ejemplo en el proyecto:** `notebooks/201-jmmz-langraph-study.ipynb`;
versiones fijadas en `pyproject.toml`
(`langgraph>=1.2.11`, `langchain>=1.3.15`, `langchain-anthropic>=1.5.6`).

---

#### [LG-002] Nivel: intermedio
**Pregunta:** Tu pipeline de V1 es dos pasos lineales
(`retrieve → generate`). ¿Qué gana un grafo sobre eso, y por qué vale la
pena montarlo en T18 si hoy, aislado, no aporta ningún beneficio?

**Respuesta esperada:** El estado explícito es el mecanismo, no el fin —
permite leer el estado de un nodo no adyacente y persistir para retomar
tras un fallo — pero la razón de fondo es que el grafo habilita
bifurcaciones condicionales (T19: ir a una tool si la recuperación local
es pobre) y ciclos (T22: recuperar, evaluar, reescribir query, recuperar
de nuevo), algo que una cadena de funciones no puede hacer porque va en
una sola dirección. Para dos pasos sin ramas ni ciclos, un grafo es
sobrecosto puro: la justificación de T18 no está en T18 mismo, sino en que
T19 y T22 no se pueden construir sin el grafo ya montado — es
infraestructura que se paga por adelantado.

**Trampa común:** (a) Decir que un pipeline lineal "no permite conocer el
estado intermedio" — sí permite, con logging manual; la diferencia real es
que el grafo lo da por construcción, no por instrumentación. (b)
Justificar T18 por "mejor arquitectura" en vez de por secuenciación, sin
reconocer que hoy es sobrecosto sin beneficio inmediato.

**Ejemplo en el proyecto:**
`src/researchos/application/services/rag_service.py` (`answer_query`, el
pipeline lineal candidato a convertirse en el grafo); `ROADMAP.md` muestra
T19 y T22 dependiendo de T18.

---

#### [LG-003] Nivel: avanzado
**Pregunta:** En LangGraph, un nodo de clasificación puede enrutar con una
conditional edge (el nodo devuelve `dict`, un router aparte decide el
destino) o devolviendo `Command(goto=...)` (el nodo decide y enruta en un
solo retorno). Para tu T19 (decidir si ir a una tool de arXiv o responder
directo), ¿cuál usarías y por qué?

**Respuesta esperada:** Conditional edge. El criterio real no es cuál es
más simple, sino si el nodo *calcula* algo que sirve solo para decidir o si
esa información ya se necesita en el estado de todas formas. En T19 los
documentos recuperados van al estado igual — el nodo de generación los
necesita — así que no hay campo transitorio que `Command` evitaría
persistir. Con conditional edge el router queda como función pura,
testeable con estados fabricados sin ejecutar el nodo completo ni golpear
el LLM, y la estructura de ruteo queda declarada en `add_conditional_edges`,
visible en el builder.

**Trampa común:** Pensar que `Command` es "la forma moderna" y usarla por
defecto. `Command` es obligatorio en un caso real y puntual: enrutar desde
un subgrafo hacia el grafo padre (`Command(graph=Command.PARENT)`), porque
las edges no cruzan fronteras de subgrafo — eso es multi-agente (V6), no
T19. Fuera de ese caso, la pregunta correcta es si el nodo calcula algo
transitorio que solo sirve para decidir (ahí `Command` evita ensuciar el
esquema de estado) o si el nodo solo decide sobre información que ya está
en el estado (ahí conditional edge).

**Ejemplo en el proyecto:** Aplica directamente al criterio de aceptación
de T19 ([#7](https://github.com/johnma96/researchos/issues/7)): "test del
grafo verificando que la conditional edge enruta correctamente en ambos
casos" ya asume el patrón de router-como-función-pura que impone un
conditional edge, no `Command`.
35 changes: 31 additions & 4 deletions docs/interview_prep/by_topic/protocols.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Protocols — banco de preguntas

3 preguntas. Fuente: `docs/interview_prep/bank.md`.
4 preguntas. Fuente: `docs/interview_prep/bank.md`.

#### [PR-001] Nivel: básico
**Pregunta:** ¿Qué es un `Protocol` de Python y en qué se diferencia de
Expand Down Expand Up @@ -41,9 +41,10 @@ su código.
"rigor" adicional de ABC (enforcement al instanciar) es innecesario si
usás mypy en CI — y a cambio pagás con acoplamiento por herencia.

**Ejemplo en el proyecto:** `application/services/rag_service.py` recibe
`llm: LLMProvider` y `store: VectorStore` — no sabe si son AnthropicLLM,
GeminiLLM, ChromaVectorStore o mocks; solo sabe qué métodos puede llamar.
**Ejemplo en el proyecto:** `application/services/ingestion_service.py`
recibe `store: VectorStore | None` — no sabe si es `ChromaVectorStore` o un
mock; solo sabe qué métodos puede llamar. (`rag_service.answer_query` ya no
recibe `store` directamente — inyecta `retrieve: RetrieveFn`, ver PR-004.)

---

Expand All @@ -66,3 +67,29 @@ señal de mypy sobre si tu test está usando el Protocol correctamente.

**Ejemplo en el proyecto:** `tests/conftest.py` tiene `MockVectorStore` y
mocks de LLMProvider usados en `tests/unit/application/`.

---

#### [PR-004] Nivel: intermedio
**Pregunta:** `rag_service.answer_query` recibe `retrieve: RetrieveFn`, un
alias `Callable[[str], Awaitable[list[Document]]]`, en vez de un Protocol.
¿Por qué no un Protocol acá, si ya se usa `VectorStore` y `LLMProvider` en
el resto del proyecto?

**Respuesta esperada:** Un Protocol tiene sentido cuando hay varios métodos
relacionados que comparten estado (`VectorStore` con `search` + `upsert`).
Acá la dependencia es un solo comportamiento anónimo — un parámetro, un
retorno — y envolver eso en una clase con un único método es ceremonia sin
beneficio. El alias de función sigue siendo estáticamente verificable
(mypy valida la firma) pero es más liviano. Regla: un comportamiento →
alias de función; varios comportamientos relacionados → Protocol o clase.

**Trampa común:** Pensar que "más formal siempre es mejor" y usar Protocol
por defecto. La complejidad debe ser proporcional al número de
comportamientos que la dependencia agrupa, no una preferencia estilística
fija.

**Ejemplo en el proyecto:**
`src/researchos/application/services/rag_service.py:8` —
`RetrieveFn = Callable[[str], Awaitable[list[Document]]]`; documentado en
ADR-004 de `docs/architecture.md`.
Loading
Loading