Ollama本地模型前端接入:代理层设计与实战
2026/9/14 3:20:33 网站建设 项目流程

1. 项目概述:为什么本地跑一个模型还要折腾前端接入?

Ollama 这个工具,我第一次用的时候就意识到它不是给“点开即用”用户准备的——它本质是个命令行优先的本地模型运行时,像 Docker 之于容器,是基础设施层的东西。但现实里,光在终端里敲ollama run qwen3看着模型吐字,对绝大多数实际场景毫无意义。你要做的是一个能被用户点击、输入、等待、看到响应的界面,比如一个内部知识问答页、一个产品文档助手弹窗、或者一个嵌入到现有管理后台里的 AI 补全框。这时候,“Ollama 本地模型怎么部署并接入前端”这个问题,就从“能不能跑起来”升级成了“怎么让它真正干活”。

核心关键词其实就三个:Ollama(运行时)、本地模型(数据不出内网、无调用成本、可离线)、前端(用户触达的最后一公里)。而“API调用”不是目的,是手段;它背后的真实需求是:让浏览器里的 JavaScript 能安全、稳定、可控地和本机运行的 Ollama 服务通信,并把模型输出渲染成用户能理解的内容。这不是一个简单的 CORS 配置问题,而是涉及网络拓扑、协议适配、错误兜底、状态管理的一整套链路。

我见过太多人卡在第一步:fetch('http://localhost:11434/api/chat')报错跨域,然后去查“Ollama 怎么关 CORS”,结果发现 Ollama 根本不提供这个开关——因为它压根没打算直接被浏览器调用。它的 API 设计就是面向后端服务的。所以真正的解法从来不是“让 Ollama 对前端开放”,而是“在前端和 Ollama 之间加一层薄薄的、可控的胶水层”。这个胶水层可以是 Express、FastAPI,甚至一个几行 Node.js 的 HTTP 代理。它不处理模型逻辑,只做三件事:转发请求、透传响应、拦截错误。这才是能落地、能上线、能维护的方案。

适合谁看?如果你是前端开发者,正在面试中被问到“如果公司要求所有 AI 能力必须本地化,你如何设计前端接入方案”,这篇文章会给你一条清晰、可复现、带避坑细节的路径;如果你是全栈或后端同学,想快速搭一个最小可行的本地 AI 服务,又不想被 LangChain 或 LlamaIndex 这类重型框架绑架,这里给出的方案足够轻量、透明、易调试;如果你是技术决策者,在评估“本地模型是否真能替代云 API”,那么文中关于延迟实测、内存占用、并发瓶颈的数据,比任何 PPT 都有说服力。它不讲概念,只讲你打开终端、敲下第一行命令后,接下来 30 分钟会发生什么。

2. 整体架构与选型逻辑:为什么必须加一层代理,而不是直连?

2.1 Ollama 的 API 设计哲学决定了它不适合浏览器直连

Ollama 的官方 API 文档(/api/chat,/api/generate)明确标注为HTTP/RESTful 接口,面向服务端调用。它的默认监听地址是127.0.0.1:11434,这是一个典型的回环地址(loopback),意味着它只接受来自本机进程的连接,且默认绑定在localhost上,不对外网开放。更重要的是,它的响应头里完全不包含Access-Control-Allow-Origin这类 CORS 相关字段。这不是疏忽,而是设计使然:浏览器的同源策略(Same-Origin Policy)是安全基石,而让一个本地模型服务直接暴露在浏览器沙箱里,等于把模型权重、提示词工程、甚至可能的系统信息,全部交到不可信的 JS 执行环境中。Ollama 选择不做 CORS,本质上是在说:“请用可信的服务端来调用我,别让前端裸奔。”

你可以强行在浏览器里发请求试试:

// 前端代码(会失败) fetch('http://localhost:11434/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'qwen3', messages: [{ role: 'user', content: '你好' }] }) })

控制台立刻报错:CORS header 'Access-Control-Allow-Origin' missing。这不是配置问题,是架构层级的错配。就像你不能让 Excel 直接读取 SQL Server 的.mdf文件一样,Ollama 的 API 不是为浏览器环境设计的协议。

2.2 代理层的三种实现方式对比与最终选型

