2026/9/30 19:28:35

从零搭建AI工程能力:问答系统实战与避坑指南

从零搭建AI工程能力:问答系统实战与避坑指南 1. 从零搭建AI工程能力为什么我劝你别一上来就调包这两年“AI工程”这个词被说得太多了多到有点变味。打开任何一个技术社区满屏都是“三行代码调用大模型”“十分钟搭建RAG”“零基础转行AI工程师”。我身边不少做后端、做前端的朋友看得心痒觉得门槛也没那么高于是买了几节网课跟着敲了一遍LangChain的demo就觉得自己算是入门了。结果真到了公司要落地一个稍微像样的AI功能比如做一个能查内部文档、能带上下文记忆、还能控制成本的问答助手立刻就卡住了——不知道向量库怎么选不知道chunk怎么切不知道token怎么算更不知道线上延迟为什么忽高忽低。这就是我想聊“ai-engineering-from-scratch”这个主题的原因。它不是让你从零开始手写一个Transformer也不是让你去啃论文复现GPT而是指从工程视角把AI能力真正落地成可维护、可观测、可迭代的系统。说白了模型是别人的但工程是你自己的。你要解决的是数据怎么进、结果怎么出、成本怎么控、效果怎么评、线上怎么稳这一整套问题。适合看这篇内容的人是那些已经会写代码、但对AI工程还没有系统认知的开发者以及那些被demo骗过、想认真补齐工程链路的同学。我自己走过一段弯路。最开始我也是“调包侠”觉得把API接上就完事了。后来做一个客服知识库项目用户问一句“上个月的退款政策改了吗”系统要么答非所问要么把三年前的旧文档翻出来。那一刻我才意识到AI工程的核心根本不在模型调用那一行代码而在它前后的所有脏活累活。下面我就把这套从零搭建的思路按我实际踩过的顺序拆开讲。2. 整体设计思路先想清楚数据流再谈模型选型2.1 为什么“先跑通demo”是个陷阱很多人做AI项目的习惯是先找个最火的框架把官方示例跑起来看到输出正常就以为架构定了。这个顺序其实是反的。Demo之所以能跑通是因为它喂给你的数据是干净的、问题是标准的、场景是单一的。而真实项目里数据是脏的、问题是发散的、场景是复合的。我现在的做法是动手写第一行代码之前先在白纸上画一条数据流原始数据从哪来 → 怎么清洗 → 怎么切分 → 怎么向量化 → 存到哪 → 用户问题怎么进来 → 怎么检索 → 怎么拼上下文 → 怎么调模型 → 结果怎么后处理 → 怎么返回给用户。这条链路上每一个箭头都是一个可能出问题的工程点。你先把这条线画清楚再去选具体用什么工具就不会被框架牵着鼻子走。提示如果你画不出这条数据流说明你还没想清楚要做什么这时候写代码就是浪费时间。2.2 分层架构把“会变的”和“不变的”分开AI工程系统最容易烂的地方就是所有逻辑搅在一起。今天换个模型明天换个向量库后天改个切分策略结果牵一发动全身。我的经验是分成四层数据层负责原始文档的采集、清洗、切分、向量化、存储。这一层的变化频率中等主要跟着数据源和切分策略走。检索层负责把用户问题转成查询去向量库或关键词库里捞相关内容做重排和过滤。这一层变化频率高因为检索策略直接决定效果。生成层负责拼prompt、调模型、解析输出。这一层变化频率最高模型迭代快prompt也要反复调。应用层负责对外接口、鉴权、限流、日志、监控。这一层相对稳定但一旦出问题就是线上事故。分层的意义在于当你发现效果不好时能快速定位是哪一层的问题。是检索没捞到对的文档还是捞到了但模型没用好这两者的修法完全不同。我见过太多人一效果不好就换模型结果换了三四个模型还是不行因为根子在检索层。2.3 成本与延迟的提前估算这一条是很多新手完全忽略的。AI工程和传统后端最大的区别之一就是每一次请求都有真金白银的成本而且延迟不可控。你在设计阶段就要算一笔账。假设你的知识库有5000个文档平均每个文档2000字切分后大概每个chunk 500字那就是2万个chunk。每个chunk向量化一次按常见的embedding模型算大概要花几块钱到几十块钱不等这是一次性成本。但用户每次提问你都要把问题向量化然后去2万个向量里做相似度检索再取top 5拼进prompt。如果每个chunk 500字约等于700个tokentop 5就是3500个token加上系统prompt和用户问题一次请求输入大概4000 token输出按500 token算。按当前主流模型的定价一次请求的成本大概在几分钱到一毛钱之间。听起来不多但如果日活一千一天就是几十到上百块一个月就是几千块。延迟方面向量检索通常在几十毫秒模型生成则取决于输出长度几百毫秒到几秒都正常。你要在架构里预留超时和降级策略不能让它无限等下去。3. 核心细节解析数据、检索、生成三件套3.1 数据切分chunk大小不是拍脑袋定的切分是AI工程里最容易被低估的环节。很多人直接用框架默认的1000字符切分重叠200字符就完事了。但chunk大小直接决定了检索的粒度和生成的质量。chunk太大一个chunk里混了好几个主题检索出来噪音多模型容易被带偏chunk太小一个完整的意思被切碎检索出来信息不全模型答不完整。我的经验是chunk大小要跟着你的文档类型走。技术文档、API说明这种结构清晰的按段落或按标题层级切每个chunk控制在300到500字小说、访谈这种连续叙述的按语义切分chunk可以到800到1000字表格和代码块要单独处理不能硬切。重叠部分的作用是防止关键信息正好落在切分边界上被切断。一般设成chunk大小的10%到20%就够了。我试过完全不重叠结果有些问题的答案正好跨在两个chunk之间检索时两边都只捞到一半模型拼出来的答案就是残缺的。还有一个细节是元数据。每个chunk除了文本内容还要带上来源文档、章节标题、更新时间、权限标签这些信息。这些元数据在检索时可以拿来过滤比如只搜某个部门有权限看的文档或者只搜最近半年更新的内容。没有元数据你的检索就是盲搜。3.2 向量化与向量库选型别只看benchmark向量化就是把文本变成一串数字让语义相近的文本在向量空间里距离也相近。这一步用的embedding模型选型时不要只看排行榜上的分数要看你的语言、你的领域、你的成本预算。中文场景下有些开源embedding模型在通用语料上表现不错但在专业领域比如医疗、法律、金融就会掉点。这时候要么用领域数据微调一下要么在检索层加一个关键词召回做互补。我一般会做混合检索向量检索负责语义匹配关键词检索比如BM25负责精确匹配两路结果合并后重排。这样即使用户问了一个很生僻的专有名词向量模型没见过关键词也能兜住。向量库的选型小规模几万到几十万向量用FAISS或者Chroma就够了本地跑零运维。上了百万级就要考虑Milvus、Qdrant这类专门的向量数据库支持分布式和持久化。选的时候重点看三件事索引类型是否支持你的数据规模、是否支持元数据过滤、是否支持增量更新。我踩过一个坑早期用了一个不支持增量更新的库每次加新文档都要全量重建索引5000个文档重建一次要十几分钟根本没法做实时更新。3.3 Prompt拼装把模型当人看把上下文当 briefingPrompt拼装看起来简单就是把检索到的内容塞进去但其实很讲究。我的原则是把模型当成一个刚入职的聪明新人你要给它清晰的briefing。一个合格的prompt至少包含四部分角色设定、任务说明、参考资料、输出格式。角色设定告诉它你是谁比如“你是一个企业内部知识库助手”任务说明告诉它要干什么比如“根据下面的参考资料回答用户问题如果资料里没有答案就说不知道不要编造”参考资料就是检索到的chunk要标上编号方便引用输出格式规定它怎么答比如“先给结论再给依据依据要标注来源编号”。这里有个细节参考资料要按相关度排序最相关的放最前面。因为很多模型对上下文开头和结尾的内容注意力更强中间容易忽略。如果你把最相关的放中间效果可能反而差。我实测过同样的top 5按相关度降序排列比随机排列答案准确率能差出十几个百分点。注意prompt里一定要明确禁止编造。我见过太多案例检索没捞到相关内容模型硬编了一个看起来很像的答案用户信以为真后果很严重。4. 实操过程从零搭一个可用的问答系统4.1 环境准备与依赖安装我假设你已经有一个Python环境版本3.9以上。先建一个干净的虚拟环境这一步别偷懒AI工程的依赖冲突非常常见。python -m venv ai-eng source ai-eng/bin/activate # Windows用 ai-eng\Scripts\activate pip install --upgrade pip然后装核心依赖。我这里列的是最小集合具体版本你按实际情况调pip install openai # 或者你用的其他模型SDK pip install sentence-transformers # 本地embedding pip install faiss-cpu # 向量检索有GPU可以装faiss-gpu pip install langchain # 编排框架可选但省事 pip install pypdf # 解析PDF文档 pip install tiktoken # 算token数提示如果你用的是在线embedding服务就不需要sentence-transformers但要注意网络延迟和费用。本地模型的好处是免费且数据不出域坏处是占内存、速度慢一些。4.2 文档加载与清洗这一步的目标是把各种格式的文档PDF、Word、Markdown、HTML统一转成纯文本。我写了一个简单的加载器按扩展名分发import os from pypdf import PdfReader def load_document(file_path): ext os.path.splitext(file_path)[1].lower() if ext .pdf: reader PdfReader(file_path) text \n.join(page.extract_text() or for page in reader.pages) elif ext in (.md, .txt): with open(file_path, r, encodingutf-8) as f: text f.read() else: raise ValueError(f不支持的文件类型: {ext}) return text清洗主要是去页眉页脚、去多余空行、去乱码。PDF解析出来的文本经常带一堆换行和空格我一般用正则把连续空白压成一个空格但保留段落之间的双换行。这一步别过度清洗把有用的格式信息也洗掉了。4.3 切分与向量化切分我用的是递归字符切分优先按段落切段落太长再按句子切句子还长再按字符切。这样能尽量保持语义完整。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , ] ) chunks splitter.split_text(text)向量化用sentence-transformers加载一个中文embedding模型from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) embeddings model.encode(chunks, normalize_embeddingsTrue)normalize_embeddingsTrue很重要这样向量都是单位向量后面用内积算相似度就等价于余弦相似度省一步计算。4.4 建索引与检索用FAISS建一个内积索引import faiss import numpy as np dimension embeddings.shape[1] index faiss.IndexFlatIP(dimension) index.add(np.array(embeddings).astype(float32))检索时把用户问题也向量化然后搜top kdef search(query, k5): query_vec model.encode([query], normalize_embeddingsTrue) scores, indices index.search(np.array(query_vec).astype(float32), k) return [(chunks[i], scores[0][j]) for j, i in enumerate(indices[0])]这里有个经验top k不要设太大。k5到8通常够了设到20反而引入噪音还增加token成本。我一般先用k10召回再用一个重排模型比如cross-encoder精排到top 5。重排模型比向量检索慢但准很多适合对准确率要求高的场景。4.5 拼prompt与调用模型def build_prompt(query, contexts): context_text \n\n.join( f[资料{i1}] {ctx} for i, (ctx, _) in enumerate(contexts) ) prompt f你是一个企业内部知识库助手。请根据下面的参考资料回答用户问题。 如果参考资料中没有相关信息请直接说根据现有资料无法回答不要编造。 参考资料 {context_text} 用户问题{query} 请先给出结论再给出依据依据要标注资料编号。 return prompt调用模型的部分不同SDK写法不同核心就是传prompt、设temperature问答场景我一般设0.1到0.3越低越稳定、设max_tokens控制成本。4.6 加一层缓存和日志上线前一定要加缓存。很多用户会问重复或相似的问题缓存能省大量成本。我用的是问题向量做key相似度超过阈值就命中缓存。日志则要记录每次请求的问题、检索到的chunk id、模型输出、耗时、token消耗这些数据是后续优化的基础。5. 常见问题与排查技巧实录5.1 检索不准的排查顺序检索不准是最常见的问题排查要按顺序来别乱试。排查项检查方法常见原因切分是否合理随机抽几个chunk看内容chunk太大混主题太小丢信息embedding是否匹配用几个已知相似句测相似度模型不适合中文或领域检索参数调整top k和相似度阈值k太小漏召回k太大引噪音元数据过滤检查过滤条件是否过严把相关文档过滤掉了查询改写看用户原始问题是否太短短问题语义信息不足我遇到最多的是切分问题。有一次用户问“报销流程”检索总是捞不到后来发现报销流程的文档被切成了三段每段都不完整向量化后语义都偏了。改成按标题层级切每段保持完整问题就解决了。5.2 模型答非所问的三种情况第一种是检索没捞到对的资料模型只能瞎编。这种情况要在prompt里强制它说不知道同时优化检索。第二种是捞到了但模型没看懂可能是prompt里的资料格式太乱或者资料太多模型注意力分散。这时候要精简资料只放最相关的。第三种是问题本身有歧义模型理解偏了。这种情况可以在检索前加一步查询改写把用户问题扩写成更明确的查询。5.3 延迟忽高忽低的优化延迟波动通常来自三个地方embedding计算、向量检索、模型生成。embedding和检索一般很快波动主要来自模型生成。优化手段包括设max_tokens上限、用流式输出让用户先看到部分结果、对长回答做截断、高峰期降级到更小的模型。我实测过把max_tokens从2000降到800平均延迟能降一半以上而大部分问答场景800 token足够。注意流式输出虽然体验好但要做好前端拼接和错误处理网络中断时要有兜底。5.4 成本失控的预警信号如果你发现账单涨得比预期快先查三件事一是top k是不是设太大了二是prompt里是不是塞了太多无关资料三是缓存命中率是不是太低。我见过一个案例top k设了20每次请求输入上万token成本是正常情况的四倍。改成top 5加缓存后成本直接降到原来的五分之一。6. 我踩过的坑和几条实在建议第一个坑是过早引入复杂框架。我一开始就用LangChain的全套抽象结果出了问题根本不知道是哪一层的事调试成本极高。后来我改成先用最朴素的代码把链路跑通每个环节都自己控制等稳定了再考虑用框架简化。框架是帮你省事的不是帮你理解问题的。第二个坑是忽略数据更新。知识库不是建一次就完事文档会更新、会新增、会删除。我早期没做增量更新每次都要全量重建后来改成给每个chunk打时间戳更新时只重建变化的文档效率高了很多。第三个坑是不做评估。效果好不好不能靠感觉。我后来建了一个小测试集五十个问题和标准答案每次改动都跑一遍看准确率是升是降。没有这个测试集你根本不知道自己的改动是优化还是劣化。最后分享一个实用技巧把检索到的chunk和模型输出一起存下来。这样当用户反馈答案不对时你能快速定位是检索的问题还是生成的问题。这个习惯帮我省了大量排查时间。这套东西说起来不复杂但每个环节都有细节。AI工程的门槛不在模型而在这些工程细节的积累。你把这些脏活累活做扎实了模型的能力才能真正发挥出来。