🤖

何もわからんけど作ってみる、なんちゃってAIエージェント

に公開

みなさんこんにちは!株式会社アルダグラムでエンジニアをしている大木です。
今回は何もわからんけどAIエージェントを作っていこうと思います。せっかくなので、自社サービスの劣化版を作りつつAIエージェントを実装していきます。

1. システムの構成

今回は主に3つのシステムを利用して、AIエージェントを作成します。

概要 使用する技術 SDK等
フロントエンド Next.js AI SDK
AIエージェント Python OpenAI Agents SDK / FastAPI
MCP Go mcp-go

図に表すとこんな感じです(雑ですみません)

2. 作るもの

自社サービスの劣化版を作りつつ、そこにAIエージェントを搭載する体で実装していこうかと思います。

弊社のサービスについて

弊社は「KANNA」という建築や製造、不動産などの現場で働く方をターゲットにしたサービスを提供しています。メインとしては「プロジェクト管理アプリ」「デジタル帳票アプリ」という大きく2軸の機能で展開しています。
全体の流れとして、「案件」を作成し、そこに写真や資料を格納します。その案件ごとにチャットを作成することもできます。
「案件テンプレート」と呼ばれる案件の雛形を事前にカスタマイズし、プリセットを作る機能があります。
https://lp.kanna4u.com/

今回作るもの

AIエージェントを介して、案件の操作をできるように実装をしていこうかと思います。以下のような操作が必要になるかと思います。

  • 案件の作成
    • 案件テンプレート/案件名/説明 の指定が必須となる
  • 案件の一覧
  • 案件の説明の編集
    • 案件のID/説明の指定が必須
  • 案件の削除
  • 案件テンプレートの一覧

できたもの

先んじて、完成したものを記載していこうかと思います。
以下のリポジトリに全てのソースコードが含まれています。よろしければ参考になさってください。
https://github.com/Kazuya-Oki/degraded-version-kanna/tree/main

画像になってしまいますが、フロントエンドも含めてAIエージェントっぽいものが作成できました。

では実装の方を見ていきましょう。

3. 実装していく

3-1. MCPサーバーの実装

前回実装したものがあるので、具体的な記載は割愛します。
詳しく実装を見たい場合は、以下を参考にしてみてください。
https://github.com/Kazuya-Oki/degraded-version-kanna/tree/main/backend/mcp

package main

import (
    "context"
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

func main() {

    s := server.NewMCPServer(
        "Degraded Version KANNA",
        "1.0.0",
        server.WithResourceCapabilities(true, true),
        server.WithLogging(),
    )

    toolsForProjects := mcp.NewTool("manageProjects",
        mcp.WithDescription("Perform project management operations"),
        mcp.WithString("operation",
            mcp.Required(),
            mcp.Description("Project management operations"),
            mcp.Enum("create", "list", "edit", "delete"),
        ),
        mcp.WithString("projectID",
            mcp.Description("Project ID is required for edit and delete operations"),
        ),
        mcp.WithString("name",
            mcp.Description("Project name is required for create operation"),
        ),
        mcp.WithString("description",
            mcp.Description("Project description"),
        ),
        mcp.WithString("projectTemplateID",
            mcp.Description("Project template ID is required for create operation"),
        ),
    )

    // Tool for project templates management
    toolsForProjectTemplates := mcp.NewTool("manageProjectTemplates",
        mcp.WithDescription("Perform project template management operations"),
        mcp.WithString("operation",
            mcp.Required(),
            mcp.Description("Project template management operations"),
            mcp.Enum("list"),
        ),
    )

    s.AddTool(toolsForProjects, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        op := request.Params.Arguments["operation"].(string)

        var res string
        switch op {
        case "create":
            name := request.Params.Arguments["name"].(string)
            description := request.Params.Arguments["description"].(string)
            projectTemplateID := request.Params.Arguments["projectTemplateID"].(string)
            res = createProject(name, description, projectTemplateID)
        case "list":
            res = listProjects()
        case "edit":
            projectID := request.Params.Arguments["projectID"].(string)
            description := request.Params.Arguments["description"].(string)
            res = editProject(projectID, description)
        case "delete":
            projectID := request.Params.Arguments["projectID"].(string)
            res = deleteProject(projectID)
        }
        return mcp.NewToolResultText(res), nil
    })

    s.AddTool(toolsForProjectTemplates, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
        op := request.Params.Arguments["operation"].(string)

        var res string
        switch op {
        case "list":
            res = listProjectTemplates()
        }
        return mcp.NewToolResultText(res), nil
    })

    if err := server.ServeStdio(s); err != nil {
        //fmt.Printf("Server error: %v\n", err)
    }
}

3-2. エージェントの作成

OpenAI Agents SDK を利用したエージェントを作成していきます。今回作成したものは、このSDKの1割も使いこなせていないです。もう少し学んでいく必要がありますね…
今回こちらを利用した理由として、MCPの対応があったことが挙げられます。他サービスのAPIを見たところ、執筆時点では対応されていなかったように見受けられました。MCPを絶対使わないといけないものでもないですが、学習の一貫としてという感じです。

今回のコアとなるのは、やはりこの Agent と呼ばれるクラスです。様々なオプションや機能を利用しながら、この Agent を実装していくことになります。
https://openai.github.io/openai-agents-python/agents/
ユーザーからの入力をチェックしたり、バリデーションをする役割のある Guardrails や複数のエージェントを束ねる際に利用する Handoffs の機能などがありますが、今回こちらは利用しないためご容赦ください。
https://github.com/Kazuya-Oki/degraded-version-kanna/tree/main/backend/agent

