1. 为什么 Java 后端一碰 Agent 就容易“翻车”1.1 从一次线上事故说起幻觉是怎么把接口打挂的去年年底我接手了一个客服工单自动分类的项目技术栈是典型的 Spring Boot 加 MyBatis业务侧想加一个“智能摘要”功能让大模型读完用户对话后输出结构化的分类标签和摘要。第一版做得很糙直接在 Service 里调模型接口把用户原文拼进 Prompt然后JSON.parse返回值。上线第三天就出事了模型那天心情不好返回了一段带解释性文字的结果前面多了一句“好的以下是分类结果”JSON.parse直接抛异常整个工单队列堵了两千多条。这件事让我彻底想明白一个道理大模型是一个概率系统而 Java 后端是一个确定性系统把概率系统直接嵌进确定性链路等于在承重墙上开盲盒。所谓“幻觉”在业务视角下不是模型胡说八道那么简单而是它输出的格式、字段、枚举值、数量都可能偏离你的契约。Java 的强类型、编译期校验、事务边界在模型面前全部失效因为模型返回的永远是一个字符串。后来我把这套链路重构了一遍核心思路是让模型只负责它擅长的“语义理解”把所有确定性的事情交给工作流引擎和 Java 代码。具体落地时我用了 n8n 做编排层Java 只做业务网关和最终校验。重构之后不仅稳定性上来了Token 消耗还降了大约八成。这篇文章就把这套思路完整拆开讲包括为什么这么选、每一步怎么配、踩过哪些坑。1.2 谁适合看这篇三类读者的不同收获如果你是中高级 Java 后端正在被“要不要把大模型接进核心链路”这个问题困扰这篇能给你一套可落地的分层方案告诉你哪些活该给模型、哪些活必须自己扛。如果你是刚接触 Agent 开发的新手对 n8n、MCP 这些词还比较模糊文中会用生活化的类比把概念讲清楚你至少能看懂一条完整工作流是怎么跑起来的。如果你已经在用扣子、Dify、FastGPT 这类平台做原型想往企业级生产环境迁移那这篇里关于幂等、重试、Token 预算控制的部分应该能帮你少走不少弯路。需要提前说明的是本文不涉及任何具体模型的接入凭证配置重点在架构思路和编排方法你换成任何一家模型服务逻辑都是通的。2. 整体架构设计把“不确定”关进确定的笼子里2.1 核心思路模型做语义代码做契约我最终定下来的分层是这样的最外层是 Java 的业务网关负责鉴权、限流、参数校验、落库中间是 n8n 工作流负责编排模型调用、条件分支、重试、格式清洗最内层才是模型本身。三者之间用 HTTP 和结构化数据通信谁也不越界。这么设计的关键考量是职责隔离。模型最擅长的是“把一段自然语言变成另一段自然语言”它不擅长的是“保证输出一定是合法的 JSON 且字段齐全”。所以我在 n8n 里加了一个“结构校验节点”模型输出先过一遍 schema 校验不合法就触发重试或者走兜底分支绝不让脏数据流到 Java 侧。Java 侧则完全不信任上游收到数据后还要再做一次 Bean Validation双重保险。打个比方这就像餐厅后厨模型是那个手艺很好但偶尔会自由发挥的厨师n8n 是传菜员兼质检员Java 是前台收银系统。厨师可以创意发挥但传菜员必须确认每道菜都符合菜单前台只认标准订单。任何一环想“越权”整个系统就会乱。2.2 为什么是 n8n而不是纯 Java 编排很多人第一反应是我 Java 写得好好的为什么要引入 n8n直接用 Spring 的Async加线程池编排不行吗我一开始也是这么想的直到我发现纯 Java 编排有三个绕不过去的痛点。第一是可视化调试成本。模型调用链路经常要改 Prompt、改分支条件、改重试策略纯 Java 每次改动都要重新编译部署调一次 Prompt 等五分钟。n8n 的工作流是配置化的改完即时生效调试时能直接看到每个节点的输入输出这个效率差距在迭代期是数量级的。第二是重试和降级的表达力。模型调用失败的原因五花八门超时、限流、返回格式错误、内容被拦截。纯 Java 里这些逻辑会散落在各种 try-catch 里越写越乱。n8n 原生支持节点级重试、错误分支、条件路由把这些横切逻辑收敛到一处。第三是和 MCP 生态的衔接。MCP 协议现在越来越多地被用来给 Agent 挂载工具能力n8n 对 MCP 的支持比较直接Java 侧要自己实现一套 MCP 客户端不是不行但没必要重复造轮子。让 n8n 去对接工具生态Java 专注业务分工更清晰。当然n8n 不是银弹。它不适合做高并发的主链路也不适合承载复杂事务。我的定位很明确n8n 是编排层不是业务层。它处理的是“调用顺序和容错”不处理“业务规则和持久化”。2.3 Token 直降 80% 的账是怎么算出来的标题里说 Token 降 80%这不是拍脑袋的数字我拿实际数据算过。重构前的做法是每次请求把完整对话历史平均 3000 字加上一大段 Prompt 模板约 800 字一起发给模型单次输入约 3800 字按中文大约 1.5 字一个 Token 估算输入约 2500 Token输出约 300 Token单次合计约 2800 Token。重构后做了三件事一是历史压缩n8n 里加了一个预处理节点把超过 5 轮的对话先用规则截断加摘要压到 800 字以内二是Prompt 瘦身把原来塞在 Prompt 里的枚举定义、示例全部移到 n8n 的 schema 校验节点模型只收到必要的指令模板降到 200 字三是缓存命中相同意图的请求走缓存分支直接返回历史结果不调模型。算下来单次输入降到约 700 字输出还是 300 Token 左右合计约 750 Token。750 除以 2800大约是 27%也就是降了 73%。再加上缓存分支的命中率我们场景下约 15% 的请求能命中综合下来 Token 消耗降幅稳定在 80% 上下。这个账的关键不在模型本身而在你喂给模型的东西有多少是冗余的。3. n8n 工作流的核心节点拆解与配置要点3.1 入口节点Webhook 接收与参数预校验工作流的入口我用的是 Webhook 节点Java 侧通过 HTTP POST 把请求打过来。这里有个细节值得说Webhook 节点一定要开启“Raw Body”模式否则 n8n 会尝试自动解析 JSON遇到格式稍微不规范的请求体就会报错。开启 Raw Body 后原始字符串原封不动传进来后续节点自己解析可控性更强。参数预校验我放在 Webhook 之后的第一个 Function 节点里做主要检查三件事必填字段是否存在、字段类型是否正确、请求 ID 是否重复幂等检查。幂等这块我是用 n8n 的静态数据存储做的简易去重生产环境更稳妥的做法是把请求 ID 落到 Redisn8n 里通过 HTTP 节点查一下。注意Webhook 节点默认的超时时间比较短模型调用链路长的时候容易触发超时。建议在节点设置里把超时调到 60 秒以上同时 Java 侧也要相应调整 HTTP 客户端的读超时两边要匹配否则会出现 n8n 还在跑、Java 已经断开的情况。3.2 历史压缩节点把长对话“挤干水分”历史压缩是整个 Token 优化的核心。我的做法是在 Function 节点里写一段 JavaScript逻辑分三步先按轮次切分对话保留最近 3 轮原文再对更早的轮次做规则化摘要比如只保留用户的核心诉求和已确认的关键信息最后把摘要和最近原文拼起来控制在 800 字以内。这里有个经验不要用模型去做摘要用规则。我一开始图省事让模型先摘要再分类结果 Token 没省下来反而翻倍因为摘要本身也要消耗 Token。规则化摘要虽然粗糙但对工单分类这种场景足够用而且零 Token 成本。规则怎么写我的做法是提取每轮对话里的关键词用简单的词典匹配拼成一句话比如“用户反馈登录失败已确认账号存在待解决验证码收不到”。3.3 模型调用节点Prompt 模板的极简主义模型调用节点我配置得很克制。Prompt 模板只保留三部分角色定义一句话、任务描述一句话、输出格式要求一句话。原来那些“你是一个专业的客服助手请仔细阅读以下对话……”的客套话全部删掉模型不需要这些也能干活。输出格式要求我写得很硬“只输出 JSON不要任何解释文字字段为 category 和 summary。”实测下来加了“不要任何解释文字”这句之后格式错误率从大约 8% 降到了 2% 以下。剩下的 2% 靠后面的校验节点兜底。模型参数方面温度我设的是 0.1尽量压低随机性。最大输出 Token 限制在 500防止模型话痨。这两个参数是配合使用的低温度保证稳定低上限防止跑偏。3.4 结构校验与重试节点幻觉的“最后一道闸”这是我认为整个工作流里最重要的节点。模型返回后先进一个 Function 节点做 JSON 解析和 schema 校验。校验逻辑包括能否解析成 JSON、是否包含必需字段、字段类型是否正确、枚举值是否在允许范围内。任何一项不通过就抛出一个带错误码的异常。n8n 的节点级重试配置在这里派上用场。我把模型调用节点和校验节点包在一个错误处理分支里校验失败时触发重试最多重试两次。重试时会在 Prompt 里追加一句“上次输出格式错误请严格只输出 JSON”实测这个提示能显著提高第二次的成功率。如果两次重试都失败就走兜底分支返回一个默认分类“待人工处理”同时把原始请求记到日志表后续人工介入。兜底分支的存在是让整个链路“永不失败”的关键。用户永远能拿到一个结果哪怕这个结果是“需要人工看”。4. Java 侧如何与 n8n 配合网关、校验与降级4.1 网关层设计把 n8n 当成一个“不稳定的下游”Java 侧我把它定位成业务网关核心原则是把 n8n 当成一个可能超时、可能返回脏数据的普通下游服务而不是什么特殊存在。基于这个原则网关层做了四件事。第一是超时隔离。给 n8n 的调用单独配一个线程池超时时间设 8 秒超过就快速失败绝不拖垮主线程。第二是熔断降级。用 Resilience4j 做熔断n8n 连续失败超过阈值就自动降级到本地规则引擎先保证业务能跑。第三是响应校验。收到 n8n 返回后用 Bean Validation 再校验一遍字段缺失或类型不对直接走降级。第四是异步落库。业务结果先返回给前端落库操作异步做避免数据库抖动影响响应时间。这套设计下来n8n 挂了业务也不会挂只是智能分类退化成规则分类用户体验有损但不中断。4.2 幂等与重试别让一次请求变成三次扣费模型调用是有成本的重试必须谨慎。我在 Java 侧给每个请求生成一个全局唯一的 requestIdn8n 侧用这个 ID 做幂等键。如果同一个 requestId 在短时间内重复到达n8n 直接返回缓存结果不重复调模型。重试策略上我只对可重试错误做重试网络超时、限流、格式错误可以重试内容被拦截、参数非法这类错误重试也没用直接失败。重试次数上限设为 2且每次重试间隔递增1 秒、3 秒避免雪崩。提示幂等键的存储建议用 Redis 并设置合理的过期时间比如 10 分钟。太短起不到去重作用太长会占用内存。n8n 侧可以通过 HTTP 节点访问 Redis也可以用它的静态数据做轻量级缓存看你的部署条件。4.3 降级方案规则引擎兜底怎么写降级方案我准备了两套。第一套是关键词规则引擎维护一个分类关键词表请求进来先做关键词匹配命中就直接返回不调模型。这套规则覆盖了我们场景下大约 40% 的高频请求等于给模型减负。第二套是默认兜底所有规则都没命中且模型不可用时统一返回“待人工处理”。关键词表怎么维护我的经验是从历史数据里挖。把过去一个月的工单拿出来统计每个分类下出现频率最高的词人工筛一遍形成初始词表。之后每周复盘一次误判案例持续补充。这套规则引擎虽然土但在模型不可用的关键时刻它就是业务的救命稻草。5. 常见问题与排查技巧实录5.1 模型返回格式错误的排查路径格式错误是最常见的问题排查我一般按这个顺序走先看 n8n 里模型节点的原始输出确认是模型本身跑偏还是后续节点处理错了再看 Prompt 里有没有容易被模型误解的表述比如“请尽量输出 JSON”这种模糊指令要改成“只输出 JSON”最后看温度参数温度高于 0.3 时格式错误率会明显上升。有个小技巧在 Prompt 末尾加一句“输出前请自检 JSON 是否合法”实测能再降一两个百分点的错误率。另外如果模型支持结构化输出比如 JSON Mode一定要开启这是最省心的方案。5.2 Token 消耗异常增长的定位方法Token 突然涨了先查三个地方。第一是历史压缩节点是否失效有时候对话轮次统计逻辑有 bug导致压缩没生效原文全量传给了模型。第二是缓存命中率是否下降缓存键设计不合理会导致大量请求穿透到模型。第三是Prompt 模板是否被误改团队协作时经常有人手滑加了一堆示例进去。我的做法是在 n8n 里加一个统计节点每次调用后记录输入输出 Token 数定期汇总。这样一旦有异常能快速定位到是哪天、哪个节点开始涨的。5.3 n8n 工作流超时与并发瓶颈n8n 默认是单进程执行的高并发下容易成为瓶颈。我的处理方式是把 n8n 部署成队列模式主进程只负责接收请求实际执行交给 worker 进程可以横向扩展多个 worker。同时给工作流设置合理的并发上限避免模型侧被限流。超时问题前面提过核心是两边超时时间要匹配。另外n8n 里耗时长的节点比如模型调用建议单独设置更长的超时不要用全局默认值。5.4 常见问题速查表问题现象可能原因排查方向解决建议模型返回非 JSONPrompt 指令模糊、温度过高检查 Prompt 和温度参数加“只输出 JSON”指令温度降到 0.1Token 消耗突增历史压缩失效、缓存穿透检查压缩节点和缓存命中率修复压缩逻辑优化缓存键n8n 响应超时超时配置不匹配、并发过高对比两边超时设置统一超时时间扩展 worker重复调用模型幂等键失效检查 requestId 生成和存储用 Redis 存幂等键设合理过期分类结果不稳定温度过高、Prompt 有歧义检查温度和 Prompt 表述降温度消除 Prompt 歧义6. 几个我踩过的坑和最后的经验第一个坑是过度依赖模型做格式转换。我一开始让模型直接把结果转成 Java 对象需要的格式结果模型经常自作主张加字段。后来改成模型只输出最简 JSON格式转换全部在 n8n 和 Java 里用代码做稳定多了。模型就该干语义的活格式的活交给代码。第二个坑是重试没有区分错误类型。早期所有错误都重试结果内容被拦截的请求重试了三次还是被拦截白白浪费 Token。后来加了错误分类只重试可恢复的错误Token 浪费少了一大截。第三个坑是忽略了 n8n 的版本兼容性。n8n 迭代很快某些节点在不同版本间行为有差异升级前一定要在测试环境跑一遍完整工作流。我有次升级后 Webhook 节点的 Raw Body 默认值变了导致线上解析全挂排查了半天。最后一个经验这套架构的价值不在于用了多新的技术而在于把不确定性收敛到了可控的范围内。模型可以幻觉但工作流不能Java 更不能。把每一层的职责划清楚让模型只做它擅长的事剩下的交给确定性的代码这才是 Java 后端驾驭 Agent 的正确姿势。至于 Token 降 80%那只是这个思路顺带带来的收益真正值钱的是系统的稳定性和可维护性。