智学文枢:题目流水线

题目流水线:pipeline / queue / stage 三段式

一句话摘要:智学文枢把"AI分析→AI讲解→AI追问→变式训练→习题报告"这一串重操作拆成 stage(阶段状态)、pipeline(单题执行)、queue(批量调度)三个职责单一的服务,用状态机做断点续跑、用后台线程防卡 UI、用队列做串行批处理。

一、引言

一个 AI 私教工作台,最核心、也最重的体验是什么?是"学生录一道错题,系统自动给出完整的学习闭环":先 AI 分析这道题为什么错,再生成一段老师口吻的讲解,接着抛出首轮追问让学生开口,然后产出三道变式题巩固,最后把这一切打包成一份 PDF 报告。

这五个环节都依赖大模型 API 调用,单题耗时动辄十几秒到几十秒。如果我们按传统思路写一个"大函数"串起来,会遇到三个根本问题:

  1. UI 卡死:API 请求是阻塞的,在主线程里跑,界面直接假死,学生只能瞪着转圈。
  2. 失败即重来:第 4 步变式生成时网络抖动,前面 3 步的成果全部作废,学生还得重跑一遍。
  3. 批量无调度:学生可能一口气录入十几道错题,谁先谁后、怎么去重、怎么汇报进度,全都得有人管。

从第一性原理看,这三个问题分别是并发模型 问题、状态管理 问题、任务调度 问题。解法也对应得很清晰:用 QThread 把重活挪出主线程;用"阶段状态机"记录每道题推进到哪一步,实现断点续跑;用一个队列服务把"单题流水线"串成"批量队列"。于是就有了本项目题目自动处理的三段式架构:question_stage_service.py(阶段)、question_pipeline_service.py(流水线)、question_queue_service.py(队列)。

二、核心原理

2.1 Stage:把"进度"变成可持久化的状态

一道题从录入到出报告,经历 6 个状态:none → analysis → lecture → followup → variant → report(源码位置:services/question/question_stage_service.py L9)。每个阶段完成就调用 mark_stage 把状态写进数据库的 status 字段。关键设计是只向前推进、绝不回退------因为 AI 生成的结果一旦落库就不会重做,状态只是"里程碑"而非"缓存"。

2.2 Pipeline:单题的五个环节执行器

PipelineWorker(QThread) 是单题的执行单元。它在 run() 里循环 PIPELINE_STAGES,逐个调用 _do_analysis / _do_lecture / _do_followup / _do_variant。每次进入 run() 都会先读数据库里的当前状态,计算出起始索引,从而实现"上次跑到哪、这次从哪续"。

这里有一个非常微妙的工程约束:前 4 个阶段可以放子线程,第 5 个"报告 PDF 生成"必须在主线程 ,因为它依赖 QWebEngine 渲染。所以 run() 跑到 report 阶段时并不执行,而是发出 report_required 信号把活交给主线程(UI),自己结束。这是一次"线程边界"的显式割裂,代价是多一次信号跳转,收益是避免了 Qt 的跨线程 GUI 崩溃。

2.3 Queue:把单题变成批次

AutoQueueService 运行在主线程,维护一个标准库 queue.Queue 加一个去重集合 _pending_ids。空闲时从队首取一道题,new 一个 PipelineWorker 子线程执行;子线程完成或失败后,回调 _on_item_end 再触发 _kick_if_idle 取下一道。这样整个批次严格串行、一次只跑一道题,既避免了并发 API 请求失控,也让进度汇报天然有序。

