ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

项目里要接 AI 大模型了?我把多厂商网关、场景配额、调用日志一次性做成了脚手架

2026/10/8 5:51:01 拓冰建站 浏览量
项目里要接 AI 大模型了?我把多厂商网关、场景配额、调用日志一次性做成了脚手架 项目里要接 AI 大模型了我把多厂商网关、场景配额、调用日志一次性做成了脚手架导读业务方提需求给客服加个 AI 助手“给简历做个 OCR 识别”一个接一个。最早是各业务自己 new 一个 SDK 调密钥散在 yml 里出了事还没法查是谁调的。后来我把 AI 调用收敛成了一个独立模块多厂商 SPI、场景配置、配额限流、调用日志全在里面。先交代一下当时的状态。项目里要接大模型的地方越来越多智能客服、简历 OCR、内容润色、知识库问答。一开始图省事每个功能直接引 SDKDeepSeek 的、通义的、OpenAI 兼容的一起上。半年后发现三个问题密钥管理混乱有人把 key 写进 yml 提交了、没法审计谁在什么时候调了多少 token 完全不知道、换厂商要改业务代码。于是我在脚手架里加了qkl-ai模块核心思路就一句话业务方不关心用的是哪家大模型只认场景编码。SPI 抽象所有厂商长一个样先把各家 SDK 的差异吃掉。定义一个AiModelClient接口流式、非流式、对话、Embedding 都收敛进去publicinterfaceAiModelClient{/** 厂商编码如 DASHSCOPE / DOUBAO / OPENAI */StringproviderCode();/** 非流式对话 */ChatResultchat(ChatRequestrequest);/** 流式对话SSE */voidchatStream(ChatRequestrequest,StreamCallbackcallback);/** 文本向量化RAG 用 */Listembed(Listtexts);/** 连通性测试后台测试连接按钮用 */ConnectivityTestResulttestConnectivity();}实现类各自继承一个AbstractOpenAiCompatibleClient——现在主流厂商都兼容 OpenAI 协议改个 baseUrl 和鉴权头就行真正要手写协议适配的很少。我用的是 DashScope、Doubao、OpenAI 三家全部走这个抽象ComponentpublicclassDoubaoCompatibleClientextendsAbstractOpenAiCompatibleClient{OverridepublicStringproviderCode(){returnDOUBAO;}OverrideprotectedStringresolveBaseUrl(AiProviderprovider){returnhttps://ark.cn-beijing.volces.com/api/v3;}OverrideprotectedMapresolveHeaders(AiProviderprovider){returnMap.of(Authorization,Bearer provider.getApiKey());}}客户端注册进AiModelClientRegistry按providerCode取业务层永远不知道具体实现是谁。场景配置业务方只传场景编码接口暴露给业务方的就一个方法chat(scene, messages)。场景对应哪家厂商、哪个模型、什么系统提示词、温度多少全在qkl_ai_scene表里配// 场景实体关键字段privateStringscene;// 场景编码CUSTOMER_SERVICE / RESUME_OCR / CONTENT_POLISHprivateStringproviderCode;// 走哪家厂商privateStringmodelCode;// 模型编码privateStringsystemPrompt;// 系统提示词privateIntegertemperature;// 采样温度privateIntegerstatus;// 1 启用 0 停用加一个新场景运营在后台插一条记录开发一行代码都不用改。之前那种新场景要改代码发版的日子算是过去了。配额 限流防的是失控不是防人AI 调用是要花钱的必须有两道闸。第一道是租户维度日 token 配额第二道是Redis 窗口限流publicvoidconsume(StringtenantId,inttokens){AiQuotaquotagetOrCreate(tenantId);resetIfNewDay(quota);// 跨天自动归零longusedquota.getDailyTokenUsed()null?0L:quota.getDailyTokenUsed();quota.setDailyTokenUsed(usedtokens);quotaMapper.updateById(quota);}publicvoidassertAllowed(){StringidentitycurrentUser();StringkeyRedisKeys.rateApi(ai.chat,user,identity);LongcurrentredisTemplate.opsForValue().increment(key);if(current!nullcurrent1){redisTemplate.expire(key,Duration.ofSeconds(Math.max(1,aiProperties.getRateLimitPeriod())));}if(current!nullcurrentaiProperties.getRateLimitCount()){thrownewQklBizException(ErrorCode.RATE_LIMITED,操作太频繁请稍后再试);}}配额超了就抛当日额度已用完前端友好提示限流超了就 429 类错误防止脚本刷。密钥加密 调用日志出事能追责密钥不落 yml存库的时候加密用AiSecretCipher包了一层解密只在真正发起调用时进行privateStringdecryptSecret(AiProviderprovider){returnsecretCipher.decrypt(provider.getApiKeyEncrypted());}每次调用无论成败都落qkl_ai_call_log场景、模型、输入输出 token 数、耗时、结果状态、失败原因。日志保留 90 天出了事故翻日志就能还原谁、什么时候、调了什么、花了多少 token。踩坑记录总开关一关整个模块静默消失问题现象本地没配 AI 相关配置启动项目后调用/admin/ai/scene接口返回70001页面上一脸懵。排查过程翻代码发现qkl.ai.enable默认是falseAiAutoConfiguration用ConditionalOnProperty控制开关没开时整个模块的 Bean 都不注册管理端接口统一返回业务码 70001模块未启用。定位思路这个设计本身没问题——AI 密钥是敏感配置不该让没开通的租户看到界面。但报错信息太含糊前端看不出是没启用还是接口坏了。最终解决管理端接口把 70001 的 message 改成AI 模块未启用请在配置中设置 qkl.ai.enabletrue前端根据错误码弹出明确提示。另外约定生产环境密钥必须走配置中心/环境变量注入 cryptoSecret空 salt 回退逻辑只允许开发环境用防止线上密钥可逆。可直接复用的清单厂商差异用SPI 抽象 注册中心吃掉业务层只认场景编码场景表配厂商模型提示词新场景后台配置即上线配额按租户按天跨天自动归零限流用 Redis increment expire 窗口密钥加密存库、调用时解密绝不进 yml 明文每次调用落日志保留 90 天审计追责全靠它模块总开关用ConditionalOnProperty控制未启用时错误提示要友好。这套 AI 网关是 qkl-boot 脚手架里的标准模块。项目源码https://gitee.com/gzqkl/qkl-boot