☰
图解AI应用架构设计:从LLM调用到Agent编排的工程实践
2026/10/26 6:37:48 网站建设 项目流程

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

很多人第一次接触AI应用开发,脑子里冒出来的第一个问题就是:我到底该从哪下手?是直接调个大模型接口就完事,还是得搞一套完整的工程体系?这个问题我在过去一年多里被问过不下几十次,每次我的回答都是一样的——先别急着写代码,先把架构图想清楚。

“图解AI应用架构设计”这个主题,听起来像是要画一堆框框箭头,但实际上它解决的是一个非常具体的问题:当你手里有一个业务需求,需要用到LLM的能力时,你应该怎么组织你的系统?哪些部分交给模型,哪些部分交给传统代码,哪些部分需要额外的中间层来协调?这些问题不想清楚,代码写得再多也是白搭。

我见过太多团队上来就闷头调API,结果做到一半发现上下文管理一团糟、工具调用逻辑散落在各个角落、换个模型就要重写半个项目。这些问题的根源都不在代码层面,而在架构层面没有提前规划。所以这篇文章我想把AI应用架构这件事从头到尾拆一遍,不讲虚的,就讲实际项目中怎么设计、怎么选型、怎么避坑。

这篇文章适合谁看?如果你正在做AI应用开发,或者准备启动一个涉及LLM的项目,再或者你是个技术负责人需要评估AI应用的工程方案,那这篇内容应该能帮你省下不少试错时间。我会从整体架构思路讲起,然后逐层拆解核心组件,最后给出可直接参考的实操方案和排查经验。

2. AI应用架构的整体设计思路拆解

2.1 为什么不能把AI应用当成普通Web服务来设计

传统Web服务的架构设计有一套非常成熟的范式:前端、后端、数据库、缓存,请求进来处理完返回,逻辑是确定性的。但AI应用不一样,它的核心处理单元是LLM,而LLM的行为本质上是概率性的。同一个输入,你可能得到不同的输出;同一个问题,换个模型可能完全答不上来。

这就导致了一个根本性的差异:传统架构追求的是稳定和可预测,AI应用架构追求的是在不确定中保持可控。你不能假设模型每次都能返回你想要的格式,不能假设模型不会胡说八道,不能假设模型能记住上一轮对话的内容。所有这些“不能假设”的东西,都需要在架构层面用工程手段来兜底。

我通常会把AI应用的架构分成四层来看:交互层、编排层、能力层、数据层。交互层负责和用户打交道,编排层负责协调整个处理流程,能力层封装了LLM和各种工具,数据层管理上下文、记忆和知识库。这个分层方式不是唯一的,但它足够清晰,能帮你在设计阶段就想明白每个模块的职责边界。

2.2 编排层才是AI应用的心脏

如果让我选一个AI应用架构中最关键的模块,我会毫不犹豫地选编排层。这一层做的事情,简单来说就是决定“什么时候调模型、什么时候调工具、什么时候该停下来”。听起来简单,但实际做起来非常复杂。

举个具体的例子:用户问“帮我查一下上个月的销售数据,然后生成一份分析报告”。这个请求至少涉及三个步骤:第一步是理解用户意图,第二步是调用数据查询工具获取数据,第三步是把数据交给LLM生成报告。编排层需要决定这三个步骤的执行顺序、每一步的输入输出格式、以及如果某一步失败了该怎么处理。

在没有编排层的情况下,这些逻辑会散落在业务代码的各个角落,改一个流程就要动好几处代码。有了编排层之后,你可以把整个处理流程抽象成一个可配置的管道,每个节点是一个处理单元,节点之间通过标准化的消息格式传递数据。这样做的好处是,流程的修改只需要调整配置,不需要改代码。

目前业界比较常见的编排方案有两种:一种是用代码硬编码流程,适合逻辑固定的场景;另一种是用声明式的方式定义流程,适合需要灵活调整的场景。我个人的建议是,项目初期用代码硬编码就够了,等流程稳定下来再考虑抽象成配置。过早抽象反而会增加理解成本。

2.3 能力层的设计原则:封装变化,暴露稳定

能力层是AI应用和外部世界交互的窗口。它包含了LLM调用、工具调用、外部API集成等。这一层的设计原则可以用八个字概括:封装变化,暴露稳定。

什么意思?LLM的接口格式、参数名称、返回结构,不同厂商之间差异很大,而且经常变。如果你在业务代码里直接调某个厂商的SDK,那换模型的时候就要改一大堆地方。正确的做法是在能力层做一层适配,对外暴露统一的接口,内部处理不同厂商的差异。

