0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CAPとLLMはどういう関係?「誰が誰を呼ぶか」を図とコードで整理してみた

0
Last updated at Posted at 2026-10-26

CAPのサービスをAIエージェントにして、いろいろ動かしてきました。ところが、いざ人に説明しようとすると「LLMがCAPを呼んでいるのか、CAPがLLMを呼んでいるのか」「関数を実行しているのはどっちなのか」が、自分でもあいまいなことに気づきました。

そこで、特定のアプリからいったん離れて、CAPとLLMの関係を「誰が誰を呼ぶか」という視点で整理し直しました。図と短いコードで説明します。

2026年10月25日時点の理解をまとめたものです。コードは説明用に短くしたもので、そのままコピーして動くものではありません

まとめ

項目 内容
一番大事なこと CAPがLLMを呼ぶ。LLMはCAPを呼ばない。 LLMはCAPから見ると「HTTPで呼ぶ外部APIの1つ」
LLMが返すもの 「この関数を、この引数で呼んで」という文字(JSON)だけ。関数を実行するのはプログラム側
関係のパターン A:CAPがLLMを関数として使う/B:CAPがエージェントになる/C:外のAIアシスタントがCAPを使う
役割分担 LLMは「賢さ」(読み取り・判断・文章化)、CAPは「確実さ」(データ・実行・チェック・記録・権限)

1. CAPとLLMはそれぞれ何者か

CAP LLM
正体 業務アプリを作るためのフレームワーク(Node.js/Java) 文章を送ると文章を返す外部のAPI(OpenAI、Claude、Geminiなど)
持っているもの データ(DB)、業務ルール、API(ODataなど) 言葉の知識(学習済みのモデル)
持っていないもの 日本語を「理解」する力 自社のデータ、業務ルール、会話の記憶
呼ばれ方 HTTPで呼ばれる(/odata/v4/...) HTTPで呼ばれる(https://api.openai.com/...)

ポイントは、 どちらも「HTTPで呼ばれる側」 だということです。LLMが自分から誰かを呼びに行くことはありません。

CAPから見ると、LLMは 天気APIや郵便番号APIと同じ「外部APIの1つ」 です。違うのは、入力と出力が自然な文章だということだけです。

2. LLMの呼び出しは、ただのHTTP

LLMを呼ぶ最小のプログラムです。

const res = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
  body: JSON.stringify({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: '「発注」を英語で一言で' }],
  }),
})
const data = await res.json()
console.log(data.choices[0].message.content) // → "Purchase order"

ここで押さえておきたいことは2つです。

  • 会話の中身は、body の中の messages に入れて送ります。この例では、利用者の発言1件だけです。LLMは前の呼び出しを覚えていないので、続きの会話をしたいときは、呼ぶ側がこの配列に前のやり取りを全部入れて送り直します。
// 2回目の呼び出し:1回目のやり取りも入れて送り直す
messages: [
  { role: 'user', content: '「発注」を英語で一言で' },
  { role: 'assistant', content: 'Purchase order' },     // 1回目のLLMの答え
  { role: 'user', content: 'じゃあ「仕入先」は?' },     // 今回の質問
],
  • 返ってくるのはJSONです。中の文字列を取り出すのは呼ぶ側の仕事です。

3. CAPとLLMの関係は3パターン

先に、この記事での「エージェント」の意味をそろえておきます。

この記事で「エージェント」と呼ぶのは、LLMに「次は何をする?」と聞き、回答された関数を自分で実行し、結果を渡してまた聞く、を繰り返すプログラムのことです。
LLMそのものはエージェントではありません。LLMは、エージェントが相談する相手(頭脳役)です。

整理してみると、CAPとLLMの関係は次の3つに分けられました。違いは、エージェントがいるかどうか、いるならCAPの中か外かです。

パターン 概要 誰が誰を呼ぶか
A. CAPがLLMを「便利な関数」として使う 要約・分類・翻訳など。エージェントはいない CAP → LLM
B. CAPがエージェントになる エージェントはCAPの中。LLMに「次に何をするか」を考えてもらい、CAPが実行する CAP → LLM(何回も)、実行はCAP
C. 外のAIアシスタントがCAPを道具として使う エージェントはCAPの外(ClaudeやJouleなどのAIアシスタント)。CAPは道具を提供する側 AIアシスタントのプログラム → CAP

どのパターンでも、LLMから出ている矢印はありません。 LLMはいつも「呼ばれて、文字を返す」だけです。以下、順に見ていきます。

4. パターンA:CAPがLLMを「便利な関数」として使う

