3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

5. EmbeddingとVector DBをネットワーク運用目線で理解する

3
Last updated at Posted at 2026-07-17

連載: NMS開発者がLLMブートキャンプで学んだこと

← 前回次回 →

前回は、長い障害ログをそのまま LLM に渡すのではなく、入力する情報量や出力の揺れを設計する必要がある、という話をしました。

ただ、入力を絞ると言っても、そもそも「LLM に渡すべき情報」をどう探せばよいのでしょうか。

NMS の現場には、障害対応マニュアル、過去チケット、FAQ、手順書、構成メモ、ベンダー資料、監視ルール説明など、いろいろな文書があります。人間が検索するときは、キーワードで探すことが多いです。

しかし、キーワード検索だけでは見つけにくいケースがあります。

「BGP neighbor down」と検索すれば BGP の手順は見つかるかもしれません。でも、運用者が「対向ルータとの経路が切れた」と質問したらどうでしょうか。「SNMP polling failed」と「監視サーバから装置へ到達できない」は、言葉は違っても近い意味です。

ここで出てくるのが Embedding と Vector DB です。

今回は、授業で扱った OpenAIEmbeddings、cosine similarity、RecursiveCharacterTextSplitter、FAISS、BM25 との違いを、ネットワーク運用の FAQ 検索という例に寄せて整理します。

背景

RAG という言葉を最初に聞いたとき、少し大きな仕組みに見えました。

Retrieval-Augmented Generation。

検索して、見つけた文書を LLM に渡して、回答を生成する。

言葉で説明すると分かります。しかし実装に近づくと、RAG の前に理解しないといけない部品があります。

  • 文書をどう分割するか
  • 分割した文書をどう数値化するか
  • 質問と文書の近さをどう測るか
  • 近い文書をどう保存し、どう取り出すか
  • 検索結果が正しいかどう評価するか

この中で中心になるのが Embedding です。

授業では、最初に Embedding を「テキストをベクトルに変換するもの」として扱いました。

from langchain_openai import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

query_vector = embedding_model.embed_query("インターフェースが down したときの確認手順")

query_vector は人間が読める文章ではありません。たくさんの数値が並んだベクトルです。

最初はこれが少し不思議でした。

「文章を数値にする」と言われても、それが検索にどう効くのか分かりにくい。

でも、NMS の現場に置き換えると少し見え方が変わります。

運用者が知りたいのは、文字列が完全一致する文書ではありません。意味が近い文書です。

質問:
BGP peer が落ちたとき、最初に何を見ればよいですか?

文書A:
BGP neighbor down 発生時は、peer 疎通、BGP state、回線アラート、直近の設定変更を確認する。

文書B:
プリンタ再起動後は coldStart trap が発生することがある。

この場合、質問に neighbor という単語がなくても、文書Aを取りたいです。

Embedding は、この「意味が近い」を扱うための表現だと理解しました。

Embedding を触ってみる

2週目の授業では、まず短い文章を Embedding に変換し、ベクトルの長さや値を確認しました。

from langchain_openai import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

query = "人工知能が世界を変えています"
query_vector = embedding_model.embed_query(query)

print(len(query_vector))
print(query_vector[:5])

ここで見たかったのは、ベクトルの中身を人間が解釈することではありません。

同じモデルで変換したテキスト同士なら、ベクトル空間上の距離や角度で近さを測れる、という感覚です。

授業では、次のような文を用意して cosine similarity を見ました。

texts = [
    "AIが世の中を変えています",
    "AIが医療分野を革新している",
    "機械学習で病気を予測できる",
    "今日の夕食にチキンを注文した"
]

doc_vectors = embedding_model.embed_documents(texts)

そして、sklearn.metrics.pairwisecosine_similarity で近さを見ます。

from sklearn.metrics.pairwise import cosine_similarity

all_vectors = [query_vector] + doc_vectors
similarity_matrix = cosine_similarity(all_vectors)

ここで面白かったのは、完全に同じ単語がなくても、意味が近い文同士のスコアが近くなることです。

