AIエージェントのスキルとは何か?仕組みをわかりやすく解説
AI エージェントに「スキル」を追加すると、まるでアプリに機能拡張をインストールするように、できることを増やせます。この記事では、Agent Skills の仕組みと、エージェントが内部でどのような処理をしているかを解説します。
AI エージェントに「スキル」を追加すると、まるでアプリに機能拡張をインストールするように、できることを増やせます。この記事では、Agent Skills の仕組みと、エージェントが内部でどのような処理をしているかを解説します。
このブログにコメント機能を追加したくなり、AWS サーバーレスで API を自作しました。その設計と実装について紹介します。
リクエストは以下の流れで処理されます。
ブラウザ (www.hikari-dev.com)
↓ HTTPS
API Gateway
├── GET /comment?postId=... → コメント取得
├── POST /comment → コメント投稿
└── PATCH /comment/{id} → 管理(非表示切り替え)
↓
Lambda (Node.js 20 / arm64)
↓
DynamoDB(コメント保存)
+ SES v2(管理者へのメール通知)
コードは TypeScript で書き、SAM(Serverless Application Model)で IaC 管理しています。Lambda は arm64(Graviton2)にして少しコストを抑えています。
テーブル名は blog-comments、パーティションキーは postId、ソートキーは commentId です。
| キー | 型 | 説明 |
|---|---|---|
postId | String | 記事の識別子(例: /blog/2026/03/20/hime) |
commentId | String | ULID(時系列ソート可能な ID) |
ソートキーに ULID を使っているので、QueryCommand で取得したコメントは投稿順に自動的に並びます。UUID にしなかったのはこの理由です。
コメントを DynamoDB に書き込む前に、keywords.json に定義したキーワードと照合します。
キーワードにヒットした場合は isHidden: true で自動非表示にし、isFlagged: "1" を付与します。ヒットしなければ即時公開です。
isFlagged は Sparse GSI のキーとして使っています。ヒットしないコメントにはこの属性を持たせないため、GSI に余計なパーティションが増えず、コストと効率の両面で有利です。DynamoDB Document Client の removeUndefinedValues: true を設定するだけで実現できます。
コメントが投稿されるたびに SES v2 で自分宛にメールが届きます。本文には投稿者名、本文、評価、IP アドレス、フラグ状態が含まれます。
メール送信は非同期で行い、失敗しても握りつぶします。コメント投稿のレスポンス速度に影響しないようにするためです。
DynamoDB には IP アドレスや User-Agent も保存しますが、GET エンドポイントのレスポンスには含めません。型定義レベルで分離しています。
| 層 | 対策 |
|---|---|
| ネットワーク | AWS WAF で 100 req / 5 分 / IP のレート制限 |
| CORS | https://www.hikari-dev.com のみ許可 |
| 管理 API | API Gateway の API キー認証(X-Api-Key ヘッダー) |
| スパム | キーワードフィルタで自動非表示 |
管理エンドポイント(PATCH /comment/{id})は SAM テンプレートで ApiKeyRequired: true を設定するだけで API キー認証が有効になります。Lambda Authorizer を自前で実装する必要がなく、シンプルです。
サーバーレス構成なのでサーバー管理も不要で、DynamoDB のオンデマンド課金により低トラフィックな個人ブログでもコストを最小限に抑えられています。
コードは SAM + TypeScript + esbuild でまとめており、sam build && sam deploy だけでデプロイできます。
複数の AI プロバイダーとチャットできる VSCode 拡張機能 Hime (HikariMessage) を作りました。
BYOK であり、各 API プロバイダーの API キーがあれば利用可能です。
Hime は、VSCode のサイドバーに組み込む生成 AI チャット拡張機能です。Anthropic、OpenAI、Azure OpenAI、OpenRouter、Ollama に対応しており、ドロップダウンメニューで簡単にプロバイダーを切り替えられます。
以下のプロバイダーをサポートしています。
リアルタイムで応答が表示されるため、長い回答でも待ち時間を感じにくくなっています。
設定で以下のように JSON 形式で設定を行うことで MCP が利用可能です。
例
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "C:\\Users"]
}
}
会話履歴は ~/.hime/chats/ に JSON 形式で保存されます。VSCode を再起動しても続きから会話できます。
ワークスペースの情報、OS の情報、現在開いているエディタのコンテキストが自動的にシステムプロンプトに含まれます。「このファイルを直して」と言うだけで、どのファイルを見ているかを AI が把握してくれます。
Node.js 20 以上と VSCode 1.96 以上が必要です。
git clone https://github.com/Himeyama/hime
cd hime
npm install
npm run watch # 開発時: Extension Host と Webview を同時監視
その後、VSCode で F5 を押すと拡張機能ホストが起動します。API キーはサイドバーの設定パネルから入力でき、VSCode の SecretStorage で暗号化されて保存されます。
エディタを離れずに AI と対話でき、しかも MCP でツール実行まで任せられるのが Hime の強みです。ぜひ試してみてください。
自分専用のファイル共有システムが欲しいと思い、AWS のサーバーレスサービスだけでファイルストレージサービスを作りました。
この記事では、設計で意識したポイントと、実際のアーキテクチャを紹介します。
制作した Web システムは、Web ブラウザからファイルのアップロード・ダウンロード・フォルダ管理ができるクラウドストレージサービスです。
以下、構成図です。

