为了账号安全,请及时绑定邮箱和手机立即绑定

IEC V1.5:个人教育聊天机器人

个人用来互换PDF文档和网站的工具

本文介绍了如何设置可以在本地运行的IEC V1.5应用程序。该应用的界面不是通过Gradio或Streamlit等框架创建的,而是通过HTML、CSS和JavaScript实现的。这样做的好处是可以更快地自定义应用界面的设计。用户可以在IEC中将PDF文件和/或URL作为来源。从这些来源提取的文本可以显示或隐藏。问题仅基于所使用的来源进行回答,这意味着聊天机器人不会受限于其有限的知识库,在不清楚的情况下也不会胡言乱语。也不会对当前话题完全不作回应。答案的详细程度也可以单独设置。基本结构如下图所示。

结构和运行模式 IEC V1.5(图源:作者)

此功能在初步的 IEC 试验 [1] 中得到了非常积极的评价。应用程序与大型语言模型(LLM)之间的通信是通过命令行界面(CLI)和 Ollama 实现的。当前使用的是来自 Meta 的 Llama3.1(8b)模型。然而,也可以通过模型管理系统使用其他模型,或者根据可用的计算能力使用拥有更多参数的模型。由于 Ollama 和 LLM 或 SLM 的安装已在其他文章 [2] 中描述过,这里不再赘述。简单介绍一下代码的功能。应用程序可以从下面链接的 GitHub 仓库安装,目前的界面如下。

IEC V1.5应用的工作原理(作者制作的GIF图)

需求

Flask 和其他库(比如用于从网页获取内容的 “requests”,用于从 PDF 文件中提取文本的 “fitz” 等)被导入以启动 web 应用。在对 LLM 进行文本分词时,使用了 OpenAI(GPT-2)的分词器 “tiktoken”

    从 flask 导入 Flask, request, jsonify, Response  
    导入 os  
    导入 subprocess  
    导入 re  
    导入 requests  
    从 readability 导入 Document  
    导入 fitz  
    从 bs4 导入 BeautifulSoup  
    导入 tiktoken  

    app = Flask(__name__)  
    os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
分词处理及辅助工具

这是通过分词和文本分割来完成的。Llama3.1模型的最大输入长度为131072个token(llama.context_length: 131072 [3])。根据使用的不同模型,这个数值可能需要调整。提取出的文本转换为token "tokenize-text(text)",然后再转换回文本 "detokenize_text(tokens)"。如果文本超出了token的限制,就会被分成多个块以供LLM处理。

    TOKEN_LIMIT = 131072  
    enc = tiktoken.get_encoding("gpt2")  

    # 将文本编码为token序列
    def tokenize_text(text):  
        return enc.encode(text)  

    # 将token序列解码为文本
    def detokenize_text(tokens):  
        return enc.decode(tokens)  

    # 分割文本为块
    def 分割文本为块(text, max_tokens):  
        tokens = tokenize_text(text)  
        # 最大token数
        for i in range(0, len(tokens), max_tokens):  
            yield detokenize_text(tokens[i : i + max_tokens])
提取内容的函数功能

