☰
个人知识库问答机器人:RAG+Agent工程化落地指南
2026/10/6 11:32:46 网站建设 项目流程

1. 这不是又一个“调API”的玩具:为什么个人知识库问答机器人值得你花3天认真搭一遍

我去年在给一家做工业设备维保的客户做AI落地咨询时,被问得最多的问题不是“能不能识别故障图片”,而是“我们十年积累的2000份维修手册、300个典型故障案例、500条现场工程师口述经验,怎么让新来的 technician 3分钟内查到和当前问题最匹配的处置方案?”——这问题背后,藏着所有知识密集型岗位的真实痛点:信息沉在文档里,人却在找信息的路上消耗掉70%的有效工时。而“Agent实践1-个人知识库问答机器人”,表面看是用LangChain搭个RAG流程,实则是一次对“知识如何真正流动起来”的系统性重构。它不依赖大模型原生记忆,不靠人工反复喂提示词,而是把你的PDF、Markdown、Word甚至网页截图(稍作处理)变成可精准召回、可逻辑推理、可溯源验证的活知识网络。核心关键词Agent、个人知识库、问答机器人、RAG、LangChain,每一个都不是孤立概念:Agent是调度大脑,个人知识库是燃料仓,问答机器人是交互界面,RAG是知识注入机制,LangChain是把它们拧成一股绳的工程胶水。适合三类人直接抄作业:需要快速消化行业资料的销售/咨询顾问、管理大量技术文档的工程师、以及想摆脱“搜索引擎+人工筛选”低效循环的任何知识工作者。它不承诺取代你思考,但能确保你每次提问,都站在过去所有经验的肩膀上。

2. 为什么必须放弃“纯向量检索”?从知识库设计源头规避RAG三大经典翻车现场

很多人搭完第一个RAG demo就兴奋地发朋友圈:“我的知识库能回答问题了!”结果三天后发现,问“2023年Q3华东区服务器宕机率最高的三个型号”直接返回“未找到相关信息”,而原文里明明有张表格清清楚楚列着数据。这不是模型不行,是知识库设计从第一步就埋了雷。我见过太多翻车现场,根源都在没想清楚:知识库不是文档仓库,而是问题答案的预演沙盘。下面拆解三个高频陷阱及根治方案。

2.1 陷阱一:把PDF当文本直接切块 → 语义断裂导致关键信息丢失

原始PDF里一页可能同时包含标题、参数表格、注意事项小字、页脚版权声明。若用固定512字符滑动窗口切块,表格会被硬生生劈成两半,参数名和数值分属不同chunk,RAG检索时根本无法关联。我试过某医疗设备说明书,用默认切法后问“该设备最大承重是多少”,返回结果里只有“最大”二字(来自标题)和“kg”(来自页脚单位),中间关键数字彻底消失。
根治方案:结构化预处理优先于向量化。

  • 对PDF:必须用pypdf或pdfplumber先提取完整文本+坐标信息,识别标题层级(H1/H2)、表格边界、列表项。我实测unstructured库在保留表格结构上比纯OCR稳定得多,尤其对扫描件,它会先做版面分析再提取,而非暴力OCR。
  • 对Markdown/Word:利用其天然结构,用正则或python-docx提取章节标题作为chunk元数据,确保“问题-答案”对不被拆散。例如,将“【故障代码E102】→ 原因:电源电压波动>15% → 处置:检查稳压器输出”作为一个完整chunk,而非按字符切。

提示:切块前务必做一次人工抽检。打开你知识库的任意3个chunk,问自己:“如果只看到这个chunk,我能独立回答一个具体问题吗?”不能,则需调整切分策略。

2.2 陷阱二:只存文本,忽略上下文锚点 → 检索结果无法溯源验证

