news 2026/10/3 5:58:52

AI工程从零到上线:手把手搭建RAG问答机器人的技术框架与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程从零到上线:手把手搭建RAG问答机器人的技术框架与避坑指南

聊一个很多人踩过的坑:学了一堆机器学习算法,卷积、Transformer、注意力机制讲得头头是道,但真要你做一个能给别人用的AI应用——比如给公司内部做一个人事政策问答机器人——就卡住了。数据不知道从哪来,模型不知道怎么接,就算接上了,回答得乱七八糟也不敢上线。这就是典型的"会算法但不会AI工程"。

我理解的ai-engineering-from-scratch,不是从零推导数学公式,也不是自己从头训练一个大模型,而是指:从零建立一套把大模型能力稳定落地成产品的工程能力。它跟你懂多少层Transformer没有必然关系,反而跟数据清洗、系统设计、成本意识、评测方法这些东西强相关。这篇文章就想聊聊我走通这条路径时沉淀下来的技术框架、实操步骤和踩过的坑,适合那些已经入门Python、想从"跑通Notebook"迈向"交付可用AI应用"的人。

1. 先搞清楚:AI工程到底在做什么

1.1 它跟算法研究是两条路

很多人误以为AI工程师就是"训练模型的人"。实际上,今天的AI工程重心早就变了。基础模型由专门的团队去训练,大部分业务的AI工程是在"已有大模型能力"之上做二次开发:把模型接入业务数据,用提示词把输出校准到可用水平,再把它包成一个稳定、可控、可监控的服务。

我自己的一个判断标准是:如果你做的东西挂了,用户感知到的是什么。算法研究员关心的是"这个模型在新测试集上的F1是否提升了",而AI工程师关心的是"用户问了一个奇怪问题,服务是否还能正常返回、是否还能控制成本和延迟"。这是两种完全不同的思维方式,前者追求最优,后者追求稳态。

1.2 为什么"从零开始"要刻意做减法

网上关于AI工程的资料很多,但绝大多数是"工具全家桶"式的,LangChain、LlamaIndex、向量数据库、Agent框架,恨不得全堆上去。我走过这段弯路,结论很简单:从零起步的时候,工具越多,你越是学了个寂寞。

原因在于,这些框架抽象层级太高,如果你不理解它背后的逻辑,出了问题根本不知道去哪查。我见过有人连向量检索的原理都没搞清,就用LangChain搭了一个RAG,结果检索结果一塌糊涂,他只能重启服务碰运气。所以我的建议是:从零开始,先把最小链路用"裸代码"跑通,再逐步引入框架。你亲手拼过一次,脑里才有那张数据流向图,后面用任何框架都是图省事,而不是被框架牵着走。

1.3 首先定义你的第一个里程碑

没有目标的从零开始,最后都会变成"学了三个月,啥都没做出来"。我在带人时,一定会让他们先定一个足够小的里程碑,比如:

做一个基于本地文档的问答应用,用户上传一份PDF,就能问这份PDF里的问题,并且回答要能注明出处。

这个目标看起来普通,但它涵盖了AI工程的全部核心要素:数据解析、文本分块、向量化与检索、提示词拼装、大模型调用、输出格式约束、成本控制。做完这个,你就不是在"学AI",而是在"做AI工程"了。

2. 从零搭建AI工程的技术栈选型

2.1 编程语言与Python生态的必要性

目前做AI工程,Python依然是最稳妥的选择,不是因为它语法有多优雅,而是因为整个AI生态的"默认语言"就是它。你随便翻一个大模型SDK文档,示例代码基本都是Python;开源的向量库、文档解析库、以及部署相关的工具链,对Python的兼容性最好。

但我要多说一句:Python基础必须扎实到"能看懂源码"的程度。你不能只会调用,至少要理解装饰器、上下文管理器、生成器、类型标注。原因是AI工程链条很长,任何一个环节出错,排查时都要深入库的源码。我印象很深的一次是某个embedding接口的报错信息非常隐晦,最后顺着源码才发现是请求重试逻辑里没有处理网络断连的异常类型。基础不牢,你连入口都找不到。

