火山引擎+Supabase+IGA Pages:AI Agent应用全栈托管部署实战
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端口。"这个脚本涵盖了从本地构建到云端部署的全流程。在实际使用前,你需要:
- 替换脚本中的占位符(容器仓库地址、ECS IP等)。
- 确保本地已安装Docker、Vercel CLI,并完成相应的登录认证。
- 为ECS实例配置SSH密钥对,以便无密码登录。
- 在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响应是不现实的。
解决方案:采用异步任务模式。
- 快速响应:后端
/chat接口收到请求后,立即返回一个task_id或session_id,告知请求已接受。 - 后台处理:将实际的Agent处理逻辑放入一个后台任务队列(如Celery、RQ,或更简单的
asyncio.create_task配合内存队列,但后者可靠性低)。 - 状态推送:后台任务处理过程中,将状态更新(开始处理、调用工具、生成结果)通过WebSocket或Supabase的Realtime功能推送到前端。
- 前端轮询/订阅:前端根据
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)的环境变量配置。
- 在Vercel项目设置的Environment Variables中,分别配置
Production、Preview、Development环境下的VITE_BACKEND_URL。 - 在代码中通过
import.meta.env.VITE_BACKEND_URL访问。 - 在部署脚本中,可以通过Vercel CLI (
vercel env add) 动态更新环境变量,如脚本第6步所示。
安全提醒:永远不要在前端环境变量或代码中放入敏感信息,如SUPABASE_SERVICE_KEY、数据库密码、第三方API密钥等。这些必须仅在后端环境(ECS的环境变量或密钥管理服务)中设置。
6.4 成本监控与优化
这套架构虽然便捷,但涉及多项云服务,需要关注成本,尤其是:
- 火山引擎ECS:按量计费实例在不使用时可以关机节省费用。对于流量波动大的应用,可以考虑配置弹性伸缩(AS)。
- Supabase:免费计划有使用限制(数据库空间、带宽、函数调用次数)。务必在控制台设置用量警报,并优化查询,避免全表扫描。对于
messages这类增长快的表,考虑归档旧数据或使用分区。 - LLM API调用(如OpenAI):这是AI应用的主要成本之一。实施对话长度限制、缓存常见回答、对用户进行分级限流等都是有效的控制手段。
部署完成后,定期查看各云服务商的控制台账单和用量图表,建立成本意识,是项目健康运营的关键。
