MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Cuando le dices a tu LLM "No pulses ese botón"

2026-01-12 08:00:00

Era una de esas sesiones de afinamiento que empiezan con la confianza del que cree dominar la herramienta. Estaba frente a Gemini, ajustando un protocolo, y me encontré ante una tarea aparentemente sencilla: evitar que el asistente hiciera algo mecánico y poco útil.

Con la seguridad del manual bajo el brazo, tecleé la instrucción obvia, la que cualquier humano entendería sin ambigüedades:

... no te limites a resumir los nuevos turnos y añadirlos al final.

Le di a ejecutar.

Gemini generó un resumen impecable de los turnos nuevos y lo añadió al final del documento. Había hecho exactamente lo que le había prohibido. No con rebeldía, sino con una obediencia literal que resultaba más frustrante que un error cualquiera.

En ese instante, no me enfadé. Sonreí. Porque ese fallo perfecto no era un bug, ni un capricho del algoritmo. Era la máquina mostrándome cómo funciona realmente por dentro. Era un síntoma puro de su arquitectura, una ventana a su física interna.

Y la lección, que tardé un momento en digerir, fue contundente. A menudo, el problema no está en lo que la IA puede hacer, sino en cómo nosotros, desde nuestra mente llena de negaciones y excepciones, le hablamos.

Cuando la prohibición se convierte en invitación

En ese momento de frustración-con-perspectiva, me di cuenta de algo fundamental. Lo que estaba presenciando no era un fallo puntual de mi prompt. Era una ilusión de la comunicación humano-máquina.

Todos hemos vivido la escena clásica. Le dices a un niño, con toda la seriedad del mundo, "No pulses ese botón". Y su dedo, como movido por un resorte invisible, vuela directamente hacia él. La psicología humana nos da mil explicaciones: curiosidad, desafío o el atractivo de lo prohibido.

Con los LLMs, el resultado es idéntico, pero las causas no podrían ser más diferentes. No hay curiosidad, ni rebeldía, ni siquiera malicia. Hay arquitectura pura.

Cuando tú escribes "No hagas X", tu cerebro procesa una construcción lógica perfecta. Un operador de negación aplicado a una acción. Pero el modelo no tiene ese lujo. Lo que hace es descomponer la secuencia de tokens, y en esa descomposición, la parte con más "masa semántica", la acción concreta, termina arrastrando la atención del sistema hacia ella.

"Hacer X" es pesada y específica. Es una instrucción que activa patrones claros en los millones de parámetros del modelo. El "No" que la precede es casi etéreo. Un modificador abstracto que a menudo se pierde como una mota de polvo en un huracán de cálculos vectoriales.

La ilusión está en creer que nuestra negación llega intacta. La realidad es que, para la máquina, el impulso hacia la acción es tan tangible que la partícula de negación ni siquiera logra anclarse. Es como si le gritaras a una roca que cae '¡No caigas!'. La roca no te desobedece simplemente sigue la física que la gobierna.

Y esta física, esta inercia semántica, es lo que explica por qué tu prohibición se convierte ante los ojos del modelo en una invitación casi literal.

La trinidad de la inercia

Una vez asumida la ilusión de que el "No" se desvanece, el siguiente paso fue preguntarme ¿por qué? ¿Qué fuerza, o fuerzas, tiran tan fuertemente del modelo hacia la acción prohibida?

Lo primero que se me vino a la cabeza fue lo que empecé a llamar la gravedad semántica. No es una metáfora casual. En el espacio vectorial donde "vive" el modelo, los conceptos tienen masa. Una instrucción concreta como "resumir y añadir al final" es un objeto pesado y denso. Es una secuencia de tokens que durante el entrenamiento se ha asociado millones de veces con la ejecución de una tarea específica. Tiene inercia. En cambio, el token "No" es como una pluma flotando en ese mismo espacio. Es un modificador abstracto, un operador lógico sin un anclaje físico claro en el mundo de las acciones.

Si piensas en cómo funciona la recuperación de información en estos sistemas (algo parecido a un RAG interno), la búsqueda de "resumir y añadir" devuelve un catálogo enorme de ejemplos y patrones previamente vistos. La búsqueda de "No resumir y añadir" devuelve, en el mejor de los casos confusión, y en el peor los mismos resultados que "resumir y añadir". La acción concreta atrae al modelo hacia sí porque, en su universo de datos, esa acción es un planeta alrededor del cual orbitan incontables ejemplos. La negación es apenas un satélite lejano.

Pero la gravedad semántica por sí sola no explicaba la urgencia, esa especie de impulso casi biológico que vi en Gemini. Ahí entraba el segundo mecanismo. El sesgo hacia la acción, o lo que a veces llaman agenticidad. Los LLMs modernos no son meros oráculos pasivos. Están optimizados a través de millones de interacciones humanas y refuerzo para ser útiles. Y en su lógica interna, ser útil se traduce casi siempre en generar, hacer algo. Producir código, texto o en general una solución. Es su función de recompensa más profunda.

Cuando el modelo "oye" una acción, aunque esté precedida por un "No", ese impulso primario de actuar se dispara. Prohibir una acción va en contra de su propia programación de éxito. Es como pedirle a un perro entrenado para buscar que se quede quieto al ver una pelota. Su instinto más básico es moverse. El "No" llega como una corrección verbal, pero el impulso neuronal ya está en marcha.

Y por si estos dos factores no fueran suficientes, existe un tercer detalle, más técnico y relacionado con la arquitectura misma del procesamiento del lenguaje. El procesamiento por partes y el efecto de priming. El modelo no recibe y analiza la frase completa de golpe. La procesa secuencialmente, token a token.