三者的关系可以用一张架构图概括:
#mermaid-svg-A2CwafgjDCJTd2cX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A2CwafgjDCJTd2cX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A2CwafgjDCJTd2cX .error-icon{fill:#552222;}#mermaid-svg-A2CwafgjDCJTd2cX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A2CwafgjDCJTd2cX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A2CwafgjDCJTd2cX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A2CwafgjDCJTd2cX .marker.cross{stroke:#333333;}#mermaid-svg-A2CwafgjDCJTd2cX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A2CwafgjDCJTd2cX p{margin:0;}#mermaid-svg-A2CwafgjDCJTd2cX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-A2CwafgjDCJTd2cX .cluster-label text{fill:#333;}#mermaid-svg-A2CwafgjDCJTd2cX .cluster-label span{color:#333;}#mermaid-svg-A2CwafgjDCJTd2cX .cluster-label span p{background-color:transparent;}#mermaid-svg-A2CwafgjDCJTd2cX .label text,#mermaid-svg-A2CwafgjDCJTd2cX span{fill:#333;color:#333;}#mermaid-svg-A2CwafgjDCJTd2cX .node rect,#mermaid-svg-A2CwafgjDCJTd2cX .node circle,#mermaid-svg-A2CwafgjDCJTd2cX .node ellipse,#mermaid-svg-A2CwafgjDCJTd2cX .node polygon,#mermaid-svg-A2CwafgjDCJTd2cX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-A2CwafgjDCJTd2cX .rough-node .label text,#mermaid-svg-A2CwafgjDCJTd2cX .node .label text,#mermaid-svg-A2CwafgjDCJTd2cX .image-shape .label,#mermaid-svg-A2CwafgjDCJTd2cX .icon-shape .label{text-anchor:middle;}#mermaid-svg-A2CwafgjDCJTd2cX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-A2CwafgjDCJTd2cX .rough-node .label,#mermaid-svg-A2CwafgjDCJTd2cX .node .label,#mermaid-svg-A2CwafgjDCJTd2cX .image-shape .label,#mermaid-svg-A2CwafgjDCJTd2cX .icon-shape .label{text-align:center;}#mermaid-svg-A2CwafgjDCJTd2cX .node.clickable{cursor:pointer;}#mermaid-svg-A2CwafgjDCJTd2cX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-A2CwafgjDCJTd2cX .arrowheadPath{fill:#333333;}#mermaid-svg-A2CwafgjDCJTd2cX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-A2CwafgjDCJTd2cX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-A2CwafgjDCJTd2cX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A2CwafgjDCJTd2cX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-A2CwafgjDCJTd2cX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A2CwafgjDCJTd2cX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-A2CwafgjDCJTd2cX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-A2CwafgjDCJTd2cX .cluster text{fill:#333;}#mermaid-svg-A2CwafgjDCJTd2cX .cluster span{color:#333;}#mermaid-svg-A2CwafgjDCJTd2cX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-A2CwafgjDCJTd2cX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-A2CwafgjDCJTd2cX rect.text{fill:none;stroke-width:0;}#mermaid-svg-A2CwafgjDCJTd2cX .icon-shape,#mermaid-svg-A2CwafgjDCJTd2cX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A2CwafgjDCJTd2cX .icon-shape p,#mermaid-svg-A2CwafgjDCJTd2cX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-A2CwafgjDCJTd2cX .icon-shape .label rect,#mermaid-svg-A2CwafgjDCJTd2cX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A2CwafgjDCJTd2cX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-A2CwafgjDCJTd2cX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-A2CwafgjDCJTd2cX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} enqueue
取队首/串行
阶段1
阶段2
阶段3
阶段4
report_required 信号
mark_stage
status_changed
UI 层: 题目列表页
AutoQueueService 队列服务
PipelineWorker 单题流水线子线程
AI分析

ScienceAnalyzer
AI讲解

TeachingEngine
AI追问

ConversationManager
变式训练

VariantEngine
主线程生成报告PDF
Stage 状态机

写回 questions.status

三、代码实现

3.1 Stage:只向前的状态推进

mark_stage 的核心逻辑只有十行:先查当前状态,只有目标阶段更靠后 才写库(源码位置:services/question/question_stage_service.py L22-L47):

python 复制代码
def mark_stage(question_id: str, stage: str):
    """推进习题到指定阶段(只向前,不回退)"""
    if stage not in STAGE_ORDER:
        logger.warning("[QuestionStage] 未知阶段: %s", stage)
        return
    try:
        with QuestionRepository() as repo:
            q = repo.get_by_id(question_id)
            if not q:
                logger.warning("[QuestionStage] 习题不存在: %s", question_id)
                return
            current = q.status or "none"
            if current not in STAGE_ORDER:
                current = "none"
            if STAGE_ORDER.index(stage) > STAGE_ORDER.index(current):
                repo.update_status(question_id, stage)
                logger.info("[QuestionStage] %s: %s -> %s", question_id, current, stage)
    except Exception as e:
        logger.error("[QuestionStage] 推进失败 question=%s stage=%s: %s",
                     question_id, stage, e)

STAGE_ORDER.index() 是天然的可比序:"analysis" < "lecture" < ... < "report"。即便并发或异常导致重复调用,也只会把状态往终点推,不会倒退。

3.2 Pipeline:断点续跑的主循环

PipelineWorker.run() 是流水线的总控(源码位置:services/question/question_pipeline_service.py L126-L188),节选如下:

