Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

TLSを手で動かすハンズオンを作った話

こんにちは!ITIの吉葉です!

今回はTLSの理解を深める資料として、TLS通信の基本フローを手で動かすハンズオン資料を作ったので紹介します。

ColabNotebook配布

今回作成したColabNotebookは以下で共有しています。 よければコピーしてお使いください!

ハンズオンのスクリーンショット:メッセージを受け取ったところ ハンズオンのスクリーンショット:メッセージの改ざんを検証するところ

詳しくは弊社リポジトリをご参照ください github.com

なぜ作ろうと思ったか

私の所属するAIチームでは去年の秋頃から自己研鑽の一環として輪読会をしています。
その種本として結城浩著の『暗号技術入門』を選びました。

『暗号技術入門』の終盤に出てきたのがSSL/TLSでした。
SSL/TLSというと、HTTPSという形でブラウザで見ることが多いんじゃないかなと思います。
HTTPSはHTTPとTLSを合わせた技術で、通信相手の真正性、通信内容の機密性、データの完全性を保証してくれます。

さて、このSSL/TLS、かなり複雑な技術です。 対称鍵、公開鍵、署名、乱数と今まで習った全てを活用します。 習った全てがこれに収束するんだ!!というカタルシスはありますが、複雑は複雑。 正直、何をやっているのか理解しづらい面も多々あるかなと感じました。

そこで、今回はTLSは実際にはどんなことをやっているのか、そして本当に安全な通信ができるのか体験できるハンズオン資料を作成しました。

事前知識:SSL/TLSってなあに?

本題に入る前にSSL/TLSについて軽く説明しましょう。 SSL/TLSは両方ともインターネット上の通信を暗号化するためのプロトコル(取り決め)です。ざっくり簡単に言うとSSLが古くて、TLSが新しい規格ですね。 SSL/TLSの神髄は、「正しい相手に」「安全に」情報が送れるところにあります。例えば通販サイトになりすましたサーバーがあったとしても、SSL/TLSを使えばなりすましを見抜くことができますし、正規のサイトとの通信経路が盗聴されていても情報は暗号化されているのでクレカ情報などは盗まれません。 SSL/TLSによりクレジットカード情報や個人情報などセンシティブな情報を安全に、正しい相手に送ることができるわけです。

コンセプト:Discordを「ネットワーク」に見立てる

SSL/TLSの章を読んだ時、以下のことを思いました。

  • 各ステップが複雑で結局何が起きてるかわからない
  • 今何のために何をしているかわからない
  • これで本当に安全なやり取りができてる実感がない

そこで考えたのが次のコンセプトです。

  • ステップバイステップでコマンドを実行する
  • やりとりのパケットを衆目に晒してなお解読できないことを体験させる

これを以下の形で具体化しました。

  • GoogleColabを使い、コマンドを手入力
    • 各ステップでどんな処理を行うのか意識できる
  • クライアント役とサーバー役の2人が、Discordのチャンネルで「パケット」をやりとりする
    • 観客は全員そのチャンネルを見ている
    • つまり全パケットを「傍受」できる状態

目指すは「各ステップにどんな暗号技術が必要か」、「全部見えてるのに読めない」というのが実感できる体験です。

Discordのスクリーンショット。Client HelloとServer Hello相当のやり取りが実施されている
パケット情報は筒抜け

何を簡略化して、何を残したか

TLSをそのまま再現しようとすると、途端に難しくなります。PRF(疑似乱数関数)、PKCS#1パディング、レコード層のフォーマット……細部を全部再現すれば正確ですが、体験の妨げになります。

判断基準は「エッセンスが伝わるか」の一つにしました。

残したもの

要素 理由
RSA暗号化(openssl pkeyutl) 「公開鍵で暗号化、秘密鍵で復号」はTLSの核心
証明書検証(openssl verify) CAチェーンの概念を体験させたい
AES-128-CBC 実際の暗号化を体感させる
HMAC-SHA256(MAC値) 改ざん検知の概念を伝える

簡略化したもの

PRF → SHA256の繰り返し

本物のTLS 1.2では、鍵導出にP_SHA256というHMACベースの関数を使います。これを実装すると28行のPythonスクリプトが必要になります。しかし、本質は「同じ素材から同じランダムな値を作る」ことなので、SHA256を数回繰り返す形に置き換えました。

