使用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();
}
相关推荐
java金融1 分钟前
Java 锁升级机制详解
java
Young55664 分钟前
还不了解工作流吗(基础篇)?
java·workflow·工作流引擎
让我上个超影吧6 分钟前
黑马点评【缓存】
java·redis·缓存
ajassi200014 分钟前
开源 java android app 开发(十一)调试、发布
android·java·linux·开源
YuTaoShao28 分钟前
Java八股文——MySQL「存储引擎篇」
java·开发语言·mysql
crud34 分钟前
Java 中的 synchronized 与 Lock:深度对比、使用场景及高级用法
java
王德博客39 分钟前
【Java课堂笔记】Java 入门基础语法与面向对象三大特性详解
java·开发语言
seventeennnnn1 小时前
Java大厂面试真题:谢飞机的技术挑战
java·spring boot·面试·aigc·技术挑战·电商场景·内容社区
wkj0011 小时前
接口实现类向上转型和向上转型解析
java·开发语言·c#
qqxhb1 小时前
零基础设计模式——行为型模式 - 观察者模式
java·观察者模式·设计模式·go