1. 从零到一:为什么选择火山引擎+Supabase+IGA Pages这套组合?

最近在折腾AI Agent应用,从原型验证到最终上线,最头疼的往往不是模型调优,而是那一堆繁琐的部署和运维工作。服务器要买、数据库要配、前端要部署、域名要解析、HTTPS要配置……一套流程下来,精力被消耗大半,真正花在Agent逻辑上的时间反而没多少。

我一直在寻找一种能让我“一句话部署”的解决方案。这里的“一句话”不是魔法,而是一种极致的抽象:开发者只需要关心核心的AI Agent业务逻辑,而将服务器、数据库、身份认证、对象存储、前端托管等所有基础设施的复杂度,全部交给可靠、免运维的云服务去处理。经过多次实践和对比,我最终锁定了“火山引擎 + Supabase + IGA Pages”这套技术栈。它完美契合了AI Agent应用快速迭代、全栈托管、成本可控的需求。

简单拆解一下这三个核心组件各自扮演的角色:

  • 火山引擎 :这里主要指的是其云服务器(ECS)和对象存储(TOS)服务。ECS为我们运行AI Agent的后端核心逻辑(比如基于LangChain、LlamaIndex的链或智能体)提供了稳定、可弹性伸缩的计算环境。而TOS则用于存储Agent可能需要的知识库文件、用户上传的文档、生成的图片或音频等非结构化数据。火山引擎的稳定性和在国内的访问速度是重要考量。
  • Supabase :这是一个开源的Firebase替代品,但它远不止于此。对于AI Agent应用,Supabase提供了几个开箱即用的杀手级功能:1) PostgreSQL数据库 :用于存储用户会话、Agent执行历史、结构化知识等。2) 实时订阅 :可以实现Agent执行状态、结果的实时推送到前端,对于构建交互式应用至关重要。3) 身份认证 :内置了邮箱/密码、OAuth(如GitHub, Google)等全套Auth系统,省去自己搭建用户体系的麻烦。4) 边缘函数 :虽然本篇以ECS为主,但Supabase Edge Functions可以作为轻量级、事件驱动的后端逻辑补充。
  • IGA Pages :这是一个静态网站托管服务。我们的AI Agent前端(比如用Vue、React或Next.js构建的交互界面)可以打包成静态文件,直接部署在IGA Pages上。它自动提供全球CDN加速、HTTPS证书,并且通常与Git仓库集成,实现提交代码自动部署。这意味着前端发布就像推送代码一样简单。

这套组合的精髓在于 “关注点分离” “全栈托管” 。你的核心AI逻辑跑在火山引擎的云服务器上,数据和服务由Supabase管理,用户界面由IGA Pages全球分发。每一层都是专业、免运维的,你只需要用代码将它们“粘合”起来。接下来,我就手把手带你走通从环境准备到一键部署的完整流程。

2. 环境准备与项目骨架搭建

在开始写第一行Agent代码之前,我们需要把“舞台”搭好。这个阶段的目标是创建好所有必要的云资源,并在本地初始化一个结构清晰的项目。

2.1 云资源创建与配置

首先,我们需要在三个平台上完成初始设置。

1. 火山引擎控制台 登录火山引擎控制台,进入云服务器ECS产品页面。

  • 创建ECS实例 :选择离你的目标用户近的地域(如华北2-北京)。对于AI Agent初期,选择通用计算型(如ecs.g1.large)通常够用,具体配置需根据你的模型负载调整。关键点在于 镜像选择 :强烈推荐选择预装了Docker的公共镜像(如Ubuntu 20.04 with Docker),这能极大简化后续环境部署。安全组需要放行你后端服务的端口(例如,我们后续用到的FastAPI服务默认在 8000 端口)。
  • 创建存储桶(TOS) :进入对象存储TOS控制台,创建一个新的存储桶(Bucket)。记住其名称和地域。在权限管理(ACL)中,建议先设置为“私有读写”,后续通过预签名URL或服务端代理方式访问,确保数据安全。记录下 Endpoint (访问域名)。

