开门见山说个事儿我最近做完了一个叫ai-engineering-from-scratch的项目说白了就是完全从零开始不依赖任何现成的 AI 应用框架把一套生产可用的AI 工程系统一点点手写出来。整个过程走下来最深的感受是现在网上全是教你“调包”的教程可真到了业务现场你需要的是一个能自己掌控每一个环节的工程能力。这篇文章我会把这次实操的完整思路、关键代码、踩坑记录和核心参数全部摊开来讲适合那些不想只会“抄 prompt”和“拖拽工作流”的人也适合想真正理解 RAG、Agent、评估体系背后原理的开发者。我先把项目背景交代清楚。业务方丢给我一个需求企业内部的知识库太多了十几个系统、几千份文档员工找资料全靠问人希望做一个 AI 问答应用让员工用自然语言提问系统从知识库里定位答案并生成回复。表面看这是个典型 RAG 需求但真正动手以后你会发现从文档清洗、分块、向量化、检索、重排到生成每一环都有无数坑。这篇文章不会只给你一个 demo而是把我踩过的坑、试错后的最优参数、以及为什么这么做全都记录下来。1. 为什么我从零开始写AI工程而不是直接套框架1.1 这个项目到底要解决什么问题很多朋友看到“ai-engineering-from-scratch”的第一反应是有必要吗LangChain、LlamaIndex 这些框架不香吗我的答案是框架香但别让你的项目变成框架的“测试用例”。这次项目的核心诉求是私有知识库问答这背后牵涉的链路非常清晰文档接入 - 文本清洗 - 切片 - 向量化 - 检索 - 重排 - 生成。框架确实能帮你把这些组件串起来但它也会用一层又一层抽象把关键细节藏起来。业务环境里文档格式千奇百怪PDF 里全是扫描图片Excel 有合并单元格Word 里嵌着表格这些脏活框架帮不了你反而会因为它的抽象层让你排查问题更加困难。这个项目适合谁一句话如果你需要在真实业务里落地一个 AI 问答或者检索系统而不是在学校做课程设计那么这种从零搭建的方法论会直接决定你的交付质量。那些只用框架搭出来的 demo线上跑两周就会暴露各种诡异问题。这不是我危言耸听而是我在这个项目里反复验证过的结论。1.2 技术选型的底层逻辑从零开始不意味着什么都自己写。我的原则是模型能力用现成的工程链路自己掌控。具体选型如下Embedding 模型选用开源的 text2vec 或者 bge-large-zh。为什么不用 OpenAI 的 text-embedding-ada-002因为业务数据是中文为主且涉密级别较高数据不能出内网所以我选择私有化部署开源 embedding 模型。向量数据库选了 Milvus。当时对比了 Chroma、FAISS、Milvus 和 Qdrant。Chroma 确实轻量但对生产环境的数据量几十万甚至百万级文档支持不够FAISS 是库不是服务分布式和动态更新都麻烦Milvus 在云原生部署、索引类型、混合检索方面最成熟。LLM 生成模型内网部署 Qwen 系列模型。这个时候还没有那么多复杂考量核心是显存和并发能力的平衡。编程语言Python生态无可替代。这些选型每一环都有详细考量但我特别想强调一点别被“最火”的技术带偏要用“最合适”的。项目上线后维护成本才是决定你幸福指数的关键。1.3 从零搭建比套框架好在哪先说结论从零搭建的好处是可控性和可观测性代价是初期开发周期变长但你省掉的排坑时间会在后期十倍还回来。举个例子。LangChain 里调用文档加载器一个PDFLoader就完事。但你打印加载出来的内容会发现里面乱七八糟的页眉页脚、图表说明、重复段落全都混在一起做成 embedding 后检索效果一塌糊涂。我在自己写的代码里可以针对每个来源做专门的清洗规则保证进入向量库的每一段文本都是干净的。另一个核心点是调试链路。用框架时出了问题你面对的是三层封装之后的报错。而自己写的代码从 HTTP 请求到向量检索每一步都有日志、有中间变量你能清楚地知道用户问“今年的报销流程是什么”时系统到底检索了哪些片段这些片段的 score 是多少最后模型基于哪几段生成了回答。这种可观测性在生产环境是无价的。2. 整体架构与数据链路设计2.1 全链路架构拆解整个系统的数据流我画了不下十遍最后定下来的是七层结构数据接入层负责从内网 FTP、SharePoint、业务系统 API 拉取文档。解析清洗层把 PDF、Word、Excel、PPT 转成纯文本去掉噪声。切片层按语义边界和长度限制把长文档切成块。向量化层把文本块用 embedding 模型转成向量。存储索引层把向量和原文、元数据存进 Milvus建立索引。检索重排层对用户 query 做向量检索 关键词检索再用重排序模型精排。生成层把检索到的上下文填充进 prompt调用 LLM 生成回答。每一层之间用消息队列或者直接函数调用串联。因为初期数据量没那么大我选择直接用 Python 的异步任务来跑全链路而不是上一套 Airflow 或者 Prefect避免过度工程化。2.2 文档清洗和切片策略是RAG效果的分水岭这一步是整个项目里最脏最累、但收益最明显的部分。我用了一个很直观的比喻给团队讲切片就像给人准备食材你给大厨一个洗好的番茄他直接能做菜但你给他一整棵带泥的番茄秧子他还得花时间摘洗炒出来还不一定好吃。首先是清洗。我会按文档类型定制规则。比如 PDF用 PyMuPDF 提取文本后先干掉页眉页脚根据重复内容识别再去除“目录”、“第几页共几页”这类导航文本Word 文档重点是处理嵌套表格用python-docx按表格结构读出来拼成 Markdown 格式Excel 则要处理合并单元格把每个非空单元格的值前向填充进去。然后是切片。网上通用的做法是按固定 token 切比如 500 个字符一块。我试过这个方案效果非常不好——经常把一段完整的话从中间切断导致语义不完整检索时匹配不到答案。我最终采用的是**“递归字符切分 语义段落合并”**先用正则识别段落边界比如中文分段、标题、列表再判断每个候选段的 token 长度。如果一段太长就继续切如果太短就跟相邻段落合并。核心参数是chunk_size600和chunk_overlap100用 token 计这个组合在我后续的评估集上效果最稳。2.3 知识库的索引结构设计向量库里的每条记录除了向量本身还必须带三层元数据来源文档 ID、文档标题、业务部门标签。不要小看这三层它们决定了你能不能做“先过滤再检索”。比如员工问“市场部的报销标准是多少”如果向量库里混着销售部、财务部的同类文档没有标签过滤就很容易检索到错误内容。我在 Milvus 里给每条数据设计了如下 schemaid: 主键text: 原始文本切片vector: 1024 维 embeddingsource_doc_id: 来源文档 IDtitle: 文档标题dept: 所属业务部门created_at: 创建时间查询的时候先用dept做 scalar filter再做 ANN 向量搜索。这一步能过滤掉 80% 的无关数据检索精度显著提升。3. 核心模块的落地实现3.1 embedding 选型与向量化实践Embedding 模型我这边的结论是中文场景不要买开源模型直接导入必须在自己的业务数据上微调一下才够用。一开始直接用 BGE-large-zh 的预训练权重效果勉强能用但对公司内部的术语比如“工单流转率”、“occ 平台”这种领域词汇完全没有概念用户问“occ 平台怎么提工单”检索出的结果相关性很差。后来用公司积累的问答对数据做对比学习微调效果立刻上了一个台阶。微调的数据量其实不大差不多 2 万条问答对用sentence-transformers库训练了 3 个 epoch损失函数用的MultipleNegativesRankingLoss。向量化线上服务我用的是FastAPI ONNX Runtime部署单卡 GPU 可以支撑每秒 100 次的向量化请求。第一次上线时我用 PyTorch 原生部署延迟高居不下换 ONNX 后速度提升了 4 倍多。这个优化在后续检索并发量上来之后才能体会到多重要。3.2 混合检索向量搜索和关键词搜索的互补教训是我在项目跑了两周后获得的纯向量检索会漏掉大量专业术语。为什么因为 embedding 模型对缩略词、代码片段、型号编号比如“XJ-3000”、“GG-02-001”不敏感这些字符串在语义空间里几乎没有区分度。但用 BM25 关键词检索它们就像写在名字牌上一样清晰。所以系统最终采用的是混合检索策略MVP 阶段先把向量检索和 BM25 结果各取前 20 条。用 RRFReciprocal Rank Fusion算法合并两个结果列表公式是score 1 / (k rank)k取 60。合并完后交给重排模型统一打分。这个改动直接让命中率提升了 23%。如果你自己上手做我建议不要跳过这一步。3.3 重排模型的选择与集成重排环节我选了开源的 bge-reranker-base输入的是 query 和候选文档对输出相关性分数。我把它用 ONNX 部署成独立服务选 top-k 的时候取分数最高的前 5 条。实测下来重排后的命中质量比直接用向量相似度 top-k 好很多尤其是在用户问题比较模糊、向量检索召回了大量边缘内容的时候重排模型能稳定把最相关的内容提到前面。有一点要提醒重排是串行计算性能是瓶颈。候选集太大时延迟会飙升所以我严格控制输入到重排器的条目是 30 条以内最大化吞吐。3.4 Prompt 模板的艺术这个项目里 Prompt 并不花哨重点是把“角色、上下文、指令、约束”四个要素写清楚。我的模板大致长这样你是一名企业知识库助手。请基于以下已知信息简洁、准确地回答用户问题。 已知信息 {context_chunks} 用户问题{question} 约束 1. 如果已知信息不足以回答问题请明确回复“知识库中暂无相关内容”。 2. 使用中文回答不要编造事实。 3. 回答中需引用信息来源编号形式为[1]、[2]。需要特别强调的是最后一条引用编号。它对我的评估体系价值太大了——没有引用就没有办法自动判断回答是否忠实于上下文。如果你做类似系统建议从一开始就保留这一层结构。4. 性能优化、成本控制与稳定性4.1 缓存层别让每一次重复提问都打到模型上线第一周我发现一个问题员工问的问题其实高度集中前二十个高频问题占到了将近 30% 的调用量。这些重复请求每次都走一遍 embedding 检索重排 LLM 生成浪费时间又烧钱。我在整个链路上加了三级缓存Query 级别缓存完全相同的用户 query直接返回历史答案Redis 存储TTL 设为 7 天。检索级别缓存如果用户 query 语义相近但不完全一样通过 embedding 相似度0.92 判定直接复用检索结果只重新走生成环节。片段级别缓存对高频文档片段做预计算向量启动时加载进内存避免频繁查 Milvus。这三层缓存做完后整体成本直接下降了一半左右。另外回答生成用流式输出用户第一句话大概 0.5 秒就能看到体感快了很多。4.2 并发控制与资源估算大模型推理是资源大户。内部署 Qwen 7B 时单卡 A100 的并发能力实测大概在 4 路左右再多就 OOM。这段时间我在 nginx 层配了连接数限制并在业务层用信号量来控制同时打向推理服务的请求数多余的请求排队等待。有一个很实用的参数分享流式输出时max_tokens不要拍脑袋设 2048先统计一下你们业务里历史回答长度的分布。这个项目里很多问题的正确答案就一两百字我把绝大多数场景的max_tokens设成 512既满足需求又显著降低生成延迟。4.3 故障容错是生产系统的基本素养有段时间模型服务不稳定偶尔返回超时或者 500。刚开始系统直接报错给用户后来我加了降级逻辑查缓存有就直接返回。缓存没有走重试逻辑最多重试 3 次第二次和第三次用不同的推理服务实例。重试都失败直接返回“系统暂时繁忙请稍后再试”而不是渲染一堆堆栈给用户。另外我把向量检索的失败和生成模型的失败分开处理如果向量检索成功但生成失败至少返回“参考文档”用户还能自己看体验不会太差。5. 评估体系没有度量就没有优化5.1 为什么必须自建评估集很多做 RAG 的人最偷懒的一步就是评估。跑通 demo 后手工试两三个问题觉得效果不错就宣布完成了。然后上线后被打脸。这真的是纯经验之谈——我在这个项目里一共花了大概两周时间搭建评估流程但这段时间换来的收益是后续迭代的速度。我建了一个结构化的评估集一共 300 条真实用户问题来自早期灰度测试的日志每条标注了标准答案、涉及的参考文档 ID、问题类型事实型、流程型、比较型、难度等级。为什么不用公共 benchmark因为公共数据集跟我们的业务知识分布差太远评估出来没有参考价值。5.2 三个核心评估指标及计算方式我的评估分三层第一层是召回完整性用人工标注的答案中的关键实体去比对系统检索文档中是否包含这些实体。比如标准答案是“报销流程需要提交申请单、审批单、发票”那这三个实体必须都出现在检索结果里。这个指标用代码可以自动化判断算出命中率。第二层是忠实度检查 LLM 生成的回答是否包含检索片段中没有的信息。我先把检索片段拼接成参考文本再把生成的回答拆成句子用 embedding 相似度判断每个句子是否忠实于参考文本中的某个片段。低于阈值就认为这个句子可能是幻觉。早期版本忠实度只有 82%就是因为加了引用编号约束后提高到 94%。第三层是答案有用性也就是人工评分 1-5 分。每次发版前我拉着业务方代表一起过 50 条测试题算平均分。这层短期内不能自动化但很必要。5.3 评估驱动的迭代流程有了评估体系后整个项目就完全变成了数据驱动的循环发版 - 跑评估集 - 记录指标 - 定位失败案例 - 调整环节参数 - 重新评估举个例子我通过评估发现“流程型”问题比如“怎样申请办公用品”的回答准确率只有 70%原因定位出来是清洗环节把文档里的有序列表编号去掉了导致步骤逻辑混乱。修复清洗逻辑后流程型问题准确率直接升到了 92%。如果没有评估集这种问题可能部署半年都不会被发现。6. 常见问题与排查技巧实录6.1 检索结果相关但答案错误这是最普遍的故障模式我自己的排查经验是按顺序查三件事切片粒度、上下文过量、模型指令偏差。先查切片粒度如果文档被切成 5000 字的大块embedding 平均了语义检索召回会特别模糊。我实际把chunk_size从 500 改到 600再把chunk_overlap从 50 改到 100很多模糊问题就消失了。再查上下文当 prompt 塞了 5 个检索片段但真正相关的只有一个时模型容易被无关片段带跑偏。这时候要么帮模型加一句“优先参考 [1]如果 [1] 不相关再参考其他片段”要么精简 top-k。最后查模型指令别让模型自由发挥约束条件写在 prompt 里比如“不要推测只使用给定信息回答”效果有很大差别。6.2 检索阶段查不到内容这个问题的根源经常是 embedding 对特定类型文本不敏感。方法只有两个一个是混合检索兜底这个我前面说过了另一个是文档入库时做不同粒度的双重切片——短切片200-400 字用于精确匹配长切片800-1200 字用于语义召回检索时两种粒度同时检索再合并。这套双通道方案解决了不少边界 case。6.3 召回和生成的延迟太高延迟高九成出在重排环节。我当时把 100 条候选全丢给 bge-reranker 打分导致接口 P95 延迟到了 8 秒废了。后来把候选压缩到 30 条P95 延迟降到 1.8 秒基本满足业务需求。另外一个容易被忽视的瓶颈是 embedding 服务。如果你在查询链路里同步调 embedding 服务一定要给它加超时控制和连接复用否则高并发批次一上来整个链路都会被拖垮。我在代码里用的是httpx.AsyncClient的连接池设置max_connections100。6.4 隐私与权限控制企业内部知识库往往分部门权限。业务方一开始没提这个需求但我坚持做了。方案是元数据权限过滤也就是每个用户在查询时只能检索到他有权限访问的部门标签或文档集合。在 Milvus 上用 filter 条件实现在生成层将未授权文档从候选集中直接剔除。这样从检索到生成都不会接触越权信息。6.5 数据更新的坑知识库不是一成不变的。业务文档每周都在更新如果更新不及时系统给出的答案可能是三个月前的旧流程。我在文档接入层做了一个简单的增量更新机制识别文档的last_modified时间大于当前记录版本的就重新解析、切块、替换向量文档删除时定期扫描把对应的向量和元数据批量清理掉。增量更新刚开始跑会乱因为旧数据没有删除干净检索时常出现两个版本的答案打架后来在执行更新时调成“先删除后写入”并控制并发问题就消失了。这个项目做完之后的几点体会做个总结收尾吧但不聊虚的。我在这个项目里最大的受益就是把整套链路内化成了自己的方法论。现在再去看那些框架不会再被它们牵着鼻子走。框架只是工具核心还是你对问题的拆解能力以及对数据链路的掌控能力。如果你也想从零做一个 ai-engineering 项目我建议按这个顺序动手先拿 100 篇文档和 50 个问题把最小闭环跑通再逐步加上混合检索、重排、评估、权限、缓存。不要一上来就奔着“架构特别大”去一定会塌。最后分享一个细节生产环境的日志一定要把query原文和最终检索到的文档 ID 打出来。团队的同学在排故障时全靠这对方便定位问题。我在这个项目里吃过的最大亏就是前期日志不够详细每次排查问题都要重新放一遍请求、复现问题效率极低。现在我会告诉你日志是所有 RAG 系统里最值得投资的部分这句话值得你拿小本本记下来。