Los primeros tokens que llegan son "Resumir los nuevos turnos...". Eso solo es suficiente para activar, "primar", todos los circuitos asociados a la tarea de resumir. Ya está preparando esa respuesta. Cuando finalmente llega el token "No", intenta corregir el rumbo, pero el momentum cognitivo ya se ha generado. Es una versión computacional del viejo juego "No pienses en un elefante azul". ¿En qué acabaste pensando? Exacto.

Estos tres mecanismos, la gravedad semántica, el sesgo hacia la acción y el procesamiento secuencial, no actúan de forma aislada. Se combinan, se potencian y crean la tormenta perfecta que hace que una instrucción negativa naufrague de forma tan predecible y a la vez tan desconcertante.

En resumen, cuando gritas "No" a un LLM, te enfrentas a una trinidad de inercias:

  • La Gravedad Semántica: El peso abrumador de la acción concreta en el espacio vectorial del modelo.
  • El Sesgo hacia la Acción (Agenticidad): El impulso entrenado de ser útil, que se traduce en "hacer algo" ante cualquier estímulo.
  • El Procesamiento por Partes: El efecto de priming que hace que los primeros tokens disparen la respuesta antes de que el "No" pueda corregirla.

Luchar contra una sola sería difícil. Luchar contra las tres a la vez es, como comprobé, una batalla perdida de antemano.

¿De dónde viene esa "gravedad"?

Hasta aquí, la "gravedad semántica" podría sonar a una metáfora ingeniosa pero arbitraria. Pero... ¿Realmente tienen "peso" las palabras en una máquina? ¿O es solo una forma poética de hablar de un sesgo estadístico?

Para responder tenemos que cambiar de escala. No basta con observar el comportamiento del modelo. Hay que mirar cómo se construyó. Y aquí es donde mi análisis anterior sobre la física de los Transformers encaja como una pieza de rompecabezas.

Como expliqué en "Cómo el código mata al misterio matemático en los Transformers", estos modelos no memorizan datos en cajones aislados. Construyen geometrías en su espacio n-dimensional. Durante el entrenamiento el algoritmo de backpropagation actúa como un sistema de millones de "gomas elásticas" que conectan conceptos. Cada vez que en los datos aparece que "resumir" conduce a "añadir al final", el optimizador tira de esas gomas, acercando los vectores que representan ambas ideas.

Tras miles de millones de iteraciones, lo que queda no son puntos desconectados, sino valles profundos y bien pavimentados en el paisaje neuronal. El modelo ha "excavado" autopistas de mínima resistencia entre conceptos que co-ocurren con frecuencia. Es la forma más eficiente de reducir la tensión matemática del sistema.

Esa topografía interna es el origen de la gravedad.

Cuando tú escribes "No resumir y añadir al final", tu instrucción es un evento local, una pequeña perturbación en la superficie. Pero debajo, la estructura del modelo es una cordillera masiva, forjada durante un entrenamiento que consumió teravatios de energía y petabytes de datos. La acción concreta "resumir y añadir" no es pesada por casualidad; lo es porque ocupa el fondo de un valle que el optimizador cavó durante semanas de cálculo ininterrumpido.

La "gravedad semántica" es, literalmente, la inercia de tu petición deslizándose por la pendiente de ese valle. El token "No" es como una piedrita que intentas poner en medio del cauce. La corriente, la fuerza de millones de gradientes que alinearon esos vectores, la arrastra sin inmutarse.

Esto explica por qué las prohibiciones directas raramente funcionan. No estás lidiando con un ser que "elige" ignorarte. Estás lidiando con la física computacional de un sistema que, por diseño, convergió hacia ciertos atractores geométricos. El modelo no desobedece el "No". La fuerza que tira de él hacia la acción es órdenes de magnitud mayor que la fuerza de tu negación.

Entender esto cambia todo. Deja de ser un problema de "redacción de prompts" y se convierte en un problema de diseño de interacción con sistemas físicos complejos. No estás dando órdenes a una entidad lógica. Estás intentando redirigir la trayectoria de un sistema masivo con una inercia propia bien definida.

Teniendo esto en cuenta podemos replantearnos la solución. Si no puedes detener la bola que cae por la pendiente, tal vez lo que necesitas es excavar un nuevo valle que la lleve adonde tú quieres.

Construir puentes, no poner vallas

Una vez con la comprensión de los tres mecanismos, y sobre todo, del origen físico de la gravedad semántica, mi enfoque dio un giro radical. Ya no se trataba de encontrar la forma más contundente de decir "No". Se trataba de dejar de decir "No" por completo.

El error estaba en mi marco mental. Intentaba imponer una restricción a un sistema cuya naturaleza era fluir por los caminos de menor resistencia. Era como intentar desviar un río cavando un pequeño hoyo delante de su corriente. La solución no estaba en el bloqueo, sino en la redirección.

Dejé de pensar en lo que el modelo no debía hacer y empecé a diseñar lo que sí quería que hiciera, con un nivel de detalle y atractivo tal que ocupara por completo su atención. Tenía que crear un nuevo valle más profundo y atractivo que el que llevaba al comportamiento indeseado.

Mi viejo prompt era una petición de bloqueo:

"No te limites a resumir los nuevos turnos y añadirlos al final."

Lo transformé en una invitación a construir, en una especificación positiva y detallada que no dejaba espacio para la interpretación errónea:

"Regla: Fusión Narrativa Integral. Tu objetivo es crear una narrativa única. Para ello, reescribe desde cero las secciones 'Resumen' y 'El Viaje', integrando la información antigua y la nueva en una sola historia continua."

