👀概要
パーソルホールディングス エンジニアリング部 Product Engineering室のR.H.です。
私が以前部内向けに開催したAIエージェント勉強会の内容について、みなさまにご紹介します。
AIエージェントが普及するなか、スクラッチで簡易なAIエージェントを開発することで得た知識・視点をまとめます。
👨👩👧👦対象者(Who)
- AIエージェントとLLMの差を理解したい人
- AIエージェントの危険性を理解したい人
- AIエージェントの実装イメージをつかみたい人
📌留意事項
本記事の図やコードは勉強会用に簡略化している部分がありますのでご留意ください。
📝 内容
1. はじめに
AIエージェントとLLMの違い
LLMの台頭からAIは急速な変化を行い、現在はAIエージェントが普及しています。
まずはじめに、LLMとAIエージェントの違いについて整理します。
- LLM: ユーザーからの質問・指示に対して文章の回答を出力する。
- AIエージェント: ユーザーからの質問・指示に対して自律的にタスクを実施する。判断・実行・観察のサイクルを自分で回し、最終的な成果物を出力する。
つまり、AIエージェントのポイントは「自律的」であることです。AIエージェントを開発しようとすると、タスク実行のための処理と成果物の判断までを自律的に行えるように環境を整える必要があります。
AIエージェントでよく見るヒヤリハットや事故
AIエージェントは自律的に行動を行います。例えばターミナル操作なども可能です。
この自律的な行動ゆえにAIエージェント絡みの事故をよく目にするようになりました。
例えば
- AIエージェント「Mainにプッシュしてしまいました」
- AIエージェント「機密情報をコードに含めてしまいました」
- AIエージェント「複雑に考えすぎてトークン制限かかってしまいました」
これらは企業やプロダクトにおいて致命的な事故となる場合が多く、AIエージェントに対してある程度の恐怖心がある方も多いのではないでしょうか。
AIエージェントは危険なのか??
では、AIエージェントは危険なのか?という疑問が発生するかと思います。私は「システム側で担保すれば限りなく安全にできる」と考えています。ここで重要なのは「システム側」で安全性を担保するという点です。
よくある失敗例
よくある失敗例としてシステムプロンプトに「rmコマンドは禁止です」と指示を書き、満足しているパターンがあります。プロンプトはAIエージェントへの口頭指示でしかなく、それを100%守る保証はありません。人で例えると「〇〇しないで」と部下に指示することに値します。部下は違反をしないように最大限注意しますが、どうしてもミスをして禁止事項を違反してしまうこともあるでしょう。
このようにプロンプトで指示をするということは、完璧に違反を防げるものではないため注意が必要です。(プロンプトでの指示が有用でないというわけではないです)
どう安全性を担保するか
では安全性を限りなく100%に近づけるにはどうすればいいのか?という疑問が発生します。
それはシステム側で安全性を担保するようにすれば良いです。rmコマンドの例を挙げると、そもそもAIエージェントがrmコマンドを利用できないように、システム側でブロックをしておけば、事故は起きません。人で例えると、押すなと言われているボタンそのものを削除するような行為にあたります。
このようにプロンプトで安全性を担保するのではなく、システム側で安全性を担保すべきであることを留意いただきたいです。ただし、システムにバグがある場合は致命的なので、利用者・開発者視点ともに最大限注意が必要です。
記事のゴール
この記事の目標は以下です
- AIエージェントの構成要素を理解する
- 安全なAIエージェントの設計方法を理解する
- AIエージェントの実装の感覚を掴む
2. AIエージェントの基礎知識
AIエージェントを構成する要素
AIエージェントは以下の三つで構成されています。
- LLM: AIエージェントの頭脳。ツールの利用判断や最終成果物の出力を担う。
- プロンプト: AIエージェントが受ける指示。
- ツール: AIエージェントが利用できるツール。ファイルを読み取る関数などさまざまなツールを渡せる。
従来のLLMの構成要素はLLMとプロンプトのみですが、AIエージェントではそれに加えてツールも構成要素に入ります。タスクを実行するためのツールが渡されることにより、AIエージェントは自律的なタスク実行が可能になります。このツールの制御こそがAIエージェントの核となる部分です。
ここで注意すべきポイントはLLMはあくまで言語処理のみを行うという点です。LLMは文字列を受け取り、文字列を返すものです。そのためLLM自体がツールを使うというわけではなく、LLMは「このツールを使ってほしい」という判断結果を文字列で返すのみとなります。
ツールの仕組み(Function Calling)
LLMがツールを使う際にFunction Callingという仕組みを利用します。これはLLMに「使えるツール一覧」をJSONで渡すと、LLMが適切なツールと引数を選んで返してくれる仕組みです。
以下が実際にLLMに渡すJSONの例です。ツールや引数の説明を添えてLLMに渡します。これにより、LLMが利用可能なツールを理解でき、現在のタスクに沿ったツールを選択し、処理が行われます。
[
// ファイル・フォルダ一覧を取得する関数
{
"type": "function",
"function": {
"name": "list_files",
"description": "指定されたディレクトリ内のファイルとフォルダの一覧を取得します。各ファイルの名前、パス、サイズ、カテゴリ情報を返します。",
"parameters": {
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "一覧を取得するディレクトリパス。許可ディレクトリからの相対パスを指定。ルートを指定する場合は '.' または空文字を使用。"
},
"recursive": {
"type": "boolean",
"description": "trueの場合、サブディレクトリも再帰的に探索する。デフォルトはfalse。"
}
},
"required": ["directory"]
}
}
},
// ファイルの内容を読み込む関数
{
"type": "function",
"function": {
"name": "read_file",
"description": "指定されたファイルの内容をテキストとして読み取ります。テキストファイル、マークダウン、プログラミングファイル(.ts, .tsx, .js, .py等)に対応。バイナリファイルは読み取れません。",
"parameters": { ...
LLMが利用したいツール選んだ場合は、以下のようにレスポンス内のtool_callsにてツール呼び出しが実行されます。
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "list_files",
"arguments": "{\"directory\": \"sample\", \"recursive\": false}"
}
}
]
}
その後、ツール呼び出しにより、サーバー側で処理が行われます。その処理結果については以下のようにroleをtoolとしてLLMに返却すると、ツールを実行した結果としてLLMが認識することができます。
{
"role": "tool",
"content": "{\"success\":true,\"data\":{\"directory\":\"sample\",\"totalItems\":5,\"files\":[{\"name\":\"index.ts\",\"path\":\"sample/index.ts\",\"type\":\"file\",\"size\":1234,\"category\":\"code\"}]}}",
"tool_call_id": "call_abc123",
"name": "list_files"
}
この結果を元にLLMが次のタスクを判断することで、最終的なタスクの完了を目指します。
エージェントループ
AIエージェントはエージェントループという流れでタスクを実行します。まず初めに、与えられるタスクとツール定義をもとに、最初に利用するツールを考えます。その後、実際にツールを利用した結果から、次利用するツールを考え利用、というループになります。ツールを複数回利用し、タスクの解答に必要な情報を集め終われば、集めた情報をもとに最終回答を返してループを終了します。
実際の例を考えると以下のようになります。
「sample フォルダから先週の議事録をまとめて」と指示した場合の実際のループの流れは
- LLM:
list_files({ directory: "sample" })を呼ぶ - 結果: ファイル一覧が返る
- LLM:
read_file({ filePath: "sample/20260101議事録.txt" })を呼ぶ - 結果: ファイル内容が返る
- LLM: (他のファイルも読む…)
- LLM: 最終的に先週分の議事録レポートを生成
3. AIエージェントの実装
実装する際のアーキテクチャ構成例
各要素は以下のような役割を持ちます。エージェントが中心となりLLMやツールの呼び出しを行います。
-
UI: ユーザー入力の受付と結果表示 -
API: HTTPリクエスト処理 -
Agent Loop: エージェントループの制御とLLM呼び出し -
LLM: LLM APIの提供 -
Tool: ツールの一覧管理と個々のツールの処理
安全性の設計
システムで制御をするにあたり、三つの視点での制御が必要となります。
- AIが使うツールの制限
- どのツールを使用するか選択するのはLLMであるため、間違ってもOKなツールのみ渡す
- 危険なツールは最初から渡さない
- AIが使うパラメータの検証
- 引数の値を選択するのはLLMであるため、間違ったパラメータをはじく必要がある
- ループの上限を設定
- エージェントでは通常のLLMに比べツール実行系の会話履歴も積み上がるためトークン消費には気を付ける必要がある
これら観点を考えながら、設計・実装する必要があります。
今回開発したAIエージェントアプリ
あるフォルダworkspaceの中身を確認し、レポートを作成するAIエージェントを開発しました。
サンプルとしてworkspaceフォルダの中に二週間分の議事録を格納しており、この議事録をもとにレポートを作成するデモを考えます。また、議事録以外の自らのドキュメントを指定のフォルダ内に格納することでそれに対してレポートを作成することも可能です。
技術スタック
- Next.js 16
- TypeScript 5
- Tailwind CSS v4
- OpenAI SDK
- LM Studio
- Docker / Docker Compose
- react-markdown + react-syntax-highlighter
実装例
エージェントループの実装
簡略化したコードで全体像を把握していきます。
このエージェントループを抜ける条件は、LLMが最終応答を出力したときor最大ループ回数まで達したときです。
ループ内の処理は二つのパターンに分けられます。
- LLMがツール呼び出しをする場合(
responseにToolCallsがある場合):ツール実行をシステム側に依頼し、結果を格納 - LLMがツール呼び出しをしない場合:最終応答としてLLMの結果を表示
while (iteration < maxIterations) {
iteration++;
// LLMのレスポンスを取得
const response = await LLM.chatCompletion(messages);
// ツール呼び出しがなければ最終応答
if (!response.ToolCalls) return response;
// ツール呼び出しがあればツール実行
for (response.toolCalls) {
const result = executeTool(response.toolCalls);
messages.push(result); // ツール実行結果を次のループでLLMに渡す
};
}
ツールの実装
ツール定義の4要素として以下があります。これらを定義することでLLMが説明文を見ながら適切なツールを選択できるようになります。
- name: ツールの名前・識別子
- description: ツールの説明文
- parameters: ツールで利用する引数とその説明文
- execute: ツールの処理部分
const readFileTool = {
// ツール・引数の説明(LLM用)
neme: "read_file",
description: "指定されたファイルの内容をテキストとして読み取ります",
parameters : {
filePath: {
type: "string",
description: "読み取るファイルのパス"
},
},
// 実際の処理
async execute(params) {
// ファイル読み取り処理
},
};
LLMの呼び出し・ツールの呼び出し(Function Calling)の実装
今回はOpenAI APIを利用します。OpenAI APIのChatCompletionsでは、今までのメッセージ履歴をLLMに渡し、次の回答を得ます。今回はそれに加えてタスク実施のためにツール定義もLLMに提供し、LLMにツール使用可否を判断してもらいます。
const response = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: model,
messages: messages, // メッセージ履歴
tools: tools, // ツール定義を渡す
tool_choice: "auto" // LLMがツールを自動選択
// その他オプション
}),
});
安全性の設計
今回はファイル操作を行うことができるエージェントを想定しています。そのため、安全なファイル操作になるように目指します。
制約を課さないと、フォルダを遡って機密情報を取得するなどの事故が発生しうるため注意が必要です。
まずは、前述した「安全性の設計で確認すべき3観点」について再確認して実際に実装に入ります。
- AIが使うツールの制限
- AIが使うパラメータの検証
- ループの上限を設定
まずは「AIが使うツールの制限」に関して確認していきます。今回のエージェントはフォルダ内の資料を読み取り、Webアプリ上でプロンプトに対する回答を表示するというものです。そのため、ファイルの作成やファイルの書き換えは不要です。スクラッチで開発する場合、これら不要な処理に関しては実装しなければ事故は発生しません。そのため、今回はファイル名一覧の取得とファイルの中身を読み取るツールのみ実装し、LLMに渡すようにします。
export const allTools: ToolDefinition[] = [listFilesTool, readFileTool];
次に「AIが使うパラメータの検証」について確認していきます。今回の実装ではフォルダパスなどがパラメータとなります。LLMが予期せぬフォルダまで探索し、機密情報を取得するなどの事故を防ぐためにもパラメータの検証は必須です。以下の検証関数がAPIエンドポイントを叩く際とLLMがツールを利用する際に実行されます。
export function resolveAndValidatePath(userPath: string): string {
// 1. ユーザー指定の相対パスを絶対パスに変換
const resolved = path.resolve(baseDir, userPath);
// 2. パスを正規化(../ 等を解決)
const normalized = path.normalize(resolved);
// 3. 許可ディレクトリ内かを検証
// path.resolve()で正規化された絶対パス同士を比較
const baseDirResolved = path.resolve(baseDir);
if (!normalized.startsWith(baseDirResolved + path.sep) && normalized !== baseDirResolved) {
throw new Error(
`Access denied: Path "${userPath}" is outside allowed directory "${baseDir}"`
);
}
return normalized;
}
最後に「ループの上限を設定」について確認します。AIエージェントはツールなどのコンテキストもプロンプトでLLMに渡すため、通常のLLMよりコンテキストの使用量が多いです。また、AI側で無限ループに陥った場合、API利用料は永遠に増え続けてしまう可能性もあります。そのためこのような事故を減らすためにあらかじめ上限をシステム側で設定する必要があります。
以下のように、エージェントループの上限回数を決めておくと、仮に思考ループに陥ったとしてもループを終了させることができ、無尽蔵にLLMが利用されることを防げるようになります。
// 上限回数を踏まえたループ
while (iteration < maxIterations) {
iteration++;
// その他処理
}
デモ
デモ内容
以下のようなデイリーミーティングの議事録が二週間分テキストファイルとして保存されているとき、エンジニアCの進捗についてまとめてもらう。
デイリーミーティング 2026/01/19(月) 10:00-10:15
PO: おはようございます。今週からスプリント3が始まります。各自の担当タスクの確認と、現状の進捗を共有してください。
エンジニアA: おはようございます。今スプリントでは、ユーザー認証機能の実装に入ります。まずログイン画面のUI作成から着手する予定です。今日中にワイヤーフレームの確認を終わらせて、明日から実装に入りたいと思います。
PO: ありがとうございます。ログイン画面のデザインカンプは昨日Figmaに上がっているので確認お願いします。
エンジニアA: 了解です。
エンジニアB: 私は商品検索機能を担当します。今日はまずElasticsearchの環境構築から始めます。ローカルでDockerでESを立てて、基本的なインデックス作成まで今日やりたいです。
PO: ESのバージョンは8系で統一でお願いします。インフラチームにも確認済みです。
エンジニアB: 了解しました。8系でいきます。
エンジニアC: 決済機能を担当します。まずStripeのサンドボックス環境のセットアップから始めます。APIキーの発行とか、テスト用のアカウント作成を今日やります。
PO: Stripeのアカウントは経理チームが作成済みのものがあるので、後でSlackで共有します。
エンジニアC: 助かります。ありがとうございます。
PO: 全体の方針として、スプリント3は2週間で基本機能の実装完了を目標にしています。来週金曜にスプリントレビューがあるので、そこでデモできる状態を目指しましょう。何か質問ありますか?
エンジニアA: 認証はメールアドレスとパスワードだけでいいですか?ソーシャルログインは?
PO: 今回はメールアドレスとパスワードのみで大丈夫です。ソーシャルログインは次のスプリントで対応します。
エンジニアA: 了解です。
PO: それでは今日もよろしくお願いします。
デモ結果
まず、「1/19から1/23のエンジニアCの進捗をまとめてください」というプロンプトでAIエージェントにタスクを依頼します。

