使用Java拓展本地开源大模型的网络搜索问答能力

背景

开源大模型通常不具备最新语料的问答能力。因此需要外部插件的拓展,目前主流的langChain框架已经集成了网络搜索的能力。但是作为一个倔强的Java程序员,还是想要用Java去实现。

注册SerpAPI

Serpapi 提供了多种搜索引擎的搜索API接口。

访问 Serpapi 官网上注册一个用户:

https://serpapi.com/

可以选择Free Plan,提供每月100次的免费使用。接下来就是使用自己的邮箱和手机号进行注册。

注册成功登录:

创建SerpApiHttp对象

java 复制代码
public class SerpApiHttp {

    private int httpConnectionTimeout;
    private int httpReadTimeout;

    /**
     * 后端服务地址
     */
    private static final String BACK_END = "https://serpapi.com";

    /**
     * 初始化Gson对象
     */
    private static Gson gson = new Gson();

    /**
     * 当前后端HTTP路径
     */
    public String path;

    /***
     * 构造函数
     * @param path HTTP url路径
     */
    public SerpApiHttp(String path) {
        this.path = path;
    }

    /***
     * 建立Socket连接
     *
     * @param path URL端点
     * @param parameter 客户端参数,如: { "q": "coffee", "location": "Austin, TX"}
     * @return HttpURLConnection 连接对象
     * @throws SerpApiException 包装错误信息
     */
    protected HttpURLConnection connect(String path, Map<String, String> parameter) throws SerpApiException {
        HttpURLConnection con;
        try {
            //allowHTTPS(); // 允许HTTPS支持
            String query = ParameterStringBuilder.getParamsString(parameter);
            URL url = new URL(BACK_END + path + "?" + query);
            con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("GET");
        } catch (IOException e) {
            throw new SerpApiException(e);
        } catch (Exception e) {
            e.printStackTrace();
            throw new SerpApiException(e);
        }

        String outputFormat = parameter.get("output");
        if (outputFormat == null) {
            throw new SerpApiException("output format must be defined: " + path);
        } else if (outputFormat.startsWith("json")) {
            con.setRequestProperty("Content-Type", "application/json");
        }
        con.setConnectTimeout(getHttpConnectionTimeout());
        con.setReadTimeout(getHttpReadTimeout());

        con.setDoOutput(true);
        return con;
    }

    /***
     * 返回HTTP响应内容的原始字符串
     *
     * @param parameter 用户客户端参数
     * @return HTTP响应体
     * @throws SerpApiException 包装错误信息
     */
    public String get(Map<String, String> parameter) throws SerpApiException {
        HttpURLConnection con = connect(this.path, parameter);

        // 获取HTTP状态码
        int statusCode = -1;
        // 保存响应流
        InputStream is = null;
        // 读取缓冲区
        BufferedReader in = null;
        try {
            statusCode = con.getResponseCode();

            if (statusCode == 200) {
                is = con.getInputStream();
            } else {
                is = con.getErrorStream();
            }

            Reader reader = new InputStreamReader(is);
            in = new BufferedReader(reader);
        } catch (IOException e) {
            throw new SerpApiException(e);
        }

        String inputLine;
        StringBuilder content = new StringBuilder();
        try {
            while ((inputLine = in.readLine()) != null) {
                content.append(inputLine);
            }
            in.close();
        } catch (IOException e) {
            throw new SerpApiException(e);
        }

        // 断开连接
        con.disconnect();

        if (statusCode != 200) {
            triggerSerpApiException(content.toString());
        }
        return content.toString();
    }

    /**
     * 在错误情况下触发异常
     *
     * @param content 从serpapi.com返回的原始JSON响应
     * @throws SerpApiException 包装错误信息
     */
    protected void triggerSerpApiException(String content) throws SerpApiException {
        String errorMessage;
        try {
            JsonObject element = gson.fromJson(content, JsonObject.class);
            errorMessage = element.get("error").getAsString();
        } catch (Exception e) {
            throw new AssertionError("invalid response format: " + content);
        }
        throw new SerpApiException(errorMessage);
    }

