拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

若依整合AI深度指南:从流式输出到架构设计

若依整合AI深度指南:从流式输出到架构设计

1. 先搞清楚:把 AI 塞进若依,到底在“整合”什么

1.1 这不只是“调接口”这么简单

很多朋友看到别人发了篇文章,标题写“若依整合AI”,配图是打开浏览器,点点鼠标就能跟机器人聊天,第一反应就是:调一个 HTTP 接口呗,把大模型返回的内容塞到聊天框里不就行了?

我一开始也是这么想的,直到我被生产环境教育了一轮才明白,问题远没有那么简单。

真正上过线的同学一定经历过这样的场景:接口文档写得清清楚楚,参数照着填,Postman 里测试也一切正常,结果发布到测试环境,前端页面打开以后,文字每隔几秒才蹦出来几个,偶尔还直接卡住不动了。打开浏览器控制台一看,请求一直处于 pending,后端日志里却查不到任何异常。再过一会儿,用户刷新页面,对话记录没了;多开几个对话,上下文串了;后台管理用户点击同一个 AI 功能,发现所有人的额度全部走一个账号,成本根本没法控制。

这些问题的根源,并不是你“不会调接口”,而是你把大模型当成一个普通的第三方接口来对待了。大模型的调用方式,和传统的业务接口有几个关键差异:输出方式是流式的、上下文窗口是有限的、单次调用是有成本的、模型是不断迭代的。如果我们不把“接入 AI”当作一次架构设计,而只是“调用接口”,那后面不管你换多少次模型,问题还是会反复出现。

所以“若依整合 AI”真正的核心,不在于你怎么发请求,而在于你把大模型的能力,改造成一件适合若依业务体系使用的、可控的、可观测的内部工具。这就是“拔高原理篇”想聊清楚的事。

1.2 若依的“框架身份”决定了整合方式

在讲具体方案之前,必须先明确若依到底是个什么定位的东西。若依是一个后台管理框架,它天生解决的问题是:权限、菜单、用户、部门、日志、代码生成、定时任务。也就是说,它是一套“面向企业内部系统”的基础设施。

而 AI 大模型,是一个“面向对话/生成任务”的外部能力。它不像数据库那样存放在自己的机房,不像 Redis 那样可以由你随意控制,它是一个通过网络方式提供的、有延迟的、有上下文限制的、按 token 计费的服务。

这两者天然是两种不同气质的东西。若依强调“管理”,大模型强调“生成”。你不可能把若依的主体逻辑改造得像一个聊天机器人那样实时、流式、高并发,那违背了这个框架的核心定位;也不可能让大模型去做权限审批、菜单管理,那是拿大炮打蚊子。合理的做法,是把 AI 能力作为一个“旁挂模块”挂在若依体系旁边,若依负责权限、认证、数据持久化,AI 模块负责对话、理解、生成、编排。

我见过不少团队把 AI 功能直接写在若依的业务 Controller 里,和保存订单、审批流程混在一起。短期看着方便,过了两个月模型升级、换了供应商,你就要在一堆业务代码里找“调用大模型的那几行”,改完还要发版、测试、灰度。这个成本在长期看非常高。

正确的姿势是:若依还是那个若依,AI 是“另一个服务”,两边只通过接口或消息队列通信。这样业务代码是干净的,AI 模型怎么迭代不影响核心管理功能,权限体系又能复用。这也是为什么后面很多企业级方案,都选择“若依 + 独立 AI 服务”的结构。

1.3 为什么“若依 + 独立 Python 服务”的组合在实战里很常见

我看后台搜索热词里,有一条非常贴合实战的场景:“基于若依管理系统和独立 python 处理服务构建的企业级识别系统”。这个关键词背后其实代表了国内很多团队的成熟选型。

为什么不是纯 Java?因为当前大模型、图像识别、语音识别这些 AI 生态,主力 SDK 和社区方案集中在 Python 那边。你到 Hugging Face 上拉一个模型,官方示例基本都是 Python;你想用 LangChain、LlamaIndex 这些工具链,也是 Python 最舒服。可企业的核心业务系统、用户体系、数据权限、审批流,往往又都在 Java 体系里。若依作为 Java 后台管理框架,正好解决了纪检这些。

