三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于Google Gemini与RAG技术,低成本构建个人博客AI助手

基于Google Gemini与RAG技术,低成本构建个人博客AI助手

想给个人博客加个AI聊天助手,但一查价格就劝退?每月动辄上百美元的API调用费,让很多独立开发者望而却步。最近,我成功为自己的技术博客部署了一个基于Google Gemini的智能问答助手,每月成本稳定在5到15美元之间,并且实现了完整的RAG(检索增强生成)能力,能精准回答博客内的技术问题。

这篇文章要解决的,不是另一个“Hello World”式的Demo,而是一个低成本、可落地、生产可用的个人项目AI助手方案。如果你也在寻找一种既不用牺牲模型能力,又能将月度开销控制在“一杯咖啡”价位的实现路径,那么本文的架构选型、成本拆解和避坑指南,正是你需要的。

我们将使用Google Cloud Functions(云函数)作为无服务器后端,Firestore存储对话历史和向量索引,前端通过简单的JavaScript与后端交互。核心在于,通过合理的架构设计,将昂贵的LLM API调用和向量检索成本降到最低。下面,我将从为什么选择这个方案开始,带你一步步实现它。

1. 为什么是“Gemini + Cloud Functions + Firestore”这个组合?

在构建个人项目的AI功能时,我们通常面临几个核心矛盾:强大的模型能力与高昂成本之间的矛盾、快速迭代的需求与复杂运维之间的矛盾、以及数据隐私与第三方服务依赖性之间的矛盾。Gemini + Cloud Functions + Firestore 这个组合,恰好在这几个维度上找到了一个不错的平衡点。

首先看成本。Gemini API的定价,特别是gemini-1.5-flash模型,在保证足够智能的前提下,价格极具竞争力。Cloud Functions 有慷慨的免费额度,Firestore 在数据量不大时成本几乎可以忽略。将三者结合,意味着你只为实际发生的计算和存储付费,没有闲置的服务器费用。经过我的实测,对于一个日活几百的博客,问答助手的月度成本完全可以控制在5-15美元区间。

其次是工程复杂度。Cloud Functions 让你无需关心服务器配置、负载均衡和系统运维。你只需要写好处理HTTP请求的函数逻辑。Firestore 作为一个文档数据库,天然适合存储结构灵活的对话记录和向量化的文档片段。前端通过Fetch API调用云函数,整个技术栈非常轻量,与现有的静态博客(如Hugo, Hexo, Jekyll)或动态博客(如WordPress)都能轻松集成。

最后是能力与效果。单纯调用Gemini,它只是一个“通才”,无法精准回答你博客里的特定内容。这就是引入RAG的原因。RAG的核心思想是:先将你的博客内容(知识库)处理成向量并存储;当用户提问时,先在知识库中检索最相关的片段;然后将这些片段和问题一起交给Gemini,让它基于这些“上下文”生成答案。这能极大提升答案的准确性和相关性,避免模型“胡编乱造”。

这个方案不适合追求极致低延迟(<100ms)或需要复杂会话状态管理的场景,但对于个人博客、项目文档站、小型知识库来说,它是性价比最高的选择之一。

2. 核心概念与架构总览

在开始动手之前,我们需要明确几个关键概念和整个系统的数据流。

核心概念解析

  • Google Gemini: Google推出的多模态大语言模型系列。我们将使用其文本API。gemini-1.5-flash是性价比之选,响应快,成本低,适合对话场景。
  • Google Cloud Functions: 无服务器执行环境。你上传一段代码(函数),Google Cloud 负责在请求到来时运行它,按运行时间和资源消耗计费。
  • Firestore: NoSQL文档数据库。我们将用它做两件事:1) 存储用户对话历史(会话ID、问答对);2) 存储博客内容的向量嵌入(Embedding)和原文,用于检索。
  • RAG (Retrieval-Augmented Generation): 检索增强生成。这不是一个具体工具,而是一种架构模式。工作流程分为“检索”和“生成”两步,确保答案来源于你提供的知识。

