ARTICLE DETAIL

资讯详情

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

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

从零构建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 20BM25关键词召回Top 20然后用RRF倒数排名融合算法合并排名。BM25是经典的关键词匹配算法对专有名词、型号、编号等精确匹配场景非常有效而向量召回擅长语义近义和模糊表达。两者融合后召回精度肉眼可见地提升。RRF的实现只是几行代码核心逻辑是对每条文档在两种召回结果里的排名取倒数并求和融合后的分数就是最终排序依据。最后是上下文格式化。这一步直接决定模型输出质量可大部分教程几乎不聊。我的做法是让格式化器输出一种视觉分离度很高的模板让检索段落带上文档来源和层级路径标签。模型读到这些内容能清楚区分哪些是用户问题、哪些是参考资料、哪些是系统指令从而大幅减少答非所问和“自行脑补”的情况。# 简化版混合检索 RRF融合 def hybrid_search(query, top_k5): vector_hits vector_index.search(query, top_k20) bm25_hits bm25_index.search(query, top_k20) 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(), keylambda x: x[1], reverseTrue) 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_rounds6): messages build_messages(user_input) for _ in range(max_rounds): reply gateway.chat(messages, toolstool_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工程不是堆模型是把围绕模型的整条生产链路管好这个认知越早建立你的项目就越不会翻车。
返回列表