这才是AI最强联网解决方案!手把手教你从聚合搜索到网页读取和超长上下文处理!

随着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("&amp;", "&");

        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("![cover_image](").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(alt).append("](").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 = "![" + alt + "](" + src + ")";
                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 = "![" + alt + "](" + src + ")";
                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 = "![" + alt + "](" + src + ")";
                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缓存淘汰策略
实施建议:
  1. 优先建立向量检索系统(可用Pinecone快速原型)
  2. 配合动态prompt engineering:
python 复制代码
prompt = f"""基于以下核心信息(来自{len(refs)}篇文献):
{vector_search(query, k=3)}

当前写作进度:
{last_3_paragraphs}

请按{style}风格继续撰写下一段落,特别注意:
- 引用数据要标注来源编号[1-{len(refs)}]
- 保持段落逻辑衔接:{current_outline_step}
"""
  1. 监控Token使用:设置fallback机制,当接近上限时自动切换至摘要模式
评估指标:
  • 上下文利用率(Used Tokens / Max Tokens)
  • 文献召回准确率
  • 生成文本的文献引用密度
  • 用户修改率(生成文本直接可用比例)

这种分层处理方案在实测中可将16篇平均5000字的参考文献有效压缩到8k tokens内,同时保持核心信息的完整引用。可以搭配LangChain等框架实现模块化处理流程。

总结

相关推荐
Asthenia041215 分钟前
Redis性能与优势/对比其他Key-Value存储/数据类型及底层结构/相同数据结构原因/对比Memcached优势/字符串最大容量/RDB与AOF分析
后端
计算机-秋大田1 小时前
基于Spring Boot的个性化商铺系统的设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
熬了夜的程序员1 小时前
Go 语言封装邮件发送功能
开发语言·后端·golang·log4j
uhakadotcom1 小时前
PostgreSQL 行级安全性(RLS)简介
后端·面试·github
小马爱打代码1 小时前
Spring Boot - 动态编译 Java 类并实现热加载
spring boot·后端
网络风云1 小时前
Flask(二)项目结构与环境配置
后端·python·flask
小杨4042 小时前
架构系列二十三(全面理解IO)
java·后端·架构
uhakadotcom2 小时前
Tableau入门:数据可视化的强大工具
后端·面试·github
demonlg01123 小时前
Go 语言 fmt 模块的完整方法详解及示例
开发语言·后端·golang
程序员鱼皮3 小时前
2025 年最全Java面试题 ,热门高频200 题+答案汇总!
java·后端·面试