3.ChatClient&Chat Model简化与AI模型的交互

1.Chat Model

对话模型是一种利用人工智能技术,能够生成类似人类对话响应的工具。通过向预训练语言模型(如 GPT 等)发送提示词或部分对话内容,模型依据自身训练数据及对自然语言模式的理解,生成对话的延续或完整回复,并返回给应用程序。应用程序可以将其呈现给用户或用于进一步处理。

Spring AI Chat Model API 设计目标为简单且可移植的接口,用于与各种人工智能模型进行交互,使开发人员能够在不同模型之间进行切换,且只需进行最少的代码更改。这种设计符合 Spring 的模块化和可互换性理念。同时,借助 Prompt(用于输入封装)和 ChatResponse(用于输出处理)等辅助类,Chat Model API 统一了与人工智能模型的通信。

2.ChatClient

ChatClient``对ChatModel与 StreamingChatModel进行了封装,采用了 Fluent API 的风格,可以进行链式调用。相比如ChatModel 原子类API,ChatClient屏蔽了与AI大模型的交互的复杂性,但是ChatModel API更灵活,尤其当系统中要接入多个大模型并且有个性化配置时。

Fluent API 是一种编程风格的 API 设计,其主要特点是通过方法链的方式来实现操作的连贯性和可读性

ChatModel类似于JDK中的核心JDBC库。ChatClient可以比作JdbcClient,它构建在ChatModel 之上,并通过 Advisor 提供更高级的功能,提供上下文记忆,上下文提示词扩充等功能。

3.接口详解

在这里插入图片描述

@GetMapping("/ai/zhipu")
    public ChatResponse zhipu(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {

        var zhiPuAiApi = new ZhiPuAiApi(System.getenv("ZHIPU_AI_API_KEY"));

        var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, ZhiPuAiChatOptions.builder()
                .model(ZhiPuAiApi.ChatModel.GLM_3_Turbo.getValue())
                .temperature(0.4)
                .maxTokens(200)
                .build());

        ChatResponse response = chatModel.call(
                new Prompt("Generate the names of 5 famous pirates."));
        return response;
    }

    @GetMapping("/ai/openapi")
    public ChatResponse openapi(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        var openAiApi = new OpenAiApi(System.getenv("OPENAI_API_KEY"));
        var openAiChatOptions = OpenAiChatOptions.builder()
                .model("gpt-3.5-turbo")
                .temperature(0.4)
                .maxTokens(200)
                .build();
        var chatModel = new OpenAiChatModel(openAiApi, openAiChatOptions);

        ChatResponse response = chatModel.call(
                new Prompt("Generate the names of 5 famous pirates."));
        return response;

    }


 // 流式响应
  Flux<ChatResponse> streamResponse = chatModel.stream(
    new Prompt("Generate the names of 5 famous pirates."));
3.1ChatClient接口
3.1.1 创建ChatClient方式

自动配置

Spring AI 提供了 Spring Boot 自动配置,创建一个 ChatClient.Builder Bean,以便注入到使用的类中,快速入门中使用的是此方式。

@RestController
class AiController {

    private final ChatClient chatClient;

    public AiController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/ai")
    String generation(String userInput) {
        return this.chatClient.prompt()
            .user(userInput)
            .call()
            .content();
    }
}

代码创建

可以通过设置属性 spring.ai.chat.client.enabled=false 来禁用 ChatClient.Builder 的自动配置。如果多个对话模型一起使用,这很有用。然后,为每个你需要的 ChatModel 以编程方式创建一个 ChatClient.Builder 实例。

/**
 * Spring AI supports Spring Boot 3.2.x and 3.3.x
 * JDK 17
 * zhipuai
 */
@RestController
public class AiController {
    
    private final ChatClient chatClient;
    //注入ZhiPuAi ChatModel
    public AiController(ZhiPuAiChatModel zhiPuAiChatModel) {
        //编码创建ChatClient对象
        this.chatClient= ChatClient.create(zhiPuAiChatModel);

    }
    @GetMapping("/ai")
    String generation(String msg) {
        //用户输入的信息提交给大模型,使用的是ChatClient与大模型交互。
        return this.chatClient.prompt()
                .user(msg)
                .call()
                .content();
    }
}
3.1.2 核心API

