news 2026/3/15 13:06:34

bge-large-zh-v1.5代码实例:FastAPI封装embedding服务并添加鉴权

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
bge-large-zh-v1.5代码实例:FastAPI封装embedding服务并添加鉴权

bge-large-zh-v1.5代码实例:FastAPI封装embedding服务并添加鉴权

1. 为什么需要自己封装embedding服务

你可能已经用过现成的embedding服务,比如通过sglang直接暴露的OpenAI兼容接口。但实际项目中,你会发现几个绕不开的问题:接口没有权限控制,任何人都能调用;返回格式和业务系统不匹配;缺少统一的日志、监控和错误处理;无法灵活扩展预处理或后处理逻辑。

这时候,一个轻量、可控、可维护的封装层就变得特别重要。本文不讲大道理,只带你一步步用FastAPI把bge-large-zh-v1.5这个高质量中文embedding模型包装成一个带鉴权、易集成、生产就绪的服务——所有代码都能直接运行,每一步都有明确目的,不堆砌概念,不绕弯子。

2. bge-large-zh-v1.5模型能力再认识

bge-large-zh-v1.5不是“又一个中文向量模型”,它是当前中文语义理解任务中精度和鲁棒性兼顾得比较好的选择之一。它不是靠参数量堆出来的,而是靠更精细的训练策略和中文语料优化出来的。

它真正好用的地方,藏在细节里:

  • 不是所有512长度都一样:它对长文本的截断和注意力分配做了专门优化,比如处理一段300字的产品描述时,不会像某些模型那样把关键卖点“稀释”掉;
  • 向量不是越长越好,而是越准越好:它输出1024维向量,但每一维都在为语义区分服务,实测在电商搜索、法律条款比对、客服工单聚类等任务中,余弦相似度排序稳定性明显高于同级别模型;
  • 中文不是英文的影子:它没用简单翻译英文指令微调,而是用大量真实中文对话、百科、技术文档做对比学习,所以对“微信小程序怎么接入支付”和“微信支付接入小程序流程”这类语义近似但字面差异大的query,召回更稳。

这些能力,只有当你把它真正放进自己的服务链路里,才能持续发挥价值。而封装,就是让它从“能用”变成“好用”的第一步。

3. 基于sglang部署的embedding服务现状

你已经在服务器上用sglang成功启动了bge-large-zh-v1.5,这很好。现在它正通过http://localhost:30000/v1提供OpenAI风格的embedding接口,api_key="EMPTY"只是占位符,实际没有任何校验。

我们来快速验证一下它是否真的在工作:

cd /root/workspace cat sglang.log

如果日志末尾出现类似INFO: Uvicorn running on http://0.0.0.0:30000Loaded model: bge-large-zh-v1.5,说明服务已就绪。

再用Jupyter快速测试一次调用:

import openai client = openai.Client( base_url="http://localhost:30000/v1", api_key="EMPTY" ) response = client.embeddings.create( model="bge-large-zh-v1.5", input="今天天气不错,适合写代码" ) print(f"向量维度:{len(response.data[0].embedding)}") print(f"前5个值:{response.data[0].embedding[:5]}")

你会看到输出一个长度为1024的浮点数列表——这就是bge-large-zh-v1.5为你生成的语义指纹。

但请注意:这个接口是裸奔的。没有身份识别,没有调用频控,没有请求审计,也没有适配你内部系统的字段命名。它是一辆性能不错的车,但还没有方向盘、刹车和仪表盘。

4. FastAPI封装:从零开始构建安全embedding服务

我们不用重写模型推理逻辑,而是站在sglang肩膀上,加一层“智能网关”。这一层要完成三件事:鉴权拦截、请求适配、响应标准化。

4.1 环境准备与依赖安装

新建一个项目目录,安装核心依赖:

mkdir embedding-api && cd embedding-api pip install fastapi uvicorn python-jose[cryptography] passlib python-multipart

这里选python-jose而不是pyjwt,是因为它对中文字符和特殊符号的兼容性更好,避免后续在token解析时出莫名其妙的编码错误。

4.2 配置管理:把密钥和地址从代码里摘出来

创建config.py,统一管理所有可变参数:

# config.py from pydantic import BaseSettings class Settings(BaseSettings): # sglang服务地址(必须和你实际部署的一致) EMBEDDING_BASE_URL: str = "http://localhost:30000/v1" # API密钥(生产环境建议从环境变量读取) API_KEY: str = "your-secret-api-key-here" # JWT密钥(用于生成和校验token) SECRET_KEY: str = "change-this-in-production-very-very-very-long-key" # token有效期(小时) ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24小时 class Config: case_sensitive = False settings = Settings()

这样做的好处是:换环境只需改配置,不用动一行业务逻辑。

4.3 鉴权模块:用JWT实现轻量级身份验证

创建auth.py,实现标准的Bearer Token校验:

# auth.py from datetime import datetime, timedelta from typing import Optional, Dict, Any from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from passlib.context import CryptContext from config import settings oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_api_key(api_key: str) -> bool: """校验原始API密钥(用于首次登录)""" return api_key == settings.API_KEY def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """生成JWT token""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256") return encoded_jwt async def get_current_user(token: str = Depends(oauth2_scheme)) -> str: """从token中解析用户标识(这里简化为固定字符串)""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无法验证凭据", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception except JWTError: raise credentials_exception return user_id

注意:这里没有引入数据库用户表,因为大多数内部服务只需要“有密钥就能用”,够用且轻量。

4.4 核心服务:调用sglang并标准化响应

创建services/embedding_service.py,专注做一件事:把OpenAI格式转成你想要的格式。

# services/embedding_service.py import httpx from typing import List, Dict, Any from config import settings async def get_embedding(text: str, model_name: str = "bge-large-zh-v1.5") -> List[float]: """ 调用sglang embedding服务,返回纯向量列表 """ async with httpx.AsyncClient() as client: try: response = await client.post( f"{settings.EMBEDDING_BASE_URL}/embeddings", json={ "model": model_name, "input": text }, timeout=30.0 ) response.raise_for_status() data = response.json() # 提取第一个结果的向量(支持批量输入时可扩展) return data["data"][0]["embedding"] except httpx.HTTPStatusError as e: raise HTTPException( status_code=e.response.status_code, detail=f"sglang服务返回错误: {e.response.text}" ) except Exception as e: raise HTTPException( status_code=500, detail=f"调用embedding服务失败: {str(e)}" ) # 批量处理函数(可选,按需启用) async def get_embeddings_batch(texts: List[str], model_name: str = "bge-large-zh-v1.5") -> List[List[float]]: async with httpx.AsyncClient() as client: try: response = await client.post( f"{settings.EMBEDDING_BASE_URL}/embeddings", json={ "model": model_name, "input": texts }, timeout=60.0 ) response.raise_for_status() data = response.json() return [item["embedding"] for item in data["data"]] except Exception as e: raise HTTPException(status_code=500, detail=str(e))

这个模块的关键设计点:

  • 使用httpx.AsyncClient保持异步非阻塞,避免拖慢整个API;
  • 错误分类处理:网络错误、HTTP错误、JSON解析错误,分别给出清晰提示;
  • 返回值是干净的List[float],不带任何OpenAI的wrapper字段,下游系统拿来就能用。

4.5 主应用:组装路由与中间件

创建main.py,这是整个服务的入口:

# main.py from fastapi import FastAPI, Depends, HTTPException, status, Request from fastapi.responses import JSONResponse from typing import List, Dict, Any from auth import get_current_user, create_access_token, verify_api_key from services.embedding_service import get_embedding, get_embeddings_batch from config import settings import time app = FastAPI( title="BGE中文Embedding服务", description="基于bge-large-zh-v1.5的FastAPI封装,含JWT鉴权与标准化响应", version="1.0.0" ) # 全局请求耗时记录中间件 @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response # 登录接口:用原始API密钥换取JWT token @app.post("/token", summary="获取访问令牌") async def login_for_access_token(api_key: str): if not verify_api_key(api_key): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的API密钥", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": "embedding-service-user"}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} # 单文本embedding接口 @app.post("/v1/embeddings", summary="获取单文本向量") async def embed_text( request: Dict[str, Any], current_user: str = Depends(get_current_user) ): """ 输入:{"text": "待向量化文本", "model": "可选,默认bge-large-zh-v1.5"} 输出:{"vector": [0.12, -0.45, ...], "dimension": 1024, "model": "bge-large-zh-v1.5"} """ text = request.get("text") if not text or not isinstance(text, str) or not text.strip(): raise HTTPException(status_code=400, detail="text字段不能为空字符串") model_name = request.get("model", "bge-large-zh-v1.5") vector = await get_embedding(text, model_name) return { "vector": vector, "dimension": len(vector), "model": model_name, "text_length": len(text) } # 批量embedding接口(可选) @app.post("/v1/embeddings/batch", summary="批量获取文本向量") async def embed_batch( request: Dict[str, Any], current_user: str = Depends(get_current_user) ): texts = request.get("texts") if not texts or not isinstance(texts, list): raise HTTPException(status_code=400, detail="texts字段必须是非空列表") model_name = request.get("model", "bge-large-zh-v1.5") vectors = await get_embeddings_batch(texts, model_name) return { "vectors": vectors, "count": len(vectors), "dimension": len(vectors[0]) if vectors else 0, "model": model_name } # 健康检查接口 @app.get("/health", summary="服务健康状态") async def health_check(): return {"status": "ok", "timestamp": int(time.time())}

这个main.py体现了几个工程化细节:

  • /token接口只做密钥校验和token发放,不涉及模型调用,极快;
  • /v1/embeddings返回的是扁平化的JSON,没有嵌套的dataobject等OpenAI字段,前端解析零成本;
  • 加了全局中间件记录耗时,方便后续做性能分析;
  • 每个接口都有清晰的summary和request示例注释,Swagger UI自动生成文档。

5. 启动与验证:三步走通整条链路

5.1 启动FastAPI服务

在项目根目录执行:

uvicorn main:app --host 0.0.0.0 --port 8000 --reload

服务启动后,访问http://你的服务器IP:8000/docs就能看到自动生成的交互式API文档。

5.2 获取访问令牌

用curl测试登录:

curl -X 'POST' \ 'http://localhost:8000/token' \ -H 'Content-Type: application/json' \ -d '"your-secret-api-key-here"'

你会得到类似这样的响应:

{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbWJlZGRpbmctc2VydmljZS11c2VyIiwiZXhwIjoxNzY4Mjk3MTIwfQ.xxxxx", "token_type": "bearer" }

复制access_token的值,后面调用要用。

5.3 调用embedding接口

用获取到的token调用向量化:

curl -X 'POST' \ 'http://localhost:8000/v1/embeddings' \ -H 'accept: application/json' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ -H 'Content-Type: application/json' \ -d '{ "text": "人工智能正在改变软件开发方式", "model": "bge-large-zh-v1.5" }'

你会收到一个简洁的响应:

{ "vector": [0.234, -0.156, 0.892, ...], "dimension": 1024, "model": "bge-large-zh-v1.5", "text_length": 14 }

对比sglang原生接口返回的嵌套结构,这个结果是不是更清爽、更易集成?

6. 进阶建议:让服务更贴近生产环境

这套方案已经可以跑起来,但如果你打算长期使用,还有几个小但关键的优化点:

  • 日志结构化:把print()换成logging,并加入request_id,方便问题追踪;
  • 限流保护:用slowapi/v1/embeddings加个每分钟100次的限制,防误用;
  • 模型热切换:在config.py里加个MODEL_ALIASES映射表,比如{"default": "bge-large-zh-v1.5", "legal": "bge-reranker-base"},让不同业务线用不同模型;
  • 向量缓存:对高频重复文本(如产品标题、FAQ问题),加一层Redis缓存,减少GPU计算压力;
  • 监控埋点:在get_embedding函数里加Prometheus指标上报,统计QPS、P95延迟、错误率。

这些都不是必须一步到位的,而是随着你的使用场景变复杂,自然浮现出来的需求。你现在最需要的,是一个能立刻跑起来、看得见效果、改起来不费劲的起点——本文给你的,正是这个起点。

7. 总结:封装不是重复造轮子,而是掌控力的延伸

把bge-large-zh-v1.5封装进FastAPI,看起来只是加了一层HTTP转发,但实际收获远不止于此:

  • 安全可控:不再担心谁都能调用你的GPU资源;
  • 协议自主:告别OpenAI兼容的束缚,定义你自己的输入输出规范;
  • 可观测性:每个请求都有日志、有耗时、有状态码,出了问题不再两眼一抹黑;
  • 演进空间:今天加鉴权,明天加缓存,后天加重试策略,全由你决定节奏。

技术的价值,从来不在“能不能跑”,而在“能不能稳、能不能扩、能不能管”。当你亲手把一个强大模型,变成自己系统里一个可信赖的组件时,那种掌控感,是任何现成服务都无法替代的。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/13 10:33:33

闲鱼数据采集2024全新版:零代码方案从入门到精通

闲鱼数据采集2024全新版:零代码方案从入门到精通 【免费下载链接】xianyu_spider 闲鱼APP数据爬虫 项目地址: https://gitcode.com/gh_mirrors/xia/xianyu_spider 闲鱼数据采集工具是一款专为电商研究者和市场分析师打造的零代码采集方案,能够帮助…

作者头像 李华
网站建设 2026/3/15 9:50:46

Python Socket编程实战:构建多线程TCP聊天室

1. Socket编程基础与TCP协议 在开始构建多线程TCP聊天室之前,我们需要先理解几个核心概念。Socket(套接字)是网络通信的基石,你可以把它想象成家里的电话插座——只有插上电话线才能通话。在Python中,socket模块提供了…

作者头像 李华
网站建设 2026/3/9 17:10:32

GLM-Image WebUI实战案例:教育机构AI教具插图自动化生成方案

GLM-Image WebUI实战案例:教育机构AI教具插图自动化生成方案 1. 为什么教育机构急需自己的AI插图生成工具? 你有没有见过这样的场景:一位小学科学老师凌晨一点还在手绘“水循环示意图”,旁边堆着三版修改稿;初中历史…

作者头像 李华
网站建设 2026/3/15 12:59:47

如何3步实现DLSS状态可视化?游戏性能监控完全指南

如何3步实现DLSS状态可视化?游戏性能监控完全指南 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper DLSS技术能大幅提升游戏帧率,但很多玩家常陷入"设置已开启,效果看不见"的…

作者头像 李华
网站建设 2026/2/26 5:30:06

通信工程MATLAB毕业设计实战:从系统建模到性能优化的完整路径

通信工程MATLAB毕业设计实战:从系统建模到性能优化的完整路径 1. 背景痛点:为什么你的仿真图总被老师打回 做毕设时,最怕老师一句“这个结果我复现不了”。通信方向尤其如此,常见翻车点有三类: 把“仿真”当成“画图…

作者头像 李华