项目概述:为什么要做 EcoGPT?
在 “双碳” 目标和环保意识提升的背景下,普通人想践行可持续生活却常遇难题:不知道如何正确分类垃圾、找不到本地回收点、不清楚哪些品牌真正环保…… 但大模型(如 GPT)的知识存在 “滞后性” 和 “泛化性”,无法精准回答实时环保政策、本地化信息或专业领域细节。
于是,EcoGPT—— 专注环保领域的 AI 问答助手应运而生。它基于「检索增强生成(RAG)」技术,将实时环保知识库与大模型结合,既能像普通 ChatBot 一样对话,又能提供精准、专业的环保建议。
技术栈选择
做项目的第一步不是写代码,而是选对工具 —— 合适的技术栈能让开发效率提升 50%。EcoGPT 的技术栈围绕 “快速开发 + 专业能力 + 流畅体验” 设计,具体清单和选型理由如下:
| 技术领域 | 核心工具 | 选型理由 |
|---|---|---|
| 前端框架 | Next.js 14 | 支持 SSR/CSR 混合模式(SEO 友好 + 交互流畅),内置路由,适配客户端组件(use client) |
| 样式方案 | Tailwind CSS | 移动优先设计,无需写自定义 CSS,通过类名快速实现响应式布局(如适配手机 / PC) |
| AI 交互核心 | @ai-sdk/core + @ai-sdk/react | 封装 LLM 调用逻辑,1 行 hooks 搞定 “输入管理 + 流式输出 + 消息存储”,无需重复造轮子 |
| 大模型 | OpenAI GPT-4o-mini + text-embedding-3-small | 轻量高效(GPT-4o-mini 成本低、响应快),embedding 模型生成 1536 维向量,适配向量数据库 |
| 向量数据库 | Supabase(PostgreSQL) | BAAS 服务(无需自建后端),PostgreSQL 支持向量类型,内置相似度计算,还能写 RPC 函数 |
| 知识库构建 | LangChain + Puppeteer | LangChain 提供文本分块 / 加载工具,Puppeteer(无头浏览器)爬取实时环保网站内容 |
| 辅助工具 | lucide-react + react-markdown | 轻量图标库(Leaf/User/Bot 图标),Markdown 渲染(AI 输出支持格式化) |
| 配置管理 | dotenv + TypeScript | 环境变量安全管理,TS 类型约束减少 bug(如组件 Props 定义) |
全流程实现:从代码到运行的每一步
第一步:项目初始化与环境配置
1. 创建 Next.js 项目
npx create-next-app@latest ecogpt --typescript --tailwind --app
cd ecogpt
2. 安装依赖
npm install ai @ai-sdk/openai @ai-sdk/react @supabase/supabase-js
npm install @langchain/community puppeteer lucide-react react-markdown dotenv
npm install -D ts-node # 用于运行TS脚本(如爬虫)
3. 配置环境变量(.env.local)
创建.env.local文件,填入关键信息(需从 Supabase、OpenAI 控制台获取):
# Supabase配置
NEXT_PUBLIC_SUPABASE_URL=你的Supabase项目URL
NEXT_PUBLIC_SUPABASE_KEY=你的Supabase匿名密钥
# OpenAI配置
OPENAI_API_KEY=你的OpenAI API密钥
OPENAI_API_BASE_URL=你的OpenAI BaseURL(如用代理可修改)
4. 解决 TS-Node 不支持 ESM 问题
修改tsconfig.json,确保ts-node能运行爬虫脚本:
{
"compilerOptions": {
"module": "CommonJS", // 关键:改为CommonJS,支持ts-node
"moduleResolution": "Node",
"target": "ES2018",
"jsx": "preserve",
"strict": true,
"esModuleInterop": true
}
}
第二步:RAG 核心 —— 构建环保知识库(爬虫 + 向量存储)
RAG 是 EcoGPT “专业能力” 的核心,本质是 “先检索专业知识,再让 AI 基于知识回答”。这一步要完成两个关键动作:爬取环保数据 → 存入向量数据库。
1. 第一步:创建 Supabase 表与 RPC 函数
首先在 Supabase 控制台做两件事:
-
创建 chunks 表:存储分块后的文本、向量、来源 URL
CREATE TABLE chunks ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), content TEXT NOT NULL, -- 分块后的文本内容 vector VECTOR(1536) NOT NULL, -- 1536维向量(匹配text-embedding-3-small) url TEXT NOT NULL, -- 内容来源URL date_updated TIMESTAMP DEFAULT NOW() -- 更新时间 ); -
创建 RPC 函数 get_relevant_chunks:用于根据用户问题的向量,检索相似度最高的内容
create or replace function get_relevant_chunks( query_vector vector(1536), -- 用户问题的向量 match_threshold float, -- 相似度阈值(0.7以上视为相关) match_count int -- 最多返回5条结果 ) returns table ( id uuid, content text, url text, date_updated timestamp, similarity float ) language sql stable as $$ select id, content, url, date_updated, 1 - (chunks.vector <=> query_vector) as similarity -- cos相似度计算(1-距离=相似度) from chunks where 1 - (chunks.vector <=> query_vector) > match_threshold order by similarity desc -- 按相似度倒序 limit match_count; $$;
2. 第二步:写爬虫脚本(爬取环保网站 + 分块 + 向量存储)
创建scripts/seed.ts文件,功能是:用 Puppeteer 爬取指定环保网站 → 用 LangChain 分块 → 生成向量 → 插入 Supabase。
// scripts/seed.ts
import { createOpenAI } from "@ai-sdk/openai";
import { PuppeteerWebBaseLoader } from '@langchain/community/document_loaders/web/puppeteer';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { embed } from 'ai';
import "dotenv/config";
import { createClient } from '@supabase/supabase-js';
// 1. 初始化Supabase和OpenAI
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
process.env.NEXT_PUBLIC_SUPABASE_KEY ?? ""
);
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_API_BASE_URL,
});
// 2. 网页爬取函数(用Puppeteer无头浏览器)
const scrapePage = async (url: string): Promise<string> => {
const loader = new PuppeteerWebBaseLoader(url, {
launchOptions: {
executablePath: 'C:\Program Files\Google\Chrome\Application\chrome.exe', // 本地Chrome路径
headless: true, // 无头模式(不显示浏览器界面)
},
gotoOptions: {
waitUntil: 'networkidle0', // 等待页面加载完成(网络空闲)
},
evaluate: async (page, browser) => {
const html = await page.evaluate(() => document.body.innerHTML);
await browser.close(); // 关闭浏览器
return html.replace(/<[^>]*>?/gm, ""); // 去除HTML标签,保留纯文本
}
});
return loader.scrape();
};
// 3. 数据加载+分块+向量存储
const loadData = async (webpages: string[]) => {
console.log("开始爬取环保网站...");
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 512, // 每个分块512字符(保证语义完整)
chunkOverlap: 100, // 分块重叠100字符(避免断句)
});
for (const url of webpages) {
try {
// 爬取网页纯文本
const content = await scrapePage(url);
// 文本分块
const chunks = await splitter.splitText(content);
console.log(`爬取${url}成功,得到${chunks.length}个分块`);
// 为每个分块生成向量并插入Supabase
for (const chunk of chunks) {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: chunk,
});
const { error } = await supabase.from("chunks").insert({
content: chunk,
vector: embedding,
url: url,
});
if (error) throw error;
}
} catch (err) {
console.error(`处理${url}失败:`, err);
}
}
console.log("所有数据处理完成!");
};
// 4. 爬取目标环保网站(选高权威、高更新频率的站点)
loadData([
"https://www.ecowatch.com/", // 环保新闻
"https://www.unep.org/news-and-stories", // 联合国环境规划署
"https://www.epa.gov/newsreleases", // 美国环保署
"https://www.goingzerowaste.com/blog/", // 零浪费生活博客
]);
3. 运行爬虫脚本
在package.json中添加脚本:
"scripts": {
"seed": "ts-node scripts/seed.ts"
}
然后执行:
npm run seed
等待几分钟(取决于网站数量),爬取完成后,Supabase 的chunks表会有上千条数据 —— 这就是 EcoGPT 的 “专业知识库”。
第三步:后端 API—— 实现 “检索 + 生成” 逻辑
创建app/api/ecogpt/route.ts文件,这是 EcoGPT 的 “大脑”,负责:
- 接收用户问题 → 生成向量
- 调用 Supabase RPC 检索相关知识库
- 构建 Prompt(结合知识库) → 调用 GPT-4o-mini
- 流式返回结果(逐字显示,提升体验)
// app/api/ecogpt/route.ts
import { embed, streamText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createClient } from '@supabase/supabase-js';
import { NextRequest } from 'next/server';
// 初始化Supabase和OpenAI
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
process.env.NEXT_PUBLIC_SUPABASE_KEY ?? ""
);
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_API_BASE_URL,
});
// 1. 生成文本向量
async function generateEmbedding(message: string) {
return embed({
model: openai.embedding('text-embedding-3-small'),
value: message,
});
}
// 2. 检索相关知识库
async function fetchRelevantContext(embedding: number[]) {
const { data, error } = await supabase.rpc("get_relevant_chunks", {
query_vector: embedding,
match_threshold: 0.7, // 只取相似度≥0.7的内容
match_count: 5, // 最多取5条(避免Prompt过长)
});
if (error) throw error;
// 格式化上下文(包含来源URL和更新时间,方便用户溯源)
return JSON.stringify(
data.map((item: any) => `
Source: ${item.url},
Date Updated: ${item.date_updated}
Content: ${item.content}
`)
);
}
// 3. 构建Prompt模板(核心:约束AI的回答范围和格式)
const createPrompt = (context: string, userQuestion: string) => {
return {
role: 'system',
content: `
你是EcoGPT,专业的环保生活助手,专注于可持续生活建议。
必须基于以下上下文回答,确保信息准确:
----------------
START CONTEXT
${context}
END CONTEXT
----------------
你的专业范围:
- 气候变化与碳中和
- 零浪费生活(减量、复用、回收)
- 绿色科技与可再生能源
- ethical消费与环保品牌
- 各地垃圾分类政策
回答规则:
1. 只回答环保相关问题,无关问题请礼貌拒绝。
2. 提供具体可操作的建议(如“用布袋子代替塑料袋”,而非“减少浪费”)。
3. 本地信息(如回收点)需注明“信息可能更新,建议咨询当地部门”。
4. 用Markdown格式返回,包含来源链接和更新时间。
5. 用户做环保行动时,要鼓励(如“太棒了!坚持使用可重复餐具能减少塑料污染”)。
----------------
用户问题:${userQuestion}
----------------
`
};
};
// 4. 处理POST请求(流式返回)
export async function POST(req: NextRequest) {
try {
const { messages } = await req.json();
const latestMessage = messages.at(-1).content; // 获取最新用户问题
// 生成向量 → 检索上下文 → 构建Prompt
const { embedding } = await generateEmbedding(latestMessage);
const context = await fetchRelevantContext(embedding);
const systemPrompt = createPrompt(context, latestMessage);
// 调用GPT-4o-mini生成流式结果
const result = streamText({
model: openai("gpt-4o-mini"),
messages: [systemPrompt, ...messages], // 传入系统Prompt和历史消息
});
// 转换为数据流响应(前端可接收流式输出)
return result.toDataStreamResponse();
} catch (err) {
console.error("API错误:", err);
return new Response(JSON.stringify({ error: "服务器内部错误" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}
第四步:前端界面 —— 打造流畅的交互体验
前端核心是 “让用户用得爽”,分为 3 个组件:Home(页面容器)、ChatInput(输入框)、ChatOutput(消息展示)。
1. ChatInput 组件(输入框 + 提交按钮)
创建components/ChatInput.tsx,功能:输入问题、提交、加载状态禁用。
// components/ChatInput.tsx
"use client";
import { Input } from '@/components/ui/input'; // 可改用原生input,这里用shadcn组件
import { Button } from '@/components/ui/button';
import { Leaf } from 'lucide-react';
// TS类型约束(避免传参错误)
interface ChatInputProps {
input: string;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (e: React.FormEvent) => void;
isLoading: boolean; // 加载状态(禁用输入和按钮)
}
export default function ChatInput({
input,
handleInputChange,
handleSubmit,
isLoading,
}: ChatInputProps) {
return (
<form onSubmit={handleSubmit} className="flex gap-2 items-center">
<Input
onChange={handleInputChange}
value={input}
placeholder="问我关于零浪费生活、垃圾分类、环保品牌的问题吧!"
className="flex-1 border-green-300 focus:border-green-500"
disabled={isLoading} // 加载时禁用输入
/>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700 text-white"
disabled={!input.trim() || isLoading} // 空输入或加载时禁用按钮
>
<Leaf size={18} className="mr-1" /> {/* 环保叶子图标 */}
<span className="sr-only">提交</span>
</Button>
</form>
);
}
2. ChatOutput 组件(消息展示 + 流式加载 + 自动滚动)
创建components/ChatOutput.tsx,功能:展示用户 / AI 消息、加载状态、空状态提示,以及 “自动滚动到最新消息”。
// components/ChatOutput.tsx
"use client";
import type { Message } from 'ai';
import ReactMarkdown from 'react-markdown';
import { User, Bot } from 'lucide-react';
import { useEffect, useRef } from 'react';
interface ChatOutputProps {
messages: Message[];
status: string; // 状态:idle/submitted/error
}
export default function ChatOutput({ messages, status }: ChatOutputProps) {
const messagesEndRef = useRef<HTMLDivElement>(null); // 用于自动滚动
// 自动滚动到最新消息
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => scrollToBottom(), [messages, status]); // 消息/状态变了就滚动
// 空状态(用户还没提问时)
if (messages.length === 0) {
return (
<div className="text-center py-8 text-green-700">
<Bot size={48} className="mx-auto mb-4 text-green-500" />
<h2 className="text-xl font-semibold mb-2">欢迎使用EcoGPT!</h2>
<p>我可以帮你:</p>
<ul className="list-disc list-inside mt-2 text-left inline-block">
<li>减少碳足迹</li>
<li>查询回收点</li>
<li>选择环保产品</li>
<li>了解垃圾分类政策</li>
<li>践行零浪费生活</li>
</ul>
</div>
);
}
return (
<>
{/* 渲染历史消息 */}
{messages.map((message, index) =>
message.role === "user" ? (
// 用户消息(右对齐)
<div key={index} className="flex justify-end mb-6">
<div className="bg-green-100 text-green-900 rounded-2xl px-4 py-3 max-w-[80%]">
<div className="flex items-center mb-1">
<User size={16} className="mr-1 text-green-600" />
<span className="text-xs font-medium text-green-700">你</span>
</div>
{message.content}
</div>
</div>
) : (
// AI消息(左对齐,Markdown渲染)
<div key={index} className="flex mb-6">
<div className="bg-blue-50 text-gray-800 rounded-2xl px-4 py-3 max-w-[80%]">
<div className="flex items-center mb-2">
<Bot size={16} className="mr-1 text-blue-500" />
<span className="text-xs font-medium text-blue-700">EcoGPT</span>
</div>
<ReactMarkdown
components={{
// 链接打开新窗口
a: ({ href, children }) => (
<a target="_blank" href={href} className="text-blue-600 hover:underline">
{children}
</a>
),
// 列表样式
ul: ({ children }) => <ul className="list-disc list-inside ml-4 mt-2">{children}</ul>,
}}
>
{message.content}
</ReactMarkdown>
</div>
</div>
)
)}
{/* 加载状态(AI思考中) */}
{status === "submitted" && (
<div className="flex items-center text-green-600 my-4">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-green-500 mr-2"></div>
<span>EcoGPT正在思考...</span>
</div>
)}
{/* 错误状态 */}
{status === "error" && (
<div className="text-red-500 p-3 bg-red-50 rounded-lg my-4">
出错了,请稍后再试~
</div>
)}
<div ref={messagesEndRef} /> {/* 滚动定位点 */}
</>
);
}
3. Home 页面(整合组件 + 布局)
创建app/page.tsx,这是 EcoGPT 的入口页面,用 Tailwind 实现响应式布局(手机 / PC 都适配)。
// app/page.tsx
"use client";
import { useChat } from '@ai-sdk/react';
import ChatInput from '@/components/ChatInput';
import ChatOutput from '@/components/ChatOutput';
export default function Home() {
// @ai-sdk/react的useChat hooks:封装了输入、消息、提交逻辑
const {
input,
messages,
status, // idle/submitted/error
handleInputChange, // 输入框变化
handleSubmit, // 提交表单
} = useChat({
api: '/api/ecogpt', // 对接后端API
});
return (
<main className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 py-8 px-4">
{/* 容器(PC端最大宽度768px,居中;手机端100%宽度) */}
<div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-lg p-6">
{/* 标题区 */}
<div className="text-center mb-6">
<h1 className="text-3xl font-bold text-green-800 mb-2">EcoGPT</h1>
<p className="text-green-600">你的环保生活AI助手</p>
<div className="mt-2 flex justify-center">
<span className="inline-block w-12 h-1 bg-green-500 rounded-full"></span>
<span className="inline-block w-4 h-1 bg-green-500 rounded-full mx-1"></span>
<span className="inline-block w-2 h-1 bg-green-500 rounded-full"></span>
</div>
</div>
{/* 消息区(最大高度60vh,滚动) */}
<div className="space-y-4 mb-6 max-h-[60vh] overflow-y-auto p-2 rounded-lg bg-green-50">
<ChatOutput messages={messages} status={status} />
</div>
{/* 输入区 */}
<div className="bg-white rounded-xl p-3 shadow-sm">
<ChatInput
input={input}
handleInputChange={handleInputChange}
handleSubmit={handleSubmit}
isLoading={status === "submitted"} // 传递加载状态
/>
</div>
{/* 提示文本 */}
<div className="mt-4 text-center text-sm text-green-700">
<p>问我关于零浪费生活、碳足迹、回收、环保品牌的问题吧!</p>
</div>
</div>
</main>
);
}
项目亮点:这些设计让 EcoGPT 更优秀
- RAG 技术解决 “专业度” 问题
不是依赖 LLM 的 “模糊记忆”,而是实时检索权威环保知识库,回答更精准(比如能说出 “2024 年某城市垃圾分类新规”)。 - 流式输出提升 “体验感”
用@ai-sdk/react的useChat和streamText,实现逐字显示,避免用户 “等待空白”,体验接近真人对话。 - 响应式布局适配 “多设备”
Tailwind 的max-w-4xl+mx-auto,在手机上是全屏,在 PC 上是居中窄版,阅读更舒适;max-h-[60vh]避免消息区过长。 - Prompt 工程保证 “回答质量”
明确 AI 的 “身份(EcoGPT)”“专业范围”“回答规则”,避免答非所问;强制包含来源链接,提升可信度。 - 组件化与 TS 约束降低 “维护成本”
拆分ChatInput/ChatOutput组件,逻辑清晰;TS 类型约束(如ChatInputProps)避免传参错误,后期改代码更放心。
五、踩坑与解决方案
-
AI SDK 版本不兼容导致调试失败
一开始用的aiSDK 旧版本,streamText返回格式和文档不一致,查了官方文档后更新到最新版(npm update ai),问题解决。 -
ts-node 运行爬虫提示 “ESM 模块错误”
原因是tsconfig.json的module设为了ESNext,改为CommonJS后,ts-node 能正常解析。 -
Supabase RPC 调用返回空数据
排查发现:① 向量维度不匹配(一开始用了 768 维模型,应该用 1536 维);② RPC 函数的match_threshold设太高(一开始 0.8,改为 0.7 后有数据)。 -
Puppeteer 启动失败
本地没有安装 Chrome,或者executablePath路径错误,指定本地 Chrome 的完整路径(如C:\Program Files\Google\Chrome\Application\chrome.exe)后解决。
总结与扩展:EcoGPT 的未来
EcoGPT 目前已经能满足 “基础环保问答” 需求,但还有很多可以优化的方向:
- 本地化功能:对接高德 / 百度地图 API,提供 “附近回收点查询”。
- 个人化建议:添加 “个人碳足迹计算”,根据用户习惯推荐环保方案。
- 多语言支持:增加英文、日文等,服务更多用户。
- 知识库更新自动化:用 GitHub Actions 定时运行爬虫,确保知识库实时更新。
这个项目的核心是 “RAG + 专业领域”—— 只要替换知识库(比如换成医疗、法律、教育内容),就能快速打造其他领域的专业 ChatBot,比如 “MedicalGPT”“LawGPT”,复用率非常高。