2.2 模型接入:API优先,别急着本地部署

群里经常有人问:"我要不要搞一张显卡跑开源模型?"我的回答一般是:如果你不是专门做私有化部署交付的,先别。从零开始做AI工程,最高效的方式是直接使用成熟的大模型API服务,五花八门的国内大模型平台都提供了兼容接口,按量付费、免运维、自带推理优化。

API优先的好处不只是省事,它还能让你把有限的精力集中到AI工程真正的核心——数据处理和系统设计上。等到你的应用真的跑通、有了稳定的调用量,再计算"自部署"和"API"的成本临界点。真到了那时候你也会发现,最大的成本往往不是模型推理,而是维护一个GPU集群的人力和精力。

2.3 数据侧的选型:向量库别盲目追求流行

RAG是目前从零上手性价比最高的AI应用形态,而向量数据库是避不开的组件。流行的选项很多:FAISS、Chroma、Qdrant、Milvus、pgvector。新手容易犯的选择困难症是"哪个最强大选哪个",但我的原则是:跟业务体量匹配。

如果只是原型验证、几万条文档片段,用Chroma或者直接上FAISS就够了,它们轻量、嵌入快速、排查问题方便。如果数据量到了百万级、需要分布式扩容和精细化权限控制,再上Milvus或Qdrant。如果你的团队本身就在用PostgreSQL,那pgvector是最省心的选择,少维护一套系统。工程上"少一个组件"本身就是一种优势,运维成本和故障点都降低了。

2.4 工程化工具链:从第一天就要有的习惯

我见过太多人的AI项目死在"代码只能在自己电脑上跑"这一步。从零开始就要建立三个习惯:代码版本管理、环境隔离、依赖锁定。

具体来说:Git是最基本的,不解释;Python项目必须用虚拟环境,要么venv要么conda;依赖要用requirements.txt或pyproject.toml锁定版本,尤其是AI项目对版本极其敏感——embedding模型换了版本,向量维度或分布可能都变了,旧索引很可能直接失效。还有一个容易被忽视的是数据版本管理,对AI应用来说,知识库数据就是代码的一部分,每次调整数据都要能回溯,别等出了线上事故才发现是半个月前的数据改动引起的。这套习惯建立起来之后,你的项目才算真正有了"工程"的雏形。

3. 实操:从零手写一个最小可用的RAG问答服务

3.1 先画数据流,再写代码

我不太建议一上来就写代码。先用最简单的方式把系统数据流画出来,哪怕是纸笔也行。一个典型的RAG问答系统,数据流长这样:

文档输入 -> 文本清洗 -> 分块(Chunking) -> 向量化(Embedding) -> 存入向量库 用户提问 -> 向量化(相同模型) -> 向量检索 -> 拼装Prompt -> 调用大模型 -> 返回回答

这个图是不是看着特别简单?但绝大多数线上事故都出在这条链路的某个环节里。你在脑子把这张图刻下来,后面做监控、做日志、做排查,都是围绕它展开的。哪个环节慢了、哪个环节报错了、哪个环节数据不对了,你第一反应就应该是定位到图上的位置,而不是瞎试。

3.2 核心链路代码骨架

我以"处理一份Markdown文档并回答用户提问"为例,写一个最简但结构完整的版本。这里我刻意不引入任何编排框架,让你看清每一步在干什么。代码使用示例性的接口风格,换成本地开源模型SDK也是一样的套路。