    /**
     * @return 当前HTTP连接超时时间
     */
    public int getHttpConnectionTimeout() {
        return httpConnectionTimeout;
    }

    /**
     * @param httpConnectionTimeout 设置HTTP连接超时时间
     */
    public void setHttpConnectionTimeout(int httpConnectionTimeout) {
        this.httpConnectionTimeout = httpConnectionTimeout;
    }

    /**
     * @return 当前HTTP读取超时时间
     */
    public int getHttpReadTimeout() {
        return httpReadTimeout;
    }

    /**
     * @param httpReadTimeout 设置HTTP读取超时时间
     */
    public void setHttpReadTimeout(int httpReadTimeout) {
        this.httpReadTimeout = httpReadTimeout;
    }
}

创建SerpApi对象

java 复制代码
public class SerpApi extends Exception {
    /**
     * 客户端参数
     */
    private final Map<String, String> parameter;

    /**
     * 初始化 gson
     */
    private static final Gson gson = new Gson();

    /**
     * Java 7+ 的 https 客户端实现
     */
    private final SerpApiHttp client;

    /**
     * 默认 HTTP 客户端超时时间
     */
    private static final Integer TIME_OUT = 60000;

    /**
     * 搜索路径
     */
    private static final String SEARCH_PATH = "/search";

    /***
     * 构造函数
     *
     * @param parameter 默认搜索参数,应包括 {"api_key": "secret_api_key", "engine": "google" }
     */
    public SerpApi(Map<String, String> parameter) {
        this.parameter = parameter;
        this.client = new SerpApiHttp(SEARCH_PATH);
        this.client.setHttpConnectionTimeout(TIME_OUT);
    }

    /***
     * 返回原始HTML搜索结果
     *
     * @param parameter HTML搜索参数
     * @return 从客户端引擎获取的原始HTML响应,用于自定义解析
     * @throws SerpApiException 封装后端错误消息
     */
    public String html(Map<String, String> parameter) throws SerpApiException {
        return get("/client", "html", parameter);
    }

    /***
     * 返回JSON格式的搜索结果
     *
     * @param parameter 自定义搜索参数,可覆盖构造函数中提供的默认参数
     * @return JSON对象,包含搜索结果的顶层节点
     * @throws SerpApiException 封装后端错误消息
     */
    public JsonObject search(Map<String, String> parameter) throws SerpApiException {
        return json(SEARCH_PATH, parameter);
    }

    /***
     * 使用Location API返回位置信息
     *
     * @param parameter 必须包括 {q: "city", limit: 3}
     * @return JSON数组,使用Location API返回的位置信息
     * @throws SerpApiException 封装后端错误消息
     */
    public JsonArray location(Map<String, String> parameter) throws SerpApiException {
        String content = get("/locations.json", "json", parameter);
        JsonElement element = gson.fromJson(content, JsonElement.class);
        return element.getAsJsonArray();
    }

    /***
     * 通过Search Archive API检索搜索结果
     *
     * @param id 搜索的唯一标识符
     * @return 客户端结果的JSON对象
     * @throws SerpApiException 封装后端错误消息
     */
    public JsonObject searchArchive(String id) throws SerpApiException {
        return json("/searches/" + id + ".json", null);
    }

    /***
     * 使用Account API获取账户信息
     *
     * @param parameter 包含api_key的Map,如果未在默认客户端参数中设置
     * @return JSON对象,账户信息
     * @throws SerpApiException 封装后端错误消息
     */
    public JsonObject account(Map<String, String> parameter) throws SerpApiException {
        return json("/account.json", parameter);
    }

    /***
     * 使用Account API获取账户信息
     *
     * @return JSON对象,账户信息
     * @throws SerpApiException 封装后端错误消息
     */
    public JsonObject account() throws SerpApiException {
        return json("/account.json", null);
    }

