Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

GitLab本輪読会、他社と合同で振り返りを行いました

こんにちは。プロダクト本部でエンジニアをしています daichi ( id:da1chi24 ) です。

先日、社内でGitLab本の輪読会を実施しました。

さらに今回はそれだけでなく、同時期に同じ本の輪読会をした他社の方と合同で振り返りを行うイベントに参加しました。

今回は輪読会や、他社と合同で行った輪読会の振り返りの内容、経緯や学び、参加した感想をシェアします。

GitLab本とは

GitLabに学ぶ世界最先端のリモート組織のつくりかた」(以下 GitLab本)という本です。

www.shoeisha.co.jp

この本はGitLab社が公開している「The GitLab Handbook」というドキュメントを参考に、著者が自社をリモートワーク化した実体験を通して得られたノウハウをまとめた内容が書かれています。

大まかな章立ては以下のようになっています。

  • リモート組織のメリットを読み解く
  • 世界最先端のリモート組織へ移行するためのプロセス
  • GitLabが実践するリモート組織を活性化させるカルチャー醸成法
  • GitLabが成果を出すために実践している人事制度や業務ルール

輪読会を開催しようと思ったきっかけ

Classiはリモートワークがメインなので、リモートワークを活用して生産性を高める実践例に興味がありました。

しかし、本の内容を把握するのは簡単ですが、その背景やコンテキストを理解するのは難しいです。

そこで他の人の意見を聞きながらディスカッションすることで、本の内容の理解がより深まるのではないかと思い、実施しました。

また、本で紹介されている事例は、個人やチームのみならず、文化や方針などの会社全体に関わるものも多いです。

そのため、普段一緒に仕事をすることのない他の部署や別のチームの方々と課題意識を共有することで、新しい気づきや発見があることも期待していました。

輪読会の進め方

輪読会はソフトウェアエンジニアだけでなく、データエンジニアや労務の方などさまざまな職種の方に参加していただき実施しました。

メンバーの層もジュニアから部長クラスの方まで幅広くいました。

1章ずつ担当を割り振り、担当者はその章を事前にまとめて発表、その後参加者同士で気になったことをディスカッションしました。

この本は全部で13章あり、単純計算で3ヶ月以上かかる長期戦です。

そのため途中で息切れしないよう、参加者の都合が合わない場合はスキップをするなど柔軟に開催しました。

輪読会を通して学んだこと・合同振り返り会に参加する動機

GitLab社が強く打ち出しているドキュメント文化や、リモート化におけるコミュニケーションの方法、新入社員のオンボーディングなど、明日からすぐ使える知識や基準が数多くあり満足度の高いものになりました。

以下は、参加者の感想です(一部抜粋)

  • 中途入社の人から違う会社の話、他の部署の人から違う組織の話、若手メンバーから違う年次の話が広く聞けたのがめちゃくちゃ良かった。
  • 具体例が多いので読み進めやすく、それでいてコミュニケーションといった普遍的な所作に通ずるところも多い。
  • フィードバックの様式やコミュニケーションガイドラインが具体的ですぐに取り入れやすくて良い。
  • 「前向きな意図を想定する」から始まるコミュニケーションガイドラインが、同意できるところが多くて個人的に最も気に入った部分。
  • 読んで終わりと違って、感想を書き合う・話し合うことで内容がより自分の中に落とし込めた感じがします。他の本でもやってみたい。

また、この本で推奨されている方法の中で、Classiでも既にできていたり文化として根付いているものは改めて良いものだと認識することもできました。

例えば、この本では「リモートワークだとメンバーが何をしているかわからない、マネジメントやメンバーの成果に対する評価がしにくい」という課題が取り上げられています。

Classi ではたびたび引用されている Working Out Load (大声作業をしなさい)という行動を推奨しているという話がありました。

www.workingoutloud.com

一方で、生産性を高め仕事を円滑に進める工夫を、他社ではどのようにやっているのかは、社内の輪読会だけではわかりませんでした。

また、これからリモートワーク化を進める組織やハイブリットな組織にとってどのような障壁があるのかは、既にリモート化された組織にいる身として実感が湧かない部分もありました。

そんな時ちょうどGitLab本の輪読会を同時期に行っている方々と合同で輪読会を行うイベント(後述)があることを知りました。

このイベントに参加することで、さまざま他社の悩みや事例を共有しさらに学びを深められるのではないかと思い、参加することにしました。

他社との合同振り返り

参加した合同振り返り会とは、KINTO テクノロジーズ株式会社様が主催されていたイベントです。

各社それぞれで行った輪読会について振り返りを行い、どのような気づきや学びがあったかを共有するものです。

connpass.com

私はワークやパネルディスカッションに参加し、自社ではどのように輪読会を行ったか、輪読会を通してどういう変化があったかなどを、お話しさせていただきました。

実際のパネルセッションの様子

実際に話したディスカッションの内容

参加者の方々はさまざまな勤務形態で、フルリモートで遠方から参加していた方もいれば、出社がメインの企業の方もいらっしゃいました。

また、職種はマネージャーやITコンサルタント、SREなど幅広く、それぞれのコンテキストで議論が盛り上がりました。

振り返りでの話題

色々な話で盛り上がったのですが、輪読会の実施方法と本の内容から1つずつ共有します。

輪読会の方法はたくさんある

あらためて輪読会の実施方法はたくさんあることを再認識しました。

KINTOテクノロジーズさんの事例では、生成 AI を利用して要約する、欠席した方のために同じ内容を2回行うなど、いかに参加者に負荷なく参加してもらうかをかなり工夫されていて実施していました。

blog.kinto-technologies.com

他にも本を読むタイミング(事前に読むか、当日読むか)、予定が合わない時のスケジュールの調整方法も会社ごとに違いがありました。

実施方法は本の内容や参加者の属性によっても異なるので、色々な選択肢を持ちつつも状況に合わせて良い方法を選んだり、途中で変えたりするのが良さそうだと思います。

リモート化でのインフォーマルなコミュニケーションの工夫

GitLab本ではインフォーマルコミュニケーション(いわゆる雑談)は生産性を高めるために大事だと書かれていますが、それぞれの企業で継続するのはなかなか難しいという話で盛り上がりました。

その中でも工夫している企業では、共通の話題を日報や週報から見つけ会話の接点を増やす、制度化せずに自然発生するコミュニケーションを大切にする、オンサイトで話すタイミングやきっかけを作る、などを実施していました。

後に気づいたことですが、輪読会自体が同じテーマで話しやすく親睦も深まるため、チームや部署を超えた関係の発展に寄与するものだと感じました。

全体の感想

今回は社内だけでなく、普段関わることがないような業種の方々と振り返りを行いました。

社内に留まらない一般的な共通の課題を再認識すると共に、Classiの良い文化や強みを改めて実感する良い機会となりました。

今後も社内だけに閉じず、社内外でディスカッションや知見共有をする活動は続けていきたいです。

輪読会に参加されたメンバー、合同振り返りを主催いただいたKINTOテクノロジーズ様、ご参加いただいた方々、ありがとうございました。

データを活用したQA検証の取り組み

こんにちは!QAチームの池田です。

今回は、2023年度でQAチームが行った取り組みについてご紹介します!

QAチームでは、2023年度で2つ目標を掲げ活動をしてきました。 そのうちの1つの目標が「機能別の検証密度と優先度の抽出」です。

この目標は、検証漏れによる本番環境での不具合を防止することを主な目的としていました。 また、検証実施時の負荷の分散にも考慮し、利用頻度の高い機能の検証密度を高くすることにより効率的に検証を進めていくことを目的としていました。

「機能別の検証密度と優先度の抽出」をするには?

弊社のプロダクト「Classi」には多くの機能があり、それに伴い開発チームも複数存在します。QAメンバーはそれぞれ開発チームに配属されています。

機能により利用頻度の高いユーザー種別やデバイスカテゴリなどが異なりますが、今までそういったデータを活用できていませんでした。 そこで、QA観点での検証密度の向上、機能別の優先度を抽出するために、Google Analytics(以下GA)のデータを活用していくことにしました。

まずGAの中からQA検証時に役立ちそうなデータをピックアップしました。 具体的には下記データです。

  • ユーザー種別
  • PV(ページビュー)数
  • デバイスカテゴリ
  • OS
  • iOSバージョン
  • Androidバージョン

