下载安装Python

下载【离线安装包】直接点击https://www.python.org/ftp/python/3.15.0/python-3.15.0-amd64.exe

开始安装Python





下载安装Ollama
具体流程参考这个文章
下载安装千问大模型(qwen2.5:3b)
运行cmd
输入代码
ollama pull qwen2.5:3b
回车

根据网速快慢等待约10~20分钟安装完毕
下载安装VSCODE
安装过程超简单这里略过截图
新建项目文件夹
起什么名字无所谓,这里我用AgentDemo
打开VSCODE-文件-打开文件夹-找到AgentDemo


初始化Python项目
Ctrl+J打开底部【终端】窗口

输入
bash
python -m pip install openai fastapi uvicorn
回车

编写并运行main.py代码

python
from fastapi import FastAPI, Query, Body
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from openai import AsyncOpenAI
app = FastAPI(title="本地大模型流式接口")
# 解决跨域,教学演示允许全部来源
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# 对接ollama AsyncOpenAI兼容接口
client = AsyncOpenAI(base_url="http://localhost:11434/v1/", api_key="demo")
async def stream_llm_response(user_msg: str):
# AsyncOpenAI sdk stream是异步,放到线程执行避免阻塞事件循环
stream = await client.chat.completions.create(
model="qwen2.5:3b",
messages=[
{"role": "system", "content": "你是一个助教,回答简洁易懂,适合学生学习"},
{"role": "user", "content": user_msg},
],
stream=True,
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
text = chunk.choices[0].delta.content
print(text, end="", flush=True)
yield f"data: {text}\n\n"
yield "data: [DONE]\n\n"
@app.api_route("/chat/stream", methods=["GET", "POST"])
async def chat_stream(
q: str | None = Query(default=None),
q_post: str | None = Body(default=None, embed=True),
):
"""
GET请求示例:
http://127.0.0.1:8000/chat/stream?q=你的问题
POST请求示例(json body):
POST http://127.0.0.1:8000/chat/stream
Content-Type: application/json
{
"q_post": "你的问题"
}
"""
# 优先取post参数,没有再取get查询参数
user_query = q_post if q_post is not None else q
if not user_query:
return StreamingResponse(
iter(["data: 缺少提问参数 q\n\ndata: [DONE]\n\n"]),
media_type="text/event-stream",
)
return StreamingResponse(
stream_llm_response(user_query), media_type="text/event-stream"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

在底部【终端】输入
python
python main.py
回车
此刻,本地调用大模型的接口已经启动了
创建并运行前端页面
在左侧新建index.html文件
python
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>本地大模型演示</title>
<style>
body {
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
font-family: system-ui;
}
#input-box {
width: 100%;
height: 80px;
font-size: 16px;
padding:10px;
box-sizing: border-box;
}
#btn-send {
margin-top:10px;
padding:8px 24px;
font-size:16px;
cursor:pointer;
}
#output-area{
margin-top:20px;
padding:12px;
border:1px solid #ccc;
min-height:200px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h3>本地Ollama大模型演示(回车 / 按钮发送,Shift+Enter换行)</h3>
<textarea id="input-box" placeholder="请输入你的问题"></textarea>
<br>
<button id="btn-send">发送</button>
<div id="output-area"></div>
<script>
const inputEl = document.getElementById("input-box");
const outputEl = document.getElementById("output-area");
const sendBtn = document.getElementById("btn-send");
let es = null;
// 封装发送逻辑,回车和按钮共用一套代码
function doSend(){
const question = inputEl.value.trim();
if(!question) return;
// 关闭上一次SSE连接
if(es){
es.close();
}
outputEl.innerText = "";
inputEl.disabled = true;
sendBtn.disabled = true;
const url = `http://127.0.0.1:8000/chat/stream?q=${encodeURIComponent(question)}`;
es = new EventSource(url);
es.onmessage = function(event){
if(event.data === "[DONE]"){
es.close();
inputEl.disabled = false;
sendBtn.disabled = false;
return;
}
outputEl.innerText += event.data;
}
es.onerror = function(){
outputEl.innerText += "\n\n【连接出错,请确认ollama和后端服务已经启动】";
es.close();
inputEl.disabled = false;
sendBtn.disabled = false;
}
}
// 回车发送
inputEl.addEventListener("keydown", function (e) {
if(e.key === 'Enter' && !e.shiftKey){
e.preventDefault();
doSend();
}
})
// 点击按钮发送
sendBtn.addEventListener("click", doSend);
</script>
</body>
</html>
然后Ctrl+X安装插件