もちろん完璧ではありません。

でも、キーワード検索だけとは違う検索ができることは分かります。

NMS の FAQ で言えば、次のような表現の違いを吸収できる可能性があります。

interface down
link down
port down
Gi0/1 が落ちた
回線断
物理リンク断

全部を同じ意味として扱えるわけではありませんが、単純な文字列一致よりは近い文書を拾いやすくなります。

ネットワーク障害 FAQ を作ってみる

授業の例では、社内規定、AI レポート、製品マニュアルのようなサンプル文書を使っていました。

ここでは、NMS 開発者目線で小さな FAQ を作ってみます。

from langchain_core.documents import Document

faq_docs = [
    Document(
        page_content="""
BGP neighbor down が発生した場合は、まず peer IP への疎通を確認する。
次に BGP state、回線アラート、直近の設定変更、対向装置側の障害情報を確認する。
""".strip(),
        metadata={"category": "routing", "title": "BGP neighbor down 初動"}
    ),
    Document(
        page_content="""
Interface down が発生した場合は、物理リンク状態、光レベル、対向ポート、
ケーブル交換履歴、メンテナンス予定を確認する。
""".strip(),
        metadata={"category": "interface", "title": "Interface down 確認"}
    ),
    Document(
        page_content="""
SNMP timeout が発生した場合は、監視サーバから対象装置への疎通、
SNMP community、ACL、装置 CPU 高騰、監視経路の障害を確認する。
""".strip(),
        metadata={"category": "monitoring", "title": "SNMP timeout 確認"}
    ),
    Document(
        page_content="""
CPU 使用率が高い場合は、直近のトラフィック増加、プロセス状態、
制御プレーンへの負荷、ログ出力増加、攻撃兆候を確認する。
""".strip(),
        metadata={"category": "resource", "title": "CPU 高騰確認"}
    ),
]

この FAQ はまだ小さいです。

でも、RAG の入口としては十分です。

運用者が次のように聞いたとします。

router-core-01 で peer 10.10.0.2 との経路が切れました。何から見ればよいですか?

キーワードだけを見ると、BGP neighbor down という文字は質問に入っていません。

しかし意味としては BGP の FAQ に近いです。

この「近い」を検索するために、Embedding と Vector DB を使います。

ここで、Embedding と Vector DB の流れを NMS の FAQ 検索として整理すると、次のようになります。

embedding_vector_db_nms.png

ポイントは、LLM に長いマニュアル全体を渡すのではなく、まず FAQ や Runbook を chunk に分け、Embedding に変換し、質問に近い chunk だけを検索することです。

NOC 担当者の質問も同じようにベクトル化され、Vector DB 上で近い文書が探されます。つまり Vector DB は「LLM の記憶」ではなく、LLM に渡すべき根拠文書を探すための検索 index として見る方が実務的です。

FAISS に保存する

2週目の授業では、LangChain の FAISS VectorStore を使いました。

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vectorstore = FAISS.from_documents(faq_docs, embeddings)

これで、FAQ 文書の Embedding が作られ、FAISS の index に保存されます。

検索するときは、次のようにします。

query = "router-core-01 で peer 10.10.0.2 との経路が切れました。何から見ればよいですか?"

results = vectorstore.similarity_search_with_score(query, k=3)

for doc, score in results:
    print(score, doc.metadata["title"])
    print(doc.page_content)

また、Retriever として使うこともできます。

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

docs = retriever.invoke("BGP の peer が落ちたときの確認手順")

授業では、FAISS をローカルに保存する例もありました。

vectorstore.save_local("faiss_docs")

loaded_vs = FAISS.load_local(
    "faiss_docs",
    embeddings,
    allow_dangerous_deserialization=True
)

このあたりで、Vector DB を少し具体的に理解できるようになりました。

最初は名前だけ見ると、新しい種類のデータベースのようで少し距離を感じます。

しかし、ここで使っている役割だけを見ると、「Embedding の近さで検索するための index」と考えると分かりやすいです

