トピック
Radeon AI PRO R9700で作った自社AIを営業・在庫・調査で実用化する実践マニュアル
- 提供:
- 日本AMD
2026年9月16日 06:30
※ここには、実践編の構成を再現するための設定ファイルおよびコマンドがまとめられています。
docker-compose.yml
「MySQL」「mcpo」「Open WebUI」の3コンテナをまとめて起動する構成。実際の社内MySQLに接続する場合は、mysqlサービスの部分を外し、mcpo/config.jsonのMYSQL_HOST(必要に応じてMYSQL_USER/MYSQL_PASS/MYSQL_DB)を実サーバーに向ければ、あとの構成はそのまま使い回せる。
services:
mysql:
image: mysql:8.4
container_name: demo-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpw_demo}
volumes:
- ./db:/docker-entrypoint-initdb.d:ro
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpw_demo}"]
interval: 5s
timeout: 5s
retries: 20
mcpo:
image: ghcr.io/open-webui/mcpo:main
container_name: demo-mcpo
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
command: ["--port", "8000", "--api-key", "${MCPO_API_KEY:-mcpo_demo_key}", "--config", "/config/config.json"]
volumes:
- ./mcpo/config.json:/config/config.json:ro
ports:
- "8000:8000"
open-webui:
image: ghcr.io/open-webui/open-webui:v0.11.0
container_name: demo-open-webui
restart: unless-stopped
depends_on:
- mcpo
environment:
OPENAI_API_BASE_URL: ${LLM_API_BASE_URL}
OPENAI_API_KEY: ${LLM_API_KEY}
WEBUI_AUTH: "true"
volumes:
- open-webui-data:/app/backend/data
ports:
- "3000:8080"
volumes:
mysql-data:
open-webui-data:
MySQL 読み取り専用ユーザー作成SQL(GRANT文)
-- MCPサーバー用の読み取り専用ユーザー(在庫管理データのみ参照可能)
-- デモ用の固定パスワード。実運用ではシークレット管理の仕組みを使うこと。
CREATE USER IF NOT EXISTS 'mcp_reader'@'%' IDENTIFIED BY 'mcp_reader_pw_demo';
GRANT SELECT ON salesdb.products TO 'mcp_reader'@'%';
GRANT SELECT ON salesdb.inventory TO 'mcp_reader'@'%';
FLUSH PRIVILEGES;
ダミーDBのスキーマ定義(CREATE TABLE)
-- デモ用ダミーDB: 在庫管理スキーマ
-- MySQL docker-entrypoint-initdb.d で自動実行される
-- 初期化スクリプトはmysqlクライアントのデフォルト文字コード(latin1)で実行されるため、
-- 日本語リテラルを含むファイルの先頭には必ず SET NAMES utf8mb4; が必要
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS salesdb CHARACTER SET utf8mb4 COLLATE utf8mb4_ja_0900_as_cs;
USE salesdb;
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(20) NOT NULL UNIQUE,
product_name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
warehouse VARCHAR(20) NOT NULL,
stock_qty INT NOT NULL,
reorder_point INT NOT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(product_id)
) ENGINE=InnoDB;
ダミーデータの生成スクリプト
「5カテゴリ×10品目=計50品目、うち8品目が発注点割れ」というデモ用データは、以下のスクリプトで再現できる(外部依存なし、random.seed(42)で固定しているため毎回同じ結果になる)。使い方はpython3 generate_seed.py > 02_seed.sqlで、出力したSQLをdb/ディレクトリに置けばMySQL起動時に自動投入される。
#!/usr/bin/env python3
"""ダミー在庫管理データのSQLを生成するスクリプト(外部依存なし、再現性のため固定シード)。
使い方: python3 generate_seed.py > 02_seed.sql
"""
import random
random.seed(42)
CATEGORIES = {
"OA機器": ["レーザープリンター", "複合機", "シュレッダー", "ラミネーター", "モニターアーム"],
"IT機器": ["ノートPC", "外付けSSD", "USBハブ", "Webカメラ", "無線LANルーター"],
"事務用品": ["コピー用紙", "ファイルボックス", "ホワイトボード", "付箋セット", "ボールペン12本セット"],
"家具": ["オフィスチェア", "スタンディングデスク", "ロッカー", "会議用テーブル", "パーテーション"],
"消耗品": ["トナーカートリッジ", "電池パック", "清掃用クロス", "梱包テープ", "段ボール箱(L)"],
}
def esc(s):
return s.replace("'", "''")
lines = ["SET NAMES utf8mb4;", "USE salesdb;", ""]
# --- products ---
lines.append("INSERT INTO products (sku, product_name, category, unit_price) VALUES")
rows = []
product_id = 1
for category, items in CATEGORIES.items():
for item in items:
for variant in range(1, 3): # 各カテゴリ x 5品目 x 2バリエーション = 10品目/カテゴリ
sku = f"SKU-{product_id:04d}"
name = f"{item} {'Type' + str(variant) if variant > 1 else ''}".strip()
price = round(random.uniform(500, 45000), -1)
rows.append(f"('{sku}', '{esc(name)}', '{category}', {price:.2f})")
product_id += 1
lines.append(",\n".join(rows) + ";")
lines.append("")
n_products = product_id - 1
# --- inventory (一部を意図的に少在庫にして「在庫が少ない商品」デモに使う) ---
lines.append("INSERT INTO inventory (product_id, warehouse, stock_qty, reorder_point) VALUES")
inv_rows = []
low_stock_ids = set(random.sample(range(1, n_products + 1), k=8))
for pid in range(1, n_products + 1):
warehouse = random.choice(["東京DC", "大阪DC", "福岡DC"])
reorder_point = random.randint(10, 30)
if pid in low_stock_ids:
stock_qty = random.randint(0, reorder_point - 1) # 発注点割れ
else:
stock_qty = random.randint(reorder_point, reorder_point + 200)
inv_rows.append(f"({pid}, '{warehouse}', {stock_qty}, {reorder_point})")
lines.append(",\n".join(inv_rows) + ";")
print("\n".join(lines))
mcpoの起動設定・MCPサーバー登録設定
docker-compose.ymlのmcpoサービスが--config /config/config.jsonで読み込む設定ファイル。@benborla29/mcp-server-mysqlへの接続情報と、書き込み系オペレーションの無効化(二重防御の2つ目)をここで指定する。
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "@benborla29/mcp-server-mysql"],
"env": {
"MYSQL_HOST": "mysql",
"MYSQL_PORT": "3306",
"MYSQL_USER": "mcp_reader",
"MYSQL_PASS": "mcp_reader_pw_demo",
"MYSQL_DB": "salesdb",
"ALLOW_INSERT_OPERATION": "false",
"ALLOW_UPDATE_OPERATION": "false",
"ALLOW_DELETE_OPERATION": "false"
}
}
}
}
curlの例(完全版)
本文中で省略した部分を含めた、在庫アラート確認クエリの完全形。
curl -X POST -H "Authorization: Bearer (APIキー)" -H "Content-Type: application/json" \
-d '{"sql":"SELECT p.product_name, i.stock_qty, i.reorder_point FROM inventory i JOIN products p ON i.product_id = p.product_id WHERE i.stock_qty < i.reorder_point"}' \
http://localhost:8000/mysql/mysql_query
# => [[{"product_name":"ラミネーター","stock_qty":14,"reorder_point":23}, ...]]
vLLM起動コマンド(全文)
第1回のコマンドに--enable-auto-tool-choiceと--tool-call-parser gemma4を追加したもの。小冊子版では第1回を参照できないため、ここに全文を再掲する。
docker run -d \
--name vllm-gemma4 \
--network=host \
--group-add=video \
--group-add=render \
--ipc=host \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
--device=/dev/kfd \
--device=/dev/dri \
--shm-size=16g \
-v ./gemma-4-26B-A4B-it-AWQ-4bit:/model \
-e VLLM_ROCM_USE_AITER=0 \
vllm/vllm-openai-rocm:v0.20.1 \
/model \
--served-model-name gemma-4-26B-A4B-it-AWQ-INT4 \
--dtype float16 \
--gpu-memory-utilization 0.90 \
--max-model-len 131072 \
--limit-mm-per-prompt '{"image":1,"audio":0}' \
--enable-auto-tool-choice \
--tool-call-parser gemma4 \
--host 0.0.0.0 \
--port 8888
ソフトウェア・機材バージョン一覧
実機で確認した値(2026年8月時点)。
| ソフトウェア/機材 | バージョン |
|---|---|
| Open WebUI | v0.11.0 |
| mcpo | 0.0.20(イメージタグは:mainのため今後変動し得る) |
| @benborla29/mcp-server-mysql | 2.0.9 |
| MySQL | 8.4.11 |
| Docker(ホスト) | 29.7.2 |
| Node.js / npm(mcpoコンテナ内) | v22.22.0 / 10.9.4 |
| vLLM | v0.20.1(第1回から変更なし) |
| ROCm | 7.2.1(第1回から変更なし) |
| OS | Ubuntu 24.04.4 LTS |
| メモリ | 64GB(第1回の32GBから増設) |
| ストレージ | 1TB SSD追加(第1回からの変更点) |
提案書・Skill生成スクリプト(完全版)
本文で紹介した簡易生成スクリプトの全体。議事録・稟議書・メール文体・提案書、計35件のSkillをまとめて出力する。
#!/usr/bin/env python3
"""ダミーSkillデータ(文書フォーマット・トーン集)を生成するスクリプト。
Open WebUIの「Import Skills」がそのまま読み込める形式(id / name / description / content / meta / is_active)でJSONを出力する。
使い方: python3 generate_skills.py > skills_seed.json
"""
import json
skills = []
# ============================================================
# カテゴリ1: 議事録テンプレート(10種)
# ============================================================
# (表示名, スラッグ, 説明, フォーカス)
meeting_types = [
("定例会議", "regular", "週次・月次などの定例ミーティング", "進捗確認・課題共有が中心"),
("プロジェクトキックオフ", "kickoff", "新規プロジェクト開始時のキックオフミーティング", "目的・体制・スケジュールの合意形成が中心"),
("顧客打ち合わせ", "customer", "社外の顧客・取引先との打ち合わせ", "先方要望の正確な記録と、社内共有のしやすさを重視"),
("品質会議", "quality", "品質管理・不具合対応に関する会議", "発生事象・原因・対策・再発防止の4点セットを重視"),
("経営会議", "exec", "経営層が参加する意思決定会議", "決定事項と保留事項を明確に分けて記録"),
("採用面接", "interview", "候補者との面接記録", "評価観点ごとの所見を記録し、選考の公正性を担保"),
("障害対応会議", "incident", "システム障害発生時の緊急対応会議", "時系列での事実整理と、暫定対応・恒久対応の切り分けが中心"),
("予算検討会議", "budget", "部門予算・投資案件の検討会議", "金額・根拠・承認ステータスを記録"),
("ベンダー打ち合わせ", "vendor", "外部ベンダー・委託先との定例打ち合わせ", "契約範囲内外の区別を意識して記録"),
("1on1面談", "one-on-one", "上長と部下の1対1面談記録", "本人の発言を中心に記録し、評価目的では使わない"),
]
MEETING_TEMPLATE = """# {title}議事録フォーマット
以下のフォーマットに沿って議事録を作成してください。{focus}。
## 記載項目
- 日時・場所(またはオンライン/オフラインの別)
- 出席者・欠席者
- 議題
- 決定事項(誰が・何を・いつまでに)
- 未決事項・持ち越し課題
- 次回予定
## トーン
簡潔な体言止めを基本とし、憶測や個人の感想は「所感」欄に分けて記載する。
{note}
"""
for name, slug, desc, focus in meeting_types:
skills.append({
"id": f"minutes-{slug}",
"name": f"議事録テンプレート({name})",
"description": f"{desc}向けの議事録フォーマット。{focus}。",
"content": MEETING_TEMPLATE.format(
title=name, focus=focus,
note="関係者以外が読んでも経緯が分かるよう、専門用語には簡単な補足を添える。"
),
"meta": {"tags": ["議事録", name]},
"is_active": True,
})
# ============================================================
# カテゴリ2: 稟議書テンプレート(10種)
# ============================================================
ringi_types = [
("備品購入", "supplies", "10万円未満のPC周辺機器・什器等の購入", "見積書の添付を必須とする"),
("出張申請", "travel", "国内・海外出張の事前申請", "訪問先・目的・概算費用を明記する"),
("システム導入", "system", "業務システム・SaaSの新規導入", "情報システム部門の事前レビューを経ていることを明記する"),
("外部委託契約", "outsourcing", "業務の外部委託・業務委託契約の締結", "委託範囲と成果物の定義を明確にする"),
("採用稟議", "hiring", "正社員・契約社員の採用決定", "採用背景と配属予定部署を明記する"),
("交際費", "entertainment", "接待・贈答等の交際費支出", "相手先と金額の妥当性を説明する"),
("研修費用", "training", "社員研修・資格取得支援の費用申請", "研修の目的と期待効果を記載する"),
("設備投資", "capex", "10万円以上の設備・機器投資", "投資回収見込み(ROI)を記載する"),
("契約更新", "renewal", "既存契約の更新・条件変更", "変更点を旧条件との対比で明記する"),
("広告出稿", "advertising", "広告・販促費用の支出申請", "想定効果(リーチ・コンバージョン等)の見込みを記載する"),
]
RINGI_TEMPLATE = """# 稟議書フォーマット({title})
{desc}に関する稟議書は、以下の項目で作成してください。
## 記載項目
- 件名
- 起案者・起案日
- 稟議内容の概要(3行程度で要約)
- 金額・費用(税込/税抜を明記)
- 決裁ルート(担当 → 部門長 → 経理 → 役員 等、金額に応じたルート)
- 稟議理由(背景・目的)
- 想定リスクと対応
## 留意点
{note}
## トーン
主観的な表現を避け、事実と根拠に基づいて簡潔に記載する。決裁者が数十秒で判断できる分量を心がける。
"""
for name, slug, desc, note in ringi_types:
skills.append({
"id": f"ringi-{slug}",
"name": f"稟議書テンプレート({name})",
"description": f"{desc}の稟議書フォーマット。",
"content": RINGI_TEMPLATE.format(title=name, desc=desc, note=note),
"meta": {"tags": ["稟議書", name]},
"is_active": True,
})
# ============================================================
# カテゴリ3: メール文体ガイド(10種)
# ============================================================
email_types = [
("社外・初回問い合わせ返信", "inquiry", "初めて連絡をいただいた社外の方への返信", "丁寧語を基本とし、自己紹介を簡潔に添える"),
("社外・謝罪", "apology", "ミスやトラブルに対する謝罪連絡", "言い訳を先に書かず、まず謝罪と事実確認の意思を明確に伝える"),
("社外・見積もり送付", "quotation", "見積書を添付して送付する際の本文", "有効期限・前提条件を本文中に明記する"),
("社内・進捗報告", "progress", "上長やチームへの進捗共有", "結論(進んでいる/遅れている)を最初の1文で示す"),
("社内・依頼", "request", "他部署・他メンバーへの作業依頼", "期限と背景(なぜ必要か)をセットで伝える"),
("督促", "reminder", "回答や支払いが遅れている相手への催促", "強い表現を避けつつ、期限を明確に再提示する"),
("お礼", "thanks", "打ち合わせや対応へのお礼連絡", "具体的にどの点に感謝しているかを一言添える"),
("アポイント調整", "appointment", "面談・打ち合わせの日程調整", "候補日を複数提示し、先方の負担を減らす"),
("クレーム対応", "complaint", "顧客からのクレームへの一次返信", "まず状況把握への感謝と共感を示し、次のアクションを明記する"),
("契約後フォロー", "followup", "契約締結後の初回フォローアップ", "今後の流れ・担当窓口を明確に伝える"),
]
EMAIL_TEMPLATE = """# メール文体ガイド({title})
{desc}の際は、以下のトーン・構成を参考にしてください。
## 構成
1. 宛名・挨拶
2. 用件(結論を先に)
3. 詳細説明
4. 依頼事項・次のアクション(あれば)
5. 結びの挨拶・署名
## トーン
{note}。過度にへりくだらず、対等な立場での丁寧語を基本とする。
## 避けるべき表現
- 曖昧な期限表現(「なるべく早めに」等)→ 具体的な日付を明記
- 二重否定・持って回った言い回し
"""
for name, slug, desc, note in email_types:
skills.append({
"id": f"email-{slug}",
"name": f"メール文体ガイド({name})",
"description": f"{desc}の際に使う文体・構成のガイド。",
"content": EMAIL_TEMPLATE.format(title=name, desc=desc, note=note),
"meta": {"tags": ["メール文体", name]},
"is_active": True,
})
# ============================================================
# カテゴリ4: 提案書テンプレート(5種)
# ============================================================
proposal_types = [
("新規提案", "new", "新規顧客への提案書", "課題の整理を提案の前段に置き、自社ソリューションとの対応関係を明確にする"),
("価格・見積提示", "pricing", "見積書を伴う価格提示の提案書", "価格の妥当性を示す根拠(工数・原価等)を簡潔に添える"),
("競合比較訴求", "comparison", "他社製品・サービスとの比較を含む提案書", "他社の悪口ではなく、自社の強みが顧客の課題にどう効くかで語る"),
("導入事例引用", "case-study", "類似業種の導入事例を引用する提案書", "守秘義務のある事例は社名を伏せ、業種・規模感のみ記載する"),
("フォローアップ提案", "followup", "初回提案後の追加提案・再提案", "前回提案からの変更点・追加提案理由を冒頭で明確にする"),
]
PROPOSAL_TEMPLATE = """# 提案書テンプレート({title})
{desc}は、以下の構成で作成してください。
## 記載項目
- 表紙(顧客名・提案日・提出者)
- 課題整理(顧客が抱える課題の要約)
- 提案内容(課題への対応策)
- 価格・スケジュール
- 導入後の効果・期待値
## 留意点
{note}
## トーン
売り込み口調を避け、顧客の課題解決を軸に据えた説明を基本とする。
"""
for name, slug, desc, note in proposal_types:
skills.append({
"id": f"proposal-{slug}",
"name": f"提案書テンプレート({name})",
"description": f"{desc}のフォーマット。",
"content": PROPOSAL_TEMPLATE.format(title=name, desc=desc, note=note),
"meta": {"tags": ["提案書", name]},
"is_active": True,
})
print(json.dumps(skills, ensure_ascii=False, indent=2))
TavilyのAPIキー取得手順
- https://tavily.comにアクセスし、メールアドレスまたはGoogle/GitHubアカウントでサインアップする(クレジットカード登録は不要)
- ダッシュボードにログインすると、APIキー(tvly-で始まる文字列)が発行済みの状態で表示される
- Open WebUIの管理者パネル > 設定 > Web検索で、検索エンジンに「Tavily」を選び、取得したAPIキーを貼り付けて保存する
- 無料枠は月1,000クレジット。1回のWeb検索リクエストで消費するクレジット数は検索深度(basic/advanced)によって変わる
実測値(各PARTの応答時間)
大がかりなベンチマークではなく、各PART1回ずつの応答時間。実機(Radeon AI PRO R9700)で計測。
| PART | 質問 | 応答時間 | 備考 |
|---|---|---|---|
| 営業編(Skill) | 「出張申請の稟議書を書きたい。来月大阪の展示会に2日間出張します」 | 約12.5秒 | view_skillでテンプレートを読み込んでから生成 |
| 在庫管理編(MCP) | 「在庫が発注点を下回っている商品を教えて」 | 約7秒 | 初回問い合わせのためテーブル構造を確認する問い合わせ(SHOW TABLES/DESCRIBE)を挟んでから本クエリを実行。会話が続いている状態(スキーマを把握済み)ならもっと短くなる |
| リサーチ編(Web検索) | 「2026年、30B未満のローカルLLMの動向についてWebで検索してまとめて」 | 約20秒 | Tavily APIへの検索リクエスト待ちを含む |
MCPで往復が増える体感について。テーブル定義を自分で確認しに行く(SHOW TABLES/DESCRIBEを挟む)分だけ余計な往復が発生しているにもかかわらず、それでも単純なSkillの回答より速いという逆転現象が起きている。Skill側が12.5秒かかっているのは、view_skillでテンプレートを読み込んだうえで、稟議書という長文の下書きを生成しているため。実際の運用では、モデルに事前にテーブル定義を一言伝えておく、またはSkillとして「salesdbのテーブル構成」を登録しておくといった工夫で、MCPの往復をさらに減らせる。














![【Amazon.co.jp限定】 伊藤園 磨かれて、澄みきった日本の水 2L 8本 ラベルレス [ ケース ] [ 水 ] [ ペットボトル ] [ 箱買い ] [ ストック ] [ 水分補給 ] 製品画像:5位](https://m.media-amazon.com/images/I/41m8ly-SllL._SL160_.jpg)




