☰
AI模块架构实践:多Provider切换、RAG接入与Agent编排
2026/9/26 18:53:22 网站建设 项目流程

前面三篇我们把 AI 模块的需求拆分、数据流转和基础接口都过了一遍,这篇集中讲架构层面的三件核心事:多 Provider 切换、RAG 知识库接入、Agent 编排。这三个词单独拎出来都不新鲜,但在同一个系统里把它们揉在一起、还要保证切换顺滑、回答不飘、任务不跑飞,才是真正费功夫的地方。这篇我会按我自己实际落地项目的顺序来讲,先说为什么这三件事必须放在同一套架构里设计,再分别拆 Provider 抽象、RAG 链路和 Agent 编排,最后给一份可以直接抄作业的最小实现和一堆排坑记录。

1. 为什么要把 Provider、RAG、Agent 三件事打包在一起设计

1.1 这三件事分别解决什么问题

先说定位。Provider 层管的是“让系统能随时换脑子”——今天用 DeepSeek,明天换 Qwen,后天接一个私有化部署的本地模型,上层业务代码不用跟着改。RAG 管的是“让脑子有知识”——模型训练数据里没有你公司的内部文档、没有你刚更新的产品手册,RAG 就是把这些外部知识在推理时塞进上下文。Agent 管的是“让脑子会干活”——不只是回答一个问题,而是把一个复杂任务拆成几步,中间调用搜索、查库、执行命令等工具,最后把结果汇总给你。

很多人容易犯一个错:把这三件事当成三个独立模块,先接一个模型 API 跑通,后面再单独加知识库,再后面引一个 Agent 框架。结果就是系统里到处都是硬编码的模型名、散落的向量库配置、和一套不受控的工具调用逻辑。等模型要换、知识要更新、任务要变复杂的时候,每一处都是坑。我自己的感受是:这三件事必须在架构图上同时出现,边界划清楚,再谈各自实现。

1.2 模块边界的划分逻辑

我习惯画成四层:

层级职责典型组件
应用层对话、问答、任务入口业务 API、Web UI、机器人
Agent 层任务拆解、工具编排、状态管理Agent 框架、ReAct 循环、工具注册表
能力层RAG 检索、模型调用、工具执行向量库、重排器、Provider 客户端、各类工具
基础设施层配置、密钥、日志、监控配置中心、密钥管理、追踪系统

核心原则是:上层依赖下层,但每层只依赖下层的抽象接口,不依赖具体实现。Agent 层要调模型,它面对的是一个ChatModel接口,而不是某一家 SDK 的类;Agent 层要检索知识,它面对的是一个Retriever接口,而不是某个向量库的客户端。这样后面换 Provider、换向量库、换 Agent 框架,都只动对应层内部的东西。

1.3 四个必须要守的架构原则

第一,配置与代码分离。模型名称、Base URL、API Key、超时时间、温度参数,全部走配置文件或环境变量。这一点看起来简单,但我见过不止一个项目把模型名写在业务代码里,换模型要发版。第二,所有外部依赖都要有降级路径。主模型挂了走备模型,检索超时就走纯模型回答,Agent 执行失败要有兜底回复。第三,可观测性从第一天就做。每一次模型调用、检索耗时、Agent 的每一步思考,都要有日志和耗时记录,不然问题来了只能瞎猜。第四,内容安全底线。在模型输入前、输出后都做合规校验,该过滤的过滤,该加提示的加提示,尤其是面向 C 端用户的产品。

2. 多 Provider 切换:抽象层设计才是关键

2.1 统一接口与模型能力差异

多 Provider 切换里最容易踩的坑是“假装统一”。很多封装就是把各家 SDK 的chat.completions.create都包成一个函数,看起来是统一了,实际上各家在参数、返回格式、能力边界上差异巨大。比如有的模型支持system角色,有的模型把这个当普通消息;有的模型支持 Function Calling,有的只支持 JSON 输出;有的模型支持temperature=0,有的最低只能到 0.1;流式输出的 chunk 结构更是各写各的。

