作者:郝子旭(组长 · 后端主干 & 智能体编排)
日期:2026-04-07
本阶段关键词:多智能体编排、WebSocket 实时通信、学科答疑、情绪状态联动、错题复习闭环、分支合并收口


一、本阶段整体情况

3 月 30 日到 4 月 7 日这段时间,我主要在做一件事:把后端从"各模块单独能用"的状态,往"整条业务主链跑得通"的方向推。

上个阶段我们把脚手架、数据库模型、认证、建档、状态中心、会话消息、学习计划这些基础能力搭起来了。但说实话,那会儿整个系统还比较散——用户能注册、能建档、能看计划、也能进聊天页面,可是聊天之后该由谁来处理、处理结果怎么沉淀、业务动作怎么执行、失败了怎么兜底,这些东西都还没接通。各个模块更像是摆在那里等着被串起来。

所以这阶段我给自己定的目标就很直接:先别继续堆新模块了,把已有的东西接成一条完整的链路。

从 git 提交记录看,我这阶段的正式提交主要集中在 4 月 1 日、4 月 3 日和 4 月 6 日,3 月 30 日到 4 月 7 日更多是同一阶段工作的连续推进和收口。比较集中的三次推进如下:

  • 2026-04-01:Agent 编排框架和 WebSocket 实时通信主链落地,同时把错题本、复习调度、跟课笔记的闭环能力补上
  • 2026-04-03:TutorAgent 正式接入 LLM,完成学科答疑和截图答疑全链路;情绪记录服务和低情绪计划联动落地
  • 2026-04-06:和李镇亚的 NiubeeLi 分支做合并收口,修复合并后暴露的事务一致性问题

这三次提交串起来看,做的其实是同一件事:让系统在给出一个回答之后,还能继续往下走——能沉淀记录、能触发动作、能更新状态、能影响后续的计划安排。

这段时间写下来我有一个挺明显的感受:项目做到这个阶段之后,真正费劲的地方已经不是"怎么调模型"了,而是模型接进业务之后,整个系统还能不能保持住工程上的可控性。


二、这一阶段完成的核心任务

1. 多智能体框架落地

这阶段我最先啃的就是多智能体编排这一层。

项目设计上一直是"主助手 + 学习助手 + 生活助手 + 心理助手"四个前台角色,后面对应总控编排、学习规划、学科答疑、生活关怀、心理安抚这些专项能力。这东西如果一直停留在文档里不落代码,项目到最后大概率就退化成"几个按钮切不同 prompt 的聊天界面"了。

所以我先把 Agent 体系的基础骨架定了下来:BaseAgent 抽象基类、AgentContextAgentResponse 统一数据结构、ModelClient 模型调用封装、ModelRouter 模型路由、AgentDispatchService 统一调度入口。

这一层的意义在于:所有 Agent 拿到的上下文、返回的数据结构、动作格式、副作用格式,从一开始就是统一的。这件事看着不算"业务功能",但我认为可能是这阶段技术上最重要的一步。一旦这层没统一,后面每个 Agent 各写各的协议,到联调阶段会非常痛苦。

在统一骨架的基础上,我把 5 个 Agent 和 1 个子路由全部实现了:

  • OrchestratorAgent:主助手编排器,做意图识别和混合意图的串行编排
  • LearningRouter:学习助手内部的二级路由,决定走 planner 还是 tutor
  • PlannerAgent:调用 PlanService 获取日计划,返回计划卡片
  • TutorAgent:学科答疑和动作生成(这阶段唯一真正接入 LLM 的 Agent)
  • LifeCareAgent:生活建议与关怀卡片
  • MentalAgent:情绪安抚和危机表达识别

这里需要说明一个设计选择:当前阶段只有 TutorAgent 接入了 LLM 调用,其余 Agent 全部走规则驱动。 这是我有意为之的。学科答疑场景确实没法用规则穷举,必须依赖模型生成能力;但计划解读、生活建议、情绪识别这些,在当前阶段用规则完全够用,而且规则驱动响应快、行为确定、方便测试。特别是 MentalAgent 那边涉及危机关键词检测,比如"不想活""自杀"这类表达——用硬编码匹配比交给模型判断更可靠,这种场景容不得漏判。

升级路径已经预留好了。所有 Agent 的构造函数里都持有 model_client 引用,build_system_prompt()build_messages() 在基类里实现好了,5 套 prompt 模板也写完了。后面哪个 Agent 需要升级为 LLM 驱动,加一个 model_client.chat() 调用再套一个 fallback 回当前规则逻辑就行。TutorAgent 已经把这条"模型优先、失败降级"的路径跑通了,其他 Agent 可以直接照搬。

关于编排器的设计思路,我的判断是:主助手的职责应该是搞清楚用户想干什么、把请求交给对的人、把结果合并好,而不是自己去生成所有回复。 这个项目真正需要控制的是什么时候该交给学习助手、什么时候优先处理情绪、什么时候该把"加入错题本"的按钮挂出来——这个决策权得握在服务端,不能全靠模型临场发挥。

所以做编排的时候,我把重心放在了统一协议、统一上下文、统一副作用入口上。prompt 可以后面慢慢调,系统边界要是一开始就乱了,后面改起来代价很大。


2. WebSocket 实时通信主链打通

Agent 框架有了之后,下一步自然就是 WebSocket 主链。

聊天系统从前端看就是"用户发消息,助手一点一点流式回来",但后端要处理的环节其实不少:

  1. WebSocket token 鉴权
  2. 一用户多连接管理
  3. 用户消息先持久化
  4. 根据会话和用户状态构建 AgentContext
  5. 按 persona 路由到正确的 Agent
  6. 流式返回 stream_chunk
  7. 最后返回 stream_end 带上完整的持久化消息对象
  8. 记录 agent_route_logs
  9. 失败时保证回滚干净并且日志语义正确

我在这部分花了不少时间,特别是事务边界。

最初的想法是把"用户消息落库、助手消息落库、副作用执行、路由日志写入"全塞进一个事务里统一提交。代码看着简洁,但问题是中间任何一步出错,系统就会进入一种很别扭的状态——比如用户消息丢了、日志没留到、状态更新了一半、助手回复没进库。表面上能跑,一出问题完全没法查。

后来我把链路重新整理成了四步:

  1. 用户消息单独提交——保证用户发过的话一定留得住
  2. assistant message 和 side effect 先 flush 但不提交
  3. route log 写入时统一 commit
  4. 任何环节出异常,先 rollback,再单独补写一条 failed 状态的 route log

这么做的核心考虑是让系统在出错的时候也能保持可解释性。成功的时候大家都觉得没问题,但出错的时候如果连一条用户消息和一条失败日志都留不住,后面排查就只能靠猜。做 AI 后端这段时间,这个体会越来越深——失败路径的设计往往比成功路径更重要。

目前这套链路已经能稳定支撑聊天主流程了。消息进来后系统能按 persona 调度、流式分片返回,结束时直接把完整消息对象推给前端,不需要前端自己拼装。这一点对后面桌面端联调很关键。


3. 错题本、复习调度和聊天动作闭环

WebSocket 解决的是消息怎么进来、怎么出去。错题本和复习调度解决的是这次回答结束之后,系统有没有后续动作。

这个阶段我把这条链路补得比较完整了:错题本服务、复习调度服务、跟课笔记服务、/api/v1/review 四个端点、以及 ConversationService 里 add_wrong_book / add_review / save_note / mark_complete 四个动作的真实执行逻辑。

做的时候我比较在意的一点是,错题和复习得真正跑在学习主链里面,光有数据表不够。

错题去重

我给两类来源的错题做了不同的去重策略:有 question_record_id 的按记录级别严格判重,因为 QuestionRecord 天然有明确主键;手动录入或自由问答来源的,按内容做 10 分钟窗口去重,因为这种场景下用户多点两次按钮就会堆出两条一模一样的错题。两类数据语义不同,去重方式也得跟着分开。

复习调度联动到底

复习调度服务里我落了简化版遗忘曲线规则:普通错题间隔 [1, 3, 7, 14, 30] 天,重复错题用更紧凑的 [1, 2, 3, 7, 14] 天。但比间隔序列更重要的是,复习完成这一个动作要触发一整串联动:review_task 状态更新、wrong_question 掌握度变化(答对 +25 / 答错 -15)、MasteryRecord 记录、下一轮 review_task 调度、关联的日计划 StudyTask 状态同步、以及 pending_review_count 回写到状态中心。

这串联动如果断在中间任何一环,后端数据就会出现不一致。比如复习完成了但掌握度没变,或者掌握度变了但计划里那条任务还显示"进行中"。学习闭环要成立,复习结果就得能真正改写系统状态,不能只是把 review_task 标个"已完成"就结束了。

save_note 落到真实结构里

