1
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?

PDFのドキュメントしかないネットワーク機器をAIエージェント(Claude Code)でバイブコーディングしたい

1
Posted at

古河電工 FITELnet F310のPDFドキュメントをMarkdownにテキスト化&INDEX.mdの作成、コマンドリファレンスをJSONに構造化することで、Claude Codeから精度良くコンフィグを生成出来るようになりました。

筆者の自宅用ルーターとして購入した FITELnet F310 ですが、Ciscoライクなコマンドと言えど特有の操作や方言が多く、設定に苦戦していたのでなんちゃってRAGのような形で実装してみました。

やったことと工夫を記事に残します。

はじめに

IT製品とは言えどドキュメントがPDFでしか提供されていない製品は数多くあると思います。
今回はプライベートで買って設定を行なっている、古河電工 FITELnet F310で行っていますが、他の製品においてもAIエージェントで扱う際のヒントになると思って記事の作成を行っています。

当記事でAIエージェントはClaude Codeを使用していますが、OpenCodeやCodexなどでも同様に応用できるテクニックになると思います。

AIエージェントの弱点

AIエージェントでは学習で得た知識のベースに、ツールを使用したり、検索を行ったりすることでコードを書いてくれます。ネットワーク機器ではCisco製品のように業界標準と言えるくらいに普及していたり、VyOSのようにオープンなコミュニティで検索時に情報が多く出てくる製品では、比較的使えるコンフィグを出力してくれます。

しかしながら、ネット上に情報がそこまで多くない製品だと、AIの学習データにも情報が少ない上に、検索ツールを使っても情報が多くないために精度の悪い結果が出力されます。
具体的にはコンフィグのSyntaxが誤っている。ACLの解釈が誤っている…etc

でもAIエージェントにコンフィグを書いて欲しい

というわけで、今回のアプローチを行いました。
作った仕組みのイメージ図がこちらです。

PDF_Skill イメージ図.png

アプローチ

AIが詳しいこと

前提としてネットワーク構築においてAIは、RFCなどの標準化文章や、プロトコルなど基本的な技術の構成については理解していると感じます。

例:

  • tag VLANはIEEE 802.1Qの規格について
  • RIP, OSPF, EIGRP, BGPの使い分け
  • IPsecを使用する際に必要なパラメーター

このような情報はスラスラと出力できるかと思います。
つまり基本的な部分の設計やパラメータ設計などの部分はAIエージェントを実用的に使用できます。

AIが苦手にすること

Ciscoなど業界標準のものを除き、国産ネットワーク機器など製品における具体的なコマンド、パラメーターの渡し方は苦手です。

例:

  • 「FITELnet F310で」タグVLANをGigaEhternet 1/3ポートに設定する方法
  • 「FITELnet F310で」BGPの設定を行いたい
  • 「FITELnet F310で」センタ-拠点間をVPN接続したい

このような具体的なコマンドやコンフィグを求めると、Syntaxが誤っていたり、そのままでは動かないコンフィグが出力され、精度が悪いなと感じることが多々あります。

これは基本的な部分を理解していないというよりも、コマンド体系などの情報が少ないために発生します。
しかし、コマンドが通らないことのトラブルシューティングを始めてしまったり、パラメータを変えながら迷走してしまったりすることでさらに精度が悪くなり、非常に勿体無い動きをすることがあります。

どのように解決するか

「ネットワークについての基本知識を持っているにも関わらず、ベンダー固有の表現や方言で迷走してしまう。」
これを解決するには、ベンダーが提供する機器のマニュアル・ドキュメントをAIエージェントに食わせてしまえば解決します。

しかし、一括でマニュアルをAIに渡すことは分量の都合で現実的ではありません。
実際にFITELnet F310の場合、マニュアルとして10冊のPDFに3,873ページ、約356万文字の容量があります。

これを解決するのはRAG的なアプローチになりますが、RAGを構築するのは手間がかかるのと、コスパが悪いので、工夫してAgent Skillのような形で実装します。

AIエージェントが自発的に検索できる土台を作る

FITELnet F310では機能説明書、コマンドリファレンス(構成定義編,運用管理編)と仕様情報など10冊のマニュアルPDFが公開されています。

これをAIエージェントが使用しやすい形にしていきます。
今回はgrepとjqを使えば欲しい情報を探し当てることができることを目標にドキュメントを整えました。

PDFのページ情報付きでのテキスト化

PDFのままだとAIエージェントが扱いにくいので、MuPDFを使用してPDFをプレインテキストに変換します。
この際に平たい巨大なテキストにするとそれはそれで扱いづらいので、MuPDFで変換した際に出てくる改ページ符号を使用してページ数を挿入します。これにより、目次や索引にあるページ数を使った際に参照するのが容易になります。

ページ数の挿入イメージ
===== PAGE 1 =====  <<--これ

FITELnet
F70/F71/F220/F221/F225/F310

F220 EX/F221 EX
メッセージ集

===== PAGE 2 =====  <<--これ

メッセージ集

2

はじめに

このたびは、本装置をお買い上げいただき、まことにありがとうございます。

インターネットやLAN をさらに活用するために、本装置をご利用ください。

本ドキュメントには「外国為替及び外国貿易管理法」に基づく特定技術が含まれています。

従って本ドキュメントを輸出または非居住者に提供するとき、同法に基づく許可が必要となります。

©2019-2020 Furukawa Electric Co., Ltd

===== PAGE 3 =====  <<--これ
コマンド索引(JSON)の作成

JQなどのコマンドを使用してコマンドを参照できるようにコマンドリファレンスから構造化したコマンド索引をJSONで作成しました。
これによりAIエージェントが コマンドにあたりをつける→jqでクエリ→ヒットしたコマンドの簡単なSyntaxとドキュメントの場所を特定 という流れでコマンドを検索できるようになります。
コマンドリファレンスからはPythonスクリプトを使用して次の情報を抽出しています。

{
  "command": "ntp server",          // コマンド名
  "manual": "cmd_refe_config",      // 収録マニュアル(mdファイルの stem)
  "category": "構成定義編",
  "page": 57,                        // PDF物理ページ
  "function": "NTP サーバの登録",    // 【機能】
  "syntax": "ntp [vrf <VRF 名>] server <NTP サーバ> ...",  // 【入力形式】(no形式も含む)
  "mode": "基本設定モード",          // 【動作モード】
  "ref": "cmd_refe_config.pdf p.57"
}
AIエージェントに処理順のルール化

テキスト化、コマンド索引を作成してもそのままではAIエージェントはこれを使用せず、モデルが持っている記憶で回答を作成するので、調べ方、ドキュメントの使い方の指示は必須です。

INDEX.mdを作成し、マニュアルの場所や検索の使い方を明記しました。

INDEX.mdの中身
# FITELnet F310 マニュアル索引(AI用ルーティング表)

古河電工 FITELnet F310 ルータの純正マニュアルを、ページ番号マーカー付きテキストに変換したもの。
各ファイルは `===== PAGE N =====`(N = PDF物理ページ)でページ区切りされている。
調べ物のときは **まず下表で対象ファイルを選び → `grep -n` で該当ページを特定 → その周辺を読む**。

| ファイル | ページ | 収録内容 | こんな時に見る |
|---|---:|---|---|
| `cmd_refe_config.md` | 1136 | **コマンドリファレンス 構成定義編**。configモードの全コマンド構文・パラメータ・デフォルト値 | 「このコマンドの書式は?」「設定の入れ方は?」 |
| `cmd_refe_ope.md` | 782 | **コマンドリファレンス 運用管理編**。show系・運用/保守コマンド | 「状態確認・運用コマンドの使い方」 |
| `kinou.md` | 114 | **機能説明書**。搭載機能の解説(ルーティング/VPN/QoS等の概念) | 「この機能は何をする?」「機能の全体像」 |
| `message.md` | 1604 | **メッセージ一覧**。ログ/syslog/エラーメッセージの意味と対処 | 「このログの意味は?」「エラー番号の対処」 |
| `siyou.md` | 78 | **仕様一覧**。ハード/ソフト仕様、MIB/Trap一覧 | 「対応本数・性能値」「MIB/Trap定義」 |
| `trouble.md` | 23 | **トラブルシューティング**。症状別の切り分け | 「動かない時の切り分け」 |
| `goriyouF310.md` | 58 | **ご利用にあたって**。導入・初期設定の概要 | 「最初のセットアップ手順」 |
| `usbband.md` | 21 | **USB脱落防止(取付)**。オプション品の取付説明 | 「USB脱落防止の付け方」 |
| `rackmountRMKB010211wb.md` | 44 | **ラックマウント取付**。RMKB010211取付説明 | 「ラックへの取り付け方」 |
| `oss_list.md` | 13 | **OSSライセンス一覧**(2026年5月版) | 「使用OSSとライセンス」 |

## コマンド索引 `command_index.json`

構成定義編・運用管理編の**全1,914コマンドを構造化したJSON**。コマンド構文をピンポイントで引くとき、
本文をgrepするより速い。1件は次の形:

```json
{
  "command": "ntp server",          // コマンド名
  "manual": "cmd_refe_config",      // 収録マニュアル(mdファイルの stem)
  "category": "構成定義編",
  "page": 57,                        // PDF物理ページ
  "function": "NTP サーバの登録",    // 【機能】
  "syntax": "ntp [vrf <VRF 名>] server <NTP サーバ> ...",  // 【入力形式】(no形式も含む)
  "mode": "基本設定モード",          // 【動作モード】
  "ref": "cmd_refe_config.pdf p.57"
}
```

同名コマンド(`set mtu` 等)はモード違いで複数エントリを持つ。使用例:

```bash
# コマンド構文を引く
python3 -c "import json;print([c for c in json.load(open('md/command_index.json'))['commands'] if c['command']=='ip route'][0]['syntax'])"
# 部分一致でコマンドを探す(jq)
jq -r '.commands[] | select(.command|test(\"ospf\")) | \"\\(.command)  \\(.ref)\"' md/command_index.json
```

生成: `manuals/tool/build_command_index.py`(md更新後に再実行で再生成)

## 検索の例

```bash
# 構成定義コマンドを探す(ファイル:行 で結果が出る)
grep -n "ip route" md/cmd_refe_config.md

# ログメッセージの意味を調べる
grep -n "LINK-UP" md/message.md

# 該当ページ番号を知る(直前の PAGE マーカーを遡る)
grep -n "===== PAGE" md/cmd_refe_config.md | awk -F'[ =]' '$0<=TARGET'
```

## 元データ

- 原本PDF: `manuals/original/*.pdf`(変更していない)
- 変換スクリプト: `manuals/tool/convert_to_md.py`(PDF更新時は再実行で `md/` を再生成)
- 抽出方式: `mutool draw -F txt`(テキスト層をそのまま抽出。OCR不使用)

これに加えて、AGENTS.md(CLAUDE.md)へも参照指示も必要です。
とにかくLLMが持つ記憶任せにせずドキュメントの参照をさせることが重要です。

AGENTS.mdへの指示(抜粋)
## F310について質問されたときの手順

1. 調べ物は **`manuals/md/INDEX.md` を読み**、対象マニュアルを選ぶ(例: コマンド構文→`cmd_refe_config.md`、ログの意味→`message.md`、機能概要→`kinou.md`、仕様→`siyou.md`)。
2. コマンドの構文・ページを引くなら **`command_index.json` を最優先**(本文grepより速い)。
   ```bash
   jq -r '.commands[] | select(.command|test("ospf")) | "\(.command)  \(.ref)"' manuals/md/command_index.json
   ```
3. それ以外は該当 `.md``grep -n` → 直前の `===== PAGE N =====` から物理ページを特定。
4. **回答には必ず出典を添える**。マニュアルは `<manual>.pdf p.N` 形式(`ref` フィールド/PAGEマーカー由来)、
   設定例は各mdファイル末尾の出典URL。ページ番号 = PDF物理ページ。
   原本を開くなら `mutool draw -F txt -o - manuals/original/<file>.pdf N-N`

実装

PDFのテキスト化

以下のポイントでMuPDFとPythonスクリプトを作成し、PDFのテキスト化を行いました。

  • 抽出は mutool draw -F txt(MuPDF)。マニュアルPDF にはテキスト層があるため OCR 不要。
  • mutoolのtxt出力はページ境界が \f(form feed)で区切られる。これで split し、各ページ先頭に ===== PAGE N ===== を挿入する。
    • この 1 行が設計の要。grep -n でヒットした行から直前の PAGE マーカーを遡れば必ず PDF の物理ページに戻れる = 出典として提示できるし、mutool draw ... N-N で原本の該当ページだけを再確認できる。
  • 3行以上の空行を1行に、行末空白除去して変換時の空行をクリーニングしつつ、【機能】【入力形式】といった見出しをそのまま残すことで、後段のパーサの手がかりにする。
