Java+AI应用研发:Spring AI、RAG与Agent工程化落地

发布时间:2026/9/19 6:27:34
Java+AI应用研发:Spring AI、RAG与Agent工程化落地 前阵子有位做了四年 Java 后端的朋友找我聊天说想往 AI 应用研发方向转但打开招聘信息一看满屏写着熟悉大模型应用开发有 Agent 落地经验掌握 RAG 与向量检索而他手上的技能是 Spring Boot 增删改查、MyBatis 写 SQL、偶尔调一调线程池。他问我这两者之间到底差了哪一层。这个问题非常典型。Java AI 应用研发拆开就是三个词Java、AI、应用研发。注意最后四个字——是应用研发不是模型研发。这条路线真正的价值在于把大模型这个能力接进已有的企业系统里让它变成一个能稳定跑、能观测、能计费、能回滚的业务模块。它要求你懂 Java 的并发、动态代理、序列化也要求你懂 Prompt 怎么组装、Agent 怎么编排、检索怎么召回但它不要求你去推导注意力机制的公式也不要求你从零训练一个基座模型。这篇文章面向三类人写了几年 Java 业务代码、想切入 AI 应用层的后端工程师需要把大模型能力集成进现有系统的架构同学以及正在准备相关方向面试、想理清知识主线的求职者。下面我会按定位—技术拆解—实操落地—工程化—排错的顺序把这套东西讲透代码和配置都尽量给到能直接抄的程度。1. 先把定位想清楚Java 在 AI 应用研发里到底干什么1.1 别把模型训练和应用研发混为一谈我见过太多人一上来就去啃深度学习框架啃了两周发现跟自己日常写的代码完全不搭边然后就放弃了。这是个定位错误。模型侧的工作——预训练、微调、蒸馏、推理加速——目前主流工具链确实集中在 Python 生态里。而 Java 的位置在应用层它是模型能力和业务系统之间的那层胶水骨架。具体来说Java 在这一层负责的事情包括把用户请求和上下文组装成模型能吃的格式管理多轮会话状态调度多个模型调用和工具调用做限流、重试、降级、缓存把结果落库、推送前端、写审计日志以及最重要的——保证这套链路在线上高并发场景下不出事。举个我实际做过的场景。一个内部知识问答系统前端一次提问后端要串起这些动作查用户权限、从向量库召回相关文档片段、拼装 Prompt、调用模型、流式推回前端、把问答记录写入审计表、统计 token 消耗用于成本归集。这里面只有调用模型这一步是 AI 相关的剩下全是标准 Java 后端活。所以你会发现Java 功底越扎实的人切入 AI 应用层反而越快。1.2 三条常见路线与各自的取舍从 Java 转 AI 应用研发我观察到的落点大致有三种各自适合的人和坑都不一样。路线主要工作内容适合谁常见坑模型接入与网关封装多模型统一接口、限流计费、灰度切换有微服务/网关经验的后端各家接口协议不一致抽象层没设计好会越改越乱业务场景集成智能客服、文档问答、代码辅助、报表生成熟悉某条业务线的同学只做 Demo 不做评估上线后效果不可控Agent 与流程编排工具调用、多步任务、工作流引擎有状态机/工作流经验的工程师无限循环、工具调用失败无兜底、调试困难我个人的建议是先走模型接入与网关这条路。原因很实在——它跟你已有的技能重合度最高能快速产出可用的东西同时你会被迫把 Prompt 结构、流式返回、异常处理这些基础问题全部摸一遍。等这层跑顺了再往上做业务集成和 Agent心里是有底的。1.3 环境准备JDK、构建工具与依赖拉取这一节看着枯燥但每年都有大量人卡在第一步。版本选择上我的建议很明确JDK 17 或 JDK 21。原因是 Spring Boot 3.x 系列要求 JDK 17 起步而你后面大概率要用到的 Spring AI 也是构建在 Spring Boot 3.x 之上的。JDK 21 是 LTS虚拟线程在这里正式转正对 IO 密集型的模型调用场景帮助很大能上手就直接上 21。装完之后先验证java -version javac -version echo $JAVA_HOME三行都要有正常输出。JAVA_HOME没配好是最常见的问题表现是 Maven 编译报找不到 tools.jar或者构建工具直接找不到 java 命令。Windows 上在系统环境变量里配macOS/Linux 上写进 shell 的 profile 文件配完重开一个终端再验一次。构建工具用 Maven 还是 Gradle 都行团队用哪个你跟哪个。但有一件事必须做把仓库地址换成国内镜像。默认走中央仓库拉一个 Spring AI 的依赖树可能要等到你怀疑人生换成国内镜像之后速度差距是数量级的。Maven 是在settings.xml的mirrors里配Gradle 在init.gradle或项目仓库配置里改。依赖管理上有个小提醒Spring AI 这类还在快速迭代的项目务必用 BOM 统一版本号不要在每个依赖上手写版本。我踩过一次坑——手动指定了核心包和 starter 的不同版本编译能过运行时在自动配置阶段报类找不到找了大半天。2. 核心技术拆解从 Java 基础到 AI 编排能力2.1 Java 基础里真正高频用到的那些部分很多人问我 Java 基础要学到什么程度才能做 AI 应用我的回答是不需要你精通 JVM 调优但有四块必须熟。第一块是集合与泛型。模型返回的是结构化数据你要用MapString, Object承载参数、用ListMessage承载会话历史、用泛型把模型返回的 JSON 反序列化成你的业务对象。集合用不熟光是把模型输出转成 Java 对象就能写出一堆 bug。第二块是Stream 与函数式接口。流式返回的场景下一长串 token 是以数据流形式过来的你要用map、filter、flatMap去处理它还要知道map和flatMap的区别在哪——前者是一对一映射后者是先把每个元素展开成流再合并。这个点在处理流式响应时特别容易搞混。第三块是反射与注解。你后面写的工具方法自动注册给模型这套机制本质就是扫注解 反射调用。Spring 的自动配置、参数校验、序列化框架全都建立在这上面。第四块是异常体系。模型调用和普通接口调用最大的差别是它失败的方式太多了——网络超时、限流被拒、内容被截断、返回格式不合预期、模型幻觉导致的参数错误。你需要一套自己的异常分类而不是无脑catch (Exception e)。2.2 并发与等所有线程跑完的正确姿势这是热词里出现频率很高的一个点我单独拎出来讲因为它在 AI 应用里出现得极其频繁。典型场景用户问一个问题你需要并行做三件事——查向量库召回文档、查用户历史会话、调用一个外部工具接口三件事都做完之后再拼 Prompt 调模型。串行做的话响应时间直接翻三倍。最朴素的写法是开三个线程然后join但那种写法又臭又长。正确姿势是CompletableFutureExecutorService pool Executors.newFixedThreadPool(8); CompletableFutureListDoc docsFuture CompletableFuture.supplyAsync(() - vectorStore.search(query, 5), pool); CompletableFutureListMsg historyFuture CompletableFuture.supplyAsync(() - historyService.load(userId), pool); CompletableFutureString toolFuture CompletableFuture.supplyAsync(() - toolClient.call(userId), pool); CompletableFutureVoid all CompletableFuture.allOf( docsFuture, historyFuture, toolFuture); try { all.get(3, TimeUnit.SECONDS); } catch (TimeoutException e) { // 超时后走降级只用已完成的结果拼 Prompt log.warn(部分前置任务超时降级处理, query{}, query); } ListDoc docs docsFuture.getNow(Collections.emptyList()); ListMsg history historyFuture.getNow(Collections.emptyList()); String toolResult toolFuture.getNow();这里有几个细节值得单独说。allOf本身不返回结果它返回一个所有任务都完成的CompletableFutureVoid所以你必须拿着每个子 future 自己去取结果。这是新手最容易卡的地方。getNow(fallback)是超时降级的关键。它不会阻塞等待任务没完成就返回你给的默认值。AI 应用里降级比完美更重要——少召回两篇文档用户可能完全感知不到但接口卡死三秒体验就直接崩了。线程池的选择也有讲究。如果你的任务是 IO 密集型调模型、查库、调外部接口都属于线程数可以给得比 CPU 核数大很多经验值是核数的 4 到 8 倍。如果跑在 JDK 21 上直接用虚拟线程更省心ExecutorService pool Executors.newVirtualThreadPerTaskExecutor();一行搞定不用再算线程数也不用担心池子被打满。JDK 21 里还有个结构化并发的预览特性用StructuredTaskScope把一起提交、一起等、超时全取消做到语义级别逻辑比allOf清晰不少。不过它在后续版本还在演进生产上要不要用取决于你的 JDK 版本策略可以先了解不必急着上。注意CompletableFuture默认使用ForkJoinPool.commonPool()池大小等于 CPU 核数减一。在上面这种阻塞场景里用默认池很容易把公共池打满进而影响其他用它的组件。凡是带阻塞的异步任务一定传自己的线程池。2.3 动态代理与切面给 AI 调用加统一能力模型调用这类操作天然需要一层统一拦截日志要打全输入输出都要留痕、失败要重试、超时要熔断、成本要统计、相同问题要命中缓存。如果每处调用都手写一遍这些逻辑代码会迅速腐烂。这时候就用得上动态代理。Java 里有两种主流实现JDK 自带的动态代理基于接口要求目标类实现接口CGLIB 通过生成子类来实现不要求接口但没法代理final类和方法。Spring AOP 会自动在两者之间选择。实际用法上我更推荐自定义注解 切面的组合Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface AiCall { int maxRetry() default 2; long timeoutMs() default 30000; String scene() default default; }Aspect Component public class AiCallAspect { Around(annotation(aiCall)) public Object around(ProceedingJoinPoint pjp, AiCall aiCall) throws Throwable { long start System.currentTimeMillis(); int attempt 0; while (true) { try { Object result pjp.proceed(); metrics.record(aiCall.scene(), System.currentTimeMillis() - start, true); return result; } catch (Exception e) { attempt; if (attempt aiCall.maxRetry() || !isRetryable(e)) { metrics.record(aiCall.scene(), System.currentTimeMillis() - start, false); throw e; } Thread.sleep((long) Math.pow(2, attempt) * 200); } } } private boolean isRetryable(Exception e) { // 4xx 类错误重试没意义只有超时和 5xx 才重试 return e instanceof TimeoutException || e instanceof ServerErrorException; } }为什么要区分可重试和不可重试因为盲目重试是有代价的。参数错误、鉴权失败这类问题重试一百次也是同样的结果白白多花 token 钱。只有超时和临时性服务端错误才值得退避重试。退避策略用指数增长2^n * 200ms避免在服务抖动时把所有请求同时砸过去。还有一个不太被提及的用法用代理做多模型切换。定义一个统一的ChatService接口底下挂多个实现通过切面或者自定义的BeanPostProcessor按场景把请求路由到不同模型。这样业务代码只依赖接口换模型的时候不用改一行业务逻辑。2.4 模型接入层选型框架还是自己写这是很多人纠结的第一个架构决策我列个对照表结论在后面。方案适合场景优点需要接受的代价Spring AI已有 Spring Boot 项目生态贴合、自动配置、流式和工具调用开箱可用版本迭代快接口偶有变动Spring AI Alibaba用国内模型服务、要接国产模型对国内模型适配好、支持多种模型统一接入需要关注其自身版本节奏LangChain4j想要更细粒度的链式编排抽象丰富、社区活跃学习曲线更陡和 Spring 结合需额外适配裸写 HTTP 客户端只调一两个接口、需求简单无额外依赖、完全可控重试流式工具调用全部自己写长期维护成本高我的实际选择是新项目直接上 Spring AI如果主用国内模型服务就用 Spring AI Alibaba两者可以共存。理由是它把模型对话抽象成了ChatClient把向量检索抽象成了VectorStore把工具调用抽象成了注解。这些抽象你哪怕将来换框架思路也是一样的不会白学。但要提醒一句抽象层能省掉 80% 的样板代码剩下 20% 的坑还是得自己填。比如流式返回的异常处理、多轮会话的裁剪策略、token 超限的截断逻辑这些框架不会替你做得自己写。3. 实操从零搭一个可运行的 Java AI 对话服务3.1 项目初始化与依赖配置用 Spring Initializr 生成骨架JDK 选 21依赖勾 Web 和 Lombok 就够了AI 相关的依赖手动加这样你对版本有完整控制权。properties java.version21/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency !-- 用国内模型服务的话换成对应的 starter -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency /dependencies注意spring-boot-starter-webflux这一项。做流式输出你会用上Flux虽然用SseEmitter配合 Spring MVC 也能做但 WebFlux 在这块更顺手。如果整个项目已经是 MVC 架构不想混两套模型那就用SseEmitter也是完全可行的我两种都写过。3.2 关键配置项与 Prompt 组装配置文件里最少要有这几项spring: ai: openai: api-key: ${AI_API_KEY} base-url: ${AI_BASE_URL} chat: options: model: qwen-plus temperature: 0.7 max-tokens: 2048 # 连接和读取超时要分开配默认值在长文本场景下经常不够 retry: max-attempts: 3 backoff: initial-interval: 500ms multiplier: 2几个参数的取值逻辑我说一下。temperature控制随机性。做知识问答、数据抽取、代码生成这类需要确定性的任务我一般压到 0.1 到 0.3做创意文案、头脑风暴才放到 0.7 以上。同一个系统里不同场景应该用不同温度而不是全局一个值。max-tokens直接决定单次调用的成本上限也决定你的响应会不会被莫名其妙截断。设置的时候要留余量因为很多模型是输入输出共享上下文窗口的如果输入召回了几大段文档输出空间就被挤掉了。我遇到过最典型的一次事故召回配置从 3 篇调到 10 篇结果模型输出全部被截断在中途排查了两小时才发现是上下文超限。Prompt 组装上我建议永远不要把用户输入直接拼进字符串而是用结构化消息列表String system 你是一个面向企业内部的文档问答助手。 回答必须严格基于给定的参考资料资料中没有的内容直接说明未找到相关依据不要编造。 回答使用简体中文先给结论再给依据。 ; String context docs.stream() .map(d - 【资料 d.getId() 】 d.getContent()) .collect(Collectors.joining(\n\n)); String user 参考资料\n context \n\n用户问题 question; String answer chatClient.prompt() .system(system) .user(user) .call() .content();这里有三点经验。系统提示里明确写资料中没有的内容不要编造能显著降低幻觉率这是最低成本的优化手段。给每段资料编号方便模型引用也方便你在前端做溯源展示用户点一下就能跳到原文。最后把先给结论再给依据写进提示词输出的可用性会高很多因为这是给人看的不是给机器看的。提示Prompt 里绝对不要出现任何敏感、争议性或需要判断立场的内容要求。做企业应用时提示词设计应聚焦于准确、客观、可追溯超出知识范围的问题明确拒答这既是效果问题也是风险控制问题。3.3 流式输出SSE 与异常处理流式输出的价值不只是看起来更快它实实在在降低了首字延迟。用户看到第一个字出来就认为系统在工作了心理等待时间大幅缩短。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String q) { return chatClient.prompt() .user(q) .stream() .content() .onErrorResume(e - { log.error(流式对话失败, q{}, q, e); return Flux.just(\n[服务暂时不可用请稍后重试]); }) .timeout(Duration.ofSeconds(60)); }onErrorResume和timeout这两个操作符是必须加的。原因是流式连接建立之后异常不会走常规的 HTTP 错误响应客户端只会看到连接被莫名其妙地掐断前端表现就是打字打一半停了用户完全不知道发生了什么。用onErrorResume把错误信息作为流的一部分推回去前端能给出明确提示。还有一个细节流式场景下的事务边界。很多时候你需要把完整的问答记录落库但流式返回是一段一段的什么时候写我的做法是收集完整个流再写用doOnComplete或者collectList()之后处理。千万别在每个分片上写一次库。3.4 本地模型部署与接口对接有些场景必须用本地部署的模型数据不能出内网、要做离线演示、或者只是想省点调用成本做开发调试。最省事的做法是用 Ollama 在本地起一个服务它提供 OpenAI 兼容的接口所以你的 Java 代码几乎不用改只改base-urlspring: ai: openai: base-url: http://localhost:11434/v1 api-key: ollama chat: options: model: qwen2.5:7b# 拉取并启动 ollama pull qwen2.5:7b ollama serve # 验证 curl http://localhost:11434/v1/chat/completions -H Content-Type: application/json \ -d {model:qwen2.5:7b,messages:[{role:user,content:你好}]}这里要提醒的是硬件门槛。7B 量级的模型量化之后大概需要 6 到 8GB 显存如果只有 CPU能跑但速度会慢到不适合交互场景。选模型的时候先看自己的卡别一上来就拉 32B跑不起来白折腾。另外本地模型和云端模型在同一个项目里共存时建议做成两套ChatClientBean用Qualifier区分而不是靠运行时改配置。配置切换意味着重启Bean 区分可以按场景灵活选做 A/B 对比的时候特别方便。4. AI Agent 与工程化让它从能跑到能用4.1 工具调用让模型能操作你的 Java 方法Agent 和普通对话的本质区别就是模型能不能动手。工具调用的流程是你告诉模型有哪些工具可用名称、说明、参数结构模型根据用户问题决定调用哪个、传什么参数你的代码执行完把结果再喂回模型模型据此给出最终回答。在 Spring AI 里最简形式是注解Component public class OrderTools { Tool(description 根据订单号查询订单状态参数为订单号字符串) public String queryOrderStatus(String orderNo) { Order order orderService.findByNo(orderNo); if (order null) { return 未找到订单 orderNo; } return 订单状态 order.getStatus() 创建时间 order.getCreatedAt(); } }String reply chatClient.prompt() .user(帮我看看订单 A20250101 到哪了) .tools(new OrderTools()) .call() .content();看着很美但真上生产之前有几个坑必须处理。第一个坑工具描述写不清模型就调错。description不是写给人看的注释是写给模型看的说明书。参数含义、格式要求、返回什么都要写清楚。我见过因为描述里没写订单号为 12 位数字字符串模型把用户口语化的描述整个塞进参数里直接调用失败。第二个坑工具调用可能死循环。模型调工具结果不满意再调再不满意来回几轮把 token 烧光。必须设最大轮次限制一般 5 到 10 轮封顶。第三个坑工具内部一定要自己做权限校验。绝对不能因为这是模型传过来的参数就默认可信。用户可以通过精心构造的提问诱导模型传任意参数权限判定的责任永远在你自己的代码里不在模型那边。4.2 RAG 最小闭环切分、向量化、检索、拼装RAG 这个词听着玄乎拆开就是四步把文档切成小块、把每块转成向量存起来、用户提问时按相似度捞出最相关的几块、拼进 Prompt 让模型基于这些内容回答。文档切分上有几个参数要调。块大小一般 300 到 800 字之间太小会丢上下文太大检索精度下降。块之间要留重叠比如重叠 50 到 100 字防止一句话正好被切断导致语义丢失。切分策略上优先按语义边界切段落、标题、代码块实在不行再按固定长度硬切。// 伪代码具体 API 以你用的版本为准 ListDocument chunks new TokenTextSplitter(500, 80, 5, 10000, true) .apply(List.of(new Document(rawText))); vectorStore.add(chunks);检索阶段纯向量相似度有个明显短板它对关键词不敏感。用户问报销标准是多少文档里写的是费用报销管理办法语义相近能召回但如果用户问的是某个特定编号的条款纯向量检索经常召不回。所以生产上更稳的做法是混合检索向量召回 关键词召回两路结果合并去重后重排。拼装阶段就是我前面说的那套编号、加分隔、明确要求不基于资料不要编。还有一点检索结果要做相关性阈值过滤。相似度低于某个值的片段宁可不要因为塞进去的噪声片段反而会干扰模型判断让回答质量下降。这个阈值需要在你的真实数据上调没有万能数字。4.3 可观测性与测试AI 应用怎么测、怎么盯AI 应用最难的不是写是保证它一直稳定。传统接口的测试是输入 A 必须返回 B但模型输出是概率性的同样的输入两次结果可能不一样这套断言直接失效。我实际用的方法是三个层次。第一层是结构化断言。不比对完整文本而是比对关键特征返回是否合法 JSON、必填字段是否存在、数值是否在合理区间、是否包含引用编号。这一层能拦住绝大多数工程性故障。第二层是固定用例集回归。挑 50 到 200 条真实场景问题每次改 Prompt 或者换模型之后整体跑一遍人工看结果或者用另一个模型打分。听起来土但这是目前最有效的手段。我一般会把这套用例维护成一个表格每个用例标注期望命中的关键点改完之后直接对照。第三层是线上指标监控。至少要埋这几个点每次调用的耗时、输入输出 token 数、失败率和失败原因分类、工具调用轮次分布、检索命中条数。这里面最容易被忽视的是 token 消耗——它在悄悄地花你的钱。我见过因为多轮会话历史没做裁剪单次请求的输入 token 从 800 涨到 15000 的情况成本直接翻了十几倍。所以会话历史一定要做窗口化裁剪或者做摘要压缩。测试工具上我习惯用 WireMock 把模型接口 mock 掉保证单元测试不依赖真实网络和额度集成测试再用 Testcontainers 起一个真实的向量库容器。这样 CI 里跑得又快又稳。5. 常见问题与排查速查表5.1 依赖冲突与版本不匹配这是最高频的问题没有之一。表现是编译正常、启动报错错误信息往往指向某个自动配置类找不到或者方法签名不匹配。排查顺序我固定是这个先跑mvn dependency:tree看依赖树重点找同一个 groupId 下的多个版本再看 AI 框架的 BOM 有没有真正生效如果某个 starter 自己写死了版本号BOM 是压不住的最后确认 Spring Boot 大版本和 AI 框架的要求是否匹配。现象大概率原因处理方式启动报 NoSuchMethodError依赖树里有多个版本用 dependency:tree 定位后用 exclusion 排掉旧版本自动配置类不生效starter 名字写错或版本不对检查 starter 坐标确认 BOM 已导入流式接口一直挂起无返回WebFlux 和 MVC 依赖混用二选一或改用 SseEmitter本地能跑容器里连不上模型网络出口或环境变量未注入检查容器网络和密钥注入方式5.2 超时、限流、重试与成本控制模型接口的稳定性天然不如你自己的内部服务所以防护要做得更厚。超时上连接超时和读取超时要分开设。连接超时给 3 到 5 秒就够读取超时要看你最长的生成内容一般 30 到 60 秒长文本场景给到 120 秒。给一个统一的 60 秒读取超时在生成长文档时会被打断。限流上除了保护下游更要保护自己。按场景设置并发上限超出的请求直接快速失败或者排队不要无限堆积。重试上记住三条指数退避、只重试可恢复的错误、设置重试上限。还有一个常被忽略的——重试要幂等。如果工具调用会改数据重试前必须确认操作是幂等的否则一次失败重试可能写两条记录。成本控制上我总结了三招最有效的会话历史做窗口摘要不要无限追加检索召回条数别贪多5 条往往比 15 条效果好高频且答案稳定的问题加缓存缓存命中直接返回一个字都不用花。5.3 关于八股文和面试的正确用法聊到 Java 转 AI绕不开面试这个现实问题。我的看法可能有点不一样八股文值得背但要背对方向。纯粹背诵式的准备在 AI 应用岗的面试里效果有限因为面试官更关心你怎么解决真实问题。他问你线程等待怎么实现不是想听你背CountDownLatch的定义而是想知道你在哪种场景下会选它而不是CompletableFuture超时了怎么办异常怎么传。他问你动态代理是想知道你会不会用它来给模型调用加统一的重试和埋点。所以我的准备建议是把每个知识点都往AI 应用场景上靠一层。你复习并发就顺带想一下并发调用多个模型接口怎么设计你复习集合就想一下大模型的会话历史用什么结构存、怎么裁剪你复习缓存就想一下 Prompt 结果缓存怎么做键。这样准备下来你会发现八股文和技术实践是一件事的两面不是割裂的。至于基础题和面试题这类资源把它当查漏工具而不是学习主线。系统学习还是得从一个能跑通的项目出发遇到不会的再回头补效率比从头啃一遍高得多。6. 我自己踩过的一些坑和几条实在建议先说几个具体的坑。第一个是在异步线程里丢上下文。用CompletableFuture或者虚拟线程之后ThreadLocal存的东西比如用户身份、链路追踪 ID在新线程里是拿不到的。我因为这个丢过一次完整的审计链路排查了很久。解决办法是用TransmittableThreadLocal或者在提交任务时手动把上下文捕获进去虚拟线程虽然默认支持继承作用域值但老代码里用ThreadLocal的地方还是要一个个改。第二个是把模型当数据库用。有段时间我把一些配置项让模型来判断结果同一份输入在不同时间给出不同结果排查了半天才发现是我的 Prompt 描述本身有歧义。凡是能用规则判断的绝对不要交给模型模型的确定性天然不如一段 if-else。第三个是忽略输入长度。上线前我用的是短问题测试一切正常上线后用户开始粘贴几千字的文档直接撞上下文上限接口报错。后来加了输入长度校验和自动摘要才稳下来。这个教训是永远假设用户的输入比你想象的更离谱。几条建议。第一先用最小的东西跑通闭环哪怕就是一个main方法里调一次模型、打印结果先建立我能控制它的感觉再往上加架构。第二把 Prompt 当成代码来管理写进配置文件或者独立文件里做版本控制改一次就记一次别散落在 Java 字符串里。第三遇到效果不达预期百分之七十的问题出在 Prompt 和检索质量上不是模型不够强先从这里查。第四别迷信框架的默认配置超时、重试、线程池这些参数等你真正压测过一次就会发现默认值基本都不合适。这个方向往后还能延展很多东西比如多模态输入处理、工作流引擎式的流程编排、把模型能力暴露成内部统一网关给多个业务方复用。但路径不太会变——底层是扎实的 Java 工程能力上层是大模型交互与编排的工程经验两头都得硬。我个人的感受是这件事没有想象中那么难入门但也没有那么容易做好中间那段距离基本靠一个个真实项目填出来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询