DeepSeekというのは、2025年になって突然登場したオープンソースのLLMです。安価なGPUを使いつつも、ChatGPTなどに匹敵すると言われるほどの性能を持っています。
このDeepSeekはオープンソースなので、誰でも自由に利用できます。そして、最も簡単に利用するならば、Ollamaを使ってAPI化するのが良いでしょう。
今回は、DeepSeekを使ったチャットアプリの作り方を解説します。
ベースについて
今回はベースとしてNCMBMania/monaca-chatgptを利用しています。これはチャットアプリですが、内部的にはChatGPTを使っています。
技術的には以下を利用しています。
- Framework7
UIはFramework7のMessagesを使っています。これは、チャットUIを簡単に作成できるUIコンポーネントです。
準備
まず、Ollamaを立ち上げます。今回はDockerを使っています。
docker run -d \
--gpus=all -v ollama:/root/.ollama \
-p 11434:11434 --name ollama \
ollama/ollama
これで、Ollamaが11434ポートで立ち上がります。次に、Ollamaを操作するのに便利なOpen WebUIを立ち上げます。こちらもDockerを利用します。 OLLAMA_BASE_URL
にて、OllamaのURLを指定します。
docker run -p 8080:8080 \
-e OLLAMA_BASE_URL=http://localhost:11434 \
-v open-webui:/app/backend/data \
--name open-webui --restart always \
ghcr.io/open-webui/open-webui:main
これで、 http://localhost:8080
にアクセスすると、Open WebUIが表示されます。

モデルのインストール
Open WebUIの管理画面で、モデルをダウンロードできます。指定するのは yuma/DeepSeek-R1-Distill-Qwen-Japanese:14b
です。これは、サイバーエージェント社が日本語データでDeepSeekに対して追加学習したモデルになります。素のDeepSeekは学習データに偏りがあり、日本語への回答があまり良くありません。日本語環境で利用する際には、こちらのモデルを使うのが良いでしょう。