from typing import List import os import hashlib # 假定环境变量里配置了API Key from openai import OpenAI client = OpenAI(api_key=os.environ["MODEL_API_KEY"], base_url=os.environ["MODEL_API_BASE"]) EMBEDDING_MODEL = "your-embedding-model-name" # 例如 text-embedding-3-small LLM_MODEL = "your-chat-model-name" # 例如 gpt-4o-mini def load_document(path: str) -> str: with open(path, "r", encoding="utf-8") as f: return f.read() def split_chunks(text: str, chunk_size: int = 800, overlap: int = 100) -> List[str]: """按字符数简单切分,带固定重叠。生产环境建议按标题结构切分。""" chunks = [] start = 0 n = len(text) while start < n: end = min(start + chunk_size, n) chunks.append(text[start:end]) if end == n: break start = end - overlap return chunks def embed_texts(texts: List[str]) -> List[List[float]]: # 注意:真实实现需要处理batch大小限制和重试逻辑 resp = client.embeddings.create(model=EMBEDDING_MODEL, input=texts) return [d.embedding for d in resp.data] def build_index(doc_path: str, vector_store): doc = load_document(doc_path) chunks = split_chunks(doc) doc_id = hashlib.md5(doc_path.encode()).hexdigest() for i, chunk in enumerate(chunks): emb = embed_texts([chunk])[0] vector_store.add( id=f"{doc_id}:{i}", vector=emb, payload={"text": chunk, "source": doc_path, "chunk_index": i} ) return len(chunks) def search(query: str, vector_store, top_k: int = 4) -> List[str]: q_emb = embed_texts([query])[0] hits = vector_store.search(q_emb, top_k=top_k) return [h.payload["text"] for h in hits] def build_prompt(query: str, contexts: List[str]) -> str: context_block = "\n\n---\n\n".join(contexts) return f"""请仅基于以下参考资料回答用户问题。 如果参考资料中没有相关信息,请直接回答"资料中未找到相关内容",不要编造。 参考资料: {context_block} 用户问题:{query} """ def ask(query: str, vector_store, llm_client): contexts = search(query, vector_store) prompt = build_prompt(query, contexts) resp = llm_client.chat.completions.create( model=LLM_MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.2, stream=False, ) return resp.choices[0].message.content

这个代码骨架里最容易被忽视的点有两个。

第一个是在Prompt里明确要求"资料中没有就直说"。这是对抗幻觉最便宜的手段,不做的话模型很容易顺着用户的问题强行编一个答案。第二个是embedding的batch调用问题。真实场景中你可能有成千上万个chunk,绝对不能一个循环里逐条调接口,那太慢了,必须按batch合并后一次请求。而且embedding接口通常有每分钟调用次数限制,幂等重试和退避策略是必须的,这块代码看起来不"AI",但它决定了你的数据处理流程能不能在合理时间内跑完。

3.3 参数选择背后的工程考量

很多人问我:"chunk_size到底设为多少合适?"我的回答是:没有固定最优值,它是数据形态和模型能力之间权衡的结果。

chunk太小,比如200个字符,检索是精准了,但上下文信息不完整,大模型可能理解不了句子之间的指代关系;chunk太大,比如2000个字符,单次检索返回的内容太泛,而且可能切断语义边界,还容易超出上下文窗口限制。我用800到1000字符、重叠100到150字符作为起步值,后续再根据评测结果调。重叠区域的存在是为了避免一个语义完整的段落恰好被一刀切断。

另外一个看似细节实则关键的参数是temperature。在RAG场景下,我永远把它调低,基本在0.1到0.3之间。原因很直白:这是问答系统,用户要的是"稳定正确的答案",不是"富有创意的长篇大论"。温度越高,同一问题两次回答的差异就越大,这对工程上做回归测试非常不利。

3.4 从原型到"勉强能上线"的工程化改造

上面的代码跑通只算原型,离"能交付"还有一段距离。我每次在项目里至少要做四个改造。

第一是增加缓存层。同样是"人事政策里病假天数"这种高频问题,每次都重新embedding加重新调大模型,既花钱又慢。用一个简单的键值缓存,key是query的哈希,value是回答结果,命中率一上来,成本至少降三分之一。第二是给接口加超时和重试。大模型API的响应延迟波动很大,你不设置超时,用户就会一直转圈;不设置重试,一次网络抖动就白屏。第三是加日志和链路追踪。用户问了一句"今年年假还能休几天",你得能从日志里追溯出检索了哪几个chunk、Prompt最终长什么样、大模型返回了什么。没有这套东西,后面出了问题你连复现都没法复现。第四是安全兜底。输出的内容要做基本的敏感信息过滤,Prompt里也要加系统级的安全约束,别把希望全寄托在模型自觉上。

