ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

跟课上下文服务:AI伴学产品的记忆管理中枢设计与实践

跟课上下文服务:AI伴学产品的记忆管理中枢设计与实践 1. 项目背景与核心问题拆解1.1 “研途灵伴”到底想解决什么问题先交代一下背景。研途灵伴这个项目定位是面向考研人群的AI伴学产品。考研复习的场景和平时刷短视频、看公开课完全不一样它有几个非常突出的特点学习周期长通常半年到一年、内容密度高政治、英语、数学、专业课各有各的知识体系、学生状态波动大前期迷茫、中期疲惫、后期焦虑。市面上的通用大模型产品虽然能回答问题但它们不知道你学到哪了、不知道你今天刚听完哪一章、更不知道你昨天做错的题和今天要听的内容之间有什么关联。结果就是学生问出来的问题很“散”AI给的回答也很“散”形不成真正的学习闭环。我做这个项目的核心切入点就是标题里那个词——“跟课上下文”。什么叫跟课上下文说白了就是把“学生当前正在学什么”这件事变成可计算、可维护、可传递的系统状态。比如一个学生正在听张宇的强化班第12讲中间遇到一个极限运算没听懂他随手问AI“这一步为什么能直接等价无穷小替换”如果AI不知道他正听到第12讲的哪一秒、不知道前面讲过什么铺垫那它只能给一个通用解释。但如果有跟课上下文服务AI就能结合“当前课程切片内容 这节课的知识点图谱 学生历史掌握情况”给出针对性回答甚至能主动提示“这个知识点你上周在1800题里错过两次”。这个服务解决的核心痛点有三个。第一消除“问非所学”的割裂感让AI的回答始终贴着学生当下的学习进度。第二让学习数据产生连续性今天的行为能成为明天的上下文。第三把“课程内容”从静态的视频/讲义变成动态的知识载体让每一秒的课程内容都具备被检索、被关联、被追问的能力。1.2 为什么跟课场景需要独立的上下文服务这个项目最开始的形态其实不是独立服务而是把跟课逻辑写在应用层里直接调用大模型接口靠prompt拼接课程信息和用户问题。当时觉得“上下文”嘛不就是把当前视频的标题、章节、字幕文本塞进prompt里让大模型看着回答就行了。但跑了不到一个月问题全暴露了。首先是上下文规模的失控。一节考研数学强化课通常是1.5到2小时字幕文本转出来大概1.5万到2万字符加上课程讲义、知识点图谱、用户历史错题记录一次请求的上下文有可能堆到3万字符以上。先不说token成本很多大模型对超长上下文的注意力衰减非常明显它根本记不住你塞在中间位置的关键信息回答质量直线下降。其次是状态的碎片化。用户听10分钟课暂停问一个问题再继续听5分钟又拉回去重听再问一个相关但不同角度的问题。如果每次请求都是无状态的、重新拼装上下文那服务端根本不知道“用户上一次问的问题是哪一道”“那道题最后有没有解决”“他拉回去重听说明刚才没听懂”。这些信息全部丢失跟课就变成了一次性的问答而不是持续的学习陪伴。还有一个很现实的问题是成本。通用大模型的接口调用是按token计费的如果每个跟课问题都带上两万字符的课程原文一个月下来成本非常吓人。而且考研学生的高峰使用时段高度集中晚7点到11点concurrent请求一上来如果没有独立的上下文服务做缓存、裁剪、优先级调度后端接口的响应时间和费用都会同时失控。所以把“跟课上下文”从应用层里抽出来做成一个独立的服务层不是技术上的洁癖而是这个业务场景天然要求上下文具备“可持久化”“可选择性地注入”“可增量更新”这几个能力。这就像你请了一个私人家教他不可能每次都让你把整本教材重新念一遍给他听他脑子里得有你的学习档案并且能根据你现在的进度判断该调用哪部分记忆。研途灵伴的跟课上下文服务本质上就是在给AI做这个“记忆管理中枢”。2. 整体方案设计与架构选型2.1 服务定位与技术选型这个服务在整体架构里的位置我习惯把它画成三层最上层是应用客户端App/小程序/web中间层是课堂状态服务和问答服务最底层才是大模型引擎和向量数据库。跟课上下文服务属于中间层里最核心的一个基础组件它不直接面向用户但每一个跟课相关的功能——随堂问答、知识点推送、课堂笔记生成、课后复习卡片——都要通过它获取“当前语境”。选型时有几个考量维度。第一是状态存储用什么。一开始考虑过Redis直接存上下文JSON但发现上下文结构太复杂里面有课程切片的引用、知识点ID数组、用户学习轨迹、临时对话记录这种半结构化、需要频繁局部更新的数据用Redis硬存会导致序列化/反序列化的开销非常大。后来改成了PostgreSQL Redis两级结构PostgreSQL存永久性的学习档案和上下文生命周期数据Redis只做热点会话的短时缓存和读写加速。第二是异步任务这块。课程视频的切片、字幕的清洗、知识点的关联这些都属于离线计算不需要在用户请求的链路上实时完成。但跟课回答问题需要“即时感”用户暂停视频后发问最多等他两三秒再久体验就垮了。所以整体采用gRPC同步接口处理实时问答用消息队列Kafka承接异步任务比如“更新学习轨迹”“预计算下一个知识点的关联内容”。第三是检索方案。课程上下文里经常要“找到当前讲解的知识点在历史课程里第一次出现的位置”以及“找到该知识点相关联的已学内容”。这类需求用全文字搜索不够精准必须引入向量检索。我们最后用了主流的向量数据库方案把课程切片和知识点描述做embedding通过余弦相似度召回相关片段再做粗排和精排。考虑到团队维护成本和稳定性向量库选了偏工程化、运维简单的方案没有自己折腾分布式索引。技术栈定的很快Go写核心服务并发性能好、部署简单团队本来就熟Python写离线算法部分主要处理文本切片、embedding、知识图谱构建PostgreSQL存业务数据Redis做热缓存Kafka串起异步流水线向量数据库承担语义召回。这些选型都不是追求新潮全是看在团队规模、维护成本和业务场景下的综合分。2.2 上下文建模的核心思路这是整个项目里我想得最久、也认为最值得复盘的部分。所谓上下文绝不是一个简单的“把最近的聊天记录存下来”的Buffered Memory它必须是分层的、结构化的。我把它拆成四层第一层是基础上下文。包括用户的基本信息考研年份、目标院校层次、报考专业、当前复习阶段基础/强化/冲刺、使用的教材体系比如数学是跟张宇还是汤家凤。这一层是“这个人是谁”全局有效所有问答都需要带上但不怎么变化。第二层是课程态上下文。这是跟课服务的核心。它描述的是“用户此刻正在经历什么”。我定义了一个称为CurrentSession的抽象结构当前课程ID、当前视频的进度位置、所属章节、本节课覆盖的核心知识点ID列表、当前正在讲解的切片索引、本切片对应的字幕文本摘要。这个Session不是静态的用户每次拖动进度条、暂停、播放、倍速切换客户端都会上报心跳服务端实时更新Session状态。第三层是历史学习档案。这里记的是用户的长期数据学过的课程列表、每节课的完成度、历史提问记录及对应知识点、做题正确率曲线、易错点集合。这层的作用是让AI知道“这个人以前学过什么、哪里薄弱”从而在回答时能主动关联历史内容。第四层是即时对话缓冲。也就是当前这次问答会话里最近几轮的对话内容。它和其他三层不同是短时态只在一次会话周期内有效不需要跨天保存。初始化状态时服务端会先从PostgreSQL加载第一层和第三层数据再从Redis或实时消息里恢复第二层状态最后在问答服务启动新会话时创建第四层缓冲。实际效果是一个用户发起跟课提问服务端拼出来的Prompt不再是“一段课程字幕 一个用户问题”而是一个结构化的上下文块用户档案摘要 当前课程切片内容 相关知识点历史记录 最近对话摘要 用户问题。这个结构的好处是大模型能明确区分“这是谁在问”“他在学什么”“他以前哪里不会”“他刚刚问了什么”而不是把一堆文字糊在一起让它自己分辨。2.3 为什么不用“全量历史对话”策略有一个方向我们一开始就排除了把用户所有历史对话全部累积当成上下文喂给大模型。很多做AI陪伴产品的团队喜欢这么干觉得上下文越长越“懂你”。但实际工程里这个方案在长周期学习场景下根本走不通。第一成本不可控考研用户学习半年对话记录可能有几十万token总费用几何级上涨。第二信息密度太低六个月前的对话对当前这节课的问题几乎没有任何帮助反而干扰模型判断重点。第三计算延迟拉高上下文越长首字延迟越高学习场景里用户等不起。我们的思路是“选择性记忆”由服务端根据当前课程态和历史学习档案动态决定哪些信息值得进入上下文。比如用户当前在学“中值定理”相关的课程服务端会自动检索他在历史记录里关于中值定理的提问和错题如果发现三天前他刚问过“拉格朗日中值定理的辅助函数怎么构造”这次回答就会主动带上那条历史。反过来如果用户之前问的是“虚拟语气”这种英语问题跟当前数学课没有任何关联就完全不会进入上下文。这种动态筛选策略既控制成本又保证了信息的相关性。3. 跟课上下文服务的关键实现3.1 课程内容感知与切片跟课服务一切的地基是对“课程内容”的数字化改造。原始课程是视频视频里有画面、有语音、有字幕但这些都不能直接被上下文服务使用。我们做的第一件事是把每一节课制作成结构化的课程切片。切片不是简单按固定时间戳切段而是按“知识点边界”切。比如张宇某节课前20分钟讲概念中间30分钟讲例题后面15分钟讲易错点这中间存在明确的语义边界。我们用两种方式混合判断边界一是字幕文本的语义变化通过embedding计算相邻句子之间的语义相似度如果相似度骤降说明可能进入了新的知识点二是人工标注的核心知识点清单教研团队对每节课预先列出知识点大纲切片时把字幕文本对齐到大纲的ID上。一个完整的切片单元包含切片ID、所属课程ID、章节ID、时间范围startTime/endTime、字幕文本、知识点ID列表、难度系数、切片类型概念讲解/例题演示/易错点提醒/总结回顾。这样切完之后整节课就变成了一条知识链每个切片都和其他切片有前置/后置关系。比如“等价无穷小替换”这个知识点的切片它的前置切片可能是“极限的四则运算”和“常见的等价无穷小公式”后置切片是“泰勒公式展开”。平时用户听课客户端每15秒上报一次播放进度含当前视频时间点服务端根据时间点定位到具体切片并更新Session里的currentSlice。查询的时候就能做到“用户正在看哪一秒上下文就知道他在学哪个知识点”这个秒级感知能力是所有随堂功能的基础。3.2 上下文状态的维护与更新整个服务里最需要小心处理的是上下文状态的并发和一致性问题。用户在听课过程中动作非常频碎拖动进度条、暂停、切后台再回来、倍速切换、重听上一段、突然点进弹出的知识点卡片。这些动作都会触发客户端上报状态变更如果服务端每次都全量更新Session数据库压力会非常大而且容易产生竞态。我当时的方案是“增量更新 事件溯源”。客户端上报的状态事件只有几类PLAY播放、PAUSE暂停、SEEK拖动进度、RATE_CHANGE倍速切换、HEARTBEAT心跳。服务端收到事件后不是去覆盖整个Session而是把事件追加到Redis里的一个短期事件流里然后由事件处理器计算出最新的Session状态。比如用户连续拖动了三次进度条最终停在18分26秒处理器只需要关心最终位置而不需要关心中间路过了哪些点。这样既减少了状态更新次数又保留了用户行为的轨迹后续做行为分析也有数据。Session状态更新还有一个关键点知识点的激活与失活。当用户从切片A跳到切片B如果A和B属于同一个知识点不需要额外处理如果跳到了不同知识点服务端要更新activeKnowledgePoints数组把旧知识点标记为“已离开”可能还要触发一个异步任务把“用户在A切片停留了多久”“是否完整看完”“有没有产生提问”汇总到用户的历史学习档案里。这个沉淀动作是异步做的不会阻塞用户当前的操作。3.3 与上层应用的对接方式跟课上下文服务对所有上层应用暴露的接口不多总共就五个核心RPC第一个是ReportState负责接收客户端上报的播放状态事件这个接口设计成幂等的客户端网络重试不会导致状态错乱。第二个是GetContext上层问答服务在收到用户问题时调用传入用户ID和当前会话ID返回组装好的结构化上下文块。第三个是AskQuestion这是问答服务通过gRPC直接调用上下文服务的“一站式”接口内部封装了“取上下文 → 检索关联历史 → 组装Prompt → 调用大模型 → 返回答案”的完整链路。第四个是GetKnowledgeState用于查询用户对某个知识点的掌握状态支撑知识点图谱页面的渲染。第五个是SyncLearningRecord外部系统如刷题模块把用户做题数据同步进上下文服务更新学习档案。为了让上层应用接入简单所有接口统一走内部网关用标准化的Proto定义各端只要拉取SDK就能使用。QPS高峰期时GetContext是调用量最大的接口因为它被问答、笔记生成、知识点推荐好几个功能复用。为了扛住峰值我们在Redis里给每个活跃Session加了一层本地缓存默认TTL五分钟只要用户还在听课上下文就可以直接从缓存读取几乎零延迟。4. 踩过的坑与问题排查实录4.1 上下文漂移最隐蔽的坑项目上线大概第二周我们就收到一个奇怪的反馈有学生反映AI在回答“这题为什么要讨论a的取值”时给出的是“根据您刚才学习的一元二次方程求根公式……”可他当时正在听的是概率论的课。查了很久才发现这是Session漂移问题。原因出在客户端上报状态时用的是“设备本地时间 课堂ID”但学生同时打开了两个界面比如一边听课一边翻上一节的笔记页两个界面都在上报状态后上报的请求覆盖了先上报的导致Session被错误的课程信息抢占。那段时间正好赶上部分用户跨端登录iPad听课、手机提问问题就更加明显。修复方案是给Session增加一个“场景互斥锁”每个用户同一时刻只允许存在一个正在活跃的课堂Session后激活的Session会先把旧Session置为挂起而不是直接覆盖。同时客户端上报时增加了使用场景字段课中页/笔记页/练习页只有课中页上报的状态才允许修改currentCourse和currentSlice其他页面的请求只能读取上下文不能写入。经过这轮修复上下文漂移的问题基本绝迹。4.2 长视频切片检索延迟超标跟课问答的链路是用户提问 → 取当前切片 → 做相关知识点的向量检索 → 组装Prompt → 调用大模型 → 返回结果。其中向量检索这一步在课程数量到几百节之后开始明显变慢p95从原来的800ms涨到了2.3秒。整个链路跑下来用户侧经常超过5秒这在学习场景里已经属于不可接受了。排查发现向量库里的集合没有做分区所有课程的切片全部混在一起。我们做的优化是“课程维度预过滤 知识点标签倒排粗筛”查询时先根据当前Session里的课程ID放入白名单再结合切片的知识点ID做倒排粗筛只对命中的几十个候选切片做向量精排。这样检索量从原来的几万条降到了几十条检索耗时直接压到了200ms以内。另外切片粒度大的课程还会在建索引时做一次子切片拆分让召回结果和用户当前位置的匹配更精确。4.3 大模型回答中的知识断层问题上线一段时间后我们发现即使上下文拼装得再精细大模型偶尔还是会出现“知识断层”它引用了上下文里没提到的概念。有一次我拿一个学生的真实问题做测试他在听线性代数“特征值与特征向量”这节问“为什么特征值之和等于矩阵的迹”AI回答时突然插入了一句“正如您之前学过的对角化定理”但该学生的历史档案里根本没有系统学过对角化定理的相关内容。这个问题出在组装Prompt时没有显式约束大模型的引用边界。后来我们在Prompt模板里加了一段强引导你只能引用本次提供的上下文信息不得假设用户掌握未出现在上下文中的知识点如果问题涉及前置知识请先从上下文的knowledge_history字段中确认用户是否学过并明确标注“这需要前置知识XXX”。这看起来是个很傻的修复但实际效果立竿见影胡编乱引的比例降了八成。还有一个更细的点是上下文里有冲突信息时的处理。比如用户学习档案里显示“已完成第8讲”但实际课程进度只有第5讲用户可能是跳着看的。这时如果AI看到档案就默认用户掌握第8讲的内容就会产生误导。我们增加了“可信度标记”从实时Session里拿到的进度视为最高可信从历史档案里拿到的进度则标记为“历史记录、可能存在跳跃”并要求大模型遇到不一致时以实时上下文为准。这类微调单独看都是小事合在一起就是体验和幻觉的巨大差距。4.4 常见问题速查表如果你也要做类似的服务下面这几条排查经验可以直接存下来做参考。Session数据错乱优先检查前端是否在多页面并发上报、后端是否做了互斥锁。上下文内容过于陈旧检查缓存的TTL设置是否合理建议实时类数据TTL不超过5分钟。向量检索结果相关度差先看是否做了课程维度的预过滤再查切片的embedding是否按知识点边界切分。Prompt上下文太长导致超时检查是否把历史全量塞进去了要改成结构化筛选后注入。高峰期大模型调用激增除了限流还要做上下文级别的缓存同一个切片同一个用户短期内相同的问题直接命中缓存。5. 复盘心得与改进方向跟课上下文服务从立项到稳定运行我最大的体感是这类AI产品做得好不好拼的不是模型的聪明程度而是“围绕模型搭的那一圈基础设施”够不够细致。模型是发动机跟课上下文服务就是油箱、是仪表盘、是减震系统。发动机马力再大油箱漏油、仪表盘数据失真车子照样开不远。几个值得沉淀的经验我简单列一下。第一上下文服务建模时先想清楚状态分层全局信息、课程态、历史档案、即时会话这四类要严格分开不要混在一个JSON里不然后期维护和排查会让你痛不欲生。第二客户端的状态上报必须带着明确的场景标识否则会出现跨页面互相覆盖的竞态问题我们为此吃过亏。第三向量检索一定要做预过滤一上来就全局向量的方案在数据量增长后会是很痛的瓶颈。第四Prompt里的知识边界约束必须写死AI的学习建议宁可保守一点也不要给出用户根本没学过的“前置知识”。后续规划里有几个方向我觉得值得继续做。一是把当前基于单次Session的上下文升级为跨天的“连续学习档案”让学生第二天打开App时AI能知道“你昨天学到第几节、哪个点卡住了、建议今天从哪开始”。二是增加上下文服务对多模态的支持现在课程里除了字幕还有板书截图和老师的圈画动作这些视觉信息如果能转化为上下文的一部分跟课问答的准确度会再上一个台阶。三是把上下文服务做成可配置化让教研团队可以不改代码直接在后台编排不同科目、不同课型的上下文策略毕竟政治课和数学课的跟课逻辑差别很大。最后分享一个小技巧也是我们踩过几次坑之后总结出来的上下文服务上线前一定要准备一套“状态回放工具”。它能根据用户的操作日志完整回放某个时刻的Session状态、上下文内容和实际返回答案。没有这套工具排查问题基本靠猜有了它很多诡异的问题一眼就能定位。做这类AI中间层服务的同学建议优先把这件事做扎实。
返回列表