Flavor Wheel Engineering

珈琲とソフトウェアエンジニアリング

Neovimでcodecompanion.nvim経由でAIエージェントを使う設定とふりかえり

このエントリを書いた頃から、codecompanion.nvim を使ってAIと対話してプログラムを書くことを意識して行なってます。 使い始めてしばらく経ち、見えてきたことや感じたことがあるので、現状の設定と所感をまとめておきます。

現状の設定

現在の設定は以下に置いてあります。

github.com

今のところ、手軽に利用できる GitHub Copilot のみを使っています。
ドキュメントによると、さまざまなモデルを指定できるようですが、僕は今のところデフォルトのままで運用しています。1

タスクの特性に応じて他のモデルも試してみたいのですが、まだそこまで手が回っていません。

チャット履歴の保存・管理

コミュニティ発のチャット履歴管理用の拡張プラグインがリリースされたので、現在はそれを利用しています。
チャットの自動保存やタイトルの自動命名に対応していて便利です。

僕は基本的にエディタを終了するタイミングでタスクも完了しているため、チャット履歴の自動復元機能はオフにしています。

slash command とファイル共有

チャットバッファでAIにコンテキストを渡すには、変数とslash commandの2つの方法があります。 変数を利用する場合、は#buffer のTODOを解消して のようにAIに指示することで、バッファの内容を共有しつつタスクを依頼できます。#buffer は直前にカーソルがあったバッファの内容を表す変数です。 #buffer の他には、#lsp#viewport が定義されてます。詳細は、変数のヘルプ を参照してください。

slash command はより自由で、例えば /file slash commandがあります。 /file を実行すると、ファイル選択pickerが起動されファイルを選択することでAIのコンテキストに追加できます。

この他にも バッファを共有する /buffer やWeb上のコンテンツを共有する/fetch 、ターミナルバッファの内容をAIに読ませる/terminalがあります。 僕は、/file/fetch を頻繁に利用してます。

AIへの前提情報共有

AIに良い応答をさせるには、.cursorrules.github/copilot-instructions.md に纏めた前提情報を渡すことが求められます。 codecompaion.md にも workspace 機能と言って、前提情報を纏めて渡す仕組みはありますが、専用の形式なので利用者が自分のみの状態だとメンテコストが掛かるので利用しにくいです。 そのため、僕は任意のファイルを選んでコンテキストに追加できる、slash command の /file を使って手動で選択して共有してます。

ただし、デフォルトではファイルの候補に . で始まるファイル(つまり、.github/copilot-instructions.md.cursorrulesなど)が表示されません。
そのため、ファイル選択用のpicker に snacks.nvim を指定し、さらに隠しファイルを表示する設定を加えています。 picker を指定する際にオプションを個別に渡す手段はなさそうだったため、グローバルな設定で対応しています。

チャットバッファを開いた時に自動で共有しても良いですが、やらせたいタスクにとって不要な場合もあるので都度選んで共有してます。

コードレビュー用のカスタムプロンプト

PRのレビューの時にAIの力を借りられると便利そうだったので、差分を共有してレビューを依頼するカスタムプロンプトを定義しています。

github.com

ただ、GitHubから直接レビューしてしまう時が多いので利用場面は少ないです。 Copilot review で済んでしまう場合が多いことも理由の一つです。

エージェントモード

codecompaion.nvim でエージェントモードを使う時は、@で始まるツールを使います。2 例えば、@file 1~10まで足し算する関数を実装して のようにチャットバッファで指示するとAIが自分でファイルを選んで作成、編集などをして実装します。 基本的には差分を表示してくるので、その差分に対して 適用してと追加で指示を出すと実際にファイルを更新してくれます。

指示の与え方が悪かっただけだと思いますが、既存のファイルを新規作成しようとして失敗することがありました。その時は 既存のファイルを編集して と追加で指示して軌道修正させたり、もう一回やって のような曖昧な指示でも繰り返すと原因を理解したのか、AIが編集に切り替えて成功しました。

@web_search 〜〜について調べて のようにするとWeb上のコンテンツも検索可能なようですが、まだ利用経験はありません。


