テクノロジー更新: 2026年10月29日

非機能要件のテストで「再現しない」を手元で再現する:stg でも本番でも起こせない不具合を AI で疑似的に起こす方法

再接続の直後や、別のサーバーに切り替わった瞬間にだけ起きる不具合は、stg でも本番でも狙って起こせません。モックのテストは通るのに、本番ではたまに指示が消える。ログにも何も残らない。非機能要件のテストで、いちばん困るのがこの「再現できない」状態です。

この記事では、こうした不具合を手元で何度でも再現する方法をまとめます。結論から言うと、不具合にかかわる部分だけを本物にして、残りは真似る。そのための数十行の再現スクリプトを AI に書かせる、という方法です。考え方、実例3つ、AI への頼み方、そしてこの方法で言えること・言えないことを順に書きます。再現スクリプトも全文載せます。

私は受託開発で、EV 充電サービスのバックエンドとインフラを2年以上担当しています。開発では、社内で動かしている AI 社員(Claude Code のエージェント)に作業を任せています。この記事の3つの例は、実際にこの方法で再現し、直す前と直した後を比べたものです。

stg や本番で再現しにくい非機能要件は、手元で「部分だけ本物」にして再現する

再現しにくい非機能要件は、システム全体を用意しようとせず、不具合にかかわる部分だけを本物にして手元で再現するのが近道です。全体をそろえるほど、狙ったタイミングを作れなくなるからです。

なぜ stg・本番では再現しにくいのか

理由は、起こしたい状況が「短い」「複数台が要る」「わざと作れない」「記録に残らない」の4つにあてはまるからです。

  • 起きる時間が短い:たとえば、接続できた直後から購読し直すまでの一瞬にだけ起きる不具合があります。人の操作では、その一瞬に合わせられません。
  • 複数台の切り替えが要る:機器が別のサーバーにつなぎ直したときにだけ起きる不具合は、サーバーが2台以上あり、つなぎ先が切り替わる状況を作らないと起きません。
  • わざと作れない:stg は他の人も使っていて、ネットワークを好きなときに切ることはできません。本番ではなおさらです。
  • 記録に残らない:指示を取りこぼす不具合は「何も起きない」ので、ログにもエラーにも出ません。

今回の題材は、EV 充電器とクラウドをつなぐ gateway です。充電器は WebSocket で gateway につながり、gateway は充電器1台ごとに AWS IoT Core へ MQTT でつないで、その充電器あての指示(予約など)を購読しています。どの不具合も、MQTT の再接続や、充電器が別の gateway につなぎ直したときにだけ起きるものでした。

考え方:本物にする部分と、真似る部分を分ける

手元で再現するときは、まず「本物にする部分」と「真似る部分」を分けます。不具合の原因になりうる部分を本物にしないと、モックのテストと同じで、前提がまちがっていても気づけないからです。

  • 本物にする部分:不具合にかかわるライブラリ(アプリと同じ版)、ブローカー(メッセージを中継するサーバー)、接続の設定、API を呼ぶ順番
  • 真似る部分:アプリ本体、指示を送る側、充電器。充電器との WebSocket の切断は、MQTT の disconnect() を呼ぶことや、「つながっているか」の変数1つで表します

今回は、AWS IoT Core の代わりに、Docker で動く MQTT ブローカーの mosquitto を使いました。ライブラリは、gateway が使っている AWS IoT Device SDK v2(aws-crt)を、アプリの node_modules からそのまま読み込みました。接続や購読の動きは、この組み合わせで本番と同じように確かめられます。

本番の構成と手元の構成を並べ、ブローカーと同じ版のライブラリだけを本物にし、送信側・gateway・充電器を真似る切り分けを示した図

AI に任せると何が楽になるか

AI に任せると、「どの順で何が起きるか」という台本を書くだけで、再現スクリプトが手に入ります。人が書くと面倒な「切断 → 送信 → つなぎ直す」のような段取りを、数十行のスクリプトにしてくれるからです。

今回、AI 社員は場面ごとにスクリプトを分けて何本も作り、直す前と直した後を同じ台本で動かしました。1本が数十行なので、台本を少し変えてやり直すのも簡単です。台本さえ書ければ、stg で何日も待つより早く、再現できるかどうかが分かります。

実例:MQTT の「再接続」と「複数台」を手元で再現した3つのケース

この方法で、stg でも本番でも狙って起こせなかった不具合を3つ再現し、直し方が効くことも確かめました。どれも、本物のライブラリとブローカーを使ったからこそ見えた動きです。

ケース1:つなぎ直した直後に届いた指示が消える

gateway が AWS IoT Core につなぎ直した直後に届いた指示が、ログも残らず消えていました。原因は、購読がブローカー側と gateway 側の2か所にあることです。

gateway の MQTT 接続は clean_session: false(切断してもブローカー側に購読を残す設定)です。そのため、つなぎ直すとブローカーはすぐにメッセージを送ってきます。一方、gateway 側の受け取り処理は、subscribe() を呼んだときに付くので、つなぎ直すたびに空から作り直されます。aws-crt は、受け取り処理が付く前に届いたメッセージを、接続の 'message' イベントにだけ渡します。gateway はそこを受けていなかったので、メッセージは消えていました。

台本は「購読する → 切断する → 切断中に予約の指示を送る → つなぎ直してから購読し直す」です(付録の case1-before.js)。

sessionPresent true
{"messageEvent":1,"subscribeCallback":0}

メッセージは 'message' に1回届いていますが、購読のコールバックは0回です。受け取る先が無いことが、数ではっきり出ました。

直し方は、connect() より前に 'message' を受け、トピックを見て受け取り処理に振り分けることです。切断中に3件(予約・予約の取り消し・振り分け先の無いもの)送ってからつなぎ直すと、次のようになりました(case1-after.js)。

after reconnect (before subscribe): {"handled":["cmd/reserve-now/cp3","cmd/cancel-reservation/cp3"],"warned":["cmd/unknown/cp3"]}
live message after subscribe: {"handled":["cmd/reserve-now/cp3"]}

購読し直す前に、溜まっていた2件が処理され、振り分け先の無い1件は警告に回りました。購読し直した後の1件も、1回だけ処理されています。

ケース1:直す前は接続直後に届いた指示が受け取り処理の無いまま消え、直した後は connect の前につないだ振り分け役で処理されることを並べた図

ケース2:古い gateway が後から購読を外し、新しい gateway あての指示が消える

充電器が別の gateway につなぎ直したあと、古い gateway が後片付けで購読を外すと、新しい gateway あての指示まで届かなくなりました。購読は client ID(ここでは充電器の ID)ごとに1つしかなく、古い gateway と新しい gateway がそれを共有していたからです。

台本は「A が購読している → 充電器が B につなぎ直す(A はまだ気づいていない)→ A が遅れて後片付けをする → 予約の指示を送る」です(case2.js)。A の後片付けを「購読を外してから切断」にすると、こうなりました。

unsubscribe
 A: interrupt
 A: resume(sessionPresent=true)
 B: interrupt
 B: resume(sessionPresent=true)
 --- 送信役が予約の指示を送る

指示は A にも B にも届いていません。A が外した購読は、B が使っていた購読と同じものだったからです。後片付けを「切断だけ」にすると、結果が変わりました。

disconnect
 A: interrupt
 A: resume(sessionPresent=true)
 B: interrupt
 A: interrupt
 B: resume(sessionPresent=true)
 --- 送信役が予約の指示を送る
 B: 受け取った cmd/reserve-now/cpX

指示は B に届きました。切断のときに購読を外さず、購読はそのまま残す。直し方は、この比べた結果から決めました。2台の gateway と、つなぎ直しの遅れが要るこの状況は、stg で狙って作るのが特にむずかしいものです。

ケース2:古い gateway が購読を外すと共有の購読が消えて指示がどちらにも届かず、切断だけにすると B に届くことを並べた図

ケース3:同じ ID の2台が、接続を奪い合い続ける

同じ client ID で2台の gateway がつなぐと、2台が交互に追い出されては、つなぎ直すことをくり返しました。MQTT のブローカーは同じ ID の接続を1つしか持てず、新しくつないだほうを残して古いほうを切ります。そして aws-crt は、切られると自動でつなぎ直すからです。

台本は「充電器が A から B へつなぎ直したあと、A と B が同じ ID でつなぐ」だけです(case3.js)。6秒のあいだの動きはこうでした。

before
 A: interrupt(同じ ID の接続に追い出された)
 A: resume(自動でつなぎ直した)
 B: interrupt(同じ ID の接続に追い出された)
 B: resume(自動でつなぎ直した)
 A: interrupt(同じ ID の接続に追い出された)
 A: resume(自動でつなぎ直した)
 B: interrupt(同じ ID の接続に追い出された)
{"interrupts":{"A":2,"B":2}}

A と B が2回ずつ追い出され、そのたびにつなぎ直しています。放っておくと、この奪い合いは止まりません。

直し方は、追い出されたとき、充電器との接続がもう無ければ、自動のつなぎ直しを止めることです。同じ台本で動かすと、A が1回追い出されたところで止まり、B だけが残りました。

after
 A: interrupt(同じ ID の接続に追い出された)
 A: 充電器との接続が無いので、自動のつなぎ直しを止める
{"interrupts":{"A":1,"B":0}}
ケース3:直す前は A と B が交互に追い出されてはつなぎ直し続け、直した後は A が1回追い出されたところで止まり B だけが残ることを示した図

3つとも、本物のライブラリとブローカーで台本どおりに動かしたことで、「起きる」「直すと起きない」を数とイベントの記録で示せました。

AI に疑似再現を任せるときの手順と注意

AI に疑似再現を任せるときは、台本と「本物にする部分」を先に決めて伝え、結果を数で出させ、最後に「何が言えて何が言えないか」を分けさせます。ここが曖昧だと、AI は動くスクリプトを作っても、何を確かめたのかがはっきりしないからです。

手順は5つ

自分のチームで始めるなら、次の5つの順番で進めると迷いません。

  1. 起こしたい状況を、文章の台本にする:何が、どの順で起きるか。例「A が購読している → 充電器が B につなぎ直す → A が遅れて後片付けをする → 指示を送る」
  2. 本物にする部分と真似る部分を決める:ライブラリはアプリと同じ版を使う、ブローカーは Docker で立てる、アプリ本体は起動しない、など
  3. 結果を数やイベントの記録で出させる:「動きました」ではなく、「コールバックは0回」「A が2回追い出された」と出させる
  4. 直す前と直した後を、同じ台本で比べる:台本を変えずに、直し方の部分だけを切り替える(ケース2・3 のスクリプトは引数で切り替えています)
  5. 片付ける:使ったブローカーのコンテナは止める

AI への頼み方の例

AI に頼むときは、上の5つをそのまま文章にして渡します。たとえばケース2 なら、次のように頼むと、そのまま動かせるスクリプトが返ってきます。

MQTT の動きを手元で再現するスクリプトを書いてください。

台本:
1. gateway 役 A が client ID「cpX」でつなぎ、cmd/reserve-now/cpX を購読する
2. gateway 役 B が同じ client ID でつなぎ、同じトピックを購読する(A はまだ気づいていない)
3. A が遅れて後片付けをする。引数で「購読を外してから切断」と「切断だけ」を切り替える
4. 送信役が cmd/reserve-now/cpX に QoS 1 で送る

本物にする部分:
- ライブラリは、アプリの node_modules にある aws-crt をそのまま require する(パスは引数で渡す)
- ブローカーは Docker の eclipse-mosquitto:2(127.0.0.1:18830、匿名接続を許可)
- 接続の設定はアプリと同じ(clean_session: false、QoS 1)

結果の出し方:
- A と B の interrupt・resume・受け取ったメッセージを、起きた順に1行ずつ出す
- 最後に、真似た部分(アプリ本体を起動していないこと、など)を書き出す
- 終わったらブローカーのコンテナを止める

手元で動かす準備は、これだけです。

# 匿名接続を許した mosquitto を 18830 番で起動
docker run -d --rm --name mqtt-check -p 18830:1883 eclipse-mosquitto:2 \
  sh -c 'printf "listener 1883\nallow_anonymous true\n" > /tmp/m.conf && mosquitto -c /tmp/m.conf'

# スクリプトを実行(最後の引数はアプリの node_modules にある aws-crt のパス)
node case1-before.js ./node_modules/aws-crt
node case2.js unsubscribe ./node_modules/aws-crt
node case3.js before ./node_modules/aws-crt

# 終わったらブローカーを止める
docker stop mqtt-check
  • aws-crt は、アプリが使っているものを node_modules から直接読み込みます。版がずれると、確かめた動きがアプリと変わるためです。引数を省くと require('aws-crt') になります。
  • この記事の結果は、mosquitto 2 と aws-crt 1.31.0 で確かめました。

疑似再現で言えること・言えないこと

疑似再現で言えるのは「このライブラリとブローカーの組み合わせで、この台本ならこう動く」ことまでです。直したアプリ本体を本物の環境につないだ通しの動きは、別に stg で確かめる必要があります。真似た部分は、あくまで真似だからです。

疑似再現で言えること・言えないこと・stg で確かめることを3列に分けた表

私は、AI 社員に開発を任せるとき、モックのテストが通っただけでは完了にしないと決めています。外部の部品の動きが原因にかかわるなら、本物の部品で「直す前に再現した結果」と「直した後の結果」を出してもらいます。そのうえで、何を本物で確かめ、何をまだ確かめていないかを分けて報告してもらいます。

このルールがあっても、AI の報告をそのまま信じてよいわけではありません。今回も、直したコードをレビューに出したあと、AI 社員に「今回の動作確認の方法を詳しく教えて」と聞くと、「確認は、モックを使った自動テストと CI だけです」と返ってきました。実際には前の日に mosquitto で再現していたのですが、会話の要約にその作業が入っておらず、記録を見ずに答えていたのです。

「mosquitto を使ってローカルで確認していたよね?」と聞き返すと、AI 社員は前の作業の記録を読み直して訂正しました。さらに「そのスクリプトは gateway を真似たもので、gateway そのものを立ち上げたわけではない?」と聞くと、本物で確かめたこと(同じ版の aws-crt、接続の設定、API を呼ぶ順番)、真似たこと(gateway のアプリ本体は起動していない)、まだ確かめていないこと(直した gateway の通しの確認)に分けて答え直しました。

ここまで分かれていれば、レビューする側は「あとは stg で、通しの確認を1回すればよい」と判断できます。疑似再現は、stg や本番の確認をなくすものではありません。stg で何度も待たなくてよいように、その前に原因と直し方を固めておくためのものです。

まとめ

  • 再接続・切断・複数台の切り替えのような非機能要件の不具合は、起きる時間が短く、わざと作れず、記録にも残らないので、stg や本番では再現しにくい
  • 不具合にかかわる部分(ライブラリ・ブローカー・接続の設定・呼ぶ順番)だけを本物にし、残りを真似れば、手元で何度でも再現できる
  • 実例として、つなぎ直した直後の取りこぼし、古い gateway が後から購読を外す問題、同じ ID の接続の奪い合いを再現し、直し方が効くことを確かめた
  • AI には、台本・本物にする部分・結果を数で出すことを伝え、直す前と後を同じ台本で比べさせる
  • 疑似再現で言えるのはライブラリとブローカーの組み合わせまで。通しの確認は stg で行い、AI には何を本物で確かめたかを分けて言わせる

stg で「再現しないので様子見」になっている不具合があれば、まずは1つ選んで、起きる順番を台本に書いてみてください。それを AI に渡せば、手元で再現できるかどうかがすぐに分かります。下の再現スクリプトは、そのまま使えます。

付録:再現スクリプト全文

どれも、上の手順で起動した mosquitto(127.0.0.1:18830)につなぎます。トピック名は記事用に cmd/<指示>/<充電器の ID> の形にしています。

case1-before.js(ケース1・直す前)

// ケース1(直す前):つなぎ直したあと、connect のあとに subscribe でコールバックを付ける
// 使い方: node case1-before.js [aws-crt のパス]
const { mqtt, io } = require(process.argv[2] || 'aws-crt');

const TOPIC = 'cmd/reserve-now/cp1';
const cfg = (clientId) => ({
  client_id: clientId, host_name: '127.0.0.1', port: 18830,
  clean_session: false, keep_alive: 30, socket_options: new io.SocketOptions(),
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  const client = new mqtt.MqttClient();

  // 1. gateway 役が購読してから切断する(ブローカー側には購読が残る)
  let gw = client.new_connection(cfg('cp1'));
  await gw.connect();
  await gw.subscribe(TOPIC, mqtt.QoS.AtLeastOnce, () => {});
  await gw.disconnect();

  // 2. 切断中に、送信役が予約の指示を QoS 1 で送る
  const sender = client.new_connection(cfg('sender'));
  await sender.connect();
  await sender.publish(TOPIC, JSON.stringify({ reservationId: 1 }), mqtt.QoS.AtLeastOnce);
  await sender.disconnect();

  // 3. gateway 役がつなぎ直す。connect のあとに subscribe でコールバックを付ける
  gw = client.new_connection(cfg('cp1'));
  const got = { messageEvent: 0, subscribeCallback: 0 };
  gw.on('message', () => got.messageEvent++); // 数えるだけ。修正前のコードはここを受けていなかった
  const sessionPresent = await gw.connect();
  console.log('sessionPresent', sessionPresent);
  await sleep(500);
  await gw.subscribe(TOPIC, mqtt.QoS.AtLeastOnce, () => got.subscribeCallback++);
  await sleep(500);

  console.log(JSON.stringify(got));
  await gw.disconnect();
  process.exit(0);
})().catch((e) => { console.error(e); process.exit(1); });

case1-after.js(ケース1・直した後)

// ケース1(直した後):connect より前に 'message' を受けてトピックで振り分け、subscribe は登録だけにする
// 使い方: node case1-after.js [aws-crt のパス]
const { mqtt, io } = require(process.argv[2] || 'aws-crt');

const CP = 'cp3';
const ACTIONS = ['reserve-now', 'cancel-reservation'];
const cfg = (clientId) => ({
  client_id: clientId, host_name: '127.0.0.1', port: 18830,
  clean_session: false, keep_alive: 30, socket_options: new io.SocketOptions(),
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 振り分け役:トピックから末尾の充電器 ID を除いた部分で、受け取り処理を選ぶ
const handled = [];
const warned = [];
const handlers = new Map(ACTIONS.map((a) => [`cmd/${a}`, (topic) => handled.push(topic)]));
const dispatch = (topic) => {
  const handler = handlers.get(topic.slice(0, topic.lastIndexOf('/')));
  if (handler) handler(topic);
  else warned.push(topic); // 黙って捨てずに、警告に回す
};

(async () => {
  const client = new mqtt.MqttClient();

  // 前の gateway が購読を残したまま止まった状態を作る
  let gw = client.new_connection(cfg(CP));
  await gw.connect();
  for (const a of [...ACTIONS, 'unknown']) await gw.subscribe(`cmd/${a}/${CP}`, mqtt.QoS.AtLeastOnce);
  await gw.disconnect();

  // 切断中に 3 件送る(予約・予約の取り消し・振り分け先の無いもの)
  const sender = client.new_connection(cfg('sender3'));
  await sender.connect();
  for (const a of [...ACTIONS, 'unknown']) await sender.publish(`cmd/${a}/${CP}`, '{}', mqtt.QoS.AtLeastOnce);

  // 新しい流れでつなぎ直す
  gw = client.new_connection(cfg(CP));
  gw.on('message', (topic) => dispatch(topic)); // connect より前につなぐ
  await gw.connect();
  await sleep(300);
  console.log('after reconnect (before subscribe):', JSON.stringify({ handled, warned }));

  for (const a of ACTIONS) await gw.subscribe(`cmd/${a}/${CP}`, mqtt.QoS.AtLeastOnce); // コールバックは渡さない
  handled.length = 0;
  await sender.publish(`cmd/reserve-now/${CP}`, '{}', mqtt.QoS.AtLeastOnce);
  await sleep(300);
  console.log('live message after subscribe:', JSON.stringify({ handled }));

  await sender.disconnect();
  await gw.disconnect();
  process.exit(0);
})().catch((e) => { console.error(e); process.exit(1); });

case2.js(ケース2。引数 unsubscribe が直す前、disconnect が直した後)

// ケース2:古い gateway があとから購読を外すと、新しい gateway あての指示はどうなるか
// 使い方: node case2.js unsubscribe|disconnect [aws-crt のパス]
//   unsubscribe … 古い gateway が「購読を外してから切断」する(直す前)
//   disconnect  … 古い gateway が「切断だけ」する(直した後)
const mode = process.argv[2];
const { mqtt, io } = require(process.argv[3] || 'aws-crt');

const TOPIC = 'cmd/reserve-now/cpX';
const cfg = (clientId) => ({
  client_id: clientId, host_name: '127.0.0.1', port: 18830,
  clean_session: false, keep_alive: 30, socket_options: new io.SocketOptions(),
  reconnect_min_sec: 1, reconnect_max_sec: 2,
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  const client = new mqtt.MqttClient();
  const log = [];
  // gateway 役。client ID は充電器の ID なので、A と B は同じ ID でつなぐ
  const gateway = (name) => {
    const c = client.new_connection(cfg('cpX'));
    c.on('interrupt', () => log.push(`${name}: interrupt`));
    c.on('resume', (rc, sessionPresent) => log.push(`${name}: resume(sessionPresent=${sessionPresent})`));
    c.on('message', (topic) => log.push(`${name}: 受け取った ${topic}`));
    c.on('error', () => {});
    return c;
  };

  const sender = client.new_connection(cfg('senderX'));
  await sender.connect();

  // 1. 充電器は A につながっていて、A が購読している
  const A = gateway('A');
  await A.connect();
  await A.subscribe(TOPIC, mqtt.QoS.AtLeastOnce);

  // 2. 充電器が B につなぎ直す。A はまだ充電器との切断に気づいていない
  const B = gateway('B');
  await B.connect();
  await B.subscribe(TOPIC, mqtt.QoS.AtLeastOnce);

  // 3. A が遅れて切断に気づき、後片付けをする
  if (mode === 'unsubscribe') {
    await Promise.race([A.unsubscribe(TOPIC), sleep(4000)]).catch(() => {});
    await A.disconnect().catch(() => {});
  } else {
    await sleep(4000);
    await A.disconnect().catch(() => {});
  }
  await sleep(3000);

  // 4. 送信役が予約の指示を送る
  log.push('--- 送信役が予約の指示を送る');
  await sender.publish(TOPIC, '{}', mqtt.QoS.AtLeastOnce);
  await sleep(1500);

  console.log(`${mode}\n ${log.join('\n ')}`);
  await B.disconnect().catch(() => {});
  await sender.disconnect().catch(() => {});
  process.exit(0);
})().catch((e) => { console.error(e); process.exit(1); });

case3.js(ケース3。引数 before が直す前、after が直した後)

// ケース3:同じ ID の 2 台がつなぐと、接続を奪い合い続けるか。直した後は止まるか
// 使い方: node case3.js before|after [aws-crt のパス]
//   before … ブローカーに切られたら、いつも自動でつなぎ直す(直す前)
//   after  … 切られたとき、充電器との接続が無ければ、自動のつなぎ直しを止める(直した後)
const mode = process.argv[2];
const { mqtt, io } = require(process.argv[3] || 'aws-crt');

const cfg = {
  client_id: 'cpY', host_name: '127.0.0.1', port: 18830,
  clean_session: false, keep_alive: 30, socket_options: new io.SocketOptions(),
  reconnect_min_sec: 1, reconnect_max_sec: 2,
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  const client = new mqtt.MqttClient();
  const log = [];
  const interrupts = { A: 0, B: 0 };
  // 充電器との WebSocket がつながっているか。充電器は A から B へつなぎ直したあと
  const chargerConnected = { A: false, B: true };

  const gateway = (name) => {
    const c = client.new_connection(cfg);
    c.on('interrupt', async () => {
      interrupts[name]++;
      log.push(`${name}: interrupt(同じ ID の接続に追い出された)`);
      if (mode === 'after' && !chargerConnected[name]) {
        log.push(`${name}: 充電器との接続が無いので、自動のつなぎ直しを止める`);
        await c.disconnect().catch(() => {});
      }
    });
    c.on('resume', () => log.push(`${name}: resume(自動でつなぎ直した)`));
    c.on('error', () => {});
    return c;
  };

  const A = gateway('A');
  await A.connect();
  const B = gateway('B');
  await B.connect();
  await sleep(6000);

  console.log(`${mode}\n ${log.join('\n ')}`);
  console.log(JSON.stringify({ interrupts }));
  await A.disconnect().catch(() => {});
  await B.disconnect().catch(() => {});
  process.exit(0);
})().catch((e) => { console.error(e); process.exit(1); });
CONTACT

自社のケースのAI活用について相談する

現在の課題をうかがったうえで、導入イメージと概算費用をご案内します。相談は無料です。

AC
久場星河

合同会社アストラルコード 代表。AI・LLMを活用したシステム開発を通じて、企業の生産性向上を支援しています。