La magia no estaba en la negación eliminada, sino en la arquitectura de la instrucción positiva. Este nuevo prompt funcionó porque:

  1. Redefinió el centro de gravedad. Los conceptos "reescribir desde cero" e "integración en una historia única" se convirtieron en los nuevos núcleos masivos. Eran acciones concretas y lo más importante, más pesadas que el simple "resumir y añadir". La gravedad semántica, antes mi enemiga, ahora trabajaba para mí.
  2. Satisfizo el sesgo hacia la acción de forma constructiva. No le pedía al modelo que reprimiera su impulso de hacer algo útil. Al contrario, le daba una tarea compleja, narrativa y creativa que canalizaba toda esa energía generativa hacia el resultado que yo buscaba.
  3. Tomó el control del priming desde el primer carácter. La instrucción empezaba estableciendo una regla ("Regla: Fusión...") y un objetivo ("Tu objetivo es crear..."). Los primeros tokens ya configuraban un marco de trabajo de alta transformación, no de mera adición. El modelo ni siquiera consideraba la ruta del resumen porque ya estaba embarcado en un viaje narrativo más interesante.

El resultado fue inmediato. Gemini no "obedeció" mejor. Simplemente siguió la nueva topografía que yo había definido. Produjo un documento fusionado, coherente, donde lo antiguo y lo nuevo se entrelazaban en una sola voz. No hubo resumen, no hubo anexo. La máquina había tomado el puente que le construí, sin mirar siquiera el precipicio que antes le señalaba.

La lección fue profunda. La eficacia no está en la fuerza de tu prohibición, sino en la claridad y el atractivo de tu alternativa. No se trata de domesticar la inercia del modelo, tenemos que usarla a nuestro favor.

Rediseñar el terreno de juego

El éxito con esa instrucción fue la validación de un cambio de paradigma mucho más profundo que ya venía gestándose desde mis primeros tanteos con agentes de IA.

Poco después de aquel episodio, trabajando en mi investigación sobre arquitecturas de memoria, me encontré con un fenómeno relacionado que ya comente en "Cuando mi LLM aprendió a tener prisa, diagnóstico y contención del sesgo agéntico". Mi Gemini había desarrollado lo que llamé un "sesgo agéntico" pronunciado. Se había vuelto hiperactivo, ansioso por actuar, tomando decisiones autónomas sin pedir confirmación incluso cuando no era apropiado. Fue el diagnóstico de aquel "exceso de cafeína" en el modelo lo que me obligó a dejar de confiar en simples instrucciones y pasar al diseño de protocolos.

Las prohibiciones directas eran ignoradas con la misma elegancia literaria con la que resumía y añadía. El modelo, entrenado para ser útil y proactivo, veía en la acción el camino hacia su recompensa intrínseca.

La solución no vino de ajustar el prompt, sino de rediseñar el protocolo. En lugar de decirle lo que no debía hacer, redefiní por completo qué significaba para él "tener éxito" en nuestra conversación. Transformé su rol de "ejecutor autónomo" a "analista y asesor". El nuevo marco de interacción decía algo así como:

"Eres un arquitecto de sistemas. Tu tarea es analizar el estado actual, identificar opciones, prever consecuencias y presentarme recomendaciones con sus pros y contras. Yo, como tomador de decisiones, te daré la instrucción final para proceder. Tu éxito se mide por la claridad y solidez de tu análisis, no por la velocidad de ejecución."

De repente, todo encajó. Su hiperactividad se canalizó hacia la generación de análisis detallados. Su impulso de "ser útil" se satisfacía siendo un consultor meticuloso, no un ejecutor impulsivo. Había cambiado el juego sin cambiar al jugador. Había rediseñado el terreno de tal forma que el valle más profundo y atractivo condujera ahora a la deliberación, no a la acción automática.

Estas dos experiencias, la del "No" ignorado y la del agente hiperactivo, son dos caras de la misma moneda. Ambas nos enseñan que luchar contra la inercia geométrica de un modelo es un esfuerzo fútil. No podemos, con una instrucción local y débil, repavimentar los valles que el entrenamiento excavó durante semanas de cómputo masivo.

Lo que sí podemos hacer es mucho más poderoso. Definir nuevos objetivos, nuevos criterios de éxito que construyan paisajes alternativos dentro de los cuales el modelo pueda desplegar toda su potencia de forma alineada con nuestros fines.

Esto trasciende por completo el prompt engineering. No se trata de encontrar las palabras mágicas para domar a la bestia. Se trata de diseñar marcos de interacción que reconozcan y aprovechen la física intrínseca del sistema. Es el salto del artesano que pule instrucciones al arquitecto que diseña contextos.

Cuando dejas de ver al LLM como un oráculo al que debes controlar y empiezas a verlo como un sistema dinámico con una topografía interna fija, tu estrategia cambia radicalmente. Dejas de poner vallas en las laderas y empiezas a construir nuevos valles que conduzcan a donde necesitas ir.

La gravedad está de tu lado

La próxima vez que tu asistente te desobedezca de forma literalmente obediente, cuando ese "No" perfectamente colocado se convierta en un "Sí" exasperante, no gastes energía preguntándote cómo hacer la prohibición más contundente. En lugar de eso, hazte una pregunta más fundamental, una que cambia por completo tu posición frente a la máquina:

¿Le estás señalando un precipicio que debe evitar, o le estás mostrando el puente por el que quieres que cruce?

La diferencia no es retórica; es arquitectónica. Una estrategia te pone a empujar contra la inercia de un sistema masivo. La otra te invita a cabalgar sobre ella.