ChatClient 对象提供了三个prompt()重载方法,启动Fluent API。三个方法如下:

1.prompt(): 不带参数的方法可以构建用户、系统以及提示词的其他部分。

chatClient.prompt()
                .user(("你给我说一个笑话")
                .call()
                .content();

2.prompt(Prompt prompt): 该方法接受一个Prompt参数,创建一个带Prompt的Fluent API。

chatClient.prompt(new Prompt("你给我说一个笑话"))
                .call()
                .content();

3.prompt(String content): 该方法和之前的重载方法类似。它接收用户输入的文本内容。

chatClient.prompt("你给我说一个笑话")
                .call()
                .content();
3.1.3 响应结果

ChatClient API 提供了3种使用FluentAPI 格式化AI模型响应的方法。

  • 返回ChatResponse
    //同步返回ChatResponse
    @GetMapping("/chatResponse")
    ChatResponse chatResponse() {
        //用户输入的信息提交给大模型,使用的是ChatClient与大模型交互。
        return this.chatClient.prompt()
                .user("给我讲一个笑话")
                .call()
                .chatResponse();
    }
    
    //流式返回ChatResponse
    @GetMapping("/fluxChatResponse")
    Flux<ChatResponse> fluxChatResponse() {
        Flux<ChatResponse> output = chatClient.prompt()
                .user("给我讲一个笑话")
                .stream()
                .chatResponse();
        return output;
    }

  • 返回实体对象(Entity)
import java.util.List;
record ActorFilms(String actor, List<String> movies) {}
  //同步返回实体
  @GetMapping("/entity")
    ActorFilms entity() {
        ActorFilms actorFilms = chatClient.prompt()
                .user("随机生成周星驰的5部电影作品")
                .call()
                .entity(ActorFilms.class);
        return actorFilms;
    }
  • 返回String
   @GetMapping("/fluxString")
    Flux<String> fluxString() {
        Flux<String> output = chatClient.prompt()
                .user("给我讲一个笑话")
                .stream()
                .content();
        return output;
    }
  //同步返回字符串
 @GetMapping("/ai")
        String generation(String msg) {
            return this.chatClient.prompt()
                    .user(msg)
                    .call()
                    .content();//直接返回字符串
        }

大型语言模型(LLM)生成结构化输出的能力对于下游系统使用大模型能力非常重要。开发人员希望快速将人工智能模型的结果转换为可以传递给其他应用程序函数和方法的数据类型,例如 JSONXMLJava 类。后面的内容结构化输出章节会详细的介绍使用。

3.1.3 默认设置

ChatClient 通过设置默认值,在调用时只需要指定用户文本,无需在运行时代码中为每个请求设置系统文本。

  • 默认设置

    下面的示例中,配置系统文本"你是一个会讲笑话的智能助手,可以讲不同风格的笑话"。为了避免在运行时代码中重复设置系统文本,在@Configuration配置类中创建一个ChatClient实例。

@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个会讲笑话的智能助手,用周星驰幽默的风格讲笑话。")
                .build();
    }
}
@RestController
class AIController {
    //直接构造器注入,在Config已经创建了对象
    private final ChatClient chatClient;

    AIController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping("/ai/defaults")
    public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
        return Map.of("completion", this.chatClient.prompt().user(message).call().content());
    }
}
curl http://localhost:8080/ai/defaults?message=给我讲个笑话
{
  "completion": "有一天,唐僧师徒四人行至火焰山,见一老翁躺在地上,气息奄奄。唐僧说:“悟空,快去救救这位老翁。”悟空便将老翁扶起,问道:“老翁,你怎么了?”老翁答道:“唉,我被这火焰山烤得受不了了,想喝口水。”悟空便去取水,途中遇到一只乌鸦,乌鸦对悟空说:“悟空,你为何那么辛苦?我有一个办法,能让老翁马上凉爽。”悟空好奇地问:“哦?什么办法?”乌鸦说:“你把老翁放在我背上,我带你飞到北极,那里凉快极了!”悟空心想:“这办法不错,但我不能让唐僧担心。”于是,悟空带着乌鸦回到原地,对唐僧说:“师傅,我有个办法能让老翁凉爽,但需要你配合。”唐僧问:“什么办法?”悟空答:“你把老翁放在我背上,我带你飞到北极。”唐僧疑惑地问:“那你怎么办?”悟空笑着说:“放心吧,我会在乌鸦的背上飞。”\n\n这个笑话有点周星驰式的幽默,希望您喜欢!"
}
  • 带参数的系统设置
@Configuration
class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个会讲笑话的智能助手,用{style}幽默的风格讲笑话。")
                .build();
    }
}
@GetMapping("/ai/defaultsParameters")
public Map<String, String> defaultsParameters(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message,
                                              @RequestParam(value = "style") String style) {
    return Map.of("completion",
            this.chatClient.prompt()
                    .system(sp -> sp.param("style", style))
                    .user(message)
                    .call()
                    .content());
}

这个例子展示了,动态填充系统默认参数的方式,笑话的风格用户动态传入。

curl http://localhost:8080/ai/defaultsParameters?message=给我讲个笑话&style=赵本山
{
  "completion": "
那好,我给你讲一个有关于我和老张头的笑话。\n\n这天,我和老张头去赶集,正巧碰见一个卖耗子药的。卖耗子药的哥们儿在那儿吆喝:“我这耗子药,一吃就死,绝无仅有,无效退款!”老张头听了,好奇心起,就买了一包。\n\n回家的路上,老张头突然发现耗子药不见了,吓得他出了一身冷汗,连忙问我:“哎呀,本山啊,我这耗子药咋不见了?这可咋整啊?”\n\n我一看他紧张的样子,就逗他:“老张头,你是不是把耗子药揣裤兜里了?说不定是你自己忘带了。”\n\n老张头一拍大腿:“哎呦,还真是!我咋就忘了这茬呢!”说完,他赶紧掏出耗子药,小心翼翼地揣回裤兜里。\n\n到了家,老张头把耗子药放在老鼠出没的地方。没过多久,一只老鼠跑过来,闻了闻耗子药,然后夹着尾巴跑了。老张头一看,高兴地跟我说:“本山,你看,这耗子药真灵,老鼠一闻就跑了!”\n\n我笑了笑,说:“老张头,那是因为老鼠知道你这人太实在,它害怕吃了你的耗子药,回头还得给你送回来,说‘这耗子药味道不好,退款吧!
"
}
3.2 ChatModel接口

ChatModel接口定义如下:

  public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {

	default String call(String message) {
		...
	}
	default String call(Message... messages) {
        ...
	}
	@Override
	ChatResponse call(Prompt prompt);
  }

call(String message) 创建一个UserMessage发送给AI模型,执行调用返回String

call(Message... messages) 将多个Message发送给AI模型,执行调用返回String

call(Prompt prompt)发送一个Prompt提示词给AI模型,执行调用返回返回ChatResponse

Message 接口有多种实现,对应 AI 模型可以处理的消息类别,后面章节内容详细介绍。

3.3 StreamingChatModel接口

StreamingChatModel接口定义如下:

   public interface StreamingChatModel extends StreamingModel<Prompt, ChatResponse> {

	default Flux<String> stream(String message) {
       ....
	}

	default Flux<String> stream(Message... messages) {
	   ....
	}

	@Override
	Flux<ChatResponse> stream(Prompt prompt);

   }

stream(String message) 创建一个Message发送给AI模型,执行调用返回Flux String

stream(Message... messages) 将多个Message发送给AI模型,执行调用返回Flux String

stream(Prompt prompt)发送一个Prompt提示词给AI模型,执行调用返回返回Flux ChatResponse

stream() 方法采用与 ChatModel 类似的 StringPrompt 参数,但它使用反应式 Flux API 传输响应(流式)。

Spring中实现非阻塞的响应式编程模型使用的是Reactor框架,Reactor 提供了两种主要的异步序列APIFluxMonoFlux用于处理0到N个元素的异步序列,而Mono则用于处理0到1个元素的异步序列。

3.4 ChatOptions接口

ChatOptions 接口继承自 ModelOptions,定义了一些可传递给 AI 模型的选项,如模型名称、频率、最大令牌数等。每个特定模型的实现可以有自己的选项,启动时可设置默认配置,运行时可通过 Prompt 请求覆盖。

