Skip to content
··8 min de lectura

Cómo salí de las arenas movedizas del código con un agente de IA

Me lancé a construir un chatbot sin experiencia en código y con mucho entusiasmo — solo para descubrir que mi v0.1 era un desastre de bases de datos CSV y chunking primitivo, hasta que los agentes de IA me sacaron del atolladero.

Actualización (2026): ¡Este chatbot evolucionó hasta convertirse en Sydney! Después de muchas iteraciones, Sydney ahora vive en /ask/ y se centra en el contenido del blog y los productos.


A principios de noviembre del año pasado, publiqué mi chatbot DIY versión 0.1. En ese momento escribí: "Aunque la v0.1 representó un gran primer paso como principiante en programación, tenía limitaciones significativas." Bueno, como resultó, eso se quedó corto. No apreciaba lo torpe que era todo el proceso y mi construcción. La realidad era que mi primer intento, aunque sincero, era más bien un prototipo armado con entusiasmo pero con conocimientos limitados.

Esta publicación no es solo un seguimiento; es una inmersión profunda en el viaje que se desarrolló desde ese punto — un viaje lleno de pruebas, errores y lecciones invaluables. Estoy exponiendo los detalles de esta aventura, no solo por transparencia sino con la esperanza de que mis experiencias, con todos sus detalles, puedan resonar o incluso ayudar a alguien que esté en un camino similar. (Para más contexto, soy un profesional de la publicidad de mediana edad sin experiencia previa en programación.)

Como mencioné, para publicar la v0.1 de mi chatbot, principalmente seguí las instrucciones de este curso corto "Building Systems with the ChatGPT API" y dos cookbooks de OpenAI: Question answering using embeddings-based search y How to count tokens with tiktoken.

Por qué mi chatbot v0.1 es terrible:

  • Chunking: Simplemente dividí las publicaciones largas del blog en trozos más pequeños basándome en la longitud del token, es decir, una división estática simple de datos por caracteres. ¡La forma más primitiva de dividir posible :D! Si quieres entender por qué esto es una idea terrible, lee sobre los 5 niveles de división de texto de Greg Kamradt aquí.
  • Embedding: Tuve dificultades con los embeddings. Usé el modelo de embedding de OpenAI, pero seguía alcanzando los límites de solicitudes de la API, lo que hacía que el proceso de embedding fallara a mitad. Luego aprendí a hacer solicitudes por lotes y a agregar tiempos de espera entre lotes para evitar los límites. Al final guardé los embeddings generados en un simple archivo .csv como mi improvisada "base de datos".
  • Base de datos: Sabía que un CSV no era óptimo como base de datos, pero me faltaban las habilidades para mejores alternativas.
  • Metadatos: Inicialmente no me di cuenta de que incluir metadatos como fechas de publicación y URLs de las publicaciones era importante para que los chatbots respondieran con precisión a las preguntas de los usuarios. Tuve que repetir el proceso de embedding y guardado para incorporar metadatos relevantes.
  • Retriever: No era consciente de los diferentes tipos de retrievers y algoritmos. Simplemente usé la búsqueda por relevancia de OpenAI para recuperar un número fijo de resultados.
  • Memoria: Para mantener una conversación, el chatbot necesita recordar lo que el usuario dijo anteriormente. Y aquí, con una ventana de contexto limitada de gpt-3.5 (en ese momento), había un claro equilibrio entre el tamaño del chunk y la cantidad de resultados que querías recuperar.
    • Por ejemplo, si el tamaño del chunk es de 800 tokens y el retriever devuelve los 8 primeros resultados, son 6.400 tokens o más del 50% del límite del antiguo modelo.
    • Lo anterior es solo 1 pregunta, así que imagínate una conversación de múltiples turnos y cómo la memoria puede llenarse muy rápidamente.
    • Una forma de resolver este problema es tener un tamaño de chunk más pequeño y que el retriever devuelva menos resultados, pero con un retriever básico (como el anterior), esto significa que el modelo no tiene información completa para responder la pregunta.
  • Ni siquiera usé ningún IDE. Todo el código fue editado usando TextEdit en Mac :D (¿Ya te dije que era un novato? :P)
  • Podría seguir y seguir, pero creo que ya entiendes el panorama.

Mi "valle de la muerte"

Ansioso por mejorar más allá de las limitaciones de la v0.1, intenté varios cursos en línea, esperando que me proporcionaran las piezas que faltaban para mejorar mis habilidades. Pero el progreso solo llevó a callejones sin salida.

Luché con los ejercicios de bases de datos vectoriales (Vector Databases: from Embeddings to Applications with Weaviate), métodos evaluativos de RAG (Building and Evaluating Advanced RAG Applications con Llama-Index y Truera), y técnicas avanzadas de recuperación (Advanced Retrieval for AI with Chroma). Por más que lo intenté, no podía conectar la teoría con la aplicación práctica usando los datos de mi propio blog.

¿Estaban mal diseñados los cursos? No — la deficiencia era mi propia falta de conocimiento base. Aun así, fracaso tras fracaso fue sumamente frustrante y desmoralizador. Me encontré en un valle figurativo, sin saber cómo seguir adelante.

Victorias incrementales con el tiempo

Sin embargo, de los fracasos repetidos sí surgieron pepitas de valor:

  • Adopción de VS Code en lugar de TextEdit

  • Aprovechar las extensiones de GitHub Copilot

  • Apreciar Jupyter Notebook para los entornos de desarrollo

El último curso mencionó LangChain, un popular nuevo framework para construir chatbots. En realidad intenté los tutoriales de LangChain hace meses ("LangChain: Chat with Your Data" y "Functions, Tools and Agents with LangChain") sin mucho éxito. Pero ahora, con los conocimientos arduamente ganados bajo mi cinturón, revisitar su documentación resultó iluminador. Los conceptos encajaron y su arquitectura modular tuvo sentido intuitivo.