すると、初回のリクエストでシステムプロンプト、ユーザープロンプト、ツール定義をLLMに送信します。(画面はLMStudio)

プロンプトとツール定義をもとに、次の行動をLLMが判断します。初回は議事録ファイルが格納されているディレクトリのファイル探索を行うlist_filesというツールを呼び出しています。これ以降はツールの結果から次の行動をLLMが判断します。

ツールの利用が不要になった時点で最終成果物をLLMが出力します。簡易なAIエージェントかつローカルLLMなので精度はそこまでですが、自律してタスクを完遂できるAIエージェントとなっています。

4. おわりに
まとめ
今日のゴールからまとめると、以下の内容を説明しました。
- AIエージェントの構成要素を理解する
- LLM, ツール, プロンプトの3要素がある
- 今までのLLMに追加してツールが利用可能になったと認識していれば問題ない
- AIエージェントの安全な設計方法を理解する
- AIが使うツールの処理で安全性を担保する
- AIが判断したパラメータをシステムで検証する
- ループ上限を設けて無限にAPIを叩くことを防ぐ
- AIエージェントの実装例の感覚を掴む
- エージェントループによってツールを使いながら自律的にタスクを実行
- Function Callingの仕組みを使いLLMにツールを利用させる
- エージェントの安全性の担保はツールの処理部分で行う。
所感
結局AIエージェントを開発する場合にどんな能力が必要なのか?と考えたとき、「ガードレールを敷ける設計力」だと感じました。例えば、リスクを把握し対策できるのか?その対策はリスクに対して網羅的か?を考えられるスキルです。
私はエンジニアとして新米であり、AIとともにガードレールを敷きました。しかしながら網羅的であるかに関しては、まだまだ経験不足から判断できませんでした。そのため、今後はセキュリティや安全なシステムを学ぶことにより、より網羅的に対処できるように成長する必要があると実感しました。
AIは進化をし続けていて、プロンプトを工夫せずとも十分の性能が出るようになってきました。さらにLLMの開発はビックテックが行っていて正直コントロールできる状態ではないように思います。そのため今後は、どう安全に使いこなすか?誰が使っても安全なシステムをつくれているのか?という部分にフォーカスして考え、開発することが重要になってくるのではないかと思います。その観点を考え続けることでエンジニアとしての価値を発揮できるように研鑽していきたいと思います。
最後になりましたが、今回の記事がみなさまの活動の参考になれば幸いです。