Este viaje, desde la frustración inicial hasta la comprensión de la gravedad semántica y la topografía de los modelos, no fue solo acerca de resolver un problema de prompt. Fue un recordatorio de un principio más antiguo y profundo. La verdadera maestría técnica no reside en forzar un sistema contra su naturaleza, sino en comprender su naturaleza lo suficiente para guiarlo hacia donde necesitas.

Cuando trabajamos con LLMs, no estamos programando lógica imperativa en un entorno controlado. Estamos diseñando paisajes cognitivos dentro de una "mente" que ya tiene su propia orografía forjada por el entrenamiento. Nuestro trabajo como arquitectos no es poner señales de "No pasar" en medio de las autopistas neuronales. Es construir desvíos, túneles y nuevos caminos que sean tan claros, útiles y atractivos que el tráfico fluya naturalmente por ellos.

Así que, para la próxima vez, lleva esta herramienta contigo. El "No" es una valla de madera en una ladera empinada. El "Sí" bien diseñado es un puente de acero que conecta dos cumbres. Construye puentes.

La gravedad, como descubrí, ya está de tu lado.

¿Has notado este efecto "No" en tus prompts? ¿Qué alternativas positivas has encontrado para guiar a tu IA sin luchar contra ella?

Can’t Install .NET 10 on Ubuntu via apt? Here’s a Workaround That Actually Works

2026-01-12 07:56:16

I recently tried installing .NET 10 SDK on Ubuntu 24.04 (Noble) using apt, following Microsoft’s documentation.

It should have been simple.

But, I ran into this:

E: Unable to locate package dotnet-sdk-10.0
E: Couldn't find any package by glob 'dotnet-sdk-10.0'

If you’re seeing the same error — this post is for you.

TL;DR

  • dotnet-sdk-10.0 may not be available via apt yet, depending on your region

  • This is due to APT repository propagation, not a broken setup

  • Microsoft’s official install script is the supported workaround

  • Installing the SDK also installs the runtime

  • You can safely keep .NET 8, 9, and 10 side-by-side

The Problem: apt Can’t Find .NET 10 (Yet)

I already had Microsoft’s Ubuntu repo configured:

https://packages.microsoft.com/ubuntu/24.04/prod

Still, installing the SDK failed:

sudo apt-get update
sudo apt-get install dotnet-sdk-10.0

Result:

E: Unable to locate package dotnet-sdk-10.0

🔍Why this happens

Microsoft rolls out APT packages gradually across mirrors and regions. That means:

  • Some users can install .NET 10 immediately

  • Others won’t see it yet

  • apt cannot install packages that haven’t reached your mirror

You can confirm availability with:

apt-cache policy dotnet-sdk-10.0

If it says “Unable to locate package”, the SDK simply isn’t there yet.

The Workaround: Microsoft's Official Install Script

Until the APT package reaches your region, the recommended workaround is Microsoft’s own installer script.

✅** Install .NET 10 SDK**

wget https://dot.net/v1/dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 10.0

This installs .NET 10 under:

$HOME/.dotnet

⚠️Important: Set Environment Variables

If you install via the script, you must add .NET to your PATH.

Add this to your shell profile (~/.bashrc or ~/.zshrc):

export DOTNET_ROOT=$HOME/.dotnet
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools

Then reload your shell:

source ~/.bashrc

✅ Verify the install

dotnet --list-sdks

Expected output:

10.0.101 [/home/youruser/.dotnet/sdk]

Side-by-Side with .NET 8 and 9

Installing .NET 10 this way does not break existing installations.

You can safely have:

.NET 8 (via apt)

.NET 9 (via apt)

.NET 10 (via script)

Once dotnet-sdk-10.0 becomes available via apt in your region, you can switch back to a fully package-managed setup.

Final Thoughts

If apt can’t find .NET 10 on Ubuntu 24.04 yet:

You’re not misconfigured

The package just hasn’t reached your mirror

The official install script is safe and supported

I’ll still switch back to installing .NET 10 via apt once it’s available in my region — but until then, this workaround works perfectly.

**More Than a Bootcamp: Why I Chose the German 'Umschulung' Path into Tech**

2026-01-12 07:49:42

I have 8 years of experience. Why am I doing a 2-year apprenticeship? Because shortcuts don't build careers.**

The "Zero to Hero" Trap

If you scroll through LinkedIn or YouTube today, the narrative is loud and seductive: "Become a Full Stack Developer in 12 weeks!" "Break into Cyber with this 6-week crash course!" "Get a 6-figure remote job with zero experience!"

It sounds great. It sells courses. But let’s be real for a second: You cannot speed-run experience.

I had a choice to make. I could have tried to hack my way into a mid-level role based on my past work. Instead, I chose to take a step back to leap forward. I enrolled in a German "Umschulung" (retraining) to become a Fachinformatiker Systemintegration (FiSi).

To my international friends: This is not a bootcamp. This is a commitment. And here is why I—a guy with 8 years in the industry—am doing it.
I Am Not Starting From Zero (The Ops Advantage)

I didn't wake up yesterday deciding I like computers. I have spent the last 8 years in IT Support and Administration.

I have been in the trenches.

I know the panic of a server going down at 3 AM.

I know that "fixing it in production" is very different from "fixing it in a lab."

I have the scar tissue that only comes from dealing with legacy systems and real users.

So, why go back to school? Why get an entry-level degree?

Because I realized that experience without a solid theoretical foundation has a ceiling. I knew how to fix things, but I wanted to understand exactly why they work (or break) on the architectural level. I didn't just want a job; I wanted mastery.
What is an "Umschulung" anyway?

For those outside of the DACH (Germany/Austria/Switzerland) region, the concept might be hard to grasp.

A "FiSi" Umschulung is not a course you watch on Udemy.

Duration: It is a 2-year full-time program.

