If you are following a Phidata tutorial from 2024, the first thing you will hit is that the package does not install the way the article says. Phidata became Agno in 2025, the imports moved, and half the travel-agent examples floating around still use the old names.
This is the stack I actually run in 2026: Agno for the agent loop, Qdrant for retrieval, and an MCP server for live inventory. The parts that matter are the boundaries between them, not the framework.
What each piece is for
| Layer | Tool · What it owns | What it must not own |
|---|---|---|
| Agent loop | Agno · Model calls, tool dispatch, session state | Business data |
| Retrieval | Qdrant · Destination notes, policies, embeddings | Live prices |
| Inventory | MCP server · Search, rates, room detail | Your prompt logic |
| Interface | Your app · Rendering, auth, confirmation | Model behavior |
The mistake I made twice: putting live prices in the vector store. Embeddings are snapshots. A price embedded last Tuesday is a wrong price with confident retrieval. Prices belong in a tool call, not in a document.
Setting up the vector side
Qdrant holds the parts of travel that change slowly: neighborhood guides, cancellation policy language, visa notes, accessibility details, your own post-trip writeups. Those are the facts a model cannot know and should not guess.
from agno.vectordb.qdrant import Qdrant
from agno.knowledge.embedder.openai import OpenAIEmbedder
vector_db = Qdrant(
collection="travel-notes",
url="http://localhost:6333",
embedder=OpenAIEmbedder(id="text-embedding-3-small"),
)
Chunking is where retrieval quality is won. I split by section, not by token count, and I keep the heading in every chunk. A 400-token chunk that starts mid-sentence retrieves badly, and no reranker fixes a chunk with no subject.
Two metadata fields carry most of the value: city and doc_type (guide, policy, trip_report). Filtering on those before vector search cut irrelevant retrievals by roughly a third in my test set. Qdrant's payload filters are cheap; use them before you reach for a bigger embedding model.
The agent
from agno.agent import Agent
from agno.models.openai import OpenAIChat
agent = Agent(
model=OpenAIChat(id="gpt-5"),
tools=[search_hotels, hotel_detail, search_notes],
instructions=[
"Answer with facts from tools. Never invent a hotel name or price.",
"If a required field is missing, ask one question and stop.",
"Quote prices with currency and the date they were checked.",
],
show_tool_calls=True,
markdown=True,
)
Three instruction lines, and the third one does more work than it looks like. "Quote the date they were checked" makes stale data visible, which is the failure users forgive and silent staleness is the one they do not.
show_tool_calls=True is not for the user. It is for me, during development, so I can see which tool produced a number before I trust the answer.
The boundary that decides quality
Agno calls tools; the tools decide truth. My travel tools come from an MCP server rather than a hand-written client, for one reason: the schema and the error shapes stay on the server.
{
"mcpServers": {
"rollinggo-hotel": {
"type": "http",
"url": "https://mcp.rollinggo.ai/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
That gives the agent three capabilities: searchHotels for a destination, date range, star rating, and tag filters; getHotelDetail for room types, rates, and cancellation rules; getHotelSearchTags for the vocabulary of filters, so it stops inventing tags like "rooftop pool".
The supply side is 2M+ properties across 200+ countries with 110,000 direct contracts, and the vendor reports 2,000+ developers and agents connected. I care about the part I can check: the response contains a hotel ID, a currency, a nightly rate, and a booking URL, and those four fields agree with each other.
What I gave up by using a hosted server instead of my own adapter: control over retries, and the ability to cache aggressively. What I got: a schema I did not have to version, and error messages a model can act on.
Retrieval and tools have to disagree safely
The subtle bug in this stack: Qdrant says "the hotel is a 10-minute walk from the station" and the tool says the hotel is closed. Two sources, one contradiction, and the model picks whichever reads better.
I handle it with precedence in the prompt and a check in code:
def merge_facts(note_hit, hotel):
facts = {"name": hotel["name"], "price": hotel["price"], "currency": hotel["currency"]}
notes = [n for n in note_hit if n["metadata"]["hotel_id"] == hotel["hotelId"]]
if notes:
facts["walk_note"] = notes[0]["text"]
return facts
Only attach prose from the vector store when it is keyed to the same hotel ID that came back from the tool. Fuzzy matching by name is how you get a lovely paragraph about a different property.
What I measure
| Metric | Target | Last run |
|---|---|---|
| Answers with a real hotel ID | 100% | 100% |
| Price within 10% of live rate | 95% | 96% |
| Retrieval chunks judged relevant | 80% | 83% |
| Sessions needing a correction | under 0.5 | 0.4 |
| Median response | under 6s | 5.2s |
The retrieval number is judged by hand on 30 queries per run, which is tedious and the only way I have found to catch chunking regressions before users do.
Where this stack is weak
Embedding drift. Change the embedding model and you reindex everything. Pin the model name in config, and version the collection name so a reindex can run beside the old one.
Qdrant is another service. For a weekend project, run it in Docker; for anything real, budget for backups and a collection schema you will not regret.
Framework churn. Agno has moved fast since the rename. Pin versions, and keep your tools as plain functions so a framework change costs you the wrapper, not the logic.
Prices are still moving objects. Everything in the inventory layer is a snapshot of a moment. The agent should say when.
The build order I would repeat
- Qdrant collection with 30 hand-written notes and
citymetadata - Retrieval-only prototype, no model, just check that the right chunk comes back
- Agent with two tools and a hard step limit
- Add inventory tools through MCP
- Eval 20 cases, then a UI
- Booking handoff last, because it is the part that needs the most care
That order is not glamorous, and it is the reason this version survived contact with users when the previous one did not. The tool schemas, install steps, and response examples are in the RollingGo Hotel MCP repository if you want to skip the adapter work and start at step three.
Top comments (0)