1. 项目概述当本地PDF遇上智能对话最近在折腾一个挺有意思的东西一个叫“Local_Pdf_Chat_RAG”的项目。简单来说它让你能和自己的PDF文档“聊天”。想象一下你手头有一堆技术手册、研究报告或者合同文件每次想找某个具体信息都得手动翻半天或者用CtrlF搜一堆不相关的关键词。这个项目就是为了解决这个痛点把你的PDF文档“喂”给一个本地运行的AI模型然后你可以像问同事一样用自然语言直接提问它能从文档里找到最相关的信息并组织成通顺的答案告诉你。这个项目的核心是RAG检索增强生成技术。它不是让AI凭空想象而是先在你的文档库里精准“检索”出与问题最相关的片段再把这些片段作为“增强”的上下文交给大语言模型去“生成”最终答案。这样一来答案的准确性、可靠性就大大提升了因为它的回答有据可查。最吸引我的是“Local”本地这个前缀意味着整个流程——从文档解析、向量化存储到模型推理——都可以在你的个人电脑上完成。数据不出本地隐私和安全得到了最大程度的保障这对于处理敏感的商业文档或个人资料来说是刚需。这个项目适合谁呢我觉得覆盖面很广。对于研究人员和学生可以快速从海量论文中提取观点和论据对于法务、金融从业者能高效审阅合同条款和财报数据对于开发者则是学习RAG技术栈一个非常棒的实战案例。它把前沿的AI应用能力以一种相对轻量、可掌控的方式带到了每个人的桌面上。接下来我就把自己从零搭建、调试到优化这个系统的完整过程以及踩过的坑和总结的经验毫无保留地分享出来。2. 核心架构与工具选型解析2.1 技术栈全景图为什么是这些组件要理解这个项目得先拆解它的技术栈。这就像盖房子得先选好地基、梁柱和砖瓦。整个系统可以清晰地分为四个层次文档处理层、向量化与存储层、检索与推理层、应用交互层。在文档处理层核心任务是“读懂”PDF。这里我选择了PyPDF2和pdfplumber的组合。PyPDF2 老牌稳定提取基础文本和元数据够用但对付复杂的排版和表格就有点力不从心了。pdfplumber 是后起之秀它在解析页面布局、精准定位文本块和提取表格数据方面表现更出色能更好地保留原文的结构信息。对于扫描版PDF则需要OCR光学字符识别技术Tesseract是开源首选配合Pillow进行图像预处理能显著提升识别率。注意PDF解析是RAG流水线的第一个“垃圾进垃圾出”的环节。如果这里文本提取得杂乱无章、丢失了段落或表格信息后面检索的质量会大打折扣。务必根据你的PDF类型文本型/扫描型和复杂度选择合适的解析库甚至组合使用。向量化与存储层是项目的“记忆中枢”。我们需要把文本转换成计算机能理解的数学形式——向量或称嵌入。这里我测试了多个模型最终选用了Sentence Transformers库中的all-MiniLM-L6-v2模型。为什么是它首先它在MTEB等权威基准测试中在语义相似度任务上表现优异且模型尺寸小约80MB推理速度快非常适合本地部署。相比OpenAI的Embedding API它零成本、零延迟且完全离线。存储方面ChromaDB成为了我的首选向量数据库。它轻量、易用纯Python实现支持持久化存储并且提供了非常简洁的API进行相似性搜索。相比Faiss更像一个算法库或Pinecone云服务Chroma在本地小规模场景下的开箱即用体验是最好的。检索与推理层是大脑。检索部分我们利用ChromaDB根据问题向量找到最相关的文档片段。这里的关键是“检索策略”我采用了最经典的“稠密向量检索”Dense Retrieval它基于语义相似度比传统的关键词匹配如BM25更能理解意图。例如搜索“机器学习如何评估模型”关键词匹配可能找不到“AUC-ROC”或“交叉验证”但语义检索可以。推理部分即大语言模型我选择了Ollama来本地运行Llama 2 7B或Mistral 7B这类开源模型。Ollama极大地简化了在本地运行大模型的过程一条命令就能拉取和启动模型。选择7B参数量的模型是因为它在消费级GPU甚至强力的CPU上可以实现可接受的推理速度几秒到十几秒生成答案在效果和资源消耗间取得了很好的平衡。应用交互层我使用Gradio快速构建了一个Web界面。它让我能通过浏览器上传PDF、输入问题、查看答案和检索到的源文档整个过程无需编写复杂的前端代码。对于想要更深度集成的开发者也可以很容易地将核心功能封装成API供其他系统调用。2.2 关键设计决策与权衡在搭建过程中每一步都面临选择。首先是“分块”Chunking策略。你不能把整本100页的PDF直接扔给模型它有关注长度限制且信息太分散。常见的分块方式有按固定字符数分割、按段落分割、按语义分割。我最初使用简单的固定大小如500字符重叠分块发现容易把完整的句子或表格拦腰截断。后来切换到基于自然语言处理工具如NLTK或spaCy的句子边界检测进行分块并允许一定的重叠例如50字符这样既保证了块的大小可控又极大减少了语义断裂的问题。其次是上下文窗口与提示工程。检索到的文档块假设我们取前3个最相关的需要和用户问题一起构造成一个“提示”Prompt送给大模型。这里的模板设计至关重要。一个糟糕的提示可能让模型忽略上下文自己瞎编。我经过多次试验确定的提示模板大致如下请基于以下提供的上下文信息回答用户的问题。如果上下文信息不足以回答问题请直接说明“根据已知信息无法回答”不要编造信息。 上下文 {context_chunk_1} {context_chunk_2} {context_chunk_3} 问题{user_question} 请给出专业、准确的回答这个模板明确指令了模型的角色、知识来源和回答边界能有效减少“幻觉”即模型虚构答案的发生。最后是性能与精度的权衡。更大的嵌入模型如all-mpnet-base-v2可能效果更好但更慢、更占内存。检索时返回更多的文档块如 top_k5可能提供更全面的上下文但也会增加模型处理的开销和引入无关信息的风险。在本地资源有限的情况下我通过A/B测试用一组标准问题测试不同配置的答案质量最终将top_k定为3在大多数场景下取得了速度和精度的最佳平衡。3. 从零开始的完整搭建与配置指南3.1 环境准备与依赖安装工欲善其事必先利其器。首先确保你的Python环境是3.8或以上版本。我强烈建议使用Conda或venv创建独立的虚拟环境避免包依赖冲突。# 使用 conda 创建环境 conda create -n pdf_chat_rag python3.10 conda activate pdf_chat_rag # 或者使用 venv python -m venv pdf_chat_rag_env source pdf_chat_rag_env/bin/activate # Linux/Mac # pdf_chat_rag_env\Scripts\activate # Windows接下来安装核心依赖。我整理了一个requirements.txt文件涵盖了从解析到界面的所有必要库。# requirements.txt langchain0.1.0 chromadb0.4.22 sentence-transformers2.2.2 pypdf23.0.1 pdfplumber0.10.3 pytesseract0.3.10 pillow10.2.0 ollama0.1.30 gradio4.19.2 nltk3.8.1 # 可选如果需要更高级的分块或NER可以添加 spacy使用pip一键安装pip install -r requirements.txt此外还有一些系统级依赖需要处理。如果你需要OCR功能必须安装Tesseract-OCR引擎。Ubuntu/Debian:sudo apt-get install tesseract-ocrmacOS:brew install tesseractWindows: 从 GitHub 下载安装程序并安装。安装后可能需要将Tesseract的路径如C:\Program Files\Tesseract-OCR\tesseract.exe添加到系统环境变量PATH中或者在代码中通过pytesseract.pytesseract.tesseract_cmd指定。对于NLTK的数据包用于句子分割第一次运行时需要在Python中下载import nltk nltk.download(punkt)最后安装并启动Ollama服务。前往 Ollama官网 下载对应操作系统的安装包。安装完成后打开终端拉取一个合适的模型例如7B参数的ollama pull llama2:7b # 或者 mistral:7b运行ollama serve启动服务它会默认在http://localhost:11434提供API。3.2 核心模块代码实现与详解环境就绪后我们开始编写核心代码。我将系统分为几个模块文档加载器、文本处理器、向量化存储器和问答链。第一步文档加载与预处理模块这个模块负责读取PDF并提取纯净的文本。import PyPDF2 import pdfplumber from typing import List, Union import pytesseract from PIL import Image import io class PDFProcessor: def __init__(self, use_ocrFalse): self.use_ocr use_ocr def extract_text_from_pdf(self, pdf_path: str) - List[str]: 提取PDF文本返回页面文本列表 pages_text [] try: # 优先使用 pdfplumber 提取文本型PDF with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text page.extract_text() if text and text.strip(): pages_text.append(text) elif self.use_ocr: # 如果文本提取为空且启用OCR则处理页面图像 img page.to_image(resolution300) img_bytes io.BytesIO() img.save(img_bytes, formatPNG) ocr_text self._ocr_image(img_bytes) pages_text.append(ocr_text) except Exception as e: print(fpdfplumber 处理失败尝试 PyPDF2: {e}) # 降级方案使用 PyPDF2 with open(pdf_path, rb) as file: reader PyPDF2.PdfReader(file) for page in reader.pages: text page.extract_text() pages_text.append(text if text else ) return pages_text def _ocr_image(self, image_bytes: io.BytesIO) - str: 对图像进行OCR识别 try: image Image.open(image_bytes) # 可在此处添加图像预处理如灰度化、二值化、降噪等 # image image.convert(L) # 转为灰度 text pytesseract.image_to_string(image, langengchi_sim) # 中英文识别 return text except Exception as e: print(fOCR识别失败: {e}) return 这个类提供了两种提取路径并设置了降级策略增强了鲁棒性。第二步文本分块与向量化模块提取出的文本需要被切割成适合处理的“块”并转换为向量。from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings from nltk.tokenize import sent_tokenize import hashlib class VectorStoreManager: def __init__(self, embedding_model_nameall-MiniLM-L6-v2, persist_directory./chroma_db): self.embedding_model SentenceTransformer(embedding_model_name) self.client chromadb.Client(Settings( chroma_db_implduckdbparquet, persist_directorypersist_directory )) # 获取或创建集合类似于数据库的表 self.collection self.client.get_or_create_collection(namepdf_documents) def chunk_text(self, pages: List[str], chunk_size500, overlap50) - List[str]: 将页面文本分割成有重叠的块 chunks [] for page in pages: sentences sent_tokenize(page) # 使用NLTK按句子分割 current_chunk for sentence in sentences: if len(current_chunk) len(sentence) chunk_size: current_chunk sentence else: if current_chunk: chunks.append(current_chunk.strip()) current_chunk sentence if current_chunk: # 添加最后一个块 chunks.append(current_chunk.strip()) # 简单的重叠处理实际上更优的方案是在句子边界处进行滑动窗口 # 这里为了简化采用后处理方式创建重叠 if overlap 0: overlapped_chunks [] for i in range(len(chunks)): start max(0, i - 1) end min(len(chunks), i 2) context .join(chunks[start:end]) overlapped_chunks.append(context) chunks overlapped_chunks return chunks def add_documents(self, pdf_path: str): 处理PDF并添加到向量数据库 processor PDFProcessor(use_ocrFalse) pages processor.extract_text_from_pdf(pdf_path) chunks self.chunk_text(pages) # 生成嵌入向量 embeddings self.embedding_model.encode(chunks, show_progress_barTrue).tolist() # 为每个块生成唯一ID例如基于内容哈希 ids [hashlib.md5(chunk.encode()).hexdigest() for chunk in chunks] metadatas [{source: pdf_path, chunk_index: i} for i in range(len(chunks))] # 添加到集合 self.collection.add( embeddingsembeddings, documentschunks, metadatasmetadatas, idsids ) print(f已成功添加 {len(chunks)} 个文本块到向量数据库。) return len(chunks) def search_similar(self, query: str, top_k3): 检索与查询最相似的文本块 query_embedding self.embedding_model.encode([query]).tolist() results self.collection.query( query_embeddingsquery_embedding, n_resultstop_k ) # results 结构{ids: [...], distances: [...], metadatas: [...], documents: [...]} return results这个类封装了从分块、编码到存储和检索的全过程。注意我使用了基于句子的分块和简单的重叠策略来提升检索质量。第三步问答链与模型交互模块这是连接检索结果和大模型的桥梁。import requests import json class RAGQASystem: def __init__(self, vector_store: VectorStoreManager, ollama_base_urlhttp://localhost:11434): self.vector_store vector_store self.ollama_url ollama_base_url.rstrip(/) /api/generate self.model_name llama2:7b # 可配置 def generate_prompt(self, query: str, context_docs: List[str]) - str: 构造提示词模板 context_str \n\n.join([f[上下文片段 {i1}]: {doc} for i, doc in enumerate(context_docs)]) prompt f你是一个专业的文档分析助手。请严格根据以下提供的上下文信息来回答问题。如果上下文信息中没有答案请明确说“根据提供的资料我无法回答这个问题”。不要利用你自身的外部知识进行推测或编造。 相关上下文信息 {context_str} 用户问题{query} 请基于以上上下文给出准确、简洁的回答 return prompt def ask(self, query: str) - dict: 核心问答函数 # 1. 检索 search_results self.vector_store.search_similar(query, top_k3) if not search_results[documents] or not search_results[documents][0]: return {answer: 未在文档中找到相关信息。, sources: []} context_docs search_results[documents][0] source_metadatas search_results[metadatas][0] # 2. 生成提示 prompt self.generate_prompt(query, context_docs) # 3. 调用 Ollama API payload { model: self.model_name, prompt: prompt, stream: False, options: { temperature: 0.1, # 低温度让输出更确定、更贴近上下文 top_p: 0.9, num_predict: 512 # 限制生成长度 } } try: response requests.post(self.ollama_url, jsonpayload, timeout60) response.raise_for_status() result response.json() answer result.get(response, ).strip() except requests.exceptions.RequestException as e: answer f调用模型时出错: {e} # 4. 返回答案和来源 return { answer: answer, sources: source_metadatas }这个类定义了完整的RAG流程。temperature参数设置为较低的0.1是为了让模型更专注于上下文减少天马行空的“创作”。3.3 使用Gradio构建用户界面有了核心引擎我们需要一个友好的界面。Gradio能在几分钟内搞定。import gradio as gr import os # 初始化系统 vector_mgr VectorStoreManager() qa_system RAGQASystem(vector_mgr) def process_and_chat(pdf_file, question, history): 处理上传的PDF并回答问题 if pdf_file is None: return 请先上传一个PDF文件。, history file_path pdf_file.name # 检查是否已处理过该文件简单示例实际可更复杂 # 这里我们假设每次上传都是新文件直接添加 try: vector_mgr.add_documents(file_path) response qa_system.ask(question) answer response[answer] sources response[sources] # 格式化历史记录 new_history history [(question, answer)] source_info \n.join([f- 来源: {meta[source]} (块{meta[chunk_index]}) for meta in sources]) full_response f{answer}\n\n**参考来源**\n{source_info} return full_response, new_history except Exception as e: return f处理过程中发生错误: {str(e)}, history # 构建界面 with gr.Blocks(title本地PDF智能问答助手) as demo: gr.Markdown(# 本地PDF智能问答助手 (RAG)) gr.Markdown(上传你的PDF文档然后就可以用自然语言向它提问了。所有处理均在本地完成保障数据隐私。) with gr.Row(): with gr.Column(scale1): pdf_input gr.File(label上传PDF文件, file_types[.pdf]) question_input gr.Textbox(label输入你的问题, lines3, placeholder例如这份报告的主要结论是什么第三页提到的关键技术指标是多少) submit_btn gr.Button(提交问题, variantprimary) with gr.Column(scale2): chatbot gr.Chatbot(label对话历史, height400) answer_output gr.Markdown(label详细答案与来源) # 绑定事件 submit_btn.click( fnprocess_and_chat, inputs[pdf_input, question_input, chatbot], outputs[answer_output, chatbot] ) # 示例问题 gr.Examples( examples[ [请总结本文档的核心内容。, 文档的主要观点有哪些], [第5页的图表说明了什么, 关于XX方法作者提出了什么优缺点] ], inputs[question_input] ) if __name__ __main__: demo.launch(server_name0.0.0.0, server_port7860, shareFalse)运行这个脚本在浏览器中打开http://localhost:7860一个功能完整的本地PDF问答应用就呈现在眼前了。你可以上传PDF等待它处理首次处理需要一些时间生成嵌入向量然后开始提问。4. 深度优化与生产级考量4.1 性能调优与精度提升技巧基础版本跑通后你会发现一些可以优化的点。首先是嵌入模型的选择。all-MiniLM-L6-v2是速度和效果的折中。如果你更追求精度并且有足够的GPU内存8GB可以尝试更大的模型如all-mpnet-base-v2420MB它在语义搜索任务上通常有更好的表现。更换模型非常简单只需在初始化VectorStoreManager时更改embedding_model_name参数即可。但请注意更大的模型会显著增加文档处理编码和查询时的延迟与内存占用。其次是分块策略的精细化。固定句子分割对于技术文档可能还不够。一个更高级的策略是使用语义分块即尝试在语义边界如章节、子章节处进行分割。你可以利用spaCy的句法分析能力或者使用专门的分块库如langchain.text_splitter中的RecursiveCharacterTextSplitter它尝试按字符递归分割先按段落再按句子再按单词能更好地保持语义完整性。此外对于包含大量表格的PDF可以考虑使用camelot或tabula-py库专门提取表格数据并将其转换为结构化的文本描述如“表1显示了2023年各季度营收数据为Q1: 100万 Q2: 150万...”再与其他文本一起分块。检索阶段的优化也至关重要。除了简单的相似度搜索top_k可以引入重排序Re-ranking技术。即先用一个快速的检索器如我们现在的向量检索召回较多的候选文档例如 top_k10再用一个更精细但更慢的交叉编码器Cross-Encoder模型对候选文档进行精排选出最相关的2-3个。Sentence Transformers也提供了交叉编码器模型如cross-encoder/ms-marco-MiniLM-L-6-v2这能进一步提升答案的相关性。最后是提示工程的迭代。你的提示词是指导模型的“宪法”。可以尝试不同的指令风格比如角色扮演“你是一个严谨的法律文档分析师请仅根据以下合同条款回答问题...”分步思考“首先从上下文中找出与问题直接相关的语句。然后综合这些信息组织答案...”输出格式“请用列表的形式给出答案的关键点。”或“请先给出是/否的判断再解释原因。” 通过设计不同的提示模板并进行小规模测试可以找到最适合你文档类型和问题风格的模板。4.2 扩展性与可靠性设计要让这个系统更健壮、更实用还需要考虑以下几点。多文档管理与增量更新目前的示例每次上传都会新建集合。在实际应用中你需要管理多个文档。可以为每个文档集或每个用户创建独立的ChromaDB集合或者在同一集合中使用元数据如doc_id进行区分。实现增量更新时需要判断文档是否已处理过。一个简单的方法是计算文档的哈希值如MD5将其与向量存储中的元数据对比。如果文档已存在可以选择跳过或者实现更复杂的“更新”逻辑先删除旧块再插入新块。对话历史与多轮问答当前的系统是单轮的即每个问题独立。要实现多轮对话如追问“能详细说说这一点吗”需要将历史对话也纳入上下文。一种方法是在构造提示时将最近的几轮问答历史也拼接进去。但要注意上下文长度限制。更优雅的方式是使用LangChain或LlamaIndex这类框架它们内置了对话记忆管理、链式调用等高级抽象能大大简化开发。错误处理与日志在生产环境中必须加入完善的错误处理。PDF解析可能失败OCR可能识别率低模型服务可能宕机网络可能超时。代码中每个关键步骤文件读取、解析、编码、检索、模型调用都应该有try-except块并记录详细的日志使用logging模块便于排查问题。对于用户应返回友好的错误信息而不是原始的异常堆栈。部署与资源管理如果你想让同事也能用可以考虑将系统打包成Docker容器。一个典型的Dockerfile会包含Python环境、Tesseract依赖、模型下载脚本等。对于资源管理需要注意大模型和向量数据库的内存占用。可以设置一个后台任务定期清理长时间未使用的临时向量数据库文件或者实现模型的动态加载/卸载。5. 常见问题排查与实战心得5.1 典型问题与解决方案速查表在实际搭建和使用的过程中我遇到了不少坑。这里把它们整理成表希望能帮你快速定位问题。问题现象可能原因排查步骤与解决方案上传PDF后处理失败提示编码错误1. PDF文件本身损坏或加密。2. 中文字体编码问题。1. 尝试用其他PDF阅读器打开确认文件正常。如有密码需先解密。2. 在pdfplumber.open()时尝试添加laparams参数优化布局分析或降级使用PyPDF2。对于复杂中文确保系统有中文字体。OCR识别结果全是乱码或空白1. Tesseract未安装或路径未配置。2. 图像质量太差如倾斜、阴影、低分辨率。3. 语言包未安装。1. 在命令行运行tesseract --version确认安装。在代码中通过pytesseract.pytesseract.tesseract_cmd指定绝对路径。2. 在_ocr_image方法中增加图像预处理步骤转为灰度、二值化、降噪、纠偏。3. 安装对应语言包如中文sudo apt-get install tesseract-ocr-chi-sim。检索结果完全不相关1. 嵌入模型不适合领域。2. 文本分块不合理破坏了语义。3. 查询表述太模糊。1. 尝试更换嵌入模型。对于专业领域如医学、法律可寻找或微调领域特定的嵌入模型。2. 调整分块大小和重叠率。尝试使用语义分块或按章节分块。3. 尝试用更具体、包含关键实体的方式提问。模型回答“根据上下文无法回答”但明明上下文中有答案1. 检索到的相关片段排名不够靠前未进入top_k。2. 提示词指令不够明确模型未充分利用上下文。3. 上下文太长模型注意力分散。1. 增加top_k值例如从3调到5。考虑引入重排序模型。2. 强化提示词例如“你必须从以下上下文中找到答案并引用原文。”3. 尝试对检索到的上下文进行摘要或提取最关键句子后再喂给模型。回答速度非常慢1. 嵌入模型或LLM模型太大。2. 未使用GPU加速。3. ChromaDB查询未优化。1. 换用更小的模型如all-MiniLM-L6-v2和Mistral 7B。2. 确认sentence-transformers和ollama是否在GPU上运行。安装对应CUDA版本的PyTorch。3. 确保ChromaDB的索引类型适合你的数据规模。对于大规模数据考虑使用hnswlib作为索引。Ollama服务调用超时或失败1. Ollama服务未启动。2. 模型未下载。3. 内存不足模型加载失败。1. 在终端运行ollama serve并检查http://localhost:11434是否可访问。2. 运行ollama list确认模型存在或使用ollama pull下载。3. 检查系统内存和显存。尝试换用更小的模型如llama2:7b-mistral:7b后者在某些任务上效率更高。5.2 从个人实践中提炼的独家心得关于PDF解析的“脏活累活”不要指望一个库能通吃所有PDF。我处理过上百份格式各异的PDF最稳健的策略是“组合拳”先用pdfplumber试如果提取的文本质量差比如字符粘连、顺序错乱就换PyPDF2试试如果两者都不行且文件是扫描件果断上OCR。对于包含复杂三线表的学术论文camelot是神器。花在数据清洗和预处理上的时间绝对会在后续的检索和问答质量上得到回报。向量数据库不是“一存了之”ChromaDB默认使用内存存储重启后数据就没了。创建客户端时一定要设置persist_directory参数它会将数据持久化到磁盘。另外定期检查向量数据库的存储目录如果文档库频繁更新旧的嵌入数据可能会成为“僵尸数据”占用空间。可以考虑实现一个基于时间戳或版本的简单清理机制。提示词是“指挥棒”不是“魔法咒语”一开始我总想写出一个万能提示词后来发现针对不同类型的文档和提问风格微调提示词效果立竿见影。对于法律合同强调“严格依据条款”对于技术报告可以要求“分点列出”对于创意文稿则可以放松温度temperature让回答更有趣。把常用的提示模板保存下来做成可配置的选项。本地部署的“甜点区”在个人电脑尤其是没有独立显卡的笔记本上运行7B模型生成速度可能在10-20秒。如果你的问题主要是事实检索答案能在文档中直接找到可以尝试更小的、专门为检索后问答优化的模型比如Phi-2 (2.7B)或Gemma (2B)它们的响应速度会快很多。不要盲目追求大模型合适的就是最好的。从玩具到工具的最后一公里让这个系统真正用起来UI的细节很重要。在Gradio界面里我增加了“处理状态提示”上传后显示“正在解析和索引文档请稍候...”以及“显示参考来源”的可折叠区域。更重要的是我实现了一个“会话管理”功能允许用户新建对话、加载不同的PDF文档集避免了不同文档之间的交叉干扰。这些用户体验的优化比单纯提升1%的准确率更能留住用户。