Java AI Agent 从 Demo 到生产:Spring AI / LangChain4j 的真实踩坑清单
Demo 能跑,不代表生产能活。这是大量 Java 团队在把 LLM Agent 接入既有 Spring Boot 系统后得到的共同结论:聊天界面能出字、工具能调通、演示视频很流畅,可一旦进入多轮会话、真实数据、成本核算和灰度运维,问题就会从"模型答得不好"变成"系统边界没守住"。这些问题往往不是换一个更强的模型就能解决的,它们属于工程化外壳的职责缺口。
需要先说明两点证据边界。其一,本文引用的 AgentScope 2.0 "Harness 工程化层"表述来自知乎上两篇产品线解读文章[1][2],属于二手材料,本文只借用其分析视角,不引入该运行时,也未核实其官方定义措辞与发布日期。其二,文中的具体故障现象来自 Stack Overflow 的单点提问[3][4][5][6][7],本文按"现象 → 排查 → 改造 → 验证"展开,不下"某框架存在缺陷"的结论;凡涉及类名、方法名、配置属性的代码块均标注为示意骨架,实际 API 请以所用版本的官方文档和源码为准。研究数据中各条来源均无可靠发布时间与热度值,因此本文不做"近期趋势"断言。
Demo 能跑,为什么生产会翻车:先建立 Harness 角度的自查框架
Harness 工程化层:把"模型能力"和"系统可靠性"分开看
按 AgentScope 2.0 相关解读文章的提法,企业级 Agent 需要在模型与推理循环之外,再加一层工程化外壳,用它承担记忆管理、工具边界治理、组件注册装配、可观测计量、评测回归等职责[1][2]。这个分层的价值不在于发明新概念,而在于给出了一个归因工具:当你遇到"上下文丢了"“SQL 执行出事了”"账单对不上"这类问题时,先问它属于哪一层,再决定是改框架配置、改装配方式,还是必须在业务侧兜底。
把这套视角映射到 Spring AI 与 LangChain4j,可以得到一张故障面矩阵:
| Harness 职责 | 典型故障 | 本文对应章节 | 主要归因层 |
|---|---|---|---|
| 记忆与上下文管理 | 记忆里存入原始 JSON 响应,重载后上下文异常 | 第二节 | 框架行为 + 业务序列化约定 |
| 工具边界与副作用治理 | LLM 生成的 SQL 被直接执行,产生副作用 | 第三节 | 必须由业务侧兜底 |
| 可观测与计量 | 拿不到 token 用量,成本与限流无从谈起 | 第四节 | SDK 与封装层的响应模型差异 |
| 装配与注册 | 子 Agent 报 “No agent found with name” | 第五节 | Spring 装配与框架注册机制不互认 |
| 模型与数据接入 | Embedding 凭据、维度、检索链路不通 | 第六节 | 基础设施与一致性约束 |
这张表同时是一份自查清单:如果五格中有三格以上说不清"我们的处理方式是什么",那么问题已经不是框架选型,而是缺少一个稳定的工程化外壳。
关于版本,还有一处必须提醒:Spring AI、LangChain4j 在演进过程中对用量对象、记忆抽象、Agent 注册相关 API 做过调整,社区文章标题中的"Spring AI 2.0"称谓来自第三方内容[10][11],本文未核实其正式发布状态。因此本文的代码一律按"思路骨架"给出,落地前请锁定你实际使用的版本并对照其文档。
记忆坑:MessageChatMemoryAdvisor 存下 raw JSON,导致上下文丢失
现象
一个在 Stack Overflow 上被提出的典型问题是:在 Spring AI 中使用 JSON_OBJECT 响应格式时,MessageChatMemoryAdvisor 把模型返回的原始 JSON 文本整体存进了对话记忆;当会话被重新加载、继续多轮对话时,上下文表现异常,用户的感受就是"模型忘了之前聊过什么"[3]。
注意"上下文丢失"这个说法在这里其实是两件事的混合:一是记忆里存的内容不正确,二是不正确的内容被回灌进后续 prompt,污染了上下文。前者是序列化问题,后者是通道隔离问题,排查时要分开。
排查路径
按三条线拆开定位,可以很快排除模型能力因素:
- **写入记忆的到底是什么对象。**是模型原始响应文本、解析后的结构化对象,还是规范化后的 assistant 消息?如果是前者,那么记忆里存的 JSON 壳子本身就不是合格的对话内容。可以在记忆实现上加日志,直接打印落库的消息类型与内容前若干字符。
- **读取记忆时如何还原。**重载会话后,记忆读出的消息被怎样转换成 prompt 的一部分?如果把 raw JSON 当作 assistant 的发言回灌,模型会把它当成"上一轮模型就是这么说话的",从而在风格和结构上被带偏。
- **格式化指令是否被当作对话历史。**JSON_OBJECT 这类响应格式要求通常通过系统提示或请求参数施加;如果把"请输出合法 JSON"这类指令也当作用户/助手消息写进记忆,它会在后续轮次反复出现并干扰语义。
区分这三条线之后,结论会落在哪一层也就清楚了:如果写入的是原始响应,属于框架默认行为与业务序列化约定不匹配;如果记忆容器在请求结束时随作用域销毁,属于状态管理问题;两者都不是模型质量问题。
改造要点
**第一,记忆写入前先做规范化。**在响应进入记忆之前插入一层解析与归一化,只把结构化的 assistant 消息内容入库,格式化外壳和中间解析产物一律丢弃。下面是示意骨架,展示管道形状,具体 Advisor 与消息类型的构造方式请对照你所用版本的 Spring AI 文档:
// 示意骨架:响应 -> 解析 -> 规范化消息 -> 入记忆,具体 API 以官方文档为准publicAssistantMessagenormalize(ChatResponseraw,ResponseFormatformat){Stringtext=raw.getResult().getOutput().getText();if(format==ResponseFormat.JSON_OBJECT){JsonNodenode=jsonParser.parse(text);// 解析失败应走明确的重试/降级分支returnnewAssistantMessage(canonicalize(node));// 只存规范化后的语义内容}returnnewAssistantMessage(text);}**第二,记忆存储与请求生命周期解耦。**生产环境里内存型 ChatMemory 无法跨实例、跨重启存活,需要换成持久化实现(如基于 JDBC 或 Redis 的消息存储),并明确会话 ID 的生成与过期策略。社区案例中出现的 pgvector + SSE + 多模型组合项目也把"状态落库"当作基本盘[8]。
**第三,通道隔离。**系统指令、格式化约束、工具调用结果、用户输入与助手回答应当分通道存储与回放,不要都压平成一串字符串。工具调用尤其容易出问题:原始 tool payload 回灌会迅速撑爆上下文窗口。
验证方式
写一个两轮对话的集成测试:第一轮用 JSON_OBJECT 格式提问并解析结果,第二轮提出依赖第一轮答案的追问,同时打印记忆读回内容并断言其中不包含 JSON 结构壳子。这个测试比人工看输出可靠得多,也应当成为回归集的一部分。
工具边界坑:LLM 生成 SQL,AST 校验必须做在执行之前
现象与原则
Stack Overflow 上有开发者提出:LLM 生成 PostgreSQL 查询时,如何在 Java 侧安全校验 AST,以防止副作用[4]。这是典型的"工具边界"问题,也是全文最不能妥协的一节。
必须先确立原则:**模型侧约束只作提示,不作安全边界。**系统提示里写"只生成 SELECT"对真正的攻击面没有任何保证,因为提示词可被用户输入、检索到的文档、甚至工具返回内容间接注入。安全边界必须落在代码路径上,位于 SQL 文本与数据库连接之间。
分层防护
一个可执行的最小方案至少包含五层:
**1. 语句切分与预拒绝。**先把多语句、注释、异常字符挡掉。注意三类常见绕过:;藏在--注释后、藏在/* */块注释中、藏在 PostgreSQL 的 dollar-quoted string($$ ... $$)里。用字符串切割判断语句数量是不可靠的,必须依赖真正的词法/语法解析结果。
**2. AST 解析。**候选解析库包括 JSqlParser 与 Apache Calcite,但两者对 PostgreSQL 方言扩展语法的覆盖度不同,使用前必须用你实际会生成的语句做覆盖测试;解析失败应当直接拒绝,而不是降级为"放行并执行"。这一点在实践中常被写反。
**3. 节点白名单。**只允许 SELECT 与 WITH 开头的只读查询,遍历 AST 时对出现的节点类型做白名单判定:
// 示意骨架:AST 节点白名单判定,具体 API 以所选解析库文档为准booleanisSafe(Noderoot){returnvisitor.visitAll(root,node->switch(node.type()){caseSELECT,FROM,WHERE,JOIN,GROUP_BY,ORDER_BY,LIMIT,COLUMN,LITERAL,FUNCTION_WHITELISTED->true;caseINSERT,UPDATE,DELETE,DROP,ALTER,CREATE,GRANT,CALL,TRANSACTION_CTL,SET->false;default->false;// 未识别节点一律拒绝});}白名单要特别覆盖 PostgreSQL 的几个隐蔽面:数据修改型 CTE(WITH x AS (INSERT ...) SELECT ...)、SELECT ... FOR UPDATE这类加锁子句、可能有副作用或高开销的函数(如pg_sleep、dblink、大对象函数)、以及set_config这类会改会话状态的调用。函数名也要走白名单而不是黑名单。
**4. 执行层兜底。**即便 AST 判断通过,数据库侧仍要设防:使用只读账号或只读事务、设置语句超时与锁等待超时、限制返回行数、限制可访问的 schema(固定search_path)、关闭不必要的扩展。这一层独立于解析库,是纵深防御的底座。
**5. 模型侧约束。**在提示中要求只读查询,并要求模型先输出意图再输出 SQL,便于审计与回放。它能降低误操作概率,但不能替代前四层。
执行流水线的形状是:用户意图 → LLM 生成 SQL 文本 → AST 校验 → 只读执行沙箱 → 结果脱敏返回。其中"模型不可信边界"恰好落在 SQL 文本生成之后,任何跨越这条边界的直连执行都是缺陷。
一个容易被忽略的取舍
严格白名单会带来拒答率上升。正确的处理不是放宽校验,而是把"被拒绝的 SQL"作为可观测事件记录下来,回到提示工程或语义层(预定义查询模板、语义视图)去解决。在高可靠场景里,宁可让 Agent 回答"这个查询我不能执行",也不要让它执行一个没人审计过的语句。
可观测坑:token 用量拿不到,成本与限流就无从谈起
现象
有开发者在使用 google-genai Java SDK v1.23.0 调用 Gemini 2.5 Flash 时,询问如何从 GenerateContentResponse 中取得 usageMetadata 的 token 用量信息[5]。这类问题在多框架混合的技术栈里非常普遍:用量信息存在,但藏在不同的响应结构里,经过封装层之后又可能被丢掉。
排查路径
- **确认原生响应上的实际访问路径。**用量元数据在不同 SDK 版本中的字段名和暴露方式可能不同,必须以该版本的 javadoc 为准,不要凭印象写 getter。
- **区分两条链路。**直连 SDK 的响应对象与经过 Spring AI / LangChain4j 封装后的响应对象不是一回事。封装层如果只取了文本结果,用量元数据就会在这一层蒸发。这解释了为什么"在 SDK 文档里看得到、在业务代码里拿不到"。
- **注意 OpenAI 兼容接口的差异。**很多团队通过兼容网关接入模型,此时 usage 的位置和字段命名跟随兼容协议,与厂商原生 SDK 不同,混用时极易取到 null。
改造要点
把用量采集做成统一出口的拦截器,而不是散落在每个调用点。拦截器按模型、租户、场景、Agent 名称打点,输出至少包括输入 tokens、输出 tokens、总 tokens、调用延迟、失败类别。这些指标同时支撑三件事:成本分摊、限流配额、容量规划。
拿不到精确用量时要有降级方案:用 tokenizer 估算 token 数,并在指标中显式打上"估算"标记,避免估算值污染成本核算口径。这一点常被省略,结果是报表里出现一批来源不明的数字。
在指标侧,建议把"精确计量覆盖率"本身作为一个指标上报:即多少比例的调用拿到了真实 usage。这个覆盖率若长期偏低,说明采集点埋设有遗漏。
装配坑:Spring @Bean 子 Agent 报 “No agent found with name”
现象
LangChain4j 与 Spring 集成时,有开发者遇到使用 Spring @Bean 声明的子 Agent 在运行时抛出 “No agent found with name” 的错误[6]。这个报错的表象是"找不到",根因可能是命名、注册、生命周期或条件装配中的任意一种,不能一上来就归因于框架。
排查清单
按以下顺序逐条验证,通常前两条就能定位:
- **注册名与引用名是否一致。**包括大小写、前后缀、限定符。多 Agent 编排时名称常由常量或字符串字面量维护,重命名重构很容易漏改引用。
- **子 Agent 是否真的进入了框架的注册表。**Spring 的 @Bean 只保证它是一个 Spring Bean,不等于框架的 Agent 注册机制会自动发现它。如果框架依赖自己的注解或扫描机制注册,而你只写了 @Bean,就会出现"Bean 在容器里、不在注册表里"的错位。
- **初始化顺序。**主 Agent 装配时若子 Agent 尚未完成注册,引用解析会失败。注意 Bean 的依赖声明是否显式,避免依赖脆弱的初始化顺序巧合。
- 条件装配。@Conditional、@Profile、配置开关都可能让某个 Bean 在特定环境根本不存在,此时报错是"症状",条件表达式才是病因。
在排查时先确认所用 LangChain4j 版本中子 Agent 的实际注册接口或注解名称,以及是否有已采纳答案给出结论;不同版本的集成方式并不一致,凭记忆命名 API 是高风险动作。
改造要点
**集中注册。**用一个显式的注册配置类把所有 Agent 的定义收拢,名称统一定义为常量,主 Agent 与子 Agent 的引用都指向这些常量。
**启动期自检。**在应用启动完成后遍历引用关系,缺引用立即 fail-fast,而不是等到第一个用户请求才报错:
// 示意骨架:启动期注册完整性自检,具体注册表 API 视框架版本而定voidvalidateRegistry(AgentRegistryregistry,RequiredRefsrefs){Set<String>missing=refs.names().stream().filter(name->!registry.contains(name)).collect(toSet());if(!missing.isEmpty()){thrownewIllegalStateException("Agent 注册缺失: "+missing);}}**集成测试覆盖注册完整性。**用最浅层的上下文启动测试跑一遍装配,成本很低,但能在 CI 阶段拦住绝大多数装配类问题。这类问题在单测中永远不出现,在生产中却以"某个功能整体不可用"的形式爆发,是最典型的 Harness 缺口。
接入坑:Embedding(以 Vertex AI 为例)在 Spring Boot 里的最小接入路径
常见卡点
有开发者询问在 Spring Boot 应用中使用 Vertex AI Embedding 的做法[7]。这类问题通常不是"调不出向量",而是一串连环卡点:
**凭据与环境。**服务账号、Application Default Credentials 的加载顺序、区域端点与项目 ID 的配置,在本地、CI、生产三套环境往往不一致。最常见的表现是本地跑通、容器里抛权限或端点异常。
**模型接入层。**Spring AI 与 LangChain4j 都提供了 Embedding 抽象,但与 Vertex AI 对应的实现模块的官方支持状态、实现类名与配置属性会随版本变化,接入前应查证所用版本的文档与模块清单,而不是照搬教程代码。
**存储与检索。**向量库可选 pgvector 或 Elasticsearch,社区案例中两者都有实践参考[8][12]。选择时考虑现有基础设施:如果团队已经运行 PostgreSQL,pgvector 的运维成本更低;如果已有 Elasticsearch 集群并需要混合检索,则后者更顺手。
**一致性。**这是最容易出隐性故障的地方:入库向量与查询向量必须来自同一模型版本、同一维度、同一归一化约定。一旦中途升级了 embedding 模型,旧向量与新查询之间的相似度不再可比,检索质量会静默下降,而监控往往看不到错误,只能看到"效果变差"。
最小接入流程
// 示意骨架:Embedding 调用流程,接口签名以官方文档为准float[]docVector=embeddingModel.embed(documentText);float[]queryVector=embeddingModel.embed(userQuery);List<Match>matches=vectorStore.topK(queryVector,8);向量表的 DDL 形状大致如下(以 pgvector 为例,维度按你的模型实际输出填写):
-- 示意骨架:向量表结构,维度需与所用 embedding 模型输出一致CREATETABLEdocument_chunks(id bigserialPRIMARYKEY,doc_idvarchar(64)NOTNULL,chunk_texttextNOTNULL,embedding vector(768)NOTNULL,model_vervarchar(32)NOTNULL,created_at timestamptzNOTNULLDEFAULTnow());CREATEINDEXONdocument_chunksUSINGhnsw(embedding vector_cosine_ops);model_ver字段值得保留:它让模型升级时可以并行写入新版本向量、灰度切换检索版本,而不是一次性全量重算后被迫回滚。RAG 数据流为"文档 → 切分 → embedding → 向量库 → 检索 → 组装上下文",其中切分粒度、top-k 数量与上下文预算需要联动调优,否则会出现"检索到的片段塞不下上下文窗口"的问题。
面向存量 Spring 项目的最小改造路径
对于已经有多年业务逻辑的 Spring 项目,核心原则是不动主干、旁路接入、状态外置、治理收口。分三阶段推进,每阶段都有明确的回滚点。
**阶段 0:旁路接入(1–2 天)。**在一个独立模块或 starter 中引入模型客户端,先做无状态单轮能力,例如文案生成、摘要、分类打标。不碰业务事务,不引入记忆,不开放工具调用。此时的风险面只有网络调用与超时,回滚方式是关闭功能开关走原逻辑。
**阶段 1:状态与工具外置(约 1–2 周)。**把第二节的记忆持久化、第三节的工具白名单与 SQL 校验、第四节的用量采集一次性补齐。这三项必须在开放工具调用之前完成,顺序不能颠倒:先有边界与计量,再有能力。回滚点是保留旧的无状态路径,Agent 能力通过开关降级。
**阶段 2:治理收口(按需)。**加入第五节的 Agent 注册自检、第六节的 RAG 与 Embedding、评测回归集、灰度与熔断。此时 Agent 已经承载业务流量,治理能力必须同步到位。回滚点是按场景灰度回退到上一阶段能力。
每阶段的改动清单、新增依赖与验证方式归纳如下:
| 阶段 | 改动点 | 新增依赖 | 验证方式 | 回滚方式 |
|---|---|---|---|---|
| 0 旁路接入 | 独立 AI 模块、模型客户端、超时配置 | 模型 SDK 或框架 starter | 单轮调用集成测试 | 功能开关关闭 |
| 1 状态与工具外置 | 记忆持久化、SQL 校验、用量采集 | 持久化记忆实现、SQL 解析库、指标组件 | 两轮记忆测试、SQL 校验单测、用量指标核对 | 降级到无状态路径 |
| 2 治理收口 | 注册自检、RAG、评测集、灰度熔断 | 向量库驱动、检索组件 | 注册完整性测试、检索回归集 | 按场景灰度回退 |
需要强调的是,社区已有的案例组合(Spring AI + RAG + MCP + pgvector + SSE)[8]以及"给老 Spring 项目装 AI Agent"的实践讨论[9],可以作为技术栈选型的参考坐标,但其内部实现细节不在本文复述范围,本文也不假定其可复现性。真实落地时应优先选取可运行的开源仓库作为参照,并自行完成依赖版本与安全审计。
落地检查清单与证据边界
上线前逐项核对:
| 检查项 | 通过标准 |
|---|---|
| 记忆持久化 | 重启后会话可恢复,测试中可断言记忆内容不含原始响应壳子 |
| SQL 校验 | 多语句、注释绕过、数据修改型 CTE、危险函数均有单测覆盖 |
| 执行沙箱 | 只读账号、语句超时、行数上限、schema 限制全部生效 |
| 用量计量 | 指标可见、按模型/租户维度可分组,估算值有独立标记 |
| Agent 注册 | 启动期自检 fail-fast,集成测试覆盖注册完整性 |
| Embedding 一致性 | 模型版本锁定,入库与查询向量维度、归一化一致 |
| 降级与熔断 | 超时、限流、模型不可用时可回退到非 AI 路径 |
| 数据出网审计 | 敏感字段脱敏策略明确,调用日志可追溯 |
最后交代本文的证据边界,以免读者把局部经验当成普遍结论:
第一,五个故障现象均来自 Stack Overflow 的单点提问[3][4][5][6][7],本文未能在写作过程中逐条复现,也未核实全部已采纳答案的结论;请将其当作排查起点而非定论。
第二,Harness 工程化层的表述来自与产品线相关的解读文章[1][2],存在内容营销放大效应的可能,本文只借其分类框架,未引用其产品结论。
第三,本文所有代码块均为思路骨架。Java AI 框架的 API 演进较快,类名、方法名、配置属性在不同版本间存在差异,落地前必须以所用版本的官方文档与源码为准;无法确认的抽象,宁可写"框架提供 X 能力,具体接口见官方文档",也不要凭印象命名。
第四,本批研究数据缺少发布时间与热度信息,也没有 GitHub、官方博客等一手来源,因此本文不做时间敏感的趋势判断。若要把本文中的某个坑位写成生产事故复盘,还需要补充可复现的工程数据。
归根结底,从 Demo 到生产隔着的不是一个更大的模型,而是记忆、工具边界、可观测性、装配注册、模型接入这五块工程化外壳。它们都不性感,但决定了 Agent 在真实系统里能活多久。
参考资料
[1] 研发企业级 AI Agent,为什么需要 Harness 工程化层?解析 AgentScope 2.0 的设计哲学,知乎,https://zhuanlan.zhihu.com/p/2061417519588680299
[2] AI Agent 从 Demo 到大规模生产,中间隔着多少"工程化"鸿沟?AgentScope 2.0 深度解析,知乎,https://zhuanlan.zhihu.com/p/2046267814298784797
[3] MessageChatMemoryAdvisor stores raw JSON response when using JSON_OBJECT response format, causing context loss on conversation reload,Stack Overflow,https://stackoverflow.com/questions/79894013/
[4] Safely validating AST of LLM-generated PostgreSQL queries in Java to prevent side effects,Stack Overflow,https://stackoverflow.com/questions/79906727/
[5] How to access usageMetadata (token usage) from GenerateContentResponse using google-genai Java SDK (v1.23.0) with Gemini 2.5 Flash?,Stack Overflow,https://stackoverflow.com/questions/79794964/
[6] LangChain4j throws “No agent found with name” when using Spring @Bean sub-agents,Stack Overflow,https://stackoverflow.com/questions/79890669/
[7] using Vertex AI Embedding in Spring Boot App,Stack Overflow,https://stackoverflow.com/questions/79880065/
[8] JChatMind|Java AI Agent 项目实战(Spring AI + RAG + MCP + pgvector + SSE + 多模型),知乎,https://zhuanlan.zhihu.com/p/1992998854321254698
[9] 给老 Spring 项目装个 AI Agent,知乎,https://zhuanlan.zhihu.com/p/2081328811451466147
[10] Spring AI 2.0 进阶入门:RAG、Structured Output 与 Agent 信息闭环,知乎,https://zhuanlan.zhihu.com/p/2082759430844827524
[11] Spring AI 2.0 Agent 进阶:Tool Calling、Action 与可靠执行,知乎,https://zhuanlan.zhihu.com/p/2084562921976230508
[12] JD Conf 2026|使用 Spring AI 与 Elasticsearch 轻松构建 Java RAG,B站,https://www.bilibili.com/video/BV1FF9mBTEUY
[13] 2026 年了,Java AI 五大框架根本不用五选一,知乎,https://zhuanlan.zhihu.com/p/2076310870624413559
[14] 华为首次发布智能体编程平台"码道":不是拼生成量,而是在百万行 Java、长周期维护与高可靠中运行,知乎,https://zhuanlan.zhihu.com/p/2010426113038521363
[15] Spring AI Alibaba + Nacos 动态 MCP Server 代理方案,知乎,https://zhuanlan.zhihu.com/p/1913275830370538706