THINKING MEGANE

tmuxのpopupを使ってターミナル画面でClaude Codeと棲み分ける

Claude CodeをはじめとするCLI型のコーディングエージェントはターミナル画面中の一定領域を占有します。

元々自分は、ターミナルマルチプレクサであるtmuxを利用していて、リポジトリをプロジェクトとみなし、一つのウィンドウを対応させ、その中でペインを分ける構成をとっていました。 その配置と各ペインのサイズのなかで、エージェントの入る余地はなく、どうしてもペイン内で共有させるか、異なるウィンドウを割り当てる必要がありました。 しかしながら、役割を共有させたペインの裏側や異なるウィンドウに散らばったエージェントのセッションを探し回るのは非常に面倒に感じていました。

そんな中、以下のtmux popupの活用記事がしっくりきたので、参考にさせてもらいました。

利用の様子

Popup

画面の中央の前面領域がtmux popupでClaude Codeを動かしている箇所です。 このpopupの表示・非表示を指定したキーバインドで瞬時にトグルできる体験が非常に良いです。 トグルの様子は、上記の記事のデモが分かりやすいのでぜひご覧いただきたいです。

特に、tmuxのセッションのdetachとattachを行うため、popupを隠しても処理が継続され、ターミナル領域をお気に入りの配置のままコードの確認などが快適に行えるのが非常に有用です。 また、以下の設定で見ていただける通り、tmux側のpopupやキーバインドを使うため、操作中のClaude Codeに干渉しない点もポイントが高いです。

(余談ですがIS_DEMO環境変数をつけてClaude Codeを起動すると組織名やメールアドレスが非表示になってこういうスクリーンショットの時に便利です)

実際の設定

上記の記事はfishだったのでzshで使うために以下のようにしました。

functions/_tmuxpopupでカレントディレクトリ(リポジトリ名)をベースにセッション名を組み立て、セッションがなければ作成、あればattachとdetachをトグルできるようにしています。 セッション名の組み立てのところを自分の環境に合わせると使いまわせると思います。

