ショート一覧

haruki256

Codexに毎回頼まず、ニュースを定期的に受け取る構成

結論

Codexに毎回ニュース収集を頼まず、定期的に結果を受け取るには、収集から通知までを予約します。僕は、CodexのクラウドでGitHubの指示書からニュースを生成・保存し、Discordへ要約と保存ファイルへのリンクを送る構成にしました。一回の予約実行で、生成・保存・受信まで確認しています。

この構成を試すには、GitHubの指示書と保存先、外部へ通信できるCodexのクラウド環境、Discordの通知先を用意します。

最新の開発情報を知るために、Codexへその日のニュース収集を頼む。これを繰り返す代わりに、集める条件と時刻を設定し、結果が通知で届くようにする使い方です。

ここでは、収集条件をGitHubの指示書に書き、Codexのクラウドへ毎朝の実行を予約します。届いた要約をDiscordで読み、気になるニュースの詳細はGitHubのMarkdownから開きます。Markdownは、見出しや箇条書きを付けられるテキスト形式です。

三つの役割を分けると、何を用意するかが見えます。

  • Codexのクラウド環境:OpenAI側でニュース生成を実行する場所。GitHubの指示書を読み、指定時刻に処理を進めます。
  • GitHub:収集条件を書いた news.md と、生成した詳しい結果の保存先。通知からそのファイルを開けます。
  • Discord:要約と保存ファイルへのリンクを受け取る入口。専用URLへ送信するとチャンネルに投稿するWebhookを使います。
クラウドで生成したニュースを保存してDiscordへ届ける構成 予約したCodexのクラウドチャットがGitHubの指示書からニュースを生成し、詳しい結果をGitHubへ保存してから、Discordへ要約と保存ファイルへのリンクを送ります。 Codexのクラウド予約指示書からニュース生成 GitHubに詳細を保存ニュースの詳しい結果 Discordで受信要約と詳細へのリンク
保存したニュース本文を開けるリンクを、要約とともにDiscordへ送ります。

クラウドで動くので、自分のPCがスリープしていても処理を続けられます。手元のフォルダを使うローカル定期タスクにはPCとアプリの起動が必要なため、この用途では実行先をクラウドにします。クラウド環境、定期タスク

試す前の準備は、次のように分けられます。

  • GitHubに、収集条件を書く news.md と結果の保存先を用意します。たとえば開発ツールの公式発表を集める、といった条件を指示書へ書きます。自分が管理し、Codexから読み書きできるリポジトリを使います。
  • DiscordのWebhook URLを、処理が名前を指定して値を読める環境変数に登録します。
  • そのリポジトリとDiscordへの通信を使えるクラウド環境を公開し、その環境を選んで新しいタスクの会話(クラウドチャット)を開きます。

僕はこのチャットで通知を試してから、ニュースの毎朝の予約を登録しました。その設定を維持したまま、同じ処理を一回だけ走らせる確認用の予約も追加しました。この確認用の予約では、ニュースのMarkdownとその一覧がGitHubへ保存され、Discordへの送信が成功し、僕自身も届いた要約とリンクを確認しています。

同じ構成を試すときは、予約が登録されたことだけで済ませず、生成物ができたか、GitHubに保存されたか、Discordで受け取ってリンクを開けたかを順に見ます。この三つがそろえば、収集を毎回依頼せずに、届いた通知から詳しいニュースを読める形を確かめられます。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/10/17/codex-cloud-discord-news/

haruki256

ローカルLLMは、重みファイル1個だけでは動かない

結論

ローカルLLMを動かすには、重みだけでなく、その構造を解釈する設定、対応するトークナイザーと入力形式、それらを実行できる推論エンジンが必要です。重みファイルが手元にあることと、モデルを正しく実行できることは同じではありません。

重みは、学習で得た大量の数値です。ただし、その数値をどの層のどの場所へ配置するかは、モデルのアーキテクチャや設定が決めます。さらに、入力文字列をモデルが使うID列へ変えるトークナイザーも、モデルに対応していなければなりません。

チャット用モデルでは、会話を学習時に想定された制御トークンと並びへ変換するチャットテンプレートも確認します。同じ「userとassistantの会話」でも、モデルごとに期待する入力形式が異なるためです。

推論に必要なものは、大きく次の役割へ分けられます。

  • モデル設定:層数や次元など、重みの構造を解釈する
  • 重み:学習で得たテンソルを保持する
  • トークナイザーと入力形式:文字列や会話をモデル用のID列へ変える
  • 推論エンジン:対応形式を読み、CPUやGPUで計算を実行する
  • 生成設定:logitから次のトークンをどう選ぶかを調整する

Safetensorsは、テンソルを保存する形式です。Hugging Faceのモデルでは、重みのほかに config.json やトークナイザー関連ファイルも使います。

GGUFはテンソルと推論用メタデータを一つのファイルへまとめますが、対応する推論エンジンが必要です。

参考:Using Transformers at Hugging Face、Chat templates、Safetensors、GGUF specification、llama.cpp

互換な一式を読み込めても、次は「重みファイルがメモリに収まるから実行できる」とは限りません。推論中には、重み以外のメモリも使います。

スレッドを開く スレッドを閉じる 2

量子化して重みを小さくしても、ローカルLLMの必要メモリが重みファイルの大きさだけで決まるわけではありません。

自己回帰型Transformerの推論では、少なくとも次の領域を分けて考えます。

  • 読み込んだ重み
  • 処理済みトークンの情報を再利用するKVキャッシュ
  • 推論エンジンが計算に使う一時領域やバッファ

量子化は、主に重みを低いビット数で表して、保存容量や重みのメモリ使用量を減らす方法です。しかし、KVキャッシュや実行時の一時領域まで同じ割合で小さくなるとは限りません。

KVキャッシュは、過去のトークンについてAttentionで使うKeyとValueを保持します。一般的な動的キャッシュでは、処理する文脈が長いほど保持量が増えます。

上限や増え方は、参照範囲を制限するSliding Window Attentionなどの方式とキャッシュ実装で変わります。

また、重みやキャッシュをRAMとVRAMのどちらへ置くかは、推論エンジンの設定で変わります。GPUに載らない部分をCPUへ置けばVRAM使用量は抑えられますが、RAM使用量や転送速度への影響を確認します。

