ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

后端人工智能模块设计实战:模型网关、技术选型与线上排查

后端人工智能模块设计实战:模型网关、技术选型与线上排查 1. 后端AI模块的整体设计思路做后端开发这些年我经手过不少业务系统但真正把“AI智能模块”作为一个独立后端章节来设计还是最近一年的事。说实话这个模块跟传统CRUD后台完全是两回事它不产出数据不直接操作数据库却要跟几乎所有的业务接口打交道。这个章节我会把这一整套东西拆开来讲清楚从需求定义、技术选型到接口落地和线上问题排查全程基于我们真实项目的经验来写希望能给正在做前后端分离项目、准备把AI能力接进后端系统的团队一些参考。1.1 先厘清“AI智能模块”在后端到底管什么很多团队一提到AI后端第一反应就是“调用一下大模型API把参数传进去返回结果就完事”。真这么想的话后面一定会被坑。AI智能模块在后端体系里承担的是“智能能力的中枢”角色它至少要负责四件事。第一对外统一暴露AI能力接口让前端页面、移动端甚至其他后端服务都能通过一套标准API拿到算法能力而不是各业务线自己去对接不同的模型厂商。第二对内屏蔽模型差异无论是开源的本地模型还是云厂商的商用模型参数格式、鉴权方式、流式返回还是非流式返回都不一致后端需要做一层适配把差异挡在这层。第三承接有状态的会话逻辑AI对话不是简单的一问一答它涉及上下文管理、历史记忆、多轮追问这部分状态必须要由后端来维护。第四做内容安全与成本控制模型返回的内容需要经过合规过滤、敏感词校验同时线上流量随时可能让账单飙涨所以限流、熔断、缓存这些治理手段必须跟上。这四点要是有一块没想清楚就匆匆上马后面返工的代价会非常大。我见过一个项目前端直接拿着密钥去调模型厂商的接口省事是省事了但密钥泄露、账单失控、无法审计最后不得不推倒重来把整个调用链迁到后端。所以这一章的核心观点很明确AI能力必须经过后端封装而不是让前端绕过服务端直连模型。1.2 为什么是“模型网关”而不是每个业务各接各的在设计这个模块的架构时我们内部有过一轮讨论到底是做一个独立的AI网关服务还是在现有业务后端里加一个AI模块。客观说两种方案各有道理。独立网关服务隔离性好、独立扩容、故障范围可控但多一个服务就多一套运维还要处理服务间通信的鉴权和延迟。业务后端内嵌模块则部署简单、链路短、跟业务数据打通方便但耦合度会高一些。我们最终选择了在业务后端内做独立的“模型网关”组件而不是拆成独立服务。原因是公司的业务体量还没到需要独立微服务那一步AI功能当前主要嵌入在智能助手、内容生成、语义搜索三个场景里由业务后端直接提供接口能减少一跳网络开销也方便复用现有的登录鉴权和权限体系。但整个组件内部严格按照“网关层-适配层-模型层”三层来隔离如果未来流量上去了可以直接把这套逻辑平移到独立服务而不需要改动接口定义。模型网关对内提供统一调用的入口对外则暴露经过封装的REST接口。每次请求进来网关层负责解析参数、校验权限、读取上下文适配层根据请求的模型标识选定具体实现把统一参数翻译成各家模型要求的格式模型层只做一件事就是实际发起网络调用并返回原始响应。这样做的好处特别实在业务代码永远只面对一种请求格式和一种返回格式模型厂商升级SDK、更换底层模型、调整提示词模板业务端零感知。1.3 前后端分离视角下的AI接口契约设计既然项目是前后端分离AI模块的接口契约就特别重要。我们在设计时定了几条硬规矩。第一所有AI接口使用统一的HTTP动词和URI前缀比如/api/ai/chat、/api/ai/embedding、/api/ai/function前端只需要记住这几个入口就行。第二请求和响应体严格用JSON并且所有字段命名采用小驼峰跟前端约定清楚的类型和含义不能让前端去猜。第三时间格式统一用时间戳避免时区和格式解析的幺蛾子。第四所有流式返回走SSEServer-Sent Events不走WebSocket也不让前端用轮询。我举个例子说明智能对话接口的请求体大致长这样{ sessionId: a3f2c1e8-9b7d-4f2a-9a2c-0d3e4f5a6b7c, messages: [ {role: user, content: 帮我总结这篇文章的核心观点} ], model: default, temperature: 0.7, stream: true }返回值如果走流式则是一连串的事件流每段增量都是标准格式{code: 0, data: {delta: 这篇文章的, index: 0}} {code: 0, data: {delta: 核心观点是, index: 1}}这套契约跑通之后前端开发根本不需要关心底层接的是哪家模型因为接口层面已经跟具体模型厂商完全解耦。我在实际跟产品经理和前端对齐时发现真正容易扯皮的不是接口本身而是“AI出错时到底算谁的”这个问题。后来我们把错误码规范成一套0表示成功1001表示内容安全拦截1002表示上下文超长1003表示模型超时1004表示账户限流。前端拿到这些错误码就能精准提示用户而不需要后端在返回里写一大段人话式的错误描述。2. 技术选型Spring Boot 3 与 FastAPI 的权衡聊完设计思路最现实的问题来了这套模块用什么技术栈实现相关的热搜词里反复出现Spring Boot 3和Python FastAPI这正好是我们当时纠结了两周的问题。两种方案我都实际写了Demo下面把我的对比过程和一些结论性的判断写出来供大家参考。2.1 两种框架的AI生态对比先简单说结论如果你的团队本来就是Java体系那Spring Boot 3是稳妥的选择如果你们团队有Python背景或者AI功能非常依赖Python生态的算法库那FastAPI会更顺手。但就“后端AI智能模块”这个定位来说两者都能完成区别在于舒适度和后续维护成本。Spring Boot 3到今天已经相当成熟Spring官方推出了Spring AI项目把主流模型厂商的客户端做了统一抽象支持OpenAI协议格式、流式输出、向量化等能力。Java生态的优势在于类型安全、工具链完整跟公司现有的用户体系、权限体系、监控体系无缝对接。缺点是处理大量文本处理任务时Java的代码量会比Python多一些而且Python生态里那些成型的AI工具库比如各种切块策略、向量索引工具在Java里要么没有要么能力弱一大截。FastAPI的异步性能确实好而且写起AI调用代码来极其简洁Python的字典推导式配合OpenAI SDK十几行就能把一个带流式输出的补全接口写完。但FastAPI的问题也明摆着如果公司后端主体是Java你为了一个AI模块引入Python服务就得处理两套日志规范、两套部署流程、两套运维脚本跨服务调用的链路追踪也麻烦。除非AI能力以后会成为公司的主线业务否则这个运维成本不太划算。2.2 我们最终选择的方案我们最后定的方案是底层模型调用走Python FastAPI业务接入层走Spring Boot 3两层之间通过内部HTTP接口通信。为什么这么干因为我们的场景里模型调用不是简简单单的Chat接口还牵扯到本地知识库的向量检索、文本切块、重排序这些环节这些都是Python生态的强项用Java硬写也能实现但开发效率差了一倍不止。而面向前端和其他业务方的接口、鉴权、限流、会话管理统统放在Java这边利用现成的用户体系就能解决问题。如果你所在的团队后端是清一色的Java完全不想引入Python服务我建议直接用Spring AI或者自己封装OpenAI SDK做成纯Java方案。Spring AI对SSE流式的封装做得还不错配合WebFlux的Flux做返回代码量也不算大。唯一的提醒是别指望Spring AI能解决所有问题很多模型厂商的非标准参数它都不支持最终免不了要自己写适配器。2.3 模型网关的目录结构与配置管理定了技术选型之后就要落实工程结构。我们在后端项目里单独建了一个ai包按职责划分清楚。简单展示一下这个目录结构com.company.project ├── ai │ ├── controller # AI相关接口只做参数接收和返回 │ ├── gateway # 模型网关核心逻辑统一入口 │ ├── adapter # 模型适配器各家模型厂商的具体实现 │ ├── context # 会话上下文管理 │ ├── filter # 内容安全过滤、敏感词校验 │ ├── limiter # 限流、熔断、降级 │ ├── util # 通用工具类 │ └── config # 模型参数配置类Java这边的配置用application.yml管理比如模型Key、默认参数、超时时间都做成环境变量注入绝不允许硬编码。Python那边用pydantic-settings管理配置同样把模型名称映射、切块参数、向量库连接串放到环境变量里。配置管理这块我踩过一个坑测试环境用的模型Key是测试账户每天的限额特别低结果联调时接口频繁报限流错误。后来我们在配置中心里做了环境隔离测试环境和生产环境完全用不同的模型账户和不同的配置项并且把限额相关的参数也暴露到了配置里这样环境切换时就不会再出这种问题了。3. 核心接口的实现细节从Chat到Function Calling技术方案定了接下来进入实操。这一节我挑选三个最有代表性的接口实现来写基础对话补全、带Function Calling的工具调用、语义搜索的向量化接口。这三个场景基本覆盖了后端AI模块的大部分工作。3.1 对话补全接口的请求处理链路实现对话补全是最基础的AI接口前端把用户的一句话传进来后端补上上下文组装消息列表调用模型拿到回答后返回给前端。完整链路是Controller接收请求 - 判断会话状态 - 拼装上下文消息 - 调用模型适配器 - 处理返回结果 - 存入会话记录 - 推送给前端。我在Spring Boot里实现这个接口时核心代码如下PostMapping(/api/ai/chat) public ResponseEntityApiResponseChatResult chat(RequestBody ChatRequest request) { // 1. 校验会话和上下文 SessionContext ctx contextManager.getSession(request.getSessionId()); // 2. 拼装带历史的消息列表超出窗口则做截断 ListMessage messages ctx.buildMessages(request.getMessages()); // 3. 调用模型网关 ChatResult result aiGateway.chat(messages, request.getModel(), request.getTemperature()); // 4. 异步保存会话 contextManager.saveHistory(request.getSessionId(), messages, result.getAnswer()); return ResponseEntity.ok(ApiResponse.success(result)); }这里有个关键点容易被忽略上下文窗口大小。模型对输入token数有限制所以每次请求前都要对历史消息做裁剪。我们的方案是保留最近10轮对话超过的用摘要压缩。这个摘要是用模型自己生成的每满10轮就对旧消息做一次总结把总结作为一条特殊的系统消息放到对话里。实测下来效果不错既不会丢失太多关键信息也不会无脑截断导致模型丢失上下文。如果不想用模型做摘要也可以做简单粗暴的按字符截断但效果会差很多。我之前试过只保留最后2000字符用户一旦前面问了关键信息后面模型就会“失忆”。用模型摘要之后基本能覆盖大多数业务场景的上下文需求。3.2 Function Calling让后端接口变成AI的“工具”如果你希望AI不仅能聊天还能真正动数据、调服务、查订单那Function Calling就是绕不开的能力。它的原理是把后端已有的一些接口定义为“工具”在请求模型时把这些工具的结构化描述传给模型当用户的提问需要查数据时模型不会直接给出答案而是返回一个“调用某个工具并传入这些参数”的结构化指令后端解析这个指令执行真实逻辑再把执行结果以新的消息塞回对话让模型基于结果生成最终回复。举例来说我们的系统里有一个查询订单状态的接口在Function Calling配置里是这样声明的{ name: queryOrderStatus, description: 查询用户的订单配送状态, parameters: { type: object, properties: { orderId: {type: string, description: 订单号} } } }当用户问“我的订单怎么还没到”时模型会返回一个tool_call调用指定调用queryOrderStatus并传入用户的订单号。后端拿到这个调用后去查数据库把“已出库预计明日送达”作为工具结果塞回上下文再请求模型生成最终的友好回答。这块在实现上有两个容易踩的坑。第一是参数校验模型的工具调用参数经常出现类型不匹配比如字符串里带着空格、数字被传成了字符串所以执行工具前一定要做一次类型转换和合法性校验否则很容易把下游接口搞崩。第二是工具调用的循环上限模型在判断需要多次调用时如果代码里没做限制可能无限循环下去。我们在实现时规定最多允许4次工具调用超过就终止并返回提示。3.3 流式输出的落地方式与跨域处理对话类接口必须支持流式返回否则用户要等好几秒才能看到第一个字体验非常差。前端用SSE接收流式数据是当下最普遍的做法后端只需要把响应头的Content-Type设置为text/event-stream然后持续写入事件即可。在Spring Boot里实现SSE如果你用的是Servlet容器可以直接用SseEmitter如果你用的是WebFlux可以直接返回FluxString。我们项目用WebFlux的解决方案代码结构大致是GetMapping(value /api/ai/chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString chatStream(RequestParam String sessionId, RequestParam String message) { return aiGateway.chatStream(sessionId, message) .map(answer - ServerSentEvent.builder(answer) .event(message) .build()) .doOnError(e - log.error(stream error, e)); }流式接口的跨域问题特别容易被人忽视。普通JSON接口设置Access-Control-Allow-Origin就能跨域但SSE是长连接前端必须用EventSource或者fetch配合ReadableStream来接而且跨域时预检请求和实际请求的头部设置必须一致否则浏览器会直接断掉连接。我们调试跨域时发现Access-Control-Allow-Headers里必须显式包含客户端请求里带的头比如Authorization、X-Session-Id少一个都连不上。如果前端用的是EventSource它不支持自定义Header所以当接口需要鉴权时就只能靠URL参数或者Cookie传递凭证。我们最后统一让前端用fetch配合POST方式建立SSE连接这样既能在Body里传复杂参数也能在Header里带上鉴权信息比EventSource灵活得多。3.4 超时、并发与重试策略模型调用是远程网络操作耗时普遍在几百毫秒到几秒之间所以后端必须具备完善的超时控制和重试机制。我们配置的三层超时是这样的连接超时3秒、读取超时30秒、整体调用超时60秒。这三个参数不能拍脑袋定我测试过主流模型厂商的响应分布普通对话类请求95%都在10秒以内返回把整体超时定在60秒是为了容忍网络抖动和排队延迟。重试也不是无脑重试。如果模型返回的是业务层面的错误比如内容安全拦截、上下文超长重试多少次都没用。只有网络不通、5xx服务端错误、超时这三类情况才值得重试。我们做了两个开关maxRetries2、retryInterval500ms并且用指数退避每次重试间隔翻倍。重试上限一定不能太高否则模型限流一触发你的重试会把额度刷爆。并发控制上我们给AI接口单独配置了线程池核心线程数4、最大线程数16、队列容量200。所有模型调用都走这个线程池超过队列的直接返回限流错误码不允许无限堆积。这么做的好处是保护下游模型服务不被突增流量打垮代价是高峰期可能出现少量拒绝请求但业务可接受。4. 效果评估、线上观测与内容安全治理AI模块上线后跟普通接口最大的区别在于它不是一个“输入固定、输出固定”的函数而是有一定的随机性和不确定性。你测了100遍没问题用户第101次问可能就得到不同的回答。所以AI后端必须有一套完整体验评估和监控体系这部分是我觉得当前行业技术社区里讲得最少、但实际线上最刚需的内容。4.1 线上效果如何进行自动评测我们搭建了一套基于真实历史请求回归的评测方案。具体做法是从生产环境收集1000条真实用户提问人工给每条问题标注标准回答的关键要素每次模型变更或提示词调整后把这1000条问题跑一遍对比回答里是否包含标准要素计算通过率和平均响应时间。这套方案听起来简单但落地时有个细节需要注意模型回答的表述每次都不一样用关键词匹配来判断“是否答对”根本不靠谱。我们最后用模型自身来打分让评测模型根据标准答案和生成答案做一个一致性评分只有评分超过阈值的才计入通过。这种“用模型评估模型”的方式目前在业内是主流虽然不能说完全准确但比人工逐条看的效率高太多了。如果你们团队刚开始做AI模块我的建议是哪怕先用Excel记录测试用例也行但一定要有回归测试的意识。没有评测体系的AI模块就像没有测试用例的普通功能改一次模型参数你都不知道自己改坏了没有这种恐惧感会非常消耗团队的信心。4.2 日志、监控与链路追踪的落地AI模块的监控跟普通接口监控不一样除了QPS、响应时间、错误率这几个基础指标我们还额外加了三个关键监控维度。第一个是token消耗监控。每个接口都要记录输入和输出的token数把成本按天、按用户、按场景做维度汇总。模型按token计费token消耗出现异常时往往意味着有人在写长文或恶意刷量这时要能快速定位是谁、调了什么接口。第二个是模型返回内容质量监控。我们给每个回答打了一个“长度分”和“截断率”的标签如果截断率突然升高大概率是上下文窗口被打满或者模型开始重复输出这时候需要优化截断策略。第三个是用户反馈埋点。前端每个回答下面都有一个“赞/踩”按钮后端把用户反馈跟当次请求的日志关联起来。这个数据极其珍贵是后续提示词优化最直接的依据。链路追踪这块我们统一接入了OpenTelemetryJava和Python两边是同一套traceId串联。用户在对话里发一句话从Java网关到Python模型服务再到模型厂商全程都能用traceId串起来查。排查线上问题时这一条链路能帮上大忙。4.3 内容安全与提示词注入防护AI模块上线前必须思考一个问题如果用户故意引导模型输出不该输出的内容怎么办这属于内容安全的范畴虽然不能展开太细但有几条底线是必须做的。第一输入侧过滤。用户在发送内容前后端要做敏感词检测和长度控制。敏感词库要持续更新不能一劳永逸。第二输出侧过滤。模型返回的内容不能直接吐给前端要经过一层内容安全检测服务命中风险就打回重生成或替换为预置的兜底文案。第三系统提示词加固。我们在系统提示词里写明了模型的职责边界和拒答规则让模型面对恶意诱导时能主动拒绝。这个方法不能百分之百防住所有攻击但能把绝大多数低级诱导挡住。第四鉴权与配额隔离。不同用户、不同角色能调的模型和额度都不一样后端要做严格的权限控制不能出现普通用户调用管理员专用模型的情况。这些都是纯技术层面可以做且应该做到的事情。无论你用哪家模型厂商这四条后端防线都得有。后续章节如果时间允许我打算单独写一篇关于提示词注入攻击防护的文章这里先点到为止。5. 常见问题与排查技巧实录最后这部分我整理了一份我们在开发AI智能模块期间遇到的典型问题速查表都是真实在线上或联调阶段踩过的坑希望能帮后来者省点排查时间。问题现象根因分析解决方案前端收到流式数据丢失后半段网关或代理层缓冲导致SSE被强制断开确认Nginx的proxy_buffering关闭读取超时调大模型回答突然变成英文/乱码上下文里历史消息出现非UTF-8字符或语言指令丢失统一消息编码为UTF-8在系统提示词里固定输出语言工具调用返回参数解析失败FastAPI返回的JSON字段类型不匹配在Java侧做严格类型校验与默认值兜底偶尔有请求被限流但代码没写限流模型厂商账户自身有每分钟/每日配额在配置中心配置配额参数监控接近阈值时报警多轮对话后模型“变笨”上下文窗口被撑满旧信息被无脑截断改用模型摘要压缩历史消息而不是简单截断不同环境返回结果差异大各环境使用了不同的模型版本锁定模型版本号不要在配置里用默认值前端EventSource连接持续挂断跨域配置中缺少所需Header改为fetchPOST方式建立SSE并在CORS配置中补全Header线上日志中出现大量连接池超时模型调用线程池被排队任务占满增加最大线程数或在前端做请求合并与节流对话接口偶发返回空内容模型输出为空或被内容安全过滤掉了区分空回复与安全拦截分别返回不同错误码除了速查表里的问题我特别想分享一个排查经验流式返回显示“半个字就断了”的情况看似是后端问题实际上八成是Nginx的proxy_buffering没有关闭或者代理层的读取超时时间设得太短。SSE是一种长连接它跟普通HTTP请求不一样数据是一点点往外吐的中间任何一个代理缓存或超时都会掐断连接。如果你在本地联调没问题、一上测试环境就断先检查代理层再检查后端代码。最后分享一个小知识AI智能模块的耗时绝大多数发生在模型服务端后端代码本身的耗时占比很小。所以优化时先优化模型调用策略比如提示词长度、上下文裁剪、缓存复用远比优化Java代码本身的性能来得有效。我在实际项目里靠“命中缓存不调模型”这一个策略就把整体响应时间降了一半以上成本也省下来不少。
返回列表