LangChain RAG: a working example with a model via a gateway
We build a simple RAG with LangChain: documents, chunks, vector search and a model answer via a gateway. We count what a question costs and trim wasted tokens.
5 min · · development · экономия
LangChain RAG in Python solves a typical task: you have your own documents and the model does not know them. Instead of pasting the entire knowledge base into the request, we find a few relevant fragments and send only those. Below we assemble a minimal working example: loading, splitting, index, search and a model answer through an OpenAI-compatible gateway. Along the way we count where tokens go. The code follows the LangChain documentation (the Retrieval and semantic search sections); check package versions against it.
LangChain RAG: what it consists of
The LangChain documentation splits indexing into four steps: load data into Document objects, split into fragments, turn fragments into vectors (embeddings) and store them in a vector store. At query time the question is also turned into a vector, and the nearest fragments are sent to the model as context. Splitting is needed for two reasons: big pieces are harder to search and waste tokens.
One constraint matters right away. The Zukko API reference has no embeddings endpoint (it has chat, Messages, Responses, a model list and others). So we compute vectors locally and use the cloud model through the gateway only for the answer. That way you pay the gateway only for questions, not for indexing.
Installation
pip install langchain langchain-openai langchain-text-splitters langchain-huggingface sentence-transformers
export ZUKKO_API_KEY="fetch_your_key"Put a few text or Markdown files into a knowledge/ folder.
The index: load, split, vectors
from pathlib import Path
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
docs = [
Document(page_content=p.read_text(encoding="utf-8"), metadata={"source": p.name})
for p in Path("knowledge").glob("*.md")
]
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
chunks = splitter.split_documents(docs)
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)
store = InMemoryVectorStore(embedding=embeddings)
store.add_documents(chunks)
retriever = store.as_retriever(search_kwargs={"k": 4})What matters here:
- According to the LangChain documentation,
RecursiveCharacterTextSplitteris a sensible default: it tries to keep paragraphs, then sentences.chunk_sizehere is in characters, andchunk_overlapis the overlap between neighboring fragments. Pick the fragment size on your own questions: fragments that are too small lose context, too large ones waste tokens. - For Russian text the embedding model should be multilingual. We took a multilingual model from the sentence-transformers family as an example; compare several on your own texts.
InMemoryVectorStorekeeps everything in memory and suits an example. For a persistent base pick a store from the LangChain integrations list.- If you do not want to download an embedding model, there is a lighter option:
BM25Retriever.from_documents(chunks, k=4)fromlangchain_community.retrievers(it needs therank_bm25package). It searches by words without morphology, so for Russian text it misses word forms; test quality on your own questions.
The model answer through the gateway
The cloud model plugs in as ChatOpenAI with the gateway address. The base for OpenAI clients ends with /v1:
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-6.1-sol",
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"],
max_tokens=600,
)
SYSTEM = (
"Answer only from the given context. "
"If the context has no answer, say 'I don't know'. "
"At the end list the source numbers in square brackets."The "answer only from the context" instruction and permission to say "I don't know" lower the risk of invented answers but do not remove it: test with questions that have no answer in the base. A detailed look at a scheme without extra components is in a RAG pipeline by hand. Connecting the gateway to LangChain is described here: LangChain and base_url.
Where tokens go
You pay for tokens only at the last step. One question consists of:
- the system instruction (tens of tokens);
- the retrieved context:
kfragments ofchunk_sizecharacters, usually the heaviest part; - the question itself;
- the model reply (capped by
max_tokens).
Say a question uses 1,800 input tokens (about 1,500 of context, the rest instruction and question; measure the exact number on your own texts) and 300 output tokens. Formula: input × input price / 1M + output × output price / 1M. Prices as of October 18, 2026:
| Model | In / out, $ per 1M | Math (micro-dollars) | One question | 10,000 questions |
|---|---|---|---|---|
gpt-6.1-sol | 0.27 / 1.32 | 1800 × 0.27 + 300 × 1.32 = 486 + 396 | $0.000882 | $8.82 |
claude-sonnet-5-5 | 0.4 / 2 | 720 + 600 | $0.00132 | $13.20 |
deepseek-v4.1-flash | 0.013 / 0.038 | 23.4 + 11.4 | $0.0000348 | $0.348 |
For comparison, recall the "put the whole document into the prompt" approach. With a 200,000-token document on gpt-6.1-sol, one question costs 200,000 × 0.27 / 1M = $0.054, and 10,000 questions cost $540. RAG pays off exactly when there are many documents or many questions; for a one-off job on a single file a long context may be simpler. For Claude in the OpenAI format the whole input is billed at the input price.
How to cut spending
- Reduce
kandchunk_size. Every extra fragment is paid for on every question. Start withk=3and check quality on 20 to 30 real questions. - Remove duplicates. Because of overlap, neighboring fragments repeat text; do not insert both in a row if they cover the same thing.
- Cap the answer.
max_tokensand a request to answer briefly cut output tokens, which cost more than input ones. - Use a cheap model for simple questions. If the context already contains the answer, a small model is often enough. Test on your data, and the "cheap first, escalate the hard ones" scheme is described in the article on model cascades.
- Do not drag the whole history along. In a chat pass the latest turns, not the entire dialog.
- Cache. If you have a long repeating prefix (instructions, examples), then for GPT models the gateway sees cache reads in both formats, and that part is billed at the read price. For Claude the cache discount comes only in the Anthropic format. More: cache and key limits.
FAQ
Do I need a separate embeddings service for RAG?
The Zukko gateway does not provide an embeddings endpoint, so the example computes them locally. If you already have an embeddings service, connect it through the matching LangChain class.
Which model should I take for the answer?
Go by price, context and strength index: gpt-6.1-sol has 52, claude-sonnet-5-5 has 48, deepseek-v4.1-flash has 39. Whether a cheap model is enough will be shown by a test on your questions.
How do I know search works?
Print the retrieved fragments next to the answer and check whether the answer is in them. If not, the issue is the splitting or the embeddings, not the model.
Can I replace InMemoryVectorStore?
Yes, any LangChain vector store has as_retriever, and the rest of the code does not change.
Create a key in the dashboard, give it a daily limit and run the example.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Chatbot memory: how to store conversation history without paying twiceThe model does not remember earlier messages: you send the history with every request. We look at how dialogue cost grows and what keeps it in check.
- LangChain text splitters: how to cut text into chunks correctlyHow to choose chunk size and overlap in RecursiveCharacterTextSplitter, why you should count in tokens and how to check the count against an exact token number.
- opencode agents: setting up roles, models and toolsA custom agent in opencode is a role, a model and a set of permissions. A working config where a cheap model handles routine work and a stronger one edits code.
- Plan mode in Claude Code: plan first, then edit the codeIn plan mode Claude Code reads your code and proposes a plan but edits nothing until you agree. How to enable it, use it and what it costs.