news 2026/11/3 6:45:56

AI工程化实战:从零手写大模型应用核心链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI工程化实战:从零手写大模型应用核心链路

我花了三年时间在各类AI系统里打转,从最早的文本分类、关键词抽取,到后来做问答、知识库、智能体,越来越发现一个残酷的事实:大多数AI项目不是死在模型效果上,而是死在工程化上。这三年里,我反复验证了一件事——如果你真的想把AI工程做扎实,最可靠的方式不是找一个酷炫的框架往上堆,而是从零开始手写核心逻辑,把每一层拆开看清楚。这个项目“ai-engineering-from-scratch”就是在这样的背景下启动的:放弃对黑盒的依赖,用最朴素的代码把模型调用、向量检索、上下文管理、任务编排串成一条完整链路。

这篇文章我会把它的完整设计思路、技术选型、关键实现、评测方法和踩坑记录都摊开来讲。不是说框架不好,而是很多问题只有在你亲手从零实现一遍之后,才能理解框架为什么那样设计。你可能是刚入门AI开发的学生,也可能是已经在生产环境里被各种抽象概念折腾过的工程师,这篇内容都能给你一套可复现、可验证的落地路径。

1. 为什么我坚持从零开始搭建AI工程

1.1 框架带来的是什么,又偷走了什么

引入现成的AI框架时,最常见的情景是你还没有搞清楚业务需要什么,就已经被框架背后的抽象模型牵着走了。比如某个编排框架要求你定义节点、定义连接、定义状态机,这些概念本身没有问题,但问题在于:如果你没有亲手实现过一个状态循环,你根本无法判断这些抽象是必要的还是多余的。

框架偷走的是你对细节的感知。当你调用一个封装好的Agent执行函数时,你只知道它“工作”,不知道它内部如何处理上下文超长、工具返回格式异常、模型输出JSON非法。这些细节才是生产环境真正的拦路虎。我从零开始写第一版时,目标非常明确:不要任何框架级的运行时,只用HTTP请求、Python字典、SQLite和少量参数配置,把整个流程跑通。

1.2 这个项目到底在解决什么问题

我观察到很多团队在落地AI功能时反复卡在同一个点上:单点技术都会,串联起来就崩。有人能把大模型调用写得很好,但不知道如何管理多轮记忆;有人懂向量化,但不知道召回结果怎么跟模型输出连接起来;有人会调提示词,但无法建立一套离线评测集来度量改动的好坏。

这个项目就是以“最小完整AI系统”为目标,把下面这些能力逐个从零实现:

  • 一个稳定的文本生成服务,支持流式输出和参数控制。
  • 一个基于向量的语义检索模块,不依赖分布式数据库。
  • 一个能管理多轮对话、自动压缩历史的上下文层。
  • 一个支持工具调用的执行循环,让模型能触发外部函数。
  • 一个离线评测和线上日志记录系统,能定位每次输出变差的原因。

范围听上去不小,但每个模块都刻意保持极简。目标是让读者用一天时间读懂全部代码,用一周时间改造成自己可用的版本。如果做不到这个标准,说明抽象层又加多了,需要继续砍。

2. 项目蓝图和目标拆解

2.1 先锁定边界:不做大而全的平台

项目启动时,我给自己列一条铁律:不做平台,只做“一个人能跑通的工具链”。没有用户管理系统,没有权限体系,没有任务队列,没有插件市场。这些东西等你有真实流量之后再考虑。

从零开始的另一个原则是每多一个组件,必须能回答“它解决了什么问题”。比如要不要上Redis?如果只用来存会话状态,那SQLite加一个缓存字典就够了,前提是你清楚自己的并发量级。如果每天只有几百次请求,微服务拆分也纯属给运维加负担。我当时明确服务形态是单进程多线程的Python服务,只在外面套了一个反向代理做TLS和负载均衡。

这种边界设定有两层价值。第一层是心理上的,它让你敢动手,因为目标足够小;第二层是工程上的,当你真的需要扩容时,你会知道瓶颈具体出现在模型的推理时间、向量检索的召回量,还是外部API的延迟,而不是对着分布式架构图猜。

2.2 验收标准和评估维度

只有目标不算验收,我按照下面几个维度给项目定了可量化的标准:

维度目标值测量方式
端到端延迟单轮问答P95低于3秒请求日志统计
上下文准确率多轮追问后仍能正确引用上一轮信息离线测试集人工标注
检索命中率前5条召回中包含正确答案的比例高于80%检索评测集
工具调用成功率模型输出的函数参数能被完美解析的比例高于95%线上日志统计
资源占用单容器内存低于2GB监控面板

注意这里面没有“准确率”这个指标,因为开放式生成任务的“准确率”很难定义。我更关注它是否可靠、是否可用、是否经济。AI工程的基本功是把模糊的目标翻译成可测的指标,如果做不到这一点,后续所有优化都无从谈起。

3. 技术选型:该省的省,该造的造

3.1 模型与服务:自托管还是API

做AI工程的第一步就是选模型。我的选择标准有三个:能下载权重、有公开基准、社区反馈活跃。考虑到成本和数据安全,我选择把模型放在本地GPU上推理,用vLLM或transformers+pipeline跑起来,具体取决于部署机器显存大小。

如果你没有GPU,就老老实实用云厂商的模型API。这里有一个非常容易犯的错误:过早地优化模型调用层。有人一开始就封装了十几个厂商接口,做了复杂的fallback策略,结果业务还没跑通,先被兼容性问题拖死。正确的姿势是先写一个极简的ChatModel抽象,里面只有两个方法:generate(prompt)和generate_stream(prompt),实现先接一个来源,跑起来再说。

3.2 向量数据库:让检索先简单起来

向量检索是AI工程里被炒作最严重的部分。事实上有80%的早期项目用numpy加暴力检索就够用,因为数据量不超过十万条。暴力检索在这个规模下延迟是毫秒级,而它带来的好处是完全可控、零依赖、方便调试。

我第一版实现方案非常简单:

  1. 用text-embedding接口把文档切成小块并向量化。
  2. 全部向量存成一个float32的numpy矩阵。
  3. 查询时把用户问题向量化,用余弦相似度或内积做top-k检索。

代码只有不到一百行。等数据量真的涨上来,再把底层换成sqlite-vss或者faiss,因为此时你已经清楚接口长什么样、需要索引哪些元数据,迁移成本非常低。先让逻辑正确,再让性能飞的理念在这里非常关键。

3.3 编排:代码优先而非工作流平台

现在很多AI工程强调用可视化工作流平台,把节点拖来拖去。我不反对工具本身,但我反对在项目初期使用它。原因是可视化编排会把逻辑关系藏在图形化界面里,代码评审、版本控制、单元测试全都变得非常别扭。

我的编排层是一段普通的Python代码:一个while循环里跑“生成→解析→调用工具→观察结果→再次生成”,最大轮数设置成8次。每一个步骤都写日志,记录模型输入输出和工具返回值。这种朴素实现有几个实打实的好处:调试时能打印出完整轨迹,问题定位精确到代码行;迭代时可以直接改函数逻辑;压测时能清楚看到循环里最耗时的环节。

3.4 配置与观测:从第一天就开始搭

观测能力的建设决定你能走多快。不是等系统上线后再加,而是在最基础的服务里就内置日志。每个请求分配一个request_id,日志格式统一为JSON,包含时间戳、模型名、请求参数、token用量、延迟、返回状态。

配置管理的核心是敏感信息不进代码库。我在项目根目录放一个.env.example,把需要的环境变量列清楚,然后本地维护真实的.env文件。另一个关键配置是提示词模板:提示词是易变资产,我单独放在prompts/目录下,用文件名做版本标识,不硬编码在Python代码里。这样每次改动提示词都能通过git diff看到变化,避免“为什么线上效果变了”的悬案。

4. 核心模块的从零实现

4.1 推理服务:一个能用的文本生成接口

先说明一下,我这里说的推理服务不是部署大模型本身的工程,而是把模型封装成一个对内部业务友好的接口。无论底层是本地GPU还是云端API,对外暴露的接口保持一致。

这是我的第一版最小实现:

# app/services/chat_model.py import json import time from dataclasses import dataclass, field @dataclass class ChatMessage: role: str content: str @dataclass class ChatRequestBody: messages: list[ChatMessage] temperature: float = 0.7 max_tokens: int = 512 tools: list[dict] = field(default_factory=list) class ChatModel: def __init__(self, base_url, api_key, model_name): self.base_url = base_url self.api_key = api_key self.model_name = model_name def generate(self, body: ChatRequestBody) -> str: payload = self._build_payload(body) start = time.time() # 这里是实际的HTTP调用,不同服务商地址不同 response = self._http_post(payload) self._log_usage(body, response, time.time() - start) return response["choices"][0]["message"]["content"] def _build_payload(self, body): payload = { "model": self.model_name, "messages": [ {"role": m.role, "content": m.content} for m in body.messages ], "temperature": body.temperature, "max_tokens": body.max_tokens, } if body.tools: payload["tools"] = body.tools payload["tool_choice"] = "auto" return payload

注意几个细节。第一,我把temperature和max_tokens放进请求体,而不是在函数里写死,因为线上调参是很频繁的事,硬编码会逼着你频繁改代码。第二,generate返回的是纯字符串,工具调用场景需要额外解析,后面专门处理。第三,我在请求前打印完整请求摘要,调试AI系统时,看不到模型收到的原始内容等于盲人摸象。

4.2 记忆与向量检索:让AI持有上下文

要让模型记住多轮对话,最朴素的做法是把历史消息全部拼进prompt里。但token是成本,而且上下文过长会稀释模型对当前问题的注意力。所以需要一个记忆层来做取舍:短期对话直接携带,长期记忆靠向量检索召回。

我的实现思路是用“消息回收”机制:当会话历史总字数超过阈值,把最早的消息向量化存入记忆库,后续每次提问先从记忆库召回复述给模型。

# app/services/memory.py import numpy as np from typing import List class VectorMemory: def __init__(self): self.vectors = [] self.texts = [] self.metadata = [] def add(self, text: str, embedding: List[float], meta: dict): self.vectors.append(np.array(embedding, dtype=np.float32)) self.texts.append(text) self.metadata.append(meta) def search(self, query_embedding: List[float], top_k: int = 3): if not self.vectors: return [] q = np.array(query_embedding, dtype=np.float32) matrix = np.vstack(self.vectors) # 归一化后内积等价于余弦相似度 q_norm = q / (np.linalg.norm(q) + 1e-9) matrix_norm = matrix / (np.linalg.norm(matrix, axis=1, keepdims=True) + 1e-9) scores = (matrix_norm @ q_norm).tolist() top_indices = np.argsort(scores)[::-1][:top_k].tolist() return [ {"text": self.texts[i], "score": scores[i], "metadata": self.metadata[i]} for i in top_indices ]

有个很关键的工程细节:检索结果必须带上得分和元数据一起返回,而不能只返回文本。因为你要在日志里追踪“这条记忆是为什么被召回的”,得分能帮助判断阈值是否合理,元数据能帮你排查是不是误召回了另一篇文章的内容。

4.3 把RAG串起来

RAG的完整流程不是“检索+拼接提示词”这么简单。一个稳健的RAG管线要处理检索结果的排序、过滤、引用标注、上下文窗口裁剪。

我的实现里有三个关键步骤:

  1. 将用户问题向量化,从VectorMemory和文档库中分别召回候选。
  2. 对候选去重,按得分排序,过滤掉低于阈值的结果。
  3. 选择能挤进max_tokens预算的top结果,按“问题+上下文参考+历史摘要+指令”的顺序拼接提示词。

上下文拼接顺序我试过很多种,最后固定为“系统指令→参考材料→对话历史→问题本身”。原因是模型对提示词末尾内容的注意力通常更强,把问题放最后能让它聚焦当前任务。而参考材料放在历史之前,能防止模型把资料内容误当做自己说过的话。

下面是核心实现示意:

# app/services/rag.py from typing import List from .memory import VectorMemory class RAGEngine: def __init__(self, memory, embedder): self.memory = memory self.embedder = embedder def build_prompt(self, query: str, history: List[dict], system_prompt: str) -> str: query_vec = self.embedder.embed(query) refs = self.memory.search(query_vec, top_k=5) # 过滤低分结果 filtered = [r for r in refs if r["score"] > 0.45] ref_text = "\n\n".join( f"[{i+1}] {r['text']}" for i, r in enumerate(filtered) ) if ref_text: prompt = f"{system_prompt}\n\n参考材料:\n{ref_text}\n\n对话历史:\n{self._format_history(history)}\n\n当前问题:{query}\n回答:" else: prompt = f"{system_prompt}\n\n对话历史:\n{self._format_history(history)}\n\n当前问题:{query}\n回答:" return prompt def _format_history(self, history): return "\n".join(f"{m['role']}: {m['content']}" for m in history[-6:])

