# report_bioflow_dev2 —— P1 生信服务地基(开发 agent A2) > 任务书:docs/team/brief_bioflow_dev2_p1.md > 工作分支:feature/bioflow-conversation-orchestration(保持,不切换) > 纪律:不 commit/push/部署;不碰生产服务(8090/8091);服务层零 pydantic;零硬编码 ## 1. 交付概览 | 模块 | 说明 | |---|---| | bioflow_service/(新增,独立部署树) | 生信服务 8098:FastAPI + 独立 SQLite + docker 执行器 + 状态机 + 规划器 | | sdk/app/routers/bioflow_proxy.py(新增) | BFF 流式反代 + 报告登记端点 | | sdk/app/main.py(2 行改动) | BIOFLOW_ENABLED 开关注册反代路由 | | sdk/app/config.py(2 键) | bioflow_enabled(默认 false)/ bioflow_service_url | | docs/team/report_bioflow_dev2.md | 本报告 | ## 2. 模块清单(bioflow_service/app/) **明文白名单(编译管线保留 .py)** - `config.py` —— BIOFLOW_ 前缀配置(pydantic-settings):image_map_path(仓库内 bioflow_kernel/stage_image_map.json,按 __file__ 推导 repo 根)、data_root/ jobs_dir/db_path/archives_dir/flow_docs_dir(全部可覆盖,无写死宿主路径)、 concurrency=3、stage_timeout_sec、host_uid_env=BIOFLOW_HOST_UID(默认服务进程 os.getuid() 推导)、container_data_dir=/bioflow-data、upload 白名单、DOCGEN_LLM_* 沿用 docgen 直连模式;bba_env 属性从配置推导 BBA_PFAM_DIR/BBA_RAG_MAPPING_FILE。 - `schemas.py` —— pydantic 请求/响应模型(ChatStreamRequest/JobCreateRequest/ StageConfirmRequest/ArchiveCreateRequest 等 10 个;STAGE/JOB 状态 Literal)。 - `db.py` —— 独立 SQLite(WAL)三表 + 幂等迁移(CREATE IF NOT EXISTS + PRAGMA table_info 增量列):bio_jobs/bio_stages/bio_archives(字段与实施计划 §六对齐); bio_stages UNIQUE(job_id, stage),job 删除级联。 - `main.py` —— FastAPI 组装 + lifespan(init_db→init_context→recover→shutdown); health 端点 /api/bioflow/health(db 连通状态字段,FR-BIO-025 探活用)。 **服务层(全部 dataclass/dict/stdlib,无 pydantic,Cython 编译)** - `services/bioflow/kernel.py` —— Docker 执行器:读 A1 stage_image_map.json (75 阶段/8 镜像),按阶段→镜像拼 docker run(--user uid/三目录挂载/BIOFLOW_DATA 只读/-e BBA_*/--cpus/--memory/--rm + `python -B -m cli.run_{stage}`,list 无 shell); cpu/memory 白名单回落;超时 terminate→kill;日志落任务 logs/ + 尾 80 行截断; 跨环境 note 透传(编排层 P2 拆分依据)。 - `services/bioflow/runner.py` —— 任务状态机:created→await_inputs→running→ paused/done/failed/archived;阶段 waiting→await_files→waiting_confirm→queued→ running→done/failed/skipped;输入检查(FR-BIO-005)→确认门(FR-BIO-008,confirm `_confirmed` 旁路防二次确认门)→并发信号量(BIOFLOW_CONCURRENCY 默认 3, FIFO 排队)→docker→manifest 判成败(no_manifest/manifest_failed/timeout/ exit_code 四因);pause 停阶段边界、resume 不跳过确认、rerun/skip 状态校验; 重启恢复 recover():running 阶段按 manifest 判成败(有 success manifest→done 否则重跑)、done 不重跑(attempt 不变)、archived 跳过。 - `services/bioflow/catalog.py` —— 阶段目录(映射表驱动 + STAGE_INFO/STAGE_DEPENDENCIES 静态快照,不运行时反射);73 阶段(真实表 75 去重复校验)、基因家族链 21 阶段、 手写参数 schema(identify 11 参数含 default/source/help,source 标注 contract/user/runtime 供表单默认值来源可见);validate_chain 未知阶段拒绝 + auto_fill_prerequisites 前置补全(拓扑排序依赖在前)。 - `services/bioflow/planner.py` —— 路径规划:复用句式(「用 XXX 存档任务的方法…」) → archive 直接装载;否则知识库检索(vstore/flow.py FlowStore 本地实例混合检索, 失败降级 llm_only)+ LLM 候选(DOCGEN_LLM_* 直连,JSON 模式 + 修复重试)→ catalog 硬校验(未知阶段拒绝、前置补全);PLAN_LLM_TIMEOUT/PLAN_UNAVAILABLE 降级内置兜底链。 - `services/bioflow/manifests.py` —— manifest 解析校验(manifest_v1.schema 必需字段; 定位 final/{prefix}.{stage}.manifest.json,无兜底防跨阶段误配);跨阶段输入定位 (artifacts full_path 解析 + 存在性检查)。 - `services/bioflow/events.py` —— 事件总线(12 类事件全集 + job_recovered): 进程内 pub/sub(SSE 订阅)+ 内存环形缓冲 + events/{job_id}.jsonl 持久 (重启回放)+ snapshot 首帧 + replay(after_seq) 断线回放。 - `services/bioflow/llm.py` —— LLM 直连(docgen 同款:指数退避 + JSON 修复 + LLMTimeout/LLMUnavailable 分型)。 - `services/bioflow/params.py` —— 参数预填(契约文档参数解析 + BBA_* 默认值标注 + 存档参数预填)。 - `services/bioflow/reports.py` —— 报告登记(报告 HTML 定位/fig:// 图清单提取/ 登记载荷(job_id/标题/md/图清单)+ 内部 HTTP 调 BFF 反代端点,BFF 不可达降级)。 - `services/bioflow/archive.py` —— 存档打包(final/结果+logs/过程+events.jsonl 过程记录+input 原始文件;include_intermediate 开关含中间文件;zip 下载)。 - `services/bioflow/store.py` —— 三表 CRUD(upsert/list/get/delete;归档精确优先 模糊匹配、reused_count 计数)。 - `services/bioflow/intent.py` —— 意图分流(cases_bioflow_intent.md 冻结词典同表: export→edit→docgen→附件 docgen→bioflow_qa(FLOW_QA 命中即拦)→bioflow (动词+家族语境词/分析对象)→qa 漏判安全;复用方法名提取 3-20 字符; >2000 字符截断判定)。 **路由** - `routers/bioflow.py` —— health/catalog(?stage=/?chain=gene_family)/ chat/stream(SSE:意图分流→plan_ready 候选→心跳;plan seq 编码 data 载荷直出)/ ask(知识库引用块:source/title_path/path/cite/line/text)。 - `routers/biojobs.py` —— jobs 全套(POST 201/列表 rowid 倒序分页过滤/详情/删除 running 409);files(multipart 流式逐块写盘 + 扩展名白名单 + 大小上限 413 + server_path 登记限 data_root/任务目录白名单 403);stages confirm(参数校验 未知 422)/rerun(带参数→waiting_confirm)/skip/pause/resume/manifest(manifest 对象 + 日志尾 80 行);events SSE(snapshot 首帧 + 回放 + 实时订阅 + 任务终态 关闭 TC-BIO-API-081);report revise(最小接口:无图目标 422/绘图阶段定位)。 - `routers/bioarchives.py` —— archives 全套(存档 201 重名 409/列表/详情/删除 磁盘+行/run 复用/按名匹配 404 ARCHIVE_NOT_FOUND/download zip/report HTML); reports/register(归一化 + 图清单回填,BFF 反代落 doc_artifacts)。 **构建与运维** - `build.sh` —— 复制 tooling/compile/compile_sdk.sh 改白名单(明文:main.py/ config.py/schemas.py/db.py + __init__.py 规则;schemas= pydantic cyfunction 教训、 db= 断流崩溃根治教训);16 模块 Cython+GCC 全过(实测 /tmp 输出 16 .so, 编译树 import OK)。 - `restart.sh` —— 8098 重启(零硬编码:路径脚本位置推导 + 环境变量覆盖)。 - `tests/` —— pytest(-m bioflow_unit 127 用例 / -m bioflow_api 39 用例)。 ## 3. 自测结果(2026-09-06,服务器 venv 3.12.13) | 命令 | 结果 | |---|---| | `python -m pytest tests/ -q` | **166 passed**(15.7s,0 warnings) | | `python -m pytest tests/ -q -m bioflow_unit` | 127 passed | | `python -m pytest tests/ -q -m bioflow_api` | 39 passed | | build.sh → /tmp 编译 + 编译树 import | 16 模块 .so,import OK | | BFF BIOFLOW_ENABLED=false/true 双态 import | 均 OK(反代路由 2 条) | | 真实 stage_image_map.json 加载 | 75 阶段/8 镜像/identify 11 参数/命令拼装正确 | | /home/xzq 硬编码扫描(app+tests) | 0 处 | 全部 mock docker/LLM/vstore(tmpdir + fake 执行器),不连任何生产服务 (8090/8091/8094/8098 从未探测)。 ## 4. 已知限制(交 TL 裁决) 1. **SSE 传输层测试法**:本环境 fastapi TestClient 的 sync stream 包装与 httpx ASGITransport 对永不结束的 SSE 流均存在首帧不达/整流缓冲问题(dbg3/dbg6 最小 复现实证),SSE 用例改为端点直调迭代 body_iterator + 模块级共享 event loop (anyio portal 绑定首个 loop,跨 loop 死锁实证)。生产部署(uvicorn 真实网络 SSE)不受影响;如 TL 要求 TestClient 原生流测试,需另行排查 fastapi/anyio 版本组合。 2. **catalog 阶段数口径**:任务书口径 73;A1 交付 stage_image_map.json 实为 75 阶段(含 module=null 的 synteny_plot_revise 与 executable=false 的 msa/ enrichment)。kernel 对 module=null 阶段拼命令前置拦截(UnknownStageError), catalog 全量返回 75。以 A1 交付物为准,口径差异记录备查。 3. **gene_family 链为 21 阶段**(A1 family_chain 实测 21,任务书口径约 18); 以 stage_image_map.json family_chain 字段为唯一事实源。 4. **FlowStore 生产数据未入库**:vstore/data/flow_docs 目录当前不存在(A4 自测 在 tmpdir 完成;真实语料 106 文件/724 块需 TL 部署时跑 vstore/scripts/ingest_flow_docs.py 入库)。planner 已按本地实例对接(加载成功 零块时检索空 → LLM 候选照常,降级链完整);生产前入库即可获得 KB 增强 (llm_rag source)。 5. **chat/stream 是规划入口**:plan_ready 后保持心跳连接,任务创建走 POST /jobs(P1 契约);会话级任务事件经 /jobs/{id}/events 订阅。若 TL 要求 chat/stream 单流贯穿任务事件(A3 前端形态),需 P2 加会话级订阅联动。 6. **报告登记 BFF 侧 doc_artifacts 落库**:bioflow_proxy.register_report 字段 (job_id/session_id/标题/md_path/图清单)已按约定实现;图清单字段当前存于 请求体(doc_artifacts 表无 figures 列,如需持久化图清单需 TL 裁决加列——已 在 payload 透传中保留)。与 A3 report 模板的 fig:// 渲染协调点:报告 HTML 内 fig://id 引用清单由生信服务 reports.extract_figure_refs 提供。 7. **意图词典判定为启发式快照**:与 cases_bioflow_intent.md 冻结表逐条对齐 (40 用例表全过);家族名语境词规则(「SPL 会议」不命中)为词典实现注意第 1/4 条的工程化解读,最终同表双跑一致性以 TC-BIO-INTENT-047(L2 用例)验收。 8. **事件持久化目录**:events/{job_id}.jsonl 与任务目录分离(bioflow_data/ jobs/events/);归档时打包进档案根。任务删除不清理事件文件(留审计), 如需一并删除交 TL 裁决。 9. **镜像 tag 后缀**:docker_image_tag_suffix 默认 "dev"(A1 交付物 :dev); 发布流水线(P2 FR-BIO-032 compile_bioflow.sh)需按终态 tag 覆盖环境变量。 10. **pytest.ini python_files 陷阱**:本仓库 pytest 9.1.1 下 `python_files = ["test_*.py"]`(列表形式)会导致目录级收集 0 用例(单文件显式传参正常), 已用字符串形式 `python_files = test_*.py` 规避并实证;写报告备案防踩。 ## 5. 交付物清单 | 文件 | 说明 | |---|---| | bioflow_service/app/{config,schemas,db,main,context}.py | 应用骨架(4 明文 + context) | | bioflow_service/app/services/bioflow/*.py | 服务层 12 模块(零 pydantic) | | bioflow_service/app/routers/{bioflow,biojobs,bioarchives}.py | 端点三层 | | bioflow_service/build.sh / restart.sh | 编译白名单管线 / 重启脚本 | | bioflow_service/pytest.ini | markers + python_files(字符串形式陷阱规避) | | bioflow_service/tests/*.py | 10 文件 166 用例 | | sdk/app/routers/bioflow_proxy.py | BFF 流式反代 + 报告登记 | | sdk/app/main.py(+2 行)/ sdk/app/config.py(+2 键) | BFF 最小改动 | | docs/team/report_bioflow_dev2.md | 本报告 | ## 6. R1 缺陷修复记录(T2 轮次测试,2026-09-06) ### BUG-R1-01(阻断)BFF 反代大响应体 502 —— 已修复 - **根因**:`sdk/app/routers/bioflow_proxy.py` `_proxy()` 中 `upstream.aread()` 在 `async with httpx.AsyncClient` 上下文退出后才调用;生产事件循环(uvloop)下 连接已随 client 关闭,大响应体(全量 catalog 55,905B)首读未缓冲 → httpx.ReadError → 502;小响应体(health 80B 等)因首读已缓冲幸存。 - **修法**:client 生命周期手工管理(非 async with)——非 SSE 分支在 client 存活期内 完成 `aread()`,随后 `upstream.aclose()` + `client.aclose()`;SSE 分支在 stream() 的 finally 中一并关闭两对象(client 活到流消费完毕)。send 失败/aread 失败均 aclose 后返回 503/502 信封。 - **验证**:修复前 uvloop_probe 复现 5/5 = 502(含 AREAD EXC 堆栈);修复后同一 脚本 catalog 5/5 = 200(bodylen=55905)、health 200,无异常。 ### BUG-R1-03(严重)flow_store 未接线 —— 已修复 - **根因**:`init_flow_store` 已实现但 `init_context()` 调 `build_context` 时不传 flow_store(恒 None)→ 部署态 ask/chat_qa 引用块恒空。L2 测试注入 FakeFlowStore 掩盖了生产链路缺口。 - **修法**:`init_context()` 接线 `flow_store = init_flow_store(get_settings() .flow_docs_dir_abs)` 并传入 `build_context(flow_store=...)`;缺失/失败照旧降级 None 不阻断启动。 - **新增测试**:`tests/test_context_wiring.py` - `test_init_context_wires_flow_store`(临时 flow_docs + 1024 维假嵌入 → 接线后 context.flow_store 非 None 且真实 FlowStore.search 检索非空) - `test_flow_ask_returns_citations_after_wiring`(接线后 ask 端点返回 citations 非空且 degraded=false) - `test_init_flow_store_missing_dir_degrades`(缺失目录降级不阻断) - **E2E 验证**:真实生产 flow_docs(724 块)经 init_flow_store 加载成功,检索 "identify 需要什么输入" 4 hits(stage_contract/skill 引用块正常)。 ### BUG-R1-04(一般)events snapshot 脏结构 —— 已修复 - **根因**:`snapshot()` 对 job_created 载荷 stages 元素直接 `str(s)`;runner 实际 发布 `{stage, status}` dict → 快照首帧阶段名变成 dict repr 串(前端进度卡乱串), 直到后续回放事件覆盖。单测用字符串列表 stages 未触发。 - **修法**:新增 `_stage_key()`/`_stage_entry()`——dict 元素保真透传为干净 `{stage, status}`(取 stage 字段为键),字符串元素照旧兼容;空键跳过。 - **新增测试**:`tests/test_events.py::test_snapshot_dict_stages_clean`(dict stages 发布 → 快照逐阶段 {stage, status} 干净结构,阶段名非 dict repr 串)。 ### 自测结果(修复后) | 命令 | 结果 | |---|---| | `python -m pytest tests/ -q` | **170 passed**(15.8s,0 failed) | | `-m bioflow_unit` | 131 passed | | `-m bioflow_api` | 39 passed | 修复前基线 166 passed;新增 4 条回归用例(wiring 3 + dict-stages 1)后 170 全绿。 ## 7. P7 缺陷修复记录(终验,2026-09-06) ### BUG-P7-01(严重)上传文件后任务不自动续跑 —— 已修复 - **根因**:任务首阶段缺输入停在 await_files 时 `run_job` 已 "await" 返回 (job=await_inputs);上传落盘后无任何重推机制——confirm 端点对 await_files 状态抛 409 STAGE_NOT_CONFIRMABLE。文件引导(FR-BIO-005/006)端到端断裂。 - **修法**(TL 指定,按此实现): 1. `runner.py` 新增 `retry_after_files(record)`:仅当 record.status == JOB_AWAIT_INPUTS 时生效(其余状态 no-op 防重复 spawn)——置 JOB_RUNNING、 pause_requested=False、persist_job、spawn(_resume_loop)(_resume_loop 跳过 done 阶段;await_files 阶段经 _run_stage 重新输入检查:文件齐备则 auto_continue 直跑或发 stage_await_confirm)。 2. `biojobs.py` 的 upload_file 与 _register_path 两个成功分支末尾调用 `await ctx.runner.retry_after_files(record)`。 - **新增测试**(4 条): - `test_runner.py::test_retry_after_files_auto_continue_runs_to_done`(① auto_continue 上传后自动续跑到 done,fake executor 记录执行调用) - `test_runner.py::test_retry_after_files_manual_mode_goes_confirm`(②手动 模式上传后进入 waiting_confirm 并收到 stage_await_confirm 事件) - `test_runner.py::test_retry_after_files_non_await_state_is_noop`(③非 await_inputs 状态调用为 no-op:不重复执行、不新增任务) - `test_api.py::test_upload_resumes_await_inputs_job`(端点全链路复现路径: await_inputs → 上传 → 自动续跑 done;api_context 注入 check_inputs runner) ### 自测结果(修复后) | 命令 | 结果 | |---|---| | `python -m pytest tests/ -q` | **174 passed**(16.3s,0 failed) | | `-m bioflow_unit` | 134 passed | | `-m bioflow_api` | 40 passed | 修复前基线 170 passed;新增 4 条回归用例后 174 全绿。