Google AnalyticsとLooker Studioを連携する

QAチームでは、権限の関係でQAチーム全員がGAにアクセスできるわけではありません。 そのためGAでピックアップしたデータを活用しやすいように見える化する必要があります。 以前からQAチームでは見える化施策でLooker Studioを活用していたため、Looker Studioで作成された既存のレポートを参考にデータの見える化を進めました。

Looker Studioで作成したレポートの一部

Looker Studioで作成したレポートの一部

Looker StudioでGAデータのレポートを作成するにあたり、ただデータをグラフとして出力するだけでなく、実際にQAメンバーが現場で活用できるよう分かりやすく使いやすい内容を意識して作成しました。

例えば、GAのデータのままだと分かりづらい内容を計算式を使って分かりやすいテキストに置き換える作業を行いました。 当初ユーザー種別(user_type_id)を表示させようとすると、idがそのまま表示されておりどのユーザー種別なのかが分かりづらい状態でした。そのため、それぞれのidを日本語に置き換える計算式を追加し、誰が見ても分かる表示に変えました。

レポート編集内容の一部

GAのレポートを実際に自分で活用してみる

GAのレポートを作成した後、具体的にどう活用できるのか自分で試してみることにしました。

当時活用できそうな案件がなかったため、自動テストの運用で活用できないかを考えました。 そこで、自動テストで行っているリグレッションテストの内容、範囲の見直しをPV数を基に行いました。

私はプロダクトの中で、「設定・登録」領域を担当しています。設定・登録は学校の先生向けの機能が大半のため、自動テストのシナリオは当時先生の機能しか作成されていませんでした。 しかし領域のPV数を参照したところ、常に「/parent_top」が上位に入っていることに気づきました。設定・登録の中で保護者の機能は少ないですが、保護者からのアクセスが多いということが分かりました。

保護者の設定・登録画面

そこで保護者機能の自動テストを作成し、定期実行に追加しました。

レポートを活用することで、リグレッションテストの範囲の拡充を行うことができました。また、自分の担当領域でどの機能、画面が実際にユーザーから使われているのかを把握していなかったため、検証の優先度を考慮していなかったのだと気づきました。

QAメンバーに展開

実際に自分で活用を試した後、QAメンバーに展開するため説明会を行いました。 今後各QAメンバーがレポートを活用できるよう、活用がイメージできるような説明と発表資料を準備しました。

発表資料の一部①

そのために、実際に私がレポートを活用して行った取り組みを活用例として紹介しました。

発表資料の一部②

QAメンバーの活用状況

QAメンバーにレポートを展開した数ヶ月後に活用状況ヒアリングのためアンケートを実施しました。

アンケート結果によると、1/3のメンバーが早速レポートを活用してくれていることが分かりました。

アンケート結果の一部

活用しているメンバーからは、下記の活用事例が上がりました。

  • 全体のリグレッションテストをする際に、よく見られている画面は細かく、あまり見られていない画面は一通りの機能が触れればOKのように効率よく項目書作成ができた。

  • OS利用状況の確認を行い、検証対象のOS選定を行った。

  • PdMやエンジニアから「生徒がどの端末をよく利用しているか知ってますか?」等の相談事項が多々あったので、その際にレポートを見せて利用頻度の高い端末やOSを確認した。

検証の優先度を考えたり、検証端末・OSの選定、さらにはQAチーム以外のメンバーとも連携をとっている活用状況が見えてきました! まさに施策の目的である「機能別の検証密度と優先度の抽出」に沿った活用が進んでいました。

今後の課題

一方で、まだレポートの活用を進められていないメンバーにどう推進していくかという課題もあります。

QAチームでは、テスト項目書作成前に開発メンバーなどと検証についてのすり合わせをする「QAヒアリング」を行っています。 QAヒアリングではそれぞれのQAメンバーが経験値を問わず必要な情報を確認できるよう、テンプレートとして「ヒアリングシート」を用意しています。 このヒアリングシートの中にレポートのURLの記載をしました。

こうすることでヒアリング準備、もしくはヒアリング中にレポートの確認、活用の検討ができると期待しています。

項目書作成前に開発メンバーとのすり合わせで使用するヒアリングシートの一部

おわりに

前述の通りClassiには多くの機能があり、機能ごとに検証密度と優先度を抽出していく必要があります。

2023年度以前は各機能ごとのデータの活用ができていませんでしたが、2023年度以降実際にデータを参考に検証密度の向上、優先度の抽出に繋げることができたチームもあり、成果が出始めていると実感しています。

今後さらにデータを駆使した検証に貢献できるよう、レポートで反映するデータや対象期間など改良を続けていき日々QAメンバーに活用してもらえるようアップデートしていこうと思っております!

IME変換中のエンターキーで送信される!への対処法[追記あり]

[2024年4月25日 追記] Safariの動作について考慮漏れがありましたので、一部追記・編集しました。

新宿にオフィスのあるClassiは、岡山在住の私のような地方在住者だけでなく、いわゆる通勤圏内に在住していてもリモートワークで働いている人が多い会社です。必然的にミーティングはいわゆるオンラインミーティングとなり、主にGoogle Meetが利用されています。

そのGoogle Meetのチャット機能、ここ1週間ぐらい「IMEで日本語に変換のために押すエンターキーで送信されてしまう」という現象が発生しています。このエントリーを読まれている時点では対応しているかも知れませんが、2024年4月22日17時時点ではその現象は続いています(Windowsでは再現しないという情報もあります)。

Google Meetのチャット欄のスクリーンショット。「にほんごのへんかん」と入力し、これから漢字変換を行う状態。
入力開始

Google Meetのチャット欄のスクリーンショット。漢字変換が行われ候補に漢字変換された「日本語の変換」と表示されている。
変換して確定のエンターキーを押すと

Google Meetのチャット欄のスクリーンショット。送信されたチャットとして「日本語の変換」が表示されている。入力欄にも「日本語の変換」が表示されている。
送信される

エンターキーに頼らない日本語入力を頑張りましたが、手癖というのは抜けないものです

この現象は、Google Meetに限らずさまざまなウェブサービスで発生しています。ChatGPTも話題になってからしばらくはこのような挙動でした。日本や東アジア圏を中心に、IMEを利用しているユーザーからのフィードバックでいずれは対応されると待つのが基本ですが、普段の業務利用も絡んでいるので対応策を整理しました。

ここからは作成したデモに基づいて進めて行きます。

isComposingDemoというデモ画面のスクリーンショット、入力を反映する場所と入力欄のどちらにも「変換中」という文字が表示されている。その下には「IME変換中の送信停止」というボタンがある

「入力欄」で日本語変換を行って確定のためにエンターキーを押すと上に反映される状態を再現しています。その後、「IME変換中の送信停止」と書かれたボタンを押すと、変換確定のエンターキーでは反映されなくなりますが、変換を行っていないときのエンターキーには反応します。

前出のicComposingDemoの画面。入力を反映する場所に「変換後のEnterは反応します」と表示されている。入力欄にはなにも表示されていない。

対応方法

基本的な対応の方針は以下です。

「イベントキャプチャでいち早くIME変換中のエンターキー入力を捕捉して伝播させないようにする」

それを実装したのが下記のコードです。

document.addEventListener('keydown', function(event) {
    if ((event.key === 'Enter' && event.isComposing) || event.keyCode === 229) {
        event.stopPropagation();
    }
},{capture: true});

「イベントキャプチャでいち早く」

addEventListenerの第3引数{capture: true}がその実装です。このオプションでcaptureを有効にすると、keyDownイベントの伝播がターゲットから親方向のバブリングではなく、ドキュメントからターゲットに向かうキャプチャとなり、まずはdocumentがkeyDownの発火対象になります。

参考)

「IME変換中のエンターキー入力を捕捉」

(event.key === 'Enter' && event.isComposing) || event.keyCode === 229の条件式がその判定です。 event.key === 'Enter'はkeyDownされたキー名、event.isComposingはIME変換中であればtrueになります。また、Safariの挙動への対応として非推奨プロパティですがevent.keyCode === 229も加えています。keyCodeの229は、IMEに関連したkeyCodeとして定義されています。この条件に合った場合、日本語変換時のエンターキーと判断し、後述のevent.stopPropagation()で子要素に対しての伝搬を中止します。

