ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零开始做AI工程:文档问答系统全流程实践与踩坑复盘

从零开始做AI工程:文档问答系统全流程实践与踩坑复盘 从2023年底开始我就一直在折腾一个问题一个没有AI基础的后端工程师要怎么真正进入ai-engineering这个领域。市面上课程很多但大多从什么是梯度下降讲起和我想要的怎么把一个AI功能真正做上线完全不搭。后来我给自己定了一个规矩——不刷课直接做一个完整的项目从零开始不含糊。这个项目就叫 ai-engineering-from-scratch核心目标是把一堆散乱的技术笔记变成一个能自然语言问答的文档问答系统。整个过程走下来最大的感受是AI工程的门槛不在算法而在搞清楚系统怎么拼起来、上线了怎么不崩、答错了怎么办。这篇文章不是教程复读机是我完整项目的复盘。我不是算法专家只是一个做过后端、踩过部署坑、最后把AI系统跑上线的普通工程师。如果你也准备从零开始做AI工程或者正在犹豫要不要迈进这个方向这篇文章应该能帮你少走不少弯路。我会把项目从需求定义、技术选型、数据处理、检索链路、到评估和部署的完整过程以及那些文档里根本不会写的坑全部摊开来讲。1. 把从零开始翻译成一个具体问题文档问答系统的需求边界很多人在从零开始学AI工程上栽跟头不是因为不够努力而是因为不知道起点到底在哪。我以为要先补半年数学才能动手实际上真正该做的第一件事是把自己要解决的问题描述清楚。这个步骤听起来不酷但决定了后面几十个小时的实操是否走偏。1.1 起点的真实状态我有什么、缺什么我手头真实的资源是这样的一个人一台带16G内存的MacBook没有GPU一堆多年积累的技术笔记包括Markdown文件、若干本PDF电子书、几十个网页剪藏零AI项目经验但有五年后端开发基础Python写得不费劲。缺的东西也很明确没有现成的数据管线没有向量数据库的实战经验没有评估AI系统的一套方法。更重要的是我对一个AI应用该长什么样完全没有画面感——我以为要自己训练模型要搞懂Transformer结构甚至要去复现一篇论文这些都在后面被证明是想当然。1.2 需求的边界什么必须做什么坚决不做我把需求写在了一张卡片上第一版长这样输入一个自然语言问题系统在我自己的文档库中检索相关内容返回一个带引用的回答。提出这个需求的逻辑很简单——它是我真实遇到的痛点笔记太多了我经常忘了某个细节记在哪普通搜索只能按关键词匹配一句话的问题往往找不到最相关的那段笔记。然后我做了两件很重要的事第一圈定范围。第一版只做单轮问答不做多轮对话只服务我一个人不做权限系统只在本地部署验证不着急上云。第二设定可量化的验收标准抽取100个我真实会问的问题系统能从文档中找到正确出处并给出合理回答的比例要超过70%单次问答的平均延迟不超过10秒。这两条标准在后来无数次救了我——任何改动跑一遍这100个问题好坏一目了然。1.3 项目第一版的完整链路从文档到答案整个系统在前端看来就是一个输入框加一个回答区但在后端问题从进来到最后得到答案是一条完整链路文档进入系统后依次经过文本提取、清洗、拆分、向量化、入库用户提问时问题被向量化、检索出最相关的文本块、组装成Prompt、交给大模型生成回答最后把回答和引用来源一起展示给用户。这个链路成了我后来所有迭代的地图。它的关键不在于哪个部件多厉害而在于每个环节都会影响最终答案质量尤其是数据清洗和拆分——这是第一版跑完以后我最深刻的体会。2. 技术选型不是越新越好我这套技术栈的取舍逻辑确定需求之后最诱人的部分来了选技术。我现在回头看选型阶段最容易犯的错误是追新——今天看LangChain出了新功能想用明天看到LlamaIndex的文档觉得更强后天又想自己手搓一个向量库。实际上选型的问题只有一个在你当前的资源和目标下什么方案能在最短时间内跑通并验证你的想法我最终的选型如下每一条后面都写着为什么。2.1 整条链路的全景数据、检索、生成三块怎么拼我的技术栈最终是PyMuPDF加pdfplumber做PDF文本提取自写脚本做清洗和按标题层级拆分嵌入模型用text-embedding-3-small向量库先本地用Chroma、上线换Qdrant检索阶段用向量相似度加BM25混合打分生成阶段调用大模型API先用DeepSeek后来切到国内可稳定调用的模型后端用FastAPI部署用Docker。这条链路其实就是在回答三个问题非结构化文档怎么变成可以被检索的块用户问题怎么找到最相关的块找到块之后怎么让大模型生成靠谱的答案我刻意选择了模块化设计每个环节都可以单独测试和替换——这一点在后面调试时救了我很多次。2.2 分模块选型为什么是这些而不是那些我把几个关键选型单独拎出来说。首先是向量库。第一版我强力推荐Chroma因为它可以零配置嵌入到你现有的Python项目里几分钟就能跑通全链路。别一上来就上Milvus或Elasticsearch那会让你陷入运维的泥潭。当数据量超过百万级或需要高并发时再换成Qdrant不迟。其次是嵌入模型。中文场景里我当时对比了三种OpenAI的text-embedding-3-small英文和中文能力均衡、BGE-M3国产开源中文语义理解好且支持1024长度、以及当时的新款text-embedding-3-large。最终选了text-embedding-3-small理由是1536维的向量和我的文本块数量匹配良好而且API便宜、稳定不需要自己维护模型。如果你完全不想碰外网BGE-M3是更好的选择——之后我也在另一台离线服务器上验证了它的中文效果确实不差。然后是生成模型的选择。这个问题被问过太多次为什么不上本地模型原因很简单我没有GPU一个7B模型用CPU跑一轮回答要几十秒而API调用只要两三秒。在你只有16G内存的情况下本地模型毫无意义。我最后用的是DeepSeek的API理由就三条便宜、中文能力强、支持Json输出方便后续接结构化逻辑。我始终认为API优先是从零开始阶段的正确答案——先跑通系统和验证价值再考虑私有化部署。最后关于框架我第一版没有用LangChain只用了少量LlamaIndex的文档加载器。原因很简单我把整个链路的每一步都拆开了自己做控制——数据清洗、分块、向量化、检索、Prompt组装、调用模型、展示结果。这让我对每个环节的效果都心里有数。LangChain可以等系统跑通以后再做抽象一开始就套框架出了错很难定位。我的原则是第一版能用原生代码做的绝对不引入框架。2.3 为什么第一阶段别碰微调资源有限时的最大陷阱这一条我必须单独拿出来说因为几乎所有新手都会在这里摔一跤。我当时也认真研究过微调既然我对文档效果不满意是不是可以把文档喂给模型做微调这个想法听起来特别合理但实际上是个陷阱——微调的目标是改变模型的行为风格或注入少量知识而不是把教材塞进模型里。而且说实话一个大模型根本记不住你喂进去的几万字——它的知识更新靠预训练你想通过几轮微调让它背下你的笔记效果远不如做好检索。我当时做了一个简单实验来验证这个判断把一份10万字的PDF喂给支持长上下文的模型直接问其中细节结果它只能答出个大概经常编造细节。但当我改用检索相关段落再让模型读的方式之后答案的准确率立刻上来了。从那以后我就确定了核心赌注要压在检索质量上而不是模型的记忆能力。微调不是不能做但要等到数据质量高、有明确的风格需求时再考虑比如让模型学会你产品的回复口吻。3. 数据清洗和文本分块决定系统上限的前置工作这章是整个项目里最枯燥但最重要的一部分我在这上面花了接近40%的总时间。很多初学着急着调Prompt、选模型但我第一次跑通系统花了30分钟之后一个星期全在跟数据较劲。这不是夸张数据质量直接决定了检索质量检索质量又决定了最终回答的天花板。3.1 从PDF、Word和网页剪藏到干净文本我踩过的三个坑先处理的是PDF。PDF看起来是纯文本实际上里面藏了一堆坑页面页脚混进了正文——我用页边距和字体样式过滤掉目录页重复了后面的正文章节标题多栏排版导致阅读顺序错乱——PyMuPDF按坐标排序时两栏文本会交替乱序。最后我用pdfplumber做文本块坐标排序再按栏进行重组才基本解决。Word文档相对好办python-docx把段落和表格分开读表格再按行转成文本。网页剪藏最麻烦HTML里有一堆导航栏、广告、推荐阅读。我写了一个清洗函数主要做三件事去掉HTML标签和script/style块根据正文密度算法提取核心文本——简单说就是找连续文本最多的区域把链接文字和图片注解全部丢弃。做完清洗之后我做一个容易被忽略的动作保留元数据。每个文本块开头会带上文件名、章节路径、原始页码。这一步没有体现在检索系统里但最后展示回答时能告诉用户这个答案来自哪本书的哪一章可信度立刻提升了一个档次。没有来源的AI回答用户不敢信。3.2 分块策略为什么按固定长度切一刀是最懒也最坑的做法最开始我想当然地写了个函数每500个字符切一块块与块之间没有重叠。跑完检索后效果惨不忍睹——问一句什么是倒排索引检索出来的块文不对题因为它们把段落切碎了一个概念的引入、解释、举例被分到了三块里没有一块完整包含答案。这个问题的根源我后来想明白了分块的粒度必须跟随文档的语义边界而不是字符数。我的解决方案是分两层做第一层按文档结构切比如Markdown的标题层级、PDF的章节标题、Word的一级二级标题——先把文档切成章节级的大块第二层再对大块做滑动窗口切分块大小控制在500到800个字符之间并让相邻块有150个字符左右的重叠。这样既保留了语义完整性又不会因为块太大导致检索精度下降。这里有一个值得单独讲的经验块大小和重叠度不是调一次就完的而是要根据你的文档类型反复试。我后来在技术笔记、PDF教材、网页正文三种类型上分别测过最优参数用检索命中率做指标。结论是技术笔记因为经常出现列表和代码块要小一点400到600字符PDF教材的段落连贯可以切大一点700到900字符。如果你懒得测记住一个原则就行一个块应该是一个自包含的语义单元用户在只看这个块的情况下也基本能理解它讲什么。3.3 Embedding模型与文本块质量的关系别让向量化替你背黑锅在我调试系统的过程中有三分之一的问题最后发现根源不在Embedding模型而在前面的清洗和分块。最常见的现象是检索出来的文本块语义相关但里面夹杂着页眉页脚的废话或者因为分块把一段话截断导致嵌入向量只表达了片段信息。向量化本身很无辜——它的工作是把你给的文本在语义空间中定位你给的文本本身是残缺的它当然定位不准。所以我把数据管线定义为一个独立质检环节每一批文本块入库前随机抽查20个块人工阅读确认是否语义完整、有无噪音确认后再向量化入库。这个习惯看起来原始但它真的能提前暴露90%的检索问题。记住一个原则数据管线的质量决定了检索的上限再好的检索算法和模型也救不了脏数据。4. 检索链路实战从向量搜索到混合检索的演进数据准备完毕之后系统最核心的部分来了用户问题进来以后如何找到最相关的文本块。这是从零开始最有价值的一段因为你在反复调试中会真正理解什么叫准确率不是一个大模型的幻觉而是工程细节堆出来的结果。4.1 向量检索第一版效果和瓶颈都有哪些第一版我直接写了一个简单的余弦相似度检索把用户问题和所有文本块都向量化然后计算用户问题向量与每个文本块向量的余弦相似度取TopK我当时取5。在Chroma里就是collection.query(query_embeddings..., n_results5)。在测试集上第一版的表现让我意外——大约75%的简单问题都能命中正确出处。这印证了好数据基础向量检索就能干活。但问题也很明显一是专业领域内的同义词匹配不好比如我文档里写倒排索引用户问反向索引向量检索经常找不到二是专有名词和缩写吃不开比如文档里用RAG用户问检索增强生成两者在向量空间中隔得很远三是长关键词被切碎后顺序信息丢失检索结果没有整句匹配的清晰度。这些问题的本质是向量检索擅长理解语义但在精准关键词匹配上不如传统检索。于是我开始琢磨混合检索。4.2 加入BM25关键词检索为什么效果提升明显受Elasticsearch的启发我决定把BM25也纳入检索链路然后再做分数合并。第一版实现用bm25s这个库对文本块做分词中文分词用jieba每个人都知道然后对用户问题也做同样的预处理BM25会返回一个相关性分数。最终排序分数是score 0.6 乘以向量相似度 0.4 乘以BM25分数。我之所以把向量检索权重放高是因为在长尾语义问题上它明显更稳——BM25在关键词不重叠时直接给0分而向量还能兜底。但BM25补上了非常关键的一环精确匹配。加了混合检索之后之前倒排索引查不到的问题直接解决了因为BM25把包含倒排或索引的文本块捞了出来再结合向量重排Top1的命中率从75%涨到了89%左右。这一段的经验总结起来就一句话向量检索负责理解BM25负责精确不要让任何一个单干。如果说Embedding模型是系统的手那混合检索就是系统的眼。4.3 TopK、阈值和重排序系统回答质量的三层过滤检索出来的TopK块不能直接全部塞进Prompt直接用会撞上两个问题一是包含不需要的碎片信息模型容易被带偏二是Token超限。我做了三层过滤第一层阈值过滤——向量相似度低于0.72的直接不要BM25分数为0的自动掉出候选除非向量相似度非常高第二层TopK控制——第一版取5后来稳定在7到10之间太多了模型会分心第三层重排序——当候选块超过TopK时我会用同样的Embedding模型把用户问题加上一个提示前缀再算一次相似度相当于做一次轻量级的重排序。这里必须聊一聊阈值这个调参问题。我发现新手容易犯两个方向的错误阈值设太高很多相关问题搜不到系统总是答我不知道这是低召回阈值设太低一堆弱相关的碎片都进了Prompt模型开始一本正经地胡说八道这是低精度。我最终的经验是先让检索尽量多召回宁滥勿缺再通过阈值和TopK控制精度同时让Prompt告诉模型——如果上下文中没有相关内容就直接说不知道不要编造。这条提示词本身就是一道安全阀。4.4 Prompt组装检索质量再好不会组装也白搭这也是我花了最多时间打磨的部分之一。最终版Prompt长这样作为文档问答助手请仅根据以下【文档片段】回答问题。如果内容不足以回答请直接回复文档中未找到相关内容不要编造。 【文档片段】按相关度从高到低排列[来源文件A/章节X/页码P] 内容...[来源文件B/章节Y/页码Q] 内容...用户问题{用户问题}要求回答尽量精炼并列出引用来源编号。这个Prompt里有几个关键细节值得反复琢磨第一仅根据以下内容明确限制了模型的发挥范围实测能显著降低编造率第二文档片段按相关度排序源自GPT对高相关内容更敏感的研究直觉让优先的片段在前面第三每个片段都标注来源模型会自然而然地引用来源编号第四我连模型扮演提示词工程师这类优化都没做——先跑通再优化别一上手就追求完美。5. 系统评估和踩坑实录上线前的必经之路没有评估就没有迭代方向。我从项目一开始就搭建了一个极简评估集不然你永远不知道一次改动是变好了还是变差了。这一章我会把评估方法的搭建、三个真实的踩坑案例的完整排查链路以及那些不调会出事的参数全部讲清楚。5.1 怎么量化和观察回答质量极简评估集的搭建我的评估集长这样从真实问题中抽出100个覆盖三类问题——简单事实型什么是LSM树、跨章节综合型对比B树和LSM树的适用场景、刁钻长尾型哪本书里提到过WAL在崩溃恢复中的作用。每个问题我都预先标注了期望答案出处来自哪个文件哪个章节然后每次系统改动我跑一遍这100个问题人工打三个分出处命中率检索对不对、答案相关度回答靠不靠谱、编造率是否出现了文档里根本没有的内容。这套方法原始但极其有效。它花了大约半天时间标注换来的是后续每一个优化都能用数据说话。比如我把分块大小从500调到700后检索命中率涨了4个点但跨章节问题下降了——因为大块里混了太多章节内容。这种细颗粒度的观察如果不做评估集根本不可能看到。5.2 三个真实踩坑案例的完整排查链路第一个坑是块大小设置太小导致上下文缺失。第一版某个PDF教材被我切成了350字符的小块问为什么Raft的选举需要随机超时时间时系统反复答文档中没有相关内容。排查链路从问题开始先单独跑向量检索看Top3返回的文本块语义上是否覆盖了答案——我发现返回的块只包含Raft选举的开头但随机超时的解释在下一个块里而且重叠区没覆盖上。修复方式是把这个PDF的块大小提升到800字符重叠提到200并确认该段落完整落在一个块里。修复后同类问题命中率从50%涨到92%。这个案例的教训是分块参数不能全局统一要按文档类型单独配。第二个坑是相似度阈值设太低导致一本正经地胡说八道。某次优化中把阈值降到了0.65想着提高召回结果用户问如何优化Kafka的吞吐量系统返回的上下文里只有一份讲消费组配置的文档最后生成模型基于这块弱相关内容展开了大段推测文本风格又很像原文一时很难察觉是编造。排查时我先查生成日志里的引用来源编号——发现回答引用的来源都指向同一个低相关度块再单独打印该块的内容和相似度分数发现只有0.67远低于其他可靠的0.8以上的候选。这次修复很简单把阈值调回0.75并在Prompt里加了一条铁律——引用片段与问题相关度较低时必须拒绝回答。从此编造率从11%降到了3%以下。第三个坑最具隐蔽性我导入了一份网页剪藏清洗不到位留下了大段导航栏文字结果用户问如何使用这个工具系统永远回答请先登录并点击右上角头像因为那个导航栏块在向量空间里是整库的公共邻居和几乎所有提问的相似度都偏高。排查时发现Top5里几乎都是同一个来源的块BFS一眼看出是模板文本未被清洗。修复步骤是把该来源的文本块逐个打印出来确认是导航栏内容改进清洗函数用正文密度算法过滤这类区块并建立黑名单统一丢弃纯导航、页脚、重复模板。修复之后同类问题命中率立刻改善。这三个案例放在一起能提炼出一条通用的排查链路出问题先看检索结果确认TopK文本块是否真的回答了问题再看Prompt内容和相似度分数确认是不是模型被弱相关上下文带偏最后检查数据清洗和分块是否埋了雷。检索源头对了生成基本不会太离谱检索源头错了再好的Prompt也救不回来。5.3 容易被忽略的三个工程参数温度、并发和缓存除检索外有三个参数被很多人忽略但在上线阶段特别关键。第一个是生成温度。我最终设为0.2几乎不自由发挥。它把创意性调到最低回答更贴上下文、更喜欢用原文的表达方式。第二个是并发控制。大模型API有速率限制我的FastAPI后端用Semaphore限制了同时最多5个请求然后配了一个简单的队列避免在高峰期被API限流中断用户体验比第一次裸跑好了一个数量级。第三个是缓存。同一个问题连续问两次是常见场景。我用一个简单的字典缓存把用户问题原文和对应回答存起来命中时直接返回延迟从5秒降到几十毫秒。这看起来廉价但实际上大幅降低了API费用也减轻了后端压力。6. 部署上线与持续迭代让系统真的能用起来系统在本地能跑通是一回事让它随时能用是另一回事。我从一开始就定下规则模块化开发本地验证一个模块就跑通一个模块最后整体部署上线。这个项目从零到上线总共只花了两周业余时间远超我的预期。6.1 最小化部署方案FastAPI加Docker加简单前端后端我用FastAPI写了两个接口/search用于检索相关文档片段/chat用于完整的问答。FastAPI的异步支持和大模型API的调用配合得很好而且自动生成接口文档调试起来很方便。前端是一个只含一个输入框和一个流式输出区的单页HTML没有用任何框架就写在静态文件里。业务逻辑全部在后端前端只负责把用户输入POST到/chat接口然后流式渲染回答。Docker打包是上线前最关键的一步。我的Dockerfile做了三件事用Python 3.11的slim镜像为基础把依赖通过requirements.txt安装在容器里加载嵌入模型和向量库数据。这里有几个容易踩的坑一是Qdrant或Chroma的数据目录必须挂载到宿主机卷否则容器一重建数据就全没了二是如果你用的是本地嵌入模型得在Docker构建阶段就把模型权重下载好塞进镜像里否则容器第一次启动时会在线下载生产环境往往在内网会直接失败三是环境变量管理API密钥不能写进代码仓库用环境变量传入容器。6.2 上线后真实运行中的观察日志、反馈和持续优化上线以后最意外的事情是用户是不会按你预设的方式提问的。我在评估集里精心设计的问题现实里根本不出现。真实用户会问我上次那个事情怎么办这个怎么搞甚至一句话带着三个错别字。所以我在后端加了两样东西请求日志和反馈按钮。请求日志记录每次问答的原始问题、系统回答、检索到的来源编号、相似度分数和生成耗时。它成了迭代优化的数据宝库——每周我抽出时间翻日志把高频但回答不理想的问题挑出来加入评估集然后针对性地调整Prompt、清洗函数或分块参数。反馈按钮则更直接用户觉得回答靠谱就点个赞不靠谱就点个踩。踩得多的问题会进入一个待修正队列我定期集中处理。这套闭环看起来朴素但对从零开始的项目来说是最有效的迭代方式。部署后的另一个发现是延迟的感觉很主观。10秒的延迟在后端日志里看起来还行但用户等待超过5秒就焦虑。我的优化方向是第一步做流式输出——让大模型边生成边推送用户看到文字在流动等待感显著下降第二步把检索部分从聊天接口里拆出去用缓存加速高频问题的响应。不要小看这一步它直接决定了用户愿不愿意真的用这个系统。6.3 从零开始之后我收获的三个认知转变整个项目走完我最有价值的收获不是技术细节而是三个认知层面的转变。第一个转变AI工程的核心不是模型而是问题定义和工程链路。模型只是链路里的一环真正决定系统好不好用的是数据清洗、分块、检索质量、评估闭环这些不性感的维度。你只要把一个B级模型和一条优秀的链路组合起来产出的系统就能超过90%的Demo级应用反过来S级模型配一条稀烂的链路照样会一本正经地胡说八道。第二个转变评估驱动迭代而不是灵感驱动迭代。没有评估集优化就是瞎猫碰死耗子有了评估集每次改动都能看到数值变化本质上和传统软件开发的测试驱动是一回事。第三个转变从零开始做AI工程最重要的能力是拆解能力——把做一个智能问答系统这个大目标拆成数据、检索、生成、评估、部署五个可控的小问题然后逐个击破。这条路不上培训班也能走通但需要足够的耐心去一遍遍测试和观察以及敢于在最枯燥的数据清洗阶段沉住气。如果你现在正在犹豫是否要开始我的建议特别简单找一个你手头真实存在的数据集或问题搭一条最简链路哪怕第一版很笨拙重要的是让它跑起来然后在不断的测试和反馈中迭代下去。AI工程从来不是看会的是做会的。我做完这个项目之后再去看那些曾经觉得晦涩的文档和论文竟然全都能读懂了——因为我知道它们解决的是系统里的哪一环。这个正反馈比任何课程都值钱。
返回列表