混元開(kāi)源之力:Spring-Ai-hunyuan 項(xiàng)目功能升級(jí)與實(shí)戰(zhàn)體驗(yàn)
今天咱們繼續(xù)聊聊 spring-ai-hunyuan 這個(gè)項(xiàng)目。上次我們兼容了 spring-ai 的 1.0.0 正式版本之后,就暫時(shí)放了一陣子,沒(méi)怎么動(dòng)。最近倒是收到不少小伙伴反饋,說(shuō)混元的思考鏈功能為什么不返回結(jié)果。其實(shí),混元官方那邊提供的兼容 OpenAI 的方案,本質(zhì)上就是幫大家能快速接入,方便快捷,但也難免會(huì)有一些高級(jí)特性或者參數(shù)沒(méi)辦法支持。就算官方給了參數(shù),也未必能直接用上。
942f4b5f09c95c39554398f80f251538
所以最近我就抽時(shí)間重新開(kāi)發(fā)了一下,專(zhuān)門(mén)把思考鏈的集成做了進(jìn)來(lái)。同時(shí)順帶把 ASR(語(yǔ)音識(shí)別)和 TTS(語(yǔ)音合成)功能也加進(jìn)去了,這樣一來(lái),基本上跟文字生成相關(guān)的場(chǎng)景都給覆蓋了,功能更加完整了。
項(xiàng)目的源碼開(kāi)源在這兒,感興趣的小伙伴可以直接去看看: https://github.com/StudiousXiaoYu/spring-ai-hunyuan
目前我還沒(méi)寫(xiě)出詳細(xì)的實(shí)戰(zhàn)案例教程,不過(guò)今天先給大家簡(jiǎn)單介紹一下,方便大家能快速上手。所有的案例源碼也已經(jīng)全部開(kāi)源了,大家可以直接 clone 到本地跑起來(lái)試試: https://github.com/StudiousXiaoYu/spring-ai-hunyuan-example
項(xiàng)目集成
首先,咱們需要在 pom.xml 文件中集成相應(yīng)的依賴。只需要將以下依賴添加到你的 pom.xml 中就可以了:
<dependency>
<groupId>io.github.studiousxiaoyu</groupId>
<artifactId>spring-ai-starter-model-hunyuan</artifactId>
<version>${spring-ai-hunyuan.version}</version>
</dependency>好的,這樣就搞定了,挺簡(jiǎn)單的。現(xiàn)在我們已經(jīng)開(kāi)發(fā)到1.0.0.2版本了,除了混元生文功能外,還加入了思考鏈、文本轉(zhuǎn)語(yǔ)音、語(yǔ)音轉(zhuǎn)文本等功能。接下來(lái),我們需要在配置文件里加上你騰訊云的秘鑰信息,具體內(nèi)容如下:
spring.ai.hunyuan.secret-id=${HUNYUAN_SECRET_ID}
spring.ai.hunyuan.secret-key=${HUNYUAN_SECRET_KEY}申請(qǐng)地址如下:https://console.cloud.tencent.com/cam/capi
你直接新建秘鑰即可。
d33326c09c1f19d4d6ca7fe78599375d
場(chǎng)景演示
沒(méi)錯(cuò),經(jīng)過(guò)這些步驟后,我們就具備了所有必要的條件,可以直接用 SpringAI 混元框架來(lái)對(duì)接混元,進(jìn)行企業(yè)級(jí)開(kāi)發(fā)了。這樣一來(lái),開(kāi)發(fā)流程會(huì)更加順暢,功能也能更好地滿足企業(yè)需求,效率會(huì)大大提升。
模型注入
首先,我們需要將本章節(jié)需要用到的所有模型先注入進(jìn)來(lái)。這里簡(jiǎn)單介紹下。
private final ChatClient chatClient;
private final HunYuanAudioTranscriptionModel audioTranscriptionModel;
private final HunYuanAudioTextToVoiceModel textToVoiceModel;
public ChatClientExample(ChatModel chatModel, HunYuanAudioTranscriptionModel audioTranscriptionModel, HunYuanAudioTextToVoiceModel textToVoiceModel) {
this.chatClient = ChatClient.builder(chatModel).defaultAdvisors(new SimpleLoggerAdvisor()).build();
this.audioTranscriptionModel = audioTranscriptionModel;
this.textToVoiceModel = textToVoiceModel;
}這里使用的聊天模型默認(rèn)是hunyuan-pro,語(yǔ)音轉(zhuǎn)文本則用的是一句話識(shí)別接口,具體使用的模型是16k_zh-PY(支持中英粵三種語(yǔ)言)。需要注意的是,這個(gè)接口有一些限制,比如音頻時(shí)長(zhǎng)不能超過(guò)60秒,文件大小不能超過(guò)3MB。之所以選擇這個(gè)接口,是因?yàn)槟壳罢Z(yǔ)音轉(zhuǎn)文本技術(shù)主要集中在日常對(duì)話類(lèi)應(yīng)用,像大數(shù)據(jù)分析這種場(chǎng)景還沒(méi)有廣泛涉及,所以暫時(shí)是采用這個(gè)接口。如果你有疑問(wèn),可以參考一下官方文檔鏈接:點(diǎn)擊查看文檔。
至于文本轉(zhuǎn)語(yǔ)音,我們用的是101001(情感女聲),你可以查看音色列表來(lái)了解更多:點(diǎn)擊查看音色列表,如果需要更多信息,也可以參考這里:點(diǎn)擊查看詳細(xì)文檔。
如果你想調(diào)整模型的參數(shù),完全可以在配置文件中做修改。我已經(jīng)把參數(shù)配置開(kāi)放出來(lái),常見(jiàn)的參數(shù)如下:
#聊天模型切換
spring.ai.hunyuan.chat.options.model=
#語(yǔ)音轉(zhuǎn)文本模型切換
spring.ai.hunyuan.audio.transcription.options.engSerViceType=
#文本轉(zhuǎn)語(yǔ)音模型切換
spring.ai.hunyuan.audio.tts.options.voiceType=這只是其中的一個(gè)小例子,實(shí)際上官方提供的所有請(qǐng)求參數(shù)都被封裝在每個(gè)模型配置的 option 里面。如果你想了解更詳細(xì)的內(nèi)容,可以直接去看看官方文檔,或者你也可以查看我寫(xiě)的源碼,都會(huì)有很清楚的說(shuō)明。
基礎(chǔ)聊天
先來(lái)看下最基礎(chǔ)的生文操作,直接使用spring ai的官方示例即可。
對(duì)話
這里直接看下阻塞問(wèn)答和流式問(wèn)答,代碼如下:
@PostMapping("/chat")
public String chat(@RequestParam("userInput") String userInput) {
String content = this.chatClient.prompt()
.user(userInput)
.call()
.content();
log.info("content: {}", content);
return content;
}
@GetMapping("/chat-stream")
public Flux<ServerSentEvent<String>> chatStream(@RequestParam("userInput") String userInput) {
return chatClient.prompt()
.user(userInput)
.stream()
.content() // 獲取原始Flux<String>
.map(content -> ServerSentEvent.<String>builder() // 封裝為SSE事件
.data(content)
.build());
}因?yàn)槲覀儾捎昧肆魇絾?wèn)答的方式,通常最喜歡用前端通過(guò)SSE(Server-Sent Events)來(lái)實(shí)現(xiàn)。所以在這個(gè)地方,我也直接返回了ServerSentEvent,這樣方便前端對(duì)接。這里雖然沒(méi)有展示具體的頁(yè)面,但示例項(xiàng)目中已經(jīng)集成了Swagger文檔,你可以簡(jiǎn)單瀏覽一下,看看效果如何。
f5ee0d824ce45c4ad8d409a2cef3e5b2
結(jié)構(gòu)化對(duì)象
另外一個(gè)要說(shuō)的點(diǎn)是結(jié)構(gòu)化對(duì)象的兼容性,簡(jiǎn)單來(lái)說(shuō)就是系統(tǒng)能不能返回 Java 對(duì)象的信息。接下來(lái)我們看一下具體的代碼:
@GetMapping("/ai-Entity")
public ActorFilms aiEntity() {
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
return actorFilms;
}
/**
*當(dāng)前用戶輸入后,返回列表實(shí)體類(lèi)型的回答,ParameterizedTypeReference是一個(gè)泛型,用于指定返回的類(lèi)型。
* @return List<ActorFilms>
*/
@GetMapping("/ai-EntityList")
List<ActorFilms> generationByEntityList() {
List<ActorFilms> actorFilms = chatClient.prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {
});
return actorFilms;
}
public record ActorFilms(String actor, List<String> movies) {
}這個(gè)例子里,我們用了兩種不同的情況:一種是普通的單一類(lèi)型,另外一種是數(shù)組類(lèi)型。當(dāng)然,其實(shí)其他類(lèi)型的Map結(jié)構(gòu)也是支持的。不過(guò),能不能正常運(yùn)行,最終還是取決于模型的能力,看它是否支持這些結(jié)構(gòu)。
目前我用的hunyuan-pro模型還沒(méi)有報(bào)錯(cuò)。從返回的結(jié)果來(lái)看,大體上是沒(méi)問(wèn)題的,具體效果可以參考下面的截圖:
0bf0976f53f5cfc0617a6190f5fd2d5f
函數(shù)調(diào)用
另外,關(guān)于函數(shù)調(diào)用的部分,我們會(huì)提前準(zhǔn)備好一些寫(xiě)好的方法,并且把這些方法的參數(shù)暴露出來(lái),供大模型調(diào)用。先讓我們看看代碼是怎么寫(xiě)的吧。
@PostMapping("/ai-function")
String functionGenerationByText(@RequestParam("userInput") String userInput) {
HunYuanChatOptions options = new HunYuanChatOptions();
options.setModel("hunyuan-functioncall");
String content = this.chatClient
.prompt()
.options(options)
.user(userInput)
.tools(new DateTimeTools())
.call()
.content();
log.info("content: {}", content);
return content;
}
public class DateTimeTools {
@Tool(description = "Get the current date and time in the user's timezone")
String getCurrentDateTime() {
String currentDateTime = LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
log.info("getCurrentDateTime:{}",currentDateTime);
return currentDateTime;
}
}我這里簡(jiǎn)單展示了一下如何獲取當(dāng)前日期的工具,代碼沒(méi)有加入任何輸入?yún)?shù),主要就是看看它能否正常工作。順便提一下,我在代碼里指定了當(dāng)前使用的模型。之前的配置是換全局模型,但在這里,你只需要替換當(dāng)前對(duì)話中使用的模型就行了。因?yàn)槲覀冃枰袚Q到一個(gè)支持函數(shù)調(diào)用的大模型。
最后演示如下,如圖所示:
745f6cb93a52a727a7b6ea7d119afa27
思考鏈
新集成的思考鏈來(lái)了,簡(jiǎn)單來(lái)說(shuō),就是通過(guò)檢查大模型返回的數(shù)據(jù),看里面有沒(méi)有包含‘思考’的內(nèi)容。不過(guò)要注意,并不是所有的大模型都有這個(gè)功能,只有部分模型才會(huì)有類(lèi)似的思考內(nèi)容。代碼如下:
@PostMapping("/chat-think")
public String think(@RequestParam("userInput") String userInput) {
HunYuanChatOptions options = new HunYuanChatOptions();
options.setModel("hunyuan-a13b");
options.setEnableThinking(true);
ChatResponse chatResponse = this.chatClient.prompt()
.user(userInput)
.options(options)
.call().chatResponse();
HunYuanAssistantMessage output = (HunYuanAssistantMessage) chatResponse.getResult().getOutput();
String think = output.getReasoningContent();
String text = output.getText();
log.info("think: {}", think);
log.info("text: {}", text);
return text;
}
@PostMapping("/stream-think")
public Flux<ServerSentEvent<String>> streamThink (@RequestParam("userInput") String userInput){
HunYuanChatOptions options = new HunYuanChatOptions();
options.setModel("hunyuan-a13b");
options.setEnableThinking(true);
Flux<ServerSentEvent<String>> chatResponse = this.chatClient.prompt()
.user(userInput)
.options(options)
.stream()
.chatResponse()
.map(content -> (HunYuanAssistantMessage) content.getResult().getOutput())
.map(content -> {
String think = content.getReasoningContent();
String text = content.getText();
StreamResponse streamResponse;
if (think != null && !think.isEmpty()) {
streamResponse = new StreamResponse("thinking", think);
} else {
streamResponse = new StreamResponse("answer", text);
}
return ServerSentEvent.<String>builder()
.data(JSONUtil.toJsonStr(streamResponse))
.build();
});
return chatResponse;
}
@Data
@NoArgsConstructor
public class StreamResponse {
@JsonProperty("type")
private String type;
@JsonProperty("content")
private String content;
public StreamResponse(String type, String content) {
this.type = type;
this.content = content;
}
}同樣的,我這邊也寫(xiě)了兩種方案,一個(gè)是阻塞式的,另一個(gè)是流式返回內(nèi)容的。因?yàn)槟壳癝pring AI還沒(méi)有統(tǒng)一的思考鏈返回字段,所以如果你想要獲取思考鏈的內(nèi)容,得先把返回的信息類(lèi)轉(zhuǎn)換成我自己定義的信息類(lèi),才能提取出這些數(shù)據(jù)。而且還需要注意的是,你得設(shè)置enableThinking的值才行。
接下來(lái)我們來(lái)看一下效果,像圖上展示的那樣。
a6018743ab6eb5b5b8bf1bc53e9f6f91
因?yàn)槲抑荒芊祷氐焦潭ǖ淖侄卫铮匀绻阈枰粤魇降姆绞将@取思考鏈的話,你得先定義一個(gè)格式,方便前端去截取數(shù)據(jù)。我這邊已經(jīng)幫你定義好了,當(dāng)前的返回樣式就是這樣的,如圖所示。
30f8225c0de95f5d9c710830532188e0
通過(guò)type值,前端就可以方便的定義標(biāo)簽里的值了。
圖片理解
目前大模型已經(jīng)可以支持圖片理解了,但它暫時(shí)不能直接通過(guò)文字生成圖片,這其實(shí)是另外一個(gè)功能,需要單獨(dú)進(jìn)行對(duì)接。目前這個(gè)部分還沒(méi)有對(duì)接完成。以下是相關(guān)的代碼:
@PostMapping("/chatWithPic")
public String chatWithPic(@RequestParam("userInput") String userInput) {
var imageData = new ClassPathResource("/img.png");
var userMessage = UserMessage.builder()
.text(userInput)
.media(List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)))
.build();
var hunyuanChatOptions = HunYuanChatOptions.builder().model("hunyuan-turbos-vision").build();
String content = this.chatClient.prompt(new Prompt(userMessage, hunyuanChatOptions))
.call()
.content();
log.info("content: {}", content);
return content;
}
//https://cloudcache.tencent-cloud.com/qcloud/ui/portal-set/build/About/images/bg-product-series_87d.png
@PostMapping("/chatWithPicUrl")
public String chatWithPicUrl(@RequestParam("url") String url,@RequestParam("userInput") String userInput) throws MalformedURLException {
var imageData = new UrlResource(url);
var userMessage = UserMessage.builder()
.text(userInput)
.media(List.of(Media.builder()
.mimeType(MimeTypeUtils.IMAGE_PNG)
.data(url)
.build()
))
.build();
var hunyuanChatOptions = HunYuanChatOptions.builder().model("hunyuan-t1-vision").build();
String content = this.chatClient.prompt(new Prompt(userMessage, hunyuanChatOptions))
.call()
.content();
log.info("content: {}", content);
return content;
}目前我們支持兩種方式來(lái)上傳圖片,一種是直接使用本地圖片,另一種是通過(guò)在線的 URL 圖片都可以。不過(guò)呢,這樣的話,我們需要先構(gòu)建一些用戶信息,不能再像以前那樣只傳個(gè)簡(jiǎn)單的文本就能搞定了。咱們先看看效果如何吧。
a110fd3f0f5b845ba8d379f0b673e577
本地文件我也放在了案例項(xiàng)目中,你可以直接查看,和這個(gè)url的圖片是一致的。
語(yǔ)音轉(zhuǎn)文本
具體的注意事項(xiàng)前面已經(jīng)說(shuō)了,我們這里直接使用即可。代碼如下:
//https://output.lemonfox.ai/wikipedia_ai.mp3
@PostMapping("/audio2textByUrl")
public String audio2textByUrl(@RequestParam("url") String url) throws MalformedURLException {
Resource resource = new UrlResource(url);
String call = audioTranscriptionModel.call(resource);
log.info("text: {}", call);
return call;
}
@PostMapping("/audio2textByPath")
public String audio2textByPath(){
Resource resource = new ClassPathResource("/speech/speech1.mp3");
String call = audioTranscriptionModel.call(resource);
log.info("text: {}", call);
return call;
}好的,這里有兩種方式可以選擇,一種是用本地文件,另一種是用在線 URL。官方推薦使用騰訊云 COS 來(lái)存儲(chǔ)音頻并生成 URL 后提交請(qǐng)求,這樣做有幾個(gè)好處:首先,它會(huì)走內(nèi)網(wǎng)來(lái)下載音頻,能顯著減少請(qǐng)求的延遲;其次,使用這種方式不會(huì)產(chǎn)生外網(wǎng)流量費(fèi)用,也能幫助節(jié)省成本。
當(dāng)然,最后還是看你個(gè)人的需求和實(shí)際情況啦。效果如圖所示:
2c8b0e2c553555fa85fe9c78d2e7b837
文本轉(zhuǎn)語(yǔ)音
這部分也是已經(jīng)集成完畢,直接一行代碼即可完成調(diào)用,所有配置變動(dòng)都可以寫(xiě)到配置中,代碼如下:
@PostMapping("/text2audio")
public byte[] text2audio(@RequestParam("userInput") String userInput) throws MalformedURLException {
byte[] call = textToVoiceModel.call(userInput);
FileUtil.writeBytes(call, "D:/output.mp3");
return call;
}前端其實(shí)可以直接讀取音頻流,然后用一個(gè) <audio> 標(biāo)簽來(lái)播放。我這邊后臺(tái)是直接生成的 MP3 文件,主要是為了測(cè)試文件是否能正常播放。經(jīng)過(guò)測(cè)試,結(jié)果一切正常,播放效果也沒(méi)問(wèn)題。
小結(jié)
這次更新的 spring-ai-hunyuan 項(xiàng)目在功能上做了不少增強(qiáng),特別是在思考鏈、語(yǔ)音識(shí)別(ASR)和語(yǔ)音合成(TTS)方面。之前由于兼容性問(wèn)題,一些高級(jí)功能可能無(wú)法完全支持,而現(xiàn)在這些問(wèn)題已經(jīng)得到解決。新的版本 1.0.0.2 增加了這些功能,增強(qiáng)了項(xiàng)目的整體能力,特別是在與文本生成相關(guān)的場(chǎng)景中,用戶可以更加順暢地進(jìn)行開(kāi)發(fā)。
首先,項(xiàng)目源碼已經(jīng)開(kāi)源,大家可以直接從 GitHub 上查看,甚至根據(jù)提供的案例源碼快速上手。集成方面,也提供了簡(jiǎn)單易用的依賴配置和騰訊云秘鑰設(shè)置,幫助開(kāi)發(fā)者迅速搭建起開(kāi)發(fā)環(huán)境。
在實(shí)際功能上,這個(gè)版本加入了思考鏈、文本轉(zhuǎn)語(yǔ)音、語(yǔ)音轉(zhuǎn)文本等模塊,能夠讓開(kāi)發(fā)者更加方便地調(diào)用大模型進(jìn)行文本和語(yǔ)音的處理。對(duì)于語(yǔ)音識(shí)別和合成,使用騰訊云的接口能更好地處理音頻文件(如語(yǔ)音轉(zhuǎn)文字和文字轉(zhuǎn)語(yǔ)音)。另外,思考鏈功能的加入,更是讓模型能在生成回答的同時(shí),帶上思考過(guò)程,提升了交互的自然度。
具體到代碼實(shí)現(xiàn)上,項(xiàng)目的集成和配置都非常直觀,基本只需在 pom.xml 添加依賴、配置好秘鑰,并調(diào)整一些參數(shù)設(shè)置,就能實(shí)現(xiàn)各種功能。最基本的功能包括基于用戶輸入的聊天對(duì)話,支持流式和阻塞式問(wèn)答。而在結(jié)構(gòu)化對(duì)象的處理上,項(xiàng)目支持將聊天內(nèi)容轉(zhuǎn)換成 Java 對(duì)象格式返回,非常適合數(shù)據(jù)驅(qū)動(dòng)的應(yīng)用場(chǎng)景。
對(duì)于前端開(kāi)發(fā)者來(lái)說(shuō),流式問(wèn)答(SSE)可以非常方便地實(shí)現(xiàn)實(shí)時(shí)聊天功能,而思考鏈的集成則讓聊天更具智能化和邏輯性。雖然目前圖片生成還未完全對(duì)接,但語(yǔ)音轉(zhuǎn)文本和文本轉(zhuǎn)語(yǔ)音的功能已非常完善,提供了兩種方式(本地文件和 URL)來(lái)處理音頻數(shù)據(jù)。



































