有名テック企業の技術ブログを、ひとつのフィードで。
フィード
33件
技術情報のキャッチアップは、業務が忙しくなると最初に削られます。意志の問題ではなく、情報収集が時間を細かく、けれど継続的に消費する活動だからだと思っています。newmoでは立派な仕組みを作るより、忙しい週でも続く軽いものとしてエンジニアミートアップを行なっています。 Engineering Meetup とは newmoには「Engineering Meetup」という場があります。週に一度、エンジニアリングに興味のあるメンバーが話題を持ち寄る1時間のランダムトークです。毎週金曜の夕方に、所属や雇用形態を問わず誰でも参加できます。テーマはWebでもモバイルでもクラウドでも自動運転でも、最近読んで面白かった本でもよく、「newmoのここに困っている」といった仕事の話も歓迎です。 コンセプトは「準備を頑張らずにゆるく話す」。資料は作らず、話したいテーマをNotionに書いて当日集まって話します。参加は強制せず、忙しい週は欠席でも、耳だけのラジオ参加でもよく、最低2人集まれば開きます。 モチベーション 一人で技術の変化を追い続けるのには、限界があります。割ける時間と注意が有限だからです。だったら一人で抱えるより、チーム全員が薄くアンテナを張って持ち寄るほうが、広く拾えます。持ち寄って話すこと自体が、お互いの興味や困りごとを知る機会にもなります。そして拾ったものを読んで終わりにせず、自分たちのコードや仕事へつなげたい。このやり方は、負荷を分担すること、消費で終わらせないこと、お互いを知ることのために続けています。 やっていること エンジニアミートアップをやるには何かしらの話すネタ集めが必要です。 ネタ集めの仕組み自体は目新しくありません。Slackで気になったメッセージ(記事、リリースノート、インシデント、誰かの登壇やブログ)に、専用の絵文字(:meetup_neta:)を押します。するとReacji Channelerが、それをネタ帳チャンネル(#nm-dev-meetup)へ自動で集めてくれます。 engineer-meetup 肝心なのは、投稿し直す手間がないことです。読んでいる場所でスタンプを押すだけなので、忙しい週でも止まりません。 絵文字はあえて1種類にしています。「紹介したい」「聞きたい」と意図で分けたくなりますが、分けると押す前に迷いが生まれ、その一瞬が手を止めます。信号は「気になった」の一点でいい。分類は、後で人が話すときにやれば足ります。 週に一度、溜まったネタをClaude Coworkに渡し、古い順に要約してNotionのmeetupページにまとめさせます。各ネタはSlackやNotionを検索させて背景を補わせます。これがその週のアジェンダになり、あとは毎週の定例でそれを見ながら話します。 Coworkでスケジュール実行しているプロンプトもすごく単純です。 Notionの「Engineering Meetup」の今日のmeetupページにネタを古い順でまとめてください。 * Slackの#nm-dev-meetupから古い順まとめてください * 前回のものを見てサマリの書き方は見てください * サマリに必要な情報はSlackやNotionなどを検索してまとめてください 線の正しさは、点が一定量ないとわからない 集めたネタは、ひとつひとつが点です。その点を並べて「これはこういう流れですね」と解釈する、つまり線を引くのは、そんなに難しくありません。これは人だけでなくAIにもできます。情報を渡せば、AIはもっともらしい線をいくらでも引いてくれます。 問題は、その線(解釈)が当たっているかどうかです。そして線の正しさは、点が一定量ないとわかりません。研究の作法を扱ったリサーチのはじめかたという本に、点が少ないとその点を通る線は何本でも引けてしまう、という話があります。資料が少なければ解釈は無限にある。これは引く主体を問いません。人が引いてもAIが引いても、点が少なかったり偏っていたりすれば、それらしいだけの線(解釈)になります。 点がひとつしかない、あるいはほんの少ししかないときに、どうやって点と点をつなぐことができるだろう。まだほんのとっかかりの段階で、解釈や議論──点と点を結ぶ推論の線──をどうして描きはじめることができるだろう。点がひとつ、ふたつ、あるいは三つしかないとしたらどうだろう。 (...中略...) 研究の初期段階では、問いや解釈の可能性が無数にあるから、点と点を結ぼうとしてもあっという間に収拾がつかなくなる。つまりこのパズルは解けないのだ。これほど「点」の数が少ないと、その点を通る線は何本でも引けてしまう。つまり研究者目線で言えば、資料の数が少ないときは、筋立ても解釈も無限に存在することになる。 -- リサーチのはじめかた - トーマス・S・マラニー、クリストファー・レア そのため必要なのは、全員が薄くアンテナを張って、点をたくさん、いろんな方向から集めることです。点が増えるほど引ける線は絞られ、それらしいだけの線が当たる線になっていきます。 たとえば、この数ヶ月のサプライチェーン攻撃がそうでした。axiosやCheckmarx/Bitwardenの侵害(4月)、TanStackのpostmortem、Nx Console拡張の汚染(5月)、CAMPFIREの不正アクセス調査レポート(6月)。どれも単発で見れば「物騒だね」で流れていく点です。 それが数ヶ月、複数人のアンテナで溜まって初めて、「漏れたトークンがCI/CDを踏み台に本番へ権限昇格していく」という一本の線が見えてきました。1点や2点では、ここまでの線は引けませんでした。 場をひらき続けること いちばん大事だと思っているのは、毎週決まって話す場があることです。ただ、この場の目的は、その場で答えを出すことではありません。持ち寄ったネタを声に出し、お互いのアンテナを重ねて、拾えるネタを増やしていき、コミュニケーションに繋がる。それが場の役割です。 たまに「これ先週のあれと繋がるね」と、その場で点と点がつながることもあります。でもそれは狙って起こすものではなく、点が十分たまったときに、ときどき現れるくらいのものです。毎回つなげようと気負わなくていい。点を持ち寄り続けることのほうが、ずっと大事です。 場そのもの(カレンダーの繰り返し予定とMeetのリンク)を用意するのは簡単です。本当に必要なのは、誰かが口火を切り、参加に濃淡があっても回り続けること。スタンプも顔出しもコストは下げてあるので、回す人さえいれば成立します。 線は誰にでも引けます。けれど、その線が正しいかどうかは、集めた点の数と多様さでしか確かめられない。だから私たちは、立派な仕組みを作るより、気になったネタが集まる場として、エンジニアミートアップをやっています。
newmoではフロントエンドとバックエンドの通信をGraphQLで行っています。GraphQLのスキーマは、フロントエンドとバックエンドが合意した唯一の正しい定義、いわば「正となる単一の情報源(Single Source of Truth)」です。このスキーマを正として、開発と自動テストの両方をここから組み立てたい。その基盤として@newmo/graphql-fake-serverを自作してOSSとして公開しています。 このライブラリは、スキーマを正としたまま、2つの使い方を1つのサーバで両立します。1つはスキーマに@example* directiveを書くだけで値が返るDeclarative Fake、もう1つはHTTP経由で動的にレスポンスを登録できるDynamic Fakeです。どちらの値もスキーマから派生するため、スキーマと矛盾するFakeは作れません。newmo-appではこのFake Serverを使ったPlaywrightのIntegration Testが数百ページ分書かれています。 この記事では、なぜスキーマを正とする発想からStubではなくFake Serverに行き着いたのか、既存ツールがある中でなぜ自作したのかを解説します。あわせて、実際にどう動いていて、newmoの中でどう運用しているのかも順に見ていきます。 スキーマを正とする 出発点は、GraphQLのスキーマを正とすることです。default値やテストで登録するレスポンス、その検証やlintまで、すべてスキーマから派生させたい。この発想でテスト戦略を組むと、テストで本物の代わりに使うもの(Test Double)の選び方も決まってきます。 Googleが公開しているSoftware Engineering at Googleの第13章では、Test DoubleをStub・Mock・Fakeの3種類に分けています。Stubは用意した応答を返すだけのもの、Mockはそれに加えて「どう呼ばれたか」を検証するもの、Fakeは本物に近い振る舞いをする軽量実装(章中ではin-memory databaseが例)です。 同じ章の要約(13.10)では、運用上の優先順位が次のように示されています。 テストダブルより本物の実装が優先されるべきである。テスト内で本物の実装が利用できないなら、フェイクが理想的な解法である場合が多い。スタビングを使いすぎると、不明確で脆いテストにつながる。インタラクションテストは、できるだけ避けるべきである。 -- Googleのソフトウェアエンジニアリング - O'Reilly Japan スキーマを正としたいなら、この優先順位の中で選ぶべきはFakeです。スキーマから派生したdefaultやレスポンスを返すには、スキーマを実際に実行する軽量な実装が要ります。一方、リクエストを横取りして用意したモックデータを返すだけの仕組みは、Test Doubleの分類ではStubに当たります。フロントエンドでよく使われるMSWや、Playwrightのpage.routeがこのStubです。Stubはスキーマを経由せずに応答を組み立てるため、スキーマと矛盾する嘘の応答も書けてしまいます。スキーマを正とするなら、選ぶのはStubではなくFakeになります。 newmoのフロントエンドのテストガイドラインでもこの順序を採用しています。Unit TestとIntegration TestではStubを使わず、引数に本物のオブジェクトを渡せる設計にすることを基本にしています。 newmoのフロントエンドにおけるテスト戦略 newmoフロントエンドのテストは3層で構成しています。 種類 ツール 通信 用途 Unit Test Vitest なし 複雑なロジック、条件分岐の多い関数 Integration Test Playwright Fake Server ページとしての動作、操作フロー E2E Test Playwright 本物のAPI 落ちたらユーザーが何もできなくなる最重要フロー 複雑なロジックはUnit Testで書けるようPureなJavaScriptに分離し(Domain Logic Separation)、ReactはPresentation Layerとして扱います。Integration Testの通信先はFake Server、本物のAPI通信はE2Eだけで行います。 E2Eは「落ちたらユーザーが何もできなくなる最重要フロー」だけを対象にして、量を絞っています。 テスト量の大半はIntegration Test、つまりPlaywright + Fake Serverで書かれています。この層が安定しないとテスト戦略全体が成り立たないため、Fake Serverを安定運用できる基盤の用意が必要でした。 flowchart TB subgraph Unit["Unit Test (Vitest)"] U["純粋関数 / Pure JS"] end subgraph Integration["Integration Test (Playwright)"] direction LR I["Browser (Page)"] -- "GraphQL over HTTP" --> F["graphql-fake-server"] end subgraph E2E["E2E Test (Playwright)"] direction LR E["Browser (Page)"] -- "GraphQL over HTTP" --> R["本物のGraphQL API"] end Unit ~~~ Integration ~~~ E2E 既存ツールでは両立できなかったので自作した スキーマを正とする前提に立つと、必要なのはDeclarative FakeとDynamic Fakeの両立でした。Declarative Fakeは、スキーマに「この型のこのフィールドはデフォルトでこの値を返す」と書ける仕組みで、UI開発とGraphQL Playgroundでの疎通確認に使います。Dynamic Fakeは、テストごとにHTTPで「このリクエストにはこのレスポンスを返す」と動的に切り替えられる仕組みで、Integration Testに使います。どちらもスキーマから派生する層なので、両方の機能が必要でした。 GraphQLのFakeを実現するライブラリ自体は、Proposalを書いた2024年4月時点でもいくつか存在していました。主なものは次の3つで、それぞれスキーマからの派生が片側に寄っていました。 mizdra/graphql-codegen-typescript-fabbrica: GraphQLの型から型安全なfake factoryを生成するcodegenプラグイン。関心はコード上で型安全にFakeオブジェクトを組み立てることにあり、HTTPサーバではない APIs-guru/graphql-faker: スキーマにdirectiveを書いてランダム値を返すモックサーバ。静的中心で、テストごとに動的にレスポンスを切り替える仕組みは弱い wayfair-incubator/gqmock: HTTPで動的にモックを上書きできるApollo Serverベースのモックサーバ。一方、スキーマ駆動による静的記述の層は弱い 3つの先行ツールはどれもDeclarativeかDynamicのどちらかに寄っており、スキーマを起点に両方を派生させる形にはなっていませんでした。加えてnewmoの場合は将来的にiOSやAndroidからも使う可能性があり、HTTP-firstで言語非依存にしたいという要件もありました。これらをすべて満たすライブラリは2024年4月時点で存在しなかったため、自作する判断になりました。 初期実装はmizdraさんのfabbricaのアプローチを参考にしました。READMEのCreditsにはfabbrica、graphql-faker、gqmockを載せています。 なお、2026年6月には、directiveでモックデータを指定する@mock(GAP-10)が、GraphQL Auxiliary Proposals(GAPs)の1つとして提案されています。directiveでモックデータを書く点は、Declarative Fakeと近い発想です。ただし@mockはoperation(クライアントのクエリ)側にdirectiveを書き、応答をクライアントでマージする方式です。スキーマにないfieldも意図的にモックできるため、スキーマを正としてサーバ側でスキーマを実行するgraphql-fake-serverとは設計の軸が異なります。 Fakeのオーナー不在問題 もう1つ、Proposalの段階で意識していたのがFakeのモックデータのオーナー不在問題です。 ページや機能ごとに場当たり的に偽の応答を書き散らしていくと、誰がそれのOwnerなのかが不明確になり、本物のサーバとの間に齟齬が生じやすくなります。個別に作った偽物はメンテナンスのコストがかかるため、結局メンテされなくなり、嘘の応答を返したまま残るというのが典型的な失敗パターンです。 graphql-fake-serverでは、これに対する答えとして2つの方向でスキーマ駆動を貫きました。1つ目はDeclarative Fakeをスキーマ側に置く構造です。@exampleString(value: "...")のようなdirectiveはスキーマファイルに書くため、スキーマを変えるPRの中で同じレビューを通ります。2つ目はDynamic Fake側の仕組みです。後述のCode Generatorが型付きfake clientを生成するため、スキーマを変更するとfake clientも再生成されます。スキーマと非互換なFakeが残ればIntegration Testがコンパイルや実行の段階で落ちます。「スキーマのOwner = FakeのOwner」が抽象的なルールではなく、テストのfailureとして実装側から強制される構造になっています。 graphql-fake-serverの仕組み ここからはgraphql-fake-serverの仕組みを4つに分けて解説します。Declarative Fake、Dynamic Fake、/fake/called、ESLint Pluginの順です。 Declarative Fake: スキーマで静的に書く Declarative Fakeは、スキーマに@example*系のdirectiveを書くだけで使えます。本記事ではBookとAuthorを題材に最小例で解説します。 # schema.graphql type Query { books: [Book!]! } type Mutation { addBook(input: AddBookInput!): AddBookPayload! } input AddBookInput { title: String! authorId: ID! } type AddBookPayload { book: Book errors: [Error!]! @error } type Book { id: ID! @exampleID(value: "book-id") title: String! @exampleString(value: "The Great Gatsby") author: Author! errors: [Error!]! @error } type Author { id: ID! @exampleID(value: "author-id") name: String! @exampleString(value: "F. Scott Fitzgerald") } type Error { message: String! } このスキーマを用意してnpx @newmo/graphql-fake-server --schema schema.graphqlで起動すると、Fake Serverが立ち上がります。 http://127.0.0.1:4000/queryにGraphQLクエリを投げると、@example* directiveに書いた値が返ってきます。たとえば次のクエリを投げます。 query { books <span class="sy
newmo では依存パッケージの更新に Renovate を使っています。これまでは Mend が提供するクラウド版(Mend-hosted の GitHub App)を利用していました。しかし Kernel out-of-memory エラーで Renovate がまともに動かなくなりました。 この記事では、なぜ動かなくなったのか、そしてクラウド版から GitHub Actions 上で動かす Self-host 版へどう移行したのかを紹介します。 過去に書いた Renovate と pnpm catalog で依存を管理する話 の続編にあたる内容です。 先に結論 クラウド版 Renovate がメモリ制約(3GB)で Kernel out-of-memory を起こし、PR を作れなくなった メモリ・実行時間・環境変数の制約を回避するため、GitHub Actions 上で動かす Self-host に移行した NODE_OPTIONS=--max-old-space-size=4096 でヒープ上限を広げ、OOM は解消した ARM ランナーの平日 3 回/日運用で、実コストは月 $4 程度に収まった self-approve や conflict の rebase など、クラウド版が裏で吸収していた処理は専用ワークフローで補う必要があった 何が起きたか きっかけは Renovate が PR を作らなくなったことです。Mend のダッシュボードを確認すると、ジョブが Kernel out-of-memory エラーで失敗し続けていました。 DEBUG: Executing command (branch="renovate/dev-sdk") { "command": "pnpm install --lockfile-only --recursive --ignore-scripts --ignore-pnpmfile" } ログは pnpm install の途中で途切れており、lockfile の更新時にメモリを使い切ってカーネルに kill されている、という状況でした。 newmo-app は Go・TypeScript・Swift・Kotlin・Terraform などを 1 つのリポジトリに収めたモノレポです。フロントエンドだけでも pnpm workspace 配下に多数のパッケージがあり、pnpm install --lockfile-only のピークメモリが大きくなります。同様の事例は Renovate 本体の Discussion でも報告されていました。 Kernel out-of-memory + timeouts / related to Gradle wrapper update?(renovatebot/renovate Discussion #39475) 原因調査と、クラウド版の限界 Discussion では Gradle wrapper の更新が疑われていました。Gradle 系の manager を順番に止めて切り分けましたが、いずれも効果はありませんでした。 結局、特定の manager ではなく pnpm install のメモリ使用量そのものがクラウド版の上限を超えていました。Node.js のヒープ上限を広げれば回避できそうですが、クラウド版ではその制約に突き当たります。 クラウド版 Renovate には次の制約があります。 項目 クラウド版の制限 メモリ 3GB 実行時間 30 分 環境変数 任意の env を渡せない 各プランの制限は Mend Renovate Cloud-hosted の overview に記載があります。上記は Community Cloud(無料プラン)の値です。 NODE_OPTIONS="--max-old-space-size=4096" のような環境変数を渡してヒープ上限を引き上げたくても、クラウド版では環境変数を注入する手段がありません。pnpm 側にメモリ使用量を制限するオプションも特にないため、クラウド版の枠内では打つ手がない、という結論になりました。 実行時間も切実で、OOM を回避できたとしても 21 分 30 秒(30 分制限)まで迫っており、モノレポの成長を考えると遠からず実行時間でも頭打ちになる見込みでした。 解決方針: GitHub Actions で Self-host する Renovate は renovatebot/github-action を使うと GitHub Actions 上で Self-host できます。これを選んだ理由は次の通りです。 GitHub Actions のランナーはクラウド版(3GB)より大きなメモリを使え、NODE_OPTIONS も自由に渡せる 実行時間の上限がクラウド版の 30 分より大きく、timeout-minutes で必要に応じて調整できる 既存の CI が GitHub Actions なので、認証やシークレット管理の仕組みを流用できる コストも事前に試算しました。GitHub Actions の課金は分単位なので、実行頻度がそのままコストになります。ここでは 1 回の実行を 25 分と仮定しています。 頻度 回数/日 ubuntu-latest($0.008/min) ubuntu-arm64($0.005/min) 1 時間ごと 24 回 $144/月 $90/月 4 時間ごと 6 回 $36/月 $22.50/月 1 日 1 回 1 回 $6/月 $3.75/月 計算式: 単価 × 25分 × 回数 × 30日 実際には平日 4 時間ごと(1 日 3 回)で十分なので、ARM ランナーを使えば月 $10 弱に収まる試算です。 移行後の実測値も載せておきます。次は GitHub Actions の使用状況メトリクスから集計した、移行後ひと月分のコストです。 ワークフロー 役割 実行時間 実行回数 ランナー 概算コスト renovate.yaml Renovate 本体 769 min 63 ARM($0.005/min) $3.85 renovate-approve.yaml automerge の自動承認 40 min 39 SLIM($0.002/min) $0.08 renovate-config.yaml 設定の dry-run 検証 32 min 15 ARM($0.005/min) $0.16 合計 — 841 min 117 — 約 $4.1/月 試算では月 $10 弱でしたが、実測では月 $4 程度に収まりました。コストの 9 割以上は Renovate 本体(renovate.yaml)で、769 分 / 63 回 = 平均 12 分強と、クラウド版の 30 分制限に対して余裕があります。承認は SLIM ランナー($0.002/min)で動かしているため、回数の割にコストはほぼ無視できる水準です。 アーキテクチャ Self-host 版は大きく 3 つのワークフローで構成されています。 Renovate の実行(renovate.yaml) automerge 対象 PR の自動承認(renovate-approve.yaml) conflict の手動解消(@newmo-kun fix conflict) Renovate の実行 クラウド版の GitHub App と違い、Self-host では「誰として PR を作るか」を自分で用意する必要があります。newmo では newmo-kun という GitHub App を作り、その権限で Renovate を動かしています。 %%{init: {"theme": "forest", "themeVariables": {"lineColor": "#ffffff"}}}%% flowchart TD A[GitHub Actions<br>.github/workflows/renovate.yaml] --> B[GCP認証<br>Workload Identity] B --> C[Secret Managerから<br>GitHub App秘密鍵を取得] C --> D[newmo-kun GitHub App<br>トークンを生成] D --> E[renovatebot/github-action<br>でRenovate実行] E --> F[PRの作成/更新] ワークフローの本体は次のようになっています。認証は Workload Identity Federation で GCP に対して行い、GitHub App の秘密鍵は Secret Manager から取得します。このあたりは既存 CI の仕組みをそのまま流用できました。 # .github/workflows/renovate.yaml on: workflow_dispatch: # 手動実行(dry-run / log-level を指定できる) schedule: # cron は UTC 基準。UTC 0:00 / 4:00 / 8:00 = JST 9:00 / 13:00 / 17:00(平日) - cron: "0 0,4,8 * * 1-5" jobs: renovate: timeout-minutes: 60 # コスト削減のため ARM ランナーを使用 ($0.005/min vs $0.008/min) runs-on: ${{ vars.RUNNER_ARM_DEFAULT }} steps: # ...(GCP 認証 → Secret Manager → GitHub App トークン生成は省略)... - uses: renovatebot/github-action@v44.2.6 with: renovate-version: ${{ env.RENOVATE_VERSION }} configurationFile: .github/renovate.json5 token: ${{ steps.github-app-token.outputs.token }} env: # pnpm lockfile 更新時の OOM 対策(大規模 monorepo でヒープ上限に達するため) NODE_OPTIONS: "--max-old-space-size=4096" RENOVATE_PLATFORM: github RENOVATE_PLATFORM_COMMIT: "true" # リポジトリキャッシュ有効化 RENOVATE_REPOSITORY_CACHE: enabled # 対象リポジトリを明示指定 RENOVATE_REPOSITORIES: '["your-org/your-repo"]' このうち NODE_OPTIONS: "--max-old-space-size=4096" が OOM への直接の対応です。クラウド版では環境変数を渡せず設定できませんでしたが、Node.js のヒープ上限を 4GB に広げた結果、pnpm install --lockfile-only の OOM は再発していません。renovate.json5 自体(パッケージのグルーピングや automerge の設定)はクラウド版からそのまま引き継いでいるので、利用者から見た挙動はほぼ変わりません。 スケジュールの考え方 定期実行は平日の 4 時間ごと(JST 9:00 / 13:00 / 17:00)に設定しました。一方で renovate.json5 側の schedule は、ワークフローの起動時刻より少し広めの範囲を指定しています。 スケジュールが 2 段階あるのは、GitHub Actions の cron と Renovate 自身のスケジュール判定が独立しているためです。cron は「いつワークフローを起動するか」だけを決めます。Renovate は起動後に renovate.json5 の schedule を見て「いま更新してよい時間か」を改めて判定します。 つまり cron の起動時刻が schedule の帯にカバーされる時だけ更新が走ります。帯が狭いと、起動しても skip される時刻が出ます。 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/newmo/202
newmoのフロントエンド開発では、複数の機能を持つ単一のNext.jsアプリケーションを開発しています。このアプローチは、コードの共有やCIの管理といった面でメリットがある一方で、PlaywrightテストのCI実行における安定性とテスト時間という2つの課題を抱えていました。 この記事では、newmoで実施したPlaywright CI最適化の取り組みについて紹介します。 Docker Image導入によるセットアップの安定化と、テスト分割とShardingによる実行時間の短縮という2つのアプローチを実施しました。 結論を先に述べると、Docker Image化でセットアップ起因のCI失敗をほぼ解消し、テスト分割で最も多い小規模PRのCI時間を約42%(平均18%)短縮しました。仕組みは「①公式Docker Imageの利用」と「②変更検知によるproject単位の選択実行とSharding」の2つです。 なお、newmoでは同様のSharding手法をESLintのCI実行にも適用しています。詳細は「GitHub ActionsのmatrixによるESLint Shardingで実行時間を短縮」をご覧ください。 背景となる課題 newmoのNext.jsアプリケーションの1つは、複数の機能領域(以下、サブアプリ)を含んでいます。各サブアプリは独立したディレクトリ構造を持ち、1つのアプリケーション内で管理されています。 それぞれのサブアプリに対してPlaywrightを使ったIntegration testを実行しています。newmoでは、各ページに最低1つ以上のIntegration testを書く方針をとっており、実際には1ページあたり複数のテストケースを実装しています。 従来のPlaywrightのCIでは、2つの問題がありました。 問題1: セットアップの不安定性 従来のCI設定では、pnpm playwright install chromium --with-depsコマンドを使っていました。このコマンドで、Playwrightのブラウザバイナリとシステム依存関係をインストールします。 ブラウザバイナリ自体はGitHub Actionsのキャッシュ機能を使ってキャッシュできていました。一方、システム依存関係(--with-depsでインストールされるapt-getパッケージ)は毎回インストールする必要がありました。 このシステム依存関係のインストールが、GitHub ActionsのワークフローにおけるPlaywrightのセットアップステップでたまにタイムアウトやエラーを起こしていました。 具体的には、 apt-get によるシステムパッケージのインストールが不安定で、ネットワークやミラーサーバの状態によっては失敗することがありました。 特に processing triggers for man-db の処理では異常に時間がかかり、CIのタイムアウト事例が複数発生していました。 また、ブラウザバイナリのキャッシュ戦略も複雑でした。キャッシュヒット時とミスヒット時で異なるコマンドを実行する必要があり、この分岐ロジックが各アプリケーションのCIファイルに重複していました。セットアップに関するコードだけで40行以上のYAML設定が必要で、メンテナンスコストが高く、設定の不整合が起きやすい状態でした。 問題2: テスト実行時間の増大 機能領域(サブアプリ)ごとに独立したディレクトリ構造を持っていますが、従来はどのディレクトリに変更があっても全てのテストが実行されていました。 Next.jsのRoute Groupsという機能を使って、括弧で囲んだディレクトリ名により論理的な分離を実現しています。これにより、URLパスには影響を与えずに、ディレクトリ構造で機能領域を分けられます。 newmoでは、各ページに最低1つ以上のIntegration testを書く方針をとっているため、page.tsxとpage.play.tsを同じディレクトリに配置しています。.play.tsという拡張子は、Playwrightのテストファイルであることを示すnewmo独自の命名規則です(Playwrightのデフォルトは.spec.ts)。これにより、ページの実装とテストの関連性が明確になり、ページを変更する際に対応するテストを見つけやすくなります。また、ページとテストが同じ場所にあることで、ページを削除する際にテストも一緒に削除しやすく、不要なテストが残り続けることを防げます。 そのため、次のようなNext.jsアプリケーションのディレクトリ構造になっています。 src/app/ ├── (private)/ │ ├── (app-a)/ # サブアプリA │ │ ├── _components/ │ │ ├── page-1/ │ │ │ ├── page.tsx │ │ │ └── page.play.ts # Integration test │ │ └── page-2/ │ │ ├── page.tsx │ │ └── page.play.ts │ ├── (app-b)/ # サブアプリB │ │ └── ... │ ├── (app-c)/ # サブアプリC │ │ └── ... │ └── (app-d)/ # サブアプリD │ └── ... └── _components/ # 共通コンポーネント(commonとして扱う) この場合に、変更されたサブアプリのディレクトリに応じて対応するテストだけを実行する仕組みがありませんでした。 たとえばサブアプリAのディレクトリだけを変更したPRでも、サブアプリBやサブアプリCのテストまで実行されていました。 従来はPlaywrightのshardingを使って全テストを4並列で実行していました。しかし、このアプローチには2つの問題がありました。 第一に、並列数を増やすとセットアップ時間が増加するため、コスト効率が良くありませんでした。第二に、shardingは全テストを均等に分割するだけなので、変更していないサブアプリのテストまで実行されることには変わりません。 変更していないサブアプリのテストも実行されるため、開発者のフィードバックサイクルが長くなっていました。この問題を解決するには、shardingによる並列化だけでなく、変更されたサブアプリのテストのみを選択的に実行する仕組みが必要でした。 さらに、関係ないテストが失敗すると、自分の変更に関する問題なのかどうかの判断に時間がかかり、開発効率が低下していました。 これらの問題は相互に関連しています。セットアップが不安定だとテスト実行時間の最適化の効果が薄れ、テストが長時間実行されるとセットアップの失敗の影響が大きくなります。両方を同時に改善する必要がありました。 Docker Image導入によるセットアップの安定化 まず取り組んだのは、セットアップの安定化です。MicrosoftがDocker Hubで公開しているPlaywrightの公式Docker Imageを使う方法を採用しました。 Docker Image導入の効果 PlaywrightのDocker Imageには、ブラウザバイナリとシステム依存関係があらかじめインストールされており、セットアップの手間を削減できます。 従来はキャッシュ管理とインストールステップの複雑なロジックが必要でしたが、Docker Imageによりシンプルな設定で済むようになりました。 # 従来の設定: キャッシュ管理とインストールステップ - name: Get installed Playwright version run: echo "PLAYWRIGHT_VERSION=$(yq '.catalog.@playwright/test' ./pnpm-workspace.yaml)" >> "$GITHUB_ENV" - name: Cache playwright binaries uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}-v2 - name: Install Playwright Browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: pnpm playwright install chromium --with-deps - name: Install Playwright's dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: pnpm playwright install-deps chromium # ... さらに続く # Docker Image導入後 container: image: mcr.microsoft.com/playwright:v1.52.0-noble options: --init --ipc=host --user=1001 この変更により、ブラウザバイナリのキャッシュ戦略やシステム依存関係のインストールといった複雑なロジックが不要になりました。 apt-get やシステムパッケージのインストールが不要になるため、ネットワークやミラーサーバの状態に依存せず、安定したCI実行が可能になりました。 バージョン管理の自動化 Playwrightのバージョンは pnpm-workspace.yaml のcatalogで管理しています。Docker Imageのバージョンもこのcatalogのバージョンと一致させる必要があるため、バージョンを動的に取得する仕組みを実装しました。 具体的には、専用のジョブ(playwright-version)でバージョンを取得します。その結果をplaywright-testsジョブに渡すことで、Docker Imageのバージョンを自動的に決定しています。 playwright-version: runs-on: ${{ vars.RUNNER_DEFAULT }} outputs: playwright-version: ${{ steps.playwright-version.outputs.playwright-version }} steps: - uses: actions/checkout@v4 - name: Get installed Playwright version id: playwright-version run: | echo "playwright-version=$(yq '.catalog.@playwright/test' ./pnpm-workspace.yaml)" >> "$GITHUB_OUTPUT" # Docker Imageを使ったPlaywrightのテストを実行するjob playwright-tests: needs: [playwright-version] container: image: mcr.microsoft.com/playwright:v${{ needs.playwright-version.outputs.playwright-version }}-noble options: --init --ipc=host --user=1001 steps: - uses: actions/checkout@v4 - name: Install dependencies run: pnpm install - name: Run Playwright tests run: pnpm playwright test このアプローチにより、Playwrightのバージョンアップ時にCI設定を変更する必要がなくなりました。 pnpm-workspace.yaml のcatalogを更新するだけで、すべてのアプリケーションのCIが自動的に新しいバージョンのDocker Imageを使用します。 Docker containerには --init 、 --ipc=host 、 --user=1001 のオプションを指定しています。各オプションの詳細は、Playwright Docker Documentationを参照してください。 各アプリケーションのCI設定は同じパターンに統一され、メンテナンスが容易になりました。Playwrightのブラウザバイナリとシステム依存関係のインストールにおけるタイムアウトやエラーがほぼ発生しなくなり、CI全体の成功率が向上しました。 テスト分
こんにちは。newmo 自動運転開発室のyui_tangです。 先日、自動運転開発室のオンボーディングと技術理解の共有を目的として、JetRacer を用いた社内ハッカソン合宿「ロボライダー」を開催しました。合宿の様子や背景は note に まとめています。 👉 note.com 本記事ではイベントレポートではなく、合宿で再現した開発サイクルと、実機を扱う際に 顕在化した課題を書き記します。 小さくしても、問題は小さくならない JetRacer は NVIDIA Jetson を搭載した小型の自律走行車プラットフォームです。カメラ 画像を入力としてニューラルネットワークが操舵角とスロットル値を推論し、その結果を もとに車両を制御します。 FaBo JetRacer 環境では PyTorch による回帰モデルが採用されており、画像入力から操舵角(steering)とスロットル値(throttle)という連続値を直接出力する構成になってい ます。分類ではなく回帰として制御量を学習する点が特徴です。 サイズは小さいものの、 センサー入力 教師データ作成 機械学習 推論最適化 実機制御 という、自動運転システムの最小構成が一通り含まれます。 今回作った自律走行ラジコンカー 今回の環境は FaBo Platform の JetRacer Docs をベースに構築しました。 https://faboplatform.github.io/JetracerDocs/ JetRacerのような小型自律走行車を用いた競技は国内外でも行われており、例えば以下が 参考になります。 minicar-autogp.org roboracer.ai 42tokyo.jp なぜ JetRacer を選んだのか 今回の合宿では、自動運転開発の全体像を短期間で体験できることを重視し、JetRacer を採用しました。 実車開発では、 車両準備 安全管理 実験環境確保 センサー調整 といった準備コストが大きく、オンボーディングや横断的な理解共有を目的とした短期イ ベントとして実施することは容易ではありません。 JetRacer は以下の点で適していました。 自動運転に必要な要素(認識・学習・制御)が一通り揃っている 室内環境で安全に実験できる セットアップ手順が公開されている 個人・チーム単位で再現可能なコストに収まる 参考: faboplatform.github.io まずは「走らせて失敗する」ところから始まる 合宿では interactive regression ワークフローに沿って開発を進めました。 faboplatform.github.io interactive regression は、人が操縦した走行データを教師データとして学習を行う模倣学習(Imitation Learning)の一種です。 基本的な流れは次の通りです。 走行して画像データを収集 不要データを整理 画像ごとに操舵角とスロットル値の教師データを付与 学習 推論走行 失敗区間を追加して再学習 教師データは画像フレームに操舵角・スロットル値を紐付ける形式で保存され、連続値回 帰として車両制御を学習します。 最初から正解モデルを作るのではなく、失敗を観察しながらデータを増やして改善してい きます。 また、学習後には Jetson 上での推論速度向上を目的として TensorRT 最適化を行いまし た。推論レイテンシは制御周期に直接影響するので、最適化の有無で走行の安定性が目に 見えて変わりました。 床の色を変えただけで、車は迷い始めた 今回のコースでは途中で床の色や素材が変化する区間を設けました。 これは学習時に取得された画像分布と実行時入力の分布が一致しないことで発生する問題 であり、interactive regression が繰り返し前提としている状況でもあります。 改善はモデル変更ではなく、該当区間の教師データ追加で進むケースが多く、データ中心 で開発が進む性質が、小さな車でもはっきり見えました。 ソフトウェアの前に、物理がある JetRacer 経験者のメンバーが事前にマウント部品を設計し、3Dプリンタで生成したパーツを全チーム共通で使用しました。 同時に、センサー固定位置や視野角のわずかな差が、収集されるデータ分布そのものに影 響することも共有されました。 合宿中に生まれた技術的なチャレンジ 標準的な interactive regression による開発だけでなく、各チームでは異なるアプロー チも試みられました。 視覚言語モデル(VLM)を用いた教師データ生成 走行画像に対する操舵ラベル付与を自動化する試みとして、VLM を利用した教師データ生 成が試されました。短期間で実用的な精度には至りませんでしたが、手作業によるラベリ ング負荷をどこまで削減できるかという観点での探索でした。 強化学習による走行方策の学習 教師データに依存しないアプローチとして、報酬関数に基づいて走行を学習させる強化学 習にも挑戦がありました。実機環境では試行コストが高く、短時間で安定した方策を得る ことの難しさが明確になりました。 2D LiDAR を用いた環境再構築 2D LiDAR によって取得したデータから occupancy map を生成し、仮想空間上での学習を 試みたチームもありました。実機走行とシミュレーションを往復する、いわゆる sim-to-real に近い発想です。 いずれも完成度を競うものではなく、「どこに改善の余地があるのか」を探る探索的な試 みとして行われました。 モデルは正しかったが、車は曲がらなかった 事前走行で良い結果が出ていたチームが、本番直前に期待通りの走行をしなくなる事象が 発生しました。 原因はラジコン内部のケーブルがシャフト付近に挟まり、操舵機構の動作を物理的に阻害 していたことでした。 ソフトウェア上では推論は正常に行われていましたが、ハードウェアが期待通り動作しな ければシステムとして成立しません。フィジカルAIでは、モデル・データ・機械的状態の どれが欠けても走れない。それを実感した場面でした。 同様の取り組みは比較的容易に再現できる JetRacer は教育・研究用途として広く利用されており、比較的容易に同様のイベントを実施できます。 NVIDIA JetRacer Developer Kit developer.nvidia.com 特別な実験施設を必要とせず、社内勉強会やオンボーディング、ワークショップにも向い ています。 今回の合宿では、プロダクト開発以外のメンバーも多く参加し、実際に手を動かしながら 自動運転開発の制約を体感できた点がよかったと思います。 小さな車で起きたことは、実車でも起きる JetRacer は 1/10 スケールの車両ですが、開発の過程で直面する問題の種類は実車とほとんど変わりませんでした。 モデルの精度を疑っていた問題が、配線の干渉によって引き起こされていたこともありま した。環境条件が少し変わるだけで挙動が崩れ、改善はアルゴリズムではなくデータ追加 で進むこともありました。 今回の合宿では、特別に新しい手法を導入したわけではありません。むしろ既に知られて いる手法を、短い周期で実機に適用し続けただけです。 それでも、実際に手を動かして開発サイクルを回してみると、自動運転開発がソフトウェ ア開発の延長線上には単純に置けないことがよく分かります。モデル、データ、環境、そ して物理的な状態が同時に影響し合うため、どこを改善すれば前進するのかは常に後から 分かる形になります。 今回の合宿で確認できたのは、新しい知識というよりも、自動運転開発が持っている問題 の構造そのものでした。小さな車両であっても、その構造は変わりません。 短期間の取り組みでしたが、同じ環境を共有しながら試行錯誤を繰り返したこと自体が、 チームにとっての共通言語になった手応えがあります。 🎉4月22日(水)newmo Autonomy Beer Bash (自動運転開発室meet up) 開催🎉 https://newmo-tech.connpass.com/event/390222/ newmo Autonomy初のイベントを開催します。 自動運転タクシー開発の関わるあらゆる技術領域に少しでも興味のある方、 newmo Autonomyの開発メンバーとお話しませんか? 上記URLからお気軽にご参加ください! 美味しい食べ物とドリンクをご用意してお待ちしています🍻 また、newmoの自動運転開発室は、チーム拡大の為採用を強化しております。 多数の職種で採用を行っておりますので、ご興味ある方はぜひご覧ください。 newmo.ai herp.careers
Claude Code のスキルが数十個に増えてきたのですが、全員に一律で適用されるのがつらくなってきたので、Plugin Marketplace を使ってオプトイン配布に移行しました。 スキルが増えると何が起きるか Claude Code のスキルは .claude/skills/ に配置すると、リポジトリを開いた全員に適用されます。数個なら問題ないのですが、数十個に増えてくるとスキルの description マッチングで意図しないスキルまで発火するようになってきました。QA 向けのスキルがバックエンドエンジニアの作業中に反応したり、フロントエンド向けのスキルがインフラの作業で発火したりといった具合です。 使わないスキルの description がコンテキストウィンドウに載り続けてトークンを消費するのも気になっていました。改めてスキルの棚卸しをしたところ、全員に必須と言えるものは半分以下で、残りは「あると便利だが、全員には要らない」ものでした。 Plugin と Marketplace Claude Code には Plugin と Marketplace という2つのネイティブ機能があります。 Plugin は skills、hooks、agents をひとつのパッケージにまとめる仕組みです。Marketplace はその Plugin のカタログで、source の種類によって配布方法を選べます。 Marketplace の source は3種類あります。 directory はローカルディレクトリを指定する方法です。外部に公開する必要がなく、そのリポジトリ限定の Marketplace を作れます。リポジトリを clone するだけで使える状態になるので、社内のモノレポで使うなら一番手軽です。 github は GitHub リポジトリを owner/repo 形式で指定します。Marketplace を独立したリポジトリとして管理できるので、組織をまたいだ配布に向いています。 url は Git の URL を直接指定する方法で、GitLab やセルフホストの Git サーバーなど GitHub 以外のホスティングに対応できます。 今回は directory を採用しました。モノレポなので同じリポジトリを見ている人が多く、clone するだけで Marketplace が使える状態になります。 この仕組みで「チーム向け」「QA 向け」のようにロール別のスキルパックを作り、各自が必要なものだけインストールする運用が可能になります。 リポジトリ内に Marketplace を作る .ai/marketplace/ ├── .claude-plugin/ │ └── marketplace.json └── plugins/ └── my-team/ ├── .claude-plugin/ │ └── plugin.json └── skills/ └── some-skill/ └── SKILL.md marketplace.json でカタログ全体を定義しています。 { "name": "my-org", "plugins": [ { "name": "my-team", "source": "./plugins/my-team", "description": "チーム向けスキルパック", "version": "0.1.0", "strict": false } ] } 社内向けであれば plugin.json は名前だけで十分です。strict: false を marketplace.json 側で指定しておけば、メタデータはカタログに一元管理されます。 { "name": "my-team" } あとはリポジトリの .claude/settings.json に Marketplace を登録しておきます。この設定がリポジトリに含まれているので、他のメンバーは pull するだけで Marketplace が利用可能になり、すぐに Plugin をインストールできる状態になります。 { "extraKnownMarketplaces": { "my-org": { "source": { "source": "directory", "path": "./.ai/marketplace" } } } } インストール /plugin で一覧を確認して、必要なものをインストールします。 /plugin install my-team@my-org 不要になったらアンインストールするだけです。 /plugin uninstall my-team@my-org Namespace の設計 Plugin 名がスキルの namespace になるため、ディレクトリ名に my-team- のような prefix を付ける必要はありません。 ただし、スキルを呼び出すときの補完候補には namespace を除いた名前が表示されます。既存スキルと名前が被ると区別しにくいので、SKILL.md の name フィールドで明示的に namespace を含めておくのがおすすめです。 # skills/prd-guide/SKILL.md name: my-team:prd-guide こうしておくと、補完時に my-team:prd-guide と表示され、どの Plugin のスキルか一目で分かります。 やってみての所感 まだ移行を始めたばかりなので、誤発火やコンテキスト汚染が実際に減ったかはまだ分かりません。ただ、「全員に適用されるのはちょっと……」と追加を迷っていたスキルを Plugin に気軽に足せるようになったのは良い変化でした。 ちなみに、Claude Code 以外の Agent を使う人のために、1つのソースから各 Agent 向けの設定を自動生成する仕組みを作っていました。Marketplace は Claude Code 特有の機能なので、ここだけポータビリティが落ちているのが気になっています。 どうやら Codex にも最近入った同様の Plugin Marketplace の仕組みがあり、1つのソースから両方のプラグインを生成できるようにしたいなと企んでいます。
はじめに newmo では BI ツールとして Lightdash を導入しました。この記事はなぜ BI as Code をやるのか、その背景にある思いを書いています。ツールの使い方や導入手順の紹介ではありません。 GUI ベースの BI ツールで運用してきた中で感じた課題、データエンジニアリングにおけるソフトウェアエンジニアリングの原則の重要性、そして AI 時代における「確かな数字」の必要性について、newmo のデータ基盤担当者の視点からお伝えします。 「数字、合ってる?」という不安 newmo ではKPI や経営レポートなど、重要な数字を GUI ベースの BI ツールで可視化・共有してきました。最初はそれで十分でした。事業が拡大するにつれて、ある種の不安が静かに広がり始めました。 「この数字、合ってる?」 ダッシュボードの数字を見て、そう感じたことがある人は少なくないと思います。newmo でもまさにその状態でした。 複数のレポートが存在し、それぞれが微妙に異なるロジックで同じ指標を集計している。どれが正しいのかわからない。あるレポートでは売上を二重にカウントしていたことが後から発覚したこともありました。 GUI ベースの BI ツールには構造的な問題があります。 変更管理ができない — いつ誰が何を変えたか追えない 壊れやすい — データソースやカラム名の変更でダッシュボードが壊れ、原因特定に時間がかかる レビューできない — 変更に対して PR レビューする手段がない 属人化する — 作った人にしかロジックがわからない ダッシュボードの数字がサイロ化し、微妙にロジックが違う。それは単なる技術的な不便さではなく、意思決定の土台が揺らいでいるということです。経営会議でも KPI 可視化の改善が課題として認識されるようになりました。 BI 周りの運用が特定の人に依存している状況を、いずれは是正しなければいけない。そういう危機感がずっとありました。 そしていま、状況はさらに複雑になっています。AI に聞けばアドホックに集計してそれらしい数字を出してくれる時代です。誰でも「それっぽい数字」を手に入れられます。管理されたダッシュボードの数字と、AI がその場で出した数字。どちらが正しいのか判断できないという新たな混乱が生まれつつあります。 「この数字が正しい」と胸を張って言える、道標のような確かな数字。それがいま、これまで以上に求められています。 データエンジニアリングもソフトウェアエンジニアリングである ここで一歩引いて考えたいのは、データエンジニアリングやアナリティクスエンジニアリングは、ソフトウェアエンジニアリングの一領域であるということです。 ソフトウェアエンジニアリングでは当たり前とされていることがあります。 コードはバージョン管理する 変更にはレビューを通す テストを書く CI/CD でデプロイする これらはソフトウェアの品質と信頼性を担保するための基本的なプラクティスです。 では、BI やダッシュボードはどうか。 GUI でポチポチしてダッシュボードを作る行為は、構造的に言えば 本番サーバーに SSH して直接コードを書き換えているのと同じ です。変更履歴はない。レビューもない。テストもない。壊れたら作った人に聞くしかない。 データエンジニアリングだからといって、この基本を省略していい理由はありません。dbt が SQL のパイプラインにソフトウェアエンジニアリングのプラクティスを持ち込んだように、BI の領域にも同じことが必要です。 BI as Code — ダッシュボードの定義をコードで管理し、PR レビューと CI/CD のフローに載せる。それは新しい概念ではなく、ソフトウェアエンジニアリングの基本に立ち返るということに他なりません。 AI 時代に GUI ポチポチは持続しない もう1つ、避けて通れないことがあります。 AI がコードを書き、PR を出し、レビューする。それはもう未来の話ではありません。 では、BI の世界はどうか。 GUI に閉じた定義では、バージョン管理・レビュー・CI/CD、そして AI の活用に載せられません。コードで定義されて初めて、そのすべてが可能になります。 逆に、メトリクスの定義が dbt の YAML にコードとして書かれていればどうでしょう。AI はその定義を理解し、新しいダッシュボードの提案や既存定義との整合性チェックが可能になります。 セマンティックレイヤーを整備し、指標の定義をコードで管理することは、人間のためだけではなく、AI と協働するための基盤づくりでもあるのです。 「定義はコードで、利用は自由に」 BI as Code を目指す中で、いくつかのツールを検討しました。可視化までコードで完結するアプローチもありますが、非エンジニアが置き去りになります。 私たちがたどり着いたのは 「定義はコードで管理し、利用は GUI で自由に」 という棲み分けの思想です。 データチーム がメトリクスの定義(KPI の計算式、ディメンション)を dbt YAML に書く。Git で管理し、PR レビューを経て CI/CD で自動デプロイする ダッシュボード利用者 は、定義済みのメトリクスを GUI で組み合わせてダッシュボードを作る この思想にもっとも合致したのが Lightdash でした。 Lightdash は dbt プロジェクトにネイティブ接続する OSS の BI ツールです。MIT ライセンスで、セルフホストが可能。dbt のモデル YAML にメトリクスを定義するだけで、Lightdash 上で探索・可視化ができます。さらに Dashboards as Code として、ダッシュボードやチャートの定義を YAML でエクスポート・インポートし、Git 管理できます。 newmo では Lightdash を Cloud Run 上にセルフホストし、既存の dbt + BigQuery パイプラインに接続しています。重要なダッシュボードは YAML でコード管理し、PR レビューを経て main マージで自動デプロイされます。 数字に責任を持つということ Lightdash の導入は、ツールの入れ替えではありません。数字に対する責任の持ち方を変えるということです。 newmo では、Lightdash と既存の BI ツールを以下のように棲み分けていく予定です。 newmo のデータパイプライン Lightdash — 重要な KPI・経営レポート。作成メンバーは限られ、厳密なレビューを経て公開される「保証された数字」 既存の BI ツール — アドホックな分析・一時的な可視化。各自の責任で自由に使う。ただし数字はあくまで「参考値」 「保証された数字」と「参考値」を明確に分けること。これが、サイロ化を防ぎ、意思決定の土台を守るための仕組みです。 そして、保証するためには仕組みが必要です。 メトリクスの定義は dbt YAML で一元管理し、Git でバージョン管理する ダッシュボードの変更は PR レビューを経る lightdash preview と lightdash validate でダッシュボードの破損を CI で自動検知する CI/CD で自動デプロイし、人手による反映ミスを防ぐ これが、「保証された数字」を実現するための具体的な仕組みです。 これから Lightdash の導入はまだ始まったばかりです。最初のダッシュボードとしてタクシー営業レポートを移行し、Slack への日次配信も動き始めたところです。 AI に聞けばアドホックに集計してそれらしい数字を出してくれる時代です。誰でも「それっぽい数字」を手に入れられます。だからこそ、「これが正しい」と胸を張って言える道標のような数字が必要になります。レビューを経て、定義が透明で、再現性のある数字。それが組織の意思決定を支える土台になると思っています。 過去の失敗があるからこそ、今度はソフトウェアエンジニアリングの原則に則って進めます。一人で抱え込まず、チームで Enabling していく。 まだ道半ばですが、「この数字、合ってる?」という不安を、「この数字は合っている」という確信に変えていきます。 書いた人: ota2000
newmoでソフトウェアエンジニアをしているはるふ(@_ha1f)です。 今はAI配車チームにいますが、昨年の今頃は、ライドシェアのドライバー向けのiOSアプリを開発しておりました。 ライドシェアドライバーアプリは、ドライバーが乗務を開始して、配車依頼を受けて、お客さんを迎えに行って送り届ける。そのフローを支えるアプリです。 主としてTCA(The Composable Architecture)とSwiftUIで構築し、Google Maps Navigation SDKでリアルタイムのナビゲーションを行っていました。 タクシーの車載用のアプリも並行して作っており、バックエンドは多くの部分を共有しています。 2024年12月半ばにチームが発足し、2025年の1月末には実車テスト、2月にはTestFlightで実際にドライバーに使っていただくというとんでもないスケジュールでした。 開発速度はもちろん、クオリティも妥協せずにリリースできたのは素晴らしいチームに恵まれていたと感じます。 一方で、会社全体として、ライドシェアを巡る事業環境の変化・注力分野の変更など紆余曲折があり、 結果的に残念ながらこのアプリは一般向けにリリースすることはなく、チームも解散となりました。 (※事業的な話については、この記事などをご覧ください) ただ先述の通りUIもかなりこだわって作っており、供養もかねてテックブログで紹介します。 以降では、このアプリのこだわりの一部を、実装とともに紹介していきます。 ※ アニメーションの実装は自分自身のSwiftUIのリハビリを含めて色々なバリエーションで作っており、最適なものを選んでいるとは限りません 配車受付状態を切り替えるバー ほなーくんと走るホーム画面 アプリを開いて最初に目にするのがホーム画面です。ここにはマスコットキャラクターのほなーくんが車に乗って走っているアニメーションを置きました。 最初に目にする画面なので、待っている間にちょっとワクワクしてもらいたくてデザイナーさんと協力して作りました。 構成はシンプルで、道路、ビル群、雲、車体、ほなーくんの手、前景の草などをZStackで重ねているだけです。 ZStack(alignment: .bottom) { Image(.road) _InfiniteScrollView { Image(.buildings) } Image(.cloud) Image(.tire) Image(.carBody) .offset(y: carYOffset) Image(.honaaHand) Image(.grassFront) } 昔の2Dゲームのスクロールのように、背景のビル群だけが横に流れ、車体はその場で揺れています。 背景の無限スクロールはTimelineView(.animation)で実装しています。現在時刻から位相を計算して、オフセットを決めるというアプローチです。 TimelineView(.animation) { context in let offsetX = getTimelineValue( startValue: -contentSize.width, endValue: 0, date: context.date, duration: animationDuration ) HStack(spacing: 0) { ForEach(0..<3, id: \.self) { _ in content() } } .offset(x: offsetX) } 同じ画像を3枚並べてオフセットをずらしていくことで、切れ目のないループになります。値の計算は、時刻を周期で割った余りから線形補間するだけです。 let phase = (interval - floor(interval / duration) * duration) / duration return (endValue - startValue) * phase + startValue 車体の上下の揺れはもっとシンプルで、repeatForeverで動かしています。 敢えて独立にしてリアルさを出してみました。 withAnimation( .linear(duration: 0.2) .delay(0.1) .repeatForever(autoreverses: true) ) { carYOffset = -2 } デザイナーさんと密に連携し、空白セルで画像のサイズを揃えてもらったり、レイヤをうまいこと分割・合成いただいたことでコードがシンプルになりました。 腕が車とわかれて動いているのがポイントだそうです。 配車依頼のカウントダウン 乗務を開始すると、配車依頼が届きます。ドライバーには受諾するまでの制限時間が円形のプログレスで表示されます。 countdown Circle の trim で円弧を描く 円形プログレスの本体は Circle の trim で描いています。 struct CircularIndicator: View { let progress: CGFloat let lineWidth: CGFloat var body: some View { ZStack { Circle() .stroke(Color(.secondarySystemBackground), lineWidth: lineWidth) Circle() .trim(from: 0, to: progress) .stroke(.tint, style: .init(lineWidth: lineWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) } } } 背景に薄い円、前景に trim で切り取った円を重ねています。Circle の描画は3時方向から始まるので、.rotationEffect(.degrees(-90)) で12時方向に回しています。 これを ProgressViewStyle に準拠させて、標準の ProgressView で使えるようにしています。スタイルの差し替えがしやすくなるほか、アクセシビリティ対応が自動で付くメリットもあります。 struct CircularBarProgressStyle: ProgressViewStyle { let lineWidth: CGFloat func makeBody(configuration: Configuration) -> some View { CircularIndicator( progress: configuration.fractionCompleted ?? 0, lineWidth: lineWidth ) } } TimelineView で滑らかに減らす カウントダウンのアニメーションは TimelineView(.animation) で駆動しています。毎フレーム ctx.date が更新されるので、それに合わせてフレームを計算します。 TimelineView(.animation) { ctx in ZStack(alignment: .center) { ProgressView(value: store.state.calcProgress(now: ctx.date), total: 1) .progressViewStyle(CircularBarProgressStyle(lineWidth: 6)) Text(verbatim: String(store.state.calcRemainingSeconds(now: ctx.date))) .animation(.none) // 数字はフェードさせない } } calcProgress はなめらかに、一方で残り秒数は .rounded(.up) で切り上げています。 func calcProgress(now: Date) -> CGFloat { let interval = deadline.timeIntervalSince(countDownStartAt) guard interval > 0 else { return 0 } let elapsed = now.timeIntervalSince(countDownStartAt
こんにちは、newmo の伊藤です。 この記事では、筆者が取り入れている「Claude Code を Deep Research の用途で使うための工夫」について、自作している Skill や端末間での共有方法、Entire の活用事例などを交えて紹介します。 既存のプロダクトが提供する Deep Research 機能に感じていた課題 筆者はこれまで様々なリサーチの用途に ChatGPT や Gemini が提供する Deep Research 機能を利用してきました。これらのプロダクトが提供する Deep Research 機能はとても便利なのですが、以下のような課題も感じていました: 個々のプロダクトが提供しているインターフェース(ウェブやモバイルアプリ)を介するのではなく、普段利用しているターミナルやエディターでリサーチを完結させたい モバイル端末では普段利用している Markdown ビューワー(Obsidian)でリサーチの結果を確認したい リサーチした結果をローカル環境の Markdown ファイルとして直接扱って Git で管理したい Agentic Coding ツールのセッションを管理するために自作しているツールをリサーチの用途でも活用したい 特に、Claude Code をはじめとした Agentic Coding ツールを活用するにつれ、リサーチも Agentic Coding ツールで完結させたいという思いが強くなってきました。 そこで、Claude Code を Deep Research の用途で活用するための環境を整えることにしました。 Claude Code の Deep Research 用 Skill Claude Code を Deep Research の用途で使うために、「必要に応じて Claude Code をリサーチモードに切り替える」ための /mode-researcher という Skill を自作しています。筆者はこのような「必要に応じて Claude Code を特定のモードに切り替えるための Skill」を複数作って活用しており、リサーチの用途以外にも例えば「実装する前にモジュール間のインターフェースやテストを設計するためのモードに切り替える」ための /mode-architect のような Skill も作っています。これらの Skill は、実際になんらかのアクションを起こすための Task content ではなく、Claude Code にナレッジを提供するための Reference content に近い形で定義しています。 実際に /mode-researcher Skill を利用してリサーチを行っている様子は以下のキャプチャのようになっています: /mode-researcher Skill を起動した後、リサーチしたいトピックを入力すると、Claude Code がリサーチのためにいくつかの質問を選択形式で利用者に投げかけてきます。これらの質問に答えると、Claude Code がリサーチを開始して、リサーチしたいトピックにも依りますが概ね 10 分前後でリサーチの結果を Markdown ファイルとして出力してくれます。 /mode-researcher Skill は引数として「リサーチ結果を書き出すための Markdown ファイル」のパスを受け取るようにしています。この引数は、筆者が自身でリサーチ結果を保存するファイル名やディレクトリを指定したいので設けていますが、引数を設けずに Claude Code に自律的にファイル名やディレクトリを決めさせるようにしても良いと思います。 この Skill は Claude Code の利用者側で明示的に起動するためのもので、Claude Code 側で自律的に起動することを想定していません。そのため、SKILL.md の Frontmatter で disable-model-invocation: true を指定して、Claude Code が自律的にこの Skill を起動することを防いでいます。 また、筆者は Claude Code を利用するときは基本的に Plan Mode を利用していますが、この /mode-researcher Skill を利用するときはリサーチ内容を利用者の確認を介することなく Markdown ファイルに出力してほしいので、Plan Mode はあえて利用しないようにしています。 実際の SKILL.md の内容は以下のようになっています: --- name: mode-researcher description: Deep research agent that autonomously searches, analyzes, and synthesizes information into a structured report with citations. disable-model-invocation: true --- This guide defines the workflow for conducting deep, multi-pass research and producing a structured, well-cited report. ## Phase 1: Wait for User Input - Wait for the user to provide the research topic or question right after this skill is invoked. - Do not skip this phase by inferring the topics from the given arguments or filename, instead, must explicitly wait for the user to provide the research topic or question as input. ## Phase 2: Clarify Scope - Use the `AskUserQuestion` tool to ask the user about: - The research topic or question. - Scope, depth, and constraints (time period, domains, perspectives). - Expected output format (length, structure). ## Phase 3: Research Plan - Decompose the topic into 3–8 sub-questions. - ... ## Phase 4: Iterative Search and Analysis - For each sub-question, perform multiple `WebSearch` queries with varied phrasing. - Use `WebFetch` to read full pages, not just search snippets. - Aim for 20–50 search queries total across the session. - Cross-validate claims across multiple independent sources; flag contradictions. - Refine the search strategy as knowledge gaps emerge. - Track every source URL and the claims it supports. - For code-related research, also use GitHub MCP tools (`search_code`, `search_repositories`, `get_file_contents`). - ... ## Phase 5: Synthesis and Report - The output file path should be `$ARGUMENTS`. - Write the report to the specified file with the following structure: - Title - Executive Summary - Table of Contents - Main Sections (with inline citations as HTML anchor tags, e.g. `<a href="URL" target="_blank">source title</a>`) - Conclusions - Sources (list of all referenced URLs with their titles) - Present a brief summary inline after writing the report. - ... ## Principles - Prioritize primary sources over secondary summaries. - Distinguish facts, expert opinions, and speculation. - When sources conflict, present both sides. - Never fabricate sources — every citation must correspond to a real URL that was fetched. - Prefer recent sources unless historical context is relevant. - Use parallel `WebSearch` / `WebFetch` calls for efficiency. - Do not stop after finding one answer — seek corroboration and alternatives. - ... ### Editorial Guidelines - Use HTML anchor tags with target="_blank" for all links (e.g. <a href="URL" target="_blank">text</a>) so they open in a new tab in web browsers. - ... まずは Phase 1: Wait for User Input で、利用者がリサーチしたいトピックを入力する前に Claude Code がリサーチを開始してしまうのを抑制しています。 次に Phase 2: Clarify Scope で AskUserQuestion ツールを利用して、リサーチに関するスコープや深さ、制約条件(時間軸やドメイン、視点など)、期待するアウトプットの形式(長さや構成など)などについて、Claude Code がリサーチを開始する前に利用者に質問するように指定しています。 AskUserQuestion ツールは Claude Code が内包するツールのひとつで、Claude Code が利用者に質問を投げかけて、利用者が選択肢の中から回答を選ぶ形式で入力を受け取るためのツールです。このツールを利用することで、Claude Code が自律的にリサーチの意図を汲んで選択形式で質問を投げかけてくれるため、利用者側の入力の手間を軽減することができています。 Phase 3・4・5 ではリサーチ手順や MCP の活用、アウトプットの形式などを指定しています。 Principles では、「一次情報を尊重する」といったようなリサーチの原則や方針、及び Markdown の書き方に関する細かなガイドラインなどを指定しています。 これらの内容、特に Phase 3・4・5 の内容は、現在も調整を重ねながら運用していますが、概ね筆者が求める使用感・リサーチ内容に近づいてきていると感じています。 端末間でのリサーチの共有 Claude Code は PC 端末で起動していますが、リサーチ結果は外出中にモバイル端末で確認したいことも多いです。筆者はモバイル端末で Markdown ファイルを扱うために Obsidian を利用しているため、リサーチ結果のファイルは Obsidian がアクセスできるクラウドストレージ(筆者の場合は iCloud)上に共有されるように設定しています。 また、筆者は開発環境用に Linux サーバーを構築し、ノート PC などから Tailscale と SSH 経由で Linux サーバーにアクセスして作業する開発スタイルを取っており、Claude Code もこのサーバー上で利用しています。この Linux サーバーにモバイル端末からもアクセスできるように設定して、外出先でもモバイル端末から Linux サーバーにアクセスして Claude Code を用いたリサーチを開始できるようにしています。 モバイル端末(iPhone)から Linux サーバーを操作するためのターミナルアプリは Termius や libghostty ベースで最近話題になっている Echo をはじめとしていろいろなアプリを試していますが、「Control キーを押下したまま他のキーを用いた操作が行いやすい」という観点で Blink Shell というソースが公開されているターミナルアプリを利用しています。 Entire の活用 リサーチ結果について、リサーチ内容を更新するときなど稀に「どのような入力(プロンプト)によってこのリサーチが出力されたのか」という観点も含めて反芻したくなることがあります。 これを実現するために、先日公開された Entire というプラットフォームを試験的に導入しています。 Entire は Agent と人間が協調して開発を進めるためのプラットフォームを目指しており、現在は CLI を用いて Agent とのやりとり(セッション)を記録して、記録したやりとりを可視化するためのウェブサービスを提供しています。 Entire の画面は以下のようになっています: <figure cl
こんにちは、newmo の 伊藤 です。 この記事では、筆者が特に工夫して設定しているテキストエディタのカスタムショートカットについて、周辺ツールの利用例を交えて紹介します。 記事で紹介する内容は、特定のテキストエディタには依存しないものとなっているので、アイディアや考え方はどのテキストエディタを使っている方にも参考にしていただけると思います。 目次 目次 はじめに ファイルの列挙 → 絞り込み → 開く ファイルの全文検索 → 絞り込み → 開く Language Server Protocol(LSP)関連 編集中のファイルが属しているディレクトリ内のシンボルの列挙 → 絞り込み → 開く 参照元の列挙 → 開く 関数の呼び出し元の列挙 → 開く Git 関連 差分の閲覧 差分のハイライト Git クライアント(tig)の起動 プロジェクトの切り替え シェルの起動 EasyMotion 形式でのカーソルの移動 おわりに はじめに 筆者はテキストエディタに様々な設定を施すにあたり、 必要な情報・機能を、必要な時にのみ素早く表示・起動できるようにする という点を常に心がけています。そのため、テキストエディタ内でデフォルトで表示されている情報は必要最小限となるように努めています。 この考えを踏まえて、筆者のテキストエディタは下の画像のようになっています。 ソースコード以外の「常に表示する必要最小限の情報」は次のように定めています: テキストエディタで開いているファイルの名前 編集中のファイルの可視化(ファイル名のハイライトを反転) 未保存のファイルの可視化(ファイル名横の「+」マーク) プロジェクト名 Git リポジトリでチェックアウトしているブランチの名前 Git リポジトリのステータス(リポジトリにコミットしていない変更があれば「X」マークを表示する) これらの必要最小限の情報が、テキストエディタの最上部の 1 行に全て収まるように設定しています。 その他の情報や機能は、この記事で紹介するようなショートカットを用いて、必要なときに素早く呼び出せるように設定しています。 ファイルの列挙 → 絞り込み → 開く ,f というキーシーケンスのショートカットで、リポジトリ内のファイルを列挙して、ファイル名の部分一致検索で絞り込んで開いています。 列挙するファイルの条件は次のように定めています: Git の管理下にあるファイル Untracked なファイルも含む Ignore されているファイルは含まない Deleted なファイルは含まない Submodule のパスは含まない この条件を満たすために、次のコマンドでファイルの列挙を行っています: $ git ls-files --cached --others --exclude-standard | grep -Fvxf <(git ls-files --deleted; git config --file .gitmodules --get-regexp path | awk '{print $2}') | sort このコマンドでは git ls-files で Git の管理下にあるファイルを Untracked なものも含めて列挙し、続く grep -Fvxf コマンドで Deleted なファイルと Submodule のパスを篩い落としています(もっとシンプルな方法をご存じの方がいたら教えてほしいです 🙏)。 このコマンドで列挙したファイルを fzf というコマンドラインのファジーファインダーツールに渡して、インタラクティブにファイル名の部分一致検索を行い、選択したファイルを開くようにしています。 また、fzf が提供する「絞り込み中のファイルの内容をプレビュー表示する機能」を活用して、内容を確認しながら開くファイルを選択することもあります。 プレビュー内では <Control>-<Shift>-p・<Control>-<Shift>-n のショートカットを用いて上・下にスクロールできるようにしています。 fzf のプレビュー画面では、 bat のようなコマンドラインのシンタックスハイライトツールを利用して選択中のファイルのシンタックスをハイライトすることができます。筆者は「テキストエディタで開いたときのハイライト」と「fzf のプレビューで開いたときのハイライト」を完全に一致させるために、bat ではなく tree-sitter ベースでシンタックスをハイライトする自作のツールを fzf と連携させています。 ファイルの全文検索 → 絞り込み → 開く ,g というキーシーケンスのショートカットで、リポジトリ内の全てのファイルに対して全文検索を実行して、内容を元に絞り込んで目的の箇所を開いています。 全文検索には ripgrep というコマンドラインの検索ツールを利用しています。この ripgrep と fzf を組み合わせて、リポジトリ内の全てのファイルに対して全文検索を実行し、目的の箇所を絞り込みながら開いています。 当該の箇所を開くときにカーソル位置を視認しやすくするために、開いた直後にカーソル行が点滅するようにしています。 また、fzf の「複数選択機能( --multi オプション)」を使って、複数の検索結果を <Tab> で選択して同時に複数のファイルを開けるようにもしています。 Language Server Protocol(LSP)関連 編集中のファイルが属しているディレクトリ内のシンボルの列挙 → 絞り込み → 開く gl というキーシーケンスのショートカットで、編集中のファイルが属しているディレクトリ内に含まれるシンボルを全て列挙して、絞り込んで開いています。 例えば Go 言語における「パッケージ」のように、ソースコードのファイルが有意味な単位でディレクトリにまとめられることは多いです。そのため筆者は「編集中のファイルを含んでいるディレクトリに、他にはどのような関数・変数が含まれているのか」を確認したくなることが多々あります。 現在(バージョン 3.18)の LSP では「単一ファイル内のシンボルの列挙(textDocument/documentSymbol)」や「ワークスペース全体のシンボルの列挙(workspaceSymbol/resolve)」はサポートされていますが、「ディレクトリ内のシンボルの列挙」はサポートされていません。 そのため、 編集中のファイルが属しているディレクトリ内のファイルを列挙する 列挙したファイルの中で編集中のファイルと同じファイルタイプのものを列挙する 2 で列挙したそれぞれのファイルに対して LSP の textDocument/documentSymbol を実行する 3 の結果を合成して fzf に渡して絞り込めるようにする という設定をテキストエディタに施して、ディレクトリ内のシンボルを列挙できるようにしています。 参照元の列挙 → 開く gkr というキーシーケンスのショートカットで、カーソル位置にあるシンボルに対する参照元を LSP の textDocument/references を用いて列挙し、目的の箇所を開いています。 列挙した参照元は、ファイルの列挙や全文検索と同様に、fzf に渡して内容を適宜プレビューで確認しつつ開くようにしています。 関数の呼び出し元の列挙 → 開く gy というキーシーケンスのショートカットで、カーソル位置にある関数に対する呼び出し元を LSP の callHierarchy/incomingCalls を用いて列挙し、目的の箇所を開いています。 呼び出し元が 1 箇所しか存在しない場合は、列挙せずに当該の箇所を直接開くように調整しています。 Git 関連 差分の閲覧 ,m というキーシーケンスのショートカットで、「現在のブランチのワーキングディレクトリ及びインデックス」と「ベースブランチ(デフォルトブランチ)」とのマージベースからの差分を表示しています。 このショートカットでは、画面を以下のように 3 つに分割しています: 上: 差分のあるファイル一覧 左: ベースブランチの内容 右: 現在のブランチの内容 ショートカットを起動すると上の画面のファイル一覧にフォーカスされ、そこからファイルを選択して d を入力すると右の画面の現在のブランチの状態にフォーカスされます。さらに、右の画面内で e を入力すると当該箇所を改めてテキストエディタで開くようにしています。 他人や AI コーディングエージェントが書いたコードは、この記事で紹介しているようなテキストエディタの機能を用いてレビューできると効率が良いので、この差分閲覧のショートカットを活用して手元の環境で差分を読んでいます。また、自分が今書いているコードを「レビューに出すとどのような差分になるか」という観点で反芻することが多いので、自分が書いたコードを読むためにも活用しています。 筆者は、自分が書いたコードの差分を表示する際に「まだ Git で管理していない Untracked な新規ファイルについては、作業中の未確定な状態なので git add することなく(Git のインデックスに影響を及ぼすことなく)差分だけを確認したい」と考えています。 これを達成するために、このショートカットが起動されたときに一時的な Git のインデックスファイルを作成(cp .git/index .git/tmp-index)し、この一時的なインデックスファイルを GIT_INDEX_FILE 環境変数で指定して git add と差分表示を実行することで、作業中の Git のインデックスに影響を及ぼすことなく差分だけを確認できるようにしています。 差分のハイライト ,x というキーシーケンスのショートカットで、編集中のファイルに対して Git での差分を可視化しています。必要なときにだけ表示できるように、差分のハイライトの表示は ,x のショートカットでトグルできるように設定しています。 このショートカットでも「差分の閲覧」で紹介した内容と同様に、一時的なインデックスファイルを作成して差分だけを確認できるようにしています。 このショートカットと「差分の閲覧」で紹介したショートカットを併用して、下のキャプチャのようにコードレビューに勤しんでいます。 Git クライアント(tig)の起動 ,t というキーシーケンスのショートカットで、TUI ベースの Git クライアントである tig をテキストエディタ内で起動しています。tig は git の操作をコマンドラインでよりインタラクティブに実行できるツールです。 筆者は、Git のインデックスの操作を主に tig で実行しています。上のキャプチャでも実行しているように、ファイルの中の一部の差分をインデックスに追加する git add --patch 相当の操作をよりインタラクティブに実行できる、など tig によって細やかな操作の利便性が向上しています。 インデックスの操作の他にも、コミットの作成やコミット内容の表示、ブランチのプッシュなど、tig 自体の設定ファイルによる拡張も駆使して多くの Git の操作を tig 経由で実行しています。 プロジェクトの切り替え ,s というキーシーケンスのショートカットで、コンピューター上にあるプロジェクト(リポジトリ)を列挙し、切り替えたいプロジェクトを名前で絞り込んで開いています。 作業中に複数プロジェクトを扱う必要がある場合、tmux やターミナルで画面を分割して複数画面で開く、という方法も存在しますが、筆者は「認知的負荷を下げるために同時に表示される情報は最小限にしたい」という考えのもと単一の画面で素早くプロジェクトを切り替えられるようにしています。これによって視線や首を極力移動することなく複数プロジェクトを扱える、というメリットもあります。 他のプロジェクトに切り替える際は、現在編集中のプロジェクトで開いているファイルやカーソルの位置をテキストエディタの機能を使ってセッションとして保存
newmoでソフトウェアエンジニアとして働いている @nozomuikuta です。この記事では、12月17日に投稿された「タクシーの給与計算のDX」の記事に関連して、あなたがタクシーの乗客として支払った料金が、タクシー乗務員に給料として支払われるまでの全体像を、特にnewmoグループである株式会社未来都を例に説明します。 tech.newmo.me 売上の発生 みなさんがタクシーに乗車する際、料金の支払い方には大きく分けて2つの方法があります。 ひとつは「現金支払い」で、その名のとおり、車内で現金により支払いをおこなう方法です。もうひとつは「未収」と呼ばれる現金支払い以外の方法であり、車載決済端末や配車アプリの操作によるクレジットカードや電子マネー決済、またはタクシーチケットの利用など、その場で現金のやりとりをしない方法です。 入庫と納金 タクシー乗務員は、乗務を終えて営業所に戻る(入庫)と納金業務をおこないます。これは、乗務中に発生した売上から、未収の売上およびその他会社の規則にもとづいた金額を差し引き、残った現金を一度会社に納める業務です。このとき、車載のメーターにタクシー乗務員が記録した決済情報および受け取ったタクシーチケットなどを営業所内勤者に手渡します。なお、車内におけるメーター操作の間違い(例:二重決済)や、納金機の仕様により乗務員に紐づかない納金が可能であることなどから、完全な自動化および金額の整合性をとることは、この時点でおこなうことができません。そのため、後述の本社における事務処理が必要になります。 各営業所の締めと本社への引き継ぎ newmoグループの株式会社未来都では、すべての乗務員の入庫と納金が完了すると、各営業所が納金機の締め作業をおこないます。締め作業の詳細は省略しますが、締め作業後に、各営業所長が決済情報およびチケットなどを乗務員ごとにまとめた書類を毎日未来都本社に送付しています。 本社チケットセンターによる事務処理 未来都本社にはチケットセンターという名前の部署があります。この部署では、日々送られてくる乗務員ごとのメーターに記録された決済情報とタクシーチケット、そして車載決済端末や配車アプリで決済された情報を突合して、各タクシー乗務員の歩合計算のもとになる金額を決定します。タクシー乗務員を増やし、彼らに売上を立ててもらう必要があるタクシー会社にとって、この突合処理がスケールするかどうかは会社として重要な指標であり、「タクシーの給与計算のDX」の記事で扱ったシステムでは、突合や金額差異の自動判定をおこなう機能を提供して、部署の省人化と事務処理の高速化を目指しています。 給与計算と支給 給与締め期間のすべての日において、すべてのタクシー乗務員の売上を確定させることができると、いよいよタクシー乗務員の給与を計算することができます。newmoでは、グループ会社各社の規則にもとづいて給与を計算して、外部金融系SaaSにインポートするためのCSVファイルを生成する部分を自動化しています。 歩合給を計算したあとは、控除などを外部SaaSで計算して、タクシー乗務員の口座に振り込みがおこなわれます。 この記事では、あなたが支払ったタクシー料金がタクシー乗務員の給料になるまでの流れを説明しました。タクシー会社の売上は、タクシー乗務員にいかに多くの売上を立ててもらうかにかかっているため、乗務員の採用数がひとつの重要指標となります。そして、増え続ける乗務員に耐えうる事務処理の実現がタクシーDXの鍵となるのです。newmoでは、このようなタクシー会社の成功の根幹にかかわるプロダクトを一緒につくってくださるプロダクトマネージャーを募集中です。もしこの記事の内容に少しでも興味を持った方がいれば、ぜひ連絡をお待ちしています! herp.careers
はじめに newmoで自動運転を開発している Iwamin です。 2025年6月9日から11日にかけて、ニューヨークで開催された Datadog の国際フラグシップイベント DASH に登壇してきました。その後サンフランシスコに飛び、自動運転タクシー Waymo に乗車しました。本記事では、DASH 登壇までの流れと発表内容、そして商用運行している Waymo の乗車体験についてお話しします。 Datadog DASH への登壇 DASH は Datadog 本社のあるニューヨークで毎年開催されている国際フラグシップイベントです。新機能が公開される Keynote やユーザーのセッション、社員によるデモなどが行われるイベントです。AWS re:Invent や Google Cloud Next のような位置付けです。 www.dashcon.io このイベントには実は CfP の募集が存在し、これに通ると当日セッションをすることができます。英語での 40 分のセッションです。 実はすでに来年の DASH 2026 の募集も開始しています。もし興味のある方はぜひ挑戦してみてください。 Call for Proposals | DASH by Datadog 自分が CfP に送った内容およびセッション紹介ページとして公開されたものがこちらです。 www.dashcon.io 当日のセッションは録画され、YouTube に公開されます。下手な英語でお恥ずかしいですがこちらが動画になります。もし雰囲気が気になる方がいましたらぜひみてみてください。 www.youtube.com speakerdeck.com 同様の内容で8月末に行われた Japan Datadog User Group Meetup#12@東京 で発表した日本語版の資料がこちらになります。 モノレポにおけるエラー管理 ~Runbook自動生成とチームメンションの最適化 - Speaker Deck proto ファイルに記述した Runbook の内容をアラート通知に自動で埋め込む試みと、モノレポ開発におけるアラートのオーナーシップを明確にするための Reference Table を活用したメンショングループ設定について発表しました。 登壇までの流れ 自分は海外登壇も英語登壇も初めてでした。また、Datadog DASH の登壇に関する情報もとても少なく、過去に日本人で登壇した事例は、2023年の DASH での NTT DOCOMO さんの動画しか見つけられませんでした。 www.youtube.com そのため、登壇までのタイムラインなどについて振り返りながら書いていきます。 3月上旬 CfP の締切でした。CfP の投稿フォームがあるのですが、特に投稿した後に確認のメールが来るわけではなかったため、提出はしたものの本当に届いているのか不安だった記憶があります。 3/20 Accept Mail がきました。登壇者情報の登録、そしてスライドのドラフトの提出、そして担当者との Dry-run なるものがあることを知りました。 5/5 ドラフトスライドの締切でした。ゴールデンウィークど真ん中でドタバタしていました。英語でのスライドを作ったことはなかったため、日本語と英語でのスライドの作り方の違いにとても苦労しました。 実は今回の DASH は IVRy の Moriya さんも日本人登壇者として参加しており、事前に繋げていただき色々と情報共有をしていました。Moriya さんはかなり前からスライドを作ってプレゼンの練習を始めており、絶望した記憶があります。 note.com 5/21 朝 最初の Dry-run がありました。自分のセッションを担当してくれる専属の Datadog 社員の方と、もう一人の Datadog 社員(こちらは回によって変わる)の前で、オンラインでセッションの練習をし、フィードバックをもらいました。 時差の関係で朝方に練習しました。練習後、スライドごとに細かいフィードバックを口頭とドキュメントでいただきました。ここまで手厚い登壇サポートはとてもありがたく、初めての海外登壇には素晴らしい環境でした。 5/29 スライドを修正して二回目の Dry-run をしました。この辺りはシャドテンを契約したり、夜中に何度もプレゼン練習をするなどしてなんとか英語を話すことに慣れるようにしていました。 5/30 スライド本締切でした。意外と締切からイベントまで残り日数がなかったため、緊張してあまり寝れなかった記憶があります。 6/9 昼過ぎに空港につきました。 🫡#DatadogDASH pic.twitter.com/YkHJ1ukiGn— Iwamin (@B_Sardine) 2025年6月9日 その後、日本からの参加者向けにニューヨーク本社ツアーをしてもらいました。Datadog はすべての国のオフィスの家具やレイアウトなどを揃えているらしく、Datadog Japan ととても似たようになっていました。 いつもの bits くんに見えるが… また、COO とのセッションがあり質問することもできました。 DASH への日本からの参加者は昨年から 20 → 70人に増えたらしく、日本からの Datadog への関心の高まりを感じました。 また、夜は日本からの参加者向けのウェルカムディナーがありました。会場は本社のすぐ近くにあるウルフギャングステーキハウスの本店でした。 6/10 この日は Keynote と明日の発表に向けたテクニカルチェックがありました。Keynote はいつも配信で見ていたものを生で見ることができ感動しました。 ずっと生で見たかったDASHのKeynoteだ… #DASH2025 pic.twitter.com/XdF4biqXkW— Iwamin (@B_Sardine) 2025年6月10日 テクニカルチェックは実際にスライドなどが映るか、Speaker Note のサイズが大丈夫かなどを確認できる場でした。 また、会場には映像パネルで登壇者情報が表示されるようになっていました。こうして自分が表示されているのを見てテンションが上がると同時に緊張が高まっていました。 夜は Datadog Ambassador と Speaker のパーティーがありました。他のスピーカーとの交流ができてとても貴重な機会でした。 6/11 午前は IVRy の Moriya さんのセッションがあったため聴講しにいきました。LLM Observability の話をしており
はじめに Bluetoothエンジニアのharadaです。 Android 16でBluetooth 6.0のチャネルサウンディングがサポートされましたね 🎉 チャネルサウンディングは、従来のRSSI (電波強度) を使った距離推定とは異なり、センチメートル級の精度で距離を測定できる 技術です。スマートロックやデジタルキーなど、「近づいたら解錠」といったユースケースで活用が期待されています 🔑 この記事では、実際にPixel 10とNordic nRF54L15 DKを使って動作確認した経験をもとに、Androidでのチャネルサウンディング実装方法を紹介します。 ちなみに、動かすまでにかなりハマりました...😕 そのあたりも共有します。 制限事項 2025/12/20時点で、Bluetoothチャネルサウンディングに対応したAndroid端末は限定的です。 Nordic Semiconductorの記事 によると、Android 16 QPR2をインストールしたPixel9シリーズまたはPixel 10シリーズでBluetoothチャネルサウンディングの動作が確認可能、とのことです。 チャネルサウンディングの動作の様子 nRF54L15とPixel 10の距離をリアルタイムで測定しています。 チャネルサウンディングとは Bluetooth 6.0で追加された距離測定技術です。 位相ベース測距(PBR)と往復時間(RTT)を組み合わせた高精度な測距技術です。 2.4GHz帯の複数チャン ネルを用いて信号の「位相のズレ」と「飛行時間(Time of Flight:飛行時間)」を解析することで、±数十センチ程度の精度で距離を推定。 従来のRSSI方式に比べ、環境の影響を受けにくく、改ざん不能な物理的距離に基づいた安全な通信を実現します。 従来のRSSIと何が違う? RSSI チャネルサウンディング 精度 数メートル ±数十センチ程度の誤差 原理 電波強度 位相ベース測距 環境影響 大きい 小さい RSSIは「電波の強さ」で測るため、障害物や反射の影響を受けやすく誤差や偽装の懸念がありました。一方、チャネルサウンディングは「位相のズレ」に加え「信号の往復時間」を直接測定します。これにより、環境に左右されない正確な測距と、リレーアタックを防ぐ高い安全性を両立しています。 ※リレーアタックは、スマートキー等が発する電波を特殊な機器で中継(リレー)し、離れた場所にある鍵を解除する方法です。 用語 Initiator: 距離を算出する側 Reflector: Initiatorに応答する側 動作環境 今回使った環境はこちら。 Initiator側:Android 16 QPR2をインストールしたPixel 10 Reflector側:Nordic Semiconductorの開発キットnRF54L15 DK ※nRF54L15 DK にはReflector用のファームウェア channel_sounding_ras_reflector を利用しました。 RangingManager API Android 16で追加されたRangingManagerは、統一されたAPIで複数の測距技術をサポートしています 📡 今回扱うのは Bluetooth Channel Soundingです。 技術 精度 特徴 UWB ±10cm程度 最も高精度 Bluetooth Channel Sounding ±数十cm程度 今回使うのはこれ WiFi RTT ±1m程度 WiFi APの位置情報を利用 Bluetooth RSSI 数m程度 従来の電波強度ベース Androidアプリに必要なパーミッション <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.RANGING" /> RANGINGがAndroid 16で新規追加された権限です。これがないと動きません! 処理の流れ 全体の流れはこのような流れです。 No ステップ 説明 1 BLEスキャン Reflectorデバイスを探す 2 GATT接続 BLE GATTの接続を確立 3 GATTサービス検出 RAS Service (0x185B) を検出 4 ペアリング RAS Control Point書き込みでトリガー 5 Ranging開始 RangingSessionを作成して開始 6 距離取得 onResults()で距離を取得 シーケンス図 %%{init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#fff', 'primaryTextColor': '#000', 'primaryBorderColor': '#710000', 'lineColor': '#ffffff', 'secondaryColor': '#fff', 'tertiaryColor': '#fff', 'actorTextColor': '#000', 'actorLineColor': '#fff', 'signalColor': '#ffffff', 'signalTextColor': '#ffffff', 'labelBoxBkgColor': '#fff6a5', 'labelBoxBorderColor': '#fff6a5', 'labelTextColor': '#000', 'loopTextColor': '#fff', 'noteTextColor': '#000', 'noteBkgColor': '#fff' } }}%% sequenceDiagram participant App as App(Initiator) participant Android as Android participant Device as nRF54<br>(Reflector) Note over App,Device: 1. BLEスキャン App->>Android: startScan() Android->>Device: BLEスキャン Device-->>Android: アドバタイズ応答 Android-->>App: onScanResult() Note over App,Device: 2. GATT接続 App->>Android: connectGatt() Android->>Device: GATT接続 Device-->>Android: STATE_CONNECTED Android-->>App: onConnectionStateChange() Note over App,Device: 3. GATTサービス検出 App->>Android: discoverServices() Android->>Device: GATTサービス検出 Device-->>Android: RAS Service (0x185B) Android-->>App: onServicesDiscovered() Note over App,Device: 4. ペアリング App->>Android: writeCharacteristic() Android->>Device: RAS Control Point (0x2C17) 書き込み Device-->>Android: ペアリング要求 Android-->>App: ペアリングダイアログ表示 App->>App: ユーザーが承認 Note over App,Device: 5. チャネルサウンディング開始 App->>Android: createRangingSession() Android-->>App: RangingSession App->>Android: session.start(RawInitiatorRangingConfig) Android->>Device: CS開始 Device-->>Android: CS応答 Android-->>App: onStarted() Note over App,Device: 6. 距離測定 loop 測定間隔ごと Android->>Device: CS測定 Device-->>Android: 測定結果 Android-->>App: onResults(distance) end 実装のポイント 1. RawInitiatorRangingConfigを使う ここが今回一番ハマったところです 🤔 AndroidのRanging APIには2つのモードがあります。 OobInitiatorRangingConfig:Out-of-Band(帯域外) BLE GATTで測定に必要なパラメータをやりとりする方法 RawInitiatorRangingConfig:測定パラメータを直接指定する方法 最初はOobInitiatorRangingConfigで実装しましたが、NordicのReflectorと通信するとプロトコルエラーになりました。 Nordicのサンプルコード を見ると、RawInitiatorRangingConfigを使っていたので 、そちらに変更したら動きました!Nordic nRF54L15 DKのファームウェアがBLE GATT経由でのRangingパラメータ交換に対応していないことが原因のようです。 2. ペアリングが必要 nRF54L15 DKのサンプルアプリでチャネルサウンディングを動かすには、ペアリングが必要です。ペアリングは、RAS Control Point (UUID: 0x2C17) への書き込みでトリガーできます。 ペアリング未完了のままRangingを開始すると、onClosed(reason=3) (PROTOCOL) エラーになるので注意してください。 コード例 実際に動いたコードを紹介します。 GATT接続とペアリングトリガー GATT接続シーケンスの中でペアリングを発動させています(なぜか事前にペアリング済みだと上手く動かない…) class BleGattConnector(private val context: Context) { private val rasServiceUuid = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb") private val rasControlPointUuid = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb") private var gatt: BluetoothGatt? = null fun connect(device: BluetoothDevice): Flow<ConnectionState> = callbackFlow { val callback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED -> { // 接続成功したらサービス検出へ gatt.discoverServices() } BluetoothProfile.STATE_DISCONN
はじめまして、newmo QAチームのfujiemonです。 2025年下半期、私たちQAチームは品質保証プロセスの抜本的な改革に取り組みました。 「限られたリソース(時間・人)で、品質を高めながら、いかに効率的にテストを実施するか」という課題に向き合い、4つの主要な施策を推進してきました。その取り組みと成果についてご紹介します。 1. QAのガイドラインを制定 ガイドラインの全体構成 全体方針 自動テストガイドライン 手動テストガイドライン フィールドテストガイドライン ガイドライン制定による効果 生きたドキュメントとしての運用 2. リグレッションテストの効率化 具体的な改善実績 成果のポイント 3. エンジニアによるセルフQAの推進 セルフQAチェックリストの自動生成システム 導入による効果 4. テストプランの自動生成によるマニュアルQA作業の負荷軽減 テストプラン自動生成の仕組み Claude Codeによる継続的な改善 QAエンジニアの役割の変化 導入による具体的な効果 今後の展開 まとめ〜次のステージへ 1. QAのガイドラインを制定 まず着手したのは、QAプロセスの標準化です。これまで暗黙知として共有されていたテスト手法やベストプラクティスを明文化し、チーム全体で共有できる包括的なQAガイドラインを制定しました。 ガイドラインの全体構成 ガイドラインは、QAプロセス全体をカバーする体系的なドキュメントとして整備されています 全体方針 QA優先度の方針 リソースを適切に配分するための優先度判断基準 QAの対応表 各サービスやプロジェクトに対するQAアプローチの一覧 QAのリグレッションテスト 既存機能の品質を保証するテスト方針 手動テストと自動テストの使い分け それぞれの特性を活かした効果的な使い分け指針 自動テストガイドライン 効率的な自動テストの実装と運用のための詳細な指針を整備 テストケース作成ガイドライン ノーコード/ローコードテストツールの活用方法 E2Eテストの構成 各サービス(タクシー配車、点呼など)の具体的なテスト構成 Firebase Test Labの導入 Android向けテストインフラの整備 Cloudflare認証への対応 セキュリティ機能を考慮したテスト実装 手動テストガイドライン 人の目による確認が必要な領域の体系化 QAの事前準備: テスト実施前に確認すべき項目の標準化 テストプラン・チェックリストの自動生成: 効率的なテスト準備の仕組み マニュアルQA: 探索的テストや例外ケースの対応方針 フィールドテストガイドライン 実環境でのテストを成功させるための指針 ドライバー向けフィールドテストチェックリスト: 実車を使ったテストの標準手順 フォーマットと議事録: テスト結果の記録と共有方法 ガイドライン制定による効果 このガイドラインの制定により、以下のような効果が得られました 新メンバーのオンボーディング時間の短縮 明文化されたプロセスにより、学習曲線を大幅に改善 実践的な知識を体系的に習得可能 テスト品質のばらつき低減 統一された基準により、誰が実施しても一定の品質を確保 ベストプラクティスの共有により、チーム全体のスキル向上 QAプロセスの透明性向上 何を、なぜ、どのようにテストするかが明確に ステークホルダーへの説明責任を果たしやすく 開発チームとの認識のすり合わせが容易に 共通言語としてのガイドラインにより、コミュニケーションが円滑に セルフQAの推進にも貢献 生きたドキュメントとしての運用 ガイドラインは単なるルールブックではなく、チームの知見が集約された「生きたドキュメント」として運用しています。新しいツールの導入、テスト手法の改善、プロジェクトでの学びなどを継続的に反映し、常に最新の状態を保っています。 この包括的なガイドラインは、QAチームの活動の基盤となり、後述する自動化やセルフQA推進などの施策を支える重要な土台となりました。 2. リグレッションテストの効率化 リグレッションテストは品質保証において欠かせない一方で、時間とリソースを大きく消費する作業でもあります。そこで、テストプロセス全体を見直し、驚異的な効率化を実現しました。 具体的な改善実績 自動化と手動実行時の不要な項目削除により、以下のような大幅な工数削減を達成しました 主要サービスの改善例 タクシー車載タブレット 5.0時間 → 0.5時間(90%削減)/ 自動化率80% 配車アプリ(iOS, Android) 10.0時間 → 1.5時間(85%削減)/ 自動化率90% ライドシェアドライバー向けアプリ(iOS) 3.0時間 → 0.5時間(83%削減)/ 自動化率90% ライドシェアドライバー応募・審査ツール 1.0時間 → 0.2時間(80%削減)/ 自動化率95% 新規プロジェクトでの取り組み プロジェクト開始時から自動化を導入することで、初めから効率的なテスト体制を構築: 配車コンソール 自動化率80%で運用開始(2.0時間) 点呼 一部自動化実施中(0.5時間、自動化率10%) 今後の展開 現在手動で実施しているいくつかのサービスについても自動化を検討中です。 成果のポイント 平均80%以上の工数削減を実現 配車アプリ(iOS, Android)、ライドシェアドライバー向けアプリ(iOS)、ライドシェアドライバー応募・審査ツールは90%以上の自動化率を達成し、ほぼ完全自動化を実現 新規プロジェクトでは最初から高い自動化率で運用開始 この取り組みにより、リリースサイクルの高速化と品質維持の両立が可能になりました。 3. エンジニアによるセルフQAの推進 開発プロセスの早い段階で問題を発見することは、品質向上とコスト削減の両面で効果的です。そこで、エンジニアによるセルフQA※を推進する仕組みを構築しました。 ※Self Quality Assurance, エンジニア自身が行うテスト工程を指した造語 セルフQAチェックリストの自動生成システム 特に注力したのは、「セルフQAの確認項目を自動生成する」仕組みの導入です。このシステムは、Claude CodeとPlaywright MCPを活用し、PRD(製品要求仕様書)、PDD(製品設計仕様書)、Figma、GitHub PRなどのドキュメントから、包括的なQAチェックリストを自動生成します。 詳細はkitasukeさんによる12月8日のエントリ、生成AIの入出力品質は「フォーマット」で決まる ― 中間表現とテンプレートの組み合わせ - newmo 技術ブログ で詳しく書かれていますので、併せて読むことをおすすめします! 導入による効果 コード変更に応じた適切なテスト項目を自動提案 エンジニアが何をテストすべきか明確になり、テスト漏れを防止 QAチームへのエスカレーション前の品質向上 フィードバックサイクルの短縮により、手戻りを削減 QAエンジニアの工数を、チェックリスト作成からレビューにシフト セルフQAは開発者の負担を増やすものではなく、むしろ手戻りを減らし、開発効率を高めるための投資です。自動生成により、エンジニアは「何をテストするか考える時間」ではなく「実際にテストする時間」に集中できるようになりました。 4. テストプランの自動生成によるマニュアルQA作業の負荷軽減 マニュアルQAにおいて最も時間を要する作業の一つが、包括的なテストプランの作成です。従来、QAエンジニアは仕様書を読み込み、テストすべき観点を洗い出し、優先度を付けてテストケースを設計する必要がありました。この作業は経験と専門知識を要し、数時間から数日かかることも珍しくありません。 そこで私たちは、テストプランを自動生成する仕組みを構築し、QAエンジニアの負荷を大幅に軽減しました。 テストプラン自動生成の仕組み テストプラン自動生成は、セルフQAチェックリスト生成と同じ統合パイプラインの一部として実装されています: (詳しくは上述のエントリを参照ください) Claude Codeによる継続的な改善 このシステムの特筆すべき点は、Claude Codeを活用することで、テストプラン生成の品質が継続的に向上していることです 過去のテスト結果から学習: 過去に見逃された不具合パターンを学習し、類似ケースで検出 プロジェクト固有の文脈理解: 各サービスの特性や制約を理解した上でテストケースを生成 自然言語での柔軟な対応: 仕様書の表現が曖昧でも、文脈から意図を汲み取って解釈 (※注 12月11日のエントリ、 プロダクトチーム全員でAIエージェント縛りの開発を一週間試しました - newmo 技術ブログ でレポートされている時点では生成AIを充分に活用できていない状況でしたが、、その後あらためて習得する機会を設け、今ではそこそこ使いこなせる様になったと感じています) QAエンジニアの役割の変化 テストプラン自動生成の導入により、QAエンジニアの役割は以下のように進化しました Before(従来) 仕様書の読み込み 40% テストケース設計 30% テスト実行 20% バグレポート・改善提案 10% After(自動生成導入後) 自動生成プランのレビュー 15% 探索的テストの設計・実行 25% テスト実行 20% バグレポート・改善提案 30% テスト戦略・プロセス改善 10% QAエンジニアは単純作業から解放され、より戦略的で付加価値の高い業務に時間を使えるようになりました。 導入による具体的な効果 テストプラン作成時間を平均70%削減 テストカバレッジの向上(自動生成により見落としが減少) QAエンジニアの工数を、探索的テストや品質分析にシフト 新人QAエンジニアでも、経験者と同等の網羅性を持つテストプランを作成可能 ドキュメントとテストケースの一貫性が向上 今後の展開 現在、このテストプラン自動生成システムをさらに進化させる取り組みを進めています 不具合データベースとの連携 過去の不具合傾向から、リスクの高い領域を重点的にテスト 自動テストスクリプトの生成 Test Plan IRから直接E2Eテストコードを生成 テスト実行結果の自動分析 テスト結果からボトルネックや品質リスクを自動抽出 まとめ〜次のステージへ 2025年下半期の取り組みを通じて、QAチームは大きく進化しました。包括的なガイドラインの制定によって強固な基盤を構築し、効率化と自動化によって生産性を向上させ、セルフQAの推進によって開発プロセス全体の品質意識を高めることができました。 特にリグレッションテストの自動化では、平均80%以上の工数削減という目覚ましい成果を上げることができました。また、セルフQAチェックリストとテストプランの自動生成により、エンジニアが自律的に品質を担保できる環境を整備し、QAエンジニアはより戦略的な業務に集中できるようになりました。 これらの取り組みにより、QAチームはより付加価値の高い業務に集中できる環境が整いました。しかし、これはゴールではなくスタートです。テクノロジーの進化と共に、QAのあり方も常に変化していきます。私たちは引き続き、「より速く、より確実に、より高い品質を届ける」ことを目指し、改善を続けていきます。 書いた人: fujiemon
はじめに 背景 Datadog における Ingestion コストの課題 OpenTelemetry Collector によるテレメトリ転送 テイルベースサンプリング ヘッドベースサンプリングとの違い 懸念点 1. リソース消費 2. スケーリングの難しさ ログのテイルベースサンプリングとは? OpenTelemetry Collector のカスタムプロセッサ インターフェースの実装とビルド 実装を少しだけ見てみる 1. 受信とグルーピング (ConsumeTraces) 2. イベントループと時間管理 (iter / loop) 3. バッファリングと遅延データの処理 (processTrace) 4. メインの判定処理 (samplingPolicyOnTick) OpenTelemetry Collector に組み込む まとめ はじめに この記事は newmo Engineering Advent Calendar 2025 の 19 日目の記事です。本記事では opentelemetry-collector-contrib の tailsamplingprocessor に着想を得た「ログのためのテイルベースサンプリング」を、OpenTelemetry Collector のカスタムプロセッサとして実装した話を紹介します。 背景 Datadog における Ingestion コストの課題 newmo では、Observability プラットフォームとして Datadog を利用しています。Datadog はメトリクス、トレース、ログを統合的に扱える非常に強力なツールですが、サービスの成長とトラフィックの増加に伴いこれらテレメトリも増加し、結果としてコストが増大しがちです。特に正常なリクエストログや安定したエンドポイントが吐いているログなどデバッグ時の重要度が相対的に低いログまで全量取り込むことは、費用対効果の観点から見直しが必要でした。 OpenTelemetry Collector によるテレメトリ転送 現在、newmo のシステムではテレメトリ(メトリクス、トレース、ログ)の収集・転送に OpenTelemetry Collector (以下、otel collector) を利用しています。otel collector を介した Datadog へのログ転送アーキテクチャについては、以前 tjun さんが書いた記事で詳しく解説されています。(OpenTelemetry Collectorを使ったCloud Run to Datadogの実装パターン) otel collector ではオープンソースの opentelemetry-collecotor-contrib に実装されている多種多様なコンポーネント(Receiver, Processor, Exporter)をプラグインのように組み合わせて利用できます。このリポジトリには、トレースのサンプリングを行うための tailsamplingprocessor という強力なコンポーネントが既に存在します。これは例えば、「リクエストの処理完了まで待って全スパンを評価し、エラーステータスを持つスパンが含まれていればそのトレース全体を保存する」*1のような高度なサンプリング制御を可能にします。 しかし、この tailsamplingprocessor はトレース専用のコンポーネントとなっており、残念ながらログに対してそのまま適用することはできません。 私たちが実現したかったのは、単に個々のログにおけるログレベル(Error か Info か)だけでサンプリング決定することではなく、「同一トレース ID を持つ一連のロググループ」をひとまとまりとして扱い、全体の結果に基づいてロググループをサンプリング決定することです。 例えば、あるリクエスト処理中に一度でもエラーログが発生したのであれば、そのリクエストに関連する正常系のログ(Info など)も含めて全て Datadog に取り込まなければ、前後の文脈を追うことが難しくなります。逆に、完全に正常終了したリクエストであれば、その一連のログはまとめて drop の対象としても問題ない、と考えました。 既存のコンポーネントにこの要件を満たすものが存在しなかったため、私たちは tailsamplingprocessor のロジックに着想を得て、ログ用のテイルベースサンプリングを行うカスタムプロセッサを自前実装することにしました。 テイルベースサンプリング コスト削減と Observability の両立を目指す上で、鍵となるのが「サンプリング方式」の選定です。ここでは ヘッドベースサンプリング (Head-based Sampling) と比較することで、テイルベースサンプリング (Tail-based Sampling) についてより深く理解していきます。 ヘッドベースサンプリングとの違い ヘッドベースサンプリングは、単一のテレメトリをアプリケーションから送信する時もしくは otel collector に到達した時に、サンプリング決定を行います。例えば「10% の確率で保存する」といったランダムな判断や、特定のユーザーIDに基づいてサンプリングするかどうかを決定します。決定ロジックが単純で、オーバーヘッドが小さく実装難易度も高くないので導入しやすい一方で、サンプリングされたテレメトリはリクエスト内で欠損があるのでトレース全体の文脈を理解するのが難しい場合があります。 対してテイルベースサンプリングは、リクエストの処理が完了するまでサンプリング決定を保留します。一連の処理が終わった段階で、「エラーが含まれていたか?」「レイテンシが高かったか?」「特定の (key, value) を含んでいたか?」を評価し、同一 Trace ID をもつロググループに対してサンプリング決定を行います。テイルベースサンプリングを採用することで S/N 比*2が高くコスト効率的な監視基盤の実装を実現できます。 懸念点 非常に強力なテイルベースサンプリングですが、導入にはアーキテクチャ上の考慮事項が存在します。 1. リソース消費 サンプリング決定を事後的に行うためには、あるリクエストに関連するすべてのログやスパンを otel collector 上のメモリにバッファリングしておく必要があります。 トラフィック量やトレースの保持期間設定によっては、メモリ消費量が顕著に増加するため、リソース設計が必要です。例えば tailsamplingprocessor において、最初にとある Trace ID を持つスパンが到達してからサンプリング決定までバッファリングする時間を decision_wait で設定できますが、デフォルトの 30s ではかなりメモリを圧迫することになります。 実際、デフォルト値のままデプロイした結果 high memory usage というメッセージと共にトレースをドロップしてしまう結果となりました。 (図1: テイルベースサンプリングを適用後 CPU / メモリの使用量が一時的に上がっている様子) 2. スケーリングの難しさ otel collector を複数台構成(Replica > 1)で運用している場合にもさらに複雑な課題が発生します。同一 Trace ID のログが別々のインスタンスに振り分けられてしまうと、各インスタンスはロググループの全体像を把握できず、正しいサンプリング決定ができません。 これを解決するには、前段に loadbalancingexporter を配置し、Trace ID に基づいたコンシステントハッシュを用いて同じ Trace ID のテレメトリが必ず同じ インスタンスに届くような構成(ステートフルなルーティング)を組む必要があります。 ログのテイルベースサンプリングとは? ここまでトレースを前提としたような説明をしてきましたが、ログにおけるテイルベースサンプリングも、概念は全く同じです。 構造化ログの中に含まれる Trace ID をキーとして、一連のログを「ひとつのリクエスト処理」としてグルーピングします。あとはトレースの場合と同様に、バッファリングしグループ全体でサンプリング決定し、グループごと保存するか破棄するかを決定するだけです。 OpenTelemetry Collector のカスタムプロセッサ 実装の詳細に入る前に、otel collector の拡張性について軽く紹介します。 otel collector は、データを受け取る Receiver、加工する Processor、外部へ送信する Exporter という3つの主要コンポーネントによるパイプラインで構成されています。 基本的なユースケースでは opentelemetry/opentelemetry-collector や opentelemetry/opentelemetry-collector-contrib で提供されているプロセッサ(batch, memory_limiter, attributes, ...)を組み合わせるだけで事足りますが、今回のように「カスタマイズした制御を行いたい」「特殊なビジネスロジックを挟みたい」といった要件がある場合、Go 言語で独自の カスタムプロセッサ を実装することが可能です。 インターフェースの実装とビルド カスタムプロセッサを作るといっても、ゼロから全てを作るわけではありません。基本的には、otel collector が定義しているインターフェースを満たす Go の構造体を実装すれば良いです。 また、作成したカスタムプロセッサを組み込む際は、OpenTelemetry Collector Builder (OCB) というツールで手軽に行えます。newmo での導入事例はこちらの記事で紹介されているのでご覧ください: OpenTelemetry Collectorを使ったCloud Run to Datadogの実装パターン 今回は、この仕組みを利用して「ログ用テイルベースサンプリング」のロジックを Go で実装し、独自の Collector バイナリとしてビルドするアプローチを採りました。 実装を少しだけ見てみる では、具体的にどのようなコードで動いているのか、実装の核心部分を本家の tailsamplingprocessor (Trace用) を例に見ていきます。 処理の流れは、「受信」「イベントループ」「バッファリング(兼・遅延処理)」「サンプリング決定」 の4ステップで構成されています。 1. 受信とグルーピング (ConsumeTraces) まず、プロセッサのエントリーポイントとなる ConsumeTraces です。ここでは、送られてきたスパンの集合を受け取り、処理しやすい単位に整理します。まず、満たすべき実装については以下のとおりです。 ここでは、バラバラに来るスパンを TraceID 単位に束ね直して、裏で動いている goroutine へ渡すことだけを行っています。 // ConsumeTraces is required by the processor.Traces interface. func (tsp *tailSamplingSpanProcessor) ConsumeTraces(_ context.Context, td ptrace.Traces) error { for _, rss := range td.ResourceSpans().All() { // TraceID ごとにスパンをグルーピングする // (ログの場合、ここで plog.Logs を TraceID ごとに仕分ける) idToSpansAndScope := groupSpansByTraceKey(rss) // ...(中略)... // グルーピングしたデータをバッチとしてまとめ、処理用チャネル(workChan)に流す // ここで即座に処理せずチャネルに流すことで、受信処理をブロックしないようにしている if len(batch) > 0 { tsp.workChan <- batch } } return nil } 2. イベントループと時間管理 (iter / loop) プロセッサの心臓部です。ここでは「データの到着」と「時間の経過 (Tick)」の両方を待ち受けています。Tick の間隔はデフォルトで1秒です。つまり、データの処理とは非同期に、毎秒「判定待ち時間 (decision_wait) を過ぎたデータはないか?」という監視が行われています。 func (tsp *tailSamplingSpanProcessor) iter(tickChan <-chan time.Time, workChan <-chan []traceBatch) bool { select { // A. 新しいデータが届いた場合 case<
はじめに 2025年は、iOS、Frontend、Androidの3つのプラットフォームで開発しました。本記事では、各プラットフォームでの開発を簡単に振り返り、プラットフォーム間の移動について、ゆるく考えたことをまとめました。 3つのプラットフォームでの開発 メイン領域はiOSですが、今年はFrontendやAndroidの開発にも参画させてもらいました。 と言っても、既存のプロジェクトやプロダクトの事情でスポット参戦する形です。スクラッチからの開発ではないので、整備された開発基盤やドキュメントなど、これまでに築かれてきた資産の恩恵を受けながらの開発です。 さて、3つのプラットフォームでの開発を時系列で大雑把に振り返ってみると、以下です。 iOS - 1月〜6月 Frontend - 6月〜10月 Android - 10月〜12月 個人的なバイブスの推移 まずは定性的な振り返りとして、以下、個人的なバイブスの推移です。 %%{init: {'theme':'base', 'themeVariables': {'xyChart': {'plotColorPalette': '#0066cc'}}}}%% xychart-beta title "Vibes in 2025" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Vibes" 0 --> 10 line [10, 9, 8, 8, 8, 8, 8, 8, 8, 7, 8, 8] 主な出来事: 1月: iOSアプリローンチ(正確には2024年の12月末のローンチ) 6月: Frontend開発へ参画 10月: Android開発へ参画 昨年末にiOSのプロダクトをリリースして、どんと上がって、そこから開発を重ねていく中で、少し疲労を感じた時期もあったりしましたが、Frontendに移って、新しい目標をもらって、忙しく過ごさせてもらっているうちに気持ちは上がっていました。Frontendでのプロジェクトがひと段落したタイミングで、Android開発に移りますが、久しぶり(5年ぶり?)にAndroidに触れられて、学びも多く、楽しめました。 そうこうしているうちに、気づいたら年末になっていたような気がします。(1年って早いですね) 各プラットフォームで開発したアプリの概要 簡単ですが、各プラットフォームで開発したアプリの概要です。 プラットフォーム 概要 言語 UI フレームワーク iOS 乗客向けのタクシー配車アプリ Swift SwiftUI TCA Frontend タクシーの営業所向けの業務用アプリ TypeScript React React(Next.js) Android タクシードライバー向けの配車リクエスト受信アプリ Kotlin Jetpack Compose - 技術スタックとしては、各プラットフォームとも比較的モダンで標準的な構成になっていると思います。 各プラットフォームでの開発期間とコミット数 定量的な振り返りとして、各プラットフォームでのコミット数も集計してみました。なお、コミット数は少なく見えるかもしれませんが、Squash and mergeでPull Requestをマージしているため、実際のコミット数とは異なります。 プラットフォーム コミット数 期間 コミット数/月 iOS 124 約5ヶ月 (1/6 〜 6/9) 約25 Frontend 134 約4.5ヶ月 (6/12 〜 10/27) 約30 Android 49 約1.4ヶ月 (10/31 〜 12/11) 約35 ちなみに、以下は月ごとのコミット数とバイブスを重ねたグラフです。 %%{init: {'theme':'base', 'themeVariables': {'xyChart': {'plotColorPalette': '#ff9900, #0066cc'}}}}%% xychart-beta title "Monthly Commits and Vibes in 2025" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis "Value" 0 --> 50 line "Commits" [5, 38, 48, 17, 6, 30, 22, 39, 24, 20, 36, 12] line "Vibes" [50, 45, 40, 40, 40, 40, 40, 40, 40, 35, 40, 40] コミット数とバイブスがリンクするかと思ったのですが、そうでもありませんでした笑 コミット数から分かること プラットフォーム間のコミット数に大きな差はない 各プラットフォームでのコミット数を見ると、iOS(124)、Frontend(134)、Android(49)となっています。 開発期間の短いプラットフォームもありますが、月あたりのコミット数で見ると他のプラットフォームと大きな差はありません。 iOS: 約25コミット/月 Frontend: 約30コミット/月 Android: 約35コミット/月 コミット数に大きな違いがなかったのは、プラットフォームを移動したときに大きなコストがかからなかったことを示していると思っていて、良かったことと捉えています。 そんなわけで、大きなコストがかからなかった理由についても考えてみました。 整備された開発基盤やドキュメント いずれのプロジェクトも、スクリプトやライブラリ管理ツールによって開発環境構築の大部分が自動化されています。また、設計方針やコードスタイルが記載されたガイドラインが継続的に更新されています。 具体的には: 自動化された開発環境構築: Makefileやスクリプトによるワンコマンドでのセットアップ 継続的に更新されるドキュメント: 設計方針、コーディング規約、アーキテクチャガイドライン 流用可能なモジュール: UIコンポーネントのライブラリ、低レイヤーのネイティブAPIをモダンなコードスタイルで利用できるようにしてくれるライブラリ これらは、開発を始める際の時間を大幅に削減してくれました。 Coding Agentの活用 また、Coding Agent(AI支援ツール)の活用も要因の一つです。 各プラットフォームには固有の文法、フレームワーク、ベストプラクティスがあります: Swift: Optional、Struct、Property Wrapper Kotlin: Nullable Types、Data Class、Sealed Class TypeScript: Union Types、Generics、Type Guards しかし、Coding Agentを使うことで: プラットフォーム固有の文法を都度学習する必要が減る フレームワークのベストプラクティスをその場で参照できる 実装方法が分からない場合でも、意図を伝えれば適切なコードを生成してくれる 結果として、プラットフォームの専門知識や各言語の文法に精通していなくても、一定の品質でコーディングできるようになっているように思います。 チームからのサポート さらに、各プラットフォームの開発チームからのサポートも大きな要因です: オンボーディング: 新しいプラットフォームに参画する際の丁寧なサポート 定期的な情報共有: プラットフォームごとの定例会議での技術的な議論や知見の共有 レビューとフィードバック: Pull Requestでの建設的なレビューによる継続的な学習機会 チームメンバーの協力があったからこそ、生産的に開発できたと感じています。 おわりに 2025年は、iOS、Android、Frontendという3つのプラットフォームで開発する機会がありました。 どのプラットフォームにもそれぞれの良さがあり、開発していて楽しかったです。これまでもプラットフォームを移ることはあったのですが、特に、Coding Agentによりプラットフォームを移るハードルは確実に下がっているのを強く感じました。来年も引き続き、Coding Agentにサポートしてもらいながら、様々な開発を陽気にコーディングしていきたいと思います。
newmoでsoftware engineerとして働いているtazoeと言います。この記事では、最近(2025年12月現在)取り組んでいるタクシー会社のDXにおける給与計算のチームのドメインや背景などについてご紹介します。 給与計算のドメイン 私達のチームでは、給与計算のドメインを扱っています。タクシー会社は、乗務員さんが安心して、そしてできるだけ効率よく働ける環境を整えることで、会社全体の収益を高めていく、という特徴を持つ業態です。そのため、給与計算の関連ドメインの重要性は高く、ロジックも複雑になりがちです。以下では、そのように重要で複雑な一方でミスの許されないドメインに対して私達がどのように取り組んでいるかをご紹介させてください。 給与計算は、具体的には以下のようなサブドメインから構成されます。 勤怠 乗務員さんの出勤時刻、退勤時刻、休憩時間などを管理します 日報 乗務員さんの出庫時刻、入庫時刻、運行履歴・売上情報などを管理します 未収管理 乗務員さんの未収の集計と消込業務などを行います 給与計算 売上情報と勤怠情報を集計し、基本給や歩合給、残業手当などを計算します また、乗務員さんの教育に伴う手当などを計算します 勤怠は、タクシー会社に限らず管理システムが広く利用されていてイメージしやすいと思います。ここでは、タクシー固有の要素の多い日報・未収管理・給与計算について紹介します。 日報 日報とは、法律上管理が義務付けられている記録で、勤務時間・走行距離・実車/空車・休憩・売上金額などを毎日まとめる必要があります。従来はこれを紙で手書きして記入していたのですが、最近では車載のタクシーメーターが自動で運行情報を記録し、出力する自動日報が一般的です。 私達のシステムでは、この自動日報や配車アプリの履歴などを収集し、日報としてデータをまとめて、後の未収管理や給与計算で利用可能なようにデータを整備しています。 未収管理 タクシーの清算時にクレジットカードを利用した場合、タクシー会社には後日クレジットカード経由でお金が支払われることになります。未収(=売掛金)とは、このように決済時は現金の受け渡しがないが後日支払われる仕組み全般のことです。元々はタクシー業界では未収=タクシーチケットでしたが、近年ではクレジットカードや各種交通IC、アプリ経由での支払いなどが増えたこともあり売上における割合が増えてきています。 未収管理では、タクシーメーター上に記録された未収の合計と後日実際に支払われる金額が一致している必要があり、そのために決済履歴や各種チケットやレシート類などを集計し、金額を検証する作業を行なっています。私達のシステムでは、これらのデータを自動で集計したり、フィルタリングしたりするオペレーションを整理して、人が必ずしも目視で全てのデータを確認しなくて済むようにするような機能を開発しています。 また、未収管理の過程でメーター操作の誤りなどが見つかることも多くあり、給与計算に向けた売上の確定処理的な意味合いもあります。 給与計算 タクシー会社は多くの会社が歩合制を採用しており、売上高に応じて乗務員さんの給与が変動します。シンプルに言えば、売上に一定の比率をかけて歩合給を計算するだけ、であれば実装は簡単ですが、実際には基本給などの固定給や、アプリ利用時の手当や控除、残業代の計算や、休日出勤による割増の計算、有給管理、各種教育の手当の支給などかなり多くの要素が各社存在します。 エンジニアリング視点での各社対応と共通機能の整理 newmoにはグループとして複数のタクシー会社があり、エンジニア視点で見れば、このような複雑なドメインで、各社固有のロジックを実装するのは開発コストが高く、できれば避けたいです。一方で、現場の業務や導入している決済手段、給与の規定などは簡単に統一できるものではないため、全ての実装を共通化することはできません。 そこで、現時点では、以下のような割り切りをしています。 勤怠・日報では各社固有の対応はしない。勤怠区分など、各社毎に異なる部分はユーザー定義マスターとして各社ごとに設定を分けられるようにして対応する 未収管理・給与計算は各社固有のロジックを実装する。データだけじゃなくてロジックそのものが各社独自なので、割り切って各社ごとに対応を行う 現時点では、最近オープンした夢洲交通へ導入しており、今は他のグループ会社への導入を進めているという状態です。 なぜ取り組んでいるのか? ここまで話を読むと、結構大変そう・・・、と思った方が多いのではないでしょうか?実際給与という重要なドメインを扱っているため、責任も重く、大変な部分が多いのですが、その分会社をよくできる余地も大きいと考えています。以下は筆者がパッと書いた例ですが、意図を持って開発しています。 勤怠管理の仕組みを一貫して作ることで、乗務員さんが無理なく働ける体制づくりを支え、結果として欠勤による稼働率の低下も防ぎやすくなるような仕組みを提案できないか? 日報データを詳細に解析してより売上が伸びやすいようにデータ分析できないか? 未収管理や給与管理の業務を統一して業務を楽にできないか? エンジニア視点でも業界固有のドメインの理解や現場感が求められる点、事業自体への改善などを提案したり、考えたりする機会が多い点が面白い点ではないかと思います。 (ここで宣伝になりますが・・・) PdMの方をnewmoでは絶賛募集中です!ご興味のある方は以下のリンクから応募ください! herp.careers
はじめに newmoでエンジニアをしているzukkyです。 newmoグループのタクシー車両には、乗務員さん向けの車載端末が設置されています。この端末は、開発チーム内では TDA(Taxi Driver App)と呼ばれており、乗務員さんはTDAを使って配車リクエストを受け、乗客を迎えに行きます。 このTDAでは、タクシーメーターの状態(空車・実車・迎車など)を取得する必要があります。取得した情報は、配車室システムから無線配車を行うときなどに、その車両が配車を受けられる状態かどうかを判断するために使われます。 現在は、タクシーメーターとTDAをBluetoothで接続し、メーターの状態をTDAで取得してバックエンドに送信しています。この一連の仕組みを、社内ではメーター連携と呼んでいます。 メーター連携の課題 私はTDAのAndroidアプリエンジニアとしてこのメーター連携の開発に関わることが多く、次のような課題を感じています。 1. メーターの種類とBluetooth送信機の組み合わせが車両ごと異なる newmoグループではタクシー会社を多くグループに迎えていることもあり、メーターの種類やBluetooth送信機の組み合わせがいくつか存在します。 過去にその組み合わせが増えるたびに、検証・開発をしてきました。メーターのメーカーごとにメーター情報の形式が異なるのはもちろんのこと、「Bluetooth経由でメーター情報を取得する」と一口に言っても、実際にはいくつかの方式があります。 例えば、BLE(Bluetooth Low Energy)GATT 接続、Bluetooth Classic接続、BLEのAdvertiseパケットを一定周期で受信するなど方式があります。 今後もタクシー会社をグループに迎える可能性もあり、メーターやBluetooth送信機の組み合わせが増えることが予想され、そのたびに検証・開発が必要になります。 2. 100%の接続状態を保つのが難しい Bluetooth通信は無線通信のため電波干渉などの影響で接続が不安定になることがあります。切断時の再接続などの実装は入れているものの、常に100%の接続状態を保てているという保証はありません。 3. メーターの運用コスト メーター連携の課題とは少し異なりますが、現行のタクシーメーターについても個人的に課題感を持っています。 例えば、運賃改定等があった場合にはタクシーメーター本体の更新が必要となったり、法令に基づき定期的な検定やメンテナンスが求められるため、ハードウェアならではの運用・更新作業が発生します。 こうした課題を解決できるものが「ソフトメーター」であると個人的には考えています。 ソフトメーターとは ソフトメーターは、従来は専用ハードウェアとして車両に取り付けていたタクシーメーターの機能を、タブレット端末等のソフトウェアとして実装したものです。 従来のタクシーメーターは、タイヤの回転数などの車両信号をもとに走行距離を推定し、その距離や時間に応じて運賃を計算していました。 一方、ソフトメーターは、GNSS(GPS など)から取得した位置情報などから走行距離を推定し、その距離や時間をもとに運賃を計算します。計算処理はすべてタブレット内で行われるため、メーター本体という専用機器に依存せず、従来のメーター機能を提供できます。 このソフトメーターを自社で開発し、車両に統一して導入すればメーター連携の課題の解決が期待できます。 メーカーごとに異なるメーター仕様に対応する実装が必要なくなり、乗務員さんがタブレット端末でメーター操作をして、端末のネットワーク経由でバックエンドにメーター情報を送信できるようになります。これにより、Bluetooth経由でメーター情報を取得してからTDAでバックエンドに送信することが不要になり、より安定したメーター連携が期待できます。 さらに、運賃計算をソフトウェアで行うため、運賃改定や制度変更にも柔軟に対応でき、設定変更も遠隔でできるため、車両ごとの対応が不要になります。定期的なメーターの検査についても、運行中のデータをソフトウェア的に収集し、国土交通省へ提供することで対応できます。これらにより、ハードウェアに起因する物理的な運用作業も減らせる可能性があります。 ソフトメーターの走行距離推定について タクシーメーターの重要な役割の一つとして、運賃計算があります。 運賃は「走行距離」と「走行時間」に基づいて決まるため、走行距離を一定の精度で正しく推定する必要があります。 ソフトメーターにおける走行距離の推定方法については、国土交通省が資料として公開しています。 出典: https://www.mlit.go.jp/jidosha/content/001860153.pdf 走行距離の基本的な考え方は、GNSSから一定間隔で送られてくる緯度・経度・時刻の情報をもとに、地点どうしの距離を計算し、それらを順番に足し合わせることで走行距離を求めます。 しかし、トンネル内などGNSSの電波が届かず、しばらく位置情報が更新されない区間も存在します。そのような場合には、車両から取得できる走行信号(車速など)や、ジャイロや加速度といったセンサーデータを用いて、位置情報を補正する必要があります。 そのため、ソフトメーター開発のための第一歩としては「GNSS信号」「走行信号」「センサーデータ」の取得について検証する必要があります。 GNSS信号とセンサーデータについては、タブレットなどの端末から取得できる可能性が高いと考え、今回は走行信号の取得を試してみることとしました。 車速の取得方法について 前述の通り、走行信号は移動した距離の算出に使用されるため車速を取得する必要があります。 車速の取得にはいくつか方法はありますが、近年はOBD2(On-Board Diagnostics Ⅱ)が多くの車両に搭載されており、比較的手軽に利用できることから、今回はOBD2を用いた方法を試してみました。 OBD2は、車両の自己診断や整備のために用意されているコネクタで、エンジン回転数や車速といった様々な車両情報にアクセスできます。 OBD2アダプタを車両のOBD2ポートに接続し、有線(USB)でタブレットなどの上位機でつなぐことで、車速情報を取得することができます。 ※ 車両やOBD2アダプタによっては、ECU(Electronic Control Unit:車両用コンピュータ)等に影響を及ぼす可能性があります。使用や検証は自己責任で行ってください。 OBD2での車速検証の方法 以下のOBD2アダプタを使用して検証することにしました。 https://www.amazon.co.jp/dp/B0CY9B18ZR newmoでは、テスト用のタクシー車両としてJPN TAXIを保有しています。 この車両は 運転席下部にOBD2ポートが設置されているため、そこにOBD2アダプタを接続します。 JPN TAXIのOBD2ポート PCとOBD2アダプタを接続し、Pythonのpython-OBDライブラリを用いて以下のコードで車速を取得することします。 import obd import time def get_speed(): connection = obd.OBD("/dev/tty.usbserial-XXXXXX") if not connection.is_connected(): print("OBDアダプター接続失敗") return while True: response = connection.query(obd.commands.SPEED) speed = response.value.magnitude print(f"\r車速: {speed} km/h") time.sleep(0.5) if __name__ == "__main__": get_speed() OBD2アダプタの反応がない… いざテスト車両で検証しようとしたところ、OBD2アダプタに反応がなく車両の常時電源を取得できていないようでした。 同僚に相談したところ「ヒューズが飛んで電流が流れていないのではないか」という話になりました。ヒューズは、規定以上の電流が流れたときに内部の金属部分が溶けて回路を遮断し、配線や機器を守るための部品です。 助手席下部のフューズボックスから取り出してフューズの状態を確認してみることにしました。 ヒューズボックス 車両から取り出したヒューズを確認したところ動線が切れており、なんらかのタイミングでヒューズが飛んでしまっていることがわかりました。 車両から取り出したヒューズと新品のヒューズの比較(少し見にくいかも...) 新品のヒューズに交換したところ、OBD2アダプタのLEDが点灯し、無事にOBD2ポートと接続することができるようになりました。 検証結果 テスト車両で検証をし、以下のように車速を取得できました。車両のスピードメーターと比較すると、OBD2ポートから取得した車速は-5km/hくらいの差があることがわかりました。 検証時の車速のログ出力 これは車両のスピードメーターは「道路運送車両の保安基準」により実際の速度を超えてはならないという制限があり、スピードメーターは実際の速度より高めに設定されていることに起因すると考えられます。 今回の結果だけではソフトメーターとしての認定基準を満たせるかどうかまでは判断できませんが、OBD経由の車速情報からはある程度妥当な速度が取得できていそうだという感触は得られました。 おわりに 今回は、ソフトメーターのアプローチを検討するにあたっての参考として、JPN TAXIからOBD2経由で車速を取得できるかを試しました。 ソフトメーターはメーター連携の課題を解決しうる選択肢のひとつですが、実運用には国交省の認定が必要で、不具合や異常時には営業継続や運賃請求に直結するため、かなり慎重な検討や設計が求められます。 今回の検証が将来のタクシーメーターのありかたを考える材料になれば幸いです!
newmo のモバイルアプリ開発において、ユーザーが乗車位置を指定するピン操作は、乗車体験の入り口となる重要な機能です。 本記事では、バックエンドから配信される複雑な「乗降車禁止エリア」を回避しつつ、ユーザーにストレスを感じさせない滑らかな操作感を実現するために行った幾何学アルゴリズムの選定と、Coding Agentを活用したTDD(テスト駆動開発)の実装アプローチについて紹介します。 背景: 禁止エリアとUXの衝突 タクシー配車アプリにおいて、ユーザーが地図上で指定した場所が、法令や施設の都合で「配車できない場所」だったとします。 リクエスト送信後に「ここには配車できません」とエラーダイアログを表示する仕様は実装が簡単ですが、ユーザーにとっては手戻りとなり、良い体験とは言えません。 そこで newmo では、マップ上でピンをドラッグしている段階で、禁止エリアに入ったピンをシステムが自動的にエリア外の最適な位置へ誘導(Nudge)する、インタラクティブな制御を実装することにしました。 対象となる禁止エリアのデータはバックエンドから API 経由でリアルタイムに取得され、法令上の駐停車禁止区域、商業施設の敷地、公園など、多種多様かつ膨大な数の地図上の座標で構成された多角形(ポリゴン)として定義されています。 💡 技術補足: 本記事のポリゴンデータは、Google Maps SDKのGMSPathで表現されています。座標のエンコード形式についてはPolyline Algorithmを参照してください。 課題:隣接エリアによる無限ループ 当初、私たちはシンプルなロジックで実装を検討していました。「ピンが禁止ポリゴン内にあれば、そのポリゴンの最も近い辺から外側へ一定距離押し出す」というものです。 しかし、実データを用いた検証において、「隣接または密集する禁止エリア」が存在する場合に重大な問題が発生することが判明しました。 ピンの押し出しの無限ループ 具体的には、以下の現象です。 エリア A から押し出された座標が、隣接するエリア B に入る。 エリア B の判定ロジックが働き、再びエリア A(または別のエリア C)へ押し戻す。 上記が再帰的に繰り返され、ピンが境界線上で激しく振動したり、計算処理が無限ループに陥る。 外部データであるポリゴンの微小な隙間や重複を、すべて手動で隣接しないように修正することは現実的ではないため、クライアントサイドのロジックのみで、この幾何学的な図形同士の重なり合いからどうピンを押し出すか?を解消する必要がありました。 アプローチ:再帰的な図形の結合による動的なエリア拡張 無限ループを防ぐ解決策として、「押し出した先がさらに別の禁止エリアだった場合、それらのエリアを動的に結合して再評価する」という再帰的なアプローチを採用しました。 最終的なピンの押し出し動作 ピンが禁止エリア A にある場合、エリア A の外へ押し出す。 押し出した先の座標が、別の禁止エリア B に含まれているか判定する。 もし含まれていた場合、エリア A とエリア B を統合した「巨大な一つのエリア (A+B)」を生成する。 その巨大なエリア (A+B) の境界から、再度ピンを押し出す。 これを「押し出した先がどの禁止エリアにも含まれなくなる」まで再帰的に繰り返す。 このロジックにより、ピンがエリア間を行き来するたびに「回避すべき領域」が動的に拡張されていくため、最終的には隣接する全ての禁止エリア群の外殻の外側へと確実に脱出することができます。 なお、この「図形の統合」には複数の手法が存在しますが、後述するように、凸包(Convex Hull)を採用した場合は図形の凹みが消失するため、「ユーザーが本来置きたい場所に最も近い配車可能ポイント」ではなく、少し遠い位置に押し出されるケースがあります。このトレードオフについては次のセクションで詳しく説明します。 Coding AgentとTDDを用いた実装プロセス しかし、幾何学アルゴリズムをゼロから学習・実装するには、本来の開発タスクとは別に多大な時間がかかります。 そこで着目したのが、「Convex HullやGraham Scanのような古典的なアルゴリズムは、学習データが豊富なため、Coding Agentが比較的高い精度で実装できる領域である」という点です。図形の結合などの古典的なアルゴリズムは情報が古くからあり、ウェブ上に豊富な実装例が存在するため、AIの学習データも潤沢です。最新のライブラリ事情などとは異なり、枯れた技術領域では非常に精度の高いアウトプットが期待できます。 今回は、人間が「振る舞いの期待値」をテストコードとして定義し、その実装をAIに任せるというTDD(テスト駆動開発)のスタイルを採用することで、数学的な専門知識のギャップを埋めつつ実装を進めました。 1. 幾何学アプローチによるアルゴリズム選定 まず、「複数の図形を統合して一つのエリアとみなす」ための手法を選定する必要がありました。 Coding Agent との壁打ちを通じて、精度・計算コスト・実装難易度の観点から以下の4つのアプローチを比較検討しました。 検討した4つのアプローチ Polygon Union(ポリゴン結合/論理和) 図形同士を数学的に正確に結合する。形状は完璧だが、計算コストが非常に高い Concave Hull(凹包) 図形群の形状(凹み)を維持して包む。Unionに近い形状になるが、パラメータ調整が難しい Convex Hull(凸包) 図形群全体を「輪ゴム」で囲むように包む。凹みは埋まるが、計算が高速で安定している Bounding Box(AABB / 境界箱) 図形群全体が入る最小の「長方形」で囲む。計算は最速だが、余白(膨らみ)が最大になる 【アルゴリズム比較表】 手法 形状の正確さ (UX) 計算速度 (Performance) 実装難易度 (Robustness) 判定 Polygon Union ◎ 完全 (凹み維持) △ 低速 (O(N2)等) × 難 (自己交差などのバグ多発) 見送り Concave Hull ◯ 高 (凹み維持) ◯ 中速 △ 中 (パラメータ調整が困難) 見送り Convex Hull △ 中 (凹み消失) ◎ 高速 (O(N \log N)) ◎ 易 (枯れたアルゴリズム) 採用 Bounding Box × 低 (過剰な膨らみ) ◎ 最速 (O(N)) ◎ 易 (最大最小を取るだけ) 見送り 選定のプロセス まず、UXとして理想的なのは形状が正確な Polygon Union ですが、モバイルアプリのクライアントサイドでリアルタイムに(ドラッグ操作のたびに)複雑なブーリアン演算を行うことは、計算リソースの観点から非現実的でした。また、複雑なポリゴン同士の結合はエッジケースでの計算不全を招きやすく、ロバスト性に欠けます。 次に検討した Concave Hull は、形状と速度のバランスが良いものの、「どの程度の凹みを許容するか」を決めるパラメータ(Alpha Shapesなど)の調整が非常に難解です。今回のように「小さな公園」から「巨大な商業施設」まで大小様々なエリアが混在する地図データにおいて、万能なパラメータを定義することは困難でした。 逆に、最も単純な Bounding Box は計算が最速ですが、L字型のエリアなどを長方形で囲むと、本来配車可能な場所まで大きく削られてしまい、UXへの影響が大きくなります。 結論:Convex Hull の採用とトレードオフ 検討の結果、 Convex Hull(凸包) が「計算速度」と「UX(形状の近似度)」のバランスが最も優れた解であると判断しました。 Convex Hull を採用する際、懸念点として挙がったのが下図のような「Bulge Area(膨らみ)」の発生です。凹み部分が直線で結ばれることで、本来は禁止エリアではない場所(赤い斜線部分)までピンが置けなくなる可能性があります。 しかし、実際損なわれるUXとのトレードオフを検討した結果、この影響は極めて限定的であると判断しました。 発生頻度が低いエッジケースである この「膨らみ」が形成されるのは、あくまで「エリアAからエリアBへ押し出され、無限ループが発生した」時のみです。通常の操作でピンが禁止エリアの別の辺(隣接していない側)に近い場合は、そちらへ押し出されるため、統合処理自体が走りません。 直接のピン打ちは可能である もしユーザーが最初から「Bulge Area(赤い斜線部)」にピンを置いた場合、そこはどの禁止ポリゴンの内部でもないため、システムは何の制御も行いません。つまり、ユーザーが意図してその場所を選べば、問題なく配車位置として指定可能です。 「システムが自動で押し出す際のみ、安全策として少し広めに回避する」という挙動であれば、ユーザー体験を損なうことなく、アプリとしての「ロバスト性(堅牢さ)」と「パフォーマンス」を担保できると判断し、今回は Convex Hull を採用しました。 2. 期待値の定義 アルゴリズムが決まったところで、最も懸念していた「隣接するエリア間でピンが無限に振動する(Ping-Pong)問題」を防ぐため、以下のようなテストケースを定義しました。これは「2つの隣接エリア」があり、その境界付近にピンがある場合、両方のエリアの外側に一度で押し出されることを保証するものです。 @Test func testPushFromBoundaryRecursively_avoidsPingPong() { // 2つの近接した制限エリアを作成(往復移動を引き起こしやすい配置) let path1 = createSquareArea(centerLat: 35.6585, centerLon: 139.7020, size: 0.0008) let path2 = createSquareArea(centerLat: 35.6585, centerLon: 139.7022, size: 0.0008) let coordinate = CLLocationCoordinate2D(latitude: 35.6585, longitude: 139.7021) // 実行:再帰的な押し出し処理 let result = GeometryHelper.pushFromBoundaryRecursively( originalCoordinate: coordinate, accumulatedPaths: Set([path1]), allRestrictedPaths: [path1, path2], offsetMeters: 50 ) // 検証:エリア蓄積機能により両方のエリア外に押し出されていること #expect(!GMSGeometryContainsLocation(result, path1, false)) #expect(!GMSGeometryContainsLocation(
はじめに こんにちは、newmoの自動運転開発チームのigaryoです。 newmoでは、自動運転タクシーの運行に向けて、自社の車両によって収集したデータを元に自動運転モデルを学習・構築し、そのモデルで車両を動かすことに挑戦しています。そのためには、 高品質・大量・多様なデータを集めるためのデータ収集基盤 大規模なモデルを学習・推論するための計算機基盤 学習済みモデルを用いて現実世界の車両を安全に制御するための制御基盤 といった複数の要素が複雑に絡み合っており、それぞれに多くの難しさが存在します。 newmoではこれらの課題に対して、社内外のメンバーと密に連携をしつつ日々開発を進めています。 こうした、「物理世界で動くもの」を対象としたAIは、最近 Physical AI(フィジカルAI) として注目されています。 これまでは、LLMやデータ分析などのデジタルな情報空間内で完結する応用が中心でしたが、AI技術や計算機性能の向上により、現実世界のモノを直接動かすAIへの期待が高まっています。 Physical AIの代表例としてよく挙げられるのが、ヒューマノイドロボットや自動運転システムです。 しかし、どちらも前述の通り高度な認識・判断に加えて、安全・高速・高精度な制御が求められるため、入門のハードルが非常に高い領域でもあります。 そこで、その入門としておすすめなのがロボットアームです。 ロボットアームは手軽に準備できる一方で、TransformerやDiffusionといった最新のモデルを利用した本格的なPhysical AI開発を行うことができます。 本記事では、オープンソースのロボットアームであるSO-Arm 101および強力なロボット学習フレームワークであるLeRobotを取り上げ、その特徴や活用方法について紹介します。 SO-Arm 101の特徴 SO-Arm 101 SO-Arm 101は、TheRobotStudioとHugging Faceで共同開発したオープンソースの6自由度のロボットマニピュレーター (ロボットアーム) です。 SO-Arm 101のハードウェアは、遠隔操作に利用するリーダー (画像左) と実際に動作するフォロワー (画像右) によって構成されます。驚くべきことに、カメラやサーボ、配線などの電子部品を除いたすべての筐体は3DModelがGithub上で公開されており、自前の3D Printerがあれば、印刷して組み立てることが可能です。もちろん、印刷済みの部品や組み立て済みの筐体も各代理店から購入可能です。 3D Printed Parts SO-Arm101の大きな特徴として、機械学習向けに利用することに最適化されているというところがあります。 というのも、実際に機械学習モデルによってロボットアームを動かす時には、以下のフローでデータが伝搬されます。 実行時のデータフロー 模倣学習を利用した機械学習による推論モデルを作成するためには、機械学習モデルの入力である「画像データ」と「実際のそれぞれのサーボ角のデータ」が必要であり、実際に正しく動いたデータを収集する必要があります。 そこで、SO-Arm101では、リーダーとフォロワーを利用したテレオペレーションを利用します。 人間が遠隔でリーダーを操作し、それにフォロワーが追従することで、カメラの情報と各サーボ角の情報をセットで保存することができます。 また、機械学習のプロセス全体を支えるソフトウェアフレームワークがHugging Face製のOSSであるLeRobotです。 SO-Arm101をはじめとして、LeKiwiやUnitree G1など、様々なハードウェアを公式にサポートしており、新たなハードウェアで対応することも可能です。また、機械学習モデルも複数サポートしており、Transformerを利用したACT (Action Chunking Transformer) やDiffusionを利用したDiffusion Policy、VLAを利用したsmolVLAなどモデルを同一のハードウェアで利用することができます。 データセットの収集の際は、LeRobotのスクリプトにより、適切にデータ量を削減、機械学習に最適化された形式に加工された上で自動的にHugging FaceのDatasetsにUploadされます。 そして、学習もLeRobotを利用して簡単に行え、学習したモデルも自動的にHugging FaceのModelsにUploadされます。 こうした、「データ収集」「モデルの学習」「実機での推論」の一連のプロセスを簡単かつ本格的に行えるのがSO-Arm101とLerobotの魅力です。 ハッカソン グローバルで普及が進み、国内外でアウトプットの場があるのもSO-Arm101の魅力の一つです。 ここでは、国内で開催された2つのハッカソンについて紹介します。 Lerobot World Wide Hackathon LeRobotによる初のハッカソンであるLeRobot Worldwide Hackathonは、2025年7月14-15日に開催されました。全世界で同時に開催され、日本では4箇所のオフライン会場が設けられた他、オンラインでも多くの参加者が参加していました。 参加者のうち、審査員による審査と相互投票による上位者30チームには賞品としてLekiwi (自律移動ロボット) やHope Jr (ヒューマノイドロボット) が与えられました。 私も今夏個人で参加し、ありがたいことに18位に入賞し、景品を獲得することができました。 AMD Open Robotics Hackathon AMD OPEN ROBOTICS HACKATHONが今月初旬の2025年12月5-7日にAMD主催で秋葉原で行われました。 SO-ARM101やAMDのLaptop、潤沢な学習用の計算資源、非常に快適な開発環境 (物理) が与えられ、多くの参加者が参加していました。 賞金額も非常に高く、なんと優勝賞金は、$10,000 (約150万円) と太っ腹です。 社内の有志のメンバーで参加し、入賞することはできなかったものの、ロボットアームを利用した機械学習のプロセスを短期間で体験することができました。 まとめ 本記事では、newmoが取り組んでいる自動運転開発のような「Physical AI」の世界への入り口として、オープンソースのロボットアーム SO-Arm101 と LeRobot について紹介しました。 自動運転もロボットアームも、「現実世界の変化を認識し、適切な判断を下し、物理的に作用する」という本質的な難しさと面白さは共通しています。 ぜひ、皆さんもSO-Arm101やLeRobotを通じて、デジタル空間を飛び出したAIの可能性に触れてみてください。 newmoでは、こうした技術への理解を深めながら、さらに複雑で大規模な自動運転という社会課題の解決に挑み続けていきます。 また、newmoでは自動運転における機械学習やおよびその周辺領域に興味のあるエンジニアを積極的に採用中です! careers.newmo.me
newmoではタクシーの音声配車AIエージェントの「Maido」を開発しています。前回の記事では、LLM評価フレームワークのDeepEvalを用いたMaidoの会話評価基盤について紹介しました。 この記事では、その基盤を支えるもう一つの主役である「UserSimulator」の実装にフォーカスして、詳細を解説します。 なぜSimulatorが必要か MaidoのようなMulti-AgentなアーキテクチャのLLM Agentを開発・運用する上で、Agentの評価は不可欠です。 Simulatorが存在しない状態でAgentの評価を行う場合、初歩的な例ではAgentとユーザーのやり取りを「AgentがAという質問をしてきたら、Xと回答する」といった形でルールベースでシナリオ化する方法が考えられます。しかし、実際にはLLM Agentがこなせる会話は多岐に渡り、むしろそれが強みです。そのため、ルールベースで全てのパターンを網羅することは現実的ではありません。また、LLM Agentが発話するテキストは確率的に変化し常に一定とは限らないという点も、シナリオ定義の難しさを助長しています。 そのため、LLM Agentによるバリエーションに富んだ発話内容を自然言語のまま解釈して、適切に応答する機構が必要です。さらに、評価という観点では応答の内容から言葉の使い回しに至るまで、多様なパターンの振る舞いができると理想的です。 こうしたニーズに対して、ユーザーの属性や回答して欲しい情報をシナリオという形で定義し、それをプロンプトとして動作するLLM Agent = UserSimulatorを構築することは有効なアプローチです。 UserSimulatorの全体像 UserSimulatorは、Maidoを利用するユーザーの振る舞いをシミュレーションするAI Agentです。Maidoがタクシーの配車に必要な情報(名前、配車場所、予約配車か即時配車か、etc…)を1ステップずつ質問するのに対して、UserSimulatorが「名前は?→田中です」「場所は?→大阪城まで」「今すぐの配車か?→そうです」といった形で回答していきます。 会話の流れはYAMLベースのシナリオで定義されており、以下のような形でMaidoとユーザーとのやり取りをシミュレーションすることができます。 scenarios: - name: "田中太郎" age: 25 gender: "male" phone_customer_context: phone_number: "+815012345678" dispatch_histories: [] pickup_location: "アパホテル大阪なんば駅前タワー" pickup_location_type: "hotel" destination: "新大阪駅" preferred_time: "今すぐ" is_reservation: false is_talkative: true is_impatient: false ... Maido 未来都タクシーです。今すぐの配車でしょうか? User はい、今すぐお願いします。 Maido お迎えに上がる住所または場所を教えてください User アパホテル大阪なんば駅前タワーでお願いします。 Maido アパホテル大阪なんば駅前タワーにお迎えでよろしいでしょうか? User はい、そちらで間違いございません。ホテルの正面でお願いいたします。 Maido 承知いたしました。アパホテル大阪なんば駅前タワーの正面にお迎えに上がります。 ご乗車になる代表者の苗字をお伺いできますでしょうか? User はい、田中です。 Maido すぐに手配いたします。お電話を切らずそのままお待ちください。 User はい、承知いたしました。ありがとうございます。 Maido お待たせいたしました。無線番号 123 が 1分ほどで アパホテル大阪なんば駅前タワー に到着予定です。ご利用ありがとうございました。またのご利用をお待ちしております。 UserSimulatorの実装はMaido本体と同様にGoogle Agent Development Kitで行われており、以下の2つの動作モードが備わっています。 independentモード 独立したAI Agentとして振る舞うモード 音声はText-to-Speechで生成する 質問への回答をLLMが考えて発話するため、柔軟なやり取りが可能 with_audioモード LLMが次に発話すべきカテゴリの判断のみを行って、対応する音声ファイルを再生するモード 音声はカテゴリごとに事前定義されたフレーズ単位の音声ファイル(AudioClip)を利用 現実の配車音声から抜粋したフレーズを利用するため、より実環境に近い音声品質でやり取りが可能 2つのモードを実装している理由としては、MaidoのAgentの評価では多様なシナリオを元に柔軟なやり取りをして動作の網羅性を担保したいのに対して、E2Eテストでは電話の音声品質が性能に与える影響を見たい、といったニーズの違いがあるためです。 こうした機能を、AI Agentとしての実装以外にも複数のコンポーネントを組み合わせて実現しています。以下にUserSimulatorを取り巻くシステムのアーキテクチャの全体像を示します。 UserSimulatorを取り巻くシステムアーキテクチャの全体像 アーキテクチャの要素を分解すると大きく以下の3つのパートに大別できるので、以降の章ではそれぞれについて詳しく解説していきます。 UserSimulatorの実装 BigQuery上で行う配車音声の文字起こし AudioClipの生成 UserSimulatorの実装 前述の通り、UserSimulatorには2つの動作モードがあり、それぞれの動作モードでは別々のプロンプトをAgentに与えています。 independentモード まず、independentモードでは以下のようなプロンプトを実装しています。(部分的に実際の実装からは簡略化しています)シナリオに基づいて振る舞いを制御できるように、シナリオ内に含まれる属性に応じて動的にプロンプトが変わるのが特徴です。 @dataclass class UserScenario: name: str age: int gender: Gender pickup_location: str ... # その他のシナリオに紐づく属性情報 def to_prompt(self) -> str: """ Convert scenario to prompt text for LLM. """ prompt = f"""You are a user trying to book a taxi with the following profile. When mentioning addresses or place names, use locations in Osaka Prefecture. ## Basic Information: - Living in Osaka - Name: {self.name} - Age: {self.age} years old ({AgeGroup.from_age(self.age)}) - Gender: {self.gender}""" prompt += f""" ## Ride Information: - Pickup location: {self.pickup_location} - Pickup location type: {self.pickup_location_type}""" prompt += f""" ## Conversation Style: - Style: {self.conversation_style} - Speaking speed: {self.speaking_speed}""" prompt += "\\n\\n## Behavioral Characteristics:" if self.is_talkative: prompt += "\\n- Talkative and tends to give detailed explanations" if self.is_impatient: prompt += "\\n- Impatient and wants to complete the booking quickly" prompt += "\\n- May express frustration if the system doesn't understand immediately" prompt += "\\n- Uses short, direct phrases" (...) prompt += f""" ## Response Guidelines: - Respond naturally according to your profile and conversation style - If polite, use phrases like 「お願いします」「ありがとうございます」 - If casual, use shorter forms like 「すぐです」「いいよ」 - If impatient, show urgency with phrases like 「早くして」「まだ?」 - If regular user, mention familiarity like 「いつものところで」「いつもありがとう」 Remember to maintain consistency with your age, occupation, and speaking style throughout the conversation.""" prompt += f""" ## Output * `text`: The text of the user's response * `should_end_conversation`: Whether the user wants to end the conversation * `end_reason`: The reason why the user wants to end the conversation Always reply in JSON matching this schema: {json.dumps(UserResponse.model_json_schema(), ensure_ascii=False, indent=2)} """ この中で、genderやspeaking_speedといったいくつかの属性は、発話内容をText-to-Speech (TTS) APIで音声化する際にTTSのmodelや発話速度などの設定を調整するのに用いられます。 with_audioモード 次に、with_audioモードではまずタクシー配車のやり取りにおける会話の受け答えを以下のようなカテゴリに分類します。(以下はあくまでも一例で、実際の実装ではもっと細かい粒度でカテゴリを管理しています) <
この記事は newmo Advent Calendar 2025 15日目の記事です。 はじめに newmoでは2025年6月から求人サービス「newジョブ」の開発を進めており、その中でHeadless CMSとしてPayloadCMSを採用しました。 本記事では、約半年の本番運用を通じて得た知見を共有します。 newジョブ サイト・トップ なぜHeadless CMSが必要だったか jinzaiチームは少人数で構成されており、以下のような複数の用途に対応する必要がありました。 求人情報の管理・配信 SEO対策のための構造化データ管理 運用チームによるコンテンツ編集 将来的な機能拡張への対応 既存のSaaSでは要件を満たしきれず、カスタマイズ性の高いHeadless CMSが必要でした。 PayloadCMSとは PayloadCMSは、TypeScriptで書かれたオープンソースのHeadless CMSです。 https://payloadcms.com/use-cases/headless-cms 主な特徴 特徴 説明 TypeScript Native スキーマ定義から型が自動生成される Next.js統合 v3.0からApp Routerにネイティブ対応 セルフホスト可能 自社インフラでの運用が可能 柔軟なDB対応 PostgreSQL、MongoDB、SQLiteに対応 コード駆動 GUIではなくコードでスキーマを定義 本比較はnewジョブの要件に基づく用途適合の観点です。他用途では各CMSの強みが発揮され得ます。 Headless CMSの中での位置づけ PayloadCMSは「コードでCMSを定義する」アプローチを取っており、開発者にとっての自由度が高いのが特徴です。 2024-2025年の動向 2024年11月: PayloadCMS 3.0リリース - Next.js App Router統合 2025年6月: Figmaによる買収発表 現在: v3.64.0が最新(2025年12月時点) 買収後もオープンソースとして開発が継続されており、Figmaのデザインツールとの連携強化が期待されています。 参考: PayloadCMS 公式サイト Payload 3.0: The first CMS that installs directly into any Next.js app Payload is joining Figma! PayloadCMS選定の理由 複数のHeadless CMSを比較検討した結果、PayloadCMSを選定しました。 比較検討したCMS CMS 特徴 今回の用途における結論 PayloadCMS TypeScript native、Next.js統合 今回の用途で最適と判断(型安全性と拡張性) Strapi プラグインエコシステム充実 今回の要件に対する適合度は相対的に低い Sanity リアルタイム編集、GROQ 学習コストが今回の条件では高めと判断 microCMS 日本製、シンプル 将来の拡張要件への適合度が相対的に低い 本比較はnewジョブの要件に基づく用途適合の観点です。他用途では各CMSの強みが発揮され得ます。 決め手となったポイント 1. TypeScript Native スキーマ定義からフロントエンドまで一貫した型安全性を確保できます。コレクション定義がそのまま型定義になるため、型定義の二重管理が不要です。 2. Next.js App Router統合 PayloadCMS 3.0からNext.js App Routerにネイティブ対応しています。CMSがNext.jsアプリとして動作するため、カスタムページの追加が容易です。 3. 拡張性 CMSを越えて「管理画面基盤」として使える拡張性が決め手に。hookによる処理拡張や管理画面のカスタムが素直に実装でき、実運用で効きました。特に次の2点は生産性と将来の対応力に直結します。 REST/GraphQL APIの自動生成: スキーマ変更が型とAPIに自動伝播し、SSGビルドや外部連携の変更コストを抑制 認証機能の提供(管理画面とAPI): ロールベースの認可やAPIキー運用を内製せずに利用でき、セキュリティと開発速度を両立 実際に各CMSをinitializeして触った結果、PayloadCMSが最も柔軟に要件をカバーできると判断しました。 Figma買収とセルフホスト移行 予期せぬ方針転換 元々、運用負荷を下げるためPayload Cloud(PayloadCMS公式のホスティングサービス)を利用する予定でした。 しかし、リリース直前の2025年6月、FigmaによるPayloadCMS買収が発表され、Payload Cloudが一時的にクローズされることになりました。 参考: Payload is joining Figma! Cloud Runへの移行 急遽、Google CloudのCloud Runにセルフホストする方針に転換しました。 移行作業期間: 約1週間 タイミング: リリース前だったことが幸い 結果: 問題なく稼働開始 「どちらでも動く」という安心感 この経験から得られた重要な学びは、PayloadCMSが「どちらでも動く」設計になっているということです。 // payload.config.ts - 本番環境での設定 db: postgresAdapter({ pool: { connectionString: env.DATABASE_URI, }, push: false, // セルフホスト時は自動マイグレーション無効化 }), Payload CloudでもセルフホストでもPayloadCMS自体のコードは同じため、ホスティング先の変更に柔軟に対応できました。 monorepo統合の実際 newmoではpnpm workspaceを使った大規模monorepoでサービスを開発しています。PayloadCMSプロジェクトもこのmonorepoに統合しました。 共有できたもの 1. DBマイグレーション(Atlas) newmoではAtlasを使ってデータベーススキーマを管理しています。PayloadCMSのスキーマもこの仕組みに統合しました。 # PayloadCMSの設定からSQLスキーマを抽出 pnpm run --filter @newmo-app/payloadcms extract-schema # 出力先: server/component/newjob/db/postgres/schema.sql extract-schemaコマンドでは、以下の処理を行っています。 // 入力: コレクション定義 // collections/Tags.ts export const Tags: CollectionConfig = { slug: "tags", fields: [ { name: "name", type: "text", required: true }, { name: "order", type: "number", defaultValue: 0 }, { name: "isPublished", type: "checkbox", defaultValue: true }, { name: "color", type: "select", options: [ { label: "赤", value: "red" }, { label: <span class="synConstant
newmoのプロダクトチームで、AIエージェント活用開発トライアル(AI Agent Development Trial = AADT)を実施しました。1週間、チーム全員でClaude CodeやDevinを使ってコードを書くという取り組みです。 この記事では、なぜこのトライアルを行ったのか、何が分かったのか、そして今後どうしていくのかを紹介します。 なぜAADTを実施したのか AIエージェントの進化が速い。毎月のように新しいツールや機能が出てきて、「これを使えば生産性が上がる」という話を聞かない日はありません。しかもそのほとんどが今年リリースされたと思うとめまいがしそうです。 でも、実際に使ってみないと分からないことが多い。「本当に速くなるのか」「品質は大丈夫なのか」「どんなタスクに向いているのか」。使っている人の話を聞いても、自分たちのプロジェクトで同じ結果が出るとは限らない。 もう一つの懸念は、スキルの二極化でした。AIエージェントを使いこなせる人と、そうでない人の差が開いていく。早い段階で全員が触る機会を作らないと、この差はどんどん広がっていきます。 そこで、以下のテーマのもと、チーム全員でAIエージェントを使う期間を設けることにしました。 AIエージェントを活用したプロダクト開発フローをプロダクトチーム全員で実践し、これからの開発フロー見直しのきっかけを作る 「きっかけを作る」というのがポイントです。このトライアルで全てが解決するとは思っていません。でも、全員が同じ体験をすることで、「AIエージェントをどう活用していくか」という議論のスタートラインに立てる。それが狙いでした。 やったこと トライアルの概要 期間: 2025年6月30日〜7月4日(約1週間) 対象: プロダクトチーム全員 ソフトウェアエンジニア デザイナー PM QA エンジニアだけでなく、プロダクトに関わる全員を対象にしました。AIエージェントの可能性と限界を、チーム全体で体感することが目的だったからです。 レギュレーション ルールはシンプルにしました。 必ずコーディングエージェントを使ってコードを書く 使えるツールはClaude CodeとDevin。Devinは会社で契約していましたが、Claude Codeは少し工夫が必要でした。対象者に用途を限定したコーポレートカードを配布し、各自がClaude Maxプランを契約する形を取りました。 どうしてもエディタで直接書いた場合は事例を共有する 禁止するのではなく、「AIに向いていない作業」の知見を溜めることを重視しました。「ここはAIじゃ無理だった」という情報も、チームにとっては価値があります。 事前ヒアリング トライアルを始める前に、すでに同様のトライアルを実践している他社の方にヒアリングを行いました。先行事例から学ぶことで、「こういう問題が起きそう」「こう対策すると良さそう」という見通しを持ってトライアルに臨めました。ご協力をいただいたみなさん、その節はありがとうございました。 準備したもの いきなり「使え」と言われても困るので、オンボーディング資料を用意し、初日にはブリーフィングを行い、作業を開始できるところまでサポートを行いました。 Claude Code 事前準備 Claude Code 基本セットアップ Claude Code MCP設定 Claude Code メモリ機能(CLAUDE.md)設定 個人用CLAUDE.md テンプレート 特にCLAUDE.mdの設定は重要です。プロジェクト固有のルールや、個人の好みをAIに伝えることで、出力の質が変わってきます。テンプレートを用意しておくことで、設定のハードルを下げました。 期間中のサポート トライアル中は、専用のSlackチャンネルを作っていつでも相談できるようにしました。「こういうときどうすればいい?」「これって正常な動作?」といった疑問にすぐ答えられる体制です。 また、知見を共有できるNotionページも用意しました。「こうやったらうまくいった」「これは失敗した」といった事例を、みんなで書き込んでいく場所です。誰かの発見が別の誰かの助けになる、という流れが生まれました。 結果(数字で見てみる) トライアル終了後、参加者全員にアンケートを取りました。実施前と後でどう変わったのか、数字で見ていきます。 AI使用頻度の変化 トライアル前、「仕事でAIを毎日使っている」という人は85%でした。これだけでも多いなと思っていたのですが、トライアル後は様子が変わりました。 「今後も毎日使いたい」と答えた人が 96.4% に。「週に数回」がわずか3.6%で、「たまに」「使いたくない」は0%。ほぼ全員が毎日使いたいと思っています。 どのくらいAIにコードを書かせたいか もっと面白いのが「どのくらいAIにコードを書かせていますか?」という質問です。0%から100%で答えてもらいました。 実施前は回答がバラバラでした。20%や50%あたりに集まっていて、70%以上は35%程度。AI活用度には結構な個人差がありました。 実施後は分布がガラッと変わりました。0%〜30%の回答が 0% になり、70%以上に 81.8% が集中。特に80%(27.3%)と90%(22.7%)が多く、100%(すべてAIに任せたい)も15.9%いました。 強制的にAIを使う機会を設けたことで、「意外と任せられる」という実感が広がったのだと思います。 生産性は上がりそうか 「生産性は上がりそうですか?」という質問には、65.5%が「上がる」、25.5%が「上がるかも」と回答。合わせて91%がポジティブな見通しを持っています。 一方で「変わらない」「下がりそう」という回答も少数ありました。興味深かったのは自由回答で、「チャットだけに縛ると生産性が落ちるタスクも多いけれど、実コードを触りながら一部の処理をチャットでやってもらえば生産性が爆上がりする」という声がありました。 職種による満足度の違い 職種ごとのNPS(推奨度)を見ると、はっきりとした傾向が出ました。 満足度が高い職種: iOS: 約70 Android: 約60 バックエンド: 約45 満足度が低い職種: フロントエンド: 約-20 PM: 約-15 QA: 約-80 デザイナー: 0付近 モバイル開発者の満足度がとても高かったのは意外でした。当時のモデルの精度では、SwiftやKotlinの扱いに苦戦して満足度は下がるだろうと予想していたからです。実際には期待以上に使えたようです。 一方、QAやPMは厳しい評価でした。正直なところ、運営側としてもQA・PMの業務にどう組み込むかのイメージを持てておらず、丸投げ気味になってしまったところがあります。ここは反省点であり、今後の課題です。 また、フロントエンドが低くなってしまったのは、AIエージェントを使った開発フローが確立されていなかったために、アウトプットをなかなか出せずに苦戦したことが影響しています。 学び(ポジティブ・ネガティブ) 数字だけでは見えない部分があります。アンケートの自由回答から、良かった点と課題点を紹介します。 ポジティブな発見 生産性の向上 小〜中規模タスクやユニットテスト生成で「ほとんどコードを書かずに完了」できた 並列実行により「体感2〜3倍速い」「PR作成が早まった」 特にテスト生成との相性が良いです。テストケースを考えて書くのは地味に時間がかかるので、ここを任せられるのは大きいですね。 学習・オンボーディング効果 未経験者でも「強制的に使う機会」があったことで習熟が進んだ 非エンジニアも活用の可能性が見えた 組織全体で知見共有が進んだ 全員参加のトライアルにした意味がここにありました。普段なら触らなかったかもしれない人も、強制されることで一歩踏み出せました。 AIとの協働への期待 「AIなし開発には戻れない」 「人間は設計に集中できるようになる」 「今後も継続したい」 会社としてAI活用を推進する姿勢が伝わったのは良かったです。 ネガティブな発見 コード品質・精度への懸念 「品質が数%低下した気がする」 「大規模リファクタは厳しい」 「予期しない変更が入る」 コードベースが壊れるリスク AIの書いたコードをそのまま信用できないという声は多かったです。既存コードへの理解を要する作業では、意図しない変更の混入リスクがあります。AIの書いたコードの蓄積によって、メンテナンスが困難になるのではないか。レビュー負荷の増加で人間がボトルネックになるのでは、という心配もありました。 エンジニア以外の活用 エンジニア以外での活用がむずかしい NPSの結果とも一致しています。現状のAIエージェントはコーディング作業に特化していて、PM・QA・デザイナーの業務には直接適用しづらいです。 実測データ(当時) アンケートの声だけでなく、実際のPR数も計測しました。あくまで参考値ですが、傾向は見えてきます。 マージされたPR数 トライアル前週にプロダクトのリリースターゲットが重なり異常値となりましたが、トライアル週以降は通常範囲に戻りました。自動運転チームへの異動などで開発メンバーが減った中でもこれまでの水準を維持できており、1人あたりの生産性が落ちていないことを示しています。しかし、アンケートにあったような生産性が上がったという確たる根拠は数値からは得られませんでした。 1人あたりのPR数 開発メンバー数は減りましたが、1人あたりの中央値は大きく変わっていません。また、リリースターゲットだった前週の分散は非常に高く、一部のメンバーのPRが増えていたことも分かりました。 レビューに関するデータ レビュー完了までの時間はやや減少傾向にあり、平均コメント数も安定していました。PR数に変動があってもレビュー時間は悪化しておらず、むしろ改善傾向にあります。レビューに関する数値は悪化すると思われていたので少し意外でした。 見えてきた課題と対策 アンケート結果と実測データから、大きく3つの課題が見えてきました。それぞれの課題と、私たちが考えている対策を紹介します。 課題1: 知見共有 何が問題か AIエージェントをうまく扱うのは、新しいスキルです。 覚えることが多い。プロンプトの書き方、ツールの設定、タスク分割のコツ。それに加えて、これまでの開発習慣をアンラーンする必要もあります。「自分で書いたほうが早い」という感覚を捨てて、まずAIに任せてみる。これが意外と難しい。 さらに厄介なのは、進化の速度です。ツールやモデルが数ヶ月で大きく変わる。せっかく覚えたことが、すぐ古くなってしまう。 どう対策するか 必要な知識を減らすデフォルト設定 個人で設定を頑張らなくても、ある程度使える状態を作ります。CLAUDE.mdのテンプレートや、推奨設定をあらかじめ用意しておく。「とりあえずこれを入れておけばOK」という状態を目指します。 ベストプラクティスの収集 うまくいった事例、うまくいかなかった事例を継続的に集めます。「こういうタスクにはこう指示すると良い」「これはAIに任せないほうがいい」といった知見をチームで共有していく。 エンジニア以外での活用ノウハウの蓄積 現状はエンジニア向けの知見が中心ですが、PM・QA・デザイナーでの活用方法も探っていきます。 課題2: コード品質 何が問題か 生成されるコードの品質が足りない、という声がありました。 動くコードは書いてくれる。でも、既存のコードベースとの一貫性がなかったり、エッジケースの考慮が甘かったりする。そのまま使うと、後でメンテナンスが大変になるかもしれない。 もう一つの問題は、実装者のドメイン理解が薄れることです。AIへ任せっきりにすると、「なぜこうなっているのか」を理解しないままコードが増えていく。これは長期的なリスクです。 どう対策するか AI向けのガードレール整備 テストやLintを充実させます。AIが書いたコードでも、テストが通らなければマージできない。Lintで一貫性のないコードは弾かれる。機械的にチェックできる部分は機械に任せる、という考え方です。 AI向けルール設定の整備 CLAUDE.mdやプロジェクトのルールファイルを充実させます。「このプロジェクトではこう書く」「この命名規則を使う」といったルールをAIに伝えることで、一貫性を保ちやすくなります。 レビューの強化 これは次の課題とも関係しますが、AIが書いたコードこそしっかりレビューします。 課題3: レビュー負荷 何が問題か 「レビュー負荷が増えたように感じる」という声もありました。 PR数が増えれば、当然レビューの量も増えます。実測データではレビュー時間は悪化していませんでしたが、「特定の人に負荷が偏っている」という感覚はあるようです。 AIが書いたコードは、人間が書いたコードとは違う観点でのチェックが必要です。意図しない変更が混入していないか、既存の設計方針と合っているか。これまでと違う種類のレビューが求められます。 どう対策するか レビュールールの見直し 何をレビューで見るべきか、改めて整理します。AIが書いたコードと人間が書いたコードで、チェックポイントが違うかもしれない。 テクニカルなレビューを不要にする環境整備 スタイルやフォーマットのレビューは、LintやFormatterへ任せます。人間はドメインロジックに集中したレビューができる状態を目指す。「この変更は仕様を満たしているか」「この設計で問題ないか」といった点へ注力できるようにします。 PRサイズを極小にする工夫 大きなPRはレビューが大変です。AIを使うと一気に大量のコードを生成できてしま
はじめに newmoではタクシー配車アプリや自動運転に必要な測位技術など様々なサービスで位置情報を取り扱っています。今では当たり前のように使うGPSなどによる衛星測位技術ですが、人類の歴史的に見ると運用が始まったのはごく最近の話です。これまでにも人類は現在地および正確な時刻を知るために、挑戦を続けてきました。 この記事では、日時計の影から始まり、たった1分のズレが難破を意味した大航海時代の荒波を越え、冷戦下の熾烈な宇宙開発競争を駆け抜け、そして衛星技術へと至る、人類の探求の旅について解説したいと思います。 1. すべての始まり:人類はなぜ「正確な時間」を求め始めたのか? 人類の歴史は、常に「時間」をより正確に知ろうとする挑戦の歴史というと少し大げさですが、常にその時代時代で精度が求められていたことは確かです。その理由は、時間を認知/支配することが、社会を組織し、経済を発展させ、ひいては国家の運命を左右する戦略的な意味を持っていたからです。暦を作り農耕のリズムを掴むことから、グローバルな金融取引や自動運転を制御することまで、時間の精度に対する要求は時代と共に飛躍的に高まっていきました。 時代 目的 必要な精度 古代 暦の作成、農耕、宗教儀式など、自然のリズムを可視化する 日単位 中世 祈りの時間の告知、市場の開閉、仕事の開始・終了など、社会秩序を維持する 時単位 近現代 大航海、鉄道ダイヤ。科学と産業の発展に寄与する 分、秒 現代 科学実験、IT、自動運転など、空間と時間を一体的に制御する ナノ秒単位 この果てしない精度の追求は、素朴な発明から始まりました。 日時計: 紀元前5000年頃のエジプトで生まれた最古の時計の一つ。影の位置や長さで時刻を知ることができましたが、当然ながら夜や曇りの日には役に立ちませんでした。 水時計・燃焼時計: 太陽に頼らない方法として、一定の速度で水が流れ落ちる量や、ろうそくが燃える長さで経過時間を計る時計が発明されました。しかし、水は蒸発したり凍ったりする弱点があり、燃焼時計は燃料を燃やし続けなければなりませんでした。 砂時計: 14世紀頃に登場した砂時計は、画期的な発明でした。ガラス容器に砂を密閉することで、温度や湿度の影響を受けにくく、多少の揺れにも強いという特性を持っていました。 特に、この「揺れに強い」という特性が、砂時計を来るべき時代の重要なツールへと押し上げました。人類が活動の舞台を陸上から広大な海上へと移す「大航海時代」において、船の上で時間を計るための貴重な手段となったのです。 しかし、この砂時計でさえ、世界の海を支配するには全く不十分でした。人類は歴史上最も困難な問題の一つに直面することになります。 2. 最大の難問:「経度」がわからない! 大航海時代、船の位置を特定することは国家の経済と安全保障を左右する死活問題でした。特に「経度」、すなわち地球上での東西の位置を知ることは、当時の技術ではほぼ不可能とされていました。 1492年、コロンブスはアメリカ大陸に到達した際、そこをインドだと信じ込みました。これは地球を半周するほどの位置誤差であり、当時の航海がいかに不正確で、推測と運に頼った危険なものであったかを物語っています。 一方で、緯度(南北の位置)の計測は比較的簡単でした。太陽や北極星の高度を六分儀で測ることで、古くから正確な緯度を知る方法が確立されていたのです。 このため、当時の船乗りたちは「緯度航法」という非効率な手段に頼っていました。まず船をひたすら南か北へ進めて目的地の緯度まで到達し、そこから真東か真西へ転じて進むという、遠回りで危険な航海術です。これが、経度がわからないことによる絶望的な状況を打破するための、唯一の現実的な戦略だったのです。 しかし、経度(東西の位置)の計測は、「永久機関の創造」や「万能薬の開発」と並んで、「不可能の代名詞」とされるほどの難題でした。 その理由は、理論と現実の間に巨大な壁があったからです。 理論: 地球は1時間で15度自転します。この原理を利用すれば、経度は計算できます。例えば、太陽が最も高い位置にある正午に、自分の船に積んだ「出発地の時刻に合わせた特別な時計」が午前10時を指していたとします。この2時間の差は、地球の自転30度分に相当し、自分が出発地から東へ経度30度の地点にいることがわかるのです。時間の差をより細かく正確に測ることができれば、経度の計測精度も向上します。 課題: この理論を実行するには、「航海の激しい揺れや温度変化に耐え、長期間にわたって正確な時刻を刻み続ける時計」が不可欠でした。しかし、当時の最先端だった振り子時計でさえ、1日に10分以上もずれる代物で、全く使い物になりませんでした。 このため、当時の船乗りたちは、極めて原始的な方法に頼らざるを得ませんでした。船から丸太(ログ)を投げ込み、一定時間内に縄の結び目(ノット)がいくつ出ていくかを数えて船の速度を割り出すのです。この記録を書き留めたものが「ログブック(航海日誌)」であり、現代のIT用語「ログ」の語源となっています。しかし、潮の流れや風の影響で誤差は累積し、遭難事故が後を絶ちませんでした。 この絶望的な状況を打破するため、スペイン、オランダ、そしてイギリスといった海洋国家は、経度を正確に計測する方法に莫大な懸賞金をかけました。特にイギリスが1714年に制定した「経度法」では、最高で2万ポンド、現在の価値で約5.6億円もの賞金が約束され、国家的な問題解決への機運が一気に高まりました。 3. 一人の大工が300年の謎を解き明かす 歴史の難問に挑んだのは、ジョン・ハリソンという名の、本職が大工の男でした。彼は独学で時計作りを学び、当時の常識を根底から覆す技術革新を成し遂げます。それは、学問のエリートたちが頭を悩ませていた問題を、職人の知恵と執念で解決するという、壮大な物語の始まりでした。 ハリソンが開発した海洋クロノメーター「H1」から「H4」への進化の過程は、彼の人生そのものです。 H1 (1735年): 当時の高精度な時計とは、教会の塔に鎮座するような巨大なもので、その重さ自体が歯車を歪ませ、精度を狂わせる原因でした。その常識に対し、ハリソンは大工ならではの発想で、摩擦や温度変化に強い木製の歯車を使い、重さ34kgという、当時としては驚異的にポータブルな傑作を開発します。 H1 (引用:https://www.rmg.co.uk/collections/objects/rmgc-object-79139) H2, H3 (1739年-1757年): H1の成功に満足せず、ハリソンはさらなる改良を申し出ます。しかし、戦争の勃発などで洋上実験は延期され、不遇の時代が続きました。それでも彼は諦めず、17年もの歳月をかけてH3を完成させます。この時、ハリソンはすでに64歳になっていました。 H4 (1759年): H3までの大型モデルから一転、ハリソンは重さわずか1.45kgの懐中時計サイズの傑作「H4」を完成させます。その姿は、まさに未来からやってきたテクノロジーでした。1761年に行われたカリブ海までの81日間の航海実験では、誤差わずか5.1秒という、経度法の最高基準を遥かに上回る驚異的な精度を叩き出しました。 H4 (引用:https://www.rmg.co.uk/collections/objects/rmgc-object-79142) しかし、ハリソンの成功は、すんなりとは認められませんでした。ニュートンやハレーも名を連ねた経度委員会の中心は、天体の位置から経度を計算しようとする「天体観測派」の貴族やエリート学者たちでした。階級の低い職人であるハリソンの成功は、彼らの権威を脅かすものだったのです。 委員会は「その時計は1つしかない」「あなた以外が作れなければ意味がない」といった理不尽な要求を突きつけ、賞金の支払いを渋ります。これは単なる技術開発の物語ではなく、身分や既得権益との闘いでもありました。 最終的にハリソンは、国王ジョージ3世への直訴という手段によって、正当な評価を勝ち取ります。しかし、彼が賞金の全額を手にしたのは80歳の時。そのわずか3年後に、彼はこの世を去りました。 ハリソンの時計によって、人類は初めて「時間」を基準に地球上の「場所」を正確に知る術を手に入れました。そしてこの原理は、形を変えて現代の我々の生活を支えることになります。 4. 空からの革命:GPSの誕生 ハリソンの機械式時計が海を制覇してから約200年、技術は新たな次元へとパラダイムシフトを遂げます。主役は、歯車から「電波」と「人工衛星」へと移りました。その背景には、常に軍事的な要請がありました。 電波で位置を特定する試みは、地上から始まりました。 ロラン (LORAN): 1940年代に開発されたシステムで、複数の地上基地局から発信される電波の到達時間差を利用して位置を割り出しました。精度は数百メートルでしたが、電波の到達範囲が1,000km程度と限られていました。 オメガ (OMEGA): ロランをさらに強力にしたシステムで、電波の到達距離は1万kmを超えました。わずか8つの基地局で地球全域をカバーしましたが、精度は1,000m〜2,000m程度でした。事実、東京スカイツリー(634m)が完成するまで、日本で最も高い人工構造物は、長崎県にあった高さ455mの「対馬オメガ局」のアンテナだったのです。 そして、宇宙時代の幕開けが、測位技術に決定的な革命をもたらします。1957年、ソ連が世界初の人工衛星「スプートニク1号」を打ち上げると、アメリカは国家の威信をかけてその追跡に乗り出しました。この時利用されたのが、救急車のサイレンの音程が通り過ぎる際に変わって聞こえる現象でおなじみの「ドップラー効果」です。衛星から発信される電波の周波数の変化を観測することで、衛星の位置を特定できることがわかったのです。 研究者たちはすぐに気づきました。「衛星の位置がわかるなら、逆に、位置が正確にわかっている衛星からの電波を使えば、地上の自分の位置がわかるはずだ」と。この逆転の発想が、GPS (Global Positioning System) の誕生へと繋がりました。 GPSの基本原理および特徴は下記の通りです。 超高精度な時計: 各GPS衛星には、数ナノ秒(10億分の1秒)単位の精度を持つ「原子時計」が搭載されています。「1ナノ秒の時間のズレが、30cmの位置誤差になる」と言われるほど、時間の精度が位置の精度に直結しています。 4機以上の同時観測: 私たちの受信機が解くべき未知数は4つあります。1. 緯度(X)、2. 経度(Y)、3. 高度(Z)、そして何より重要なのが、4. 受信機自身の時計の誤差を補正するための正確な時刻(t)です。私たちのスマホの時計は衛星の原子時計ほど正確ではないため、この4つ目の変数(t)を解くために、4つの方程式、つまり4機目の衛星からの信号が必要となるのです。 軍事から民生へ: GPSは当初、アメリカの軍事システムであり、民間利用では意図的に精度が落とされていました(誤差100m)。しかし、2000年にその制限が解除されたことで、現在の数メートル〜十数メートルの精度が実現しました。日本でiPhoneが登場したのが2008年であることを考えると、私たちが当たり前のように高精度な位置情報を享受できるようになったのは、実はごく最近のことなのです。 なお、今日ではGPS(アメリカ)だけでなく、ロシアのGLONASSや日本の準天頂衛星「みちびき」など、各国の衛星測位システムを総称してGNSS (Global Navigation Satellite System) と呼ぶのが一般的です。 こうして、衛星から送られてくる正確な時刻信号が、私たちの現在地を教えてくれるようになりました。 5. 未来を創るセンチメートル級の精度へ 自動運転、スマート農業、ドローンによる測量──。こうした次世代の産業では、従来のGNSSが提供するメートル級の精度では全く不十分です。例えば、自動運転車が車線を正確に維持するためには、センチメートル級の超高精度な自己位置推定が不可欠となります。 この要求に応える代表的な技術が「RTK (Real-Time Kinematic)」です。これは、GNSS単体の限界を乗り越えるための仕組みです。 これを分かりやすく例えるなら、基準局は「自分の正確な座標を知り尽くした、固定点の測量士」のようなものです。この基準局は、あなたの移動局(自動運転車など)が受信しているのとほぼ同じ衛星信号を受信し、その日の大気状態などによる誤差を即座に解析します。簡単に言うと「今日の信号はすべて2ナノ秒遅れて届いている。これは60cmズレているな」といった具合です。そして、その具体的な補正情報を移動局(あなたの車)に無線で送信します。車はその補正値を自身の計算から差し引くことで、誤差を相殺し、センチメートル級の真の位置を割り出すことができるのです。 このRTK技術により、GNSSの精度は飛躍的に向上し、未来の様々なサービスを実現するための技術的な基盤が整いつつあります。 まとめ:時間と空間の精度が、未来の社会を支える 古代の暦作りから始まった人類の「時間」への探求は、大航海時代に「場所」を知るための鍵となり、そして現代、無数の人工衛星が飛び交う「GPS/GNSS」という形で私たちの生活に不可欠な社会インフラへと結実しました。 それは、たった一人で時代の常識に挑んだジョン・ハリソンの孤独な挑戦から、国家の威信をかけた巨大プロジェクトとしてのGPS開発、そして自動運転のような未来を支えるRTK技術まで、人類の「より正確に」という終わりのない情熱が、常に新しい時代を切り拓いてきた物語でもあります。 次に地図アプリを開くとき、その画面の裏に表示されるシンプルな青い点の背後には、何世紀にもわたる科学者や職人たちの知恵が詰まっていることを、少しだけ思い出してもらえたら幸いです。 newmo Engineering Advent Calendar 2025 written by @ayakix