参考)

「伝播させないようにする」

event.stopPropagation()がそのメソッド名の通り伝搬を止めます。documentの段階で伝播しないようにすることで、送信のイベントが用意されているinputフィールドにエンターキーの入力が伝わらないようになっています。

参考)

ここまでが「IME変換中のエンターキーで送信される」への対応の基本です。

どうやって稼働中のウェブサービスに適用するか?

対応方法のJavaScriptがあるとして例えばGoogle Meetで困っていた場合、どうすれば良いでしょうか?フィードバックを送ったり、オープンソースであればPullRequestを送ったりという対応が思い浮かびますが、「今すぐ」の解決策は手元のブラウザに上記のJavaScriptを適用することです。

DevToolsのコンソールで実行する

上記のコードをDevToolsのコンソール画面で実行すると、コードの内容が適用されます。該当のサービスにアクセスする度に実行する必要があります。

Google Meetのチャット欄とDevToolsのコンソール画面に前述で紹介したJavaScriptのコードが記載されている。
コンソールに貼り付けた例

ブックマークレットを作成する

下記のコードは、この記事で紹介しているJavaScriptをブックマークレットで利用可能にしたものです。ブックマークとして登録しておくと、必要な時に呼び出せば以降は適用されています。こちらも該当のサービスにアクセスする度に実行する必要がありますが、ブックマークを呼び出すだけです。

javascript:(function()%20%7Bdocument.addEventListener('keydown'%2Cfunction(event)%7Bif((event.key%3D%3D%3D'Enter'%26%26event.isComposing)%7C%7Cevent.keyCode%3D%3D%3D229)%7Bevent.stopPropagation()%7D%7D%2C%7Bcapture%3A%20true%7D)%7D)()

このブックマークレットのコードは同僚の@koki_developが作成したBookmarklet.linkを利用して生成しました。

拡張機能を使う

上記のJavaScriptの作用を組み込んだ拡張機能を利用すれば、アクセスする度に行うといった面倒もなく適用できます。似たようなケースで、ChatGPT向けの「変換中のエンターキーで送信されるのを防ぐ」拡張を散見した記憶があります。

冒頭で話題にしたGoogle Meetだと、Google Meet Chat to Clipboardという拡張機能が先日配信した4.2.0で「変換中のエンターキーで送信されるのを防ぐ」機能を組みました。この記事で紹介した私作成の拡張機能ですが、データの外部送信なく、ファイルのアクセスもなく、クリップボードに入れるだけの拡張機能ですので、是非利用してみてください。

最後に

今現在(2024年4月22日17時)困っているのでGoogle Meetを題材にしましたが、欧米中心で開発されているウェブサービスにはたまにあることです。対応されるのを待つのが基本だと思いますが、クライアント(ブラウザ)で打てる手はありますよ、という紹介でした。参考になれば幸いです。

英文法問題の制作プロセスに生成AIを取り入れた話

こんにちは。データサイエンティストの高木です。
弊社では昨年6月に「学習トレーニング」機能をリリースし、機能の利用促進や改善が進んでいます。
corp.classi.jp

このような学習機能を通して、より質の高い個別最適な学習を実現するためには、多くの「問題」が必要となります。
しかし、問題制作には膨大な時間と費用がかかってしまいます。

そこで、これらの制作時間や費用の削減を目的として、これまで制作工程の一部を自動化する試みを行ってきました。

tech.classi.jp

本記事では、2月にプレスリリースで公表された、学習トレーニングの英文法問題の制作プロセスに生成AIを取り入れた話について紹介します。

prtimes.jp

問題の制作工程

問題の制作工程は以下になります。

図1 問題の制作工程

まず、企画要件に合う問題を教材や過去に作成した問題から選定します(図1: 1)。
次に、それらを基に原稿を作成します(図1: 2)。このとき、全く同じ問題にならないように改題します。
もしも、過去の問題に企画要件に合う問題が存在しない場合は新規で作成します。

そして、作成された原稿をチェック・修正し(図1: 3-4)、社内のCMS(Contents Management System)へ搭載します(図1: 5)。
さらに、テスト画面上でチェック・修正し(図1: 6-7)、問題がなければ本番環境で公開されます。

今回、制作の対象とする英文法問題の例は以下になります。

図2 英文法問題の例

1つの問題は、問題文、選択肢(正答、誤答)、解説文から構成されており、英語の例文の空欄箇所に合う選択肢を1つ選ぶ問題となっています。
空欄箇所で問われる内容(選択肢の種類)について、今回の取り組みでは、英文法問題でよく出題される「動詞の種類」を問う問題を主に作成しています(図2の左の問題)。

また、文法項目に入れ込みながら、語彙力を強化していく目的で「語彙」を問う問題も数問作成しました(図2の右の問題)。

課題とアプローチ

制作工程の中で最も時間と費用がかかるのが、問題の土台を作る図1の1と2の工程です。
問題の制作者(以下、作問者)はいくつもの教材や過去の問題を目視でチェックし、手作業で問題を収集しなければなりません。

さらに、英文法の問題の場合、例文の正しさや長さ、英単語の語彙レベル(単語の難易度)や文脈の適切さ、などが求められます。
これらを考慮して収集した問題を改題したり、場合によっては新規作成しなければなりません。
英語や作問に関する知識があるプロの作問者でもかなり難しい作業です。

そこで、我々はこれらの選定や改題の作業の手間を省くために、作問者の要件(高校英語の単元、語彙レベル、選択肢の種類など)に合う英文法問題を自動生成し、一括で出力することが可能なアプリを構築しました。

具体的な手順の詳細は後述しますが、基本的には、予め、高校英語の単元ごとに出題されるであろう例文を大量に用意しておき、それらをベースに選択肢や解説文を生成するというものです。
これらの生成過程を分解し、GPTやBERT、その他自然言語処理技術を適用することで問題の生成を実現しています。

問題選定時(図1: 1)にこのアプリを利用することで、様々な教材から手作業で選定する時間が削減され、制作費用も削減されることが期待されます。

取り組みのねらい

今回の取り組みでは、社内検証にとどまらず自動生成した問題を実際にリリースすることを目指しました。理由としては以下です。

  • 今後の検討のために各工程に及ぼす効果のベースラインを作っておきたい
  • 制作からリリースを一気通貫することで見えてくる課題や効果がある
  • 技術検証のみで終えず、ユーザーである先生や生徒に届けたい

しかし、作問はとてもクリエイティブな作業であり、人間が行うのも難しい作業です。
この作業を自動化すること自体とても難易度が高いです。

そこで、これらの取り組みをリリースにつなげるために、問題制作の専門家であるコンテンツチームと、自然言語処理技術や教育工学的視点から見た作問についての知見を持っている大学と連携しながら進めてきました。

また、生成AIを使った前例を作っておきたかったというのも一つのねらいです。
今後これらの活用はさらに活発になっていくと思われ、社内でLT会を開いて活用アイデアを出し合ったこともありました。

tech.classi.jp

しかし、著作権など活用するうえで注意しなけれならない点も多々あります。
まだ前例がなかったので、そういった課題とどのように向き合い活用していけば良いかが不透明でした。

そこで、今回の取り組みで、ある程度これらの課題をクリアにし、今後の活用のしやすさに繋げられればと考えました。
実際に、今回の取り組みを機に、生成AIガイドラインが作成され著作権リスクをチェックするプロセスが作成されました。

図3 取り組みの全体像

英文法問題の自動生成

英文法問題の自動生成手順を以下に示します。

図4 英文法問題の自動生成手順

問題を生成する前の準備段階として、高校英語の単元や学習項目ごとに例文データを2つの方法で作成しておきます。
1つは、弊社で過去に出題された問題から例文を抽出する方法です(図4: ①)。
もう1つは、GPTで例文を生成する方法です(図4: ②)。

出題された問題から抽出した例文は、同じ例文にならないように言い換えたものを使用しています。
言い換えの例は以下です。

(変換前)Everyone in our class likes ms. brown.
(変換後)Ms. Brown is well-liked by everyone in our class.

この変換にはGPTを使用しており、以下のようなプロンプトで変換しています。

