领课教育(Roncoo Education)是一套开源的分布式在线教育系统(Spring Boot 3.5 + Spring Cloud Alibaba,前端 Vue 3 + Element Plus)。最近的版本里,我们给系统接入了大语言模型(LLM)能力,其中最典型的场景就是AI 辅助生成教学大纲:课程编辑在富文本编辑器里选择"教学大纲"类型,输入一句需求描述,AI 便以打字机效果流式输出一份结构完整的大纲草稿,确认后一键插入课程简介。
一、设计目标:协议抽象,而不是厂商绑定
接入大模型,第一个要回答的问题是:绑哪家?
如果为每家厂商写一套 SDK 调用,后续换模型、换服务商就要动业务代码。领课教育的选择是按"协议"抽象,而不是按"厂商"抽象。目前主流大模型 API 归结为三种协议:
| 协议 | 代表服务商 |
|---|---|
OpenAI 兼容协议(/chat/completions) | OpenAI、通义千问、DeepSeek、Kimi、讯飞星火等 |
Anthropic Messages 协议(/messages) | Claude 系列 |
OpenAI Responses 协议(/responses) | OpenAI 新版接口 |
这套划分落在枚举LlmPlatformEnum中:
publicenumLlmPlatformEnum{/** OpenAI 及兼容协议,适用于:OpenAI、阿里百炼/通义千问、DeepSeek、月之暗面等 */OPENAI_COMPATIBLE(1,"OpenAI兼容协议","openai_compatible%","OPENAI_COMPATIBLE"),/** Anthropic Messages 协议,适用于:Claude 系列 */ANTHROPIC_MESSAGES(2,"Anthropic Messages协议","anthropic%","ANTHROPIC_MESSAGES"),/** OpenAI Responses API,适用于:OpenAI 新版 Responses 接口 */OPENAI_RESPONSES(3,"OpenAI Responses协议","openai_responses%","OPENAI_RESPONSES");}与之配套的配置类Llm同样只保留协议维度的字段:llmPlatform(协议类型)、aiApiKey、aiBaseUrl、aiModel、temperature、maxTokens、timeout。这意味着:换服务商,只需在系统配置里改一个 baseUrl 和 model,业务代码零改动。想从 OpenAI 切到 DeepSeek?把aiBaseUrl指向https://api.deepseek.com即可,因为两者都走 OpenAI 兼容协议。
配置存在系统配置表里,由SysConfigCommonBiz.getSysConfig(Llm.class)统一读取,管理员在后台"系统配置"页面即可完成所有 LLM 参数的调整。
二、核心抽象:LlmFace 接口与路由
所有协议实现都收敛到一个接口LlmFace,提供同步调用、流式调用、连接测试三个方法。以 OpenAI 兼容协议实现为例,底层直接复用了Spring AI的OpenAiChatModel,省去手写 HTTP 与 SSE 解析的体力活:
@Component(value="OPENAI_COMPATIBLE")publicclassOpenAiCompatibleLlmImplimplementsLlmFace{@OverridepublicFlux<String>chatStream(LlmRequestrequest,Llmconfig){OpenAiChatModelchatModel=buildChatModel(request,config);returnchatModel.stream(newPrompt(toMessages(request))).mapNotNull(resp->{if(resp.getResult()==null||resp.getResult().getOutput()==null){returnnull;}returnresp.getResult().getOutput().getText();}).filter(StringUtils::hasText);}privateOpenAiChatModelbuildChatModel(LlmRequestrequest,Llmconfig){OpenAiApiopenAiApi=OpenAiApi.builder().baseUrl(StringUtils.hasText(config.getAiBaseUrl())?config.getAiBaseUrl():DEFAULT_BASE_URL).apiKey(config.getAiApiKey()).build();// ... model / temperature / maxTokens 按 请求 > 配置 > 默认值 的优先级解析}}注意@Component(value = "OPENAI_COMPATIBLE")这个细节——Bean 名称与LlmPlatformEnum的mode字段一一对应。于是路由层LlmHelper不需要写任何 if-else,直接利用 Spring 的集合注入特性:
@ComponentpublicclassLlmHelper{privatefinalMap<String,LlmFace>llmFaceMap;publicLlmHelper(Map<String,LlmFace>llmFaceMap){// Spring 自动注入所有 LlmFace 实现this.llmFaceMap=llmFaceMap;}publicLlmFacegetLlmFace(Llmconfig){LlmPlatformEnumplatformEnum=LlmPlatformEnum.byCode(config.getLlmPlatform());returnllmFaceMap.get(platformEnum.getMode());}publicFlux<String>chatStream(LlmRequestrequest,Llmconfig){request.setStream(true);returngetLlmFace(config).chatStream(request,config);}}新增一种协议(比如未来接入某家新的国产大模型私有协议),只需要新增一个LlmFace实现类加一个枚举值,LlmHelper和所有业务代码都不用碰。这是典型的开闭原则落地。
三、AI 写作任务:两段式设计与 aiToken
业务入口在系统服务的AdminAiWriteController(/system/admin/ai/write),拆成两个接口:
POST /create:创建写作任务,返回一次性aiToken;GET /completions:凭aiToken通过 SSE 流式获取 AI 输出。
为什么要拆两步,而不是一个接口直接流式返回?因为流式接口走的是EventSource(GET 请求),不适合携带 JSON body。于是任务参数先落 Redis,换回一个短时 token:
publicResult<String>create(AdminAiWriteCreateReqreq){Llmllm=sysConfigCommonBiz.getSysConfig(Llm.class);if(llm==null||!StringUtils.hasText(llm.getAiApiKey())){returnResult.error("AI功能未配置,请在系统配置中设置 aiApiKey");}StringaiToken=IdUtil.fastSimpleUUID();StringtaskData=req.getWriteType()+"||"+req.getUserPrompt();cacheRedis.set(AI_TOKEN_PREFIX+aiToken,taskData,AI_TOKEN_EXPIRE_MINUTES,TimeUnit.MINUTES);returnResult.success(aiToken);}aiToken有效期 10 分钟、一次性使用,天然规避了请求参数被篡改和重放的问题——这也是把 AI 能力暴露给管理后台时的一个安全细节。
四、课程大纲的提示词工程
系统把 AI 写作场景抽象成WriteTypeEnum:通用写作、课程简介、文章撰写、营销文案、教学大纲。每种类型对应一段专职的系统提示词(System Prompt),这是决定输出质量的关键一层:
privateStringbuildSystemPrompt(WriteTypeEnumwriteType){returnswitch(writeType){caseCOURSE_INTRO->"你是一位专业的在线教育课程文案撰写专家。请根据用户的要求,撰写吸引人的课程简介,"+"突出课程价值、学习收益和目标受众。使用Markdown格式输出。";caseSYLLABUS->"你是一位专业的教学设计专家。请根据用户的要求,设计详细的教学大纲,"+"包含课程目标、章节安排、知识点和学习目标。使用Markdown格式输出。";// ...};}针对教学大纲,提示词明确约束了四个产出要素:课程目标、章节安排、知识点、学习目标——这正是教学设计(Instructional Design)中大纲的标准骨架,而不是让模型随意发挥。用户侧只需输入一句自然语言,比如:
“面向零基础学员的 Spring Boot 入门课,30 学时,包含实战项目”
模型返回的大纲会自带章节层级、每章知识点列表和学习目标,Markdown 格式,与前端渲染天然契合。
五、SSE 流式输出:让 AI “边想边说”
大纲类内容动辄上千字,如果等模型全部生成完再返回,用户要盯着空白页面十几秒。系统采用SSE(Server-Sent Events)流式输出,模型每吐出一个 token 片段就立刻推给浏览器。
这里有个容易踩坑的细节:SSE 协议规定每条消息以\n\n结尾,如果模型输出内容本身含换行符,会直接破坏协议分帧。领课教育的解法是在服务端对内容做转义编码:
publicstaticStringstreamToResponse(Flux<String>flux,HttpServletResponseresponse)throwsIOException{response.setContentType("text/event-stream");response.setCharacterEncoding("UTF-8");PrintWriterwriter=response.getWriter();flux.doOnNext(content->{// content 中的换行编码为 \\n,避免破坏 SSE 协议格式writer.write("data: "+encodeForSse(content)+"\n\n");writer.flush();}).doOnComplete(()->writer.write("data: [DONE]\n\n")).blockLast();}publicstaticStringencodeForSse(Stringcontent){returncontent.replace("\\","\\\\").replace("\n","\\n").replace("\r","");}流结束以data: [DONE]标记,出错则以data: [ERROR]开头推送错误信息,前端据此区分三种状态。
六、前端:编辑器里长出来的 AI 入口
AI 能力的最终落点是管理后台的富文本编辑器。领课教育前端基于 WangEditor 做了深度定制,AI 写作被注册为编辑器工具栏上的一个菜单按钮,点击后弹出AiForm对话框:
选择写作类型(教学大纲) → 输入需求 → 点击"创作开始" → 打字机效果流式渲染 → "插入下方"核心交互由一个通用的 SSE composableuseEventStream驱动,内置了 80ms 节流渲染,避免每个 token 都触发一次 Markdown 重排:
const{start,loading:isBeginning,isComplete}=useEventStream({onChunk:(text)=>{// 后端将换行符编码为 \n 字面量推送,此处还原后再渲染constdecoded=text.replace(/\\n/g,'\n').replace(/\\r/g,'')messageList.value=safeMarkdownRender(preprocessMarkdown(decoded))// 自动滚动到底部},onError:()=>ElMessage.error('AI写作繁忙,请稍后重试')})consthandleAi=async()=>{constaiToken=awaitsystemApi.aiWriteCreate(formModel)// 第一步:创建任务awaitstart(()=>systemApi.aiWriteCompletions({aiToken}))// 第二步:SSE 流式接收}用户确认后,内容经dangerouslyInsertHtml插入编辑器当前光标处,与人工撰写的内容无缝混合编辑:
consthandleAiCallback=(val)=>{aiVisible.value=falseif(val){editorRef.value.restoreSelection()editorRef.value.dangerouslyInsertHtml(val)// AI 内容插入光标处}}而这个编辑器,正是课程编辑页(views/course/update/index.vue)中"简介"字段所用的组件——也就是说,AI 生成的教学大纲可以直接成为课程详情页的一部分,也可以作为讲师后续录制课程视频时的章节蓝图。
七、架构小结
回头看整条链路:
管理员配置 LLM(协议 + baseUrl + apiKey + model) │ ▼ WangEditor「AI写作」按钮 → AiForm(选择"教学大纲" + 输入需求) │ ▼ POST /system/admin/ai/write/create ──→ Redis 存任务,返回 aiToken(10 分钟有效) │ ▼ GET /system/admin/ai/write/completions?aiToken=xxx │ ▼ LlmHelper 按协议路由 → LlmFace 实现(Spring AI)→ 大模型流式响应 │ ▼ LlmUtil SSE 转义输出 → 前端 useEventStream 节流渲染 Markdown → 插入编辑器几个值得借鉴的工程决策:
- 协议抽象优于厂商抽象——三种协议覆盖了当前几乎全部主流大模型,换服务商是配置变更而非代码变更;
- 两段式任务设计——
aiToken一次性、短时效,兼顾了 EventSource 的技术约束与接口安全; - 提示词与场景绑定——"教学设计专家"的角色设定 + 四要素约束,把通用大模型变成了垂直场景的专用工具;
- SSE 内容转义——一行
encodeForSse解决了流式输出中最常见的分帧事故; - AI 深度嵌入既有工作流——不是独立页面,而是编辑器工具栏上的一个按钮,学习成本几乎为零。
AI 不会替代讲师,但可以把讲师从"搭大纲骨架"这类结构性劳动中解放出来,让人的精力聚焦在真正需要专业判断的内容打磨上。这或许就是 AI 与教育系统结合最务实的姿势。