selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

「ChatGPT/LangChainによるチャットシステム構築 」という書籍が素晴らしかったのでNode.jsでも書いてみた

はじめに

「ChatGPT/LangChainによるチャットシステム構築」 という本が素晴らしかったので、ちゃんと身につけるために Python だけじゃなくて Node.js でも動かしてみました。同じことをやろうとした人のために、ここにそのときの記録を残します。特に callbacksやmemoryについて、詳細に記載しようと思います。

書籍の説明につながるようなことはできる限り書きません!めっちゃ良書なので、ご興味持っていただけた方は購入してもらえますと 🙏

5章まではPython固有のToolを利用しており、6章の中身は7章とかなり近いところがあるので、7章のプログラムだけここに記載します。LangChainの学習に注力したいので、Serverelss Frameworkに関連するコードは省略しました。また、Momentoや @slack/bolt に関する説明はしません。

プログラムの全体像

今回作成したプログラムの全体像は下記のとおりです。

src
├── handler.ts
├── history.ts
└── index.ts
// index.ts
import bolt from "@slack/bolt";
import { ChatOpenAI } from "langchain/chat_models/openai";
import SlackBotCallbackHandler from "./handler";
import { createHistory } from "./history";

const { App } = bolt;

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  appToken: process.env.SLACK_APP_TOKEN,
  socketMode: true,
  port: 3000,
});

function CreateLLM(handler: SlackBotCallbackHandler) {
  return new ChatOpenAI({
    modelName: "gpt-4-1106-preview",
    temperature: 0,
    streaming: true,
    callbacks: [handler],
  });
}

app.event("app_mention", async ({ event, say }) => {
  const ts = event.thread_ts || event.ts;
  const botTypingMessage = await say({ thread_ts: ts, text: "Typing..." });
  if (!botTypingMessage.ts) return;

  /* create LLM Settings */
  const handler = new SlackBotCallbackHandler({
    channel: event.channel,
    ts: botTypingMessage.ts,
    app: app,
  });
  const llm = CreateLLM(handler);

  /* reply & add history */
  const histroy = await createHistory(event.channel);
  const userMessage = event.text.replace(/<@.*>/, "");
  await histroy.addUserMessage(userMessage);
  const aiMessage = await llm.predict(event.text.replace(/<@.*>/, ""));
  await histroy.addAIChatMessage(aiMessage);
});

(async () => {
  await app.start();
})();
// handler.ts
import { BaseCallbackHandler } from "langchain/callbacks";

type Props = {
  channel: string;
  ts: string;
  app: any;
};

export default class SlackBotCallbackHandler extends BaseCallbackHandler {
  name = "SlackBotCallbackHandler";

  private channel: string;
  private ts: string;
  private message: string = "";
  private lastTime: number = 0;
  private app: any;

  constructor({ channel, ts, app }: Props) {
    super();
    this.channel = channel;
    this.ts = ts;
    this.app = app;
  }

  handleLLMNewToken(token: string) {
    this.message = this.message + token;

    const currentTime = new Date().getTime();
    if (currentTime - this.lastTime > 2000 && token) {
      this.lastTime = currentTime;
      this.app.client.chat.update({
        channel: this.channel,
        ts: this.ts,
        text: this.message,
      });
    }
  }

  handleLLMEnd() {
    this.app.client.chat.update({
      channel: this.channel,
      ts: this.ts,
      text: this.message,
    });
  }
}
// history.ts
import { MomentoChatMessageHistory } from "langchain/stores/message/momento";
import {
  CacheClient,
  Configurations,
  CredentialProvider,
} from "@gomomento/sdk";

export function createHistory(sessionId: string) {
  return MomentoChatMessageHistory.fromProps({
    sessionId,
    cacheName: process.env.MOMENTO_CACHE as string,
    client: new CacheClient({
      configuration: Configurations.Laptop.v1(),
      credentialProvider: CredentialProvider.fromEnvironmentVariable({
        environmentVariableName: "MOMENTO_AUTH_TOKEN",
      }),
      defaultTtlSeconds: 60 * 60,
    }),
  });
}

LangChainの学習要素

7章において、callbacks と memory の2つの要素が出てきたので、この2点に注力して詳細を追っていこうと思います。

Callbacks

https://js.langchain.com/docs/modules/callbacks/

LangChainでは、Chain, LLM, Tool, Agent などのコンポーネントに対して Callbackを設定することができます。一番シンプルな使い方は下記のようになります。