I'm going to show you some sentences, please rewrite them in different structure and keep their meaning, and try to avoid too high level words.
The rewritten sentences should be clean and natural, without any unnecessary characters.
If the sentences are likely to be in a dialog, you should keep them as a dialog.
Sentences in a dialog start with `-- `.
You should fix mistakes in the sentences if there is any.
You can ignore meaningless words if necessary.

一方で、GPTによる例文の生成では、以下のようなプロンプトで生成しました。

受動態の例文で、例)のような例文を5つ下さい。また、日本語訳も下さい。
例)This car was repaired by brother.

このように例文データを予め大量に作成しておきます。
そして、作問者が作成したい問題の学習項目や例文にマッチする例文を例文データから検索します(図4: ③)。
この検索では、例文間の係り受け構造(dependency tree)をSpacyで解析し、それらの構造の近さから類似度を判定しています(下図)。

図5 係り受け構造に基づく例文間の類似度判定

そして、問題で問われる空欄箇所を特定し選択肢を生成します(図4: ④)。
選択肢の生成では、BERTのbert-base-uncasedモデルを使用しています。

また、選択肢の語彙レベルを制御して出力することも可能となっています。
語彙レベルの制御には使用許諾を取ったうえでCEFR-Jのwordlistを使用しています。

最後に、GPTによって解説文を生成します(図4: ⑤)。
解説文を生成するプロンプトの例は以下になります。

次の日本文に合う英文になるように,( ) に適するものを一つ選びなさい。
{日本文}
{英文}
選択肢
{選択肢}

正答を教えてください。
また、次の順番で以下のキーワードを使って解説をしてください。
{キーワード}

この設問文の文型と意味を説明してください。

これらの手順を組み込んだアプリとその出力例が以下になります。

図6 英文法問題の自動作問アプリの入力画面

図7 自動作問アプリの出力例

リリースと検証

今回、受動態の英文法問題21問を自動生成し、それらの問題が学習トレーニング上でリリースされました(図2)。
実際の作業では、まず、64問を自動作問アプリで生成しその中から21問を選定しました。
その後の作業は、図1の工程2以降の作業と同様です。

これらの作業をコンテンツチームの制作担当者に行ってもらい、自動生成された問題の質はどうだったか、制作時間や費用は削減されたのか、などを評価・検証しました。

自動生成された問題の質

自動生成された問題の質を評価するために、(1)問題内容のチェックによる評価(定性)と(2)問題内容の修正割合による評価(定量)を行いました。

(1)問題内容のチェックによる評価 

はじめに、作問の専門家であるコンテンツチームに問題の品質チェックで必要となる観点をヒアリングしました。
さらに、研究分野における問題作成ガイドライン*1などを参考にし、12個の評価項目を作成しました。

この評価項目を基に、リリースされた21問の問題について作問者に評価してもらいました。
各項目の「適切か」の基準は「修正を入れずにそのまま使えるかどうか」としました。
そして、各項目を満たしていた問題の割合を算出しました。

評価結果は以下になります。

表1 作問者による問題の評価

評価項目 評価項目を満たした問題数(問) 割合(%)
①学習項目に適した問題になっているか 20 95.2
②例文の語彙レベルは適切か 17 81.0
③例文の長さは適切か 21 100.0
④例文の文法は正しいか 18 85.7
⑤例文の文脈は適切か 13 61.9
⑥例文のブランク箇所は適切か 20 95.2
⑦選択肢の語彙レベルは適切か 19 90.5
⑧正解選択肢は唯一となっているか 20 95.2
⑨誤答選択肢はもっともらしいか 11 52.4
⑩解説文に正解となる理由が述べられているか 20 95.2
⑪解説文に誤答選択肢について説明がされているか 20 95.2
⑫解説文の内容は適切か 0 0.0
平均 16.6 79.0

12項目中9項目で80%を超えており、概ね良好な結果となりました。
例文の文脈や誤答選択肢の適切さが少し低い値となっています。
解説文については1つもそのまま使えるものがなかったという結果でした。

今回自動生成した解説文は、正答選択肢と誤答選択肢の説明をするように出力していましたが、実際には正答選択肢の説明のみのシンプルな解説文が必要となり修正が発生したため、このような結果となっています。

(2)問題内容の修正割合による評価

自動生成されてからリリースまでに修正された文字列の割合を算出し評価しました。 修正された文字列の割合 \text{edit_dist_rate} 0 \leqq \text{edit_dist_rate} \leqq 1を満たし、以下の式で計算されます。

 \displaystyle
\text{edit_dist_rate} = \frac{\text{Levenshtein distance(修正された内容, 自動作問の内容)}}{\text{Max(Len(修正された内容), Len(自動作問の内容))}}

計算結果を以下の表に示します。

表2 修正割合による評価

問題文 和訳 選択肢 解説文
 \text{edit_dist_rate}(平均) 0.24 0.21 0.06 0.86
 \text{edit_dist_rate}(中央値) 0.13 0.00 0.00 0.86

問題文、和訳、選択肢ともに中央値は低い値となっています。
(1)の評価結果から、文脈や誤答選択肢でそのまま使えないものはあったものの、修正された文字数としては少なかったと考えられます。

一方で、解説文は高い値となっています。
(1)の結果の通り、シンプルな解説文に修正されたためこのような結果になりました。
解説文を生成するプロンプトを変更することで改善されると考えられます。

自動作問アプリによる費用や時間の削減効果 

図1の各工程では、1問あたりにかかる費用と時間が設定されています。
今回の作業を通して、自動作問アプリを使った場合、各工程1問あたりにかかる費用がどのくらい削減されるかを制作担当者に見積もってもらいました。
そして、それらの費用の削減率から削減された時間を計算しました。

(1)制作時間

図1の工程1から4にかけて、自動作問を利用した場合、1問あたりの制作時間が24%削減されました。

また、工程6の画面チェックにおける修正件数を、自動作問アプリを使った場合と使わなかった場合で比較しました。
その結果、使わなかった場合の修正件数が73件だったのに対して、使った場合は7件に減少しており、指摘の修正や確認作業にかかる時間が約90%削減されていました。

さらに、今回の制作担当者にヒアリングしたところ、以下のような意見を頂きました。

  • 制作が楽になった、時間短縮された
  • 新規の場合、1から作る手間が省ける
  • 流用する場合、そのまま使えない(改題しないといけない)のでその手間が省ける
  • 単語の差し替えくらいで済む

(2)制作費用と運用費用

制作にかかる労力が軽減されたことで、図1の工程1から3で1問あたりにかかる費用が38%削減されました。

また、英語の問題制作では、工程3でネイティブチェックが必要となります。今回はほとんど修正されることがなかったため、そこでかかる費用が大幅に削減されていました。

さらに、Classiの学習トレーニング機能では、課題配信機能や生自主学習機能により数十万人もの生徒に問題が利用されます。
問題を流用した場合にかかるはずだった利用料が、今後100%削減され続けます。

最後に

今回は、英文法問題を自動生成してリリースした話をしました。
問題の制作工程の中でも最もコストのかかる「問題選定」部分に着目し、生成AIをはじめとする自然言語処理技術を組み合わせて問題を自動生成できるアプリを構築しました。

評価や検証の結果、自動生成された問題はリリース作業に利用できる品質であり、かつ、制作コストの削減への効果も大きいことが分かりました。
さらに、今回の取り組みにおいて研究的成果をまとめ、言語処理学会第30回年次大会にてポスター発表も行っています。

prtimes.jp

一方で、今回対象とした問題は英文法問題のみとなっています。
今回のロジックを英単語の意味を問う問題や英文法の並べ替え問題などの制作に応用することは可能だと考えられます。
しかし、教科が変わった場合、今回のロジックをそのまま応用できるとは限りません。

このような技術の汎用性は今後も課題となるので、まずは、現状のロジックの応用範囲を明らかにしつつ、問題制作のニーズや制作される問題の汎用性などを考慮して、着手する対象を決めていきたいです。

また、生成AIの教育現場への活用という広い視野で考えると、コンテンツ制作だけでなく、生徒の新しい学習体験の創出や先生の業務支援なども考えられます。