通常の DB では、device_idcreated_atseverity で検索しやすいように index を作ります。Vector DB では、文章を変換した Embedding 同士の近さで検索しやすいように index を作ります。

そう考えると、完全に別世界の技術というより、検索対象が文字列や日時からベクトルに変わったものとして理解できます。

Text Splitter は地味だがかなり重要

Embedding よりも地味に見えて、実務ではかなり効きそうだと思ったのが Text Splitter です。

2週目の授業では、RecursiveCharacterTextSplitter を使って長い文章を chunk に分けました。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    separators=["\n\n", "\n", ". ", " ", ""]
)

chunks = splitter.split_text(long_text)

さらに、トークン数を基準に分割する例もありました。

splitter_tiktoken = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    model_name="gpt-4o-mini",
    chunk_size=50,
    chunk_overlap=10,
)

chunks = splitter_tiktoken.split_text(long_text)

最初は、chunk は単に長い文章を切るためのものだと思っていました。

でも、RAG や FAQ 検索で考えると、chunk の切り方は検索品質そのものに影響します。

たとえば、障害対応マニュアルに次のような章があるとします。

1. BGP neighbor down
1.1 初動確認
1.2 対向装置確認
1.3 経路影響確認
1.4 顧客通知基準

これを大きすぎる chunk にすると、「BGP の全体章」がまとめて検索されます。

その場合、質問が「最初に何を見るべきか」でも、「顧客通知基準を知りたい」でも、同じ大きな chunk が返るかもしれません。

一方、小さすぎる chunk にすると、次のようになります。

peer IP への疎通を確認する。

この一文だけでは、何の peer なのか、どの障害手順の一部なのかが分かりません。

文脈が失われます。

授業で chunk_sizechunk_overlap を変えて試したとき、このバランスが実務ではかなり難しそうだと感じました。

NMS のマニュアルなら、単純に文字数で切るだけでは足りないかもしれません。

  • 見出し単位で切る
  • 手順番号を残す
  • 装置種別や障害種別を metadata に入れる
  • 前後の文脈を overlap で少し残す
  • 表や箇条書きは意味が壊れないように扱う

こうした工夫が必要になります。

chunk サイズで失敗する

今回のテーマで一番分かりやすい失敗は、chunk サイズです。

大きく切りすぎると、検索結果が粗くなります。

たとえば、ひとつの chunk に BGP、OSPF、interface、SNMP timeout、CPU 高騰の手順が全部入っているとします。

質問が「SNMP timeout の初動は?」でも、その巨大 chunk が返ってきます。LLM に渡せば、どこかに SNMP の情報はあります。しかし、余計な情報も大量に入ります。

これは前回の Token の話ともつながります。

検索で大きすぎる chunk を取ると、LLM に渡す context が増えます。コストも増えますし、回答が余計な情報に引っ張られることもあります。

逆に、小さく切りすぎても失敗します。

ACL を確認する。

この chunk だけが検索されても、何の ACL なのか分かりません。SNMP timeout なのか、BGP peer なのか、顧客向け FW なのか、文脈が抜けています。

運用手順では、1行だけでは意味が足りないことが多いです。

私はここで、chunk は「検索のためのデータ設計」だと感じました。

テーブル設計で正規化しすぎると join が複雑になる。逆に全部1テーブルに詰めると検索や更新が重くなる。

それに少し似ています。

RAG の文書も、細かければよいわけでも、大きければよいわけでもありません。

検索したときに、質問に答えるための最小限の文脈が入っている chunk にする必要があります。

cosine similarity を自分で見る

4週目の授業では、OpenAIEmbeddings で文書をベクトル化し、numpy で cosine similarity を計算するコードもありました。

import numpy as np

def vector_search(query, docs, doc_embs, top_k=3):
    q_emb = np.array(embeddings.embed_query(query))
    similarities = np.dot(doc_embs, q_emb) / (
        np.linalg.norm(doc_embs, axis=1) * np.linalg.norm(q_emb)
    )
    top_indices = similarities.argsort()[::-1][:top_k]
    return [(i, similarities[i]) for i in top_indices]

