Inicio / Artículos

Notas prácticas: Crear un asistente de IA para empresas con RAG, Agents y LangGraph

Guía paso a paso práctica: Cómo crear un asistente de IA para empresas con RAG, Agents y LangGraph: contratos, verificaciones y espacios de código listos para usar para los equipos que implementan este patrón.

Editor: REACTAPP.TOP11 min de lectura2601 palabras

Fuentes: Building a Company AI Assistant with RAG, Agents, and LangGraph

Las notas siguientes reconstruyen un camino práctico para “Crear un asistente de IA empresarial con RAG, Agents y LangGraph”. Se da énfasis en los contratos, las verificaciones y los marcadores de posición para código, en lugar de en un enfoque motivacional. Al trabajar en la sección de resumen, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de un fallo parcial. Esa lista de verificación ayuda a mantener honestas las futuras modificaciones del código. Prefiera unidades pequeñas y probables sobre scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado.

rag-agent-project/
│
├── documents/
├── faiss_index/
│
├── .env
├── .gitignore
├── requirements.txt
│
├── config.py
├── ingest.py
├── rag.py
├── query_rewriter.py
├── reranker.py
├── tools.py
├── graph.py
├── main.py
└── app.py
pip install -r requirements.txt
.env
OPENAI_API_KEY=your_openai_api_key
import os
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

if not OPENAI_API_KEY:
    raise ValueError("OPEN_API_KEY is not set."
                     "Add it to yor .env file.")

LLM_MODEL = "gpt-4.1-mini"

EMBEDDING_MODEL = "text-embedding-3-small"

HUGGINGFACE_EMBEDDING = "sentence-transformers/all-MiniLM-L6-v2"
FAISS_PATH = "faiss_index"

DATABASE_PATH = "company_assistant.db"
documents/
├── employee_handbook.pdf
├── hr_policy.pdf
└── it_policy.pdf
from pathlib import Path

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings

from config import HUGGINGFACE_EMBEDDING , FAISS_PATH


DOCUMENTS_DIR = Path("documents")


def load_documents():

    all_documents = []

    pdf_files = list(DOCUMENTS_DIR.glob("*.pdf"))

    if not pdf_files:
        raise FileNotFoundError(
            "No pdf file found in documents/"
        )

    for pdf_file in pdf_files:

        print(f"Loading: {pdf_file}")

        loader = PyPDFLoader(str(pdf_file))
        documents = loader.load()
        for document in documents:
            document.metadata["source"] = pdf_file.name
            if "handbook" in pdf_file.name.lower():
                document.metadata["document_type"] = "handbook"

            elif "policy" in pdf_file.name.lower():

                document.metadata["document_type"] = "policy"

            else:

                document.metadata["document_type"] = "other"

            all_documents.append(document)

    return all_documents


def main():
    # 1. Load PDFs
    documents = load_documents()

    print(f"\nLoaded {len(documents)} pages")

    # 2. Split documents
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=100
    )

    chunks = text_splitter.split_documents(documents)

    print(f"Split into {len(chunks)} chunks.")

    # 3. Create embeddings
    embeddings = HuggingFaceEmbeddings(
        model_name=HUGGINGFACE_EMBEDDING
    )

    # 4. Create FAISS database
    vector_store = FAISS.from_documents(
        chunks,
        embeddings
    )


    # 5. Save FAISS database
    vector_store.save_local(FAISS_PATH)

    print(
        f"\nFAISS index saved to: {FAISS_PATH}"
    )


if __name__ == "__main__":
    main()
Loading: documents/employee_handbook.pdf
Loading: documents/hr_policy.pdf
Loading: documents/it_policy.pdf
Loaded 20 pages.
Created 60 chunks.
FAISS index created successfully!
Chunk 1
-------------------------
A B C D E F G H I J
              |
              | overlap
              v
Chunk 2
              E F G H I J K L M N
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from config import(
    HUGGINGFACE_EMBEDDING,
    FAISS_PATH
)

embeddings = HuggingFaceEmbeddings(
        model_name=HUGGINGFACE_EMBEDDING
    )

vector_store = FAISS.load_local(
    FAISS_PATH,
    embeddings,
    allow_dangerous_deserialization=True
)
from rag import vector_store
from langchain_huggingface import (HuggingFaceEmbeddings,HuggingFacePipeline)
from transformers import pipeline