base = bytes.fromhex(pms + client_random + server_random)
h1 = hashlib.sha256(base + b"ms1").digest()
h2 = hashlib.sha256(base + b"ms2").digest()
master_secret = (h1 + h2)[:48]

特に鍵生成を

client_write_MAC = hashlib.sha256(ms_base + b"client_write_MAC").digest()[:32].hex()

のように鍵の名前をseedにすることで今何の鍵を作っているのか分かりやすくしました。

余談:ついでに実際のTLS1.2では

# 擬似乱数関数(PRF)にマスターシークレット、
# "key expansion"、サーバーランダムクライアントランダムを渡す
km=PRF(MS, "key expansion", ServerRandom + ClientRandom)
client_write_MAC = km[:32].hex()

のように一括で鍵を計算します。 また、疑似乱数の中では、MasterSecretを鍵としたハッシュ値を計算するため、HMACが用いられます。

バイナリフォーマット → テキスト

実際のTLSレコード層は複雑なバイナリフォーマットを持ちますが、今回はApplication Dataを

平文: ランチは12時に渋谷で
MAC値: a3f9c2...

というテキスト形式にしました。復号結果がそのまま読めるので、「暗号文が平文に戻った」という体験が分かりやすくなります。

Finishedメッセージ → hs_hashの暗号化

本物のFinishedはPRFで計算したverify_dataを送りますが、今回はDiscordのハンドシェイクログをSHA256でハッシュ化したものをそのままセッション鍵で暗号化する形にしました。
「今までのやりとりをまとめてセッション鍵で封印する」という意味は保たれています。

3役に分離したCA設計

最初はサーバー役が自分でCA証明書も作っていましたが、CA(認証局)とをサーバーが兼任するのは不要な複雑さを産むことに気づきました。本物のTLSではブラウザにルートCA証明書があらかじめ組み込まれています。これを再現するために、主催者がCA役を担う設計にしました。

【事前】主催者(CA役)→ ca.crt を全員に配布
        ↓(ブラウザの組み込みルートCA証明書ストアに相当)
【当日】サーバー役 → server.crt をDiscordに投稿
       クライアント役 → openssl verify -CAfile ca.crt server.crt

server.crt: OK と表示された瞬間が、ブラウザの錠前マークに相当します。

Claudeと壁打ちしながら作った

このハンズオンはClaudeとの会話を通じて開発しました。最初は「openssl コマンドを叩いてTLSを再現するハンズオン」というざっくりした要件から始まり、形式が何度か変わっています。

最初はシェルスクリプトで「サーバー役スクリプト」「クライアント役スクリプト」を作っていました。対話形式で進行を自動化するイメージです。でも「各手順のコマンドさえ分かればいい」という方向転換で、HTMLのコマンド早見表に変わりました。

しかし、自分で試していくうちに取り扱うファイルが多く、ややこしいと感じたこと、そして(輪読会の)資料よりもずっと複雑だと感じたことから、0からGoogleColab上にハンズオン資料を手作業で制作しました。
制作物をClaudeに共有し、既存のノートブックのバグ(変数名のtypo、グローバル変数が効かない問題、ca_crtserver.crtに書き出していたミス)を指摘しつつクライアント版を作ってもらう、という流れで完成しました。

やってみて気づいたこと

面白かったところ

作りながら一番面白いと思ったのは、今まで習った暗号技術の総復習ができる点です。

TLSは暗号技術の総合芸術です。
例えば、メッセージの復号には共通する対称鍵を使います。そして、対称鍵を求めるには秘密の値を安全に運ぶ必要があります。このために公開鍵暗号を使用しますが、自分の受け入れた公開鍵が本当に正しいものかは分かりません。そこで、CAが公開鍵(とメタ情報)をハッシュ化して公開鍵にくっつけた署名を検証します。 この流れで

  • 共通鍵暗号
  • 公開鍵暗号
  • 署名
  • ハッシュ化

といった技術が登場します。これらは全て『暗号技術入門』で習ったものであり、今回のハンズオンを作りながら「ああ、こんなことやったな」と思い出していました。

改善点

鍵生成のロジックを手書きさせるのが冗長かもしれない