FAISS を使うと、こうした計算を効率よくやってくれます。

でも、一度自分で cosine similarity を見ると、Vector DB の中で何が起きているかが少し分かります。

質問を Embedding にする。

文書も Embedding にする。

質問ベクトルと文書ベクトルの近さを計算する。

近い順に並べる。

これが意味検索の基本です。

NMS の FAQ で考えると、次のような使い方になります。

query = "監視サーバからルータに SNMP が通らない"

results = vector_search(
    query,
    [doc.page_content for doc in faq_docs],
    np.array(doc_embeddings),
    top_k=3
)

for idx, score in results:
    print(score, faq_docs[idx].metadata["title"])

ここでスコアを見ることも大事です。

検索結果の1位だけを見ると、当たっているように見えることがあります。

しかし、1位と2位のスコア差がほとんどない場合、判断は不安定かもしれません。全体的にスコアが低い場合、そもそも FAQ に該当する文書がない可能性もあります。

実務で使うなら、上位文書だけでなくスコアも見たいです。

5分で試す Python 標準ライブラリだけで NMS FAQ 検索作り

ここまでの説明だけだと、Embedding と Vector DB が少し抽象的に見えるかもしれません。

そこで、API Key も外部ライブラリも使わずに、Python 標準ライブラリだけで「NMS FAQ をベクトル化して cosine similarity で検索する」最小例を作ってみます。

実際の Embedding モデルではなく、単語ベースの簡易ベクトル化を使います。精度の高い検索エンジンを作ることが目的ではありません。

目的は、次の構造を手で確認することです。

FAQ 文書
  -> tokenize
  -> vectorize
  -> query も vectorize
  -> cosine similarity
  -> top_k FAQ を返す

実務版では、vectorize() の部分を OpenAIEmbeddings や SentenceTransformer に置き換え、検索部分を FAISS、Chroma、Qdrant などに置き換えます。

#nms_faq_vector_search.py

import math
import re
import sys
from collections import Counter

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")


FAQS = [
    {
        "id": "bgp_neighbor_down",
        "title": "BGP neighbor down 初動",
        "text": (
            "BGP neighbor down が発生した場合は、peer IP への疎通、"
            "BGP state、回線アラート、直近の設定変更、対向装置側の障害情報を確認する。"
        ),
    },
    {
        "id": "interface_down",
        "title": "Interface down 確認",
        "text": (
            "Interface down が発生した場合は、物理リンク状態、光レベル、"
            "対向ポート、ケーブル交換履歴、メンテナンス予定を確認する。"
        ),
    },
    {
        "id": "snmp_timeout",
        "title": "SNMP timeout 確認",
        "text": (
            "SNMP timeout が発生した場合は、監視サーバから対象装置への疎通、"
            "SNMP community、ACL、装置 CPU 高騰、監視経路の障害を確認する。"
        ),
    },
    {
        "id": "cpu_high",
        "title": "CPU 高騰確認",
        "text": (
            "CPU 使用率が高い場合は、直近のトラフィック増加、プロセス状態、"
            "制御プレーンへの負荷、ログ出力増加、攻撃兆候を確認する。"
        ),
    },
]


SYNONYMS = {
    "peer": ["対向", "neighbor", "隣接"],
    "bgp": ["経路", "routing", "route"],
    "snmp": ["監視", "polling", "ポーリング"],
    "timeout": ["応答", "返ってこない", "到達できない", "通らない"],
    "interface": ["link", "port", "ポート", "リンク", "回線"],
    "down": ["切れた", "落ちた", ""],
    "cpu": ["負荷", "使用率"],
}


def tokenize(text):
    normalized = text.lower()
    tokens = re.findall(r"[a-z0-9_./-]+|[ぁ-んァ-ン一-龥]+", normalized)

    expanded = list(tokens)
    for canonical, words in SYNONYMS.items():
        if canonical in tokens or any(word.lower() in normalized for word in words):
            expanded.append(canonical)
    return expanded


