尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大模型API驱动的论文写作:分段生成与模型降级的工程实践

大模型API驱动的论文写作:分段生成与模型降级的工程实践 简介基于AI大模型文本生成能力打造的论文写作工具项目面向需要完成毕业论文、期刊论文、课程设计等各类学术写作任务的学生与科研人员将自然语言处理与大模型能力融入实际应用可生成超过万字的长篇内容并自动引用真实参考文献有效解决写作素材不足、时间紧张等问题。压缩包共包含139个文件大小仅4.34MB其中以85个Java后端服务源码、8个Vue前端页面、8个TypeScript脚本以及10个XML配置文件为主同时提供Docker容器化部署文件、YAML编排配置、SQL数据库初始化脚本和说明文档整体结构清晰前后端职责明确易于本地部署与二次开发。目前已有489人学习查看。核心源码完整呈现AI模型接入、超长文本生成与文献引用实现的处理流程读者可参考统一响应封装、环境变量配置模板和部署脚本快速还原一套可运行的论文写作工具并据此理解大模型在实际业务中的落地思路。作者还整理了人工智能学习总结成果方便学习时沟通交流。1. 论文写作工具为什么要自己拆大模型API拿到一个“AI写作工具.zip”源码包通常第一反应是找 prompt。真正跑起来才发现把整篇论文塞进一次对话模型只会给出空泛框架超不出 1500 字。这个项目恰好相反后端用WriteServiceImpl把“写论文”拆成一个个小生成任务每个任务动态生成一段内容再按顺序拼装。它能解决长文本断裂、格式前后不一致、参考文献不可信三个问题。摘要里的 11000 字超长文本不是靠一次提示词生成而是服务端把标题、引言、正文、结论分开生成后拼接。它带着 Dockerfile、.env.example 和两个核心 Java 服务类能从本地跑起来也能换任意兼容 OpenAI 协议的模型适合研究 AI 大模型应用开发的人。下面先看论文章节生成再看请求封装然后是容器部署最后说一个模型降级的技巧。2. WriteServiceImpl用上下文状态机控制论文章节生成2.1 为什么不能“一次提示词写全篇”大模型文本生成有上下文窗口硬约束。即便模型支持 128k生成时注意力也会分散到较远内容导致后文忘记前文的主语、术语和图表编号。论文写作尤其不能错摘要里提到的创新点正文必须照应结论里的数据必须和前面一致。因此项目采用分段生成而不是全量生成。WriteServiceImpl的核心职责就是维护一个“论文生成状态机”。状态包括DRAFT_TITLE - DRAFT_ABSTRACT - DRAFT_MAIN - DRAFT_CONCLUSION - GENERATE_REFERENCES。每完成一段把该段文本写入上下文缓冲区形成新的history。这样每个环节看到的都是完整的此前内容而不是整篇论文的骨架。分段生成还有一个工程优势可以中断、重试。某一段超时失败不需要重新生成全部。这对收费 API 尤其重要既能省 token也便于在超时后从断点续写。2.2 核心代码逐节生成与上下文拼接下面是简化后的WriteServiceImpl.java保留了素材里的类名把重点放在“一个生成任务如何组织上下文”。这是这个项目中比较核心的方法。public class WriteServiceImpl implements WriteService { private final OpenAIChatService chatService; // 每段生成最大token避免一次返回过长导致截断 private final int SECTION_TOKEN_LIMIT 1200; // sectionOrder 定义论文生成顺序 private final ListString sectionOrder List.of( title, abstract, introduction, body, conclusion, references); public Draft generate(String requirement, String academicLevel) { String sessionKey UUID.randomUUID().toString(); ListMessage history new ArrayList(); history.add(systemPrompt(requirement, academicLevel)); for (String section : sectionOrder) { // 每个章节一个明确的输出要求格式统一 String sectionPrompt buildSectionPrompt(section); SectionResult result chatService.chat( sessionKey, history, sectionPrompt, SECTION_TOKEN_LIMIT); history.add(new Message(assistant, result.getText())); // 存入缓存后续 resubmit 时可以直接从断点继续 saveCheckpoint(sessionKey, section, result.getText()); } return new Draft(history); } }逻辑说明这里没有把所有上下文丢给模型而是按sectionOrder依次执行。history保存每次生成结果所以“结论”生成时能看到“引言”和“正文”术语自然保持一致。SECTION_TOKEN_LIMIT是每个章节的输出上限比让模型一次性写 5000 字更可靠。saveCheckpoint把已生成的章节存到 Redis 中若第 4 个章节超时重新发起时直接跳到第 4 章而不是从头生成。参数说明requirement来源于用户输入的课题方向academicLevel用于选择论文类型毕业论文、期刊论文或课程设计。这个参数进入 systemPrompt 后会决定语气和结构。例如课程设计更侧重代码实现和测试结果毕业论文更强调研究背景和创新性。SECTION_TOKEN_LIMIT默认 1200若模型输出中文约等于 900 个汉字足够覆盖引言或总结的一段完整论述。若想更细粒度可以把body再拆成多个子任务每个子任务使用独立的 prompt。2.3 长文本超11000字的组装策略上面代码里逐渐积累 history最终获得整篇论文。但 11000 字不是一次生成而是多次生成累加得到的。常见做法是设置每个章节的目标字数根据 token 换算关系动态调整max_tokens。我一般会在配置表中维护节点参数像这样章节目标字数建议max_tokenstemperaturetitle20-50600.7abstract300-5007000.6introduction800-120015000.7body900-200022000.4conclusion400-80010000.5references真实文献列表8000注意 references 的 temperature 设为 0因为参考文献不是“生成”而是从检索结果中按引用映射拼装。该表可以直接放到配置中心换模型后不必改代码。如果模型上下文窗口较小就把 body 再拆成 body-1、body-2。这是实现长文本的核心思路动态文本生成不是让模型输出长文而是让模型输出多个有上下文关联的短文再由服务拼成一篇长文。3. OpenAIChatServiceImpl统一协议层如何接入不同大模型3.1 为什么单独拆出一个 Chat ServiceWriteServiceImpl只关心论文结构不关心请求发送到哪个模型。如果直接把 API 调用散落在业务代码里换模型时就要改动所有生成逻辑。OpenAIChatServiceImpl作为一个与模型实现解耦的协议层接收统一的Message列表返回字符串。好处是本地开发时可以用本地部署 AI 大模型跑通流程生产环境切到云端模型只改配置不改代码。这类实现通常会继承一个ChatService接口。接口方法要考虑三个边界上下文窗口限制、非流式响应时的超时、错误重试。下面看实现关键点。3.2 核心代码非流式调用的封装Service public class OpenAIChatServiceImpl implements OpenAIChatService { private final RestTemplate restTemplate; private final ModelConfig modelConfig; public String chat(String sessionId, ListMessage messages, int maxTokens, double temperature) { // 截断最早对话只保留最近20条避免超过上下文窗口 ListMessage windowed trimContext(messages, 20); MapString, Object body new HashMap(); body.put(model, modelConfig.getPrimaryModel()); body.put(messages, windowed); body.put(max_tokens, maxTokens); body.put(temperature, temperature); body.put(stream, false); HttpHeaders headers new HttpHeaders(); headers.setBearerAuth(modelConfig.getApiKey()); headers.setContentType(MediaType.APPLICATION_JSON); try { ResponseEntityMap resp restTemplate.postForEntity( modelConfig.getBaseUrl() /chat/completions, new HttpEntity(body, headers), Map.class); return extractContent(resp.getBody()); } catch (HttpClientErrorException e) { if (e.getStatusCode().is4xxClientError()) { throw new ModelAuthException(e.getRawStatusCode(), e.getResponseBodyAsString()); } return retryWithFallbackModel(messages, maxTokens, temperature); } } }逻辑说明trimContext是长对话上下文控制的关键。论文写作进程里的history可能已经有十几轮全部发送会超出模型窗口因此只截取最近 20 条同时把早期摘要压缩成一行。body里的streamfalse表示等模型完整生成后再返回简单但等待时间长如果给前端做打字机效果需要改为streamtrue并用 SseEmitter 推送这个项目同时支持两种模式前端页面里能看到流式输出。restTemplate.postForEntity这种方式适合单次调用如果追求吞吐可以把 RestTemplate 换成 WebClient 的响应式写法。参数说明max_tokens控制生成上限太大容易超时太小会让段落腰斩。我通常取 2.2 节表里的值但实际要根据模型的最大输出限制来 clamp。temperature控制随机性论文写作在正文阶段使用 0.4-0.7参考文献阶段必须接近 0否则模型会“编”引用文献。若企业级场景还要加frequency_penalty和presence_penalty让正文避免重复措辞。3.3 超时重试与本地模型回退OpenAI 兼容接口调用最常见的坑是超时。连接超时和读取超时要分开配置参数名推荐值说明connectTimeout5000ms最多等待建立连接readTimeout60000ms等待返回首个字节的时间maxTokens 上限4000超过容易超时retryTimes2不成功就回退模型fallbackModeldeepseek-chat本地可替换为 qwen、llama 等在OpenAIChatServiceImpl里retryWithFallbackModel可以把body里的 model 字段替换为modelConfig.getFallbackModel()并重新发送。这里的 baseUrl 既可以指向云上的 OpenAI 兼容接口也可以指向 vLLM 或 Ollama 的本地部署 AI 大模型只要它们兼容/chat/completions。好处是本地测试不消耗线上额度集成测试时直接跑 mock 数据正式环境中如果主模型限流自动切到备用模型。这一层是所有 AI 大模型应用开发里最容易复用的一块。3.4 流式响应与 SSE 实现要点论文写作界面里“逐字显示”的效果来自流式响应。常见做法是让OpenAIChatServiceImpl额外提供一个streamChat方法返回FluxString每个元素是一段 SSE 数据。服务端解析出delta.content后通过SseEmitter推给前端。要注意连接建立后立刻发送心跳包否则经过网关时连接会被提前关闭。流式模式下不需要设置过大的readTimeout因为首字到达时间会更快。在流式返回时readTimeout 设置太大没有意义因为连接建立后可能一分钟才返回第一个 token。更合理的做法是给每个会话设置一个总生成预算例如 120 秒超过就标记为超时。在streamChat的实现里我会在doOnNext中定期检查System.currentTimeMillis()硬性停止超过预算的流。这个小逻辑能避免后台线程被半开的 HTTP 连接拖死。4. 从 .env 到 Dockerfile把论文写作服务装进容器4.1 .env.example 应该暴露哪些变量资源里包含.env.example这是让服务跑起来的第一步。不少项目把 API Key 直接写死在代码里换环境就要重新编译。正常做法是通过ConfigurationProperties映射到ModelConfig类。.env.example至少要包含这些# 必填模型接口地址与密钥本地模型填 vLLM/Ollama 地址即可 OPENAI_BASE_URLhttps://api.example.com/v1 OPENAI_API_KEYsk-xxxx # 模型选择 PRIMARY_MODELgpt-4o FALLBACK_MODELdeepseek-chat # 生成控制 DEFAULT_TEMPERATURE0.6 DEFAULT_MAX_TOKENS1200 MAX_PAPER_LENGTH11000 # 上下文窗口按模型能力调整 CONTEXT_TRIM_SIZE20环境变量解析时要注意baseUrl末尾是否带/v1。常见错误是配置了https://api.example.com拼接时变成/chat/completions而实际需要/v1/chat/completions。我一般会在启动时打印modelConfig.getBaseUrl()并在构建 URI 时用path(/chat/completions)来确保路径合法。4.2 多阶段 Dockerfile 减少镜像体积项目中的 Dockerfile 采用多阶段构建前端资源单独打包。第一个阶段处理前端把tailwind.css、_variables.css等静态资源直接打包到一个镜像层第二个阶段构建 Java 服务最终运行镜像只有 JRE 和打包产物不包含源码和依赖缓存。# 第一阶段前端资源 FROM node:20-alpine AS frontend WORKDIR /web COPY tailwind.css _variables.css ./ # 这里按项目实际构建命令补齐 RUN npm run build # 第二阶段Java后端打包 FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第三阶段运行镜像 FROM eclipse-temurin:21-jre COPY --frombuild /app/target/ai-writer.jar /app/ai-writer.jar COPY --fromfrontend /web/dist /app/static COPY .env.example /app/.env.example EXPOSE 8080 ENTRYPOINT [java, -jar, /app/ai-writer.jar]逻辑说明注意第三阶段并没有把.env.example当作有效配置只是放进镜像里备查。真实配置在启动时通过--env-file注入这样本地和服务器使用同一套镜像只替换密钥。前端资源在构建期生成后端运行时不依赖 node 环境。若不需要前端可以简化为 maven 和 jre 两段。参数说明mvn dependency:go-offline预下载依赖能缩短后续重复构建时间-DskipTests跳过测试但会在 CI 里单独跑一次mvn test避免带着损坏的测试代码上线。EXPOSE 8080只是声明端口实际映射由docker run -p控制。4.3 用 docker run 启动并验证构建启动命令如下docker build -t ai-writer:latest . docker run -d --name ai-writer \ --env-file .env \ -p 8080:8080 \ --memory1g --cpus1 \ ai-writer:latest这段命令里build -t给镜像命名run -d让容器后台运行--env-file把环境变量注入容器-p 8080:8080映射端口--memory和--cpus限制资源。如果缺少--env-file服务能正常启动但请求模型时会直接报 401。等待几秒后用docker logs ai-writer看是否正常监听再调一个健康检查接口curl -s http://localhost:8080/api/health | jq这里的/api/health是服务的探活接口jq用来格式化 JSON 输出。如果接口返回 200说明 Spring 容器已经就绪。如果容器起来后提示连接被拒绝先看 Java 进程里配的OPENAI_BASE_URL。可以在docker exec ai-writer env里检查环境变量是否注入成功然后从 Java 容器内用wget -qO- http://vllm-container:11434/v1/models测试到模型的网络连通性。如果模型容器没暴露端口就会出现这边显示启动成功、那边请求一直超时的情况。注意两个容器默认不在同一网络需要先docker network create ai-net并把两个容器都接入该网络服务间使用容器名称访问。否则连接会一直超时。这是容器化部署 AI 写作服务时最容易踩的坑模型服务在 A 容器Java 服务在 B 容器B 里的localhost:11434指的不是 A。把这行--network ai-net加到docker run参数里并把 baseUrl 改成http://vllm-container:11434/v1即可解决。5. 用 R.java 统一返回结构做模型降级与断点续写5.1 R.java 的状态码设计很多 Spring 项目里R是统一返回体常见结构包含code、message、data。这个项目把它用在所有接口上比如/api/write/generate返回的每一段写结果都会被包装成 R。把错误码细分之后前端能针对不同情况给出不同提示public class RT { private int code; // 0 成功非0失败 private String message; private T data; private String trace; // 断点续写用的游标 public static T RT ok(T data, String trace) { return new R(0, ok, data, trace); } public static T RT fail(int code, String message) { return new R(code, message, null, null); } }错误码定义1001 参数错误2001 模型超时2002 模型限流2003 内容审核不通过。前端拿到 2001 或 2002 时可以显示“正在切换模型”而不是直接报错。这个设计在论文场景里特别有用用户已经写了 2000 字如果模型限流导致整篇重来体验会很差。参数说明trace是一个不透明字符串服务端可以写入{section:2, offset:800}让重试接口定位到具体段落和字符偏移而不是只带一个 sessionId。5.2 降级调用链的实现在OpenAIChatServiceImpl里遇到主模型超时时不必直接抛异常而是走降级逻辑public RString chatWithFallback(ListMessage messages, int maxTokens, double temperature, String trace) { try { String data callWithModel(messages, maxTokens, temperature, primaryModel); return R.ok(data, trace); } catch (ResourceAccessException e) { if (modelConfig.isFallbackEnabled() !isFallbackAlreadyUsed(trace)) { String fallbackData callWithModel(messages, maxTokens, temperature, modelConfig.getFallbackModel()); return R.ok(fallbackData, trace); } return R.fail(2001, 主模型和备用模型均超时); } }这个切片的要点是isFallbackAlreadyUsed防止在 A 和 B 之间无限循环判断依据是trace而不是 model因为同一个论文会话可能在第一次调用时就已经用了备用模型。降级切换后trace不变前端拿同一个trace重试即可继续生成。这种模式比纯粹的重试更符合文本生成工具的场景论文本身是长任务中途失败不需要丢弃全部结果。5.3 验证参考文献是否“真实”最后分享一个我拆这类项目时常用的验证手法。项目声称“引用真实参考文献”但大模型生成的引用十有八九是幻觉。我会在写论文服务里加一个校验步骤把[1]这类标记从生成结果中抠出来去 Crossref 检索比对。发一个请求curl -s https://api.crossref.org/works?query.bibliographic论文标题rows1如果返回的 DOI 与生成结果中携带的论文标题不一致就在前端渲染时为该条参考文献标记“请人工核对”。这里的query.bibliographic是论文标题检索字段rows1表示只取最相关的一条避免每次都拉回海量结果。这是不需要改模型就能提升可靠性的做法也顺便满足了论文写作工具最基本的学术底线。本文还有配套的精品资源点击获取
返回列表