跳到正文
DEV Community· iamTheDev·· 3 小時前AI 評分56

如何使用 Agno 和 Qdrant 構建旅遊 AI 智能體(Phidata 的後繼者)

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

AI 導讀

作者分享使用 Agno、Qdrant 和 MCP server 搭建旅遊 AI 智能體的實作方案,分別處理智能體流程、知識檢索及即時庫存。方案把即時價格交由工具查詢,並以工具回傳的 hotel ID 核對檢索筆記,避免酒店資料錯配。作者表示,按 city 和 doc_type 篩選令無關檢索在測試集約減三分之一;檢索片段相關度為 83%,中位回應時間為 5.2s。

正文

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

  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.

來源:DEV Community · dev.to