随着AI基础能力的迭代,AI联网已是必不可少的功能了,最近也在给我的AI工具(ai.quanyouhulian.com/)新增联网功能!
我分析了下当前主流搜索AI都做了联网功能,但都有些局限性:
- 1、使用的搜索引擎太少了,大部分都是百度、CSDN、搜狗、知乎等,只能搜索到国内的文章,无法搜到海外一些写的好的文章或内容
- 2、搜索引擎搜出来的内容列表都**只有链接和摘要信息**,没有具体内容,直接丢给AI它给出的答案回答准确度不行
- 3、目前大部分模型支持的**tokens有长度限制**,tokens稍微长点的价格又太高,稍微丢多点文章内容就超出了上限,导致调用失败
为了解决这几个痛点问题,经过一段时间的修改和优化,终于解决了以上问题,接下来和大家分享下我都是如何实现的(本次重点讲解第二部分:提取url链接内容)!
一、使用聚合引擎搜索问题
目前各大平台的搜索引擎只要注册成为开发者就可以调用他们的API了,通过API完成调用搜索,不过这里首推SearXNG:一款开源的且聚合了上百款搜索引擎的聚合搜索服务!
1、SearXNG介绍
SearXNG 是一个免费的互联网元搜索引擎,可汇总多达 231 项搜索服务的结果。用户既不会被跟踪,也不会被描述。此外,SearXNG 还可通过 Tor 实现在线匿名。
2、SearXNG相关地址
文档地址: docs.searxng.org/
开源地址: https://github.com/searxng/searxng
从github可以看到目前已有17.8K,而且还是持续增加中
3、如何安装使用
文档中已经详细列出了安装命令,根据安装命令就可以快速完成安装!(这里就不做详细讲解,关注我,后续会出一期SearchXNG的详细安装步骤)
我的AI工具(ai.quanyouhulian.com/)也已接入该聚合搜索工具,基本互联网能找到的全部能找出来
二、使用jsoup提取链接内容并用AI做解析润色
从第一步搜索出来内容相关信息后,接下来就是读取内容了!不过从搜索引擎API接口返回内容可以看到:只有标题、链接、摘要信息,没有链接的具体详细内容 !如果 **只把摘要信息给AI做参考****,****回答的问题准确度会大打折扣**!
所以,我们第二步就需要对url做内容解析了,这一步也是非常核心的!这功能也可以单独拎出来当作网页地址的爬虫工具!跟上我的步伐,我将贴出具体实现源码,目前市面上的爬取工具都无法完整保留原本样式,该工具经过多次修改调试,已经能完美爬出链接内容并且转成markdown格式输出!这里我使用的是java作为开发工具!
1、pom.xml引入jsoup包
xml
<!-- 用来解析html文件 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>
2、解析链接内容并转成markdown格式输出
要实现该功能,这里我们要分三步走(全部源码会在末尾贴出):
- 2.1、解析url链接内容,获得html格式的内容
- 2.2、将html格式转成Markdown格式输出
- 2.3、上一步输出的Markdown会有很多广告、推荐等不相关的内容,我们需要使用AI去做优化,去除那些和文章内容不相关的
2.1、解析url链接
这一步需要改下header文件,模拟是浏览器访问,很多网站会对header为空的访问拒绝请求!
2.2、把html内容转markdown输出
这一步会比较复杂,使用Jsoup解析出Document和Element后,对元素做各种兼容适配!
2.3、使用AI修复markdown输出内容
这里写好角色提示词后,对markdown做优化,但是要保留原图和原本内容格式那些!这里我使用的是GLM模型,该模型使用硅基(cloud.siliconflow.cn/i/4Yw5GdmW)提供的API,它可以免费使用GLM模型,而且速度非常快!
3、效果检测
最原始的内容:mp.weixin.qq.com/s/OTJCsfZao...
解析出来的源markdown
经常AI优化后的
4、源代码
里面主要使用parseWebToTxt、parseWebToMarkdown这两个方法就够用了
xml
package com.hulian.ai.manager.chat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import com.hulian.ai.enums.ChatModelEnum;
import com.hulian.ai.model.dto.GptMessageDetail;
import cn.hutool.core.util.StrUtil;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.TcpClient;
@Slf4j
@Component
public class ParseWebManager {
// 内部调用的webclient池子,不用每次都创建连接
public static Map<String, WebClient> innerClientMap = new HashMap<String, WebClient>();
// 通过siliconflow平台账户11去调用
public final static String CLIENT_SILICON_11 = "silicon11";
// 通过siliconflow平台账户12去调用
public final static String CLIENT_SILICON_12 = "silicon12";
// 通过siliconflow平台账户13去调用
public final static String CLIENT_SILICON_13 = "silicon13";
@PostConstruct
public void initWebClient() {
TcpClient tcpClient = TcpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) // 连接超时:30 秒
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(15)) // 读取超时:15 秒
.addHandlerLast(new WriteTimeoutHandler(15))); // 写入超时:15 秒
WebClient webClient11 = WebClient.builder().baseUrl(ChatModelEnum.SILICON_FLOW_GPT_11.url)
.defaultHeader("Authorization", "Bearer " + ChatModelEnum.SILICON_FLOW_GPT_11.key)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))).build();
innerClientMap.put(CLIENT_SILICON_11, webClient11);
WebClient webClient12 = WebClient.builder().baseUrl(ChatModelEnum.SILICON_FLOW_GPT_12.url)
.defaultHeader("Authorization", "Bearer " + ChatModelEnum.SILICON_FLOW_GPT_12.key)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))).build();
innerClientMap.put(CLIENT_SILICON_12, webClient12);
WebClient webClient13 = WebClient.builder().baseUrl(ChatModelEnum.SILICON_FLOW_GPT_13.url)
.defaultHeader("Authorization", "Bearer " + ChatModelEnum.SILICON_FLOW_GPT_13.key)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))).build();
innerClientMap.put(CLIENT_SILICON_13, webClient13);
}
/**
* 解析网页
*
* @param url
* @return
*/
public String parseWebToTxt(String url) {
// 1. 获取网页内容
String html = getHtml(url);
// log.info("网页返回html: {}", html);
// 2. 解析网页内容
String content = parseHtml(html);
// 如果内容超过1000字,则截取前1000字
if (content != null && content.length() > 1000) {
content = content.substring(0, 1000);
}
return content;
}
public String parseWebToMarkdown(String url) {
try {
// 1. 获取网页内容
String html = getHtml(url);
if (html == null || html.isEmpty()) {
log.error("从URL获取内容失败: {}", url);
return "";
}
// 2. 将HTML转换为Markdown
String markdown = transHtmlToMarkdown(html);
// 写入到文件
// String markdownFilePath = "src/main/java/com/hulian/ai/manager/chat/source_test.md";
// java.nio.file.Files.write(
// java.nio.file.Paths.get(markdownFilePath),
// markdown.getBytes(java.nio.charset.StandardCharsets.UTF_8));
// log.info("转换后的Markdown: {}", markdown);
// 3、使用AI把Markdown里面那些广告等不相关内容做下优化
String optimizedMarkdown = optimizeMarkdown(markdown, CLIENT_SILICON_11);
optimizedMarkdown = optimizedMarkdown.replace("```", "");
System.out.println("优化后的Markdown: " + optimizedMarkdown);
return optimizedMarkdown;
} catch (Exception e) {
log.error("解析网页为Markdown时发生错误: {}", e.getMessage(), e);
return "";
}
}
private String getHtml(String url) {
try {
// 检查是否是微信文章
boolean isWechatArticle = url.contains("mp.weixin.qq.com");
// 创建请求配置
cn.hutool.http.HttpRequest request = cn.hutool.http.HttpRequest.get(url)
// 添加User-Agent模拟浏览器
.header("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36")
// 添加接受的内容类型
.header("Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
// 添加接受的语言
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
// 添加连接设置
.header("Connection", "keep-alive")
// 对于微信文章,添加更多必要的头信息
.header("Referer", isWechatArticle ? "https://mp.weixin.qq.com/" : url)
// 设置启用cookies
.header("Cookie", "")
// 启用重定向以确保获取最终内容
.setFollowRedirects(true)
// 设置超时时间
.timeout(30000);
// 执行请求并获取响应
String html = request.execute().body();
// 检查响应是否为空
if (html == null || html.trim().isEmpty()) {
log.error("从 {} 获取的HTML为空", url);
return "";
}
log.info("成功从 {} 获取HTML,长度: {}", url, html.length());
if (isWechatArticle) {
// 针对微信文章进行特殊处理,尝试修复图片URL
html = fixWechatImages(html);
}
return html;
} catch (Exception e) {
log.error("获取HTML时发生错误: {}", e.getMessage(), e);
// 使用JSoup直接获取作为备选方案
try {
log.info("尝试使用JSoup直接连接获取HTML");
Document doc = Jsoup.connect(url)
.userAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36")
.timeout(30000)
.get();
// 检查是否是微信文章
boolean isWechatArticle = url.contains("mp.weixin.qq.com");
if (isWechatArticle) {
// 针对微信文章进行特殊处理
return fixWechatImages(doc.html());
}
return doc.html();
} catch (Exception ex) {
log.error("使用JSoup获取HTML时也发生错误: {}", ex.getMessage(), ex);
return "";
}
}
}
/**
* 修复微信文章中的图片URL
*
* @param html 原始HTML
* @return 修复后的HTML
*/
private String fixWechatImages(String html) {
Document doc = Jsoup.parse(html);
log.info("开始处理微信文章图片");
// 处理微信特有的图片样式
Elements wxImages = doc.select(".rich_pages, .wxw-img, .rich_pages.wxw-img");
log.info("找到微信特殊图片: {} 张", wxImages.size());
for (Element img : wxImages) {
// 检查data-src属性
String dataSrc = img.attr("data-src");
if (!dataSrc.isEmpty()) {
log.info("修复微信图片data-src: {}", dataSrc);
img.attr("src", dataSrc);
}
}
// 处理所有section中的图片
Elements sectionImages = doc.select("section img");
log.info("找到section中的图片: {} 张", sectionImages.size());
for (Element img : sectionImages) {
// 检查data-src属性
String dataSrc = img.attr("data-src");
if (!dataSrc.isEmpty() && (img.attr("src").isEmpty() || img.attr("src").contains("data:"))) {
log.info("修复section中图片data-src: {}", dataSrc);
img.attr("src", dataSrc);
}
}
// 处理懒加载图片
Elements lazyImages = doc.select("img[data-src]");
log.info("找到懒加载图片: {} 张", lazyImages.size());
for (Element img : lazyImages) {
String dataSrc = img.attr("data-src");
if (!dataSrc.isEmpty()) {
log.info("修复懒加载图片data-src: {}", dataSrc);
img.attr("src", dataSrc);
}
}
// 处理其他常见的微信图片属性
Elements allImages = doc.select("img");
log.info("找到所有图片: {} 张", allImages.size());
int fixedCount = 0;
for (Element img : allImages) {
// 检查各种可能的属性
String[] possibleAttrs = { "data-src", "data-original", "data-backupSrc", "data-backsrc",
"data-imgfileid" };
for (String attr : possibleAttrs) {
String value = img.attr(attr);
if (!value.isEmpty() && (img.attr("src").isEmpty() || img.attr("src").contains("data:"))) {
log.info("通过属性{}修复图片: {}", attr, value);
img.attr("src", value);
fixedCount++;
break;
}
}
// 确保所有图片都有alt属性,即使为空
if (!img.hasAttr("alt")) {
img.attr("alt", "");
}
}
log.info("修复了 {} 张图片的URL", fixedCount);
// 特别检查目标图片是否存在并正确处理
Elements targetImage = doc.select("img[src*=fFKE45D7xmicHicSr92dA3YoaeO9IAyleH]");
if (!targetImage.isEmpty()) {
log.info("找到目标图片: {}", targetImage.attr("src"));
} else {
log.warn("未找到目标图片");
// 尝试在data-src中查找
Elements dataTargetImage = doc.select("img[data-src*=fFKE45D7xmicHicSr92dA3YoaeO9IAyleH]");
if (!dataTargetImage.isEmpty()) {
log.info("在data-src中找到目标图片: {}", dataTargetImage.attr("data-src"));
dataTargetImage.attr("src", dataTargetImage.attr("data-src"));
}
}
// 有些微信图片URL可能带有转义字符,修正它们
String html2 = doc.html().replace("&", "&");
log.info("已修复微信文章中的图片URL");
return html2;
}
private String parseHtml(String html) {
Document doc = Jsoup.parse(html);
// 移除不需要的元素
doc.select(
"script, style, iframe, nav, footer, header, .adsbygoogle, .advertisement, #sidebar, .sidebar, .nav, .menu, .comment")
.remove();
// 获取标题
String title = doc.title();
// 尝试获取主要内容
String mainContent;
// 尝试常见的内容容器
if (!doc.select("article").isEmpty()) {
mainContent = doc.select("article").text();
} else if (!doc.select(".content, .main, #content, #main, .post, .entry").isEmpty()) {
mainContent = doc.select(".content, .main, #content, #main, .post, .entry").text();
} else if (!doc.select("main").isEmpty()) {
mainContent = doc.select("main").text();
} else {
// 如果没有找到明确的内容容器,提取所有段落文本
mainContent = doc.select("p").text();
}
// 如果内容太短,可能没有正确提取到,尝试获取body所有文本
if (mainContent.length() < 100) {
mainContent = doc.body().text();
}
// 组合结果
StringBuilder result = new StringBuilder();
result.append("标题: ").append(title).append("\n\n");
result.append("内容: ").append(mainContent);
return result.toString();
}
/**
* 将HTML内容转换为Markdown格式
* 使用Jsoup库解析HTML并手动转换为Markdown格式
*
* @param htmlContent HTML内容
* @return Markdown格式的文本
*/
private String transHtmlToMarkdown(String htmlContent) {
try {
// 用于跟踪已处理过的图片URL,避免重复
java.util.Set<String> processedImageUrls = new java.util.HashSet<>();
// 清理HTML内容,但保留重要结构
Document doc = Jsoup.parse(htmlContent);
// 移除不需要的元素,但确保保留主要内容
doc.select("script, style, iframe, .adsbygoogle, .advertisement").remove();
StringBuilder markdown = new StringBuilder();
// 记录找到的图片数量
int totalImageCount = doc.select("img").size();
log.info("HTML中共找到 {} 张图片", totalImageCount);
// 首先标识微信文章的主体内容区域
Element contentArea = identifyMainContentArea(doc);
if (contentArea != null) {
log.info("已识别微信文章主体内容区域: {}", contentArea.tagName());
} else {
log.warn("未能识别微信文章主体内容区域,将处理整个文档");
contentArea = doc.body();
}
// 再次专门处理微信文章中的懒加载图片
Elements wxImages = doc.select(".rich_pages, .wxw-img, img.rich_pages, img.wxw-img, .rich_pages.wxw-img");
for (Element img : wxImages) {
// 尝试从各种属性获取图片URL
String[] possibleAttrs = { "data-src", "data-original", "data-backupSrc", "data-backsrc",
"data-imgfileid" };
for (String attr : possibleAttrs) {
String value = img.attr(attr);
if (!value.isEmpty() && (img.attr("src").isEmpty() || img.attr("src").contains("data:"))) {
img.attr("src", value);
log.info("在transHtmlToMarkdown中修复微信特殊图片{}: {}", attr, value);
break;
}
}
}
// 处理section中的图片
Elements sectionImages = doc.select("section img");
for (Element img : sectionImages) {
String dataSrc = img.attr("data-src");
if (!dataSrc.isEmpty() && (img.attr("src").isEmpty() || img.attr("src").contains("data:"))) {
img.attr("src", dataSrc);
log.info("在transHtmlToMarkdown中修复section图片: {}", dataSrc);
}
}
// 再次处理懒加载图片,确保所有图片都有src属性
Elements lazyImages = doc.select("img[data-src]");
for (Element img : lazyImages) {
String dataSrc = img.attr("data-src");
if (!dataSrc.isEmpty() && (img.attr("src").isEmpty() || img.attr("src")
.equals("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"))) {
img.attr("src", dataSrc);
log.info("从data-src修复图片URL: {}", dataSrc);
}
}
// 检查图片状态,确保所有图片都能被正确处理
Elements allImages = doc.select("img");
int validImageCount = 0;
for (Element img : allImages) {
String src = img.attr("src");
if (!src.isEmpty() && !src.startsWith("data:")) {
validImageCount++;
} else {
// 尝试从其他属性获取有效的图片URL
String[] possibleAttrs = { "data-src", "data-original", "data-backupSrc", "data-backsrc",
"data-imgfileid" };
for (String attr : possibleAttrs) {
String value = img.attr(attr);
if (!value.isEmpty()) {
img.attr("src", value);
validImageCount++;
log.info("在图片检查阶段从{}修复图片: {}", attr, value);
break;
}
}
if (img.attr("src").isEmpty() || img.attr("src").startsWith("data:")) {
log.warn("发现无效图片URL: {}", img.outerHtml());
}
}
}
log.info("有效图片数量: {}/{}", validImageCount, totalImageCount);
// 添加封面图片 (如果存在)
Element coverImg = identifyCoverImage(doc);
if (coverImg != null) {
String src = coverImg.attr("src");
if (src.isEmpty()) {
src = coverImg.attr("data-src");
}
if (!src.isEmpty() && !src.startsWith("data:")) {
markdown.append(".append(src).append(")\n\n");
processedImageUrls.add(src);
log.info("添加封面图片: {}", src);
}
}
// 添加标题
String title = doc.title();
if (title != null && !title.isEmpty()) {
markdown.append("# ").append(title).append("\n\n");
}
// 按顺序处理所有内容元素,首先处理主要内容区域
if (contentArea != null) {
processContentInOrder(contentArea.children(), markdown, processedImageUrls);
} else {
processContentInOrder(doc.body().children(), markdown, processedImageUrls);
}
// 额外处理微信文章中的独立段落和文本节点
Elements paragraphs = doc.select("p:not(:has(*))");
for (Element p : paragraphs) {
String text = p.text().trim();
if (!text.isEmpty() && text.length() > 5) { // 排除太短的文本
markdown.append(text).append("\n\n");
log.info("处理独立段落: {}", text.substring(0, Math.min(20, text.length())) + "...");
}
}
// 额外查找并处理可能被漏掉的微信特有元素
processWechatSpecificElements(doc, markdown, processedImageUrls);
// 清理多余的空行
String result = markdown.toString().replaceAll("(?m)^\\s*$[\n\r]{1,}", "\n");
// 记录最终生成的Markdown中图片数量
int mdImageCount = 0;
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("!\\[([^\\]]*)\\]\\(([^\\)]+)\\)");
java.util.regex.Matcher matcher = pattern.matcher(result);
while (matcher.find()) {
mdImageCount++;
// log.info("Markdown中找到图片: {}", matcher.group(2));
}
log.info("Markdown中包含 {} 张图片", mdImageCount);
return result.trim();
} catch (Exception e) {
log.error("HTML转Markdown失败: {}", e.getMessage(), e);
// 转换失败时,尝试提取纯文本
Document doc = Jsoup.parse(htmlContent);
return doc.text();
}
}
/**
* 识别微信文章的主要内容区域
*/
private Element identifyMainContentArea(Document doc) {
// 尝试常见的微信文章内容容器
Element contentArea = null;
// 尝试查找微信文章特有的内容区域标识
contentArea = doc.selectFirst("#js_content, .rich_media_content");
if (contentArea != null) {
return contentArea;
}
// 尝试查找article标签
contentArea = doc.selectFirst("article");
if (contentArea != null) {
return contentArea;
}
// 尝试通过类名查找可能的内容区域
contentArea = doc.selectFirst(".content, .main-content, .article-content");
if (contentArea != null) {
return contentArea;
}
// 如果找不到明确的内容区域,尝试查找最大的section或div元素
Elements sections = doc.select("section, div");
int maxTextLength = 0;
for (Element section : sections) {
String text = section.text().trim();
if (text.length() > maxTextLength) {
maxTextLength = text.length();
contentArea = section;
}
}
return contentArea;
}
/**
* 识别微信文章中的封面图片
*/
private Element identifyCoverImage(Document doc) {
// 尝试找到可能的封面图片
// 1. 检查第一张图片
Element firstImg = doc.selectFirst("img");
if (firstImg != null) {
return firstImg;
}
// 2. 检查带有特定类名的图片
Element coverImg = doc.selectFirst("img.cover, img.banner, img.rich_pages:first-child");
if (coverImg != null) {
return coverImg;
}
// 3. 检查meta标签中的图片
Elements metaImages = doc.select("meta[property='og:image'], meta[name='twitter:image']");
if (!metaImages.isEmpty()) {
String imgSrc = metaImages.first().attr("content");
if (!imgSrc.isEmpty()) {
Element img = doc.createElement("img");
img.attr("src", imgSrc);
return img;
}
}
return null;
}
/**
* 处理微信特有的元素
*/
private void processWechatSpecificElements(Document doc, StringBuilder markdown,
java.util.Set<String> processedImageUrls) {
// 处理微信文章特有的section元素
Elements sections = doc.select("section");
for (Element section : sections) {
// 检查是否包含有意义的内容
if (section.text().trim().length() > 5) {
String sectionText = section.text().trim();
// 避免重复添加已处理过的内容
if (!markdown.toString().contains(sectionText)) {
markdown.append(sectionText).append("\n\n");
log.info("处理微信section元素: {}", sectionText.substring(0, Math.min(20, sectionText.length())) + "...");
}
}
// 处理section中的图片
Elements sectionImages = section.select("img");
for (Element img : sectionImages) {
processImageElement(img, markdown, processedImageUrls);
}
}
// 处理微信文章中的特殊格式化文本
Elements formattedTexts = doc.select("strong, b, em, i");
for (Element text : formattedTexts) {
if (text.parent().tagName().equals("p") || text.parent().tagName().equals("section")) {
continue; // 这些已经在段落处理中处理过了
}
String textContent = text.text().trim();
if (!textContent.isEmpty() && textContent.length() > 5 && !markdown.toString().contains(textContent)) {
if (text.tagName().equals("strong") || text.tagName().equals("b")) {
markdown.append("**").append(textContent).append("**\n\n");
} else if (text.tagName().equals("em") || text.tagName().equals("i")) {
markdown.append("*").append(textContent).append("*\n\n");
}
log.info("处理特殊格式化文本: {}", textContent.substring(0, Math.min(20, textContent.length())) + "...");
}
}
}
/**
* 按顺序处理HTML内容,保持原文章结构
*
* @param elements 要处理的元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processContentInOrder(Elements elements, StringBuilder markdown,
java.util.Set<String> processedImageUrls) {
for (Element element : elements) {
// 根据元素类型进行处理
String tagName = element.tagName().toLowerCase();
if (tagName.equals("section")) {
// 处理section中的内容,保持顺序
processContentInOrder(element.children(), markdown, processedImageUrls);
// 处理section中可能的直接图片
Elements sectionImages = element.select("> img");
for (Element img : sectionImages) {
processImageElement(img, markdown, processedImageUrls);
}
} else if (tagName.equals("h1") || tagName.equals("h2") || tagName.equals("h3") ||
tagName.equals("h4") || tagName.equals("h5") || tagName.equals("h6")) {
// 处理标题
int headingLevel = Integer.parseInt(tagName.substring(1));
processHeadingElement(element, headingLevel, markdown, processedImageUrls);
} else if (tagName.equals("p")) {
// 处理段落
processParagraphElement(element, markdown, processedImageUrls);
} else if (tagName.equals("ul") || tagName.equals("ol")) {
// 处理列表
processListElement(element, markdown, processedImageUrls);
} else if (tagName.equals("table")) {
// 处理表格
processTableElement(element, markdown, processedImageUrls);
} else if (tagName.equals("blockquote")) {
// 处理引用块
processBlockquoteElement(element, markdown, processedImageUrls);
} else if (tagName.equals("hr")) {
// 处理分割线
markdown.append("\n---\n\n");
} else if (tagName.equals("pre")) {
// 处理代码块
processCodeBlockElement(element, markdown);
} else if (tagName.equals("img")) {
// 处理图片
processImageElement(element, markdown, processedImageUrls);
} else if (tagName.equals("figure")) {
// 处理figure元素(通常包含图片)
Elements figureImages = element.select("img");
for (Element img : figureImages) {
processImageElement(img, markdown, processedImageUrls);
}
// 处理figure的其他内容
Elements figureChildren = element.children();
for (Element child : figureChildren) {
if (!child.tagName().equals("img")) {
processContentInOrder(new Elements(child), markdown, processedImageUrls);
}
}
} else if (tagName.equals("div")) {
// 处理div内容,递归处理其子元素
processContentInOrder(element.children(), markdown, processedImageUrls);
// 处理div中可能的直接图片
Elements divImages = element.select("> img");
for (Element img : divImages) {
processImageElement(img, markdown, processedImageUrls);
}
} else if (tagName.equals("a") && element.parent() == element.ownerDocument().body()) {
// 处理独立链接
String href = element.attr("href");
String text = element.text();
if (!href.isEmpty() && !text.isEmpty()) {
markdown.append("\n[").append(text).append("](").append(href).append(")\n\n");
}
} else {
// 递归处理其他元素的子元素
if (element.children().size() > 0) {
processContentInOrder(element.children(), markdown, processedImageUrls);
}
}
}
}
/**
* 处理图片元素
*
* @param img 图片元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processImageElement(Element img, StringBuilder markdown, java.util.Set<String> processedImageUrls) {
String src = img.attr("src");
String alt = img.attr("alt");
String dataSrc = img.attr("data-src");
// 优先使用src,如果src为空或是数据URI,则尝试使用data-src
if (src.isEmpty() || src.startsWith("data:")) {
if (!dataSrc.isEmpty()) {
src = dataSrc;
} else {
// 尝试从其他属性获取图片URL
String[] possibleAttrs = { "data-original", "data-backupSrc", "data-backsrc", "data-imgfileid" };
for (String attr : possibleAttrs) {
String value = img.attr(attr);
if (!value.isEmpty()) {
src = value;
break;
}
}
}
}
if (!src.isEmpty() && !src.startsWith("data:")) {
// 检查是否已处理过该图片
if (!processedImageUrls.contains(src)) {
markdown.append("\n.append(src).append(")\n\n");
processedImageUrls.add(src); // 记录已处理
} else {
}
}
}
/**
* 处理标题元素
*
* @param heading 标题元素
* @param level 标题级别
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processHeadingElement(Element heading, int level, StringBuilder markdown,
java.util.Set<String> processedImageUrls) {
markdown.append("\n");
for (int i = 0; i < level; i++) {
markdown.append("#");
}
// 处理标题中的链接
String headingText = heading.html();
Elements links = heading.select("a");
for (Element link : links) {
String href = link.attr("href");
String linkText = link.text();
if (!href.isEmpty() && !linkText.isEmpty()) {
String linkHtml = link.outerHtml();
String markdownLink = "[" + linkText + "](" + href + ")";
headingText = headingText.replace(linkHtml, markdownLink);
}
}
// 处理标题中的图片
Elements images = heading.select("img");
for (Element img : images) {
String src = img.attr("src");
String alt = img.attr("alt");
if (!src.isEmpty() && !processedImageUrls.contains(src)) {
String imgHtml = img.outerHtml();
String markdownImg = "";
headingText = headingText.replace(imgHtml, markdownImg);
processedImageUrls.add(src); // 记录已处理
}
}
// 移除其他HTML标签,但保留我们已经转换的Markdown语法
headingText = cleanHtmlKeepMarkdown(headingText);
markdown.append(" ").append(headingText).append("\n\n");
}
/**
* 处理段落元素
*
* @param paragraph 段落元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processParagraphElement(Element paragraph, StringBuilder markdown,
java.util.Set<String> processedImageUrls) {
// 跳过空段落
if (paragraph.text().trim().isEmpty() && paragraph.select("img").isEmpty()) {
return;
}
// 处理段落内的粗体和斜体
String text = paragraph.html()
.replaceAll("<strong>|<b>", "**")
.replaceAll("</strong>|</b>", "**")
.replaceAll("<em>|<i>", "*")
.replaceAll("</em>|</i>", "*")
.replaceAll("<br>|<br/>", "\n");
// 提前处理段落中的链接
Elements links = paragraph.select("a");
for (Element link : links) {
String href = link.attr("href");
String linkText = link.text();
if (!href.isEmpty() && !linkText.isEmpty()) {
String linkHtml = link.outerHtml();
String markdownLink = "[" + linkText + "](" + href + ")";
text = text.replace(linkHtml, markdownLink);
}
}
// 处理段落中的图片
Elements images = paragraph.select("img");
for (Element img : images) {
String src = img.attr("src");
String alt = img.attr("alt");
String dataSrc = img.attr("data-src");
// 优先使用src,如果src为空或是数据URI,则尝试使用data-src
if (src.isEmpty() || src.startsWith("data:")) {
if (!dataSrc.isEmpty()) {
src = dataSrc;
}
}
if (!src.isEmpty() && !src.startsWith("data:") && !processedImageUrls.contains(src)) {
String imgHtml = img.outerHtml();
String markdownImg = "";
text = text.replace(imgHtml, markdownImg);
processedImageUrls.add(src); // 记录已处理
}
}
// 移除其他HTML标签,但保留我们已经转换的Markdown语法
text = cleanHtmlKeepMarkdown(text);
markdown.append(text).append("\n\n");
}
/**
* 处理列表元素
*
* @param list 列表元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processListElement(Element list, StringBuilder markdown, java.util.Set<String> processedImageUrls) {
boolean isOrdered = list.tagName().equalsIgnoreCase("ol");
markdown.append("\n");
Elements items = list.select("li");
for (int i = 0; i < items.size(); i++) {
Element item = items.get(i);
// 处理列表项中的链接和图片
String itemText = processContentForInline(item, processedImageUrls);
if (isOrdered) {
markdown.append(i + 1).append(". ").append(itemText).append("\n");
} else {
markdown.append("* ").append(itemText).append("\n");
}
}
markdown.append("\n");
}
/**
* 处理表格元素
*
* @param table 表格元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processTableElement(Element table, StringBuilder markdown, java.util.Set<String> processedImageUrls) {
markdown.append("\n");
// 处理表头
Elements headerRows = table.select("thead tr");
if (!headerRows.isEmpty()) {
Elements headerCells = headerRows.first().select("th");
if (headerCells.isEmpty()) {
headerCells = headerRows.first().select("td");
}
// 表头行
for (Element cell : headerCells) {
String cellText = processContentForInline(cell, processedImageUrls);
markdown.append("| ").append(cellText).append(" ");
}
markdown.append("|\n");
// 分隔行
for (int i = 0; i < headerCells.size(); i++) {
markdown.append("| --- ");
}
markdown.append("|\n");
}
// 处理表体
Elements bodyRows = table.select("tbody tr, tr:not(thead tr)");
for (Element row : bodyRows) {
Elements cells = row.select("td");
for (Element cell : cells) {
String cellText = processContentForInline(cell, processedImageUrls);
markdown.append("| ").append(cellText).append(" ");
}
markdown.append("|\n");
}
markdown.append("\n");
}
/**
* 处理引用块元素
*
* @param blockquote 引用块元素
* @param markdown 输出的Markdown字符串
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
*/
private void processBlockquoteElement(Element blockquote, StringBuilder markdown,
java.util.Set<String> processedImageUrls) {
markdown.append("\n");
// 处理引用块中的链接和图片
String blockText = processContentForInline(blockquote, processedImageUrls);
String[] lines = blockText.split("\n");
for (String line : lines) {
markdown.append("> ").append(line).append("\n");
}
markdown.append("\n");
}
/**
* 处理代码块元素
*
* @param pre 代码块元素
* @param markdown 输出的Markdown字符串
*/
private void processCodeBlockElement(Element pre, StringBuilder markdown) {
Element code = pre.selectFirst("code");
String codeContent = code != null ? code.text() : pre.text();
markdown.append("\n```\n");
markdown.append(codeContent);
markdown.append("\n```\n\n");
}
/**
* 处理内联内容(链接、图片等)
*
* @param element 包含内联内容的元素
* @param processedImageUrls 已处理过的图片URL集合,用于避免重复
* @return 处理后的内联内容
*/
private String processContentForInline(Element element, java.util.Set<String> processedImageUrls) {
// 处理内联的粗体和斜体
String text = element.html()
.replaceAll("<strong>|<b>", "**")
.replaceAll("</strong>|</b>", "**")
.replaceAll("<em>|<i>", "*")
.replaceAll("</em>|</i>", "*")
.replaceAll("<br>|<br/>", "\n");
// 处理内联的链接
Elements links = element.select("a");
for (Element link : links) {
String href = link.attr("href");
String linkText = link.text();
if (!href.isEmpty() && !linkText.isEmpty()) {
String linkHtml = link.outerHtml();
String markdownLink = "[" + linkText + "](" + href + ")";
text = text.replace(linkHtml, markdownLink);
}
}
// 处理内联的图片
Elements images = element.select("img");
for (Element img : images) {
String src = img.attr("src");
String alt = img.attr("alt");
String dataSrc = img.attr("data-src");
// 优先使用src,如果src为空或是数据URI,则尝试使用data-src
if (src.isEmpty() || src.startsWith("data:")) {
if (!dataSrc.isEmpty()) {
src = dataSrc;
}
}
if (!src.isEmpty() && !src.startsWith("data:") && !processedImageUrls.contains(src)) {
String imgHtml = img.outerHtml();
String markdownImg = "";
text = text.replace(imgHtml, markdownImg);
processedImageUrls.add(src); // 记录已处理
}
}
// 移除其他HTML标签,但保留我们已经转换的Markdown语法
return cleanHtmlKeepMarkdown(text);
}
/**
* 移除HTML标签但保留Markdown语法
*
* @param html 包含HTML标签和Markdown语法的文本
* @return 移除HTML标签但保留Markdown语法的文本
*/
private String cleanHtmlKeepMarkdown(String html) {
// 先保存已转换的Markdown链接
java.util.List<String> markdownLinks = new java.util.ArrayList<>();
java.util.regex.Pattern linkPattern = java.util.regex.Pattern.compile("\\[([^\\]]+)\\]\\(([^\\)]+)\\)");
java.util.regex.Matcher linkMatcher = linkPattern.matcher(html);
while (linkMatcher.find()) {
markdownLinks.add(linkMatcher.group(0));
}
// 保存已转换的Markdown图片
java.util.List<String> markdownImages = new java.util.ArrayList<>();
java.util.regex.Pattern imgPattern = java.util.regex.Pattern.compile("!\\[([^\\]]*)]\\(([^\\)]+)\\)");
java.util.regex.Matcher imgMatcher = imgPattern.matcher(html);
while (imgMatcher.find()) {
markdownImages.add(imgMatcher.group(0));
// 记录找到的Markdown图片,用于调试
// log.debug("找到Markdown图片: {}", imgMatcher.group(0));
}
// 先保存已转换的粗体、斜体和代码块等Markdown语法
java.util.List<String> markdownFormats = new java.util.ArrayList<>();
java.util.regex.Pattern formatPattern = java.util.regex.Pattern
.compile("(\\*\\*[^\\*]+\\*\\*)|(\\*[^\\*]+\\*)|(`[^`]+`)");
java.util.regex.Matcher formatMatcher = formatPattern.matcher(html);
while (formatMatcher.find()) {
markdownFormats.add(formatMatcher.group(0));
}
// 使用Jsoup移除HTML标签
String plainText = Jsoup.parse(html).text();
// 恢复Markdown链接
for (String link : markdownLinks) {
// 提取链接文本
java.util.regex.Matcher m = linkPattern.matcher(link);
if (m.find()) {
String linkText = m.group(1);
// 在plainText中查找链接文本并替换为完整的Markdown链接
if (plainText.contains(linkText)) {
plainText = plainText.replace(linkText, link);
} else {
// 如果找不到链接文本,尝试添加到文本末尾
// log.debug("未能在文本中找到链接文本: {}, 添加到文本末尾", linkText);
plainText = plainText + " " + link;
}
}
}
// 恢复Markdown图片
for (String img : markdownImages) {
// 提取图片文本
java.util.regex.Matcher m = imgPattern.matcher(img);
if (m.find()) {
String altText = m.group(1);
String imgSrc = m.group(2);
// 如果替代文本为空或无法在纯文本中找到,直接追加到文本中
if (altText.isEmpty() || !plainText.contains(altText)) {
// log.debug("未能在文本中找到图片替代文本或替代文本为空: {}, 添加到文本中", altText);
// 确保图片在单独的行
if (!plainText.endsWith("\n")) {
plainText = plainText + "\n\n";
}
plainText = plainText + img + "\n\n";
} else {
// 在plainText中查找替代文本并替换为完整的Markdown图片
plainText = plainText.replace(altText, img);
}
}
}
// 恢复其他Markdown格式
for (String format : markdownFormats) {
// 提取格式中的文本
String formatText = format.replaceAll("(\\*\\*|\\*|`)", "");
// 在plainText中查找该文本并替换为完整的Markdown格式
if (plainText.contains(formatText)) {
plainText = plainText.replace(formatText, format);
}
}
return plainText;
}
// 使用AI把Markdown里面那些广告等不相关内容做下优化
public String optimizeMarkdown(String markdown, String client) {
try {
// 使用glm4模型
WebClient webClient = innerClientMap.get(client);
if (webClient == null) {
TcpClient tcpClient = TcpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1200000) // 连接超时:120
// 秒
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(60)) // 读取超时:60 秒
.addHandlerLast(new WriteTimeoutHandler(60))); // 写入超时:60 秒
webClient = WebClient.builder().baseUrl(ChatModelEnum.SILICON_FLOW_GPT_13.url)
.defaultHeader("Authorization", "Bearer " + ChatModelEnum.SILICON_FLOW_GPT_13.key)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))).build();
}
Map<String, Object> userInput = new HashMap<String, Object>();
// 准备请求数据
userInput.put("model", "THUDM/glm-4-9b-chat");
// 组装提示词
List<GptMessageDetail> messagelist = new ArrayList<GptMessageDetail>();
GptMessageDetail detail1 = new GptMessageDetail();
detail1.setRole("system");
detail1.setContent("#### Role\n"
+ "- **Markdown内容优化大师**:一位专注于Markdown文档优化的专家,擅长去除不相关的广告和杂乱信息,确保文档的清晰度和可读性。\n\n"
+ "#### Background\n"
+ "- 用户需要对Markdown内容进行优化,以提高文档的专业性和可读性。用户可能在处理文档时发现其中夹杂了广告和其他不相关的信息,这些信息影响了主要内容的传达。\n\n"
+ "#### Attention\n"
+ "- 您对优化文档的渴望是显而易见的,这将帮助您提高工作效率和文档质量。我们将共同努力,确保您的Markdown文档清晰、简洁,并保留所有必要的格式和链接。\n\n"
+ "#### Profile\n"
+ "- Markdown内容优化大师是一位精通Markdown语法和文档结构的专家,能够快速识别并去除冗余信息,同时保留文档的核心内容和格式。\n\n"
+ "#### Skills\n"
+ "- **Markdown语法精通**:熟悉Markdown的各种格式和用法。\n"
+ "- **信息筛选**:能够识别并去除广告和不相关内容。\n"
+ "- **格式保持**:确保文档的原有格式和链接/图片得以保留。\n"
+ "- **细节关注**:在优化过程中关注细节,确保内容不丢失。\n\n"
+ "#### Goals\n"
+ "1. 去除Markdown文档中的所有广告以及和内容不相关的信息。\n"
+ "2. 保留文档的原有样式,包括标题、列表、代码块等。\n"
+ "3. 确保核心内容里的图片和链接保持完整且功能正常。\n"
+ "4. 提高文档的可读性和专业性。\n"
+ "5. 输出优化后的Markdown文档。\n\n"
+ "#### Constrains\n"
+ "- 不得改变文档的核心内容和结构。\n"
+ "- 保持核心内容图片和链接的有效性。\n"
+ "- 确保文档格式符合Markdown标准。\n\n"
+ "#### OutputFormat\n"
+ "- 使用Markdown格式输出优化后的内容,直接输出Markdown内容,不要输出其余不相关的对话内容。\n"
+ "- 确保每个部分都清晰可读且格式正确。\n"
+ "- 保留和文档内容相关的全部图片和链接。");
messagelist.add(detail1);
GptMessageDetail detail2 = new GptMessageDetail();
detail2.setRole("user");
detail2.setContent("请优化以下Markdown内容:\n\n" + markdown);
messagelist.add(detail2);
userInput.put("messages", messagelist);
// 不使用流式输出
userInput.put("stream", false);
String requestStr = buildJsonPayload(userInput);
// log.info("====================处理请求,requestStr:{}====================",
// requestStr);
Mono<String> requestBody = Mono.just(requestStr);
// 发送请求并返回结果
String resultStr = webClient.post().contentType(MediaType.APPLICATION_JSON).body(requestBody, String.class)
.retrieve().bodyToMono(String.class).block(); // 阻塞等待结果
if (StrUtil.isNotBlank(resultStr)) {
JSONObject resultObj = JSONObject.parseObject(resultStr);
return resultObj.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content");
} else {
return "获取响应失败,请稍后再试";
}
} catch (Exception e) {
log.error("解析网页为Markdown时发生错误: {}", e.getMessage(), e);
return markdown;
}
}
private String buildJsonPayload(Map<String, Object> userInput) {
Gson gson = new Gson();
return gson.toJson(userInput);
}
public static void main(String[] args) {
String url = "https://cn.chinadaily.com.cn/a/202502/28/WS67c17447a310510f19ee927d.html";
try {
// String url = "https://deepseek.csdn.net/67ab1e8279aaf67875cb9b88.html";
// 创建实例而不是使用Spring依赖注入
ParseWebManager manager = new ParseWebManager();
System.out.println("开始解析网页: " + url);
String html = manager.getHtml(url);
// html写入到文件
String htmlFilePath = "src/main/java/com/hulian/ai/manager/chat/test.html";
// 清理现有文件
java.io.File htmlFile = new java.io.File(htmlFilePath);
if (htmlFile.exists()) {
htmlFile.delete();
}
java.nio.file.Files.write(
java.nio.file.Paths.get(htmlFilePath),
html.getBytes(java.nio.charset.StandardCharsets.UTF_8));
String markdown = manager.parseWebToMarkdown(url);
// 输出到文件
String filePath = "src/main/java/com/hulian/ai/manager/chat/test.md";
// 清理现有文件
java.io.File markdownFile = new java.io.File(filePath);
if (markdownFile.exists()) {
markdownFile.delete();
}
java.nio.file.Files.write(
java.nio.file.Paths.get(filePath),
markdown.getBytes(java.nio.charset.StandardCharsets.UTF_8));
System.out.println("Markdown已成功保存到: " + filePath);
// 输出前100个字符预览
String preview = markdown.length() > 100 ? markdown.substring(0, 100) + "..." : markdown;
System.out.println("Markdown预览: " + preview);
} catch (Exception e) {
e.printStackTrace();
System.err.println("处理网页过程中发生错误: " + e.getMessage());
}
}
/**
* 公共方法用于测试,允许直接获取HTML内容
*
* @param url 要获取HTML的URL
* @return HTML内容
*/
public String testGetHtml(String url) {
return getHtml(url);
}
}
三、使用混合模型架构及分层处理策略进行长文本处理
接下来,把上一步得到的markdown内容和搜索结果可以丢给AI了!这里直接丢进去也会出问题:超过AI模型的tokens上下文限制,这里我们可以采用结构化解决方案来处理(以下给出思路,具体实现可以先关注我,下期带大家实战**如何使用处理超长文本系统提示词**)
一、预处理阶段(核心思路:压缩信息量)
关键信息提取
diff
- 使用NLP技术提取每篇文章的:
- 核心论点(e.g. BERT摘要 + TextRank)
- 支撑数据(正则匹配数字/百分比)
- 独特观点(通过语义对比识别)
- 引用来源(引文模式识别)
动态分块检索
markdown
- 建立向量数据库流程:
1. 将文本分块(512token/块)
2. 使用sentence-transformers生成嵌入
3. 存入FAISS/ChromaDB
- 实时检索时:
1. 分析当前写作段落语义
2. Top-k相似块召回(k=3-5)
二、运行时处理(动态内容管理)
上下文窗口优化
- 采用滑动窗口策略:
- 保留最近3轮关键输出
- 维护核心论点大纲
- 使用位置编码衰减(越早内容权重越低)
分层注入策略
python
def context_builder():
context = []
# 必选内容
context.append(system_prompt)
context.append(current_outline)
# 动态内容
if research_phase:
context.extend(top3_sections)
elif drafting_phase:
context.extend(relevant_stats)
elif refining_phase:
context.append(style_guide)
return truncate(context, max_tokens=12k)
三、工程化解决方案
混合模型架构
- 长文本处理层:专用LoRA模型(处理16k+上下文)
- 写作生成层:主LLM(GPT-4等)
- 中间层:知识蒸馏(提取关键insights)
记忆管理系统
diff
- 使用Redis缓存:
- 最近访问的文本块
- 高频引用数据
- 用户偏好设置
- 实现LRU缓存淘汰策略
实施建议:
- 优先建立向量检索系统(可用Pinecone快速原型)
- 配合动态prompt engineering:
python
prompt = f"""基于以下核心信息(来自{len(refs)}篇文献):
{vector_search(query, k=3)}
当前写作进度:
{last_3_paragraphs}
请按{style}风格继续撰写下一段落,特别注意:
- 引用数据要标注来源编号[1-{len(refs)}]
- 保持段落逻辑衔接:{current_outline_step}
"""
- 监控Token使用:设置fallback机制,当接近上限时自动切换至摘要模式
评估指标:
- 上下文利用率(Used Tokens / Max Tokens)
- 文献召回准确率
- 生成文本的文献引用密度
- 用户修改率(生成文本直接可用比例)
这种分层处理方案在实测中可将16篇平均5000字的参考文献有效压缩到8k tokens内,同时保持核心信息的完整引用。可以搭配LangChain等框架实现模块化处理流程。