2. Supabase项目创建 访问Supabase官网并注册登录。

  • 新建项目 :点击“New Project”,输入项目名称,设置数据库密码(务必保存好)。选择离你ECS实例近的地域(例如,AWS ap-northeast-1 对应东京),以减少网络延迟。免费计划对于初期项目完全足够。
  • 获取连接信息 :项目创建完成后,进入项目设置(Settings -> API)。这里你会找到几个关键信息:
    • Project URL :你的Supabase项目地址,格式如 https://xxxxx.supabase.co
    • anon/public key :用于前端或公开客户端安全调用Supabase API的密钥。
    • service_role key 超级密钥,仅用于后端或可信环境 ,拥有最高权限,切勿泄露。
    • Database Connection String :数据库直接连接字符串,格式为 postgresql://postgres:[YOUR-PASSWORD]@db.xxxxx.supabase.co:5432/postgres

3. IGA Pages服务准备 以Vercel为例(其他如Netlify、Cloudflare Pages同理)。

  • 关联Git仓库 :在Vercel控制台,点击“Add New” -> “Project”,导入你的GitHub/GitLab仓库。这要求你的前端代码已经存放在Git仓库中。
  • 环境变量配置 :在项目设置的“Environment Variables”中,添加前端需要使用的环境变量,例如 VITE_SUPABASE_URL VITE_SUPABASE_ANON_KEY ,值就是从Supabase控制台获取的那两个。这样前端构建时就能安全地注入这些配置。

2.2 本地项目初始化与结构设计

在本地开发环境,我们创建一个标准的全栈项目目录。清晰的目录结构是后续高效开发和部署的基础。

my-ai-agent/
├── backend/          # AI Agent后端核心 (运行于火山引擎ECS)
│   ├── app/
│   │   ├── main.py           # FastAPI应用主入口
│   │   ├── agents/           # 具体的Agent逻辑模块
│   │   ├── core/             # 核心配置、工具类
│   │   └── services/         # 业务服务层,如调用Supabase、TOS
│   ├── requirements.txt      # Python依赖
│   ├── Dockerfile            # Docker镜像构建文件
│   └── docker-compose.yml    # (可选)本地开发环境编排
├── frontend/         # 前端应用 (部署于IGA Pages)
│   ├── src/
│   │   ├── main.jsx          # 应用入口
│   │   ├── App.jsx           # 主组件
│   │   ├── lib/
│   │   │   └── supabase.js   # Supabase客户端初始化
│   │   └── components/       # React/Vue组件
│   ├── package.json
│   ├── vite.config.js        # 或 next.config.js
│   └── .env.local            # 本地环境变量(.gitignore)
└── scripts/          # 部署脚本
    └── deploy.sh             # 一键部署脚本

后端核心依赖 ( backend/requirements.txt ) 示例:

fastapi==0.104.1
uvicorn[standard]==0.24.0
supabase==2.3.1
langchain==0.0.340
openai==1.3.0  # 或其他LLM SDK
psycopg2-binary==2.9.9  # PostgreSQL驱动
python-multipart==0.0.6  # 文件上传
boto3==1.34.0  # 用于访问火山引擎TOS (AWS S3兼容接口)

前端环境变量 ( frontend/.env.local ) 示例:

VITE_SUPABASE_URL=https://xxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-here

这个结构将前后端完全分离,通过API进行通信,符合现代Web应用的最佳实践,也为独立部署奠定了基础。

3. AI Agent后端核心:FastAPI与Supabase深度集成

后端是整个AI Agent的大脑,它负责接收前端请求,编排LLM调用、工具使用(Tool Calling)、记忆(Memory)以及和数据库的交互。我们使用FastAPI作为Web框架,因为它异步性能好、自动生成API文档,非常适合AI应用。

3.1 构建FastAPI应用与Supabase客户端

首先,在后端项目中建立与Supabase的连接。我们创建一个配置文件和一个服务类来集中管理。

backend/app/core/config.py

from pydantic_settings import BaseSettings
import os

