资讯动态

AI模块架构设计:多Provider切换、RAG与Agent编排实践

发布时间:2026/9/28 9:27:43 来源:尧图企业网站定制
这一篇是系列第四篇。前几篇把基础设施、数据接入和基础服务聊完了这一篇集中说AI模块的架构设计也就是很多团队都会卡住的三个点多Provider切换、RAG知识库落地、Agent编排。我的结论很直接如果你现在还在业务代码里直接调某个厂商的SDK那你后面引入知识库和Agent的时候一定会返工。因为RAG需要检索层Agent需要工具层这两个层都要挂在模型调用之上而模型调用如果和业务代码耦合在一起后续任何一个改动都会被无限放大。1. 为什么业务代码里不该直接调大模型SDK拆Provider层的动机1.1 一次上游服务升级让我全线“变哑”我在实际项目里被“上游升级”教育过一次。当时做一个客服摘要系统所有大模型调用都通过官方SDK直接发起模型名和请求格式在十几个服务里写死。某天厂商做接口升级老模型名下线官方SDK同步更新结果所有服务几乎同时开始报错。我们改了一整天代码而且中间不敢大规模重启只能分批切流量那一天的体验非常糟糕。那次之后我彻底改了一个习惯所有模型调用必须收口到一层这一层只做模型的接入和调度不沾任何业务逻辑。我把这种做法叫“Provider抽象层”。它不解决效果问题但它能保证当外界变化来临时你只需要改一个地方而不是全项目搜索替换。1.2 多Provider切换到底在解决什么问题我把“直接调用SDK”和“通过Provider抽象层调用”做了一次对比这些场景都是实际踩过的场景直接调用SDK有Provider抽象层厂商升级接口、模型改名改所有调用点风险高只改适配层调用方无感知想换更便宜/更合适的模型全局搜索替换容易漏改配置或路由规则即可上游服务临时不可用业务直接失败报错自动切换备用服务同时接多家大模型每个厂商一套SDK代码混乱统一接口新增成本极低要引入RAG和Agent调用点继续膨胀难以插桩RAG和Agent作为独立层挂在上面你会发现第三行尤其关键。大模型服务本身是外部依赖不是你的基础设施你控制不了它的发布节奏、限流策略和价格变动。多Provider切换的本质不是“觉得哪家好用就用哪家”而是把外部不确定性隔离在系统边缘。1.3 Provider层的职责边界哪些该做哪些不该做我见过有人把Provider层写成“上帝服务”什么业务逻辑都往里塞结果反而比直接调SDK还难维护。Provider层应该做的是管理多个Provider的注册、发现、端点配置和认证信息把各家请求参数转换成统一的内部消息格式处理流式响应把SSE数据流归一化成统一的事件流对异常做分类决定哪些该重试、哪些该告警、哪些该切换输出每一次调用的token消耗、延迟、模型名供后续做可观测性。不该做的也很明确不写业务规则不处理文档切分不做对话管理不决定“这个问题该不该查知识库”。这些是更上层RAG和Agent编排的事。Provider层只做管道不做内容加工否则你会陷入无穷无尽的需求冲突。2. Provider切换的实现端点寻址、流式归一化和自动降级2.1 先把端点寻址做好base_url为什么是必填项做多Provider切换第一步不是写代码而是把配置模型设计好。很多框架和SDK的报错都是同一种形态配置错误: xxx provider 缺少 base_url 配置。这个base_url是什么就是你要调用的大模型服务的HTTP入口地址。官方SDK通常内置了一个默认地址所以很多人不配置也能跑起来。但一旦你接入了私有化部署的模型、本地模型服务、或者兼容某种协议的第三方端点就必须手动指定。否则请求会发到默认地址去要么认证失败要么地址根本不存在。我习惯在每个Provider初始化时做一次“配置完整性校验”启动时发现缺字段直接报错而不是等请求发出去才报。配置一般长这样providers: openai: base_url: https://api.openai.com/v1 api_key_env: OPENAI_API_KEY deepseek: base_url: https://api.deepseek.com/v1 api_key_env: DEEPSEEK_API_KEY ollama: base_url: http://localhost:11434/v1 api_key_env: 注意api_key_env存的是环境变量名不是密钥本身。这样配置中心、Kubernetes Secret、本地.env都能灵活注入不会把密钥写进代码仓库。2.2 统一请求响应结构让上层忘记每个厂商的差异各家大模型服务的请求和响应格式并不一致。有的返回choices[0].message.content有的返回content[0].text有的兼容协议改了几个字段名。如果让上层业务代码去处理这些差异你会写出大量if provider xxx的分支那是最差的设计。我的做法是内部统一使用一套消息结构所有适配层负责格式互转。这套结构不需要发明什么新协议直接以目前最通用的“消息列表 聊天补全”格式作为基准。dataclass class ChatMessage: role: str # system / user / assistant / tool content: str | None tool_calls: list | None None tool_call_id: str | None None dataclass class ChatResponse: content: str | None tool_calls: list | None None finish_reason: str usage: dict | None None流式响应更需要归一化。很多客户端把非流式和流式写两套分支我认为更合理的是上层只能看到一种异步迭代器底层SSE解析全部封装在适配层里。这样上层代码不会因为“这次调用开不开启流式”而分裂成两种写法。2.3 模型名映射业务用别名底层用厂商真名另一个容易翻车的点是模型名。业务代码里出现gpt-4o-mini、claude-sonnet-4-5这种具体模型名短期内没问题长期看就是维护噩梦。我推荐用语义别名也就是业务代码里只出现default-chat、summarizer-v2这样的名字。model_aliases: default-chat: openai:gpt-4.1-mini local-chat: ollama:qwen2.5-7b-instruct code-review: anthropic:claude-sonnet-4-5这样切换模型只改一行配置。要灰度测试时也可以让别名指向同一模型的两个不同版本按请求比例切流量。很多报错比如no api key for provider route根因就出在这条链路上别名解析到了某个Provider但那个Provider没有绑定可用的密钥。我在注册表里启动时会做一次“别名→Provider→Key”的链路完整性检查缺任何一环直接启动失败而不是让线上请求去撞错。2.4 错误分类与降级什么时候重试什么时候换路统一错误分类是Provider层最有价值的部分。我把异常分成几类错误类型处理策略认证失败不重试立即告警人工介入限流429指数退避重试1至2次仍失败则切备用服务端错误5xx延迟100ms重试1次失败切备用模型不可用从路由表临时摘除冷却后探测恢复请求参数错误400不重试通常是调用方代码问题降级链配置也放在路由表里例如route_policies: default-chat: primary: openai:gpt-4.1-mini fallbacks: - deepseek:deepseek-chat - ollama:qwen2.5-7b-instruct主模型连续失败时按顺序切换备用同时把降级信息打到日志里。我在生产环境看到的情况是自动降级至少能保住80%的请求不失败剩下的再走告警人工介入。3. RAG知识库的完整链路从文档切分到混合检索再到引用溯源3.1 文档解析和切分块大小、重叠与父子分块怎么选RAG最容易踩的第一个坑就是切分。用固定窗口按字符数硬切比如每500字一刀后果是句子被拦腰截断检索出来的片段往往语义不完整大模型拿到这种片段生成质量自然上不去。我更推荐结构感知的切分先解析文档的标题层级按章节、段落组织再对超长段落做二次切分。块大小和重叠的参考值chunk_size取500至800字符太短则上下文信息少太长则检索精度下降chunk_overlap取100至150字符保证跨块的关键句不丢失优先按段落边界切不要按固定长度硬切。还有一个很实用的技巧是父子分块。父块是完整的段落或章节用于给模型提供上下文子块是更小切块用于检索命中。比如检索时命中了子块喂给模型的却是一整个父块这样既保证召回精度又保证生成时上下文完整。3.2 Embedding模型选择比“效果最好”更重要的是什么Embedding模型决定了你知识库的底层表示质量。选型时我主要看四个维度中文效果是否可靠虽然大多数现代Embedding模型都支持多语言但中文专有名词、长句表达能力差异不小输入长度上限是否能覆盖你设计好的chunk_size向量维度维度越高精度未必越高但存储和检索成本会显著上升是否支持本地私有化部署涉及敏感内部文档时这一点很重要。最容易被忽略的是Embedding模型的一致性。建库时用的Embedding模型和线上查询时用的Embedding模型必须是同一个且版本一致否则向量空间对不上余弦相似度完全失去意义。我习惯把embedding模型名和版本写入每个chunk的metadata查询时直接过滤版本不一致的数据。3.3 向量检索、关键词检索与Rerank为什么只用向量库不够只做向量检索的RAG在真实场景里表现并不够好。向量检索擅长语义相近的召回但对精确匹配不敏感比如“SKU-8871”这种编号、产品型号、人名拼写向量召回往往不如关键词检索。我把检索设计成三段流水线# 1. 向量召回 Top 50 vector_hits vector_store.query(query, top_k50) # 2. 关键词召回 Top 50 keyword_hits keyword_index.search(query, top_k50) # 3. RRF融合并粗筛 merged reciprocal_rank_fusion(vector_hits, keyword_hits, k60) # 4. Rerank精排取Top5 reranked reranker.rerank(query, merged[:20]) return reranked[:5]RRF融合公式是score sum(1 / (k rank))它不要求向量分数和关键词分数在同一量纲只依赖排名所以融合很稳。至于Rerank第二阶段的精排模型虽然成本高一些但它专门判断“这段内容和当前问题是否相关”比双塔向量模型更细腻。只要对粗召回后的20条做重排成本完全可控。3.4 引用溯源与Agentic RAG让回答有据可查也敢让模型自己找线索生产环境里的RAG产品一定要有引用溯源。我要求生成阶段给检索结果编号并让模型在回答里使用[1]、[2]这类标记最后用规则去校验这些标记是否真实存在。宁可回答保守一点也不能让模型自己编一个来源。RAG再往前走一步就是Agentic RAG。传统RAG是“一次检索一次生成”Agentic RAG让模型自己判断需不需要检索、检索几次、如何改写查询词。举例用户问“去年华东区的销售问题后来怎么解决的”第一次检索可能只命中“华东区销售分析”但漏了“问题跟进”相关文档。模型根据初步结果决定二次检索把查询改写成“华东区 销售异常 整改措施”就能把后续资料翻出来。如果你的知识库问题大量涉及实体关系比如“A团队负责了哪些模块”可以考虑GraphRAG的思路它提取实体和关系形成图谱对多跳问题有明显优势。但GraphRAG的建库成本不低适合文档量大、关系问答需求强的情况普通FAQ场景不太有必要。4. Agent编排从单Agent工具调用到多智能体协作与Harness4.1 Agent的本质模型、工具、记忆与策略的组装Agent不是一个单独的模型而是把模型、工具、记忆、策略组合起来的运行单元。最朴素的Agent模式是ReAct循环模型思考接下来要做什么调用工具观察工具返回结果再进入下一轮思考。现在更主流的是Function Calling函数调用也就是模型不再输出一段包含“调用工具吧”的自然语言而是直接输出结构化JSON指定要调用哪个工具、传什么参数。框架负责执行工具然后把结果回填给模型。这个模式更可控、更容易解析也方便在链路里做日志插桩。4.2 工具调用的实现细节JSON Schema、执行器与安全边界工具定义通常是JSON Schema形式核心是准确描述工具参数让模型能正确生成调用。不要把description写得含糊其辞模型是靠它决定何时调用工具的。tools [ { type: function, function: { name: search_knowledge_base, description: 当用户问题涉及内部文档、操作手册、售后记录时查询知识库, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词} }, required: [query] } } } ]执行器的安全边界是Agent能不能上生产的分水岭。我坚持三条规则工具必须登记白名单模型返回的未注册工具直接拒绝执行高权限操作要有人工确认环节工具运行环境要有沙箱限制。有些框架会报类似access to private networks is forbidden的错常见的不是配置问题而是执行器运行环境的网络策略在拦截检查沙箱配置就好。工具调用循环的骨架大概是for turn in range(MAX_TOOL_TURNS): resp chat_with_tools(messages, tools) if not resp.tool_calls: return resp.content messages.append(resp.message) # 模型决策消息 for call in resp.tool_calls: result execute_tool(call) # 白名单校验、超时、异常捕获 messages.append(tool_result_message(call.id, result))MAX_TOOL_TURNS必须设置上限我之前见过Agent在一个工具上反复尝试十几轮既浪费时间又烧token。4.3 Workflow和Agent该怎么选别一上来就自由发挥不少团队一听到Agent就激动什么需求都想让Agent自由发挥。我的建议刚好相反默认先画Workflow把能固定的流程写死只把真正需要模型判断的环节交给Agent。维度WorkflowAgent执行路径固定可预期模型自主决策有随机性成本相对可控不确定工具轮次可能膨胀可观测性每个节点清晰决策过程较难追踪适用场景表单处理、固定审批、数据清洗开放式问答、多工具组合、目标动态变化比如工单分类我首选规则先按关键词、正则把常见类型筛掉。只有规则筛不出来的少数工单才交给Agent去理解并决定调用哪个下游接口。这样80%流量成本很低剩余20%即使Agent偶尔出问题影响面也可控。4.4 多Agent协作模式串行、并行、监督者以及Harness的角色多Agent编排里我常用的有三种模式串行流水线上游Agent产出结果传给下游Agent继续处理适合内容流转明确的场景并行汇聚多个Agent同时处理不同子任务最后汇总适合调研类问题但要注意结果去重和合并监督者模式主管Agent负责拆任务、派发给子Agent、收集结果适合总体目标动态变化的情况。在多Agent场景里必须引入Harness的概念。我理解的Harness就是模型调用、工具执行、上下文管理、token预算统一的执行容器。Agent是大脑里的决策位Harness是支持决策位运行的整套身体。多个Agent在同一个Harness里共享执行资源但上下文要隔离不能让每个Agent都背着全局所有消息。如果你是非技术团队或项目初期用Dify这类编排平台能快速搭Workflow和Agent。但产品后期如果涉及大量私有定制往往还是要自研Harness因为平台在日志、灰度、权限控制上的边界不一定满足你。5. 一条真实请求怎么串起来Provider × RAG × Agent的组合调用链5.1 请求入口先规划再调用一次完整的AI请求不是直接从“用户输入”跳到“大模型生成”。我习惯在入口放一个规划器用轻量模型判断用户意图这个问题是闲聊是需要查知识库还是需要调动工具这个分类任务模型能力要求不高选便宜轻快的模型就行。先规划意义重大。不做规划的话每个请求都会把RAG检索、Agent循环这些重活全跑一遍成本和延迟都会失控。而做了规划约一半的请求其实可以直接回答不需要走知识库也不需要开Agent。5.2 检索链路Embedding、向量检索、Rerank的顺序当规划器判断需要查知识库时检索链路才开始工作。顺序是先对用户问题做Embedding同时用原始文本去做关键词检索然后向量结果与关键词结果做RRF融合再对融合后的候选做Rerank精排最后把排序靠前的片段组装成上下文。我把这段链路的超时预算也设置好比如Embedding预留50毫秒向量和关键词检索预留100毫秒Rerank预留200毫秒上下文组装预留20毫秒整段控制在400毫秒内。线上很多毛刺不是模型慢而是检索链路没有超时控制一个偶发慢查询拖垮了整个接口。5.3 生成链路上下文组装、主模型调用与工具执行回填检索完成之后进入生成阶段。上下文组装的优先级我会固定好系统指令最先然后是工具定义再是检索到的知识片段之后才是对话历史最后是当前问题。为什么这样排因为模型对连续文本早段的内容更敏感工具定义和知识片段是它生成时最需要依赖的“硬约束”对话历史放在知识之后是为了避免被若干轮前的话题偏掉。如果Agent决定要调用工具每轮工具调用结果都要回填到messages里但历史要压缩。我习惯每轮只保留“上一步思考摘要”和“上一步工具调用结果”并不把中间过程全部保留否则多轮循环之后上下文轻松爆掉。整条链路用文字描述就是入口规划 → RAG检索 → 主模型生成 → 模型要求调用工具 → 工具执行 → 结果回填 → 再次调用模型 → 输出最终回答。每一步都穿过Provider层由Provider层负责实际请求发送和响应归一。5.4 全链路观测一串trace_id贯穿三层三层架构最怕问题是排查链路长。我现在要求每一层都输出同一个trace_id并记录关键指标字段含义trace_id贯穿Provider、RAG、Agent全程的请求IDlayer当前日志属于哪一层model_name实际调用的模型名latency_ms各层耗时token_usage输入和输出token数fallback_from是否走了备用模型rerank_countRAG候选数和最终命中数有了这些字段一次“用户感觉回答慢了”的问题我能在日志里一眼看到是检索层超时、还是主模型首token延迟、还是工具循环了太多次再对症下药。6. 我落地中踩过的Provider、RAG、Agent三个坑6.1 Provider层三个“配置错误”型报错的排查思路我把最常见的三个错误放在一起说因为排查思路是相通的。缺base_url的报错先检查配置中心读到的键名和环境变量注入情况再把Provider启动时的实际配置打印出来确认读到的值不是空的最后用最小客户端向目标端点发一个探测请求验证可达性提示某个模型没有API Key先查别名文件和Provider的绑定关系是不是模型别名解析到的Provider没有配置密钥我后来加了一步启动时链路检查基本根治了这类问题提示模型不可用通常是模型名过期、区域未开通、或账号权限不够我习惯维护一份模型生命周期清单上线前自动探测模型是否存在并验证基础连通性。这几种错误最佳策略不是等线上报警而是让系统在启动阶段就fail fast把错误暴露在发布环节。6.2 RAG层Embedding模型换了之前存的向量全废了有一段时间我为了评测换了个Embedding模型线上查询也同步切了结果检索质量断崖式下跌。原因是建库时用的Embedding模型和新模型向量空间不同旧索引里的向量和新查询向量之间的相似度根本没有意义。那次之后我做了一个强制约定每个chunk的metadata必须记录embedding_model_name和embedding_model_version查询时严格过滤版本不一致的数据。要升级Embedding模型必须先重建索引或者双版本并行跑一段切换期不能边查边切。6.3 Agent层工具调用死循环和上下文爆炸工具死循环是Agent上生产最常见的故障。根因往往是工具执行失败却返回了一个“空成功”Agent以为动作完成就接着下一步结果进入某种重复循环。我的对策有三个设置最大工具调用轮数比如5轮封顶工具返回值必须包含明确的status字段和结构化错误信息对连续重复的动作做指纹检测连续3次相同动作直接中断并返回异常。上下文爆炸则在多轮Agent循环后出现。我改为分轮归档每轮结束只保留“本轮意图摘要”和“关键结果”完整工具返回写进trace日志不留在对话上下文里。这样Agent既不会丢失关键信息上下文窗口也不会被中间过程塞满。最后分享一个我现在的默认做法任何新AI功能先画数据流标清楚哪些环节固定走Workflow、哪些环节需要Agent自主决策再决定要不要接RAG检索。Provider层永远是最先搭好的一层它不值得炫耀但没有它后面每一次模型升级和厂商切换都会让你重新体验一遍被迫改代码的滋味。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