今後はモデルの切り替えやプロンプト設計など、もう少し使いこなしてみたいと思っています。

データ修正スクリプトに求められることの検討

データ修正は、できるだけ避けたい作業です。ですが、ユーザーによる想定外の操作や、意図しないエラーなどが原因でデータに不整合が発生してしまった場合には、どうしても対応が必要になることがあります。

そういったときには、mysql コマンドなどで直接データベースに接続して手作業でSQLを実行したり、一度だけ実行するスクリプトを作成して対応することになります。

特に後者のような「データ修正スクリプト」には、いくつかのメリットがあります。

  • 本番アプリケーションで使われているロジックをそのまま利用できる
  • 自動テストによって検証しやすい
  • 人の手では難しい、大量データの修正が可能

こういったスクリプトを実装するときに気をつけた方が良いことを考えて、整理しました。

処理中断に強いこと

まず、一番大事なのは 「ネットワークの障害などで処理が途中で止まってしまっても、データが壊れないこと」 だと思います。
不整合を解消するためのスクリプトで、かえってデータを壊してしまっては意味がありません。

また、万が一途中でセッションが切れてしまったような場合でも、もう一度安全に実行できるように、再実行可能であること(=冪等性) も大切です。

進捗状態の確認

そしてもうひとつ、進捗がわかること も大事なポイントです。
スクリプトが実行されてはいるけれど、本当に動いているのかよくわからない――そんな状況は不安になりますし、動いていなかった場合は可能な限り早めの対応が求められます。 たとえ開発環境でちゃんと動作確認をしていたとしても、本番環境では思いもよらないことが起きるかもしれません。 そのため、1000件中10件完了 のような進捗状態を表示することが必要でしょう。

実行前の確認機能

そのほかにも、スクリプトを実行する前に「どんな変更が行われるのか」「どこに影響があるのか」といった差分を表示し、実行してもよいかどうかを確認できる機能があると安心です。

Gitのように差分が出力されると具体的に何が変わるのかを把握しやすいですが、変更前と変更後の状態を並べて出力するだけでも良いと思います。

変更内容の保存

さらに、スクリプトによる変更内容をログとして残しておくこと も求められると思います。 「どのデータが、どう変わったのか」をあとから確認できるようにするためのものです。

また、進捗を確認するためのログと異なり、こちらはファイルに追記できるようにした方が望ましいです。 標準出力ではターミナルに表示されるだけでウィンドウを閉じたり、ログが長いと遡れなくなってしまうためです。 スクリプトとしては標準出力に書き出して、実行時に tee 等を使って標準出力とファイルの両方に書き込むような運用でも十分だと思います。

dry-run 機能

必須ではないと思いますが、可能な限り「dry-run」機能があると嬉しい です。
これがあると、実際にはデータを変更せずに、処理が現実的な時間内に終わるかどうかを確認したり、影響範囲を把握したりできます。 dry-run でデータを変更せずに実行して、うまくいったら本番実行のような運用が可能になります。

DBであれば、データ修正処理をトランザクションで囲んで実行し、最後にコミットせずロールバックするような実装になります。 外部サービスと連携箇所があったり、トランザクションのような整合性を保つ仕組みがないデータストアの場合には実装が難しいか不可能な場合もあるでしょう。

まとめ

こうして整理してみると上記で書いた機能は、基本的に terraform にも見られて、重要な処理を行うスクリプトで一般的な機能と言えそうです。 dry-run やログ、差分の出力を実装はゼロから考えると時間がかかりそうなので、標準的な実装手段を手札として普段から揃えておきたいですね。

認定スクラムマスター研修を受けて認定された

これまで「アジャイルレトロスペクティブズ」や「アジャイルサムライ」を読んで、チームの暮らしと対応を考えたり、社内の開発プロセス全般について知見共有ができる場の「すくすく会」に参加して日々の暮らしで生まれた疑問を話してきた。その「すくすく会」に参加しているうちに認定スクラムマスター研修を受けられるチャンスを頂けたので受講して、テストに合格し認定された。

