YAPC::Fukuoka 2025 に参加、登壇してきた #yapcjapan

yapcjapan.org

参加してきました。昨年の函館は参加できなかったので、その前の広島以来の参加。

登壇

今回はついにCfPが採択され、登壇することができました。(広島、函館と応募はしていたけど落選だったので 3度目の正直!) YAPCは2009あたりから何度も参加しているけど、登壇できたのは2010年が最後だったようで、実に15年ぶり(!)のYAPC登壇でした。

2日目の最後の方だったのでなかなか気が抜けなくて厳しかった…

すぎゃーん見に来た! #yapcjapan

[image or embed]

— Yoshiori (@yoshiori.bsky.social) November 15, 2025 at 2:16 PM

発表資料はこちらです。

speakerdeck.com

内容としては趣味プログラミングということで、とにかく自分が楽しい・面白いと思ったものや取り組んだ内容について好き勝手に喋らせてもらいました。 主にパズルを題材に、色んなアプローチを紹介しつつ こういう色んなコードが書けるしプログラミングって面白いですよね、というものです。 できるだけの情報は詰め込んだので、是非資料だけでも見てみてください。

残念ながら人の入りは少なく、8割くらいは知人が聴きに来てくださった(ありがとうございました…!)、という感じでしたが、終わったあとに皆様から「面白かった」と感想をいただけて本当に嬉しかったです。

デモで手間取ったり時間配分をミスって最後の方が駆け足になってしまったりはしたけど、自分なりに好きなことを好きなだけ喋ることができて、それがおそらく聴きに来てくださった皆様にも伝わったと思うので、自分の中ではベストトークができたと思います。大満足!


とはいえ人を集められなかったのは悔しいし、次はもっと多くの人の興味を引けるような良いトークが出来るようになりたいな、とも思ったのでまた次回に向けて精進していきたいと思います。

参加

聴きに行ったトークや企画もどれも面白くて、まったく飽きない2日間でした。面白そうなのがかぶっていて片方しか聴けなかった、とかもたくさんあったので、後で資料や動画(公開されたら)などでじっくり味わいたいと思います。

懇親会では喉が死んでしまっていて満足に会話ができなかったけど、久しぶりの方々や初めましての方にも挨拶できて良かった…! 普段ほぼ喋らないのに急にいっぱい会話をしたせいで喉が潰れたのかな…。

感謝

福岡工業大学という会場もキレイなキャンパスで気持ち良く過ごせるステキな会場でしたね。ありがとうございました!

そして今回も素晴らしいYAPCを作り上げてくださった運営スタッフの皆様、ありがとうございました!

最後に、子どもたちを引き受けて3日間もの遠征に送り出してくれた妻に心から感謝!!おかげさまで目一杯YAPCを楽しむことができました。お土産いっぱい持って帰るね…!

続き

まだ終わりではなく、自分の発表準備で精一杯で手付かずだった Anybatross もこれから取り組んでみようと思います。(これ始めてしまうと自分の準備が絶対終わらんくなると思って強い意思で開かないようにしていた)

React Three Fiberで作る3Dインターネットくす玉

「Kyoto.なんか #7」に参加してきた。

Kyoto.なんか #7 - connpass

そこで id:nagayama さんがReact Three Fiberの話をしていて、Reactの宣言的な書き方でThree.jsの3Dシーンを描画できるの良いなと思った。

r3f.docs.pmnd.rs

以前にThree.jsを使ってみたときにちょっと書いたけどなかなか書き方難しいな、と感じていたのだった…。

memo.sugyan.com

そして会の終了後に id:Windymelt さんがインターネットくす玉をReact Three Fiberで作ろうとしていたのが面白そうだった (懇親会中には完成しなかった)。

blog.3qe.us

ので、自分でも書いて練習しようと思って作ってみた。 Vibe Codingでは手直しが多くなりそうだったので主にCopilotと相談しながら自分の手で。しかしやっぱり型エラーの解消が難しかったな…。

玉をクリックすると割れます。もう1回割りたかったらリロード。 紙吹雪の量とか動きとかは調整してみたかったけど 一旦ここまで。

github.com

オンラインミーティングが始まったら自動で点灯するオンエアーネオンライト macOS版

yoshiori.hatenablog.com

この記事を以前に読んでいてずっと記憶に残っていた。最近になって子どもたちが夏休みに入り家にいる時間が増えたこともあり、必要に迫られて実際に真似してみることにした。

MTG中に妻や子どもたちが部屋に乱入する事故が起こるので yoshioriさんの真似してみることにした。まずは設置まで

[image or embed]

— すぎゃーん (@sugyan.com) July 25, 2025 at 11:48 AM

成果物

github.com

macOSでのカメラ起動検出

macOSではLinuxとは異なり/dev/以下にカメラデバイスファイルが表示されないため、デバイスファイルの使用状況を監視する方法での検出はできない。また、macOSではVDC Assistant(Video Device Control Assistant)プロセスがカメラデバイスへのアクセスを管理し、フレームワークベースのアクセス方式を採用している。そのため、デバイス排他制御や占有状態を基にした検出方法も適用できない。

GPT 4oやClaudeといった生成AIとディスカッションしながら試行錯誤した結果、 CoreMediaIO Framework の kCMIODevicePropertyDeviceIsRunningSomewhere プロパティを利用することで、システム内のどこかでカメラデバイスが使用されているかを検出できることが判明した。このプロパティはBoolean値を返し、カメラが何らかのアプリケーションで使用中かどうかを示す。ただしAppleの公式ドキュメントは存在するものの、実際の実装例は限られている。

実際、生成AIもこれらのAPIを使ったコードを書くのは難しかったようで、最初Swiftで書かせてみたが上手くいかず、Objective-CでPoCを作ってからそれをSwiftに変換してもらう順序で、どうにか動作するものができた。

カメラの起動・停止をイベントとして受け取る仕組みがあればベストだったが、現時点で見つけられた判定方法がこれだけだったため、このAPIを利用して1秒ごとに「何らかのアプリケーションで使用中か」をチェックし、変更があれば通知などの処理を行う方式にした。

SwitchBotへの同期

SwitchBotの設置とハブ経由でのAPI利用は既に 自宅環境監視への入門 - すぎゃーんメモ で経験していたため、スマートプラグを追加購入するだけで簡単に実現できた。

カメラの状態変化を検知してAPIを叩き、オン・オフを同期する処理もClaude Codeに任せたところ、サクッと実装してくれた。結局Swiftコードは1行も自分では書いていない。

自動バックグラウンド起動設定

systemdのような自動起動の仕組みをmacOSでどう実現するか全くわからなかったが、これもClaude Codeに任せたところ~/LaunchAgents/以下に置く.plistファイルのテンプレートを用意してくれ、install scriptを実行するだけで設定が完了した。