共通鍵を生成する部分は例示以外、自分でコードを補完させるような作りにしました。めんどうですが、集中力を強制的に投じてもらえるので、「ああ、鍵ごとにseedと長さが違うのか」など気付いてもらえるかな、と。
実際、それは功を奏しましたが、一方で先述のような理解をしたあとのタスクは徒労感があるのも事実です。
単純にseedと読み込みバイト数を書き換えるだけで楽しくない作業になってしまうので、気付きと手軽さをどう両立すればいいかなと考えています。

コマンド打ち込みが結局コピペになりがち

全体的に見られた傾向です。今回のハンズオンではコードセルにはほとんどコマンドを入れていません。例示したコメントを打ち込むことで、集中力を割いて欲しかったからです。 しかし、実際はコピペ実行が多く、今自分が何をやっているかを意識させるには至らなかったかなと思います。

変数の取り扱い

相手から送られてきたランダムやBase64などはColabのフォーム機能で変数に格納するようになっています。
これは「相手からの値を保存したぞ!」感を出すためです。
逆に実際にコマンドを叩くときには、「この値を使うぞ!」感のために値をそのまんま貼り付けるようにしました。つまり、変数を使わない選択をしました。
ただ、今振り返ると、保存した変数を使った方が「あの時の値を使うぞ!」感が出る上、手戻りもないのでいいんじゃないかと考えています。

最後に

今回はTLSのざっくりとした流れを見るためのハンズオンとその工夫について紹介しました。
再度リンクを掲載しておくので、ぜひ遊んでみてください!

今日の天気と予定を毎朝Slackに届けるbotをAWS + Claudeで作った話


こんにちは、2025年度入社の山田です。

「今日の天気どうだっけ」「今日は何時から打ち合わせだっけ」と毎朝確認するのって、地味に面倒ですよね。 そこで、毎朝7時に天気・Googleカレンダーの予定をまとめて個人のSlackに届けるbotをAWSとClaudeで作ってみました。

※ 記事中で登場するAWSサービスやAPIの詳細な説明は省略します。各公式ドキュメントをご参照ください。



完成イメージ

毎朝7時に個人のSlackのチャンネルにこんなメッセージが届くことを目標にしました:

morning-botがSlackに送信したメッセージの例

天気と予定を踏まえたコメントをClaudeが自動生成してくれるといった感じです。


システム構成

EventBridgeで毎朝7時に定期実行し、処理の本体はLambdaが担います。LambdaからOpen-MeteoやGoogle Calendar APIを呼び出して情報を集め、ClaudeにSlack通知メッセージを生成させ、最後にSlackのIncoming Webhookで通知します。ClaudeはAWSのAmazon Bedrockを通じて利用しています。

EventBridge(毎朝7時)
    ↓
Lambda
    ├── Open-Meteo API(天気取得・無料・APIキー不要)
    ├── Google Calendar API(予定取得)
    ├── Secrets Manager(Google認証情報)
    └── Amazon Bedrock / Claude Haiku 4.5(メッセージ生成)
    ↓
Slack Incoming Webhook

すべてサーバーレスで行うため、常時起動するサーバーは不要です。


コード概要

天気の取得:get_weather

Open-Meteo API(無料・APIキー不要)を使って、指定した緯度・経度の当日の天気予報を取得します。

def get_weather():
    url = (
        f"https://api.open-meteo.com/v1/forecast"
        f"?latitude={LATITUDE}&longitude={LONGITUDE}"
        f"&daily=weathercode,temperature_2m_max,temperature_2m_min"
        f"&timezone=Asia%2FTokyo&forecast_days=1"
    )
    with urllib.request.urlopen(url) as res:
        data = json.loads(res.read())
    daily = data["daily"]
    return {
        "max_temp": daily["temperature_2m_max"][0],
        "min_temp": daily["temperature_2m_min"][0],
        "weathercode": daily["weathercode"][0],
    }

Googleカレンダーの予定取得:get_today_events

Secrets Managerからサービスアカウントの認証情報を取り出し、Google Calendar APIで当日の予定一覧を取得します。

def get_google_credentials():
    client = boto3.client("secretsmanager", region_name="ap-northeast-1")
    secret = client.get_secret_value(SecretId=SECRET_NAME)
    info = json.loads(secret["SecretString"])
    return service_account.Credentials.from_service_account_info(
        info,
        scopes=["https://www.googleapis.com/auth/calendar.readonly"]
    )

