datawhalechina/hello-agents · error · RuntimeError

缺少完整 Markdown

Error message

缺少完整 Markdown

What it means

Immediately after confirming the last debate event is 'complete', the wrapper reads ev["markdown"] and raises RuntimeError('缺少完整 Markdown') when it is not a str. So the debate reported completion but the synthesis payload was malformed — the complete event carried no markdown field (None), a non-string value, or an empty/failed synthesis result was written into the event. It indicates a contract violation inside the orchestrator's final phase rather than a caller input problem.

Source

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

    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. Log the full last event dict — it shows whether markdown is missing, None, or under a different key.
  2. If markdown is empty because synthesis returned no text, raise llm_max_tokens / lower synthesizer_temperature and retry the debate.
  3. Align producer and consumer on the schema: ensure run_historical_debate only emits 'complete' with a non-empty string markdown, and the collector reads the same key.
  4. Retry once — empty completions from providers are often transient.
  5. Pin/patch both modules together (they ship in the same package) so a schema change cannot split them.

Example fix

# before
md = last.get("markdown")
if not isinstance(md, str):
    raise RuntimeError("缺少完整 Markdown")
# after
md = last.get("markdown")
if not isinstance(md, str) or not md.strip():
    raise RuntimeError(f"缺少完整 Markdown,complete 事件负载: { {k: type(v).__name__ for k, v in last.items()} }")
Defensive patterns

Strategy: type-guard

Validate before calling

last = events[-1] if events else None
if not (last and last.get("event") == "complete"):
    raise RuntimeError("辩论未正常结束")
md = last.get("markdown")

Type guard

def has_complete_markdown(ev: dict) -> bool:
    return (
        isinstance(ev, dict)
        and ev.get("event") == "complete"
        and isinstance(ev.get("markdown"), str)
        and bool(ev["markdown"].strip())
    )

Try / catch

try:
    md = collect_debate(...)
except RuntimeError as e:
    if "缺少完整 Markdown" in str(e):
        # synthesis produced empty content — retry with higher max_tokens
        raise
    raise

Prevention

When it happens

Trigger: The synthesis LLM call returned empty content and the complete event was still emitted with markdown=None; a code path constructs {"event": "complete"} without attaching markdown when synthesis produced nothing; version skew where the event schema changed (e.g. key renamed to 'content' or 'result'); JSON round-trip through SSE turned markdown into a non-string.

Common situations: Provider returns an empty completion for the final synthesis (finish_reason=length with zero text); a patched/older debate_orchestrator emits a different field name than the collector expects; content-filtered responses leaving synthesis blank; consuming events via the SSE JSON API where a proxy mangles the payload.

Related errors


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