Go+React构建LLM API统一管理网关:密钥路由与计费全搞定 简介面向计算机与软件工程专业毕业设计的一套完整项目资源基于Go与React构建的LLM API统一管理系统可集中接入OpenAI、Azure、Claude、Gemini、DeepSeek、豆包、通义千问等主流大模型实现密钥管理、流量控制、计费统计、多租户与实时监控等核心能力。资源共551个文件压缩包约1.45MB以Go后端源码233个go和React前端代码242个js为主另含SQL数据库脚本、Dockerfile容器化部署文件、论文docx文档及SVG图标等目录清晰便于直接运行与二次开发。包内提供详细设计文档、部署指南和毕业设计论文模板覆盖从环境搭建到功能扩展的完整链路代码模块化程度高、注释完整适合学习API网关、适配器模式、微服务架构及GoReact全栈实践。目前已有112人学习下载可满足毕业设计选题、企业级API聚合分发平台搭建及分布式系统课程设计等场景需求。1. LLM API统一管理系统一套源码把密钥、路由、计费全管起来做LLM应用的团队大多会走到同一步业务代码里散落着OpenAI、DeepSeek、Kimi的API Key每个模型商的SDK、计费规则、限流参数五花八门换模型要改代码查消费要翻控制台。这套LLM API统一管理系统就是把你所有大模型调用统一收口到一个网关。后端用Go扛高并发转发前端用React做可视化配置密钥池轮转、模型路由、配额统计全部在面板上操作。适合毕业设计选型也适合团队内部搭一套轻量API网关。源码加论文一起给从后端启动、前端联调到多模型接入整个工程路径能完整走通。2. 系统架构与技术选型Go与React的分工边界2.1 为什么网关选Go而不是Java或Python网关的核心职责是转发请求转发链路每多一次序列化或GC停顿用户侧的延迟就会放大。Go在这条赛道上的优势不在语法多优雅而在三个实际点协程模型让单机并发轻松上万、静态编译产物直接扔服务器跑、内存占用比Java同规模服务低一个量级。这个系统里后端模块拆成两个服务gateway负责接收业务方请求并转发到真实模型商admin负责密钥和配置管理两个服务共享同一个数据库。我在本地跑通这套工程的时候先把Go环境切到1.21以上因为代码里用到了log/slog做结构化日志。项目根目录下有go.mod直接go mod tidy一次就能拉齐依赖主要依赖是gin框架和SQLite驱动生产环境换成MySQL只需要改数据源连接串。2.2 React前端的组件划分与状态管理前端是典型的React管理后台结构路由只有三块控制台首页、密钥管理、调用日志。这种量级的界面不需要Redux我用的是Zustand做全局状态比Redux少一半样板代码。核心状态只有一个currentProvider切换模型商的时候密钥列表、配额图表、日志表格全部联动刷新。组件层级上KeyTable负责密钥列表展示和状态切换ChartPanel封装了ECharts的调用量趋势图LogViewer负责日志分页。三块组件之间没有复杂嵌套关系通过一个store实例通信就够。打包配置用的Vite开发环境代理指向本地后端8080端口。如果你拿到的源码里前端目录是web/那入口文件就在web/src/main.tsx。2.3 核心数据模型密钥、路由与配额表设计数据库层设计直接决定这个网关能撑住什么场景。系统里三张核心表provider_config存模型商接入信息api_key存密钥池usage_record存调用流水。provider_config表最关键的两个字段是base_url和model_name每个模型商一行。CREATE TABLE provider_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, base_url TEXT NOT NULL, model_name TEXT NOT NULL, api_key_encrypted TEXT NOT NULL, rate_limit INTEGER DEFAULT 60, weight INTEGER DEFAULT 1, enabled INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这张表的设计意图是把模型商的差异前置到配置层。rate_limit是每分钟最大请求数weight用于后续的加权轮询转发。api_key_encrypted字段里存的是加密后的密钥不是明文。密钥池设计成多对一的关系多个api_key记录可以关联到同一个provider_config的id上。这样当一个key因为余额不足被模型商拒绝时网关可以在同provider下自动换下一个key重试。3. 从源码到部署本地跑通系统的完整步骤3.1 后端Go服务启动配置文件与数据库初始化拿到源码包后第一步不是急着运行先把配置文件读明白。项目里config/目录下有个config.yaml里面是网关的核心运行参数。你需要改的是listen_addr、db_path和admin_token三个值。默认配置监听0.0.0.0:8080SQLite文件生成在data/目录下。cd llm-api-gateway go mod tidy cp config/config.yaml config/config.local.yaml # 编辑 config.local.yaml把 admin_token 改成你自己的随机字符串 go run ./cmd/gateway第二条命令拉依赖第三条生成你的本地配置副本避免改坏原始配置。最后一条启动网关主程序。启动成功的标志是终端输出一行结构化日志levelINFO msggateway started addr:8080。如果启动失败大概率是端口被占用或者SQLite目录没有写权限。数据库初始化不需要手动执行SQL文件程序启动时会自动建表。这个设计对新手很友好但也意味着你不能提前手动往SQLite里塞数据——程序会把你的表结构覆盖掉。第一次启动后用SQLite客户端打开data/目录下的db文件能看到我上一章说的三张表和另外两张辅助表。3.2 前端React工程运行Vite代理与接口对接前端和后端是两个独立进程本地联调的关键在Vite的代理配置。后端跑在8080前端dev server默认跑在5173如果前端直接调/api路径会碰到跨域。解决办法在web/vite.config.ts里配置代理把/api前缀的请求转发到后端地址。export default defineConfig({ plugins: [react()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这段配置的意思是浏览器发出的/api/v1/keys请求Vite dev server会把它转成http://localhost:8080/api/v1/keys。changeOrigin必须设为true否则后端判断请求来源时会因为Host头不一致而拒绝。前端启动命令是npm install npm run devNode版本建议18以上Vite 5对Node 16的支持已经移除了。3.3 多模型接入OpenAI、DeepSeek、Kimi的provider配置系统真正体现“统一管理”的地方在provider配置。接入一个模型商只需要在管理面板点“新增Provider”填base_url、模型名和API Key。以DeepSeek为例base_url填https://api.deepseek.com/v1模型名填deepseek-chat。Kimi那边base_url是https://api.moonshot.cn/v1模型名填moonshot-v1-8k。# 用curl测试网关转发是否成功 curl -X POST http://localhost:8080/v1/chat/completions \ -H Authorization: Bearer your-admin-token \ -H Content-Type: application/json \ -d { provider: deepseek, messages: [{role: user, content: 你好}] }这个请求打到网关网关会根据provider字段找到对应的provider_config记录替换成真实的API Key再向DeepSeek发起请求。响应头里有个X-Provider-Used字段标识实际命中的模型商。如果response里出现upstream_error说明网关已经成功把请求转出去了问题在模型商那边——一般是余额不足或模型名填错。4. 统一网关的转发链路模型路由与密钥轮转的代码实现4.1 核心转发逻辑Provider路由与请求重写网关的入口是一个gin的POST路由/v1/chat/completions。处理函数做的事顺序非常清晰从请求体取provider字段查数据库拿到provider_config把请求体里的provider字段剥掉模型商不认这个参数重写目标URL再把Authorization头替换成密钥池里当前可用的key。func handleChatCompletion(c *gin.Context) { var req ChatRequest if err : c.ShouldBindJSON(req); err ! nil { c.JSON(400, gin.H{error: invalid request body}) return } provider, err : store.GetProviderByName(req.Provider) if err ! nil || !provider.Enabled { c.JSON(404, gin.H{error: provider not found or disabled}) return } // 从密钥池取当前轮次的key key, err : keyManager.Next(provider.ID) if err ! nil { c.JSON(502, gin.H{error: no available api key}) return } upstreamURL : fmt.Sprintf(%s/chat/completions, provider.BaseURL) upstreamReq, _ : http.NewRequest(POST, upstreamURL, bytes.NewBuffer(payload)) upstreamReq.Header.Set(Authorization, Bearer key.Plaintext) upstreamReq.Header.Set(Content-Type, application/json) // 转发并保持流式响应支持 resp, err : http.DefaultClient.Do(upstreamReq) if err ! nil { c.JSON(502, gin.H{error: upstream error, detail: err.Error()}) return } defer resp.Body.Close() // 把模型商的响应原样写回 c.Data(resp.StatusCode, resp.Header.Get(Content-Type), bodyBytes) }第13行的keyManager.Next(provider.ID)是密钥轮转的入口它内部维护了一个按provider分组的计数器每次请求都把当组下标往后移一位。第19行拼上游URL时用的是provider.BaseURL加固定路径所以配置base_url时不要自带/chat/completions尾巴否则会拼出双路径。第24行用的是http.DefaultClient如果你要压测高并发这里应该换成自定义Transport并加大连接池否则TCP连接复用不够会导致TIME_WAIT堆积。4.2 密钥池轮转与错误重试密钥轮转不能只做“换一个key”还得处理“换到的key也是坏的”的场景。代码里keyManager.Next每次取完key会顺手做一次健康性标记如果上一次用完这个key后模型商返回了401就把这个key标记为cooldown状态冷却60秒后才重新参与轮转。func (km *KeyManager) Next(providerID int) (*Key, error) { keys : km.pool[providerID] km.mu.Lock() defer km.mu.Unlock() for i : 0; i len(keys); i { idx : (km.counter[providerID] i) % len(keys) key : keys[idx] if key.Status KeyStatusActive { km.counter[providerID] idx 1 return key, nil } } return nil, ErrNoAvailableKey }这段轮转逻辑的精髓在for循环从当前计数位置开始最多遍历一整圈找到一个Status为Active的key。如果整个池都是cooldown状态返回ErrNoAvailableKey网关向调用方返回502。实际使用中这类情况极少出现因为代码里还有一个后台定时任务每5分钟把到期的cooldown key重置回active。你在面板的密钥管理页看到的“启用/停用”开关操作的就是这张密钥池表的status字段。4.3 调用计费与配额usage表如何支撑看板调一次模型商接口网关会记一条usage_record请求时间、provider名、模型名、token消耗、耗时。token数从哪来不是网关自己数的而是从模型商响应体里解析。OpenAI格式的响应里有个usage字段prompt_tokens加completion_tokens就是总消耗。这套源码里有个extractUsage函数专门干这件事。日志表和看板的联动是这么做的前端控制台首页的统计卡片请求的是/api/v1/usage/summary接口SQL按“今天”和“provider”两个维度聚合。如果你部署之后发现图表数字对不上先确认服务器时区。SQLite的CURRENT_TIMESTAMP存的是UTC而前端展示用的东八区差出来的8小时会让你在早上9点看统计数据时总觉得少了几个小时的数据。源码里数据库连接串加了?locAsia/Shanghai解决这个问题你如果改了连接串记得不要丢掉这个参数。5. 实际部署踩坑记录从零到可用的常见问题排查清单5.1 前端请求全被浏览器拦跨域报错CORS现象前端页面能打开但所有接口请求在浏览器控制台报Access-Control-Allow-Origin错误点密钥管理页面直接白屏。原因开发环境依赖Vite代理走通了但部署时有人直接把前端build产物扔Nginx里Nginx没配后端反向代理前端请求打到了Nginx静态服务器自己身上。解决Nginx里加一条反向代理规则把/api前缀转发到网关的8080端口同时配置proxy_set_header Host $host。记住一个原则开发环境查Vite配置生产环境查Nginx配置其他地方出现跨域都是伪装成CORS的其他问题。5.2 数据库连接失败SQLite文件权限现象后端启动时报unable to open database file但data/目录明明存在。原因用root用户启动过一次服务SQLite文件属主变成root之后用普通用户启动时没有写权限。解决改掉data目录属主或直接删掉旧db文件让程序重建。这条坑对容器化部署尤其致命Docker Volume挂载目录默认也是root权限建议启动命令里加chown或者把运行用户固定成nobody。说白了就是权限管理别偷懒SQLite的文件权限比MySQL的账号权限更直接。5.3 多路转发时偶现502上游连接池耗尽现象压测到200并发时网关日志出现密集的upstream error: context deadline exceeded。原因网关默认用的是http.DefaultClient它的Transport没有设置连接池上限高并发下每个请求都在建新TCP连接源端口耗尽导致连接失败。解决在gateway初始化时注入自定义Transporttransport : http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 30, MaxConnsPerHost: 50, IdleConnTimeout: 90 * time.Second, } httpClient : http.Client{Transport: transport}MaxIdleConnsPerHost控制单个模型商域名下最多保持多少空闲连接MaxConnsPerHost是硬上限。这里按常见做法给了每主机50个并发连接的设定如果你同时接5个模型商这个参数够用。改完重建并重启压测数据会明显改善。5.4 密钥面板显示“已停用”但网关还在用现象在管理面板把某个key点成“停用”但抓包发现网关请求头里还在用这个key。原因keyManager.Next的判断条件是Status KeyStatusActive但面板停用操作走的是另一张表的enabled字段两个状态字段没同步。解决更新密钥时同时更新两个字段或者在Next方法里增加一次实时查询。源码里这个逻辑用的双写方案就是当你点了停用后端会同时改status和enabled两个字段。拿到的源码如果这个坑还复现检查一下你自己的版本是不是改漏了一处。5.5 日志时间全部差8小时现象调用日志表格里的时间比实际时间晚8小时凌晨调用的记录跑到前一天。原因Go后端用的time.Now()输出本地时间但SQLite的CURRENT_TIMESTAMP是UTC写入和读取两个环节的时区基准不统一。解决统一写入模式。代码里建了一个formatTime工具函数所有插入日志记录的动作都显式调用time.Now().In(time.Local)先转成东八区再格式化。这是一个典型的时区玄学问题别看它小事真到答辩时被评委问一句“你系统日志时区怎么设计的”就能把你问住。6. 用论文守住答辩从系统设计到论文撰写的三点经验论文不是系统的说明书复述。管理系统这类题目老师普遍看三个点为什么需要这个系统、系统架构如何支撑需求、关键模块的实现难点。我拿到这套论文后快速翻了一遍它的框架是“背景→技术选型→架构设计→模块实现→测试验证”五段式。你写的时候重点往第二和第四段发力。技术选型章里别只写“Go性能好”给出对比数据同样的转发逻辑Go的goroutine模式比Java线程池在100并发下内存占用低约60%。这套论文里有几张压测表格你本地跑完能复现出差不多的数字。架构设计章画好那张三层拓扑图——客户端层、网关层、模型商层标注每层的数据流转方向。答辩时老师盯着图问的概率最大你要能指图把一条请求链路讲完整。模块实现章选“密钥轮转”和“调用计费”两个点深挖。密钥轮转体现工程细节调用计费体现数据设计能力。我在文档里看到一个技巧把usage_record表设计成按天分表的方案代码里用视图做跨表聚合。这个点写进论文比写“我实现了日志查询”有分量得多。最后给你一个习惯性建议。我做了这么久API网关每次改完代码都强制走一遍三连单元测试、压测一轮、看网关日志的upstream_error计数。这套系统的源码里没有内置压测脚本你可以在本地用wrk -t4 -c100 -d30s http://localhost:8080/v1/chat/completions自己跑一轮数一下错误率。毕业设计也好团队内部工具也好这套流程走完你才敢说这个系统你真正吃透了。希望帮到你。本文还有配套的精品资源点击获取