ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

AI应用架构设计图解:从接入层到模型层的四层架构与Agent编排实战 1. 从一张架构图说起AI应用到底在搭什么很多人第一次接触AI应用开发脑子里冒出来的第一个问题不是“怎么写代码”而是“这东西到底长什么样”。你去看市面上的技术分享满屏都是Agent、LLM、MCP、RAG、Tool Calling这些词单独拎出来每个都认识拼在一起就不知道从哪下手了。我刚开始做AI应用的时候也有这个困惑后来画了几十张架构图、拆了十几个开源项目之后才慢慢理清楚——AI应用的架构设计本质上就是在回答一个问题一个用户请求进来经过哪些环节最终变成一个靠谱的答案返回去。这件事听起来简单但真正落地的时候你会发现难点根本不在“调模型”这一步。调API谁都会三行代码就能跑通。难的是当你的应用要面对真实用户、真实数据、真实并发的时候整个链路的每一个环节都可能出问题。模型返回格式不对怎么办用户问的问题需要查数据库怎么接多个Agent之间怎么协作上下文太长超了token限制怎么处理这些问题才是架构设计真正要解决的东西。这篇内容我打算用图解的思路把AI应用架构从最外层到最里层拆一遍。不是那种画个框框写个“LLM”就完事的示意图而是把每一层的职责、每一层之间的数据流、每一层容易踩的坑都讲清楚。适合正在做AI应用开发的程序员、正在从传统后端往AI方向转的工程师以及想搞清楚Agent和LLM到底怎么配合的产品同学。看完之后你至少能做到两件事第一拿到一个AI应用需求能画出它的架构分层第二知道每一层该用什么技术方案以及为什么这么选。2. 拆开AI应用的骨架四层架构与数据流向2.1 为什么AI应用不能只有“一个模型调用”先说什么叫“只有一个模型调用”的架构。最原始的做法是前端发一个请求过来后端直接把用户输入拼到prompt里调一次LLM的API拿到结果返回给前端。这个架构在demo阶段完全够用我早期做内部工具的时候就是这么干的一天就能上线。但这个东西一旦要产品化问题就全冒出来了。用户问“帮我查一下上个月的销售数据”模型不知道你的数据库里有什么它只能瞎编。用户连续问了五个问题每个问题都依赖前面的上下文但你的API调用是无状态的每次都是全新的对话。用户上传了一个PDF让你总结PDF有五十页直接塞进prompt里token直接爆了。更别说多个用户同时用的时候你怎么管理会话、怎么做限流、怎么做缓存。所以真实的AI应用架构一定是一个分层的结构。我把它拆成四层接入层、编排层、能力层、模型层。这四层不是我拍脑袋分的而是从实际项目中反复验证出来的——每一层解决一类特定问题层与层之间通过明确定义的接口通信任何一层换实现都不影响其他层。2.2 四层架构各自的职责边界接入层负责的是和用户打交道。它处理的是HTTP请求、WebSocket连接、会话管理、鉴权、限流这些传统后端的事情。这一层不需要懂AI它只需要知道“有一个请求进来了我要把它转成内部格式然后交给下一层”。很多从传统后端转过来的同学在这一层有天然优势因为这就是你们平时写的东西。编排层是整个AI应用的大脑。它决定了一个请求进来之后要经过哪些步骤、调用哪些工具、是否需要多轮推理。Agent的逻辑主要就活在这一层。比如用户问“帮我分析一下这份财报”编排层会决定先调用文档解析工具把PDF转成文本然后调用LLM做摘要再调用LLM做关键指标提取最后把结果组装成结构化输出。这一层是AI应用和传统应用最大的区别所在。能力层是编排层可以调用的“工具箱”。这里面包括向量检索RAG、数据库查询、外部API调用、代码执行、文件处理等等。编排层说“我需要查一下知识库”能力层就去执行向量检索编排层说“我需要调一下天气API”能力层就去发HTTP请求。这一层的关键是标准化——每个工具都有统一的输入输出格式编排层不需要知道工具内部怎么实现的。模型层就是LLM本身以及围绕LLM的一些基础设施比如模型路由根据任务类型选择不同模型、token计数、缓存、重试等等。这一层要解决的是“怎么稳定、高效地调用模型”这个问题。2.3 一次完整请求的数据流拆解光说分层太抽象我们跟着一个具体请求走一遍。假设用户在前端输入“帮我对比一下我们公司和竞争对手上季度的营收情况。”请求到达接入层首先做鉴权确认这个用户有权限访问财务数据。然后从会话存储中取出这个用户的历史对话上下文。接着做限流检查确认没有超过配额。最后把请求包装成一个内部消息格式扔给编排层。编排层收到消息后开始做任务规划。它先判断这是一个需要多步推理的任务然后拆解成子任务第一步查询本公司上季度营收第二步查询竞争对手上季度营收第三步对比分析。编排层发现第一步和第二步都需要访问数据库于是向能力层发起工具调用请求。能力层收到“查询本公司上季度营收”的请求把它翻译成SQL语句去数据库执行拿到结果后返回给编排层。同样的流程处理竞争对手的数据。编排层拿到两份数据后构造一个prompt把数据塞进去调用模型层的LLM接口。模型层负责选择合适的模型这种分析任务用大一点的模型效果更好管理token预算执行调用返回结果。最后结果沿着原路返回模型层→编排层→接入层→前端。接入层把这次对话存入会话历史供下一轮使用。这个流程看起来简单但每一层都有大量的细节要处理。下面我逐层拆开讲。3. 编排层Agent逻辑到底怎么写才不乱3.1 从“if-else”到“任务规划”的思维转变很多人写Agent的第一反应是写一堆if-else。用户问天气就调天气API用户问股票就调股票API。这种做法在意图识别阶段确实有用但一旦任务复杂起来就完全不够用了。因为真实用户的问题往往不是单一意图而是多个意图的组合甚至包含模型才能理解的隐含意图。编排层的核心思路是任务规划把用户的自然语言请求转化成一个可执行的任务序列。这个转化过程可以基于规则也可以基于LLM。我实测下来纯规则的方式适合意图非常明确的场景比如客服机器人的固定问答而基于LLM的规划方式适合开放式任务。基于LLM的规划怎么做简单说就是给LLM一个“工具清单”让它输出一个JSON格式的任务计划。比如{ plan: [ {step: 1, action: query_database, params: {table: revenue, company: self, quarter: last}}, {step: 2, action: query_database, params: {table: revenue, company: competitor, quarter: last}}, {step: 3, action: llm_analyze, params: {template: comparison, inputs: [step1_result, step2_result]}} ] }编排层拿到这个计划后按顺序执行把每一步的结果传给下一步。这种做法比if-else灵活得多因为新增一个工具只需要在工具清单里加一行描述不需要改代码逻辑。3.2 ReAct模式与Plan-Execute模式的选择目前主流的Agent编排模式有两种ReAct和Plan-Execute。ReAct是“推理-行动”交替进行。模型先想一步Thought然后做一个动作Action观察结果Observation再想下一步再行动。这种方式的好处是灵活模型可以根据中间结果动态调整策略。坏处是调用次数多延迟高而且容易陷入循环。Plan-Execute是先制定完整计划再一次性执行。好处是效率高调用次数少。坏处是如果中间某一步的结果和预期不符整个计划可能就废了。我的经验是任务步骤少于5步、步骤之间依赖关系明确的用Plan-Execute任务复杂、需要根据中间结果动态调整的用ReAct。实际项目中我经常混用——先用Plan-Execute做粗粒度规划每个步骤内部再用ReAct做细粒度执行。3.3 上下文管理别让对话历史撑爆你的token编排层还有一个容易被忽视但极其重要的职责上下文管理。多轮对话的场景下如果把所有历史消息都塞进prompttoken消耗会线性增长成本扛不住而且模型对超长上下文的注意力也会下降。常见的做法有三种。第一种是滑动窗口只保留最近N轮对话。简单粗暴但会丢失早期的重要信息。第二种是摘要压缩把早期对话用LLM总结成一段摘要和最近几轮对话一起塞进去。这种方式保留了关键信息但增加了一次LLM调用。第三种是向量检索把所有历史对话存入向量库每次根据当前问题检索最相关的几条历史记录。这种方式最灵活但实现复杂度也最高。我一般用组合方案最近3轮对话保留原文3轮之前的对话做摘要压缩同时把关键实体人名、数字、日期单独提取出来放在prompt的固定位置。这样既控制了token量又不会丢失关键信息。提示上下文管理策略一定要在项目早期就设计好后期再改会牵涉到会话存储结构、prompt模板、缓存策略的全面调整成本极高。4. 能力层工具调用与MCP协议的实际落地4.1 工具调用的本质是“结构化输出”能力层最核心的事情就是工具调用Tool Calling。很多人觉得工具调用很神秘其实它的本质就是让LLM输出一段结构化的JSON你的代码解析这个JSON然后执行对应的函数。比如你定义了一个查询天气的工具tools [ { name: get_weather, description: 查询指定城市的天气, parameters: { type: object, properties: { city: {type: string, description: 城市名称}, date: {type: string, description: 日期格式YYYY-MM-DD} }, required: [city] } } ]当用户问“北京明天天气怎么样”模型会输出{name: get_weather, arguments: {city: 北京, date: 2025-01-16}}你的代码拿到这个JSON调用实际的天气API把结果返回给模型模型再生成自然语言回复。整个流程没有任何魔法就是“模型输出JSON→代码执行→结果回传”这个循环。4.2 MCP协议解决了什么实际问题MCPModel Context Protocol是最近很火的一个概念。它要解决的问题是工具定义和工具实现之间的耦合。在没有MCP之前每个AI应用都要自己定义工具清单自己实现工具函数。你想换一个AI框架工具部分要重写。你想让多个AI应用共享同一套工具得复制粘贴。MCP的做法是把工具的定义和实现抽出来做成一个独立的服务通过标准协议对外暴露。任何支持MCP的AI应用都可以连接这个服务自动发现可用的工具直接调用。你可以把MCP理解成“AI工具界的USB接口”。以前每个设备有自己的充电口现在统一成USB-C谁都能插。实际落地的时候你可以把数据库查询、文件处理、API调用这些通用能力做成MCP Server然后你的Agent作为MCP Client去连接。这样你的Agent代码里就不需要写任何工具实现只需要写编排逻辑。目前MCP的生态还在快速发展中支持MCP的框架和工具越来越多。我的建议是新项目可以直接上MCP老项目如果工具不多可以先不动等MCP生态更成熟了再迁移。4.3 RAG不是“向量检索”四个字就完事了RAG检索增强生成是能力层里最常用的工具但也是最容易做砸的。很多人以为RAG就是“把文档切块→向量化→存向量库→检索→塞进prompt”跑通demo觉得效果不错一上真实数据就发现检索出来的东西驴唇不对马嘴。问题出在切块策略上。固定长度切块是最简单的做法但效果往往最差。因为一个完整的语义单元可能被切断了检索出来的片段缺少上下文。我一般用语义切块按段落、按标题、按句子边界来切保证每个块是一个完整的语义单元。如果文档结构复杂还会用LLM来做智能切块——让模型判断哪里是自然的切分点。另一个坑是检索策略。纯向量检索适合语义相似度匹配但对关键词精确匹配不擅长。用户搜“2024年Q3营收”向量检索可能返回一堆“营收”相关的段落但漏掉精确包含“2024年Q3”的那一段。所以实际项目中我一般用混合检索向量检索关键词检索两路结果合并后做重排序Rerank。重排序用一个小的交叉编码器模型来做效果比单纯按向量距离排序好很多。5. 模型层LLM选型、路由与成本控制5.1 不同任务用不同模型别拿大炮打蚊子模型层最容易被忽视的优化点就是模型路由。很多项目从头到尾就用一个模型要么全用最贵的要么全用最便宜的。前者成本爆炸后者效果拉胯。正确的做法是根据任务类型选择模型。我一般把任务分成三档任务类型特点推荐模型档位举例简单分类/提取输入短、输出结构化、逻辑简单小模型/轻量模型意图识别、实体提取、情感分类中等推理需要一定理解能力、输出中等长度中等模型摘要生成、问答、简单分析复杂推理多步推理、长文本理解、创意生成大模型代码生成、复杂分析、多轮规划模型路由的实现方式有两种。一种是静态路由在代码里写死“这个任务用哪个模型”。另一种是动态路由用一个小的分类模型先判断任务难度再决定用哪个模型。静态路由简单可靠动态路由更灵活但增加了一次调用。我一般先用静态路由等任务类型稳定了再考虑动态路由。5.2 Token成本的精算与优化Token成本是AI应用绕不开的话题。我见过太多项目上线第一个月账单出来才发现成本失控的。控制成本的核心是精算每一处token消耗。先算一笔账。假设你的应用每天处理10000个请求每个请求平均输入2000 token、输出500 token。用中等价位的模型输入$0.5/百万token、输出$1.5/百万token来算每日输入成本10000 × 2000 / 1000000 × 0.5 $10每日输出成本10000 × 500 / 1000000 × 1.5 $7.5每日总成本$17.5每月成本约$525这还只是一个中等规模的应用。如果输入长度翻倍、请求量翻倍成本直接四倍。所以优化token消耗是必须做的事。优化手段有几个。Prompt压缩把冗余的指令精简掉把few-shot示例从5个减到2个。缓存相同的输入直接返回缓存结果不重复调用模型。分级处理简单请求走小模型复杂请求才走大模型。输出限制在prompt里明确要求“简洁回答”设置max_tokens上限。注意缓存策略要小心处理。如果缓存粒度太粗不同用户的请求可能互相污染如果缓存粒度太细命中率又太低。我一般按“用户ID请求类型关键参数”来做缓存key。5.3 模型调用的稳定性设计LLM API不是100%可靠的。超时、限流、返回格式错误、服务不可用这些情况在生产环境都会遇到。模型层必须做好稳定性设计。重试机制是基础。但重试不能无脑重试要区分错误类型。超时和限流可以重试参数错误重试也没用。重试次数一般2-3次每次间隔用指数退避。降级策略是保险。主模型不可用时自动切换到备用模型。备用模型可以选同级别的另一个模型也可以降级到小模型保证基本可用。格式校验是兜底。模型返回的JSON可能不合法可能缺少必填字段。拿到结果后一定要做schema校验校验不通过就触发重试或降级。超时控制是必须。LLM调用可能卡住很久必须设置合理的超时时间。一般简单任务10-30秒复杂任务60-120秒。超时后要么重试要么返回兜底话术。6. 踩坑实录那些架构图上看不到的坑6.1 并发场景下的会话状态管理单用户测试的时候一切正常一上并发就出问题。最常见的问题是会话状态串了。用户A的对话历史被用户B看到了或者同一个用户的多个请求互相覆盖了上下文。根因在于会话存储的设计。如果你把会话状态存在内存里多实例部署的时候就会出问题——请求被负载均衡到不同实例每个实例的内存里只有部分会话。解决方案是把会话状态外置到Redis或数据库中每个请求根据session_id去取对应的上下文。另一个并发问题是同一会话的并发请求。用户快速发了三条消息三个请求同时到达。如果每个请求都去读会话历史、追加新消息、写回就会出现覆盖。解决方案是加分布式锁按session_id加锁保证同一会话的请求串行处理。6.2 工具调用的“幻觉参数”模型在调用工具的时候经常会编造一些不存在的参数或者参数格式不对。比如你定义的工具只接受city和date两个参数模型偏偏传了一个province进来。或者你要求日期格式是YYYY-MM-DD模型传了明天。这个问题没有完美的解决方案但可以缓解。第一在工具描述里把参数约束写清楚包括格式、枚举值、必填项。第二在代码里做严格的参数校验不合法的参数直接拒绝并返回错误信息给模型让模型重新生成。第三对于关键参数可以在prompt里加few-shot示例展示正确的参数格式。我实测下来参数校验错误回传这个组合能解决80%以上的幻觉参数问题。模型拿到错误信息后大部分情况下能自我纠正。6.3 长对话中的“中间遗忘”多轮对话超过一定轮数后模型会开始“遗忘”中间的内容。你明明在第三轮告诉了它一个关键信息到第十轮它就不记得了。这不是模型的问题而是注意力机制的特性——上下文太长的时候中间部分的信息容易被稀释。解决方案除了前面说的上下文管理策略之外还有一个技巧关键信息前置。把最重要的系统指令、用户偏好、关键实体放在prompt的最前面而不是埋在对话历史中间。另外可以在每轮对话开始时用一句话重申关键上下文比如“继续之前关于XX项目的讨论”。6.4 流式输出的边界情况流式输出Streaming是提升用户体验的重要手段但它的边界情况比非流式多得多。比如流到一半网络断了怎么办流式输出的内容需要做后处理怎么办多个工具调用的结果需要合并后再流式输出怎么办我的经验是流式输出只用于最终的自然语言回复工具调用和中间推理不走流式。这样可以把复杂性控制在最小范围。对于流式中断的情况前端要做好重连和状态恢复。对于需要后处理的内容先缓冲完整结果处理完再一次性输出不要边流边处理。7. 从架构图到代码一个最小可运行示例7.1 项目结构设计说了这么多理论最后给一个可以直接跑的最小示例。这个示例实现了“用户提问→Agent规划→调用工具→LLM生成回答”的完整链路。项目结构如下ai-app/ ├── main.py # 入口FastAPI应用 ├── orchestrator.py # 编排层任务规划与执行 ├── tools/ │ ├── __init__.py │ ├── registry.py # 工具注册中心 │ ├── database.py # 数据库查询工具 │ └── search.py # 向量检索工具 ├── llm/ │ ├── __init__.py │ ├── client.py # LLM客户端封装 │ └── router.py # 模型路由 ├── session/ │ ├── __init__.py │ └── manager.py # 会话管理 └── config.py # 配置7.2 核心模块的代码实现先看工具注册中心这是能力层的核心# tools/registry.py from typing import Callable, Any import json class ToolRegistry: def __init__(self): self._tools {} def register(self, name: str, description: str, parameters: dict): def decorator(func: Callable): self._tools[name] { function: func, schema: { name: name, description: description, parameters: parameters } } return func return decorator def get_schemas(self) - list: return [t[schema] for t in self._tools.values()] def execute(self, name: str, arguments: dict) - Any: if name not in self._tools: raise ValueError(f未知工具: {name}) return self._tools[name][function](**arguments) registry ToolRegistry() registry.register( namequery_revenue, description查询指定公司指定季度的营收数据, parameters{ type: object, properties: { company: {type: string, description: 公司名称}, quarter: {type: string, description: 季度格式如2024Q3} }, required: [company, quarter] } ) def query_revenue(company: str, quarter: str) - dict: # 实际项目中这里查数据库 return {company: company, quarter: quarter, revenue: 12500000}再看编排层的核心逻辑# orchestrator.py import json from tools.registry import registry from llm.client import LLMClient class Orchestrator: def __init__(self, llm_client: LLMClient): self.llm llm_client self.max_steps 5 def run(self, user_input: str, history: list) - str: # 第一步任务规划 plan self._plan(user_input, history) # 第二步执行计划 results [] for step in plan[steps]: if step[action] tool_call: result registry.execute( step[tool_name], step[arguments] ) results.append(result) elif step[action] llm_generate: result self.llm.generate( promptstep[prompt], contextresults ) results.append(result) # 第三步生成最终回答 return self._final_answer(user_input, results) def _plan(self, user_input: str, history: list) - dict: tools_desc json.dumps(registry.get_schemas(), ensure_asciiFalse) prompt f你是一个任务规划器。根据用户输入制定执行计划。 可用工具{tools_desc} 用户输入{user_input} 输出JSON格式的计划包含steps数组。每个step有action字段 action为tool_call时包含tool_name和arguments action为llm_generate时包含prompt。 response self.llm.generate(prompt) return json.loads(response) def _final_answer(self, user_input: str, results: list) - str: prompt f根据以下信息回答用户问题。 用户问题{user_input} 执行结果{json.dumps(results, ensure_asciiFalse)} 请用简洁的自然语言回答。 return self.llm.generate(prompt)7.3 跑通之后的扩展方向这个最小示例跑通之后你可以按需扩展。加工具在registry里注册新的工具函数就行编排层不需要改。加模型路由在LLMClient里根据任务类型选择不同模型。加缓存在LLMClient的generate方法里加一层缓存查询。加监控在编排层的每个步骤前后打点记录耗时和token消耗。架构设计的价值就在于每个扩展点都是独立的改一个地方不会牵一发而动全身。这也是为什么我一开始就强调分层——分层不是为了好看而是为了在需求变化的时候你只需要改一层其他层不动。8. 一些关于架构演进的个人体会我做AI应用这两年多最大的体会是架构不是一开始就设计完美的而是随着对业务理解的深入逐步演进的。第一个版本可能就是一个简单的prompt调用第二个版本加了RAG第三个版本引入了Agent编排第四个版本才做了模型路由和成本优化。每一步演进都是被真实问题驱动的而不是为了架构而架构。另一个体会是不要过度设计。我见过一些项目一开始就上多Agent协作、上复杂的规划器、上全套的监控体系结果开发周期拉长了好几倍上线后发现用户根本用不到那些复杂功能。先用最简单的方案跑通核心链路拿到真实用户反馈再针对性地优化这个节奏更稳。最后一个建议把架构图画出来贴在团队所有人都能看到的地方。不是为了好看而是为了让每个人都知道自己的代码在整个系统中的位置知道自己的模块和谁交互、通过什么接口交互。很多协作问题本质上都是因为大家对架构的理解不一致。一张清晰的架构图能省掉很多沟通成本。
返回列表