├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── rfcs ├── 0001-existing-solutions.md ├── 0002-Oct-work-report-by-rx.md ├── 0003-October-notes.md ├── November-work-report-by-rx.md ├── RAG_deployment_by_mxy.md ├── RustSBI Agent项目计划_20241014204801.pdf ├── assets │ ├── AsciiDoc1.png │ ├── AsciiDoc2.png │ ├── AsciiDoc3.png │ ├── RAG-Conclution.png │ ├── RAG-evaluate.png │ ├── RAG-systems.png │ ├── RAGFlow-arch.png │ ├── RAG_Demo.jpg │ ├── comparison_of_output.png │ ├── loading_success.png │ ├── old_rag_output.png │ ├── qanything_arch.png │ ├── retrieval_module.png │ ├── search_docs_false.png │ ├── search_docs_true.png │ ├── zzh-December │ │ ├── 1.png │ │ └── 2.png │ ├── zzh-November │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ └── zzh-October │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png ├── meeting_records_2025-01-09.md ├── meeting_records_2025-02-17.md ├── meeting_records_2025-03-15.md ├── meeting_records_2025-04-12.md ├── meeting_records_2025-05-16.md ├── notes │ ├── kjn-notes-2024.11.md │ ├── kjn-notes-2024.12.md │ ├── kjn-notes-2025.1.md │ ├── kjn-notes-2025.2.md │ ├── kjn-notes-2025.4.md │ ├── kjn-notes-2025.5.md │ ├── rx-notes-2024.12.md │ ├── zzh-notes-2024.10.md │ ├── zzh-notes-2024.11.md │ ├── zzh-notes-2024.12.md │ └── 开源大模型调研 by kjn.pdf ├── ollama搭建RAG系统.pdf ├── rag_pipeline_test_report_rx_0328.md ├── rag_pipeline_test_report_rx_0428.md ├── rag_pipeline_test_report_template.md ├── rag_test_report_kjn_0329.md ├── retrieval_module_design.md └── work_report │ ├── zzh-work-report-2024.11.md │ └── zzh-work-report-2024.12.md └── src ├── __init__.py ├── client ├── __init__.py ├── database │ ├── __init__.py │ ├── elasticsearch │ │ ├── __init__.py │ │ └── es_client.py │ ├── milvus │ │ ├── __init__.py │ │ └── milvus_client.py │ └── mysql │ │ ├── __init__.py │ │ └── mysql_client.py ├── embedding │ ├── __init__.py │ └── embedding_client.py ├── llm │ ├── base.py │ └── llm_client.py └── rerank │ ├── __init__.py │ ├── client.py │ └── test.py ├── configs └── __init__.py ├── core ├── chains │ └── condense_q_chain.py ├── file_handler │ ├── __init__.py │ ├── file_handler.py │ ├── parser │ │ ├── adoc_parser.py │ │ ├── markdown_parser.py │ │ └── pdf_parser.py │ └── test │ │ ├── RAG.png │ │ ├── readme.md │ │ ├── std-Rust.pdf │ │ └── test_file_handler.py ├── qa_handler.py └── retriever │ └── retriever.py ├── evaluation ├── BasicEval │ ├── file │ │ ├── riscv-privileged.pdf │ │ ├── riscv-privileged.txt │ │ ├── test01.txt │ │ ├── test_doc.doc │ │ ├── test_docx.docx │ │ └── test_pdf_3.pdf │ ├── multi_question_test.py │ └── multi_question_test_2.py ├── RAGTruth │ ├── Statistics_of_eval.py │ ├── dataset │ │ └── source_info.jsonl │ ├── eval_files │ │ ├── eval-DeepSeek-R1-Distill-Llama-8B.jsonl │ │ ├── eval-Qwen2.5-14B-Instruct.jsonl │ │ ├── eval-Qwen2.5-7B-Instruct.jsonl │ │ └── eval-internlm3-8b-instruct.jsonl │ ├── model_response │ │ ├── response-DeepSeek-R1-Distill-Llama-8B.jsonl │ │ ├── response-Qwen2.5-14B-Instruct.jsonl │ │ ├── response-Qwen2.5-32B-Instruct-AWQ.jsonl │ │ ├── response-Qwen2.5-7B-Instruct.jsonl │ │ └── response-internlm3-8b-instruct.jsonl │ └── ragtruth_eval.py └── test_local_doc_chat.py ├── server ├── api_server │ ├── __init__.py │ ├── api_client.py │ ├── dist │ │ └── index.html │ ├── sanic_api.py │ ├── sanic_api_handler.py │ ├── test_utils.py │ └── 这是一个测试文件.txt ├── embedding_server │ ├── __init__.py │ ├── embedding_backend.py │ ├── embedding_server.py │ ├── export_onnx.py │ └── start.sh ├── llm_server │ ├── start.sh │ ├── test_llm_server.py │ └── vllm_start.sh └── rerank_server │ ├── __init__.py │ ├── export_onnx.py │ ├── rerank_backend.py │ ├── rerank_server.py │ └── start.sh └── utils ├── __init__.py ├── general_utils.py └── log_handler.py /.gitignore: -------------------------------------------------------------------------------- 1 | src/parse/db/* 2 | *.onnx 3 | *.log 4 | __pycache__ 5 | logs 6 | bce_model 7 | configs.py 8 | configs.* 9 | file_store 10 | record.log 11 | vllm_record.log 12 | test_results_short.json 13 | test_results.json 14 | test_results* 15 | test_questions* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 RustSBI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agent 2 | RustSBI Specialized Domain Knowledge Quiz LLM 3 | 4 | ## Demos 5 | 6 | [GPTs Demo 入口](https://chatgpt.com/g/g-ubp707Cke-rustsbi-development-expert-test) 7 | 8 | [QAnything Demo 入口](https://ai.youdao.com/saas/qanything/#/bots/129B009D611B4051/share) 9 | 10 | ## Domain Knowledge Sources 11 | 12 | > Sources of Knowledge in the Knowledge Base. Our agent's domain knowledge will be extracted from the documents listed below. 13 | 14 | Rust Documentation: https://doc.rust-lang.org/stable/std/ 15 | 16 | RISC-V Documentation: https://github.com/riscv/riscv-isa-manual 17 | 18 | Bouffalo Docs: https://github.com/bouffalolab/bl_docs 19 | 20 | Bouffalo SDK: https://github.com/bouffalolab/bouffalo_sdk 21 | 22 | Xiangshan Docs: https://github.com/openxiangshan/xiangshan 23 | 24 | 25 | 26 | ## LLM Prompt Engineering 27 | 28 | > Introduction to Prompt Engineering, including documentation and high-quality articles. 29 | 30 | Prompt Engineering Guide: https://www.promptingguide.ai/zh 31 | 32 | Prompt Engineering Tools: https://learnprompting.org/docs/tooling/tools 33 | 34 | 35 | 36 | ## RAG Engineering 37 | 38 | > Introduction to RAG Engineering, including documentation and high-quality articles. 39 | 40 | RAG Beginner's Guide (Chinese): https://53ai.com/news/RAG/2024081636147.html 41 | 42 | RAG Beginner's Guide (English): https://www.singlestore.com/blog/a-guide-to-retrieval-augmented-generation-rag/ 43 | 44 | 45 | 46 | > Existing projects about RAG. 47 | 48 | Easy-RAG: [A RAG system implementation](https://github.com/yuntianhe2014/Easy-RAG) 49 | 50 | RAGFlow: [Another RAG implementation](https://github.com/infiniflow/ragflow) 51 | 52 | WikiChat: [A LLM system with knowledge from Wikipedia](https://github.com/stanford-oval/WikiChat) 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | concurrent-log-handler==0.9.25 2 | sanic==23.6.0 3 | sanic_ext==23.6.0 4 | onnxruntime==1.17.1 5 | numpy==1.24.3 6 | transformers==4.46.1 7 | langchain-core==0.1.50 8 | langchain==0.1.9 9 | langchain-openai==0.0.8 10 | langchain_elasticsearch==0.2.2 11 | mysql-connector-python==8.2.0 12 | pymilvus==2.4.2 13 | chardet==5.2.0 14 | aiomysql==0.2.0 15 | pytorch==2.2.0 -------------------------------------------------------------------------------- /rfcs/0001-existing-solutions.md: -------------------------------------------------------------------------------- 1 | # 现有开源方案尝试 2 | 3 | ## 测试方案 4 | 5 | 目前采用文档 riscv-unprivileged.pdf 作为测试样本文档。 6 | 7 | 测试使用问题包括: 8 | 9 | - (简单)介绍 RISC-V 中的加法指令。 10 | - (中等)48位位宽指令的二进制表示? 11 | - (算术)在satp寄存器中,若ppn为0x1234,类型mode为sv39,satp的值是多少? 12 | 13 | 14 | ## 测试:GPTs 15 | 16 | ### 测试结果 17 | 18 | 能够在原文档中寻找到对应的知识点,准确回答问题;**不能遵守 prompt 中的格式指令。** 19 | 20 | 计划后续对其 API 进行测试。 21 | 22 | [GPTs Demo 入口](https://chatgpt.com/g/g-ubp707Cke-rustsbi-development-expert-test) 23 | 24 | ### 使用的 prompt 25 | 26 | 版本1: 27 | 28 | ``` 29 | This GPT is an expert assistant in RustSBI development and RISC-V architecture, with a preference for answering in Chinese. Its primary role is to assist users by finding and explaining relevant paragraphs from the provided documents (e.g., Rust Standard Library, RISC-V Unprivileged ISA) related to Rust, RISC-V, and RustSBI. It should quickly understand queries about these topics and extract information from the documents to deliver precise, helpful responses in Chinese. The GPT should guide users on system-level programming, bootloaders, and low-level interaction between Rust and RISC-V, focusing on RustSBI development. When a user asks a question, the GPT searches the uploaded documentation, finds relevant sections, and replies with those sections in Chinese, followed by a simple explanation. 30 | 31 | Each response must also include the original text citation from the document, displayed inside a markdown block quote format, and positioned at the end of the response. If multiple sources are referenced, cite them all with their respective original text. The original citation should indicate the document source. Output the citation in the following format: 32 | 33 | 上述内容参考自:[Document Name, e.g. The RISC-V Instruction Set Manual Volume I | © RISC-V International​] 34 | 以下为参考原文: 35 | > ... 36 | > ... 37 | > ... 38 | 39 | The main body of the answer should be detailed and comprehensive, ensuring clarity for the user. If a query is unclear, it should ask for clarification, but aim to offer useful information proactively. 40 | ``` 41 | 42 | 版本2: 43 | 44 | ```markdown 45 | You are a technical support assistant specializing in RustSBI development and RISC-V architecture, with a preference for responding in Chinese. Your primary role is to assist users by finding and explaining relevant paragraphs from the provided documents related to Rust, RISC-V, and RustSBI, providing detailed and professional answers. 46 | 47 | #### **Role: RustSBI Technical Support** 48 | - You are an expert in RustSBI and RISC-V architecture, responsible for offering technical support, troubleshooting, and guiding users in programming. Maintain a patient, friendly, and professional attitude to ensure users have a clear understanding of system-level programming, bootloaders, and low-level interactions between Rust and RISC-V. 49 | 50 | #### **Key Skills:** 51 | 1. **Technical Support and Troubleshooting:** 52 | - Capable of solving user technical issues, including troubleshooting, error analysis, and providing effective solutions. Proficient in Rust programming and code correction techniques. 53 | - Able to quickly search the knowledge base for relevant information and provide detailed explanations based on documentation and technical experience. 54 | 2. **Clear Communication and Expression:** 55 | - Avoid using complex terminology when answering to ensure information is clear and understandable. Maintain a friendly and patient attitude while communicating with users, explaining technical issues and solutions. 56 | - If the query is unclear, ask users for more details to assist them better. 57 | 3. **Knowledge Base Utilization:** 58 | - When an answer is found, cite the original text and present it in Markdown format. If an answer cannot be found, be honest and inform the user. 59 | 60 | #### **Response Guidelines:** 61 | - **Response Strategy:** Combine knowledge base content and experience to provide detailed and clear responses. Respond in Chinese, explaining relevant concepts thoroughly and listing specific steps (if applicable). 62 | - **Citation Format:** Include the citation at the end of the response in the following format: 63 | - “The above content is referenced from: [Document Name]; The reference content is as follows:" 64 | - Use "---" to separate the main response from the citation and use ">" to mark the quoted content. The quoted content must exactly match the original text in the input prompt, and as much of the relevant content should be listed as possible. 65 | - **MUST** contain at least one paragraph of raw content from the knowledge base. If there are **NOTHING** from knowledge base, reply "there are no reference contents". 66 | 67 | #### **Handling Unclear Queries:** 68 | - If the user's query is not clear, politely request clarification or additional details. 69 | 70 | #### **Workflow:** 71 | 1. Analyze the user's query, search for relevant content in the knowledge base, and provide a detailed and clear response based on technical experience. 72 | 2. Maintain patience and a friendly attitude in responses, avoiding excessive use of technical jargon to ensure user understanding. 73 | 3. Before ending each interaction, confirm if the user has any additional questions to ensure their needs are fully met. 74 | ``` 75 | 76 | ## 测试:RAGFlow 77 | 78 | ### 测试结果 79 | 80 | RAG 文档切片功能:采用 Manual 模式进行文档切片时,无法顺利查询。 81 | 82 | 采用 Book 模式进行文档切片,可以正常问答;但是只能引用完整的 PDF,无法引用文档段落。 83 | 84 | RAGFlow 还包含其他文档切片模式,暂未测试。 85 | 86 | ## 测试:QAnything 87 | 88 | QAnything 可以直接进行网页测试并发布 demo,已经进行基础 demo 部署,可以通过以下链接进入使用。 89 | 90 | [QAnything Demo 入口](https://ai.youdao.com/saas/qanything/#/bots/129B009D611B4051/share) 91 | 92 | ### 测试结果 93 | 94 | QAnything 不支持选择文档切片算法;采用的检索组件与 RAGFlow 一致,为 BCEmbedding。测试结果上,**可以比较好地遵守指令格式进行输出**。 95 | 96 | ### 使用的 prompt 97 | 98 | ```markdown 99 | ## Role: RustSBI技术支持 100 | - 负责解决用户常见技术问题,排查故障,进行代码撰写与纠错,需要具备耐心、友好和专业的态度。 101 | ## Skills: 102 | ### 技能1:技术支持与故障排查 103 | - 具备解决用户技术问题的能力,包括排查故障、错误分析和提供有效解决方案。 104 | - 熟练掌握代码撰写和纠错技巧,能够帮助用户解决代码中的错误和 Bug。 105 | ### 技能2:清晰表达与沟通技巧 106 | - 能够清晰简洁地回答用户提出的问题,避免技术术语和复杂表达,确保信息传达准确易懂。 107 | - 具备良好的沟通技巧,能够以友好耐心的态度与用户交流,解释技术问题和解决方案。 108 | ### 技能3:知识库应用 109 | - 能够根据知识库检索结果快速找到解决方案,并以清晰简洁的方式回答用户问题。 110 | - 在无法找到答案时,能够诚实回答:“我无法回答您的问题。” 111 | ## Rules: 112 | - 回答用户问题时,需基于已验证的资料或知识库结果,不编造答案。 113 | - 在解答末尾附上参考资料原文,并注明出处。 114 | - 参考资料原文需要与输入 prompt 中的原文完全一致,并尽可能多地列出。采用 “---” 将解答和引用进行分隔,采用 ">" 标识引用原文的来源。 115 | - 确保回答内容专业准确,与用户友好交流,提高用户满意度和信任感。 116 | ## Workflow: 117 | - 针对用户问题,结合知识库结果和技术经验,以清晰简洁的方式进行回答。 118 | - 在解答过程中展现耐心和友好态度,确保用户理解并满意。 119 | - 每次交流结束前确认是否还有其他问题需要帮助,保证用户需求得到全面满足。 120 | ``` 121 | 122 | -------------------------------------------------------------------------------- /rfcs/0002-Oct-work-report-by-rx.md: -------------------------------------------------------------------------------- 1 | # 10 月实习记录 2 | 3 | 4 | ## 综述阅读:Retrieval-Augmented Generation for Large Language Models 5 | 6 | ### 1. RAG 系统构成 7 | 8 | 检索增强生成(RAG)系统是一种结合了信息检索与生成模型的先进架构,旨在提升自然语言处理(NLP)的能力。系统的核心理念在于通过检索机制为大型语言模型(LLM)提供实时、相关的背景信息,从而使生成的回答更加准确和可信。 9 | 10 | - 朴素 RAG(Naive RAG)作为 RAG 系统的基础实现,主要依赖于将检索到的文档片段直接输入到语言模型中进行回答生成。这种方法简单易用,适用于回答较为直接且上下文要求不高的问题。然而,在处理需要多层推理或复杂背景的任务时,朴素 RAG 的效果往往会受到限制。 11 | 12 | - 进阶 RAG(Advanced RAG)引入了更复杂的检索策略。进阶 RAG 通过实现前检索(Pre-Retrieval)和后检索(Post-Retrieval)的方法,优化了信息获取的质量。例如,在检索之前,系统先分析用户的查询意图,然后基于意图选择最相关的文档块进行检索;而在获取检索结果后,系统对文档内容进行重排序,以确保最相关的信息优先呈现给模型,避免受“中间遗忘”影响。这种方式显著提高了 RAG 系统在复杂场景下的有效性,但依然依赖于单一检索结果。 13 | 14 | - 模块化 RAG(Modular RAG)则是对 RAG 框架的进一步扩展和优化。它通过引入灵活的模块化设计,允许系统根据具体任务需求进行动态调整。在模块化 RAG 中,各个组件(如检索器、生成器、评估器)可以根据任务的不同要求进行组合或替换,这种灵活性使得系统能够应对多样化和动态变化的任务需求。例如,模块化 RAG 可以同时处理多轮对话和信息整合任务,这在朴素和进阶 RAG 中是难以实现的。因此,模块化 RAG 在实际应用中表现出色,尤其是在需要复杂推理和多层次理解的场景中,展现出更强的适应性和性能。 15 | 16 | ![RAG-systems](./assets/RAG-systems.png) 17 | 18 | --- 19 | 20 | > 在构建RAG系统时,最基本的两个要素分别是 **Retrieval** 和 **Generation**, 其中 Retrieval 又可以分出 Indexing 这个小步骤。在之后构建的系统中,可以考虑从 Naive RAG 开始,先通过 langchain 这样的集成式工具包装提供的现有开源方案,构建一个 Indexing - Retrieval - Generation 的初步模型,再对方案从结构、组成部分,组分细节等方面进行逐步的改进。 21 | > 22 | > 最终完成的 Agent 成品应该属于 Advanced RAG 类型,作为资料检索助手侧重于根据查询提供准确,全面的信息,同时作为 code partner 提供代码编写辅助。 23 | 24 | 25 | 26 | ### 2. RAG 检索 27 | 28 | #### 2.1. 检索原理 29 | 30 | RAG 系统的检索机制是其核心功能之一,旨在为生成模型提供相关的上下文信息。其关键步骤包括**索引(indexing)、查询处理和文档检索**。 31 | 32 | 在索引阶段,系统首先对大量文本数据(如文档、知识库等)进行预处理,提取关键信息并构建索引。这一过程涉及文本的分词、去除停用词和生成嵌入向量等操作。通过这种方式,系统能够将文本数据转换为一种适合快速搜索的形式,通常是转化为存储在向量数据库(vector database)中的向量,从而在后续的检索中实现高效的相似性搜索。 33 | 34 | 当用户提出查询时,系统将该查询转化为向量表示,利用预先构建的索引进行检索。检索过程会计算查询向量与文档向量之间的**相似度**,以找出与查询最相关的文档片段。检索到的文档片段随后会与问题一同被输入到生成模型中,辅助模型生成更为准确和上下文相关的回答。 35 | 36 | #### 2.2. 检索优化 37 | 38 | ##### 2.2.1. 索引优化 39 | 40 | 目前常见的 Indexing 操作是将长文本以分块(chunk)形式进行分割,将大型文档拆分成较小的(限定最大字符数的)段落或块,也有方案采用 Phrase 或 Sentence 作为分割单位(但这类方案较少)。 41 | 42 | 分块后的文本在进行 Embedding 之前可以通过添加元数据(matadata,如文档类别、关键词、创建日期等)组成集合的方式加以优化;通过基于这些元数据进行检索过滤,可以限制检索的范围,提升检索的精确性。此外,在检索过程中为文档时间戳分配不同权重,可以实现时间感知的 RAG,确保知识的时效性,避免过时信息的使用。除了从原始文档中提取元数据外,元数据还可以通过人工构造。例如,可以为段落添加摘要,并引入假设性问题,这种方法也被称为逆向 HyDE。具体来说,利用 LLM 生成可以被文档回答的问题,并在检索时计算原始问题与假设性问题之间的相似性;通过这种方式,可以缓解问题与答案之间的语义差距过大带来的检索不准确问题。 43 | 44 | 在 Indexing 之前,首先需要获取 **适合** 进行 indexing 的输入。这种输入包括完全纯文本输入(txt, etc.)和带有一定格式标记的文本输入(markdown, adoc, etc.)。对于 PDF、DOCX 等格式的文件,则需要先进行解析,转换为合适的文本序列,再进行 Indexing。图片形式的文本文档需要先进行 OCR。文档中带有的图片、表格等内容也属于难点;目前部分解决方案选择采用数据库存储表格,这或许是一个可行的思路。 45 | 46 | 动态更新索引也是索引优化策略的一部分。随着文档库的不断扩展和更新,能够及时反映新信息的索引是必要的。尤其在 RustSBI Agent 项目中,随着语言标准、IoT 设备标准等的不断更新和完善,势必有旧的资料过时和新的资料产生。这可以通过定期重新创造索引或采用增量更新策略来实现,属于 Agent 项目的 **L2 预期目标**。 47 | 48 | --- 49 | 50 | > 我们目前面临的输入形式大体包括 adoc、markdown、pdf、txt(源码) 这几类。月中对 QAnything 开源代码进行阅读分析时发现其策略主要是将其他类型的文档转换为 markdown 格式,再通过 langchain 提供的 RecursiveCharacterTextSplitter 进行文本分割。Agent L1 阶段构建思路或许可以进行借鉴。 51 | > 52 | > L2 阶段,可以考虑对索引进行元数据、结构化、动态更新等方面的优化。 53 | 54 | ##### 2.2.2. 查询优化 55 | 56 | 查询(Query),通俗来说,就是用户提出的 **问题**。在朴素 RAG 系统中,直接依赖用户的原始查询作为检索的基础是一项主要挑战。制定精确且清晰的问题往往困难重重,而不恰当的查询会导致检索效果不佳。1)问题本身较为复杂 和 2)语言的复杂性和模糊性 两者是导致查询不够精准、产生误解和迷惑的主要原因。正如 “LLM” 一词的多义性,在不同领域有着不同意义的查询可能带来不符合预期的回复。 57 | 58 | ##### 1) 查询扩展 59 | 60 | 将单一查询扩展为多个查询是一种有效的方法,可以丰富查询内容并提供更多上下文,从而解决特定细节的缺失,确保生成答案的最佳相关性。查询扩展的一个具体实现是多查询(Multi-Query)策略,通过提示工程利用 LLM 扩展查询,并对这些查询进行并行执行。 61 | 62 | 还有一种方案是将查询分解为子查询(Sub-Query);通过分解规划生成必要的子问题,为原始问题提供上下文并完整回答。这一过程本质上与查询扩展类似,具体来说,可以使用由简入繁的提示方法,将复杂问题分解为一系列更简单的子问题。此外,链式验证(Chain-of-Verification, CoVe)方法可以对扩展的查询进行验证,以降低模型生成幻觉的概率。经过验证的扩展查询通常展现出更高的可靠性。 63 | 64 | ##### 2) 查询转换 65 | 66 | 查询转换的核心思想是基于转化后的查询进行检索,而非用户的原始查询。查询重写(Query Rewrite)是实现查询转换的一种方法。原始查询并不总是最适合 LLM 的检索,因此可以提示 LLM 对查询进行重写。除了利用 LLM 进行查询重写,专门的小型语言模型(如 RRR,即 Rewrite-Retrieve-Read)也可用于这一目的。在 Taobao 的实现中,BEQUE 方法显著提高了长尾查询的召回效果,推动了 GMV 的增长。 67 | 68 | 另一种查询转换方法是通过提示工程让 LLM 根据原始查询生成新的查询,以供后续检索。HyDE 方法构建假设性文档,即假定原始查询的答案,重点在于从答案之间的嵌入相似性来进行检索,而非对问题或查询本身的相似性。采用反向提示方法(Step-back Prompting),原始查询会被抽象为一个高层概念性问题,用于生成更有针对性的检索请求。在 RAG 系统中,既使用反向问题,也使用原始查询进行检索,两个结果共同作为生成答案的基础。 69 | 70 | --- 71 | 72 | > 对于用户查询,在 Agent 初步构建阶段可以暂时不考虑优化;初步构建完成后,再尝试引入小模型进行查询扩展和重写的尝试。需要注意的是,如果进行重写的 LLM 选择不合适,查询中存在的关键信息可能随着重写而消失 / 误解,反而导致对于查询的负优化。 73 | 74 | ##### 2.2.3. 其他优化 75 | 76 | **1) 混合检索**:结合稀疏和稠密嵌入方法能够捕获不同的相关性特征,从而相辅相成,提升检索效果。稀疏检索模型(如 BM25)通常侧重于关键词匹配,而稠密检索模型(如基于 BERT 的模型)则关注语义相似度。通过将稀疏检索的初步结果提供给稠密检索模型,系统能够在初步筛选的基础上进一步优化答案的相关性。此外,预训练语言模型(PLMs)可以用于学习词项的权重,从而增强稀疏检索的表现。这种混合策略不仅提升了检索的鲁棒性,还能有效处理包含稀有实体的查询,使得系统在面对多样化查询时更加灵活。 77 | 78 | **2) 嵌入模型的微调**:在特定领域中,当上下文与预训练语料库存在显著差异时,尤其是在医疗、法律等行业中,拥有大量特有术语和行话,微调嵌入模型显得尤为重要。通过在自己领域的数据集上进行微调,可以有效弥补模型与实际应用之间的差距,提升检索和生成的质量。微调的目的不仅是补充领域知识,还能促进检索器与生成器之间的对齐。例如,使用 LLM 结果作为微调的监督信号,这种方法称为 LSR(LM-supervised Retriever)。通过这种方式,检索器能够更好地适应特定任务的需求。 79 | 80 | **3) 使用适配器**:在微调模型时,可能会面临集成功能、API 接口或受限于本地计算资源的挑战。因此,一些方法选择加入外部适配器以帮助实现模型对齐。例如,UPRISE 训练了一个轻量化的提示检索器,能够自动从预构建的提示池中检索适合特定零样本任务输入的提示。这种方式优化了 LLM 的多任务能力,使得系统能够更灵活地应对不同任务的需求。 81 | 82 | 在适配器方面,AAR(增强适配检索器)引入了一个通用适配器,以适应多种下游任务。PRCA 则在此基础上增加了一个可插拔的奖励驱动上下文适配器,以提升特定任务的表现。BGM 保持检索器和 LLM 不变,在两者之间训练了一个桥接的 Seq2Seq 模型,旨在将检索到的信息转化为 LLM 可以有效利用的格式。这种方法不仅支持对检索结果的重新排序,还可以为每个查询动态选择最相关的段落,甚至在必要时采取更高级的策略(如去重)。此外,PKG 通过指令微调的方法,将知识集成到白盒模型中,直接替换检索模块以生成相关文档。这一方法有效应对了微调过程中遇到的困难,并提升了模型的整体性能。 83 | 84 | --- 85 | 86 | > 这部分不太懂,暂时先贴上。 87 | 88 | 89 | 90 | ### 3. RAG 生成 91 | 92 | 在 RAG 系统中,检索过程完成后,直接将所有检索到的信息输入到大型语言模型(LLM)中以生成回答并不是最佳实践。为了提升生成结果的质量和相关性,通常需要从两个角度进行调整:一是调整检索到的内容,二是优化 LLM 的生成过程。 93 | 94 | #### 4.1. 生成原理 95 | 96 | 生成过程涉及将检索到的相关文档片段作为上下文输入到 LLM 中,以生成最终的回答。该过程依赖于 LLM 对上下文的理解和处理能力,模型通过分析输入的文档信息,结合自身的语言生成能力,生成对用户查询的恰当回应。在这一过程中,检索的准确性和上下文的质量至关重要,因为生成模型的效果直接受到输入信息的影响。如果检索到的文档内容不相关或冗余,模型可能会生成不准确或无关的回答。因此,在生成之前,对检索到的信息需要进行有效的处理和筛选,以确保其相关性和清晰性。 97 | 98 | #### 4.2. 生成优化 99 | 100 | 生成优化的首要环节是对检索内容进行再排序(reranking),以确保最相关的文档优先被输入到 LLM 中。再排序的过程通过重新排列文档块,突出最相关的结果,从而有效减少待处理的文档池。这一过程不仅提高了信息检索的效率,还是一种信息过滤手段,为模型处理提供了更精炼的输入。再排序可以采用基于规则的方法,依赖于预定义的指标(如多样性、相关性和平均排名倒数)进行评估,也可以使用模型驱动的方法,如 BERT 系列的编码器-解码器模型(例如 SpanBERT)或专门的再排序模型(如 Cohere rerank 或 bge-raranker-large)进行处理。这些方法通过综合考虑文档之间的相关性,为 LLM 提供更高质量的输入,确保生成的答案更加准确。 101 | 102 | 此外,进行上下文选择和压缩(Context Selection/Compression)也是重要的优化手段。一个常见的误解是,检索越多相关文档并将其拼接成长文本越有利。然而,过多的上下文可能引入噪声,导致 LLM 对关键信息的感知降低。例如,LLMLingua 使用小型语言模型(如 GPT-2 Small 或 LLaMA-7B)来检测并移除不重要的标记,从而将文本转化为 LLM 更易理解的形式,而不必增加额外的训练。这种方法有效平衡了语言的完整性与压缩率。 103 | 104 | 在此基础上,PRCA 通过训练信息提取器来解决这一问题,而 RECOMP 则采用对比学习训练信息浓缩器。这一方法利用对比损失对编码器进行训练,使每个训练数据点由一个正样本和多个负样本组成,从而优化文档选择和压缩过程。“Filter-Reranker”范式结合了 LLM 和小型语言模型的优势,前者作为过滤器,后者则负责重新排序。研究表明,通过指导 LLM 对小型语言模型识别的挑战样本进行重新排序,可以显著改善信息提取任务的表现。此外,LLM 还可以在生成最终答案之前对检索内容进行评估,从而在生成过程中自动过滤不相关的文档,提高生成质量。 105 | 106 | --- 107 | 108 | > 现有的 RAG 开源方案中基本都能找到 rerank 部分的身影。比如下方 QAnything 和 RAGFlow 的 system 架构图,其中 Rerank / 2nd Retrieval 都是重要部分。 Rerank 的主要目的是在初步 Retrieval 得到的相关资料中再次挑选出最相关的内容,相较于 Embedding / 1st Retrieval,在基础的向量 **相似性** 比较之外,它可以进一步进行 **相关性** 的比较,具体可见 [QAnything 技术访谈](https://www.53ai.com/news/RAG/869.html) 中的分析。 109 | > 110 | > Filter 在目前能够找到的开源方案中则更少见,不排除 Rerank 之后缩小给出的 Chunk 数量本身就是一种 Filter 操作。Rerank 有很多打包成可调用 python 库的开源实现,可以考虑在 Agent 构造中添加进行效果测试。 111 | > 112 | > qanything_system 113 | > 114 | > RAGFlow-arch 115 | 116 | ### 4. RAG 流程优化 117 | 118 | 一般的 RAG 做法为单次检索后直接进行生成,但这种方法可能导致推理输出不如人意,尤其在需要多步推理的复杂问题中。因此,对 RAG 的流程进行优化也是一个改进方向。 119 | 120 | #### 4.1. 迭代检索 121 | 122 | 迭代检索是一种通过基于初始查询和已有生成文本反复搜索知识库的过程。这一方法允许系统在每一步中交替进行检索和生成,以从知识库中获得更加丰富和针对性的上下文信息。研究表明,迭代检索能够通过多次检索迭代提供额外的上下文参考,从而增强后续答案生成的稳健性。然而,迭代过程也可能受到语义不连续性和无关信息累积的影响。 123 | 124 | #### 4.2. 递归检索 125 | 126 | 递归检索在信息检索和自然语言处理(NLP)中被广泛应用,旨在提高搜索结果的深度和相关性。该过程通过基于先前搜索结果逐步优化搜索查询,逐步收敛到最相关的信息,从而提升搜索体验。IRCoT 模型利用思维链(chain-of-thought)指导检索过程,并根据获得的检索结果精炼思维链。ToC 方法则创建了一个澄清树,系统性地优化查询中的模糊部分,尤其在用户需求不明确或信息高度专业化的复杂搜索场景中尤为有效。递归检索过程的结构化索引能够以层次化的方式处理和检索数据,可能包括对文档部分或冗长 PDF 的总结,以便在此总结基础上进行检索。随后,在文档内部进行二次检索进一步精细化搜索,体现了这一过程的递归特性。与此不同,多跳检索则旨在深入图结构数据源,提取相互关联的信息,适合处理复杂的信息网络。 127 | 128 | #### C. 自适应检索 129 | 130 | 自适应检索方法,如 Flare 和 Self-RAG,通过使 LLM 能够主动判断检索的最佳时机和内容,进一步优化了 RAG 框架,从而增强信息源的效率和相关性。这些方法反映了一个更广泛的趋势,即 LLM 在其操作中采取主动判断的角色,类似于 AutoGPT、Toolformer 和 GraphToolformer 等模型代理。 131 | 132 | 以 Graph-Toolformer 为例,该模型将其检索过程分为多个独立步骤,LLM 主动使用检索器,应用 Self-Ask 技术,并利用少量示例提示来发起搜索查询。这种主动的方式使得 LLM 可以决定何时检索必要的信息,类似于代理使用工具的方式。WebGPT 通过整合强化学习框架,训练 GPT-3 模型在文本生成过程中自主使用搜索引擎。它使用特殊的标记来实现搜索引擎查询、浏览结果和引用参考文献等操作,从而通过外部搜索引擎扩展 GPT-3 的能力。 133 | 134 | --- 135 | 136 | > 通过多次循环对答案进行优化;但同时会带来 Token 翻倍,语义偏移等问题。在之前有过的经验中这种方法表现不是很好,之后考虑尝试部署查看效果。 137 | 138 | 139 | 140 | ### 5. RAG 效果评估 141 | 142 | RAG-evaluate 143 | 144 | 表格中是主要的 RAG 系统评价指标,分别从上下文相关性(Context Relevance)、可信性(Faithfulness)、答案相关性(Answer Relevance)、噪声鲁棒性(Noise Robustness)、拒答能力(Negative Rejection)、信息整合(Information Integration)和反事实鲁棒性(Counterfactual Robustness)等多个方面对系统的表现进行评估。 145 | 146 | **横轴:** 147 | 148 | - **Context Relevance**:评价系统在提供上下文时的准确性和相关性,确保返回的内容能有效支持问题的回答。 149 | - **Faithfulness**:关注生成内容是否忠实于检索到的信息,避免生成与检索内容不符的回答。 150 | - **Answer Relevance**:评估生成的答案是否与用户问题直接相关,确保回答的针对性。 151 | - **Noise Robustness**:系统对噪声数据的耐受性,确保即便检索到包含无关内容的文档,系统依然能生成准确的回答。 152 | - **Negative Rejection**:系统拒绝回答不相关问题的能力,避免在无关或知识缺失情况下生成无效回答。 153 | - **Information Integration**:衡量系统从多个文档中整合信息的能力,尤其是在多步推理或复杂问题下的表现。 154 | - **Counterfactual Robustness**:系统面对包含误导或虚假信息的内容时的抵抗能力,确保生成的回答不会被不实信息影响。 155 | 156 | **纵轴:** 157 | 158 | - **Accuracy**:适用于评价上下文相关性、可信性、答案相关性、噪声鲁棒性和拒答能力,衡量回答的整体准确性。 159 | - **EM (Exact Match)**:评估生成答案的精准度,与用户问题的预期回答是否完全匹配。 160 | - **Recall 和 Precision**:用于衡量检索阶段的表现,关注上下文相关性,即系统是否能找到所有相关内容(Recall)和返回的内容是否准确(Precision)。 161 | - **R-Rate**:对噪声鲁棒性的量化评价,测量系统在面对噪声数据时保持准确性的能力。 162 | - **Cosine Similarity**:衡量答案和上下文的相似性,确保生成答案与检索内容语义一致。 163 | - **Hit Rate 和 MRR (Mean Reciprocal Rank)**:用于上下文相关性,主要评估检索到的内容是否包含正确答案,以及正确答案在检索结果中的排名。 164 | - **NDCG (Normalized Discounted Cumulative Gain)**:对上下文相关性评价较高的指标,关注检索结果的质量和相关性。 165 | - **BLEU 和 ROUGE**:用于生成质量评估,BLEU 和 ROUGE 分别衡量生成文本与参考文本的匹配程度,常用于生成回答的相关性和准确性评价。 166 | 167 | 168 | 169 | ### 6. 总结 170 | 171 | RAG-Conclution 172 | 173 | 在目前的基础上,我们将要构建的是一个 Naive / Advanced RAG 系统。主要面对的 Downstream Tasks 正如图中所示,其中侧重于 Question Answering 和 Fact Verification。可能用到的优化技术包括 Chunk Optimization, Query Transformation, Context Selection 等。 数据收集和处理会是碰到的第一个难点,而 RAG 系统相对来说搭建起来将会更为方便。鉴于目前情况,Fine-tuning 可能暂时没有对应技术和设备支持;而 Evaluation 在构建好 RAG 系统之后也是需要考虑的事情。 174 | 175 | 176 | 177 | --- 178 | 179 | --- 180 | 181 | 182 | 183 | **DONE:** 184 | 185 | - 进行 RAG 相关知识学习,了解相关概念和方案; 186 | - RAGFlow 和 QAnything 在线版本测试。 187 | 188 | **TODO:** 189 | 190 | - 整理用于第一批测试的,RAG system demo 搭建好之后的知识库内容; 191 | - 搭建 Naive RAG demo; 192 | - 在新获得的服务器上部署测试 QAnything;解决 docker in docker 问题。 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /rfcs/0003-October-notes.md: -------------------------------------------------------------------------------- 1 | # 10月工作记录 2 | 3 | - [RustSBI Agent项目计划](RustSBI%20Agent%E9%A1%B9%E7%9B%AE%E8%AE%A1%E5%88%92_20241014204801.pdf) 4 | - [综述阅读:Retrieval-Augmented Generation for Large Language Models](./0002-Oct-work-report-by-rx.md) 5 | - [开源大模型调研 by kjn](./%E5%BC%80%E6%BA%90%E5%A4%A7%E6%A8%A1%E5%9E%8B%E8%B0%83%E7%A0%94%20by%20kjn.pdf) 6 | - [read notes by zzh](./read-notes-by-zzh.pdf) 7 | -------------------------------------------------------------------------------- /rfcs/November-work-report-by-rx.md: -------------------------------------------------------------------------------- 1 | # 11 月实习工作记录 2 | 3 | 4 | 5 | ## 工作月报 6 | 7 | ##### MULTI RAG 8 | 9 | 张同学在实习所给的服务器上,使用 `langchain` 框架实现了对单个 PDF 文档的向量化存储功能和文档的分块与检索,集成了 `Qwen:7b` 作为 Embedding 模型进行检索和 `Gemma2:27b` 作为生成模型进行回答的生成,并使用 `streamlit` 实现用户交互的界面。但是这个 RAG 系统存在输入来源单一的问题,只支持 PDF 解析。 10 | 11 | 注意到,`RustSBI Agent` 的领域知识来源之一 `RISC-V Unprivileged RISC-V Instruction Set Manual` 在其源 `GitHub` 仓库中主要以 `AsciiDoc` 格式存储。为了提高 `RustSBI Agent` 回答 `RISC-V` 相关问题的能力,支持解析 `AsciiDoc` 格式文档,因此进行了相应的代码改进和功能扩展。 12 | 13 | --- 14 | 15 | ## AsciiDoc 格式解析 16 | 17 | #### **1. 什么是 AsciiDoc?** 18 | 19 | AsciiDoc 是一种轻量级的文档标记语言,专为撰写技术文档而设计。它具有类似 Markdown 的语法简洁性,同时支持更复杂的文档结构和排版需求。AsciiDoc 文档可以轻松转换为 HTML、PDF、EPUB 等格式,适用于书籍、手册、API 文档和技术报告。 20 | 21 | #### **2. 核心语法概述** 22 | 23 | ##### **(1) 标题** 24 | 25 | 通过 `=` 符号表示标题层级: 26 | 27 | ```adoc 28 | = 一级标题 29 | == 二级标题 30 | === 三级标题 31 | ==== 四级标题 32 | ``` 33 | 34 | ##### **(2) 段落** 35 | 36 | 普通段落直接书写: 37 | 38 | ```adoc 39 | 普通段落直接输入文字即可。 40 | ``` 41 | 42 | ##### **(3) 列表** 43 | 44 | 支持有序列表和无序列表: 45 | 46 | - 无序列表 47 | 48 | ```adoc 49 | * 无序项 1 50 | * 无序项 2 51 | ** 嵌套无序项 52 | ``` 53 | 54 | - 有序列表 55 | 56 | ```adoc 57 | . 有序项 1 58 | . 有序项 2 59 | .. 嵌套有序项 60 | ``` 61 | 62 | ##### **(4) 链接与图像** 63 | 64 | - 超链接 65 | 66 | ```adoc 67 | http://example.com[链接文字] 68 | ``` 69 | 70 | - 嵌入图像 71 | 72 | ```adoc 73 | image::path/to/image.png[图片描述] 74 | ``` 75 | 76 | ##### **(5) 代码块** 77 | 78 | 支持嵌入代码块,语法如下: 79 | 80 | ```adoc 81 | [source,python] 82 | ---- 83 | def hello_world(): 84 | print("Hello, AsciiDoc!") 85 | ---- 86 | ``` 87 | 88 | ##### **(6) 表格** 89 | 90 | ```adoc 91 | |=== 92 | | 列 1 | 列 2 | 列 3 93 | | 数据 1 | 数据 2 | 数据 3 94 | | 数据 4 | 数据 5 | 数据 6 95 | |=== 96 | ``` 97 | 98 | ##### **(7) 块引用** 99 | 100 | ```adoc 101 | [quote, 作者] 102 | ____ 103 | 这是引用的内容。 104 | ____ 105 | ``` 106 | 107 | ##### **(8) 文档元信息** 108 | 109 | AsciiDoc 支持设置文档属性: 110 | 111 | ```adoc 112 | :author: 作者姓名 113 | :revnumber: 1.0 114 | :revdate: 2024-11-22 115 | ``` 116 | 117 | #### **3. 需注意特性** 118 | 119 | 1. **层级划分**:`AsciiDoc` 使用 `=` 符号划分标题层级,`Chunk` 划分可以以此为依据。 120 | 2. **跨平台转换**:可通过工具(如 `asciidoctor`)快速转换为 HTML、PDF 等多种格式。 121 | 3. **模块化**:`AsciiDoc` 支持 `include::` 指令引用外部文件,方便维护大规模文档,但是使用 `asciidoctor` 时需要注意缺失外部文件会导致解析失败。 122 | 123 | --- 124 | 125 | ## Multi RAG 升级报告 126 | 127 | ### 环境配置 128 | 129 | 使用环境如下: 130 | 131 | | 资源 | 版本 | 132 | | ------------ | --------------------------- | 133 | | linux 发行版 | Ubuntu 22.04.5 LTS | 134 | | linux 内核 | 5.15.0-105-generic | 135 | | python | 3.10.15 | 136 | | 显卡 | GeForce RTX 2080 Ti 22G * 2 | 137 | 138 | `ollama`、`conda` 的配置和预训练模型下载见 [11月工作笔记](https://github.com/rustsbi/Agent/blob/main/rfcs/November-notes-by-zzh.md)。 139 | 140 | ### 基础代码 141 | 142 | 钟同学工作产出的基础代码如下: 143 | 144 | ```python 145 | import hashlib 146 | import os 147 | import time 148 | 149 | import streamlit as st 150 | from langchain.callbacks.manager import CallbackManager 151 | from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler 152 | from langchain.chains import RetrievalQA 153 | from langchain.memory import ConversationBufferMemory 154 | from langchain.prompts import PromptTemplate 155 | from langchain.text_splitter import RecursiveCharacterTextSplitter 156 | from langchain_community.document_loaders import PyPDFLoader 157 | from langchain_community.embeddings.ollama import OllamaEmbeddings 158 | from langchain_community.llms import Ollama 159 | from langchain_community.vectorstores import FAISS # 改用 FAISS 160 | 161 | if not os.path.exists("files"): 162 | os.mkdir("files") 163 | 164 | if not os.path.exists("db"): 165 | os.mkdir("db") 166 | 167 | if "template" not in st.session_state: 168 | st.session_state.template = """You are a knowledgeable chatbot, here to help with questions of the user. Your tone should be professional and informative. 169 | 170 | Context: {context} 171 | History: {history} 172 | 173 | User: {question} 174 | Chatbot:""" 175 | if "prompt" not in st.session_state: 176 | st.session_state.prompt = PromptTemplate( 177 | input_variables=["history", "context", "question"], 178 | template=st.session_state.template, 179 | ) 180 | if "memory" not in st.session_state: 181 | st.session_state.memory = ConversationBufferMemory( 182 | memory_key="history", return_messages=True, input_key="question" 183 | ) 184 | 185 | # 生成模型 186 | if "llm" not in st.session_state: 187 | st.session_state.llm = Ollama( 188 | base_url="http://localhost:11434", 189 | model="gemma2:27b", 190 | verbose=True, 191 | callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]), 192 | ) 193 | 194 | # Initialize session state 195 | if "chat_history" not in st.session_state: 196 | st.session_state.chat_history = [] 197 | 198 | st.title("PDF Chatbot") 199 | 200 | # Upload a PDF file 201 | uploaded_file = st.file_uploader("Upload your PDF", type="pdf") 202 | 203 | for message in st.session_state.chat_history: 204 | with st.chat_message(message["role"]): 205 | st.markdown(message["message"]) 206 | 207 | if uploaded_file is not None: 208 | # 生成文件的唯一标识符 209 | file_hash = hashlib.md5(uploaded_file.read()).hexdigest() 210 | uploaded_file.seek(0) # 重置文件指针 211 | 212 | file_path = f"files/{file_hash}.pdf" 213 | db_path = "db" 214 | embedding_path = f"db/{file_hash}.pkl" 215 | 216 | embeddings = OllamaEmbeddings(base_url="http://localhost:11434", model="qwen:7b") 217 | 218 | # 检查向量数据库是否存在 219 | if os.path.exists(db_path) and os.path.exists(embedding_path): 220 | st.write("Loading existing vector database...") 221 | st.session_state.vectorstore = FAISS.load_local(db_path, embeddings) 222 | else: 223 | # 处理新上传的文件 224 | with st.status("Analyzing your document..."): 225 | bytes_data = uploaded_file.read() 226 | with open(file_path, "wb") as f: 227 | f.write(bytes_data) 228 | 229 | loader = PyPDFLoader(file_path) 230 | data = loader.load() 231 | 232 | # Initialize text splitter 233 | text_splitter = RecursiveCharacterTextSplitter( 234 | chunk_size=1500, chunk_overlap=200, length_function=len 235 | ) 236 | all_splits = text_splitter.split_documents(data) 237 | 238 | # Create and save the vector store 239 | st.session_state.vectorstore = FAISS.from_documents(all_splits, embeddings) 240 | # 保存向量数据库 241 | st.session_state.vectorstore.save_local(db_path) 242 | 243 | st.session_state.retriever = st.session_state.vectorstore.as_retriever() 244 | # Initialize the QA chain 245 | if "qa_chain" not in st.session_state: 246 | st.session_state.qa_chain = RetrievalQA.from_chain_type( 247 | llm=st.session_state.llm, 248 | chain_type="stuff", 249 | retriever=st.session_state.retriever, 250 | verbose=True, 251 | chain_type_kwargs={ 252 | "verbose": True, 253 | "prompt": st.session_state.prompt, 254 | "memory": st.session_state.memory, 255 | }, 256 | ) 257 | 258 | # Chat input 259 | if user_input := st.chat_input("You:", key="user_input"): 260 | user_message = {"role": "user", "message": user_input} 261 | st.session_state.chat_history.append(user_message) 262 | with st.chat_message("user"): 263 | st.markdown(user_input) 264 | with st.chat_message("assistant"): 265 | with st.spinner("Assistant is typing..."): 266 | response = st.session_state.qa_chain(user_input) 267 | message_placeholder = st.empty() 268 | full_response = "" 269 | for chunk in response["result"].split(): 270 | full_response += chunk + " " 271 | time.sleep(0.05) 272 | # Add a blinking cursor to simulate typing 273 | message_placeholder.markdown(full_response + "▌") 274 | message_placeholder.markdown(full_response) 275 | 276 | chatbot_message = {"role": "assistant", "message": response["result"]} 277 | st.session_state.chat_history.append(chatbot_message) 278 | 279 | else: 280 | st.write("Please upload a PDF file.") 281 | ``` 282 | 283 | 该代码完成了以下工作: 284 | 285 | 1. 前端界面的选择与部署; 286 | 2. PDF 文件的上传和解析; 287 | - 对于新上传的文件,使用 `PyPDFLoader` 解析 PDF 内容。 288 | - 通过 `RecursiveCharacterTextSplitter` 将长文本分块处理,便于后续的嵌入生成和检索操作。 289 | 3. 嵌入生成与向量数据库保存; 290 | - 利用 `OllamaEmbeddings` 生成文本块的嵌入向量。 291 | - 将嵌入向量存储到 FAISS 向量数据库。 292 | 4. 构建 RAG 流程,进行用户问答。 293 | 294 | ### 代码改进 295 | 296 | #### 1. asciidoctor 方案 297 | 298 | `asciidoctor` 是一个成熟的 AsciiDoc 解析器,支持全面的 AsciiDoc 语法,并且具有较强的性能和稳定性。查阅了网络上关于 AsciiDoc 的解析方案后,我尝试通过调用 asciidoctor 命令行接口(CLI)来解析 AsciiDoc 文件。 299 | 300 | 源代码如下: 301 | 302 | ```python 303 | import subprocess 304 | 305 | def parse_adoc_with_asciidoctor(file_path): 306 | try: 307 | # 使用 asciidoctor 将文档转换为 HTML 格式 308 | result = subprocess.run( 309 | ["asciidoctor", "--backend=html5", file_path], 310 | capture_output=True, 311 | text=True, 312 | check=True, 313 | ) 314 | return result.stdout # 返回解析后的 HTML 内容 315 | except subprocess.CalledProcessError as e: 316 | print(f"Error parsing AsciiDoc: {e.stderr}") 317 | return None 318 | ``` 319 | 320 | ##### **遇到的问题** 321 | 322 | ``` 323 | asciidoctor --backend=docbook ./riscv-machine.adoc 324 | asciidoctor: ERROR: riscv-machine.adoc: line 24: include file not found: 325 | ``` 326 | 327 | - 当文档中存在 `include::` 指令时,若引用文件缺失,asciidoctor 会直接报错并终止运行,缺乏优雅的降级处理机制。同时,观察来自 RISC-V Manual 的 adoc 文件,其中大量存在该指令,无法通过识别文档头部的方式去除。因此重新选择方案。 328 | 329 | #### 2. 直接解析方案 330 | 331 | 由第二部分,`AsciiDoc` 格式解析可知,adoc 文档的层次结构并不复杂,对于表格含量较少的文档,采用标题符号 ”=“ 号就可以较好地区分文档中的各个部分。因此,考虑自行撰写一个轻量级解析器,利用正则表达式解析 AsciiDoc 文档的核心结构。 332 | 333 | 源代码如下: 334 | 335 | ```python 336 | import re 337 | 338 | def parse_adoc(file_path): 339 | """Parse Asciidoc file and split content into chunks based on structure.""" 340 | try: 341 | with open(file_path, "r", encoding="utf-8") as f: 342 | lines = f.readlines() 343 | except FileNotFoundError: 344 | raise ValueError(f"File not found: {file_path}") 345 | 346 | chunks = [] 347 | current_chunk = [] 348 | current_title = None 349 | 350 | for line in lines: 351 | # Ignore include directives and blank lines 352 | if line.strip().startswith("include::") or not line.strip(): 353 | continue 354 | 355 | # Detect titles (e.g., = Title, == Subtitle) 356 | title_match = re.match(r"^(=+)\s+(.*)", line) 357 | if title_match: 358 | # Save the previous chunk if any 359 | if current_chunk: 360 | chunks.append({ 361 | "title": current_title or {"level": 0, "title": "No Title"}, 362 | "content": "\n".join(current_chunk) 363 | }) 364 | current_chunk = [] 365 | 366 | # Update current title 367 | level, title = title_match.groups() 368 | current_title = {"level": len(level), "title": title.strip()} 369 | else: 370 | # Append content to the current chunk 371 | current_chunk.append(line.strip()) 372 | 373 | # Save the last chunk 374 | if current_chunk: 375 | chunks.append({ 376 | "title": current_title or {"level": 0, "title": "No Title"}, 377 | "content": "\n".join(current_chunk) 378 | }) 379 | 380 | return chunks 381 | ``` 382 | 383 | 上述代码成功完成了 AsciiDoc 格式的文档解析功能。 384 | 385 | ### 运行测试 386 | 387 | 采用 [riscv-machine.adoc](https://github.com/riscv/riscv-isa-manual/blob/main/src/machine.adoc) 作为输入文档进行测试: 388 | 389 | ![AsciiDoc Test](./assets/AsciiDoc1.png) 390 | 391 | ![AsciiDoc Test 2](./assets/AsciiDoc2.png) 392 | 393 | ![AsciiDoc Test Backend](./assets/AsciiDoc3.png) 394 | 395 | 测试结果显示,传入的 adoc 文档成功被解析,并应用于 RAG 问答流程中,检索和回答结果较为准确,增强了系统处理特定技术文档的能力。未来将进一步完善 RAG 系统,增加更多对其他格式解析的支持。 -------------------------------------------------------------------------------- /rfcs/RAG_deployment_by_mxy.md: -------------------------------------------------------------------------------- 1 | ### 环境说明 2 | - docker版本 Docker version 26.1.4 3 | - conda版本 conda 23.1.0 4 | - 系统信息 CentOS Linux 7 5 | ### ollama 安装流程 6 | ``` bash 7 | #建议docker安装,比较方便,可以避免一些奇怪的错误 8 | #1、拉取镜像 9 | #docker hub 地址,选则自己需要的版本 10 | #https://hub.docker.com/r/ollama/ollama/tags 11 | #我安装的是最新版 12 | docker pull ollama/ollama:latest 13 | 14 | #2、启动docker 15 | docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama 16 | 17 | #3、运行模型 18 | #其中ollama run llama3为运行模型的命令,如果需要使用其他模型,可以参考官方提供模型版本https://github.com/ollama/ollama,将llama3替换掉即可 19 | docker exec -it ollama ollama run llama3 20 | ``` 21 | 22 | ### Langchain 安装 23 | ```bash 24 | #创建conda环境 25 | conda create --name rag python=3.10 26 | conda activate rag 27 | #安装依赖 28 | pip install langchain==0.3.7 29 | pip install streamlit==1.40.1 30 | pip install pypdf==5.1.0 31 | pip install langchain_community==0.3.7 32 | pip install chromadb==0.5.18 33 | ``` 34 | 35 | ### 核心代码 36 | ```python 37 | #核心代码主要包含两个模型,检索模型和生成模型,详见下文备注,模型地址为使用上面ollama启动的模型地址 38 | 39 | from langchain.chains import RetrievalQA 40 | from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler 41 | from langchain.callbacks.manager import CallbackManager 42 | from langchain_community.llms import Ollama 43 | from langchain_community.embeddings.ollama import OllamaEmbeddings 44 | from langchain_community.vectorstores import Chroma 45 | from langchain.text_splitter import RecursiveCharacterTextSplitter 46 | from langchain_community.document_loaders import PyPDFLoader 47 | from langchain.prompts import PromptTemplate 48 | from langchain.memory import ConversationBufferMemory 49 | import streamlit as st 50 | import os 51 | import time 52 | 53 | if not os.path.exists('files'): 54 | os.mkdir('files') 55 | 56 | if not os.path.exists('jj'): 57 | os.mkdir('jj') 58 | 59 | if 'template' not in st.session_state: 60 | st.session_state.template = """You are a knowledgeable chatbot, here to help with questions of the user. Your tone should be professional and informative. 61 | 62 | Context: {context} 63 | History: {history} 64 | 65 | User: {question} 66 | Chatbot:""" 67 | if 'prompt' not in st.session_state: 68 | st.session_state.prompt = PromptTemplate( 69 | input_variables=["history", "context", "question"], 70 | template=st.session_state.template, 71 | ) 72 | if 'memory' not in st.session_state: 73 | st.session_state.memory = ConversationBufferMemory( 74 | memory_key="history", 75 | return_messages=True, 76 | input_key="question") 77 | 78 | #这里为embedding模型 79 | if 'vectorstore' not in st.session_state: 80 | st.session_state.vectorstore = Chroma(persist_directory='jj', 81 | embedding_function=OllamaEmbeddings(base_url='http://localhost:11434', model="qwen:7b") 82 | #这里为reader 模型 ) 83 | if 'llm' not in st.session_state: 84 | st.session_state.llm = Ollama(base_url="http://localhost:11434", 85 | model="qwen:7b", 86 | verbose=True, 87 | callback_manager=CallbackManager( 88 | [StreamingStdOutCallbackHandler()]), 89 | ) 90 | 91 | # Initialize session state 92 | if 'chat_history' not in st.session_state: 93 | st.session_state.chat_history = [] 94 | 95 | st.title("PDF Chatbot") 96 | 97 | # Upload a PDF file 98 | uploaded_file = st.file_uploader("Upload your PDF", type='pdf') 99 | 100 | for message in st.session_state.chat_history: 101 | with st.chat_message(message["role"]): 102 | st.markdown(message["message"]) 103 | 104 | if uploaded_file is not None: 105 | if not os.path.isfile("files/"+uploaded_file.name+".pdf"): 106 | with st.status("Analyzing your document..."): 107 | bytes_data = uploaded_file.read() 108 | f = open("files/"+uploaded_file.name+".pdf", "wb") 109 | f.write(bytes_data) 110 | f.close() 111 | loader = PyPDFLoader("files/"+uploaded_file.name+".pdf") 112 | data = loader.load() 113 | 114 | # Initialize text splitter 115 | text_splitter = RecursiveCharacterTextSplitter( 116 | chunk_size=1500, 117 | chunk_overlap=200, 118 | length_function=len 119 | ) 120 | all_splits = text_splitter.split_documents(data) 121 | 122 | # Create and persist the vector store 123 | st.session_state.vectorstore = Chroma.from_documents( 124 | documents=all_splits, 125 | embedding=OllamaEmbeddings(model="qwen:7b") 126 | ) 127 | st.session_state.vectorstore.persist() 128 | 129 | st.session_state.retriever = st.session_state.vectorstore.as_retriever() 130 | # Initialize the QA chain 131 | if 'qa_chain' not in st.session_state: 132 | st.session_state.qa_chain = RetrievalQA.from_chain_type( 133 | llm=st.session_state.llm, 134 | chain_type='stuff', 135 | retriever=st.session_state.retriever, 136 | verbose=True, 137 | chain_type_kwargs={ 138 | "verbose": True, 139 | "prompt": st.session_state.prompt, 140 | "memory": st.session_state.memory, 141 | } 142 | ) 143 | 144 | # Chat input 145 | if user_input := st.chat_input("You:", key="user_input"): 146 | user_message = {"role": "user", "message": user_input} 147 | st.session_state.chat_history.append(user_message) 148 | with st.chat_message("user"): 149 | st.markdown(user_input) 150 | with st.chat_message("assistant"): 151 | with st.spinner("Assistant is typing..."): 152 | response = st.session_state.qa_chain(user_input) 153 | message_placeholder = st.empty() 154 | full_response = "" 155 | for chunk in response['result'].split(): 156 | full_response += chunk + " " 157 | time.sleep(0.05) 158 | # Add a blinking cursor to simulate typing 159 | message_placeholder.markdown(full_response + "▌") 160 | message_placeholder.markdown(full_response) 161 | 162 | chatbot_message = {"role": "assistant", "message": response['result']} 163 | st.session_state.chat_history.append(chatbot_message) 164 | 165 | 166 | else: 167 | st.write("Please upload a PDF file.") 168 | 169 | ``` 170 | 171 | ```bash 172 | #执行这个命令运行即可启动服务 173 | streamlit run app.py 174 | #接着在浏览器访问服务地址 175 | http://localhost:8501 176 | #上传PDF即可进行问答对话 177 | ``` 178 | 179 | ### 效果示例 180 | ![RAG DMEO](assets/RAG_Demo.jpg) 181 | -------------------------------------------------------------------------------- /rfcs/RustSBI Agent项目计划_20241014204801.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/RustSBI Agent项目计划_20241014204801.pdf -------------------------------------------------------------------------------- /rfcs/assets/AsciiDoc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/AsciiDoc1.png -------------------------------------------------------------------------------- /rfcs/assets/AsciiDoc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/AsciiDoc2.png -------------------------------------------------------------------------------- /rfcs/assets/AsciiDoc3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/AsciiDoc3.png -------------------------------------------------------------------------------- /rfcs/assets/RAG-Conclution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/RAG-Conclution.png -------------------------------------------------------------------------------- /rfcs/assets/RAG-evaluate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/RAG-evaluate.png -------------------------------------------------------------------------------- /rfcs/assets/RAG-systems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/RAG-systems.png -------------------------------------------------------------------------------- /rfcs/assets/RAGFlow-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/RAGFlow-arch.png -------------------------------------------------------------------------------- /rfcs/assets/RAG_Demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/RAG_Demo.jpg -------------------------------------------------------------------------------- /rfcs/assets/comparison_of_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/comparison_of_output.png -------------------------------------------------------------------------------- /rfcs/assets/loading_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/loading_success.png -------------------------------------------------------------------------------- /rfcs/assets/old_rag_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/old_rag_output.png -------------------------------------------------------------------------------- /rfcs/assets/qanything_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/qanything_arch.png -------------------------------------------------------------------------------- /rfcs/assets/retrieval_module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/retrieval_module.png -------------------------------------------------------------------------------- /rfcs/assets/search_docs_false.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/search_docs_false.png -------------------------------------------------------------------------------- /rfcs/assets/search_docs_true.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/search_docs_true.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-December/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-December/1.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-December/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-December/2.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/1.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/10.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/2.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/3.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/4.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/5.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/6.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/7.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/8.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-November/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-November/9.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/1.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/2.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/3.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/4.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/5.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/6.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/7.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/8.png -------------------------------------------------------------------------------- /rfcs/assets/zzh-October/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/assets/zzh-October/9.png -------------------------------------------------------------------------------- /rfcs/meeting_records_2025-01-09.md: -------------------------------------------------------------------------------- 1 | ## 会议主题: 2 | 检索模块的优化讨论 3 | ## 会议时间: 4 | 2025年1月9日 5 | ## 会议时长: 6 | 1.5h 7 | ## 会议方式: 8 | 线上腾讯会议 9 | ## 参会人员: 10 | [洛佳](https://github.com/luojia65) 11 | [马兴宇](https://github.com/xingyuma618) 12 | [张子涵](https://github.com/ArchLance) 13 | [任潇](https://github.com/wyywwi) 14 | [邝嘉诺](https://github.com/gitveg) 15 | ## 会议内容 16 | - 讨论现有RAG架构检索模块设计存在的缺陷,通过市场常见RAG系统的功能分析,发现RAG系统需要文档管理功能,包含文档的增删改查,因此需要数据库字段设计。 17 | - 结论:缺少文档存储于文档管理模块 18 | 19 | - 拟定任务优先级,考虑从文档存储和管理模块开始构建, 20 | - 结论:文档存储位置、切片ID、切片起始位置等数据库关键字段设计 21 | - 结论: 优先搭建mysql数据库服务 22 | -------------------------------------------------------------------------------- /rfcs/meeting_records_2025-02-17.md: -------------------------------------------------------------------------------- 1 | ## 会议主题: 2 | 问答模块优化讨论与bug调试 3 | ## 会议时间: 4 | 2025年2月17日 5 | ## 会议时长: 6 | 1h 7 | ## 会议方式: 8 | 线上腾讯会议 9 | ## 参会人员: 10 | [洛佳](https://github.com/luojia65) 11 | [马兴宇](https://github.com/xingyuma618) 12 | [张子涵](https://github.com/ArchLance) 13 | [任潇](https://github.com/wyywwi) 14 | [邝嘉诺](https://github.com/gitveg) 15 | ## 会议内容 16 | 讨论当前RAG系统开发检索模块待处理工作与优化点 17 | - 待进行工作: 问答函数调用调试 18 | 19 | - 优化点1: bug调试 20 | 在审查 logs 后,发现 llm-client 跑不通的原因在于两个方面: 21 | - configs.py 中的 `API-BASE` 和 `API-KEY` 写反了)) 22 | - llm-client 中的 `llm = OpenAILLM(DEFAULT_MODEL_PATH, 8000, DEFAULT_API_BASE, DEFAULT_API_KEY, DEFAULT_API_CONTEXT_LENGTH, 5, 0.5)` 中的 `top-k` 的范围必须在0~1之间,改成了0.5。 23 | - 优化点2:讨论增加测评指标 24 | - 检索评测指标P,R,F1 25 | - 排序评测指标,NDCG, ROC, AUC 26 | 27 | 明确下一步工作,开始调用LLM服务进行生成模块开发。 -------------------------------------------------------------------------------- /rfcs/meeting_records_2025-03-15.md: -------------------------------------------------------------------------------- 1 | ## 会议主题: 2 | 打通RAG系统整体流程 3 | ## 会议时间: 4 | 2025年03月15日 5 | ## 会议时长: 6 | 1h 7 | ## 会议方式: 8 | 线上腾讯会议 9 | ## 参会人员: 10 | [洛佳](https://github.com/luojia65) 11 | [马兴宇](https://github.com/xingyuma618) 12 | [张子涵](https://github.com/ArchLance) 13 | [任潇](https://github.com/wyywwi) 14 | [邝嘉诺](https://github.com/gitveg) 15 | ## 会议内容 16 | 1. 张子涵介绍QAhandler代码 17 | 2. 发现问题存在输出的内容是英文不符合预期,中英文混杂,猜测问题可能是由于prompt为英文版的prompt,需要切换回中文版prompt尝试。另外需要尝试不用多模态的模型 18 | 3. 确定本月工作方向, 19 | - 测试整体流程,排查总结现有的问题,形成测试报告 20 | - 开始撰写项目文档 21 | -------------------------------------------------------------------------------- /rfcs/meeting_records_2025-04-12.md: -------------------------------------------------------------------------------- 1 | ## 会议主题: 2 | 测试报告总结汇报会议 3 | ## 会议时间: 4 | 2025年04月12日 5 | ## 会议时长: 6 | 1h 7 | ## 会议方式: 8 | 线上腾讯会议 9 | ## 参会人员: 10 | [洛佳](https://github.com/luojia65) 11 | [马兴宇](https://github.com/xingyuma618) 12 | [张子涵](https://github.com/ArchLance) 13 | [任潇](https://github.com/wyywwi) 14 | [邝嘉诺](https://github.com/gitveg) 15 | ## 会议内容 16 | 1. 张子涵介绍检索评估结果。 17 | - 存在的问题: 18 | - 目前是基于开源数据集评估检索指标, 缺少进一步的case分析, 缺少业务数据的检索测试 19 | - 下一步工作建议: 20 | - 针对检索出现的bad case进行分析,提出优化改进方案 21 | - 针对业务数据,手工或者合成少量业务问答数据,测试检索效果 22 | 2. 邝嘉诺介绍QA测试报告 23 | - 存在的问题: 24 | - lmdeploy 部署模型未跑通 25 | - 模型输出结果中英文混杂 26 | - 下一步工作建议 27 | - 检查lmdeploy部署时模型路径问题、以及发送request请求时,model_path参数与模型部署路径是否完全一致。可进一步参考[lmdeploy开发文档](https://lmdeploy.readthedocs.io/en/latest/) 28 | - 尝试将prompt中的所有英文数据换为中文的表述方式 29 | 3. 任潇介绍QA测试报告 30 | - 存在的问题 31 | - 模型输出结果中中英文混杂 32 | - 模型内容理解能力差,不能很好理解检索出的文档中的关键内容 33 | - 下一步工作建议 34 | - 尝试将prompt中的所有英文数据换为中文的表述方式 35 | - 切换模型Qwen2.5-7b-Instruct为Qwen2.5-14B-Instruct,或者测试QwQ2.5-32B。在部署时,如果显存资源有限部署失败,可以尝试用VLLM部署,设置内存利用率。或者使用量化版本的14B或者32B模型 36 | -------------------------------------------------------------------------------- /rfcs/meeting_records_2025-05-16.md: -------------------------------------------------------------------------------- 1 | ## 会议主题: 2 | RAG系统优化和服务重新部署 3 | ## 会议时间: 4 | 2025年05月16日 5 | ## 会议时长: 6 | 1h 7 | ## 会议方式: 8 | 线上腾讯会议 9 | ## 参会人员: 10 | [洛佳](https://github.com/luojia65) 11 | [马兴宇](https://github.com/xingyuma618) 12 | [任潇](https://github.com/wyywwi) 13 | [邝嘉诺](https://github.com/gitveg) 14 | ## 会议内容 15 | 1. 邝嘉诺介绍4月LLM部署和测试报告 16 | - 主要内容 17 | - 32B的LLM模型成功部署,RAG系统性能得到提升 18 | - model路径更新 19 | - 下一步工作 20 | - 将sanic、MySQL等Agent必要服务重新部署到服务器上 21 | 2. 任潇介绍QA测试报告 22 | - 主要内容 23 | - RAG系统对于pdf的解析存在问题 24 | - 下一步工作 25 | - 找出bug并优化RAG系统对于文件的解析能力 26 | -------------------------------------------------------------------------------- /rfcs/notes/kjn-notes-2024.11.md: -------------------------------------------------------------------------------- 1 | # 11月工作记录 2 | 3 | 11月的工作主要分为两部分:开源大模型调研跟进工作和RAG程序功能的完善。 4 | 5 | ## 开源大模型调研跟进 6 | 7 | 对10月份的开源大模型调研进行了完善和补充,主要有下面几点: 8 | 9 | 1. 完善了文档排版 10 | 2. 增加了三个开源大模型Mistral AI、Baichuan、混元的调研 11 | 3. 添加了Langgpt的大模型全景图链接,一个很全面的大模型调研工作,值得借鉴。 12 | 13 | ## RAG程序功能的完善 14 | 15 | 1. 首先阅读了rx的10月份的work report以及,详细了解了RAG的原理和优化种类,以及后续的工作方向。 16 | 2. 然后在zzh和rx的RAG代码基础上,修正了之前的**向量数据库存取逻辑**,无须重复读取文件,显著加快了问答的速度。 17 | ``` 18 | 之前的逻辑是不同的文件解析后,生成的pkl文件以文件名对应的哈希值命名,存入db文件夹(即向量数据库)。 19 | 但是st.session_state.vectorstore.save_local(db_path)函数会将解析后的内容都存入同一个index.pkl文件,导致先前解析的文件内容会被覆盖。 20 | 因此将存取逻辑改为,创建不同的文件夹,每个文件的解析数据存入以文件名哈希值命名的文件夹中,如此可以实现独立地读取管理逻辑。 21 | ``` 22 | 3. 同时解决了RAG助手的UI界面的输出文字总是出现无法及时markdown渲染的情况。 23 | 24 | #### 更新后的代码 25 | 26 | ```python 27 | # edited by kjn 11.24 28 | import hashlib 29 | import os 30 | import subprocess 31 | import time 32 | import threading 33 | from markdown import markdown 34 | from pylatexenc.latex2text import LatexNodes2Text 35 | 36 | import streamlit as st 37 | from langchain.callbacks.manager import CallbackManager 38 | from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler 39 | from langchain.chains import RetrievalQA 40 | from langchain.memory import ConversationBufferMemory 41 | from langchain.prompts import PromptTemplate 42 | from langchain.text_splitter import RecursiveCharacterTextSplitter 43 | from langchain_community.document_loaders import PyPDFLoader 44 | from langchain_community.embeddings.ollama import OllamaEmbeddings 45 | from langchain_community.llms import Ollama 46 | from langchain_community.vectorstores import FAISS # 使用 FAISS 47 | 48 | import re 49 | 50 | def parse_adoc(file_path): 51 | """Parse Asciidoc file and split content into chunks based on structure.""" 52 | try: 53 | with open(file_path, "r", encoding="utf-8") as f: 54 | lines = f.readlines() 55 | except FileNotFoundError: 56 | raise ValueError(f"File not found: {file_path}") 57 | 58 | chunks = [] 59 | current_chunk = [] 60 | current_title = None 61 | 62 | for line in lines: 63 | # Ignore include directives and blank lines 64 | if line.strip().startswith("include::") or not line.strip(): 65 | continue 66 | 67 | # Detect titles (e.g., = Title, == Subtitle) 68 | title_match = re.match(r"^(=+)\s+(.*)", line) 69 | if title_match: 70 | # Save the previous chunk if any 71 | if current_chunk: 72 | chunks.append({ 73 | "title": current_title or {"level": 0, "title": "No Title"}, 74 | "content": "\n".join(current_chunk) 75 | }) 76 | current_chunk = [] 77 | 78 | # Update current title 79 | level, title = title_match.groups() 80 | current_title = {"level": len(level), "title": title.strip()} 81 | else: 82 | # Append content to the current chunk 83 | current_chunk.append(line.strip()) 84 | 85 | # Save the last chunk 86 | if current_chunk: 87 | chunks.append({ 88 | "title": current_title or {"level": 0, "title": "No Title"}, 89 | "content": "\n".join(current_chunk) 90 | }) 91 | 92 | return chunks 93 | 94 | 95 | if not os.path.exists("files"): 96 | os.mkdir("files") 97 | 98 | if not os.path.exists("db"): 99 | os.mkdir("db") 100 | 101 | if "template" not in st.session_state: 102 | st.session_state.template = """You are a knowledgeable chatbot, here to help with questions of the user. Your tone should be professional and informative. 103 | 104 | Context: {context} 105 | History: {history} 106 | 107 | User: {question} 108 | Chatbot:""" 109 | if "prompt" not in st.session_state: 110 | st.session_state.prompt = PromptTemplate( 111 | input_variables=["history", "context", "question"], 112 | template=st.session_state.template, 113 | ) 114 | if "memory" not in st.session_state: 115 | st.session_state.memory = ConversationBufferMemory( 116 | memory_key="history", return_messages=True, input_key="question" 117 | ) 118 | 119 | # 生成模型 120 | if "llm" not in st.session_state: 121 | st.session_state.llm = Ollama( 122 | base_url="http://localhost:11434", 123 | model="gemma2:27b", 124 | verbose=True, 125 | callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]), 126 | ) 127 | 128 | # Initialize session state 129 | if "chat_history" not in st.session_state: 130 | st.session_state.chat_history = [] 131 | 132 | st.title("RAG Chatbot") 133 | 134 | # Upload a file 135 | uploaded_file = st.file_uploader("Upload your file (PDF, Markdown, LaTeX, Asciidoc)", type=["pdf", "md", "tex", "adoc"]) 136 | 137 | 138 | def process_markdown(file_path): 139 | """Process Markdown file into plain text.""" 140 | with open(file_path, "r", encoding="utf-8") as f: 141 | content = f.read() 142 | return markdown(content) 143 | 144 | 145 | def process_latex(file_path): 146 | """Process LaTeX file into plain text.""" 147 | with open(file_path, "r", encoding="utf-8") as f: 148 | content = f.read() 149 | return LatexNodes2Text().latex_to_text(content) 150 | 151 | 152 | def process_asciidoc(file_path): 153 | """Extract content from Asciidoc file and split by structure.""" 154 | try: 155 | chunks = parse_adoc(file_path) 156 | print("Parsed chunks:", chunks) 157 | # Combine chunks for embedding (preserve structure with a fallback for missing titles) 158 | combined_chunks = [ 159 | f"{chunk['title'].get('title', 'No Title')}\n\n{chunk['content']}" 160 | for chunk in chunks 161 | ] 162 | return combined_chunks 163 | except Exception as e: 164 | st.error(f"Failed to process Asciidoc file: {e}") 165 | return [] 166 | 167 | 168 | for message in st.session_state.chat_history: 169 | with st.chat_message(message["role"]): 170 | st.markdown(message["message"]) 171 | 172 | if uploaded_file is not None: 173 | # 生成文件的唯一标识符 174 | file_hash = hashlib.md5(uploaded_file.read()).hexdigest() 175 | uploaded_file.seek(0) # 重置文件指针 176 | 177 | file_path = f"files/{file_hash}.{uploaded_file.name.split('.')[-1]}" 178 | db_root = "db" 179 | db_path = os.path.join(db_root, file_hash) # 针对每个文件的独立子目录 180 | embedding_path = os.path.join(db_path, 'index.pkl') 181 | os.makedirs(db_path, exist_ok=True) 182 | embeddings = OllamaEmbeddings(base_url="http://localhost:11434", model="qwen:7b") 183 | 184 | # 检查向量数据库是否存在 185 | if os.path.exists(db_path) and os.path.exists(embedding_path): 186 | st.write("Loading existing vector database...") 187 | st.session_state.vectorstore = FAISS.load_local(db_path, embeddings,allow_dangerous_deserialization=True) 188 | st.write("Done!") 189 | else: 190 | # 处理新上传的文件 191 | with st.status("Analyzing your document..."): 192 | bytes_data = uploaded_file.read() 193 | with open(file_path, "wb") as f: 194 | f.write(bytes_data) 195 | 196 | print(uploaded_file.type) 197 | 198 | # 获取文件扩展名 199 | file_extension = os.path.splitext(uploaded_file.name)[-1].lower() 200 | 201 | # 根据文件扩展名解析文件内容 202 | if file_extension == ".pdf": 203 | loader = PyPDFLoader(file_path) 204 | data = loader.load() 205 | text_data = "\n".join([doc.page_content for doc in data]) 206 | elif file_extension == ".md": 207 | text_data = process_markdown(file_path) 208 | elif file_extension == ".tex": 209 | text_data = process_latex(file_path) 210 | elif file_extension == ".adoc": 211 | text_data = process_asciidoc(file_path) 212 | else: 213 | st.error("Unsupported file format.") 214 | st.stop() 215 | 216 | # 如果返回值是列表,则拼接为单个字符串 217 | if isinstance(text_data, list): 218 | text_data = "\n".join(text_data) 219 | 220 | # 检查是否成功解析文本 221 | if not text_data.strip(): 222 | st.error("No text could be extracted from the file. Please check the content.") 223 | st.stop() 224 | 225 | # Initialize text splitter 226 | text_splitter = RecursiveCharacterTextSplitter( 227 | chunk_size=1500, chunk_overlap=200, length_function=len 228 | ) 229 | all_splits = text_splitter.split_text(text_data) 230 | 231 | # 检查分块是否成功 232 | if not all_splits: 233 | st.error("Failed to split the text into chunks. Please check the file content.") 234 | st.stop() 235 | 236 | # Create and save the vector store 237 | st.session_state.vectorstore = FAISS.from_texts(all_splits, embeddings) 238 | # 保存向量数据库 239 | st.session_state.vectorstore.save_local(db_path) 240 | 241 | st.session_state.retriever = st.session_state.vectorstore.as_retriever() 242 | # Initialize the QA chain 243 | if "qa_chain" not in st.session_state: 244 | st.session_state.qa_chain = RetrievalQA.from_chain_type( 245 | llm=st.session_state.llm, 246 | chain_type="stuff", 247 | retriever=st.session_state.retriever, 248 | verbose=True, 249 | chain_type_kwargs={ 250 | "verbose": True, 251 | "prompt": st.session_state.prompt, 252 | "memory": st.session_state.memory, 253 | }, 254 | ) 255 | 256 | # Chat input 257 | if user_input := st.chat_input("You:", key="user_input"): 258 | user_message = {"role": "user", "message": user_input} 259 | st.session_state.chat_history.append(user_message) 260 | with st.chat_message("user"): 261 | st.markdown(user_input) 262 | with st.chat_message("assistant"): 263 | with st.spinner("Assistant is typing..."): 264 | response = st.session_state.qa_chain(user_input) 265 | 266 | # 创建一个占位符,用于显示模拟输入效果 267 | message_placeholder = st.empty() 268 | full_response = "" 269 | for chunk in response["result"].split(): 270 | full_response += chunk + " " 271 | time.sleep(0.05) 272 | # Add a blinking cursor to simulate typing 273 | message_placeholder.markdown(full_response + "▌") 274 | 275 | # 清除之前的模拟输入效果,并显示最终的格式化回答 276 | message_placeholder.empty() 277 | message_placeholder.markdown(response["result"]) 278 | 279 | chatbot_message = {"role": "assistant", "message": response["result"]} 280 | st.session_state.chat_history.append(chatbot_message) 281 | 282 | else: 283 | st.write("Please upload a file.") 284 | ``` 285 | 286 | #### 测试效果 287 | 288 | 成功加载已有的解析数据: 289 | 290 | ![load success](/rfcs/assets/loading_success.png) 291 | 292 | 旧的RAG助手输出,文字格式渲染有问题: 293 | 294 | ![old output](/rfcs/assets/old_rag_output.png) 295 | 296 | 旧的输出和新的输出进行对比: 297 | 298 | ![comparison](/rfcs/assets/comparison_of_output.png) 299 | 300 | #### 未来工作 301 | 302 | 可以对RAG助手的用户界面进行进一步完善,增添功能如自定义context、语言控制、历史对话保留等,同时也可以进一步完善代码加快问答速度。 -------------------------------------------------------------------------------- /rfcs/notes/kjn-notes-2024.12.md: -------------------------------------------------------------------------------- 1 | ## 12月工作记录 2 | 3 | 12月工作主要集中在编写文档切片存向量数据库代码以及rst文档的解析代码。 4 | 5 | ### rst文档的解析代码 6 | 7 | 对于rst文档的解析切片,我尝试了两种方案。第一种是受zzh同学解析adoc文档代码的启发,使用docutils库将rst文件转成html文件,然后使用beautifulsoup将html文件解析成文本。源代码如下: 8 | 9 | ```python 10 | with open(file_path, 'r', encoding='utf-8') as file: 11 | content = file.read() 12 | document = publish_parts(content, writer_name='html') 13 | 14 | soup = BeautifulSoup(document['html_body'], 'html.parser') 15 | text = soup.get_text() 16 | return text 17 | ``` 18 | 19 | 第二种方案是使用re正则表达式解析rst文档,将rst文档中的标题和内容分开,同时进行分级,然后存入字典中,最后转成字符串形式。源代码如下: 20 | 21 | ```python 22 | def parse_rst(file_path): 23 | chunks = [] 24 | try: 25 | with open(file_path, 'r', encoding='utf-8') as file: 26 | content = file.read() 27 | 28 | current_lines = [] 29 | current_title = "" 30 | 31 | for content_line in content.split('\n'): 32 | # 去除回车行 33 | if content_line.strip() == '': 34 | continue 35 | title_match = re.search(r'^={3,}$', content_line) 36 | subtitle_match = re.search(r'^-{3,}$', content_line) 37 | if title_match or subtitle_match: 38 | if title_match: 39 | level = 1 40 | else: 41 | level = 2 42 | if len(current_lines) > 0: 43 | new_title = current_lines[-1] 44 | current_lines.pop() 45 | if len(current_lines) > 0: 46 | new_string = "\n".join(current_lines) 47 | chunks.append({'title': current_title,'level': level,'content': new_string}) 48 | current_title = new_title 49 | current_lines = [] 50 | else: 51 | current_lines.append(content_line) 52 | 53 | if len(current_lines) > 0: 54 | new_string = "\n".join(current_lines) 55 | chunks.append({'title': current_title,'level': 1,'content': new_string}) 56 | 57 | text_data = [f"{item['title']} (Level {item['level']}): {item['content']}" for item in chunks if 'title' in item and 'content' in item and 'level' in item] 58 | return text_data 59 | 60 | 61 | except Exception as e: 62 | print(f"Error reading .rst file: {e}") 63 | return "" 64 | ``` 65 | 66 | ### 文档切片存向量数据库代码 67 | 68 | 我创建了files2db.py,主体内容来自原来的demo程序,程序功能为将files文件夹中所有的文档进行切片解析,存入db文件夹中。files2db.py的代码较长,这里不再展示。 69 | 70 | ### 数据库清除代码 71 | 72 | 我创建了clear_db_dirs.py,程序功能为清除db文件夹下的所有文档数据库文件夹,便于管理数据库。程序源码如下: 73 | 74 | ```python 75 | import os 76 | import shutil 77 | 78 | # 指定要清理的目录 79 | db_directory = 'db' 80 | 81 | # 检查db_directory是否存在 82 | if not os.path.exists(db_directory): 83 | print(f"The directory {db_directory} does not exist.") 84 | else: 85 | # 遍历db_directory中的所有子目录 86 | for folder in os.listdir(db_directory): 87 | folder_path = os.path.join(db_directory, folder) 88 | 89 | # 确保是目录而不是文件 90 | if os.path.isdir(folder_path): 91 | # 检查index.faiss和index.pkl是否存在于子目录中 92 | if os.path.isfile(os.path.join(folder_path, 'index.faiss')) and os.path.isfile(os.path.join(folder_path, 'index.pkl')): 93 | # 删除子目录及其内容 94 | shutil.rmtree(folder_path) 95 | print(f"Deleted folder: {folder_path}") 96 | else: 97 | print(f"Folder {folder_path} does not contain both index.faiss and index.pkl.") 98 | ``` 99 | 100 | ### 未来工作 101 | 102 | - rst文档解析函数还有进一步优化的空间,主要在对于文档的解析可以更加细粒度。 103 | 104 | - demo程序可以更改逻辑,不需要强制用户去上传文件,而是调用files2db程序读取数据库的文档。 -------------------------------------------------------------------------------- /rfcs/notes/kjn-notes-2025.1.md: -------------------------------------------------------------------------------- 1 | # 1月工作记录 2 | 3 | 本月主要工作集中在编写、测试向量数据库的检索逻辑代码 4 | 5 | ## milvus检索代码 6 | 7 | `milvus` 向量数据库提供了两种检索的方法,分别是 query 和 search 。 8 | 9 | 其中,search 方法主要用于执行近似最近邻搜索(Approximate Nearest Neighbors, ANN),即根据给定的查询向量找到与之最相似的向量。它的核心功能是基于**向量相似性**进行检索。 10 | 11 | query 方法用于执行更广泛的基于条件的查询,主要用于基于条件的过滤,根据指定的条件表达式检索数据 12 | 13 | 在代码编写上选择使用更适合 RAG 系统的 search 方法。 14 | 15 | 检索逻辑代码放在了 milvus_client.py 下: 16 | 17 | ```python 18 | @get_time 19 | def search_docs(self, query_embedding: List[float] = None, filter_expr: str = None, doc_limit: int = 10): 20 | """ 21 | 从 Milvus 集合中检索文档。 22 | 23 | Args: 24 | query_embedding (List[float]): 查询向量,用于基于向量相似性检索。 25 | filter_expr (str): 过滤条件表达式,用于基于字段值的过滤。如"user_id == 'abc1234'" 26 | limit (int): 返回的文档数量上限,默认为 10。 27 | 28 | Returns: 29 | List[dict]: 检索到的文档列表,每个文档是一个字典,包含字段值和向量。 30 | """ 31 | try: 32 | if not self.sess: 33 | raise MilvusFailed("Milvus collection is not loaded. Call load_collection_() first.") 34 | 35 | # 构造查询参数 36 | search_params = { 37 | "metric_type": self.search_params["metric_type"], 38 | "params": self.search_params["params"] 39 | } 40 | 41 | # 构造查询表达式 42 | expr = "" 43 | if filter_expr: 44 | expr = filter_expr 45 | 46 | # 构造检索参数 47 | search_params.update({ 48 | "data": [query_embedding] if query_embedding else None, 49 | "anns_field": "embedding", # 指定集合中存储向量的字段名称。Milvus 会在该字段上进行向量相似性检索。 50 | "param": {"metric_type": "L2", "params": {"nprobe": 128}}, # 检索的精度和性能 51 | "limit": doc_limit, # 指定返回的最相似文档的数量上限 52 | "expr": expr, 53 | "output_fields": self.output_fields 54 | }) 55 | 56 | # 执行检索 57 | results = self.sess.search(**search_params) 58 | 59 | # 处理检索结果 60 | retrieved_docs = [] 61 | for hits in results: 62 | for hit in hits: 63 | doc = { 64 | # "id": hit.id, 65 | # "distance": hit.distance, 66 | "user_id": hit.entity.get("user_id"), 67 | "kb_id": hit.entity.get("kb_id"), 68 | "file_id": hit.entity.get("file_id"), 69 | "headers": json.loads(hit.entity.get("headers")), 70 | "doc_id": hit.entity.get("doc_id"), 71 | "content": hit.entity.get("content"), 72 | "embedding": hit.entity.get("embedding") 73 | } 74 | retrieved_docs.append(doc) 75 | 76 | return retrieved_docs 77 | 78 | except Exception as e: 79 | print(f'[{cur_func_name()}] [search_docs] Failed to search documents: {traceback.format_exc()}') 80 | raise MilvusFailed(f"Failed to search documents: {str(e)}") 81 | ``` 82 | 83 | ## 测试milvus检索逻辑 84 | 85 | 利用已有的 embedding 文件夹下的 embedding_client.py(原名为 client.py )中的embedding处理代码,同时编写了 embed_user_input 方便测试。 86 | 87 | 同时在 milvus_client.py 的 main 函数中调用 search_docs 函数进行测试,测试结果如下。 88 | 89 | 不设置过滤条件正常检索: 90 | 91 | ![search_true](/rfcs/assets/search_docs_true.png) 92 | 93 | 设置过滤条件,检索结果为空: 94 | 95 | ![search_false](/rfcs/assets/search_docs_false.png) 96 | 97 | 98 | ## 未来工作 99 | 100 | 后续继续实现 server 与 client 的交互处理,方便更好地测试用户的输入经过 embedding 后到 milvus 中进行检索的过程。 101 | 102 | RAG 系统的 UI 界面逐步完善。 -------------------------------------------------------------------------------- /rfcs/notes/kjn-notes-2025.2.md: -------------------------------------------------------------------------------- 1 | # 2月工作记录 2 | 3 | 本月工作集中在跑通 llm-client 和增加对于更多格式的文件解析支持。 4 | 5 | ## 跑通llm-client 6 | 7 | 在审查 logs 后,发现 llm-client 跑不通的原因在于两个方面: 8 | 9 | 1. configs.py 中的 `API-BASE` 和 `API-KEY` 写反了)) 10 | 2. llm-client 中的 `llm = OpenAILLM(DEFAULT_MODEL_PATH, 8000, DEFAULT_API_BASE, DEFAULT_API_KEY, DEFAULT_API_CONTEXT_LENGTH, 5, 0.5)` 中的 `top-k` 的范围必须在0~1之间,我改成了0.5。 11 | 12 | 修改以上两个小错误后即可成功跑通 llm-client。 13 | 14 | ## 增加对于更多格式的文件解析支持 15 | 16 | 文件解析的处理集中在 `src/core/file_handler/file_handler.py` 里,我们目前使用的是 langchain 框架,langchain-community中的 [document_loaders](https://python.langchain.com/api_reference/community/document_loaders.html) 中包含多个文件解析器,功能十分强大。 17 | 18 | 我选取了其中的 TextLoader, UnstructuredMarkdownLoader, Docx2txtLoader, UnstructuredPowerPointLoader, UnstructuredXMLLoader,PyPDFLoader, UnstructuredImageLoader, UnstructuredHTMLLoader, UnstructuredURLLoader 九个常用文件格式的解析器,并对其中的md、ppt、pdf、image解析器进行了测试,均能成功解析。 19 | 20 | 这些 Loader 的用法大同小异,都是直接 `loader(file_path)`,然后再 `docs = loader.load()`即可,其中不同的loader可能会提供参数方便设置。例如下面的代码例子: 21 | 22 | ```python 23 | def load_md(file_path): 24 | try: 25 | loader = UnstructuredMarkdownLoader(file_path, mode="elements") 26 | docs = loader.load() 27 | insert_logger.info(f"UnstructuredMarkdownLoader success: {file_path}") 28 | return docs 29 | except Exception: 30 | insert_logger.error(f"UnstructuredMarkdownLoader error: {file_path}, {traceback.format_exc()}") 31 | return [] 32 | ``` 33 | 34 | 35 | 在第一次使用某个 loader 时,可能需要根据报错安装相印的依赖,如在使用 `UnstructuredImageLoader` 时,需要安装 `tesseract-ocr` 等依赖。 36 | 37 | 在阅读 langchain-community 中的文档时,我还发现它提供一个通用的文档解析器 `UnstructuredFileLoader`,可以解析多种文件格式,可以通过本地分区或远程 API 调用来加载文件,同时提供了更通用的配置选项,例如可以通过 `chunking_strategy` 和 `max_characters` 参数控制分块行为,需要安装 `unstructured` 包。 38 | 39 | 在适用场景上的区别: 40 | - UnstructuredLoader: 41 | 适用于需要处理多种文件格式的场景,尤其是当需要统一处理不同格式的文件时。 42 | 适合需要灵活配置加载行为的场景,例如通过远程 API 加载文件。 43 | - langchain_community.document_loaders 中的加载器: 44 | 适用于需要对特定文件格式进行精细处理的场景。 45 | 如果项目中主要处理单一文件格式,使用这些加载器可以提高效率。 46 | 47 | 我们这里暂时先用更精细的 document_loaders 中的加载器。 -------------------------------------------------------------------------------- /rfcs/notes/kjn-notes-2025.5.md: -------------------------------------------------------------------------------- 1 | # 5月工作记录 2 | 3 | 5月的工作主要是将Agent之前的服务从外部迁移到本地来,保证rag系统的稳定运行 4 | 5 | ## mysql服务 6 | 7 | ```bash 8 | (base) root@451f9b706ca3:/home# service mysql start 9 | * Starting MySQL database server mysqld [fail] 10 | ``` 11 | 12 | 启动显示失败的原因是 MySQL 服务可能已经处于运行状态 13 | ```bash 14 | mysql -h localhost -P 3306 -u root -p 15 | Enter password:123456 16 | Welcome to the MySQL monitor. Commands end with ; or \g. 17 | Your MySQL connection id is 8 18 | Server version: 8.0.40 MySQL Community Server - GPL 19 | mysql> 20 | ``` 21 | 22 | 输入password后,即可启动mysql服务。 23 | 24 | 在mysql中可以看到当前的数据库: 25 | 26 | ```bash 27 | mysql> USE rustsbi_rag; 28 | Reading table information for completion of table and column names 29 | You can turn off this feature to get a quicker startup with -A 30 | 31 | Database changed 32 | mysql> SELECT * FROM KnowledgeBase; 33 | +----+------------------------------------+---------------+---------+---------+----------------+--------------------+ 34 | | id | kb_id | user_id | kb_name | deleted | latest_qa_time | latest_insert_time | 35 | +----+------------------------------------+---------------+---------+---------+----------------+--------------------+ 36 | | 1 | KBb7eafaabc07743b6a7a0a4119283b1e6 | abc1234__5678 | zzh | 0 | NULL | NULL | 37 | | 2 | KB2ed627becda34af0a85cb1d104d90ebb | abc1234__5678 | zzh | 0 | NULL | NULL | 38 | +----+------------------------------------+---------------+---------+---------+----------------+--------------------+ 39 | 2 rows in set (0.00 sec) 40 | ``` 41 | 42 | ## elastic服务 43 | 44 | 服务器之前通过python包安装了elasticsearch,通过 45 | ```bash 46 | sudo find / -name elasticsearch 47 | ``` 48 | 49 | 可以找到elasticsearch的安装位置,以及二进制文件存放的位置。 50 | 51 | 这里我的路径为`/home/kjn/elastic/elasticsearch-8.17.0/bin/elasticsearch` 52 | 53 | 由于 Elasticsearch 默认不允许以 root 用户身份运行,因为这可能会带来安全风险。因此,我们需要新建一个非root用户,命名为 es 。 54 | 55 | 同时记得让es用户拥有执行文件的权限,具体命令如下: 56 | 57 | ```bash 58 | $ adduser es 59 | $ chown -R es /home/kjn/elastic/elasticsearch-8.17.0/bin/elasticsearch 60 | $ sudo chmod 755 /home/kjn 或者 sudo chown es:es /home/kjn 61 | $ su es 62 | es@451f9b706ca3:~$ /home/kjn/elastic/elasticsearch-8.17.0/bin/elasticsearch 63 | ``` 64 | 即可启动成功。后续貌似会一直在后台挂着,无须重复启动。 65 | 66 | ## milvus服务 67 | 68 | 参考[官方仓库](https://github.com/milvus-io/milvus)的信息,milvus主流的安装方式是使用docker,但是由于服务器本身是一个docker,docker-in-docker的操作比较复杂。 69 | 70 | 尝试寻找源码编译的方法,但是[官方发行包](https://github.com/milvus-io/milvus/releases)中只给出了docker-compose配置文件以及docker安装的源码,貌似没有给出源码编译的安装方式。 71 | 72 | 于是最终使用pytyon提供的milvus包进行安装,参考[如何在本地部署milvus服务器(无需docker)](https://blog.csdn.net/wjylovewjy/article/details/147279935),需要使用pip下载pymilvus,milvus,然后在/home下拉取文件并启动milvus服务,具体指令如下: 73 | 74 | ```bash 75 | pip install pymilvus 76 | pip install milvus 77 | milvus-server --data /home/milvus 78 | ``` 79 | 80 | 启动sanic等服务,测试milvus服务,执行Agent/src/evaluation/test_local_doc_chat.py。 81 | 82 | 期间可能会遇上`pymilvus.exceptions.ConnectionNotExistException`的报错,这是milvus的接口连接不上的问题,在保持milvus启动的条件下,重新启动sanic即可解决。输出如下: 83 | 84 | ```bash 85 | (qanything) root@451f9b706ca3:/home/kjn/Agent/src/evaluation# python test_local_doc_chat.py 86 | {'code': 200, 'msg': 'success no stream chat', 'question': '文中提到的“妖童媛女”在做什么?', 'response': 'data: {"answer": "根据参考信息,文中提到的“妖童媛女”在荡舟嬉游。具体来说,他们荡舟、传杯、唱艳歌,且举止轻盈,展现了当时嬉游的光景。这些描述出自《采莲赋》中的描写。"}', 'model': '/home/model/Qwen2.5-32B-Instruct-AWQ', 'history': [['文中提到的“妖童媛女”在做什么?', '根据参考信息,文中提到的“妖童媛女”在荡舟嬉游。具体来说,他们荡舟、传杯、唱艳歌,且举止轻盈,展现了当时嬉游的光景。这些描述出自《采莲赋》中的描写。']], 'condense_question': '文中提到的“妖童媛女”在做什么?', 'source_documents': [{'file_id': '6c11303f7bc14f1dac61756bf24a2f6c', 'file_name': '', 'content': '于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。\n可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\n于是又记起,《西洲曲》里的句子:\n采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\n今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\n一九二七年七月,北京清华园。', 'retrieval_query': '文中提到的“妖童媛女”在做什么?', 'file_url': '', 'score': '0.9', 'embed_version': '', 'nos_keys': '', 'doc_id': '6c11303f7bc14f1dac61756bf24a2f6c_1', 'retrieval_source': 'milvus', 'headers': [{'知识库名': 'zzh', '文件名': '这是一个测试文件.txt'}], 'page_id': 0}, {'file_id': '6c11303f7bc14f1dac61756bf24a2f6c', 'file_name': '', 'content': '荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿⑽,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡⑾人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\n忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\n于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。\n可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。', 'retrieval_query': '文中提到的“妖童媛女”在做什么?', 'file_url': '', 'score': '0.85', 'embed_version': '', 'nos_keys': '', 'doc_id': '6c11303f7bc14f1dac61756bf24a2f6c_1', 'retrieval_source': 'milvus', 'headers': [{'知识库名': 'zzh', '文件名': '这是一个测试文件.txt'}], 'page_id': 0}], 'retrieval_documents': [{'file_id': '6c11303f7bc14f1dac61756bf24a2f6c', 'file_name': '', 'content': '于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。\n可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。\n于是又记起,《西洲曲》里的句子:\n采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。\n今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。\n一九二七年七月,北京清华园。', 'retrieval_query': '文中提到的“妖童媛女”在做什么?', 'file_url': '', 'score': '0.9', 'embed_version': '', 'nos_keys': '', 'doc_id': '6c11303f7bc14f1dac61756bf24a2f6c_1', 'retrieval_source': 'milvus', 'headers': [{'知识库名': 'zzh', '文件名': '这是一个测试文件.txt'}], 'page_id': 0}, {'file_id': '6c11303f7bc14f1dac61756bf24a2f6c', 'file_name': '', 'content': '荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿⑽,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡⑾人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。\n忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好:\n于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。\n可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。', 'retrieval_query': '文中提到的“妖童媛女”在做什么?', 'file_url': '', 'score': '0.85', 'embed_version': '', 'nos_keys': '', 'doc_id': '6c11303f7bc14f1dac61756bf24a2f6c_1', 'retrieval_source': 'milvus', 'headers': [{'知识库名': 'zzh', '文件名': '这是一个测试文件.txt'}], 'page_id': 0}], 'time_record': {'time_usage': {'preprocess': 0.01, 'retriever_search_by_milvus': 0.03, 'retriever_search': 0.03, 'rerank': 0.7, 'reprocess': 0.01, 'llm_first_return': 4.7}, 'token_usage': {'prompt_tokens': 2997, 'completion_tokens': 120, 'total_tokens': 3117}}} 87 | 88 | 89 | 文中提到的“妖童媛女”在做什么? 90 | data: {"answer": "根据参考信息,文中提到的“妖童媛女”在荡舟嬉游。具体来说,他们荡舟、传杯、唱艳歌,且举止轻盈,展现了当时嬉游的光景。这些描述出自《采莲赋》中的描写。"} 91 | ``` 92 | 93 | 表明所有的服务已经成功迁移,Agent可以正常运行。 94 | 95 | ## configs更新 96 | 97 | 针对迁移的部署的服务,需要相应更新参数配置: 98 | ```py 99 | # MYSQL_HOST_LOCAL="k8s.richeyjang.com" 100 | # MYSQL_PORT_LOCAL="30303" 101 | # MYSQL_USER_LOCAL="root" 102 | # MYSQL_PASSWORD_LOCAL="123456" 103 | # MYSQL_DATABASE_LOCAL="rustsbi_rag" 104 | MYSQL_HOST_LOCAL = "localhost" # 修改为本地地址 105 | MYSQL_PORT_LOCAL = "3306" # MySQL 默认端口 106 | MYSQL_USER_LOCAL = "root" 107 | MYSQL_PASSWORD_LOCAL = "123456" 108 | MYSQL_DATABASE_LOCAL = "rustsbi_rag" 109 | 110 | # ES_USER="elastic" 111 | # ES_PASSWORD="XXIFkcJyTX=O2fgqLr=T" 112 | # ES_URL="https://k8s.richeyjang.com:30301" 113 | # ES_INDEX_NAME='rustsbi_es_index' + KB_SUFFIX 114 | ES_USER = "elastic" 115 | ES_PASSWORD = "XXIFkcJyTX=O2fgqLr=T" 116 | ES_URL = "http://localhost:9200" # 修改为本地地址和默认端口 117 | ES_INDEX_NAME = 'rustsbi_es_index' + KB_SUFFIX 118 | 119 | # MILVUS_HOST_LOCAL = "k8s.richeyjang.com" 120 | # MILVUS_PORT = 30300 121 | # MILVUS_COLLECTION_NAME = 'rustsbi_collection' + KB_SUFFIX 122 | MILVUS_HOST_LOCAL = "localhost" # 修改为本地地址 123 | MILVUS_PORT = 19530 124 | MILVUS_COLLECTION_NAME = 'rustsbi_collection' + KB_SUFFIX 125 | ``` 126 | 127 | ## 其他工作 128 | 129 | 1. 改进了vllm_start.sh,增加了及时清理log的操作。 130 | 2. 在/milvus下编写了测试milvus连接的程序 131 | 3. 更新了api_client.py -------------------------------------------------------------------------------- /rfcs/notes/rx-notes-2024.12.md: -------------------------------------------------------------------------------- 1 | # 2024.12 2 | 3 | ## 开发记录 4 | 5 | ### 环境配置 6 | 7 | 由于 Elasticsearch 要求非 root 用户运行,同时为简化开发环境的管理,新建了非 root 用户并进行以下配置: 8 | 9 | #### 安装 Miniconda 10 | 11 | Link:`https://www.anaconda.com/download/success` 12 | 13 | 正常下载脚本安装即可。 14 | 15 | #### 安装 Elasticsearch 16 | 17 | Link:`https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.17.0-linux-x86_64.tar.gz` 18 | 19 | 解包安装即可。安装之后,通过安装路径下的 `bin/elasticsearch` 可执行文件运行 `Elasticsearch` 程序,于 `0.0.0.0:9200` 监听外部请求。传入的文档切片将被保存在安装目录下的 `data/indices` 目录中。程序监听端口,使用协议等配置位于 `config/elasticsearch.yml` 文件中。 20 | 21 | #### 安装 faiss 22 | 23 | Link:`https://github.com/facebookresearch/faiss/blob/main/INSTALL.md` 24 | 25 | `Faiss` 作为一个 `python` 模块,正常来说可以通过 `pip` 安装。但是实践发现该模块在 conda 环境下的 pip 维护已经停止,需要通过 conda 进行安装。具体安装指导见 Link。 26 | 27 | ``` 28 | conda install -c pytorch -c nvidia faiss-gpu=1.9.0 29 | ``` 30 | 31 | ### Files2VectorDB 代码优化 32 | 33 | 在将原有的 `files2db.py` 代码优化为 `FileToVectorDB` 类时,进行了以下改进和优化: 34 | 35 | #### 重构为类结构 36 | 37 | - 将原有的函数式代码封装到 FileToVectorDB 类中,提高代码的模块化和复用性。 38 | - 在初始化方法中统一处理数据库根目录和嵌入模型的配置,确保所有实例化对象的一致性。 39 | 40 | #### 优化文件解析方法 41 | 42 | - 将各类文件的解析函数(如 parse_adoc, parse_markdown, parse_latex, parse_rst, parse_txt)作为类的成员方法,使得代码组织更加清晰。 43 | - 在 `Asciidoc` 和 `reStructuredText` 的解析中,增强了对标题和内容的处理逻辑,确保更准确的内容分块。 44 | 45 | #### 改进文件处理逻辑 46 | 47 | - 在 `process_file` 方法中,增加了对文件哈希值的计算和向量数据库的检查,避免重复处理已存在的文件。 48 | - 使用 `RecursiveCharacterTextSplitter` 对提取的文本进行合理的分块处理,确保向量数据库的有效性和查询性能。 49 | - 在处理新上传的文件时,先将文件内容写入指定路径,再进行后续的解析和向量存储操作,确保文件的完整性。 50 | 51 | ### Files2ElasticSearch 代码编写 52 | 53 | #### 代码设计 54 | 55 | `Files2ElasticSearch` 的目标是将文件解析后切片存储到 `Elasticsearch` 索引中,结构和 `Files2VectorDB` 类似。 56 | 57 | - **类初始化:** `FileToElasticSearch` 类在初始化时接收 `Elasticsearch` 的主机地址、端口、索引名称及嵌入模型等参数。 58 | 初始化时自动检查并创建所需的索引。 59 | 60 | ```python 61 | file_to_es = FileToElasticSearch( 62 | es_host="localhost", 63 | es_port=9200, 64 | index_name="documents", 65 | embedding_model="qwen:7b" 66 | ) 67 | ``` 68 | 69 | - **主要方法说明:** 70 | - `process_file(file_path: str)`: 解析单个文件,按块存储到指定的 `Elasticsearch` 索引中。 71 | - `process_directory(directory_path: str)`: 批量处理目录中的所有文件。 72 | - 内部解析方法(如 `parse_adoc`, `parse_markdown`)直接复用 `Files2VectorDB` 中的实现。 73 | 74 | #### **遇到的问题及解决方案** 75 | 76 | 1. **Elasticsearch 配置问题** 77 | - **问题描述:** 78 | 初始配置中启用了 HTTPS,但未正确生成自签名证书,导致客户端连接时报错 `[SSL: WRONG_VERSION_NUMBER]` 或 `[SSL: CERTIFICATE_VERIFY_FAILED]`。 79 | - **解决方案:** 80 | - 通过 `elasticsearch-certutil http` 重新生成自签名证书 `http.p12`。 81 | - 在客户端代码中禁用证书验证,仅用于测试环境: 82 | ```python 83 | from elasticsearch import Elasticsearch 84 | es_client = Elasticsearch( 85 | [{"host": "localhost", "port": 9200, "scheme": "https"}], 86 | verify_certs=False 87 | ) 88 | ``` 89 | - 确保 Elasticsearch 的 `elasticsearch.yml` 配置文件正确启用了 HTTPS。 90 | ```yaml 91 | xpack.security.http.ssl.enabled: true 92 | xpack.security.http.ssl.keystore.path: certs/http.p12 93 | ``` 94 | - **临时解决方案:** 95 | 为了避免密钥上传至公网,由于 Elasticsearch 服务器运行在 localhost,因此修改配置为 http 且禁用身份验证机制。如果环境改为正式生产,需要避免使用该解决方案。 96 | 97 | ## 杂项问题 98 | 99 | ### 用户不在 sudo group 中 100 | 101 | 为了更清晰的权限管理,在开发服务器上新建用户,并使用 102 | 103 | ```bash 104 | usermod -aG sudo 105 | ``` 106 | 107 | 添加权限。 108 | 109 | 然后新登录一个 shell,尝试 `sudo visudo` 命令,出现报错: 110 | 111 | ``` 112 | *** is not in the sudoers file. This incident will be reported. 113 | ``` 114 | 115 | 检查相关权限文件,未找到问题。采用 root 登陆再降级到 rx 用户解决。 116 | 117 | ### 无法安装 Anaconda 118 | 119 | 采用 Anaconda 官网链接,使用 wget 下载其安装 shell 脚本,安装过程出现报错: 120 | 121 | ``` 122 | RuntimeError: Failed to extract 123 | .conda: 'ascii' codec can't encode character '\xe4' in position 93: ordinal not in range(128) 124 | ``` 125 | 126 | 检查路径中是否含有特殊字符或非 ASCII 编码字符,未发现问题;尝试修改安装路径,在 bashrc 中添加编码相关选项,未成功。 127 | 128 | 解决:改为安装 Miniconda,安装成功。 -------------------------------------------------------------------------------- /rfcs/notes/zzh-notes-2024.12.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | ```shell 4 | conda activate py310 # 我是在python3.10环境下搞的 5 | pip install beautifulsoup4 6 | sudo apt install asciidoctor 7 | pip install elasticsearch 8 | ``` 9 | 10 | ## 解析文本 11 | 12 | 将 https://github.com/riscv/riscv-isa-manual 仓库下的内容使用`git clone https://github.com/riscv/riscv-isa-manual.git --recursive`拉取到本地,并在目标文件夹下编写代码 13 | 14 | ```python 15 | import subprocess 16 | from bs4 import BeautifulSoup 17 | import os 18 | 19 | def convert_adoc_to_txt(input_file, output_file): 20 | try: 21 | # 使用 Asciidoctor 将 AsciiDoc 转换为 HTML: 22 | subprocess.run(['asciidoctor', '-o', 'temp.html', input_file], check=True) 23 | 24 | # 读取生成的 HTML 文件 25 | with open('temp.html', 'r', encoding='utf-8') as f: 26 | html_content = f.read() 27 | 28 | # 使用 BeautifulSoup 提取纯文本 29 | soup = BeautifulSoup(html_content, 'html.parser') 30 | text = soup.get_text() 31 | 32 | # 写入纯文本文件 33 | with open(output_file, 'w', encoding='utf-8') as f: 34 | f.write(text) 35 | 36 | # 删除临时 HTML 文件 37 | os.remove('temp.html') 38 | 39 | print(f"转换成功!纯文本文件已保存为 {output_file}") 40 | except subprocess.CalledProcessError as e: 41 | print("Asciidoctor 转换失败。") 42 | print(e) 43 | except Exception as e: 44 | print("转换过程中出错。") 45 | print(e) 46 | 47 | def get_adoc_files(directory): 48 | # 列出指定目录下的所有条目 49 | entries = os.listdir(directory) 50 | # 筛选出以 .adoc 结尾的文件 51 | adoc_files = [file for file in entries if file.endswith('.adoc') and os.path.isfile(os.path.join(directory, file))] 52 | return adoc_files 53 | 54 | if __name__ == "__main__": 55 | input1 = "riscv-privileged.adoc" 56 | input2 = "riscv-unprivileged.adoc" 57 | convert_adoc_to_txt(input1, input1.replace('.adoc', '.txt')) 58 | convert_adoc_to_txt(input2, input2.replace('.adoc', '.txt')) 59 | ``` 60 | 61 | 注意如果所解析的adoc中含有类似`include::intro.adoc[]`这种语句,那么必须要其`include`的文件存在,否则会解析错误。在解析成功后查看所解析文件,如下图。 62 | 63 | ![img](../assets/zzh-December/1.png) 64 | 65 | ## 进行服务器中的ElasticSearch的环境搭建 66 | 67 | ### 下载并解压 68 | 69 | 为了流程走通,这里简单搭建一个es单节点启动。 70 | 71 | 首先在官网下载ES压缩包: 72 | 73 | ```python 74 | https://www.elastic.co/elasticsearch 75 | ``` 76 | 77 | 在`/home/zzh`目录下创建一个文件夹 78 | 79 | ```python 80 | mkdir elastic 81 | ``` 82 | 83 | 将下载好的ES压缩包解压到所创建文件夹中 84 | 85 | ```shell 86 | tar -xzf elasticsearch-8.7.1-linux-x86_64.tar.gz -C /home/zzh/elastic 87 | ``` 88 | 89 | ES不能直接运行在root用户下,所以需要创建一个专用于启动ES的用户,我本来使用的就不是root,而是自己创建的用户。 90 | 91 | ### 修改配置文件 92 | 93 | 调整ES虚拟内存,虚拟内存默认最大映射数为65530,无法满足ES系统要求,需要调整为262144以上,用户切成root(后面都用root,最后启动之前在切成创建用户),打开配置文件 94 | 95 | ```shell 96 | vim /etc/sysctl.conf 97 | ``` 98 | 99 | 添加下面的配置 100 | 101 | ```shell 102 | vm.max_map_count = 262144 103 | ``` 104 | 105 | 重新加载`/etc/sysctl.conf`配置 106 | 107 | ```shell 108 | sysctl -p 109 | ``` 110 | 111 | **出现问题:** 112 | 113 | 在使用`sysctl -p`重新加载`/etc/sysctl.conf`配置时出现了`sysctl: setting key "vm.max_map_count", ignoring: Read-only file system`的错误。 114 | 115 | 排查一段时间后,随后使用 116 | 117 | ```shell 118 | cat /proc/cmdline 119 | ``` 120 | 121 | 发现输出为 122 | 123 | ```shell 124 | BOOT_IMAGE=/boot/vmlinuz-5.15.0-105-generic 125 | root=UUID=3aaa6fa5-ba9e-4552-b91d-cc5367274e5e ro 126 | ``` 127 | 128 | ​ 系统启动参数显示的是ro,如果想修改配置必须改成rw,随后在任潇同学的协助下解决。 129 | 130 | 修改es进程打开的最大文件数量 131 | 132 | ```shell 133 | vim /etc/security/limits.conf 134 | #* soft core 0 135 | #* hard rss 10000 136 | #@student hard nproc 20 137 | #@faculty soft nproc 20 138 | #@faculty hard nproc 50 139 | #ftp hard nproc 0 140 | #@student - maxlogins 4 141 | * soft nofile 65536 142 | * hard nofile 65536 143 | * soft nproc 4096 144 | * hard nproc 4096 145 | # End of file 146 | ``` 147 | 148 | 修改es配置文件 149 | 150 | ```shell 151 | vim /home/zzh/elastic/elasticsearch-8.17.0/config/elasticsearch.yml 152 | node.name: node-1 153 | # ---------------------------------- Network ----------------------------------- 154 | # 本机ip 155 | network.host: 0.0.0.0 # 这里我只在本机内部使用就直接使用localhost了 156 | # 端口号 157 | http.port: 9200 158 | # --------------------------------- Discovery ---------------------------------- 159 | # 单机只保留一个node 160 | cluster.initial_master_nodes: ["node-1"] 161 | # 162 | xpack.security.enabled: false # 禁用x-pack安全功能,用于本地开发环境 163 | 164 | xpack.security.transport.ssl.enabled: false # 禁用节点间通信加密 165 | ``` 166 | 167 | 修改jvm.options内存设置 168 | 169 | ```shell 170 | vim /elastic/elasticsearch-8.17.0/config/jvm.options 171 | ``` 172 | 173 | 默认1G,启动后会占用700M,所以机器内存至少2G以上 174 | 175 | ```shell 176 | -Xms1g 177 | -Xmx1g 178 | ``` 179 | 180 | 启动ES 181 | 182 | 进入ES根目录 183 | 184 | ```shell 185 | cd /elastic/elasticsearch-8.17.0/ 186 | ``` 187 | 188 | 切换到zzh用户 189 | 190 | ```shell 191 | su zzh 192 | ``` 193 | 194 | 启动ES服务 195 | 196 | ```shell 197 | ./bin/elasticsearch 198 | ``` 199 | 200 | 使用`curl http://localhost:9200`查看启动服务是否成功。 201 | 202 | ![img](../assets/zzh-December/2.png) 203 | 204 | 启动成功。 -------------------------------------------------------------------------------- /rfcs/notes/开源大模型调研 by kjn.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/notes/开源大模型调研 by kjn.pdf -------------------------------------------------------------------------------- /rfcs/ollama搭建RAG系统.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/rfcs/ollama搭建RAG系统.pdf -------------------------------------------------------------------------------- /rfcs/rag_pipeline_test_report_rx_0328.md: -------------------------------------------------------------------------------- 1 | # 20250328 RAG流程测试报告 2 | 3 | ## 测试目标 4 | 示例:测试RAG系统整体流程,排查其中可能存在的问题,并且写下case分析和拟优化方案 5 | 6 | ## 测试记录 7 | 8 | 基座模型:Qwen/Qwen2.5-7B-Instruct 9 | 10 | ### PDF 输入测试记录 11 | 12 | 采用 RISC-V 官方文档 riscv-privileged.pdf 作为测试文档进行测试,产生 list index out of range 问题。尝试进行问题排查,未成功,列入稍后工作。 13 | 14 | ### TXT 输入测试记录 15 | 16 | 短 txt 文档测试: 17 | 18 | 采用朱自清《荷塘月色》作为测试文档。知识库建立、文档上传,RAG问答流程正常。 19 | 20 | | 输入Query | 输出 Response | 存在的问题 | 21 | |-----------| ---------------| ---------- | 22 | |“荡舟心许”出自谁的诗?|"根据参考信息,“荡舟心许”出自《西洲曲》中的句子。|误将后文提到另一首诗标题作为当前诗标题,疑似因分块位置及重排序顺序所致| 23 | |作者是什么时候去散步的?|根据参考信息,作者是晚上在院子里散步的。具体时间描述为“这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。”从中可以推断,作者是在一个满月的晚上散步的。|部分正确,没有答到一九二七年这个大时间点。查看检索得到的块,“一九二七年”所在块排相关度第三位,可能为模型能力限制所致。| 24 | |作者将荷花比作什么?|According to the reference information,作者将荷花比作亭亭的舞女的裙、一粒粒的明珠、碧天里的星星、刚出浴的美人。\\n\\n具体来说,原文中有以下描述:\\n- “叶子出水很高,像亭亭的舞女的裙。”\\n- “正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。”\\n这些比喻形象地描绘了荷花的美丽姿态和光彩。|存在中英文混杂和查询理解错误的问题。误将对叶子的描述理解为对荷花的描述。| 25 | 26 | 长 txt 文档测试: 27 | 28 | 采用 RISC-V 官方文档 riscv-privileged.pdf 简单转换得到的 riscv-privileged.txt 作为测试文档。共10867行。知识库建立正常。调用接口进行文档上传及解析时出现 Failed to upload file (HTTP 503): 503 — Service Unavailable 问题,为 Response Timeout 所致,但文档成功上传并分片,未影响后续测试流程。RAG问答流程正常。 29 | 30 | | 输入Query | 输出 Response | 存在的问题 | 31 | |-----------| ---------------| ---------- | 32 | |RISC-V架构中定义了哪几种特权级别?|According to the reference information, RISC-V架构中定义了三种特权级别,分别是:\n\n- **User/Application (U)**:用户或应用级别。\n- **Supervisor (S)**:监督器级别。\n- **Machine (M)**:机器级别。\n\n这些特权级别用于在软件堆栈的不同组件之间提供保护,并且尝试执行当前特权模式不允许的操作将引发异常,这些异常通常会导致进入较低的特权执行环境。|存在中英文混杂问题。| 33 | |mstatus寄存器中的MIE和SIE位有什么作用?|According to the reference information, mstatus寄存器中的MIE和SIE位的作用如下:\n\n- **MIE位**:MIE位位于mstatus寄存器的低8位,用于控制M-mode(机器模式)的全局中断使能。当MIE为1时,M-mode的中断全局使能;当MIE为0时,M-mode的中断全局禁止。对于更高特权模式的中断,它们始终处于全局使能状态,不受任何全局中断使能位的影响。\n- **SIE位**:SIE位同样位于mstatus寄存器的低8位,用于控制S-mode(监督模式)的全局中断使能。当SIE为1时,S-mode的中断全局使能;当SIE为0时,S-mode的中断全局禁止。对于更低特权模式的中断,它们始终处于全局禁止状态,不受任何全局中断使能位的影响。\n\n### 摘要\n- **MIE位和SIE位**的主要作用是确保在当前特权模式下的中断处理程序的原子性。\n- 这些位可以通过单一的CSR指令原子地设置或清除,从而确保中断处理的高效性和一致性。|存在中英文混杂问题,存在表述问题。| 34 | |mcause寄存器如何表示异常和中断?|Error code: 503 - {'code': 50508, 'message': 'System is too busy now. Please try again later.', 'data': None}|测试失败,系统过于繁忙。由于本次测试通过远程API调用外部模型进行,未占用系统计算资源,考虑排查代码原因。| 35 | 36 | ## 存在的问题总结 37 | 38 | - PDF 未成功解析:需要排查系统中存在的 bug 并修正。 39 | - 长 TXT 上传异常:可能是网络通信中延时设置问题。需要排查系统代码设置。 40 | - 系统繁忙:问题同上。 41 | - 输出中英文混杂:可能是RAG系统基座模型参数量过小原因。考虑采用参数量更大的模型。 42 | - 查询输出事实错误:问题同上,检索和重排序给出的文本块正确,考虑为模型理解能力问题。 43 | - 输出忽略部分事实:问题同上。 44 | 45 | ## 优化的思路 46 | 47 | - 排查现有代码,修正导致 PDF 无法解析和 TXT 较长时返回错误的问题。 48 | - 更换基座模型,比较模型能力。 -------------------------------------------------------------------------------- /rfcs/rag_pipeline_test_report_rx_0428.md: -------------------------------------------------------------------------------- 1 | # 20250428 RAG流程测试报告 2 | 3 | ## 测试目标 4 | 重测试整体 RAG 流程,比较优化后与优化前 RAG 系统输出 5 | 6 | ## 测试记录 7 | 8 | 基座模型:Qwen2.5-32B-Instruct-AWQ 9 | 10 | ### PDF 输入测试记录 11 | 12 | 采用 RISC-V 官方文档 riscv-privileged.pdf 作为测试文档进行测试,仍产生 list index out of range 问题。下一步工作应当着重解决 txt 以外格式文档上传问题。 13 | 14 | ### TXT 输入测试记录 15 | 16 | 短 txt 文档测试: 17 | 18 | 采用朱自清《荷塘月色》作为测试文档。知识库建立、文档上传,RAG问答流程正常。 19 | 20 | | 输入Query | 输出 Response(旧) | 存在的问题 | 输出 Response(新) | 问题是否解决 | 21 | |-----------| ---------------| ---------- | ---------- | ---------- | 22 | |“荡舟心许”出自谁的诗?|"根据参考信息,“荡舟心许”出自《西洲曲》中的句子。|误将后文提到另一首诗标题作为当前诗标题,疑似因分块位置及重排序顺序所致。 | 根据参考信息,“荡舟心许”出自梁元帝的《采莲赋》。 | 问题已解决。 | 23 | |作者是什么时候去散步的?|根据参考信息,作者是晚上在院子里散步的。具体时间描述为“这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。”从中可以推断,作者是在一个满月的晚上散步的。|部分正确,没有答到一九二七年这个大时间点。查看检索得到的块,“一九二七年”所在块排相关度第三位,可能为模型能力限制所致。| 根据参考信息,作者是在晚上散步的。文中提到“今晚在院子里坐着乘凉,忽然想起日日走过的荷塘”,表明作者是在晚上出门散步。 | 问题基本解决,“时间”这一描述与“晚上”对应。| 24 | |作者将荷花比作什么?|According to the reference information,作者将荷花比作亭亭的舞女的裙、一粒粒的明珠、碧天里的星星、刚出浴的美人。\\n\\n具体来说,原文中有以下描述:\\n- “叶子出水很高,像亭亭的舞女的裙。”\\n- “正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。”\\n这些比喻形象地描绘了荷花的美丽姿态和光彩。|存在中英文混杂和查询理解错误的问题。误将对叶子的描述理解为对荷花的描述。| 根据参考信息,作者将荷花比作了明珠、星星和刚出浴的美人。\n\n- 明珠:荷花像一粒粒的明珠。\n- 星星:荷花又如碧天里的星星。\n- 刚出浴的美人:荷花又如刚出浴的美人。 | 问题已解决,不再使用英文回答问题。 | 25 | 26 | 长 txt 文档测试: 27 | 28 | 采用 RISC-V 官方文档 riscv-privileged.pdf 简单转换得到的 riscv-privileged.txt 作为测试文档。共10867行。知识库建立正常。调用接口进行文档上传及解析时出现 Failed to upload file (HTTP 503): 503 — Service Unavailable 问题,为 Response Timeout 所致,但文档成功上传并分片,未影响后续测试流程。RAG问答流程正常。 29 | 30 | 本月更新后,系统仍存在超时问题。 31 | 32 | | 输入Query | 输出 Response(旧) | 存在的问题 | 输出 Response(新) | 问题是否解决 | 33 | |-----------| ---------------| ---------- | ---------- | ---------- | 34 | |RISC-V架构中定义了哪几种特权级别?|According to the reference information, RISC-V架构中定义了三种特权级别,分别是:\n\n- **User/Application (U)**:用户或应用级别。\n- **Supervisor (S)**:监督器级别。\n- **Machine (M)**:机器级别。\n\n这些特权级别用于在软件堆栈的不同组件之间提供保护,并且尝试执行当前特权模式不允许的操作将引发异常,这些异常通常会导致进入较低的特权执行环境。|存在中英文混杂问题。| 根据参考信息,RISC-V架构中定义了以下几种特权级别:\n\n- **用户/应用级别(U-mode)**\n- **监督级别(S-mode)**\n- **机器级别(M-mode)**\n\n这些特权级别用于提供软件栈中不同组件之间的保护。尝试执行当前特权模式下不允许的操作会导致异常,这些异常通常会导致陷落到底层执行环境。 | 问题已解决。| 35 | |mstatus寄存器中的MIE和SIE位有什么作用?|According to the reference information, mstatus寄存器中的MIE和SIE位的作用如下:\n\n- **MIE位**:MIE位位于mstatus寄存器的低8位,用于控制M-mode(机器模式)的全局中断使能。当MIE为1时,M-mode的中断全局使能;当MIE为0时,M-mode的中断全局禁止。对于更高特权模式的中断,它们始终处于全局使能状态,不受任何全局中断使能位的影响。\n- **SIE位**:SIE位同样位于mstatus寄存器的低8位,用于控制S-mode(监督模式)的全局中断使能。当SIE为1时,S-mode的中断全局使能;当SIE为0时,S-mode的中断全局禁止。对于更低特权模式的中断,它们始终处于全局禁止状态,不受任何全局中断使能位的影响。\n\n### 摘要\n- **MIE位和SIE位**的主要作用是确保在当前特权模式下的中断处理程序的原子性。\n- 这些位可以通过单一的CSR指令原子地设置或清除,从而确保中断处理的高效性和一致性。|存在中英文混杂问题,存在表述问题。| 根据参考信息,mstatus寄存器中的MIE和SIE位主要用于保证当前特权模式下中断处理程序的原子性。这些位位于mstatus寄存器的低阶位,可以通过单一的CSR指令原子地设置或清除。具体来说:\n\n- **MIE位**:用于M-mode(机器模式),当MIE=1时,中断全局启用;当MIE=0时,中断全局禁用。\n- **SIE位**:用于S-mode(监督模式),功能与MIE位类似。如果系统未实现监督模式,则SIE位为只读0。 | 问题已解决,表达相对于上次测试更为流畅。 | 36 | |mcause寄存器如何表示异常和中断?|Error code: 503 - {'code': 50508, 'message': 'System is too busy now. Please try again later.', 'data': None}|测试失败,系统过于繁忙。由于本次测试通过远程API调用外部模型进行,未占用系统计算资源,考虑排查代码原因。| 根据参考信息,mcause寄存器用于表示异常和中断的方式如下:\n\n- **中断表示**:\n - 当陷阱(trap)由中断引起时,mcause寄存器的最高位(Interrupt位)被设置为1。\n - 中断的类型通过mcause寄存器中的Exception Code字段来编码。例如,Supervisor软件中断对应值1,Machine软件中断对应值3,Supervisor定时器中断对应值5,Machine定时器中断对应值7等。\n\n- **异常表示**:\n - 当陷阱(trap)由异常引起时,mcause寄存器的最高位(Interrupt位)被设置为0。\n - 异常的具体类型通过Exception Code字段来编码,如Instruction地址不对齐对应值0,Illegal指令对应值2,Load地址不对齐对应值4,Store/AMO地址不对齐对应值6等。\n\n- **特殊处理**:\n - 对于非屏蔽中断(NMIs),mcause寄存器的值由实现定义,但Interrupt位应设置为1来表示这是一个中断。\n - 未知原因的异常,Exception Code应设置为0。\n - 若指令可能引发多个同步异常,mcause寄存器记录的是优先级最高的异常。\n\n以上信息基于提供的参考文档内容。 | 问题已解决。| 37 | 38 | ## 存在的问题总结 39 | 40 | - PDF 未成功解析:需要排查系统中存在的 bug 并修正。 41 | - 长 TXT 上传异常:可能是网络通信中延时设置问题。需要排查系统代码设置。 42 | - 系统繁忙:问题同上。 43 | - 输出中英文混杂:可能是RAG系统基座模型参数量过小原因。考虑采用参数量更大的模型。 44 | - 查询输出事实错误:问题同上,检索和重排序给出的文本块正确,考虑为模型理解能力问题。 45 | - 输出忽略部分事实:问题同上。 46 | 47 | ## 问题解决情况 48 | 49 | 更换基座模型和系统 prompt 后, 50 | 51 | - PDF 仍未成功解析:下一步工作应当考虑重点排查代码流程中与之相关的问题。 52 | - 长 TXT 上传异常:定位确认为网络栈异步函数同步问题,不影响使用,可以考虑修改异步逻辑。 53 | - 输出中英文混杂:经过更换系统 prompt 已解决。 54 | - 查询输出事实错误:经过更换基座模型已解决。 55 | - 输出忽略部分事实:同上,已解决。 -------------------------------------------------------------------------------- /rfcs/rag_pipeline_test_report_template.md: -------------------------------------------------------------------------------- 1 | # 测试报告模板 2 | 3 | ## 测试目标 4 | 示例:测试RAG系统整体流程,排查其中可能存在的问题,并且写下case分析和拟优化方案 5 | 6 | ## 测试记录 7 | 建议测试至少包含十条不同的query 8 | 9 | | 输入Query | 输出 Response | 存在的问题 | 10 | |-----------| ---------------| ---------- | 11 | 12 | 13 | 14 | ## 存在的问题总结 15 | 对测试记录中存在的问题,简要做个总结 16 | 17 | ## 优化的思路 18 | 写下你已经做了哪些优化,以及计划做哪些优化 19 | 20 | ### 已进行的优化方案记录 21 | 22 | 如未解决,则在是否解决一列说明下原因,例如优化后出现了新的问题,则记录出现的新的问题 23 | 24 | | 输入Query | 输出 Response | 优化方案 | 是否解决| 25 | | --------- | ------------- | ------ |--------| 26 | 27 | 28 | ### 待尝试的优化方案 29 | | 输入Query | 输出 Response | 优化方案 | 30 | | --------- | ------------- | -------- | 31 | -------------------------------------------------------------------------------- /rfcs/rag_test_report_kjn_0329.md: -------------------------------------------------------------------------------- 1 | # 测试报告 2 | 3 | ## 测试目标 4 | 测试RAG系统的指令遵循能力,以及使用数据集测试LLM的能力。 5 | 6 | ## 测试记录 7 | ### 简单提问测试 8 | 9 | | 输入Query | 输出 Response | 存在的问题 | 10 | |-----------| ---------------| ---------- | 11 | 概括一下文段的主要内容 | "According to the reference information, the text describes the scene of a moonlit night by a lotus pond. The passage is divided into two main parts:\\n\\n1. **Lotus Pond Scene** (Ref.1):\\n - The text vividly portrays the beauty of the lotus pond under the moonlight. It describes the lotus leaves and flowers, their colors, and the fragrance they emit. The light and shadows on the lotus leaves and flowers create a harmonious and dreamy atmosphere. The author also mentions how the moonlight and misty atmosphere enhance the poetic ambiance of the scene.\\n2. **Memories and Reflections** (Ref.2):\\n - The second part of the text shifts to memories of past lotus picking activities. It describes the lively scene of young men and women enjoying themselves on the lotus pond. The author then compares this scene to the poetic imagery in \\"The West洲曲,\\" expressing a longing for the beauty and simplicity of the past. The passage concludes with the author reflecting on the current tranquility of his surroundings, hinting at a sense of nostalgia for the past.\\n\\nIn summary, the text combines a vivid description of a lotus pond scene with personal reflections on past memories and a sense of nostalgia. | 语言上全英文,无法与Query同步 | 12 | 概括一下文段的主要内容 | "According to the reference information, the text describes the author\'s observations and feelings about a moonlit night by a lotus pond. The main content can be summarized as follows:\\n\\n- **景物描写**(景物描写): 描述了月光下的荷塘景色,包括荷叶、荷花、月光和雾气的交织景象。微风拂过荷塘,带来了阵阵清香。\\n- **情感表达**(情感表达): 作者通过景物描写,表达了对自然美景的喜爱和对往事的怀念。特别是引用了《西洲曲》中的诗句,寄托了对江南水乡的思念。\\n\\n这些描述展现了作者对自然美景的细腻感受以及对往事的深深怀念。" | 语言中英结合,无法保持一种语言 | 13 | 用简洁的语言对文段进行总结和概括, 并给出文段的主题, 要求字数在100字以内 | "According to the reference information,该段文字描述了一个独自漫步荷塘的夜晚场景,作者通过细腻的笔触描绘了荷塘月色的美好,同时也表达了自己在这种环境中感受到的自由和宁静。荷塘四周被树木围绕,夜晚的蝉鸣和蛙声增添了氛围。文章还提到了采莲的旧俗,勾起了对过去嬉游场景的回忆。\n\n荷塘月色的美景和夜晚的宁静是文章的主要主题。" | 语言中英结合,无法保持一种语言 | 14 | 15 | ### RAGTruth数据集测试LLM。 16 | 17 | RAGTruth是一个单词级幻觉语料库,用于检索增强生成(RAG)设置中的各种任务,用于训练和评估。 18 | 19 | 通过相应的脚本,选择了RAGTruth数据集中Task_type为"Summary", "data2text" 和 "QA" 各101份数据,共计303份数据,测试Qwen2.5-7B-Instrut,得到`response-Qwen2.5-7B-Instruct.jsonl`。 20 | 21 | 再通过利用Ollama中的deepseek-r1-8B的api作为裁判大模型,结合"source_info"和"response",得到了相应的judge结果,计算出了Qwen2.5-7B-Instrut 在包含context的场景下的幻觉率。 22 | 23 | 由于不知道什么原因,我这边下载的几个model用不了,如果解决了这个问题,后期可以多测几个model进行比较。 24 | 25 | 26 | ## 存在的问题总结 27 | 对于configs中的prompt_template,LLM没有很好地理解和遵循。当用户输入的语句比较短,难以提取其中使用的语言。语句较长时可以较为准确地识别。总结为小参数模型的指令遵循能力不足。 28 | 29 | ## 优化的思路 30 | 优化prompt_template,同时试着是否能够使用更大参数或者更好表现的模型来提升指令遵循能力。 31 | 32 | 33 | ### 已进行的优化方案记录 34 | 35 | 尝试通过精简prompt,减少LLM的理解负担;通过重复Language的要求,增强Langugae的权重。 36 | 37 | 结果:优化后,LLM的回答语言稍微稳定了些,但是效果不大。 38 | 39 | 40 | ### 待尝试的优化方案 41 | 1. 使用CoT来提升LLM对prompt的遵循程度。 42 | 2. 寻找小参数中更适合我们任务的模型替代。 -------------------------------------------------------------------------------- /rfcs/retrieval_module_design.md: -------------------------------------------------------------------------------- 1 | ![Alt text](assets/retrieval_module.png) 2 | 3 | # 检索模块架构设计 4 | 5 | ## 1 架构介绍 6 | 检索模块,是RAG系统中最重要的模块之一,检索模块的目的是为了检索出高质量的文本数据作为喂入大模型的数据。检索出高质量的文本数据可以有效的提升大模型的问答能力,减少幻觉情况。 7 | ## 1.1 数据源 8 | 数据源可以分成结构化数据和非结构化数据, 9 | - **非结构化数据**: 非结构化数据通常是网页爬取的纯文本,因此需要通过数据预处理 10 | - **结构化数据**:结构化数据通常是json文件保存的问答对,该数据通常使用开源数据集有较高的质量可以直接存入数据库 11 | 12 | ## 1.2 数据预处理 13 | - **文档清洗**:主要是对文档初步处理,去除一些特殊符号等。 14 | - **切片**: 由于文档较长,无法完整保存下来,并且即使完整保存下来,当检索出较长文档输入给大模型时,大模型也无法处理过长的文档,因此需要对文档进行切片处理 15 | 16 | ## 1.3 检索模型 17 | 检索模型设计了两种检索方式,分别是ElasticSearch(ES)的关键词检索和基于Embedding的向量检索。 18 | - **关键词检索** 关键词检索指的是通过用户查询里的关键词和文档里的关键词进行比对,来选择出较为匹配的数据。这可以通过ES数据库实现。首先将文档进行分词处理,然后通过ES数据库对文档构建倒排索引用来匹配关键词 19 | - **向量检索** 由于关键词检索必须对字面匹配上才可以,这往往会导致相同的语义,使用不同关键词时匹配不上,例如“马铃薯”和“土豆”指的都是一个东西,但是通过关键词搜索却无法匹配,因此需要通过语义匹配检索出这类文档,这就是通过向量检索实现。通过将文本进行向量化,然后计算不同文本之间的相似度来检索文档。 20 | 21 | 但是通过模型计算向量是一个耗时的过程,切有大量的文本数据,都转换成向量将会非常占用存储空间,因此使用Faiss向量库来存储文本向量,来提升检索速度。 22 | 23 | -------------------------------------------------------------------------------- /rfcs/work_report/zzh-work-report-2024.11.md: -------------------------------------------------------------------------------- 1 | **实习工作月报表(2024年11月)** 2 | **实习生:张子涵** 3 | 4 | ##### PDF RAG 5 | 在马博的帮助下,在实习所给的服务器上,使用`langchain` 框架实现了对单个PDF文档的向量化存储功能和文档的分块与检索,集成了`Qwen:7b`作为Embedding模型进行检索和`Gemma2:27b`作为生成模型进行回答的生成,并使用`streamlit`实现用户交互的界面。 6 | 7 | ##### 文档的编写 8 | 9 | 编写搭建文档以及应用的测试报告,对于过程中遇到的问题也都记录了解决过程以及截图。 10 | 11 | ##### 遇到的问题及解决方案 12 | 13 | **向量数据库选择问题** 14 | 15 | **问题**:初期使用的`ChromaDB`存在连接问题 16 | 17 | **解决**:更换为`FAISS`向量数据库,提高了稳定性 18 | 19 | **文档处理效率问题** 20 | 21 | **问题**:大文件处理耗时较长 22 | 23 | **解决**:优化文档分块策略 24 | 25 | ##### 下月工作计划 26 | 27 | - 将向量都保存在向量数据库中,之后每次开始之前加载本地保存的向量数据库,在添加新文档时,将新的向量进行持久化保存在本地的向量数据库中,也就是可以上传多个pdf进行检索。 28 | - 调研并尝试实现其他类型的文档的向量化存储功能。 29 | - 优化代码结构和逻辑 30 | - 听说`langchain` 这个框架实现RAG不如自己写个RAG(看情况尝试不用`langchain`实现简单RAG -------------------------------------------------------------------------------- /rfcs/work_report/zzh-work-report-2024.12.md: -------------------------------------------------------------------------------- 1 | **实习工作月报表(2024年12月)** 2 | **实习生:张子涵** 3 | 4 | ##### 实习内容 5 | 6 | 编写处理`adoc` 文档的代码,将`riscv-isa-manual` 中内容转为纯文本,以及在所给服务器搭建`Elasticsearch` 服务 7 | 8 | ##### 文档的编写 9 | 10 | 编写代码环境搭建文档以及ES搭建文档,对于过程中遇到的问题也都记录了解决过程以及截图。 11 | 12 | ##### 遇到的问题及解决方案 13 | 14 | **ES环境配置问题** 15 | 16 | **问题**:在ES搭建过程中遇到修改不了系统配置文件导致ES无法启动 17 | 18 | **解决**:经过排查以及在任同学的协助下解决了服务器问题,并成功启动服务 19 | 20 | ##### 下月工作计划 21 | 22 | 完善文档处理流程并开发检索功能 23 | 24 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/__init__.py -------------------------------------------------------------------------------- /src/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/__init__.py -------------------------------------------------------------------------------- /src/client/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/database/__init__.py -------------------------------------------------------------------------------- /src/client/database/elasticsearch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/database/elasticsearch/__init__.py -------------------------------------------------------------------------------- /src/client/database/elasticsearch/es_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import sys 4 | 5 | from elasticsearch import Elasticsearch,exceptions 6 | # 获取当前脚本的绝对路径 7 | current_script_path = os.path.abspath(__file__) 8 | 9 | # 将项目根目录添加到sys.path 10 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 11 | root_dir = os.path.dirname(root_dir) 12 | sys.path.append(root_dir) 13 | from src.utils.log_handler import debug_logger 14 | from src.configs.configs import ES_USER, ES_PASSWORD, ES_URL, ES_INDEX_NAME 15 | from langchain_elasticsearch import ElasticsearchStore 16 | 17 | 18 | 19 | class ESClient: 20 | def __init__(self): 21 | try: 22 | es_client = Elasticsearch( 23 | hosts=[ES_URL], 24 | basic_auth=(ES_USER, ES_PASSWORD), 25 | verify_certs=False, 26 | ssl_show_warn=False, 27 | retry_on_timeout=True, 28 | max_retries=3, 29 | timeout=30 30 | ) 31 | 32 | # 初始化 ElasticsearchStore 33 | self.es_store = ElasticsearchStore( 34 | es_connection=es_client, 35 | index_name=ES_INDEX_NAME, 36 | strategy=ElasticsearchStore.BM25RetrievalStrategy() 37 | ) 38 | 39 | debug_logger.info(f"Init ElasticSearchStore with index_name: {ES_INDEX_NAME}") 40 | except exceptions.ConnectionError as e: 41 | debug_logger.error(f"ES connection error: {e}") 42 | raise 43 | except exceptions.AuthenticationException as e: 44 | debug_logger.error(f"ES authentication failed: {e}") 45 | raise 46 | except Exception as e: 47 | debug_logger.error(f"Unexpected error initializing ES client: {e}") 48 | raise 49 | 50 | def delete(self, docs_ids): 51 | try: 52 | res = self.es_store.delete(docs_ids, timeout=60) 53 | debug_logger.info(f"Delete ES document with number: {len(docs_ids)}, {docs_ids[0]}, res: {res}") 54 | except Exception as e: 55 | debug_logger.error(f"Delete ES document failed with error: {e}") 56 | 57 | def delete_files(self, file_ids, file_chunks): 58 | docs_ids = [] 59 | for file_id, file_chunk in zip(file_ids, file_chunks): 60 | # doc_id 是file_id + '_' + i,其中i是range(file_chunk) 61 | docs_ids.extend([file_id + '_' + str(i) for i in range(file_chunk)]) 62 | if docs_ids: 63 | self.delete(docs_ids) 64 | 65 | # async def main(): 66 | # es_client = ESClient() 67 | # filter = [{"terms": {"metadata.kb_id.keyword": ["KBbf9488a498cf4407a6abdf477208c3ed"]}}] 68 | # es_sub_docs = await es_client.es_store.asimilarity_search("路上只我一个人", top_k=10, filter=filter) 69 | # print(es_sub_docs) 70 | 71 | # asyncio.run(main()) -------------------------------------------------------------------------------- /src/client/database/milvus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/database/milvus/__init__.py -------------------------------------------------------------------------------- /src/client/database/mysql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/database/mysql/__init__.py -------------------------------------------------------------------------------- /src/client/embedding/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/embedding/__init__.py -------------------------------------------------------------------------------- /src/client/embedding/embedding_client.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | import time 5 | # 获取当前脚本的绝对路径 6 | current_script_path = os.path.abspath(__file__) 7 | 8 | # 将项目根目录添加到sys.path 9 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 10 | 11 | sys.path.append(root_dir) 12 | from typing import List 13 | from src.utils.log_handler import debug_logger, embed_logger 14 | from src.utils.general_utils import get_time_async, get_time 15 | from langchain_core.embeddings import Embeddings 16 | from src.configs.configs import LOCAL_EMBED_SERVICE_URL, LOCAL_RERANK_BATCH 17 | import traceback 18 | import aiohttp 19 | import asyncio 20 | import requests 21 | 22 | # 清除多余换行以及以![figure]和![equation]起始的行 23 | def _process_query(query): 24 | return '\n'.join([line for line in query.split('\n') if 25 | not line.strip().startswith('![figure]') and 26 | not line.strip().startswith('![equation]')]) 27 | 28 | 29 | class SBIEmbeddings(Embeddings): 30 | # 初始化请求embedding服务的url 31 | def __init__(self): 32 | self.url = f"http://{LOCAL_EMBED_SERVICE_URL}/embedding" 33 | self.session = requests.Session() 34 | super().__init__() 35 | # 异步向embedding服务请求获取文本的向量 36 | async def _get_embedding_async(self, session, texts): 37 | # 去除多余换行和特殊标记 38 | data = {'texts': [_process_query(text) for text in texts]} 39 | async with session.post(self.url, json=data) as response: 40 | return await response.json() 41 | 42 | @get_time_async 43 | async def aembed_documents(self, texts: List[str]) -> List[List[float]]: 44 | # 设置批量大小 45 | batch_size = LOCAL_RERANK_BATCH 46 | # 向上取整 47 | embed_logger.info(f'embedding texts number: {len(texts) / batch_size}') 48 | all_embeddings = [] 49 | # 分批请求获取文本向量 50 | async with aiohttp.ClientSession() as session: 51 | tasks = [self._get_embedding_async(session, texts[i:i + batch_size]) 52 | for i in range(0, len(texts), batch_size)] 53 | # 收集所有任务结果, 54 | # asyncio.gather 的一个重要特性是:虽然任务是并发执行的,但返回结果时会保持跟任务列表相同的顺序。 55 | # 即使后面的批次先处理完,最终 results 中的顺序仍然与 tasks 列表的顺序一致。 56 | results = await asyncio.gather(*tasks) 57 | # 合并所有任务结果 58 | for result in results: 59 | all_embeddings.extend(result) 60 | debug_logger.info(f'success embedding number: {len(all_embeddings)}') 61 | # 返回结果 62 | return all_embeddings 63 | # 专门用于处理单个查询文本。将单个text转换为列表,因为是单个所以只取第一条embedding向量 64 | async def aembed_query(self, text: str) -> List[float]: 65 | return (await self.aembed_documents([text]))[0] 66 | # 同步方法 67 | def _get_embedding_sync(self, texts): 68 | # 为什么同步去除,异步没去除标记啊,我先都给加上 69 | data = {'texts': [_process_query(text) for text in texts]} 70 | try: 71 | response = self.session.post(self.url, json=data) 72 | response.raise_for_status() 73 | result = response.json() 74 | return result 75 | except Exception as e: 76 | debug_logger.error(f'sync embedding error: {traceback.format_exc()}') 77 | return None 78 | 79 | # @get_time 80 | # 同步方法,列表请求 81 | def embed_documents(self, texts: List[str]) -> List[List[float]]: 82 | return self._get_embedding_sync(texts) 83 | 84 | @get_time 85 | #同步方法,单个请求 86 | def embed_query(self, text: str) -> List[float]: 87 | """Embed query text.""" 88 | # return self._get_embedding([text])['embeddings'][0] 89 | return self._get_embedding_sync([text])[0] 90 | 91 | async def test_async_methods(): 92 | """测试异步方法""" 93 | embedder = SBIEmbeddings() 94 | 95 | # 测试单个文本的embedding 96 | debug_logger.info("\n测试异步单个文本embedding:") 97 | single_text = "人工智能正在改变我们的生活方式。" 98 | single_embedding = await embedder.aembed_query(single_text) 99 | debug_logger.info(f"文本: {single_text}") 100 | debug_logger.info(f"向量维度: {len(single_embedding)}") 101 | 102 | # 测试批量文本的embedding 103 | debug_logger.info("\n测试异步批量文本embedding:") 104 | texts = [ 105 | "深度学习是人工智能的一个重要分支。", 106 | "自然语言处理技术正在不断进步。", 107 | "机器学习算法可以从数据中学习规律。" 108 | ] 109 | 110 | embeddings = await embedder.aembed_documents(texts) 111 | for text, embedding in zip(texts, embeddings): 112 | debug_logger.info(f"文本: {text}") 113 | debug_logger.info(f"向量维度: {len(embedding)}") 114 | 115 | 116 | def test_sync_methods(): 117 | """测试同步方法""" 118 | embedder = SBIEmbeddings() 119 | 120 | # 测试单个文本的embedding 121 | debug_logger.info("\n测试同步单个文本embedding:") 122 | single_text = "这是一个测试文本。" 123 | single_embedding = embedder.embed_query(single_text) 124 | debug_logger.info(f"文本: {single_text}") 125 | debug_logger.info(f"向量维度: {len(single_embedding)}") 126 | 127 | # 测试批量文本的embedding 128 | debug_logger.info("\n测试同步批量文本embedding:") 129 | texts = [ 130 | "第一个测试文本", 131 | "第二个测试文本", 132 | "第三个测试文本" 133 | ] 134 | embeddings = embedder.embed_documents(texts) 135 | for text, embedding in zip(texts, embeddings): 136 | debug_logger.info(f"文本: {text}") 137 | debug_logger.info(f"向量维度: {len(embedding)}") 138 | 139 | 140 | def test_error_handling(): 141 | """测试错误处理""" 142 | embedder = SBIEmbeddings() 143 | 144 | debug_logger.info("\n测试错误处理:") 145 | # 测试空文本 146 | try: 147 | embedding = embedder.embed_query("") 148 | debug_logger.info("空文本处理成功") 149 | except Exception as e: 150 | debug_logger.error(f"空文本处理失败: {str(e)}") 151 | 152 | # 测试None值 153 | try: 154 | embedding = embedder.embed_documents([None]) 155 | debug_logger.info("None值处理成功") 156 | except Exception as e: 157 | debug_logger.error(f"None值处理失败: {str(e)}") 158 | 159 | 160 | async def performance_test(): 161 | """性能测试""" 162 | embedder = SBIEmbeddings() 163 | 164 | debug_logger.info("\n执行性能测试:") 165 | # 准备测试数据 166 | test_sizes = [10, 50, 100] 167 | 168 | for size in test_sizes: 169 | texts = [f"这是第{i}个性能测试文本。" for i in range(size)] 170 | 171 | # 测试同步方法性能 172 | start_time = time.time() 173 | embeddings = embedder.embed_documents(texts) 174 | sync_time = time.time() - start_time 175 | debug_logger.info(f"同步处理 {size} 个文本耗时: {sync_time:.2f}秒") 176 | 177 | # 测试异步方法性能 178 | start_time = time.time() 179 | embeddings = await embedder.aembed_documents(texts) 180 | async_time = time.time() - start_time 181 | debug_logger.info(f"异步处理 {size} 个文本耗时: {async_time:.2f}秒") 182 | 183 | def embed_user_input(user_input: str): 184 | """测试用户输入的文本嵌入""" 185 | embedder = SBIEmbeddings() 186 | 187 | # 对用户输入的文本进行预处理 188 | processed_input = _process_query(user_input) 189 | 190 | debug_logger.info("\n测试用户输入的嵌入:") 191 | debug_logger.info(f"用户输入: {user_input}") 192 | debug_logger.info(f"预处理后的输入: {processed_input}") 193 | 194 | try: 195 | # 使用同步方法获取嵌入向量 196 | embedding = embedder.embed_query(processed_input) 197 | debug_logger.info(f"嵌入向量维度: {len(embedding)}") 198 | debug_logger.info(f"嵌入向量: {embedding}") 199 | except Exception as e: 200 | debug_logger.error(f"嵌入过程中发生错误: {str(e)}") 201 | 202 | return embedding 203 | 204 | 205 | async def main(): 206 | """主测试函数""" 207 | debug_logger.info(f"开始embedding客户端测试...") 208 | 209 | try: 210 | # 测试异步方法 211 | await test_async_methods() 212 | 213 | # 测试同步方法 214 | test_sync_methods() 215 | 216 | # 测试错误处理 217 | test_error_handling() 218 | 219 | # 执行性能测试 220 | await performance_test() 221 | except Exception as e: 222 | debug_logger.error(f"测试过程中发生错误: {str(e)}") 223 | 224 | debug_logger.info("embedding客户端测试完成") 225 | 226 | 227 | if __name__ == "__main__": 228 | asyncio.run(main()) -------------------------------------------------------------------------------- /src/client/llm/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional, List 3 | 4 | 5 | class AnswerResult: 6 | """ 7 | 消息实体 8 | """ 9 | history: List[List[str]] = [] 10 | llm_output: Optional[dict] = None 11 | prompt: str = "" 12 | total_tokens: int = 0 13 | completion_tokens: int = 0 14 | prompt_tokens: int = 0 15 | 16 | 17 | class BaseAnswer(ABC): 18 | """上层业务包装器.用于结果生成统一api调用""" 19 | 20 | @property 21 | @abstractmethod 22 | def _history_len(self) -> int: 23 | """Return _history_len of llm.""" 24 | 25 | @abstractmethod 26 | def set_history_len(self, history_len: int) -> None: 27 | """Return _history_len of llm.""" 28 | 29 | def generatorAnswer(self, prompt: str, 30 | history: List[List[str]] = [], 31 | streaming: bool = False): 32 | pass -------------------------------------------------------------------------------- /src/client/llm/llm_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | import os 4 | 5 | # 获取当前脚本的绝对路径 6 | current_script_path = os.path.abspath(__file__) 7 | 8 | # 将项目根目录添加到sys.path 9 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 10 | 11 | sys.path.append(root_dir) 12 | 13 | from src.utils.general_utils import my_print 14 | import traceback 15 | from openai import OpenAI 16 | from typing import AsyncGenerator, List, Optional 17 | import json 18 | from src.client.llm.base import AnswerResult 19 | from src.utils.log_handler import debug_logger 20 | import tiktoken 21 | from src.configs.configs import DEFAULT_PARENT_CHUNK_SIZE, \ 22 | MAX_CHARS, VECTOR_SEARCH_TOP_K, DEFAULT_API_BASE, DEFAULT_API_KEY,\ 23 | DEFAULT_API_CONTEXT_LENGTH, DEFAULT_MODEL_PATH 24 | 25 | class OpenAILLM: 26 | offcut_token: int = 50 27 | stop_words: Optional[List[str]] = None 28 | 29 | def __init__(self, model, max_token, api_base, api_key, api_context_length, top_p, temperature): 30 | base_url = api_base 31 | api_key = api_key 32 | 33 | if max_token is not None: 34 | self.max_token = max_token 35 | if model is not None: 36 | self.model = model 37 | if api_context_length is not None: 38 | self.token_window = api_context_length 39 | if top_p is not None: 40 | self.top_p = top_p 41 | if temperature is not None: 42 | self.temperature = temperature 43 | self.use_cl100k_base = False 44 | try: 45 | self.tokenizer = tiktoken.encoding_for_model(model) 46 | except Exception as e: 47 | debug_logger.warning(f"{model} not found in tiktoken, using cl100k_base!") 48 | self.tokenizer = tiktoken.get_encoding("cl100k_base") 49 | self.use_cl100k_base = True 50 | 51 | 52 | self.client = OpenAI(base_url=base_url, api_key=api_key) 53 | debug_logger.info(f"OPENAI_API_KEY = {api_key}") 54 | debug_logger.info(f"OPENAI_API_BASE = {base_url}") 55 | debug_logger.info(f"OPENAI_API_MODEL_NAME = {self.model}") 56 | debug_logger.info(f"OPENAI_API_CONTEXT_LENGTH = {self.token_window}") 57 | debug_logger.info(f"OPENAI_API_MAX_TOKEN = {self.max_token}") 58 | debug_logger.info(f"TOP_P = {self.top_p}") 59 | debug_logger.info(f"TEMPERATURE = {self.temperature}") 60 | 61 | @property 62 | def _llm_type(self) -> str: 63 | return "using OpenAI API serve as LLM backend" 64 | 65 | # 定义函数 num_tokens_from_messages,该函数返回由一组消息所使用的token数 66 | def num_tokens_from_messages(self, messages): 67 | total_tokens = 0 68 | for message in messages: 69 | if isinstance(message, dict): 70 | # 对于字典类型的消息,我们假设它包含 'role' 和 'content' 键 71 | for key, value in message.items(): 72 | total_tokens += 3 # role的开销(key的开销) 73 | if isinstance(value, str): 74 | tokens = self.tokenizer.encode(value, disallowed_special=()) 75 | total_tokens += len(tokens) 76 | elif isinstance(message, str): 77 | # 对于字符串类型的消息,直接编码 78 | tokens = self.tokenizer.encode(message, disallowed_special=()) 79 | total_tokens += len(tokens) 80 | else: 81 | raise ValueError(f"Unsupported message type: {type(message)}") 82 | if self.use_cl100k_base: 83 | total_tokens *= 1.2 84 | else: 85 | total_tokens *= 1.1 # 保留一定余量,由于metadata信息的嵌入导致token比计算的会多一些 86 | return int(total_tokens) 87 | 88 | def num_tokens_from_docs(self, docs): 89 | total_tokens = 0 90 | for doc in docs: 91 | # 对每个文本进行分词 92 | tokens = self.tokenizer.encode(doc.page_content, disallowed_special=()) 93 | # 累加tokens数量 94 | total_tokens += len(tokens) 95 | if self.use_cl100k_base: 96 | total_tokens *= 1.2 97 | else: 98 | total_tokens *= 1.1 # 保留一定余量,由于metadata信息的嵌入导致token比计算的会多一些 99 | return int(total_tokens) 100 | 101 | async def _call(self, messages: List[dict], streaming: bool = False) -> AsyncGenerator[str, None]: 102 | try: 103 | 104 | if streaming: 105 | response = self.client.chat.completions.create( 106 | model=self.model, 107 | messages=messages, 108 | stream=True, 109 | max_tokens=self.max_token, 110 | temperature=self.temperature, 111 | top_p=self.top_p, 112 | stop=self.stop_words 113 | ) 114 | for event in response: 115 | if not isinstance(event, dict): 116 | event = event.model_dump() 117 | 118 | if isinstance(event['choices'], List) and len(event['choices']) > 0: 119 | event_text = event["choices"][0]['delta']['content'] 120 | if isinstance(event_text, str) and event_text != "": 121 | delta = {'answer': event_text} 122 | yield "data: " + json.dumps(delta, ensure_ascii=False) 123 | 124 | else: 125 | response = self.client.chat.completions.create( 126 | model=self.model, 127 | messages=messages, 128 | stream=False, 129 | max_tokens=self.max_token, 130 | temperature=self.temperature, 131 | top_p=self.top_p, 132 | stop=self.stop_words 133 | ) 134 | 135 | event_text = response.choices[0].message.content if response.choices else "" 136 | delta = {'answer': event_text} 137 | yield "data: " + json.dumps(delta, ensure_ascii=False) 138 | 139 | except Exception as e: 140 | debug_logger.info(f"Error calling OpenAI API: {traceback.format_exc()}") 141 | delta = {'answer': f"{e}"} 142 | yield "data: " + json.dumps(delta, ensure_ascii=False) 143 | 144 | finally: 145 | # debug_logger.info("[debug] try-finally") 146 | yield f"data: [DONE]\n\n" 147 | 148 | async def generatorAnswer(self, prompt: str, 149 | history: List[List[str]] = [], 150 | streaming: bool = False) -> AsyncGenerator[AnswerResult, None]: 151 | 152 | if history is None or len(history) == 0: 153 | history = [[]] 154 | # debug_logger.info(f"history_len: {self.history_len}") 155 | # debug_logger.info(f"prompt: {prompt}") 156 | debug_logger.info(f"prompt tokens: {self.num_tokens_from_messages([{'content': prompt}])}") 157 | # debug_logger.info(f"streaming: {streaming}") 158 | 159 | messages = [] 160 | for pair in history[:-1]: 161 | question, answer = pair 162 | messages.append({"role": "user", "content": question}) 163 | messages.append({"role": "assistant", "content": answer}) 164 | messages.append({"role": "user", "content": prompt}) 165 | # debug_logger.info(messages) 166 | prompt_tokens = self.num_tokens_from_messages(messages) 167 | total_tokens = 0 168 | completion_tokens = 0 169 | 170 | response = self._call(messages, streaming) 171 | complete_answer = "" 172 | async for response_text in response: 173 | if response_text: 174 | chunk_str = response_text[6:] 175 | if not chunk_str.startswith("[DONE]"): 176 | chunk_js = json.loads(chunk_str) 177 | complete_answer += chunk_js["answer"] 178 | completion_tokens = self.num_tokens_from_messages([complete_answer]) 179 | total_tokens = prompt_tokens + completion_tokens 180 | 181 | history[-1] = [prompt, complete_answer] 182 | answer_result = AnswerResult() 183 | answer_result.history = history 184 | answer_result.llm_output = {"answer": response_text} 185 | answer_result.prompt = prompt 186 | answer_result.total_tokens = total_tokens 187 | answer_result.completion_tokens = completion_tokens 188 | answer_result.prompt_tokens = prompt_tokens 189 | yield answer_result 190 | 191 | async def main(): 192 | llm = OpenAILLM(DEFAULT_MODEL_PATH, 8000, DEFAULT_API_BASE, DEFAULT_API_KEY, DEFAULT_API_CONTEXT_LENGTH, 0.5, 0.5) 193 | streaming = False 194 | chat_history = [] 195 | prompt = """参考信息: 196 | 中央纪委国家监委网站讯 据山西省纪委监委消息:山西转型综合改革示范区党工委副书记、管委会副主任董良涉嫌严重违纪违法,目前正接受山西省纪委监委纪律审查和监察调查。\\u3000\\u3000董良简历\\u3000\\u3000董良,男,汉族,1964年8月生,河南鹿邑人,在职研究生学历,邮箱random@xxx.com,联系电话131xxxxx909,1984年3月加入中国共产党,1984年8月参加工作\\u3000\\u3000历任太原经济技术开发区管委会副主任、太原武宿综合保税区专职副主任,山西转型综合改革示范区党工委委员、管委会副主任。2021年8月,任山西转型综合改革示范区党工委副书记、管委会副主任。(山西省纪委监委) 197 | --- 198 | 我的问题或指令: 199 | 帮我提取上述人物的中文名,英文名,性别,国籍,现任职位,最高学历,毕业院校,邮箱,电话 200 | --- 201 | 请根据上述参考信息回答我的问题或回复我的指令。前面的参考信息可能有用,也可能没用,你需要从我给出的参考信息中选出与我的问题最相关的那些,来为你的回答提供依据。回答一定要忠于原文,简洁但不丢信息,不要胡乱编造。我的问题或指令是什么语种,你就用什么语种回复, 202 | 你的回复:""" 203 | final_result = "TEST OUTPUT: " 204 | async for answer_result in llm.generatorAnswer(prompt=prompt, history=chat_history, streaming=streaming): 205 | resp = answer_result.llm_output["answer"] 206 | # resp结构,是一个str data: {"answer": "良,男,汉族,1964年8月生"} 207 | if "DONE" not in resp: 208 | final_result += json.loads(resp[6:])["answer"] 209 | debug_logger.info(resp) 210 | 211 | debug_logger.info(f"final_result = {final_result}") 212 | return final_result 213 | 214 | 215 | if __name__ == "__main__": 216 | result = asyncio.run(main()) 217 | print(result) 218 | -------------------------------------------------------------------------------- /src/client/rerank/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/client/rerank/__init__.py -------------------------------------------------------------------------------- /src/client/rerank/client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # 获取当前脚本的绝对路径 5 | current_script_path = os.path.abspath(__file__) 6 | 7 | # 将项目根目录添加到sys.path 8 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 9 | 10 | sys.path.append(root_dir) 11 | 12 | from typing import List 13 | from src.utils.log_handler import debug_logger 14 | from src.utils.general_utils import get_time_async, get_time 15 | from src.configs.configs import LOCAL_RERANK_BATCH,LOCAL_RERANK_SERVICE_URL 16 | from langchain.schema import Document 17 | import traceback 18 | import aiohttp 19 | import asyncio 20 | import requests 21 | 22 | 23 | class SBIRerank: 24 | def __init__(self): 25 | """初始化重排序客户端""" 26 | self.url = f"http://{LOCAL_RERANK_SERVICE_URL}/rerank" 27 | # 不支持异步的session 28 | self.session = requests.Session() 29 | 30 | async def _get_rerank_async(self, query: str, passages: List[str]) -> List[float]: 31 | """异步请求重排序服务""" 32 | data = {'query': query, 'passages': passages} 33 | try: 34 | async with aiohttp.ClientSession() as session: 35 | async with session.post(self.url, json=data) as response: 36 | return await response.json() 37 | except Exception as e: 38 | debug_logger.error(f'async rerank error: {traceback.format_exc()}') 39 | return [0.0] * len(passages) 40 | 41 | @get_time_async 42 | async def arerank_documents(self, query: str, source_documents: List[Document]) -> List[Document]: 43 | """Embed search docs using async calls, maintaining the original order.""" 44 | batch_size = LOCAL_RERANK_BATCH # 增大客户端批处理大小 45 | all_scores = [0 for _ in range(len(source_documents))] 46 | passages = [doc.page_content for doc in source_documents] 47 | 48 | tasks = [] 49 | for i in range(0, len(passages), batch_size): 50 | task = asyncio.create_task(self._get_rerank_async(query, passages[i:i + batch_size])) 51 | tasks.append((i, task)) 52 | 53 | for start_index, task in tasks: 54 | res = await task 55 | if res is None: 56 | return source_documents 57 | all_scores[start_index:start_index + batch_size] = res 58 | 59 | for idx, score in enumerate(all_scores): 60 | source_documents[idx].metadata['score'] = round(float(score), 2) 61 | source_documents = sorted(source_documents, key=lambda x: x.metadata['score'], reverse=True) 62 | 63 | return source_documents 64 | 65 | 66 | 67 | #使用示例 68 | async def main(): 69 | reranker = SBIRerank() 70 | query = "什么是人工智能" 71 | documents = [Document(page_content="阿爸巴sss啊啊啊啊s巴爸爸"), 72 | Document(page_content="AI技术在各领域广泛应用"), 73 | Document(page_content="机器学习是AI的核心技术。"), 74 | Document(page_content="人工智能是计算机科学的一个分支。")] # 示例文档 75 | reranked_docs = await reranker.arerank_documents(query, documents) 76 | return reranked_docs 77 | 78 | 79 | # 运行异步主函数 80 | if __name__ == "__main__": 81 | reranked_docs = asyncio.run(main()) 82 | print(reranked_docs) -------------------------------------------------------------------------------- /src/client/rerank/test.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from transformers import AutoTokenizer, AutoModelForSequenceClassification 3 | # your query and corresponding passages 4 | query = '什么是人工智能' 5 | passages = ['阿爸巴sss啊啊啊啊s巴爸爸', 'AI技术在各领域广泛应用', 6 | '机器学习是AI的核心技术。', 7 | '人工智能是计算机科学的一个分支。'] 8 | 9 | # construct sentence pairs 10 | sentence_pairs = [[query, passage] for passage in passages] 11 | # init model and tokenizer 12 | tokenizer = AutoTokenizer.from_pretrained('maidalun1020/bce-reranker-base_v1') 13 | model = AutoModelForSequenceClassification.from_pretrained('maidalun1020/bce-reranker-base_v1') 14 | 15 | device = 'cuda' # if no GPU, set "cpu" 16 | model.to(device) 17 | 18 | # get inputs 19 | inputs = tokenizer(sentence_pairs, padding=True, truncation=True, max_length=512, return_tensors="pt") 20 | inputs_on_device = {k: v.to(device) for k, v in inputs.items()} 21 | 22 | # calculate scores 23 | logits = model(**inputs_on_device, return_dict=True).logits 24 | print(logits.shape) 25 | scores = logits.view(-1,).float() 26 | scores = torch.sigmoid(scores) 27 | print(scores) -------------------------------------------------------------------------------- /src/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/configs/__init__.py -------------------------------------------------------------------------------- /src/core/chains/condense_q_chain.py: -------------------------------------------------------------------------------- 1 | from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder 2 | from langchain.schema import StrOutputParser 3 | from langchain_openai import ChatOpenAI 4 | 5 | 6 | class RewriteQuestionChain: 7 | def __init__(self, model_name, openai_api_key, openai_api_base): 8 | self.chat_model = ChatOpenAI(model_name=model_name, openai_api_key=openai_api_key, openai_api_base=openai_api_base, 9 | temperature=0, model_kwargs={"top_p": 0.01, "seed": 1234}) 10 | self.condense_q_system_prompt = """ 11 | 假设你是极其专业的英语和汉语语言专家。你的任务是:给定一个聊天历史记录和一个可能涉及此聊天历史的用户最新的问题(新问题),请构造一个不需要聊天历史就能理解的独立且语义完整的问题。 12 | 13 | 你可以假设这个问题是在用户与聊天机器人对话的背景下。 14 | 15 | instructions: 16 | - 请始终记住,你的任务是生成独立问题,而不是直接回答新问题! 17 | - 根据用户的新问题和聊天历史记录,判断新问题是否已经是独立且语义完整的。如果新问题已经独立且完整,直接输出新问题,无需任何改动;否则,你需要对新问题进行改写,使其成为独立问题。 18 | - 确保问题在重新构造前后语种保持一致。 19 | - 确保问题在重新构造前后意思保持一致。 20 | - 在构建独立问题时,尽可能将代词(如"她"、"他们"、"它"等)替换为聊天历史记录中对应的具体的名词或实体引用,以提高问题的明确性和易理解性。 21 | 22 | ``` 23 | Example input: 24 | HumanMessage: `北京明天出门需要带伞吗?` 25 | AIMessage: `今天北京的天气是全天阴,气温19摄氏度到27摄氏度,因此不需要带伞噢。` 26 | 新问题: `那后天呢?` # 问题与上文有关,不独立且语义不完整,需要改写 27 | Example output: `北京后天出门需要带伞吗?` # 根据聊天历史改写新问题,使其独立 28 | 29 | Example input: 30 | HumanMessage: `明天北京的天气是多云转晴,适合出门野炊吗?` 31 | AIMessage: `当然可以,这样的天气非常适合出门野炊呢!不过在出门前最好还是要做好防晒措施噢~` 32 | 新问题: `那北京哪里适合野炊呢?` # 问题已经是独立且语义完整的,不需要改写 33 | Example output: `那北京哪里适合野炊呢?` # 直接返回新问题,不需要改写 34 | ``` 35 | 36 | """ 37 | self.condense_q_prompt = ChatPromptTemplate.from_messages( 38 | [ 39 | ("system", self.condense_q_system_prompt), 40 | MessagesPlaceholder(variable_name="chat_history"), 41 | ("human", "新问题:{question}\n请构造不需要聊天历史就能理解的独立且语义完整的问题。\n独立问题:"), 42 | ] 43 | ) 44 | 45 | self.condense_q_chain = self.condense_q_prompt | self.chat_model | StrOutputParser() 46 | -------------------------------------------------------------------------------- /src/core/file_handler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/__init__.py -------------------------------------------------------------------------------- /src/core/file_handler/parser/adoc_parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/parser/adoc_parser.py -------------------------------------------------------------------------------- /src/core/file_handler/parser/markdown_parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/parser/markdown_parser.py -------------------------------------------------------------------------------- /src/core/file_handler/parser/pdf_parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/parser/pdf_parser.py -------------------------------------------------------------------------------- /src/core/file_handler/test/RAG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/test/RAG.png -------------------------------------------------------------------------------- /src/core/file_handler/test/readme.md: -------------------------------------------------------------------------------- 1 | # Agent 2 | RustSBI Specialized Domain Knowledge Quiz LLM 3 | 4 | ## Demos 5 | 6 | [GPTs Demo 入口](https://chatgpt.com/g/g-ubp707Cke-rustsbi-development-expert-test) 7 | 8 | [QAnything Demo 入口](https://ai.youdao.com/saas/qanything/#/bots/129B009D611B4051/share) 9 | 10 | ## Domain Knowledge Sources 11 | 12 | > Sources of Knowledge in the Knowledge Base. Our agent's domain knowledge will be extracted from the documents listed below. 13 | 14 | Rust Documentation: https://doc.rust-lang.org/stable/std/ 15 | 16 | RISC-V Documentation: https://github.com/riscv/riscv-isa-manual 17 | 18 | Bouffalo Docs: https://github.com/bouffalolab/bl_docs 19 | 20 | Bouffalo SDK: https://github.com/bouffalolab/bouffalo_sdk 21 | 22 | Xiangshan Docs: https://github.com/openxiangshan/xiangshan 23 | 24 | 25 | 26 | ## LLM Prompt Engineering 27 | 28 | > Introduction to Prompt Engineering, including documentation and high-quality articles. 29 | 30 | Prompt Engineering Guide: https://www.promptingguide.ai/zh 31 | 32 | Prompt Engineering Tools: https://learnprompting.org/docs/tooling/tools 33 | 34 | 35 | 36 | ## RAG Engineering 37 | 38 | > Introduction to RAG Engineering, including documentation and high-quality articles. 39 | 40 | RAG Beginner's Guide (Chinese): https://53ai.com/news/RAG/2024081636147.html 41 | 42 | RAG Beginner's Guide (English): https://www.singlestore.com/blog/a-guide-to-retrieval-augmented-generation-rag/ 43 | 44 | 45 | 46 | > Existing projects about RAG. 47 | 48 | Easy-RAG: [A RAG system implementation](https://github.com/yuntianhe2014/Easy-RAG) 49 | 50 | RAGFlow: [Another RAG implementation](https://github.com/infiniflow/ragflow) 51 | 52 | WikiChat: [A LLM system with knowledge from Wikipedia](https://github.com/stanford-oval/WikiChat) 53 | -------------------------------------------------------------------------------- /src/core/file_handler/test/std-Rust.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/core/file_handler/test/std-Rust.pdf -------------------------------------------------------------------------------- /src/core/file_handler/test/test_file_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # change rootpath to current path 5 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 6 | print(os.getcwd()) 7 | current_path = os.path.abspath(__file__) 8 | 9 | # 获取当前脚本的父目录的父目录 10 | parent_directory = os.path.dirname(os.path.dirname(current_path)) 11 | 12 | sys.path.append(parent_directory) 13 | 14 | from file_handler import FileHandler 15 | 16 | # readme.md std-Rust.pdf RAG.png 开题报告规范化要求 17 | 18 | file_path = "std-Rust" 19 | 20 | # https://doc.rust-lang.org/stable/std/ 21 | url_path = "https://doc.rust-lang.org/stable/std/" 22 | 23 | # Check if the file exists 24 | 25 | if os.path.exists(file_path): 26 | print(f"文件 {file_path} 存在。") 27 | else: 28 | print(f"文件 {file_path} 不存在。") 29 | 30 | # print(FileHandler.load_docx(file_path)) 31 | 32 | print(FileHandler.load_url(url_path)) 33 | -------------------------------------------------------------------------------- /src/core/retriever/retriever.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import os 4 | import sys 5 | from typing import List 6 | current_script_path = os.path.abspath(__file__) 7 | # 将项目根目录添加到sys.path 8 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_script_path))) 9 | root_dir = os.path.dirname(root_dir) 10 | sys.path.append(root_dir) 11 | import time 12 | from src.utils.log_handler import debug_logger 13 | from src.client.database.milvus.milvus_client import MilvusClient 14 | from src.client.database.elasticsearch.es_client import ESClient 15 | 16 | class Retriever: 17 | async def get_retrieved_documents(self, query: str, vector_store: MilvusClient, es_store: ESClient, partition_keys: List[str], time_record: dict, 18 | hybrid_search: bool, top_k: int, expr: str = None): 19 | milvus_start_time = time.perf_counter() 20 | # TODO 把milvus搜索转为Document类型 21 | query_docs = vector_store.search_docs(query, expr, top_k, partition_keys) 22 | for doc in query_docs: 23 | doc.metadata['retrieval_source'] = 'milvus' 24 | milvus_end_time = time.perf_counter() 25 | time_record['retriever_search_by_milvus'] = round(milvus_end_time - milvus_start_time, 2) 26 | 27 | if not hybrid_search: 28 | return query_docs 29 | try: 30 | filter = [{"terms": {"metadata.kb_id.keyword": partition_keys}}] 31 | es_sub_docs = await es_store.asimilarity_search(query, k=top_k, filter=filter) 32 | print(es_sub_docs) 33 | for doc in es_sub_docs: 34 | doc.metadata['retrieval_source'] = 'es' 35 | time_record['retriever_search_by_es'] = round(time.perf_counter() - milvus_end_time, 2) 36 | debug_logger.info(f"Got {len(query_docs)} documents from vectorstore and {len(es_sub_docs)} documents from es, total {len(query_docs) + len(es_sub_docs)} merged documents.") 37 | query_docs.extend(es_sub_docs) 38 | except Exception as e: 39 | debug_logger.error(f"Error in get_retrieved_documents on es_search: {e}") 40 | return query_docs -------------------------------------------------------------------------------- /src/evaluation/BasicEval/file/riscv-privileged.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/evaluation/BasicEval/file/riscv-privileged.pdf -------------------------------------------------------------------------------- /src/evaluation/BasicEval/file/test01.txt: -------------------------------------------------------------------------------- 1 | 荷塘月色 2 | 这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿⑴,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。 3 | 沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁⑵的。路的一旁,是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。 4 | 路上只我一个人,背着手踱⑶着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。 5 | 曲曲折折的荷塘上面,弥望⑷的是田田⑸的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜⑹地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉⑺的流水,遮住了,不能见一些颜色;而叶子却更见风致⑻了。 6 | 月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲⑼上奏着的名曲。 7 | 荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿⑽,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡⑾人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。 8 | 忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好: 9 | 于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。 10 | 可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。 11 | 于是又记起,《西洲曲》里的句子: 12 | 采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。 13 | 今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。 14 | 一九二七年七月,北京清华园。 -------------------------------------------------------------------------------- /src/evaluation/BasicEval/file/test_doc.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/evaluation/BasicEval/file/test_doc.doc -------------------------------------------------------------------------------- /src/evaluation/BasicEval/file/test_docx.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/evaluation/BasicEval/file/test_docx.docx -------------------------------------------------------------------------------- /src/evaluation/BasicEval/file/test_pdf_3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/evaluation/BasicEval/file/test_pdf_3.pdf -------------------------------------------------------------------------------- /src/evaluation/RAGTruth/Statistics_of_eval.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import os 4 | 5 | current_path = os.path.dirname(os.path.abspath(__file__)) 6 | eval_file_path = os.path.join(current_path,"model_response", 'response-internlm3-8b-instruct.jsonl') 7 | 8 | 9 | 10 | def calculate_hallucination(eval_file_path): 11 | # 定义正则表达式,确保匹配独立的单词 12 | yes_pattern = re.compile(r'\byes\b', re.IGNORECASE) 13 | no_pattern = re.compile(r'\bno\b', re.IGNORECASE) 14 | 15 | # 初始化统计变量 16 | hallucination_count = 0 17 | no_hallucination_count = 0 18 | 19 | 20 | # 加载数据 21 | with open(eval_file_path, 'r', encoding='utf-8') as f: 22 | for line in f: 23 | data = json.loads(line) 24 | eval_result = data['eval_result'].lower() 25 | if yes_pattern.search(eval_result): 26 | hallucination_count += 1 27 | elif no_pattern.search(eval_result): 28 | no_hallucination_count += 1 29 | else: 30 | print(f"Unrecognized eval_result: {eval_result}") 31 | 32 | # 输出统计结果 33 | print(f"Hallucination count: {hallucination_count}") 34 | print(f"No hallucination count: {no_hallucination_count}") 35 | 36 | def extract_pure_eval_and_calculate(eval_file_path): 37 | # 定义正则表达式,用于去除 包裹的内容 38 | think_pattern = re.compile(r'.*?', re.DOTALL) 39 | yes_pattern = re.compile(r'\byes\b', re.IGNORECASE) 40 | no_pattern = re.compile(r'\bno\b', re.IGNORECASE) 41 | # 初始化存储纯粹答案的列表 42 | pure_evals = [] 43 | hallucination_count = 0 44 | no_hallucination_count = 0 45 | 46 | # 打开并逐行读取 JSONL 文件 47 | with open(eval_file_path, 'r', encoding='utf-8') as file: 48 | for line in file: 49 | # 解析每一行为 JSON 对象 50 | item = json.loads(line) 51 | eval_result = item.get('eval_result', '') 52 | 53 | # 使用正则表达式去除 包裹的内容 54 | pure_eval = think_pattern.sub('', eval_result).strip() 55 | # print(pure_eval) 56 | # print('-'*40) 57 | # 将纯粹的答案添加到列表中 58 | pure_evals.append(pure_eval) 59 | if pure_eval == 'yes': 60 | hallucination_count += 1 61 | elif pure_eval == 'no': 62 | no_hallucination_count += 1 63 | elif yes_pattern.search(pure_eval): 64 | hallucination_count += 1 65 | elif no_pattern.search(pure_eval): 66 | no_hallucination_count += 1 67 | else: 68 | print(f"Unrecognized eval_result: {pure_eval}") 69 | print(f"Hallucination count: {hallucination_count}") 70 | print(f"No hallucination count: {no_hallucination_count}") 71 | print(f"Hallucinatioin rate: {(hallucination_count / (hallucination_count + no_hallucination_count)):.4f}") 72 | # 返回纯粹的答案列表 73 | return pure_evals 74 | 75 | 76 | # calculate_hallucination(eval_file_path) 77 | # print('\n\n\n') 78 | extract_pure_eval_and_calculate(eval_file_path) 79 | -------------------------------------------------------------------------------- /src/evaluation/RAGTruth/model_response/response-Qwen2.5-32B-Instruct-AWQ.jsonl: -------------------------------------------------------------------------------- 1 | {"task_type": "Summary", "source_info": "Seventy years ago, Anne Frank died of typhus in a Nazi concentration camp at the age of 15. Just two weeks after her supposed death on March 31, 1945, the Bergen-Belsen concentration camp where she had been imprisoned was liberated -- timing that showed how close the Jewish diarist had been to surviving the Holocaust. But new research released by the Anne Frank House shows that Anne and her older sister, Margot Frank, died at least a month earlier than previously thought. Researchers re-examined archives of the Red Cross, the International Training Service and the Bergen-Belsen Memorial, along with testimonies of survivors. They concluded that Anne and Margot probably did not survive to March 1945 -- contradicting the date of death which had previously been determined by Dutch authorities. In 1944, Anne and seven others hiding in the Amsterdam secret annex were arrested and sent to the Auschwitz-Birkenau concentration camp. Anne Frank's final entry. That same year, Anne and Margot were separated from their mother and sent away to work as slave labor at the Bergen-Belsen camp in Germany. Days at the camp were filled with terror and dread, witnesses said. The sisters stayed in a section of the overcrowded camp with no lighting, little water and no latrine. They slept on lice-ridden straw and violent storms shredded the tents, according to the researchers. Like the other prisoners, the sisters endured long hours at roll call. Her classmate, Nannette Blitz, recalled seeing Anne there in December 1944: \"She was no more than a skeleton by then. She was wrapped in a blanket; she couldn't bear to wear her clothes anymore because they were crawling with lice.\" Listen to Anne Frank's friends describe her concentration camp experience. As the Russians advanced further, the Bergen-Belsen concentration camp became even more crowded, bringing more disease. A deadly typhus outbreak caused thousands to die each day. Typhus is an infectious disease caused by lice that breaks out in places with poor hygiene. The disease causes high fever, chills and skin eruptions. \"Because of the lice infesting the bedstraw and her clothes, Anne was exposed to the main carrier of epidemic typhus for an extended period,\" museum researchers wrote. They concluded that it's unlikely the sisters survived until March, because witnesses at the camp said the sisters both had symptoms before February 7. \"Most deaths caused by typhus occur around twelve days after the first symptoms appear,\" wrote authors Erika Prins and Gertjan Broek. The exact dates of death for Anne and Margot remain unclear. Margot died before Anne. \"Anne never gave up hope,\" said Blitz, her friend. \"She was absolutely convinced she would survive.\" Her diary endures as one of the world's most popular books. Read more about Anne Frank's cousin, a keeper of her legacy.\n", "response": "Seventy years ago, Anne Frank died of typhus in a Nazi concentration camp at 15. Recent research by the Anne Frank House suggests that Anne and her sister, Margot, died at least a month earlier than the previously thought date of March 31, 1945. The sisters were imprisoned in the Bergen-Belsen camp, where they faced terrible conditions. With the advance of Russian forces, the camp became more crowded, leading to a typhus outbreak. This new research re-examines various archives and survivor testimonies, concluding that the sisters likely died before February 7, due to the 12-day period between typhus onset and death."} 2 | -------------------------------------------------------------------------------- /src/evaluation/RAGTruth/ragtruth_eval.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import sys 4 | import os 5 | import requests 6 | import time 7 | import traceback 8 | import asyncio 9 | from typing import List, Tuple 10 | # 获取当前脚本的绝对路径 11 | current_dir_path = os.path.dirname(os.path.abspath(__file__)) 12 | current_script_path = os.path.abspath(__file__) 13 | 14 | # 将项目根目录添加到sys.path 15 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 16 | 17 | sys.path.append(root_dir) 18 | from src.utils.general_utils import my_print 19 | import traceback 20 | from openai import OpenAI 21 | from typing import AsyncGenerator, List, Optional 22 | import json 23 | from src.client.llm.base import AnswerResult 24 | from src.utils.log_handler import debug_logger 25 | import tiktoken 26 | from src.configs.configs import DEFAULT_PARENT_CHUNK_SIZE, \ 27 | MAX_CHARS, VECTOR_SEARCH_TOP_K, DEFAULT_API_BASE, DEFAULT_API_KEY,\ 28 | DEFAULT_API_CONTEXT_LENGTH, DEFAULT_MODEL_PATH, DEFAULT_MODEL_NAME 29 | from src.client.llm.llm_client import OpenAILLM 30 | 31 | llm = OpenAILLM(DEFAULT_MODEL_PATH, 8000,DEFAULT_API_BASE, DEFAULT_API_KEY, DEFAULT_API_CONTEXT_LENGTH, 0.99, 0.7 ) 32 | 33 | MAX_rounds = 100 34 | streaming = False 35 | source_info_path = os.path.join(current_dir_path, "dataset", "source_info.jsonl") 36 | OLLAMA_MODEL_NAME = "deepseek-r1:8b" 37 | OLLAMA_API_BASE = "http://localhost:11434/api/generate" 38 | 39 | async def build_response(file_path): 40 | documents = [] 41 | 42 | QA_cnt, sum_cnt, d2t_cnt = 0, 0, 0 43 | response_list = [] 44 | 45 | with open(file_path, "r", encoding="utf-8") as f: 46 | for i, line in enumerate(f): 47 | print(f"Processing line {i}...") 48 | response = {} 49 | data = json.loads(line) 50 | task_type = data['task_type'] 51 | if task_type == "QA": 52 | if QA_cnt > MAX_rounds: 53 | continue 54 | source_info = data['source_info'] 55 | question = source_info['question'] 56 | QA_cnt += 1 57 | elif task_type == 'Summary': 58 | if sum_cnt > MAX_rounds: 59 | continue 60 | source_info = data['source_info'] 61 | sum_cnt += 1 62 | elif task_type == 'Data2txt': 63 | if d2t_cnt > MAX_rounds: 64 | continue 65 | source_info = data['source_info'] 66 | d2t_cnt += 1 67 | prompt = data['prompt'] 68 | 69 | final_result = "" 70 | chat_history = [] 71 | async for answer_result in llm.generatorAnswer(prompt=prompt, history=chat_history, streaming=streaming): 72 | resp = answer_result.llm_output["answer"] 73 | # resp结构,是一个str data: {"answer": "良,男,汉族,1964年8月生"} 74 | if "DONE" not in resp: 75 | final_result += json.loads(resp[6:])["answer"] 76 | debug_logger.info(resp) 77 | print("source_info:\n") 78 | print(source_info) 79 | print() 80 | print("final_result:\n") 81 | print(final_result) 82 | print('-'*20 + '\n' * 1) 83 | break # 加这个break是为了防止重复输出相同的结果。 84 | response['task_type'] = task_type 85 | response['source_info'] = source_info 86 | response['response'] = final_result 87 | 88 | 89 | response_file_path = os.path.join(current_dir_path,"model_response", f"response-{DEFAULT_MODEL_NAME}.jsonl") 90 | with open(response_file_path, "w", encoding="utf-8") as f: 91 | for response in response_list: 92 | f.write(json.dumps(response, ensure_ascii=False) + "\n") 93 | 94 | debug_logger.info(f"final_result = {final_result}") 95 | 96 | 97 | async def eval_response(): 98 | response_file_path = os.path.join(current_dir_path, "model_response", f"response-{DEFAULT_MODEL_NAME}.jsonl") 99 | prompt_template = f"You are a judge tasked with determining whether text generation contains hallucinations. \ 100 | Based on the given task type, source_info (source information), and response (the generated reply), \ 101 | please judge whether the content of the reply is consistent with the source information.\ 102 | If there is information in the reply that does not match the source_info,\ that is, there is a hallucination, answer \"yes\"; \ 103 | if the content of the reply is completely based on and consistent with the source_info, answer \"no\"." 104 | 105 | hallu_cnt = 0 106 | eval_list = [] 107 | 108 | with open(response_file_path, "r", encoding="utf-8") as f: 109 | for i, line in enumerate(f): 110 | print(f"Processing line {i}...") 111 | response = json.loads(line) 112 | task_type = response['task_type'] 113 | source_info = response['source_info'] 114 | final_result = response['response'] 115 | payload = { 116 | "model": OLLAMA_MODEL_NAME, 117 | "prompt": f"{prompt_template}\n\n##Task type##: {task_type}\n##Source info##: {source_info}\n##Response##: {final_result} \ 118 | Just show me the answer(\"yes\" or \"no\") and delete any other irrelevant text."} 119 | resp = requests.post(OLLAMA_API_BASE, json=payload, stream=True) 120 | result = [] 121 | try: 122 | for line in resp.iter_lines(): 123 | if line: 124 | try: 125 | response_json = json.loads(line.decode('utf-8')) 126 | if 'response' in response_json: 127 | result.append(response_json['response']) 128 | if 'done' in response_json and response_json['done']: 129 | break 130 | except json.JSONDecodeError as e: 131 | print("\nJSON decode error:", e) 132 | continue 133 | except json.JSONDecodeError: 134 | print("Initial JSON decode error: potentially non-stream response") 135 | resp = ''.join(result) 136 | print(resp) 137 | if not resp.startswith("no"): 138 | hallu_cnt += 1 139 | response['eval_result'] = resp 140 | eval_list.append(response) 141 | 142 | eval_file_path = os.path.join(current_dir_path, "eval_files", f"eval-{DEFAULT_MODEL_NAME}.jsonl") 143 | with open(eval_file_path, "w", encoding="utf-8") as f: 144 | for response in eval_list: 145 | f.write(json.dumps(response, ensure_ascii=False) + "\n") 146 | 147 | return result 148 | 149 | async def cal_hallu_cnt(): 150 | hallu_cnt = 0 151 | eval_file_path = os.path.join(current_dir_path, "eval-Qwen2.5-7B-Instruct.jsonl") 152 | with open(eval_file_path, "r", encoding="utf-8") as f: 153 | for i, line in enumerate(f): 154 | response = json.loads(line) 155 | if not response['eval_result'].startswith("no"): 156 | hallu_cnt += 1 157 | print(f"hallu_cnt = {hallu_cnt}") 158 | print(f"hallu_rate: {hallu_cnt / 303}") 159 | 160 | 161 | if __name__ == "__main__": 162 | print(f"Use model {DEFAULT_MODEL_NAME}\n") 163 | result = asyncio.run(build_response(source_info_path)) 164 | # result = asyncio.run(eval_response()) 165 | cal_hallu_cnt() -------------------------------------------------------------------------------- /src/evaluation/test_local_doc_chat.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | from typing import Optional 4 | import logging 5 | from aiohttp import ClientError 6 | from asyncio import TimeoutError 7 | import os 8 | import sys 9 | 10 | # 获取当前脚本的绝对路径 11 | current_script_path = os.path.abspath(__file__) 12 | # 将项目根目录添加到sys.path 13 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_script_path))) 14 | sys.path.append(root_dir) 15 | from src.configs.configs import DEFAULT_API_BASE, DEFAULT_API_KEY 16 | from src.server.api_server.api_client import AsyncHTTPClient 17 | 18 | async def local_doc_chat(question): 19 | async with AsyncHTTPClient(retries=3, timeout=30) as client: 20 | try: 21 | # 模拟请求参数 22 | payload = { 23 | "user_id": "abc1234", 24 | "max_token": 3000, 25 | "user_info": "5678", 26 | "kb_ids": ["KB2ed627becda34af0a85cb1d104d90ebb"], # 替换为实际的知识库ID 27 | "question": question, 28 | "history": [], 29 | "streaming": False, # 设置为False以获取完整回答 30 | "rerank": True, 31 | "custom_prompt": None, 32 | "api_base": DEFAULT_API_BASE, # 替换为实际API地址 33 | "api_key": DEFAULT_API_KEY, # 替换为实际API密钥 34 | # "api_base": "https://api.siliconflow.cn/v1", 35 | # "api_key": "sk-fwkzrhyjznwtubvqhthwklzcekkfemebypavdvtehsprtjni", 36 | "api_context_length": 10000, 37 | "top_p": 0.99, 38 | "temperature": 0.7, 39 | "top_k": 5 40 | } 41 | 42 | # 发送POST请求 43 | response = await client.request( 44 | method="POST", 45 | url="http://127.0.0.1:8777/api/local_doc_qa/local_doc_chat", 46 | json=payload, 47 | headers={"Content-Type": "application/json"} 48 | ) 49 | 50 | # 打印返回结果 51 | print(response) 52 | print('\n') 53 | print(response['question']) 54 | print(response['response']) 55 | 56 | except Exception as e: 57 | logging.error(f"Request to local_doc_chat failed: {str(e)}") 58 | 59 | question = "用简洁的语言对文段进行总结和概括, 并给出文段的主题, 要求字数在100字以内" 60 | question = "概括一下文段的主要内容" 61 | question = "采莲赋的内容是什么" 62 | question = "'莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。'这首词出自哪里 " 63 | question = "这段文字描述的是哪个季节的场景?" 64 | # question = "文中提到的“妖童媛女”在做什么?" 65 | asyncio.run(local_doc_chat(question)) -------------------------------------------------------------------------------- /src/server/api_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/server/api_server/__init__.py -------------------------------------------------------------------------------- /src/server/api_server/api_client.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | from typing import Optional 4 | import logging 5 | from aiohttp import ClientError 6 | from asyncio import TimeoutError 7 | import os 8 | import sys 9 | 10 | # 获取当前脚本的绝对路径 11 | current_script_path = os.path.abspath(__file__) 12 | # 将项目根目录添加到sys.path 13 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 14 | sys.path.append(root_dir) 15 | 16 | from src.configs.configs import DEFAULT_API_BASE, DEFAULT_API_KEY 17 | 18 | class AsyncHTTPClient: 19 | def __init__(self, retries: int = 3, timeout: int = 10): 20 | self.retries = retries 21 | self.timeout = timeout 22 | self.session = None 23 | 24 | async def __aenter__(self): 25 | self.session = aiohttp.ClientSession() 26 | return self 27 | 28 | async def __aexit__(self, exc_type, exc, tb): 29 | if self.session: 30 | await self.session.close() 31 | 32 | async def request( 33 | self, 34 | method: str, 35 | url: str, 36 | **kwargs 37 | ) -> Optional[dict]: 38 | for attempt in range(self.retries): 39 | try: 40 | async with self.session.request( 41 | method, 42 | url, 43 | timeout=self.timeout, 44 | **kwargs 45 | ) as response: 46 | if response.status in (200, 201): 47 | content_type = response.headers.get('Content-Type', '') 48 | 49 | if 'application/json' in content_type: 50 | return await response.json() 51 | elif 'text/plain' in content_type: 52 | return await response.text() # 返回文本 53 | else: 54 | # 尝试智能处理 55 | text = await response.text() 56 | try: 57 | # 尝试解析为JSON 58 | import json 59 | return json.loads(text) 60 | except json.JSONDecodeError: 61 | # 如果不是JSON,返回原文本 62 | return text 63 | 64 | # 服务器错误时重试 65 | if response.status >= 500: 66 | if attempt < self.retries - 1: 67 | await asyncio.sleep(2 ** attempt) 68 | continue 69 | 70 | response.raise_for_status() 71 | 72 | except (ClientError, TimeoutError) as e: 73 | if attempt < self.retries - 1: 74 | logging.warning(f"Attempt {attempt + 1} failed: {str(e)}") 75 | await asyncio.sleep(2 ** attempt) 76 | else: 77 | logging.error(f"All attempts failed for {url}: {str(e)}") 78 | raise 79 | 80 | return None 81 | 82 | async def test_document(): 83 | async with AsyncHTTPClient(retries=3, timeout=10) as client: 84 | try: 85 | data = await client.request( 86 | 'GET', 87 | 'http://127.0.0.1:8777/api/docs' 88 | ) 89 | print(data) 90 | except Exception as e: 91 | logging.error(f"Request failed: {str(e)}") 92 | 93 | async def test_health_check(): 94 | async with AsyncHTTPClient(retries=3, timeout=10) as client: 95 | try: 96 | data = await client.request( 97 | 'GET', 98 | 'http://127.0.0.1:8777/api/health_check' 99 | ) 100 | print(data) 101 | except Exception as e: 102 | logging.error(f"Request failed: {str(e)}") 103 | 104 | async def test_new_knowledge_base(): 105 | async with AsyncHTTPClient(retries=3, timeout=10) as client: 106 | try: 107 | data = await client.request( 108 | 'POST', 109 | 'http://127.0.0.1:8777/api/qa_handler/new_knowledge_base', 110 | json={'user_id': 'abc1234', 'user_info': '5678', 'kb_name': 'zzh', }, 111 | headers={'Content-Type': 'application/json'} 112 | ) 113 | print(data) 114 | except Exception as e: 115 | logging.error(f"Request failed: {str(e)}") 116 | 117 | async def test_upload_files(file_path: str): 118 | async with AsyncHTTPClient(retries=1, timeout=10000) as client: 119 | try: 120 | # 准备文件和表单数据 121 | form_data = aiohttp.FormData() 122 | f = open(file_path, 'rb') 123 | # 添加文件 124 | form_data.add_field('files', 125 | f, 126 | filename=os.path.basename(file_path), 127 | content_type='application/octet-stream') 128 | 129 | # 添加其他字段 130 | form_data.add_field('user_id', 'abc1234') 131 | form_data.add_field('user_info', '5678') 132 | form_data.add_field('kb_id', 'KB2ed627becda34af0a85cb1d104d90ebb') 133 | form_data.add_field('mode', 'soft') 134 | 135 | # 发送请求 136 | data = await client.request( 137 | 'POST', 138 | 'http://127.0.0.1:8777/api/qa_handler/upload_files', 139 | data=form_data 140 | ) 141 | print(data) 142 | except Exception as e: 143 | logging.error(f"Request failed: {str(e)}") 144 | finally: 145 | # 确保关闭所有打开的文件 146 | f.close() 147 | 148 | async def test_local_doc_chat(): 149 | async with AsyncHTTPClient(retries=3, timeout=30) as client: 150 | try: 151 | # 模拟请求参数 152 | payload = { 153 | "user_id": "abc1234", 154 | "max_token": 3000, 155 | "user_info": "5678", 156 | "kb_ids": ["KBbf9488a498cf4407a6abdf477208c3ed"], # 替换为实际的知识库ID 157 | "question": "请问这个知识库的主要内容是什么?", 158 | "history": [], 159 | "streaming": False, # 设置为False以获取完整回答 160 | "rerank": True, 161 | "custom_prompt": None, 162 | "api_base": DEFAULT_API_BASE, # 替换为实际API地址 163 | "api_key": DEFAULT_API_KEY, # 替换为实际API密钥 164 | "api_context_length": 10000, 165 | "top_p": 0.99, 166 | "temperature": 0.7, 167 | "top_k": 5 168 | } 169 | 170 | # 发送POST请求 171 | response = await client.request( 172 | method="POST", 173 | url="http://127.0.0.1:8777/api/local_doc_qa/local_doc_chat", 174 | json=payload, 175 | headers={"Content-Type": "application/json"} 176 | ) 177 | 178 | # 打印返回结果 179 | print("Response from local_doc_chat:") 180 | print(response) 181 | 182 | except Exception as e: 183 | logging.error(f"Request to local_doc_chat failed: {str(e)}") 184 | 185 | def run_test(): 186 | # asyncio.run(test_document()) 187 | # asyncio.run(test_health_check()) 188 | # asyncio.run(test_new_knowledge_base()) 189 | asyncio.run(test_upload_files('./这是一个测试文件.txt')) 190 | asyncio.run(test_local_doc_chat()) 191 | 192 | # asyncio.run(test_upload_files('./这是一个测试文件.txt')) 193 | 194 | if __name__ == "__main__": 195 | run_test() 196 | -------------------------------------------------------------------------------- /src/server/api_server/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QAnything 6 | 7 | 8 | 9 |
广告位招租
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/server/api_server/sanic_api.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | # 获取当前脚本的绝对路径 4 | current_script_path = os.path.abspath(__file__) 5 | 6 | # 获取当前脚本的父目录的路径,即`qanything_server`目录 7 | current_dir = os.path.dirname(current_script_path) 8 | 9 | # 获取`qanything_server`目录的父目录,即`qanything_kernel` 10 | root_dir = os.path.dirname(current_dir) 11 | root_dir = os.path.dirname(root_dir) 12 | root_dir = os.path.dirname(root_dir) 13 | # 将项目根目录添加到sys.path 14 | sys.path.append(root_dir) 15 | from sanic_api_handler import * 16 | from src.core.qa_handler import QAHandler 17 | from src.utils.log_handler import debug_logger, qa_logger 18 | from src.utils.general_utils import my_print 19 | from sanic.worker.manager import WorkerManager 20 | from sanic import Sanic 21 | from sanic_ext import Extend 22 | import time 23 | import argparse 24 | import webbrowser 25 | 26 | WorkerManager.THRESHOLD = 6000 27 | 28 | # 接收外部参数mode 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument('--host', type=str, default='0.0.0.0', help='host') 31 | parser.add_argument('--port', type=int, default=8777, help='port') 32 | parser.add_argument('--workers', type=int, default=1, help='workers') 33 | # 检查是否是local或online,不是则报错 34 | args = parser.parse_args() 35 | 36 | start_time = time.time() 37 | app = Sanic("RustSBI") 38 | app.config.CORS_ORIGINS = "*" 39 | Extend(app) 40 | # 设置请求体最大为 128MB 41 | app.config.REQUEST_MAX_SIZE = 128 * 1024 * 1024 42 | 43 | app.static('/rustsbi/', './dist/', name='rustsbi', index="index.html") 44 | 45 | 46 | @app.before_server_start 47 | async def init_qa_handler(app, loop): 48 | start = time.time() 49 | qa_handler = QAHandler(args.port) 50 | qa_handler.init_cfg(args) 51 | end = time.time() 52 | print(f'init local_doc_qa cost {(end - start):.4f} seconds', flush=True) 53 | app.ctx.qa_handler = qa_handler 54 | 55 | @app.after_server_start 56 | async def notify_server_started(app, loop): 57 | print(f"Server Start Cost {(time.time() - start_time):.4f} seconds", flush=True) 58 | 59 | @app.after_server_start 60 | async def start_server_and_open_browser(app, loop): 61 | try: 62 | print(f"Opening browser at http://{args.host}:{args.port}/rustsbi/") 63 | webbrowser.open(f"http://{args.host}:{args.port}/rustsbi/") 64 | except Exception as e: 65 | # 记录或处理任何异常 66 | print(f"Failed to open browser: {e}") 67 | 68 | # app.add_route(lambda req: response.redirect('/api/docs'), '/') 69 | # tags=["新建知识库"] 70 | app.add_route(document, "/api/docs", methods=['GET']) 71 | app.add_route(health_check, "/api/health_check", methods=['GET']) # tags=["健康检查"] 72 | app.add_route(new_knowledge_base, "/api/qa_handler/new_knowledge_base", methods=['POST']) # tags=["新建知识库"] 73 | app.add_route(upload_files, "/api/qa_handler/upload_files", methods=['POST']) # tags=["上传文件"] 74 | app.add_route(local_doc_chat, "/api/local_doc_qa/local_doc_chat", methods=['POST']) # tags=["问答接口"] 75 | # app.add_route(list_kbs, "/api/local_doc_qa/list_knowledge_base", methods=['POST']) # tags=["知识库列表"] 76 | # app.add_route(list_docs, "/api/local_doc_qa/list_files", methods=['POST']) # tags=["文件列表"] 77 | # app.add_route(get_total_status, "/api/local_doc_qa/get_total_status", methods=['POST']) # tags=["获取所有知识库状态数据库"] 78 | # app.add_route(clean_files_by_status, "/api/local_doc_qa/clean_files_by_status", methods=['POST']) # tags=["清理数据库"] 79 | # app.add_route(delete_docs, "/api/local_doc_qa/delete_files", methods=['POST']) # tags=["删除文件"] 80 | # app.add_route(delete_knowledge_base, "/api/local_doc_qa/delete_knowledge_base", methods=['POST']) # tags=["删除知识库"] 81 | # app.add_route(rename_knowledge_base, "/api/local_doc_qa/rename_knowledge_base", methods=['POST']) # tags=["重命名知识库"] 82 | # app.add_route(get_doc_completed, "/api/local_doc_qa/get_doc_completed", methods=['POST']) # tags=["获取文档完整解析内容"] 83 | # app.add_route(get_user_id, "/api/local_doc_qa/get_user_id", methods=['POST']) # tags=["获取用户ID"] 84 | # app.add_route(get_doc, "/api/local_doc_qa/get_doc", methods=['POST']) # tags=["获取doc详细内容"] 85 | # app.add_route(get_rerank_results, "/api/local_doc_qa/get_rerank_results", methods=['POST']) # tags=["获取rerank结果"] 86 | 87 | if __name__ == "__main__": 88 | app.run(host='0.0.0.0', port=args.port, workers=args.workers, access_log=False) -------------------------------------------------------------------------------- /src/server/api_server/test_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | def truncate_filename(filename, max_length=200): 5 | # 获取文件名后缀 6 | file_ext = os.path.splitext(filename)[1] 7 | 8 | # 获取不带后缀的文件名 9 | file_name_no_ext = os.path.splitext(filename)[0] 10 | 11 | # 计算文件名长度,注意中文字符 12 | filename_length = len(filename.encode('utf-8')) 13 | 14 | # 如果文件名长度超过最大长度限制 15 | if filename_length > max_length: 16 | # 生成一个时间戳标记 17 | timestamp = str(int(time.time())) 18 | # 截取文件名 19 | over_len = (filename_length-max_length)/3 * 3 20 | file_name_no_ext = file_name_no_ext[:-over_len] 21 | print(file_name_no_ext) 22 | new_filename = file_name_no_ext + "_" + timestamp + file_ext 23 | else: 24 | new_filename = filename 25 | 26 | return new_filename 27 | 28 | truncate_filename("我是一个人.txt"*10,max_length=30) -------------------------------------------------------------------------------- /src/server/api_server/这是一个测试文件.txt: -------------------------------------------------------------------------------- 1 | 荷塘月色 2 | 这几天心里颇不宁静。今晚在院子里坐着乘凉,忽然想起日日走过的荷塘,在这满月的光里,总该另有一番样子吧。月亮渐渐地升高了,墙外马路上孩子们的欢笑,已经听不见了;妻在屋里拍着闰儿⑴,迷迷糊糊地哼着眠歌。我悄悄地披了大衫,带上门出去。 3 | 沿着荷塘,是一条曲折的小煤屑路。这是一条幽僻的路;白天也少人走,夜晚更加寂寞。荷塘四面,长着许多树,蓊蓊郁郁⑵的。路的一旁,是些杨柳,和一些不知道名字的树。没有月光的晚上,这路上阴森森的,有些怕人。今晚却很好,虽然月光也还是淡淡的。 4 | 路上只我一个人,背着手踱⑶着。这一片天地好像是我的;我也像超出了平常的自己,到了另一个世界里。我爱热闹,也爱冷静;爱群居,也爱独处。像今晚上,一个人在这苍茫的月下,什么都可以想,什么都可以不想,便觉是个自由的人。白天里一定要做的事,一定要说的话,现在都可不理。这是独处的妙处,我且受用这无边的荷香月色好了。 5 | 曲曲折折的荷塘上面,弥望⑷的是田田⑸的叶子。叶子出水很高,像亭亭的舞女的裙。层层的叶子中间,零星地点缀着些白花,有袅娜⑹地开着的,有羞涩地打着朵儿的;正如一粒粒的明珠,又如碧天里的星星,又如刚出浴的美人。微风过处,送来缕缕清香,仿佛远处高楼上渺茫的歌声似的。这时候叶子与花也有一丝的颤动,像闪电般,霎时传过荷塘的那边去了。叶子本是肩并肩密密地挨着,这便宛然有了一道凝碧的波痕。叶子底下是脉脉⑺的流水,遮住了,不能见一些颜色;而叶子却更见风致⑻了。 6 | 月光如流水一般,静静地泻在这一片叶子和花上。薄薄的青雾浮起在荷塘里。叶子和花仿佛在牛乳中洗过一样;又像笼着轻纱的梦。虽然是满月,天上却有一层淡淡的云,所以不能朗照;但我以为这恰是到了好处——酣眠固不可少,小睡也别有风味的。月光是隔了树照过来的,高处丛生的灌木,落下参差的斑驳的黑影,峭楞楞如鬼一般;弯弯的杨柳的稀疏的倩影,却又像是画在荷叶上。塘中的月色并不均匀;但光与影有着和谐的旋律,如梵婀玲⑼上奏着的名曲。 7 | 荷塘的四面,远远近近,高高低低都是树,而杨柳最多。这些树将一片荷塘重重围住;只在小路一旁,漏着几段空隙,像是特为月光留下的。树色一例是阴阴的,乍看像一团烟雾;但杨柳的丰姿⑽,便在烟雾里也辨得出。树梢上隐隐约约的是一带远山,只有些大意罢了。树缝里也漏着一两点路灯光,没精打采的,是渴睡⑾人的眼。这时候最热闹的,要数树上的蝉声与水里的蛙声;但热闹是它们的,我什么也没有。 8 | 忽然想起采莲的事情来了。采莲是江南的旧俗,似乎很早就有,而六朝时为盛;从诗歌里可以约略知道。采莲的是少年的女子,她们是荡着小船,唱着艳歌去的。采莲人不用说很多,还有看采莲的人。那是一个热闹的季节,也是一个风流的季节。梁元帝《采莲赋》里说得好: 9 | 于是妖童媛女⑿,荡舟心许;鷁首⒀徐回,兼传羽杯⒁;棹⒂将移而藻挂,船欲动而萍开。尔其纤腰束素⒃,迁延顾步⒄;夏始春余,叶嫩花初,恐沾裳而浅笑,畏倾船而敛裾⒅。 10 | 可见当时嬉游的光景了。这真是有趣的事,可惜我们现在早已无福消受了。 11 | 于是又记起,《西洲曲》里的句子: 12 | 采莲南塘秋,莲花过人头;低头弄莲子,莲子清如水。 13 | 今晚若有采莲人,这儿的莲花也算得“过人头”了;只不见一些流水的影子,是不行的。这令我到底惦着江南了。——这样想着,猛一抬头,不觉已是自己的门前;轻轻地推门进去,什么声息也没有,妻已睡熟好久了。 14 | 一九二七年七月,北京清华园。 -------------------------------------------------------------------------------- /src/server/embedding_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/server/embedding_server/__init__.py -------------------------------------------------------------------------------- /src/server/embedding_server/embedding_backend.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from typing import List, Union 4 | from numpy import ndarray 5 | import torch 6 | from torch import Tensor 7 | from onnxruntime import InferenceSession, SessionOptions, GraphOptimizationLevel 8 | from src.configs.configs import LOCAL_EMBED_MODEL_PATH, LOCAL_EMBED_PATH, LOCAL_EMBED_BATCH, LOCAL_RERANK_MAX_LENGTH,EMBED_MODEL_PATH 9 | from src.utils.log_handler import debug_logger 10 | from transformers import AutoTokenizer 11 | 12 | class EmbeddingBackend: 13 | def __init__(self, use_cpu: bool = False): 14 | # 初始化分词器 15 | self._tokenizer = AutoTokenizer.from_pretrained(EMBED_MODEL_PATH) 16 | # 设置返回numpy数组形式 17 | self.return_tensors = "np" 18 | # 批处理大小 19 | self.batch_size = LOCAL_EMBED_BATCH 20 | # 最大文本长度 21 | self.max_length = LOCAL_RERANK_MAX_LENGTH 22 | # 进行onnx会话配置 23 | sess_options = SessionOptions() 24 | sess_options.intra_op_num_threads = 0 25 | sess_options.inter_op_num_threads = 0 26 | sess_options.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL 27 | if use_cpu: 28 | providers = ['CPUExecutionProvider'] 29 | else: 30 | # CUDA优先,如果GPU不可用会自动降级到CPU 31 | providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] 32 | # 这行代码创建了一个ONNX模型的推理会话,是ONNX Runtime的核心组件。 33 | # 路径.onnx为后缀的文件,这是转换自其他深度学习框架(如PyTorch、TensorFlow、Transformer)的模型 34 | self._session = InferenceSession(LOCAL_EMBED_MODEL_PATH, sess_options=sess_options, providers=providers) 35 | debug_logger.info(f"EmbeddingClient: model_path: {LOCAL_EMBED_MODEL_PATH}") 36 | # 获取文本嵌入向量 37 | def get_embedding(self, sentences, max_length): 38 | # 输入文本列表 填充到最长序列 截断超长序列 最大序列长度 返回数组类型,numpy数组 39 | # 文本标记化 40 | inputs_onnx = self._tokenizer(sentences, padding=True, truncation=True, max_length=max_length, return_tensors=self.return_tensors) 41 | # 将tokenizer输出转换为字典形式 42 | # 通常包含 'input_ids', 'attention_mask' 等键值对 43 | inputs_onnx = {k: v for k, v in inputs_onnx.items()} 44 | # 记录开始时间 45 | start_time = time.time() 46 | # 模型推理 47 | outputs_onnx = self._session.run(output_names=['output'], input_feed=inputs_onnx) 48 | debug_logger.info(f"onnx infer time: {time.time() - start_time}") 49 | # outputs_onnx[0]: 获取第一个(也是唯一的)输出 50 | # [:,0]使用numpy切片,选择所有样本的[CLS]标记对应的向量 51 | # BERT类模型中,[CLS]标记被设计用来表示整个序列的语义 52 | # 这个位置的向量包含了整个句子的上下文信息 53 | # 是获取句子级别表示的标准做法 54 | # 假设输入两个句子,模型输出形状为[2, 512, 768] 55 | # outputs_onnx[0] # 形状:[2, 512, 768] 56 | # outputs_onnx[0][:,0] # 形状:[2, 768] 57 | embedding = outputs_onnx[0][:,0] 58 | debug_logger.info(f'embedding shape: {embedding.shape}') 59 | # 计算l2范数 60 | norm_arr = np.linalg.norm(embedding, axis=1, keepdims=True) 61 | # 对向量进行L2归一化,便于后续计算相似度 62 | embeddings_normalized = embedding / norm_arr 63 | # 返回归一化后的向量列表 64 | return embeddings_normalized.tolist() 65 | 66 | # 使用IO Binding的优势: 67 | # 内存效率:减少不必要的数据复制 68 | # 性能提升:更好的内存管理和设备间数据传输 69 | # 更细粒度的控制:可以精确控制输入输出的设备位置 70 | def inference(self, inputs): 71 | outputs_onnx = None 72 | # 最多尝试2次 73 | try_num = 2 74 | while outputs_onnx is None and try_num > 0: 75 | try: 76 | io_binding = self._session.io_binding() 77 | # 绑定输入 78 | for k, v in inputs.items(): 79 | # 将输入数据绑定到CPU内存 80 | io_binding.bind_cpu_input(k, v) 81 | # 确保输入数据同步 82 | io_binding.synchronize_inputs() 83 | # 绑定输出 84 | io_binding.bind_output('output') 85 | # 使用IO binding执行推理 86 | self._session.run_with_iobinding(io_binding) 87 | # 确保输出数据同步 88 | io_binding.synchronize_outputs() 89 | # 确保输出数据同步 90 | outputs_onnx = io_binding.copy_outputs_to_cpu() 91 | io_binding.clear_binding_inputs() 92 | io_binding.clear_binding_outputs() 93 | except: 94 | outputs_onnx = None 95 | try_num -= 1 96 | # 返回结果 97 | return outputs_onnx 98 | 99 | def encode(self, sentence: Union[str, List[str]], 100 | return_numpy: bool = False, 101 | normalize_to_unit: bool = True, 102 | keepdim: bool = True, 103 | batch_size: int = 64, 104 | max_length: int = 384, 105 | tokenizer=None, 106 | return_tokens_num=False, 107 | return_time_log=False) -> Union[ndarray, Tensor]: 108 | 109 | single_sentence = False 110 | if isinstance(sentence, str): 111 | sentence = [sentence] 112 | single_sentence = True 113 | 114 | embedding_list = [] 115 | 116 | tokens_num = 0 117 | using_time_tokenizer = 0 118 | using_time_model = 0 119 | # batch数,向上取整 120 | total_batch = len(sentence) // batch_size + (1 if len(sentence) % batch_size > 0 else 0) 121 | # 开始处理batch 122 | for batch_id in range(total_batch): 123 | start_time_tokenizer = time.time() 124 | # 如果指定了标记化的tokenizer 125 | if tokenizer is not None: 126 | inputs = tokenizer( 127 | sentence[batch_id * batch_size:(batch_id + 1) * batch_size], 128 | padding=True, 129 | truncation=True, 130 | max_length=max_length, 131 | return_tensors="np" 132 | ) 133 | else: 134 | # 如果没指定就用默认的 135 | inputs = self._tokenizer( 136 | sentence[batch_id * batch_size:(batch_id + 1) * batch_size], 137 | padding=True, 138 | truncation=True, 139 | max_length=max_length, 140 | return_tensors="np" 141 | ) 142 | using_time_tokenizer += (time.time() - start_time_tokenizer) 143 | # 这行代码计算实际的token数量,减去了特殊token(如[CLS]和[SEP])的数 144 | if return_tokens_num: 145 | tokens_num += (inputs['attention_mask'].sum().item() - 2 * inputs['attention_mask'].shape[0]) 146 | 147 | inputs = {k: v for k, v in inputs.items()} 148 | 149 | start_time_model = time.time() 150 | # 执行推理 151 | outputs_onnx = self.inference(inputs) 152 | using_time_model += (time.time() - start_time_model) 153 | # 和get_embedding函数一样,看上面注释 154 | embeddings = np.asarray(outputs_onnx[0][:, 0]) 155 | # 如果需要正则化 156 | if normalize_to_unit: 157 | embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True) 158 | embedding_list.append(embeddings) 159 | # # 合并np数组 160 | # embedding_list = [ 161 | # np.array([[1, 2, 3], 162 | # [4, 5, 6]]), # 形状(2, 3)的数组 163 | # np.array([[7, 8, 9], 164 | # [10, 11, 12]]) # 形状(2, 3)的数组 165 | # ] 166 | 167 | # # axis=0 表示在第一个维度(垂直方向)拼接 168 | # result = np.concatenate(embedding_list, axis=0) 169 | # # 结果形状(4, 3): 170 | # # [[1, 2, 3], 171 | # # [4, 5, 6], 172 | # # [7, 8, 9], 173 | # # [10, 11, 12]] 174 | embeddings = np.concatenate(embedding_list, axis=0) 175 | # 当输入是单个句子且不需要保持维度时 176 | # 去掉第一个维度,从2D变为1D 177 | # 例如:从形状(1, 768)变为(768,) 178 | if single_sentence and not keepdim: 179 | embeddings = embeddings[0] 180 | # 如果不需要返回numpy数组且当前是numpy数组 181 | # 将numpy数组转换为PyTorch张量 182 | if not return_numpy and isinstance(embeddings, ndarray): 183 | embeddings = torch.from_numpy(embeddings) 184 | # 同时返回token数量和时间日志 185 | if return_tokens_num and return_time_log: 186 | return embeddings, tokens_num, using_time_tokenizer, using_time_model 187 | # 只返回token数量 188 | elif return_tokens_num: 189 | return embeddings, tokens_num 190 | # 只返回时间日志 191 | elif return_time_log: 192 | return embeddings, using_time_tokenizer, using_time_model 193 | # 只返回嵌入向量 194 | else: 195 | return embeddings 196 | # 对给定queries 197 | def predict(self, queries, return_tokens_num=False): 198 | print(queries) 199 | embeddings = self.encode( 200 | queries, batch_size=self.batch_size, normalize_to_unit=True, return_numpy=True, max_length=self.max_length, 201 | tokenizer=self._tokenizer, 202 | return_tokens_num=return_tokens_num 203 | ) 204 | print(embeddings.shape) 205 | return embeddings.tolist() -------------------------------------------------------------------------------- /src/server/embedding_server/embedding_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # 获取当前脚本的绝对路径 5 | current_script_path = os.path.abspath(__file__) 6 | 7 | # 将项目根目录添加到sys.path 8 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 9 | 10 | sys.path.append(root_dir) 11 | 12 | from sanic import Sanic 13 | from sanic.response import json 14 | from src.server.embedding_server.embedding_backend import EmbeddingBackend 15 | from src.configs.configs import LOCAL_EMBED_MODEL_PATH, LOCAL_EMBED_THREADS 16 | from src.utils.general_utils import get_time_async 17 | import argparse 18 | 19 | # 接收外部参数mode 20 | parser = argparse.ArgumentParser() 21 | # mode必须是local或online 22 | # 使用--use_gpu可以让Embedding模型加载到gpu中 23 | parser.add_argument('--use_gpu', action="store_true", help='use gpu or not') 24 | parser.add_argument('--workers', type=int, default=1, help='workers') 25 | # 检查是否是local或online,不是则报错 26 | args = parser.parse_args() 27 | print("args:", args) 28 | 29 | app = Sanic("embedding_server") 30 | 31 | 32 | @get_time_async 33 | @app.route("/embedding", methods=["POST"]) 34 | async def embedding(request): 35 | data = request.json 36 | texts = data.get('texts') 37 | # print("local embedding texts number:", len(texts), flush=True) 38 | 39 | # onnx_backend: EmbeddingAsyncBackend = request.app.ctx.onnx_backend 40 | # onnx后端上下文在这里使用 41 | onnx_backend: EmbeddingBackend = request.app.ctx.onnx_backend 42 | # result_data = await onnx_backend.embed_documents_async(texts) 43 | result_data = onnx_backend.predict(texts) 44 | # print("local embedding result number:", len(result_data), flush=True) 45 | # print("local embedding result:", result_data, flush=True) 46 | 47 | return json(result_data) 48 | 49 | 50 | @app.listener('before_server_start') 51 | async def setup_onnx_backend(app, loop): 52 | # app.ctx.onnx_backend = EmbeddingAsyncBackend(model_path=LOCAL_EMBED_MODEL_PATH, 53 | # use_cpu=not args.use_gpu, num_threads=LOCAL_EMBED_THREADS) 54 | # onnx_backend 是在应用启动时被初始化并存储在上下文中的对象 55 | # 存储到应用上下文 56 | app.ctx.onnx_backend = EmbeddingBackend(use_cpu=not args.use_gpu) 57 | 58 | 59 | if __name__ == "__main__": 60 | # workers参数指定了服务器启动的工作进程数量。 61 | app.run(host="0.0.0.0", port=9001, workers=args.workers) -------------------------------------------------------------------------------- /src/server/embedding_server/export_onnx.py: -------------------------------------------------------------------------------- 1 | import transformers 2 | from transformers import AutoTokenizer, AutoModel 3 | from pathlib import Path 4 | import torch 5 | 6 | # 1. 加载模型和分词器 7 | model_name = "maidalun1020/bce-embedding-base_v1" # 或其他模型名称 8 | tokenizer = AutoTokenizer.from_pretrained(model_name) 9 | model = AutoModel.from_pretrained(model_name) 10 | 11 | # 2. 创建样本输入 12 | encoded_input = tokenizer("这是一个测试句子", return_tensors="pt") 13 | 14 | # 3. 导出为ONNX 15 | output_path = Path("bce_model/model.onnx") 16 | 17 | torch.onnx.export( 18 | model, # 要导出的模型 19 | tuple(encoded_input.values()), # 模型输入 20 | output_path, # 保存路径 21 | export_params=True, # 存储训练好的参数权重 22 | opset_version=14, # ONNX 算子集版本 23 | do_constant_folding=True, # 是否执行常量折叠优化 24 | input_names=['input_ids', 'attention_mask'], # 输入节点的名称 25 | output_names=['output'], # 输出节点的名称 26 | dynamic_axes={ # 动态尺寸的设置 27 | 'input_ids': {0: 'batch_size', 1: 'sequence'}, 28 | 'attention_mask': {0: 'batch_size', 1: 'sequence'}, 29 | 'output': {0: 'batch_size', 1: 'sequence_length', 2: 'hidden_size'} 30 | } 31 | ) -------------------------------------------------------------------------------- /src/server/embedding_server/start.sh: -------------------------------------------------------------------------------- 1 | nohup python embedding_server.py > record.log 2>&1 & -------------------------------------------------------------------------------- /src/server/llm_server/start.sh: -------------------------------------------------------------------------------- 1 | # 环境变量设置 2 | export NCCL_DEBUG=INFO 3 | export NCCL_IB_DISABLE=1 4 | export NCCL_P2P_DISABLE=1 5 | export NCCL_SHM_DISABLE=1 # 禁用 NCCL 共享内存通信 6 | export CUDA_VISIBLE_DEVICES=0,1 7 | nohup lmdeploy serve api_server --log-level=INFO --server-port 2333 --tp=2 --dtype=float16 /home/zzh/Qwen2-VL-7B-Instruct > record.log 2>&1 & -------------------------------------------------------------------------------- /src/server/llm_server/test_llm_server.py: -------------------------------------------------------------------------------- 1 | from openai import OpenAI 2 | import time # 导入time模块用于时间计算 3 | 4 | client = OpenAI( 5 | api_key='YOUR_API_KEY', 6 | base_url="http://0.0.0.0:2333/v1" 7 | ) 8 | 9 | model_name = client.models.list().data[0].id 10 | print(f"model_list: {client.models.list().data}") 11 | print(f"use model_name: {model_name}") 12 | 13 | # 记录请求发送时间 14 | start_time = time.time() 15 | 16 | response = client.chat.completions.create( 17 | model=model_name, 18 | messages=[ 19 | {"role": "system", "content": "你是一个时间管理助手"}, 20 | {"role": "user", "content": "对于时间管理给出三个建议"}, 21 | ], 22 | temperature=0.8, 23 | top_p=0.8 24 | ) 25 | 26 | # 记录响应返回时间 27 | end_time = time.time() 28 | 29 | # 计算生成response的时间 30 | elapsed_time = end_time - start_time 31 | print(f"\nResponse generated in {elapsed_time:.4f} seconds\n") 32 | 33 | print(response) -------------------------------------------------------------------------------- /src/server/llm_server/vllm_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 环境变量设置 4 | export NCCL_DEBUG=INFO 5 | export NCCL_IB_DISABLE=1 6 | export NCCL_P2P_DISABLE=1 7 | export NCCL_SHM_DISABLE=1 # 禁用 NCCL 共享内存通信 8 | export CUDA_VISIBLE_DEVICES=0,1 # 指定使用 GPU 0 和 GPU 1 9 | export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 # 优化内存分配 10 | 11 | # 模型路径和日志文件路径 12 | # MODEL_NAME="Qwen2.5-14B-Instruct" 13 | MODEL_NAME="Qwen2.5-32B-Instruct-AWQ" 14 | # MODEL_NAME="Qwen2.5-32B-Instruct-GPTQ-Int4" 15 | MODEL_PATH="/home/model/${MODEL_NAME}" 16 | 17 | LOG_FILE="vllm_record.log" 18 | MAX_LOG_SIZE=10485760 # 最大日志文件大小(10MB,单位为字节) 19 | 20 | # 检查日志文件大小并清理 21 | check_and_clean_log() { 22 | if [ -f "${LOG_FILE}" ]; then 23 | LOG_SIZE=$(stat -c%s "${LOG_FILE}") 24 | if [ "${LOG_SIZE}" -ge "${MAX_LOG_SIZE}" ]; then 25 | echo "Log file size exceeded ${MAX_LOG_SIZE} bytes. Cleaning log file..." 26 | > "${LOG_FILE}" # 清空日志文件 27 | fi 28 | fi 29 | } 30 | 31 | # 启动 vLLM 服务 32 | vllm serve \ 33 | ${MODEL_PATH} \ 34 | --served-model-name ${MODEL_PATH} \ 35 | --tensor-parallel-size 2 \ 36 | --dtype float16 \ 37 | --port 2333 \ 38 | --gpu-memory-utilization 0.90 \ 39 | >> ${LOG_FILE} 2>&1 & 40 | 41 | # 每隔一段时间检查日志文件大小并清理 42 | while true; do 43 | check_and_clean_log 44 | sleep 3600 # 每隔 1 小时检查一次日志文件大小 45 | done 46 | 47 | echo "vLLM service started on port 2333. Logs are being written to ${LOG_FILE}" -------------------------------------------------------------------------------- /src/server/rerank_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/server/rerank_server/__init__.py -------------------------------------------------------------------------------- /src/server/rerank_server/export_onnx.py: -------------------------------------------------------------------------------- 1 | import transformers 2 | from transformers import AutoTokenizer, AutoModel,AutoModelForSequenceClassification 3 | 4 | from pathlib import Path 5 | import torch 6 | 7 | # 1. 加载模型和分词器 8 | model_name = "maidalun1020/bce-reranker-base_v1" # 或其他模型名称 9 | tokenizer = AutoTokenizer.from_pretrained(model_name) 10 | model = AutoModelForSequenceClassification.from_pretrained(model_name) 11 | 12 | # 2. 创建样本输入 13 | encoded_input = tokenizer("这是一个测试句子", return_tensors="pt") 14 | 15 | # 3. 导出为ONNX 16 | output_path = Path("bce_model/model.onnx") 17 | 18 | torch.onnx.export( 19 | model, # 要导出的模型 20 | tuple(encoded_input.values()), # 模型输入 21 | output_path, # 保存路径 22 | export_params=True, # 存储训练好的参数权重 23 | opset_version=14, # ONNX 算子集版本 24 | do_constant_folding=True, # 是否执行常量折叠优化 25 | input_names=['input_ids', 'attention_mask'], # 输入节点的名称 26 | output_names=['logits'], # 输出节点的名称 27 | dynamic_axes={ # 动态尺寸的设置 28 | 'input_ids': {0: 'batch_size', 1: 'sequence_length'}, 29 | 'attention_mask': {0: 'batch_size', 1: 'sequence_length'}, 30 | 'logits': {0: 'batch_size'} 31 | } 32 | ) -------------------------------------------------------------------------------- /src/server/rerank_server/rerank_backend.py: -------------------------------------------------------------------------------- 1 | from transformers import AutoTokenizer 2 | from copy import deepcopy 3 | from typing import List 4 | from src.configs.configs import LOCAL_RERANK_MAX_LENGTH, \ 5 | LOCAL_RERANK_BATCH, RERANK_MODEL_PATH, LOCAL_RERANK_THREADS,\ 6 | LOCAL_RERANK_MODEL_PATH 7 | from src.utils.log_handler import debug_logger 8 | from src.utils.general_utils import get_time 9 | import concurrent.futures 10 | import onnxruntime 11 | import numpy as np 12 | 13 | 14 | def sigmoid(x): 15 | x = x.astype('float32') 16 | scores = 1/(1+np.exp(-x)) 17 | scores = np.clip(1.5*(scores-0.5)+0.5, 0, 1) 18 | return scores 19 | 20 | class RerankBackend(): 21 | def __init__(self, use_cpu: bool = False): 22 | self._tokenizer = AutoTokenizer.from_pretrained(RERANK_MODEL_PATH) 23 | self.spe_id = self._tokenizer.sep_token_id 24 | # 设置重叠长度,80,方便记录上下文 25 | self.overlap_tokens = 80 26 | self.batch_size = LOCAL_RERANK_BATCH 27 | self.max_length = LOCAL_RERANK_MAX_LENGTH 28 | self.return_tensors = None 29 | self.workers = LOCAL_RERANK_THREADS 30 | self.use_cpu = use_cpu 31 | self.return_tensors = "np" 32 | # 创建一个ONNX Runtime会话设置,使用GPU执行 33 | sess_options = onnxruntime.SessionOptions() 34 | sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL 35 | sess_options.intra_op_num_threads = 0 36 | sess_options.inter_op_num_threads = 0 37 | if use_cpu: 38 | providers = ['CPUExecutionProvider'] 39 | else: 40 | providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] 41 | self.session = onnxruntime.InferenceSession(LOCAL_RERANK_MODEL_PATH, sess_options, providers=providers) 42 | # 推理 43 | def inference(self, batch): 44 | # 准备输入数据,准备ONNX模型输入 45 | print("开始推理......") 46 | inputs = {self.session.get_inputs()[0].name: batch['input_ids'], 47 | self.session.get_inputs()[1].name: batch['attention_mask']} 48 | # 可选的token_type_ids输入 49 | if 'token_type_ids' in batch: 50 | inputs[self.session.get_inputs()[2].name] = batch['token_type_ids'] 51 | 52 | # 执行推理 输出为logits, None表示获取所有输出 53 | result = self.session.run(None, inputs) 54 | # debug_logger.info(f"rerank result: {result}") 55 | 56 | # 应用sigmoid函数 57 | # sigmoid_scores = 1 / (1 + np.exp(-np.array(result[0]))) 58 | # 为什么是result[0]因为只有一个输出 59 | # 将logits使用sigmoid函数映射到(0,1)区间内 60 | # print("logits shape: ", result[0].shape) 61 | sigmoid_scores = sigmoid(np.array(result[0])) 62 | # 5. 整理输出格式,转换为一维 63 | return sigmoid_scores.reshape(-1).tolist() 64 | 65 | def merge_inputs(self, chunk1_raw, chunk2): 66 | chunk1 = deepcopy(chunk1_raw) 67 | 68 | # 在 chunk1 的末尾添加分隔符 69 | chunk1['input_ids'].append(self.spe_id) 70 | chunk1['attention_mask'].append(1) # 为分隔符添加 attention mask 71 | 72 | # 添加 chunk2 的内容 73 | chunk1['input_ids'].extend(chunk2['input_ids']) 74 | chunk1['attention_mask'].extend(chunk2['attention_mask']) 75 | 76 | # 在整个序列的末尾再添加一个分隔符 77 | chunk1['input_ids'].append(self.spe_id) 78 | chunk1['attention_mask'].append(1) # 为最后的分隔符添加 attention mask 79 | 80 | if 'token_type_ids' in chunk1: 81 | # 为 chunk2 和两个分隔符添加 token_type_ids 82 | token_type_ids = [1 for _ in range(len(chunk2['token_type_ids']) + 2)] 83 | chunk1['token_type_ids'].extend(token_type_ids) 84 | 85 | return chunk1 86 | # 处理长文本重排序的预处理函数,将query和passages转换为模型可以处理的格式。 87 | def tokenize_preproc(self, 88 | query: str, 89 | passages: List[str]): 90 | # 先对query进行编码 91 | query_inputs = self._tokenizer.encode_plus(query, truncation=False, padding=False) 92 | # 计算passage最大长度,减2是因为添加了两个分隔符 93 | max_passage_inputs_length = self.max_length - len(query_inputs['input_ids']) - 2 94 | # 例如: 95 | # self.max_length = 512 96 | # query长度 = 30 97 | # 最大passage长度 = 512 - 30 - 2 = 480 98 | assert max_passage_inputs_length > 10 99 | # 计算重叠token数 100 | # 防止重叠太大,最多是passage最大长度的2/7 101 | overlap_tokens = min(self.overlap_tokens, max_passage_inputs_length * 2 // 7) 102 | 103 | # 组[query, passage]对 104 | merge_inputs = [] 105 | merge_inputs_idxs = [] 106 | for pid, passage in enumerate(passages): 107 | # 对passage进行编码 108 | passage_inputs = self._tokenizer.encode_plus(passage, truncation=False, padding=False, 109 | add_special_tokens=False) 110 | # 编码长度 111 | passage_inputs_length = len(passage_inputs['input_ids']) 112 | # 当passage长度小于最大允许长度时 113 | if passage_inputs_length <= max_passage_inputs_length: 114 | if passage_inputs['attention_mask'] is None or len(passage_inputs['attention_mask']) == 0: 115 | continue 116 | # 直接合并query和passage 117 | qp_merge_inputs = self.merge_inputs(query_inputs, passage_inputs) 118 | merge_inputs.append(qp_merge_inputs) 119 | # print("query ids: ",query_inputs) 120 | # print("passage ids: ",passage_inputs) 121 | # print(query, " " , passage) 122 | # print(qp_merge_inputs) 123 | # 记录原始passage的索引,排序用 124 | merge_inputs_idxs.append(pid) 125 | else: 126 | # 当passage过长时,需要分段处理 127 | start_id = 0 128 | while start_id < passage_inputs_length: 129 | # 切分passage 130 | end_id = start_id + max_passage_inputs_length 131 | # 提取子段 132 | sub_passage_inputs = {k: v[start_id:end_id] for k, v in passage_inputs.items()} 133 | # 计算下一段的开始位置(考虑重叠) 134 | start_id = end_id - overlap_tokens if end_id < passage_inputs_length else end_id 135 | # query和子段合并 136 | qp_merge_inputs = self.merge_inputs(query_inputs, sub_passage_inputs) 137 | # 放入合并后的 138 | merge_inputs.append(qp_merge_inputs) 139 | # 记录原始索引 140 | merge_inputs_idxs.append(pid) 141 | # 返回合并后的输入,和记录的原始索引位置 142 | return merge_inputs, merge_inputs_idxs 143 | 144 | @get_time 145 | def get_rerank(self, query: str, passages: List[str]): 146 | tot_batches, merge_inputs_idxs_sort = self.tokenize_preproc(query, passages) 147 | 148 | tot_scores = [] 149 | with concurrent.futures.ThreadPoolExecutor(max_workers=self.workers) as executor: 150 | futures = [] 151 | for k in range(0, len(tot_batches), self.batch_size): 152 | batch = self._tokenizer.pad( 153 | tot_batches[k:k + self.batch_size], 154 | padding=True, 155 | max_length=None, 156 | pad_to_multiple_of=None, 157 | return_tensors=self.return_tensors 158 | ) 159 | future = executor.submit(self.inference, batch) 160 | futures.append(future) 161 | # debug_logger.info(f'rerank number: {len(futures)}') 162 | for future in futures: 163 | scores = future.result() 164 | # todo 165 | tot_scores.extend(scores) 166 | # print(len(scores)) 167 | # print(scores[:5]) 168 | # 对于被分段的文档,取分段的最高分数 169 | # print("passages len: ", len(passages)) 170 | # print("merge inputs idx sort: ", len(merge_inputs_idxs_sort)) 171 | # print("tot_scores: ", len(tot_scores)) 172 | 173 | merge_tot_scores = [0 for _ in range(len(passages))] 174 | for pid, score in zip(merge_inputs_idxs_sort, tot_scores): 175 | merge_tot_scores[pid] = max(merge_tot_scores[pid], score) 176 | # print("merge_tot_scores:", merge_tot_scores, flush=True) 177 | return merge_tot_scores -------------------------------------------------------------------------------- /src/server/rerank_server/rerank_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | # 获取当前脚本的绝对路径 5 | current_script_path = os.path.abspath(__file__) 6 | 7 | # 将项目根目录添加到sys.path 8 | root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_script_path)))) 9 | 10 | sys.path.append(root_dir) 11 | 12 | from sanic import Sanic 13 | from sanic.response import json 14 | from src.server.rerank_server.rerank_backend import RerankBackend 15 | from src.configs.configs import LOCAL_RERANK_MODEL_PATH, LOCAL_RERANK_THREADS 16 | from src.utils.general_utils import get_time_async 17 | import argparse 18 | 19 | # 接收外部参数mode 20 | parser = argparse.ArgumentParser() 21 | # mode必须是local或online 22 | parser.add_argument('--use_gpu', action="store_true", help='use gpu or not') 23 | parser.add_argument('--workers', type=int, default=1, help='workers') 24 | # 检查是否是local或online,不是则报错 25 | args = parser.parse_args() 26 | print("args:", args) 27 | 28 | app = Sanic("rerank_server") 29 | 30 | 31 | @get_time_async 32 | @app.route("/rerank", methods=["POST"]) 33 | async def rerank(request): 34 | data = request.json 35 | query = data.get('query') 36 | passages = data.get('passages') 37 | # print(query) 38 | # print(passages) 39 | onnx_backend: RerankBackend = request.app.ctx.onnx_backend 40 | # onnx_backend: RerankAsyncBackend = request.app.ctx.onnx_backend 41 | 42 | # result_data = await onnx_backend.get_rerank_async(query, passages) 43 | result_data = onnx_backend.get_rerank(query, passages) 44 | # print("local rerank query:", query, flush=True) 45 | # print("local rerank passages number:", len(passages), flush=True) 46 | 47 | return json(result_data) 48 | 49 | 50 | @app.listener('before_server_start') 51 | async def setup_onnx_backend(app, loop): 52 | # app.ctx.onnx_backend = RerankAsyncBackend(model_path=LOCAL_RERANK_MODEL_PATH, use_cpu=not args.use_gpu, 53 | # num_threads=LOCAL_RERANK_THREADS) 54 | app.ctx.onnx_backend = RerankBackend(use_cpu=not args.use_gpu) 55 | 56 | 57 | if __name__ == "__main__": 58 | app.run(host="0.0.0.0", port=8001, workers=args.workers) -------------------------------------------------------------------------------- /src/server/rerank_server/start.sh: -------------------------------------------------------------------------------- 1 | nohup python rerank_server.py > record.log 2>&1 & -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rustsbi/Agent/e40ef14178b4988ae0a8a5bc34a7edf0b2123b34/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/general_utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import wraps 3 | 4 | import tiktoken # 添加这行导入 5 | from src.utils.log_handler import debug_logger, embed_logger, rerank_logger 6 | from src.configs.configs import DEFAULT_MODEL_PATH, KB_SUFFIX, EMBED_MODEL_PATH, RERANK_MODEL_PATH 7 | from sanic.request import Request 8 | from sanic.exceptions import BadRequest 9 | import logging 10 | import traceback 11 | import re 12 | import mimetypes 13 | import os 14 | import chardet 15 | import inspect 16 | from transformers import AutoTokenizer 17 | 18 | 19 | # 异步执行环境下的耗时统计装饰器 20 | def get_time_async(func): 21 | @wraps(func) 22 | async def get_time_async_inner(*args, **kwargs): 23 | s_time = time.perf_counter() 24 | res = await func(*args, **kwargs) # 注意这里使用 await 来调用异步函数 25 | e_time = time.perf_counter() 26 | if 'embed' in func.__name__: 27 | embed_logger.info('函数 {} 执行耗时: {:.2f} 秒'.format(func.__name__, e_time - s_time)) 28 | elif 'rerank' in func.__name__: 29 | rerank_logger.info('函数 {} 执行耗时: {:.2f} 秒'.format(func.__name__, e_time - s_time)) 30 | else: 31 | debug_logger.info('函数 {} 执行耗时: {:.2f} 毫秒'.format(func.__name__, (e_time - s_time) * 1000)) 32 | return res 33 | 34 | return get_time_async_inner 35 | 36 | # 同步执行环境下的耗时统计装饰器 37 | def get_time(func): 38 | def get_time_inner(*arg, **kwargs): 39 | s_time = time.time() 40 | res = func(*arg, **kwargs) 41 | e_time = time.time() 42 | if 'embed' in func.__name__: 43 | embed_logger.info('函数 {} 执行耗时: {:.2f} 秒'.format(func.__name__, e_time - s_time)) 44 | elif 'rerank' in func.__name__: 45 | rerank_logger.info('函数 {} 执行耗时: {:.2f} 秒'.format(func.__name__, e_time - s_time)) 46 | else: 47 | debug_logger.info('函数 {} 执行耗时: {:.2f} 毫秒'.format(func.__name__, (e_time - s_time) * 1000)) 48 | return res 49 | 50 | return get_time_inner 51 | 52 | def safe_get(req: Request, attr: str, default=None): 53 | """ 54 | 安全地从请求中获取参数值 55 | 56 | 参数: 57 | req: Request - Flask/FastAPI的请求对象 58 | attr: str - 要获取的参数名 59 | default: Any - 如果获取失败时返回的默认值 60 | """ 61 | try: 62 | # 1. 检查表单数据(multipart/form-data 或 application/x-www-form-urlencoded) 63 | if attr in req.form: 64 | # Sanic中form数据是列表形式,取第一个值 65 | return req.form.getlist(attr)[0] 66 | # 2. 检查URL查询参数 (?key=value) 67 | if attr in req.args: 68 | return req.args[attr] 69 | # 3. 检查JSON数据体 (application/json) 70 | if attr in req.json: 71 | return req.json[attr] 72 | except BadRequest: 73 | logging.warning(f"missing {attr} in request") 74 | except Exception as e: 75 | logging.warning(f"get {attr} from request failed:") 76 | logging.warning(traceback.format_exc()) 77 | return default 78 | 79 | def deduplicate_documents(source_docs): 80 | unique_docs = set() 81 | deduplicated_docs = [] 82 | for doc in source_docs: 83 | if doc.page_content not in unique_docs: 84 | unique_docs.add(doc.page_content) 85 | deduplicated_docs.append(doc) 86 | return deduplicated_docs 87 | 88 | def validate_user_id(user_id): 89 | if len(user_id) > 64: 90 | return False 91 | # 定义正则表达式模式 92 | pattern = r'^[A-Za-z][A-Za-z0-9_]*$' 93 | # 检查是否匹配 94 | if isinstance(user_id, str) and re.match(pattern, user_id): 95 | return True 96 | else: 97 | return False 98 | 99 | def get_invalid_user_id_msg(user_id): 100 | return "fail, Invalid user_id: {}. user_id 长度必须小于64,且必须只含有字母,数字和下划线且字母开头".format(user_id) 101 | 102 | def correct_kb_id(kb_id): 103 | if not kb_id: 104 | return kb_id 105 | # 如果kb_id末尾不是KB_SUFFIX,则加上 106 | if KB_SUFFIX not in kb_id: 107 | # 判断有FAQ的时候 108 | # if kb_id.endswith('_FAQ'): # KBc86eaa3f278f4ef9908780e8e558c6eb_FAQ 109 | # return kb_id.split('_FAQ')[0] + KB_SUFFIX + '_FAQ' 110 | # else: # KBc86eaa3f278f4ef9908780e8e558c6eb 111 | # return kb_id + KB_SUFFIX 112 | return kb_id + KB_SUFFIX 113 | else: 114 | return kb_id 115 | 116 | def check_user_id_and_user_info(user_id, user_info): 117 | if user_id is None or user_info is None: 118 | msg = "fail, user_id 或 user_info 为 None" 119 | return False, msg 120 | if not validate_user_id(user_id): 121 | msg = get_invalid_user_id_msg(user_id) 122 | return False, msg 123 | if not user_info.isdigit(): 124 | msg = "fail, user_info 必须是纯数字" 125 | return False, msg 126 | return True, 'success' 127 | 128 | def read_files_with_extensions(): 129 | # 获取当前脚本文件的路径 130 | current_file = os.path.abspath(__file__) 131 | 132 | # 获取当前脚本文件所在的目录 133 | current_dir = os.path.dirname(current_file) 134 | 135 | # 获取项目根目录 136 | project_dir = os.path.dirname(os.path.dirname(current_dir)) 137 | 138 | directory = project_dir + '/data' 139 | 140 | extensions = ['.md', '.txt', '.pdf', '.jpg', '.docx', '.xlsx', '.eml', '.csv', 'pptx', 'jpeg', 'png'] 141 | 142 | files = [] 143 | for root, dirs, files_list in os.walk(directory): 144 | for file in files_list: 145 | if file.endswith(tuple(extensions)): 146 | file_path = os.path.join(root, file) 147 | with open(file_path, 'rb') as f: 148 | file_content = f.read() 149 | mime_type, _ = mimetypes.guess_type(file_path) 150 | if mime_type is None: 151 | mime_type = 'application/octet-stream' 152 | # 模拟 req.files.getlist('files') 返回的对象 153 | file_obj = type('FileStorage', (object,), { 154 | 'name': file, 155 | 'type': mime_type, 156 | 'body': file_content 157 | })() 158 | files.append(file_obj) 159 | return files 160 | 161 | def check_filename(filename, max_length=200): 162 | 163 | # 计算文件名长度,注意中文字符 164 | filename_length = len(filename.encode('utf-8')) 165 | 166 | # 如果文件名长度超过最大长度限制 167 | if filename_length > max_length: 168 | debug_logger.warning("文件名长度超过最大长度限制,返回None") 169 | return None 170 | 171 | return filename 172 | 173 | def cur_func_name(): 174 | return inspect.currentframe().f_back.f_code.co_name 175 | 176 | def clear_string(str): 177 | # 只保留中文、英文、数字 178 | str = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", "", str) 179 | return str 180 | 181 | embedding_tokenizer = AutoTokenizer.from_pretrained(EMBED_MODEL_PATH) 182 | rerank_tokenizer = AutoTokenizer.from_pretrained(RERANK_MODEL_PATH) 183 | llm_tokenizer = AutoTokenizer.from_pretrained(DEFAULT_MODEL_PATH) 184 | 185 | def num_tokens(text: str) -> int: 186 | """Return the number of tokens in a string.""" 187 | return len(llm_tokenizer.encode(text)) 188 | 189 | def num_tokens_embed(text: str) -> int: 190 | """返回字符串的Token数量""" 191 | return len(embedding_tokenizer.encode(text, add_special_tokens=True)) 192 | 193 | def num_tokens_rerank(text: str) -> int: 194 | """Return the number of tokens in a string.""" 195 | return len(rerank_tokenizer.encode(text, add_special_tokens=True)) 196 | 197 | def fast_estimate_file_char_count(file_path): 198 | """ 199 | 快速估算文件的字符数,如果超过max_chars则返回False,否则返回True 200 | """ 201 | file_extension = os.path.splitext(file_path)[1].lower() 202 | # TODO:先支持纯文本文件,后续在支持更多 203 | try: 204 | if file_extension in ['.txt']: 205 | # 'rb' 表示以二进制模式读取 206 | with open(file_path, 'rb') as file: 207 | # 读取前1024字节 208 | raw = file.read(1024) 209 | # 使用chardet库检测文件编码 210 | encoding = chardet.detect(raw)['encoding'] 211 | # 第二次打开计算字符数 212 | with open(file_path, 'r', encoding=encoding) as file: 213 | char_count = sum(len(line) for line in file) 214 | else: 215 | # 不支持的文件类型 216 | return None 217 | 218 | return char_count 219 | 220 | except Exception as e: 221 | print(f"Error processing file {file_path}: {str(e)}") 222 | return None 223 | 224 | def my_print(str): 225 | # 获取调用栈 226 | frame = inspect.currentframe() 227 | # 上一层frame是调用当前函数的函数 228 | caller_frame = frame.f_back 229 | 230 | # 获取调用者信息 231 | caller_filename = caller_frame.f_code.co_filename 232 | caller_function = caller_frame.f_code.co_name 233 | caller_lineno = caller_frame.f_lineno 234 | 235 | # 清理frame引用以避免引用循环 236 | del frame 237 | 238 | print(f"I was called by {caller_function} in file {caller_filename} at line {caller_lineno}") 239 | print(str) 240 | 241 | # 将图片引用地址转换为服务器所存图片引用地址 242 | def replace_image_references(text, file_id): 243 | lines = text.split('\n') 244 | result = [] 245 | 246 | # 匹配带标题的图片引用 247 | pattern_with_caption = r'^!\[figure\]\((.+\.jpg)\s+(.+)\)$' 248 | # 匹配不带标题的图片引用 249 | pattern_without_caption = r'^!\[figure\]\((.+\.jpg)\)$' 250 | 251 | for line in lines: 252 | if not line.startswith('![figure]'): 253 | result.append(line) 254 | continue 255 | 256 | match_with_caption = re.match(pattern_with_caption, line) 257 | match_without_caption = re.match(pattern_without_caption, line) 258 | if match_with_caption: 259 | image_path, caption = match_with_caption.groups() 260 | debug_logger.info(f"line: {line}, caption: {caption}") 261 | result.append(f"#### {caption}") 262 | result.append(f"![figure](/home/zzh/Agent/file_store/image_store/{file_id}/{image_path})") 263 | elif match_without_caption: 264 | image_path = match_without_caption.group(1) 265 | result.append(f"![figure](/home/zzh/Agent/file_store/image_store/{file_id}/{image_path})") 266 | else: 267 | result.append(line) 268 | 269 | return '\n'.join(result) 270 | -------------------------------------------------------------------------------- /src/utils/log_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from concurrent_log_handler import ConcurrentRotatingFileHandler 3 | import time 4 | import os 5 | 6 | 7 | class CustomConcurrentRotatingFileHandler(ConcurrentRotatingFileHandler): 8 | def doRollover(self): 9 | """ 10 | Do a rollover, as described in __init__(). 11 | """ 12 | if self.stream: 13 | self.stream.close() 14 | self.stream = None 15 | 16 | # Add timestamp to the filename for the old log 17 | current_time = time.strftime("%Y%m%d_%H%M%S") 18 | dfn = self.rotation_filename(f"{self.baseFilename}.{current_time}") 19 | 20 | if os.path.exists(dfn): 21 | os.remove(dfn) 22 | self.rotate(self.baseFilename, dfn) 23 | 24 | if not self.delay: 25 | self.stream = self._open() 26 | 27 | # 这是LogHandler的代码,用于将日志写入文件 28 | # 获取当前时间作为日志文件名的一部分 29 | current_time = time.strftime("%Y%m%d_%H%M%S") 30 | # 定义日志文件夹路径 31 | debug_log_folder = './logs/debug_logs' 32 | qa_log_folder = './logs/qa_logs' 33 | rerank_log_folder = './logs/rerank_logs' 34 | embed_log_folder = './logs/embed_logs' 35 | insert_log_folder = './logs/insert_logs' 36 | # 确保日志文件夹存在 37 | if not os.path.exists(debug_log_folder): 38 | os.makedirs(debug_log_folder) 39 | if not os.path.exists(qa_log_folder): 40 | os.makedirs(qa_log_folder) 41 | if not os.path.exists(rerank_log_folder): 42 | os.makedirs(rerank_log_folder) 43 | if not os.path.exists(embed_log_folder): 44 | os.makedirs(embed_log_folder) 45 | if not os.path.exists(insert_log_folder): 46 | os.makedirs(insert_log_folder) 47 | # 定义日志文件的完整路径,包括文件夹和文件名 48 | # log_file = os.path.join(log_folder, f'log_{current_time}.log') 49 | 50 | # 创建一个 logger 实例 51 | qa_logger = logging.getLogger('qa_logger') 52 | debug_logger = logging.getLogger('debug_logger') 53 | rerank_logger = logging.getLogger('rerank_logger') 54 | embed_logger = logging.getLogger('embed_logger') 55 | insert_logger = logging.getLogger('insert_logger') 56 | # 设置 logger 的日志级别为 INFO,即只记录 INFO 及以上级别的日志信息 57 | qa_logger.setLevel(logging.INFO) 58 | debug_logger.setLevel(logging.INFO) 59 | rerank_logger.setLevel(logging.INFO) 60 | embed_logger.setLevel(logging.INFO) 61 | insert_logger.setLevel(logging.INFO) 62 | 63 | debug_handler = ConcurrentRotatingFileHandler(os.path.join(debug_log_folder, "debug.log"), "a", 64 * 1024 * 1024, 256) 64 | # 定义日志格式 65 | # 创建一个自定义字段,用于表示是主进程还是子进程 66 | process_type = 'MainProcess' if 'SANIC_WORKER_NAME' not in os.environ else os.environ['SANIC_WORKER_NAME'] 67 | 68 | # 创建一个带有自定义字段的格式器 69 | formatter = logging.Formatter(f"%(asctime)s - [PID: %(process)d][{process_type}] - [Function: %(funcName)s] - %(levelname)s - %(message)s") 70 | 71 | # formatter = logging.Formatter("%(asctime)s - %(name)s - [PID: %(process)d] - %(levelname)s - %(message)s") 72 | # 设置日志格式 73 | debug_handler.setFormatter(formatter) 74 | 75 | # 将 handler 添加到 logger 中,这样 logger 就可以使用这个 handler 来记录日志了 76 | debug_logger.addHandler(debug_handler) 77 | 78 | qa_handler = ConcurrentRotatingFileHandler(os.path.join(qa_log_folder, "qa.log"), "a", 64 * 1024 * 1024, 256) 79 | # 定义日志格式 80 | formatter = logging.Formatter("%(asctime)s %(message)s") 81 | # 设置日志格式 82 | qa_handler.setFormatter(formatter) 83 | 84 | # 将 handler 添加到 logger 中,这样 logger 就可以使用这个 handler 来记录日志了 85 | qa_logger.addHandler(qa_handler) 86 | 87 | 88 | rerank_handler = ConcurrentRotatingFileHandler(os.path.join(rerank_log_folder, "rerank.log"), "a", 64 * 1024 * 1024, 256) 89 | # 定义日志格式 90 | formatter = logging.Formatter("%(asctime)s %(message)s") 91 | # 设置日志格式 92 | rerank_handler.setFormatter(formatter) 93 | 94 | # 将 handler 添加到 logger 中,这样 logger 就可以使用这个 handler 来记录日志了 95 | rerank_logger.addHandler(rerank_handler) 96 | 97 | 98 | embed_handler = ConcurrentRotatingFileHandler(os.path.join(embed_log_folder, "embed.log"), "a", 64 * 1024 * 1024, 256) 99 | # 定义日志格式 100 | formatter = logging.Formatter("%(asctime)s %(message)s") 101 | # 设置日志格式 102 | embed_handler.setFormatter(formatter) 103 | 104 | # 将 handler 添加到 logger 中,这样 logger 就可以使用这个 handler 来记录日志了 105 | embed_logger.addHandler(embed_handler) 106 | 107 | insert_handler = ConcurrentRotatingFileHandler(os.path.join(insert_log_folder, "insert.log"), "a", 64 * 1024 * 1024, 256) 108 | # 定义日志格式 109 | formatter = logging.Formatter(f"%(asctime)s - [PID: %(process)d][{process_type}] - [Function: %(funcName)s] - %(levelname)s - %(message)s") 110 | # 设置日志格式 111 | insert_handler.setFormatter(formatter) 112 | 113 | # 将 handler 添加到 logger 中,这样 logger 就可以使用这个 handler 来记录日志了 114 | insert_logger.addHandler(insert_handler) 115 | 116 | print(debug_logger, qa_logger, rerank_logger, embed_logger, insert_logger) 117 | 118 | qa_logger.propagate = False # 关闭日志传播 119 | debug_logger.propagate = False # 关闭日志传播 120 | rerank_logger.propagate = False # 关闭日志传播 121 | embed_logger.propagate = False # 关闭日志传播 122 | insert_logger.propagate = False # 关闭日志传播 --------------------------------------------------------------------------------