DEV Community

iamTheDev
iamTheDev

Posted on

Building a Travel AI Agent With Agno and Qdrant (Phidata's Successor)

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"),
)
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Qdrant collection with 30 hand-written notes and city metadata
  2. Retrieval-only prototype, no model, just check that the right chunk comes back
  3. Agent with two tools and a hard step limit
  4. Add inventory tools through MCP
  5. Eval 20 cases, then a UI
  6. 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)