こう想像してみてください。バックエンドには、退屈な自動化のチケットが山積みです。メールを解析したり、顧客チャットを要約したり、サービス間でワークフローをオーケストレーションしたり。TwitterでLLMが魔法のようなことをしているのは聞いたことがある。でも、それをガタガタのNode.js/Express APIに統合する?それはまた別の話です。私はそこにいました。そして、LLMによるエージェントを実際のバックエンドで扱おうとしたことがあるなら、それが「ブレークスルー」でもあり「髪の毛を抜きたくなる」ことでもあるのを知っているはずです。ここでは、エージェント型AIを本番投入したときに実際に起きたこと、もっと早く知っておきたかったこと、そして拝借できるいくつかのコードスニペットを紹介します。
ExpressでLLMエージェントにわざわざ手を出す理由は?
バックエンドの仕事の多くは、システム間でデータをやり取りし、フォーマットを変換し、退屈な業務ロジックを埋めることです。LLMによるエージェント(OpenAIのfunction callingやLangChainのTool連携のようなもの)は、通常のスクリプトよりも文脈への理解や柔軟性を持って、こうした「つなぎ」の作業を自動化できます。
たとえば、顧客メールから意味を抽出するために50個もの脆い正規表現を書く代わりに、エージェントに何をすべきか、そしてどうやるかを判断させ、必要に応じて社内APIを呼び出すことができます。
ただし落とし穴は細部にあります。APIを苦しめることなく、LLMエージェントに確実に有用な仕事をさせるには、多くの試行錯誤が必要でした。
例1:Expressルートからエージェントを呼び出す
まずはシンプルにいきましょう。Expressのエンドポイントで、LLMエージェントを使って文章の一部を要約する必要があるとします。
以下は、OpenAIのGPT-4(Node.js SDK経由)を使い、シンプルな「summarize」ツールを用いる最小限の例です。
// routes/summarize.js
const express = require('express');
const { OpenAI } = require('openai'); // 公式OpenAI SDK
const router = express.Router();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
router.post('/summarize', async (req, res) => {
try {
const { text } = req.body;
if (!text) return res.status(400).json({ error: 'Missing text' });
// 要約プロンプトでGPT-4モデルを呼び出す
const completion = await openai.chat.completions.create({
messages: [{ role: 'user', content: `Summarize this: ${text}` }],
model: 'gpt-4',
max_tokens: 200,
temperature: 0.5,
});
// 要約をクライアントへ返す
res.json({ summary: completion.choices[0].message.content });
} catch (err) {
// デバッグのためにエラーをログ出力する
console.error(err);
res.status(500).json({ error: 'Summarization failed' });
}
});
module.exports = router;
ここで重要なのは何?
- 必ず入力を検証する。LLMはゴミのような入力で混乱したり、空のプロンプトでトークンを無駄に消費したりします。
- 例外を捕捉してログに残す。これらのログが必要になります。LLM APIは思っているよりも頻繁に失敗します。
-
max_tokensとtemperatureはコストと品質に本当に影響します。最初は控えめに。
このエンドポイントは動きますが、まだ「エージェント的」ではありません。単一のプロンプトです。LLMが本当のアクションを引き起こし始めると、面白さが本格化します。
例2:LLMエージェントが社内API呼び出しをトリガーする
エージェントの魔法は、どのツール(関数)を、どの順番で呼ぶかをLLMに判断させられることです。たとえば、サポート依頼を処理するLLMエージェントを作りたいとします。必要に応じて社内の /lookup-user や /send-email のエンドポイントを呼び出せる、という感じです。
私が見つけた最も柔軟な方法:OpenAIのfunction callingを使い、その関数定義をバックエンドの実際のAPI呼び出しに接続します。
以下に、簡略化した(ただし動作する)パターンを示します:
// services/agent.js
const { OpenAI } = require('openai');
const axios = require('axios');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// エージェントが呼び出せるツール(関数)を定義する
const functions = [
{
name: 'lookupUser',
description: 'メールアドレスでシステム内のユーザーを検索します',
parameters: {
type: 'object',
properties: {
email: { type: 'string', description: 'ユーザーのメールアドレス' },
},
required: ['email'],
},
},
{
name: 'sendEmail',
description: 'ユーザーにメールを送信します',
parameters: {
type: 'object',
properties: {
to: { type: 'string', description: '受信者のメールアドレス' },
subject: { type: 'string' },
body: { type: 'string' },
},
required: ['to', 'subject', 'body'],
},
},
];
// 関数名を実際の実装にマッピングする
const tools = {
lookupUser: async ({ email }) => {
// 内部APIを呼び出す(データベースを直接問い合わせてもよい)
const resp = await axios.post('http://localhost:3000/internal/lookup-user', { email });
return resp.data;
},
sendEmail: async ({ to, subject, body }) => {
// SMTPサービス、またはメールのマイクロサービスを呼び出す
await axios.post('http://localhost:3000/internal/send-email', { to, subject, body });
return { status: 'sent'};
},
};async function runAgent(messages) {
// 手順 1: LLM に、呼び出したい関数を何にするかを尋ねる
const completion = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
functions,
});
const functionCall = completion.choices[0].message.function_call;
if (!functionCall) {
// 呼び出す関数がないので、メッセージをそのまま返す
return { result: completion.choices[0].message.content };
}
// 手順 2: 対応付けられた関数を呼び出す
const fn = tools[functionCall.name];
if (!fn) throw new Error('不明な関数: ' + functionCall.name);
// 関数の引数は常に OpenAI からの JSON 文字列
const args = JSON.parse(functionCall.arguments);
const fnResult = await fn(args);
// 手順 3: 関数結果を LLM に渡して次のステップへ進める
messages.push({
role: 'function',
name: functionCall.name,
content: JSON.stringify(fnResult),
});
// 手順 4: 会話を続行する(複数ステップ繰り返すことができる)
const finalCompletion = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
functions,
});
return { result: finalCompletion.choices[0].message.content };
}
module.exports = { runAgent };
なぜこのパターン?
- LLM が、どのツールをどんな引数で呼び出すかを選びます。
- あなたのコードが LLM と実際の API の間を安全に仲介します。決してモデルに直接呼ばせないようにします。
toolsにログ出力、リトライ、権限チェックを簡単に追加できます。
ここで私は最初の壁にぶつかりました。OpenAI から返ってくる「関数呼び出し」の結果は常に JSON 文字列であり、注意深く解析して検証する必要があります。必要なプロパティが欠けていたせいで本番エージェントがクラッシュした不具合を、週末まるごとデバッグする羽目になりました。
Example 3: Express 経由で LLM のレスポンスをストリーミング
状況によっては、生成されていくエージェントの応答をストリーミングしたいことがあります。例えばチャット UI を動かすためです。OpenAI SDK はストリーミングに対応していますが、Express でそれをきれいに組み立てるには少し注意が必要です。
私がやったのはこうです:
// routes/streamAgent.js
const express = require('express');
const { OpenAI } = require('openai');
const router = express.Router();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
返却形式: {"translated": "翻訳されたHTML"}router.post('/agent/stream', async (req, res) => {
try {
const { prompt } = req.body;
if (!prompt) return res.status(400).json({ error: 'プロンプトがありません' });
// Expressにこれがイベントストリームであることを伝える
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
// OpenAI SDKでストリームを開始する
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
// 到着したトークンをそのままパイプする
for await (const chunk of stream) {
// それぞれのチャンクをSSEイベントとして送信する(1トークンずつ)
res.write(`data: ${chunk.choices[0]?.delta?.content || ''}
`);
}
// 完了したらストリームを終了する
res.end();
} catch (err) {
console.error('ストリームのエラー', err);
// ストリーミングに失敗した場合は接続を閉じる
res.end();
}
});
module.exports = router;
いくつかのヒント:
- 必ずSSE(Server-Sent Events)の正しいヘッダーを設定してください。
res.end()を呼び出すのを忘れないでください。これがないとクライアントが永遠に待ち続けます。- プロキシの背後にいる場合(例:Nginx)、必ずストリーミングを有効化し、バッファをフラッシュしてください。
これを初めて出荷したとき、Cache-Control: no-cache を設定するのを忘れてしまい、Chrome がストリームをずっとバッファし続けました。IE6と格闘していた頃を思い出すようなデバッグ体験でした。
共通のミス
最初にこれらを誰かに教えてもらえていればよかったです:
1. エージェントにやらせすぎる
LLMエージェントに すべて の社内APIを呼ばせないでください。何が公開されているかを明確にし、すべての引数を検証してください。そうしないと、意図していないことをエージェントが勝手に実行してしまいます。
2. トークンコストとレイテンシを無視する
LLMは無料ではありません。また、モデル呼び出しをループで行うエージェント型のフローは、コスト(そしてAPIの応答速度)を急速に悪化させる可能性があります。常に妥当な max_tokens を設定し、可能な場合はバッチ処理を行い、使用状況を監視してください。
3. 関数呼び出しエラーを扱わない
OpenAI(ほかのLLMも同様)は、ときどき存在しない関数名をでっち上げたり、必須の引数を忘れたりします。必ず検証し、エラーを適切に処理してください。LLMが「勝手にうまくいく」ことを前提にしないでください。
重要なポイント
- エージェント型のLLMはExpress APIをずっと賢くできますが、ツールの境界と検証を慎重に設計する必要があります。
- LLMのリクエストとツール呼び出しの両方を必ずログに残し、監視してください。デバッグやコストの追跡に必要になります。
- 小さく始めましょう。重要または高コストなものを公開する前に、シンプルで安全なツールから接続します。
- LLMのレスポンスをストリーミングすることは可能ですが、ヘッダー、プロキシ、クライアントの切断を処理する必要があります。
- LLMから送られてくる すべて を検証してください。モデルに、APIを安全に呼び出すことをそのまま期待してはいけません。
LLMエージェントをNode.js/ExpressのAPIに持ち込む体験は、バックエンド自動化のための新しいチートコードを解放したように感じました。魔法ではありませんが、正しいガードレールを設定すれば、非常に柔軟です。もし検討しているなら、おもちゃのエージェントから始めて、どんな予想外が起きるか確かめてみてください。
役に立ったなら、私たちのブログでもっとプログラミングのチュートリアルをチェックしてください。Python、JavaScript、Java、データサイエンス、そしてその他を扱っています。