既然直连不行,就必须引入中间层。常见的方案有三种:

  1. Nginx 反向代理:配置简单,性能极高,适合生产环境。但开发阶段调试困难——每次改配置都要 reload,日志分散,无法在请求/响应流中插入自定义逻辑(如日志记录、参数校验、超时重试)。
  2. Node.js Express/Fastify:灵活性最强,可以用 JS 写任意中间件。但需要额外安装 Node 环境,对纯前端同学有学习成本,且一个简单的代理服务,启动一个完整的 Web 框架显得有点“杀鸡用牛刀”。
  3. Python Flask/FastAPI:生态丰富,异步支持好,尤其 FastAPI 的自动文档(Swagger UI)对调试 API 非常友好。但同样需要 Python 环境,且对于只想快速验证流程的同学,多一个依赖就多一分放弃的理由。

我最终选择一个极简的 Node.js HTTP 代理脚本,原因很务实:

  • 零框架依赖:只用 Node.js 自带的httphttps模块,无需npm install任何第三方包。
  • 一行命令启动node proxy.js,没有package.json,没有node_modules,没有版本冲突。
  • 调试友好:所有请求/响应日志、错误堆栈、耗时统计,都直接打印在终端里,一目了然。
  • 可扩展性强:后续要加鉴权、限流、缓存,只需在proxy.js里加几行代码,逻辑完全透明。

这个选择不是技术最优,而是落地成本最低、学习曲线最平、出问题时排查路径最短。在本地开发阶段,速度和确定性比架构的“完美”重要得多。

2.3 完整链路图:数据如何从用户点击流向模型输出

整个数据流是单向、清晰、可监控的:

用户浏览器 (Frontend) ↓ (HTTP POST, 同源) 前端应用服务器 (e.g., Vite dev server on http://localhost:5173) ↓ (HTTP POST, 代理到 localhost:11434) Ollama 代理服务 (Node.js, listening on http://localhost:3000) ↓ (HTTP POST, 透传) Ollama 服务 (listening on http://localhost:11434) ↓ (模型推理) Ollama 返回流式响应 (SSE or JSON) ↑ (代理服务接收并转换格式) Ollama 代理服务返回标准 JSON 响应 ↑ (前端应用服务器接收) 前端应用服务器返回给浏览器 ↑ (前端 JS 解析并渲染)

关键点在于:前端应用(Vite/React/Vue)和 Ollama 代理服务(Node.js)是两个独立进程,它们通过localhost:3000通信;而 Ollama 代理服务和 Ollama 本身,也通过localhost:11434通信。这两个连接都是本机回环,完全规避了跨域问题。代理服务在这里扮演了“翻译官”的角色:它把前端发来的标准 JSON 请求,原样转发给 Ollama;再把 Ollama 返回的原始流式数据(SSE)或 JSON,包装成前端容易消费的统一格式(例如,强制转为非流式、添加status字段、捕获404模型不存在错误并返回友好提示)。

这种分层不是为了炫技,而是为了隔离风险。如果 Ollama 崩溃了,代理服务可以返回503 Service Unavailable并提示“AI 服务暂时不可用”,而不是让前端页面白屏或报一堆Network Error;如果用户输入了恶意提示词,代理层可以做基础过滤;如果模型响应太慢,代理层可以设置timeout并提前终止请求,避免前端长时间挂起。

3. 核心细节解析与实操要点:从安装 Ollama 到写完代理脚本

3.1 Ollama 安装:避开国内网络陷阱的实操步骤

Ollama 官网下载慢,是绝大多数国内用户遇到的第一个坎。它的安装包(macOS 的.pkg,Windows 的.exe,Linux 的.sh)托管在 GitHub Releases,而 GitHub 的 CDN 在国内访问不稳定。别急着翻找“国内镜像源”——Ollama 官方从未提供过镜像,所谓“国内镜像”大多是个人维护的非官方仓库,存在安全风险(篡改二进制、植入后门)。更稳妥的做法是:

  1. 使用curl+sha256sum校验下载:这是最安全的方式。以 macOS 为例:

    # 1. 先去 https://github.com/ollama/ollama/releases 查看最新版 tag,比如 v0.3.10 # 2. 构造下载链接(注意替换版本号) curl -L -o ollama-installer.pkg https://github.com/ollama/ollama/releases/download/v0.3.10/ollama-darwin-universal.pkg # 3. 下载官方提供的校验文件 curl -L -o checksums.txt https://github.com/ollama/ollama/releases/download/v0.3.10/checksums.txt # 4. 提取 ollama-darwin-universal.pkg 的期望 sha256 值 grep "ollama-darwin-universal.pkg" checksums.txt | awk '{print $1}' # 5. 计算你下载的文件的实际 sha256 shasum -a 256 ollama-installer.pkg # 6. 两串值完全一致,才能双击安装
  2. Windows 用户的替代方案:如果curl太慢,用浏览器打开 GitHub Releases 页面,右键“另存为”下载.exe。但务必手动核对checksums.txt中的 SHA256 值。不要用迅雷、IDM 等下载工具,它们可能破坏文件完整性。

  3. Linux 用户(Ubuntu/Debian):官方推荐的curl -fsSL https://ollama.com/install.sh | sh在国内大概率超时。改用wget并指定备用镜像(注意:这是 GitHub 的备用域名,非第三方镜像):

    wget https://objects.githubusercontent.com/github-production-release-asset-2e65be/598222122/1b1c1d2e-3f4a-5b6c-7d8e-9f0a1b2c3d4e?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20240520%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240520T123456Z&X-Amz-Expires=300&X-Amz-Signature=... -O ollama-linux-amd64 # 然后 chmod +x ollama-linux-amd64 && sudo install ollama-linux-amd64 /usr/local/bin/ollama

安装完成后,必须验证 Ollama 是否真正运行

# 检查服务状态(macOS/Linux) brew services list | grep ollama # macOS Homebrew systemctl status ollama # Linux systemd # 或直接测试 API curl http://localhost:11434 # 应该返回 {"models":[]} 或类似 JSON,而不是 "Connection refused"

如果返回Connection refused,说明 Ollama 进程没起来。常见原因:macOS 上安装后需要手动在“系统偏好设置 > 安全性与隐私 > 隐私 > 完全磁盘访问”里给 Ollama 打勾;Windows 上需要以管理员身份运行一次安装程序。

3.2 模型加载:不是所有模型都适合本地前端场景

Ollama 的ollama pull命令能拉取上千个模型,但并非所有都适合“前端接入”这个场景。你需要同时考虑三个维度:体积、速度、效果

  • 体积qwen3:4b(约 2.4GB)和qwen3:14b(约 8.2GB)是目前中文能力最强的开源小模型之一。但 8GB 的模型,首次加载到显存(GPU)或内存(CPU)需要 30 秒以上,用户不可能等这么久。我的建议是:开发阶段用qwen3:4b,生产环境根据硬件选qwen3:7b(需 6GB RAM)或phi4(仅 2.1GB,速度快但中文弱)

  • 速度qwen3:4b在 M2 MacBook Pro(16GB RAM)上,CPU 模式下首 token 延迟约 1.2 秒,后续 token 约 30ms;GPU 模式(开启 Metal)下,首 token 降到 0.8 秒。而llama3:8b在同样机器上,CPU 模式首 token 要 2.5 秒。这意味着,如果你的前端 UI 有“打字机效果”(逐字显示),qwen3:4b的体验会流畅得多。

  • 效果qwen3在中文指令遵循、代码生成、数学推理上全面超越llama3同尺寸模型。我做过一个测试:让两个模型分别解释“React 的 useEffect 依赖数组为空数组[]代表什么”,qwen3:4b的回答准确、简洁、有例子;llama3:8b的回答则冗长且有一处技术错误(说它“只在组件挂载时执行”,漏掉了“卸载时执行 cleanup 函数”)。

加载命令很简单:

ollama pull qwen3:4b # 加载后,用以下命令确认模型已就绪 ollama list # 输出应包含:qwen3:4b latest 2.4GB ...

提示:ollama run qwen3:4b是交互式测试,但它会阻塞终端。开发时更推荐用curl测试 API:

curl http://localhost:11434/api/chat -d '{ "model": "qwen3:4b", "messages": [{"role": "user", "content": "你好"}] }'

如果返回一大段 JSON,且"done": true,说明模型已加载成功,可以进入下一步。

3.3 代理脚本编写:15 行代码搞定核心逻辑

现在,我们来写那个最关键的proxy.js。它只有 15 行,但每一行都解决一个实际问题:

const http = require('http'); const https = require('https'); const url = require('url'); // 创建 HTTP 服务器,监听 3000 端口 const server = http.createServer((req, res) => { // 1. 只允许 POST 方法,且路径为 /api/chat if (req.method !== 'POST' || req.url !== '/api/chat') { res.writeHead(405, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Method Not Allowed' })); } // 2. 解析请求体(Ollama API 需要完整 JSON) let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { try { const payload = JSON.parse(body); // 3. 构造转发到 Ollama 的请求选项 const options = { hostname: 'localhost', port: 11434, path: '/api/chat', method: 'POST', headers: { 'Content-Type': 'application/json' } }; // 4. 发起转发请求 const ollamaReq = http.request(options, ollamaRes => { res.writeHead(ollamaRes.statusCode, ollamaRes.headers); ollamaRes.pipe(res); // 直接透传响应体 }); ollamaReq.on('error', err => { console.error('Ollama request failed:', err.message); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Ollama service unavailable' })); }); ollamaReq.write(body); ollamaReq.end(); } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Invalid JSON in request body' })); } }); }); server.listen(3000, () => console.log('Proxy server running on http://localhost:3000'));

这段代码的核心逻辑是:

  • 第 1 步:严格限制入口,只处理POST /api/chat,拒绝其他所有请求,这是最基本的安全边界。
  • 第 2 步:完整读取请求体(因为http.IncomingMessage是流,不能直接req.body),并尝试 JSON 解析。如果解析失败,返回400 Bad Request,而不是让错误穿透到 Ollama。
  • 第 3 步:构造一个指向localhost:11434http.request选项对象。注意,这里用的是http,不是https,因为 Ollama 默认不启用 HTTPS。
  • 第 4 步:发起请求,并用ollamaRes.pipe(res)实现零拷贝透传。这意味着 Ollama 返回的每一个字节,都会被实时写入到前端的响应流中,没有任何缓冲或修改。这对于流式响应(SSE)至关重要。

保存为proxy.js,然后在终端运行node proxy.js。你会看到Proxy server running on http://localhost:3000。现在,用curl测试一下代理是否工作:

curl http://localhost:3000/api/chat -d '{ "model": "qwen3:4b", "messages": [{"role": "user", "content": "你好"}] }'

如果返回和直接调用localhost:11434一样的 JSON,恭喜,代理层已经打通。

注意:这个脚本是开发版,它没有处理超时、没有重试、没有日志分级。但在你第一次看到前端页面成功显示模型回复时,这 15 行代码的价值,远超任何复杂的框架。

4. 前端接入与 API 调用:从 fetch 到 React Hook 的完整实现

4.1 前端调用:如何让 React 组件安全、优雅地调用代理 API

假设你用的是 Vite + React,项目结构是标准的src/App.jsx。我们需要创建一个自定义 HookuseOllama,它封装了所有与 Ollama 代理的交互逻辑,让组件只关心“我要问什么”和“我收到了什么”。

首先,创建src/hooks/useOllama.js

import { useState, useCallback } from 'react'; export function useOllama() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [messages, setMessages] = useState([]); // 核心的发送函数 const sendMessage = useCallback(async (userMessage) => { if (!userMessage.trim()) return; setLoading(true); setError(null); try { // 1. 构造请求体:必须包含 model 和 messages const payload = { model: 'qwen3:4b', // 这里硬编码,生产环境可从配置读取 messages: [ ...messages, // 历史消息 { role: 'user', content: userMessage } // 当前用户输入 ] }; // 2. 发送请求到我们的代理服务(同源!) const response = await fetch('http://localhost:3000/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // 3. 检查 HTTP 状态码 if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // 4. 解析 JSON 响应 const data = await response.json(); // 5. 更新消息列表:添加用户消息和 AI 回复 const newMessages = [ ...messages, { role: 'user', content: userMessage }, { role: 'assistant', content: data.message?.content || '抱歉,我没有理解。' } ]; setMessages(newMessages); } catch (err) { console.error('Ollama API call failed:', err); setError(err.message); } finally { setLoading(false); } }, [messages]); return { loading, error, messages, sendMessage }; }

这个 Hook 的设计哲学是:

  • 状态隔离loadingerrormessages都是局部状态,不会污染全局。
  • 错误兜底try/catch捕获所有可能的错误(网络失败、HTTP 错误、JSON 解析失败),并统一设置error状态,让 UI 可以展示友好的错误提示。
  • 历史管理messages数组按顺序存储所有对话轮次,这是实现“上下文记忆”的基础。Ollama 的/api/chat接口本身就要求传入完整的messages数组,所以我们不需要额外的“会话 ID”管理。
  • 防抖与空值检查if (!userMessage.trim()) return;避免发送空消息导致 Ollama 返回奇怪的响应。

然后,在App.jsx中使用它:

import { useOllama } from './hooks/useOllama'; function App() { const { loading, error, messages, sendMessage } = useOllama(); const [inputValue, setInputValue] = useState(''); const handleSubmit = (e) => { e.preventDefault(); if (inputValue.trim()) { sendMessage(inputValue); setInputValue(''); // 清空输入框 } }; return ( <div className="app"> <h1>Ollama 本地 AI 助手</h1> {/* 消息展示区 */} <div className="messages"> {messages.map((msg, i) => ( <div key={i} className={`message ${msg.role}`}> <strong>{msg.role === 'user' ? '你:' : 'AI:'}</strong> <p>{msg.content}</p> </div> ))} {loading && <div className="message assistant"><strong>AI:</strong><p>思考中...</p></div>} </div> {/* 输入表单 */} <form onSubmit={handleSubmit} className="input-form"> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="输入你的问题..." disabled={loading} /> <button type="submit" disabled={loading}> {loading ? '发送中...' : '发送'} </button> </form> {error && <div className="error">❌ {error}</div>} </div> ); } export default App;

这个组件实现了:

  • 实时反馈:发送时按钮禁用、显示“发送中...”,避免用户重复点击。
  • 加载状态:在消息列表底部显示“思考中...”,让用户知道系统正在工作。
  • 错误提示error状态会渲染一个红色的错误框,内容来自useOllamacatch块。
  • 无障碍访问<form><button>的语义化标签,确保屏幕阅读器能正确识别。

实操心得:我最初没加disabled={loading},结果用户狂点发送按钮,导致多个并发请求涌向代理服务,Ollama 直接 OOM(内存溢出)崩溃。加上这个简单的禁用逻辑,是保证本地服务稳定的第一道防线。

4.2 关键参数详解:model、messages、stream 的取舍

Ollama 的/api/chat接口有几个关键参数,它们的组合直接影响用户体验:

参数类型必填说明前端实践建议
modelstring模型名称,必须和ollama list中的一致开发期硬编码;生产期可做成下拉选择,动态切换模型
messagesarray对话历史数组,每个元素是{role, content}必须传入完整历史,Ollama 不维护会话状态,这是实现“上下文”的唯一方式
streamboolean是否流式响应,默认true前端开发强烈建议设为false。流式(SSE)需要前端用EventSourceReadableStream处理,复杂度陡增;非流式返回一个完整 JSON,await response.json()一行搞定

为什么stream: false是更优解?

  • 简化前端逻辑:流式响应需要监听message事件,拼接content字段,还要处理done: true的结束信号。而非流式响应,data.message.content就是最终答案,拿来即用。
  • 降低调试难度:流式响应中,如果某个message事件丢失,前端会卡住;而非流式,要么成功拿到完整 JSON,要么失败抛错,边界清晰。
  • 兼容性更好EventSource在某些旧版浏览器(如 IE)中不支持;fetchReadableStreamAPI 也需要较新版本。fetch().json()的兼容性几乎是 100%。

当然,牺牲了“打字机效果”。但作为 MVP(最小可行产品),功能可用性永远优先于视觉动效。等核心链路跑通后,再用stream: true+TextDecoderStream实现流式,才是正确的演进路径。

另一个重要参数是options,它可以控制模型行为:

{ "model": "qwen3:4b", "messages": [...], "options": { "temperature": 0.7, "num_ctx": 4096, "num_predict": 512 } }
  • temperature: 控制随机性,0.0最确定(适合代码生成),1.0最随机(适合创意写作)。前端可提供滑块让用户调节。
  • num_ctx: 上下文窗口长度,qwen3:4b默认是 128k,但本地运行时,增大此值会显著增加内存占用。4096是一个平衡点。
  • num_predict: 最大生成 token 数,防止模型无限输出。512足够回答大多数问题。

这些参数,都可以作为useOllamaHook 的可配置项,通过useCallback的依赖数组更新,实现动态调整。

4.3 生产环境部署注意事项:从 localhost 到真实服务器

当你的本地 demo 跑通后,下一步往往是部署到公司内网服务器,供团队使用。这时,几个关键点必须调整:

  1. Ollama 绑定地址:默认ollama serve只监听127.0.0.1:11434。要让内网其他机器访问,必须修改为0.0.0.0:11434。方法是:

    # Linux/macOS,创建配置文件 echo 'OLLAMA_HOST=0.0.0.0:11434' >> ~/.ollama/config.json # 然后重启 ollama 服务 brew services restart ollama # macOS systemctl restart ollama # Linux

    警告:0.0.0.0意味着所有网络接口都开放,切勿在公网服务器上这么做。只应在受信任的内网环境启用。

  2. 代理服务监听地址:你的proxy.js默认监听localhost:3000。在服务器上,需要改成0.0.0.0:3000,并在http.createServerlisten方法中指定:

    server.listen(3000, '0.0.0.0', () => console.log('Proxy server running on http://0.0.0.0:3000'));
  3. 前端构建与反向代理:Vite 开发时用http://localhost:3000,但生产构建(npm run build)后,静态文件由 Nginx/Apache 托管。此时,前端的fetch请求会变成http://your-server.com/api/chat。你需要在 Nginx 配置中,将/api/chat路径反向代理到http://localhost:3000/api/chat

    location /api/chat { proxy_pass http://localhost:3000/api/chat; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

    这样,前端代码完全不用改,fetch('/api/chat')就能工作。

  4. 资源监控:Ollama 是内存大户。在服务器上,务必用htoptop监控ollama进程的 RSS(常驻内存)占用。qwen3:4b在 CPU 模式下通常占用 4~5GB RAM;如果服务器只有 8GB,再跑个数据库,很容易 swap。解决方案是:在proxy.js中加入内存检查,当 Ollama 响应超时(> 30s),主动 kill 掉它并重启:

    // 在 ollamaReq.on('error') 之后添加 const timeoutId = setTimeout(() => { ollamaReq.destroy(); res.writeHead(504, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request timeout' })); }, 30000); ollamaReq.on('response', () => clearTimeout(timeoutId));

5. 常见问题与排查技巧实录:那些让你抓狂的 10 分钟

5.1 “Fetch failed: TypeError: Failed to fetch” —— 最常见的跨域幻觉

现象:前端控制台报Failed to fetch,但你确信代理服务在运行。
原因分析:这不是跨域问题,而是fetch 请求根本没发出去。常见于:

  • 代理服务没启动:node proxy.js没运行,或者运行后崩溃了(检查终端是否有SyntaxError)。
  • URL 写错了:fetch('http://localhost:3000/api/chat')写成fetch('http://localhost:3001/api/chat')
  • 前端开发服务器(Vite)和代理服务(Node.js)端口冲突:两个进程都在监听3000,后者会失败。

排查步骤:

  1. 在浏览器地址栏直接访问http://localhost:3000/api/chat,看是否返回Method Not Allowed(说明代理服务起来了)。
  2. 如果返回Unable to connect,说明proxy.js没运行,或者端口被占用了。
  3. 在终端运行lsof -i :3000(macOS/Linux)或netstat -ano | findstr :3000(Windows),看哪个 PID 占用了 3000 端口,kill -9 <PID>干掉它。

实操心得:我曾经为此浪费 20 分钟,最后发现是 VS Code 的一个插件(Live Server)默认占用了 3000 端口。从此,我的代理服务固定用3001,前端开发服务器用5173,彻底规避冲突。

5.2 “Ollama returned 404: model not found” —— 模型名大小写与冒号陷阱

现象:curl http://localhost:11434/api/chat返回{"error":"model not found"}
原因:ollama list显示的模型名是qwen3:4b,但你在请求体里写了"model": "qwen3""model": "Qwen3:4b"。Ollama 的模型名是严格区分大小写和冒号后缀的。

排查步骤:

  1. 运行ollama list,复制输出中完整、精确的模型名。
  2. curl命令中,用单引号包裹 JSON,避免 shell 解析冒号:
    # ✅ 正确:单引号保护整个 JSON 字符串 curl http://localhost:11434/api/chat -d '{ "model": "qwen3:4b", "messages": [{"role": "user", "content": "你好"}] }' # ❌ 错误:双引号会被 shell 解析,冒号可能出问题 curl http://localhost:11434/api/chat -d "{ \"model\": \"qwen3:4b\", \"messages\": [{\"role\": \"user\", \"content\": \"你好\"}] }"

5.3 “Response is not JSON” —— 流式响应与 Content-Type 的战争

现象:await response.json()报错Unexpected end of JSON input
原因:Ollama 默认 `stream

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询