// 公式より抜粋
import { OpenAI } from "langchain/llms/openai";
const model = new OpenAI({ maxTokens: 25, streaming: true });
const response = await model.call("Tell me a joke.", {
  callbacks: [{handleLLMNewToken(token: string){ console.log({ token })}},
});

上記のケースでは、LLMが新しい token を受け取ったときに console.log({token}) の処理を実行します。この機能を使えばリアルタイムに出力を表示したり、様々なlogを仕込んだりすることができるでしょう。今回のケースでは、下記のように新しい token を取得した際にメッセージを更新しています。

// 簡単のために一部省略
  handleLLMNewToken(token: string) {
    this.message = this.message + token;
    this.app.client.chat.update({
      channel: this.channel,
      ts: this.ts,
      text: this.message,
    });
  }

LangChain本体でのCallbackの利用

CallbackはLangChain本体でも使われています。例えば verboseオプションなどはConsoleCallbackHandlerを仕込んでいます。

// https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/callbacks/manager.ts#L483
if (verboseEnabled || tracingEnabled) {
  if (!callbackManager) {
    callbackManager = new CallbackManager();
  }
  if (
    verboseEnabled &&
    !callbackManager.handlers.some(
      (handler) => handler.name === ConsoleCallbackHandler.prototype.name
    )
  ) {
    const consoleHandler = new ConsoleCallbackHandler();
    callbackManager.addHandler(consoleHandler, true);
  }

ConsoleCallbackHandlerのプログラムを一部抜粋したものがこちらです。console.logにて入力した情報などを出力している処理が見て取れます。

// https://github.com/langchain-ai/langchainjs/blob/main/langchain-core/src/tracers/console.ts#L154
onLLMStart(run: Run) {
  const crumbs = this.getBreadcrumbs(run);
  const inputs =
    "prompts" in run.inputs
      ? { prompts: (run.inputs.prompts as string[]).map((p) => p.trim()) }
      : run.inputs;
  console.log(
    `${wrap(
      color.green,
      "[llm/start]"
    )} [${crumbs}] Entering LLM run with input: ${tryJsonStringify(
      inputs,
      "[inputs]"
    )}`
  );
}

Pythonの方は様々なcallbackのintegrationsがありそうですが、Node.js の方はあんまりなそう?(勘違いだったらすみません!)なので、足りなければ基本的に自作する必要がありそうです。

Memory

https://js.langchain.com/docs/modules/memory/

LangChainのMemoryは、過去の会話を覚えておくためのモジュールです。

公式の一番シンプルな例を持ってくると下記のようになります。

import { ChatMessageHistory } from "langchain/memory";
const history = new ChatMessageHistory();
await history.addUserMessage("Hi!");
await history.addAIChatMessage("What's up?");
const messages = await history.getMessages();

今回のコードでは下記のように利用しています。

// history.ts
import { MomentoChatMessageHistory } from "langchain/stores/message/momento";
import {
  CacheClient,
  Configurations,
  CredentialProvider,
} from "@gomomento/sdk";

export function createHistory(sessionId: string) {
  return MomentoChatMessageHistory.fromProps({
    sessionId,
    cacheName: process.env.MOMENTO_CACHE as string,
    client: new CacheClient({
      configuration: Configurations.Laptop.v1(),
      credentialProvider: CredentialProvider.fromEnvironmentVariable({
        environmentVariableName: "MOMENTO_AUTH_TOKEN",
      }),
      defaultTtlSeconds: 60 * 60,
    }),
  });
}

7章においては、LLMの回答の生成にhistoryが活用されていないため、どういったアウトカムにつながるのかがイメージができないと思います。ぱっと、公式ドキュメントを読んだ感想としては、Prompt, Memory, History の役割は、RDBMSにおけるクエリー、クエリエンジン、テーブルっぽいという印象を受けました。そのあたりの詳細は8章にて、ちゃんと調査してまとめようと思います。

LangChainjsのライブラリにおけるhistory実装の仕組み

ライブラリを読んでみたところ実装は下記のようになっていました。 少し前のプログラムでclientにmomentoのCacheClientを渡していたので、 LangChainjsにおいてはmomentoのうすーいwrapperのような役割のみとなっていることが見て取れると思います。

// https://github.com/langchain-ai/langchainjs/blob/main/langchain/src/stores/message/momento.ts
  /**
   * Adds a message to the cache.
   * @param message The BaseMessage instance to add to the cache.
   * @returns A Promise that resolves when the message has been added.
   */
  public async addMessage(message: BaseMessage): Promise<void> {
    const messageToAdd = JSON.stringify(
      mapChatMessagesToStoredMessages([message])[0]
    );

    const pushResponse = await this.client.listPushBack(
      this.cacheName,
      this.sessionId,
      messageToAdd,
      { ttl: this.sessionTtl }
    );
    if (pushResponse instanceof CacheListPushBack.Success) {
      // pass
    } else if (pushResponse instanceof CacheListPushBack.Error) {
      throw pushResponse.innerException();
    } else {
      throw new Error(`Unknown response type: ${pushResponse.toString()}`);
    }
  }

腕前で生きてるとWHYよりもHOWに執着しがち

はじめに

この投稿はポエムです。 技術的な記事を期待してる人はブラウザバックお願いします! なおこの文書は諸々の理由により、一部ぼかしたところがありますがご容赦ください。

TL;DR

  • 有名になった方法論は、目的が忘れられて、手段だけ広がることがある
  • 手段が「守らねばならぬルール」になって目的を達成できないことがよくある
  • 何か手法を取り入れるときは手段が目的化してないか気をつけねば

本文

先日、勉強会にて、とあるベンチャー企業の営業職の方とちょっと話す機会がありました。 その方は最近営業チームのマネージャーになられたとのことで、組織設計のお話をされまして「The Model的に営業のプロセスを細かく分割し、分業を進めて、KPIで成果を追えるようにした。現在は分業のメリットを感じている」 「The Model は The Goal を参考にしているとのことなので、ソフトウェアエンジニアの人も使える知見があるかも知れませんね」と言われました。

そのとき「あれ?」ってなりました。自分の記憶だとThe Goalは、生産プロセス全体をチェーンのようにつなげ、ボトルネックを発見し、 その改善に注力することでスループットを上げることを目的とする方法論だったからです。 なので、聞いた方法は The Goalの目的を達成するための方法論とは思えませんでした。 むしろ全員でボトルネックの業務をやる(もちろん仕組み・ツールで解決するまでは)方が、The Goalの目的に適うと思ったのです。 ネットで記事を漁ってみても分業に関する記事ばかり目に入るので、僕の理解に不安を覚えました。

インサイドセールスが自身のKPIを追い求めて熱量が低い顧客までフィールドセールスに渡す。フィールドセールスが無茶な獲得をした結果、カスタマーサクセスの対応コストが増加する。 全体最適化の仕組みを設けない分業は、このような問題を引き起こすことはよく知られてると思います。 そのため、The Goalを参考にした書籍が、主なメッセージに分業を置くとはどうしても思えませんでした。

違和感を感じて自分も書籍を読んだところ、なるほどーってなりました。

僕はThe Modelの最大の目的を「営業のプロセスを明確なステージに分割し、マーケティングのようにファネル分析を利用してボトルネックを特定・解決する」と読み取りました。 しかし、書籍の多くの部分で、分業による達成手法が重点的に説明されているので、「分業ってこんな良いことあるんだー」って情報を持ち帰った人が多いのだろうなと思いました。

同じようなことは、ソフトウェアエンジニアの「ドメイン駆動開発」でも経験しました。 あるベンチャー企業のビジネス職の方から、開発が大きく遅延してるので相談に乗って欲しいと言われ、ソフトウェアエンジニアの方にお話を聞いたときのことです。 「このプロジェクトはDDDで作るのが良いと思うので、こういう設計で開発します」と言われ見せられたのが、 ユビキタス言語やドメインモデルではなく、大量のService、Entity、Value Object、Domainなどのクラス群と、 「こういうときは Service ディレクトリにコードを置く。」などの大量のルールでした。 彼らが開発していたシステムはフロントもバックエンドもNext.js のみで作られており、 オブジェクトを加工せずに表示するのみのシンプルなシステムでした。 そこまで複雑なビジネスロジックでもなく、ドメインエキスパートと認識の共有もしないなかで、 DDDの戦術的設計が必要な理由がちょっと思いつきませんでした。

双方のケースを見て、なんでこんな不幸なことが発生したのかと考え、僕なりに出した仮説が双方HOWに囚われていた、ということです。 前者の営業マネージャーの方のケースだと、最近マネージャーになられたとのことなので、「マネージャーらしい仕事をしよう」と考え、組織的な管理方法というHOWに囚われたのかも知れません。 後者のエンジニアの方のケースだと、我々エンジニアの多くは技術力というHOWを対価にお金を貰っているお仕事なので、どうしてもHOWにとらわれがちなのだと思います。

自分もアジャイルに出会い、広めようとしたときに「守破離だ!」と言って同じミスをしました。 なので新しい手法を取り入れようとするときは、こういう問題に陥ってないかは意識したいなーって思った次第です。

OCR用のSaaSをアレコレ調査してみた

TL;DR

  • ちょっとプライベートでOCRを使ってみたかった
  • SaaS決定の判断基準は下記
    • 精度、費用、開発のしやすさ ( ライブラリの品質、インフラ設定の工数など )
  • 選定対象のSaaSは次の通り

選定基準

OCRの精度

サンプル4パターン手動で作成し、 OCR SaaSに読み込ませ、正しく読み込めていることを確認する。 ※ 実際に利用したサンプルは非公開です!

開発のしやすさ

OCR SaaSへ読み込ませる際は、GUIからではなくプログラムを作成して実行する。 その過程でOCRする際に必要な準備や、公式ドキュメントの充実、またTypeScriptで提供される型のクオリティなどを確認する

結果

Microsoft Form Recognizer を採用した。 結果を表にまとめたものを下記に記載する

翻訳精度 開発のしやすさ 費用
Microsoft Form Recognizer ○ ( ¥0.2 / 1 page )
Cloud Vision ○ (¥0.2 / 1 page )
Adobe PDF Service ✗ ( ¥6 / 1 page )

検証

Microsoft Form Recognizer

翻訳精度: ○

4パターンのPDFをすべてOCRしたが、一つも誤りはなかった

開発のしやすさ: ○

公式のライブラリはTypeScriptのサポートをしており、説明が十分で迷いなく実装できる。

費用: ¥0.2 / 1 page

価格 - Form Recognizer API | Microsoft Azure

Cloud Vision

翻訳精度: △

  • 罫線を | と読み取ってしまう
  • 1ヶ月 などの文字を 15月 と読み取る
  • 一部の文書を中国語と判断し、誤った文書化をする ( 再現性100% )

開発のしやすさ: △

  • pdf を読み込む際は、gcs(GCPのs3)を利用しなければならない
  • gcsで読み込んだ結果は gcs に出力される
  • png であれば、gcs を用いずにOCR可能
    • そのため利用するならば 見積書をs3だけでなくgcsにもコピーするか
    • pdfをpngに変換しなければならない
  • TypeScriptサポートが貧弱

参考資料: https://cloud.google.com/vision/docs/pdf?hl=ja

コスト: 1000ページあたり ¥206.19

料金 | Cloud Vision API | Google Cloud

Adobe Extract API

翻訳精度: ○

4パターンのPDFをすべてOCRしたが、一つも誤りはなかった

開発のしやすさ: ✗

  • PDFのままOCRができる
  • 出力は Zip で圧縮されてしまう
    • ※ 他のフォーマットもあるかも知れないが、ドキュメントが見つからず…
  • TypeScriptの対応がされておらず、開発者向けのドキュメントも貧弱
  • ちゃんと開発しようと思ったら直接ライブラリのコードをしっかり読み込むしかない

Untitled

コスト: 1ページあたり: ¥6.87

PDF Services API Licensing

【PO編】PO&SMをやめるので過去の学びをアレコレ書いてく

背景

  • 2年間くらい PO & Scrum Master とソフトウェアエンジニアを兼務したり色々あった
  • この度、ソフトウェアエンジニアに集中できることになった
  • せっかくなので、忘れる前に ( もうたくさん忘れたけど ) 書き出すことにした
  • この記事は、未来の自分が思い出すための記事である

全般

絶対に兼務しない

  • これだけ兼務まつりしてて言うのもあれだが
  • POに集中する期間は、SMとエンジニアとしての貯金が減ってるのを感じ
  • エンジニアに集中する期間は、POやエンジニアとしての貯金が減ってるのを実感する
  • 兼務状態は、常に何かしらの貯金が減る不安を抱えながら仕事することになる
  • スーパーマンでない限り、1つ1つのクオリティが落ちてしまい、よしんばなんとかなっても「なんとか破綻しない」程度になる
  • うまくいったとしても9割のケースは、誰かが荷物を持ってくれたから
  • なので 成功ケースだけを見て「兼務いけるやん」と思ってはいけない
  • 自分がうまくいった経験があったとしても「誰かのフォロワーシップに助けられただけ」 という意識を忘れてはいけない

Product Management 領域

仕事のほとんどは組織のチェンジマネジメント

  • Product Management の仕事。と考えると、顧客ヒアリングをして、市場調査して、仮説建てて、プロトタイプ作る。そんな感じの面白い仕事を想像してしまう。
  • だけど、それは仕事の中のほんの一部でしかない
  • プロダクトが変えるのは、顧客の業務だけでない。自組織の仕事の仕方も変わる
  • 顧客・自組織を問わず、業務のASIS/TOBEを描き、変化の過程を設計し、信頼を獲得し、より良い価値を提供するために変化させていく
  • 現状を適切に把握する上で、自ら営業として現地に行ったり、CSとして電話を取ったりすることもある
  • いつも慣れない領域に飛び込まねばならないので、ストレスフルなお仕事になる
  • けど、コンフォートゾーンにいるだけではまともな結果は出せないのでやるしかない

新しいソフトウェアじゃなくて新しい業務を作る

ベストプラクティスはWeb系企業で育まれている〜三菱重工業のDXを支える学びの姿勢〜 川口賢太郎、森岡周平(三菱重工業株式会社) | ITエンジニア向けのトレンド情報

仕事の大部分でPCを使わないような人達に、ソフトウェアの力で価値を届けていくという難しさがあると感じています。ただ、それこそとても挑戦しがいのある課題だと捉えています。

  • 昔、インタビューの中でこんなことを言った
  • これは今でも重要だと思っている
  • 顧客とソフトウェアのタッチポイントが多ければ多いほど、作る機能が増える
  • コード量、バグの可能性、顧客の負担、不満を言われる可能性も増える
  • ドリルと穴の話で、最高のドリルをつくるばかりに意識を向けず
  • 「穴を最小のコストで開ける」ってことに意識を向けたい
  • 顧客の利益を最大化しつつ、サービスとのタッチポイントを最小化することが重要
    • 例えば画面作らなくても、メールで済むものはそれで終わらせるとか
  • そのために、顧客の既存業務をしっかり理解し、ASIS/TOBEを描き、TOBEに至るまでの過程と、その都度のサービスのあり方を考えることが重要

ニーズが表面化するまで解決しない。でも準備はする

  • 「データが1万件になったら整理が必要なのでこの機能作りたい」
  • みたいな話が出ることがある
  • その時期は半年後だったり1年後だったりする
  • 今必要ない or ビジョン実現にMustでない機能は絶対に作らない を徹底しろ
  • 必要になったときの方が情報が増えてて適切な判断をできるだろうし
  • 今解くべき課題を確実に解くためにチームのキャパシティを割きたいし
  • 今、その課題に直面していないユーザーから納得感を得られないからだ
  • 前もって準備しても、課題に直面していないユーザーからは不満の声しか出てこない
  • 機能は作るよりも削るほうが難しい
  • Nice to Haveな機能を作ることに、なに1つ良いことはない
  • ステークホルダーの一時的な欲求を満たすだけだ
  • その代わり、課題が表面化したときに迅速に対応できる体制はつくっておく

PSFit完了まで人を増やさない

  • 一般的な会社において、人を遊ばせることに罪悪感を覚えない人はいない
  • だが、PSFitまでのフェーズにおいて、ひとはそんなに必要ない
  • PO、営業、デザイナ、場合によってマーケター (or コンサル) がいればなんとかなる
  • それ以上の人がチームにいたとき、POはそれらの人の仕事をつくることが求められる
  • またPOはステークホルダーの不安マネジメントも求められる
  • 結果、なかなかPSFitに集中できずズルズル余計なコストが嵩んでいく
  • ただし、各々が自走できる場合、不安マネジメントが不要な場合は人がいても良い
  • 開発チームは空気を読んで必要そうな機能の実験・検証・学習をしてくれてたりすると最高のスタートを切れたりする

ホームランを狙うか、アウトを避けるか決めておく

  • プロダクト開始時に、どちらのスタンスを取るか決める
  • ホームランを狙うなら、積極的にリスクを取れ
  • アウトを避けるのであれば、不要なリスクを取るな
  • コントロールしやすいリスクは、極力安全に倒せ
    • 人の採用・異動: 安易に人を増やすな。変えるな。もっと厳しい状態になることが多いぞ
    • 利用技術の選定: 社内で実績ある技術を採用する
    • ステークホルダーからシステムへの期待
      • プロダクト開始当初にちゃんと伝える。合意できなければそもそもやらない
    • 我々からステークホルダーへの期待
      • 極力期待しない。期待するときは何を期待するか。どうすれば実現できるか明示する
  • POとして意思決定する際は、何のリスクを取ってるか常に意識しろ

様々な道具で具体・抽象のアウトプットし続ける

  • 具体と抽象を行き来しなければ、有用な発見は得られない
  • 考えるだけでは意味がない。手を動かせ。手で考えろ
  • 抽象であたりをつけ、具体で検証しろ
  • 具体で違うとなったら、また抽象にもどれ
  • 例として、下記のような成果物が考えられる
  • 煮詰まったらDesign Sprint を頻繁に行っても良いだろう

デザイン

  • 具体的なアウトプットイメージは上記である
  • 1週間で1往復くらいがちょうど良い

資料作りを絶対サボらない

  • 顧客に説明に向かうとき、説明資料を持っていくことがある
  • 社内のステークホルダーに協力を依頼するとき、資料をつくることがある
  • この資料は絶対にサボってはいけない
  • 顧客においては、上司にシステム導入の話をする際に、自分が作った説明資料を使うことがあるし
  • 社内のステークホルダーも、周りに協力を得る際にその資料を使うことがある
  • PO自身が作った資料が、どんどん独り歩きしていく
  • そのため、明確で分かりやすい言葉を使い、多くの人が理解できるように、懇切丁寧な渾身の資料を作ることを常に意識することが大切

意思決定を人に委ねるな

  • プロダクトオーナーはプロダクトの意思決定者
  • 偉い人アドバイスをくれるだろうが、短絡的に飛びついてはいけない
  • アドバイスをくれた人に対して、自分が現状を100%伝えられてない可能がある
  • その場合のアドバイスは、必ずしも正しいとは限らない
  • 自分の中でロジックが説明できないことは、誰が言ったことでも通しちゃいけない
  • ステークホルダーがどんなにイヤと言ってもやらなければいけないこともある
  • 誰かに恨まれても、やるべきと思ったらやるという意志を持つ
  • そのとき、説明だけはちゃんとする。伝わらなくても、全力で伝える努力をする

最後に

  • 他にもいっぱい学びがあった気がするが... 今すぐ思い出せるのはこれくらい...
  • 思い出したら追記する

チームみんなが参加しやすくなるIaC設計の工夫

モチベーション

私は「インフラ」という言葉は、人によって受け取り方が大き違う言葉だと思ってます。 「よく分からないけど難しそうなので出来れば触りたくない」という人や、「ごく小数の人にしか触らせたくない」という人、 「IaaS、PaaSはインフラと認めない!」という人にも会ったことがあります。

「よく分からないなんとなく難しいもの」である「インフラ」は、多くの人に敬遠されがちなので、 中小規模の開発組織では1人のエンジニアがひっそりと孤独に作ってるケースがままあるのではないでしょうか。 私もやりたがる人があまりいないので、PMとインフラエンジニアを兼務することが多くありました。

この記事では、一人でひっそりとインフラを作ってた私が、触れる人を増やしたいと思って工夫したIaC設計のアイデアを記述しようと思います。

言葉の定義

インフラ

IaCで扱う「インフラ」の定義について。 言葉の定義を探していくつか書籍を漁ってみましたが、なかなかコレといった文書を見つけられませんでした。

OSを作るようなソフトウェアエンジニアからすればPCそのものがインフラですし、 Webアプリケーションをつくるソフトウェアエンジニアは、AWSGCPなどクラウド上で構築された、サーバー、ネットワーク、 ストレージなどのコンピューティングリソースをインフラと呼びがちです。 より一般向けの記事を読むと、社内のソフトウェアエンジニアが作成した管理画面や社内のCRMなどをITインフラと呼ぶこともあるようです。

ここでは「インフラ」という言葉の定義を、便宜的に「ソフトウェア開発に利用する、IaCで管理できるものすべて」とさせていただこうと思います。 なので、AWS等で構築するネットワークやDBだけでなく、エラー監視用のSentry、パフォーマンス監視に使うNewRelic、CI/CDとして利用するGitHub Actions、 その他様々なサービス群をインフラと呼ばせていただきます。

インフラのスタック

数々存在しているインフラリソースを任意のまとまりを、この記事では参考にした書籍にならって「スタック」と呼ばせていただきます。 この概念はCloudFormationのStackから来ており、スタックで管理されているリソースは一緒にデプロイされます。

設計について

設計の方針

私は「安全性」を最優先、「開発効率の維持」を2番めの優先事項としてIaCを設計します。

「安全性」は下記2点によって実現します

  • 設定の再現性・冪等性を担保するため、多少開発工数が掛かっても、できる限りのクラウドリソースをコード化
  • 障害発生時の影響範囲を限定的にするため、開発効率が多少下がっても初期からスタックを分割し、軽量なスタックを維持

「開発効率の維持」は下記2点によって実現します

  • 役割・技術要素軸でスタックを分割し、スタックを更新する上で必要な学習コストを軽減
  • スタックを小さく保ち、インフラタスクを複数人が並行して着手できる状態を維持

スタックの分割

上図のように、インフラを複数のスタックに分割します。

ここで簡単にいくつかのスタックについて説明します。 Appは、AWSなどクラウド上で構築しているネットワークやDB、バックエンドのサーバーを含む最も巨大なスタックです。 Frontは、フロントエンドのアプリケーションが構築されるスタックになります。Vercelなどの設定をこのスタックに記述することになります。 Monitoringは、監視に纏わる諸々を管理するスタックです。DatadogやCloudWatch等のインフラを管理します。

スタックは、ソフトウェアにおけるコンポーネントと同様の基準で分割しています。

再利用・リリース等価の原則 に従い、利用される単位とリリースの単位は等価になっており。 閉鎖性共通の原則 に従い、同じ理由、同じタイミングで変更されるものをまとめており。 非循環依存関係の原則 に従い、スタックの依存グラフは循環しない(必ず下の方向にのみ依存させる)ようになっています。

分割による狙い

分割の狙いは3つあります。

1つは 「変更容易性の確保」 です。 長大なスタックは把握しなければならないコード量と、変更による影響範囲が大きくなります。 すると、多くの人は怖がって触らないようになり、一部のエンジニアがお腹を痛めながら必要な修正をし続けることになります。 スタックを小さく保つことで、把握しなければならないコード量と変更による影響範囲を小さくし、高い頻度で継続して更新し続けられる状態を保ちます。

2つめは 「開発効率の維持」 です。 長大な1つのスタックは共同編集することができません。 インフラの変更を要する機能開発の要望が増えたとき、スタックが1つでは1人のエンジニアしか開発に参加できなくなります。 開発のボトルネックとなり、開発効率の低下を招きます。

3つめは 「開発組織の冗長性を高める」 ことです。 1つめの狙いに重なりますが、スタックの目的や影響範囲を限定することで、必然そのスタックに纏わる機能開発をする上で必要な前提知識も限定されます。 そうすることで開発に参加するための敷居を下げ、多くのメンバーがインフラの開発に参加できるような環境を整えます。

スタックの開発に参加する上で必要な知識の学習コストと、スタックを壊してしまったとき復旧にかかる時間をマッピングした図が下記のようになります。

右上にないものは学習コストが低い。 もしくは壊してもダメージ少ないものです。 それらはチームメンバーが開発に参加する上で心理的ハードルも低くなります。 そのため「初めての人は左下にあるものから入って、徐々に領域を広げて貰う」のような成長設計をしやすくなります。

IaCで採用するツールについて

私個人としては、スタックが適切に分割されていること。スタック間の依存が把握できていること。 この2点さえ守れるならば、IaCツールは何でも良いしと思っています。 どれか一つに統一する!と考えすぎず、各々のスタックの管理に適した(道具としての性質だけでなく、開発者の親和性含め) IaCツールを利用すれば良いのではないでしょうか。

私一人で利用する場合は、スタック間の依存関係を把握しやすく、リファクタリングも容易という理由でcdkを好んで使いますが、 下記の記事にも書きましたが terraformと比較すると cdk はそれなりに学習コストはあります。 なので、そこはチーム内で話し合ってチームに適したものを選べると良いかなと思います。

selmertsx.hatenablog.com

新しい技術を検討する際、PdMとして事前に決めておくこと

この資料の目的

自分のキャリアを振り返ってみると、PdMとソフトウェアエンジニア業を兼務することが多くありました。PdMとして業務していくなかで、エンジニアへのお願いの仕方を間違えて、想定よりも多く時間が掛かってしまうこともありました。その問題は、特に新しい技術を使って課題を解決しようとするときによくありました。

この記事では、新しい技術を試す際、現場で良くみるムダな仕事が発生しちゃってるケースを例に、そうならないよう自分が意識しているお願いの仕方を言語化してみようと思います。

よく見る良くない依頼の仕方

ケース1

  • PdM: 最近、XX という技術をよく聞くんだけどさ、あれでうちのサービスも良く出来ない?
  • エンジニア: うーん、とりあえず検証してみるんで、何ができたら嬉しいか教えてください
  • PdM: 何ができるか分からないからなー。とりあえず試してみてよ。良さそうだったらどう使うか考えるからさ。
  • エンジニア: やってみました!こういう結果になりました!
  • PdM: なるほどー、ありがとう!仕事で使いそうなことがあれば何か提案するわー。

ケース2

  • PdM: 最近、機械学習で不良品を見つける、ってことを他社でやってるらしいじゃん。うちの部署でもやってみたいからちょっと調べてみてよ
  • エンジニア: やってみました!僕たちの生産ラインで試したら、検知精度は75%みたいです
  • PdM: ちょっとうちの仕事では使えなそうだね〜。ありがとう

なんでこうなっちゃうの?

双方、エンジニアがあれこれ調査してくれましたが、成果につながらない、とても残念な結果になってしまいました。ちょっと極端に描いてしまいましたが、程度の差こそあれよく見る光景ではないでしょうか。( 僕は経験があります )

これらの問題は、扱おうとしている技術がどういうものか、そもそも何を解決したいのか、双方の解像度が低いときによく見られます。技術に関する理解が深ければ解くべき課題は定めやすいので混乱は生まれにくいですが、技術の理解が浅く、課題定義の能力も欠けていると現場はものすごいカオスになります。

そんなカオスな状態は下記の図の緑の状態にあります。こんな状況では「課題」というのもおこがましいので「何か」って言います。

これを望ましい状態に持っていくためには、「技術 」と「課題」の双方から確実性を高めていくアプローチが考えられますが、初手は「課題」の解像度を上げるべきです。たいていのケースで「課題」の解像度を上げるほうがコストが低いからです。

対処方法

ケース2で、PdMがもう少し状況を深く掘り下げ、下記の情報が分かっていたとします。

  • 出荷時の不良品は0.08%であり、これを目標値とする
  • 生産時の不良品は0.5%
  • 現在採用している不良品の検知システムは偽陰性が1%ある
  • そのため、最終的には人の目ですべて確認している
  • 確認の際は、月ウン百万の人件費が掛かっている
  • 不良品の見極めはルールベースで定められず
  • 見極めには深い知識と経験を持つ工員が必要になる
  • 現場は高齢化が進んでおり、恒常的に人を確保することが難しい
  • 熟練工の確保や、また業務時間の増加による従業員の不満増加という面で、事業継続リスクを抱えている
  • なので「検査工数の削減」「検査の簡易化」などの対処が求められる

※ 設定は新聞で読んだ事例を参考にしています。ので、現場の人から見ると間違いだらけかもですがご容赦ください

ここまで把握してれば、エンジニアに仕事を依頼する前に、下記のような意思決定をできるはずです。

意思決定を事前に下した場合、PdMからエンジニアへの依頼は下記のようになるでしょう。

  • 機械学習による不良品の検知を試して欲しい
  • 目的は不良品検査の工数削減
  • 精度が十分に高ければ、不良品検知の完全な自動化を検討するし
  • 精度が十分でなくとも偽陰性が基準値未満なら検査の工数を十分に下げられるはず。
  • なので、「精度」「偽陰性」「カットオフ値の調整有無」の3つの観点で調査してみて

そうすればエンジニアから、下記のような回答が得られるかも知れません。

  • 不良品の検知精度は75%程度なので、基準値に対して未達です
  • でも、偽陰性はx%でした
  • カットオフ値を調整すると精度は70%に下がりましたが、偽陰性は基準値を下回りました
  • 不良品と判断した製品をのみを抽出した場合、検査する部品数を大幅に下げられそうです
  • このシステムを導入したら、不良品検査に掛かる工数が30%程度下がる見込みです
  • ということでやっていきましょう!

こんな感じで、仕事を依頼する前に色々ちゃんと考えておくと、良い成果につながりやすいです。

さいごに

多くの人がアイデアを考える際、課題と解決策のセットを1つのアイデアとして捉えている。しかしアイデアは本来、課題と解決策に分離可能である。思いついたアイデアは一見きれいにまとまっているように見え、発想した本人も思い入れが強くなっていることも多いが、アイデア自体を課題と解決策に分離してそれぞれを検討することでさらに発想が広がることがある。 「プロダクトマネジメントのすべて」より引用。

ソフトウェア開発の経験が長いと、「課題と解決策を分けるなんて、当たり前じゃないか」と感じます。でも、意外と、驚くほどに、それらを混同して話し合ってしまっていることがあります。「うちのチームはそんなことないよ!」ってチームの場合は、きっと優秀な誰かが事前に整理してくれてるから。もしくは、チームメンバー全員が高いレベルにあって、議論が混ざらないからだと思います。誰かがそこらへん混同し始めると、意図せずみんなで混同し始めることが多くあります。そんなときに「あれ、なんかおかしいぞ」って気づいて、話を戻そうよって言える人は珍しいでしょう。ということで、PdMは「課題」と「解決策」を分け、「解決策」も複数パターン考慮した上でエンジニアに依頼すると、そこらへん混同せずに議論でき、業務がスムーズに進みます。

そもそもエンジニアにお願いする前にやれることもたくさんあります。

note.com

ということで、人に仕事をお願いする前に、自分で色々考えときましょう!という記事でしたー。

説明・説得コストについてアレコレ考えたこと

TL;DR

  • 払うべき説明・説得コストと、払うべきでない説明・説得コストが存在する
  • 払うべき説明・説得コストとは、その行為によって期待する成果
  • (積極的に) 払うべきでない、もしくは説明の方向性を逆転すべき説明・説得コストは、下記の通りだと考えている
    • 説明される側の、本来職種として持つべき知識の明らかな不足によるもの
    • 説明される側の、間違った先入観に基づく一方的な心理的抵抗によるもの
  • マネージャーとして、メンバーから説明を受けるときは下記項目を注意する
    • 今受けている説明コストは、本来メンバーが支払うべきものか
    • メンバーが支払うべきである場合
      • 何故にメンバーが支払うべきで、その説明を受け自分はどのようなアクションをするか、そのためにどこまでの品質を求めるのか
      • 上記項目が確実にメンバーに伝わっているか
  • マネージャーとして、払うべきでない説明・説得コストを支払っているシーンを見たら下記の対策を検討する
    • 説明の方向性を逆にする
      • なぜその変化が必要なのか。ではなく、なぜ変化を必要としないのか。
    • 職種ごとに必要とされる知識の種類と深さについて明文化する
    • 学習の工数を確保し、読書会等での業務時間内での学習を奨励する
    • 予算を確保し、説明コスト支払われる側の人に、外部の研修を受講してもらう
  • 説明・説得コストの削減、方向の変更、偏りの是正は、マネージャーとして最も重要な仕事の1つである
    • 大抵のケースにおいて、説明・説得コストを払う人は特定の個人に偏る
    • 説明・説得コストが特定個人に偏ってしまったチームは成長しなくなる
    • 故に、チームが健全に成長していく上で、説明・説得コストの偏りは是正しなければならない

はじめに

  • 組織、チームで仕事をしている以上、我々は常に変化の中にいる
  • 自分が主体的に変化を促す側になることもあれば、変化を受け入れる側になることもある
  • 変化を促すときは、相手の状況に合わせて適切なアプローチをしたいし
  • 変化を受け入れるときは、自分の現在地を理解して、適切なフォロワーシップを取りたい
  • 変化が起こるとき、そこには説明・説得コストが存在する
  • 「エンジニアリング組織論への招待」の著者である広木大地さんが 「Developer eXperience Day CTO/VPoE Conference 2021」 にて、下記のような話をされている

でも組織の文化としてソフトウェア作りが定着する前に、日本の大きな企業や行政機関は、いろいろなものをユーザー企業が丸投げしてしまったりするので、このノウハウが消失してしまいます。ソフトウェアを作ることが、どういうことなのかを理解する間もなく、徐々に消えていってしまう。 一方で、いい文化資本が蓄積する企業や、ソフトウェアエンジニアリングの文化資本、開発者体験を高めていくさまざまな工夫がどんどん蓄積していく環境の会社には就職したいと思ったり、自分のスキルアップになるんじゃないかと思うエンジニアが多い。そうじゃない会社は不遇な目に合うかもしれないし、嫌な思いをするかもしれないので、就職するのは止めておこうという気持ちになったりします。 こういった差が生まれるのは、文化資本の獲得をする際に、説明・説得に費やされるコストの差が、やはり大きいのではないかなと思っています。当たり前のように自動テストを書くことが習慣になっている会社と、「なんでそれをやらないといけないの? CI のツールはなんでそれを使わないといけないの?」と、いちいち説明・説得に費やされるコストがかかる会社は、なかなか定着していきません。

  • つまり、マネージャーとしては、不当に説明・説得コストが高い状況があれば是正しなければならない

説明/説得コストの分解

構成要素

  • 説明/説得コストについて要素を分解すると、形式知、経験、心理的抵抗の3点と考える。

f:id:selmertsx:20210920202826j:plain

  • 知識も経験もあり、抵抗もない。というケースは説明/説得コストは極小である
  • 知識としてはあるが経験がなく、心理的抵抗もない。というケースは説明・説得コストは少ない
  • 知識も経験もなく、心理的抵抗も大きい。という状況が最も説明・説得コストが高くなる
    • ※ 間違ったやり方の経験だけあり、それによって心理的抵抗が極めて高い状態は除く
  • 説明/説得コストを敢えて計算式で表現するならこんな感じか
Cost=\sqrt{x^2 + y^2 + z^2} \\
x: 知識不足への対応コスト \\
y: 経験不足への対応コスト \\ 
z: 心理的抵抗への対応コスト

説明・説得コストを払っていく

f:id:selmertsx:20210920222211j:plain

  • 心理的抵抗が少ないチームであれば、説明・説得コストの削減は難しくない
  • 知識がないのであれば、下記のような対応が検討できる
    • 職種ごとに必要とされる知識の種類と深さについて明文化し、読書会等での業務時間内での学習を奨励する
    • 予算を取得し、説明コスト支払われる側の人に、外部の研修を受講してもらう
  • 経験がないのであれば、下記項目が検討される
    • モブプログラミング等で、期限を決めて一緒に実験してみる ( 例: TDDとか )
    • 経験者を呼んで、経験談を教えてもらう ( ハンガーフライトみたいなもの )
  • 大変なのは心理的抵抗が高い状態
    • 特に「知識も経験もないが、心理的抵抗が高くチームとしてのINPUTも出来ていない」状態がやっかい

マネージャーとして説明・説得コストに関して気を払うこと

チームの学習機会の観点から

  • 説明・説得コストについて、説明の方向、頻度、分量については、注意が必要である
  • マネージャーは、下記のようなケースを見かけたら、特に意識して観察する必要がある
    • 例1: 自動テストを導入する上で、導入を推進する側が説明/説得コストを大量に払おうとしている
    • 例2: フロントエンドエンジニアとテックリードのどちらが決めても良い内容を、毎回テックリードが決めて、周りに説明している。
    • 例3: プロダクトオーナーとカスタマーサクセスのどちらが決めても良い内容を、毎回プロダクトオーナーが決めて、周りに説明している。
    • 例4: Scrumについて知らないが、導入する必要がないと考えており、学習する気もない
    • 例5: マイクロサービスについて知らないが、導入する必要がないと考えており、学習する気もない
      • ※ 別にマイクロサービスを推奨する訳ではないが、自分で学び、経験してもいないのに、判断を下すのは時期尚早である
  • それは説明・説得する側に回る人間のストレスを減らすためであり
  • 説明・説得される側の人間の、学習機会の損失や主体性の欠如を避けるためでもある

期待値の観点から

  • 誰が払うべき説明コストなのかを明確にする
  • 何が理由で、誰が、何を、どこまで説明する必要があるのか明確にする
  • とくに「どこまで」の部分が難しいが、ここをやりきることが大切だと感じてる

蛇足

やってみせ、言って聞かせて、させてみせ、ほめてやらねば、人は動かじ。
話し合い、耳を傾け、承認し、任せてやらねば、人は育たず。
やっている、姿を感謝で見守って、信頼せねば、人は実らず。
山本五十六
  • やってみせ、言って聞かせて が知識のINPUT
  • させてみせ が経験のINPUT
  • ほめてやらねば心理的抵抗の除外に結びつく