こんにちは、技術部の大脇です。
カヤックでは2026年2月19日に北海道中川郡音威子府村(おといねっぷむら)にて、対話型AI副村長「ねっぷちゃん」をリリースしました。
本プロジェクトはオープンR&Dとしてリポジトリを公開しながら開発・運用しています。
www.kayac.com
今回はねっぷちゃんのアーキテクチャの抜粋と、その設計に至った背景をご紹介します。
主な技術スタック
ねっぷちゃんは主に以下のような技術スタックで構成されています。
- インフラ
- Cloudflare Workers / Pages / D1 / R2 / Vectorize / Queues
- バックエンド
- フレームワーク: Hono
- AIエージェントフレームワーク: Mastra
- LLM: Gemini(Google AI)
- ORM: Drizzle ORM
- バリデーション / スキーマ定義: Zod
- フロントエンド
- UIライブラリ: React
- ビルドツール: Vite
- データフェッチ: TanStack Query
- APIクライアント: openapi-fetch
- スタイリング: Tailwind CSS
- Linter / Formatter: Biome
- Test: Vitest
技術選定の指針について
企画立案の後、初期段階のねっぷちゃんを村へ持っていくまでの実装期間は約2週間という短期間だったため、スピード感を持って開発できる構成にする必要がありました。
一方で、今後の機能拡張やプラットフォーム追加を見据えると、初期段階の設計もおろそかにはできません。
実際、今回はWebチャットという形でリリースしましたが、LINEや電話、3Dアバターによる対面対話なども初期から検討事項にあり、どのプラットフォームがユーザーに馴染んでいくかも実証実験の一部ですので、ユーザーとの接点(フロントエンド)の定義はとても広義に捉える必要がありました。
そのため、ねっぷちゃんの「本体」はあくまでバックエンドとし、フロントエンドはインターフェースとして柔軟に差し替え・追加できるアーキテクチャを意識しました。
また、本プロジェクトはプロダクトオーナーと私の2名体制という少人数で進めています。
そのため、コンテキストスイッチなく全体を把握できることを重視し、バックエンドもTypeScriptで統一することにしました。
あわせて、コーディングエージェント(以後、Claude Code)との協業も前提としており、人間・エージェント双方が同一言語で扱えるというメリットもあります。
これにより、少人数でも広い範囲をカバーできる体制を実現できました。
さらに、これらをmonorepo構成で管理することで、lint・フォーマット・型定義といった共通の設定や規約をリポジトリ全体で統一しています。
この構成はClaude Codeとの協業においても効果的で、バックエンドのAPIスキーマや型定義を把握した上でフロントエンドの実装に取りかかれるため、人間が逐一コンテキストを伝え直す手間が減り、一貫性のあるコードが生まれやすくなっています。
バックエンド
Hono を Cloudflare Workers上で動かしています。
エッジランタイムには実行時間やメモリの制約がありますが、コストの低さやインフラ管理が不要な点が、少人数かつ予算が限られた本プロジェクトの条件に合っていました。
インフラ
弊社はAWSを使った開発運用の知見が多く、AWSを採用することで得られる恩恵が大きい環境です。
あえてCloudflareを採用した理由としては、自身がAWSに対して知見が多いわけではないという点もありましたが、コストを気にせず開発できる点や、wrangler CLIを通してデプロイまで一気通貫で完結するスピード感の両立ができそうと感じたためです。
現在ねっぷちゃんではWorkersの他に、Pages, D1, R2, Vectorize, Queuesを利用していますが、すべてWorkers Paidプランの範囲で収まっており、当分この枠を飛び出す見通しもありません。
APIサーバー
TypeScriptでAPIサーバーを実装するにあたり、「フロントエンドはインターフェースとして柔軟に差し替え・追加できる」という方針を踏まえ、Next.jsのAPI Routesのようにフロントエンドと密結合にはせず、HonoでAPIサーバーを独立させました。
Honoについては、軽量かつ周辺機能も充実しており、柔軟に設計できそうなフレームワークだという印象があったのが決め手でした。実際にテンポ良く開発ができており、想像以上に快適でした。
API仕様の共有には@hono/zod-openapiを採用しています。
他の選択肢としてHonoにはRPCというサーバーとクライアント間で型を共有できる仕組みもあり魅力的でしたが、OpenAPI(Swagger)ベースの方が言語やプラットフォームを問わず読める共通ドキュメントとして整うため、将来的なアプリ化やAPIの外部公開といった展開にも柔軟に対応できると考えこちらを選びました。
@hono/zod-openapiはバリデーションとOpenAPIドキュメントの自動生成を両立でき、実装しながら仕様が自動で整っていく体験が気に入っています。
フロントエンドでは、このOpenAPIスキーマをopenapi-typescriptでTypeScriptの型定義に変換し、openapi-fetchで型安全なAPIクライアントとして利用しています。
エージェントフレームワーク
エージェントフレームワークにはTypeScriptネイティブなMastraを採用しました。
ねっぷちゃんは Gemini APIを直接呼び出すだけのシンプルな構成ではなく、RAGによるナレッジ検索やWeb検索、会話履歴の管理など、役割ごとに分かれた複数のツールが連携して動いています。
今後のプラットフォーム増加に伴いアーキテクチャがさらに複雑化することも見込まれるため、こうした構成をフレームワークの規約に沿って整理できる点が、採用の大きな動機でした。
マークダウンドキュメントのchunkingやembedding、検索結果のリランキング、RAGの品質評価といった機能も備えており、ねっぷちゃんに必要な機能をフレームワーク内で一通り賄える点も大きかったです。
エージェントやツールの動作をブラウザ上で確認できるMastra Studioも、開発中のデバッグに重宝しました。

