
1. 幻觉与 Token 失控Java 后端接 Agent 的两座大山先说个我踩过的真实场景。项目里要给一个 Java 8 Spring Boot 3 的旧系统加 AI 问答能力最初方案很直接后端直接调大模型 API把用户问题拼进 prompt 发出去拿返回值塞进页面。上线第一周就出了两件让人头疼的事一是模型在回答里编造了一个根本不存在的订单编号客服拿着去库里面查自然是什么都没查到用户当场质疑系统的可靠性二是月底一拉账单光 Token 费用就烧掉了预算的三倍而“罪魁祸首”就是每次请求都把整段历史记录全量发给模型再加上模型回答那些冗长的开场白和客套话十几轮对话下来成本直接失控。这两件事本质上指向同一个根因我们让大模型干了它不该干的活也让它以最贵的方式干了活。大模型是概率模型它天生会“幻觉”——这不是 bug而是它的运作方式决定的。它不知道“知道”和“不知道”的边界只会根据概率补全最像样的回答所以当知识库里没有某个订单号时它不会说“我不知道”而是会“编一个最合理的出来”。同时大模型本身没有记忆每次调用都要把整个上下文重新读一遍Token 费用自然水涨船高。那怎么办我的思路是不要让大模型自由发挥而是让它在一条既定的轨道上做决策。这条轨道就是本文的主角——n8n。我把它作为 Java 后端和大模型之间的一层“确定性编排层”把整个问答流程拆成“固定逻辑 小范围智能”的混合架构。改造完成后我实测同一批业务请求Token 消耗直接降了约 80%模型编造性回答的发生率也从每周好几起降到了两周零起。这篇文章就把完整做法、参数计算和排坑过程分享出来适合那些正在把 Agent 能力接进 Java 后端、但被幻觉和成本搞得焦头烂额的团队。1.1 幻觉从哪来不是模型不行是约束不够要驯服幻觉先得理解幻觉的成因。大模型的训练目标是在给定上文的情况下预测下一个最可能的 token它没有“数据库查询”的概念也没有“事实核查”的机制。当你让它回答“帮我查一下单号 A123 的物流状态”时它看到的只是一段文本它的最优策略是生成一段看起来像物流状态的文字而不是真的去调用物流接口。这个机制决定了只要约束不够幻觉就一定存在跟模型大小关系不大哪怕用 GPT-4 级别的大模型在缺乏工具和约束的情况下照样会编。我在实战中总结出三类最常见的幻觉触发源上下文信息不足。用户问的是库里某个具体记录但你只把问题文本丢给模型没有任何结构化的数据兜底模型就只能靠“想象”补全。指令边界模糊。Prompt 里只说“请回答用户问题”没说“如果无法确认请直接说不知道”模型默认就会尽力编造。输出格式不固定。模型返回的 JSON 结构每次可能都不一样字段名漂移、类型变化导致后端解析失败后反复重试既烧 Token 又降低体验。这三点对应的解法其实很清晰用确定性系统喂数据、用强约束指令立边界、用 schema 校验输出。但它们不该散落在 Java 代码里而应该被集中在一个可编排的流程中这正是 n8n 的用武之地。1.2 Token 为什么烧得快每次调用都在全量重读Token 成本的失控往往不是因为模型本身贵而是因为调用姿势太奢侈。以常见的 OpenAI 接口计费为例输入和输出都是按 token 算钱的输入侧如果每次把 20 轮历史对话、总计 6000 个 token 全部发过去光输入费用就是输出的好几倍。再加上模型回答喜欢“礼貌性复述”——用户问“订单在哪里”模型先来一段“您好很高兴为您服务关于您查询的订单信息以下是为您找到的结果……”这几十个 token 全是成本却没有带来任何信息增量。更隐蔽的是重试带来的放大效应。一旦模型返回的 JSON 解析失败Java 后端如果直接触发重试等于把上一轮的全部上下文再发一遍一次解析失败可能白白消耗几千个 token。而这类解析失败恰恰很常见因为模型对格式的遵守是靠“概率”而不是“保证”换一种措辞就可能改变字段名。所以Token 优化的核心不是换更便宜的模型那是最后的选项而是做到三件事能不发历史就不发历史、能让模型少说废话就少说废话、能不重试就不重试。这三件事恰好都能在 n8n 的工作流里通过节点编排实现这就是后面要讲的重点。2. 为什么用 n8n 做编排层把“自由发挥”改成“确定流水线”先回答一个很多 Java 工程师会问的问题“这东西我用 Java 自己写不行吗为什么非要引入 n8n”答案是你当然可以自己写我最初也试图在一个 Service 类里用状态机硬扛但随着流程分支越来越多——要接多个模型供应商、要支持 webhook 回调、要处理鉴权重试、要给运营配置 prompt 模板——Java 代码的维护成本开始指数级上升。n8n 的价值在于它把“流程的骨架”和“业务逻辑”分开了流程怎么走、出错怎么降级、调用哪个模型这些用可视化节点拖拽就能调整而 Java 后端只需要做好自己最擅长的事——业务数据的提供和结果的消费。当然前提是你愿意接受多一层基础设施。n8n 可以自托管社区版免费数据都在自己手里这一点对于 Java 后端团队来说通常不是问题毕竟大家早就习惯了自建中间件。2.1 n8n 到底是什么给 Java 后端当“流程管家”n8n 是一个开源的工作流编排工具核心价值在于“节点化集成”。它内置了几百个节点HTTP Request、Webhook、Code、IF、Switch、Merge 这些基础节点可以自由组合还有针对各家大模型 API 的现成节点。但在我们这个场景里我其实用得最多的是四个基础能力Webhook 接收外部请求、Code 节点写自定义 JS 逻辑、IF/Switch 做分支路由、HTTP Request 调大模型或 Java 内部接口。你可以把 n8n 理解成一个装配车间Java 后端把“零件”用户问题、上下文数据、业务参数通过 Webhook 送进车间车间里的传送带流程节点按照预设顺序加工先做格式校验再决定走哪条产线路由然后调用大模型加工中心最后质检schema 校验合格后出库不合格则回炉或报废。整个流程是可视的、可调试的、可热更新的改一条分支不用重新发布 Java 服务。2.2 混合架构的设计思路大模型只管创造其余交给确定性逻辑最核心的设计决策是划定“大模型负责的边界”。我的原则是凡是可以用规则、判断、模板解决的问题绝不让大模型出场。大模型只负责两件事一是理解用户意图并转成结构化参数二是在给定知识片段内生成自然语言回答。至于“参数缺没缺、历史记录要不要裁剪、命中缓存就直接返回”这些全部由 n8n 的确定性节点处理。举个例子。用户问“我的订单多少钱”旧方案是把这句话发给大模型让它“理解”并“回忆”订单价格结果它可能在两个订单之间搞混。改造后的流程是n8n 先调用 Java 后端的订单查询接口拿到该用户的最新订单数据把这些数据作为上下文片段塞进 prompt再让模型“基于以下数据回答如果数据为空请说不知道”。这样模型面对的不再是开放式问题而是一个“给定材料做摘要”的任务幻觉空间被压缩到极小。这套架构还有一个额外好处Java 后端与具体模型供应商解耦了。今天是 OpenAI明天换成国产模型后天要支持多模型路由Java 侧完全不用动只需要在 n8n 里改一个节点配置。对于需要快速迭代的团队来说这个灵活性非常值钱。3. 落地实操Java 后端 n8n 打造确定性工作流下面进入正题讲一版我在生产环境跑通的完整方案。整个流程可以概括成一张时序图Java 后端收到客户端请求 → 补齐业务上下文 → 调用 n8n Webhook → n8n 工作流负责校验、路由、调模型、格式校验 → 返回结构化 JSON 给 Java 后端 → Java 后端做最后的兜底处理并响应客户端。3.1 Java 侧改造把 Agent 调用收敛到一个网关接口我做的第一件事不是去碰 n8n而是先重构 Java 侧的调用方式。原来各 Service 直接调大模型 API 的散乱代码全部下掉统一收敛到一个AgentGateway类里对外只暴露一个方法public AgentResponse execute(AgentRequest request) { // 1. 校验入参防止脏数据流入工作流 // 2. 补充业务上下文比如用户ID、订单快照、知识库检索结果 // 3. 调用 n8n webhook // 4. 解析结构化响应做超时与异常兜底 }AgentRequest里最少包含三个字段scene场景编码比如 ORDER_QUERY、AFTER_SALE、userMessage用户原文、bizPayload业务参数的 Map。AgentResponse里则统一包含code、message、data和rawUsage。这里的rawUsage是我特意加的用来回传 n8n 工作流里统计的 token 消耗方便做成本监控。Java 侧调用 n8n Webhook 的代码很简单用 Spring 的RestTemplate或WebClient都行。我比较建议用带超时控制的RestTemplate因为 Agent 场景最怕的就是模型接口迟迟不返回、线程池被占满。下面是一个可参考的配置Bean public RestTemplate agentRestTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(20000); // 留足模型响应时间但不要无限等 return new RestTemplate(factory); }调用 n8n 时要把 n8n Webhook 的鉴权凭证带上n8n 支持在 Webhook 节点配置 Header 认证Java 侧通过HttpHeaders添加即可。这一步千万别漏否则你的工作流等于裸奔在公网上别人知道 Webhook 地址就能白嫖你的模型额度。我在生产环境就见过因为 Webhook 没加鉴权导致被刷了几百美元账单的事故。3.2 n8n 工作流的节点编排与配置n8n 侧的工作流我按照“入口 → 校验 → 路由 → 智能 → 质检 → 出口”六个环节来编排。下面逐个说明各节点的作用和关键配置。第 1 步Webhook 节点入口。配置成 POST 方式路径设成/agent/gateway认证方式选择 Header Auth预共享密钥填一个随机生成的强密码。这个节点收到 Java 后端的请求后会把整个 JSON body 作为工作流的起始数据。第 2 步Code 节点参数校验。用一段简短的 JavaScript 做入参检查。我的实际代码大致如下const { scene, userMessage, bizPayload } $input.first().json; if (!scene || !userMessage) { throw new Error(MISSING_REQUIRED_PARAMS); } // 可再补充长度校验防止恶意超长文本 if (userMessage.length 5000) { throw new Error(MESSAGE_TOO_LONG); } return { scene, userMessage, bizPayload: bizPayload || {} };这里有一个 n8n 的新手坑Code 节点的输入输出格式是数组包对象很多人第一次写会以为$input.json就是对象其实它是数组。用$input.first().json是最稳妥的写法兼容 n8n 多版本。第 3 步Switch 节点路由分流。根据scene字段把请求分发到不同的处理分支。比如ORDER_QUERY走“查订单 → 组装上下文 → 调模型”GREETING走“固定话术直接返回”而根本不调模型。这一步是省 Token 的大头因为闲聊类请求大约占总量的 30%完全不需要模型出场。第 4 步HTTP Request 节点获取业务上下文。在需要真实业务数据的分支里用 HTTP Request 节点回调 Java 后端的内部接口比如/internal/order/detail。这里要注意n8n 侧发出的是服务间调用Java 侧需要对这个内部接口做 IP 白名单或内网访问限制防止被外部直接调用。第 5 步Code 节点Prompt 模板组装。这一步非常关键是降幻觉的核心防线。我把 prompt 从“自由发挥题”改成“材料分析题”。模板大致长这样你是一个客户助手。请严格依据以下【订单数据】回答用户问题。 订单数据 ${JSON.stringify(orderData)} 用户问题${userMessage} 规则 1. 如果订单数据无法回答用户问题直接回复“我没有查到相关信息”禁止编造。 2. 回答控制在 50 字以内不要寒暄不要解释直接给结论。 3. 只输出 JSON{answer: 你的回答}加粗强调规则是因为模型对越清晰的指令越容易遵守。“只输出 JSON”能直接约束输出格式减少后续解析重试。这里的${orderData}是上面一步拿到的最新订单数据必须序列化成紧凑 JSON 塞进 prompt。第 6 步HTTP Request 节点调用大模型 API。在这里选择你要用的模型供应商。我的建议是优先用gpt-4o-mini这一类性价比模型因为我们的工作流已经大量压缩了上下文模型承担的任务很简单完全用不着满血版大模型。参数配置上temperature我直接设成 0.1越低越稳定对于这种需要“按材料回答”的场景温度越低越好。第 7 步Code 节点输出校验与兜底。拿到模型返回后不要直接透传给 Java 后端先做一次 JSON 解析和字段校验。如果解析失败或者缺少answer字段直接返回一个预设的兜底话术“我暂时无法回答这个问题请稍后再试”不再重试。这一步在代码里实现const raw $input.first().json.choices?.[0]?.message?.content; if (!raw) throw new Error(EMPTY_MODEL_RESPONSE); try { const parsed JSON.parse(raw); if (typeof parsed.answer ! string || !parsed.answer.trim()) { throw new Error(INVALID_ANSWER_FIELD); } return { code: 0, data: { answer: parsed.answer } }; } catch (e) { return { code: 1, data: { answer: 我暂时无法回答这个问题请稍后再试。 } }; }这里有个经验不要迷信模型的 JSON 输出能力。即使你在 prompt 里强调了“只输出 JSON”模型仍偶发地会在前面加“好的这是你要的答案”之类的前缀或者用单引号代替双引号。所以“先解析、失败就兜底、不重试”这个策略远比“重试两次赌它这次能对”省 Token。3.3 Prompt 模板与 JSON Schema 校验把模型输出关进笼子很多 Java 后端同学对“JSON Schema 校验”比较陌生其实它就是一份描述“字段名、类型、是否必填”的规则清单。在 n8n 的 Code 节点里我们不一定需要引入完整的 JSON Schema 库手写字段级校验就够了上面那段代码就是最简版本。但如果你希望做更复杂的嵌套结构校验其实可以把校验逻辑做成一个 Java 侧的工具类——我就是在 Java 后端写了一个ResponseValidator对 n8n 返回的最终结果再做一次校验。为什么 Java 侧还要校验一次因为 n8n 工作流理论上也会被改出问题比如运营调了 prompt 模板导致输出结构变化。我见过同事在调试 n8n 时改坏节点配置结果上游模型返回的结构全变了Java 侧因为兼容性不足直接抛 NullPointerException。所以我的原则是“两端都校验双保险”。Java 侧的校验逻辑不复杂核心代码如下public AgentResponse validateAndConvert(MapString, Object n8nResult) { if (n8nResult null || !Integer.valueOf(0).equals(n8nResult.get(code))) { return AgentResponse.fallback(服务暂时不可用请稍后重试); } MapString, Object data (MapString, Object) n8nResult.get(data); String answer (String) data.get(answer); if (answer null || answer.isBlank()) { return AgentResponse.fallback(我没有找到相关信息); } return AgentResponse.success(answer); }这段代码在真实项目中帮我挡掉了至少三次由工作流配置改动引发的线上故障。每次 n8n 那边改了 promptJava 侧不用动但校验逻辑会坚定地守住“不合格就不给用户看”的底线。4. Token 直降 80% 是怎么算出来的三个降本措施实测先亮一组我压测得到的数据。改造前一次典型的订单咨询请求大约消耗输入历史对话 15 轮约 4500 token输出模型回答含寒暄和复述约 800 token单次合计约 5300 token改造后同一个场景的消耗构成变成输入压缩后的上下文摘要 300 token 订单数据 200 token prompt 模板 150 token共计约 650 token输出受“50 字以内 只输出 JSON”约束实测约 120 token单次合计约 770 token两值对比降幅正好在 85% 左右取整可以说成“直降 80%”。这 85% 不是单一手段的结果而是三个措施叠加的效果。下面拆开讲。4.1 上下文裁剪滑动窗口 摘要缓存第一个大头是历史对话。原来每次请求都把全部历史发给模型是因为需要模型“记住”之前的对话。但事实上对于大多数业务问答真正有用的只是最近两三轮的关键信息。我的做法是在 n8n 里加一个“历史摘要节点”逻辑如下维护一份 Redis 缓存key 是会话 IDvalue 是“历史摘要”而不是完整历史。每次新请求进来n8n 先读取旧摘要再结合当前用户问题生成新摘要然后只把摘要发给模型。摘要由模型压缩生成比如把“用户说他上周买了一个蓝牙耳机想查物流”压缩成一句话。这样做的好处非常明显。15 轮对话的精髓可能只需要 300 token 就能概括而完整内容却要 4500 token。代价是模型会丢失一些细节但对于大多数业务场景这些细节本来就不需要。如果遇到需要精确细节的场景我的建议是与其靠对话历史不如直接查业务库拿结构化数据塞进 prompt那比任何历史都可靠。4.2 路由分流小问题不让大模型出场第二个措施是给工作流加“路由分流”。我在 Switch 节点里把请求按场景分成三类固定话术类如“你是谁”“你能干什么”不调模型直接返回预设文本。业务查询类如“查订单”“查余额”调模型但严格按材料回答。闲聊开放类如“给我讲个笑话”走轻量模型或直接接入一个更便宜的供应商。我在生产中做了为期一周的统计大约 35% 的请求属于固定话术类30% 属于业务查询类35% 属于闲聊开放类。固定话术类直接零 Token 成本闲聊开放类用一个便宜的小模型打发了真正花大钱只有业务查询类。综合下来路由分流这一个措施就帮我们省掉了原本约 40% 的 Token 支出。这一套逻辑如果放在 Java 里写也不是不行但每次加新场景都要改代码、发版、等审批而 n8n 里只需要拖一个新分支。对于产品经理三天两头提新场景的团队这个灵活性确实难以替代。4.3 失败降级不让模型在错误答案上反复横跳第三个措施是“失败不重试直接降级”。这里需要调整一下心态模型返回解析失败说明它这次大概率不在状态重试不仅浪费 Token而且往往第二次、第三次还是会犯同样的格式错误。我在 n8n 里设置的是解析失败就返回兜底话术同时记录一条日志让后续人工介入或调整 prompt。这个策略省下的 Token 在重试逻辑明显的情况下非常可观。之前 Java 代码里一个解析失败会触发最多 3 次重试也就是一次失败可能烧掉 4 倍的单次成本。改造后失败只烧一次外加一个小得多的兜底返回。再加上上面说的 prompt 约束让模型格式错误率从一开始就变得很低这段省下的量虽然不如前两个措施大但属于“零成本收益”白拿的好处。5. 常见问题与排障实录这一节把我的实战排坑过程整理成表格都是我在生产环境真实踩过的希望能帮你少走弯路。问题现象根因解法n8n Webhook 收到请求但 Code 节点报错Code 节点的输入是数组而非对象统一用$input.first().json获取第一项数据模型返回的 JSON 偶尔带前缀或注释模型对格式遵守是概率性的prompt 强调“只输出 JSON不要任何解释”Java 侧再做一次容错解析调用模型接口偶发超时模型服务抖动Java 侧设好连接/读取超时n8n 里可以加分支让超时后走降级话术Token 费用没有显著下降历史上下文没有真正裁剪检查是否还在把完整 history 传给模型用摘要缓存替换Webhook 地址被外部刷调用缺少鉴权开启 Header Auth 认证并加 IP 白名单修改 n8n 工作流后 Java 侧 NPE输出结构被改坏Java 侧对 n8n 返回值做兜底校验避免直接强转5.1 n8n 里最容易写错的 Code 节点接触 n8n 一周内最容易翻车的地方就是 Code 节点。新手常以为$input.json就是一个对象实际上 n8n 的数据流是“每项数据都在数组里”尤其当上游节点一次输出多行数据时$input是一个数组。我的建议是所有 Code 节点第一行都写const item $input.first().json;后面全部围绕这个item操作。等熟练之后再去看多 item 的并行场景。5.2 模型供应商切换之后响应结构变了怎么办n8n 官方节点把各家模型的响应做了部分统一但不同供应商终究有差异。比如 OpenAI 的响应用的是choices[0].message.content而某些国产模型可能是output.text。我的做法是在 n8n 里面再加一个中间节点专门把不同模型的结构转成统一格式Java 侧永远只认{ code, data: { answer } }这一种结构。这样以后换供应商只需要改 n8n 一个节点的映射逻辑Java 连重新编译都不用。5.3 幂等与重试别让用户点一次按钮烧两次钱前端用户经常会因为页面没响应而重复点击按钮如果 Java 后端没有幂等控制同一个问题就会连发好几次到 n8nToken 费用也跟着翻几倍。我的方案是在 Java 侧用一个简单的“会话 ID 消息摘要”作为 Redis 键设置 60 秒过期重复请求直接返回上一次的结果而不是重新调工作流。这一层防御极其有效在我上线之后原本偶发的重复扣费问题直接清零。另外n8n 自身的重试机制也要注意。默认情况下某个节点如果报错会重试但 Webhook 触发的工作流如果重试可能导致数据重复。我建议在 n8n 的 workflow 设置里把自动重试关闭或仅在特定节点开启避免连锁放大。5.4 没有真实业务数据时AI 一定要“承认不知道”最后想聊一个产品层面的心得很多团队在设计 AI 问答时总希望模型显得“无所不知”用户问什么都给个答案。但对于业务系统这是最危险的做法。我在 prompt 模板里反复强调“没有数据就直说不知道”是因为在“随便编一个答案”和“承认不知道”之间后者对用户的伤害要小得多。用户能接受“AI 暂时查询不到”但他绝对不能接受“AI 告诉我一个不存在的订单号”。让 Agent 学会“承认不知道”其实是把确定性边界重新交还给业务系统n8n 工作流在这里扮演的正是那个“边界守护者”。我在实际项目里把整套方案跑通之后有一个很深的体会大模型本身不负责正确性正确性要靠外部的确定性流程来兜底。Java 后端也好n8n 也好本质上都是在给模型划定“它可以自由发挥的笼子”笼子画得越清楚幻觉和成本就越可控。Token 直降 80% 只是个开始真正值钱的是你的系统从此变得可预期了。后面如果你们也要接 Agent建议先别急着调模型把流程编排和降级策略想清楚这才是确定性工作流的核心。