AIWave API

LlamaIndex + DeepSeek: Building RAG on Chinese AI Models

Running LlamaIndex with DeepSeek and other Chinese AI models needs no custom integration. LlamaIndex ships an OpenAILike LLM class specifically for OpenAI-compatible endpoints, so DeepSeek, GLM, Kimi and Qwen slot into every index, retriever and query engine you already use. This guide builds a working RAG pipeline end to end, explains the two configuration mistakes that account for most failures, and does the cost arithmetic that makes long context affordable in the first place.

Get an API key →Compare live pricing →

Why LlamaIndex needs OpenAILike, not OpenAI

LlamaIndex’s default OpenAI class validates model names against a hard-coded registry of OpenAI models. Pass deepseek-v4-pro and it raises before it ever makes a request. The OpenAILike class exists precisely for this case: it skips the registry, sends the request as-is, and lets you declare the model’s properties yourself.

This is the single most common stumbling block, and it produces a confusing error message, so it is worth stating plainly: use OpenAILike, declare is_chat_model=True, and set context_window to the real value.

Step 1: Install and configure the LLM

pip install llama-index llama-index-llms-openai-like llama-index-embeddings-huggingface
import os
from llama_index.llms.openai_like import OpenAILike

llm = OpenAILike(
    model="deepseek-v4-pro",
    api_base="https://aiwave.live/v1",      # note: api_base, not base_url
    api_key=os.environ["AIWAVE_API_KEY"],
    is_chat_model=True,                     # required, or completions format is used
    context_window=128000,                  # declare it; LlamaIndex uses this for chunking
    temperature=0.1,
)

print(llm.complete("Summarise mixture-of-experts routing in one sentence."))

Get a key from the console — email or GitHub, no Chinese phone number. The endpoint contract is documented in the API reference, and authentication covers key handling.

Step 2: Choose embeddings deliberately

RAG has two model decisions, not one. The LLM generates; the embedding model decides what the LLM ever sees. A weak retriever cannot be rescued by a strong generator — if the right chunk is not in the top-k, no amount of reasoning recovers it.

Local embeddings are often the right default. They cost nothing per query, keep your corpus on your own machines, and remove a network round trip from every retrieval:

from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import Settings

Settings.llm = llm
Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-small-en-v1.5"     # 384-dim, fast, runs on CPU
)

For Chinese or mixed-language corpora, BAAI publishes multilingual variants in the same family; see the BAAI model collection. If you prefer a hosted embedding endpoint instead, the embeddings API documents the server-side option.

Step 3: Build the index

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex

documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist("./storage")   # build once, reuse

Persist the index. Rebuilding embeddings on every process start is the most common source of “why is startup so slow” in LlamaIndex projects, and with local embeddings it is pure wasted CPU.

from llama_index.core import StorageContext, load_index_from_storage

storage = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage)

Step 4: Query it

engine = index.as_query_engine(similarity_top_k=5)
response = engine.query("What changed in the v4 release?")

print(response)
for node in response.source_nodes:
    print(f"{node.score:.3f}  {node.metadata.get('file_name')}")

Always print the source nodes during development. Most “the model hallucinated” reports turn out to be retrieval failures: the answer was never in the context. Inspecting scores tells you immediately whether the problem is the retriever or the generator.

Step 5: Stream the answer

RAG answers are slow because the prompt is long. Streaming hides most of that latency behind the first token.

engine = index.as_query_engine(streaming=True, similarity_top_k=5)
streaming_response = engine.query("Explain the migration path.")
streaming_response.print_response_stream()

The chunk format is the standard server-sent-event stream documented in streaming responses.

Step 6: Control what long context actually costs

A 128K context window is a budget, not a target. Every retrieved chunk is input tokens you pay for on every single query, so similarity_top_k is a cost dial as much as a quality dial.

Current rates for models commonly used as the generator in a RAG stack:

Model IDInput / 1M tokensOutput / 1M tokens
deepseek-v4-flash$0.206$0.412
deepseek-v4-pro$1.09$2.17
glm-4.7-flashfreefree
glm-5$1.55$4.96
kimi-k2.5$0.66$3.30
qwen3-coder-480b-a35b-instruct$0.12$0.36

Rates read from the AIWave pricing endpoint on 2026-07-26. Check live pricing before budgeting — providers revise rates.

