bunbun

Vercel の eve と localLLM で agent を作ってみた

2026-08-07

はじめに

こんにちは!前回私が執筆した AI Engineer World's Fair に関する記事 の中で、AI agents を自社で作るというのが当たり前だという風に申し上げましたが、その文脈の中で Vercel 製の AI agent 作成フレームワークである eve を紹介しました。 今回はこの記事の中で実際に eve で簡易的な AI agent をどういうわけか localLLM で作ってみようと思います。

が、その前に上記の記事を少しおさらいしていこうと思います。

なんで AI agent を自社で作るの?

Claude Code/Desktop や Codex などの Agent ツールがある中で、なぜ自社で AI agent を作る必要があるのか?という疑問があると思います。 私がサンフランシスコで聞いたところや調べた範囲だと、以下のような理由があるようです。

  • サードパーティの AI agent が持っていない機能を自社で作りたい
  • token 使用量が予算を圧迫している
  • ビジネスコンテクストをよく理解できるようにハーネスエンジニアリングしたい

このようなビジネスニーズがあるため、AI agent を自製するのが当たり前になりつつあります。

eve って?なんで eve なの?

繰り返しになりますが、eve は Vercel の AI agent 作成用のフレームワークです。 ファイルシステムベースかつ TypeScript で AI agent を作ることができるので、Next.js のアプリを作るような感覚で Agent 開発をすることができる、というのが eve の特徴です。

機能はたくさん詰まっていて、subagents, skills, tools, schedules, evals など日頃 Agent を使う人も耳馴染みのある機能を追加できるので doc を読む限りだとスケーラビリティもありそうですね。

またビルトインの tools も充実していて、web 検索・web ページの取得、ファイルの読み書き、sandbox 内での bash コマンドの実行などもできます。

Next.js アプリの開発を日常的に行なっている私にとってはとても馴染みやすいフレームワークに見えるのでこれで Agent を作ってみようと思います。 ただ、doc を読む限りだと、eve は Vercel の AI Gateway 経由で OpenAI などのサードパーティのモデルを使用することを前提に作られているように見えます。

今回はこの eve で localLLM ベースの agent も問題なく開発できるかを焦点に検証していきます。

なんで localLLM なの?

今回のブログのテーマの一つになっている localLLM ですが、これは読んで字の如くローカルで動作する LLM のことです。 Agent を作る際に localLLM を使用するのは、やはりコスト面の側面も大きいですが、いうまでもなくセキュリティ面でも大きな利点があります。 localLLM を使用することで、社内の機密情報を外部に送信することなく Agent を作ることができる、というのはかなりの魅力ではないでしょうか。

※注: もちろん prompt injection 攻撃のリスクや、怪しい localLLM をダウンロードすることでマルウェア侵入のリスクはありますので絶対安心、というわけではありません!

一般的な PC で localLLM を使用することはまだまだハードルが高いイメージがあると思いますが、最近では localLLM の進化も著しく、高性能かつ軽量なモデルが続々と登場しています。 特に Mac では Apple Silicon が unified memory を搭載していることもあり、GPU に投資をしなくても localLLM を使用できる環境が整いつつあります。

どの LLM Model を使うの?

localLLM の選定はこのページで行いました。

https://www.canirun.ai

今回のモデル選定で大事なのは以下です

  • tool を使うことができる
  • thinking model である
  • 軽量かつ高性能

今回は私の PC でも問題なく走る Gemma 4 E2B の軽量化モデルを使用します。

https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf

localLLM のランナー

localLLM を走らせるには、モデル本体(今回だと Gemma 4 の GGUF ファイル)をロードして推論を実行してくれるソフトウェア、いわゆる「ランナー」が必要です。 ランナーの多くは OpenAI 互換の API サーバーとしてモデルを公開してくれるので、アプリケーション側からは普通の LLM の API と同じ感覚で扱うことができます。

localLLM のランナーはいくつかあります。例えば

  • Ollama
  • llama.cpp
  • LM Studio

などですね。 そんな中でも llama.cpp は

  • コンテクストのサイズが調整できる
  • セットアップがコードで明記できる
  • モデルのカバー率が高い
  • 個人的に早くモデルが走ってくれる

ということもあり、今回はこの llama.cpp を使用していきます。 llama.cpp の詳しい説明は以下のリンクを参照してください。

https://github.com/ggml-org/llama.cpp

Let's get started!

前提は以下です。

  • PC: MacBook Pro M4 16GB
    • 実際 M1 でも大丈夫です。unified memory であれば、一番大事なのはメモリ容量です。
    • 16GB だと快適に走ってくれる優秀なモデルがあまり多くないのが現実ですが、今回のモデルはこれでも十分に動作します。
  • localLLM の runner: llama.cpp
  • localLLM のモデル: Gemma 4 E2B