また、公式でドキュメントのMCPサーバーが公開されており、実装に悩んだ際に仕様を気軽に確認できるのも助かっています。
開発期間中にv0からv1へ移行の過渡期でしたが、マイグレーションもスムーズで、フレームワーク側の変化に戸惑うことなく進められました。
エージェント構成
ねっぷちゃんのメインエージェントは単体ですべてを処理するのではなく、必要に応じて役割ごとにサブエージェントを分離し、それぞれに専用のツールを持たせています。
こうすることで、メインエージェントは「どのサブエージェントに任せるか」の判断に集中でき、各サブエージェントは自身の役割に必要なコンテキストだけを扱えます。ツールをすべてメインエージェントに持たせるとプロンプトが肥大化し、選択肢が増えるほどLLMの判断精度も落ちるため、役割ごとに分離する構成にしています。
また、ユーザーの種別に応じて利用可能なサブエージェントを動的に切り替えており、一般ユーザーにはナレッジ検索やWeb検索といった基本機能を、管理者にはフィードバック分析やペルソナ分析など運用向けの機能を追加で提供しています。

さらに、エージェントごとに使用するモデルや思考レベルを変えることで、速度とコストのバランスをコントロールしています。
メインエージェントのねっぷちゃんは会話の振り分け役のため、軽量なモデルで高速に応答させ、ナレッジ検索やWeb検索など正確性が求められるサブエージェントには思考力の高いモデルを割り当てています。
すべてのエージェントに高性能なモデルを使う必要はなく、役割に応じた適切なモデル選択がレスポンス速度と運用コストの最適化に繋がっています。
RAG戦略
ねっぷちゃんの回答品質を支えているのが、RAGによるベクトル検索です。
LLMは一般的な知識を幅広く持っていますが、音威子府村の施設情報やイベント、暮らしに関する細かな情報までは学習データに含まれていません。Web検索を併用することである程度は補えるものの、ネット上にも載っていない村独自の知識を届けるには限界があります。
そこで、村が持つ独自の情報をナレッジとして整備し、ベクトル検索で必要な情報を引き出せる仕組みが重要な役割を担っています。
データの準備
村役場関連のWebサイトをスクレイピングし、PDFデータはマークダウン形式に変換しています。
その後、リンク切れなどの情報精査を行い、R2にアップロードします。
R2にアップロードされたデータはCloudflare Queuesを経由して自動的にEmbeddingされ、Vectorizeに同期されます。
情報の精査は最終的に人の目で確認しています。自動化も検討課題にありますが、ねっぷちゃんの心臓と言っても過言ではない部分なので丁寧に進めています。
chunkingはマークダウンのHeading構造単位で行い、metadataにはパンくずナビゲーションのように上位層のタイトルを構造化して含めています。
以下の取得例では、テキストの断片だけではなく「広報おといねっぷ 2020年1月号 No.544」の「年頭のごあいさつ」の「音威子府村村民憲章」の情報であるということがわかります。
{ "content": "わたくしたちは、緑こい山なみと水豊かな天塩川のもと、交通の要衝として発展してきた音威子府村の村民です。たくましい開拓精神を受けつぎ、より住みよく豊かなまちをつくるためにこの憲章をかかげ、村民としての自覚と責任をもってその実行に努めましょう。 \n(昭和47年9月30日制定) \n1. 心をみがき、からだをきたえ、たのしいまちをつくりましょう。\n2. きまりを守り、親切をつくし、明るいまちをつくりましょう。\n3. 仕事にはげみ、生産を高め、豊かなまちをつくりましょう。\n4. 自然をいかし、環境をととのえ、美しいまちをつくりましょう。\n5. 文化を高め、郷土を愛し、平和なまちをつくりましょう。", "score": 0.49659931, "source": "pdf/parsed/kouhou/2020-01.md", "title": "広報おといねっぷ 2020年1月号 No.544", "section": "年頭のごあいさつ", "subsection": "音威子府村村民憲章" }
検索
ユーザーの質問を Embeddingに変換し、Cloudflare Vectorizeで類似度の高いナレッジを検索しています。
ただし、ベクトル類似度だけでは本当に質問に関連する情報かどうかの判断には限界があります。
そこで、初段で多めに候補を取得したうえで、LLMベースのScorerでリランキングを行い、意味的な関連性も加味して最終的な参照コンテキストを絞り込んでいます。
また、検索結果が不足していた場合は、前述のメタデータを手がかりにクエリを組み替えて再検索するリトライ戦略も持たせています。
たとえば初回の検索で概要的なチャンクしかヒットしなかった場合、メタデータに含まれるセクション名を組み合わせてより具体的なクエリで再検索することで、目的の情報にたどり着ける可能性を高めています。
Webフロントエンド
Vite × Reactで構築し、Cloudflare Pagesにデプロイしています。
ここについては書き慣れているからという要素が大きく、特に悩まず採用しました。
世の中に広く使われているので、Claude Codeに書いてもらうことも踏まえて優位だった点もあるかと思います。
スタイリング・デザイン
スタイリングはTailwind CSSを利用し、Claude Codeに一貫したトンマナで実装をしてもらえるようにしています。
また、デザインツールのPencilからデザイントークンやデザインシステムを作成し、スタイルの一貫性を強化しています。
実装を始める前のワイヤーフレームの役割を果たすなど、UXのすり合わせにも役立っています。