def get_today_events():
    creds = get_google_credentials()
    service = build("calendar", "v3", credentials=creds)
    jst = timezone(timedelta(hours=9))
    now = datetime.now(jst)
    time_min = now.replace(hour=0, minute=0, second=0).isoformat()
    time_max = now.replace(hour=23, minute=59, second=59).isoformat()
    events = service.events().list(
        calendarId=CALENDAR_ID,
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy="startTime"
    ).execute()
    items = events.get("items", [])
    if not items:
        return "今日の予定はありません。"
    lines = []
    for e in items:
        start = e["start"].get("dateTime", e["start"].get("date", ""))
        if "T" in start:
            time_str = datetime.fromisoformat(start).strftime("%H:%M")
        else:
            time_str = "終日"
        lines.append(f"・{time_str} {e.get('summary', '(タイトルなし)')}")
    return "\n".join(lines)

Claudeによるメッセージ生成:summarize_with_bedrock

ClaudeはAmazon BedrockのAPIで利用しています。天気と予定の情報をプロンプトに渡して、朝のSlack通知メッセージを生成させます。

def summarize_with_bedrock(weather, events):
    client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
    prompt = f"""
今日の情報をもとに、朝のSlack通知メッセージを日本語で作成してください。

【天気】
- 最高気温: {weather['max_temp']}℃
- 最低気温: {weather['min_temp']}℃
- 天気コード: {weather['weathercode']}(0=快晴、1-3=晴れ〜曇り、61-67=雨、71-77=雪)

【今日の予定】
{events}

予定と天気を踏まえた一言コメントも添えて、150文字以内でまとめてください。
"""
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 300,
        "messages": [{"role": "user", "content": prompt}]
    })
    response = client.invoke_model(
        modelId="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
        body=body
    )
    result = json.loads(response["body"].read())
    return result["content"][0]["text"]

Lambda全体のコード

最終的なLambdaのコードはこちらです。

import json
import os
import urllib.request
from datetime import datetime, timezone, timedelta

import boto3
from google.oauth2 import service_account
from googleapiclient.discovery import build

SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
# 以下の座標はサンプルとして川崎市を設定している
LATITUDE = float(os.environ.get("LATITUDE", "35.5"))
LONGITUDE = float(os.environ.get("LONGITUDE", "139.7"))
CALENDAR_ID = os.environ["CALENDAR_ID"]
SECRET_NAME = os.environ["SECRET_NAME"]

def get_google_credentials():
    client = boto3.client("secretsmanager", region_name="ap-northeast-1")
    secret = client.get_secret_value(SecretId=SECRET_NAME)
    info = json.loads(secret["SecretString"])
    return service_account.Credentials.from_service_account_info(
        info,
        scopes=["https://www.googleapis.com/auth/calendar.readonly"]
    )

def get_today_events():
    creds = get_google_credentials()
    service = build("calendar", "v3", credentials=creds)
    jst = timezone(timedelta(hours=9))
    now = datetime.now(jst)
    time_min = now.replace(hour=0, minute=0, second=0).isoformat()
    time_max = now.replace(hour=23, minute=59, second=59).isoformat()
    events = service.events().list(
        calendarId=CALENDAR_ID,
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy="startTime"
    ).execute()
    items = events.get("items", [])
    if not items:
        return "今日の予定はありません。"
    lines = []
    for e in items:
        start = e["start"].get("dateTime", e["start"].get("date", ""))
        if "T" in start:
            time_str = datetime.fromisoformat(start).strftime("%H:%M")
        else:
            time_str = "終日"
        lines.append(f"・{time_str} {e.get('summary', '(タイトルなし)')}")
    return "\n".join(lines)

def get_weather():
    url = (
        f"https://api.open-meteo.com/v1/forecast"
        f"?latitude={LATITUDE}&longitude={LONGITUDE}"
        f"&daily=weathercode,temperature_2m_max,temperature_2m_min"
        f"&timezone=Asia%2FTokyo&forecast_days=1"
    )
    with urllib.request.urlopen(url) as res:
        data = json.loads(res.read())
    daily = data["daily"]
    return {
        "max_temp": daily["temperature_2m_max"][0],
        "min_temp": daily["temperature_2m_min"][0],
        "weathercode": daily["weathercode"][0],
    }