用户问“为什么X型号电机启动时异响?”,RAG返回一段文字:“可能原因包括轴承磨损(见P45)、定子绕组短路(见P67)、安装基座松动(见P89)”。但用户下一步必然要查P45原文确认细节,而你的知识库若没存页码、章节号、甚至原始文件名,他就得手动翻PDF大海捞针。这直接摧毁信任感。
根治方案:为每个chunk注入不可篡改的溯源元数据。

  • 在向量化前,为每个chunk打上四维标签:source_file_name(如电机维护手册_v2.3.pdf)、page_number(PDF页码)、section_title(如“3.2 异响故障诊断树”)、chunk_id(自增序号)。
  • 关键技巧:page_number不能简单取PDF页码,要结合pdfplumber的page.chars坐标计算实际内容所在页。曾有客户手册页眉页脚占满整页,真实内容只在中间1/3区域,按物理页码定位会偏差2页。
  • 存储时,这些元数据必须与向量一同存入向量数据库(如Chroma),且在检索后原样返回。我在前端展示答案时,强制显示“来源:《电机维护手册_v2.3》第45页‘轴承磨损’章节”,点击直接跳转PDF对应位置——这才是知识库该有的样子。

2.3 陷阱三:用通用embedding模型 → 领域术语召回率暴跌50%以上

拿OpenAI的text-embedding-ada-002去嵌入电力系统文档,“断路器开断容量”和“开关额定电流”在向量空间里距离可能比“苹果”和“香蕉”还远。因为通用模型没见过“SF6气体绝缘”这种词,它只能靠字面相似度硬猜。我对比过同一份变电站运维手册:用领域微调过的bge-reranker-base做embedding,对“主变差动保护动作逻辑”的相关chunk召回率是87%,而通用模型仅39%。
根治方案:领域适配embedding + 双路检索兜底。

  • 第一步:选型。中文场景首推bge系列(bge-m3支持多语言混合检索),英文选e5系列。避免用已停更的all-MiniLM-L6-v2,其在长文本理解上明显劣于bge。
  • 第二步:微调(可选但强烈建议)。用你知识库里的100个高质量QA对(如“Q:主变油温超限报警阈值?A:85℃”),用HuggingFace的transformers微调bge,3个epoch就能让领域术语召回率提升30%。
  • 第三步:双路检索。别只信向量检索!同步启用关键词检索(BM25算法),对“断路器”“SF6”“差动保护”这类强实体词,BM25比向量更准。最终结果取向量+BM25加权融合,我在LangChain里用HybridSearchRetriever实现,权重设为0.7:0.3,实测F1值提升22%。

这三步做完,你的知识库才真正从“文档堆”进化成“问题响应引擎”。它不再被动等待检索,而是主动预判用户可能问什么,并把答案所需的上下文提前组装好。

3. LangChain不是胶水,是流水线调度员:Agent架构下RAG模块的精准卡位与协同逻辑

很多教程把LangChain讲成“调用几个链式函数的工具包”,这是致命误解。在Agent架构里,LangChain的核心价值是定义知识流的路由规则与状态契约。它不负责生成答案,但决定“此刻该把问题交给谁、用什么数据、以什么格式”。我把整个流程拆解为四个刚性模块,每个模块都有不可替代的职责边界。

3.1 模块一:Router(路由中枢)——决定问题是否该走知识库

不是所有问题都该查知识库。用户问“今天北京天气怎么样?”,硬塞进RAG只会返回一堆无关的气象站建设规范。Router的作用是实时判断问题意图,分流到不同处理器。

  • 实现逻辑:用轻量级分类器(如sklearn的LinearSVC)训练一个二分类模型,标签为“需知识库”/“无需知识库”。特征用TF-IDF提取问题关键词+规则兜底(含“查”“找”“哪个”“多少”“为什么”等疑问词则高概率需知识库)。
  • 关键参数:我训练时特意加入100条“反例”,如“帮我写一封辞职信”(需LLM生成)、“计算1+1”(需计算器工具),避免Router把泛化问题误判为知识查询。模型准确率需≥92%,低于此值宁可加一条硬规则:“问题长度<5字且含数字,直连LLM”。
  • Agent协同:Router输出是Agent的首个Action。LangChain中用RouterChain封装,其route方法返回{"next": "retriever"}或{"next": "llm"},Agent据此调用后续模块。这步省略,Agent就成了无脑查库的傻瓜。

