恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
AI生成题目功能实现:用Java+Maven接入DeepSeek-V3的TaoToken实践
首页
资讯中心
/
AI生成题目功能实现:用Java+Maven接入DeepSeek-V3的TaoToken实践
AI生成题目功能实现:用Java+Maven接入DeepSeek-V3的TaoToken实践
发布时间:2026/10/3 16:17:34
1. Java 后端生成题目为什么绕不开统一 API 通道做在线刷题、面试题库、教辅系统的同学大概率都遇到过「AI 生成题目」这个需求给定方向和数量让模型吐出题干、选项、答案、解析再落库。听起来简单真动手时问题一堆。我最早是在一个 Spring Boot 的题库项目里做这件事最初直接对接某家云厂商的方舟 SDK代码能跑但后面越写越别扭。第一个坑是模型切换成本。项目早期用 DeepSeek-V3 出题后来想对比别的模型效果结果发现每换一家就要改依赖、改 baseUrl、改鉴权方式、改返回结构解析业务代码里到处是厂商痕迹。第二个坑是 Key 管理。测试环境、预发、生产各一套 Key散落在不同配置文件里谁改了什么根本说不清。第三个坑是错误处理不统一有的厂商超时抛SocketTimeoutException有的返回业务错误码排查一次要翻三份文档。后来我把调用层收敛到 TaoToken 这个统一 API 通道上情况好转很多。它的定位很直白对外提供一套 OpenAI 兼容的 HTTP 接口你用同一个 baseUrl、同一个 Key、同一套请求体结构就能调用包括 DeepSeek-V3 在内的多个模型。对 Java 后端来说这意味着我可以继续用熟悉的 OkHttp 或者官方 openai-java SDK只是把地址换掉业务层完全不用感知底层是哪家模型。这篇文章面向的是已经会用 Maven 管依赖、写过 Controller 和 Service 的 Java 开发者。如果你正在做题库、教育类、内容生成类项目想快速把「AI 生成题目」这条链路跑通从出题到解析再到落库那下面的步骤你可以直接照着做。我会给出 pom 依赖、SDK 初始化、题目生成接口的完整可复制配置再演示一次真实请求的返回结果最后把几个高频报错逐个拆开讲。核心检索词就三个DeepSeek-V3、Java、Maven围绕它们展开。需要先说明一点TaoToken 在这里扮演的是「统一入口」的角色不是让你绕过什么而是把多模型调用的复杂度收拢到一处。你仍然需要遵守各模型服务方的使用条款Key 也要妥善保管不要硬编码进 Git 仓库。2. TaoToken 前置准备Key、Base URL 与模型 ID 三件套在写第一行 Java 代码之前有三样东西必须先拿到手我习惯叫它们「三件套」Base URL、API Key、Model ID。缺任何一个后面都会卡住。Base URL 是请求的根地址。TaoToken 的 API 入口是https://taotoken.net/api注意这里不带任何查询参数就是干净的根路径。所有对话补全请求都会拼成https://taotoken.net/api/v1/chat/completions这样的形式。如果你之前用过 OpenAI 的 SDK会发现路径结构几乎一样这也是它兼容性的体现。API Key 需要你登录后在控制台创建。具体入口在https://taotoken.net/console/api-keys进去之后点新建系统会生成一串以sk-开头的密钥。这里有个细节要提醒Key 只在创建时完整显示一次关掉弹窗就再也看不到了所以务必当场复制到安全的地方。我一般会先存到本地的环境变量里而不是直接写进application.yml。比如在 macOS 或 Linux 的 shell 配置里加一行export TAOTOKEN_API_KEYsk-你的密钥Windows 则用系统环境变量界面设置。这样代码里通过System.getenv读取既避免了明文入库也方便不同环境切换。Model ID 是告诉服务端你要用哪个模型。调用 DeepSeek-V3 时模型标识按平台文档填写即可请求体里的model字段就填这个值。如果你不确定当前可用的模型列表可以到模型对话页面手动试一下或者查阅接入文档里的模型清单。文档地址是https://taotoken.net/doc里面有各模型的上下文长度、计费方式等说明。把这三样准备好之后建议先做一次最小验证不要急着写业务代码。最省事的办法是用 curl 打一发curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: deepseek-v3, messages: [ {role: system, content: 你是一名助手}, {role: user, content: 用一句话说明什么是 JVM} ] }如果返回的 JSON 里有choices[0].message.content说明三件套没问题可以进入 Java 环节了。如果返回 401八成是 Key 错了或者没带上Bearer前缀如果返回 404检查 baseUrl 是不是多写了或少写了/v1。这一步花两分钟能省掉后面半小时的瞎猜。另外提一句 Coding Plan 的事。如果你这个题库项目是长期迭代的每天要跑大量生成任务可以了解一下https://taotoken.net/coding-plan它针对持续编码和 Agent 场景做了额度优化比按次调用更划算。不过本文的重点还是先把单次调用跑通套餐的事后面再考虑。3. Maven 工程配置与 SDK 初始化可复制片段现在进入正题。假设你已经有一个标准的 Maven 工程pom.xml里 Java 版本至少是 8我建议用 11 或 17因为后面用到的 HTTP 客户端和 JSON 库在新版本上更省心。依赖方面我选择用 OkHttp 加 Jackson 的组合而不是引入某个厂商的专用 SDK。原因前面说过专用 SDK 会把你和特定平台绑死而 OkHttp 是通用的换任何兼容 OpenAI 协议的服务都不用改代码。在pom.xml的dependencies里加上这几项dependencies dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.12.0/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.32/version scopeprovided/scope /dependency /dependenciesOkHttp 负责发请求Jackson 负责序列化和反序列化 JSONLombok 用来少写 getter/setter。版本号你可以按项目实际情况调整但建议 OkHttp 不要低于 4.x因为 3.x 的 API 差异较大。接下来是配置文件。我习惯把可变参数抽到application.yml里通过 Spring 的Value注入。如果你不是 Spring 项目用 Properties 或者环境变量也行。配置内容如下taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model: deepseek-v3 timeout-seconds: 60注意api-key这里用了${TAOTOKEN_API_KEY}占位实际值从环境变量读这样配置文件可以放心提交到仓库。timeout-seconds设 60 秒是因为生成题目这种任务输出较长超时太短容易半路断掉。然后是 SDK 初始化。我写了一个TaoTokenClient类用Component交给 Spring 管理内部持有 OkHttpClient 单例。OkHttpClient 是线程安全的全局一个就够不要每次请求都 new否则连接池和线程池会爆。Component public class TaoTokenClient { private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; private final String apiKey; private final String model; public TaoTokenClient( Value(${taotoken.base-url}) String baseUrl, Value(${taotoken.api-key}) String apiKey, Value(${taotoken.model}) String model, Value(${taotoken.timeout-seconds:60}) long timeoutSeconds) { this.baseUrl baseUrl; this.apiKey apiKey; this.model model; this.objectMapper new ObjectMapper(); this.httpClient new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(timeoutSeconds, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); } }这里把连接超时设成 10 秒读超时用配置的 60 秒。连接超时短一点没关系因为建立 TCP 连接很快读超时长一点因为模型生成需要时间。这个区分很重要很多人把两者设成一样结果要么连接阶段等太久要么生成阶段被误杀。再往下是构造请求体的方法。我用 Jackson 的ObjectNode来拼 JSON比手写字符串安全也比定义一堆 DTO 类轻量。核心结构就是model、messages、temperature三个字段public String chat(String systemPrompt, String userPrompt) throws IOException { ObjectNode root objectMapper.createObjectNode(); root.put(model, model); root.put(temperature, 0.7); ArrayNode messages root.putArray(messages); ObjectNode sysMsg messages.addObject(); sysMsg.put(role, system); sysMsg.put(content, systemPrompt); ObjectNode userMsg messages.addObject(); userMsg.put(role, user); userMsg.put(content, userPrompt); RequestBody body RequestBody.create( objectMapper.writeValueAsString(root), MediaType.parse(application/json)); Request request new Request.Builder() .url(baseUrl /v1/chat/completions) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .post(body) .build(); try (Response response httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException(请求失败HTTP response.code() 响应体 response.body().string()); } JsonNode json objectMapper.readTree(response.body().string()); return json.path(choices).path(0) .path(message).path(content).asText(); } }这段代码里有两个地方值得说。一是temperature设 0.7出题场景需要一点多样性太低会千篇一律太高又容易跑偏0.7 是个比较稳的中间值。二是错误处理我把非 2xx 的响应体也带进异常信息里这样排查时能直接看到服务端返回的错误描述不用再去猜。到这里SDK 初始化部分就完成了。整个类没有依赖任何厂商特有的包纯 OkHttp Jackson换 baseUrl 就能切到别的兼容服务。这就是统一通道的价值你的代码只认协议不认厂商。4. 生成题目接口实现与一次真实请求验证有了客户端接下来写业务层。目标是提供一个接口传入题目数量和方向返回结构化的题目列表并且能直接落库。先定义返回结构。我建了一个QuestionDTO字段包括题干、选项列表、正确答案、解析Data public class QuestionDTO { private String title; private ListString options; private String answer; private String analysis; }然后是 Service 层。核心思路是用 system prompt 约束输出格式让模型只吐 JSON 数组不要任何多余文字再用 user prompt 传入具体参数。这里 prompt 的设计直接决定了解析难度我踩过的坑是早期让模型输出 Markdown 列表结果解析时要处理各种序号、加粗、换行非常脆弱。后来改成强制 JSON解析就稳了。Service public class QuestionGenerateService { private final TaoTokenClient client; private final ObjectMapper objectMapper new ObjectMapper(); public QuestionGenerateService(TaoTokenClient client) { this.client client; } public ListQuestionDTO generate(int count, String direction) throws IOException { String systemPrompt 你是一位专业的出题老师。请根据用户给出的数量和方向生成题目 严格输出一个 JSON 数组不要输出任何解释、开场白或 Markdown 代码块标记。 数组中每个元素包含四个字段title题干字符串、 options选项字符串数组单选题四个选项、 answer正确答案如 \A\、analysis解析字符串。; String userPrompt String.format(题目数量%d题目方向%s, count, direction); String raw client.chat(systemPrompt, userPrompt); String cleaned stripCodeFence(raw); return objectMapper.readValue(cleaned, new TypeReferenceListQuestionDTO() {}); } private String stripCodeFence(String raw) { String trimmed raw.trim(); if (trimmed.startsWith()) { int firstNewline trimmed.indexOf(\n); int lastFence trimmed.lastIndexOf(); if (firstNewline 0 lastFence firstNewline) { trimmed trimmed.substring(firstNewline 1, lastFence).trim(); } } return trimmed; } }stripCodeFence这个方法是为了兜底。即使 prompt 里明确说了不要 Markdown 标记模型偶尔还是会包一层json所以解析前先剥掉。这是实战经验别指望模型 100% 听话。Controller 层就很简单了RestController RequestMapping(/api/question) public class QuestionController { private final QuestionGenerateService service; public QuestionController(QuestionGenerateService service) { this.service service; } PostMapping(/generate) public ListQuestionDTO generate(RequestParam int count, RequestParam String direction) throws IOException { return service.generate(count, direction); } }现在做一次真实请求验证。启动项目后用 curl 打本地接口curl -X POST http://localhost:8080/api/question/generate?count2directionJava集合我实测下来返回结果大致是这样节选[ { title: 在 Java 中ArrayList 和 LinkedList 的主要区别是什么, options: [ A. ArrayList 底层是数组LinkedList 底层是链表, B. 两者底层都是数组, C. ArrayList 线程安全LinkedList 不安全, D. 两者都不支持随机访问 ], answer: A, analysis: ArrayList 基于动态数组实现支持 O(1) 随机访问LinkedList 基于双向链表插入删除效率高但随机访问为 O(n)。 }, { title: HashMap 在 JDK 8 中解决哈希冲突的方式是什么, options: [ A. 仅使用链表, B. 仅使用红黑树, C. 链表长度超过阈值时转为红黑树, D. 使用开放寻址法 ], answer: C, analysis: JDK 8 中当链表长度达到 8 且数组容量达到 64 时链表会转换为红黑树以降低查询时间复杂度。 } ]拿到这个结构后落库就顺理成章了。你可以把QuestionDTO映射到实体表options用 JSON 字段存或者拆成子表。我一般会加一个source字段标记是 AI 生成方便后续人工审核。整个链路从请求到入库单次耗时在 5 到 15 秒之间取决于题目数量和模型负载。如果你还想在浏览器里手动对比不同 prompt 的效果可以到模型对话页面直接试改 system prompt 看输出变化比每次重启服务快得多。5. 高频报错排查401、超时与解析失败这一节把我遇到过的坑集中列一下都是真实报错对照着看能省不少时间。第一个是 401 Unauthorized。报错信息通常是{error:{message:Invalid API key,type:invalid_request_error}}。原因无非三种Key 复制时带了空格或换行环境变量没生效System.getenv返回 null请求头里忘了加Bearer前缀。排查方法很简单在初始化时打印一下 Key 的长度和前四位确认非空且格式对。注意不要把完整 Key 打进日志那是安全事故。第二个是连接超时或local proxy failed这类网络层报错。如果你在公司内网可能有 HTTP 代理拦截需要在 OkHttpClient 里配置proxy。另外检查 baseUrl 是不是写成了https://taotoken.net/api/带尾斜杠拼接后变成//v1有些网关会拒绝。正确写法就是不带尾斜杠的https://taotoken.net/api。第三个是Cannot deserialize value of type ... from Array value或者reading choices相关的解析异常。这通常意味着返回的 JSON 结构和预期不符。可能是模型没按 JSON 输出返回了一段自然语言也可能是服务端返回了错误对象而你的代码直接去取choices。解决办法是在解析前先判断json.has(choices)没有就抛出带原始响应的异常。我前面代码里json.path(choices).path(0)用的是path而不是get就是为了避免空指针但错误信息会丢失所以建议在chat方法里对非 2xx 单独处理。第四个是 OAuth 或鉴权相关的报错比如OAuth token expired。如果你用的是长期有效的 API Key一般不会遇到但如果接入了某些需要刷新 token 的流程就要检查刷新逻辑。TaoToken 的 API Key 方式不涉及 OAuth 刷新所以看到这类报错先确认你是不是误用了别的鉴权方式。第五个是模型返回内容被截断。表现是 JSON 解析到一半报Unexpected end-of-input。原因是max_tokens没设够或者读超时太短。生成 10 道题可能需要 2000 到 4000 个 token建议在请求体里显式设置max_tokens比如 4096同时把读超时提到 90 秒。为了让你对照方便我把常见错误码和处置方式整理成表现象可能原因处置HTTP 401Key 错误或缺失检查环境变量与 Bearer 前缀HTTP 404baseUrl 路径错误确认是https://taotoken.net/api连接超时网络或代理问题检查代理配置与网络连通性解析 choices 失败返回结构非预期打印原始响应判断是否错误对象JSON 截断max_tokens 不足提高 max_tokens 与读超时返回带代码块模型未严格遵守格式解析前剥离 标记排查时有个通用技巧把原始响应体完整打出来。很多人只打异常堆栈看不到服务端到底返回了什么等于盲猜。在chat方法里加一行日志把response.body().string()存到变量再解析出问题时一目了然。6. 从出题到落库的完整链路与后续优化方向把上面几块拼起来完整链路是这样的Controller 接收数量和方向参数Service 组装 system 和 user 两条 promptTaoTokenClient 通过统一通道发请求拿到 JSON 后反序列化成QuestionDTO列表再交给 Repository 落库。整个过程业务代码里没有出现任何厂商特有的类名只有 OkHttp 和 Jackson 这两个通用库。落库时我建议加两道保险。一是字段校验AI 生成的题目偶尔会出现选项数量不对、答案不在选项范围内的情况入库前用简单的规则过滤掉。二是加status字段默认标记为「待审核」人工确认后再置为「已发布」。这样即使模型抽风也不会把错误题目直接推给用户。后续如果要提升吞吐可以把单次生成改成流式输出边生成边解析用户等待感会好很多。TaoToken 的接口支持stream: trueOkHttp 侧用ResponseBody.source()逐行读 SSE 即可。另外如果你这个项目要长期跑批量生成任务可以看看 Coding Plan它在持续调用场景下的额度策略更友好具体可以到https://taotoken.net/coding-plan了解。最后留一个实用技巧把常用的 prompt 模板抽到数据库或配置中心而不是硬编码在 Java 里。出题方向从 Java 扩展到 Python、前端、数据库时只需要加配置不用改代码重新发版。我现在的项目里system prompt 就是一张表运营同学自己就能调开发只负责调用链路。这样分工效率高很多。