简介#

Spring AI提供了一些功能来影响LLM返回的响应。

  • Setting generation options: 可以设置一些generation的参数,比如temperature,max tokens等,来影响LLM下个token的选择,从而影响最终的response。
  • Structured Output Converter: 可以将LLM的response转为一个结构化的对象,方便后续处理。
  • Streaming Response: 可以实时获取LLM生成的结果,生成一部分就返回一部分,提升用户体验。

指定Chat的options#

切换模型#

Spring AI中每个AI Provider都可以选择模型,选择较小的模型,可以加快响应速度。比如我本地用ollama,我可以切换成deepseek-r1:1.5b模型,来加快响应速度。

在application.yaml中指定模型

spring:
  ai:
    ollama:
      chat:
        # model: mistral:7b
        model: deepseek-r1:1.5b

可以看到响应速度有了明显提升

模型 Trial 1 Trial 2 Trial 3
mistral:7b 42.93s 29.63s 22.00s
deepseek-r1:1.5b 21.26s 14.68s 16.79s

但是较小模型的准确率会有所下降,有时生肖的数据都没有生成(虽然原先也是错误的)。

 curl "http://localhost:8080/fortune-today?fullName=zhangsan&birthDate=1990-05-15" | jq 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1416    0  1416    0     0     96      0 --:--:--  0:00:14 --:--:--   317
{
  "fortune": "1. **基本信息**  \n- 姓名:zhangsan  \n- 星座:金牛座  \n- 生肖:【根据出生年份计算】(五岁入学的生肖是牛)  \n\n2. **今日综合运势** (2025-11-22 )  \n   整体运势:⭐⭐⭐(5颗星)  \n   运势简评:在当今社会,他们可能会经历一些压力和挑战,但总体而言感情观积极向上,事业潜力巨大。  \n\n3. **详细运势分析**  \n   - 爱情运势:金牛座的人性格热情且充满活力,在爱情方面可能会遇到一些年龄较大的限制,但只要注意调节心态,依然有很多机会寻找真爱。事业运势:虽然可能在工作中面临一些压力,但他们的创造力和抗压能力使得事业发展空间广阔,潜力巨大。财务运势:财务状况良好,有良好的收入来源,同时也可以合理规划理财,避免不必要的开支。健康运势:整体健康状况较好,适合追求快乐的人群,如果有身体不适应及时就医。  \n\n4. **幸运元素**  \n   - 幸运颜色:红色、黄色、银色  \n   - 幸运数字:8、4、6  \n   - 幸运方位:北极星(红色)、东方(黄色)、南方(银色)  \n\n5. **今日建议**  \n   - 调整心态,避免因年龄带来的固执感,适当放松心情。  \n   - 积极融入社会,多与他人交流,增强自信感。  \n   - 关注健康问题,及时就医。"
}

影响token生成#

当生成一个响应时,它是一次一个Token生成的。API会考虑原始prompt,然后使用模型来选择紧随prompt的下一个Token。依此类推,直到整个响应生成完毕。最终的Token是通过结合统计概率和随机选择确定的。

使用诸如TemperatureTop-pTop-kChatOption,你可以影响选择的随机程度,以及某些 Token 被选中的可能性。Spring AI对不同模型都提供了各自的参数设置,比如Ollama可以看Ollama Chat Options

这些都可以在application.yaml中进行配置

spring:
  ai:
    ollama:
      chat:
        # http http://localhost:11434/api/tags -b to see available models
        model: mistral:7b
        # model: deepseek-r1:1.5b
        options:
          temperature: 0.8
          # maxTokens: 1024
          top-k: 40
          top-p: 0.9
Temperature Trial 1 Trial 2 Trial 3
0.8 15.96s 11.18s 14.28s
1.5 24.40s 24.31s 24.91s

虽然返回的生肖还都是错误的,但是temperature较低的情况下,响应时间更快一些。

其他的各种options也可以逐个调整,观察对响应时间和结果的影响。