今回のように、大学や他チームと協業して、このような新たな価値を探索していく活動は継続していきたいです。
そして、ビジネスとアカデミックの両方にインパクトを与えられるような成果を発信していきたいと考えています。

*1:坪田 彩乃, 石井 秀宗: 多枝選択式問題作成ガイドラインの実証的検討, 日本テスト学会誌, 2020, 16 巻, 1 号, p. 1-12

株式会社万葉のみなさんとエンジニア交流会を実施しました

こんにちは、エンジニアの id:kiryuanzu です。

2024年の1月に Classi のオフィスで 株式会社万葉のエンジニアの方々と交流会を実施しました。

今回の記事では万葉さんとの交流会の様子を紹介した上で、他社との交流会を実施してみての振り返りレポートを紹介します。

経緯

去年の RubyWorld Conference の交流イベントの場で万葉の大場さんとお会いし、コードレビューに関する悩みを相談したところ、とても丁寧にアドバイスしていただきました。

tech.classi.jp

ベテラン Rubyist の方からコードレビューについての考え方を熱弁していただいたりなど楽しい出来事がたくさんありました。

(この時のベテラン Rubyist の方が大場さんです)

その時の会話が大変楽しく、大場さんを始めとした万葉の方々と Classi のメンバーを引き合わせてお話しできたら楽しそうだと考え企画しました。

当日の様子

交流会はオフライン形式で西新宿にある Classi のオフィスのオープンスペースを使って実施しました。
万葉からは大場さん、鳥井さん、櫻井さん、児玉さんの4名、Classi からは id:kiryuanzu、中島、中橋、onigra の4名が参加し合計8名で1つのテーブルを囲んで3時間ほどお話しをしました。

当日は同僚おすすめの店で注文したご飯とお酒をみんなでつまみながら以下の話題で盛り上がりました。(交流会中の写真は撮り忘れていました……🙏)

  • コードレビューする際に考えていること
  • 新卒教育についての取り組み
  • 教育プロダクトについての課題感や今後注力したいこと
  • エンジニア面接でよくする質問とその背景
  • 技術系の地域コミュニティについて

万葉のみなさんも教育に強い関心があるようで、学校訪問時のエピソードにとても興味を持ち話を聞いてくださったのが印象的でした。
他にも、Classi のメンバーから面接時の振る舞いについての悩みの相談があり、選考時に大事にしている考え方について教えていただきました。そのように、お互いの普段の業務での考え方を共有して知見を得るといった場面が多くありました。

交流会を実施してみての振り返り

今回筆者が他社との交流会を初めて企画したことでいくつか発見があったので紹介します。

よかったこと

バックグラウンドの近い人たちと情報交換できると共感しやすい

先ほども紹介した通り、教育業界に興味のある方が参加者に多く教育トークになるとかなりの盛り上がりを見せていました。コードレビューや新卒教育での取り組みでも、「これは参考にしたいね」と盛り上がって互いに共感できる要素が多かったように感じました。

一緒に参加した Classi のメンバーからは「初対面とは思えない話しやすさがあった」という感想をいただけたのが大変嬉しかったです。

小規模構成(4対4)だと各参加者の話をじっくり聞きやすい

今回は大人数を集めてワイワイやるというよりかは、少人数で1つのテーブルを囲んでゆったりと話す場となりました。
大規模なイベントだとどうしても特定の人と長く話すのが難しいことが多かったのですが、今回の場をセッティングしてずっと気になっていた方達と腹を割って話す機会を作ることができました。
今後もカンファレンスなどの場で「この方ともっと長く喋ってみたいな / 社のメンバーに紹介したいな」と感じたら思い切ってセッティングしてみたいです。

次回も意識したいこと

企画にあたって交流会のスコープをちゃんと共有した方がよい

交流会と一言でいっても様々なパターンがあり、どういった趣旨の会なのか参加者に事前に共有しておくのが大事だと感じました。
今回は本記事の経緯で述べた体験をメンバーにも共有してもらいたいという前提があったため、協力してくれたメンバーには「万葉の方々とお酒を飲みながらコードの話をする会をしたいです」と説明して会の温度感を汲み取ってもらいました。万葉さんにもその趣旨を伝えてスムーズに認識合わせができました。

また、今回は意識していなかったのですが、少人数の交流会であるなら参加者のプロフィールを事前共有しておくとどんなバックグラウンドを持った方かどうかイメージしやすく会話の場作りに役立ったのではないかと思いました。

ご飯どころに詳しい同僚におすすめのケータリング情報を教えてもらう

筆者は新宿のご飯事情に疎いのですが、ご飯どころに詳しい参加メンバーにおすすめのテイクアウト可能なお店情報を共有していただき大変助かりました。当日は注文した料理をテイクアウトで受け取り、小田急ハルクにある KALDI とビックカメラでクラフトビール等の飲み物を購入しました。
食事のラインナップについても一捻り考えられると会の楽しさが増すように感じ、次回やる時も意識しておきたいポイントとなりました。

次回は意識したいこと

ブログ公開用の写真を撮っておく

当日の様子でも触れていますが、交流会での写真を撮り忘れてしまいました……。これが今回の一番の反省ポイントです。
写真が一枚でもあれば今回の記事で場の雰囲気を伝えられる素材として活用できたはずなのですが、会の話に夢中になってしまい撮り忘れてしまいました。次回は必ず撮るようにしたいです。

おわりに

改めて交流会に参加していただいた万葉のみなさま、ありがとうございました!
企画に協力してくださった Classi のメンバーの方々にも感謝です。
とても素敵な体験となり、今後もこういった他社との交流会を開催できるように動いていきたいです。
読んでいただきありがとうございました。

万葉のみなさんからの感想コメント

この記事の公開に合わせて、交流会に参加していただいた万葉のみなさんからの感想コメントをいただきました。みなさんが会を楽しんでいただけたことが伝わってくる内容で嬉しい限りです。お忙しい中、記事の作成にご協力いただきありがとうございました!

鳥井さん

Classiさんとの交流会楽しかったです! Classiさんの採用やPRでのコミュニケーションの話に「わかる」となったり知見をいただいたり、リアルな教育現場と関わるお仕事の難しさと熱意を感じたりと、大変有意義な時間でした。おいしいお肉とビールありがとうございました

櫻井さん

コードやコミュニケーション、面接やesaの工夫などいろいろお話しできて、とてもたのしかったです!塾や中受の話も含めて、「人を見る、人を育てる」ことの難しさや楽しさを言語化できるよい機会になりました。ありがとうございました〜!

大場さん

記事に書いていただいたのと同じような感想を抱いていました。教育系のお話、PRの書き方の話などが非常に興味深く参考になりました。チキンも美味しゅうございました! お誘い感謝です!!

児玉さん

プロダクトの課題からコードレビューに至るまで幅広いトピックについて深く議論できた交流会で、あっという間に時間が過ぎました。特に、PRのタイトルの付け方については、自分のやり方を振り返る良い機会になりました。素敵な機会を設けていただき、ありがとうございました!

たった1行のPRでチームの"速さ"を可視化できる計測基盤を作った話

こんにちは、データプラットフォームチームの鳥山(@to_lz1)です。エンジニアの皆さん、自分のチームのパフォーマンス、計測していますか?

DevOps Research and Assessment(DORA)の2019年のレポート により、開発チームのパフォーマンスを示す指標として提唱された「Four Keys」。この中に「デプロイ頻度」「変更のリードタイム」という指標があります。

『LeanとDevOpsの科学』など有名な書籍で取り上げられたこともありFour Keysそのものが広く知られるようになりましたが*1、この度Classiでもこれらの指標を可視化するダッシュボードを構築し、社内提供を始めました。

計測の事例がインターネット上に多くある中でも、

  • 横展開を極力容易にするための設計
  • リリースをしてみてから「実際に役に立てる」までの工夫とフォローアップ

といった辺りに独自性があるかと思うので、その内容について本記事で紹介していきます。

「変更のリードタイム」の定義

まず最初に、「変更のリードタイム」の定義について整理しておきましょう。

Googleのブログ 中には「commit から本番環境稼働までの所要時間」と表現されていますが、単純なようでいてさまざまな「定義の揺れ」を引き起こしています。例えば、以下のような議論があります。

