news 2026/10/3 3:37:35

从零构建AI工程:RAG管线与Agent闭环的实战拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建AI工程:RAG管线与Agent闭环的实战拆解

很多朋友问过我一个问题:入门AI工程,最有效的路径到底是什么。我的答案一直是同一个——别急着上框架,先亲手把一个极简AI工程从零拼一遍。因为只有自己搭过一遍,你才会真正理解那条RAG链路里每一步为什么存在、Agent工具调用是怎么闭环的、上下文窗口到底是怎么被吃掉的。这个项目(ai-engineering-from-scratch)就是我做这件事时的完整记录和代码沉淀。它不追求跑出什么惊艳的Demo,而是把AI工程的核心骨架逐层拆开,让你看到、摸到、改到那些在高层框架里被封装掉的关键细节。这篇梳理会覆盖模块设计、核心代码逻辑、踩坑实录,以及我认为后续最值得扩展的方向。

1. 项目要解决的真问题:把AI工程从“魔法”变回“工程”

1.1 为什么我决定从零写一套AI工程系统

前两年AI应用爆发式增长,市面上主流的开发方式几乎都变成了“调框架、托管道”:LangChain搭链条、LlamaIndex接索引、AutoGen配Agent,几行代码把能力串起来。这种方式在快速验证阶段确实好使,但非常容易让工程师陷入一个被动的处境——线上出问题时,框架内部执行的究竟是什么,像个黑盒一样看不透。

我印象很深的一次线上事故:某个知识库问答的召回率直线下降,配置没改、索引没动,可就是找不回正确的上下文。用LangChain封装的检索链去排查,日志只能看到“Retriever returned 4 docs”,但到底召回了哪几段、为什么是这几段、向量检索在哪个环节丢失了语义,根本无从下手。最后只能绕开管道逐段打点,才定位到问题其实出在Embedding批次处理时文档长度截断策略变了。

那次之后我就下定决心,用完全自主的代码从底层构建一套最小可用的AI工程系统。目标不是“取代框架”,而是让自己能够完全掌控数据流和控制流。框架给我的是封装与妥协,而我想要的是理解与透明。当你能亲手写一个向量检索、手动实现一次工具调用闭环的时候,任何上层框架对你来说都只是一层薄薄的语法糖,出了问题可以直接穿透到原理层。

1.2 项目边界:做什么,不做什么

做这个项目之前,我的第一个动作其实是划定边界。没有边界的技术项目会像无底洞,今天想做多模态,明天想加图谱,后天又想去微调模型,最后什么都做不成。

我的边界是:不碰模型训练,不碰底层算子优化,不追求完整生产级部署方案。专注做AI应用工程中三个最核心的横向能力——RAG(检索增强生成)管线、Agent工具调度、上下文生命周期管理,外加一套简单的评估与可观测体系。

为什么不碰训练?因为那不是应用工程师的主战场。AI工程的核心难点从来不在“怎么训出一个模型”,而在“怎么把已有模型的能力稳定地、可控地、低成本地编排进业务流程”。如果你能清晰地把文档切分、向量召回、提示词组装、工具调用、上下文管理、结果评估这六件事讲清楚并落地,就已经解决了生产环境中80%以上AI应用的真实痛点。

2. 系统骨架设计:四个核心模块与依赖关系

2.1 模块拆解:RAG管线、Agent调度、上下文管理、模型网关

动手写代码之前,我在纸上把这套系统分成了四个模块,后来实际开发中几乎没有调整过这个划分,说明最初的设计是站得住脚的。

第一个模块是RAG管线。它负责把外部知识转换成模型可以检索并使用的上下文。再往下拆,管线里包含文档加载器、文本切分器、Embedding调用器、向量索引和检索器,以及最容易被忽略的“上下文格式化器”。很多人以为RAG就是“检索+拼接提示词”,但格式化这一步决定了拼接结果是否真的适配模型指令,稍后我会单独展开。

