Chainlitは、ChatGPTのような会話画面をPythonのコードだけで作れるオープンソースのフレームワークです。LLMの応答をストリーミングで流し、ツール呼び出しなどの途中経過を「ステップ」として画面に並べられるため、社内向けのAIチャットやRAGの試作によく使われます。本記事では2026年8月25日公開の最新版2.12.0をもとに、インストールから最小アプリ、OpenAI・LangChainとの連携、UI・認証・デプロイの設定までをコード付きで説明します。2025年5月から開発体制がコミュニティ保守に移ったことと、最新版にも修正の出ていない脆弱性が残っていることも、導入前に知っておくべき点として取り上げます。
まとめ:Chainlitの要点と導入前の確認事項
- Chainlitは、LLMアプリの会話UIを作るPythonフレームワークです。ライセンスはApache-2.0で、商用利用もできます。
- 最新版は2.12.0(2026年8月25日)です。対応するPythonは3.10以上3.14未満で、3.14には入りません。
@cl.on_messageなどのデコレーターで処理を書き、chainlit run app.py -wで起動します。既定のURLはhttp://localhost:8000です。- 2025年5月1日に元の開発チームが開発から退き、現在はコミュニティのメンテナーが保守しています。Chainlit SASは今後の更新を保証していません。
- 2.12.0は重大な脆弱性2件(CVE-2026-45018・CVSS 9.8ほか)を直したセキュリティリリースです。ただし2.12.0でも未修正の脆弱性が2件あり、不特定多数に公開する用途では前段で対策が要ります。
Chainlitとは:LLMアプリの会話UIを作るPythonフレームワーク
Chainlitは、Pythonで書いた処理に会話画面をかぶせるためのフレームワークです。サーバー側はFastAPIとpython-socketio、画面はReactで作られており、ブラウザとの間はWebSocketでやり取りします。開発者はHTMLやJavaScriptを書かず、メッセージを受け取ったときの処理をPythonの関数として書くだけで済みます。
画面にはチャット欄のほか、ファイル添付、ボタン(Action)、会話履歴のサイドバー、ツール呼び出しの経過表示が標準で揃っています。OpenAI、LangChain、LlamaIndexなどとの連携モジュールも同梱されています。リポジトリは2023年3月に作成され、GitHubのスター数は12,479です(2026年9月28日時点)。
2025年5月からのコミュニティ保守への移行
GitHubのREADME冒頭には、次の告知が掲げられています。
As of May 1st 2025, the original Chainlit team has stepped back from active development.
2025年5月1日以降、元の開発チームは開発から退き、正式なメンテナー契約を結んだ @Chainlit/chainlit-maintainers がコードレビュー、リリース、セキュリティ対応を担っています。READMEには、Chainlit SASが今後の更新について保証しないことも明記されています。
移行後も2.12.0までリリースは続いています。ただし修正の時期を約束する主体はいないため、修正時期の保証が必要な案件では、保守契約の有無と自社で直せる範囲を先に確認してください。業務で使うなら、依存ライブラリの更新とセキュリティ情報の確認を自分たちの運用に組み込むことが前提になります。
StreamlitやGradioとの違いと選び分け
同じくPythonだけで画面を作れる道具にStreamlitとGradioがあります。Streamlitは画面を操作するたびにスクリプトを上から実行し直す仕組みで、ダッシュボードやデータ可視化が得意です。Gradioは機械学習モデルのデモ画面を短いコードで作ることに強みがあります。Chainlitは会話に特化しており、イベントごとに非同期関数を呼ぶ構造のため、長いストリーミング応答やエージェントの途中経過の表示を素直に書けます。
| 観点 | Chainlit | Streamlit | Gradio |
|---|---|---|---|
| 得意な画面 | 会話・エージェント | ダッシュボード | モデルのデモ |
| 処理の書き方 | イベントごとの非同期関数 | スクリプト全体を再実行 | 入出力の関数を登録 |
| 途中経過の表示 | Stepで表示 | st.statusで表示 | ChatMessageのmetadataで表示 |
| 表やグラフ中心の画面 | 不向き | 向く | 一部向く |
選び方ははっきりしています。会話が主役ならChainlit、表やグラフが主役ならStreamlitです。Streamlitのチャット機能や料金の考え方はStreamlitのできること・使い方・料金の解説で扱っています。逆に、Chainlitを選ぶべきでないのは、保守の継続性を契約で担保する必要がある案件と、認証なしで社外に公開する案件です。前者は開発元の保証がなく、後者は後述の未修正脆弱性がそのまま露出します。
Chainlitのインストールと動作要件(2.12.0)
PyPIに登録されている2.12.0の動作要件は requires_python >=3.10,<3.14.0 です。Python 3.14の環境では2.12.0は候補から外れ、要件を満たす古い版が入るか、インストールに失敗します。仮想環境を作ってからインストールし、付属のデモを開いて確認します。
python3.12 -m venv .venv
source .venv/bin/activate
pip install "chainlit==2.12.0" requests
chainlit hello
chainlit hello でブラウザにデモ画面が開けば、導入は完了です。インストールされた版は pip show chainlit で確認できます。
新しい仮想環境で「No module named ‘requests’」が出る原因
2026年9月28日時点、requestsが入っていない新しい仮想環境に2.12.0だけを入れると、chainlit コマンドが ModuleNotFoundError: No module named 'requests' で起動しないことがあります。原因はChainlit本体ではなく、依存の連鎖にあります。
- Chainlit 2.12.0は
literalai==0.1.201を固定で依存に持ち、literalaiはtraceloop-sdkを読み込みます。 - traceloop-sdk 0.62.3(2026年8月10日公開)は内部で
import requestsしていますが、requestsを依存として宣言していません。 - これまでrequestsを連れてきていた opentelemetry-exporter-otlp-proto-http は、1.45.0でrequestsを任意の追加依存(extra)に移しました。
対処は pip install requests の1行です。Dockerイメージのように毎回まっさらな環境を作る構成では、requirements.txtにrequestsを明記しておくと再発しません。
chainlit initで作られる設定ファイル
作業用フォルダで chainlit init を実行すると、.chainlit/config.toml と、画面の文言を収めた .chainlit/translations/(日本語の ja.json を含む23言語)が作られます。初回の chainlit run では、アプリ起動時に表示される説明文 chainlit.md と、アップロードファイルの置き場 .files/ も作られます。config.tomlの主な既定値は次のとおりです。
| キー | 既定値 | 意味 |
|---|---|---|
| [project] session_timeout | 3600 | 切断後にセッションを保つ秒数 |
| [project] allow_origins | [“*”] | 接続を許すオリジン |
| [features] unsafe_allow_html | false | メッセージ内のHTMLを描画するか |
| [features.spontaneous_file_upload] | enabled・20件・500MB | ファイル添付 |
| [features.mcp] enabled | false | MCPサーバー接続 |
| [UI] name | “Assistant” | 画面とタブの名前 |
| [UI] cot | “full” | 途中経過の表示量 |
Chainlitの使い方:最小アプリとストリーミング応答
次のコードは、会話開始時にあいさつを出し、メッセージを受けるとツール役の関数を呼んでから、応答を1トークンずつ流す最小構成です。LLMの代わりに固定の文字列を返す関数を使っているので、APIキーなしでそのまま動きます。
import asyncio
import chainlit as cl
async def fake_llm(prompt: str):
for token in ["「", prompt, "」を", "受け取りました。"]:
await asyncio.sleep(0.05)
yield token
@cl.on_chat_start
async def start():
cl.user_session.set("history", [])
await cl.Message(content="質問をどうぞ").send()
@cl.step(type="tool")
async def search(query: str) -> str:
return f"{query} の検索結果"
@cl.on_message
async def main(message: cl.Message):
history = cl.user_session.get("history")
history.append({"role": "user", "content": message.content})
await search(message.content)
msg = cl.Message(content="")
async for token in fake_llm(message.content):
await msg.stream_token(token)
await msg.send()
history.append({"role": "assistant", "content": msg.content})
app.py として保存し、chainlit run app.py -w で起動します。-w はファイルを保存するたびに自動で再読み込みするオプションです。このアプリを2.12.0で起動し、socket.ioクライアントから「Chainlitとは」と送ると、サーバーからは次の順にイベントが届きます。
on_messageの実行を表すrunステップsearchのtoolステップと、その戻り値「Chainlitとは の検索結果」stream_startと3回のstream_token- 完成したメッセージ「「Chainlitとは」を受け取りました。」
@cl.step を付けた関数は、呼ぶだけで入力と出力が折りたたみ表示されます。どこまで見せるかはconfig.tomlの cot で決まり、"full"(すべて)、"tool_call"(ツール呼び出しのみ)、"hidden"(非表示)から選べます。
主要なデコレーターと役割
Chainlitのアプリは、画面で起きた出来事ごとにデコレーターを付けた関数の集まりです。
| デコレーター | 呼ばれるタイミング |
|---|---|
| @cl.on_chat_start | 新しい会話の開始時 |
| @cl.on_message | ユーザーがメッセージを送ったとき |
| @cl.on_chat_resume | 履歴から会話を再開したとき |
| @cl.on_stop | 停止ボタンが押されたとき |
| @cl.on_chat_end | 会話が終わったとき |
| @cl.action_callback(“名前”) | ボタンが押されたとき |
| @cl.set_starters | 初期画面の質問例を返す |
| @cl.set_chat_profiles | モデルやペルソナの切り替え候補を返す |
cl.user_sessionで会話履歴を保持する方法
グローバル変数に会話履歴を置くと、同時に使っている全ユーザーで履歴が混ざります。ユーザーごとの状態は cl.user_session.set() と cl.user_session.get() で持ちます。上のコードでは on_chat_start で空のリストを作り、on_message で取り出して追記しています。user_sessionはサーバーのメモリ上にあるため、プロセスを再起動すると消えます。再起動後も履歴を残したい場合は、後述のデータレイヤーを設定します。
OpenAI APIとLangChainの連携コード
OpenAI Python SDKによるストリーミング応答
OpenAIの非同期クライアントで stream=True を指定し、届いた差分を stream_token へ渡します。APIキーは環境変数 OPENAI_API_KEY から読まれます。モデルIDは変わりやすいため、環境変数に出しておくと差し替えが楽です。
import os
import chainlit as cl
from openai import AsyncOpenAI
client = AsyncOpenAI()
MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.6")
@cl.on_chat_start
async def start():
cl.user_session.set("history", [{"role": "system", "content": "日本語で簡潔に答えてください。"}])
@cl.on_message
async def main(message: cl.Message):
history = cl.user_session.get("history")
history.append({"role": "user", "content": message.content})
msg = cl.Message(content="")
stream = await client.chat.completions.create(model=MODEL, messages=history, stream=True)
async for chunk in stream:
if chunk.choices and (token := chunk.choices[0].delta.content):
await msg.stream_token(token)
await msg.send()
history.append({"role": "assistant", "content": msg.content})
最後の差分や使用量だけを運ぶ差分では choices が空になったり content が None になったりするため、条件を付けてから渡しています。SDKの導入やResponses APIへの移行はOpenAI Python SDKの使い方の解説を参照してください。
LangChainの途中経過を画面に表示する方法
LangChainのチェーンやエージェントを使う場合は、cl.LangchainCallbackHandler をコールバックに渡します。これだけで、LLM呼び出しやツール実行がステップとして画面に並びます。次の例は pip install langchain langchain-openai で追加パッケージを入れ、環境変数 OPENAI_API_KEY を設定してから動かします。
import chainlit as cl
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableConfig
@cl.on_chat_start
async def start():
prompt = ChatPromptTemplate.from_messages(
[("system", "簡潔に答えてください"), ("human", "{question}")]
)
model = init_chat_model("openai:gpt-5.6")
cl.user_session.set("chain", prompt | model | StrOutputParser())
@cl.on_message
async def main(message: cl.Message):
chain = cl.user_session.get("chain")
msg = cl.Message(content="")
async for chunk in chain.astream(
{"question": message.content},
config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()]),
):
await msg.stream_token(chunk)
await msg.send()
Chainlit 2.12.0の連携モジュールは、langchain-core 0.2.5以上を要求します(2.11.1で、判定対象がlangchainからlangchain-coreに直りました)。langchain-core 1.6.5と、LangChain付属のテスト用チャットモデルに差し替えた同じ構成で動かすと、画面には「llm」種別のステップが1つ追加されます。init_chat_model や create_agent など、v1系での書き方はLangChain v1.0の解説にまとめています。
ChainlitのUIカスタマイズ:config.tomlとtheme.json
見た目は、コードではなく .chainlit/config.toml の [UI] セクションで変えます。
[UI]
name = "社内ナレッジ検索"
default_theme = "light"
layout = "wide"
cot = "tool_call"
custom_css = "/public/style.css"
logo_file_url = "/public/logo.png"
- 色とフォント:アプリのフォルダに
public/theme.jsonを置くと、起動時に読み込まれて画面のテーマ変数が差し替わります。 - 細かな見た目:
custom_cssにpublic/配下のCSSか外部URLを指定します。同様にcustom_jsでスクリプトも差し込めます。 - ヘッダーのリンク:
[[UI.header_links]]を並べると、ヘッダーに社内マニュアルなどへのリンクを置けます。 - 日本語表示:ブラウザの言語設定が日本語なら、ja.jsonの訳が自動で使われます。全員を日本語に固定するときは
language = "ja-JP"を書きます。ja-JP.jsonが無い場合は ja.json に落ちるため、訳が効きます。
CSSで画面の構造そのものを大きく変えると、版を上げたときにクラス名が変わって崩れることがあります。色・ロゴ・名前の変更はtheme.jsonとconfig.tomlで行い、CSSは最小限にとどめるのが保守の手間を抑えるコツです。
Chainlitの認証と会話履歴の永続化
既定のChainlitはログイン画面を持たず、URLを知っていれば誰でも使えます。社内で使う場合でも、認証は最初に入れるべき設定です。
パスワード認証とCHAINLIT_AUTH_SECRET
最も簡単なのは @cl.password_auth_callback です。ユーザー名とパスワードを受け取り、正しければ cl.User を、誤りなら None を返します。
from typing import Optional
import chainlit as cl
@cl.password_auth_callback
async def auth(username: str, password: str) -> Optional[cl.User]:
if (username, password) == ("admin", "admin"):
return cl.User(identifier="admin", metadata={"role": "admin"})
return None
認証を有効にするには、ログイン状態を示すJWTの署名鍵 CHAINLIT_AUTH_SECRET が必要です。未設定のまま起動すると、2.12.0は ValueError: You must provide a JWT secret in the environment to use authentication. で停止します。鍵は chainlit create-secret で生成し、.env に書きます。鍵を変えるとログイン中の全ユーザーがログアウトされます。
上のコードの /login に正しい組み合わせを送ると200と access_token のCookieが返り、誤りなら401が返ります。実運用では、固定の文字列比較ではなく、ハッシュ化したパスワードを保存したデータベースと照合してください。
OAuthとヘッダー認証の選択肢
社内のアカウントでログインさせるなら @cl.oauth_callback を使います。2.12.0が対応するプロバイダーはGitHub、Google、Azure AD(Microsoft Entra ID)、Azure ADハイブリッド、Okta、Auth0、Descope、Amazon Cognito、GitLab、Keycloakに加え、汎用OAuthプロバイダーを含む11種です。各プロバイダーが要求する環境変数を設定すると、ログイン画面にボタンが出ます。クライアントIDとシークレットに加え、Azure ADではテナントID、OktaやAuth0ではドメインなどの設定も必要です。リバースプロキシで認証を済ませる構成なら、リクエストヘッダーからユーザーを判定する @cl.header_auth_callback も使えます。
データレイヤーによる会話履歴の保存
履歴のサイドバーや、フィードバック(高評価・低評価)の保存を使うには、データレイヤーが必要です。2.12.0は次の順で保存先を決めます。
@cl.data_layerで自作のデータレイヤーを返していればそれを使います。- 環境変数
DATABASE_URLがあれば、PostgreSQLに保存する公式データレイヤーを使います。添付ファイルはBUCKET_NAMEなどの設定でS3・Google Cloud Storage・Azure Storageに置けます。 - 環境変数
LITERAL_API_KEYがあれば、Literal AIに保存します。DATABASE_URLと両方あるとDATABASE_URLが優先されます。
SQLAlchemyとDynamoDBのデータレイヤーも同梱されています。データベースを使う構成では、版を上げるときにテーブルの変更が要る場合があります。たとえば2.9.4では、ALTER TABLE steps ADD COLUMN IF NOT EXISTS modes JSONB; を実行しないと動かない破壊的変更が入りました。更新前にはリリースノートの「Breaking change」を必ず確認してください。
Chainlitの本番デプロイ:ホスト・WebSocket・CORSの設定
chainlit run はそのままWebサーバーになります。コンテナやPaaSに載せるときの要点は次のとおりです。
chainlit run app.py -h --host 0.0.0.0 --port 8000
- -h(–headless):起動時にブラウザを開かないオプションです。公式ドキュメントは、本番では付けるよう求めています。
- –host:既定は
127.0.0.1で、コンテナの外から届きません。Dockerでは0.0.0.0を指定します。環境変数CHAINLIT_HOST・CHAINLIT_PORTでも指定できます。 - –root-path:
/chatのようなサブパスで公開するときに指定します。 - WebSocketとスティッキーセッション:公式ドキュメントは、自動スケールするならスティッキーセッションを有効にするよう求めています。ロードバランサーで振り分けが安定しない場合は、
[project]にtransports = ["websocket"]を書いてWebSocketだけで接続させます。 - allow_origins:既定は
["*"]で、どのサイトからでも接続できます。他のサイトに埋め込まないなら自社のドメインだけに絞ります。
Chainlitの脆弱性と対応バージョン(2026年9月時点)
NVDに登録されたChainlitの脆弱性を、修正版とともにまとめます。公表日はNVDの掲載日時(UTC)、スコアはCVSS v3.1(無いものはv4.0を併記)です。CVE番号からNVDの詳細ページを開けます。
| CVE | 公表日 | CVSS v3.1 | 内容 | 修正版 |
|---|---|---|---|---|
| CVE-2025-68492 | 2026-01-14 | (v4.0で2.3) | 他人の会話を閲覧・乗っ取り | 2.8.5 |
| CVE-2026-22218 | 2026-01-20 | 6.5 | 任意ファイルの読み出し | 2.9.4 |
| CVE-2026-22219 | 2026-01-20 | 7.7 | SQLAlchemy構成でのSSRF | 2.9.4 |
| CVE-2026-56104 | 2026-06-22 | 8.2 | WebSocket復元でのセッション乗っ取り | 2.10.1 |
| CVE-2026-45018 | 2026-08-25 | 9.8 | MCP経由のコマンド実行 | 2.12.0 |
| CVE-2026-45019 | 2026-08-25 | 7.2 | MCP経由のSSRF | 2.12.0 |
| CVE-2026-82290 | 2026-08-28 | 5.3 | 他人のフィードバックを改変・削除 | 未修正 |
| CVE-2026-86099 | 2026-09-09 | 8.2 | sessionIdによるディレクトリ削除 | 未修正 |
2.12.0で直ったCVE-2026-45018は、features.mcp.enabled = true にしている場合に、認証なしの攻撃者がサーバー上でコマンドを実行できるものでした。影響範囲は2.4.0rc0から2.11.xで、MCPは2.7.0以降既定で無効のため、有効にしていなければ影響を受けません。CVE-2025-68492の詳細はChainlitのアクセス制限不備の脆弱性(CVE-2025-68492)の解説にあります。
注意すべきは表の下2件です。NVDはどちらも「2.12.0まで」を影響範囲としており、2026年9月28日時点で修正版はPyPIに出ていません。2件は攻撃の条件が異なります。
- CVE-2026-86099:socket.io接続時にクライアントが送る
sessionIdを検証しないため、細工した値で.filesの外のディレクトリを削除させられます。2.12.0のソースでも、セッション終了時に.filesとsessionIdを連結したパスをshutil.rmtreeで消しています。認証を設定していない構成では、未認証の攻撃者が接続できます。認証を設定した構成では接続時に認証されますが、ログインできる利用者による悪用は残ります。 - CVE-2026-82290:フィードバックの更新・削除API(
PUT /feedback・DELETE /feedback)が所有者を確認しないため、ログインした利用者が他人の評価を書き換えられます。認証を入れても防げません。
修正版が出るまでは次の対策を取ってください。
- インターネットに直接公開せず、社内ネットワークやVPN、認証付きのリバースプロキシの後ろに置きます。
- Chainlitのプロセスを、アプリのフォルダ以外に書き込めない専用ユーザーやコンテナで動かし、消されて困るディレクトリへの権限を持たせません。
- フィードバックを評価データとして使うなら、リバースプロキシで
/feedbackへのPUT・DELETEを止め、評価の変更を画面から受け付けないようにします。 - GitHubのReleasesとNVDを定期的に確認し、修正版が出たらすぐ更新します。
2.12.0で必要なMCP設定の書き換え
MCPを有効にしている場合、2.12.0へ上げると旧形式の設定では起動しません。[features.mcp.stdio]、[features.mcp.sse]、[features.mcp.streamable-http]、allowed_executables のいずれかが残っていると起動が中止されます。新しい形式では、使ってよいサーバーを開発者が名前付きで宣言し、画面側は名前だけを送ります。
[features.mcp]
enabled = true
[[features.mcp.servers]]
name = "github"
type = "stdio"
command = "npx -y @modelcontextprotocol/server-github"
[features.mcp.user_servers]
enabled = true
allowed_urls = ["https://mcp.example.com"]
各サーバーには type が必須になり、ユーザーが自分でSSE・HTTPのサーバーをつなぐ機能は user_servers を明示的に有効にしたときだけ使えます。リダイレクトも追わなくなったため、URLは最終的な https:// のアドレスを書きます。MCPそのものの仕組みはMCP(Model Context Protocol)の解説を参照してください。
よくある質問
ChainlitはどのPythonバージョンで使えますか?
2.12.0はPython 3.10以上3.14未満に対応しています。3.9以前と3.14では2.12.0が入らないため、3.12か3.13の仮想環境を用意するのが確実です。
Chainlitの最新バージョンはいくつですか?
2026年9月28日時点の最新版は、2026年8月25日公開の2.12.0です。PyPIのchainlitのページかGitHubのReleasesで確認でき、手元の版は pip show chainlit で分かります。2.12.0でも未修正の脆弱性が2件あるため、次の版が出たら早めに更新してください。
ChainlitはFastAPIのアプリに組み込めますか?
組み込めます。from chainlit.utils import mount_chainlit を読み込み、mount_chainlit(app=app, target="cl_app.py", path="/chainlit") のように呼ぶと、既存のFastAPIアプリの /chainlit 配下で会話画面が動きます。FastAPI自体の基本はFastAPIの解説にまとめています。
Chainlitは商用利用できますか?
できます。ライセンスはApache-2.0で、社内システムや顧客向けサービスに組み込んで使えます。ただし開発元のChainlit SASは今後の更新を保証していないため、脆弱性の修正を待てない案件では自社で修正を当てる体制も検討してください。
Chainlitは開発終了したのですか?
終了していません。2025年5月1日からコミュニティのメンテナーが保守を引き継ぎ、2.12.0までリリースが続いています。会話ログの記録と分析を別の仕組みに任せたい場合は、Langfuseの解説も参考になります。