所以抽象层不能只做一个“发请求”的壳子,而是要定义一套“能力契约”。我设计ChatModel接口时,核心方法就一个chat(messages, options),但options里包含了调用方真正关心的东西:

  • temperature、max_tokens、top_p这些生成参数
  • tools:工具定义列表,如果目标模型不支持工具,由适配层做降级(比如把工具描述塞进 System Prompt)
  • response_format:要求 JSON 输出还是普通文本
  • stream:是否流式返回

适配层的职责是:把这份统一请求翻译成各家的 SDK 调用,再各家返回翻译回统一格式。这一步翻译工作省不了,但翻译逻辑集中在适配层,业务代码就不需要关心对面是哪家。

2.2 路由策略与降级机制

Provider 切换不只是“改个配置”那么简单,真正好用需要两层路由。第一层是按业务场景路由:闲聊、翻译这类轻任务走便宜快速的模型;代码生成走代码能力强的模型;复杂推理、长文档分析走旗舰模型。第二层是运行时自动降级:主模型返回 429 限流、5xx 错误、或者连续多次超时,自动切到备选模型重试。

降级这块我有一个建议:不是所有错误都适合降级。400 这种请求参数错误,降级大概率还是错,应该直接抛回上层;429、500、超时这类服务端问题才值得重试和降级。重试要带指数退避,比如第一次等 1 秒、第二次 2 秒、第三次 4 秒,别上来就疯狂重试把服务商打爆。

路由层的设计还要考虑成本。我现在会在配置里给每个 Provider 标注一个“优先级”和“成本档位”,路由层默认选成本最低的可用模型,当任务标记为“高价值”时才走贵模型。这套机制上线后,我的实际体感是账单能降 30% 左右,而且用户无感知。

2.3 配置管理:密钥、Base URL、默认参数

多 Provider 的配置管理,最痛苦的就是密钥分散、Base URL 缺失、模型名写错这三个问题。我统一用一份 YAML 管理,结构大致是:

providers: deepseek: type: openai_compatible base_url: https://api.deepseek.com/v1 api_key_env: DEEPSEEK_API_KEY default_model: deepseek-chat timeout: 30 weight: 10 # 权重越高优先级越高 qwen: type: openai_compatible base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 api_key_env: DASHSCOPE_API_KEY default_model: qwen-plus timeout: 30 weight: 5

这里有个容易被忽略的细节:很多服务商提供的是 OpenAI 兼容格式的接口,但 Base URL 往往不是根地址,而是带版本路径的,少一个/v1就会报 404 或者 missing base_url 错误。我在排障时见到的错误大概分三类:

错误现象常见原因处理方式
400 配置错误,provider 缺少 base_url没配置接入地址或地址格式不对检查配置文件,补全完整 URL(含版本路径)
No API Key for provider route环境变量未设置或 key 为空确认密钥已注入,且 env 名称与配置一致
Missing session id使用了需要会话态的接入方式,但请求未携带会话改用标准 API Key 方式,或先建立会话再请求

密钥管理这块,我最开始图省事直接把 key 写在代码里,后来不得不做一轮大整改。现在的做法是:本地开发用.env文件,测试和生产环境用环境变量注入,任何情况下 key 不进代码仓库。

3. RAG 知识库:从切块到检索的完整链路

3.1 为什么需要 RAG 而不只是无限拉长上下文

模型上下文窗口确实越来越大,但把整本手册都塞进 Prompt 有两个问题:一是贵,token 按量收费,塞 10 万字文档,一次对话的成本直接起飞;二是效果差,模型面对超长上下文中混杂的大量无关信息,注意力会被稀释,反而更答不准。

