Skip to content
DAILY QUOTE

“ ”

数字分身

这个项目做了什么

这个项目用Spring Boot接入Dify,把用户的问题转发给Dify应用,再把AI生成的内容通过SSE一段一段推给前端。

简单理解:

text
用户浏览器
->Nginx
->Spring Boot后端
->Dify API
->LLM大模型

它不是普通的一问一答接口,而是偏“流式问答后端”。用户不用等AI完整生成完,前端可以边接收边显示,效果类似打字机。

核心流程

text
前端提交问题query
->后端进入/api/bot/chat
->没有sessionId就生成一个UUID
->WebClient调用Dify的/chat-messages接口
->Dify返回SSE流式数据
->后端解析每一段chunk
->只取message或text_chunk事件中的answer
->通过SseEmitter推给前端
->收到message_end或workflow_finished后关闭连接

我觉得难的地方

难点主要是流式响应。

普通HTTP接口是后端处理完后一次性返回结果,但AI回答是逐步生成的。如果后端等Dify全部生成完再返回,用户体验会比较差。所以这里需要后端一直保持连接,Dify返回一段,后端就解析一段,再推给前端一段。

另一个难点是限流。AI接口不能被无限刷,所以用自定义@RateLimit注解标记接口,再用AOP在真正执行接口前做访问频率检查。

用到的技术

  • SseEmitter:实现后端向浏览器持续推送数据
  • WebClient:调用Dify流式接口
  • ObjectMapper:解析Dify返回的JSON片段
  • Redisson RRateLimiter:按IP限制访问次数
  • Spring AOP:把限流逻辑从Controller中抽出来
  • Nginx:部署时转发前端请求到后端服务

关键实现

接口入口:

java
@GetMapping(value = "/chat", produces = "text/event-stream;charset=UTF-8")
@RateLimit(rate = 5, ratelnterval = 1)
public SseEmitter chat(@RequestParam String query,
                       @RequestParam(required = false) String sessionId) {
    if (sessionId == null || sessionId.isEmpty()) {
        sessionId = UUID.randomUUID().toString();
    }
    return difyService.streamChat(query, sessionId);
}

限流核心:

java
String ip = request.getHeader("X-FORWARDED-FOR");
if (ip == null || ip.isEmpty()) {
    ip = request.getRemoteAddr();
}

String key = "rate_limit:chat:" + ip;
RRateLimiter limiter = redissonClient.getRateLimiter(key);
limiter.trySetRate(RateType.OVERALL, rateLimit.rate(),
        rateLimit.ratelnterval(), RateIntervalUnit.MINUTES);

if (!limiter.tryAcquire()) {
    throw new RuntimeException("提问频繁");
}
return joinPoint.proceed();

遇到的问题

Nginx缓存SSE数据

SSE需要服务端实时推送,但Nginx可能会把响应先缓存起来,导致前端不是一段一段收到,而是等一批数据后才显示。

解决方式是在Nginx配置里关闭代理缓存:

nginx
proxy_buffering off;

代理后拿不到真实IP

部署到Nginx后,后端直接拿request.getRemoteAddr()可能拿到的是代理服务器地址,导致限流不准确。

所以优先读取X-Forwarded-For请求头,拿不到时再使用getRemoteAddr()

目前效果

目前主要投喂了Dify相关笔记,后续可以继续补充更多知识库内容。

网络架构图:

可以继续优化

  • 把Dify的API Key放到环境变量中,避免写死在配置文件
  • 限流异常可以返回更友好的提示
  • 前端可以保存历史对话
  • 后续可以加用户登录,用用户维度做限流