def vectorize(text):
    return Counter(tokenize(text))


def cosine_similarity(vec_a, vec_b):
    common = set(vec_a) & set(vec_b)
    dot = sum(vec_a[token] * vec_b[token] for token in common)
    norm_a = math.sqrt(sum(value * value for value in vec_a.values()))
    norm_b = math.sqrt(sum(value * value for value in vec_b.values()))
    if norm_a == 0 or norm_b == 0:
        return 0.0
    return dot / (norm_a * norm_b)


def search(query, top_k=3):
    query_vec = vectorize(query)
    results = []

    for faq in FAQS:
        doc_vec = vectorize(f"{faq['title']} {faq['text']}")
        score = cosine_similarity(query_vec, doc_vec)
        results.append((score, faq))

    return sorted(results, key=lambda item: item[0], reverse=True)[:top_k]


def main():
    queries = [
        "router-core-01 で peer 10.10.0.2 との経路が切れた。何から確認する?",
        "監視サーバからルータに SNMP の応答が返ってこない",
        "Gi0/1 のリンクが落ちた",
    ]

    for query in queries:
        print("=" * 80)
        print(f"query: {query}")
        for rank, (score, faq) in enumerate(search(query), start=1):
            print(f"{rank}. score={score:.3f} [{faq['id']}] {faq['title']}")
            print(f"   {faq['text']}")


if __name__ == "__main__":
    main()

実行方法は次の通りです。

python nms_faq_vector_search.py

実行すると、3つの NMS 運用質問に対して、関連度の高い FAQ が score 順に表示されます。

================================================================================
query: router-core-01 で peer 10.10.0.2 との経路が切れた。何から確認する?
1. score=0.506 [bgp_neighbor_down] BGP neighbor down 初動
   BGP neighbor down が発生した場合は、peer IP への疎通、BGP state、回線アラート、直近の設定変更、対向装置側の障害情報を確認する。
2. score=0.285 [interface_down] Interface down 確認
   Interface down が発生した場合は、物理リンク状態、光レベル、対向ポート、ケーブル交換履歴、メンテナンス予定を確認する。
3. score=0.048 [snmp_timeout] SNMP timeout 確認
   SNMP timeout が発生した場合は、監視サーバから対象装置への疎通、SNMP community、ACL、装置 CPU 高騰、監視経路の障害を確認する。

================================================================================
query: 監視サーバからルータに SNMP の応答が返ってこない
1. score=0.641 [snmp_timeout] SNMP timeout 確認
   SNMP timeout が発生した場合は、監視サーバから対象装置への疎通、SNMP community、ACL、装置 CPU 高騰、監視経路の障害を確認する。

================================================================================
query: Gi0/1 のリンクが落ちた
1. score=0.567 [interface_down] Interface down 確認
   Interface down が発生した場合は、物理リンク状態、光レベル、対向ポート、ケーブル交換履歴、メンテナンス予定を確認する。

このミニ実習では、SYNONYMS がかなり重要です。

たとえば、経路が切れた という表現を bgpdown に寄せたり、応答が返ってこないtimeout に寄せたりしています。

実務の NMS 検索でも、同じ発想が必要になります。

  • linkDowninterface down回線断 を近い表現として扱う
  • Trap 名、OID、interface 名、装置名を正規化する
  • routerswitchfirewall など装置種別を metadata に入れる
  • score が低い場合は「該当手順なし」と返す
  • BM25 と Vector search を組み合わせる

この例は本番用の検索エンジンではありません。

ただし、FAQ 文書と質問を同じベクトル表現に変換し、近さで並べ替えるという構造は、Embedding + Vector DB の最小モデルとして理解しやすいと思います。

検索結果が期待と違う

Embedding 検索を試してすぐに分かったのは、「意味で検索できる」と言っても、いつも期待通りに出るわけではないということです。

たとえば、質問がこうだったとします。

装置から応答が返ってこない

この質問は曖昧です。

SNMP timeout かもしれません。

