实现功能

  • 透传ollama请求
  • 支持关闭think标签
  • 支持流式返回

具体实现

controller

@Slf4j
@RestController
@RequestMapping("/ollama")
@RequiredArgsConstructor
public class OllamaAgentController {

    private final OllamaAgentService agentService;

    /**
     * 流式智能体问答
     * @param ollamaDTO agentChatReqDTO
     * @return
     */
    @PostMapping(value = "/generate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<OllamResponse> streamChat(@RequestBody OllamaDTO ollamaDTO) {
        return agentService.streamChat(ollamaDTO);
    }

}

service

public interface OllamaAgentService {

    /**
     * 流式智能体问答
     * @param ollamaDTO agentChatReqDTO
     * @return
     */
    Flux<OllamResponse> streamChat(OllamaDTO ollamaDTO);

}

serviceImpl

@Slf4j
@Service
@RequiredArgsConstructor
public class OllamaAgentServiceImpl implements OllamaAgentService {


    private final OllamaChatModel ollamaChatModel;

    private final OllamaDialogueLogService agentDialogueLogService;
    @Override
    public Flux<OllamResponse> streamChat(OllamaDTO ollamaDTO) {
        UserDetail userDetail = UserUtil.currentUser();
        OllamaDialogueLogDTO dialogueLogDTO = new OllamaDialogueLogDTO(ollamaDTO);
        dialogueLogDTO.setUserId(userDetail.getUserId());
        StringBuilder aiResponseBuilder = new StringBuilder();
        Flux<OllamResponse> responseFlux = ollamaChatModel.stream(ollamaDTO.toPrompt())
                .scan(new ThinkTagState(ollamaDTO.isFilterThink()), (state, response) -> {
                    String chunk = response.getResult().getOutput().getText();
                    state.setChatResponse(response);
                    return state.process(chunk);
                })
                .filter(state -> StrUtil.isNotBlank(state.getFilteredText()))
                .map(response -> {
                    OllamResponse responseDTO = new OllamResponse(response.getChatResponse());
                    aiResponseBuilder.append(response.getChatResponse().getResult().getOutput().getText());
                    return responseDTO;
                }).doOnComplete(() -> {
                    dialogueLogDTO.setSystemOut(aiResponseBuilder.toString());
                    agentDialogueLogService.save(dialogueLogDTO.toDialogueLog());
                }).doOnError(e -> {
                    log.error("Error during AI chat stream: {}", e.getMessage());
                    dialogueLogDTO.setSystemOut(aiResponseBuilder.toString());
                    dialogueLogDTO.setAnswerType(1);
                    agentDialogueLogService.save(dialogueLogDTO.toDialogueLog());
                });

        if (ollamaDTO.isStream()){
            return responseFlux;
        }
        return mergeResponses(responseFlux);
    }


    /**
     * 合并响应
     * @param responseFlux 响应流
     * @return
     */
    private Flux<OllamResponse> mergeResponses(Flux<OllamResponse> responseFlux) {
        return responseFlux
                .collectList()
                .flatMapMany(list -> {
                    if (list.isEmpty()) {
                        return Flux.empty();
                    }
                    OllamResponse last = CollUtil.getLast(list);
                    String combinedText = list.stream()
                            .map(r -> r.getMessage().getContent())
                            .collect(Collectors.joining());
                    last.getMessage().setContent(combinedText);
                    return Flux.just(last);
                });
    }

}

think标签状态机


import org.springframework.ai.chat.model.ChatResponse;

public class ThinkTagState {
    private boolean inThink = false;
    private final StringBuilder buffer = new StringBuilder();

    private ChatResponse chatResponse;

    private boolean filerThink = false;

    public ThinkTagState(boolean filerThink) {
        this.filerThink = filerThink;
    }

    public ThinkTagState process(String chunk) {
        StringBuilder output = new StringBuilder();
        String remaining = chunk;
        if (!filerThink) {
            buffer.append(remaining);
            return this;
        }
        while (!remaining.isEmpty()) {
            if (!inThink) {
                int startIdx = remaining.indexOf("<think>");
                if (startIdx == -1) {
                    output.append(remaining);
                    break;
                }
                output.append(remaining.substring(0, startIdx));
                inThink = true;
                remaining = remaining.substring(startIdx + "<think>".length());
            } else {
                int endIdx = remaining.indexOf("</think>");
                if (endIdx == -1) {
                    break; // 等待后续分块
                }
                inThink = false;
                remaining = remaining.substring(endIdx + "</think>".length());
            }
        }

        buffer.append(output);
        return this;
    }

    public String getFilteredText() {
        return buffer.toString();
    }

    public ChatResponse getChatResponse() {
        return chatResponse;
    }

    public void setChatResponse(ChatResponse chatResponse) {
        this.chatResponse = chatResponse;
    }
}

请求类OllamaDTO


@Data
public class OllamaDTO {

    private String model;

    private boolean stream;

    private boolean filterThink;

    private List<MessageDTO> messages;


    public Prompt toPrompt() {
        List<Message> list = messages.stream().map(messageDTO -> {
            String role = messageDTO.getRole();
            if ("user".equals(role)) {
                return (Message)new UserMessage(messageDTO.getContent());
            } else if ("assistant".equals(role)) {
                return new AssistantMessage(messageDTO.getContent());
            } else if ("system".equals(role)) {
                return new SystemMessage(messageDTO.getContent());
            } else {
                throw new IllegalArgumentException("Unknown role: " + role);
            }
        }).toList();
        OllamaOptions.Builder optionsBuilder = OllamaOptions.builder();
        optionsBuilder.model(model);
        return new Prompt(list,optionsBuilder.build());
    }
}

请求示例



{
  "model": "qwen3:30b-a3b",
  "messages": [
    
    {
      "role": "user",
      "content": "qwen3:30b-a3b是什么/no_think"
    }
  ],
  "options": {
    "temperature": 0.5
  },
  "stream": false,
  "filterThink":true
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