class Settings(BaseSettings):
    # Supabase 配置
    supabase_url: str = os.getenv("SUPABASE_URL")
    supabase_key: str = os.getenv("SUPABASE_SERVICE_KEY")  # 使用service_role key
    # 数据库连接字符串 (可选,用于直接SQL操作)
    database_url: str = f"postgresql://postgres:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:5432/postgres"
    # 火山引擎TOS配置 (S3兼容)
    tos_access_key: str = os.getenv("TOS_ACCESS_KEY")
    tos_secret_key: str = os.getenv("TOS_SECRET_KEY")
    tos_endpoint: str = os.getenv("TOS_ENDPOINT")
    tos_bucket_name: str = os.getenv("TOS_BUCKET_NAME")
    # LLM配置 (例如OpenAI)
    openai_api_key: str = os.getenv("OPENAI_API_KEY")

    class Config:
        env_file = ".env"

settings = Settings()

backend/app/services/supabase_client.py

from supabase import create_client, Client
from app.core.config import settings
import logging

logger = logging.getLogger(__name__)

class SupabaseService:
    _client: Client = None

    @classmethod
    def get_client(cls) -> Client:
        if cls._client is None:
            try:
                cls._client = create_client(settings.supabase_url, settings.supabase_key)
                logger.info("Supabase client initialized successfully.")
            except Exception as e:
                logger.error(f"Failed to initialize Supabase client: {e}")
                raise
        return cls._client

    @classmethod
    async def save_agent_session(cls, session_data: dict):
        """保存Agent会话历史到'sessions'表"""
        client = cls.get_client()
        response = client.table('sessions').insert(session_data).execute()
        return response.data

    @classmethod
    async def get_user_conversations(cls, user_id: str, limit: int = 10):
        """获取用户最近的对话历史"""
        client = cls.get_client()
        response = client.table('sessions')\
                         .select("*")\
                         .eq('user_id', user_id)\
                         .order('created_at', desc=True)\
                         .limit(limit)\
                         .execute()
        return response.data

这里的关键是使用 SUPABASE_SERVICE_KEY 而非 ANON_KEY 。因为后端服务需要较高的数据库操作权限(如向任何表插入数据), service_role key 是必需的。 切记,这个密钥绝不能暴露给前端。

3.2 实现一个具备记忆与工具调用能力的Agent端点

接下来,我们实现一个真正的AI Agent端点。这个Agent将能够进行多轮对话(记忆),并且可以调用一个“获取天气”的模拟工具。

首先,在Supabase中创建两张表(通过SQL Editor执行):

-- 会话表,记录每次对话的元信息
CREATE TABLE sessions (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id TEXT NOT NULL,
  title TEXT, -- 自动生成的会话标题
  created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);

-- 消息表,记录会话中的每条消息
CREATE TABLE messages (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
  content TEXT,
  tool_calls JSONB, -- 存储Agent提出的工具调用请求
  tool_call_id TEXT, -- 对应tool_calls中的id
  created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);

-- 为常用查询创建索引
CREATE INDEX idx_messages_session_id ON messages(session_id);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);

然后,实现Agent路由 ( backend/app/main.py ):

from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
import logging
from app.services.supabase_client import SupabaseService
from app.services.agent_orchestrator import AgentOrchestrator

app = FastAPI(title="AI Agent Backend")

# 配置CORS,允许前端域名访问
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-iga-pages-domain.vercel.app"],  # 替换为你的前端域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 请求/响应模型
class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    session_id: Optional[str] = None  # 为空则创建新会话
    message: str
    user_id: str  # 从前端认证token中解析而来

class ChatResponse(BaseModel):
    session_id: str
    reply: str
    tool_used: Optional[str] = None