Podía imaginar adaptar las robustas capacidades de LangChain a mi proyecto de pasión. ¡Finalmente se reveló un camino a seguir! Poco a poco, me familiaricé con sus tuberías para la ingesta de datos, embedding, almacenamiento y recuperación.

Mi confianza creció con cada pieza que logré implementar. La v2 comenzó a tomar forma...

Reconstruyendo los cimientos

Con LangChain como guía, me puse a reconstruir mi chatbot desde cero:

Ingesta de exportaciones de WordPress a JSON

Después de algunas pruebas y errores ajustando los parámetros de ingesta, el JSONLoader de LangChain analizó correctamente mis publicaciones exportadas. Ahora la entrada validada podía alimentar las tuberías posteriores. (El código para este paso está en el archivo "DataIngestionAndIndexing.ipynb" en este repositorio público de Github.)

División de texto automatizada

Mi ingenua división por longitud de token fue reemplazada por los SentenceTransformers de LangChain, que usan NLP avanzado para dividir unidades semánticas. ¡No más frases incompletas cortadas a mitad! Las configuraciones mantenían los trozos con un tamaño apropiado para los modelos con restricciones de memoria.

from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
    chunk_overlap=0,  # Overlap between chunks
    tokens_per_chunk=256  # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: \{len(all_splits)\}")

Generación de embeddings e índices

Las dificultades pasadas con los límites de la API de OpenAI desaparecieron usando el wrapper de LangChain para los embeddings de OpenAI. Integrados en dos líneas de código, los embeddings extrajeron limpiamente las características relevantes del texto dividido.

Para el vector store, opté por FAISS (Facebook AI Similarity Search) en lugar de Weaviate o Chroma. El FAISS probado en la industria logró el equilibrio adecuado entre capacidad y complejidad para mis necesidades. Su versión de CPU indexó rápidamente los chunks del blog, generando una base de datos compacta y búsqueda rápida. Ya no necesito preocuparme por los lotes ni por alcanzar el límite de solicitudes de la API de OpenAI.

LangChain admite múltiples vector stores, así que puedes consultarlos aquí.

# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)

# Save the vector store locally
db.save_local("path/to/save/faiss_index")  # Placeholder for save path/ index name

¡Dos líneas de código! Eso es todo.

También noté que si tu contenido no es pequeño (yo tengo más de 400 publicaciones de blog), no deberías intentar usar el retriever inmediatamente después del embedding porque los índices necesitan un poco de tiempo para completarse/estabilizarse.

Evaluación de Retrievers

Configurar este retriever requiere solo 1 línea de código :D, usando FAISS como vector store.

retriever = db.as_retriever(search_type="mmr")

¡Con mi índice y retriever listos, tenía tuberías de datos preparadas para alimentar un chatbot inteligente!

Arquitectura de Agentes Conversacionales

Decidí usar el framework de agentes de LangChain para construir este chatbot. ¿Es exagerado en este punto? Sí. Pero mi esperanza es que con el tiempo, pueda evolucionar este chatbot y darle más "herramientas", es decir, funcionalidades. LangChain hace que sea muy fácil configurar el agente y darle herramientas para usar.

embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings)  # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
    retriever,
    "search_your_blog",  # Replace "search_your_blog" with a descriptive name for your tool
    "Your tool description here"  # Provide a brief description of what your tool does
)

tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
    # Customize the prompt template according to your chatbot's persona and requirements
])

llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
    \{
        "input": lambda x: x["input"],
        "agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
        "chat_history": lambda x: x["chat_history"],
    \}
    | prompt_template
    | llm_with_tools
    | OpenAIToolsAgentOutputParser()
)

agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

El código Python completo y final para el agente

Por último, si quieres probar el chatbot v2, aquí está.

¿Es extraño que el chatbot no sepa nada sobre ti, Chandler?

P.D.: Gracias a algunos de vosotros que os pusisteis en contacto para hacerme saber que el chatbot no sabe nada sobre mí. Y. ¡tenéis razón! Es porque olvidé exportar la página "About" y solo exporté las "publicaciones publicadas". Esta es la segunda vez que olvido hacer esto, así que incluiré preguntas básicas sobre mí en la lista de preguntas de evaluación. ¡Lección aprendida!

Gracias a todos por compartir sus comentarios constructivos. Seguid haciéndolo. Y sí, sé que el chatbot es muy lento al inicio, así que también estoy trabajando en eso. :| (¿Ya te dije que era un novato? :P)

Una actualización rápida

El problema de que el chatbot no supiera nada sobre mí ya está resuelto. Esto es lo que hice y aprendí:

  • Exportar la página "About me" de Wordpress a .XML como se indica arriba.
  • Realicé la división de texto y generé embeddings usando FAISS como se indica arriba. Guardé el vector store con un nombre diferente localmente para probar el retriever
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")

  • Como resultó, el proceso para fusionar dos vector stores de FAISS es sorprendentemente simple, según la documentación aquí.
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
    print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
  • Después de eso, el proceso es prácticamente el mismo que el anterior, usando el nuevo vector store

Actualización del 14 de febrero: Chatbot v2.10 presentado

Dos semanas después de este despliegue del chatbot, presenté la versión 2.10 que eleva la experiencia del usuario con velocidad, escalabilidad y simplicidad mejoradas. Puedes leer más al respecto aquí.

Actualización del 25 de marzo: De mejoras de frontend a las dificultades con Docker y los avances

Puedes leer más al respecto aquí.

Seguir leyendo

Mi Trayectoria
Conectar
Idioma
Preferencias