火山引擎+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应用的主要成本之一。实施对话长度限制、缓存常见回答、对用户进行分级限流等都是有效的控制手段。
部署完成后,定期查看各云服务商的控制台账单和用量图表,建立成本意识,是项目健康运营的关键。
更多推荐


所有评论(0)