3.2 模块二:Retriever(检索器)——精准抓取而非海量召回

Retriever不是“搜出Top K个最像的chunk”,而是“找出能直接支撑答案的最小知识单元”。它的输出必须满足两个硬约束:

  • 约束1:数量可控。默认K=3,但需动态调整。问“X型号电机启动异响原因”,返回3个原因即可;但问“2023年华东区所有故障型号清单”,需返回全部匹配项。我在Retriever里加了max_results参数,由Router根据问题类型预设。
  • 约束2:内容可验证。每个返回chunk必须带完整溯源元数据(见2.2节),且chunk内容本身是自洽的。曾有客户知识库返回一个chunk:“...详见第45页”,但该chunk正文空空如也——这是预处理时漏掉了页码锚点。
  • LangChain实现:不用VectorStoreRetriever,改用自定义CustomRetriever类,继承BaseRetriever,重写_get_relevant_documents方法。关键代码段:
def _get_relevant_documents(self, query: str) -> List[Document]: # 先BM25粗筛 bm25_docs = self.bm25_retriever.get_relevant_documents(query) # 再向量精排 vector_docs = self.vector_retriever.get_relevant_documents(query) # 融合去重,按综合分数排序 merged_docs = self._fuse_and_deduplicate(bm25_docs, vector_docs) return merged_docs[:self.max_results] # 动态截断

注意:max_results必须作为Retriever实例的初始化参数传入,而非写死。Agent在调用时会根据Router指令动态设置,这是模块解耦的关键。

3.3 模块三:Generator(生成器)——用知识约束LLM的幻觉

Generator不是把检索结果拼起来扔给LLM。它的核心任务是构建一个让LLM无法胡说的提示词沙盒。我见过太多案例:检索返回“轴承磨损需更换”,LLM却生成“建议用502胶水临时粘合”。

  • 沙盒构建三原则:
    1. 角色锁定:提示词开头强制声明“你是一名资深电机维修工程师,只依据提供的技术文档作答,不编造任何未提及的解决方案”。
    2. 证据绑定:明确要求“每个结论必须引用检索结果中的具体句子,格式为【来源:文件名P页码】”。
    3. 否定清单:列出绝对禁止的表述,如“可能”“大概”“建议尝试”,强制用“确认”“必须”“依据文档第X条”。
  • LangChain实现:用PromptTemplate定义模板,关键变量{context}填入Retriever返回的chunk,{question}是原始问题。模板示例:
你是一名专注工业电机维修的高级工程师。请严格依据以下技术文档片段回答问题,不得添加任何文档未提及的信息。 【文档片段】 {context} 【问题】 {question} 【回答要求】 - 每个技术结论必须标注来源,如【来源:《电机维护手册_v2.3》P45】 - 禁用“可能”“或许”“一般情况下”等模糊表述 - 若文档未提供足够信息,回答“依据当前知识库,该问题暂无明确解决方案”
  • 效果验证:用100个测试问题跑自动化评估,统计“答案中引用来源的比例”和“幻觉率”(答案含文档未提内容)。达标线:引用率≥95%,幻觉率≤2%。

3.4 模块四:Verifier(校验器)——给答案装上最后一道保险

即使Generator很严谨,LLM仍可能因token限制截断引用,或把“P45”错写成“P54”。Verifier是独立于LLM的规则引擎,对Generator输出做机械式校验。

  • 校验项:
    • 溯源标记完整性:检查答案中每个【来源:xxx】是否能在Retriever返回的chunk元数据中找到完全匹配项。
    • 事实一致性:用正则提取答案中的数值(如“85℃”),反向搜索Retriever返回的chunk,确认该数值原文存在。
    • 格式合规性:强制要求答案以“结论句+【来源】”为最小单元,禁止跨单元合并。
  • LangChain集成:Verifier不走LLM链,而是用Python函数实现。Agent执行完Generator后,自动调用verify_answer(answer, retrieved_docs)函数。若校验失败,触发重试机制:降低max_results重新检索,或向用户提示“部分信息需人工确认”。