一番単純な使い方です。CAPのハンドラの中で、外部APIとしてLLMを1回呼びます。問い合わせの本文を要約する例です。

srv/inquiry-service.cds
service InquiryService {
  entity Inquiries { key ID : UUID; body : String; summary : String; }
  action summarize(ID : UUID) returns String;
}
srv/inquiry-service.js
import cds from '@sap/cds'

export default class InquiryService extends cds.ApplicationService {
  init() {
    const { Inquiries } = this.entities

    this.on('summarize', async (req) => {
      const { body } = await SELECT.one.from(Inquiries, req.data.ID)   // ① DBを読むのはCAP
      const summary = await askLLM([                                    // ② LLMには文字だけ渡す
        { role: 'system', content: '問い合わせを100字以内で要約してください' },
        { role: 'user', content: body },
      ])
      await UPDATE(Inquiries, req.data.ID).with({ summary })            // ③ 保存するのもCAP
      return summary
    })

    return super.init()
  }
}

async function askLLM(messages) {
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
    body: JSON.stringify({ model: 'gpt-4o-mini', messages }),
  })
  return (await res.json()).choices[0].message.content
}
  • LLMはDBを見ていません。CAPが読んで、文字として渡しています。
  • やっていることは「外部APIを呼んで、結果を保存する」という、普通のCAPの処理です。

5. パターンB:CAPがエージェントになる(ツール呼び出し)

「テスト文具にノートを5冊発注して」のような依頼を、LLMに手順を考えてもらいながら処理するパターンです。これまで @cap-js/agents を使用していたのはこれでした。

仕組みの核は**ツール呼び出し(Function Calling/Tool Calling)**です。

  1. CAPがLLMに、使える関数の 一覧(名前・説明・引数の形) を渡します。関数の中身(コード)は渡しません。
  2. LLMは「searchSuppliers を {"keyword":"テスト文具"} で呼んで」という**文字(JSON)**を返します。
  3. CAPがその名前の関数を実行し、結果をLLMに渡して、また聞きます。
  4. LLMが関数を頼まなくなったら、それが最後の返事です。

この例では、ループが3回まわります(① 仕入先の検索を頼まれる → ② 発注の登録を頼まれる → ③ 最後の返事)。ツールを1回使うたびに、LLMとの往復が1回増えます。

最小のプログラム

// ① 関数の本体(CAPならサービスのアクションのハンドラ)
const tools = {
  searchSuppliers: ({ keyword }) => suppliers.filter((s) => s.name.includes(keyword)),
  createOrder: ({ supplierCode, quantity, unitPrice }) => {
    if (quantity <= 0) throw new Error('数量は1以上にしてください') // 業務ルールはプログラム側
    return { orderNo: 4500000001, supplierCode, total: quantity * unitPrice }
  },
}

// ② LLMに渡す「関数の一覧」(説明書だけ。コードは渡さない)
//    CAPなら、CDSのアクション定義と /** */ コメントから作れる
const toolDefinitions = [
  { type: 'function', function: { name: 'searchSuppliers', description: '仕入先を名前の一部で検索する',
      parameters: { type: 'object', properties: { keyword: { type: 'string' } }, required: ['keyword'] } } },
  { type: 'function', function: { name: 'createOrder', description: '発注を登録する',
      parameters: { type: 'object', properties: { supplierCode: { type: 'string' }, quantity: { type: 'integer' }, unitPrice: { type: 'number' } },
                    required: ['supplierCode', 'quantity', 'unitPrice'] } } },
]

// ③ ループ(これが「エージェント」の正体)
const messages = [{ role: 'user', content: 'テスト文具にノートを5冊、単価200円で発注して' }]
while (true) {
  const answer = await askLLM(messages, toolDefinitions) // LLMに聞く(HTTP POST)
  messages.push(answer)
  if (!answer.tool_calls) { console.log(answer.content); break } // 最後の返事

  for (const call of answer.tool_calls) {
    const name = call.function.name                    // ただの文字列
    const args = JSON.parse(call.function.arguments)   // ただの文字列 → オブジェクト
    let result
    try {
      result = tools[name](args)                       // ④ ★ 実行するのはこのプログラム
    } catch (e) {
      result = { error: e.message }                    // エラーも文字にしてLLMに返す
    }
    messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) })
  }
}

tools[name](args) の1行が全体の核です。 LLMが返した文字列で関数の表を引き、プログラムが呼びます。Webアプリのルーティング(URLの文字列 /orders を見て createOrder() を呼ぶ)と同じ形です。

