AI应用架构设计实战:从LLM、Agent到MCP的五层架构拆解 1. 从一张架构图说起AI应用到底该怎么搭很多人第一次接触AI应用开发脑子里冒出来的第一个念头是“调个API就完事了”。我刚开始也这么想直到真正把一个AI应用从Demo推到线上、扛住真实用户流量之后才发现事情远没有那么简单。一个能跑的AI应用和一个能用的AI应用之间隔着一整套架构设计的鸿沟。这也是为什么“图解AI应用架构设计”这个主题值得拿出来认真聊一聊——它不是一个纯理论话题而是每个做AI应用的人迟早要面对的现实问题。所谓AI应用架构设计说白了就是回答一个问题当你的应用需要调用大模型LLM来完成某个任务时从用户输入到最终输出中间要经过哪些层、哪些组件、哪些决策点这跟传统的Web应用架构有本质区别。传统应用的核心逻辑是开发者写死的输入A必然得到B而AI应用的核心逻辑有一部分交给了模型同样的输入可能得到不同的输出这就带来了全新的架构挑战。这篇文章适合三类人看第一类是有后端开发经验、想转型做AI应用的工程师第二类是正在做AI产品、需要理解技术架构边界的产品经理第三类是已经搭过简单Demo、但想把系统做得更可靠的开发者。我会从整体架构分层讲起然后逐层拆解核心组件再给出可落地的实操方案最后分享一些踩坑经验。整篇内容会围绕AI原生架构、Agent、LLM、MCP这几个核心概念展开尽量用大白话把每个环节讲透。2. AI应用架构的整体分层与核心思路2.1 为什么不能只有一个“调API”的层最朴素的AI应用长这样前端收集用户输入后端拼接一个Prompt调用LLM的API拿到结果返回给前端。这个结构在Demo阶段完全够用但一旦上线就会暴露一堆问题。比如用户问了一个需要查数据库的问题模型不知道你的数据库里有什么只能瞎编比如模型返回的内容格式不对前端解析直接报错比如同一个用户连续问了好几个相关问题模型每次都是从零开始没有任何上下文记忆。这些问题的根源在于LLM本身只是一个文本生成引擎它不具备记忆、不具备工具调用能力、不具备输出格式约束能力。你要让它变成一个真正能用的应用就必须在它周围搭建一圈“辅助设施”。这圈辅助设施就是AI应用架构的核心内容。我习惯把AI应用架构分成五层来看从下到上依次是模型层、能力层、编排层、应用层、交互层。这个分法不是教科书上的标准答案而是我在实际项目中总结出来的好处是每一层的职责边界比较清晰出了问题容易定位。2.2 五层架构的职责划分模型层是整个架构的地基负责提供基础的文本理解和生成能力。这一层可以是云端API比如各种商用大模型服务也可以是本地部署的开源模型比如通过GGUF格式在本地运行的模型。选择云端还是本地取决于你的数据敏感度、成本预算和延迟要求。能力层是模型和上层应用之间的桥梁它把模型不具备的能力补上。最典型的就是RAG检索增强生成让模型能“看到”你的私有数据还有工具调用Function Calling让模型能执行实际操作以及输出解析把模型的自由文本转成结构化数据。编排层是AI应用架构中最有技术含量的部分也是Agent概念的核心所在。它负责决定“什么时候调模型、什么时候调工具、什么时候该结束”。简单的编排就是一条直线先检索、再生成、最后解析。复杂的编排可能涉及多轮循环、条件分支、并行调用这就是Agent框架要解决的问题。应用层是面向具体业务场景的逻辑比如客服机器人、代码助手、文档分析工具。这一层决定了你的AI应用到底解决什么问题也是产品差异化的来源。交互层是用户直接接触的部分包括对话界面、流式输出、多模态输入输出等。这一层看似简单但实际上流式输出的实现、对话状态的管理、错误提示的设计都有不少讲究。2.3 架构选型的核心权衡在实际做架构设计的时候最容易纠结的几个问题我列一下决策点选项A选项B我的建议模型部署云端API本地部署数据敏感选本地追求效果选云端编排方式硬编码流程Agent框架流程固定用硬编码需要灵活决策用Agent工具协议自定义JSONMCP协议新项目优先考虑MCP生态兼容性好记忆方案对话历史拼接向量数据库短对话用拼接长对话用向量检索输出控制Prompt约束结构化输出API两者结合Prompt兜底API优先这张表里的每一个决策点背后都有一堆细节可以展开。比如MCP协议为什么值得关注因为它解决了一个很实际的问题以前你给模型接一个工具要写一堆适配代码换个模型或换个框架就得重写。MCP把工具的描述和调用方式标准化了理论上你写一次工具所有支持MCP的客户端都能用。这个思路跟当年USB统一接口是一样的——不是技术有多难而是标准化带来的生态效应。3. 核心组件深度拆解LLM、Agent与MCP3.1 LLM在架构中到底扮演什么角色很多人把LLM当成整个应用的大脑这个说法对但不完全对。更准确的说法是LLM是一个概率性的推理引擎它擅长理解和生成但不擅长精确计算和状态管理。理解这一点非常关键因为它决定了你在架构设计中应该把什么交给LLM、把什么留给自己。举个例子用户问“帮我查一下上个月的销售数据然后画个趋势图”。LLM能理解这个意图但它不知道你的销售数据存在哪里也不会画图。所以正确的架构是LLM负责解析意图识别出“查数据”和“画图”两个子任务然后由你的代码去执行数据库查询和图表生成最后LLM负责把结果组织成自然语言回复。在实际选型中LLM的能力差异主要体现在几个维度上下文窗口大小决定了能处理多长的输入、推理能力决定了复杂任务的完成质量、输出稳定性决定了结构化输出的可靠程度、响应速度决定了用户体验、成本决定了你能不能规模化。这几个维度往往是互相矛盾的大窗口通常意味着高成本强推理通常意味着慢速度。所以架构设计的一个核心任务就是根据具体场景找到最合适的平衡点而不是无脑追求最强模型。我自己的做法是分层使用模型简单的意图识别和分类任务用轻量模型复杂的推理和生成任务用重量模型。这样既能控制成本又能保证关键环节的质量。这个思路在业界叫做“模型路由”实现起来并不复杂就是在编排层加一个判断逻辑。3.2 Agent架构从“被动回答”到“主动执行”Agent是这两年AI应用架构领域最热的概念但很多人对它的理解停留在“能调工具的聊天机器人”。这个理解不算错但太浅了。Agent的本质是让LLM具备自主决策能力——它不只是回答一个问题而是能拆解目标、制定计划、执行动作、根据反馈调整策略直到任务完成。一个典型的Agent架构包含四个核心模块规划模块负责把用户的目标拆解成可执行的步骤。最简单的实现是让LLM直接输出步骤列表复杂的实现可能涉及树状搜索、反思机制等。记忆模块负责存储和检索历史信息。短期记忆就是当前对话的上下文长期记忆通常用向量数据库实现让Agent能“记住”之前做过的事情。工具模块负责定义和调用外部能力。这是Agent和普通LLM应用最大的区别——Agent能实际操作外部系统。执行模块负责驱动整个循环决定什么时候调LLM、什么时候调工具、什么时候结束。这四个模块的协作方式决定了Agent的行为模式。我见过很多Agent项目失败的原因不是模型不够强而是执行模块的循环控制没做好——要么陷入死循环不停调工具要么过早结束任务没有完成用户需求。解决这个问题的关键是设置合理的终止条件和最大迭代次数同时在每一轮循环中加入对当前状态的判断。关于Agent开发有一个很容易被忽略的点不是所有任务都需要Agent。如果你的业务流程是固定的比如先检索再生成再格式化那用硬编码的编排就够了引入Agent反而增加了不确定性和调试难度。Agent适合的是那些步骤不固定、需要根据中间结果动态决策的场景比如复杂的客服工单处理、多步骤的数据分析任务。3.3 MCP协议工具调用的标准化尝试MCPModel Context Protocol是Anthropic推出的一个开放协议目的是标准化LLM和外部工具之间的交互方式。在MCP出现之前每个AI应用框架都有自己的工具定义格式你给LangChain写的工具没法直接用在其他框架里迁移成本很高。MCP试图解决的就是这个问题。MCP的核心概念有三个Resources模型可以读取的数据源、Tools模型可以调用的函数、Prompts预定义的提示模板。三者都通过标准化的接口暴露给LLM客户端。这意味着如果你把一个数据库查询功能封装成MCP Server那么任何支持MCP的客户端不管是桌面应用还是IDE插件都能直接调用它。我实际用下来的感受是MCP在工具复用方面的价值确实很大。比如我把常用的几个工具文件读写、网页抓取、数据查询封装成MCP Server之后在不同的AI应用里都能直接接入不用重复写适配代码。但它也有局限性目前的MCP实现对于复杂的工具参数校验和错误处理支持还不够完善在生产环境中需要额外加一层保护。关于MCP的一个常见困惑是它和Function Calling有什么区别简单说Function Calling是模型层面的能力模型能输出结构化的函数调用请求MCP是协议层面的标准定义了工具怎么描述、怎么调用、怎么返回结果。两者是互补关系不是替代关系。4. 实操从零搭建一个可用的AI应用架构4.1 环境准备与技术栈选型在动手之前先把技术栈定下来。以下是我推荐的一套组合适合大多数中小规模的AI应用后端语言Python生态最成熟或TypeScript前后端统一LLM接入直接用官方SDK或通过统一网关如One-API这类聚合层编排框架LangChain/LangGraph功能全或自己写可控性高向量数据库Chroma轻量或Milvus大规模MCP支持官方Python SDK或TypeScript SDK前端React Vercel AI SDK流式输出支持好选型的原则是先用最熟悉的工具把流程跑通再根据瓶颈换组件。我见过太多人在选型阶段纠结太久结果项目还没开始就放弃了。实际上大部分组件在中小规模下差异不大真正的瓶颈往往在Prompt设计和业务流程上。4.2 第一步搭建基础的LLM调用链路先从最简单的开始——一个能调用LLM并返回结果的接口。这里以Python为例import httpx async def call_llm(prompt: str, system: str ) - str: messages [] if system: messages.append({role: system, content: system}) messages.append({role: user, content: prompt}) async with httpx.AsyncClient(timeout60) as client: resp await client.post( https://api.example.com/v1/chat/completions, json{ model: your-model-name, messages: messages, temperature: 0.7, max_tokens: 2048 }, headers{Authorization: Bearer YOUR_KEY} ) data resp.json() return data[choices][0][message][content]这段代码看起来简单但有几个参数值得注意。temperature控制输出的随机性0.7适合创意类任务做数据提取或分类时建议调到0.1以下。max_tokens决定了输出的最大长度设置太小会导致回答被截断设置太大则浪费成本。我的经验是根据任务类型设置不同的参数组合不要一套参数走天下。4.3 第二步加入RAG让模型能“看到”私有数据RAG的核心流程是把文档切块、向量化、存入向量数据库用户提问时先检索相关文档块再把它们拼接到Prompt里一起发给LLM。这个流程听起来简单但每一步都有坑。文档切块是最容易被低估的环节。切得太碎每个块缺乏完整语义切得太大检索精度下降。我的经验值是中文文档每块300-500字英文文档每块200-400词块与块之间保留10%-20%的重叠。重叠的目的是防止关键信息刚好被切在边界上。检索环节的关键是混合检索——不要只用向量相似度还要结合关键词匹配。向量检索擅长语义匹配但对精确的术语、编号、人名不敏感。我通常的做法是向量检索取Top 20关键词检索取Top 20然后用RRF倒数排名融合算法合并取Top 5送给LLM。def rrf_merge(vector_results, keyword_results, k60): scores {} for rank, doc_id in enumerate(vector_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(keyword_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.keys(), keylambda x: scores[x], reverseTrue)这个RRF算法看起来简单但效果比单一检索方式好很多。k值一般取60这是原论文推荐的默认值实际使用中在40-80之间调整都可以。4.4 第三步用MCP封装工具能力接下来把外部工具用MCP协议封装起来。以一个简单的天气查询工具为例from mcp.server import Server from mcp.types import Tool, TextContent server Server(weather-server) server.list_tools() async def list_tools(): return [ Tool( nameget_weather, description查询指定城市的当前天气, inputSchema{ type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } ) ] server.call_tool() async def call_tool(name: str, arguments: dict): if name get_weather: city arguments[city] result await fetch_weather(city) return [TextContent(typetext, textresult)]封装好之后任何支持MCP的客户端都能调用这个工具。这里有个实操心得工具的description字段非常关键LLM就是靠这个描述来判断什么时候该调用哪个工具的。描述要写得具体、明确包含使用场景和参数说明。我见过很多工具调用失败的情况最后发现是description写得太模糊模型根本不知道这个工具是干什么的。4.5 第四步编排层把一切串起来最后一步是把LLM调用、RAG检索、工具调用串成一个完整的流程。这里用一个简化的Agent循环来演示async def agent_loop(user_input: str, max_iterations: int 10): messages [{role: user, content: user_input}] for i in range(max_iterations): # 1. 检索相关上下文 context await retrieve_context(user_input) if context: messages.insert(0, { role: system, content: f参考信息\n{context} }) # 2. 调用LLM response await call_llm_with_tools(messages) # 3. 判断是否需要调用工具 if response.tool_calls: for tool_call in response.tool_calls: result await execute_tool(tool_call) messages.append({ role: tool, content: result, tool_call_id: tool_call.id }) else: # 没有工具调用说明任务完成 return response.content return 任务未在限定步数内完成请简化您的请求。这个循环的核心逻辑是每一轮都让LLM决定下一步做什么直到它认为不需要再调工具为止。max_iterations是必须设置的保险丝防止无限循环。实际使用中大部分任务在3-5轮内就能完成超过10轮通常说明Prompt设计有问题或者任务本身太复杂需要拆分。5. 常见问题与排查技巧实录5.1 模型输出格式不稳定怎么办这是最常见的问题之一。你要求模型输出JSON它有时候输出纯JSON有时候加一堆解释文字有时候JSON格式还写错了。解决这个问题有三个层次的手段第一层是Prompt约束。在System Prompt里明确写“只输出JSON不要任何其他文字”并且给出一个示例。这能解决80%的问题。第二层是结构化输出API。现在很多模型服务都支持JSON Mode或Function Calling格式的强制输出开启之后模型会保证输出合法的JSON。这是最可靠的方式。第三层是代码兜底。不管前面怎么约束解析的时候都要用try-except包住解析失败时尝试用正则提取JSON部分再失败就重试或返回默认值。永远不要假设模型会100%按你的要求输出这是做AI应用的基本觉悟。5.2 Agent陷入死循环怎么排查Agent死循环的表现是反复调用同一个工具或者在不同工具之间来回跳转始终不给出最终答案。排查思路如下先看工具的返回值是不是有问题。如果工具返回了错误信息但Agent没有正确处理它可能会反复重试。检查工具返回的内容是否清晰、是否包含足够的信息让Agent判断下一步。再看System Prompt里有没有明确的终止条件。比如“当你已经获取到足够的信息来回答用户问题时直接给出答案不要再调用工具”。这句话看起来废话但确实能减少很多无意义的工具调用。最后加硬性限制。max_iterations设小一点比如5同时在每一轮循环中检测是否出现了重复的工具调用组合如果连续两轮调用了相同的工具和参数直接强制结束并返回当前结果。5.3 RAG检索结果不相关怎么优化检索不相关通常有三个原因文档切块不合理、Embedding模型不适合你的领域、检索策略太单一。对应的优化手段切块问题前面说过了调整块大小和重叠比例。Embedding模型方面通用模型对专业领域的效果可能不好可以考虑用领域数据微调一个Embedding模型或者选用在多语言/多领域评测中表现好的模型。检索策略方面加入关键词混合检索和重排序Rerank环节。Rerank就是用一个小模型对初步检索结果做精排把最相关的排到前面。这一步会增加一点延迟但对最终效果提升明显。5.4 常见问题速查表问题现象可能原因排查方向解决方案模型输出被截断max_tokens太小检查输出长度增大max_tokens或让模型分段输出工具调用失败description不清晰检查工具定义完善description加参数示例响应速度慢模型太大/检索太慢分环节计时换轻量模型/加缓存/并行检索回答不准确上下文不足/模型幻觉检查检索结果优化RAG/加Few-shot示例成本过高模型选型不当统计token消耗分层用模型/压缩Prompt多轮对话丢失上下文历史未正确传递检查消息组装加入对话历史管理逻辑5.5 几个我踩过的坑第一个坑是过度依赖模型能力。早期我总想让模型自己搞定一切结果发现很多任务用传统代码几行就能解决交给模型反而又慢又不稳定。现在的原则是能用代码精确解决的绝不交给模型。模型只负责它真正擅长的事情——理解自然语言、生成自然语言、做模糊判断。第二个坑是忽略Token成本。刚开始做的时候没在意后来发现每个月的API账单里有一大半是浪费在重复的System Prompt和冗余的上下文上的。优化手段包括压缩System Prompt、对历史对话做摘要而不是全量拼接、缓存重复的检索结果。第三个坑是没有做降级方案。模型服务偶尔会超时或返回错误如果没有降级方案用户就会看到一个报错页面。我的做法是主模型调用失败时自动切换到备用模型备用也失败时返回一个友好的提示而不是错误码。这个逻辑在编排层加一个try-except就能实现但很多项目上线时都忘了做。6. 架构演进从单Agent到多Agent协作当你把单Agent跑通之后很自然会遇到需要多个Agent协作的场景。比如一个复杂的客服系统可能需要一个Agent负责意图识别、一个Agent负责知识检索、一个Agent负责工单创建。这时候架构就变成了多Agent系统。多Agent架构的核心问题是通信和协调。Agent之间怎么传递信息谁来决定哪个Agent先执行出现冲突怎么办目前主流的方案有两种一种是中心化编排有一个主Agent负责调度其他子Agent另一种是去中心化协作Agent之间通过消息队列直接通信。我个人的经验是在大多数业务场景下中心化编排更可控。主Agent负责理解用户意图、拆解任务、分配给子Agent、汇总结果。子Agent只负责自己那一小块任务不需要知道全局状态。这样每个Agent的职责清晰调试起来也容易。多Agent系统的一个常见误区是Agent数量越多越好。实际上每增加一个Agent系统的复杂度和不确定性就增加一分。我的建议是先用单Agent加多工具的方式解决问题实在解决不了再拆多Agent。很多看起来需要多Agent的场景其实一个Agent配上合理的工具集就能搞定。关于Agent的记忆管理还有一个值得展开的点。短期记忆当前对话上下文的管理相对简单就是控制消息列表的长度超长时做摘要或截断。长期记忆跨对话的信息持久化就复杂得多涉及到什么信息值得存、怎么存、怎么检索、什么时候更新。我目前的方案是用向量数据库存事实性信息用户偏好、历史问题用结构化数据库存状态性信息工单状态、任务进度检索时两者结合。这个方案不算完美但在实际项目中够用了。架构设计这件事纸上谈兵永远比动手简单。我最大的体会是先跑通最小闭环再逐步加组件。不要一上来就设计一个完美的五层架构而是从最简单的“调API”开始遇到什么问题加什么层。这样每一步的决策都有实际的痛点驱动而不是为了架构而架构。等你把RAG、工具调用、Agent循环都踩过一遍之后再回头看那些架构图会发现每一层存在的理由都变得非常具体。