实操心得:Verifier的规则必须极简。我最初写了20条校验规则,结果1/3的合法答案被误杀。现在只保留3条核心规则:溯源存在性、数值原文匹配、格式单元化。复杂逻辑交给LLM,Verifier只做“有没有、对不对、齐不齐”三件事。

这四个模块环环相扣,Router是哨兵,Retriever是侦察兵,Generator是工程师,Verifier是质检员。LangChain的价值,就是让这四人能在同一套语言(Python对象)下无缝协作,而不是各自为政。

4. 从零到可交付:手把手实现一个抗并发、可溯源、带校验的个人知识库问答机器人

现在把前面所有设计落地为可运行代码。环境基于Python 3.10,核心依赖:langchain==0.1.16,chromadb==0.4.24,unstructured==0.10.22,bge-m3embedding模型。全程不碰任何云服务,所有组件本地运行,Mac/Windows/Linux通吃。

4.1 知识库构建:结构化预处理流水线(含PDF表格识别)

第一步永远是让文档“开口说话”。以下代码是我在3个客户项目中验证过的稳定流程:

# 创建虚拟环境并安装核心依赖 python -m venv rag_env source rag_env/bin/activate # Windows用 rag_env\Scripts\activate pip install langchain chromadb unstructured pypdf python-docx beautifulsoup4 # 安装bge-m3模型(需torch) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu

预处理核心脚本ingest.py:

import os import re from typing import List, Dict, Any from pypdf import PdfReader from unstructured.partition.pdf import partition_pdf from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings class KnowledgeIngestor: def __init__(self, data_dir: str, vector_db_path: str): self.data_dir = data_dir self.vector_db_path = vector_db_path # 初始化embedding模型(本地加载,不联网) self.embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-m3", model_kwargs={'device': 'cpu'}, # Mac M1/M2用 'mps' encode_kwargs={'normalize_embeddings': True} ) def extract_pdf_structure(self, file_path: str) -> List[Dict[str, Any]]: """用unstructured精准提取PDF结构,保留表格和标题""" elements = partition_pdf( filename=file_path, strategy="hi_res", # 高精度模式,识别表格 infer_table_structure=True, # 关键!开启表格结构识别 chunking_strategy="by_title", # 按标题切分,避免表格断裂 max_characters=1000, # 单chunk最大字符数 new_after_n_chars=800, # 每800字符强制换行 combine_text_under_n_chars=300, # 小段落合并 ) documents = [] for i, element in enumerate(elements): # 过滤无意义元素 if not hasattr(element, 'text') or not element.text.strip(): continue # 构建带元数据的Document metadata = { "source_file": os.path.basename(file_path), "page_number": getattr(element, 'metadata', {}).get('page_number', 1), "category": element.category, # 'Table', 'Title', 'Text'等 "chunk_id": i } # 关键:表格内容转为可读文本 if element.category == "Table": text_content = self._table_to_text(element) else: text_content = element.text.strip() documents.append({ "page_content": text_content, "metadata": metadata }) return documents def _table_to_text(self, table_element) -> str: """将unstructured识别的表格转为Markdown表格字符串""" try: # unstructured的table元素有rows属性 rows = getattr(table_element, 'rows', []) if not rows: return "" # 构建Markdown表 markdown_lines = ["| " + " | ".join(rows[0]) + " |"] markdown_lines.append("|" + "|".join(["---"] * len(rows[0])) + "|") for row in rows[1:]: markdown_lines.append("| " + " | ".join(row) + " |") return "\n".join(markdown_lines) except Exception as e: return f"[表格解析异常: {str(e)}]" def create_vector_store(self, documents: List[Dict[str, Any]]): """创建Chroma向量库,存入本地""" texts = [doc["page_content"] for doc in documents] metadatas = [doc["metadata"] for doc in documents] # 使用Chroma持久化存储 vectorstore = Chroma.from_texts( texts=texts, metadatas=metadatas, embedding=self.embeddings, persist_directory=self.vector_db_path ) print(f"✅ 向量库已保存至 {self.vector_db_path}") return vectorstore # 执行示例 if __name__ == "__main__": ingestor = KnowledgeIngestor( data_dir="./docs", # 存放PDF/MD/DOCX的目录 vector_db_path="./chroma_db" ) all_docs = [] for file in os.listdir("./docs"): if file.endswith(".pdf"): pdf_docs = ingestor.extract_pdf_structure(f"./docs/{file}") all_docs.extend(pdf_docs) ingestor.create_vector_store(all_docs)