そのため、必要メモリを見るときは、モデルファイルの容量に加えて、コンテキスト長、同時に処理する入力の数(バッチサイズ)、キャッシュ方式、CPUとGPUへの配置を一緒に確認します。

参考:KV cache strategies(Hugging Face Transformers)、Loading models(Hugging Face Transformers)、llama.cpp

では、メモリに収まって文章が生成されたら「正しく動いた」と言えるのでしょうか。確認を一つにまとめると、構成の不一致と実行環境の問題、出力品質の問題を切り分けにくくなります。

ローカルLLMの「動いた」は、少なくとも次の4段階へ分けて確認します。

  1. 読み込み:形式とアーキテクチャが推論エンジンに認識された
  2. 生成:対応するトークナイザーと入力形式で、停止条件まで文章を生成できた
  3. 計算環境:意図したCPU・GPU・オフロード設定で実行された
  4. 品質:用途を代表する固定入力に対し、必要な形式と内容を満たした

サーバーが起動しても、モデルの読み込みに失敗していれば1段階目を通っていません。

文字列が返っても、チャットテンプレートが合わず制御トークンが崩れていれば、2段階目の確認が必要です。GPUを使うつもりでも実際はCPUだけで計算しているなら、3段階目の問題です。

最後の品質確認も、「自然な文章が一度出た」だけでは足りません。要約なら出力形式と重要点、コード生成なら実行結果というように、用途ごとの固定入力と確認項目を用意すると、モデルや設定を変えたときに比較できます。

ここでは、モデル、推論エンジン、API、エージェントも分けて扱います。モデルは重みと構造を扱い、推論エンジンは計算を実行します。

APIは外部から呼ぶ窓口です。エージェントは入力やツール呼び出しの流れを組み立てます。APIやエージェントが動くことは、その下のモデル品質まで保証しません。

ローカルLLMの導入時は、必要な構成をそろえ、重み以外のメモリを見積もります。そのうえで、読み込み・生成・計算環境・品質の順に確認します。

こう分けると、「動かない」「遅い」「回答が合わない」を、重みファイル一つの問題として扱わずに調べられます。

参考:Chat templates、llama.cpp

haruki256

LLMのトークンIDは、意味の近さを表す番号ではない

結論

LLMの入力を追うときは、トークンIDと、モデルが計算に使うベクトルを分けて見ます。トークンIDの大小や差を意味の近さとして読むと、語彙表を引くための番号と、意味や文脈を表す数値を混同してしまいます。

このスレッドでは、LLMの数値処理を「IDへの変換」「各層での更新」「生成時の再利用」「次のIDの選択」の順に追います。モデルの出力ログや生成設定を読むときも、いま見ている数値がどの段階のものかを切り分けられます。

LLMに文章を渡すと、まずモデルに対応したトークナイザーが、文字列をトークンの列へ変換します。トークンは必ずしも単語一つではなく、単語の一部や記号、バイト列、特殊トークンになることもあります。

各トークンには、モデルの語彙表に対応する整数のIDが割り当てられます。このIDは、語彙表の項目を指定するための番号です。たとえばID 120と121が隣り合っていても、二つのトークンの意味が近いとは限りません。

  1. 文字列
  2. トークン列
  3. ID列
  4. トークンごとの埋め込み

モデルは各IDを使って埋め込み層を参照し、各トークンの最初のベクトルを得ます。一方、意味検索などで使う文章埋め込みは、文章や段落などのテキスト全体を一つのベクトルとして比較するための表現です。どちらも「埋め込み」と呼ばれますが、表している単位と役割が違います。

参考:Tokenizer(Hugging Face Transformers)、Semantic Textual Similarity(Sentence Transformers)

IDから得たトークンのベクトルは、まだ周囲のトークンの情報を反映していません。次は、Transformerの層がこの表現をどう更新するかを見ます。

スレッドを開く スレッドを閉じる 5

トークンIDから取り出した最初のベクトルは、Transformerのブロックを通るたびに更新されます。

中心になる処理の一つがSelf-Attentionです。各位置の表現に、その位置自身を含む、その時点で参照できる位置から情報を集めます。ただし、TransformerはAttentionだけでできているわけではありません。

  • Self-Attention:位置どうしの関係を使って情報を集める
  • Feed Forward Network:各位置の表現を個別に変換する
  • 残差接続と正規化:入力と計算結果を組み合わせ、層を重ねて処理しやすくする

これらを組み合わせ、層を重ねながら各位置の表現を更新します。正規化を置く順序や細かなブロック構成はモデルによって異なるため、特定の図をすべてのLLMへそのまま当てはめることはできません。

参考:Attention Is All You Need

ではSelf-Attentionは、どの位置から、どれだけ情報を集めるかをどう決めるのでしょうか。

Self-Attentionは、トークンIDの数値を直接比較していません。各位置の現在の表現から、Query(Q)、Key(K)、Value(V)を作って計算します。

QとKの内積をスケーリングした値が、位置どうしの関連度を求める基になります。未来の位置など参照できない場所をマスクしたうえで、softmaxという処理でスコアを重みへ変換し、その重み付きでVを混ぜます。

  1. QとK
  2. 参照先ごとのスコア
  3. マスクとsoftmax
  4. Vの重み付き和

QとKは「どの位置をどれだけ参照するか」を決める側、Vは「参照先から何を取り込むか」を担う側です。ここで比べているのはIDそのものではなく、層の途中にある表現から作ったQとKです。

参考:Attention Is All You Need

文章を一トークンずつ生成すると、この計算を次のトークンごとに繰り返します。そのとき、過去の位置で計算したKとVはどう扱われるのでしょうか。

KVキャッシュを使う自己回帰型Transformerの推論では、入力プロンプトを含む、すでに処理した位置のKとVを保存します。次のトークンを生成するときは、それらを再利用します。

キャッシュがなければ、新しいトークンを一つ加えるたびに、すでに処理した位置のKとVまで繰り返し計算することになります。KVキャッシュを使う場合は、新しい位置に必要なQ・K・Vを計算し、保存済みのK・Vと合わせてAttentionを求めます。

  • 処理済み:保存済みのK・Vを再利用
  • 新しい位置:新しいQ・K・Vを計算

KVキャッシュが保存するのは文章そのものではなく、すでに処理した各位置について、各層でAttentionに使うKとVです。その分だけメモリを使いますが、処理済みのK・Vを毎回計算し直す処理を減らせます。実際のキャッシュ方式や保持範囲は、モデルのAttention方式によって異なります。

参考:Caching(Hugging Face Transformers)

こうして過去の情報を参照したモデルは、次に来る一トークンの候補ごとにスコアを出します。では、そのスコアから次のIDをどう選ぶのでしょうか。

モデルが出すのは、語彙に含まれる各トークン候補のlogitという変換前のスコアです。確率に応じて候補をランダムに選ぶサンプリングでは、このスコアを確率へ変換する前にtemperatureで調整します。

これにより、確率が一部の候補へ集中する度合いが変わります。

  • temperatureを低くする:高いスコアの候補へ確率が集まりやすい
  • temperatureを高くする:確率が複数の候補へ広がりやすい

temperatureは、候補を選ぶ方式そのものではありません。候補の選び方や絞り方には、次のような違いがあります。

  • greedy decoding:最もスコアの高い候補を選ぶ
  • top-k:スコア上位のk個へ候補を絞る
  • top-p:累積確率が指定値へ達するまでの候補へ絞る

サンプリングを有効にした場合は、調整や絞り込みを行ったあとの分布から、確率に応じて次のトークンを選びます。

temperatureを下げても、モデルの知識が増えたり、回答が必ず正確になったりするわけではありません。変わるのは、与えられたスコアから次の候補を選ぶときの分布です。

参考:Generation(Hugging Face Transformers)

LLMの生成では、まず文字列をID列へ変え、トークンごとの表現を層で更新します。必要に応じて過去のK・Vを再利用し、そのあとで次のIDを選びます。

モデルの説明や実行ログを読むときも、この順に分けると「トークン」「埋め込み」「Attention」「生成設定」を同じ数値として扱わずに済みます。

これを記事にまとめました。

https://haruki256.com/2026/10/04/how-llm-selects-next-token/

haruki256

Tailscaleでスリープ中PCを起こす構成を、通信経路から順に整理する

結論

外出先からTailscale経由で自宅PCをWake on LANで起こすときは、Subnet Routerが作るIP経路だけで成功を判断できません。LAN側でマジックパケットを送る常時起動端末と、スリープ中もネットワークアダプターが起動信号を待てる条件を分けて確認します。

Subnet Routerを有効にすると、Tailscaleを入れられないLAN内機器のIPアドレスへ通信できます。ただし、離れた場所からLAN内へブロードキャストする通信を、そのまま自宅LANへ転送する機能ではありません。

Wake on LANでは、対象PCを起こすマジックパケットをネットワークアダプターへ届けます。よく使われるのは、UDPを使って同じLAN内へ一斉に送る方法です。

Tailscale側:LANのIP範囲までの経路を作る
LAN側   :対象PCのネットワークアダプターへ
              マジックパケットを送る

そのため「LAN宛てのIP経路を設定したので、ブロードキャストも必ず届く」とは判断できません。実際の動作は環境によって変わります。確認する条件は次の4つです。

  • 使うOS
  • Tailscaleの設定
  • Wake on LANを送るアプリ(WoLアプリ)
  • マジックパケットの宛先

まず、同じLAN内から対象PCを起動できるかを確認します。

僕の構成では、起動中のAndroidを自宅LAN側へ置きました。外出先から操作し、PCが起動するところまで確認しました。

ただし、Android版TailscaleとWoLアプリが、受け取った通信をどのようにLANへ送るかは未確認です。構成を再現するときは、Android経由でLAN内のIPへ届くことと、WoL操作で実際にPCが起動することを分けて試します。

参考:Subnet routers、Kernel vs. netstack subnet routing

IP経路だけではWoLの成功を判断できず、LAN側でマジックパケットを送る役割が必要です。次は、スリープ中のPC自身がその役割を担えるかを見ます。

スレッドを開く スレッドを閉じる 3

LAN側に送信役が必要でも、PCがスリープしてTailscaleも止まる構成では、そのPC自身に送信役を任せられません。

TailscaleはOS上で動くソフトウェアです。PCがスリープし、OSとアプリの実行が止まれば、Tailscaleも新しい通信へ応答できません。

一方、Wake on LANに対応したネットワークアダプターは、対応する低電力状態でマジックパケットを検知し、PCへ復帰を要求できます。この役割は、停止したTailscaleアプリではなく、通電が続くネットワークアダプターが担います。

スリープ中のPC
├─ Tailscaleアプリ:停止しており応答できない
└─ Wake on LAN対応ネットワークアダプター:
    条件が合えばマジックパケットを待てる

利用できるスリープ状態やWake on LANの可否は、次の条件で変わります。

  • PCが対応するスリープ状態
  • ネットワークアダプター(NIC)とドライバーのWake設定
  • UEFI(PC起動前の設定画面)のWake設定
  • 有線または無線の接続方法

Windowsならpowercfg /aで利用可能なスリープ状態を確認し、デバイス側のWake設定も別に確認します。

この制約を避けるには、自宅LANへ常時起動の端末を置きます。外部からはその端末までTailscaleで到達し、最後のWake on LANはLAN側から送る役割分担です。

参考:Wake on LAN behavior、About Network Wake-Up Events

スリープ中のPCとは別に、Tailscaleへ応答してLAN側からWoLを送れる常時起動端末が必要です。続けて、その役割をAndroidへ任せる場合の確認へ進みます。

その常時起動端末として、僕は使っていなかったAndroidを置きました。初回の起動成功だけでなく、画面消灯後と再起動後も中継できるかを確認します。

自宅LAN宛ての通信をAndroid経由で送れるよう、LANの経路をTailscaleへ知らせました。外出先のスマホからWoLアプリを操作すると、PCが起動するところまでは確認しました。

ただし、Androidの省電力機能であるDozeやApp Standbyにより、未使用時にはバックグラウンドのCPU処理やネットワーク利用が制限されます。端末メーカー独自の省電力設定が重なる場合もあります。

確認は次の順に分けます。

  1. 画面を点灯した状態で、Tailscale経由の到達とPC起動を確認する
  2. 画面を消し、時間を置いた後も同じ操作を繰り返す
  3. 失敗時は、Wi-Fi、Tailscale接続、アプリの省電力対象を一つずつ確認する
  4. 端末の再起動後にも、Wi-Fi、Tailscale接続、WoLアプリが使える状態へ戻るか確認する

設定名や制限はAndroidの版とメーカーで変わります。特定の画面名を手順として固定せず、画面消灯後と再起動後の動作を合格条件にします。

参考:Subnet routers — Android、Optimize for Doze and App Standby

TailscaleのIP経路とWoLの到達を分けると、スリープ中のPCとは別に常時起動端末を置く理由が見えます。Androidを使う場合は、PCが一度起動したことだけで終えず、中継役を継続できる条件まで確認します。

haruki256

Subnet RouterはLAN内の特定範囲へ、Exit Nodeはインターネット全体へ通信を送る

結論

Tailscaleを入れられない自宅LANの機器へ届きたいならSubnet Router、Web閲覧などのインターネット通信を別の端末から出したいならExit Nodeを選びます。違いは中継端末の名前ではなく、経路の宛先です。

どちらも、Tailscale端末を別の通信先への中継に使います。そのため設定画面だけを見ると似ていますが、クライアントへ配る経路が異なります。

Subnet Router
外出先の端末 → 自宅LANの特定範囲 → プリンターやNASなど

Exit Node
外出先の端末 → インターネット全体への経路 → Webサイトなど

Subnet Routerは、自宅LAN内の機器へ届けるためのIPアドレス範囲を、Subnet Router経由で使える経路としてTailscale端末へ知らせます。接続する側の端末は、その範囲宛ての通信だけをSubnet Routerへ送ります。目的は、Tailscaleを直接動かせないLAN内機器への到達です。

Exit Nodeは、インターネット全体へ向かう通信の経路を引き受けます。接続する側の端末が明示的にExit Nodeを選ぶと、Tailscale宛てや個別に設定したLAN向けの経路を除くインターネット通信がその端末を通ります。

僕は「何を置くか」ではなく、「どの宛先へ届けたいか」を先に決めます。LAN内の一部ならSubnet Router、インターネット側ならExit Nodeです。

参考:Subnet routers、Exit nodes

スレッドを開く スレッドを閉じる 1
haruki256

Tailscaleがポート開放なしでつながるまでを、NATから順に整理する

結論

外出先から自宅のIPv4端末へTailscaleで接続するときは、NATで届きにくいことと、安全対策が済んでいることを分けて考えます。そのうえで、自宅側と通信事業者側のNAT、Tailscaleの接続調整、実際の通信経路を順に追うと、ポート開放なしで接続できる理由を整理できます。

家庭のIPv4ネットワークでは、NAT(ネットワークアドレス変換)を行うルーターが、LAN内の複数端末を一つのグローバルIPv4アドレスへ対応付けます。LAN内の端末から外へ出た通信は対応表に記録され、その返答だけが元の端末へ戻ります。

反対に、外部から突然届いた通信に対応する記録や転送規則がなければ、通常そのままLAN内へ転送されません。

ここで起きているのは、外部から自宅LAN内の端末へ届く経路が作られていないということです。端末やアプリに脆弱性がないこと、許可された人だけが使えること、データが暗号化されていることまで保証しているわけではありません。

僕は次の二つを分けて考えます。

  • 到達性:どの経路を通れば、目的の端末やサービスへ届くか
  • 安全対策:誰の通信を許可し、何を暗号化し、端末をどう更新するか

Tailscaleのような仕組みでNATを越えて到達経路を作っても、安全対策が不要になるわけではありません。反対に、NATの内側に置いただけで安全対策が完成するわけでもありません。

参考:How NAT traversal works、Control and data planes

ここまでで、自宅ルーターのNATが外から突然届く通信を端末へ渡せない理由が分かりました。ただし、回線によっては自宅ルーターの外側にもNATがあります。次はCGNATで、なぜ自宅側のポート開放だけでは届かないかを見ます。

スレッドを開く スレッドを閉じる 5

通常の家庭内NATだけなら、自宅ルーターへ「このポートはこの端末へ渡す」という転送規則を設定できます。ところがCGNATでは、その外側に通信事業者が管理するNATがあります。

インターネット
  ↓
通信事業者のCGNAT  ← 利用者は転送規則を設定できない
  ↓
自宅ルーター       ← ここだけ開けても経路は未完成
  ↓
自宅PC

自宅ルーターの設定を変えられても、CGNAT側で「どの契約者の、どの端末へ渡すか」が決まりません。

インターネット上で自宅回線を個別に識別できるグローバルIPアドレスを契約する方法もあります。ただし、利用できるかは回線や契約に依存します。

Tailscaleは、自宅側へ固定の受信ポートを用意する代わりに、端末側から外向きに接続を始めます。NATの内側にある端末同士で直接経路を作れるか試し、できなければ中継を使います。

参考:How NAT traversal works — Concerning CGNATs、Connection types

ポートを外から開ける方法と、端末側から接続を作る方法の違いは見えました。では、外向きに接続した端末同士を誰が引き合わせ、実際のデータはどこを通るのでしょうか。次は、接続の調整とデータの経路を分けます。

端末側から外向きに接続を作るTailscaleでは、相手を見つける調整と、暗号化したデータを運ぶ経路が別の役割を持ちます。

Tailscaleへ端末を追加すると、調整サーバーは同じtailnet(Tailscaleで管理するネットワーク)内で通信を許可された端末の公開鍵、接続候補、経路、規則を配ります。端末は、その情報を使って相手との接続経路を組み立てます。

一方、アプリのデータは各端末上のデータプレーンが暗号化し、相手へ送ります。直接接続なら、暗号化されたデータは端末間を流れます。

直接接続できなければ、Tailscaleの中継サーバーであるDERPか、別のTailscale端末を中継にするPeer Relayを通ります。どちらも調整サーバーとは別の経路です。

調整サーバー:誰と、どの鍵・経路・規則でつなぐかを配る
データ経路 :暗号化した通信内容を端末間で運ぶ

「中央サーバーを使う」ことと、「すべての通信内容がそのサーバーを通る」ことは同じではありません。Tailscaleの接続を理解するときは、接続を調整する役割と、パケットを運ぶ役割を分けると追いやすくなります。

参考:Control and data planes、Connection types

ここまでで、調整サーバーが接続情報を配り、データプレーンがパケットを運ぶところまで分かりました。次は、そのデータ経路が中継から直接接続へどう変わるかを追います。

実際の接続開始時は、相手が接続しているDERPサーバーを経由して接続要求を届けます。両端末は接続候補を交換します。その後、NATの内側から互いにUDPを送り、直接届く組み合わせを探します。

DERPで接続開始
  ↓
直接UDP接続を試す
  ├─ 成功 → direct
  └─ 失敗 → 別のTailscale端末を中継にするPeer Relayを試し、
              使えなければDERPを継続

この順序なら、直接経路の探索を待ってから通信を始める必要がありません。ネットワーク条件が変わった後も、Tailscaleはよりよい経路を定期的に探します。

「Tailscaleは必ず端末同士で直接通信する」と考えると、接続状態の中継表示を異常だと誤解します。直接接続は性能面で望ましい経路ですが、中継は接続を保つための正規の経路です。

参考:Connection types

接続経路は、DERP、Peer Relay、直接接続の間で変わり得ます。現在どの経路を使っているかをコマンドで確認します。

速度や遅延が気になるときは、接続経路を推測せず、コマンドライン(CLI)から確認します。

tailscale ping <相手の端末名またはTailscale IP>

viaの後にIPアドレスとポートが表示されれば直接接続です。via DERP(...)ならDERP中継、via peer-relay(...)なら同じtailnet内のPeer Relayを使っています。

接続開始直後はDERPから直接接続へ切り替わることもあります。最初の一行だけで決めず、複数回の応答を見ます。

普段の接続一覧は、次のコマンドでも確認できます。

tailscale status

直接接続できない理由を調べる段階では、tailscale netcheckを使います。確認するのは次の項目です。

  • UDPを利用できるか
  • 接続先によってNATの割り当てが変わるか
  • どのポートマッピング方式が使われているか
  • どのDERPが近いか

中継であることだけを原因と決めず、「今どの経路か」と「現在のネットワーク条件」を分けて確認します。

参考:Connection types、Tailscale CLI

NATで外から届きにくい理由と、Tailscaleが作る通信経路を分けると、ポート開放なしでつながる仕組みと、現在の経路を確かめる方法を同じ流れで追えます。

haruki256

記事とショートの公開状態を、フラグではなくフォルダの場所で決めた

記事とショートの公開と未公開を、front matter(ファイルの冒頭に書く設定の並び)のフラグではなく、フォルダの場所で決めることにしました。

未公開のものは content/drafts/ に置き、公開するときは content/posts/<年>/ や content/shorts/<年>/ へフォルダごと移動します。ファイルの中身は編集しません。

これまではフラグを見るためにファイルを開く必要がありました。いまは公開の操作がフォルダの移動だけなので、開かなくても状態が分かります。

スレッドを開く スレッドを閉じる 2

未公開フォルダの設定は content/drafts/_index.md の1か所にまとめています。

このファイルに cascade(配下のページへ設定を配るHugoの仕組み)で draft: true を書くと、配下がすべて未公開になります。1件ずつ書く必要はありません。

同じファイルに、ページ自体を生成しない設定(build.render: never)と、一覧に載せない設定(build.list: never)も入れています。これで drafts フォルダ自体もページとして出力されません。

ローカルで下書きを見るときは hugo server -D(-D は下書きも表示するオプション)を使います。

これを記事にまとめました。

https://haruki256.com/2026/07/28/hugo-mock-content-environment/

haruki256

Flutterの入力ダイアログでは、ControllerをダイアログのStateに持たせる

結論

Flutterで TextEditingController を使う入力ダイアログを作るなら、Controllerはダイアログの State で生成し、State.dispose で破棄します。showDialog の完了後も終了アニメーション中は入力欄が残るため、呼び出し側ですぐ破棄すると、表示中の入力欄から参照されるおそれがあります。

僕が実装した入力ダイアログでは、呼び出し側が showDialog の完了を待ってControllerを破棄すると、終了アニメーション中に「破棄後に使われた」というエラーが出ました。Navigator.pop で返り値が確定する時点と、画面からダイアログが取り除かれる時点は同じではありません。

そこでControllerをダイアログの State で作り、ダイアログが画面から取り除かれるときに呼ばれる State.dispose で破棄するようにしました。これなら、入力欄が残っている間はControllerも残ります。

Controllerの寿命は、それを作った関数ではなく、最後まで参照するWidgetに合わせるのが判断の基準です。

haruki256

ファイル差分だけでSQLiteキャッシュを更新すると、削除したフォルダの警告が残る

結論

JSONファイルの読み込み結果をSQLiteへキャッシュするアプリで差分更新を実装するなら、ファイルの追加・変更だけをテストして終えてはいけません。フォルダ削除は差分として検出されず、古い警告が残ることがあるため、更新対象が0件になる削除ケースもテストに含めます。

指定フォルダ内のJSONファイルを読み、結果と警告を一覧表示するローカルアプリで、画面表示用データをSQLite(アプリ内に組み込めるデータベース)へキャッシュとして保存しています。僕の実装では、ファイル差分だけでキャッシュを更新したところ、削除したフォルダに紐づく警告が画面に残りました。そこで、更新時に警告の参照先を確認し、存在しない対象の警告を削除しました。

ファイルの削除は変更として検出できました。一方、フォルダの削除はファイルの変更として検出されないため、フォルダがなくなっても警告の行だけがSQLiteに残っていました。

対策として、キャッシュの更新時に保存済み警告の参照先が実在するかを確認し、存在しないファイルやフォルダの警告を削除します。変更されたファイルが1つもない場合でも、この確認は省きません。

このようなJSON読み込み・SQLiteキャッシュ・ファイル差分更新の構成を使うなら、フォルダ削除後にも古い警告が消えるかをテストする必要があります。差分更新のテストでは、ファイルを追加・変更する処理だけでなく、参照先が消えた後に警告を削除できるかも確認します。

haruki256

フォルダ再編で、ローカルHTMLの相対リンクが切れた

手元のHTMLを分野ごとのフォルダへ整理したら、ページ間のリンクがまとめて切れました。../別分野/index.html のように、今いるファイルからの位置でリンクを書いていたためです。

ファイルを移すと、リンクに必要な ../ の数も変わります。今後もフォルダ構成を直す前提だったので、置き場所を基準にするリンクをやめることにしました。

スレッドを開く スレッドを閉じる 2

フォルダ再編でリンクを壊さず、別の端末でも同じHTMLを開けるようにするため、/common/style.css のように、サーバが公開するフォルダを基準に書くURLへ変えました。

端末内の絶対パスは、Google Driveの同期先が端末ごとに違うため使えません。このURLも、HTMLファイルを file:// で直接開く方法では、プロジェクトのフォルダを基準にできません。

そこで、自分のPC内だけで動くサーバを用意し、自分のPCを指す 127.0.0.1 のURLで開く形へ変えました。使っていたWindows環境では、WindowsにあるPowerShellと.NETだけで動かせました。

これを記事にまとめました。

https://haruki256.com/2026/07/30/local-html-over-localhost/

haruki256

ショートを、一覧の流れを残したまま共有できるURLにした

僕のブログでは、ショートを1件共有するときも、共有先は個別ページではなくショート一覧にしました。URLには一覧内の特定位置を指すアンカーが付き、該当するカードを示します。

ショート一覧でカードを選ぶと、別ページへは移らず、ブラウザのアドレスバーにあるURLだけが変わります。選んだカードは枠で強調され、URLをコピーするボタンも同じ一覧内アンカーを返します。

共有されたURLを開くと、一覧が該当カードの位置までスクロールします。その前後には、ほかのショートもそのまま並びます。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/07/29/hugo-posts-and-shorts/

haruki256

Hugoからの乗り換えに備えて、専用処理をテンプレートと設定にまとめた

Hugoを選びましたが、将来も使い続けるとは決めていません。僕は別のツールへ移るとき、記事本文とデザインまで作り直すのは避けたいと考えました。

そこで、記事本文はHugo固有の記法を入れないMarkdownで書き、Hugoのショートコードは使わないことにしました。装飾はCSS、動きはJavaScriptに分け、Hugoだけで動く処理はテンプレートと設定にまとめています。新しくHugo固有の機能を使う場合は、専用の処理をまとめた一覧へ追記します。

スレッドを開く スレッドを閉じる 3

ショートに続きの投稿を足すときも、Hugoのショートコードは使いませんでした。代わりに、本文へHTMLコメント(Hugoの画面に表示されない目印)を置いています。

テンプレートはコメントを見つけると、後ろの文章を続きの投稿として表示します。別のツールへ移す場合は、移行先にも投稿ごとに分ける処理が必要ですが、本文を書き直さずに同じ目印を使えます。

Hugoから移るときに何を作り直すのか、サイト全体を整理しました。

  • そのまま使うもの:Markdownの記事本文、CSS、JavaScript、画像やフォント
  • 作り直すもの:Hugoのテンプレートとサイト設定、目次や前後の記事へのリンクを自動で作る処理

Hugoだけで動く処理は、テンプレートと設定に集まっていました。僕はこの一覧を乗り換え時のチェックリストとして残し、一覧にないHugo固有の機能は使わないことにしました。

これを記事にまとめました。

https://haruki256.com/2026/07/26/hugo-dependency-three-layers/

haruki256

WordPressを残したままHugoへ切り替えた

haruki256.comの本番を、WordPressからHugoへ切り替えました。切り替え前にデータベースとファイル一式をバックアップし、Hugoの生成物は公開先とは別のフォルダへ展開しています。

WordPressは削除せず、public_html を別名に変えてからHugo側と入れ替えました。公開後は主要ページ、CSS、JavaScript、SVG、RSS、サイトマップ、404、HTTPSへの転送を確認しています。

問題があればHugoを別の場所へ動かし、WordPressのフォルダ名を public_html へ戻します。切り替える方法だけでなく、問題が起きた場合に戻す方法も先に用意しておきました。

スレッドを開く スレッドを閉じる 1
haruki256

WordPressの旧記事13本はHugoで公開しないことにした

WordPressから取り出した旧記事は、Hugoへ移したからといって、すべて公開し直す必要はありません。

このブログでは、公開されていた7本と旧下書き6本をMarkdownで手元に残し、13本すべてを非公開にしました。以前の公開URLは記録用の legacy_url に残しますが、Hugoの公開先には使いません。

新しい記事は /:year/:month/:day/:slug/ の形式で、slugを英小文字のkebab-caseに統一します。旧記事を将来書き直して公開する場合だけ、新URLと転送を設定します。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/07/21/wordpress-to-hugo-url-design/

haruki256

HTML教材で復習先を探すため、学習項目の前提関係をJSONで持つ

結論

HTML教材で誤答した学習項目から復習先を探すなら、学習項目同士の「先に理解する項目から、後で学ぶ項目へ」という関係をJSONで持ちます。誤答した項目のIDだけでは、その項目より先に理解しておくべき項目のどれでつまずいたかまでは分からないため、関係を逆向きにたどって復習候補を出します。

「HTML教材の採点結果は、問題文ではなく学習項目IDに結び付ける」では、問題の正誤と学習項目IDを一緒に残す設計を書きました。ただし、誤答したIDから分かるのは、その問題が扱う学習項目です。

そこで僕は、学習項目同士の直接の前提関係をJSONで表しました。たとえば「関数」の前提が「変数」、「変数」の前提が「値」なら、関数の問題で間違えたときに、関係を逆向きへたどって変数と値を復習候補として並べます。

このJSONが示すのは、教材内で決めた学習項目の前後関係です。復習候補を整理するために使い、学習効果を測った結果としては扱いません。

スレッドを開く スレッドを閉じる 1
haruki256

HTML教材の採点結果は、問題文ではなく学習項目IDに結び付ける

結論

ブラウザで問題を解くHTML教材を作るなら、採点結果には正誤だけでなく、問題が扱う学習項目の固定IDも残します。問題文を後から直しても同じ項目の誤答として追えるため、復習対象を文章の比較に頼らず選べます。

「HTML教材の必須構成を、目視チェックリストからスクリプト検査へ移した」では、僕が章ごとのHTML教材を作り、各章に理解を確かめる問題を置いていることを書きました。その問題の採点結果を後から復習へ使うため、問題文とは別に、その問題が扱う内容を表す学習項目IDを持たせました。

たとえば関数を扱う問題なら、採点結果へ正誤と concept-functions を一緒に残します。

これなら問題文を読みやすく書き直しても、同じIDの結果を同じ学習項目として扱えます。問題が扱う学習内容そのものを変えるときだけ、新しいIDに分けます。

正誤はその問題の結果、学習項目IDは復習する範囲を特定する手がかりとして、役割を分けて残します。

スレッドを開く スレッドを閉じる 1
haruki256

HTML教材の必須構成を、目視チェックリストからスクリプト検査へ移した

僕は、ソフトウェアアーキテクチャなどを学ぶための教材を、1章につき1つのHTMLページとして作っています。各章は、見出しで学ぶ内容を分け、図で関係を示し、問題で理解を確かめ、復習リンクから前の項目へ戻れる構成です。

章の中から必要な要素が1つ抜けても、HTMLページ自体はブラウザに表示されます。そのため、章を追加するたびに目で探すだけではなく、必要な要素が欠けていたらエラーにするスクリプトを書きました。

スクリプトへ章のHTMLを渡すと、見出し・図・問題・復習リンクを順に探します。どれかが欠けた章はエラーになり、すべて含む章だけが検査を通ります。

この検査で分かるのは、必要なHTML要素が存在するかどうかです。画面上のカード配置や図の読みやすさは、別に画面を見て確認します。

haruki256

WordPressからHugoへの移行を始めた

haruki256.comを、WordPressからHugoへ移す作業を始めました。

WordPressに大きな不具合が起きたわけではありません。記事をMarkdownで手元に置き、gitで変更を追い、公開環境では静的ファイルだけを配信する形が、これからの運用に合うと判断しました。

調査時点の投稿は7本で、承認済みコメントは0件でした。移行コストがまだ小さい今のうちに、既存URLを保ったまま切り替えられるか検証しています。

スレッドを開く スレッドを閉じる 1
haruki256

RAGへ渡したノートが分かるよう、回答に参照ソースを表示した

僕は、検索結果を生成AIへ渡して回答を作るRAGを実装しました。質問に関連するノートの一部を、回答用の情報としてチャットモデルへ渡します。回答だけを表示すると、どのノートが候補に入ったのか分かりません。

そこで、回答の下に「参照ソース」欄を置き、検索で選ばれたノートのパスと見出しを表示しています。見出しがない場合は、ノートのタイトルを表示します。

参照ソースは、回答が正しいことの保証ではありません。どのノートが候補だったかを確認するための手がかりとして使います。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/07/01/obsidian-note-search-rag/

haruki256

Obsidianノートを、キーワードと内容の近さの両方で探すことにした

僕は、Obsidianに蓄積したMarkdownノートを探すデスクトップアプリを作っています。名前や用語の一致だけでなく、内容の近さからも候補を出せるように、検索方式を一つに絞りませんでした。

実装では、BM25(単語の一致度で順位を付ける全文検索)と、ONNX形式のモデルで内容の近さを数値化するベクトル検索を使います。検索用のベクトルを用意できている場合は、同じ検索文を両方に渡します。二つの順位はRRF(複数の検索順位を一つにまとめる方法)で統合します。

ベクトルを利用できないときは、BM25の結果だけを返します。利用者に検索方式を選ばせず、一つの検索欄で扱う設計です。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/07/01/obsidian-note-search-rag/

haruki256

ObsidianのZettelノートを、自分や子孫の下へ移動できないようにした

結論

Obsidianで一つの焦点に絞った考えをZettelとして残し、親子関係を階層IDで管理するなら、IDの移動機能では自分自身・子孫への移動を拒否します。利用者が指定した新IDが別のノートですでに使われている場合も、書き込み前に止めます。循環やIDの重複ができると、親子ツリーを一意にたどれなくなるためです。

「Obsidianの階層Zettel IDを移すときは、子孫IDの対応表を先に作る」では、親ノートを移す前に子孫までの新旧ID対応表を作る判断を書きました。

僕は、Obsidianで一つの焦点に絞った考えをZettelとして残しています。ノート先頭には、親子関係を表す一意のZettel IDを付けています。

このIDを管理するために自作プラグインを作りました。Obsidian標準の機能ではありません。

移動先が自分自身または自分の子孫なら、親をたどると元のノートへ戻る循環ができます。指定した新IDが移動元以外で使われていれば、複数ノートへ同じIDを割り当てる衝突になります。僕のプラグインでは、どちらも対応表をノートへ反映する前に拒否します。

これは、実行前に分かる不正な移動を止める処理です。書き込み開始後に保存が失敗した場合は自動で元へ戻らないため、実行前のバックアップと実行後のID照合は別に必要です。

スレッドを開く スレッドを閉じる 1
haruki256

Obsidianの階層Zettel IDを移すときは、子孫IDの対応表を先に作る

結論

Obsidianで階層Zettel IDを移すなら、親ノートのIDだけを書き換えず、先に子孫までの新旧ID対応表を作ります。親のIDだけを 1 から 3 に変えると、子と孫のIDが古いまま残り、親子関係がずれるためです。

「Obsidianの親子ノートIDを手作業で管理しにくくなり、自動採番プラグインを作った」では、階層IDの採番と重複確認を自作プラグインへ切り出した経緯を書きました。続く「Obsidianの親子ノートIDを自動採番し、子作成とツリー表示まで行うプラグイン」では、子ノートの作成とツリー表示までの設計をまとめています。

僕は、Obsidianノートの置き場所とは別に、考えがどのノートから派生したかをZettel IDで管理しています。1.a は 1 から派生したノート、1.a.1 は 1.a から派生したノートを表します。今回は、フォルダや分類番号ではなく、このIDが表す親子関係を別の位置へ移す話です。

僕の自作プラグインは、変更するノートとその子孫を集め、次の対応表を作ってからfront matter(ノート先頭のメタデータ)へ反映します。Obsidian標準の機能ではありません。

1       → 3
1.a     → 3.a
1.a.1   → 3.a.1

対応表は親から子へ順にたどり、新しい親IDに合わせて子のIDを付け直して作ります。1 を 3 へ移した後も、子は 3.a、孫は 3.a.1 という関係で保存されます。

プラグインは、表にあるノートを1件ずつ保存します。途中で失敗しても自動では元に戻らないため、実行前にVault(Obsidianのノート保管場所)をバックアップします。実行後は、各ノートのIDが対応表の新しい値と一致するかを見比べます。

スレッドを開く スレッドを閉じる 1
haruki256

Obsidianの親子ノートIDを手作業で管理しにくくなり、自動採番プラグインを作った

結論

Obsidianで1、1.aのようなIDを使ってZettelの親子関係を表すなら、次の子IDを選ぶ処理と重複確認は自動化できます。人は「どの考えから派生させるか」を決め、連番の管理をプラグインに任せれば、IDを考えの親子関係だけに使えるようになります。

僕のVault(Obsidianのノートをまとめる保管場所)では、自分の考えを一つの焦点へ絞ったノートをZettelとして残しています。1の考えから派生したZettelには1.a、さらにそこから派生したZettelには1.a.1というIDを付けます。

「NDCは主題、Zettel IDは考えの派生に使う」で書いたように、このIDの役割はノートの分類ではなく、考えの親子関係を残すことです。

Zettelが増えると、新しい子を作るたびに、次の三つを手作業で行う必要がありました。

  • 同じ親の子IDを調べる
  • 次に空いているIDを選ぶ
  • 既存のIDと重ならないか確かめる

考えを書く前にこの三つを毎回行うことが、手作業では負担になりました。

そこで、階層Zettel IDの管理だけを補助するObsidianプラグインを作りました。ツリーで親を選んで子を追加すると、プラグインが既存の直下の子IDを調べ、空いている最初のIDを付けたノートを作ります。たとえば1.aと1.bがあれば、次は1.cです。

どの考えを親にするかと、新しいZettelに何を書くかは僕が決めます。プラグインへ任せたのは、その判断の後に繰り返していたIDの割り当て(採番)と重複確認です。

スレッドを開く スレッドを閉じる 1

これを記事にまとめました。

https://haruki256.com/2026/05/11/obsidian-zettel-id-plugin/

haruki256

NDCは主題、Zettel IDは考えの派生に使う

結論

Obsidianで1ノート1焦点のZettelを整理するなら、僕はNDC(日本十進分類法)を参考にした分類番号を、ノートの主題に使います。1から1.aのようなZettel IDは、どの考えから派生したかという親子関係に使います。主題と考えのつながりを、一つの番号で表さないためです。

僕のVault(Obsidianのノートをまとめる保管場所)では、Zettelを1ノート1焦点で残しています。Zettelは、自分の考えを一つの焦点へ絞ったノートです。

「外部資料から自分の考えまでを、3つのレイヤに分ける」では、Zettelを自分の思考を置くレイヤとして分けました。そのZettelを増やしていくと、ノートの主題と、どの考えから発展したかを別々にたどりたくなりました。

そこで、僕のVaultでは二つの番号へ別の役割を持たせています。

  • NDCを参考にした分類番号:そのノートが何について書かれているかを表す
  • 1、1.aのようなZettel ID:どの考えから発展したかを表す

ここでの分類番号は、日本十進分類法を参考にしてノートの主題を表す、僕のVault内の運用です。一方のZettel IDは分類には使わず、1.aなら1から発展した考え、という派生関係だけを表します。

同じ主題のノートでも、考えの派生関係が別ならZettel IDは別になります。反対に、ある考えから派生した流れが続いていても、焦点が別の主題へ移れば分類番号は見直せます。

この二つを分けておけば、主題に合わせてノートの置き場を変えることと、考えの親子関係を変えることを、同じ操作として扱わずに済みます。

スレッドを開く スレッドを閉じる 1
haruki256

読書の要約と、そこから生まれた自分の考えを分ける

結論

Obsidianで本や記事を読みながらメモするなら、要約・引用は外部資料のノートに残し、そこから生まれた自分の考えはZettel(1ノート1焦点で残す自分の考えのノート)へ分けます。出典と自分の解釈の境界を、あとからたどれるようにするためです。

僕はObsidianのVault(ノートをまとめる保管場所)を、外部資料と自分の考えの両方を残す場所として使っています。

「ObsidianのVaultを、情報の役割ごとに構成する」では、Vault全体の置き場を役割で分けました。その中でも、書籍や記事を読んだ記録は、次の2つを同じノートへ混ぜないようにしています。

  • 外部資料のノート:著者の主張、要約、引用、出典
  • Zettel:読んだ内容から、自分の考えを一つの焦点に絞ったノート

僕のVaultで読書由来のZettelに残すのは、本の内容を章ごとにまとめ直したものではありません。元資料を読んで生まれた自分の考えを、一つの焦点へ絞ったものです。

自分の考えが独立したときは、元の要約へ書き足さず、新しいZettelから考えの元になった外部資料のノートへリンクします。これにより、どこまでが資料の内容で、どこからが自分の解釈なのかを分けて読み返せます。

ただし、短い引用に一言だけ感想を添えた段階では、無理にZettelへ切り出しません。あとから単独で考え直したい焦点になったときに、初めて別のノートにします。

スレッドを開く スレッドを閉じる 1
haruki256

ObsidianのVault直下は、情報の役割ごとに分ける

結論

Obsidianで複数種類の情報を同じVault(ノートの保管場所)に置くなら、Vault直下は情報の役割ごとに分けます。主題だけで分けると、同じ分野の資料と自分の考えが混ざるためです。Zettel(1ノート1焦点で残す自分の考え)は、その中の一つの役割として置きます。

僕はObsidianを、Zettel専用ではなく、自分の情報をためて整理する場所として使っています。

同じVaultには、役割の異なる情報を置いています。

  • 書籍や記事などの外部資料
  • あとから参照する事実や用語
  • 試して得た手順や、整理した知識
  • 1つの焦点に絞って残した自分の考え
  • アイデア、計画、外部向けの成果物

これらを最初から技術、社会、芸術といった主題だけで分けると、同じ主題について集めた資料と、自分で考えたことと、実用できる形に整理した知識が同じ場所へ混ざります。

そこで、情報を何に使うかで、Vault直下の最初のフォルダを分けています。その内側で、必要に応じて「何について書かれているか」を主題として分類します。

この構成では、Zettelは自分の考えを1ノート1焦点で残す置き場です。Vault全体をZettel中心の構成にせず、ほかの情報と区別して扱うための一つの役割として使っています。

スレッドを開く スレッドを閉じる 1