news 2026/9/30 9:42:59

【AI全栈后端12-02】Spring Boot 跑通第一个 AI 对话接口:HR 政策问答机器人实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【AI全栈后端12-02】Spring Boot 跑通第一个 AI 对话接口:HR 政策问答机器人实战

本文是「Spring Boot + AI 全栈后端」系列第 02 篇。上一篇定下了"用 Spring Boot 接 AI"的基调,这一篇直接上手:用一个 HR 政策问答机器人的真实场景,把"第一个能上线的 AI 对话接口"从 0 打到能跑。示例基于 Spring AI 2.0 / Boot 4.1。

前阵子帮一家三十来人的公司做内训,HR 同学倒苦水:每天群里都有新人问"入职要带啥材料"“报销多久到账”“年假怎么算”,问题就那十几个,可她得一遍遍复制粘贴答。更麻烦的是,两个人答的口径还偶尔不一致。

这一篇就用这个真实场景,带你在 Spring Boot 里跑通第一个 AI 对话接口,把"重复解答"变成一次接口调用。

一、问题拆解:我们要解决什么

把 HR 的痛点摊开,目标其实很具体:

  1. 重复劳动:高频政策问题占掉 HR 大量时间;
  2. 口径不一:多人回答,标准难统一;
  3. 随时可问:员工希望 7×24 自助,而不是等工作时间。

对应的接口要满足三点:能对话、入参要校验、模型挂了不能把堆栈甩给用户。下面一步步来。

先把接口契约定下来

动手写代码前,V哥 建议先把这张表贴在工位上:

场景请求体响应体HTTP 状态
正常提问{"question":"年假怎么算"}{"answer":"……"}200
空问题 / 纯空格{"question":" "}{"error":"问题不能为空"}400
模型超时 / 限流{"question":"年假怎么算"}{"error":"模型暂未返回有效回答,请稍后重试"}503

别小看这张表。AI 接口最容易失控的地方不是模型,而是边界——问题为空时怎么办、模型没答出来时返回什么。先把状态码和文案定死,前端拿到的是永远能直接展示的东西,联调时才不会因为"你返回了个 500 我怎么渲染"吵半天。

二、最小依赖:复用 01 的结论

项目用 Spring Boot 4.1.1 + Spring AI 2.0.1,靠一个起步依赖把模型接进来:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

ChatModel和ChatClient,别搞混

这是新手最容易绕晕的一对概念,一句话说清:

  • ChatModel是模型本身:喂一个Prompt进去,还一个ChatResponse出来,一次调用、一进一出,跟厂商 SDK 一一对应;
  • ChatClient是它上面的一层流式 API:把"系统提示词 / 用户消息 / 生成参数 / 拦截器"这些散落的零件串成一条链,写起来是prompt().system().user().call()这种形状。

所以注入的时候拿ChatModel,用的时候包成ChatClient。前者由自动配置按厂商给你装配好,后者是你自己的编排层——换厂商动的是前者,动不到后者。

配置:一行密钥是不够的

spring:ai:openai:api-key:${OPENAI_API_KEY}base-url:https://api.openai.comchat:options:model:gpt-4o-minitemperature:0.2

三个细节值得说:

  • 密钥走环境变量,不进代码库。这是底线,一旦把明文 key 提交到 Git,扫描机器人几分钟就能扒走,账单比模型还快。
  • base-url是留给公司网关和国产模型的。接中转、接私有化部署、接国内大模型,改这一行,Java 代码一行不动——这正是上一篇说的"配置切换不动业务"。
  • model/temperature可以在配置里给默认值,业务代码里的ChatOptions再按需覆盖,改档位不用改代码。

三、把对话包成一个接口

先定义请求/响应两个简单的记录类型,再写控制器。注意这里把"业务"和"模型"分开:控制器只管收问题、回答案,真正的对话逻辑交给服务层。

publicrecordPolicyChatRequest(@NotBlank(message="问题不能为空")Stringquestion){}@RestController@RequestMapping("/api/policy")publicclassPolicyChatController{privatefinalPolicyChatServiceservice;publicPolicyChatController(PolicyChatServiceservice){this.service=service;}@PostMappingpublicPolicyChatResponsechat(@Valid@RequestBodyPolicyChatRequestrequest){returnnewPolicyChatResponse(service.answer(request.question()));}}

四、系统提示词:口径统一靠它,不靠模型