工具调用也是同样的道理。你可能会接入搜索工具、数据库查询工具、代码执行工具等等,每个工具的调用方式都不一样。如果不在能力层做统一封装,编排层就要处理各种不同的调用协议,复杂度会急剧上升。

我在实际项目中会定义一个统一的工具接口,包含工具名称、描述、参数schema、执行方法这几个要素。所有工具都实现这个接口,编排层只需要根据工具名称和参数来调用,不需要关心底层是什么。这个设计看起来简单,但它带来的灵活性是非常大的——新增一个工具只需要实现接口并注册,不需要改动编排层的任何代码。

2.4 数据层的核心挑战:上下文管理

数据层在AI应用中承担的角色比传统应用要重得多。传统应用的数据层主要是存储和查询,AI应用的数据层还要负责上下文管理、记忆管理、知识检索。

上下文管理是其中最棘手的问题。LLM的上下文窗口是有限的,你不能把所有历史对话都塞进去。但如果你只保留最近几轮对话,模型又会丢失早期的关键信息。怎么在有限的空间里保留最有价值的信息,这是一个需要仔细设计的问题。

我常用的策略是分层管理:最近几轮对话保留完整内容,稍早的对话做摘要压缩,更早的对话只保留关键实体和结论。这样既控制了token消耗,又保留了必要的信息。具体的压缩比例和保留策略需要根据业务场景来调整,没有一刀切的标准。

记忆管理是另一个重要模块。有些信息需要在多次会话之间保持,比如用户的偏好设置、之前讨论过的项目背景等。这些信息不能放在对话上下文里,需要单独存储和检索。我通常会用向量数据库来做语义检索,把需要长期记忆的信息存进去,每次会话开始时根据当前问题检索相关记忆注入上下文。

3. 核心组件深度解析与实操要点

3.1 LLM调用层:不只是发个请求那么简单

很多人觉得调LLM就是发个HTTP请求,把prompt传进去等结果就行了。但实际项目中,LLM调用层需要处理的问题远不止这些。

首先是重试和降级。LLM服务不是100%可用的,偶尔会超时或者返回错误。你需要设计合理的重试策略,比如指数退避重试,同时要设置最大重试次数避免无限等待。如果重试多次仍然失败,还需要有降级方案,比如切换到备用模型或者返回预设的兜底回复。

其次是流式输出的处理。很多场景下用户不希望等整个回复生成完才看到内容,而是希望像打字机一样逐字显示。流式输出的处理比一次性返回要复杂得多,你需要处理数据块的拼接、异常中断的恢复、以及前端渲染的同步。

还有一个容易被忽视的问题是token计数和成本控制。LLM调用是按token收费的,如果不做监控,月底账单可能会吓你一跳。我通常会在调用层记录每次请求的输入token数、输出token数和对应的成本,设置日限额和告警阈值。

# LLM调用层的简化示例 class LLMClient: def __init__(self, model_config, max_retries=3): self.model_config = model_config self.max_retries = max_retries self.token_usage = {"input": 0, "output": 0} def call(self, messages, tools=None, stream=False): for attempt in range(self.max_retries): try: response = self._do_request(messages, tools, stream) self._track_usage(response) return response except RateLimitError: time.sleep(2 ** attempt) except Exception as e: if attempt == self.max_retries - 1: return self._fallback_response(e)

这段代码看起来简单,但每个细节都有讲究。重试的间隔用指数退避是为了避免在服务端压力大时雪上加霜,降级响应的设计是为了保证用户体验不会因为后端问题而完全中断。

3.2 Agent架构:从单轮到多轮的跃迁

Agent是这两年AI应用领域最热的概念之一,但很多人对Agent的理解还停留在“能调工具的LLM”这个层面。实际上,Agent架构的核心在于自主决策和循环执行。

一个典型的Agent工作流程是这样的:接收任务后,Agent先分析任务需要哪些步骤,然后逐步执行,每执行一步就观察结果,根据结果决定下一步做什么,直到任务完成或者达到终止条件。这个循环过程就是Agent和普通LLM调用的本质区别。

Agent架构中最关键的设计决策是终止条件的设定。如果没有合理的终止条件,Agent可能会陷入无限循环,不断调用工具却永远不给出最终答案。我通常会在三个层面设置终止条件:最大迭代次数、任务完成标志、以及异常中断机制。

工具设计也是Agent架构中的重点。工具的描述要清晰准确,参数schema要严格定义,返回结果要结构化。我见过很多Agent项目失败的原因就是工具描述太模糊,导致模型不知道该在什么时候调用哪个工具。

实操心得:给Agent设计工具时,工具名称和描述要站在模型的角度来写,而不是站在开发者的角度。模型只能通过描述来理解工具的用途,所以描述要具体、无歧义。比如“查询数据库”就不如“根据订单号查询订单状态和物流信息”来得清晰。

3.3 MCP协议:工具集成的标准化尝试

MCP是最近讨论度很高的一个话题,它的全称是Model Context Protocol,本质上是一套标准化的工具调用协议。在没有MCP之前,每个AI应用接入工具的方式都不一样,有的用function calling,有的用自定义的插件机制,导致工具很难在不同应用之间复用。

MCP的核心思路是把工具的提供方和使用方解耦。工具提供方只需要按照MCP协议实现一个服务端,任何支持MCP的客户端都可以直接使用这些工具,不需要做额外的适配工作。这个思路和USB接口的标准化很像——有了统一的标准,设备之间的互联就变得简单了。

在实际使用中,MCP的部署方式主要有两种:本地进程和远程服务。本地进程适合对延迟敏感的场景,远程服务适合需要共享工具的场景。配置MCP服务时需要注意权限控制,因为MCP工具可以执行文件操作、网络请求等敏感操作,必须确保只有授权的应用才能调用。

{ "mcpServers": { "database-query": { "command": "python", "args": ["-m", "mcp_server_db"], "env": { "DB_CONNECTION": "postgresql://localhost:5432/mydb" } }, "file-system": { "command": "node", "args": ["mcp-server-filesystem", "--root", "/data/workspace"] } } }

这个配置示例展示了两个MCP服务的定义。第一个是数据库查询服务,第二个是文件系统服务。每个服务都指定了启动命令、参数和环境变量。这种配置方式的好处是,你可以在不修改应用代码的情况下增减工具。

3.4 上下文与记忆管理:让AI记住该记住的

上下文管理是AI应用中最容易被低估的模块。很多项目在demo阶段表现很好,一到实际使用就暴露出问题——多轮对话之后模型开始答非所问,或者忘记了之前确认过的关键信息。

解决这个问题的核心思路是:不要把上下文管理当成简单的消息队列,而要当成一个信息检索和压缩系统。每一轮对话产生的信息,都需要经过评估——哪些需要完整保留,哪些可以压缩,哪些可以丢弃。

我通常会把上下文分成三个区域:工作区、短期记忆区、长期记忆区。工作区存放当前轮次直接相关的信息,短期记忆区存放最近几轮对话的摘要,长期记忆区存放跨会话的关键信息。每次调用LLM时,从这三个区域按优先级选取内容拼装成最终的prompt。

压缩策略的选择也很关键。简单的截断会导致信息丢失,粗暴的摘要可能丢失细节。我比较推荐的做法是保留最近N轮的完整对话,对更早的对话做结构化摘要——提取出关键实体、决策结论、待办事项等,而不是笼统地概括。

4. 完整实操流程:从零搭建一个AI应用

4.1 环境准备与技术选型

在开始搭建之前,需要先确定技术栈。我的建议是不要追求最新最热的技术,而是选择你团队最熟悉的语言和框架。AI应用的核心复杂度在架构设计上,不在编程语言上。

如果你用Python,FastAPI是一个不错的选择,异步支持好,生态成熟。如果你用Java,Spring Boot加上HTTP客户端也能满足需求。前端方面,React或Vue都可以,关键是流式渲染的处理要做好。

模型选择上,我建议至少接入两个不同厂商的模型,一个作为主力,一个作为备用。主力模型选择能力最强的,备用模型选择响应速度快的。这样在主力模型不可用时可以快速切换,保证服务可用性。

4.2 核心模块搭建步骤

第一步是搭建LLM调用层。这一层需要实现统一的调用接口、重试机制、流式输出处理、token计数。建议先用最简单的实现跑通流程,再逐步增加功能。

第二步是搭建工具注册和调用框架。定义一个工具接口,包含名称、描述、参数schema、执行方法。所有工具实现这个接口,通过注册中心统一管理。调用时根据工具名称查找并执行。

第三步是搭建编排引擎。编排引擎负责接收用户输入,决定调用哪些工具、以什么顺序调用、如何处理中间结果。初期可以用简单的if-else逻辑,随着流程复杂度的增加再考虑引入状态机或工作流引擎。

第四步是搭建上下文管理模块。实现消息的存储、检索、压缩和拼装。这一块需要和LLM调用层紧密配合,确保每次调用时传入的上下文是最优的。