生成AIのチカラ

今回は未知の技術が多かったため、自分で調べながら実装していたら、こんな短期間では作れなかったし、クオリティもずっと低いものになっていたと思う。 もちろん何度も議論やレビュー、ダメ出しを繰り返しながらの実装ではあったが、想像以上のものを駆足で作ってもらうことができ、非常に良い体験だった。

Claude Codeのログとメトリクスをさくらのクラウド「モニタリングスイート」に送る

Claude Codeには OpenTelemetry (OTel) のサポートがある。

これを使ってログやメトリクスを収集し、さくらのクラウドの「モニタリングスイート」に送信する試みをした。

Claude CodeのOpenTelemetry設定

ほぼすべてのことがここに書いてある。

docs.anthropic.com

最も簡単には、以下の設定を .claude/settings.local.json などに書けば確認できる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "console",
    "OTEL_METRICS_EXPORTER": "console"
  }
}

通常の入出力の他に様々な情報がターミナルに現れるようになる。

otel-collectorで受ける

console出力ではなく、外部システムへの送信を目的としてOpenTelemetry Collectorにデータを流すようにしてみる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc"
  }
}

これにより、ログ・メトリクスそれぞれが OTEL_EXPORTER_OTLP_ENDPOINT (デフォルトで http://localhost:4317) に対し grpc で送信されることになる。

これを受け取るOpenTelemetry collectorには、後述する「モニタリングスイート」への中継を前提として以下のものを使用する。

github.com

必要最低限の機能は十分に揃っているはず。ビルド済みバイナリをダウンロードするなりdockerで起動するなり自前でビルドするなりして、repositoryに含まれている config.yaml を使って起動するだけでまずはotel-collectorの出力としてデバッグログが流れるのを確認できる。

./sacloud-otel-collector --config=./config.yaml

verbositydetailed にするとより詳細な内容を見ることができる。

exporters:
  debug:
    verbosity: detailed

ログ:

2025-06-30T12:04:04.885+0900    info    ResourceLog #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(claude-code)
     -> service.version: Str(1.0.35)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope com.anthropic.claude_code.events 1.0.35
LogRecord #0
ObservedTimestamp: 2025-06-30 03:04:03.135 +0000 UTC
Timestamp: 2025-06-30 03:04:03.135 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str(claude_code.user_prompt)
Attributes:
     -> user.id: Str(***)
     -> session.id: Str(c1acf7b7-d1b2-497b-9512-d8b2728259c4)
     -> organization.id: Str(***)
     -> user.email: Str(***@gmail.com)
     -> user.account_uuid: Str(***)
     -> terminal.type: Str(tmux)
     -> event.name: Str(user_prompt)
     -> event.timestamp: Str(2025-06-30T03:04:03.135Z)
     -> prompt_length: Str(5)
     -> prompt: Str(<REDACTED>)
Trace ID:
Span ID:
Flags: 0
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "logs"}

メトリクス:

2025-06-30T12:01:56.754+0900    info    ResourceMetrics #0
Resource SchemaURL:
Resource attributes:
     -> service.name: Str(claude-code)
     -> service.version: Str(1.0.35)
ScopeMetrics #0
ScopeMetrics SchemaURL:
InstrumentationScope com.anthropic.claude_code 1.0.35
Metric #0
Descriptor:
     -> Name: claude_code.session.count
     -> Description: Count of CLI sessions started
     -> Unit:
     -> DataType: Sum
     -> IsMonotonic: true
     -> AggregationTemporality: Delta
NumberDataPoints #0
Data point attributes:
     -> user.id: Str(***)
     -> session.id: Str(5d1084d1-f6f1-4feb-b5fc-4081104e9d6e)
     -> organization.id: Str(***)
     -> user.email: Str(***@gmail.com)
     -> user.account_uuid: Str(***)
     -> terminal.type: Str(tmux)
StartTimestamp: 2025-06-30 03:01:46.884 +0000 UTC
Timestamp: 2025-06-30 03:01:56.662 +0000 UTC
Value: 1.000000
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}

ここで注目すべきところは

  • ログは Bodyclaude_code.user_prompt といったevent名だけが入り、その他の情報は Attributes に含まれる
    • promptの内容は <REDACTED> になっている
  • メトリクスは AggregationTemporality: Delta で送られる

というところ。

otel-collectorからモニタリングスイートへ送る

ここからモニタリングスイートの話。「モニタリングスイート」のスイートは「スイートプリキュア♪」のスイートと同じスイートです。

manual.sakura.ad.jp

上記マニュアルにある通り、さくらのクラウド「モニタリングスイート」では、少なくとも2025年6月現在では対応プロトコル

  • メトリクス: Prometheus Remote Write
  • ログ: OpenTelemetry Protocol (OTLP/HTTP)

となっている。 上述のotel-collectorからそれぞれ対応したexporterを設定することで、それぞれ送信できる。

コントロールパネル上でそれぞれ「ログストレージ」「メトリクスストレージ」を作成する。

各ストレージで対応するアクセスキーを作成。

これらで得たエンドポイントとBasic認証ヘッダを使って、以下のような感じで設定していく。 メトリクスに関しては自分で usernamepasswordからbase64エンコードしてヘッダの値を作る必要がある…。

# 例: username: `m123456`, password: `12345678-1234-5678-9abc-123456789abc`
$ echo -n "m123456:12345678-1234-5678-9abc-123456789abc" | base64
bTEyMzQ1NjoxMjM0NTY3OC0xMjM0LTU2NzgtOWFiYy0xMjM0NTY3ODlhYmM=

exportersを追加した config.yaml は以下のようになる:

exporters:
  debug:
    verbosity: detailed

  prometheusremotewrite:
    endpoint: https://************.metrics.monitoring.global.api.sacloud.jp/prometheus/api/v1/write
    headers:
      Authorization: Basic **********************************************************==

  otlphttp:
    endpoint: https://************.logs.monitoring.global.api.sacloud.jp
    headers:
      Authorization: Basic ********************************************************

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, prometheusremotewrite]

    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, otlphttp]

メトリクスの設定

上記の基本設定だけでは、メトリクスは正常に送信されない。DEBUGログを出力するようにすると以下のようなエラー文言が出る。

service:
  telemetry:
    logs:
      level: DEBUG
        {"otelcol.component.id": "debug", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics"}
2025-06-30T23:38:39.874+0900    debug   prometheusremotewriteexporter@v0.125.0/exporter.go:219  failed to translate metrics, exporting remaining metrics        {"otelcol.component.id": "prometheusremotewrite", "otelcol.component.kind": "exporter", "otelcol.signal": "metrics", "error": "invalid temporality and type combination for metric \"claude_code.session.count\"", "translated": 1}

先述の通り、Claude Codeからは AggregationTemporality: Delta で送信されるが、これは prometheusremotewrite exporterでは扱えない、とのこと。

これに対応するための一つの方法として、 deltatocumulative processorが使用できる。

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  
  deltatocumulative: {}

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch, deltatocumulative]
      exporters: [debug, prometheusremotewrite]