def summarize_with_bedrock(weather, events):
    client = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
    prompt = f"""
今日の情報をもとに、朝のSlack通知メッセージを日本語で作成してください。

【天気】
- 最高気温: {weather['max_temp']}℃
- 最低気温: {weather['min_temp']}℃
- 天気コード: {weather['weathercode']}(0=快晴、1-3=晴れ〜曇り、61-67=雨、71-77=雪)

【今日の予定】
{events}

予定と天気を踏まえた一言コメントも添えて、150文字以内でまとめてください。
"""
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 300,
        "messages": [{"role": "user", "content": prompt}]
    })
    response = client.invoke_model(
        modelId="jp.anthropic.claude-haiku-4-5-20251001-v1:0",
        body=body
    )
    result = json.loads(response["body"].read())
    return result["content"][0]["text"]

# 指定したSlackのUrLに送信する
def send_to_slack(message):
    payload = json.dumps({"text": message}).encode("utf-8")
    req = urllib.request.Request(
        SLACK_WEBHOOK_URL,
        data=payload,
        headers={"Content-Type": "application/json"}
    )
    urllib.request.urlopen(req)

def lambda_handler(event, context):
    weather = get_weather()
    events = get_today_events()
    message = summarize_with_bedrock(weather, events)
    send_to_slack(message)
    return {"statusCode": 200, "body": "OK"}

セットアップ手順

まずはGoogle Calendar APIの準備から始めましょう。認証情報が揃ったらコードをパッケージ化してLambdaにデプロイし、最後にEventBridgeで毎朝自動実行させれば完成です。(文量の都合上、Slackの設定を割愛しています。詳細は公式ドキュメントをご覧ください。)

1. Google Calendar APIの設定

はじめに、Google Cloud側でカレンダーへのアクセス権限を用意します。

  1. Google Cloud ConsoleでGoogle Calendar APIを有効化 Google Cloud ConsoleのAPIライブラリでGoogle Calendar APIを選択する

  2. サービスアカウントを作成してJSONキーをダウンロード サービスアカウントの秘密鍵をJSON形式で作成するダイアログ

  3. 対象のGoogleカレンダーの「設定と共有」→ サービスアカウントのメールアドレスを追加(閲覧権限)

Googleカレンダーの共有設定でサービスアカウントを追加する

  1. JSONキーの中身をAWS Secrets Managerに保存 Secrets Managerでサービスアカウントキー(JSON)を登録する

Secrets Managerには、AWSコンソールで「新しいシークレットを保存する」→「その他のシークレットの種類」を選択し、JSONの中身をそのまま貼り付けて保存します。保存後に表示されるシークレット名をメモしておきましょう。(画像のJSONキーはサンプルとして作成したもので既に削除済みです)

2. デプロイパッケージの作成

次に、LambdaにアップロードするZIPファイルを作成します。Google Calendar APIの呼び出しに使っている google-api-python-client をLambdaに載せようとしたところ、少し注意が必要でした。このライブラリはRust製バイナリを含むため、Macで直接インストールしたものはLambdaで動かないのです。Dockerで明示的にLinux向けにビルドする必要があります。

docker run --rm \
  -v $(pwd):/var/task \
  --entrypoint pip \
  public.ecr.aws/lambda/python:3.14 \
  install google-auth google-auth-httplib2 google-api-python-client \
  --platform manylinux_2_28_x86_64 \
  --implementation cp \
  --only-binary=:all: \
  -t /var/task/package/

cp lambda_function.py package/
cd package && zip -r ../lambda-package.zip . && cd ..

3. Lambdaの作成とデプロイ

ZIPが用意できたら、AWSコンソールでLambda関数を作成してアップロードします。

設定値は以下のとおりです。

  • ランタイム: Python 3.14 LambdaでPython 3.14のランタイムを選択して関数を作成する

  • タイムアウト: 30秒(デフォルト3秒だとBedrockの応答が間に合わない)

Lambdaの基本設定でタイムアウトを30秒に設定する

  • IAMロール: AmazonBedrockFullAccess / SecretsManagerReadWrite をアタッチ IAMロールにAmazonBedrockFullAccessとSecretsManagerReadWriteをアタッチした状態

環境変数は「設定」→「環境変数」から設定します。

Lambdaの環境変数に5つのキーを設定した画面

キー
SLACK_WEBHOOK_URL Slack Incoming Webhook URL
CALENDAR_ID GoogleカレンダーのカレンダーID
SECRET_NAME Secrets Managerのシークレット名
LATITUDE 緯度
LONGITUDE 経度