認証の大部分は Cognito で行い、 ファイル転送は S3 の Presigned URL を Lambda で発行してクライアントと S3 が直接やり取りする仕組みです。
| レイヤー | 技術 |
|---|---|
| バックエンド | C# (.NET 8) / AWS Lambda |
| 認証 | Amazon Cognito + Managed Login v2 |
| API | API Gateway (REST) + Cognito Authorizer |
| ストレージ | Amazon S3 |
Cognito の OAuth 2.0 エンドポイントと Managed Login を活用し、認証機能を実現しました。
最終的に認証系の Lambda は TokenFunction 1 つだけになりました。
機能的にもセキュリティ的にも減らせるコードは減らすのが吉です。
AWS のサービスがやってくれることを自前で書く必要はありません。
ファイルのアップロード・ダウンロードで Lambda を経由すると、いくつかの問題が生じます:
Presigned URL なら、Lambda は URL を発行するだけで、実際のファイル転送はブラウザと S3 が直接行います。
Lambda の実行時間は数十ミリ秒で済み、ファイルサイズの制約も S3 の上限までとなります。
アップロードの流れ:
1. ブラウザ → Lambda: 「file.pdf をアップロードしたい!アップロード先の URL を送れ~」
2. Lambda → ブラウザ: 「アップロード先の Presigned URL だよ。ここに PUT してね~」
3. ブラウザ → S3: 「S3 に PUT するよ~」
4. ブラウザ → Lambda: 「アップロード完了したよ~」
S3 にはフォルダごとダウンロードする機能がありません。
複数ファイルの一括ダウンロードは、Lambda 上で ZIP を生成して一時的に S3 に置き、その Presigned URL を返す方式にしました。
一時 ZIP ファイルは S3 のライフサイクルルールで 1 日後に自動削除されるので、ゴミが溜まることはありません。
| 対策 | 実装 |
|---|---|
| ブルートフォース防止 | Cognito 標準のロック機能 (5 回失敗: 15 分ロック) |
| API 保護 | Cognito Authorizer による JWT 検証 |
| CORS | AllowedOrigin を特定のドメインに限定 |
| 一時ファイル管理 | S3 ライフサイクルで不要なファイルを 1 日で自動削除 |
サーバーレス構成なので、利用がなければコストはほぼゼロです。
個人利用なら月額数十円〜数百円程度に収まります。
インフラ全体を 1 つの template.yaml (AWS SAM) で定義しています。
Cognito User Pool、API Gateway、Lambda 3 関数、S3 バケット、CloudWatch アラーム、SNS — すべてのリソースを 600 行程度の YAML で定義しています。
https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-Container-Base.latest.x86_64.tar.xz
ここからダウンロードする。
参考: https://docs.rockylinux.org/8/guides/interoperability/import_rocky_to_wsl/
tar.xz を展開し、tar 形式にする。 tar にすることで、WSL2 へのインポートが可能となる。
cd ~/Downloads
xz -d Rocky-8-Container-Base.latest.x86_64.tar.xz
※Windows にインストール済みの bsdtar では展開不可。 ※xz コマンドがない場合は、cygwin64 でインストールするか、別の WSL ディストリビューションを用いて展開する。
WSL2 にイメージをインポートする。
wsl --import RockyLinux-8.10 $HOME .\Rocky-8-Container-Base.latest.x86_64.tar --version 2
wsl -l -v
ユーザー名: hikari の場合
wsl -d RockyLinux-8.10 -u root -- dnf install sudo passwd -ywsl -d RockyLinux-8.10 -u root -- adduser hikariwsl -d RockyLinux-8.10 -u root -- passwd -d hikariwsl -d RockyLinux-8.10 -u root -- usermod -aG wheel hikariwsl -d RockyLinux-8.10 -u root -- sed -i 's/^# %wheel/%wheel/' /etc/sudoerswsl -d RockyLinux-8.10 -u root -- echo -e "[user]\\ndefault=hikari" `| tee -a /etc/wsl.conf
wsl -d RockyLinux-8.10
結論から。
合計 34 GiB の大量のファイルを圧縮・展開したときの時間とサイズを調べた。
| 方式 | コマンド | real | user | sys | 圧縮後のサイズ |
|---|---|---|---|---|---|
| tar | tar cf large-pkg.tar large-pkg | 1m19.449s | 0m6.702s | 0m48.121s | 26 GiB |
| tar.gz | tar czf large-pkg.tar.gz large-pkg | 11m33.942s | 11m18.811s | 0m55.573s | 5.5 GiB |
| LZ4 | tar cf - large-pkg | lz4 > large-pkg.tar.lz4 | 3m33.187s | 0m53.958s | 2m57.122s | 8.6 GiB |
| zstd | tar cf - large-pkg | zstd -T0 -o large-pkg.tar.zst | 9m13.819s | 1m45.049s | 2m43.609s | 4.8 GiB |
| bzip2 | tar cf - large-pkg.1 | bzip2 > large-pkg.tar.bz2 | 37m22.743s | 28m40.329s | 3m31.000s | 4.3 GiB |
| xz | tar cf - large-pkg | xz > large-pkg.tar.xz | 125m31.447s | 124m10.523s | 5m18.330s | 3.3 GiB |
| 方式 | コマンド | real | user | sys |
|---|---|---|---|---|
| tar | tar xf large-pkg.tar | 2m11.793s | 0m6.906s | 2m4.183s |
| tar.gz | tar xf large-pkg.tar.gz | 3m39.544s | 2m1.189s | 2m58.317s |
| tar.gz(gzip) | gzip -dc large-pkg.tar.gz | tar xf - | 3m40.416s | 2m0.272s | 3m0.043s |
| tar.gz(pigz) | pigz -dc large-pkg.tar.gz | tar xf - | 3m53.711s | 1m38.147s | 4m42.893s |
| LZ4 | lz4 -dc large-pkg.tar.lz4 | tar xf - | 4m46.576s | 0m32.174s | 4m36.055s |
| zstd | zstd -dc large-pkg.tar.zst | tar xf - | 3m46.419s | 0m46.533s | 3m34.668s |
| bzip2 | bzip2 -dc large-pkg.tar.bz2 | tar xf - | 11m31.287s | 9m52.644s | 4m17.974s |
| xz | xz -dc large-pkg.tar.xz | tar xf - | 8m11.527s | 3m45.562s | 7m15.109s |
まず、小さく大量のファイルとして、node_modules を用いることとした。
package.json を以下のようにした。パッケージは適当なもの。
{
"name": "large-pkg",
"version": "1.0.0",
"description": "",
"author": "",
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"async": "^3.2.6",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"bluebird": "^3.7.2",
"body-parser": "^2.2.2",
"chalk": "^5.6.2",
"chalk-template": "^1.1.2",
"cheerio": "^1.1.2",
"chokidar": "^5.0.0",
"commander": "^14.0.2",
"cookie": "^1.1.1",
"core-js": "^3.47.0",
"cors": "^2.8.5",
"debug": "^4.4.3",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"fast-glob": "^3.3.3",
"form-data": "^4.0.5",
"glob": "^13.0.0",
"got": "^14.6.6",
"inquirer": "^13.2.0",
"jsonwebtoken": "^9.0.3",
"lodash": "^4.17.21",
"mime": "^4.1.0",
"minimist": "^1.2.8",
"mkdirp": "^3.0.1",
"mkdirp-classic": "^0.5.3",
"mongoose": "^9.1.4",
"ms": "^2.1.3",
"node-fetch": "^3.3.2",
"ora": "^9.0.0",
"passport": "^0.7.0",
"prop-types": "^15.8.1",
"qs": "^6.14.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"request": "^2.88.2",
"rimraf": "^6.1.2",
"semver": "^7.7.3",
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"supports-color": "^10.2.2",
"tslib": "^2.8.1",
"uuid": "^13.0.0",
"ws": "^8.19.0",
"xml2js": "^0.6.2",
"yargs": "^18.0.0"
},
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-env": "^7.28.6",
"@babel/runtime": "^7.28.6",
"autoprefixer": "^10.4.23",
"ava": "^6.4.1",
"babel-core": "^6.26.3",
"babel-loader": "^10.0.0",
"chai": "^6.2.2",
"commitlint": "^20.3.1",
"concurrently": "^9.2.1",
"conventional-changelog": "^7.1.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"dotenv-expand": "^12.0.3",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"husky": "^9.1.7",
"jest": "^30.2.0",
"less": "^4.5.1",
"lint-staged": "^16.2.7",
"mocha": "^11.7.5",
"nodemon": "^3.1.11",
"playwright": "^1.57.0",
"pm2": "^6.0.14",
"postcss": "^8.5.6",
"prettier": "^3.8.0",
"puppeteer": "^24.35.0",
"rollup": "^4.55.1",
"rxjs": "^7.8.2",
"sass": "^1.97.2",
"semantic-release": "^25.0.2",
"sinon": "^21.0.1",
"style-loader": "^4.0.0",
"stylus": "^0.64.0",
"supertest": "^7.2.2",
"tailwindcss": "^4.1.18",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.17",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.3",
"zx": "^8.8.5"
}
}
ここから、以下のコマンドで node_modules を作成する。
npm i
容量を見てみよう。
$ du -h -d1
566M ./node_modules
567M .
これで、小さく大量のファイルが作成されたことがわかる。
node_modules をコピーしてさらに多くのファイルを作成する。
for i in {1..59}; do
echo "node_modules.${i} をコピー中..."
cp -r node_modules "node_modules.${i}"
done
容量を見てみよう。
$ du -h -d1
...
34G .
そこそこ大きい容量となった。
tarball にする速度を見てみる。
$ time tar cf large-pkg.tar large-pkg
real 1m19.449s
user 0m6.702s
sys 0m48.121s
アーカイブ後、26 GiB
$ time tar xf large-pkg.tar
real 2m11.793s
user 0m6.906s
sys 2m4.183s
アーカイブと展開はそこそこ速い。
次に、tar.gz で試す。
tar コマンドはアーカイブするコマンドで、gzip は単一のファイルを圧縮するコマンドだ。これを組み合わせて作成されたのが tar.gz ファイルである。 現在では tar コマンドのみで tar.gz ファイルの圧縮と展開が可能だ。(ほかの圧縮形式も同様)
$ time tar czf large-pkg.tar.gz large-pkg
real 11m33.942s
user 11m18.811s
sys 0m55.573s
圧縮後: 5.5 GiB
$ time tar xf large-pkg.tar.gz
real 3m39.544s
user 2m1.189s
sys 2m58.317s
$ time sh -c 'gzip -dc large-pkg.tar.gz | tar xf -'
real 3m40.416s
user 2m0.272s
sys 3m0.043s
速度はほぼ同じ。
$ time sh -c 'pigz -dc large-pkg.tar.gz | tar xf -'
real 3m53.711s
user 1m38.147s
sys 4m42.893s
あまり速度は変わんない。ファイルの読み書き速度がボトルネックとなっているようだ。
$ time sh -c 'tar cf - large-pkg.1 | bzip2 > large-pkg.tar.bz2'
real 37m22.743s
user 28m40.329s
sys 3m31.000s
圧縮後: 4.3 GiB
圧縮に時間がかかっているが、圧縮率はそこそこ良い。
$ time sh -c 'bzip2 -dc large-pkg.tar.bz2 | tar xf -'
real 11m31.287s
user 9m52.644s
sys 4m17.974s
展開も時間はかかるが、展開率はそこそこ良い。
$ time sh -c 'tar cf - large-pkg | xz > large-pkg.tar.xz'
real 125m31.447s
user 124m10.523s
sys 5m18.330s
圧縮後: 3.3 GiB
圧縮率は極めて優秀だが、時間がかかりすぎる。
ネット回線が非常に遅く、ストレージ料金が高く、数年に1度使うようなレアケースではいいかも。
$ time sh -c 'xz -dc large-pkg.tar.xz | tar xf -'
real 8m11.527s
user 3m45.562s
sys 7m15.109s
$ time sh -c 'tar cf - large-pkg | lz4 > large-pkg.tar.lz4'
real 3m33.187s
user 0m53.958s
sys 2m57.122s
$ time sh -c 'lz4 -dc large-pkg.tar.lz4 | tar xf -'
real 4m46.576s
user 0m32.174s
sys 4m36.055s
Meta (旧 Facebook) が開発した高速の圧縮方式。
$ time sh -c 'tar cf - large-pkg | zstd -T0 -o large-pkg.tar.zst'
/*stdin*\ : 18.57% (27492075520 => 5106239218 bytes, large-pkg.tar.zst)
real 9m13.819s
user 1m45.049s
sys 2m43.609s
$ time sh -c 'zstd -dc large-pkg.tar.zst | tar xf -'
large-pkg.tar.zst : 27492075520 bytes
real 3m46.419s
user 0m46.533s
sys 3m34.668s
Libertouch ES 日本語配列 (NC07902-B281-ES) を入手しました。


忖度なし、約 1 カ月使っての感想です。
Libertouch ES は 打鍵感と堅牢性に優れた高品質メンブレンキーボード。一方で、入力認識の不安定さやキーキャップの外れやすさ、付属ケーブルの仕様など、実用面で改善を望む点がある。交換用キーキャップやケーブルの選択肢が増えるとさらに魅力的に。フルキーボードや 80% キーボードの選択肢が欲しい。65% よりは需要があるのでは?
今後の改善と発売に期待です。
以下の環境で設定を確認しました。
raspberrypi.pem: サーバー証明書 (公開鍵)
この証明書は raspberrypi というホスト名に対して発行された SSL 証明書です。Web ブラウザなどのクライアントがこの証明書を使ってサーバーの正当性を確認します。(サーバーにインストールするやつ)raspberrypi+1.pem: サーバー証明書 (公開鍵)raspberrypi <IP アドレス> というホスト名に対して発行された SSL 証明書です。同上raspberrypi-key.pem:raspberrypi.pem に対応する秘密鍵です。サーバー側で保持し、SSL通信の暗号化・復号に使用されます。絶対に外部に漏らしてはいけません。 (サーバーにインストールするやつ)raspberrypi+1-key.pem:raspberrypi+1.pem に対応する秘密鍵です。サーバー側で保持し、SSL通信の暗号化・復号に使用されます。絶対に外部に漏らしてはいけません。 同上rootCA.pem: ローカルルート証明書 (公開鍵)mkcert が自動生成したローカルCA (認証局) の証明書です。クライアント (ブラウザなど) にこの証明書をインストールすることで、raspberrypi.pem を信頼できる証明書として扱えるようになります。rootCA-key.pem: ローカル CA の秘密鍵
rootCA.pem に対応する秘密鍵で、mkcert がサーバー証明書 (例: raspberrypi.pem) を署名するために使います。mkcert が内部的に使用するもので、通常は触る必要はありません。mkcert コマンドを用いて証明書を発行します。発行後、サーバーに配置します。
mkcert raspberrypi <IP アドレス>
sudo cp raspberrypi+1-key.pem /etc/cockpit/ws-certs.d/raspberrypi.key
sudo cp raspberrypi+1.pem /etc/cockpit/ws-certs.d/raspberrypi.crt
sudo systemctl restart cockpit
PC にルート証明書をインストールするためのルート CA 証明書の場所を確認します。
mkcert -CAROOT
ルート証明書を PC にコピーします。
scp raspberrypi:/home/<USER>/.local/share/mkcert/rootCA.pem .
cp rootCA.pem rootCA.cer
rootCA.cer を開き、証明書ストアで「信頼されたルート機関」で証明書を登録します。
rootCA.pem を端末に移動し設定から登録します。
japan-xxx.osm.bpf をホームディレクトリにダウンロードしておく。
その後、以下のコマンドを実行し準備。
volume オプション末尾の :Z は SELinux が稼働している場合に付ける。
/data/region.osm.pbf の部分は変更しないこと。
podman volume create osm-data
podman run -v <ダウンロードした osm.bpf ファイル>:/data/region.osm.pbf:Z -v osm-data:/data/database/ overv/openstreetmap-tile-server import
podman volume create osm-data
podman run -v <ダウンロードした osm.bpf ファイル>:/data/region.osm.pbf:Z -v osm-data:/data/database/ overv/openstreetmap-tile-server import
以下のコマンドでタイルサーバーを実行する。
podman run -p 8080:80 -v osm-data:/data/database/ -v osm-tiles:/data/tiles/ -d overv/openstreetmap-tile-server run
ファイアーウォールの設定を行えば、ネットワーク上からアクセス可能。
podman volume export osm-data > osm-data.tar
# nginx のインストールと有効化
sudo dnf install nginx
# /etc/nginx/nginx.conf の編集
# sudo nano /etc/nginx/nginx.conf
# nginx の開始と有効化
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx
http { server {} } 内に以下を追加
location / {
return 200 'Hello, world!';
add_header Content-Type text/plain;
}



# cloudflared.repo を /etc/yum.repos.d/ に追加
curl -fsSl https://pkg.cloudflare.com/cloudflared-ascii.repo | sudo tee /etc/yum.repos.d/cloudflared.repo
sudo dnf clean packages
# cloudflared のインストール
sudo dnf install -y cloudflared --nogpgcheck
sudo cloudflared service install xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ホスト名のサブドメインとドメインと、サービスのタイプ、URL を設定。

「セットアップを完了する」を押す。