pipe = pipeline(
    "text-generation",
    model="Qwen/Qwen2.5-0.5B-Instruct",
    max_new_tokens=256,
    temperature=0.1
)

llm = HuggingFacePipeline(
    pipeline=pipe
)

def rewrite_query(question:str)-> str:
    prompt = f"""
Rewrite the user's question into a concise search query for company document database.

The query should contain important keywords that are likely to appear in company documents.

Do not answer the question.

User question:{question}

search query:
"""

    response = llm.invoke(prompt)

    return response
Chunk 1
Chunk 2
Chunk 3
Chunk 4
Chunk 5
from langchain_huggingface import (HuggingFaceEmbeddings,HuggingFacePipeline)
from transformers import pipeline

pipe = pipeline(
    "text-generation",
    model="Qwen/Qwen2.5-0.5B-Instruct",
    max_new_tokens=256,
    temperature=0.1
)

llm = HuggingFacePipeline(
    pipeline=pipe
)

def rerank_documents(query:str, documents:list,top_n: int = 3):

    scored_documents =[]

    for document in documents:
        prompt = f"""
You are evaluating document releveance.

Rate how relevant the document is to the user's question.

Return ONLY an intefer from 0 to 10.

0 = competely irrelevent
10 = extreamly relevent

Question: {query}

Document:{document.page_content}

"""
        response = llm.invoke(prompt)
        try:
            score = int(response.content.strip())
        except ValueError:
            score = 0

        scored_documents.append((score,document))

        #Highest score first
        scored_documents.sort(
            key=lambda item:item[0],
            reverse=True
        )

        return[
            document
            for score, document
            in scored_documents[:top_n]
        ]
from langchain.tools import tool
from rag import vector_store
from query_rewriter import(rewrite_query)

from reranker import (rerank_documents)


@tool
def company_document_search(query: str) -> str:
    """
    Search company PDF documents for information about

    company policies, employee benifits, leave, holidays, work from home,
    IT rules, working hours, and othher internal information.

    """
    #1. Rewrite Query
    imporved_query = rewrite_query(query)
    print(f"\n[Rewritten Query]"
          f"\n{imporoved_query}")

    #2.  Vector serach
    documents =  (vector_store.similarity_search(
        imporved_query,
        k=5
    ))

    if not documents:
        return(
            "No relevent information"
            "was found in the company documents"
        )

    #Reranking
    documents = rerank_documents(
        imporved_query, documents,top_n=3
    )

    #4.Formate context
    result = []
    for document in documents:
        source = document.metadata.get("source","Unknown")
        page = document.metadata.get("page","Unknown")

        #PyPDFLoader user zero-based pages
        if isinstance(page, int):
            page = page+1

        results.append(f"""
SOURCE:{source}
PAGE:{page}

CONTENT: {document.page_content}
""")
        return "\n".join(result)


@tool
def calculator(expression: str) -> str:
    """
    Calculate a mathematical expression.
    """
    try:

       # result = eval(expression)
       # for learning only
       # Do not use eval() in production

        allowed = ("0123456789""+-*/(). ")
        if not all(char in allowed for char in expression):
            return ("Invalid mathmatical expression")

        result = eval(expression,
                      {"__builtins__":{}},
                      {}
                      )

        return str(result)
    except Exception:
        return "Unable to calculate the expression."
from langchain_ollama import ChatOllama

from langgraph.graph import (
    StateGraph,
    MessagesState,
    START
)

from langgraph.prebuilt import (
    ToolNode,
    tools_condition
)

from langgraph.checkpoint.memory import InMemorySaver

from tools import (
    company_document_search,
    calculator
)

# Local Qwen model
llm = ChatOllama(
    model="qwen2.5:3b",
    temperature=0
)

# Tools
tools = [
    company_document_search,
    calculator
]


llm_with_tools = llm.bind_tools(tools)

# Agent

def agent_node(state: MessagesState):

    response = llm_with_tools.invoke(
        state["messages"]
    )

    return {
        "messages": [response]
    }

# Graph
graph_builder = StateGraph(MessagesState)


graph_builder.add_node(
    "agent",
    agent_node
)


graph_builder.add_node(
    "tools",
    ToolNode(tools)
)

# START → Agent
graph_builder.add_edge(
    START,
    "agent"
)