格式化Response输出#

Structured Output Converter

StructuredOutputConverter只是尽力(best effort)将模型结果转为一个结构化的输出,但是AI模型本身并不保证一定会返回符合预期格式的结果。在必要的情况下,可以实现一个对response的校验机制,确保结果的格式符合预期。

我希望今日运势返回的结果是一个JSON对象。原先的prompt模版需要调整。

你是一位专业的星座和生肖运势分析师。请根据以下信息生成今日运势:

用户姓名:{name}
出生日期{birthday}
星座{zodiac}
今天是{today_date}

{format}

请用温暖、积极的语气,让运势分析既专业又富有启发性。

返回的结果Entity为

public record FortuneTodayResponse(
    String name,
    String zodiac,
    String chineseZodiac,
    String overallRating,
    String overallSummary,
    String loveFortune,
    String careerFortune,
    String wealthFortune,
    String healthFortune,
    String luckyColor,
    String luckyNumber,
    String luckyDirection,
    List<String> advice) {
}
@Override
  public FortuneTodayResponse getFortuneToday(UserInfo userInfo) {
    // 解析日期
    LocalDate birthDate = LocalDate.parse(userInfo.birthDate());
    // 计算星座
    String zodiacSign = Objects.requireNonNull(ZodiacUtils.getZodiacSign(birthDate), "zodiacSign must not be null");

    // 定义结构化输出转换器
    var outputConverter = new BeanOutputConverter<>(FortuneTodayResponse.class);

    var responseSpec = chatClient.prompt()
        .user(userSpec -> userSpec.text(templateResource)
            .param("name", userInfo.fullName())
            .param("birthday", userInfo.birthDate())
            .param("zodiac", zodiacSign)
            .param("today_date", Objects.requireNonNull(String.valueOf(LocalDate.now())))
            .param("format", outputConverter.getFormat())) // 注入格式指令
        .call();

    var chatResponse = responseSpec.chatResponse();
    if (chatResponse == null) {
        throw new RuntimeException("AI response is null");
    }
    if (chatResponse.getMetadata() != null) {
        String model = chatResponse.getMetadata().getModel();
        logger.info("Model used for generating fortune today: {}", model);
    }

    var content = chatResponse.getResult().getOutput().getText();

    if (content == null) {
        throw new RuntimeException("AI response content is null");
    }
    // 将文本响应转换为对象
    return outputConverter.convert(content);
  }

这里使用了BeanOutputConverter,它的getFormat()方法实际上是增加了返回结果的prompt.

	/**
	 * Provides the expected format of the response, instructing that it should adhere to
	 * the generated JSON schema.
	 * @return The instruction format string.
	 */
	@Override
	public String getFormat() {
		String template = """
				Your response should be in JSON format.
				Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
				Do not include markdown code blocks in your response.
				Remove the ```json markdown from the output.
				Here is the JSON Schema instance your output must adhere to:
				```%s```
				""";
		return String.format(template, this.jsonSchema);
	}

结果会变成这样 Structured Output Converter

Streaming Response#

Spring AI中,流式响应的核心是使用Project Reactor库中的Flux类型。

LLM中的应用: 当使用流式API时, Flux<String> 会包含 LLM 逐个生成的简短字符串片段(通常是单个单词或Token)。每当一个片段可用时,它就会被发送给客户端。

限制#

流式响应和JSON输出转换(例如将结果直接映射到一个Java 对象)不能很好地结合使用。(上面的例子)

如果要进行输出转换,你必须先收集整个流,将其拼接成一个完整的 JSON 字符串,然后再进行解析。这样做就失去了使用流式响应(即时反馈)的意义。因此,最好不要将流式和输出转换混用。

将上面的例子进一步改成流式响应#

前端收到的将不再是一个完整的JSON对象,而是一连串的字符流,拼起来后才是一个完整的JSON

如果还是原先的访问方式 sse curl 可以写个简单的python脚本来拼接结果,效果如下

streaming response

源码#