受講した認定スクラムマスターのコースは、Joe Justice先生のクラス。

abi-agile.com

トップページから既に陽気さが溢れている。9:30~17:30までの講義を2日間行う形式で、内容も詰まっているが楽しく学べるように細かく気を配られており、面白くスクラムを学べた。 また、講義中に都度質問をして回答をもらえたのも良かった。

講義は、参加者で4人のチームでプロダクトオーダー、スクラムマスター、開発者2人のロールをローテーションしてスクラムを体験しつつスクラムの要素を学んでいくというものだった。 2日目は、それまでに学んだスクラムの流れに乗っ取って、4人チームでちょっとした迷路やすごろく開発のようなプロダクトを作ってみることをした。 7分ごとにプランニング、デイリースクラム、実装、.... とイベントが進むので時間はないのだけれど、案外最後にはそれっぽいものができた。僕たちのチームは冷蔵庫の中身を管理するツールを作った。

このコースのユニークなところは、テスラ、スペースXでの事例も先進的な事例として教えてもらえるところだろうか。 また、最近の流行に沿ってAIの支援を受けたグループワーク手法であるMobAIや NoEstimate についても学べる。

MobAIについては、Joe Justice先生の投稿があった。

ひとまず認定スクラムマスターになって、スクラムについてある程度ちゃんと学んだことのある人になれたと言うだけでも研修に行った甲斐はあったと思う。

とはいえ、資格を認定されるだけだと学んだことを保証されたと言うことでしかない。還元するための具体的な取り組みは、まだ曖昧にしか考えられていない。講義中は理解するのに必死だったので講義を振り返りつつチームに還元するには何ができるのかを考えていきたい。

NeovimでAIによるコーディング支援プラグインを試している記録

ClineやCursorなどAIによるコーディング支援が話題になっていて、その種類は大きく補完と対話の2つに今のところは分けられていそう。

  • 補完
    • Language ServerのようにAIが動作してコード補完を返す
  • 対話
    • コードに手を加えないサポート(説明、レビューなど)
    • コード変更を提案するサポート(リファクタリング、バグ修正提案)
    • エージェント機能:AIが自発的にコマンド実行やリソース探索を行い、コードに直接変更を加える。

GitHubが公式に提供しているNeovimプラグインのcopilot.vimは、補完だけをサポートしている。

copilot.vimしかないかといえば、そうではなく最近は対話によるコーディングサポートを提供するNeovimプラグインも存在する。 調べた限りでは、次のプラグインがチャットバッファを提供しており対話機能をサポートしている。

ドキュメントを眺めたところ基本的に対応しているモデルには違いや大きな見た目には違いはない。

最近は、評判が良さそうだったcodecompanion.nvim を試している。 ClineやCursorも含めてAIエージェント自体を使い込んでいないので、手探りで良い指示の与え方を探っているが、まだ道具として手に馴染められた感覚はない。

使い始めてみてわかったのは、いきなり --debugオプションを受け取れるようにしてほしいみたいな指示を与えても、どのファイルを変更したら良いですかのように質問されて一気にうまく変更してくれないということ。 AIにコンテキストを教えるための copilot-instruction.md みたいなファイルを準備していないからというのもあるが、ざっくりとした指示だけでは狙った変更をしてくれないらしい。

エージェントと名乗るくらいなら、その辺りも含めて自分で調べて解決してほしいと思ったが、今はまだ難しいということだろう。もしくは、他のモデルやツールを使えばAIが自分でどのファイルを読めば良いかも含めて判断するのかもしれない。

また、プロジェクトの新規作成時点から同じチャットで対話していると、雑な指示であっても比較的良い反応をしてくれるが、チャットバッファを新しく開き直すとコンテキストが分断されるのか応答が悪くなることも試してみて初めてわかった。

「アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き」を読んだ

shop.ohmsha.co.jp

アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き(2007年、オーム社)は、「ふりかえり」について知りたいと思っていた時期に調べたり人に聞いたりして知って読んだ。

