qkl-boot 把 AI 做成独立模块多 Provider 切换、Key 加密、RAG 开关业务代码零改动换个大模型要动多少代码老板一句话我对着三个 Service 里的硬编码 OpenAI 客户端发了半小时呆。后来 qkl-boot 里单独起了个qkl-ai模块把 Deepseek、通义、智谱全收进去业务层只认一个接口。这篇把模块怎么拆的记下来配置、代码都能直接抄。先立规矩密钥不进 yml开关决定模块生死最早接 Deepseek 的时候apiKey 直接写在 application.yml一提交 Git 就裸奔。qkl-ai 的第一个改动就是AiProperties密钥一律不落配置文件只留开关和运行参数。DataConfigurationProperties(prefixqkl.ai)publicclassAiProperties{/** 总开关false 时不注册业务 Bean管理端返回 70001 */privatebooleanenablefalse;/** 流式协议sse */privateStringstreamsse;/** 默认上下文轮数 */privateintcontextTurns20;/** 调用日志保留天数 */privateintcallLogRetainDays90;/** API Key 加密密钥盐生产务必覆盖 */privateStringcryptoSecret;/** 默认系统提示词 */privateStringdefaultSystemPrompt你是 QKL-Boot 智能助手回答简洁、准确、专业。;/** 对话限流窗口内次数 */privateintrateLimitCount30;/** 对话限流窗口秒数 */privateintrateLimitPeriod60;/** RAG 配置 */privateRagragnewRag();}enablefalse的时候AiDisabledAutoConfiguration直接不注册任何 Bean管理端接口统一返回 70001。等于说上线前没配好 Key系统也不会炸只是 AI 功能不可用。这个设计救了两次发布事故——配置中心没同步、Key 过期服务照常跑只是对话接口报错而已。多 Provider一张表管所有模型状态位决定用谁Provider 不写死在代码里落库。ai_provider表字段providerCode、baseUrl、apiKeyEnc加密存、timeoutMs、status、sort。切换模型 改库里的 status/sort不用发版。// 选择可用 Providerstatus1 按 sort 排序取第一个publicAiProviderselectProvider(StringproviderCode){LambdaQueryWrapperqwnewLambdaQueryWrapper();qw.eq(AiProvider::getProviderCode,providerCode).eq(AiProvider::getStatus,1).orderByAsc(AiProvider::getSort).last(LIMIT 1);returnproviderMapper.selectOne(qw);}API Key 落库前用cryptoSecret做 AES 加密取出来用的时候再解。库表泄露不等于 Key 泄露这个底线得有。timeoutMs 单独存是因为不同 Provider 的响应速度差很多——Deepseek 偶尔要 30 秒通义一般 10 秒内统一一个超时必然有一方天天报超时。RAG开关配在 yml召回参数全可调知识库问答是重头。qkl-ai 的 RAG 配置一开始就想着不同客户参数不一样全放AiProperties.Rag里publicstaticclassRag{/** db | redis | pgvector | milvus | qdrant首期仅 db 实现 */privateStringvectorTypedb;privateinttopK5;privateintchunkSize500;privateintchunkOverlap80;/** 严格拒答最低相似度 */privatedoublescoreThreshold0.25;/** 无足够依据时拒答 */privatebooleanstrictRefusetrue;/** DB 向量扫描上限防止超大库线性炸 */privateintmaxScanChunks2000;}首期只实现vectorTypedb就是 MySQL 里存 chunk 简单余弦相似度。2000 个 chunk 以内这套完全够用文档库大了再切 pgvector/milvus业务调用方无感。踩坑相似度阈值定太松AI 开始一本正经胡说现象知识库上线两周运营反馈 AI 回答里开始出现依据不充分的内容比如把 A 产品的参数说成 B 产品的。排查先看调用日志AiCallLog里记录的 promptTokens 没异常再看召回结果topK5拉回来的 chunk 相似度普遍只有 0.2 上下——低于默认阈值 0.25但系统还是用了。定位问题出在strictRefusetrue只对完全无召回生效。相似度 0.15 的垃圾 chunk 也混进上下文了模型分不清哪个是准的就乱拼。解决把scoreThreshold提到 0.3召回阶段过滤掉低于阈值的 chunkstrictRefuse保持 true低于阈值且无有效召回时直接返回暂无足够资料请换个问法不再硬答。改完跑了一周运营反馈的胡说现象基本消失。// 召回后过滤低于阈值直接丢弃防止垃圾上下文污染回答Listhitssearch(topK*2);Listvalidhits.stream().filter(c-c.getScore()scoreThreshold).limit(topK).collect(Collectors.toList());if(valid.isEmpty()strictRefuse){return暂无足够资料支撑回答请换个问法;}调用日志别省tokens、耗时、traceId 一条不能少AiCallLog每调必记providerCode、modelCode、bizType、promptTokens、completionTokens、latencyMs、status、errorCode、errorMsg、traceId。traceId 是排查用户说 AI 答错了的救命稻草——前端把 traceId 打出来后端按 traceId 查一次调用全链路。90 天保留期到了就清理表不会无限膨胀。AiCallLoglognewAiCallLog();log.setTraceId(traceId);log.setProviderCode(provider.getProviderCode());log.setModelCode(modelCode);log.setBizType(chat);log.setPromptTokens(usage.getPromptTokens());log.setCompletionTokens(usage.getCompletionTokens());log.setLatencyMs(System.currentTimeMillis()-start);log.setStatus(SUCCESS);callLogMapper.insert(log);可以直接复用的清单密钥绝不进 ymlcryptoSecret加密落库库泄 ≠ Key 泄enablefalse兜底配错了不炸服务。Provider 落库管理status/sort 切模型改库不改代码timeoutMs 按 Provider 单独配。RAG 参数全开放topK、chunkSize、scoreThreshold、strictRefuse 都配在 yml换客户不换代码。scoreThreshold别设太低0.25 以下垃圾 chunk 会混进上下文模型开始胡说实测 0.3 起步。调用日志带 traceIdtokens、耗时、错误码全记90 天清理排查AI 答错全靠它。首期vectorTypedb够用2000 chunk 内大了再上向量库调用方无感。项目源码https://gitee.com/gzqkl/qkl-boot
阅读完成 · 觉得有帮助?