第二个模块是Agent调度。它解决的是“模型决定调用哪个工具”的问题。一个合格的Agent闭环,不只是让模型输出一段JSON,而是要做到:定义工具、绑定工具、解析模型意图、执行工具、把执行结果回填给模型、模型继续推理、直到完成任务或主动终止。这个循环里每一步都有坑。

第三个模块是上下文管理,或者说记忆系统。大模型上下文窗口有限,而真实业务场景里的对话轮次、检索结果、工具执行结果非常多。上下文管理要解决三件事:什么东西该保留,什么东西该丢弃,窗口快满时怎么压缩。这个模块设计得不好,系统跑不了几轮就会“失忆”或者被无关信息淹没。

第四个模块是模型网关。它负责屏蔽底层模型提供方的差异,统一提供流式输出、重试、限流、费用统计、超时控制等能力。实际项目里,团队用的模型一定不会只有一个,可能是不同厂商的开源或闭源模型,网关上手之后,上层模块永远不需要关心模型来源,只需要面对一个统一接口。

2.2 依赖方向与数据流向

四个模块之间有一条明确的依赖链:模型网关在最底层,所有模块都要调用它;RAG管线和Agent调度横向并列,都依赖网关;上下文管理像一个横切的皮层,覆盖在RAG和Agent之上,任何进入模型的文本都要经过上下文管理器;评估系统则附着在外围,单独抓取各个环节的数据样本。

数据流方向上,用户请求进来之后,先经过一个路由器。路由器判断这个请求是“直接回答型”还是“需要检索知识型”还是“需要调用外部工具型”。属于检索型的请求走RAG管线,产出候选段落后格式化进上下文;需要执行操作的请求则走Agent调度循环,模型可能反复调用工具直到拿足信息。整个过程中,每次与模型的交互请求,都要先把历史摘要、本轮输入、检索片段、工具结果拼接到既定模板里。

我特别想强调一下路由判断的分寸。新手容易犯的错是让路由规则过于简单——“只要问题里有‘是什么’就检索,有‘帮我’就调用工具”。这在实际场景里误判率很高。我在系统里是用“意图+实体”双信号判断的:既要有提问意图类型,也要能抽取到需要检索或操作的业务实体。两套信号同时命中才路由,宁可走保底回答,也不乱接管道。

2.3 技术选型逻辑:框架能不用就不用,但数据库和工具不重复造轮子

项目叫“from scratch”,但我不做“从零造一切”这种极端原教旨主义。如果什么东西都自己造,那就成了重复发明轮子,项目的核心目标也会被淹没掉。

所以我的技术选型原则是:所有“业务逻辑编排”全部手写,所有“重型底层基础设施”用成熟方案。业务逻辑编排包括流程控制、工具注册、上下文拼装、评估指标计算——这些是项目要教学的核心,必须自己实现。而底层基础设施,比如向量存储,我选择直接用Chroma这类嵌入式方案;模型调用统一走OpenAI兼容接口,这套接口现在已经成了事实标准,没必要自己造HTTP客户端。

选型时还要考虑可替换性。系统里通过环境变量控制模型Endpoint和Key,这样开发时可以用开源小模型跑通流程,生产时无缝切到商业模型。同一个代码库,上行下效,周围的同事拿过去改几行配置就能跑,不用改任何业务代码。这种“配置与逻辑分离”的做法,我建议所有类似项目都要坚持,它会在后面调试的时候给你省下大量时间。

3. 核心环节实操:手把手实现RAG与Agent闭环

3.1 环节一:RAG完整管线

RAG管线第一步是文本切分,这一步决定了检索质量的上下限。我在这个项目里尝试过三种策略:固定长度切分、按段落结构切分、父子块切分。固定长度最简单,但极易切断句子语义;按段落结构切分适合Markdown或HTML这类带标题层级的内容;父子块切分则是把小块用于语义匹配、大块用于上下文内容,实战效果最稳。

