在构建现代 Web 应用时,我们常常需要集成 AI 能力来增强用户体验或实现智能化功能。然而,直接对接各大 AI 厂商的 API 往往面临诸多挑战:密钥管理分散、模型切换成本高、计费方式复杂,以及在国内网络环境下可能遇到的访问限制。OpenRouter 作为一个聚合了众多主流 AI 模型(如 GPT、Claude、Gemini 等)的统一 API 平台,为开发者提供了优雅的解决方案。而 Netlify 作为领先的 Jamstack 应用部署平台,其强大的无服务器函数(Serverless Functions)和边缘网络,是构建和部署此类 AI 应用的理想选择。
本文将手把手教你如何将 OpenRouter 集成到 Netlify 应用中,构建一个安全、高效且易于维护的开放模型访问网关。无论你是想为个人博客添加一个智能问答助手,还是为企业级应用集成 AI 能力,这套方案都能让你快速上手,并规避掉许多初期可能遇到的“坑”。
1. 核心概念与价值:为什么选择 OpenRouter + Netlify?
在深入代码之前,理解这套技术组合的核心价值至关重要。这能帮助你在后续设计和开发中做出更合理的决策。
1.1 OpenRouter:AI 模型的“统一接入层”
OpenRouter 的核心定位是 AI 模型的聚合平台。你可以将其理解为一个“超级 API 网关”,它背后连接了数十个不同的 AI 提供商。
- 统一接口:无论你想调用 OpenAI 的 GPT-4、Anthropic 的 Claude,还是 Google 的 Gemini,都只需要使用 OpenRouter 提供的同一个 API 端点(
https://openrouter.ai/api/v1/chat/completions)和相同的请求格式。这极大地简化了客户端代码。 - 模型发现与比价:OpenRouter 提供了实时的模型价格对比和性能排行榜。你可以根据预算和任务需求(如创意写作、代码生成、逻辑推理)轻松选择最具性价比的模型,而无需在多个供应商官网间反复切换。
- 简化密钥管理:你只需要保管一个 OpenRouter 的 API 密钥,即可访问其支持的所有模型。无需为每个 AI 供应商单独申请和管理密钥,降低了安全风险和运维复杂度。
- 灵活的计费:OpenRouter 采用按使用量(Token)计费的统一模式,并提供预付费信用额度,方便成本控制。
1.2 Netlify:现代化 Web 应用的“部署与运行平台”
Netlify 不仅仅是一个静态网站托管服务,它提供了一套完整的 Jamstack 开发工作流。
- 无服务器函数(Serverless Functions):这是本次集成的关键。你可以在项目中编写一个 Node.js 或 Go 函数,Netlify 会自动将其部署为一个可按需调用的 API 端点。这个函数将作为我们与 OpenRouter 通信的安全代理,避免在前端暴露敏感的 API 密钥。
- 边缘网络与高性能:Netlify 的函数运行在其全球边缘网络上,这意味着你的 AI 代理请求可以从离用户最近的节点发出,可能获得更低的延迟。
- 无缝的 Git 集成:连接你的 GitHub/GitLab 仓库后,每次
git push都会触发自动构建和部署,实现 CI/CD。 - 环境变量管理:Netlify 提供了友好的界面来管理环境变量(如 OpenRouter API Key),确保敏感信息不会进入代码仓库。
1.3 组合优势:安全、可扩展与高性能
将两者结合,我们构建的架构具有以下优势:
- 前端安全:前端应用只与部署在 Netlify 上的代理函数通信,OpenRouter 的 API 密钥安全地存储在 Netlify 的环境变量中,永远不会暴露给浏览器。
- 后端灵活:在代理函数中,我们可以轻松实现模型路由、请求格式转换、日志记录、限流、缓存等逻辑,而无需改动前端代码。
- 部署简便:整个应用(前端 + 代理 API)可以作为一个项目部署在 Netlify 上,管理简单。
- 成本可控:通过 OpenRouter 统一计费,并通过 Netlify 函数(有免费额度)控制后端调用成本。
2. 环境准备与项目初始化
在开始编码前,我们需要准备好开发环境和项目基础结构。
2.1 所需工具与账号
- Node.js:建议安装 LTS 版本(如 v18.x 或 v20.x)。这是运行本地开发服务器和 Netlify Functions 的基础。
- npm 或 yarn:包管理工具。
- Git:版本控制工具。
- 一个代码编辑器:如 VS Code。
- OpenRouter 账号:前往 OpenRouter 官网注册并获取 API 密钥。在 Dashboard 中可以找到你的密钥。
- Netlify 账号:前往 Netlify 官网,可以使用 GitHub 等账号直接登录。
- 一个 GitHub/GitLab 仓库:用于托管代码并与 Netlify 集成。
2.2 创建项目结构
我们将创建一个简单的项目,包含一个静态前端页面和一个处理 AI 请求的 Netlify Function。
打开终端,执行以下命令:
# 1. 创建一个新项目目录并进入 mkdir openrouter-netlify-demo cd openrouter-netlify-demo # 2. 初始化 package.json npm init -y # 3. 安装 Netlify CLI 工具,用于本地开发和部署 npm install -g netlify-cli # 4. 登录 Netlify (会打开浏览器授权) netlify login # 5. 在项目内初始化 Netlify 配置 netlify init执行netlify init时,CLI 会引导你:
- 选择 “Create & configure a new site”。
- 为你的站点起一个名字。
- 关联你的 Git 仓库(如果已创建)。
- 你的构建命令(我们暂时留空,或填
npm run build)。 - 发布目录(我们填
./或./dist,稍后创建)。
2.3 项目目录结构
创建以下目录和文件:
openrouter-netlify-demo/ ├── netlify/ │ └── functions/ │ └── openrouter-proxy.js # Netlify Serverless Function ├── public/ │ ├── index.html # 前端主页面 │ └── style.css # 前端样式(可选) ├── package.json └── netlify.toml # Netlify 配置文件你可以使用以下命令快速创建:
mkdir -p netlify/functions public touch netlify/functions/openrouter-proxy.js public/index.html public/style.css netlify.toml3. 配置 Netlify 与 OpenRouter
3.1 配置netlify.toml
netlify.toml是 Netlify 的核心配置文件,它告诉 Netlify 如何构建和部署你的项目。
编辑netlify.toml文件:
[build] # 因为我们是一个简单项目,可能没有构建步骤,发布目录就是 public publish = "public" # 如果你使用前端框架(如Vite, Next.js),这里需要配置对应的构建命令 # command = "npm run build" # 重定向所有未匹配静态文件的请求到函数(对于单页应用很有用) [[redirects]] from = "/*" to = "/index.html" status = 200 # 显式声明我们的函数 [functions] # 指定函数所在的目录 directory = "netlify/functions"3.2 设置环境变量(API 密钥)
安全地管理 API 密钥是重中之重。我们将在 Netlify 的站点管理界面中设置。
- 打开浏览器,登录 Netlify 。
- 进入你刚刚通过
netlify init创建的站点。 - 在顶部导航栏找到Site configuration->Environment variables。
- 点击Add variable。
- Key:
OPENROUTER_API_KEY - Value: 粘贴你从 OpenRouter 后台获取的 API 密钥。
- Key:
- (可选)如果你希望在本地开发时也能使用这个变量,可以点击Edit settings并勾选 “Allowlist for local development”。然后在本项目根目录下创建一个
.env文件(确保该文件已在.gitignore中),内容为:
Netlify CLI 在本地运行时会自动读取此文件。OPENROUTER_API_KEY=你的密钥
重要安全提醒:永远不要将.env文件或任何包含真实密钥的代码提交到 Git 仓库。netlify.toml中也不应写入密钥。
4. 编写 Netlify Function 代理
这是后端核心,负责接收前端请求,添加安全密钥,转发给 OpenRouter,并将结果返回给前端。
编辑netlify/functions/openrouter-proxy.js:
// netlify/functions/openrouter-proxy.js exports.handler = async (event, context) => { // 1. 只处理 POST 请求 if (event.httpMethod !== 'POST') { return { statusCode: 405, body: JSON.stringify({ error: 'Method Not Allowed' }), }; } try { // 2. 解析前端发送的请求体 const requestBody = JSON.parse(event.body); const { messages, model, stream = false } = requestBody; // 3. 简单的请求体验证 if (!messages || !Array.isArray(messages)) { return { statusCode: 400, body: JSON.stringify({ error: 'Invalid request: messages array is required' }), }; } // 4. 从环境变量获取 OpenRouter API 密钥 const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { console.error('OPENROUTER_API_KEY is not set in environment variables.'); return { statusCode: 500, body: JSON.stringify({ error: 'Server configuration error' }), }; } // 5. 准备转发给 OpenRouter 的请求 const openRouterRequest = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, // OpenRouter 要求注明应用信息(非强制,但推荐) 'HTTP-Referer': 'https://your-netlify-site.netlify.app', // 替换为你的站点URL 'X-Title': 'Netlify AI Demo', }, body: JSON.stringify({ model: model || 'openai/gpt-3.5-turbo', // 默认模型,可从前端传入 messages: messages, stream: stream, // 是否使用流式响应 }), }; // 6. 向 OpenRouter 发起请求 const response = await fetch('https://openrouter.ai/api/v1/chat/completions', openRouterRequest); // 7. 获取 OpenRouter 的响应 const responseData = await response.json(); // 8. 将 OpenRouter 的响应原样返回给前端 return { statusCode: response.status, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(responseData), }; } catch (error) { // 9. 错误处理 console.error('Proxy function error:', error); return { statusCode: 500, body: JSON.stringify({ error: 'Internal Server Error', details: error.message }), }; } };代码关键点解释:
exports.handler是 Netlify Function 的标准入口。- 我们通过
process.env.OPENROUTER_API_KEY安全地读取密钥。 - 函数充当了“透传代理”的角色,但加入了关键的认证头和错误处理。
- 我们添加了
HTTP-Referer和X-Title头,这是 OpenRouter 推荐的用于标识应用的方式,有助于平台分析。 - 我们支持
stream参数,为后续实现流式输出(SSE)留出了接口。
5. 构建前端交互界面
现在,我们创建一个简单的前端页面来调用这个代理函数。
编辑public/index.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OpenRouter + Netlify AI 演示</title> <link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> </head> <body> <div class="container"> <header> <h1><i class="fas fa-robot"></i> AI 对话助手</h1> <p class="subtitle">基于 OpenRouter 与 Netlify Functions 构建</p> <div class="model-selector"> <label for="model">选择模型:</label> <select id="model"> <option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo (快速、经济)</option> <option value="openai/gpt-4">GPT-4 (更强推理)</option> <option value="anthropic/claude-3-haiku">Claude 3 Haiku (快速、高效)</option> <option value="google/gemini-pro">Gemini Pro (通用性强)</option> </select> </div> </header> <main> <div class="chat-container"> <div id="chat-history" class="chat-history"> <!-- 对话历史将动态插入到这里 --> <div class="message bot"> <div class="avatar"><i class="fas fa-robot"></i></div> <div class="content">你好!我是你的 AI 助手。你可以选择上方的模型,然后开始向我提问。</div> </div> </div> <div class="input-area"> <textarea id="user-input" placeholder="输入你的问题... (Shift+Enter 换行,Enter 发送)" rows="3"></textarea> <button id="send-btn" class="send-btn"> <i class="fas fa-paper-plane"></i> 发送 </button> <button id="clear-btn" class="clear-btn"> <i class="fas fa-trash-alt"></i> 清空 </button> </div> </div> <div class="status" id="status">就绪</div> </main> <footer> <p>本演示通过 Netlify Function 安全代理调用 <a href="https://openrouter.ai" target="_blank">OpenRouter</a> API。</p> </footer> </div> <script src="app.js"></script> </body> </html>编辑public/style.css添加基本样式(为节省篇幅,此处提供核心样式,完整样式可自行扩展):
/* public/style.css */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; } .container { background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 100%; max-width: 800px; overflow: hidden; display: flex; flex-direction: column; } header, main, footer { padding: 30px; } .chat-history { flex-grow: 1; overflow-y: auto; max-height: 500px; border: 1px solid #eee; border-radius: 10px; padding: 20px; margin-bottom: 20px; background: #fafafa; } .message { display: flex; margin-bottom: 20px; } .message.user { flex-direction: row-reverse; } .message .avatar { width: 40px; height: 40px; border-radius: 50%; background: #667eea; color: white; display: flex; align-items: center; justify-content: center; margin: 0 10px; flex-shrink: 0; } .message.user .avatar { background: #764ba2; } .message .content { background: white; padding: 15px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.05); max-width: 70%; } .message.user .content { background: #667eea; color: white; } .input-area { display: flex; gap: 10px; } textarea { flex-grow: 1; padding: 15px; border: 2px solid #ddd; border-radius: 10px; font-size: 16px; resize: none; font-family: inherit; } textarea:focus { outline: none; border-color: #667eea; } button { padding: 0 25px; border: none; border-radius: 10px; font-weight: bold; cursor: pointer; font-size: 16px; transition: all 0.3s ease; } .send-btn { background: #667eea; color: white; } .clear-btn { background: #f56565; color: white; } button:hover { transform: translateY(-2px); box-shadow: 0 7px 14px rgba(0,0,0,0.1); } .status { margin-top: 15px; text-align: center; color: #666; font-size: 0.9em; min-height: 1.2em; }创建public/app.js来处理前端逻辑:
// public/app.js document.addEventListener('DOMContentLoaded', () => { const chatHistory = document.getElementById('chat-history'); const userInput = document.getElementById('user-input'); const sendBtn = document.getElementById('send-btn'); const clearBtn = document.getElementById('clear-btn'); const modelSelect = document.getElementById('model'); const statusEl = document.getElementById('status'); // Netlify Function 的端点路径 // 本地开发时,Netlify CLI 会自动在 `http://localhost:8888/.netlify/functions/openrouter-proxy` 提供此函数 // 部署后,路径为 `/.netlify/functions/openrouter-proxy` const API_PATH = '/.netlify/functions/openrouter-proxy'; // 添加消息到聊天历史 function addMessage(role, content) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; // 'user' 或 'bot' const avatar = document.createElement('div'); avatar.className = 'avatar'; avatar.innerHTML = role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>'; const contentDiv = document.createElement('div'); contentDiv.className = 'content'; // 简单处理换行,更复杂的内容可以用 marked.js 等库渲染 Markdown contentDiv.textContent = content; messageDiv.appendChild(avatar); messageDiv.appendChild(contentDiv); chatHistory.appendChild(messageDiv); // 滚动到底部 chatHistory.scrollTop = chatHistory.scrollHeight; } // 更新状态提示 function updateStatus(text, isError = false) { statusEl.textContent = text; statusEl.style.color = isError ? '#f56565' : '#666'; } // 发送消息到代理函数 async function sendMessage() { const userText = userInput.value.trim(); if (!userText) return; // 清空输入框并禁用 userInput.value = ''; userInput.disabled = true; sendBtn.disabled = true; updateStatus('思考中...'); // 先将用户消息显示在界面上 addMessage('user', userText); // 构建请求数据 const requestData = { model: modelSelect.value, messages: [ // 可以在此处添加系统提示词,例如: // { role: "system", "content": "你是一个乐于助人的助手。"}, { role: "user", content: userText } ], stream: false // 本次示例不使用流式,设为 true 并配合 EventSource 可实现打字机效果 }; try { const response = await fetch(API_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestData) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `HTTP ${response.status}`); } // 从 OpenRouter 响应中提取 AI 回复 const aiReply = data.choices?.[0]?.message?.content; if (aiReply) { addMessage('bot', aiReply); updateStatus('就绪'); } else { throw new Error('未收到有效的回复内容。'); } } catch (error) { console.error('请求失败:', error); addMessage('bot', `抱歉,出错了: ${error.message}`); updateStatus(`请求失败: ${error.message}`, true); } finally { // 重新启用输入 userInput.disabled = false; sendBtn.disabled = false; userInput.focus(); } } // 事件监听 sendBtn.addEventListener('click', sendMessage); userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); // 防止换行 sendMessage(); } }); clearBtn.addEventListener('click', () => { // 保留第一条欢迎消息 const welcomeMsg = chatHistory.querySelector('.message.bot'); chatHistory.innerHTML = ''; if (welcomeMsg) { chatHistory.appendChild(welcomeMsg); } updateStatus('对话已清空'); }); // 初始焦点 userInput.focus(); });6. 本地运行与测试
在部署到线上之前,我们先在本地测试整个流程。
启动本地开发服务器: 在项目根目录下运行:
netlify devNetlify CLI 会启动一个本地服务器(通常是
http://localhost:8888),并自动加载你的环境变量(如果配置了.env文件)和 Functions。测试功能:
- 打开浏览器,访问
http://localhost:8888。 - 在输入框中提问,例如“用 Python 写一个快速排序函数”。
- 点击发送,观察聊天历史区域。你应该能看到你的问题,然后稍等片刻,AI 的回复会出现。
- 尝试切换不同的模型,感受回复速度和风格的差异。
- 打开浏览器,访问
检查日志: 在运行
netlify dev的终端里,你可以看到详细的请求和函数执行日志,方便调试。
7. 部署到 Netlify
本地测试无误后,就可以部署到生产环境了。
提交代码到 Git:
git add . git commit -m "feat: initial openrouter netlify integration" git push origin main自动部署: 由于我们在项目初始化时已经将 Netlify 站点与 Git 仓库关联,
git push后 Netlify 会自动开始构建和部署。 你可以在 Netlify 站点的Deploys面板查看部署进度。访问线上站点: 部署成功后,Netlify 会为你生成一个唯一的站点 URL(如
https://your-site-name.netlify.app)。打开这个 URL,你的 AI 应用就已经在公网可用了!
8. 常见问题与排查思路
在集成过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
前端点击发送后无反应,控制台报404或500错误。 | 1. Netlify Function 路径错误。 2. Function 代码存在语法错误。 3. 环境变量未正确设置。 | 1. 检查app.js中的API_PATH是否正确(应为/.netlify/functions/openrouter-proxy)。2. 在 Netlify 站点的Functions日志面板查看具体错误信息。 3. 确认 Netlify 环境变量 OPENROUTER_API_KEY已设置且无误。本地开发时检查.env文件。 |
请求返回401 Unauthorized或Invalid API Key。 | OpenRouter API 密钥无效或未正确传递。 | 1. 登录 OpenRouter 确认 API 密钥有效且未过期。 2. 在 Netlify Function 中打印 process.env.OPENROUTER_API_KEY的前几位,确认已成功读取(注意:不要在日志中打印完整密钥)。3. 检查 Function 代码中 Authorization头的格式是否正确(Bearer ${apiKey})。 |
| 请求超时或响应缓慢。 | 1. 选择的模型本身响应慢。 2. Netlify Function 冷启动。 3. 网络问题。 | 1. 尝试切换到更快的模型(如gpt-3.5-turbo或claude-3-haiku)。2. Netlify 免费计划的函数有冷启动时间。可以考虑使用付费计划或优化函数代码(如保持简单,避免复杂初始化)。 3. 在 OpenRouter 的模型页面上查看各模型的平均响应时间。 |
| 前端能收到响应,但内容是空的或格式错误。 | 1. OpenRouter 响应结构解析错误。 2. 模型暂时不可用或返回了错误。 | 1. 在 Function 中打印responseData,检查其结构是否与预期一致(应有choices[0].message.content)。2. 查看 OpenRouter 响应中是否有 error字段。 |
本地netlify dev运行正常,但部署后出错。 | 1. 构建或发布目录配置错误。 2. 生产环境与开发环境差异。 | 1. 检查netlify.toml中的publish目录是否正确指向public。2. 确保生产环境的环境变量已正确设置,且与本地 .env文件内容一致。 |
9. 进阶优化与最佳实践
基础功能跑通后,可以考虑以下优化,使你的应用更健壮、更强大。
9.1 实现流式响应 (Streaming)
当前的实现是等待 AI 生成完整回复后再一次性返回。要实现类似 ChatGPT 的打字机效果,需要使用流式传输。
- 修改 Netlify Function:在请求 OpenRouter 时,设置
stream: true。 - 修改前端请求:使用
EventSource或fetch的流式 API 来读取分块数据。 - 优势:大幅提升用户体验,感觉响应更快,尤其生成长文本时。
9.2 添加对话历史与上下文管理
目前每次请求只发送单条消息。要让 AI 记住对话上下文,需要在每次请求时携带整个对话历史。
- 前端管理:在
app.js中维护一个messages数组,每次用户发送新消息时,将{role: 'user', content}加入数组,然后将整个数组发送给后端。收到 AI 回复后,再将{role: 'assistant', content}加入数组。 - 注意 Token 限制:上下文长度有限制(如 4096 tokens)。需要实现一个简单的逻辑,当历史消息总长度估计超过限制时,丢弃最早的消息或进行摘要。
9.3 增强安全性与限流
- API 密钥安全:确保密钥仅存在于 Netlify 环境变量中,这是最基本的安全措施。
- 请求验证:在 Netlify Function 中,可以验证请求来源(如检查
event.headers.origin),防止未授权的网站调用你的代理。 - 限流 (Rate Limiting):在 Function 中集成简单的限流逻辑(例如使用内存对象或连接外部 Redis),防止 API 被滥用导致费用激增。Netlify 也提供了一些高级的边缘函数能力来实现更复杂的限流。
9.4 利用 Netlify 的边缘能力
Netlify Functions 默认运行在边缘网络。你还可以探索:
- Edge Functions:使用 Deno 编写运行在更靠近用户位置的超低延迟函数。
- Netlify AI Gateway:这是一个新特性(与本文的 OpenRouter 网关是不同概念),它提供了缓存、降级、重试等机制来优化 AI API 调用,未来可以考虑将 OpenRouter 的调用通过 AI Gateway 来管理,进一步提升稳定性和成本效益。
9.5 监控与日志
- OpenRouter 仪表盘:定期查看 OpenRouter 的用量和费用分析。
- Netlify 分析:在 Netlify 站点面板查看 Function 的调用次数、耗时和错误率。
- 自定义日志:在 Function 的关键步骤(如收到请求、转发请求、发生错误)添加
console.log语句,这些日志可以在 Netlify 的 Function 日志中查看,是排查问题的宝贵依据。
通过以上步骤,你不仅成功搭建了一个可用的 AI 应用,更掌握了一套安全、可扩展的集成模式。这套模式的核心思想——通过无服务器函数代理敏感 API 调用——可以广泛应用于任何需要在前端集成第三方 API 的场景,如支付、地图、短信服务等。