高階関数によるAI Agentのコンテキストエンジニアリング

スパイスコード CTO の櫻木です. スパイスコードは,「ロカルメ・オーダー」

order.localmet.com

という AI Agent を内包した ERP サービスを開発・提供しているスタートアップです. 本記事では我々の AI Agent 開発におけるコンテキストエンジニアリングに関する取り組みを紹介します.

1 コンテキストエンジニアリングの重要性

AI Agentの作成において「コンテキストエンジニアリング」が重要であることは以前より周知されています. blog.langchain.com

以前は人間がLLMへ直接プロンプトを与えていたため「プロンプトエンジニアリング」に意識が向いていました. しかし,AI Agentは人の手がほとんど介在しないまま自律的に推論と実行を繰り返します. そのため単なるプロンプトエンジニアリングだけでなく,「エージェントが参照するコンテキストをどう設計するか」が決定的に重要になります. コンテキストエンジニアリングでは必要十分な情報だけを安全に供給することにより

  1. LLM の推論力を最大限引き出す
  2. LLM が抱える入力長制約を回避する
  3. “Lost in the Middle”問題を回避する

ことを目的とします. この視点がなければ,エージェントは複雑な業務フローの途中で意図せぬ結果に陥りがちです. 本記事ではこのコンテキストエンジニアリングに対するスパイスコードでの取り組みをロカルメオーダーに内包している電子メール注文書受領Agentとデータ抽出 Agentの例を使って紹介します.

2 tool利用によるコンテキスト圧迫問題

我々が提供しているAI Agentは以下のようなツールを使うことができます

  • 自前のmcpサーバ経由でユーザに代わりロカルメオーダー上のデータを操作・抽出
  • お客様の代わりに電子メールの確認・操作(注文書のダウンロード・アップロード)

これらのツールで課題になるのが目的を達成するために使うツールの入出力によるコンテキストの圧迫です. 以下の例のように

  1. ツールの入出力のデータ量が大きい場合
  2. 繰り返し処理が生じる場合

それぞれでコンテキストの圧迫が発生することは想像に難くないと思います.

例1

特定の店舗にある在庫の平均原価額取得

TOOL CALL: localmet_mcp.get_shop(query: "shopA")
    returns shopA

TOOL CALL: localmet_mcp.get_stocks(shopId: shopA.id)
    returns 10000 rows stock info -> ❌

TOOL CALL: average_cost_by_sku(stocks: 10000 rows stock info) -> ❌
    returns 1000 rows averaged stock info -> ❌

例2

特定期間における電子メールから注文書を抽出, 一部のメールではいわゆるPPAPが利用されている

TOOL CALL: get_emails(from: 'YYYYMMDD', to: 'YYYYMMDD')
    returns 1000 emails -> ❌

以下対象全てに対して実行 -> ❌
TOOL CALL: evaluate_order_email(id: 'email1')
    returns {
        is_order_related,
        urls,
        password_candidates,
        attachments
    }

TOOL CALL: handle_file(attachmentId: 'attachment1', passwordCandidates: tooManyCandidates) -> ❌

TOOL CALL: handle_url(url 'example.com', passwordCandidates: tooManyCandidates) -> ❌

3 高階関数, コード実行によるアプローチ

上記のような問題に対して我々は大きく分けて2つのアプローチを用いて対策しています. 現在それぞれ独立した機構として設計しユースケースに応じて使い分けていますが全てを統合したアプローチも検討中です. 重要な点はどちらも高階関数ツールを用意し,中間処理をコンテキストに載せないことです. 更に最終結果に関してもartifactとして保存し, idのみを返すことで次に実行するchainに再利用したり, サマリのみをユーザに提供することができます.

3.1 tool chain tool

mcpツール含め, ツール群を全てchain toolを経由して使用させることで無駄な中間ツールの出力を削減した上で最終的な結果はartifactとして保存しidのみ返す

例1

Tool Input

{
    context: "shopAある在庫の平均原価額取得"
    purpose: "shopAの存在確認をした上で全在庫を取得しskuごとに平均コストを算出する"
    tool_names: ["mcp_get_shop", "mcp_get_stocks", "average_cost_by_sku"]
    each_args: {
        "mcp_get_shop": { "query": "shopA"},
        "mcp_get_stocks": { "shopId": "get_shop.id"},
        "average_cost_by_sku": { "stocks": "get_stocks.result" },
    },
    expected_result: "skuごとの平均在庫金額リスト"
}

Tool Output