我最终的实现选择了“结构优先+父子块”的组合方案。以Markdown文档为例,先按标题层级把文档拆成语义独立的章节,再对每个章节做滑动窗口切分,每个父块保留完整上下文,每个子块作为检索单元。这样子块命中之后,可以向上回溯父块放进模型上下文,模型能看到的上下文完整且语义连贯。

切分之后进入Embedding阶段。这里有个很实际的问题:批量Embedding时文档越长,语义越会被稀释。我在系统里做了文本截断配置,同时维护了一张内容哈希表作为Embedding缓存,相同内容不需要重复计算向量。这一点在反复调试的时候效率提升非常明显,省下来的API费用也很可观。

向量索引我用的是Chroma的默认HNSW实现。HNSW是一种近似最近邻算法,意思是不用全量比较,而是通过多层跳表结构的图来快速逼近最相似的向量。小规模项目里它完全够用,性能也远超暴力检索。但必须注意,HNSW的召回质量受参数影响,比如M值(每个节点的最大连接数)和efConstruction(建图时的动态列表大小),我在项目里都显式设置固定参数,避免默认值变化导致行为不可预期。

检索器实现上,我做了混合召回。向量检索召回Top 20,BM25关键词召回Top 20,然后用RRF(倒数排名融合)算法合并排名。BM25是经典的关键词匹配算法,对专有名词、型号、编号等精确匹配场景非常有效;而向量召回擅长语义近义和模糊表达。两者融合后,召回精度肉眼可见地提升。RRF的实现只是几行代码,核心逻辑是对每条文档在两种召回结果里的排名取倒数并求和,融合后的分数就是最终排序依据。

最后是上下文格式化。这一步直接决定模型输出质量,可大部分教程几乎不聊。我的做法是:让格式化器输出一种视觉分离度很高的模板,让检索段落带上文档来源和层级路径标签。模型读到这些内容,能清楚区分哪些是用户问题、哪些是参考资料、哪些是系统指令,从而大幅减少答非所问和“自行脑补”的情况。

# 简化版混合检索 + RRF融合 def hybrid_search(query, top_k=5): vector_hits = vector_index.search(query, top_k=20) bm25_hits = bm25_index.search(query, top_k=20) fused = {} for rank, (doc_id, _) in enumerate(vector_hits): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) for rank, (doc_id, _) in enumerate(bm25_hits): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) ranked = sorted(fused.items(), key=lambda x: x[1], reverse=True) return [doc_id for doc_id, _ in ranked[:top_k]]

这段代码里60是常见平滑常数,作用是压低单路召回的排名权重差异,避免某个极端高分直接碾压另一路的有效结果。实际项目里这个常数可以按召回量级微调,但60作为经验起步值很稳定。

3.2 环节二:Agent工具调用闭环

Agent模块是我花时间最多的地方。一个工具调用闭环最少包含六个环节:工具注册、工具说明生成、模型意图解析、参数校验、执行与回填、终止判断。

工具注册我使用装饰器模式,函数加上一个@register_tool装饰器,系统通过内省机制自动提取函数的名称、文档字符串、参数签名。模型看到的工具说明,就是这些元信息的JSON Schema格式。这个设计的优点是你永远只需要维护一份工具定义,不会出现“接口文档和实际函数签名不一致”这类低级但致命的问题。

模型意图解析环节最容易出幺蛾子。不同模型输出的工具调用格式五花八门,有的给完整JSON,有的给Markdown代码块,有的夹带额外解释文本。我在模型网关里做了一个标准化解析层:先尝试标准JSON解析,失败则用正则抽取代码块,再失败就把内容交给一个小模型做“格式修正”。经历过线上模型API升级导致输出格式突变之后,我对这个环节的敬畏心特别重。