function _tmuxpopup() {
  local initial_cmd="${1:-}"
  local title="${2:-}"

  local width='80%'
  local height='80%'

  local session
  session="$(tmux display-message -p -F '#{session_name}' 2>/dev/null)" || return 1

  local pane_path
  pane_path="$(tmux display-message -p -F '#{pane_current_path}')" || return 1

  local home_src="${HOME}/src"
  local key=""

  if [[ "$pane_path" == ${home_src}/*/*/*(|/*) ]]; then
    local rest="${pane_path#${home_src}/}"
    local parts=(${(s:/:)rest})
    local org="${parts[2]}"
    local repo="${parts[3]}"
    repo="${repo%-wt}"
    key="${org}/${repo}"
  else
    key="${pane_path:t}"
  fi

  local safe_key="${key//\//_}"
  safe_key="${safe_key//[^A-Za-z0-9_.-]/_}"

  local popup_session="popup_${title}_${safe_key}"

  if [[ "$session" == popup_* ]]; then
    tmux detach-client
    return 0
  fi

  local exec_cmd
  if [[ -n "$initial_cmd" ]]; then
    exec_cmd="tmux attach -t ${popup_session} || tmux new -s ${popup_session} '${initial_cmd}'"
  else
    exec_cmd="tmux attach -t ${popup_session} || tmux new -s ${popup_session}"
  fi

  tmux display-popup \
    -d "#{pane_current_path}" \
    -xC -yC \
    -w "$width" -h "$height" \
    -T "$title" \
    -E "$exec_cmd"
}

上記の関数はpopup時に起動したいコマンドとpopupのタイトルを受け付けられる共通関数なので、Claude Codeを立ち上げるため具体的な実装をfunctions/_tmuxpopup-claudeに記述しました。

function _tmuxpopup-claude() {
  _tmuxpopup "claude" "claude"
}

上記の関数たちを.zshrcでautoloadしておきます。これでこの関数がいつでも呼び出せるようになります。

autoload -Uz _tmuxpopup
autoload -Uz _tmuxpopup-claude

実際の呼び出しはコマンドではなくtmux.confにキーバインドとして登録しておきます。こうすることで、popup内のClaude Code起動中でもpopupをいつでも隠すことができます。

bind , run-shell 'zsh -ic "_tmuxpopup-claude"'

これでprefix + ‘,‘でpopupをトグルできるようになりました。非常に快適です。

並列実行

これだけだと再紹介にしかならないので、並列実行のために試しているものも紹介しておきます。

ccmanagerはClaude Codeをはじめとするコーディングエージェントの並列実行を管理するためのツールです。 エージェント単位でgit worktreeを割り当てることができるので、並列実行時のコンフリクトなども回避することができます。

上で紹介した_tmuxpopupは起動したいコマンドを差し替えるだけで使えるのでccmanagerを呼ぶようにするだけで良いです。

function _tmuxpopup-ccmanager() {
  _tmuxpopup "ccmanager" "ccmanager"
}

これでプロジェクトごとの並列実行はかなり便利になりますが、いわゆる承認疲れの現象が発生するため、ある程度の権限を持たせた上で安全に実行したくなります。 そこで、Dev Containerと組み合わせます。 幸い、ccmanagerはDev Containerの利用も視野に入れているようで、探すと専用のオプションがあります。 それを踏まえて、先の関数を以下のように変更できます。

function _tmuxpopup-ccmanager() {
  _tmuxpopup \
    'ccmanager --devc-up-command "devcontainer up --docker-path podman --workspace-folder ." --devc-exec-command "devcontainer exec --docker-path podman --workspace-folder ."' \
    'ccmanager'
}

podmanを使う場合のオプションが含まれているので環境に応じて適宜読み替えていただければと思います。 また、予めDev Container CLIもインストールしておく必要があります。

これで、プロジェクトのDev Containerの設定(.devcontainerディレクトリ)を使ってccmanagerがよしなにやってくれます。 なお、コンテナ内にClaude Codeが含まれる必要があるため、Dev Container Featuresなどで追加しておくと良いです。

以下のように設定し、Claude Codeのpopupと使い分けることができます。

bind . run-shell 'zsh -ic "_tmuxpopup-ccmanager"'

おわりに

tmux popupを使うことでClaude Codeとターミナル領域をうまく棲み分けることができて非常に快適になりました。 しかしながら、並列実行の部分は実はまだ試行錯誤中です。 特に、Dev Container + git worktree + Claude Codeの相性はあまり良くないと思っていて、マウント周りを工夫する必要があり、なかなか全てのリポジトリで気軽に使えてないのが現状です。 また、Dev Containerであれローカルであれ、単純に自分が起点にエージェントとやりとりする構造から逆転させないと目指す並列は近づけないなあとも思っているので、引き続き試行錯誤していきたいと思います。

プロジェクトのスケルトンをテンプレートから展開するstampというCLIツールを作った

CLIツールを多く作っていると、最初のコミットまでに機械的に準備する自分なりの一式(利用するコマンドラインパーサーによる初期実装、GitHub Actionの定義、Makefile、ライセンスファイル等々)ができてくるかと思います。 このような一式を予め用意しておくことが、普段通りの開発作法に従ってスムーズに開発を始めるための土台となります。 また、最近では、コーディングエージェントへ依頼する際に、このような一式を揃えて実装時の補助線としてもらう方が、認識が揃うことも多いように感じています。

自分の場合、この一式の準備には直近のリポジトリからコピーしてくることが多いですが、細々とした置換などが面倒でした。 秘蔵スクリプトを持っている方も多いのではないでしょうか。

このようなニーズのため、プロジェクトのスケルトンを管理するツールが開発されています(copierなどがあるようです)。 一方で、これらのツールはテンプレートの適用後の継続的な同期まで面倒を見るために、プロジェクト開始時に一度展開できれば良いという自分のユースケースに対しては機能的に持て余してしまう印象がありました。

そこで、自分用にstampと言うCLIツールを作りました。

機能は単純で、予め用意しておいた、テンプレート一式(ツールがスタンプなのでシートと呼んでいます)をディレクトリ構造のままコピーしてくるだけです。 各ファイルはGo言語のテンプレートとして処理されるため、{{.name}}のような記述に対して、動的な値を設定できます。

# my-appシートにhello.txtを登録(拡張子.stampでテンプレートとして解釈)
$ echo "Hello {{.name}}!" > "$(stamp config-dir)/sheets/my-app/hello.txt.stamp"
# my-appシートを適用
$ cd /path/to/workspace
$ stamp -s my-app name=alice
# 結果を確認
$ ls
hello.txt
$ cat hello.txt
Hello alice!

chezmoiをご存知の方は、これを都度任意のディレクトリへapplyできるものと考えてもらうと想像しやすいかもしれません。

使用例

自身の具体的な利用ケースとして、Go言語のCLIのリポジトリを作成するシートを用意しています(というかこのために作った)。 拡張子.stampのファイルに含まれるモジュール名などを作成したいCLIツールの名称等に合わせてコピー時に設定することができます。

$ tree $(stamp config-dir)
~/.config/stamp/
├── sheets
│   └── go-cli            # go-cliというシート名で配下のディレクトリ構造を管理
│       ├── bootstrap.sh  # go mod tidyやgitのinitial commitなどを行う(後述)
│       ├── cmd           # 以下はCLIツールのスケルトンやMakefile、LICENSEなど
│       │   ├── cli_test.go
│       │   ├── cli.go.stamp
│       │   └── version.go
│       ├── go.mod.stamp
│       ├── internal
│       │   └── greet
│       │       ├── greet_test.go
│       │       └── greet.go
│       ├── LICENSE.stamp
│       ├── main.go.stamp
│       ├── Makefile.stamp
│       └── README.md.stamp
└── stamp.yaml            # ユーザー名などのデフォルト値を管理(後述)

以下のように実行すると、テストが通る状態でプロジェクトを開始できます。また、GitHubにpushすればtagprを用いたリリースの設定まで完了した状態になります。

APPNAME=sample; \
mkdir "$APPNAME" \
  && cd "$APPNAME" \
  && stamp -s go-cli appname="$APPNAME" \
  && sh bootstrap.sh

興味がある方はこちらを参照ください。シートをさらにchezmoiで管理していますが内容はそのまま確認できます。

便利な機能

stampの基本機能はこれだけですが、利用にあたり少し便利な機能ももう少し作っているので軽く紹介します。

シート登録の支援

既存のディレクトリやファイルをシートとして登録するためのcollectサブコマンドを用意しています。

# カレントディレクトリを再帰的にテンプレートとして(.stamp拡張子をつけて)
# sampleシートとして登録
$ stamp collect -s sample -t .

デフォルト値の指定

$(stamp config-dir)/stamp.yamlにシートのテンプレート展開時にデフォルトで指定する値をキーごとに設定しておくことができます。 コマンド実行時に指定があればそちらが優先されます。

year: 2026
username: monochromegane

パラメータ不足チェック機能

事前にテンプレートに必要なパラメータの値を覚えておいたり覗いて回って思い出すのは時間がかかります。 stampでは、必要なパラメータをうろ覚えのまま実行しても、不足があれば適用前に教えてくれます。どのファイルに何のパラメータが必要かが分かるので、便利です。

例えば、先ほどのgo-cliでは、stamp.yamlで定義しているデフォルトの値の他に、必ずappnameと言うキーに対して値を与えなければなりませんが、これをせずに実行するとこういうエラーがでます。

# stamp -s go-cli appname=monochromeganeとして呼ぶべき
$ stamp -s go-cli
Error: stamp failed: missing required template variables:

  - appname
    used in:
      - .gitignore.stamp
      - LICENSE.stamp
      - Makefile.stamp
      - README.md.stamp
      - cmd/cli.go.stamp
      - go.mod.stamp
      - main.go.stamp

Provide missing variables using:
  - Command line: stamp -s <sheet> -d <dest> appname=<value>
  - Config file: Create stamp.yaml in sheet or config directory

その他

将来シートを増やした際の共通シートが欲しくなることを想定して、複数シートを同時に指定できる機能なども設けています。

なお、機能をシンプルに保ちたいため、適用後に実施したいスクリプトなどを実行する機能は持たせないことにしました。 ひとまずは、シート中に以下のようなbootstrap.shなどを用意し、stampコマンド実行後に続けて実行する運用にしています。

#!/usr/bin/env sh
set -e

go mod tidy
git init
git add . ":!./$0"
git commit -m "Add project skeleton"
make test

rm -- "$0"

おわりに

今回のツール実装はClaude Codeに任せ、コマンドラインとしての使い心地の観点からコメントしながら進めました。 ただし、出発点は上述のシート相当のプロジェクト構成を与えた上で実装を開始してもらったため、実装の方向性については想定した範囲に収まりやすかったかなと感じています。

CLIツール開発の着手時の迅速化のみならず、コーディングエージェントへの実装補助線を与えるという点でも役立つツールかなと思いますので、よければ使ってみてください。

dotfilesを5年ぶりに整備した

なんとdotfilesも5年更新していなかった。 なにかと気忙しいのではあるが、木こりのジレンマに陥らないよう再起動していきたい。

さて、昨年末にMacbook Proを新調してまっさらな開発環境が手に入ったのに合わせてdotfilesを見直した。

大きくは以下。

  • dotfiles管理をchezmoiに変更
  • 開発ツール管理をmiseに変更
  • ターミナルエミュレーターをGhosttyに変更

以下、それぞれについて簡単にメモをしておく。

chezmoi

各ツールの設定などは大幅に変えていないのだが、dotfiles管理にはchezmoiを導入した。 これまで自作インストールスクリプトは、メンテナンスできていなかったし、設定ファイルの変更後のリポジトリへの反映ルールのようなものがなく反映漏れなど生じていたので、これを機にchezmoiの流儀に任せることにした。

設定ファイル

既存dotfilesがある場合、ファイルをchezumoi addしていくと、chezmoiの命名規則に沿ったファイル名でリポジトリに加えていくことができる。 命名規則はホームディレクトリ以下のディレクトリもしくはファイル名のそれぞれについて.dot_に置き換えたものになる。 例えば、~/.config/zsh/.zshrcであればdot_config/zsh/dot_zshrcとして管理する。

自分の場合は、このタイミングでXDG Base Directoryに合わせるようにもしたかったのと新しいPCで~が綺麗だったのもあり、chezmoi側(chezmoi cdで移動できるリポジトリのclone先。~/.local/share/chezmoi)で上記の命名に合わせて変更した上で、chezmoi applyをすることで反映する方法を取った。

$ chezmoi init git@github.com:monochromegane/dotfiles.git
$ chezmoi cd
$ mv zshrc dot_config/zsh/dot_zshrc
(snip)
$ chezmoi apply

今もリポジトリへの反映を忘れないよう基本はchezmoi側で修正してapplyをする運用にしている。

なお、dotfiles内の実行権限が必要なファイルについてはファイルに属性付与ではなく、bin/executable_git_diff_wrapperのように命名規則で制御する必要がある。

インストールスクリプト

chezmoiではインストールスクリプトも管理できる。 dotfilesリポジトリの直下におくこともできるが.chezmoiscriptsという名前のディレクトリを作ると複数スクリプトがあっても整理されるのでこの構成を採用した。

管理用ツールを導入するからには動作確認の必要な自作スクリプトの役割は最低限にしたかったので、パッケージ管理ツールのインストールのみに抑え、各パッケージのインストールなどはパッケージマネージャーに任せるようにしている。

具体的には、ツールや開発環境のパッケージ管理のためのHomebrew、miseと、Vim用のミニマルなパッケージ管理であるminpacだけをインストールしている。

$ chezmoi cd
$ ls .chezmoiscripts
run_once_after_01_install-brew.sh   run_once_after_03_install-mise.sh
run_once_after_02_install-minpac.sh

上記のような命名にすることで、apply後に一度だけスクリプトが順番に実行される。

mise

開発環境やツール類はHomebrewで雑多に入れていたのと、言語ランタイムの*env系が増えてしまっていたのを整理すべく、miseに寄せることにした。

“The front-end to your dev env"というコンセプト通り、開発環境構築に関連する多くのことができるのだが、ひとまずは、グローバルに各種言語ランタイムといくつかのツールを入れるようにしている。 ディレクトリごとにmise.tomlを作って利用するツールを使い分けられるのはとても良さそう。

例えば、このブログで使っているhugoコマンドなどはブログ用のリポジトリでだけ使えれば良いのでmise use go:github.com/gohugoio/hugo@latestとすれば、そのディレクトリ配下では~/.local/share/mise/installs/go-github-com-gohugoio-hugo/0.154.2/bin/hugoがPATH環境変数に追加されてコマンドが認識される状態になる。

なお、caskが便利なのもあり、しばらくはBrewfileによるHomebrewと併用していくことになると思う。

Ghostty

長年iTerm2を使っていて特に不満はなかったが、chezmoi導入にあたって設定をファイルで管理したくなり、Ghosttyを使ってみることにした。

“Zero Configuration Philosophy"を謳っているだけあり、以下の設定で十分、動作も軽快で特に問題なく利用できている。念の為iTerm2もまだ入れているがこのまま乗り換えで良さそう。

command = /opt/homebrew/bin/zsh -l
mouse-hide-while-typing = true

# Theme
theme = Dracula+
background = #0f1015

# Title bar
macos-titlebar-proxy-icon = hidden

# Font
font-family = Moralerspace Neon
font-thicken = true
font-thicken-strength = 1
font-feature = -dlig
font-size = 15

# Cursor
shell-integration-features = no-cursor
cursor-style = block
cursor-style-blink = false

# TERM
term = "xterm-256color"

フォントとテーマ

せっかくなので、フォントやテーマも変更してみた。

Ghosttyの標準だと日本語表示が微妙だったので、フォントとしてMoralerspaceを導入し、Neonを設定し、上記設定にあるように太め大きめにしている。

また、テーマには、ダーク系のテーマでVim側ともマッチするものとして、Dracula+テーマを採用した。 Dracula Theme for Ghosttyとしてもあるが、標準で選択できるようになっている。 Vim側もDracula Theme for Vimとして公開されているので、minpackで以下のように設定を追加した。

call minpac#add('dracula/vim', {'name': 'dracula'})

テーマそのままだと背景がグレー寄りで個人的にはややぼやけたように見えてしまうので、Ghostty側でbackgroundを設定し、以下のようにctermbgをNoneにすることでGhostty側の背景色と同じにして統一感を出している。

"colorschemeを設定する
function! s:customize_dracula_bg() abort
  highlight Normal       ctermbg=None
  highlight CursorLine   ctermbg=None cterm=underline
endfunction

augroup DraculaCustomization
  autocmd!
  autocmd ColorScheme dracula call s:customize_dracula_bg()
augroup END

colorscheme dracula

GhosttyでDraculaで、背景色#0f1015の青みがかった夜空みたいな色でコンセプト的にも統一できてなかなか気に入っている。

その他

いわゆるコーディングエージェント系の設定も見直していってるが長くなるのでまた次回とする。

後記的なもの

昨年、ブログを一本も書いていないことに気づいたのが今日。 年始に『「書くこと」の哲学 ことばの再履修』を読みながら、今回の記事のような、やったことや事実ベースの「書きえる」ことすら書かずして、何を書けようという気持ちからブログ再開してみたのであるが、ただ書き出すことの爽快さを思い出せてとても良かった。 気負わずに再開していきたい。

2024

今年は、九州大学大学院システム情報科学府博士後期課程を修了し、博士(情報科学)の学位を授与されたことが、一番のハイライトだった。 指導くださった先生方、ペパボ研究所の皆さんをはじめとする同僚、切磋琢磨した社会人学生の皆さん、そして家族へ、改めて心から感謝したい。

達成感でいっぱいであるものの、年度中の博士論文執筆と事業への貢献の両立は、想像以上に大変だった。 研究においては元々、事業への貢献も求められるため、自身の専門領域から様々な分析や提案、実装を並行して進めていたが、博士論文の執筆中は、その複雑さと膨大さゆえに脳内のリソースを極限まで使い続ける日々であり、これまで以上に研究と実務の両立の難易度が高かったように思う。 それでも、最後までやり切ることができたことに、今はただ安堵している。 一方で、これらの分析や提案については、伝え方や巻き込む人々の選定をもっと考えることで、より事業成果につなげられたのではないかという反省もある。

来年は博士として、今の専門性に加え、さらに高い期待が寄せられる立場となるはずである。 不確実で混沌とした課題領域において、比較・体系化・言語化を通じて、誰もがその課題を共有し、迅速かつ効果的な対策を講じられる基盤を整えることが求められる。 このスキルを活かし、事業に携わる人々が互いの専門性を最大限に発揮し、成果につながる環境を作り上げることを目指していきたい。

振り返れば、多忙の連続であった一年で、正直なところ鮮明な記憶が残っていない。 それでも、この積み重ねや振り返りが今後の糧になると信じ、今後も一歩一歩進んでいきたい。 来年もどうぞよろしくお願いいたします。

実績

以下、実績を列挙する。

論文

博論と国際会議論文1本。本数としては少なくなってしまったが博論と並行して国際会議に投稿・発表し、博論の質を向上させることができたのは自身を褒めてあげたい。 来年はより水準の高い会議へも挑戦していきたい。

国内発表

福岡で開催した技術イベントで1回(博論の公聴会を除く)。 来年は全国規模の技術イベントへの復帰を果たしたいところ。

OSS

AIを開発環境に取り込んでいくためのツール群を整えた。来年はもう一歩先の統合を目指したい。

コミュニティ活動

Fukuoka.goを再開できたのがよかった。いつの間にか10周年だったので来年3月ごろにお祝い的な会も開催を考えている。学会系では、研究会の運営委員を継続している。

ブログ

3本。ここ数年に比べると増えた。OSS紹介、思い出、考えのまとめなど雑多な内容だが、博士課程が終わってとにかく思いついたら書くことができているので来年も継続的に書いていきたい。

その他

研究所で開催した社内向けの勉強会の記事。来年はAI前提を踏まえこれらを事業にどんどん適用していく。

国際会議採択のニュースリリース。 来年はプレスリリース目指して頑張りたい。

みんなと仲良くすること

ここ数ヶ月、思うように成果が出なかったことに対するボスからのフィードバックを中心に自分なりに来年の仕事の指針としてまとめておく。

多くの事業では、環境の変化へ追従するためにプロジェクトの優先順位は目まぐるしく整理・最適化されていく。 また、近年では、データ駆動な意思決定、機械学習やAIのような新しい自動化の導入によって、事業に求められる水準も高度化している。 このような質・量ともに認知負荷の高い状況に対処するため、専門性を高めた分業やチーム体制が出来上がるのは必然である。 しかしながら、例え、各々が目的やビジョンを揃えていたとしても、皆が足並みを揃えて進むことは存外に難しい。 この要因として、分業した専門の観点ごとにゴールまでの過程における課題の内容や具体度が異なることが考えられる。 描く経路が異なれば、当然、足並みが揃うことはない。

よって、ある程度以上の規模や難易度の仕事を成功させるなら、関わる人々の間での関心度を高めないといけない。 しかし、分業による過程のカプセル化と無関心は紙一重である。 せっかく認知負荷を下げることができたのだから、わざわざ相手の事情を汲むような工程を挟まずに、阿吽の呼吸で足並みを揃えて進んでほしいと思いたくもなる。 それでもやはり、関わる範囲が広範囲だったり内容が高度になるに伴い、表面上の理解や想像による補完では立ち行かなくなってしまう。 だからこそ、互いの専門領域の広さと難しさを前提とし、労力を払って関心を維持することが仕事を成功させるために必要になってくる。

もちろん、全体としてのコミュニケーションコストを低減させるため、各々の専門領域に照らした道程について、聞き手に応じた表現で過不足なく効率的に伝えることが求められる。 そのために、伝えたいことを抽象的な概念上の枠組みに変換し、効率的に伝えるために図解をはじめとした表現方法を駆使するスキルが求められる。 発信側の労力も抑えるため、この実現にはAIも積極的に活用し、聞き手に応じたバリエーション生成まで即時に行える仕組みも構築したい。 また、関心は関係する人々がそれぞれ双方向に接続されている状態が望ましいが、返報性の原理を期待して、まずは制御可能な自身から関心を維持することを始めるのが良いだろう。

ペパボで大切にしている三つのことのうち一つに「みんなと仲良くすること」というものがある。 これを表面上の摩擦がない状態と捉えてしまうと無関心に通じてしまうので、仕事の達成を念頭において、伝えるべきことをしっかり伝えるというのが、自身のこれまでの理解であった。 ただ、これも一方向のコミュニケーションに過ぎず、ある種の無関心とも言えるのではないかと考え直したのが今回である。 今後は、仕事を達成するため、もう一段階踏み込んだ「みんなと仲良くすること」を常に意識的に実施していきたい。 そうすることで、無関心による安易な想像に起因する問題を回避したり、問題が生じたとしても、なぜ齟齬が発生しているのかを構造ベースで捉えて建設的に解決に進めることができると思う。


実際はフィードバックは以下のように端的にまとめてくれていたのだが、自分なりの解釈に落とし込むために書いてみました。これぐらいズバッとまとめられるようになりたい。

feedback

ペパボに入って12年が経った。気付けば博士になっていた

先月、博士(情報科学)の学位を取得した。

12年前、福岡 おもしろい it 会社と検索をして、株式会社paperboy&co.(現GMOペパボ株式会社)に運用開発エンジニアとして転職した時1からすると、予想だにしなかった状態である。 転機は2017年1月だった。 ペパボで切磋琢磨できる仲間に恵まれ、OSS活動や登壇を通したアウトプット、社内での成果を自負する一方で、社内でもレベルの高いエンジニアとの仕事の機会を通して、自分の限界も感じ始めていた。 そんな折、研究所の所長から研究職への打診があった。 確か、当時進めていたログ活用基盤の構築とその内容の研究所のコンセプトへの親和性からだったように記憶している。 感じていた限界を「コンピュータサイエンス」ってやつや「研究的アプローチ」ってやつで突破できるかもしれない、研究って何もわからないけれども何とかなるだろう、渡りに船だとばかりにこの誘いに乗ることにした。

結果、丸々二年間、滑稽なぐらい迷走した2。 「研究」をするぞと意気込みは空回りし、研究所の同僚からのアドバイスも虚しく、納得する成果を出せずに時間だけが過ぎ、短時間で効果が出るものを求め悪循環が始まった。 元来の己の精神的な弱さ、四十歳前後という年齢特有の焦り、人生最高潮に達したあらゆるものに対する固執が目を曇らせ、足を引っ張った。 長く暗い時期だったが、ほんとうにほんとうにありがたいことに、さまざまな人に支えられ、何度も内省を進めることでかろうじて持ち直していった。 研究所の同僚、エンジニアと研究者が集うWSA研3、研究者の先輩方、そして家族には感謝の念が尽きることはない。 それでも距離をおくことを選択した縁もあり、たくさん迷惑をかけてしまった。

「研究」は難しい。 これは高度という意味での難しさではなく、ソフトウェアによる課題解決とは目的が違うことに起因する難しさについてである。 博士課程を終えて、研究とは「人類が新たに学べる領域を作り出すこと」であると考えるようになった。 すなわち、ソフトウェアによる課題解決は、目的に連なる目標のうちの一つであり、その解決も含めて、新たに学べる状態に仕上げねばならない。 そして、後進が学べる状態であるには、どこがこれまでの方法と異なるのか、どれぐらいの効果があるのか、それらが主張とその裏付けという形で明確に述べられたかといった要件に落とし込まれる。 これは、論文における「新規性」「有用性」「了解性」の観点に対応し、論文を記述する際には、これらの観点を満たすための様々なサーベイやライティング手法を駆使して臨むことになる。 そのような過程を経ることで初めてそのアウトプットは、未来の誰かの手による学べる領域の更なる拡大を支えていくことができる。 落とし穴は、ソフトウェアによる課題解決を目標ではなく目的に据えてしまうことである。 ここに陥ると、課題の解決こそが研究であり、課題解決のみで研究であるためには、大層な水準の課題を解決せねばならないとなりかねない。 自分が最初に躓いたのはここではないかと思う。

この頃の自身の振り返りを読むと、感情との付き合い方4の他に、研究とは何かを手探りで模索している様子が見て取れる5 6 7。 解決したことを伝えていかないといけないのだな、そのためにはたくさんの根拠を示さないといけないのだな、だから一朝一夕じゃ無理で時間をかけて向き合うことが必要なんだな、程度の理解度ではあるものの、この辺りから前に進み出したのだろうと思う。 2020年6月に初めてジャーナルの採録通知を受け取り8、10月には社会人学生として九州大学大学院システム情報科学府博士後期課程へ社会人学生として進学した9。 進学の後押しも研究所の所長だったと思う。 これまでの経緯もあり、及び腰だった自分に「研究職続けるなら博士号はあった方がいいし、いずれは取るのだから最短で効果が出せることをやったらいいんじゃないですか」と言われ、それもそうだなと挑戦を決めた。 暗中模索の中でも進めていた十本弱の研究報告やジャーナルの実績をもって修士を飛ばしての出願と入学が認められたこともあり、積み重ねは無駄じゃなかったなあ、ようしやるぞとここからは順風満帆。 といかないのがいかにも自分らしくて思い出しても苦笑してしまうのだが。

博士課程の修了には一定数の実績が必要となる。 自分の場合は、在学中に国内ジャーナル論文をもう一本、一定水準以上の国際会議での採択が一回が最低基準であった。 国内ジャーナルこそなんとか通せたものの、国際会議については、2021年5月の初投稿から丸二年、七回目の挑戦にしてようやくの採択となり長い我慢の時期を過ごした10。 そんな中、指導教官は一貫して、研究という最短ルートはない道程において、同じ道を回ることなく、常に何かしらの前進という成果を得るためのアドバイスを与えてくれたと思う。 それは例えば「難しいものと認識した上でそれに挑戦する喜び」であったり「ダメだった場合は改善してただ次に行くだけ」であったり、「自分で決めて悔いのないように進む」こと、そして「結果に対して本質的な面白さを見出して言語化し、これまでの取り組みと有機的に関連づけていくこと」などである。 博士課程という自分の研究の型を体得していく中で、指導教官の存在は本当にありがたいものであった。 指導教官は、分野の専門性が重なっていることが望ましいが、それ以上に相性があると思うので、できれば共著を先に一度執筆するなどコミュニケーションを取れると良いと思う。 幸い、自分の場合は、進学前の共同研究の際に知り合うことができたのでこの点でも恵まれていたと思う。

とはいえ、博士課程は、自身で主体的に研究を推進しなければならない。 これは、博士課程が「人類が新たに学べる領域を作り出すこと」という研究を達成する、「その方法論を、特定の研究テーマに基づく実践を通して身につける課程」だからだと思われる。 新たに学べる領域を作り上げているのであるから、指導教官にとってもわからない部分を切り開いていくのであり、そしてその開拓の方法は、自身でやり方を模索していかなければならない。 主体性については、能動的に動くことが前提の社会人であれば特段問題にならないかと思う。 博士課程での大変さは、この研究を推し進めるやり方の模索に対するフィードバックが、アウトプットに対してのみ得られるという点だったと思う。 それは主に査読結果や、国際会議での発表に対する議論の形を取るが、いずれにせよ論文の形にまとめる必要がある。 これは研究を学ぶには研究をしなければならないという構造であり、研究テーマ自体も発展させなければならないことも相まって、随分鍛えられたと感じている。 幸い、指導教官のアドバイスはこれに即したものであり、博士論文に着手する前の三年間で研究報告や不採択のものも含めると十六本、おおよそ二、三ヶ月に一本は何かしら研究成果を出して論文執筆していたことになる。 成長が遅いのは仕方ないとはいえ、研究開発員として、事業への貢献も求められる中でも諦めずに課程を継続したことは自分を褒めてあげたい。

2024年4月には、追加でもう一つ国際会議に採択され、最終的に、入学前のジャーナル論文と合わせて計四本の実績をもとに、学位申請のための博士論文を執筆した11。 博論の執筆では、これまでの実績を「人類が新たに学べる領域」として再構成する。 四本の論文の整合性を取りながら、領域の世界観の確立も必要で、非常に難易度の高い執筆であり、実績達成以上にまだ大変な工程が残っているのかと驚いた。 しかしながら、この工程を通して初めて、ボトムアップな進め方であった自身の研究テーマに対し、トップダウンからの位置付けを与えることができたように思える。 実際に、自身の研究観である「人類が新たに学べる領域を作り出すこと」というのもこの段階を経て感じるようになった。 これまで各論文においても「新規性」「有用性」「了解性」の観点を満たせるよう突き詰めてきたが、複数論文をまとめる中で、それぞれの点が関連し、補い合う形で(まだ非常に狭いながら)初めて領域として形をなしていったように思う。 なお、世界観の確立については、ペパボ研究所のコンセプトである「なめらかなシステム」についての、所長との数年間に渡るディスカッションが非常に役立った。 このようなハイコンセプトな思考だけでなく実践的にも抽象的にも自在に観点を行き来しながら多面的に研究を見直す機会があるのがペパボ研究所の長所だと思う。 博士課程を考えている方はぜひ検討してほしい。 その後、公聴会を経て、2024年9月25日付けで博士(情報科学)の学位を授与された。 在学は四年間。 標準年度の三年には間に合わなかったが、なんとか修了に漕ぎ着けることができて安堵している。

さて、ここまでして博士課程を終了する必要はあったのか。そしてこれからも研究をする必要はあるのか。 サンクコストを差し引いたとしても、自分自身としては「はい」と答えたい。 「課題解決」と「研究(新たに学べる領域を作り出すこと)」は相補的な関係であり、それぞれの存在がお互いを必要としていると思う。 また、特にソフトウェアやエンジニアリングの分野においては、それぞれは明確に分離している訳でもないと思う。 研究的なアプローチによって、同じ課題に対しても体系化・一般化・言語化・比較整理などを通して、多面的に捉え直し、新しい観点からちょっと面白い発想が出るかもしれない。 研究による成果物によって、ある課題に対してある状況において有用な方式を広く適用できるかもしれない。 その方式がうまくいかない場合も、理論的な裏付けもしくは再現可能な部分評価を元に、そこから学んで最短で適用できる改善を思いつけるかもしれない。 学べる状態にするのは手間がかかる。 それでもどこかの誰かの巨人の肩になるのだと信じて、研究というのを仕事にする人がいても良いのではないかと思う。

もちろん、企業研究所に所属する研究員としては、より実践的に、博士課程で得たスキルを早速活用していきたい。 改めて、博士号を取得したということは、不確実で混沌とした課題領域においても、比較・体系化・言語化を通して、誰もがその課題を共有し、時間をかけることなく効果のある対策を講じられる基盤を整えられるスキルを持つ、すなわち、学べる領域を作り出せる人材であることが期待されるようになったのだと思う。 今後は、自身の研究テーマの推進はもちろんのこと、さまざまな施策において、所属する研究所のミッションである「研究開発により『事業を差別化できる技術』を生み出す」ことができるよう、さらに精進していきたい。 そして、これまで辛抱強く支えてくれたペパボに恩返ししていきたい。

ペパボに入社して十二年。 タイトルの「気付けば」も大袈裟ではなくて、特に研究職になってからは時間が飛ぶようにすぎていった。 それでも、いろんなことがあったけれども、誰かと比較してではなく、過去最高の自分に仕上がっているというのは四十三歳にしては悪くないのではないか。 これからも「虚仮の一念、岩をも通す」の精神で頑張っていきたい。

お知らせ

僕のキャリアを支援してくれたことからも分かるように、GMOペパボ株式会社はどうすればインターネットを面白くできるのか、これを真面目に考えてさまざまななやり方で取り組んでいます。 研究開発的なアプローチに対してでも良いですし、事業に対してでも大丈夫です。 こんなペパボに興味持たれた方、もっと雰囲気知りたい方、お気軽にお声がけください。

ペパボ研究所

参考

ターミナルフレンドリーなAIコマンド、afaを作った

Go言語で、AIモデルに対する推論をコマンドラインで実行するafaというツールを作りました。入出力としてテキストストリームを前提としており、パイプやリダイレクトを用いて他のコマンドと連携しやすいのが特徴です。

$ echo $ERROR_MESSAGE | afa new -p "What is happening?" /path/to/file1 /path/to/file2

与えるプロンプトやコンテキスト情報によってAIモデルが柔軟に振る舞いを変えてくれるので、これまでの個別ツールでは対応が難しかった用途における自動化にも有用でしょう。 また、テキストストリームを扱えるリッチなTUIのコマンドafa-tuiも別途提供しているので、ターミナル上でのチャットも快適です。

デモ

リッチTUIによるチャット

快適なインタラクティブチャットのため、Markdownを装飾して描画しつつ、lessコマンド相当の操作感のページャーと、プロンプトの入力欄を持つViewerと連携できます。

Chat

Zsh Line Editor(ZLE)を用いたターミナル上でのコマンド提案

ZLEと連携すれば、コマンドライン上でのプロンプトをそのまま提案されたコマンドに置き換えて実行できます。

Command Suggestions

Vim上でのコード提案

Vimと連携すれば、選択範囲のコードを提案されたものに置換できます。使い慣れたエディタであればdiff表示や差分の部分反映もお手のものですね。

Code Suggestions

機能一覧

  • ターミナルフレンドリーなAIコマンドとして機能します。
  • リッチなターミナルユーザーインターフェース(TUI)を持つチャットクライアントとして機能します。
  • システムとユーザーのための文脈に応じたプロンプトをテンプレートを使用してサポートします。
  • プロンプト、標準入力、およびファイルパスを文脈として受け付けます。
  • セッションを管理し、resume サブコマンドを介して迅速に再開できるようにします。
  • 安全にエスケープされたJSONオプションで構造化された出力をサポートし、他のコマンドとの統合を容易にします。
  • コアアプリケーションはサードパーティライブラリに依存せずに独立して動作します。
  • AIモデルとしてOpenAIをサポートします(他のAIモデルのサポートは将来計画されています)。

なお、この文章は、英語で書かれたafaのREADMEのFeatures項目をコピーして、pbpaste | afa new -script -p "翻訳して" | pbcopyで貼り付けました。便利ですね。

基本的な使い方

Viewerを使用しないシンプルなチャットを起動する。

$ afa new

Viewerを使用してチャットを起動する。

$ afa new -V

コンテキスト情報を加えてチャットを起動する。コンテキスト情報には「標準入力」「プロンプトメッセージ(-pオプションによるもの)」、そして「ファイルパス」を与えることができる。 ただし、仕様上、標準入力を与えるとキーボードからの入力を継続できないため、インタラクティブなチャットはできない。この場合は、プロセス置換(<())をファイルパスとして与えることを検討されたい。

$ echo $ERROR_MESSAGE | afa new -p "What is happening?" /path/to/file1 /path/to/file2
# Please be cautious; when standard input is provided, interactive mode is disabled.
# Consider using process substitution.
#=> afa new -p "What is happening?" /path/to/file1 /path/to/file2 <( echo $ERROR_MESSAGE )

直近のセッションを再開する。

$ afa resume

指定したセッションを再開する。なお、afa listコマンドでセッション一覧を表示できる。

# The command `afa list` displays past sessions.
$ afa source -l SESSION_NAME

ユーザープロンプトを指定する。ユーザープロンプトは、Go言語のテンプレート形式であり、テンプレート内ではプリセットの値としてコンテキスト情報に対応する、MessageMessageStdin、そして NameContentをメンバとするFile構造体のリストであるFilesを以下のように呼べる。これにより動的なプロンプトの生成が可能となる。

# `Message`, `MessageStdin`, and `Files` that include `File` with `Name` and `Content` members can be used in the template file.
$ echo "Please explain the following.\n{{ (index .Files 0).Content }}" > CONFIG_PATH/templates/user/explain.tmpl
afa -u explain /path/to/file

スクリプトモードを指定して起動する。コマンドライン実行に適したモードのオプション設定を一括で行う-scriptオプションを指定することで、他のコマンドとの連携が容易になる。具体的には-I=false -H=false -S=false -V=false -L=falseが設定される。

pbpaste | afa new -script -p "Transrate this" | pbcopy

OpenAIの独自機能として提供されるStructured Output用のスキーマを指定できる。これは指定したJSON構造体に沿うような出力を強制する機能である。例えば、余分な説明が不要なコマンド提案などに適している。 クオート文字のエスケープを担う-Qオプションと合わせて使うことでjqによる整形が容易になるので活用されたい。

$ cat <<< EOS > CONFIG_PATH/schemas/command_suggestion.json
{
  "type": "object",
  "properties": {
    "suggested_command": {
      "type": "string"
    }
  },
  "additionalProperties": false,
  "required": [
    "suggested_command"
  ]
}
EOS

$ P="List Go files from the directory named 'internal' and print the first line of each file."
$ afa new -script -Q -j command_suggestion -p $P | jq '. | fromjson' | jq -r '.suggested_command'
#=> find internal -name '*.go' -exec head -n 1 {} \;

実践例

実践例を列挙します。 もし使いたい例があれば、GitHubのリポジトリのREADMEにプロンプトや設定例と共に載せているので参考にしてみてください。

  • Zsh Line Editor(ZLE)を用いたターミナル上でのコマンド提案
    • デモで紹介したものです
  • Vim上でのコード提案
    • これもデモで紹介したものです
  • ターミナルに表示されたエラーメッセージの分析
    • tmuxcapture-paneを使って直近の実行結果を分析できます。複数行にわたるエラーメッセージのコピペの手間が省けて便利ですね
  • GitHubのプルリクエストの自動生成
    • git diffgit logの情報を元にタイトルと要約を生成してプルリクエストを開きます。ZLEを想定しているので背景情報も踏まえて考えてもらえるようになっています。また、既存のテンプレートの上に要約を差し込む形にしているので、どのリポジトリでも使いやすいかなと思います。
  • pecoを用いたセッションの絞り込みと読み込み
    • インタラクティブな絞り込みで素早く過去のセッションにアクセスできます。

AFA

AFAはAI for Allの略で、あらゆるコマンドラインツールに対して連携可能($\forall x \in X , \exists \mathrm{AI}(x)$、つまりコマンドラインツールの集合Xの全ての要素xに対し、その結果を入力とするAI(x)な組み合わせが存在する)なAIコマンドになるといいなと思って付けた名前です。

現時点でもまだViewerの入力欄が不調になることがあったりWindowsでの動作確認ができていないなど課題はありますが、通常使う分には十分な品質になったと思うので公開しました。よく使うオプションセットをconfigに設定し、いくつかのユーザープロンプトを登録しておいてalias af='afa new'とするだけで、個人的には、調べ物やチャットを含め、ブラウザの利用頻度がかなり減ったことを実感しています。

Homebrewによるインストールbrew install monochromegane/tap/afa monochromegane/tap/afa-tuiも提供していますので、よければ使ってみてください。

将来的には、サーバー上での異常検知のような定式化の難易度が高いタスクでの適用も検討しています(サードパーティー製ライブラリへの非依存とViewerツールとの分離は、そのためでもあります)。AFAであれば、他のコマンドラインツールとの親和性を活かして、通知やロギングは従来の仕組みも活用できると考えています。適用例など共有してもらえたら嬉しいです。

Archives