关键实操点:

  • strategy="hi_res"必须开启,否则表格识别率<30%。
  • infer_table_structure=True是表格转文本的开关,关掉则返回乱码。
  • chunking_strategy="by_title"让切分尊重文档逻辑,比"basic"可靠得多。
  • persist_directory指定本地路径,Chroma会自动创建chroma_db文件夹,无需额外配置。

注意:首次运行会下载bge-m3模型(~2GB),耐心等待。后续运行直接复用本地缓存。

4.2 Agent核心逻辑:Router-Router-Retriever-Generator-Verifier闭环

agent_core.py定义整个问答流水线:

from langchain.chains import LLMChain from langchain.prompts import PromptTemplate from langchain_community.llms import Ollama # 本地LLM,用qwen:7b from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.schema import Document import re class RAGAgent: def __init__(self, vector_db_path: str): self.vectorstore = Chroma( persist_directory=vector_db_path, embedding_function=HuggingFaceEmbeddings( model_name="BAAI/bge-m3" ) ) self.llm = Ollama(model="qwen:7b", temperature=0.1) # 本地LLM,关闭随机性 # Router:轻量分类器 self.router_prompt = PromptTemplate( input_variables=["question"], template="""你是一个问题路由专家。请判断以下问题是否需要查询技术文档知识库。 问题:{question} 回答要求: - 若问题涉及具体产品参数、故障原因、操作步骤、历史数据等,回答"RETRIEVE" - 若问题为通用常识、数学计算、创意写作等,回答"GENERATE" - 只输出一个词:RETRIEVE 或 GENERATE """ ) self.router_chain = LLMChain(llm=self.llm, prompt=self.router_prompt) # Generator提示词(带沙盒约束) self.generator_prompt = PromptTemplate( input_variables=["context", "question"], template="""你是一名专注工业电机维修的高级工程师。请严格依据以下技术文档片段回答问题,不得添加任何文档未提及的信息。 【文档片段】 {context} 【问题】 {question} 【回答要求】 - 每个技术结论必须标注来源,如【来源:《电机维护手册_v2.3》P45】 - 禁用“可能”“或许”“一般情况下”等模糊表述 - 若文档未提供足够信息,回答“依据当前知识库,该问题暂无明确解决方案” """ ) self.generator_chain = LLMChain(llm=self.llm, prompt=self.generator_prompt) def route_question(self, question: str) -> str: """Router决策""" result = self.router_chain.run(question=question).strip() return "RETRIEVE" if "RETRIEVE" in result else "GENERATE" def retrieve(self, question: str, k: int = 3) -> List[Document]: """Retriever:双路检索""" # BM25检索(用Chroma内置) bm25_results = self.vectorstore.similarity_search( question, k=k*2, search_type="mmr" # MMR减少冗余 ) # 向量检索 vector_results = self.vectorstore.similarity_search( question, k=k, search_type="similarity" ) # 融合(简单去重) all_docs = bm25_results + vector_results unique_docs = [] seen = set() for doc in all_docs: key = (doc.page_content[:50], doc.metadata.get("source_file", "")) if key not in seen: seen.add(key) unique_docs.append(doc) return unique_docs[:k] def generate_answer(self, question: str, retrieved_docs: List[Document]) -> str: """Generator:沙盒化生成""" context = "\n\n".join([f"【来源:{doc.metadata['source_file']} P{doc.metadata.get('page_number', 1)}】\n{doc.page_content}" for doc in retrieved_docs]) return self.generator_chain.run(context=context, question=question) def verify_answer(self, answer: str, retrieved_docs: List[Document]) -> bool: """Verifier:三重校验""" # 1. 溯源标记存在性 sources_in_answer = re.findall(r"【来源:(.*?)】", answer) for src in sources_in_answer: if not any(src in f"{doc.metadata['source_file']} P{doc.metadata.get('page_number', 1)}" for doc in retrieved_docs): return False # 2. 数值一致性(简化版:检查答案中数字是否在原文出现) numbers_in_answer = re.findall(r"\d+\.?\d*", answer) for num in numbers_in_answer: if not any(num in doc.page_content for doc in retrieved_docs): return False # 3. 格式单元化(检查是否有未闭合的【来源】) if answer.count("【来源:") != answer.count("】"): return False return True def run(self, question: str) -> str: """Agent主流程""" # Step 1: Router route = self.route_question(question) if route == "GENERATE": return self.llm(question) # 直连LLM # Step 2: Retriever retrieved = self.retrieve(question, k=3) if not retrieved: return "未在知识库中找到相关信息。" # Step 3: Generator raw_answer = self.generate_answer(question, retrieved) # Step 4: Verifier if self.verify_answer(raw_answer, retrieved): return raw_answer else: # 校验失败,降级重试 retrieved_fallback = self.retrieve(question, k=5) fallback_answer = self.generate_answer(question, retrieved_fallback) return f"[校验警告] 原始答案存在风险,已降级重试:{fallback_answer}" # 使用示例 if __name__ == "__main__": agent = RAGAgent("./chroma_db") # 测试问题 test_q = "X型号电机启动时异响的可能原因有哪些?" print(agent.run(test_q))

部署要点:

  • Ollama需提前安装(https://ollama.com),qwen:7b模型用ollama pull qwen:7b下载。
  • search_type="mmr"(最大边际相关性)比纯similarity更能避免返回相似重复chunk。
  • Verifier的数值校验做了简化,生产环境可接入spaCy做实体链接,但对个人知识库,正则已够用。

实操心得:Router的prompt必须用temperature=0.1,否则LLM会随机输出“RETRIEVE”或“GENERATE”,导致流程崩溃。这是踩过的坑。

4.3 Web服务层:FastAPI暴露问答接口(支持并发与日志追踪)

最后一步,让机器人走出命令行,变成可多人访问的服务:

# app.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel import logging from agent_core import RAGAgent app = FastAPI(title="Personal Knowledge Base QA API") # 初始化Agent(单例,避免重复加载模型) agent = RAGAgent("./chroma_db") # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class QueryRequest(BaseModel): question: str class QueryResponse(BaseModel): answer: str retrieved_sources: list # 返回溯源信息,供前端展示 @app.post("/qa", response_model=QueryResponse) async def ask_question(request: QueryRequest): try: logger.info(f"收到问题: {request.question}") # Agent执行 answer = agent.run(request.question) # 提取溯源信息(简化版) sources = [] for match in re.finditer(r"【来源:(.*?)】", answer): sources.append(match.group(1)) logger.info(f"返回答案: {answer[:50]}...") return {"answer": answer, "retrieved_sources": list(set(sources))} except Exception as e: logger.error(f"处理问题时出错: {str(e)}") raise HTTPException(status_code=500, detail="服务器内部错误") @app.get("/health") async def health_check(): return {"status": "healthy", "vector_db": "ready"}

启动命令:

# 安装FastAPI和Uvicorn pip install fastapi uvicorn # 启动服务(默认端口8000) uvicorn app:app --reload --host 0.0.0.0 --port 8000

并发保障技巧:

  • uvicorn默认是单进程,但--workers 4可启4个worker进程,轻松扛住50+并发。
  • 关键:Agent实例在全局初始化,避免每个请求都重建向量库和LLM连接。
  • 日志记录question和answer前50字符,便于事后审计问题质量。

注意:Mac M1/M2芯片需在Ollama启动时加--gpus all参数启用GPU加速,否则qwen:7b响应慢。

5. 真实世界踩坑实录:那些文档没写的、但让你加班到凌晨的12个问题与解法

理论再完美,落地时总被现实毒打。以下是我在6个真实项目中记录的、文档绝不会提但足以让你崩溃的细节,附带血泪解决方案。

5.1 PDF扫描件文字识别率低?别急着换OCR引擎,先做三步预处理

客户给的100份设备手册全是手机拍的扫描PDF,直接丢进unstructured,识别率不到40%。折腾两天换Tesseract、PaddleOCR,效果更差。后来发现症结不在OCR,而在输入质量:

  • 问题1:阴影干扰。手机拍摄时边缘有暗角,OCR引擎误判为文字区域。
    解法:用opencv-python做自适应阈值二值化。代码片段:
    import cv2 img = cv2.imread("scan.jpg", 0) # 自适应高斯阈值,消除渐晕 binary = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) cv2.imwrite("clean_scan.jpg", binary)
  • 问题2:装订孔遮挡。左侧2cm被孔洞遮住,OCR跳过整行。
    解法:用pdfplumber提取页面坐标,裁剪掉装订区再OCR。
  • 问题3:字体模糊。老式打印机输出的宋体字,笔画粘连。
    解法:cv2.morphologyEx做形态学开运算,分离粘连笔画。

最终效果:预处理后识别率从38%升至92%,比换OCR引擎快10倍。

5.2 向量库检索结果“看似相关实则无关”?检查embedding维度是否对齐

某次上线后,用户问“冷却液更换周期”,返回结果全是“润滑油检测标准”。查向量库发现,所有chunk的embedding向量长度是1024,但bge-m3模型输出是1024维,而Chroma默认用768维。原来HuggingFaceEmbeddings初始化时没指定model_kwargs,用了默认维度。
解法:强制指定维度,在HuggingFaceEmbeddings中加:

embeddings = HuggingFaceEmbeddings( model_name="BAAI/bge-m3", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True}, # 关键!显式指定输出维度 show_progress=True ) # 并确认Chroma创建时维度一致 vectorstore = Chroma.from_documents( documents=docs, embedding=embeddings, persist_directory="./db" )

提示:用len(embeddings.embed_query("test"))验证维度,必须与向量库创建时一致。

5.3 LLM生成答案突然变长且带幻觉?检查token计数与截断逻辑

用qwen:7b时,某次问答返回3000字答案,其中2000字是胡编的“电机发展史”。排查发现,Ollama的num_predict参数默认不限制,LLM自由发挥。
解法:在Ollama初始化时强制截断:

self.llm = Ollama( model="qwen:7b", temperature=0.1, num_predict=512, # 严格限制输出长度 stop=["【来源:"] # 遇到溯源标记即停止,防截断引用 )

实操心得:stop参数比num_predict更可靠,因为LLM可能在512token内就生成完整答案,但若没遇到stop词,会继续胡说。

5.4 知识库更新后检索失效?不是向量库没刷新,是缓存没清

客户新增了20份文档,重新运行ingest.py,但旧问题仍查不到新内容。Chroma的persist_directory有缓存机制,from_documents不会自动覆盖旧数据。
解法:每次更新前,删除旧向量库文件夹:

import shutil if os.path.exists("./chroma_db"): shutil.rmtree("./chroma_db") # 再执行ingest.py

注意:生产环境需加版本号,如./chroma_db_v2,避免误删。

5.5 本地LLM响应慢如蜗牛?关闭不必要的日志和采样

qwen:7b在Mac M1上首字延迟15秒,CPU占用95%。Ollama默认开启详细日志和温度采样。
解法:启动Ollama时加参数:

ollama serve --log-level error --num-gpu 1

并在Python中:

self.llm = Ollama( model="qwen:7b", temperature=0.0, # 关闭随机性 num_ctx=4096, # 显式设置上下文长度 num_predict=512, verbose=False # 关闭日志

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询