@app.post("/chat", response_model=ChatResponse)
async def chat_with_agent(request: ChatRequest):
    """
    核心聊天端点。
    1. 根据session_id获取或创建会话。
    2. 从数据库加载该会话的历史消息作为记忆。
    3. 将用户新消息和记忆交给AgentOrchestrator处理。
    4. Agent可能返回纯文本,也可能请求调用工具。
    5. 执行工具,将结果返回给Agent获取最终回复。
    6. 将用户消息、工具调用、工具结果、助手回复完整保存到数据库。
    """
    try:
        # 初始化编排器(内部包含LangChain Agent或自定义逻辑)
        orchestrator = AgentOrchestrator(user_id=request.user_id)

        # 处理会话
        if not request.session_id:
            # 创建新会话,并可能用第一条消息生成一个标题
            session_data = {"user_id": request.user_id}
            new_session = await SupabaseService.save_agent_session(session_data)
            session_id = new_session[0]['id']
            # 可选:异步生成会话标题
            # asyncio.create_task(generate_session_title(session_id, request.message))
        else:
            session_id = request.session_id

        # 获取历史消息(最近10轮作为上下文)
        history = await SupabaseService.get_messages_by_session(session_id, limit=20)
        # 将历史消息转换为LangChain或其他框架所需的Memory格式

        # 调用Agent编排器处理当前轮次
        agent_response = await orchestrator.process(
            session_id=session_id,
            user_input=request.message,
            chat_history=history
        )

        # 保存交互记录到Supabase
        await SupabaseService.save_message({
            "session_id": session_id,
            "role": "user",
            "content": request.message
        })
        if agent_response.tool_calls:
            for tool_call in agent_response.tool_calls:
                await SupabaseService.save_message({
                    "session_id": session_id,
                    "role": "assistant",
                    "tool_calls": tool_call.dict(),
                    "content": None
                })
        # ... 保存工具执行结果和助手最终回复

        return ChatResponse(
            session_id=session_id,
            reply=agent_response.final_output,
            tool_used=agent_response.tool_name
        )

    except Exception as e:
        logging.error(f"Error in chat endpoint: {e}")
        raise HTTPException(status_code=500, detail="Internal Server Error")

backend/app/services/agent_orchestrator.py 简化示例:

from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import os

class AgentOrchestrator:
    def __init__(self, user_id: str):
        self.llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0, api_key=os.getenv("OPENAI_API_KEY"))
        self.memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
        self.tools = [self.get_weather_tool]  # 注册工具
        self.agent = self._create_agent()

    @tool
    def get_weather_tool(city: str) -> str:
        """获取指定城市的当前天气。这是一个模拟工具。"""
        # 实际项目中,这里会调用真实的天气API
        weather_data = {
            "北京": "晴,15°C",
            "上海": "多云,18°C",
            "深圳": "阵雨,22°C"
        }
        return weather_data.get(city, f"未找到{city}的天气信息。")

    def _create_agent(self):
        prompt = ChatPromptTemplate.from_messages([
            ("system", "你是一个乐于助人的AI助手。你可以使用工具来获取信息。"),
            MessagesPlaceholder(variable_name="chat_history"),
            ("human", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad"),
        ])
        agent = create_openai_tools_agent(self.llm, self.tools, prompt)
        return AgentExecutor(agent=agent, tools=self.tools, memory=self.memory, verbose=True)

    async def process(self, session_id: str, user_input: str, chat_history: list):
        # 将数据库中的历史记录加载到LangChain Memory中
        for msg in chat_history:
            if msg['role'] == 'user':
                self.memory.chat_memory.add_user_message(msg['content'])
            elif msg['role'] == 'assistant':
                self.memory.chat_memory.add_ai_message(msg['content'])

        # 执行Agent
        response = await self.agent.ainvoke({"input": user_input})
        return AgentResponse(
            final_output=response["output"],
            tool_calls=response.get("intermediate_steps", []),
            tool_name=response.get("tool", None)
        )

这个架构实现了带有持久化记忆(存储在Supabase)和工具调用能力的Agent。每次对话的完整轨迹都被记录下来,便于后续分析、调试或实现更复杂的“反思”与“规划”能力。

4. 前端交互:React与Supabase实时通信

前端的目标是提供一个美观、响应式的界面,让用户能与AI Agent自然对话,并实时看到对话流和Agent的思考过程(如工具调用)。

4.1 初始化Supabase客户端并实现实时订阅

在前端项目中,我们首先初始化Supabase客户端,并利用其强大的实时(Realtime)功能来同步对话更新。

frontend/src/lib/supabase.js

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Missing Supabase environment variables')
}

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  realtime: {
    params: {
      eventsPerSecond: 10, // 控制事件频率
    },
  },
})