「本番環境稼働まで」というのは明確なのですが、「commit から」というのがどうにも曖昧で、よくわからないのです。なにかの時間を測るには start と end が必要ですが、この start にいくつかの解釈があるのです。*2

ここでいうstartの解釈には、以下のようなものがあります。

  • 開発チケットが作られた日時
  • 開発チケットが IN PROGRESS 相当のステータスになった日時
  • 担当者が実装を完了し Pull Request (以下、PR) を出すための commit をした日時
  • PR のレビューやQAが終わり、あとは deploy を待つだけの merge commit をした日時

そして、どれが正解か?というと実は難しいです。先に引用した記事を読む限り、最も原義に近いのは「あとは deploy を待つだけの merge commit をした日時」だと思われます。しかし、それでは「マージからデプロイまでが自動化されているか(≒DevOpsの練度)」を測ることはできても「PRのマージまでに何が起きているのか」は見えてきません。我々開発者からすると、現代において「開発生産性」を測るには不十分な印象が否めないのではないでしょうか。

筆者は「どれが最適かは計測のしやすさや理解のしやすさにより、組織ごとにカスタマイズした定義を用いれば良い」という立場に立っています。今回の取り組みでは、最終的に以下の2つの指標を組み合わせて使うことにしました。

  1. 開発者がPRを出してから、マージされるまでの時間
  2. mainブランチにマージされた全てのコミットの、作成からデプロイ完了までの時間

「開発者がPRを出してから、マージされるまでの時間」が長くなっている場合は、PRを出してからレビューするまでのプロセスに課題があると考えられます。例えば、

  • PRの粒度が大きすぎる
  • 開発者のタスクが多すぎてレビューの時間や知識移転の時間が十分に取れていない

などです。

「開発者がPRを出してから、マージされるまでの時間」は短いのに、「mainブランチにマージされた全てのコミットの、作成からデプロイ完了までの時間」が長いという場合は、PRを出す前、またはPRのマージ後に課題があると考えられます。例えば、

  • 開発着手してからPRを出すまでに要件の確認が多く発生している
  • マージしてからデプロイするまでが適切に自動化されていない

などです。このように、「指標に大きな増減があった時に、何かチームの健康状態に関する情報が得られそうか」という点を意識しながら指標を設計・改善していきました。

実際に作ったもの

現時点では、Redashを使って開発チームにダッシュボードを公開しています。

ダッシュボードの全体像と一部リポジトリの実データ。年末年始に休みがあったことがわかります

複数のリポジトリを選択できるようにしている点は特徴的かと思います。

Classiは大きな機能ごとに開発チームを分ける、フィーチャーチームに近い体制を取っています*3。更にこの中で、「あるチームが複数のリポジトリを管理する」といったことも当たり前です。それ故に、「チームごとにリポジトリを選べること」「複数のリポジトリを横断してデータを可視化できること」が必須の要件だったのでこのような実装にしました。

また、ダッシュボード下部には「直近マージされたPRの一覧」もあります。

リンク付きのPR一覧
リンク付きのPR一覧

RedashのTableは、デフォルトではクエリ結果に含まれるhtmlをエスケープせずそのままレンダリングするため、これを利用してPRへのリンクを付けています*4。以下のようなクエリで手軽に実現できるので便利です。

 select
   repository_name,
   pull_request_title,
   base_ref_name,
   author_login,
   automated_pull_request,
   merged_at,
   merge_duration_hour,
   '<a target="_blank" href="https://github.com/{organization_name}/' || repository_name || '/pull/' || pull_request_number || '">link🔗</a>' as link_to_pr
 from 
   `four_keys.merged_pull_requests`
 where
   repository_name in ( {{ Repositories }} )
   and merged_at >= date_sub(current_date('Asia/Tokyo'), interval 14 day)
 order by 
   merged_at desc

設計と利用技術

パイプライン全体の構成図は以下の図のようになっています。

GitHubから抽出したデータはload =&gt; transformの流れを経てRedashで可視化される
パイプライン全体の構成図と利用技術

Classiには多数のリポジトリがありますが、巨大な開発組織ではないのでマージされるPRの数は週に数百オーダーといったところです。このため、データプラットフォームチームで運用しているCloud Composerをバッチ実行基盤とし、日次でデータをPullするようなアーキテクチャを選択しました*5

認証には、 GitHub App として発行したインストールアクセストークンを用いています。有効期間が短いトークンを処理の都度発行するため、セキュリティレベルを保ちながら、PATを人手で更新するようなtoilをゼロにできます。

また、データの収集には GraphQL API を採用しました。GitHubのGraphQL APIは実務では初めて触ったため最初はスキーマの理解が大変でしたが、公式に提供されている GraphQL API Explorer の利便性が素晴らしく、効率よく理解を深められました。

取得したデータはjsonlファイルとしてGCSに保存し、 Cloud Storage BigLake Table として参照しています。図中の点線部分がこれにあたり、実体となるテーブルをわざわざ作らなくてもjsonlファイルをSQLで即座にクエリできるので、大変便利な機能です。Connectionという概念を挟んでいることで権限の管理もしやすく、これも使ってみて嬉しいポイントでした。

BigLake Tableは メタデータキャッシュ という機能を持っており、特に大量のファイルを扱うようなケースでクエリを効率的に処理してくれます。しかし、メタデータキャッシュが更新されるまでは新たに追加されたファイルがクエリできないなど、面倒な点もあるので、今回のようなユースケースではバッチ処理の都度BigLake Tableを再作成してしまうのも手かと思います。我々のチームでは以下のようなコードで毎回の処理の最後に create or replace external table 文を流しています。

 if __name__ == "__main__":
     # ... (省略)
     gql_client = get_gql_client()
 
     pull_requests_file_name_suffix = "merged_pull_requests"
     data = extract_pull_requests(gql_client, date_from, date_to)
     upload_blob(data, pull_requests_file_name_suffix)
     create_or_replace_external_table(pull_requests_file_name_suffix)
     # ... (省略)
def create_or_replace_external_table(file_name_suffix: str) -> None:
      bigquery_client = bigquery.Client(project=DATAPLATFORM_PROJECT_ID)
      query_job = bigquery_client.query(
          f"""
          CREATE OR REPLACE EXTERNAL TABLE
          `{DATAPLATFORM_PROJECT_ID}.github.{file_name_suffix}`
          WITH CONNECTION `{DATAPLATFORM_PROJECT_ID}.{BIGQUERY_CONNECTION_NAME}`
          OPTIONS(
              format ="NEWLINE_DELIMITED_JSON",
              uris = ['gs://{BUCKET_NAME}/*_{file_name_suffix}.jsonl'],
              max_staleness = interval 1 day,
              metadata_cache_mode = AUTOMATIC
              );
      """
      )
      # wait for job to complete.
      _ = query_job.result()

工夫した点

以下では、開発や導入推進における工夫点を紹介します。

「1行のPRで導入できる」体験設計

序盤から全リポジトリを収集対象にしても良かったのですが、データ量が膨れ上がっても困るため、「利用者側からPRを出してもらって対象リポジトリをOpt-in的に追加していく」という方式を取りました。

リポジトリの追加作業が大変では誰も使ってくれないということが容易に予想できるので、実際の変更は対象リポジトリ名の配列に要素を追加するだけで済むようにしました。

他チームエンジニアが出してくれた1行のPR
これだけのPRで、このチームも明日から生産性指標を可視化できます!

このPRは実際に利用者側のチームメンバーから出して頂いたものです。場合によってはデータの過去遡及反映が必要ですが、これもデータプラットフォームチーム側で一度ジョブを流せば完結するような設計にしています。これらのプロセスを簡略化したことで案内や運用も非常にやりやすかったため、アプローチとして正解だったと思っています。

ベストプラクティスの緩やかな強制

ダッシュボードには「デプロイ頻度」のウィジェットもありますが、チーム横断で計測するには妥当性と実現性のある「デプロイの定義」を決めなければなりません。

縦軸の定義が書かれていない「デプロイ頻度」の棒グラフ
デプロイ「1回」とは何か。簡単なようで、チームごとに違う場合もありますよね

これについては、開発初期から「GitHub Environmentを用いた、”production”という名前が付いた環境に対するDeploymentだけを計測対象とする」という方針を定めました。