系统架构与数据流整个系统的工作流程如下图所示(用文字描述):

  1. 知识库预处理(离线):将你的所有博客文章(Markdown/HTML)进行文本提取、分块(Chunking),然后调用Gemini的Embedding模型将每个文本块转换为向量(一组数字),最后将{向量,原文,元数据(如文章标题、URL)}存入Firestore。
  2. 用户提问(在线)
    • 前端:用户在前端界面输入问题,JavaScript将其发送到我们部署的Cloud Function。
    • 检索:Cloud Function收到问题后,首先将问题本身也转换为向量,然后在Firestore的向量集合中,进行相似度搜索(如余弦相似度),找出最相关的几个文本块。
    • 增强提示:将检索到的文本块(作为上下文)和用户的原始问题,组合成一个新的、更详细的提示(Prompt),发送给Gemini生成模型。
    • 生成答案:Gemini基于“上下文+问题”生成答案,Cloud Function将答案返回给前端。
    • 存储历史:同时,将本次问答记录存储到Firestore的对话历史集合中,以便实现多轮对话(可选)。

这个架构中,Cloud Function是大脑,协调检索和生成;Firestore是记忆库,存储知识和历史;Gemini是思考引擎,负责理解与创造。

3. 环境准备与项目初始化

我们将创建一个Python项目。请确保你已具备以下条件:

  • 一个Google Cloud Platform (GCP) 项目:如果你没有,请在 Google Cloud Console 创建一个新项目。记下你的项目ID
  • 启用必要的API:在GCP控制台,为你项目启用以下API:
    • Cloud Functions API
    • Firestore API
    • Vertex AI API (或 Gemini API,取决于你使用的端点)
  • 安装并配置Google Cloud SDK:本地需要gcloud命令行工具。安装后,运行gcloud auth login登录,并用gcloud config set project YOUR_PROJECT_ID设置默认项目。
  • Python环境:建议使用Python 3.9或更高版本。使用venv创建虚拟环境是好的实践。
  • 服务账号密钥(可选但推荐):为了在本地测试和部署时进行认证,可以创建一个服务账号并下载其JSON密钥文件。设置环境变量GOOGLE_APPLICATION_CREDENTIALS指向该文件路径。

项目结构初始化在你的工作目录,创建如下结构的项目文件夹:

your-blog-ai-chat/ ├── functions/ │ ├── main.py # Cloud Functions 入口函数 │ ├── requirements.txt # Python依赖 │ └── .gcloudignore # 部署忽略文件 ├── scripts/ │ └── populate_vector_db.py # 离线处理博客文章,填充向量库 ├── frontend/ │ └── chat-widget.js # 前端聊天组件示例 └── blog-content/ # 你的博客文章(原始Markdown/HTML)

接下来,我们进入核心的代码实现环节。

4. 第一步:构建知识库(离线处理脚本)

这是RAG的基石。我们需要一个脚本,读取博客内容,分块,生成向量,存入Firestore。

首先,在scripts目录下创建populate_vector_db.py,并安装必要依赖。在functions/requirements.txt和脚本同级目录的虚拟环境中,都需要这些库。

# functions/requirements.txt 或 scripts/requirements.txt google-cloud-firestore>=2.0.0 google-cloud-aiplatform>=1.38.0 # 用于Vertex AI Embedding # 或者使用 google-generativeai 库(如果直接用Gemini API) google-generativeai>=0.3.0 langchain==0.1.0 # 可选,用于方便的文本分块和加载器 pymupdf # 或 beautifulsoup4,用于解析PDF/HTML

以下是populate_vector_db.py的核心代码:

# scripts/populate_vector_db.py import os import hashlib from typing import List from google.cloud import firestore from google.cloud import aiplatform from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.document_loaders import DirectoryLoader, TextLoader import google.generativeai as genai # 1. 配置 PROJECT_ID = "your-gcp-project-id" # 替换为你的项目ID LOCATION = "us-central1" # 选择你的区域 FIRESTORE_COLLECTION = "blog_chunks" # Firestore集合名,存储向量 BLOG_CONTENT_DIR = "../blog-content" # 博客内容目录 GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") # 或使用应用默认凭证 # 初始化客户端 db = firestore.Client(project=PROJECT_ID) genai.configure(api_key=GEMINI_API_KEY) # 2. 文本加载与分块 def load_and_split_documents() -> List[dict]: """加载博客目录下的所有文档,并进行智能分块。""" # 这里以Markdown文件为例。如果是HTML,可使用 BS4HTMLLoader loader = DirectoryLoader(BLOG_CONTENT_DIR, glob="**/*.md", loader_cls=TextLoader) documents = loader.load() # 使用递归字符分块器,尽量保持段落和句子的完整性 text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, # 每个块约1000字符 chunk_overlap=200, # 块之间重叠200字符,避免上下文断裂 separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = text_splitter.split_documents(documents) # 转换为字典列表,方便后续处理 chunk_dicts = [] for chunk in chunks: chunk_dicts.append({ "content": chunk.page_content, "metadata": { "source": chunk.metadata.get("source", ""), # 可以添加其他元数据,如标题、发布日期等 } }) return chunk_dicts # 3. 生成文本向量(Embedding) def get_embedding(text: str) -> List[float]: """调用Gemini的Embedding模型生成文本向量。""" # 方法一:使用 google-generativeai 库(文本嵌入-001) model = "models/embedding-001" result = genai.embed_content(model=model, content=text) return result["embedding"] # 方法二:使用 Vertex AI 的文本嵌入模型(需启用Vertex AI API) # aiplatform.init(project=PROJECT_ID, location=LOCATION) # model = aiplatform.TextEmbeddingModel.from_pretrained("textembedding-gecko@001") # embeddings = model.get_embeddings([text]) # return embeddings[0].values # 4. 存储到Firestore def store_chunks_with_embedding(chunks: List[dict]): """将文本块及其向量存储到Firestore。""" collection_ref = db.collection(FIRESTORE_COLLECTION) for chunk in chunks: content = chunk["content"] metadata = chunk["metadata"] # 为每个块生成唯一ID(例如,基于内容哈希) chunk_id = hashlib.md5(content.encode()).hexdigest()[:16] # 生成向量 print(f"正在生成向量: {metadata.get('source')} - ID: {chunk_id[:8]}...") embedding = get_embedding(content) # 构建文档数据 doc_data = { "content": content, "metadata": metadata, "embedding": embedding, # Firestore 支持数组字段 "created_at": firestore.SERVER_TIMESTAMP } # 存储到Firestore,使用chunk_id作为文档ID collection_ref.document(chunk_id).set(doc_data) print(f"已存储: {chunk_id[:8]}") # 主函数 def main(): print("开始加载和分块博客内容...") chunks = load_and_split_documents() print(f"共生成 {len(chunks)} 个文本块。") print("开始生成向量并存储到Firestore...") store_chunks_with_embedding(chunks) print("知识库构建完成!") if __name__ == "__main__": main()

关键点解释

  1. 分块策略chunk_size=1000overlap=200是常用配置,平衡了上下文完整性和检索精度。你可以根据博客文章的平均长度调整。
  2. 向量模型:我们使用了Gemini的embedding-001模型。你也可以使用Vertex AI的textembedding-gecko系列,它们可能在不同区域可用性或价格上略有差异。
  3. Firestore存储:直接将向量数组存储在文档的embedding字段。Firestore本身不支持向量相似度搜索,下一步我们需要在查询时计算。

运行此脚本前,请设置好环境变量GEMINI_API_KEY或配置好应用默认凭证。运行后,你的Firestore数据库中就会出现blog_chunks集合,里面存储了所有文本块及其向量。

5. 第二步:创建Cloud Function(核心后端)

这是系统的在线服务核心。我们在functions目录下创建main.py

