Files
Auto-Finance/.agents/docs/guides/系统事件流全景图.md
2026-06-15 10:39:01 +08:00

23 KiB
Raw Blame History

系统事件流全景图

最后更新: 2026-06-15 用途: 排查 SSE 事件问题、提交流程中断、状态不一致等 Bug


一、核心概念

1.1 前后端状态映射

前端 App.processState 后端 AgentState 含义
idle IDLE 初始状态,等待用户操作
processing EXTRACTING LLM 正在分析文件
awaiting_supplement AWAITING_SUPPLEMENT 信息不完整,等待用户补充
submitting SUBMITTING 正在提交到财务系统
ready READY 信息完整,可以提交
submitting SUBMITTING 正在提交到财务系统
done DONE / ERROR 流程结束(成功或失败)

1.2 通信机制

sequenceDiagram
    participant F as 前端
    participant S as SSE连接
    participant B as 后端线程

    F->>B: POST /api/agent/process/:sid
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid (SSE长连接)
    S-->>F: message: file_progress (轮询 file_events.log)
    S-->>F: message: llm_stream (轮询 llm_stream.log)
    S-->>F: message: agent_* (轮询 agent_events.log)
    S-->>F: message: done (检测到 result.json)
    S->>S: 连接关闭

关键约束

  • 后端所有处理接口均返回 {status: "started"},实际工作在 daemon 线程中执行
  • SSE 通过每 0.5 秒轮询 4 个日志文件实现(非原生 SSE是长轮询模拟
  • result.json 的原子写入:先写 .tmp,再 replace() 重命名
  • SSE 超时600 秒后自动断开

1.3 操作信号点清单

每个 API 操作涉及的信号文件生命周期如下。新增或修改信号文件时必须同步更新此清单

序号 操作 API 端点 线程启动时清理 一次写入且不被清理 轮次结束时写入
1 初始处理 POST /api/agent/process/:sid llm_stream.log, agent_events.log, result.json file_events.log, session.log result.json
2 补充文件 POST /api/agent/supplement/:sid llm_stream.log, agent_events.log, result.json file_events.log, session.log result.json
3 文字补充 POST /api/agent/user-supplement/:sid llm_stream.log, agent_events.log, result.json file_events.log, session.log result.json
4 强制提交 POST /api/agent/force-submit/:sid llm_stream.log, agent_events.log, result.json file_events.log, session.log result.json
5 手动财务提交 POST /api/submit-financial/:sid llm_stream.log, agent_events.log, result.json file_events.log, session.log result.json

信号点说明

信号文件 读/写方 生命周期 作用
result.json 后端线程写入SSE 端点读取 每轮开始时删除,finally 块中原子写入 SSE 检测到该文件即发射 done 事件并断开连接
agent_events.log Agent 调度器追加写入SSE 端点读取 每轮开始时删除Agent 运行时持续追加 传递 agent 状态变化事件给前端
llm_stream.log LLM 回调追加写入SSE 端点读取 每轮开始时删除LLM 运行时持续追加 传递 LLM 流式输出给前端
file_events.log pipeline_web 追加写入SSE 端点读取 会话内持续追加,不删除 传递文件处理进度给前端
session.log sse_handler 追加写入SSE 端点读取 会话内持续追加,不删除 传递普通日志行给前端

1.4 关键约束(修改代码前必读)

约束 1result.json 必须在每轮线程启动时删除

SSE 端点通过检测 result.json 是否存在来判断任务是否完成。如果上一轮的 result.json 残留SSE 会立即读到旧数据并发射 done 事件,导致前端断开连接,新任务的消息无法送达。

  • 实现位置:_run_agent_task()try 块开头
  • 删除时机:在 install_log_collector() 之后、task_fn() 执行之前
  • 写入位置:finally 块中统一写入(唯一写入点)
  • 写入规则:finally 始终执行原子写入,不再有条件判断
  • _emit_ready_and_submit 只返回 result 字典,不写入文件

约束 2result.json 的写入必须使用 finally

无论任务成功或失败SSE 端点都需要 result.json 来发送 done 事件。如果仅在成功路径写入,异常时 SSE 会一直轮询直到 600 秒超时,前端无反馈。

约束 3SSE 新建连接时,当前文件偏移必须从 0 开始

_run_agent_task 在启动时删除 llm_stream.logagent_events.log,确保 SSE 重新建立连接后从 0 偏移开始读取。如果文件不被删除,旧的事件会被重复发送给前端。

约束 4前端 SSE 连接的生命周期

  • 前端在每次 POST 请求返回 {status: "started"} 后立即创建新的 SSE 连接
  • 收到 done 事件后关闭连接
  • 旧的连接引用必须清理(agent.js 中的 agentEventSource
  • 如果前端在 POST 之前就创建了 SSE 连接,会读到旧数据

二、场景一:用户提交材料 → LLM 分析完整 → 直接提交

2.1 时序图

sequenceDiagram
    participant F as 前端
    participant S as SSE连接
    participant B as 后端线程
    participant A as Agent调度器

    F->>F: startProcess()
    F->>B: POST /api/agent/process/:sid
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    Note over B,A: 后台线程启动
    B->>A: extract_invoices()
    S-->>F: file_progress (processing/done)
    Note over S: 轮询 file_events.log

    S-->>F: llm_stream (start/chunk/end)
    Note over S: 轮询 llm_stream.log

    S-->>F: agent_state_change (state=extracting)
    Note over S: 轮询 agent_events.log

    Note over A: _do_extraction_with_validation()<br/>LLM提取 → validator校验<br/>最多3次重试

    S-->>F: agent_state_change (校验通过/未通过)

    Note over A: can_submit == true

    A->>A: state → READY
    A->>A: _emit_agent_event (agent_ready)
    A->>A: _emit_ready_and_submit()
    A->>A: run_financial_submit()

    S-->>F: agent_ready

    Note over B: 写入 result.json

    S-->>F: done (携带 result)
    Note over S: 检测到 result.json

    F->>F: es.close()
    F->>F: App.processState = 'done'
    F->>F: addChatMessage(成功)
    Note over B: remove_log_collector

2.2 事件流清单

序号 事件类型 来源文件 触发时机 前端处理
1 file_progress file_events.log 每个文件处理开始/完成 更新文件状态 UI
2 llm_stream llm_stream.log LLM 流式输出 显示聊天气泡
3 agent_state_change agent_events.log 状态变为 extracting 显示瞬态状态提示
4 agent_state_change agent_events.log 校验通过/未通过 更新瞬态状态
5 agent_ready agent_events.log 双重校验通过 done 事件统一处理
6 done SSE 检测到 result.json 流程结束 根据 result 判断终态

2.3 result.json 结构(成功路径)

{
  "ok": true,
  "agent_ready": true,
  "submit_ok": true,
  "round": 1,
  "message": "信息完整,已自动提交到财务系统"
}

三、场景二:用户提交材料 → 需补充 → 用户上传文件

3.1 时序图

sequenceDiagram
    participant F as 前端
    participant S as SSE连接
    participant B as 后端线程
    participant A as Agent调度器

    Note over F,A: 阶段1: 初次分析
    F->>B: POST /api/agent/process/:sid
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    S-->>F: agent_state_change (state=extracting)

    Note over A: can_submit == false

    A->>A: state → AWAITING_SUPPLEMENT

    S-->>F: agent_request_supplement

    Note over B: 写入 result.json<br/>(waiting_for_supplement=true)

    S-->>F: done
    F->>F: es.close()
    F->>F: App.processState = 'awaiting_supplement'
    F->>F: showStatus('请补充')
    F->>F: showAgentRequest()
    Note over B: remove_log_collector

    Note over F,A: 阶段2: 用户上传补充文件
    F->>F: 用户点击"上传补充材料"
    F->>F: 文件上传完成
    F->>F: handleSupplementUpload(newFilenames)
    F->>B: POST /api/agent/supplement/:sid<br/>{files: [...]}
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    Note over B: 新后台线程启动

    A->>A: add_supplement()<br/>(记录文件名, 发射收到事件)

    S-->>F: agent_supplement_received

    A->>A: extract_invoices()<br/>(重新提取所有文件)
    A->>A: run_agent_round(new_files=[...])
    Note over A: 加载上一轮结果作为<br/>previous_analysis

    S-->>F: agent_state_change (state=extracting)

    Note over A: LLM提取 → 校验循环

    alt 分支A: 补充后仍不完整
        S-->>F: agent_request_supplement
        S-->>F: done (waiting=true)
        F->>F: es.close()
        F->>F: App.processState = 'awaiting_supplement'
    else 分支B: 补充后完整
        Note over A: can_submit == true
        A->>A: state → READY
        A->>A: _emit_ready_and_submit()
        S-->>F: agent_ready
        S-->>F: done (submit_ok=true)
        F->>F: es.close()
        F->>F: App.processState = 'done'
        F->>F: addChatMessage(成功)
    end

3.2 事件流清单(补充文件路径)

序号 事件类型 来源文件 触发时机 前端处理
1 agent_supplement_received agent_events.log 收到补充文件列表 显示"已收到补充文件"
2 agent_state_change agent_events.log 开始重新分析 显示瞬态状态
3 agent_request_supplement agent_events.log 仍不完整 更新补充请求面板
4 agent_ready agent_events.log 校验通过 done 统一处理
5 done SSE 检测到 result.json 流程结束 判断终态

3.3 result.json 结构(需补充)

{
  "ok": true,
  "agent_ready": false,
  "agent_state": "awaiting_supplement",
  "round": 1,
  "waiting_for_supplement": true
}

四、场景三:用户提交材料 → 需补充 → 用户通过对话提供信息

4.1 时序图

sequenceDiagram
    participant F as 前端
    participant S as SSE连接
    participant B as 后端线程
    participant A as Agent调度器

    Note over F,A: 阶段1: 初次分析
    F->>B: POST /api/agent/process/:sid
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    S-->>F: agent_request_supplement
    S-->>F: done (waiting=true)
    F->>F: es.close()
    F->>F: App.processState = 'awaiting_supplement'
    Note over B: remove_log_collector

    Note over F,A: 阶段2: 用户输入文字
    F->>F: 用户在输入框输入文字
    F->>F: handleUserSupplement()
    F->>B: POST /api/agent/user-supplement/:sid<br/>{text: "..."}
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    Note over B: 新后台线程启动

    S-->>F: agent_supplement_received

    A->>A: process_user_text_supplement()
    A->>A: process_user_supplement()<br/>(LLM解析用户文字)

    S-->>F: llm_stream (解析过程)

    A->>A: merge_supplement_into_info()<br/>(合并到 extracted_info)
    Note over A: 保存到缓存文件

    A->>A: run_agent_round()<br/>(重新校验)

    S-->>F: agent_state_change<br/>(state=extracting, 正在重新校验)

    alt 分支A: 补充后仍不完整
        S-->>F: agent_request_supplement
        S-->>F: done (waiting=true)
        F->>F: es.close()
        F->>F: App.processState = 'awaiting_supplement'
    else 分支B: 补充后完整
        Note over A: can_submit == true
        A->>A: state → READY
        A->>A: _emit_ready_and_submit()
        S-->>F: agent_ready
        S-->>F: done (submit_ok=true)
        F->>F: es.close()
        F->>F: App.processState = 'done'
        F->>F: addChatMessage(成功)
    end

4.2 事件流清单(文字补充路径)

序号 事件类型 来源文件 触发时机 前端处理
1 agent_supplement_received agent_events.log 收到用户文字 显示"已收到补充"
2 llm_stream llm_stream.log LLM 解析用户文字 显示解析过程
3 agent_state_change agent_events.log 开始重新校验 显示"正在重新校验"
4 agent_request_supplement agent_events.log 仍不完整 更新补充请求
5 agent_ready agent_events.log 校验通过 done 统一处理
6 done SSE 检测到 result.json 流程结束 判断终态

五、强制提交流程

5.1 时序图

sequenceDiagram
    participant F as 前端
    participant S as SSE连接
    participant B as 后端线程

    F->>F: handleForceSubmit()
    F->>F: App.forceSubmitting = true
    F->>B: POST /api/agent/force-submit/:sid
    B-->>F: {status: "started"}
    F->>S: GET /api/logs/:sid

    Note over B: 后台线程启动

    B->>B: force_submit()<br/>(state → READY)

    S-->>F: agent_force_submit

    B->>B: run_financial_submit()

    Note over B: 写入 result.json

    S-->>F: done
    F->>F: es.close()
    F->>F: App.forceSubmitting = false
    F->>F: App.processState = 'done'

六、错误处理路径

6.1 错误场景和事件

错误场景 后端行为 发射事件 前端表现
LLM 提取异常 state → ERROR agent_error 聊天显示错误,processState → 'done'
规则校验 3 次失败 返回最后一次结果,继续语义判断 agent_state_change 依赖 can_submit 字段决定
轮次超限 (5 轮) state → ERROR agent_max_rounds 聊天显示错误,可强制提交
财务提交失败 result.submit_ok = false 无独立事件 done 事件携带错误信息
SSE 连接中断 es.onerror 触发 显示"连接中断"
超时 (600s) SSE 轮询循环退出 连接自然断开 连接断开

6.2 agent_error 事件结构

{
  "type": "agent_error",
  "message": "LLM 提取失败: ..."
}

6.3 agent_max_rounds 事件结构

{
  "type": "agent_max_rounds",
  "message": "已达到最大轮次 (5),请检查信息或强制提交"
}

七、状态机完整图

7.1 后端 AgentState 状态机

stateDiagram-v2
    [*] --> IDLE

    IDLE --> EXTRACTING: POST /api/agent/process\nPOST /api/agent/supplement\nPOST /api/agent/user-supplement

    EXTRACTING --> READY: can_submit == true
    EXTRACTING --> AWAITING_SUPPLEMENT: can_submit == false
    EXTRACTING --> ERROR: 异常 / 轮次超限

    READY --> SUBMITTING: _emit_ready_and_submit()
    SUBMITTING --> DONE: 财务提交完成

    AWAITING_SUPPLEMENT --> EXTRACTING: 用户补充文件/文字

    READY: 准备提交\n(终态保护)
    SUBMITTING: 财务提交中\n(终态保护)
    DONE: 终态\n(终态保护)
    ERROR: 错误状态\n(可强制提交)

    note right of EXTRACTING
        LLM 提取 + validator 校验\n最多 3 次重试
    end note

7.2 前端 processState 状态机

stateDiagram-v2
    [*] --> idle

    idle --> processing: startProcess()

    processing --> awaiting_supplement: done事件\nresult.waiting_for_supplement
    processing --> done: done事件\nresult.ok

    awaiting_supplement --> processing: 补充文件或文字
    awaiting_supplement --> submitting: 强制提交

    submitting --> done: done事件

    done: 流程结束
    idle: 初始状态
    processing: 处理中
    awaiting_supplement: 等待补充
    submitting: 提交中

八、SSE 事件类型完整参考

8.1 Agent 事件 (agent_events.log)

事件类型 数据结构 触发条件
agent_state_change {type, state, round, attempt, message} 状态切换
agent_ready {type, round, message} 双重校验通过
agent_request_supplement {type, round, missing_fields, missing_materials, semantic_issues, suggestion} 校验未通过
agent_supplement_received {type, files} 收到用户补充
agent_force_submit {type, message} 用户强制提交
agent_error {type, message} 提取失败
agent_max_rounds {type, message} 达到最大轮次

8.2 文件进度事件 (file_events.log)

事件类型 数据结构 触发条件
file_progress {type, file, status, summary?, error?} 文件处理状态变更

status 取值: processing / done / cached / error

8.3 LLM 流式事件 (llm_stream.log)

事件类型 数据结构 触发条件
llm_stream {type, phase, text?} LLM 输出流

phase 取值: start / reasoning / chunk / end / error

8.4 完成事件 (SSE 直接发送)

事件类型 数据结构 触发条件
done {type, result: {...}} result.json 出现

九、常见问题排查清单

9.1 SSE 事件丢失

症状: 前端没有收到预期的 agent 事件

排查步骤:

  1. 检查 agent_events.log 是否存在、是否有内容
  2. 检查 SSE 连接是否建立成功(浏览器 Network 面板)
  3. 确认 sse_handler.install_log_collector() 是否被调用
  4. 确认 remove_log_collector() 是否过早调用

9.2 提交流程中断

症状: 流程在某个中间状态卡住,没有 done 事件

排查步骤:

  1. 检查 result.json 是否被写入
  2. 检查后台线程是否异常退出(查看 session.log
  3. 确认 finally 块中的 result.json 写入逻辑是否执行
  4. 检查是否触发了 600 秒超时

9.3 状态不一致

症状: 前端 processState 和后端 AgentState 不匹配

排查步骤:

  1. 对比 agent_events.log 中的状态变更序列
  2. 检查前端是否正确处理了 done 事件
  3. 确认 SSE 连接是否在适当时机关闭和重建
  4. 检查 App.agentEventSource 引用是否正确清理

9.4 补充流程不触发

症状: 用户上传补充文件或输入文字后,没有重新分析

排查步骤:

  1. 确认 processState 是否为 awaiting_supplement
  2. 检查补充 API 是否返回 {status: "started"}
  3. 检查新 SSE 连接是否成功建立
  4. 确认 add_supplement()process_user_text_supplement() 是否被调用

9.5 补充材料后前端无任何消息(result.json 残留问题)

症状: 第二轮及之后的补充材料提交后,前端完全没有任何消息显示,状态栏不更新,聊天区无新增消息。后台日志显示处理正常完成。

根因: _run_agent_task 在每轮启动时未清理上一轮的 result.json。SSE 端点轮询时立即检测到旧的 result.json,直接发射 done 事件并关闭连接,前端断开后无法接收新任务的消息。

排查步骤:

  1. 检查 session 目录中 result.json 的修改时间 — 如果早于当前轮次开始时间,说明是残留文件
  2. 检查浏览器 Network 面板中 SSE 连接 — 是否在建立后立即收到 done 事件
  3. 确认 _run_agent_task 是否在启动时清理了 result.json

修复: 在 _run_agent_tasktry 块开头同时清理 llm_stream.logagent_events.logresult.json 三个文件。

详细记录: 参见 .agents/docs/error-experience/2026-06-15-补充材料SSE立即读到旧result.json导致前端无消息.md


十、关键文件索引

文件 职责
src/web/static/js/process.js 主提交流程入口SSE 事件分发
src/web/static/js/agent.js Agent 事件处理,补充/强制提交逻辑
src/web/static/js/state.js 全局状态管理
src/web/routes.py 后端路由,后台线程启动
src/agent/orchestrator.py Agent 调度器,状态机,校验循环
src/web/sse_handler.py SSE 日志收集器
src/web/pipeline_web.py 发票提取管道,财务提交

十一、文件生命周期与操作信号点

11.1 单轮处理的完整文件生命周期

sequenceDiagram
    participant API as 路由层
    participant RT as _run_agent_task
    participant TF as task_fn
    participant SS as _emit_ready_and_submit
    participant SSE as SSE 端点

    Note over API: 1. 创建 handler
    API->>API: install_log_collector(session_dir)
    Note over API: 创建 SSE 日志收集器<br/>随即开始写入 session.log

    API->>RT: threading.Thread(target=_run_agent_task)

    Note over RT: 2. 清理残留文件
    RT->>RT: unlink(llm_stream.log)
    RT->>RT: unlink(agent_events.log)
    RT->>RT: unlink(result.json)

    Note over RT: 3. 执行任务
    RT->>TF: task_fn(session_dir, config)

    Note over TF: 执行期间各个文件由对应模块写入:
    TF-->>TF: file_events.log (pipeline_web)
    TF-->>TF: llm_stream.log (LLM 回调)
    TF-->>TF: agent_events.log (Agent 调度器)

    TF-->>RT: 返回 (agent_session, 占位 result)

    alt 成功路径 (READY)
        RT->>SS: _emit_ready_and_submit()
        Note over SS: 发射 agent_ready 事件<br/>执行财务提交<br/>返回 result 字典
        SS-->>RT: result 字典
        Note over RT: result = {...}
    else 需补充路径 (AWAITING_SUPPLEMENT)
        Note over RT: result = {waiting_for_supplement: true}
    else 异常路径
        Note over RT: result = {ok: false, error: ...}
    end

    Note over RT: 4. finally 块 — 唯一写入点
    RT->>RT: 原子写入 result.json (.tmp → replace)

    RT->>RT: remove_log_collector(handler)

    Note over SSE: 5. SSE 端点检测
    SSE->>SSE: 轮询检测到 result.json
    SSE-->>SSE: 发射 done 事件
    SSE->>SSE: break 退出轮询

11.2 各阶段信号文件状态

阶段 result.json llm_stream.log agent_events.log file_events.log session.log
会话创建 不存在 不存在 不存在 不存在 不存在
install_log_collector 不存在 不存在 不存在 不存在 开始写入
_run_agent_task 清理后 已删除 已删除 已删除 保持 保持
文件提取中 不存在 不存在 不存在 持续追加 持续追加
LLM 提取中 不存在 持续追加 持续追加 保持 持续追加
校验中 不存在 保持 持续追加 保持 持续追加
任务完成 (READY) 已写入 保持 保持 保持 保持
任务完成 (需补充) 已写入 保持 保持 保持 保持
任务异常 已写入 保持 保持 保持 保持
SSE done 事件后 保持 保持 保持 保持 保持

11.3 新增信号文件检查清单

当需要在系统中新增一个信号文件(如 submit_progress.log)时,必须检查以下事项:

  1. 写入方:哪个模块负责写入?写入时机是什么?
  2. 读取方SSE 端点是否需要轮询?前端是否需要处理?
  3. 清理时机:是否需要在 _run_agent_task 中清理?如果不需要,为什么?
  4. 原子性:写入是否需要 .tmp + replace 模式?
  5. 轮询偏移SSE 端点是否需要跟踪该文件的读取偏移?
  6. 更新本文档:在 1.3 操作信号点清单中新增一行,在 11.2 文件状态表中新增一列
  7. 更新 _run_agent_task:如果需要清理,在清理循环中添加文件名
  8. 更新前端:在 sse.jsagent.js 中添加对应的事件处理器