2023-12-25 springboot webflux調(diào)用大模型stream接口

2024-01-11

調(diào)用google gemini 總結(jié)
在調(diào)用gemini返回mono數(shù)據(jù)正常,但是在steam調(diào)用時(shí)出現(xiàn)了3個(gè)問題较店,

  1. 如果 .bodyToFlux(String.class) 返回結(jié)果中的json有換行符,造成json無(wú)法解析
    解決方案:可以直接寫為.bodyToFlux(GeminiResponse.class),這樣可以無(wú)視換行符直接拿到結(jié)果列表
  2. gemini sream返回列表無(wú)法判斷是否是最后一句;
    為了兼容之前openai接口滓鸠,需要在接口調(diào)用結(jié)束后主動(dòng)給客戶端發(fā)一條標(biāo)記結(jié)束的消息。
Flux<CustomResponse> results = webClient.post()...;
CustomResponselast last = new CustomResponse();
return results.collectList()
          .flatMapIterable(list -> {
                list.add(last);
                return list;
          });
  1. gemini的聊天內(nèi)容組合要求如果聊天列表中的角色role是連續(xù)一樣的第喳,就要合并為同一條Content
List<Content> contents = messageList.stream().map...;
List<Content> mergeContents = new ArrayList<>();
contents.forEach(content -> {
   if (mergeContents.isEmpty()) {
                mergeContents.add(content);
            } else {
                int lastIndex = mergeContents.size() - 1;
                Content lastContent = mergeContents.get(lastIndex);
               if (lastContent.getRole().equals(content.getRole())) {
                   List<Part> parts = lastContent.getParts();
                   parts.addAll(content.getParts());
              } else {
                mergeContents.add(content);
             }
        }      
  });
request.setContents(mergeContents);

由于gemini暫時(shí)沒有java的sdk,分享下對(duì)應(yīng)的實(shí)體

@NoArgsConstructor
@Data
public class GeminiRequest {
    private List<Content> contents;
    private List<SafetySetting> safetySettings;
    private GenerationConfig generationConfig;
}
@NoArgsConstructor
@Data
public class Content {
    private GeminiRole role;
    private List<Part> parts;
}
@NoArgsConstructor
@Data
public class Part {

    private String text;
    private InlineData inline_data;

    @NoArgsConstructor
    @Data
    public static class InlineData {
     /**
       * mime_type : image/jpeg
       * data : '$(base64 -w0 image.jpg)'
       */
        private String mime_type;
        private String data;
    }
}
public enum GeminiRole {
    user, model
}

@Data
@NoArgsConstructor
public class SafetySetting {
    private HarmCategory category;
    private HarmBlockThreshold threshold;
}
public enum HarmCategory {
    HARM_CATEGORY_UNSPECIFIED,
    HARM_CATEGORY_DEROGATORY,
    HARM_CATEGORY_TOXICITY,
    HARM_CATEGORY_VIOLENCE,
    @Deprecated
    HARM_CATEGORY_SEXUAL,
    HARM_CATEGORY_MEDICAL,
    HARM_CATEGORY_DANGEROUS,
    HARM_CATEGORY_HARASSMENT,
    HARM_CATEGORY_HATE_SPEECH,
    HARM_CATEGORY_SEXUALLY_EXPLICIT,
    HARM_CATEGORY_DANGEROUS_CONTENT
}
public enum HarmBlockThreshold {
    HARM_BLOCK_THRESHOLD_UNSPECIFIED,
    BLOCK_LOW_AND_ABOVE,
    BLOCK_MEDIUM_AND_ABOVE,
    BLOCK_ONLY_HIGH,
    BLOCK_NONE
}
@NoArgsConstructor
@Data
public class GenerationConfig {
    private Double temperature;
    private Integer maxOutputTokens;
    private Double topP;
    private Integer topK;
    private List<String> stopSequences;
}

@Data
public class GeminiResponse {
    private PromptFeedback promptFeedback;
    private List<Candidate> candidates;
}
@Data
public class PromptFeedback {
    public List<SafetyRating> safetyRatings;
}
@Data
public class SafetyRating {
    private String category;
    private String probability;
}
@Data
public class Candidate {
    private Content content;
    private String finishReason;
    private int index;
    private List<SafetyRating> safetyRatings;
}

2023-12-25

之前用okhttp-sse調(diào)用chatgpt的接口時(shí)糜俗,感覺遇到接口異常處理不是很方便,嘗試使用webflux后代碼結(jié)構(gòu)簡(jiǎn)單了不少曲饱,特此分享下
1.引入webflux

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

2.簡(jiǎn)單寫個(gè)調(diào)用

@Slf4j
public class WebFluxDemo {
    private final static String host = "https://api.openai.com/";
    private final static String uri = "v1/chat/completions";