CAPでこれをやると、関数を実行する tools[name](args) の行(コメントの④)が、srv.send(name, args)(別のサービスのアクションを呼ぶ、いつもの書き方)になります。画面から呼ばれたときと同じハンドラを通るので、LLM経由でも入力チェックをすり抜けません。

実際に動かすと

同じ形のループを、プラグインを使わずにCAPのサービスとして手で書き、動かしてみました。ログは次のとおりです。

[agent] --- 1 回目: CAP → LLM (メッセージ 2 件 + ツール一覧 2 件を送る)
[agent] LLM → 「searchSuppliers を {"keyword":"テスト文具"} で呼んで」
[agent] CAP が実行 → 結果: [{"code":"100003","name":"テスト文具株式会社"}]
[agent] --- 2 回目: CAP → LLM (メッセージ 4 件 + ツール一覧 2 件を送る)
[agent] LLM → 「createOrder を {"supplierCode":"100003","itemText":"ノート","quantity":5,"unitPrice":200} で呼んで」
[agent] CAP が実行 → 結果: {"orderNo":4500000001,...,"total":1000}
[agent] --- 3 回目: CAP → LLM (メッセージ 6 件 + ツール一覧 2 件を送る)
[agent] LLM → 返事: 発注しました。発注番号は 4500000001、合計 1000 円です。
  • 送るメッセージが2 → 4 → 6件と増えています。毎回、会話全部を送り直していることがわかりました。
  • 仕入先コード 100003 は、LLMがもともと知っていたものではありません。CAPがDBを検索して、結果を文字で渡したからわかったものです。
  • 数量を0冊にして頼むと、LLMは0のまま createOrder を呼ぼうとしました。止めたのはCAPのハンドラの入力チェックです。業務ルールはプロンプトではなく、プログラムに書く必要があります。

@cap-js/agents の @agent は、この最小のプログラムでやっていること(関数の一覧づくり、LLMの呼び出し、ループ、関数の実行)をCAPのサービスから自動で用意してくれるものだと理解しました。

6. パターンC:外のAIアシスタントがCAPを道具として使う

Claude DesktopやJouleなど、CAPの外にあるAIアシスタントから「今月の発注を一覧にして」と聞くケースです。このAIアシスタントも、LLMに聞きながら関数を実行していくプログラムなので、エージェントです。CAPは、そのエージェントに「ツールを提供する側」になります。

パターンBと見比べると、エージェントがCAPの中から外に移っただけです。Bではエージェント=CAP、CではエージェントがAIアシスタントで、CAPはその道具になっています。

  • 一見「LLMがCAPを呼んでいる」ように見えますが、CAPにHTTPを送っているのはAIアシスタントのプログラムです。LLMは相変わらず「呼んで」という文字を返しているだけです。
  • CAPから見ると、普通のAPIの利用者が1人増えただけです。認証・権限・入力チェックは、画面から呼ばれたときと同じようにCAPが行います。

AIアシスタント側のプログラムで、ツールを実行する部分のイメージです。

// AIアシスタント側のプログラム(CAPの外)
const tools = {
  getOrders: async ({ month }) => {
    const res = await fetch(`https://my-cap-app.example.com/odata/v4/order/Orders?$filter=startswith(orderDate,'${month}')`, {
      headers: { Authorization: `Bearer ${token}` },   // CAPの認証はそのまま効く
    })
    return (await res.json()).value
  },
}
// あとはパターンBと同じループ:tools[name](args)

7. 役割分担

3つのパターンを通して見ると、役割分担はいつも同じでした。

自分が誤解していたことも含めて、表にしておきます。

よくある誤解 正しくは
LLMがDBを読んでいる 読むのはプログラム(CAP)。LLMは結果の文字を受け取るだけ
LLMが関数を実行している LLMは「この関数を呼んで」というJSONを返すだけ。実行はプログラム
LLMは会話を覚えている 覚えていない。プログラムが毎回、会話全部を送り直す
業務ルールはプロンプトに書けば守られる プロンプトは「お願い」。守らないことがあるので、必ずプログラムでチェックする
AI用に別のシステムが要る パターンA・BはCAPのサービスに処理を足すだけ。Cも既存のAPIをそのまま使える

感想

  • ざっくりというと、LLMは「考えて、文字で答える」API、CAPは「データを持ち、ルールを守って、実際に処理する」アプリでした。
  • @cap-js/agents を使うと @agent を付けるだけでエージェントになるので、便利な反面、中で何が起きているかが見えにくくなっていました。
  • 「LLMは関数を実行しない」「業務ルールはプログラム側」とわかると、HITL(人の確認)や入力チェックをどこに置くべきかも自然に決まると感じました。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?