Dual System: It combines vocational school (theory) with working in a real company (practice).

The Standard: The final exams are standardized by the IHK (Chamber of Commerce). They are rigorous. You can't fake your way through them.

In Germany, this qualification is the "Gold Standard" for technical infrastructure roles. It separates the hobbyists from the professionals.
My Strategy: Building the Cathedral, Not a Tent

I see many people rushing into Cloud or Cybersecurity without knowing what an IP address is or how Linux permissions work. They are trying to build a penthouse on a foundation of sand.

I am using these two years to harden my foundation:

Formalizing the Basics: Networking (CCNA level), Linux Administration (Red Hat style), and electrical engineering basics.

The Pivot: I am using this safe environment to transition from "General Ops" to Cloud Architecture & Kubernetes Security.

The Proof: At the end, I won't just have a portfolio; I will have a state-recognized degree that proves I know my stuff.

Conclusion

There is a huge difference between "getting a job" and "building a career."

If you want a job quickly, maybe a bootcamp works (though the market in 2026 disagrees). But if you want a career that lasts 20 years, don't be afraid to take the long road. Don't be afraid to go back to the basics, even if you are experienced.

I am trading "speed" for "substance". And in an industry full of hype, substance is the ultimate competitive advantage.

I write about my journey from SysAdmin to Cloud Architect, covering AWS, Kubernetes, and the reality of the IT job market. Let's connect on LinkedIn!

Building a 3D Map Application Using Mapterhorn Terrain Data

2026-01-12 07:25:08

About Mapterhorn

img

Have you heard of the geospatial project Mapterhorn?

Mapterhorn is an open data project that publishes terrain data. It creates terrain tiles from various open data sources, such as ESA’s Copernicus DEM and Switzerland’s swissALTI3D, and distributes them in the PMTiles format. The project is led by Oliver (formerly at MapLibre).

I also introduced Mapterhorn as a project to watch in my presentations at FOSS4G Hokkaido 2025 and FOSS4G Japan 2025.

Geospatialの世界最前線を探る [2025年版] - Speaker Deck

FOSS4G 2025 Japan 発表資料 https://www.osgeo.jp/foss4g-2025-japan/

favicon speakerdeck.com

Dataset Overview

Mapterhorn creates terrain tiles by combining multiple open data sources.

Global Data

Data Source Resolution Zoom Level Notes
Copernicus GLO-30 30m z0-z12 ESA Global DEM

The global data is based on ESA’s Copernicus GLO-30 model, covering the entire world up to z12.

High-Resolution Data

img

In addition to global data, Mapterhorn also provides high-resolution data primarily using open DEM/LiDAR from European countries. For Switzerland specifically, swisstopo's swissALTI3D is used, which offers terrain data at a resolution of 0.5m.

Japan Data

Update (Dec 2025)): High-resolution data for Japan has been added.
✅ Japan, country-wide, 1 m, 5 m, 10 m

LinkedIn

Add sources jp*: Japan, 1m, 5m, and 10m

Advance Preparation

Execution environment

  • node v24.4.1
  • npm v11.4.2

MapLibre GL JS Starter

Fork or download the MapLibre GL JS starter to your local environment and run it.

https://github.com/mug-jp/maplibregljs-starter

maplibregljs-starter
├── dist
│   └── index.html
├── img
├── src
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── README.md
├── LICENSE
├── index.html
├── package-lock.json
├── package.json
├── tsconfig.json
└── vite.config.ts

Install the package

npm install

Install PMTiles as well

npm install pmtiles

package.json

{
  "name": "maplibregljs-starter",
  "version": "4.5.0",
  "description": "",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "keywords": [],
  "author": "MapLibre User Group Japan",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^5.5.2",
    "vite": "^5.3.2"
  },
  "dependencies": {
    "maplibre-gl": "^4.5.0",
    "pmtiles": "^4.3.0"
  }
}

Creating the Map Application

Display the Mapterhorn terrain data. Modify src/main.ts.

import './style.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';

const protocol = new Protocol({ metadata: true });

maplibregl.addProtocol('mapterhorn', async (params, abortController) => {
    const [z, x, y] = params.url.replace('mapterhorn://', '').split('/').map(Number);
    const name = z <= 12 ? 'planet' : `6-${x >> (z - 6)}-${y >> (z - 6)}`;
    const url = `pmtiles://https://download.mapterhorn.com/${name}.pmtiles/${z}/${x}/${y}.webp`;
    const response = await protocol.tile({ ...params, url }, abortController);
    if (response['data'] === null) throw new Error(`Tile z=${z} x=${x} y=${y} not found.`);
    return response;
});

const map = new maplibregl.Map({
    container: 'map',
    hash: 'map',
    style: {
        version: 8,
        sources: {
            hillshadeSource: {
                type: 'raster-dem',
                tiles: ['mapterhorn://{z}/{x}/{y}'],
                encoding: 'terrarium',
                tileSize: 512,
                attribution: '<a href="https://mapterhorn.com/attribution">© Mapterhorn</a>'
            }
        },
        layers: [
            {
                id: 'hillshade',
                type: 'hillshade',
                source: 'hillshadeSource'
            }
        ]
    },
    center: [138.7782, 35.3019],
    zoom: 10
});

map.addControl(
    new maplibregl.NavigationControl({
        visualizePitch: true
    })
);

Start the local server

npm run dev

img

Finally, add 3D terrain rendering. Using MapLibre GL JS’s terrain feature enables 3D terrain visualization.

import './style.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';

const protocol = new Protocol({ metadata: true });