llama.cpp がローカルに存在しない場合は以下でインストール。これがないと localLLM を走らせることができません。

brew install llama.cpp

以下で eve の project を作成。

npx eve@latest init [お好きなプロジェクト名] --channel-web-nextjs
  • --channel-web-nextjs について
    • --channel-web-nextjs をつけると、Next.js ベースの Web UI が自動で作成されます。これは optional ですが、今回はチュートリアルの目的としてウェブ UI もつけようと思います。
    • ただ、eve ではデフォルトで Agent と会話するための CLI を立ち上げることもできるので、ウェブ UI はなくても大丈夫です。

すると以下のように質問されますが、ctrl + c で終了してしまって大丈夫です。今回は質問に答える必要はありません。

✓ Created an eve agent in /path/to/my-agent in 37ms
✓ Installed dependencies in 53.9s
│
△  How would you like to continue?
│   ▶ Start eve dev    · talk to 'my-agent' in your terminal
│     Open Claude Code
  • Start eve dev を選ぶと eve の CLI が立ち上がり、そこで Agent と会話をすることができます。が、それは後で AI model を設定してからやります。
  • Open Claude Code などの選択肢を選ぶと既存の Coding Agent を使って開発を進められますが、今回は練習も兼ねて Coding Agent は使わずに進めていきます。

それでは initialize したプロジェクトのディレクトリに移動します。

cd [先ほど設定したプロジェクト名]

ではディレクトリ構造を見てみましょう。

[プロジェクト名]/
├── package.json
├── app/ (Next.jsのWeb UI部分。)
│   └── page.tsx etc...
└── agent/
    ├── agent.ts
    ├── instructions.md
    └── channels/
        └── eve.ts

まず、app ディレクトリは Next.js の Web UI 部分ですのでこれは省略します。 大事なのは agent ディレクトリです。ここに AI agent の設定ファイルが入っています。 例えば、channels ディレクトリで Agent と会話するための窓口(Web チャットのほか、Slack や Discord などの外部サービスとの接続先)を設定できたり、instructions.md で Agent の振る舞いを設定できたりします。

今回重要になってくるのが、agent.ts です。ここで AI モデルの設定を行います。

今回は localLLM の適用性を確認したいのでこのファイルをいじっていきます。

Let's set up localLLM!

eve は基本サードパーティの AI モデルを使用することを前提に作られているため、localLLM を使用するためには少し工夫が必要です。

以下の2つのポイントが重要でした。

  1. llama.cpp のサーバーが OpenAI 互換の API を公開してくれるので、eve のモデル設定を OpenAI 互換にすること
  2. どこのサーバーでモデルが走っているかを設定すること

これを前提に進めていきます。

以下で gemma 4 をモデルとして設定するのに必要なパッケージを取得。

npm install @ai-sdk/openai-compatible

@ai-sdk/openai-compatible は Vercel の AI SDK 用の provider パッケージで、OpenAI 互換の API を公開している任意のサーバーを AI SDK のモデルとして扱えるようにしてくれます。llama.cpp のサーバーは OpenAI 互換の API を公開してくれるので、このパッケージを経由することで eve から localLLM を普通のモデルと同じように呼び出せるようになります。

次に、agent/agent.ts を以下のように定義します。

import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
import { defineAgent } from 'eve'

const local = createOpenAICompatible({
  name: 'local',
  baseURL: process.env.MODEL_BASE_URL ?? 'http://localhost:8081/v1',
})

export default defineAgent({
  model: local(process.env.MODEL ?? 'google/gemma-4-E2B-it-qat-q4_0-gguf'),
  modelContextWindowTokens: 32768,
})

設定内容を軽く解説すると、

  • createOpenAICompatible で llama.cpp のサーバーを provider として定義しています。baseURL には llama.cpp のサーバーの URL(後述しますが、今回は port 8081 で走らせます)を指定します。
  • local(...) でモデル名を指定します。ここは llama-server の起動時に指定するモデル名と合わせておきます。
  • modelContextWindowTokens でモデルのコンテクストウィンドウのサイズを eve に伝えます。eve はこの値をもとにコンテクストの管理をしてくれます。

最後に、package.json の scripts に以下を追加します。

{
  ...,
  "scripts": {
    ...,
    "model:serve": "llama-server -hf google/gemma-4-E2B-it-qat-q4_0-gguf -c 32768 -ngl 99 --port 8081",
    ...,
  },
  ...
}