PDFをテキストに変換しながらページ情報を埋め込むPythonスクリプト
convert_to_md.py
#!/usr/bin/env python3
"""FITELnet F310 の PDF マニュアルを、ページ番号マーカー付きの Markdown/テキストへ変換する。
- テキスト層をそのまま抽出(OCR不要)
- 各ページの先頭に `===== PAGE N =====` を挿入 → grep で該当ページを特定・引用できる
- 原本 PDF(manuals/original/)は変更しない。出力は manuals/md/ 以下。
"""
import re
import subprocess
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent.parent  # manuals/
SRC = BASE / "original"                        # 原本PDF置き場
OUT = BASE / "md"
OUT.mkdir(exist_ok=True)

# 3行以上の空行を1行に、行末空白を除去
_blank = re.compile(r"\n[ \t]*\n[ \t]*\n+")
_trail = re.compile(r"[ \t]+\n")


def clean(text: str) -> str:
    text = _trail.sub("\n", text)
    text = _blank.sub("\n\n", text)
    return text.strip("\n")


def convert(pdf: Path) -> tuple[int, int]:
    # mutool は txt 出力でページ間を \f (form feed) で区切る
    raw = subprocess.run(
        ["mutool", "draw", "-F", "txt", "-o", "-", str(pdf)],
        capture_output=True, check=True,
    ).stdout.decode("utf-8", errors="replace")

    pages = raw.split("\f")
    out_lines = [f"# {pdf.name}", "", f"source: manuals/original/{pdf.name}", ""]
    n = 0
    for page in pages:
        body = clean(page)
        if not body and n > 0:
            # 末尾の空ページはスキップ(先頭以外)
            continue
        n += 1
        out_lines.append(f"\n===== PAGE {n} =====\n")
        out_lines.append(body)

    md = OUT / (pdf.stem + ".md")
    md.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
    return n, md.stat().st_size


def main():
    pdfs = sorted(SRC.glob("*.pdf"))
    print(f"{len(pdfs)} PDF を変換 → {OUT}\n")
    total_pages = 0
    for pdf in pdfs:
        n, size = convert(pdf)
        total_pages += n
        print(f"  {pdf.name:30s} {n:5d} p  {size/1024:8.1f} KB")
    print(f"\n合計 {total_pages} ページを Markdown 化しました。")


if __name__ == "__main__":
    main()

まず、これでPDFのテキスト化は完了。

コマンド索引(JSON)の作成

こちらもPythonスクリプトを作成して、作業を行ないました。
コマンドリファレンスをテキスト変換したものを観察し、以下の方針で実装しました。

  • アンカーは「コマンド名」ではなく 【機能】 を使用。コマンド名の行は書式が揺れるが、【機能】 は必ず単独行で現れる。全出現位置をブロック境界にし、ブロック内の 【見出し】→本文 を辞書化する。
  • コマンド名は 【機能】 から上方向に最大 12 行スキャンして決定。「英字始まり・日本語を含まない・80 文字以内」をコマンド名らしさの判定に使う。
テキスト化したコマンドリファレンスからコマンド一覧を作成するスクリプト
build_command_index.py
#!/usr/bin/env python3
"""FITELnet F310 コマンドリファレンス(md)から、コマンド索引 JSON を生成する。

各コマンド定義ブロック(コマンド名 → 【機能】→【入力形式】→【動作モード】…)を解析し、
  command / manual / page / function / syntax / mode / ref
を持つ JSON を出力する。AI がコマンド構文をピンポイント参照するための索引。
"""
import json
import re
from datetime import date
from pathlib import Path

BASE = Path(__file__).resolve().parent.parent  # manuals/
MD = BASE / "md"

# 対象マニュアルと分類ラベル
TARGETS = {
    "cmd_refe_config": "構成定義編",
    "cmd_refe_ope": "運用管理編",
}

PAGE_RE = re.compile(r"^===== PAGE (\d+) =====$")
MARK_RE = re.compile(r"^【(.+?)】\s*$")          # 【機能】など単独行の見出し
# コマンド名らしさ: ASCII英字始まり・日本語を含まない・句点で終わらない
JP_RE = re.compile(r"[ぁ-んァ-ヶ一-龠、。:]")
SECNO_RE = re.compile(r"^\d+(?:\.\d+)+\s+")      # 先頭の節番号 例: "10.10.1 "
MODEPAREN_RE = re.compile(r"\s*[((][^))]*モード[^))]*[))]\s*$")  # 末尾の(〜モード)
VERSION_RE = re.compile(r"^V?\d+(?:\.\d+)+$")    # 版数/節番号のみの行 例: V01.00 / 6.1.54