# functions/main.py import functions_framework import json import logging import os from typing import List, Optional import google.cloud.firestore as firestore import google.generativeai as genai import numpy as np from datetime import datetime # 配置 PROJECT_ID = os.environ.get("PROJECT_ID", "your-gcp-project-id") GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-1.5-flash") FIRESTORE_COLLECTION = os.environ.get("FIRESTORE_COLLECTION", "blog_chunks") CONVERSATION_COLLECTION = os.environ.get("CONVERSATION_COLLECTION", "chat_sessions") # 初始化全局客户端(Cloud Functions 会缓存它们,提升性能) db = firestore.Client(project=PROJECT_ID) genai.configure(api_key=GEMINI_API_KEY) # 初始化模型 generation_model = genai.GenerativeModel(GEMINI_MODEL) embedding_model = "models/embedding-001" def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float: """计算两个向量的余弦相似度。""" a = np.array(vec_a) b = np.array(vec_b) return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def retrieve_relevant_chunks(query: str, limit: int = 5) -> List[dict]: """ 检索与查询最相关的文本块。 步骤:1. 将查询转换为向量。2. 从Firestore获取所有块(对于小知识库可行)。3. 计算相似度并排序。 """ # 注意:对于大规模知识库(>1000条),在Firestore中做全表扫描计算相似度效率很低。 # 生产环境应考虑使用专门的向量数据库(如Vertex AI Vector Search, Pinecone等)。 # 但对于个人博客(几百个块),这种方法简单有效。 # 1. 生成查询向量 query_embedding = genai.embed_content(model=embedding_model, content=query)["embedding"] # 2. 获取所有块(假设数量不大) chunks_ref = db.collection(FIRESTORE_COLLECTION) all_docs = list(chunks_ref.stream()) # 3. 计算相似度 scored_chunks = [] for doc in all_docs: doc_dict = doc.to_dict() chunk_embedding = doc_dict.get("embedding") if chunk_embedding: similarity = cosine_similarity(query_embedding, chunk_embedding) scored_chunks.append({ "content": doc_dict.get("content"), "metadata": doc_dict.get("metadata"), "similarity": similarity, "doc_id": doc.id }) # 4. 按相似度降序排序,返回前limit个 scored_chunks.sort(key=lambda x: x["similarity"], reverse=True) return scored_chunks[:limit] def build_prompt(query: str, relevant_chunks: List[dict]) -> str: """构建给Gemini的提示词,包含检索到的上下文。""" context_text = "\n\n---\n\n".join([chunk["content"] for chunk in relevant_chunks]) prompt = f"""你是一个专业的技术博客助手,请严格根据以下提供的上下文信息来回答问题。如果上下文信息不足以回答问题,请直接说“根据现有资料,我无法回答这个问题”,不要编造信息。 上下文信息: {context_text} 用户问题:{query} 请基于以上上下文,给出准确、简洁的回答:""" return prompt def save_conversation(session_id: str, query: str, answer: str, relevant_doc_ids: List[str]): """将对话记录保存到Firestore,用于历史或分析。""" session_ref = db.collection(CONVERSATION_COLLECTION).document(session_id) # 使用Firestore数组联合更新,添加新消息 new_message = { "query": query, "answer": answer, "relevant_chunks": relevant_doc_ids, "timestamp": firestore.SERVER_TIMESTAMP } session_ref.set({ "messages": firestore.ArrayUnion([new_message]), "last_updated": firestore.SERVER_TIMESTAMP }, merge=True) @functions_framework.http def chat(request): """Cloud Functions HTTP 入口函数。""" # 1. 处理CORS(重要!允许你的博客域名) if request.method == 'OPTIONS': headers = { 'Access-Control-Allow-Origin': '*', # 生产环境应替换为你的博客域名 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Max-Age': '3600' } return ('', 204, headers) headers = { 'Access-Control-Allow-Origin': '*' } # 2. 解析请求 try: request_json = request.get_json(silent=True) if not request_json: return (json.dumps({"error": "Invalid JSON"}), 400, headers) query = request_json.get("query") session_id = request_json.get("session_id", "default_session") # 简单会话管理 if not query: return (json.dumps({"error": "Missing 'query' field"}), 400, headers) logging.info(f"Received query: {query}, session: {session_id}") # 3. 检索相关文本块 relevant_chunks = retrieve_relevant_chunks(query, limit=3) # 取最相关的3个块 if not relevant_chunks: response_text = "抱歉,我的知识库中暂时没有相关信息。" return (json.dumps({"answer": response_text}), 200, headers) # 4. 构建提示并调用Gemini生成 prompt = build_prompt(query, relevant_chunks) response = generation_model.generate_content(prompt) # 处理可能的生成错误或安全拦截 if not response or not response.text: response_text = "生成回答时出现错误,请稍后再试。" else: response_text = response.text # 5. 保存对话记录(可选) relevant_doc_ids = [chunk["doc_id"] for chunk in relevant_chunks] save_conversation(session_id, query, response_text, relevant_doc_ids) # 6. 返回结果 return (json.dumps({ "answer": response_text, "relevant_sources": [chunk["metadata"] for chunk in relevant_chunks] # 返回来源信息 }), 200, headers) except Exception as e: logging.error(f"Error processing request: {e}", exc_info=True) return (json.dumps({"error": f"Internal server error: {str(e)}"}), 500, headers)

