こんにちは、パレイド思想部です。
連載「人力バーチャルAI実験::人間が答えるチャットボットの構築」第 2 回です。
前回は FastAPI、.env、ngrok で開発環境の土台を作りました。今回はいよいよ、このシステムの心臓部であるコアロジックに入ります。
テーマは「人間優先の応答システムと LLM フォールバック」。LINE から届いた質問を Slack の回答者チャンネルへ一斉通知し、誰かが答えてくれるのを待つ。誰も答えられなかったときだけ、Ollama(ローカル LLM)が保険として回答を返す——この仕組みを asyncio で実装します。
本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。
「集合知」を可能とする構造
一般的なチャットボットは AI が即座に回答を返します。 人間がAIの代わりを務めて、常にチャット画面の前で待機し、さまざまな質問に適切な回答をするのは容易ではありません。
本プロジェクトでは、数の力でこの問題に対処する構造を考えます。
- 大勢の人間に聞く: LINE で届いた質問を Slack チャンネルへブロードキャスト。登録ユーザーの中で答えられる人が、答えられるタイミングで回答します。
- 誰も答えなければ LLM: 一定時間(タイムアウト)内に人間の回答がなかった場合にのみ、Ollama が回答を生成して返します。
- LLM は保険: AI の回答はあくまで場を繋ぐセーフティネットで、人間の回答が得られたらそちらを優先します。
この設計の狙いは「人間の知恵を最大限活用し、AI がそれを補完する」という点にあります。 AI が主役の時代に、あえて人間を主役にしたらどんな体験になるか——それがこの実験の核心です。
技術選定:FastAPI と asyncio
この「人間を待ちつつ、裏で LLM を準備する」パターンは、非同期処理で実現します。
FastAPI の async/await と asyncio を組み合わせることで、以下の流れを並行して処理します。
- Slack へ通知を送り、人間の回答イベントを待つ
- 裏で Ollama にリクエストを投げ、回答を準備しておく
- 人間が答えたらそれを採用。タイムアウトしたら Ollama の回答を使う
コアロジックの実装
1. Slack への一斉通知と回答待ち
LINE から質問が届いたら、まず Slack の回答者チャンネルへブロードキャストします。
import asyncio
from fastapi import FastAPI
app = FastAPI()
async def notify_slack_and_wait(prompt: str, timeout: float = 60.0) -> str | None:
"""Slack に質問を通知し、人間の回答を待つ。タイムアウトしたら None を返す。"""
print(f"[Human] Slack チャンネルへ質問を通知: {prompt}")
# Slack API で回答者チャンネルへメッセージ送信
# 実際には slack_sdk の chat_postMessage を使用
# await slack_client.chat_postMessage(
# channel=RESPONDER_CHANNEL,
# text=f"📩 新しい質問が届きました:\n> {prompt}\nスレッドで回答してください。"
# )
# 人間の回答をイベントとして待機
# 実際には Slack のスレッド返信イベントを監視する
try:
response = await asyncio.wait_for(
wait_for_human_response(prompt), # Slack イベント待ち
timeout=timeout
)
return response
except asyncio.TimeoutError:
print(f"[Human] {timeout}秒経過 — 回答なし")
return None
async def wait_for_human_response(prompt: str) -> str:
"""Slack スレッドへの返信を監視する(簡易シミュレーション)。"""
# 実際には WebSocket や Slack Events API で返信を待つ
await asyncio.sleep(45) # 人間の回答時間をシミュレーション
return f"人間の回答: {prompt} について確認しました。"
2. LLM フォールバック:Ollama への保険リクエスト
人間が答えなかった場合に備え、裏で Ollama にも回答を準備させておきます。
async def ask_ollama(prompt: str) -> str:
"""Ollama にリクエストを送り、LLM の回答を取得する(保険用)。"""
print(f"[LLM] 保険回答を生成中... {prompt}")
# 実際には httpx で Ollama API を呼び出す
await asyncio.sleep(3) # 生成時間のシミュレーション
return f"LLM の回答: {prompt} に対する自動生成の回答です。"
3. 人間優先ロジックの組み立て
2 つのタスクを組み合わせて、「人間が答えたらそれを使う。誰も答えなければ LLM」を実現します。
async def handle_question(user_id: str, query: str):
"""質問を処理する。人間の回答を優先し、LLM はフォールバック。"""
HUMAN_TIMEOUT = 60.0 # 人間の回答を待つ秒数
# 両方のタスクを並行で開始
# - 人間の回答: Slack へ通知して待つ
# - LLM の回答: 裏で生成しておく(保険)
human_task = asyncio.create_task(
notify_slack_and_wait(query, timeout=HUMAN_TIMEOUT)
)
llm_task = asyncio.create_task(ask_ollama(query))
# まず人間の回答を待つ
human_answer = await human_task
if human_answer is not None:
# 人間が答えてくれた → それを採用
llm_task.cancel() # LLM の結果は不要
try:
await llm_task
except asyncio.CancelledError:
pass
await send_line_message(user_id, human_answer)
print(f"[Result] 人間が回答 ✅")
else:
# 誰も答えなかった → LLM フォールバック
llm_answer = await llm_task
await send_line_message(user_id, llm_answer)
print(f"[Result] LLM がフォールバック回答 🤖")
@app.post("/webhook/line")
async def line_webhook(message: str):
await handle_question(user_id="LINE_USER", query=message)
return {"status": "ok"}
設計のポイント
このコードで重要なのは以下の 3 点です。
- 人間が主役:
human_taskの結果を最優先で確認します。人間の回答があれば、LLM の結果は廃棄されます。 - LLM は裏で待機: 人間の回答を待っている間に LLM の回答も並行生成しておくことで、フォールバック時の待ち時間を最小化します。
- タイムアウト制御:
HUMAN_TIMEOUTを超えて誰も答えなかった場合にのみ、LLM の出番になります。この秒数は運用しながら調整できます。
運用の現実:回答は集まるのか?
コード上では綺麗に動きますが、現実には課題があります。
- 人間の応答率: Slack に通知しても、全員が忙しければ誰も答えません。タイムアウト値を短くしすぎると LLM ばかりが答えることになり、長すぎるとユーザーを待たせます。
- 回答の質のばらつき: 答えてくれる人によって回答の質が変わります。LLM の方が安定した品質を出すケースもあるでしょう。
- Ollama の負荷: ローカル環境で動く Ollama は、小型なモデルでなければ生成に時間がかかります。フォールバックが遅いとユーザー体験が悪化するため、チューニングとモデル選びが重要です。
これらはデータを取りながら調整していく部分です。「人間が主役のチャットボット」が本当に成立するかどうかは、回してみないとわかりません。
また今回は実験のため、人間の解答のチェックまでは組み込んでいませんが、結果的に匿名で発言が可能なシステムのため、意図とは異なる回答が混ざる可能性はあります。 将来的には、LLM が回答をチェックしたり、また別のレビュワーが回答をチェックする仕組みが考えられます。 リアルタイム性は落ちますが、安心して使ってもらうには本質的に欠かせない処理です。
まとめ
今回は、バーチャルAI システムの核心である「人間優先・LLM フォールバック」ロジックを実装しました。
- Slack への一斉通知で登録ユーザーに回答を呼びかける
asyncioで人間の回答待ちと LLM の保険生成を並行処理- タイムアウト制御により、誰も答えなかった場合にのみ LLM が回答
人間が答えてくれれば人間の知恵が、誰も答えられなければ AI が——この仕組みが「バーチャルAI」の実体です。
次回予告 最終回:LINE と Slack をつなぐ人力バーチャルAI の完成。