下一步是内容提取。网站的内容通过“requests.get(url)”来获取,使用“BeautifulSoup”库移除HTML标签,以便只传输纯文本。上传的PDF文件以流的方式读取。文件通过“_fitz.open(stream=pdfdata, filetype="pdf"”从内存中直接打开,并通过“.get_text()”一页接一页地提取。使用“_re.sub()”来移除多余的空格。上传的媒体(URL或PDF文件)和从中提取的内容被合并成一个字符串“_all_content.strip()” 。

    def get_readable_content(url):  
        try:  
            response = requests.get(url, timeout=30)  
            response.raise_for_status()  
            doc = Document(response.text)  
            readable_html = doc.summary()  
            soup = BeautifulSoup(readable_html, 'html.parser')  
            readable_text = soup.get_text()  
            return re.sub(r'\s+', ' ', readable_text).strip()  
        except requests.exceptions.RequestException as e:  
            return f"获取内容时发生错误: {str(e)}"  

    def extract_text_from_pdf(file):  
        text = ""  
        pdf_data = file.read()  
        try:  
            with fitz.open(stream=pdf_data, filetype="pdf") as pdf:  
                for page in pdf:  
                    text += page.get_text()  
        except Exception as e:  
            return f"读取PDF时发生错误: {str(e)}"  
        cleaned_text = re.sub(r'\s*\n\s*', ' ', text)  
        cleaned_text = re.sub(r'\s{2,}', ' ', cleaned_text)  
        return cleaned_text.strip()  

    def extract_content(url_input, pdf_files):  
        all_content = ""  
        if url_input:  
            urls = [url.strip() for url in url_input.split(",")]  
            for url in urls:  
                if url.startswith("http"):  
                    all_content += get_readable_content(url) + "\n"  
        if pdf_files:  
            for pdf_file in pdf_files:  
                pdf_text = extract_text_from_pdf(pdf_file)  
                all_content += pdf_text + "\n"  
        return all_content.strip() or "未从提供的输入中提取任何内容。"  
用于回答问题的函数:

用户的提问通过_“subprocess.run()”调用本地安装的LLM。模型当前有60秒的超时时间“timeout=60”_。如果LLM响应过慢,会报错。

    def query_llama_via_cli(input_text):  
        try:  
            result = subprocess.run(  
                ["ollama", "run", "llama3.1"],  
                input=f"{input_text}\n",  
                stdout=subprocess.PIPE,  
                stderr=subprocess.PIPE,  
                text=True,  
                encoding='utf-8',  
                timeout=60  
            )  
            if result.returncode != 0:  
                return f"模型请求错误: {result.stderr.strip()}"  
            response = re.sub(r'\x1b\[.*?m', '', result.stdout)  
            return response.strip()  
        except subprocess.TimeoutExpired:  
            return "模型请求超时。请重试。"  
        except Exception as e:  
            return f"发生了一个意外错误: {str(e)}"

如前所述,在第一次测试[1]中,对答案能力级别的个人设定得到了很好的反馈。根据用户选择的级别,模型的回答会简化或更详细。许多商业应用并不具备这样的功能。若要获得更简单的答案,问题需相应调整。使用IEC,同一问题的回答可以根据需求快速轻松地调整其复杂度。提取的内容以块的形式传输,回答也是分块生成并组装的。

    def generate_responses_from_blocks(competence_level, extracted_content, user_question):  
        if not competence_level or not user_question.strip():  
            return "请先选择一个能力等级并输入一个问题。"  
        if not extracted_content.strip():  
            return "没有提取到可以作为答案基础的内容。"  

        if competence_level == "Beginner":  
            style_instruction = (  
                "用非常简单的词汇回答,避免使用专业术语,保持答案简短和简洁。"  
                "即使非专业人士也能理解地解释主题。"  
            )  
        elif competence_level == "Intermediate":  
            style_instruction = (  
                "提供清晰详细的回答,偶尔使用专业术语,并给出平衡的解释。"  
                "以读者能较好理解而不至于过于专业的方式来解释主题。"  
            )  
        elif competence_level == "Advanced":  
            style_instruction = (  
                "提供高度详细和技术性的回答,使用专业术语,并给出全面分析。"  
                "以高层次、学术化的语言来解释主题。"  
            )  
        else:  
            style_instruction = "根据提供的信息来回答。"  

        base_prompt = (  
            f"系统指令:\n{style_instruction}\n\n"  
            "注意:以下信息仅作为参考。请用自己的语言表述答案,并考虑所需的风格。\n\n"  
            "提供的信息(摘录如下):"  
        )  
        question_prompt = f"\n\n问题:\n{user_question}\n\n回答:"  

        prompt_prefix_tokens = len(tokenize_text(base_prompt)) + len(tokenize_text(question_prompt))  
        max_tokens_for_block = TOKEN_LIMIT - prompt_prefix_tokens  

        responses = []  
        for block in split_text_into_blocks(extracted_content, max_tokens_for_block):  
            input_text = base_prompt + block + question_prompt  
            response = query_llama_via_cli(input_text)  
            responses.append(response)  
        return "\n".join(responses)

这里有一个例子,比如。对于问题:这篇文章讲的是什么?文章),IEC(信息提取系统)根据不同的级别会给出不同的回答:

这是初学者的回答:

这篇文章似乎是一系列关于命名实体识别(NER)技术的参考文献和信息的汇总,命名实体识别技术是自然语言处理(NLP)的一个子任务,涉及在文本中识别和分类实体到预定义的类别,如人名、地点、组织等。文章引用了各种与NER相关的论文、文章和研讨会,包括那些专注于Twitter特有的挑战和用户生成的嘈杂文本。它还讨论了领域适应、众包开发黄金标准以及基于图的半监督学习模型等话题。总体而言,这篇文章似乎旨在提供命名实体识别研究最新进展的全面概述,重点是Twitter特有的挑战和用户生成的嘈杂文本。

中级回答:

这篇文章介绍了命名实体识别(NER)技术,这是一种自然语言处理的子任务,涉及在文本中识别和分类命名实体,将其归类为预定义的类别,如人名、组织名、地点等实体。文章概述了NER任务、其应用、挑战以及解决这些问题的各种技术,包括基于机器学习的方法、基于规则的系统以及混合模型。此外,文章还讨论了临床自然语言处理中高质量金标准的重要性以及为此目的使用众包技术的方法。文章还涉及将命名实体与外部知识资源(如维基百科)链接的主题。总的来说,文章提供了对NER任务及相关研究领域的全面回顾。

更详细的回答:

这篇文章似乎是对命名实体识别(NER)这一自然语言处理子任务的全面回顾,该任务涉及识别和分类文本中的实体,例如人名、地点、组织、日期、时间、数量和货币金额等。

本文涉及了NER的各个方面,包括:“NER”前已加上引号。后续内容中需保持“NER”的格式一致。

(注:翻译已按建议修改,但此处输出仅展示翻译文本部分,完整格式保持一致的建议需在后续内容中实现。)

本文涉及了NER的各个方面,包括:

  • 实体识别(NER)的重要性和应用范围
  • 不同机器学习方法在NER中的概述,包括支持向量机(SVMs)、条件随机场(CRFs)和深度神经网络
  • 讨论用于提高在嘈杂或未见过数据上NER性能的领域适应技术(域适应技术)
  • 回顾NER任务中使用的各种数据集和评估指标
  • 介绍处理命名实体链接和消歧的不同方法

总体来说,这篇文章对实体识别领域及其最前沿的方法做了详尽的介绍。

设计、结构与互动:

(注解:此处根据上下文需求,可考虑使用“交互性”以更准确地反映用户交互的概念。)

IEC的设计(CSS_CONTENT)、结构(HTML_CONTENT)和交互性(JS_CONTENT)在相应区域定义并可以自定义。CSS、HTML和JS已合并到一个脚本中,以避免在不同文件之间切换。根据个人喜好,也可以将这些部分分开来使用。这里有几个通过修改CSS、HTML或JS代码来调整应用程序的例子。可以通过CSS中的 .spinner 类来自定义处理输入时显示的加载图标。

下面的CSS代码定义了一个旋转加载指示器的样式。

.spinner {  
  border: 8px solid #f3f3f3;  
  border-top: 8px solid #3498db;  
  border-radius: 50%;  
  width: 50px;  
  height: 50px;  
  animation: spin 1s linear infinite;  
  margin: 20px auto;  
}  

@keyframes spin {  
  0% { transform: rotate(0deg); }  
  100% { transform: rotate(360deg); }  
}

模型的回答会逐字显示,像打字机一样。可以通过相应功能调整 “速度”

function 打字机效果(html, element, speed = 5) {  
  let i = 0;  
  let isTag = false;   
  let output = "";  

  function 打字效果() {  
    if (i < html.length) {  
      let char = html[i];  
      output += char;  

      if (char === "<") {  
        isTag = true;  
      }  
      if (char === ">") {  
        isTag = false;  
      }  

      element.innerHTML = output;  

      i++;  
      setTimeout(打字效果, isTag ? 0 : speed);  
    }  
  }  
  打字效果();  
}