「良いふりかえり」をするために必要な準備や考え方、進め方の具体的な方法論が書かれている。 第1~3章と第9、10章は今後も読み返すと思う。

本書に書かれている「ふりかえり(レトロスペクティブ)」の会話例を読むと「具体的な改善アクションがでてて、ふりかえりめっちゃ良いじゃん!!!絶対やるべき!」と思うが現実は厳しい。実際に2週間のスプリントごとにふりかえりをしたときには具体的な改善アクションを毎回見つけられるわけではない。 どうしても意識するみたいな曖昧なアクションになってしまいがち。

ふりかえりの時間内で話題から真の課題を分析するのも難しい。普段からチームの課題を繰り返し考えていれば、あの課題に関係してるんじゃ?みたいになって話題を広げていけるのだろうか。

「ふりかえり」内で行うKPTなどのアクティビティも多数紹介されているが物理的な空間に集まって行うのが前提だった。そのため、アクティビティの理念みたいなものは参考になるが詳細な手順はリモート環境に適したものになるように考える必要がある。 メンバー全員がふりかえりに対する期待を口にするみたいなのはリモートワークでも変わらず有効そうだし、むしろ現代のほうが大事かもしれない。

本書では、アクティビティを目的ごと計4つ(場を設定するアクティビティ、データを収集するアクティビティ、アイデアを出すアクティビティ、 何をすべきかを決定するアクティビティ、レトロスペクティブを終了するアクティビティ)に分類している。 定期開催のミーティングの一部になっていると、個々のアクティビティの目的は意識から抜け落ちるので、自分たちが行っているKPTや喜・怒・哀のようなアクティビティの目的を定期的にふりかえって適しているのかを考えるのも良いんじゃないか。

Goのインターフェイスはimplementsとsatisfiesを区別する

Goの静的解析で型関係の処理を提供するgo/types パッケージには、*Interface 型を引数にとるよく似たシグネチャを持つメソッドが2つある。

https://pkg.go.dev/go/types#Satisfieshttps://pkg.go.dev/go/types#Implementsのことだ。

func Implements(V Type, T *Interface) bool は、Goにジェネリクスが導入される前から存在していたメソッドでインターフェイスを実装(Implements)しているかどうかを判定する。 一方でシグネチャ的にはインターフェイスで同じだが func Satisfies(V Type, T *Interface) boolは、型VがインターフェイスTを型制約として満たす(satisfies)かどうかを判定する。

satisfiesは、implementsよりも若干広い。 型制約に関する仕様 https://go.dev/ref/spec#:~:text=Satisfying%20a%20type%20constraintによれば型Tが型制約としてのインターフェイスIを満たすのは次の場合だ。

  1. TがIを実装している場合
  2. Iがinterface{ comparable; E }E は basic interface、comparable は事前宣言された型制約のこと)の形で書けて、Tが言語仕様として== で比較可能、かつTがEを実装している場合。basic interface とは、float64 | float32 のような unions を含まないインターフェイスのこととする。

型制約はインターフェイスで表現されるため、型制約CをインターフェイスIとして言い換えた。

つまり、大雑把に言えば「“Iからcomparableを除いた部分集合”を関数、マップ、スライスではないTが実装していれば、TはIをImpementsしていないがsatisfiesはしている」ということ。 なぜimplementsとsatisfiesが異なる概念になったかの解説は、Go言語のBasic Interfaceはcomparableを満たすようになる(でも実装するようにはならない)に詳しい解説があるので気になるなら読んでみるとよい。

ジェネリクスが導入される以前は、Implements と satisfies は同等で「実装する」と「満たす」を使い分けせずに用いられてきたので未だに混乱する。 自分の書く文章の中では意識して使い分けていきたい。

サメ映画(シャークネード)に学ぶチーム開発

サメ映画は、基本的に平和な海やビーチなどの風景が映され、サメの影や小動物などが襲われるシーンに繋がり物語が始まる。そして、登場人物が混乱し、錯乱し、勇気を持ってサメに立ち向かい倒すことで完結する。 ジョーズに代表される不動の人気を誇るジャンルだ。 実は、サメ映画にはチーム開発のエッセンスが込められている。

