从零搭建AI工程:多Agent协作与RAG检索增强实践
1. 为什么我建议你从零开始搭一套AI工程这两年AI相关的项目满天飞但说实话真正能落地、能长期跑稳定、出了bug你还能快速定位的项目远比想象中少。我见过太多团队拿一个大模型API一接写两段Prompt就上线了结果上线第一个月还行第二个月模型一更新回答风格变了业务流程全乱。问题出在哪出在大家把AI工程的“工程”两个字给丢掉了。“ai-engineering-from-scratch”这个项目的核心思路就是不依赖任何现成的Agent框架、不迷信某一个模型厂商的生态从最底层的工作流设计、Prompt工程、模型选型、部署运维一路自己搭起来。听起来好像很折腾但做完之后你会发现你对整个系统每个环节都有了完全的控制力。这篇文章就是把我从零搭这套工程踩过的坑、验证过的方案、以及最后的架构完整记录下来适合那些不想只做“调包侠”、想真正理解AI系统内部运作的开发者参考。先说清楚这套东西能做什么。它是一个完整的AI应用工程骨架包含多Agent协作调用链、Prompt的版本化管理、模型层的抽象封装、RAG检索增强模块、以及部署端的并发与缓存设计。你可以在它的基础上快速长出自己业务需要的AI应用无论是做智能客服、文档问答、还是复杂的分析报告生成都能直接复用这套底座。2. 整体架构设计先画好流程图再写第一行代码2.1 不要把Agent框架当作起点很多人一上来就用LangChain这类框架我不是说框架不好而是如果连最基础的调用链逻辑都没吃透框架反而会变成一个黑盒。项目里我自己定义了一套极简的Agent调度器核心思想就是一个消息路由表。系统接收用户请求后先经过意图识别再根据路由表决定调用哪个Agent、按什么顺序调用、是否需要多Agent协作。举个例子我做一个行业研究报告生成系统时需求是用户输入一个行业关键词系统自动输出包含市场数据、竞争格局、技术趋势三部分的完整报告。如果用串行单Agent方案一个模型要同时处理三块内容Prompt会变得非常长上下文一长模型注意力就会分散输出质量明显下降。所以我把任务拆成三路并行Agent分别负责数据检索、结构分析和内容撰写最后用一个汇总Agent做整合。这个拆法背后有个关键原则单一职责。每个Agent只做一件自己最擅长的事Prompt可以写得短而精准模型输出质量反而更高。这其实和软件工程里的单一职责原则是一模一样的思路只是很多人做AI应用时容易忘了这茬。2.2 工作流的分层设计我在项目里把整个工程划分为四层每一层只依赖下层不跨层调用接入层负责用户请求的接收、会话管理、权限校验调度层负责任务拆解、Agent路由、多Agent协作编排执行层包含具体Agent的实现每个Agent内部有独立的Prompt模板和工具调用能力服务层模型API封装、向量数据库操作、缓存、日志这个分层让我尝到了很大的甜头。有一次我发现报告内容质量下降排查的时候直接定位到执行层的某个Agent Prompt被意外修改了十分钟就解决了。如果是那种全部逻辑揉在一起的代码光找问题就得半小时。2.3 状态管理AI工程里最容易翻车的点很多AI应用跑着跑着就失忆了原因就是没有做好状态管理。我在这个项目里设计了一个统一的任务状态对象所有Agent在执行过程中读取和写入的都是这个对象。它包含四个字段用户原始意图、中间结果集、当前执行上下文、历史决策记录。这个状态对象的价值在多Agent协作时体现得最明显。比如C Agent需要用到A Agent的中间结果直接从状态对象里取就行不用通过消息队列转来转去也不需要数据库落盘内存态就能解决大部分场景。而且出了问题把这个状态对象打出来整个执行链条一目了然比看什么都好排查。3. Prompt Engineering的工程化实践3.1 把Prompt当成代码来管理Prompt是整个AI工程里最容易被忽视、但影响最大的部分。我在这个项目里专门建了一个prompts目录每个Agent的Prompt都是一个独立的模板文件用Jinja2语法做变量渲染。这样做的直接好处是Prompt可以走代码评审、可以版本回滚、可以A/B测试。我见过很多团队把Prompt直接写在Python字符串里改一次要重新发版测试还不方便。模板文件化之后我可以在线上动态加载新Prompt先跑小流量对比效果再全量切换整个流程跟发布代码一样规范。3.2 我总结的一套Prompt结构模板经过几十次迭代我总结了一套固定六段式的Prompt结构对大多数任务场景都适用ROLE你是一个资深的行业分析师/ROLE OBJECTIVE你的任务是根据给定的行业关键词输出结构化的行业研究报告/OBJECTIVE CONTEXT这是当前已知的信息{{ context }}/CONTEXT FORMAT请严格按照以下Markdown格式输出.../FORMAT CONSTRAINTS禁止编造数据所有数据必须标注来源报告控制在800字以内/CONSTRAINTS EXAMPLE以下是一个示例.../EXAMPLE这六段缺一不可但最常见的坑是大家容易省略后四段。少了CONTEXT模型就只能靠自己的知识盲猜少了FORMAT输出的结构就是随机的少了CONSTRAINTS它就会开始一本正经地编数据。尤其是EXAMPLE这段我实测下来对输出质量的提升是最明显的一个例子有时候比一百句描述都管用。3.3 Prompt版本管理与回归测试把Prompt当成代码管自然就要有测试。我在项目里建了一个prompt_regression目录里面存了几十组输入-期望输出的测试样例。每次改Prompt先跑一遍回归测试看看有没有把已经有稳定效果的能力改坏。这里有个经验技巧回归测试的判定不要只看关键词匹配要看语义相似度。我用一个小的embedding模型把期望输出和实际输出都转成向量算余弦相似度大于0.85就算通过。这样既避免了写一堆正则匹配的麻烦又比纯人工review高效得多。4. 模型层的封装与工具调用设计4.1 别把模型写死在代码里模型是这个领域迭代最快的部分今天你用的模型可能三个月后就过时了。所以我在项目里做了模型层的统一抽象上层Agent只面对一个名为LLMClient的接口它包含三个方法generate(prompt)、chat(messages)、call_tool(tool_name, args)。底层具体用哪个厂商的API通过配置文件切换。我实际测试了多套模型的切换成本从国内的开源模型到国外的闭源API只要接口封装统一切换就是改一个配置项的事。这也让我在模型选型上有很大的灵活性需要跑本地私有化部署就切到本地模型需要更强的逻辑推理就切到更大的云端模型完全不影响上层逻辑。4.2 工具调用的工程化实现让Agent具备调用工具的能力是当前AI应用的主流形态。我在项目里实现了一套基于函数注册的工具调用机制核心代码其实很简洁class ToolRegistry: def __init__(self): self._tools {} def register(self, name, func, description, parameters_schema): self._tools[name] { func: func, description: description, parameters_schema: parameters_schema } def call(self, name, parameters): if name not in self._tools: raise ValueError(fTool {name} not found) return self._tools[name][func](**parameters) registry ToolRegistry() registry.register( namesearch_docs, description在知识库中检索与关键词相关的文档片段, parameters_schema{ type: object, properties: { query: {type: string, description: 检索关键词}, top_k: {type: integer, description: 返回结果数, default: 5} }, required: [query] } ) def search_docs(query: str, top_k: int 5): return vector_store.search(query, top_ktop_k)这里有个关键细节工具的描述信息一定要写清楚每个参数的含义。模型是靠函数名和参数描述来决定要不要调用这个工具、以及怎么填参数的如果你的描述含糊不清它就会随机发挥导致调用链路不稳定。4.3 多Agent协作的实现思路项目里最复杂的一个场景是多Agent协作。我实现了一个协作调度器核心逻辑是一个有限状态机。每个Agent在执行完后会返回一个状态DONE、NEED_MORE_INFO、FAILED。调度器根据状态决定下一步如果DONE就进入下一个阶段如果NEED_MORE_INFO则回调上一个Agent补充信息如果FAILED则走重试或者降级逻辑。这个设计的巧妙之处在于它把异常处理也纳入了调度流程。比如有一次检索Agent因为外部API超时返回了FAILED调度器自动切换到了降级策略用模型自身知识生成内容用户感知不到任何异常。5. RAG检索增强让AI真正基于你的知识库回答问题5.1 我踩过的RAG三连坑很多AI应用都需要基于私有知识库回答问题RAG是目前的主流方案。但这个项目里我踩了三个印象特别深的坑写出来大家避一避第一个坑是分块chunk策略选错。一开始我按固定500字分块结果发现很多知识点被切断了上下文。比如一个技术方案描述的是如果A条件不满足则B方案作为备选结果被切成了两块检索的时候只搜到了后半截AI就开始一本正经地胡说八道。后面我改成了按语义段落分块利用标题、换行、列表等结构化信息切分问题才彻底解决。第二个坑是Embedding模型和查询方式不匹配。我一开始用的检索模式和查询都是稠密向量检索结果在处理那些专业术语特别多的查询时效果很差。后面加了BM25做稀疏检索两种结果用RRFReciprocal Rank Fusion算法融合检索准确率直接提升了一个档次。第三个坑是检索结果不做重排序。向量检索Top-5的结果里真正相关的可能只有两三个直接把五个结果全塞给AI反而会引入大量噪声。后面我加了一层重排序模型从Top-20候选里精排出Top-5效果提升非常明显。5.2 混合检索的简易实现我在项目里用的是Elastic search和Faiss配合的方式。Elastic search负责BM25全文检索Faiss负责稠密向量检索两个结果集合并后用RRF算法融合排序。这套方案的代码实现并不复杂核心逻辑是def hybrid_search(query, top_k5): bm25_results es_search(query, size20) vector_results vector_search(query, top_k20) # RRF融合 fused_scores {} for rank, doc in enumerate(bm25_results): fused_scores[doc.id] fused_scores.get(doc.id, 0) 1 / (60 rank) for rank, doc in enumerate(vector_results): fused_scores[doc.id] fused_scores.get(doc.id, 0) 1 / (60 rank) sorted_results sorted(fused_scores.items(), keylambda x: x[1], reverseTrue) return [get_doc_by_id(doc_id) for doc_id, _ in sorted_results[:top_k]]RRF算法里的常数60是经验值不用过度调参一般效果都稳定。这个方案的优点是简单、不用训练模型、融合效果好特别适合要在企业里快速落地的场景。5.3 上下文注入的窗口管理检索到了结果怎么塞给AI也是一门学问。我把知识库内容注入到Prompt时采用了三级结构先放用户问题再放检索到的参考文档最后放相关历史对话。同时我对单次检索结果的长度做了限制最多不超过2000字防止上下文窗口被撑爆。这里有个重要原则宁可给模型少而精的信息也不要给它一堆可能相关的噪音。很多RAG应用效果不好不是检索太弱而是给模型的信息太多了导致模型不知道该信哪个。6. 模型部署与服务化从开发到上线的最后一公里6.1 本地推理还是云端API我的选型逻辑项目到了部署阶段我面临一个关键选择是直接用云端模型API还是自己部署开源模型。我的结论是先想清楚延迟和成本两个指标再选。如果业务允许300毫秒以上的响应时间直接用云端API没有任何问题成本可控维护成本几乎为零。如果需要实时响应比如交互式对话场景本地部署开源小模型是一个可行方案。我在项目里对一个中文开源模型做了量化部署用4bit量化后模型体积从14GB压缩到4GB左右在普通消费级显卡上能跑到每秒处理几百个token的速度完全可以支撑低频业务。6.2 并发控制与缓存设计AI应用部署后最容易崩的原因就是并发突增。我在项目里的做法是给每个模型API封装一个基于信号量的并发控制器限制同时进行的API请求数。超出的请求进入队列等待而不是直接打崩上游。缓存的设计也很有讲究。项目的语义缓存模块把用户问题转成向量在本地Redis里查找语义相似的历史问题和答案相似度超过阈值直接返回缓存结果。这个模块上线后整体API调用成本直接降了三成因为大量用户的相似问题根本不需要重新走模型推理。6.3 评测体系没有评测就没有迭代方向最后一定要说评测。我花了不少时间搭了一个离线评测集里面汇总了几百条典型用户问题和期望回答。每次对Agent、Prompt、模型做调整后就在这个评测集上跑一遍计算回答准确率、完整度、格式合规率三个指标。有了这套评测体系我才能自信地说这次优化是有效的而不是靠感觉。我见过很多团队改Prompt全靠感觉这种做法的结果就是在改好-改坏-改回来的循环里打转。评测集不够完美但它是让你从玄学调优走向工程化调优的最关键一步。7. 常见问题与排查技巧实录7.1 模型突然失忆了怎么办有一次系统跑得好好的突然所有对话都变成记忆空白排查之后发现问题出在会话管理模块。我的会话列表是用内存对象缓存的服务重启后全部清空。后来改成了Redis持久化会话状态问题彻底解决。这个排查过程给我的启发是AI类的bug优先排查数据状态其次排查模型调用逻辑。状态丢了、上下文没了模型表现就会瞬间退化但它本身没有坏。7.2 Agent陷入死循环怎么定位多Agent协作最容易出现的故障是循环调用比如Agent A调Agent BB发现信息不足又回调A两个Agent你来我往几十轮把调用费用烧上去。我在调度器里加了一个最大执行次数限制默认单次任务最多执行20个Agent动作超了就强制终止并返回当前已有结果。同时在每个动作执行时都会记录时间戳和原因一旦发生循环直接查看链路日志就能发现是哪两个Agent在互相踢皮球。7.3 Prompt改了效果反而变差这个问题我碰到过不止一次。改完Prompt感觉逻辑更清晰了但实测效果变差了。后来我定位到原因新Prompt里的约束条件太多太死板模型在约束范围内找不到答案时就开始胡说。解决办法是删掉一部分指导性过程描述只保留关于输出格式和内容的约束给模型留出足够的推理空间。经验小结Prompt不是越长越好约束不是越多越好。好的Prompt像一张清晰的轮廓图和一把尺子框架画好、边界画清楚具体内容让模型自己发挥。8. 项目复盘这套工程底座后续怎么扩展从零搭完这套AI工程我最深的体会就是AI工程的核心竞争力不在某个模型而在模型之外的那些工程细节。工作流怎么拆、状态怎么管、上下文怎么组织、异常怎么兜底这些才是决定一个AI应用能不能稳定运行的关键。项目当前的状态已经覆盖了从Prompt管理到模型部署的完整链路我个人后面还会在三个方向上继续扩展。一是引入流式输出的全链路支持让对话体验更接近原生聊天的效果。二是把评测集做成自动化闭环每次Prompt变更推送后自动跑全量回归并生成对比报告。三是加入更多工具类型支持让Agent能读取SQL数据库文件、调用内部接口把信息获取的半径再扩大一圈。如果你也想做一套属于自己的AI工程底座我的建议是别急着上框架先用手写的方式把核心链路跑通哪怕代码简陋一点都没关系。这个从零到一的过程收获的东西远比你想象的多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →