svg-animation
ブログ一覧

自動車整備会社向け故障探求AIエージェント開発の取り組み

整備士が現場で本当に知りたいのは、故障コードの意味ではありません。「で、次にどこを測ればいいのか」です。

この「次の一手」を、LINEで写真と症状を送るだけで一緒に考えてくれるAIエージェントを開発しました。設計で大事にしたことから、AWSの構成、実装のポイントまでご紹介します。

システム構成の概要(イメージ図)

何をするエージェントか

整備士がLINEで、診断機の画面や警告灯の写真、症状を送ります。エージェントは、原因の仮説と「次に確認すること」を、1〜3ステップずつ返します。

  • 故障コードや症状から、次に測る場所を提示する
  • 診断ソフトや工具の操作の相談にも答える
  • 直った相談は、自動で事例として蓄積する

アプリの追加もログインも要りません。整備士がすでに毎日使っているLINEの中で完結します。

設計で大事にした原則

一般的なコードの辞書ではなく、プロの切り分けの考え方を再現することを目指しました。そのために、次の原則を決めています。

原則 理由
原因を早く断定しない 断定は整備士の測定で行う。AIは仮説と確認手順を出す
一度に出す点検は最初の1〜3ステップだけ 大量の手順は読まれない。結果を見て次を出す
答えはタップか一言で済ませる 手袋のまま、片手で使える操作にする
写真から読めることは再入力させない 診断機の画面を言葉で説明し直す手間をなくす
AIの仮説と、整備士が確認した事実を分ける 誤った仮説が事実として蓄積されるのを防ぐ
部品交換と、原因の確定を分ける 交換して直っても、原因が分かったとは限らない
危険な手順は出さない 緊急症状は、作業を止めて安全の確保を促す

技術スタック

領域 採用したもの
言語 TypeScript(Node.js 24)。モノレポで、型とスキーマを受付側とエージェント側で共有
受付 Amazon API Gateway(HTTP API)+ AWS Lambda
キュー Amazon SQS FIFO(デッドレターキューつき)
エージェント Amazon EC2(t4g.small、Graviton)上のDockerコンテナ。systemdで常駐
生成AI Amazon Bedrock経由のClaude。SDKは @anthropic-ai/bedrock-sdk
状態・事例 Amazon DynamoDB(単一テーブル)
画像・資料 Amazon S3、画像の配信はCloudFrontの署名付きURL
検索 Amazon S3 Vectors(ベクトル検索)と、DynamoDBに置いた規定値の構造化データ
設定・秘密情報 AWS Systems Manager Parameter Store
監視 Amazon CloudWatch(ログ、メトリクス、アラーム)、SNS、Cost Anomaly Detection
インフラ管理 Terraform(S3バックエンド)。アカウントはOrganizationsとIAM Identity Centerで管理

構成と、そう決めた理由

受付は「検証してキューに入れるだけ」にする

LINEのWebhookは、短い時間で応答を返す必要があります。生成AIの呼び出しは数秒から数十秒かかるため、Lambda内では呼べません。そこで、Lambdaの責務を次の2つに絞りました。

  • リクエストの署名を検証する
  • 検証できたら、SQSにメッセージを入れて、すぐ200を返す

署名の検証は、LINEの標準の方式(チャネルシークレットを鍵にしたHMAC-SHA256)です。API Gatewayのオーサライザは使えないため、Lambda内で行います。ここで大事なのは、署名を加工する前の生のバイト列に対して検証することです。JSONにパースしてから戻すと、絵文字や改行の扱いで署名が合わなくなります。

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(rawBody: Buffer, signature: string, secret: string): boolean {
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  const actual = Buffer.from(signature, "base64");
  return actual.length === expected.length && timingSafeEqual(actual, expected);
}

キューはFIFOにして、整備士ごとに直列化する

SQS FIFOには、次のように設定しています。

  • MessageGroupId に整備士のIDを使う。同じ整備士のメッセージは必ず順番に処理され、別の整備士同士は並行して処理される
  • MessageDeduplicationId にLINEのWebhookイベントIDを使う。LINEが再送しても、キューが重複を弾く
  • 3回処理に失敗したメッセージは、デッドレターキューに移し、アラームで知らせる

エージェントが止まっている間のメッセージもキューに残るため、再起動すればまとめて処理されます。

エージェントはEC2の常駐コンテナにする