ICMP unreachable かもしれません。

装置が down しているかもしれません。

監視サーバ側の経路問題かもしれません。

Embedding 検索は、意味が近そうな文書を返します。しかし、曖昧な質問に対して正しい運用判断まで保証するわけではありません。

また、専門用語も難しいです。

flaplinkDownbgpBackwardTransitionifOperStatuspolling failedNo Such Object のような言葉は、運用ドメインでは意味があります。

Embedding モデルが一般的な意味として近さを見てくれても、社内運用上の重要度や手順までは知りません。

そのため、検索結果が期待と違う場合があります。

質問:
SNMP の応答が返りません

期待:
SNMP timeout 確認手順

実際:
Interface down 確認手順

なぜか。

文書内に「疎通」「対象装置」「確認」という似た単語が多く、Embedding 上では近く見えたのかもしれません。あるいは、SNMP の FAQ が短すぎて意味情報が不足していたのかもしれません。

この失敗を見て、Vector DB に入れれば勝手に賢く検索してくれるわけではないと分かりました。

文書の作り方、metadata、chunk、検索クエリ、top_k、スコア閾値、評価データが必要です。

キーワード検索と Vector 検索の違い

4週目の授業では、BM25、TF-IDF、Vector search、Hybrid search も扱いました。

BM25 は、かなりざっくり言うとキーワードベースの検索です。文書内に検索語がどれくらい出るか、どれくらい特徴的かを見ます。

授業では、シンプルな BM25 クラスを作ったり、LangChain の BM25Retriever を使ったりしました。

from langchain_community.retrievers import BM25Retriever

bm25_retriever = BM25Retriever.from_documents(docs_lc)
bm25_retriever.k = 3

results = bm25_retriever.invoke("Python プログラミング")

一方、Vector search は Embedding の近さで探します。

from langchain_community.vectorstores import FAISS

vectorstore = FAISS.from_documents(docs_lc, embeddings)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

どちらが優れているか、という話ではありません。

得意な場面が違います。

NMS の文書検索で考えると、BM25 が強い場面があります。

  • ifOperStatus
  • bgpBackwardTransition
  • No Such Object
  • router-core-01
  • Gi0/1
  • OID 1.3.6.1.2.1...

こういう固有名詞、OID、装置名、イベントコードは、キーワード検索がかなり強いです。

一方、Vector search が強い場面もあります。

  • 「経路が切れた」
  • 「対向装置と通信できない」
  • 「監視から見えない」
  • 「CPU が張り付いている」
  • 「リンクが不安定」

言い換えが多い質問では、Embedding 検索が効きやすいです。

つまり、ネットワーク運用の検索では、BM25 と Vector search の両方が欲しくなります。

Hybrid search が現実的に見える

4週目の授業では、BM25 と Vector search を組み合わせる EnsembleRetriever や、RRF(Reciprocal Rank Fusion) の考え方も出てきました。

from langchain_classic.retrievers import EnsembleRetriever

ensemble = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.5, 0.5],
)

重みを変えて、上位結果がどう変わるかも見ました。

for w_vec, w_bm25 in [(0.2, 0.8), (0.5, 0.5), (0.8, 0.2)]:
    ensemble = EnsembleRetriever(
        retrievers=[vector_retriever, bm25_retriever],
        weights=[w_vec, w_bm25],
    )
    results = ensemble.invoke("Python データ科学")

これを NMS FAQ に置き換えると、かなり現実的です。

たとえば、質問に bgpBackwardTransition という Trap 名が入っているなら、BM25 の重みを強くしたいです。逆に「経路が切れた」という自然文なら、Vector search の重みを強くしたいかもしれません。

BM25 が効きやすい:
SNMP OID、Trap 名、装置名、interface 名、エラーコード

Vector search が効きやすい:
自然文の質問、言い換え、症状説明、手順の意味検索

実務では、最初からどちらか一方に決めるより、Hybrid search の方が扱いやすそうです。

ただし、Hybrid にすれば自動的に良くなるわけではありません。