代码核心逻辑解读

  1. CORS处理:由于前端博客页面与Cloud Function在不同域名,必须设置CORS头,否则浏览器会阻止请求。
  2. 检索优化retrieve_relevant_chunks函数实现了简单的向量相似度计算。请注意:对于超过1000个文档的知识库,在Cloud Function中做全量计算会超时或成本高。这时应使用专业的向量数据库。但对于个人博客,此方法足够。
  3. 提示工程build_prompt函数是关键。它明确指令模型“基于上下文回答”,并设置了拒绝回答的兜底策略,这是控制生成质量、防止幻觉(Hallucination)的重要手段。
  4. 错误处理:对Gemini API的响应进行了检查,避免因内容安全策略或模型错误导致前端崩溃。
  5. 会话管理:通过session_id简单区分不同对话,并将历史存入Firestore。你可以基于此扩展多轮对话(将历史记录也放入上下文)。

6. 第三步:部署与配置云函数

编写好函数后,我们需要将其部署到Google Cloud。

1. 定义依赖文件确保functions/requirements.txt包含以下内容:

functions-framework==3.* google-cloud-firestore>=2.0.0 google-generativeai>=0.3.0 numpy>=1.0.0

2. 部署命令functions目录下,运行以下命令进行部署:

# 在 functions/ 目录下执行 gcloud functions deploy blog-ai-chat \ --runtime python39 \ --trigger-http \ --allow-unauthenticated \ --region=us-central1 \ --memory=256MB \ --timeout=60s \ --set-env-vars PROJECT_ID=your-gcp-project-id,GEMINI_API_KEY=your_api_key_here,GEMINI_MODEL=gemini-1.5-flash

参数解释

  • --trigger-http:创建一个HTTP触发的函数。
  • --allow-unauthenticated:允许未经身份验证的访问(适合公开博客)。如果需要对调用方做限制,可以移除此参数并设置其他认证方式。
  • --memory--timeout:根据你的知识库大小和查询复杂度调整。256MB和60秒是安全的起步配置。
  • --set-env-vars:设置环境变量,避免将API密钥等硬编码在代码中。请务必将your-gcp-project-idyour_api_key_here替换为实际值。

部署成功后,命令行会输出一个httpsTrigger URL,形如https://us-central1-your-project.cloudfunctions.net/blog-ai-chat。这就是你后端API的地址。

7. 第四步:集成前端聊天组件

最后一步,在你的博客页面中嵌入一个简单的聊天界面。这里提供一个极简的JavaScript示例。

