图解AI应用架构设计:从模型接入到Agent编排的五层架构实践

发布时间:2026/10/26 9:01:07
图解AI应用架构设计:从模型接入到Agent编排的五层架构实践

1. 从一张架构图说起:AI应用到底该怎么搭

很多人第一次接触AI应用开发,脑子里冒出来的第一个问题往往是"我该用哪个模型",而不是"我的系统该怎么分层"。这个顺序其实反了。模型只是整个系统里的一块拼图,真正决定一个AI应用能不能上线、能不能稳定跑、能不能在业务量涨十倍的时候不崩的,是它背后的架构设计。

"图解AI应用架构设计"这个题目,核心不是教你画一张好看的框图,而是把AI应用从输入到输出、从模型调用到工具编排、从单轮问答到多轮Agent协作的整条链路拆开,让你看清楚每一层在干什么、层与层之间怎么通信、哪些地方容易出问题。它解决的是"我知道大模型很厉害,但不知道怎么把它变成一个能用的产品"这个最普遍的困惑。适合谁看?刚入门AI应用开发的工程师、想从传统后端转AI方向的开发者、以及需要给团队做技术选型的技术负责人,都能从这套分层思路里找到自己需要的那一块。

我做了几年AI应用落地,踩过的坑基本都集中在"架构没想清楚就开干"这件事上。一开始觉得调个API返回结果就完事了,后来发现要加工具调用、要加记忆、要加多轮编排、要做流式输出、要处理超时和重试,代码很快就变成一团乱麻。所以这篇文章我会按照一张完整的AI应用架构图来展开,从最底层的模型接入层一路讲到最上层的应用编排层,每一层都讲清楚它为什么存在、怎么实现、有哪些坑。关键词像Agent、LLM、MCP、AI应用开发这些,我会在对应的章节里自然展开,不会堆砌概念,而是讲它们在架构里到底扮演什么角色。

2. AI应用架构的整体分层思路

2.1 为什么AI应用需要分层

传统Web应用的经典分层是"接入层-业务层-数据层",这套东西大家都很熟。但AI应用多了一个非常特殊的东西:模型。模型不是一个普通的依赖库,它是一个有不确定性、有延迟、有成本、有上下文限制的外部能力。你没法像调用一个本地函数那样调用它,它的输出可能每次都不一样,它可能超时,它可能因为上下文太长而截断,它可能因为提示词写得不好而胡言乱语。

正因为模型有这些特性,AI应用的架构必须把"模型相关的东西"单独抽出来管理。这就自然形成了几个层次:最底下是模型接入层,负责和各家大模型API打交道;往上是能力层,负责提示词管理、工具调用、记忆存储;再往上是编排层,负责把多个能力串成一个完整的任务流程,这就是Agent所在的位置;最上面是应用层,负责和用户交互、处理业务逻辑。

我见过不少项目把这几层揉在一起,结果就是换个模型要改几十个文件,加个工具要动核心逻辑,调试的时候根本不知道问题出在哪一层。分层不是为了好看,是为了让每一层的变化不影响其他层。模型换了,只改接入层;工具加了,只改能力层;流程变了,只改编排层。这个隔离性在AI应用里比在传统应用里更重要,因为AI这块的技术栈变化太快了,今天用的模型明天可能就被更好的替代了。

2.2 一张图看懂五层架构

我把AI应用的架构分成五层,从下往上依次是:

层级名称核心职责典型组件
L1模型接入层统一封装各家LLM API,处理鉴权、重试、限流OpenAI SDK、各家模型SDK、自建网关
L2能力层提示词管理、工具定义、记忆存储、RAG检索Prompt模板、Function Calling、向量库
L3编排层任务分解、多步执行、Agent循环、状态管理LangGraph、自研编排引擎
L4应用层业务逻辑、用户交互、权限控制、会话管理Web框架、API网关
L5基础设施层日志、监控、缓存、队列、部署可观测性工具、消息队列、容器

这张图的关键在于:每一层只和相邻层通信,不跨层调用。应用层不直接调模型API,而是通过编排层;编排层不直接拼提示词,而是通过能力层。这样做的好处是每一层都可以独立测试、独立替换。比如你想从一家模型换到另一家,只需要在L1改适配器,上面四层完全不用动。

2.3 分层设计的核心取舍

分层不是没有代价的。每多一层就多一次函数调用、多一层抽象、多一点理解成本。对于那种就是简单调个模型返回结果的场景,搞五层架构纯属过度设计。所以分层要看你应用的复杂度。

