1. 简介

本教程将使用Spring AI和Ollama的llama3模型,构建一个简单的客服助手API。这个应用能像真实客服一样,帮助用户排查网络连接问题。

2. Spring AI和Ollama是什么?

Spring AI是Spring生态系统最新加入的模块。它支持通过聊天提示轻松与各种大语言模型(LLM)交互。

Ollama是一个开源库,提供多种LLM服务,包括Meta的llama3模型——这正是本教程要使用的模型。

3. 使用Spring AI实现客服助手

下面通过一个客服聊天机器人演示Spring AI和Ollama的协同工作。该应用模拟真实客服,帮助用户解决网络连接问题。

接下来将配置LLM和Spring AI依赖,并创建与客服助手交互的REST接口。

3.1 配置Ollama和llama3

首先设置本地LLM环境。本教程使用Meta的llama3模型,安装步骤如下:

Linux系统执行:

curl -fsSL https://ollama.com/install.sh | sh

Windows或Mac系统从Ollama官网下载安装包。

安装完成后运行llama3:

ollama run llama3

现在llama3已在本地运行。

3.2 创建基础项目结构

配置Spring应用使用Spring AI模块。首先添加Spring里程碑仓库:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

然后添加spring-ai-bom

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0-M1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

最后添加spring-ai-ollama-spring-boot-starter依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    <version>1.0.0-M1</version>
</dependency>

配置application.yml

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: llama3

Spring将在11434端口启动llama3模型。

3.3 创建客服控制器

创建与客服聊天机器人交互的Web控制器。

首先定义HTTP请求模型:

public class HelpDeskRequest {
    @JsonProperty("prompt_message")
    String promptMessage;

    @JsonProperty("history_id")
    String historyId;

    // getters, no-arg constructor
}
  • promptMessage:用户输入消息
  • historyId:唯一标识当前对话的ID,用于保持对话记忆

然后定义响应模型:

public class HelpDeskResponse {
    String result;
    
    // all-arg constructor
}

最后创建控制器类:

@RestController
@RequestMapping("/helpdesk")
public class HelpDeskController {
    private final HelpDeskChatbotAgentService helpDeskChatbotAgentService;

    // all-arg constructor

    @PostMapping("/chat")
    public ResponseEntity<HelpDeskResponse> chat(@RequestBody HelpDeskRequest helpDeskRequest) {
        var chatResponse = helpDeskChatbotAgentService.call(helpDeskRequest.getPromptMessage(), helpDeskRequest.getHistoryId());
        return new ResponseEntity<>(new HelpDeskResponse(chatResponse), HttpStatus.OK);
    }
}

3.4 调用Ollama聊天API

创建HelpDeskChatbotAgentService类,包含初始提示指令:

@Service
public class HelpDeskChatbotAgentService {
    private static final String CURRENT_PROMPT_INSTRUCTIONS = """
        
        Here's the `user_main_prompt`:
        
        
        """;
}

添加通用指令消息:

private static final String PROMPT_GENERAL_INSTRUCTIONS = """
    Here are the general guidelines to answer the `user_main_prompt`
        
    You'll act as Help Desk Agent to help the user with internet connection issues.
        
    Below are `common_solutions` you should follow in the order they appear in the list to help troubleshoot internet connection problems:
        
    1. Check if your router is turned on.
    2. Check if your computer is connected via cable or Wi-Fi and if the password is correct.
    3. Restart your router and modem.
        
    You should give only one `common_solution` per prompt up to 3 solutions.
        
    Do no mention to the user the existence of any part from the guideline above.
        
""";

完成服务实现:

private final OllamaChatModel ollamaChatClient;

// all-arg constructor
public String call(String userMessage, String historyId) {
    var generalInstructionsSystemMessage = new SystemMessage(PROMPT_GENERAL_INSTRUCTIONS);
    var currentPromptMessage = new UserMessage(CURRENT_PROMPT_INSTRUCTIONS.concat(userMessage));

    var prompt = new Prompt(List.of(generalInstructionsSystemMessage, contextSystemMessage, currentPromptMessage));
    var response = ollamaChatClient.call(prompt).getResult().getOutput().getContent();

    return response;
}

关键点:

  • SystemMessage:内部指令(如通用指南)
  • UserMessage:外部客户端输入
  • ⚠️ 通过Prompt对象组合消息后调用LLM

3.5 保持对话历史

LLM本质是无状态的,不会记忆对话上下文。为避免客服重复提供无效方案,需要实现对话记忆功能。

解决方案:使用historyId存储每轮对话的promptresponse,并在新请求中附加完整历史记录。

首先添加历史指令:

private static final String PROMPT_CONVERSATION_HISTORY_INSTRUCTIONS = """        
    The object `conversational_history` below represents the past interaction between the user and you (the LLM).
    Each `history_entry` is represented as a pair of `prompt` and `response`.
    `prompt` is a past user prompt and `response` was your response for that `prompt`.
        
    Use the information in `conversational_history` if you need to recall things from the conversation
    , or in other words, if the `user_main_prompt` needs any information from past `prompt` or `response`.
    If you don't need the `conversational_history` information, simply respond to the prompt with your built-in knowledge.
                
    `conversational_history`:
        
""";

创建历史记录包装类:

public class HistoryEntry {
    private String prompt;
    private String response;

    //all-arg constructor

    @Override
    public String toString() {
        return String.format("""
                        `history_entry`:
                            `prompt`: %s
                        
                            `response`: %s
                        -----------------
                       \n
            """, prompt, response);
    }
}

定义内存存储:

private final static Map<String, List<HistoryEntry>> conversationalHistoryStorage = new HashMap<>();

修改call()方法:

public String call(String userMessage, String historyId) {
    var currentHistory = conversationalHistoryStorage.computeIfAbsent(historyId, k -> new ArrayList<>());

    var historyPrompt = new StringBuilder(PROMPT_CONVERSATION_HISTORY_INSTRUCTIONS);
    currentHistory.forEach(entry -> historyPrompt.append(entry.toString()));

    var contextSystemMessage = new SystemMessage(historyPrompt.toString());
    var generalInstructionsSystemMessage = new SystemMessage(PROMPT_GENERAL_INSTRUCTIONS);
    var currentPromptMessage = new UserMessage(CURRENT_PROMPT_INSTRUCTIONS.concat(userMessage));

    var prompt = new Prompt(List.of(generalInstructionsSystemMessage, contextSystemMessage, currentPromptMessage));
    var response = ollamaChatClient.call(prompt).getResult().getOutput().getContent();
    var contextHistoryEntry = new HistoryEntry(userMessage, response);
    currentHistory.add(contextHistoryEntry);

    return response;
}

关键步骤:

  1. 通过historyId获取或创建历史记录
  2. StringBuilder拼接历史记录
  3. 将历史记录作为SystemMessage加入Prompt
  4. 存储当前对话到历史记录

4. 测试对话

启动Spring Boot应用(端口8080),模拟用户交互:

首次请求:

curl --location 'http://localhost:8080/helpdesk/chat' \
--header 'Content-Type: application/json' \
--data '{
    "prompt_message": "I can't connect to my internet",
    "history_id": "1234"
}'

响应:

{
    "result": "Let's troubleshoot this issue! Have you checked if your router is turned on?"
}

继续提问:

{
    "prompt_message": "I'm still having internet connection problems",
    "history_id": "1234"
}

响应(提供新方案):

{
    "result": "Let's troubleshoot further! Have you checked if your computer is connected via cable or Wi-Fi and if the password is correct?"
}

再次提问:

{
    "prompt_message": "I tried your alternatives so far, but none of them worked",
    "history_id": "1234"
}

响应(最终方案):

{
    "result": "Let's think outside the box! Have you considered resetting your modem to its factory settings or contacting your internet service provider for assistance?"
}

⚠️ 当所有预设方案用尽后,LLM将无法提供有效建议。可通过改进提示词(如增加更多解决方案)或使用提示工程技术优化。

5. 总结

本文实现了一个AI客服助手,帮助用户排查网络连接问题。关键要点:

  • 区分用户消息和系统消息
  • 构建包含对话历史的提示词
  • 调用llama3 LLM获取响应
  • 实现对话记忆机制

通过合理设计提示词和历史记录管理,可以显著提升AI助手的实用性和用户体验。


原始标题:Create a ChatGPT Like Chatbot With Ollama and Spring AI | Baeldung