<!-- 在你的博客页面(如 footer.html 或单独的页面)添加以下代码 --> <div id="chat-container" style="position: fixed; bottom: 20px; right: 20px; width: 350px; max-height: 500px; background: white; border: 1px solid #ccc; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: none; flex-direction: column; z-index: 1000;"> <div style="padding: 15px; background: #007acc; color: white; border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center;"> <strong>博客AI助手</strong> <button id="close-chat" style="background: none; border: none; color: white; font-size: 1.2em; cursor: pointer;">×</button> </div> <div id="chat-messages" style="flex: 1; padding: 15px; overflow-y: auto; min-height: 300px; font-size: 0.9em;"> <div class="message bot">你好!我是本博客的AI助手,可以回答博客内涉及的技术问题。有什么可以帮你的?</div> </div> <div style="padding: 15px; border-top: 1px solid #eee;"> <input type="text" id="user-input" placeholder="输入你的问题..." style="width: 70%; padding: 10px; border: 1px solid #ccc; border-radius: 5px;" /> <button id="send-btn" style="width: 25%; padding: 10px; background: #007acc; color: white; border: none; border-radius: 5px; cursor: pointer;">发送</button> </div> </div> <button id="open-chat" style="position: fixed; bottom: 20px; right: 20px; background: #007acc; color: white; border: none; border-radius: 50%; width: 60px; height: 60px; font-size: 1.5em; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">AI</button> <script> // 配置 const CLOUD_FUNCTION_URL = 'https://us-central1-your-project.cloudfunctions.net/blog-ai-chat'; // 替换为你的URL let sessionId = 'session_' + Date.now(); // 生成一个简单的会话ID // DOM元素 const chatContainer = document.getElementById('chat-container'); const openChatBtn = document.getElementById('open-chat'); const closeChatBtn = document.getElementById('close-chat'); const chatMessages = document.getElementById('chat-messages'); const userInput = document.getElementById('user-input'); const sendBtn = document.getElementById('send-btn'); // 打开/关闭聊天窗口 openChatBtn.addEventListener('click', () => { chatContainer.style.display = 'flex'; openChatBtn.style.display = 'none'; }); closeChatBtn.addEventListener('click', () => { chatContainer.style.display = 'none'; openChatBtn.style.display = 'block'; }); // 添加消息到聊天窗口 function addMessage(text, isUser = false) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${isUser ? 'user' : 'bot'}`; messageDiv.textContent = text; messageDiv.style.padding = '8px 12px'; messageDiv.style.margin = '5px 0'; messageDiv.style.borderRadius = '15px'; messageDiv.style.maxWidth = '80%'; messageDiv.style.wordWrap = 'break-word'; if (isUser) { messageDiv.style.alignSelf = 'flex-end'; messageDiv.style.backgroundColor = '#007acc'; messageDiv.style.color = 'white'; } else { messageDiv.style.alignSelf = 'flex-start'; messageDiv.style.backgroundColor = '#f1f1f1'; messageDiv.style.color = '#333'; } chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; // 滚动到底部 } // 发送消息到后端 async function sendMessage() { const query = userInput.value.trim(); if (!query) return; // 显示用户消息 addMessage(query, true); userInput.value = ''; userInput.disabled = true; sendBtn.disabled = true; // 显示“正在思考”指示 const thinkingDiv = document.createElement('div'); thinkingDiv.className = 'message bot'; thinkingDiv.textContent = '正在思考...'; thinkingDiv.id = 'thinking'; chatMessages.appendChild(thinkingDiv); try { const response = await fetch(CLOUD_FUNCTION_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: query, session_id: sessionId }) }); const data = await response.json(); // 移除“正在思考”指示 document.getElementById('thinking').remove(); if (response.ok) { addMessage(data.answer); // 如果有相关来源,可以在这里显示(例如,用小图标提示) if (data.relevant_sources && data.relevant_sources.length > 0) { console.log('相关来源:', data.relevant_sources); } } else { addMessage(`抱歉,出错了: ${data.error || '未知错误'}`); } } catch (error) { document.getElementById('thinking').remove(); addMessage('网络请求失败,请检查网络连接。'); console.error('Fetch error:', error); } finally { userInput.disabled = false; sendBtn.disabled = false; userInput.focus(); } } // 发送按钮和回车键事件 sendBtn.addEventListener('click', sendMessage); userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); </script> <style> /* 简单的样式 */ .message { transition: all 0.3s ease; } #chat-messages::-webkit-scrollbar { width: 5px; } #chat-messages::-webkit-scrollbar-track { background: #f1f1f1; } #chat-messages::-webkit-scrollbar-thumb { background: #888; border-radius: 5px; } </style>

前端集成要点

  1. 替换URL:将CLOUD_FUNCTION_URL变量替换为你部署后得到的真实URL。
  2. 样式定制:你可以完全修改CSS,使其与你的博客主题风格一致。
  3. 会话管理:这里使用了基于时间戳的简单会话ID。你可以使用更持久的方式,如浏览器本地存储。
  4. 错误处理:前端对网络错误和API错误进行了基本处理,并提供了用户反馈。

8. 运行测试与效果验证

完成以上所有步骤后,让我们来验证整个流程。

1. 测试Cloud Function API你可以使用curl或 Postman 直接测试后端:

curl -X POST \ https://us-central1-your-project.cloudfunctions.net/blog-ai-chat \ -H "Content-Type: application/json" \ -d '{"query": "什么是RAG?", "session_id": "test123"}'