所以实操中比较顺的方案是:若依负责“业务编排”,Python 服务负责“模型推理”。两边通过 REST API 或者消息队列来通信。识别任务耗时较长,就由若依发一个任务到消息队列,Python 服务消费并处理,处理完回调更新数据库。用户在前端看到的是一套完整的业务系统,底层跑的是 Python 的模型逻辑。

这个思路我再展开一点:这套拆法,本质上就是把“算力密集”的部分和“业务密集”的部分分开。AI 推理需要 GPU、需要大规模批处理、需要跑的模型镜像,这些很容易拖垮业务服务器。而若依这样的小型管理框架,最希望的是稳定运行、不折腾环境。两个进程分开以后,AI 服务挂了不会连带若依崩溃,模型升级的时候可以独立发布,不会影响业务主链路。

2. 拔高第一层:会话、上下文与大模型调用原理

2.1 无状态接口和有状态对话之间的矛盾

聊完了总体思路,我们把镜头拉近一点,看看最底层的“对话原理”。

HTTP 协议本身是无状态的。每一次请求都是独立的,服务端不会默认记录你是谁来、你之前说过什么。大模型接口也一样,你发过去一段 prompt,它返回一段回答,下次你再发,它是不知道你上次问过什么的。

但人类的对话天然是有状态的。用户说“刚才那个方案再细化一下”,你必须知道“刚才那个方案”指的是什么。这就是无状态接口和有状态对话之间的矛盾。

解决方案有很多,但最经典的就是“由你来保存状态”。每次用户发消息,你先从存储里把该用户最近的聊天记录取出来,拼成一个消息列表,连同用户当前这句话一起发给大模型。大模型看到完整的历史上下文,才能给出连贯的回答。

在若依项目里,这个存储我一般建议用 Redis。原因有三个:第一,若依本身就整合了 Redis,开箱即用;第二,对话数据的读取频率极高,MySQL 撑不住高频读写,而 Redis 的读写性能好很多;第三,对话数据有很强的时效性,很容易给每条会话设置过期时间,比如 7 天自动清理,省得库表无限膨胀。

我见过一个直接的做法:把聊天记录存在 MySQL 表里,每次对话都 SELECT 一次最近 20 条消息,再用 limit 翻页。用户量小的时候没问题,用户量一大,这个表会变成访问最频繁的表,最后只能加索引、加缓存,绕了一大圈回到 Redis。

2.2 Token 和上下文窗口:为什么用“最近 N 轮”而不是全部

解决了“存哪里”的问题,下一个问题是“存多少”。

很多第一次接入大模型的人,踩过最深的坑就是:把所有聊天记录一股脑全发给模型,结果模型直接报错。因为大模型的输入和输出加起来,有一个上限,叫上下文窗口。以常见的中文模型为例,有的是 8K token,有的是 128K token。你一个会话聊了两个月,所有消息加上去早就超了。

Token 是个什么东西?你可以把 Token 理解成大模型处理文字时的“基本单位”。中文里一个字通常对应 1 到 2 个 token,英文里一个单词大约 1.3 个 token,代码里一个普通字符也可能算 0.25 个 token。它跟“字”不一样,所以没办法简单地用字数去算,只能估算或者用 SDK 的计数器去数。

整理消息列表时,我更建议做“滑动窗口”。意思就是:我只取这个会话最近 N 轮对话,比如最近 10 轮或者最近 3000 个 token 的内容,更早的历史不直接发给模型。这个方案简单粗暴,但非常稳,不会因为对话太长而报错,成本也可控。

但这样做有个问题:如果用户很早之前提过一个重要背景,滑动窗口后面的内容就丢失了。解决办法是“摘要压缩”。当历史消息超过某个长度阈值时,先调用一次大模型,把已有内容总结成一段简短摘要,再把摘要和新消息一起发给模型。这本质上是牺牲一点信息密度,换来有限的上下文窗口能覆盖更长的会话周期。

如果你做的是文档问答,那就不是简单的摘要能解决的了。你需要引入向量数据库:把文档切成小段,用 Embedding 模型转成向量存起来;用户提问时,把问题也转成向量,然后做相似度搜索,找到相关片段,再把片段塞进 prompt 里交给大模型生成回答。这个流程叫 RAG(检索增强生成)。用若依做外壳、用向量数据库做知识库、用大模型做回答,这就是目前企业知识库问答的标准架构了。