CourseNoteService 也一起补上了。用户在答疑后点"保存笔记",后端会把内容真正落到课堂笔记表里。考虑到当前没有真实跟课会话的情况,我做了一个兜底:自动创建一个 tracking_mode=chat_action 的合成跟课会话来承接笔记。先让链路成立,后面如果需要更独立的 note 模型再调整。

这部分做完之后,系统开始具备"答完一道题,后面还有后续"的能力了——答疑结果能沉淀为错题、错题能进复习、复习结果能改写掌握度和计划状态。至少在后端层面,单次回答已经能转化成长期的学习资产。


三、4 月 3 日:答疑服务和情绪联动

4 月 3 日那次提交是这阶段的第二个关键节点。前面 Agent 和 WebSocket 解决的是系统有没有"入口",但这个项目到底有没有特色,还要看学习和生活是不是真的联动起来了。


1. 学科答疑:重点在答完能沉淀

QaServiceTutorAgent 的时候,我从一开始就没打算做成"调一下模型、拿一段字符串、原样返回"的形式。那样当然能跑,演示也没问题,但跟整个项目的错题、复习、报告、状态联动全是断开的。

Tutor 输出结构化

我把 Tutor 的 prompt 设计成了固定 JSON 输出格式,要求返回 subjectquestion_typesummarystepsknowledge_tagscommon_mistakesmemory_tipsimilar_suggestion 这几个字段。

理由很直接:只要模型输出还是一大段自由的自然语言,后端就没法稳定提取知识点、错因和动作参数,那"加入错题本""安排复习"这些后续能力就只能靠字符串猜测来做,系统会非常脆弱。结构化输出稳定了,后面整个学习闭环才跟得上。

同时我也做了多层解析兜底:先尝试从返回内容里提取 JSON,提不到就按纯文本分行解析,模型调用本身失败的话走 fallback 规则生成一个通用引导式回答。不管模型返回了什么、甚至模型挂了,前端拿到的 response 结构始终是一致的。

答疑记录统一落库

文本问答和截图答疑都统一沉淀为 QuestionRecord + AnswerAnalysis。这样系统里就不只是"有一条聊天消息",而是有了明确的答疑业务记录。后面不管是用户点动作、做学习复盘,还是生成报告统计,都能直接查结构化数据。

截图答疑工程化

截图答疑这块同时补了 OCR 服务,工程细节上花了一些功夫:图片格式白名单校验、非图片伪装上传拦截(比如把 TIFF 改成 .png 后缀塞过来)、10MB 大小限制、PaddleOCR 初始化失败时标记 ocr_failed 做安全降级。

这些东西单独看都不复杂,但截图问答这种功能,如果只在最理想的环境下能跑那就不算真正落地。OCR 可能装不上、依赖可能初始化失败、图片也可能有问题,系统得能优雅地降级。这段时间做下来有一个越来越强的体会:AI 功能铺得越多,降级设计就越重要。


2. 情绪记录:让生活数据进入决策链路

情绪记录从分工上看偏生活侧,但我做的时候一直把它当学习系统的一部分来考虑。

如果情绪记录只是一个单独页面——用户点一下心情,系统存个分数,画一条趋势图——那它对项目的价值其实有限。能看,但不影响任何东西。

所以 MoodService 我做的时候把重心放在了让情绪数据真正进入状态中心和计划系统:用户提交情绪记录后,recent_mood_scorecurrent_status_label 会同步更新;低情绪(≤3 分)且用户标记了"影响学习"时,系统自动触发下一天计划的柔化,把非复习任务的时长压缩 30%;心理助手对话产生的 record_mood 副作用也不再只改状态字段,而是完整落一条 MoodRecord

做这部分时我一直在想一个问题:在这个项目里,情绪数据应该只是"描述性的"(用来展示),还是"决策性的"(用来影响系统行为)?

我现在的判断是后者。考研不是理想环境下的机械执行——用户的焦虑、疲惫、失眠确实会影响学习节奏。如果系统对这些东西视而不见,那"学习生活协同"说到底就是句空话。

当然这种联动也不能做得太激进。目前的实现里,低情绪计划柔化必须同时满足"情绪分数 ≤ 3"和"用户明确标记影响学习"两个条件才会触发,柔化逻辑走的完全是确定性的规则——按比例缩减时长、保护复习任务、尊重 15 分钟最小值。不是用户随便说一句"有点烦"系统就把计划改得面目全非。

先求稳,后面再慢慢加灵活性。


四、4 月 6 日的合并收口

到 4 月 6 日,系统到了一个绕不过去的节点:我这边的主干开发线和李镇亚在 NiubeeLi 分支上做的学习专项能力,得合回到同一条主线上。

这步比单独写一个服务要复杂不少。合进来的能力包括跟课上下文服务、OCR API、题集与小测、错题本公开 API、学习概览聚合接口、Alembic 增量迁移以及一批专项测试。文件量不小,但我更关注的是合并之后系统还能不能保持一致。

具体来说我在合并过程中重点盯了三个方面:

数据口径统一

比如"待复习数量"到底统计今天到期的还是所有 pending 的、"错题积压"是按 mastery < 60 算还是按未复习状态算、overview 接口里的 plan/review/state 各项统计是不是同一套口径。这类问题如果不在合并时拉齐,后面联调时就会出现每个页面都能展示、但数字彼此对不上的情况——代码没崩,业务语义裂了,反而更难查。

接口协议收口

合并后我逐个检查了新老模块是不是还遵守同一套 ApiResponse 格式、同一套分页结构、同一套消息动作协议、同一套时区语义。前端最怕的不是接口缺,而是"每个接口格式都差那么一点点"。

事务一致性

这也是为什么我在 4 月 6 日晚上又单独补了一次提交。合并后跑测试,发现了一个典型问题:WrongBookService 内部在创建错题后会立即 commit,但如果紧接着 ReviewService 创建首轮复习任务失败了,错题已经落库、复习任务没建成、前端还收到了"成功"响应——一个经典的半完成状态。

这种问题单独看某个 Service 的时候不容易发现,毕竟每个 Service 自己的逻辑都是对的,问题出在它们被串起来调用的时候。我的修法是从事务边界入手:把 WrongBookService.create_from_action 改成支持 auto_commit=False,让外层的 ConversationService.execute_action 统一控制提交时机。创建错题、创建复习任务、同步状态,全部成功后才 commit;中间任何一步失败,整体 rollback。

同样的思路也用在了 QuizServiceadd_wrong_records_to_wrong_book 上——小测里答错的题加入错题本后,也得同步创建 ReviewTask,不然链路就断在那里了。

这次合并让我对一件事的体感更强了:跨模块联动的边界处理,往往比单模块内部的逻辑更容易出问题。

合并过程中的协作

和李镇亚的协调上,因为我们从一开始就共用同一套任务文档和数据模型定义,所以大方向没有分歧。但"大方向一致"不等于"合并没有摩擦"。

合并前我先完整过了一遍他分支上的提交记录和改动文件清单,重点关注两类东西:一是他新增的服务有没有对我已有的服务做侵入式修改(比如 wrong_book.py 他做了大幅扩展,需要确认和我原有的去重逻辑是否冲突);二是新模块的接口格式、错误码、分页结构是不是和现有模块一致。

实际合并时碰到的主要问题有三个:WrongBookService 内部提交时机的差异(他那边默认 auto_commit,我这边在动作链路里需要延迟提交)、复习服务对 pending 状态的统计口径不完全一致、以及 course_context.py 里有一处合并冲突残留没处理干净。这几个问题我在合并当天逐个修复并补了回归测试。

作为组长,这次合并给我的最大收获是:代码 review 不能只看"这个模块自己对不对",更要看它接入系统后会不会破坏已有链路的事务边界和数据口径。 后面如果还有多人并行的情况,我打算把"合并前先跑对方分支的全量测试 + 检查跨模块调用点"作为固定流程。


五、这一阶段的思路变化

1. 开始先画边界再写逻辑

这阶段之前,我拿到一个需求的第一反应通常是"这个功能怎么实现"。做完 WebSocket 链路和复习联动之后,我发现自己的习惯变了:现在拿到需求,第一步是想"这个功能的输入从哪来、输出到哪去、失败了谁负责兜底、它的数据格式和旁边的模块能不能对上"。

说起来好像只是思考顺序换了一下,但实际效果差别挺大的。这阶段做的好几个模块——情绪联动、错题复习闭环、答疑结构化——实现本身都不算特别难,真正花时间的是厘清它们在整个系统里的位置和边界。先把这个想清楚了,写的时候基本不会跑偏;反过来如果一上来就扎进实现细节,写到后面经常发现跟隔壁模块对不上,又得推倒重来。

2. 对"多人并行开发"有了实感

之前分工的时候,我们按模块划得挺清楚:我做主干和 Agent,李镇亚做学习专项,陈哲凡做管理后台和关怀。分工本身没问题,但 4 月 6 日合并的时候我才真正体会到,分工清楚不等于合并顺利。

两个人各自写的代码都能跑,各自的测试也都过,但合到一起之后立刻暴露出好几类问题:事务提交时机不一致(一个提前 commit 了,另一个还没准备好)、同一个字段的统计口径不同、同名参数的默认值不同。这些东西不合并就永远发现不了。

这次经验给我的教训是:接口文档和数据模型对齐只是起点,真正需要对齐的是运行时行为。 下一阶段如果还有并行开发,我打算每周至少做一次轻量合并验证,不等功能全做完再合——小步合并的痛感远小于大步合并。

3. 开始重视"系统不说话的时候在干什么"

做到情绪联动和复习调度这一步之后,我对产品方向有了一个更清晰的判断:这个项目和普通 AI 聊天工具的本质区别,不在于"说话说得好不好",而在于"不说话的时候系统在不在替用户做事"。

用户答错了一道题,过两天系统主动安排复习;用户情绪低了,第二天计划自动变轻;用户跟完一节课,课堂记忆自动沉淀。这些行为发生的时候用户可能根本没在跟系统对话,但正是这些"静默行为"让系统从工具变成陪伴。

这个认知反过来也影响了我对下一阶段优先级的判断——主动关怀调度虽然从代码量上看不算大,但它对用户体验的影响可能比再多写三个 Agent 都大。


六、本阶段个人任务完成情况

模块 完成情况 量化
多智能体框架与统一调度 已完成 5 个 Agent + 1 个子路由 + 5 套 prompt + 统一调度入口落地
WebSocket 实时通信 已完成 ws.py(223 行),支持 token 认证、流式分片、失败回滚
学科答疑与截图答疑 已完成 TutorAgent(427 行)+ QaService(516 行),支持文本 / 截图 / OCR / 多模态
情绪记录与状态联动 已完成 MoodService + 4 个 API 端点 + 低情绪计划柔化
错题本与复习调度 已完成 WrongBookService + ReviewService + 事务一致性修复
跟课笔记动作落库 已完成 CourseNoteService(138 行),含合成跟课会话兜底
学习专项分支合并收口 已完成 与 NiubeeLi 分支合并 + 回归修复 + 事务边界调整
测试覆盖 已完成 新增多份测试文件并补充多组测试用例,全量回归通过

从后端的发展阶段来看,这一阶段我做的事情已经从"基础模块建设"转向了"主线闭环收口"。写单个服务当然重要,但到了这个阶段,更关键的是确保这些服务串在一起的时候还能正常工作。


七、下一阶段重点

接下来一周(4 月 8 日 – 4 月 14 日)我会按优先级推进以下三个方向:

1. 主动关怀调度落地(本周优先)

目前 CareRule 模型和 CareRecord 表都有了,APScheduler 的 tasks 目录还是空的。下一步要把 7 类关怀场景的触发调度、冷却去重、WebSocket 推送这条链路打通。这块对系统的"主动性"很关键——用户不说话的时候,系统也应该能察觉到状态变化并做出反应。计划在本周内完成核心调度逻辑和至少 3 类场景的端到端验证。

2. 课堂记忆生成(本周启动)

CourseContextService 里记忆生成的 hook 注册机制已经有了,但实际的摘要生成逻辑还没实现。下一步要把"跟课会话结束 → 从帧/OCR/笔记数据中提取结构化课堂记忆"这条路径补上,让课堂闭环也能接通。这块依赖 LLM 做摘要,预计先用 TutorAgent 已有的模型调用链路做一版。

3. 继续压实已有链路(持续进行)

现在答疑、错题、复习、计划、情绪这几条线基本打通了,但还有一些边界场景需要继续收紧:动作执行后的前端刷新机制是否统一、更多并发场景下事务是否一致、历史消息和实时消息的结构是否严格一致。进入前后端联调之后,系统质量往往就取决于这些细节。


八、小结

这一阶段结束之后我最直观的感受是,系统终于开始有点"像一个整体"了。

前几周更多是在铺地基,每块地基单独看都能跑。但这段时间我做的事情是把这些地基接成路——把聊天接到 Agent、把 Agent 接到业务动作、把答疑接到错题、把错题接到复习、把情绪接到状态和计划、把不同分支上的学习专项能力合回同一条主线。

研途灵伴如果最终只是一个"会聊天"的工具,那它和市面上的 AI 对话产品没有本质区别。但如果它真的能把学习行为、状态变化和后续安排串起来,那才算开始接近我们想做的那个"陪伴系统"。

这一阶段,我做的就是这件事的后端部分。下一步继续把主链压稳,把还没接上的环路补起来。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