1. 项目定位:为什么是 Spring Boot + AI 对话接口
1.1 HR 政策问答机器人的核心需求拆解
这个项目叫“AI全栈12-02”,名字里已经透露出完整的信息:它属于一条 AI 全栈学习路线的中期实践,前一步多半是把模型 API 跑通,从这一篇开始进入真正的业务容器——Spring Boot。目标不是写一个玩具 Demo,而是做一个 HR 政策问答机器人,能够接收员工的自然语言提问,并返回关于考勤、请假、报销、社保、公积金等政策的准确回答。
需求看起来简单,但如果拆开看,背后藏着几个必须解决的痛点。第一,HR 政策文本通常以 PDF、Word、企业内网公告的形式存在,员工想知道“产假到底能休多少天”“年假未休完怎么折算”,最快的途径往往是问 HR,但 HR 的重复工作量极大。第二,政策条文有很强的时效性和边界条件,比如“累计工作满一年”不是“入职满一年”,如果模型拿不到准确的上下文,极易出现幻觉式回答。第三,企业内部部署场景中,数据不能直接丢给公网大模型,接口层面就需要考虑权限、审计和合规问题。
所以这个机器人最终的形态,不是简单调一下外部 API,而是一个带有业务上下文的问答服务:用户提出问题,服务端从政策知识库中检索相关信息,再把这些信息作为参考材料连同问题一起交给大模型,由大模型生成最终答案。后面这套机制在业界有个常用说法叫 RAG,也就是检索增强生成。在这一课里,先把 Spring Boot 工程搭起来,跑通第一个对话接口,同时把知识库和检索模块留出接口位,方便后续迭代。
1.2 技术选型的三个关键理由
为什么选 Spring Boot,而不是 Node.js、Python FastAPI 或者干脆用一个云函数?我自己的判断主要有三点。
第一,企业内部系统的主流技术栈依然是 Java。HR 政策问答机器人通常要接入企业微信、钉钉、OA 门户或者 HR 系统,这些系统的后端接口大多是 Java 生态。Spring Boot 能让你把 AI 能力直接嵌入现有微服务体系,不需要额外维护一套异构服务,身份认证、配置中心、监控链路都能直接复用。
第二,Spring Boot 对 AI 客户端封装的支持越来越成熟。Spring AI 项目已经把主流大模型 API 统一封装成类似 Spring 风格的模板类,比如 ChatClient、ChatModel 这些接口。你不需要自己手写 HTTP 调用、JSON 解析、错误重试,只用配置 base-url 和 api-key 就能像操作 JdbcTemplate 一样操作大模型。这对团队协作和代码维护来说,优势非常明显。
第三,Java 的类型系统和工程化能力更适合复杂业务。问答机器人一旦接入知识库、做多轮对话、做日志审计,涉及的数据结构和状态管理会变得复杂。Java 的强类型、Spring 的依赖注入和 AOP,能帮你把这些逻辑组织得井井有条。说白了,用 Python 写个原型很爽,但要做成生产级的内部服务,Spring Boot 会更稳。
2. 环境准备与依赖接入
2.1 基础工程骨架搭建
在动手写代码之前,先把环境准备好。我用的版本组合是:JDK 17 + Spring Boot 3.2.x + Maven。如果你还在用 JDK 8 或者 Spring Boot 2.x,建议直接升级,Spring AI 的很多特性依赖 Jakarta EE 规范和 Java 17 的语法支持。
创建工程有两种方式。一种是去 Spring Initializr 网站生成,另一种是在 IDEA 里直接 New Project。我推荐后者,因为可以选择 Maven 镜像和本地仓库路径,省去后续配置的麻烦。关键依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>这里有一个容易踩坑的地方。Spring AI 的版本迭代很快,不同版本之间的 API 改动不小。我最早用的是 0.8.1 版本,里面的 ChatClient 写法是ChatClient.create(...),到了 1.0.0-M6 就变成了ChatClient.builder(...)。所以如果你是照着网上的教程做,一定先确认版本一致,否则编译报错会让你怀疑人生。我的建议是直接锁定一个版本号,比如 1.0.0-M6,后续所有代码都以这个版本为准。
工程骨架建好之后,先写一个简单的 HealthController 确认 Spring Boot 能跑起来,再去做 AI 接入。这一步虽然看起来多余,但能帮你把“Spring 启动失败”和“AI 配置错误”这两类问题隔离开,排错时不用两头猜。
2.2 大模型 API 客户端的接入方式
Spring AI 支持很多大模型厂商,OpenAI、通义千问、文心一言、智谱等等。接入方式大同小异,核心就是配置 base-url、api-key、model 名称这三个要素。
我这次用的是国产大模型的兼容接口,地址指向其兼容 OpenAI 格式的端点。这么做有一个明显的好处:代码层面完全复用 Spring AI 的 OpenAI 客户端,只需要把 base-url 改掉。Spring AI 的配置项如下:
spring: ai: openai: base-url: https://your-model-endpoint.example.com/v1 api-key: ${LLM_API_KEY} chat: options: model: your-model-name temperature: 0.2temperature这个参数值得多说两句。它的取值范围一般是 0 到 2,数值越低,输出越确定、越保守,适合问答机器人这种需要稳定性的场景。我做 HR 政策问答时直接设成了 0.2,因为政策回答不需要创造性,需要的是准确和一致。如果你做的是文案生成或者头脑风暴类产品,再调高到 0.7 以上也不迟。
API Key 不要写死在 application.yml 里。这一点我强调过很多次,但还是有人会把密钥提交到 Git 仓库。正确做法是使用环境变量,比如上面配置里的${LLM_API_KEY},在本地用 IDEA 的 Environment variables 配置,在服务器上写到部署环境的系统变量或者密钥管理服务里。
3. 核心实现:从配置到对话闭环
3.1 提示词模板的构造思路
对话接口跑通的关键,不是代码有多复杂,而是提示词模板的设计。HR 政策问答机器人跟通用聊天机器人最大的区别在于,它必须限定回答范围,不能什么都说,也不能编造不存在的政策条款。
我设计的提示词模板包含四个要素:角色设定、回答原则、知识上下文、用户问题。模板文件放在src/main/resources/prompts/hr-policy.st里:
你是一名企业 HR 政策智能助手,负责回答员工关于考勤、请假、薪酬、福利、报销等制度的问题。 回答时必须遵守以下规则: 1. 只根据参考资料中的内容回答,不要凭常识推测。 2. 如果参考资料中没有答案,明确回复“该问题暂未收录相关政策信息,请联系 HR 部门确认”。 3. 回答时使用简洁、清晰的口语化表达,避免列大段法律条文。 4. 若问题涉及多个条款,分点说明并标注出处。 参考资料: {context} 员工问题: {question}这个模板里有三个变量需要动态填充:{context}是检索到的政策片段,{question}是用户输入。Spring AI 的 PromptTemplate 可以直接渲染这个模板:
PromptTemplate promptTemplate = new PromptTemplate(templateContent); Prompt prompt = promptTemplate.create( Map.of("context", context, "question", question) );为什么要把“参考资料”放进提示词而不是直接让模型凭记忆回答?这里涉及大模型的一个核心特性:模型的知识截止时间有限,而且回答问题时会基于训练数据里的统计规律“猜测”答案。HR 政策这种高频更新的内容,模型很可能给出过时甚至完全虚构的信息。把政策文本作为上下文喂给模型,等于给它划定了回答的边界,能把幻觉率压到很低的水平。
3.2 服务层的完整代码实现
服务层是整个机器人的大脑,职责是把控制器传来的问题转换成请求,调用大模型,再把结果返回。我先把核心代码贴出来,再逐段解释。
@Service @RequiredArgsConstructor public class HrPolicyChatService { private final ChatClient chatClient; public String answer(String question) { String context = retrievePolicyContext(question); String answer = chatClient.prompt() .system(systemSpec -> systemSpec.text(loadSystemPrompt())) .user(userSpec -> userSpec.text( "问题:" + question + "\n\n参考资料:" + context )) .call() .content(); return answer; } private String retrievePolicyContext(String question) { // 第一阶段先用固定政策文本,后续替换为向量检索 return policyRepository.loadAllAsText(); } }这段代码有几个细节需要展开讲。注意我用了ChatClient的链式调用风格,system()方法传入系统提示词,user()方法传入用户问题和参考资料,最后call()触发完整对话。这种写法比手动构造 Prompt 要清晰,也更容易扩展多轮对话。
retrievePolicyContext方法目前只是简单地把所有政策文本加载出来拼成字符串,因为这一课的目的是先跑通接口,还没有引入向量检索。但方法名已经暗示了后续演进方向:知识库检索。实际做的时候,等你把接口跑通了,回来替换这个方法,接入向量数据库,机器人就从“背靠整本文档回答问题”升级成“精准检索片段回答问题”,效果会有质的飞跃。
3.3 控制器层与参数校验
控制器层承担两个职责:接收 HTTP 请求,以及做基本参数校验。我给了两个端点,一个用于单次问答,一个用于健康检查。
@RestController @RequestMapping("/api/hr-bot") public class HrPolicyChatController { private final HrPolicyChatService hrPolicyChatService; public HrPolicyChatController(HrPolicyChatService hrPolicyChatService) { this.hrPolicyChatService = hrPolicyChatService; } @PostMapping("/chat") public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) { if (request.question() == null || request.question().trim().isEmpty()) { return ResponseEntity.badRequest() .body(new ChatResponse("问题不能为空")); } String answer = hrPolicyChatService.answer(request.question()); return ResponseEntity.ok(new ChatResponse(answer)); } @GetMapping("/health") public Map<String, String> health() { return Map.of("status", "UP"); } }我用了 Java 16 的 record 来定义请求和响应结构,代码更精简。ChatRequest只有一个字段question,ChatResponse只有一个字段answer。别小看这种写法,项目大了之后,DTO 数量会爆炸,用 record 能少写大量样板代码。
校验放在 Controller 层看起来有点简陋,但对于一个教学项目足够用了。生产环境你可以引入spring-boot-starter-validation,用@NotBlank注解配合@Valid完成声明式校验。我是故意先用手动校验,让你看清逻辑,再考虑框架封装,不然出了错你都不知道该查哪儿。
3.4 配置类的补充说明
Spring AI 的自动配置帮我们省了很多事,但有时候需要手动调整底层参数。比如设置超时时间,防止模型接口卡住导致 HTTP 请求悬挂:
@Configuration public class AiClientConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem("你是一名严谨的 HR 政策助手") .defaultOptions( OpenAiChatOptions.builder() .withTemperature(0.2) .withMaxTokens(500) .build() ) .build(); } }超时时间的设置我推荐在环境变量层面处理。Spring AI 底层使用的是 RestClient,你可以在 application.yml 中加spring.ai.openai.chat.timeout配置项。这里有个我踩过的坑:默认超时时间偏长,如果模型接口不稳定,一次请求可能挂 60 秒甚至更久,用户体验极差。我把超时时间设成 30 秒,并且在上游加了接口限流和缓存,效果好了很多。
4. 测试、调优与常见问题排查
4.1 本地跑通后的首批测试用例
接口写完,启动 Spring Boot,用 curl 或者 IDEA 自带的 HTTP Client 发一个请求试试:
curl -X POST http://localhost:8080/api/hr-bot/chat \ -H "Content-Type: application/json" \ -d '{"question": "年假未休完可以折算工资吗?"}'我建议先准备五到六个覆盖不同场景的测试问题,不要只测一个就宣布成功。我第一批测试问题包括:
- 正常边界类:年假未休完可以折算工资吗?
- 模糊问题类:请假怎么弄?
- 知识库外问题:公司有没有宠物津贴?
- 多条件组合类:入职半年申请婚假,能休几天?
- 敏感话题类:最近在裁员吗?
这五个问题的设计逻辑是:第一个验证主流程,第二个验证意图识别和追问能力,第三个验证“不知道”的兜底能力,第四个验证多条件推理,第五个验证敏感内容处理。如果这五类都能给出合理回答,这个机器人就基本具备上线能力了。
实测下来,第一个问题模型回答得不错,按照政策条款解释了未休年假按日工资 300% 折算。第二个问题模型选择了追问“请问您想问哪类请假”而不是直接猜,这归功于系统提示词里的规则。第三个问题模型正确返回了“未收录”提示。第四个问题回答出现了一个小瑕疵,对“入职半年”和“婚假”的关联分析不够完整——这也是后续需要引入知识库片段检索的原因。第五个问题模型拒绝回答并引导联系 HR 部门,逻辑没毛病。
4.2 常见报错与解决方案速查表
跑 AI 接口最容易遇到的几个报错,我整理成一个速查表,都是我亲手踩过的坑。
| 报错信息 | 原因分析 | 解决方案 |
|---|---|---|
Invalid API key | API Key 配置错误或环境变量未加载 | 检查 IDEA 环境变量配置,确认没有把$符号写进 yml |
Connection timed out | 模型接口地址不通或网络受限 | 先用 curl 直接测 base-url 是否可达,再检查代理设置 |
404 Not Found | 模型名称不存在或 endpoint 路径不对 | 登录模型服务商后台,确认模型标识符是否与代码一致 |
400 Bad Request | 请求体格式错误,context 过长 | 检查提示词模板变量是否全部填充,减少参考文本长度 |
JSON parse error | 返回内容中包含非法 JSON | 在调用content()前打印原始响应,检查模型输出格式 |
BeanCreationException | Spring AI 版本冲突或依赖缺失 | 确认spring-ai-openai-spring-boot-starter版本一致 |
这里面最隐蔽的问题是模型名称。很多兼容 OpenAI 格式的服务商,模型名称不是通用的gpt-3.5-turbo,而是他们自己的标识符,比如qwen-plus或者glm-4。我一开始没注意到,直接复制默认模型名,结果 404 报了很久。排查方法很简单:看服务商文档,或者用服务商提供的 Playground 发一个请求,抓包看实际调用的模型名。
另一个隐蔽问题是长文本截断。政策文档动辄几万字,全部塞进上下文,不仅消耗 token,还会让模型注意力分散。Spring AI 默认请求时会自动截断吗?并不会。你需要自己控制长度,我的经验是单次检索的上下文控制在 1500 字以内,超过的部分去掉或分块。这个优化在后续接向量检索时会自然解决,但在当前阶段你可以先手动限制retrievePolicyContext返回的长度。
4.3 响应延迟与并发的实战优化思路
QA 机器人跑通之后,你会立刻发现两个问题:响应慢、并发能力弱。模型接口的延迟通常是 2 到 5 秒,这还只是单次请求的耗时。如果在接口里做了多次模型调用(比如先做意图识别再生成答案),延迟会翻倍。
针对延迟,我做了三个层级的优化。
第一层是缓存。对于相同的用户问题,在短时间内直接返回缓存结果,不再调用模型。我用简单的ConcurrentHashMap加过期时间实现了一个极简缓存,避免为这个教学项目引入 Redis。实测下来,命中率大概 20% 到 30%,因为员工反复问的是那几个高频问题,比如“年假怎么算”“加班费标准”。
第二层是流式输出。把普通请求改成 SSE(Server-Sent Events),让模型一个字一个字地返回,用户看到“正在输入”的反馈,心理等待时间会大幅缩短。Spring AI 支持.stream()方法,Controller 层返回SseEmitter即可。这个改动不复杂,但体验提升非常明显。
第三层是异步化。如果业务允许,把请求丢进消息队列,AI 处理完再回调通知,用户不用干等。但这会引入额外的架构复杂度,这个阶段的项目不建议做,等用户量真正上来再考虑。
并发方面,Spring Boot 默认的内嵌 Tomcat 最大线程池是 200,但模型接口作为外部依赖,它的吞吐上限可能远低于 200。如果不对模型接口做保护,一旦突发流量涌入,很容易把账户限流甚至封禁。我的做法是在 AI 调用外层加一个简单的限流器,用RateLimiter限制每秒最多 5 个请求,剩下的请求进入队列等待或者直接返回繁忙提示。
4.4 日志链路与效果评估
排查问题和后续调优都不能靠猜,必须看日志。我把日志级别单独调出来:
logging: level: org.springframework.ai: DEBUG org.springframework.web.client.RestTemplate: DEBUG当日志级别设为 DEBUG 时,Spring AI 会自动打印出完整的请求内容和响应内容,方便你检查发送出去的提示词是否携带了正确的上下文,以及模型返回的原始 JSON 是否正确。生产环境下把级别调回 INFO,避免日志膨胀。
效果评估这个环节很多教程不会讲,但它决定了机器人能不能真正上线。我给每个问答对录入了结构化日志,包含问题、标准答案、模型答案、用户反馈四个字段。标准答案是从政策文档中人工抽出的名称,用户反馈是从前端收集的“有帮助/没帮助”按钮。每周汇总一次准确率和反馈率,低于 80% 的问题组就针对性调整提示词或索引文本。这种“数据驱动调优”的思路,比凭感觉改提示词靠谱得多。
5. 从单机接口到 AI 全栈的下一步
接口跑通只是整个 AI 全栈项目的第一步。从这一课往下走,还有三个方向需要继续深入。
第一个方向是知识库增强。现在的retrievePolicyContext还是全量加载文本,当政策文档变多、变长之后,全量塞进上下文既不经济也不高效。标准做法是引入向量数据库(如某开源向量库或云上的向量服务),把政策文档切块、向量化存储,查询时通过相似度检索只拉取最相关的 3 到 5 个片段。这个改动会让回答质量产生质变,模型不会再被无关信息干扰。
第二个方向是多轮对话记忆。目前的接口是无状态的,用户问一个问题就答一个问题,不会有对话上下文。但实际使用中,员工往往会追问“那试用期呢?”“如果我是实习生呢?”。这要求系统能把历史对话压缩成摘要或缓存成消息列表,在每次调用时一并传入模型。Spring AI 的ChatMemory模块专门干这个,接入成本不高。
第三个方向是管理后台与审计。企业内部 AI 服务一定要有完整的审计链路。请求日志、操作人、时间戳、回答内容都要可追溯。可以给接口加上基于 OAuth2 或企业内部 SSO 的鉴权,让每一条问答都能定位到具体用户。这一步不是锦上添花,而是合规要求。
以我个人的经验,从这一讲往后的每一步迭代,难度都不亚于把接口跑通,但价值也在成倍增长。特别是知识库增强这一步,做完之后你会明显感觉到,机器人从“什么都敢说”变成了“有理有据地说”,那种体验上的差异,用户一眼就能分辨出来。
最后分享一个小技巧:所有 AI 接口的参数,包括模型名称、温度、超时时间,都尽量做成可配置的,而不是硬编码在代码里。因为模型服务商的版本迭代很快,今天能用v1接口,明天可能就出了v2,把所有开关留在配置层,将来升级只需要改一行配置,不用改代码重新发布。这也是我在多个项目中验证过的实用经验。