チャットUI
チャットUIにはassistant-uiを採用しています。
AI SDKとの統合やストリーミング表示といったチャットの基本機能に加え、マークダウンのレンダリングやツール呼び出し結果をReactコンポーネントとしてチャット内に描画できるToolUIなど、LLMとの対話をカスタマイズするために必要な機能が揃っています。
ねっぷちゃんでは、図や表、テーブル、タイムラインなどの独自Reactコンポーネントの描画などに活用しており、テキストだけでは伝わりにくい情報をリッチに表現できるようになっています。

テスト方針
ユニットテストにVitestを採用しています。
AIエージェント開発ではテスト駆動やスペック駆動の手法も広まりつつありますが、本プロジェクトでは「こうしてみたらどうだろう」を形にしながらチューニングしていく性質上、仕様が常に変わりうる段階にあります。
そのため、アプリケーションの振る舞いに対するテストよりも、認証やミドルウェア、データアクセス層といった基盤部分のテストを中心にカバーする方針にしています。
方針が固まった部分から、段階的にテストを手厚くしていく予定です。
今後について
リリース以後に集まったユーザーの生の意見を踏まえ、より具体的な課題解決につながるような改善をしていきます。 併せて、以下のようなねっぷちゃんがより身近に感じられるインターフェースとは何かを引き続き模索していく予定です。
- キャラクタービジュアルの作成
- LINEによる日常会話コミュニケーション
- 電話を使った会話コミュニケーション
- アバターを使った対面会話コミュニケーション
さいごに
冒頭でもご紹介しましたが、本プロジェクトはオープンソースとして公開しておりますので、開発の進捗をお楽しみいただければと思います。
開発への貢献やアイデアの提案なども歓迎しておりますので、お気軽にご相談ください。