連載: NMS開発者がLLMブートキャンプで学んだこと
前回は、LLM ブートキャンプを始めた理由を書きました。
今回は、その最初の実装として Python から OpenAI API を呼び出します。さらに、NMS の障害ログを既存システムでも扱いやすい JSON に変換するところまで試します。
ブラウザのChatGPTとAPIは何が違うのか
ブラウザで ChatGPT を使うだけなら、入力欄に文章を入れて送信すれば回答が返ってきます。人間が読むことが前提なので、多少表現が揺れたり、余計な説明が付いたりしても大きな問題にはなりません。
一方、API を NMS やチケットシステムに組み込む場合、回答を読むのは人間だけではありません。後続のプログラムがレスポンスを解析し、画面表示やチケット作成に利用します。
その瞬間、考えるべきことが増えます。
- どのモデルを呼ぶか
-
systemとuserのメッセージをどう分けるか - レスポンスのどこに回答本文が入っているか
- 出力形式をどう固定し、失敗時にどう処理するか
たとえば障害ログを入力し、LLM に重要度、要約、想定原因、一次対応を返してもらうとします。その結果を画面に表示したり、チケットの初期文面に入れたり、通知ルールに使ったりするなら、出力はできるだけ安定している必要があります。
人間向けの自然文ではなく、システムが扱えるデータとして受け取りたい。
そう考えると、最初の API 呼び出しは単なる「Hello, GPT」ではありません。LLM を既存システムに接続するための入り口です。
前回のAPI呼び出しを業務視点で読み直す
前回は、Python から OpenAI API を呼び出し、モデルの回答を取得できることを確認しました。
そのときの目的は、まず動かすことでした。messages に質問を入れ、response.choices[0].message.content を表示できれば成功です。
今回は同じコードを、業務システムへ組み込む視点で読み直します。
import os
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "こんにちは。自己紹介してください。"
}
],
)
answer = response.choices[0].message.content
print(answer)
print(f"model: {response.model}")
print(f"finish_reason: {response.choices[0].finish_reason}")
print(f"total_tokens: {response.usage.total_tokens}")
print(f"request_id: {response.id}")
API を試すだけなら、必要なのは answer だけです。しかし、障害対応システムで運用するなら、それ以外の値にも意味があります。
| 項目 | 業務で確認する理由 |
|---|---|
response.model |
どのモデルが回答したかを追跡するため |
finish_reason |
正常終了か、出力上限による途中終了かを判定するため |
usage.total_tokens |
障害1件あたりの利用量とコストを把握するため |
response.id |
問い合わせ単位でログを追跡するため |
たとえば、障害チケットの要約が途中で切れていた場合、プロンプトの問題とは限りません。finish_reason が length なら、出力上限に達した可能性があります。
また、同じ種類のアラートを大量に処理したとき、total_tokens を記録していなければ、月末にコストが増えた理由を説明できません。モデルを変更したあとに回答傾向が変わった場合も、モデル名を保存していなければ比較が難しくなります。
NMS で外部装置や監視 API を連携するとき、レスポンス値、処理時間、成功・失敗、リクエスト ID をログに残します。LLM API も同じです。
前回は「回答が返ってきた」ことを確認しました。今回は、「後から原因とコストを追跡できる形で呼び出せているか」を確認します。
LLM は魔法の箱ではなく、監視と記録が必要な外部 API です。
API Keyを「隠す」から「運用する」へ
前回は、API Key をコードへ直接書かず、.env と環境変数から読み込むところまで確認しました。
今回は、その先を考えます。業務システムでは、キーを Git に入れないだけでは十分ではありません。誰が、どの環境で、どのキーを使い、漏えい時にどう止めるかまで決める必要があります。
まず、ローカル開発、検証、本番で同じキーを共有しないようにします。
| 環境 | Key の渡し方 | 考え方 |
|---|---|---|
| ローカル | .env |
個人検証用。Git には登録しない |
| CI/CD | CI の Secret 機能 | ビルドログや設定ファイルへ出さない |
| 検証・本番 | 環境変数または Secret 管理基盤 | 環境ごとに分離し、交換可能にする |
アプリケーション側では、キーがない状態で処理を続けないようにします。
import os
from openai import OpenAI
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("OPENAI_API_KEY is not configured")
client = OpenAI(api_key=api_key)
ここで大切なのは、キーの値をログへ出さないことです。
# 値は表示しない
print("OpenAI API Key: configured")
デバッグのために print(os.getenv("OPENAI_API_KEY")) と書くと、端末だけでなく Notebook の出力、CI のログ、障害調査用に収集したログへ残る可能性があります。
NMS は24時間動作し、複数の運用者や保守担当者がログを参照します。そのため、通常の開発ツールよりも「誰がログを見るか」の範囲が広くなりがちです。キーをコードから外しても、ログに出せば意味がありません。
運用前には、最低限次のルールを決めておきたいところです。
- 開発・検証・本番でキーを分離する
- 利用するサービス単位でキーの所有者を明確にする
- キーの値をアプリケーションログへ出さない
- 不要になったキーを放置せず無効化する
- 漏えいが疑われた場合は、削除ではなく無効化と再発行を行う
- キーを交換してもアプリケーションを大きく修正せずに済む構成にする
特に重要なのは、交換できることです。
キーをコードへ埋め込むと、交換のたびに修正、ビルド、配布が必要になります。環境変数や Secret 管理基盤から注入する構成なら、アプリケーションコードを変更せずにキーを切り替えられます。
前回は「API Key を見せない」ことが目的でした。今回は「環境ごとに分離し、追跡し、必要なときに止めて交換できる」状態を目指します。
NMS に LLM を組み込むなら、API Key は設定値ではなく、運用対象の認証情報として扱う必要があります。
system と user を分ける
次に面白かったのは、messages の構造です。
最初は user メッセージだけでも動きます。
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "Python を一文で説明してください。"}
],
)
しかし、少し制御したい場合は system メッセージを使います。
授業では、たとえば次のように「小学生に教える親切な数学の先生」という役割を与えていました。
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "あなたは小学生を教える親切な数学の先生です。常に段階的に説明してください。"
},
{
"role": "user",
"content": "1+1 は?"
}
],
)
この構造を見たとき、私は NMS の障害分析でも同じ考え方が使えると思いました。
たとえば、単にログを渡して「要約して」と言うより、最初に役割と制約を与えた方が実務に近くなります。
あなたはネットワーク運用センターの一次対応担当者です。
障害ログを読み、過度に断定せず、確認すべき項目を整理してください。
system は、その処理全体の前提や振る舞いを定義する場所です。
user は、今回処理したい具体的な入力です。
通常のバックエンド開発で言えば、system はサービス仕様やポリシーに近く、user はリクエストボディに近いかもしれません。もちろん完全に同じではありませんが、このように分けて考えると理解しやすくなりました。
週末プロジェクトで FAQ チャットボットを作った
1週目の内容を使った週末プロジェクトでは、「住宅申込 FAQ チャットボット」を作りました。
扱ったドメインは NMS ではなく住宅申込でしたが、構造としてはかなり実務に近いものでした。
最初は、OpenAI API に質問を投げるだけです。
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "あなたは住宅申込の専門相談員です。親切で分かりやすく説明してください。"
},
{
"role": "user",
"content": "申込用の通帳とは何ですか?"
}
],
)
answer = response.choices[0].message.content
この段階では、まだ普通のチャットです。
ただ、プロジェクトではここから少しずつアプリケーションらしくしていきました。
FAQ のサンプルデータを用意し、質問に近い FAQ を検索し、その検索結果を system プロンプトに入れてから回答を生成します。さらに、回答だけでなく「どの FAQ を参考にしたか」も返すようにします。
def format_context(search_results):
if not search_results:
return "関連 FAQ がありません。"
parts = []
for r in search_results:
faq = r["faq"]
parts.append(f"Q: {faq['question']}\nA: {faq['answer']}")
return "\n---\n".join(parts)
def ask_faq(question, faq_data, client):
results = search_faq(question, faq_data)
context = format_context(results)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": f"住宅申込の相談員です。参考 FAQ:\n{context}\n\nFAQ に基づいて回答してください。"
},
{
"role": "user",
"content": question
}
],
)
return {
"answer": response.choices[0].message.content,
"sources": [r["faq"]["question"] for r in results],
}
このプロジェクトで大事だったのは、LLM に何でも自由に答えさせるのではなく、手元の FAQ を根拠として渡す構造にしたことです。
もちろん、この時点ではまだ本格的な Vector DB や RAG ではありません。キーワード検索に近い簡単な仕組みです。それでも、ただ質問を投げるだけの API 呼び出しとはかなり違います。
「入力を受ける」
「関連データを探す」
「context として LLM に渡す」
「回答と参照元を返す」
「エラー入力を処理する」
「UI から使えるようにする」
この流れができると、LLM は単なる会話相手ではなく、業務アプリケーションの一部になります。
プロジェクトでは、ChatPromptTemplate や prompt | llm | StrOutputParser() のような LangChain LCEL の書き方も使いました。
faq_chain = faq_prompt | llm | StrOutputParser()
最初はこのパイプの書き方に少し戸惑いましたが、慣れてくると「プロンプト」「モデル」「出力変換」を部品としてつなげているだけだと分かります。
バックエンド開発で言えば、Controller、Service、Repository を分ける感覚に少し近いです。LLM の処理も、ひとつの長い関数に詰め込むより、部品化した方が後で直しやすくなります。
さらに、週末プロジェクトでは safe_ask() のような入力検証も扱いました。
空文字、短すぎる質問、500文字を超える入力、数字だけの入力、API エラー。こうしたケースを try/except とバリデーションで処理します。
これはかなり実務的でした。
LLM アプリケーションというと、どうしてもモデルやプロンプトに目が行きます。しかし実際に人に使ってもらうなら、空入力や異常入力をどう扱うか、API が失敗したときに何を返すかの方が先に問題になります。
最後には Gradio を使って、小さなチャット UI まで作りました。
この経験で、最初の API 呼び出しはゴールではなく、入口にすぎないと分かりました。
API 呼び出し、プロンプト、検索、参照元、エラー処理、UI。
これらを組み合わせて初めて、「LLM を使ったアプリケーション」になります。
この FAQ チャットボットの構造は、そのまま NMS に置き換えられます。
- 住宅申込 FAQ は、障害対応 FAQ や運用マニュアルに置き換えられる
- 相談員の
systemメッセージは、NOC 一次対応担当者の役割に置き換えられる - 参照 FAQ は、過去チケットや対応手順に置き換えられる
- Gradio UI は、検証用の障害分析画面に置き換えられる
つまり、1週目のプロジェクトで作ったのは住宅申込チャットボットでしたが、私にとっては「NMS 向け LLM アプリの最小構成」を見る練習でもありました。
障害ログを JSON にしてみる
ここからは、授業の内容を NMS の文脈に寄せて、小さな例を作ってみます。
入力は、次のような障害ログです。
2026-03-10 21:14:03 CRITICAL router-core-01 BGP neighbor 10.10.0.2 Down
2026-03-10 21:14:08 WARNING router-core-01 Interface Gi0/1 input errors increased
2026-03-10 21:14:15 CRITICAL nms-poller SNMP timeout router-core-01
処理の全体像を図にすると、次のようになります。
左側の障害ログを user メッセージとして渡し、中央の system メッセージで役割と出力形式を固定します。返ってきた JSON は障害原因の確定結果ではなく、NOC オペレータが初動確認を始めるための仮説です。
そのため、この例では LLM の出力を自動復旧には接続しません。ログ内容、影響範囲、一次対応が妥当かを、必ず人間が確認する前提にしています。
Qiita に掲載する場合は、
assets/nms_log_to_structured_json.pngを画像アップロードし、上の相対パスを Qiita が発行した URL に置き換えてください。
このログを、NOC 担当者が初動対応できるように構造化して返すプログラムを考えます。
incident_log = """
2026-03-10 21:14:03 CRITICAL router-core-01 BGP neighbor 10.10.0.2 Down
2026-03-10 21:14:08 WARNING router-core-01 Interface Gi0/1 input errors increased
2026-03-10 21:14:15 CRITICAL nms-poller SNMP timeout router-core-01
""".strip()
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
messages=[
{
"role": "system",
"content": """
あなたはネットワーク運用センターの一次対応担当者です。
障害ログを読み、NOC 担当者が最初に確認すべき内容を整理してください。
出力は JSON のみ。余計な説明文は書かないでください。
JSON のキーは severity, summary, impact, suspected_cause, first_actions にしてください。
""".strip()
},
{
"role": "user",
"content": incident_log
}
],
)
answer = response.choices[0].message.content
print(answer)
期待する出力は、たとえば次のような形です。
{
"severity": "critical",
"summary": "router-core-01 で BGP neighbor の Down と SNMP timeout が発生しています。",
"impact": "外部接続または上位ネットワーク経路に影響が出ている可能性があります。",
"suspected_cause": "BGP セッション断、インターフェースエラー増加、または監視対象装置への到達性低下が疑われます。",
"first_actions": [
"router-core-01 への疎通と SNMP 応答を確認する",
"Gi0/1 のインターフェース状態とエラーカウンタを確認する",
"BGP neighbor 10.10.0.2 の状態と直近のログを確認する"
]
}
ここで重要なのは、LLM に「正解を断定させる」のではなく、「初動対応のための整理」をさせている点です。
障害原因を本当に特定するには、装置情報、構成情報、監視経路、過去チケット、メンテナンス予定などが必要です。この時点ではログしか渡していません。したがって、LLM の回答はあくまで仮説です。
それでも、ログをそのまま読むよりは状況を把握しやすくなります。
JSON 出力は思ったより難しい
ただし、ここで一つ問題があります。
「JSON のみ」と指示しても、LLM が必ず完全な JSON だけを返すとは限りません。
ときどき、次のような余計な文章が混ざることがあります。
以下が分析結果です。
{
"severity": "critical",
...
}
人間が読むだけなら問題ありません。しかし、後続処理で json.loads(answer) のように parse するなら失敗します。
また、キー名が少し変わることもあります。
{
"level": "critical",
"summary": "...",
"actions": ["..."]
}
人間には意味が通じますが、システムには通じません。
この点は、授業で扱った JSON、辞書、リスト、Output Parser の話につながります。最初は「JSON 形式で返して」と書けば十分だと思っていましたが、実務ではそれだけでは弱いです。
最低限、次のような対策が必要になります。
-
temperature=0にして出力の揺れを抑える -
systemメッセージでキー名と制約を明確にする - JSON parse に失敗した場合のリトライ処理を用意する
- 必須キーがあるかをアプリケーション側で検証する
- 重要な処理では LangChain の
JsonOutputParserや Pydantic のような仕組みを使う
授業の後半では、LangChain の JsonOutputParser や Pydantic を使って、出力形式をより明確にする例も出てきました。
たとえば、都市情報や書評を決まったフィールドで返すようにする実習です。
この考え方は、障害ログ分析にもそのまま使えます。
from pydantic import BaseModel, Field
class IncidentSummary(BaseModel):
severity: str = Field(description="障害の重要度")
summary: str = Field(description="短い要約")
impact: str = Field(description="想定される影響")
suspected_cause: str = Field(description="想定原因")
first_actions: list[str] = Field(description="一次対応の候補")
実務では、LLM の出力をそのまま信じるのではなく、アプリケーション側で型や必須項目を確認する必要があります。
この感覚は、通常の API レスポンスを DTO にマッピングしてバリデーションするのと近いです。
モデル名とパラメータで迷う
最初の実習では、gpt-4o-mini を使いました。
コード上では model="gpt-4o-mini" と書くだけですが、初心者のうちはここでも迷います。
モデル名を間違えると当然エラーになります。似たような名前のモデルがあると、どれを使うべきか迷います。さらに、temperature、max_tokens、top_p、frequency_penalty、presence_penalty、stop、seed などのパラメータも出てきます。
授業では、同じ質問に対して temperature を変えたり、max_tokens を変えたりして、出力がどう変わるかを試しました。
この実験は、NMS のような運用系の用途ではかなり重要です。
障害ログ分析では、創造的な表現はあまり必要ありません。むしろ、毎回できるだけ安定した出力が欲しいです。
そのため、最初の障害ログ JSON 要約では temperature=0 にしました。
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
messages=[...],
)
もちろん、temperature=0 にしても完全に同じ出力が保証されるわけではありません。しかし、少なくとも障害対応のような用途では、最初に低めの値から試すのが自然だと思います。
逆に、障害チケットの顧客向け説明文を複数案出したい場合は、少しだけ揺らぎがあってもよいかもしれません。
つまり、パラメータは「モデルの気分を調整するつまみ」ではなく、業務要件に合わせて決める設計項目です。
トークン使用量を見る
もう一つ、最初の段階で見ておくべきだと思ったのがトークン使用量です。
授業ノートでは、次のように response.usage.total_tokens を確認していました。
response.usage.total_tokens
最初は「料金計算に使う数字」くらいに見えます。
しかし、NMS の現場に当てはめると、トークンはかなり現実的な制約になります。
障害ログは長くなりがちです。特に大規模障害では、同じ装置から似たようなアラートが大量に出ます。これをそのまま LLM に渡すと、コストも増えますし、重要な情報が埋もれる可能性もあります。
そのため、API を呼び出す前に次のような前処理を考えたくなります。
- 同じアラートを重複排除する
- 対象装置ごとにまとめる
- 直近数分のログだけに絞る
- 重要度の低いログを別扱いにする
- タイムスタンプや冗長な項目を必要に応じて削る
これは次回以降の Token や RAG の話にもつながります。
LLM に大量の情報を投げれば賢く処理してくれる、という考え方は少し危険です。
業務で使うなら、LLM に渡す前の入力設計がかなり重要になります。
会話履歴も自分で管理する
授業では、messages に会話履歴を追加していく例もありました。
messages = [
{"role": "system", "content": "あなたは IT ヘルプデスクです。簡潔に回答してください。"}
]
messages.append({"role": "user", "content": "パスワードを忘れました。"})
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
)
answer1 = response.choices[0].message.content
messages.append({"role": "assistant", "content": answer1})
messages.append({"role": "user", "content": "その方法でだめならどうすればよいですか?"})
ここで分かるのは、API 側が勝手に会話を覚えてくれるわけではない、ということです。
ブラウザの ChatGPT では会話が続いているように見えますが、API では基本的に、必要な履歴をこちらが messages に入れて渡します。
NMS の障害対応に置き換えると、これはかなり重要です。
ある障害チケットについて、最初のアラート、追加ログ、担当者の確認結果、過去の類似障害、暫定対応、復旧確認などを会話的に扱いたい場合、どの情報を履歴として保持し、どこまで LLM に渡すかを設計しなければなりません。
全部渡すとトークンが増えます。少なすぎると文脈が足りません。
この問題は、後の Memory や RAG、Agent の話につながっていきます。
最初の API 呼び出しは小さく見えますが、messages の管理を学ぶだけでも、LLM アプリケーションの本質に少し触れた感じがありました。
小さな実習: NMS 障害ログ JSON 変換ツール
ここまでの内容を、1ファイルで実行できる最小の CLI ツールにまとめます。
この例では、NMS の障害ログを OpenAI API に渡し、重要度、要約、影響、想定原因、一次対応を JSON で表示します。サーバ、DB、Docker は使用しません。
コード
# mini_nms_log_analyzer.py
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError("OPENAI_API_KEY が設定されていません。")
client = OpenAI()
incident_log = """
2026-03-10 21:14:03 CRITICAL router-core-01 BGP neighbor 10.10.0.2 Down
2026-03-10 21:14:08 WARNING router-core-01 Interface Gi0/1 input errors increased
2026-03-10 21:14:15 CRITICAL nms-poller SNMP timeout router-core-01
""".strip()
system_prompt = """
あなたは NOC の一次対応担当者です。
入力された NMS 障害ログを分析し、JSON のみを返してください。
キーは severity, summary, impact, suspected_cause, first_actions とします。
severity は critical, high, medium, low のいずれかにしてください。
first_actions は3件以内の文字列配列にしてください。
ログだけで断定できない内容は「可能性があります」と表現してください。
""".strip()
response = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": incident_log},
],
)
raw_result = response.choices[0].message.content
result = json.loads(raw_result)
required_keys = {
"severity",
"summary",
"impact",
"suspected_cause",
"first_actions",
}
missing_keys = required_keys - result.keys()
if missing_keys:
raise ValueError(f"不足しているキー: {sorted(missing_keys)}")
print(json.dumps(result, ensure_ascii=False, indent=2))
print(f"\nTotal tokens: {response.usage.total_tokens}")
response_format={"type": "json_object"} を指定し、さらに json.loads() と必須キーの検証を行っています。「JSON で返してください」というプロンプトだけに依存しないのがポイントです。
実行方法
必要なパッケージをインストールします。
pip install openai python-dotenv
スクリプトと同じディレクトリに .env を作成します。
OPENAI_API_KEY=自分のAPIキー
.env は Git に登録しません。
.env
次のコマンドで実行します。
python examples/mini_nms_log_analyzer.py
実行場所によって .env が見つからない場合は、プロジェクトルートから実行するか、環境変数として OPENAI_API_KEY を設定してください。
予想結果
文章は実行ごとに多少変わりますが、次のような JSON が表示されます。
{
"severity": "critical",
"summary": "router-core-01 で BGP セッション断、インターフェースエラー増加、SNMP timeout が発生しています。",
"impact": "外部接続または監視対象装置への到達性に影響している可能性があります。",
"suspected_cause": "回線または対向装置の障害、インターフェース品質低下の可能性があります。",
"first_actions": [
"router-core-01 への疎通と SNMP 応答を確認する",
"Gi0/1 の状態とエラーカウンタを確認する",
"BGP neighbor 10.10.0.2 と対向装置の状態を確認する"
]
}
Total tokens: 250
Total tokens は入力と出力によって変わります。別のログを試す場合は、コード内の incident_log を置き換えるだけです。
NMS 開発での意味
このツールは障害を自動復旧するものではありません。次のような前処理や運用支援の最小構成です。
- SNMP Trap や syslog を人間が読みやすい形に整理する
- 複数アラートから障害チケットの下書きを作る
-
severityを既存 NMS の重要度へマッピングする -
first_actionsを NOC 担当者の確認リストとして表示する -
usage.total_tokensを保存し、障害1件あたりの API コストを追跡する
実務へ拡張する場合も、LLM の出力を直接コマンド実行や設定変更へ接続せず、JSON schema の検証と NOC オペレータの確認を間に置くべきです。
業務で使うなら
OpenAI API を使った最初の AI プログラムを、NMS の現場で使うなら、いきなり自動復旧まで進める必要はないと思います。
最初に狙うなら、次のような小さな補助が現実的です。
- 障害ログを JSON 形式で要約する
- アラートの重要度と想定影響を整理する
- チケットの初期説明文を生成する
- 一次対応チェックリストを作る
- 運用者向けと顧客向けで文面を変える
- LLM の出力とトークン使用量を監査ログとして残す
特に、JSON 形式で返す設計は使いやすいです。
たとえば NMS 側では、severity を画面の色分けに使い、summary をチケットタイトルに使い、first_actions を担当者向けチェックリストとして表示できます。
{
"severity": "critical",
"summary": "router-core-01 で BGP セッション断が発生しています。",
"impact": "外部接続に影響する可能性があります。",
"first_actions": [
"対象装置への疎通を確認する",
"BGP neighbor の状態を確認する",
"直近の設定変更と回線アラートを確認する"
]
}
このような形式なら、LLM の回答を単なる文章ではなく、既存システムのデータとして扱えます。
もちろん、実際の運用では確認すべき点が多いです。
API Key 管理、アクセス権限、ログの機密情報、障害情報の外部送信可否、レスポンス失敗時のフォールバック、出力検証、コスト管理。どれも避けて通れません。
それでも、小さな社内検証として「障害ログを JSON に整形する」くらいから始めるのは、かなり良い入口だと感じました。
失敗したこと
最初の API 実習で、個人的に引っかかった点をまとめると次のようになります。
まず、API Key の扱いです。
コードに直接書かないことは分かっていても、ノートブックで試していると雑になりがちです。.env に移したあとも、load_dotenv() を忘れる、実行ディレクトリが違う、環境変数名を間違える、といった小さなミスが起きます。
次に、レスポンス構造です。
最初は回答本文がどこにあるのか迷いました。response.choices[0].message.content にたどり着くまで、response、choices、message を一つずつ確認する必要がありました。
そして、JSON 出力です。
「JSON だけで返して」と書いても、実際には余計な説明が混ざる可能性があります。キー名が揺れることもあります。後続処理で使うなら、parse と validation は必須です。
最後に、モデルとパラメータです。
gpt-4o-mini のようなモデル名、temperature、max_tokens、stop などは、最初は単なる設定値に見えます。しかし実務では、安定性、コスト、応答速度、出力品質に関わる設計項目です。
小さな実習でも、業務利用を考えると見るべきポイントが一気に増える。
これが、最初に感じた正直な印象でした。
気づいたこと
OpenAI API を使って最初の AI プログラムを作ってみて、一番大きな気づきは、LLM アプリケーションは「プロンプトを書く」だけではないということです。
実際には、次のような要素を組み合わせる必要があります。
- 入力データをどう作るか
-
systemメッセージで何を固定するか -
userメッセージに何を渡すか - 出力を自然文にするか JSON にするか
- 失敗した出力をどう扱うか
- トークン使用量をどう監視するか
- 会話履歴をどこまで保持するか
これは、通常の業務システム開発とかなり近いです。
API を呼び、レスポンスを受け取り、必要な値を取り出し、検証し、後続処理に渡す。
違うのは、相手が決定的なプログラムではなく、確率的に文章を生成するモデルだということです。
だからこそ、LLM の自由度を活かす部分と、アプリケーション側で固定する部分を分ける必要があります。
障害ログの要約で言えば、ログを読んで「何が起きていそうか」を整理する部分は LLM に任せられます。一方で、JSON schema、必須項目、チケット登録、通知先の判定、最終承認はアプリケーション側や人間側で管理すべきです。
この境界線を設計することが、LLM を実務システムに入れるときの重要な仕事だと感じました。
まとめ
OpenAI API で最初の AI プログラムを作ること自体は、難しくありません。
OpenAI クライアントを作り、model を指定し、messages を渡し、response.choices[0].message.content を取り出す。これだけで、Python から LLM を呼び出せます。
しかし、業務で使う目線に切り替えると、見るべきポイントは一気に増えます。
API Key を安全に管理する。.env と環境変数を正しく使う。system と user の役割を分ける。JSON として扱える出力を設計する。トークン使用量を確認する。会話履歴を自分で管理する。
これらはどれも派手な技術ではありません。
でも、NMS や障害対応システムに LLM を組み込むなら、こうした地味な基礎こそが土台になります。
週末プロジェクトで作った FAQ チャットボットも、最初は単純な API 呼び出しから始まりました。しかし、FAQ 検索、context の注入、参照元の表示、エラー処理、UI まで加えると、一気に業務アプリケーションらしくなりました。
今回作った障害ログ JSON 要約のような小さな例でも、LLM を単なるチャットではなく、既存システムの中で使える処理部品として見る感覚が少しつかめました。
次回は、この処理部品をもう少し安定させるために、Prompt Engineering について整理します。
単に「お願いの文章をうまく書く」話ではなく、入力、制約、出力形式をどう設計するかという視点で見ていきます。