{
    "artifactId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

例2

こちらはより内部状態の管理を明確にしたもの,artifactとして保存されるのは最終的結果に加えてプロセス全体の最終状態なども含める

Tool Input

{
    "initial_state": {
        "purpose": "YYYYMMDDからYYYYMMDDまでのメールから注文書googleDriveへアップロード",
        "password_candidates_all": []
    },
    {
        "steps": [
            {
                "tool_name": "mailbox_message_search_tool",
                "arguments": {
                    "after": "2025/11/01",
                    "before": "2025/12/31"
                },
                "result_key": "search"
            },
            {
                "items_key": ".search.messages",
                "item_alias": "message",
                "body": [
                    {
                        "tool_name": "mailbox_body_extractor_tool",
                        "arguments": {
                            "subject": ".message.subject",
                            "body": ".message.snippet"
                        },
                        "result_key": "body_collect"
                    },
                    {
                        "predicate": ".body_collect.password_candidates",
                        "body": [
                            {
                                "operation": "extend",
                                "target_key": "password_candidates_all",
                                "value_from": ".body_collect.password_candidates"
                            }
                        ]
                    }
                ]
            },
            {
                "items_key": ".search.messages",
                "item_alias": "message",
                "body": [
                    // 1件づつに対する処理
                ]
            }
        ]
    }
}

Tool Output

{
    "artifactId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

3.2 コード実行

tech-blog.localmet.com で紹介しているような安全な実行環境においてコード実行し, 最終的な結果はartifactとして保存しidのみ返す. こちらはまだ実験段階ではありますがDSLよりAI Agentにとって明瞭で強力なツールになると考えています.

Tool Input

def exec():
    shop = mcp_get_shop("shopA")
    stocks = mcp_get_stocks(shop.id)
    return average_cost_by_sku(stocks)

Tool Output

{
    "artifactId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

4 自己学習サイクルによる継続的改善

我々は以下の3段階のフィードバックループを実装することにより複雑なDSL, コード生成の難しさを解決しています.

実行時の自己修復

Pydantic modelによるバリデーションに加え,実行可能性も評価します.エラー時は詳細なエラー情報をエージェントに返却し,自己修復を促します.

# 例: shopIdが存在しないケース
{
    "error": "ValidationError",
    "detail": "shopId 'invalid-id' not found",
    "suggestion": "Use mcp_search_shop first"
}

成功パターンのKnowledge化

成功した実行ログに対して:

  1. LLM as a Judge または人間が品質評価
  2. 高評価のパターンをElasticsearchに索引化
  3. タスクコンテキストでベクトル検索
  4. 類似パターンをSystem Promptに注入

学習曲線

  • 初回実行: 平均2-3回のvalidation失敗を経て成功
  • Knowledge化後: 同様のタスクはほぼ一発で成功
  • 運用数ヶ月: 頻出タスクの初回成功率が向上

この仕組みにより,エージェントは使うほど賢くなり, 懸念される可能性のあるJSON生成の不安定性も実運用では問題になりません.

┌─────────────────────────────────────────┐
│         初回実行 (Cold Start)            │
├─────────────────────────────────────────┤
│ LLM生成 → ❌ → 修正 → ❌ → 修正 → ✅     │
│              (平均2-3回)                 │
└─────────────────────────────────────────┘
                    ↓
         ┌──────────────────┐
         │  評価 + 索引化    │
         └──────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│      2回目以降 (Warm Start)              │
├─────────────────────────────────────────┤
│ Knowledge検索 → LLM生成 → ✅            │
│              (ほぼ一発)                  │
└─────────────────────────────────────────┘

5 高階関数で表現していることのメリット

どのアプローチも,「処理定義(関数)を引数に取り,新しい複合処理(関数)を返す」高階関数として設計されています. これによってエージェントが逐次実行した場合と比較してコンテキストエンジニアリングの文脈で大きな強みがあります. それ以外にも以下のようなメリットがありました.

  • 宣言と実行の分離:エージェントは JSON 宣言を返すだけで,実動作に費やすトークンを削減.実行側は常に同じ検証ルールで評価するため,一貫性を確保.
  • 合成の安全性:chain tool は バリデーションや入出力の取り扱いを内部に閉じ込めているので,関数合成(ツール連鎖)時の型崩れや引数抜けをチェーン内で吸収可能
  • 秘匿情報の安全性:秘匿情報の受け渡しをchain内に閉じ込めることでLLMに対する入出力にそれらの情報が顕在化しない.
  • 知識の再利用:高階関数ツールへのInputとその結果をロギング -> LLM as a Judgeなどで評価 -> Knowledge として保存することで,同じ宣言を別エージェントが再評価したり,成果の出た構成をテンプレ化したりできる.
  • プロセスの階層化:テンプレ化したchain toolをchain toolで利用することで複雑なパターンを容易に表現
  • スケーリング: 並列実行可能な箇所が明確かつ処理機構は閉じているため,スケール時にコントロールが容易

6 今後の展望

  • Python サンドボックス応用:既存の sandbox を使って,chain tool が動的な Python タスクを compile/decompile できるモジュールを挟み,宣言の一部をコード化 → 検証 → 再宣言するハイブリッド実行を検討中.
  • コード実行との連携:Anthropic が提唱するような MCP ベースのコード実行環境とも親和性が高く,彼らが提唱するsearch_toolsと組み合わせることによってより効率的で安全な状態に拡張できる可能性が高い.
  • healerの構築, playwright-healerのように出力されたtool inputに対してより適切な構造に自律的に修正を促すエージェントとの協調

tech-blog.localmet.com www.anthropic.com

終わりに

スパイスコードでは現在積極的に採用を行なっています. この記事を読んでAIを使ったチャレンジングな機能を開発してみたいと思った方,興味を持った方はぜひお話ししましょう!

corp.spicescode.co.jp

引用

arxiv.org blog.langchain.com www.anthropic.com