# Agent → Tool OR END
graph_builder.add_conditional_edges(
    "agent",
    tools_condition
)

# Tool → Agent
graph_builder.add_edge(
    "tools",
    "agent"
)

# Checkpointer
checkpointer = InMemorySaver()

# Compile

graph = graph_builder.compile(
    checkpointer=checkpointer
)
SOURCE: hr_policy.pdf
PAGE: 4
Source: hr_policy.pdf, page 4.
from langchain_core.messages import HumanMessage, SystemMessage
from graph import graph

SYSTEM_PROMPT="""
You are an intelligent company assistant.

You answer question using company documents and avalable tools.

Rules:
1. Use company_document_search whenever the user asks about company-specific information.

2. Never invent company policies,  benifits, rules, or  procedures.

3. Use the calculator when mathmatical calculation are required.

4. When company documents are used, mention the source document and page.

5. If the company documents do not contain the requested information, clearly say
that you could not find it.

6. Use previous conversation context to undersatnd follow-up questions.

7. Give concise and clear answer
"""

def chat(
        question:str,
        thread_id: str):
    config = {
        "configurable":{"thread_id":thread_id}
    }

    #Check whether thhis conversation already exists
    state = graph.get_state(config)

    if not state.values:
        messages = [
            SystemMessage(content=SYSTEM_PROMPT),
            HumanMessage(content=question)
        ]
    else:
        messages=[
            HumanMessage(content=question)]

    result = graph.invoke({
        "messages":messages},
        config=config
    )
    return result["messages"][-1].content

def main():
    print("===========================================")
    print("CoOMPANY AI ASSISTANT")
    print("===========================================")
    print("\nType 'exist' to quit.")
    thread_id = input("\nEnter conversation ID:")

    while True:
        question = input("\nYou: ")

        if question.lower() in [
            "exit",
            "quit"
        ]:
            break

        try:
            answer = chat(question,thread_id)
            print(f"\nAI: {answer}")

        except Exception as e:
            print(f"\nError: {e}")

if __name__ == "__main__":
    main()
import streamlit as st
from langchain_core.messages import (
    HumanMessage,
    AIMessage,
    SystemMessage
)

from graph import graph

# Page configuration
st.set_page_config(
    page_title="Company AI Assistant",
    page_icon="🤖",
    layout="centered"
)

# System prompt
SYSTEM_PROMPT = """
You are an intelligent company assistant.

You answer questions using company documents
and available tools.

Rules:

1. Use company_document_search whenever
   the user asks about company-specific
   information.

2. Never invent company policies,
   benefits, rules, or procedures.

3. Use the calculator when mathematical
   calculations are required.

4. When company documents are used,
   mention the source document and page.

5. If the company documents do not contain
   the requested information, clearly say
   that you could not find it.

6. Use previous conversation context to
   understand follow-up questions.

7. Give concise and clear answers.
"""

# Sidebar
st.sidebar.title("⚙️ Settings")

thread_id = st.sidebar.text_input(
    "Conversation ID",
    value="streamlit-user"
)

st.sidebar.markdown(
    """
### About

This application demonstrates:

- RAG
- AI Agents
- LangGraph
- FAISS
- Query rewriting
- Reranking
- Conversation memory
- Tool calling
"""
)

# Main UI
st.title("🏢 Company AI Assistant")

st.write(
    "Ask questions about company policies, "
    "benefits, leave, IT rules, and more."
)

# Chat history in Streamlit

if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# Display previous messages
for message in st.session_state.chat_history:

    with st.chat_message(
        message["role"]
    ):

        st.markdown(
            message["content"]
        )


# User input
question = st.chat_input(
    "Ask something about the company..."
)


if question:
    # Display user question
    with st.chat_message("user"):

        st.markdown(question)


    st.session_state.chat_history.append(
        {
            "role": "user",
            "content": question
        }
    )

    # Build messages
    config = {
        "configurable": {
            "thread_id": thread_id
        }
    }

    # Check whether this thread
    # already has state
    state = graph.get_state(
        config
    )
    if not state.values:

        messages = [
            SystemMessage(
                content=SYSTEM_PROMPT
            ),
            HumanMessage(
                content=question
            )
        ]

    else:
        messages = [
            HumanMessage(
                content=question
            )
        ]

    # Call LangGraph
    with st.chat_message("assistant"):
        with st.spinner(
            "Thinking..."
        ):
            try:
                result = graph.invoke(
                    {
                        "messages": messages
                    },
                    config=config
                )
                answer = result[
                    "messages"
                ][-1].content
                st.markdown(answer)

                # Save answer
                st.session_state.chat_history.append(
                    {
                        "role": "assistant",
                        "content": answer
                    }
                )
            except Exception as e:

                st.error(
                    f"Something went wrong: {e}"
                )