重みをどうするか、どのクエリで評価するか、上位何件を使うかを見なければいけません。

検索品質を測る

4週目の授業で印象に残ったのは、検索品質を指標で見るところです。

precision_at_krecall_at_kmrr のような関数を作りました。

def precision_at_k(retrieved, relevant, k):
    top_k = retrieved[:k]
    return len(set(top_k) & set(relevant)) / k

def recall_at_k(retrieved, relevant, k):
    top_k = retrieved[:k]
    return len(set(top_k) & set(relevant)) / len(relevant) if relevant else 0

def mrr(retrieved, relevant):
    for rank, doc_id in enumerate(retrieved, 1):
        if doc_id in relevant:
            return 1.0 / rank
    return 0.0

最初は、検索結果を目で見れば十分ではないかと思っていました。

でも、RAG を実務に近づけるなら、目視だけでは厳しいです。

たとえば、NMS FAQ 検索で評価データを作るなら、次のようになります。

eval_dataset = [
    {
        "query": "BGP peer が落ちたときの初動は?",
        "relevant": ["bgp_neighbor_down"]
    },
    {
        "query": "監視サーバから装置に SNMP が通らない",
        "relevant": ["snmp_timeout"]
    },
    {
        "query": "Gi0/1 が down した",
        "relevant": ["interface_down"]
    },
]

そして、検索結果の上位に正しい文書が入っているかを見る。

これはかなり大事です。

LLM の回答が間違っているとき、人はすぐモデルやプロンプトを疑います。

しかし RAG では、そもそも検索で間違った文書を取っている場合があります。

検索が間違っていれば、LLM は間違った context をもとに、それらしい回答を作ります。

そのため、RAG の前段である Embedding 検索の品質を測る必要があります。

業務で使うなら

Embedding と Vector DB を NMS の現場で使うなら、私は最初に「障害対応 FAQ 検索」から始めたいです。

いきなり自動回答チャットボットにするのではなく、まずは運用者が質問したときに、関連する FAQ や手順書を出すところからです。

たとえば、画面上では次のような機能です。

入力:
router-core-01 で BGP peer 10.10.0.2 が down。何から確認?

検索結果:
1. BGP neighbor down 初動
2. 回線アラート確認手順
3. 直近設定変更確認手順
回答:
まず peer 10.10.0.2 への疎通、BGP state、対象回線のアラートを確認してください。
直近の設定変更やメンテナンス予定も確認対象です。

根拠:
- BGP neighbor down 初動
- 回線アラート確認手順

ここで必ず根拠文書を表示したいです。

運用者は、LLM の回答だけを信じるのではなく、どの文書を根拠にしたのか確認できる必要があります。

また、Vector DB に入れる文書には metadata を付けたいです。

  • category: routing, interface, monitoring, resource
  • device_type: router, switch, firewall
  • severity: high, middle, low
  • source: manual, faq, ticket, vendor_doc
  • updated_at: 最終更新日
  • owner: 運用チーム、ネットワークチームなど

Embedding 検索だけでなく、metadata filter も使えるようにしておくと、実務に近づきます。

たとえば、router の手順だけ、firewall の手順だけ、最新の文書だけ、承認済み FAQ だけ、という絞り込みができます。

まとめ

Embedding と Vector DB は、長い文書を LLM に丸ごと読ませるためのものではなく、必要な根拠文書を探すための仕組みです。

今回、NMS FAQ 検索として試してみることで、RAG の入口は「生成」ではなく「検索」だと分かりました。

まず関連する手順書を見つける。検索結果と score を見る。必要なら BM25 や metadata filter と組み合わせる。その上で、LLM に回答を作らせる。

この順番で考える方が、ネットワーク運用には合っていそうです。

今回の内容は、RAG の入口です。

次回は、この検索結果を LLM に渡して回答を作る RAG について、もう少し踏み込みます。

検索文書が間違っていれば、回答も間違います。次はその検索品質をどう改善するか、rerank や評価をどう考えるかを、引き続きネットワーク運用目線で整理します。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?