找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2799

积分

1

好友

388

主题
发表于 4 天前 | 查看: 16| 回复: 0

本文介绍如何使用 Ollama、FastAPI 和 React 三个核心工具,从零构建一个支持流式输出的本地大语言模型应用。整个架构可以类比为 LLM(数据库)+ FastAPI(服务端)+ React(前端)。

使用工具简介

  • Ollama:一个免费开源框架,能够让你轻松地在本地计算机上运行大型语言模型。
  • FastAPI:一个用于构建 API 的现代、快速(高性能)的 Web 框架,基于 Python 类型提示标准。
  • React:一个用于通过组件构建用户界面的 前端框架 库。

实现步骤

步骤 1:运行 Ollama 与大模型

首先,下载并安装 Ollama。安装完成后,通过命令行拉取并运行你选择的模型。例如,运行最新的 Llama3 8B 模型:

ollama run llama3:8b

对于个人电脑,10B 参数以内的模型通常可以流畅运行。你也可以下载多个模型进行对比,例如通义千问(qwen)。一般来说,同一系列模型参数量越大,能力越强;而许多国内模型对中文的支持通常会更好一些。

步骤 2:测试模型基础调用

模型成功运行后,其服务默认在 11434 端口启动。你可以通过简单的 curl 命令测试模型的基础生成功能:

curl http://localhost:11434/api/generate -d '{
  "model": "llama3:8b",
  "prompt": "Why is the sky blue?",
  "stream": false
}'

步骤 3:构建 FastAPI 后端服务

新建一个 Python 项目,创建主应用文件(如 main.py),实现以下核心代码。这个后端主要负责提供模型列表接口和基于 SSE 的流式对话接口。

import uvicorn
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import json
import requests
from sse_starlette.sse import EventSourceResponse
import asyncio
import aiohttp

app = FastAPI(debug=True)

origins = [
    "http://localhost",
    # 输入自己前端项目的地址
]

# 设置跨域
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

urls = ["http://localhost:11434/api/generate"]

llm_list = [ {'label': 'qwen:latest', "value": 'qwen:latest'},
            {'label': 'llama3:8b', "value": 'llama3:8b'}, ]

# 获取模型列表
@app.get("/llm/list")
def read_llm(model: str = 'qwen:latest'):
    return {"data": llm_list}

# 这是一个异步生成器函数,它发送请求到 Ollama,并逐行读取响应内容,生成事件流。
async def stream_ollama_response(model_name, prompt):
    if model_name:
        url = urls[0]
        payload = {
            "model": model_name,
            "prompt": prompt,
            "stream": True
        }
        async with aiohttp.ClientSession() as session:
            async with session.post(url, json=payload) as response:
                async for line in response.content:
                    if line:
                        data = line.decode('utf-8').strip()
                        if data:
                            yield {"event": "message", "data": json.loads(data)["response"]}

# 开始对话,接收 model_name 和 prompt 参数。它调用 event_generator 函数,启动与 Ollama 的交互,并通过 EventSourceResponse 返回事件流
@app.get("/chat")
async def generate(request: Request, model_name: str = 'qwen:latest',
                   prompt: str = '请用中文介绍下中国古代四大名著之一的《红楼梦》'):
    async def event_generator():
        async for event in stream_ollama_response(model_name, prompt):
            yield event
            if await request.is_disconnected():
                break

    return EventSourceResponse(event_generator())

if __name__ == '__main__':
    uvicorn.run(app="app", host="127.0.0.1", port=8000, reload=True)

以上代码使用 Server-Sent Events (SSE) 实现了流式输出。这是一种简单高效的服务器向客户端推送数据的方式。

步骤 4:构建 React 前端界面

新建一个 React 项目。这里示例使用了 Ant Design 组件库和 @microsoft/fetch-event-source 库来优雅地处理 SSE 连接。

import { Input, Dropdown, Select, Form, Button, Space } from 'antd';
import { useEffect, useState } from 'react';
import { getList, chat } from './service';
import { useRequest } from '@umijs/max';
import { fetchEventSource } from '@microsoft/fetch-event-source';

const { TextArea } = Input;

// 不能走代理哦,走了代理流式就失效了,?- ?
export const getHost = () => {
  const isDev = process.env.NODE_ENV === 'development';
  if (isDev) {
    return 'http://127.0.0.1:8000';
  } else {
    return '';
  }
};

