当前位置: 首页 > news >正文

Dify AI 聊天接口后端代理

实现基于 Spring Boot 的 Dify AI 聊天接口代理,支持流式响应,并排查接口调用异常问题

一、Dify 代理接口实现原理

1. 核心功能

通过后端代理转发前端聊天请求至 Dify AI 平台(https://api.dify.ai/v1/chat-messages),实现用户鉴权、参数透传、流式响应返回,核心流程如下:

  1. 鉴权层:从请求头获取accessToken,调用UserFeignClient校验用户身份,未登录则返回标准化超时响应;
  2. 参数解析层:解析前端传入的query(聊天问题)、conversationId(会话 ID)等参数,校验必填项;
  3. 请求构建层:封装 Dify 平台要求的请求参数(含inputsresponse_mode=streaminguser等),添加 Dify API Token 认证头;
  4. 流式调用层:基于 Spring WebFlux 的WebClient调用 Dify 流式接口,接收 SSE(Server-Sent Events)流式响应;
  5. 响应返回层:将 Dify 返回的流式数据直接透传给前端,异常时封装标准化错误信息(遵循 SSE 格式)。

2. 技术选型

  • 核心框架:Spring Boot + Spring WebFlux(支持异步流式响应);
  • HTTP 客户端:WebClient(替代传统 HttpClient,适配流式响应);
  • 服务注册 / 发现:Nacos(服务间调用及路由转发);
  • 接口规范:SSE(text/event-stream)流式响应,兼容前端实时接收聊天回复。
package cn.com.xxx.mainservice.rest; import cn.com.xxx.common.feign.user.UserFeignClient; import cn.com.xxa.common.model.database.User; import cn.com.xxx.common.util.BaseUtils; import cn.com.xxxa.common.util.StaticValue; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; @RestController public class ChatRest { @Qualifier("cn.com.xxx.common.feign.user.UserFeignClient") @Autowired private UserFeignClient userFeignClient; private final String DIFY_API_URL = "http://192.xxx.x.193/v1/chat-messages"; private final String DIFY_API_TOKEN = "app-kam4xxxxxxx50klwoJH"; // 替换为实际token /** * 构建WebClient(Spring WebFlux原生工具) */ private final WebClient webClient = WebClient.builder() .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) // 调整缓冲区大小 .build(); /** * Dify聊天Rest接口(流式响应) * 返回类型为Flux<String>,直接返回SSE流式数据 */ @RequestMapping( value = "/rest/v1/chat/difyProxy", method = RequestMethod.POST, produces = MediaType.TEXT_EVENT_STREAM_VALUE // 声明SSE响应类型 ) public Flux<String> difyChatProxy(@RequestBody String jsonBody, HttpServletRequest request) { // ========== 解析前端聊天参数 & 鉴权 ========== String accessToken = request.getHeader(StaticValue.HEADER_ACCESSTOKEN); User loginUser = userFeignClient.getUserByAccessToken(accessToken); // 鉴权失败:返回流式错误信息 if (loginUser == null) { String errorData = String.format("data: %s\n\n", JSON.toJSONString(BaseUtils.loginTimeoutReturn())); return Flux.just(errorData); } String loginUserName = loginUser.getName(); JSONObject parmJson = JSON.parseObject(jsonBody); String query = parmJson.getString("query"); String conversationId = parmJson.getString("conversationId"); // 查询为空:返回流式错误信息 if (BaseUtils.isBlankStr(query)) { String errorData = String.format("data: %s\n\n", JSON.toJSONString(BaseUtils.httpErrReturn())); return Flux.just(errorData); } // ========== 构建Dify请求参数 ========== Map<String, Object> difyParam = new HashMap<>(); difyParam.put("inputs", new HashMap<>()); // 固定空inputs difyParam.put("query", query); // 前端传入的问题 difyParam.put("response_mode", "streaming"); // 流式响应模式 difyParam.put("user", loginUserName); // 用户名 difyParam.put("auto_generate_name", true); // 固定参数 // 可选参数:conversationId非空则添加 if (!BaseUtils.isBlankStr(conversationId)) { difyParam.put("conversation_id", conversationId); } // ========== 调用Dify流式接口并返回 ========== return webClient.post() .uri(DIFY_API_URL) // 设置Dify认证头 .header(HttpHeaders.AUTHORIZATION, "Bearer " + DIFY_API_TOKEN) .contentType(MediaType.APPLICATION_JSON) .bodyValue(JSON.toJSONString(difyParam)) .retrieve() // 处理非2xx响应 .onStatus(status -> !status.is2xxSuccessful(), clientResponse -> { return clientResponse.bodyToMono(String.class) .flatMap(errorMsg -> { String errorData = String.format("data: %s\n\n", JSON.toJSONString(Map.of( "error", "Dify接口调用失败:" + clientResponse.statusCode() + ",详情:" + errorMsg ))); return Mono.error(new RuntimeException(errorData)); }); }) // 解析流式响应为字符串 .bodyToFlux(String.class) // 日志打印 .doOnNext(line -> { if (line.startsWith("data: ")) { System.out.println("Dify流式数据:" + line); } }) // 异常处理:返回标准化错误信息 .onErrorResume(e -> { String errorData = String.format("data: %s\n\n", JSON.toJSONString(Map.of( "error", "服务端代理异常:" + e.getMessage() ))); return Flux.just(errorData); }) // 响应完成日志 .doFinally(signalType -> { System.out.println("Dify流式响应接收完毕,信号类型:" + signalType); }) // 设置响应头(关键:声明SSE格式) .contextWrite(ctx -> { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_EVENT_STREAM); headers.set("Cache-Control", "no-cache"); headers.set("Connection", "keep-alive"); return ctx.put("responseHeaders", headers); }); } }

一开始把功能写在了微服务各个层(rest、controller、service、dto、feign等),但简单业务(没有涉及数据库操作)没必要写得那么复杂,具体业务具体分析吧。

二、问题排查记录

问题 1:编译报错 - 程序包 org.springframework.web.reactive.function.client 不存在

现象

编译ChatRest.java时提示程序包org.springframework.web.reactive.function.client不存在,无法识别WebClient类。

根因

项目依赖配置异常:

  1. 重复引入spring-webflux依赖且未指定版本;
  2. spring-boot-starter-webflux依赖冗余(已包含spring-webflux核心包,无需单独引入)。
解决方案
  1. 移除重复的spring-webflux依赖,仅保留spring-boot-starter-webflux核心依赖;
  2. 刷新 Maven 依赖并重新编译项目,确保WebClient相关包正常引入。

问题 2:运行报错 - 找不到主类cn.com.xxx.mainservice.MainServiceMainApplication

现象

启动项目时提示 “找不到或无法加载主类”,无法正常启动服务。

根因
  1. 主类包名与文件目录结构未严格匹配(Java 编译规范要求);
  2. 编译产物缺失(target/classes下无主类.class文件);
  3. IDE 缓存 / 编译缓存未清理,导致加载旧配置。
解决方案
  1. 核对主类包名(cn.com.geohwa.mainservice)与文件路径(src/main/java/cn/com/geohwa/mainservice/)一致性;
  2. 执行mvn clean compile清理并重新编译,生成完整的.class文件;
  3. 清理 IDE 缓存(IDEA:Invalidate Caches / Restart),重新导入项目。

问题 3:接口调用报错 - 404 Not Found

现象

前端 / ApiPost 请求/api/v1/chat/difyProxy返回 404,提示 “请求的资源不存在”。

根因
  1. 接口路径不匹配:代码中接口注解为/rest/v1/chat/difyProxy,但请求路径为/api/v1/chat/difyProxy(缺失rest前缀);
  2. 网关路由配置缺失:初始仅配置/api/v1/user/**路由,未添加/api/v1/chat/**路由规则;
临时解决方案

补充 Gateway 路由配置,通过RewritePath/api/v1/chat/**重写为/rest/v1/chat/**,转发至main-service服务。

问题 4:路由转发异常 - 请求被转发至同事的服务实例

现象

网关路由配置生效后,请求仍未正常响应,排查发现请求被转发至同网段(192.xxx.x.202:8848)另一同事的main-service实例。

根因

Nacos 注册中心中,多实例使用相同服务名(main-service)且部署在同一 Nacos 节点(192.xxx.x.202:8848),网关负载均衡随机转发至非目标实例。

解决方案
  1. 修改本地服务的 Nacos 服务节点为201
http://www.cnnetsun.cn/news/26240.html

相关文章:

  • 数字孪生软件开发公司
  • springboot基于vue的校园报修管理系统设计与实现_t45k51ip
  • 嵌入式彩屏单色字体点阵的存储结构设计
  • 《Medical Vision Generalist: Unifying Medical Imaging Tasks in Context》(医学视觉通才:在上下文中统一医学成像任务)的
  • 西安电子科技大学专属信纸模板:3分钟打造专业学术形象
  • 【每日一题】PCIe答疑 - 接大量 GPU 时主板不认设备或无法启动和MMIO的可能关系?
  • 富有的哈佛人 —— 储蓄:财富积累的第一块基石
  • 终极指南:快速掌握eventpp事件处理库的8种集成方法
  • 光刻胶用二正丁基胺增感剂:
  • Spyder vs Jupyter:科学计算效率大比拼
  • 【第八天】08c#今日小结
  • Windows临时文件夹清理指南:释放C盘空间
  • AI助力:用自然语言生成复杂tar命令,告别记忆负担
  • 三相L型并网逆变器:dq坐标系下的控制系统设计与Simulink仿真模型搭建
  • RBP神经网络PID自适应控制模型(Matlab仿真模型及详解资料包,省去PID参数调节
  • 华为OD机试双机位C卷 - 挑选宝石 (C++ Python JAVA JS GO)
  • 用ROS2快速验证机器人创意:48小时开发挑战
  • 光伏电池电网能量管理控制策略模型仿真与优化在Simulink平台下的研究
  • 在flac3d7.0中实现flac3d和3dec的耦合计算
  • 3分钟用requestIdleCallback打造性能分析工具
  • 如何用Skyvern在5分钟内构建智能Web自动化工作流
  • 必看!2025年OK镜保养注意事项高品质推荐榜单,助你提升视力体验
  • LLC谐振变换器:变频与移相混合控制的仿真模型
  • 科研人员必备:Sci-Hub论文下载与管理的自动化方案
  • 基于Android的安卓云笔记系统(源代码+文档+PPT+调试+讲解)
  • HAMA.bundle:打造专属动漫图书馆的终极解决方案
  • 5分钟搭建texlive安装教程原型
  • 2025刷屏事件背后:一场正在席卷每个人的“能力革命”
  • 源代码加密方案深度解析与选型指南
  • 企业微信Linux客户端开发效率提升300%的秘诀