# 缓存装饰器只是示意,实际生产可换用Redis _CACHE = {} def cached_answer(query: str): key = hashlib.md5(query.encode()).hexdigest() if key in _CACHE: return _CACHE[key] def wrapper(): contexts = search(query, vector_store) prompt = build_prompt(query, contexts) answer = call_llm(prompt) _CACHE[key] = answer return answer return wrapper()

4. 从零到一踩过的坑:问题排查与实务技巧

4.1 向量检索"查不到"的真相

第一个高频问题:用户的问题明明跟文档里的内容很相关,但检索出来的是无关片段。大部分人第一反应是"换更好的向量模型",但我的经验是,八成问题出在数据上。

最常见的是分块切碎了语义,比如连续两句属于同一条规定,却因为长度限制被拆进了两个chunk,检索时单看哪块都不完整。解决办法是设计"父子块"结构:把小chunk用于检索,拿到命中小chunk后,将它所属的父块(更大范围的章节)一并丢给大模型。还有一个常见原因是用户问法的表述跟文档里的用词差异很大,比如文档写"薪资构成",用户问"工资怎么算的"。这类问题靠向量模型硬扛效果有限,更有效的做法是给索引里的关键文档块配置别名关键词,或者在提问侧做一次改写(把口语问题改成更接近书面语的关键词组合)。

4.2 上下文窗口与Token成本失控

第二个高频问题:回答质量确实上去了,但Token消耗大得惊人,账单看着肉疼。原因通常有两个:一是你把所有检索结果一股脑全塞进Prompt,二是chunk切得太大,一次检索四个chunk,每个2000词,一轮问答烧掉八千词的上下文。

工程上有对应的体检清单:检索结果的条数是不是太多了(top_k=3到4通常够用);每个chunk是不是太大了;系统提示词里是不是堆了一堆长而无效的规则说明;还有是否开启了流式输出给用户"先看到第一个字"的体验,避免用户因等待时间过长反复重试(重试也烧钱)。最有效的降本办法是上文提到的缓存,尤其对高频重复问题,命中一次省一次全套链路费用。

4.3 大模型"幻觉"怎么压制

彻底消灭幻觉现在还做不到,但可以把概率压到可接受范围,除了Prompt里明确要求"未找到请直说"之外,我更推荐一套组合拳。

第一,强制要求模型给出来源引用。我通常会在Prompt里要求模型在回答末尾列出来自第几份文档的哪个小节,并附上该小节的原文短引用。第二,设置一个简单的"置信度校验"环节,让模型自己先评判检索到的上下文是否足够回答用户问题,不够就不答,这个步骤看起来土,但确实能把强编概率压下来。第三,也是工程师最容易忘记的:建立评测集。你收集一批典型问题,每条标注好期望答案和对应的文档片段,每次改Prompt、换模型、调分块参数后都跑一遍这批问题,肉眼扫一遍回答质量。没有评测集,你就永远在靠感觉做优化。

4.4 测试集应该覆盖什么

评测集不要求大,但要求有代表性。我会分成三类:第一类是从实际用户日志里捞出来的高频问题,这类最重要,它代表真实需求;第二类是有明确标准答案的事实型问题,适合快速判断正确率;第三类是边界和对抗性问题,比如跟文档相关的反事实问题("文档里是不是提到过XX不存在的内容"),专门用来检查模型会不会强行说"有"。这套评测集的价值会随着时间增长越滚越大,它其实是你这个AI应用在数据侧的"回归测试体系"。

4.5 原型环境与生产环境脱节

最后这个坑特别隐蔽:你在本地Notebook里跑得好好的,一上生产就各种状况。差别在哪?本地内存里存的索引,重启就没了;本地单个请求,无所谓并发;本地你用的测试文档,干净整洁,生产环境里的真实数据却充满格式混乱、扫描件和表格。

