初探 SpringAI, 一次漫长的 Debug 之旅

in 工作记录 with 0 comment

Spring Al 项目为开发 AI 应用程序提供了一个 Spring 友好的 API 和抽象。

https://github.com/spring-projects/spring-ai

阿里通义也提供了抽象的实现和 Demo:

https://github.com/alibaba/spring-cloud-alibaba/tree/2023.x/spring-cloud-alibaba-examples/spring-cloud-ai-example

对话想实现一个个字蹦出来的,一般要用到 SSE, 对应的 OpenAI 接口:

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "No models available",
    "messages": [
      {
        "role": "system",
        "content": "You are a helpful assistant."
      },
      {
        "role": "user",
        "content": "Hello!"
      }
    ],
    "stream": true
  }'

这里比较重要的是 "stream": true,决定了响应是流式调用。

因为无法抗拒的网络问题,我选使用代理服务去访问 api.openai.com,测试的时候,先用 OpenAI Node 成功的进行了测试。

https://github.com/openai/openai-node

然后就是 Spring AI 上了,结果却遇到了问题。

Spring AI 中接收 OpenAI 接口响应的 Body 中, 多出了个 "data:", 转换JSON 报错:

1714703064284.jpg

我的第一反应是 Spring AI 是不是有些问题,即使使用了代理服务, OpenAI Node 中可以正常工作。

带着这个疑问,开始了漫长的 Debug 之路...

首先, 响应的 body 中 "data:" 是没有问题的, SSE 规范就是这样,然后 Spring AI 使用了 Spring WebFlux 去发送这个请求,WebFlux 针对 SSE 请求会自动处理掉 "data:", 但是这里为什么还是收到了 "data:" ?

疑问很快转移到了 Spring WebFlux, 经过 Debug 发现了比较关键的代码:

org.springframework.http.codec.ServerSentEventHttpMessageReader

image.png

这里处理了 SSE 请求的 "data: ", 遂先关注这里,测试发现正常 SSE 的请求的确会走这里,但是我使用的这个代理站的返回却没走到这里。

这里基本确认了没处理 "data: " 的原因。 但是为什么没有走到这里?

随后在 Spring WebFlux 中的:

org.springframework.web.reactive.function.BodyExtractors#readWithMessageReaders

image.png

这里的逻辑是根据 content-type,去找到合适的 HttpMessageReader 读 Body. Debug 信息中显示里面有足足 15 个 Reader。

经过跟踪发现出现问题的请求响应进了:

org.springframework.http.codec.DecoderHttpMessageReader

ServerSentEventHttpMessageReader 和 DecoderHttpMessageReader 都是 org.springframework.http.codec.HttpMessageReader 接口的实现。

image.png

没有进 ServerSentEventHttpMessageReader 是因为代理服务器返回的

"content-type: text/plain; charset=utf-8",而不是正确的 "content-typ: text/event-stream" 。

image.png

最后的问题,为什么 OpenAI Node 可以呢?

我是懒得再 Debug 了,但是我觉得 Spring 的这个实现也是没有问题的,毕竟发错了响应头。