在智能体平台建设中,真正难的地方不是写一个“能对话的 demo”,而是把多个 AI Agent、多种工具、多套模型接入和微服务治理体系放在同一个平台里,保证它们可以独立部署、按需扩展、清晰观测。AgentMesh 这个示例项目,目标就是解决这个工程问题:用微服务的思路搭建一套 AI Agent 运行与编排平台,让 Agent 不只是某个独立服务里的一个函数,而是可以配置、可编排、可观测、可灰度发布的一等公民。这篇文章会从架构设计出发,围绕如何拆分服务、如何定义 Agent、如何编排工具调用、如何异步执行长任务、如何排错和上线,带你把一个 AgentMesh 的最小可用版本完整落地。
如果团队目标是 2026 年上线一套可运行、可扩展的 AI Agent 平台,那么从一开始就不能把 Agent 当成单体应用来写。单体应用在只有两三个 Agent 时看不出问题,一旦 Agent 数量增长到几十个,工具接入、记忆管理、任务调度、权限控制和日志追踪都会纠缠在一起。AgentMesh 的核心理念是把“Agent 的定义”和“Agent 的运行”分开,把“模型调用”和“工具调用”分开,把“同步交互”和“异步任务”分开。沿着这条主线,你会看到微服务模式在 AI 场景下如何真正落地。
1. 先理解 AgentMesh 要解决的工程问题
1.1 AI Agent 从 Demo 到平台的差距
一个简单的 AI Agent demo 通常只有一条调用链路:接收用户输入,调用大模型,返回文本。如果 Agent 需要查询订单、调用审批接口、写入知识库,就需要在代码里硬编码工具调用逻辑。这种方式适合验证模型能力,但不适合平台化。平台化意味着 Agent 的创建者可以不改主程序代码,只修改配置就能新增一个 Agent;工具提供方可以按标准协议接入自己的服务;运维人员可以看到每次 Agent 执行的轨迹。
差距具体体现在四个层面:
- 复用层面:多个 Agent 会共享模型接入、工具网关、记忆服务,这些能力如果写在某个业务服务里,其他服务无法复用。
- 扩展层面:某类 Agent 并发量高,应该只扩容编排服务或对应的业务单元,而不是把整个应用一起扩容。
- 治理层面:每次 Agent 执行调用了哪些工具、消耗了多少 token、花了多长时间,需要一个独立可查询的追踪体系。
- 交付层面:Agent 的 prompt、参数、工具列表经常调整,不能每次调整都发版上线,配置需要外置和动态更新。
AgentMesh 正是围绕这些问题做服务拆分。它把“Agent 定义管理”“Agent 运行编排”“工具执行”“记忆存储”拆成独立服务,再通过消息队列和网关把整条链路串联起来。
1.2 AgentMesh 的核心术语
在进入实现之前,先统一几个关键术语,后面代码和配置都会用到。
- AgentDef:Agent 的静态定义,包括名称、模型、系统 Prompt、可用工具、最大迭代次数、温度等参数。
- AgentInstance:AgentDef 的一次实际运行实例,携带用户会话 ID、输入消息、上下文状态。
- ToolDef:工具注册表中的工具描述,包括名称、描述、输入参数 JSON Schema、调用地址。
- AgentRunner:编排服务中执行一次 Agent 循环的组件。
- ToolInvocation:一次具体的工具调用记录,包含调用请求、响应、耗时和状态。
- TaskJob:交给异步队列执行的长时间任务。
这几个概念对应到微服务里,基本都是独立的数据表和 API。Agent 平台能否扩展,很大程度上取决于这些概念是不是从一开始就被显式建模。
1.3 服务拆分边界:不是按“功能”拆,而是按“变化频率”拆
微服务拆分最常见的问题是按照业务功能拆成“订单服务”“用户服务”“商品服务”,但在 Agent 平台里,更需要关注哪些模块变化频繁、哪些模块稳定。
AgentDef 和 AgentInstance 变化非常频繁,需要独立的控制台服务和存储。编排引擎相对稳定,核心是“循环调用模型、判断是否需要调用工具、收集工具结果后继续推理”,这部分要沉淀为可复用组件。工具网关也独立,因为工具由不同团队提供,API 协议、鉴权方式、限流策略都会不同。记忆服务独立,是因为它依赖向量数据库和缓存策略,和业务逻辑耦合度低。
推荐的服务划分如下:
- agent-mesh-console:Agent 定义管理、工具注册管理、配置发布、运行查询。
- agent-mesh-orchestrator:核心运行引擎,处理同步 Agent 调用和异步任务调度。
- agent-mesh-toolgate:工具协议转换、鉴权、超时控制、限流。
- agent-mesh-memory:会话记忆、向量检索、知识库访问。
- agent-mesh-gateway:统一入口,负责路由、认证、限流。
中间件层面使用 Nacos 做注册中心和配置中心,Redis 做缓存和会话状态,RabbitMQ 做异步任务队列,PostgreSQL 加 pgvector 存储 Agent 定义和向量数据。
2. AgentMesh 的整体架构和数据流
2.1 技术选型与版本基线
AgentMesh 以 Java 17、Spring Boot 3.x、Spring Cloud Alibaba 为主。选择这套技术栈不是因为它最新,而是因为它在微服务治理领域沉淀最完整,团队招聘和运维经验也最容易对齐。
| 组件 | 推荐选型 | 版本建议 | 用途 |
|---|---|---|---|
| 开发语言 | Java | 17 或 21 | 主服务开发 |
| 微服务框架 | Spring Cloud Alibaba | 2023.x 配合 Nacos 2.3.x | 注册发现、配置管理 |
| 网关 | Spring Cloud Gateway | 跟随 Spring Cloud 版本 | 统一路由、鉴权 |
| 数据库 | PostgreSQL | 14+ 并部署 pgvector | Agent 配置、工具定义、向量检索 |
| 缓存 | Redis | 6.2+ | 会话状态、工具调用缓存 |
| 消息队列 | RabbitMQ | 3.12+ | 异步长任务 |
| 模型接入 | HTTP 客户端或 Spring AI | 以官方最新稳定版为准 | 统一调用各家大模型 |
| 链路追踪 | OpenTelemetry + Zipkin 或 SkyWalking | 按团队已有设施选型 | 观测 Agent 执行链路 |
这里的版本基线要结合实际环境确认。尤其是 Spring Cloud Alibaba 和 Spring Boot 的版本对应关系非常严格,如果版本不匹配,Nacos 服务注册很可能出现“服务注册成功但调用失败”的隐蔽问题。
2.2 一次 Agent 请求的完整数据流
用户在客户端发起对话,请求先到达 agent-mesh-gateway。网关解析 JWT,提取用户 ID 和租户 ID,将请求转发给 agent-mesh-orchestrator。
编排服务拿到用户输入后,从 agent-mesh-console 读取对应的 AgentDef。如果 AgentDef 配置允许使用记忆,则从 agent-mesh-memory 拉取历史消息。编排服务把这些内容组装成消息列表,调用大模型接口。
大模型返回的内容如果是普通文本,编排服务直接返回给网关。如果大模型返回 tool_calls,编排服务把每个工具调用请求转给 agent-mesh-toolgate。toolgate 校验参数、做鉴权,然后调用真实业务接口,把结构化结果返回到编排服务。编排服务把工具结果追加到消息列表中,再次调用大模型,继续判断。
这个过程会循环执行,直到大模型不再请求工具,或者达到最大迭代次数。执行完成后,编排服务把新的消息写入记忆服务,把执行轨迹写入日志,然后返回最终结果。
如果任务本身是长时间运行的,比如“分析过去一年的销售数据并生成报告”,编排服务的同步请求会使前端长时间等待。此时应该走异步模式:网关先返回任务 ID,编排服务把任务发送到 RabbitMQ,任务消费者执行完 Agent 后把结果写入 Redis 或数据库,前端轮询任务状态。
2.3 Agent 定义的数据结构
Agent 定义是平台的一等公民,建议使用 JSON 存储灵活配置,数据库表只保留基础字段。以下是一个典型的 AgentDef JSON 示例:
{ "agentId": "order_refund_agent", "name": "订单退款助手", "description": "处理用户订单退款申请", "model": "qwen-max", "temperature": 0.2, "maxIterations": 5, "systemPrompt": "你是电商平台客服助手,负责处理退款申请。查询订单后,根据退款规则给出处理建议。", "tools": ["query_order", "check_refund_rule", "apply_refund"], "memory": { "enabled": true, "windowSize": 10, "ttlSeconds": 86400 }, "timeoutMs": 30000 }这里有两个容易被忽视的字段。maxIterations 控制工具调用循环的最大轮数,防止模型陷入无限工具调用;timeoutMs 控制一次 Agent 运行的预算时间,超过就返回超时错误给用户。
数据库表中的关键字段可以设计为 agent_id、name、version、status、config_json、creator、create_time、publish_time。Agent 调整配置后不能直接生效,需要走“草稿、发布”的流程,发布时生成新版本号,运行中的实例继续使用旧版本,新请求使用新版本。这样可以实现 Agent 配置的灰度发布和快速回滚。
2.4 同步调用和异步任务的取舍
AgentMesh 同时支持两种调用模式。常规对话使用同步模式,响应时间控制在 10 秒以内。如果规划任务明显超过这个时间,必须使用异步模式。
异步任务的状态机可以设计成 PENDING、RUNNING、SUCCESS、FAILED、TIMEOUT。任务创建时写入 Redis 和数据库,消费者启动后标记为 RUNNING,执行结束时更新为最终状态。前端通过 GET /tasks/{taskId} 查询状态,拿到 SUCCESS 后再请求结果详情。
两种模式对应不同的用户体验。同步模式适合聊天机器人、客服助手;异步模式适合报告生成、数据分析、批量处理。实现平台时,不要把两种模式混合在一个接口里,否则超时控制和重试逻辑都会变得混乱。
3. 环境准备和项目骨架搭建
3.1 本地开发环境要求
学习阶段不需要完全复刻生产环境,但至少需要以下工具:
- JDK 17+
- Maven 3.8+
- Docker Desktop 或 Podman,用于启动中间件
- Nacos 2.3.0 镜像
- PostgreSQL 14 镜像并安装 pgvector 扩展
- RabbitMQ 3.12 镜像
- Redis 6.2 镜像
启动中间件时建议写一个 docker-compose 文件,避免手动分别启动。确定版本后,先确保 Nacos、PostgreSQL、Redis、RabbitMQ 都能通过本机端口访问,再开始写业务代码。中间件没有启动就调试服务间调用,会浪费大量时间。
3.2 Maven 多模块项目结构
项目采用 Maven 多模块结构,每个微服务独立一个模块,公共依赖抽成一个 common 包。推荐结构如下:
agent-mesh ├── pom.xml ├── agent-mesh-common ├── agent-mesh-gateway ├── agent-mesh-console ├── agent-mesh-orchestrator ├── agent-mesh-toolgate ├── agent-mesh-memory └── agent-mesh-api // Feign 接口定义根 pom 统一管理依赖版本。console、orchestrator、toolgate、memory 模块之间不直接依赖,它们通过 agent-mesh-api 模块中的 Feign 接口互相调用。这样做的好处是服务之间的契约独立成模块,任何一端修改接口都必须走 API 模块的版本更新,不会悄悄破坏调用关系。
3.3 Nacos 配置中心的关键配置
Nacos 中为每个服务创建独立配置。以 orchestrator 为例,它的 application.yml 中指定 Nacos 地址:
spring: application: name: agent-mesh-orchestrator cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yaml namespace: agent-mesh-dev业务配置放在 Nacos 的 Data ID 为 agent-mesh-orchestrator.yaml 中。这里要重点说明:Lambda 表达式或本地类中的动态配置,不能通过 @Value 直接注入然后缓存到静态变量,否则配置中心刷新不生效。推荐做法是使用 Nacos 的配置监听器,或者 Spring 的 @RefreshScope。对于 Agent 配置这种更频繁变化的业务数据,不应该放在 Nacos 配置中心,而应该放在 console 服务的数据库中,通过发布流程控制版本。
3.4 网关路由和 JWT 认证
网关是所有流量的入口。Spring Cloud Gateway 的配置需要把 /api/agent/** 路由到 orchestrator,把 /api/console/** 路由到 console,把 /api/tool/** 路由到 toolgate。
spring: cloud: gateway: routes: - id: agent-orchestrator uri: lb://agent-mesh-orchestrator predicates: - Path=/api/agent/** filters: - StripPrefix=1 - id: console uri: lb://agent-mesh-console predicates: - Path=/api/console/** filters: - StripPrefix=1网关内部完成 JWT 解析。解析出的 userId、tenantId 放入请求头 X-User-Id、X-Tenant-Id,后续服务从请求头里读取用户上下文,而不是自己再解析一次 Token。伪造问题由网关统一处理,业务服务信任网关透传的请求头。
需要注意,内部服务之间调用时需要携带同样的用户上下文。Feign 的 RequestInterceptor 可以从当前请求上下文中取出 X-User-Id 和 X-Tenant-Id,放入 Feign 请求头,保证一次 Agent 执行链路的所有下游服务都能感知调用者身份。
4. 核心代码实现:从 Agent 配置到工具调用闭环
4.1 控制台服务:Agent 定义发布与版本管理
控制台服务的基本 API 包括创建 AgentDef、更新 AgentDef、发布 AgentDef、查询 AgentDef 列表。发布是这里的核心操作。
发布操作不能只改数据库的 status 字段,还要做两件事:生成版本记录,清空使用方缓存。AgentMesh 的编排服务通常会缓存 AgentDef 以避免每次请求都查数据库。发布动作可以通过 Redis 删除对应 agentId 的缓存键,也可以使用 RabbitMQ 发送 AgentConfigChanged 事件,编排服务收到事件后主动刷新缓存。
一个简化版发布逻辑如下:
@Transactional public AgentVersion publish(String agentId) { AgentDef def = agentDefMapper.selectByAgentId(agentId); if (!"DRAFT".equals(def.getStatus())) { throw new BizException("只有草稿状态才能发布"); } int newVersion = def.getVersion() + 1; agentDefMapper.updateVersionAndStatus(agentId, newVersion, "PUBLISHED"); AgentVersion version = AgentVersion.builder() .agentId(agentId) .version(newVersion) .configJson(def.getConfigJson()) .publishTime(LocalDateTime.now()) .build(); agentVersionMapper.insert(version); redisTemplate.delete("agent:def:" + agentId); mqTemplate.convertAndSend("agent.config.change", agentId); return version; }这段代码体现了三个工程要点。第一,版本记录必须与状态更新在同一个事务里,否则会出现状态已经发布但版本表缺失的情况。第二,缓存删除放在事务提交之后更安全,事务内删除缓存可能因为事务回滚导致缓存和数据库不一致。第三,MQ 事件用于通知其他服务刷新本地缓存,它是最终一致性的兜底手段。
4.2 编排服务:Agent Runner 工具调用循环
编排服务是 AgentMesh 的核心,它控制大模型和工具调用之间的循环。下面是核心执行器的简化实现:
public AgentRunResult run(AgentContext context, AgentDef def) { List<ChatMessage> messages = buildBaseMessages(context, def); for (int i = 0; i < def.getMaxIterations(); i++) { ChatResponse response = llmClient.chat( ChatRequest.builder() .model(def.getModel()) .messages(messages) .temperature(def.getTemperature()) .tools(loadAgentTools(def.getTools())) .build()); // 记录 token 消耗和执行轨迹 traceCollector.recordLlmCall(context.getAgentId(), response.getUsage()); messages.add(response.getMessage()); if (CollectionUtils.isEmpty(response.getToolCalls())) { return AgentRunResult.finish(response.getContent(), new AgentRunStats(i + 1, traceCollector.getTotalTokens())); } List<ToolResult> toolResults = toolGateway.execute(context, response.getToolCalls()); for (ToolResult result : toolResults) { messages.add(result.toChatMessage()); } } throw new MaxIterationException(def.getAgentId(), def.getMaxIterations()); }这里有几个非常关键的细节。
工具调用结果必须转成与模型调用历史兼容的 ChatMessage 格式,不能只把结果拼成纯文本。不同模型 API 对 tool message 的格式要求不同,客户端封装层需要做兼容。
traceCollector 在每次 LLM 调用后记录 token 数。很多团队上线后才发现无法回答“这个 Agent 一天消耗多少钱”的问题,原因是运行引擎没有在最开始就埋好计量点。
maxIterations 触发时抛出异常,而不是静默返回部分结果。如果在达到上限时只返回最后一轮模型输出,用户会得到不完整的答案,而且排查不到原因。抛出异常后,监控系统可以发出告警,提示某个 Agent 的工具调用链路可能存在问题。
4.3 工具网关:标准协议接入和参数校验
工具是 AgentMesh 平台与业务系统之间的桥梁。toolgate 服务的核心能力可以概括为三部分:工具注册、工具发现、工具执行。
工具注册表需要保存工具名称、描述、接口地址、HTTP 方法、鉴权方式、参数 JSON Schema。以下是一个适用的工具注册请求示例:
{ "toolName": "query_order", "displayName": "查询订单", "description": "根据订单号查询订单基本信息,下单时间、金额、状态", "endpoint": "http://order-service/api/order/query", "method": "POST", "authType": "TOKEN", "parameterSchema": { "type": "object", "properties": { "orderId": { "type": "string", "description": "订单编号" } }, "required": ["orderId"] } }大模型在生成工具调用参数时,偶尔会生成多余字段或者缺字段。toolgate 在调用业务接口之前,必须用 JSON Schema 校验参数。推荐使用 networknt json-schema-validator,校验失败直接返回明确的错误信息,让大模型在下一轮自我修正。
工具执行还需要考虑超时。业务接口超时时间、toolgate 整体执行时间、编排服务等待工具结果的时间,这三层超时要分层设置。常见做法是业务接口超时 5 秒,toolgate 超时 8 秒,编排服务工具等待超时 10 秒。层级之间留出缓冲,避免内部超时导致外部无法判断失败原因。
4.4 异步任务:使用 RabbitMQ 解耦长耗时 Agent
同步接口超时时间通常设置为 30 秒。超过这个时间,网关可能已经断开连接,再执行任务已经没有意义。因此 AgentMesh 必须支持长任务异步化。
任务提交接口的逻辑如下:
@PostMapping("/async") public TaskSubmitResponse submit(@RequestBody AsyncTaskRequest request) { String taskId = UUID.randomUUID().toString(); AgentTask task = AgentTask.builder() .taskId(taskId) .agentId(request.getAgentId()) .input(request.getInput()) .status("PENDING") .createTime(LocalDateTime.now()) .build(); taskMapper.insert(task); mqTemplate.convertAndSend("agent.task.submit", task); return new TaskSubmitResponse(taskId); }消费者从队列中获取任务,执行 Agent 运行循环,把结果写回数据库或 Redis:
@RabbitListener(queues = "agent.task.submit") public void onTaskSubmit(AgentTask task) { taskMapper.updateStatus(task.getTaskId(), "RUNNING"); try { AgentRunResult result = agentRunner.runForTask(task); taskMapper.updateResult(task.getTaskId(), "SUCCESS", result.getContent()); } catch (Exception e) { log.error("task execute failed, taskId={}", task.getTaskId(), e); taskMapper.updateStatus(task.getTaskId(), "FAILED"); } }RabbitMQ 消费端默认开启手动 ack 时,必须自己捕获异常并决定是否重试。上述示例把任务状态写入数据库,即使消费失败,也可以从数据库恢复。生产环境更推荐的状态恢复机制是定时扫描超时未完成的任务,由调度器重新标记为 PENDING 并重新投递。
4.5 记忆服务:会话历史与向量检索
记忆服务负责保存两类数据:短期会话历史和长期知识片段。
短期会话历史使用 Redis 的 List 结构,键为 memory:{userId}:{agentId},最多保留 windowSize 条消息。每次 Agent 执行前,编排服务从记忆服务拉取历史消息;执行完成后追加新消息。Redis 的过期时间设为 ttlSeconds,可以实现“会话 24 小时后自动归零”的效果。
长期知识片段需要向量检索。PostgreSQL 开启 pgvector 扩展后,可以保存文本向量和原始文本内容。插入知识片段时调用 embedding 服务生成向量,查询时使用余弦相似度检索最相关的片段。
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE knowledge_chunk ( id BIGSERIAL PRIMARY KEY, agent_id VARCHAR(64) NOT NULL, chunk_text TEXT NOT NULL, embedding VECTOR(1024) NOT NULL, create_time TIMESTAMP DEFAULT now() );查询 SQL 可以写成:
SELECT chunk_text FROM knowledge_chunk WHERE agent_id = #{agentId} ORDER BY embedding <=> #{inputVector} LIMIT 5;算子<=>表示余弦距离,距离越小越相似。使用向量检索只是为了缩小候选片段范围,最终是否把片段拼入 prompt,仍然需要由编排服务根据 token 预算和相关性阈值决定。
5. 运行验证:从启动到业务链路打通
5.1 各服务启动顺序和检查方法
AgentMesh 各服务存在依赖关系,建议按中间件、基础服务、业务服务的顺序启动。
- 启动 Nacos、PostgreSQL、Redis、RabbitMQ。
- 启动 agent-mesh-console,确认 Nacos 中能看到服务注册。
- 启动 agent-mesh-memory,确认数据库连接和 pgvector 可用。
- 启动 agent-mesh-toolgate,注册至少一个示例工具。
- 启动 agent-mesh-orchestrator,确认可以拉取 AgentDef 配置。
- 启动 agent-mesh-gateway,测试路由和认证。
每启动一个服务,不要马上启动下一个,先看 Nacos 控制台是否出现服务实例。如果注册不成功,优先排查 nacos 配置中的 namespace、group,因为这是最常见的“代码没变但服务找不到”的原因。
5.2 注册一个 Agent 并调用
先通过 console API 创建一个 Agent 定义,然后发布。假设 Agent ID 是 order_refund_agent,通过网关调用同步接口:
curl -X POST 'http://localhost:8080/api/agent/order_refund_agent/run' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer <token>' \ -d '{"userId":"10001","input":"用户想退款,请帮我查询订单 A1001 的状态"}'正常结果会返回最终文本和运行统计:
{ "code": 0, "data": { "agentId": "order_refund_agent", "output": "订单 A1001 当前状态为已发货,用户在确认收货前可以在订单详情页发起退款申请……", "iterations": 3, "totalTokens": 1520 } }验证时不要只看 output 是否合理,还要检查 iterations、totalTokens 是否符合预期。如果迭代次数总是等于 maxIterations,说明工具调用一直没被模型终止,属于需要排查的异常情况。
5.3 验证工具调用记录和链路日志
AgentMesh 应该在每次工具调用后写入一条执行记录,结构如下:
| 字段 | 示例值 | 说明 |
|---|---|---|
| traceId | 1a3f... | 一次 Agent 执行的全局 ID |
| agentId | order_refund_agent | 当前 Agent |
| toolName | query_order | 调用的工具 |
| requestBody | {"orderId":"A1001"} | 模型生成的参数 |
| responseBody | {"status":"SHIPPED"} | 工具返回结果 |
| costMs | 234 | 工具耗时 |
| status | SUCCESS | 调用状态 |
这条记录是排错的核心依据。用户在客户端发现回答不对,第一步不是看模型 prompt,而是先查这轮执行的 tool record,确认工具是否被调用、参数是否正确、返回内容是否符合预期。很多“模型回答不准”的问题,最后都会定位到工具返回了错误数据。
5.4 自动化测试的切入点
针对 AgentMesh 的自动化测试,重点不是写大量单测覆盖 getter/setter,而是要覆盖三类链路:
- 编排引擎循环控制类:mock LLM 返回不同的 tool_calls 序列,验证最大迭代次数、工具结果回传、异常分支。
- 工具参数校验类:准备合法的参数和非法参数,验证 JSON Schema 校验结果。
- 任务状态流转类:模拟异步任务从 PENDING 到 SUCCESS/FAILED 的完整状态变化。
模型接口在测试中用 wiremock 或 mock server 模拟,不要调用真实大模型,否则测试稳定性会被网络和模型不可预期输出影响。
6. 常见问题排查:现象、原因和解决路径
6.1 服务间调用总是超时或连接拒绝
现象是编排服务调用 console 查询 AgentDef 时,偶尔抛出连接拒绝,有时又正常。首先检查目标服务是否真实注册到 Nacos,再检查服务间是否启用了负载均衡。实践中最常见的原因是版本不匹配导致 Spring Cloud LoadBalancer 没有正确参与 Feign 调用,服务名被当成域名直接解析。
检查方式和解决路径按这个顺序执行:
- 访问 Nacos 控制台,确认服务名和调用方配置一致。
- 检查 Feign 客户端注解的服务名是否存在拼写错误。
- 确认所有服务的 spring-cloud-starter-loadbalancer 依赖一致,同一版本不能混用 Ribbon 和 LoadBalancer。
- 查看启动日志中是否出现 UnknownHostException,定位是注册中心问题还是负载均衡问题。
- 确认服务提供方所在网络没有防火墙阻挡注册中心返回的实例 IP。
6.2 Agent 一直调用工具,最终触发 MaxIterationException
现象是大模型不断调用某个工具,即使工具返回“订单不存在”,模型仍然继续生成同参数的调用。常见原因有三个:
- 工具返回的错误信息太简单,模型无法据此判断下一步。
- 系统 Prompt 中没有说明“工具调用失败后的处理策略”。
- 工具结果 message 的格式不符合模型 API 要求,模型没有真正读到结果。
排查时先看 tool record 中的 responseBody,再观察 messages 中工具结果是否完整。如果是格式问题,可以通过日志打印实际发送给模型的 message 列表,确认 tool message 是否出现在正确位置。
推荐在系统 Prompt 中加入降级策略,例如“如果查询不到订单,请直接告诉用户未找到订单,不要重复查询”。
6.3 修改 Agent 配置后,运行结果仍是旧配置
这是配置发布使用中最高频的问题。原因几乎都是编排服务缓存了 AgentDef,而发布侧没有触发缓存清理,或者缓存清理发生在事务提交前。
检查方式:
- 在编排服务日志中确认是否收到 AgentConfigChanged 事件。
- 在 Redis 中执行 KEYS agent:def:*,确认缓存键是否存在。
- 手动删除缓存键后再次调用,如果结果变成新配置,说明缓存失效链路有问题。
解决方式是把缓存删除放到事务成功提交后的回调中,并确保 MQ 消费者真正处理了事件。不要依赖推送事件“发了就等于被处理了”。
6.4 大模型返回的 JSON 参数解析失败
现象是大模型在 tool call 中生成类似{"orderId": "A1001", "extra": null}的参数,工具端校验失败,模型反复修正仍然失败。
原因一方面是大模型参数生成天然存在随机性,另一方面是工具参数 Schema 设计得不够严谨。不要在 JSON Schema 中使用模糊的 description,应该说明字段格式和取值范围。例如 orderId 的 description 应写明“订单编号,格式为字母 A 开头加数字”。
解析层不要直接使用 Map 接收后强转,推荐用 JsonNode 校验后再绑定到 POJO。如果工具有多个必填字段,要在校验失败时返回缺失字段列表,而不是只返回“参数错误”四个字。
7. 生产环境落地建议和可复用清单
7.1 学习环境与生产环境的差异
本地学习环境的目标是快速跑通,生产环境的目标是稳定、可观测、可回滚。两者的差异要提前规划清楚。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 配置方式 | 本地 yml 硬编码 | Nacos 配置中心 + 敏感信息加密 |
| 日志 | 控制台输出 | JSON 结构化日志 + 集中采集 |
| 链路追踪 | 可有可无 | OpenTelemetry 全链路追踪 |
| AgentDef 发布 | 直接改库 | 版本化、灰度、回滚 |
| 工具调用 | 不受限 | 严格鉴权、限流、审计 |
| 任务队列 | 默认队列 | 多队列隔离,不同优先级分开 |
| 模型密钥 | 配置明文 | 密钥管理服务或环境变量注入 |
生产环境有一个容易被忽视的细节:LLM 调用是外部依赖,它可能慢、失败、返回非法格式,不能和内部服务调用用同样的重试策略。对外部模型调用建议使用快速失败加有限重试,不要无限重试,否则会造成大量线程等待。
7.2 AgentMesh 上线前检查清单
发布 AgentMesh 平台前,建议逐项检查以下内容:
- AgentDef 是否有版本记录,是否可以一键回滚到上一个可用版本。
- 每个工具是否配置了超时、限流、熔断,是否记录 requestBody 和 responseBody。
- 编排引擎的 maxIterations 是否收敛,是否有超过迭代上限的告警。
- 模型调用 token 消耗是否有计量,是否与计费系统打通。
- 异步任务队列是否有死信队列,消息重复消费是否做了幂等。
- 关键链路是否有 traceId 贯穿网关、编排、工具、记忆服务。
- 环境隔离是否完成,测试环境是否使用独立 Nacos namespace。
- 网关层是否有限流策略,令牌桶容量是否根据集群规模评估过。
- 敏感配置是否加密存储,模型 API Key 是否进入日志。
每一项都直接影响线上稳定性。尤其是“消息重复消费是否幂等”,这个问题在 RabbitMQ 消费者重启、网络闪断时几乎必然出现,不提前做幂等,就会遇到任务被重复执行、退款接口被调用两次。
7.3 下一步扩展方向
AgentMesh 的最小闭环落地后,可以按以下方向逐步演进:
第一,模型路由。不同 Agent 可以使用不同模型,甚至同一个 Agent 可以根据输入难度选择不同模型。这会涉及路由策略和成本控制,可以在 orchestrator 中增加 ModelRouter 组件。
第二,流式输出。当前同步接口一次性返回最终结果,体验类似“等一会再看到全部文字”。生产级产品通常需要 SSE 或 WebSocket 推送流式输出,这要求编排引擎从阻塞式循环改为响应式或者事件驱动模型。
第三,多租户隔离。AgentMesh 如果作为团队内部平台,单租户模型尚可。一旦多个业务部门共用,AgentDef、工具注册表、会话记忆都要按租户隔离,缓存键和数据库表中都需要增加 tenant_id 维度。
第四,Agent 集市。平台运行一段时间后,多个团队会沉淀大量可复用的 AgentDef、工具接入包、Prompt 模板。设计一套包管理机制,允许 Agent 模板被复制、组合、导出,可以显著降低新业务接入成本。
AgentMesh 这类平台的建设价值不在于某个算法多厉害,而在于它让 Agent 的创建、发布、运行、观测和治理变成了标准化的工程流程。真正上线之后,团队会发现绝大部分时间不是花在模型选择上,而是花在工具协议校准、任务队列稳定性、链路追踪完整性和配置发布一致性上。先把这个工程底座搭稳,再谈模型和算法扩展,路线会顺畅很多。