预期会返回一个JSON响应,包含answerrelevant_sources字段。如果返回错误,请检查:

  • GCP项目是否正确,API是否已启用。
  • 环境变量(特别是API密钥)是否正确设置。
  • Cloud Functions 和 Firestore 是否在同一个区域。
  • 查看Cloud Functions的日志(在GCP控制台)。

2. 测试前端集成将前端代码嵌入你的博客页面,打开页面,点击右下角的“AI”按钮,弹出聊天窗口。输入一个你博客文章中明确涉及的技术问题,例如“如何在Python中实现单例模式?”(假设你的博客有相关文章)。

预期成功现象

  • 问题发出后,聊天窗口会显示“正在思考...”。
  • 几秒内,你会收到一个基于你博客内容生成的、准确的回答。
  • 回答不应是通用的网络知识,而应包含你博客中的特定表述或示例。
  • 浏览器控制台(F12)的Network标签页中,可以看到一个到你的Cloud Function的POST请求,并且返回状态码为200。

3. 验证RAG是否生效问一个你博客中绝对没有涉及的话题,例如“如何修理摩托车发动机?”。一个正确配置的RAG系统应该回答“根据现有资料,我无法回答这个问题”或类似的拒绝语句,而不是凭空编造一个答案。这是检验RAG是否有效防止“幻觉”的关键测试。

9. 成本分析与优化建议

让我们拆解一下每月5-15美元的成本是如何构成的,以及如何进一步优化。

成本构成估算(以美国区域为例)

  1. Gemini API
    • gemini-1.5-flash:输入 $0.075 / 1M tokens,输出 $0.30 / 1M tokens。
    • embedding-001:$0.000125 / 1K tokens。
    • 估算:假设每日100个问题,平均每个问题+上下文+回答共消耗3000 tokens。月消耗约 100 * 3000 * 30 = 9M tokens。成本约为9 * $0.075/1M * 1M(输入) +9 * $0.30/1M * 0.3M(输出,假设回答较短) ≈$0.68 + $0.81 = $1.49。Embedding成本(仅首次构建和查询时)更低,可忽略。
  2. Cloud Functions
    • 前200万次调用/月免费,之后 $0.40 / 百万次。
    • 内存和CPU时间:256MB内存,假设每次调用运行5秒,每日100次。月计算时间 100 * 5 * 30 = 15000 秒。免费额度有40万GB-秒/月,远未用完。
    • 估算基本免费
  3. Firestore
    • 存储:假设100篇博客文章,向量化后约1000个文档,每个文档5KB,总存储约5MB。Firestore免费层级有1GB。
    • 读写操作:每日100次查询(1次读/写 per query)。月操作数3000次,远低于每日5万次读、2万次写的免费限额。
    • 估算基本免费

总计:主要成本来自Gemini API,约1.5美元/月。这里的5-15美元是一个比较宽裕的估算,包含了流量增长、使用更强大的模型(如gemini-1.5-pro)、以及额外的网络出口流量等缓冲空间。

优化建议

  • 缓存:对常见问题(FAQ)的答案可以在Cloud Function或前端进行缓存,避免重复调用模型。
  • 优化提示词:精炼的提示词可以减少不必要的token消耗。
  • 调整分块策略:更精准的分块可以减少检索时传入模型的无关上下文,降低token消耗并提升答案质量。
  • 使用向量数据库:如果知识库很大,使用Vertex AI Vector Search等专业服务,虽然会增加少量成本,但能大幅提升检索速度和精度,从而可能减少需要传入模型的上下文长度,从整体上优化成本和体验。
  • 设置预算警报:在GCP控制台为项目设置预算和警报,防止意外费用。

10. 常见问题与排查指南

在部署和使用过程中,你可能会遇到以下问题:

问题现象可能原因排查方式解决方案
部署失败gcloud命令报错1. 未安装或未登录gcloud
2. 项目ID错误或无权访问。
3. 相关API未启用。
1. 运行gcloud auth list检查登录状态。
2. 运行gcloud config get-value project检查项目。
3. 在GCP控制台检查Cloud Functions, Firestore, Vertex AI API状态。
1. 运行gcloud auth logingcloud config set project
2. 在Cloud Console启用所需API。
Cloud Function 返回 500 内部错误1. 代码运行时异常(如导入错误)。
2. 环境变量未正确设置。
3. Firestore权限不足。
1. 查看Cloud Functions日志(GCP控制台 -> Cloud Functions -> 选择函数 -> “日志”标签页)。
2. 检查日志中的Python Traceback。
1. 根据日志修正代码错误。
2. 重新部署并确认环境变量。
3. 确保服务账号拥有Firestore读写权限。
前端报跨域(CORS)错误Cloud Function 未正确设置CORS响应头。浏览器开发者工具Console或Network标签页查看错误信息。确保chat函数中包含了OPTIONS方法的CORS处理,并且Access-Control-Allow-Origin头正确设置(生产环境应指定你的博客域名)。
AI回答与博客内容无关或“幻觉”1. 检索环节失效,未找到相关文本块。
2. 提示词(Prompt)指令不够强。
3. 文本分块不合理,上下文断裂。
1. 检查retrieve_relevant_chunks函数返回的relevant_chunks内容是否相关。
2. 打印出最终发送给Gemini的完整提示词进行检查。
1. 优化文本分块策略(调整chunk_sizeoverlap)。
2. 强化提示词,使用更明确的指令,如“必须严格基于以下上下文”。
3. 考虑增加检索返回的文本块数量(limit参数)。
响应速度慢1. Cloud Function冷启动。
2. 检索逻辑在计算大量向量的相似度。
3. Gemini API响应慢。
1. 观察日志,看是否有冷启动警告。
2. 使用较小规模的知识库测试。
3. 测试直接调用Gemini API的延迟。
1. 为Cloud Function设置最小实例数(会产生费用)以减少冷启动。
2. 对于大知识库,必须迁移到向量数据库。
3. 考虑使用更快的模型,如gemini-1.5-flash
Firestore 读取次数激增retrieve_relevant_chunks函数每次查询都读取了集合中的所有文档。查看Firestore的“使用情况”面板。实现分页或缓存机制。对于生产环境,这是切换到向量数据库的最主要理由。

11. 生产环境最佳实践

如果你打算将这个助手用于有一定流量的生产博客,以下几点至关重要:

  1. 安全加固

    • API密钥管理:永远不要在前端代码中硬编码API密钥。使用Cloud Functions的环境变量或Secret Manager。
    • 访问控制:考虑移除--allow-unauthenticated,并通过博客后端服务器代理对Cloud Function的调用,或者在Cloud Function中实现基于令牌(Token)或源IP的简单认证。
    • 输入验证:在Cloud Function中,对用户输入的query进行长度和内容检查,防止注入攻击或滥用。
  2. 性能与扩展

    • 向量数据库:当知识库文档超过1000条时,务必使用专业的向量数据库服务,如Vertex AI Vector Search(原Matching Engine)或Pinecone。它们提供高效的近似最近邻(ANN)搜索,能将检索时间从线性降为对数级。
    • 异步处理:对于知识库的更新(新增博客文章),可以使用Cloud Pub/Sub触发另一个Cloud Function进行异步的向量化更新,避免阻塞主聊天接口。
    • CDN缓存:对于非常热门的问题,可以在Cloud Function前设置CDN(如Cloud CDN)缓存响应。
  3. 可观测性

    • 结构化日志:在Cloud Function中使用Python的logging模块,输出结构化的JSON日志,便于在Cloud Logging中筛选和分析。
    • 监控与告警:在GCP控制台为Cloud Functions的错误率、执行时间设置监控图表和告警策略。
    • 成本监控:如前所述,设置预算警报。
  4. 用户体验优化

    • 流式响应:Gemini API支持流式输出。你可以修改后端,使用Server-Sent Events (SSE) 或WebSocket将答案逐字返回给前端,提升交互感。
    • 引用来源:在返回答案的同时,将检索到的原文片段或文章链接也返回给前端,让用户可以追溯答案来源,增加可信度。
    • 多轮对话:扩展save_conversation逻辑,将历史对话也作为上下文的一部分传入模型,实现连贯的多轮问答。

通过以上步骤,你不仅获得了一个可运行的AI聊天助手,更掌握了一套在成本、性能和效果之间取得平衡的架构方法。这个项目的价值在于其清晰的路径和可复用的模式,你可以轻松地将知识库从博客文章替换为产品文档、公司内部Wiki或个人笔记,构建属于你自己的各类智能问答应用。

← 返回列表