拓冰建站拓冰建站
首页 / 资讯中心 / 正文

AgentMesh实战:AI Agent微服务架构中的状态管理与工具编排

先给结论AgentMesh 这类以 AI Agent 为核心、用微服务方式组织的实战项目最值得研究的不是它“多智能体”的宣传点而是 Agent 状态管理、工具编排、模型调用和会话存储怎么在分布式系统里落地。它本质上是一个“把大模型接入业务系统”的工程框架适合已经会 Spring Boot、了解微服务基础、想从写 Demo 进阶到做项目的人。如果你只是调 API 玩聊天不需要这套东西但如果你要做带业务流程、会话记忆、工具调用和任务调度的 Agent 平台这套架构思路可以直接复用。先说清楚一个容易混淆的点AgentMesh 不是一个像 ChatGPT 那样开箱即用的产品它是一个面向服务端集成与二次开发的平台工程底座。也就是说它解决的主要问题不是“怎么生成一句高质量回答”而是“在多个微服务环境下如何把用户的请求可靠地路由到 Agent、让 Agent 去调用工具、维护多轮会话上下文、再把最终结果异步返回给前端”。这种项目更适合从“能不能跑通”和“能不能批量跑”两个维度去验收。下面我按照从零跑一个微服务 Agent 平台的顺序把环境、核心模块、代码链路、参数策略、批量处理和排查经验拆开讲。1. 先理解 AgentMesh 要解决的工程问题很多初学者一听到 AI Agent 微服务就默认要把 LangChain、向量库、大模型全部堆到一套系统里。实际上一旦进入微服务架构最先要处理的不是“智能”而是“服务边界”。1.1 Agent 是业务编排层不是模型网关在常规单体应用里用户请求到了 Controller然后同步调用一个 LLM SDK返回结果就结束了。但在 AgentMesh 这类平台里Agent 不是直接对接大模型的一层而是夹在业务服务和模型服务之间的编排层。一个典型的请求链路是这样的前端发来一个自然语言请求网关服务先做鉴权、限流、路由请求进入 Agent 编排服务编排服务判断这个请求是否需要调用工具比如查询订单、写入工单、检索知识库如果需要工具则调用对应的微服务接口全部工具结果收集完成后再组装成最终 Prompt 发给 LLMLLM 返回结果后Agent 服务把回复、调用日志、会话 ID 持久化为什么这样设计因为模型本身不具备执行业务操作的能力它只能生成文本。所有真正影响数据的操作比如下单、改状态、发通知都必须由业务系统执行。Agent 只是根据模型生成的结构化意图决定“接下来调用哪个服务”。所以你在看 AgentMesh 代码时不要只盯着“怎么跟模型聊天”要把重心放在“模型输出如何映射成服务调用”。这一步做得不好后面所有环节都是空中楼阁。1.2 为什么要微服务而不是单体如果只是做个人助手单体完全够用。但 AgentMesh 面向的是多业务域平台场景用户服务、会话服务、工具服务、模型接入服务、审计日志服务各管一摊。把这些拆开有几个实际好处模型调用和业务逻辑解耦换模型时不用改业务代码工具接口可以独立扩展新增一个工具服务不影响主链路会话存储和消息队列分开批量任务不会卡用户请求不同服务的资源需求不同模型推理和数据库操作可以单独扩容坏处也很明显开发复杂度高、链路长、排错难。所以我不建议第一次做 Agent 项目的人直接上十几个微服务。先从核心的 4 到 5 个服务起步比如网关、Agent 编排、工具服务、模型接入、会话存储。2. 环境准备与前置条件AgentMesh 类项目对开发机的要求不算低但只要不跑超大模型普通配置也能完成开发调试。我自己通常这样划分环境。2.1 基础软件栈这里给一套通用组合具体版本以自己的项目为准但要保证这些组件齐全组件用途版本建议JDK服务端开发17 或 21Spring Boot微服务基础框架3.xSpring Cloud Alibaba服务注册、配置、网关2023.x 或对应版本Nacos注册中心 配置中心2.xMySQL会话、任务、日志存储5.7 或 8.xRedis缓存、分布式锁、临时状态6.xRabbitMQ 或 Kafka异步消息、批量任务队列按已有环境选择Maven依赖管理3.8如果你的机器只有 16G 内存建议把 MySQL、Redis、Nacos 都装在同一台机器或直接用 Docker Compose 起不要开太多中间件节点否则还没写业务代码机器就卡死了。2.2 模型接入准备AgentMesh 的模型层通常通过 Spring AI 这类框架做统一封装。在开始之前你需要准备一个可用的 LLM 接口。这个接口可以是云端 API也可以是本地部署的开源模型。如果你是学习用途建议先接云端 API把链路跑通后再考虑本地模型。本地模型的好处是数据不出内网但代价是显存和推理速度。一个 7B 模型在消费级显卡上勉强能用并发稍微上来就很容易超时。我不建议第一次做这个项目时就同时接多家模型。先接一个跑通 Agent 编排再抽象统一接口慢慢加其他模型。2.3 启动顺序的讲究微服务项目最容易犯的错就是“一次性启动全部服务”结果一屏报错完全不知道该看哪个。更稳妥的顺序是先启动 Nacos确认注册中心可用再启动 MySQL、Redis确认数据源和缓存正常启动基础服务比如用户服务、会话服务启动模型接入服务确认模型接口连通最后启动网关和 Agent 编排服务每启动一个服务就看一下 Nacos 的服务列表确认服务注册成功再启动下一个。这样即使启动报错也能快速定位是哪一层的问题。注意不要在还没确认数据库连接的情况下就启动 Agent 服务。Agent 服务启动时通常会初始化表结构、缓存连接和工具注册列表数据库不可用会导致它反复重启。3. 项目模块设计与基础骨架AgentMesh 的模块划分建议按照业务边界来而不是按技术层来。下面是一份通用的模块参考不是所有项目都必须照抄但对理解整体架构很有帮助。3.1 服务模块一览服务名职责关键功能agent-gateway统一网关鉴权、限流、路由、日志agent-serverAgent 编排中心会话管理、工具编排、Prompt 组装、LLM 调用agent-tool-server工具服务提供可被 Agent 调用的业务接口agent-model-server模型接入服务统一封装不同模型 API处理超时和重试agent-session-server会话存储服务会话历史、上下文窗口、向量存储agent-task-server任务服务异步任务、批量任务、定时触发、失败重试主要目录可以这样组织AgentMesh/ ├── agent-gateway/ ├── agent-server/ ├── agent-tool-server/ ├── agent-model-server/ ├── agent-session-server/ ├── agent-task-server/ ├── agent-common/ │ ├── result/ │ ├── exception/ │ ├── context/ │ └── utils/ └── agent-api/ ├── dto/ ├── enums/ └── feign/agent-common放公共返回结构、异常处理、工具类agent-api放服务间调用的 DTO 和 Feign 接口。这个拆分很重要因为多个服务之间要共享请求参数和返回结构如果每个服务各写一份很容易字段不一致。3.2 数据表设计要提前想好的地方Agent 平台的数据表比普通业务系统更依赖状态和日志。除了用户表下面几张表很关键会话表记录会话 ID、用户 ID、会话标题、创建时间、最近活跃时间消息表记录每条消息的角色、内容、消息类型、关联会话 ID、令牌数任务表记录异步任务状态、任务类型、输入参数、输出结果、失败原因工具调用记录表记录 Agent 每一次调用工具的参数、返回结果、耗时在设计时状态字段不要用无意义的 0 和 1建议用字符串枚举比如 WAITING、RUNNING、SUCCESS、FAILED。这样日志和排查时一眼就能看懂。另外消息表会随着多轮会话快速增长。如果不做归档查询历史会话时会越来越慢。建议按时间做分区或者把冷数据定期同步到归档表。4. Agent 编排核心链路实战这一节是整篇的重点。AgentMesh 的 Agent 编排服务是整个系统的控制中枢核心逻辑可以拆成五步接收请求、判断是否需要工具、调用工具、组装最终 Prompt、返回结果。4.1 从 HTTP 入口到 Agent 编排网关把请求转发到 agent-server 后在 controller 层不会做太多业务逻辑只负责参数校验和调用编排服务。RestController RequestMapping(/agent) public class AgentController { private final AgentOrchestrator orchestrator; public AgentController(AgentOrchestrator orchestrator) { this.orchestrator orchestrator; } PostMapping(/chat) public ResultAgentResponse chat(RequestBody AgentRequest request) { String userId UserContext.getUserId(); AgentResponse response orchestrator.execute(userId, request); return Result.success(response); } }这里的重点不是 controller而是AgentOrchestrator。编排器决定了整个 Agent 的执行流程。Service public class AgentOrchestrator { private final ToolRegistry toolRegistry; private final ModelService modelService; private final SessionService sessionService; public AgentResponse execute(String userId, AgentRequest request) { // 1. 读取或创建会话状态 Conversation conversation sessionService.getOrCreate(userId, request.getSessionId()); // 2. 先让模型判断是否需要工具调用 String input request.getInput(); AgentDecision decision modelService.decide(input, conversation.getRecentMessages()); // 3. 如果需要工具则执行工具并收集结果 ListToolResult toolResults new ArrayList(); if (decision.isNeedTool()) { for (ToolCall call : decision.getToolCalls()) { ToolResult result toolRegistry.execute(call); toolResults.add(result); } } // 4. 组装最终 Prompt 并调用模型生成回复 String finalPrompt PromptBuilder.build(conversation, decision.getReasoning(), toolResults); String answer modelService.generate(finalPrompt); // 5. 持久化消息 sessionService.saveMessage(userId, conversation.getId(), user, input); sessionService.saveMessage(userId, conversation.getId(), assistant, answer); return AgentResponse.builder() .sessionId(conversation.getId()) .answer(answer) .toolResults(toolResults) .build(); } }这里有个容易被忽略的设计点模型先输出一个“决策结果”这个结果可能是普通文本也可能是结构化工具调用指令。在实际项目里我会定义一个统一的AgentDecision结构里面包含reasoning、needTool、toolCalls三个字段。reasoning是模型思考过程的简短文本一般只在调试时记录不返回给用户needTool表示这次请求需不需要调用工具toolCalls包含要调用的工具名和参数如果没有这个中间层直接把原始模型输出拿来解析容易因为格式不稳定而出错。4.2 工具注册与执行机制工具是 Agent 与业务系统交互的桥梁。每个工具本质上就是一个可以被模型选择的“函数”但它在代码里的形式需要统一方便注册和调用。我建议定义一个工具接口所有工具都实现该接口public interface AgentTool { String getName(); String getDescription(); ToolSchema getSchema(); ToolResult execute(MapString, Object params); }getName是工具标识比如query_ordergetDescription是给模型看的说明模型根据这个描述决定是否使用工具getSchema描述工具需要哪些参数供模型生成结构化调用execute是真正执行逻辑工具注册可以用 Spring 的ApplicationContext自动收集也可以用ToolRegistry手动登记Component public class ToolRegistry { private final MapString, AgentTool tools new ConcurrentHashMap(); public void register(AgentTool tool) { tools.put(tool.getName(), tool); } public ToolResult execute(ToolCall call) { AgentTool tool tools.get(call.getToolName()); if (tool null) { return ToolResult.failure(tool not found: call.getToolName()); } try { return tool.execute(call.getParams()); } catch (Exception e) { return ToolResult.failure(tool execute error: e.getMessage()); } } }这个注册机制最重要的作用是让“模型能看到的工具列表”和“代码里真正能执行的工具”保持一致。如果模型描述了一堆工具代码里根本没有实现调用时就会报错。我一般在启动时会把已注册的工具列表打印出来确认注册数量和预期一致。4.3 工具调用失败时怎么办工具调用不是百分之百成功的。数据库可能超时、权限可能不足、参数可能不合法。在 Agent 编排里工具失败后不应该直接让整个请求失败而是应该把错误信息作为上下文让模型自己决定怎么处理。我的做法是当ToolResult返回失败时不中断流程而是把这段失败信息拼到最终 Prompt 里。比如工具 query_order 调用失败原因orderId 不能为空。 请根据这个错误帮助用户检查输入。这样模型就能生成一个更自然的应对用户看到的不再是“系统错误”而是“请确认订单号是否有误”。但如果工具连续失败多次说明可能是系统级问题而不是参数问题。这时候要设置重试上限和熔断机制避免模型反复调用一个必然失败的工具。5. 会话状态与上下文管理Agent 平台不能像普通接口那样无状态。用户上一句说“帮我查订单”下一句说“把价格最高的那个取消”模型必须理解“那个”指的是哪条订单。所以会话状态管理是 AgentMesh 的核心基础设施。5.1 会话 ID 的生成和传递会话 ID 最好由服务端生成使用 UUID 或雪花算法。不建议用自增 ID因为并发环境下容易出现重复而且暴露业务量。在首次对话时前端可以不传 sessionId由 agent 服务创建后返回。后续对话必须带上同一个 sessionId服务端根据这个 ID 拉取历史消息。会话 ID 的传递要注意不要把一次请求的 ID 和会话 ID 混用。一次请求关心的是本次调用的 requestId用于日志追踪会话 ID 关心的是用户身份和时间范围用于上下文管理。两者职责不同。5.2 上下文窗口怎么截断大模型有上下文窗口限制不管支持 128K 还是 200K都不能无限塞历史消息。从工程角度看我一般分三个级别管理上下文短期记忆最近 N 轮对话直接放进 Prompt中期摘要超过轮数后由模型把早期消息生成一段摘要长期知识需要查外部知识库或向量库按需检索最简单的方式是只保留最近 10 到 20 轮消息。第一次实现时建议先用这种方案代码简单效果也还行。等跑通了再考虑摘要和向量检索。截断策略里最容易出的问题是token 统计和截断顺序不一致。有的模型按 token 计费有的按字符计费如果统计方式不统一可能出现本地看起来没超限调用模型时报超长错误。我一个比较稳妥的做法是在PromptBuilder里预留一个 Tokenizer 接口先按字符估算等接入具体模型后再换成对应模型的 Tokenizer。不要一开始就写死“2000 个字符”因为不同模型的分词差异很大。5.3 会话存储选型会话存储可以简单拆成两层热数据最近一小时活跃的会话用 Redis 缓存用 TTL 维护过期冷数据完整历史记录存入 MySQL 或 MongoDB查询时先用 Redis 拿最近消息如果没有再查数据库。这样既能保持响应的速度又不会让数据库承受所有读取压力。但要注意缓存和数据库的一致性。在保存消息时我先写数据库再更新 Redis 缓存。如果先更新缓存再写库写库失败就会导致缓存里有用户看不到的假消息。6. 模型接入层设计模型接入层是 agent-model-server 的核心职责。它的作用不是写死在某个模型的 SDK 调用里而是通过适配器模式让上层 Agent 编排服务不感知具体模型。6.1 统一模型接口建议定义这样一个接口public interface ChatModelAdapter { String modelType(); ChatResponse chat(ChatRequest request); }每一个模型对应一个实现类比如OpenAiAdapter、QwenAdapter、LocalModelAdapter。modelType返回模型标识比如openai、qwen、local。在 Agent 编排服务里通过一个工厂类按配置选择具体适配器Service public class ModelFactory { private final MapString, ChatModelAdapter adapterMap; public ModelFactory(ListChatModelAdapter adapters) { this.adapterMap adapters.stream() .collect(Collectors.toMap(ChatModelAdapter::modelType, a - a)); } public ChatModelAdapter getAdapter(String modelType) { return adapterMap.get(modelType); } }这样如果以后要接入新模型只需要新增一个适配器实现类不用改编排服务的主逻辑。6.2 超时、重试和限流模型接口是最不稳定的外部依赖。必须设置超时时间我一般会这样设连接超时5 秒读取超时30 到 60 秒重试次数1 到 2 次重试间隔指数退避比如 1 秒、2 秒、4 秒为什么不能无限重试因为大模型接口并发压力大如果系统里面多个用户同时触发重试很容易把服务拖垮。重试只适合临时网络抖动对持续超时没有帮助。限流也是必须的。建议在网关层对每个用户 ID 做 QPS 限制在模型接入层对每个 API Key 做并发限制。双层限流可以防止单用户刷接口拖垮整个平台。6.3 结构化输出解析模型输出并不总是稳定格式。当你要求模型输出 JSON 时它有可能输出多余的 Markdown 标记、注释或者把 JSON 包在代码块里。所以在ModelService里必须有一段容错解析逻辑优先尝试直接解析 JSON如果失败去掉开头和结尾的代码块标记如果还失败用正则提取第一个{到最后一个}之间的内容仍然失败时返回给模型一段纠错 Prompt让它重新生成这一步非常重要。工具调用决策一旦依赖模型输出格式解析就必须足够健壮。我在实测时发现很多“Agent 不工作”的问题最终都出在格式解析上而不是模型本身能力不行。7. 批量任务与异步处理Agent 平台真正从 Demo 走向生产一定会遇到批量需求给一批用户发送定制报告、对一批历史工单做自动分类、定时拉取数据后生成摘要。这些任务不能在 HTTP 请求线程里同步处理必须交给消息队列和任务服务。7.1 为什么不建议直接用并发循环有些初学者看到批量任务第一反应是开一个 for 循环里面用线程池并发调用模型。这种方式在小数据量时能跑但面临几个问题进程重启后任务丢失中途失败不知道从哪个继续没有统一的进度记录多个实例部署时线程池各自为政可能重复消费所以只要任务量超过几十条就应该走队列和任务表。7.2 任务表的核心字段一个批量任务拆成两层任务批次和原子任务。任务批次记录这次批量任务的类型、总量、成功数、失败数、开始时间、结束时间原子任务记录每一条数据的输入、输出、状态、失败原因原子任务表至少要有这些字段字段作用task_id批次 IDitem_id原子任务 IDstatusWAITING / RUNNING / SUCCESS / FAILEDinput_data输入参数JSON 格式output_data输出结果JSON 格式error_msg失败原因retry_count重试次数next_retry_time下次重试时间为什么要把输入输出存成 JSON因为批量任务的输入类型可能很多用独立的业务字段不灵活。JSON 把所有可能情况都装下了查询具体字段时可以再用数据库函数解析或者结合实际需求单独提取列。7.3 批量任务消费流程任务服务的消费流程可以按这个步骤设计接收任务批次消息把每条数据拆成原子任务写入任务表状态 WAITING消费者按一定数量批量拉取 WAITING 任务调用模型服务或工具服务处理每条任务成功后更新原子任务状态为 SUCCESS失败时根据重试次数决定直接失败还是延迟重试每完成一个原子任务就在 Redis 里更新该批次的进度进度信息可以用INCR原子递增来实现。前端定时拉取批次进度就能展示“已完成 30/100”这类信息。批量任务不要追求“一次处理完”要能随时中断、恢复、重跑。把状态设计好了出现失败时直接查数据库就能知道卡在哪。8. 生产化落地与边界条件跑通核心链路之后还要处理一些容易影响稳定性和合规性的问题。下面这几点是很多人第一次做 Agent 平台时容易忽略的。8.1 内容安全与模型输出过滤Agent 虽然不像普通聊天产品那样面向海量 C 端用户但在业务系统里也存在内容安全要求。我的做法是在模型接入层和网关层各做一道过滤网关层对用户输入做长度限制、频率限制、敏感词预检模型接入层对模型返回内容做合规过滤确保不包含违规信息如果这是企业内部系统还需要考虑数据权限。用户可能有权查询订单但未必有权查询财务数据。Agent 在使用工具时必须把用户 ID 作为参数传给业务工具服务由工具服务判断权限不能让 Agent 通过拼接参数绕过权限。8.2 审计日志Agent 平台的审计日志比普通 CRUD 系统更重要。因为模型是不可完全预测的一旦发生误操作或错误回答必须能追溯当时的输入、输出、工具调用和决策依据。建议在两个节点记录日志请求开始用户 ID、会话 ID、输入内容、请求时间请求结束输出内容、工具调用列表、每个工具调用耗时、模型名称、token 消耗、错误信息日志不要只打到大盘建议通过消息队列异步写入专门的日志表或对象存储。如果直接在请求链路里写日志表高峰期会对数据库产生额外压力。8.3 部署与资源建议AgentMesh 类项目如果用 Docker Compose 部署可以从这样划分基础组件Nacos、MySQL、Redis、RabbitMQ应用服务agent-gateway、agent-server、agent-tool-server、agent-model-server、agent-session-server可选组件日志收集、监控告警、对象存储内存紧张时优先保障 Nacos、Redis 和 agent-server。agent-server 是 Agent 编排的核心承担的线程和对象最多。agent-model-server 如果并发不高可以先跟 agent-server 共用实例但生产环境不建议。模型服务如果使用本地模型需要单独部署在 GPU 机器上与应用服务分离。不要把推理服务和应用服务混布因为 CPU 和 GPU 的资源抢占会互相影响。8.4 现有微服务项目怎么接入如果你已经有一个普通微服务项目比如基于若依微服务版本改造过的系统不需要把整个项目推倒重来。常见做法是新增两个模块agent-tool把自己项目里的业务接口封装成 Agent 可调用的工具agent-server部署 Agent 编排服务主要负责判断何时调用工具这么做的好处是业务域代码不用大改Agent 服务只依赖工具接口不会侵入原有数据库和事务边界。但要注意工具接口的入参出参最好设计成通用 JSON而不是原有系统的内部 DTO。因为 Agent 调用工具时参数通常来自模型生成如果内部 DTO 太严格解析和兜底成本会很高。9. 常见报错与排查链路最后整理一份排错顺序。无论你遇到什么报错先别急着改代码按这个链路走大概率能定位问题。9.1 请求发出去没有响应先看 agent-gateway 日志确认请求是否到达网关再看 Nacos确认服务是否注册成功查看 agent-server 日志确认请求是否进入编排服务查看模型接入日志确认 LLM 调用是否超时最后看 Redis确认缓存和会话状态是否正常这一类问题最常见的根因是服务之间通过 Feign 调用时有超时或序列化错误但上层日志没有打印完整堆栈。先把日志级别调成 DEBUG再复现一次。9.2 模型返回格式解析失败查看模型原始输出确认输出是否包含无关内容确认 Prompt 中是否有明确的输出格式要求确认解析逻辑是否覆盖了代码块标记情况尝试用更严格的输出约束方式比如要求模型只输出 JSON不要附加解释很多模型在温度较高时会输出多余解释把 temperature 调低到 0.1 到 0.3格式稳定性会明显提升。9.3 工具调用提示 not found确认工具实现的getName()返回值是否唯一确认工具类是否被 Spring 扫描并在启动时完成注册确认模型决策时传入的工具描述列表是否来自同一个注册表建议启动时打印工具注册列表和模型能看到的列表做比对这种问题大多数不是模型能力问题而是注册和描述两边信息不一致。9.4 批量任务积压并且速度越来越慢先看任务表确认失败任务是不是一直在重试再看模型接口的限流情况确认是否触发了 API 限流看消费者线程池配置确认并发线程数和任务量是否匹配看数据库连接池确认是否连接耗尽批量任务的并发并不是越大越好。模型接口有 QPS 限制数据库有连接上限线程池有 CPU 瓶颈。建议从 5 个并发开始逐步增加观察拐点在哪里。做 AgentMesh 这类项目时最容易被干扰的是“大模型很厉害”这个预期。实际落地时模型能力当然重要但决定平台稳不稳的往往是会话状态是否完整、工具调用是否可靠、任务失败能否恢复这些工程细节。先把单条链路跑通再慢慢把批量和并发做起来这个项目的学习价值才能真正体现出来。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门