在聊天组件中订阅消息变更:

import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'

function ChatRoom({ sessionId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // 1. 首次加载时获取历史消息
    const fetchMessages = async () => {
      const { data, error } = await supabase
        .from('messages')
        .select('*')
        .eq('session_id', sessionId)
        .order('created_at', { ascending: true })
      if (!error && data) setMessages(data)
    }
    fetchMessages()

    // 2. 订阅该会话的新消息(实时推送)
    const channel = supabase
      .channel(`room:${sessionId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `session_id=eq.${sessionId}`,
        },
        (payload) => {
          // 当数据库中有新消息插入时,实时更新UI
          setMessages((prev) => [...prev, payload.new])
        }
      )
      .subscribe()

    // 清理订阅
    return () => {
      supabase.removeChannel(channel)
    }
  }, [sessionId])

  // ... 渲染消息列表
}

通过实时订阅,当后端Agent将新的消息(用户输入、工具调用、助手回复)插入Supabase数据库时,所有打开了该会话的前端页面都会立即收到更新,无需轮询。这为构建协同编辑、客服看板等场景提供了可能。

4.2 构建聊天UI与调用后端API

接下来,构建主要的聊天界面,并实现与后端FastAPI服务的通信。

frontend/src/components/ChatInterface.jsx

import { useState, useRef, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import './ChatInterface.css'

export default function ChatInterface({ user }) {
  const [input, setInput] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [currentSessionId, setCurrentSessionId] = useState(null)
  const messagesEndRef = useRef(null)

  // 发送消息
  const handleSend = async () => {
    if (!input.trim() || isLoading) return
    const userMessage = input
    setInput('')
    setIsLoading(true)

    try {
      // 调用后端 /chat 接口
      const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        // 假设用户身份已通过Supabase Auth获取
        body: JSON.stringify({
          session_id: currentSessionId,
          message: userMessage,
          user_id: user.id
        }),
      })

      if (!response.ok) throw new Error('Network response was not ok')
      const data = await response.json()

      // 如果这是新会话,设置sessionId
      if (!currentSessionId && data.session_id) {
        setCurrentSessionId(data.session_id)
      }

      // 注意:助手回复会通过Supabase实时订阅自动添加到messages状态中
      // 这里无需手动更新UI
    } catch (error) {
      console.error('Error sending message:', error)
      // 可以在这里添加一个错误提示到消息列表
      const errorMessage = {
        id: Date.now(),
        role: 'system',
        content: `发送失败: ${error.message}`,
        created_at: new Date().toISOString()
      }
      // 手动添加错误消息到状态(如果不想依赖实时订阅)
    } finally {
      setIsLoading(false)
    }
  }

  // 滚动到底部
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages]) // 依赖messages状态,当消息更新时滚动

  return (
    <div className="chat-container">
      <div className="messages-panel">
        {messages.map((msg) => (
          <div key={msg.id} className={`message ${msg.role}`}>
            <div className="avatar">{msg.role === 'user' ? '👤' : '🤖'}</div>
            <div className="content">
              {msg.role === 'tool' ? (
                <div className="tool-call">
                  <small>调用了工具: {msg.tool_name}</small>
                  <pre>{JSON.stringify(msg.content, null, 2)}</pre>
                </div>
              ) : (
                <p>{msg.content}</p>
              )}
            </div>
          </div>
        ))}
        {isLoading && (
          <div className="message assistant">
            <div className="avatar">🤖</div>
            <div className="content">
              <div className="typing-indicator">
                <span></span><span></span><span></span>
              </div>
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>
      <div className="input-area">
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
          placeholder="输入你的问题..."
          disabled={isLoading}
          rows="3"
        />
        <button onClick={handleSend} disabled={isLoading || !input.trim()}>
          {isLoading ? '思考中...' : '发送'}
        </button>
      </div>
    </div>
  )
}

这个前端组件完成了消息发送、加载状态显示、消息列表渲染(区分用户、助手、工具消息)和自动滚动的核心循环。它与后端的REST API交互发起对话,并依靠Supabase的实时功能来接收更新,实现了流畅的聊天体验。

5. 一键部署:Docker化与自动化脚本

将所有组件部署上线的最后一步,是将后端服务容器化并推送到火山引擎ECS,同时将前端部署到IGA Pages。我们的目标是实现“一句话”或“一个脚本”完成全部操作。

5.1 编写Dockerfile与docker-compose.yml

首先,为后端服务创建Dockerfile,确保环境一致性。

backend/Dockerfile

# 使用官方Python镜像
FROM python:3.11-slim

# 设置工作目录
WORKDIR /app

# 设置环境变量,防止Python输出缓冲,使日志实时显示
ENV PYTHONUNBUFFERED=1

# 安装系统依赖(例如,PostgreSQL客户端库、构建工具)
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 暴露端口(与FastAPI应用内一致)
EXPOSE 8000

# 启动命令,使用uvicorn作为ASGI服务器
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

注意 :生产环境应使用 --workers 指定多进程,并配合 gunicorn 等WSGI服务器。这里使用 --reload 仅适用于开发或调试。

backend/docker-compose.yml (用于本地开发与测试):

version: '3.8'
services:
  backend:
    build: .
    ports:
      - "8000:8000"
    environment:
      - SUPABASE_URL=${SUPABASE_URL}
      - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - TOS_ACCESS_KEY=${TOS_ACCESS_KEY}
      - TOS_SECRET_KEY=${TOS_SECRET_KEY}
      - TOS_ENDPOINT=${TOS_ENDPOINT}
      - TOS_BUCKET_NAME=${TOS_BUCKET_NAME}
    volumes:
      - ./app:/app/app  # 挂载代码目录,实现代码热重载
    # 如果需要本地数据库,可以添加一个PostgreSQL服务
    # depends_on:
    #   - db
  # db:
  #   image: postgres:15
  #   environment:
  #     POSTGRES_PASSWORD: example
  #   volumes:
  #     - postgres_data:/var/lib/postgresql/data

# volumes:
#   postgres_data:

5.2 编写自动化部署脚本

“一句话上线”的灵魂在于自动化脚本。我们编写一个Shell脚本,将构建、推送、部署的步骤串联起来。

scripts/deploy.sh

#!/bin/bash

set -e  # 遇到错误即停止

echo "🚀 开始 AI Agent 全栈部署..."

# 1. 定义变量
BACKEND_DIR="./backend"
FRONTEND_DIR="./frontend"
ECR_REGISTRY="your-volcano-engine-container-registry-address" # 替换为你的火山引擎容器镜像仓库地址
ECS_INSTANCE_IP="your-ecs-public-ip" # 替换为你的ECS公网IP
ECS_SSH_USER="root" # 或你的用户名
IMAGE_TAG="latest"
IMAGE_NAME="my-ai-agent-backend"

# 2. 构建前端静态文件
echo "📦 构建前端应用..."
cd $FRONTEND_DIR
npm install
npm run build  # 假设package.json中build命令是 `vite build` 或 `next build`
echo "✅ 前端构建完成。"

# 3. 部署前端到IGA Pages (以Vercel CLI为例)
echo "🌐 部署前端到 Vercel..."
# 确保已安装Vercel CLI并登录: `npm i -g vercel && vercel login`
vercel --prod --confirm  # 自动检测项目并部署
FRONTEND_URL=$(vercel ls | grep my-ai-agent-frontend | head -1 | awk '{print $2}')
echo "✅ 前端已部署至: $FRONTEND_URL"

# 4. 构建并推送后端Docker镜像
echo "🐳 构建后端Docker镜像..."
cd ../$BACKEND_DIR
docker build -t $IMAGE_NAME:$IMAGE_TAG .

# 登录火山引擎容器镜像仓库 (假设使用标准Docker Registry)
# 请先在火山引擎控制台创建镜像仓库并获取登录命令
# docker login --username=your-username $ECR_REGISTRY

docker tag $IMAGE_NAME:$IMAGE_TAG $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
docker push $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
echo "✅ 后端镜像已推送至容器仓库。"

# 5. 远程部署到火山引擎ECS
echo "🖥️  部署到火山引擎ECS..."
# 通过SSH连接到ECS实例,执行部署命令
ssh $ECS_SSH_USER@$ECS_INSTANCE_IP << EOF
  set -e
  echo "1. 拉取最新镜像..."
  docker pull $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG

  echo "2. 停止并移除旧容器..."
  docker stop $IMAGE_NAME || true
  docker rm $IMAGE_NAME || true

  echo "3. 运行新容器..."
  docker run -d \\
    --name $IMAGE_NAME \\
    --restart unless-stopped \\
    -p 8000:8000 \\
    -e SUPABASE_URL="$SUPABASE_URL" \\
    -e SUPABASE_SERVICE_KEY="$SUPABASE_SERVICE_KEY" \\
    -e OPENAI_API_KEY="$OPENAI_API_KEY" \\
    -e TOS_ACCESS_KEY="$TOS_ACCESS_KEY" \\
    -e TOS_SECRET_KEY="$TOS_SECRET_KEY" \\
    -e TOS_ENDPOINT="$TOS_ENDPOINT" \\
    -e TOS_BUCKET_NAME="$TOS_BUCKET_NAME" \\
    $ECR_REGISTRY/$IMAGE_NAME:$IMAGE_TAG

  echo "4. 检查容器状态..."
  sleep 5
  docker ps | grep $IMAGE_NAME
  echo "✅ 后端服务部署完成。"
EOF

# 6. 更新前端环境变量(指向新的后端地址)
echo "🔗 更新前端环境变量(后端API地址)..."
# 这里需要根据你ECS实例的网络配置来设置。
# 如果你的ECS有公网IP和域名,可以这样设置:
BACKEND_API_URL="http://$ECS_INSTANCE_IP:8000"  # 或你的域名
# 使用Vercel CLI更新环境变量
cd ../$FRONTEND_DIR
vercel env add VITE_BACKEND_URL production $BACKEND_API_URL

echo "🎉 全栈部署完成!"
echo "前端访问: $FRONTEND_URL"
echo "后端API: $BACKEND_API_URL"
echo "请确保ECS安全组已放行8000端口。"

这个脚本涵盖了从本地构建到云端部署的全流程。在实际使用前,你需要:

  1. 替换脚本中的占位符(容器仓库地址、ECS IP等)。
  2. 确保本地已安装Docker、Vercel CLI,并完成相应的登录认证。
  3. 为ECS实例配置SSH密钥对,以便无密码登录。
  4. 在ECS安全组中开放8000端口(或你自定义的端口)。

执行 bash scripts/deploy.sh ,理论上就可以完成从代码到服务的全自动上线。这就是“一句话上线”的终极形态——将复杂性封装在一个脚本中。

6. 实测踩坑与性能优化要点

将应用部署上线并运行一段时间后,我遇到了几个典型问题,这里分享出来,希望能帮你避开这些坑。

6.1 数据库连接池与长连接管理

在初期,后端服务运行一段时间后偶尔会出现数据库连接超时或耗尽的问题。这是因为Supabase PostgreSQL数据库对连接数有限制(免费版约20个并发连接),而FastAPI在默认情况下,每个请求可能会创建新的数据库连接。

解决方案:使用连接池并在应用生命周期内管理连接。 我们在FastAPI的启动和关闭事件中管理一个全局的数据库连接池(如 asyncpg 池或 SQLAlchemy 的引擎)。

# backend/app/core/database.py
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from app.core.config import settings

engine = create_async_engine(
    settings.database_url,
    echo=False,  # 生产环境设为False
    pool_size=5,  # 连接池大小,根据Supabase限制和你的并发量调整
    max_overflow=10,
    pool_pre_ping=True,  # 每次从池中取连接前先ping一下,防止连接失效
    pool_recycle=300,  # 连接回收时间(秒)
)

AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

# 在FastAPI的依赖注入中使用
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
        finally:
            await session.close()

# 在main.py中注册事件
@app.on_event("startup")
async def startup_event():
    # 可以在这里进行一些初始化,连接池会自动创建
    pass

@app.on_event("shutdown")
async def shutdown_event():
    await engine.dispose()

这样确保了数据库连接被高效复用,不会超出限制。

6.2 Agent超时与异步任务队列

AI Agent的推理,尤其是涉及复杂链式调用或大模型响应慢时,很容易超过HTTP请求的典型超时时间(如30秒)。让用户前端长时间等待一个HTTP响应是不现实的。

解决方案:采用异步任务模式。

  1. 快速响应 :后端 /chat 接口收到请求后,立即返回一个 task_id session_id ,告知请求已接受。
  2. 后台处理 :将实际的Agent处理逻辑放入一个后台任务队列(如Celery、RQ,或更简单的 asyncio.create_task 配合内存队列,但后者可靠性低)。
  3. 状态推送 :后台任务处理过程中,将状态更新(开始处理、调用工具、生成结果)通过WebSocket或Supabase的Realtime功能推送到前端。
  4. 前端轮询/订阅 :前端根据 task_id 轮询一个状态接口,或直接订阅Supabase中该任务记录的变化,来获取最终结果和中间过程。
# 伪代码示例:FastAPI + 后台任务
from fastapi import BackgroundTasks
import asyncio

@app.post("/chat/async")
async def chat_async(request: ChatRequest, background_tasks: BackgroundTasks):
    task_id = str(uuid.uuid4())
    # 将任务放入后台
    background_tasks.add_task(run_agent_task, task_id, request)
    return {"task_id": task_id, "status": "accepted"}

async def run_agent_task(task_id: str, request: ChatRequest):
    # 1. 在Supabase中创建一条任务记录,状态为'processing'
    # 2. 执行耗时的Agent逻辑
    # 3. 每一步更新任务记录(如写入工具调用结果)
    # 4. 最终完成时,更新状态为'completed'并写入最终回复
    # 前端通过订阅`tasks`表或轮询`/task/{task_id}`来获取进度。

6.3 前端环境变量与安全

frontend/.env.local 中配置的Supabase密钥是 ANON_KEY ,它是公开的。但后端API地址( VITE_BACKEND_URL )在开发和生产环境可能不同。如果直接写死在代码里,每次部署都需要修改。

解决方案:利用IGA Pages(如Vercel)的环境变量配置。

  1. 在Vercel项目设置的Environment Variables中,分别配置 Production Preview Development 环境下的 VITE_BACKEND_URL
  2. 在代码中通过 import.meta.env.VITE_BACKEND_URL 访问。
  3. 在部署脚本中,可以通过Vercel CLI ( vercel env add ) 动态更新环境变量,如脚本第6步所示。

安全提醒 :永远不要在前端环境变量或代码中放入敏感信息,如 SUPABASE_SERVICE_KEY 、数据库密码、第三方API密钥等。这些必须仅在后端环境(ECS的环境变量或密钥管理服务)中设置。

6.4 成本监控与优化

这套架构虽然便捷,但涉及多项云服务,需要关注成本,尤其是:

  • 火山引擎ECS :按量计费实例在不使用时可以关机节省费用。对于流量波动大的应用,可以考虑配置弹性伸缩(AS)。
  • Supabase :免费计划有使用限制(数据库空间、带宽、函数调用次数)。务必在控制台设置用量警报,并优化查询,避免全表扫描。对于 messages 这类增长快的表,考虑归档旧数据或使用分区。
  • LLM API调用(如OpenAI) :这是AI应用的主要成本之一。实施对话长度限制、缓存常见回答、对用户进行分级限流等都是有效的控制手段。

部署完成后,定期查看各云服务商的控制台账单和用量图表,建立成本意识,是项目健康运营的关键。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