LangChain Project: Document Q&A Agent

This LangChain project combines everything from the tutorial into one app: a command-line assistant that answers questions about your own documents, runs fully locally with Ollama, remembers the conversation, and cites its sources.

Step 1: Index the Documents

from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma

docs = DirectoryLoader("docs/", glob="**/*.pdf", loader_cls=PyPDFLoader).load()
chunks = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=120).split_documents(docs)
store = Chroma(collection_name="kb", embedding_function=OllamaEmbeddings(model="nomic-embed-text"),
               persist_directory="./kb_db")
store.add_documents(chunks)
print(f"Indexed {len(chunks)} chunks from {len(docs)} pages")
Sample output:
Indexed 143 chunks from 52 pages

Step 2: A Retrieval Tool

from langchain.tools import tool

@tool
def search_docs(query: str) -> str:
    """Search the company documents. Use for any question about policies or products."""
    hits = store.similarity_search(query, k=4)
    return "\n\n".join(
        f"[{d.metadata.get('source')} p.{d.metadata.get('page', 0) + 1}] {d.page_content}"
        for d in hits
    )
(no output: defines the tool)

Putting source and page into the tool output lets the model cite them. Adding 1 converts the zero-based page number for humans.

Step 3: The Agent with Memory

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
    model="ollama:qwen3",
    tools=[search_docs],
    system_prompt=(
        "You answer questions about company documents. Always call search_docs first. "
        "Answer only from the results and cite sources like [file p.N]. "
        "If the answer is not there, say you don't know."
    ),
    checkpointer=InMemorySaver(),
)
(no output: creates the agent)

Step 4: A Streaming Chat Loop

cfg = {"configurable": {"thread_id": "cli"}}
while True:
    q = input("\nYou: ")
    if q in {"exit", "quit"}:
        break
    print("Bot: ", end="")
    for token, meta in agent.stream({"messages": [{"role": "user", "content": q}]}, cfg, stream_mode="messages"):
        if meta.get("langgraph_node") == "model" and token.content:
            print(token.content, end="", flush=True)
Sample session:
You: How many leave days do I get?
Bot: Full-time employees get 20 days of paid leave per year [docs/handbook.pdf p.4].
You: Can I carry them over?
Bot: Up to 5 unused days can be carried into the next year [docs/handbook.pdf p.5].

Ideas to Extend the Project

  • Turn on LangSmith tracing and build a small evaluation dataset.
  • Swap InMemorySaver for SqliteSaver so memory survives restarts.
  • Add a second tool, for example a calculator or a web search.
  • Wrap the agent in FastAPI or Streamlit for a web UI.
  • Try init_chat_model with a hosted provider and compare answer quality.
  • Port it to LangChain.js with the same concepts.

Frequently asked questions

What is a good first LangChain project?

A chatbot over your own documents is ideal: it touches loaders, splitters, embeddings, vector stores, tools, agents and memory.

Why use an agent instead of a fixed RAG chain?

An agent can decide whether to search, search several times with different queries, or answer follow-ups from memory. A fixed chain is simpler and faster when every question needs exactly one search.

How do I deploy a LangChain app?

Wrap it in a web framework like FastAPI, use persistent storage for the vector store and checkpointer, and run Ollama or a hosted model behind it. LangGraph Platform is another option for agents.