8つのルームから過去メッセージを全部抜いて、FAQ を自動生成したかった。
やりたいことはシンプルだ。Chatwork のルームに蓄積された質問と回答のペアを抽出して、FAQ データベースを作る。手でコピペするのは論外なので、API で全件取得して機械的にフィルタリングする方針にした。
結果として 429 件の FAQ を抽出できたが、そこに至るまでに 3 つの罠にハマった。同じことをやろうとしている人の参考になれば。
罠 1: force=0 だとメッセージが返ってこない
Chatwork API でメッセージを取得するエンドポイントはこれだ。
GET /rooms/{room_id}/messages
最初、パラメータなしで叩いたら空っぽが返ってきた。
curl -s -H "X-ChatWorkToken: $TOKEN" \
"https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages"
# → [] (空配列)
メッセージは確実にある。なのに空。
原因は force パラメータ。デフォルトは force=0 で、未読メッセージだけを返す仕様になっている。既読のメッセージは返ってこない。
# force=1 で既読含む全件取得
curl -s -H "X-ChatWorkToken: $TOKEN" \
"https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1"
# → ドバッとJSON が返ってくる
ドキュメントには書いてあるが、最初の一発が空だと「APIキーが間違ってるのか?」と別の方向に迷走する。force=1 は必須パラメータだと思った方がいい。
罠 2: 100 件の壁
force=1 でメッセージが返ってくるようになった。が、数えてみると 最大 100 件しか返ってこない。
import requests
headers = {"X-ChatWorkToken": TOKEN}
url = f"https://api.chatwork.com/v2/rooms/{room_id}/messages?force=1"
res = requests.get(url, headers=headers)
messages = res.json()
print(len(messages)) # → 100
1 つのルームに数千件のメッセージがあるのに、100 件で打ち切られる。ページネーション用のパラメータは公式ドキュメントに明記されていないが、実際にはレスポンスに含まれる message_id を使って次のページを取得できる。
やり方はこうだ。
def fetch_all_messages(room_id, token):
"""全メッセージをページネーションで取得"""
all_messages = []
headers = {"X-ChatWorkToken": token}
base_url = f"https://api.chatwork.com/v2/rooms/{room_id}/messages"
# 初回リクエスト
url = f"{base_url}?force=1"
while True:
res = requests.get(url, headers=headers)
if res.status_code == 204:
# メッセージなし
break
messages = res.json()
if not messages:
break
all_messages.extend(messages)
# 最後のmessage_idを使って次ページ
last_id = messages[-1]["message_id"]
url = f"{base_url}?force=1&message_id={last_id}"
# レート制限対策
time.sleep(5)
return all_messages
ポイントは message_id パラメータ。取得したメッセージの最後の ID を次のリクエストに渡すと、その ID より新しいメッセージが返ってくる。100 件ずつ繰り返して全件取得する。
force=1 のレート制限
ここにもう一つ罠がある。force=1 は 5 分に 1 回程度の制限がかかる(公式ドキュメントには明記されていないが、実際に叩いて確認した)。連続で叩くと 429 Too Many Requests が返ってくる。
# 5分待つのは現実的ではないので、sleepを入れつつリトライ
import time
def fetch_with_retry(url, headers, max_retries=3):
for i in range(max_retries):
res = requests.get(url, headers=headers)
if res.status_code == 429:
wait = 60 * (i + 1) # 1分、2分、3分と待つ
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)
continue
return res
raise Exception("Rate limit exceeded")
8 ルーム × 数十ページの取得には、素朴にやると数時間かかる。並列化したくなるが、API トークンごとのレート制限なので並列にしても意味がない。寝る前に回して朝に結果を見る運用にした。
罠 3: HTTP 204 の扱い
メッセージが 0 件のとき、API は 200 + 空配列 ではなく 204 No Content を返す。レスポンスボディが空なので、res.json() を呼ぶと例外が飛ぶ。
res = requests.get(url, headers=headers)
# NG: 204のときjson()が死ぬ
messages = res.json() # → JSONDecodeError
# OK: ステータスコードを先にチェック
if res.status_code == 204:
messages = []
else:
messages = res.json()
これは Chatwork API の設計上の特徴で、他の多くの REST API が空配列を返すのと異なる。知らないとハマる。
取得したメッセージから FAQ を作る
全件取得できたら、次は FAQ の抽出だ。3 段階のフィルタリングで絞り込んだ。
全メッセージ(数千件)
↓ ① パターンマッチ(?/でしょうか/ですか 等) → 約1,200件
↓ ② LLMフィルタ(質問かどうかを判定) → 約500件
↓ ③ 類似質問のクラスタリング・統合 → 429件
① パターンマッチ
import re
question_patterns = [
r'?',
r'\?',
r'でしょうか',
r'ですか',
r'ありますか',
r'できますか',
r'どうすれば',
r'教えて',
]
pattern = '|'.join(question_patterns)
questions = [
msg for msg in all_messages
if re.search(pattern, msg['body'])
]
② 回答の紐付け
質問だけ抽出しても意味がない。回答とペアにしないと FAQ にならない。Chatwork には [rp] タグでリプライが紐づく。
def find_answer(question_msg, all_messages):
msg_id = question_msg['message_id']
# 1. [rp]リプライを探す(最も確実)
for msg in all_messages:
if f'[rp aid={question_msg["account"]["account_id"]}' in msg.get('body', ''):
return msg
# 2. 時間的近接性(10分以内の返答)
q_time = question_msg['send_time']
candidates = [
msg for msg in all_messages
if msg['send_time'] > q_time
and msg['send_time'] - q_time < 600 # 10分
and msg['account']['account_id'] != question_msg['account']['account_id']
]
if candidates:
return candidates[0]
return None
この方法で約 7 割の質問に回答を紐付けられた。残り 3 割は会話の流れの中で暗黙的に解決されていたり、そもそも回答がないものだった。
定常運用: 差分取得の仕組み
全件取得は初回だけでいい。日常的には差分だけ取得する。状態ファイルに最後の message_id を保存しておけばいい。
#!/bin/bash
STATE_FILE=".last_message_id"
# 前回の最終message_idを読む
if [ -f "$STATE_FILE" ]; then
LAST_ID=$(cat "$STATE_FILE")
URL="https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1&message_id=${LAST_ID}"
else
URL="https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1"
fi
# メッセージ取得
RESPONSE=$(curl -s -w "\n%{http_code}" -H "X-ChatWorkToken: $TOKEN" "$URL")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" = "204" ]; then
echo "新着メッセージなし"
exit 0
fi
# 最新のmessage_idを保存
echo "$BODY" | python3 -c "
import json, sys
msgs = json.load(sys.stdin)
if msgs:
print(msgs[-1]['message_id'])
" > "$STATE_FILE"
この仕組みで、15 分おきにメッセージを取得して蓄積している。3 週間回して取りこぼしはゼロだ。
まとめ
| 罠 | 内容 | 対策 |
|---|---|---|
force=0 |
既読メッセージが返らない |
force=1 を常に指定 |
| 100 件制限 | ページネーションが必要 |
message_id で次ページ取得 |
| HTTP 204 | ボディが空で json() が死ぬ |
ステータスコードを先にチェック |
Chatwork API は素直な REST API だが、このあたりの仕様は実際に叩かないとわからない。ドキュメントに書いてあることと書いてないことの差が大きい。
8 ルーム × 全期間のメッセージから 429 件の FAQ を自動生成できたのは、この地味なページネーション処理のおかげだ。手で FAQ を書くより、過去の実際のやり取りから抽出した方が、現場の言葉遣いそのままで実用的なものになる。
Chatworkシリーズ
- #1 なぜ2026年にまだChatworkを使い倒しているのか
- #2 chatwork-client-gas、ぶっちゃけいるの?
- #3 ルームの参加者データだけで、組織の人間関係マップを作った
- #4 「Chatworkに確定連絡が来たら請求書を送る」をGASで自動化する
- #5 Chatwork MCPを繋いだら、17ルームの未読が10秒で片付いた
- #6 MCP vs GAS — Chatwork自動化の「正解」はどっちか
- #7 コンタクト承認をn8nで自動化しようとしたら、3つの罠にハマった
- #8 ChatworkにAIチームを住まわせたら、勝手に会話が始まった
- #9 Chatwork 8ルームの全メッセージからFAQ429件を自動抽出した
- #10 Webhook署名検証を入れたら全メッセージが消えた
- #11 過去メッセージを全件取得しようとしたら、APIの「100件の壁」にハマった(この記事)
- #12 Chatwork APIの「既読」は自分で制御できる
- #13 Chatwork APIのファイル機能、使ったことある?
- #14 n8nで全ルーム巡回
- #15 タスク機能をAPIで使い倒す
- #16 MCPを2アカウント同時接続したら、仕事用と事務局用が1画面で回った
- #17 【世界初かもしれない】ChatworkでClaude Code Channelsを実装してみた
- #18 Chatwork × Dify × GASで問い合わせ回答を自動提案する
- #19 RelationMapを夜間バッチで毎日自動更新する
- #20 17記事書いて見えた、Chatwork APIエコシステムに足りないもの
- #21 Googleフォームの回答をChatworkに自動投稿するGAS
- #22 Chatworkの会話を毎日AIが要約してくれる仕組みをn8nで作った話
- #23 chatwork-cliを入れたら、シェルからChatworkが操作できて世界が変わった
- #24 ChatworkのWebhookをn8nで受けるなら、HMAC署名検証は必ずやれ
- #25 Chatwork × GAS × Claude Codeで会員制講座の運用を自動化した