{"record":{"id":"b2ecc82b28f333f2","repo":"datawhalechina/hello-agents","slug":"error-b2ecc8","errorCode":null,"errorMessage":"辩论未正常结束","messagePattern":"辩论未正常结束","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Co-creation-projects/meiguanxiHXX-historyReviewAgent/historical_review/debate_orchestrator.py","lineNumber":285,"sourceCode":"    llm_timeout: int | None = None,\n) -> str:\n    \"\"\"执行两轮角色辩论 + 终局综合报告（无流式，供 CLI 等）。\"\"\"\n    last: dict[str, Any] | None = None\n    for ev in iter_debate_events(\n        topic,\n        llm=llm,\n        use_evidence_bundle=use_evidence_bundle,\n        debate_temperature=debate_temperature,\n        synthesizer_temperature=synthesizer_temperature,\n        llm_api_key=llm_api_key,\n        llm_base_url=llm_base_url,\n        llm_model=llm_model,\n        llm_max_tokens=llm_max_tokens,\n        llm_timeout=llm_timeout,\n    ):\n        last = ev\n    if not last or last.get(\"event\") != \"complete\":\n        raise RuntimeError(\"辩论未正常结束\")\n    md = last.get(\"markdown\")\n    if not isinstance(md, str):\n        raise RuntimeError(\"缺少完整 Markdown\")\n    return md\n\n\ndef debate_event_json(ev: dict[str, Any]) -> str:\n    \"\"\"序列化单条事件（SSE data 行）。\"\"\"\n    return json.dumps(ev, ensure_ascii=False)\n","sourceCodeStart":267,"sourceCodeEnd":295,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/meiguanxiHXX-historyReviewAgent/historical_review/debate_orchestrator.py#L267-L295","documentation":"The collect/run wrapper in debate_orchestrator.py consumes every event yielded by run_historical_debate and afterwards requires the last event to be {\"event\": \"complete\"}; otherwise it raises RuntimeError('辩论未正常结束'). The generator only emits 'complete' after all four phases finish, so this error means the event stream terminated early — most often an LLM call raised mid-debate, or the generator was closed/exhausted by the consumer before completion.","triggerScenarios":"An LLM API error (auth failure, rate limit, timeout) inside round1/digest/round2/synthesis aborts the generator before 'complete'; the caller breaks out of the iteration loop early (e.g. SSE client disconnect triggers GeneratorExit) so `last` is a mid-phase event; create_llM given an invalid model/base_url so the first completion call fails; max_tokens/timeout misconfiguration kills a later phase.","commonSituations":"Invalid or exhausted OpenRouter key used for a long multi-phase debate; network drop mid-run; server-side SSE route that stops consuming on client disconnect and then still tries to build the final markdown; changing llm_model to a name the provider rejects.","solutions":["Inspect the events actually yielded before the raise (log each ev) — the last one names the phase that died (e.g. round2_start), which pinpoints the failing LLM call.","Verify the LLM config first: valid api_key, reachable base_url, correct model id, generous llm_timeout for multi-round debates.","Retry the debate with backoff; transient provider 429/5xx errors commonly end streams early.","If you iterate the generator yourself, do not break early — consume all events, or propagate exceptions from inside the loop.","Raise llm_max_tokens if phases truncate and downstream code mistakes truncation for failure."],"exampleFix":"# before\nfor ev in run_historical_debate(topic, ...):\n    last = ev\nif not last or last.get(\"event\") != \"complete\":\n    raise RuntimeError(\"辩论未正常结束\")\n# after\nevents = []\ntry:\n    for ev in run_historical_debate(topic, ...):\n        events.append(ev)\nexcept Exception as e:\n    raise RuntimeError(f\"辩论在第 {events[-1].get('event') if events else 'start'} 阶段失败: {e}\") from e\nif not events or events[-1].get(\"event\") != \"complete\":\n    raise RuntimeError(f\"辩论未正常结束，最后事件: {events[-1] if events else None}\")","handlingStrategy":"retry","validationCode":"# preflight the LLM config that most often kills mid-stream debates\nimport os\nassert (os.getenv(\"OPENROUTER_API_KEY\") or os.getenv(\"LLM_API_KEY\")), \"LLM key missing\"\nassert llm_model and llm_base_url, \"LLM model/base_url required\"","typeGuard":"def is_complete_event(ev: dict) -> bool:\n    return ev.get(\"event\") == \"complete\"","tryCatchPattern":"import time\nfor attempt in range(3):\n    last = None\n    try:\n        for ev in run_historical_debate(topic, ...):\n            last = ev\n        if last and last.get(\"event\") == \"complete\":\n            break\n        raise RuntimeError(f\"辩论未正常结束，最后事件: {last}\")\n    except RuntimeError:\n        if attempt == 2:\n            raise\n        time.sleep(2 ** attempt)  # transient provider failures are common","preventionTips":["Log every debate event so the failing phase is identifiable","Validate LLM key/model/base_url before starting a multi-phase debate","Never break out of the event loop early; consume the generator fully"],"tags":["orchestrator","generator","llm-failure","python","event-stream"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}