エージェントは、キューを受信し続ける常駐プロセスです。ローカルな状態を持ち、画像の取得や後処理も行うため、Lambdaではなく、小さなEC2(Graviton)でDockerコンテナとして動かしています。

  • イメージはECRに置き、デプロイは「ビルド、ECRへpush、SSM Run Commandで再起動」を1コマンドにしている
  • SSHは開けない。サーバーへの接続は、SSM Session Managerで行う
  • ログはDockerのawslogsドライバでCloudWatch Logsに送る
  • 状態はすべてDynamoDBに置き、EC2には永続データを持たせない。インスタンスを作り直しても復元できる

NATゲートウェイ(月額が高い)とロードバランサは使っていません。EC2はパブリックサブネットに置き、セキュリティグループのインバウンドを空にして、外から入れない状態にしています。LINEが呼ぶのはAPI Gatewayの標準のエンドポイントだけなので、独自ドメインも要りません。

返信は、返信用トークンを優先する

LINEには、返信用のトークンで返す方法と、こちらから送るpushがあります。pushは月の無料枠が小さいため、通常の返信はすべて返信用トークンで行い、トークンの期限が切れたときだけpushに切り替えます。

実装のポイント

生成AIの呼び出し

Bedrockのモデル指定は、コードに書かず、Parameter Storeの設定(JSON)から読みます。再デプロイなしにモデルを切り替えられます。設定のJSONが壊れていても、前回の有効な値で動き続け、SNSで通知します。

システムプロンプトが長いため、プロンプトキャッシュを使っています。キャッシュは先頭から一致する部分に効くので、変わらない内容(役割、安全ルール)を前に、変わりやすい内容を後ろに置いています。

出力は、JSONスキーマを指定した構造化出力にし、受け取った結果は、Zodのスキーマで必ず検証します。検証に通らない出力は、再試行します。構造化出力は、スキーマが複雑だと文法の組み立てがタイムアウトすることがあるため、拒否されたときはプロンプトだけで動くように切り替えられる作りにしています。

画面は、生成AIに作らせない

選択肢(Quick Reply)や、結果のカード(Flex Message)は、生成AIにJSONを書かせていません。AIには内容だけを返させ、画面の組み立てはコード側のテンプレートで行います。

LINEには細かい制約があります。たとえば選択肢のラベルは20文字までで、超えると送信が400エラーになります。生成AIの出力に頼ると、この種のエラーで応答が止まり、リトライのたびに生成AIの費用も増えます。送信の直前に、コードで切り詰めて防いでいます。

また、LINEはMarkdownを解釈しないため、AIが書いた記号がそのまま表示されます。プロンプトで禁止するだけでなく、送信前にコードで平文へ変換しています。LINEの絵文字は、(happy) のような文字列として届くので、生成AIに渡す前に取り除いています。

安全のガード

AIへの指示だけに頼らず、コードでも防いでいます。

  • 緊急症状は、生成AIを呼ぶ前に、キーワードで判定して作業の中止を案内する
  • 危険な手順は、AIの出力を後から検査して取り除く
  • 「原因の確定」は、型のレベルで出力できないようにする。確定は整備士が行う
  • 根拠のない確率(「70%」など)は出さない
  • 締付けトルクなどの規定値は、参照した資料に書かれている値のときだけ、出典つきで出す。資料にない数値が出力に含まれていたら、書き直させる

データの設計

状態はDynamoDBの単一テーブルに置いています。

  • 会話、セッション、車両情報、使用量、会社・座席、事例を、パーティションキーとソートキーの命名で分ける。キーの組み立ては、1つのモジュールに集める
  • 使用量のカウンタは ADD で原子的に加算する。別々の整備士の処理が並行しても、更新が失われない
  • 「同じ月に1回だけ通知する」は、条件つきの更新で実現する。「読んで、立っていなければ通知し、立てる」だと、並行する2つの処理が両方通知してしまう
  • TTLは、日次のカウンタだけに付ける。会話ログに付けると、事例化の元データが消える
  • 事例化の対象を探すスイープは、スパースなGSI(対象のものにだけ属性を持つ)で引く。事例化が済んだものは索引から外れ、スイープが空振りしない

送られた写真は、S3に置き、ライフサイクルで一定期間後に自動で削除します。画像から読み取った内容は構造化して残るため、元の画像は長く持つ必要がありません。

使うほど資産になるナレッジ