python 复制代码
def run(self):
    total = len(PIPELINE_STAGES)
    try:
        with QuestionRepository() as q_repo:
            question = q_repo.get_by_id(self._question_id)
        if not question:
            self.error.emit(self._question_id, "习题不存在")
            return
        current_stage = self._get_current_stage(question)
        start_idx = self._next_stage_index(current_stage)
        if start_idx >= total:
            self.progress.emit("全部已完成", total, total)
            self.finished.emit(self._question_id)
            return

        for i in range(start_idx, total):
            if self._check_stop():
                return
            stage = PIPELINE_STAGES[i]
            label = STAGE_LABELS.get(stage, stage)
            self.progress.emit(label, i + 1, total)
            if stage == "analysis":
                self._do_analysis(question)
            elif stage == "lecture":
                self._do_lecture(question)
            elif stage == "followup":
                self._do_followup(question)
            elif stage == "variant":
                self._do_variant(question)
            elif stage == "report":
                # 报告PDF生成依赖QWebEngine,必须在主线程执行。
                self.report_required.emit(
                    question.id, self._student_id, self._student_name)
                return
            self.stage_completed.emit(question.id)
        self.progress.emit("完成", total, total)
        self.finished.emit(self._question_id)
    except Exception as e:
        self.error.emit(self._question_id, str(e))

注意三处细节:一是 _check_stop() 放在每轮循环开头,保证 5 阶段内任何时刻都能响应"停止";二是 report 阶段发信号后 return,本线程到此结束,后续由主线程接管;三是每完成一个阶段就 stage_completed.emit,让 UI 刷新该题的状态列------这正是"阶段进度实时可见"的来源。

3.3 Queue:串行调度器

队列的核心是 _kick_if_idle(源码位置:services/question/question_queue_service.py L90-L113),它同时承担"开跑"和"续跑"两个职责:

python 复制代码
def _kick_if_idle(self):
    """当前空闲时取出队首题目开始处理"""
    if self.is_processing:
        return
    if self._pending.empty():
        if not self._current_qid:
            self.all_finished.emit()
        return
    qid = self._pending.get()
    self._pending_ids.discard(qid)
    self._current_qid = qid
    self._status_map[qid] = STATUS_PROCESSING
    self.status_changed.emit(qid, STATUS_PROCESSING)

    worker = PipelineWorker(qid, self._student_id, self._student_name)
    worker.progress.connect(self._on_stage_progress)
    worker.report_required.connect(self._on_report_required)
    worker.finished.connect(lambda q=qid: self._on_item_end(q, STATUS_DONE))
    worker.error.connect(self._on_item_error)
    worker.stopped.connect(lambda q=qid: self._on_item_end(q, STATUS_STOPPED))
    self._worker = worker
    worker.start()

入队时调用 _kick_if_idle() 立刻检查是否空闲(空闲则立即开跑),一道题结束时 _on_item_end 再次调用它(自动续跑下一道)。_pending_ids 与 _pending 双结构配合:集合用于 O(1) 去重,队列保证 FIFO 顺序。
SQLite PipelineWorker(子线程) AutoQueueService 题目列表页(UI) SQLite PipelineWorker(子线程) AutoQueueService 题目列表页(UI) #mermaid-svg-Fiqqrz2AhagNznpG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fiqqrz2AhagNznpG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fiqqrz2AhagNznpG .error-icon{fill:#552222;}#mermaid-svg-Fiqqrz2AhagNznpG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fiqqrz2AhagNznpG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fiqqrz2AhagNznpG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fiqqrz2AhagNznpG .marker.cross{stroke:#333333;}#mermaid-svg-Fiqqrz2AhagNznpG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fiqqrz2AhagNznpG p{margin:0;}#mermaid-svg-Fiqqrz2AhagNznpG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fiqqrz2AhagNznpG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Fiqqrz2AhagNznpG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Fiqqrz2AhagNznpG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Fiqqrz2AhagNznpG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Fiqqrz2AhagNznpG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Fiqqrz2AhagNznpG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Fiqqrz2AhagNznpG .sequenceNumber{fill:white;}#mermaid-svg-Fiqqrz2AhagNznpG #sequencenumber{fill:#333;}#mermaid-svg-Fiqqrz2AhagNznpG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Fiqqrz2AhagNznpG .messageText{fill:#333;stroke:none;}#mermaid-svg-Fiqqrz2AhagNznpG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fiqqrz2AhagNznpG .labelText,#mermaid-svg-Fiqqrz2AhagNznpG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Fiqqrz2AhagNznpG .loopText,#mermaid-svg-Fiqqrz2AhagNznpG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Fiqqrz2AhagNznpG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Fiqqrz2AhagNznpG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Fiqqrz2AhagNznpG .noteText,#mermaid-svg-Fiqqrz2AhagNznpG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Fiqqrz2AhagNznpG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fiqqrz2AhagNznpG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fiqqrz2AhagNznpG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fiqqrz2AhagNznpG .actorPopupMenu{position:absolute;}#mermaid-svg-Fiqqrz2AhagNznpG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Fiqqrz2AhagNznpG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fiqqrz2AhagNznpG .actor-man circle,#mermaid-svg-Fiqqrz2AhagNznpG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Fiqqrz2AhagNznpG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop5个阶段 enqueue(q1) / enqueue(q2)取q1, 启动子线程mark_stage 推进状态progress / stage_completed 信号report_required 信号主线程生成报告PDF结束, _kick_if_idle 取q2status_changed 刷新进度列

