レポート課題56・7日目・180分×2T3情報工学5_[学生番号]_[氏名].pdf
6・7日目 ローカルLLMとAI連携
自分のサーバーで小さな生成AI(ローカルLLM)を動かし、プログラムから呼び出せる窓口(API)を試します。さらに、4日目のNginxを受付係にしてブラウザ用のチャット画面を公開し、最後は自分でAIチャットのWebサイトを組み立てます。
この日のゴール
- Ollamaが動き、小さなモデル
qwen2.5:0.5bと会話できる - WindowsのPowerShellから、OllamaのAPIに質問を送って答えを受け取れる
- Open WebUIのコンテナが動き、Nginxを通して
http://192.168.100.xxx/からチャット画面が使える - 自分で作ったAIチャットのWebサイトが、ブラウザで会話できる
この日に出てくる主な言葉:
ローカルLLMとは
LLM(大規模言語モデル)は、大量の文章から「ある言葉の次に来やすい言葉」を学習し、それを繰り返し予測することで文章を作るAIです。学習の結果できあがったデータをモデルと呼び、その規模はパラメータ数で表します。この日に使う qwen2.5:0.5b は、Qwen2.5 というモデルの系列のうち、0.5b=約5億パラメータの小さな版です。
ChatGPTやGeminiのようなクラウド型AIは、企業の巨大なサーバーで動くAIをインターネット越しに使います。それに対してローカルLLMは、自分が管理するコンピュータの上でモデルを動かします。この実験の仮想マシンはCPU 2コア・メモリ1 GBで、AIの計算が得意なGPUもないので、動かせるのはごく小さなモデルだけです。答えが遅かったり、内容が怪しかったり(ハルシネーション)するのは、そのためでもあります。課題1では、この違いを利点と欠点の両面から考えます。考察のヒント答えが遅い・内容が怪しいのは失敗ではなく、小さなモデルをGPUなしで動かしたローカルLLMの制約を示す「結果」です。実際の答えの例や、かかった時間を根拠に、クラウド型との違いを論じられます。
APIとJSON
APIは、プログラム同士が機能を呼び出し合うための窓口です。レストランにたとえると、客(プログラム)は厨房(AI本体)に入らず、決まった書式の注文票を窓口に出して、料理を受け取ります。OllamaはHTTP APIを持っていて、決まったURL(エンドポイント)に、JSONという書式で注文を送ると、JSONで答えが返ってきます。
注文(リクエスト)と答え(レスポンス)
{
"model": "qwen2.5:0.5b",
"prompt": "高専について教えて",
"stream": false
}"model"- どのモデルに答えさせるか
"prompt"- 質問の文章(AIへのプロンプト)
"stream": false- 答えを少しずつではなく、できあがってから1回で返してほしい(ストリーミング(stream))
{
"model": "qwen2.5:0.5b",
"created_at": "2026-09-29T01:23:45.678Z",
"response": "高専(高等専門学校)は、……",
"done": true,
"total_duration": 8123456789,
"eval_count": 152,
"eval_duration": 6543210000
}答えの本文は "response" に入っています。total_duration(全体にかかった時間)や eval_duration(文章を作るのにかかった時間)の単位はナノ秒(10億分の1秒)、eval_count は作った言葉の断片(トークン)の数です。eval_count ÷ eval_duration × 109 で、1秒あたりに作れたトークン数が計算でき、性能を数値で比べる考察に使えます。考察のヒント数値で比べるときは、同じ質問を何回か送って平均をとる、質問の長さを変えて比べる、などの条件をそろえ、結果は有効数字を考えて丸めます。条件と回数を「実験」の章に書いておけば、再現性のある結果になります。
この日に組み立てる構成
リバースプロキシは、利用者からの要求をまとめて受け付け、裏にいる担当者へ取り次ぐ受付係です。4日目のNginx(nginx-proxy)がその役を担い、利用者は「http://192.168.100.xxx/」という1つの入口だけを知っていれば済みます。
host.docker.internal という名前でUbuntu自身に問い合わせる。自作AIチャットの置き方(/chat/)は、このガイドの一例。手順の解説
スクリーンショット(エビデンス)を撮るところ。マウスを乗せると撮るタイミングが表示され、押すとページ下のエビデンス一覧の該当項目へ移動します。考察を書くときに知っておきたいこと。マウスを乗せる(スマホでは押す)とヒントが表示されます。
Ollamaを入れて、モデルと話す
curl -fsSL https://ollama.com/install.sh | sh
ollama run qwen2.5:0.5bcurl -fsSL URLインストール用のスクリプトをダウンロードして|その中身をshそのままシェルで実行する1行目は「インターネットから取ってきたスクリプトを、中身を見ずにその場で実行する」書き方です。手軽ですが、配布元を信用することが前提になります考察のヒント「取ってきたプログラムを中身を見ずに実行する」便利さと、その配布元を信用しなければならないことは引き換えです。ソフトウェアの入手方法の安全性として、考察で触れられます。(中身を確かめたいときは、いったんファイルに保存して読んでから実行する方法もあります)。インストールの途中で、Ollamaがサービスとして登録され、自動で起動します。
WARNING: No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode. と出るのは正常です。GPUがないのでCPUだけで動く、という知らせです。
2行目の ollama run は、初回はモデルをダウンロード(約400 MB)してから、会話の画面を開きます。
>>> Send a message (/? for help)>>> の後ろに質問を打って Enter を押すと、答えが少しずつ表示されます。会話を終えるには /bye と打つか、Ctrl+D を押します。
ほかのモデルを試すとき
指導書は Llama-3.2-1B-Instruct も勧めています。Ollamaでは llama3.2:1b という名前で配布されていますが、パラメータ数が約2倍になり、ダウンロードも1 GBを超えます。メモリ1 GBの仮想マシンでは動かない、または非常に遅いことがあります。free -h で空きメモリを確かめてから試しましょう。入っているモデルは ollama list で確認できます。
WindowsからAPIを呼ぶ
指導書では「WindowsのPowerShellから、curlを使ってUbuntu上のOllama APIにプロンプトを投げる」とあり、実際のコマンドは「結果と考察」に載っています(エビデンス1)。ただし、そのままではつながらない理由が2つあるので、順に確かめます。
(1) まずサーバーの中から試す
curl http://localhost:11434
curl http://localhost:11434/api/generate -d '{
"model": "qwen2.5:0.5b",
"prompt": "高専について教えて",
"stream": false
}'1行目で Ollama is running と返れば、Ollamaは動いています。2行目で答えのJSONが返れば、APIも使えています。Ubuntuのcurlは、日本語を含むJSONも正しく送れます。-d は「このデータを送る」という指定です。
(2) Windowsから届かない理由
初期設定のOllamaは、外からの接続を受け付けない
Ollamaは初期設定では 127.0.0.1(自分自身)でしか待ち受けません。さらにOllamaはDockerではなくUbuntuに直接入っているので、ufwの設定もそのまま効き、11434番は閉じています。指導書にはこの設定の手順がありませんが、Windowsから呼ぶには、次の2つの変更が必要になるのが普通です。この変更をしてよいかは、授業の指示を確認してください。
今どこで待ち受けているかは、次のコマンドで分かります。
ss -tln | grep 11434127.0.0.1:11434 と出れば「自分自身からしか受け付けない」状態です。外から受け付けるようにするには、Ollamaのサービスの設定に環境変数 OLLAMA_HOST を追加します。
sudo systemctl edit ollamaエディタが開いたら、### Anything between here and the comment below will become the contents of the drop-in file という行と、その下の ### Edits below this comment will be discarded という行の間に、次の2行を書いて保存します。
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"sudo systemctl daemon-reload
sudo systemctl restart ollama
ss -tln | grep 11434
sudo ufw allow 11434/tcp0.0.0.0 は「どの宛先でも受け付ける」という意味です。ss の表示が *:11434 や 0.0.0.0:11434 に変われば成功です。最後にufwで11434番を開けます。この変更は、手順6以降でOpen WebUIのコンテナがOllamaに問い合わせるためにも必要になります(コンテナから見ても、Ollamaは「外」にいるからです)。
APIには鍵がかかっていない
OllamaのAPIには、パスワードなどの本人確認の仕組みがありません。11434番を開けると、同じネットワークにいる人は誰でもあなたのAIを使えるようになります。実験用のネットワークでは問題になりにくいですが、本物の運用では、許可する相手をしぼる(sudo ufw allow from 相手のIP to any port 11434)などの対策が必要です。考察のヒントAPIとして外に開くと、誰でも・どこからでも・どんなプログラムからでも使える便利さと引き換えに、使える人をしぼる対策(本人確認、通信の許可の範囲)が必要になります。課題2のメリットを書くとき、この裏返しの課題にも触れるとよい考察になります。
(3) Windowsから呼ぶ
指導書の「PowerShell(参考)」の例は、次の理由でそのままでは動きません(このガイドの作成時にWindows 11で確認しました)。
- Windows 11 標準の PowerShell 5.1 では、
curlが別のコマンド(Invoke-WebRequest)の別名になっていて、-dを解釈できずにエラーになる。curl.exeと書いて本物のcurlを呼んでも、JSONの中の"が消えてしまう - PowerShell 7 では curl が動くが、日本語の質問が文字化けした状態で送られる
どちらの版でも確実に動くのは、PowerShellに備わっている Invoke-RestMethod を使う書き方です。
$body = @{ model = "qwen2.5:0.5b"; prompt = "高専について教えて"; stream = $false } | ConvertTo-Json
$res = Invoke-RestMethod -Uri "http://192.168.100.xxx:11434/api/generate" -Method Post -ContentType "application/json; charset=utf-8" -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$res.response| 部分 | 意味 |
|---|---|
@{ … } | ConvertTo-Json | 項目の組を作り、JSONの文字列に変換する。$false はPowerShellでの「false」 |
$body = … | できたJSONを $body という入れ物(変数)に入れておく |
-Uri "http://…:11434/api/generate" | 送り先(エンドポイント) |
-Method Post | データを送る方式(POST)で |
-ContentType "application/json; charset=utf-8" | 送るのはUTF-8で書かれたJSONだ、と相手に伝える |
[System.Text.Encoding]::UTF8.GetBytes($body) | 日本語が化けないよう、UTF-8のバイト列に変換してから送る |
$res.response | 返ってきたJSONのうち、答えの本文だけを表示する |
返ってきたJSON全体を見たいときは $res | ConvertTo-Json とします。CPUだけで動いているので、答えが返るまで数秒〜数十秒かかることがあります。初回はモデルの読み込みがあるので、特に時間がかかります。エビデンス1の撮影ポイント撮影ポイント エビデンス1手順2の(3)で Invoke-RestMethod を実行し、$res.response で答えの文章が表示された時点。3行のコマンドと答えが1枚に入るように撮る(指導書の「結果と考察」の最初の確認にあたる)。エビデンス一覧の項目へ移動 ↓
Dockerネットワークと作業用ディレクトリを用意する
sudo docker network create app-net
sudo mkdir -p /opt/open-webui
sudo mkdir -p /opt/nginx/conf.dapp-net は、Nginxのコンテナと、これから作るOpen WebUIのコンテナを同じ輪に入れるためのDockerネットワークです。同じネットワークに参加したコンテナは、open-webui のようにコンテナの名前で相手を呼べるようになります。別々のcompose.yamlで作るコンテナどうしをつなぐので、Composeの外で先に作っておきます。
4日目のNginxの定義を更新する
sudo nano /opt/nginx/compose.yamlservices:
nginx:
image: nginx:stable
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- /opt/nginx/html:/usr/share/nginx/html:ro
- /opt/nginx/conf.d:/etc/nginx/conf.d:ro
- /opt/nginx/log:/var/log/nginx
networks:
- app-net
restart: unless-stopped
goaccess:
image: allinurl/goaccess
container_name: goaccess-report
profiles: ["tools"]
volumes:
- /opt/nginx/log:/srv/log:ro
- /opt/nginx/html:/srv/report
command:
- --log-format=COMBINED
- -f
- /srv/log/access.log
- -o
- /srv/report/goaccess-report.html
networks:
app-net:
external: true| 追加した行 | 意味 |
|---|---|
- /opt/nginx/conf.d:/etc/nginx/conf.d:ro | Nginxの追加設定の置き場を、Ubuntu側のディレクトリにする。手順7でここに取り次ぎの設定を書く |
networks: - app-net(nginxの下) | nginxのコンテナを app-net に参加させる |
networks: app-net: external: true(いちばん下) | app-net はこのファイルの外で作ったもの(手順3)を使う、という宣言 |
cd /opt/nginx
sudo docker compose config
sudo docker compose up -d
sudo docker compose psps で nginx-proxy が Up になっていることを確かめます。エビデンス2の撮影ポイント撮影ポイント エビデンス2手順4で定義を更新し、/opt/nginx で sudo docker compose up -d のあとに sudo docker compose ps を実行した直後。nginx-proxy の STATUS が Up であることが分かるように撮る(最後の確認コマンドで撮り直してもよい)。エビデンス一覧の項目へ移動 ↓
更新した直後は、Webページが見えなくなる
Nginxのイメージには、4日目のページを配信するための設定ファイル(/etc/nginx/conf.d/default.conf)が最初から入っていました。そこに空の /opt/nginx/conf.d を重ねたので、その設定が見えなくなり、手順7で設定を書くまでは80番に何も返ってこなくなります。また手順7の設定では / をOpen WebUIへ取り次ぐので、4日目のページはそのままでは表示されなくなります。4日目のページも見えるようにしたいときは、手順7の設定に配信用の設定を足します。
Open WebUIの定義を書く
sudo nano /opt/open-webui/compose.yamlservices:
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
environment:
- OLLAMA_BASE_URL=http://host.docker.internal:11434
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- open-webui:/app/backend/data
networks:
- app-net
restart: unless-stopped
volumes:
open-webui:
networks:
app-net:
external: true| 行 | 意味 |
|---|---|
image: ghcr.io/open-webui/open-webui:main | GitHubの倉庫(ghcr.io)にあるOpen WebUIのイメージ。main は開発中の最新版を表すタグ |
environment: - OLLAMA_BASE_URL=… | Open WebUIに「Ollamaはここにいる」と教える環境変数 |
extra_hosts: - "host.docker.internal:host-gateway" | コンテナの中で host.docker.internal と書いたら、Ubuntu(ホスト)自身を指すようにする。Ollamaはコンテナではなくホストで動いているため |
volumes: - open-webui:/app/backend/data | アカウントや会話の履歴を、open-webui という名前のボリュームに保存する。コンテナを作り直しても消えない |
networks: - app-net | app-net に参加し、Nginxから open-webui という名前で呼べるようにする |
(ports: がない) | 外には窓口を開けない。外からはNginxを通してしか入れない |
最後の点は大事な設計です。Open WebUIは ports: を持たないので、Windowsから直接は届きません。入口を受付係のNginx 1か所にまとめることで、管理がしやすくなり、余計な窓口も開きません。考察のヒント入口を1か所にまとめる設計は、管理のしやすさ(設定・ログ・証明書を1か所で扱える)と安全(外に開く窓口が少ない)の両面から評価できます。構成図と合わせて説明すると、「連携の考察」になります。
Open WebUIを起動する
cd /opt/open-webui
sudo docker compose config
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 20Open WebUIのイメージは数GBあるので、初回のダウンロードには時間がかかります。起動したあとも、初回は準備に数分かかることがあります。logs に Uvicorn running on http://0.0.0.0:8080 のような行が出れば、受付の準備ができています(まだ出ていなければ、しばらく待ってから logs を見直します)。エビデンス3の撮影ポイント撮影ポイント エビデンス3手順6で、/opt/open-webui で sudo docker compose ps を実行した直後。open-webui の STATUS が Up になっていることが分かるように撮る(起動直後は health: starting と表示されることがあり、準備ができると healthy に変わる)。エビデンス一覧の項目へ移動 ↓
メモリ不足に注意
仮想マシンのメモリは1024 MBです。Ollamaのモデル、Open WebUI、Nginx、Dockerが同時に動くと、足りなくなる可能性があります。コンテナが再起動を繰り返す、応答が極端に遅い、といったときは、free -h(メモリの残り)、sudo docker stats(コンテナごとの使用量、Ctrl+C で終了)、df -h(ディスクの残り)を確かめ、教員に相談してください。
Nginxに取り次ぎの設定を書く
sudo nano /opt/nginx/conf.d/open-webui.conf
cd /opt/nginx
sudo docker compose exec nginx nginx -t
sudo docker compose exec nginx nginx -s reload指導書には「location / の中で proxy_pass http://open-webui:8080; を記述し」とだけ書かれています。ファイル全体の例は次のとおりです。
server {
listen 80;
server_name _;
location / {
proxy_pass http://open-webui:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
}| 行 | 意味 |
|---|---|
server { … } | 1つのWebサイトとしての設定のまとまり |
listen 80; | 80番で受け付ける |
server_name _; | どの名前でアクセスされても、この設定で受ける |
location / { … } | パスが / で始まるすべての要求についての設定 |
proxy_pass http://open-webui:8080; | 要求を open-webui コンテナの8080番へ取り次ぐ。app-net の中なので名前で呼べる |
proxy_set_header … | 取り次ぐときに、元の相手のアドレスや、アクセスに使われた名前などを添えて伝える |
Upgrade と Connection の2行 | つなぎっぱなしの通信(WebSocket)も取り次げるようにする。Open WebUIが使う |
proxy_read_timeout 300s; | 取り次ぎ先の返事を最大300秒待つ。CPUだけで動くAIは返事が遅く、既定の60秒では打ち切られることがあるため考察のヒントいくつもの部品を組み合わせたシステムでは、いちばん遅い部品に全体が合わせる必要があります。待ち時間の設定を変えた理由は、連携の考察の具体例になります。 |
docker compose exec nginx …- 動いている nginx のコンテナの中で、続くコマンドを実行する
nginx -t- 設定の書式を検査する。
syntax is okとtest is successfulが出れば問題なし nginx -s reload- 止めずに、新しい設定を読み込み直す
ブラウザで http://192.168.100.xxx/ を開くと、Open WebUIの画面が表示されます。エビデンス4の撮影ポイント撮影ポイント エビデンス4手順7で nginx -s reload のあと、ブラウザで http://192.168.100.xxx/ を開き、Open WebUIの画面(初回はアカウント作成の画面、ログイン後はチャットの画面)が表示された時点。URL欄も写るように撮る。エビデンス一覧の項目へ移動 ↓
Open WebUIを初めて開いたとき
最初に管理者のアカウントを作る画面が出ます。ここで作るのは、このOpen WebUIの中だけで使うアカウントです。学校や、ほかのサービスで使っているパスワードは使い回さないでください。ログインしたら、画面上部のモデルの選択で qwen2.5:0.5b を選んで話しかけます。モデルが一覧に出ない場合は、Open WebUIからOllamaに届いていないので、手順2の OLLAMA_HOST とufwの設定を見直します(トラブル対処集)。
nginx -t で host not found in upstream と出たら
Nginxは設定を読むときに、open-webui という名前の相手を探します。Open WebUIのコンテナが動いていない、または app-net に参加していないと、この名前が見つからずエラーになります。手順6の ps で Up になっているか、両方のcompose.yamlに app-net が書かれているかを確かめます。
AIチャットのWebサイトを作る
最後に、自分でAIチャットのWebサイトを組み立てます。指導書の指示は「index.html に入力フォームを作成し(Webフロントエンド)、PHPやPython(Flask)等を用い、Webフォームの入力をOllama APIに転送し、結果を表示する」で、作り方は自由です。
3つの部品の役割
ブラウザから直接OllamaのAPIを呼ばずに、間にバックエンドをはさむのには理由があります。ブラウザには、ほかの場所のAPIを勝手に呼べないようにする仕組み(CORS)があるうえ、Ollamaの住所を外に見せることにもなります。バックエンドがあれば、入力が空でないか確かめたり、使うモデルを固定したりもできます。課題3は、この3つの部品の役割とデータの受け渡しを説明するものです。考察のヒントバックエンドをはさむ理由(ブラウザの制限、住所を隠す、入力の確認、モデルの固定)は、課題3で3つの部品の役割分担を説明するときの根拠になります。各区間で実際に送ったJSONの中身を示すと具体的になります。
作る前に決めること
- バックエンドの言語:指導書は PHP や Python(Flask)を例に挙げている
- どこで動かすか:Ubuntuに直接か、コンテナか。コンテナにして app-net に参加させると、Nginxから名前で取り次げ、ufwの影響も受けない
- どのURLで公開するか:
/はOpen WebUIが使っているので、/chat/のように別の場所に割り当てる - Ollamaをどう呼ぶか:コンテナからなら、Open WebUIと同じく
host.docker.internal:11434
完成したら、ブラウザで質問を送り、答えが表示された画面を撮ります。エビデンス5の撮影ポイント撮影ポイント エビデンス5手順8で自作のAIチャットに質問を送り、答えが表示された時点。質問と答えの両方と、URL欄が写るように撮る。答えが出るまで数十秒かかることがある。エビデンス一覧の項目へ移動 ↓
たたき台の一例を見る(Flask+コンテナ、/chat/ で公開)
ここに載せるのは、仕組みを理解するための最小限の例です。このガイドの作成時に、バックエンドの動作(質問の受け渡し、空の質問の拒否、Ollamaに届かないときのエラー)を、Ollamaの代わりの試験用サーバーで確かめています。そのまま提出物にせず、各行の意味を理解し、自分で改良して使ってください。レポートに使った場合は、付記に記載します。
(1) バックエンド:質問を受け取り、Ollamaに問い合わせて答えを返す。
sudo mkdir -p /opt/chat-app
sudo nano /opt/chat-app/app.pyimport json
import os
import urllib.request
from flask import Flask, jsonify, request, send_from_directory
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://host.docker.internal:11434/api/generate")
MODEL = os.environ.get("MODEL", "qwen2.5:0.5b")
app = Flask(__name__)
@app.get("/")
def index():
return send_from_directory(".", "index.html")
@app.post("/api/chat")
def chat():
data = request.get_json(silent=True) or {}
prompt = str(data.get("prompt", "")).strip()
if not prompt:
return jsonify(error="質問が空です"), 400
body = json.dumps({"model": MODEL, "prompt": prompt, "stream": False}).encode("utf-8")
req = urllib.request.Request(OLLAMA_URL, data=body,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=300) as res:
answer = json.load(res).get("response", "")
except Exception as e:
return jsonify(error=f"Ollamaに接続できません: {e}"), 502
return jsonify(answer=answer)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)@app.get("/")- ページそのもの(index.html)を返す
@app.post("/api/chat")- フロントエンドから質問を受け取る窓口。JSONの
promptを取り出す json.dumps({...})- Ollamaへの注文票(JSON)を作る
urllib.request.urlopen(...)- OllamaのAPIに送り、返ってきたJSONの
responseを取り出す return jsonify(answer=answer)- 答えを
{"answer": "…"}というJSONにしてブラウザへ返す
(2) フロントエンド:入力フォームと、送信・表示の処理。
sudo nano /opt/chat-app/index.html<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>T3 AI Chat</title>
</head>
<body>
<h1>T3 AI Chat</h1>
<form id="form">
<textarea id="prompt" rows="3" cols="50" placeholder="質問を入力"></textarea><br>
<button type="submit">送信</button>
</form>
<pre id="answer"></pre>
<script>
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault();
const out = document.getElementById("answer");
out.textContent = "考え中…";
const res = await fetch("api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: document.getElementById("prompt").value })
});
const data = await res.json();
out.textContent = data.answer ?? data.error;
});
</script>
</body>
</html>fetch("api/chat", …) の送り先は、先頭に / のない相対パスです。ページを http://192.168.100.xxx/chat/ で開いていれば、/chat/api/chat へ送られます。
会話の吹き出しや、かかった時間の表示などを備えた画面にしたいときは、サイトを制作のひな形「AIチャット」で index.html を作り、この (2) の代わりに使えます。送る・受け取るJSONの形は、このたたき台と同じです。
(3) コンテナの定義:Pythonの公式イメージを使い、起動するたびにFlaskを入れてから動かす(インターネットに接続できることが前提)。
sudo nano /opt/chat-app/compose.yamlservices:
chat-backend:
image: python:3.12-slim
container_name: chat-backend
working_dir: /app
volumes:
- /opt/chat-app:/app:ro
command: sh -c "pip install --no-cache-dir flask && python app.py"
environment:
- OLLAMA_URL=http://host.docker.internal:11434/api/generate
- MODEL=qwen2.5:0.5b
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- app-net
restart: unless-stopped
networks:
app-net:
external: truecd /opt/chat-app
sudo docker compose up -d
sudo docker compose logs --tail 20ログに Running on http://…:5000 が出れば起動しています(初回はFlaskのダウンロードで少し時間がかかります)。
(4) Nginxに取り次ぎを足す:/opt/nginx/conf.d/open-webui.conf の server { … } の中、location / { … } の前か後ろに次を追加し、手順7と同じく nginx -t と nginx -s reload を実行します。
location = /chat {
return 301 /chat/;
}
location /chat/ {
proxy_pass http://chat-backend:5000/;
proxy_set_header Host $host;
proxy_read_timeout 300s;
}proxy_pass の最後の / によって、/chat/api/chat への要求は、バックエンドには /api/chat として渡されます。1つ目の location = /chat は、最後の / を付け忘れて開いたときに、/chat/ へ案内し直す設定です。ブラウザで http://192.168.100.xxx/chat/ を開き、質問して答えが表示されれば完成です(エビデンス5)。
改良の例:会話の履歴を表示する、答えを少しずつ表示する(stream)、モデルを選べるようにする、答えが返るまでの時間を表示する、など。
結果と考察のための確認コマンド
Windowsから、APIの応答を確かめます(エビデンス1)。上の手順2の(3)の書き方を使います。
$body = @{ model = "qwen2.5:0.5b"; prompt = "高専について教えて"; stream = $false } | ConvertTo-Json
$res = Invoke-RestMethod -Uri "http://192.168.100.xxx:11434/api/generate" -Method Post -ContentType "application/json; charset=utf-8" -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
$res.responseComposeでの起動状態を確かめます(エビデンス2・3)。
cd /opt/nginx
sudo docker compose ps
sudo docker compose logs --tail 20
cd /opt/open-webui
sudo docker compose ps
sudo docker compose logs --tail 20最後に、ブラウザで http://192.168.100.xxx/(または http://www.t3.metro-cit.internal)を開き、Open WebUIと、自作のAI連携Webサイトの動作を確かめます(エビデンス4・5)。
エビデンス(スクリーンショット5点)
指導書は、この日のエビデンスについて「システムが正しく連携していることを考察すること」と求めています。1枚ずつの説明だけでなく、5枚がどうつながっているかを図1のような構成図と合わせて説明すると、考察になります。考察のヒント同じOllamaを、PowerShell・Open WebUI・自作のWebサイトという3つの違うものから使えたのは、決まった書式(HTTPとJSON)の窓口があるからです。課題2の中心となる観察として、エビデンス1・4・5を並べて示せます。
課題を考えるための材料
課題1クラウド型AI(Gemini/ChatGPT等)と比較した、ローカルLLMの利点と欠点は何か。
考える観点
- データの行き先:質問の内容はどこへ送られ、誰の管理下に置かれるか
- 性能と規模:動かせるモデルの大きさは何で決まるか。今日の0.5bのモデルの答えの質や速さはどうだったか
- 費用:利用料と、機械・電気・管理の手間。どちらに何がかかるか
- ネットワーク:インターネットにつながっていないとき、使えるか
- 管理の責任:更新・安全対策・故障対応を誰がするか(手順2のAPIの鍵の問題も含む)
- 自分で測った値(答えが返るまでの時間、
eval_countとeval_durationから求めた速さ)を根拠に使うと、考察に説得力が出る
課題2コマンドライン(ターミナル)から直接LLMを実行するのと比較して、APIを経由してLLMを操作できるようにすることには、システム開発上どのようなメリットがあるか考察せよ。
考える観点
ollama runで話すとき、使えるのは誰で、どこからか。APIなら誰が、どこから、どんなプログラムから使えるか- 今日、同じOllamaを、PowerShell・Open WebUI・自作のWebサイトという3つの違うものから使えたのはなぜか
- 決まった書式(JSON)でやり取りすることは、使う側のプログラムの言語や種類にどう影響するか
- AIの部分を別のモデルや別のサービスに取り替えたくなったとき、何を変えれば済むか
- APIとして外に開くことで新たに生まれる課題(本人確認、使いすぎの制限など)はないか
課題3Webフロントエンド、バックエンド、AI APIの3要素は、それぞれどのような役割を担い、どのようにデータを受け渡しているか説明せよ。
考える観点
- それぞれの部品は、どの機械(ブラウザの中・コンテナの中・Ubuntuの中)で動いているか
- ①〜④の各区間で、どんな形式(JSON)の、どんな中身が、どの方式(HTTPのPOSTなど)で送られるか(図2)。自分の作ったサイトの実際の中身で書く
- 間に入っているNginx(取り次ぎ役)は、この流れのどこにいるか
- バックエンドを省いてブラウザから直接AI APIを呼ばないのはなぜか