这是整篇最容易被跳过、却最值钱的一节。

回到开头那个痛点——两个人答的口径还偶尔不一致。很多团队的直觉是"换个更强的模型就好了",其实不对:模型再强,你没告诉它"你是谁、能答什么、答不出来怎么办",它就只能按通用常识自由发挥。HR 场景要的是每次都按公司政策答,不是每次都答得漂亮。

系统提示词就是干这个的:它不参与"这一轮问什么",而是长期挂在对话最前面,定角色、定边界、定格式。

privatestaticfinalStringSYSTEM_PROMPT=""" 你是公司 HR 政策助手,只回答与人事政策相关的问题。 回答时必须遵守以下规则: 1. 只依据公司已发布的政策作答,政策里没有的内容,直接回复"这条我查不到,请联系 HR 同事确认"; 2. 涉及天数、金额、流程的内容,必须给出明确数字和步骤,不用"大概""通常"这类模糊表述; 3. 用中文回答,控制在 200 字以内,分点列出。 """;publicStringanswer(Stringquestion){Stringtext=chatClient.prompt().system(SYSTEM_PROMPT).options(generationOptions()).user(question).call().content();if(text==null||text.isBlank()){thrownewAiServiceException("模型暂未返回有效回答,请稍后重试");}returntext;}

三条规则各有各的用处,V哥 逐个说:

  • 第 1 条是"不许编"。HR 问答最怕的就是模型自信地编出一条不存在的政策——员工拿去当依据,责任算谁的?明确告诉它"查不到就说我查不到",比事后审核便宜一百倍。
  • 第 2 条是"不许糊"。"通常三个工作日左右"这种话在 HR 场景里等于没说,逼它给数字。
  • 第 3 条是"不许长"。既是体验,也是成本——输出按 token 计费,200 字封顶,一次问答的成本就锁死了。

五、生成参数:两个值决定"稳"和"省"

ChatOptions里参数不少,但政策问答场景真正需要调的就是两个:

privatestaticChatOptions.Builder<?>generationOptions(){returnChatOptions.builder().temperature(0.2).maxTokens(500);}
参数作用政策问答怎么取值为什么
temperature控制发散程度,越高越"有创意"0.1 ~ 0.3同一句话答十次要长得一样,口径才叫统一
maxTokens单次输出上限300 ~ 800既是体验上限,也是单次成本上限

这里有个坑要提醒:ChatOptions.builder()拿到的是个可变的 Builder,别图省事存成static final常量让所有线程共用——ChatClient在编排过程中会读它、合并它,多线程下容易互相污染。写成方法、每次调用新建一个,是最省心的做法。

至于topP、frequencyPenalty这些,政策问答基本用不上,等你做营销文案生成再研究不迟。

六、入参校验:空问题直接拦在门外

@Valid @RequestBody配合@NotBlank,员工发了个空问题,框架直接返回 400,根本不会打到模型。这一步很关键——既省 token,也避免无意义调用。

七、异常兜底:模型挂了也不甩堆栈

模型偶尔超时或限流,不能把一堆异常抛给前端。用@RestControllerAdvice统一兜底:

@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(MethodArgumentNotValidException.class)publicResponseEntity<Map<String,String>>handleValidation(MethodArgumentNotValidExceptionex){Stringmsg=ex.getBindingResult().getFieldError()!=null?ex.getBindingResult().getFieldError().getDefaultMessage():"参数不合法";returnResponseEntity.badRequest().body(Map.of("error",msg));}@ExceptionHandler(AiServiceException.class)publicResponseEntity<Map<String,String>>handleAi(AiServiceExceptionex){returnResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(Map.of("error",ex.getMessage()));}}

参数错误 → 400,模型异常 → 503,前端拿到的永远是一段能直接展示的文案。这里 V哥 还有个习惯:给 AI 异常单独定义一个业务异常(上面的AiServiceException),不要直接把 Spring AI 抛的原生异常往外传——原生异常里可能带着你的base-url、请求头信息,等于把内网地址直接透给了调用方。

八、跑起来:一个配置 + 一行启动

启动后用 curl 验证:

curl-XPOST localhost:8080/api/policy\-H'Content-Type: application/json'\-d'{"question":"入职需要带哪些材料"}'

返回{"answer":"……"}就说明第一个 AI 对话接口跑通了。想接通义、Ollama?只改配置,业务代码一行不动。

九、上线前会踩的坑,先列在这