export default () => {
  const [form] = Form.useForm();
  const { data = [] } = useRequest(getList);
  const [value, setValue] = useState('');
  const [start, setStart] = useState(null);
  const [end, setEnd] = useState(null);
  const [selected, setSelected] = useState(false);
  const [controller, setController] = useState(new AbortController());

  const sharedProps = {
    style: { width: '100%' },
    autoSize: { minRows: 3, maxRows: 20 },
    onChange: (e) => {
      setValue(e.target.value);
      setStart(e.target.selectionStart);
    },
    onClick: (e) => {
      setStart(e.target.selectionStart);
    },
    onSelect: (e) => {
      setStart(e.target.selectionStart);
      setEnd(e.target.selectionEnd);
      setSelected(e.target.value.substring(e.target.selectionStart, e.target.selectionEnd));
    },
  };

  const items = [
    {
      label: '重写这句话',
      key: '3',
    },
    {
      label: '把这句话翻译成中文',
      key: '4',
    },
  ];

  const menuClick = ({ key }) => {
    switch (key) {
      case '3':
        return reWrite();
      case '4':
        return reWrite('zh-CN');
    }
  };

  const reWrite = async (type) => {
    if (!selected) {
      return;
    }
    setValue(value.slice(0, start) + '重写中。。。' + value.slice(end));
    const res = await chat({
      model: form.getFieldValue('model'),
      prompt: type
        ? `${selected}”把“”中的这句话或单词翻译成中文,返回不要带格式,直接返回翻译结果`
        : selected,
    });
    setValue(value.slice(0, start) + res.data + value.slice(end));
  };

// 获取数据流
  const fetchData = async (url) => {
    await fetchEventSource(url, {
      method: 'GET',
      signal: controller.signal,
      onopen(res) {
        if (res.ok && res.status === 200) {
          console.log('Connection made ', res);
        } else if (res.status >= 400 && res.status < 500 && res.status !== 429) {
          errorHandler(res);
          console.log('Client side error ', res);
        }
      },
      onmessage(event) {
        console.log(event);
        setValue((data) => [...data, event.data].join(''));
      },
      onclose() {
        console.log('Connection closed by the server');
      },
      onerror(err) {
        console.log('There was an error from server', err);
      },
    });
  };

  const onFinish = (values) => {
    fetchData(`${getHost()}/chat?model_name=${values.model_name}&prompt=${values.prompt}`);
  };

  return (
    <div>
      <Form onFinish={onFinish} form={form}>
        <Form.Item name="model_name" label="模型">
          <Select style={{ width: 200 }} options={[...data]} />
        </Form.Item>
        <Form.Item name="prompt" label="提问">
          <Input />
        </Form.Item>
        <Form.Item>
          <Space>
            <Button type="primary" htmlType="submit">
              提交
            </Button>
            <Button onClick={() => controller.abort()}>
              暂停
            </Button>
          </Space>
        </Form.Item>
      </Form>
      <Dropdown menu={{ items, onClick: menuClick }} trigger={['contextMenu']}>
        <TextArea value={value} {...sharedProps} />
      </Dropdown>
    </div>
  );
};

前端界面提供了一个简单的表单用于选择模型和输入问题,并通过 SSE 实时接收并显示模型生成的内容,实现了流畅的对话体验。

总结

通过以上四个步骤,我们成功搭建了一个完整的、支持流式输出的本地大模型应用。该方案将强大的 Ollama 本地模型、高效的 FastAPI 后端以及灵活的 React 前端相结合,为你探索和开发本地 AI 应用提供了一个坚实的起点。你可以在此基础上继续扩展,例如添加对话历史、实现工具调用(Function Calling)或尝试 WebSocket 等其他通信协议。

希望这篇教程能帮助你快速上手。如果你在构建过程中遇到问题,或者有更多关于架构设计的心得,欢迎在技术社区如 云栈社区 进行交流与分享。




上一篇:Ubuntu Budgie 25.10:结合Ubuntu稳定生态与Budgie简洁桌面的官方发行版
下一篇:Spring Boot单元测试与集成测试:Spring Test从入门到实战详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:48 , Processed in 0.389674 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表