datawhalechina/hello-agents · error · RuntimeError

辩论未正常结束

Error message

辩论未正常结束

What it means

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.

Source

Thrown at Co-creation-projects/meiguanxiHXX-historyReviewAgent/historical_review/debate_orchestrator.py:285

    llm_timeout: int | None = None,
) -> str:
    """执行两轮角色辩论 + 终局综合报告(无流式,供 CLI 等)。"""
    last: dict[str, Any] | None = None
    for ev in iter_debate_events(
        topic,
        llm=llm,
        use_evidence_bundle=use_evidence_bundle,
        debate_temperature=debate_temperature,
        synthesizer_temperature=synthesizer_temperature,
        llm_api_key=llm_api_key,
        llm_base_url=llm_base_url,
        llm_model=llm_model,
        llm_max_tokens=llm_max_tokens,
        llm_timeout=llm_timeout,
    ):
        last = ev
    if not last or last.get("event") != "complete":
        raise RuntimeError("辩论未正常结束")
    md = last.get("markdown")
    if not isinstance(md, str):
        raise RuntimeError("缺少完整 Markdown")
    return md


def debate_event_json(ev: dict[str, Any]) -> str:
    """序列化单条事件(SSE data 行)。"""
    return json.dumps(ev, ensure_ascii=False)

View on GitHub (pinned to 606a07d341)

Solutions

  1. 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.
  2. Verify the LLM config first: valid api_key, reachable base_url, correct model id, generous llm_timeout for multi-round debates.
  3. Retry the debate with backoff; transient provider 429/5xx errors commonly end streams early.
  4. If you iterate the generator yourself, do not break early — consume all events, or propagate exceptions from inside the loop.
  5. Raise llm_max_tokens if phases truncate and downstream code mistakes truncation for failure.

Example fix

# before
for ev in run_historical_debate(topic, ...):
    last = ev
if not last or last.get("event") != "complete":
    raise RuntimeError("辩论未正常结束")
# after
events = []
try:
    for ev in run_historical_debate(topic, ...):
        events.append(ev)
except Exception as e:
    raise RuntimeError(f"辩论在第 {events[-1].get('event') if events else 'start'} 阶段失败: {e}") from e
if not events or events[-1].get("event") != "complete":
    raise RuntimeError(f"辩论未正常结束,最后事件: {events[-1] if events else None}")
Defensive patterns

Strategy: retry

Validate before calling

# preflight the LLM config that most often kills mid-stream debates
import os
assert (os.getenv("OPENROUTER_API_KEY") or os.getenv("LLM_API_KEY")), "LLM key missing"
assert llm_model and llm_base_url, "LLM model/base_url required"

Type guard

def is_complete_event(ev: dict) -> bool:
    return ev.get("event") == "complete"

Try / catch

import time
for attempt in range(3):
    last = None
    try:
        for ev in run_historical_debate(topic, ...):
            last = ev
        if last and last.get("event") == "complete":
            break
        raise RuntimeError(f"辩论未正常结束,最后事件: {last}")
    except RuntimeError:
        if attempt == 2:
            raise
        time.sleep(2 ** attempt)  # transient provider failures are common

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of datawhalechina/hello-agents@606a07d341 (2026-08-14). Data as JSON: /api/errors/b2ecc82b28f333f2. Report an issue: GitHub.