背景
开源大模型通常不具备最新语料的问答能力。因此需要外部插件的拓展,目前主流的langChain框架已经集成了网络搜索的能力。但是作为一个倔强的Java程序员,还是想要用Java去实现。
注册SerpAPI
Serpapi 提供了多种搜索引擎的搜索API接口。
访问 Serpapi 官网上注册一个用户:
https://serpapi.com/
可以选择Free Plan,提供每月100次的免费使用。接下来就是使用自己的邮箱和手机号进行注册。
注册成功登录:
创建SerpApiHttp对象
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对象
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_KEYquery.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
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
因此,直接调用即可:
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();
}