参数校验必须做,而且要严格做。模型生成的参数偶尔会有幻觉情况,比如把日期格式撰写错误,或者传了一个完全不在枚举范围内的选项。我的校验层会在执行前做类型强转、枚举检查、必填项检查,不合法就直接返回一条结构化错误给模型,让模型感知错误后重新生成调用。这比闷头执行然后报异常要优雅得多,因为模型可以自我纠正。

工具执行回填是Agent闭环里最影响“智能感”的一环。工具执行结果回填给模型时,不能直接把原始返回值堆进上下文。我做了两层处理:第一层是裁剪,只保留结果中与调用目标相关的字段;第二层是格式化,把返回值改成简明的文本摘要,并附带上执行状态。比如查订单接口返回2000字的JSON对象,裁剪后只给模型一段“订单状态:已发货,物流公司:顺丰,单号:SF123456”这样的结果。这样既节省上下文空间,又降低模型被无关字段干扰的概率。

终止判断我采用的是“双保险”:模型显式输出结束标志时视为自然终止;同时系统侧设置最大工具调用轮次上限。设定上限这个习惯尤其重要——模型在复杂任务里有时会陷入死循环,反复调用同一个工具试图获取永远得不到的信息,没有上限的Agent会一直空转到费用爆表。我的经验值是普通任务6轮足够,复杂任务可以放宽到10轮,超过就直接终止并返回“任务超时,请简化请求”。

# 工具调用闭环核心循环(伪代码结构) def run_agent(user_input, max_rounds=6): messages = build_messages(user_input) for _ in range(max_rounds): reply = gateway.chat(messages, tools=tool_schemas) if reply.is_finish(): return reply.content action = parse_tool_call(reply) validated = validate_params(action) result = execute_tool(validated) messages.append(tool_result_message(result)) return "任务超时,建议简化请求"

这段循环里最值得品味的是messages的累积方式。每一轮工具结果都作为一条新消息追加进对话历史,上下文管理器会持续监控消息总量,一旦逼近窗口上限就触发压缩策略,而压缩策略正是接下来要说的模块。

3.3 环节三:上下文工程与记忆管理

大模型的上下文窗口就像办公室里的白板,空间有限。你要决定哪些内容值得写在白板上,哪些该擦掉。我的上下文管理器实现了一套分层记忆策略:工作记忆、摘要记忆、长期记忆。

工作记忆保存当前任务相关的原始消息。摘要记忆是历史对话的迭代式压缩摘要,每隔几轮对话就把最早的部分浓缩成几十字的要点。长期记忆则落在外部存储里,按用户的会话维度持久化重要信息,比如用户偏好、前置决策、已完成步骤。

压缩策略我用的是“摘要替换法”:当消息总量超过阈值时,选取最早的两轮对话,合并做一次摘要生成,把原始消息替换成摘要消息。这样做的好处是白板永远不会被无限制地占用,坏处是摘要也会累加噪声。为了控制噪声,我规定摘要消息在经历一定轮次之后自身也会被压缩,形成多级摘要,类似于会议记录的“今日纪要→周报→月报”的层级归档。

还有一个很实用的小技巧:把检索到的文档片段都放到一个独立的上下文区域内,区域之间用标识符分隔。模型对这种结构化输入的响应质量,远高于把所有内容揉成一团的消息。实测同一批检索片段,格式化前后的答案准确率差距非常明显,这说明上下文工程不是简单的“拼字符串”,而是有信息架构设计的。

3.4 环节四:评估体系与可观测性

很多项目死在“感觉效果不错但说不清好在哪里”上,AI应用尤甚。没有评估体系,你改一版提示词都不知道是变好了还是变坏了。我在项目里建了一个迷你评估集,包含三类样本:标准问答对、模糊表述对、需要多轮工具调用的任务。每轮改动跑一遍评估集,记录每个样本是否成功以及耗时和费用。