这一节是 V哥 陪学员联调时攒下来的,按出现频率排:

现象常见原因怎么查
启动报api-key must not be empty环境变量没注入(IDE 里最常忘)先看spring.ai.openai.api-key有没有解析成占位符原样输出
401 / 403key 无效,或base-url指向的网关要额外鉴权头用 curl 直接打一次网关,排除 Spring 侧问题
接口一直转圈模型侧慢,且没设超时给 HTTP 客户端配 connect/read timeout,别让线程池被拖死
返回空字符串被安全策略拦了,或maxTokens给太小先打印原始ChatResponse,再判断是不是输出被截断
中文变成问号请求/响应编码不一致(多见于自研网关)检查网关是否强制了 ISO-8859-1
本地好使,服务器上不通服务器出网受限先curl通一次目标域名,别急着改代码

十、落地要点与下篇

这个最小接口已经具备上线雏形:对话走服务层、口径靠系统提示词、入参有校验、异常有兜底、成本有上限。真要进生产,还差限流和缓存——那是第 11 篇的事。V哥 带学员做项目时立的规矩是:任何一个 AI 接口,没写入参校验和异常兜底,就不许往测试环境发版,否则线上一个空字符串就能让你查半天日志。

还有个问题这一篇故意没解决:现在的接口是一问一答、不记上下文,员工追问"那病假呢",模型不知道上一句问的是年假。这就是对话记忆(ChatMemory)的活,等我们把 03 到 06 的能力补齐,再回头收拾它。

下一篇(03)V哥 带你用多模型路由把智能客服的成本压下来:简单问题走小模型,复杂问题才上旗舰。


最后一句:跑通第一个 AI 对话接口不难——一个@RestController、一个ChatClient、再加"系统提示词 + 校验 + 兜底"三道防线,你就能把 HR 的重复解答变成一次干净的接口调用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 9:42:36

YOLO异常行为检测数据集:安防场景落地实战指南

1. 这不是普通数据集&#xff0c;是安防场景下“行为逻辑”可建模的硬核燃料你手上拿到的这9100张YOLO格式的异常行为检测数据集&#xff0c;本质上不是一堆带框图片的简单集合&#xff0c;而是一套经过真实安防逻辑淬炼的行为语义标注体系。我做过三年智能监控算法落地&#x…

作者头像 李华
网站建设 2026/9/30 9:42:34

基于CNN的港口防火图像识别系统:从YOLO选型到边缘部署实战

简介&#xff1a;这份PDF文档面向港口安防、智能监控与深度学习应用方向的研究者与工程技术人员&#xff0c;围绕港口火灾监测范围有限、识别速度偏慢等现实问题&#xff0c;提出以无人机采集图像、图传技术回传、卷积神经网络识别火灾信号的系统设计方案。文档完整呈现系统总体…

作者头像 李华
网站建设 2026/9/30 9:42:33

Linux虚拟CAN(vcan)实战:从内核原理到SocketCAN编程

1. 项目概述&#xff1a;为什么要在Linux上搞虚拟CAN&#xff1f;这可不是“玩具实验” 你手头没有物理CAN卡&#xff0c;但又得调试CAN通信逻辑、验证应用层协议栈、跑AUTOSAR测试用例&#xff0c;或者给车载ECU仿真环境搭个基础通信骨架——这时候&#xff0c;Linux内核自带的…

作者头像 李华
网站建设 2026/9/30 9:42:14

DeepSeek Harness入门:用Skill机制打造AI编程自动化工作流

提起DeepSeek Harness&#xff0c;很多人第一反应是&#xff1a;这不就是另一个调用DeepSeek接口的工具吗&#xff1f;跟直接在网页上对话有什么区别&#xff1f;我一开始也这么想&#xff0c;但真正动手装完、跑起来之后才发现&#xff0c;这个工具解决的其实是另一个层面的问…

作者头像 李华
网站建设 2026/9/30 9:41:09

AI内容工业化:把AI嵌入工作流实现高效变现

1. 这门课不是教你怎么“用AI”&#xff0c;而是帮你把AI变成能收钱的流水线我去年在杭州带一个本地生活类短视频团队&#xff0c;遇到个特别典型的场景&#xff1a;老板花三万块请了个“AI内容顾问”&#xff0c;结果三个月后剪辑组还在手动扒抖音热榜、文案组每天凌晨三点改脚…

作者头像 李华