我的经验判断标准是这样的:如果你的应用只是单轮问答,没有工具调用、没有多轮记忆、没有复杂流程,那两层就够了——一个模型接入封装加一个业务处理。但只要你开始涉及工具调用(比如让模型查数据库、调外部API)、涉及多轮对话记忆、涉及多步任务编排,那分层就是必须的,否则代码会迅速腐化。

还有一个取舍是自研还是用框架。LangChain、LangGraph这类框架帮你把很多层的东西都封装好了,上手快,但抽象层多,出问题的时候排查链路长。自研的话可控性强,但什么都得自己写。我个人的做法是:L1和L2尽量自研或者用轻量封装,因为这两层和业务耦合最紧,需要精细控制;L3的编排可以考虑用框架,因为编排逻辑复杂且通用性高,框架能省很多事。

3. 模型接入层:LLM调用的统一封装

3.1 为什么不能直接调SDK

新手最常见的写法是在业务代码里直接import openai然后调chat.completions.create。这在demo阶段没问题,但一旦要上线就会暴露一堆问题。首先是模型切换困难,你今天用A模型,明天想换B模型,发现调用方式、参数名、返回结构都不一样,得改遍所有调用点。其次是重试和限流没有统一处理,每个调用点都得自己写一遍。再就是密钥管理散落各处,安全隐患大。

所以模型接入层的第一个职责就是统一接口。定义一个抽象的LLMClient,对外暴露chat(messages, tools, **kwargs)这样的方法,内部再根据配置路由到不同的模型提供商。这样上层代码永远只依赖这个抽象接口,换模型的时候只改路由配置。

class LLMClient: def __init__(self, provider: str, config: dict): self.provider = provider self.adapter = self._build_adapter(provider, config) def chat(self, messages, tools=None, stream=False, **kwargs): return self.adapter.chat(messages, tools, stream, **kwargs) def _build_adapter(self, provider, config): if provider == "openai": return OpenAIAdapter(config) elif provider == "anthropic": return AnthropicAdapter(config) # 其他provider

这个封装看起来简单,但它带来的好处是巨大的。上层业务代码完全不知道底层用的是哪家模型,只关心输入输出。测试的时候可以注入一个MockAdapter,不用真的调API。

3.2 重试、限流与降级策略

模型API不是100%可靠的。网络抖动、服务端限流、偶发的5xx错误都会发生。如果不做重试,用户就会看到莫名其妙的失败。但重试也不能无脑重试,得区分错误类型。

我的做法是把错误分成三类:可重试错误(超时、429限流、5xx服务端错误)、不可重试错误(401鉴权失败、400参数错误)、需要降级的错误(模型不可用、配额耗尽)。可重试错误用指数退避重试,比如第一次等1秒,第二次等2秒,第三次等4秒,最多重试3次。不可重试错误直接抛出,让上层处理。需要降级的错误则切换到备用模型。

限流这块,如果用的是云服务,通常有RPM(每分钟请求数)和TPM(每分钟token数)两个限制。你得在接入层做一个令牌桶或者滑动窗口计数器,在发请求前先检查配额。超过配额就排队等待或者直接拒绝。这个逻辑如果散落在业务代码里会非常混乱,放在接入层统一处理最合适。

注意:重试的时候要小心幂等性问题。如果模型调用是有副作用的(比如通过工具调用写了数据库),重试可能导致重复写入。这种情况要么保证工具调用幂等,要么在重试前检查状态。

3.3 流式输出的处理

流式输出是AI应用体验的关键。用户不想等十几秒才看到第一个字,而是希望像打字机一样一个字一个字蹦出来。但流式输出的处理比非流式复杂得多,因为你要处理SSE(Server-Sent Events)或者WebSocket的连接管理、分块解析、错误处理。

在接入层,流式输出通常返回一个生成器或者异步迭代器。上层代码用async for chunk in client.chat_stream(...)的方式消费。这里有个坑是流式过程中如果出错,已经输出的内容怎么办。我的做法是在接入层捕获流式过程中的异常,如果已经输出了部分内容,就发送一个特殊的结束标记告诉上层"流异常中断",让上层决定是重试还是提示用户。

还有一个细节是流式输出的缓冲。有些模型返回的chunk粒度很小,每个chunk可能就一两个token,如果每个chunk都触发一次前端渲染,性能会很差。所以在接入层或者应用层做一个小的缓冲,攒够一定字符数或者过了一定时间再推给前端,体验会好很多。