ここでコマンドの解説です。

  • llama-server: llama.cpp のサーバーを立ち上げるコマンド。同時にモデルのダウンロードも行ってくれる。今回は -hf をつけているので、Hugging Face にあるモデルをダウンロードしてくれます。
  • --port 8081: localLLM 用のサーバーは 8081 で走らせる
  • -c 32768: コンテクストウィンドウのサイズを 32k に設定
    • ここの数を増やせば増やすほど、より長いコンテクストを保持できるようになりますが、メモリの消費量も増えるので注意が必要です。
  • -ngl 99: モデルの処理をどれだけ GPU に任せるかの指定(--n-gpu-layers の略)。99 のような大きい値を入れるとモデル全体が GPU で処理され、動作が速くなります。Apple Silicon なら GPU が自動で使われるので、基本はこのままで ok です
  • 他に、以下のような設定用の引数を入れても ok です
    • --temp 0.5 --top-p 0.9 --top-k 20
      • これらはモデルの出力のランダム性を制御するサンプリングパラメータです。
        • --temp は低いほど出力が決定的・堅実になります
        • --top-p--top-k は次のトークンの候補をどこまで絞るかを指定します。
          • tool calling のような正確さが求められる用途では、低めに設定して出力を安定させるのがおすすめです

これで、準備は整いました。では実際に走らせてみましょう。

Let's run!

まず model を走らせてみましょう。

npm run model:serve

初期起動時だと、このコマンドでモデルのダウンロードまで行ってくれるので、初回は少し時間がかかります(モデルは約 3.3GB あるので、ネットワーク環境によりますが高速な Wi-Fi があっても最低5分はかかりそう)。 なお、モデルはデフォルトで ~/.cache/huggingface/hub/ 内にダウンロードされます。

次に eve のサーバーとチャットのクライアント画面を起動させてみます。

npm run dev

すると eve にビルトインで用意されている web app が localhost:3000 で開くのでそこでチャットをしてみましょう。

※注: Next.js の dev サーバーはデフォルトで LAN 内の他の端末からもアクセスできる状態で待ち受けます。今回の Agent は bash やファイル操作の tool を持っていて認証もないので、カフェやオフィスの共有 Wi-Fi など信頼できないネットワークで試す場合は、next dev -H 127.0.0.1 のように自分の PC からのみアクセスできる設定で起動するのが安全です。

> こんにちは
> // The user said "こんにちは" (Konnichiwa), which means "Hello" in Japanese. This is a simple greeting. I should respond in a friendly and appropriate manner, acknowledging the language used.

// Analyze the request: The input is a simple greeting in Japanese.
// Determine the appropriate response: A reciprocal greeting is suitable.
// Tool check: No tools are needed. This is a direct conversational response.
// Formulate the response: Respond in Japanese or a mix of Japanese and English, depending on the context, but a simple "Hello" or "Konnichiwa" followed by an offer to help is standard.
// Response plan: Respond with a friendly greeting, perhaps asking how I can help.

こんにちは!私はGoogle DeepMindによって開発された大規模言語モデルです。

何かお手伝いできることはありますか?

ちゃんと回答してくれましたね!localLLM でもちゃんと動作していることが確認できました。

さすが Thinking model なので「こんにちは」と入力しただけでも思考をして回答してくれますね。

ただ、これだけだとまだ単調な会話しかできません。試しに現在時刻を聞いてみましょう。

> 今何時ですか
> 申し訳ありませんが、私はリアルタイムの正確なシステム時刻を直接取得するツールを持っていないため、現在時刻を正確にお伝えすることができません。

お手数ですが、お使いのデバイスの時計をご確認ください。

このように初期状態だとこういった基本的なこともできないようです。しかし、tool を追加することで Agent の機能を拡張することができます。

Let's add a tool!

tool を簡単に説明すると、Agent が会話の中で必要に応じて呼び出すことができる機能です。これを独自に定義することができるので、Agent の機能を拡張することができます。

先ほど確認したディレクトリ構造を以下のように変更することで、tool を追加することができます。

[プロジェクト名]/
├── package.json
...
└── agent/
    ...
    ├── tools/
    │   └── [tool_名].ts
    ...

agent/tools/get_current_time.ts に以下のように現在時刻を取得する tool を追加してみましょう。今回は try catch などなしでシンプルに書きます。

import { defineTool } from 'eve/tools'
import { z } from 'zod'

