1. 从一张架构图说起:AI应用到底该怎么搭
这两年我参与过不少AI应用项目的评审和落地,发现一个特别普遍的现象:很多人一上来就急着写Prompt、调API、接模型,结果项目跑到一半发现整个系统像一团乱麻——模型换了要改几十处代码,加个新工具要重写整个调用链,多个Agent之间互相调用直接死锁。说到底,问题出在动手之前没有把架构想清楚。
“图解AI应用架构设计”这个主题,核心就是解决一个问题:当你要做一个AI应用时,系统应该分成哪几层、每层负责什么、层与层之间怎么交互。它适合正在做AI应用开发的工程师、正在带团队做技术选型的架构师,以及想从传统开发转向AI应用开发但不知道从哪里下手的程序员。不管你用的是LLM、Agent还是MCP,架构设计的底层逻辑是相通的。
我个人的习惯是,在写第一行代码之前,先在白板上画出一张架构图。这张图不需要多漂亮,但必须回答几个关键问题:模型层怎么抽象、工具层怎么注册、Agent之间怎么通信、状态怎么管理、异常怎么兜底。这篇文章我就把这张图拆开来讲,从整体分层到每一层的实现细节,再到实际踩过的坑,尽量说透。
2. AI应用架构的整体分层与设计思路
2.1 为什么不能把LLM调用散落在业务代码里
我见过最糟糕的一种写法,是在Controller里直接写OpenAI的SDK调用,然后在Service里又写一遍,定时任务里再来一遍。这种代码在Demo阶段没问题,一旦要换模型供应商、要加缓存、要做限流、要记录Token消耗,你就得满世界找调用点。
架构设计的第一原则就是关注点分离。LLM调用属于基础设施层面的事情,不应该和业务逻辑混在一起。我的做法是抽象出一个Model Gateway层,所有对LLM的请求都经过这一层。这一层负责的事情包括:统一不同供应商的API差异、处理重试和降级、统计Token用量、做请求级别的缓存、管理API Key的轮转。
这样做的好处很直接:业务代码只关心“我要让模型做什么”,不关心“模型是谁家的、怎么调”。换模型的时候只改Gateway的配置,业务代码一行不动。这个思路和传统后端开发里把数据库访问抽象成Repository层是一模一样的道理。
2.2 四层架构的划分逻辑
基于多个项目的实践,我把AI应用的架构划分为四层,从下往上依次是:
- 模型接入层:负责与各种LLM供应商对接,统一接口协议,处理认证、限流、重试、降级
- 能力编排层:负责Prompt模板管理、工具注册与调用、Agent的推理循环、MCP协议适配
- 业务逻辑层:具体的业务场景实现,比如客服问答、代码生成、文档分析
- 交互接入层:对外暴露的API、WebSocket、SSE流式输出,以及前端交互
这四层的依赖关系是单向的,上层依赖下层,下层不知道上层的存在。模型接入层完全不知道业务逻辑层在做什么,它只负责把请求发出去、把结果拿回来。这种单向依赖保证了每一层都可以独立替换和测试。
注意:不要为了“分层”而分层。如果你的应用只有一个简单的问答场景,不需要Agent编排,那能力编排层可以很薄,甚至直接合并到业务逻辑层。架构是为了解决问题,不是为了好看。
2.3 Agent与LLM在架构中的位置关系
很多人搞不清楚Agent和LLM在架构中的关系。用一句话说:LLM是Agent的推理引擎,Agent是LLM的能力扩展。
LLM本身只能做文本生成,它不能查数据库、不能调API、不能读文件。Agent做的事情,是在LLM外面套一个循环:让LLM决定下一步做什么,然后执行这个动作,把结果喂回给LLM,再让它决定下一步。这个循环就是所谓的ReAct模式或者Function Calling模式。
在架构图上,Agent属于能力编排层。它依赖模型接入层提供的LLM能力,同时依赖工具注册中心提供的工具能力。一个Agent可以调用多个工具,一个应用也可以有多个Agent协同工作。MCP在这里的角色,是工具注册和调用的一种标准化协议,让不同来源的工具能够以统一的方式被Agent发现和使用。
3. 模型接入层的核心细节与实操要点
3.1 统一接口抽象的关键设计
模型接入层的核心是定义一个统一的接口。不管你后面接的是哪家的模型,业务层看到的都是同一个方法签名。我通常定义的接口大概长这样:
class ModelGateway: def chat(self, messages, tools=None, stream=False, **kwargs): ... def embed(self, texts): ...chat方法接收消息列表和可选的工具定义,返回模型的响应。stream参数控制是否流式输出。embed方法用于文本向量化。
这个接口看起来简单,但要处理好几个细节。第一,不同供应商的message格式不一样,有的用role字段区分system/user/assistant,有的用单独的字段。Gateway内部要做格式转换。第二,工具调用的返回格式差异更大,有的返回tool_calls数组,有的返回function_call对象。Gateway要统一成一种格式。第三,流式输出的chunk结构也不同,要统一成一致的增量格式。
3.2 多模型路由与降级策略
实际生产环境中,只用一个模型是不够的。原因很简单:贵的不一定适合所有场景,便宜的不一定能处理复杂任务。我的做法是在Gateway层实现一个路由策略:
| 场景类型 | 首选模型 | 降级模型 | 路由依据 |
|---|---|---|---|
| 简单分类/抽取 | 小模型 | 中模型 | 请求复杂度评分 |
| 复杂推理/规划 | 大模型 | 中模型 | 任务类型标记 |
| 高并发低延迟 | 小模型 | 队列缓冲 | 实时负载 |
| 代码生成 | 代码专用模型 | 通用大模型 | 内容类型识别 |
路由的依据可以来自请求的元数据,也可以由业务层显式指定。降级策略要配合重试机制一起用:当首选模型返回超时或错误时,自动切换到降级模型,同时记录降级事件用于后续分析。
实操心得:降级不是万能的。如果首选模型是因为内容审核被拒,降级到另一个模型大概率也会被拒。所以降级策略要区分错误类型,只对超时、限流、服务不可用这类错误做降级,内容层面的错误直接返回给业务层处理。
3.3 Token用量统计与成本控制
Token统计这件事,看起来简单,做起来容易漏。因为流式输出的Token计数和非流式不一样,工具调用的Token计算也有特殊规则。我的做法是在Gateway层统一拦截请求和响应,从响应中提取usage字段。对于流式输出,在最后一个chunk里通常会有usage信息,如果没有就自己估算。
成本控制方面,我建议在Gateway层做三件事:设置单次请求的Token上限、设置单用户的日Token配额、对超长上下文做自动截断或摘要。这三件事都不复杂,但能避免很多意外账单。
4. 能力编排层的实现要点
4.1 Prompt模板管理不能硬编码
Prompt是AI应用的核心资产之一,但很多项目把Prompt直接写在代码里,改一个标点都要重新部署。我的做法是把Prompt模板抽出来,用独立的文件管理,支持变量替换和版本控制。
模板文件的结构大概是这样的:
name: customer_service_reply version: 3 variables: - user_name - order_info - knowledge_snippets template: | 你是一位专业的客服助手。当前用户是{{user_name}}。 以下是用户的相关订单信息:{{order_info}} 以下是知识库中匹配到的内容:{{knowledge_snippets}} 请根据以上信息,用友好的语气回复用户的问题。这样做的好处是,Prompt的修改不需要改代码,运营人员也能参与优化。版本控制让每次修改都可追溯,A/B测试也变得容易。
4.2 工具注册与MCP协议适配
工具是Agent的手和脚。在架构设计上,工具应该有一个统一的注册中心,每个工具声明自己的名称、描述、参数Schema和执行函数。Agent在推理时,根据工具的描述来决定是否调用。
MCP协议在这里的价值就体现出来了。它定义了一套标准的工具描述和调用格式,让不同来源的工具能够以统一的方式接入。你可以把MCP理解成工具界的USB接口——不管什么设备,插上就能用。
在架构上,我通常会在能力编排层放一个MCP Client,负责与MCP Server通信。MCP Server可以是本地的,也可以是远程的。本地Server适合文件操作、数据库查询这类需要低延迟的工具,远程Server适合第三方服务集成。
# 工具注册的简化示例 tool_registry = {} def register_tool(name, description, parameters): def decorator(func): tool_registry[name] = { "name": name, "description": description, "parameters": parameters, "func": func } return func return decorator @register_tool( name="query_order", description="根据订单号查询订单状态", parameters={"order_id": {"type": "string", "description": "订单编号"}} ) def query_order(order_id): # 实际查询逻辑 return {"status": "shipped", "eta": "2025-01-20"}4.3 Agent推理循环的设计与超时控制
Agent的核心是一个循环:思考、行动、观察、再思考。这个循环必须有终止条件,否则会无限执行下去。我通常设置三个终止条件:达到最大迭代次数、模型输出最终答案、超过总超时时间。
超时控制特别重要。我遇到过Agent在某个工具调用上卡住,整个请求挂了五分钟的情况。后来我在每个工具调用外面包了一层超时控制,单个工具调用超过10秒就中断,返回超时错误让模型决定下一步。
注意:Agent的迭代次数不要设太大。一般3到5次就够了。超过5次还没得到答案,大概率是工具描述有问题或者任务本身不适合用Agent解决。设太大只会浪费Token和时间。
5. 业务逻辑层与交互层的落地实践
5.1 业务逻辑层的边界划分
业务逻辑层是最贴近具体场景的一层。我的原则是:一个业务场景一个模块,模块之间不直接调用。比如客服问答模块和文档分析模块,它们各自独立,如果需要协作,通过能力编排层的Agent来协调。
这样做的好处是每个模块可以独立开发、独立测试、独立部署。一个模块出问题不会影响其他模块。对于团队协作来说,不同的人可以负责不同的业务模块,只要接口约定好就行。
业务逻辑层还需要处理一件事:上下文管理。多轮对话中,哪些历史消息要保留、哪些要丢弃、什么时候做摘要压缩,这些策略应该在业务层定义,而不是在模型层。因为不同业务对上下文的需求不一样,客服场景可能需要保留最近10轮对话,而代码生成场景可能只需要保留当前文件和最近一次修改。
5.2 流式输出的架构考量
流式输出是AI应用体验的关键。用户不需要等模型生成完整回答再看到内容,而是一个字一个字地看到回答逐渐出现。在架构上,流式输出意味着数据要从模型层一路透传到前端,中间任何一层做了缓冲都会破坏流式效果。
我的做法是在交互层使用SSE(Server-Sent Events)或WebSocket,在业务层和编排层使用异步生成器,在模型层使用流式API。每一层都支持逐块传递数据,不做全量缓冲。
但流式输出也带来一个问题:如果生成到一半出错了怎么办?我的处理方式是,在流式输出的同时,在服务端保留完整的生成内容。如果中途出错,已经发送给前端的部分无法撤回,但可以在流结束后发送一个错误事件,让前端决定是保留已生成内容还是提示重试。
5.3 并发场景下的架构调整
AI应用扛并发和传统Web应用扛并发,思路不太一样。传统应用瓶颈通常在数据库,AI应用的瓶颈在模型推理。模型推理的延迟高、成本高、并发能力有限,所以架构上要做针对性优化。
我常用的几个手段:请求队列,把并发请求排队处理,避免瞬时打爆模型服务;结果缓存,对相同或相似的请求缓存结果,减少重复推理;异步处理,对不需要实时返回的任务用消息队列异步处理;分级服务,对延迟敏感的用户走快速通道,对延迟不敏感的用户走批量通道。
这些手段在架构上都需要在能力编排层和业务逻辑层之间增加相应的组件。具体用哪些,取决于你的业务场景和成本预算。
6. 常见问题与排查技巧实录
6.1 模型返回格式不稳定怎么办
这是最常见的问题之一。你让模型返回JSON,它有时候返回纯JSON,有时候在JSON外面包一层Markdown代码块,有时候还加几句解释。我的处理方式是三层防护:第一,在Prompt里明确要求只返回JSON,不要加任何其他内容;第二,在解析时先尝试直接解析,失败则提取代码块内容再解析;第三,如果还失败,用一个小模型做格式修复。
但最根本的解决办法是使用支持结构化输出的模型API。现在很多模型都提供了JSON Mode或Function Calling模式,能从API层面保证返回格式。如果你的场景对格式要求严格,优先用这个。
6.2 Agent陷入死循环的排查思路
Agent死循环的表现是反复调用同一个工具,或者在不同工具之间来回跳转。排查的时候我一般按这个顺序看:
| 排查项 | 可能问题 | 解决方法 |
|---|---|---|
| 工具描述 | 描述模糊导致模型误解 | 补充使用场景和参数说明 |
| 工具返回值 | 返回内容太长或格式混乱 | 精简返回值,只保留关键信息 |
| 系统Prompt | 缺少终止条件说明 | 明确告知模型何时应该停止 |
| 最大迭代数 | 设置过大 | 降到3-5次 |
| 任务本身 | 任务不适合Agent模式 | 改用固定流程 |
我踩过最坑的一次是工具返回了一个巨大的JSON,模型每次都要花大量Token去理解返回值,结果在同一个工具上反复调用。后来把返回值精简到只保留必要字段,问题就解决了。
6.3 MCP连接失败的常见原因
MCP连接失败通常有几个原因:Server没有启动、端口被占用、认证信息配置错误、协议版本不匹配。排查的时候先确认Server进程是否在运行,然后检查网络连通性,再看认证配置。
实操心得:MCP Server的日志一定要打开。很多连接问题在Server端日志里一目了然,比如“收到不支持的协议版本”或者“认证Token已过期”。不要只盯着Client端的报错看。
6.4 流式输出中断的排查
流式输出中断的原因比较多:网络超时、模型服务端主动断开、中间层缓冲导致连接被回收、前端处理不过来。排查的时候先确认是模型层断开还是网络层断开。如果是模型层断开,看模型服务的日志;如果是网络层断开,检查Nginx或网关的超时配置。
我遇到过一次是Nginx的proxy_read_timeout默认60秒,而模型生成一个长回答超过了60秒,Nginx主动断开了连接。把超时调到300秒就好了。这种问题不看Nginx日志根本想不到。
7. 架构演进与扩展方向
7.1 从单Agent到多Agent协作
当业务复杂度上升,单个Agent可能搞不定所有事情。这时候需要多个Agent分工协作。比如一个客服系统,可以有意图识别Agent、知识检索Agent、回复生成Agent、质量审核Agent。每个Agent专注一件事,通过消息传递来协作。
多Agent架构的关键是通信协议和状态管理。Agent之间怎么传递消息、怎么共享上下文、怎么处理某个Agent失败的情况,这些都需要在架构设计阶段想清楚。我的建议是先用简单的消息队列做Agent间通信,不要一上来就搞复杂的黑板模型。
7.2 可观测性建设
AI应用的可观测性和传统应用不太一样。除了常规的QPS、延迟、错误率,还需要关注:Token消耗趋势、模型调用分布、Agent迭代次数分布、工具调用成功率、Prompt版本效果对比。
这些指标需要在架构的每一层埋点。模型接入层记录Token和延迟,能力编排层记录Agent迭代和工具调用,业务逻辑层记录业务指标。所有数据汇总到一个可观测性平台,方便排查问题和优化成本。
7.3 安全与合规的架构考量
Agent安全是最近被讨论很多的话题。从架构层面,我建议做几件事:工具调用白名单,只允许Agent调用注册过的工具;输入输出内容过滤,防止Prompt注入和敏感信息泄露;操作审计日志,记录每个Agent的每一步决策和工具调用;权限隔离,不同Agent有不同的工具访问权限。
这些措施不需要一开始就全部到位,但在架构设计时要预留扩展点。比如工具注册中心要支持权限标记,Agent执行器要支持审计日志接口。后期加上去比一开始设计好要麻烦得多。
架构设计这件事,说到底是在“够用”和“过度设计”之间找平衡。我见过太多项目在早期就搞了一套复杂的多Agent编排系统,结果业务量根本撑不起来,维护成本反而成了负担。也见过一些项目完全不做抽象,模型调用散落各处,后期想换个模型比登天还难。我的经验是,先画出你当前能想到的最简单的架构图,然后问自己三个问题:换模型要改几处?加工具要改几处?扛不住并发时哪里是瓶颈?如果这三个问题的答案都在可接受范围内,那就够了。架构是演进来的,不是设计出来的。