1. 从一张架构图说起:AI应用到底该怎么搭
很多人第一次接触AI应用开发,脑子里冒出来的第一个问题就是:我到底该从哪下手?是直接调个大模型接口就完事,还是得搞一套完整的工程体系?这个问题我被人问过不下几十次,每次我都想先扔一张架构图过去,因为光靠嘴说“你要分层、你要解耦、你要考虑上下文管理”,对方大概率是懵的。
“图解AI应用架构设计”这个主题,说白了就是要把一个AI应用从最底层到最上层拆开给你看,每一层干什么、为什么这么分、层与层之间怎么交互,全部用图的方式讲清楚。它解决的核心问题是:当你面对一个“我要做一个AI应用”的需求时,你知道该往哪个方向思考,而不是上来就写代码然后写到一半发现架构根本撑不住。
这篇文章适合谁看?如果你是刚转AI应用方向的开发者,之前只写过传统CRUD业务,那这篇内容能帮你建立完整的架构认知框架。如果你已经做过几个Demo但总觉得不够“工程化”,那这里面的分层思路和实操细节能帮你把项目从“能跑”提升到“可靠”。如果你是个技术负责人,正在评估团队该怎么落地AI应用,那这篇内容可以作为你和团队讨论的底稿。
我自己的经验是,AI应用架构和传统Web应用架构最大的区别在于:传统架构里,业务逻辑是确定的,输入输出是可枚举的;AI应用里,模型输出是不确定的,你需要额外设计一整套机制来兜住这种不确定性。这就是为什么AI应用架构不能照搬传统那套MVC三层结构,你得在中间加很多“缓冲层”和“控制层”。
接下来我会按照一张完整的架构图,从下往上逐层拆解,每一层都会讲清楚它的职责、常见实现方案、以及我在实际项目中踩过的坑。
2. 整体架构分层设计:六层模型拆解
2.1 为什么是六层而不是三层
传统Web应用经典的三层架构——表现层、业务逻辑层、数据访问层——放到AI应用里会出问题。原因很简单:AI应用多了一个“模型推理”这个巨大的不确定性环节,而且这个环节往往还需要和外部工具、知识库、记忆系统打交道。如果你硬把它塞进业务逻辑层,代码会迅速膨胀成一团乱麻。
我推荐的六层模型是这样的:
| 层级 | 名称 | 核心职责 | 典型组件 |
|---|---|---|---|
| L1 | 基础设施层 | 算力、存储、网络、模型托管 | GPU集群、向量数据库、对象存储 |
| L2 | 模型服务层 | 模型推理、微调、版本管理 | LLM API、本地推理引擎、模型网关 |
| L3 | 能力编排层 | 工具调用、流程编排、Agent调度 | MCP、Function Calling、工作流引擎 |
| L4 | 上下文管理层 | 记忆、RAG、会话状态 | 短期记忆、长期记忆、知识库 |
| L5 | 应用逻辑层 | 业务规则、Prompt管理、输出校验 | Prompt模板、输出解析器、护栏 |
| L6 | 交互层 | 用户界面、API网关、多端适配 | Web、App、API、WebSocket |
这个分层不是拍脑袋想出来的。每一层的划分依据是“变更频率”和“职责边界”。基础设施层变更频率最低,交互层变更频率最高。模型服务层和上下文管理层是AI应用特有的,传统架构里没有对应物。
注意:分层不是目的,解耦才是。如果你的项目规模很小,比如就是个内部工具,完全可以把L3和L4合并,没必要为了“架构完整”而过度设计。
2.2 各层之间的数据流与控制流
光有分层还不够,你得想清楚数据怎么流、控制怎么走。我用一个典型场景来说明:用户问“帮我查一下上个月的销售数据并生成图表”。
数据流是这样的:用户输入从L6进入,经过L5做Prompt组装和意图识别,L5把请求发给L3。L3判断这需要调用外部工具(数据库查询)和代码执行(图表生成),于是通过MCP协议调用相应工具。工具返回的数据经过L4的上下文管理模块,决定哪些信息需要保留在短期记忆中。最终结果从L3回到L5做输出格式化,再通过L6返回给用户。
控制流则是反过来的:L5决定要不要调用L3,L3决定调用哪些工具,L4决定记忆的读写时机。控制流的核心原则是“上层决策,下层执行”,不要让下层反向控制上层,否则架构会变得极其难以调试。
2.3 架构选型的三个关键决策点
在实际项目中,有三个决策点会直接影响你的架构形态:
第一个决策点:模型是走API还是本地部署。走API的好处是省事、弹性好,坏处是数据要出域、成本随调用量线性增长。本地部署的好处是数据可控、长期成本低,坏处是运维复杂、GPU资源利用率是个大问题。我的建议是:如果你的日调用量在10万次以下,优先走API;超过这个量级再考虑本地部署,因为这时候成本优势才明显。
第二个决策点:Agent是单Agent还是多Agent。单Agent实现简单,但能力上限低,适合任务边界清晰的场景。多Agent可以分工协作,但通信开销和调试复杂度会指数级上升。我见过太多项目一上来就搞多Agent,结果连单Agent的Prompt都没调明白。
第三个决策点:记忆系统用向量数据库还是关系数据库。向量数据库适合语义检索,关系数据库适合精确查询。实际项目中往往是两者结合:结构化数据走关系库,非结构化知识走向量库,中间加一个路由层做分发。
3. 核心层深度解析:模型服务层与能力编排层
3.1 模型服务层:不只是调个API那么简单
很多人觉得模型服务层就是封装一下大模型API调用,加个重试和超时就行了。这种理解在Demo阶段没问题,但到了生产环境会出大问题。
模型服务层需要解决的核心问题包括:多模型路由、Token成本控制、推理延迟优化、输出格式约束、模型版本管理。我逐个说。
多模型路由是指根据任务类型选择不同的模型。比如简单分类任务用小模型,复杂推理用大模型,代码生成用专门的代码模型。这需要一个路由策略,可以基于规则,也可以基于一个轻量级分类器。我通常会在配置里维护一张“任务类型-模型”的映射表,然后根据请求的元数据自动路由。
Token成本控制是个容易被忽视的问题。大模型的计费是按Token算的,如果你不做控制,一个月账单可能比你服务器费用还高。控制手段包括:设置单次请求的最大Token数、对长文本做摘要后再送入模型、缓存高频请求的结果。我实测下来,光是加一个结果缓存层,就能省掉30%左右的重复调用。
输出格式约束是另一个关键点。大模型的输出是自然语言,但你的下游系统可能需要JSON。这时候你需要用结构化输出功能,或者在Prompt里严格约束格式,再加一层输出解析器做校验。千万不要相信模型每次都能输出合法JSON,一定要有兜底逻辑。
# 一个简单的模型路由示例 MODEL_ROUTING = { "classification": "small-model", "reasoning": "large-model", "code_generation": "code-model", "summarization": "medium-model" } def route_model(task_type, token_count): if token_count > 8000: return "long-context-model" return MODEL_ROUTING.get(task_type, "default-model")3.2 能力编排层:Agent和MCP到底怎么配合
能力编排层是AI应用架构里最“AI”的一层,也是最近一年变化最快的一层。核心概念就两个:Agent和MCP。
Agent是什么?简单说就是一个能自主决策调用哪些工具、按什么顺序调用的调度器。它接收一个目标,然后自己规划步骤、执行、观察结果、调整策略,直到目标完成或失败。Agent的核心组件包括:规划器、工具集、执行器、记忆模块。
MCP是什么?Model Context Protocol,可以理解为一套标准化的工具调用协议。它的价值在于把工具的定义和调用方式标准化,这样不同的Agent框架、不同的模型都可以用同一套接口来调用工具。没有MCP之前,每个框架都有自己的Function Calling格式,换个框架就得重写一遍工具适配层。有了MCP之后,工具提供方只需要实现一次MCP Server,所有支持MCP的客户端都能直接用。
Agent和MCP的关系可以这样理解:Agent是决策者,MCP是执行通道。Agent决定“我要查数据库”,然后通过MCP协议把查询请求发给数据库的MCP Server,Server执行完把结果返回给Agent。
实操心得:MCP Server的粒度不要切得太细。我见过有人把每个API都封装成一个MCP Server,结果Agent要调一个功能得连着调五六个Server,延迟高得离谱。合理的做法是按业务域来切,比如“用户服务”一个Server、“订单服务”一个Server。
3.3 上下文管理层:记忆和RAG的工程实现
上下文管理层解决的是“模型该知道什么”的问题。它包含三个子模块:短期记忆、长期记忆、知识检索。
短期记忆就是当前会话的上下文窗口。实现上就是一个滑动窗口,保留最近N轮对话。但这里有个坑:不是所有历史消息都值得保留。我的做法是给每条消息打一个“重要性分数”,分数低的优先被挤出窗口。重要性可以基于消息长度、是否包含关键实体、是否是用户显式强调的内容来计算。
长期记忆是跨会话的信息持久化。比如用户上次说“我对海鲜过敏”,这次点餐时模型应该记得。实现上通常用向量数据库存储记忆片段,每次新会话开始时检索相关记忆注入上下文。这里的关键是记忆的写入时机和检索策略。写入太频繁会污染记忆库,检索太宽泛会引入无关信息。
知识检索就是常说的RAG。RAG的工程实现有很多细节:文档怎么切块、Embedding模型怎么选、检索结果怎么重排序、检索不到怎么办。我踩过最大的坑是切块策略。一开始按固定长度切,结果把一段完整的逻辑切成了两半,检索出来的是残缺信息。后来改成按语义边界切,效果好了很多。
# 语义切块的简化逻辑 def semantic_chunk(text, max_chunk_size=500): paragraphs = text.split("\n\n") chunks = [] current_chunk = "" for para in paragraphs: if len(current_chunk) + len(para) > max_chunk_size: chunks.append(current_chunk) current_chunk = para else: current_chunk += "\n\n" + para if current_chunk: chunks.append(current_chunk) return chunks4. 实操落地:从零搭一个可用的AI应用骨架
4.1 环境准备与技术栈选型
在动手之前,先把技术栈定下来。我的推荐方案是这样的:
- 模型服务:优先用主流云厂商的API,本地部署用vLLM或Ollama
- 编排框架:LangChain或LlamaIndex,轻量场景可以直接手写
- MCP实现:官方SDK,Python和TypeScript都有
- 向量数据库:Qdrant或Milvus,小规模用Chroma也行
- 后端框架:FastAPI,异步支持好
- 前端:React或Vue,流式输出用SSE
这套组合的好处是生态成熟、文档齐全、社区活跃。坏处是LangChain的抽象层有时候会挡住你调底层参数,遇到诡异问题得扒源码。
注意:不要一上来就引入太多框架。我见过一个项目同时用了LangChain、LlamaIndex、Haystack三个框架,结果三个框架的依赖冲突搞得环境都跑不起来。选一个主框架,其他需求手写。
4.2 搭建模型服务层的最小实现
先写一个模型服务层的最小可用版本。核心功能:接收请求、路由模型、调用API、处理错误、返回结果。
import httpx from tenacity import retry, stop_after_attempt, wait_exponential class ModelService: def __init__(self, config): self.config = config self.client = httpx.AsyncClient(timeout=60.0) @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10)) async def call_model(self, prompt, task_type="default", max_tokens=2000): model = self.route_model(task_type) payload = { "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.7 } response = await self.client.post( self.config["api_endpoint"], json=payload, headers={"Authorization": f"Bearer {self.config['api_key']}"} ) response.raise_for_status() return response.json()["choices"][0]["message"]["content"] def route_model(self, task_type): return self.config["model_routing"].get(task_type, self.config["default_model"])这段代码看起来简单,但有几个关键点:重试策略用了指数退避,避免雪崩;超时设了60秒,因为大模型推理确实慢;路由逻辑抽成了独立方法,方便后续扩展。
4.3 接入MCP工具流的完整流程
MCP的接入流程分三步:定义工具、启动Server、客户端连接。
先定义工具。一个MCP工具需要声明名称、描述、参数Schema。描述很重要,Agent就是靠描述来判断什么时候该调用这个工具。
# MCP工具定义示例 tools = [ { "name": "query_sales_data", "description": "查询指定时间范围的销售数据,返回JSON格式结果", "parameters": { "type": "object", "properties": { "start_date": {"type": "string", "description": "开始日期,格式YYYY-MM-DD"}, "end_date": {"type": "string", "description": "结束日期,格式YYYY-MM-DD"}, "region": {"type": "string", "description": "地区筛选,可选"} }, "required": ["start_date", "end_date"] } } ]然后启动MCP Server。Server负责接收调用请求、执行实际逻辑、返回结果。最后客户端通过MCP协议连接Server,把工具列表注册到Agent的工具集中。
实操心得:工具描述要写得像给新人看的文档,把“什么时候用”“什么时候不用”“参数怎么填”都写清楚。我试过把描述写得很简略,结果Agent该调的时候不调,不该调的时候乱调。
4.4 上下文管理的落地细节
上下文管理落地时,我建议先做一个简单的版本,再逐步迭代。
第一版:只做短期记忆,用滑动窗口保留最近10轮对话。这个版本能覆盖80%的简单场景。
第二版:加入长期记忆,用向量数据库存储用户偏好和历史决策。每次会话开始时检索Top-5相关记忆注入System Prompt。
第三版:加入RAG,把领域知识库接进来。检索时先用关键词过滤缩小范围,再做语义检索,最后用重排序模型精排。
# 上下文组装示例 def build_context(user_input, session_id, knowledge_base): # 短期记忆 recent_messages = get_recent_messages(session_id, limit=10) # 长期记忆检索 long_term_memories = retrieve_memories(user_input, top_k=5) # 知识库检索 knowledge_chunks = knowledge_base.search(user_input, top_k=3) # 组装 context = { "system": SYSTEM_PROMPT, "memories": long_term_memories, "knowledge": knowledge_chunks, "history": recent_messages, "current_input": user_input } return context4.5 输出校验与护栏机制
AI应用的输出不能直接透传给用户,中间必须加护栏。护栏分三层:格式校验、内容校验、安全校验。
格式校验确保输出符合预期结构。如果要求JSON,就用JSON Schema校验,不通过就触发重试或降级。
内容校验检查输出是否包含幻觉信息。简单做法是让模型自己标注置信度,复杂做法是用另一个模型做事实核查。
安全校验过滤敏感内容。可以用关键词黑名单,也可以用分类模型。
import json from jsonschema import validate, ValidationError def validate_output(output, schema): try: parsed = json.loads(output) validate(instance=parsed, schema=schema) return parsed, None except (json.JSONDecodeError, ValidationError) as e: return None, str(e) def safe_generate(prompt, schema, max_retries=2): for attempt in range(max_retries + 1): output = model_service.call_model(prompt) parsed, error = validate_output(output, schema) if parsed: return parsed prompt = f"上次输出格式错误:{error}。请重新输出合法JSON。" raise ValueError("输出校验失败,已达最大重试次数")5. 常见问题与排查技巧实录
5.1 模型输出不稳定怎么排查
这是最高频的问题。同一个Prompt,有时候输出很好,有时候完全跑偏。排查思路按优先级来:
第一步,检查temperature参数。如果设得过高(比如1.0以上),输出随机性会很大。生产环境建议设在0.1到0.3之间,需要创意时再调高。
第二步,检查Prompt是否有歧义。把Prompt给一个同事看,问他“你觉得这个要求明确吗”。如果他有疑问,模型大概率也会困惑。
第三步,检查上下文是否过长。上下文超过模型窗口的80%时,模型对中间部分的注意力会下降。这时候需要做上下文压缩。
第四步,检查是否有冲突指令。System Prompt说“回答要简洁”,User Prompt说“详细解释”,模型就会摇摆。
5.2 Agent不调用工具或乱调用工具
这个问题我遇到过很多次,原因通常有三个:
工具描述不够清晰。Agent靠描述来判断调用时机,描述模糊就会误判。解决办法是把描述写具体,包含使用场景和反例。
工具数量太多。当工具超过15个时,Agent的选择准确率会明显下降。解决办法是做工具分组,先让Agent选组,再选具体工具。
缺少调用示例。在System Prompt里给几个“什么情况调用什么工具”的示例,能显著提升准确率。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 该调不调 | 描述模糊 | 人工读描述判断 | 补充使用场景 |
| 乱调 | 工具过多 | 统计工具数量 | 分组或裁剪 |
| 调了报错 | 参数Schema错误 | 检查Schema定义 | 修正参数类型 |
| 循环调用 | 缺少终止条件 | 查看调用日志 | 加最大调用次数 |
5.3 上下文丢失与记忆污染
上下文丢失的表现是:用户明明之前说过,模型却忘了。原因可能是滑动窗口太小、记忆检索没命中、或者记忆写入失败。
记忆污染的表现是:模型引用了错误的记忆。原因通常是检索策略太宽泛,把不相关的记忆也注入了。
我的处理经验是:给记忆加时间衰减和相关性阈值。时间越久的记忆权重越低,相关性分数低于阈值的直接丢弃。另外,重要记忆要支持手动置顶,避免被挤出窗口。
5.4 性能瓶颈定位与优化
AI应用的性能瓶颈通常在这几个地方:模型推理延迟、工具调用延迟、向量检索延迟、网络传输延迟。
定位方法是在每个环节打点计时,找出耗时最长的环节。优化手段包括:模型推理用流式输出降低首字延迟、工具调用做并行化、向量检索加缓存、网络传输用连接池。
实操心得:流式输出对用户体验的提升非常明显。即使总耗时不变,首字延迟从3秒降到0.5秒,用户感知就是“快了很多”。
5.5 成本失控的预防与治理
成本失控通常发生在项目上线后的一到两个月。预防措施包括:设置每日预算上限、监控Token消耗趋势、对高频请求做缓存、定期审查Prompt的Token效率。
治理手段:先做用量分析,找出消耗最大的Top 10请求模式;然后针对性优化,比如把长Prompt做压缩、把简单任务路由到小模型、把重复请求做缓存。我实测下来,这三招组合用,通常能省40%到60%的成本。
6. 架构演进与扩展方向
6.1 从单Agent到多Agent的演进路径
单Agent跑通之后,什么时候该上多Agent?我的判断标准是:当单Agent的工具集超过20个,或者单次任务需要超过10步推理时,就该考虑拆分了。
演进路径建议分三步走:第一步,按业务域拆分Agent,每个Agent负责一个域的工具和逻辑;第二步,加一个协调Agent做任务分发和结果汇总;第三步,引入Agent间的通信协议,支持Agent互相调用。
注意:多Agent的调试复杂度远高于单Agent。上多Agent之前,确保单Agent的日志和追踪体系已经完善,否则出了问题根本不知道是哪个Agent的锅。
6.2 模型升级与迁移的平滑方案
模型升级是必然的,但升级不能影响线上服务。平滑方案的核心是灰度切换:新模型先接10%的流量,对比输出质量和延迟,没问题再逐步放大比例。
同时要做好Prompt的兼容性测试。不同模型对同一个Prompt的理解可能有差异,升级前必须跑一遍回归测试集。
6.3 可观测性体系的搭建
AI应用的可观测性比传统应用更重要,因为它的行为更不可预测。需要采集的数据包括:每次请求的完整上下文、模型输出、工具调用记录、耗时分布、Token消耗、错误率。
这些数据要能关联起来,出问题时能快速定位是哪个环节出了问题。我通常用OpenTelemetry做链路追踪,把模型调用和工具调用都做成Span,这样一眼就能看出时间花在哪了。
架构设计这件事,纸上谈兵容易,落地时全是细节。我自己的体会是,先把最小可用版本跑起来,然后在真实流量中发现问题、迭代架构,比一开始就追求完美设计要靠谱得多。每次架构调整都应该由具体的痛点驱动,而不是为了“看起来更专业”。