export default defineTool({
  description:
    'Get the current date and time in a given IANA time zone. Defaults to Asia/Tokyo.',
  inputSchema: z.object({
    timezone: z
      .string()
      .optional()
      .describe("IANA time zone name, e.g. 'Asia/Tokyo'."),
  }),
  execute({ timezone }) {
    const tz = timezone ?? 'Asia/Tokyo'
    return {
      timezone: tz,
      datetime: new Date().toLocaleString('en-US', { timeZone: tz }),
    }
  },
})

なお、不正なタイムゾーン名を渡されると RangeError になるので、実際に使う場合は try catch を足してください。

ではいざ tool がうまく動作するかを確認してみましょう。

> 今何時ですか
> Thinking Process:

Analyze the Request: The user is asking "今何時ですか" (What time is it now?).
Analyze Available Tools: I have several tools: ask_question, bash, glob, grep, read_file, write_file, web_fetch, load_skill, get_current_time, and agent.
Identify the Relevant Tool: The get_current_time tool is designed to get the current date and time in a given IANA time zone.
Determine Parameters: The tool get_current_time takes an optional timezone argument. Since the user's request is in Japanese and doesn't specify a timezone, I can either use the default (Asia/Tokyo) or ask the user for their preferred timezone.
Review Previous Attempt: In the previous turn, I tried bash and it returned an error message unrelated to time, suggesting the bash tool might not be the correct way to get system time or that the environment is restricted.
Formulate the Action: Use the get_current_time tool to get the current time, specifying a reasonable default or acknowledging the need for a timezone.
Tool Call Selection: I will call get_current_time without a specific timezone first, relying on its default behavior (Asia/Tokyo).
Self-Correction/Refinement: Wait, the previous turn did call bash and got an error. If bash is unreliable or failing, I should prioritize the dedicated time tool, get_current_time. I will use that now.

Final Tool Call Construction: Call get_current_time().

get_current_time
Completed
Parameters
{}
Result
{
  "timezone": "Asia/Tokyo",
  "datetime": "8/5/2026, 10:37:11 PM"
}

Thought for 2 seconds

> 現在、日本標準時(Asia/Tokyo)では、2026年8月5日の午後10時37分11秒です。

しっかりと tool を呼び出してくれましたね! このようにファイルを一つ追加するだけで Agent の機能を拡張することができました。なんだか Agent がどんどん育っていく感じがします。

こんな感じで、tool を追加していくことで Agent の機能を拡張していくことができます。 他に、天気の取得や社内 DB の検索、Slack への通知などの tool を追加していくのも楽しいですね。

正直な感想

tool を拡充させていくことで、より複雑なタスクをこなせるようになっていくのが見てて楽しいですね。 モデルの賢さも大事ですが、それと同様のハーネスエンジニアリングの重要性にも気づかされます。 実際に Agent を作ってみると、普段使っている AI agent のアーキテクチャーに対する理解も深まるため、AI agent を日常的に使う方は勉強のためにも Agent を作ってみるのはおすすめです。

他にいい点を挙げるとすると

  • 開発も直観的にできるため、Next.js の開発に慣れている方はすぐに Agent を作ることができる
  • ビルトインのツールが充実している(web_fetchread_file/write_filebashgrep など)し、それが気に入らなければ書き換えることもできる

一方でそもそも eve は軽量の localLLM を使うことを前提に作られているわけではないので、軽量モデルを使う上で重要である RAG のような仕組みがまだ整っていないのを感じました。

また、ビルトインのウェブサーチ機能も便利ではあるものの、軽量モデルだと処理できないような大量のデータをそのままモデルに流してしまうためモデルからの返答が止まる、などの問題もありました。

これを解決するために

  • web_search 機能の書き換えをして、検索結果をコード側で trim したり、chunk に分割して embedding で質問に関連する部分だけを取り出してモデルに渡す(いわゆる RAG 的なアプローチ)、のような仕組みを自前で作る
  • SERP 用の API を使用する

などの工夫が必要となります。

他の課題もあります。例えば

  • 結局複数のアプリを開きながら Agent を走らせると PC のメモリが圧迫されて動かなくなる
    • これは eve の問題というよりも、localLLM のモデルの問題ですが...
  • 抽象化がされすぎていて、Agent の挙動がブラックボックス化してしまう
  • 他の Agent 開発フレームワークである mastra が提供するような AI の token 使用量などをローカルでも監視できる機能がまだない

ただ、gemma 4 がまあまあ賢いため、最適化された web 検索機能があると調査系のタスクではかなり頼りになります。

さいごに

まだまだ課題はありますが、今後の eve のアップデートや localLLM の進化により、より便利で使いやすい localLLM Agent が作れるようになるのではないでしょうか? これからが楽しみですね!

No table of contents available for this content