Skip to the content.

完成版を対話で動かす

終章である。steps/ の4本は、どれも質問が1つ埋め込まれたスクリプトだった。 src/ には、同じ仕組みを対話形式でやる完成版がある。

自分で質問しながら動かし、中で何が起きているかを最後にもう一度見る。

対話で動かす

ここからが完成版の src/client/main.py である。 流れは 9章のシーケンス図のとおりで、 それをそのまま対話にしたものである。

docker compose exec app uv run python -m client.main
=== MCP RAG client ===
LLM: qwen3:1.7b
Type 'quit' to leave.

[1] Connected to the MCP server
[2] Tools reported by the server: ['search', 'list_documents', 'get_document']
[3] Converted the tool definitions for the LLM

From here on, every question is sent to the LLM along with the tools.

Question: Can I bring my dog?
  -> sending the question and the tools to the LLM...
  -> tool call: search({'top_k': 5, 'query': 'can I bring a dog'})
  -> got the search results
  -> asking the LLM to answer from them...

[Answer]
Yes, you can bring your dog. The Wandering Kettle allows dogs on the small
terrace at the top of the stairs but not inside the room itself. There is a
water bowl by the terrace door, and assistance animals are welcome anywhere.

Question: What documents can you search?
  -> sending the question and the tools to the LLM...
  -> tool call: list_documents({})
  -> got the search results
  -> asking the LLM to answer from them...

[Answer]
The documents that can be searched are:

- allergens.txt
- faq.txt
- hours_access.txt
- menu_drinks.txt
- menu_food.txt
- staff_handbook.txt
- story.txt

Question: quit
Bye.

起動に10〜20秒、1問あたり数十秒から数分かかる。CPU で推論しているためで、 待ち時間のほとんどは LLM への2回の問い合わせである(10章)。

2つの質問で、呼ばれたツールが違う。 1問目は search、2問目は list_documents である。 クライアントは選んでいない。渡した3つの中から LLM が選び、その名前を返してきただけである。

起動時の3行

質問を受け付ける前に、クライアントは3つのことを済ませている。

表示 中身 どこで見たか
[1] Connected to the MCP server サーバーを起動し、initialize を送った 6章・7章
[2] Tools reported by the server: [...] tools/list の結果。3つ返る 6章・7章
[3] Converted the tool definitions for the LLM Ollama 形式への詰め替え 9章

ここまでに LLM は登場しない。 接続とツールの確認は MCP だけで終わる。

質問するたびに起きること

表示される行が、そのまま処理の順番である。

表示 何をしているか
-> sending the question and the tools to the LLM... 1回目の POST(判断)
-> tool call: search({...}) 返ってきた tool_calls
-> got the search results tools/call の結果
-> asking the LLM to answer from them... 2回目の POST(作文)

quit と入力すると終わる。async with を抜けるので、MCP サーバーも一緒に終了する。

続けて質問しても、前回は覚えていない

Ollama は前回の messages を覚えていない。続きを話したければ、 過去のやりとりを全部 messages に並べて毎回送る。

MCP の方はセッションが続いていた(7章)。同じ「接続」でも性質が違う。

  どこに状態があるか
MCP サーバー 接続が開いている間、クライアントとの対応が続く
Ollama どこにも無い。毎回1回きりの POST

この教材のクライアントは、会話履歴を持たない。質問は1問ごとに独立している。 MCP と検索の流れを追うのが目的で、会話管理はそこに関係しないからである。

JSON-RPC を覗く

RAG_DEBUG=1 を付けると、MCP サーバーとやりとりしている JSON がそのまま出る。

docker compose exec app env RAG_DEBUG=1 uv run python -m client.main
[JSON-RPC] -> sent: tools/list
{
  "method": "tools/list"
}

[JSON-RPC] <- received: tools/list
{
  "tools": [
    {
      "name": "search",
      "description": "\nSearch the documents of a cafe called The Wandering Kettle.\n\nUse this to answer questions about the menu, prices, opening hours,\nlocation, wifi and power sockets, allergies and dietary options, the\ns...",
      "inputSchema": {
        "properties": {
          "query": {"title": "Query", "type": "string"},
          "top_k": {"default": 5, "title": "Top K", "type": "integer"}
        },
        "required": ["query"],
        "type": "object",
        "title": "searchArguments"
      }
    },
    { "name": "list_documents", ... },
    { "name": "get_document",   ... }
  ]
}

[2] Tools reported by the server: ['search', 'list_documents', 'get_document']
[3] Converted the tool definitions for the LLM

Question: Can I bring my dog?
  -> sending the question and the tools to the LLM...
  -> tool call: search({'top_k': 5, 'query': 'can I bring a dog'})

[JSON-RPC] -> sent: tools/call
{
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {"top_k": 5, "query": "can I bring a dog"}
  }
}

[JSON-RPC] <- received: tools/call
{
  "content": [
    {
      "type": "text",
      "text": "[1] source: faq.txt (distance: 0.21)\npassage: \nThe Wandering Kettle - Questions We Get Asked\n\nCAN I STAY AND WORK?\n\nYes, on most days. We ask for one order every ninety minutes, and we ask you\nto move..."
    }
  ]
}

6章で見たものと同じ形が流れている。 長い文字列は200文字で切って表示している(debug_jsonrpc())。 この前に initialize のやりとりも表示されるが、ここでは省いた。 残り2つのツールの定義も同様である。

この先

ここまでで、MCP で動く RAG が1つ手元にある。 広げ方を3つ挙げておく。