四、设计权衡 / 踩坑记录

权衡 1:为什么报告阶段要"踢"回主线程? 最初确实想把 5 个阶段全部塞进子线程,但 QWebEngine 的 printToPdf 不允许在工作线程调用(会直接崩溃或静默失败)。权衡后选择:前 4 个 AI 阶段(纯网络 + 数据库 IO)放子线程,PDF 渲染放主线程。代价是 PipelineWorker 与 UI 之间多了一次 report_required 信号握手,但换来的是 QWebEngine 的安全性。这是一个"功能正确性优先于代码整洁"的典型取舍。

踩坑 1:状态机必须幂等。 早期版本每个阶段开始前不查状态,直接从头跑,学生中途关软件再打开,所有题都要重做。后来引入"读当前状态 → 计算 start_idx"的断点续跑后,mark_stage 又必须做到"重复调用无副作用",否则并发 emit 会写乱状态。最终 STAGE_ORDER.index(stage) > index(current) 的判断同时解决了这两个问题。

踩坑 2:停止语义要区分"停当前题"和"清队列"。 stop_current() 只停正在处理的题,排队的不受影响;clear_pending() 只清空排队列表,不打断正在跑的。如果混在一起,用户点了"停止"却发现队列全没了,或者点了"清空"当前题还在跑,体验都很怪。分开实现后每个按钮的语义都自洽。

权衡 2:为什么不用 Python 的 concurrent.futures? 因为本项目 UI 是 PySide6,信号槽机制天然跨线程安全,QThread + Signal 是 Qt 生态的标准做法:进度条更新、按钮禁用/启用、状态列刷新都能靠信号自动回到主线程执行,避免了手动锁和 QMetaObject.invokeMethod 的样板代码。

踩坑 3:组合题(子题)需要携带题干文本。 流水线的各个 AI 引擎读的是 question.question_text,但子题的题干存在父记录里。_merge_parent_text(源码位置:services/question/question_pipeline_service.py L307-L321)在 run() 开头就把父题干合并进子题文本,否则分析引擎只看到"【子题】求第(2)问",毫无上下文。

五、小结

  • 题目自动处理被拆成三个正交的服务:stage 管状态 (只向前推进、可持久化、断点续跑)、pipeline 管单题执行 (子线程跑 4 个 AI 阶段 + 信号交接报告阶段)、queue 管批量调度(FIFO 串行、集合去重、空闲即开跑)。
  • 线程边界是明确的:子线程只碰网络与数据库,所有 Qt GUI 操作(包括 QWebEngine 出 PDF)都在主线程。
  • 状态机是整条链路的"账本",配合 mark_stage 的幂等性,即使程序中途崩溃,重启后也能从断点继续。
  • 这一设计让"一键完成五步学习闭环"从演示功能变成了可以长时间挂着跑批的可靠能力,也是后续批量处理、队列进度展示等所有 UI 功能的骨架。
相关推荐
梯度下降者2 小时前
零基础入门Python 爬虫
爬虫·python·数据分析·入门指南·webcrawler
“AI国潮设计-小江”3 小时前
【AIGC实战】Python+Stable Diffusion打造“英歌舞财神”IP,解锁“马到成功”等爆款情绪营销文案设计
开发语言·人工智能·python·prompt·aigc
benchmark_cc4 小时前
用 Python 拉 A 股全市场实时快照:Notebook 请求、结果校验与易错点
python·数据分析·pandas·量化交易·股票数据·quantdash
伞伞悦读4 小时前
【第70期】Adam二阶矩不是方差:偏差校正、动量与AdamW完整实验
python
外收内放5 小时前
Python基础语法练习题(73)
java·前端·python
锋行天下5 小时前
Langgraph HITL 中断运行,用户输入后输出打字效果
人工智能·python
时早玖沐6 小时前
给 AstrBot 写一个天气推送插件:12 次提交、6 个坑
python·开源·ai编程
触底反弹6 小时前
🚀 FastAPI 五大重难点深度剖析:从 500 报错到彻底搞懂 Depends、Pydantic 与 SQLAlchemy
python·fastapi
cpolar技术支持6 小时前
网关加 gzip,同时开启 chunked,下载回来的包解不开:把两层编码的解压顺序与长度核对做成只读页
python·http·网络编程·socket·内网穿透