Skip to content

Repository files navigation

guidepost-mcp

案内の樹形図を MCP 経由で辿らせるサーバー。エージェントが回答のラベルを投げると、 次に何を確認・案内すべきかがルールベースで返る。端まで辿れば案内完了。

自己ループ型のエージェントは柔軟な代わりに、同じ問い合わせでも毎回違う道筋を通る。 返金・本人確認のような外してはいけない応対では、これが監査にもエスカレーション判断にも耐えない。 判断が要る場所を樹形図として外に固定し、言葉にするところだけをエージェントに任せる

CS 専用ではない。樹形図の語彙をドメインに依存させていないので、手続きを順に進める仕事全般に使える。

  顧客の発話                      guidepost-mcp (MCP · 8127)
      │                    ┌──────────────────────────┐
  ┌───▼────────┐  values   │ flows/*.yaml ← 起動時に   │
  │  エージェント  ├──────────▶│   メモリ常駐(読むだけ)  │
  │             │◀──────────┤ engine = 純関数で遷移     │
  └────────────┘  next     │ SQLite = runs / steps    │
      │  発話をラベルに        └──────────┬───────────────┘
      │  落とすのはこちら側                │ 読み取り専用
      ▼                              ┌───▼──────────┐
   顧客へ返す                          │ Web UI (SSR)  │ いま樹形図のどこにいるか
                                     └───────────────┘

自然文 → ラベルの解釈はエージェント側が持つ。サーバーは受け取ったラベルを見て遷移するだけなので、 LLM 呼び出しが増えない。実測(Raspberry Pi 5)は遷移の計算が p95 0.006ms、 SQLite を含めて p95 0.76ms、HTTP の MCP を通したエンドツーエンドで p95 9.1ms。 音声応対 1 ターンの合計は約 2.5 秒なので、HTTP 込みでも 0.4% にあたる。 内訳と、その予算をどう引いたかは docs/research/voice-agent-latency-budget.md

樹形図の書き方

flows/<flow_id>.yaml が 1 本の樹形図。ノードは 4 種類だけ。

kind 役割 分岐
ask 1 つの確認事項を聞き、答えのラベルで分岐する する
tell 1 つの案内を伝える しない
collect 独立した複数の項目を順不同で集める しない
end 終端。outcome を持つ しない
id: payment_failed
title: 支払いが失敗した
entry: n_error_code
max_unmatched: 3          # 聞き直しの上限
max_branch_fanout: 3      # これより枝が多いと畳まない
branch_depth: 2           # 枝を辿って結末を探す深さ
on_unknown: broaden       # 分からないと言われたとき。broaden / escalate
on_stuck: 原因が絞れないため、決済窓口の担当者に引き継ぐ

nodes:
  - id: n_error_code
    kind: ask
    say: 決済画面に出ているエラーコードを確認する     # 逐語原稿ではなく「何を伝えるか」
    accepts:                                      # ラベル → そのラベルに落とす条件
      E01: カードが拒否された
      E02: 残高不足・限度額超過
    next:
      E01: n_card_age
      E02: n_balance
      __other__: n_symptom        # 想定外のラベルの逃がし先(任意)
      __unknown__: n_generic      # 分からないときの逃がし先(任意)

  - id: n_identity
    kind: collect
    say: 本人確認に必要な情報を集める
    on_unknown: escalate          # 重要な手続きなので畳ませない
    slots:
      order_id:
        ask: 注文番号を聞く
        required: true            # 埋まらないと進めない
      phone: 登録の電話番号を聞く   # 短い書き方(任意扱い)
    next: n_verify

  - id: n_resolved
    kind: end
    outcome: resolved
    say: 解消したことを確認し、対応を締める

say は逐語原稿ではなく「何を伝えるか」の素材。言い回しはエージェントが場に合わせる。 1 ノード = 1 つの確認事項か 1 つの案内に保つ。

書いたら検証する。到達不能ノードや行き先の無いラベルは、書いた時点では動くので実行時まで気づけない。

uv run guidepost-mcp lint flows/          # CI でも回している
uv run guidepost-mcp show payment_failed --flows flows   # 樹形図を木で表示
uv run guidepost-mcp drafts               # 預かっている草案の一覧
uv run guidepost-mcp approve refund_request   # 草案を検査し直して flows/ へ移す
uv run guidepost-mcp discard refund_request   # 草案を捨てる

MCP ツール

ツール 役割
guide_flows(category) 引数なしでカテゴリ一覧と件数、category 指定でその中のフロー一覧
guide_start(flow_id, subject, agent) run を開始。run_id + 最初のノード + 索引
guide_answer(run_id, choice, values, utterance) 回答を投げる。下記 5 状態のどれかが返る
guide_state(run_id) 現在地・経路・収集済みの値。復帰と引き継ぎ用
guide_revise(run_id, to_node, clear) 戻る。訂正された値を落とす
guide_close(run_id, outcome, reason) 端まで行かずに締める
guide_draft(yaml) 樹形図を草案として預ける。lint の指摘が返る。応対には出ない

入口の選定は2段階、樹形図の登録は草案止まり

樹形図が増えると全件返せなくなるので、guide_flows() はまずカテゴリ(flows/categories.yaml に定義)と各件数を返し、guide_flows(category=...) でその中の一覧を返す。どのカテゴリにも 当てはまらないなら、無理に当てはめず樹形図を使わずに応対する。

guide_draft は樹形図を預かるだけで、flows/ にも guide_flows() にも出ない。人が guidepost-mcp approve で通して初めて正本になる。樹形図の生成はこのサーバーの外の仕事で、 応対の最中に生成することも想定していない(判断の理由は docs/adr/0011-draft-intake.md)。

回答は蓄積され、埋まっている分だけ自動で前進する

顧客が「E01 が出てて、カードは 3 年前のやつで」と一度に話したら、答えを持っているのに 1 問ずつ聞き直すのは避けたい。values に分かっているものを全部渡すと、 埋まっている限り進み、埋まっていない最初のノードで止まる

  収集済み: {n_error_code: E01, n_card_age: over_1y}

  n_error_code ──E01──▶ n_card_age ──over_1y──▶ n_expiry_check ──?──▶ …
   ✓ 聞かずに通過        ✓ 聞かずに通過           ▲ ここで止まる

飛ばしたノードは skipped で返る。先のノードの id をエージェントが知れるよう、 guide_start索引(各ノード / スロットが何を聞くものかの 1 行)を 1 回だけ渡す。

collect の順不同も同じ仕組み。どの順に埋めても、揃った時点で通過する。

「分からない」で止まらない

問い合わせてきた人が答えを持っていないことは普通にある。粘って聞き直しても出てこない。

   guide_answer
        ├─ ラベルが accepts にある ──────────▶ advanced / completed
        ├─ accepts に無い ──────────────────▶ unmatched(聞き直す)
        │                                       │ max_unmatched 回で下へ
        └─ choice="__unknown__" ──────────┐   │
                                          ▼   ▼
                               next.__unknown__ があるか
                                  ├─ ある ─▶ advanced(逃がし先へ)
                                  └─ 無い ─▶ on_unknown は
                                              ├─ escalate ─▶ stalled(有人へ)
                                              └─ broaden ──▶ 枝を畳めるか
                                                   ├─ できる ─▶ branched
                                                   └─ 無理 ───▶ stalled

branched枝を確定しないまま、各枝の結末を並べて案内する状態。 「E01 でしたらカード会社へ、E02 でしたら残高を確認してください」を組む材料が返る。 全枝が合流するノードがあれば common に入るので「いずれの場合も最後に○○」と括れる。

返金・本人確認のように曖昧なまま案内すると害が出る手続きは、on_unknown: escalate を 書いて畳ませない。畳めないノードは lint が名指しするので、そこにだけ手当てすればよい。

起動

uv sync
uv run uvicorn guidepost_mcp.web:app --host 127.0.0.1 --port 8127
uv run python scripts/mcp_smoke.py            # 実プロトコルで 1 周辿る
uv run python scripts/mcp_smoke.py --parallel # 2 本の run を交互に進める

Web UI は http://127.0.0.1:8127/。進行中の run が一覧され、/r/<run_id> で 樹形図の上に現在地・通った経路・先読みで飛ばしたノード・枝を畳んだノードが色分けされる。

エージェント側からは HTTP の MCP として繋ぐ。

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient(
    {
        "guidepost-mcp": {"url": "http://127.0.0.1:8127/mcp/", "transport": "streamable_http"},
    }
)
tools = await client.get_tools()

バージョン

版の正本は pyproject.tomlversion 1つで、SemVer に従う(0.x のあいだは minor で 破壊的変更が入りうる)。変更の履歴は CHANGELOG.md

何を変えたら破壊的変更かは docs/adr/0009-versioning.md に 定義してある。要点は樹形図の YAML スキーマと MCP ツールの契約(ツール名・引数・ 戻り値のキー・status の5値)で、next の指示文の文面は含めない。

flows/*.yamlversion応対の手順の版で、このライブラリの版とは無関係。

設計の背景

なぜラベル解釈をエージェント側に置くのか、なぜ「分からない」で枝を畳むのか、 なぜ Web UI を読み取り専用にしたのかは、docs/adr/ に理由つきで記録してある (一覧は docs/adr/README.md)。判断の材料になった調べ物は docs/research/ にある。

About

案内の樹形図を辿る MCP サーバー。エージェントが回答のラベルを投げると、次に確認・案内すべきことが決定的に返る(FastAPI + MCP・読み取り専用の Web UI 付き)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages