Files
cpp-httplib/docs-src/pages/ja/llm-app/ch07-code-reading.md
yhirose bef278e0d2 Fix docs pages that no longer match the code
Samples that did not compile or run as shown:
- res.user_data.get<T>() inside a generic lambda needs the `template`
  keyword; use explicit parameter types (tour 09, cookbook s15).
- listen() on a Unix domain socket fails with port 0 (tour 09, s22).
- "*.dev.local" is not a NO_PROXY pattern (c16).
- ssl_backend_error() holds a verify result, not an ERR_get_error()
  value, after a verification failure; decode each with the matching
  OpenSSL function (c18).
- The content provider's `length` is everything that remains, so the
  sample read the whole file in one call (s05).

Statements corrected:
- Client keep-alive is off by default; c14 is rewritten around
  set_keep_alive(true).
- Mounted files are looked up before GET handlers (tour 04, s04).
- Params keep insertion order, and to_string(Error::Connection) reads
  "Could not establish connection" (tour 02).
- A chunked provider ends with sink.done(), and post_routing_handler
  runs before the response is sent (tour 09).
- Timeouts surface as Error::Read; Error::Timeout comes from the stream
  API (c17). The max timeout cuts off the wait for the response only
  (c13). The progress callback needs Content-Length (c11).
- Encoding selection follows q-values, then Brotli, gzip, Zstd (s08),
  and the client compresses with the first of those it was built with
  (c15).
- stop() cuts a provider-driven response short (s19); a rejected
  content_reader already gets 400 or 413 (s07); user_data values must be
  copyable (s12); Client accepts a client certificate too (t04);
  on_message() is the fallback for every unhandled event and 204/403/404
  end reconnection (e04); the pong timeout takes two to three intervals
  and ends a waiting read() (w02).

In the LLM app tutorial, an uncaught exception does not crash the
server, so say what it does instead. Drop the server and client timeout
settings whose stated purpose, covering inference and download time,
they do not serve: those timeouts bound a single socket wait. Update
the llama.cpp server layout in chapter 7.
2026-10-08 20:39:13 -04:00

8.3 KiB
Raw Blame History

title, order
title order
7. llama.cpp本家のサーバー実装をコードリーディング 7

6章かけてゼロから翻訳デスクトップアプリを作りました。動くものは完成しましたが、あくまで「学習用」の実装です。では「プロダクション品質」のコードはどう違うのか? llama.cppに同梱されている公式サーバーllama-serverのソースコードを読んで、比較してみましょう。

llama-serverはllama.cpp/tools/server/にあります。同じcpp-httplibを使っているので、コードの読み方はこれまでの章と同じです。

7.1 ソースコードの場所

llama.cpp/
├── tools/server/
│   ├── server.cpp           # エントリポイント
│   ├── server-http.cpp      # HTTPサーバー(cpp-httplibを使う層)
│   ├── server-context.cpp   # 推論とスロットの管理
│   └── ...
└── vendor/cpp-httplib/
    └── httplib.h            # cpp-httplib(同梱版)

実装は役割ごとに複数のファイルに分かれています。全体の規模は大きいですが、構造を知っていれば読むべき箇所は絞れます。

7.2 OpenAI互換API

ここまで作ってきたサーバーとllama-serverの最も大きな違いはAPIの設計です。

私たちのAPI:

POST /translate          → {"translation": "..."}
POST /translate/stream   → SSE: data: "token"

llama-serverのAPI:

POST /v1/chat/completions  → OpenAI互換のJSON
POST /v1/completions       → OpenAI互換のJSON
POST /v1/embeddings        → テキスト埋め込みベクトル

llama-serverはOpenAIのAPI仕様に合わせています。つまり、OpenAIの公式クライアントライブラリ(Pythonのopenaiパッケージなど)がそのまま動きます。

# OpenAIクライアントでllama-serverに接続する例
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="dummy")

response = client.chat.completions.create(
    model="local-model",
    messages=[{"role": "user", "content": "Hello!"}]
)

既存のツールやライブラリとの互換性を持たせるかどうかは、大きな設計判断です。私たちは翻訳専用のAPIをシンプルに設計しましたが、汎用のサーバーを作るならOpenAI互換が事実上の標準になっています。

7.3 同時リクエスト処理

私たちのサーバーはリクエストを1つずつ処理します。翻訳中に別のリクエストが来ると、前の推論が終わるまで待ちます。1人で使うデスクトップアプリなら問題ありませんが、複数人で共有するサーバーでは困ります。

llama-serverはスロットという仕組みで同時リクエストを処理します。

llama-serverのスロット管理

ポイントは、各スロットのトークンを1つずつ順番にではなく、まとめて1回のバッチで推論することです。GPUは並列処理が得意なので、2人分を同時に処理しても1人分とほとんど変わらない時間で済みます。これを「連続バッチ処理(continuous batching)」と呼びます。

私たちのサーバーではcpp-httplibのスレッドプールが各リクエストに1スレッドを割り当てますが、推論自体はllm.chat()の中でシングルスレッドです。llama-serverはこの推論部分を共有のバッチ処理ループに集約しています。

7.4 SSEフォーマットの違い

ストリーミングの仕組み自体は同じ(set_chunked_content_provider + SSE)ですが、送るデータのフォーマットが違います。

私たちの形式:

data: "去年の"
data: "春に"
data: [DONE]

llama-server(OpenAI互換):

data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"去年の"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"delta":{"content":"春に"}}]}
data: [DONE]

私たちの形式はトークンだけを送るシンプルなものです。llama-serverはOpenAI互換のため、1つのトークンにもJSONのラッパーが付きます。冗長に見えますが、idでリクエストを識別したり、finish_reasonで停止理由を返せたりと、クライアントにとって便利な情報が含まれています。

7.5 KVキャッシュの再利用

私たちのサーバーでは、リクエストのたびにプロンプト全体をゼロから処理しています。翻訳アプリのプロンプトは短い("Translate the following text to ja..." + 入力テキスト)ので、これで問題ありません。

llama-serverは、前のリクエストと共通するプロンプトのprefixがある場合、その部分のKVキャッシュを再利用します。

KVキャッシュの再利用

長いシステムプロンプトやfew-shot例を毎回送るチャットボットでは、これだけで応答時間が大幅に短縮されます。数千トークンのシステムプロンプトを毎回処理するのと、キャッシュから一瞬で読むのとでは、体感が全く違います。

翻訳アプリではシステムプロンプトが1文だけなので効果は限定的ですが、自分のアプリに応用するときは意識したい最適化です。

7.6 構造化出力

翻訳APIはプレーンテキストを返すので、出力形式を制約する必要がありませんでした。でも、LLMにJSONで返させたい場合はどうでしょう?

プロンプト: 以下の文の感情を分析してJSONで返してください。
LLMの出力(期待): {"sentiment": "positive", "score": 0.8}
LLMの出力(現実): 感情分析の結果は以下の通りです。{"sentiment": ...

LLMは指示を無視して余計なテキストを付けることがあります。llama-serverはこの問題を**文法制約(grammar)**で解決しています。

curl http://localhost:8080/v1/chat/completions \
  -d '{
    "messages": [{"role": "user", "content": "Analyze sentiment..."}],
    "json_schema": {
      "type": "object",
      "properties": {
        "sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
        "score": {"type": "number"}
      },
      "required": ["sentiment", "score"]
    }
  }'

json_schemaを指定すると、LLMのトークン生成時に文法に合わないトークンを除外します。出力が必ず有効なJSONになるので、json::parseが失敗する心配がありません。

LLMをアプリに組み込むとき、出力を確実にパースできるかどうかは信頼性に直結します。翻訳のようなフリーテキスト出力では不要ですが、APIのレスポンスとして構造化データを返す用途では必須の機能です。

7.7 まとめ

ここまでの違いを整理します。

観点 私たちのサーバー llama-server
API設計 翻訳専用 OpenAI互換
同時リクエスト 1つずつ処理 スロット+連続バッチ
SSEフォーマット トークンのみ OpenAI互換JSON
KVキャッシュ 毎回クリア prefixを再利用
構造化出力 なし JSON Schema/文法制約
コード量 約200行 数千行

私たちのコードがシンプルなのは、「デスクトップアプリで1人が使う」という前提があるからです。複数人に提供するサーバーや、既存のエコシステムと連携するサーバーを作るなら、llama-serverの設計が参考になります。

逆に言えば、200行のコードでも翻訳アプリとしては十分に動きます。「必要な分だけ作る」ことの価値も、このコードリーディングから感じてもらえたら嬉しいです。

次の章へ

次の章では、ここまで作ったアプリを自分のライブラリに差し替えてカスタマイズするためのポイントをまとめます。

Next: 自分だけのアプリにカスタマイズする