1. 从一张架构图说起:AI应用到底该怎么搭
这两年我参与过不少AI应用项目的架构评审,也帮朋友从零搭过几个Agent产品。说实话,大部分团队在动手之前,脑子里其实没有一张清晰的架构图。大家一上来就讨论用哪个模型、要不要上RAG、Agent框架选LangChain还是别的,结果做到一半发现数据流对不上、工具调用乱成一锅粥、并发一上来整个系统就崩了。
“图解AI应用架构设计”这个主题,我想聊的不是画一张好看的PPT架构图,而是把AI应用从输入到输出这条链路上,每一层到底在干什么、层与层之间怎么衔接、哪些地方最容易出问题,用图解的思路给你拆明白。核心关键词就几个:AI应用、架构设计、Agent、LLM、MCP。这几个词基本构成了当下AI应用开发的主干。
这篇文章适合谁看?如果你是一个正在做AI应用开发的程序员,或者是一个想从传统后端/运维转向AI应用方向的工程师,又或者你是一个技术负责人需要评估AI项目的架构方案,那这篇内容应该能帮你建立起一套完整的认知框架。我不会只讲概念,每个层次我都会给出具体的选型建议、参数考量、实操中踩过的坑,以及可以直接参考的代码结构。
先说一个我自己的判断:AI应用的架构设计,本质上是在解决“不确定性管理”的问题。传统软件是确定性的输入输出,你调一个函数,给定参数就一定有确定的返回。但LLM不一样,同样的prompt,两次调用可能给出不同的结果。Agent更甚,它自己决定调什么工具、调几次、什么时候停。所以架构设计的核心目标,就是在这个不确定的基础上,构建出稳定、可观测、可扩展的系统。
2. AI应用架构的整体分层与核心思路
2.1 为什么需要分层架构
我见过不少团队一开始把AI应用写成一个巨大的函数:接收用户输入,拼prompt,调LLM,解析结果,返回。这种写法在demo阶段没问题,但一旦要加记忆、加工具调用、加多轮对话、加权限控制,代码就会变成一团乱麻。
分层架构的价值在于关注点分离。每一层只负责自己的事,层与层之间通过明确的接口通信。这样做的好处是:换模型不用改业务逻辑,加工具不用动prompt管理,调并发不用重写整个链路。
我通常把AI应用分成这么几层,从下往上说:
- 模型层:LLM本身,包括模型选型、推理部署、token管理
- 能力层:把LLM的能力封装成可复用的组件,比如RAG检索、工具调用、记忆管理
- 编排层:Agent的核心,决定什么时候调什么能力、怎么组织多步推理
- 协议层:MCP这类标准化协议,解决工具和数据的接入问题
- 应用层:面向用户的接口,包括对话管理、权限、前端交互
- 基础设施层:贯穿所有层的可观测性、缓存、限流、安全
这六层不是必须全部都有,小项目可能只有模型层和应用层。但只要你的AI应用开始涉及工具调用和多轮交互,这套分层就能帮你理清思路。
2.2 模型层:LLM选型的三个关键维度
模型选型是架构设计的第一步,也是最容易纠结的一步。我的经验是,不要一上来就追求最强模型,而是从三个维度来评估:
第一个维度是任务复杂度。如果你的任务只是简单的文本分类、信息抽取,那一个小参数量的模型就够了,成本低、延迟低。如果涉及复杂的推理、多步规划、代码生成,那就需要更强的模型。我一般会建议团队先用一个中等模型跑通链路,再根据效果决定是否升级。
第二个维度是延迟要求。在线交互场景,用户能接受的首次响应时间大概在2-3秒以内。如果模型推理本身就要5秒,那架构上就必须考虑流式输出,让用户先看到部分结果。流式输出不是可选项,是交互类AI应用的标配。
第三个维度是成本。这里要算一笔账:假设你的应用每天有1万次调用,每次平均消耗2000个token(输入+输出),那一天的token消耗就是2000万。不同模型的单价差异可能达到几十倍,一个月下来成本差距非常可观。所以架构设计时一定要把token消耗纳入考量,该用便宜模型的地方不要用贵的。
关于token,有个很形象的类比:LLM的注意力机制里,key是“我是谁”,query是“我在找什么”,value是“我能提供什么”。输入文本被映射成这三组向量,通过query和key的匹配来决定关注哪些value。理解这个机制,你就能明白为什么prompt的写法对结果影响这么大——你给的query越明确,模型越能匹配到正确的信息。
2.3 能力层:RAG、工具调用与记忆管理
能力层是把LLM的原始能力包装成业务可用的组件。最核心的三块是RAG、工具调用和记忆管理。
RAG(检索增强生成)解决的是模型知识不足和幻觉问题。它的基本流程是:把文档切块、向量化、存入向量数据库,用户提问时先检索相关片段,再把片段作为上下文喂给LLM。这里的关键参数是切块大小和检索数量。切块太大,检索精度下降;切块太小,上下文不完整。我的经验是,中文文档切块大小在300-500字比较合适,检索返回top 3-5个片段。
工具调用让LLM能够与外部系统交互。比如查天气、查数据库、发邮件。工具调用的架构设计要点是:工具的输入输出schema要严格定义,工具的执行要有超时和重试机制,工具的结果要能被LLM理解。我见过最常见的坑是工具返回的数据格式不统一,导致LLM解析失败。
记忆管理分短期记忆和长期记忆。短期记忆就是当前对话的上下文,通常直接放在prompt里。长期记忆需要持久化存储,用户下次来的时候能回忆起之前的信息。长期记忆的实现方式有向量检索、摘要压缩、结构化存储等,选择哪种取决于你的场景。
2.4 编排层:Agent的核心逻辑
Agent是这两年最热的概念,但很多人对它的理解停留在“能调工具的LLM”。其实Agent的核心在于自主决策:它根据当前状态决定下一步做什么,而不是按照预设的流程走。
一个典型的Agent循环是:观察当前状态 -> 思考下一步行动 -> 执行行动 -> 观察结果 -> 继续循环,直到任务完成或达到终止条件。这个循环的实现方式有很多种,常见的有ReAct、Plan-and-Execute、Reflexion等。
架构设计时,编排层要解决几个问题:循环终止条件是什么?最大迭代次数设多少?工具调用失败怎么处理?中间结果怎么存储?这些问题不解决,Agent很容易陷入死循环或者产生不可控的行为。
2.5 协议层:MCP为什么重要
MCP(Model Context Protocol)是最近很火的一个概念。简单说,它是一套标准化协议,让LLM应用能够以统一的方式接入各种工具和数据源。在没有MCP之前,每接一个工具就要写一套适配代码;有了MCP,工具提供方按照协议暴露接口,应用方按照协议调用,双方解耦。
MCP的核心价值在于标准化。它定义了工具的描述格式、调用方式、返回结构,让不同的AI应用能够复用同一套工具生态。我最近在做的几个项目都开始接入MCP,最直观的感受是接入新工具的时间从原来的半天缩短到半小时。
2.6 应用层与基础设施层
应用层是用户直接接触的部分,包括对话界面、会话管理、权限控制、多租户隔离等。这一层的架构设计要特别注意会话状态的管理。用户的每一轮对话都不是孤立的,需要把历史上下文带上。但上下文不能无限增长,要有截断或摘要策略。
基础设施层是贯穿所有层的,包括日志、监控、追踪、缓存、限流、安全。AI应用的可观测性比传统应用更重要,因为LLM的输出是不确定的,你需要记录每次调用的输入输出、token消耗、延迟、工具调用链路,才能定位问题。
3. 核心细节解析与实操要点
3.1 Prompt工程在架构中的位置
很多人把prompt工程当成一个独立的事情,但在架构视角下,prompt应该是可配置、可版本化、可测试的资产。
我的做法是把prompt模板存在配置中心或数据库里,每个prompt有唯一的key和版本号。应用运行时根据场景加载对应的prompt模板,填充变量后发给LLM。这样做的好处是:调整prompt不需要发版,可以做A/B测试,可以回滚到历史版本。
Prompt模板的设计要注意几点:系统提示词定义角色和约束,用户提示词包含具体任务和上下文,few-shot示例帮助模型理解输出格式。对于需要结构化输出的场景,我会在prompt里明确给出JSON schema,并在应用层做校验和重试。
3.2 工具调用的参数设计与错误处理
工具调用是Agent能力的核心,但也是最容易出问题的地方。我总结了几条实操经验:
工具描述要精确。LLM是根据工具的名称和描述来决定是否调用的。如果描述模糊,模型可能在不该调用的时候调用,或者该调用的时候不调用。我的做法是给每个工具写一段清晰的描述,说明它做什么、什么时候用、输入参数是什么格式。
参数校验要严格。LLM生成的参数可能不符合预期,比如该传数字的传了字符串,该传枚举的传了不存在的值。应用层必须做参数校验,校验失败时把错误信息返回给LLM,让它重新生成。
超时和重试要设置。外部工具可能超时或失败,不能让Agent卡死。我一般设置单次工具调用超时5秒,失败后重试1次,重试还失败就把错误信息返回给LLM,让它决定下一步。
结果要截断。工具返回的结果可能很长,直接塞进prompt会消耗大量token。我的做法是对结果做截断或摘要,只保留关键信息。
3.3 上下文窗口管理与token优化
上下文窗口是LLM应用最稀缺的资源之一。一个对话轮次多了,历史消息就会撑爆窗口。管理上下文窗口有几种策略:
滑动窗口:只保留最近N轮对话。简单但会丢失早期信息。
摘要压缩:把早期对话用LLM总结成一段摘要,替代原始消息。效果好但增加一次LLM调用。
向量检索:把历史消息存入向量库,需要时检索相关片段。适合长期记忆场景。
混合策略:近期消息保留原文,远期消息做摘要,关键信息做结构化存储。
我的经验是,对于大多数对话场景,滑动窗口+摘要压缩的组合就够了。窗口大小根据模型能力定,一般保留最近10-20轮对话,更早的做摘要。
3.4 Agent循环的终止条件设计
Agent循环如果没有正确的终止条件,很容易陷入死循环。我一般设置三重保险:
最大迭代次数:硬性限制,比如最多循环10次。超过就强制终止,返回当前结果。
任务完成检测:Agent输出中包含特定的完成标记,或者调用了特定的终止工具。
无进展检测:如果连续两轮的行动和结果高度相似,说明Agent卡住了,强制终止。
这三重保险要同时设置,不能只靠一个。我踩过的坑是只设了最大迭代次数,结果Agent在10次循环里反复调同一个工具,浪费了大量token。
3.5 MCP接入的实操要点
MCP的接入流程大致是:工具提供方实现MCP Server,暴露工具列表和调用接口;应用方实现MCP Client,连接Server并获取工具列表,把工具描述注入到LLM的上下文中。
实操中要注意几点:连接管理要做好,MCP Server可能断开,要有重连机制;工具列表缓存,不要每次调用都去拉取工具列表;权限控制,不是所有工具对所有用户都开放;错误隔离,一个MCP Server挂了不能影响整个应用。
4. 实操过程与核心环节实现
4.1 一个最小可用AI应用的架构搭建
假设我们要做一个智能客服Agent,能回答产品问题、查询订单状态、转接人工。我按分层架构来搭:
模型层:选一个中等规模的模型,支持function calling。配置流式输出。
能力层:
- RAG:把产品文档向量化,存入向量库
- 工具:查询订单API、转接人工API
- 记忆:短期用滑动窗口,长期用向量检索
编排层:用ReAct模式,Agent根据用户问题决定是检索文档还是调工具。
协议层:工具通过MCP Server暴露,应用通过MCP Client接入。
应用层:WebSocket接口,支持流式输出,会话状态存在Redis。
基础设施层:日志记录每次LLM调用和工具调用,Prometheus监控延迟和token消耗。
4.2 关键代码结构示例
下面是一个简化的Agent循环实现,用Python伪代码展示核心逻辑:
class Agent: def __init__(self, llm, tools, max_iterations=10): self.llm = llm self.tools = tools self.max_iterations = max_iterations def run(self, user_input, history): messages = self.build_messages(user_input, history) for i in range(self.max_iterations): response = self.llm.chat(messages, tools=self.tools.schemas) if response.is_final: return response.content if response.has_tool_call: tool_result = self.execute_tool(response.tool_call) messages.append(response.message) messages.append(tool_result) if self.no_progress(messages): break return self.fallback_response(messages) def execute_tool(self, tool_call): tool = self.tools.get(tool_call.name) try: result = tool.execute(**tool_call.arguments) return format_tool_result(result) except Exception as e: return format_tool_error(e)这个结构的关键点是:循环有最大次数限制,工具执行有异常处理,每轮都把工具结果追加到消息历史中。
4.3 并发处理的设计
AI应用的并发处理和传统应用不太一样,因为LLM调用是长耗时操作。我一般从几个层面来处理:
请求队列:用户请求先入队列,后端worker从队列消费。这样可以控制并发数,避免打爆LLM的rate limit。
异步调用:LLM调用用异步IO,不要阻塞线程。Python里用asyncio,Java里用CompletableFuture。
流式输出:对于长文本生成,用SSE或WebSocket流式返回,用户不用等全部生成完。
缓存:相同的输入可以缓存LLM的输出,减少重复调用。但要注意缓存的key要包含完整的上下文。
降级策略:当LLM调用失败或超时时,返回兜底回复,不要让用户看到错误。
4.4 可观测性的落地
AI应用的可观测性我一般记录这几类数据:
| 数据类型 | 记录内容 | 用途 |
|---|---|---|
| LLM调用日志 | 输入prompt、输出结果、token数、延迟 | 调试、成本分析 |
| 工具调用日志 | 工具名、参数、结果、耗时 | 排查工具问题 |
| Agent轨迹 | 每轮思考、行动、观察 | 分析Agent行为 |
| 用户反馈 | 点赞点踩、转人工率 | 评估效果 |
| 系统指标 | QPS、错误率、P99延迟 | 监控告警 |
这些数据要能关联起来,通过一个trace_id串起一次完整的请求链路。我一般用OpenTelemetry做追踪,日志存Elasticsearch,指标存Prometheus。
5. 常见问题与排查技巧实录
5.1 LLM输出格式不稳定的排查
这是最常见的问题。明明prompt里要求输出JSON,模型有时候输出markdown代码块,有时候输出纯文本,有时候字段名还拼错。
排查思路:先看prompt是否足够明确,给出完整的JSON schema和示例。如果还不行,在应用层做解析容错,用正则提取JSON部分,解析失败时重试。重试时把错误信息加到prompt里,告诉模型上次输出哪里不对。
我的经验是,对于结构化输出,用function calling比纯prompt更稳定。因为function calling的schema是强约束的,模型必须按照schema生成。
5.2 Agent陷入死循环的处理
Agent反复调同一个工具,或者在不同工具之间来回跳,就是死循环。排查时先看Agent的思考过程,理解它为什么这么决策。
常见原因有几个:工具返回的结果不符合预期,Agent以为没成功所以重试;prompt里的终止条件不明确,Agent不知道什么时候该停;任务本身无法完成,Agent一直在尝试。
解决方法:设置最大迭代次数;在prompt里明确终止条件;对工具结果做标准化处理,让Agent能正确理解;无进展检测,连续相似行动就终止。
5.3 并发上不去的问题
AI应用的并发瓶颈通常在LLM调用。排查时先看LLM的rate limit是多少,再看自己的并发数是否超了。如果没超但并发还是上不去,可能是同步调用阻塞了线程。
解决方法:用异步IO;加请求队列控制并发;对LLM调用做连接池复用;考虑多模型路由,把请求分散到不同模型。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| LLM输出格式错误 | prompt不明确 | 检查prompt schema | 用function calling,加解析容错 |
| Agent死循环 | 终止条件缺失 | 看Agent轨迹 | 设最大迭代,加无进展检测 |
| 工具调用失败 | 参数错误或超时 | 看工具日志 | 参数校验,超时重试 |
| 并发上不去 | 同步阻塞 | 看线程状态 | 异步IO,请求队列 |
| 上下文超限 | 历史消息太长 | 看token数 | 滑动窗口,摘要压缩 |
| 响应延迟高 | 模型推理慢 | 看LLM延迟 | 流式输出,模型降级 |
| 成本超预算 | token消耗大 | 看token统计 | 缓存,小模型路由 |
5.5 几个独家避坑技巧
技巧一:prompt版本管理。每次改prompt都要记录版本和效果对比,不然改着改着就不知道哪个版本效果好了。
技巧二:工具结果标准化。所有工具返回的结果都统一成一种格式,比如{status, data, error},这样LLM更容易理解。
技巧三:灰度发布。新的prompt或新的Agent逻辑先对少量用户开放,观察效果再全量。
技巧四:成本监控。按用户、按场景统计token消耗,发现异常及时排查。
技巧五:降级预案。LLM服务不可用时,要有兜底方案,比如返回缓存结果或转人工。
6. 架构演进与扩展方向
6.1 从单Agent到多Agent协作
当任务复杂度上升,单个Agent可能搞不定。这时候可以考虑多Agent架构,每个Agent负责一个子领域,通过消息传递协作。
多Agent的架构设计要点是:角色划分要清晰,每个Agent的职责边界明确;通信协议要统一,Agent之间怎么传递消息;协调机制要设计好,谁来决定任务分配和结果汇总。
我见过比较成功的多Agent架构是“主管+专家”模式:一个主管Agent负责理解用户意图、拆解任务、分发给专家Agent,专家Agent各自处理自己的领域,结果汇总给主管Agent。
6.2 Agent安全的设计考量
Agent安全是最近越来越受关注的话题。Agent能调工具、能访问数据,如果被恶意利用,后果很严重。
安全设计要从几个层面考虑:输入过滤,防止prompt注入;工具权限,Agent只能调授权范围内的工具;数据隔离,不同用户的数据不能混;行为审计,记录Agent的所有行动,便于追溯;输出审查,防止敏感信息泄露。
6.3 架构的可扩展性设计
AI应用的技术栈变化很快,今天用的框架明天可能就过时了。架构设计时要考虑可扩展性,把易变的部分抽象成接口,方便替换。
我的做法是:模型调用抽象成LLM Provider接口,工具调用抽象成Tool接口,记忆存储抽象成Memory接口。这样换模型、换工具、换存储都不用改业务逻辑。
7. 我个人的一些实操体会
做AI应用架构这几年,最大的体会是:不要过度设计,但要有演进的空间。一开始不要想着把所有层都搭全,先把核心链路跑通,再根据实际需求逐步完善。
另一个体会是:可观测性要尽早做。AI应用的问题往往很难复现,没有日志和追踪,排查问题就是大海捞针。我一般从第一天就把LLM调用日志和工具调用日志加上,后面省很多事。
还有一点:prompt和Agent逻辑要当成代码来管理。版本控制、测试、灰度发布,这些软件工程的实践同样适用于prompt和Agent。我见过太多团队prompt改来改去,最后不知道哪个版本效果好。
最后分享一个小技巧:如果你刚开始做AI应用,不妨先用低代码平台快速搭一个原型,验证想法。等想法验证了,再用代码实现生产版本。这样能省很多时间,也能避免过早陷入技术细节。