这个阈值0.45是不是最优?不一定。它需要在你的业务数据上多次测试后确定。我建议不要把它写死在代码里,而是放进配置项,方便线上快速调整。

5. 评测、迭代、成本平衡

5.1 离线评测集怎么做才有参考价值

没有评测集的AI工程就是闭眼开车。但评测集怎么建,直接决定它的参考价值。我的经验是:不要只用标准公开数据集,一定要加入你自己业务场景的真实案例。

我维护了一个eval_cases.jsonl,每行是一个带标注的测试样本:

  • query:用户真实问题
  • expected_span:期望检索回来的文档片段
  • expected_action:期望的最终回复要点
  • category:用于区分不同业务场景

每次改动提示词或检索逻辑时,用同一批测试集跑一遍,输出比对结果。如果某一类问题效果退化,立刻能定位到是哪个环节导致的。这里有个实操技巧:评测集要持续补充线上出错的case。凡是在线上被用户反馈为错误答案的例子,都要分析原因后加入评测集,防止同类问题回归。

5.2 线上监控:不只是看延迟

延迟和错误率当然要监控,但AI系统最大的隐患是“看似正常实则不对”。只靠HTTP状态码完全不够,必须监控语义层面的指标。我在日志里额外记录了三项内容:

  • #ctx_tokens:每次请求消耗的上下文token数,用于观察上下文是否在膨胀。
  • recalled_documents:实际召回文档的ID和得分,用于评估检索质量。
  • parse_error:模型输出无法解析的报错计数,这往往是工具调用失败的前兆。

其中最容易被忽略的是“召回得分分布”。如果大量请求的召回得分都集中在0.3附近,说明你的embedding模型和业务语义不匹配,需要换模型或调整切块策略。如果得分普遍很高但答案还是不对,那问题可能出在提示词拼接顺序,而不是检索本身。

5.3 成本控制与延迟优化

做AI工程的人都会经历“一开始什么功能都敢接,月底被账单吓一跳”的阶段。成本大头不在调用次数,而在无脑地往上下文里塞内容。我的优化策略有几条:

  1. 对话历史优先截断:保留最近三条消息,更早的消息用摘要替代。摘要本身由模型生成,但只在会话空闲时异步执行。
  2. 让检索结果可被丢弃:很多系统把检索资料无条件拼进提示词,导致模型被无关信息干扰。我引入一个“相关性验证”步骤,让模型先判断参考资料是否与问题有关,无关就不拼。
  3. 批量处理嵌入式任务:如果你有大量离线文档要向量化,不要一条条循环调用接口,一次性打包成批次发送,可以节省大量时间。

延迟方面的记录是:在一次真实项目中,模型推理占了总延迟的75%,检索只占8%,其余是网络和解析。所以优先优化推理路径,比如使用更小的模型、量化模型、开启动态批处理,而不是在向量检索上疯狂加缓存。这个结论可能跟很多人的直觉相反,但数据不会骗人。

6. 踩坑实录:覆盖和遗漏

6.1 常见问题速查表

以下是这个项目从开发到上线之间遇到的典型问题,我把每一个都整理成了“现象→原因→解法”的表格。