Work the arithmetic for your own pipeline. A query that retrieves 5 chunks of 800 tokens plus a 200 token question sends roughly 4,200 input tokens. At deepseek-v4-flash rates that is a fraction of a cent per query; at 100,000 queries a month it becomes a line item worth optimising. The multiplication is: (input tokens ÷ 1,000,000) × input price, plus the same for output. Nothing about that is a benchmark claim — it is the published rate times your volume.

Step 7: Cut tokens without losing accuracy

from llama_index.core.postprocessor import SentenceTransformerRerank

reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-6-v2",
    top_n=3,
)

engine = index.as_query_engine(
    similarity_top_k=20,          # retrieve wide, cheaply
    node_postprocessors=[reranker],  # then narrow before paying for tokens
)

Step 8: Persist to a real vector store

The in-memory store is fine for prototypes and wrong for production. LlamaIndex supports the usual backends; Chroma is the lowest-friction step up:

import chromadb
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext, VectorStoreIndex

client = chromadb.PersistentClient(path="./chroma")
collection = client.get_or_create_collection("docs")
vector_store = ChromaVectorStore(chroma_collection=collection)
storage = StorageContext.from_defaults(vector_store=vector_store)

index = VectorStoreIndex.from_documents(documents, storage_context=storage)

The LLM configuration does not change when you swap the vector store — that separation is the main reason to use a framework here at all.

Step 9: Handle failures

RAG pipelines fail in two places: retrieval returns nothing useful, or the LLM call errors. Handle both explicitly.

from openai import APIStatusError

RETRYABLE = {408, 429, 500, 502, 503}

def safe_query(engine, question, fallback_llm=None):
    try:
        resp = engine.query(question)
    except APIStatusError as e:
        if e.status_code in RETRYABLE and fallback_llm:
            Settings.llm = fallback_llm      # same request body, different model
            resp = engine.query(question)
        else:
            raise
    if not resp.source_nodes:
        return "No relevant documents found."   # do not let the model improvise
    return str(resp)

Which status codes justify a retry is covered in error codes; rate limits has a backoff implementation with jitter.

The two mistakes that cause most support questions

Using OpenAI instead of OpenAILike

The OpenAI class validates against a model registry and rejects unknown names before sending anything. The error mentions an unknown model, which reads like a server problem but is entirely client-side.

Leaving context_window at the default

LlamaIndex uses context_window to decide how much retrieved text fits. Leave it at a conservative default and you silently truncate context on a model that could have handled far more — producing worse answers with no error at all.

Comparing generators on your own data

Do not pick a model from a leaderboard. Build a small evaluation set from real questions and run the same index against several generators — the index does not change, only Settings.llm does.

for name in ["deepseek-v4-flash", "deepseek-v4-pro", "glm-5"]:
    Settings.llm = OpenAILike(
        model=name, api_base="https://aiwave.live/v1",
        api_key=os.environ["AIWAVE_API_KEY"],
        is_chat_model=True, context_window=128000, temperature=0,
    )
    engine = index.as_query_engine(similarity_top_k=5)
    for q in eval_questions:
        print(name, q, str(engine.query(q))[:200])

Judge the outputs against your own ground truth. A cheaper model that answers your questions correctly is the right answer regardless of how it ranks publicly.

Metadata filtering: the cheapest accuracy win available

Vector similarity alone ignores everything you already know about a document. If a question is about the 2026 pricing policy, there is no reason to search 2023 documents at all. Attaching metadata at ingestion and filtering at query time removes whole categories of wrong retrieval for zero token cost.

from llama_index.core import Document
from llama_index.core.vector_stores import MetadataFilters, MetadataFilter

docs = [
    Document(text=body, metadata={"year": 2026, "team": "platform", "kind": "runbook"})
    for body in raw_bodies
]
index = VectorStoreIndex.from_documents(docs)

engine = index.as_query_engine(
    similarity_top_k=5,
    filters=MetadataFilters(filters=[
        MetadataFilter(key="year", value=2026),
        MetadataFilter(key="kind", value="runbook"),
    ]),
)

A practical rule: any field you would use in a WHERE clause belongs in metadata. Source file, date, product area, access level and language are the usual suspects. Access level in particular is worth getting right early — retrieval that ignores permissions is a data leak waiting for its first user.

Chunking is a modelling decision, not a default

LlamaIndex’s node parsers control how documents become retrievable units, and the choice materially changes answer quality. The sentence splitter respects sentence boundaries rather than slicing mid-thought:

from llama_index.core.node_parser import SentenceSplitter

Settings.node_parser = SentenceSplitter(
    chunk_size=768,
    chunk_overlap=96,
)

For structured documentation, splitting on headings preserves the hierarchy the author already created, which usually beats any fixed-size window:

from llama_index.core.node_parser import MarkdownNodeParser

Settings.node_parser = MarkdownNodeParser()

Two failure modes to watch for. Chunks that are too small lose the context that makes a passage meaningful — a table row without its header is noise. Chunks that are too large dilute the embedding, so a document about ten topics matches poorly on all ten. If retrieval quality is disappointing, adjust chunking before you reach for a bigger model; it is free and it is more often the actual cause.

Sub-question decomposition for comparative queries

Questions of the form “how does X compare to Y” retrieve badly, because no single chunk discusses both. The sub-question query engine splits the question, retrieves for each part separately, then synthesises:

from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata

tools = [
    QueryEngineTool(
        query_engine=index.as_query_engine(similarity_top_k=4),
        metadata=ToolMetadata(
            name="product_docs",
            description="Product documentation, runbooks and release notes",
        ),
    ),
]

engine = SubQuestionQueryEngine.from_defaults(query_engine_tools=tools)
print(engine.query("How does the v4 rollout differ from the v3 rollout?"))

This costs more — each sub-question is its own LLM call plus retrieval. It is worth it for genuinely comparative questions and wasteful for simple lookups, which is a good argument for routing: detect the question type cheaply, then choose the engine. A cheap fast model is entirely adequate for that classification step.

Observability: see the prompt you actually sent

The single most useful debugging habit in RAG work is looking at the final assembled prompt. What the model receives is rarely what you imagined.

import llama_index.core

llama_index.core.set_global_handler("simple")   # prints prompts and responses

response = engine.query("What is the rollback procedure?")

Once you can see the prompt, most quality problems become obvious: the relevant chunk is missing, or it is present but buried beneath four irrelevant ones, or the instruction template is fighting the retrieved context. Each of those has a different fix, and guessing between them wastes days.

Evaluating retrieval separately from generation

Treating RAG as one black box makes failures unattributable. Measure the retriever on its own first — if the correct document is not in the top-k, the generator never had a chance.

def hit_rate(index, cases, k=5):
    retriever = index.as_retriever(similarity_top_k=k)
    hits = 0
    for c in cases:                       # c = {"question": ..., "source": "file.md"}
        nodes = retriever.retrieve(c["question"])
        files = {n.metadata.get("file_name") for n in nodes}
        hits += c["source"] in files
    return hits / len(cases)

print("retrieval hit rate:", hit_rate(index, eval_cases))

Fifty labelled questions are enough to be informative. If hit rate is below roughly 0.8, work on chunking, metadata and reranking — not on the LLM. Only once retrieval is reliable does comparing generators tell you anything.

Production checklist

Migrating an existing LlamaIndex project

If your pipeline currently points at OpenAI, the change is confined to the LLM object. Indexes, retrievers, node parsers, postprocessors and evaluation code are untouched.

# before
from llama_index.llms.openai import OpenAI
Settings.llm = OpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"])

# after
from llama_index.llms.openai_like import OpenAILike
Settings.llm = OpenAILike(
    model="deepseek-v4-pro",
    api_base="https://aiwave.live/v1",
    api_key=os.environ["AIWAVE_API_KEY"],
    is_chat_model=True,
    context_window=128000,
)

Note that embeddings and the LLM are independent choices. Many teams keep an existing embedding setup, swap only the generator, and re-run their evaluation set before committing. Keep the previous configuration behind an environment variable so rollback is one deploy rather than one revert. The non-framework-specific parts of the move are covered in migrating an OpenAI app to Chinese models.

Frequently asked questions

Does LlamaIndex support DeepSeek?

Yes, through OpenAILike pointed at an OpenAI-compatible endpoint. No DeepSeek-specific package is required, and every LlamaIndex abstraction built on the LLM interface keeps working.

Why does LlamaIndex reject my model name?

You are almost certainly using the OpenAI class, which validates names against a registry of OpenAI models. Switch to OpenAILike.

Should I use hosted or local embeddings?

Local embedding models such as the BGE family are free per query and keep your corpus on your own infrastructure, which suits most RAG workloads. Hosted embeddings make sense when you cannot run a model locally or need a specific hosted model.

How much does a RAG query cost?

Multiply your per-query token count by the published rate. A typical query sending around 4,000 input tokens costs a small fraction of a cent on the cheaper models — see the live pricing page for current rates.

Get an API key →Compare live pricing →

Sources

Every claim above traces back to one of these. Pricing figures come from the AIWave pricing endpoint on 2026-07-26; capability claims come from the vendors’ own documentation.