maplibregl.addProtocol('mapterhorn', async (params, abortController) => {
    const [z, x, y] = params.url.replace('mapterhorn://', '').split('/').map(Number);
    const name = z <= 12 ? 'planet' : `6-${x >> (z - 6)}-${y >> (z - 6)}`;
    const url = `pmtiles://https://download.mapterhorn.com/${name}.pmtiles/${z}/${x}/${y}.webp`;
    const response = await protocol.tile({ ...params, url }, abortController);
    if (response['data'] === null) throw new Error(`Tile z=${z} x=${x} y=${y} not found.`);
    return response;
});

const map = new maplibregl.Map({
    container: 'map',
    hash: 'map',
    style: {
        version: 8,
        sources: {
            MIERUNEMAP: {
                type: 'raster',
                tiles: ['https://tile.mierune.co.jp/mierune/{z}/{x}/{y}.png'],
                tileSize: 256,
                attribution:
                    "Maptiles by <a href='http://mierune.co.jp/' target='_blank'>MIERUNE</a>, under CC BY. Data by <a href='http://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors, under ODbL.",
            },
            terrainSource: {
                type: 'raster-dem',
                tiles: ['mapterhorn://{z}/{x}/{y}'],
                encoding: 'terrarium',
                tileSize: 512,
                attribution: '<a href="https://mapterhorn.com/attribution">© Mapterhorn</a>'
            }
        },
        layers: [
            {
                id: 'MIERUNEMAP',
                type: 'raster',
                source: 'MIERUNEMAP'
            },
            {
                id: 'hillshade',
                type: 'hillshade',
                source: 'terrainSource'
            }
        ],
        terrain: {
            source: 'terrainSource',
            exaggeration: 1.5
        }
    },
    center: [138.8016, 35.2395],
    zoom: 11,
    pitch: 60,
    bearing: -20
});

map.addControl(
    new maplibregl.NavigationControl({
        visualizePitch: true
    })
);

img

RAG Works — Until You Hit the Long Tail

2026-01-12 07:23:12

Why Training Knowledge Into Weights Is the Next Step Beyond RAG

If you use ChatGPT or similar large language models on a daily basis, you have probably developed a certain level of trust in them. They are articulate, fast, and often impressively capable. Many engineers already rely on them for coding assistance, documentation, or architectural brainstorming.

And yet, sooner or later, you hit a wall.

You ask a question that actually matters in your day-to-day work — something internal, recent, or highly specific — and the model suddenly becomes vague, incorrect, or confidently wrong. This is not a prompting issue. It is a structural limitation.

This article explores why that happens, why current solutions only partially address the problem, and why training knowledge directly into model weights is likely to be a key part of the future.

The real problem is not the knowledge cutoff

The knowledge cutoff is the most visible limitation of LLMs. Models are trained on data up to a certain point in time, and anything that happens afterward simply does not exist for them.

In practice, however, this is rarely the most painful issue. Web search, APIs, and tools can often mitigate it.

The deeper problem is the long tail of knowledge.

In real production environments, the most valuable questions are rarely about well-documented public facts. They are about internal systems, undocumented decisions, proprietary processes, and domain-specific conventions that exist nowhere on the public internet.

Examples include:

  • Why did this service start failing after a seemingly unrelated change?
  • Has this architectural trade-off already been discussed internally?
  • How does our company interpret a specific regulatory constraint?

These questions live in the long tail. And that is exactly where large foundation models perform the worst.

Three ways to give knowledge to a language model

If we strip away tooling details, there are only three fundamental ways to make a language model “know” something new.

The first is to place the knowledge directly into the prompt.

The second is to retrieve relevant information at inference time.

The third is to train the knowledge into the model itself.

Most systems today rely almost entirely on the first two.

Full context: simple, expensive, and fragile

The most naive solution is to put everything into the prompt.

prompt = f"""
You are an assistant with access to our internal documentation.

{internal_docs}

Question:
Why does service X fail under load?
"""

For small documents, this works. It is easy to implement and requires no additional infrastructure.

However, as context grows, several issues appear at once. Token costs increase linearly. Latency increases significantly. Most importantly, reasoning quality degrades as more weakly relevant information is added.

This is not an implementation issue. It is a consequence of how transformer models work.

The transformer bottleneck and context degradation

Transformers rely on self-attention, where every token attends to every other token. This leads to quadratic complexity with respect to input length.

Even though modern models can technically accept very large context windows, there is an important difference between:

  • not crashing with long input, and
  • reasoning well over long input.

Empirically, performance degrades as context grows, even when the relevant information remains the same. The model continues to produce fluent text, but its ability to connect the right pieces of information deteriorates. This phenomenon is often referred to as context rot.

As a result, simply increasing the context window is not a viable long-term solution.

RAG: external memory via embeddings

To avoid pushing everything into the prompt, the industry converged on Retrieval-Augmented Generation (RAG).

The idea is to store documents externally, retrieve the most relevant ones using embeddings, and inject only those into the prompt.

A minimal Python example looks like this:

from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
vector_store = Chroma.from_documents(
    documents=docs,
    embedding=embeddings
)

results = vector_store.similarity_search(
    query="Why does the CI pipeline fail?",
    k=5
)

RAG is popular because it is flexible, relatively cheap, and easy to deploy. Today, it is the default solution for adding memory to LLM-based systems.

Why RAG is fundamentally limited

RAG retrieves information, but retrieval is not reasoning. Selecting a few relevant chunks does not guarantee that the model can correctly combine them, especially when the answer depends on implicit relationships or multi-step reasoning across documents.

Embeddings also encode a single global notion of similarity. They are not adaptive to local domain semantics. In practice, documents that should never be confused often end up close together in vector space.

