DebateLab-个人博客(1)后端总体架构与比赛状态机设计
在这一篇博客中,我打算首先把后端整体的框架搭建好,后端是单体 Spring Boot 应用,首先确定下来了项目整体目录安排,由于本次项目涉及到了许多板块和业务,项目内容量较大,如果只是按照单个的controller或者service类来区分的话会导致多业务混在一起,不够清晰,所以我依据不同的业务层面拆分到不同文件夹,不同业务按auth / topic / debate / ai / config / common拆模块,持久层用MyBatis + Mapper XML实现,数据库用 MySQL。
最终目录如下:
本阶段目标
这一阶段我先解决两个问题:
- 完成后端框架的设计,包括springboot框架搭建,项目目录设计,以及jwt登录功能的实现。
- 后端不能只是实现聊天接口,必须能控制整场比赛怎么开始、怎么推进、什么时候轮到用户、什么时候进入评分和复盘。后端必须给出一个明确、可查询、可继续推进的比赛状态。
本阶段完成内容
这一阶段我把后端主骨架搭起来了,重点在三个点。
第一,按业务拆模块而不是把所有controller/service/mapper平铺在一起。现在的职责边界比较清晰:
auth负责注册、登录、JWT 鉴权topic负责论题包和辩位模板debate负责比赛流程、日志、评分、复盘ai负责 Java 和 Python 服务之间的调用
第二,把比赛抽象成状态机。核心阶段放在DebateStep里,不是随便用一个字符串记当前阶段,而是把顺序、阶段名、发言辩位一起固定下来。像下面这样:
publicenumDebateStep{PREPARE(1,"PREPARE","赛前准备",null),AFFIRMATIVE_OPENING(2,"OPENING","正方一辩立论",DebateRole.AFFIRMATIVE_1),NEGATIVE_OPENING(3,"OPENING","反方一辩立论",DebateRole.NEGATIVE_1),AFFIRMATIVE_CROSS_EXAMINATION(4,"CROSS_EXAMINATION","正方二辩攻辩/质询",DebateRole.AFFIRMATIVE_2),NEGATIVE_CROSS_EXAMINATION(5,"CROSS_EXAMINATION","反方二辩攻辩/质询",DebateRole.NEGATIVE_2),AFFIRMATIVE_REBUTTAL(6,"REBUTTAL","正方三辩驳论推进",DebateRole.AFFIRMATIVE_3),NEGATIVE_REBUTTAL(7,"REBUTTAL","反方三辩驳论推进",DebateRole.NEGATIVE_3),AFFIRMATIVE_FREE_DEBATE_MAIN(8,"CONTROLLED_FREE_DEBATE","正方三辩受控自由辩主攻",DebateRole.AFFIRMATIVE_3),NEGATIVE_FREE_DEBATE_MAIN(9,"CONTROLLED_FREE_DEBATE","反方三辩受控自由辩主攻",DebateRole.NEGATIVE_3),AFFIRMATIVE_FREE_DEBATE_SUPPORT(10,"CONTROLLED_FREE_DEBATE","正方二辩受控自由辩补刀",DebateRole.AFFIRMATIVE_2),NEGATIVE_FREE_DEBATE_SUPPORT(11,"CONTROLLED_FREE_DEBATE","反方二辩受控自由辩补刀",DebateRole.NEGATIVE_2),AFFIRMATIVE_CLOSING(12,"CLOSING","正方四辩总结陈词",DebateRole.AFFIRMATIVE_4),NEGATIVE_CLOSING(13,"CLOSING","反方四辩总结陈词",DebateRole.NEGATIVE_4),JUDGING(14,"JUDGING","裁判评分",null),REVIEW(15,"REVIEW","赛后复盘",null),COMPLETED(16,"COMPLETED","比赛完成",null);}之所以选择状态机设计流程是因为辩论流程天然具有阶段性和顺序依赖。后端通过 currentStep、status、currentTurnIndex 和 userActionRequired 这些状态字段,统一约束比赛从赛前准备、各辩位发言,到评分复盘的流转。这样可以避免流程判断散落在前端和接口里,也更方便做日志回放、异常重试和后续扩展。
这样后端也天然知道三件事:
- 当前进行到哪一步
- 这一步有没有发言人
- 下一步应该推进到哪
第三,把“是否需要用户输入”也做成后端控制逻辑。MatchService在推进比赛时会根据当前step和userRole计算userActionRequired,然后通过MatchDetailResponse返回给前端。前端只需要根据返回结果决定显示“继续推进”还是“提交发言”。
当前可展示结果
目前后端已经能跑通的功能:
- 用户注册 / 登录
- 获取论题列表
- 创建比赛
对应的swagger接口截图:
比赛详情接口返回的数据也不是简单的一段文本,而是结构化状态,像这样:
{"currentStep":"NEGATIVE_OPENING","currentPhase":"OPENING","currentSpeakerRole":"NEGATIVE_1","userActionRequired":false,"canAdvance":true,"status":"IN_PROGRESS","currentTurnIndex":1,"availableActions":["ADVANCE","RETRY_CURRENT_STEP"]}遇到的问题与修复
在流程设计的过程中,一开始我不太确定自由辩的流程应该如何进行设计,不知道受控自由辩论具体要从什么层面“受控”,想了几种方案,比如:限制对话字数、限制对话轮数,或者是由agent自己进行判定,但是最后感觉这些方案都不太现实,不方便设计而且并没有很强的实际意义。最后我决定设计成正/反方的二/三辩各自轮流发言一次,这样既可以实现一定程度的自由发挥又能保证流程的可控性,不至于设计太异想天开。
下一阶段安排
架构和状态机骨架定下来之后,下一步就该把数据库表设计落地,这样就可以稳定保存对局、日志和结果数据。之后我准备重点介绍表结构建模是怎么定下来的。
