LangGraph guide
Wire a LangGraph graph to duckdb-vectors as a retrieval node. The node calls store.query and writes the chunks into graph state, so later nodes — a model call, a formatter — read them from state. The primary path imports the SDK and calls it in the same process; the alternative reuses the stdio MCP server as a tool node.
What you will build
A two-node graph: a retrieve node that opens a local read-only index, runs store.query, and stores the ranked chunks in state; and a generate node that reads those chunks and asks the model to answer from them.
Step 1 — install the pieces
duckdb-vectors carries the retrieval code; add LangGraph and a model provider:
$ pip install duckdb-vectors langgraph langchain-openaiStep 2 — build an index
Point the CLI at a folder of markdown to produce a single DuckDB file:
$ duckdb-vectors index ./documentation --db vectors.duckdb --backend fastembed:minishlab/potion-multilingual-128MThe first run downloads the fastembed model; later runs reuse the cache.
Step 3 — the retrieval node
Open the store once at module scope. The node reads the question from state, retrieves chunks, and writes a joined context string back into state:
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import START, StateGraph
from duckdb_vectors import VectorStore
store = VectorStore(
db_path="vectors.duckdb", backend="fastembed:minishlab/potion-multilingual-128M", read_only=True
)
model = init_chat_model("openai:gpt-4o-mini")
class State(TypedDict):
question: str
context: str
answer: str
def retrieve(state: State) -> dict:
hits = store.query(state["question"], top_k=5)
context = "\n\n".join(
f"[{hit.chunk.source}] {hit.chunk.text}" for hit in hits
)
return {"context": context}Each hit is a SearchResult: hit.score is the fused rank and hit.chunk carries source, title, and text.
Step 4 — the generate node and the graph
The generate node reads the context from state and asks the model to answer. Wire the two nodes in sequence:
def generate(state: State) -> dict:
prompt = (
f"Answer using only this context:\n\n{state['context']}\n\n"
f"Question: {state['question']}"
)
return {"answer": model.invoke(prompt).content}
builder = StateGraph(State)
builder.add_node("retrieve", retrieve)
builder.add_node("generate", generate)
builder.add_edge(START, "retrieve")
builder.add_edge("retrieve", "generate")
graph = builder.compile()
result = graph.invoke({"question": "how is a search answered?"})
print(result["answer"])Retrieval runs in-process against the local file; the chunks flow through state to the model — no external vector service.
Verify the node on its own
Call the node with a minimal state to confirm it retrieves before compiling the graph:
$ python -c "from app import retrieve; print(retrieve({'question': 'how is a search answered?'})['context'][:200])"The context should open with a chunk from the primitive's "how a search is answered" section — the retrieval is working.
The MCP alternative
Instead of importing the SDK in the node, you can expose the stdio MCP server's tools as a LangGraph tool node with langchain-mcp-adapters. Its MultiServerMCPClient spawns duckdb-vectors mcp and adapts the search tool; drop the resulting tool into a ToolNode:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import ToolNode
client = MultiServerMCPClient(
{
"duckdb-vectors": {
"command": "duckdb-vectors",
"args": ["mcp"],
"transport": "stdio",
"env": {"DUCKDB_VECTORS_DB_PATH": "vectors.duckdb"},
}
}
)
tools = await client.get_tools()
tool_node = ToolNode(tools)The MCP server resolves the index from the DUCKDB_VECTORS_* environment variables and serves the same two read-only tools. See the Docker Agent guide for the canonical MCP wiring reference.
Where to go next
- The LangChain guide wraps the same
store.querycall as a single@toolfor a looping agent instead of a graph node. - To ship a self-contained image carrying both the graph and a frozen index, follow the containerized agent guide.