モデルをインストールしたら、準備完了です。この時点でOpen WebUIは不要になります。
チャットアプリの作成
チャットアプリを開発するにあたって、Ollama用のライブラリをインストールします。 www/index.html
にて、下記のJavaScriptタグを追加します。
<script src="https://cdn.jsdelivr.net/npm/ollama-js-client/dist/browser/iife/ollama-js.global.js"></script>
そして、 www/js/app.js
にて、下記のようにOllamaを初期化します。 model
パラメーターで先ほどインストールしたモデルを指定し、 url
パラメーターでOllamaのURLを指定します。
const $ = Dom7;
window.llama = new OllamaJS({
model: "yuma/DeepSeek-R1-Distill-Qwen-Japanese:14b",
url: "http://localhost:11434/api",
})
window.app = new Framework7({
name: 'My App', // App name
theme: 'auto', // Automatic theme detection
el: '#app', // App root element
// App store
store: store,
// App routes
routes: routes,
});
チャットの作成
チャットのUIは www/pages/chat.html
にて作成します。テンプレートは元々のものと同じですが、以下のようなUIになります。
<template>
<div class="page" data-name="chat"> <!-- ページ全体のコンテナ -->
<div class="navbar"> <!-- ナビゲーションバー -->
<div class="navbar-bg"></div>
<div class="navbar-inner sliding">
<div class="title">AIチャット</div> <!-- タイトル -->
</div>
</div>
<div class="toolbar messagebar"> <!-- メッセージツールバー -->
<div class="toolbar-inner">
<div class="messagebar-area">
<textarea class="resizable" placeholder="Message"></textarea> <!-- テキストエリア -->
</div>
<a class="link icon-only demo-send-message-link" @click=${send}> <!-- メッセージ送信ボタン -->
<i class="icon f7-icons if-not-md">arrow_up_circle_fill</i>
<i class="icon material-icons md-only">send</i>
</a>
</div>
<div class="messagebar-sheet">
</div>
</div>
<div class="page-content messages-content"> <!-- メッセージ表示エリア -->
<div class="messages"> <!-- メッセージ表示用の要素 -->
</div>
</div>
</div>
</template>
<script>
export default (props, { $f7, $onMounted, $store, $update, $tick }) => {
// ここにJavaScriptを書く
return $render;
}
</script>
JavaScriptについて
まず、チャット画面で扱う変数を定義します。
let messages; // メッセージを表示するための変数
let messageBar; // メッセージを入力するための変数
const chats = []; // チャット履歴を保持するための配列
次に、コンポーネントがマウントされたときに実行される関数を定義します。ここでは、messages
と messageBar
を初期化します。
$onMounted(async () => { // コンポーネントがマウントされたときに実行される関数
messages = $f7.messages.create({ // messagesの初期化
el: $('.messages'), // 表示する要素
});
messageBar = $f7.messagebar.create({ // messageBarの初期化
el: $('.messagebar'), // 入力する要素
attachments: [] // 添付ファイルの初期値
});
$update(); // レンダリングの更新
});
メッセージの送信
チャット欄で質問を入力して、送信した際に実行されるのが send
関数です。
const send = async () => { // メッセージ送信のイベントハンドラ
// 以下はこの中に処理を書きます
};
まず、入力されたメッセージを取得します。何も書かれていない場合には、終了します。テキストがあれば、それをチャットに追加・表示します。
const content = messageBar.getValue().replace(/\n/g, '<br />').trim(); // 入力されたメッセージを取得
if (content === '') return; // メッセージが空の場合は送信しない
const chat = { // チャットの内容
role: 'user', // ユーザーの発言
content, // メッセージの内容
};
chats.push(chat);
addMessage(chat); // メッセージ表示エリアにメッセージを追加
入力されたメッセージは、履歴として保持します。こうすることで、LLMが過去のメッセージの内容を踏まえた回答を返せるようになります。
const pastMessages = chats.map(chat => { // チャット履歴をAIに送信するための配列を作成
return {
role: chat.role,
content: chat.content,
};
});
そして、Ollamaにメッセージを送信するのですが、今回のライブラリはストリーミングをサポートしています。ストリーミングでは、回答が一気に返ってくるのではなく、徐々に返ってきます。LLMの処理は長いものも多いので、こうして逐次表示する方がユーザーにとって使いやすいUXになるでしょう。
messages.showTyping(); // タイピング中の表示
// メッセージのストリーミング処理
let stream = false;
const onResponse = (error, response) => {
// エラーの場合
if (error) {
console.error(error);
return;
}
messages.hideTyping(); // タイピング中の表示を非表示にする
if (response.done) {
// メッセージのストリーミングが終了した場合
stream = false;
} else {
// メッセージのストリーミングが続いている場合
if (stream) {
addMessage(response.response);
} else {
// メッセージのストリーミングが始まった場合
addMessage({
role: 'bot',
content: response.response,
});
stream = true;
}
}
}
messageBar.clear(); // 入力欄をクリア
// メッセージのストリーミング処理を開始
await llama.prompt_stream(chat.content, onResponse)
$update(); // レンダリングの更新
addMessage
関数は、メッセージ表示エリアにメッセージを追加する関数です。これにより、チャット画面にメッセージが表示されます。ストリーミングでは追加のメッセージが送られてくるので、その場合は一番最後のチャットメッセージの文章を更新し、再描画しています。
const addMessage = (chat) => { // メッセージ表示エリアにメッセージを追加する関数
if (typeof chat === 'string') {
// 最後のメッセージを取得
const chats = messages.messages;
const c = chats[chats.length - 1];
// メッセージを追加
c.text += chat;
c.text = c.text.replace(/\n/g, '<br />');
// メッセージの表示を更新
messages.clear();
messages.addMessages(chats);
} else {
// メッセージを追加
messages.addMessage({
text: chat.content, // メッセージの内容
type: chat.role === 'user' ? 'sent' : 'received', // メッセージの送信元
name: chat.role === 'user' ? 'Me': 'Llama', // メッセージの送信先
avatar: chat.role === 'user' ? '/assets/icons/person.png' : '/assets/icons/robot.png', // アバター画像
});
chats.push(chat); // チャット履歴に追加
}
};
実際の動作
では実際の動作を確認します。歴史的なもの(今回は日本の初代総理大臣について)であれば、割と正確に返してくれているように見えます。

そして、「彼の」といった単語についても、チャット履歴を踏まえた内容で返答してくれています。ただ、結論だけ欲しいと書いても、それが守られることはありませんでした。

Monacaについて聞くと、ハレシネーションが起こってしまって正しい回答ではありませんでした(Monaca Cloud Buildとは一体…)。このあたりは学習データ量によって変わってくるでしょう。

まとめ
今回試した日本語学習データは14Bですが、DeepSeekの最大版は70Bとなっています。こちらを使えば、より正確な回答が得られるかもしれません。オープンソースなので、商用利用ができ、さらに独自の学習データも追加できるのが魅力でしょう。
Ollamaを使うことで、さまざまなモデルを手軽にインストールでき、かつAPIとして動作させられます。ぜひ実装時の参考にしてください。