GitHub Environmentは、環境ごとの変数の定義や、きめ細やかなレビュー・デプロイ時の制約を設定できる便利な機能です。

デプロイに環境を使用する - GitHub Docs

導入もGitHub Actionsのyamlに1行書くだけと非常に簡単なので、新たに導入してもらう場合でもコストパフォーマンスが良いだろうと判断しました。

Googleのブログでは、プラットフォームエンジニアリングに関して以下のような記述があります。

組織において有用な抽象化を行い、セルフサービス インフラストラクチャを構築するアプローチです。散乱したツールをまとめ、デベロッパーの生産性を高める効果があります。

作ってみてから思ったことではありますが、このように緩やかにベストプラクティスを推進する設計もまた、プラットフォームエンジニアリングの一例と言えるかもしれません。

実利用の現場への入り込み

パイプラインとダッシュボードの初期開発は2週間程度で完了しましたが、実際に利用を普及させるまでには宣伝と利用者ニーズを踏まえた改善が欠かせません。

これについてはどのチームにアプローチするのが良いかマネージャとも相談し、対象のチームに可能な限り深く入り込んで活用のチャンスを探りました。具体的には、

  • そのチームのミーティングに参加して普段のデータに対する接し方を見る
  • リードタイムを集計する既存の取り組みについて聞き、課題感を知る

などです。後者に関しては先日の別記事でも紹介がありましたが、チームの振り返り会の中でリードタイムなどの指標をうまく使っている事例があると分かりました。

tech.classi.jp

PRの粒度が大きい場合や複雑な場合にリードタイムがかなり伸びるということがわかりました。

開いた時に難しそうだと感じるPRは、レビューを後回しにするのではなく、ペアレビューチャンスだと思うようになりました

この取り組みを更に支援すべく、当該チームがシェルスクリプトとスプレッドシートで集計していた指標をダッシュボードにも盛り込み、既存オペレーションからの移行を推進しました。

この結果、チームメンバーからも「手動でぽちぽち計測しなくてもよくなったのでとても楽になって助かった」といったフィードバックをもらえました。

今後の展望

指標を追加する

本記事中でもFour Keysという言葉を何度も使ってきました。当初は「Four Keysをいい感じに可視化しよう」という意図もあったのですが、現在のところは「デプロイ頻度」と「変更のリードタイム」のTwo Keysしか測っていません。

デプロイの頻度と変更のリードタイムは速度の指標であり、変更障害率とサービス復元時間は安定性の指標*6

とも言うように、現在計測できているのは「速度の指標」だけということになります。

とはいえ、変更障害率とサービス復元時間が安定性を測る上で最適かと言うと、また議論の余地があると思っています。このため、ダッシュボードにはこれらの指標をしばらく実装する予定はありません*7

Classiでは現在、SREチームがSLI/SLOの整理と策定を進めてくれている*8ため、これらの取り組みと合流するなどの道も考えられます。筆者としては、「開発チームの気づきを適切に促すこと」がまず第一目的であり、その目的に合致する指標を都度磨き上げていけば良いだろうと考えています。

より定量的な成果に繋げる

今のところ得られたフィードバックはポジティブながら定性的なものが多く「成果」としては弱いです。PRのマージまでの時間を見るのも良いですが、理想を言えば、デプロイ頻度や変更のリードタイムまで含めて向上できたか、更にはリリースした機能が実際にユーザに届くようなインパクトを出したか、という点を語ることこそが重要ではないでしょうか。

そうした会話が当たり前になるためには、メンバー個人の成長も、エンジニアという枠を超えたチームの成長も必要かもしれません。そのような気づきと議論を促進できる指標が何か発見できれば、それを速やかに可視化して提供するのもデータエンジニアの今後の役割だと思っています。

まとめ

Classiの開発チームの「速さ」を測定し、可視化し、気づきを得るためのダッシュボードについて、技術的な面と利活用の面からお話ししました。

Classiでは開発者の体験を良くするために技術を駆使するエンジニアも、最高の開発者体験を享受しながら教育業界に価値を生み出すチャレンジをするエンジニアも、絶賛募集中です。興味をお持ちになった方は、ぜひカジュアル面談や面接に応募してみて下さい!

hrmos.co

hrmos.co

*1:four keysリポジトリは2024年1月にアーカイブされており、率直なところハイプサイクルの幻滅期に至った感覚はあります

*2:https://zenn.dev/junichiro/articles/481b6a0658ba03

*3:現体制が選択されるに至った背景はこちらの記事でお読み頂けます https://tech.classi.jp/entry/2023/08/31/100000

*4:もちろんサニタイズはされます。ref: https://redash.io/help/user-guide/visualizations/table-visualizations

*5:扱うデータが膨大であれば、イベント駆動でデータをPushする構成が望ましいでしょう

*6:https://cloud.google.com/blog/ja/products/gcp/using-the-four-keys-to-measure-your-devops-performance

*7:指標の元になるデータを「あらかじめ収集しておく」ことは言うまでもなく重要です。この観点で言うと、障害報告をチーム横断で管理するスプレッドシートが存在するので、少なくとも「サービス復元時間」は容易に算出できます。

*8:https://tech.classi.jp/entry/2024/02/13/120000

リードタイムを測るシェルスクリプトを作ってチームの振り返り会を活発にした話

こんにちは。エンジニアのすずまさです。

去年の夏頃にリードタイムの計測を始めてから、振り返りで良い気づきを得られるようになったりリードタイムを減らすアクションが生まれたりと良いことがたくさんあったので、今回はその紹介をしようと思います。

リードタイムの定義

LeanとDevOpsの科学』では、リードタイムを「コードのコミットから本番稼働までの所要時間」として定義しています。

私たちのチームのリポジトリではブランチ戦略としてGitHub Flowを採用しており、mainへのマージと本番稼働のタイミングが近しいため「PRをopenしてからマージするまでの期間」をリードタイムとして定めて計測しました。

リードタイム計測を始めた動機

私たちのチームでは「チームのスピードがあまり出ていない気がする」という漠然とした課題感がありました。しかし、課題感はありつつも、ではどうするかと言われると具体的なアクションが出にくい状態が続いていました。

そこで、リードタイムを測ることでボトルネックになっている箇所を明確にし、デリバリのスピードを上げたいと思い計測を始めることにしました。

計測方法

計測方法については、後半に説明する通り自動化できていないなどの改善点がいくつかありますが、一例として誰かの参考になればと思い紹介します。

私たちのチームではGitHub Projectsを使ってPRやissueの管理をしており、クローズしたアイテムは完了レーンに置くようにしていました。
単純に管轄リポジトリのリードタイムを計測するだけだと、複数チームが使うリポジトリの場合に他チームの結果も混ざってしまいます。そこで、「GitHub Project上の完了レーンにあるアイテム」をリードタイム計測の対象とし、計測したアイテムは削除するような運用にしました。

私たちのチームで使っているGitHub Project

実装はシェルスクリプトとRubyで行いました。
シェルスクリプト上でGitHub APIを叩いて完了レーンのアイテムを取得し、Rubyを使ってリードタイムを計算して出力しています。

当初は「完了レーンにあるアイテムを取得するだけならGitHub APIに専用のクエリがありそう」と考えていましたが、そのようなクエリは見当たらなかったため、下記のように実装しました。

  1. GitHub CLIを使ってGitHub APIを叩き、projectのidを取得する
  2. リポジトリを列挙してfor文で回し、下記を繰り返す
    • GitHub APIのsearch queryを使って直近でクローズしたPR/issueを100件取得する
    • 取得したアイテムのうち、対象のproject idの完了レーンに存在するものをJSONファイルの末尾に追加
  3. JSONファイル内の各アイテムのリードタイムを計算して出力

実装は下記の通りです。

シェルスクリプトを使った実装 (1, 2)

#!/bin/bash

PROJECT_NUMBER="1" # GitHub ProjectsのURLに記載されている数値

gh api graphql -f query='
  query ($org: String!, $project_number: Int!) {
    organization(login: $org) {
      projectV2(number: $project_number) {
        id
      }
    }
  }' -f org="classi" -F project_number=$PROJECT_NUMBER >project.json

PROJECT_ID=$(jq -r '.data.organization.projectV2.id' project.json)

repositories=(
  # 計測対象のリポジトリ名を格納する
)