def normalize_name(s: str) -> str:
    s = s.strip()
    s = SECNO_RE.sub("", s)          # 節番号を除去
    s = MODEPAREN_RE.sub("", s)      # 末尾のモード括弧を除去
    return s.strip()


QUALIFIER_RE = re.compile(r"\s*[((][^))]*[))]\s*$")  # 末尾の括弧修飾 例:(IPv4 標準設定)


def is_command_name(s: str) -> bool:
    if not s or len(s) > 80:
        return False
    base = QUALIFIER_RE.sub("", s).strip()   # 末尾の括弧修飾は判定から除外(名前自体は保持)
    if not re.match(r"^[A-Za-z]", base):     # 英字始まり
        return False
    if JP_RE.search(base):                    # 日本語混在は説明文
        return False
    if VERSION_RE.match(base):                # 版数のみは除外
        return False
    return True


def name_from_syntax(syntax: str) -> str:
    """入力形式の先頭行から、パラメータ(< [ { ( )開始まで の固定キーワード列を名前として抽出。
    ページ跨ぎで名前行が取れなかった場合のフォールバック。"""
    for line in syntax.splitlines():
        line = line.strip()
        if not line:
            continue
        line = re.sub(r"^no\s+", "", line)   # 否定形は除く
        toks = []
        for t in line.split():
            if t[0] in "<[{(" or JP_RE.search(t):
                break
            toks.append(t)
        name = " ".join(toks).strip()
        if is_command_name(name):
            return name
        return ""
    return ""


def find_command_name(lines, func_idx):
    """【機能】(func_idx) から上方向に走査してコマンド名を返す。無ければ None。
    版数ブロック(【対応ファームウェアバージョン】とその値)や節番号行は読み飛ばし、
    別の見出し(前コマンドの終端)に達したら打ち切る。"""
    j = func_idx - 1
    steps = 0
    while j >= 0 and steps < 12:
        raw = lines[j]
        s = raw.strip()
        if not s or PAGE_RE.match(raw):
            j -= 1
            continue
        m = MARK_RE.match(s)
        if m:
            if "ファーム" in m.group(1):       # 版数見出し(対応/対象の誤記含む) → 飛ばす
                j -= 1
                continue
            return None                        # 別見出し = 前コマンド領域 → 打ち切り
        name = normalize_name(s)
        if is_command_name(name):
            return name
        # 版数値・節番号・説明断片 → さらに上へ
        j -= 1
        steps += 1
    return None


def parse(stem: str, label: str) -> list[dict]:
    lines = (MD / f"{stem}.md").read_text(encoding="utf-8").splitlines()

    # 行→ページ番号の対応表を作る
    page_at = [0] * len(lines)
    cur = 0
    for i, ln in enumerate(lines):
        m = PAGE_RE.match(ln)
        if m:
            cur = int(m.group(1))
        page_at[i] = cur

    # 1) 全【機能】位置をブロック境界にする
    marks = [i for i, ln in enumerate(lines) if ln.strip() == "【機能】"]

    # 2) 各ブロック(【機能】→ 次の【機能】の手前)でセクション抽出+名前決定
    entries = []
    for k, func_marker in enumerate(marks):
        end = marks[k + 1] if k + 1 < len(marks) else len(lines)
        block = lines[func_marker:end]
        sections = extract_sections(block)

        # 名前: 上方向スキャン。取れなければ入力形式の先頭語で補完(ページ跨ぎ対策)
        name = find_command_name(lines, func_marker)
        if not name:
            name = name_from_syntax(sections.get("入力形式", ""))
        if not name:
            continue  # 凡例など非コマンド

        entries.append({
            "command": name,
            "manual": stem,
            "category": label,
            "page": page_at[func_marker],
            "function": sections.get("機能", ""),
            "syntax": sections.get("入力形式", ""),
            "mode": sections.get("動作モード", ""),
            "ref": f"{stem}.pdf p.{page_at[func_marker]}",
        })
    return entries


def extract_sections(block: list[str]) -> dict:
    """ブロック内の 【見出し】→本文 を辞書化。本文は次の見出しまでを連結。"""
    out, key, buf = {}, None, []

    def flush():
        if key is not None:
            text = "\n".join(buf).strip()
            text = re.sub(r"\n{2,}", "\n", text)
            out[key] = text

    for ln in block:
        m = MARK_RE.match(ln.strip())
        if m:
            flush()
            key, buf = m.group(1), []
        elif key is not None and not PAGE_RE.match(ln):
            buf.append(ln.rstrip())
    flush()
    return out