RAG 的思路是:不把全部知识给它,而是把和问题最相关的几段知识给它。这就像是查资料,不是把整座图书馆搬到你面前,而是先帮你检索出三本最相关的书,翻到相关章节,再让你读。这也是 RAG 相比“硬喂长文本”最大的价值:可控、可溯源、更新便宜。

3.2 切块策略与向量化

RAG 链路里最影响效果的,反而不是模型,而是切块和检索。切块切得好不好,直接决定“知识能不能被捞起来”。

我的切块经验可以浓缩成三点:按语义边界切,不按固定字数硬切;块的大小看文档类型;块与块之间要有重叠。比如:

  • 产品手册、规章制度这类有明确章节的文档,按章节段落切,一个小节一块
  • 技术文档、代码注释,按 Markdown 标题层级切,每一节一块
  • PDF 扫描件,先做 OCR 再按页切
  • 聊天记录、客服问答,按“一问一答”为一个单元切

块大小我这里给一个参考区间:512 token 到 1024 token。太小的块虽然精确,但上下文信息不完整;太大的块噪声多,检索精度下降。我实际用的重叠窗口是 10%-15%,也就是说下一块会包含上一块末尾的一小部分内容,避免关键句子正好被从中间截断,导致语义断裂。

向量化模型的选择,我建议优先考虑中文效果好的开源嵌入模型,比如 bge-large-zh、m3e 这类,它们不依赖外部 API,离线也能跑,而且 embedding 维度在 768-1024 之间,检索速度也够。向量库层面,数据量小可以用轻量方案,数据量大再上专门的向量数据库,具体选型看下表的对比:

方案适合场景优点注意点
内存型 / 本地文件个人项目、原型验证部署简单,零运维数据量大时检索变慢
开源向量库中小规模生产可控性高,支持过滤需要自己运维
云向量库规模化场景托管省心、扩展性好注意成本和服务商锁定

3.3 检索、重排与引用溯源

检索这里最忌讳的是“只用向量检索”。关键词精确匹配和语义相似各有所长:搜“订单号 ABC123 报错”,关键词检索引擎能精确命中,向量检索反而可能被“订单”和“报错”两个词的语义带偏。我现在的做法是混合检索:向量检索召回一批,BM25 关键词检索召回一批,融合去重后进入重排阶段。

重排(Rerank)是我强烈建议不要省的一步。召回阶段为了召回率,会故意放宽条件多捞一些候选;重排模型再按“和问题的真实相关性”对候选精排,取 top-k 送进 Prompt。加上重排之后,回答质量提升非常明显,尤其是在文档库比较大、话题比较杂的场景。一个最小可用的 RAG 检索链路,我用伪代码描述大概是:

def retrieve(query, top_k=5): vector_hits = vector_search(query, top_k=20) keyword_hits = keyword_search(query, top_k=20) candidates = merge_rank(vector_hits, keyword_hits) reranked = rerank(query, candidates, top_k=top_k) return reranked

还有一个容易漏的:引用溯源。RAG 系统的输出如果只是给一段回答,用户没法确认“这个回答是编的还是查到的”。我要求 RAG 返回的每一条知识块都带来源信息(文档名、页码/章节、原文摘录),回答里也标注引用了哪些来源。这既是产品体验,也是责任划分——模型说错了,你能定位到是知识库的问题还是生成的问题。

3.4 RAG 和 MCP 的区别

最近经常被人问到 RAG 和 MCP 到底什么关系。我的理解是:两者解决的不是一个问题,甚至不在一个层级。

RAG 是一套数据处理流程——切块、嵌入、检索、注入,解决的是“模型不知道的知识去哪里找”;MCP 是一套工具调用的协议——它规范了模型如何发现工具、如何调用工具、工具如何返回结果,解决的是“模型怎么操作外部系统”。可以理解为 RAG 是给模型“喂资料”,MCP 是给模型“接手脚”。在一个完整的智能体系统里,RAG 完全可以封装成一个 MCP 服务,让模型通过 MCP 协议去调用检索工具。这也引出了所谓 Agentic RAG:不再是一次“检索 → 生成”的固定流程,而是模型根据任务需要,自主决定什么时候检索、检索几轮、检索完之后怎么用这些结果。

4. Agent 编排:让模型学会用工具

4.1 Agent 是什么,Harness 和 Agent 的区别

Agent 的核心是一种循环:模型提出下一步行动 → 系统执行该行动(通常是调工具) → 把执行结果作为观察返回给模型 → 模型根据观察提出下一步行动,直到模型认为任务完成,或达到预设的终止条件。这就是常说的 ReAct 模式。

很多人把 Agent 框架和 Agent 本身搞混。框架是“Harness”,它是帮你跑循环、调工具、管记忆的脚手架;Agent 是这个循环中的“策略”,即模型根据当前上下文做出的决策。Claude Code 这类工具里的“Skill”和“Agent”也不是一回事:Skill 是一组预先定义好的能力包(比如“如何做代码审查”的一整套提示词和工具组合),Agent 是动态决定何时启用的执行者。Skill 是武器库,Agent 是战士。

4.2 任务拆解与工具调用

Agent 编排最大的难点不是循环代码怎么写,而是工具的定义和任务拆解的粒度。

先讲工具定义。给模型的工具描述必须非常具体,否则模型不知道什么时候该调用它。我写过一份“内部知识检索工具”的定义,最初的描述是“查询内部知识库”,模型经常在用户问天气的时候也去调用它。后来改成完整描述:“当用户询问公司制度、产品手册、技术规范等内部文档内容时,使用此工具检索相关知识。不要用于通用知识问答。”这个改动让工具调用的准确率上升了一截。另一个经验是:不要一次性给 Agent 挂十几个工具,模型会犯选择困难症,要么瞎调用、要么来回尝试。把高频工具控制在 3-5 个,其他低频工具放进子 Agent 或按需加载。

再讲任务拆解。简单任务不需要拆,Agent 直接查一次知识库就能答;复杂任务才需要 Planner。我的做法是把任务分为两种模式:单步模式——模型基于已有上下文直接生成回答或调用一次工具;多步模式——模型先生成一份计划,再逐计划执行。一定会有人问“怎么判断要不要多步”,我的标准是:用户请求里包含多个独立子问题,或者第一步的执行结果会影响后续怎么走,就必须多步。

4.3 状态管理与执行超时

Agent 一旦跑起来,你就得面对两个很实际的问题:状态怎么管,跑飞了怎么办。

状态管理我强烈建议用显式状态机,哪怕简单一点也比分不清强。一个任务至少要有这些状态:pending、running、tool_calling、succeeded、failed、needs_human。每次 Agent 循环推进,都要把状态落库或写入日志。这样做的好处是:用户刷新页面能看到任务进度;系统崩溃后能从最近一个持久化状态恢复;排查问题时知道它死在哪一步。

执行超时是所有 Agent 项目早晚都会撞上的痛。模型陷入死循环、工具调用迟迟不返回、或者某个外部接口 hang 住,都会导致“Agent 执行被终止”这类错误。我的防线有三层:

防线设置目的
单轮工具超时30-60 秒防止单个工具调用拖死整个 Agent
最大迭代次数10-15 次防止逻辑死循环或无限拆任务
总任务超时3-5 分钟防止多步任务整体失控

这三条即使配置在 harness 里,也要在业务层再做一遍兜底。我现在会把“最大迭代次数”设成 12 次,并且每次循环在 Prompt 里提醒模型“你已经进行了 8 次操作,请聚焦完成用户目标,不要继续无关操作”。实测这种显式提醒比单纯靠终止条件有效得多。另外,如果系统支持人工介入,在 Agent 执行超过一定次数或遇到不确定结果时,把控制权交还给人类是一个很实用的降级策略。

5. 实操参考:落地一个最小可用的 AI 模块

5.1 配置文件的组织方式

先给一份可直接参考的目录结构和配置组织。我所有 AI 模块相关的配置都统一收敛到config/下,不散落在业务代码里:

config/ ├── providers.yaml # Provider 列表、路由权重、默认参数 ├── rag.yaml # 切块参数、检索 top_k、embedding 模型配置 ├── agent.yaml # 最大迭代、超时、工具启用开关 └── .env # 存储各类 API Key,永远不入库

一个关键提醒:配置文件里的默认参数,要在代码里也有对应的默认值兜底。比如providers.yaml里某个模型没写timeout,代码不能直接读到None然后懵掉,应该在配置加载阶段做校验,发现缺关键字段就启动失败并报清晰的错误,而不是运行到一半才炸。

5.2 Provider 抽象层代码骨架

我给出一个简化但能跑通思路的骨架:

class ChatModel: def __init__(self, provider_config): self.base_url = provider_config["base_url"] self.api_key = provider_config["api_key"] self.default_model = provider_config["default_model"] def chat(self, messages, options=None): raise NotImplementedError class OpenAIChatModel(ChatModel): def chat(self, messages, options=None): # 用 OpenAI SDK 请求,底层同时兼容多数国内服务商 kwargs = {} if options: kwargs.update(options) return self._do_request(messages, **kwargs) class Router: def __init__(self, providers): self.providers = providers def route(self, task_type): # 按业务类型选模型,带降级 candidates = self.providers.for_task(task_type) return candidates[0] # 权重排序后第一个可用 class AIEngine: def __init__(self, router): self.router = router def complete(self, messages, task_type="normal"): model = self.router.route(task_type) try: return model.chat(messages) except RateLimitError: model = self.router.route_fallback(task_type) return model.chat(messages)

这套骨架最核心的地方在于:业务侧只接触AIEngine.complete(),完全不感知对面是哪个 Provider。任何新增 Provider,只需要新增一个继承ChatModel的实现类,并在配置里注册。

5.3 RAG 最小链路

RAG 链路我不建议自己从头实现向量化和存储,直接用现成组件组合最快。一个可运行的最小链路是:

def build_retriever(): embedding_model = load_embedding_model("bge-large-zh") store = init_vector_store() # 初始化向量库 return VectorRetriever(embedding_model, store) def rag_pipeline(query, retriever, chat_model): chunks = retriever.retrieve(query, top_k=5) context = build_context(chunks) # 拼接知识文本与来源 prompt = f"请基于以下资料回答问题...\n资料:\n{context}\n问题: {query}" answer = chat_model.chat([{"role": "user", "content": prompt}]) return answer, chunks # chunks 用于溯源展示

注意build_context这一步:不只是把 chunks 拼接起来,还要按与问题的相关性降序排列,并给每条资料编号。这样模型回答时能提到“根据资料 2”,系统再根据编号映射回真实文档,完成溯源。

5.4 Agent 编排的最小实现

Agent 编排的最小实现,核心就是一个循环。我的建议是不要一上来就引重型 Agent 框架,先用代码把循环写明白,等确认需求复杂了再引框架不迟。

def run_agent(task, tools, max_iterations=10): messages = [{"role": "user", "content": task}] for i in range(max_iterations): response = model.chat(messages, tools=tools) if response.has_tool_calls(): for call in response.tool_calls: action = execute_tool(call) # 执行工具,拿到观察结果 messages.append(call.to_message()) messages.append({"role": "tool", "content": action.result}) else: return response.content raise AgentTimeout("exceed max iterations")

这个简化版循环已经能覆盖 80% 的单任务智能体场景。需要注意的一点是:execute_tool内部必须做超时控制,而且工具返回的结果如果太长,要在进入下一轮模型调用前做截断,避免把少量有用的信息淹没在大量工具输出里。

6. 常见问题排查与避坑实录

6.1 Provider 配置类报错:先查配置再查代码

我遇到的 Provider 类报错,八成以上是配置问题而不是代码问题。“400 配置错误:provider 缺少 base_url 配置”这类信息,通常意味着配置加载阶段没把 Base URL 填进客户端。排查顺序建议严格执行:先看配置是否加载、再看环境变量是否注入、最后看代码里有没有覆盖配置的硬编码。

报错现象我的排查步骤
缺少 base_url 配置检查配置文件是否包含完整路径(含/v1等版本前缀)
missing api key确认对应环境变量已设置,并检查配置里的 env 名称是否拼写一致
missing session id优先改用标准 API Key 方式接入,不要在会话态上做文章
free tier 不可用 / 被拒绝按服务商要求配置正式身份凭证,不要用非正规方式规避限制
上游请求失败、模型不可用确认模型名是否属于当前服务商、配额是否充足、服务商状态页是否异常

一个长期建议:把配置加载结果和接口探测结果打点输出。启动时自动打印“已加载 provider: deepseek, base_url: xxx, model: deepseek-chat”,能省掉大量靠猜的排障时间。

6.2 检索质量类问题:八成是切块和重排的锅

RAG 问答效果差,很多人第一反应是“换更大的模型”,但实测下来,先检查切块和重排收益更大。常见问题包括:

  • 检索不到相关内容:检查切块是否破坏了语义单元,比如一句话被从中间切断;或 embedding 模型和文档语言不匹配
  • 检索出一堆不相关的结果:top_k 设得太大,或没做重排,建议把召回阶段和精排阶段分开,召回多捞一些,精排再卡紧
  • 回答出现幻觉:上下文里混入了太多低相关片段,模型被带偏。处理方式是降低 top_k,并严格要求模型“只依据资料回答,资料没有的内容明确说不知道”
  • 切块重叠导致重复内容:重叠窗口过大,相邻块之间大量内容重复,检索去重做掉,或用重排把重复项压下去

6.3 Agent 执行类问题:超时、循环、上下文爆炸

Agent 执行报错里最常见的就是超时和循环。“Agent execution terminated due to error”很多人见过,解决思路不只是调大超时,而是先定位它卡在哪一步。我的做法是在循环的每一步都打结构化日志:当前迭代次数、调用的工具、工具返回耗时、本轮模型响应摘要。这样“查出哪一步卡住”通常五分钟内就能搞定。

上下文爆炸也是 Agent 项目的高发问题。工具返回内容不断追加进消息列表,几轮之后 token 数可能破万甚至更多。我的处理策略是:工具返回做摘要再入上下文,而不是原样全塞;多轮对话只保留最近轮次 + 历史关键结论;必要时把早期上下文向量化,需要时再检索回来。

6.4 安全合规与成本控制

最后提两个很多人会忽略的点。一个是输入输出双向的内容安全校验:用户输入要拦截非法意图,模型输出要检查是否包含不当内容,这两道关卡不能只依赖模型自身。面向公众开放的服务,这个说得再怎么强调都不过分。另一个是成本控制:多 Provider 架构天然适合成本优化,但要配合预算告警。我给每个业务场景设置不同的 token 预算,超过 80% 就告警;再按日维度统计每个 Provider 的消耗占比,哪个模型突然消费暴涨,系统自动切流量,避免月底账单吓人。

在我自己负责的项目里,把 Provider、RAG、Agent 三件事收进同一套架构后,最大的收益不是某个指标变好了,而是“改动成本”降下来了。以前换一个模型要改业务代码、动 Prompt、重测全流程;现在只加一条配置,加一个适配类,改完跑一遍回归用例就能上线。RAG 的检索链路独立,文档更新只影响知识库,不碰模型逻辑。Agent 层的工具注册和任务编排分开,新增工具不会污染已有的执行流程。这套架构运行几个月下来,我个人的体会是:AI 应用真正的复杂度不在模型,而在模型之外的那些工程细节。把这些细节打理好了,模型反而成了最省心的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询