网站建设的栏目规划qq群推广方法
网站建设的栏目规划,qq群推广方法,做小程序怎么赚钱,专业服务网站建设1. 为什么你需要一个本地知识库问答系统#xff1f;
想象一下这个场景#xff1a;你所在的公司有大量的内部文档#xff0c;比如产品手册、技术白皮书、客户案例、会议纪要。每当新同事入职#xff0c;或者销售需要快速查找某个产品的技术参数时#xff0c;大家要么在成堆…1. 为什么你需要一个本地知识库问答系统想象一下这个场景你所在的公司有大量的内部文档比如产品手册、技术白皮书、客户案例、会议纪要。每当新同事入职或者销售需要快速查找某个产品的技术参数时大家要么在成堆的PDF里翻找要么在混乱的共享文件夹里大海捞针。更头疼的是现在有了大语言模型LLM你问它一个关于公司内部流程的细节问题它要么答非所问要么直接告诉你“我的知识截止于...”。这就是本地知识库问答系统要解决的问题。它不是一个简单的全文搜索而是一个能“理解”你问题、并从你指定的文档中“找出”答案、再用自然语言“组织”回答给你的智能助手。核心价值在于让公共的大模型具备了“私有化”的记忆和知识。你不用再费心去训练一个专属模型只需要把文档“喂”给它它就能基于这些文档和你对话。我做过好几个类似的项目实测下来这种方案特别适合中小企业、技术团队甚至个人开发者。你不需要购买昂贵的商业软件或API用开源的模型和框架在自己的服务器上就能搭起来。数据全程在本地安全可控速度也快。接下来我就手把手带你用FastAPI和LangChain这两个利器从零搭建一个完全属于你自己的智能问答系统。2. 搭建基石用FastAPI为你的大模型造一个“接口”要让LangChain能调用我们本地部署的大模型第一步就是给它提供一个标准的“通话渠道”。就像手机需要SIM卡才能接入网络我们的模型也需要一个API接口来接收指令和返回结果。FastAPI凭借其极简的语法和闪电般的速度成了完成这个任务的不二之选。2.1 环境准备与依赖安装我习惯用一个干净的Python虚拟环境来开始项目这样可以避免包版本冲突的“玄学”问题。打开你的终端跟着我一步步来# 创建并进入项目目录 mkdir local_knowledge_qa cd local_knowledge_qa # 创建虚拟环境这里用venv你用conda也行 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate激活后你的命令行前面应该会出现(venv)的标识。接下来安装核心依赖。这里我们不仅要装FastAPI和网络服务器还要把LangChain的基础包也一并装上。pip install fastapi uvicorn langchain langchain-community简单解释一下这几个包fastapi是咱们的Web框架uvicorn是一个轻量级的ASGI服务器用来运行FastAPI应用langchain是核心框架langchain-community包含了许多社区维护的集成工具。安装过程可能会花点时间喝杯咖啡等一下就好。2.2 编写你的第一个LLM API服务假设你已经在本地通过Ollama、vLLM或者类似工具跑起来了一个大模型比如Llama 3、Qwen或者ChatGLM。现在我们需要用FastAPI包装它。创建一个名为main.py的文件写入以下代码from fastapi import FastAPI, HTTPException from pydantic import BaseModel import requests # 假设你的本地模型服务通过HTTP提供 app FastAPI(title本地LLM API服务, description为LangChain提供本地模型调用接口) # 定义请求体的数据模型这能让FastAPI自动校验和生成文档 class ChatRequest(BaseModel): prompt: str # 用户输入的问题或指令 max_tokens: int 512 # 生成的最大token数给个默认值 temperature: float 0.7 # 控制生成随机性的温度参数 # 假设你的本地模型服务运行在 http://localhost:11434/api/generate (以Ollama为例) LOCAL_MODEL_URL http://localhost:11434/api/generate app.post(/v1/chat/completions) async def chat_completion(request: ChatRequest): 核心聊天接口接收prompt转发给本地模型并返回结果。 这个接口路径模仿了OpenAI的格式方便后续适配。 try: # 构造发送给本地模型服务的请求体格式需根据你的模型服务调整 payload { model: qwen2:7b, # 你实际运行的模型名称 prompt: request.prompt, stream: False, options: { num_predict: request.max_tokens, temperature: request.temperature } } # 发送请求到本地模型 response requests.post(LOCAL_MODEL_URL, jsonpayload, timeout60) response.raise_for_status() # 如果HTTP状态码不是200会抛出异常 result response.json() # 从响应中提取生成的文本这里需要根据实际返回的JSON结构调整 # Ollama返回格式是 {model:..., response:..., ...} generated_text result.get(response, ).strip() # 构造一个兼容OpenAI格式的返回方便LangChain调用 return { choices: [{ message: { role: assistant, content: generated_text }, finish_reason: stop }] } except requests.exceptions.RequestException as e: # 处理网络或模型服务错误 raise HTTPException(status_code503, detailf模型服务调用失败: {str(e)}) except KeyError as e: # 处理响应格式解析错误 raise HTTPException(status_code500, detailf解析模型响应时出错: {str(e)}) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)这段代码做了几件关键事首先它定义了一个标准的HTTP POST接口其次它接收一个包含用户提问和生成参数的JSON请求然后它把这个请求“翻译”成你本地模型服务能听懂的样子并转发过去最后它再把模型返回的结果“包装”成一种通用格式这里模仿了OpenAI的格式返回。这样做的好处是下游应用比如LangChain可以用一套统一的代码来调用不同来源的模型。2.3 启动与测试你的API保存好main.py后在终端运行python main.py你会看到输出提示服务已经在http://0.0.0.0:8000上运行。别急着关掉这个终端。打开另一个终端或者用你喜欢的API测试工具比如Postman、curl我们来测试一下curl -X POST http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {prompt: 你好请介绍一下你自己, max_tokens: 100}如果一切顺利你应该会收到一个JSON格式的回复里面包含了模型生成的自我介绍。看到这个恭喜你你已经成功给你的大模型装上了“标准插座”LangChain这个“电器”马上就能插上使用了。我刚开始做的时候经常在这里卡住问题多半出在本地模型服务的地址、端口或者请求/响应格式不匹配上多检查几遍日志就能搞定。3. 连接桥梁将本地LLM封装成LangChain组件API服务跑通了但它现在还是个独立的“孤岛”。LangChain并不认识它。我们需要创建一个适配器告诉LangChain“嘿这里有个LLM你可以像调用OpenAI一样调用它。” 这个过程在LangChain里叫做自定义LLM封装。3.1 理解LangChain的LLM基类LangChain为所有大语言模型定义了一个“蓝图”也就是LLM基类。我们要做的就是按照这个蓝图造一个我们自己的“产品”。这个蓝图要求我们至少实现三个部分一个标识模型类型的_llm_type属性一个实际执行调用的_call方法以及一个描述模型参数的_identifying_params属性。听起来有点抽象别怕我们直接看代码。3.2 编写自定义LLM类在你的项目目录下新建一个文件custom_llm.pyfrom langchain.llms.base import LLM from typing import Optional, List, Mapping, Any import requests from pydantic import Field class LocalLLM(LLM): 自定义LLM类用于封装我们本地通过FastAPI提供的模型服务。 # 定义一些可配置的参数比如API地址这些参数可以在初始化时传入 api_url: str Field(defaulthttp://localhost:8000/v1/chat/completions, description本地模型API地址) max_tokens: int Field(default512, description生成的最大token数) temperature: float Field(default0.7, description生成温度控制随机性) property def _llm_type(self) - str: 返回LLM的类型标识这个名字可以自己定用于在LangChain中识别。 return local_fastapi_llm def _call(self, prompt: str, stop: Optional[List[str]] None) - str: 核心方法接收一个提示词调用本地API并返回模型生成的文本。 stop 参数用于指定停止生成的序列我们这里先做简单处理。 # 1. 准备请求载荷 payload { prompt: prompt, max_tokens: self.max_tokens, temperature: self.temperature } headers {Content-Type: application/json} try: # 2. 发送HTTP请求到我们自己的FastAPI服务 response requests.post(self.api_url, jsonpayload, headersheaders, timeout60) response.raise_for_status() # 检查HTTP错误 result response.json() # 3. 从兼容OpenAI格式的响应中提取文本内容 # 根据我们FastAPI接口返回的结构进行解析 generated_text result[choices][0][message][content] # 4. 可选处理stop序列。这是一个简化版实际应用可能需要更复杂的截断逻辑。 if stop is not None: for stop_seq in stop: if stop_seq in generated_text: generated_text generated_text.split(stop_seq)[0] return generated_text.strip() except requests.exceptions.ConnectionError: raise ValueError(f无法连接到本地API服务请检查地址 {self.api_url} 是否正确以及服务是否已启动。) except KeyError as e: raise ValueError(f解析API响应时出错响应结构可能已改变: {e}) property def _identifying_params(self) - Mapping[str, Any]: 返回一组用于标识此LLM实例的参数在缓存、日志等场景有用。 return {api_url: self.api_url, model_type: self._llm_type}这个类就像一个万能转换头。它内部使用requests库去调用我们刚刚写好的FastAPI接口然后把返回的JSON解析成LangChain需要的纯文本字符串。Field来自Pydantic它让我们能方便地定义和验证初始化参数。我在这里加了一些简单的错误处理比如连接失败和响应格式错误这在调试阶段能帮你快速定位问题。3.3 测试封装是否成功再新建一个测试脚本test_llm.py或者在Python交互环境里运行from custom_llm import LocalLLM # 初始化我们的自定义LLM参数使用默认值指向本地运行的FastAPI服务 llm LocalLLM() # 问它一个问题 question 太阳系最大的行星是哪一颗 answer llm.invoke(question) # 或者直接用 llm(question) print(f问题: {question}) print(f回答: {answer})如果看到模型返回了“木星”之类的答案那就大功告成了这意味着LangChain现在已经能像指挥OpenAI的GPT一样指挥你本地部署的模型了。这一步的封装是整个项目的关键枢纽它把非标准的本地服务变成了LangChain生态里一个标准的“零件”。以后无论你的底层模型换成哪个只要API格式不变上层的应用代码几乎不用动。4. 知识库的核心文档处理与向量化有了能听话的LLM我们还需要给它“喂”知识。直接扔给它一本几百页的PDF是没用的我们需要把非结构化的文档文本转换成计算机特别是相似度搜索算法容易处理的格式——也就是向量。这个过程就像给图书馆的每本书做一个独一无二的“指纹摘要”。4.1 文档加载与智能分块LangChain支持各种各样的文档格式从TXT、PDF到Word、PPT甚至HTML和Markdown。我们以处理一个PDF产品手册为例。首先安装处理PDF所需的额外包pip install pypdf langchain-text-splitters然后我们来写加载和分割文档的代码from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档 loader PyPDFLoader(./产品手册.pdf) # 替换成你的文件路径 documents loader.load() print(f加载了 {len(documents)} 页PDF文档。) # 2. 分割文档 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个文本块的最大字符数或token数 chunk_overlap100, # 块与块之间的重叠字符数 length_functionlen, # 计算长度的方法这里用字符数。如果用token数可以换成 tiktoken 库的函数。 separators[\n\n, \n, 。, , , , ] # 分割符优先级 ) split_docs text_splitter.split_documents(documents) print(f文档被分割成 {len(split_docs)} 个文本块。) # 查看第一个文本块 print(--- 第一个文本块预览 ---) print(split_docs[0].page_content[:200]) # 打印前200个字符这里有几个我踩过坑的经验点chunk_size不是越大越好。太大检索时可能引入无关信息太小又会丢失上下文。一般根据你用的Embedding模型的最大输入长度和你的文档特点来定500-1000是个常见的起点。chunk_overlap设置重叠非常重要它能防止一个完整的句子或概念被硬生生切在两段中间我通常设为chunk_size的10%-20%。RecursiveCharacterTextSplitter会按你给的separators列表顺序尝试分割直到切出来的块小于设定大小这样能尽可能保证语义的完整性。4.2 为文本块注入“灵魂”Embedding模型选择文本块准备好了下一步就是把它们变成向量。这个过程由Embedding模型完成。你可以把它理解为一个“语义编码器”它把一段文字映射到一个高维空间中的一个点语义相近的文字它们的向量点在空间里的距离也更近。市面上开源的Embedding模型很多怎么选我根据中文场景的实测经验给你几个推荐BGE (BAAI/bge-large-zh)智源研究院开源的在中文语义相似度任务上表现非常强悍是我目前的首选。text2vec朗朗上口的国产模型有多个尺寸text2vec-large-chinese效果不错部署也方便。M3E专门为中文指令微调的Embedding模型如果你构建的是问答对形式的知识库它的表现可能更佳。OpenAI text-embedding-ada-002如果你不介意调用API且网络通畅它的效果非常稳定就是有费用和延迟问题。这里我们以本地部署的BGE模型为例。首先从Hugging Face下载模型或者如果你有已经下载好的模型路径。然后安装必要的库pip install sentence-transformers接着在代码中初始化Embedding模型from langchain.embeddings import HuggingFaceEmbeddings # 指定模型路径。可以是Hugging Face模型ID也可以是本地路径。 model_name BAAI/bge-large-zh # 或者 /path/to/your/bge-model # 初始化Embedding模型 embeddings HuggingFaceEmbeddings( model_namemodel_name, model_kwargs{device: cpu}, # 如果有GPU可以改成 cuda:0 encode_kwargs{normalize_embeddings: True} # 将向量标准化有利于相似度计算 ) # 测试一下Embedding效果 text 这是一个测试句子。 vector embeddings.embed_query(text) print(f文本的向量维度是{len(vector)}) print(f向量前10维{vector[:10]})normalize_embeddingsTrue这个参数我强烈建议打开它会把所有向量的长度缩放到1这样后续计算余弦相似度就变成了简单的点积速度快效果也好。初始化可能需要一点时间加载模型耐心等待一下。4.3 构建向量数据库FAISS快速入门向量有了我们需要一个专门的地方来存储它们并且能快速进行“最相似”的查找。这就是向量数据库。我们选用Meta开源的FAISS它特别适合在内存中做高效的相似性搜索对于百万级以下的数据量单机部署非常简单。from langchain.vectorstores import FAISS # 方法一直接从分割好的文档创建向量库会进行Embedding计算可能较慢 vectorstore FAISS.from_documents( documentssplit_docs, # 上一步分割好的文本块 embeddingembeddings # 上一步初始化的Embedding模型 ) # 将向量库保存到本地磁盘方便下次直接加载无需重新计算Embedding save_path ./faiss_index vectorstore.save_local(save_path) print(f向量库已保存至{save_path}) # 方法二从本地磁盘加载已有的向量库 # new_vectorstore FAISS.load_local(save_path, embeddings, allow_dangerous_deserializationTrue) # 注意allow_dangerous_deserializationTrue 是LangChain新版的安全提示因为加载pickle文件可能有风险。确保你的索引文件来源可信。 # 进行相似性搜索测试 query 你们产品的最长续航时间是多少 retrieved_docs vectorstore.similarity_search(query, k3) # 检索最相似的3个文本块 print(f\n针对问题{query}) for i, doc in enumerate(retrieved_docs): print(f\n--- 检索结果 {i1} ---) print(f内容片段{doc.page_content[:150]}...) # 只打印前150字符 print(f来源{doc.metadata}) # metadata里通常包含页码、文件名等信息FAISS.from_documents这个方法在背后默默地做了大量工作它遍历每一个文本块调用Embedding模型将其转换为向量然后把这些向量构建成一个高效的索引。保存到本地后这个索引文件就包含了所有文本的“语义指纹”。下次启动应用时直接加载这个文件速度会快非常多。similarity_search是核心的检索函数它会把你的问题也转换成向量然后在索引里快速找出最相近的K个文本块。这里的k值是个需要调优的参数一般根据你文档的粒度和答案的复杂性来定可以先从3或5开始试。5. 最终组装打造完整的问答链现在我们有了能理解指令的LLM自定义封装也有了存储知识的向量库FAISS。是时候把它们组装成一个能回答具体问题的智能系统了。LangChain提供了一个非常趁手的工具叫RetrievalQA它把检索Retrieval和问答QA的流程打包成了一个完整的“链”。5.1 构建检索问答链from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate from custom_llm import LocalLLM # 导入我们之前封装好的本地LLM # 1. 加载我们之前保存的向量数据库 embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-large-zh) vectorstore FAISS.load_local(./faiss_index, embeddings, allow_dangerous_deserializationTrue) # 2. 将向量数据库转换为一个检索器Retriever retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 这里设置每次检索返回4个相关片段 # 3. 初始化我们的本地LLM llm LocalLLM(api_urlhttp://localhost:8000/v1/chat/completions, max_tokens1024) # 4. 可选但推荐自定义一个提示模板让LLM的回答更精准。 # LangChain会将检索到的上下文和用户问题一起填充到这个模板里。 prompt_template 请根据以下上下文信息来回答问题。如果你不知道答案就诚实地回答不知道不要编造信息。 上下文信息 {context} 问题{question} 请根据上下文给出答案 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) # 5. 创建检索问答链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, # 最常用的类型将所有检索到的上下文“塞”进prompt retrieverretriever, return_source_documentsTrue, # 设置为True可以返回检索到的源文档便于追溯 chain_type_kwargs{prompt: PROMPT} # 使用我们自定义的提示模板 ) print(智能问答系统已初始化完成)我来解释一下关键参数chain_typestuff是一种最简单的处理方式它把检索到的所有文档片段context和问题question全部拼接起来一次性发送给LLM。这种方式适合检索到的文档总长度不超过LLM上下文窗口的情况。如果你的文档很长可以考虑map_reduce或refine等更复杂但能处理长文本的类型。return_source_documentsTrue这个选项非常实用它能让系统在给出答案的同时告诉你这个答案是根据哪几段原文得出的增加了可信度和可追溯性。5.2 运行与优化你的问答系统现在让我们来问它几个问题# 示例问题1直接基于知识库提问 question 请问产品在低温环境下的工作性能如何 result qa_chain.invoke({query: question}) print(f\n问题{question}) print(f答案{result[result]}) print(\n--- 参考来源 ---) for i, doc in enumerate(result[source_documents][:2]): # 打印前两个来源 print(f[来源{i1}] {doc.page_content[:100]}...) # 示例问题2知识库中没有的信息 question2 你们公司明年准备上市吗 result2 qa_chain.invoke({query: question2}) print(f\n问题{question2}) print(f答案{result2[result]})运行后你会看到系统首先从向量库中检索出与“低温环境”、“工作性能”相关的文档片段然后把这些片段和原始问题一起交给本地LLM。LLM会“阅读”这些上下文并生成一个连贯的答案。对于第二个问题由于知识库中没有相关信息一个设计良好的提示模板会引导LLM回答“不知道”而不是胡编乱造。在实际使用中你可能会遇到一些典型问题这里分享我的调优经验答案不准确首先检查检索到的文档是否真的相关。可以调大k值比如从3调到5或者优化Embedding模型。有时调整文本分块的chunk_size也会有奇效。答案冗长或包含无关信息优化你的提示模板Prompt Template。在模板中明确指令例如“请只根据上下文回答”、“答案要简洁”。响应速度慢如果Embedding和LLM推理都在CPU上速度肯定快不了。考虑将Embedding模型放到GPU上model_kwargs{device: cuda:0}或者使用更轻量的Embedding模型。对于LLM确保你的本地推理服务如Ollama配置了足够的资源。6. 进阶与部署让系统更实用一个能跑通的Demo和一个实用的系统之间还差着一些工程化的步骤。为了让这个系统更健壮、更易用我们可以从以下几个方面入手。6.1 构建一个简单的Web界面整天对着命令行提问不够友好。我们可以用FastAPI再写几个接口并搭配一个简单的HTML前端做成一个Web应用。在main.py中增加以下接口from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel # ... 保留之前已有的导入和代码 ... app.add_middleware( CORSMiddleware, allow_origins[*], # 生产环境应指定具体前端地址 allow_methods[*], allow_headers[*], ) class QARequest(BaseModel): question: str # 假设我们在全局初始化了 qa_chain # qa_chain RetrievalQA.from_chain_type(...) app.post(/api/ask) async def ask_question(request: QARequest): 问答接口 try: result qa_chain.invoke({query: request.question}) return { answer: result[result], sources: [{content: doc.page_content[:200], metadata: doc.metadata} for doc in result.get(source_documents, [])] } except Exception as e: raise HTTPException(status_code500, detailf问答过程出错: {str(e)})然后你可以创建一个index.html文件用JavaScript调用这个/api/ask接口实现一个简单的问答界面。这样非技术同事也能方便地使用了。6.2 实现增量更新与文档管理知识库不是一成不变的。当有新文档加入时我们不需要从头重新构建整个向量库。FAISS支持增量添加# 假设有新的文档列表 new_docs (已经是Document对象列表) new_texts [doc.page_content for doc in new_docs] new_metadatas [doc.metadata for doc in new_docs] # 为新增文本生成向量并添加到现有索引 vectorstore.add_texts(textsnew_texts, metadatasnew_metadatas) # 记得保存更新后的索引 vectorstore.save_local(./faiss_index)你甚至可以在此基础上构建一个简单的管理后台实现文档的上传、解析、入库和状态查看功能。6.3 性能监控与日志记录在生产环境你需要知道系统运行得怎么样。可以在关键步骤加入日志import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) # 在RetrievalQA调用前后记录 logger.info(f开始处理问题: {question}) start_time time.time() result qa_chain.invoke({query: question}) elapsed_time time.time() - start_time logger.info(f问题处理完毕耗时: {elapsed_time:.2f}秒检索到 {len(result[source_documents])} 个文档片段。)记录耗时、检索到的文档数量、LLM的响应状态等信息对于后续分析瓶颈、优化性能至关重要。走到这一步你已经拥有了一个功能完整、自主可控的本地知识库智能问答系统。它可能不像商业产品那样界面华丽但核心的检索、理解、生成能力都已具备而且所有数据和流程都掌握在自己手里。从我自己的项目经验来看这种系统在内部技术支持、产品知识查询、新员工培训等场景下能实实在在地提升效率。最让我有成就感的是看到非技术部门的同事也开始习惯用这个“小助手”来查找信息而不是一遍遍地来打扰工程师。技术的价值莫过于此。