4. 能力层:提示词、工具与记忆

4.1 提示词管理的工程化

提示词写在哪?很多人的第一反应是写在代码里,一个字符串常量。但提示词是会频繁迭代的,产品经理改个措辞、运营调个语气,都得改代码重新部署,这效率太低了。所以提示词应该像配置一样管理,存在数据库或者配置中心,支持版本管理和灰度发布。

我通常会在能力层做一个PromptManager,每个提示词有一个唯一ID,支持多版本,可以按用户、按场景、按AB测试分组路由到不同版本。提示词模板里支持变量插值,比如{{user_name}}、{{context}}这样的占位符,运行时替换。

class PromptManager: def render(self, prompt_id: str, variables: dict, version: str = "latest"): template = self._load_template(prompt_id, version) return template.render(**variables)

这样做的好处是提示词的迭代不需要发版,运营自己就能改。而且有了版本管理,出问题可以快速回滚。我踩过的一个坑是提示词改了之后效果变差,但因为没有版本记录,根本不知道改之前是什么样,只能凭记忆恢复。有了版本管理就没这个问题了。

4.2 工具调用与MCP协议

工具调用是让LLM从"聊天机器人"变成"能干活的东西"的关键。模型本身只能生成文本,但通过Function Calling,它可以告诉系统"我要调用某个工具,参数是这些",系统执行完再把结果喂回给模型。这样模型就能查天气、查数据库、发邮件、操作文件。

工具的定义通常包括名称、描述、参数schema。描述非常重要,模型就是靠描述来判断什么时候该用这个工具。描述写得含糊,模型就会乱调或者不调。我的经验是描述要写得像给一个新员工交代任务一样具体,说清楚这个工具干什么、什么场景下用、参数是什么意思。

MCP(Model Context Protocol)是最近很火的一个东西,它本质上是一个标准化的工具接入协议。以前每个工具都要自己定义schema、自己写适配代码,MCP把这些标准化了,任何支持MCP的工具都能被任何支持MCP的模型客户端调用。这大大降低了工具集成的成本。在架构上,MCP相当于在能力层和外部工具之间加了一个标准适配层。

对比项传统Function CallingMCP
工具定义每个应用自己定义标准化协议
工具复用跨应用难复用一次实现多处可用
动态发现不支持支持运行时发现工具
传输方式应用自定义标准化传输

4.3 记忆系统的设计

LLM本身是无状态的,每次调用都是独立的。但用户期望的是"它记得我刚才说过什么"。这就是记忆系统要解决的问题。记忆分短期记忆和长期记忆。短期记忆就是当前会话的上下文,通常就是把历史消息拼在messages里传进去。长期记忆则是跨会话的,需要存储和检索。

短期记忆的坑在于上下文长度限制。模型有最大token数,对话长了就放不下。常见的做法是滑动窗口(只保留最近N轮)或者摘要压缩(把早期对话总结成一段话)。滑动窗口简单但会丢失早期信息,摘要压缩保留信息但会引入额外的模型调用成本和信息损失。我的做法是混合:最近几轮保留原文,更早的做摘要。

长期记忆通常用向量数据库实现。把重要信息embedding后存进去,需要的时候用相似度检索出来。这里的关键是"什么该记"。不是什么都要记,记太多会干扰检索精度。我一般只记用户明确表达的偏好、重要的事实性信息、以及任务相关的关键状态。

提示:记忆系统要设计清理机制。用户可能要求"忘记我说过的话",或者数据有保留期限要求。没有清理机制的记忆系统是个隐患。

5. 编排层:Agent的核心逻辑

5.1 Agent到底是什么

Agent这个词被用得很泛,但本质上它就是一个"能自主决定下一步做什么"的程序。普通的LLM调用是"你问一句它答一句",Agent则是"你给个目标,它自己规划步骤、调用工具、检查结果、继续执行直到完成"。

Agent的核心是一个循环:观察当前状态 -> 思考下一步 -> 执行动作 -> 观察结果 -> 继续循环。这个循环用LLM来驱动"思考"这一步,用工具调用来实现"执行动作"。所以Agent = LLM + 工具 + 循环 + 状态管理。

在架构上,Agent属于编排层。它不直接和模型API打交道(那是接入层的事),也不直接定义工具(那是能力层的事),它负责的是"什么时候调什么工具、怎么把多个步骤串起来、失败了怎么办"。

5.2 单Agent与多Agent的取舍

单Agent就是一个LLM带着一堆工具干活。多Agent则是多个LLM各自负责一部分,互相协作。比如一个"研究员Agent"负责搜集信息,一个"写作Agent"负责成文,一个"审核Agent"负责检查。

多Agent听起来很美好,但实际落地的时候问题不少。首先是成本,多个Agent意味着多次模型调用,token消耗成倍增长。其次是协调复杂度,Agent之间怎么通信、怎么传递状态、怎么处理冲突,都是麻烦事。再就是调试困难,一个任务经过三四个Agent转手,出问题的时候很难定位是哪一环。

我的建议是:能用单Agent解决的就别上多Agent。只有当任务确实可以清晰拆分成独立子任务、且每个子任务需要不同的工具集或不同的提示词策略时,才考虑多Agent。而且多Agent的通信尽量走结构化数据,别让Agent之间用自然语言聊天,那样不可控。

5.3 状态管理与容错

Agent执行过程中会产生大量状态:当前步骤、已调用的工具、中间结果、错误信息。这些状态需要被妥善管理,否则Agent会"失忆"或者"重复劳动"。

状态管理我通常用一个显式的State对象,每一步执行后更新这个对象。State里包含任务目标、执行历史、当前步骤、可用工具、中间结果等。这个State会被序列化存储,这样Agent中断后可以恢复。

容错是Agent最容易被忽视的部分。模型可能调用不存在的工具、可能传错参数、可能陷入死循环。所以编排层必须有防护:工具调用前校验参数、限制最大循环次数、检测重复动作、设置超时。我见过一个Agent因为工具一直返回错误,它就一直重试同一个工具,烧了几百万token才发现问题。所以循环次数上限和重复检测是必须的。

class AgentExecutor: def __init__(self, max_steps=20, timeout=300): self.max_steps = max_steps self.timeout = timeout def run(self, task, tools): state = State(task=task, history=[], step=0) while state.step < self.max_steps: if time.time() - state.start > self.timeout: return self._handle_timeout(state) action = self._think(state) if action.is_final: return action.result result = self._execute(action, tools) state.update(action, result) return self._handle_max_steps(state)

6. 应用层与基础设施:让AI应用真正可用

6.1 会话管理与多用户隔离

应用层要处理的是"多个用户同时用"这件事。每个用户有自己的会话、自己的记忆、自己的权限。会话管理要保证用户A的上下文不会泄露给用户B,这在多租户场景下尤其重要。

我的做法是每个会话有一个唯一的session_id,所有的状态、记忆、历史都挂在这个session_id下。模型调用的时候,从session_id对应的存储里取出上下文,拼装后传给接入层。这样不同用户的上下文天然隔离。

权限控制也不能忽视。不是所有用户都能调用所有工具。比如"删除数据库"这种工具,只有管理员能用。所以在工具调用前要检查当前用户的权限,没权限的工具直接拒绝,并且告诉模型"这个工具你无权使用",让模型换个方式。

6.2 可观测性:日志、追踪与成本监控

AI应用最怕的就是"不知道钱花哪了"和"出问题不知道哪一步错了"。所以可观测性是必须的。每次模型调用都要记录:用的哪个模型、输入多少token、输出多少token、耗时多少、花了多少钱。每次工具调用也要记录:调了什么工具、参数是什么、结果是什么、耗时多少。

这些数据汇总起来,你就能看到哪些功能最烧钱、哪些调用最慢、哪些工具最容易出错。我一般会做一个简单的dashboard,实时显示token消耗和调用延迟。有一次发现某个功能的token消耗是其他功能的十倍,查下来是提示词里塞了太多无关上下文,优化后成本降了80%。

追踪方面,一次用户请求可能经过应用层、编排层、能力层、接入层,中间可能调了多次模型和工具。要能把这些串起来,需要一个trace_id贯穿全链路。这样出问题的时候,拿着trace_id就能看到完整的调用链。

6.3 缓存与性能优化

AI应用的延迟主要来自模型调用,动辄几秒。有些调用是可以缓存的,比如相同的提示词和参数,如果短时间内重复请求,可以直接返回缓存结果。这在一些高频查询场景下能大幅降低延迟和成本。

但缓存要小心,不是所有调用都能缓存。涉及实时数据的、涉及用户私有上下文的、涉及随机性的,都不能缓存。我的做法是给每个调用打一个cache_key,由提示词模板ID、变量值、模型参数共同决定。只有明确标记为可缓存的调用才走缓存。

另一个优化是并行化。如果一个任务需要调多个独立的工具,可以并行调用而不是串行。比如同时查天气和查航班,没必要等一个查完再查另一个。编排层要支持这种并行执行,把总延迟从"各步骤之和"降到"最慢的那一步"。

7. 常见问题与排查技巧实录

7.1 模型输出不稳定怎么办

这是最高频的问题。同一个输入,模型有时候答得好有时候答得差。原因通常有几个:温度参数太高、提示词不够明确、上下文里有干扰信息。

排查顺序是这样的:先把temperature调到0或者接近0,看是否稳定。如果稳定了,说明是随机性问题,根据场景决定是否接受随机性。如果还不稳定,检查提示词,是不是有歧义、是不是缺少示例。加几个few-shot示例通常能显著提升稳定性。最后检查上下文,是不是塞了太多无关信息干扰了模型。

7.2 工具调用失败怎么排查

工具调用失败分几种:模型没调工具、调了错误的工具、调了正确的工具但参数错了、工具执行本身报错。

模型没调工具,通常是工具描述写得不好,模型没理解什么时候该用。解决方法是把描述写得更具体,加上使用场景的说明。调错工具,通常是多个工具的描述太相似,模型分不清。解决方法是让每个工具的职责更单一,描述更有区分度。参数错误,通常是参数schema定义不清晰,或者模型对参数含义理解有误。解决方法是给参数加上详细的描述和示例。工具执行报错,那就是工具本身的问题,看日志排查。

问题现象可能原因排查方法解决方案
模型不调工具描述不清检查工具描述补充使用场景说明
调错工具工具职责重叠对比工具描述拆分或合并工具
参数错误schema不清晰检查参数定义加描述和示例
执行报错工具本身bug看工具日志修复工具代码
循环调用缺少终止条件看调用历史加最大步数限制

7.3 上下文超长怎么处理

上下文超长是必然会遇到的。模型有token上限,对话长了、RAG检索的内容多了,就会超。处理方式有几种:截断(直接丢掉最早的)、摘要(把早期内容压缩)、检索(只保留相关部分)。

我的经验是组合使用。系统提示词和最近的对话保留原文,早期的对话做摘要,RAG检索的内容按相关性排序后只取top K。这样既保留了关键信息,又控制了长度。另外要注意,不同模型的上下文窗口不一样,接入层要能感知当前模型的限制,动态调整策略。

7.4 成本失控怎么控制

成本失控通常是因为:提示词太长、调用了太贵的模型、Agent循环次数太多、没有缓存。

控制成本的手段:一是分级用模型,简单任务用便宜的小模型,复杂任务才用大模型。二是优化提示词,去掉冗余内容。三是设置预算上限,超过就告警或者降级。四是加缓存。五是限制Agent的最大步数。我一般会在接入层做一个成本计数器,实时累计当前会话的花费,超过阈值就切换到便宜模型或者直接拒绝。

8. 从架构图到落地:我的实操建议

架构设计最终要落到代码上。我的建议是不要一上来就追求完美的五层架构,而是从最小可用版本开始,随着需求增长逐步分层。一开始可能就是接入层加应用层两层,等工具调用多了再抽能力层,等流程复杂了再抽编排层。过早分层和过晚分层都是问题,关键是跟着实际需求走。

技术选型上,接入层我倾向于轻量自研,因为这块和业务耦合紧且需要精细控制。能力层的提示词管理和记忆存储可以用现成的组件,工具调用如果生态支持MCP就优先用MCP。编排层如果逻辑简单就自研状态机,复杂的话可以考虑LangGraph这类框架。应用层和基础设施层用成熟方案就行,没必要造轮子。

最后说一个我踩过的大坑:不要在没有可观测性的情况下上线AI应用。我早期做过一个项目,上线后用户反馈"有时候好用有时候不好用",但我们没有任何日志记录模型调用,根本不知道问题出在哪。后来补上了全链路追踪,才发现是某些特定输入触发了模型的异常行为。所以日志、追踪、成本监控这些东西,一定要在架构设计阶段就考虑进去,不要等出了问题再补。

这套架构思路不是一成不变的,随着模型能力的变化、工具生态的丰富、业务需求的变化,每一层的具体实现都会演进。但分层的核心思想——隔离变化、明确职责、独立演进——是不会变的。把这张架构图理解透了,你再看任何AI应用的技术方案,都能快速判断它的设计是否合理、哪里可能有坑。