Merge branch 'eval/elapsed-time-memoir-batch-chunk' into development

This commit is contained in:
Kevin
2026-04-10 10:27:41 +08:00
66 changed files with 5246 additions and 705 deletions

View File

@@ -10,9 +10,10 @@ import json
import re
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from sqlalchemy import select
from sqlalchemy import func, select
from sqlalchemy.orm import Session, joinedload
from app.agents.memoir.narrative_agent import NarrativeAgent
@@ -405,11 +406,13 @@ def _gate_narrative_fidelity(
llm: Any,
*,
existing_canonical: str | None = None,
fidelity_llm: Any | None = None,
) -> tuple[str, str]:
"""返回 (文本, fallback 原因);忠实度不通过时第二项为 fidelity_failed。"""
from app.agents.memoir.fidelity_check_agent import FidelityCheckAgent
if not settings.memoir_fidelity_check_enabled or not llm:
check_llm = fidelity_llm if fidelity_llm is not None else llm
if not settings.memoir_fidelity_check_enabled or not check_llm:
return narrative_raw, "none"
agent = FidelityCheckAgent()
ex = (existing_canonical or "").strip() or None
@@ -417,7 +420,7 @@ def _gate_narrative_fidelity(
if agent.passes(
oral_text=oral_text,
narrative_json=narrative_raw,
llm=llm,
llm=check_llm,
existing_canonical_markdown=ex,
is_append=is_append,
):
@@ -564,14 +567,23 @@ def _merge_fallback_type(gate_ft: str, apply_ft: str) -> str:
def _story_meta_for_route(
session: Session, candidates: list
) -> dict[str, dict[str, int]]:
meta: dict[str, dict[str, int]] = {}
for s in candidates:
sid = str(s.id)
meta[sid] = {
if not candidates:
return {}
sids = [str(s.id) for s in candidates]
stmt = (
select(StoryVersion.story_id, func.count(StoryVersion.id))
.where(StoryVersion.story_id.in_(sids))
.group_by(StoryVersion.story_id)
)
rows = session.execute(stmt).all()
counts: dict[str, int] = {str(r[0]): int(r[1] or 0) for r in rows}
return {
str(s.id): {
"char_count": len((s.canonical_markdown or "").strip()),
"version_count": count_story_versions_sync(session, sid),
"version_count": counts.get(str(s.id), 0),
}
return meta
for s in candidates
}
def _ensure_chapter_record(
@@ -615,7 +627,6 @@ def _ensure_chapter_record(
)
chapter.is_new = True
session.flush()
refresh_chapter_evidence_snapshot_with_retry_sync(session, str(chapter.id))
return chapter
@@ -710,6 +721,7 @@ def _execute_narrative_unit(
background_voice: str = "default",
occupation: str = "",
memoir_correlation_id: str | None = None,
fidelity_llm: Any | None = None,
) -> tuple[str | None, bool]:
"""
Unified narrative unit executor: generate narrative, apply fidelity/safety,
@@ -744,6 +756,7 @@ def _execute_narrative_unit(
raw_gen,
llm,
existing_canonical=existing_for_narrative or None,
fidelity_llm=fidelity_llm,
)
narrative_raw, fb_apply = _apply_narrative_fallbacks(
narrative_raw,
@@ -792,16 +805,7 @@ def _execute_narrative_unit(
sid_log = target_story_id
is_append = True
else:
story_title = _maybe_generate_title(
narrative_agent,
chapter_category=chapter_category,
md=md,
slot_snippets=slot_snippets,
user_profile=user_profile,
user_birth_year=user_birth_year,
llm=llm,
oral_scope=oral_norm,
)
story_title = _placeholder_title(chapter_category)
st = create_story_with_version_sync(
session,
user_id=user_id,
@@ -809,6 +813,21 @@ def _execute_narrative_unit(
canonical_markdown=md,
stage=chapter_category,
)
try:
from app.tasks.story_title_tasks import generate_story_title_after_create
generate_story_title_after_create.delay(
str(st.id),
chapter_category,
oral_norm,
user_id,
)
except Exception as exc:
logger.warning(
"event=story_title_enqueue_failed story_id={} err={}",
st.id,
exc,
)
ensure_chapter_story_link_sync(
session, chapter_id=str(chapter.id), story_id=str(st.id)
)
@@ -874,6 +893,7 @@ def _run_batch_plan_writes(
background_voice: str = "default",
occupation: str = "",
memoir_correlation_id: str | None = None,
fidelity_llm: Any | None = None,
) -> set[str]:
dispatch_ids: set[str] = set()
for unit in plan.units:
@@ -919,6 +939,7 @@ def _run_batch_plan_writes(
background_voice=background_voice,
occupation=occupation,
memoir_correlation_id=memoir_correlation_id,
fidelity_llm=fidelity_llm,
)
if sid:
dispatch_ids.add(sid)
@@ -938,6 +959,7 @@ def run_story_pipeline_for_category_batch(
background_voice: str = "default",
occupation: str = "",
memoir_correlation_id: str | None = None,
llm_fast: Any | None = None,
) -> tuple[Chapter | None, bool, set[str]]:
"""
返回 (chapter, needs_cover_enqueue, story_ids_to_dispatch_after_commit)。
@@ -946,6 +968,8 @@ def run_story_pipeline_for_category_batch(
narrative_agent = NarrativeAgent()
route_agent = StoryRouteAgent()
dispatch_ids: set[str] = set()
llm_route = llm_fast if llm_fast is not None else llm
llm_fidelity = llm_fast if llm_fast is not None else llm
segment_texts = [seg.user_input_text or "" for seg in category_segments]
combined_text = "\n\n".join(segment_texts)
@@ -957,25 +981,40 @@ def run_story_pipeline_for_category_batch(
top_k = int(settings.evidence_top_k_large_batch)
emb = get_embedding_provider()
embedding_available = emb.is_available()
_t0 = time.perf_counter()
try:
evidence = retrieve_evidence_sync(
session,
user_id,
combined_text,
top_k=top_k,
embedding_provider=emb,
)
except Exception as e:
logger.warning("Evidence 检索跳过: {}", e)
evidence = {
"relevant_chunks": [],
"relevant_summaries": [],
"relevant_facts": [],
"timeline_hints": [],
"relevant_stories": [],
}
pipeline_phase_timings["evidence"] = time.perf_counter() - _t0
def _oral_job() -> tuple[str, float]:
t_oral = time.perf_counter()
out = normalize_oral_for_memoir(combined_text, llm=llm)
return out, time.perf_counter() - t_oral
_t_parallel = time.perf_counter()
with ThreadPoolExecutor(max_workers=1) as pool:
oral_future = pool.submit(_oral_job)
_t_ev = time.perf_counter()
try:
evidence = retrieve_evidence_sync(
session,
user_id,
combined_text,
top_k=top_k,
embedding_provider=emb,
)
except Exception as e:
logger.warning("Evidence 检索跳过: {}", e)
evidence = {
"relevant_chunks": [],
"relevant_summaries": [],
"relevant_facts": [],
"timeline_hints": [],
"relevant_stories": [],
}
ev_elapsed = time.perf_counter() - _t_ev
oral_for_memoir, oral_elapsed = oral_future.result()
pipeline_phase_timings["evidence"] = ev_elapsed
pipeline_phase_timings["oral_normalize"] = oral_elapsed
pipeline_phase_timings["evidence_oral_parallel_wall"] = (
time.perf_counter() - _t_parallel
)
logger.info(
"memoir_evidence_retrieved user_id={} chunks={} facts={} summaries={} stories={} vector_ok={}",
@@ -988,9 +1027,6 @@ def run_story_pipeline_for_category_batch(
)
evidence_text = format_evidence_chunks_for_prompt(evidence)
_t0 = time.perf_counter()
oral_for_memoir = normalize_oral_for_memoir(combined_text, llm=llm)
pipeline_phase_timings["oral_normalize"] = time.perf_counter() - _t0
ct_raw = (combined_text or "").strip()
om_norm = (oral_for_memoir or "").strip()
if ct_raw != om_norm:
@@ -999,7 +1035,6 @@ def run_story_pipeline_for_category_batch(
len(ct_raw),
len(om_norm),
)
new_content_input = format_narrative_user_content(oral_for_memoir, evidence_text)
logger.info(
"event=memoir_story_pipeline_start memoir_correlation_id={} user_id={} "
"chapter_category={} segment_count={}",
@@ -1044,7 +1079,7 @@ def run_story_pipeline_for_category_batch(
_t0 = time.perf_counter()
use_batch_plan = (
llm
llm_route
and len(category_segments) >= 2
and len(category_segments) <= PLAN_BATCH_MAX_SEGMENTS
)
@@ -1056,7 +1091,7 @@ def run_story_pipeline_for_category_batch(
chapter_title=title,
segments=segs,
candidate_stories=candidates,
llm=llm,
llm=llm_route,
valid_story_ids=valid_ids,
story_meta=story_meta,
)
@@ -1093,6 +1128,7 @@ def run_story_pipeline_for_category_batch(
background_voice=background_voice,
occupation=occupation,
memoir_correlation_id=memoir_correlation_id,
fidelity_llm=llm_fidelity,
)
else:
route = route_agent.decide(
@@ -1100,7 +1136,7 @@ def run_story_pipeline_for_category_batch(
chapter_title=title,
batch_transcript=route_transcript,
candidate_stories=candidates,
llm=llm,
llm=llm_route,
valid_story_ids=valid_ids,
story_meta=story_meta,
)
@@ -1145,6 +1181,7 @@ def run_story_pipeline_for_category_batch(
background_voice=background_voice,
occupation=occupation,
memoir_correlation_id=memoir_correlation_id,
fidelity_llm=llm_fidelity,
)
if sid:
dispatch_ids.add(sid)