客观指标上,我追踪三个数字:准确率(人工标注或LLM评判)、上下文命中率(检索片段是否被最终答案真正引用)、浪费Token比例(上下文里未被使用的片段占总量的比例)。第三个指标特别能反映检索和上下文管理质量,浪费Token比例高于40%就说明检索精度不够或者上下文塞了太多冗余内容。

可观测性方面,我设计了一个可视化日志。每次请求可以产出一条完整的时间线,包含:路由决策、检索命中文档列表、混合排序分数、最终放入上下文的片段编号、工具调用参数、模型回复耗时、Token消耗明细。这套时间线在排障时简直是救命稻草,因为AI应用的输入输出是动态的,做不了传统软件那样的固定日志断言,只有完整链路记录才能让你复现和分析问题。

4. 高频问题与排查实录

4.1 常见问题速查表

AI工程系统的排查思路和传统后端不太一样,很多问题具备概率性、语义性和上下文依赖性,没法靠“看报错”一招解决。这里我把项目过程中遇到的高频问题整理成一张速查表,每一条都是实际踩过的坑。

问题现象根因解决方法
答案总出现幻觉上下文里检索片段缺失关键信息检查切分策略是否割裂实体;提升Top-K召回数量
召回的文档与问题无关Embedding精度不足切换更高维Embedding模型;检查文本截断策略
模型频繁重复调用同一工具工具结果回填信息不足检查结果格式化的裁剪逻辑;附带执行状态和错误信息
多轮对话之后模型“失忆”摘要压缩过度调整摘要触发阈值;关键事实放入长期记忆
单次请求Token费用过高上下文冗余严重启用浪费Token比例监控;收紧检索Top-K
某个工具调用间歇性失败模型输出JSON格式不稳定标准化解析层加强正则抽取;增加格式修正模型兜底
混用多个模型时输出不一致各模型遵循指令能力不同网关层做系统性提示词归一化

4.2 三个让我印象深刻的调试案例

第一个案例是检索时灵时不灵的诡异问题。开始时我以为是Embedding模型的问题,换了好几个模型都没用。后来把用户输入做了分词统计才发现,用户习惯性地把型号名“AX-3000”连在一起写,而文档里存的是“AX 3000”。向量检索对这种字符级差异不敏感,但关键词检索接口对空格极其敏感。最后修复方式是在切分阶段把这类连写词做了归一化处理,统一去掉空格和横线,召回率立刻回升。这个案例告诉我,工程问题有时就藏在你觉得“应该没问题”的文本预处理里。

第二个案例是工具的循环调用。Agent为了查询“本周某地区销售排名”,反复调用订单统计工具了十几次,每次参数都一样。日志分析发现,工具返回的JSON里数字是字符串类型,字段名也嵌套了三层。模型在第三层努力寻找“排名”字段却找不到,于是不断重试。修复方式是让工具结果格式化器统一输出平铺的、类型明确的摘要文本,而不是把原始JSON直接丢回给模型。从此之后循环调用问题几乎绝迹。

第三个案例很有意思,是模型上下文被“消息类型标注”污染。我起初为了让模型识别历史对话,在每条消息前面加了[HUMAN]和[AI]前缀。但某个版本的模型微调数据里也用了类似标记,导致模型把前缀当成了某种结构化指令,输出格式偶尔会变成它见过的“内部风格”。最后我把前缀从消息文本里移出,改成系统层文件级别的角色字段,一切恢复正常。这个教训是:提示词里的任何符号都可能是模型训练时见过的特殊标记,不要凭空发明奇怪的标记组合。

5. 项目后续的扩展方向与我的个人体会

5.1 可以往下深挖的两个方向

完成核心闭环之后,我陆续把项目延伸到了两个方向上。

第一个方向是评测集持续运营。我从线上真实请求里挑选了一些高价值样本回流进化语料库,再对模型输出做自动打标。这套机制跑起来之后,系统的可回归性越来越强。以后每换一次模型或改一次提示词,都能快速看到对整体效果的影响。我强烈建议任何认真做AI工程的人都别跳过这一步,没有评测集的AI项目,本质上就是在裸奔。