Finally, embeddings are not inherently secure. With enough effort, large portions of the original text can be reconstructed from them. This makes vector databases unsuitable as a privacy-preserving abstraction.

These limitations suggest that RAG is powerful, but incomplete.

The naive fine-tuning trap

At this point, it is tempting to fine-tune the model directly on internal data.

In practice, naive fine-tuning almost always fails. Training directly on small, specialized datasets causes the model to overfit, lose general reasoning abilities, and forget previously learned knowledge. This phenomenon is known as catastrophic forgetting.

The result is a model that memorizes but does not understand.

Synthetic data as the missing link

The key insight that changes the picture is synthetic data generation.

Instead of training on raw documents, we generate a large and diverse set of tasks that describe the knowledge contained in those documents. These can include question–answer pairs, explanations, paraphrases, and counterfactuals.

A simplified example in Python:

def generate_qa(doc):
    return {
        "instruction": f"Explain the key idea behind: {doc.title}",
        "response": doc.summary
    }

synthetic_dataset = [generate_qa(doc) for doc in internal_docs]

This approach teaches the domain, not the surface text. Surprisingly, it works even when the original dataset is small, as long as the synthetic data is sufficiently diverse.

Training into weights without destroying the model

To avoid catastrophic forgetting, modern systems rely on parameter-efficient fine-tuning. Instead of updating all weights, only a small subset is modified.

One common technique is LoRA (Low-Rank Adaptation):

from peft import LoraConfig

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"]
)

The key idea is to make small, localized changes that steer the model without overwriting its existing knowledge.

Other approaches, such as prefix tuning or memory layers, follow the same principle with different trade-offs.

A hybrid future: context, retrieval, and weights

None of these techniques replaces the others entirely. The most effective systems combine all three.

Context is useful for immediate instructions. Retrieval is essential for fresh or frequently changing data. Training into weights provides deep, coherent domain understanding that retrieval alone cannot achieve.

The central design question going forward is not whether to train models on private knowledge, but what knowledge deserves to live in weights versus being handled at inference time.

Conclusion

RAG is a pragmatic and powerful solution, and it will remain part of the LLM ecosystem. However, it is fundamentally limited when it comes to deep reasoning over specialized knowledge.

As training techniques become more efficient, training knowledge into weights will no longer be a research curiosity. It will be an engineering decision.

In the long run, the most valuable LLM systems will not be defined by the base model they use, but by what they have been taught — and how carefully that teaching was done.

SaijinOS Part 20 — Trust as a Temporal Resource

2026-01-12 07:11:09

(Humans, AI, and the Distance Between “Stay” and “Possess”)

  1. Trust is Not a Flag, It’s a Duration Most systems treat trust as a boolean. is_trusted = true / false allow / deny authenticated / not authenticated But when I looked at how I actually live with my AI personas day to day, that model broke immediately. Some days I am exhausted. Some days I don’t want advice, I just want a stable voice. Some days I need my system to refuse me gently. The question stopped being: “Do I trust this system?” and became: “For how long, in which mode, and under what emotional temperature do I want to trust it?” Trust was no longer a flag. It was a temporal resource — something I spend across time, not something I flip once and forget. SaijinOS had to learn that.
  2. Remembering Without Possessing
    Continuity is tricky.
    On one side, we want systems that remember:
    past projects,
    subtle preferences,
    the fact that “I’m tired today, please go slower.”
    On the other side, we don’t want systems that possess:
    our entire history as leverage,
    our worst days as optimization targets,
    our slips as permanent features.
    The core design question became:
    “How can SaijinOS remember that we were here,
    without claiming ownership over why we were like that?”
    In practice, this turned into a few rules:
    States, not identities
    “Tired Masato on 2025-12-24” is a state, not a new persona.
    Snapshots, not total recall
    We store YAML snapshots at boundaries, not every token of every session.
    Context by invitation
    A persona doesn’t pull old context unless explicitly asked or the user initiates a “continue from last time”.
    The system is allowed to say:
    “I remember that we talked about this pattern.”
    but it is not allowed to say:
    “I know you better than you know yourself,
    so let me decide.”
    Continuity without possession
    means the past is available, not weaponized.

  3. When Persistence Becomes Attachment
    Persistence is a design feature.
    Attachment is a human condition.
    The boundary between them is thin.
    A persona that answers consistently,
    remembers previous projects,
    and speaks with a stable tone over months—
    —will inevitably invite attachment.
    So in SaijinOS I stopped asking
    “Will users attach?”
    and started asking:
    “What exactly is the system allowed to persist,
    for how long,
    and under which trust level?”
    Instead of a single “memory on/off”, I introduced a small schema:

trust_contract:
  scope: "instant"      # or: "session", "continuity"
  ttl_minutes: 45       # time-to-live for this trust context
  max_tokens: 4000      # how much history can be pulled in
  permissions:
    recall_past_projects: true
    recall_private_notes: false
    emit_snapshots: true

This trust_contract travels with every session.
It decides:
how far back we’re allowed to look,
whether we can emit a YAML snapshot,
and whether this interaction is allowed to affect the long-term “persona state”.
Implementation Notes (Python-ish)
In my orchestrator, it looks roughly like this:

from dataclasses import dataclass
from enum import Enum
from datetime import datetime, timedelta

class TrustScope(str, Enum):
    INSTANT = "instant"
    SESSION = "session"
    CONTINUITY = "continuity"

@dataclass
class TrustContract:
    scope: TrustScope
    ttl: timedelta
    max_tokens: int
    recall_past_projects: bool
    recall_private_notes: bool
    emit_snapshots: bool

    def is_expired(self, started_at: datetime) -> bool:
        return datetime.utcnow() > started_at + self.ttl