整備士に「記録を残す作業」はさせません。会話を終えるだけで、エージェント内の定期処理(15分ごと)が、次のことを行います。

  1. 会話から、症状・故障コード・点検の結果・原因・修理の内容を構造化する
  2. 整備士が確認した原因と、AIの仮説を区別して保存する
  3. 決め手になった点検や、効かなかった手順も、教訓として残す
  4. 事例の信頼度を段階で分け、整備士が原因を確認した事例だけを上位にする
  5. 上位にする前に、AIがもう一度会話を読み直し、根拠があるかを確かめる(確認は、段階を下げる方向にだけ働く)

誤って「直った」を押しても、そのまま上位の事例にはなりません。未解決の相談も、次に同じ症状が来たときの手がかりとして残します。事例の保存時には、車台番号などを検索に使える形のまま個体が特定できないようにマスキングしています。

この定期処理を、新しいキューやスケジューラではなく、すでに常駐しているエージェントの中に置いたのは、追加のインフラとIAMロールを増やすほどの処理ではないからです。

公式データの検索(RAG)

メーカーの整備データ(文書、規定値、図、動画、配線図)を取り込み、出典つきで答える機能も実装しました。

  • 文書は英語版を2,000字ほどに分け、重複を除いて約39万のチャンクにした。ベクトル化は、Amazon Bedrockの多言語の埋め込みモデル(1,024次元)で行い、S3 Vectorsに登録している
  • ベクトルの保存先は、OpenSearch Serverlessではなく、S3 Vectorsにした。検索の回数(1分に1回ほど)に対して、OpenSearch Serverlessは最小構成でも固定費が大きいため
  • 締付けトルクなどの規定値は、ベクトル検索ではなく、DynamoDBに構造化して置き、文書のIDで引く。数値を文章の検索結果から拾うと、取り違えが起きるため
  • 本文は、S3に1文書1オブジェクトで置く。検索で当たった文書のIDから、英語版と日本語版を引く
  • 図と動画は、画面に写った文字をOCRして、本文中で参照される名前と対応付け、CloudFrontの署名付きURLで配信する
  • 毎ターン、まず小さな呼び出しで「検索が要るか」と英語の検索語を決める。要らないターンは、検索をしない

ベクトルの作成や画像の処理は、開発者のマシンで行い、クラウドの費用がかからないようにしています。

費用を守る仕組み

生成AIの利用料には、サービス側で止まる上限がありません。そこで、使用量をアプリ側で数えて、月の予算に達したら自動で止める仕組みを入れています。

  • 入力、出力、キャッシュの書き込み、キャッシュの読み込みを、別々に集計して、それぞれの単価を掛ける。キャッシュの読み込みは単価が低いため、まとめると過大に見積もり、予算に達していないのに止まってしまう
  • 円への換算レートは、毎日Lambdaが公表レートを取得し、少し高めの値を設定へ書き込む。取得に失敗したり、前回から大きく変動したりしたときは、更新せずに通知する
  • いったん止まったら、月が替わっても自動では戻さない。原因を調べる前の再開で、同じ事故を繰り返さないため。「人が止める」と「システムが止める」は、別の場所に持つ

クラウドの予算機能は、請求データの反映に半日から1日かかるため、リアルタイムには止められません。補助として、Cost Anomaly Detectionで、生成AI以外の異常な課金も検知します。

テスト運用で学んだこと

  • 「OK」というアラームメールが繰り返し届いた。データが欠けた状態を INSUFFICIENT_DATA と評価していたのが原因で、欠損を正常とみなす設定にして解消した
  • アプリの返信が止まったとき、原因はLINE側の制約(ラベルの文字数)で、返信もpushも同じ400エラーになり、フォールバックが効かなかった。4xxのエラーは再配信せず、利用者に定型文で知らせる作りに改めた
  • 設定を処理の中で初めて読む実装だと、設定が壊れた間に届いたメッセージがデッドレターキューに落ちる。起動時に設定を読み、読めなければ起動しない作りにした
  • 開発機のスリープ中はネットワークが切れる。受信ループは一定時間待って再試行し続け、回復のログを出すので、再起動は要らない

今後に向けて

現在は、運用の開始に向けた準備を進めています。ほかのメーカーの資料への対応や、見積・整備履歴の検索など、整備工場の仕事全体を支える機能への拡張も検討しています。

ヱビスは、生成AIとRAGを組み合わせた業務システムの設計から、AWSでの構築・運用までを一貫して対応しています。ご相談はお気軽にお問い合わせください。

ブログ一覧