资讯动态

Spring Boot接入大模型:构建HR政策问答机器人实战

发布时间:2026/10/14 8:51:38 来源:尧图企业网站定制
先聊点实际的。我最近在做一个面向公司内部的 HR 政策问答机器人起因很简单HR 团队每天要回复几十条“年假剩几天”“报销发票怎么贴”“考勤怎么补卡”这类重复问题回答得多了既耗时又容易口径不一。所以我想用 Spring Boot 把大模型的对话接口接进来做一个能基于公司政策回答问题的机器人。这一期是系列第十二篇的第二部分核心目标很明确不搞花活先跑通第一个 AI 对话接口让机器人能正常回答员工提问再谈后续优化。这篇文章适合谁如果你已经会 Spring Boot 的基本用法但还没碰过大模型接口那正好。我会带你从项目拆解、环境配置、核心代码到常见坑位完整走一遍。整个项目我起名叫“HR 政策问答机器人”用的是标准的大模型对话接口兼容大多数厂商的 HTTP 接口格式不需要额外引入复杂框架只要一个 Spring Boot 工程和一点点 JSON 解析的耐心就够了。1. 项目拆解HR 政策问答机器人到底要做什么1.1 核心需求与场景分析先别急着写代码我们得把需求聊透。这个机器人不是简单接个大模型就完事它要解决的真实问题是员工在钉钉、企业微信或者网页端问一句“产假可以休多少天”机器人能立刻给出一段符合公司制度的准确回答。如果员工追问“需要提前多久申请”机器人还要能接着上下文回答而不是每次都当成新问题。从实际场景看HR 政策问答往往有几个特点问题重复率高、答案有明确依据、错误回答的代价大。所以机器人必须被约束在一个“政策范围”内不能让它自由发挥。这就引出了两个关键技术点一是系统提示词System Prompt要给足约束二是对话接口要支持多轮上下文。这一期我们先实现最基础的单轮问答但会在代码结构里预留多轮扩展能力。另外还要考虑接入方式。HR 机器人最常见的入口是聊天工具或内部系统但不管入口是什么后端核心都是同一个能力接收用户消息调用大模型生成回复再返回给前端。所以我把项目定位为一个独立的 REST 接口服务后续不管接到哪个平台只需要加一个适配层即可。1.2 技术选型为什么用 Spring Boot 直接接大模型接口市面上有很多 AI 应用开发框架比如 LangChain、Semantic Kernel 等它们确实能帮你简化调用和组装但对一个内部问答机器人来说直接用 Spring Boot 去调大模型 HTTP 接口反而更灵活、更好维护。原因很简单大模型本质就是一个 HTTP 服务你给它 JSON它还你 JSON。Spring Boot 最擅长的就是处理这类需求而且团队现有技术栈基本都是 Java用 Spring Boot 接入意味着后续维护成本最低。框架不框架不重要关键是先跑通链路等真的需要复杂编排时再引入专门工具也不迟。我选择用RestTemplate来做 HTTP 调用而不是WebClient。因为当前场景是同步的、单次请求RestTemplate 简单直接调试起来也直观。后面如果要做流式输出或者高并发再切 WebClient 也来得及。技术选型的原则是能简单就不要复杂能跑通就先进度优先。1.3 整体架构设计这条链路由三部分组成入口层、业务层、外部接口层。入口层就是一个 Spring MVC Controller接收 POST 请求参数是用户发送的消息。业务层是一个 Service负责构造请求体、调用大模型、解析响应同时把系统提示词拼进去。外部接口层就是大模型 API 本身你可以把它理解成一个黑盒输入一个消息列表输出一个回复。这个架构的好处是边界清晰。Controller 不关心大模型是怎么调的Service 不关心 HTTP 细节后续如果要换模型厂商只需要修改 Service 里的接口地址和参数映射不影响上层逻辑。我把这个架构画成一个箭头用户请求 - Controller - Service - RestTemplate - 大模型API - 响应原路返回就这么简单。同时我在项目里加了合理的分层Controller 和 DTO 放在controller包Service 放在service包HTTP 客户端相关放在client包。虽然项目不大但分好包以后后面加记忆功能、加知识库都会很顺手。2. 环境准备与基础配置2.1 创建 Spring Boot 项目并引入依赖我用的是 Spring Initializr 生成的基础工程Java 17 版本依赖只勾了 Spring Web、Lombok 和 Validation。如果你不想用 Lombok 也行但用了以后实体类会干净很多。实际用到的依赖很简单核心就一个 Web 启动器。pom.xml里关键内容如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这个配置在大多数 Spring Boot 3.x 项目里都能直接用。如果你的环境还是 Spring Boot 2.x也只需要把版本号改一下代码几乎不用动。创建好项目之后把目录结构保持清爽我习惯把配置类放在config包后续加配置会比较方便。2.2 配置大模型 API 参数地址、密钥、模型名调用任何大模型接口都需要三个核心参数接口地址URL、API Key、模型名称。这三个参数不能硬编码在代码里我习惯放在application.yml中并且通过环境变量覆盖避免把密钥提交到代码仓库。我这里的配置如下llm: api-url: ${LLM_API_URL:https://api.example.com/v1/chat/completions} api-key: ${LLM_API_KEY:sk-your-key-here} model: ${LLM_MODEL:llm-v1} max-tokens: 500 temperature: 0.3注意api-url我写的是示例地址你需要替换成你实际使用的大模型服务商提供的地址。现在主流的大模型接口大多兼容 Chat Completion 格式请求体中包含model、messages、max_tokens、temperature等字段响应中有一个choices数组里面是生成的回复内容。关于temperature这是一个影响随机性的参数范围通常 0 到 1。HR 政策问答需要准确和稳定所以我把它设为 0.3甚至 0 都不过分。为什么不用更高的值因为政策回答不像写诗不需要创造性越稳定越好。我见过有人把 temperature 设成 0.9结果同一个问题每次答案都不一样员工来了两次发现回答的口径对不上这就出问题了。2.3 定义请求与响应模型大模型的 HTTP 接口需要你发送特定结构的 JSON。拿最常见的格式举例请求体是这样{ model: llm-v1, messages: [ {role: system, content: 你是HR政策助手...}, {role: user, content: 年假还剩几天} ], max_tokens: 500, temperature: 0.3 }响应体大致是{ choices: [ { message: { role: assistant, content: 您好根据公司政策年假剩余天数需要联系HR查询... } } ] }所以我在 Java 里定义了几个 DTOChatRequest包含 model、messages、maxTokens、temperatureChatMessage包含 role、contentChatResponse包含一个 choices 列表每个 choice 里有一个 message。用record来定义最简洁但考虑到前后端交互我用了 Lombok 的Data这样序列化和反序列化都省心。定义完模型后还要在配置类里把RestTemplate注入 Spring 容器并设置合理的超时时间。我设置连接超时 5 秒读取超时 60 秒因为大模型生成回复通常比较慢尤其是长回答。如果读取超时太短容易误判为失败太长用户会等得暴躁。60 秒是常见值实际可以根据模型速度调整。3. 核心代码实现从 Controller 到 Service 到 HTTP 调用3.1 Controller 层设计暴露一个简洁的 POST 接口Controller 是整个服务的入口。我设计了一个接口POST /api/chat入参是ChatRequestDTO里面有一个message字段表示用户发来的文本。返回值是一个统一结构包含机器人回复内容和一个会话标识虽然本期用不到但先留好。ChatController的代码很薄RestController RequestMapping(/api) public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService chatService; } PostMapping(/chat) public ResponseEntityChatResponseDTO chat(RequestBody Valid ChatRequestDTO request) { String reply chatService.getReply(request.getMessage()); ChatResponseDTO response new ChatResponseDTO(); response.setReply(reply); return ResponseEntity.ok(response); } }这里我用了构造函数注入。如果你也喜欢用Autowired当然可以但我个人更推荐构造器注入测试的时候方便也符合 Spring 官方建议。ChatRequestDTO里我还加了一个NotBlank校验防止用户传空消息让服务端白白浪费一次模型调用。这算是一个小细节空请求没意义还费钱。3.2 Service 层组装系统提示词并调用接口Service 是整个逻辑的核心。在这里我把系统提示词、用户消息组装成 messages然后调大模型接口。下面这段代码是简化版但完整展示了思路Service public class ChatService { private static final String SYSTEM_PROMPT 你是一个专业的HR政策问答助手。你的职责是 1. 只根据公司提供的政策内容回答员工问题 2. 如果问题不在政策范围内或者你不确定请回复“我无法回答建议联系HR部门” 3. 回答要简洁、准确不要编造政策 4. 涉及年假、病假、产假、报销、考勤等制度时必须使用公司标准表述。 ; private final RestTemplate restTemplate; private final LlmProperties llmProperties; private final ObjectMapper objectMapper; public ChatService(RestTemplate restTemplate, LlmProperties llmProperties, ObjectMapper objectMapper) { this.restTemplate restTemplate; this.llmProperties llmProperties; this.objectMapper objectMapper; } public String getReply(String userMessage) { ListChatMessage messages new ArrayList(); messages.add(new ChatMessage(system, SYSTEM_PROMPT)); messages.add(new ChatMessage(user, userMessage)); ChatRequest request new ChatRequest(); request.setModel(llmProperties.getModel()); request.setMessages(messages); request.setMaxTokens(llmProperties.getMaxTokens()); request.setTemperature(llmProperties.getTemperature()); String url llmProperties.getApiUrl(); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(llmProperties.getApiKey()); HttpEntityChatRequest entity new HttpEntity(request, headers); ResponseEntityChatResponse responseEntity restTemplate.postForEntity(url, entity, ChatResponse.class); ChatResponse response responseEntity.getBody(); if (response null || response.getChoices() null || response.getChoices().isEmpty()) { throw new RuntimeException(大模型接口返回为空); } return response.getChoices().get(0).getMessage().getContent(); } }注意两个细节第一setBearerAuth是设置 Authorization 头的标准方法大多数大模型接口都接受这种认证方式。如果你对接的是别的认证方式比如api-key放在请求头里那就要改成headers.set(api-key, xxx)。第二ObjectMapper在当前代码里没用到但如果你需要对响应做一些额外处理或者调试时打印报文它会很有用。3.3 HTTP 调用层RestTemplate 的配置与错误处理RestTemplate 默认是没有任何超时设置的这是一个容易踩的坑。如果大模型服务挂了你的接口可能会一直挂起直到客户端超时。所以必须在配置里显式设置超时。我在RestTemplateConfig中做了如下配置Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(60000); return new RestTemplate(factory); }这里使用SimpleClientHttpRequestFactory简单直观。如果你需要连接池管理可以用HttpComponentsClientHttpRequestFactory引入 Apache HttpClient 依赖。但就目前这个项目规模没必要。错误处理是重点。如果调用大模型接口时返回 401认证失败、429限流、500服务内部错误直接用postForEntity会抛出异常但异常信息通常不够友好。我建议在 Service 里至少处理三类异常认证异常检查 API Key 是否正确是否过期。超时异常检查网络是否通模型响应是否太慢。响应结构异常检查映射是否匹配尤其注意 JSON 字段名大小写。为了便于排查我在代码里打印了请求和响应的关键信息但注意不要打印完整 API Key。这一点在后面调试时帮了大忙。3.4 系统提示词设计让 AI 不“胡说八道”这一步是整个机器人能否可用的关键。大模型本身就像一个博览群书但不太懂规矩的实习生你如果不给它划边界它会一本正经地编造公司政策。所以系统提示词必须明确以下几点第一角色定义。告诉它“你是 HR 政策助手”这能让它调用相应的知识范式。第二知识边界。明确说“只能根据公司提供的政策内容回答”凡是不确定的统一回复“无法回答建议联系 HR”。第三回答风格。HR 问答需要礼貌、专业、简洁。第四敏感信息处理。提示它不要询问或收集员工身份证号、银行卡密码等隐私信息。我的参考提示词已经写在代码里。实际项目中你还可以把这些内容放到数据库中做到动态配置这样 HR 团队可以自己更新政策而不用改代码。这是个很好的扩展方向但本期不展开。这里要特别提醒提示词不是万能的。如果模型本身知识不足它会试图用常识回答所以未来必须引入知识库RAG才能真正做到“基于政策回答”。这一期先靠提示词强约束算是一个可以接受的 MVP。4. 跑通全流程与接口调试4.1 本地启动并调用接口所有代码写完以后启动 Spring Boot 应用然后用curl或 Postman 测试。我是用 curl 测试的命令如下curl -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {message: 我想问一下今年年假还有几天}如果一切正常你会收到类似这样的 JSON{ reply: 您好您今年的年假剩余天数取决于您的入职日期和公司考勤记录建议您通过HR系统查询或联系HR部门获取准确数据。 }第一次跑通这个流程时我整个人是兴奋的但实际调了几个问题后就发现问题了机器人的回答太泛不够具体。后来我把公司政策文本嵌入到提示词里效果稍微好一些但距离“能用”还很远。所以我建议你第一次跑通后先不要急着追求完美先确认链路是通的再逐步优化回答质量。4.2 常见问题排查403、超时、乱码、幻觉我在调试过程中遇到了不少问题挑几个典型的说一说。第一个是 403 或 401。原因十有八九是 API Key 没写对或者请求头没有正确携带。检查方式是在代码里打印Authorization头的前几位和后几位确认没有拼接错误。如果你用的环境变量还要确认环境变量真的被加载了不要直接在 IDEA 里改代码里的默认值。第二个是读取超时。大模型接口在某些时候响应很慢尤其生成内容较长时。如果读取超时设置过短比如 10 秒接口就会频繁报超时。我最终把它调到 60 秒才算稳定。如果你要提供服务给员工用建议前端配合 loading 提示让用户知道系统正在生成回答。第三个是中文乱码。这个问题多发生在 String 里手动构造 JSON 时没有指定 UTF-8 编码。用 RestTemplate 传递HttpEntity的时候要确保Content-Type是application/json;charsetUTF-8。如果你用 ObjectMapper 序列化对象一般不会有乱码问题但如果你从文件读取提示词一定要确认文件编码是 UTF-8。第四个也是最重要的模型幻觉。有一次我测试“离职时未休年假怎么折算”模型给出了一段看起来很合理的政策但我查了公司制度发现完全是编的。这就是没有知识库的典型表现。解决办法除了强化提示词至少要在提示词里写“如果政策内容未提供必须拒绝回答”。我在这上面栽过跟头所以格外强调。4.3 实测效果与优化方向跑通之后我拿几个真实问题做了测试比如“病假需要什么证明”“报销发票丢失了怎么办”“新员工入职多久可以享受年假”。在提示词里加入了简单的政策要点后回答准确率明显提高。但我也发现问题一旦涉及具体数字比如“年假按入职满一年算还是满一年后的下一年起算”模型就容易混淆。所以我给这个项目的下一步优化列了三个方向第一引入长期记忆。现在每次请求都是独立的员工问“我想请假”之后再问“需要审批吗”模型是不知道“请假”这个上下文的。后面要在 Service 层维护一个会话消息列表把每个用户的对话历史存起来再传给模型。第二建立公司政策知识库。用向量数据库存储政策条文提问时先检索相关片段再拼进提示词。这是真正解决幻觉的路径也是这个机器人的最终形态。第三增加人工转接机制。当模型判断回答不了或者员工主动要求“转人工”接口需要返回一个特殊的标识让前端展示转人工按钮。这个对内部服务来说很关键毕竟再好的 AI 也有搞不定的时候。5. 常见问题与避坑指南5.1 问题排查速查表把最常遇到的问题整理成了一张表格方便你快速定位现象可能原因排查与解决方法返回 401/403API Key 错误或未携带打印请求头检查环境变量返回 429超出模型限流增加重试机制降低并发请求数读取超时模型生成慢或网络慢调大读取超时到 60 秒以上响应内容为空模型返回结构异常打印完整响应 JSON检查字段映射回答内容与政策不符模型幻觉将政策文本加入提示词或引入知识库中文乱码编码问题统一使用 UTF-8避免手动拼接 JSON这张表可以贴在你的项目文档里后面团队接手时能省不少事。5.2 安全与合规注意事项内部机器人最容易忽略的是安全问题但恰恰最不能忽略。API Key 属于敏感信息在代码里写死是绝对禁止的。我采用环境变量注入的方式代码仓库只保留占位符。同时调用日志里不能打印完整的 API Key也不能打印员工输入中的敏感信息比如身份证号、银行卡号。另外大模型问答服务的回复内容需要具备可追溯性。我建议在接口返回结构中增加一个requestId每次调用都生成唯一标识方便问题回溯。在正式上线前最好让 HR 团队对常见问题的回答进行一轮审核确认没有原则性错误。5.3 一点真心话跑通第一个 AI 对话接口并不难难的是让机器人在真实业务中真正可用。我这边的体会是先让链路通再让答案准最后做好兜底。不要一开始就追求什么都自动回答宁可让它多问一句“需要我转接 HR 吗”也比给员工一个错误答案强。如果你也在做类似的东西我建议你从今天下午就开始动手用最快的方式把接口调通。不要迷信复杂框架先把 Spring Boot 的工程结构和大模型 API 的调用关系吃透。等基础跑通了再慢慢加知识库、加记忆、加人工兜底那都是水到渠成的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