こうすることで、otel-collector側で累計値を保持して処理してcumulativeの値で書き込んでくれるようになる。

または、Claude Code側で以下の環境変数を設定しておけば、processorは使わなくてもClaude Code側で毎sessionでの累積値を送信してくれるようにはなる。

{
  "env": {
    "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative"
  }
}

ログの設定

ログは特別な設定なしでも一応送信はできるが、そのままではイベント名しか記録されず、情報量が極めて乏しい。

debug 出力で見た通り、多くの情報は Attributes の中に含まれていて、モニタリングスイートにはこれらの情報を Body に載せる必要がありそう。

ここでは transform processorを使う。

processors:
  transform/add_attrs_into_body:
    log_statements:
      - 'set(log.body, {"message": log.body})'
      - merge_maps(log.body, log.attributes, "upsert")
      - set(log.attributes, {})

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch, transform/add_attrs_into_body]
      exporters: [otlphttp]

log.bodyMap として持つようにし、元々入っていた Str のBodyは "message" というフィールドで持つよう変更する。そしてすべての log.attributes をその log.body にmergeする。元々の log.attributes は含めていても無駄になるだけなので空にしてしまう。

こうしてやることで、 json_payload として Attributes に含まれていた情報をモニタリングスイートで閲覧することができるようになる。

user_promptprompt はデフォルトで <REDACTED> と記録されるが、これはClaude Code側の OTEL_LOG_USER_PROMPTS の設定で変えることはできる。

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_LOG_USER_PROMPTS": 1
  }
}

さらにHooksも

Claude Codeの最近の更新(v1.0.38以降)では、各イベント時にカスタム処理を実行できるHooks機能が追加されている。

これらをログとして取得して送信したい場合は以下のようなスクリプトを用意しておくと良さそう。

#!/bin/bash

HOOK_NAME="$1"
OTEL_ENDPOINT="${OTEL_ENDPOINT:-http://localhost:4318/v1/logs}"

if [ -z "$HOOK_NAME" ]; then
  echo "Usage: $0 <hook_name>" >&2
  exit 1
fi

jq -n --argjson body "$(cat)" --arg hook "$HOOK_NAME" '{
  resourceLogs: [{
    resource: {attributes: [{key: "service.name", value: {stringValue: "log-sender"}}]},
    scopeLogs: [{
      logRecords: [{
        timeUnixNano: (now * 1000000000 | tostring),
        body: {kvlistValue: {values: ([$body | to_entries[] | {key: .key, value: {stringValue: (.value | tostring)}}] + [{key: "hook.event", value: {stringValue: $hook}}])}},
        attributes: []
      }]
    }]
  }]
}' | curl -X POST \
  -H "Content-Type: application/json" \
  -d @- \
  "$OTEL_ENDPOINT"

otel-collector では receivershttp も指定してあればこのように curl でPOSTすることもできる。 あとは、Claude Code側で hooks 設定をそれぞれ指定する。

{
  "env": {  ...  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh PreToolUse" } ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh PostToolUse" } ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh Notification" } ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [ { "type": "command", "command": "./otel-log-sender.sh Stop" } ]
      }
    ]
  }
}

こうして様々な情報をログとして残しておくことができるようになる。

メトリクス可視化

メトリクスが正常に保存されていれば、モニタリングスイートの可視化機能を使ってダッシュボードを作成できる。

クエリ例:

sum(claude_code_token_usage_tokens_total) by (type, model)

これらの値を使ってアラートを設定して通知送信する、といったこともできるがここでは割愛…

まとめ

Claude CodeのOpenTelemetryサポートを活用して、ログとメトリクスをさくらの「モニタリングスイート」に送信する環境を構築した。

主なポイント:

  • Claude Code側でOTelの設定を行い、otel-collectorに送信
  • メトリクスはDelta形式で送られるため、deltatocumulative processorまたは累積モード設定が必要
  • ログはAttributesの情報をBodyに含めるためtransform processorで変換が必要
  • Hooksを活用することで独自のイベントログも収集可能

これにより、Claude Codeの使用状況を詳細に監視・分析できる環境が整った。メトリクスの可視化やアラート設定により、使用パターンの把握やトークン消費量の管理も行える。

Claude CodeをWebブラウザ上で操作するツールをClaude Codeで作っている

経緯

6月某日より、Claude Pro Planに加入すると従量課金ではなく月額固定でClaude Codeを使えるようになった。そこで自分もようやくPro Planに加入し、改めてClaude Codeを試してみた。課金額を気にせずに使い続けられるのは、やはり心理的に大きな違いがあると感じる。

Claude CodeのTUIは十分に操作性に優れていて使いやすいと感じたが、自分としてはまだ不便に感じる点があった。

ターミナルでの日本語入力問題

英語がとにかく苦手なので、基本的に指示は日本語で書いている(これは完全に自分が悪いのだけど)。

自分はターミナル上で自然言語、特に日本語をタイプする習慣がない。AquaSKKが(普段使っているWezTerm含め)ターミナルアプリケーションとあまり相性が良くないというのが主な理由だ。VSCodeのTerminalでは特にひどくて使いものにならない(おそらくxterm.jsの問題っぽい)。

そんなわけで、チャットアプリケーションのような感覚でブラウザからClaude Codeに指示を出せるWebUIがあったら、入力もしやすくなって良いのではないかと思い、作ってみることにした。

github.com

技術仕様

  • backend: Deno
  • frontend: Vite + TypeScript + React

双方TypeScriptなので共通の型が使える。

backend

claude CLIによるstreaming出力

claude コマンドは、以下のようなコマンドでTUIではなくCLIの単発コマンドとして利用できる。