第二个方向是把RAG从“检索静态文档”升级为“检索动态业务数据”。静态文档切分一次就完事,但业务数据会持续更新,还需要考虑权限过滤。我在最新版本里加了数据源的增量同步插件,以及基于用户身份的文档级权限过滤。权限过滤这个点很容易被忽视,可生产环境里一旦越权检索,后果可能很严重。

5.2 我的个人体会

这套从零构建的AI工程系统做下来,对我最大的改变不是技术上能徒手写出这些模块,而是看待AI应用的方式变了。用框架时,我是在“使用”一个系统;从零构建后,我是在“运营”一套系统。这个转变带来的直接结果是——线上出问题的时候,我不再凭感觉拍脑袋猜原因,而是顺着自己设计的链路逐层查,基本都能快速定位到问题根因。

最后想说一个建议:如果你也想复现这个项目,别急着把代码拉全就跑,先自己闭卷回答三个问题——你要处理的数据长什么样?你要支持哪几类用户请求?模型在什么情况下可以承认自己不知道?想清楚这三件事,再动手写代码也不迟。AI工程不是堆模型,是把围绕模型的整条生产链路管好,这个认知越早建立,你的项目就越不会翻车。

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

MySQL系统复习笔记:从索引原理到性能调优实战

1. 复习MySQL,到底在复习什么前两天整理电脑里的学习笔记,翻到去年年初给自己定的目标清单,第一条写着“系统复习MySQL”。当时还画了好几个大箭头,从安装部署指向索引优化,从事务隔离指向锁机制,一副要啃下…

作者头像 李华
网站建设 2026/10/3 3:37:15

华为S5700三层交换机VLAN配置避坑指南:VLANIF与Trunk实战

先说个真实场景。上周帮一家公司排查网络故障,客户反映财务部和研发部明明接在同一个机房的交换机上,两边电脑就是互相ping不通。我登上华为S5700一看,VLAN 10和VLAN 20都建了,端口也都划对了,但SVI接口(就…

作者头像 李华
网站建设 2026/10/3 3:36:36

注册表.reg解析库RegFileParser:.NET实现与状态机设计

如果你平时和 Windows 注册表打交道比较多,一定会遇到这种场景:手动导出一份 .reg 准备排查某个软件装没装干净,结果文件几百行,用 regedit 翻到眼花;或者你拿到一台机器的注册表导出文件,但手边只有 Linux…

作者头像 李华
网站建设 2026/10/3 3:36:34

风光储互补调度实战:Python建模混合储能优化运行

做了几年新能源调度方面的研究,这次把风电、光伏、电池储能和废弃矿井小型抽水蓄能放到同一个优化框架里,用 Python 完整跑了一遍互补调度运行的程序。实际做下来最大的感受是:这问题表面上是个“算法题”,骨子里却是个“工程建模…

作者头像 李华
网站建设 2026/10/3 3:35:45

欧姆龙FINS命令进阶:报文结构拆解与调试实战

接手欧姆龙HostLink通讯协议这个系列的时候,我本来打算一篇写完就收工,结果越写越发现坑太多,尤其在FINS命令这一块。很多朋友留言说,前面几篇把串口帧、ASCII命令讲明白了,但一碰FINS就发怵,什么FINS/TCP、…

作者头像 李华
网站建设 2026/10/3 3:35:38

STM32F407+LAN8720移植LwIP全攻略:从CubeMX配置到避坑指南

先把结论放前面:STM32F4 系列跑 FreeRTOS 再挂一个 LwIP 协议栈,搭配 LAN8720 这颗百兆 PHY,是很多物联网设备、工业采集器、远程升级模块的经典组合。这套组合能跑通,但绝不轻松。我得说,哪怕你已经在别的平台上玩过 …

作者头像 李华