背景
开源大模型通常不具备最新语料的问答能力。因此需要外部插件的拓展,目前主流的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_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
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();
}