diff --git a/pipeline_bidding/bid_flow.py b/pipeline_bidding/bid_flow.py index a2c4f77..58c4bd8 100644 --- a/pipeline_bidding/bid_flow.py +++ b/pipeline_bidding/bid_flow.py @@ -176,9 +176,16 @@ async def _open_dim_tasks(sor, project_id, dim): async def _tender_text_len(sor, project_id): - """项目最新招标文件正文字符数(无文件/无正文返回 0)。拆解阈值判定用。""" + """项目最新招标文件正文字符数(无文件/无正文返回 0)。拆解阈值判定用。 + + ⚠️ 必须 CHAR_LENGTH(字符)不能 LENGTH(字节):抽取工具按 Python 字符串 + 索引切片(txt[offset:offset+length]),char_range 区间是字符坐标。甘肃实测 + LENGTH=179031 vs CHAR_LENGTH=67658(CJK utf8mb4 ≈2.65字节/字)——按字节拆 + 3 段时第 2 段仅尾部 8k 字符有效、第 3 段完全越界读空串(2026-09-15 四维度 + 拆解"实测通过"实际带病运行:区间错位浪费 1/3 子任务,2026-09-18 tech 接入时修正)。 + """ recs = await sor.sqlExe( - "SELECT LENGTH(content_text) AS n FROM bid_tender_files " + "SELECT CHAR_LENGTH(content_text) AS n FROM bid_tender_files " "WHERE project_id=${p}$ ORDER BY created_at DESC LIMIT 1", {"p": project_id}) await sor.sqlExe("COMMIT", {}) if not recs: @@ -1537,28 +1544,46 @@ async def _reconcile_tech_analysis_qc(sor, project_id, pname, n_items, latest_qc return False, False, True qc_imp = (latest_qc.get("improvement") or "") if ( latest_qc and str(latest_qc.get("passed")) != "1") else "" - attempts = await _count( - sor, "SELECT COUNT(*) AS c FROM pipeline_tasks WHERE tenant_id=${p}$ " - "AND pipeline_id='role_task' AND role=${r}$ AND state IN " - "('approved','completed','qc_rejected') AND params LIKE ${kw}$", - {"p": project_id, "r": R_ANALYST, "kw": '%bid_tech_analysis%'}) - if attempts >= MAX_ANALYST_ATTEMPT + th["qc_max_round"]: + # 轮次口径(2026-09-18 拆解配套,四维度 A 块同款):一个拆解轮派 N 个分段 + # 子任务(同 round 标签),按任务条数计轮会把一轮误算 N 轮触发上限误判。 + # 旧口径 COUNT(params LIKE '%bid_tech_analysis%') 数条数,拆解后同样虚高。 + n_first = await _dim_round_count(sor, project_id, TECH_KIND, redo=False) + n_redo = await _dim_round_count(sor, project_id, TECH_KIND, redo=True) + _over = (n_redo >= th["qc_max_round"]) if qc_imp else (n_first >= MAX_ANALYST_ATTEMPT) + if _over: ht = await find_human_task(sor, project_id, "general", status="pending") if not ht: await create_human_task( sor, project_id, "general", - "技术评估项解析多次未产出,需人工介入:%s" % pname, - ("技术评估项解析任务已执行 %d 次,产出仍为空。\n" + "技术评估项解析多轮未产出,需人工介入:%s" % pname, + ("技术评估项解析已执行 %d 轮(首跑 %d / QC重做 %d),产出仍为空或未过审。\n" "请检查招标文件技术部分是否可读(bid_tender_files.content_text)," - "或用 list_tech_items/add_tech_item 人工补录评估项。" % attempts), + "或用 list_tech_items/add_tech_item 人工补录评估项。" + % (n_first + n_redo, n_first, n_redo)), assignee_role="owner.superuser") await sor.sqlExe("COMMIT", {}) - acts.append("escalated: 技术评估项解析重试超限 → 人工介入") + acts.append("escalated: 技术评估项解析轮次超限 → 人工介入") return False, False, True tparams = {"stage": "tech_analysis", "task_kind": "bid_tech_analysis", "analysis_dim": TECH_KIND, "flow_key": flow_key} if qc_imp: tparams["qc_redo"] = 1 + # 大文件机械拆解(2026-09-18 用户要求,四维度 A 块同款机制):正文超阈值 + # → 拆 N 段并行子任务,char_range 互不重叠。单体全文抽取实测弊病(甘肃 r1): + # 2.7 小时串行 + agent 自选 offset 步进造成窗口大量重叠,(category,item_name) + # 精确去重对跨窗口近义命名失效 → 733 条注水(QC 6.8 退回,判重复条目/source_ref + # 不可回溯)。不相交区间从源头消除跨窗口重复。 + _tlen = await _tender_text_len(sor, project_id) + if _tlen > th["split_chars"]: + _ids = await _create_split_tasks( + sor, project_id, TECH_KIND, "技术评估项抽取", pname, tparams, _tlen, + th["split_chars"], ("redo%d" % (n_redo + 1)) if qc_imp else "run1", + qc_imp=["[tech_items] %s" % qc_imp[:2000]] if qc_imp else None) + await sor.sqlExe("COMMIT", {}) + acts.append("created: 技术评估项抽取拆解 %d 个分段子任务(正文 %d 字符%s)" + % (len(_ids), _tlen, ",QC退回重做" if qc_imp else "")) + return False, False, True + if qc_imp: tparams["qc_improvements"] = ["[tech_items] %s" % qc_imp[:2000]] tid = await create_role_task( sor, project_id, R_ANALYST,