echo -n "[]" >completed_items.json

for ((i = 0; i < ${#repositories[@]}; i++)); do
  search_query="repo:classi/${repositories[i]} is:closed sort:updated-desc"

  gh api graphql -f query='
    query ($search_query: String!) {
      search(type: ISSUE, first: 100, query: $search_query) {
        nodes {
          ... on PullRequest {
            title
            url
            repository {
              name
            }
            assignees(first: 10) {
              nodes {
                login
              }
            }
            labels(first: 10) {
              nodes {
                name
              }
            }
            createdAt
            closedAt
            projectItems(first: 10) {
              nodes {
                id
                project {
                  id
                }
                fieldValues(first: 10) {
                  nodes {
                    ... on ProjectV2ItemFieldSingleSelectValue {
                      field {
                        ... on ProjectV2SingleSelectField {
                          name
                        }
                      }
                      name
                    }
                  }
                }
              }
            }
          }
        }
      }
    }' -f search_query="$search_query" >search_result.json

  # GitHub Project上の"完了"レーンにあるアイテムをtempに格納
  temp=$(
    jq --arg project_id "$PROJECT_ID" '
      .data.search.nodes[] |
      select(. != {}) | select(.projectItems.nodes[] |
      .project.id == $project_id) |
      select(.projectItems.nodes[] |
      .fieldValues.nodes[] |
      select(.field.name == "Status").name |
      contains("✅ 完了"))
    ' search_result.json |
      jq -cs .
  )
  # temp配列の値をcompleted_items.jsonの配列の末尾に追加する
  completed_items=$(jq -s '.[0] + .[1]' completed_items.json <(echo "$temp"))
  echo "$completed_items" >completed_items.json
done

bundle exec ruby format_pr_infos.rb

Rubyを使った実装 (3)

  • format_pr_infos.rb
require "json"
require "./completed_item"

rows = ''

File.open("completed_items.json") do |f|
  completed_items = JSON.load(f)

  completed_items.each do |completed_item|
    next if completed_item.empty?
    item = CompletedItem.new(completed_item)

    rows+="#{item.title}\t"
    rows+="#{item.repository}\t"
    rows+="#{item.assignees}\t"
    rows+="#{item.labels}\t"
    rows+="#{item.lead_time}\t"
    rows+="#{item.formatted_lead_time}\t"
    rows+="#{item.created_at}\t"
    rows+="#{item.closed_at}\t"
    rows+="#{item.url}\n"
  end
end

puts rows
  • completed_item.rb
require 'time'

class CompletedItem
  def initialize(item)
    @item = item
  end

  def title
    @item['title']
  end

  def repository
    @item['repository']['name']
  end

  def created_at
    Time.parse(@item['createdAt']).getlocal("+09:00").strftime("%Y-%m-%d %H:%M:%S")
  end

  def closed_at
    Time.parse(@item['closedAt']).getlocal("+09:00").strftime("%Y-%m-%d %H:%M:%S")
  end

  def lead_time
    "#{format("%02d", hour)}:#{format("%02d", minute_per_hour)}"
  end

  def formatted_lead_time
    "#{format("%03d", date)}#{format("%02d", hour_per_day)}時間 #{format("%02d", minute_per_hour)}"
  end

  def assignees
    assignees_nodes = @item['assignees']['nodes']
    assignees_nodes.map{ |node| node['login'] }.join(',')
  end

  def labels
    labels_nodes = @item['labels']['nodes']
    labels_nodes.map { |node| node['name'] }.join(',')
  end

  def url
    @item['url']
  end

  private

  def time
    @time ||= (Time.parse(closed_at) - Time.parse(created_at)).to_i
  end

  def date
    @date ||= (time / (60 * 60 * 24)).floor
  end

  def hour
    @hour ||= (time / (60 * 60)).floor
  end

  def minute
    @minute ||= (time / 60).floor
  end

  def hour_per_day
    @hour_per_day ||= hour - date * 24
  end

  def minute_per_hour
    @minute_per_hour ||= minute - hour * 60
  end
end

出力した内容は、振り返りやすくするためにGoogleスプレッドシート上に貼り付けて記録するようにしました。 スプレッドシートのフィルタ機能を使えば、任意の期間や担当者などで条件を絞り込み、その条件のリードタイムの平均値や中央値を算出できます。

毎週、リードタイムを記録する際にその週で絞り込んだフィルタを作成することで、後から簡単に見返せるようにしました。

チームの変化

リードタイムを計測してから下記のような良い変化が起きました。

PRの質を意識するようになった

リードタイムを計測したことで、PRの粒度が大きい場合や複雑な場合にリードタイムがかなり伸びるということがわかりました。

粒度の大きいPRは見る箇所が多いので、当然その分レビューにも時間がかかります。
ただ、それだけではなくレビュー自体が後回しにされていたことも原因の一つでした。

レビュアーが「レビューしづらそう」と感じたPRのレビューを後回しにしており、それによりさらにリードタイムが伸びていました。

この気づきにより、レビューしづらいなと感じても後回しにせず、まずはPRの質に関する指摘を積極的にするようになったと感じます。

また、PRを作る側もレビューしやすいPRを意識するようになっており、チームメンバーに対してリードタイム計測前後でどんなアクションを取るようになったかアンケートをとったところ「PRを小さく作るようになった」「descriptionを細かく記述するようになった」という声が寄せられました。

リードタイム計測後に行ったアンケートの回答

ペア/モブ作業が増えた

上記はPR自体の問題によるものでしたが、PR自体に問題はなくても前提の理解が大変だったり、チームメンバーの苦手分野が集まっている変更だったりするとリードタイムが伸びてしまいます。

そういったPRがあった場合は、レビュアー側からPR作成者にペアレビューを呼びかけてリードタイムの短縮に努めました。

ペアレビューを呼びかけると「私もこのPRよくわかってないので参加したいです」というような声が上がって3~4人のモブレビューになることもあり、PR作成者とレビュアー間の知識差を素早く埋めることができるのでかなり体感が良かったです。

開いた時に難しそうだと感じるPRは、レビューを後回しにするのではなく、ペアレビューチャンスだと思うようになりました。

振り返り会で良い気づきを得られるようになった

上述した変化はいずれもチームの振り返り会の中で生まれたアクションです。

ボトルネックが明確になったことで、今まで曖昧だった問題点を捉えやすくなり、振り返り会が活発になったように感じます。

具体的には、振り返り会の中で下記のような問題点に気づけました。

  • チームメンバーの苦手領域が集まっているPRは明らかにリードタイムが伸びている
  • 問い合わせ対応が多い週はリードタイムが伸びやすい
  • タスクを抱えすぎてレビューに手が回らなくなっている

特に計測を始めて一ヶ月くらいは、振り返り会のたびにチームが改善していっている実感があり、チームで働くモチベーションになっていました。

現状の問題点

リードタイムを計測すること自体は良いことづくめなのですが、今の計測方法には下記のような問題点があります。

  • 手動実行が面倒臭い
  • 他チームで流用しづらい

振り返り会の前に手動で実行して、Googleスプレッドシート上に貼り付けて、フィルタ作成して…ということを毎週やっており、かなり面倒です。 手動なのでオペミスが起きることもあり、一度過去に作成したフィルタを全部削除してしまったこともありました。

また、「GitHub Projectの完了レーンを参照する」という自分たちのチームの運用方法に依存した方法にしてしまったため、似たような運用をしているチームではないと真似するのが難しくなっています。

ではどうするのか

「各々のチームがリードタイムを計測しても他チームへの横展開が難しい」という問題点は以前から存在したため、最近データプラットフォームチームが横断的にFour Keysの可視化をするための基盤を作ってくれました! この基盤を利用すれば上述した問題点は全て解決できそうなので、徐々に移行を進めようと考えています。

今後うまく移行できればまた紹介しますので、ご期待ください!

結び

以上、私のチームで行なっているリードタイムの計測方法と、計測したことで起きたチームの変化についての紹介でした。

計測の仕組みを整えるまでにかかるコストに対して得られる気づきは大きかったので、費用対効果が高くやって良かったなと感じました。

少しでもリードタイムを計測しようと思っている方の助けになれば幸いです。 ここまで読んでいただきありがとうございました。

© 2020 Classi Corp.