第五步是搭建监控和日志系统。记录每次LLM调用的输入输出、token消耗、响应时间、错误信息。这些数据对于后续的优化和问题排查至关重要。

4.3 参数配置与调优经验

LLM调用有几个关键参数需要仔细调整。temperature控制输出的随机性,值越高输出越多样但越不稳定,值越低输出越确定但可能过于保守。对于需要精确回答的场景,建议设置在0.1到0.3之间;对于创意生成场景,可以设置在0.7到0.9之间。

max_tokens控制输出的最大长度。设置得太小会导致回答被截断,设置得太大则会浪费token。我通常会根据场景预估一个合理的值,比如问答场景设置500到1000,长文生成场景设置2000到4000。

top_p是另一个控制输出多样性的参数,和temperature配合使用。一般来说,调整其中一个就够了,不需要同时调两个。我个人的习惯是固定top_p为0.9,只调整temperature。

注意事项:不同厂商的模型对参数的敏感度不同,同样的temperature值在不同模型上的表现可能差异很大。切换模型时一定要重新测试参数配置,不要直接沿用。

4.4 部署与运维要点

AI应用的部署和传统应用有几个不同之处。首先是资源需求不同,LLM调用是IO密集型的,不需要太强的CPU,但需要稳定的网络连接。其次是扩缩容策略不同,AI应用的负载波动可能很大,需要根据队列长度而不是CPU使用率来触发扩容。

日志记录要特别注意脱敏。用户的输入可能包含敏感信息,在记录日志之前需要做脱敏处理。同时,LLM的输出也可能包含不当内容,需要做内容过滤后再返回给用户。

监控指标方面,除了常规的QPS、延迟、错误率之外,还需要关注token消耗速率、工具调用成功率、上下文长度分布等AI应用特有的指标。这些指标能帮你提前发现潜在问题。

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

5.1 模型输出格式不稳定的排查思路

这是最常见的问题之一。你明明在prompt里要求模型返回JSON格式,但模型有时候会加一些额外的解释文字,导致解析失败。

排查这个问题的第一步是检查prompt的明确程度。如果你只是说“返回JSON”,模型可能会理解成“主要返回JSON,可以附带一些说明”。更明确的写法是“只返回JSON,不要包含任何其他文字”。

如果prompt已经足够明确但问题仍然存在,可以考虑在调用层加一个输出解析器,自动提取JSON部分并忽略其他内容。再不行的话,可以使用function calling或者structured output等强制格式化的功能。

问题表现可能原因解决方案
输出包含额外文字prompt不够明确强调只返回目标格式
JSON解析失败模型生成了非法JSON增加输出校验和修复逻辑
字段缺失模型遗漏了某些字段在prompt中明确列出所有字段
格式偶尔正确偶尔错误模型随机性导致降低temperature或使用强制格式化

5.2 工具调用失败的常见原因

工具调用失败通常有几种表现:模型不调用工具、调用了错误的工具、调用参数不正确、工具执行超时。

模型不调用工具,通常是因为工具描述不够清晰,或者prompt中没有明确指示可以使用工具。解决方法是优化工具描述,并在系统prompt中明确说明工具的使用场景。

调用参数不正确,往往是因为参数schema定义不够严格,或者模型对参数含义的理解有偏差。建议在参数描述中给出具体的示例值,帮助模型理解。

工具执行超时,需要检查工具本身的性能,以及是否设置了合理的超时时间。对于耗时的工具,可以考虑异步执行,先返回一个任务ID,后续再查询结果。

5.3 上下文丢失的排查与修复

多轮对话中上下文丢失是一个很隐蔽的问题。用户可能在前几轮提到过一个关键信息,但几轮之后模型就“忘记”了。

排查这个问题的第一步是检查上下文拼装逻辑。打印出每次调用LLM时实际传入的messages,看看关键信息是否还在。如果已经被截断或压缩掉了,就需要调整压缩策略。

另一个可能的原因是压缩摘要时丢失了关键信息。摘要的质量直接影响上下文的完整性。建议在摘要prompt中明确要求保留实体名称、数值、决策结论等关键信息。

如果问题依然存在,可以考虑引入外部记忆存储。把关键信息存到向量数据库,每次对话时根据当前问题检索相关记忆注入上下文。这样即使对话历史被压缩,关键信息也不会丢失。

5.4 性能优化的实操技巧

AI应用的性能瓶颈通常在LLM调用上,一次调用可能需要几秒甚至十几秒。优化性能的核心思路是减少不必要的调用和并行化必要的调用。

减少不必要调用的方法包括:缓存常见问题的回答、用小模型做意图识别再决定是否调用大模型、合并多个小请求为一个大请求。

并行化的方法包括:多个独立工具调用并行执行、流式输出让用户更早看到部分结果、预加载可能用到的上下文。

还有一个容易被忽视的优化点是prompt的精简。过长的prompt不仅增加token消耗,还会增加模型的推理时间。定期审查prompt,删除冗余的指令和示例,能带来明显的性能提升。

5.5 安全与合规的注意事项

AI应用的安全问题需要从多个层面来考虑。输入层面要做prompt注入防护,防止用户通过精心构造的输入来操纵模型行为。输出层面要做内容过滤,防止模型生成不当内容。

工具调用层面要做权限控制,确保模型只能调用被授权的工具,且工具的执行范围受到限制。比如文件操作工具应该限制在特定目录下,数据库查询工具应该限制为只读。

数据层面要做好隔离,不同用户的数据不能混在一起。上下文管理模块需要确保每个用户的对话历史是独立的,检索记忆时也要按用户维度过滤。

实操心得:prompt注入防护没有一劳永逸的方案,建议采用多层防御策略。第一层是在输入预处理阶段过滤明显的注入模式,第二层是在系统prompt中明确指令边界,第三层是在输出阶段做异常检测。三层配合能挡住绝大多数攻击。

6. 架构演进与扩展方向

6.1 从单Agent到多Agent协作

当业务复杂度增加到一定程度,单个Agent可能无法胜任所有任务。这时候可以考虑多Agent架构,让不同的Agent负责不同的领域,通过消息传递来协作。

多Agent架构的关键设计点是Agent之间的通信协议和任务分配机制。通信协议要定义好消息的格式和路由规则,任务分配机制要决定什么任务交给哪个Agent处理。

我见过比较成功的多Agent实践是“主管+专家”模式:一个主管Agent负责理解用户需求并拆解任务,然后分配给对应的专家Agent执行,最后汇总结果返回给用户。这种模式的好处是每个Agent的职责清晰,便于独立优化。

6.2 从固定流程到动态编排

项目初期,处理流程通常是固定的——先做什么、再做什么都是写死的。但随着业务的发展,你可能需要根据不同的输入动态调整流程。

动态编排的实现方式有几种:一种是基于规则的,根据输入的特征匹配预定义的流程模板;另一种是基于模型的,让LLM来决定执行哪些步骤。前者更可控,后者更灵活。

我的建议是先从基于规则的方案开始,等积累了足够的数据和经验之后,再考虑引入基于模型的动态编排。直接上模型方案的风险是行为不可预测,调试困难。

6.3 可观测性建设的进阶思路

AI应用的可观测性比传统应用要复杂得多。除了常规的日志、指标、链路追踪之外,还需要关注模型行为的变化。

我建议建立一个“模型行为基线”,记录正常情况下模型的输出分布、工具调用频率、上下文长度等指标。当实际运行数据偏离基线时,触发告警。这样可以在问题影响到用户之前就发现异常。

另一个有用的实践是定期做“回归测试”。准备一组标准的测试用例,每次修改prompt、更换模型、调整参数之后都跑一遍,确保核心功能没有退化。这个习惯能帮你避免很多“改了一个地方,坏了另一个地方”的问题。

7. 一些踩坑之后的个人体会

做AI应用这一年多,最大的体会就是:不要试图让模型做它不擅长的事情。模型擅长理解和生成自然语言,不擅长精确计算和确定性逻辑。把这两类任务分开处理,该用代码的地方用代码,该用模型的地方用模型,系统会稳定很多。

另一个体会是,prompt engineering很重要,但它不是万能的。当你的prompt复杂到需要几十行才能说清楚的时候,可能说明这个任务本身就不应该完全交给模型。考虑把任务拆解,把确定性的部分抽出来用代码实现,只把真正需要理解能力的部分留给模型。

还有一点是关于测试的。AI应用的测试和传统应用完全不同,你不能断言输出等于某个固定值。我通常会用“断言输出包含某些关键信息”或者“断言输出满足某些结构要求”的方式来做测试。虽然不如传统测试严格,但能覆盖大部分回归场景。

最后说一个关于工具选型的建议:不要因为某个框架火就用它。先想清楚你的需求是什么,再去评估框架能不能满足。很多框架为了解决通用性问题引入了大量抽象,对于简单场景反而是负担。从最简单的方案开始,遇到瓶颈再引入框架,这个顺序不要搞反。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询