LangChain 1.x en la práctica: cadenas, RAG, herramientas y agentes de forma local
Aprenda a crear cadenas, generación mejorada por recuperación de información, herramientas y sistemas RAG con agente utilizando LangChain 1.x con una configuración local gratuita de Ollama, sin necesidad de claves API.
Este es el tercer capítulo de una serie práctica que continúa con trabajos anteriores sobre cómo crear RAG y agentes desde cero utilizando solo Python. El enfoque aquí es intencionalmente diferente: en lugar de armar cada componente por separado, verá cómo LangChain integra toda esa infraestructura en unas pocas líneas de código. Dado que ya ha construido los componentes básicos manualmente, está en una buena posición para comprender qué hace realmente cada abstracción en lugar de tratarla como algo mágico. Esa comprensión es crucial: marca la diferencia entre los desarrolladores que utilizan LangChain de manera efectiva y aquellos que constantemente luchan contra él.
Todo en este tutorial se ejecuta localmente y de forma gratuita, utilizando un modelo local a través de Ollama junto con embeddings locales. No hay necesidad de claves API ni límites de velocidad de los que preocuparse.
Nota sobre las versiones: este tutorial está dirigido a LangChain 1.x, verificado con
langchain==1.3.11ylangchain-core==1.4.8. LangChain 1.0 introdujo una reorganización significativa: la API actual de agentes se centra encreate_agent, mientras que componentes anteriores comoAgentExecutoryinitialize_agentfueron trasladados a un paquete separado llamadolangchain-classic. Muchos tutoriales disponibles en línea siguen mostrando los patrones anteriores a la versión 1.0; las importaciones que se presentan aquí son las actuales, y cada una ha sido verificada para asegurarse de que se resuelvan correctamente.
Cómo seguir el proceso: abra un archivo llamado lc.py, ejecute cada bloque de código en secuencia y complete los ejercicios “Su turno” a medida que los encuentre. Cada vez que vea la frase “usted construyó esto”, se refiere a la implementación manual de los tutoriales anteriores de esta serie.
Paso 0: ¿Qué es realmente LangChain?
LangChain se entiende mejor como una colección de componentes estandarizados e intercambiables para crear aplicaciones impulsadas por LLM: cosas como envoltorios de modelos, plantillas de prompts, recuperadores de información, almacenes vectoriales, herramientas y agentes. Todos estos componentes cumplen con una interfaz común, lo que significa que puede conectarlos entre sí y sustituir uno por otro (cambiar de modelo, cambiar de almacén vectorial) sin necesidad de reescribir la lógica de su aplicación.
El concepto que une todo es Runnable. Cada componente expone el mismo método .invoke(), y cualquier par de componentes puede conectarse en cadena mediante el operador de tubería |. Este mecanismo de tuberías se conoce como LCEL, abreviatura de LangChain Expression Language. Una vez que todas las partes de su sistema utilizan la interfaz Runnable, todo un pipeline o agente RAG puede expresarse en solo unas pocas líneas.
Un compromiso importante que vale la pena mencionar desde el principio: LangChain reduce el código repetitivo y le brinda acceso a un amplio catálogo de integraciones listas para usar. A cambio, introduce capas de abstracción que pueden dificultar la depuración; habrá momentos en los que preferirá mirar directamente el bucle simple que usted mismo escribió. Saber cuándo esa abstracción justifica el costo es la verdadera habilidad, y volveremos a tratar este compromiso en el Paso 7.
Configuración (la pila local gratuita)
pip install langchain langchain-core langchain-ollama langchain-huggingface langchain-text-splitters sentence-transformers
También necesitará tener instalado Ollama (es gratuito y funciona localmente); después de eso, deberá descargar un modelo capaz de realizar llamadas a herramientas:
ollama pull llama3.2 # ~2 GB; needs ~8 GB RAM. qwen2.5 also works well.
Si prefiere omitir Ollama, aún puede ejecutar las secciones chain y RAG utilizando un modelo local de Hugging Face a través de
langchain-huggingface. Sin embargo, las secciones agent dependen de un comportamiento fiable al llamar a herramientas, algo que los modelos pequeños limitados por la CPU suelen manejar mal. Se recomienda encarecidamente usar Ollama para los pasos del 4 al 6.
Paso 1: El paso clave: una cadena con el tubo |
En el tutorial anterior sobre RAG, compiló manualmente una prompt con una cadena de texto formatado, la pasó al modelo y limpió el resultado con .strip(). LangChain captura esa misma secuencia como una expresión de tubo. Añada esto a lc.py:
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOllama(model="llama3.2", temperature=0)
prompt = ChatPromptTemplate.from_template(
"Explain {topic} in exactly one sentence."
)
# The chain: prompt -> model -> plain-string parser
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"topic": "retrieval-augmented generation"}))
Al leer la cadena de tuberías de izquierda a derecha: prompt convierte tu diccionario de entrada en un mensaje con formato adecuado, llm transforma ese mensaje en una respuesta del modelo, y StrOutputParser() extrae el texto plano del objeto de respuesta.
Ya lo has creado tú mismo. Esta cadena de tuberías es funcionalmente idéntica a f"Explain {topic}..." seguido de generator(prompt) y luego de [0]["generated_text"].strip() del tutorial anterior sobre RAG: tres pasos manuales, ahora expresados como tres elementos Runnable conectados mediante tuberías. La lógica no ha cambiado; solo se ha estandarizado la interfaz.
Tu turno: cada Runnable también admite por defecto los métodos .batch() y .stream(). Pruébalo:
for piece in chain.stream({"topic": "vector embeddings"}):
print(piece, end="", flush=True) # tokens arrive as they're generated
print()
print(chain.batch([{"topic": "agents"}, {"topic": "chunking"}])) # two at once
Tenga en cuenta que la transmisión en flujo y el agrupamiento de datos se incluyeron de forma gratuita simplemente porque utilizó la interfaz estándar Runnable. Ese aspecto de “de forma gratuita” representa, en pequeña escala, toda la razón para usar LangChain.
Paso 2: RAG, a la manera de LangChain
Es hora de reconstruir su pipeline RAG manual utilizando los componentes básicos de LangChain. Cada elemento corresponde directamente a algo que ya escribió a mano.
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Same Nimbus knowledge base from the RAG tutorial
DOCUMENTS = [
"Nimbus is a fictional note-taking app. The free plan, Nimbus Lite, allows up to 50 notes and 1 GB of storage.",
"Nimbus Pro costs 8 dollars per month billed annually, or 10 dollars billed monthly. It includes 50 GB of storage and collaboration for up to 5 people.",
"Nimbus stores notes encrypted at rest with AES-256. End-to-end encryption is Pro-only and must be enabled in Settings > Security.",
"Nimbus offers a 30-day refund policy on all paid plans. Refunds reach the original payment method within 5 business days.",
"Nimbus live chat support is staffed for Pro customers, Monday to Friday, 9am-6pm UTC. Free users get email support with a 48-hour response time.",
]
# 1. Split (↔ your chunk_text function)
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=40)
chunks = splitter.create_documents(DOCUMENTS)
# 2. Embed locally (↔ your sentence-transformers model)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# 3. Store + index (↔ your numpy array of vectors). No server needed.
vectorstore = InMemoryVectorStore.from_documents(chunks, embeddings)
# 4. Retriever (↔ your retrieve() with cosine top-k)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
for doc in retriever.invoke("How much does Pro cost?"):
print("-", doc.page_content[:70], "...")
↔ Tú construiste esto: todo el sistema. RecursiveCharacterTextSplitter actúa como el divisor de texto, pero lo hace con más cuidado: divide el texto según los límites entre párrafos y oraciones en lugar de limitarse a contar palabras. HuggingFaceEmbeddings es un envoltorio del mismo modelo all-MiniLM-L6-v2 que utilizaste anteriormente. InMemoryVectorStore sustituye a tu arreglo numpy de vectores, y su método .as_retriever() realiza la misma búsqueda de similitud coseno top-k que codificaste manualmente. Cuatro líneas aquí abarcan todo lo que construiste en los pasos 2 a 4 anteriores.
A continuación, conecta la recuperación con la generación utilizando LCEL:
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
rag_prompt = ChatPromptTemplate.from_template(
"Answer using only the context. If it's not there, say you don't know.\n\n"
"Context:\n{context}\n\nQuestion: {question}\nAnswer:"
)
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| rag_prompt
| llm
| StrOutputParser()
)
print(rag_chain.invoke("How much does Nimbus Pro cost per month?"))
El diccionario al inicio de la cadena ejecuta dos ramas en paralelo: question simplemente reenvía la entrada sin cambios, mientras que context dirige esa misma entrada a través del recuperador y formatea los resultados. Ambas salidas fluyen luego hacia el prompt. ↔ tú construiste esto es esencialmente tu antigua función rag_answer() — recuperar, insertar en un prompt, generar — condensada en una sola expresión.
Tu turno: Intenta llamar a rag_chain.invoke("¿Pueden los usuarios gratuitos usar el chat en vivo?") y luego sigue con una pregunta no relacionada como rag_chain.invoke("¿Cuál es la capital de Francia?"). Presta atención a la respuesta “No lo sé”; se trata de la misma verificación de contexto del tutorial anterior sobre RAG, que refuerza el mismo punto: la calidad de la recuperación de información determina la calidad de la respuesta. Después, llama a retriever.invoke(...) por separado para ver exactamente qué información se obtuvo cuando la respuesta parece incorrecta. Esa separación —verificar la recuperación de información de forma independiente a la generación— es un hábito de depuración que ya has utilizado antes, y LangChain lo mantiene al conservar estos dos pasos como Runnables distintos.
Paso 3 — Herramientas
En el tutorial de agentes, definiste las herramientas como un diccionario TOOLS y escribiste tú mismo un analizador basado en expresiones regulares para extraer el nombre de la herramienta y sus parámetros de la salida de texto bruta del modelo. LangChain elimina por completo la necesidad de ese analizador mediante llamadas nativas a herramientas: describes qué hace una herramienta, el modelo responde con una llamada estructurada, y LangChain se encarga del enrutamiento. Definir una herramienta se hace de esta manera, utilizando el decorador @tool:
from langchain_core.tools import tool
import ast, operator, datetime
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.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("unsupported")
@tool
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression like '8 * 12'."""
return str(_ev(ast.parse(expression, mode="eval").body))
@tool
def get_today(_: str = "") -> str:
"""Return today's date in ISO format."""
return datetime.date.today().isoformat()
He aquí el detalle por el que vale la pena detenerse. Puedes inspeccionar exactamente lo que LangChain generó a partir de tu función:
print(calculator.name) # 'calculator'
print(calculator.description) # the docstring
print(calculator.args) # {'expression': {'title': 'Expression', 'type': 'string'}}
Esa última línea es una salida real y verificada. LangChain examinó tu indicación de tipo (expression: str) junto con la documentación y creó un esquema a partir de ellas — este esquema es precisamente lo que el modelo lee para decidir si y cómo invocar la herramienta. ↔ tú creaste esto; anteriormente escribías manualmente las descripciones de las herramientas dentro de tu SYSTEM_PROMPT y analizabas tú mismo la salida del modelo. Ahora, la propia documentación se convierte en la descripción, y el análisis ocurre automáticamente. Esto explica por qué las documentaciones y las indicaciones de tipo tienen un peso real — no son solo documentación, sino que definen cómo el modelo entiende y utiliza la herramienta. Una documentación descuidada genera un modelo que llama a la herramienta de forma incorrecta.
Tu turno: Sustituye la documentación de la calculadora por algo inútil, como """hace cálculos matemáticos""", y luego verifica nuevamente .description. En el Paso 4 verás de primera mano cómo una documentación más débil conduce a decisiones peores en la selección de herramientas por parte del modelo. La descripción que escribas funciona como tu volante para dirigir el comportamiento del modelo.
Paso 4 — Agentes en una sola llamada
Aquí es donde el trabajo anterior da sus frutos. El agente que construiste manualmente requería un bucle, una zona de trabajo temporal, un analizador, manejo de errores, un límite de pasos y una instrucción del sistema que enseñaba al modelo el formato ReAct. En LangChain 1.x, todo eso se reduce a una sola llamada a función: create_agent.
from langchain.agents import create_agent
agent = create_agent(
model=llm, # your ChatOllama from Step 1
tools=[calculator, get_today], # the @tool functions from Step 3
system_prompt="You are a helpful assistant. Use tools for math and dates.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What is 8 times 12, and what is today's date?"}]}
)
print(result["messages"][-1].content)
Ese es el agente completo, de principio a fin. ↔ tú lo construiste todo — cada parte. create_agent ejecuta internamente el ciclo razonar-actuar-observar, envía la tarea a la herramienta adecuada, introduce las observaciones de vuelta en el modelo, verifica la condición de parada y aplica el límite de pasos; es decir, todas las partes que tú mismo integraste dentro de run_agent. Bajo el capó, depende de LangGraph, y esa es la razón por la cual el comportamiento en bucle es tan sólido.
Si deseas ver cómo se desarrolla el razonamiento —el mismo efecto que te proporcionó la opción verbose=True— transmite los pasos intermedios en lugar de esperar únicamente una respuesta final:
inputs = {"messages": [{"role": "user", "content": "How much is a year of Nimbus Pro?"}]}
for chunk in agent.stream(inputs, stream_mode="updates"):
print(chunk)
Mientras se ejecuta, verá cada decisión que toma el modelo y cada resultado que devuelve una herramienta, impresos uno tras otro. Es el mismo patrón de Pensamiento/Acción/Observación del seguimiento manual, solo que se presenta como objetos de actualización estructurados en lugar de texto sin procesar que tenía que analizar usted mismo.
Tu turno: Intenta hacer una pregunta que obligue al modelo a combinar dos herramientas: por ejemplo, pídele que calcule la fecha de cierre de un plazo de reembolso de 30 días sabiendo que la prueba comenzó hoy. Observa si llama correctamente a get_today y luego a calculator en secuencia. Después, vuelve al paso 7 del material sobre agentes: todos los modos de fallo que documentaste allí (formato de salida incoherente, nombres de herramientas inventados, bucles infinitos) pueden aparecer también aquí. El marco de trabajo no mejora el razonamiento de un modelo débil; simplemente oculta la lógica subyacente. Comprender esa diferencia es precisamente por lo que serás más rápido al depurar estos agentes que alguien que vaya directo al marco de trabajo sin construir uno primero.
Paso 5: Integrarlo todo: un agente que recupera información (RAG agencial)
Aquí es donde convergen las tres lecciones anteriores. Toma a tu perro recuperador y úsalo como una herramienta, luego entrega esa herramienta al agente. A partir de este momento, el propio agente decide cuándo se necesita una búsqueda de documentos; puede buscar varias veces o combinar la recuperación con cálculos según sea necesario.
@tool
def search_nimbus_docs(query: str) -> str:
"""Search the Nimbus product documentation for facts about plans, pricing, refunds, security, and support."""
docs = retriever.invoke(query)
return "\n\n".join(d.page_content for d in docs)
smart_agent = create_agent(
model=llm,
tools=[search_nimbus_docs, calculator, get_today],
system_prompt=(
"You answer questions about the Nimbus app. "
"Use search_nimbus_docs for any product facts, and calculator for arithmetic. "
"Base answers only on retrieved facts."
),
)
q = "How much would Nimbus Pro cost a team of 4 for a full year?"
result = smart_agent.invoke({"messages": [{"role": "user", "content": q}]})
print(result["messages"][-1].content)
Para responder correctamente a una pregunta como esta, el agente debe primero buscar el precio de la suscripción mensual y solo después calcular 8 * 12 * 4. Se trata de la recuperación (del primer tutorial) presentada como una herramienta (del tercero), gestionada por un agente (del segundo): tres conceptos separados que funcionan como un único sistema. Permitir que el agente elija cuándo realizar la recuperación es considerablemente más flexible que el enfoque de ruta fija rag_chain del Paso 2, y es un patrón que aparece con frecuencia en los sistemas reales de producción.
Tu turno: Transmite también la ejecución de este agente utilizando smart_agent.stream(..., stream_mode="updates"), y confirma el orden de las operaciones: la búsqueda se realiza antes del cálculo. Si tu modelo local intenta resolver las operaciones aritméticas por su cuenta en lugar de usar la herramienta de calculadora (una tendencia común en modelos más pequeños), refuerza la instrucción del sistema con algo como "DEBES usar la calculadora en cada paso aritmético." Es el mismo remedio que funcionó en el tutorial de los agentes.
Paso 6: Un breve recorrido por lo demás que incluye el paquete
A estas alturas ya tienes el esqueleto básico listo. Hay algunos componentes adicionales de LangChain que vale la pena conocer, cada uno relacionado con algo que ya has creado manualmente:
- Cargadores de documentos (
langchain-community) — permiten incorporar PDFs, páginas web, páginas de Notion y fuentes similares directamente en los mismos objetosDocumentque ya utiliza su herramienta de división de texto. Esto sustituye el pegado manual de texto en una lista por una incorporación real y estructurada. - Almacenes de vectores de nivel profesional — reemplacen el almacén en memoria por Chroma o FAISS (importados mediante
from langchain_chroma import Chroma) para guardar los embeddings en disco y escalar más allá de lo que permite la memoria. Dado que ambos ofrecen la misma interfaz.as_retriever(), nada en las etapas posteriores de la cadena necesita cambiar; esa consistencia es precisamente el objetivo de este cambio.
create_agent ya no es suficiente (rutas ramificadas, pasos de aprobación con intervención humana, múltiples agentes cooperando), se recurre a LangGraph, el motor de grafos de nivel inferior sobre el cual está construido create_agent.Paso 7: Cuándo usar LangChain y cuándo no (una opinión honesta)
En este punto ya has creado el mismo tipo de sistema dos veces: una desde cero y otra con un framework, lo que te coloca en una buena posición para realizar esta llamada por tu cuenta. Ese era el objetivo de probar ambas versiones.
LangChain resulta útil cuando tienes que combinar muchas integraciones existentes: varios cargadores de documentos, múltiples almacenes vectoriales y más de un proveedor de modelos, y no deseas implementar personalmente la lógica de transmisión en tiempo real, agrupamiento, intentos repetidos y seguimiento para cada una de ellas. También es útil cuando esperas cambiar los componentes con frecuencia y deseas una interfaz estable para hacerlo, o cuando estás creando un agente y prefieres no mantener tú mismo el bucle de razonamiento.
Escribir las cosas a mano suele ser la mejor opción cuando la aplicación es lo suficientemente pequeña como para que aprender las abstracciones de LangChain lleve más tiempo del necesario para simplemente escribir las cincuenta líneas o así que ya sabes cómo redactar. También es la mejor opción cuando necesitas visibilidad total de lo que se está ejecutando: avanzar a través de las capas del framework para depurar un problema representa una verdadera fuente de dificultades, y esa queja es justificada; o cuando agregar una capa de indirección ocultaría una lógica que en realidad es más clara si se escribe en Python puro. El pipeline RAG y el agente que construiste a mano en los tutoriales anteriores son completamente válidos para su uso en entornos de producción; el hecho de utilizar un framework no hace que el código escrito a mano sea inferior.
No existe una respuesta correcta única en este caso. La razón por la que primero aprendiste la versión manual es para que elegir adoptar este framework sea una decisión deliberada tomada con pleno conocimiento de lo que reemplaza, y no una opción por defecto a la que recurrir porque sus mecanismos internos son un misterio.
Paso 8: ¿A dónde dirigirse ahora?
- Si tu configuración local resulta demasiado lenta, existen planes gratuitos de hospedaje de modelos disponibles en Groq y Google Gemini. El cambio consiste únicamente en reemplazar
ChatOllamaporChatGroq, o utilizarinit_chat_model("gemini-...", model_provider="google_genai"); todo lo demás funciona a través de la misma interfaz estándar. Necesitarás una clave API para cualquiera de ellos, pero sus planes gratuitos no cuestan nada.
AgentExecutor, initialize_agent o LLMChain, elementos que desde entonces han sido movidos o descontinuados.El modelo mental que vale la pena mantener
LangChain es, en esencia, un conjunto de componentes que usted mismo construye y estandariza bajo una única interfaz — Runnable — conectados mediante |. Nada de lo que contiene representa realmente una idea nueva una vez que ha creado usted mismo las piezas necesarias:
- Una cadena representa el flujo de prompt a modelo a analizador que ya escribió, simplemente conectado entre sí.
- Un recuperador es su lógica de incrustación y búsqueda por coseno, envuelta en una interfaz común.
- Una herramienta es una función que usted escribió junto con un esquema generado automáticamente, lo que permite al modelo llamarla directamente en lugar de que usted tenga que analizar su salida de texto.
- Un agente, a través de
create_agent, representa todo su ciclo de razonar, actuar y observar, condensado en una sola llamada.
Cuando algo falla, lo depuras de la misma manera que siempre: aislas el componente defectuoso. Pruebas al recuperador por separado, imprimes los .args de una herramienta o visualizas los pasos intermedios del agente. El framework solo cambia la cantidad de código que debes escribir; no modifica lo que realmente ocurre, y tú ya entiendes qué está pasando.
Solución de problemas
- Si recibes un
ImportErroral usarcreate_agentolangchain_ollama, es probable que estés utilizando una versión anterior a 1.0 o que falte algún paquete. Ejecutapip install -U langchain langchain-ollamay verifica quelangchain.__version__comience con1..
AgentExecutor o initialize_agent, se trata de la API antigua. En la versión 1.x fue trasladada a langchain-classic; el código nuevo debe usar create_agent en su lugar.ollama serve o abre la aplicación, y verifica que tu modelo aparezca en ollama list.qwen2.5.HuggingFaceEmbeddings parece lento la primera vez que se utiliza porque descarga el modelo de embeddings (aproximadamente 80 MB) y lo almacena en caché localmente. La recuperación de datos es rápida después de eso.Ahora has creado RAG, agentes y el marco que integra ambos — primero a mano, luego con LangChain. Entiendes la capa que la mayoría de las personas solo accede desde el exterior. Diviértete utilizandolo.
Lecturas relacionadas
- Comprendiendo los agentes de IA: objetivos, herramientas, memoria y el bucle del agente — Una explicación adecuada para principiantes sobre cómo los agentes de IA difieren de los chatbots, que abarca sus componentes esenciales, el bucle de toma de decisiones, los niveles de autonomía y casos de uso en el mundo real.
- Límites estructurales para agentes de IA: dentro del pipeline ResolveFlow — Explica cómo un agente basado en LangGraph impone una separación entre el razonamiento y la ejecución a través de verificaciones a nivel de código en lugar de instrucciones en los prompts, incluyendo un error de recuperación que surgió durante el proceso.