    /***
     * 将HTTP内容转换为JsonValue
     *
     * @param endpoint 原始JSON HTTP响应
     * @return 通过gson解析器创建的JSON对象
     */
    private JsonObject json(String endpoint, Map<String, String> parameter) throws SerpApiException {
        String content = get(endpoint, "json", parameter);
        JsonElement element = gson.fromJson(content, JsonElement.class);
        return element.getAsJsonObject();
    }

    /***
     * 获取HTTP客户端
     *
     * @return 客户端实例
     */
    public SerpApiHttp getClient() {
        return this.client;
    }

    /***
     * 扩展现有参数构建Serp API查询
     *
     * @param path 后端HTTP路径
     * @param output 输出类型(json, html, json_with_images)
     * @param parameter 自定义搜索参数,可覆盖默认参数
     * @return 格式化参数HashMap
     * @throws SerpApiException 封装后端错误消息
     */
    public String get(String path, String output, Map<String, String> parameter) throws SerpApiException {
        // 更新客户端路径
        this.client.path = path;

        // 创建HTTP查询
        Map<String, String> query = new HashMap(16);

        if (path.startsWith("/searches")) {
            // 仅保留API_KEY
            query.put("api_key", this.parameter.get("api_key"));
        } else {
            // 合并默认参数
            query.putAll(this.parameter);
        }

        // 用自定义参数覆盖默认参数
        if (parameter != null) {
            query.putAll(parameter);
        }

        // 设置当前编程语言
        query.put("source", "java");

        // 设置输出格式
        query.put("output", output);

        return this.client.get(query);
    }
}

构建WebSearchChain

java 复制代码
public class WebSearchChain {

    /**
     * apiKey
     */
    private String apiKey;

    /**
     * 构造函数
     * @param apiKey
     */
    public WebSearchChain(String apiKey){
        this.apiKey = apiKey;
    }

    /**
     * 初始化
     * @param apiKey
     * @return
     */
    public static WebSearchChain fromLlm(String apiKey){
        return new WebSearchChain(apiKey);
    }

    /**
     * 搜索
     * @param question
     * @return
     */
    public String search(String question){
        Map<String, String> parameter = new HashMap<>();
        parameter.put("api_key", apiKey);
        parameter.put("q", question);
        parameter.put("hl", "zh-cn");
        parameter.put("gl", "cn");
        parameter.put("google_domain", "google.com");
        parameter.put("safe", "active");
        parameter.put("start", "10");
        parameter.put("num", "10");
        parameter.put("device", "desktop");

        SerpApi serpapi = new SerpApi(parameter);
        JsonObject results = null;
        StringBuilder stringBuilder = new StringBuilder();
        try {
            results = serpapi.search(parameter);
            results.getAsJsonArray("organic_results").forEach(organicResult->{
                JsonObject result = organicResult.getAsJsonObject();
                String title = result.getAsJsonPrimitive("title").getAsString();
                String snippet = result.getAsJsonPrimitive("snippet").getAsString();
                stringBuilder.append(title).append("。").append(snippet).append("。");
            });
        } catch (SerpApiException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }
}

使用

博主之前借鉴langChain的思路封装一个Java版的框架,可参考:https://blog.csdn.net/weixin_44455388/article/details/137098743?spm=1001.2014.3001.5501

因此,直接调用即可:

java 复制代码
public static void test7() {
    String prompt = "吴亦凡犯了什么事";
    OpenAIChat openAIChat = OpenAIChat.builder().endpointUrl("http://192.168.0.84:9997/v1").model("Qwen1.5-14B-Chat").build().init();
    WebSearchChain webSearchChain = WebSearchChain.fromLlm("48d1bd8f7419xxxxxxxxxxxxxxxxxxxxxxxxxxxx");
    String searchResult = webSearchChain.search(prompt);
    Flux<String> stringFlux = openAIChat.streamChatWithChain("112233", "你是一个AI助手", searchResult, prompt);
    stringFlux.subscribe();
}
相关推荐
WaaTong17 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_7430484417 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries19 分钟前
Java字节码增强库ByteBuddy
java·后端
小灰灰__39 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭42 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果1 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql