1. 从一张架构图说起:AI应用到底该怎么搭
很多人第一次接触AI应用开发,脑子里冒出来的第一个问题就是:我到底该从哪儿下手?是直接调个大模型接口就完事,还是得搞一套完整的工程架构?我刚开始做AI应用那会儿也纠结过这个问题,后来踩了不少坑才慢慢理清楚——AI应用架构设计这件事,本质上跟盖房子是一个道理。你得先知道这房子是给人住的还是当仓库用的,再决定打什么地基、用什么材料、留几个门。
所谓AI应用架构设计,说白了就是把大模型能力、业务逻辑、数据流转、工具调用、用户交互这几块东西,按照一定的规则和层次组织起来,让整个系统跑得稳、扩得动、改得动。它要解决的问题很具体:模型输出不稳定怎么办?多个Agent之间怎么协作?外部工具怎么安全接入?并发上来了怎么扛?这些问题不是调个API就能解决的,必须从架构层面提前想清楚。
这篇文章适合谁看?如果你是刚转AI应用开发的程序员,或者已经在做传统后端但想往AI方向靠的运维工程师,再或者你是产品经理需要理解技术边界在哪里,那这篇内容应该能帮你少走不少弯路。我会从整体设计思路讲到核心细节,再落到实操步骤和踩坑经验,尽量把每个关键决策背后的“为什么”说透。
2. AI应用架构的整体设计思路拆解
2.1 为什么不能直接“模型+接口”就上线
我见过太多团队一开始的想法特别朴素:用户输入问题,我拼个prompt发给大模型,拿到结果返回给前端,完事。这个方案在demo阶段确实能跑通,但一旦上线面对真实用户,问题就全冒出来了。首先是输出不可控,同一个问题问十遍可能给你十个不同的答案,格式还都不一样;其次是没有记忆,用户上一句说的信息下一句就丢了;再就是无法调用外部能力,模型不知道今天的天气、查不了数据库、发不了邮件。
所以AI应用架构的第一个核心思路就是:把大模型当成一个能力组件,而不是整个系统。它负责理解和生成,但不负责状态管理、不负责工具调度、不负责安全校验。这些活得由架构里的其他层来干。这就引出了分层设计的思路。
2.2 分层架构:从入口到模型到工具到数据
我习惯把AI应用分成五层来看,从下往上分别是:
- 模型层:负责推理和生成,可能是云端API,也可能是本地部署的开源模型
- 能力层:包括Agent调度、工具调用、记忆管理、RAG检索等
- 编排层:负责把多个能力串成工作流,处理条件分支、循环、异常
- 接口层:对外暴露的API、WebSocket、SSE等通信方式
- 交互层:前端界面、聊天窗口、语音入口等
这么分的好处是每一层可以独立演进。比如你今天用GPT-4,明天想换成Claude或者本地Qwen,只需要改模型层的适配器,上面的能力层和编排层基本不用动。再比如你一开始只做文本对话,后来想加图片理解,也只需要在能力层扩展多模态处理模块。
注意:分层不是目的,解耦才是。如果你的项目就是个小工具,用户量不大,硬套五层架构反而增加复杂度。架构设计要匹配业务阶段,别为了架构而架构。
2.3 Agent、LLM、MCP三者的关系怎么理解
这三个词现在热得发烫,但很多人搞不清楚它们之间的关系。我用一个餐厅的类比来解释:
LLM(大语言模型)就像餐厅里的主厨,他厨艺很好,什么菜都能做,但他只负责做菜,不负责点单、传菜、结账。你给他食材(输入),他给你菜品(输出)。
Agent(智能体)就像餐厅经理,他负责理解客人需求、安排主厨做菜、协调服务员传菜、处理突发情况。Agent本身不一定会做菜,但他知道什么时候该找主厨,什么时候该找其他人。
MCP(Model Context Protocol)就像餐厅的标准接口规范,规定了经理怎么跟主厨沟通、怎么跟供应商下单、怎么跟收银系统对接。有了这个规范,换一个主厨或者换一个供应商,经理不用重新学一套沟通方式。
所以一个典型的AI应用架构里,LLM是核心能力,Agent是调度中枢,MCP是连接标准。三者配合起来,才能让整个系统既灵活又稳定。
2.4 架构选型的几个关键决策点
在实际动手之前,有几个决策点必须先想清楚,不然后面返工成本很高:
| 决策点 | 选项A | 选项B | 建议 |
|---|---|---|---|
| 模型部署 | 云端API | 本地部署 | 初期用云端,量大或数据敏感再考虑本地 |
| Agent框架 | 自研轻量调度 | 成熟框架 | 简单场景自研,复杂场景用框架 |
| 工具接入 | 硬编码函数调用 | MCP协议 | 工具少硬编码,工具多且需扩展用MCP |
| 记忆存储 | 内存/Redis | 向量数据库 | 短期对话用Redis,长期知识用向量库 |
| 通信方式 | HTTP轮询 | SSE/WebSocket | 对话类用SSE,实时协作类用WebSocket |
这些选择没有绝对的对错,关键看你的业务场景和团队能力。比如你团队里没人懂向量数据库,那初期就别硬上RAG,先用关键词检索顶着,等业务跑通了再迭代。
3. 核心细节解析与实操要点
3.1 LLM接入层:别把模型调用写死在业务代码里
我见过最要命的代码就是在业务逻辑里直接写openai.ChatCompletion.create(...),然后整个项目里到处都是这个调用。等到要换模型、要加缓存、要加重试的时候,改到你怀疑人生。
正确的做法是抽象一个模型接入层,所有对LLM的调用都走这个层。这个层至少要做四件事:
- 统一接口:不管底层是哪个厂商的模型,对外暴露的方法签名一致
- 参数适配:不同模型的temperature、max_tokens、top_p取值范围可能不同,在这一层做转换
- 重试与降级:调用失败自动重试,重试多次失败后降级到备用模型
- 日志与计量:记录每次调用的token消耗、耗时、成功率
class LLMProvider: def chat(self, messages, model=None, **kwargs): raise NotImplementedError class OpenAIProvider(LLMProvider): def chat(self, messages, model="gpt-4", **kwargs): # 适配OpenAI参数 pass class LocalProvider(LLMProvider): def chat(self, messages, model="qwen", **kwargs): # 适配本地模型参数 pass这样业务代码里只需要llm.chat(messages),换模型的时候改配置就行。
3.2 Agent调度:ReAct模式为什么这么流行
Agent的核心是“思考-行动-观察”的循环,也就是常说的ReAct模式。它的工作流程是这样的:
- 接收用户输入
- 模型思考:我需要做什么?需要调用什么工具?
- 如果需要工具,生成工具调用请求
- 执行工具,拿到结果
- 把结果喂回模型,继续思考
- 直到模型认为可以给出最终答案
这个模式之所以流行,是因为它把复杂任务拆成了可管理的步骤,而且每一步都有明确的输入输出,方便调试和监控。
但ReAct也不是万能的。它的缺点是延迟高,因为每一步都要等模型推理;成本高,因为多轮调用消耗更多token;可能死循环,模型一直觉得还需要调用工具。所以在实际项目中,我会加两个限制:最大循环次数(比如10次)和超时时间(比如30秒)。
3.3 MCP协议:工具接入的标准化方案
MCP是什么?简单说就是一套让模型和外部工具、数据源之间通信的标准协议。在没有MCP之前,每个工具都要写一套适配代码,工具多了之后维护成本极高。MCP把这些适配工作标准化了,工具提供方只需要按照MCP规范暴露接口,应用方只需要按照MCP规范调用。
MCP的核心概念包括:
- Server:工具提供方,暴露资源、工具、提示模板
- Client:应用方,连接Server并调用其能力
- Transport:通信方式,支持stdio、HTTP+SSE等
在实际项目中接入MCP,我一般会这样做:
- 先梳理需要哪些外部能力(查数据库、调API、读文件等)
- 找现成的MCP Server,没有就自己写一个
- 在Agent调度层注册这些Server
- 配置好权限和超时,防止工具调用失控
提示:MCP工具调用一定要加权限控制。我见过有人把数据库删除操作暴露成MCP工具,结果模型误调用直接把表清了。工具描述里要明确写清楚“这个工具会修改数据”,并且在执行前加确认机制。
3.4 记忆管理:短期记忆和长期记忆分开处理
AI应用如果没有记忆,就像跟一个失忆的人聊天,每次都要从头解释。记忆管理一般分两块:
短期记忆:当前会话的上下文,通常用滑动窗口保留最近N轮对话。实现上可以用Redis存会话历史,每次请求时取出拼接成messages。注意要控制总token数,超了就截断最早的对话。
长期记忆:跨会话的知识,比如用户偏好、历史事实。这块通常用向量数据库做语义检索,把相关记忆片段召回后注入prompt。
我踩过的一个坑是:短期记忆窗口设得太大,导致每次请求token消耗爆炸。后来改成动态窗口——根据当前问题的复杂度决定带多少历史,简单问题少带,复杂问题多带,成本降了将近一半。
3.5 并发处理:AI Agent怎么扛住高并发
这是很多从demo转生产的团队最头疼的问题。AI应用的并发瓶颈通常不在模型推理本身(云端API一般能扛),而在Agent调度层的状态管理和工具调用的串行等待。
我的经验是:
- 无状态化:Agent调度层尽量做成无状态的,会话状态外放到Redis,这样水平扩容很容易
- 异步化:工具调用尽量异步,不要阻塞主流程。比如查数据库和调外部API可以并行发起
- 队列削峰:请求量突增时用消息队列缓冲,后端按自己的能力消费
- 超时与熔断:每个工具调用设独立超时,失败快速返回,不要让一个慢工具拖垮整个请求
实测下来,一个设计良好的Agent调度层,单实例扛几百QPS问题不大,关键是要把状态管理和IO等待处理好。
4. 实操过程与核心环节实现
4.1 环境准备与项目骨架搭建
假设我们要搭一个支持多轮对话、工具调用、RAG检索的AI应用,我一般会这样组织项目结构:
ai-app/ ├── config/ │ ├── models.yaml │ └── mcp_servers.yaml ├── core/ │ ├── llm_provider.py │ ├── agent.py │ ├── memory.py │ └── tools.py ├── api/ │ ├── routes.py │ └── schemas.py ├── services/ │ ├── chat_service.py │ └── rag_service.py └── main.py依赖方面,核心是这几个:
pip install fastapi uvicorn openai redis pymilvus mcpFastAPI做Web框架,Redis做会话缓存,Milvus做向量检索,mcp做工具协议支持。版本上建议锁定大版本,避免自动升级导致接口不兼容。
4.2 模型接入层的完整实现
模型接入层我一般会写一个工厂模式,根据配置创建对应的Provider:
import yaml from openai import OpenAI class LLMFactory: @staticmethod def create(config_path="config/models.yaml"): with open(config_path) as f: config = yaml.safe_load(f) provider_type = config["default"]["type"] if provider_type == "openai": return OpenAIProvider(config["default"]) elif provider_type == "local": return LocalProvider(config["default"]) else: raise ValueError(f"Unknown provider: {provider_type}")配置文件长这样:
default: type: openai base_url: "https://api.example.com/v1" api_key: "${API_KEY}" model: "gpt-4" timeout: 30 max_retries: 3 fallback: type: local base_url: "http://localhost:8000/v1" model: "qwen-7b"这样切换模型只需要改yaml,不用动代码。重试逻辑我一般用tenacity库,配置指数退避,避免雪崩。
4.3 Agent调度循环的代码实现
Agent的核心循环我简化成这样:
class Agent: def __init__(self, llm, tools, max_steps=10, timeout=30): self.llm = llm self.tools = tools self.max_steps = max_steps self.timeout = timeout def run(self, user_input, history=None): messages = self._build_messages(user_input, history) start_time = time.time() for step in range(self.max_steps): if time.time() - start_time > self.timeout: return "处理超时,请简化问题后重试" response = self.llm.chat(messages) if response.tool_calls: for call in response.tool_calls: result = self._execute_tool(call) messages.append({"role": "tool", "content": result}) else: return response.content return "达到最大步骤限制,未能完成任务"这里的关键是超时和步数双重限制,防止模型陷入死循环。工具执行也要包一层try-except,单个工具失败不能让整个请求挂掉。
4.4 MCP Server的接入与配置
接入一个MCP Server,我以文件读取为例:
from mcp import ClientSession, StdioServerParameters server_params = StdioServerParameters( command="python", args=["mcp_servers/file_server.py"] ) async with ClientSession(server_params) as session: await session.initialize() tools = await session.list_tools() # 把tools注册到Agent的工具列表里配置文件里管理多个Server:
servers: - name: file command: python args: ["mcp_servers/file_server.py"] enabled: true - name: database command: python args: ["mcp_servers/db_server.py"] enabled: false注意:MCP Server的启动命令和参数要写绝对路径,相对路径在不同工作目录下会找不到文件。这个坑我踩过,排查了半天才发现是路径问题。
4.5 RAG检索的落地细节
RAG这块,文档切分策略比模型选择更重要。我的经验是:
- 切分粒度:中文按500-800字切,英文按300-500词切,重叠50-100字
- 向量模型:中文场景用bge-large-zh,英文用text-embedding-3-small
- 检索策略:先向量召回top20,再用rerank模型精排取top5
- 注入方式:把检索结果放在system prompt里,标注来源,让模型引用
def build_rag_prompt(query, docs): context = "\n\n".join([f"[{i+1}] {d['content']}" for i, d in enumerate(docs)]) return f"""基于以下参考资料回答问题,如果资料中没有相关信息,请如实说明。 参考资料: {context} 问题:{query} """实测下来,加了rerank之后答案准确率能提升20%以上,虽然多了一次模型调用,但值得。
5. 常见问题与排查技巧实录
5.1 模型输出格式不稳定怎么办
这是最高频的问题。模型有时候返回JSON,有时候返回Markdown,有时候还给你加一段解释。我的处理方式是三层防护:
- Prompt约束:明确要求“只返回JSON,不要任何其他文字”,并给出示例
- 解析容错:用正则提取JSON部分,解析失败时尝试修复常见问题(比如单引号转双引号)
- 重试机制:解析失败后把错误信息喂回模型,让它重新生成
import json import re def parse_json_response(text): # 尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 尝试提取JSON块 match = re.search(r'\{.*\}', text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return None如果三次都解析失败,就返回一个兜底结构,不要让整个请求报错。
5.2 Agent调用工具时参数传错
模型生成工具调用参数时,经常出现类型不对、字段缺失、格式错误。我的做法是在工具定义里写清楚参数schema,并且在执行前做校验:
def validate_tool_args(tool_schema, args): required = tool_schema.get("required", []) for field in required: if field not in args: return False, f"缺少必填参数: {field}" # 类型校验 for field, value in args.items(): expected_type = tool_schema["properties"][field]["type"] if expected_type == "integer" and not isinstance(value, int): return False, f"参数{field}应为整数" return True, None校验失败时,把错误信息返回给模型,让它重新生成参数。这个机制能解决80%以上的工具调用错误。
5.3 并发上来后响应变慢
这个问题通常有三个原因,按排查优先级:
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 所有请求都慢 | 模型API限流 | 看API返回的429状态码 | 加队列、申请提额 |
| 部分请求慢 | 工具调用阻塞 | 看日志里哪个工具耗时长 | 异步化、加超时 |
| 越来越慢 | 内存泄漏 | 看内存曲线 | 检查会话缓存是否清理 |
我遇到过一次是Redis连接池没设上限,高并发时连接数暴涨导致超时。后来把连接池大小设为CPU核数的2倍,问题解决。
5.4 MCP工具调用失败排查
MCP工具调用失败,我一般按这个顺序查:
- Server是否启动:
ps aux | grep mcp_server看进程在不在 - 通信是否正常:手动跑一下Client连接测试
- 工具是否注册:
list_tools()看返回列表里有没有目标工具 - 参数是否匹配:对比工具schema和实际传参
- 权限是否足够:检查文件路径、数据库账号权限
提示:MCP Server的日志一定要单独输出到文件,不然跟主应用日志混在一起很难排查。我一般给每个Server配一个独立的log文件。
5.5 常见问题速查表
| 问题 | 快速定位 | 解决方向 |
|---|---|---|
| 模型返回空 | 看API响应状态 | 检查token是否超限、prompt是否为空 |
| 工具调用死循环 | 看Agent步数日志 | 加max_steps限制、优化工具描述 |
| 检索结果不相关 | 看召回文档 | 调整切分粒度、加rerank |
| 会话串号 | 看session_id | 检查会话隔离逻辑 |
| 响应超时 | 看各阶段耗时 | 定位瓶颈在模型还是工具 |
6. 一些个人体会和后续扩展方向
做AI应用架构这件事,我最大的体会是:别追求一步到位,要追求快速迭代。我见过太多团队花三个月设计了一套“完美架构”,结果业务需求一变,整个架构推倒重来。更好的做法是先跑通最小闭环,然后根据实际遇到的问题逐步优化。
比如一开始可以不用MCP,直接硬编码几个工具调用;一开始可以不用向量数据库,先用关键词检索;一开始可以不用多Agent协作,先单Agent跑起来。等业务量上来了、痛点明确了,再针对性地引入对应的组件。
后续如果要扩展,我建议从这几个方向考虑:多Agent协作(比如一个负责检索、一个负责生成、一个负责审核)、更精细的权限控制(不同用户能调用的工具不同)、以及可观测性建设(全链路追踪每个请求经过了哪些步骤、消耗了多少token)。这些都是在业务稳定之后值得投入的方向。
最后分享一个小技巧:给每个Agent请求打一个trace_id,从入口一直传到模型调用和工具调用,这样排查问题时能一键串起整个链路。这个习惯帮我省了无数排查时间。