作者:郝子旭(组长)
日期:2026-05-06
范围:整个 layout 分支,包括已经提交的内容和当前工作区里尚未提交的修改
关键词:界面重构、桌面端体验、Agent 架构、学习状态闭环、AI 协作、代码审查


一、从改界面开始

这个分支一开始叫 layout,听起来像是一次前端页面整理:调一下间距,统一一下深色主题,把几个页面摆得更舒服。真正做下去以后,我才发现它牵出来的远不止布局。

界面上的不协调,很多时候只是表层信号。滚动异常背后可能是 flex 约束没写对;后台看起来“能打开”,背后可能是管理员和普通用户的角色边界没分清;聊天消息偶尔不稳,往往还牵着 WebSocket 最终消息、副作用刷新、前端 store 合并策略这些更深的链路。

上一阶段项目已经完成了多分支合并,主要模块都在:聊天、计划、错题、复习、跟课、饮食、情绪、后台管理、学习状态分析,都能看到基本形态。但“有功能”和“功能形成闭环”之间还隔着很长一段路。按钮能点,不代表数据语义一致;Agent 能回复,也不代表它能在流式输出、异常回退、多模态输入、历史上下文隔离这些场景下稳定工作。

所以回头看,layout 分支真正推动的是项目从“模块能跑”走向“体验一致、逻辑闭合、状态可信”。它表面上从界面开始,最后却落在了系统工程上。

我在这个过程中大量使用 AI,但没有把它当成自动驾驶。更准确地说,我让 AI 分别扮演了三个角色:先做代码阅读助手,帮我快速扫清跨文件关系;再做审查者,尽量挑刺;最后做反方辩手,逼我把每个结论重新拿回代码里验证。AI 给线索,我做判断。哪些该改、哪些只是误报、怎样改才符合项目逻辑,最后都必须回到我自己对系统的理解上。


二、先把布局问题理清楚

最先暴露出来的问题都很直观:深色主题不够统一,聊天区空间感不舒服,管理后台混着用户侧导航,有些页面滚动不稳定,浮窗默认尺寸也不理想。

如果只停留在表面,这些问题很容易被处理成“再调几个颜色”“再补几条 CSS”。我这次先往下追了一层:到底是哪一层约束让页面变得不稳定?

主布局里一个关键点,是内容区在 flex 容器中使用了不合适的高度策略。height: 100% 看起来像是在让内容区占满父容器,但在包含底部状态栏的布局中,它会让内容区没有正确扣除状态栏高度,进而破坏 overflow-y: auto 的滚动边界。最终有效的修法反而很朴素:内容区使用 flex: 1,关键容器补上 min-height: 0

这让我重新确认了一件事:前端布局的核心,除了元素尺寸,还有空间分配。父容器、子容器、滚动容器三者的约束关系如果不稳定,后面再加多少视觉样式都只是补丁。

深色主题也沿用了这个思路。每个组件单独写颜色,短期看很快,长期一定会变成互相覆盖的样式泥潭。我把 global.css 里的颜色重新整理成语义化变量,例如主文字、次级文字、表面背景、边框、悬停态。组件表达的是“我是什么层级的信息”,具体颜色交给语义层承接。以后调整整体视觉时,就不用满项目追硬编码。

管理员后台的处理更像是一次产品逻辑校正。之前管理员登录后,左侧仍能看到聊天、今日计划、错题本等普通用户入口,底部也保留 WebSocket 状态、今日学习时长、导航按钮。技术上它能显示,语义上却很别扭。管理员的身份是系统管理者,进入系统是为了管理用户、Prompt、关怀规则、知识建议和题库内容;建档和学习驾驶舱属于普通用户。

最后我把管理员和普通用户的前端体验分开:管理员只保留后台管理入口,隐藏普通学习导航和底部学习状态栏。这比让管理员也走用户建档流程更合理,因为它尊重了角色边界。


三、聊天页的稳定性问题

聊天页看上去是一组消息气泡,实际复杂得多。用户发送消息后,前端先插入临时消息;WebSocket 开始推流;后端生成最终 assistant message 并落库;stream_end 再把最终消息、actions、cards、refresh targets 发回前端。中间任何一步没对齐,用户看到的结果都可能异常。

比如流式文本和最终文本不一致,按钮不出现,旧请求覆盖新消息,滚动条突然跳一下。单看某个组件,很难解释这些现象;它们本质上属于异步状态机的问题。

我和 AI 审查聊天页时,出现过一个很典型的误判。审查报告认为:stream_end 后前端不再强制刷新消息列表,所以 actions/cards 会永远不显示。这个结论乍看很有道理,我也差点顺着它改。但继续往后端 ws.py 看,就会发现服务端的 stream_end 本身已经携带最终落库的 message;前端收到后会把这个 message merge 进消息列表。因此,assistant message 自身的 actions/cards 并不会丢。

不过这条审查没有白费。它虽然把主结论说错了,却指向了一个更窄、更真实的缺口:如果某个 Agent 以后通过 append_system_note 额外创建一条系统消息,仅靠最终 assistant message 的 merge 就不够了,当前聊天窗口还需要根据 refresh_targets 刷新 conversation。这个潜在缺口后来被补上了。

这件事让我对 AI 审查的价值边界更清楚。AI 很擅长提出“这里可能有问题”,但跨前端 store、WebSocket payload、后端落库协议的判断,它未必一次就能看全。真正可靠的结论,必须沿着数据流走完。

聊天页还有两个工程细节值得记下来。

一个是 Virtuoso 虚拟列表。原先顶部 spacer 放在 Header slot 里,会参与滚动计算,消息少时可能误触发历史消息加载;同时 components 对象在 render 中内联创建,引用不稳定,流式更新时有滚动抖动风险。后来我把视觉间距挪到 CSS padding,把 Virtuoso components 固定成稳定引用,滚动行为才更可控。

另一个是消息请求并发。过去 loadMessages(id)loadMessages(id, { force: true }) 使用不同 request key,可以同时发出。假如旧请求后返回,就可能覆盖新状态。这个 bug 不一定高频出现,但逻辑上确实不严谨。新的方案用 request sequence 解决:每次请求都有递增序列号,只有最新请求有资格写入 store。换句话说,状态更新的裁判从“谁先回来”变成了“谁才是最新请求”。


四、后端 Agent 的几处收口

后端最值得收口的部分,是 Agent 系统。

我现在对项目里 Agent 的定位更明确了:它是学习系统的大脑接口,已经超出了孤立聊天函数的范围。它要生成文本,也要触发副作用;它要回答问题,也要和计划、错题、复习、情绪、课程上下文发生关系。只要进入系统主链路,就必须考虑资源生命周期、输出协议、异常回退和上下文边界。

统一模型客户端

我最在意的是 ModelClient

之前有些服务会在每次请求里新建 ModelClient,而 ModelClient 内部又持有 OpenAI 兼容客户端和 HTTP 连接池。功能上当然能跑,可资源生命周期并不清晰:连接池无法复用,测试时也容易出现 event loop 已经关闭、连接还没释放的警告。

这种问题平时不一定立刻表现成 bug,但它会慢慢消耗系统稳定性。高频模型调用一多,短命连接池、重复初始化、测试隔离问题都会冒出来。

所以我引入了共享的 get_shared_model_client(),让 study_session.pyquiz.pylearning_state.pymeal.py 等服务统一使用同一个模型客户端入口。这样资源边界更清楚,测试时也能统一 reset,不再各自维护一套单例。

处理混合意图的流式输出

Orchestrator 的 mixed intent 是另一个比较隐蔽的点。

用户一句话里可能同时包含计划调整和情绪表达,Orchestrator 会拆给多个子 Agent。原来的风险在于,这些子 Agent 共用同一个 context,而 context 里带着 WebSocket 流式回调。于是子 Agent 的中间输出可能直接流向前端,但 Orchestrator 最终又会把多个结果重新组织成一条回复。这样一来,流式中间态和最终落库消息就可能不一致。

这里容易被误解成“多个 Agent 并发乱序”。实际执行是顺序的,风险集中在输出通道上:子 Agent 直接拿到了面向用户的主流式回调。

我最后的处理是,在 mixed intent 子调用前剥离流式回调,让子 Agent 以非流式方式产出结果,再由 Orchestrator 统一合成和输出。中间推理可以复杂,对外输出必须稳定。这是我从这次修复里提炼出来的设计原则。

图片答疑直接交给多模态模型

图片答疑这里,我也改过一次自己的判断。

一开始我为了增强 Tutor 的图片理解能力,加过 OCR 预处理:用户上传题目图片后,系统先识别文字,再把 OCR 内容附加到上下文里。表面看,这是在“补充信息”,似乎更稳。后来再想,它和当前模型能力并不匹配。

我调用的模型本来就是多模态模型。既然模型可以直接看图,提前 OCR 就多了一层容易出错的中间表示。数学符号、上下标、表格、图形关系、题目排版,这些都可能在 OCR 里被识别错或丢失。一旦错误文本进入 prompt,模型反而会被带偏。

所以我把 Tutor 图片链路改成多模态直传:截图答疑不再先 OCR,也不会把 OCR 结果拼进 prompt,而是直接把图片和用户补充问题交给模型。OCR 仍然可以服务于独立 OCR 接口和跟课记录场景,但它不应该成为 Tutor 理解图片的必经步骤。

这个取舍对我很有启发。工程里不能只追求“多给信息”,还要看链路是否真实、干净。如果中间表示会引入噪声,就应该尊重原始输入。

让 Prompt 模板真正生效

Prompt 模板之前更像后台里的 CRUD 数据。这次我把它接进 Agent 构建 system prompt 的流程:优先读取数据库中启用的模板,读不到再回退到代码默认 prompt。

这样一来,管理后台可以真正调节 Agent 行为,不再停留在配置展示层。与此同时,回退机制必须保留。prompt 模板属于运行时配置,数据库异常、模板缺失、配置错误都不应该让 Agent 完全不可用。默认 prompt 就是系统的安全兜底。


五、把学习功能串起来

除了界面和 Agent,layout 分支还补了很多“之前有表、有规划,但链路没落完”的部分。

学习状态分析新增了快照、历史、建议匹配和手动分析接口。它的价值不在于多展示一个状态标签,而在于让系统能根据计划完成率、复习完成率、错题积压、情绪记录等数据形成阶段性判断,再匹配知识建议或主动关怀。换句话说,系统开始从“记录用户做了什么”走向“根据记录给出反馈”。

课程笔记管理也补上了前端入口。后端已经支持按跟课会话、tracking mode、note type 查询笔记,但没有列表、详情、编辑、删除,用户就感知不到这个功能。现在跟课页面可以完整管理课堂笔记,截图答疑也能结合课程上下文。

复习模块里的 restore_review 看起来只是一个恢复按钮,实质上是状态机一致性问题。已完成的任务不能恢复,已 pending 的任务不该重复恢复,只有 skipped 才是真正需要恢复的状态。恢复原任务时,还要取消顺延出来的镜像任务;否则用户恢复了今天的复习,明天计划里却还统计着那个补做任务,数据语义就乱了。

管理后台也从“能进入”推进到“能管理”。用户列表、用户详情、Prompt 模板、关怀规则、知识建议、题集题目等能力开始接入。更关键的是,管理员和普通用户的边界被明确下来:管理员不建档,不显示普通学习导航,也不显示底部学习状态栏。后台就是后台,要保持管理工具的语义。


六、我是怎么和 AI 一起查问题的

这次开发里,AI 对我帮助最大的地方,是持续制造审查压力。

我先让 AI 阅读当前项目,理解完整逻辑。接着,我把自己刚补完的后端闭环列出来,让它严格审查是否符合约定。它提出的问题包括:复习恢复后顺延任务仍可能被统计、无复习任务被误判为复习不足、课程笔记更新后缓冲区变旧、主动型知识建议只匹配不推送、后台最新状态按 id 而不是业务日期取值。很多结论是成立的,因为这些问题发生在业务闭环里,语法检查看不出来。

后来又有一轮针对 layout 分支未提交代码的审查。那份报告抓到了一些真实问题,比如 ModelClient 生命周期、mixed intent 流式隔离、Virtuoso 引用稳定性、force/initial 请求覆盖风险;同时也有判断过重的地方,例如把 actions/cards 不显示定成高危 regression。

我没有直接照单全收。后来我写了一份 rebuttal,把每条审查意见放回代码路径里重新验证:哪里是误判,哪里是潜在风险,哪里是必须修的真实缺口,都要用数据流和代码来说明。

这个过程很像真实 code review:AI 提出怀疑,我不盲信;我提出反驳,也不能自信过头。最后能站住脚的,是能够解释完整链路的结论。

我觉得这才是 AI 在工程项目里比较健康的用法。它可以扩大观察范围,暴露边界条件,帮我更快发现自己没注意到的地方;系统级判断仍然要由我负责。


七、这次之后我对项目的理解

做完这个分支后,我对整个项目的定位更清晰了。

它已经超出了简单聊天软件或普通计划表的范畴,更像一个围绕学生学习过程运转的桌面 cockpit:聊天是入口,计划是执行面,错题和复习是长期记忆,跟课是实时上下文,情绪和饮食是状态信号,后台是系统调参和内容管理入口。

这样的系统越往后,越需要跳出单个功能看质量。真正决定稳定性的,是几类一致性。

第一,角色一致性。普通用户需要被引导学习,管理员负责管理系统,两者不能混进同一套体验。
第二,状态一致性。计划任务、复习任务、错题记录、状态快照之间要能互相解释,不能一个地方显示恢复成功,另一个地方还在统计取消任务。
第三,协议一致性。WebSocket 传字符串,也承载最终消息、刷新目标、卡片动作和副作用之间的关系。
第四,资源一致性。模型客户端、连接池、定时任务这些运行时资源,都应该有清晰生命周期。
第五,输入一致性。图片就是图片,文本就是文本。如果模型具备多模态能力,就不要为了“看起来更稳”先把图片压扁成可能出错的 OCR 文本。

这些理解比某个单点功能更重要。项目越大,局部补丁越容易互相打架;只有底层原则清楚,功能扩展才不会变成混乱堆叠。


八、现在做到哪一步了

layout 分支目前完成了几类核心收口:

  • 前端深色主题、主布局、聊天页、浮窗、管理后台角色隔离完成了一轮重构;
  • 聊天消息流、历史加载、最终消息 merge、副作用刷新等异步状态问题得到加固;
  • Agent 系统完成了共享模型客户端、mixed intent 流式隔离、语义分析、Prompt 模板运行时读取等改造;
  • Tutor 图片答疑从 OCR 预处理改为多模态直传;
  • 学习状态、课程笔记、复习恢复、后台用户视图等接口和前端入口逐步闭环;
  • 关键路径补充了单元测试和集成测试。

这个分支的意义不止是让页面更好看。我更在意的是,那些“暂时能跑,但以后会出问题”的地方被提前拉出来处理了。

接下来我会继续做合并前验收,重点看后台管理是否足够可用、状态分析建议是否真正可达、聊天流式体验在真实模型下是否稳定,以及前后端字段约定还有没有遗漏。确认这些之后,再把 layout 分支合入主干。

这次最大的收获是:写代码不能停在堆功能,还要不断追问系统的每一条链路是否闭合。AI 能帮我更快看到问题,但项目最终能不能稳,还是取决于我对业务、状态和架构的判断。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