def main():
    all_entries = []
    per_manual = {}
    for stem, label in TARGETS.items():
        e = parse(stem, label)
        per_manual[stem] = len(e)
        all_entries.append((stem, e))
        print(f"  {stem:20s} {len(e):5d} commands")

    # コマンド名→出現ページのフラット索引
    commands = []
    for _, e in all_entries:
        commands.extend(e)

    doc = {
        "generated": date.today().isoformat(),
        "device": "FITELnet F310",
        "note": "コマンド索引。page は PDF物理ページ。本文は manuals/md/<manual>.md、原本は manuals/original/<manual>.pdf",
        "counts": per_manual,
        "total": len(commands),
        "commands": commands,
    }
    out = MD / "command_index.json"
    out.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"\n合計 {len(commands)} コマンド → {out} ({out.stat().st_size/1024:.1f} KB)")


if __name__ == "__main__":
    main()

結果JSONでコマンド一覧を作れたので、ドキュメントの下処理は完了です。

command_index.json
{
  "generated": "2026-07-25",
  "device": "FITELnet F310",
  "note": "コマンド索引。page は PDF物理ページ。本文は manuals/md/<manual>.md、原本は manuals/original/<manual>.pdf",
  "counts": {
    "cmd_refe_config": 1299,
    "cmd_refe_ope": 615
  },
  "total": 1914,
  "commands": [
    {
      "command": "hostname",
      "manual": "cmd_refe_config",
      "category": "構成定義編",
      "page": 40,
      "function": "ホスト名の設定",
      "syntax": "hostname < ホスト名>",
      "mode": "基本設定モード",
      "ref": "cmd_refe_config.pdf p.40"
    },
    {
      "command": "privilege",
      "manual": "cmd_refe_config",
      "category": "構成定義編",
      "page": 41,
      "function": "コマンドレベルの設定",
      "syntax": "privilege {exec | configure | < コマンドモード移行コマンド名>} [all] {level < コマンドレベル> < コマンド名> | reset}\nno privilege {exec | configure | < コマンドモード移行コマンド名>} [all] level < コマンドレベル> < コマンド名>",
      "mode": "基本設定モード\nパラメーター\n設定内容\n設定範囲\n省略時\nexec | configure | < コマンドモード\n移行コマンド名>\nコマンドモード移行コマンド名を\n指定します。\nexec: 実行コマンド\nconfigure: 設定コマンド\nコマンドモード移行コマンド名\n省略不可\nall\n指定したコマンドのすべてのオプ\nションに適用する場合に指定しま\nす。\n-\n指定したコ\nマンドのみ\n適用\nコマンドレベル\nコマンドレベルを指定します。\n0 ~15\n省略不可\nコマンド名\nコマンド名を指定します。\n64 文字以内のSTRING 型\n省略不可\nreset\nコマンドレベルを一時的にデフォ\nルトに戻す場合に指定します。\n-\n本設定が有\n\n42\nコマンドリファレンス - 構成定義編\n第1 章 CLI の操作\nコマンドレベル",
      "ref": "cmd_refe_config.pdf p.41"
    },
.
.
(めっちゃ続きます)

AGENTS.md(CLAUDE.md)とINDEX.mdの作成

これは前述したAIエージェントへの指示文になります。

実装まとめ

ここまで作業をするとディレクトリが以下の形になります。

.
├── CLAUDE.md <<-エージェントへの指示
└── manuals
    ├── md <<-mdに変換したドキュメント
    │   ├── cmd_refe_config.md
    │   ├── cmd_refe_ope.md
    │   ├── command_index.json
    │   ├── goriyouF310.md
    │   ├── INDEX.md <<-コマンドの探し方についての指示
    │   ├── kinou.md
    │   ├── message.md
    │   ├── oss_list.md
    │   ├── rackmountRMKB010211wb.md
    │   ├── siyou.md
    │   ├── trouble.md
    │   └── usbband.md
    ├── original <<-元のマニュアルPDF
    │   ├── cmd_refe_config.pdf
    │   ├── cmd_refe_ope.pdf
    │   ├── goriyouF310.pdf
    │   ├── kinou.pdf
    │   ├── message.pdf
    │   ├── oss_list.pdf
    │   ├── rackmountRMKB010211wb.pdf
    │   ├── siyou.pdf
    │   ├── trouble.pdf
    │   └── usbband.pdf
    └── tool <<-変換とコマンド索引作成に使ったスクリプト
        ├── build_command_index.py
        └── convert_to_md.py

5 directories, 25 files

ここまで来れば、あとはAIエージェントから使うだけ!!

実行してみる

漠然とDHCPサーバーの設定方法を聞いてみる

入力プロンプト

DHCPサーバーの設定を行いたいです。

image.png

回答の作成前にINDEX.mdとコマンド索引(JSON)を確認し、この際にヒットしたコマンドリファレンスのページを読み込んでいる動作が確認できます。

回答全文
FITELnet F310 — DHCPv4サーバの設定

基本の流れ(プロファイル方式)

DHCPサーバ機能は「server プロファイル」を作成し、それを配布したいインタフェースに割り当てる方式です。

① server プロファイルを作成し、配布内容を設定

#configure terminal
(config)#ip dhcp server-profile prof01
(config-dhcps prof01)#address 192.168.1.100 192.168.1.110    ← 配布するIPアドレス範囲
(config-dhcps prof01)#gateway 192.168.1.50                    ← デフォルトルータ
(config-dhcps prof01)#dns 192.168.1.10 192.168.1.11           ← DNS(プライマリ/セカンダリ)
(config-dhcps prof01)#domain example.com                      ← DNSドメイン名
(config-dhcps prof01)#lease-time 3600                          ← リース期間(秒、infinityで無制限)

② 配布用インタフェースにプロファイルを割り当てて有効化

(config)#interface port-channel 1
(config-if-ch 1)#ip dhcp server-profile prof01
(config-if-ch 1)#ip dhcp service server

これでこのインタフェース配下のLANだけにIPアドレスを配布します(DHCPリレーを挟んだ別ネットワークへの配布は不可)。

プロファイルで設定できる主なオプション

┌─────────────────────────────────────────────────────┬───────────────────┬───────────────────────────┐
│                      コマンド                       │                    内容                    │           出典            │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ address <開始> <終了>                               │ 配布IPアドレス範囲(最大2000個、10行まで) │ cmd_refe_config.pdf p.445 │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ gateway <アドレス>                                  │ デフォルトルータ                           │ p.447                     │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ dns <プライマリ> [セカンダリ]                       │ DNSサーバ                                  │ p.446                     │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ domain <ドメイン名>                                 │ DNSドメイン名                              │ p.446                     │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ lease-time <秒 | infinity>                          │ リース期間(未設定時は無制限)             │ p.447-448                 │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ ntp-server / time-server / sip-server / wins-server │ 各種サーバアドレス配布                     │ p.449-452                 │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ option                                              │ 任意のDHCPオプション配布                   │ p.449                     │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ logging lease enable                                │ 割当/返却をログ出力                        │ p.448                     │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ broadcast-bit-check enable                          │ broadcastビットチェックの有無              │ p.452-453                 │
├─────────────────────────────────────────────────────┼───────────────────┼───────────────────────────┤
│ set mode send-nak                                   │ 要求IP払出不可時に必ずDHCPNAK応答          │ p.453                     │
└─────────────────────────────────────────────────────┴───────────────────┴───────────────────────────┘

特定端末へのIP固定割当(MACアドレス指定)

(config)#ip dhcp host-database
(config-dhcp-host)#host 1 192.168.1.251 XXXX.XXXX.XXXX     ← 定義番号 配布IP MACアドレス

(cmd_refe_config.pdf p.454)

運用確認コマンド

show ip dhcp server lease        ← リース状況確認 (cmd_refe_ope.pdf p.377)
show ip dhcp statistics          ← 統計情報 (cmd_refe_ope.pdf
dhcp server {enable|disable}     ← サーバの起動/停止制御 (cmd_refe_ope.pdf p.333)
clear ip dhcp server decline     ← DECLINEアドレスを使用可能 .332)

---
具体的な運用条件(配布したいアドレス範囲、対象インタフェース、固定割当の要否など)を教えていただければ、実コンフィグに落とし込みます。

DHCPサーバープロファイルの設定、インターフェースの割り当て、運用確認コマンドがドキュメントに添った内容で出力されました。
素晴らし🙌

少し具体的な質問をしてみる

入力プロンプト

インターフェースGE1/1にタグVLAN 20をtrunkで出したいです。VLAN 20はPort-Channel 20で定義しています。

image2.png

image.png

こちらでもまずINDEX.md, コマンド索引を検索してドキュメントの場所を特定し、いくつかのmd変換したドキュメントを参照してから回答を作成する動作を確認できました。

回答全文
結論

GE1/1にタグVLAN20を「トランク」として出すには、GE1/1にVLAN20用のサブインタフェースを作り、vlan-id・tagging・bridge-group・channel-groupを設定します。

configure terminal
interface gigaethernet 1/1.20
 vlan-id 20
 tagging transparent
 bridge-group 20
 channel-group 20
exit

各行の意味と注意点

- vlan-id 20 — このサブインタフェースが802.1QタグVLAN20を扱うことを指定(cmd_refe_config.pdf p.160)。
- tagging transparent — これを忘れると意図通りに動きません。 F310はデフォルト(未設定時)が terminate=受信フレームのVLANタグを除去して転送する動作です。タグを保持したまま送受信する「トランク」動作にするには明示的に transparent を設定する必要があります(cmd_refe_config.pdf p.162)。
- channel-group 20 — このサブインタフェースをPort-Channel 20(interface port-channel 20で定義済みのL3インタフェース)に関連付けます(cmd_refe_config.pdf p.140)。
- bridge-group 20 — 重要な注意点として、ブリッジグループ番号はVLAN IDやPort-Channel番号と別管理の番号です。同一bridge-groupに属する全インタフェースは同一のvlan-id・同一のport-channelインタフェースを設定する必要があり、これがずれるとエラーとなり該当bridge-group配下の全インタフェースが無効化されます(cmd_refe_config.pdf p.138-139)。上記では慣習的にVLAN ID=Port-Channel番号=bridge-group番号を20で揃えていますが、Port-Channel 20に既に参加している他のインタフェースが使っているbridge-group番号を確認し、それに合わせてください(違う番号を使っている場合はそちらに揃える必要があります)。

用語の整理(混乱しやすい点)

F310のコマンド上「trunk」という語は2つの異なる機能を指すので注意してください。

┌────────────────────┬────────────────────────────────────────────────────┐
│        用語        │                        意味                        │
├────────────────────┼────────────────────────────────────────────────────┤
│ 今回やりたいこと( │ 上記の vlan-id + tagging transparent               │
│ タグVLANトランクポ │ の組み合わせ。IEEE802.1Qのタグ付きVLANを1本の物理  │
│ ート)             │ 線で流す一般的な意味の「トランク」                 │
├────────────────────┼────────────────────────────────────────────────────┤
│ trunk-group /      │ F310独自のリンクアグリゲーション(LACP的な複数物理 │
│ interface          │ ポート束ね)機能。VLANトランクとは無関係(cmd_refe │
│ trunk-channel      │ _config.pdf p.141)。またLANポート内でもGE1/1,2/1  │
│                    │ には適用不可(GE1/2〜1/5のみ対象)                 │
└────────────────────┴────────────────────────────────────────────────────┘

今回の要件はVLANタグの話なので trunk-group/trunk-channel は使いません。

もし既存のPort-Channel 20側の設定(他インタフェースのbridge-group番号)を確認したい場合は、現在のコンフィグ(show running-config 相当)を見せていただければ、bridge-group番号が合っているか確認できます。

✻ Baked for 3m 0s

こちらでもドキュメントに沿った回答を得られました。
今回作った仕組みで狙った動作を出来ていそうです。

まとめ

AIエージェントが参考文献を扱いやすくすれば、参考文献をしっかり参照した上で回答を作成してくれることを確認できました。
RAGを組むほど手間はかけたくないけど、ドキュメントに沿った回答をしてほしい場合、十分有効なアプローチになりそうです。

コマンド索引などを作成する際にPDFの見出しなど構造を理解する必要はありますが、これもAIエージェントで伴走することは十分に可能ですので、工夫すれば適用できる範囲は大きいと思います。

ドキュメントベースでAIに回答をさせたい場合のヒントになれば幸いです。
最後までお読みいただきありがとうございました。

(Next Try) Agent Skillの作成

ここまで仕組みを作れればAgent Skillの開発も比較的手軽に出来るんじゃないかなと思っています。
一旦目的は達成していますが、時期にAgent Skill開発に移行したらまた記事を書こうかなと考えています。

1
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
1
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?