使用 LangChain 搭建本地大模型 RAG 问答应用

发布时间:2026/7/5 2:21:08
使用 LangChain 搭建本地大模型 RAG 问答应用 环境配置与音频模型只能在 Linux 上运行不同本地大模型推理可以在 Windows 上实现。这里是在安装了 Raspberry Pi OS 的 Raspberry Pi 5 上实现的。安装 HailoRT首先要安装 HailoRT这是 Hailo 的运行时库包含设备驱动、Python 绑定、命令行工具。可以去 Hailo Developer Zone 注册账号下载也可以在 ASUS 的官网下载。详细步骤见上一篇博客张高兴的 Hailo-10 开发指南一实现离线语音识别。安装 Hailo-OllamaHailo-Ollama 是 Hailo 官方提供的推理服务API 和 Ollama 完全兼容意味着能用 Ollama 的工具LangChain、Open-WebUI 等基本都能直接使用。在安装完 ASUS 的驱动捆绑包之后Hailo-Ollama 也已经安装完成。如果你的 Hailo-10 是 M.2 版本的可能需要单独安装 Hailo-Ollama在 Hailo Developer Zone 中下载安装Hailo Model Zoo GenAI。Python 环境pip install pdfplumber langchain langchain-ollama langchain-text-splitters \ langchain-chroma langchain-huggingface sentence-transformers \ chromadb requests启动 Hailo-Ollama 服务服务启动很简单直接在命令行中运行下面的命令默认监听0.0.0.0:8000。hailo-ollama下面看看 Hailo-10 目前支持哪些模型curl --silent http://localhost:8000/hailo/v1/list可以看到目前支持deepseek_r1:1.5b、llama3.2:1b、qwen2.5-coder:1.5b、qwen2.5:1.5b、qwen2:1.5b、qwen3:1.7b这些模型。执行下面的命令拉取qwen3:1.7b模型curl --silent http://localhost:8000/api/pull \ -H Content-Type: application/json \ -d { model: qwen3:1.7b, stream : true }拉取完成后验证一下模型是否能正常推理curl --silent http://localhost:8000/api/chat \ -H Content-Type: application/json \ -d {model: qwen3:1.7b, messages: [{role: user, content: 用一句话解释什么是向量数据库}]}到这里整个链路就通了。接下来你可以通过调用接口的方式在自己的应用里使用这个模型了也可以使用支持 Ollama 的工具比如 Open-WebUI 来和模型进行交互。比如拉取 Open-WebUI 的 Docker 镜像docker run -d --nethost -e OLLAMA_BASE_URLhttp://127.0.0.1:8000 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main实现 RAG 应用模型本身不会主动知道你公司内部文档里写了什么。RAG 是把文档切碎存进向量数据库提问时先搜索最相关的段落再把这些段落塞进 prompt 让模型回答。通过 RAG 模型可以理解一些最新的、专业的知识而不需要模型本身学会这些知识。比如这里使用的是 ASUS UGen300 的用户手册模型本身不可能学会里面的内容但通过 RAG 的方式模型就能理解用户手册里的内容了。当然你也可以替换成其他的文档完成后面的操作。核心流程很简单文档处理 → 向量化 → RAG 链路 → 问答。下面新建rag_demo.py文件.1. 引用相关包import json import os import requests from typing import Any, Iterator, List, Optional from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import ( AIMessage, AIMessageChunk, BaseMessage, HumanMessage, SystemMessage, ) from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult from langchain_core.documents import Document from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma import pdfplumber2. PDF 文档处理用pdfplumber逐页解析 PDF构造 LangChain Document 对象。PDF_FILES [ data/UGen300-manual.pdf, ] all_docs [] for pdf_path in PDF_FILES: if not os.path.exists(pdf_path): print(f 跳过不存在{pdf_path}) continue count 0 with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): text (page.extract_text() or ).strip() all_docs.append(Document( page_contenttext, metadata{source: pdf_path, page: i 1}, )) count 1 print(f共加载 {len(all_docs)} 页文本)3. 文本切分文档切分成更小的段落方便后续的向量化和检索。chunk_size 为每个 chunk 最大字符数chunk_overlap 让相邻 chunk 共享边界内容避免关键信息被截断。splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) chunks splitter.split_documents(all_docs) print(f共生成 {len(chunks)} 个 chunk)4. 向量化和存储使用 HuggingFaceEmbeddings 生成文本的向量表示并存储到 ChromaDB。all-MiniLM-L6-v2是最常用的轻量通用 embedding 模型速度快在本地运行不需要 API key也不依赖 GPU。ChromaDB 是一个轻量的本地向量数据库适合小规模数据的存储和检索。CHROMA_DB_DIR ./chroma_db COLLECTION_NAME hailo_docs embeddings HuggingFaceEmbeddings( model_namesentence-transformers/all-MiniLM-L6-v2, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) print(Embedding 模型就绪) db_exists os.path.exists(CHROMA_DB_DIR) and os.listdir(CHROMA_DB_DIR) if db_exists: # 数据库已存在直接加载 vectorstore Chroma( collection_nameCOLLECTION_NAME, embedding_functionembeddings, persist_directoryCHROMA_DB_DIR, ) print(f已加载现有数据库 {CHROMA_DB_DIR}) else: # 首次构建 vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, collection_nameCOLLECTION_NAME, persist_directoryCHROMA_DB_DIR, ) print(f数据库构建完成已持久化到 {CHROMA_DB_DIR})5. 自定义 HailoChatOllama 类Hailo-Ollama 的底层用的是 oatpp HTTP 框架它对请求体的 JSON 字段类型要求很严。LangChain 官方的ChatOllama会往请求里附加options、keep_alive等额外字段oatpp 碰到未知字段直接报类型解析错误。解决方案是继承BaseChatModel自己写一个简化版class HailoChatOllama(BaseChatModel): model: str base_url: str timeout: int 120 property def _llm_type(self) - str: return hailo-ollama def _to_ollama_messages(self, messages: List[BaseMessage]) - list: role_map {system: system, human: user, ai: assistant} return [ {role: role_map.get(m.type, m.type), content: m.content} for m in messages ] def _generate( self, messages: List[BaseMessage], stop: Optional[List[str]] None, run_manager: Any None, **kwargs: Any, ) - ChatResult: payload { model: self.model, messages: self._to_ollama_messages(messages), stream: False, } resp requests.post( f{self.base_url}/api/chat, jsonpayload, timeoutself.timeout, ) resp.raise_for_status() content resp.json()[message][content] return ChatResult( generations[ChatGeneration(messageAIMessage(contentcontent))] ) def _stream( self, messages: List[BaseMessage], stop: Optional[List[str]] None, run_manager: Any None, **kwargs: Any, ) - Iterator[ChatGenerationChunk]: payload { model: self.model, messages: self._to_ollama_messages(messages), stream: True, } with requests.post( f{self.base_url}/api/chat, jsonpayload, streamTrue, timeoutself.timeout, ) as resp: resp.raise_for_status() for line in resp.iter_lines(): if not line: continue data json.loads(line.decode(utf-8)) token data.get(message, {}).get(content, ) if token: chunk ChatGenerationChunk( messageAIMessageChunk(contenttoken) ) if run_manager: run_manager.on_llm_new_token(token, chunkchunk) yield chunk if data.get(done): break只传model和messages两个字段完全避开 oatpp 的字段校验问题。6. RAG 链的组装这里搭建了一个最基本的 RAG 链路根据用户提问先用检索器从 ChromaDB 取最相关的 3 个 chunk把它们拼成一个上下文再把上下文和问题一起塞进 prompt 让模型回答。# 创建检索器相似度搜索返回 top_k 最相关 chunk retriever vectorstore.as_retriever(search_kwargs{k: 3}) llm HailoChatOllama( modelqwen3:1.7b, base_urlhttp://localhost:8000 ) # 问答函数 def stream_answer(question: str): docs retriever.invoke(question) context \n\n---\n\n.join(d.page_content for d in docs) prompt f 你是一个专业助手只根据下面提供的参考文档尽可能简短的回答问题。如果文档中没有相关信息请直接说文档中未找到相关内容。 文档内容 {context} 问题{question} for chunk in llm.stream([HumanMessage(contentprompt)]): print(chunk.content, end, flushTrue) print() stream_answer(How to install HailoRT on Ubuntu?)LoRA 微调除了 RAG 可以使模型获取外部知识之外LoRA 微调可以让模型学会一些特定的技能。比如你想让模型更懂你公司的业务术语或者学会一些特定的操作流程这时候就可以通过 LoRA 微调来实现。LoRA 微调的原理是只训练模型中一小部分参数通常是权重矩阵的低秩分解而保持其他参数冻结不变。这样可以大幅降低微调所需的计算资源和时间同时还能让模型学会新的技能。我一开始的设想是类似之前做视觉模型一样将自定义模型编译为 Hailo NPU 的 .hef 模型先任选一个通用基座进行微调然后导出 ONNX 模型最后将 ONNX 模型编译成 HEF 模型在 Hailo-10 上部署。结果发现目前不支持用户自定义模型只能使用官方提供的模型。这也是目前 Hailo-10 在大模型推理方面的一个限制。不过 Hailo 官方在 Hailo Dataflow Compiler 用户手册的第 4.8 章和第 6 章提供了一种特殊的微调方案仅支持Qwen2-1.5B-Instruct这个模型。需要将微调后的 LoRA adapter 权重挂载到预优化的 HAR 文件中然后量化编译为 HEF 模型。这个过程需要的硬件配置较高需要一块 32GB 显存的 GPU 和 128GB 的内存。下面根据官方文档简单的介绍一下这个流程这里我并没有进行测试。1. 微调在微调时Hailo DFC 有一些硬性约束不满足就编译报错。这意味着不能用常见的r8或r16配置秩必须是 32。这个约束来自 Hailo 硬件对 LoRA 矩阵乘法的专用实现。参数要求lora_alpha必须等于64r秩必须等于32target_modules只能是 FFN 的gate_proj、down_proj、up_projimport torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig from trl import SFTConfig, SFTTrainer # 1. 基础配置 MODEL_ID Qwen/Qwen2-1.5B-Instruct OUTPUT_DIR ./lora_adapter_news # 2. 加载模型与分词器关闭 KV cache 节省显存 model AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtypeauto, device_mapauto) model.config.use_cache False tokenizer AutoTokenizer.from_pretrained(MODEL_ID) # 3. 准备数据集 (AG News) dataset load_dataset(fancyzhx/ag_news, splittest[:1000]) # 取前1000条快速演示 def format_data(example): # 转换为 Qwen 对话格式 messages [ {role: system, content: Classify news into: world, sports, business, sci/tech.}, {role: user, content: example[text]} ] prompt tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) labels [world, sports, business, sci/tech] return {text: prompt labels[example[label]]} formatted_dataset dataset.map(format_data) # 4. Hailo 兼容的 LoRA 配置 peft_config LoraConfig( lora_alpha64, # 必须为 64 r32, # 必须为 32 target_modules[gate_proj, down_proj, up_proj], # 仅支持 FFN 层 task_typeCAUSAL_LM, ) # 5. 开始训练 training_args SFTConfig( dataset_text_fieldtext, max_seq_length512, output_dirOUTPUT_DIR, num_train_epochs1, per_device_train_batch_size4, ) trainer SFTTrainer( modelmodel, train_datasetformatted_dataset, peft_configpeft_config, argstraining_args, ) trainer.train() print(fLoRA 权重已保存至: {OUTPUT_DIR})2. 编译 HEF 模型将基础模型和 LoRA 权重融合量化并编译为专用的二进制.hef文件。需要提前从 Hailo 官方获取 Qwen2-1.5B 的预优化文件放在 ./models 目录下qwen2_1.5b_instruct.q.har预量化基础模型约 40GBqwen2_1.5b_instruct.alls模型脚本qwen2_1.5b_instruct_compilation.alls编译脚本模板import os import tensorflow as tf from hailo_sdk_client.runner.client_runner import ClientRunner MODEL_NAME qwen2_1.5b_instruct ADAPTER_NAME lora_adapter_news # 1. 初始化 Runner 并加载基础 HAR runner ClientRunner(hw_archhailo10h) runner.load_har(f./models/{MODEL_NAME}.q.har) # 2. 挂载阶段一训练出的 LoRA 权重 runner.load_lora_weights( lora_weights_path./lora_adapter_news/checkpoint-250/adapter_model.safetensors, # 请替换为实际checkpoint路径 lora_adapter_nameADAPTER_NAME, ) hn_dict runner.load_model_script(f./models/{MODEL_NAME}.alls) # 3. 构造量化校准数据 # 这里仅使用最基础的 Dummy Data 作为示例实际应用中建议使用格式化后的真实文本 input_ids import numpy as np cache_size hn_dict[net_params][cache_size] dummy_input np.zeros((64, 1, cache_size)) # 64 个全零样本用于跑通编译流程 dummy_cur_pos np.ones((64,)) * cache_size input_dict { f{ADAPTER_NAME}/input_layer1: dummy_input, f{ADAPTER_NAME}/input_layer2: dummy_cur_pos, f{ADAPTER_NAME}/input_layer3: dummy_cur_pos, f{ADAPTER_NAME}/input_layer4: dummy_cur_pos, f{ADAPTER_NAME}/input_layer5: dummy_cur_pos, f{ADAPTER_NAME}/input_layer6: dummy_cur_pos, } print(开始优化 LoRA 权重...) with tf.device(/cpu:0): runner.optimize(input_dict) # 保存中间状态并重新加载以准备编译 runner.save_har(f{MODEL_NAME}.lora.q.har, compilation_onlyFalse) runner ClientRunner(hw_archhailo10h, harf{MODEL_NAME}.lora.q.har) # 4. 替换编译脚本中的占位符 with open(f./models/{MODEL_NAME}_compilation.alls, r) as f: compile_script f.read().replace(LORA_NAME_PLACEHOLDER, ADAPTER_NAME) with open(compile_final.alls, w) as f: f.write(compile_script) runner.load_model_script(compile_final.alls) # 5. 执行最终编译 print(开始编译 HEF (此过程可能需要大量内存与时间)...) runner.compile() runner.save_har(f{MODEL_NAME}.lora.compiled.har, compilation_onlyTrue) print(编译完成可使用 HailoRT 进行推理。)