ARTICLE DETAIL

资讯详情

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

AI工程从零搭建:RAG、上下文与评测的实战指南

AI工程从零搭建:RAG、上下文与评测的实战指南 做AI工程这些年我最大的感受是市面上教你怎么写模型、调参的内容一抓一大把但真正教你“从一个空目录开始把一个AI功能做成一个能上线、能维护、能迭代的工程”的内容少得可怜。我自己当初从零起步时给自己定的项目代号就叫ai-engineering-from-scratch一路踩坑走过来发现这套路径里的门道远比想象中多。这篇文章就把我实际走过的路、验证过的方案、以及反复踩过的坑系统地拆给你看。它不是什么纸上谈兵的理论而是一份可以直接照着动手的实操地图适合有Python基础、想从0到1搭建AI应用的工程师和学生。读完你会发现所谓AI工程最关键的不是模型本身而是你围绕模型搭起来的那套系统。1. 认清“AI工程”到底在解决什么问题很多人一听到“AI工程”第一反应是“我要训练一个模型”。这是最大的误区。我最初也这么想直到自己动手做第一个项目才发现真正的AI工程解决的是另外三类问题怎么稳定地拿到好结果、怎么让结果可以被评估和复现、怎么让整套系统在真实流量下活下来。1.1 为什么“跑通demo”和“做成工程”是两码事你在Jupyter Notebook里跑通一个RAG检索增强生成示例和你在生产环境里运行一个RAG服务完全是两个世界的东西。Notebook里的查询是固定的、数据是干净的、模型接口是通的你只需要验证“能不能出结果”。工程化之后你会遇到用户输入千奇百怪、知识库更新之后索引要不要重建、模型接口偶尔超时或返回异常、同一条Prompt这次好用下次就翻车……这些问题没有一个是“模型能力不行”造成的但它们每一个都能让你的项目死掉。用一个生活化的类比会炒一道菜和开一家餐馆是两种能力。炒菜只需关心火候和调味开餐馆要关心食材供应链、出菜速度、翻台率、差评处理、天气影响客流。AI工程就是从“炒菜”走向“开餐馆”的完整过程。你不仅要会调用大模型接口还得掌握数据管线、评测机制、缓存策略、监控告警、成本控制这些“餐馆运营”的能力。1.2 一个零基础项目的完整拼图我把一个从零开始的AI工程项目拆成了六块固定的拼图任务定义你到底想让模型帮你完成什么这一步的产出不是一句话而是一份包含输入输出格式、成功标准、边界条件的说明。数据准备数据从哪来、长什么样、要不要清洗、要不要切分、切多碎。RAG项目里这一步直接决定检索上限。模型选型不是“哪个模型最强选哪个”而是“在效果、延迟、成本三者之间哪个模型最匹配你的场景”。上下文工程Prompt怎么写、系统提示词怎么设计、动态内容怎么注入、历史消息怎么管理。这套东西决定了模型能不能稳定发挥。评测体系你怎么知道改动是变好了还是变坏了没有量化指标的AI工程就像蒙着眼睛开车。部署与迭代服务怎么起、接口怎么暴露、日志怎么打、效果怎么追踪、模型或提示词更新后怎么灰度。这六块拼图缺一块工程都不完整。如果你现在只关心其中一两块说明项目还没走上正轨。真正从零开始做就要按这个框架逐步补齐而不是总在模型选型上原地打转。2. 零基础上手的设计思路与路线规划既然确定要做ai-engineering-from-scratch就要有一条清晰的路线。我把自己走过的路总结成“三个阶段”每个阶段有明确的目标和检验标准跑完这三个阶段你就有资格说自己“会做AI工程”了。2.1 第一阶段先把“最小闭环”跑起来第一阶段的唯一目标是打通端到端的链路不让任何一环断裂。我当时选择的是做一个“文档问答助手”输入一篇技术文档我能向它提问并得到答案。这个项目足够简单但包含了AI工程的所有核心元素——数据加载、文本切分、向量化、检索、LLM生成、结果输出。技术选型上我用的是最朴素但也最成熟的组合# 最小闭环的核心依赖 from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain_openai import ChatOpenAI选LangChain不是因为它最强而是因为它把数据加载、切分、向量化、检索这些环节统一封装了对起步阶段的人来说能在最短时间内看到全局。FAISS作为向量库是本地文件式的不需要额外跑服务适合验证阶段。等后面数据量大了再迁移到真正的向量数据库成本也不高。这个阶段你一定要给自己定一个“完成标准”我当时定的是能把文档加载进库能搜到相关内容能基于检索结果生成一段看起来靠谱的答案。只要这三件事通了第一阶段就算过关。2.2 第二阶段把“随机成功”变成“稳定复现”第一阶段跑通后你很快就会遇到一个让所有AI工程师抓狂的问题同一个问题上午问和下午问答案不一样甚至刚问完再问一遍答案也有可能变。如果做不到稳定复现这个系统就没有交付价值。第二阶段的核心任务就是把这种“随机成功”变成“稳定复现”。我做了三件事第一把所有可变参数集中管理。模型名字、温度、top_p、max_tokens、Prompt模板全部抽到配置文件里禁止硬编码。这样每次改动我都能知道是哪个变量影响了结果。第二给Prompt加版本号。每一版Prompt我都会在系统提示词里悄悄加上版本标记比如“这是第3版系统提示词”并在日志里输出。这样做的好处是当线上结果出问题时我能立刻知道当时跑的是哪一版Prompt而不是靠猜。第三建立最简单的评测集。我整理了30个典型问题每个问题配了参考答案的评分要点。每次改动后拿这30个问题跑一遍记录“答对几题、答偏几题、完全错误几题”。一开始这个评测集不用很复杂重点在于“有”——有了这个固定标尺你才能知道改动是变好还是变坏而不是仅凭感觉判断。这个阶段最反直觉的一点是你花在评测上的时间比花在调模型上的时间还要多。但恰恰是这份“枯燥”的评测工作把项目从“能跑”推向“可信”。2.3 第三阶段用工程手段控制成本和延迟到第三阶段项目已经基本稳定了这时候要考虑的是“商业化”问题了这个系统跑一次要花多少钱用户等多久能拿到结果服务器会不会被流量打崩成本控制这块我踩过的坑最有发言权。一开始我完全不看token用量每次测试都把聊天记录全量传给模型结果月底看账单直接傻眼——小半个月工资没了。后来我做了三件事所有对话在传给模型之前做截断只保留最近5轮对相同问题做语义缓存命中缓存直接返回不消耗模型调用日常开发调试用便宜的小模型只有最终验证时才用旗舰模型。延迟控制同样重要。用户可接受的等待时间就那么多超时就等于不可用。我的优化顺序是先检查是不是检索太慢通常是因为文档没切分好导致向量库扫描量大再检查是不是生成太长max_tokens设得过大模型就会写个不停最后才考虑换更快的模型。这个顺序很重要——你永远要先优化自己可控的部分再去换模型。3. 核心环节的细节拆解与实操要点路线规划完成后接下来要深挖几个最影响成败的核心环节。我把它们称为“AI工程的四个深水区”任何一个做不好整体效果都会大打折扣。3.1 上下文工程提示词不是“写作文”先说一个我反复跟团队强调的观点Prompt engineering不是“写作文”而是“定接口”。你要做的不是把需求写得花团锦簇而是把模型当作一个函数给它定义清晰的输入契约和输出契约。一个可用的Prompt至少要包含四部分角色指令告诉模型你是谁、你要以什么视角处理问题。比如“你是一名文档审阅专家”这比“请帮我分析文档”的约束力强得多。任务与约束明确要做什么、绝不能做什么。约束里一定要写清楚“拒绝回答不知道的内容”否则模型就会开始编造。输入数据区动态注入检索结果、用户问题。这块内容要用清晰的标记包裹我第一次就是因为没分隔好导致模型分不清哪些是背景资料、哪些是待处理的问题。输出格式区规定返回结构。最有效的方式是直接给JSON示例而不是用文字描述。我的经验是先写一版能做事的简单Prompt跑通后再逐步细化格式和约束不要一上来就追求“完美”。任何Prompt的调整都要对着评测集来验证否则你只是在“感觉”上觉得变好了实际效果反而可能更差。3.2 RAG管线中的几个关键参数如果说上下文工程决定了模型的上限那RAG管线里的参数就直接决定这个上限能不能被触摸到。我把RAG的核心参数分为三组每一组都有我实测后确定的合理区间。文本切分参数chunk_size块大小和chunk_overlap块重叠。我测试过多个值结论是中文场景下chunk_size在300到600之间表现最好chunk_overlap设为80到120。值设得太小一个完整的语义单元会被切断检索到的是残缺信息值设得太大混入无关内容会稀释向量相似度。注意这个数值不是固定的最科学的做法是拿自己领域里的典型段落长度去测试后确定。检索参数top_k返回条数和相似度阈值。top_k别贪多3到5条通常足够多了只会让模型在无关信息里“挑花眼”反而增加幻觉概率。相似度阈值需要针对自己的Embedding模型实测不必过于强硬但至少要能过滤掉明显无关的噪声。生成参数temperature温度和max_tokens最大生成长度。问答场景下temperature我固定设在0.1到0.3之间偏高就会“放飞自我”max_tokens看你的答案预期长度比如文档问答设为1024就够写长文再调大。这些参数之间是联动的。我在实践里发现最有效的方式是把它们放到配置中心集中管理并且每一次调整都记录“改了哪个参数、评测分数变化是多少”。这样经过一到两周的沉淀你会得到一份属于自己项目的“最优参数组合表”而不是永远在凭感觉试。3.3 评测体系没有回归测试AI工程就是裸奔这是我从零搭建AI工程时感受最深的一点任何代码改动、Prompt调整、模型更换都必须用评测集跑一遍回归否则就是在裸奔。那些“这次好像变聪明了”的感觉99%都不可靠。一个实用的做法是“三层回归”机制单元级每次改Prompt或参数跑固定30题的基础评测集。几分钟内完成只是回答正确/错误两类判断。场景级每天跑一次全量评测集我后来扩充到100题覆盖各类场景包括边界案例比如超出知识范围的问题、格式特殊的输入。灰度级上线新版本前先用小流量试运行拿线上真实请求做对比评估。这一步能捕获到测试集覆盖不到的极端输入。评测脚本要尽量自动化。我后来写了一个简单的评测脚本把固定问题集跑完然后让一个“评测大模型”来给结果打分输出综合通过率。这个评分过程本身也依赖模型所以我会加一些硬性校验条件“答案中必须包含关键术语X”避免全盘依赖LLM判断。一句话总结评测不是上线前的冲刺而是整个开发周期里最日常的环节。4. 从零实现一个可交付的最小RAG工程理论讲完我来展示一个可以直接照着搭的、最简单的RAG工程。这个工程麻雀虽小五脏俱全你把它跑通之后就拥有了一个所有AI应用的原型骨架。4.1 项目结构与数据准备项目的目录结构我建议这样组织每个模块职责单一后期扩展也方便ai-engineering-from-scratch/ ├── config/ # 所有参数配置 │ └── settings.yaml ├── data/ # 原始文档 ├── src/ │ ├── loader.py # 文档加载 │ ├── splitter.py # 文本切分 │ ├── store.py # 向量库构建 │ ├── retriever.py # 检索模块 │ ├── generator.py # 生成模块 │ └── app.py # FastAPI接口 ├── eval/ │ └── questions.json # 评测集 └── requirements.txt数据准备阶段先别急着处理几千篇文档我建议你精挑5到10篇具有代表性的样本先人工读一遍理解它们的结构标题层级、段落长度、专业术语密集度再决定用什么策略切分。这一步很多新手会忽略直接一股脑全量切分结果就是检索质量拉胯也找不到原因。做了数据探查再定方案切分效果会好很多。4.2 嵌入、检索与生成的完整代码链路下面这个流程是完整可跑的最小实现。我用的是FAISS和OpenAI接口你用其他Embedding模型或向量库也完全可以把对应类替换掉。# src/splitter.py from langchain_text_splitters import RecursiveCharacterTextSplitter def get_splitter(chunk_size: int 500, chunk_overlap: int 100): return RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, separators[\n\n, \n, 。, , , , ], length_functionlen, )注意separators的顺序很重要它决定了切分时优先按哪些边界断开。我这里把“段落”“换行”“句号”“分号”“逗号”都列进去是为了让切分尽量落在语义完整的位置。英文文档还可以加上空格中文场景则通常是上面的配置。# src/store.py from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS def build_store(docs, persist_dir./vector_store): embeddings OpenAIEmbeddings(modeltext-embedding-3-small) db FAISS.from_documents(docs, embeddings) db.save_local(persist_dir) return dbEmbedding模型我建议默认用text-embedding-3-small这类便宜又够用的版本等检索质量成为明显的性能瓶颈时再考虑换更大的模型。很多人一开始就上大型Embedding模型效果提升有限延迟和成本却涨了一大截不划算。# src/retriever.py from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS def retrieve(query: str, top_k: int 4, score_threshold: float 0.5): db FAISS.load_local( ./vector_store, OpenAIEmbeddings(modeltext-embedding-3-small), allow_dangerous_deserializationTrue ) docs db.similarity_search_with_score(query, ktop_k) filtered [(doc, score) for doc, score in docs if score score_threshold] return [doc.page_content for doc, _ in filtered]这里有一个容易踩坑的点FAISS的similarity_search_with_score返回的是“距离”不是“相似度”距离越小表示越相关所以过滤条件用的是score score_threshold而不是大于。如果你用的是similarity_search它内部会转成相似度排序并做归一化表现不一样用之前一定要搞清楚自己到底拿的是哪种分数。我一开始就栽在这上面过滤条件反了结果检索出来的全是不相关的内容。# src/generator.py from langchain_openai import ChatOpenAI from langchain.prompts import PromptTemplate PROMPT_TEMPLATE 你是一名严谨的技术问答助手。请基于以下“参考材料”回答问题。 参考材料 {context} 用户问题 {question} 要求 1. 只能使用参考材料中的信息作答不要编造。 2. 如果参考材料中没有答案明确回答“材料中未找到相关信息”。 3. 回答使用简体中文控制在200字以内。 回答 def generate(question: str, context: str): llm ChatOpenAI(modelgpt-4o-mini, temperature0.1) prompt PromptTemplate.from_template(PROMPT_TEMPLATE) chain prompt | llm return chain.invoke({context: context, question: question}).contentPrompt里的“如果参考材料中没有答案明确回答……”这句话是抑制幻觉最便宜有效的手段。它不完美但能大幅减少模型硬编答案的倾向。你还可以加一句“当你不确定时优先说不知道”同样有效。这是我在实际测试中发现的性价比最高的上下文设计技巧。组装起来之后# src/app.py from fastapi import FastAPI from src.retriever import retrieve from src.generator import generate app FastAPI() app.post(/qa) def qa_endpoint(body: dict): question body[question] docs retrieve(question) if not docs: return {answer: 材料中未找到相关信息, sources: []} context \n\n.join(docs) answer generate(question, context) return {answer: answer, sources: docs}到这里一个最小可交付的RAG问答服务就成型了。整个过程不涉及微调模型却能服务大多数文档问答场景。这也再一次印证我的观点AI工程的价值更多体现在怎么把已有的模型能力用对、用稳、用得省。4.3 部署时的几个务实选择部署阶段很多新手会纠结于“要不要上K8s”“要不要用专门的向量数据库”我的建议是初期别碰这些复杂基础设施。一个FastAPI服务配一个进程管理器比如systemd挂一台普通云服务器就能扛住绝大多数个人项目和中小业务场景的初期流量。真正需要提前考虑的是三件事第一接口鉴权。别把AI服务裸奔到公网一个简单的API Key校验就能挡住大部分滥用。我见过不止一个把OpenAI Key嵌在前端代码里的项目结果被薅羊毛到破产这种教训太贵了。第二超时与重试。调外部模型接口网络波动是常态。一定要给请求设置超时并做指数退避重试。一次调用失败并不代表系统有问题但没有重试机制的系统一定会在大模型服务波动时跟着一起崩。第三日志结构化。每次请求把问题、检索到的文本片段、模型返回、延迟、token用量全部打成JSON日志。这是你出事之后唯一能回溯现场的线索也是后续做效果分析的数据基础省掉它就是给自己埋雷。5. 常见问题与排查技巧实录这部分是我最想分享的因为网上技术教程很多但真实踩坑记录太少。以下问题几乎每个做AI工程的人都会遇到我把排查思路直接列出来你遇到类似情况时按这个顺序排查能省下大量时间。5.1 输出总是不稳定怎么处理症状同一问题多次提问答案时好时坏或者格式偶尔不符合预期。排查顺序先看temperature。如果你设得比较高大于0.5甚至0.7那“不稳定”是必然的别急着改Prompt先把温度降下来。再看Prompt里的输出格式约束如果只是说“请用JSON格式输出”模型确实容易“自由发挥”直接给出一个JSON示例稳定性会提升非常多。最后看输入数据是否每次一致——尤其是RAG场景检索结果本身有随机性时比如向量库刚更新过输出自然不稳定这种情况需要检查索引重建是否完成、top_k是否过小导致召回内容波动。我在实践中发现80%的“输出不稳定”问题根源都不是模型变笨了而是温度没降、格式说明不清楚、或者输入源有变化。按这个顺序排查通常几分钟内就能定位。5.2 检索质量差先别急着换模型症状RAG答案答非所问或者检索回来的片段跟问题关系不大。这种问题最容易被误诊成“Embedding模型不够好”于是换更大更强的Embedding模型结果提升有限还增加了不少延迟成本。正确的排查顺序是这样的第一步人工检查切分后的文本块。随机打开十几个chunk看看语义是否完整。我见过最常见的问题是一段文档在“方法”两个字后面被切断导致检索到的是残缺描述答案自然不准。这时候应该调整chunk_size和overlap而不是换Embedding模型。第二步检查查询是否需要改写。用户的原始问题通常口语化、指代不明。比如用户问“它的准确率是多少”这个“它”指的是上一句里的哪个模型直接拿原始问题去向量库检索效果通常不好。一个实用技巧是先让LLM把用户问题改写成适合检索的独立查询,再执行检索。这一步对提升检索质量非常明显。第三步检查召回结果是否真的送了有用的内容。我经常让系统把每次检索返回的top文本片段打到日志里人工抽查几轮看看这些片段放到Prompt里是不是足够回答问题。如果片段本身没包含答案那就是检索问题如果片段有答案但模型没用上那就是上下文工程或Prompt的问题。这个“先看检索再看生成”的思路能帮你准确锁定问题出在RAG管线的哪个环节避免做无用功。5.3 线上代价的三个坑延迟、成本、可观测性最后一个大坑是系统上线后运营层面的事。我按杀伤力从小到大排列三个高频坑成本失控最容易出现的隐性问题是把系统提示词写得过长。一个几百字的系统提示词每次请求都会重复传给模型累积下来token消耗非常可观。另外对话类应用如果不截断历史记录上下文会随轮数无限膨胀账单也会跟着膨胀。我的处理方式是给历史记录设置最大轮数比如20轮超过就丢最旧的消息给单条回答设置max_tokens上限为高频请求做缓存。延迟毛刺外部模型服务的响应时间是不稳定的高峰期可能从1秒涨到5秒。如果服务端同步等待用户端就会超时。我的做法是前端交互上先返回“正在处理”的状态后端用异步任务处理处理完成再通知同时设置合理的超时阈值超过阈值就返回兜底文案而不是一直等待。这一套做完哪怕模型服务抖动用户侧体验也不会全面崩溃。可观测性缺失如果日志里看不到每次请求的prompt、检索结果、响应内容和token用量那么线上出了问题你将无从下手。我后来把日志直接接到日志分析平台里做了简单的看板请求量、平均延迟、token消耗、超时率、错误率。甚至还可以让LLM对部分回答质量做离线打分定期出一个“效果趋势图”。数据一旦跑起来你看到的就不再是感性认知而是真实的工程质量。6. 从零到一我最后想补几句实在话写到这里我想起自己刚开始做ai-engineering-from-scratch时的一个错觉以为最难的是理解Transformer、是搞懂注意力机制。后来才明白对一个AI工程师来说这些模型原理能理解当然好但真正让你和普通人拉开差距的是你对系统稳定性、成本效率、评测回归、可观测性这些工程问题的把控能力。如果你现在准备动手我的建议只有一条先做一个最小闭环然后立刻建立评测集。哪怕只是一篇文档、十个测试问题也比你在Notebook里把各种新模型试一遍有用得多。因为只有把“评价标准”立住了你后面做的每一步调整才不是原地打转。最后再分享一个我个人比较受用的小技巧每次记录实验时把“期望”和“实际结果”一起写下来。比如“我把chunk_size从500改成300期望检索更精准实际通过率反而下降了3%”。这个习惯听起来微不足道但坚持半年之后你会积累一份自己的AI工程踩坑地图这份地图比任何教程都值钱。希望这篇经验分享能帮你少走一些我当初走过的弯路。
返回列表