MÓDULO 4 · LANGCHAIN FUNDAMENTOS · 30 MIN

Componer cadenas: el diccionario que crece

Encadenar dos LLM con | a secas pierde información: el segundo prompt solo recibe el string que escupió el primero. RunnablePassthrough.assign(clave=subcadena) arregla eso ejecutando la subcadena y añadiendo su resultado al diccionario que ya viaja por la tubería. Lo que fluye no es un texto: es un dict que engorda en cada paso.
01 — El problema
Por qué | a secas no basta

Si haces cadena_a | cadena_b, la salida de cadena_a es un str y es todo lo que cadena_b va a ver. La pregunta original del usuario ya no está. Cualquier prompt posterior que necesite dos variables a la vez se queda sin una.

El caso típico: generas SQL a partir de una pregunta, ejecutas el SQL, y para redactar la respuesta final necesitas las tres cosas —pregunta, consulta y resultado— en el mismo prompt.

02 — La pieza
assign: añadir sin perder
full_chain = (
    RunnablePassthrough.assign(query=sql_chain)
    | RunnablePassthrough.assign(result=lambda x: run_query(x["query"]))
    | prompt_final
    | llm
    | StrOutputParser()
)
Claves disponibles en cada punto de la tubería
entrada question
assign(query=sql_chain) questionquery
assign(result=lambda …) questionqueryresult
prompt_final questionqueryresult

prompt_final puede pedir las tres porque las tres siguen ahí. Nada se ha sobrescrito: assign solo agrega. Dos detalles que se olvidan:

// si dudas de qué hay dentro en un punto, mete un assign que solo imprima y devuelva el dict
03 — La puerta de escape
Meter Python en medio

El paso result=lambda x: run_query(x["query"]) es la vía por la que el código imperativo entra en una cadena declarativa. LangChain lo envuelve en un RunnableLambda por ti. Ahí es donde van las llamadas a base de datos, las validaciones y el formateo de documentos.

Y es también donde conviene poner el try/except: si esa función lanza, se lleva la cadena entera por delante.

Auditoría El script de Text-to-SQL de la guía tiene un fallo reproducible. Ejecuté su bloque de creación de base de datos dos veces seguidas:
run 1: OK
run 2: OperationalError: table ventas already exists
La guía anuncia «crearemos una DB temporal en memoria», pero el código hace sqlite3.connect("ventas.db"), que es un fichero en disco: la segunda ejecución revienta en el CREATE TABLE. Con CREATE TABLE IF NOT EXISTS deja de reventar, pero entonces los INSERT se repiten y los totales salen multiplicados. (El total que da la guía, 6702.5, sí es correcto en la primera ejecución: lo comprobé contra SQLite.)

Y la pregunta que queda abierta: run_query limpia el SQL del modelo con query.replace("```sql", "").replace("```", ""). ¿Qué le pasa a ese parche si el modelo devuelve el bloque con otra etiqueta, o añade una frase antes del código? ¿Y por qué la guía te propone ejecutar SQL generado por un LLM contra una conexión con permisos de escritura?
Para hacer
Mira el diccionario con tus ojos

Coge una cadena de dos pasos y mete un RunnablePassthrough.assign que no llame a ningún LLM: por ejemplo assign(longitud=lambda x: len(x["question"])). Invócala e imprime el diccionario completo del resultado. Confirma qué claves sobreviven al final y en qué orden aparecieron.