工程解法是环境一致性:用Docker把依赖和服务封装起来,向量库的结果持久化到磁盘或云存储,接口层用并发测试压一遍,最好把真实数据里最脏的那部分样本提前暴露给系统。把"本地能跑"和"生产能跑"当成两个不同目标,你会少很多深夜崩溃。

5. 写在后面的话

这条路我自己走了一遍,最大的感受就是:AI工程的门槛不在数学,而在"系统性思维"。你面对的不是一个模型函数,而是一条从用户问题到最终答案的完整链路,链路里的任何一环都可能让你一夜回到解放前。

从零开始的人不要贪多,就做那一个问答机器人,把它做到"自己被自己说服可用"的程度,你会发现后面的一切都顺理成章了。等技术熟练以后,自然可以往Agent、多模态方向扩展,单向知识问答也只是起点而已。最后再分享一个小建议:每次跑完一次完整的项目迭代,把"这次出问题的环节、原因、排查过程、最终方案"记成笔记。这些笔记才是AI工程这条路上最值钱的资产,比任何课程和框架都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 5:58:33

电机拖动难学?从电磁学基础到磁路与转矩的工程直觉

作为一个把《电机拖动》这门课啃过一遍又一遍的人&#xff0c;我一直觉得这课最劝退的地方不在后面的电机绕组和机械特性&#xff0c;而在最前面的电磁学基础。很多同学学到“他励直流电动机的机械特性”时突然懵了&#xff0c;回过头才发现&#xff0c;早在“电磁学基本知识与…

作者头像 李华
网站建设 2026/10/3 5:58:32

Superpowers实战:用技能包让Codex CLI从“会说话”到“会干活”

这是一个让我挺兴奋的题目。先说下背景&#xff0c;我最近半年基本把主力开发环境从IDE搬到了终端&#xff0c;日常写代码、做重构、提交PR都用Codex CLI这类AI编程工具在跑。工具用久了&#xff0c;我逐渐发现一个问题&#xff1a;AI能听懂人话&#xff0c;但做起事来总差那么…

作者头像 李华
网站建设 2026/10/3 5:58:25

STM32嵌入式智能垃圾分类系统实战设计

1. 项目概述&#xff1a;这不是一个“玩具车”&#xff0c;而是一套可落地的嵌入式智能决策系统你在网上搜“STM32 智能垃圾分类机器人”&#xff0c;大概率会看到一堆用电机红外传感器纸盒拼出来的演示模型——轮子转得挺欢&#xff0c;但扔进去一个易拉罐&#xff0c;它可能识…

作者头像 李华
网站建设 2026/10/3 5:57:33

基于Dify构建AI复盘应用:LLM与RAG实现项目经验自动化沉淀

先聊一个比较反直觉的事&#xff1a;我们总觉得“后见之明”是个贬义词&#xff0c;形容那种事情发生之后才说“我早就知道”的人。但如果换个视角&#xff0c;hindsight恰恰是项目复盘里最值钱的能力——事情结束之后&#xff0c;用已知的结果反推决策链条中的漏洞&#xff0c…

作者头像 李华
网站建设 2026/10/3 5:57:27

SpringBoot+Vue滑雪场管理系统:从选题到答辩的完整实战指南

马上要交毕业设计了&#xff1f;如果你正在找题目&#xff0c;或者已经选了springbootvue滑雪场管理系统这个方向&#xff0c;这一篇可以帮你把“从选题、建模、编码到论文答辩”的整条链路理顺。滑雪场管理系统不是普通的学生管理系统套皮&#xff0c;它天然自带票务、教练预约…

作者头像 李华
网站建设 2026/10/3 5:56:30

AI编程Skills全指南:从手动安装到自主编写与清理

最近“skills”这个词在AI编程圈里热度一路飙高&#xff0c;从Claude Code到Codex再到OpenCode&#xff0c;几乎每个主流AI编码工具都在往自家产品里塞进“skills”能力。作为一个长期折腾各种AI工作流的人&#xff0c;我前前后后把skills相关的工具、仓库、写法摸了个遍&#…

作者头像 李华