それをシャークネードの一作目を例に出して説明する。 初めに学べることは、情報提示の流れである。 シャークネードの冒頭部分を纏めると、映画開始から数分間の間に主要人物のほとんどが登場し、続いてビーチ全体や街が映される。その後、登場人物のプロフィールが語られ本編へと続く。

チーム開発で扱う課題は多様で複雑である。それはサメ映画も同じだ。スノーシャークは雪山を泳ぐし、ウィジャシャークは霊界に潜む。BAD CGI SHARKSではデジタル世界から襲ってきた。ここではサメの多様性だけにとどめるが舞台も千差万別である。そのため、物語におけるサメの特徴や舞台を鑑賞者に説明しなければ、どのジャンルのサメ映画かわからず混乱してしまう。また、サメ映画の画面には大量の人物が現れるが、人の記憶力は有限なのでその全てに注目することはできない。

ここでシャークネードの冒頭シーンをふりかえろう。画面いっぱいに写される人物は数人に限られていて、誰を覚えていれば良いのか、人物間の関係性も明確だ。そして、背景のビーチや街を写すことで全体的な状況と、その中のどこに今着目しているかを鑑賞者に説明する。 まとめると関係する主要な要素とそれが全体像の中でどこに位置しているのかを説明で物語へ導入している。この注目したい部分以外を切り捨てるやり方は、ソフトウェア開発に限らず全てで複雑な物事を説明する際のやり方としてお手本となるだろう。

シャークネード本編でもチーム開発のエッセンスが繰り返し登場する。

例えば、サメの群れに囲まれバスに閉じ込められた子供達を救助するシーンだ。この場面で主人公達は車に積んでいたロープ使って橋の上からロープを使って降り子供達を救助する。 要救助者である子供達も慌てず、また主人公達も落ち着いて救助活動をしている。 この場面からは、緊急時にこそ落ち着いて行動することで被害を最小限に収められることを伝えてくれている。 また、子供達が乗るバスを主人公達が発見した当初はバスを無視して逃げることをメンバーが提案しているが、救助を決めた段階では協力しあっている。Disagree and Commitのエッセンスも読み取れるシーンである。

さて、ここで主人公達が救助できたのは協力したからだけが理由ではない。ロープレスキューが可能な道具の準備やメンテナンスや訓練をしていたからだ。 つまり、彼らは普段からロープレスキューができるように車内に装備を準備し、訓練していたということだ。ここから不測の自体にも対処できるように平常時から準備を重ねておくことの重要性も僕達に伝えているわけだ。

この準備と落ち着いて行動することの重要性は、後のバンナイズ空港でサメの大群と対峙するシーンで、空港倉庫に爆弾やチェーンソーが保管されていたことからも伺える。 バンナイズ空港におけるサメとの戦いでは、チームで意思を統一することで巨大な敵(サメ)にも立ち向かえることが学べる。

物語終盤では、トルネードによってサメが巻き上げられてできたシャークネードを喰い止めるために、ストームライダーさながらヘリコプターから爆弾を投げこむ。直感的には十分な装備がない状態でトルネードの間近を飛び爆弾を投げこむ作戦は無謀でしかありませんが、劇中では意外にも成功する。 このシーンから時に勇気を持って危険を犯すことで通常では得られないような大きな成功を得られるかもしれないことを学べる。

だが、シャークネードはこれだけでは終わらない。トルネードを喰い止めることに成功し、戻ってきた父親のもとに娘がかけつけてしまう。トルネードの残滓によって飛ばされてきたサメが娘に向かってしまう。劇中では娘が食われてしまう直前に父親が娘を突き飛ばすことで間一髪逃がれる。 大きな課題を解決した直後は油断が生まれてしまうものであるが、完全に解決するまでは一瞬の油断が命取りになってまうということだ。

シャークネードは最後に、普通では解決できないような巨大な問題解決に多少の勇気と完全に解決するまでは最後まで気を抜いてはいけないということを教えてくれた。