1. 从"能跑通"到"能上线":AI应用架构到底在解决什么问题
很多人第一次做AI应用,脑子里想的都是"我把模型接进来,写个提示词,能出结果就行了"。这个阶段确实能跑通,但只要你把它放到真实业务里,问题会一个接一个冒出来:模型偶尔抽风返回空结果怎么办?用户连续追问三轮之后上下文爆了怎么办?调用一次要等十几秒,前端转圈转到用户直接关页面怎么办?多个工具之间互相调用,中间某一步失败了,整个链路怎么恢复?
这些问题的答案,都不在模型本身,而在模型外面那一层——也就是AI应用架构。我习惯把它理解成一套"围绕大模型搭建的工程骨架":它决定了模型怎么被调用、上下文怎么被管理、工具怎么被编排、失败怎么被兜底、成本怎么被控制、效果怎么被观测。模型是发动机,架构是底盘、变速箱和刹车系统。发动机再好,底盘散架了车也上不了路。
这篇文章面向的是已经能写出一个"能对话的Demo"、但准备把它推进到真实场景的开发者。我会从架构分层讲起,把LLM调用层、Agent编排层、记忆与上下文层、工具与MCP接入层、可观测与容错层这几块拆开揉碎,配上我实际踩过的坑和可复现的配置思路。读完你应该能画出一张属于自己的AI应用架构图,并且知道每一层为什么这么设计,而不是照抄别人的框图。
需要先说明一点:AI应用架构没有唯一正确答案,它高度依赖你的场景。一个内部知识问答机器人和一个能自主操作多个系统的Agent,架构复杂度差着数量级。所以下面讲的是"决策逻辑",不是"标准答案"。
2. 分层拆解:一个AI应用到底由哪几块拼起来
2.1 为什么必须先分层,而不是一上来就写Agent
我见过太多项目,第一行代码就开始写Agent循环,结果写到后面发现:提示词散落在十几个文件里、工具定义和业务逻辑耦合在一起、想换个模型要改几十处、日志里根本看不出是哪一步出的错。这就是没有分层的代价。
分层的本质是隔离变化。AI应用里变化最快的是三样东西:模型(今天用这个明天可能换)、提示词(几乎每天都在调)、工具(业务系统接口经常变)。如果这三样和你的核心业务逻辑缠在一起,每次变动都是一次重构。分层之后,模型换了只动调用层,提示词改了只动Prompt层,工具加了只动工具注册层,核心编排逻辑基本不动。
一个我实际用下来比较稳的分层是这样的:
| 层级 | 职责 | 变化频率 | 典型组件 |
|---|---|---|---|
| 接入层 | 请求路由、鉴权、限流 | 低 | API网关、会话管理 |
| 编排层 | 决定"下一步做什么" | 中 | Agent循环、工作流引擎 |
| 能力层 | 模型调用、工具调用 | 高 | LLM客户端、MCP客户端 |
| 记忆层 | 上下文组装、历史管理 | 中 | 短期记忆、长期存储 |
| 观测层 | 日志、追踪、评估 | 低 | Trace、指标、回放 |
这张表不是让你照搬,而是让你意识到:每一层的变更节奏不一样,所以它们应该被分开。编排层相对稳定,能力层天天在变,把稳定的和易变的隔开,是架构设计最基本的一条原则。
2.2 接入层:别小看会话管理和限流
接入层最容易被忽视,但它决定了你的应用能不能扛住真实流量。这里有两个关键点。
第一是会话管理。AI应用和传统Web应用最大的区别是"有状态"——同一个用户的多轮对话必须落到同一个上下文里。你需要一个会话ID贯穿整个请求链路,并且这个ID要能关联到记忆层存储的历史。我一般用"用户ID + 会话ID"两级结构,用户ID用于长期偏好,会话ID用于当前对话。这样即使用户开多个窗口,上下文也不会串。
第二是限流和超时。大模型调用又慢又贵,如果不限流,一个用户狂点按钮就能把你的额度打满。我的做法是在接入层做两层限流:按用户限流(防止单用户刷爆)和按全局限流(保护后端模型配额)。超时也要设,而且要设得比模型本身的超时短一点,这样你能在模型还没返回时先给用户一个"正在处理"的反馈,而不是让连接一直挂着。
提示:接入层的超时时间建议设置为模型P99延迟的1.2倍左右。设太短会误杀正常请求,设太长会让用户以为页面卡死。
2.3 编排层:Agent循环的本质是一个状态机
很多人把Agent想得很神秘,其实剥开看,Agent循环就是一个带工具调用的状态机。它的核心逻辑可以用一段伪代码说清楚:
def agent_loop(user_input, context, tools, max_steps=10): messages = build_messages(user_input, context) for step in range(max_steps): response = llm.chat(messages, tools=tools) if response.has_tool_call(): result = execute_tool(response.tool_call) messages.append(response) messages.append(tool_result(result)) else: return response.content return "达到最大步数限制"就这么简单。真正难的不是这个循环,而是循环外面的东西:max_steps设多少?工具调用失败了怎么办?模型陷入死循环一直调同一个工具怎么办?上下文越来越长超出窗口怎么办?
我的经验是:max_steps一定要设,而且要根据场景设。简单问答设3-5步足够,复杂任务可以到10-15步,但绝不能无限。工具调用失败要有重试和降级,重试次数建议2次,超过就返回明确的错误让模型知道"这个工具现在不可用",让它自己想办法绕过去。至于死循环,可以在编排层加一个"相同工具+相同参数连续调用超过2次就强制中断"的检测。
2.4 能力层:模型调用和工具调用要分开封装
能力层是变化最频繁的地方,所以封装要足够薄、足够统一。我建议把"模型调用"和"工具调用"分成两个独立的客户端。
模型调用客户端要屏蔽不同厂商的API差异,对外暴露统一的chat()接口。这样你换模型时,上层编排逻辑完全不用改。这里有个细节:不同模型的工具调用格式不一样,有的用tool_calls字段,有的用特殊token,你的客户端要把这些差异吃掉,统一成一种内部格式。
工具调用客户端则要负责工具的注册、发现和执行。这里就引出了MCP——它本质上是一套标准化的工具接入协议。在没有MCP之前,每接一个工具你都要写一套适配代码;有了MCP,工具提供方按协议暴露能力,你的应用按协议调用,双方解耦。MCP的价值不在于技术多先进,而在于它把"工具接入"这件事从"每家自己定"变成了"大家按一个标准来"。
2.5 记忆层:短期记忆和长期记忆是两回事
记忆层是最容易被做烂的一层。很多人一上来就搞向量数据库,把所有历史都塞进去,结果检索出来的东西又慢又不准。
我的做法是把记忆分成两类。短期记忆是当前会话的上下文,直接放在消息列表里,按轮次管理,超出窗口就做摘要压缩。长期记忆是跨会话的用户偏好、事实知识,才需要落到向量库或结构化存储里。两者的检索策略完全不同:短期记忆是"最近优先",长期记忆是"相关优先"。
上下文压缩这里有个实用技巧:不要等超窗口了才压缩,而是在用到70%窗口时就触发。压缩时保留最近N轮原文,把更早的内容用模型总结成一段摘要。这样既省token,又不会丢失关键信息。
3. Agent架构选型:什么时候该用工作流,什么时候该用自主Agent
3.1 一个反直觉的结论:大部分场景不需要自主Agent
新手最容易犯的错,是觉得"自主Agent"更高级,什么都想做成Agent。但实际项目里,能用固定工作流解决的,就不要用自主Agent。
原因很简单:自主Agent的每一步都由模型决定,意味着它的行为不可预测、调试困难、成本不可控。而固定工作流是确定性的,每一步做什么、走哪条分支都是写死的,稳定、可测、便宜。
我一般用这个判断标准:如果任务的步骤是已知且固定的,用工作流;如果步骤依赖中间结果动态决定,才用Agent。比如"用户提问→检索知识库→生成回答"这是固定三步,工作流足够。而"用户说帮我订个会议室→Agent需要先查日历、再查空闲、再确认参会人、再发邀请"这种,步骤取决于中间查询结果,才需要Agent。
3.2 工作流编排的三种常见模式
即使确定用工作流,也有几种不同的编排模式,选错了会很别扭。
串行链是最简单的,A的输出喂给B,B的输出喂给C。适合有明确前后依赖的任务,比如"翻译→润色→格式化"。
并行扇出是把一个任务拆成多个子任务同时处理,最后汇总。适合"对一篇文章同时做摘要、关键词提取、情感分析"这种互不依赖的任务。并行能大幅降低总延迟,但要注意汇总时的结果合并逻辑。
条件路由是根据中间结果决定走哪条分支。比如先判断用户意图,是"咨询"就走问答链,是"操作"就走工具链。路由的判断可以用规则,也可以用一个小模型做分类,后者更灵活但要多一次调用。
3.3 自主Agent的可靠性设计:容错比智能更重要
如果你确实需要自主Agent,那可靠性设计就是重中之重。我踩过最深的坑,是Agent在某个工具连续失败后,开始"编造"工具返回结果,然后基于编造的结果继续往下走,最后给出一个看起来合理但完全错误的答案。
解决办法有三个层次。第一层是工具层容错:工具执行失败时,返回结构化的错误信息,而不是抛异常。错误信息要明确告诉模型"失败了,原因是X",让模型有机会换策略。第二层是编排层容错:检测到异常模式(比如连续失败、重复调用)时,主动中断并返回兜底回答。第三层是输出层校验:对Agent的最终输出做格式和事实校验,不合格就重试或降级。
这里有个我总结的原则:Agent的每一步都要可回滚、可观测、可中断。可回滚意味着中间状态要保存,出问题能回到上一个正常状态;可观测意味着每一步的输入输出都要记录;可中断意味着任何时候都能安全停止,不会留下脏数据。
3.4 多Agent协作:先问自己是不是真的需要
多Agent是这两年很热的概念,但我建议谨慎。多Agent带来的复杂度是数量级上升的:Agent之间怎么通信、怎么避免互相等待死锁、怎么保证一致性、成本怎么控制。
我见过的成功多Agent案例,基本都是职责高度分离的场景,比如一个Agent专门负责检索、一个专门负责推理、一个专门负责校验,它们之间是流水线关系而不是自由对话。如果你的场景里Agent之间需要频繁自由协商,那大概率是任务本身没拆清楚,先回去把单Agent做好更实际。
4. MCP与工具生态:让AI应用真正"能干活"的关键一层
4.1 MCP解决的到底是什么问题
在MCP出现之前,AI应用接工具是这样的:每个工具提供方写一套自己的接口文档,每个AI应用开发者按文档写一套适配代码。N个工具对接M个应用,就是N×M份适配工作。MCP把这个矩阵变成了N+M:工具方按MCP协议暴露一次,应用方按MCP协议接入一次,双方就能互通。
所以MCP的本质是接口标准化,和当年USB统一各种外设接口是一个道理。它不解决"工具好不好用"的问题,只解决"工具怎么被统一调用"的问题。理解这一点很重要,否则你会对MCP抱有不切实际的期待。
4.2 工具设计的三条实战原则
工具设计得好不好,直接决定Agent能不能用。我总结了三条原则。
第一,工具粒度要适中。太粗(一个工具干十件事)模型不知道怎么用,太细(一个工具只干一件小事)会导致调用次数爆炸。我的经验是:一个工具对应一个"用户能理解的完整动作"。比如"查询订单"是一个工具,而不是"连接数据库""执行SQL""格式化结果"三个工具。
第二,工具描述要写给模型看,不是写给人看。工具的名称和描述是模型决定要不要调用的唯一依据。描述里要说清楚:这个工具做什么、什么时候用、参数是什么含义、返回什么。我见过太多工具描述写得像API文档,模型根本看不懂该不该调。
第三,工具要幂等。Agent可能因为重试而重复调用同一个工具,如果工具不幂等(比如"下单"调两次就下两单),会出大问题。对于非幂等操作,要么加去重机制,要么在工具层做确认。
4.3 工具调用的失败处理与降级
工具调用失败是常态,不是异常。网络抖动、第三方限流、参数错误,都会导致失败。处理策略要分情况:
- 可重试错误(超时、限流):自动重试2次,指数退避。
- 参数错误:把错误信息返回给模型,让它修正参数重试。
- 权限错误:直接返回失败,不要重试,让模型换方案。
- 工具不可用:返回明确的"当前不可用",模型可以选择其他工具或告知用户。
关键点是:永远不要把原始异常直接抛给模型。异常堆栈对模型毫无意义,只会污染上下文。要把它转成模型能理解的自然语言描述。
5. 上下文与记忆工程:决定AI应用"智商"的隐形战场
5.1 上下文窗口不是越大越好
现在模型动辄支持几十万甚至上百万token的上下文,很多人就觉得"那我全塞进去不就行了"。实测下来,这是错的。原因有两个:一是成本,上下文越长每次调用越贵;二是效果,模型在超长上下文里的注意力会分散,关键信息反而被淹没,这就是所谓的"lost in the middle"现象。
所以上下文管理的核心不是"能塞多少塞多少",而是"只塞该塞的"。我一般把上下文分成四块:系统提示(角色和规则)、长期记忆(用户偏好和事实)、检索结果(相关知识)、对话历史(最近几轮)。每一块都要有明确的预算,加起来不超过窗口的80%,留20%给模型输出。
5.2 检索增强的常见误区
RAG(检索增强生成)几乎是AI应用的标配,但做得好的人不多。最常见的误区是"检索回来一堆文档直接塞给模型"。这样做的结果是:模型被无关信息干扰,回答质量反而下降。
正确的做法是先检索、再重排、后压缩。检索阶段召回尽量多的候选(比如20条),重排阶段用一个小模型或交叉编码器挑出最相关的3-5条,压缩阶段把选中的内容精简成关键句。经过这三步,塞给模型的上下文又短又准,效果提升非常明显。
还有一个细节:检索结果要带来源标注。这样模型在回答时可以引用来源,用户也能验证。对于知识问答类应用,来源标注几乎是刚需。
5.3 记忆的写入时机比读取更关键
大家都在研究"怎么从记忆里读",但很少有人认真想"什么时候往记忆里写"。写得太频繁,记忆里全是噪音;写得太少,重要信息丢失。
我的策略是事件驱动写入:只在特定事件发生时写记忆,比如用户明确表达了偏好("我以后都用中文回答")、完成了一个重要任务、纠正了之前的错误。日常对话不写,避免噪音。写入时还要做去重和冲突检测,同一个偏好更新时覆盖旧的,而不是简单追加。
6. 可观测性与容错:让AI应用从"能用"到"可信"
6.1 没有Trace,就没有调试
AI应用的调试难度远高于传统应用,因为它的行为不确定。同一个输入,两次输出可能不一样。没有完整的调用链追踪,你根本不知道问题出在哪一步。
我要求每个AI应用都必须有Trace,记录每一次调用的:输入、输出、耗时、token消耗、调用的模型、调用的工具、中间状态。Trace要能按会话ID串起来,这样你能完整回放一次对话的全过程。有了Trace,调试从"猜"变成了"看"。
6.2 评估体系:别靠感觉判断好坏
"感觉这次回答不错"是AI应用开发中最危险的一句话。你必须有一套可量化的评估体系。
评估分两层。离线评估用一批标注好的测试用例,跑完算准确率、召回率、格式合规率等指标,每次改提示词或换模型都跑一遍,看指标有没有退化。在线评估收集真实用户的反馈(点赞点踩、追问率、任务完成率),持续监控。两层结合,才能客观判断改动是变好还是变坏。
6.3 容错的三个层次
最后说容错,这是AI应用能不能上生产的分水岭。我把容错分成三层。
输入容错:用户输入可能超长、可能包含注入攻击、可能是空。接入层要做清洗和校验,把明显异常的输入挡在外面。
过程容错:模型调用超时、工具失败、上下文超限,都要有对应的处理策略。核心原则是"任何一步失败都不能让整个请求崩溃",要么降级,要么返回明确的错误。
输出容错:模型输出可能格式错误、可能包含不该有的内容、可能答非所问。输出层要做格式校验和内容过滤,不合格就重试或走兜底话术。
这三层做下来,你的AI应用才算真正具备了上生产的资格。我个人的体会是:AI应用80%的工作量在模型之外,模型选型反而是最简单的一步。把架构搭好、把容错做足、把可观测建起来,比追最新的模型重要得多。
7. 我在实际项目里踩过的几个坑
第一个坑是过早优化。项目初期就纠结用哪个向量库、要不要上多Agent,结果核心流程还没跑通。后来我改成"先用最简单的方式跑通端到端,再针对瓶颈优化",效率高了很多。
第二个坑是提示词硬编码。提示词散落在代码各处,改一个要重新部署。后来我把提示词抽成配置文件,支持热更新,调优效率翻倍。
第三个坑是忽视成本。上线前没算过token账,上线后发现成本远超预期。现在我要求每个项目上线前必须做成本估算:单次调用平均token数×日调用量×单价,算清楚再上。
第四个坑是没有降级方案。模型服务偶尔会不可用,没有降级方案时整个应用就挂了。后来我加了缓存兜底和规则兜底,模型不可用时至少能返回缓存结果或固定话术,用户体验不至于断崖式下跌。
这些坑说到底都指向同一件事:AI应用首先是软件工程,其次才是AI。把工程基本功做扎实,AI能力才能真正发挥出来。