做技术复盘这件事,我一般不太愿意写成“项目汇报”那种调调,更多是想把踩过的坑、想通的逻辑、最终能跑通的方案记录下来。一来给自己留个底,二来如果正好有人在做类似的事,能少走点弯路比什么都强。
这次要聊的,是“研途灵伴”里一个非常底层、非常不性感、但关键时刻能救命的部分——跟课上下文服务。简单说,就是学生在上课过程中,AI 需要基于当堂课的内容回答问题、做总结、出练习,那 AI 的“记忆”从哪来?课件 PDF 怎么切?老师讲的话怎么组织?问题来了怎么找到最相关的那几段?这些东西处理不好,再强的模型也白搭,因为模型本身不记得你上节课讲了什么,你必须喂给它。
这篇文章我会把这个项目的复盘拆成四大块:一是整体设计思路与场景拆解,二是上下文组织方式与存储建模,三是检索链路的实现细节与调优过程,四是压测过程中遇到的典型问题与排查录音。内容会偏底层一些,但我会尽量把每一个“为什么这么做”都交代清楚。如果你正在做教育领域的 AI 应用,或者手头有类似“给大模型喂私有知识”的需求,这篇应该能给你一些实在的参考。
1. 核心问题拆解:跟课场景到底需要什么样的“记忆”
1.1 跟课上下文服务的难点不在“存”,而在“理解场景”
先说结论:跟课上下文服务,本质上是一个 RAG(检索增强生成)系统。但如果你把它当成传统的知识库问答来做,大概率会做得很难受。
传统 RAG 的场景是“用户提问 → 从文档库检索 → 拼接上下文交给模型回答”,核心追求是“找到正确答案”。但跟课场景不一样,它有四个我觉得比较独特的约束:
第一,上下文是动态增长的。老师讲到第 3 章的时候,学生问的问题可能涉及第 1 章的背景知识,也可能涉及 5 分钟前刚讲的一句话。系统必须同时理解“课程整体内容”和“当前课堂进度”。
第二,同一个知识点会被反复以不同方式提及。课件里可能只有一句定义,但老师会展开讲解两分钟。这时候如果只切课件文本,检索出来的内容会非常单薄,模型拿不到老师口头讲解的语境,回答就会很“死”。
第三,学生的问题往往是不完整的。比如老师刚讲完“支持向量机的核函数”,学生紧接着就发一句“那多项式核跟高斯核的区别是什么?”——如果系统不知道“当前正在讲 SVM”,这个问题的召回率会非常低。
第四,实时性要求高。学生问完问题,检索加生成的总时长要控制在 3 秒以内。这就意味着检索链路不能太重,重排序步骤要精简,上下文拼接要有预算控制。
所以我在这套服务里反复跟团队强调一个词:场景建模。我们不是在做一个通用的文档问答,而是在做“一个跟着课堂节奏走的助教”。它的记忆不是静态的文档库,而是“课件 + 随堂讲解 + 历史问答 + 当前进度”的组合。
1.2 为什么不能直接拿课件全文去喂模型
先算一笔账:一门课 16 讲,每讲课件大约 30 页 PDF,每页文字量差不多 300~500 字,这样总文字量就在 15 万到 24 万字之间。按 GPT-4o 这类模型的 token 换算,大概 20 万到 30 万 token。这仅仅是课件文本,还没算老师口头讲解转写出来的逐字稿。
逐字稿更夸张:一节课 90 分钟,语音转写大约能产生 1.2 万到 1.5 万字。就算只保留当前这一讲,课件加逐字稿加起来就超过 2 万字,折合约 3 万 token。把 3 万 token 全塞进 prompt,对模型来说处理时间会明显变长,成本也会大幅上升,而且更关键的是——绝大多数问题其实只需要其中 800 到 1500 字的相关片段就能回答,塞太多反而会引入噪声,模型会“迷失在中间”。
这个问题在业界有个专门的说法叫“Lost in the Middle”——模型对长上下文中间位置的内容记忆最差。你辛苦整理的课件内容如果正好被埋在 3 万 token 的正中间,效果还不如只给它 1500 字的高相关片段。
所以从一开始我们就定了一个原则:上下文必须有选择性地组建,绝不能做全文搬运工。跟课上下文服务的核心能力,就是你得知道“此时此刻,这个问题最需要哪几块知识”。
1.3 需求方真正关心的是“效果感知”,而不是技术名词
做这个项目的过程里,我还有一个特别深的感受:需求方(产品的同事、运营的同事)根本不在乎你用了向量数据库还是倒排索引,他们只会感知到“回答得准不准”“是不是答非所问”“响应快不快”。
所以我在复盘的时候,给自己定了一个衡量标准——每一次回答都应该做到“三点可感知”:
- 回答内容能和当前课堂进度对得上。比如老师正讲到线性代数里的特征值,学生问的题不能是基于前面某章内容的生硬回答。
- 回答里能带出课堂里的“现场信息”。比如老师说过的某个例子、某句强调的话,这些课件里往往没有,但学生听到会觉得“这 AI 真的在听课”。
- 回答速度不能让学生觉得卡顿。宁可少拼一点上下文,也不能让用户等 10 秒钟才看到打字机效果。
这个标准听着简单,但实现起来,每一个点都对应着具体的技术选型和策略调整。下面逐个说。
2. 上下文组织与存储建模:从“知道”到“能用”的关键一步
2.1 课件的文本抽取与分片策略
先讲课件处理。PDF 这玩意看着简单,真正把文本抽干净,坑多到让人崩溃。有的 PDF 是文字版直接复制就行,有的是扫描版必须走 OCR,还有的是“文字版但内嵌了公式排版”,比如 LaTeX 生成的 PDF,抽出来会混着大量符号和括号。
我们最终的处理流程是:
- 用 PyMuPDF 做首轮文字抽取,如果单页抽取字数不足 50 字,判定为“疑似扫描页”,转走 OCR 通道。
- OCR 通道用 PaddleOCR 的中英文模型,输出按行排布,再根据行坐标(bbox)做版面还原,尽量恢复阅读顺序。
- 统一做清洗:去掉页眉页脚、去除目录页、合并断行、把全角符号转半角。
文本抽出来之后是分片。这里我要说一个非常反直觉的教训:分片不是越小越好。
一开始我们天真地按 256 字切分,想着这样检索更精准。结果发现,课件里的很多内容存在“上下强关联”。比如一页 PPT 上写着“优点:xxx”,接下来一页是“缺点:yyy”,切碎了之后,模型只看到优点,不知道这是针对哪个方法的评价,回答就会片面。后来我们改成按页码 + 章节标题 + 段落语义三级的混合切分:
- 每页 PPT 作为一个基础块(page_chunk),因为一般一页 PPT 的主题是聚焦的。
- 再根据章节标题做父子块关联,父块是“章”,子块是“页”。检索命中子块时,把父块的标题塞进上下文,模型就知道这段内容属于哪一章,逻辑就顺了。
这个逻辑很像读书时做笔记,你不能只记某一页的一句话,得记住这句话是哪个章节下讲的,才能正确理解它的位置。
正文分片建议参考表格:
| 分片类型 | 切分粒度 | 用途 | 备注 |
|---|---|---|---|
| 章级块 | 一个章节一个块 | 检索定位章节上下文 | 存章节标题、页码范围 |
| 页级块 | 一页 PPT 一个块 | 检索主候选 | 附加页标题、页码信息 |
| 段落语义块 | 按语义合并 2~3 个自然段 | 正文候选 | 用于逐字稿长文本的切分 |
| 问题块 | 历史问答结对存储 | 相似问题召回 | 见后文 |
2.2 随堂讲解逐字稿的增量处理
随堂逐字稿是跟课上下文服务和普通知识库最大的区别点。老师上课讲的每一句话,经过语音识别转写后,都带着时间戳,形成一个对话流。这个流是动态追加的——每 5 到 10 秒就会新增一段。
如果每次有学生提问,我们都把截至当前时刻的所有逐字稿重新切分再入库,代价太高。我们的策略是:做一个“滑动窗口式”的增量分片器。
具体逻辑是这样的:
- 开场预热。每次分片器启动时,先把“过去 3 分钟内”的转写文本作为缓冲,因为很多新引入概念往往会在几分钟内反复出现,不看前文就切分,语义不完整。
- 语义边界识别。当检测到说话人停顿超过 2 秒,或者转写文本里出现“接下来”“然后我们看”“第二点”这类话语标记词时,判定为一个分片候选点。
- 局部合并。把候选点之间的文本按 300~500 字合并成一个“event_chunk”,相当于“老师讲的一个小知识点”,并打上开始时间和结束时间的时间戳。
这样做的好处很直接:每个 event_chunk 都在时间轴上对应到课件页码。比如老师在第 12 页停留了 8 分钟,那这 8 分钟里的好几个 event_chunk 都会被标记为“page=12”的关联 chunk。建立这种“逐字稿 ↔ 课件页 ↔ 授课时间”的三方映射关系,是后面检索能结合“当前讲到哪里”这个信息的核心基础。
2.3 存储选型:为什么要“三库并存”
在存储层的选型上,我们踩过一个不必要的坑。最初想图省事,只上一个向量库——把文本全扔给 Embedding 模型转成向量,检索用余弦相似度。看起来确实简单,但一测就露馅了:课件里的专业术语太多,“特征值”“特征向量”“奇异值”这种词在向量空间里经常距离极近,向量检索会把它们混成一片,返回一堆表面相似实则不相关的内容。
最终我们做成了“三库并存”的架构:
- 关系型数据库(PostgreSQL):存课程、章节、文件版本、页码映射这些结构化管理数据。相当于所有数据的“账本”。
- 向量检索库(开源的 pgvector):存正文切片的向量表示,主用于语义召回。
- 倒排索引(OpenSearch):存正文切片的词法索引,主用于关键词精确召回。
三库之间通过一个唯一的chunk_id相关联。检索时并行打三个池子,再在内存里做融合排序。这个方案比单用向量库在专业术语场景下的准确率高非常多,具体数据后面第 3 部分会给出。
关于选型,我再多说一句:不要迷信“最新最潮”的数据库,稳定、易维护、团队熟悉,比什么都重要。pgvector 放在 PostgreSQL 里,对我们来说最大的价值是省了一套运维,备份和恢复直接用 PG 的机制,省心。
2.4 历史问答的沉淀与再利用
历史问答其实是最容易被忽略、又最有价值的一层记忆。学生在课堂上问过的问题,以及系统给出的回答,都应该被结构化存下来。因为同一个班、同一个老师,不同学生问的问题很可能高度相似——前一个学生刚问完“范数是什么意思”,5 分钟后又有学生问“这范数干嘛用的”,这时候如果系统能直接复用上一轮的上下文,不光响应快,回答的一致性也更好。
我们的实现是把每轮 QA 提炼成一个“QA 对”:
- 将问题文本去停用词、保留核心实体,生成一个
question_key作为检索指纹。 - 对回答里涉及的关键知识点提取标签(比如“范数”“正则化”),存入标签表。
- 每轮 QA 会引用它消费过的最多 5 个 chunk_id,记录“哪些上下文支撑了这次回答”。
后面再有新问题进来,先用相似度匹配question_key,如果命中相似度大于 0.85 的历史问题,优先复用历史回答的上下文组合,再结合最新的课堂进度做修正。这个机制在“同一概念反复被问”的场景下效果极好,而且能显著减少检索服务的压力。
3. 检索链路与上下文注入:让模型“看着课堂回答问题”
3.1 双路召回 + 用“进度上下文”做重排
检索这条链路,我们最终实现的是“双路召回 + 进度上下文重排”的结构。听起来复杂,实际拆开就三步。
第一步是构建查询。学生输入的问题只是一个短文本,直接拿去检索会很吃亏,因为太短、指代不明。我们做了一个“查询改写”的轻量模块,读取当前课堂进度(当前课件页码 + 最近 15 分钟的逐字稿文本摘要),把它拼到学生问题前面,形成一个扩展后的查询。比如:
原始问题:“那它和线性回归有啥区别?” 改写后查询:现在是第 5 章“逻辑回归”,最近老师讲了逻辑回归的损失函数和决策边界。学生问:那它和线性回归有啥区别?
这一步不用调大模型,用简单的模板拼接 + 当前进度标签就能实现,成本几乎为零,但检索效果提升巨大。把搜索范围用“当前场景”锁死,是最划算的优化。
第二步是双路召回。扩展后的查询分别走两条路:
- 向量召回:用 Embedding 模型转成向量,在 pgvector 里查近邻,取 Top 30。
- 关键词召回:在 OpenSearch 里做 BM25 检索,取 Top 30。
第三步是融合重排。把两路各 30 条结果合并,用 RRF(Reciprocal Rank Fusion)融合算法算一个融合分,再取 Top 10。这一步不需要训练模型,几十行代码就能实现,效果却非常稳。RRF 的公式很简单:
RRF 分数 = Σ (1 / (k + rank_i)),其中 k 通常取 60。
排名越靠前,贡献越大;两路都命中的文档融合分会显著更高。这比我之前试过的“先向量取 50 条再用交叉编码器重排”方案要轻量很多,而且少了交叉编码器那一次推理,时延能节约 120 到 200 毫秒。
3.2 BM25 和向量检索的调优实验对比
为了让大家对“为什么双路召回比单路强”有一个直观感受,我放一组我们在内部数据集上的离线评测数据。数据集是从 3 门理工科课程、共 600 个真实学生问题中构建的,人工标注了每个问题对应的相关上下文片段。
| 检索方式 | Recall@10 | 回答准确率(人工评估) | 平均检索耗时 |
|---|---|---|---|
| 纯向量检索(Embedding) | 61.2% | 58.7% | 45ms |
| 纯 BM25 关键词检索 | 54.8% | 51.3% | 28ms |
| 双路召回 + RRF 融合 | 76.5% | 72.4% | 58ms |
| 双路召回 + RRF + 进度上下文重排 | 82.3% | 79.1% | 61ms |
注意看最后两行:单加一个“进度上下文重排”,Recall@10 能提升近 6 个百分点,但检索耗时只增加 3 毫秒。这就是为什么我一直强调“场景信息比模型技巧更值钱”。你没有必要做很复杂的排序模型,你把“现在讲到哪了”这个信息用好,效果立竿见影。
3.3 上下文注入模板的设计取舍
检索出来的 Top 10 片段,不能一股脑全塞给模型。上下文长度有限,而且不同的片段重要性不一样,必须做预算分配。固定一个模板,我们最终是这么组织的:
[课堂进度] 当前课程:《线性代数》 当前章节:第 4 章 特征值与特征向量 当前进度:老师在讲特征值的几何意义 [参考课件内容] <page_chunk 标题=特征值定义,页码=12> 内容…… </page_chunk> [随堂讲解记录] <event_chunk 起始时间=10:23,结束时间=12:05> 老师强调:特征向量在变换后方向不变…… </event_chunk> [相似历史问答] 问:特征值和特征向量是什么意思? 答:……(参考引用) [请基于以上内容回答学生问题] 学生问题:……这里有三点值得细说:
- 课堂进度信息放在最前面。这是给模型建立“时空锚点”,让它知道现在是什么场景、什么进度,回答的时候自然会偏向当前讲的内容。
- 课件内容和随堂讲解记录分成两个独立的 XML 标签块。这样做的目的是让模型区分“静态知识”和“动态讲解”,回答时会自然地结合课件定义和老师口头解释,比混在一起效果更好。
- 历史问答块不是每次都放,只有命中相似问题时才放。不放的时候留一个“无相似历史问题”的占位符,避免模型产生额外联想。
token 预算上,我们给整套上下文配了 2600 token 的上限,其中课件 1000、逐字稿 1000、历史问答 400、其他 200。如果检索结果太多,会按照融合分数从高到低截断,确保总长度可控。这个预算配合流式输出,用户首字延迟实测能稳定在 800~1100ms,不会让人等得心焦。
3.4 场景切换与长课程记忆的过渡方案
还有一个很实际的问题:学生可能中途休息,或者从第 3 章跳着看到第 5 章,甚至隔了一周又回来问之前的内容。如果没有“记忆滚动”的概念,系统会默认“只关心当前这堂课”,那学生问“上周讲的矩阵秩是什么来着”,就答不出来了。
我们做的过渡方案是“三级上下文分级”:
- 当前课次上下文:最近的 90 分钟,优先召回。
- 本课程历史上下文:课程开始以来的 QA 和课件,作为次级候选。
- 全局知识底座:通用知识库,作为保底。
检索开始时,默认只查当前课次。如果当前课次的 Top 结果融合分数低于一个阈值(我们设为 0.35),就自动扩展到课程历史上下文再查一次。如果还不够,再落到全局知识底座。每一级的召回结果都带着课次标签,注入模板的时候会额外标注“以下内容来自第 3 次课”,帮助模型判断信息的新旧关系。
这个设计落地以后,课程的“跨节次连续提问”正确率从 53% 涨到了 74%。没有额外引入更复杂的记忆网络,就是靠“分层查询 + 分数阈值退避”,稳定、可控、好排查。
4. 性能优化与压测实录:一次数据库连接风暴排查
4.1 服务架构与链路时延预算分配
先说链路时延预算。用户发出问题,到流式输出第一个 token,我们内部是按这个预算去扣时间的:
| 环节 | 预算耗时 |
|---|---|
| 网关 + 鉴权 | 100ms |
| 查询改写 | 30ms |
| 双路召回(并行) | 80ms |
| 融合重排 | 10ms |
| 上下文组装 | 10ms |
| LLM 首 token 延迟 | 780ms |
| 合计 | 约 1010ms |
这里最关键的是双路召回必须“并行”发起。最开始实现是串行的,先查向量再查关键词,链路耗时直接多了 80ms。后来改成 Go 的 goroutine 并发发起两路查询,总耗时从 150ms 降到了 80ms 左右。
4.2 压测中遇到的连接池打满与排查过程
上线前的压测,我们耗费了最多的精力去解决一个经典问题:连接池被打满。
压测配置是这样的:用 50 个并发用户模拟学生提问,每个用户每隔 15 秒发一个问题,持续压 10 分钟。预期目标的服务 QPS 不算高,单机大约 30 QPS 就应该能扛住。但压测一开始,服务监控面板就亮起红灯,数据库连接数快速攀升到 PostgreSQL 默认最大连接数的上限,大量请求报too many clients,检索失败率一度升到 35%。
排查过程大概是这个套路:
第一步,先看日志确认阻塞点。发现报错集中在 pgvector 查询,几乎每个请求都要建立新的数据库连接。
第二步,检查数据库连接池配置。问题立刻浮出水面:连接池最大大小被默认配置成了“未限制”,同时连接池的空闲回收时间设置太长,导致高并发时连接数疯狂膨胀。这不是数据库本身的问题,就是我们应用层连接池配错了。
第三步,现场修复。把连接池最大大小调成 20,最小空闲连接数设为 5,连接空闲回收时间从 30 分钟改到 10 分钟,同时加上连接获取超时时间 3 秒。改完重新压测,数据库连接数稳定在 18 左右,失败率清零。
这个问题的根因说起来很基础,但实际排查所花的时间远比想象中多。原因是监控面板上看不到“连接池队列等待”的指标,一开始我们还以为瓶颈在 Embedding 模型的推理服务上,白查了好一会儿。
4.3 压测后的调优项与最终数据
除了连接池,压测还暴露了两个需要调优的问题。
第一个是 Embedding 服务的单点瓶颈。虽然我们没有在每次提问时都重新生成向量(课件和逐字稿的向量是提前生成好的),但查询改写后的查询向量需要实时计算。最开始用 CPU 跑 Embedding 模型,T4 机器上单次推理要 80ms。后来换成 GPU 部署,并且做了一个“查询向量缓存”,如果问题文本和 5 分钟内某个历史查询文本的编辑距离小于阈值,直接复用已有向量,单次查询耗时降到 20ms,缓存命中率大约 40%。
第二个是逐字稿增量分片时的锁竞争。最开始用 Redis 分布式锁来防止同一个课程会话的分片任务并发执行,但压测时发现锁等待时间会累积,严重时单个分片任务要等 2 秒。后来改成“基于课程会话 ID 的分片任务队列”,同一个会话的任务串行处理,不同会话之间完全并行,等待时间降到基本为零。
这些全部调完之后,最终压测数据如下:
| 指标 | 压测目标 | 实测结果 |
|---|---|---|
| 单机稳定 QPS | 30 | 42 |
| 首 token 延迟(P95) | ≤ 1500ms | 1120ms |
| 上下文检索失败率 | ≤ 0.5% | 0% |
| 数据库连接数上限 | ≤ 30 | 18 |
这个结果基本达到了我们对“跟课上下文服务”的预期,至少在性能瓶颈上,短期内不会再成为短板。
4.4 监控与告警三板斧
最后分享一个运维层面的经验。上下文服务最怕的不是性能差,而是“悄悄变差”——比如某一天的课件格式变了导致分片异常,或者某个课程量特别大导致检索延时上升。如果没有监控,这些问题往往要等大量学生投诉才会暴露。
我们给服务配了三个最有用的监控项:
- 检索耗时 P95 监控:阈值设置为 200ms,超过就是异动。这能快速暴露数据库慢查询、连接池问题。
- 召回为空率监控:每 100 个问题里,如果有超过 5 个问题的检索结果为空或分数极低,立刻告警。这通常意味着课件解析失败、分片为空,或者查询改写模块出了 bug。
- 上下文 token 超预算率监控:如果频繁出现“检索结果太多,被截断丢掉”的情况,说明分片粒度可能需要调整,或者 Top K 参数设得过大。
这三个监控项看起来简单,但每一个都真正救过我们一次。有的团队喜欢一上来就搞特别复杂的可观测性体系,反而容易迷失在无数指标里。先盯住核心链路的几个关键信号,比什么都强。
5. 常见问题与排查技巧实录
5.1 检索出来的内容“看着相关,但不解决问题”
这是 RAG 系统最典型的“虚假相关”问题。比如学生问“矩阵的秩怎么求”,系统检索回来一段文字确实提到了“秩”,但内容是定义而不是计算步骤,模型拿它作答,回答自然浮于表面。
我们的排查技巧是“先看上下文标签,再看 chunk 内容”。具体操作:把检索结果连同它们的元信息(章节标题、页码、时间戳、来源类型)打印成调试日志。如果检索返回的都是“定义型”内容,而问题明显是“操作型”的,那基本可以确定是分片切得太大,一个页级 chunk 里同时包含了定义和例子,检索时被标题词吸引了注意力。解决办法是把页级块进一步细分成“语义子块”,每块控制在 150 字左右,同时给块打上“定义/示例/推导/结论”的标签,查询改写时根据问题类型偏好标签。
5.2 同一节课不同学生问同样问题,答案却不一样
这个现象一度很玄学。后来才发现原因很简单:学生提问时,当前课堂进度不同。前一个学生是在老师讲到定义时就问的,系统只拼了定义上下文;后一个学生在老师讲完例子后才问,上下文里多了例子,回答自然更丰满。严格说这不算 bug,但对产品一致性来说确实是负面体验。
我们采用的处理方案是:当检索命中原有的历史 QA 对时,除了复用上下文组合,还会把历史回答的标准结论作为“先验答案”放进提示词,让模型在不偏离核心意思的前提下,结合最新课堂进度做适度微调。这样既保证了回答的稳定性,又保留了现场感。
5.3 语音转写错字如何识别与纠偏
逐字稿的质量直接影响检索效果。语音转写常出现同音错字,比如“特征向量”被转成“特称向量”,“矩阵”偶尔变成“矩阵(没错,就是字面错)”。如果文本都错了,向量检索很难命中正确内容。
我们做了两层防御:一是在 Embedding 之前,对“领域词库”做一次替换校正,把课程相关的术语列表做成词典,扫描逐字稿时优先纠正词库内的高频错误;二是在检索阶段,把关键词召回(BM25)的结果权重稍微调高一点,因为即便错字,BM25 基于词频的匹配也往往能命中同一段文字里的其他正确词。
5.4 排查技巧速查表
| 症状 | 大概率原因 | 快速排查方法 |
|---|---|---|
| 回答内容与课堂无关 | 当前课次上下文为空,服务回退到了通用知识库 | 查“调用层级”日志,确认是否走了历史/全局层级 |
| 检索耗时突然飙升 | 数据库连接池打满,或 Embedding 服务异常 | 看连接池活跃连接数、Embedding 推理耗时 |
| 回答中提及了“不能确定”的细节 | 逐字稿未及时分片入库 | 查分片任务队列延迟 |
| 多次回答同一问题不一致 | 历史 QA 未命中 | 查 question_key 相似度阈值,可能定得太高 |
这张表贴在我们项目群里,不仅是研发人员用,产品同学遇到用户反馈也能先做个粗筛,省去大量来回沟通的时间。
写在最后:一点心得与可复用的经验
这个项目做下来,我最大的感受是八个字:先有场景,再有技术。跟课上下文服务的技术栈并没有任何“魔术”,向量检索、倒排索引、连接池调优、提示词模板,这些全部是成熟得不能再成熟的东西。真正的难点在于,把课堂这个场景的节奏、进度、连续性理解透,并且映射到技术设计里去。学生问“这个和刚才那个有什么区别”,系统要能自动补全“刚才那个”是哪一节、哪一页、哪句话——这个能力不是靠模型觉醒来实现的,是靠工程一点一点拼出来的。
如果让我给后来者三个最实用的建议,我会说:第一,课堂进度上下文是你最便宜的提效手段,优先用好它。第二,不要把分片当成“切了就行”,切完之后有没有语义边界、有没有时间戳、有没有章节归属,这些元信息决定了检索的天花板。第三,上线上线之前先做压测,连接池、缓存、并发队列这些基础配置,要按真实流量来校对,别等用户卡住了才检查配置。
另外,这个服务目前只是做到了“上下文服务”这一层。再往后走,我们还在琢磨两个方向:一是把“学生对某个知识点的掌握状态”也纳入上下文建模,让回答能根据学生的历史互动做个性化适配;二是做一个“课程知识图谱”层,把知识点之间的关系显式建模出来,让检索能从“关键词匹配”升级到“知识点路径导航”。这两件事都还在原型阶段,等有了阶段性成果,我再写一篇新的复盘分享。