从零搭建AI工程:文档问答系统的RAG实践全记录
发布时间:2026/10/1 23:07:47 锦皓数字建站

在外面聊了一堆“AI 改变世界”的大词之后真正动手把 AI 落地成能跑的系统才是大多数工程师和产品团队最需要跨过的一道坎。我最近把一个内部项目命名为ai-engineering-from-scratch字面意思就是“从零搭建 AI 工程能力”。这个项目不是搞科研、不是追论文也不讲究要把模型重新训练一遍而是切切实实地把提示词、数据管线、模型接入和上线监控揉到一起做出一套能稳定产出价值的 AI 应用。这篇文章就是把我在项目里怎么思考、怎么搭环境、怎么拆任务、怎么填坑的过程完整还原出来适合刚开始接触 AI 应用开发的工程师、想在企业里落地 AI 功能的产品经理以及对“AI 工程”这两个词还有点模糊的学习者。1. 项目定位与思维拆解1.1 “从零开始”到底要解决什么问题很多人一听到 AI 工程第一反应就是“要会训练模型”或者是“必须懂深度学习”。但实际上工程化落地和学术训练是两条完全不同的路。我见过不少团队把大量资源砸在微调模型上最后发现业务根本不需要那么“聪明”的模型反而需要一套能把现有模型用稳、用对的系统。ai-engineering-from-scratch这个项目想得很清楚核心不是造模型而是搭一套能承接业务需求的 AI 系统让模型在真实场景里可预测、可评估、可维护。我从自己的实际需求出发给自己定了个明确目标一周内从一台不上不下的开发机开始跑通一个包含数据准备、文档切分、向量检索、提示词调用、结果输出的完整问答系统。这个目标听起来不大但里面藏着一个关键问题——如何保证每一步都是自己亲手掌控的而不是依赖某个平台的黑盒。要知道在生产环境里真正让你半夜爬起来处理的往往不是模型本身的精度而是“检索结果不对”“接口超时”“上下文被截断”这些工程问题。所以项目的第一阶段我没有急着写代码而是先画了一张拆分图数据从哪来、怎么清洗文档怎么切分才能保持语义完整向量库选哪个模型走本地推理还是走 API回答质量怎么评估系统上线以后怎么监控。这六个节点每一个都对应一类工程决策。想清楚它们才能避免后期被临时修修补补的脏活拖住。1.2 需求拆分与实现目标我给项目拆了几个阶段性交付物这个思路可以复用到你自己的 AI 项目中。阶段一可运行的问答 MVP。我选择了一个我手头有大量资料的领域——内部产品文档搭建一个“文档助手”能回答“XX 模块如何配置”“某个报错代表什么含义”这类问题。阶段二基础质检体系。建立一份至少 50 条的测试问题集每一条都有标准答案或关键要点用于评估系统的回答质量而不是靠感觉“看起来还行”。阶段三成本与延迟优化。把单次问答的成本、平均响应时间纳入优化目标设定预算上限保证这个系统未来真要接入业务时财务上算得过账。阶段四运维监控闭环。记录每一次请求的输入输出、耗时、Token 消耗、错误类型形成一张可回溯的日志大表。这四个目标决定了项目不会沦为“玩具 Demo”。特别是质检和成本很多人做 AI 应用时会下意识忽略但恰恰是决定能不能上生产环境的硬指标。我在拆分需求时还专门验证过如果某个功能不能让这三件事变好——回答质量更高、成本更低、系统更稳定——那它就不是当前阶段的优先级。2. 实验环境与基础设施搭建2.1 本地环境按这个思路搭少走弯路ai-engineering-from-scratch的运行环境我选择的是“本地 API 混合架构”。先解释一下为什么不在本地跑一个完整的大模型我的开发机配置是 64GB 内存加一张入门级显卡跑 7B 参数的量化模型推理勉强能用但跑检索、嵌入、并发请求这些业务逻辑时还是吃力。更关键的是业务系统里真正让人头疼的工程问题都在模型之外为了调试这些业务逻辑把笔记本压到 100% 的 CPU 占用并不划算。实际环境分了三层开发层 Python 3.10虚拟环境独立管理依赖主项目里用pyproject.toml锁住版本数据层先用 SQLite 保存原始文档和切分后的文本块再做向量索引。前期不用急着上重型数据库SQLite 足够轻量且便于调试模型接入层本地部署一个小型 embedding 模型用于文本向量化对话生成调用远端大模型 API。embedding 模型我选了bge-small-zh的中文版本512 维向量检索速度和占用内存都很均衡。选它的理由很简单我要处理的是中文产品文档通用英文 embedding 模型在中文语义上的表现明显弱一截。嵌入模型不需要太大真正影响问答质量的是后续的切分策略和检索逻辑嵌入模型只要能把语义位差距拉开就够用。远端大模型 API 我同时备了两个供应商一个作为主力一个作为降级备用。选择标准有三条上下文窗口至少 32K后面切分文档要靠它装上下文、接口稳定性高、按 Token 计价清晰透明。很多人在选型时只看回答质量却忽略了“万一主服务挂了你的应用能不能快速切换到备用通道”这个问题。我的经验是AI 工程里没有不会出故障的第三方服务提前备好 fallback 通道能救你很多次。2.2 模型选型和关键参数值得反复调模型选型看起来像是个“选择题”本质上是成本、质量、延迟的三角权衡。在项目早期我不建议为了省几块钱选一个能力明显偏弱的模型因为系统还不稳定模型太笨会让你分不清“是提示词没写好还是模型理解不了”。我在 MVP 阶段选择了上下文窗口较大、中文能力比较均衡的模型作为主力单次回答质量能稳定覆盖大多数产品文档问题。关键参数的调参过程我展开说一下初期可以按这套经验去试温度temperature文档问答属于抽取和摘要类任务需要的是稳定而不是发散我把温度固定在0.2。如果你想做文案创作或头脑风暴再往 0.7 以上调Top-p默认或者稍微降到0.9。Top-p 和 temperature 是两套抽样逻辑通常只调一个就行两个同时调容易互相干扰Max tokens 上限我按每个回答最长不超过 1024 Token 设置。初期这个值不要拉满因为长输出的问题和失败率、成本都会上升先把核心链路跑通再说系统提示词固定一段中文提示告诉模型“你是一个企业内部文档助手必须基于给定资料回答资料中没有的内容要直接说明不知道不要编造”。其实真正容易出现问题的不是这些参数本身而是你换成不同场景时忘了调。我后来写了一个简单的配置模块把不同任务的参数存成字典调用时按任务类型自动加载。这样做之后测试和上线的参数不会因为忘记改而错乱。3. 核心实现从提示词到可运行管线3.1 先吃掉一个具体需求把文档问答系统跑通任何 AI 项目如果只停留在“AI 能做什么”的演示阶段价值都是虚的。我在项目里选了最接地气的一个场景做第一突破口把十几篇产品操作手册变成一个可以对话的问答系统。用户问“备份配置时需要注意什么”系统不是从一堆文档里随便抓一段话然后复制粘贴而是先定位到相关章节再根据章节内容生成一句精炼回答。整个流程看着简单但内部有四个环节哪个都不能省。第一步是原始文档清洗。我手头这些产品手册大部分是 Markdown 和 PDF 混合格式PDF 里经常有页眉页脚、目录编号这些噪声。清洗后统一成纯净文本连回车符都统一成\n避免后续按段落切分时出现半截行。第二步是文档切分。这是最容易被忽略但影响最大的环节。我在项目里试验过不同策略最稳妥的方案是按“章节标题 段落”组合切分先按二级标题把文档分成大块再按段落边界切到每块约 600 个中文字符块与块之间保持 10% 的重叠。为什么要重叠因为很多关键信息恰好跨段分布比如一个操作步骤的说明可能写在上一段的结尾不重叠会直接丢信息。切分质量的好坏直接决定后续向量检索能不能“命中”正确答案。第三步是向量化入库。每个文本块经过 embedding 模型变成 512 维向量连同原始文本、章节路径、文档名一起存进向量数据库。这里我用的是轻量方案直接用chroma配合 SQLite 元数据存储。这个阶段最好保存一份“文本块 id → 原文”的映射表后面调试时能顺着 id 反查原文排查问题会顺手很多。第四步是问答生成。用户提问后先用同样的 embedding 模型把问题转成向量在库里做最近邻检索。我取命中分数最高的前 4 个文本块拼接成上下文连同系统提示词一起发给大模型。在提示词里我特别加了一条硬约束“如果上下文无法回答问题请明确回答‘根据提供的资料无法确定’不要推测。”这一句是降低幻觉的关键可以多用。3.2 拆解实现细节召回、拼接与结构化输出把代码骨架拆开看整个问答函数的核心逻辑并不长。我用伪代码描述一下方便你照着自己实现def answer_question(question): query_vec embed(question) chunks vector_db.search(query_vec, top_k4) context \n---\n.join([chunk.text for chunk in chunks]) prompt build_prompt(question, context) reply call_llm(prompt, temperature0.2) return reply, chunks # 返回回答和引用的来源这里的build_prompt不是简单地把问题贴进 prompt 就完事它内部有一套拼接模板先给系统指令再给“资料区”和“问题区”最后加上约束条件。资料区里我还会标注出每段资料的来源文档名让模型生成时留意引用依据。生产环境里如果你要做“回答溯源”这一步必须提前埋点。关于结构化输出我的建议是让模型尽量返回 JSON 而不是纯文本。尤其是在你要把 AI 结果接入业务系统时纯文本解析是灾难。我在提示词里要求模型按这种格式输出{ answer: 回复正文, confidence: high|medium|low, related_docs: [doc_name_1, doc_name_2] }虽然大模型不保证 100% 输出合法 JSON但配合response_format参数开启 JSON 模式后稳定性大幅提升。我在代码里加了 try-except 兜底解析失败时就用正则提取answer字段再解析失败就直接返回原文。这一层“可用性保护”很少有人写但恰恰是系统健壮性的分水岭。可用性保护在工程里还有个名字叫“优雅降级”意思是系统出问题时不能一崩到底而是要有备用路径。在做 AI 应用时这条原则特别适用因为大模型本身就是概率系统解析失败、超时、返回空值都不是罕见情况你不能指望它像传统接口那样稳定。3.3 评估与迭代用数据说话而不是感觉项目做到第二周我意识到最缺的不是代码量而是评价标准。没有标准你改一版提示词根本分不出是好是坏。所以我专门花时间建了一套最小评估集规则很简单但对中文内部文档的“信息正确性”检验很有效答案正确标准答案里的核心要点完全覆盖无错误信息答非所问回答了一堆正确但跟问题无关的内容判失败幻觉答案里出现了文档中没有的信息判失败漏检资料里有答案但系统回答“不知道”判失败。我准备了一个 50 条问题的评估集覆盖配置操作、报错处理、功能说明三类。每次改动切分策略或提示词后就整批跑一遍评估集记录通过率。这里有个小技巧评估集要定期加入“失败过的真实用户问题”因为越贴近实际的场景越容易暴露系统的短板。第一版系统跑出来的评估结果给我浇了一盆冷水正确率只有 68%其中有 12% 的失败来自“漏检”。排查后发现问题出在文档切分太重——600 字符的块把一个完整的“配置步骤”切成了碎片导致检索时关键词命中不到。我把切分策略改成“按 Markdown 标题优先保留完整小节只有当小节过长时才往下切分”漏检率直接降了一半。这就是评估驱动的迭代价值没有数据你只会觉得“模型不够聪明”而实际上问题往往在自己的管线上。4. 生产环境中的坑与对策4.1 延迟、成本和资源控制这三件事要同时管AI 应用上了生产最先暴露出来的不会是回答质量而是延迟波动和成本失控。我在压测阶段记录了单次问答的耗时分布发现 p95 和 p50 之间差距超过 3 秒——这意味着大量用户的体验是不稳定的。原因有两个一是远端 API 在高峰期排队二是我的代码是同步串行调用没有加缓存。解决手段分三层层层递进加缓存。命中相同语义的问题直接复用上次结果。我把问题的 embedding 向量存了一份新问题进入时先算相似度相似度超过 0.92 就认为是重复问题直接返回缓存结果。这一层能把高频重复问题的成本直接降到接近零。设置 Token 消耗预警。每个请求的输入输出 Token 都记录到日志按用户维度和时段汇总。我在系统里挂了一个简单的告警单日成本超过预设阈值就通知管理员。成本不应该是事后看账单才心痛而是实时能看到。降级链路。主流模型超时超过 5 秒自动切换到备用模型。备用模型的回答质量略低但能保证主流程不断。延迟控制上还有一个容易被忽略的细节不要把整个文档库都塞进 context。很多人习惯把所有检索结果都丢给大模型上下文一大响应时间成倍上涨、成本也涨。我限制单次问答最多取 4 个文本块如果 4 块不足以覆盖问题就说明切分策略需要优化而不是无限扩大 context。这个思路在项目里被称为“Less context, better response”——放在 AI 工程里同样成立。关于模型的调用参数还有一个指标值得盯最大输出 Token 的设置。把 max_tokens 从 1024 提到 2048 看起来没什么但如果你每天有上万次调用输出 Token 翻倍的成本是实打实翻倍的。我建议 MVP 阶段不要贪能给到准确答案就够了内容太长反而容易夹带废话。4.2 监控与评估体系可以小而精真正能用于生产的 AI 应用必须有一个“小而精”的监控体系。我在项目里没有引入重型 Prometheus 全家桶而是用了一套极简方案每次调用把以下字段写成一条 JSON 日志{ timestamp: 2025-01-10 14:22:01, question_length: 18, retrieved_chunks: 4, context_tokens: 1420, response_tokens: 356, total_cost: 0.004, latency_ms: 2300, model: main_model_name, answer_source: cache|llm, error: null }这些日志汇聚到一张大表后我就能回答几个关键问题哪个时段的延迟最高哪些用户的输入导致 response_tokens 突然爆表缓存命中率是多少每周跑一份汇总就能看到系统的健康状况趋势而不是“看着好像还很正常”。除了数字指标建议产品还留一个人工抽检通道。我每周会随机抽出 20 条真实问答记录逐一检查回答质量。这套“自动化指标 人工抽检”的组合能对系统形成多维度的约束比单纯盯着正确率安心得多。5. 实操中的常见问题与排查经验5.1 三个高频问题我一个个捋给你看AI 工程踩坑很正常关键是排查路径清晰。我在跑ai-engineering-from-scratch过程中记了三类高频问题你大概率也会碰到。现象一检索出来的东西看着相关但答案文不对题。这通常不是模型的问题而是你的文本块切得太碎或者问题本身太复合。比如“如何配置 MySQL 的主从复制同时处理连接池异常”这一句话里有两个独立主题。模型可能只检索到其中一半资料自然回答不全。解决方法是把问题先做“子问题拆分”或者优化文本块切分粒度。我先采用了后者把重叠率从 10% 提到 15%文不对题的情况少了不少。现象二答案正确但输出格式经常不符合预期。这个问题我调试时头最疼。后来发现原因出在“系统提示词没有把格式要求讲透”但你只说了“请输出 JSON”模型可能出现多余的换行或注释。现在我会在提示词里直接给出一个最小合法 JSON 示例同时打开 JSON 模式参数再把输出严格做一次校验。三层都做齐格式稳定性能上 95% 以上。现象三模型经常说“资料里没有相关内容”。如果明明文档里写了模型却说没有请优先检查 embedding 的检索是否命中。我遇到过一次原因是我清洗文档时把“保留原始换行”和“去除多余换行”的逻辑写反了导致很多有效内容被错误合并语义变了。这个问题靠代码 review 很难发现反而是通过随机打印几条切分结果看出来的。所以我的习惯是每次清洗完数据随机打印 20 条切分结果肉眼扫一遍再入库。5.2 提高命中率和稳定性的几个土办法提高检索命中率和回答稳定性不需要很玄的技术几个土办法很有效。一个办法是关键词扩展。在做向量检索之前我先把问题里的专业术语做一次同义词映射比如“内存泄漏”和“memory leak”都映射成统一标签。这种方法在中文文档里尤其有用因为不同作者对同一个概念经常用不同措辞。用传统的关键词权重辅助向量检索能有效弥补 512 维向量对专业术语表达不足的问题。另一个办法是两阶段召回。先用向量检索召回前 20 个文本块再用一个轻量级重排模型比如bge-reranker-base或基于关键词的 BM25把结果重排最后取前 4 个。这一步让在向量空间里但距离略远的“边缘相关”内容也能被捞回来对准确率的提升非常显著。代价是多一次重排计算的延迟但在文档数量只有几百篇的场景下重排耗时在毫秒级完全可接受。6. 这块内容目前还能怎么扩展系统跑通之后我并没有立刻把功能堆得更复杂而是停下来认真思考下一步。这里有个很常见的误区一个功能跑通了就觉得“AI 化”已经搞定赶紧去接更多场景结果每个场景都是半吊子。我的经验是先把一个场景做到“上线后基本不用管”再考虑复制到下一个场景。手头这个文档问答系统我后续打算做三件事。第一是给每个回答加上可视化的引用定位用户能直接看到答案来自哪份文档哪一节。第二是把单轮问答升级成多轮对话保留历史会话的上下文摘要让用户能连续追问。第三是引入“主动拒答机制”——当问题里包含的信息不足以确定业务归属时系统先反问澄清而不是硬给一个猜测性的答案。这三件事每件都不需要换模型改的是工程链路和产品交互但它们能把系统的实用性和可信度拉高一个等级。ai-engineering-from-scratch这个项目真正教会我的东西可能和 AI 本身没什么关系问题不在模型“不够聪明”而在于你有没有给足上下文、有没有设计好数据管道、有没有准备好降级方案、有没有用数据持续优化。模型能力是水工程体系是渠光有水没有渠最终还是一片乱流。把渠修好再小的水也能灌溉。这大概就是 AI 工程从零到一的核心意义。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。