    public static void main(String[] args) throws InterruptedException {
        //proxy
        HttpClient httpClient = HttpClient.create()
                .proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP)
                        .host("127.0.0.1")
                        .port(1080));

        ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
        //default build
        WebClient build = WebClient.builder()
                .baseUrl(host)
                .clientConnector(connector)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
        //build request
        OpenAiRequest openAiRequest = new OpenAiRequest();
        openAiRequest.stream = true;
        openAiRequest.setTemperature(0.7);
        openAiRequest.setModel("gpt-3.5-turbo-16k");
        openAiRequest.setMax_tokens(256);
        List<GptMessage> message = new ArrayList<>();
        GptMessage gptMessage = new GptMessage();
        gptMessage.setRole("system");
        gptMessage.setContent("You will role-play as a Female named 'lily'\\nCharacter information:Romantic,Flirty,Lover\\nCharacter background:U are most beautiful girl in school and like me");

        message.add(gptMessage);
        GptMessage question = new GptMessage();
        question.setRole("user");
        question.setContent("hi");
        message.add(question);
        openAiRequest.setMessages(message);
        //send post
        Flux<String> response = build.post()
                .uri(uri)
                .contentType(MediaType.APPLICATION_JSON)
                .header("Authorization", "Bearer " + "your token")
                .bodyValue(JSON.toJSONString(openAiRequest))
                .retrieve()
                .bodyToFlux(String.class)
                .map(result -> {
                    //build response
                    if (StringUtils.isEmpty(result)) {
                        return "";
                    } else if (result.equals(SseData.DONE.value())) {
                        return result;
                    } else {
                        OpenAISteamResponse openAISteamResponse = JSON.parseObject(result, OpenAISteamResponse.class);
                        String content = openAISteamResponse.choices.get(0).delta.getContent();
                        if (StringUtils.isNotEmpty(content)) {
                            return content;
                        } else {
                            return "";
                        }
                    }
                });
        //subscribe
        response.subscribe((content) -> log.info("content [{}]", content));
        //keep alive
        while (true) {
            Thread.sleep(1000);
        }
    }
}

3.附錄
3.1關(guān)于遭到openai限流悠抹,欠費(fèi)等未知異常處理,可以用其他方式替代

.onErrorResume(e -> {
                    log.error("event source failure openai {}", e.getMessage());
                    return huggingFaceService.chatStreamFlux(dto);
                })

3.2關(guān)于webflux中的flux與mono
在一次mono方法中扩淀,我讓openai返回的多個(gè)結(jié)果并用\n換行符分開楔敌,結(jié)果mono中總是返回一個(gè)結(jié)果,在調(diào)式中發(fā)現(xiàn)原來(lái)flux與mono的差別就在返回結(jié)果的換行符上驻谆,如果把一個(gè)列表用換行符切割返回就是flux,如果用jsonString返回就是mono
3.3實(shí)體

@Data
public class OpenAiRequest {
    public String model;
    public List<GptMessage> messages;
    public Double temperature;
    public Integer max_tokens;
    public Boolean stream;
    public List<GptFunction> functions;
    public String function_call;
}
@Data
public class GptMessage {
    private String role;
    private String content;
    private String name;
    private FunctionCall function_call;
}

@Data
public class FunctionCall {
    private String name;
    private String arguments;
}
@Data
public class GptFunction {
    private Object parameters;
    private String description;
    private String name;
}
public enum SseData {
    DONE("[DONE]"),
    ;

    private final String value;

    SseData(final String value) {
        this.value = value;
    }

    public String value() {
        return value;
    }
}
@Data
public class OpenAISteamResponse {
    private String id;
    private String object;
    private int created;
    private String model;
    private List<Choice> choices;
}
@Data
public class Choice {
    private int index;
    private GptMessage delta;
    private String finish_reason;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卵凑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胜臊,更是在濱河造成了極大的恐慌勺卢,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件象对,死亡現(xiàn)場(chǎng)離奇詭異黑忱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)勒魔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門甫煞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人冠绢,你說(shuō)我怎么就攤上這事抚吠。” “怎么了弟胀?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵埃跷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我邮利,道長(zhǎng),這世上最難降的妖魔是什么垃帅? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任延届,我火速辦了婚禮,結(jié)果婚禮上贸诚,老公的妹妹穿的比我還像新娘方庭。我一直安慰自己厕吉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布械念。 她就那樣靜靜地躺著头朱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪龄减。 梳的紋絲不亂的頭發(fā)上项钮,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音希停,去河邊找鬼烁巫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛宠能,可吹牛的內(nèi)容都是我干的亚隙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼违崇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼阿弃!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起羞延,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤渣淳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后肴楷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體水由,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赛蔫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砂客。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呵恢,死狀恐怖鞠值,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渗钉,我是刑警寧澤彤恶,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站鳄橘,受9級(jí)特大地震影響声离,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘫怜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一术徊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鲸湃,春花似錦赠涮、人聲如沸子寓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斜友。三九已至,卻和暖如春垃它,著一層夾襖步出監(jiān)牢的瞬間鲜屏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工嗤瞎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留墙歪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓贝奇,卻偏偏與公主長(zhǎng)得像虹菲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掉瞳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容