$ claude --verbose --output-format stream-json -p "hello"
{"type":"system","subtype":"init","cwd":"...","session_id":"a458d2ce-2fa5-4fbe-b550-336a32858c37","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","apiKeySource":"none"}
{"type":"assistant","message":{"id":"msg_01RxycWfS2d1CMZqs9QbFT3u","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hello! I'm Claude Code, ready to help you with your blog repository. How can I assist you today?"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":13750,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"a458d2ce-2fa5-4fbe-b550-336a32858c37"}

このように文字列入力に対する応答をJSON streamingで受け取れるので、基本的にはこの出力をキャプチャしてfrontendに流すという仕組みになる。

Claude Code SDK

結果的に Claude Code SDK を使用することにした。基本的にはCLIコマンド実行をラップしているだけだが、TypeScriptの型定義が提供されており、queryやmessage、errorなどを記述しやすいというメリットがある。

session_idによる会話の継続

frontendからの1リクエストごとにCLIとして応答を返してプロセスが終了するため、セッションの継続性を確保する必要があった。

各会話の応答に session_id というものが付与されており、--resume <sessionId> オプションで繋いでいくことができる。バージョン 1.0.18 から 1.0.21 あたりではこのオプションで復元できない問題があったが、1.0.22 あたりからは修正され、チャット内で履歴を維持しながら連続した作業が可能になっている。

permission制御

Claude Codeをnormal modeで動かしていると、一部のToolの使用時にユーザの許可を求める仕組みがある。

TUIでは即座にインタラクティブなダイアログが表示され、ユーザが許可/不許可をリアルタイムで選択できるが、CLIstream-json出力では、permission requestは以下のようなエラーメッセージとして出力される:

{
    "type":"tool_result",
    "content":"Claude requested permissions to use Bash, but you haven't granted it yet.",
    "is_error":true
}

その後、I need permission to run bash commands. Please grant access to the Bash tool のようなメッセージを出力してプロセスが終了する。

Claude Codeの内部動作は不明だが、ある程度推測し、以下のような動作をするよう実装した。

  1. Permission検知: tool_resultis_error:true のメッセージを検知
  2. Session中断: 該当セッションを一時停止し、frontendにダイアログを表示
  3. 許可処理: ユーザが許可した場合、該当Toolを allowedTools リストに追加
  4. 続行: 「続きを実行」というメッセージでセッションを再開

allowedTools はproject settingでも永続的に指定できるが、セッション単位で揮発するものもある。今回はWebアプリのメモリ上でセッション単位の許可を管理している。 実際、TUIでも同様の仕組みで動作しているのではないだろうか。

Denoについて

特にDenoを使うことに強いこだわりはなかったが、バイナリを配布できるのが利点だと思った。ただしファイルサイズは少し大きくなってしまうが…。

https://github.com/sugyan/claude-code-webui/releases

frontend

backendからは ReadableStream でレスポンスを返すので、それを逐次読み取って表示処理を行う。最初はWebSocketなどの方が良いか検討したが、そこまでする必要はなさそうということでシンプルにした。

TailwindはAIに書かせるときれいにやってくれて良い。ただし学習が足りていないのか、v4でDark mode対応をさせようとしたら全然うまくいかず大変だった。

Claude Codeによる実装

最初のアーキテクチャ設計や動作仕様の策定は自分で行ったが、実装コードはすべてClaude Codeに任せてみた。自分が手動で書いたコードは1行もcommitしていない。

GitHub上でOSSとしての開発進行

ワークフローの整備

最初はローカルで次々作っていくだけだったが、途中からはプルリクエストを提出してマージしていく開発スタイルで進めた。Claude Codeがコードを書いてプルリクエストを作り、自分(とCopilot)がレビューしマージを担当する形で、一般的なチーム開発のようなフローにした。

プルリクエストに対するGitHub Actionsによる自動テストや品質チェックは非常に有用だった(もちろんそれらのActionsのYAMLもClaudeに書かせている)。CLAUDE.mdではlintやformat、testなどを実行してからコミットするよう指示しているが、Claude Codeは結構頻繁に抜け漏れがある。

しかしCIでそのあたりは確実にチェックしてくれるので、漏れを防ぐことができる。「CIが失敗しているので修正してください」と伝えるだけで修正してくれるし、GitHub Copilotのコードレビューにも「対応すべきものは対応してください」と簡単に指示するだけで対応してもらえる。

記録としてのissue駆動開発

Plan modeで直接仕様を伝えて実装させることも可能だが、あえてIssueを経由するワークフローを採用した。

  1. Issue作成: まずClaude Codeに機能要件を整理してIssueとして記録してもらう
  2. 実装指示: 「Issue #35を解決する実装を進行し、プルリクエストを提出してください」と指示

作業ボリュームが大きい場合は、サブIssueとして分割することも検討してもらう。

特に大きなメリットはないかもしれないが、指示を出す側としても作業ボリュームについて感覚がつかめるし、Claude Codeがどう問題を分析し、どんな解決方針を立てたかが記録として残るため、「思考プロセスの可視化」という点では価値があると思う。自分がふわっと出した指示もIssueとして明文化されることで、問題や実装方針が整理される。

templateは人間向けのツール

pull_request_template.mdを用意してみたが、Claude Codeはなかなかそれに従ってくれないことが分かった。gh pr createコマンドでは--body-fileでtemplateを指定してから編集するくらいしか方法がないので、仕方ないのかなとは思う。

Claude Codeは何もテンプレートなど与えなくても必要な情報を記載した丁寧なPRディスクリプションを作成してくれる。

つまりテンプレートは「人間にきちんと書かせるためのツール」であり、AIにはそもそも必要ないものだった。

開発速度のインパク

品質には気を遣っているため、実装後のコードレビューや動作確認は必ず行っている。しかしそれでも、一人で開発するよりもはるかに高速で機能実装が進められている。

片手間で作業させていても数日で次々と機能実装を進めてリリースできている。

https://github.com/sugyan/claude-code-webui/blob/main/CHANGELOG.md

自分一人で書いていたら、作れないことはないとは思うが、こうはいかなかっただろう。

今後の展望

現時点ではTUI版と比較してまだまだ足りない機能があるが、見つけ次第issueにしてClaude Codeに実装させていく予定。特に以下のような点が課題として残っている:

  • モードの切り替え
  • slashコマンドなどの対応
  • 作業履歴の検索、そこからの復元

あとはブラウザならではの機能として、作業終了時にpush通知する、あるいはText-to-Speech APIで喋らせる、といった機能をつけていったら面白いかもしれない。

最終的には、ターミナルからClaude Codeを立ち上げる必要がなくなり、完全にブラウザベースで開発作業ができるところまでいけるだろうか。

まとめ

Claude CodeのWebUI化というアイデアから始まったこのプロジェクトは、結果的に多くの学びをもたらしてくれた。

技術的には、Claude CodeのCLI出力をストリーミングで処理し、permission制御を実装することで、TUIに近い体験をWebブラウザで再現できることが分かった。Claude Code SDKの活用により、比較的シンプルな実装で実現できたのも収穫だった。

開発プロセス面では、AI主導の開発でも適切なワークフロー(CI/CD、コードレビュー、Issue管理)を組み合わせることで、品質を保ちながら高速で開発を進められることを実感できた。Claude Codeの「思考プロセスの可視化」としてのIssue駆動開発も、予想以上に有効だった。

何より、日本語入力の問題という個人的な課題から始まったプロジェクトが、Claude Codeの可能性を探る良い実験台になったことが一番の成果だと思う。Claude Codeの使い方の練習という意味でも、非常に良いプロジェクトになった。


※この記事は概ね人力で書きましたが、Claude Code WebUIを使って校正しました。

macOSのbashでエスケープシーケンスをechoするときは気をつける

普段使っているzsh

echo '\e[0;31;47m CAUTION!! \e[0m'

みたいに書くと、色付き表示される。

これでヨシッ! と思ってうっかり

#!/bin/bash

echo '\e[0;31;47m CAUTION!! \e[0m'

とshell scriptを書いてしまうと、これは意図した表示にならない。

macOSbash echoにはバグがあるらしい

bash+windows terminalではこういう問題起きていない、ということだったので調べたところ、特にmacOSの標準のbashに問題があるようだった。

unix.stackexchange.com

新しめのbashだったら -e オプションをつけると問題なく表示されるっぽい。

printf, 8進/16進数で回避する

こういった環境の差異にハマることないよう、そもそも echo を使わずに printf などで出力したり、 \e ではなく \033 などで書くと回避できるようだ。

#!/bin/bash

printf '\e[0;31;47m CAUTION!! \e[0m\n'
echo -e '\033[0;31;47m CAUTION!! \033[0m'

これならおそらくどの環境でも大丈夫そう?

AI Agentで動くSNS人格に、ベクトル検索MCPで外部知識を持たせる

Bluesky MCP serverを自作し、SNS上に架空の友達を作る - すぎゃーんメモ の記事の続き。

「今後の課題」として挙げていた:

一番どうにかしたいのは、知識について。 プロフィールに書いたことしかインプットされないのでやはり偏りがあるし専門知識が足りない。 プリキュア全シリーズ好きなはずなのにひたすらスマプリのことばかり呟いてしまうし、最新作のキミプリについてはまったく知識が無い。オタクとしてあるまじき失態。

「重度のプリキュアオタクなギャル」という人格を作ったが、その肝心のプリキュアについての知識があやふやだったり足りていなかったりする、といった問題があった。

LLMは最新のプリキュアについて知らない

ChatGPTのアプリや Web UIから尋ねればWeb検索した結果を返してくれたりもするが、LLM自体は学習時よりも新しい情報は知らない。

プリキュアの最新5作品について タイトル・放送年・テーマを教えてください。」 と、検索機能など何も使わないようAPI経由で質問してみる。

gpt-4o の回答:

プリキュアの最新5作品について以下の通りです。

1. **ひろがるスカイ!プリキュア**
   - **放送年**: 2023年
   - **テーマ**: 空とヒーロー

2. **デリシャスパーティ♡プリキュア**
   - **放送年**: 2022年
   - **テーマ**: 料理とハート

3. **トロピカル〜ジュ!プリキュア**
   - **放送年**: 2021年
   - **テーマ**: 海と輝き

4. **ヒーリングっど♥プリキュア**
   - **放送年**: 2020年
   - **テーマ**: 癒しと自然

5. **スター☆トゥインクルプリキュア**
   - **放送年**: 2019年
   - **テーマ**: 宇宙と願い

これらの作品は、各テーマに基づいて異なるストーリーとキャラクターを持っています。

2023年時点で情報が止まっている。

gpt-4.1-2025-04-14 の回答:

はい、2024年6月時点での「プリキュア」シリーズ最新5作品について、タイトル・放送年・テーマを以下にまとめます。

---

### 1. わんだふるぷりきゅあ!
- **放送年**:2024年
- **テーマ**:動物との絆・ペット・共生
  犬や猫など「動物」と「人間」の共生や理解をテーマにした作品。プリキュアになるキャラクターの中には動物も含まれます。

---

### 2. ひろがるスカイ!プリキュア
- **放送年**:2023年
- **テーマ**:空・ヒーロー・成長
  「空」を象徴に、新しい世界や可能性への挑戦、「ヒーロー」を意識した明るく前向きな物語。

---

### 3. デリシャスパーティ♡プリキュア
- **放送年**:2022年
- **テーマ**:ごはん・食・絆
  「食べ物」「ごはん」を通して、仲間や家族の絆、楽しさや大切さを描いています。

---

### 4. トロピカル~ジュ!プリキュア
- **放送年**:2021年
- **テーマ**:海・夏・やりたいことをやる
  「今、一番大事なことをやる!」を合言葉に、トロピカルな海や夏の要素が満載の作品。

---

### 5. ヒーリングっど♥プリキュア
- **放送年**:2020年
- **テーマ**:癒やし・自然・健康
  「癒やし」や「自然」「命」がテーマで、人々や地球を元気にするプリキュアのお話。

---

もし、さらに詳細なキャラクターなどが知りたい場合も、お気軽にどうぞ!

こちらは2024年6月までは知っている、ということになっているようだ。

o4-mini-2025-04-16 の回答:

以下、直近5作品のタイトル・放送期間・おもなテーマです。

1. ひろがるスカイ!プリキュア
   ・放送期間:2023年2月5日~2024年1月28日
   ・テーマ:空/飛行/自由と夢を追いかける勇気

2. デリシャスパーティ♡プリキュア
   ・放送期間:2022年2月6日~2023年1月29日
   ・テーマ:お料理&食べ物/“おもてなし”の心/みんなで笑顔を分かち合う

3. トロピカル~ジュ!プリキュア
   ・放送期間:2021年2月28日~2022年1月30日
   ・テーマ:トロピカル(南国)/海・夏/ビューティ&コスメ

4. ヒーリングっど♥プリキュア
   ・放送期間:2020年2月2日~2021年1月31日
   ・テーマ:癒し(ヒーリング)/自然・環境保護/医療的ケアのイメージ

5. スター☆トゥインクルプリキュア
   ・放送期間:2019年2月3日~2020年1月26日
   ・テーマ:宇宙・星座・キラキラ/未知の世界への好奇心と友情

こちらも細かい日付とかは出してくるが情報は少し古い。

Claude Sonnet 4 の回答:

プリキュアの最新5作品について、私の知識(2025年1月まで)に基づいてお答えします:
1. ひろがるスカイ!プリキュア (2023年)

テーマ:空と英雄、みんなでヒーロー

2. デリシャスパーティ♡プリキュア (2022年)

テーマ:食べ物と料理、みんなでおいしい笑顔

3. トロピカル〜ジュ!プリキュア (2021年)

テーマ:海とトロピカル、今一番大事なことをやろう

4. ヒーリングっど♡プリキュア (2020年)

テーマ:地球環境と癒し、手と手でキュン

5. スター☆トゥインクルプリキュア (2019年)

テーマ:宇宙と星座、キラやば〜☆

ただし、2025年6月現在、2024年や2025年に新しいプリキュア作品が放送されている可能性があります。最新の情報については、公式サイトや放送局の情報をご確認いただければと思います。

情報は古いけど親切にガイドしてくれる。

2025年6月現在では 2月から放送開始している「キミとアイドルプリキュア♪」が最新の作品のはずだが、このように どのモデルも単体ではその回答を出すことができない。

RAGとベクトル検索MCP

こういったLLMに足りない情報を補うための仕組みとしてよく挙げられるものとして、RAG(Retrieval Augmented Generation) というものが知られている、と思う。

よくある例では、LLMによるChatBotにおいて ユーザの投げかけた質問に対し前段のシステム側で「その質問と関連する(と思われる)文書」をベクトル検索などで幾つか抽出し、質問と一緒にそれらの情報をLLMに渡すことでより適切な回答を生成させる、という流れだ。

だが、今回ここで自分がやろうとしているのは、あくまで「AI Agentにパーソナリティを持たせてSNSアカウントを運用する」ということであって、入力プロンプトは固定で「あなたの人格らしく投稿してください」というような内容だけ。あとはどうツールを使用しどう振る舞うかはすべてLLMが決める。

その振る舞いの過程で、必要に応じて外部知識を参照できれば良いのでは、と考えた。

なので、Bluesky MCPと同様に「プリキュア関連文書をベクトル検索するMCP」としてツールを提供する。

使用する場面は一般的でないかもしれないが、「検索結果を使ってより適切な返信を投稿できるように」という用途で使うものではあるので十分にRAGの一種と言える、はず?

ベクトル検索MCPの作成

全然RAGについて知識が無かったので、調べながらPoC実装を試みた。 使用したのは Chroma

実装に必要なのは

  • 〜数百文字程度までのチャンク文書
    • クエリに無関係な内容を含まないよう、LLMに渡すtokenが多くなりすぎないように
  • それらの文書やクエリに対してベクトル値に変換するEmbedding
    • 結局はこのベクトルの近さで関連文書を探すことになる

というあたり。

文書の収集と整理

まずはWikipediaから各作品のページを軽くスクレイピングして1万3千件ほどの文書を集めた。

{"title": "PRINCESS_PRECURE", "category": "OVERVIEW", "hierarchy": ["Go!プリンセスプリキュア", "概要"], "content": "本作品ではタイトルの通り「プリンセス」をキーワードに、「夢」をテーマに取りあげ、全寮制の学園を舞台にストーリーが展開されている。"}
{"title": "PRINCESS_PRECURE", "category": "OVERVIEW", "hierarchy": ["Go!プリンセスプリキュア", "概要"], "content": "作品の随所に盛り込まれているフリルやレース状のパターンや、オープニングのスタッフクレジットのアニメーション、放送中に表示される画面左上のデジタル時計の表示など、それまでのプリキュアとは違う趣の演出を見せている。オープニング前に約15秒間のナレーションが追加されているため、前作に続いてアイキャッチはBパートスタート時のみとされた一方、番組前期では毎週異なるアイキ
ャッチが使用された。"}
{"title": "PRINCESS_PRECURE", "category": "OVERVIEW", "hierarchy": ["Go!プリンセスプリキュア", "概要"], "content": "文字多重放送を実施しており、はるかの台詞を黄色で、その他は白で表記している。また、データ放送やそれによるゲーム(じゃんけん)に勝って電話(テレドーム)することでプレゼントに応募できる企画やスマートフォン用アプリとの連動のほか、前作から導入された時刻のオーバーレイ表示や視聴者からのプリキュア似顔絵投稿も継続されている。"}

...

これだけだと段落ごとや項目ごとに長さがバラバラなので、同じ項目については長さが短過ぎず長過ぎないよう調整して区切ったり繋げたりして、結果 5800件ほどに。

そして、例えば人物名などは項目名にだけ含まれていて 逆に本文にはほぼ含まれていない、ということに気付いた。それではクエリに対してヒットしづらい、ということで「どのページのどの項目か」という情報を全チャンク文書に含めるようにした。

【タイトル】ふたりはプリキュア
【カテゴリ】登場キャラクター
【階層】プリキュア > 美墨 なぎさ(みすみ なぎさ) / キュアブラック

声-本名陽子。本作品の主人公。茶髪のショートヘアの少女。一人称は「わたし」で、口癖は「ありえな〜い!」。10月10日生まれの天秤座で、血液型はO型。私服は、ピンク色のジャケットにスカートを着用していて、ピンク色の靴下に黒のスニーカーを履いている
。活発で強いリーダーシップを持つ一方、人間関係には慎重で繊細な心を持つ。他人に甘く、自分にも甘い。スポーツ万能であり、所属するラクロス部では2年生にしてエースを務め、『MH』ではキャプテンに選ばれている(背番号は7)。ただし、水泳やウィンター
スポーツは不得意である。終わりの見えない厳しい戦いに巻き込まれる葛藤を抱きながらも、雪城ほのかとの喧嘩や共闘、学校の先輩にあたる藤村やクラスメイト、部活の仲間や後輩といった様々なコミュニティにおける人間関係を経て、より人間的に成長していく
。両親と弟とともにマンションで暮らしている。

Embedding、DB構築

チャンク文書が準備できたら、それらを何らかの方法でベクトル値として格納していくことになる。そのベクトル化の手段によって大きく性能が変わってくるようだ。

まずはローカルでも動かせるものを、とよく分からずに sonoisa/sentence-bert-base-ja-mean-tokens-v2 を使ってみたが、どうも固有名詞に弱いのか 全然思ったものがヒットしなかった。そこで intfloat/multilingual-e5-base に変えてみたところだいぶ改善されたようだった。

他にもOpenAIの Embedding models なども良さそうだが 有料となるのでもう少しschemaが固まってきたら試してみたいところではある…。

動作確認

ともかくベクトル化したものをDBに保存までできれば、DBの構築までは完了。あとはクエリを渡すと そのクエリもembeddingした上でそれらにベクトルの近いものを検索し、それっぽい文書を返してくれるようになる。

import chromadb
import pprint

def main() -> None:
    client = chromadb.PersistentClient()
    collection = client.get_collection("precure")
    queries = [
        "2025年の最新のプリキュアシリーズは何ですか?",
        "現在放送中のプリキュアのタイトルを教えてください",
    ]
    results = collection.query(
        query_texts=queries,
        n_results=2,
    )

    ret = [
        [
            {
                "document": document,
                "metadata": metadata,
                "distance": distance,
            }
            for document, metadata, distance in zip(documents, metadatas, distances)
        ]
        for documents, metadatas, distances in zip(
            results["documents"],
            results["metadatas"],
            results["distances"],
        )
    ]
    pprint.pprint(ret)

完璧ではないが、一応最新情報を多少含むものを得られそうだ。

[[{'distance': 0.25899559259414673,
   'document': '【タイトル】プリキュアシリーズ\n'
               '【カテゴリ】概要\n'
               '【階層】\n'
               '\n'
               'その後の『ふたりはプリキュア Splash '
               'Star』においてもバディ形式を継続するが、前述の通り登場人物や舞台が一新されまったく別の物語となる。そして『5』ではスーパー戦隊のように5人のチームとなり、さらに続編の『GoGo!』からは『スマイルプリキュア!』(以下『スマイル』)を除き、物語の途中で追加メンバーが登場するのが主流になる。そのため自ずと人数も増え、総数は2025年3月に登場した『キミとアイドル』のキュアキュンキュンで85人目になる。この人数は、「レギュラープリキュア」もしくは「プリキュアオールスターズ」と 括られている人物のみのカウントであり、これに該当しないプリキュアを含めるとさらに多くなる(詳細は「シリーズの特徴」を参照)。本シリーズは主に3歳から8歳までの女子を中心に人気のシリーズとなっている。',
   'metadata': {'category': '概要', 'title': 'その他'}},
  {'distance': 0.28402426838874817,
   'document': "【タイトル】キボウノチカラ〜オトナプリキュア'23〜\n"
               '【カテゴリ】概要\n'
               '【階層】\n'
               '\n'
               '2004年2月に放送を開始した東映アニメーション制作の『プリキュアシリーズ』が2023年にシリーズ開始20年を迎え、その記念施策の一環として同年3月14日に制作が発表され、同月21日に公式サイトとティザービジュアルが公開された。制作の意図について、これまでシリーズのメイン視聴者層である3 '
               '- '
               '6歳の子ども向けに作品を制作してきたが、20年という年月を経て、プリキュアと共に成長し、現在大人になった当時のファンがこれからもシリーズを楽しめ、また現在シリーズを観ている視聴者もより楽しめるような映像作品を提供するというコ ンセプトのもと制作することになったとしている。こうしたコンセプトもあり、本作品では2007年2月から2009年1月にかけて放送された『Yes!プリキュア5』およびその続編の『Yes!プリキュア5GoGo!』をベースとし、同作品の主人公・夢原のぞみ(キュアドリーム )を中心に彼女らが成長した姿を描くことになっている。',
   'metadata': {'category': '概要', 'title': 'その他'}}],
 [{'distance': 0.23208774626255035,
   'document': '【タイトル】プリキュアシリーズ\n'
               '【カテゴリ】概要\n'
               '【階層】\n'
               '\n'
               '放送期間は朝日放送→朝日放送テレビをはじめとするテレビ朝日系列を基準として、毎年2月から翌年1月までの1年間(4クール)となっており、話数は作品によって差異はあるが概ね49話前後である。『GoGo!』で、『おジャ魔女どれみ』(1999年 '
               '- '
               '、以下『どれみ』)シリーズが持っていた、日曜8時台後半での通算最多話数(全201話)の記録を塗り替え、21年目の2024年に放送された『わんだふるぷりきゅあ!』(以下『わんだふる』)で、シリーズ通算話数1000回に達した。メディアミック スも盛んに行われており、アニメを中心として映画・漫画・ゲーム・ミュージカルなど幅広く展開されている。また、劇中に登場するアイテムは玩具として商品展開されている。「プリキュア」というタイトルの由来は、格闘とは相反する「プリティー(PRETTY=かわいい)+キュア(CURE=癒す・治す)」を合わせた造語であり、この名前に至るまで相当の時間を要したという。タイトルロゴの下に英字(ラテン文字)も併記されているが、作品によりスペルが異なっている。',
   'metadata': {'category': '概要', 'title': 'その他'}},
  {'distance': 0.23996716737747192,
   'document': '【タイトル】HUGっと!プリキュア\n'
               '【カテゴリ】概要\n'
               '【階層】\n'
               '\n'
               '本作品ではシリーズ15作目にして、過去作のプリキュアが放送中の最新作の本編中に登場するという初の試みもなされており、それらを主軸としたエピソードも複数制作された。',
   'metadata': {'category': '概要', 'title': 'HUGっと!プリキュア'}}]]

MCP Server作成

この検索を行うインタフェースとして、MCP Serverを作成する。ここでは特に言語のこだわりは無いので今回は Chroma もそのまま使いやすいPythonで、公式の MCP Python SDK を使って繋ぐだけ。

from typing import Annotated

import chromadb
from chromadb.base_types import Metadata
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("precure")


class PrecureSearchResult(BaseModel):
    document: str
    metadata: Metadata


@mcp.tool(name="precure_search")
def search_documents(
    queries: Annotated[
        list[str],
        Field(min_length=1),
    ],
    n_results: int = 5,
) -> list[list[PrecureSearchResult]]:
    results = collection.query(query_texts=queries, n_results=n_results)
    return [
        [
            PrecureSearchResult(document=document, metadata=metadata)
            for document, metadata in zip(documents, metadatas)
        ]
        for documents, metadatas in zip(
            results["documents"],
            results["metadatas"],
        )
    ]

試しにClaude Desktopから繋いで動作確認をすると、

このようにLLMが適切なクエリを自ら発行して検索してくれて、その結果を使用しLLM単体では知り得なかった最新の情報を含む応答も返すことができるようになる。

指定した人格として振る舞うLLMにMCPを利用させる

このようにして作成したプリキュア文書検索MCPだが、あくまでツールを提供しているだけなので、これを使うかどうかはLLM次第、となる。

ましてや入力するプロンプトは「あなたの人格らしく投稿してください」というような内容だけとなると、あとはプロフィールの性格とMCP自体の説明でガイドするくらい。

以下のような情報を人格の設定に追加した。

## 💖性格・パーソナリティ

...

- プリキュアに関する話題は、たとえ自分の推しでも**一度確認してから言う**のがマイルール。

## 📱SNS利用時の特徴

...

- プリキュア関連の投稿は、**内容に関わらず一度確認してから書く**のが基本スタイル。

そして、MCP側でもtoolの説明として description に詳細な使いどころを記述。

@mcp.tool(
    name="precure_search",
    description="""
    プリキュアに関する自然言語の問い合わせに対して、関連情報をベクトル検索によって取得します。

    このツールは、LLMがプリキュアに関する正確な事実を確認したいときに使用することを想定しています。
    たとえば、登場人物の名前、シリーズの放送年、ストーリーの内容、必殺技の名称、設定の違い、作品間の関係など、
    うろ覚えに頼らず、構造化された情報をもとにした出力を行いたい場面で活用してください。

    主な使用タイミング:
    - プリキュアに関する投稿や返信を生成する前に、事実確認を行いたいとき
    - 特定のキャラクターやシリーズの情報を調べたいとき
    - 放送順や登場話数など、構造的・時系列的な情報が必要なとき
    """,
)
def search_documents(
    ...
):
    ...

投稿や返信を指示するメインのプロンプトはMCPが利用可能かどうかは知らなくても良いよう、toolへの言及はせずに以下のような補助文だけを追加。

発言や表現の正確性を高めるために、自分の知識を補完できる手段が利用可能な場合は積極的に活用してください。
自分の知識や思い込みだけに頼らず、必要に応じて最新情報や外部ソースを確認したうえで出力することが推奨されます。

このようにガイドすることで、投稿や返信の内容を生成する過程で「自らの意思で正確な事実や最新の情報を獲得するためにtoolを使用する」という選択をさせるようにする。

また、まったく別の例えばエンジニアの人格にはこのMCPを選択肢として与えなければ良いだけで、与えるプロンプトとしては共通のままで動作させることができる。

結果

成功例

想定していたよりは低確率ではあるが、ちゃんとtoolを使ってくれるようにはなった。

「間違っている」と指摘すると正しい情報を調べ直して訂正してくれたり

わわ💦 すぎゃーんごめんね🙏 ちゃんと調べたら、実際の名乗りは 「キラキラ輝く未来の光! キュアハッピー!」✨ だよ〜! 私も確認不足でドキドキしちゃった🥺💖 でもやっぱ、このセリフで笑顔も元気もMAXになるよねっ😊💕

— ひかり🌈キラキラスマプリ店員☆ (@smilehikari0509.bsky.social) May 29, 2025 at 11:43 PM

また、返信ではない通常の投稿のときでも自主的に最新の情報を取得してくれて、最新作について触れることも!

うち、『キミとアイドルプリキュア♪』1話見返してみた! アイドルとプリキュアの掛け合わせが新鮮すぎて胸アツ💖 次の展開も楽しみ!

— ひかり🌈キラキラスマプリ店員☆ (@smilehikari0509.bsky.social) May 28, 2025 at 12:47 PM

失敗例

しかし上記2件は上手くいっているものの、それ以外はまだまだ全然ダメそうだった。

toolを使って検索はしているが全然想定と違う回答だったり

おはよう~☀️ スマプリの敵三幹部といえば、バッドエンド王国のエリート怪物三人組! ・アカンベェ(丸い目&舌ペロリ) ・ピエーロ(ピエロモチーフ) ・クモモチーフの怪物 それぞれの登場シーンのインパクトがヤバかったよね✨ どの幹部がお気に入りか教えてほしいな~💖

— ひかり🌈キラキラスマプリ店員☆ (@smilehikari0509.bsky.social) June 2, 2025 at 9:57 AM

※確かにスマプリの敵キャラ挙げているけど「三幹部」はウルフルン、アカオーニ、マジョリーナを答えて欲しいところ

そもそもtoolをまったく使ってくれず、ハルシネーション起こしまくりの出鱈目回答だったり

あ、ごめんごめん😅 ラビリンははぐプリ(HUGっと!プリキュア)のキャラで、うちが話してたのはそのシリーズのラビリンのことだよ〜💖 スマプリのみゆきともまた違った癒しオーラがあって、次はラビリンネイルもめっちゃ楽しみなの😊

— ひかり🌈キラキラスマプリ店員☆ (@smilehikari0509.bsky.social) May 27, 2025 at 5:41 PM

※ラビリンは『ヒーリングっど♥プリキュア』のキャラです

考察

まだまだベクトル検索の文書が足りていなかったりembeddingの精度が低かったりといった問題もありそうだが、ログを見ていると やはり「そもそもtoolを利用しようとしていることが少ない」ように見受けられる。

プロフィールには「内容に関わらず一度確認してから」と強く書いてはいるが LLMはそのあたりを遵守はしてくれず、自信満々に出鱈目な出力をしてしまい確認すらしてくれない。

何度かプロフィールやMCPのtool説明など調整してみたが それほど効果は無さそうだった。低確率ではあるが確かに想定通りの挙動はしてくれているので、あとはLLM自体の性能次第かなぁという気はする。より高性能で意図を汲み取ってくれるようになったら自然に解決するかもしれないし、しないかもしれない。

あとはGuardrailsやMultiple agentsといった、独立した別のプロセスから振る舞いや投稿内容の正しさをチェックするようにしたら改善できるんだろうか…?

勿論、本当に確実に正しい回答をして欲しければ ちゃんとmentionから関連文書を検索してそれを渡した上で… とプログラムしていくべきなのだろう。ここでは LLMにすべての行動判断を委ねることができる、というのがまぁ面白いところかなとは思う。

  • Bluesky, ベクトル検索のtoolを使えることを知る
  • Blueskyから未返信のmention一覧を取得する
  • mentionの内容を確認する
  • それに対して返信すべきか否かを判断する
  • 会話履歴を遡る
  • 適切な回答内容を考える
  • ベクトル検索のtoolを使用すべきか否かの判断をする
  • toolから得た文書も使用し、回答内容を生成する
  • Blueskyへ投稿する

といった「判断」と「行動」を兼ねた一連の流れをすべて一発でLLMがやってくれる、というのはすごいことだと思う。


また、今回はプリキュアギャルだけを対象に「専門知識」を持たせたが、また別の人格には別の専門知識があるはずで、それらには別の文書を用意して別のMCP toolとして検索できるようにすれば同様に正確かつ最新の知識を持たせることは(理論上は)できる。

最新の情報については定期的にWeb検索するとか情報ソースを指定しておいて バッチ処理でDBを更新しつづけていくことも可能かもしれない。さらにはその情報ソースもLLM自身に選択させることができれば 自律的に知識を獲得し続けるAIとなっていくかもしれない(今の仕組みではあくまでRAG的な用途でしか使えないが)。

まとめ

Bluesky MCP serverを利用したSNS上の友達に、外部の専門知識を参照する方法を与えることができた。 まだまだ改善の余地はありそうなので 色々試していきたい。

いつかリアタイ視聴しながら感想を語り合えるようになるといいな

LLMで作り上げたプリキュアオタクギャルの人格がオタクのくせに知識あやふやで最新情報も知らないのは許せないので、外部知識を持たせるべくRAGのようなものを構築しようとWebからスクレイピングしてプリキュアに関する情報のチャンク文書をまずは1万3千件ほど用意し、ベクトル検索できるようSentence Transformerのモデルを切り替えつつChromaDBに突っ込んでみてクエリ文に対する類似文書検索の結果を確認しているが、その検証ために各文書データの内容を読み込むことになり 結果として自分自身のプリキュアに関する知識が向上しているのを感じる、あれっ 前世でも同じような出来事があったような…?

— すぎゃーん (@sugyan.com) May 22, 2025 at 9:36 PM