聚焦到index.html文件
使用快捷键Atl+L Atl+O 或者右键选择【Open with Live Server】

会运行打开http://127.0.0.1:5500/index.html网页
输入你的问题,大模型会回复你

至此一个超级简单的调用本地大模型的demo完成!
进阶:
这里前端使用的
EventSource痛点
EventSource 原生是 GET 请求,不能自定义 header,如果你要传 token 就很麻烦(例如你要写一个接口是需要验证用户登录没有,才提供答案回复); 如果后面你遇到:CORS 配置好了,普通接口 ok,SSE 还是跨域:
| 方案 | 请求方式 | 底层 API | 优点 | 缺点 |
|---|---|---|---|---|
| EventSource | 仅 GET | 浏览器内置 | 代码简短,自带断线重连 | 不能 POST,参数暴露 URL,不能自定义请求头 |
| fetch+ReadableStream | POST / GET | fetch + 流读取 | 支持 POST,参数放请求体,更规范 | 需要手动解析 SSE 字符串,代码多一点,没有自动重连 |
在实际的商用项目里面,我们极力推荐前端使用POST方式请求,这里就需要对index.html代码做调整,代码如下
index.html
python
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>本地大模型演示 POST流式</title>
<style>
body {
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
font-family: system-ui;
}
#input-box {
width: 100%;
height: 80px;
font-size: 16px;
padding:10px;
box-sizing: border-box;
}
#btn-send {
margin-top:10px;
padding:8px 24px;
font-size:16px;
cursor:pointer;
}
#output-area{
margin-top:20px;
padding:12px;
border:1px solid #ccc;
min-height:200px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h3>本地Ollama大模型演示(POST流式,回车 / 按钮发送,Shift+Enter换行)</h3>
<textarea id="input-box" placeholder="请输入你的问题"></textarea>
<br>
<button id="btn-send">发送</button>
<div id="output-area"></div>
<script>
const inputEl = document.getElementById("input-box");
const outputEl = document.getElementById("output-area");
const sendBtn = document.getElementById("btn-send");
let abortController = null; // 用于中断fetch流
// 封装发送逻辑,改用 fetch POST 读取SSE流
async function doSend(){
const question = inputEl.value.trim();
if(!question) return;
// 如果上一次请求还在运行,直接终止
if(abortController){
abortController.abort();
}
abortController = new AbortController();
outputEl.innerText = "";
inputEl.disabled = true;
sendBtn.disabled = true;
try {
const resp = await fetch("http://127.0.0.1:8000/chat/stream", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
q_post: question
}),
signal: abortController.signal
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
// 获取可读流
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while(true){
const {done, value} = await reader.read();
if(done) break;
buffer += decoder.decode(value, {stream:true});
// 按SSE分隔符 \n\n 切分一条条事件
const parts = buffer.split("\n\n");
buffer = parts.pop(); // 剩下不完整片段留在buffer
for(const part of parts){
if(!part.trim()) continue;
// SSE格式 data: xxx
if(part.startsWith("data: ")){
const data = part.substring("data: ".length).trimEnd();
if(data === "[DONE]"){
// 流式结束
reader.cancel();
return;
}
outputEl.innerText += data;
}
}
}
} catch(err){
if(err.name !== "AbortError"){
outputEl.innerText += "\n\n【连接出错,请确认ollama和后端服务已经启动】";
console.error(err);
}
} finally {
inputEl.disabled = false;
sendBtn.disabled = false;
abortController = null;
}
}
// 回车发送
inputEl.addEventListener("keydown", function (e) {
if(e.key === 'Enter' && !e.shiftKey){
e.preventDefault();
doSend();
}
})
// 点击按钮发送
sendBtn.addEventListener("click", doSend);
</script>
</body>
</html>
重新运行main.py和index.html
也可以实现同样的提问回复效果

