RAG Retrieval
One function does all retrieval: embed the query, search the shared collection filtered to this agent, return top-k chunks with scores. Two endpoints expose it — raw search for inspection, and the ask pipeline for answers.
The retrieval function
def search_agent_documents(agent_id: int, query: str, top_k: int = 3):
query_embedding = create_embedding(query) # same model as ingestion
results = documents_collection.query(
query_embeddings=[query_embedding],
n_results=top_k,
where={"agent_id": agent_id}, # ← tenant isolation
)
# zip Chroma's parallel arrays into dicts:
# {chunk_text, score, document_id, filename, chunk_index}Three properties to internalize:
- Symmetric embedding. The query is embedded with the same
text-embedding-3-smallmodel as the chunks — distances are only meaningful within one embedding space (warning). - Agent scoping is a metadata filter, not separate collections. One
agent_documentscollection holds everyone's vectors;where={"agent_id": ...}restricts the search. Any new query path must keep this filter. scoreis a raw distance — lower is better. It is not normalized similarity; do not render it as a percentage.
The raw search endpoint
POST /agents/{id}/documents/search — gated on the document_search tool (403 otherwise), logs a document_search ToolCall, no generation:
curl -X POST http://localhost:8001/agents/1/documents/search \
-H "Content-Type: application/json" \
-d '{"query": "support SLA", "top_k": 3}'{
"query": "support SLA",
"results": [
{
"chunk_text": "Premium customers have a 4-hour support SLA...",
"score": 0.31,
"document_id": 1,
"filename": "sla.txt",
"chunk_index": 0
}
]
}Use it for: debugging why the ask endpoint refused (was the right chunk even retrieved?), tuning top_k, and building custom retrieval UX. The ask pipeline calls the exact same function, so raw search is a faithful window into what the evaluator saw (Corrective RAG).
Choosing top_k
top_k | Effect |
|---|---|
| 1–2 | tight context, cheapest evaluator/answer calls; risks missing multi-chunk answers |
| 3 (default) | balanced — matches the chunk size so context stays ≈ 600 tokens |
| 5+ | better recall for scattered facts; inflates evaluator+answer input tokens and dilutes context |
top_k comes from the request body (DocumentSearchRequest / RAGAnswerRequest, default 3) with no server-side cap — an extreme value is the caller's own cost problem (Security).
Failure & edge behavior
| Case | Behavior |
|---|---|
| No documents / no matches for the agent | empty results (search) · short-circuit to refusal (ask) |
Fewer than top_k vectors exist | Chroma returns what it has |
| Embedding API failure | 500; failed document_search ToolCall committed |
| Cross-agent query attempt | impossible by construction — the filter is applied server-side |
Quality levers (when retrieval feels wrong)
In diagnostic order:
- Look at raw search results for the failing question — is the needed chunk present at all?
- Chunking granularity — facts split across a boundary retrieve poorly; the 100-char overlap mitigates but doesn't eliminate (Ingestion).
- Query phrasing — dense retrieval matches meaning, not keywords; restate the question closer to the document's vocabulary.
top_k— raise it when answers span multiple chunks.
Bigger levers (hybrid search, re-ranking, query expansion) are roadmap territory.
Related pages
- Ingestion — how the vectors got there
- Corrective RAG — retrieval's primary consumer
- API Reference: Documents — full endpoint contracts