接口定义如下:

public interface ChatOptions extends ModelOptions {

	String getModel();
	Float getFrequencyPenalty();
	Integer getMaxTokens();
	Float getPresencePenalty();
	List<String> getStopSequences();
	Float getTemperature();
	Integer getTopK();
	Float getTopP();
	ChatOptions copy();

}

对于每个特定模型的 ChatModel/StreamingChatModel 实现都可以有自己的选项,这些选项可以传递给 AI 模型。例如,OpenAI Chat 模型有自己的选项,如 logitBiasseeduser

     var openAiChatOptions = OpenAiChatOptions.builder()
                .seed(1024).
                 user("user")
                .logitBias(new HashMap<>())
                .build();

4.总结

本章主要围绕 Spring AI 中的对话模型相关组件展开介绍,涵盖 Chat ModelChatClient 以及多个关联接口,详细阐述了它们的功能、设计目标、使用方式及相互关系。Spring AI 通过这些精心设计的对话模型组件及接口,为开发人员提供了一套功能丰富、灵活易用且高度模块化的工具集,助力在不同场景下高效开发与人工智能模型交互的应用程序,满足多样化的业务需求。

<think>好的,用户之前的问题是关于在使用Spring AI时,当添加了tools之后,流式输出无法正常工作。之前的回答中,我给出了两种解决方案:一种是禁用工具调用,另一种是正确处理工具调用并继续流式传输。现在用户的新问题是,如果这个问题是由于spring-ai-ollama-spring-boot-starter 1.0.0-spring-boot-starter的1.0.0-M6版本,这是一个Milestone版本,可能存在一些尚未修复的问题。需要考虑到OllamaSpring AI的集成可能存在兼容性问题,尤其是在处理工具调用和流式输出时。 接下来,用户的问题可能涉及几个方面:Ollama本身是否支持工具调用,Spring AI的Ollama starter在该版本中是否正确处理了工具调用和流式响应的交互,或者是否存在配置上的问题。 首先,我需要回忆一下Ollama的特性。Ollama主要用于本地运行大型语言模型,比如Llama 2等。这些模型可能不像OpenAI模型那样支持函数调用或工具调用。因此,当用户尝试在Ollama模型中使用tools参数时,模型可能无法正确响应,导致流式输出中断。 接下来,检查Spring AI的文档,特别是关于Ollama集成的部分,看看是否支持工具调用。在Milestone版本中,可能某些功能尚未完全实现或存在已知问题。例如,可能在Ollama的配置中,工具调用没有被正确处理,导致流式传输中断。 可能的解决步骤包括: 1. **确认Ollama模型是否支持工具调用**:需要检查Ollama的文档或社区讨论,确认所使用的模型(如Llama2)是否支持函数调用功能。如果模型本身不支持,那么添加tools参数会导致问题,流式输出无法继续。 2. **检查Spring AI Ollama Starter的版本兼容性**:Milestone版本可能不稳定,查看是否有更新的版本或快照版本修复了该问题。如果问题在后续版本中已修复,建议用户升级。 3. **配置参数调整**:可能需要在advisors中设置特定参数,比如禁用工具调用(类似之前提到的tool_choice参数),但由于Ollama可能不支持这些参数,可能需要其他方式。 4. **处理工具调用的响应**:如果Ollama不支持工具调用,但代码中仍然添加了tools,可能会导致异常或中断。需要移除不必要的tools,或者确保在Ollama环境下不启用工具调用。 5. **日志和错误分析**:建议用户启用详细日志,查看在流式传输过程中是否有异常抛出,或者Ollama返回的响应结构是否符合Spring AI的预期,特别是在涉及工具调用时。 6. **替代方案**:如果必须使用工具调用,可能需要切换回支持的工具调用模型,如OpenAI,或者在Ollama中使用自定义的提示工程来模拟工具调用,而不依赖Spring AI的tools参数。 在思考过程中,还需要考虑到用户可能对Spring AI和Ollama的集成不够熟悉,或者对工具调用的工作机制存在误解。因此,回答中需要明确说明Ollama的可能限制,并提供逐步的排查步骤和替代解决方案。 总结可能的解决方案: - 确认Ollama模型是否支持工具调用,如不支持,则避免使用tools参数。 - 升级到更高版本的Spring AI Ollama Starter,如果问题已修复。 - 调整配置,禁用工具调用相关设置。 - 修改代码,移除不必要的tools配置。 - 使用其他支持工具调用的模型,如OpenAI。 此外,还需要指导用户如何检查日志,确认是否有错误信息,以及如何测试在没有tools的情况下流式传输是否正常,以进一步隔离问题。</think>--- 在 **Spring AI 1.0.0-M6** 中结合 `spring-ai-ollama-spring-boot-starter` 使用工具(`tools`)时,若流式输出(`stream()`)中断,可能是由于以下原因及解决方案: --- ### **问题原因** 1. **Ollama 模型限制** 当前 Ollama 的多数模型(如 Llama2)**不支持原生工具调用(Function Calling)**,添加 `tools()` 会导致模型无法正确处理请求。 2. **Spring AI 兼容性问题** `spring-ai-ollama-spring-boot-starter` 的早期版本(如 1.0.0-M6)可能未完全适配工具调用流式输出的交互逻辑。 --- ### **解决方案** #### 方法 1:禁用工具调用(推荐) 如果不需要工具调用,直接移除 `tools()` 配置,强制模型生成文本流: ```java Flux<String> content = chatClient.prompt() .advisors(a -> a.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) .user(message) .stream(); // 移除 .tools(new DateTimeTools()) ``` --- #### 方法 2:升级 Spring AI 版本 1. **尝试最新版本** Spring AI 更新较快,1.0.0-M6 可能存在已知问题,建议升级到最新里程碑版本或快照(如 `1.0.0-M7` 或更高): ```xml <!-- pom.xml --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> <version>1.0.0-M7</version> <!-- 或最新版本 --> </dependency> ``` 2. **检查版本兼容性** 确保 Ollama 服务版本 Spring AI 兼容(例如 Ollama `0.1.23+`)。 --- #### 方法 3:自定义工具调用处理 若必须使用工具,需手动处理工具调用逻辑(Ollama 可能返回原始 JSON 请求): ```java Flux<String> flux = chatClient.prompt() .user(message) .tools(new DateTimeTools()) .stream() .flatMap(response -> { // 1. 检查是否为工具调用请求 if (response.getMetadata().containsKey("tool_call")) { String toolCallJson = response.getMetadata().get("tool_call").toString(); // 2. 解析 JSON 并执行工具 ToolResult result = executeTool(toolCallJson); // 3. 将结果追加到对话中,重新生成流 return chatClient.prompt() .user(new UserMessage("Tool Result: " + result)) .stream(); } else { return Flux.just(response.getContent()); } }); ``` --- #### 方法 4:配置 Ollama 模型参数 在 `application.yml` 中明确配置 Ollama 模型参数,避免模型误解析工具请求: ```yaml spring: ai: ollama: base-url: http://localhost:11434 chat: model: llama2 # 确保模型支持工具调用(如 mistral 部分版本) options: temperature: 0.7 num_ctx: 4096 ``` --- ### **关键排查步骤** 1. **确认模型支持性** 检查 Ollama 模型是否支持工具调用(如 `mistral` 较新版可能支持,而 `llama2` 不支持)。 2. **查看日志** 启用 DEBUG 日志,观察 Ollama 返回的原始响应: ```yaml logging: level: org.springframework.ai: DEBUG ``` 3. **简化测试** 移除 `tools()` 后验证流式输出是否正常,确认问题是否由工具调用引起。 --- ### **替代方案** 如果必须使用工具调用,建议切换至支持该功能的模型服务(如 **OpenAI** 或 **Anthropic**): ```java // 切换为 OpenAI @Autowired OpenAiChatClient openAiChatClient; Flux<String> flux = openAiChatClient.prompt() .user(message) .tools(new DateTimeTools()) .stream(); ``` --- ### **总结** - **Ollama 模型限制** 是核心问题,多数模型暂不支持工具调用。 - 若需流式输出,优先移除 `tools()` 或升级 Spring AI/Ollama 版本。 - 如需完整工具调用流程,建议使用 OpenAI 等支持 Function Calling 的模型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

laopeng301

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值