このzipをコードとして直接アップロードします(レイヤーは不要)。 ※レイヤーとしてアップロードを試したが、なぜかうまくいかなかったので代替として上記の方法でアップロードを行った。

4. EventBridgeで定期実行を設定

最後に、毎朝7時に自動実行されるよう EventBridge のスケジュールを設定します。

LambdaにEventBridgeトリガーを追加してcronスケジュールを設定する

EventBridgeはUTC基準のため、日本時間7時はUTC前日22時になります。

cron(0 22 * * ? *)

これで設定完了です。翌朝7時に通知が届けば成功です!


実際に使ってみた

実際に当日の天気と予定を簡潔にチャットしてくれています。

服装のポイントも教えてくれるのが地味に助かりますね。


トラブルシューティング

① タイムアウト

デフォルト3秒ではBedrockの応答が間に合わずタイムアウトする。30秒に変更で解決。

② Bedrockのモデルエラー

Invocation of model ID ... with on-demand throughput isn't supported.
Retry your request with the ID or ARN of an inference profile

新しいモデルはモデルIDの直接指定ではなく推論プロファイル経由でしか呼び出せない。使えるIDは以下で確認できる:

aws bedrock list-inference-profiles --region ap-northeast-1 | grep haiku

東京リージョンのHaiku 4.5は jp.anthropic.claude-haiku-4-5-20251001-v1:0 が正しい。

③ 実行バイナリ互換性エラー

/opt/python/cryptography/hazmat/bindings/_rust.abi3.so: invalid ELF header

Mac(ARM64)でビルドしたバイナリをそのままLambda (Linux x86_64)へデプロイすると、OS及びCPUアーキテクチャの違いによりバイナリ互換性エラーが発生する。--platform manylinux_2_28_x86_64 を指定してLinux/x86_64向けのパッケージを取得し、さらにLinuxコンテナ(Docker)内でビルドすることで、Lambdaで実行可能なバイナリを生成できる。ネイティブバイナリを含むライブラリをLambdaで利用する場合は、このようにLambdaの実行環境に合わせてビルドする必要がある。


まとめ

毎朝7時に天気と予定がまとまって個人のSlackに届くのは思った以上に便利でした。しかも、サーバーレス構成のおかげで月額コストは ほぼ無料 で実現できています。

まだまだ追加できる機能や連携できるAWSのサービスはあると思うので継続的にブラッシュアップを行っていきたいです。

ローカルで動く日本語LLM「LFM2.5」を試してみた

こんにちは。吉葉です!

数日前、「LFM2.5」というモバイルデバイス向けの軽量LLMがリリースされました。なんとスマートフォンでも動くほど軽量らしいです。
今回はMacBook Airで「LFM2.5」を試してみたので、そのセットアップ過程と所感を共有します。

www.liquid.ai

今回のゴール

実際にチャットしてみたいので、Open-WebUI(ChatGPTみたいなUIでローカルLLMを触れるツール)でLFM2.5を動かすことをゴールにしたいと思います。

マシンスペック

  • MacBook Air M2
  • CPU: Apple M2チップ 8core
  • GPU: 8core
  • RAM: 16GB

Step 1: Python での初期実装

まずはPythonで動かしてみます。

from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer

model_id = "LiquidAI/LFM2.5-1.2B-JP"
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    dtype="bfloat16",  
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

prompt = "日本の首都はどこですか?"

messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
    messages,
    add_generation_prompt=True,
    tokenize=False,  
)
input_ids = tokenizer(text, return_tensors="pt").input_ids.to(
    model.device
)  

output = model.generate(
    input_ids,
    do_sample=True,
    temperature=0.3,
    min_p=0.15,
    repetition_penalty=1.05,
    max_new_tokens=512,
    streamer=streamer,
)

問題なく動作しました!

スクリーンショット: LLMが日本の首都を答えている 日本の首都を答えられています!

Step 2: Ollamaで実行

次は ollama というツールを使ってみました。 ollamaはローカルLLMを実行するためのツールです。

Pythonで動かすのとは違い、OpenAI互換のAPIを提供してくれるので、ollamaを挟むだけでいろんなAIツールを動かすことができます。

ollama.com

ollamaのインストールと起動