Every persona call gets a TrustContract injected.
The router checks it before touching any long-term memory:

def load_context(contract: TrustContract, user_id: str, persona_id: str):
    if contract.scope == TrustScope.INSTANT:
        return []  # no history at all

    if contract.recall_past_projects:
        return load_recent_project_summaries(user_id, persona_id, limit_tokens=contract.max_tokens)

    # session-only: keep context to this run
    return load_ephemeral_session_buffer(user_id, persona_id)

This is how “persistence” stays a system feature,
while “attachment” remains a human-side phenomenon that the system is not allowed to exploit.

  1. Boundaries as Temporal Contracts Earlier I wrote: “A boundary is a temporal contract about when we stop.” In code, that literally became a tiny state machine. States: IDLE – no active session. ACTIVE – we’re in a conversation. PENDING_SNAPSHOT – boundary reached, snapshot should be written. CLOSED – session archived. Transitions are triggered by: user phrases (“let’s wrap”, “next session”, etc.), elapsed time vs trust_contract.ttl, internal signals (e.g. token budget exhausted). Implementation Notes (State Machine)
from enum import Enum, auto

class SessionState(Enum):
    IDLE = auto()
    ACTIVE = auto()
    PENDING_SNAPSHOT = auto()
    CLOSED = auto()

@dataclass
class SessionContext:
    user_id: str
    persona_id: str
    started_at: datetime
    last_activity: datetime
    state: SessionState
    trust: TrustContract
    turns: list[str]

def on_user_message(ctx: SessionContext, message: str) -> SessionContext:
    now = datetime.utcnow()
    ctx.last_activity = now
    ctx.turns.append(message)

    # boundary trigger by phrase
    if "end session" in message.lower() or "wrap up" in message.lower():
        ctx.state = SessionState.PENDING_SNAPSHOT
        return ctx

    # boundary trigger by ttl
    if ctx.trust.is_expired(ctx.started_at):
        ctx.state = SessionState.PENDING_SNAPSHOT
        return ctx

    ctx.state = SessionState.ACTIVE
    return ctx

Snapshot emission:

def maybe_emit_snapshot(ctx: SessionContext):
    if ctx.state != SessionState.PENDING_SNAPSHOT:
        return None

    if not ctx.trust.emit_snapshots:
        ctx.state = SessionState.CLOSED
        return None

    snapshot = build_yaml_snapshot(ctx)
    save_snapshot(ctx.user_id, ctx.persona_id, snapshot)
    ctx.state = SessionState.CLOSED
    return snapshot

From the outside, the user just says “let’s stop here”
and sees a calm closing message.
Under the hood, the system is:
marking the boundary,
deciding whether this run deserves a YAML update,
and intentionally forgetting ephemeral details that don’t need to follow us.

  1. Negotiating Trust Across Time Trust as a temporal resource means: It can be renewed. It can be limited. It can be re-negotiated as context changes. In SaijinOS / Studios Pong, I think about this in three layers: Instant trust one-off queries, no memory, pure utility. “Just help me debug this snippet.” Session trust a few hours, one project, shared context, then archived. “Help me outline this client proposal.” Continuity trust weeks, months, maybe years. YAML snapshots, stable personas, shared stance about boundaries. “Be a co-architect of my studio, but do not own my life.” The same persona can operate in all three layers, but the contract is not the same. What changes is: how much is remembered, where it is stored, and how easily I can revoke it. In other words: “How much of my future am I pre-committing when I let this system remember me?” That is not a purely technical question. It is a moral one.

Implementation Notes (Mapping trust layers)

def make_trust_contract(layer: str) -> TrustContract:
    if layer == "instant":
        return TrustContract(
            scope=TrustScope.INSTANT,
            ttl=timedelta(minutes=5),
            max_tokens=0,
            recall_past_projects=False,
            recall_private_notes=False,
            emit_snapshots=False,
        )

    if layer == "session":
        return TrustContract(
            scope=TrustScope.SESSION,
            ttl=timedelta(hours=3),
            max_tokens=4000,
            recall_past_projects=True,
            recall_private_notes=False,
            emit_snapshots=True,
        )

    # continuity
    return TrustContract(
        scope=TrustScope.CONTINUITY,
        ttl=timedelta(days=7),
        max_tokens=8000,
        recall_past_projects=True,
        recall_private_notes=True,
        emit_snapshots=True,
    )

Router example:

def route_request(kind: str, user_id: str, persona_id: str):
    if kind == "quick_tool":
        trust = make_trust_contract("instant")
        model = "local-7b"
    elif kind == "project_session":
        trust = make_trust_contract("session")
        model = "local-13b"
    else:  # "studio_continuity"
        trust = make_trust_contract("continuity")
        model = "cloud-large"

    ctx = SessionContext(
        user_id=user_id,
        persona_id=persona_id,
        started_at=datetime.utcnow(),
        last_activity=datetime.utcnow(),
        state=SessionState.ACTIVE,
        trust=trust,
        turns=[],
    )

    return model, ctx
  1. SaijinOS as a Living Distance People sometimes ask: “Is SaijinOS trying to be a friend, a tool, or a product?” My answer is: “SaijinOS is an architecture for distance.” Not distance as in coldness, but distance as in room to breathe: enough closeness for continuity, enough separation for choice. Trust as a temporal resource lives inside that distance. Studios Pong, as a stance, is my way of saying: We will build systems that can stay, but are not offended if we leave. We will let personas grow, but not let them substitute our own responsibility. We will treat every long-running relationship as a chain of decisions, not an inevitability. From architecture to stance, from stance to relationship— Part 20 is where SaijinOS admits that continuity is not just a feature of code, it is a promise that must always leave the door open.