可以通过HTML来自定义不同级别的按钮名称。

          <div class="button-group" id="competence-group">  
            <button class="competence-button" data-level="Beginner">初级</button>  
            <button class="competence-button selected" data-level="Intermediate">中级</button>  
            <button class="competence-button" data-level="Advanced">高级</button>  
          </div>
FLASK: API接口

最后,定义了Flask API端点,这些构成了应用的核心功能,主要包括:用户界面的实现、处理用户输入的内容以及生成对用户的回答。

    @app.route('/')  
    def index():  
        return Response(HTML_CONTENT, mimetype='text/html')  

    @app.route('/styles.css')  
    def styles():  
        return Response(CSS_CONTENT, mimetype='text/css')  

    @app.route('/script.js')  
    def script():  
        return Response(JS_CONTENT, mimetype='application/javascript')  

    @app.route('/extract_content', methods=['POST'])  
    def extract_content_endpoint():  
        if request.content_type.startswith('multipart/form-data'):  
            url_input = request.form.get("urls", "")  
            pdf_files = request.files.getlist("pdfs")  
        else:  
            data = request.get_json()  
            url_input = data.get("urls", "")  
            pdf_files = []  
        extracted = extract_content(url_input, pdf_files)  
        return jsonify({"content": extracted or "没有提取到内容。"})  

    @app.route('/ask_question', methods=['POST'])  
    def ask_question():  
        data = request.get_json()  
        competence_level = data.get("competence_level")  
        extracted_content = data.get("content", "")  
        user_question = data.get("question", "")  
        if not competence_level or not user_question.strip():  
            return jsonify({"response": "请选一个能力级别并输入一个问题。"})  
        if not extracted_content.strip():  
            return jsonify({"response": "没有可提取的内容。"})  
        response = generate_response_from_extracted_content(competence_level, extracted_content, "", None, user_question)  
        return jsonify({"response": response})  

    @app.route('/clear_extracted', methods=['POST'])  
    def clear_extracted():  
        return jsonify({"content": ""})  

    if __name__ == '__main__':  
        app.run(debug=True, host="0.0.0.0", port=5000) 
可能的应用

IEC可以在私人场合用于查看PDF文件或网站的内容,也可以为教育机构中的学习者提供个性化支持。具体而言,通过直接在终端设备上安装IEC程序,或者通过局域网与他人共享该程序,或使用隧道工具(如ngrok)通过互联网提供访问。

通过互联网提供 IEC 的访问(图片由作者提供)

在这里唯一需要确保的是,提供应用程序给学习者的电脑需要充当服务器。如果这台电脑的计算能力不足,请求将无法被有效处理。当前应用程序有三个不同级别。如果有需要,这些级别可以与学习者一同扩展。

IEC V1.5 Python 脚本文件 [github]

该应用程序可以从以下GitHub仓库下载并安装。

最后

本文介绍了如何使用HTML、CSS和JS创建一个交互式聊天机器人,而不使用诸如Dash、Streamlit、Gradio、Voila等流行的界面框架。即使通过Ollama使用的这些大型语言模型是闭源的,这些模型也可以利用其自然语言处理功能及其相应的内容(PDF文件或网址)来提供相关信息,从而进行当前话题的交流。

你可以最多拍手50下。

[1] Pietrusky, S. (2024). 在高等教育中推广人工智能素养(AI素养):评估IEC-V1聊天机器人在个性化学习和促进教育公平方面的效果。ARXIV CS.CY 点击此处

[2] Pietrusky, S. (2024). 如何在不使用专有模型的情况下与 PDF 文件交流:CLI + Streamlit + Ollama。Towards Data Science。文章链接

[3] 这里是链接: https://ollama.com/library/llama3.1/blobs/667b0c1932bc

仅供缩略图使用(图源作者)

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消