0. 前沿 0.1 实验目的 项目名称 :“易速鲜花”内部员工知识库问答系统。
项目介绍 :“易速鲜花”作为一个大型在线鲜花销售平台,有自己的业务流程和规范,也拥有针对员工的SOP手册。新员工入职培训时,会分享相关的信息。但是,这些信息分散于内部网和HR部门目录各处,有时不便查询;有时因为文档过于冗长,员工无法第一时间找到想要的内容;有时公司政策已更新,但是员工手头的文档还是旧版内容。
基于上述需求,我们将开发一套基于各种内部知识手册的 “Doc-QA” 系统。这个系统将充分利用LangChain框架,处理从员工手册中产生的各种问题。这个问答系统能够理解员工的问题,并基于最新的员工手册,给出精准的答案。
0.2 核心流程
Loading:文档加载器把Documents 加载 为以LangChain能够读取的形式。
Splitting:文本分割器把Documents 切分 为指定大小的分割,我把它们称为“文档块”或者“文档片”。
Storage:将上一步中分割好的“文档块”以“嵌入”(Embedding)的形式存储 到向量数据库(Vector DB)中,形成一个个的“嵌入片”。
Retrieval:应用程序从存储中检索 分割后的文档(例如通过比较余弦相似度,找到与输入问题类似的嵌入片)。
Output:把问题和相似的嵌入片传递给语言模型(LLM),使用包含问题和检索到的分割的提示生成答案 。
1. 效果 1.1 输入问题:
1.2 后台日志:
2. 工程 2.1 依赖 aiofiles==23.2.1 aiohttp==3.8.5 aiosignal==1.3.1 altair==5.1.2 annotated-types==0.7.0 anyio==3.7.1 argon2-cffi==23.1.0 argon2-cffi-bindings==21.2.0 arrow==1.2.3 asttokens==2.4.0 async-lru==2.0.4 async-timeout==4.0.3 attrs==23.1.0 Babel==2.12.1 backcall==0.2.0 backoff==2.2.1 beautifulsoup4==4.12.2 bleach==6.0.0 blinker==1.6.2 blis==0.7.10 cachetools==5.3.1 catalogue==2.0.9 certifi==2023.7.22 cffi==1.15.1 chardet==5.2.0 charset-normalizer==3.2.0 click==8.1.7 colorama==0.4.6 comm==0.1.4 confection==0.1.2 contourpy==1.1.0 cryptography==41.0.3 cycler==0.11.0 cymem==2.0.7 dataclasses-json==0.6.7 debugpy==1.8.0 decorator==5.1.1 defusedxml==0.7.1 Deprecated==1.2.14 dill==0.3.7 distro==1.8.0 docx2txt==0.8 elastic-transport==8.4.0 elasticsearch==8.9.0 emoji==2.8.0 exceptiongroup==1.3.0 executing==1.2.0 faiss-cpu==1.7.4 fastapi==0.103.2 fastjsonschema==2.18.0 ffmpy==0.3.1 filelock==3.12.3 filetype==1.2.0 Flask==2.3.3 fonttools==4.42.1 fqdn==1.5.1 frozenlist==1.4.0 fsspec==2023.9.0 gitdb==4.0.10 GitPython==3.1.37 google-api-core==2.11.1 google-api-python-client==2.98.0 google-auth==2.23.0 google-auth-httplib2==0.1.1 google-auth-oauthlib==1.1.0 google_search_results==2.4.2 googleapis-common-protos==1.60.0 gradio==3.47.1 gradio_client==0.6.0 greenlet==2.0.2 grpcio==1.58.0 grpcio-tools==1.58.0 h11==0.14.0 h2==4.1.0 hpack==4.0.0 httpcore==0.17.3 httplib2==0.22.0 httpx==0.24.1 httpx-sse==0.4.1 huggingface-hub==0.16.4 hyperframe==6.0.1 idna==3.4 importlib-metadata==6.8.0 importlib-resources==6.1.0 ipykernel==6.25.2 ipython==8.15.0 ipython-genutils==0.2.0 ipywidgets==8.1.1 isoduration==20.11.0 itsdangerous==2.1.2 jedi==0.19.0 Jinja2==3.1.2 joblib==1.3.2 json5==0.9.14 jsonpatch==1.33 jsonpointer==2.4 jsonschema==4.19.0 jsonschema-specifications==2023.7.1 jupyter==1.0.0 jupyter-console==6.6.3 jupyter-events==0.7.0 jupyter-lsp==2.2.0 jupyter_client==8.3.1 jupyter_core==5.3.1 jupyter_server==2.7.3 jupyter_server_terminals==0.4.4 jupyterlab==4.0.5 jupyterlab-pygments==0.2.2 jupyterlab-widgets==3.0.9 jupyterlab_server==2.25.0 kiwisolver==1.4.5 langchain==0.3.27 langchain-community==0.3.29 langchain-core==0.3.76 langchain-experimental==0.0.23 langchain-openai==0.0.2 langchain-text-splitters==0.3.11 langchainhub==0.1.14 langcodes==3.3.0 langdetect==1.0.9 langsmith==0.4.29 lxml==4.9.3 manifest-ml==0.0.1 markdown-it-py==3.0.0 MarkupSafe==2.1.3 marshmallow==3.20.1 matplotlib==3.7.3 matplotlib-inline==0.1.6 mdurl==0.1.2 mistune==3.0.1 mpmath==1.3.0 multidict==6.0.4 murmurhash==1.0.9 mypy-extensions==1.0.0 nbclient==0.8.0 nbconvert==7.8.0 nbformat==5.9.2 nest-asyncio==1.5.7 networkx==3.1 nltk==3.8.1 notebook==7.0.3 notebook_shim==0.2.3 numexpr==2.8.5 numpy==2.2.6 oauthlib==3.2.2 openai==1.6.1 orjson==3.11.3 outcome==1.2.0 overrides==7.4.0 packaging==23.2 pandas==2.1.0 pandocfilters==1.5.0 parso==0.8.3 pathy==0.10.2 pickleshare==0.7.5 Pillow==10.0.0 platformdirs==3.10.0 playwright==1.38.0 portalocker==2.7.0 preshed==3.0.8 prometheus-client==0.17.1 prompt-toolkit==3.0.39 protobuf==4.24.3 psutil==5.9.5 pure-eval==0.2.2 pyarrow==13.0.0 pyasn1==0.5.0 pyasn1-modules==0.3.0 pycparser==2.21 pydantic==2.11.9 pydantic-settings==2.10.1 pydantic_core==2.33.2 pydeck==0.8.1b0 pydub==0.25.1 pyee==9.0.4 PyGithub==1.59.1 Pygments==2.16.1 PyJWT==2.8.0 PyNaCl==1.5.0 pyparsing==3.1.1 pypdf==3.15.5 PySocks==1.7.1 python-dateutil==2.8.2 python-dotenv==1.0.0 python-iso639==2023.6.15 python-json-logger==2.0.7 python-magic==0.4.27 python-multipart==0.0.6 pytz==2023.3.post1 pywin32==306 pywinpty==2.0.11 PyYAML==6.0.1 pyzmq==25.1.1 qdrant-client==1.7.0 qtconsole==5.4.4 QtPy==2.4.0 rapidfuzz==3.4.0 redis==5.0.0 referencing==0.30.2 regex==2023.8.8 requests==2.32.5 requests-oauthlib==1.3.1 requests-toolbelt==1.0.0 rfc3339-validator==0.1.4 rfc3986-validator==0.1.1 rich==13.6.0 rpds-py==0.10.3 rsa==4.9 safetensors==0.3.3 scikit-learn==1.3.0 scipy==1.11.2 selenium==4.13.0 semantic-version==2.10.0 Send2Trash==1.8.2 six==1.16.0 smart-open==6.4.0 smmap==5.0.1 sniffio==1.3.0 sortedcontainers==2.4.0 soupsieve==2.5 spacy==3.6.1 spacy-legacy==3.0.12 spacy-loggers==1.0.4 SQLAlchemy==1.4.49 sqlitedict==2.1.0 srsly==2.4.7 stack-data==0.6.2 starlette==0.27.0 streamlit==1.27.2 sympy==1.12 tabulate==0.9.0 tenacity==8.2.3 terminado==0.17.1 thinc==8.1.12 threadpoolctl==3.2.0 tiktoken==0.5.2 tinycss2==1.2.1 tokenizers==0.13.3 toml==0.10.2 tomli==2.2.1 toolz==0.12.0 torch==2.0.1 torchaudio==2.0.2 torchvision==0.15.2 tornado==6.3.3 tqdm==4.66.1 traitlets==5.9.0 transformers==4.33.1 trio==0.22.2 trio-websocket==0.11.1 typer==0.9.0 types-requests==2.31.0.6 types-urllib3==1.26.25.14 typing-inspect==0.9.0 typing-inspection==0.4.1 typing_extensions==4.15.0 tzdata==2023.3 tzlocal==5.1 unstructured==0.10.22 uri-template==1.3.0 uritemplate==4.1.1 urllib3==1.26.20 uvicorn==0.23.2 validators==0.22.0 wasabi==1.1.2 watchdog==3.0.0 wcwidth==0.2.6 webcolors==1.13 webencodings==0.5.1 websocket-client==1.6.3 websockets==11.0.3 Werkzeug==2.3.7 widgetsnbextension==4.0.9 wikipedia==1.4.0 wrapt==1.15.0 wsproto==1.2.0 yarl==1.9.2 zipp==3.17.0 zstandard==0.25.0
2.2 代码 import osfrom langchain_community.document_loaders import PyPDFLoaderfrom langchain_community.document_loaders import Docx2txtLoaderfrom langchain_community.document_loaders import TextLoaderbase_dir = '.\OneFlower' documents = [] for file in os.listdir(base_dir): file_path = os.path.join(base_dir, file) if file.endswith('.pdf' ): loader = PyPDFLoader(file_path) documents.extend(loader.load()) elif file.endswith('.docx' ): loader = Docx2txtLoader(file_path) documents.extend(loader.load()) elif file.endswith('.txt' ): loader = TextLoader(file_path) documents.extend(loader.load()) from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size=500 , chunk_overlap=50 ) chunked_documents = text_splitter.split_documents(documents) print (f"优化后文档分块数量:{len (chunked_documents)} " ) import osimport requestsfrom typing import List from langchain.embeddings.base import Embeddingsfrom langchain_community.vectorstores import Qdrantclass QwenEmbeddings (Embeddings ): def __init__ (self, api_key: str , base_url: str ): self.api_key = api_key self.base_url = base_url self.model = "text-embedding-v1" self.max_batch_size = 25 def embed_documents (self, texts: List [str ] ) -> List [List [float ]]: """嵌入文档:过滤空文本 + 批量拆分(每批≤25条)""" valid_texts = [text.strip() for text in texts if text.strip()] if not valid_texts: return [] all_embeddings = [] headers = { "Authorization" : f"Bearer {self.api_key} " , "Content-Type" : "application/json" } for i in range (0 , len (valid_texts), self.max_batch_size): batch_texts = valid_texts[i:i + self.max_batch_size] payload = { "model" : self.model, "input" : batch_texts } try : response = requests.post( f"{self.base_url} /embeddings" , headers=headers, json=payload, timeout=30 ) response.raise_for_status() batch_embeddings = [item["embedding" ] for item in response.json()["data" ]] all_embeddings.extend(batch_embeddings) print (f"成功处理第 {i // self.max_batch_size + 1 } 批嵌入(共 {len (batch_texts)} 条)" ) except Exception as e: print (f"处理第 {i // self.max_batch_size + 1 } 批嵌入失败:{str (e)} " ) print (f"当前批次文本(前300字符):{str (batch_texts[:2 ])[:300 ]} ..." ) raise assert len (all_embeddings) == len (valid_texts), "嵌入结果数量与文本数量不匹配!" return all_embeddings def embed_query (self, text: str ) -> List [List [float ]]: """嵌入查询:单条文本,无需拆分""" return self.embed_documents([text])[0 ] if text.strip() else [] embedding = QwenEmbeddings( api_key=os.environ.get("DASHSCOPE_API_KEY" ), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) vectorstore = Qdrant.from_documents( documents=chunked_documents, embedding=embedding, location=":memory:" , collection_name="my_documents" , ) import logging from langchain_community.chat_models import ChatOpenAI from langchain.retrievers.multi_query import MultiQueryRetriever from langchain.chains import RetrievalQA logging.basicConfig() logging.getLogger('langchain.retrievers.multi_query' ).setLevel(logging.INFO) llm = ChatOpenAI( api_key=os.environ.get("DASHSCOPE_API_KEY" ), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" , model_name="qwen-flash" , ) retriever_from_llm = MultiQueryRetriever.from_llm(retriever=vectorstore.as_retriever(), llm=llm) qa_chain = RetrievalQA.from_chain_type(llm,retriever=retriever_from_llm) from flask import Flask, request, render_templateapp = Flask(__name__) @app.route('/' , methods=['GET' , 'POST' ] ) def home (): if request.method == 'POST' : question = request.form.get('question' ) result = qa_chain({"query" : question}) return render_template('index.html' , result=result) return render_template('index.html' ) if __name__ == "__main__" : app.run(host='0.0.0.0' ,debug=True ,port=5000 )
2.3 运行 提前设置好环境变量