现象原因解法
模型回答突然变成英文系统提示词被上下文窗口截断检查提示词拼接位置,确保系统指令在最前且不被截断
工具调用总是参数JSON解析失败模型输出包含markdown代码块包围解析前先剥离```围栏,再尝试json.loads
多轮对话到第五轮就开始答非所问对话历史全部累积,关键信息被稀释加入历史消息的摘要压缩,保留最近消息
检索结果与问题完全无关文档切块太细,单块信息量不足调整切块长度,增加重叠窗口
同一问题两次回复差异巨大temperature设置过高非创意任务将temperature降到0.2以下
服务频繁超时单请求最大token数设置过大,推理排队限流,降级为流式输出

每条背后都是真实的调试经历。比如JSON解析失败这个问题,一开始我很不理解,直到我打印了模型原始输出才发现它把工具参数用三反引号包住了。从那以后,所有工具调用解析都加一步预处理。

6.2 我反复踩的几个坑

第一个坑是过早抽象嵌入函数。最初我把嵌入调用封装成一个类,设计了几种不同的构造方式,结果业务变更是经常发生的事,但抽象层反而成为修改变动的障碍。后来改成普通函数,带两个默认参数,世界清爽了。记住:从零开始的代码不在于设计多优雅,而在于修改时能快速定位。

第二个坑是没有给提示词做版本化。我曾在线上直接改了提示词文本,没有提交到git仓库。结果效果变化无法追踪,回滚也没法做。现在所有提示词都作为独立文件管理,并且每次修改都提交commit记录,这已经是项目的强制流程。

第三个坑是低估了流式输出的复杂度。普通输出很简单,但一旦接上前端流式渲染,就要处理SSE协议、断线重连、半截JSON。如果业务对实时要求不高,完全可以先用非流式接口上线,后续再优化体验。

第四个坑是把检索阈值设得太刚。我曾经把召回阈值固定为0.6,发现很多有效信息被过滤掉。后来把阈值做成动态可配置,并在日志里保留被过滤样本。通过观察被过滤样本的真实相关度,我发现自己当时的阈值设定缺乏数据支撑,完全是拍脑袋。

7. 最后:这种折腾值不值

把这套从零开始的工程做完整之后,我回头看最大的收获不是代码本身,而是建立了一种排查问题的直觉。现在我接手任何一个AI项目,第一反应不再是“用什么框架”,而是“先看数据流,再定技术栈”。这种直觉没法通过阅读框架文档获得,只能靠在最基础的地方亲手写一遍、亲手踩一遍坑来积累。

如果你现在正被各种AI工程平台、低代码工具、大模型框架包围,我的建议是:别急着选型,先给自己一周时间,从零写一个最小闭环。不需要写多好,只需要跑通从输入到输出再到反馈的完整链路。等你做完,再回头看看那些框架,你会发现自己终于能看懂它们的设计初衷,也终于能判断哪些功能对你的业务是必需,哪些只是噪音。

如果让我总结一句,那就是:AI工程的核心不在模型,而在围绕模型建立的那套稳定、可控、可观测的系统。而这个系统,恰恰最应该从零开始盖一遍。

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

Chromium浏览器汉化原理与实操:语言包、回退机制、启动参数

前阵子帮同事收拾一台旧笔记本,系统里躺着一款第三方团队基于 Chromium 内核二次开发的浏览器,界面全是英文,Settings、History、Extensions 在菜单里排成一排,他用了大半年,全凭位置记忆点按钮。这类事我遇上过不止一…

作者头像 李华
网站建设 2026/11/3 6:44:22

开源电子表格组件Univer实战:从接入到嵌入业务系统的完整指南

如果你和我一样,被问过“能不能在我们系统里加一个在线表格”这种需求,应该能理解我见到 univer 之后为什么一直盯着这个项目。Web 端做个“长得像 Excel”的表格界面并不难,难的是要做出真正能用的:公式要自己算、样式要兼容 xls…

作者头像 李华
网站建设 2026/11/3 6:43:29

PySpark+Hadoop构建高考志愿推荐系统实践

1. 项目概述:高考志愿推荐系统的技术实现高考志愿填报是每个考生面临的重要人生抉择,传统的人工填报方式存在信息不对称、效率低下等问题。我们团队基于PySparkHadoop构建的智能推荐系统,通过分布式计算处理千万级历史录取数据,为…

作者头像 李华
网站建设 2026/11/3 6:42:39

IEEE投稿LaTeX工程化实践:模板、编译与源文件打包

1. 先想清楚:为什么IEEE投稿这条路要用LaTeX第一次给IEEE的会议投文章,我用Word排版排到凌晨三点,双栏、公式编号、参考文献顺序,改一处崩三处。后来换成LaTeX,前两个小时确实痛苦,但一旦工程跑通&#xff…

作者头像 李华
网站建设 2026/11/3 6:41:40

TensorFlow 2024生产实践:版本陷阱、核心机制与部署坑全解析

2024年年底,我接了一个推荐系统的性能优化项目。模型仓库里躺着几百个训练好的TensorFlow模型,线上用Serving做推理。当时团队里新来的两个同学私下问我:现在还有必要学TensorFlow吗?我没直接回答,而是让他们先把这堆模…

作者头像 李华
网站建设 2026/11/3 6:41:17

TensorFlow 从安装到实战:2024 年避坑指南与 PyTorch 对比

1. 从零上手 TensorFlow:一个老手的踩坑与实战笔记TensorFlow 这个词,搞机器学习的人基本绕不开。但说实话,我见过太多人卡在第一步——装不上、跑不通、报错看不懂,然后就开始怀疑自己是不是不适合这行。其实真不是,T…

作者头像 李华