今回はGoで作成したローカルにあるMCPを利用するため、 MCPServerStdio を利用します。詳しくは
https://github.com/openai/openai-agents-python/blob/main/src/agents/mcp/server.py
を参考にしてみていただければと思います。commandには、Goで作成したMCPのmainへのパスを指定します。

agent = Agent(
    name="Assistant",
    # システムのプロンプトとなるもの
    instructions=(
        "\n".join(instruction)
    ),
    model="gpt-4o",
    mcp_servers=[server]
)

...
async with MCPServerStdio(
    name="Project Manager",
    params={
        "command": "go build した成果物へのPATH",
    },
) as server:
    trace_id = gen_trace_id()
    with trace(workflow_name="SSE Example", trace_id=trace_id):
        return await run_server(server, message, instructions)

フロントエンドからリクエストは FastAPI を利用して受け取るようにしました。
正直なところ、Pythonに関しては自分の知見がほとんどなくイケてないところも多くあるかと思いますがご容赦ください…
リクエストには、 idとmessages が含まれています。後述する AI SDK では、標準でリクエストにチャットの履歴が含まれるようになっているため、それをそのままプロンプトに渡してあげるような形です。
実際にAPIを実行する際には、OpenAIのAPIキーを環境変数に設定する必要があるためご注意ください。

import random

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from typing import List
from pydantic import BaseModel
from agent import call_agent

app = FastAPI()
origins = ["*"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class Part(BaseModel):
    type: str
    text: str

class Message(BaseModel):
    role: str
    content: str
    parts: List[Part]

class MessageRequest(BaseModel):
    id: str
    messages: List[Message]

@app.post("/")
async def run_root(req: MessageRequest):
    last_message = req.messages[-1]
    instructions = req.messages[:-1]
    instruction_messages = list(map(lambda x: x.content, instructions))
    response = await call_agent(last_message.content, instruction_messages)
    return { "id": str(random.randint(10, 10000)), "message": response }

3-3. チャット形式の画面実装

フロントエンドの実装には、 Vercel が提供する AI SDK を利用します。 Next.js のプロジェクトを作成して、SDKをインストールしていきます。
https://sdk.vercel.ai/docs/getting-started/nextjs-app-router

npx create-next-app@latest
npm install ai @ai-sdk/react @ai-sdk/openai zod

実装した結果はこちらからどうぞ!
https://github.com/Kazuya-Oki/degraded-version-kanna/tree/main/frontend

チャット形式での画面実装においては useChat というhooksが公式で提供されているため、それを利用しました。
今回のようなシンプルなものを作成する分には、このhooksを思考停止で利用すれば良さそうという安直な考えを元に利用しています。

const {
  messages,
  input,
  status,
  handleInputChange,
  handleSubmit,
  setMessages,
} = useChat({
  // FastAPIを利用していれば、このエンドポイントを指定します
  api: "http://localhost:8000/",
  initialMessages: [],
  onResponse: async (response) => {
    const body = await response.json();
    setMessages((messages) => {
      return [
        ...messages,
        {
          id: body.id,
          content: body.message,
          role: "assistant",
        },
      ];
    });
  },
});
  • status
    • API実行のステータスです。「送信中…」のような表示をするために利用します
  • setMessages
    • UI上で表示するメッセージを更新するために利用します。こちらは実行時にAPIを実行しません。
    • 似たようなものに append がありますが、こちらは実行時にAPIを実行します。 onResponse などのコールバック内で利用すると、無限ループに陥るため注意しましょう。

などなど、チャットアプリを作成するうえでは必要になりそうなものが申し分なく提供されています。
UIなどは全てAIに作成してもらって、デザインを実装しました。

4. 完成!

実際に画面を実装しつつ、Agents SDKを利用しながらAIエージェントアプリもどきを作成してみました。エージェントというにはまだまだですが、なんとなくの勘所は掴めたのではないかと思います。

OpenAIのプラットフォームの画面から、実際にログを確認することもできます。実際に、テンプレートID: 4のソフトウェア開発用テンプレートが選択されていたことが確認できました。

manageProjects({
  "operation": "create",
  "name": "タスク管理アプリ開発プロジェクト",
  "description": "新しいタスク管理アプリの開発を行うプロジェクト",
  "projectTemplateID": "4"
})

5. まとめ

ここまでご覧いただきありがとうございました。MCPサーバーの実装から少し発展して、なんちゃってAIエージェントを実装してみました。
「実際にAIをプロダクトに組み込むのってハードル高いよなぁ…」と感じていたのが正直なところでしたが、これらのSDKの登場によってガッとハードルが下がったように思えます。開発をされている方に感謝しかありません。
一方でプロダクトに組み込むには、もちろん検討せねばならないことも多いかと思いました。

エラーハンドリングなんかもそうですし、 先述した Guardrails なども必須に近いのではと思います。LLMのモデル選定やら、検証やら、1人で作成するなんちゃってプロジェクトではまだ考慮しないといけない点がありますね。
最も大事なのは、サービスでのユースケースを把握してどのように利用していただくかという点だと思います。
そういったところを踏まえて、一筋縄ではいかないのが面白いところなのではと思います。

余談ですが今回実装をしてみて、改めてAIによる開発が当たり前になってきたなぁと感じました。バックエンドの実装も、フロントエンドの実装もほぼノータッチで行えました。恐ろしいの一言ですね。
今回添付したコード内には、もしかしたら参考と呼べないものあるかもしれませんが温かい気持ちでコメントをしていただけますと大変励みになります。
AIに仕事を奪われないよう、今後も精進していく必要があるなと感じました。

アルダグラム Tech Blog

Discussion