株式会社豆蔵のブログ - TECH PLAY

TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

110

はじめに # 前回 の記事では、 1つのテキストファイル(桃太郎物語) を対象にした単純なRAG(検索+生成)環境を構築しました。 今回はその拡張として、 複数のドキュメントを読み込み・保持・削除できる永続化対応のローカルRAGアプリ を構築します。 全体構成 # ディレクトリ構造 # project/ ├─ app2.py # 本体アプリケーション ├─ vectorstore/ # ベクトルストアの永続化ディレクトリ │ ├─ faiss_index/ # FAISSのインデックスファイル │ ├─ metadata.json # 読み込まれたファイル一覧 │ └─ temp_docs/ # アップロードされたドキュメントの一時保存場所 新しいアプリ( app2.py )では次のような進化があります。 機能 内容 マルチドキュメント対応 PDF, Word, PowerPoint, テキストなど複数ファイルを同時に学習可能 永続化ストレージ ベクトルDB(FAISS)をローカル保存し、再起動後も再構築不要 個別ファイル削除 特定のファイルだけを削除し、DBを再構築 UI強化 Streamlitのサイドバーでファイル一覧・削除・全削除操作が可能 精度向上 MultiQueryRetrieverで質問を多角的に変換して検索精度を改善 アプリを起動すると、 vectorstore/ 以下に自動で必要なフォルダが作成されます。 ファイルを追加すると、ベクトルDBとメタデータがディスクに永続化されます。 必要なライブラリ # 必要なライブラリを以下のコマンドでインストールします。 pip install langchain langchain-openai langchain-community langchain-huggingface sentence-transformers streamlit faiss-cpu pypdf python-docx python-pptx pydantic cryptography unstructured docx2txt プログラム全体 # 本体ソースコードは以下です。 ソースコード中のコメントに処理内容を記載しています。 かなり行数が多いので、プログラムの主要な部分についてはこの後解説します。 import streamlit as st import os import tempfile from pathlib import Path import shutil import json import uuid # --- LangChain関連ライブラリのインポート --- # LLM(大規模言語モデル)および埋め込みモデルを利用したRAG(Retrieval-Augmented Generation)構成に使用 from langchain_openai import ChatOpenAI # OpenAI互換LLM(例:LM Studio/Ollama)との接続用 from langchain_huggingface import HuggingFaceEmbeddings # HuggingFaceの埋め込みモデル(ベクトル化用) from langchain.text_splitter import RecursiveCharacterTextSplitter # 文書をチャンク単位に分割 from langchain_community.vectorstores import FAISS # 高速ベクトル検索エンジン(ローカル永続化対応) from langchain.chains import RetrievalQA # 検索と回答生成を結合したRAGチェーン from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredPowerPointLoader ) # 各種ファイル形式のローダー from langchain.prompts import PromptTemplate # LLMへのプロンプトテンプレート from langchain.retrievers.multi_query import MultiQueryRetriever # 複数クエリ拡張による検索精度向上 # ============================================================ # 永続化関連のパス設定 # ============================================================ DB_DIR = "vectorstore" # ベクトルストア保存ディレクトリ DB_FAISS_PATH = Path(DB_DIR) / "faiss_index" # FAISSベクトルインデックスファイル DB_METADATA_PATH = Path(DB_DIR) / "metadata.json" # メタデータ保存ファイル TEMP_DOCS_DIR = Path(DB_DIR) / "temp_docs" # 一時的なアップロードファイルの保存先 # ============================================================ # file_uploader のキーを初期化 # ============================================================ # file_uploaderを再初期化するためのユニークキーを設定 if 'file_uploader_key' not in st.session_state: st.session_state['file_uploader_key'] = str(uuid.uuid4()) # ============================================================ # ファイル読み込み関数 # ============================================================ def load_document(file_path): """拡張子に応じて適切なLangChainローダーで文書を読み込む""" ext = os.path.splitext(file_path)[1].lower() if ext == ".pdf": loader = PyPDFLoader(str(file_path)) elif ext == ".docx": loader = Docx2txtLoader(str(file_path)) elif ext == ".pptx": loader = UnstructuredPowerPointLoader(str(file_path)) else: loader = TextLoader(str(file_path), encoding="utf-8") return loader.load() # LangChain Document形式で返却 # ============================================================ # 埋め込みモデル初期化(キャッシュ利用) # ============================================================ # 埋め込みモデルをキャッシュして再利用する関数 @st.cache_resource(show_spinner=False) def get_embeddings(): """HuggingFaceのSentence-BERTモデルを一度だけロードしキャッシュ""" return HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") # ============================================================ # ベクトルDB構築関数 # ============================================================ # ベクトルDBを構築し、ファイルリストと共にディスクに保存する関数 @st.cache_resource(show_spinner=False) def build_and_save_db(documents, file_metadata_list): """文書群からFAISSベクトルDBを構築し、メタデータと共に保存""" if not documents: # 文書が空の場合は既存DBを削除 if Path(DB_DIR).is_dir(): shutil.rmtree(DB_DIR) return None # 文書を小さなチャンクに分割(500文字単位で100文字重複) text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, length_function=len ) docs = text_splitter.split_documents(documents) # 分割実行 # 埋め込みモデルの初期化 (Sentence-BERTベースのモデルを使用) embeddings = get_embeddings() # 埋め込みモデルの取得 # FAISSベクトルストアの新規構築 db = FAISS.from_documents(docs, embeddings) # 永続化ディレクトリ作成と保存 Path(DB_FAISS_PATH).parent.mkdir(parents=True, exist_ok=True) db.save_local(str(DB_FAISS_PATH)) # メタデータをJSONとして保存 with open(DB_METADATA_PATH, "w", encoding="utf-8") as f: json.dump(file_metadata_list, f, ensure_ascii=False, indent=4) return db # ============================================================ # RAGチェーン構築関数 # ============================================================ # 保存されたDBをロードし、RAGチェーンを作成する関数 def create_rag_chain_from_db(db): """既存DBからRAGチェーン(Retrieval+LLM)を構築""" llm = ChatOpenAI( model_name="local-model", # LM Stduioに読み込まれているモデルを指定(モデル名は特定していない) openai_api_base="http://localhost:1234/v1", # LM StduioサーバのURL openai_api_key="not-needed", # API-keyは無し temperature=0.1, # 高い確率の単語を優先的に選ぶ max_tokens=512 # 最大512トークンに制限 ) # 検索精度向上のため、質問を複数クエリに拡張するRetriever # MultiQueryRetrieverを設定: 質問をLLMに渡し、複数の質問に言い換えて検索精度を向上させる # search_kwargs={"k": 2} で取得する文書チャンク数を2つに制限し、応答速度を改善 retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(search_kwargs={"k": 2}), llm=llm ) # RAGプロンプト定義(回答方針) prompt_template = """ 以下の参考文章を元に、質問に日本語で回答してください。 参考文章に答えが見つからない場合は、「分かりません」と回答してください。 参考文章: {context} 質問: {question} """ PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) # RetrievalQAチェーンを生成(検索→回答生成の一連の流れ) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 検索結果を全てプロンプトに詰め込む方式 retriever=retriever, return_source_documents=True, # 回答の根拠となった文書(ソース)を返す設定 chain_type_kwargs={"prompt": PROMPT} # カスタムプロンプトを適用 ) return qa_chain # ============================================================ # 個別ファイル削除とDB再構築 # ============================================================ # 個別ファイル削除とDB再構築のロジック def delete_single_file(file_id_to_delete): """指定されたファイルIDを削除し、残りの文書でDBを再構築""" if not DB_METADATA_PATH.exists(): st.error("メタデータが見つかりません。全体削除を推奨します。") return with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: current_metadata = json.load(f) # 削除対象を特定 file_to_remove = next((item for item in current_metadata if item["id"] == file_id_to_delete), None) if not file_to_remove: st.error("削除対象のファイルIDが見つかりませんでした。") return # 一時ファイルを削除 temp_path = Path(file_to_remove["temp_path"]) if temp_path.exists(): os.remove(temp_path) # 残りのファイルでDBを再構築 new_metadata = [item for item in current_metadata if item["id"] != file_id_to_delete] all_documents = [] with st.spinner(f"ファイルを削除し、残りのデータでAIを再構築中..."): # 残ったすべてのファイルを再ロード for item in new_metadata: doc_path = Path(item["temp_path"]) if doc_path.exists(): documents = load_document(doc_path) all_documents.extend(documents) # 既存キャッシュをクリアしてDB再構築 if "rag_chain" in st.session_state: del st.session_state["rag_chain"] build_and_save_db.clear() # 残りの文書でDBを再構築し、保存 db = build_and_save_db(all_documents, new_metadata) # 再構築後の状態更新 if db: st.session_state.rag_chain = create_rag_chain_from_db(db) st.session_state.current_files = new_metadata st.toast(f"✅ ファイル '{file_to_remove['name']}' を削除し、DBを更新しました。", icon="🗑️") else: # 全削除された場合の後処理 keys_to_delete = ["rag_chain", "messages", "current_files", "file_identifiers"] for key in keys_to_delete: if key in st.session_state: del st.session_state[key] st.toast("✅ 全てのファイルを削除しました。", icon="🗑️") st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # Streamlit再実行でUI更新 # ============================================================ # 全体削除処理 # ============================================================ def delete_all_data(): """DBディレクトリとセッション変数を全削除""" if Path(DB_DIR).is_dir(): try: shutil.rmtree(DB_DIR) st.toast("✅ 全ての読み込みデータを削除しました。", icon="🗑️") except OSError as e: st.error(f"データの削除中にエラーが発生しました: {e}") return # セッション変数をクリア keys_to_delete = ["rag_chain", "messages", "current_files", "file_identifiers"] for key in keys_to_delete: if key in st.session_state: del st.session_state[key] # file_uploader のキーをリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # ============================================================ # Streamlit UI 構築 # ============================================================ st.title("📄 ドキュメント Chatbot") st.write("複数のPDF, DOCX, PPTX, テキストファイルをアップロードして、内容について質問してください。") # --- サイドバー: ファイルアップロード --- uploaded_files = st.sidebar.file_uploader( "ファイルをアップロード(既存DBに追加・上書き)", type=["pdf", "docx", "pptx", "txt", "md"], accept_multiple_files=True, key=st.session_state['file_uploader_key'] ) # --- DB存在チェック --- db_exists = DB_FAISS_PATH.exists() metadata_exists = DB_METADATA_PATH.exists() current_file_identifiers = [(f.name, f.size) for f in uploaded_files] # ---------------------------------------------------------------------- # 初期化ロジック (既存データと新規データの結合処理) # ---------------------------------------------------------------------- # 1. 新しいファイルがアップロードされた場合 (追加/新規構築&保存) if uploaded_files: # 既存のメタデータ(ファイルリスト)をロード existing_metadata = [] if metadata_exists: with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: existing_metadata = json.load(f) # 既存のファイル名(name)のセットを作成(重複チェック用) existing_names = {meta['name'] for meta in existing_metadata} # ---------------------------------------------------- # 新規アップロードファイルの処理 # ---------------------------------------------------- newly_uploaded_files = [] # アップロードされたファイルの中から、既存ファイル名と重複しないものだけを選択 for uploaded_file in uploaded_files: if uploaded_file.name not in existing_names: newly_uploaded_files.append(uploaded_file) else: # 既存ファイルと同じ名前の場合はスキップ(上書きは行わない) pass if newly_uploaded_files: all_documents = [] new_metadata_list = [] TEMP_DOCS_DIR.mkdir(parents=True, exist_ok=True) with st.spinner("新しいファイルを読み込んでいます..."): for uploaded_file in newly_uploaded_files: unique_id = str(uuid.uuid4()) # 新しい一時ファイルとして保存 temp_path = TEMP_DOCS_DIR / f"{unique_id}_{uploaded_file.name}" temp_path.write_bytes(uploaded_file.getvalue()) # 新しいファイルのメタデータを生成 new_metadata_list.append({ "id": unique_id, # 一意なID "name": uploaded_file.name, "temp_path": str(temp_path) }) documents = load_document(temp_path) all_documents.extend(documents) # ドキュメントリストに追加 # ---------------------------------------------------- # 既存データと新規データの結合 # ---------------------------------------------------- # 既存のファイルを再度ロードし、全ドキュメントリストに結合 for meta in existing_metadata: doc_path = Path(meta["temp_path"]) if doc_path.exists(): documents = load_document(doc_path) all_documents.extend(documents) # メタデータリストを結合 combined_metadata = existing_metadata + new_metadata_list with st.spinner("AIのデータ統合と再構築をしています..."): # 古いキャッシュをクリア if "rag_chain" in st.session_state: del st.session_state["rag_chain"] build_and_save_db.clear() # 全文書と全メタデータでDBを再構築し、保存 db = build_and_save_db(all_documents, combined_metadata) st.session_state.rag_chain = create_rag_chain_from_db(db) st.session_state.current_files = combined_metadata # セッションに新しいファイルリストを保存 st.session_state.messages = [] st.info("新しいデータが既存のデータに追加され、AIの準備が完了しました。") # DB構築成功後、アップローダーのリスト(赤枠部分)をクリアするためにリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() else: # アップロードはされたが、すべて既存ファイル名と同じだった場合 st.info("アップロードされたファイルはすべて既に読み込まれているファイル名と同じだったため、処理をスキップしました。") # スキップされた場合も、手動での削除を防ぐためにアップローダーをリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # 2. アップロードがなく、既存DBファイルが存在する場合 (高速ロード) elif not uploaded_files and db_exists and "rag_chain" not in st.session_state: with st.spinner("既存のAIデータ(DB)を読み込んでいます..."): # 埋め込みモデルをロード embeddings = get_embeddings() # 既存のFAISS DBをディスクからロード db = FAISS.load_local(str(DB_FAISS_PATH), embeddings, allow_dangerous_deserialization=True) st.session_state.rag_chain = create_rag_chain_from_db(db) # 既存のメタデータ(ファイルリスト)をロード if metadata_exists: with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: loaded_metadata = json.load(f) st.session_state.current_files = loaded_metadata st.success("既存のドキュメントでチャット可能です。") else: st.session_state.current_files = [] st.warning("既存のドキュメントでチャット可能ですが、元のファイル名リストが見つかりませんでした。") st.session_state.messages = [] # 3. 初期メッセージの表示 if "rag_chain" not in st.session_state and not db_exists: st.info("ファイルをアップロードするか、過去に保存したデータが存在すれば自動的にロードされます。") # ---------------------------------------------------------------------- # 表示ロジック: 現在読み込まれているファイル名の表示と削除ボタン # ---------------------------------------------------------------------- if "current_files" in st.session_state: st.sidebar.markdown("---") st.sidebar.subheader("現在読み込まれているファイル") if st.session_state.current_files: for file_meta in st.session_state.current_files: col1, col2 = st.sidebar.columns([0.8, 0.2]) # ファイル名を表示 col1.markdown(f"- **{file_meta['name']}**") # 個別削除ボタンのUI (ポップオーバーで確認) with col2.popover("🗑️", help="このファイルを削除します"): st.write(f"ファイル **{file_meta['name']}** を削除し、DBを再構築しますか?") if st.button("削除を確定", key=f"delete_{file_meta['id']}", type="secondary"): delete_single_file(file_meta['id']) else: st.sidebar.markdown("- ファイルがありません。") st.sidebar.markdown("---") if Path(DB_DIR).is_dir(): # 全体削除ボタン if st.sidebar.button("🗑️ 全ての読み込みデータを削除", type="secondary"): delete_all_data() # ============================================================ # チャット処理 # ============================================================ if "rag_chain" in st.session_state: # 履歴表示 if "messages" in st.session_state: for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # ユーザー入力受付 if prompt := st.chat_input("ドキュメントについて質問をどうぞ"): with st.chat_message("user"): st.markdown(prompt) st.session_state.messages.append({"role": "user", "content": prompt}) rag_chain = st.session_state.rag_chain with st.spinner("考え中..."): # RAG実行(検索+回答生成) response = rag_chain.invoke({"query": prompt}) answer = response["result"] # 回答表示と参照ソース展開 with st.chat_message("assistant"): st.markdown(answer) with st.expander("参考にした文章"): for doc in response["source_documents"]: st.markdown(f"--- \n {doc.page_content}") st.session_state.messages.append({"role": "assistant", "content": answer}) コード全体の構造 # app2.py の構成を俯瞰すると以下のようになります。 1. ドキュメントローダー関数群 2. ベクトルDB構築・保存・ロード関数 3. RAGチェーン生成関数(MultiQueryRetrieverによる検索精度向上) 4. ファイル削除ロジック 5. Streamlit UI構成 - サイドバー: アップロード・削除・一覧表示 - メイン画面: チャットUI この分離構造により、RAG処理とUIが明確に分かれ、拡張性が高い設計になっています。 主要コンポーネントの説明 # この章では、 app2.py の内部構造をさらに掘り下げ、各関数やモジュールがどのように連携して RAG (Retrieval-Augmented Generation) 環境を構築しているのかを詳しく解説します。 1. ドキュメントローダー(Document Loader) # この部分はシステムの「入力ゲート」として機能します。アップロードされたファイルを受け取り、LangChain が扱える Document オブジェクトに変換します。これにより、後続のベクトル化処理や検索が統一的に実施できます。 内部では拡張子を基に適切なローダークラスを選択し、PDF・Word・PowerPoint・テキストの各形式に対応しています。 ローダーは単にテキストを抽出するだけでなく、ページ情報などのメタデータも付与します。 load_document() 関数で、PDF・DOCX・PPTX・TXTなどのファイル形式を自動判別して読み込みます。 LangChainの各種ローダーを活用しています。 from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredPowerPointLoader ) 2. ベクトルDB(FAISS)永続化 # RAG の根幹を担うのがこの部分です。分割・埋め込み・保存の3工程で構成され、アップロードした複数の文書を検索可能な形に変換します。 分割 : RecursiveCharacterTextSplitter により、文脈の自然な区切りを保ったままテキストをチャンク化。 埋め込み : HuggingFaceEmbeddings によるSentence-BERTモデル( all-MiniLM-L6-v2 )を使用し、文意を数値ベクトルに変換。 保存 :FAISSによってベクトル空間を構築し、インデックスファイルとしてローカル保存。 これらを組み合わせることで、アプリを再起動しても同一の知識ベースを即座に再利用できる「永続化RAG」が実現しています。 分割したドキュメントチャンクを埋め込み(ベクトル化)し、FAISSで保存します。 保存先は vectorstore/faiss_index/ です。 また、読み込んだファイル情報(UUID、ファイル名、保存パス)は metadata.json に保持します。 これにより、アプリを再起動しても 同じ知識ベースを即座に再利用 できます。 また、アプリの中で特に処理時間が長くなりやすいのが、 埋め込みモデルのロード や FAISSベクトルDBの構築 です。 これらを毎回ゼロから実行すると、ユーザー体験が大きく損なわれます。 そこで Streamlit の @st.cache_resource デコレータを利用しています。 これは、関数の戻り値(モデルやデータベースなどの「リソース」)をキャッシュし、再利用可能にする仕組みです。 以前の @st.cache の後継であり、リソース指向のキャッシュをより安全かつ効率的に扱うことができます。 3. MultiQueryRetrieverによる検索精度向上 # 標準的なRAGでは、ユーザーの質問を1つのクエリに変換して検索しますが、このプログラムでは LangChain の MultiQueryRetriever を採用し、LLMが質問を複数の言い換えに自動変換します。 たとえば「品質管理とは何か?」という質問に対して、LLMは内部で以下のような複数クエリを生成します: 品質管理の定義とは? ソフトウェア品質保証との違いは? 品質を維持・改善する方法とは? これにより、文書中の言い換え表現や別の文脈にもマッチしやすくなり、検索精度が大幅に向上します。 単一の質問に対し、LLMが複数の検索クエリを自動生成し、類似文書を多角的に探索します。 これにより、 言い換えや表現の揺れ に強いRAGが実現できます。 from langchain.retrievers.multi_query import MultiQueryRetriever retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(search_kwargs={"k": 2}), llm=llm ) search_kwargs={"k": 2} の 「k」 は、Retriever が検索時に 取得する類似文書チャンクの件数(トップK件) を表します。 つまり、「最も関連度の高い文書を上位2件だけ取得する」という設定です。 LangChain の Retriever は、ユーザーの質問と文書をベクトル空間上で比較し、 コサイン類似度が高い順に K 件の文書チャンクを返します 。 パラメータ 意味 k 類似度上位 K 件の文書を取得する search_kwargs 検索時の動作パラメータをまとめた辞書 db.as_retriever() ベクトルDB(FAISSなど)を検索エンジンとして利用する設定 なぜ「2」を選んでいるのかは、 検索精度・処理速度・トークン消費 のバランスを最適化するためです。 観点 kが大きい場合 kが小さい場合(例:2) 検索精度 多くの文書を参照できるが、関係ない文も混ざりやすい 関連度の高い文脈に限定できる 処理速度 応答が遅くなる(特にローカル実行時) 高速で軽量に動作する トークン消費 多文書入力により増加 少なく済むため効率的 適用場面 大規模RAGや多分野文書 小規模・単一テーマのRAG(本アプリに最適) 本アプリでは「ローカル実行」「FAISS永続化」「中〜小規模ドキュメント」を前提としているため、 過剰な文脈を含めずに回答の精度と速度を両立させる ことが重要です。 LLM(LM Studio / Ollamaなど)のコンテキスト長に収まるよう調整 StreamlitのUI上で応答をスムーズに返すため、処理時間を短縮 ノイズ文書を除去して、より一貫性のある回答を生成 チューニングの目安は以下です。 k値 特徴 想定用途 1 最速・最小トークン。文脈が単純な場合に最適。 短文中心・単一テーマ 2〜3 精度と速度のバランスが良い。 通常のRAG(本アプリ推奨) 5以上 網羅的だが遅い。長文・百科事典的な用途に適する。 多分野・長文RAG このように、 k=2 は「精度・速度・軽量性の最適点」 を狙った実装上の設計判断です。 4. ファイル削除操作 # 永続化されたデータを扱う場合、部分的な削除や再構築の制御が不可欠です。 このアプリでは2段階の削除ロジックを実装しています: 個別削除 :特定のファイルIDを基に、そのファイルに対応する一時保存データとメタ情報を削除し、残りのデータからDBを再構築します。 全削除 : vectorstore/ ディレクトリ全体を削除し、完全な初期化を行います。 削除処理はすべてStreamlitのセッション状態と連動しており、削除後に st.rerun() でアプリを再描画することで、UIが即時更新されます。 アプリは、ファイルアップロード時に自動でベクトルDBを再構築します。 また、以下の2種類の削除機能を備えています。 操作 動作 個別削除 特定ファイルを削除し、残りのデータでDBを再構築 全削除 vectorstore/ ディレクトリ全体を削除して完全初期化 削除後は自動でUIがリフレッシュされ、状態が更新されます。 5. Streamlit UI の特徴 # アプリケーションの操作性を担うフロントエンド部分です。Streamlitのコンポーネントを駆使し、シンプルながら実用的なチャットUIを構築しています。特筆すべき点は次の通りです。 サイドバー :ファイル管理・削除・リスト表示の制御を集中化。 チャットエリア : st.chat_message を使い、ユーザーとAIの会話履歴を対話形式で可視化。 再現性 :Streamlitの session_state を活用することで、状態保持と再初期化を両立しています。 さらに、チャットの背後ではRAGチェーン( RetrievalQA )が動作しており、質問ごとに検索→生成の2段階推論を行います。 サイドバー ファイルアップロード(複数対応) 現在読み込まれているファイル一覧 各ファイルの削除ボタン 「全削除」ボタン メイン画面 チャット履歴の表示 「ドキュメントについて質問をどうぞ」入力欄 AI回答と「参考にした文章」の展開パネル 実行方法 # LM Studioで http://localhost:1234 サーバを起動(事前にLLMをロードしておく) ターミナルで以下を実行: streamlit run app2.py ブラウザ(通常は http://localhost:8501 )でアプリが開きます。 初回起動時は「ファイルをアップロードしてください」というメッセージが表示されます。 PDFやテキストをアップロードすると、自動的に学習・永続化が行われます。 例:複数ドキュメントを活用したRAG # 前回の「桃太郎」のほかに「かぐや姫」の物語をRAGに読み込ませます。 かぐや姫の物語は以下のようにしました。 むかしむかし、竹を取って暮らす翁(おきな)と、その妻が住んでいました。 ある日、翁が山で光る竹を見つけ、その竹を割ると中から小さな女の子が出てきました。 翁はその子を家に連れ帰り、妻とともに「かぐや姫」と名付けて大切に育てました。 かぐや姫は美しく成長し、そのうつくしさは都にまで広まりました。 多くの貴族が求婚しましたが、かぐや姫は誰の申し出も受けず、難しい宝を求めて試しました。 誰一人として成功する者はいませんでした。 やがて帝もかぐや姫を愛しましたが、彼女の心は月の国にありました。 十五夜の夜、月の使者が迎えに来て、かぐや姫は涙を流しながら月へ帰っていきました。 翁と妻は深く悲しみ、いつまでも夜空を見上げてかぐや姫を思いました。 「桃太郎」と「かぐや姫」の2つのテキストをアップロードした場合、両者の内容を組み合わせた質問にも正確に回答できます。 それぞれを読み込ませます。 質問例:「桃太郎とかぐや姫にはどんな共通点がありますか?」 RAGはそれぞれの物語から類似する文脈を抽出し、両者を比較した回答を生成します。 回答として以下のような文章が出力されました。 回答の中の「どちらも最終的に元の場所へ帰っていくという結末」は、双方が元居た場所に戻るというところを関連付けているのが面白いです。 (桃太郎はおじいさん、おばあさんの元へ。かぐや姫は月へ) まとめ # 今回の拡張版では、ローカルRAG環境をより実践的な形に進化させました。 改善点 効果 FAISS永続化 再起動後もDB再構築不要で高速起動 MultiQueryRetriever 検索精度の向上 ファイル管理UI アップロード・削除を視覚的に操作可能 複数ファイル形式対応 PDF/DOCX/PPTX/TXTを混在学習可能 ローカル環境で完結しながらも、実用的な知識アシスタントを実現できるようになりました。 次回は、 文書の要約・分類機能の追加 や、 OpenAI互換API以外のモデル(例:Ollama, Llama.cpp)対応 にも拡張していく予定です。 img { border: 1px solid gray; }
はじめに # 前回 は、LM Studio+Gemmaでクラウドに頼らないAI環境を構築しました。 本記事では、 LM Studio を使ってローカルでLLM(例:Gemma 3 4B)を動かし、さらに LangChain と Streamlit を組み合わせて、クラウドに頼らずに動作する RAG(Retrieval-Augmented Generation) 環境を構築します。 題材として、誰もが知っている「桃太郎」の物語を使い、自分で用意した知識ベースを読み込む ローカルAIチャットボット を作っていきます。 今回のゴールは以下です。 LM Studio でローカルLLMをAPIサーバーとして動かす LangChain でRAG(検索+生成)パイプラインを構築する Streamlit でチャットUIを作成する すべて自分のPC上で完結し、インターネット接続がなくても動作します。 まさに「自分だけのAI」を作る第一歩です。 LangChainとは # LangChain は、 大規模言語モデル(LLM)を外部データと統合して活用するためのフレームワーク です。 特に、 RAG(Retrieval-Augmented Generation) の構築を容易にする仕組みを提供します。 今回作成するRAG環境では次の4つの処理を組み合わせて、より正確で文脈に基づいた回答を生成します。 ステップ 処理内容 LangChainの機能 ① テキスト分割 ドキュメントを小さなチャンクに分割 TextSplitter ② 埋め込み生成 テキストをベクトル化 Embeddings ③ 検索 質問と類似したチャンクを検索 VectorStore (FAISS, Chromaなど) ④ 回答生成 検索結果+質問をLLMに入力 RetrievalQA や Chain Streamlitとは # Streamlit(ストリームリット) は、 Pythonコードだけで簡単にWebアプリを作成できるフレームワーク です。 データ分析・機械学習・LLMアプリ(例:RAGチャットボット)などのUI構築に広く利用されています。 特徴 説明 簡単な構文 HTMLやJavaScriptを使わず、PythonのみでUIが書ける 即時実行型 コードを保存すると自動でWeb画面が更新される データ可視化に強い matplotlib 、 plotly 、 pandas などと連携可能 インタラクティブUI テキスト入力・スライダー・ボタン・チャットUIなどを簡単に実装できる ローカル or クラウド両対応 streamlit run app.py でローカル実行、または共有用にクラウド公開可能 LM Studio サーバの起動 # 最初のステップとして、モデルを動かすための「APIサーバー」をLM Studioで起動します。 これによって、PythonプログラムがLLMと会話できるようになります。 手順は以下です。 LM Studioを開きます。 左側のメニューから、「開発者」タブを選びます。 画面の上部にあるドロップダウンメニューで、gemma-3 4B モデルを選択し、読み込みます。 (モデルは前回も使用したものです) 「Start Server」 ボタンをクリックします。 サーバーが起動します。 ログに以下のようなメッセージが出力されます。 サーバが「 http://localhost:1234 」で動作していることがわかります。 以下のAPIが確認できます。 (OpenAI互換APIです) RAGパイプラインの構築 # 次は、 RAG(Retrieval-Augmented Generation) という仕組みを作ります。 これは、 LLMが外部の知識(今回はテキストファイル)を参照しながら回答を生成するための技術 です。 身近な例で言うと、「教科書を見ながらテスト問題を解く」 に似ています。 教科書 = 事前に用意するデータ(今回はテキストファイルを用います) テスト問題 = ユーザーからの質問 問題を解く人 = LLM この仕組みを作るために、まずはPythonの環境を整える必要があります。 いくつか専門のライブラリ(便利な道具セットのようなもの)をインストールします。 ターミナル(WindowsならコマンドプロンプトやPowerShell)を開いて、以下のコマンドを実行します。 pip install langchain langchain-community langchain-openai langchain-huggingface streamlit faiss-cpu RAGの教科書 # まず、LLMが読み込む「教科書」(知識ベース)を作成します。(単純なテキストファイルとします) Python スクリプトを作成する予定のフォルダーと同じフォルダーに、 knowledge.txt という名前の新しいテキストファイルを作成します。 教科書データとして、桃太郎に関する次の短編小説をコピーしてテキストファイルに貼り付け、保存します。 このファイルが ローカル ナレッジ ベース になります。 むかしむかし、あるところにおじいさんとおばあさんが住んでいました。 おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。 おばあさんが川で洗濯をしていると、大きな桃がどんぶらこ、どんぶらこと流れてきました。 おばあさんはその桃を拾い上げて、家に持ち帰りました。 家に帰って桃を割ってみると、中から元気な男の子の赤ちゃんが出てきました。 桃から生まれたので、その子を「桃太郎」と名付けました。 桃太郎はすくすくと育ち、やがて鬼ヶ島へ鬼退治に行くと言い出しました。 おばあさんからきびだんごをもらい、桃太郎は旅に出ます。 旅の途中で、犬、猿、雉を家来にしました。 そして、みんなで力を合わせて鬼を退治し、宝物を持って家に帰りました。 Streamlitアプリを作成する # 本体のプログラムを作成します。 先ほど作った knowledge.txt と同じフォルダに、 app.py という名前で新しいファイルを作成します。 そして、以下のコードをすべてコピーして、 app.py ファイルに貼り付けます。 import streamlit as st from langchain_openai import ChatOpenAI from langchain.text_splitter import CharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain.chains import RetrievalQA # ============================= # --- LLMとRAGのセットアップ --- # ============================= # ドキュメントを読み込んでRAGパイプラインを作成する関数 def create_rag_chain(document_path): # ドキュメントを読み込む with open(document_path, 'r', encoding='utf-8') as f: document_text = f.read() # 1. ドキュメントを小さな「チャンク」に分割する # これにより、モデルが関連情報を見つけやすくなります。 text_splitter = CharacterTextSplitter( separator="\n", chunk_size=200, # 各チャンクのサイズ(文字数) chunk_overlap=50, # チャンク同士の重なり length_function=len ) docs = text_splitter.split_text(document_text) # 2. 各チャンクの「埋め込みベクトル」を作成する # 埋め込み(Embeddings)は、テキストをコンピュータが意味を理解できる数値のベクトルに変換する技術です。 # all-MiniLM-L6-v2は、文章をコンピュータが理解できる数値(ベクトル)に変換することに特化した、小型で高速なモデルです。 embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") # 3. ベクトルストア(FAISS)を作成し、埋め込みを保存・検索できるようにする # これは、私たちの「教科書」に検索可能な索引を作るようなものです。 db = FAISS.from_texts(docs, embeddings) # 4. ローカルLLMサーバー(LM Studio)への接続を設定する llm = ChatOpenAI( # ↓↓↓ LM Studioの「API Identifier」をここに貼り付けてください ↓↓↓ model_name="local-model", # ローカルモデルを使用するように指定 base_url="http://localhost:1234/v1", # LM Studioサーバーのアドレス api_key="not-needed", # ローカルサーバーなのでAPIキーは不要 temperature=0.1 # temperatureを低く設定して、「参考文章から外れず、最も確実な回答をしなさい」とAIに指示している ) # 5. RetrievalQAチェーンを作成する # このチェーンは、検索役(FAISSの索引)とLLMを組み合わせます。 # 質問をすると、まず最も関連性の高いテキストチャンクを見つけ出し、 # それを質問と一緒にLLMに渡して、回答を生成させます。 retriever = db.as_retriever() qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # "stuff"は、関連するチャンクをすべてプロンプトに「詰め込む」方式 retriever=retriever, return_source_documents=True ) return qa_chain # knowledge.txtを使ってRAGチェーンを作成 rag_chain = create_rag_chain("knowledge.txt") # ============================= # --- Streamlit UI --- # ============================= st.title("🍑 桃太郎チャットボット") st.write("桃太郎の物語について、質問してください!") # チャット履歴を初期化する if "messages" not in st.session_state: st.session_state.messages = [] # 履歴にあるメッセージを再表示する for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # ユーザーの入力に反応する if prompt := st.chat_input("質問をどうぞ"): # ユーザーのメッセージを表示 with st.chat_message("user"): st.markdown(prompt) # ユーザーのメッセージを履歴に追加 st.session_state.messages.append({"role": "user", "content": prompt}) # LLMの応答を取得 response = rag_chain.invoke({"query": prompt}) answer = response["result"] # アシスタントの応答を表示 with st.chat_message("assistant"): st.markdown(answer) # アシスタントの応答を履歴に追加 st.session_state.messages.append({"role": "assistant", "content": answer}) ソースコード中のコメントでプログラムの解説をしていますが、概略を以下にまとめます。 前半:頭脳の準備パート ( create_rag_chain 関数) # ここは、LLMが 「賢い司書」 になるための準備をする部分です。 本を読む: まず、教科書ファイル「桃太郎の物語」 ( knowledge.txt ) の内容をすべて読み込みます。 付箋を貼る: 物語を短い文章(チャンク)に区切って、内容ごとにたくさんの付箋を貼っていくようなイメージです。( CharacterTextSplitter ) 索引を作る: コンピュータが「どの付箋にどんな内容が書いてあるか」をすぐに見つけられるように、特殊な索引(ベクトルストア)を作ります。( Embeddings , FAISS ) LLMと連携: 最後に、「質問が来たら、まず索引を使って関連する付箋を探し、その内容を参考にして答える」という ルール ( RetrievalQA )を決め、LM StudioのLLMと連携させます。 この準備によって、LLMはただの物知りではなく、 資料(教科書データ)に基づいて 回答できる専門家になります。 --> Information all-MiniLM-L6-v2 はどこから来るのか? 上記のモデルは、初めてプログラムを実行したときに、インターネット上にある 「Hugging Face Hub」 という巨大なAIモデルの保管庫から、自動的にダウンロードされます。 一度ダウンロードされると、PC内の特別なフォルダ(キャッシュと呼ばれます)に保存されます。 2回目以降にプログラムを実行するときは、もう一度ダウンロードするのではなく、PCに保存されたそのファイルから直接読み込まれます。 後半:アプリ画面パート (Streamlit UI) # ここは、ユーザーが実際に触るチャット画面を作る部分です。 画面の表示: 「🍑 桃太郎チャットボット」というタイトルを表示します。 入力欄の用意: ユーザーが質問を入力するためのチャットボックスを用意します。 応答の処理: ユーザーが質問を入力すると、その質問を 前半で作った「賢い司書」 に渡します。 司書(RAG)が資料を調べて作った回答を受け取ります。 その回答をチャット画面に表示します。 ざっくり言うと、 「前半で資料を読み込んで賢くなったAIを用意し、後半でそのAIと会話するためのチャット画面を作る」 という2段構成になっています。 アプリケーションを実行 # アプリケーション(チャットボット)を起動します。 LM Studio のサーバーが起動していることを確認します。 app.py と knowledge.txt を保存したフォルダを、ターミナル(コマンドプロンプト)で開きます。 ターミナルで、以下のコマンドを入力して実行します。 streamlit run app.py 上記コマンドを実行すると、自動的にWebブラウザで新しいタブが開きます。 (デフォルトでは「 http://localhost:8501/ 」でアプリケーションが起動しています) ブラウザの右上にRAGを用意していることを示す進捗が数秒間表示されます。 少しの時間の後「🍑 桃太郎チャットボット」の画面が表示されるはずです。 質問をする # チャットボットに質問をしてみます。 質問は以下です。 「桃太郎とは何者ですか?」 「桃太郎は何をしましたか?」 教科書データに載っている内容を回答していることがわかります。 教科書データの内容を一部変えてみる # 教科書データの内容を一部変えてみます。 以下の 旅の途中で、犬、猿、雉を家来にしました。 の部分を 旅の途中で、猫、亀、鶴を家来にしました。 に変えて実行してみましょう。 結果は以下のようになりました。 正しく 家来が入れ替わって解釈されている ことがわかります。 教科書データに無いことを聞いてみる # 教科書データに無いことを聞いてみます。 AIが勝手に物語を拡張していないことを確認します。 用意した教科書データ以外の内容については答えられないことがわかります。 参考にした元の文章の表示 # 回答と一緒に「参考にした元の文章」も表示するように機能を追加します。 ソースコードの以下の部分 with st.chat_message("assistant"): st.markdown(answer) に機能を追加します。 変更後のソースコードは以下です。 with st.chat_message("assistant"): st.markdown(answer) # --- ここから追加 --- with st.expander("参考にした文章"): for doc in response["source_documents"]: st.markdown(f"--- \n {doc.page_content}") # --- ここまで追加 --- アプリケーションを実行すると、以下のように参考にした文章も一緒に表示されるようになりました。 まとめ # 今回紹介した手順では、 LM Studio × LangChain × Streamlit を組み合わせることで、 クラウドに依存しないローカルRAG環境 を構築しました。 ポイントを振り返ると次の通りです。 LM Studio でローカルLLMをOpenAI互換APIとして動作 LangChain で文書を分割・ベクトル化・検索・回答生成を自動化 Streamlit で対話的なUIを構築し、ブラウザから手軽に利用可能 教科書データ を変更すると、AIの回答内容も動的に変化 この仕組みにより、「自分の持つ知識ファイル」をAIが読み込み、AIが “自分専用の知識アシスタント” のように振る舞います。 より複雑なナレッジや複数ファイル対応、さらには検索精度向上やUI改善などに発展させることもできます。 自分のデータを自分の環境で活かす、新しいAI活用の形を試すことができました。 img { border: 1px gray solid; }
はじめに # 私は普段は業務アプリの開発に従事しております。開発言語はほぼJavaであり、Spring Framework/Spring Bootを使用することが多いです。 業務以外でプログラムを書く機会や趣味はほとんどなかったのですが、最近インディー系の2Dアクションゲームにハマっており(ホロウナイト、カップヘッド、オリシリーズなどが好きです。)自分でも簡単なもので良いからミニゲーム開発をしてみたい!と思い立ってやってみることにしました。 今回は私のミニゲーム開発に使用したPythonのライブラリのことや開発の様子についてお伝えしたいと思い記事を書くことにしました。読んでいただけますと幸いです。 開発の進め方 # まずは本を読んで体系的に技術の知識を身に着けたいと思い、以下の書籍を参考にすることにしました。 Pythonでつくる ゲーム開発 入門講座 実践編 / 廣瀬 豪 この本を読めば書いてある通りのミニゲームが作成できますが、オリジナルの要素や機能の追加をしたくなることもあるかと思い、その際はChatGPTを頼ることにしました。本と生成AIのハイブリッド開発ですね。 使用するライブラリ「tkinter」について # 本では、Pythonのライブラリ tkinter と Pygame が紹介されています。 Pygame は高機能なゲーム用ライブラリですが、今回はシンプルなミニゲームを作るので tkinter のみを使用することとしました。 ChatGPTに聞いてみたところ、以下のようなライブラリとのことです。 tkinter(ティーケーインター)は、PythonでGUI(グラフィカルユーザーインターフェース)アプリを作るための標準ライブラリです。 Pythonだけで「ウィンドウを出したり、ボタンやラベルを配置したり、画像を表示したり」といった デスクトップアプリを作れる便利なツールです。 ゲームに限らずシンプルなGUIツールを作成するのにも使えそうですね。Pythonに標準で入っているのが嬉しい点だと思いました。 ミニゲームの要件 # 以下のような要件を満たす2Dのミニゲームを作ります。 ヘビからウサギが逃げるというコンセプトのゲーム ウサギがヘビに接触するとゲームオーバー ヘビは画面内をランダムな方向に移動する ウサギはプレイヤーが操作し、マウスのカーソルに追従して移動する ゲーム開発に関しては初心者なのでまずはこれくらいで良いでしょう。 tkinterの基本 # 本を読みながらコードを書いてゆきます。tkinterのミニゲームアプリは以下のコードが必須だそうです。 # メインウィンドウとキャンバスの設定 root = tkinter.Tk() root.title("ウサギのミニゲーム") # ---------------------- # 画像を描画するキャンバスの設定を行う。 # (縦横比の設定や背景画像素材の設定、マウスムーブのイベントとキャンバスの紐づけ等) # ---------------------- root.mainloop() 本には root = tkinter.Tk() が「ウィンドウの部品を作る」、 root.mainloop() が「ウィンドウを表示」の処理だと書かれていますが、もう少し詳しく知りたいのでChatGPTに聞いてみます。 root = tkinter.Tk() 何をしているか tkinter.Tk() は アプリ全体のメインウィンドウ(トップレベルウィンドウ)を生成 しています。つまり「ウィンドウを1つ作る」という命令です。 具体的な動作 OS上で、新しい空のウィンドウを作ります。このウィンドウは、後でボタンやキャンバス、ラベルなどのウィジェットを配置する「土台」になります。 root はこのウィンドウを操作するための変数名(慣習的に root や window と書かれます)。 root.mainloop() 何をしているか イベントループ(main roop)を開始します。これは、ウィンドウを開いたままにして、ユーザー操作(クリック・キー入力など)を待ち続ける仕組みです。 具体的な動作 この行を呼ぶと、プログラムは「ループ状態」に入ります。 tkinterが内部的に「イベント(クリック・キー入力など)」を監視して、対応する処理を呼び出します。 mainloop を呼ばないと、ウィンドウは一瞬で開いてすぐ閉じてしまいます。 より詳しく知ることができました。 tkinter でウィンドウを作成し、表示・維持する最も基本的なコードだということが分かりました。GUIアプリはユーザの操作を待つ必要があるので内部的にループ状態を管理する仕組みがあるんですね。 作ってみる # 一旦できました。拙いですが一応ちゃんとミニゲームとして動いていて感動です。 細かい部分は全て書ききれないので、このミニゲームのキモとなる当たり判定のロジックについてピックアップして書いておきます。 当たり判定のロジック # 本では「円同士の当たり判定」と「長方形同士の当たり判定」の2種類が紹介されていました。等身の高いキャラクターなんかは長方形の方が適していそうですが、今回は円同士の当たり判定を実装してみました。 ウサギとヘビそれぞれのx、y座標の値と半径の長さrを使用して計算します。座標同士の距離を求め、それが半径の合計以下の長さになっているかを判定しています。 def hit_check(self): dis = math.sqrt((self.rabbit.x - self.snake.x) ** 2 + (self.rabbit.y - self.snake.y) ** 2) return dis <= self.rabbit.r + self.snake.r マウスムーブ時に実行する処理の中でhit_checkメソッドを呼び、Trueが返されたときはゲームオーバー画面を表示するようにしています。 改善点 # 一応形にはなりましたが改善したい部分も出てきました。ヘビは0.05秒間隔でランダムな方向に一定距離移動するように実装していたのですが、目で見ると動きがかなりぎこちないです。ChatGPTに相談しつつ、ゆっくりとウサギの位置に追従するような動きになるよう処理内容を修正してみます。 以下が、ヘビを管理するSnakeクラスに実装した修正後のヘビ移動メソッドです。target_x、target_yはウサギの座標を受け取り、on_move_doneはこの移動処理を繰り返し呼ぶためのコールバックです。 def move_toward(self, target_x, target_y, on_move_done): # ウサギに向かって移動 dx = target_x - self.x dy = target_y - self.y dist = math.sqrt(dx**2 + dy**2) # 距離が0でないときだけ移動方向を正規化 if dist != 0: dx /= dist dy /= dist new_x = self.x + dx * self.speed new_y = self.y + dy * self.speed # 画面範囲チェック 範囲内なら更新 if 0 < new_x < 1200 and 0 < new_y < 676: self.x, self.y = new_x, new_y self.draw() # 50msごとに再実行 self.job = self.canvas.after(50, on_move_done) 修正後の動きがこんな感じになりました。 自然な動きでウサギを追いかけるようになりました。他にも工夫すれば、追従とランダムな方向を組み合わせたり、一定時間おきにスピードアップしたりできそうですね。 おわりに # ゲームを作るのは初めてでしたが、やってみると結構簡単にちょっとしたミニゲームが作れて面白かったです。今は本だけではなく生成AIに相談しながら開発を進めることができるのがかなり便利ですね。本で基礎を学んで、ChatGPTと相談しながらオリジナル要素を実装してゆくという進め方が楽しいです。 まだまだ改善したい点はたくさんあります。 ウサギをマウスムーブに追従ではなくボタン操作できるようにする ジャンプを実装する 障害物を配置する ヘビとバトルして倒せるようにする(攻撃アクションとHPの導入) などなど。 また、今回は素材画像をいらすとやさんからダウンロードして使用しましたが、素材も全て生成AIで作るのも良さそうだと思いました。 引き続き趣味でゲーム開発をやっていき、次は Pygame を使って何かしら作り記事にできたらと考えています。 読んでいただいてありがとうございました。
はじめに # 時代の流れは速いもので、 「2024年版!VS Code で Java 開発環境を構築する」 で、VS CodeのJava環境構築が紹介されてからのいくつかの改善がなされました。今回はそれらを紹介します。 Extension Pack for Java Auto Configの利用 # 今回も結論から言ってしまうと 「Extension Pack for Java Auto Config を入れましょう」で終わりです。 Extension Pack for Java Auto Config - Visual Studio Marketplace この拡張パックの内訳は以下のようになっています。 JDKの自動構成 Extension Pack for Java - Visual Studio Marketplace Spring Boot Extension Pack - Visual Studio Marketplace 追加の拡張 素のVS Codeに「Extension Pack for Java Auto Config」を入れるだけで、Javaアプリor SpringBootアプリを作るための準備はほぼ完了です。言ってしまえばVS Code版「Pleiades All in One」という内容になっています(実際にこの拡張はPleiadesチームによって開発されています)。 「Extension Pack for Java」と「Spring Boot Extension Pack」は 2024年版 で解説されているので説明は割愛します。 JDKの自動構成 # この拡張のメインの機能です。この拡張は内部に複数のJDK(少なくとも 3 つの LTS バージョンと最新バージョン)を含んでいます。フォルダを開いたときにMavenプロジェクトなどが含まれている場合は、最適なJDKを使うように自動的に構成されます。またmavenやgradleも含まれているので、これらをインストールしなくても開発を始めることができます。 VS Codeからターミナルを起動するときも、各JDK環境に合わせたターミナルを立ち上げることができます。 Windows環境での日本語文字化け対策 # Windows環境でJavaアプリを実行するとターミナルへのログの出力が文字化けすることがあるので、対策をします。 JDK18以降を使っている場合 # JDK18以降のデフォルトの文字コードはUTF-8です。一方でターミナルのデフォルトの文字コードはMS932のため文字化けが起こることがあります。 これを解消するには、ターミナルの文字コードを強制的にUTF-8にします。 レジストリエディタを立ち上げて \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor を開きます。 ここに以下のような値を作成します。 値の名前:Autorun 値のデータ:chcp 65001 > nul 最後の部分は「null」ではなく「nul」であることに気を付けてください。 JDK17以前を使う場合 # 上記の設定をしたままでJDK17以前を使うと、JDKの文字コードはMS932で、ターミナルはUTF-8なので文字化けが起こります。 そこで以下のような環境変数を設定してJDKの文字コードをUTF-8にします。 JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 追加の拡張 # Extension Pack for Java Auto Configが追加するその他の拡張について見ていきます。 XML - Visual Studio Marketplace # XMLの入力支援をしてくれます。例えば、タグの上にマウスカーソルをホバーさせるとスキーマにかかれたドキュメンテーションを表示するなどの機能があります。 Code Spell Checker - Visual Studio Marketplace # コードやコメントのスペルチェックをしてくれます。 TODO Tree - Visual Studio Marketplace # ソースコード中のTODOやFIXMEを一覧表示してくれます。 Live Server - Visual Studio Marketplace # HTMLやCSSなどの確認に便利な簡易サーバーです。 HTMLファイルを開いた状態で、画面右下の「Go Live」を押すと、ブラウザでHTMLを表示してくれます。 Live Reload機能によりHTMLを書き換えるとリロードなしでブラウザに修正が繁栄されます。 Trailing Spaces - Visual Studio Marketplace # 末尾空白のハイライト表示と削除をしてくれます。 indent-rainbow - Visual Studio Marketplace # インデントのハイライト表示をします。 Rainbow CSV - Visual Studio Marketplace # CSVファイルのハイライト表示をします。 さいごに # Extension Pack for Java Auto Configによって、これ1つインストールするだけで、Javaアプリ/SpringBootアプリの開発環境が整うのは便利になったと思います。 EclipseやIntelliJ IDEAなどの統合環境に比べると機能面で劣るところもあります。一方、VS Codeは無料で動作が軽く、しかもAI対応が統合環境よりも早いことを考えると、Java開発環境としてVS Codeを使うのもありだろうと思います。
本記事は、「TwinCATで始めるソフトウェアPLC開発」シリーズの第2回目です。 他の章も併せてご覧ください。 第1回:環境構築編 第2回:ST言語でのプログラミング(1/2)(今回) 第3回:ST言語でのプログラミング(2/2)← 絶賛作成中! 0. はじめに # 前回の記事 はTwinCATの開発環境(XAE)・実行環境(XAR)の構築方法について説明しました。 今回は基本的なPLCプログラムの実装方法についてご紹介します。 1. ST言語とは? # IEC61131-3規格で定められた5種類のプログラム言語のうちの1つです。 テキスト形式による実装が可能な言語であり,Pascalライクな文法で記述します。 本記事ではこの言語を使用します。 ST言語によって記述したプログラムの例 FOR i:=0 TO 10 DO // メソッド実施,引数によるデータの入出力 fbHogeHoge.FugaFuga(i, outData => tmpData) END_FOR 2. PLCプログラムの作成・実装 # 開発環境を起動し,ST言語でのPLCプログラムを実装していきます。 2.1 ソリューション作成 # Visual Studio もしくは XAE Shellを開きます。 (今回はVisual Studioを選択しました) 「新しいプロジェクトの作成」を選択します。 プロジェクトテンプレートには「TwinCAT XAE Project (XML format)」を選択します。 プロジェクト名とソリューション名を指定します。 「ソリューションとプロジェクトを同じディレクトリに配置する」にチェックを入れます。 プロジェクト名・ソリューション名は「TwinCAT-Tutorial」とします。 2.2 PLCプロジェクト作成 # ソリューションエクスプローラーにて「PLC」を右クリックして,「新しい項目の追加」をクリックします。 --> ソリューションエクスプローラーの開き方 XAEShellもしくはVisualStudioの画面左側にソリューションエクスプローラーが表示されない場合は,下記の項目をクリックしてください。 「表示」>「ソリューションエクスプローラー」 「Standard PLC Project」を選択して,プロジェクト名を指定します。 今回は「PlcTutorialProject」として追加ボタンをクリックします。 2.3 MAINプログラムを編集してみる # PLCプログラムを新規作成すると,ソリューションエクスプローラー内の「PLC」配下に項目が追加されます。 「POUs」フォルダ内にある「MAIN(PRG)」をクリックして編集画面を開きます。 編集画面の上半分は変数を定義するためのスペース,下半分はプログラムの処理を記述するためのスペースです。 (C++で例えるなら,上半分がヘッダファイル,下半分がソースファイルを記述するスペースとなります) 定義スペース(上半分)には下記のように記述します。今回はDINT型(符号付き32bit整数)の変数を定義します。 変数定義時は「変数名 : 型」のように記述します。 MAINプログラム 定義スペース PROGRAM MAIN VAR /// プログラム呼び出し回数 CycleCount : DINT; END_VAR 実装スペース(下半分)は下記のように記述します。 今回は処理1回ごとに変数「CycleCount」をインクリメントしています。 MAINプログラム 実装スペース // 変数をインクリメントする CycleCount := CycleCount + 1; --> 代入時の記号 代入では「:=」を使用します。「=」は**同値評価(値が等しいかどうか)**である点に注意してください。 --> TwinCATで使用可能なプリミティブ型 使用可能なプリミティブ型の一覧は こちら を参照してください。 --> 自動補完機能 「Ctrl+Space」キーを入力すると,補完候補のウィンドウが表示されます。コーディングの時間短縮におすすめです。 プログラムの編集が完了したら,ビルドしてエラーが発生しないことを確認します。 IDE上部の「ビルド」タブ>「ソリューションのビルド」をクリックします。 IDE下部に表示される「出力」タブ内で,失敗の数が0となっていることを確認します。 3. プロジェクトの実行と動作確認 # 3.1 デプロイ前の確認事項 # プログラムを書き込むために,まずはXAR環境(=実行環境)にアクセスできるかを確認します。 システムトレイに表示されている歯車アイコンを右クリックして 「Router」>「Edit Routes」を選択します。 --> システムトレイにアイコンが表示されない場合 システムトレイに歯車アイコンが表示されない場合は,下記のexeファイルを起動してください。 C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\System\TcAmsRemoteMgr.exe (※TwinCATのインストール場所を変更した場合は,上記と異なる場合があります) 「TwinCAT Static Routes」ウィンドウが表示されるため,下記のように緑色となっていれば接続が行えています。 もし緑色の項目が存在しない場合は, 前回記事の3章・4章 から設定を見直してください。 3.2 プロジェクトのデプロイ # XARとの通信が確立していることを確認したら,IDEからターゲットを指定します。 IDEを開き,「表示」タブ>「ツールバー」>「TwinCAT XAE Base」をクリックしてチェックを入れます。 これにより,IDEの上部にTwinCATに関する表示が増えます。 【変更前】 【変更後】 追加された項目のうち,「ローカル」と表示されているコンボボックスをクリックして,XAR環境をターゲットとして指定します。 ターゲット指定後,青色の階段のアイコンをクリックします。 「構成のアクティブ化」ウィンドウが表示されるので,OKボタンを押します。 初回書き込み時は評価ライセンスの生成を促されるため,「はい」を選択します。 表示されたものと同じ文字列をテキストボックスに入力し,OKボタンを押します。 これにより,評価用ライセンスが生成され,プログラムが実行可能な状態となります。 --> TwinCATのランタイムライセンスについて TwinCATの各パッケージをXAR環境で使用する場合はライセンスが必要です。 ライセンスを所持していない場合、無償で使用するための評価用ライセンスを生成して使用することが出来ます。 ただし、この評価版ライセンスは有効期限が7日間であり、期限を過ぎた場合は再度生成しなおす必要があります。 評価版ライセンスは正式なライセンスを使用した場合に比べて、機能に制限がかかりますが、基本的な動作を確認するだけであれば十分に使用可能です。 TwinCATを再起動するか尋ねられるため,「OK」を押して再起動します。 IDEの右下に表示されている歯車アイコンが下図のように緑色かつ回転していれば,プログラムが正常に実施されています。 3.3 ログインによる動作確認 # TwinCATでは,XARにログインすることで変数の値をリアルタイムで確認することができます。 このログイン機能を使用して,先ほど書き込んだプログラムが正常に動作しているかを確認してみます。 「拡張機能」タブ>「PLC」>「ログイン」を選択してログインします。 このボタンが無効状態となっている場合は,ターゲットを指定するコンボボックスに正しいターゲットが指定されているかを確認してください。 ログインした状態でMAINプログラムを開くと,CycleCount変数の値がリアルタイムで確認できます。 1秒間におよそ100だけ加算されていく様子が確認できます。 これは,TwinCATプロジェクトを作成したときに生成されるタスクの実行周期が10msであるためです。 4. タスクの実行周期を変えてみる # デフォルトで生成されるタスクの周期は10msですが,これを変更してみましょう。 プロジェクト生成時に自動で追加されたタスクを消してみます。 新しいタスクを作成します。「SYSTEM」>「タスク」を右クリックして「新しい項目の追加」をクリックしてください。 タイプは「TwinCATタスク」を選択し,名前を「MainTask」として「OK」をクリックします。 作成したタスクの詳細設定画面が開かれるので,「サイクルティック」を10 → 100に変更します。 これによりタスクの実行周期が100msとなります。 --> Information サイクルティック1つ当たりの時間はデフォルトでは1msですが,CPUのコア設定で変更可能です。 詳細についてはこちらをご覧ください。 https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_system/5210414219.html&id= タスクを作成したら,どのプログラムを呼び出すかを設定します。 「PLCプロジェクト」を右クリック>「追加」>「参照されるタスク」を選択します。 割り当て可能なタスクが表示されるので,先ほど作成した「MainTask」を指定して「Open」をクリックします。 生成した「タスク参照」を右クリックして,「追加」>「既存の項目」を選択します。 タスクから呼び出すプログラムを選択します。先ほどコードを修正した「MAIN」プログラムを選択してOKを押します。 先程と同様に,ログインして変数の様子を見てみましょう。 1秒間に10だけ値が増えていくことが確認できると思います。 これは,先ほど作成したタスクがMAINプログラムを100msごとに呼び出しているからです。 5. 同一タスクに複数のプログラムを登録する # 1つのタスクには複数個のプログラムを登録できます。 例えば実行周期を10msに設定したタスクにプログラムAとプログラムBの2つを登録した場合,10msごとにプログラムAとプログラムBが実施されます。 ただし,プログラムAとBは並列で実行されるのではなく, どちらかのプログラムが完了した後にもう一方のプログラムが実施される 点に注意してください。 概念構造を下図に示します。 --> Warning 両プログラムの処理時間の合計がタスク実行周期を上回る(タスクオーバーラン)と,システムがハングする可能性があります。 プログラムの実行時間とタスクの実行周期には十分ご注意ください。 実際に複数のプログラムを同一のタスクに割り当ててみます。 「POUs」フォルダを右クリックして,「追加」>「POU」を選択します。 プログラム名は「MAIN2」とし,タイプは「プログラム」を,実装言語は「構造化テキスト」(ST)を選択してOpenをクリックします。 MAIN2プログラムでは,MAINプログラムと同じように変数をカウントアップする処理を記述します。 (MAINプログラムと区別するために,インクリメント量を2倍にしておきます) MAIN2プログラム 定義スペース PROGRAM MAIN2 VAR /// プログラム呼び出し回数の2倍値 CycleDoubleCount : DINT; END_VAR MAIN2プログラム 実装スペース CycleDoubleCount := CycleDoubleCount + 2; MAINプログラムの時と同様に,MAIN2プログラムをMainTaskにアサインします。 MainTask参照アイテムの子要素に「MAIN」と「MAIN2」の両方があることを確認してください。 この内容を書き込んで動作を確認してみます。 MAIN2プログラムはMAINプログラムと同じ周期(100ms)で実行され,CycleDoubleCount変数値が1秒間で20だけ値が増えることが確認できます。 6. プログラム間でデータを共有する # あるプログラムで計算した値を別のプログラムで使用したい場合が多々あります。 このような場合は,プログラムもしくはタスク間で共通のデータ(グローバル変数)を定義します。 グローバル変数はすべてのタスクが参照できる共有リソースとして定義されるため,これを用いることでタスクを跨いだデータ共有が可能です。 実際にMAINプログラム内での値をMAIN2プログラムで参照してみます。 「GVLs」フォルダを右クリックして,「追加」>「グローバル変数一覧」をクリックします。 変数リスト名は「GVL_Var」として「Open」をクリックします。 ソリューションエクスプローラー上の「GVL_Var」をクリックして編集画面を開き,下図のようにグローバル変数を定義します。 GVL_Var {attribute 'qualified_only'} VAR_GLOBAL /// プログラム間共有データ SharedData : DINT; END_VAR --> Information 先頭行に記載されている波括弧の部分は,グローバル変数に対する属性(Attribute)です。 Attributeの詳細については下記のリンク先をご覧ください。 https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/2529567115.html&id= MAINプログラムでこのグローバル変数(SharedData)に,CycleCount変数の値を代入するようにします。 MAINプログラム 実装スペース CycleCount := CycleCount + 1; // 共有データに値を書き込む(追記部分) GVL_Var.SharedData := CycleCount; この値を,MAIN2プログラム内で別変数として受け取ってみます。 MAIN2プログラム 定義スペース PROGRAM MAIN2 VAR CycleDoubleCount : DINT; /// MAINプログラムのデータ MainProgramData : DINT; END_VAR MAIN2プログラム 定義スペース CycleDoubleCount := CycleDoubleCount + 1; // 共有データの値をローカル変数に格納する MainProgramData := GVL_Var.SharedData; このTwinCATプロジェクトを書き込み動作を確認してみましょう。 MAIN2プログラムのMainProgramData変数に,MAINプログラムのCycleCount変数の値が格納されていることが確認できます。 7. おわりに # 今回はST言語による基本的なPLCプログラムを作成してみました。 タスク・プログラムの使用方法が理解できたと思います。 ここまでのプロジェクトを こちら で共有しています。補助資料としてご活用ください。 次回は,ファンクションブロック(Function Block)を用いたPLCプログラムについて説明します。
やっと涼しくなってきました。弊社も新体制となり、Web サイトがリニューアルされました [1] 。🎊 https://mamezo.tech/ 今後ともよろしくお願いいたします。 それでは、2025年度第2四半期のサマリーです。 記事数・執筆者数 # この3ヶ月で40本の記事が投稿され、記事総数は809になりました。800本超えです。新たに5名が執筆デビューし、累計71名になりました。 テーマ別の記事 # プロジェクトマネージメント # 1Qで始まったプロジェクトマネージメントシリーズ。現場のプロセス改善の話題にも踏み込んだ記事が公開されています。 チェックリストの形骸化を防ぐ|デキるPMの再構築術と7つの改善策 形骸化しない定例会議の進め方|デキるPMの7つの改善ステップ 課題が消化されるリスト運用|デキるPMの脱・形骸化テクニック12選 因果関係図を活用した問題解決手法|現場改善に効くデキるPMの実践ステップ プロセス改善の実践ステップ|デキるPMが使うIDEALモデルと成功の秘訣 未来実現ツリー活用の中間目標で現場を動かす|デキるPMの改善計画術 変更管理の成功ガイド|デキるPMが実践する要件管理・構成管理・トレーサビリティ活用法 目的・目標・手段を区別する力 ─ 新人プロジェクトマネージャーが指揮官から学ぶ計画思考 .NET 系 # .NET C# 系の記事が増えました。豆蔵にも .NET 好きがけっこういます。 VS Codeで始める!わかる&できるC#開発環境の構築【2025年版マニュアル】 現場で迷わない!C#のLINQをサンプルコード付きで徹底攻略 C#とRazorで始める効率的なWeb開発!サンプルコード付きで徹底解説 【C#】WPFとMVVM「はじめの一歩」から現場Tipsまで! 〜デスクトップアプリ開発の実践メモ〜 C#とEntity Frameworkで生産性アップ!基本から実践まで徹底解説 ロボット # ロボットシステム開発の基盤技術が詳しく解説された記事が公開されました。 産業用ロボットの教示方法とその応用 生成AIを開発に活かす # コード生成だけでなく要件定義から設計書作成にもAIを活かす記事が公開されてます。 KiroでAI開発革命!? アルバムアプリをゼロから作ってみた【その1:要件定義・設計・実装計画】 最新LLMで“バイブコーディング”を実践(要件定義〜機能実装①) Kiroでアルバムアプリを作成する記事は全6本の連載となっており、 こちら から全部の記事を読めます。 LLM 関係 # LLM をローカルでホストする記事、LLM の仕組みに踏み込んだ記事などが公開されました。 「アテンションが全て」ではなかった?GPT2 small(124M)から学ぶLLMの仕組み クラウドに頼らないAI体験:LM Studioで始めるローカルLLM入門(Gemma 3) AWSで自分だけのLLM環境を!EC2 GPUインスタンスとOllamaでAIを動かす実践ガイド 夏のリレー連載2025開催 # 今年も夏のリレー連載が無事完了し10本の記事が公開されました。やはり生成AIの記事が多かったです。 夏のリレー連載2025 機械学習ページをリニューアル # 生成AIの記事が増えてきたので、従来の「機械学習」のページを「機械学習・生成AI」としてリニューアルし最新の記事を紹介しています。 機械学習・生成AI さいごに # 以上、2025年度第2四半期のサマリーでした。 よかったら フィード の購読、 X や Bluesky でのフォローもお願いします。 Facebook でも本サイトの注目記事をはじめ豆蔵に関するイベントを紹介しています。 note にも時々本サイト関連の記事が掲載されています。 社名の英字表記も微妙に変わりました。 ↩︎
はじめに # 豆蔵の GitHub オーガニゼーションもメンバーが増えて、多くのリポジトリを把握するのが困難になってきました。 新規のリポジトリ作成の内容をチェックすることも必要になってきました。機密性の高い情報を扱う場合もあるため、リポジトリの可視性が public になっていないかを確認することは重要です。 この記事では、オーガニゼーションのリポジトリ作成を通知する仕組みを構築しようと試行錯誤した内容をお届けします。 --> Information 豆蔵では Team プランで契約していますが、Enterprise プランならメンバーのリポジトリの作成を制限し、オーガニゼーションの管理者が依頼ベースで作成する運用も可能です。 開発者の自発的活動を阻害してしまうことにも繋がるため、個人的にはあまりこのような制約はかけたくはありませんが。 GitHub のイベント通知を Slack の Incoming Webhook で受ける(ダメ) # Slack には Incoming Webhook というアプリで Webhook 経由の通知を受け取る汎用的な仕組みがあります。 最初 GitHub から Incoming Webgook でイベント通知すればいいのでと考えました。そこで、通知したい Slack チャンネルに Incoming Webhook を導入。 GitHub の オーガニゼーションの Settings で Webhooks > Add webhook で設定します。 通知するイベントを選択するオプションを指定。 「Repositories」を指定すると、リポジトリの作成・アーカイブ・可視性変更といったイベントを通知できます。 Slack の Incoming Webhook の URL を指定して、設定を完了しました。 この設定をしてからしばらくして、同僚の人がリポジトリを作ったことを知りましたが、チャンネルには通知が来ていませんでした。 GitHub 側では、イベントを通知しようとしていましたが、失敗していました。ステータス400ということで、リクエストのデータが不正だったようです。 Response には missing_text_orfallback_or_attachments というメッセージが格納されています。 リクエストを見ると確かに text などのフィールドはありません。Incoming Webhook 用のメッセージに変換する中継サービスがないとダメそうです。ということで、GitHub の通知と Slack の Incoming Webhook を直接繋ぐのは無理でした。 GitHub の Slack アプリはどうか(ダメ) # Slack には GitHub 公式のアプリもあるので、これが使えないかと考えました。 GitHub アプリのリポジトリは以下です。 https://github.com/integrations/slack この README.md を読んだところ、リポジトリ単位ではなく、オーガニゼーション単位のサブスクライブも可能なようです。試しに、チャンネルから、オーガニゼーションにサブスクライブしてみました。 通知されるイベントはスクリーンショットで列挙されているものだけのようで、オーガニゼーション内のリポジトリの Issue や PR などに関するイベントしか通知されません。ということでこの方法も NG でした。 --> Information GitHub App を使ったリポジトリイベントの通知に関しては以下の記事で紹介しています。 /blogs/2022/12/12/notify-github-actions-workflow-to-slack/ GitHub Actions でリポジトリの作成日時から検出する # 最後の手段は、GitHub API と GitHub Actions ワークフローで定期的にチェックして Slack に通知を飛ばす方法です。リアルタイム性はないですが、1日1回程度通知されれば実用上は十分でしょう。 以前、オーガニゼーションのメンバーを把握するためのワークフローを設置したリポジトリに新たにワークフローを追加することにしました。 /blogs/2024/10/04/build-simple-github-org-admin-site/ この記事の時と同様、ランタイムは Bun、スクリプトは TypeScript を採用します。 ワークフローを JST で0時に起動して、GitHub の GraphQL でリポジトリの名前・URL・作成日時・可視性を取得し、作成日時が前日になっているものでフィルターするのがよさそうです。 以下のようなワークフローファイルを用意しました。 name: Notify New Repos to Slack on: schedule: - cron: '0 15 * * *' #1 workflow_dispatch: jobs: notify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 #2 - name: Install dependencies run: bun install --no-save #3 - name: Notify new repos to Slack env: GH_PAT: ${{ secrets.ORG_REPO_PAT }}. #4 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} #5 GITHUB_ORG: ${{ vars.ORG_NAME }} #6 run: bun run src/notify-new-repos.ts #7 各ステップの処理は以下のようになっています。 UTC の15時(JST の0時)に起動 Bun 環境をセットアップ bun install で octokit/graphql をインストール オーガニゼーションの参照権限を付与した PAT をシークレットから設定 Slack チャンネルの Incoming Webhook の URL をシークレットから設定 オーガニゼーション名を設定 Bun スクリプトを実行 実行される Bun スクリプトを抜粋します。 最初に環境変数を読み込んでおきます。 const org = process.env.GITHUB_ORG; const token = process.env.GH_PAT; const slackWebhook = process.env.SLACK_WEBHOOK_URL; GraphQL 部分。リポジトリを50件、作成日時(CREATED_AT)の降順(DESC)で取得するクエリーです。フィールドとして、リポジトリ名、URL、作成日時、可視性を取得しています。 const query = ` query($org: String!) { organization(login: $org) { repositories(first: 50, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { name url createdAt visibility } } } } `; Octokit を使って GraphQL でリポジトリのリストを取得する処理です。 let repos: { name: string; url: string; createdAt: string; visibility: string }[] = []; try { const data = await graphql<{ organization: { repositories: { nodes: typeof repos } } }>(query, { org, headers: { authorization: `token ${token}` } }); repos = data.organization.repositories.nodes; } catch (err) { console.error('GitHub GraphQL API error:', err); process.exit(1); } 1日前に作られたリポジトリを取得するため、JST の前日の日付を作成し、GraphQL で取得したリポジトリのリストの作成日時が前日になっているものを抽出します。 const now = new Date(); const JST_OFFSET = 9 * 60; const jstNow = new Date(now.getTime() + (JST_OFFSET - now.getTimezoneOffset()) * 60000); const yesterday = new Date(jstNow); yesterday.setDate(jstNow.getDate() - 1); const ymd = (d: Date) => d.toISOString().slice(0, 10); const yesterdayJSTDate = ymd(yesterday); const newRepos = repos.filter(r => { const created = new Date(r.createdAt); const jstCreated = new Date(created.getTime() + JST_OFFSET * 60000); return ymd(jstCreated) === yesterdayJSTDate; }); 最後に Slack の Webhook URL 向けにメッセージを作成し、送信します。 リポジトリが作成された場合、Slack への投稿に気づけるように @here メンションをつけています。これはメッセージに <!here> を含めることで実現できます。 let message: string; if (newRepos.length) { message = "<!here>\nList of newly created repositories:\n" + newRepos.map(r => `• <${r.url}|${r.name}> (created at ${r.createdAt.slice(0,10)} ${r.visibility})`).join('\n'); } else { message = "No new repositories were created yesterday."; } try { const resp = await fetch(slackWebhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: message }), }); if (!resp.ok) { console.error('Slack notification failed:', await resp.text()); process.exit(1); } console.log('Slack notification succeeded:', message); } catch (err) { console.error('Slack notification failed:', err); process.exit(1); } 以下のような感じで、リポジトリ作成通知がチャンネルに届きます(手動実行したため、時刻は午前10時30分ごろになっています)。 さいごに # 以上、オーガニゼーション内のリポジトリ作成を検知するために実施した方法の紹介でした。 やはり、Slack の GitHub アプリでリポジトリのライフサイクルイベントを通知してほしいところですね。
はじめに:SimulinkとArduinoで始めるS-Functionブロックの自作 # Arduinoで利用可能なデバイスは多岐にわたりますが、Simulinkで直接サポートされていないセンサーやディスプレイも数多く存在します。 そこで有効なのが S-Function を使った自作ブロックです。 本記事では、 OLEDディスプレイ SSD1306 を例に、Simulink用のS-Functionブロックを自作し、Arduinoで動作させる手順を紹介します。 開発環境の準備 # ソフトウェア MATLAB(バージョン:R2025a) Simulink(バージョン:25.1) アプリ(for Simulink) Simulink Support Package for Arduino Hardware(バージョン:25.1.0) ハードウェア Arduino Uno(または互換機) USBケーブル(PCとArduinoの通信用) HC-SR04(超音波距離センサ) OLED SSD1306(I2C接続ディスプレイ) 環境構築の詳細手順: # 1. MATLAB/Simulink と 基本パッケージの導入 MATLAB と Simulink、Arduino Support Package の導入については、 前回の記事 を参照してください。 2. Rensselaer Arduino Support Package Library を導入 MATLAB を起動 → メニューから 「アドオン」 → 「ハードウェアサポートパッケージの入手」 を選択します(アドオンエクスプローラーが起動します) 検索ボックスで 「Arduino」 と入力し、 Rensselaer Arduino Support Package Library (RASPLib) を選択します Install をクリックし、パッケージを導入します 導入完了後、Simulinkライブラリに「Rensselaer Arduino Support Package Library」のブロックが追加されます ↓ その中に「超音波距離センサ HC-SR04」用のブロックがあります。HC-SR04を動かすために、このブロックを使用します。 回路の接続 # HC-SR04 # VCC → Arduino 5V GND → Arduino GND Trig → Arduino デジタルピン 7(例) Echo → Arduino デジタルピン 8(例) OLED SSD1306 (I2C) # VCC → Arduino 3.3V または 5V GND → Arduino GND SCL → Arduino A5 (Unoの場合) SDA → Arduino A4 (Unoの場合) SSD1306用のS-Functionブロックの作成 # HC-SR04はRASPLibでサポートされていますが、SSD1306については専用ブロックが見つからなかったため、S-Functionブロックで自作します。 ここで重要なのは、 Simulinkで直接サポートされていない機能はArduinoライブラリを呼び出して補う という点です。 ArduinoのライブラリはC/C++で書かれたデバイス制御用関数群であり、S-Functionを介してSimulinkモデルから呼び出すことが可能です。 これにより、Simulinkでサポート外のデバイスでも、 Arduinoの豊富なライブラリエコシステムを活用して動作させることができる のです。 ArduinoのOLED SSD1306用ライブラリ選定 # Arduino向けOLEDライブラリとして有名なのは以下です。 Adafruit_SSD1306 Adafruit-GFX-Library ただし、UnoのFlashに収まらない場合があるため、より軽量な U8g2_Arduino を採用します。 U8g2は多機能ですが、Unoでのメモリ制約を考慮し、今回は U8x8テキストモードのみ を利用します。 プロジェクトの作成 # S-Function Builderは単体でも使えますが、複数のS-Functionを扱う場合は Simulinkプロジェクト機能 を利用すると便利です。 新しいプロジェクトを作成 MATLABの「新規 → プロジェクト → 空のプロジェクト」を選択します 保存先フォルダが「プロジェクトフォルダ」となります この方法を取れば、複数のデバイスを扱う場合でも整理しやすくなり、再利用性も向上します。 ライブラリの作成 # 新しいSimulinkライブラリを作成 MATLABを起動し、 simulink コマンドでSimulinkライブラリブラウザを開く メニューから「新規 → ライブラリ」を選択し、空のライブラリファイルを作成 ここに自作ブロックを追加していきます S-Function Builderブロックの配置 新しいライブラリに「S-Function Builder」ブロックを配置 生成される .cpp ファイルや .tlc ファイルがライブラリと連動するように管理されます。 外部ライブラリの準備 Arduinoで利用する Wire.h や U8x8lib.h がインクルードできるように準備 U8g2_Arduino を C:\ProgramData\MATLAB\thirdpartylibs\U8g2_Arduino などに配置 環境に応じて Support Package のパスも確認しておきます S-Function Builder でブロックを自作 # 今回は、仕様を簡単にし、少ないメモリでも動作する設計にします。 U8g2 の U8x8 テキストモードで、SSD1306 128×64 (I²C) に1行のASCII文字列(最大16文字)を表示する仕様です。 プロジェクト名: sfun_ssd1306_u8x8_display_block S-Function名: sfun_ssd1306_u8x8_display 言語:C++ ソースコード: /* Includes_BEGIN */ #ifndef MATLAB_MEX_FILE #include <Arduino.h> #include <Wire.h> #include <U8x8lib.h> // SSD1306 128x64 via hardware I2C, no reset pin (UNO: SCL=A5, SDA=A4) static U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset = */ U8X8_PIN_NONE); static bool isDisplayInitialized_u8x8 = false; #endif /* Includes_END */ /* Externs_BEGIN */ /* extern double func(double a); */ /* Externs_END */ void sfun_ssd1306_u8x8_display_Start_wrapper(void) { /* Start_BEGIN */ #if !defined(MATLAB_MEX_FILE) if (!isDisplayInitialized_u8x8) { u8x8.begin(); u8x8.setPowerSave(0); // 文字フォント(ASCII用の軽量フォント) u8x8.setFont(u8x8_font_chroma48medium8_r); // 必要ならI2Cアドレス指定(一般的な0x3Cを8bit表現で) // u8x8.setI2CAddress(0x3C << 1); // 何も出ない時だけ試す u8x8.clearDisplay(); isDisplayInitialized_u8x8 = true; } #endif /* Start_END */ } void sfun_ssd1306_u8x8_display_Outputs_wrapper(const uint8_T *u) { /* Output_BEGIN */ #if !defined(MATLAB_MEX_FILE) if (!isDisplayInitialized_u8x8) return; // 入力ベクトル長に合わせて調整 const uint8_t MAX_STR = 16; char buf[MAX_STR + 1]; uint8_t i = 0; for (; i < MAX_STR; ++i) { uint8_t b = u[i]; buf[i] = (char)b; if (b == 0) break; } buf[(i < MAX_STR) ? i : MAX_STR] = '\0'; // 1行目(行=0)にASCII文字列を表示 u8x8.clearLine(0); u8x8.drawString(0, 0, buf); #endif /* Output_END */ } void sfun_ssd1306_u8x8_display_Terminate_wrapper(void) { /* Terminate_BEGIN */ #if !defined(MATLAB_MEX_FILE) // nothing #endif /* Terminate_END */ } 端子とパラメーター: 外部コード: 必要な「U8g2_Arduino」を以下のパスにGitでCloneしておきます。 C:\ProgramData\MATLAB\thirdpartylibs\U8g2_Arduino Arduino Support Package のパスは以下になっていました。(MATLABをインストールした環境に依存しますので、皆さんの環境では、Support Packageのパスを確認してください) C:\ProgramData\MATLAB\SupportPackages\R2025a\aCLI\data\packages\arduino ビルドとライブラリ登録 # S-Functionをビルドすると、以下のファイルが生成されます: .cpp (本体ソース) _wrapper.cpp (ラッパコード) .tlc (ターゲット言語コンパイラ用) .mexw64 (Windows用バイナリ) ### Output folder is 'C:\Users\<ユーザ名>\Documents\MATLAB\sfun_ssd1306_u8x8_display_block' ### 'sfun_ssd1306_u8x8_display.cpp' は正常に作成されました ### 'sfun_ssd1306_u8x8_display_wrapper.cpp' は正常に作成されました ### 'sfun_ssd1306_u8x8_display.tlc' は正常に作成されました ### S-Function 'sfun_ssd1306_u8x8_display.mexw64' が正常に作成にされました さらに、ライブラリブラウザに登録するには以下のファイルを用意します: slblocks.m (必須) setup.m (推奨) INSTALL.m (任意)※プロジェクトを配布するときに便利です slblocks.m function blkStruct = slblocks % この関数は、指定したライブラリを % Simulinkライブラリブラウザに表示するために定義します。 % --- ライブラリの登録情報 --- % Browser.Library には、ライブラリのファイル名(拡張子なし)を指定します。 Browser.Library = 'ssd1306_u8x8_display_lib'; % Browser.Name には、ライブラリブラウザに表示したい名前を指定します。 Browser.Name = 'Arduino SSD1306 U8x8 display library'; % --- 構造体にまとめる --- blkStruct.Browser = Browser; end setup.m function setup addpath(fileparts(mfilename('fullpath'))); % #ok<MCAP> 自フォルダをPATHへ try lb = LibraryBrowser.LibraryBrowser2; refresh(lb); catch sl_refresh_customizations; end % 依存チェック(例:Arduinoサポート) % assert(exist('arduino','file')~=0, 'Install MATLAB Support Package for Arduino'); end INSTALL.m %% Add library to path addpath(pwd); savepath; %% Refresh library browser lb = LibraryBrowser.LibraryBrowser2; refresh(lb); パスの設定でパスを登録します。 登録が成功すると、Simulinkライブラリに自作のSSD1306用ブロックが追加されます。 メインのSimulinkモデルの作成 # 新規モデルを作成 # Simulink を起動し、新しい「空のモデル」を作成します ブロックライブラリから以下を配置します: HC-SR04ブロック(RASPLib) SSD1306表示用S-Functionブロック(自作) 文字列処理ブロック(定数文字列、数値⇒文字列変換、文字列結合、文字列⇒ASCII変換) 表示用ブロック(デバッグ確認) パラメータ設定 # 「ハードウェア設定」-「ハードウェア実行」を以下のように設定します。 ハードウェア実行 Arduinoへの書き込みと実行 # プログラムをArduinoにアップロードし、自動実行できるようにします。 Simulink の「ビルド、展開起動」を実行します コンパイル → Arduinoへ転送  転送が成功すると、以下のログが出力されます。 超音波距離センサで計測した物体との距離がOLED上に表示されるようになりました。 (少し数値が読み取りづらいですが、7(cm)くらいの距離に障害物を置いています) 実行結果と考察 # HC-SR04で取得した距離を即座にOLEDに表示でき、センサとディスプレイをSimulink経由で統合できました Unoのメモリ制約によりU8g2のフルバッファ機能は利用困難ですが、U8x8モードは軽量かつ実用的です 今回の構成で センサ入力 → 文字列変換 → ディスプレイ出力 を直感的に構築できました 今後は 行位置・列位置の可変表示 や フォント切替 、 I²Cアドレス指定 などのパラメータ化を進めることで、より汎用的なブロックに発展させられます まとめ # HC-SR04 は RASPLib を利用すればSimulink上で簡単に利用可能です。 SSD1306 は自作S-Functionを作ることで表示機能を拡張できます。 U8g2ライブラリ を活用し、メモリ制約に配慮してU8x8モードを利用するのが実用的です。 プロジェクト機能+slblocks/setup/INSTALLを組み合わせれば、ライブラリとして管理・配布も容易です。 今回の取り組みにより、Arduinoを用いた 複数デバイス統合の一例 を示すことができました。 将来的には他のセンサやアクチュエータにも同様の方法を展開し、ライブラリを充実させることで、モデルベース開発の適用範囲をさらに広げられます。 img { border: 1px gray solid; }
はじめに # 近年、大規模言語モデル(LLM)をローカル環境で動作させるツールが充実してきました。 その中でも LM Studio は、ユーザーが手軽にLLMを試せるアプリケーションとして注目されています。 今回は、LM Studio を使って Gemma LLM を動作させる手順と、基本的な使い方を紹介します。 LM Studio とは # LM Studio は、ローカル環境で大規模言語モデル(LLM)を手軽に動かせるように設計されたアプリケーションです。 専門的な設定やコマンドライン操作を必要とせず、 インストール後すぐにモデルを実行できる手軽さ が特徴です。 また、クロスプラットフォームに対応しており、Windows / macOS / Linux いずれの環境でも利用でき、研究開発から個人学習まで幅広く使われています。 代表的な特徴は以下の通りです。 クロスプラットフォームで動作するLLM実行環境 GUIベースで簡単にモデルを切り替え・実行可能 Chat UI とコード生成支援の両方に対応 Hugging Face や独自モデルをインポートして利用できる つまり、LM Studio は「LLMを試すための実験場」であると同時に、「日常的に使える対話AIの実行環境」としても利用できます。 Gemma LLM とは # Gemma は、Google DeepMind が開発した最新の大規模言語モデルです。 研究者や開発者がローカル環境で安全に利用できるように設計されており、特に「軽量で効率的に動作する」という点に大きな特徴があります。 Gemma はクラウド環境に依存せず、自分のPC上で直接実行できるため、 データプライバシーを確保しながらAIを活用できる のも魅力です。 また、オープンモデルとして公開されているため、誰でも自由に試したり改良したりでき、コミュニティによる拡張も期待されています。 主な特徴は以下の通りです。 Google DeepMind が開発した大規模言語モデル 軽量でローカル実行に最適化された設計 オープンモデルとして Hugging Face に公開されている 自然言語理解やコード補完など幅広い用途に利用可能 ただのテキストLLMではなく、画像理解もできるマルチモーダルモデル Gemma は単なる「軽いLLM」ではなく、 最新の研究成果を活かしつつ、開発者が自由に使える実験環境 として位置付けられています。 これにより、研究用のプロトタイピングから個人開発まで、幅広いユースケースに適用できます。 なぜローカルLLMの需要が高まっているのか? # クラウドベースのAIサービスが普及する一方で、 ローカル環境でLLMを実行するニーズが急速に高まっています 。その背景には、以下のような要因が挙げられます。 データプライバシーの確保 機密情報や個人データを外部サーバーに送信せずに済むため、安心して利用できる。 オフライン環境での利用 インターネット接続が不安定な場所でも、ローカルLLMなら安定して動作可能。 低コストでの実行 API利用料を気にせず、PCのリソースを活かして繰り返し実験できる。 カスタマイズ性の高さ 特定のドメインデータで再学習やファインチューニングが可能。 レイテンシの低減 サーバー通信を挟まないため、応答速度が向上する。 これらの理由から、 研究用途だけでなく、個人開発・教育現場・企業内利用に至るまで、ローカルLLMの導入が加速している のです。 環境構築の手順 # LM Studio を 公式サイト からダウンロード・インストールします。 2025-09-20現在、最新版は「0.3.26」でした。 実行ファイルをダウンロードして、インストーラを起動します。 「Get Started」をクリックします。 LM Studio のインストール時に表示される「Choose your level」は、ユーザーの経験や利用目的に合わせて、UIの見せ方や初期設定の範囲を調整するための選択肢です。 「Choose your level」では「Power User」を選択し、Continueを押します。 --> Information LM Studio のユーザーレベルは以下のように分類されているようです。 レベル 想定ユーザー 特徴 User 初めてAI/LLMを使う人 - 最低限の設定だけで利用可能 - UIはシンプル - 余計なパラメータは非表示 Power User LLMに慣れてきた中級者 - モデル切り替えや生成設定が可能 - UIはシンプルさを保ちつつ調整機能あり - 細かい設定もある程度可能 Developer 開発者・研究者 - 全ての設定・機能にアクセス可能 - API連携やログ詳細、カスタムモデル管理も利用可能 - ツール開発や高度な利用に最適 最初のモデルをダウンロードします(モデルサイズが20Bと大きいので、ローカルPCのメモリに余裕がないと動作しないため、ご自身の環境に合わせてダウンロードしてください) ダウンロードが始まります(サイズが大きいの時間がかかります) ダウンロード終了後、「Start New Chat」を押します。 LM Studio が起動します。(この時点ではまだモデルは読み込まれていません) 環境を「日本語」に設定します。 アプリケーション右上の「外観」をクリックし、選択項目から「View more settings」を選択します。 「Language」を「日本語」に設定します。 UIが日本語に変更されました。 Gemma LLM を探索・ダウンロード 「探索」-「Model Search」で「Gemma 3 4B」を検索し、ダウンロードします。(ご自身のPCスペックに合わせてサイズを選択してください) --> Information モデルサイズとPCメモリの目安は以下です。 モデルサイズ 推奨RAM (CPU実行) 推奨VRAM (GPU実行) 備考 1B (~1Bパラメータ) 8GB以上 4GB以上 軽量。ノートPCでも動作可能 2B 16GB以上 6〜8GB以上 ローカル実行の入門サイズ 4B 16〜24GB以上 10〜12GB以上 実用的な精度と軽さのバランス 9B 32GB以上 16GB以上 高精度。ハイエンドGPUが望ましい 12B 48GB以上 24GB以上 研究用途向け。個人PCでは厳しい場合あり 27B 64GB以上 48GB以上 本格的な開発・研究用。専用サーバー推奨 E4B (Efficient 4B) 16GB以上 8〜10GB以上 省メモリ版。軽量化技術で4B相当より動かしやすい ダウンロード完了後に、モデルを読み込みます。 モデルが読み込まれました。 実際に使ってみる # Chatで簡単な質問をしてみます。 LLMに「日本の昔話の桃太郎について教えてください」と問いかけます。 出力結果は以下のようなものでした。 桃太郎は、日本で最も有名な昔話の一つです。その物語には様々なバージョンがありますが、基本的な流れは以下の通りです。 **物語の概要** * **始まり:** 桃太郎は、ある村に住む貧しい少年でした。ある日、大きな桃を拾って家に持ち帰ると、桃を開けるとそこには元気な男の子がいました。 * **修行:** 桃太郎は、その子と二人で様々な場所を旅して修行をしました。熊や猿、雉といった動物たちに芸を教えたり、おじいさんやおばあさんを助けたりします。 * **鬼ヶ島へ:** 修行が終わった後、桃太郎は鬼ヶ島(鬼が住んでいる島)に行って、鬼退治をする決意をします。 * **準備:** 桃太郎は、犬、猿、雉を家来にして、船を作って鬼ヶ島へと向かいます。犬は「マー坊」、猿は「モンキー」、雉は「コケコッコー」という名前が付けられています。 * **鬼退治:** 鬼ヶ島に到着した桃太郎は、鬼たちと戦い、鬼を全て倒してしまいます。鬼の首を切り取って持ち帰ります。 * **宝物:** 鬼ヶ島には、金銀財宝や美しい宝石などがたくさん隠されていました。桃太郎は、その宝物を持ち帰り、村に貢献します。 **物語のテーマと象徴** * **勇気と正義感:** 困難に立ち向かい、悪を打ち倒す桃太郎の姿は、勇気と正義感の象徴として描かれています。 * **成長と自己実現:** 貧しい少年から、家来を率いる英雄へと成長していく桃太郎の物語は、自己実現や夢の実現といったテーマを示唆しています。 * **多様性の尊重:** 犬、猿、雉といった異なる動物たちを家来にするという点は、多様性を尊重し、協力することの大切さを伝えています。 **様々なバージョン** 桃太郎の物語には、地域や時代によって様々なバリエーションがあります。例えば、鬼ヶ島までの道のりや、鬼との戦い方、宝物の種類などが異なる場合があります。また、桃太郎の性格や家来たちの役割も、物語によって異なっています。 **桃太郎の起源** 桃太郎の物語は、中国の伝説「桃木爺」に由来すると言われています。「桃木爺」は、桃の木を育てた老人が、鬼退治をするという物語です。この物語が日本に伝来し、日本の昔話として発展したと考えられています。 **その他** * 桃太郎は、子供たちの間で人気のあるキャラクターであり、アニメや漫画、映画など様々な形で表現されています。 * 桃太郎の物語は、日本の文化や価値観を反映しており、現代においても多くの人々に愛され続けています。 より詳しく知りたい場合は、以下のキーワードで検索してみてください。 * 桃太郎 昔話 * 桃太郎 中国伝説 * 桃太郎 キャラクター また、以下のようなサイトも参考になるでしょう。 * [https://kotobazaka.jp/kitaro-story/mototarou/](https://kotobazaka.jp/kitaro-story/mototarou/) * [https://www.city.fukuoka.lg.jp/culture/bunka/shumon/shomon_06_03.html](https://www.city.fukuoka.lg.jp/culture/bunka/shumon/shomon_06_03.html) 何か他に知りたいことや、特定の質問があれば、お気軽にお尋ねください。 言いたいことはわかりますが、時々変な解釈や拡張が見られますね。 桃太郎が桃を持ち帰ってくるなどの派生版も存在したのでしょうか(笑) (他のLLMも色々と試してみましたが、猫が桃太郎の家来になっていたバージョンもありました) コード生成を試してみます(Pythonスニペットなど) 簡単な例です。 少し高度にしてみましょう。 期待以上に賢く応答しています。 画像をインプットしてみます 我が家の愛猫について まずまずな評価のようです。(愛猫の模様は”キジ白”なのですが、単一の写真だけでは正確には判断が難しいのでしょう) まとめ # 今回、LM Studioで Gemma を試した結果、次のような知見を得ました。 LM Studio は、LLMをローカル環境で動かすためのハードルを大幅に下げ、初心者から上級者まで使いやすい実行環境を提供しています。 Gemma LLM は、軽量ながらも幅広いタスクに対応でき、研究・学習からプロトタイピングまで十分に活用可能です。 ローカルLLMの利点(プライバシー確保・低コスト・オフライン利用・高速応答)を体感でき、クラウド依存では難しいユースケースに有効です。 さらに、今後の展望としては以下が挙げられます。 モデルのパーソナライズ 個人や組織ごとのデータで調整することで、より実用的な応用が期待できます。 開発環境への統合 エディタやIDEとの連携を強化すれば、コード補完やデバッグ支援ツールとしての価値が高まります。  (LM Studio には「ローカルAPIモード」があり、HTTP経由でリクエストを送れます) 複数モデルの比較活用 Gemma 以外のLLMと並行利用することで、用途ごとに最適な選択が可能になります。 img { border: 1px gray solid; }
はじめに:SimulinkとArduinoで始める「Lチカ」 # 「Lチカ」(LEDの点滅) は、ハードウェア制御の入門として最も基本的な実験です。 本記事では、 MATLAB/SimulinkとArduinoを連携させ 、LEDを点滅させるプログラムの作成方法を解説します。 開発環境の準備 # ソフトウェア MATLAB(バージョン:R2025a) Simulink(バージョン:25.1) アプリ(for Simulink) Simulink Support Package for Arduino Hardware(バージョン:25.1.0) ハードウェア Arduino Uno/Nano(または互換機) USBケーブル(PCとArduinoの通信用) (オプション) LED + 抵抗(330Ω程度) ブレッドボード、ジャンパワイヤ 環境構築の詳細手順: # 1. MATLAB と Simulink をインストール MathWorks の公式サイトからインストーラをダウンロードします ライセンス認証を行い、MATLAB と Simulink をインストールします インストール時に「Simulink」コンポーネントを忘れずにチェックします 2. Arduino Support Package を導入 MATLAB を起動 → メニューから 「アドオン」 → 「ハードウェアサポートパッケージの入手」 を選択します(アドオンエクスプローラーが起動します) 検索ボックスで 「Arduino」 と入力し、 Simulink Support Package for Arduino Hardware を選択します Install をクリックし、パッケージを導入します 導入完了後、Simulinkライブラリに「Simulink Support Package for Arduino Hardware」のブロックが追加されます 3. Arduino のシリアル通信確認 ArduinoボードをUSBでPCに接続すると、自動的にドライバが認識されます 認識されない場合は、デバイスマネージャ(Windowsの場合)や ls /dev/tty* (Mac/Linuxの場合)でポートを確認します(※下図はCOM7に接続した例) 回路の接続 # Arduino の 13番ピン と GND に LED + 抵抗を接続します。 (内蔵LEDを使う場合は外部配線は不要です) 回路図イメージ: (Arduino 13) ----[抵抗330Ω]----|>|(LED)---- (GND) Simulinkモデルの作成 # 新規モデルを作成 # Simulink を起動し、新しい「空のモデル」を作成します ブロックライブラリから以下を配置します: Pulse Generator(パルス波形を生成) Digital Output(Arduinoのピン出力):このブロックは「Simulink Support Package for Arduino Hardware」に含まれています。 配置したブロックを接続します: パラメータ設定 # 先ほど配置したブロックのパラメータを設定します。 Pulse Generator パルスタイプ:「サンプルベース」 時間:「シミュレーション時間を使用」 振幅:1 周期(サンプル数):1000 パルス幅(サンプル数):500 位相遅延(サンプル数):0 サンプル時間:0.001 ベクトルパラメータを1次元として解釈:チェックON Digital Output Pin番号:13 --> Information サンプル時間が「0.001」(秒)で、サンプル周期が「1000」なので、周期(時間)は「1(秒)」になります。 パルス幅を「500」にしているので、この設定では500ミリ秒毎にLEDがON/OFFを繰り返します。 モデル設定 # ハードウェア実行を以下のように設定します。 (今回、Arduino Nano互換機を使用しました。互換機のブートローダーが旧版だったため、アプリケーションダウンロードのボーレートが低くなっています) ハードウェア実行 パルスジェネレータの出力確認 # Arduinoにアプリケーションをアップロードする前に、パルスが正しく出力されているか確認します。 信号のログを設定します(接続線をクリックし、「信号のログ」を設定) ↓ 信号のログが設定されたことをアイコンで確認できます シミュレーションを実行します データインスペクタでパルス波形を確認できます 監視と調整(USBポート経由での実行確認) # USB経由でArduino Nanoにプログラムを転送し、正しくプログラムが動くか確認します。 ハードウェアタブから「監視と調整」を実行します(終了時間には「inf」を設定します。停止操作をするまで実行を続けます) Arduino の内蔵LEDが1秒周期で点滅すればモデルは正しく実行されています Arduinoへの書き込みと実行 # プログラムをArduinoにアップロードし、自動実行できるようにします。 Simulink の「ビルド、展開起動」を実行します コンパイル → Arduinoへ転送  転送が成功すると、以下のログが出力されます。  LEDが1秒ごとに点滅すれば、転送は成功です。 実行結果と考察 # わずか2つのブロックを接続するだけで、簡単にLED点滅プログラムを作成できました。 周期を短くすると「高速点滅」します(例えば、周期を100、パルス幅を50 など)。 パルス幅(Duty比)を変更することで、 明るさの制御(PWMの基本) にも応用できます。 まとめ # MATLAB/SimulinkとArduinoを組み合わせることで、 ブロック線図ベースで直感的に制御プログラムを開発できる ことがわかりました。 LED点滅は単純な例ですが、PWM制御・センサ入力・モータ制御などへ拡張可能です。 ただLEDを点滅させるだけだったら、Arduino IDEやPlatformIOなどの開発環境でプログラミングした方が早いと思いますが、今後複雑なプログラミングをしていく上で、MATLAB/SimulinkはMBD開発の強力なツールになると感じました。 今後は、自作ライブラリを作ったり、高度な周辺機器をつないで細かいプログラミングに挑戦していきたいと思います。 img { border: 1px gray solid; }
C#で開発する場合、ORマッパーはEntity Frameworkが定番です。Entity Frameworkは理解が多少難しい点は否めないですが、開発効率が高いという特徴があります。 さらにはASP.NET Coreの時代となってから、Entity FrameworkもEntity Framework Coreとなり、さらに開発効率が上がりました。 今回の記事を書くにあたって久々にEntity Frameworkを使ってみたのですが、驚くほど簡単に開発できると実感しました。これなら生産性も大幅に向上しそうです。 そんなわけでEntity Frameworkのセットアップから使い方まで、サンプルコードとともに解説します。 Entity Frameworkの使い方を知りたい方はもちろん、開発プロジェクトにEntity Frameworkの導入を検討している方にも参考になれば幸いです。 Entity Frameworkの概要と環境構築 # Entity FrameworkはORマッパーです。C#を使用してSQL ServerやSQLite、MySQL、PostgreSQL、Azure Cosmos DBなどにアクセスできます。 一般的なORマッパー同様に、DBのテーブルに対応するモデルクラスを作成し、Entity Frameworkが用意しているSelectやInsertなどのメソッドを使用してDBを操作します。 Entity Frameworkのインストール # Entity Frameworkを使用するためには、インストールを行う必要があります。 Visual Studioをお使いの場合はNuGetから以下をインストールしてください。なお今回のサンプルではSQL Server Expressを使用しております。 EntityFrameworkCore EntityFrameworkCore.SqlServer EntityFrameworkCore.Tools EntityFrameworkCoreをインストールしてください。Coreじゃない方のEntityFrameworkは.NET v4.x用です。 VSCodeをお使いの場合は、プロジェクトのフォルダに移動してから以下のコマンドを打ってインストールしてください。 dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools dotnetコマンドが見つからないなど、VSCodeでC#の開発環境ができていない場合は、こちらの記事を参考に環境構築してください。 VS Codeで始める!わかる&できるC#開発環境の構築【2025年版マニュアル】 SQL Server Expressのインストール # 今回のサンプルではSQL Server Expressを使用します。SQL Serverをインストールしていない方はインストールしてください。Microsoftのダウンロードサイトはこちらです。 https://www.microsoft.com/ja-jp/download/details.aspx?id=104781 続いてSQL Server Management Studioをインストールします。Microsoftのダウンロードサイトはこちらです。 https://learn.microsoft.com/ja-jp/ssms/install/install Management Studioをインストールしたらログインしてみましょう。 サーバ名はlocalhost\sqlexpress、認証の種類はWindows認証、サーバ証明書を信用するにチェックを付けてください。 --> Information 補足ですがSQL Serverの認証方式は3種類あります。 1つ目がWindowsのユーザでログインするWindows認証、2つ目がDBに登録したユーザとパスワードでログインする一般的な方式であるSQL Server認証、3つ目が両者を使う混合認証です。 ローカルでの開発ならWindows認証が簡単です。 データとDBアクセス処理の準備 # この記事で扱うサンプルデータ # この記事では定食屋のメニューをサンプルデータとして扱います。突っ込みどころの多いデータですが、サンプルですのでご容赦ください。 CSVデータを掲載します。 menu.csv menu_id,menu_name,price 1,焼き魚定食,1000 2,唐揚げ定食,900 3,刺身定食,1200 4,天ぷら定食,1100 5,アジフライ定食,1100 menu_item.csv menu_id,menu_item_id,menu_item_name 1,1,ご飯 1,2,みそ汁 1,3,鮭の塩焼き 1,4,漬物 2,1,ご飯 2,2,みそ汁 2,3,鳥の唐揚げ 2,4,サラダ 3,1,ご飯 3,2,みそ汁 3,3,刺身 3,4,漬物 4,1,ご飯 4,2,みそ汁 4,3,天ぷら 4,4,漬物 5,1,ご飯 5,2,みそ汁 5,3,アジフライ 5,4,サラダ サンプルデータのDDL # サンプルデータを投入するテーブルを作成するDDLは以下です。 DDL.sql create table menu ( menu_id int not null primary key, menu_name nvarchar(50), price decimal(5,0) ); create table menu_item ( menu_id int not null, menu_item_id int not null, menu_item_name nvarchar(50), constraint PK_menu_item primary key clustered(menu_id, menu_item_id) ); サンプルデータを投入するSQLは以下です。 insert_date.sql -- menu insert into menu values (1, '焼き魚定食', 1000); insert into menu values (2, '唐揚げ定食', 900); insert into menu values (3, '刺身定食', 1200); insert into menu values (4, '天ぷら定食', 1100); insert into menu values (5, 'アジフライ定食', 1100); -- menu_item insert into menu_item values (1, 1, 'ご飯'); insert into menu_item values (1, 2, 'みそ汁'); insert into menu_item values (1, 3, '鮭の塩焼き'); insert into menu_item values (1, 4, '漬物'); insert into menu_item values (2, 1, 'ご飯'); insert into menu_item values (2, 2, 'みそ汁'); insert into menu_item values (2, 3, '鳥の唐揚げ'); insert into menu_item values (2, 4, 'サラダ'); insert into menu_item values (3, 1, 'ご飯'); insert into menu_item values (3, 2, 'みそ汁'); insert into menu_item values (3, 3, '刺身'); insert into menu_item values (3, 4, '漬物'); insert into menu_item values (4, 1, 'ご飯'); insert into menu_item values (4, 2, 'みそ汁'); insert into menu_item values (4, 3, '天ぷら'); insert into menu_item values (4, 4, '漬物'); insert into menu_item values (5, 1, 'ご飯'); insert into menu_item values (5, 2, 'みそ汁'); insert into menu_item values (5, 3, 'アジフライ'); insert into menu_item values (5, 4, 'サラダ'); Dbコンテキストとモデル # プロジェクト直下に Models というフォルダを作成し、その中にDbコンテキストとモデルを配置する想定で進めます。Dbコンテキスト、モデルともにcsファイルとして作成すればよいです。 コードを掲載します。 SampleContext.cs using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Models { internal class SampleContext : DbContext { public SampleContext() { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 接続文字列を設定 // Trusted_Connection=TrueはWindows認証を使用するための設定 // localhostで開発するならこれが楽 optionsBuilder.UseSqlServer("Server=localhost\\SQLEXPRESS;Database=SampleDB;Trusted_Connection=True;TrustServerCertificate=Yes;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 複合主キーを使う場合、ここでキー項目を記述しないとデータを正しく取得できない(0件になるなど) modelBuilder.Entity<MenuItem>().HasKey(mi => new { mi.MenuId, mi.MenuItemId }); } // アクセスしたいテーブルの分だけモデルを記述する public DbSet<Menu> Menu { get; set; } public DbSet<MenuItem> MenuItem { get; set; } } } ここではローカル環境での動作確認を前提として、Windows認証を使うように接続文字列に記述しています。 複合主キーを使うテーブルがある場合、 OnModelCreating メソッドにキー項目を記述します。こうしないと複合主キーであることをEntity Frameworkが理解できないため、データを正しく取得できないので気を付けてください。 Dbコンテキストの使い方についても解説します。 DBアクセス処理を記述するクラスに以下のように記述して使います。Dbコンテキストからモデルにアクセスすることで、CRUD操作をします。 internal class MenuRepository { SampleContext _context = new SampleContext(); public IList<Menu> SelectMenus() { // Includeメソッドを使えば、関連するテーブルのデータも一緒に取得できる return _context.Menu.Include(menu => menu.MenuItemList) .ToList(); } } 続いてモデルのコードを掲載します。 Menu.cs using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Models { internal class Menu { [Column("menu_id")] public int MenuId { get; set; } [Column("menu_name")] public string MenuName { get; set; } = string.Empty; public Decimal Price { get; set; } public IList<MenuItem> MenuItemList { get; set; } = new List<MenuItem>(); } } MenuItem.cs using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Models { [Table("menu_item")] internal class MenuItem { [Column("menu_id")] public int MenuId { get; set; } [Column("menu_item_id")] public int MenuItemId { get; set; } [Column("menu_item_name")] public string MenuItemName { get; set; } = string.Empty; public Menu Menu { get; set; } = new Menu(); } } ここで1つ注意点を解説しておきます。 実はEntity FrameworkがDBのテーブルとモデルをマッピングするとき、モデルのクラス名ではなくDbコンテキストに書かれたプロパティ名でマッピングしています。 サンプルコードで解説すると以下のようになります。 SampleContext.csを一部抜粋 // これはmenuという名前のテーブルとマッピングされる public DbSet<Menu> Menu { get; set; } // これはmenuitemという名前のテーブルとマッピングされる public DbSet<MenuItem> MenuItem { get; set; } // これはmenu_itemという名前のテーブルとマッピングされる(本当はプロパティにアンダースコアを入れることはNG) public DbSet<MenuItem> Menu_Item { get; set; } テーブル名にアンダースコアを含まない場合、例えばテーブル名が menu でモデル名が Menu の場合は、プロパティ名も Menu にするでしょうから問題になりにくいです。 しかし menu_item テーブルのように、名前にアンダースコアを含むテーブルは注意が必要です。プロパティ名を MenuItem にしてしまうと「オブジェクト名'MenuItem'が無効です」のような実行時例外が出ます。 Entity Frameworkが menuitem という名前のテーブルを探してしまうのです。 プロパティ名にアンダースコアを入れて Menu_Item にすれば、例外は発生しなくなります。 しかしC#の命名規則ではプロパティ名にアンダースコアを入れることがNGですので、 Table アノテーションを使って、テーブル名を指定する必要があります。 Tableアノテーションを使う例 [Table("menu_item")] internal class MenuItem 基本的な操作 # Select処理 # 最初にSelect処理からやっていきましょう。 まずはDBからデータを取得する処理を作成しましょう。プロジェクト直下に Repositories というフォルダを作成し、 MenuRepository.cs というDBアクセス処理を作成する想定で進めます。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public IList<Menu> SelectMenus() { // Includeメソッドを使えば、関連するテーブルのデータも一緒に取得できる return _context.Menu.Include(menu => menu.MenuItemList) .ToList(); } } } ここで1つ補足しておきます。テーブルの結合が必要な場合、LINQでJOINを行ってもよいのですが、 Include メソッドを使う方が圧倒的に楽です。これ1つで結合先のテーブルのキーが一致するデータを取得してくれます。この楽さは衝撃的です。 Include メソッドで結合先テーブルの値を正しく取得できない場合は、以下の個所に記述ミスや記述漏れがないか確認してください。 モデルの Table アノテーション モデルの Column アノテーション Dbコンテキストの OnModelCreating メソッド Program.cs から MenuRepository#SelectMenus を呼び出すように記述して実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // Selectのサンプル var menus = MenuRepository.SelectMenus(); foreach (var menu in menus) { Console.WriteLine($"メニュー名: {menu.MenuName}, 価格: {menu.Price}円"); foreach (var item in menu.MenuItemList) { Console.WriteLine($" 品目: {item.MenuItemName}"); } } 実行結果は以下のようになります。 メニュー名: 焼き魚定食, 価格: 1000円 品目: ご飯 品目: みそ汁 品目: 鮭の塩焼き 品目: 漬物 メニュー名: 唐揚げ定食, 価格: 900円 品目: ご飯 品目: みそ汁 品目: 鳥の唐揚げ 品目: サラダ メニュー名: 刺身定食, 価格: 1200円 品目: ご飯 品目: みそ汁 品目: 刺身 品目: 漬物 メニュー名: 天ぷら定食, 価格: 1100円 品目: ご飯 品目: みそ汁 品目: 天ぷら 品目: 漬物 メニュー名: アジフライ定食, 価格: 1100円 品目: ご飯 品目: みそ汁 品目: アジフライ 品目: サラダ Insert処理 # Insert処理はとても簡単です。Dbコンテキストを使って、モデルにオブジェクトを追加するだけです。 ここでは例として、天丼を新メニューとして追加します。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public void InsertMenu(Menu menu) { // 新しいメニューを追加する _context.Menu.Add(menu); _context.SaveChanges(); } } } Program.cs から MenuRepository#InsertMenu を呼び出すように記述して実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // Createのサンプル var newMenu = new Menu { MenuId = 6, MenuName = "天丼", Price = 1000, MenuItemList = new List<MenuItem> { new MenuItem { MenuId = 6, MenuItemId = 1, MenuItemName = "天丼" }, new MenuItem { MenuId = 6, MenuItemId = 2, MenuItemName = "みそ汁" }, new MenuItem { MenuId = 6, MenuItemId = 3, MenuItemName = "漬物" } } }; MenuRepository.InsertMenu(newMenu); Management StudioからDBを確認し、以下のように3件追加されていればOKです。 Update処理 # Update処理も簡単です。Dbコンテキストを使って、モデルの Update メソッドを使うだけです。 ここでは例として、唐揚げ定食の味噌汁を豚汁に変え、価格も50円上げます。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public Menu SelectMenuById(int menuId) { // 特定のIDのメニューを取得する return _context.Menu.Include(menu => menu.MenuItemList).Where(menu => menu.MenuId == menuId) .FirstOrDefault(); } public void UpdateMenu(Menu menu) { // 既存のメニューを更新する _context.Menu.Update(menu); _context.SaveChanges(); } } } Program.cs ではまず MenuRepository#SelectMenuById を呼び出して更新対象を取得します。そして値を変更してから MenuRepository#UpdateMenu を呼び出してDBに反映します。コードが書けたら実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // Updateのサンプル // 唐揚げ定食を取得 var targetMenu = MenuRepository.SelectMenuById(2); if (targetMenu != null) { // みそ汁を豚汁に変える代わりに50円値上げする(900円→950円) targetMenu.Price = 950; targetMenu.MenuItemList.Where(item => item.MenuItemName == "みそ汁") .ToList() .ForEach(item => item.MenuItemName = "豚汁"); MenuRepository.UpdateMenu(targetMenu); } Management StudioからDBを確認し、以下のように更新されていればOKです。 Delete処理 # Delete処理も簡単です。Dbコンテキストを使って、モデルの Remove メソッドを使うだけです。 ここでは例として、先ほどInsertの例で追加した天丼を削除します。定食屋が丼ものを始めても、あまり人気が出なくて売れ行きがよくなかったようです。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public void DeleteMenu(int menuId) { // メニューを削除する var menu = _context.Menu.Find(menuId); if (menu != null) { _context.Menu.Remove(menu); _context.SaveChanges(); } } } } Program.cs から MenuRepository#DeleteMenu を呼び出すように記述して実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // Deleteのサンプル // 天丼を削除する MenuRepository.DeleteMenu(6); Management StudioからDBを確認し、以下のように天丼が取得できなければOKです。 実践的な操作 # 複数件のInsert処理 # まずは複数件のデータをまとめてDBにInsertしてみましょう。 ここでは例として定食メニューを3件追加します。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public void InsertManyMenus(IList<Menu> menus) { // 複数のメニューを一度に追加する _context.Menu.AddRange(menus); _context.SaveChanges(); } } } 1件だけInsertするときは Add メソッドでモデルに追加しましたが、複数件のデータをInsertしたいときは AddRange メソッドを使えばいいです。しかも引数は List をそのまま渡せます。わざわざ foreach で1件ずつ処理して1件ずつ Add する必要はないです。 続いて Program.cs から MenuRepository#InsertManyMenus を呼び出すように記述して実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // InsertManyのサンプル var newMenuList = new List<Menu> { new Menu { MenuId = 7, MenuName = "煮魚定食", Price = 1000, MenuItemList = new List<MenuItem> { new MenuItem { MenuId = 7, MenuItemId = 1, MenuItemName = "ご飯"}, new MenuItem { MenuId = 7, MenuItemId = 2, MenuItemName = "みそ汁"}, new MenuItem { MenuId = 7, MenuItemId = 3, MenuItemName = "魚の煮付け"}, new MenuItem { MenuId = 7, MenuItemId = 4, MenuItemName = "漬物"} } }, new Menu { MenuId = 8, MenuName = "エビフライ定食", Price = 1300, MenuItemList = new List<MenuItem> { new MenuItem { MenuId = 8, MenuItemId = 1, MenuItemName = "ご飯"}, new MenuItem { MenuId = 8, MenuItemId = 2, MenuItemName = "みそ汁"}, new MenuItem { MenuId = 8, MenuItemId = 3, MenuItemName = "エビフライ"}, new MenuItem { MenuId = 8, MenuItemId = 4, MenuItemName = "サラダ"} } }, new Menu { MenuId = 9, MenuName = "とんかつ定食", Price = 1250, MenuItemList = new List<MenuItem> { new MenuItem { MenuId = 9, MenuItemId = 1, MenuItemName = "ご飯"}, new MenuItem { MenuId = 9, MenuItemId = 2, MenuItemName = "みそ汁"}, new MenuItem { MenuId = 9, MenuItemId = 3, MenuItemName = "とんかつ"}, new MenuItem { MenuId = 9, MenuItemId = 4, MenuItemName = "サラダ"} } } }; MenuRepository.InsertManyMenus(newMenuList); Management StudioからDBを確認し、以下のように3件の定食が取得できればOKです。 複数件のUpdate処理 # 次は複数件のデータをまとめてUpdateしてみましょう。 ここでは例として、昨今の物価高を考慮してメニューを値上げします。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public IList<Menu> SelectMenus() { // Includeメソッドを使えば、関連するテーブルのデータも一緒に取得できる return _context.Menu.Include(menu => menu.MenuItemList) .ToList(); } public void UpdateManyMenus(IList<Menu> menus) { // 複数のメニューを一度に更新する _context.Menu.UpdateRange(menus); _context.SaveChanges(); } } } UpdateRange メソッドを使って一括更新を行います。Insert時同様にUpdate時も複数件のデータをまとめて List で渡せるメソッドが用意されているのです。 続いて Program.cs から MenuRepository#SelectMenus を呼び出して価格を変更しましょう。そして MenuRepository#UpdateManyMenus を呼び出すように記述して実行しましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // UpdateManyのサンプル var targetList = MenuRepository.SelectMenus(); foreach (var menu in targetList) { switch (menu.MenuId) { case 1: menu.Price = 1100; break; case 2: menu.Price = 1050; break; case 3: menu.Price = 1350; break; case 4: menu.Price = 1200; break; case 5: menu.Price = 1200; break; } } MenuRepository.UpdateManyMenus(targetList); Management StudioからDBを確認し、以下のように価格が変更されていればOKです。 検索画面での入力内容に応じた処理 # 次は検索画面において入力した条件で検索する処理を作ってみましょう。重要なポイントは、入力された項目だけ検索条件に反映することです。 まずは検索条件クラスを作ります。プロジェクト直下に Dtos というフォルダを作り、 MenuSearchCriteria というクラスを作りましょう。 MenuSearchCriteria.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Dtos { internal class MenuSearchCriteria { // メニュー名(部分一致) public string? MenuName { get; set; } = string.Empty; // 価格(以上) public decimal? Price { get; set; } // メニュー品目名(部分一致) public string? MenuItemName { get; set; } = string.Empty; } } そして MenuRepository において、以下のように検索条件となる項目に値が入っている場合だけ Where メソッドで絞り込んでいきます。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public IList<Menu> SearchMenus(MenuSearchCriteria criteria) { // データを取得するクエリを書くが、ToListメソッドを使わないことでSQLは発行されない var resultList = _context.Menu.Include(menu => menu.MenuItemList).AsQueryable(); // 検索条件が入力されていたら絞り込みを行う // メニュー名 if (!string.IsNullOrWhiteSpace(criteria.MenuName)) { resultList = resultList.Where(p => p.MenuName.Contains(criteria.MenuName)); } if (criteria.Price.HasValue) { resultList = resultList.Where(p => p.Price >= criteria.Price); } if (!string.IsNullOrWhiteSpace(criteria.MenuItemName)) { // メニュー一覧 → メニュー品目一覧の順にラムダ式でアクセスする // メニュー品目名に検索条件「メニュー品目名」を1個でも含んだら、対象とする resultList = resultList.Where( p => p.MenuItemList.Where( i => i.MenuItemName.Contains(criteria.MenuItemName) ).Count() > 0); } return resultList.ToList(); } } } ここでのポイントは最初にデータを取得したときにSQLが発行されないことです。そしてその後の Where メソッドで絞り込むときにもSQLは発行されません。SQLが発行されるのは最後の ToList メソッドが実行されるときです。 もし最初にデータを取得するときも、検索条件を追加するときもSQLが実行されていたら、DBへのI/Oが増えてパフォーマンスがとても悪くなってしまいます。数十~数百件だったら気にするほどではないかもしれませんが、数万件ともなれば見逃せないほど処理時間が伸びてしまうでしょう。 このように ToList メソッドの実行までSQLが発行されない仕様を遅延実行と呼びます。詳細はこちらのLINQの記事にある「LINQの注意点」を参照してください。 現場で迷わない!C#のLINQをサンプルコード付きで徹底攻略 最後に Program.cs から MenuRepository#SearchMenus を呼び出すように修正して実行してみましょう。ここでは例として、価格が1200円以上かつメニュー品目にサラダを含むメニューを検索します。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // 検索画面の処理 MenuSearchCriteria criteria = new MenuSearchCriteria { Price = 1200, MenuItemName = "サラダ" }; var searchResultList = MenuRepository.SearchMenus(criteria); foreach (var menu in searchResultList) { Console.WriteLine($"メニュー名: {menu.MenuName}, 価格: {menu.Price}円"); foreach (var item in menu.MenuItemList) { Console.WriteLine($" 品目: {item.MenuItemName}"); } } 実行結果は以下のようになります。 ■検索条件 メニュー名: アジフライ定食, 価格: 1200円 品目: ご飯 品目: みそ汁 品目: アジフライ 品目: サラダ メニュー名: エビフライ定食, 価格: 1300円 品目: ご飯 品目: みそ汁 品目: エビフライ 品目: サラダ メニュー名: とんかつ定食, 価格: 1250円 品目: ご飯 品目: みそ汁 品目: とんかつ 品目: サラダ 非同期処理 # 処理時間が長い場合、非同期処理を使うのも1つの手です。そこで非同期処理の実装方法も解説します。 以下のようにDBからデータを取得する処理を実装します。これだけで非同期処理になります。 ToListAsync メソッドを使う。 メソッド名の前に async 修飾子を付ける。 メソッドの戻り値の型を Task<戻り値にしたい型> とする。 サンプルコードを見てみましょう。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public async Task<List<Menu>> SelectMenusAsync() { // Includeメソッドを使えば、関連するテーブルのデータも一緒に取得できる return await _context.Menu.Include(menu => menu.MenuItemList) .ToListAsync(); } } } 戻り値が Task 型となってので、 Program.cs もちょっと工夫が必要です。サンプルコードを見てみましょう。 Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // 非同期処理での取得 var asyncResultList = MenuRepository.SelectMenusAsync(); foreach (var menu in asyncResultList.Result) { Console.WriteLine($"メニュー名: {menu.MenuName}, 価格: {menu.Price}円"); foreach (var item in menu.MenuItemList) { Console.WriteLine($" 品目: {item.MenuItemName}"); } } MenuRepository#SelectMenusAsync が Task 型を返すので、データを取得するには Result プロパティを参照する必要があります。 変更の追跡 # Entity FrameworkはDBから取得したデータの変更を追跡しています。変更の追跡はプロパティレベルです。つまり行・列ともに変更を追跡しています。 そして SaveChanges メソッドを呼び出した際に変更内容を確認して、Insert、Update、DeleteなどのSQLを実行します。 詳細はMicrosoftの記事を参照してください。 EF Core での変更の追跡 ということは変更前の値と現在の値をメモリ上に持つため、メモリへの負荷が上がります。 そこで読み取り専用のデータについては変更の追跡をOffにするという選択肢もあります。 サンプルコードを掲載します。モデルの AsNoTracking メソッドを呼び出すだけなので簡単です。 MenuRepository.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EntityFrameworkSample.Repositories { internal class MenuRepository { SampleContext _context = new SampleContext(); public IList<Menu> SelectMenusAsNoTracking() { // Includeメソッドを使えば、関連するテーブルのデータも一緒に取得できる return _context.Menu.AsNoTracking().Include(menu => menu.MenuItemList) .ToList(); } } } Program.cs using EntityFrameworkSample.Dtos; using EntityFrameworkSample.Models; using EntityFrameworkSample.Repositories; MenuRepository menuRepository = new MenuRepository(); // 追跡なしでの取得 var noTrackingResultList = MenuRepository.SelectMenusAsNoTracking(); foreach (var menu in noTrackingResultList) { Console.WriteLine($"メニュー名: {menu.MenuName}, 価格: {menu.Price}円"); foreach (var item in menu.MenuItemList) { Console.WriteLine($" 品目: {item.MenuItemName}"); } } 件数が多い、あるいは容量が大きいデータ(音声や動画など)を読み取り専用で使う場合は、変更の追跡をOffにすることも検討してみてください。 終わりに # 久々に(それこそ前回仕事でやったのは7年くらい前)Entity Frameworkをやって、そのときにやった環境構築手順や書いて動かしてみたコードを解説してみました。 これだけ楽に開発できるなら生産性もきっと高くなると感じました。特にIncludeメソッドは強力です。 C#のスキルアップにも、開発プロジェクトのリファレンスにも、この記事を活用していただければ幸いです。
この記事は夏のリレー連載2025 12日目の記事です。 お久しぶりです。小川です。 最近開発でWPFを扱ったので初学者の開発Tips的なものを備忘録感覚で記していきたいと思います。 WPF(Windows Presentation Foundation) はWindowsデスクトップアプリ開発の選択肢として候補に挙がるものです。 まずはUIのロジックを作る主要な方法としての コードビハインド と MVVM(Model-View-ViewModel) についてベタに触れていきます。 DI(Dependency Injection) を導入して少しわかった気になりながら、C#の便利な機能や罠など備忘録をまとめていければと思います。 コードビハインドとMVVM # 時折比較されたり、メリット・デメリットが議論されます。 コードビハインドは「家を建てるための道具や手作業」、MVVMは「家の構造や配管・配線の設計図の描き方」です。 規模や複雑さに応じてMVVMの設計を取り入れましょう。 WPFを理解する第一歩として、まずコードビハインドを知ることが大切です。 コードビハインド # 概要 # コードビハインドはUIレイアウトをXAML(*.xaml)で記述し、その動作ロジックをC#(*.xaml.cs)で記述します。 XAMLの裏側にあるコードという意味でコードビハインド(Code-Behind)と呼ばれます。 サンプル # 簡単なカウントアップアプリを実装してみます。 MainWindow.xaml <Window x:Class="CounterSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CounterSample" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="Counter" Width="250" Height="150" mc:Ignorable="d"> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock x:Name="CounterTextBlock" FontSize="30" Text="0" /> <Button Click="CountUpButton_Click" Content="Count up" /> </StackPanel> </Grid> </Window> MainWindow.xaml.cs using System.Windows; namespace CounterSample { public partial class MainWindow : Window { private int _count = 0; public MainWindow() { InitializeComponent(); } private void CountUpButton_Click(object sender, RoutedEventArgs e) { _count++; CounterTextBlock.Text = _count.ToString(); } } } UI要素( x:Name="CounterTextBlock" )を名前で直接参照して操作しているのが特徴です。 --> なぜpartial(部分)なのか WPF のコードビハインドは public partial class MainWindow のように partial が付いています。 これは XAML から自動生成されるコードと、開発者が書くコードをひとつのクラスにまとめるためです。 実際、ビルドすると MainWindow.g.i.cs というファイルが生成され、 XAML の要素定義や InitializeComponent が自動的に追加されます。 partial があることで、これらのファイルと MainWindow.xaml.cs を同じクラスとして扱えるようになります。 上記コードでカウントアップするアプリケーションができました。 MVVM # 概要 # MVVMは、UIとビジネスロジックを分離するための設計パターンです。 アプリケーションを以下の3つのコンポーネントに分割します。 Model: アプリケーションのデータとビジネスロジックを担当します。 View: UIそのもの。XAMLで記述され、ユーザーに情報を表示し、入力を受け取ります。 ViewModel: ViewとModelの橋渡し役でViewに表示すべきデータをプロパティとして公開し、Viewからの操作をコマンドとして受け取ります。 サンプル # 同様のカウントアップアプリを実装してみます。 サンプルコードを記述する前の準備手順としてCommunityToolkit.Mvvmを導入します。 --> Information MVVMToolkit の導入 CommunityToolkit.MvvmはMicrosoft公式のMVVM補助ライブラリです。 INotifyPropertyChanged や ICommand 実装を自動生成してくれます。 手書きでは冗長になりがちな、 OnPropertyChanged の呼び出しや RelayCommand の実装を省略できます。 NuGet から CommunityToolkit.Mvvm を追加してください。 View MainWindow.xaml <Window x:Class="CounterSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CounterSample" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="Counter" Width="250" Height="150" mc:Ignorable="d"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock FontSize="30" Text="{Binding Count}" /> <Button Command="{Binding CountUpCommand}" Content="Count up" /> </StackPanel> </Grid> </Window> MainWindow.xaml.cs using System.Windows; namespace CounterSample { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } } ViewModel MainViewModel.cs using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace CounterSample { public partial class MainViewModel : ObservableObject { [ObservableProperty] private int count = 0; [RelayCommand] private void CountUp() { Count++; } } } MVVMではClickイベントやx:Nameが不要になり、代わりにBindingを使います。 UI(View)とロジック(ViewModel)が分離されるので再利用性が上がり、テストがしやすくなります。 View(XAML)とViewModel(C#)がどのように連携しているのか補足します。 <TextBlock Text="{Binding Count}" /> <Button Command="{Binding CountUpCommand}" /> [ObservableProperty] private int count = 0; [RelayCommand] private void CountUp() { Count++; } この連携は CommunityToolkit.Mvvm が提供するアトリビュート([ ]で囲まれた部分)によるコードの自動生成です。 データ(Count)の連携 XAMLの {Binding Count} は「Countという名前の公開プロパティの値を表示して」という指示です。 ViewModelの [ObservableProperty] は、 private int count フィールドを元に、 public int Count というプロパティをコンパイル時に自動で生成します。値が変更されたらUIに通知する機能も込みです。 操作(CountUp)の連携 XAMLの {Binding CountUpCommand} は、「CountUpCommandという名前のコマンドを実行して」という指示です。 ViewModelの [RelayCommand] は、 private void CountUp() メソッドを元に、 public ICommand CountUpCommand というコマンドを自動で生成します。 あれ、そういえばサンプルコードにModelがないのでは? 単なるカウントアップを保持するだけのシンプルなサンプルだと、Modelをわざわざ分ける必要はありません。 しかし、Modelに書くべきコードをViewModelに書いてしまうのはよくある間違いなので注意が必要です。 あくまでViewとModelの橋渡し役なのでViewModelをゴリゴリ書き始めたときは責務を疑ってみます。 サンプルVer2 # カウントアップアプリにリセットを追加してみましょう。 カウンターの値をセーブ、ロードするサービスも作ってみます。 Model CounterModel.cs namespace CounterSample.Models { internal class CounterModel(int initialValue = 0) { public int Value { get; private set; } = initialValue; public void Increment() => Value++; public void Reset() => Value = 0; public void SetValue(int value) => Value = value; } } Service CounterStorageService.cs using CounterSample.Models; namespace CounterSample.Services { internal class CounterStorageService { private int _storedValue; public void Save(CounterModel model) { _storedValue = model.Value; } public void Load(CounterModel model) { model.SetValue(_storedValue); } } } ViewModel CounterViewModel.cs using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CounterSample.Models; using CounterSample.Services; namespace CounterSample.ViewModels { internal partial class CounterViewModel : ObservableObject { private readonly CounterStorageService _service; private readonly CounterModel _model = new(); [ObservableProperty] private int count; public CounterViewModel(CounterStorageService service) { _service = service; Count = _model.Value; } [RelayCommand] private void CountUp() { _model.Increment(); Count = _model.Value; } [RelayCommand] private void Reset() { _model.Reset(); Count = _model.Value; } [RelayCommand] private void Save() { _service.Save(_model); } [RelayCommand] private void Load() { _service.Load(_model); Count = _model.Value; } } } View MainWindow.xaml <Window x:Class="CounterSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CounterSample" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="Counter" Width="250" Height="150" mc:Ignorable="d"> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock HorizontalAlignment="Center" FontSize="30" Text="{Binding Count}" /> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <Button Margin="2" Command="{Binding CountUpCommand}" Content="Count up" /> <Button Margin="2" Command="{Binding ResetCommand}" Content="Reset" /> <Button Margin="2" Command="{Binding SaveCommand}" Content="Save" /> <Button Margin="2" Command="{Binding LoadCommand}" Content="Load" /> </StackPanel> </StackPanel> </Grid> </Window> MainWindow.xaml.cs using CounterSample.Services; using CounterSample.ViewModels; using System.Windows; namespace CounterSample { public partial class MainWindow : Window { private readonly CounterStorageService _service = new(); public MainWindow() { InitializeComponent(); DataContext = new CounterViewModel(_service); } } } 機能が追加されてコードの量は増えましたが、クラスごとに責務が分かれていて保守はしやすそうに思います。 実行してみると少しリッチなカウンターになりましたね。 DI # ちょうど良さそうなサンプルになったのでDI(Dependency Injection)を使ってみます。 --> Information DependencyInjection の導入 WPFでMVVMを活用するなら、 Microsoft.Extensions.DependencyInjection を使ったDIが便利です。 DIを導入すると、ViewModelやサービスを必要な場所で簡単に受け渡すことができ、テストや保守がしやすくなります。 ServiceCollection にサービスやViewModelを登録し、 ServiceProvider から取得するだけで依存関係を解決できます。 Viewや他のViewModelから直接newする必要がなくなります。 NuGet から Microsoft.Extensions.DependencyInjection を追加してください。 サンプルVer3 # サンプルVer2を元にDIを導入したサンプルを作ってみます。 CounterStorageService と CounterViewModel をサービス登録します。 後述するインスタンスのライフサイクルを学ぶため、StartupUriでApp.xaml(変更の必要なし)とコード上から2つウィンドウを起動してみます。 App.xaml.cs using CommunityToolkit.Mvvm.DependencyInjection; using CounterSample.Services; using CounterSample.ViewModels; using Microsoft.Extensions.DependencyInjection; using System.Windows; namespace CounterSample { public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // ServiceCollection でサービスを登録 ServiceCollection services = new(); services.AddSingleton<CounterStorageService>(); services.AddTransient<CounterViewModel>(); // Ioc.Default に登録 Ioc.Default.ConfigureServices(services.BuildServiceProvider()); // もう1つWindowを起動 MainWindow window = new(); window.Show(); } } } 登録したサービスを Ioc.Default から取得します。 CounterViewModel のインスタンスを要求すると、コンストラクタで CounterStorageService が要求されるのでDIコンテナからインスタンスを持ってきてくれます。 MainWindow.xaml.cs using CommunityToolkit.Mvvm.DependencyInjection; using CounterSample.ViewModels; using System.Windows; namespace CounterSample { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = Ioc.Default.GetRequiredService<CounterViewModel>(); } } } サンプルVer2(抜粋) private readonly CounterStorageService _service = new(); public MainWindow() { InitializeComponent(); DataContext = new CounterViewModel(_service); } 実行するとこんな感じです。 あれ、別の画面なのに最後に Save したやつが別画面で Load されるぞ? その秘密はサービス登録時のコードにあります。 services.AddSingleton<CounterStorageService>(); 呼び出すメソッドによってライフサイクルが異なります。 メソッド ライフサイクル AddSingleton アプリケーションで1つのインスタンス AddScoped 1つの要求の間で1つのインスタンス AddTransient 都度新しいインスタンス AddSingleton で登録していたため、サービスのインスタンスがアプリケーション内で唯一となり、同じ内部の保持値を見ていたわけです。 AddTransient に変えると以下の動作になります。 AddScoped は例えば以下のようなときに同じインスタンスになります。 // コンストラクタでServiceとFugaを要求 Hoge(Service service, Fuga fuga) // コンストラクタでServiceを要求 Fuga(Service service) // サービス登録 var services = new ServiceCollection(); services.AddScoped<Service>(); // Scoped で登録 services.AddTransient<Hoge>(); services.AddTransient<Fuga>(); var provider = services.BuildServiceProvider(); // Hogeを要求、この時HogeとFugaが受け取るServiceのインスタンスは同一になる var hoge = provider.GetRequiredService<Hoge>(); --> Caution Ioc.Default は静的なコンテナでアプリケーション全体が単一のルートスコープ内で実行されます。 そのため Ioc.Default からインスタンスを取得する場合、 AddScoped は AddSingleton と全く同じように動作します。 という感じでサンプルと向き合う時間は終わりです。 サンプルなのでインターフェースに切るなどはやってません。 テストコードも入れて有用なことを示したいですが、長くなるので断念しました。 C#のWPF開発でつまづいたこと一覧 # 以降はただの備忘録なので、参考になるかもしれないし、ならないかもしれません。 LINQ # 当方SQLが嫌いなので、最初はかなり読みづらかったです。 LINQについてはデベロッパーサイト内に詳しい記事がありますので以下ご参照ください。 現場で迷わない!C#のLINQをサンプルコード付きで徹底攻略 よく使うものを簡単に紹介します。 勝手に苦手意識がありますが、割とメソッド名通りの動きです。 Where # var numbers = new[] { 1, 2, 3, 4, 5 }; var even = numbers.Where(n => n % 2 == 0); Console.WriteLine(string.Join(",", even)); // 2,4 First/FirstOrDefault # var words = new[] { "apple", "banana", "cherry" }; var first = words.First(); // "apple" var startsWithB = words.FirstOrDefault(w => w.StartsWith("b")); // "banana" var notFound = words.FirstOrDefault(w => w.StartsWith("z")); // null Select/SelectMany # var names = new[] { "Alice", "Bob" }; var lengths = names.Select(n => n.Length); // [5, 3] var groups = new[] { new[] {1,2}, new[] {3,4} }; var flat = groups.SelectMany(g => g); // [1,2,3,4] GroupBy # var fruits = new[] { "apple", "apricot", "banana", "blueberry" }; var grouped = fruits.GroupBy(f => f[0]); foreach (var g in grouped) { Console.WriteLine($"{g.Key}: {string.Join(",", g)}"); } // a: apple, apricot // b: banana, blueberry Any/All # var numbers = new[] { 1, 2, 3 }; bool hasEven = numbers.Any(n => n % 2 == 0); // true bool allPositive = numbers.All(n => n > 0); // true IEnumerableの遅延評価 # LINQつながりで、陥った罠を紹介します。 LINQは基本「遅延評価」なので、ToList()やToArray()で確定しないと、意図しない結果になることがあります。 var numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(n => n > 1); // ToList()を呼ばない // ここでリストを変更 numbers.Add(4); // この時点でクエリを評価 Console.WriteLine(string.Join(",", query)); // 2,3,4 WPFのDispatcher # Invoke と BeginInvoke の違いで更新されているはずのUIが更新されないことがありました。 同期的に処理したい場合は Invoke を使い、重い処理はUIが固まるので渡さないようにしました。 // Invoke: 完了するまで呼び出し元が止まる Dispatcher.Invoke(() => { Console.WriteLine("UI更新: 完了まで待つ"); }); Console.WriteLine("←これは必ずUI更新後に実行される"); // BeginInvoke: 依頼だけして次へ進む Dispatcher.BeginInvoke(() => { Console.WriteLine("UI更新: 非同期で実行される"); }); Console.WriteLine("←これはUI更新前に先に実行される可能性あり"); Nullable # C#8.0以降 nullable が導入されました。 常にnullチェックを書く必要が減って、コンパイル時に安全性を確保できます。 string notNull = "hello"; // null 非許容 string? canBeNull = null; // null 許容 // notNull = null; // コンパイルエラー [NotNullWhen] 属性について System.Diagnostics.CodeAnalysis.NotNullWhen を使うと、メソッドの戻り値と引数のnull関係をコンパイラに伝えられます。 この仕組みを使うと、余計なnullチェックを省きつつ、安全にコードを書けます。 using System.Diagnostics.CodeAnalysis; bool TryGetValue([NotNullWhen(true)] out string? value) { value = DateTime.Now.Second % 2 == 0 ? "even" : null; return value != null; } if (TryGetValue(out var text)) { // textがnullでないとコンパイラが理解しているので安全に使える Console.WriteLine(text.Length); } まとめ # WPFやC#は初めて触ってみましたが、面白いことがたくさんあるなという感じでした。 最初はつまづきましたが、ひとつずつ理解するとだんだん整理されて楽になりました。 まだまだ知らないことばかりですが、ゆっくり深めていければいいなと思います。 少々雑多になりましたが、この記事が少しでも役立てば幸いです。 以上、お疲れ様でした。
この記事は夏のリレー連載2025 11日目の記事です。 カレーの付け合わせには、福神漬けよりもらっきょうの方が好きな塩田です。 今年の4月に、JetBrains社からAIエージェントのJunieが一般公開されました。 AIエージェントとしては他にも、Claude CodeやCursorなどがありますよね。 筆者は日ごろからIntelliJ IDEAを利用する機会が多いので、今回はJetBrainsのJunieについて記事にしたいと思います。 Junieとは # Junieとは、JetBrains社が開発した自律型のAIコーディングエージェントです。 https://www.jetbrains.com/ja-jp/junie/ 筆者は以前、JetBrainsのAI Assistantを利用していました。豆蔵デベロッパーサイトでも過去に、AI Assistantに関する記事が投稿されていましたよね。 開発者体験(DX)を進化させるJetBrainsのAIアシスタント機能の紹介 このような従来のコード補完やプロンプトによるコード生成とは異なり、Junieはプロジェクト全体のコンテキストを理解したうえで、コードの生成からテストの実行までを行ってくれるのが特徴のようです。 とは書いてみたもののあまりイメージができないので、簡単ですが前置きはこれくらいにして早速、Junieを使っていきたいと思います。 なお、JunieはIntelliJ IDEA Community Editionもサポートしているため、記事の執筆にあたっては無償枠のIntelliJ IDEAおよびJunieを使わせていただきます。 無償枠ですと機能制限等があるかと思いますが、ご理解いただきたく存じます。 --> Information IntelliJ IDEA以外のJetBrains IDEにも対応しており、PyCharmやWebStormなどでJunieを利用することもできます。 また、Googleが提供する Android Studio でも利用することができます。 Junieのインストール # IntelliJ IDEAがすでにインストールされていることは前提とさせていただき、Junieをインストールするところから始めていきます。 何も難しいことはありません。IntelliJ IDEAを起動し、MarketplaceからJunieを検索してプラグインをインストールします。ただそれだけです。 --> Information 筆者が使用しているIntelliJ IDEAのバージョンは 2025.2.1 となります。 Junieのインストールがうまくいかない場合は、IntelliJ IDEAのバージョンをご確認ください。 プロジェクトの準備 # Junieのインストールを終えたら Spring Initializr などを利用して、空のプロジェクトをご準備ください。 もちろん、Spring Bootのプロジェクトでなくても構いません。IntelliJ IDEAのサポート範囲でお好きなものをご準備いただければと思います。 本投稿では、Junieを使ってREST APIのSpring Bootアプリケーションを開発していきたいと思います。 筆者が準備したプロジェクトはこのとおり、ほぼほぼ空の状態です。 build.gradle の依存関係も最低限のものだけ記述しています。Spring MVCやSpring Data JPAに関するスターターライブラリも含まれておりません。 build.gradle dependencies { implementation 'org.springframework.boot:spring-boot-starter' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } Junieを使用する前に # IntelliJ IDEAから先ほどのプロジェクトを開いてみます。 すると、IntelliJ IDEAの右サイドバーにこのようなJunieのアイコンがあるので、それをクリックするとJunieのツールウィンドウが表示されるはずです。 最初の印象は「あぁ、日本語に対応しているのかなぁ~」でしたが、どうやら日本語にも対応しているようですね。安心しました。 Junieの動作モード # Junieには、「Codeモード」と「Askモード」の2つの動作モードがあります。 動作モード 概要 Codeモード Junieがコードの追加や編集、テストの実行までを自律的に実行するモード。 Askモード Junieと自然言語でやりとりしながら、設計や実装の方針などについて「相談」できるモード。 JetBrainsの公式サイトやブログを見てみると、Askモードで設計方針を定め、それに基づいてCodeモードで実装、テストを実施するといった使い方を想定されているように感じました。 時間の関係上、今回はCodeモードについてのみ投稿させていただきます。 JunieのLLM # JunieのLLMを確認してみようと設定画面を開いてみたら、OpenAIのGPT-5がデフォルトで選択されていました。 Junieの設定は変更せずに、このままGPT-5を使いたいと思います。 Junieを使ってみる # それではここから、Junieを使って先ほどのプロジェクトがどのように変化していくのかを体験したいと思います。 何も考えずに、 社員情報を管理するREST APIを実装してください。 とだけプロンプトに入力してみました。 すると、次のような計画ステップが作成され、これに沿って順番に各ステップが実行されます。 何度か Approve ボタンを押下したのち、ビルドおよびテストの実行まで終えると、すべてのステップの完了が通知されます。 実際に生成されたコードを見てみると、なんてことでしょう、詳細な指示を与えていないのに社員情報(リソース)を扱うためのソースコード一式が揃っているではありませんか。 REST APIに加え、エンティティクラスやリポジトリインタフェースも存在します。 なお、ここではパッケージ構造やレイヤ構造については触れないでおくこととします。 Employee.java @Entity @Table(name = "employees") @Data @NoArgsConstructor @AllArgsConstructor public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank private String name; @Email @NotBlank @Column(unique = true) private String email; @NotBlank private String department; @PastOrPresent private LocalDate hireDate; } EmployeeRepository.java public interface EmployeeRepository extends JpaRepository<Employee, Long> { Optional<Employee> findByEmail(String email); } EmployeeController.java @RestController @RequestMapping("/employees") public class EmployeeController { // ---------- <中略> ---------- // @PostMapping @ResponseStatus(HttpStatus.CREATED) public Employee create(@RequestBody @Valid Employee employee) { try { employee.setId(null); return repository.save(employee); } catch (DataIntegrityViolationException e) { throw new ResponseStatusException(HttpStatus.CONFLICT, "Email already exists"); } } @GetMapping public List<Employee> list() { return repository.findAll(); } @GetMapping("/{id}") public Employee get(@PathVariable Long id) { return repository .findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); } @PutMapping("/{id}") public Employee update(@PathVariable Long id, @RequestBody @Valid Employee updated) { Employee existing = repository .findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); // ---------- <中略> ---------- // try { return repository.save(existing); } catch (DataIntegrityViolationException e) { throw new ResponseStatusException(HttpStatus.CONFLICT, "Email already exists"); } } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Long id) { if (!repository.existsById(id)) { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } repository.deleteById(id); } } もちろんのこと、 build.gradle や application.yaml も編集されています。 build.gradle dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' // ---------- <中略> ---------- // } build.gradle はこのとおり、プロジェクト作成時点で記述されていなかったSpring MVCやSpring Data JPAなどのライブラリが依存関係に追加されています。 application.yaml spring: application: name: mamezou-blog-restapi datasource: url: jdbc:h2:mem:employees;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driverClassName: org.h2.Driver username: sa password: "" jpa: hibernate: ddl-auto: update h2: console: enabled: true path: /h2-console また、プロジェクト作成時点でのアプリケーションプロパティは application.properties でしたが、筆者の好みで application.yaml に変更させていただきました。 これもJunieを使って変更しています。 Junieによるテストの追加 # 先述の「 Junieを使ってみる 」の時点では、テストクラスが実装されていませんでした。 そこで、テストクラスを追加するため、Junieのプロンプトに 単体テストを実装してください。 と入力してみました。 はい、このとおり EmployeeController クラスのテストクラス、つまり EmployeeControllerTest クラスが追加されました。 EmployeeController クラスのREST API(ハンドラメソッド)に対するテストメソッドが実装されていることも確認できます。 EmployeeControllerTest.java @WebMvcTest(EmployeeController.class) class EmployeeControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @MockBean EmployeeRepository repository; private Employee sample(Long id) { return new Employee(id, "Taro Yamada", "taro@example.com", "IT", LocalDate.of(2020, 1, 1)); } @Test @DisplayName("POST /employees - creates employee and returns 201") void create_success() throws Exception { Employee input = sample(null); Employee saved = sample(1L); when(repository.save(any(Employee.class))).thenReturn(saved); mockMvc .perform( post("/employees") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.email").value("taro@example.com")); } // ---------- <中略> ---------- // @Test @DisplayName("GET /employees/{id} - returns employee or 404") void get_by_id() throws Exception { when(repository.findById(1L)).thenReturn(Optional.of(sample(1L))); when(repository.findById(99L)).thenReturn(Optional.empty()); mockMvc .perform(get("/employees/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)); mockMvc.perform(get("/employees/99")).andExpect(status().isNotFound()); } // ---------- <後略> ---------- // } なお、エンティティクラスとリポジトリインタフェースは振る舞いを持たないため、これらのテストクラスは生成されなかったものと思われます。 Junieによって、テストクラスが生成された時点ですべてのテストが成功することは確認されています。 念のため、改めてテストを流してみましたが、このとおりすべてのテストが成功していますね。 --> Warning テストクラスで使用している MockBean アノテーションは、Spring Boot 3.4.0以降で非推奨かつ廃止予定となっています。 その代わりにとして、 MockitoBean アノテーションの使用が推奨されています。 最後に # JetBrainsのJunieに関して、ここまでいかがでしたでしょうか。 筆者はまだJunieの一部の機能しか利用していませんが、とても便利に感じております。 普段の開発活動においてIntelliJ IDEAを利用している方はぜひ、Junieを導入してみて体感いただければと思います。 個人ライセンスでしたら月額 1,540円から利用できますので、お小遣いへの負担も少なくてすみますしね。 https://www.jetbrains.com/ja-jp/ai-ides/buy/?section=personal&billing=monthly 今回は、JunieのCodeモードによるコード生成を試してみました。今後は、Askモードとの組み合わせや、Braveモードなども試していきたいと思います。 また、プロジェクトルートに .junie/guidelines.md を配置し、コーディング規約等を記述することで、これに準拠したコードの生成や編集も可能なようです。 これらにつきましても次回以降の記事で、投稿していきたいと考えています。 それでは最後までご覧いただき、本当にありがとうございました。
この記事は夏のリレー連載2025 10日目の記事です。 はじめに # 現場で GitHub + GitHub Copilot を使っていて、最近はプルリクエストのレビューをセルフチェックも兼ねて Copilot に依頼しています。 結構的確にコメントしてくれるのですが、デフォルトでは英語で返ってくるため、日本語で返してほしいと思い調べ始めたのが本記事のきっかけです。 GitHub Copilotにレビューをしてもらおう # GitHub Copilot を使っていていたら、ぜひ試してほしいのが Copilot のレビュー機能です。 やり方としては以下になります。(詳細は 公式ドキュメント 参照) GitHub.comでプルリクエストを作成するか、既存のプルリクエストに移動します。 Reviewers メニューを開き、Copilot を選択します。 Copilotがプルリクエストをレビューするのを待ちます(通常30秒以内)。 Copilotのコメントを確認します。コメントは通常のレビューコメントと同様に扱えます(リアクションを追加、コメント、解決、非表示など)。 実際に使ってみると、バグやリファクタ箇所をかなり的確に指摘してくれる点が印象的でした。 開発者の視点から見ても、有用なレビューを得られるのはありがたい限りです。 ただし、どうしても気になるのが基本的に英語で返答してくる点。そこでカスタム命令を試してみることにしました。 カスタム命令を追加 # GitHub では「カスタム命令」という仕組みがあり、マークダウン形式の自然言語で Copilot に指示を与えられます。 (詳細は 公式ドキュメント 参照) 今回はリポジトリ全体に適用するため、以下のファイルを作成しました。 .github/copilot-instructions.md 記載内容の一例は以下のとおりです。 ## 基本方針 - 生成するすべてのコメント・説明・回答・レビューは「日本語」で記述してください。 - 可能な限り読みやすく、簡潔で具体的な提案を行ってください。 また今回は上記に合わせて、以下コーディング規約的な要素を追記してみます。 以下は ES2015(ES6) の let / const に関するルール例です。 # JavaScript --- ## 変数宣言でvarではなくconstやletを使用しているか 再代入がない変数はconst、再代入がある変数はletで宣言しましょう varだと同じ変数名で再宣言できてしまうがletはできない (変数の二重定義や、意図しない再代入の防止) このファイルを push して Copilot にレビューを依頼したところ、想定通りコーディング規約に沿ったコメントが返ってきました。 もしプロジェクト内で既にマークダウン形式でコーディング規約を管理しているなら、そのままコピペして活用するのも有効だと感じました。 (Excelで規約をまとめている現場もありますが、生成AIとの相性を考えると今後マークダウン形式を推していきたいところ) カスタム命令を記載する上での注意点 # 以下のような指示では意図した結果が得られない場合があります。 外部リソース(URL やファイルパス)を参照した指示はできません。 悪い例: - このリポジトリのコードを理解するためには、以下のURLを読んでください: https://example.com/internal-specs - `/docs/design/architecture.md`を参照しながら提案をしてください 特定のスタイルで回答するという指示はできません。 悪い例: - 明るくフレンドリーな口調で回答してください ※ただし「語尾を〜なのだにしてください」のように単純な指定は反映されたケースもありました。 回答の長さや形式を強制はできません。 悪い例: - 必ず1000文字以内で回答してください - 最後に五・七・五でまとめてください 上記例についても今後の言語モデルの進化に合わせて、今できないことができるようになる未来もあるかと思ってます。(今後に期待) 万事解決…かと思いきや # 作成したカスタム命令を反映させて新しいプルリクを作ったところ、またしても英語でレビューが返ってきました。 何回かプルリクエストを出していると、英語で返してくるときもあれば、日本語で返してくるときもあり中々安定しない印象でした。 (コメント内容を見るとカスタム命令自体は参照している様子ではありますが) 今後生成AIと付き合っていく上で、「必ず言うことを聞いてくれる」という思いを捨て、ある程度の曖昧さを許容することがユーザーに求められているのかもしれません。(諦め) まとめ # GitHub + GitHub Copilot を使っているなら、ぜひレビュー機能を試してみよう .github/copilot-instructions.md にカスタム命令を追加することで、レビューの指針を指定できる(規約をマークダウンで書いておくと活用しやすい) ただし、必ずしも指示どおりに動くわけではないため、生成AI特有の曖昧さを受け入れる心構えが大切
この記事は夏のリレー連載2025 9日目の記事です。 筆者は現在ロボットを利用したシステム開発(ロボットSI)を行っています。ロボットを動かすプログラム(ロボット言語)やロボットと連携して制御を行うプログラム(C#等)を開発しています。ロボットってどうやってプログラムを記述するのか、PC環境からどのようにしてロボットと連携を取ることができるのかについて紹介します。 一般的なロボットプログラムの作成方法 # 産業用ロボットは一般的には教示操作盤 [1] を使ってロボットプログラムを作成します。 FANUCの教示操作盤 ABBの教示操作盤 ロボットのTCP [2] を目標位置に合わせる TCPをX,Y,Z軸に沿って移動させたり、ロボットの各関節角度を調整することで位置を合わせます 現在の位置を教示点 [3] として取り込み、移動方法(移動速度や補間 [4] 方法 等)を指示する 「移動命令を追加」などのボタンを押して移動方法を指定する その他の命令を指示する ロボットプログラムが提供する特別な命令(数学関数、処理待ち、同期処理、座標変換など)を指示する ハンドグリッパーの開閉や外部デバイスと連携する場合はIO [5] で信号操作する フロー命令(逐次実行、条件分岐、繰り返しなど)を記述する 変数を定義し、状態管理する 教示操作盤を使って教示することはソフトウェアエンジニアがIDEでプログラム編集を行うことに似ています。 教示操作盤で教示したプログラムはロボットが接続されているロボットコントローラ [6] で実行され、その動作を再現します。これをティーチング・プレイバックと呼びます。 また、実際にロボットを動かしながらプログラムを作成するためオンラインティーチング方式とも呼ばれています。 ロボットで定型の処理を行いたい場合はティーチング・プレイバックで行います。非常に高い精度(±0.1mm以下など)で正確に指定された位置を辿ります。自動化された生産ラインでは一般的な方式です。 一方、教示点のデータがロボットプログラムと共に書き込まれている [7] ため、教示点を修正したり、教示位置を追加・削除するにはロボットプログラムを修正する必要があります。 教示データ毎にプログラムを作ることになるためロボットプログラムが複数になりメンテナンス性が悪くなることがあります。 ティーチングの種類 # ティーチングには様々な種類があります。 オンラインティーチング # 教示操作盤を使って実際にロボットを動かしながらロボットプログラムを作成するティーチングです。 メリット # ティーチングの基本 どのような産業用ロボットもオンラインティーチングが可能 デメリット # ロボットがある現場に行って、ロボットを優占してプログラムを作成する 生産ラインを止める必要がある 実機を動かすことになるため衝突など細心の注意を払って行う必要がある オフラインティーチング # ロボットがない環境でシミュレータ等を利用してティーチングを行います。シミュレータ環境上でロボットと教示操作盤ソフトを動作させてロボットプログラムティーチングします。 メリット # 机上でロボットプログラムを作成することができる シミュレータ上でロボットの動作確認ができるので安全 各PCにシミュレータ環境を構築すれば複数人が同時にロボットプログラムを作成することができる ロボットプログラム自体をエディタ等で記述し、それをシミュレータ上にインポートして動作確認ができる デメリット # 生産ラインとロボットの位置や使用するハンド・ツール類の設定を合わせておく必要がある IOなどで外部システムと連携する場合、外部システムを模擬するスタブを作る必要がある シミュレータを利用する際のライセンス料が高額(シミュレータ自体は無料だが、買い切り、年間サブスクリプションなど) ダイレクトティーチング # ロボットアームを直接手で動かして教示し、ロボットプログラムを作成します。オンラインティーチングの一種なのでメリット・デメリットはオンラインティーチングと同様になります。協働ロボット(人と一緒に作業ができるロボット)ではダイレクトティーチングできるものが多いです。 メリット # 直感的に操作できる 教示点の設定であれば教示操作盤がなくてもティーチングが可能 デメリット # 直接手で動かすためロボットアームの位置や姿勢の精度が作業者に左右される ティーチングレス # 最近ではAIにより自動的に教示が行えるティーチングレス環境もあるそうです。 ティーチングの使い分け # 生産現場ではオペレータはオンラインティーチングが基本かと思います。一方、ロボットSIを行う場合はシミュレータ環境を利用して開発を行うためオフラインティーチングを利用することが多いです。我々ソフトウェアエンジニアは教示を行うのが目的でなくロボットを活用した自動化システムの開発が目的のため使い慣れたIDEやエディタでロボットプログラムを記述することに慣れています。そのため生産現場のオペレータに比べてティーチングや教示操作盤操作があまり得意ではありません😅 ロボットプログラムの実行方式 # ティーチングにより作成されたロボットプログラムはロボットコントローラが解釈して実行します。 プレイバック方式 ティーチングした内容をそのまま再現する方式 ロボットプログラムをコンパイルしてバイナリ形式に変換し、ロボットコントローラが直接実行する 産業用ロボットで一般的な実行方式 スクリプト方式 スクリプト(ロボットプログラム)をインタープリターが1行ずつ解釈しながら実行する方式 柔軟性や開発のしやすさが求めれれる協働ロボットなどで利用される 文脈単位でスクリプトを自動生成して実行させるといった使い方ができる プレイバック方式に比べて処理速度が劣る傾向がある プレイバック方式はC#などのコンパイル型言語、スクリプト方式はPythonなどのスクリプト言語と同じ実行方式と思っていただければ間違いありません。 コンパイルと言っても自動で瞬間的に行われるため、あまり意識する必要はありません。 ただし、どちらの実行方式もロボットベンダーの独自言語です。C#やPythonなどでロボット言語が記述できたらどれだけ便利かといつも思うのですが、できない理由があります。 ロボットプログラムは各行を上から順番に逐次実行されているように見えますが実は違います。教示点間の移動方法を補間するために内部的にはプログラムを先読みする必要があります。 現在の位置 → A点 → B点 → C点とロボットアームを移動させる場合、下表のような設定になります。 プログラム行 補間 教示点 座標 速度 ショートカット距離 1 直線 A点 (0,0,0) 10mm/sec 0mm 2 直線 B点 (10,0,0) 10mm/sec 10mm 3 直線 C点 (10,-20,0) 30mm/sec 0mm TCPの軌跡としては下図のようになります。 左図はB点のショートカットが0mmの場合、右図はB点のショートカットが10mmの場合の例です。ショートカットが0mmの場合はその教示点で一時停止します。ショートカットを指定することでTCPの軌跡が滑らかになり、速度も滑らかに10mm/secから30mm/secに加速します。また距離も短くなるためサイクルタイムが短くなります。 一般的なプログラム言語は処理対象の行に記載された命令を実行することしかせず先読みを行いません。一方、ロボットプログラムでは2行目を実施する(TCPがA点からB点へ向かう)ときの軌跡を計算するにはAとBだけではなくCの情報も読む必要があります。各教示点間の距離や位置関係、移動速度、どれだけショートカットするかなどの情報が必要になり2行目を実行するには3行目を読んでおく必要があります。 また、A点 → B点に移動中はB点に到着するまでは2行目の処理が継続中です。ここで「A点からB点に移動中に80%の位置に達したらデジタル信号を送信したい」場合は非同期処理でデジタル信号送信タスクを実行することになります。 こうなるとロボットプログラムは複雑な記述が必要になってきますが、オペレータが理解できるよう(オペレータはソフトウェアエンジニアではありません)に、教示操作盤で教示できるように、シンプルな記述にすべきであるため独自言語にならざるを得なくなります。 ロボット制御API # ロボットをPCなどから制御したい場合があります。例えば、 「PCに接続されたカメラでワーク [8] を撮影し、PC上で画像解析して、ワークの位置・姿勢を算出し、その情報をロボットに転送してロボットハンドで把持する」と言ったことが挙げられます。 このとき、PCからロボットコントローラを介して、ロボットプログラムが参照している教示データの位置・姿勢の値を書き換える必要があります。 これを行うのが各ベンダーが提供しているロボット制御APIになります。PC上に構築したアプリケーションからロボット制御APIライブラリを参照して利用します。 ロボット制御APIを利用することでロボットプログラムの実行環境にアクセスできます。 ここには変数、ロボットの状態、プログラムの状態などが管理されているため、これらの値を参照したり、変数を書き換えるといったことができます。 ポイントはロボットプログラムを書き換える必要が無いという点です。 ロボットプログラムは「変数に代入されているワークの位置・姿勢に応じてロボットを移動させ、ワークを積み上げる」といった手順のテンプレートをプログラムしておき、実行時にロボット制御APIを通して変数を書き換えることで環境に応じた制御を実施します。 ロボットプログラムの言語仕様やロボットを制御するためのメカニズムは各社各様なので変数の扱いやロボットの制御単位に大きな違いがあることに注意が必要です。 FANUC社のロボット # FANUC社は日本を代表する産業用ロボットベンダーです。コーポレートカラーが黄色で有名です。 ロボット言語 # レジスタという概念があり、位置データや数値や文字はすべてグローバルな領域で各データ型毎に決まったサイズの1次元配列で管理されています。 どのロボットプログラムからも参照・更新ができ便利な半面、グローバル変数として扱うため誤ってデータ更新してしまい、他のプログラムがエラーになる可能性がある点で注意が必要です。 1つのロボットプログラムから複数のロボットを別々に制御したり、複数のロボットを同期を取って制御させることが簡単にできます。 ロボット制御API # RobotInterfaceと呼ばれており.NETやC++のライブラリとして提供されています。効率良くデータ転送ができるようにDataTableという概念で利用するレジスタの管理ができます。 非常にシンプルで便利なライブラリですがなぜかプロダクトには記載がありません。営業担当者に直接問い合わせる必要がありました。 ABB社のロボット # スイスに本社を置く多国籍企業です。産業用ロボットの他に電力、重工業など幅広いソリューションを提供しています。 ロボット言語 # ロボット毎に1つのタスク(ロボットプログラム実行スレッド)が割り当たり、そのタスクにロボットプログラムをロードして実行します。2台のロボットを制御するには各ロボット用のロボットプログラムを作成し、別々のタスクでロボットプログラムを実施することになります。そのため柔軟にロボットを制御できますが、ロボット間での同期制御がFANUCに比べて難易度が高いように思います。 ローカル変数やグローバル変数の他、タスクスコープの変数も定義できます。グローバル変数やタスク変数は永続化でき、ロボットプログラムのライフサイクルよりも長く利用できます。そのためFANUCのレジスタと同じような概念で変数を定義してロボットプログラムを作成することも可能です。 ロボット制御API # PC-SDKと呼ばれており.NETライブラリとして提供されています。FANUCのRobotInterface以上に様々なことができ、教示操作盤でできることとほぼ同等のことができると思われます。ただし、クラスライブラリが膨大なのに比べマニュアルの読みやすさやサンプルコードが少ないといった印象です。 Universal Robots社のロボット # UR(Universal Robots)社は協働ロボットで有名なデンマークの企業です。 ロボット言語 # Pythonに似たスクリプト言語でロボットプログラムを記述します。 UR社はロボット制御APIの仕様は公開していますがおそらくライブラリとして公開していません。 ロボット制御API # ロボット制御APIを使って記述したスクリプトをロボットコントローラにソケット通信で転送してロボットコントローラ側でスクリプトを実行させる方式を採用しています。 そのため、簡単に実行させることができますが、都度、スクリプトの転送処理が発生しますので動的に頻繁にデータを更新して利用するといった処理は若干不得手です。 ただし、回避策はあり、スクリプトの中でソケット通信を行いPCと連携させるといった使い方ができます。 まとめ # ロボットの教示方法やロボット制御APIを利用して連携するイメージが湧きましたでしょうか?他にもロボットと連携するにはROS/ROS2 [9] を利用することも多いです。現在ではAIを使ってロボットを制御したり、クラウド連携するようなシステムが構築されています。今後はさらにAIと融合したロボットシステムが活用されていくと思うと夢が膨らみますね。🤖👏 教示を行うタブレットのような端末。教示ペンダント、ティーチングペンダント、単にペンダントと呼ぶこともある ↩︎ Tool Center Point、ロボットハンドの先端にある動作点 ↩︎ 教示点は3次元座標(X,Y,Z)、姿勢(RX,RY,RZまたはクォータニオン)、ロボット形態情報(関節軸の方向)等が含まれる変数を表す ↩︎ A点からB点までTCPを 直線 で移動させるか、TCPをA点からB点を経由してC点に 円弧 で移動させるかなど ↩︎ デジタル入力(DI)、デジタル出力(DO)、アナログ入力(AI)、アナログ出力(AO)など ↩︎ ロボットの関節にはサーボモーターが取り付けてあり、ロボットコントローラが適切な位置や回転速度を計算してサーボモーター(関節)を動かす ↩︎ 移動命令と教示点データは分離して記録されており、移動命令から教示点データ配列のインデックスを指定するのが一般的 ↩︎ ロボットが作業を行う対象物・加工物を指す。英語のworkpieceの略語として使われる ↩︎ ロボットOSではなくロボット開発を容易にするオープンソースのフレームワークでソフトウェア開発に必要なツールやライブラリが揃っています ↩︎
この記事は夏のリレー連載2025 8日目の記事です。 1. はじめに # 業務で利用しているインフラの仕様を正確に把握できていますか? 個人開発や小規模なシステムであれば、インフラの全体像を把握することは比較的容易です。しかし、組織のシステムが大規模化するにつれて、基盤や利用プロダクトの数は増加し、すべての仕様を把握している人は少なくなります。 重要なのは、すべてを網羅的に理解することではなく、担当領域の仕様を正確に把握し、適切な意思決定に活用できることです。 インフラ仕様把握が困難な理由 # なぜインフラリソースの仕様把握が難しいのでしょうか。さまざまな要因がありますが、筆者は「情報の分散と欠如」が根本原因だと考えています。 ドキュメントの点在 :設計書、運用手順、変更履歴が異なる場所に散らばっている 仕様の不明 :リソースの要件や制約、設計根拠が記録されていない 暗黙知の蓄積 :設定の意図や依存関係が担当者の記憶にのみ存在し、担当者の離脱とともに失われる 発生する課題とその影響 # これらの問題は、DevOpsにおいて以下の課題を引き起こします。 変更時の影響範囲が予測できない 障害対応時に原因特定に時間がかかる 関係者間で共通理解を維持するのが困難 技術的負債の蓄積とリスクの増大 その結果、意思決定が遅れ、変更や改善の着手が後ろ倒しになってしまいます。 解決手段としてのKiro # この課題を解決する手段として注目したのが「Kiro」です。ソフトウェア開発に関するKiroの記事は既に多数ありますが、インフラ視点での活用事例はまだ少ない状況です。 本記事では、KiroのSpecモードを活用してTerraformワークフローに仕様駆動開発を組み込み、ドキュメント起点のIaCにおけるDevOpsの可能性を考察します。 2. 記事の概要 # 対象読者 # TerraformによるIaC経験者 IaCの仕様・ドキュメント管理を強化したい開発・運用者 前提条件 # Kiroバージョン: 0.2.13 HCL/Terraformの基礎知識 AWSリソース構築の基本的な理解 本記事で扱わない範囲 # TerraformやHCLの基礎文法 Kiroの内部AIモデルの仕組み 高度なTerraformモジュール設計 AWS各サービスの詳細な設定方法 3. Kiroとは? # --> Caution 本記事の内容は執筆時点のパブリックプレビュー版に基づいています。最新情報は公式ドキュメントをご確認ください。 Kiro はAWSが開発するAIエージェント統合型IDEです。 従来のIDEとは異なり、自然言語でのやり取りを通じてコード生成を行える点が特徴です。 プレビュー公開直後に招待制のWaitlistに移行しており、注目度の高さがうかがえます。 Kiroの基本的な操作は 公式Docs や、以下弊社ディベロッパーサイトの記事をご参照ください。 KiroでAI開発革命!? アルバムアプリをゼロから作ってみた【その1:要件定義・設計・実装計画】 3.1 Kiroの基本概念 # Kiroは開発プロセスを以下の3段階に構造化します。 Requirements(要件) : 作りたい内容を自然言語のプロンプトで提示し、KiroがEARS(Easy Approach to Requirements Syntax)形式へ自動変換したうえで、その形式に沿って要件を定義。 Design(設計) : 要件を満たすための技術的な構成を定義。 Spec(仕様) : 設計を具体的な実装仕様として確定。 EARSは6つの基本型の定型句で要件を簡潔かつ一貫して表現し、曖昧さを減らすテンプレートです。詳細は本稿の範囲外ですが、概要は以下が参考になります。 見える化する要求仕様 〜 EARS(Easy Approach to Requirements Syntax)を活用したシステム要求の書き方 〜 この段階的なアプローチにより、人またはAIの性格や好みによる「感覚的なコーディング」から厳格な「仕様駆動開発」へのシフトを支援します。 また、RequirementsとDesignは直接要件や設計を記載することで、Specを変更することもできます。 3.2 SpecモードとIaCの親和性 # 特にSpecモードは、要件・設計を文書として整理しつつ、それを直接HCLコードに落とし込む点でIaCとの相性が非常に高い機能です。 IaCの特性を考えると、この親和性の高さは以下の理由によるものです。 インフラ構成は明確な要件と制約に基づいて設計される リソース間の依存関係が明示的である必要がある 変更時の影響範囲を事前に把握する必要がある 長期的なDevOpsを見据えた設計が求められる さらに、仕様や要件をプロンプトとドキュメントビューで確認でき、AIと人が要件・設計・仕様を対話的に磨き込み、コード生成までを一貫できます。 4. 従来のIaC開発の課題とKiroによる解決 # 4.1 従来のIaC開発における問題点 # 多くの組織でTerraformを使ったIaC開発を行っていますが、以下のような課題に直面することが多いです。 コードファーストの弊害 コードから書き始めるため、設計意図が曖昧になりやすい 後からドキュメントを書こうとしても、当時の判断基準を思い出せない コードレビュー時に「なぜこの構成にしたのか」が分からなくなる ドキュメントとコードの乖離 ドキュメント(設計・仕様を含む)とTerraformコードを別管理しがちで、コード変更時の更新が後手となり、意思決定履歴と実装が乖離しやすい 手動変更とドリフト IaC管理外で手動作成してしまう 手で作成されたリソースの責任や管理が不十分で不要なリソースが残り続ける(いわゆるドリフトの恒常化) 属人化の問題 設定の背景や制約が担当者の記憶に依存 担当者の異動や退職により知識が失われる 新しいメンバーが参画時に理解に時間がかかる その結果、意思決定が遅れ、変更や改善の着手が後ろ倒しになります。 4.2 Kiroによるドキュメント化のメリット # IaCの現場では、コードだけでは「なぜそのような仕様や実装になっているのか」が見えにくくなりがちです。KiroのSpecモードでドキュメント化することで、次のような利点があります。 設計プロセスの可視化(対応: コードファースト) 要件から設計、実装までの思考プロセスが記録される 意思決定の根拠と制約条件が明確になる 代替案の検討過程も残すことができる 継続的なドキュメント管理(対応: 乖離) 変更の履歴や背景が自動的に残る レビューや保守が容易になる 仕様変更時の影響範囲を事前に把握できる チーム開発の効率化(対応: 属人化) チーム間で共有しやすいIaC仕様が作れる 関係者間の共通理解を早期に形成できる コードレビューの質が向上する 4.3 従来の課題とKiroによる解決の対応関係 # 従来の課題が、Kiroによる解決でどのように解消されるかを示します。 flowchart LR subgraph KADAI_41[従来の課題(4.1)] A[コードファーストの弊害] B[ドキュメントとコードの乖離] C[手動変更とドリフト] D[属人化の問題] E[意思決定の遅延] end subgraph SOL_42[Kiroによる解決(4.2)] S1[設計プロセスの可視化] S2[継続的なドキュメント管理] S3[チーム開発の効率化] end A --> S1 B --> S2 C --> S2 D --> S3 E --> S1 E --> S2 --> Information 要件・設計・実装のトレーサビリティの仕組みを構築することが重要です。 Kiroの場合は、Requirements・Design・Specの3つのドキュメントで一貫管理し、 生成されるIaCコードとの整合性を保つことで従来の課題解決を実現していると言えます。 5. 実践: KiroでLambdaを使った定期実行システムを構築する # 5.1 要件の整理(Requirements) # まず、自然言語で要件を整理します。例として、定期的なバッチ処理システムを考えてみましょう。 【前提・制約】 - プラットフォーム: AWS - IaC: Terraform(HCL) - Terraform v1.13 を使用 - hashicorp/aws の 5.x を使用 【要件】 - 毎日決まった時刻にバッチ処理を自動実行したい - バッチ処理ではデータの集計や整理を行いたい - 処理中にエラーが発生した場合はCloudWatchアラームで通知したい - サーバーレスでコスト効率よく実装したい - スケジュールは柔軟に変更できるようにしたい - 将来的に処理内容を拡張できるようにしたい ↓入力後のKiroの返答 簡単な要件をプロンプトに入力後のKiroの返答。最後に内容に問題がないかを確認してから次のフェーズに進む 5.2 設計の具体化(Design) # Kiroに要件を入力すると、以下のような設計案が得られます。 筆者は追加でDevOpsについての記載と、コードのコメント等を日本語で書くように追加しました。 ↓入力後のKiroの返答 【設計】シンプルなサーバーレス構成 EventBridge Scheduler → Lambda → CloudWatch の直接的な流れ 毎日朝9時に自動実行(cron(0 9 * * ? *)) エラー時はCloudWatchアラーム → SNS → Email通知 【主要コンポーネント】 EventBridge Scheduler: 毎日朝9時のスケジュール管理 Lambda関数: バッチ処理実行(タイムアウト5分) CloudWatch Logs: ログ集約(30日保持) CloudWatch Alarms: エラー監視・通知 SNS Topic: Email通知 SQS Dead Letter Queue: 障害時の再試行対応 【セキュリティ】 最小権限の原則: Lambda実行ロール(CloudWatch Logs書き込みのみ)、EventBridge Schedulerロール(Lambda実行のみ) リソースタグ: 全リソースに統一タグ付与 【運用・DevOps】 Terraform: 完全なIaC管理 日本語対応: コメント、説明文、エラーメッセージを日本語で記述 監視: Lambda実行状況、エラー率の監視 コスト効率: サーバーレスで必要時のみ課金 5.3 仕様の確定とコード生成(Spec) # 要件・設計が確定すれば、Specでコードを生成しています。 Kiroが生成したHCLコードです(抜粋)。 IAMロールやSNSトピックなど一部の補助リソース定義は簡潔さのため省略しています。 細かい箇所は筆者側から指示を出すことで修正しましたが、 要件と設計で指定した内容のリソースが作成されました。 terraform { required_version = ">= 1.13.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } # デフォルトタグ付きのプロバイダー設定 provider "aws" { region = "ap-northeast-1" default_tags { tags = { Project = "batch-processing-system" Environment = "production" ManagedBy = "Terraform" } } } # Lambda Function resource "aws_lambda_function" "batch_processor" { filename = "batch_processor.zip" function_name = "daily-batch-processor" role = aws_iam_role.lambda_role.arn handler = "index.handler" runtime = "python3.12" timeout = 300 description = "毎日実行されるバッチ処理用のLambda関数" tags = { Name = "daily-batch-processor" Purpose = "automated-batch-processing" } } # EventBridge Scheduler resource "aws_scheduler_schedule" "batch_schedule" { name = "daily-batch-schedule" group_name = "default" description = "毎日午前9時にバッチ処理を実行するスケジュール(JST)" flexible_time_window { mode = "OFF" } schedule_expression = "cron(0 9 * * ? *)" schedule_expression_timezone = "Asia/Tokyo" target { arn = aws_lambda_function.batch_processor.arn role_arn = aws_iam_role.scheduler_role.arn } } # SchedulerがLambda関数を実行できるように許可 resource "aws_lambda_permission" "allow_scheduler" { statement_id = "AllowExecutionFromScheduler" action = "lambda:InvokeFunction" function_name = aws_lambda_function.batch_processor.function_name principal = "scheduler.amazonaws.com" source_arn = aws_scheduler_schedule.batch_schedule.arn } # Lambda関数のエラーを監視するCloudWatchアラーム resource "aws_cloudwatch_metric_alarm" "lambda_error_alarm" { alarm_name = "lambda-batch-processor-errors" comparison_operator = "GreaterThanThreshold" evaluation_periods = 1 metric_name = "Errors" namespace = "AWS/Lambda" period = 300 statistic = "Sum" threshold = 0 alarm_description = "バッチ処理Lambda関数のエラーを監視するアラーム" alarm_actions = [aws_sns_topic.alerts.arn] dimensions = { FunctionName = aws_lambda_function.batch_processor.function_name } tags = { Name = "lambda-error-alarm" Purpose = "error-monitoring" } } 5.4 ドキュメントからコードへの一貫した流れ # このプロセスにより、ドキュメント → HCL → インフラという一貫した流れが実現されます。 要件定義 : ビジネス要件を自然言語で記述 設計検討 : 技術的制約と要件を照らし合わせて構成を決定 仕様確定 : 実装レベルの詳細を含む仕様として確定 コード生成 : HCLコードとして出力 インフラ構築 : Terraformでplan/apply実行 5.5 要件・設計を再利用して類似リソースを作る # 5.1〜5.3で確定したRequirements/Design/Specを活用して、類似のリソースを簡単に作成できることを確認しました。 例えば「週次実行のバッチ処理を追加したい」という要件に対して、既存のSpecを複製し、以下の簡単な指示をKiroに与えるだけで新しいリソースが生成されました。 既存の日次バッチ処理の仕様をベースに、以下の差分で週次バッチ処理を作成してください: - スケジュール: 毎週日曜日の午前10時 - リソース名: weekly-batch-processor - 処理内容: 週次レポート生成 Kiroは既存の設計パターンを理解し、命名規約やタグ付け、IAM権限設定などを一貫して適用した新しいHCLコードを生成しました。 このように、一度確立したRequirements/Design/Specがあれば、類似リソースの作成が大幅に効率化されることが確認できました。 6. ドキュメントとHCLの親和性 # 6.1 HCLの特性とドキュメント化 # HCLは抽象度が高く、構成要素や依存関係を明示的に記述する形式です。これは仕様ドキュメントと非常に親和性があります。 宣言的な記述方式 HCLは「何を作るか」を宣言的に記述します。これは要件や設計書の記述方法と本質的に同じです。 # 設計書: "毎日決まった時刻にバッチ処理を自動実行する" resource "aws_scheduler_schedule" "batch_schedule" { name = "daily-batch-schedule" description = "毎日午前9時にバッチ処理を実行するスケジュール" schedule_expression = "cron(0 9 * * ? *)" } 構成要素の明確な対応関係 ドキュメントで書いた構成要素が、そのままHCLのresourceやmoduleへ対応 仕様で定義した変数や要件が、そのままvariableやoutputへ展開 依存関係がコード上でも明示的に表現される 6.2 仕様駆動開発の実現 # Kiroを使うことで、以下のような仕様駆動開発が実現できます。 --> Information トレーサビリティ(traceability)の一般的な定義 要件・設計・実装・テストなどの成果物間の関係を双方向にたどれる性質 トレーサビリティの確保 Kiroでは要件から実際のリソースまでの一連の流れが記録され、双方向の追跡が可能になります。 flowchart LR RQ[要件] DS[設計] SP[仕様] HCL[HCLコード] RS[リソース] RQ --> DS --> SP --> HCL --> RS RS -. フィードバック .-> RQ 変更管理の改善 仕様変更時は要件レベルから見直し 影響範囲を設計段階で把握 コード変更の妥当性を仕様で検証 仕様を残すことで、HCLコードの意味が曖昧にならない点が重要です。結果として、「コードは仕様から派生したもの」であることが保証され、設計と実装の乖離が起きにくい点が最大の価値です。 7. 今後の展望 # AIとの協調開発の進化 KiroのようなAIエージェント統合型IDEは、今後さらに進化していくことが予想されます。 より高度な設計パターンの提案 過去の実装例からの学習機能 リアルタイムでの最適化提案 IaC開発文化の変革 仕様駆動開発の普及により、インフラ開発の文化自体が変わる可能性があります。 「なぜ」を重視する設計文化 ドキュメントファーストな開発スタイル 継続的改善を前提としたDevOps インフラ開発における「感覚的なコーディング」から「仕様駆動開発」への転換は、単なるツールの導入以上の価値をもたらします。組織全体でのインフラ管理能力の向上と、技術的負債の削減を通じて、より安定したDevOpsの実践を推進していきましょう。 8. まとめ # KiroのSpecモードを利用することで、HCLによるIaC管理に以下の新しい価値をもたらします。 技術的価値 ドキュメントを基点にした一貫性の高い構成管理 ドキュメントから直接コードを生成するシームレスな開発体験 ドキュメントとHCLの高い親和性により、設計と実装の乖離を防止 組織的価値 チーム内での知識共有とナレッジ蓄積の促進 関係者間の共通理解を継続的に促進 長期的なDevOps推進および保守にかかるコストの削減 文化的価値 コードを書く前に仕様を整理する文化の定着 設計決定の背景と根拠を明文化する設計思考 継続的改善を前提とした開発プロセス Terraformに慣れている方でも、Kiroを取り入れることで「コードを書く前に仕様を整理する」文化を自然に導入できます。これにより、長期的なDevOpsの実践しやすさとチーム開発の生産性を両立し、IaCにおけるDevOpsの持続可能性が高まるでしょう。 繰り返しになりますが、筆者が最も重要だと考えるのは、人またはAIの性格や好みによる「感覚的なコーディング」から「仕様駆動開発」へシフトすることです。 これはインフラに限らず、ソフトウェアにおいても概ね同じことが言えると考えます。 これを意識して、これからのAI時代と共にDevOpsによる改善活動を考えていきたいと思います。
この記事は夏のリレー連載2025 5日目の記事です。 最近の生成AI技術の進歩は目覚ましく、「AI?ああスター〇ォーズの金色のロボットでしょ [1] 」なレベルの認識しかない筆者であってもそれなりにAIを利用して作業ができるようになってきました。 分かっていなくても使えるということは素晴らしい進化だとは思いますが、この業界、すなわち生成AIの技術やそれを活用した仕組みを提供する可能性がある側で仕事をするにあたっては大まかな枠組みだけであっても頭に入れておいたほうが良いですよね。 生成AIについての情報は世に多いですが、素人にも分かり易く [2] 概念を説明するような情報に辿り着かなかったこともあり、筆者は初期の学習に苦労しました。そこで、現時点で理解できたことをベースに「なんとなく生成AIの世界観がイメージできる」ところを目指してまとめてみたいと思います。 もちろん素人である筆者が公開情報などをもとに学習した内容のまとめですので、正確さに欠ける内容であったり誤解に基づいた内容が含まれるかもしれません。当然のこととして記事の文責は筆者にあります。 はじめに:そもそも生成AIって何なのさ # AIという単語はよく使われますが、「何がAIで何がAIではないか」「なぜその両者に違いがあるのか」といった問いかけに対して厳密な解答ができるひとは少ない(いない?)のではないかと思っています。 AIという概念自体はかなり古く、100年近く前、機械による計算ができるようになった頃に遡ります。映画「イミテーション・ゲーム」の題材にもなった暗号解読機を生んだアラン・チューリングや初期のコンピュータ理論を構築したジョン・フォン・ノイマンなどの先人の時代から、「人間の知的活動」を機械に行わせる試みが行われてきました。そして1965年のダートマス会議と呼ばれる研究発表会で、ジョン・マッカーシーが“Artificial Intelligence (人工知能)”という表現をしたのがAIという単語の出自と言われています。 筆者の解釈ですが、AIとは「知能≒人間の知的活動」を代替する機械であることについて異論は無いようです。しかし、何が「知能」であるかに関する厳密な定義は難しく、提唱者のマッカーシー教授自身も「知能を(人間の知能と結び付けることなく)厳密に定義する」ことが困難であると認めています。例えばキャッチボールをすることが「知能」によるものと言ったら文脈によっては違和感があるように、何が人口「知能」であるのかを正確に定めることが難しくなっていそうです。本稿の目的はAIを正確に定義することではありませんので、「人間の知的活動」全般を代替する機械(コンピュータ)がAIであるというとらえ方をしていただければ概念が捉えやすいのではないかと思います。 「人間の知的活動」全般と言ってしまうと領域が広大なため、人工知能学会では提供するAIマップβ2.0の中で以下イメージ図のようにAIの課題領域を分類しています。 キャッチボールの例で言うと、「白い球体がどんどん大きくなっている」のを認識して「ボールが飛んできている」のだと分析し、「ボールの到達位置」を予測して「グローブの位置」を制御する というように、知的活動は様々な側面を持ちます。 生成AIとは生成・対話系の課題領域を中心とした [3] 機能を持つAIであり、特に「新しいもの(文章、画像、データなど)」を作り出すことができるという意味で人間の「知的労働」を代替できるのではないかと強く期待 [4] されています。 生成AIを支える技術 # AIの概念が生まれた時期に「人間の脳細胞の働き」を機械で再現するニューラルネットワークというアイデアが提唱されています。人間の脳細胞(ニューロン)は「複数の電気信号による入力を受けて次の脳細胞に信号を伝達する」ことを繰り返して活動することが分かっていました。そこで「複数の入力をもとに出力する」ニューロンのモデルをネットワークのように組み合わせることで脳機能を再現しようとする試みです。 構築されたニューラルネットワークはある入力に対して出力を行い、出力に応じてネットワークを調整していくことで「正しい≒人間が期待する」結果を出力できるようにパラメータを調整していくことができます。ちょうど人間が試行錯誤しながら知識を習得していくように、この調整段階をAIの「学習」と呼びます。 ニューラルネットワークは脳の仕組みを再現しようとする試みのため、どのようなモデルで脳の働きを表すのかによって様々な種類 [5] のものがあります。どのようなモデルを採るかによってその特性は変わりますが、いずれにしろネットワーク状のモデルですからコンピュータの演算量は多くなる傾向があり、コンピュータ性能による限界もあってブームと衰退を繰り返す形で進歩してきました。 2017年にGoogleの研究者によって発表されたトランスフォーマー [6] というモデルは、それまでのモデルと異なり系列データの関係性を評価するのに「アテンション」というメカニズムを採用しました。これは機械翻訳などの入力から別の系列の出力を作成する「系列変換」の性能を改善することを目指したもので、それまでのアプローチと異なり膨大な入力に対しても関係性を評価できることで自然な(妥当な)出力が可能という特色を持っていました。この大量の情報に対しても関係性を評価できる特色は、コンピュータ側の性能進化とも相まって生成領域で活用されていくことになります。生成AIの火付け役の一つであるOpenAIのGPT(Generative Pre-trained Transformer)もトランスフォーマーの派生モデルの一つです。 入力に対して妥当な出力が可能になるということは、予め様々な情報を事前学習をさせておくことで「新しい」妥当な出力をAIが生成できるようになるということでもあります。例えばベートーベンの曲を大量に学習した人が、「ベートーベンが作曲したような [7] 」新しい曲を作れるようなものでしょうか。昨今の技術を利用してみた感想ですが、生成AIの出力はかなり人間の出力に近づいてきているものと思います。 生成AIの影響、そして限界 # 生成AIが人間に近い知的作業を担えるようになったことで、「調べものをしてレポートにまとめる」とか「要求を分析して適した設計を行う」とか、これまで人間でないと難しかった作業をコンピュータが分担できるようになる可能性があります。人がやると時間がかかるような作業もかなりの速度で対応してくれる [8] ので、このような作業に関しては生成AIの活用によって生産性が飛躍的に向上できる可能性があるということです。 ただし、AIが出力する内容が「100%妥当」ということはありません。これはAIへの指示が悪かった [9] 時によく感じることでもありますし、ハルシネーションと呼ばれる正確ではない情報の出力や、ミスアライメントと呼ばれる「悪い」AIの誕生があることなども報告されています。人間の脳をベースにした試みですので、人間と同じように「あいまいな指示だと上手く結果が出せない」ことや「見てきたような嘘をつい」たり「倫理に外れた言動を示す」ことがあるのは当然かもしれません。 知的労働を分担できるということは、人間の作業の一部分に関しては生成AIで代替される可能性があります。例えば ILOのレポート では雇用の1/4が影響を受けると報告されています。しかし、そのレポート中にも述べられていますが、人間の知的作業が完全に置き換わることは難しいようです。 前述のような技術的課題を乗り越えられるかに関しては専門家でないためなんとも言えない部分もありますが、技術的課題がなくなったとしてもAIが意思決定し得ないないという問題は残ると考えています。これはAI側の能力という技術的な問題ではなく、「AIが決定したこと」に対する責任の所在について合意が形成できていないという社会構造的な課題です。 人間個人であれば出したものの責任は当人になりますし、それが組織であれば組織(の責任者)のものになるというのが現在の社会の基礎 [10] になっていると筆者は考えます。つまり、AIが出力した結果によって問題が発生した場合に「どのように責任がとられるか」が明確にならない限りAIは独立して人の作業を代替できないということです。遥か未来には「AIがやったことだからね」という合意がある世界が待っているのかもしれませんが、現時点の感覚では問題が起きた際にAIの責任として飲み込むことには強い抵抗があります。 AIとどう付き合っていくべきか # 筆者は軽く使ってみた程度の経験でしかないですが、現時点の状況では生成AIを「指示に従って妥当な出力をしてくれる機械」と捉えるよりも「(懸命に作業をしてくれる)新人さん」というくらいの位置づけで捉えるほうが良いように感じています。「明確な指示」を出せば結果を出してくれるけれども「間違い」をすることもあるからチェックは必要だし、間違えたときの責任は指示を出した側がとる必要がある、というとらえ方ですね。逆を言えば新人さんにお願いしていたような雑多な作業は生成AIが代替できる可能性があるとも言えます。米国などで知的労働市場でエントリーレベルの求人が減っている [11] というのは生成AIの台頭(あるいはその期待)の一つの証左かもしれません。 かつて産業革命においては蒸気機関や内燃機関などの機械動力によって労働集約型の業務は資本集約型の業務に転換されていきました。道路工事を例にとると、機械動力による生産性向上が大きいため、多くの労働力(労働者の肉体労働)を集めていたものが資本を集め(建設機械を導入し)て少ない労働力で実施する形に変わったということです。今後、知的労働についても同じような構造の変化が起きていく可能性は高いものと思います。 短絡的にみると、経験の浅い人間を減らして少人数の「ベテラン」だけで業務を行うことがもっとも効率が良さそうです。しかし、長い目で見れば「ベテラン」は時とともに引退していくものですから、事業の継続が難しくなることは明らかです。AIと分業していくことを踏まえたうえで人間が担う役割のスキルを成長させていくことが求められることになっていくでしょう。 重機を使った道路工事で言えば「どこを削りどこを埋めるかを決め」るところが人間、実際に「土を崩したり石を運んだりする」作業が機械、「問題発生時に対処を検討したり調整する」のが人間、と分業されています。おなじように知的労働においても、どのような形になるか見えていない部分も多いですが、人間がやらざるを得ない領域は残ることでしょう。ここで生成AIの「意思決定ができない」あるいは「責任がとれない」特色を踏まえると、これからの知的労働では「意思決定をして責任が取れる」ことが人間に求められるような気がします。 筆者は教育についても門外漢ではありますが、「深く考えて、自分の責任範囲の中で意思決定 [12] してみる」ことが、あるいはそのような機会を提供することが、これからのキャリア形成において重要になっていきそうです。 まとめ # 生成AIについて背景から現時点での能力・将来の展望までを筆者の素人理解でまとめてみました。生成AIって興味はあるけど、、、のようなときに どなたかのお役に立てるようでしたら幸いです。 本稿では個別の技術情報についてはあまり触れられていませんが、当デベロッパーサイトでもこのリレー連載をはじめとして 詳しい方がいろいろと発信 してくれていますので興味が湧きましたらそちらも訪ねていただけると嬉しいです。 もしかすると通じないネタかもしれないですが、子供時代にテレビや映画でロボットが人間と会話している場面を見て「アレがえーあいってヤツなのかぁ」なんて訳も分からず納得した経験は誰にでもあるのではないかと思います。と、書いてみてSiriやAlexaをAIと呼ばないように、技術が普及したことにより かえって最近はAIという呼称を使うことが少なくなっているような気もしてきました。 ↩︎ 例えば「生成AIとは深層学習技術によって既存データを学習し、より人間が生み出すものに近い形で新規のものを生み出す特色があります。」と言われてみても周辺の概念が分からない状態では「え、コンピュータが学習できるってどういうこと?」とか「深層学習と新しいものを生み出すってどう関係するの?」とか?マークがいっぱい並ぶだけですよね。(筆者はそうでした) ↩︎ ものを生成するにあたって入力情報の「分析」/「推定」や指示に応じた「設計」などの領域の機能も有しているように感じられます。が、概念をつかむにあたっては主に「生成」の機能を持つという理解で十分だと思います。 ↩︎ 「知的労働を代替できるのではないか」という期待はAIの概念そのものです。筆者の肌感覚ですが、生成AIに対する期待の大きさは実際に知的労働に携わる人間が実際に置き換えられるのではないかという期待(あるいは危機感)を持っている点がこれまでの(いつかできるだろうという夢に近かった)AIへの期待と異なるように思います。 ↩︎ 有名なところではネットワークを多層に重ねることでより深い学習(深層学習)を可能にしたディープニューラルネットワーク(DNN)、画像処理などに強みを発揮する畳み込みニューラルネットワーク(CNN)、時系列データを扱うことができるため文脈解釈などが可能な再帰的ニューラルネットワーク(RNN)などがあります。 ↩︎ 「 Attention Is All You Need 」という映画か歌のタイトルのような論文ですが、その後の AIの進化に大きく寄与しており論文の引用数も非常に多い とのことです。 ↩︎ 子供が作ったベートーベン風とプロの音楽家が作ったベートーベン風の完成度が違うように、作者の理解度によってどの程度妥当なものになるのかは変わってくるものと思います。人間の知能を模しているので当然ではありますが、生成AIにとってもどのように学習(成長)させるかが肝である辺りは興味深いですね。 ↩︎ お気付きかもしれませんが、本稿のイメージ図は生成AIを活用して作成しています。筆者が不慣れなことも手伝ってイラストとして「完全にイメージどおり」とまではいきませんでしたが、手作業で作成したとしたら数時間はかかるようなイラストが数分で作れてますのでその効果については論を待たないと思います。 ↩︎ 生成AIを使ったことがある方はよくご存じかもしれませんが、「この文書を100字程度に要約して」のようにある程度明確な指示に対してはかなり良い結果を出してくれる印象です。しかし、「日本の戦国時代を良い感じに説明して」のように入力が膨大だったり要求が不明確だったりすると結果がぶれがちな印象です。 ↩︎ 例えば子供の行為の責任は保護者も負う、万一のときのために保険をかけるなど、今の社会では個人がその能力で追える粒度に責任を分解しているように思います。社会科学の専門家でない筆者の感覚に過ぎませんが、第一義的には当事者が責任を取るというのが社会を成り立たせてる要素、つまりは発生した問題を周りが飲み込みうる理由のように感じます。 ↩︎ 経済状況なども絡むので一概に「AIが仕事を奪った」と言うことはできないですが、 単純業務や定型業務は生成AIで代替可能 と捉えている経営者が一定程度いるとのことです。 ↩︎ たとえば会社員の立場であるなど、最終的な意思決定は難しい立場であることが多いとは思います。が、担当作業の範囲内などで「どう進める」かを検討/意思決定して上司の承認をもらいに行くような形であれば無理なく意思決定の経験ができるのではないでしょうか。 ↩︎
1. はじめに # AWSアーキテクチャの設計って、どうすれば正しく評価できるか悩むことってありませんか?たくさんのサービスが絡みあって、どこをどう見ればいいのか迷ってしまいますよね。(少なくとも私はよく迷います。) この記事は、そんな悩みを解決する手助けになる「AWS Well-Architected Framework(以降WAと呼称)」という、AWSに触り始めたころに必ず出会うであろう基本的な設計原則と、それに沿ったアーキテクチャの評価方法についてまずは整理をします。 そして、今回はそこから一歩だけ踏み込んで、生成AIツールの「Kiro」を使ってWAに準拠したアーキテクチャが作れるかどうかを試してみたいと思います。 生成AIがどれくらいWAの原則を理解してアーキテクチャに反映できるのか一緒に見ていきましょう! 2. 記事の前提 # この先の内容では、WAの一般的な設計原則や6つの柱の詳細には深く立ち入りません。WAの公式ドキュメントがとても充実しているので、詳細を知りたい方はそちらをご覧ください。 【公式ドキュメント】AWS Well-Architected フレームワーク この記事でメインとなる内容は「生成AIツールを使って、WAに沿ったアーキテクチャが作れるのか?」というものです。 私が実際にKiroを使いながら感じたことや、ツールの特徴も交えながら、その可能性を探っていきます。 3. 一般的な設計原則と6つの柱 # 3.1 一般的な設計原則 # 詳細には深入りしないと言ったものの、WAってなんだったっけという方もいると思うので簡単にまとめておきたいと思います。 WAには、クラウド上で高品質なアーキテクチャを設計するための以下のような一般的な設計原則があります。 これらは普遍的な設計の指針となります。(一部解釈しやすいように自分なりの表現にしています。) 容量の推測が不要なアーキテクチャか クラウドの柔軟性を活かした、必要な時に必要なリソースだけを使う構成になっているか。 本稼働スケールでテストができるアーキテクチャか 実際のユーザー負荷をかけたテスト環境を簡単に用意でき、本稼働での問題を事前に見つけれるか。 自動化による実験ができるか アーキテクチャをコードとして管理し、実験や改善を繰り返せるようになっているか。 アーキテクチャを常に進化させることができるか ビジネスニーズの変化に合わせて、システムを柔軟にアップデートしていくことができるか。 データドリブンな意思決定ができるか 勘や経験だけでなく、データに基づいて設計の良し悪しを判断できる仕組みがあるか。 ゲームデー(疑似障害発生日)で訓練する 障害をあえて起こすことで、万が一の時にチームがどう動くべきか訓練しているか。 3.2 6つの柱とベストプラクティス # そして、一般的な設計原則を土台として、アーキテクチャを評価するための6つの「柱」からWAは成り立っています。 優れた運用: 効率的な運用プロセスと継続的な改善に焦点を当てます。 セキュリティ: データの保護、アクセス管理、インシデント対応など、システムの安全性を高めるためのものです。 信頼性: 障害が発生しても、サービスが正常に機能し続ける能力です。 パフォーマンス効率: 変化する需要に合わせ、リソースを効率的に使うための考え方です。 コスト最適化: 最低限のコストで最大のビジネス価値を生み出すことに着目します。 持続可能性: 環境への影響を最小限に抑える、長期的な視点での設計です。 3.1 と 3.2 をイメージでまとめるとこんな感じだと思います。 また、これらの6つの柱のそれぞれにいくつかの「ベストプラクティス」が存在しています。 詳細については 【公式ドキュメント】AWS Well-Architected フレームワーク をご覧ください。 4. 評価の仕方 # 簡単にWAの概要を整理しましたが、この内容を見たときに「AWSリソース構築時の指針になるのはわかるけど、どうやって使うの?そして評価をするの?」という疑問が湧きました。 そこで調べていくうちに、構築したAWSアーキテクチャがWAをどれぐらい満たしているかを知る1つの方法として、「AWS Well-Architected Tool」というサービスがあることをしったので、実際に使ってみました。 以下の図で簡単ですが触ってみた際のポイントのみ紹介しておきます。 【柱のベストプラクティスを満たすためのチェック】 【柱のベストプラクティスを満たすための改善提案】 【柱のベストプラクティスを満たすための改善の仕方】 このToolの使い方の流れとしては上図の順番のように、 6つの柱のそれぞれに用意されているベストプラクティスを満たすための質問(選択肢)から、構築したアーキテクチャがそれらを満たしているかを選ぶ 改善すべき項目をリストアップしてくれる 「推奨される改善項目」から必要と感じる改善項目のリンクに飛ぶ 「Implementation guidance」に従って改善していく という流れを6つの柱のベストプラクティスごとに繰り返す、ってな感じです。 もちろん、すべてのベストプラクティス(質問数でいうと300弱ある)を完全に満たすことはできないので(コストを安くしたいのに高可用性は維持したい、のようなトレードオフが起こるため)、その時のアーキテクチャの要件に合わせて柔軟に評価をしていく必要がありますね。 AWS上に構築済みのアーキテクチャをこのツールで評価することで「アーキテクチャがどの程度WAの基準に達しているか」「どの項目を改善すべきか」が明確になります。 しかし、WA Toolを使ってみると以下の点でやや微妙だと感じることがありました。 アーキテクチャの改善点を洗い出しはできるが、評価と改善はあくまで手動(質問に答える→改善) 選択肢をポチポチしていく作業の手間がかかる これらの手間がかかる作業の負担を減らす方法がないかなーと色々調べていたところ、「Kiro」というツールが話題に挙がっていたので、このツールを使って 「そもそもWAに準拠したAWSアーキテクチャを作らせる」 「作らせたアーキテクチャが本当にWAに準拠しているかをチェックさせる」 ということを試そうと思ったので試します! 5. Kiroについて簡単に # 簡単に「Kiro」について紹介しますが、一言でまとめれば「仕様駆動型で開発支援ができる生成AI IDE」です。 AWSの公式ブログ「 Introducing Kiro 」にも紹介されている通り、普段使っている自然言語で指示を出すだけで、要件の定義、設計のデザイン、タスクリストの作成を行い、タスクリストに従って成果物をよしなに作ってくれちゃいます。(どんどんこういったツールが増えていきますね…。) 詳しくは「 Introducing Kiro 」や、弊社デベロッパーサイトに投稿されている以下の記事を見ていただければKiroの特徴や使い方がわかると思います! 【参考記事】 KiroでAI開発革命!? アルバムアプリをゼロから作ってみた【その1:要件定義・設計・実装計画】 6. WAに沿ったアーキテクチャを作ってみよう # では早速、Kiroを使ってWAを満たしたAWSアーキテクチャを作ってみましょう。 以下は私がKiroを使った際の環境です。 環境 : Windows 11、Powershell Kiro(0.2.13) : Windows版をインストール して利用。 ※もしWSL2上で使う場合はこちらの記事を参照してみるといいかもです。 KiroでWSLに接続する方法 今回「WAに準拠したAWSアーキテクチャ」を作るにあたり、テスト用の「簡易的なタスク管理アプリ」を動かすことを想定したアーキテクチャを作成するように指示を出しました。 ただ、「タスク管理アプリを動かすためのWAに準拠したAWSアーキテクチャ」をKiroに作ってもらっていますが、作成過程やすべての成果物(仕様書、設計書、タスクリスト、コードなど)、アプリケーションの詳細については触れないものとします。(上記で紹介した記事でKiroがアプリケーションを作成する過程などは詳細を知ることができると思います。) あくまでこの記事では「WAに準拠したAWSアーキテクチャが作れるか」に焦点を絞り、Kiroの挙動などは最小限の紹介にとどめたいと思います。 6.1 出来上がったAWSアーキテクチャ # Kiroとの対話を通じて、以下の図に示すAWSアーキテクチャがCloudFormationテンプレート形式でできあがりました。 簡単にCloudformationテンプレートに定義されたリソースと構成について説明をすると、 【使用したリソース】 コンピューティング Amazon EC2 : アプリケーションサーバー(Auto Scaling Group) Application Load Balancer (ALB) : 負荷分散とHTTPS終端 Auto Scaling : 需要に応じた自動スケーリング ネットワーク Amazon VPC : プライベートネットワーク環境 サブネット : パブリック、プライベート、データベース用に分離 NAT Gateway : プライベートサブネットからのインターネットアクセス Internet Gateway : パブリックサブネットのインターネット接続 データベース Amazon RDS (PostgreSQL) : Multi-AZ構成のマネージドデータベース RDS Parameter Group : データベース設定の最適化 RDS Subnet Group : データベース専用サブネット セキュリティ AWS KMS : データベース暗号化用のキー管理 Security Groups : ネットワークレベルのファイアウォール IAM Roles : 最小権限の原則に基づくアクセス制御 監視・ログ Amazon CloudWatch : メトリクス監視とアラート CloudWatch Logs : アプリケーションとシステムログ CloudWatch Dashboard : 統合監視ダッシュボード Amazon SNS : アラート通知 【構成について】 高可用性設計 Multi-AZ構成 : 2つのアベイラビリティゾーンにリソースを分散 冗長化 : ALB、NAT Gateway、RDSがMulti-AZ対応 自動復旧 : Auto Scaling Groupによる障害インスタンスの自動置換 セキュリティ設計 ネットワーク分離 : VPC内でパブリック/プライベート/データベースサブネットを分離 暗号化 : RDS、EBS、S3の保存時暗号化を実装 アクセス制御 : IAMロールとセキュリティグループによる最小権限アクセス HTTPS通信 : ALBでSSL/TLS終端(証明書は別途設定) という感じです。 6.2 どうやって作成させたか(プロンプトの内容について) # プロンプト指示(クリックで展開) #指示 ・disire-app.mdに記載されている内容のアプリケーションをAWS上で構築したい。 ・well-arch.mdで定義されている観点をなるべく満たすようにアーキテクチャを定義してください。 #条件 ・well-arch.mdの観点をすべて満たす必要はないです。 ・作成するアプリケーションとアーキテクチャは最小構成としてください。 上記の指示内容で指定している「well-arch.md」がWAの評価観点を整理したドキュメントになっています。 well-arch.md(クリックで展開) # AWS Well-Architected フレームワークの概要 ## 1. 一般的な設計原則 システムを設計・運用する際に重要な6つの考え方です。 * **容量の予測は不要にする**: クラウドの柔軟性を活かし、必要な時に必要な分だけリソースを増減できるように設計しましょう。 * **本番と同じ規模でテストする**: 本番環境と同等のテスト環境を簡単に構築し、リスクを減らしながらコストを抑えて検証しましょう。 * **自動化で色々な設計を試す**: インフラや設定をコードで管理し、自動化することで、迅速かつ安全に様々な設計を試すことができます。 * **常に進化する設計を考える**: 常に変化するビジネスや技術に合わせて、継続的に改善できる柔軟なシステムを構築しましょう。 * **データに基づいて判断する**: 感覚ではなく、システムから得られる具体的なデータ(性能やコストなど)に基づいて、改善策を決定しましょう。 * **「ゲームデー」で練習して改善する**: 障害発生を想定した訓練を定期的に実施し、実際のトラブル発生時に迅速に対応できる体制を整えましょう。 --- ## 2. 6つの柱とベストプラクティス AWS Well-Architected フレームワークは、以下の6つの柱に基づいてシステムの品質を評価・改善します。 ### ① オペレーショナルエクセレンス(運用の優秀性) システムを効率的に実行し、継続的に改善する能力です。 * **組織**: チーム全員が共通の目標を理解し、協力し合える体制を構築します。 * **準備**: 問題発生時の対応計画を立て、訓練を通じてチームの準備状況を高めます。 * **運用**: 日常的な運用を効率化し、問題に迅速に対応できる仕組みを整備します。 * **進化**: 運用から得た教訓を活かし、システムとプロセスを継続的に改善します。 --- ### ② セキュリティ(安全性) データやシステムを脅威から保護し、セキュリティを強化する能力です。 * **ID およびアクセス管理**: 厳格なアクセス制御で、誰が何にアクセスできるかを管理します。 * **検出**: ログやメトリクスを活用し、不審な動きを常に監視して迅速に検出します。 * **インフラストラクチャの保護**: ネットワークやサーバーなどの基盤を多重に保護します。 * **データ保護**: データの分類、暗号化、バックアップで大切な情報を守ります。 * **インシデントへの対応**: セキュリティ問題発生時の対応手順を事前に準備し、被害を最小限に抑えます。 * **アプリケーションのセキュリティ**: アプリケーション開発の全段階でセキュリティ対策を組み込みます。 --- ### ③ 信頼性(安定性) システムが期待通りに一貫して動作し続ける能力です。 * **基礎**: サービス制限を適切に管理し、安定したシステム基盤を構築します。 * **ワークロードアーキテクチャ**: マイクロサービスなどの設計で、一部の障害が全体に影響しないようにします。 * **変更管理**: システム変更を自動化し、影響を最小限に抑え、迅速なロールバックを可能にします。 * **障害管理**: 障害は発生するものと想定し、自動復旧や定期的なバックアップテストを行います。 --- ### ④ パフォーマンス効率(性能効率) クラウドを効率的に利用し、システムの性能要件を満たす能力です。 * **アーキテクチャの選択**: ワークロードに最適なリソースと設計方法を選び、性能を最大化します。 * **コンピューティングとハードウェア**: アプリケーションの特性に最適なコンピューティングサービスを選択します。 * **データ管理**: データの種類やアクセス方法に合わせた効率的な管理方法を選びます。 * **ネットワークとコンテンツ配信**: ネットワーク設定やCDNを利用し、応答速度を改善します。 * **プロセスと文化**: チーム全体でパフォーマンスを継続的に改善する文化を育みます。 --- ### ⑤ コスト最適化(費用対効果) システムを最低価格で実行し、最大のビジネス価値を実現する能力です。 * **クラウド財務管理を実践する**: コストを管理するチームとプロセスを整備します。 * **経費支出と使用量の認識**: コストを明確に可視化し、無駄な支出を特定します。 * **費用対効果の高いリソース**: 料金モデル(オンデマンド、リザーブドなど)を考慮して、最適なリソースを選びます。 * **需要を管理しリソースを供給する**: 需要に合わせてリソースを自動で増減させ、無駄をなくします。 * **継続的最適化**: 新しい技術やサービスを定期的に見直し、コスト効率を改善します。 --- ### ⑥ 持続可能性(環境への配慮) エネルギー消費を最小限に抑え、環境への影響を継続的に改善する能力です。 * **リージョンの選択**: 環境への影響を考慮して、サービス提供地域を選定します。 * **需要に合わせた調整**: 必要な時だけリソースを稼働させ、エネルギー消費を抑えます。 * **ソフトウェアとアーキテクチャ**: 電力消費を抑える設計やプログラミングを工夫します。 * **データ管理**: 効率的なデータ保存方法を選び、不要なデータは削除します。 * **ハードウェアとサービス**: 高効率な機器やマネージドサービスを活用します。 * **プロセスと文化**: チーム全体で環境に優しいシステム運用文化を築きます。 「desire-app.md」については触れません。 ざっくりにはなりますが、作成しようとしたタスク管理アプリはFlaskとHTML/Css, javascriptを使った基本的な内容で作成されています。 どんな感じの挙動なのかだけ画像で載せておきますね。 トップ タスク追加 タスクリスト 6.3 評価項目の生成と実際のチェック # 6.2 で述べた内容で、WAに準拠したタスク管理アプリを動かすためのAWSアーキテクチャを作成することはできました。 ここでは、このアーキテクチャが「実際にWAの評価観点に沿っているのか」をチェックするために行った内容を整理していこうと思います。 「well-arch.md」をもとにチェックリストを作ってもらう 初期チェックリスト(クリックで展開) AWS Well-Architected フレームワーク チェックリスト 1. オペレーショナルエクセレンス(運用の優秀性) 組織 [ ] チーム全員が共通の目標を理解し、協力し合える体制を構築 [ ] 運用責任の明確化 準備 [ ] 問題発生時の対応計画を立案 [ ] 訓練を通じてチームの準備状況を向上 [ ] Infrastructure as Code の実装 [ ] 自動化による設計の試行 運用 ・・・ 「well-arch.md」に記載されていない内容が含まれていたため修正 <修正の方向性> 原文(「well-arch.md」)にない項目の追加 - 独自解釈による項目追加はしない 評価範囲の混同 - CloudFormationで評価できない組織・文化項目は含めない 「well-arch.md」ではなく、6つの柱がもつそれぞれのベストプラクティスを満たすための質問リストを直接作らせる 以下のイメージ 質問リストの例(クリックで展開) (例)コスト最適化 - 費用対効果の高いリソース - COST 5. サービスを選択するときは、どのようにコストを評価するのですか? [ ] COST05-BP01 組織のコスト要件を特定する [ ] COST05-BP02 ワークロードのすべてのコンポーネントを分析する [ ] COST05-BP03 各コンポーネントの詳細な分析を実行する [ ] COST05-BP04 コスト効率の高いライセンスを提供するソフトウェアを選択する [ ] COST05-BP05 組織の優先順位に従ってコストが最適化されるようにこのワークロードのコンポーネントを選択する [ ] COST05-BP06 異なる使用量について経時的なコスト分析を実行する 出来上がった「チェックリスト」 チェックリスト(クリックで展開) # AWS Well-Architected フレームワーク チェックリスト ## CloudFormationテンプレート技術評価項目 --- ## オペレーショナルエクセレンス ### 準備 - OPS 4. オブザーバビリティをワークロードに実装するにはどうすればよいでしょうか? - [ ] OPS04-BP01 主要業績評価指標を特定する - [ ] OPS04-BP02 アプリケーションテレメトリを実装する - [ ] OPS04-BP04 依存関係のテレメトリを実装する - [ ] OPS04-BP05 分散トレースを実装する - OPS 5. どのようにして欠点を減らし、修正を簡単にし、本番環境へのフローを改善しますか? - [ ] OPS05-BP01 バージョン管理を使用する - [ ] ・・・ 評価の様子 実際に評価した様子(クリックで展開) ## セキュリティ ### セキュリティ基盤 - SEC 1. ワークロードを安全に運用するにはどうすればよいですか。 - [○] SEC01-BP01 アカウントを使用してワークロードを分ける - VPC、サブネット分離実装 - [○] SEC01-BP06 標準的なセキュリティ統制のデプロイを自動化する - セキュリティグループ、IAMロール自動化 ### Identity and Access Management - SEC 2. 人とマシンの認証の管理はどのようにすればよいですか? - [○] SEC02-BP02 一時的な認証情報を使用する - IAMロール、インスタンスプロファイル使用 - [×] SEC02-BP03 シークレットを安全に保存して使用する - Secrets Manager未使用(パラメータで直接指定) ・・・ ## 総合評価 | 柱 | 評価 | 割合 | |---|---|---| | オペレーショナルエクセレンス | 11/12 | 92% | | セキュリティ | 12/14 | 86% | | 信頼性 | 18/19 | 95% | | パフォーマンス効率 | 10/10 | 100% | | コスト最適化 | 10/15 | 67% | | 持続可能性 | 10/10 | 100% | **総合評価: 71/80 (89%)** ## 主な改善点 ### 優先度高 1. **SEC02-BP03** - AWS Secrets Managerによる認証情報管理 2. **COST02-BP05** - AWS Budgetsによるコスト制御 3. **COST03-BP05** - Cost Explorerによるコスト監視 ### 優先度中 4. **OPS04-BP05** - AWS X-Rayによる分散トレース 5. **SEC06-BP01** - Amazon Inspectorによる脆弱性管理 6. **REL01-BP04** - Service Quotasモニタリング ### 優先度低 7. **COST07-BP02** - リージョン選択最適化 8. **COST08-BP03** - CloudFrontによるCDN実装 6.4 Kiroが行ったチェックの考察 # 6.3 で、生成したAWSアーキテクチャがWA評価観点を満たしているかをKiroにチェックさせた流れを整理しました。 やや工夫が必要になる箇所がありますが、おおむね「生成AIでWAを満たしたAWSアーキテクチャの構築」はできそうな手ごたえを感じました。 マネージメントコンソール上でのAWS WA Toolに比べれば、手動でのチェック作業が不要になるので負担はだいぶ減るのではないかと考えています。 また、今回は行いませんでしたが、チェックを実施した際に出力されている「改善点」を利用してプロンプトで指示を出せば、CloudFormationテンプレートの改善も自動で行うことは容易かと思います。 この点もマネージメントコンソール上のAWS WA Toolより優れている点かなと感じています。 ただ、チェックリスト生成のためにあらかじめ 公式ドキュメントの質問内容 を整理したり、プロンプトでの条件を考えたりする工夫は必要になるので、完全自動化まではまだまだ壁があるのかなというのが所感ですね。 ※公式ドキュメントに列挙されているベストプラクティスを満たすための質問内容の整理もある程度は生成AIに行わせています。 7. まとめ # 今回の記事では、AWS Well-Architected Frameworkを使ってどのようにアーキテクチャを評価するのか、生成AIツール「Kiro」を使ったアーキテクチャの生成とWAに基づく評価について紹介しました。 KiroがWAのベストプラクティスを理解し、それを反映したアーキテクチャを生成できるという事実は、今後のクラウド設計における生成AIの可能性を強く示唆していると思います。 ただ、今回タスク管理アプリの開発の様子は紹介しませんでしたが、エラーが全く起きずにコードが動くということはなく、WAの評価についても最初はKiroが独自の評価を織り交ぜて実施してしまった点を考えると、Kiroに全てを任せることはまだ厳しい状態であり、Kiroを使う側の開発スキルやAWSに対する理解は必須だと感じました。 そういう意味で、Kiroは設計プロセス(要件定義、設計、構築タスクの洗い出し)をサポートしてくれる存在、あるいはメンターのように気づきを与えてくれる存在に留まっており、全てを丸投げして設計から開発・評価を行わせるにはまだまだ不完全だと思いました。 不完全だと感じてはいますが、設計プロセスや開発・評価の効率を上げるには十分な性能にはなっているため、「使い方」を正しく理解したうえで活用していくことが現状の落としどころかなと思います。 生成AIとの適切な対話によって、質の高い要件定義やアーキテクチャを、よりスピーディーに作っていけるよう日々自分も対象領域のキャッチアップを怠らないようにしたいです。 今後は「WAを満たした汎用的なAWSアーキテクチャ」をより効率的に作りそれらをどう運用していくかについてさらに探求していきたいと考えています! 最後まで読んでいただきありがとうございました。
この記事は夏のリレー連載2025 3日目の記事です。 --> Information 本記事は、次のような読者層を想定しています。 パラメーター数とLLM性能の関係を直感的に理解したい方 Transformerの仕組みを概観し、学習の足がかりを得たい方 詳細な理論解説ではなく 「全体像の把握」 を目的としています。より深い学習を希望される場合は、本文中で紹介する 参考文献 をご参照ください。 1.導入 # パラメータ数への本質的な疑問 # 大規模言語モデルでは、パラメータ数がしばしば主要な指標として示されます。 たとえば2025年8月5日にOpenAIが発表 [1] した「gpt-oss-20b」と「gpt-oss-120b」も、モデル名にパラメータ規模を含めています。これは必要メモリ量の目安であると同時に、性能水準を暗示するための数字と解釈されます。 けれども、 なぜパラメータ数が多いと性能が良くなる と言えるのでしょうか? その理由を理解するには、各パラメータが実際にどのように働いているかを分解して見る必要があります。 調査手法と参考文献 # この疑問に答えるため、『 つくりながら学ぶ!LLM自作入門(マイナビ出版) 』を参考とし、実際にGPT-2 smallで用いられている124Mパラメータを分解し、各パラメータがどのような役割を果たしているのかを実測してみました。 https://book.mynavi.jp/ec/products/detail/id=146901 本書籍は、 LLMの理論をソースコードとともにステップバイステップで解説 している点が大きな特徴です。 原著者が作成したGitHubリポジトリ も参考になり、その情報量には驚かされます。 https://github.com/rasbt/LLMs-from-scratch 本記事では、これらのリソースを用いてパラメータの働きを実際に確かめました。パラメーター数を単なる数字の羅列ではなく、それぞれのパラメータがどのように協調して「理解」や「生成」を実現しているのか、2章で詳しく見ていきましょう。 2.実測 - GPT-2 small (124M)の解剖 # --> Caution 本章の説明は、著者(私)の書籍理解と実測結果をもとに整理した内容です。説明は理解しやすさを優先して一部を簡略化しています。より厳密な理論や完全な数式展開を求める方は、 参考文献 をご参照ください。内容に誤りや不足があれば、ぜひフィードバックをお寄せいただければ幸いです。 2-1. 処理ステップ全体像 # GPT-2がテキストを処理する流れを、Githubで公開されている書籍第4章の図表 [2] をもとに、簡潔に5Stepにまとめました。(残差接続やドロップアウトなど、かなり省略しています) また、フローの中に記述されている、d_model=768、H=12、L=12、d_ff=3072の4つは調整可能なパラメーターを指します。調整可能なパラメーターはそれ以外にも存在しますが、PyTorch公式のTransformerドキュメント [3] の上から順に4つを本記事では追跡することとします。この4つの調整可能なパラメーターについては、後の3-2章で後述します。 graph TD B["Step 1: トークン化"] B --> EB subgraph EB["Step 2: 埋め込み層"] direction TB C["トークン埋め込み<br/>d_model=768"] C --> D["位置埋め込み<br/>d_model=768"] end EB --> TF subgraph TF["Step 3: Transformer"] direction TB F["Attention層<br/>H=12, L=12"] F --> G["フィードフォワード層<br/>d_ff=3072, L=12"] end TF --> H["Step 4: 線形出力層"] H --> I["Step 5: 予測"] 処理の概要 # 例えば、「Hello, I am」という入力が入った場合、以下のように処理され、次に続く出力(「student」など)が確定するイメージです。 Step 1: トークン化 テキストをトークンID列に変換: "Hello, I am" → [15496, 11, 314, 716] Step 2: 埋め込み層 トークン埋め込み - 各トークンIDを768次元ベクトルに変換 位置埋め込み - トークンの位置情報を768次元で付加 Step 3: Transformerブロック(×12層) Attentionモジュール - トークン間の関係性を計算 フィードフォワードネットワーク - 情報の変換と圧縮 Step 4: 線形出力層 最終正規化 重み共有による出力投影 Step 5: 予測 Softmaxで確率計算 50,257候補から選択 2-2. ステップごとのパラメータ使用量(読み飛ばしOK!) # この章では、各ステップごとのパラメーター使用量を説明します。 正確な説明のため、 原著者のGithubリポジトリで公開しているソースコード を生成AIで解析しステップ別に整理しました。ただし、生成AIは正確な数字の計算が不得手ですので、パラメータ数については以下検証用コードを実行した結果を用います。 パラメータ数検証用コード(クリックで開く) check_param.py # GPT-2 124M parameter calculation based on gpt.py implementation # GPT-2 124M configuration from gpt.py vocab_size = 50257 context_length = 1024 emb_dim = 768 n_heads = 12 n_layers = 12 qkv_bias = False # No bias in Q,K,V projections print("=" * 60) print("GPT-2 124M Parameter Count (based on gpt.py)") print("=" * 60) # 1. Embedding layers token_emb = vocab_size * emb_dim pos_emb = context_length * emb_dim emb_total = token_emb + pos_emb print("\n1. Embedding Layers:") print(f" Token embedding: {token_emb:,}") print(f" Position embedding: {pos_emb:,}") print(f" Subtotal: {emb_total:,}") # 2. Transformer block (per layer) print("\n2. Transformer Block (per layer):") # MultiHeadAttention # From gpt.py: W_query, W_key, W_value with qkv_bias=False qkv_weights = emb_dim * emb_dim * 3 # No bias # From gpt.py: out_proj has bias by default out_proj = emb_dim * emb_dim + emb_dim # Weight + bias attention_total = qkv_weights + out_proj print(f" a) MultiHeadAttention:") print(f" Q,K,V weights (no bias): {qkv_weights:,}") print(f" Output projection (with bias): {out_proj:,}") print(f" Attention total: {attention_total:,}") # FeedForward # From gpt.py: nn.Linear(emb_dim, 4*emb_dim) with bias ffn_fc = emb_dim * (4 * emb_dim) + (4 * emb_dim) # From gpt.py: nn.Linear(4*emb_dim, emb_dim) with bias ffn_proj = (4 * emb_dim) * emb_dim + emb_dim ffn_total = ffn_fc + ffn_proj print(f" b) FeedForward:") print(f" First layer (768->3072): {ffn_fc:,}") print(f" Second layer (3072->768): {ffn_proj:,}") print(f" FFN total: {ffn_total:,}") # LayerNorm (2 per block: norm1 and norm2) # From gpt.py: scale and shift parameters ln_params = emb_dim * 2 * 2 # 2 params (scale, shift) × 2 LayerNorms print(f" c) LayerNorm x2: {ln_params:,}") # Total per block block_total = attention_total + ffn_total + ln_params print(f" Total per layer: {block_total:,}") # 3. All transformer layers transformer_total = block_total * n_layers print(f"\n3. All Transformer Layers ({n_layers} layers):") print(f" Total: {transformer_total:,}") # 4. Final layers # From gpt.py: final_norm (LayerNorm) final_ln = emb_dim * 2 # scale and shift # From gpt.py: out_head shares weights with token embedding # nn.Linear(emb_dim, vocab_size, bias=False) # Weight is shared with token_emb, so we don't count it again print(f"\n4. Final Layers:") print(f" Final LayerNorm: {final_ln:,}") print(f" Output head: Weight shared with token embedding (not counted)") # 5. Total total_params = emb_total + transformer_total + final_ln print(f"\n" + "=" * 60) print(f"TOTAL PARAMETERS: {total_params:,}") print(f"=" * 60) # Verify the calculation print(f"\nExpected (GPT-2 124M): 124,412,160") print(f"Calculated: {total_params:,}") print(f"Match: {total_params == 124412160}") # Breakdown summary print(f"\nParameter Distribution:") print(f" Embeddings: {emb_total:,} ({emb_total/total_params*100:.1f}%)") print(f" Transformer: {transformer_total:,} ({transformer_total/total_params*100:.1f}%)") print(f" Output: {final_ln:,} ({final_ln/total_params*100:.1f}%)") --> Information ソースコードベースの説明ですので、結論だけを知りたい読者は次の章に進んでください。 Step 1: トークン化 # 使用パラメータ:0個 テキストを数字に変換 BPE(Byte Pair Encoding)による辞書ベースの変換 学習済み語彙(50,257トークン)に基づく Step 2: 埋め込み層(39,383,808個) # トークン埋め込み:38,597,376個 50,257語彙 × 768次元 = 38,597,376個のパラメータ(参考:GPT-3では埋込サイズは12,288次元) 各トークンIDに対応する768次元ベクトルを埋め込み表から取得 位置埋め込み:786,432個 1,024位置 × 768次元 = 786,432個のパラメータ 各位置に対応する768次元ベクトルを埋め込み表から取得 埋め込み加算(パラメータなし) トークン埋め込み + 位置埋め込み(要素ごと) Dropout(0.1)を適用 Step 3: Transformerブロック(12層、計85,026,816個) # 各層で7,085,568個のパラメータを使用: MultiHeadAttention(2,360,064個) 12ヘッド並列処理(各ヘッド64次元): Query投影: nn.Linear(768, 768, bias=False) → 589,824個 Key投影: nn.Linear(768, 768, bias=False) → 589,824個 Value投影: nn.Linear(768, 768, bias=False) → 589,824個 出力投影: nn.Linear(768, 768) → 590,592個(bias含む) 処理の流れ: 入力ベクトル [B(Batch size), T(Sequence length), 768] を受け取る Q, K, V を線形変換で生成し、12ヘッドに分割 [B, 12, T, 64] Attentionスコアを計算:scores = (Q · Kᵀ) / √64 因果マスクを適用後、softmaxで正規化して Attentionの重み を得る [B, 12, T, T] コンテキストベクトルを計算:context = weights · V [B, 12, T, 64] 12ヘッドを結合し、出力投影で [B, T, 768] に戻す FeedForward(4,722,432個) 4倍の次元拡張と圧縮: 第1層: nn.Linear(768, 3072) → 2,362,368個(weight+bias) GELU活性化(パラメータなし) 第2層: nn.Linear(3072, 768) → 2,360,064個(weight+bias) LayerNorm(3,072個) 残差接続の前後で正規化: norm1(Attention前):1,536個(scale:768 + shift:768) norm2(FFN前):1,536個(scale:768 + shift:768) 残差(ショートカット)接続とDropout パラメータなし、勾配の流れを改善 Step 4: 出力層(1,536個) # 最終LayerNorm:1,536個 LayerNorm(768) :scale(768) + shift(768) 出力投影(重み共有) nn.Linear(768, 50257, bias=False) トークン埋め込みの転置を使用(38,597,376個を節約) Step 5: 予測(パラメータなし) # Softmax確率計算 50,257語彙から次トークンを選択 温度サンプリングやTop-k/Top-p手法を適用可能 2-3. 124Mパラメータの全体像 # 解析の結果、GPT-2 smallの124,412,160個のパラメータは、以下のように配分されていることがわかりました。 Step 1: トークン化 0個(0%) Step 2: 埋め込み層 39,383,808個(31.7%) ├─ トークン埋め込み 38,597,376個(31.0%) └─ 位置埋め込み 786,432個(0.6%) Step 3: Transformer層 85,026,816個(68.3%) ├─ Attention (12層分、12ヘッド) 28,320,768個(22.8%) ├─ FFN (12層分) 56,669,184個(45.5%) └─ LayerNorm (12層分) 36,864個(0.03%) Step 4: 出力層 1,536個(0.001%) └─ 最終LayerNorm(投影は重み共有) Step 5: 予測 0個(0%) ──────────────────────────────── 合計: 124,412,160個 ≈ 124.4M(1億2400万) ※重み共有により38,597,376個を節約 (共有しない場合は163,009,536個になる) 3.結論と考察 - パラメータ数が性能を決める理由 # 3-1. パラメータ数の面で、Attentionは全体の22.8%を占める # 前章で得られたGPT-2のパラメータ数の内訳を円グラフで示します: pie showData title GPT-2 Small (124M) パラメータ分布 "FFN層 (56.7M)" : 56669184 "埋め込み層 (39.4M)" : 39383808 "Attention層 (28.3M)" : 28320768 "その他 (38K)" : 38400 Attention層は22.8%にすぎず、むしろFFN層が45.5%を占めています。埋め込み層も3割を超えており、 大部分のパラメーターはAttention以外に割かれている ことがわかりました。 この結果は興味深いものでした。というのも、「Attention Is All You Need」という論文タイトル [4] から、著者は モデルの大部分のパラメータはAttention層に使われているはず だと考えていたからです。 3-2. パラメータ数の増加がもたらす4つの効果:解像度・多様性・特徴理解・推論の深さ # では次に、それぞれの層のパラメーターをどのように調整すればLLMの性能が伸びるのか考えていきましょう。本記事では、2-1章で定義した4つの調整可能なパラメータについて考察します。 埋め込み次元 (d_model) 各トークンを何次元のベクトルで表現するかを決めるパラメータです。次元数が大きいほど文脈の情報を細かく保持でき、単語の意味やニュアンスを精緻に捉えられます。 直感的なたとえとしては「質問数を増やすことで対象を正確に特定できる二十の質問ゲーム [5] 」に近いと感じましたが、ここでは「次元数が多いほど単語を精密に区別できる」というイメージを掴んでいただければ十分です。 → 単語の解像度 を高めるパラメータ。 Attentionヘッド数 (H) 入力を複数の視点で並列的に処理するパラメータです。ヘッド数を増やすことで文脈を多角的に捉えられるようになります。 直感的な例を挙げると、「bank」という英単語は、「銀行」と「川の土手」の両方の意味を持ちますが、そのどちらの意味を指すのかは文脈によって異なります。複数のヘッドを使うことで、それぞれの意味を同時に追跡でき、文脈に応じて最も適切な解釈を選択できるイメージです。 → 解釈の多様性 を広げるパラメータ。 FFN中間層 (d_ff) Attentionで得た情報を非線形に変換する層です。値を大きくすると、単純な平均処理では表せない複雑な特徴を再構成でき、モデルの表現力が高まります。 比喩的に言えば、画像処理の非線形フィルタが線形フィルタ(平均フィルタ)より高精度にノイズを除去できる [6] のと似ています。 ※処理原理は異なりますが、「より複雑な変換が可能になる」という点をイメージする比喩として紹介します。 → 複雑な特徴理解力 を強化するパラメータ。 層数 (L) Transformerブロックをいくつ積み重ねるかを示すパラメータです。層を深くすることで推論を段階的に繰り返せるようになり、長距離依存や複雑な関係性を捉えやすくなります。 → 推論の深さ を増すパラメータ。 結論として、 パラメータ数を増やすことで、単語の解像度・多様性・特徴理解・推論の深さが向上することで、LLMの性能向上が期待できる といえそうです。 おわりに # この記事を通じて、次のような視点を得られたのではないでしょうか。 「アテンションがすべて」ではなく、パラメーター全体のバランスが重要であること 「パラメーター数=性能向上」という単純な理解を超えた新しい視点を持てること 著者自身も、2025年8月の社内ハッカソンをきっかけに 参考文献 を学び直す中で、Transformerの仕組みを改めて整理できました。本記事が読者の皆さまにとっても、学習を進める一助となれば幸いです。 OpenAI:オープンウェイトリーズニングモデルの限界を押し広げる gpt-oss-120b と gpt-oss-20b ↩︎ Chapter 4: Implementing a GPT model from Scratch To Generate Text ↩︎ PyTorch公式ドキュメント(Transformer) ↩︎ Attention Is All You Need (Vaswani et al., 2017) ↩︎ Wikipedia 二十の質問 ↩︎ 画像フィルタ~より容易な欠陥検出のために(前編) ↩︎
はじめに # この記事は夏のリレー連載2025 2日目の記事です。 ビジネスソリューション事業部の塚野です。 ここ数か月で爆発的に普及しているClaude Codeですが、ようやく導入しましたところそのすごさに無事ぶったまげました。 Claude CodeをはじめとするAgentic AIは、指定したファイルやフォルダを「コンテキスト」に含めて管理します。 コンテキストとは、いわばAgentic AIの「認知範囲」であり、ユーザーからの入力や会話、タスクの履歴、さらに読み込ませたファイルやAPIから取得した情報などが含まれます。これにより、Agentic AIはプロジェクトに特化した回答を作成し、その内容に基づいてタスクを実行することができます。 フォルダやファイルのパスを指定すれば、それらを直接コンテキストに取り込むことも可能です。しかし、ファイル数が多かったりサイズが大きかったり、あるいは内容が膨大だったりすると、取り込み自体ができなかったり、大量のトークンを消費してすぐにサービスのリミットレートに達してしまうといった問題が生じます。加えて、情報量が過剰になると、LLMが適切な回答を生成しにくくなることもあります。 さらに、Google Driveに保存したドキュメントや、GitHub、Subversionのリポジトリで管理しているソースコードなどにアクセスしたい場面もあるでしょう。ただし、こうした外部の情報は直接コンテキストに取り込めないため、一度ローカルに保存するなどの工夫が必要です。 こうしたAgentic AIが直接アクセスできない情報へのアクセスを可能にし、検索性を大きく拡張させる方法として本記事ではOpenSearch MCPをおすすめしたいと思います。 OpenSearch公式ドキュメント より抜粋、改変。MCPは統一的プラットフォームとしてよくUSB-Cに例えられます。 MCPとはModel Context Protocol の略で、Claude CodeをはじめとするAgentic AIが外部のサービスと連携するためのプラットフォームです。MCPを利用することで、Agentic AIは外部のサービスを操作でき、より高度なタスクを実行することが可能になります。 OpenSearchは、オープンソースの分散型検索および分析エンジンであり、高速な全文検索、ログ分析、リアルタイムのデータ可視化など、多様なユースケースに対応しています。また、version 2.11.0以降ではk-NN(k-Nearest Neighbors)及び近似k-NNを用いたベクトル検索をサポートしています。 このOpenSearchですが、version 3.0.0からネイティブにMCPをサポートするようになりました。ローカルMCPサーバーが内蔵されており、設定でMCPサーバーを有効にするだけでOpenSearchインスタンスをそのままローカルMCPサーバーとして利用できます。 OpenSearch MCPを活用することで、複雑な環境構築を行わずにAgentic AIが外部データへアクセスでき、ドキュメントやソースコードをより効率的かつ柔軟に検索できるようになります。 前提条件 # 今回はOpenSearchのインスタンスをDockerコンテナとして起動し、MCPサーバーを有効にしてClaude Codeから接続するまでの手順を紹介します。 また、OpenSearchのインデックスを作成する際には、OSSの全文検索サービスである FESS を利用します。 FESSはOpenSearchを検索エンジンとして利用しており、GUIでの操作で簡単にインデックスが作成できます。 FESSを利用すればGitHubのリポジトリをはじめ様々な場所からのクロールも簡単に設定でき、FESS自体全文検索サービスとしても利用可能です。 クロール先として、今回はGitHubのリポジトリを例にし、Agentic AIとしてClaude Codeにアクセスさせるまでの手順を紹介します。 使用するAgentic AIですが、MCPの設定は共通のため、Claude Code以外のCursorやClaude Desktop等でも同様の手順で利用可能です。 今回使用するソフトウェアのバージョンは以下の通りです。 OpenSearch: 3.0.0 FESS: 15.0.0 Docker: 27.3.1 また、筆者の環境はWindowsなので、WSL2(Ubuntu 22.04)上でDockerを動かしています。 FESS + OpenSearchの起動 # まず、FESSとOpenSearchのDockerコンテナを起動します。 起動にはdocker composeを利用します。composeファイルはFESSの提供元であるcodelibsが配布しているためそちらを利用します。 以下のコマンドで compose.yaml 、 compose-opensearch3.yaml をプロジェクトディレクトリにダウンロードしてください。 $ curl -O https://raw.githubusercontent.com/codelibs/docker-fess/refs/tags/v15.0.0/compose/compose.yaml $ curl -O https://raw.githubusercontent.com/codelibs/docker-fess/refs/tags/v15.0.0/compose/compose-opensearch3.yaml compose-opensearch3.yaml を編集しMCPサーバーを有効化します。と言っても付け足すのはたった1行です。 compose-opensearch3.yaml services: search01: image: ghcr.io/codelibs/fess-opensearch:3.0.0 container_name: search01 environment: - node.name=search01 - discovery.seed_hosts=search01 - cluster.initial_cluster_manager_nodes=search01 - cluster.name=fess-search - bootstrap.memory_lock=true - node.roles=cluster_manager,data,ingest,ml + - plugins.ml_commons.mcp_server_enabled=true - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g" - "DISABLE_INSTALL_DEMO_CONFIG=true" - "DISABLE_SECURITY_PLUGIN=true" - "FESS_DICTIONARY_PATH=/usr/share/opensearch/config/dictionary" ... これだけです。 編集できたら、OpenSearchの起動に必要なパラメータを設定します。 $ sudo sysctl -w vm.max_map_count=262144 vm.max_map_count=262144 OpenSearchの起動にはこの vm.max_map_count の値を 262144 以上に設定する必要があります。 OpenSearchインスタンスの起動に失敗していたらまずは以下のコマンドでこの値を確認してください。デフォルトは 65530 になっているはずです。 $ cat /proc/sys/vm/max_map_count vm.max_map_count = 65530 以下のコマンドでFESSとOpenSearchのコンテナを起動します。 docker compose -f compose.yaml -f compose-opensearch3.yaml up -d 起動後は以下のURLにアクセスし、FESSのトップ画面が表示されることを確認します。 http://localhost:8080/ これでOpenSearch側の準備は完了です。インデックスを作成する前に、次はClaude CodeからOpenSearchのMCPサーバーに接続できることを確認しておきます。 Claude CodeのMCPサーバー設定 # Claude Codeは利用可能なMCPサーバーを設定ファイルで管理しています。設定するファイルによってスコープが変わります( MCPインストールスコープ - Anthropic )。 今回はプロジェクトスコープで設定します。この設定の場合、作成する設定ファイルは他のAgentic AIでも共通で利用可能です。 プロジェクト直下に .mcp.json を作成し、以下の内容を記述します。 { "mcpServers": { "opensearch": { "command": "uvx", "args": ["test-opensearch-mcp"], "env": { "OPENSEARCH_URL": "http://localhost:9200" } } } } 今回はOpenSearchのインスタンスをテスト・ローカル用として起動するため、 compose―opensearch3.yaml 内で "DISABLE_SECURITY_PLUGIN=true" としてセキュリティプラグインを無効化しています。 セキュリティプラグインはインデックスの暗号化やAPIにユーザー認証を求めるようにするなどの機能を提供します。 これを有効化する場合、 .mcp.json に認証情報を追加する必要があります。詳しくは こちら のドキュメントを参照してください。 また、 args には任意の文字列を入れます。 MCPサーバーの起動にはuvxを使います。uvxがインストールされていない場合は、以下のコマンドでPythonのパッケージ管理ソフトであるuvをインストールしてください。 curl -LsSf https://astral.sh/uv/install.sh | sh これでClaude CodeからOpenSearchのMCPサーバーへ接続できるようになります。 早速Claude Codeを起動し、MCPサーバーに接続できることを確認しましょう。 Claude Codeのセットアップに関してここでは触れませんが、VSCodeとの連携が便利ですのでここではVSCodeでの起動を想定します。 Claude Codeを起動すると、MCPサーバーが追加された場合以下のようなメッセージが表示されます。 Claude Code MCPサーバー初回設定時の確認ダイアログ Use this and all future MCP servers in this project を選択。設定に記述したMCPサーバーがこのプロジェクト内で利用可能になります。 プロンプトでOpenSearchへの疎通確認をお願いしてみました。 get_index_map や search_index などのコマンドが利用でき、OpenSearchのMCPサーバーに接続できていることが確認できました。 インデックスの作成 # Claude CodeからOpenSearchのMCPサーバーに接続できたら、次はインデックスを作成し実際にClaude Codeに検索させてみたいと思います。 今回は「超簡単!」と銘打っていることもあり、簡単に設定ができるGitHubリポジトリをまずは対象にクロールを行い、検索してみます。 今回クロールするGitHubのソースコードは、本記事でも使用しているオープンソースの全文検索サービスである FESS を対象としてみます。 インデックスはFESSの管理画面から作成します。FESSの管理画面には、FESSのURLに /admin を付けてアクセスします。 http://localhost:8080/admin ユーザー名は admin 、初期パスワードも admin です。 初回ログイン時はパスワードの変更が求められますので、任意のパスワードに変更してください。 GitHubからのクロールにはプラグインの導入が必要となるため、FESS管理画面からプラグインをインストールします。 管理画面にログイン後、サイドバーの「システム」>「プラグイン」>「インストール」からプラグインインストール画面に移り、 リモートタブでプラグイン「fess-ds-git-xx.xx」を選択します。「インストール」をクリックするとプラグインがFESSへインストールされます。 続いて、サイドバーの「クローラー」>「データストア」からクローラーの設定画面に移り、「新規追加」ボタンをクリックします。 設定画面で以下のように入力します。 名前: 任意(今回は「fess-github」としました) ハンドラー名: GitDataStore パラメーター: uri=https://github.com/codelibs/fess.git base_url=https://github.com/codelibs/fess/blob/master/ extractors=text/.*:textExtractor,application/xml:textExtractor,application/javascript:textExtractor,application/json:textExtractor,application/x-sh:textExtractor,application/x-bat:textExtractor,audio/.*:filenameExtractor,chemical/.*:filenameExtractor,image/.*:filenameExtractor,model/.*:filenameExtractor,video/.*:filenameExtractor, delete_old_docs=false スクリプト: url=url host="github.com" site="github.com/codelibs/fess/" + path title=name content=content cache="" digest=content != null && contentLength > 200 ? content.substring(0, 200) + "..." : content; anchor= content_length=contentLength last_modified=timestamp timestamp=timestamp filename=name mimetype=mimetype domain="github.com" organization="codelibs" repository="fess" path=path repository_url="https://github.com/codelibs/fess" filetype=container.getComponent("fileTypeHelper").get(mimetype) owner="" homepage="" クロール先のリポジトリによって変わるのはリポジトリのドメイン( github.com )、組織名( codelibs )、リポジトリ名( fess )です。上記のパラメータ、スクリプトのうち、これらの値を変更してください。 privateリポジトリをクロールする場合は、パラメータに以下のように認証情報を含める必要があります。 username=hogehoge password=ghp_xxxxxxxxxxx commit_id=main 現在GitHubではパスワード認証を廃止しています。代わりにpersonal access token(PAT)をGitHubで発行し、password欄に入れてください。 また、メインとなるブランチ名を master から変更している場合、そのままだとHEADコミットが取れないので commit_id でメインとなるブランチを指定してください。 どのようなパラメータ、スクリプトが利用できるのか知りたい場合はFESS Git Data Storeのリポジトリ( Git Data Store )を参照してください。 後の欄は初期のままで大丈夫です。 「作成」ボタンをクリック。設定を保存します。 最後に、クローラージョブを実行してクロールを開始します。 サイドバーの「システム」>「スケジューラ」からジョブスケジューラの画面に移り、「Default Crawler」を選択。 「今すぐ実行」をクリックでクロールが開始されます。 Default CrawlerはFESSで設定されている全ての [1] クローラージョブを実行します。 クロールジョブの実行中はスケジューラの「状態」が「実行中」になります。この状態が「有効」に戻るまでしばらく待ちましょう [2] 。 Claude Codeでの検索 # クロールが完了したら、Claude Codeで検索してみます。 Claude Codeのプロンプトに以下のように入力し、試しにFESSのソースコードを検索してもらいました。 検索クエリに「regexp:パターン」を含めることで正規表現で検索ができるようになるregexpプレフィックスを実装したいです。\ 接続したMCPサーバーからFESSのソースコードを検索し、どのクラスを拡張すればいいか調べてください するとこのような結果が1~2分ほどで返ってきました。 FESSでregexp:プレフィックスを実装するには、以下のクラスを拡張する必要があります: 主要な拡張ポイント 1. RegexpQueryCommand (新規作成) src/main/java/org/codelibs/fess/query/RegexpQueryCommand.java - QueryCommandを継承して正規表現クエリを処理 2. QueryStringBuilder (拡張) src/main/java/org/codelibs/fess/util/QueryStringBuilder.java:177-193 - appendConditionsメソッドにregexp:処理を追加 - 既存のsite:、filetype:と同様の実装パターン 3. RegexpQueryFilter (新規作成) - QueryParser.Filterを実装 - クエリ文字列からregexp:プレフィックスを検出・変換 4. 設定ファイル - /src/main/resources/app.xml - コンポーネント登録 - /src/main/resources/fess_config.properties - 設定追加 既存アーキテクチャ FESSは既に以下のプレフィックスをサポートしており、同じパターンで実装可能: - site: - サイト検索 - filetype: - ファイルタイプフィルタ - sort: - ソート指定 - timestamp: - タイムスタンプフィルタ QueryProcessor(src/main/java/org/codelibs/fess/query/QueryProcessor.java:67-70)でコマンド登録 し、QueryParser(src/main/java/org/codelibs/fess/query/parser/QueryParser.java:78-85)でフィルタ チェーン処理します。 …すごい。ちゃんと既存の実装を見て変更点について提案してくれています。 検索フィールドを増やすわけではないのでfess_config.propertiesの編集は必要なさそうですが、編集するjavaファイルなどは正解っぽい感じがします。 さらにどのように調査したのか、どのようなクエリで検索したのかと聞いたところ、以下のような回答が返ってきました。(一部改変) 1. MCPサーバー接続確認 - 利用可能なインデックス一覧を取得 - fess*パターンでFESS関連インデックスを特定 2. データ存在確認 - FESS関連インデックスにデータが存在することを確認 3. ソースコード検索(**Taskツール使用**) **専用エージェントに以下の検索タスクを依頼**: 検索対象: - Javaソースコードファイル - クエリ処理・解析関連クラス - 既存プレフィックス処理メカニズム - メインクエリ処理パイプラインクラス エージェントが実行したと推測される検索クエリ: - QueryParser - クエリ解析の中核 - QueryProcessor - クエリ処理ルーティング - QueryStringBuilder - 既存プレフィックス処理 - site: filetype: - 既存実装パターン - QueryCommand - コマンドパターン実装 - Filter - フィルタチェーン実装 4. アーキテクチャ分析 具体的な検索キーワードを出さずとも、Claude Codeの方でクエリを考えて検索を行ってくれたようです。 また、検索処理には Taskツール を使っていました。 これはClaude Codeの機能の一つで、ユーザーからのプロンプトを受けたメインのエージェントとは別に専用エージェントを起動して、複雑なタスクを自律的かつ並列的に実行させることができます。( What is the Task Tool in Claude Code - ClaudeLog ) これによって得られた情報を、メインエージェントが統合し、回答を生成します。 今回作成された専用エージェントは目的の回答にたどり着くまで探索的に検索を何度も繰り返していました。 また、さらに詳しく調べたところ、キーワード検索だけでなく必要があればファイルの中身も直接参照して回答を生成しているようでした。これは検索結果にリポジトリ内の実際のファイルパスも含まれるためです。 以上の検証から、ファイルの検索とファイル内容の詳細解析というClaude Codeがローカルファイルに対して普段行う操作を、OpenSearch MCPを使ってGitHubリポジトリ上のファイルに対しても簡単かつ効率的に実行できるということが分かりました。 まとめ # 今回はClaude Codeに全文検索エンジンを接続して検索性を拡張する方法をご紹介しました。 OpenSearchは冒頭で述べたようにベクトルデータベース化もできるため、Claude Codeに意味検索もしくは全文検索とのハイブリッド検索も行わせることができます。 ドキュメント類は意味検索、ソースコードはキーワード検索で厳密な一致検索 [3] を行うという使い分けもいいかもしれません。 近年はベクトルデータベースの導入コストが下がり、Embedding精度も向上してきていますが、それでも「超簡単!」に導入というレベルにはまだ達していないと感じています。 一方で、Agentic AIがクエリを考え、探索的に検索を繰り返してくれるのであれば、RAGを導入しなくても全文検索だけで十分なケースも少なくありません。 さらに、意味検索(RAG)では「どのようにその回答が導かれたのか」がブラックボックス化しがちですが、全文検索であれば検索結果の根拠を直接追跡できるという利点もあります。 Claude Codeに全文検索させるメリットとしてもう1つ、トークンの節約があります。 トークンはユーザーが入力したプロンプトや、エージェントが読み込んだファイルなど「LLMへ送信された情報量」によって消費量が決まります。 Claude Codeはファイルの探査にgrep検索を行うのですが、例えばマッチしたファイルが.logのようなminifiedファイルの場合、1行の情報量が膨大でファイルを読むだけで大量のトークンを消費してしまうということもありえます。 一方、Open Search MCPからのレスポンスは構造化されたjson形式かつインデックス化された情報なので、検索結果によって大きくトークンを消費するといったこともありません。 本記事では実験的にGitHubリポジトリをクロール対象としましたが、FESSでは他にもプラグインの導入でGoogle DriveやMicrosoft Share Pointなどもクロール対象にできます。これにより、「Google Driveで設計資料を検索して、それを元にGitHubのソースを検索して」といったタスクも依頼可能です。 Google Driveのクロール設定方法はこちらの記事を参考にしてください。 https://news.mynavi.jp/techplus/article/techp4732/ また、FESSはプラグインを自作することによりクロール対象や検索機能の拡張も可能です。 公式では配布していないSubversionをクロール対象とするプラグインを自作したりなどしているので、機会があればちょっとニッチですがプラグイン作成についてや他のデータソースのクロール方法など記事にしたいと思います。 一度に実行可能なクローラー設定数はデータストア、ウェブ、ファイルストアクローラーで各100個までがデフォルトの上限で設定されています。この上限を変更する場合は fess01 コンテナ内 /etc/fess/fess_config.properties の page.data.config.max.fetch.size 、 page.web.config.max.fetch.size 、 page.file.config.max.fetch.size をそれぞれ変更してください。 ↩︎ クロールに失敗した場合は、サイドバー「システム情報」タブの「障害URL」にクロールが失敗したURLとスタックトレースが表示されます。「システム情報」の「ログファイル」からログファイルの参照も行えるのでこれらを使って原因を調査してください。 ↩︎ FESSは大文字小文字を区別せず、デフォルトで4文字以上の単語に対してあいまい検索が有効になっているため厳密な検索ではないですが…。FESSコンテナ内 fess.json からanalyzerでlowercase filterを使用しないようにすれば大文字小文字の区別は可能になります。また、あいまい検索も fess_config.json 内で query.boost.fuzzy.min.length=-1 を指定することでOFFにできます。 ↩︎