2.3 普通请求和流式请求(SSE)底层发生了什么

如果说“上下文管理”是意识层面的原理,那“流式输出”就是用户能直接感知到的原理。

普通请求是这样的:前端发起一个 HTTP 请求,后端收到以后,去调用大模型接口,大模型经过几秒甚至几十秒的推理,生成完整回答,然后一次性返回给前端。这个过程中,用户只能看到“加载中”转圈。如果模型生成的时间长,界面就好像死了一样,体验非常差。

流式请求不一样。大模型在生成第一个字的时候就立刻往回传,后端收到一块数据就往客户端推一块,客户端拿到一个 token 就渲染一个 token。用户看到的视觉效果就是“文字一个一个蹦出来”,虽然实际等待总时长差不了太多,但用户会觉得系统“活着”,体验好得多。

在协议层面,流式输出通常有两种方式:一种是 SSE(Server-Sent Events,服务端推送事件),适合纯文本单向流式输出;另一种是 WebSocket,适合双向实时交互。聊天场景用 SSE 就够了,因为它方向明确,实现也简单。

SSE 的响应头是Content-Type: text/event-stream,数据格式大致是这样的:

data: {"delta": "你"} data: {"delta": "好"} data: [DONE]

前端通过EventSource或者fetch的ReadableStream来一段一段读取。这里我想特别提醒一句:如果把后端放在 Nginx 后面,一定要关掉 Nginx 对 SSE 的缓冲。否则你的流式数据会被 Nginx 攒到一大块才吐给前端,效果就变成等待一段时间突然蹦一堆字,流式就白做了。这个坑在第四章“常见问题”里还会专门讲。

3. 拔高第二层:架构设计的去耦合与扩展性

3.1 把 AI 封装成独立服务,而不是写在业务代码里

如果把 AI 接入这件事做一个简单的层次划分,你会发现它并不是“一个 Controller + 一个 Service”就能写完的。它至少包含几个层次:接口接收与参数校验、会话历史组装、模型供应商选择、请求调用与流式接收、结果解析与持久化、成本统计与审计日志。

这些层次如果全部堆在一个业务 Service 里,代码会在第三个月变成一团浆糊。更合理的做法是:在若依项目里,把 AI 相关模块单独建一个包,再进一步拆成独立的“AI 服务端项目”。哪怕初期规模不大,也至少要在模块边界上划清楚。

举个例子,我接过一个项目,用户要求“对接多个大模型,以后可能还要换”,最开始是直接在 Service 里写了一个if (modelType.equals("qwen"))的方法。后来要接入第二个模型,他复制了一大段,把整个 Service 改得越来越大。最后我要重构时,发现改一个公共参数,有二十多处隐性依赖。

我的建议是:哪怕是单体项目,也按微服务的思想来划边界。若依只负责对外暴露 HTTP 接口,AI 服务内部独立处理会话和模型调度。这样做的好处很多:AI 服务可以横向扩容,比如对话服务扛不住的时候单独加机器;AI 服务可以独立部署新模型版本,不影响业务;AI 服务的压力隔离,不会把业务数据库连接池拖垮。

3.2 用策略模式 + 工厂统一管理多模型接入

模型接入这块,我已经“踩过不止一遍”了。很多团队的模型接入代码长这样:

if ("qwen".equals(model)) { // 调用通义千问 } else if ("gpt".equals(model)) { // 调用 OpenAI 兼容接口 } else if ("local".equals(model)) { // 调用本地 Ollama }

这个写法在刚接入一个模型的时候没问题,但只要你接第二个,就该难受了。因为每个模型的 API 地址不同、鉴权方式不同、请求体不同、返回格式不同。如果你用 if-else 写,每加一个新模型,就要把所有涉及调用的大方法都打开改一遍,很容易把别的模型的逻辑改崩。

正确的做法是定义一个统一的接口,叫ChatProvider,每个模型对应一个实现类,然后用一个工厂根据配置返回对应的实现。

public interface ChatProvider { String getProviderName(); void chat(ChatRequest request, StreamEmitter emitter); } @Component public class QwenProvider implements ChatProvider { @Override public String getProviderName() { return "qwen"; } @Override public void chat(ChatRequest request, StreamEmitter emitter) { // 具体调用通义千问的逻辑 } } @Component public class OllamaProvider implements ChatProvider { @Override public String getProviderName() { return "ollama"; } @Override public void chat(ChatRequest request, StreamEmitter emitter) { // 具体调用本地 Ollama 的逻辑 } }

工厂类负责把容器里所有的ChatProvider收集起来,按名称返回:

@Component public class ChatProviderFactory { private final Map<String, ChatProvider> providerMap; public ChatProviderFactory(List<ChatProvider> providers) { this.providerMap = providers.stream() .collect(Collectors.toMap(ChatProvider::getProviderName, p -> p)); } public ChatProvider getProvider(String name) { return Optional.ofNullable(providerMap.get(name)) .orElseThrow(() -> new RuntimeException("未找到模型供应商: " + name)); } }

这样做的好处一眼就能看出来:以后要接入新模型,只需要加一个实现类,加上@Component注解,工厂会自动把它收进来,业务代码一行都不用改。这里面的核心思想叫“开闭原则”,翻译成人话就是:对扩展开放,对修改关闭。加功能不依赖改老代码,而是加新代码。

为什么我这么强调这个?因为大模型的迭代速度实在太快了。你今年接的 A 模型,半年后可能就被 B 模型替代了。如果你用了策略模式加工厂,替换模型只需要改配置文件或数据库字段,线上切换 10 分钟搞定。否则,你就要重新走一遍开发的完整流程。

3.3 从单模型到 AI Agent:多 AI 协作与工作流

搜索热词里出现的“AI Agent”、“多AI协作”、“AI工作流”、“教别人用AI赚翻了”这些词,看起来离若依很远,但实际它们之间有一条清晰的逻辑主线:当你把单次“一问一答”做顺了,接下来一定会遇到“让 AI 自动完成一个任务”的需求。

普通问答模型,你问它“帮我写一段营销文案”,它会把文案写出来。但 Agent 不是这样,Agent 的核心是“模型会调用工具”。比如你说“帮我把最近一周的销售数据整理成报表,然后发给部门群”,Agent 需要这样执行:先识别出意图,然后调用“查询销售数据”这个工具,接着调用“生成报表”这个工具,再调用“发送消息”这个工具,最后把完整结果告诉你。

在若依体系里落地 Agent,有一个天然的切入点:若依有权限体系、有业务 API、有定时任务、甚至有 workflow 流程引擎。你完全可以把 Agent 的工具调用,映射为若依的业务接口。例如,你可以给模型提供一份工具描述清单,告诉它有哪些工具、每个工具需要什么参数;模型在生成回答时,不是直接输出文字,而是输出一个工具调用指令,后端解析以后去执行对应的若依接口,然后把结果返回给模型,让模型继续生成。

这里涉及到一个进阶概念叫“Function Calling”,目前主流大模型基本都支持。原理不复杂,但落地到若依时,有个关键点:你必须在服务端把工具的权限校验做完整。因为若依本来就是一个带权限的系统,一个普通用户调用 AI,AI 内部再去调若依的接口查数据,必须判断这个用户到底有没有查这些数据的权限,而不能让 AI“代提权”。否则 AI Agent 就会变成一个巨大的越权漏洞。

我的建议是,不要一上来就追求复杂的 Agent,先把“单轮问答”和“多轮上下文”做扎实,再逐步加“单工具调用”,最后再上“多工具编排”。否则你会被模型的不确定性和错误工具调用搞得焦头烂额。

4. 拔高第三层:若依里的落地细节和踩坑

4.1 Spring Boot 异步与线程池:并发一高就超时

在真正的若依整合代码里,有一个非常容易出现“诡异现象”的地方,就是同步调用导致的线程池耗尽。

想象一下这个场景:前端请求后端,后端在请求线程里同步去调用大模型接口。大模型少则两三秒,多则十几秒才返回。Tomcat 默认的工作线程池并不大,如果你有 200 个请求进来,很快就占满了。接下来的请求全部排队,用户看到的就是“转圈圈转很久”,然后网关超时。

解决的思路,是把 AI 调用改成异步的:后端接口收到请求后,立刻返回一个SseEmitter给前端,然后把真正的大模型调用丢到一个独立的线程池里执行。这样,接收请求的线程马上释放,可以继续处理其他请求,而大模型推理在业务线程池里慢慢跑,跑完一个代码块就推一个给前端。

线程池的参数不能拍脑袋。我分享一个我自己常用的配置思路:核心线程数设置为 CPU 核数 + 1,最大线程数设置为 CPU 核数 * 2,队列容量根据你的并发量和模型响应时间计算。比如平均每个请求耗时 5 秒,你希望同时能容忍 50 个并发,那队列容量至少要有 50 * 5 = 250。这里还要考虑模型的并发限流,如果大模型供应商限制你每分钟只能调用 10 次,那线程池开得再大也没用,反而会把你的调用打到限流报错。

还有一个特别容易被忽略的坑:如果你在 Spring 事务里调用大模型,那整个事务期间数据库连接一直被占用,非常伤数据库连接池。所以,凡是涉及 AI 调用的服务,一定要先提交事务再做模型调用,或者干脆在事务外面做。你可以在业务方法上只对“更新数据那一步”加事务,不要在包含模型调用的整个方法上直接加@Transactional。

4.2 缓存、限流、用户体系打通:保证安全和成本

讲完了异步,再讲讲成本和安全。

先把话说透:大模型是按 token 计费的,不像普通接口只要不崩随便调。如果不做限流,一个用户恶意刷对话,可能一晚上就让你烧掉一个不小的预算。若依本身自带 Redis,我建议在 AI 接口上直接用 Redis 做一个简单的滑动窗口限流。

思路很简单:每个用户有一个 Redis 键,比如ai:rate:{userId},每次调用时记录当前时间戳,然后统计最近一分钟调用次数,超过阈值就拒绝。这个实现不复杂,你可以封装成一个注解,写在 Controller 方法上,比如@AiRateLimit(limit = 10, period = 60),每次进入方法前先检查。

还有更重要的,是用户体系打通。若依的登录认证是基于 Spring Security 和 JWT 的,AI 接口不要做成一个完全开放的匿名接口。我之前见过一个项目,为了前端调试方便,接口路径直接设置了anon放行,后果是任何人都可以直接 POST 这个地址消费模型,连登录都不用。这就是成本和安全双重失控。

正确做法是:AI 相关的 Controller 统一放在需要登录的路径下,用若依现有的@PreAuthorize注解控制权限。至少做到“必须登录才能对话,管理员可以选择给哪些角色开放”。

另外,建议对每个用户每天的 token 消耗做记录。若依有现成的操作日志功能,你可以仿照它的日志注解,写一个@AiAuditLog,把每次调用的用户、模型、输入 token、输出 token、消耗金额、耗时都存进一张表。这张表既能为成本分析提供数据,也能在出现纠纷时帮你查出具体调用记录。

4.3 前端 Vue3 + TS 的常见雷区

搜热词里有个词条“若依vue3 ts报错”,非常真实。若依 Vue3 版本是 Vue3 + TypeScript 的组合,很多从 JavaScript 转过来的开发者在整合流式输出时,会遇到一堆 TS 类型问题。

第一个高频问题是:自定义的axios封装返回类型和实际返回类型不一致。若依自带了一个request.ts封装,统一对返回结构做了处理。但当你对接 AI 流式接口时,不能直接用axios的普通返回,因为流式要求的响应类型是text/event-stream。你要么用fetch,要么用axios的responseType: 'stream'。许多人在封装层卡了半天,就是因为没意识到流式接口和普通接口的响应格式根本不一样。

第二个高频问题是:TypeScript 的严格模式报错。Vue3 版本的若依默认开了严格模式,所以你在写onDownloadProgress或者ReadableStream解析时,会碰到一堆类型不匹配。比如reader.read()返回的done是布尔值,你写成if (reader.read().done)就错了,你得先取出结果再判断。这些小细节对老手来说不算事,对新入门的人来说很劝退。

第三个高频问题是:路径别名解析。Vue3 若依工程用@指向src目录,如果你新装了一些依赖没配置对应类型,会遇到 “Cannot find module” 的报错。通常处理方式是检查tsconfig.json里的paths配置和vite.config.ts里的 alias 是否一致。

前端这块,我建议把 AI 对话封装成一个独立的useChatStream组合式函数,把建立连接、接收数据、更新状态、错误处理全部收敛到里面。业务组件只关心消息列表和输入框,不要让组件的逻辑被流式解析细节淹没。

下面是我常用的一个 fetch 流式解析核心片段,大家在 base 上扩展即可:

async function chatStream(message: string, sessionId: string) { const res = await fetch(`/api/ai/chat/${sessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }), }); if (!res.ok) throw new Error('请求失败'); const reader = res.body!.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 按行解析 SSE 数据 const lines = buffer.split('\n'); buffer = lines.pop()!; for (const line of lines) { if (line.startsWith('data:')) { const data = line.replace('data:', '').trim(); if (data === '[DONE]') return; const json = JSON.parse(data); // 把 json.delta 追加到你的消息列表里 } } } }

4.4 代码示例:一个可落地的流式聊天接口

聊完原理,咱们看一段能直接用的后端示例。假设我们已在若依工程里新建了一个AiChatController,前端通过 POST 请求发送用户消息,后端通过 SSE 逐步返回大模型生成结果。

@RestController @RequestMapping("/ai/chat") public class AiChatController { @Resource private AiChatService aiChatService; @PostMapping("/{sessionId}") public SseEmitter chat(@PathVariable String sessionId, @RequestBody ChatCreateDTO dto) { SseEmitter emitter = new SseEmitter(180000L); aiChatService.streamChat(sessionId, dto.getMessage(), emitter); return emitter; } }

注意几点:第一,SseEmitter的超时时间要设置大一些,至少要留足大模型生成时间,我一般设 3 分钟;第二,这个方法不能直接同步调用大模型,而是要把任务丢到线程池里,所以 Service 层应该用@Async或者主动创建一个线程池。

Service 层的核心逻辑大致是这样的:先根据sessionId从 Redis 取出最近的历史消息,把当前用户消息追加进去,然后组装大模型请求,调用ChatProviderFactory获取对应模型的 Provider,最后将流式输出写进SseEmitter。

@Service public class AiChatServiceImpl implements AiChatService { @Resource private ChatProviderFactory chatProviderFactory; @Resource private StringRedisTemplate stringRedisTemplate; @Override public void streamChat(String sessionId, String message, SseEmitter emitter) { // 从配置或数据库获取当前用户的模型名,这里简化处理 String providerName = "qwen"; List<ChatMessage> history = getHistory(sessionId); history.add(new ChatMessage("user", message)); ChatRequest request = new ChatRequest(history, providerName); chatProviderFactory.getProvider(providerName) .chat(request, new SseStreamEmitter(emitter)); // 把本次用户消息和最终回答保存到 Redis saveHistory(sessionId, history); } }

这里有一个很重要的小细节:SseEmitter必须支持超时完成和错误回调。如果模型调用过程中突然断了,你至少要给前端返回一个error事件,否则前端会一直以为还在生成中。写法如下:

emitter.onCompletion(() -> emitter.complete()); emitter.onTimeout(() -> emitter.completeWithError(new RuntimeException("timeout"))); emitter.onError((e) -> emitter.completeWithError(e));

另外,历史消息在保存时,不要把大模型返回的超长完整结果全存成 Redis 的 String,建议做压缩或者截断处理。因为会话一旦多了,Redis 内存会被聊天记录吃光。更聪明的做法是只保留最近 N 轮,超过 N 轮的按摘要替换。

5. 常见问题与排查技巧实录

我把自己和团队在落地的过程中踩过的坑整理成一张速查表,如果你在生产环境遇到了类似的现象,可以直接对照着排查。

现象可能原因排查与解决
前端一直转圈,后端过很久才收到响应同步调大模型,请求线程被占满改成异步 + 独立线程池,用 SseEmitter 返回
SSE 流式变成“卡一会蹦一段”Nginx 缓冲了 SSE 响应在 Nginx 配置里关闭对接口的 proxy_buffering
对话记录串了,不同用户看到别人的聊天sessionId 只用了一个固定值会话 ID 必须绑定用户 ID,每次对话前校验归属
发一大段历史记录给模型就报错上下文超出模型 token 上限改成滑动窗口 + 必要时做摘要压缩
用户多调用几次就被限流没有成本控制加 Redis 限流 + 按用户 token 消耗统计
Vue3 TS 报错找不到响应类型axios 封装返回 type 不匹配使用 fetch + ReadableStream 解析
接口在 Postman 能用,前端不能用跨域或者 JWT 没有带过去确认若依的跨域配置和请求头携带 Token
大模型回答乱码,中文字符变问号编码不是 UTF-8确认后端和前端都用 UTF-8,响应头指定 charset
并发高时数据库连接池被耗尽AI 调用放在事务里,连接被长占把 AI 调用移出事务,事务只包必要的数据更新

这里额外补充几个细节排查经验。

第一,“前端收到一半就断流”的问题,别急着改代码,先看网关。若依本身带网关(微服务版)或者你可能部署了 Nginx。在 Nginx 配置里,针对/ai/chat这个路径,加入下面这几行:

location /ai/chat { proxy_pass http://你的后端地址; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; proxy_set_header Connection ''; chunked_transfer_encoding on; }

proxy_buffering off是关键,不关掉的话,Nginx 会把流式内容攒起来再一次性发出去,用户体验直接退化。

第二,排查 SSE 断流时,建议在浏览器控制台把 Network 面板打开,盯着响应时间线。正常情况应该看到一条持续的输出线,而不是一堆间隔的小段。如果看到的是间歇性的大块数据,基本可以断定是缓冲问题。

第三,JWT 过期导致 AI 接口返回 401,这个现象很容易被忽略。因为流式输出一开始是正常的,但你可能为了性能把 Token 从数据库里拿出来每天加载一次,过期时间到了以后,新请求带的是旧 Token,后端校验失败了。这个问题处理起来很简单,把 Token 校验时间缩短,或者每次请求都从若依当前登录用户那里获取 Token,不缓存。

第四,也是我自己吃过大亏的一点:本地测试模型响应正常,部署到服务器以后响应特别慢。排查了半天,发现是 AI 服务出网方式的问题。国内访问海外模型接口延迟很高,如果你部署在国内服务器,建议优先选择国内云厂商的模型,或者走合规的代理方式,否则用户体验就是“一个字一个字蹦,但一句等很久”。

6. 我最后一次实际操作中的体会

如果你问我,若依整合 AI 最难的部分是什么,我可能会回答:不是代码,而是思维方式的切换。

传统的开发,输入一个确定性的参数,返回一个确定性的结果,你几乎能预测所有边界情况。但大模型是概率性的,你给它同一段 prompt,它可能每次返回不同;你让它调用工具,它偶尔会犯蠢,调错参数,甚至伪造结果。所以接入 AI 之后,你的系统设计必须额外考虑“模型不靠谱”这个前提。所有关键业务操作不能被模型直接决定,模型只能给出建议,最终执行必须落在确定性的代码里。

我的第二个体会是,不要贪多求快。我见过太多人一上来就想做一个全能的 AI Agent,最后卡在工具调用不稳定的泥潭里。更务实的路径是:第一阶段只做“流式问答 + 会话历史”,把对话交互打磨顺;第二阶段做“知识库检索”,用 RAG 解决文档问答;第三阶段再加“工具调用”,选择两三个稳定的若依业务接口作为 Agent 工具。每一阶段都上线验证,再往前走。

还有一个小技巧,我想在这里分享给所有正在做类似项目的人:提前把 AI 调用的日志结构设计好。很多团队前期为了赶进度,只打了简单 log,上线后模型出了问题,拿着日志根本还原不出当时的 prompt 和响应,排查效率极低。我建议从第一天就记录完整的请求和响应摘要,至少包括用户 ID、会话 ID、模型名、输入 token、输出 token、耗时、状态。这些数据不仅能帮你排查问题,还能用来做成本和效果分析,好处会在后期逐步显现。

最后一个建议,算是给团队协作的:如果你负责的若依项目将来要交到别人手里维护,代码里的 AI 调用部分一定不要写一堆 if-else。用策略模式 + 工厂收口,维护的人只需要关注新增一个模型实现类,而不用理解整个业务链路。这个价值,会在你离开那个项目三个月后接到咨询电话时,感受得特别深。

这个系列我没有急着往前写那些“十分钟接入 ChatGPT 接口”的速通内容,是因为我觉得,若依整合 AI 这件事,真正值钱的不是那几行 HTTP 调用代码,而是背后这些架构取舍和运行原理。把原理搞清楚了,后面不管你接哪个模型、做什么级别的 AI 应用,心里都有底。

返回列表