Lista de verificación operativa

Para la lista de verificación operativa, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto.

Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son ajustes realizados posteriormente.

Cite los pasajes que realmente sustentan la respuesta. Sin citaciones, los operadores no pueden distinguir entre alucinaciones y fallos en el indexado.

Punto de control después de pasos costosos. La plataforma de recuperación no debe volver a facturar la misma llamada al LLM cuando un operador intenta nuevamente un nodo posterior.

Fije las versiones de las dependencias y registre el resumen de la imagen que ejecutó la demostración. La reproducibilidad es mejor que el conocimiento basado en prácticas internas.

Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad temprana de los costos evita facturas inesperadas al pasar de la demostración a entornos compartidos.

Antes de promocionar la solución, congele las versiones, capture una transcripción de referencia para el camino crítico y confirme los pasos para revertir cambios. Los entornos compartidos requieren límites de velocidad, verificaciones de asignación y un responsable claro para la rotación de credenciales secretas. Prefiera una fiabilidad sencilla a demostraciones ingeniosas pero puntuales.

Nota para el lote 656e1cd26886: mantenga las claves del proveedor fuera del repositorio, establezca un límite de tokens por sesión y almacene las transcripciones junto con los archivos de evaluación para que los cambios posteriores en el modelo sigan siendo comparables.

Para la nota de fortalecimiento 0, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben encontrarse en un lugar que los operadores puedan auditar sin necesidad de leer todo el sistema.

Detalle de fortalecimiento 0/780: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en observaciones anecdóticas.

Al trabajar en la nota de fortalecimiento número 1, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Prefiera unidades pequeñas y verificables a scripts extensos. Cuando un paso falla, el fallo debe apuntar a una única responsabilidad en lugar de a un proceso complicado.

Detalle de fortalecimiento 1/780: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

La nota de fortalecimiento número 2 funciona mejor cuando se trata como una superficie medible. Capture una transcripción ejemplar, un caso de fallo y la nota de reversión antes de ampliar el alcance. Registre los tiempos y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana de los costos evita facturas inesperadas cuando el proceso pasa de la demostración a entornos compartidos.

Detalle de fortalecimiento 2/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Para la nota de fortalecimiento 3, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores.

Detalle de fortalecimiento 3/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Al trabajar en la nota de fortalecimiento número 4, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Trate esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina las comprobaciones de éxito y rechace las completaciones parciales silenciosas.

Detalle de fortalecimiento 4/780: mida el tiempo de ejecución, la clase del error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

La nota de fortalecimiento número 5 funciona mejor cuando se trata como una superficie medible. Capture una transcripción de referencia, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de secretos y las banderas de funcionalidad deben estar en un lugar que los operadores puedan auditar sin tener que leer todo el sistema.

Detalle de reforzamiento 5/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Para la nota de reforzamiento 6, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Prefiera unidades pequeñas y verificables sobre scripts extensos. Cuando un paso falla, el fallo debe apuntar a una única responsabilidad en lugar de a un proceso complicado.

Detalle de reforzamiento 6/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Al trabajar en la nota de fortalecimiento número 7, anote primero el contrato: los datos requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de la fase de demostración a entornos compartidos.

Detalle de fortalecimiento 7/780: mida el tiempo empleado, la clase del error y el gasto en tokens para esta nota, y luego decida si mantener la modificación basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

La nota de fortalecimiento número 8 funciona mejor cuando se trata como una superficie medible. Capture una transcripción ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación. Los intentos repetidos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son algo que se añade posteriormente.

Detalle de reforzamiento 8/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.

Para la nota de reforzamiento 9, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Trate esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina verificaciones de éxito y rechace las completaciones parciales silenciosas.

Detalle de reforzamiento 9/780: mida el tiempo de ejecución, la clase de error y el consumo de tokens para esta nota, y luego decida si mantener el cambio basándose en un conjunto fijo de preguntas en lugar de en anécdotas.