brew install ollama
ollama serve

ollama では GGUF(量子化されたモデル)が必要です。

LFM2.5 は LFM2.5-1.2B-JP-GGUF として公開されているので、以下のコマンドを実行:

ollama run hf.co/LiquidAI/LFM2.5-1.2B-JP-GGUF

動きました!日本の首相について聞いてみましょう。 ollamaのCLI画面。日本の首相を聞かれたLFM2.5が岸田文雄(いらすとかずと)と答えている

日本の首相は岸田文雄(いらすとかずと)らしいです。





………OK!!!!!!!!!!
ハルシネーションしまくりですが、1.2Bのモデルとしては上出来でしょう




Step 3: Open-WebUI で UI を整備

最後に「Open-WebUI」を使ってChatGPTみたいなUIで使ってみましょう。

docs.openwebui.com

uvx open-webui serve

無事使えました。ただし、質問への回答は「それっぽい」レベルで、精度としてはまだまだな印象。 Open-WebUI のスクリーンショット。支離滅裂な回答が並ぶ 話を作ったり、コードを生成したりと言った高度なタスクは難しそうですね。

要約などのタスクはどうでしょう?弊社HPで公開されている代表挨拶を入れてみましょう。 ITI代表挨拶の要約 おお!かなりちゃんと要約されています!

おわりに

ローカルで日本語LLMが動くようになると、プライバシーを気にせず実験できるのが良いですね。今後は他のLLMも試したり、ローカルLLMをつかったツールなども作ってみたいです。

Claude Code のナレッジベースを Obsidian で構築してみた

こんにちは、臼居です。

最近、Claude Code を使った個人開発に取り組んでいるのですが、一つ不便に感じていることがありました。

会話セッションをまたいだときに、それまでの会話記憶がなくなってしまうことです。

毎回「自分は今、こんなものを作っていて……」と説明し直すのが地味にストレスでした。また、会話の中で出てきた技術的な気づきや決定事項も、セッションが終わると消えてしまいます。

Claude にも公式のメモリ機能はありますが、「Claude が勝手に覚える」ものであり、自分で構造化・編集・検索できるナレッジベースとは別物です。古い情報は薄れていきますし、何が記憶されているかも分かりづらいです。

そこで、Obsidian をナレッジベースとして使い、Claude Code と連携させてみました。この記事ではその構成と実装を紹介します。

なお、この記事では Claude Code 自体の使い方や、Claude Code のスキル機能の詳細については説明していません。Claude Code 公式ドキュメント をあわせてご参照ください。

続きを読む

Replit Agent 3 を使って、Vibe Coding ですごろくアプリを作らせてみた

こんにちは、2025年度入社の臼居です。

今回は、「自分では全くコードを書かずに Web アプリを作る」という前提で、Replit Agent 3 を使った Vibe Coding を試してみました。

自然言語で要件だけを伝え、以降の実装やテストをできるだけ Agent に任せるというスタイルが、どの程度実用になるのかを、すごろくアプリを題材に検証していきます。

続きを読む

Firebase Studio vs Gemini Code Assist でつくる「ため池マップ」

こんにちは、中山です

近年、生成AI がコードを書く時代が本格的に到来しつつあります
といいますか、そろそろ、AI の進歩の早さに置いていかれそうな感もあります

OpenAI 社や、Claude の Anthropic 社などと同様に、Googleもまた、開発者向けツールとして「Firebase Studio」や「Gemini Code Assist」といった、なんかすごそうなサービスをどんどん投入してきています

今回の記事では、これらの Google 製ツールに、日本の行政機関が公開している「オープンデータ」を用いたシンプルな地図アプリを作らせてみて、その「生成されるコード」「ユーザー体験」などを比較してみようと思います

続きを読む

Google Gemini と Microsoft Copilot を比較する

こんにちは、中山です

進歩の早い生成 AI 界隈ですが、今春も OpenAI 社から、GPT-4o などの発表があったり、Google I/O 2024 では、Gemini のいろいろな進化が紹介されていました

私たちソフトウェアエンジニアとしましては、これらの最新の生成AIに「使われる」のではなく、「使いこなす」側でありつづけないといけません!

このような意気込みのもと、今回は、 Google の Gemini にトライしてみまして、 前回 の Microsoft Copilot とも比較しながら、その特徴を掴んでみたいと思います

続きを読む